Loki: Add formatting for annotations (#34774)

* add formatting for loki annotations

based/copied from the same features from the prometheus datasource
This commit is contained in:
Fredrik Enestad 2021-05-28 10:12:03 +02:00 committed by GitHub
parent 35061fc66d
commit 261319a4be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 113 additions and 15 deletions

View File

@ -444,8 +444,8 @@ describe('LokiDatasource', () => {
}); });
describe('when calling annotationQuery', () => { describe('when calling annotationQuery', () => {
const getTestContext = (response: any) => { const getTestContext = (response: any, options: any = []) => {
const query = makeAnnotationQueryRequest(); const query = makeAnnotationQueryRequest(options);
fetchMock.mockImplementation(() => of(response)); fetchMock.mockImplementation(() => of(response));
const ds = createLokiDSForTests(); const ds = createLokiDSForTests();
@ -491,6 +491,58 @@ describe('LokiDatasource', () => {
expect(res[1].text).toBe('hello 2'); expect(res[1].text).toBe('hello 2');
expect(res[1].tags).toEqual(['value2']); 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', () => { describe('metricFindQuery', () => {
@ -552,7 +604,7 @@ function createLokiDSForTests(
return new LokiDatasource(customSettings, templateSrvMock, timeSrvStub as any); return new LokiDatasource(customSettings, templateSrvMock, timeSrvStub as any);
} }
function makeAnnotationQueryRequest(): AnnotationQueryRequest<LokiQuery> { function makeAnnotationQueryRequest(options: any): AnnotationQueryRequest<LokiQuery> {
const timeRange = { const timeRange = {
from: dateTime(), from: dateTime(),
to: dateTime(), to: dateTime(),
@ -565,6 +617,7 @@ function makeAnnotationQueryRequest(): AnnotationQueryRequest<LokiQuery> {
enable: true, enable: true,
name: 'test-annotation', name: 'test-annotation',
iconColor: 'red', iconColor: 'red',
...options,
}, },
dashboard: { dashboard: {
id: 1, id: 1,

View File

@ -492,8 +492,8 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
.toPromise(); .toPromise();
} }
async annotationQuery(options: AnnotationQueryRequest<LokiQuery>): Promise<AnnotationEvent[]> { async annotationQuery(options: any): Promise<AnnotationEvent[]> {
const { expr, maxLines, instant } = options.annotation; const { expr, maxLines, instant, tagKeys = '', titleFormat = '', textFormat = '' } = options.annotation;
if (!expr) { if (!expr) {
return []; return [];
@ -506,26 +506,40 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
: await this.runRangeQuery(query, options as any).toPromise(); : await this.runRangeQuery(query, options as any).toPromise();
const annotations: AnnotationEvent[] = []; const annotations: AnnotationEvent[] = [];
const splitKeys: string[] = tagKeys.split(',').filter((v: string) => v !== '');
for (const frame of data) { for (const frame of data) {
const tags: string[] = []; const labels: { [key: string]: string } = {};
for (const field of frame.fields) { for (const field of frame.fields) {
if (field.labels) { if (field.labels) {
tags.push.apply(tags, [ for (const [key, value] of Object.entries(field.labels)) {
labels[key] = String(value).trim();
}
}
}
const tags: string[] = [
...new Set( ...new Set(
Object.values(field.labels) Object.entries(labels).reduce((acc: string[], [key, val]) => {
.map((label: string) => label.trim()) if (val === '') {
.filter((label: string) => label !== '') 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); const view = new DataFrameView<{ ts: string; line: string }>(frame);
view.forEach((row) => { view.forEach((row) => {
annotations.push({ annotations.push({
time: new Date(row.ts).valueOf(), time: new Date(row.ts).valueOf(),
text: row.line, title: renderTemplate(titleFormat, labels),
text: renderTemplate(textFormat, labels) || row.line,
tags, tags,
}); });
}); });
@ -566,6 +580,16 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
} }
} }
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) { export function lokiRegularEscape(value: any) {
if (typeof value === 'string') { if (typeof value === 'string') {
return value.replace(/'/g, "\\\\'"); return value.replace(/'/g, "\\\\'");

View File

@ -4,4 +4,25 @@
instant="ctrl.annotation.instant" instant="ctrl.annotation.instant"
on-change="ctrl.onQueryChange" on-change="ctrl.onQueryChange"
datasource="ctrl.datasource" datasource="ctrl.datasource"
/> >
</loki-annotations-query-editor>
<div class="gf-form-group">
<h5 class="section-heading">Field formats<tip>For title and text fields, use either the name or a pattern. For example, {{instance}} is replaced with label value for the label instance.</tip></h5>
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label width-5">Title</span>
<input type="text" class="gf-form-input max-width-9" ng-model='ctrl.annotation.titleFormat' placeholder="alertname"></input>
</div>
<div class="gf-form">
<span class="gf-form-label width-5">Tags</span>
<input type="text" class="gf-form-input max-width-9" ng-model='ctrl.annotation.tagKeys' placeholder="label1,label2"></input>
</div>
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label width-5">Text</span>
<input type="text" class="gf-form-input max-width-9" ng-model='ctrl.annotation.textFormat' placeholder="instance"></input>
</div>
</div>
</div>
</div>