Commit 0e3e874e by Andrej Ocenas Committed by GitHub

Annotations: Add annotations support to Loki (#18949)

parent eccc6adf
......@@ -146,7 +146,8 @@ Request object passed to datasource.annotationQuery function:
"datasource": "generic datasource",
"enable": true,
"name": "annotation name"
}
},
"dashboard": DashboardModel
}
```
......
......@@ -68,4 +68,10 @@ export class DataFrameView<T = any> implements Vector<T> {
toJSON(): T[] {
return this.toArray();
}
forEachRow(iterator: (row: T) => void) {
for (let i = 0; i < this.data.length; i++) {
iterator(this.get(i));
}
}
}
......@@ -8,6 +8,7 @@ import {
LogRowModel,
LoadingState,
DataFrameDTO,
AnnotationEvent,
} from '@grafana/data';
import { PluginMeta, GrafanaPlugin } from './plugin';
import { PanelData } from './panel';
......@@ -276,6 +277,12 @@ export abstract class DataSourceApi<
* Used in explore
*/
languageProvider?: any;
/**
* Can be optionally implemented to allow datasource to be a source of annotations for dashboard. To be visible
* in the annotation editor `annotations` capability also needs to be enabled in plugin.json.
*/
annotationQuery?(options: AnnotationQueryRequest<TQuery>): Promise<AnnotationEvent[]>;
}
export interface QueryEditorProps<
......@@ -542,3 +549,18 @@ export interface DataSourceSelectItem {
meta: DataSourcePluginMeta;
sort: string;
}
/**
* Options passed to the datasource.annotationQuery method. See docs/plugins/developing/datasource.md
*/
export interface AnnotationQueryRequest<MoreOptions = {}> {
range: TimeRange;
rangeRaw: RawTimeRange;
// Should be DataModel but cannot import that here from the main app. Needs to be moved to package first.
dashboard: any;
annotation: {
datasource: string;
enable: boolean;
name: string;
} & MoreOptions;
}
......@@ -12,6 +12,7 @@ import { FunctionEditor } from 'app/plugins/datasource/graphite/FunctionEditor';
import { SearchField } from './components/search/SearchField';
import { GraphContextMenu } from 'app/plugins/panel/graph/GraphContextMenu';
import ReactProfileWrapper from 'app/features/profile/ReactProfileWrapper';
import { LokiAnnotationsQueryEditor } from '../plugins/datasource/loki/components/AnnotationsQueryEditor';
export function registerAngularDirectives() {
react2AngularDirective('sidemenu', SideMenu, []);
......@@ -102,4 +103,10 @@ export function registerAngularDirectives() {
]);
react2AngularDirective('reactProfileWrapper', ReactProfileWrapper, []);
react2AngularDirective('lokiAnnotationsQueryEditor', LokiAnnotationsQueryEditor, [
'expr',
'onChange',
['datasource', { watchDepth: 'reference' }],
]);
}
......@@ -15,6 +15,7 @@ import { AnnotationEvent } from '@grafana/data';
import DatasourceSrv from '../plugins/datasource_srv';
import { BackendSrv } from 'app/core/services/backend_srv';
import { TimeSrv } from '../dashboard/services/TimeSrv';
import { DataSourceApi } from '@grafana/ui';
export class AnnotationsSrv {
globalAnnotationsPromise: any;
......@@ -126,7 +127,7 @@ export class AnnotationsSrv {
dsPromises.push(datasourcePromise);
promises.push(
datasourcePromise
.then((datasource: any) => {
.then((datasource: DataSourceApi) => {
// issue query against data source
return datasource.annotationQuery({
range: range,
......
/**
* 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;
}
}
// 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>
);
});
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', () => {
},
};
describe('when querying', () => {
const backendSrvMock = { datasourceRequest: jest.fn() };
const backendSrv = (backendSrvMock as unknown) as BackendSrv;
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 templateSrvMock = ({
getAdhocFilters: (): any[] => [],
replace: (a: string) => a,
} as unknown) as TemplateSrv;
describe('when querying', () => {
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,
};
}
// 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,
};
<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,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment