mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
35061fc66d
commit
261319a4be
@ -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,
|
||||||
|
@ -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, "\\\\'");
|
||||||
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user