Commit f22f1d46 by Carl Bergquist Committed by GitHub

Merge pull request #10163 from grafana/9587_annotation_tags_wih_temp_var

annotations: allows template variables to be used in tag filter
parents 6ffca7f1 75882832
......@@ -45,8 +45,9 @@ can still show them if you add a new **Annotation Query** and filter by tags. Bu
### Query by tag
You can create new annotation queries that fetch annotations from the native annotation store via the `-- Grafana --` data source and by setting *Filter by* to `Tags`. Specify at least
one tag. For example create an annotation query name `outages` and specify a tag named `outage`. This query will show all annotations you create (from any dashboard or via API) that
have the `outage` tag.
one tag. For example create an annotation query name `outages` and specify a tag named `outage`. This query will show all annotations you create (from any dashboard or via API) that have the `outage` tag. By default, if you add multiple tags in the annotation query, Grafana will only show annotations that have all the tags you supplied. You can invert the behavior by enabling `Match any` which means that Grafana will show annotations that contains at least one of the tags you supplied.
In 5.4+ it's possible to use template variables in the tag query. So if you have a dashboard showing stats for different services and an template variable that dictates which services to show, you can now use the same template variable in your annotation query to only show annotations for those services.
## Querying other data sources
......
......@@ -24,6 +24,7 @@ func GetAnnotations(c *m.ReqContext) Response {
Limit: c.QueryInt64("limit"),
Tags: c.QueryStrings("tags"),
Type: c.Query("type"),
MatchAny: c.QueryBool("matchAny"),
}
repo := annotations.GetRepository()
......
......@@ -21,6 +21,7 @@ type ItemQuery struct {
RegionId int64 `json:"regionId"`
Tags []string `json:"tags"`
Type string `json:"type"`
MatchAny bool `json:"matchAny"`
Limit int64 `json:"limit"`
}
......
......@@ -211,7 +211,12 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I
)
`, strings.Join(keyValueFilters, " OR "))
sql.WriteString(fmt.Sprintf(" AND (%s) = %d ", tagsSubQuery, len(tags)))
if query.MatchAny {
sql.WriteString(fmt.Sprintf(" AND (%s) > 0 ", tagsSubQuery))
} else {
sql.WriteString(fmt.Sprintf(" AND (%s) = %d ", tagsSubQuery, len(tags)))
}
}
}
......
......@@ -78,7 +78,31 @@ func TestAnnotations(t *testing.T) {
So(err, ShouldBeNil)
So(annotation2.Id, ShouldBeGreaterThan, 0)
Convey("Can query for annotation", func() {
globalAnnotation1 := &annotations.Item{
OrgId: 1,
UserId: 1,
Text: "deploy",
Type: "",
Epoch: 15,
Tags: []string{"deploy"},
}
err = repo.Save(globalAnnotation1)
So(err, ShouldBeNil)
So(globalAnnotation1.Id, ShouldBeGreaterThan, 0)
globalAnnotation2 := &annotations.Item{
OrgId: 1,
UserId: 1,
Text: "rollback",
Type: "",
Epoch: 17,
Tags: []string{"rollback"},
}
err = repo.Save(globalAnnotation2)
So(err, ShouldBeNil)
So(globalAnnotation2.Id, ShouldBeGreaterThan, 0)
Convey("Can query for annotation by dashboard id", func() {
items, err := repo.Find(&annotations.ItemQuery{
OrgId: 1,
DashboardId: 1,
......@@ -165,7 +189,7 @@ func TestAnnotations(t *testing.T) {
OrgId: 1,
DashboardId: 1,
From: 1,
To: 15,
To: 15, //this will exclude the second test annotation
Tags: []string{"outage", "error"},
})
......@@ -173,6 +197,19 @@ func TestAnnotations(t *testing.T) {
So(items, ShouldHaveLength, 1)
})
Convey("Should find two annotations using partial match", func() {
items, err := repo.Find(&annotations.ItemQuery{
OrgId: 1,
From: 1,
To: 25,
MatchAny: true,
Tags: []string{"rollback", "deploy"},
})
So(err, ShouldBeNil)
So(items, ShouldHaveLength, 2)
})
Convey("Should find one when all key value tag filters does match", func() {
items, err := repo.Find(&annotations.ItemQuery{
OrgId: 1,
......
......@@ -2,7 +2,7 @@ import _ from 'lodash';
class GrafanaDatasource {
/** @ngInject */
constructor(private backendSrv, private $q) {}
constructor(private backendSrv, private $q, private templateSrv) {}
query(options) {
return this.backendSrv
......@@ -40,6 +40,7 @@ class GrafanaDatasource {
to: options.range.to.valueOf(),
limit: options.annotation.limit,
tags: options.annotation.tags,
matchAny: options.annotation.matchAny,
};
if (options.annotation.type === 'dashboard') {
......@@ -56,6 +57,14 @@ class GrafanaDatasource {
if (!_.isArray(options.annotation.tags) || options.annotation.tags.length === 0) {
return this.$q.when([]);
}
const tags = [];
for (const t of params.tags) {
const renderedValues = this.templateSrv.replace(t, {}, 'pipe');
for (const tt of renderedValues.split('|')) {
tags.push(tt);
}
}
params.tags = tags;
}
return this.backendSrv.get('/api/annotations', params);
......
......@@ -2,7 +2,7 @@
<div class="gf-form-group">
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label width-8">
<span class="gf-form-label width-9">
Filter by
<info-popover mode="right-normal">
<ul>
......@@ -11,18 +11,11 @@
</ul>
</info-popover>
</span>
<div class="gf-form-select-wrapper width-9">
<div class="gf-form-select-wrapper width-8">
<select class="gf-form-input" ng-model="ctrl.annotation.type" ng-options="f.value as f.text for f in ctrl.types">
</select>
</div>
</div>
<div class="gf-form" ng-if="ctrl.annotation.type === 'tags'">
<span class="gf-form-label">Tags</span>
<bootstrap-tagsinput ng-model="ctrl.annotation.tags" tagclass="label label-tag" placeholder="add tags">
</bootstrap-tagsinput>
</div>
<div class="gf-form">
<span class="gf-form-label">Max limit</span>
<div class="gf-form-select-wrapper">
......@@ -31,6 +24,22 @@
</div>
</div>
</div>
<div class="gf-form-inline">
<div class="gf-form" ng-if="ctrl.annotation.type === 'tags'">
<gf-form-switch
class="gf-form"
label="Match any"
label-class="width-9"
checked="ctrl.annotation.matchAny"
on-change="ctrl.refresh()"
tooltip="By default Grafana will only show annotation that matches all tags in the query. Enabling this will make Grafana return any annotation with the tags you specify."></gf-form-switch>
</div>
<div class="gf-form" ng-if="ctrl.annotation.type === 'tags'">
<span class="gf-form-label">Tags</span>
<bootstrap-tagsinput ng-model="ctrl.annotation.tags" tagclass="label label-tag" placeholder="add tags">
</bootstrap-tagsinput>
</div>
</div>
</div>
import {GrafanaDatasource} from "../datasource";
import q from 'q';
import moment from 'moment';
describe('grafana data source', () => {
describe('when executing an annotations query', () => {
let calledBackendSrvParams;
const backendSrvStub = {
get: (url, options) => {
calledBackendSrvParams = options;
return q.resolve([]);
}
};
const templateSrvStub = {
replace: val => {
return val
.replace('$var2', 'replaced|replaced2')
.replace('$var', 'replaced');
}
};
const ds = new GrafanaDatasource(backendSrvStub, q, templateSrvStub);
describe('with tags that have template variables', () => {
const options = setupAnnotationQueryOptions(
{tags: ['tag1:$var']}
);
beforeEach(() => {
return ds.annotationQuery(options);
});
it('should interpolate template variables in tags in query options', () => {
expect(calledBackendSrvParams.tags[0]).toBe('tag1:replaced');
});
});
describe('with tags that have multi value template variables', () => {
const options = setupAnnotationQueryOptions(
{tags: ['$var2']}
);
beforeEach(() => {
return ds.annotationQuery(options);
});
it('should interpolate template variables in tags in query options', () => {
expect(calledBackendSrvParams.tags[0]).toBe('replaced');
expect(calledBackendSrvParams.tags[1]).toBe('replaced2');
});
});
describe('with type dashboard', () => {
const options = setupAnnotationQueryOptions(
{
type: 'dashboard',
tags: ['tag1']
},
{id: 1}
);
beforeEach(() => {
return ds.annotationQuery(options);
});
it('should remove tags from query options', () => {
expect(calledBackendSrvParams.tags).toBe(undefined);
});
});
});
});
function setupAnnotationQueryOptions(annotation, dashboard?) {
return {
annotation: annotation,
dashboard: dashboard,
range: {
from: moment(1432288354),
to: moment(1432288401)
},
rangeRaw: {from: "now-24h", to: "now"}
};
}
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