From 261319a4be17efa3d8a85959b535a44d0a4d451f Mon Sep 17 00:00:00 2001 From: Fredrik Enestad Date: Fri, 28 May 2021 10:12:03 +0200 Subject: [PATCH] Loki: Add formatting for annotations (#34774) * add formatting for loki annotations based/copied from the same features from the prometheus datasource --- .../datasource/loki/datasource.test.ts | 59 ++++++++++++++++++- .../app/plugins/datasource/loki/datasource.ts | 46 +++++++++++---- .../loki/partials/annotations.editor.html | 23 +++++++- 3 files changed, 113 insertions(+), 15 deletions(-) diff --git a/public/app/plugins/datasource/loki/datasource.test.ts b/public/app/plugins/datasource/loki/datasource.test.ts index 2477d00dcf9..d94e8f02099 100644 --- a/public/app/plugins/datasource/loki/datasource.test.ts +++ b/public/app/plugins/datasource/loki/datasource.test.ts @@ -444,8 +444,8 @@ describe('LokiDatasource', () => { }); describe('when calling annotationQuery', () => { - const getTestContext = (response: any) => { - const query = makeAnnotationQueryRequest(); + const getTestContext = (response: any, options: any = []) => { + const query = makeAnnotationQueryRequest(options); fetchMock.mockImplementation(() => of(response)); const ds = createLokiDSForTests(); @@ -491,6 +491,58 @@ describe('LokiDatasource', () => { expect(res[1].text).toBe('hello 2'); expect(res[1].tags).toEqual(['value2']); }); + describe('Formatting', () => { + const response: FetchResponse = ({ + data: { + data: { + resultType: LokiResultType.Stream, + result: [ + { + stream: { + label: 'value', + label2: 'value2', + label3: 'value3', + }, + values: [['1549016857498000000', 'hello']], + }, + ], + }, + status: 'success', + }, + } as unknown) as FetchResponse; + describe('When tagKeys is set', () => { + it('should only include selected labels', async () => { + const { promise } = getTestContext(response, { tagKeys: 'label2,label3' }); + + const res = await promise; + + expect(res.length).toBe(1); + expect(res[0].text).toBe('hello'); + expect(res[0].tags).toEqual(['value2', 'value3']); + }); + }); + describe('When textFormat is set', () => { + it('should fromat the text accordingly', async () => { + const { promise } = getTestContext(response, { textFormat: 'hello {{label2}}' }); + + const res = await promise; + + expect(res.length).toBe(1); + expect(res[0].text).toBe('hello value2'); + }); + }); + describe('When titleFormat is set', () => { + it('should fromat the title accordingly', async () => { + const { promise } = getTestContext(response, { titleFormat: 'Title {{label2}}' }); + + const res = await promise; + + expect(res.length).toBe(1); + expect(res[0].title).toBe('Title value2'); + expect(res[0].text).toBe('hello'); + }); + }); + }); }); describe('metricFindQuery', () => { @@ -552,7 +604,7 @@ function createLokiDSForTests( return new LokiDatasource(customSettings, templateSrvMock, timeSrvStub as any); } -function makeAnnotationQueryRequest(): AnnotationQueryRequest { +function makeAnnotationQueryRequest(options: any): AnnotationQueryRequest { const timeRange = { from: dateTime(), to: dateTime(), @@ -565,6 +617,7 @@ function makeAnnotationQueryRequest(): AnnotationQueryRequest { enable: true, name: 'test-annotation', iconColor: 'red', + ...options, }, dashboard: { id: 1, diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index 66215501ba1..f3414256756 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -492,8 +492,8 @@ export class LokiDatasource extends DataSourceApi { .toPromise(); } - async annotationQuery(options: AnnotationQueryRequest): Promise { - const { expr, maxLines, instant } = options.annotation; + async annotationQuery(options: any): Promise { + const { expr, maxLines, instant, tagKeys = '', titleFormat = '', textFormat = '' } = options.annotation; if (!expr) { return []; @@ -506,26 +506,40 @@ export class LokiDatasource extends DataSourceApi { : await this.runRangeQuery(query, options as any).toPromise(); const annotations: AnnotationEvent[] = []; + const splitKeys: string[] = tagKeys.split(',').filter((v: string) => v !== ''); for (const frame of data) { - const tags: string[] = []; + const labels: { [key: string]: string } = {}; for (const field of frame.fields) { if (field.labels) { - tags.push.apply(tags, [ - ...new Set( - Object.values(field.labels) - .map((label: string) => label.trim()) - .filter((label: string) => label !== '') - ), - ]); + for (const [key, value] of Object.entries(field.labels)) { + labels[key] = String(value).trim(); + } } } + + const tags: string[] = [ + ...new Set( + Object.entries(labels).reduce((acc: string[], [key, val]) => { + if (val === '') { + return acc; + } + if (splitKeys.length && !splitKeys.includes(key)) { + return acc; + } + acc.push.apply(acc, [val]); + return acc; + }, []) + ), + ]; + const view = new DataFrameView<{ ts: string; line: string }>(frame); view.forEach((row) => { annotations.push({ time: new Date(row.ts).valueOf(), - text: row.line, + title: renderTemplate(titleFormat, labels), + text: renderTemplate(textFormat, labels) || row.line, tags, }); }); @@ -566,6 +580,16 @@ export class LokiDatasource extends DataSourceApi { } } +export function renderTemplate(aliasPattern: string, aliasData: { [key: string]: string }) { + const aliasRegex = /\{\{\s*(.+?)\s*\}\}/g; + return aliasPattern.replace(aliasRegex, (_match, g1) => { + if (aliasData[g1]) { + return aliasData[g1]; + } + return ''; + }); +} + export function lokiRegularEscape(value: any) { if (typeof value === 'string') { return value.replace(/'/g, "\\\\'"); diff --git a/public/app/plugins/datasource/loki/partials/annotations.editor.html b/public/app/plugins/datasource/loki/partials/annotations.editor.html index f482d74abac..c636dd9d3ba 100644 --- a/public/app/plugins/datasource/loki/partials/annotations.editor.html +++ b/public/app/plugins/datasource/loki/partials/annotations.editor.html @@ -4,4 +4,25 @@ instant="ctrl.annotation.instant" on-change="ctrl.onQueryChange" datasource="ctrl.datasource" -/> +> + + +
+
Field formatsFor title and text fields, use either the name or a pattern. For example, {{instance}} is replaced with label value for the label instance.
+
+
+ Title + +
+
+ Tags + +
+
+
+ Text + +
+
+
+