Stackdriver: Support for SLO queries (#22917)

* wip: add slo support

* Export DataSourcePlugin

* wip: break out metric query editor into its own component

* wip: refactor frontend - keep SLO and Metric query in differnt objects

* wip - load services and slos

* Fix broken test

* Add interactive slo expression builder

* Change order of dropdowns

* Refactoring backend model. slo unit testing in progress

* Unit test migration and SLOs

* Cleanup SLO editor

* Simplify alias by component

* Support alias by for slos

* Support slos in variable queries

* Fix broken last query error

* Update Help section to include SLO aliases

* streamline datasource resource cache

* Break out api specific stuff in datasource to its own file

* Move get projects call to frontend

* Refactor api caching

* Unit test api service

* Fix lint go issue

* Fix typescript strict errors

* Fix test datasource

* Use budget fraction selector instead of budget

* Reset SLO when service is changed

* Handle error in case resource call returned no data

* Show real SLI display name

* Use unsafe prefix on will mount hook

* Store goal in query model since it will be used as soon as graph panel supports adding a threshold

* Add comment to describe why componentWillMount is used

* Interpolate sloid

* Break out SLO aggregation into its own func

* Also test group bys for metricquery test

* Remove not used type fields

* Remove annoying stackdriver prefix from error message

* Default view param to FULL

* Add part about SLO query builder in docs

* Use new images

* Fixes after feedback

* Add one more group by test

* Make stackdriver types internal

* Update docs/sources/features/datasources/stackdriver.md

Co-Authored-By: Diana Payton <52059945+oddlittlebird@users.noreply.github.com>

* Update docs/sources/features/datasources/stackdriver.md

Co-Authored-By: Diana Payton <52059945+oddlittlebird@users.noreply.github.com>

* Update docs/sources/features/datasources/stackdriver.md

Co-Authored-By: Diana Payton <52059945+oddlittlebird@users.noreply.github.com>

* Updates after PR feedback

* add test for when no alias by defined

* fix infinite loop when newVariables feature flag is on

onChange being called in componentDidUpdate produces an
infinite loop when using the new React template variable
implementation.

Also fixes a spelling mistake

* implements feedback for documentation changes

* more doc changes

Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com>
Co-authored-by: Daniel Lee <dan.limerick@gmail.com>
pull/23155/head
Erik Sundell 5 years ago committed by GitHub
parent e19493ae24
commit a111cc0d5c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 142
      docs/sources/features/datasources/stackdriver.md
  2. 2
      pkg/tsdb/stackdriver/annotation_query.go
  3. 2
      pkg/tsdb/stackdriver/annotation_query_test.go
  4. 283
      pkg/tsdb/stackdriver/stackdriver.go
  5. 164
      pkg/tsdb/stackdriver/stackdriver_test.go
  6. 17
      pkg/tsdb/stackdriver/test-data/6-series-response-slo.json
  7. 57
      pkg/tsdb/stackdriver/types.go
  8. 25
      public/app/plugins/datasource/stackdriver/StackdriverMetricFindQuery.ts
  9. 69
      public/app/plugins/datasource/stackdriver/api.test.ts
  10. 72
      public/app/plugins/datasource/stackdriver/api.ts
  11. 5
      public/app/plugins/datasource/stackdriver/components/Aggregations.tsx
  12. 58
      public/app/plugins/datasource/stackdriver/components/AliasBy.tsx
  13. 2
      public/app/plugins/datasource/stackdriver/components/AlignmentPeriods.tsx
  14. 2
      public/app/plugins/datasource/stackdriver/components/Alignments.tsx
  15. 10
      public/app/plugins/datasource/stackdriver/components/AnnotationQueryEditor.tsx
  16. 28
      public/app/plugins/datasource/stackdriver/components/Fields.tsx
  17. 4
      public/app/plugins/datasource/stackdriver/components/GroupBys.tsx
  18. 12
      public/app/plugins/datasource/stackdriver/components/Help.tsx
  19. 22
      public/app/plugins/datasource/stackdriver/components/LabelFilter.tsx
  20. 140
      public/app/plugins/datasource/stackdriver/components/MetricQueryEditor.tsx
  21. 6
      public/app/plugins/datasource/stackdriver/components/Metrics.tsx
  22. 27
      public/app/plugins/datasource/stackdriver/components/QueryEditor.test.tsx
  23. 309
      public/app/plugins/datasource/stackdriver/components/QueryEditor.tsx
  24. 34
      public/app/plugins/datasource/stackdriver/components/QueryType.tsx
  25. 108
      public/app/plugins/datasource/stackdriver/components/SLOQueryEditor.tsx
  26. 6
      public/app/plugins/datasource/stackdriver/components/VariableQueryEditor.test.tsx
  27. 65
      public/app/plugins/datasource/stackdriver/components/VariableQueryEditor.tsx
  28. 248
      public/app/plugins/datasource/stackdriver/components/__snapshots__/QueryEditor.test.tsx.snap
  29. 15
      public/app/plugins/datasource/stackdriver/components/__snapshots__/VariableQueryEditor.test.tsx.snap
  30. 6
      public/app/plugins/datasource/stackdriver/components/index.ts
  31. 6
      public/app/plugins/datasource/stackdriver/constants.ts
  32. 424
      public/app/plugins/datasource/stackdriver/datasource.ts
  33. 36
      public/app/plugins/datasource/stackdriver/functions.ts
  34. 16
      public/app/plugins/datasource/stackdriver/module.ts
  35. 10
      public/app/plugins/datasource/stackdriver/partials/query.editor.html
  36. 25
      public/app/plugins/datasource/stackdriver/query_ctrl.ts
  37. 19
      public/app/plugins/datasource/stackdriver/specs/datasource.test.ts
  38. 47
      public/app/plugins/datasource/stackdriver/types.ts

@ -27,11 +27,11 @@ Grafana ships with built-in support for Google Stackdriver. Just add it as a dat
> NOTE: If you're not seeing the `Data Sources` link in your side menu it means that your current user does not have the `Admin` role for the current organization.
| Name | Description |
| --------------------- | ----------------------------------------------------------------------------------- |
| _Name_ | The data source name. This is how you refer to the data source in panels and queries. |
| _Default_ | Default data source means that it will be pre-selected for new panels. |
| _Service Account Key_ | Service Account Key File for a GCP Project. Instructions below on how to create it. |
| Name | Description |
| --------------------- | ------------------------------------------------------------------------------------- |
| _Name_ | The data source name. This is how you refer to the data source in panels and queries. |
| _Default_ | Default data source means that it will be pre-selected for new panels. |
| _Service Account Key_ | Service Account Key File for a GCP Project. Instructions below on how to create it. |
## Authentication
@ -45,8 +45,8 @@ To authenticate with the Stackdriver API, you need to create a Google Cloud Plat
The following APIs need to be enabled first:
* [Monitoring API](https://console.cloud.google.com/apis/library/monitoring.googleapis.com)
* [Cloud Resource Manager API](https://console.cloud.google.com/apis/library/cloudresourcemanager.googleapis.com)
- [Monitoring API](https://console.cloud.google.com/apis/library/monitoring.googleapis.com)
- [Cloud Resource Manager API](https://console.cloud.google.com/apis/library/cloudresourcemanager.googleapis.com)
Click on the links above and click the `Enable` button:
@ -57,24 +57,24 @@ Click on the links above and click the `Enable` button:
1. Navigate to the [APIs and Services Credentials page](https://console.cloud.google.com/apis/credentials).
2. Click on the `Create credentials` dropdown/button and choose the `Service account key` option.
{{< docs-imagebox img="/img/docs/v53/stackdriver_create_service_account_button.png" class="docs-image--no-shadow" caption="Create service account button" >}}
{{< docs-imagebox img="/img/docs/v53/stackdriver_create_service_account_button.png" class="docs-image--no-shadow" caption="Create service account button" >}}
3. On the `Create service account key` page, choose key type `JSON`. Then in the `Service Account` dropdown, choose the `New service account` option:
{{< docs-imagebox img="/img/docs/v53/stackdriver_create_service_account_key.png" class="docs-image--no-shadow" caption="Create service account key" >}}
{{< docs-imagebox img="/img/docs/v53/stackdriver_create_service_account_key.png" class="docs-image--no-shadow" caption="Create service account key" >}}
4. Some new fields will appear. Fill in a name for the service account in the `Service account name` field and then choose the `Monitoring Viewer` role from the `Role` dropdown:
{{< docs-imagebox img="/img/docs/v53/stackdriver_service_account_choose_role.png" class="docs-image--no-shadow" caption="Choose role" >}}
{{< docs-imagebox img="/img/docs/v53/stackdriver_service_account_choose_role.png" class="docs-image--no-shadow" caption="Choose role" >}}
5. Click the Create button. A JSON key file will be created and downloaded to your computer. Store this file in a secure place as it allows access to your Stackdriver data.
6. Upload it to Grafana on the data source Configuration page. You can either upload the file or paste in the contents of the file.
{{< docs-imagebox img="/img/docs/v53/stackdriver_grafana_upload_key.png" class="docs-image--no-shadow" caption="Upload service key file to Grafana" >}}
{{< docs-imagebox img="/img/docs/v53/stackdriver_grafana_upload_key.png" class="docs-image--no-shadow" caption="Upload service key file to Grafana" >}}
7. The file contents will be encrypted and saved in the Grafana database. Don't forget to save after uploading the file!
{{< docs-imagebox img="/img/docs/v53/stackdriver_grafana_key_uploaded.png" class="docs-image--no-shadow" caption="Service key file is uploaded to Grafana" >}}
{{< docs-imagebox img="/img/docs/v53/stackdriver_grafana_key_uploaded.png" class="docs-image--no-shadow" caption="Service key file is uploaded to Grafana" >}}
### Using GCE Default Service Account
@ -86,58 +86,68 @@ If Grafana is running on a Google Compute Engine (GCE) virtual machine, it is po
Read more about creating and enabling service accounts for GCE VM instances [here](https://cloud.google.com/compute/docs/access/create-enable-service-accounts-for-instances).
## Metric Query Editor
## Using the Query Editor
{{< docs-imagebox img="/img/docs/v67/stackriver-query-editor.png" max-width= "400px" class="docs-image--right" >}}
The Stackdriver query editor allows you to build two types of queries - **Metric** and **Service Level Objective (SLO)**. Both types return time series data.
The Stackdriver query editor allows you to select metrics, group/aggregate by labels and by time, and use filters to specify which time series you want in the results.
### Metric Queries
Begin by choosing a `Project`. Then select a `Service` and then a metric from the `Metric` dropdown. Use the plus and minus icons in the filter and group by sections to add/remove filters or group by clauses.
{{< docs-imagebox img="/img/docs/v70/metric-query-builder.png" max-width= "400px" class="docs-image--right" >}}
The metric query editor allows you to select metrics, group/aggregate by labels and by time, and use filters to specify which time series you want in the results.
To create a metric query, follow these steps:
1. Choose the option **Metrics** in the **Query Type** dropdown
2. Choose a project from the **Project** dropdown
3. Choose a Google Cloud Platform service from the **Service** dropdown
4. Choose a metric from the **Metric** dropdown.
5. Use the plus and minus icons in the filter and group by sections to add/remove filters or group by clauses. This step is optional.
Stackdriver metrics can be of different kinds (GAUGE, DELTA, CUMULATIVE) and these kinds have support for different aggregation options (reducers and aligners). The Grafana query editor shows the list of available aggregation methods for a selected metric and sets a default reducer and aligner when you select the metric. Units for the Y-axis are also automatically selected by the query editor.
### Filter
#### Filter
To add a filter, click the plus icon and choose a field to filter by and enter a filter value e.g. `instance_name = grafana-1`. You can remove the filter by clicking on the filter name and select `--remove filter--`.
#### Simple wildcards
##### Simple wildcards
When the operator is set to `=` or `!=` it is possible to add wildcards to the filter value field. E.g `us-*` will capture all values that starts with "us-" and `*central-a` will capture all values that ends with "central-a". `*-central-*` captures all values that has the substring of -central-. Simple wildcards are less expensive than regular expressions.
#### Regular expressions
##### Regular expressions
When the operator is set to `=~` or `!=~` it is possible to add regular expressions to the filter value field. E.g `us-central[1-3]-[af]` would match all values that starts with "us-central", is followed by a number in the range of 1 to 3, a dash and then either an "a" or an "f". Leading and trailing slashes are not needed when creating regular expressions.
### Aggregation
#### Aggregation
The aggregation field lets you combine time series based on common statistics. Read more about this option [here](https://cloud.google.com/monitoring/charts/metrics-selector#aggregation-options).
The `Aligner` field allows you to align multiple time series after the same group by time interval. Read more about how it works [here](https://cloud.google.com/monitoring/charts/metrics-selector#alignment).
#### Alignment Period/Group by Time
##### Alignment Period/Group by Time
The `Alignment Period` groups a metric by time if an aggregation is chosen. The default is to use the GCP Stackdriver default groupings (which allows you to compare graphs in Grafana with graphs in the Stackdriver UI).
The option is called `Stackdriver auto` and the defaults are:
* 1m for time ranges < 23 hours
* 5m for time ranges >= 23 hours and < 6 days
* 1h for time ranges >= 6 days
- 1m for time ranges < 23 hours
- 5m for time ranges >= 23 hours and < 6 days
- 1h for time ranges >= 6 days
The other automatic option is `Grafana auto`. This will automatically set the group by time depending on the time range chosen and the width of the graph panel. Read more about the details [here](http://docs.grafana.org/reference/templating/#the-interval-variable).
It is also possible to choose fixed time intervals to group by, like `1h` or `1d`.
### Group By
#### Group By
Group by resource or metric labels to reduce the number of time series and to aggregate the results by a group by. E.g. Group by instance_name to see an aggregated metric for a Compute instance.
#### Metadata labels
##### Metadata labels
Resource metadata labels contains information to uniquely identify a resource in Google cloud. Metadata labels are only returned in the time series response if they're part of the **Group By** segment in the time series request. There's no API for retrieving metadata labels, so it's not possible to populate the group by dropdown with the metadata labels that are available for the selected service and metric. However, the **Group By** field dropdown comes with a pre-defined list of common system labels.
Resource metadata labels contain information to uniquely identify a resource in Google Cloud. Metadata labels are only returned in the time series response if they're part of the **Group By** segment in the time series request. There's no API for retrieving metadata labels, so it's not possible to populate the group by dropdown with the metadata labels that are available for the selected service and metric. However, the **Group By** field dropdown comes with a pre-defined list of common system labels.
User labels cannot be pre-defined, but it's possible to enter them manually in the **Group By** field. If a metadata label, user label or system label is included in the **Group By** segment, then you can create filters based on it and expand its value on the **Alias** field.
### Alias Patterns
#### Alias patterns
The Alias By field allows you to control the format of the legend keys. The default is to show the metric name and labels. This can be long and hard to read. Using the following patterns in the alias field, you can format the legend key the way you want it.
@ -153,10 +163,10 @@ The Alias By field allows you to control the format of the legend keys. The defa
In the Group By dropdown, you can see a list of metric and resource labels for a metric. These can be included in the legend key using alias patterns.
| Alias Pattern Format | Description | Alias Pattern Example | Example Result |
| ------------------------ | -------------------------------- | -------------------------------- | ---------------- |
| `{{metric.label.xxx}}` | returns the metric label value | `{{metric.label.instance_name}}` | `grafana-1-prod` |
| `{{resource.label.xxx}}` | returns the resource label value | `{{resource.label.zone}}` | `us-east1-b` |
| Alias Pattern Format | Description | Alias Pattern Example | Example Result |
| -------------------------------- | ---------------------------------------- | --------------------------------- | ---------------- |
| `{{metric.label.xxx}}` | returns the metric label value | `{{metric.label.instance_name}}` | `grafana-1-prod` |
| `{{resource.label.xxx}}` | returns the resource label value | `{{resource.label.zone}}` | `us-east1-b` |
| `{{metadata.system_labels.xxx}}` | returns the meta data system label value | `{{metadata.system_labels.name}}` | `grafana` |
| `{{metadata.user_labels.xxx}}` | returns the meta data user label value | `{{metadata.user_labels.tag}}` | `production` |
@ -174,6 +184,45 @@ Example Alias By: `{{resource.type}} - {{metric.type}}`
Example Result: `gce_instance - compute.googleapis.com/instance/cpu/usage_time`
### SLO (Service Level Objective) queries
{{< docs-imagebox img="/img/docs/v70/slo-query-builder.png" max-width= "400px" class="docs-image--right" >}}
The SLO query builder in the Stackdriver data source allows you to display SLO data in time series format. To get an understanding of the basic concepts in service monitoring, please refer to Google Stackdriver's [official docs](https://cloud.google.com/monitoring/service-monitoring).
#### How to create an SLO query
To create an SLO query, follow these steps:
1. Choose the option **Service Level Objectives (SLO)** in the **Query Type** dropdown.
2. Choose a project from the **Project** dropdown.
3. Choose a [SLO service](https://cloud.google.com/monitoring/api/ref_v3/rest/v3/services) from the **Service** dropdown.
4. Choose a [SLO](https://cloud.google.com/monitoring/api/ref_v3/rest/v3/services.serviceLevelObjectives) from the **SLO** dropdown.
5. Choose a [time series selector](https://cloud.google.com/monitoring/service-monitoring/timeseries-selectors#ts-selector-list) from the **Selector** dropdown.
The friendly names for the time series selectors are shown in Grafana. Here is the mapping from the friendly name to the system name that is used in the Service Monitoring documentation:
| Selector dropdown value | Corresponding time series selector used |
| -------------------------- | --------------------------------------- |
| SLI Value | select_slo_health |
| SLO Compliance | select_slo_compliance |
| SLO Error Budget Remaining | select_slo_budget_fraction |
#### Alias Patterns for SLO queries
The Alias By field allows you to control the format of the legend keys for SLO queries too.
| Alias Pattern | Description | Example Result |
| -------------- | ---------------------------- | ------------------- |
| `{{project}}` | returns the GCP project name | `myProject` |
| `{{service}}` | returns the service name | `myService` |
| `{{slo}}` | returns the SLO | `latency-slo` |
| `{{selector}}` | returns the selector | `select_slo_health` |
#### Alignment Period/Group by Time for SLO queries
SLO queries use the same [alignment period functionality as metric queries]({{< relref "#metric-queries" >}}).
## Templating
Instead of hard-coding things like server, application and sensor name in you metric queries you can use variables in their place.
@ -185,24 +234,27 @@ types of template variables.
### Query Variable
Variable of the type *Query* allows you to query Stackdriver for various types of data. The Stackdriver data source plugin provides the following `Query Types`.
| Name | Description |
| ------------------- | ------------------------------------------------------------------------------------------------- |
| *Metric Types* | Returns a list of metric type names that are available for the specified service. |
| *Labels Keys* | Returns a list of keys for `metric label` and `resource label` in the specified metric. |
| *Labels Values* | Returns a list of values for the label in the specified metric. |
| *Resource Types* | Returns a list of resource types for the the specified metric. |
| *Aggregations* | Returns a list of aggregations (cross series reducers) for the the specified metric. |
| *Aligners* | Returns a list of aligners (per series aligners) for the the specified metric. |
| *Alignment periods* | Returns a list of all alignment periods that are available in Stackdriver query editor in Grafana |
Variable of the type _Query_ allows you to query Stackdriver for various types of data. The Stackdriver data source plugin provides the following `Query Types`.
| Name | Description |
| -------------------------------- | ------------------------------------------------------------------------------------------------- |
| _Metric Types_ | Returns a list of metric type names that are available for the specified service. |
| _Labels Keys_ | Returns a list of keys for `metric label` and `resource label` in the specified metric. |
| _Labels Values_ | Returns a list of values for the label in the specified metric. |
| _Resource Types_ | Returns a list of resource types for the the specified metric. |
| _Aggregations_ | Returns a list of aggregations (cross series reducers) for the the specified metric. |
| _Aligners_ | Returns a list of aligners (per series aligners) for the the specified metric. |
| _Alignment periods_ | Returns a list of all alignment periods that are available in Stackdriver query editor in Grafana |
| _Selectors_ | Returns a list of selectors that can be used in SLO (Service Level Objectives) queries |
| _SLO Services_ | Returns a list of Service Monitoring services that can be used in SLO queries |
| _Service Level Objectives (SLO)_ | Returns a list of SLO's for the specified SLO service |
### Using variables in queries
There are two syntaxes:
* `$<varname>` Example: `metric.label.$metric_label`
* `[[varname]]` Example: `metric.label.[[metric_label]]`
- `$<varname>` Example: `metric.label.$metric_label`
- `[[varname]]` Example: `metric.label.[[metric_label]]`
Why two ways? The first syntax is easier to read and write but does not allow you to use a variable in the middle of a word. When the _Multi-value_ or _Include all value_ options are enabled, Grafana converts the labels from plain text to a regex compatible string, which means you have to use `=~` instead of `=`.

@ -34,7 +34,7 @@ func (e *StackdriverExecutor) executeAnnotationQuery(ctx context.Context, tsdbQu
return result, err
}
func (e *StackdriverExecutor) parseToAnnotations(queryRes *tsdb.QueryResult, data StackdriverResponse, query *StackdriverQuery, title string, text string, tags string) error {
func (e *StackdriverExecutor) parseToAnnotations(queryRes *tsdb.QueryResult, data stackdriverResponse, query *stackdriverQuery, title string, text string, tags string) error {
annotations := make([]map[string]string, 0)
for _, series := range data.TimeSeries {

@ -18,7 +18,7 @@ func TestStackdriverAnnotationQuery(t *testing.T) {
So(len(data.TimeSeries), ShouldEqual, 3)
res := &tsdb.QueryResult{Meta: simplejson.New(), RefId: "annotationQuery"}
query := &StackdriverQuery{}
query := &stackdriverQuery{}
err = executor.parseToAnnotations(res, data, query, "atitle {{metric.label.instance_name}} {{metric.value}}", "atext {{resource.label.zone}}", "atag")
So(err, ShouldBeNil)

@ -43,6 +43,8 @@ var (
const (
gceAuthentication string = "gce"
jwtAuthentication string = "jwt"
metricQueryType string = "metrics"
sloQueryType string = "slo"
)
// StackdriverExecutor executes queries for the Stackdriver datasource
@ -80,8 +82,6 @@ func (e *StackdriverExecutor) Query(ctx context.Context, dsInfo *models.DataSour
switch queryType {
case "annotationQuery":
result, err = e.executeAnnotationQuery(ctx, tsdbQuery)
case "getProjectsListQuery":
result, err = e.getProjectList(ctx, tsdbQuery)
case "getGCEDefaultProject":
result, err = e.getGCEDefaultProject(ctx, tsdbQuery)
case "timeSeriesQuery":
@ -136,8 +136,8 @@ func (e *StackdriverExecutor) executeTimeSeriesQuery(ctx context.Context, tsdbQu
return result, nil
}
func (e *StackdriverExecutor) buildQueries(tsdbQuery *tsdb.TsdbQuery) ([]*StackdriverQuery, error) {
stackdriverQueries := []*StackdriverQuery{}
func (e *StackdriverExecutor) buildQueries(tsdbQuery *tsdb.TsdbQuery) ([]*stackdriverQuery, error) {
stackdriverQueries := []*stackdriverQuery{}
startTime, err := tsdbQuery.TimeRange.ParseFrom()
if err != nil {
@ -152,43 +152,65 @@ func (e *StackdriverExecutor) buildQueries(tsdbQuery *tsdb.TsdbQuery) ([]*Stackd
durationSeconds := int(endTime.Sub(startTime).Seconds())
for _, query := range tsdbQuery.Queries {
migrateLegacyQueryModel(query)
q := grafanaQuery{}
model, _ := query.Model.MarshalJSON()
if err := json.Unmarshal(model, &q); err != nil {
return nil, fmt.Errorf("could not unmarshal StackdriverQuery json: %w", err)
}
var target string
metricType := query.Model.Get("metricType").MustString()
filterParts := query.Model.Get("filters").MustArray()
params := url.Values{}
params.Add("interval.startTime", startTime.UTC().Format(time.RFC3339))
params.Add("interval.endTime", endTime.UTC().Format(time.RFC3339))
params.Add("filter", buildFilterString(metricType, filterParts))
params.Add("view", query.Model.Get("view").MustString("FULL"))
setAggParams(&params, query, durationSeconds)
sq := &stackdriverQuery{
RefID: query.RefId,
GroupBys: []string{},
}
if q.QueryType == metricQueryType {
sq.AliasBy = q.MetricQuery.AliasBy
sq.GroupBys = append(sq.GroupBys, q.MetricQuery.GroupBys...)
sq.ProjectName = q.MetricQuery.ProjectName
if q.MetricQuery.View == "" {
q.MetricQuery.View = "FULL"
}
params.Add("filter", buildFilterString(q.MetricQuery.MetricType, q.MetricQuery.Filters))
params.Add("view", q.MetricQuery.View)
setMetricAggParams(&params, &q.MetricQuery, durationSeconds, query.IntervalMs)
} else if q.QueryType == sloQueryType {
sq.AliasBy = q.SloQuery.AliasBy
sq.ProjectName = q.SloQuery.ProjectName
sq.Selector = q.SloQuery.SelectorName
sq.Service = q.SloQuery.ServiceId
sq.Slo = q.SloQuery.SloId
params.Add("filter", buildSLOFilterExpression(q.SloQuery))
setSloAggParams(&params, &q.SloQuery, durationSeconds, query.IntervalMs)
}
target = params.Encode()
sq.Target = target
sq.Params = params
if setting.Env == setting.DEV {
slog.Debug("Stackdriver request", "params", params)
}
groupBys := query.Model.Get("groupBys").MustArray()
groupBysAsStrings := make([]string, 0)
for _, groupBy := range groupBys {
groupBysAsStrings = append(groupBysAsStrings, groupBy.(string))
}
stackdriverQueries = append(stackdriverQueries, sq)
}
aliasBy := query.Model.Get("aliasBy").MustString()
return stackdriverQueries, nil
}
stackdriverQueries = append(stackdriverQueries, &StackdriverQuery{
Target: target,
Params: params,
RefID: query.RefId,
GroupBys: groupBysAsStrings,
AliasBy: aliasBy,
ProjectName: query.Model.Get("projectName").MustString(""),
func migrateLegacyQueryModel(query *tsdb.Query) {
mq := query.Model.Get("metricQuery").MustMap()
if mq == nil {
migratedModel := simplejson.NewFromAny(map[string]interface{}{
"queryType": metricQueryType,
"metricQuery": query.Model,
})
query.Model = migratedModel
}
return stackdriverQueries, nil
}
func reverse(s string) string {
@ -222,7 +244,7 @@ func interpolateFilterWildcards(value string) string {
return value
}
func buildFilterString(metricType string, filterParts []interface{}) string {
func buildFilterString(metricType string, filterParts []string) string {
filterString := ""
for i, part := range filterParts {
mod := i % 4
@ -233,33 +255,53 @@ func buildFilterString(metricType string, filterParts []interface{}) string {
if operator == "=~" || operator == "!=~" {
filterString = reverse(strings.Replace(reverse(filterString), "~", "", 1))
filterString += fmt.Sprintf(`monitoring.regex.full_match("%s")`, part)
} else if strings.Contains(part.(string), "*") {
filterString += interpolateFilterWildcards(part.(string))
} else if strings.Contains(part, "*") {
filterString += interpolateFilterWildcards(part)
} else {
filterString += fmt.Sprintf(`"%s"`, part)
}
} else {
filterString += part.(string)
filterString += part
}
}
return strings.Trim(fmt.Sprintf(`metric.type="%s" %s`, metricType, filterString), " ")
}
func setAggParams(params *url.Values, query *tsdb.Query, durationSeconds int) {
crossSeriesReducer := query.Model.Get("crossSeriesReducer").MustString()
perSeriesAligner := query.Model.Get("perSeriesAligner").MustString()
alignmentPeriod := query.Model.Get("alignmentPeriod").MustString()
func buildSLOFilterExpression(q sloQuery) string {
return fmt.Sprintf(`%s("projects/%s/services/%s/serviceLevelObjectives/%s")`, q.SelectorName, q.ProjectName, q.ServiceId, q.SloId)
}
func setMetricAggParams(params *url.Values, query *metricQuery, durationSeconds int, intervalMs int64) {
if query.CrossSeriesReducer == "" {
query.CrossSeriesReducer = "REDUCE_NONE"
}
if crossSeriesReducer == "" {
crossSeriesReducer = "REDUCE_NONE"
if query.PerSeriesAligner == "" {
query.PerSeriesAligner = "ALIGN_MEAN"
}
if perSeriesAligner == "" {
perSeriesAligner = "ALIGN_MEAN"
params.Add("aggregation.crossSeriesReducer", query.CrossSeriesReducer)
params.Add("aggregation.perSeriesAligner", query.PerSeriesAligner)
params.Add("aggregation.alignmentPeriod", calculateAlignmentPeriod(query.AlignmentPeriod, intervalMs, durationSeconds))
for _, groupBy := range query.GroupBys {
params.Add("aggregation.groupByFields", groupBy)
}
}
func setSloAggParams(params *url.Values, query *sloQuery, durationSeconds int, intervalMs int64) {
params.Add("aggregation.alignmentPeriod", calculateAlignmentPeriod(query.AlignmentPeriod, intervalMs, durationSeconds))
if query.SelectorName == "select_slo_health" {
params.Add("aggregation.perSeriesAligner", "ALIGN_MEAN")
} else {
params.Add("aggregation.perSeriesAligner", "ALIGN_NEXT_OLDER")
}
}
func calculateAlignmentPeriod(alignmentPeriod string, intervalMs int64, durationSeconds int) string {
if alignmentPeriod == "grafana-auto" || alignmentPeriod == "" {
alignmentPeriodValue := int(math.Max(float64(query.IntervalMs)/1000, 60.0))
alignmentPeriodValue := int(math.Max(float64(intervalMs)/1000, 60.0))
alignmentPeriod = "+" + strconv.Itoa(alignmentPeriodValue) + "s"
}
@ -274,24 +316,15 @@ func setAggParams(params *url.Values, query *tsdb.Query, durationSeconds int) {
}
}
params.Add("aggregation.crossSeriesReducer", crossSeriesReducer)
params.Add("aggregation.perSeriesAligner", perSeriesAligner)
params.Add("aggregation.alignmentPeriod", alignmentPeriod)
groupBys := query.Model.Get("groupBys").MustArray()
if len(groupBys) > 0 {
for i := 0; i < len(groupBys); i++ {
params.Add("aggregation.groupByFields", groupBys[i].(string))
}
}
return alignmentPeriod
}
func (e *StackdriverExecutor) executeQuery(ctx context.Context, query *StackdriverQuery, tsdbQuery *tsdb.TsdbQuery) (*tsdb.QueryResult, StackdriverResponse, error) {
func (e *StackdriverExecutor) executeQuery(ctx context.Context, query *stackdriverQuery, tsdbQuery *tsdb.TsdbQuery) (*tsdb.QueryResult, stackdriverResponse, error) {
queryResult := &tsdb.QueryResult{Meta: simplejson.New(), RefId: query.RefID}
req, err := e.createRequest(ctx, e.dsInfo, query, fmt.Sprintf("stackdriver%s", "v3/projects/"+query.ProjectName+"/timeSeries"))
if err != nil {
queryResult.Error = err
return queryResult, StackdriverResponse{}, nil
return queryResult, stackdriverResponse{}, nil
}
req.URL.RawQuery = query.Params.Encode()
@ -319,69 +352,47 @@ func (e *StackdriverExecutor) executeQuery(ctx context.Context, query *Stackdriv
opentracing.HTTPHeaders,
opentracing.HTTPHeadersCarrier(req.Header)); err != nil {
queryResult.Error = err
return queryResult, StackdriverResponse{}, nil
return queryResult, stackdriverResponse{}, nil
}
res, err := ctxhttp.Do(ctx, e.httpClient, req)
if err != nil {
queryResult.Error = err
return queryResult, StackdriverResponse{}, nil
return queryResult, stackdriverResponse{}, nil
}
data, err := e.unmarshalResponse(res)
if err != nil {
queryResult.Error = err
return queryResult, StackdriverResponse{}, nil
return queryResult, stackdriverResponse{}, nil
}
return queryResult, data, nil
}
func (e *StackdriverExecutor) unmarshalResponse(res *http.Response) (StackdriverResponse, error) {
func (e *StackdriverExecutor) unmarshalResponse(res *http.Response) (stackdriverResponse, error) {
body, err := ioutil.ReadAll(res.Body)
defer res.Body.Close()
if err != nil {
return StackdriverResponse{}, err
return stackdriverResponse{}, err
}
if res.StatusCode/100 != 2 {
slog.Error("Request failed", "status", res.Status, "body", string(body))
return StackdriverResponse{}, fmt.Errorf(string(body))
return stackdriverResponse{}, fmt.Errorf(string(body))
}
var data StackdriverResponse
var data stackdriverResponse
err = json.Unmarshal(body, &data)
if err != nil {
slog.Error("Failed to unmarshal Stackdriver response", "error", err, "status", res.Status, "body", string(body))
return StackdriverResponse{}, err
}
return data, nil
}
func (e *StackdriverExecutor) unmarshalResourceResponse(res *http.Response) (ResourceManagerProjectList, error) {
body, err := ioutil.ReadAll(res.Body)
defer res.Body.Close()
if err != nil {
return ResourceManagerProjectList{}, err
}
if res.StatusCode/100 != 2 {
slog.Error("Request failed", "status", res.Status, "body", string(body))
return ResourceManagerProjectList{}, fmt.Errorf(string(body))
}
var data ResourceManagerProjectList
err = json.Unmarshal(body, &data)
if err != nil {
slog.Error("Failed to unmarshal Resource manager response", "error", err, "status", res.Status, "body", string(body))
return ResourceManagerProjectList{}, err
return stackdriverResponse{}, err
}
return data, nil
}
func (e *StackdriverExecutor) parseResponse(queryRes *tsdb.QueryResult, data StackdriverResponse, query *StackdriverQuery) error {
func (e *StackdriverExecutor) parseResponse(queryRes *tsdb.QueryResult, data stackdriverResponse, query *stackdriverQuery) error {
labels := make(map[string]map[string]bool)
for _, series := range data.TimeSeries {
@ -389,6 +400,7 @@ func (e *StackdriverExecutor) parseResponse(queryRes *tsdb.QueryResult, data Sta
seriesLabels := make(map[string]string)
defaultMetricName := series.Metric.Type
labels["resource.type"] = map[string]bool{series.Resource.Type: true}
seriesLabels["resource.type"] = series.Resource.Type
for key, value := range series.Metric.Labels {
if _, ok := labels["metric.label."+key]; !ok {
@ -546,7 +558,7 @@ func containsLabel(labels []string, newLabel string) bool {
return false
}
func formatLegendKeys(metricType string, defaultMetricName string, labels map[string]string, additionalLabels map[string]string, query *StackdriverQuery) string {
func formatLegendKeys(metricType string, defaultMetricName string, labels map[string]string, additionalLabels map[string]string, query *stackdriverQuery) string {
if query.AliasBy == "" {
return defaultMetricName
}
@ -574,6 +586,22 @@ func formatLegendKeys(metricType string, defaultMetricName string, labels map[st
return []byte(val)
}
if metaPartName == "project" && query.ProjectName != "" {
return []byte(query.ProjectName)
}
if metaPartName == "service" && query.Service != "" {
return []byte(query.Service)
}
if metaPartName == "slo" && query.Slo != "" {
return []byte(query.Slo)
}
if metaPartName == "selector" && query.Selector != "" {
return []byte(query.Selector)
}
return in
})
@ -599,7 +627,7 @@ func replaceWithMetricPart(metaPartName string, metricType string) []byte {
return nil
}
func calcBucketBound(bucketOptions StackdriverBucketOptions, n int) string {
func calcBucketBound(bucketOptions stackdriverBucketOptions, n int) string {
bucketBound := "0"
if n == 0 {
return bucketBound
@ -615,7 +643,7 @@ func calcBucketBound(bucketOptions StackdriverBucketOptions, n int) string {
return bucketBound
}
func (e *StackdriverExecutor) createRequest(ctx context.Context, dsInfo *models.DataSource, query *StackdriverQuery, proxyPass string) (*http.Request, error) {
func (e *StackdriverExecutor) createRequest(ctx context.Context, dsInfo *models.DataSource, query *stackdriverQuery, proxyPass string) (*http.Request, error) {
u, _ := url.Parse(dsInfo.Url)
u.Path = path.Join(u.Path, "render")
@ -647,39 +675,6 @@ func (e *StackdriverExecutor) createRequest(ctx context.Context, dsInfo *models.
return req, nil
}
func (e *StackdriverExecutor) createRequestResourceManager(ctx context.Context, dsInfo *models.DataSource) (*http.Request, error) {
u, _ := url.Parse(dsInfo.Url)
u.Path = path.Join(u.Path, "render")
req, err := http.NewRequest(http.MethodGet, "https://cloudresourcemanager.googleapis.com/", nil)
if err != nil {
slog.Error("Failed to create request", "error", err)
return nil, fmt.Errorf("Failed to create request. error: %v", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", fmt.Sprintf("Grafana/%s", setting.BuildVersion))
// find plugin
plugin, ok := plugins.DataSources[dsInfo.Type]
if !ok {
return nil, errors.New("Unable to find datasource plugin Stackdriver")
}
var resourceManagerRoute *plugins.AppPluginRoute
for _, route := range plugin.Routes {
if route.Path == "cloudresourcemanager" {
resourceManagerRoute = route
break
}
}
proxyPass := "v1/projects"
pluginproxy.ApplyRoute(ctx, req, proxyPass, resourceManagerRoute, dsInfo)
return req, nil
}
func (e *StackdriverExecutor) getDefaultProject(ctx context.Context) (string, error) {
authenticationType := e.dsInfo.JsonData.Get("authenticationType").MustString(jwtAuthentication)
if authenticationType == gceAuthentication {
@ -699,55 +694,3 @@ func (e *StackdriverExecutor) getDefaultProject(ctx context.Context) (string, er
}
return e.dsInfo.JsonData.Get("defaultProject").MustString(), nil
}
func (e *StackdriverExecutor) getProjectList(ctx context.Context, tsdbQuery *tsdb.TsdbQuery) (*tsdb.Response, error) {
queryResult := &tsdb.QueryResult{Meta: simplejson.New(), RefId: tsdbQuery.Queries[0].RefId}
result := &tsdb.Response{
Results: make(map[string]*tsdb.QueryResult),
}
projectsList, err := e.getProjects(ctx)
if err != nil {
return nil, err
}
queryResult.Meta.Set("projectsList", projectsList)
result.Results[tsdbQuery.Queries[0].RefId] = queryResult
return result, nil
}
func (e *StackdriverExecutor) getProjects(ctx context.Context) ([]ResourceManagerProjectSelect, error) {
var projects []ResourceManagerProjectSelect
req, err := e.createRequestResourceManager(ctx, e.dsInfo)
if err != nil {
return nil, err
}
span, ctx := opentracing.StartSpanFromContext(ctx, "resource manager query")
span.SetTag("datasource_id", e.dsInfo.Id)
span.SetTag("org_id", e.dsInfo.OrgId)
defer span.Finish()
if err := opentracing.GlobalTracer().Inject(
span.Context(),
opentracing.HTTPHeaders,
opentracing.HTTPHeadersCarrier(req.Header)); err != nil {
return nil, err
}
res, err := ctxhttp.Do(ctx, e.httpClient, req)
if err != nil {
return nil, err
}
data, err := e.unmarshalResourceResponse(res)
if err != nil {
return nil, err
}
for _, project := range data.Projects {
projects = append(projects, ResourceManagerProjectSelect{Label: project.ProjectID, Value: project.ProjectID})
}
return projects, nil
}

@ -19,7 +19,7 @@ func TestStackdriver(t *testing.T) {
Convey("Stackdriver", t, func() {
executor := &StackdriverExecutor{}
Convey("Parse queries from frontend and build Stackdriver API queries", func() {
Convey("Parse migrated queries from frontend and build Stackdriver API queries", func() {
fromStart := time.Date(2018, 3, 15, 13, 0, 0, 0, time.UTC).In(time.Local)
tsdbQuery := &tsdb.TsdbQuery{
TimeRange: &tsdb.TimeRange{
@ -208,6 +208,99 @@ func TestStackdriver(t *testing.T) {
})
Convey("Parse queries from frontend and build Stackdriver API queries", func() {
fromStart := time.Date(2018, 3, 15, 13, 0, 0, 0, time.UTC).In(time.Local)
tsdbQuery := &tsdb.TsdbQuery{
TimeRange: &tsdb.TimeRange{
From: fmt.Sprintf("%v", fromStart.Unix()*1000),
To: fmt.Sprintf("%v", fromStart.Add(34*time.Minute).Unix()*1000),
},
Queries: []*tsdb.Query{
{
Model: simplejson.NewFromAny(map[string]interface{}{
"queryType": metricQueryType,
"metricQuery": map[string]interface{}{
"metricType": "a/metric/type",
"view": "FULL",
"aliasBy": "testalias",
"type": "timeSeriesQuery",
"groupBys": []interface{}{"metric.label.group1", "metric.label.group2"},
},
}),
RefId: "A",
},
},
}
Convey("and query type is metrics", func() {
queries, err := executor.buildQueries(tsdbQuery)
So(err, ShouldBeNil)
So(len(queries), ShouldEqual, 1)
So(queries[0].RefID, ShouldEqual, "A")
So(queries[0].Target, ShouldEqual, "aggregation.alignmentPeriod=%2B60s&aggregation.crossSeriesReducer=REDUCE_NONE&aggregation.groupByFields=metric.label.group1&aggregation.groupByFields=metric.label.group2&aggregation.perSeriesAligner=ALIGN_MEAN&filter=metric.type%3D%22a%2Fmetric%2Ftype%22&interval.endTime=2018-03-15T13%3A34%3A00Z&interval.startTime=2018-03-15T13%3A00%3A00Z&view=FULL")
So(len(queries[0].Params), ShouldEqual, 8)
So(queries[0].Params["aggregation.groupByFields"][0], ShouldEqual, "metric.label.group1")
So(queries[0].Params["aggregation.groupByFields"][1], ShouldEqual, "metric.label.group2")
So(queries[0].Params["interval.startTime"][0], ShouldEqual, "2018-03-15T13:00:00Z")
So(queries[0].Params["interval.endTime"][0], ShouldEqual, "2018-03-15T13:34:00Z")
So(queries[0].Params["aggregation.perSeriesAligner"][0], ShouldEqual, "ALIGN_MEAN")
So(queries[0].Params["filter"][0], ShouldEqual, "metric.type=\"a/metric/type\"")
So(queries[0].Params["view"][0], ShouldEqual, "FULL")
So(queries[0].AliasBy, ShouldEqual, "testalias")
So(queries[0].GroupBys, ShouldResemble, []string{"metric.label.group1", "metric.label.group2"})
})
Convey("and query type is SLOs", func() {
tsdbQuery.Queries[0].Model = simplejson.NewFromAny(map[string]interface{}{
"queryType": sloQueryType,
"metricQuery": map[string]interface{}{},
"sloQuery": map[string]interface{}{
"projectName": "test-proj",
"alignmentPeriod": "stackdriver-auto",
"perSeriesAligner": "ALIGN_NEXT_OLDER",
"aliasBy": "",
"selectorName": "select_slo_health",
"serviceId": "test-service",
"sloId": "test-slo",
},
})
queries, err := executor.buildQueries(tsdbQuery)
So(err, ShouldBeNil)
So(len(queries), ShouldEqual, 1)
So(queries[0].RefID, ShouldEqual, "A")
So(queries[0].Params["interval.startTime"][0], ShouldEqual, "2018-03-15T13:00:00Z")
So(queries[0].Params["interval.endTime"][0], ShouldEqual, "2018-03-15T13:34:00Z")
So(queries[0].Params["aggregation.alignmentPeriod"][0], ShouldEqual, `+60s`)
So(queries[0].AliasBy, ShouldEqual, "")
So(queries[0].Params["aggregation.perSeriesAligner"][0], ShouldEqual, "ALIGN_MEAN")
So(queries[0].Target, ShouldEqual, `aggregation.alignmentPeriod=%2B60s&aggregation.perSeriesAligner=ALIGN_MEAN&filter=select_slo_health%28%22projects%2Ftest-proj%2Fservices%2Ftest-service%2FserviceLevelObjectives%2Ftest-slo%22%29&interval.endTime=2018-03-15T13%3A34%3A00Z&interval.startTime=2018-03-15T13%3A00%3A00Z`)
So(len(queries[0].Params), ShouldEqual, 5)
Convey("and perSeriesAligner is inferred by SLO selector", func() {
tsdbQuery.Queries[0].Model = simplejson.NewFromAny(map[string]interface{}{
"queryType": sloQueryType,
"metricQuery": map[string]interface{}{},
"sloQuery": map[string]interface{}{
"projectName": "test-proj",
"alignmentPeriod": "stackdriver-auto",
"perSeriesAligner": "ALIGN_NEXT_OLDER",
"aliasBy": "",
"selectorName": "select_slo_compliance",
"serviceId": "test-service",
"sloId": "test-slo",
},
})
queries, err := executor.buildQueries(tsdbQuery)
So(err, ShouldBeNil)
So(queries[0].Params["aggregation.perSeriesAligner"][0], ShouldEqual, "ALIGN_NEXT_OLDER")
})
})
})
Convey("Parse stackdriver response in the time series format", func() {
Convey("when data from query aggregated to one time series", func() {
data, err := loadTestFile("./test-data/1-series-response-agg-one-metric.json")
@ -215,7 +308,7 @@ func TestStackdriver(t *testing.T) {
So(len(data.TimeSeries), ShouldEqual, 1)
res := &tsdb.QueryResult{Meta: simplejson.New(), RefId: "A"}
query := &StackdriverQuery{}
query := &stackdriverQuery{}
err = executor.parseResponse(res, data, query)
So(err, ShouldBeNil)
@ -241,7 +334,7 @@ func TestStackdriver(t *testing.T) {
So(len(data.TimeSeries), ShouldEqual, 3)
res := &tsdb.QueryResult{Meta: simplejson.New(), RefId: "A"}
query := &StackdriverQuery{}
query := &stackdriverQuery{}
err = executor.parseResponse(res, data, query)
So(err, ShouldBeNil)
@ -283,7 +376,7 @@ func TestStackdriver(t *testing.T) {
So(len(data.TimeSeries), ShouldEqual, 3)
res := &tsdb.QueryResult{Meta: simplejson.New(), RefId: "A"}
query := &StackdriverQuery{GroupBys: []string{"metric.label.instance_name", "resource.label.zone"}}
query := &stackdriverQuery{GroupBys: []string{"metric.label.instance_name", "resource.label.zone"}}
err = executor.parseResponse(res, data, query)
So(err, ShouldBeNil)
@ -304,7 +397,7 @@ func TestStackdriver(t *testing.T) {
Convey("and the alias pattern is for metric type, a metric label and a resource label", func() {
query := &StackdriverQuery{AliasBy: "{{metric.type}} - {{metric.label.instance_name}} - {{resource.label.zone}}", GroupBys: []string{"metric.label.instance_name", "resource.label.zone"}}
query := &stackdriverQuery{AliasBy: "{{metric.type}} - {{metric.label.instance_name}} - {{resource.label.zone}}", GroupBys: []string{"metric.label.instance_name", "resource.label.zone"}}
err = executor.parseResponse(res, data, query)
So(err, ShouldBeNil)
@ -318,7 +411,7 @@ func TestStackdriver(t *testing.T) {
Convey("and the alias pattern is for metric name", func() {
query := &StackdriverQuery{AliasBy: "metric {{metric.name}} service {{metric.service}}", GroupBys: []string{"metric.label.instance_name", "resource.label.zone"}}
query := &stackdriverQuery{AliasBy: "metric {{metric.name}} service {{metric.service}}", GroupBys: []string{"metric.label.instance_name", "resource.label.zone"}}
err = executor.parseResponse(res, data, query)
So(err, ShouldBeNil)
@ -337,7 +430,7 @@ func TestStackdriver(t *testing.T) {
So(len(data.TimeSeries), ShouldEqual, 1)
res := &tsdb.QueryResult{Meta: simplejson.New(), RefId: "A"}
query := &StackdriverQuery{AliasBy: "{{bucket}}"}
query := &stackdriverQuery{AliasBy: "{{bucket}}"}
err = executor.parseResponse(res, data, query)
So(err, ShouldBeNil)
@ -384,7 +477,7 @@ func TestStackdriver(t *testing.T) {
So(len(data.TimeSeries), ShouldEqual, 1)
res := &tsdb.QueryResult{Meta: simplejson.New(), RefId: "A"}
query := &StackdriverQuery{AliasBy: "{{bucket}}"}
query := &stackdriverQuery{AliasBy: "{{bucket}}"}
err = executor.parseResponse(res, data, query)
So(err, ShouldBeNil)
@ -424,7 +517,7 @@ func TestStackdriver(t *testing.T) {
So(len(data.TimeSeries), ShouldEqual, 3)
res := &tsdb.QueryResult{Meta: simplejson.New(), RefId: "A"}
query := &StackdriverQuery{AliasBy: "{{bucket}}"}
query := &stackdriverQuery{AliasBy: "{{bucket}}"}
err = executor.parseResponse(res, data, query)
labels := res.Meta.Get("labels").Interface().(map[string][]string)
So(err, ShouldBeNil)
@ -463,7 +556,7 @@ func TestStackdriver(t *testing.T) {
Convey("and systemlabel contains key with array of string", func() {
res := &tsdb.QueryResult{Meta: simplejson.New(), RefId: "A"}
query := &StackdriverQuery{AliasBy: "{{metadata.system_labels.test}}"}
query := &stackdriverQuery{AliasBy: "{{metadata.system_labels.test}}"}
err = executor.parseResponse(res, data, query)
So(err, ShouldBeNil)
So(len(res.Series), ShouldEqual, 3)
@ -475,7 +568,7 @@ func TestStackdriver(t *testing.T) {
Convey("and systemlabel contains key with array of string2", func() {
res := &tsdb.QueryResult{Meta: simplejson.New(), RefId: "A"}
query := &StackdriverQuery{AliasBy: "{{metadata.system_labels.test2}}"}
query := &stackdriverQuery{AliasBy: "{{metadata.system_labels.test2}}"}
err = executor.parseResponse(res, data, query)
So(err, ShouldBeNil)
So(len(res.Series), ShouldEqual, 3)
@ -483,6 +576,45 @@ func TestStackdriver(t *testing.T) {
So(res.Series[2].Name, ShouldEqual, "testvalue")
})
})
Convey("when data from query returns slo and alias by is defined", func() {
data, err := loadTestFile("./test-data/6-series-response-slo.json")
So(err, ShouldBeNil)
So(len(data.TimeSeries), ShouldEqual, 1)
Convey("and alias by is expanded", func() {
res := &tsdb.QueryResult{Meta: simplejson.New(), RefId: "A"}
query := &stackdriverQuery{
ProjectName: "test-proj",
Selector: "select_slo_compliance",
Service: "test-service",
Slo: "test-slo",
AliasBy: "{{project}} - {{service}} - {{slo}} - {{selector}}",
}
err = executor.parseResponse(res, data, query)
So(err, ShouldBeNil)
So(res.Series[0].Name, ShouldEqual, "test-proj - test-service - test-slo - select_slo_compliance")
})
})
Convey("when data from query returns slo and alias by is not defined", func() {
data, err := loadTestFile("./test-data/6-series-response-slo.json")
So(err, ShouldBeNil)
So(len(data.TimeSeries), ShouldEqual, 1)
Convey("and alias by is expanded", func() {
res := &tsdb.QueryResult{Meta: simplejson.New(), RefId: "A"}
query := &stackdriverQuery{
ProjectName: "test-proj",
Selector: "select_slo_compliance",
Service: "test-service",
Slo: "test-slo",
}
err = executor.parseResponse(res, data, query)
So(err, ShouldBeNil)
So(res.Series[0].Name, ShouldEqual, "select_slo_compliance(\"projects/test-proj/services/test-service/serviceLevelObjectives/test-slo\")")
})
})
})
Convey("when interpolating filter wildcards", func() {
@ -550,20 +682,20 @@ func TestStackdriver(t *testing.T) {
Convey("when building filter string", func() {
Convey("and theres no regex operator", func() {
Convey("and there are wildcards in a filter value", func() {
filterParts := []interface{}{"zone", "=", "*-central1*"}
filterParts := []string{"zone", "=", "*-central1*"}
value := buildFilterString("somemetrictype", filterParts)
So(value, ShouldEqual, `metric.type="somemetrictype" zone=has_substring("-central1")`)
})
Convey("and there are no wildcards in any filter value", func() {
filterParts := []interface{}{"zone", "!=", "us-central1-a"}
filterParts := []string{"zone", "!=", "us-central1-a"}
value := buildFilterString("somemetrictype", filterParts)
So(value, ShouldEqual, `metric.type="somemetrictype" zone!="us-central1-a"`)
})
})
Convey("and there is a regex operator", func() {
filterParts := []interface{}{"zone", "=~", "us-central1-a~"}
filterParts := []string{"zone", "=~", "us-central1-a~"}
value := buildFilterString("somemetrictype", filterParts)
Convey("it should remove the ~ character from the operator that belongs to the value", func() {
So(value, ShouldNotContainSubstring, `=~`)
@ -578,8 +710,8 @@ func TestStackdriver(t *testing.T) {
})
}
func loadTestFile(path string) (StackdriverResponse, error) {
var data StackdriverResponse
func loadTestFile(path string) (stackdriverResponse, error) {
var data stackdriverResponse
jsonBody, err := ioutil.ReadFile(path)
if err != nil {

@ -0,0 +1,17 @@
{
"timeSeries": [{
"metric": {
"type": "select_slo_compliance(\"projects/test-proj/services/test-service/serviceLevelObjectives/test-slo\")"
},
"resource": {
"type": "gce_instance",
"labels": {
"instance_id": "114250375703598695",
"project_id": "test-proj"
}
},
"metricKind": "DELTA",
"valueType": "INT64"
}
]
}

@ -6,17 +6,49 @@ import (
)
type (
// StackdriverQuery is the query that Grafana sends from the frontend
StackdriverQuery struct {
stackdriverQuery struct {
Target string
Params url.Values
RefID string
GroupBys []string
AliasBy string
ProjectName string
Selector string
Service string
Slo string
}
StackdriverBucketOptions struct {
metricQuery struct {
ProjectName string
MetricType string
CrossSeriesReducer string
AlignmentPeriod string
PerSeriesAligner string
GroupBys []string
Filters []string
AliasBy string
View string
}
sloQuery struct {
ProjectName string
AlignmentPeriod string
PerSeriesAligner string
AliasBy string
SelectorName string
ServiceId string
SloId string
}
grafanaQuery struct {
DatasourceId int
RefId string
QueryType string
MetricQuery metricQuery
SloQuery sloQuery
}
stackdriverBucketOptions struct {
LinearBuckets *struct {
NumFiniteBuckets int64 `json:"numFiniteBuckets"`
Width int64 `json:"width"`
@ -32,8 +64,7 @@ type (
} `json:"explicitBuckets"`
}
// StackdriverResponse is the data returned from the external Google Stackdriver API
StackdriverResponse struct {
stackdriverResponse struct {
TimeSeries []struct {
Metric struct {
Labels map[string]string `json:"labels"`
@ -64,7 +95,7 @@ type (
Min int `json:"min"`
Max int `json:"max"`
} `json:"range"`
BucketOptions StackdriverBucketOptions `json:"bucketOptions"`
BucketOptions stackdriverBucketOptions `json:"bucketOptions"`
BucketCounts []string `json:"bucketCounts"`
Examplars []struct {
Value float64 `json:"value"`
@ -76,18 +107,4 @@ type (
} `json:"points"`
} `json:"timeSeries"`
}
// ResourceManagerProjectList is the data returned from the external Google Resource Manager API
ResourceManagerProjectList struct {
Projects []ResourceManagerProject `json:"projects"`
}
ResourceManagerProject struct {
ProjectID string `json:"projectId"`
}
ResourceManagerProjectSelect struct {
Label string `json:"label"`
Value string `json:"value"`
}
)

@ -1,7 +1,8 @@
import isString from 'lodash/isString';
import { alignmentPeriods, ValueTypes, MetricKind } from './constants';
import { alignmentPeriods, ValueTypes, MetricKind, selectors } from './constants';
import StackdriverDatasource from './datasource';
import { MetricFindQueryTypes, VariableQueryData } from './types';
import { SelectableValue } from '@grafana/data';
import {
getMetricTypesByService,
getAlignmentOptionsByMetric,
@ -38,6 +39,12 @@ export default class StackdriverMetricFindQuery {
return this.handleAlignmentPeriodQuery();
case MetricFindQueryTypes.Aggregations:
return this.handleAggregationQuery(query);
case MetricFindQueryTypes.SLOServices:
return this.handleSLOServicesQuery(query);
case MetricFindQueryTypes.SLO:
return this.handleSLOQuery(query);
case MetricFindQueryTypes.Selectors:
return this.handleSelectorQuery();
default:
return [];
}
@ -49,7 +56,7 @@ export default class StackdriverMetricFindQuery {
async handleProjectsQuery() {
const projects = await this.datasource.getProjects();
return projects.map((s: { label: string; value: string }) => ({
return (projects as SelectableValue<string>).map((s: { label: string; value: string }) => ({
text: s.label,
value: s.value,
expandable: true,
@ -130,6 +137,20 @@ export default class StackdriverMetricFindQuery {
return getAggregationOptionsByMetric(valueType as ValueTypes, metricKind as MetricKind).map(this.toFindQueryResult);
}
async handleSLOServicesQuery({ projectName }: VariableQueryData) {
const services = await this.datasource.getSLOServices(projectName);
return services.map(this.toFindQueryResult);
}
async handleSLOQuery({ selectedSLOService, projectName }: VariableQueryData) {
const slos = await this.datasource.getServiceLevelObjectives(projectName, selectedSLOService);
return slos.map(this.toFindQueryResult);
}
async handleSelectorQuery() {
return selectors.map(this.toFindQueryResult);
}
handleAlignmentPeriodQuery() {
return alignmentPeriods.map(this.toFindQueryResult);
}

@ -0,0 +1,69 @@
import Api from './api';
import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__
import { SelectableValue } from '@grafana/data';
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getBackendSrv: () => backendSrv,
}));
const response = [
{ label: 'test1', value: 'test1' },
{ label: 'test2', value: 'test2' },
];
describe('api', () => {
const datasourceRequestMock = jest.spyOn(backendSrv, 'datasourceRequest');
beforeEach(() => {
datasourceRequestMock.mockImplementation((options: any) => {
const data = { [options.url.match(/([^\/]*)\/*$/)[1]]: response };
return Promise.resolve({ data, status: 200 });
});
});
describe('when resource was cached', () => {
let api: Api;
let res: Array<SelectableValue<string>>;
beforeEach(async () => {
api = new Api('/stackdriver/');
api.cache['some-resource'] = response;
res = await api.get('some-resource');
});
it('should return cached value and not load from source', () => {
expect(res).toEqual(response);
expect(api.cache['some-resource']).toEqual(response);
expect(datasourceRequestMock).not.toHaveBeenCalled();
});
});
describe('when resource was not cached', () => {
let api: Api;
let res: Array<SelectableValue<string>>;
beforeEach(async () => {
api = new Api('/stackdriver/');
res = await api.get('some-resource');
});
it('should return cached value and not load from source', () => {
expect(res).toEqual(response);
expect(api.cache['some-resource']).toEqual(response);
expect(datasourceRequestMock).toHaveBeenCalled();
});
});
describe('when cache should be bypassed', () => {
let api: Api;
let res: Array<SelectableValue<string>>;
beforeEach(async () => {
api = new Api('/stackdriver/');
api.cache['some-resource'] = response;
res = await api.get('some-resource', { useCache: false });
});
it('should return cached value and not load from source', () => {
expect(res).toEqual(response);
expect(datasourceRequestMock).toHaveBeenCalled();
});
});
});

@ -0,0 +1,72 @@
import appEvents from 'app/core/app_events';
import { CoreEvents } from 'app/types';
import { SelectableValue } from '@grafana/data';
import { getBackendSrv } from '@grafana/runtime';
import { formatStackdriverError } from './functions';
import { MetricDescriptor } from './types';
interface Options {
responseMap?: (res: any) => SelectableValue<string> | MetricDescriptor;
baseUrl?: string;
useCache?: boolean;
}
export default class Api {
cache: { [key: string]: Array<SelectableValue<string>> };
defaultOptions: Options;
constructor(private baseUrl: string) {
this.cache = {};
this.defaultOptions = {
useCache: true,
responseMap: (res: any) => res,
baseUrl: this.baseUrl,
};
}
async get(path: string, options?: Options): Promise<Array<SelectableValue<string>> | MetricDescriptor[]> {
try {
const { useCache, responseMap, baseUrl } = { ...this.defaultOptions, ...options };
if (useCache && this.cache[path]) {
return this.cache[path];
}
const response = await getBackendSrv().datasourceRequest({
url: baseUrl + path,
method: 'GET',
});
const responsePropName = path.match(/([^\/]*)\/*$/)[1];
let res = [];
if (response && response.data && response.data[responsePropName]) {
res = response.data[responsePropName].map(responseMap);
}
if (useCache) {
this.cache[path] = res;
}
return res;
} catch (error) {
appEvents.emit(CoreEvents.dsRequestError, { error: { data: { error: formatStackdriverError(error) } } });
return [];
}
}
async post(data: { [key: string]: any }) {
return getBackendSrv().datasourceRequest({
url: '/api/tsdb/query',
method: 'POST',
data,
});
}
async test(projectName: string) {
return getBackendSrv().datasourceRequest({
url: `${this.baseUrl}${projectName}/metricDescriptors`,
method: 'GET',
});
}
}

@ -5,10 +5,9 @@ import { SelectableValue } from '@grafana/data';
import { Segment } from '@grafana/ui';
import { getAggregationOptionsByMetric } from '../functions';
import { ValueTypes, MetricKind } from '../constants';
import { MetricDescriptor } from '../types';
export interface Props {
onChange: (metricDescriptor: MetricDescriptor[]) => void;
onChange: (metricDescriptor: string) => void;
metricDescriptor: {
valueType: string;
metricKind: string;
@ -92,7 +91,7 @@ export class Aggregations extends React.Component<Props, State> {
</label>
</div>
</div>
{this.props.children(this.state.displayAdvancedOptions)}
{this.props.children && this.props.children(this.state.displayAdvancedOptions)}
</>
);
}

@ -1,53 +1,25 @@
import React, { Component } from 'react';
import React, { FunctionComponent, useState } from 'react';
import { debounce } from 'lodash';
import { Input } from '@grafana/ui';
import { QueryInlineField } from '.';
export interface Props {
onChange: (alignmentPeriod: string) => void;
onChange: (alias: any) => void;
value: string;
}
export interface State {
value: string;
}
export class AliasBy extends Component<Props, State> {
propagateOnChange: (value: any) => void;
export const AliasBy: FunctionComponent<Props> = ({ value = '', onChange }) => {
const [alias, setAlias] = useState(value);
constructor(props: Props) {
super(props);
this.propagateOnChange = debounce(this.props.onChange, 500);
this.state = { value: '' };
}
const propagateOnChange = debounce(onChange, 1000);
componentDidMount() {
this.setState({ value: this.props.value });
}
UNSAFE_componentWillReceiveProps(nextProps: Props) {
if (nextProps.value !== this.props.value) {
this.setState({ value: nextProps.value });
}
}
onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ value: e.target.value });
this.propagateOnChange(e.target.value);
onChange = (e: any) => {
setAlias(e.target.value);
propagateOnChange(e.target.value);
};
render() {
return (
<>
<div className="gf-form-inline">
<div className="gf-form">
<label className="gf-form-label query-keyword width-9">Alias By</label>
<Input type="text" className="gf-form-input width-24" value={this.state.value} onChange={this.onChange} />
</div>
<div className="gf-form gf-form--grow">
<div className="gf-form-label gf-form-label--grow" />
</div>
</div>
</>
);
}
}
return (
<QueryInlineField label="Alias By">
<input type="text" className="gf-form-input width-26" value={alias} onChange={onChange} />
</QueryInlineField>
);
};

@ -36,7 +36,7 @@ export const AlignmentPeriods: FC<Props> = ({
<div className="gf-form-inline">
<label className="gf-form-label query-keyword width-9">Alignment Period</label>
<Segment
onChange={({ value }) => onChange(value)}
onChange={({ value }) => onChange(value!)}
value={[...options, ...templateVariableOptions].find(s => s.value === alignmentPeriod)}
options={[
{

@ -17,7 +17,7 @@ export const Alignments: FC<Props> = ({ perSeriesAligner, templateVariableOption
<div className="gf-form offset-width-9">
<label className="gf-form-label query-keyword width-15">Aligner</label>
<Segment
onChange={({ value }) => onChange(value)}
onChange={({ value }) => onChange(value!)}
value={[...alignOptions, ...templateVariableOptions].find(s => s.value === perSeriesAligner)}
options={[
{

@ -5,7 +5,7 @@ import { TemplateSrv } from 'app/features/templating/template_srv';
import { SelectableValue } from '@grafana/data';
import StackdriverDatasource from '../datasource';
import { Metrics, Filters, AnnotationsHelp, Project } from './';
import { Metrics, LabelFilter, AnnotationsHelp, Project } from './';
import { toOption } from '../functions';
import { AnnotationTarget, MetricDescriptor } from '../types';
@ -41,7 +41,9 @@ const DefaultTarget: State = {
export class AnnotationQueryEditor extends React.Component<Props, State> {
state: State = DefaultTarget;
async componentDidMount() {
async UNSAFE_componentWillMount() {
// Unfortunately, migrations like this need to go componentWillMount. As soon as there's
// migration hook for this module.ts, we can do the migrations there instead.
const { target, datasource } = this.props;
if (!target.projectName) {
target.projectName = datasource.getDefaultProject();
@ -86,7 +88,7 @@ export class AnnotationQueryEditor extends React.Component<Props, State> {
}
render() {
const { projectName, metricType, filters, title, text, variableOptionGroup, labels, variableOptions } = this.state;
const { metricType, projectName, filters, title, text, variableOptionGroup, labels, variableOptions } = this.state;
const { datasource } = this.props;
return (
@ -107,7 +109,7 @@ export class AnnotationQueryEditor extends React.Component<Props, State> {
>
{metric => (
<>
<Filters
<LabelFilter
labels={labels}
filters={filters}
onChange={value => this.onChange('filters', value)}

@ -0,0 +1,28 @@
import React, { InputHTMLAttributes, FunctionComponent } from 'react';
import { FormLabel } from '@grafana/ui';
export interface Props extends InputHTMLAttributes<HTMLInputElement> {
label: string;
tooltip?: string;
children?: React.ReactNode;
}
export const QueryField: FunctionComponent<Partial<Props>> = ({ label, tooltip, children }) => (
<>
<FormLabel width={9} className="query-keyword" tooltip={tooltip}>
{label}
</FormLabel>
{children}
</>
);
export const QueryInlineField: FunctionComponent<Props> = ({ ...props }) => {
return (
<div className={'gf-form-inline'}>
<QueryField {...props} />
<div className="gf-form gf-form--grow">
<div className="gf-form-label gf-form-label--grow" />
</div>
</div>
);
};

@ -26,7 +26,7 @@ export const GroupBys: FunctionComponent<Props> = ({ groupBys = [], values = [],
key={value + index}
value={value}
options={options}
onChange={({ value }) =>
onChange={({ value = '' }) =>
onChange(
value === removeText
? values.filter((_, i) => i !== index)
@ -43,7 +43,7 @@ export const GroupBys: FunctionComponent<Props> = ({ groupBys = [], values = [],
</a>
}
allowCustomValue
onChange={({ value }) => onChange([...values, value])}
onChange={({ value = '' }) => onChange([...values, value])}
options={[
variableOptionGroup,
...labelsToGroupedOptions([...groupBys.filter(groupBy => !values.includes(groupBy)), ...systemLabels]),

@ -105,6 +105,18 @@ export class Help extends React.Component<Props, State> {
<code>{`${'{{bucket}}'}`}</code> = bucket boundary for distribution metrics when using a heatmap in
Grafana
</li>
<li>
<code>{`${'{{project}}'}`}</code> = The project name that was specified in the query editor
</li>
<li>
<code>{`${'{{service}}'}`}</code> = The service id that was specified in the SLO query editor
</li>
<li>
<code>{`${'{{slo}}'}`}</code> = The SLO id that was specified in the SLO query editor
</li>
<li>
<code>{`${'{{selector}}'}`}</code> = The Selector function that was specified in the SLO query editor
</li>
</ul>
</div>
</div>

@ -2,7 +2,7 @@ import React, { FunctionComponent, Fragment } from 'react';
import _ from 'lodash';
import { SelectableValue } from '@grafana/data';
import { Segment } from '@grafana/ui';
import { labelsToGroupedOptions, toOption } from '../functions';
import { labelsToGroupedOptions, filtersToStringArray, stringArrayToFilters, toOption } from '../functions';
import { Filter } from '../types';
export interface Props {
@ -15,18 +15,8 @@ export interface Props {
const removeText = '-- remove filter --';
const removeOption: SelectableValue<string> = { label: removeText, value: removeText, icon: 'fa fa-remove' };
const operators = ['=', '!=', '=~', '!=~'];
const filtersToStringArray = (filters: Filter[]) =>
_.flatten(filters.map(({ key, operator, value, condition }) => [key, operator, value, condition]));
const stringArrayToFilters = (filterArray: string[]) =>
_.chunk(filterArray, 4).map(([key, operator, value, condition = 'AND']) => ({
key,
operator,
value,
condition,
}));
export const Filters: FunctionComponent<Props> = ({
export const LabelFilter: FunctionComponent<Props> = ({
labels = {},
filters: filterArray,
onChange,
@ -45,7 +35,7 @@ export const Filters: FunctionComponent<Props> = ({
allowCustomValue
value={key}
options={options}
onChange={({ value: key }) => {
onChange={({ value: key = '' }) => {
if (key === removeText) {
onChange(filtersToStringArray(filters.filter((_, i) => i !== index)));
} else {
@ -61,7 +51,7 @@ export const Filters: FunctionComponent<Props> = ({
value={operator}
className="gf-form-label query-segment-operator"
options={operators.map(toOption)}
onChange={({ value: operator }) =>
onChange={({ value: operator = '=' }) =>
onChange(filtersToStringArray(filters.map((f, i) => (i === index ? { ...f, operator } : f))))
}
/>
@ -72,7 +62,7 @@ export const Filters: FunctionComponent<Props> = ({
options={
labels.hasOwnProperty(key) ? [variableOptionGroup, ...labels[key].map(toOption)] : [variableOptionGroup]
}
onChange={({ value }) =>
onChange={({ value = '' }) =>
onChange(filtersToStringArray(filters.map((f, i) => (i === index ? { ...f, value } : f))))
}
/>
@ -90,7 +80,7 @@ export const Filters: FunctionComponent<Props> = ({
</a>
}
options={[variableOptionGroup, ...labelsToGroupedOptions(Object.keys(labels))]}
onChange={({ value: key }) =>
onChange={({ value: key = '' }) =>
onChange(filtersToStringArray([...filters, { key, operator: '=', condition: 'AND', value: '' } as Filter]))
}
/>

@ -0,0 +1,140 @@
import React, { useState, useEffect } from 'react';
import { Project, Aggregations, Metrics, LabelFilter, GroupBys, Alignments, AlignmentPeriods, AliasBy } from '.';
import { MetricQuery, MetricDescriptor } from '../types';
import { getAlignmentPickerData } from '../functions';
import StackdriverDatasource from '../datasource';
import { SelectableValue } from '@grafana/data';
export interface Props {
refId: string;
usedAlignmentPeriod: string;
variableOptionGroup: SelectableValue<string>;
onChange: (query: MetricQuery) => void;
onRunQuery: () => void;
query: MetricQuery;
datasource: StackdriverDatasource;
}
interface State {
labels: any;
[key: string]: any;
}
export const defaultState: State = {
labels: {},
};
export const defaultQuery: MetricQuery = {
projectName: '',
metricType: '',
metricKind: '',
valueType: '',
unit: '',
crossSeriesReducer: 'REDUCE_MEAN',
alignmentPeriod: 'stackdriver-auto',
perSeriesAligner: 'ALIGN_MEAN',
groupBys: [],
filters: [],
aliasBy: '',
};
function Editor({
refId,
query,
datasource,
onChange,
usedAlignmentPeriod,
variableOptionGroup,
}: React.PropsWithChildren<Props>) {
const [state, setState] = useState<State>(defaultState);
useEffect(() => {
if (query && query.projectName && query.metricType) {
datasource
.getLabels(query.metricType, refId, query.projectName, query.groupBys)
.then(labels => setState({ ...state, labels }));
}
}, [query.projectName, query.groupBys, query.metricType]);
const onMetricTypeChange = async ({ valueType, metricKind, type, unit }: MetricDescriptor) => {
const { perSeriesAligner, alignOptions } = getAlignmentPickerData(
{ valueType, metricKind, perSeriesAligner: state.perSeriesAligner },
datasource.templateSrv
);
setState({
...state,
alignOptions,
});
onChange({ ...query, perSeriesAligner, metricType: type, unit, valueType, metricKind });
};
const { labels } = state;
const { perSeriesAligner, alignOptions } = getAlignmentPickerData(query, datasource.templateSrv);
return (
<>
<Project
templateVariableOptions={variableOptionGroup.options}
projectName={query.projectName}
datasource={datasource}
onChange={projectName => {
onChange({ ...query, projectName });
}}
/>
<Metrics
templateSrv={datasource.templateSrv}
projectName={query.projectName}
metricType={query.metricType}
templateVariableOptions={variableOptionGroup.options}
datasource={datasource}
onChange={onMetricTypeChange}
>
{metric => (
<>
<LabelFilter
labels={labels}
filters={query.filters!}
onChange={filters => onChange({ ...query, filters })}
variableOptionGroup={variableOptionGroup}
/>
<GroupBys
groupBys={Object.keys(labels)}
values={query.groupBys!}
onChange={groupBys => onChange({ ...query, groupBys })}
variableOptionGroup={variableOptionGroup}
/>
<Aggregations
metricDescriptor={metric}
templateVariableOptions={variableOptionGroup.options}
crossSeriesReducer={query.crossSeriesReducer}
groupBys={query.groupBys!}
onChange={crossSeriesReducer => onChange({ ...query, crossSeriesReducer })}
>
{displayAdvancedOptions =>
displayAdvancedOptions && (
<Alignments
alignOptions={alignOptions}
templateVariableOptions={variableOptionGroup.options}
perSeriesAligner={perSeriesAligner || ''}
onChange={perSeriesAligner => onChange({ ...query, perSeriesAligner })}
/>
)
}
</Aggregations>
<AlignmentPeriods
templateSrv={datasource.templateSrv}
templateVariableOptions={variableOptionGroup.options}
alignmentPeriod={query.alignmentPeriod || ''}
perSeriesAligner={query.perSeriesAligner || ''}
usedAlignmentPeriod={usedAlignmentPeriod}
onChange={alignmentPeriod => onChange({ ...query, alignmentPeriod })}
/>
<AliasBy value={query.aliasBy || ''} onChange={aliasBy => onChange({ ...query, aliasBy })} />
</>
)}
</Metrics>
</>
);
}
export const MetricQueryEditor = React.memo(Editor);

@ -81,8 +81,8 @@ export function Metrics(props: Props) {
const { metricType, templateSrv } = props;
const metrics = metricDescriptors
.filter(m => m.service === templateSrv.replace(service))
.map(m => ({
.filter((m: MetricDescriptor) => m.service === templateSrv.replace(service))
.map((m: MetricDescriptor) => ({
service: m.service,
value: m.type,
label: m.displayName,
@ -96,7 +96,7 @@ export function Metrics(props: Props) {
}
};
const onMetricTypeChange = ({ value }: any, extra: any = {}) => {
const onMetricTypeChange = ({ value }: SelectableValue<string>, extra: any = {}) => {
const metricDescriptor = getSelectedMetricDescriptor(state.metricDescriptors, value);
setState({ ...state, metricDescriptor, ...extra });
props.onChange({ ...metricDescriptor, type: value });

@ -1,27 +0,0 @@
import React from 'react';
import renderer from 'react-test-renderer';
import { DefaultTarget, Props, QueryEditor } from './QueryEditor';
import { TemplateSrv } from 'app/features/templating/template_srv';
const props: Props = {
onQueryChange: target => {},
onExecuteQuery: () => {},
target: DefaultTarget,
events: { on: () => {} },
datasource: {
getProjects: () => Promise.resolve([]),
getDefaultProject: () => Promise.resolve('projectName'),
ensureGCEDefaultProject: () => {},
getMetricTypes: () => Promise.resolve([]),
getLabels: () => Promise.resolve([]),
variables: [],
} as any,
templateSrv: new TemplateSrv(),
};
describe('QueryEditor', () => {
it('renders correctly', () => {
const tree = renderer.create(<QueryEditor {...props} />).toJSON();
expect(tree).toMatchSnapshot();
});
});

@ -1,258 +1,113 @@
import React from 'react';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { Project, Aggregations, Metrics, Filters, GroupBys, Alignments, AlignmentPeriods, AliasBy, Help } from './';
import { StackdriverQuery, MetricDescriptor } from '../types';
import { getAlignmentPickerData, toOption } from '../functions';
import React, { PureComponent } from 'react';
import appEvents from 'app/core/app_events';
import { CoreEvents } from 'app/types';
import { MetricQueryEditor, QueryTypeSelector, SLOQueryEditor, Help } from './';
import { StackdriverQuery, MetricQuery, QueryType, SLOQuery } from '../types';
import { defaultQuery } from './MetricQueryEditor';
import { defaultQuery as defaultSLOQuery } from './SLOQueryEditor';
import { toOption, formatStackdriverError } from '../functions';
import StackdriverDatasource from '../datasource';
import { PanelEvents, SelectableValue, TimeSeries } from '@grafana/data';
import { ExploreQueryFieldProps } from '@grafana/data';
export interface Props {
onQueryChange: (target: StackdriverQuery) => void;
onExecuteQuery: () => void;
target: StackdriverQuery;
events: any;
datasource: StackdriverDatasource;
templateSrv: TemplateSrv;
}
export type Props = ExploreQueryFieldProps<StackdriverDatasource, StackdriverQuery>;
interface State extends StackdriverQuery {
variableOptions: Array<SelectableValue<string>>;
variableOptionGroup: SelectableValue<string>;
alignOptions: Array<SelectableValue<string>>;
lastQuery: string;
interface State {
lastQueryError: string;
labels: any;
[key: string]: any;
}
export const DefaultTarget: State = {
projectName: '',
metricType: '',
metricKind: '',
valueType: '',
refId: '',
service: '',
unit: '',
crossSeriesReducer: 'REDUCE_MEAN',
alignmentPeriod: 'stackdriver-auto',
perSeriesAligner: 'ALIGN_MEAN',
groupBys: [],
filters: [],
filter: [],
aliasBy: '',
alignOptions: [],
lastQuery: '',
lastQueryError: '',
usedAlignmentPeriod: '',
labels: {},
variableOptionGroup: {},
variableOptions: [],
};
export class QueryEditor extends PureComponent<Props, State> {
state: State = { lastQueryError: '' };
export class QueryEditor extends React.Component<Props, State> {
state: State = DefaultTarget;
async UNSAFE_componentWillMount() {
const { datasource, query } = this.props;
async componentDidMount() {
const { events, target, templateSrv, datasource } = this.props;
await datasource.ensureGCEDefaultProject();
if (!target.projectName) {
target.projectName = datasource.getDefaultProject();
// Unfortunately, migrations like this need to go componentWillMount. As soon as there's
// migration hook for this module.ts, we can do the migrations there instead.
if (!this.props.query.hasOwnProperty('metricQuery')) {
const { hide, refId, datasource, key, queryType, maxLines, metric, ...metricQuery } = this.props.query as any;
this.props.query.metricQuery = metricQuery;
}
events.on(PanelEvents.dataReceived, this.onDataReceived.bind(this));
events.on(PanelEvents.dataError, this.onDataError.bind(this));
const { perSeriesAligner, alignOptions } = getAlignmentPickerData(target, templateSrv);
const variableOptionGroup = {
label: 'Template Variables',
expanded: false,
options: datasource.variables.map(toOption),
};
const state: Partial<State> = {
...this.props.target,
projectName: target.projectName,
alignOptions,
perSeriesAligner,
variableOptionGroup,
variableOptions: variableOptionGroup.options,
};
this.setState(state);
if (!this.props.query.hasOwnProperty('queryType')) {
this.props.query.queryType = QueryType.METRICS;
}
datasource
.getLabels(target.metricType, target.refId, target.projectName, target.groupBys)
.then(labels => this.setState({ labels }));
await datasource.ensureGCEDefaultProject();
if (!query.metricQuery.projectName) {
this.props.query.metricQuery.projectName = datasource.getDefaultProject();
}
}
componentWillUnmount() {
this.props.events.off(PanelEvents.dataReceived, this.onDataReceived);
this.props.events.off(PanelEvents.dataError, this.onDataError);
componentDidMount() {
appEvents.on(CoreEvents.dsRequestError, this.onDataError.bind(this));
appEvents.on(CoreEvents.dsRequestResponse, this.onDataResponse.bind(this));
}
onDataReceived(dataList: TimeSeries[]) {
const series = dataList.find((item: any) => item.refId === this.props.target.refId);
if (series) {
this.setState({
lastQuery: decodeURIComponent(series.meta.rawQuery),
lastQueryError: '',
usedAlignmentPeriod: series.meta.alignmentPeriod,
});
}
componentWillUnmount() {
appEvents.off(CoreEvents.dsRequestResponse, this.onDataResponse.bind(this));
appEvents.on(CoreEvents.dsRequestError, this.onDataError.bind(this));
}
onDataError(err: any) {
let lastQuery;
let lastQueryError;
if (err.data && err.data.error) {
lastQueryError = this.props.datasource.formatStackdriverError(err);
} else if (err.data && err.data.results) {
const queryRes = err.data.results[this.props.target.refId];
lastQuery = decodeURIComponent(queryRes.meta.rawQuery);
if (queryRes && queryRes.error) {
try {
lastQueryError = JSON.parse(queryRes.error).error.message;
} catch {
lastQueryError = queryRes.error;
}
}
}
this.setState({ lastQuery, lastQueryError });
onDataResponse() {
this.setState({ lastQueryError: '' });
}
onMetricTypeChange = async ({ valueType, metricKind, type, unit }: MetricDescriptor) => {
const { templateSrv, onQueryChange, onExecuteQuery, target } = this.props;
const { perSeriesAligner, alignOptions } = getAlignmentPickerData(
{ valueType, metricKind, perSeriesAligner: this.state.perSeriesAligner },
templateSrv
);
const labels = await this.props.datasource.getLabels(type, target.refId, this.state.projectName, target.groupBys);
this.setState(
{
alignOptions,
perSeriesAligner,
metricType: type,
unit,
valueType,
metricKind,
labels,
},
() => {
onQueryChange(this.state);
if (this.state.projectName !== null) {
onExecuteQuery();
}
}
);
};
onGroupBysChange(value: string[]) {
const { target, datasource } = this.props;
this.setState({ groupBys: value }, () => {
this.props.onQueryChange(this.state);
this.props.onExecuteQuery();
});
datasource
.getLabels(target.metricType, target.refId, this.state.projectName, value)
.then(labels => this.setState({ labels }));
onDataError(error: any) {
this.setState({ lastQueryError: formatStackdriverError(error) });
}
onPropertyChange(prop: string, value: any) {
this.setState({ [prop]: value }, () => {
this.props.onQueryChange(this.state);
if (this.state.projectName !== null) {
this.props.onExecuteQuery();
}
});
onQueryChange(prop: string, value: any) {
this.props.onChange({ ...this.props.query, [prop]: value });
this.props.onRunQuery();
}
render() {
const {
groupBys = [],
filters = [],
usedAlignmentPeriod,
projectName,
metricType,
crossSeriesReducer,
perSeriesAligner,
alignOptions,
alignmentPeriod,
aliasBy,
lastQuery,
lastQueryError,
labels,
variableOptionGroup,
variableOptions,
refId,
} = this.state;
const { datasource, templateSrv } = this.props;
const { datasource, query, onRunQuery, onChange } = this.props;
const metricQuery = { ...defaultQuery, projectName: datasource.getDefaultProject(), ...query.metricQuery };
const sloQuery = { ...defaultSLOQuery, projectName: datasource.getDefaultProject(), ...query.sloQuery };
const queryType = query.queryType || QueryType.METRICS;
const meta = this.props.data?.series.length ? this.props.data?.series[0].meta : {};
const usedAlignmentPeriod = meta?.alignmentPeriod as string;
const variableOptionGroup = {
label: 'Template Variables',
expanded: false,
options: datasource.variables.map(toOption),
};
return (
<>
<Project
templateVariableOptions={variableOptions}
projectName={projectName}
datasource={datasource}
onChange={value => {
this.onPropertyChange('projectName', value);
datasource.getLabels(metricType, refId, value, groupBys).then(labels => this.setState({ labels }));
<QueryTypeSelector
value={queryType}
templateVariableOptions={variableOptionGroup.options}
onChange={(queryType: QueryType) => {
onChange({ ...query, sloQuery, queryType });
onRunQuery();
}}
/>
<Metrics
templateSrv={templateSrv}
projectName={projectName}
metricType={metricType}
templateVariableOptions={variableOptions}
datasource={datasource}
onChange={this.onMetricTypeChange}
>
{metric => (
<>
<Filters
labels={labels}
filters={filters}
onChange={value => this.onPropertyChange('filters', value)}
variableOptionGroup={variableOptionGroup}
/>
<GroupBys
groupBys={Object.keys(labels)}
values={groupBys}
onChange={this.onGroupBysChange.bind(this)}
variableOptionGroup={variableOptionGroup}
/>
<Aggregations
metricDescriptor={metric}
templateVariableOptions={variableOptions}
crossSeriesReducer={crossSeriesReducer}
groupBys={groupBys}
onChange={value => this.onPropertyChange('crossSeriesReducer', value)}
>
{displayAdvancedOptions =>
displayAdvancedOptions && (
<Alignments
alignOptions={alignOptions}
templateVariableOptions={variableOptions}
perSeriesAligner={perSeriesAligner}
onChange={value => this.onPropertyChange('perSeriesAligner', value)}
/>
)
}
</Aggregations>
<AlignmentPeriods
templateSrv={templateSrv}
templateVariableOptions={variableOptions}
alignmentPeriod={alignmentPeriod}
perSeriesAligner={perSeriesAligner}
usedAlignmentPeriod={usedAlignmentPeriod}
onChange={value => this.onPropertyChange('alignmentPeriod', value)}
/>
<AliasBy value={aliasBy} onChange={value => this.onPropertyChange('aliasBy', value)} />
<Help rawQuery={lastQuery} lastQueryError={lastQueryError} />
</>
)}
</Metrics>
></QueryTypeSelector>
{queryType === QueryType.METRICS && (
<MetricQueryEditor
refId={query.refId}
variableOptionGroup={variableOptionGroup}
usedAlignmentPeriod={usedAlignmentPeriod}
onChange={(query: MetricQuery) => this.onQueryChange('metricQuery', query)}
onRunQuery={onRunQuery}
datasource={datasource}
query={metricQuery}
></MetricQueryEditor>
)}
{queryType === QueryType.SLO && (
<SLOQueryEditor
variableOptionGroup={variableOptionGroup}
usedAlignmentPeriod={usedAlignmentPeriod}
onChange={(query: SLOQuery) => this.onQueryChange('sloQuery', query)}
onRunQuery={onRunQuery}
datasource={datasource}
query={sloQuery}
></SLOQueryEditor>
)}
<Help rawQuery={decodeURIComponent(meta?.rawQuery ?? '')} lastQueryError={this.state.lastQueryError} />
</>
);
}

@ -0,0 +1,34 @@
import React, { FunctionComponent } from 'react';
import _ from 'lodash';
import { SelectableValue } from '@grafana/data';
import { Segment } from '@grafana/ui';
import { QueryType, queryTypes } from '../types';
export interface Props {
value: QueryType;
onChange: (slo: QueryType) => void;
templateVariableOptions: Array<SelectableValue<string>>;
}
export const QueryTypeSelector: FunctionComponent<Props> = ({ onChange, value, templateVariableOptions }) => {
return (
<div className="gf-form-inline">
<label className="gf-form-label query-keyword width-9">Query Type</label>
<Segment
value={[...queryTypes, ...templateVariableOptions].find(qt => qt.value === value)}
options={[
...queryTypes,
{
label: 'Template Variables',
options: templateVariableOptions,
},
]}
onChange={({ value }: SelectableValue<QueryType>) => onChange(value!)}
/>
<div className="gf-form gf-form--grow">
<label className="gf-form-label gf-form-label--grow"></label>
</div>
</div>
);
};

@ -0,0 +1,108 @@
import React from 'react';
import { Segment, SegmentAsync } from '@grafana/ui';
import { SelectableValue } from '@grafana/data';
import { selectors } from '../constants';
import { Project, AlignmentPeriods, AliasBy, QueryInlineField } from '.';
import { SLOQuery } from '../types';
import StackdriverDatasource from '../datasource';
export interface Props {
usedAlignmentPeriod: string;
variableOptionGroup: SelectableValue<string>;
onChange: (query: SLOQuery) => void;
onRunQuery: () => void;
query: SLOQuery;
datasource: StackdriverDatasource;
}
export const defaultQuery: SLOQuery = {
projectName: '',
alignmentPeriod: 'stackdriver-auto',
aliasBy: '',
selectorName: 'select_slo_health',
serviceId: '',
sloId: '',
};
export function SLOQueryEditor({
query,
datasource,
onChange,
variableOptionGroup,
usedAlignmentPeriod,
}: React.PropsWithChildren<Props>) {
return (
<>
<Project
templateVariableOptions={variableOptionGroup.options}
projectName={query.projectName}
datasource={datasource}
onChange={projectName => onChange({ ...query, projectName })}
/>
<QueryInlineField label="Service">
<SegmentAsync
allowCustomValue
value={query?.serviceId}
placeholder="Select service"
loadOptions={() =>
datasource.getSLOServices(query.projectName).then(services => [
{
label: 'Template Variables',
options: variableOptionGroup.options,
},
...services,
])
}
onChange={({ value: serviceId = '' }) => onChange({ ...query, serviceId, sloId: '' })}
/>
</QueryInlineField>
<QueryInlineField label="SLO">
<SegmentAsync
allowCustomValue
value={query?.sloId}
placeholder="Select SLO"
loadOptions={() =>
datasource.getServiceLevelObjectives(query.projectName, query.serviceId).then(sloIds => [
{
label: 'Template Variables',
options: variableOptionGroup.options,
},
...sloIds,
])
}
onChange={async ({ value: sloId = '' }) => {
const slos = await datasource.getServiceLevelObjectives(query.projectName, query.serviceId);
const slo = slos.find(({ value }) => value === datasource.templateSrv.replace(sloId));
onChange({ ...query, sloId, goal: slo?.goal });
}}
/>
</QueryInlineField>
<QueryInlineField label="Selector">
<Segment
allowCustomValue
value={[...selectors, ...variableOptionGroup.options].find(s => s.value === query?.selectorName ?? '')}
options={[
{
label: 'Template Variables',
options: variableOptionGroup.options,
},
...selectors,
]}
onChange={({ value: selectorName }) => onChange({ ...query, selectorName })}
/>
</QueryInlineField>
<AlignmentPeriods
templateSrv={datasource.templateSrv}
templateVariableOptions={variableOptionGroup.options}
alignmentPeriod={query.alignmentPeriod || ''}
perSeriesAligner={query.selectorName === 'select_slo_health' ? 'ALIGN_MEAN' : 'ALIGN_NEXT_OLDER'}
usedAlignmentPeriod={usedAlignmentPeriod}
onChange={alignmentPeriod => onChange({ ...query, alignmentPeriod })}
/>
<AliasBy value={query.aliasBy} onChange={aliasBy => onChange({ ...query, aliasBy })} />
</>
);
}

@ -16,8 +16,10 @@ const props: VariableQueryProps = {
query: {},
datasource: {
getDefaultProject: () => '',
getProjects: async (): Promise<any[]> => [],
getMetricTypes: async (p: any): Promise<any[]> => [],
getProjects: async () => Promise.resolve([]),
getMetricTypes: async (projectName: string) => Promise.resolve([]),
getSLOServices: async (projectName: string, serviceId: string) => Promise.resolve([]),
getServiceLevelObjectives: (projectName: string, serviceId: string) => Promise.resolve([]),
},
templateSrv: { replace: (s: string) => s, getVariables: () => ([] as unknown) as VariableModel[] },
};

@ -3,6 +3,7 @@ import { VariableQueryProps } from 'app/types/plugins';
import { SimpleSelect } from './';
import { extractServicesFromMetricDescriptors, getLabelKeys, getMetricTypes } from '../functions';
import { MetricFindQueryTypes, VariableQueryData } from '../types';
import { getConfig } from 'app/core/config';
export class StackdriverVariableQueryEditor extends PureComponent<VariableQueryProps, VariableQueryData> {
queryTypes: Array<{ value: string; name: string }> = [
@ -15,6 +16,9 @@ export class StackdriverVariableQueryEditor extends PureComponent<VariableQueryP
{ value: MetricFindQueryTypes.Aggregations, name: 'Aggregations' },
{ value: MetricFindQueryTypes.Aligners, name: 'Aligners' },
{ value: MetricFindQueryTypes.AlignmentPeriods, name: 'Alignment Periods' },
{ value: MetricFindQueryTypes.Selectors, name: 'Selectors' },
{ value: MetricFindQueryTypes.SLOServices, name: 'SLO Services' },
{ value: MetricFindQueryTypes.SLO, name: 'Service Level Objectives (SLO)' },
];
defaults: VariableQueryData = {
@ -26,6 +30,8 @@ export class StackdriverVariableQueryEditor extends PureComponent<VariableQueryP
labelKey: '',
metricTypes: [],
services: [],
sloServices: [],
selectedSLOService: '',
projects: [],
projectName: '',
};
@ -63,6 +69,8 @@ export class StackdriverVariableQueryEditor extends PureComponent<VariableQueryP
this.props.templateSrv.replace(selectedService)
);
const sloServices = await this.props.datasource.getSLOServices(this.state.projectName);
const state: any = {
services,
selectedService,
@ -71,6 +79,7 @@ export class StackdriverVariableQueryEditor extends PureComponent<VariableQueryP
metricDescriptors,
projects: projects.map(({ value, label }: any) => ({ value, name: label })),
...(await this.getLabels(selectedMetricType, this.state.projectName)),
sloServices: sloServices.map(({ value, label }: any) => ({ value, name: label })),
};
this.setState(state);
}
@ -80,6 +89,7 @@ export class StackdriverVariableQueryEditor extends PureComponent<VariableQueryP
selectedQueryType: queryType,
...(await this.getLabels(this.state.selectedMetricType, this.state.projectName, queryType)),
};
this.setState(state);
}
@ -93,7 +103,16 @@ export class StackdriverVariableQueryEditor extends PureComponent<VariableQueryP
this.props.templateSrv.replace(this.state.selectedService)
);
this.setState({ ...labels, metricTypes, selectedMetricType, metricDescriptors, projectName });
const sloServices = await this.props.datasource.getSLOServices(projectName);
this.setState({
...labels,
metricTypes,
selectedMetricType,
metricDescriptors,
projectName,
sloServices: sloServices.map(({ value, label }: any) => ({ value, name: label })),
});
}
async onServiceChange(service: string) {
@ -124,10 +143,12 @@ export class StackdriverVariableQueryEditor extends PureComponent<VariableQueryP
this.setState({ labelKey });
}
componentDidUpdate() {
const { metricDescriptors, labels, metricTypes, services, ...queryModel } = this.state;
const query = this.queryTypes.find(q => q.value === this.state.selectedQueryType);
this.props.onChange(queryModel, `Stackdriver - ${query.name}`);
componentDidUpdate(prevProps: Readonly<VariableQueryProps>, prevState: Readonly<VariableQueryData>) {
if (!getConfig().featureToggles.newVariables || prevState.selectedQueryType !== this.state.selectedQueryType) {
const { metricDescriptors, labels, metricTypes, services, ...queryModel } = this.state;
const query = this.queryTypes.find(q => q.value === this.state.selectedQueryType);
this.props.onChange(queryModel, `Stackdriver - ${query.name}`);
}
}
async getLabels(selectedMetricType: string, projectName: string, selectedQueryType = this.state.selectedQueryType) {
@ -220,6 +241,40 @@ export class StackdriverVariableQueryEditor extends PureComponent<VariableQueryP
/>
</>
);
case MetricFindQueryTypes.SLOServices:
return (
<>
<SimpleSelect
value={this.state.projectName}
options={this.insertTemplateVariables(this.state.projects)}
onValueChange={e => this.onProjectChange(e.target.value)}
label="Project"
/>
</>
);
case MetricFindQueryTypes.SLO:
return (
<>
<SimpleSelect
value={this.state.projectName}
options={this.insertTemplateVariables(this.state.projects)}
onValueChange={e => this.onProjectChange(e.target.value)}
label="Project"
/>
<SimpleSelect
value={this.state.selectedSLOService}
options={this.insertTemplateVariables(this.state.sloServices)}
onValueChange={e => {
this.setState({
...this.state,
selectedSLOService: e.target.value,
});
}}
label="SLO Service"
/>
</>
);
default:
return '';
}

@ -1,248 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`QueryEditor renders correctly 1`] = `
Array [
<div
className="gf-form-inline"
>
<span
className="gf-form-label width-9 query-keyword"
>
Project
</span>
<div
className="gf-form"
onClick={[Function]}
>
<a
className="gf-form-label query-part query-placeholder"
>
Select Project
</a>
</div>
<div
className="gf-form gf-form--grow"
>
<div
className="gf-form-label gf-form-label--grow"
/>
</div>
</div>,
<div
className="gf-form-inline"
>
<span
className="gf-form-label width-9 query-keyword"
>
Service
</span>
<div
className="gf-form"
onClick={[Function]}
>
<a
className="gf-form-label query-part query-placeholder"
>
Select Services
</a>
</div>
<div
className="gf-form gf-form--grow"
>
<div
className="gf-form-label gf-form-label--grow"
/>
</div>
</div>,
<div
className="gf-form-inline"
>
<span
className="gf-form-label width-9 query-keyword"
>
Metric
</span>
<div
className="gf-form"
onClick={[Function]}
>
<a
className="gf-form-label query-part query-placeholder query-part"
>
Select Metric
</a>
</div>
<div
className="gf-form gf-form--grow"
>
<div
className="gf-form-label gf-form-label--grow"
/>
</div>
</div>,
<div
className="gf-form-inline"
>
<label
className="gf-form-label query-keyword width-9"
>
Filter
</label>
<div
className="gf-form"
onClick={[Function]}
>
<a
className="gf-form-label query-part"
>
<i
className="fa fa-plus"
/>
</a>
</div>
<div
className="gf-form gf-form--grow"
>
<label
className="gf-form-label gf-form-label--grow"
/>
</div>
</div>,
<div
className="gf-form-inline"
>
<label
className="gf-form-label query-keyword width-9"
>
Group By
</label>
<div
className="gf-form gf-form--grow"
>
<label
className="gf-form-label gf-form-label--grow"
/>
</div>
</div>,
<div
className="gf-form-inline"
>
<label
className="gf-form-label query-keyword width-9"
>
Aggregation
</label>
<div
className="gf-form"
onClick={[Function]}
>
<a
className="gf-form-label query-part query-placeholder"
>
Select Reducer
</a>
</div>
<div
className="gf-form gf-form--grow"
>
<label
className="gf-form-label gf-form-label--grow"
>
<a
onClick={[Function]}
>
<i
className="fa fa-caret-right"
/>
Advanced Options
</a>
</label>
</div>
</div>,
<div
className="gf-form-inline"
>
<label
className="gf-form-label query-keyword width-9"
>
Alignment Period
</label>
<div
className="gf-form"
onClick={[Function]}
>
<a
className="gf-form-label query-part"
>
stackdriver auto
</a>
</div>
<div
className="gf-form gf-form--grow"
>
</div>
</div>,
<div
className="gf-form-inline"
>
<div
className="gf-form"
>
<label
className="gf-form-label query-keyword width-9"
>
Alias By
</label>
<div
style={
Object {
"flexGrow": 1,
}
}
>
<input
className="gf-form-input gf-form-input width-24"
onChange={[Function]}
type="text"
value=""
/>
</div>
</div>
<div
className="gf-form gf-form--grow"
>
<div
className="gf-form-label gf-form-label--grow"
/>
</div>
</div>,
<div
className="gf-form-inline"
>
<div
className="gf-form"
onClick={[Function]}
>
<label
className="gf-form-label query-keyword pointer"
>
Show Help
<i
className="fa fa-caret-right"
/>
</label>
</div>
<div
className="gf-form gf-form--grow"
>
<div
className="gf-form-label gf-form-label--grow"
/>
</div>
</div>,
"",
"",
]
`;

@ -64,6 +64,21 @@ Array [
>
Alignment Periods
</option>
<option
value="selectors"
>
Selectors
</option>
<option
value="sloServices"
>
SLO Services
</option>
<option
value="slo"
>
Service Level Objectives (SLO)
</option>
</select>
</div>
</div>,

@ -2,10 +2,14 @@ export { Project } from './Project';
export { Metrics } from './Metrics';
export { Help } from './Help';
export { GroupBys } from './GroupBys';
export { Filters } from './Filters';
export { LabelFilter } from './LabelFilter';
export { AnnotationsHelp } from './AnnotationsHelp';
export { Alignments } from './Alignments';
export { AlignmentPeriods } from './AlignmentPeriods';
export { AliasBy } from './AliasBy';
export { Aggregations } from './Aggregations';
export { SimpleSelect } from './SimpleSelect';
export { MetricQueryEditor } from './MetricQueryEditor';
export { SLOQueryEditor } from './SLOQueryEditor';
export { QueryTypeSelector } from './QueryType';
export { QueryInlineField, QueryField } from './Fields';

@ -273,3 +273,9 @@ export const systemLabels = [
'metadata.system_labels.top_level_controller_name',
'metadata.system_labels.container_image',
];
export const selectors = [
{ label: 'SLI Value', value: 'select_slo_health' },
{ label: 'SLO Compliance', value: 'select_slo_compliance' },
{ label: 'SLO Error Budget Remaining', value: 'select_slo_budget_fraction' },
];

@ -1,28 +1,24 @@
import { stackdriverUnitMappings } from './constants';
import appEvents from 'app/core/app_events';
import _ from 'lodash';
import StackdriverMetricFindQuery from './StackdriverMetricFindQuery';
import { Filter, MetricDescriptor, StackdriverOptions, StackdriverQuery, VariableQueryData } from './types';
import {
DataQueryRequest,
DataQueryResponse,
DataSourceApi,
DataSourceInstanceSettings,
ScopedVars,
SelectableValue,
} from '@grafana/data';
import { getBackendSrv } from '@grafana/runtime';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { CoreEvents } from 'app/types';
import { StackdriverQuery, MetricDescriptor, StackdriverOptions, Filter, VariableQueryData, QueryType } from './types';
import { stackdriverUnitMappings } from './constants';
import API from './api';
import StackdriverMetricFindQuery from './StackdriverMetricFindQuery';
export default class StackdriverDatasource extends DataSourceApi<StackdriverQuery, StackdriverOptions> {
url: string;
baseUrl: string;
projectList: Array<{ label: string; value: string }>;
api: API;
authenticationType: string;
queryPromise: Promise<any>;
metricTypesCache: { [key: string]: MetricDescriptor[] };
gceDefaultProject: string;
/** @ngInject */
constructor(
@ -31,121 +27,14 @@ export default class StackdriverDatasource extends DataSourceApi<StackdriverQuer
private timeSrv: TimeSrv
) {
super(instanceSettings);
this.baseUrl = `/stackdriver/`;
this.url = instanceSettings.url!;
this.authenticationType = instanceSettings.jsonData.authenticationType || 'jwt';
this.metricTypesCache = {};
this.api = new API(`${instanceSettings.url!}/stackdriver/v3/projects/`);
}
get variables() {
return this.templateSrv.getVariables().map(v => `$${v.name}`);
}
async getTimeSeries(options: DataQueryRequest<StackdriverQuery>) {
await this.ensureGCEDefaultProject();
const queries = options.targets
.filter((target: StackdriverQuery) => {
return !target.hide && target.metricType;
})
.map((t: StackdriverQuery) => {
return {
refId: t.refId,
intervalMs: options.intervalMs,
datasourceId: this.id,
metricType: this.templateSrv.replace(t.metricType, options.scopedVars || {}),
crossSeriesReducer: this.templateSrv.replace(t.crossSeriesReducer || 'REDUCE_MEAN', options.scopedVars || {}),
perSeriesAligner: this.templateSrv.replace(t.perSeriesAligner, options.scopedVars || {}),
alignmentPeriod: this.templateSrv.replace(t.alignmentPeriod!, options.scopedVars || {}),
groupBys: this.interpolateGroupBys(t.groupBys || [], options.scopedVars),
view: t.view || 'FULL',
filters: this.interpolateFilters(t.filters || [], options.scopedVars),
aliasBy: this.templateSrv.replace(t.aliasBy!, options.scopedVars || {}),
type: 'timeSeriesQuery',
projectName: this.templateSrv.replace(t.projectName ? t.projectName : this.getDefaultProject()),
};
});
if (queries.length > 0) {
const { data } = await getBackendSrv().datasourceRequest({
url: '/api/tsdb/query',
method: 'POST',
data: {
from: options.range.from.valueOf().toString(),
to: options.range.to.valueOf().toString(),
queries,
},
});
return data;
} else {
return { results: [] };
}
}
interpolateFilters(filters: string[], scopedVars: ScopedVars) {
const completeFilter = _.chunk(filters, 4)
.map(([key, operator, value, condition = 'AND']) => ({
key,
operator,
value,
condition,
}))
.reduce((res, filter) => (filter.value ? [...res, filter] : res), []);
const filterArray = _.flatten(
completeFilter.map(({ key, operator, value, condition }: Filter) => [
this.templateSrv.replace(key, scopedVars || {}),
operator,
this.templateSrv.replace(value, scopedVars || {}, 'regex'),
condition,
])
);
return filterArray || [];
}
async getLabels(metricType: string, refId: string, projectName: string, groupBys?: string[]) {
const response = await this.getTimeSeries({
targets: [
{
refId: refId,
datasourceId: this.id,
projectName: this.templateSrv.replace(projectName),
metricType: this.templateSrv.replace(metricType),
groupBys: this.interpolateGroupBys(groupBys || [], {}),
crossSeriesReducer: 'REDUCE_NONE',
view: 'HEADERS',
},
],
range: this.timeSrv.timeRange(),
} as DataQueryRequest<StackdriverQuery>);
const result = response.results[refId];
return result && result.meta ? result.meta.labels : {};
}
interpolateGroupBys(groupBys: string[], scopedVars: {}): string[] {
let interpolatedGroupBys: string[] = [];
(groupBys || []).forEach(gb => {
const interpolated = this.templateSrv.replace(gb, scopedVars || {}, 'csv').split(',');
if (Array.isArray(interpolated)) {
interpolatedGroupBys = interpolatedGroupBys.concat(interpolated);
} else {
interpolatedGroupBys.push(interpolated);
}
});
return interpolatedGroupBys;
}
resolvePanelUnitFromTargets(targets: StackdriverQuery[]) {
let unit;
if (targets.length > 0 && targets.every(t => t.unit === targets[0].unit)) {
if (stackdriverUnitMappings.hasOwnProperty(targets[0].unit!)) {
// @ts-ignore
unit = stackdriverUnitMappings[targets[0].unit];
}
}
return unit;
}
async query(options: DataQueryRequest<StackdriverQuery>): Promise<DataQueryResponse> {
const result: DataQueryResponse[] = [];
const data = await this.getTimeSeries(options);
@ -180,31 +69,27 @@ export default class StackdriverDatasource extends DataSourceApi<StackdriverQuer
const queries = [
{
refId: 'annotationQuery',
type: 'annotationQuery',
datasourceId: this.id,
metricType: this.templateSrv.replace(annotation.target.metricType, options.scopedVars || {}),
view: 'FULL',
crossSeriesReducer: 'REDUCE_NONE',
perSeriesAligner: 'ALIGN_NONE',
metricType: this.templateSrv.replace(annotation.target.metricType, options.scopedVars || {}),
title: this.templateSrv.replace(annotation.target.title, options.scopedVars || {}),
text: this.templateSrv.replace(annotation.target.text, options.scopedVars || {}),
tags: this.templateSrv.replace(annotation.target.tags, options.scopedVars || {}),
view: 'FULL',
filters: this.interpolateFilters(annotation.target.filters || [], options.scopedVars),
type: 'annotationQuery',
projectName: this.templateSrv.replace(
annotation.target.projectName ? annotation.target.projectName : this.getDefaultProject(),
options.scopedVars || {}
),
filters: this.interpolateFilters(annotation.target.filters || [], options.scopedVars),
},
];
const { data } = await getBackendSrv().datasourceRequest({
url: '/api/tsdb/query',
method: 'POST',
data: {
from: options.range.from.valueOf().toString(),
to: options.range.to.valueOf().toString(),
queries,
},
const { data } = await this.api.post({
from: options.range.from.valueOf().toString(),
to: options.range.to.valueOf().toString(),
queries,
});
const results = data.results['annotationQuery'].tables[0].rows.map((v: any) => {
@ -226,13 +111,53 @@ export default class StackdriverDatasource extends DataSourceApi<StackdriverQuer
return stackdriverMetricFindQuery.execute(query);
}
async getTimeSeries(options: DataQueryRequest<StackdriverQuery>) {
await this.ensureGCEDefaultProject();
const queries = options.targets
.map(this.migrateQuery)
.filter(this.shouldRunQuery)
.map(q => this.prepareTimeSeriesQuery(q, options));
if (queries.length > 0) {
const { data } = await this.api.post({
from: options.range.from.valueOf().toString(),
to: options.range.to.valueOf().toString(),
queries,
});
return data;
} else {
return { results: [] };
}
}
async getLabels(metricType: string, refId: string, projectName: string, groupBys?: string[]) {
const response = await this.getTimeSeries({
targets: [
{
refId,
datasourceId: this.id,
queryType: QueryType.METRICS,
metricQuery: {
projectName: this.templateSrv.replace(projectName),
metricType: this.templateSrv.replace(metricType),
groupBys: this.interpolateGroupBys(groupBys || [], {}),
crossSeriesReducer: 'REDUCE_NONE',
view: 'HEADERS',
},
},
],
range: this.timeSrv.timeRange(),
} as DataQueryRequest<StackdriverQuery>);
const result = response.results[refId];
return result && result.meta ? result.meta.labels : {};
}
async testDatasource() {
let status, message;
const defaultErrorMessage = 'Cannot connect to Stackdriver API';
try {
await this.ensureGCEDefaultProject();
const path = `v3/projects/${this.getDefaultProject()}/metricDescriptors`;
const response = await this.doRequest(`${this.baseUrl}${path}`);
const response = await this.api.test(this.getDefaultProject());
if (response.status === 200) {
status = 'success';
message = 'Successfully queried the Stackdriver API.';
@ -260,19 +185,15 @@ export default class StackdriverDatasource extends DataSourceApi<StackdriverQuer
}
async getGCEDefaultProject() {
return getBackendSrv()
.datasourceRequest({
url: '/api/tsdb/query',
method: 'POST',
data: {
queries: [
{
refId: 'getGCEDefaultProject',
type: 'getGCEDefaultProject',
datasourceId: this.id,
},
],
},
return this.api
.post({
queries: [
{
refId: 'getGCEDefaultProject',
type: 'getGCEDefaultProject',
datasourceId: this.id,
},
],
})
.then(({ data }) => {
return data && data.results && data.results.getGCEDefaultProject && data.results.getGCEDefaultProject.meta
@ -284,44 +205,6 @@ export default class StackdriverDatasource extends DataSourceApi<StackdriverQuer
});
}
formatStackdriverError(error: any) {
let message = 'Stackdriver: ';
message += error.statusText ? error.statusText + ': ' : '';
if (error.data && error.data.error) {
try {
const res = JSON.parse(error.data.error);
message += res.error.code + '. ' + res.error.message;
} catch (err) {
message += error.data.error;
}
} else {
message += 'Cannot connect to Stackdriver API';
}
return message;
}
async getProjects() {
try {
const { data } = await getBackendSrv().datasourceRequest({
url: '/api/tsdb/query',
method: 'POST',
data: {
queries: [
{
refId: 'getProjectsListQuery',
type: 'getProjectsListQuery',
datasourceId: this.id,
},
],
},
});
return data.results.getProjectsListQuery.meta.projectsList;
} catch (error) {
console.log(this.formatStackdriverError(error));
return [];
}
}
getDefaultProject(): string {
const { defaultProject, authenticationType, gceDefaultProject } = this.instanceSettings.jsonData;
if (authenticationType === 'gce') {
@ -339,20 +222,12 @@ export default class StackdriverDatasource extends DataSourceApi<StackdriverQuer
}
async getMetricTypes(projectName: string): Promise<MetricDescriptor[]> {
try {
if (!projectName) {
return [];
}
const interpolatedProject = this.templateSrv.replace(projectName);
if (this.metricTypesCache[interpolatedProject]) {
return this.metricTypesCache[interpolatedProject];
}
const metricsApiPath = `v3/projects/${interpolatedProject}/metricDescriptors`;
const { data } = await this.doRequest(`${this.baseUrl}${metricsApiPath}`);
if (!projectName) {
return [];
}
this.metricTypesCache[interpolatedProject] = data.metricDescriptors.map((m: any) => {
return this.api.get(`${this.templateSrv.replace(projectName)}/metricDescriptors`, {
responseMap: (m: any) => {
const [service] = m.type.split('/');
const [serviceShortName] = service.split('.');
m.service = service;
@ -360,27 +235,146 @@ export default class StackdriverDatasource extends DataSourceApi<StackdriverQuer
m.displayName = m.displayName || m.type;
return m;
});
},
}) as Promise<MetricDescriptor[]>;
}
return this.metricTypesCache[interpolatedProject];
} catch (error) {
appEvents.emit(CoreEvents.dsRequestError, { error: { data: { error: this.formatStackdriverError(error) } } });
return [];
async getSLOServices(projectName: string): Promise<Array<SelectableValue<string>>> {
return this.api.get(`${this.templateSrv.replace(projectName)}/services`, {
responseMap: ({ name }: { name: string }) => ({
value: name.match(/([^\/]*)\/*$/)[1],
label: name.match(/([^\/]*)\/*$/)[1],
}),
});
}
async getServiceLevelObjectives(projectName: string, serviceId: string): Promise<Array<SelectableValue<string>>> {
let { projectName: p, serviceId: s } = this.interpolateProps({ projectName, serviceId });
return this.api.get(`${p}/services/${s}/serviceLevelObjectives`, {
responseMap: ({ name, displayName, goal }: { name: string; displayName: string; goal: number }) => ({
value: name.match(/([^\/]*)\/*$/)[1],
label: displayName,
goal,
}),
});
}
async getProjects() {
return this.api.get(`projects`, {
responseMap: ({ projectId, name }: { projectId: string; name: string }) => ({
value: projectId,
label: name,
}),
baseUrl: `${this.instanceSettings.url!}/cloudresourcemanager/v1/`,
});
}
migrateQuery(query: StackdriverQuery): StackdriverQuery {
if (!query.hasOwnProperty('metricQuery')) {
const { hide, refId, datasource, key, queryType, maxLines, metric, ...rest } = query as any;
return {
refId,
hide,
queryType: QueryType.METRICS,
metricQuery: {
...rest,
view: rest.view || 'FULL',
},
};
}
return query;
}
async doRequest(url: string, maxRetries = 1): Promise<any> {
return getBackendSrv()
.datasourceRequest({
url: this.url + url,
method: 'GET',
})
.catch((error: any) => {
if (maxRetries > 0) {
return this.doRequest(url, maxRetries - 1);
}
interpolateProps(object: { [key: string]: any } = {}, scopedVars: ScopedVars = {}): { [key: string]: any } {
return Object.entries(object).reduce((acc, [key, value]) => {
return {
...acc,
[key]: value && _.isString(value) ? this.templateSrv.replace(value, scopedVars) : value,
};
}, {});
}
throw error;
});
shouldRunQuery(query: StackdriverQuery): boolean {
if (query.hide) {
return false;
}
if (query.queryType && query.queryType === QueryType.SLO) {
const { selectorName, serviceId, sloId, projectName } = query.sloQuery;
return !!selectorName && !!serviceId && !!sloId && !!projectName;
}
const { metricType } = query.metricQuery;
return !!metricType;
}
prepareTimeSeriesQuery(
{ metricQuery, refId, queryType, sloQuery }: StackdriverQuery,
{ scopedVars, intervalMs }: DataQueryRequest<StackdriverQuery>
) {
return {
datasourceId: this.id,
refId,
queryType,
intervalMs: intervalMs,
type: 'timeSeriesQuery',
metricQuery: {
...this.interpolateProps(metricQuery, scopedVars),
projectName: this.templateSrv.replace(
metricQuery.projectName ? metricQuery.projectName : this.getDefaultProject()
),
filters: this.interpolateFilters(metricQuery.filters || [], scopedVars),
groupBys: this.interpolateGroupBys(metricQuery.groupBys || [], scopedVars),
view: metricQuery.view || 'FULL',
},
sloQuery: this.interpolateProps(sloQuery, scopedVars),
};
}
interpolateFilters(filters: string[], scopedVars: ScopedVars) {
const completeFilter = _.chunk(filters, 4)
.map(([key, operator, value, condition]) => ({
key,
operator,
value,
...(condition && { condition }),
}))
.reduce((res, filter) => (filter.value ? [...res, filter] : res), []);
const filterArray = _.flatten(
completeFilter.map(({ key, operator, value, condition }: Filter) => [
this.templateSrv.replace(key, scopedVars || {}),
operator,
this.templateSrv.replace(value, scopedVars || {}, 'regex'),
...(condition ? [condition] : []),
])
);
return filterArray || [];
}
interpolateGroupBys(groupBys: string[], scopedVars: {}): string[] {
let interpolatedGroupBys: string[] = [];
(groupBys || []).forEach(gb => {
const interpolated = this.templateSrv.replace(gb, scopedVars || {}, 'csv').split(',');
if (Array.isArray(interpolated)) {
interpolatedGroupBys = interpolatedGroupBys.concat(interpolated);
} else {
interpolatedGroupBys.push(interpolated);
}
});
return interpolatedGroupBys;
}
resolvePanelUnitFromTargets(targets: any) {
let unit;
if (targets.length > 0 && targets.every((t: any) => t.unit === targets[0].unit)) {
if (stackdriverUnitMappings.hasOwnProperty(targets[0].unit!)) {
// @ts-ignore
unit = stackdriverUnitMappings[targets[0].unit];
}
}
return unit;
}
}

@ -3,7 +3,7 @@ import { alignOptions, aggOptions, ValueTypes, MetricKind, systemLabels } from '
import { SelectableValue } from '@grafana/data';
import StackdriverDatasource from './datasource';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { StackdriverQuery, MetricDescriptor } from './types';
import { MetricDescriptor, Filter, MetricQuery } from './types';
export const extractServicesFromMetricDescriptors = (metricDescriptors: MetricDescriptor[]) =>
_.uniqBy(metricDescriptors, 'service');
@ -61,7 +61,7 @@ export const getLabelKeys = async (
};
export const getAlignmentPickerData = (
{ valueType, metricKind, perSeriesAligner }: Partial<StackdriverQuery>,
{ valueType, metricKind, perSeriesAligner }: Partial<MetricQuery>,
templateSrv: TemplateSrv
) => {
const alignOptions = getAlignmentOptionsByMetric(valueType!, metricKind!).map(option => ({
@ -92,4 +92,36 @@ export const labelsToGroupedOptions = (groupBys: string[]) => {
return Object.entries(groups).map(([label, options]) => ({ label, options, expanded: true }), []);
};
export const filtersToStringArray = (filters: Filter[]) => {
const strArr = _.flatten(filters.map(({ key, operator, value, condition }) => [key, operator, value, condition]));
return strArr.filter((_, i) => i !== strArr.length - 1);
};
export const stringArrayToFilters = (filterArray: string[]) =>
_.chunk(filterArray, 4).map(([key, operator, value, condition = 'AND']) => ({
key,
operator,
value,
condition,
}));
export const toOption = (value: string) => ({ label: value, value } as SelectableValue<string>);
export const formatStackdriverError = (error: any) => {
let message = error.statusText ?? '';
if (error.data && error.data.error) {
try {
const res = JSON.parse(error.data.error);
message += res.error.code + '. ' + res.error.message;
} catch (err) {
message += error.data.error;
}
} else if (error.data && error.data.message) {
try {
message = JSON.parse(error.data.message).error.message;
} catch (err) {
error.error;
}
}
return message;
};

@ -1,13 +1,13 @@
import { DataSourcePlugin } from '@grafana/data';
import StackdriverDatasource from './datasource';
import { StackdriverQueryCtrl } from './query_ctrl';
import { QueryEditor } from './components/QueryEditor';
import { StackdriverConfigCtrl } from './config_ctrl';
import { StackdriverAnnotationsQueryCtrl } from './annotations_query_ctrl';
import { StackdriverVariableQueryEditor } from './components/VariableQueryEditor';
import { StackdriverQuery } from './types';
export {
StackdriverDatasource as Datasource,
StackdriverQueryCtrl as QueryCtrl,
StackdriverConfigCtrl as ConfigCtrl,
StackdriverAnnotationsQueryCtrl as AnnotationsQueryCtrl,
StackdriverVariableQueryEditor as VariableQueryEditor,
};
export const plugin = new DataSourcePlugin<StackdriverDatasource, StackdriverQuery>(StackdriverDatasource)
.setQueryEditor(QueryEditor)
.setConfigCtrl(StackdriverConfigCtrl)
.setAnnotationQueryCtrl(StackdriverAnnotationsQueryCtrl)
.setVariableQueryEditor(StackdriverVariableQueryEditor);

@ -1,10 +0,0 @@
<query-editor-row query-ctrl="ctrl" has-text-edit-mode="false">
<stackdriver-query-editor
target="ctrl.target"
events="ctrl.panelCtrl.events"
datasource="ctrl.datasource"
template-srv="ctrl.templateSrv"
on-query-change="(ctrl.onQueryChange)"
on-execute-query="(ctrl.onExecuteQuery)"
></stackdriver-query-editor>
</query-editor-row>

@ -1,25 +0,0 @@
import { QueryCtrl } from 'app/plugins/sdk';
import { StackdriverQuery } from './types';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { auto } from 'angular';
export class StackdriverQueryCtrl extends QueryCtrl {
static templateUrl = 'partials/query.editor.html';
templateSrv: TemplateSrv;
/** @ngInject */
constructor($scope: any, $injector: auto.IInjectorService, templateSrv: TemplateSrv) {
super($scope, $injector);
this.templateSrv = templateSrv;
this.onQueryChange = this.onQueryChange.bind(this);
this.onExecuteQuery = this.onExecuteQuery.bind(this);
}
onQueryChange(target: StackdriverQuery) {
Object.assign(this.target, target);
}
onExecuteQuery() {
this.$scope.ctrl.refresh();
}
}

@ -3,7 +3,7 @@ import { metricDescriptors } from './testData';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { CustomVariable } from 'app/features/templating/all';
import { DataSourceInstanceSettings, toUtc } from '@grafana/data';
import { StackdriverOptions, StackdriverQuery } from '../types';
import { StackdriverOptions } from '../types';
import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
@ -180,7 +180,7 @@ describe('StackdriverDataSource', () => {
});
it('should replace the variable with the value', () => {
expect(interpolated.length).toBe(4);
expect(interpolated.length).toBe(3);
expect(interpolated[2]).toBe('filtervalue1');
});
});
@ -193,7 +193,7 @@ describe('StackdriverDataSource', () => {
});
it('should replace the variable with the value and not with regex formatting', () => {
expect(interpolated.length).toBe(4);
expect(interpolated.length).toBe(3);
expect(interpolated[0]).toBe('resource.label.zone');
});
});
@ -250,7 +250,7 @@ describe('StackdriverDataSource', () => {
describe('when theres only one target', () => {
describe('and the stackdriver unit doesnt have a corresponding grafana unit', () => {
beforeEach(() => {
res = ds.resolvePanelUnitFromTargets([{ unit: 'megaseconds' }] as StackdriverQuery[]);
res = ds.resolvePanelUnitFromTargets([{ unit: 'megaseconds' }]);
});
it('should return undefined', () => {
expect(res).toBeUndefined();
@ -258,7 +258,7 @@ describe('StackdriverDataSource', () => {
});
describe('and the stackdriver unit has a corresponding grafana unit', () => {
beforeEach(() => {
res = ds.resolvePanelUnitFromTargets([{ unit: 'bit' }] as StackdriverQuery[]);
res = ds.resolvePanelUnitFromTargets([{ unit: 'bit' }]);
});
it('should return bits', () => {
expect(res).toEqual('bits');
@ -269,7 +269,7 @@ describe('StackdriverDataSource', () => {
describe('when theres more than one target', () => {
describe('and all target units are the same', () => {
beforeEach(() => {
res = ds.resolvePanelUnitFromTargets([{ unit: 'bit' }, { unit: 'bit' }] as StackdriverQuery[]);
res = ds.resolvePanelUnitFromTargets([{ unit: 'bit' }, { unit: 'bit' }]);
});
it('should return bits', () => {
expect(res).toEqual('bits');
@ -277,10 +277,7 @@ describe('StackdriverDataSource', () => {
});
describe('and all target units are the same but doesnt have grafana mappings', () => {
beforeEach(() => {
res = ds.resolvePanelUnitFromTargets([
{ unit: 'megaseconds' },
{ unit: 'megaseconds' },
] as StackdriverQuery[]);
res = ds.resolvePanelUnitFromTargets([{ unit: 'megaseconds' }, { unit: 'megaseconds' }]);
});
it('should return the default value of undefined', () => {
expect(res).toBeUndefined();
@ -288,7 +285,7 @@ describe('StackdriverDataSource', () => {
});
describe('and all target units are not the same', () => {
beforeEach(() => {
res = ds.resolvePanelUnitFromTargets([{ unit: 'bit' }, { unit: 'min' }] as StackdriverQuery[]);
res = ds.resolvePanelUnitFromTargets([{ unit: 'bit' }, { unit: 'min' }]);
});
it('should return the default value of undefined', () => {
expect(res).toBeUndefined();

@ -21,6 +21,9 @@ export enum MetricFindQueryTypes {
Aggregations = 'aggregations',
Aligners = 'aligners',
AlignmentPeriods = 'alignmentPeriods',
Selectors = 'selectors',
SLOServices = 'sloServices',
SLO = 'slo',
}
export interface VariableQueryData {
@ -28,32 +31,60 @@ export interface VariableQueryData {
metricDescriptors: MetricDescriptor[];
selectedService: string;
selectedMetricType: string;
selectedSLOService: string;
labels: string[];
labelKey: string;
metricTypes: Array<{ value: string; name: string }>;
services: Array<{ value: string; name: string }>;
projects: Array<{ value: string; name: string }>;
sloServices: Array<{ value: string; name: string }>;
projectName: string;
}
export interface StackdriverQuery extends DataQuery {
export enum QueryType {
METRICS = 'metrics',
SLO = 'slo',
}
export const queryTypes = [
{ label: 'Metrics', value: QueryType.METRICS },
{ label: 'Service Level Objectives (SLO)', value: QueryType.SLO },
];
export interface MetricQuery {
projectName: string;
unit?: string;
metricType: string;
service?: string;
refId: string;
crossSeriesReducer: string;
alignmentPeriod?: string;
perSeriesAligner: string;
perSeriesAligner?: string;
groupBys?: string[];
filters?: string[];
aliasBy?: string;
metricKind: string;
valueType: string;
datasourceId?: number;
metricKind?: string;
valueType?: string;
view?: string;
}
export interface SLOQuery {
projectName: string;
alignmentPeriod?: string;
perSeriesAligner?: string;
aliasBy?: string;
selectorName: string;
serviceId: string;
sloId: string;
goal?: number;
}
export interface StackdriverQuery extends DataQuery {
datasourceId?: number;
refId: string;
queryType: QueryType;
metricQuery: MetricQuery;
sloQuery?: SLOQuery;
}
export interface StackdriverOptions extends DataSourceJsonData {
defaultProject?: string;
gceDefaultProject?: string;
@ -100,5 +131,5 @@ export interface Filter {
key: string;
operator: string;
value: string;
condition: string;
condition?: string;
}

Loading…
Cancel
Save