Merge pull request #10163 from grafana/9587_annotation_tags_wih_temp_var

annotations: allows template variables to be used in tag filter
This commit is contained in:
Carl Bergquist
2018-09-17 10:34:38 +02:00
committed by GitHub
8 changed files with 162 additions and 15 deletions

View File

@@ -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

View File

@@ -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()

View File

@@ -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"`
}

View File

@@ -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)))
}
}
}

View File

@@ -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,

View File

@@ -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);

View File

@@ -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>

View File

@@ -0,0 +1,84 @@
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"}
};
}