From e45f2d0a18306049e997faedb204151bccd67d07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edvard=20Falksk=C3=A4r?= <457523+edvard-falkskar@users.noreply.github.com> Date: Tue, 15 Apr 2025 16:08:29 +0200 Subject: [PATCH] Logs: Add labels as variable for use in correlations/links (#103605) --- public/app/features/explore/Logs/Logs.tsx | 5 +- .../features/explore/Logs/LogsContainer.tsx | 6 +- .../features/logs/components/LogDetails.tsx | 5 +- .../app/features/logs/components/LogRow.tsx | 14 +--- .../LogRowMessageDisplayedFields.tsx | 5 +- .../app/features/logs/components/LogRows.tsx | 5 +- .../logs/components/logParser.test.ts | 64 ++++++++++++++++++- .../app/features/logs/components/logParser.ts | 27 ++++---- .../logs/components/panel/LogList.tsx | 6 +- .../logs/components/panel/processing.ts | 2 +- public/app/plugins/panel/logs/LogsPanel.tsx | 14 ++-- public/app/plugins/panel/logs/types.ts | 9 ++- 12 files changed, 111 insertions(+), 51 deletions(-) diff --git a/public/app/features/explore/Logs/Logs.tsx b/public/app/features/explore/Logs/Logs.tsx index 3c1b6c997b0..eeee267f623 100644 --- a/public/app/features/explore/Logs/Logs.tsx +++ b/public/app/features/explore/Logs/Logs.tsx @@ -16,10 +16,8 @@ import { RawTimeRange, DataQueryResponse, LogRowContextOptions, - LinkModel, EventBus, ExplorePanelsState, - Field, TimeRange, LogsDedupStrategy, LogsSortOrder, @@ -62,6 +60,7 @@ import { LogLevelColor, dedupLogRows, filterLogLevels } from 'app/features/logs/ import { getLogLevelFromKey, getLogLevelInfo } from 'app/features/logs/utils'; import { LokiQueryDirection } from 'app/plugins/datasource/loki/dataquery.gen'; import { isLokiQuery } from 'app/plugins/datasource/loki/queryUtils'; +import { GetFieldLinksFn } from 'app/plugins/panel/logs/types'; import { getState } from 'app/store/store'; import { ExploreItemState, useDispatch } from 'app/types'; @@ -119,7 +118,7 @@ interface Props extends Themeable2 { cacheFilters?: boolean ) => Promise; getLogRowContextUi?: (row: LogRowModel, runContextQuery?: () => void) => React.ReactNode; - getFieldLinks: (field: Field, rowIndex: number, dataFrame: DataFrame) => Array>; + getFieldLinks: GetFieldLinksFn; addResultsToCache: () => void; clearCache: () => void; eventBus: EventBus; diff --git a/public/app/features/explore/Logs/LogsContainer.tsx b/public/app/features/explore/Logs/LogsContainer.tsx index 22daca32de3..3933c4682ed 100644 --- a/public/app/features/explore/Logs/LogsContainer.tsx +++ b/public/app/features/explore/Logs/LogsContainer.tsx @@ -4,7 +4,6 @@ import { connect, ConnectedProps } from 'react-redux'; import { AbsoluteTimeRange, - Field, hasLogsContextSupport, hasLogsContextUiSupport, LoadingState, @@ -27,6 +26,7 @@ import { DataQuery } from '@grafana/schema'; import { Collapse } from '@grafana/ui'; import { t } from 'app/core/internationalization'; import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource'; +import { GetFieldLinksFn } from 'app/plugins/panel/logs/types'; import { StoreState } from 'app/types'; import { ExploreItemState } from 'app/types/explore'; @@ -237,9 +237,9 @@ class LogsContainer extends PureComponent { + getFieldLinks: GetFieldLinksFn = (field, rowIndex, dataFrame, vars) => { const { splitOpenFn, range } = this.props; - return getFieldLinksForExplore({ field, rowIndex, splitOpenFn, range, dataFrame }); + return getFieldLinksForExplore({ field, rowIndex, splitOpenFn, range, dataFrame, vars }); }; logDetailsFilterAvailable = () => { diff --git a/public/app/features/logs/components/LogDetails.tsx b/public/app/features/logs/components/LogDetails.tsx index a2582b184be..972bcbe7167 100644 --- a/public/app/features/logs/components/LogDetails.tsx +++ b/public/app/features/logs/components/LogDetails.tsx @@ -1,9 +1,10 @@ import { cx } from '@emotion/css'; import { PureComponent } from 'react'; -import { CoreApp, DataFrame, DataFrameType, Field, LinkModel, LogRowModel } from '@grafana/data'; +import { CoreApp, DataFrame, DataFrameType, LogRowModel } from '@grafana/data'; import { PopoverContent, Themeable2, withTheme2 } from '@grafana/ui'; import { Trans, t } from 'app/core/internationalization'; +import { GetFieldLinksFn } from 'app/plugins/panel/logs/types'; import { calculateLogsLabelStats, calculateStats } from '../utils'; @@ -24,7 +25,7 @@ export interface Props extends Themeable2 { onClickFilterLabel?: (key: string, value: string, frame?: DataFrame) => void; onClickFilterOutLabel?: (key: string, value: string, frame?: DataFrame) => void; - getFieldLinks?: (field: Field, rowIndex: number, dataFrame: DataFrame) => Array>; + getFieldLinks?: GetFieldLinksFn; displayedFields?: string[]; onClickShowField?: (key: string) => void; onClickHideField?: (key: string) => void; diff --git a/public/app/features/logs/components/LogRow.tsx b/public/app/features/logs/components/LogRow.tsx index 30fc8c773ba..39067f7b3b3 100644 --- a/public/app/features/logs/components/LogRow.tsx +++ b/public/app/features/logs/components/LogRow.tsx @@ -1,20 +1,12 @@ import { debounce } from 'lodash'; import { MouseEvent, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { - CoreApp, - DataFrame, - dateTimeFormat, - Field, - LinkModel, - LogRowContextOptions, - LogRowModel, - LogsSortOrder, -} from '@grafana/data'; +import { CoreApp, DataFrame, dateTimeFormat, LogRowContextOptions, LogRowModel, LogsSortOrder } from '@grafana/data'; import { reportInteraction } from '@grafana/runtime'; import { DataQuery, TimeZone } from '@grafana/schema'; import { Icon, PopoverContent, Tooltip, useTheme2 } from '@grafana/ui'; import { t } from 'app/core/internationalization'; +import { GetFieldLinksFn } from 'app/plugins/panel/logs/types'; import { checkLogsError, checkLogsSampled, escapeUnescapedString } from '../utils'; @@ -41,7 +33,7 @@ export interface Props { onClickFilterLabel?: (key: string, value: string, frame?: DataFrame) => void; onClickFilterOutLabel?: (key: string, value: string, frame?: DataFrame) => void; onContextClick?: () => void; - getFieldLinks?: (field: Field, rowIndex: number, dataFrame: DataFrame) => Array>; + getFieldLinks?: GetFieldLinksFn; showContextToggle?: (row: LogRowModel) => boolean; onClickShowField?: (key: string) => void; onClickHideField?: (key: string) => void; diff --git a/public/app/features/logs/components/LogRowMessageDisplayedFields.tsx b/public/app/features/logs/components/LogRowMessageDisplayedFields.tsx index 3378f8891d7..e544ca2805e 100644 --- a/public/app/features/logs/components/LogRowMessageDisplayedFields.tsx +++ b/public/app/features/logs/components/LogRowMessageDisplayedFields.tsx @@ -1,7 +1,8 @@ import { css } from '@emotion/css'; import { memo, ReactNode, useMemo } from 'react'; -import { LogRowModel, Field, LinkModel, DataFrame } from '@grafana/data'; +import { LogRowModel } from '@grafana/data'; +import { GetFieldLinksFn } from 'app/plugins/panel/logs/types'; import { LOG_LINE_BODY_FIELD_NAME } from './LogDetailsBody'; import { LogRowMenuCell } from './LogRowMenuCell'; @@ -12,7 +13,7 @@ export interface Props { row: LogRowModel; detectedFields: string[]; wrapLogMessage: boolean; - getFieldLinks?: (field: Field, rowIndex: number, dataFrame: DataFrame) => Array>; + getFieldLinks?: GetFieldLinksFn; styles: LogRowStyles; showContextToggle?: (row: LogRowModel) => boolean; onOpenContext: (row: LogRowModel) => void; diff --git a/public/app/features/logs/components/LogRows.tsx b/public/app/features/logs/components/LogRows.tsx index f0e037c5665..37324f76710 100644 --- a/public/app/features/logs/components/LogRows.tsx +++ b/public/app/features/logs/components/LogRows.tsx @@ -5,8 +5,6 @@ import { TimeZone, LogsDedupStrategy, LogRowModel, - Field, - LinkModel, LogsSortOrder, CoreApp, DataFrame, @@ -16,6 +14,7 @@ import { config } from '@grafana/runtime'; import { DataQuery } from '@grafana/schema'; import { ConfirmModal, Icon, PopoverContent, useTheme2 } from '@grafana/ui'; import { t, Trans } from 'app/core/internationalization'; +import { GetFieldLinksFn } from 'app/plugins/panel/logs/types'; import { PopoverMenu } from '../../explore/Logs/PopoverMenu'; import { UniqueKeyMaker } from '../UniqueKeyMaker'; @@ -44,7 +43,7 @@ export interface Props { showContextToggle?: (row: LogRowModel) => boolean; onClickFilterLabel?: (key: string, value: string, frame?: DataFrame) => void; onClickFilterOutLabel?: (key: string, value: string, frame?: DataFrame) => void; - getFieldLinks?: (field: Field, rowIndex: number, dataFrame: DataFrame) => Array>; + getFieldLinks?: GetFieldLinksFn; onClickShowField?: (key: string) => void; onClickHideField?: (key: string) => void; onPinLine?: (row: LogRowModel, allowUnPin?: boolean) => void; diff --git a/public/app/features/logs/components/logParser.test.ts b/public/app/features/logs/components/logParser.test.ts index 5ddba19b5f0..3ab9194d383 100644 --- a/public/app/features/logs/components/logParser.test.ts +++ b/public/app/features/logs/components/logParser.test.ts @@ -1,8 +1,10 @@ import { DataFrameType, Field, FieldType, LogRowModel, MutableDataFrame } from '@grafana/data'; -import { ExploreFieldLinkModel } from 'app/features/explore/utils/links'; +import { mockTimeRange } from '@grafana/plugin-ui'; +import { ExploreFieldLinkModel, getFieldLinksForExplore } from 'app/features/explore/utils/links'; +import { GetFieldLinksFn } from 'app/plugins/panel/logs/types'; import { createLogRow } from './__mocks__/logRow'; -import { getAllFields, createLogLineLinks, FieldDef } from './logParser'; +import { getAllFields, createLogLineLinks, FieldDef, getDataframeFields } from './logParser'; describe('logParser', () => { describe('getAllFields', () => { @@ -462,6 +464,64 @@ describe('logParser', () => { expect(fields.length).toBe(0); }); }); + + describe('getDataframeFields', () => { + it('should add row labels as variables for links', () => { + const row = createLogRow({ + labels: { service_name: 'checkout', service_namespace: 'prod' }, + dataFrame: { + refId: 'A', + fields: [ + testTimeField, + testLineField, + { + name: 'link', + type: FieldType.string, + config: { + links: [ + { + title: 'link1', + url: 'https://service.com/${__labels.tags["service_namespace"]}/${__labels.tags["service_name"]}', + }, + ], + }, + values: ['some'], + }, + ], + length: 1, + }, + }); + + const getFieldLinks: GetFieldLinksFn = (field, rowIndex, dataFrame, vars) => { + return getFieldLinksForExplore({ + field, + rowIndex, + range: mockTimeRange(), + dataFrame: dataFrame, + vars, + }); + }; + + const fields = getDataframeFields(row, getFieldLinks); + expect(fields).toHaveLength(1); + expect(fields[0].links).toHaveLength(1); + expect(fields[0].links).toMatchObject([ + { + href: 'https://service.com/prod/checkout', + variables: [ + { + fieldPath: 'tags["service_namespace"]', + format: undefined, + found: true, + match: '${__labels.tags["service_namespace"]}', + value: 'prod', + variableName: '__labels', + }, + ], + }, + ]); + }); + }); }); const testTimeField = { diff --git a/public/app/features/logs/components/logParser.ts b/public/app/features/logs/components/logParser.ts index b566c42b89d..715361a7df6 100644 --- a/public/app/features/logs/components/logParser.ts +++ b/public/app/features/logs/components/logParser.ts @@ -1,8 +1,9 @@ import { partition } from 'lodash'; -import { DataFrame, Field, FieldWithIndex, LinkModel, LogRowModel } from '@grafana/data'; +import { DataFrame, Field, FieldWithIndex, LinkModel, LogRowModel, ScopedVars } from '@grafana/data'; import { safeStringifyValue } from 'app/core/utils/explore'; import { ExploreFieldLinkModel } from 'app/features/explore/utils/links'; +import { GetFieldLinksFn } from 'app/plugins/panel/logs/types'; import { parseLogsFrame } from '../logsFrame'; @@ -17,14 +18,7 @@ export type FieldDef = { * Returns all fields for log row which consists of fields we parse from the message itself and additional fields * found in the dataframe (they may contain links). */ -export const getAllFields = ( - row: LogRowModel, - getFieldLinks?: ( - field: Field, - rowIndex: number, - dataFrame: DataFrame - ) => Array> | ExploreFieldLinkModel[] -) => { +export const getAllFields = (row: LogRowModel, getFieldLinks?: GetFieldLinksFn) => { return getDataframeFields(row, getFieldLinks); }; @@ -66,13 +60,18 @@ export const createLogLineLinks = (hiddenFieldsWithLinks: FieldDef[]): FieldDef[ /** * creates fields from the dataframe-fields, adding data-links, when field.config.links exists */ -export const getDataframeFields = ( - row: LogRowModel, - getFieldLinks?: (field: Field, rowIndex: number, dataFrame: DataFrame) => Array> -): FieldDef[] => { +export const getDataframeFields = (row: LogRowModel, getFieldLinks?: GetFieldLinksFn): FieldDef[] => { const nonEmptyVisibleFields = getNonEmptyVisibleFields(row); return nonEmptyVisibleFields.map((field) => { - const links = getFieldLinks ? getFieldLinks(field, row.rowIndex, row.dataFrame) : []; + const vars: ScopedVars = { + __labels: { + text: 'Labels', + value: { + tags: { ...row.labels }, + }, + }, + }; + const links = getFieldLinks ? getFieldLinks(field, row.rowIndex, row.dataFrame, vars) : []; const fieldVal = field.values[row.rowIndex]; const outputVal = typeof fieldVal === 'string' || typeof fieldVal === 'number' ? fieldVal.toString() : safeStringifyValue(fieldVal); diff --git a/public/app/features/logs/components/panel/LogList.tsx b/public/app/features/logs/components/panel/LogList.tsx index f74acfdf181..e93fd9ad1f1 100644 --- a/public/app/features/logs/components/panel/LogList.tsx +++ b/public/app/features/logs/components/panel/LogList.tsx @@ -7,11 +7,8 @@ import { VariableSizeList } from 'react-window'; import { AbsoluteTimeRange, CoreApp, - DataFrame, EventBus, EventBusSrv, - Field, - LinkModel, LogLevel, LogRowModel, LogsDedupStrategy, @@ -21,6 +18,7 @@ import { TimeRange, } from '@grafana/data'; import { PopoverContent, useTheme2 } from '@grafana/ui'; +import { GetFieldLinksFn } from 'app/plugins/panel/logs/types'; import { InfiniteScroll } from './InfiniteScroll'; import { getGridTemplateColumns } from './LogLine'; @@ -38,8 +36,6 @@ import { storeLogLineSize, } from './virtualization'; -export type GetFieldLinksFn = (field: Field, rowIndex: number, dataFrame: DataFrame) => Array>; - interface Props { app: CoreApp; containerElement: HTMLDivElement; diff --git a/public/app/features/logs/components/panel/processing.ts b/public/app/features/logs/components/panel/processing.ts index 2f451ff9983..455148fd5fa 100644 --- a/public/app/features/logs/components/panel/processing.ts +++ b/public/app/features/logs/components/panel/processing.ts @@ -1,11 +1,11 @@ import Prism, { Grammar } from 'prismjs'; import { dateTimeFormat, LogLevel, LogRowModel, LogsSortOrder } from '@grafana/data'; +import { GetFieldLinksFn } from 'app/plugins/panel/logs/types'; import { escapeUnescapedString, sortLogRows } from '../../utils'; import { FieldDef, getAllFields } from '../logParser'; -import { GetFieldLinksFn } from './LogList'; import { generateLogGrammar } from './grammar'; export interface LogListModel extends LogRowModel { diff --git a/public/app/plugins/panel/logs/LogsPanel.tsx b/public/app/plugins/panel/logs/LogsPanel.tsx index 145fad771a3..be11edd4318 100644 --- a/public/app/plugins/panel/logs/LogsPanel.tsx +++ b/public/app/plugins/panel/logs/LogsPanel.tsx @@ -13,7 +13,6 @@ import { DataQueryResponse, DataSourceApi, dateTimeForTimeZone, - Field, GrafanaTheme2, hasLogsContextSupport, hasLogsContextUiSupport, @@ -45,6 +44,7 @@ import { LogRows } from '../../../features/logs/components/LogRows'; import { COMMON_LABELS, dataFrameToLogsModel, dedupLogRows } from '../../../features/logs/logsModel'; import { + GetFieldLinksFn, isIsFilterLabelActive, isOnClickFilterLabel, isOnClickFilterOutLabel, @@ -303,9 +303,15 @@ export const LogsPanel = ({ } }, [panelData.request?.app, isAscending, scrollElement, logRows]); - const getFieldLinks = useCallback( - (field: Field, rowIndex: number) => { - return getFieldLinksForExplore({ field, rowIndex, range: panelData.timeRange }); + const getFieldLinks: GetFieldLinksFn = useCallback( + (field, rowIndex, dataFrame, vars) => { + return getFieldLinksForExplore({ + field, + rowIndex, + range: panelData.timeRange, + dataFrame: dataFrame, + vars, + }); }, [panelData] ); diff --git a/public/app/plugins/panel/logs/types.ts b/public/app/plugins/panel/logs/types.ts index fb11663bdbe..758cd49dd88 100644 --- a/public/app/plugins/panel/logs/types.ts +++ b/public/app/plugins/panel/logs/types.ts @@ -1,6 +1,6 @@ import React, { ReactNode } from 'react'; -import { DataFrame } from '@grafana/data'; +import { DataFrame, Field, LinkModel, ScopedVars } from '@grafana/data'; export type { Options } from './panelcfg.gen'; @@ -13,6 +13,13 @@ type isOnClickShowFieldType = (value: string) => void; type isOnClickHideFieldType = (value: string) => void; export type onNewLogsReceivedType = (allLogs: DataFrame[], newLogs: DataFrame[]) => void; +export type GetFieldLinksFn = ( + field: Field, + rowIndex: number, + dataFrame: DataFrame, + vars: ScopedVars +) => Array>; + export function isOnClickFilterLabel(callback: unknown): callback is onClickFilterLabelType { return typeof callback === 'function'; }