From 026d13469fa8a9ed3ae24958f20c864b7e9a4ede Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Fri, 8 Nov 2019 01:38:10 -0800 Subject: [PATCH] Graphite: add metrictank meta in response (#20138) * metric tank meta * add metric tank info * fixed info box * process meta * attach metrictank meta to response * remove extra logging * response is now DataFrame * Minor refactoring and renaming of the prop * Graphite: minor fixes --- .../datasource/graphite/config_ctrl.ts | 11 ++++ .../plugins/datasource/graphite/datasource.ts | 58 +++++++++++++------ .../datasource/graphite/partials/config.html | 27 ++++++++- .../graphite/specs/datasource.test.ts | 4 +- .../app/plugins/datasource/graphite/types.ts | 5 ++ 5 files changed, 84 insertions(+), 21 deletions(-) diff --git a/public/app/plugins/datasource/graphite/config_ctrl.ts b/public/app/plugins/datasource/graphite/config_ctrl.ts index 194860656b4..ad112148157 100644 --- a/public/app/plugins/datasource/graphite/config_ctrl.ts +++ b/public/app/plugins/datasource/graphite/config_ctrl.ts @@ -1,16 +1,23 @@ import DatasourceSrv from 'app/features/plugins/datasource_srv'; +import { GraphiteType } from './types'; export class GraphiteConfigCtrl { static templateUrl = 'public/app/plugins/datasource/graphite/partials/config.html'; datasourceSrv: any; current: any; + graphiteTypes: any; /** @ngInject */ constructor($scope: any, datasourceSrv: DatasourceSrv) { this.datasourceSrv = datasourceSrv; this.current.jsonData = this.current.jsonData || {}; this.current.jsonData.graphiteVersion = this.current.jsonData.graphiteVersion || '0.9'; + this.current.jsonData.graphiteType = this.current.jsonData.graphiteType || GraphiteType.Default; this.autoDetectGraphiteVersion(); + this.graphiteTypes = Object.keys(GraphiteType).map((key: string) => ({ + name: key, + value: (GraphiteType as any)[key], + })); } autoDetectGraphiteVersion() { @@ -24,6 +31,10 @@ export class GraphiteConfigCtrl { return ds.getVersion(); }) .then((version: any) => { + if (!version) { + return; + } + this.graphiteVersions.push({ name: version, value: version }); this.current.jsonData.graphiteVersion = version; }); diff --git a/public/app/plugins/datasource/graphite/datasource.ts b/public/app/plugins/datasource/graphite/datasource.ts index 5e74778a912..0cc19318e41 100644 --- a/public/app/plugins/datasource/graphite/datasource.ts +++ b/public/app/plugins/datasource/graphite/datasource.ts @@ -1,12 +1,12 @@ import _ from 'lodash'; -import { dateMath, ScopedVars } from '@grafana/data'; +import { DataFrame, dateMath, ScopedVars, DataQueryResponse, DataQueryRequest, toDataFrame } from '@grafana/data'; import { isVersionGtOrEq, SemVersion } from 'app/core/utils/version'; import gfunc from './gfunc'; import { IQService } from 'angular'; import { BackendSrv } from 'app/core/services/backend_srv'; import { TemplateSrv } from 'app/features/templating/template_srv'; //Types -import { GraphiteQuery } from './types'; +import { GraphiteQuery, GraphiteType } from './types'; import { getSearchFilterScopedVar } from '../../../features/templating/variable'; export class GraphiteDatasource { @@ -15,6 +15,7 @@ export class GraphiteDatasource { name: string; graphiteVersion: any; supportsTags: boolean; + isMetricTank: boolean; cacheTimeout: any; withCredentials: boolean; funcDefs: any = null; @@ -32,12 +33,12 @@ export class GraphiteDatasource { this.url = instanceSettings.url; this.name = instanceSettings.name; this.graphiteVersion = instanceSettings.jsonData.graphiteVersion || '0.9'; + this.isMetricTank = instanceSettings.jsonData.graphiteType === GraphiteType.Metrictank; this.supportsTags = supportsTags(this.graphiteVersion); this.cacheTimeout = instanceSettings.cacheTimeout; this.withCredentials = instanceSettings.withCredentials; this.funcDefs = null; this.funcDefsPromise = null; - this._seriesRefLetters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; } @@ -54,12 +55,12 @@ export class GraphiteDatasource { }; } - query(options: any) { + async query(options: DataQueryRequest): Promise { const graphOptions = { from: this.translateTime(options.rangeRaw.from, false, options.timezone), until: this.translateTime(options.rangeRaw.to, true, options.timezone), targets: options.targets, - format: options.format, + format: (options as any).format, cacheTimeout: options.cacheTimeout || this.cacheTimeout, maxDataPoints: options.maxDataPoints, }; @@ -69,6 +70,10 @@ export class GraphiteDatasource { return this.$q.when({ data: [] }); } + if (this.isMetricTank) { + params.push('meta=true'); + } + const httpOptions: any = { method: 'POST', url: '/render', @@ -84,7 +89,7 @@ export class GraphiteDatasource { httpOptions.requestId = this.name + '.panelId.' + options.panelId; } - return this.doGraphiteRequest(httpOptions).then(this.convertDataPointsToMs); + return this.doGraphiteRequest(httpOptions).then(this.convertResponseToDataFrames); } addTracingHeaders(httpOptions: { headers: any }, options: { dashboardId: any; panelId: any }) { @@ -95,17 +100,34 @@ export class GraphiteDatasource { } } - convertDataPointsToMs(result: any) { + convertResponseToDataFrames(result: any): DataQueryResponse { + const data: DataFrame[] = []; if (!result || !result.data) { - return []; + return { data }; } - for (let i = 0; i < result.data.length; i++) { - const series = result.data[i]; - for (let y = 0; y < series.datapoints.length; y++) { - series.datapoints[y][1] *= 1000; + // Series are either at the root or under a node called 'series' + const series = result.data.series || result.data; + if (!_.isArray(series)) { + throw { message: 'Missing series in result', data: result }; + } + + for (let i = 0; i < series.length; i++) { + const s = series[i]; + for (let y = 0; y < s.datapoints.length; y++) { + s.datapoints[y][1] *= 1000; } + const frame = toDataFrame(s); + + // Metrictank metadata + if (s.meta) { + frame.meta = { + metrictank: s.meta, // array of metadata + metrictankReq: result.data.meta, // info on the request + }; + } + data.push(frame); } - return result; + return { data }; } parseTags(tagString: string) { @@ -139,12 +161,12 @@ export class GraphiteDatasource { // Graphite metric as annotation if (options.annotation.target) { const target = this.templateSrv.replace(options.annotation.target, {}, 'glob'); - const graphiteQuery = { + const graphiteQuery = ({ rangeRaw: options.rangeRaw, targets: [{ target: target }], format: 'json', maxDataPoints: 100, - }; + } as unknown) as DataQueryRequest; return this.query(graphiteQuery).then((result: { data: any[] }) => { const list = []; @@ -513,12 +535,12 @@ export class GraphiteDatasource { } testDatasource() { - const query = { + const query = ({ panelId: 3, rangeRaw: { from: 'now-1h', to: 'now' }, targets: [{ target: 'constantLine(100)' }], maxDataPoints: 300, - }; + } as unknown) as DataQueryRequest; return this.query(query).then(() => { return { status: 'success', message: 'Data source is working' }; }); @@ -546,7 +568,7 @@ export class GraphiteDatasource { return this.backendSrv.datasourceRequest(options); } - buildGraphiteParams(options: any, scopedVars: ScopedVars) { + buildGraphiteParams(options: any, scopedVars: ScopedVars): string[] { const graphiteOptions = ['from', 'until', 'rawData', 'format', 'maxDataPoints', 'cacheTimeout']; const cleanOptions = [], targets: any = {}; diff --git a/public/app/plugins/datasource/graphite/partials/config.html b/public/app/plugins/datasource/graphite/partials/config.html index 2c31aaf86b4..97ac5c61330 100644 --- a/public/app/plugins/datasource/graphite/partials/config.html +++ b/public/app/plugins/datasource/graphite/partials/config.html @@ -16,5 +16,30 @@ - + +
+ Type + + + +
+ +
+
+
+

+ There are different types of Graphite compatible backends. Here you can specify the type you are using. + If you are using Metrictank + then select that here. This will enable Metrictank specific features like query processing meta data. + + Metrictank is a multi-tenant timeseries engine for Graphite and friends. +

+
+
+
diff --git a/public/app/plugins/datasource/graphite/specs/datasource.test.ts b/public/app/plugins/datasource/graphite/specs/datasource.test.ts index 871e07dc20d..5cf3b9598ea 100644 --- a/public/app/plugins/datasource/graphite/specs/datasource.test.ts +++ b/public/app/plugins/datasource/graphite/specs/datasource.test.ts @@ -73,11 +73,11 @@ describe('graphiteDatasource', () => { it('should return series list', () => { expect(results.data.length).toBe(1); - expect(results.data[0].target).toBe('prod1.count'); + expect(results.data[0].name).toBe('prod1.count'); }); it('should convert to millisecond resolution', () => { - expect(results.data[0].datapoints[0][0]).toBe(10); + expect(results.data[0].fields[0].values.get(0)).toBe(10); }); }); diff --git a/public/app/plugins/datasource/graphite/types.ts b/public/app/plugins/datasource/graphite/types.ts index 930205a96d4..9ebd2cd2203 100644 --- a/public/app/plugins/datasource/graphite/types.ts +++ b/public/app/plugins/datasource/graphite/types.ts @@ -3,3 +3,8 @@ import { DataQuery } from '@grafana/data'; export interface GraphiteQuery extends DataQuery { target?: string; } + +export enum GraphiteType { + Default = 'default', + Metrictank = 'metrictank', +}