mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Annotations: Add annotations support to Loki (#18949)
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Just a simple wrapper for a react component that is actually implementing the query editor.
|
||||
*/
|
||||
export class LokiAnnotationsQueryCtrl {
|
||||
static templateUrl = 'partials/annotations.editor.html';
|
||||
annotation: any;
|
||||
|
||||
/** @ngInject */
|
||||
constructor() {
|
||||
this.annotation.target = this.annotation.target || {};
|
||||
this.onQueryChange = this.onQueryChange.bind(this);
|
||||
}
|
||||
|
||||
onQueryChange(expr: string) {
|
||||
this.annotation.expr = expr;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// Libraries
|
||||
import React, { memo } from 'react';
|
||||
|
||||
// Types
|
||||
import { DataSourceApi, DataSourceJsonData, DataSourceStatus } from '@grafana/ui';
|
||||
import { LokiQuery } from '../types';
|
||||
import { useLokiSyntax } from './useLokiSyntax';
|
||||
import { LokiQueryFieldForm } from './LokiQueryFieldForm';
|
||||
|
||||
interface Props {
|
||||
expr: string;
|
||||
datasource: DataSourceApi<LokiQuery, DataSourceJsonData>;
|
||||
onChange: (expr: string) => void;
|
||||
}
|
||||
|
||||
export const LokiAnnotationsQueryEditor = memo(function LokiAnnotationQueryEditor(props: Props) {
|
||||
const { expr, datasource, onChange } = props;
|
||||
|
||||
// Timerange to get existing labels from. Hard coding like this seems to be good enough right now.
|
||||
const absolute = {
|
||||
from: Date.now() - 10000,
|
||||
to: Date.now(),
|
||||
};
|
||||
|
||||
const { isSyntaxReady, setActiveOption, refreshLabels, ...syntaxProps } = useLokiSyntax(
|
||||
datasource.languageProvider,
|
||||
DataSourceStatus.Connected,
|
||||
absolute
|
||||
);
|
||||
|
||||
const query: LokiQuery = {
|
||||
refId: '',
|
||||
expr,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="gf-form-group">
|
||||
<LokiQueryFieldForm
|
||||
datasource={datasource}
|
||||
datasourceStatus={DataSourceStatus.Connected}
|
||||
query={query}
|
||||
onChange={(query: LokiQuery) => onChange(query.expr)}
|
||||
onRunQuery={() => {}}
|
||||
history={[]}
|
||||
panelData={null}
|
||||
onLoadOptions={setActiveOption}
|
||||
onLabelsRefresh={refreshLabels}
|
||||
syntaxLoaded={isSyntaxReady}
|
||||
absoluteRange={absolute}
|
||||
{...syntaxProps}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -1,8 +1,8 @@
|
||||
import LokiDatasource from './datasource';
|
||||
import { LokiQuery } from './types';
|
||||
import { getQueryOptions } from 'test/helpers/getQueryOptions';
|
||||
import { DataSourceApi } from '@grafana/ui';
|
||||
import { DataFrame } from '@grafana/data';
|
||||
import { AnnotationQueryRequest, DataSourceApi } from '@grafana/ui';
|
||||
import { DataFrame, dateTime } from '@grafana/data';
|
||||
import { BackendSrv } from 'app/core/services/backend_srv';
|
||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||
|
||||
@@ -22,15 +22,15 @@ describe('LokiDatasource', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const backendSrvMock = { datasourceRequest: jest.fn() };
|
||||
const backendSrv = (backendSrvMock as unknown) as BackendSrv;
|
||||
|
||||
const templateSrvMock = ({
|
||||
getAdhocFilters: (): any[] => [],
|
||||
replace: (a: string) => a,
|
||||
} as unknown) as TemplateSrv;
|
||||
|
||||
describe('when querying', () => {
|
||||
const backendSrvMock = { datasourceRequest: jest.fn() };
|
||||
const backendSrv = (backendSrvMock as unknown) as BackendSrv;
|
||||
|
||||
const templateSrvMock = ({
|
||||
getAdhocFilters: (): any[] => [],
|
||||
replace: (a: string) => a,
|
||||
} as unknown) as TemplateSrv;
|
||||
|
||||
const testLimit = makeLimitTest(instanceSettings, backendSrvMock, backendSrv, templateSrvMock, testResp);
|
||||
|
||||
test('should use default max lines when no limit given', () => {
|
||||
@@ -171,6 +171,37 @@ describe('LokiDatasource', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('annotationQuery', () => {
|
||||
it('should transform the loki data to annototion response', async () => {
|
||||
const ds = new LokiDatasource(instanceSettings, backendSrv, templateSrvMock);
|
||||
backendSrvMock.datasourceRequest = jest.fn(() =>
|
||||
Promise.resolve({
|
||||
data: {
|
||||
streams: [
|
||||
{
|
||||
entries: [{ ts: '2019-02-01T10:27:37.498180581Z', line: 'hello' }],
|
||||
labels: '{label="value"}',
|
||||
},
|
||||
{
|
||||
entries: [{ ts: '2019-02-01T12:27:37.498180581Z', line: 'hello 2' }],
|
||||
labels: '{label2="value2"}',
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
);
|
||||
const query = makeAnnotationQueryRequest();
|
||||
|
||||
const res = await ds.annotationQuery(query);
|
||||
expect(res.length).toBe(2);
|
||||
expect(res[0].text).toBe('hello');
|
||||
expect(res[0].tags).toEqual(['value']);
|
||||
|
||||
expect(res[1].text).toBe('hello 2');
|
||||
expect(res[1].tags).toEqual(['value2']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
type LimitTestArgs = {
|
||||
@@ -208,3 +239,27 @@ function makeLimitTest(
|
||||
expect(backendSrvMock.datasourceRequest.mock.calls[0][0].url).toContain(`limit=${expectedLimit}`);
|
||||
};
|
||||
}
|
||||
|
||||
function makeAnnotationQueryRequest(): AnnotationQueryRequest<LokiQuery> {
|
||||
const timeRange = {
|
||||
from: dateTime(),
|
||||
to: dateTime(),
|
||||
};
|
||||
return {
|
||||
annotation: {
|
||||
expr: '{test=test}',
|
||||
refId: '',
|
||||
datasource: 'loki',
|
||||
enable: true,
|
||||
name: 'test-annotation',
|
||||
},
|
||||
dashboard: {
|
||||
id: 1,
|
||||
} as any,
|
||||
range: {
|
||||
...timeRange,
|
||||
raw: timeRange,
|
||||
},
|
||||
rangeRaw: timeRange,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
// Libraries
|
||||
import _ from 'lodash';
|
||||
// Services & Utils
|
||||
import { dateMath, DataFrame, LogRowModel, LoadingState, DateTime } from '@grafana/data';
|
||||
import {
|
||||
dateMath,
|
||||
DataFrame,
|
||||
LogRowModel,
|
||||
LoadingState,
|
||||
DateTime,
|
||||
AnnotationEvent,
|
||||
DataFrameView,
|
||||
} from '@grafana/data';
|
||||
import { addLabelToSelector } from 'app/plugins/datasource/prometheus/add_label_to_query';
|
||||
import LanguageProvider from './language_provider';
|
||||
import { logStreamToDataFrame } from './result_transformer';
|
||||
@@ -15,6 +23,7 @@ import {
|
||||
DataQueryRequest,
|
||||
DataStreamObserver,
|
||||
DataQueryResponse,
|
||||
AnnotationQueryRequest,
|
||||
} from '@grafana/ui';
|
||||
|
||||
import { LokiQuery, LokiOptions, LokiLogsStream, LokiResponse } from './types';
|
||||
@@ -193,7 +202,7 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
|
||||
}
|
||||
};
|
||||
|
||||
runQueries = async (options: DataQueryRequest<LokiQuery>) => {
|
||||
runQueries = async (options: DataQueryRequest<LokiQuery>): Promise<{ data: DataFrame[] }> => {
|
||||
const queryTargets = options.targets
|
||||
.filter(target => target.expr && !target.hide && !target.live)
|
||||
.map(target => this.prepareQueryTarget(target, options));
|
||||
@@ -368,6 +377,52 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
|
||||
return { status: 'error', message: message };
|
||||
});
|
||||
}
|
||||
|
||||
async annotationQuery(options: AnnotationQueryRequest<LokiQuery>): Promise<AnnotationEvent[]> {
|
||||
if (!options.annotation.expr) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const query = queryRequestFromAnnotationOptions(options);
|
||||
const { data } = await this.runQueries(query);
|
||||
const annotations: AnnotationEvent[] = [];
|
||||
for (const frame of data) {
|
||||
const tags = Object.values(frame.labels);
|
||||
const view = new DataFrameView<{ ts: string; line: string }>(frame);
|
||||
view.forEachRow(row => {
|
||||
annotations.push({
|
||||
time: new Date(row.ts).valueOf(),
|
||||
text: row.line,
|
||||
tags,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return annotations;
|
||||
}
|
||||
}
|
||||
|
||||
function queryRequestFromAnnotationOptions(options: AnnotationQueryRequest<LokiQuery>): DataQueryRequest<LokiQuery> {
|
||||
const refId = `annotation-${options.annotation.name}`;
|
||||
const target: LokiQuery = { refId, expr: options.annotation.expr };
|
||||
|
||||
return {
|
||||
requestId: refId,
|
||||
range: options.range,
|
||||
targets: [target],
|
||||
dashboardId: options.dashboard.id,
|
||||
scopedVars: null,
|
||||
startTime: Date.now(),
|
||||
|
||||
// This should mean the default defined on datasource is used.
|
||||
maxDataPoints: 0,
|
||||
|
||||
// Dummy values, are required in type but not used here.
|
||||
timezone: 'utc',
|
||||
panelId: 0,
|
||||
interval: '',
|
||||
intervalMs: 0,
|
||||
};
|
||||
}
|
||||
|
||||
export default LokiDatasource;
|
||||
|
||||
@@ -3,6 +3,7 @@ import Datasource from './datasource';
|
||||
import LokiStartPage from './components/LokiStartPage';
|
||||
import LokiQueryField from './components/LokiQueryField';
|
||||
import LokiQueryEditor from './components/LokiQueryEditor';
|
||||
import { LokiAnnotationsQueryCtrl } from './LokiAnnotationsQueryCtrl';
|
||||
|
||||
export class LokiConfigCtrl {
|
||||
static templateUrl = 'partials/config.html';
|
||||
@@ -14,4 +15,5 @@ export {
|
||||
LokiConfigCtrl as ConfigCtrl,
|
||||
LokiQueryField as ExploreQueryField,
|
||||
LokiStartPage as ExploreStartPage,
|
||||
LokiAnnotationsQueryCtrl as AnnotationsQueryCtrl,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<loki-annotations-query-editor
|
||||
expr="ctrl.annotation.expr"
|
||||
on-change="ctrl.onQueryChange"
|
||||
datasource="ctrl.datasource"
|
||||
/>
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
"metrics": true,
|
||||
"alerting": false,
|
||||
"annotations": false,
|
||||
"annotations": true,
|
||||
"logs": true,
|
||||
"streaming": true,
|
||||
|
||||
|
||||
Reference in New Issue
Block a user