LogsView + TraceView: Add time range to resource attributes extension range (#111171)

This commit is contained in:
Edvard Falkskär 2025-09-24 09:49:18 +02:00 committed by GitHub
parent 3cc2fb3728
commit 2669e0a770
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 101 additions and 20 deletions

View File

@ -6,7 +6,7 @@ import { ScopedVars } from './ScopedVars';
import { DataSourcePluginMeta, DataSourceSettings } from './datasource';
import { IconName } from './icon';
import { PanelData } from './panel';
import { RawTimeRange, TimeZone } from './time';
import { AbsoluteTimeRange, RawTimeRange, TimeZone } from './time';
// Plugin Extensions types
// ---------------------------------------
@ -273,6 +273,7 @@ export type PluginExtensionResourceAttributesContext = {
// Key-value pairs of resource attributes, attribute name is the key
attributes: Record<string, string[]>;
spanAttributes?: Record<string, string[]>;
timeRange: AbsoluteTimeRange;
datasource: {
type: string;
uid: string;

View File

@ -1056,6 +1056,7 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
logOptionsStorageKey={SETTING_KEY_ROOT}
onLogOptionsChange={onLogOptionsChange}
filterLevels={filterLevels}
timeRange={props.range}
/>
</div>
)}
@ -1113,6 +1114,7 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
onPinLine={onPinToContentOutlineClick}
pinLineButtonTooltipTitle={pinLineButtonTooltipTitle}
renderPreview
timeRange={props.range}
/>
</InfiniteScroll>
</div>

View File

@ -138,6 +138,7 @@ export function LogsSamplePanel(props: Props) {
timeZone={timeZone}
enableLogDetails
scrollElement={null}
timeRange={props.timeRange}
/>
);
}

View File

@ -3,7 +3,8 @@ import userEvent from '@testing-library/user-event';
import { createRef } from 'react';
import { Provider } from 'react-redux';
import { DataFrame, MutableDataFrame, TimeRange } from '@grafana/data';
import { DataFrame, MutableDataFrame } from '@grafana/data';
import { mockTimeRange } from '@grafana/plugin-ui';
import { DataSourceSrv, setDataSourceSrv, setPluginLinksHook, setPluginComponentsHook } from '@grafana/runtime';
import { configureStore } from '../../../store/configureStore';
@ -24,7 +25,7 @@ function getTraceView(frames: DataFrame[]) {
traceProp={transformDataFrames(frames[0])!}
datasource={undefined}
topOfViewRef={topOfViewRef}
timeRange={{} as TimeRange}
timeRange={mockTimeRange()}
/>
</Provider>
);

View File

