Skip to content

Commit d8b9a59

Browse files
committed
2 parents 259d582 + 470e867 commit d8b9a59

File tree

7 files changed

+198
-6
lines changed

7 files changed

+198
-6
lines changed

dist/module.js

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/plugin.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"type": "datasource",
33
"metrics": true,
4-
"annotations": false,
4+
"annotations": true,
55
"category": "logging",
66
"name": "Scalyr",
77
"id": "scalyr-datasource",
@@ -35,6 +35,11 @@
3535
"path": "powerQuery",
3636
"method": "POST",
3737
"url": "{{.JsonData.scalyrUrl}}/api/powerQuery"
38+
},
39+
{
40+
"path": "query",
41+
"method": "POST",
42+
"url": "{{.JsonData.scalyrUrl}}/api/query"
3843
}
3944
]
4045
}

src/datasource.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,19 @@ export class GenericDatasource {
5959
return this.performTimeseriesQuery(options);
6060
}
6161

62+
annotationQuery(options) {
63+
const query = this.createLogsQueryForAnnotation(options);
64+
return this.backendSrv.datasourceRequest(query)
65+
.then( (response) => {
66+
const data = response.data;
67+
const timeField = options.annotation.timeField || "timestamp"
68+
const timeEndField = options.annotation.timeEndField || null
69+
const textField = options.annotation.textField || "message"
70+
return GenericDatasource.transformAnnotationResults(data.matches, timeField, timeEndField, textField);
71+
}
72+
);
73+
}
74+
6275
/**
6376
* Grafana uses this function to test data source settings.
6477
* This verifies API key using the facet query API.
@@ -194,6 +207,24 @@ export class GenericDatasource {
194207
};
195208
}
196209

210+
createLogsQueryForAnnotation(options) {
211+
const queryText = this.templateSrv.replace(options.annotation.queryText, options.scopedVars, GenericDatasource.interpolateVariable);
212+
213+
return {
214+
url: this.url + '/query',
215+
method: 'POST',
216+
headers: this.headers,
217+
data: JSON.stringify({
218+
token: this.apiKey,
219+
queryType: "log",
220+
filter: queryText,
221+
startTime: options.range.from.valueOf(),
222+
endTime: options.range.to.valueOf(),
223+
maxCount: 5000
224+
})
225+
};
226+
}
227+
197228
/**
198229
* Get how many buckets to return based on the query time range
199230
* @param {*} options
@@ -246,6 +277,39 @@ export class GenericDatasource {
246277
return graphs;
247278
}
248279

280+
/**
281+
* Transform data returned by time series query into Grafana annotation format.
282+
* @param results
283+
* @param options
284+
* @returns Array
285+
*/
286+
static transformAnnotationResults(results, timeField, timeEndField, textField) {
287+
const annotations = [];
288+
results.forEach((result) => {
289+
const responseObject = {};
290+
responseObject.time = Number(result[timeField]) / 1000000;
291+
if (!responseObject.time && result.attributes) {
292+
responseObject.time = Number(result.attributes[timeField]) / 1000000;
293+
}
294+
295+
responseObject.text = result[textField];
296+
if (!responseObject.text && result.attributes) {
297+
responseObject.text = result.attributes[textField];
298+
}
299+
300+
if (timeEndField) {
301+
responseObject.timeEnd = Number(result[timeEndField]) / 1000000;
302+
if (!responseObject.timeEnd && result.attributes) {
303+
responseObject.timeEnd = Number(result.attributes[timeEndField]) / 1000000;
304+
}
305+
}
306+
if (responseObject.time) {
307+
annotations.push(responseObject);
308+
}
309+
});
310+
return annotations;
311+
}
312+
249313
/**
250314
* Create powerquery query to pass to Grafana proxy.
251315
* @param queryText text of the query

src/module.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@ export {
1313
GenericDatasource as Datasource,
1414
GenericDatasourceQueryCtrl as QueryCtrl,
1515
GenericConfigCtrl as ConfigCtrl,
16-
GenericQueryOptionsCtrl as QueryOptionsCtrl
16+
GenericQueryOptionsCtrl as QueryOptionsCtrl,
17+
GenericAnnotationsQueryCtrl as AnnotationsQueryCtrl
1718
};

src/partials/annotations.editor.html

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<div class="gf-form-group">
2+
<div class="gf-form-inline">
3+
<div class="gf-form gf-form--grow">
4+
<input
5+
class="gf-form-input"
6+
placeholder="query expression"
7+
ng-model="ctrl.annotation.queryText"
8+
></input>
9+
</div>
10+
</div>
11+
</div>
12+
13+
<div class="gf-form-group">
14+
<h6>Field mappings</h6>
15+
<div class="gf-form-inline">
16+
<div class="gf-form">
17+
<span class="gf-form-label">Time</span>
18+
<input type="text" class="gf-form-input max-width-14" ng-model='ctrl.annotation.timeField' placeholder="timestamp"></input>
19+
</div>
20+
<div class="gf-form">
21+
<span class="gf-form-label">Time End</span>
22+
<input type="text" class="gf-form-input max-width-14" ng-model='ctrl.annotation.timeEndField' placeholder=""></input>
23+
</div>
24+
<div class="gf-form">
25+
<span class="gf-form-label">Text</span>
26+
<input type="text" class="gf-form-input max-width-14" ng-model='ctrl.annotation.textField' placeholder="message"></input>
27+
</div>
28+
</div>
29+
</div>

src/plugin.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"type": "datasource",
33
"metrics": true,
4-
"annotations": false,
4+
"annotations": true,
55
"category": "logging",
66
"name": "Scalyr",
77
"id": "scalyr-datasource",
@@ -35,6 +35,11 @@
3535
"path": "powerQuery",
3636
"method": "POST",
3737
"url": "{{.JsonData.scalyrUrl}}/api/powerQuery"
38+
},
39+
{
40+
"path": "query",
41+
"method": "POST",
42+
"url": "{{.JsonData.scalyrUrl}}/api/query"
3843
}
3944
]
4045
}

src/specs/datasource.test.js

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,19 @@ describe('Scalyr datasource tests', () => {
2727
]
2828
};
2929

30+
const annotationQueryOptions = {
31+
range: {
32+
from: sixHoursAgo.toISOString(),
33+
to: now.toISOString()
34+
},
35+
interval: '5s',
36+
annotation: [
37+
{
38+
queryText: '$foo=\'bar\'',
39+
}
40+
]
41+
};
42+
3043
const variables = [
3144
{
3245
multi: true,
@@ -135,4 +148,79 @@ describe('Scalyr datasource tests', () => {
135148
});
136149
});
137150

151+
describe('Annotation queries', () => {
152+
let results;
153+
beforeEach(() => {
154+
results = [
155+
{
156+
timefield: 12345,
157+
messagefield: "testmessage1",
158+
timeendfield: 54321
159+
},
160+
{
161+
timefield: 12345,
162+
messagefield: "testmessage2",
163+
timeendfield: 54321,
164+
attributes: {
165+
timefield: 11111,
166+
messagefield: "wrong",
167+
timeendfield: 22222,
168+
}
169+
},
170+
{
171+
timefield: 12345,
172+
messagefield: "testmessage4",
173+
timeendfield: 54321,
174+
attributes: {
175+
timefield2: 123456,
176+
messagefield2: "testmessage5",
177+
timeendfield2: 543211,
178+
}
179+
}
180+
];
181+
});
182+
it('Should create a query request', () => {
183+
const request = datasource.createLogsQueryForAnnotation(annotationQueryOptions, variables);
184+
expect(request.url).toBe('proxied/query');
185+
expect(request.method).toBe('POST');
186+
const requestBody = JSON.parse(request.data);
187+
expect(requestBody.token).toBe('123');
188+
expect(requestBody.queryType).toBe('log');
189+
expect(requestBody.startTime).toBe(sixHoursAgo.toISOString());
190+
expect(requestBody.endTime).toBe(now.toISOString());
191+
});
192+
193+
it('Should transform standard query results to annotations', () => {
194+
const transformedResults = GenericDatasource.transformAnnotationResults(results, "timefield", "timeendfield", "messagefield");
195+
expect(transformedResults.length).toBe(3);
196+
const resultEntry = transformedResults[0];
197+
expect(resultEntry.text).toBe("testmessage1");
198+
expect(resultEntry.time).toBe(0.012345);
199+
expect(resultEntry.timeEnd).toBe(0.054321);
200+
});
201+
202+
it('Should transform standard query results to annotations, falling back to attribute fields', () => {
203+
const transformedResults = GenericDatasource.transformAnnotationResults(results, "timefield2", "timeendfield2", "messagefield2");
204+
expect(transformedResults.length).toBe(1);
205+
const resultEntry = transformedResults[0];
206+
expect(resultEntry.text).toBe("testmessage5");
207+
expect(resultEntry.time).toBe(0.123456);
208+
expect(resultEntry.timeEnd).toBe(0.543211);
209+
});
210+
211+
it('Should transform standard query results to annotations not from attributes first', () => {
212+
const transformedResults = GenericDatasource.transformAnnotationResults(results, "timefield", "timeendfield", "messagefield");
213+
expect(transformedResults.length).toBe(3);
214+
const resultEntry = transformedResults[1];
215+
expect(resultEntry.text).toBe("testmessage2");
216+
expect(resultEntry.time).toBe(0.012345);
217+
expect(resultEntry.timeEnd).toBe(0.054321);
218+
});
219+
220+
it('Shouldn\'t transform standard query results to annotations with bad field names', () => {
221+
const transformedResults = GenericDatasource.transformAnnotationResults(results, "missingField", null, null);
222+
expect(transformedResults.length).toBe(0);
223+
});
224+
});
225+
138226
});

0 commit comments

Comments
 (0)