From 2669e0a770ccc23e8ff4e31e5e885a1f337fce5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edvard=20Falksk=C3=A4r?= <457523+edvard-falkskar@users.noreply.github.com> Date: Wed, 24 Sep 2025 09:49:18 +0200 Subject: [PATCH] LogsView + TraceView: Add time range to resource attributes extension range (#111171) --- .../src/types/pluginExtensions.ts | 3 +- public/app/features/explore/Logs/Logs.tsx | 2 ++ .../features/explore/Logs/LogsSamplePanel.tsx | 1 + .../explore/TraceView/TraceView.test.tsx | 5 ++-- .../SpanDetail/index.test.tsx | 8 ++++- .../TraceTimelineViewer/SpanDetail/index.tsx | 30 ++++++++++++++----- .../SpanDetailRow.test.tsx | 4 ++- .../logs/components/LogDetails.test.tsx | 13 ++++++++ .../features/logs/components/LogDetails.tsx | 9 ++++-- .../features/logs/components/LogRow.test.tsx | 2 ++ .../app/features/logs/components/LogRow.tsx | 12 +++++++- .../features/logs/components/LogRows.test.tsx | 6 ++++ .../app/features/logs/components/LogRows.tsx | 2 ++ .../log-context/LogRowContextModal.tsx | 6 +++- .../components/panel/LogLineDetails.test.tsx | 14 ++++++++- .../panel/LogLineDetailsComponent.tsx | 2 +- public/app/plugins/panel/logs/LogsPanel.tsx | 2 ++ 17 files changed, 101 insertions(+), 20 deletions(-) diff --git a/packages/grafana-data/src/types/pluginExtensions.ts b/packages/grafana-data/src/types/pluginExtensions.ts index 52fb88e57df..08767dcd549 100644 --- a/packages/grafana-data/src/types/pluginExtensions.ts +++ b/packages/grafana-data/src/types/pluginExtensions.ts @@ -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; spanAttributes?: Record; + timeRange: AbsoluteTimeRange; datasource: { type: string; uid: string; diff --git a/public/app/features/explore/Logs/Logs.tsx b/public/app/features/explore/Logs/Logs.tsx index f2950a31887..fd1d23675c5 100644 --- a/public/app/features/explore/Logs/Logs.tsx +++ b/public/app/features/explore/Logs/Logs.tsx @@ -1056,6 +1056,7 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { logOptionsStorageKey={SETTING_KEY_ROOT} onLogOptionsChange={onLogOptionsChange} filterLevels={filterLevels} + timeRange={props.range} /> )} @@ -1113,6 +1114,7 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { onPinLine={onPinToContentOutlineClick} pinLineButtonTooltipTitle={pinLineButtonTooltipTitle} renderPreview + timeRange={props.range} /> diff --git a/public/app/features/explore/Logs/LogsSamplePanel.tsx b/public/app/features/explore/Logs/LogsSamplePanel.tsx index 175210399c3..b4bf0ab118e 100644 --- a/public/app/features/explore/Logs/LogsSamplePanel.tsx +++ b/public/app/features/explore/Logs/LogsSamplePanel.tsx @@ -138,6 +138,7 @@ export function LogsSamplePanel(props: Props) { timeZone={timeZone} enableLogDetails scrollElement={null} + timeRange={props.timeRange} /> ); } diff --git a/public/app/features/explore/TraceView/TraceView.test.tsx b/public/app/features/explore/TraceView/TraceView.test.tsx index 92ec42b76e2..feed854fce7 100644 --- a/public/app/features/explore/TraceView/TraceView.test.tsx +++ b/public/app/features/explore/TraceView/TraceView.test.tsx @@ -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()} /> ); diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.test.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.test.tsx index dd1a23c12e8..9b382bff768 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.test.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.test.tsx @@ -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('', () => { 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('', () => { attributes: expect.objectContaining({ 'http.url': expect.arrayContaining([expect.any(String)]), }), + timeRange: { + from: 0, + to: 1000000000000, + }, datasource: { type: 'tempo', uid: 'grafanacloud-traces', diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.tsx index 99575f7badc..4550f310064 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.tsx @@ -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>((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 (
diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetailRow.test.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetailRow.test.tsx index f1768425ceb..629e6da37c5 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetailRow.test.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetailRow.test.tsx @@ -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, diff --git a/public/app/features/logs/components/LogDetails.test.tsx b/public/app/features/logs/components/LogDetails.test.tsx index f8e7b6ff3bb..454d80b8a23 100644 --- a/public/app/features/logs/components/LogDetails.test.tsx +++ b/public/app/features/logs/components/LogDetails.test.tsx @@ -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, rowOverrides?: Partial { type: 'loki', uid: 'grafanacloud-logs', }, + timeRange: { + from: 1757937009041, + to: 1757940609041, + }, attributes: { key1: ['label1'], key2: ['label2'] }, }, }); diff --git a/public/app/features/logs/components/LogDetails.tsx b/public/app/features/logs/components/LogDetails.tsx index 99313cf1ae9..83f19909845 100644 --- a/public/app/features/logs/components/LogDetails.tsx +++ b/public/app/features/logs/components/LogDetails.tsx @@ -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; + 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) => { function ComponentWithLinks(props: Props) { - const labelLinks = useAttributesExtensionLinks(props.row); + const labelLinks = useAttributesExtensionLinks(props.row, props.timeRange); return ; } diff --git a/public/app/features/logs/components/LogRow.test.tsx b/public/app/features/logs/components/LogRow.test.tsx index 3760ebf4b1f..f718fcf3a9e 100644 --- a/public/app/features/logs/components/LogRow.test.tsx +++ b/public/app/features/logs/components/LogRow.test.tsx @@ -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>, rowOverri wrapLogMessage: false, timeZone: 'utc', styles, + timeRange: mockTimeRange(), ...(propOverrides || {}), }; diff --git a/public/app/features/logs/components/LogRow.tsx b/public/app/features/logs/components/LogRow.tsx index 5c12fbbd88f..5c6bc170c61 100644 --- a/public/app/features/logs/components/LogRow.tsx +++ b/public/app/features/logs/components/LogRow.tsx @@ -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, 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} /> )} diff --git a/public/app/features/logs/components/LogRows.test.tsx b/public/app/features/logs/components/LogRows.test.tsx index a79ac2787c0..ac38cea7c3f 100644 --- a/public/app/features/logs/components/LogRows.test.tsx +++ b/public/app/features/logs/components/LogRows.test.tsx @@ -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} /> ); diff --git a/public/app/features/logs/components/LogRows.tsx b/public/app/features/logs/components/LogRows.tsx index 85c4af15f7a..3c564f5c538 100644 --- a/public/app/features/logs/components/LogRows.tsx +++ b/public/app/features/logs/components/LogRows.tsx @@ -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; pinnedLogs?: string[]; + timeRange: TimeRange; /** * If false or undefined, the `contain:strict` css property will be added to the wrapping `` for performance reasons. * Any overflowing content will be clipped at the table boundary. diff --git a/public/app/features/logs/components/log-context/LogRowContextModal.tsx b/public/app/features/logs/components/log-context/LogRowContextModal.tsx index c7aaa58a3af..2c712094a6d 100644 --- a/public/app/features/logs/components/log-context/LogRowContextModal.tsx +++ b/public/app/features/logs/components/log-context/LogRowContextModal.tsx @@ -492,6 +492,7 @@ export const LogRowContextModal: React.FunctionComponent @@ -575,6 +577,7 @@ export const LogRowContextModal: React.FunctionComponent @@ -594,6 +597,7 @@ export const LogRowContextModal: React.FunctionComponent @@ -637,7 +641,7 @@ export const LogRowContextModal: React.FunctionComponent { type: 'loki', uid: 'grafanacloud-logs', }, + timeRange: { + from: 1757937009041, + to: 1757940609041, + }, attributes: { key1: ['label1'], key2: ['label2'] }, }, }); diff --git a/public/app/features/logs/components/panel/LogLineDetailsComponent.tsx b/public/app/features/logs/components/panel/LogLineDetailsComponent.tsx index 0f274e4ed6d..240bc546313 100644 --- a/public/app/features/logs/components/panel/LogLineDetailsComponent.tsx +++ b/public/app/features/logs/components/panel/LogLineDetailsComponent.tsx @@ -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); diff --git a/public/app/plugins/panel/logs/LogsPanel.tsx b/public/app/plugins/panel/logs/LogsPanel.tsx index 80bedeef841..ac03ed51932 100644 --- a/public/app/plugins/panel/logs/LogsPanel.tsx +++ b/public/app/plugins/panel/logs/LogsPanel.tsx @@ -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} /> {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()}