Logs: Add new LogRowContext types to @grafana/data (#66404)

* Logs: Add new LogRowContext types to grafana/data

* use right type for `RowContextOptions`

* add missing renames
This commit is contained in:
Sven Grossmann
2023-04-13 13:07:28 +02:00
committed by GitHub
parent 38ee910e39
commit 7bc0692801
15 changed files with 68 additions and 55 deletions

View File

@ -139,6 +139,16 @@ export enum LogsDedupDescription {
signature = 'De-duplication of successive lines that have identical punctuation and whitespace.',
}
export interface LogRowContextOptions {
direction?: LogRowContextQueryDirection;
limit?: number;
}
export enum LogRowContextQueryDirection {
Backward = 'BACKWARD',
Forward = 'FORWARD',
}
/**
* Data sources that allow showing context rows around the provided LowRowModel should implement this method.
* This will enable "context" button in Logs Panel.
@ -147,11 +157,7 @@ export interface DataSourceWithLogsContextSupport<TQuery extends DataQuery = Dat
/**
* Retrieve context for a given log row
*/
getLogRowContext: <TContextQueryOptions extends {}>(
row: LogRowModel,
options?: TContextQueryOptions,
query?: TQuery
) => Promise<DataQueryResponse>;
getLogRowContext: (row: LogRowModel, options?: LogRowContextOptions, query?: TQuery) => Promise<DataQueryResponse>;
/**
* This method can be used to show "context" button based on runtime conditions (for example row model data or plugin settings, etc.)

View File

@ -26,6 +26,7 @@ import {
DataHoverClearEvent,
EventBus,
DataSourceWithLogsContextSupport,
LogRowContextOptions,
} from '@grafana/data';
import { reportInteraction } from '@grafana/runtime';
import { DataQuery } from '@grafana/schema';
@ -44,7 +45,6 @@ import store from 'app/core/store';
import { ExploreId } from 'app/types/explore';
import { LogRows } from '../logs/components/LogRows';
import { RowContextOptions } from '../logs/components/log-context/types';
import { LogsMetaRow } from './LogsMetaRow';
import LogsNavigation from './LogsNavigation';
@ -79,7 +79,7 @@ interface Props extends Themeable2 {
onClickFilterOutLabel: (key: string, value: string) => void;
onStartScanning?: () => void;
onStopScanning?: () => void;
getRowContext?: (row: LogRowModel, options?: RowContextOptions) => Promise<any>;
getRowContext?: (row: LogRowModel, options?: LogRowContextOptions) => Promise<any>;
getLogRowContextUi?: DataSourceWithLogsContextSupport['getLogRowContextUi'];
getFieldLinks: (field: Field, rowIndex: number, dataFrame: DataFrame) => Array<LinkModel<Field>>;
addResultsToCache: () => void;

View File

@ -11,6 +11,7 @@ import {
CoreApp,
DataFrame,
DataSourceWithLogsContextSupport,
LogRowContextOptions,
} from '@grafana/data';
import { reportInteraction } from '@grafana/runtime';
import { TimeZone } from '@grafana/schema';
@ -29,7 +30,6 @@ import {
HasMoreContextRows,
LogRowContextProvider,
} from './log-context/LogRowContextProvider';
import { RowContextOptions } from './log-context/types';
interface Props extends Themeable2 {
row: LogRowModel;
@ -50,7 +50,7 @@ interface Props extends Themeable2 {
onClickFilterLabel?: (key: string, value: string) => void;
onClickFilterOutLabel?: (key: string, value: string) => void;
onContextClick?: () => void;
getRowContext: (row: LogRowModel, options?: RowContextOptions) => Promise<DataQueryResponse>;
getRowContext: (row: LogRowModel, options?: LogRowContextOptions) => Promise<DataQueryResponse>;
getLogRowContextUi?: (row: LogRowModel) => React.ReactNode;
getFieldLinks?: (field: Field, rowIndex: number, dataFrame: DataFrame) => Array<LinkModel<Field>>;
showContextToggle?: (row?: LogRowModel) => boolean;

View File

@ -12,6 +12,7 @@ import {
DataFrame,
DataSourceWithLogsContextSupport,
DataQueryResponse,
LogRowContextOptions,
} from '@grafana/data';
import { withTheme2, Themeable2 } from '@grafana/ui';
@ -20,7 +21,6 @@ import { sortLogRows } from '../utils';
//Components
import { LogRow } from './LogRow';
import { getLogRowStyles } from './getLogRowStyles';
import { RowContextOptions } from './log-context/types';
export const PREVIEW_LIMIT = 100;
@ -43,7 +43,7 @@ export interface Props extends Themeable2 {
showContextToggle?: (row?: LogRowModel) => boolean;
onClickFilterLabel?: (key: string, value: string) => void;
onClickFilterOutLabel?: (key: string, value: string) => void;
getRowContext?: (row: LogRowModel, options?: RowContextOptions) => Promise<DataQueryResponse>;
getRowContext?: (row: LogRowModel, options?: LogRowContextOptions) => Promise<DataQueryResponse>;
getLogRowContextUi?: DataSourceWithLogsContextSupport['getLogRowContextUi'];
getFieldLinks?: (field: Field, rowIndex: number, dataFrame: DataFrame) => Array<LinkModel<Field>>;
onClickShowField?: (key: string) => void;

View File

@ -1,12 +1,11 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import { FieldType, LogRowModel, MutableDataFrame, DataQueryResponse } from '@grafana/data';
import { FieldType, LogRowModel, MutableDataFrame, DataQueryResponse, LogRowContextOptions } from '@grafana/data';
import { createLogRow } from '../__mocks__/logRow';
import { getRowContexts, LogRowContextProvider } from './LogRowContextProvider';
import { RowContextOptions } from './types';
const row = createLogRow({ entry: '4', timeEpochMs: 4 });
@ -35,7 +34,7 @@ describe('getRowContexts', () => {
],
});
let called = false;
const getRowContextMock = (row: LogRowModel, options?: RowContextOptions): Promise<DataQueryResponse> => {
const getRowContextMock = (row: LogRowModel, options?: LogRowContextOptions): Promise<DataQueryResponse> => {
if (!called) {
called = true;
return Promise.resolve({ data: [firstResult] });
@ -70,7 +69,7 @@ describe('getRowContexts', () => {
],
});
let called = false;
const getRowContextMock = (row: LogRowModel, options?: RowContextOptions): Promise<DataQueryResponse> => {
const getRowContextMock = (row: LogRowModel, options?: LogRowContextOptions): Promise<DataQueryResponse> => {
if (!called) {
called = true;
return Promise.resolve({ data: [firstResult] });
@ -95,7 +94,7 @@ describe('getRowContexts', () => {
const firstError = new Error('Error 1');
const secondError = new Error('Error 2');
let called = false;
const getRowContextMock = (row: LogRowModel, options?: RowContextOptions): Promise<DataQueryResponse> => {
const getRowContextMock = (row: LogRowModel, options?: LogRowContextOptions): Promise<DataQueryResponse> => {
if (!called) {
called = true;
return Promise.reject(firstError);
@ -142,7 +141,7 @@ describe('LogRowContextProvider', () => {
});
let called = false;
const getRowContextMock = (row: LogRowModel, options?: RowContextOptions): Promise<DataQueryResponse> => {
const getRowContextMock = (row: LogRowModel, options?: LogRowContextOptions): Promise<DataQueryResponse> => {
if (!called) {
called = true;
return Promise.resolve({ data: [firstResult] });

View File

@ -6,14 +6,14 @@ import {
DataQueryResponse,
Field,
FieldCache,
LogRowContextOptions,
LogRowContextQueryDirection,
LogRowModel,
LogsSortOrder,
toDataFrame,
} from '@grafana/data';
import { reportInteraction } from '@grafana/runtime';
import { RowContextOptions } from './types';
export interface LogRowContextRows {
before?: string[];
after?: string[];
@ -37,7 +37,7 @@ interface ResultType {
interface LogRowContextProviderProps {
row: LogRowModel;
logsSortOrder?: LogsSortOrder | null;
getRowContext: (row: LogRowModel, options?: RowContextOptions) => Promise<DataQueryResponse>;
getRowContext: (row: LogRowModel, options?: LogRowContextOptions) => Promise<DataQueryResponse>;
children: (props: {
result: LogRowContextRows;
errors: LogRowContextQueryErrors;
@ -50,7 +50,7 @@ interface LogRowContextProviderProps {
}
export const getRowContexts = async (
getRowContext: (row: LogRowModel, options?: RowContextOptions) => Promise<DataQueryResponse>,
getRowContext: (row: LogRowModel, options?: LogRowContextOptions) => Promise<DataQueryResponse>,
row: LogRowModel,
limit: number,
logsSortOrder?: LogsSortOrder | null
@ -62,7 +62,7 @@ export const getRowContexts = async (
getRowContext(row, {
// The start time is inclusive so we will get the one row we are using as context entry
limit: limit + 1,
direction: 'FORWARD',
direction: LogRowContextQueryDirection.Forward,
}),
];

View File

@ -1,4 +0,0 @@
export interface RowContextOptions {
direction?: 'BACKWARD' | 'FORWARD';
limit?: number;
}

View File

@ -9,6 +9,7 @@ import {
DataSourceInstanceSettings,
DataSourceWithLogsContextSupport,
LoadingState,
LogRowContextOptions,
LogRowModel,
ScopedVars,
} from '@grafana/data';
@ -16,8 +17,6 @@ import { DataSourceWithBackend } from '@grafana/runtime';
import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_srv';
import { RowContextOptions } from '../../../features/logs/components/log-context/types';
import { CloudWatchAnnotationSupport } from './annotationSupport';
import { DEFAULT_METRICS_QUERY, getDefaultLogsQuery } from './defaultQueries';
import { isCloudWatchAnnotationQuery, isCloudWatchLogsQuery, isCloudWatchMetricsQuery } from './guards';
@ -136,7 +135,7 @@ export class CloudWatchDatasource
getLogRowContext = async (
row: LogRowModel,
context?: RowContextOptions,
context?: LogRowContextOptions,
query?: CloudWatchLogsQuery
): Promise<{ data: DataFrame[] }> => {
return this.logsQueryRunner.getLogRowContext(row, context, query);

View File

@ -8,6 +8,7 @@ import {
MutableDataFrame,
dateTime,
DataQueryRequest,
LogRowContextQueryDirection,
} from '@grafana/data';
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
@ -61,7 +62,11 @@ describe('CloudWatchLogsQueryRunner', () => {
expect(fetchMock.mock.calls[0][0].data.queries[0].endTime).toBe(4);
expect(fetchMock.mock.calls[0][0].data.queries[0].region).toBe(undefined);
await runner.getLogRowContext(row, { direction: 'FORWARD' }, { ...validLogsQuery, region: 'eu-east' });
await runner.getLogRowContext(
row,
{ direction: LogRowContextQueryDirection.Forward },
{ ...validLogsQuery, region: 'eu-east' }
);
expect(fetchMock.mock.calls[1][0].data.queries[0].startTime).toBe(4);
expect(fetchMock.mock.calls[1][0].data.queries[0].region).toBe('eu-east');
});

View File

@ -24,6 +24,8 @@ import {
DataQueryResponse,
DataSourceInstanceSettings,
LoadingState,
LogRowContextOptions,
LogRowContextQueryDirection,
LogRowModel,
rangeUtil,
} from '@grafana/data';
@ -31,7 +33,6 @@ import { BackendDataSourceResponse, config, FetchError, FetchResponse, toDataQue
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { RowContextOptions } from '../../../../features/logs/components/log-context/types';
import {
CloudWatchJsonData,
CloudWatchLogsQuery,
@ -325,7 +326,7 @@ export class CloudWatchLogsQueryRunner extends CloudWatchRequest {
getLogRowContext = async (
row: LogRowModel,
{ limit = 10, direction = 'BACKWARD' }: RowContextOptions = {},
{ limit = 10, direction = LogRowContextQueryDirection.Backward }: LogRowContextOptions = {},
query?: CloudWatchLogsQuery
): Promise<{ data: DataFrame[] }> => {
let logStreamField = null;
@ -347,13 +348,13 @@ export class CloudWatchLogsQueryRunner extends CloudWatchRequest {
const requestParams: GetLogEventsRequest = {
limit,
startFromHead: direction !== 'BACKWARD',
startFromHead: direction !== LogRowContextQueryDirection.Backward,
region: query?.region,
logGroupName: parseLogGroupName(logField!.values.get(row.rowIndex)),
logStreamName: logStreamField!.values.get(row.rowIndex),
};
if (direction === 'BACKWARD') {
if (direction === LogRowContextQueryDirection.Backward) {
requestParams.endTime = row.timeEpochMs;
} else {
requestParams.startTime = row.timeEpochMs;

View File

@ -31,13 +31,14 @@ import {
rangeUtil,
Field,
sortDataFrame,
LogRowContextQueryDirection,
LogRowContextOptions,
} from '@grafana/data';
import { BackendSrvRequest, DataSourceWithBackend, getBackendSrv, getDataSourceSrv, config } from '@grafana/runtime';
import { queryLogsVolume } from 'app/core/logsModel';
import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_srv';
import { RowContextOptions } from '../../../features/logs/components/log-context/types';
import { getLogLevelFromKey } from '../../../features/logs/utils';
import { ElasticResponse } from './ElasticResponse';
@ -500,7 +501,7 @@ export class ElasticDatasource
return true;
}
getLogRowContext = async (row: LogRowModel, options?: RowContextOptions): Promise<{ data: DataFrame[] }> => {
getLogRowContext = async (row: LogRowModel, options?: LogRowContextOptions): Promise<{ data: DataFrame[] }> => {
const { disableElasticsearchBackendQuerying } = config.featureToggles;
if (!disableElasticsearchBackendQuerying) {
const contextRequest = this.makeLogContextDataRequest(row, options);
@ -523,10 +524,10 @@ export class ElasticDatasource
} else {
const sortField = row.dataFrame.fields.find((f) => f.name === 'sort');
const searchAfter = sortField?.values.get(row.rowIndex) || [row.timeEpochMs];
const sort = options?.direction === 'FORWARD' ? 'asc' : 'desc';
const sort = options?.direction === LogRowContextQueryDirection.Forward ? 'asc' : 'desc';
const header =
options?.direction === 'FORWARD'
options?.direction === LogRowContextQueryDirection.Forward
? this.getQueryHeader('query_then_fetch', dateTime(row.timeEpochMs))
: this.getQueryHeader('query_then_fetch', undefined, dateTime(row.timeEpochMs));
@ -539,7 +540,7 @@ export class ElasticDatasource
{
range: {
[this.timeField]: {
[options?.direction === 'FORWARD' ? 'gte' : 'lte']: row.timeEpochMs,
[options?.direction === LogRowContextQueryDirection.Forward ? 'gte' : 'lte']: row.timeEpochMs,
format: 'epoch_millis',
},
},
@ -1109,15 +1110,15 @@ export class ElasticDatasource
return freshDatabaseVersion;
}
private makeLogContextDataRequest = (row: LogRowModel, options?: RowContextOptions) => {
const direction = options?.direction || 'BACKWARD';
private makeLogContextDataRequest = (row: LogRowModel, options?: LogRowContextOptions) => {
const direction = options?.direction || LogRowContextQueryDirection.Backward;
const logQuery: Logs = {
type: 'logs',
id: '1',
settings: {
limit: options?.limit ? options?.limit.toString() : '10',
// Sorting of results in the context query
sortDirection: direction === 'BACKWARD' ? 'desc' : 'asc',
sortDirection: direction === LogRowContextQueryDirection.Backward ? 'desc' : 'asc',
// Used to get the next log lines before/after the current log line using sort field of selected log line
searchAfter: row.dataFrame.fields.find((f) => f.name === 'sort')?.values.get(row.rowIndex) ?? [row.timeEpochMs],
},
@ -1266,7 +1267,7 @@ function createContextTimeRange(rowTimeEpochMs: number, direction: string, inter
// For log context, we want to request data from 7 subsequent/previous indices
if (intervalPattern) {
const intervalInfo = intervalMap[intervalPattern];
if (direction === 'FORWARD') {
if (direction === LogRowContextQueryDirection.Forward) {
return {
from: dateTime(rowTimeEpochMs).utc(),
to: dateTime(rowTimeEpochMs).add(offset, intervalInfo.amount).utc().startOf(intervalInfo.startOf),
@ -1279,7 +1280,7 @@ function createContextTimeRange(rowTimeEpochMs: number, direction: string, inter
}
// If we don't have an interval pattern, we can't do this, so we just request data from 7h before/after
} else {
if (direction === 'FORWARD') {
if (direction === LogRowContextQueryDirection.Forward) {
return {
from: dateTime(rowTimeEpochMs).utc(),
to: dateTime(rowTimeEpochMs).add(offset, 'hours').utc(),

View File

@ -1,4 +1,4 @@
import { FieldType, LogRowModel, MutableDataFrame } from '@grafana/data';
import { FieldType, LogRowContextQueryDirection, LogRowModel, MutableDataFrame } from '@grafana/data';
import LokiLanguageProvider from './LanguageProvider';
import { LogContextProvider } from './LogContextProvider';
@ -47,14 +47,18 @@ describe('new context ui', () => {
describe('prepareLogRowContextQueryTarget', () => {
const lcp = new LogContextProvider(defaultLanguageProviderMock);
it('creates query with only labels from /labels API', async () => {
const contextQuery = await lcp.prepareLogRowContextQueryTarget(defaultLogRow, 10, 'BACKWARD');
const contextQuery = await lcp.prepareLogRowContextQueryTarget(
defaultLogRow,
10,
LogRowContextQueryDirection.Backward
);
expect(contextQuery.query.expr).toContain('uniqueParsedLabel');
expect(contextQuery.query.expr).not.toContain('baz');
});
it('should call languageProvider.start to fetch labels', async () => {
await lcp.prepareLogRowContextQueryTarget(defaultLogRow, 10, 'BACKWARD');
await lcp.prepareLogRowContextQueryTarget(defaultLogRow, 10, LogRowContextQueryDirection.Backward);
expect(lcp.languageProvider.start).toBeCalled();
});
});

View File

@ -1,4 +1,4 @@
import { FieldCache, FieldType, LogRowModel, TimeRange, toUtc } from '@grafana/data';
import { FieldCache, FieldType, LogRowContextQueryDirection, LogRowModel, TimeRange, toUtc } from '@grafana/data';
import { DataQuery } from '@grafana/schema';
import LokiLanguageProvider from './LanguageProvider';
@ -20,14 +20,15 @@ export class LogContextProvider {
async prepareLogRowContextQueryTarget(
row: LogRowModel,
limit: number,
direction: 'BACKWARD' | 'FORWARD',
direction: LogRowContextQueryDirection,
origQuery?: DataQuery
): Promise<{ query: LokiQuery; range: TimeRange }> {
let expr = await this.prepareContextExpr(row, origQuery);
const contextTimeBuffer = 2 * 60 * 60 * 1000; // 2h buffer
const queryDirection = direction === 'FORWARD' ? LokiQueryDirection.Forward : LokiQueryDirection.Backward;
const queryDirection =
direction === LogRowContextQueryDirection.Forward ? LokiQueryDirection.Forward : LokiQueryDirection.Backward;
const query: LokiQuery = {
expr,

View File

@ -31,6 +31,8 @@ import {
rangeUtil,
ScopedVars,
TimeRange,
LogRowContextOptions,
LogRowContextQueryDirection,
} from '@grafana/data';
import { BackendSrvRequest, config, DataSourceWithBackend, FetchError } from '@grafana/runtime';
import { DataQuery } from '@grafana/schema';
@ -40,7 +42,6 @@ import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_srv';
import { serializeParams } from '../../../core/utils/fetch';
import { RowContextOptions } from '../../../features/logs/components/log-context/types';
import { getLogLevelFromKey } from '../../../features/logs/utils';
import { renderLegendFormat } from '../prometheus/legend';
import { replaceVariables, returnVariables } from '../prometheus/querybuilder/shared/parsingUtils';
@ -646,10 +647,10 @@ export class LokiDatasource
getLogRowContext = async (
row: LogRowModel,
options?: RowContextOptions,
options?: LogRowContextOptions,
origQuery?: DataQuery
): Promise<{ data: DataFrame[] }> => {
const direction = (options && options.direction) || 'BACKWARD';
const direction = (options && options.direction) || LogRowContextQueryDirection.Backward;
const limit = (options && options.limit) || 10;
const { query, range } = await this.logContextProvider.prepareLogRowContextQueryTarget(
row,

View File

@ -8,7 +8,7 @@ export interface LokiInstantQueryRequest {
query: string;
limit?: number;
time?: string;
direction?: 'BACKWARD' | 'FORWARD';
direction?: LokiQueryDirection;
}
export interface LokiRangeQueryRequest {
@ -17,7 +17,7 @@ export interface LokiRangeQueryRequest {
start?: number;
end?: number;
step?: number;
direction?: 'BACKWARD' | 'FORWARD';
direction?: LokiQueryDirection;
}
export enum LokiResultType {