Logs: Add labels as variable for use in correlations/links (#103605)

pull/103339/head^2
Edvard Falkskär 3 months ago committed by GitHub
parent 08316103b5
commit e45f2d0a18
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 5
      public/app/features/explore/Logs/Logs.tsx
  2. 6
      public/app/features/explore/Logs/LogsContainer.tsx
  3. 5
      public/app/features/logs/components/LogDetails.tsx
  4. 14
      public/app/features/logs/components/LogRow.tsx
  5. 5
      public/app/features/logs/components/LogRowMessageDisplayedFields.tsx
  6. 5
      public/app/features/logs/components/LogRows.tsx
  7. 64
      public/app/features/logs/components/logParser.test.ts
  8. 27
      public/app/features/logs/components/logParser.ts
  9. 6
      public/app/features/logs/components/panel/LogList.tsx
  10. 2
      public/app/features/logs/components/panel/processing.ts
  11. 14
      public/app/plugins/panel/logs/LogsPanel.tsx
  12. 9
      public/app/plugins/panel/logs/types.ts

@ -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<DataQuery | null>;
getLogRowContextUi?: (row: LogRowModel, runContextQuery?: () => void) => React.ReactNode;
getFieldLinks: (field: Field, rowIndex: number, dataFrame: DataFrame) => Array<LinkModel<Field>>;
getFieldLinks: GetFieldLinksFn;
addResultsToCache: () => void;
clearCache: () => void;
eventBus: EventBus;

@ -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<LogsContainerProps, LogsContainerState
return hasLogsContextSupport(this.state.dsInstances[row.dataFrame.refId]);
};
getFieldLinks = (field: Field, rowIndex: number, dataFrame: DataFrame) => {
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 = () => {

@ -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<LinkModel<Field>>;
getFieldLinks?: GetFieldLinksFn;
displayedFields?: string[];
onClickShowField?: (key: string) => void;
onClickHideField?: (key: string) => void;

@ -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<LinkModel<Field>>;
getFieldLinks?: GetFieldLinksFn;
showContextToggle?: (row: LogRowModel) => boolean;
onClickShowField?: (key: string) => void;
onClickHideField?: (key: string) => void;

@ -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<LinkModel<Field>>;
getFieldLinks?: GetFieldLinksFn;
styles: LogRowStyles;
showContextToggle?: (row: LogRowModel) => boolean;
onOpenContext: (row: LogRowModel) => void;

@ -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<LinkModel<Field>>;
getFieldLinks?: GetFieldLinksFn;
onClickShowField?: (key: string) => void;
onClickHideField?: (key: string) => void;
onPinLine?: (row: LogRowModel, allowUnPin?: boolean) => void;

@ -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 = {

@ -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<LinkModel<Field>> | 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<LinkModel<Field>>
): 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);

@ -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<LinkModel<Field>>;
interface Props {
app: CoreApp;
containerElement: HTMLDivElement;

@ -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 {

@ -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]
);

@ -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<LinkModel<Field>>;
export function isOnClickFilterLabel(callback: unknown): callback is onClickFilterLabelType {
return typeof callback === 'function';
}

Loading…
Cancel
Save