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:
parent
eccc6adfde
commit
0e3e874eee
@ -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,
|
||||
|
@ -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,7 +22,6 @@ describe('LokiDatasource', () => {
|
||||
},
|
||||
};
|
||||
|
||||
describe('when querying', () => {
|
||||
const backendSrvMock = { datasourceRequest: jest.fn() };
|
||||
const backendSrv = (backendSrvMock as unknown) as BackendSrv;
|
||||
|
||||
@ -31,6 +30,7 @@ describe('LokiDatasource', () => {
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user