diff --git a/public/app/features/annotations/annotations_srv.ts b/public/app/features/annotations/annotations_srv.ts index a32ba7e72ad..4f0f9e925da 100644 --- a/public/app/features/annotations/annotations_srv.ts +++ b/public/app/features/annotations/annotations_srv.ts @@ -57,7 +57,12 @@ export class AnnotationsSrv { }; }).catch(err => { - this.$rootScope.appEvent('alert-error', ['Annotations failed', (err.message || err)]); + if (!err.message && err.data && err.data.message) { + err.message = err.data.message; + } + this.$rootScope.appEvent('alert-error', ['Annotation Query Failed', (err.message || err)]); + + return []; }); } diff --git a/public/app/plugins/datasource/mysql/datasource.ts b/public/app/plugins/datasource/mysql/datasource.ts index 2f2eaec395d..d2603bf8514 100644 --- a/public/app/plugins/datasource/mysql/datasource.ts +++ b/public/app/plugins/datasource/mysql/datasource.ts @@ -52,6 +52,68 @@ export class MysqlDatasource { }).then(this.processQueryResult.bind(this)); } + annotationQuery(options) { + if (!options.annotation.rawQuery) { + return this.$q.reject({message: 'Query missing in annotation definition'}); + } + + const query = { + refId: options.annotation.name, + datasourceId: this.id, + rawSql: this.templateSrv.replace(options.annotation.rawQuery, options.scopedVars, this.interpolateVariable), + format: 'table', + }; + + return this.backendSrv.datasourceRequest({ + url: '/api/tsdb/query', + method: 'POST', + data: { + from: options.range.from.valueOf().toString(), + to: options.range.to.valueOf().toString(), + queries: [query], + } + }).then(this.transformAnnotationResponse.bind(this, options)); + } + + transformAnnotationResponse(options, data) { + const table = data.data.results[options.annotation.name].tables[0]; + + let timeColumnIndex = -1; + let titleColumnIndex = -1; + let textColumnIndex = -1; + let tagsColumnIndex = -1; + + for (let i = 0; i < table.columns.length; i++) { + if (table.columns[i].text === 'time_sec') { + timeColumnIndex = i; + } else if (table.columns[i].text === 'title') { + titleColumnIndex = i; + } else if (table.columns[i].text === 'text') { + textColumnIndex = i; + } else if (table.columns[i].text === 'tags') { + tagsColumnIndex = i; + } + } + + if (timeColumnIndex === -1) { + return this.$q.reject({message: 'Missing mandatory time column (with time_sec column alias) in annotation query.'}); + } + + const list = []; + for (let i = 0; i < table.rows.length; i++) { + const row = table.rows[i]; + list.push({ + annotation: options.annotation, + time: Math.floor(row[timeColumnIndex]) * 1000, + title: row[titleColumnIndex], + text: row[textColumnIndex], + tags: row[tagsColumnIndex] ? row[tagsColumnIndex].trim().split(/\s*,\s*/) : [] + }); + } + + return list; + } + testDatasource() { return this.backendSrv.datasourceRequest({ url: '/api/tsdb/query', diff --git a/public/app/plugins/datasource/mysql/module.ts b/public/app/plugins/datasource/mysql/module.ts index e706ca3cd44..156cff61b61 100644 --- a/public/app/plugins/datasource/mysql/module.ts +++ b/public/app/plugins/datasource/mysql/module.ts @@ -9,10 +9,33 @@ class MysqlConfigCtrl { static templateUrl = 'partials/config.html'; } +const defaultQuery = `SELECT + UNIX_TIMESTAMP() as time_sec, + as title, + as text, + as tags + FROM + WHERE $__timeFilter(time_column) + ORDER BY ASC + LIMIT 100 + `; + +class MysqlAnnotationsQueryCtrl { + static templateUrl = 'partials/annotations.editor.html'; + + annotation: any; + + /** @ngInject **/ + constructor() { + this.annotation.rawQuery = this.annotation.rawQuery || defaultQuery; + } +} + export { MysqlDatasource, MysqlDatasource as Datasource, MysqlQueryCtrl as QueryCtrl, MysqlConfigCtrl as ConfigCtrl, + MysqlAnnotationsQueryCtrl as AnnotationsQueryCtrl, }; diff --git a/public/app/plugins/datasource/mysql/partials/annotations.editor.html b/public/app/plugins/datasource/mysql/partials/annotations.editor.html index 54e21ac902e..2f0bfd936a5 100644 --- a/public/app/plugins/datasource/mysql/partials/annotations.editor.html +++ b/public/app/plugins/datasource/mysql/partials/annotations.editor.html @@ -1,20 +1,34 @@
-
Filters
-
-
- Type -
- -
+
+
+
-
- Max limit -
- -
+
+ +
+
+
+
+ +
+
Annotation Query Format
+An annotation is an event that is overlayed on top of graphs. The query can have up to four columns per row, the time_sec column is mandatory. Annotation rendering is expensive so it is important to limit the number of rows returned. + +- column with alias: time_sec for the annotation event. Format is UTC in seconds, use UNIX_TIMESTAMP(column) +- column with alias title for the annotation title +- column with alias: text for the annotation text +- column with alias: tags for annotation tags. This is a comma separated string of tags e.g. 'tag1,tag2' + + +Macros: +- $__time(column) -> UNIX_TIMESTAMP(column) as time_sec +- $__timeFilter(column) -> UNIX_TIMESTAMP(time_date_time) > from AND UNIX_TIMESTAMP(time_date_time) < 1492750877 +
diff --git a/public/app/plugins/datasource/mysql/query_ctrl.ts b/public/app/plugins/datasource/mysql/query_ctrl.ts index 889855a343a..cf3fe9f2830 100644 --- a/public/app/plugins/datasource/mysql/query_ctrl.ts +++ b/public/app/plugins/datasource/mysql/query_ctrl.ts @@ -17,7 +17,7 @@ export interface QueryMeta { } -var defaulQuery = `SELECT +const defaultQuery = `SELECT UNIX_TIMESTAMP() as time_sec, as value, as metric @@ -54,7 +54,7 @@ export class MysqlQueryCtrl extends QueryCtrl { this.target.format = 'table'; this.target.rawSql = "SELECT 1"; } else { - this.target.rawSql = defaulQuery; + this.target.rawSql = defaultQuery; } } diff --git a/public/app/plugins/datasource/mysql/specs/datasource_specs.ts b/public/app/plugins/datasource/mysql/specs/datasource_specs.ts new file mode 100644 index 00000000000..3a82293fb61 --- /dev/null +++ b/public/app/plugins/datasource/mysql/specs/datasource_specs.ts @@ -0,0 +1,79 @@ +import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common'; +import moment from 'moment'; +import helpers from 'test/specs/helpers'; +import {MysqlDatasource} from '../datasource'; + +describe('MySQLDatasource', function() { + var ctx = new helpers.ServiceTestContext(); + var instanceSettings = {name: 'mysql'}; + + beforeEach(angularMocks.module('grafana.core')); + beforeEach(angularMocks.module('grafana.services')); + beforeEach(ctx.providePhase(['backendSrv'])); + + beforeEach(angularMocks.inject(function($q, $rootScope, $httpBackend, $injector) { + ctx.$q = $q; + ctx.$httpBackend = $httpBackend; + ctx.$rootScope = $rootScope; + ctx.ds = $injector.instantiate(MysqlDatasource, {instanceSettings: instanceSettings}); + $httpBackend.when('GET', /\.html$/).respond(''); + })); + + describe('When performing annotationQuery', function() { + let results; + + const annotationName = 'MyAnno'; + + const options = { + annotation: { + name: annotationName, + rawQuery: 'select time_sec, title, text, tags from table;' + }, + range: { + from: moment(1432288354), + to: moment(1432288401) + } + }; + + const response = { + results: { + MyAnno: { + refId: annotationName, + tables: [ + { + columns: [{text: 'time_sec'}, {text: 'title'}, {text: 'text'}, {text: 'tags'}], + rows: [ + [1432288355, 'aTitle', 'some text', 'TagA,TagB'], + [1432288390, 'aTitle2', 'some text2', ' TagB , TagC'], + [1432288400, 'aTitle3', 'some text3'] + ] + } + ] + } + } + }; + + beforeEach(function() { + ctx.backendSrv.datasourceRequest = function(options) { + return ctx.$q.when({data: response, status: 200}); + }; + ctx.ds.annotationQuery(options).then(function(data) { results = data; }); + ctx.$rootScope.$apply(); + }); + + it('should return annotation list', function() { + expect(results.length).to.be(3); + + expect(results[0].title).to.be('aTitle'); + expect(results[0].text).to.be('some text'); + expect(results[0].tags[0]).to.be('TagA'); + expect(results[0].tags[1]).to.be('TagB'); + + expect(results[1].tags[0]).to.be('TagB'); + expect(results[1].tags[1]).to.be('TagC'); + + expect(results[2].tags.length).to.be(0); + }); + }); + +});