@ -17,7 +17,7 @@ jest.mock('../utils');
import { act, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { createDataFrame, DataSourceInstanceSettings } from '@grafana/data';
import { createDataFrame, DataSourceInstanceSettings, dateTime } from '@grafana/data';
import { data } from '@grafana/flamegraph';
import { DataSourceSrv, setDataSourceSrv, setPluginLinksHook } from '@grafana/runtime';
@ -71,6 +71,8 @@ describe('<SpanDetail>', () => {
traceFlameGraphs: { [span.spanID]: createDataFrame(data) },
setRedrawListView: jest.fn(),
timeRange: {
from: dateTime(0),
to: dateTime(1000000000000),
raw: {
from: 0,
to: 1000000000000,
@ -273,6 +275,10 @@ describe('<SpanDetail>', () => {
attributes: expect.objectContaining({
'http.url': expect.arrayContaining([expect.any(String)]),
}),
timeRange: {
from: 0,
to: 1000000000000,
},
datasource: {
type: 'tempo',
uid: 'grafanacloud-traces',

View File

@ -51,12 +51,19 @@ import { ShareSpanButton } from './ShareSpanButton';
import { getSpanDetailLinkButtons } from './SpanDetailLinkButtons';
import SpanFlameGraph from './SpanFlameGraph';
const useResourceAttributesExtensionLinks = (
process: TraceProcess,
spanTags: TraceKeyValuePair[],
datasourceType: string,
datasourceUid: string
) => {
const useResourceAttributesExtensionLinks = ({
process,
spanTags,
datasourceType,
datasourceUid,
timeRange,
}: {
process: TraceProcess;
spanTags: TraceKeyValuePair[];
datasourceType: string;
datasourceUid: string;
timeRange: TimeRange;
}) => {
// Stable context for useMemo inside usePluginLinks
const context: PluginExtensionResourceAttributesContext = useMemo(() => {
const attributes = (process.tags ?? []).reduce<Record<string, string[]>>((acc, tag) => {
@ -80,12 +87,13 @@ const useResourceAttributesExtensionLinks = (
return {
attributes,
spanAttributes,
timeRange: { from: timeRange.from.valueOf(), to: timeRange.to.valueOf() },
datasource: {
type: datasourceType,
uid: datasourceUid,
},
};
}, [process.tags, spanTags, datasourceType, datasourceUid]);
}, [process.tags, spanTags, datasourceType, datasourceUid, timeRange]);
const { links } = usePluginLinks({
extensionPointId: PluginExtensionPoints.TraceViewResourceAttributes,
@ -343,7 +351,13 @@ export default function SpanDetail(props: SpanDetailProps) {
});
const focusSpanLink = createFocusSpanLink(traceID, spanID);
const resourceLinksGetter = useResourceAttributesExtensionLinks(process, tags, datasourceType, datasourceUid);
const resourceLinksGetter = useResourceAttributesExtensionLinks({
process,
spanTags: tags,
datasourceType,
datasourceUid,
timeRange,
});
return (
<div data-testid="span-detail-component">

View File

@ -14,7 +14,7 @@
import { render, screen } from '@testing-library/react';
import { createTheme } from '@grafana/data';
import { createTheme, dateTime } from '@grafana/data';
import { setPluginLinksHook } from '@grafana/runtime';
import DetailState from './SpanDetail/DetailState';
@ -48,6 +48,8 @@ const setup = (propOverrides?: SpanDetailRowProps) => {
theme: createTheme(),
traceFlameGraphs: {},
timeRange: {
from: dateTime(0),
to: dateTime(1000000000000),
raw: {
from: 0,
to: 1000000000000,

View File

@ -12,6 +12,7 @@ import {
DataFrameType,
CoreApp,
PluginExtensionPoints,
dateTime,
} from '@grafana/data';
import { setPluginLinksHook } from '@grafana/runtime';
@ -43,6 +44,14 @@ const setup = (propOverrides?: Partial<Props>, rowOverrides?: Partial<LogRowMode
theme,
styles,
app: CoreApp.Explore,
timeRange: {
from: dateTime(1757937009041),
to: dateTime(1757940609041),
raw: {
from: 'now-1h',
to: 'now',
},
},
...(propOverrides || {}),
};
@ -325,6 +334,10 @@ describe('LogDetails', () => {
type: 'loki',
uid: 'grafanacloud-logs',
},
timeRange: {
from: 1757937009041,
to: 1757940609041,
},
attributes: { key1: ['label1'], key2: ['label2'] },
},
});

View File

@ -2,6 +2,7 @@ import { cx } from '@emotion/css';
import { PureComponent, useMemo } from 'react';
import {
TimeRange,
CoreApp,
DataFrame,
DataFrameType,
@ -44,23 +45,25 @@ export interface Props extends Themeable2 {
onPinLine?: (row: LogRowModel) => void;
pinLineButtonTooltipTitle?: PopoverContent;
links?: Record<string, LinkModel[]>;
timeRange: TimeRange;
}
interface LinkModelWithIcon extends LinkModel {
icon?: IconName;
}
export const useAttributesExtensionLinks = (row: LogRowModel) => {
export const useAttributesExtensionLinks = (row: LogRowModel, timeRange: TimeRange) => {
// Stable context for useMemo inside usePluginLinks
const context: PluginExtensionResourceAttributesContext = useMemo(() => {
return {
attributes: Object.fromEntries(Object.entries(row.labels).map(([key, value]) => [key, [value]])),
timeRange: { from: timeRange.from.valueOf(), to: timeRange.to.valueOf() },
datasource: {
type: row.datasourceType ?? '',
uid: row.datasourceUid ?? '',
},
};
}, [row.labels, row.datasourceType, row.datasourceUid]);
}, [row.labels, row.datasourceType, row.datasourceUid, timeRange]);
const { links } = usePluginLinks({
extensionPointId: PluginExtensionPoints.LogsViewResourceAttributes,
@ -93,7 +96,7 @@ export const useAttributesExtensionLinks = (row: LogRowModel) => {
const withAttributesExtensionLinks = (Component: React.ComponentType<Props>) => {
function ComponentWithLinks(props: Props) {
const labelLinks = useAttributesExtensionLinks(props.row);
const labelLinks = useAttributesExtensionLinks(props.row, props.timeRange);
return <Component {...props} links={labelLinks} />;
}

View File

@ -4,6 +4,7 @@ import { ComponentProps } from 'react';
import tinycolor from 'tinycolor2';
import { CoreApp, createTheme, LogLevel, LogRowModel } from '@grafana/data';
import { mockTimeRange } from '@grafana/plugin-ui';
import { LogRow } from './LogRow';
import { getLogRowStyles } from './getLogRowStyles';
@ -40,6 +41,7 @@ const setup = (propOverrides?: Partial<ComponentProps<typeof LogRow>>, rowOverri
wrapLogMessage: false,
timeZone: 'utc',
styles,
timeRange: mockTimeRange(),
...(propOverrides || {}),
};

View File

@ -1,7 +1,15 @@
import { debounce } from 'lodash';
import { MouseEvent, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { CoreApp, DataFrame, dateTimeFormat, LogRowContextOptions, LogRowModel, LogsSortOrder } from '@grafana/data';
import {
CoreApp,
DataFrame,
dateTimeFormat,
LogRowContextOptions,
LogRowModel,
LogsSortOrder,
TimeRange,
} from '@grafana/data';
import { t } from '@grafana/i18n';
import { reportInteraction } from '@grafana/runtime';
import { DataQuery, TimeZone } from '@grafana/schema';
@ -56,6 +64,7 @@ export interface Props {
handleTextSelection?: (e: MouseEvent<HTMLTableRowElement>, row: LogRowModel) => boolean;
logRowMenuIconsBefore?: ReactNode[];
logRowMenuIconsAfter?: ReactNode[];
timeRange: TimeRange;
}
export const LogRow = ({
@ -314,6 +323,7 @@ export const LogRow = ({
styles={styles}
isFilterLabelActive={props.isFilterLabelActive}
pinLineButtonTooltipTitle={props.pinLineButtonTooltipTitle}
timeRange={props.timeRange}
/>
)}
</>

View File

@ -2,6 +2,7 @@ import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { LogRowModel, LogsDedupStrategy, LogsSortOrder } from '@grafana/data';
import { mockTimeRange } from '@grafana/plugin-ui';
import { disablePopoverMenu, enablePopoverMenu, isPopoverMenuDisabled } from '../utils';
@ -46,6 +47,7 @@ describe('LogRows', () => {
onClickHideField={() => {}}
onClickShowField={() => {}}
scrollElement={null}
timeRange={mockTimeRange()}
/>
);
@ -75,6 +77,7 @@ describe('LogRows', () => {
onClickHideField={() => {}}
onClickShowField={() => {}}
scrollElement={null}
timeRange={mockTimeRange()}
/>
);
expect(screen.queryAllByRole('row')).toHaveLength(2);
@ -105,6 +108,7 @@ describe('LogRows', () => {
onClickHideField={() => {}}
onClickShowField={() => {}}
scrollElement={null}
timeRange={mockTimeRange()}
/>
);
@ -135,6 +139,7 @@ describe('LogRows', () => {
onClickHideField={() => {}}
onClickShowField={() => {}}
scrollElement={null}
timeRange={mockTimeRange()}
/>
);
@ -162,6 +167,7 @@ describe('Popover menu', () => {
onClickFilterOutString={() => {}}
onClickFilterString={() => {}}
scrollElement={null}
timeRange={mockTimeRange()}
{...overrides}
/>
);

View File

@ -9,6 +9,7 @@ import {
CoreApp,
DataFrame,
LogRowContextOptions,
TimeRange,
} from '@grafana/data';
import { Trans, t } from '@grafana/i18n';
import { config } from '@grafana/runtime';
@ -61,6 +62,7 @@ export interface Props {
scrollIntoView?: (element: HTMLElement) => void;
isFilterLabelActive?: (key: string, value: string, refId?: string) => Promise<boolean>;
pinnedLogs?: string[];
timeRange: TimeRange;
/**
* If false or undefined, the `contain:strict` css property will be added to the wrapping `<table>` for performance reasons.
* Any overflowing content will be clipped at the table boundary.

View File

@ -492,6 +492,7 @@ export const LogRowContextModal: React.FunctionComponent<LogRowContextModalProps
const loadingStateAbove = context.above.loadingState;
const loadingStateBelow = context.below.loadingState;
const timeRange = getFullTimeRange();
return (
<Modal
@ -552,6 +553,7 @@ export const LogRowContextModal: React.FunctionComponent<LogRowContextModalProps
onClickShowField={showField}
onClickHideField={hideField}
scrollElement={null}
timeRange={timeRange}
/>
</td>
</tr>
@ -575,6 +577,7 @@ export const LogRowContextModal: React.FunctionComponent<LogRowContextModalProps
pinnedLogs={sticky ? [row.uid] : undefined}
overflowingContent={true}
scrollElement={null}
timeRange={timeRange}
/>
</td>
</tr>
@ -594,6 +597,7 @@ export const LogRowContextModal: React.FunctionComponent<LogRowContextModalProps
onClickShowField={showField}
onClickHideField={hideField}
scrollElement={null}
timeRange={timeRange}
/>
</>
</td>
@ -637,7 +641,7 @@ export const LogRowContextModal: React.FunctionComponent<LogRowContextModalProps
dispatch(
splitOpen({
queries: [contextQuery],
range: getFullTimeRange(),
range: timeRange,
datasourceUid: contextQuery.datasource!.uid!,
panelsState: {
logs: {

View File

@ -14,6 +14,7 @@ import {
LogsSortOrder,
DataFrame,
ScopedVars,
dateTime,
getDefaultTimeRange,
} from '@grafana/data';
import { setPluginLinksHook } from '@grafana/runtime';
@ -64,7 +65,14 @@ const setup = (
containerElement: document.createElement('div'),
focusLogLine: jest.fn(),
logs,
timeRange: getDefaultTimeRange(),
timeRange: {
from: dateTime(1757937009041),
to: dateTime(1757940609041),
raw: {
from: 'now-1h',
to: 'now',
},
},
timeZone: 'browser',
showControls: true,
...(propOverrides || {}),
@ -372,6 +380,10 @@ describe('LogLineDetails', () => {
type: 'loki',
uid: 'grafanacloud-logs',
},
timeRange: {
from: 1757937009041,
to: 1757940609041,
},
attributes: { key1: ['label1'], key2: ['label2'] },
},
});

View File

@ -37,7 +37,7 @@ export const LogLineDetailsComponent = memo(
const inputRef = useRef('');
const styles = useStyles2(getStyles);
const extensionLinks = useAttributesExtensionLinks(log);
const extensionLinks = useAttributesExtensionLinks(log, timeRange);
const fieldsWithLinks = useMemo(() => {
const fieldsWithLinks = log.fields.filter((f) => f.links?.length);

View File

@ -674,6 +674,7 @@ export const LogsPanel = ({
logRowMenuIconsAfter={isReactNodeArray(logRowMenuIconsAfter) ? logRowMenuIconsAfter : undefined}
// Ascending order causes scroll to stick to the bottom, so previewing is futile
renderPreview={isAscending ? false : true}
timeRange={data.timeRange}
/>
</InfiniteScroll>
{showCommonLabels && isAscending && renderCommonLabels()}
@ -727,6 +728,7 @@ export const LogsPanel = ({
logOptionsStorageKey={controlsStorageKey}
// Ascending order causes scroll to stick to the bottom, so previewing is futile
renderPreview={isAscending ? false : true}
timeRange={data.timeRange}
/>
{showCommonLabels && isAscending && renderCommonLabels()}
</div>