mirror of https://github.com/grafana/grafana.git
Logs: Add labels as variable for use in correlations/links (#103605)
This commit is contained in:
parent
08316103b5
commit
e45f2d0a18
|
@ -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…
Reference in New Issue