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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,15 @@
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { MouseEvent, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'; 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 { t } from '@grafana/i18n';
import { reportInteraction } from '@grafana/runtime'; import { reportInteraction } from '@grafana/runtime';
import { DataQuery, TimeZone } from '@grafana/schema'; import { DataQuery, TimeZone } from '@grafana/schema';
@ -56,6 +64,7 @@ export interface Props {
handleTextSelection?: (e: MouseEvent<HTMLTableRowElement>, row: LogRowModel) => boolean; handleTextSelection?: (e: MouseEvent<HTMLTableRowElement>, row: LogRowModel) => boolean;
logRowMenuIconsBefore?: ReactNode[]; logRowMenuIconsBefore?: ReactNode[];
logRowMenuIconsAfter?: ReactNode[]; logRowMenuIconsAfter?: ReactNode[];
timeRange: TimeRange;
} }
export const LogRow = ({ export const LogRow = ({
@ -314,6 +323,7 @@ export const LogRow = ({
styles={styles} styles={styles}
isFilterLabelActive={props.isFilterLabelActive} isFilterLabelActive={props.isFilterLabelActive}
pinLineButtonTooltipTitle={props.pinLineButtonTooltipTitle} 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 userEvent from '@testing-library/user-event';
import { LogRowModel, LogsDedupStrategy, LogsSortOrder } from '@grafana/data'; import { LogRowModel, LogsDedupStrategy, LogsSortOrder } from '@grafana/data';
import { mockTimeRange } from '@grafana/plugin-ui';
import { disablePopoverMenu, enablePopoverMenu, isPopoverMenuDisabled } from '../utils'; import { disablePopoverMenu, enablePopoverMenu, isPopoverMenuDisabled } from '../utils';
@ -46,6 +47,7 @@ describe('LogRows', () => {
onClickHideField={() => {}} onClickHideField={() => {}}
onClickShowField={() => {}} onClickShowField={() => {}}
scrollElement={null} scrollElement={null}
timeRange={mockTimeRange()}
/> />
); );
@ -75,6 +77,7 @@ describe('LogRows', () => {
onClickHideField={() => {}} onClickHideField={() => {}}
onClickShowField={() => {}} onClickShowField={() => {}}
scrollElement={null} scrollElement={null}
timeRange={mockTimeRange()}
/> />
); );
expect(screen.queryAllByRole('row')).toHaveLength(2); expect(screen.queryAllByRole('row')).toHaveLength(2);
@ -105,6 +108,7 @@ describe('LogRows', () => {
onClickHideField={() => {}} onClickHideField={() => {}}
onClickShowField={() => {}} onClickShowField={() => {}}
scrollElement={null} scrollElement={null}
timeRange={mockTimeRange()}
/> />
); );
@ -135,6 +139,7 @@ describe('LogRows', () => {
onClickHideField={() => {}} onClickHideField={() => {}}
onClickShowField={() => {}} onClickShowField={() => {}}
scrollElement={null} scrollElement={null}
timeRange={mockTimeRange()}
/> />
); );
@ -162,6 +167,7 @@ describe('Popover menu', () => {
onClickFilterOutString={() => {}} onClickFilterOutString={() => {}}
onClickFilterString={() => {}} onClickFilterString={() => {}}
scrollElement={null} scrollElement={null}
timeRange={mockTimeRange()}
{...overrides} {...overrides}
/> />
); );

View File

@ -9,6 +9,7 @@ import {
CoreApp, CoreApp,
DataFrame, DataFrame,
LogRowContextOptions, LogRowContextOptions,
TimeRange,
} from '@grafana/data'; } from '@grafana/data';
import { Trans, t } from '@grafana/i18n'; import { Trans, t } from '@grafana/i18n';
import { config } from '@grafana/runtime'; import { config } from '@grafana/runtime';
@ -61,6 +62,7 @@ export interface Props {
scrollIntoView?: (element: HTMLElement) => void; scrollIntoView?: (element: HTMLElement) => void;
isFilterLabelActive?: (key: string, value: string, refId?: string) => Promise<boolean>; isFilterLabelActive?: (key: string, value: string, refId?: string) => Promise<boolean>;
pinnedLogs?: string[]; pinnedLogs?: string[];
timeRange: TimeRange;
/** /**
* If false or undefined, the `contain:strict` css property will be added to the wrapping `<table>` for performance reasons. * 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. * 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 loadingStateAbove = context.above.loadingState;
const loadingStateBelow = context.below.loadingState; const loadingStateBelow = context.below.loadingState;
const timeRange = getFullTimeRange();
return ( return (
<Modal <Modal
@ -552,6 +553,7 @@ export const LogRowContextModal: React.FunctionComponent<LogRowContextModalProps
onClickShowField={showField} onClickShowField={showField}
onClickHideField={hideField} onClickHideField={hideField}
scrollElement={null} scrollElement={null}
timeRange={timeRange}
/> />
</td> </td>
</tr> </tr>
@ -575,6 +577,7 @@ export const LogRowContextModal: React.FunctionComponent<LogRowContextModalProps
pinnedLogs={sticky ? [row.uid] : undefined} pinnedLogs={sticky ? [row.uid] : undefined}
overflowingContent={true} overflowingContent={true}
scrollElement={null} scrollElement={null}
timeRange={timeRange}
/> />
</td> </td>
</tr> </tr>
@ -594,6 +597,7 @@ export const LogRowContextModal: React.FunctionComponent<LogRowContextModalProps
onClickShowField={showField} onClickShowField={showField}
onClickHideField={hideField} onClickHideField={hideField}
scrollElement={null} scrollElement={null}
timeRange={timeRange}
/> />
</> </>
</td> </td>
@ -637,7 +641,7 @@ export const LogRowContextModal: React.FunctionComponent<LogRowContextModalProps
dispatch( dispatch(
splitOpen({ splitOpen({
queries: [contextQuery], queries: [contextQuery],
range: getFullTimeRange(), range: timeRange,
datasourceUid: contextQuery.datasource!.uid!, datasourceUid: contextQuery.datasource!.uid!,
panelsState: { panelsState: {
logs: { logs: {

View File

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

View File

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

View File

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