CloudMonitoring: Add support for preprocessing (#33011)

* add support for handling preprocessors in the backend

* add preprocessor tests

* use uppercase for constants

* add super label component

* remove error message from query editor since its not working (probably cause onDataError doesnt work anymore)

* use cheat sheet instead of help

* add return type annotation for projects

* add support for preprocessing. replace segment comp with select. change components names and refactoring

* cleanup

* more pr feedback

* fix annotations editor

* rename aggregation component

* fix broken test

* remove unnecessary cast

* fix strict errors

* fix more strict errors

* remove not used prop

* update docs

* use same inline label for annotation editor

* fix react prop warning

* disable preprocessing for distribution types

* using new default values for reducer

* auto select 'rate' if metric kind is not gauge

* fix create label format

* pr feedback

* more pr feedback

* update images
This commit is contained in:
Erik Sundell 2021-05-19 08:16:05 +02:00 committed by GitHub
parent e3188458d5
commit 5042dc3b52
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 1385 additions and 923 deletions

View File

@ -81,7 +81,7 @@ The Google Cloud Monitoring query editor allows you to build two types of querie
### Metric Queries
{{< docs-imagebox img="/img/docs/v70/metric-query-builder.png" max-width= "400px" class="docs-image--right" >}}
{{< docs-imagebox img="/img/docs/google-cloud-monitoring/metric-query-builder-8-0.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.
@ -97,7 +97,7 @@ Google Cloud Monitoring supports different kinds of metrics like `GAUGE`, `DELTA
#### 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--`.
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 trash icon.
##### Simple wildcards
@ -107,13 +107,41 @@ When the operator is set to `=` or `!=` it is possible to add wildcards to the f
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
#### Pre-processing
The aggregation field lets you combine time series based on common statistics. For more information about aggregation, refer to [aggregation options](https://cloud.google.com/monitoring/charts/metrics-selector#aggregation-options).
Preprocessing options are displayed in the UI when the selected metric has a metric kind of `delta` or `cumulative`. If the selected metric has a metric kind of `gauge`, no pre-processing option will be displayed.
The `Aligner` field allows you to align multiple time series after the same group by time interval. For more information about aligner, refer to [alignment metric selector](https://cloud.google.com/monitoring/charts/metrics-selector#alignment).
If you select 'Rate', data points are aligned and converted to a rate per time series. If you select 'Delta', data points are aligned by their delta (difference) per time series.
##### Alignment Period/Group by Time
#### Grouping
You can reduce the amount of data returned for a metric by combining different time series. To combine multiple time series, specify a grouping and a function.
##### Group by
Group by resource or metric labels to reduce the number of time series and to aggregate the results by a group. For example, group by `instance_name` to view an aggregated metric for a Compute instance.
###### Metadata labels
Resource metadata labels contain information that can 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. As a result, you cannot populate the group by list with the metadata labels that are available for the selected service and metric. However, the **Group By** field list comes with a pre-defined set of common system labels.
User labels cannot be predefined, but you can 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.
##### Group by function
Select a grouping function to combine the time series in the group into a single time series.
#### Alignment
The process of alignment consists of collecting all data points received in a fixed length of time, applying a function to combine those data points, and assigning a timestamp to the result.
##### Alignment function
During alignment, all data points are received in a fixed interval. Within each interval (determined by the alignment period) and for each time series, the data is aggregated into a single point. The value of that point is determined by the type of alignment function used. For more information on alignment functions, refer to [alignment metric selector](https://cloud.google.com/monitoring/charts/metrics-selector#alignment).
##### Alignment period
The `Alignment Period` groups a metric by time if an aggregation is chosen. The default is to use the GCP Google Cloud Monitoring default groupings (which allows you to compare graphs in Grafana with graphs in the Google Cloud Monitoring UI).
The option is called `cloud monitoring auto` and the defaults are:
@ -124,23 +152,13 @@ The option is called `cloud monitoring auto` and the defaults are:
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 time series panel. For more information about grafana auto, refer to the [interval variable](http://docs.grafana.org/variables/templates-and-variables/#the-interval-variable).
It is also possible to choose fixed time intervals to group by, like `1h` or `1d`.
#### 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
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.
You can also choose fixed time intervals to group by, like `1h` or `1d`.
#### 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.
#### Metric Type Patterns
#### Metric type patterns
| Alias Pattern | Description | Example Result |
| -------------------- | ---------------------------- | ------------------------------------------------- |
@ -148,7 +166,7 @@ The Alias By field allows you to control the format of the legend keys. The defa
| `{{metric.name}}` | returns the metric name part | `instance/cpu/utilization` |
| `{{metric.service}}` | returns the service part | `compute` |
#### Label Patterns
#### Label patterns
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.
@ -190,7 +208,7 @@ Grafana issues one query to the Cloud Monitoring API per query editor row, and e
> **Note:** Available in Grafana v7.0 and later versions.
{{< docs-imagebox img="/img/docs/v70/slo-query-builder.png" max-width= "400px" class="docs-image--right" >}}
{{< docs-imagebox img="/img/docs/google-cloud-monitoring/slo-query-builder-8-0.png" max-width= "400px" class="docs-image--right" >}}
The SLO query builder in the Google Cloud Monitoring 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 Cloud Monitoring's [official docs](https://cloud.google.com/monitoring/service-monitoring).
@ -212,7 +230,7 @@ The friendly names for the time series selectors are shown in Grafana. Here is t
| SLO Compliance | select_slo_compliance |
| SLO Error Budget Remaining | select_slo_budget_fraction |
#### Alias Patterns for SLO queries
#### Alias patterns for SLO queries
The Alias By field allows you to control the format of the legend keys for SLO queries too.
@ -223,7 +241,7 @@ The Alias By field allows you to control the format of the legend keys for SLO q
| `{{slo}}` | returns the SLO | `latency-slo` |
| `{{selector}}` | returns the selector | `select_slo_health` |
#### Alignment Period/Group by Time for SLO queries
#### Alignment period/group by time for SLO queries
SLO queries use the same [alignment period functionality as metric queries]({{< relref "#metric-queries" >}}).
@ -285,7 +303,7 @@ Why two ways? The first syntax is easier to read and write but does not allow yo
## Annotations
{{< docs-imagebox img="/img/docs/v71/cloudmonitoring_annotations_query_editor.png" max-width= "400px" class="docs-image--right" >}}
{{< docs-imagebox img="/img/docs/google-cloud-monitoring/annotations-8-0.png" max-width= "400px" class="docs-image--right" >}}
[Annotations]({{< relref "../../dashboards/annotations.md" >}}) allow you to overlay rich event information on top of graphs. You add annotation
queries via the Dashboard menu / Annotations view. Annotation rendering is expensive so it is important to limit the number of rows returned. There is no support for showing Google Cloud Monitoring annotations and events yet but it works well with [custom metrics](https://cloud.google.com/monitoring/custom-metrics/) in Google Cloud Monitoring.

View File

@ -57,11 +57,13 @@ var (
)
const (
gceAuthentication string = "gce"
jwtAuthentication string = "jwt"
metricQueryType string = "metrics"
sloQueryType string = "slo"
mqlEditorMode string = "mql"
gceAuthentication string = "gce"
jwtAuthentication string = "jwt"
metricQueryType string = "metrics"
sloQueryType string = "slo"
mqlEditorMode string = "mql"
crossSeriesReducerDefault string = "REDUCE_NONE"
perSeriesAlignerDefault string = "ALIGN_MEAN"
)
func init() {
@ -210,6 +212,7 @@ func (e *Executor) buildQueryExecutors(tsdbQuery plugins.DataQuery) ([]cloudMoni
if err := json.Unmarshal(model, &q); err != nil {
return nil, fmt.Errorf("could not unmarshal CloudMonitoringQuery json: %w", err)
}
q.MetricQuery.PreprocessorType = toPreprocessorType(q.MetricQuery.Preprocessor)
var target string
params := url.Values{}
params.Add("interval.startTime", startTime.UTC().Format(time.RFC3339))
@ -345,16 +348,44 @@ func buildSLOFilterExpression(q sloQuery) string {
func setMetricAggParams(params *url.Values, query *metricQuery, durationSeconds int, intervalMs int64) {
if query.CrossSeriesReducer == "" {
query.CrossSeriesReducer = "REDUCE_NONE"
query.CrossSeriesReducer = crossSeriesReducerDefault
}
if query.PerSeriesAligner == "" {
query.PerSeriesAligner = "ALIGN_MEAN"
query.PerSeriesAligner = perSeriesAlignerDefault
}
params.Add("aggregation.crossSeriesReducer", query.CrossSeriesReducer)
params.Add("aggregation.perSeriesAligner", query.PerSeriesAligner)
params.Add("aggregation.alignmentPeriod", calculateAlignmentPeriod(query.AlignmentPeriod, intervalMs, durationSeconds))
alignmentPeriod := calculateAlignmentPeriod(query.AlignmentPeriod, intervalMs, durationSeconds)
// In case a preprocessor is defined, the preprocessor becomes the primary aggregation
// and the aggregation that is specified in the UI becomes the secondary aggregation
// Rules are specified in this issue: https://github.com/grafana/grafana/issues/30866
if query.PreprocessorType != PreprocessorTypeNone {
params.Add("secondaryAggregation.alignmentPeriod", alignmentPeriod)
params.Add("secondaryAggregation.crossSeriesReducer", query.CrossSeriesReducer)
params.Add("secondaryAggregation.perSeriesAligner", query.PerSeriesAligner)
primaryCrossSeriesReducer := crossSeriesReducerDefault
if len(query.GroupBys) > 0 {
primaryCrossSeriesReducer = query.CrossSeriesReducer
}
params.Add("aggregation.crossSeriesReducer", primaryCrossSeriesReducer)
aligner := "ALIGN_RATE"
if query.PreprocessorType == PreprocessorTypeDelta {
aligner = "ALIGN_DELTA"
}
params.Add("aggregation.perSeriesAligner", aligner)
for _, groupBy := range query.GroupBys {
params.Add("secondaryAggregation.groupByFields", groupBy)
}
} else {
params.Add("aggregation.crossSeriesReducer", query.CrossSeriesReducer)
params.Add("aggregation.perSeriesAligner", query.PerSeriesAligner)
}
params.Add("aggregation.alignmentPeriod", alignmentPeriod)
for _, groupBy := range query.GroupBys {
params.Add("aggregation.groupByFields", groupBy)

View File

@ -1043,6 +1043,169 @@ func TestCloudMonitoring(t *testing.T) {
assert.Contains(t, value, `zone=monitoring.regex.full_match("us-central1-a~")`)
})
})
t.Run("and query preprocessor is not defined", func(t *testing.T) {
tsdbQuery := getBaseQuery()
tsdbQuery.Queries[0].Model = simplejson.NewFromAny(map[string]interface{}{
"metricType": "a/metric/type",
"crossSeriesReducer": "REDUCE_MIN",
"perSeriesAligner": "REDUCE_SUM",
"alignmentPeriod": "+60s",
"groupBys": []string{"labelname"},
"view": "FULL",
})
qes, err := executor.buildQueryExecutors(tsdbQuery)
require.NoError(t, err)
queries := getCloudMonitoringQueriesFromInterface(t, qes)
assert.Equal(t, 1, len(queries))
assert.Equal(t, "REDUCE_MIN", queries[0].Params["aggregation.crossSeriesReducer"][0])
assert.Equal(t, "REDUCE_SUM", queries[0].Params["aggregation.perSeriesAligner"][0])
assert.Equal(t, "+60s", queries[0].Params["aggregation.alignmentPeriod"][0])
assert.Equal(t, "labelname", queries[0].Params["aggregation.groupByFields"][0])
assert.NotContains(t, queries[0].Params, "secondaryAggregation.crossSeriesReducer")
assert.NotContains(t, "REDUCE_SUM", queries[0].Params, "secondaryAggregation.perSeriesAligner")
assert.NotContains(t, "+60s", queries[0].Params, "secondaryAggregation.alignmentPeriod")
assert.NotContains(t, "labelname", queries[0].Params, "secondaryAggregation.groupByFields")
})
t.Run("and query preprocessor is set to none", func(t *testing.T) {
tsdbQuery := getBaseQuery()
tsdbQuery.Queries[0].Model = simplejson.NewFromAny(map[string]interface{}{
"metricType": "a/metric/type",
"crossSeriesReducer": "REDUCE_MIN",
"perSeriesAligner": "REDUCE_SUM",
"alignmentPeriod": "+60s",
"groupBys": []string{"labelname"},
"view": "FULL",
"preprocessor": "none",
})
qes, err := executor.buildQueryExecutors(tsdbQuery)
require.NoError(t, err)
queries := getCloudMonitoringQueriesFromInterface(t, qes)
assert.Equal(t, 1, len(queries))
assert.Equal(t, "REDUCE_MIN", queries[0].Params["aggregation.crossSeriesReducer"][0])
assert.Equal(t, "REDUCE_SUM", queries[0].Params["aggregation.perSeriesAligner"][0])
assert.Equal(t, "+60s", queries[0].Params["aggregation.alignmentPeriod"][0])
assert.Equal(t, "labelname", queries[0].Params["aggregation.groupByFields"][0])
assert.NotContains(t, queries[0].Params, "secondaryAggregation.crossSeriesReducer")
assert.NotContains(t, "REDUCE_SUM", queries[0].Params, "secondaryAggregation.perSeriesAligner")
assert.NotContains(t, "+60s", queries[0].Params, "secondaryAggregation.alignmentPeriod")
assert.NotContains(t, "labelname", queries[0].Params, "secondaryAggregation.groupByFields")
})
t.Run("and query preprocessor is set to rate and there's no group bys", func(t *testing.T) {
tsdbQuery := getBaseQuery()
tsdbQuery.Queries[0].Model = simplejson.NewFromAny(map[string]interface{}{
"metricType": "a/metric/type",
"crossSeriesReducer": "REDUCE_SUM",
"perSeriesAligner": "REDUCE_MIN",
"alignmentPeriod": "+60s",
"groupBys": []string{},
"view": "FULL",
"preprocessor": "rate",
})
qes, err := executor.buildQueryExecutors(tsdbQuery)
require.NoError(t, err)
queries := getCloudMonitoringQueriesFromInterface(t, qes)
assert.Equal(t, 1, len(queries))
assert.Equal(t, "REDUCE_NONE", queries[0].Params["aggregation.crossSeriesReducer"][0])
assert.Equal(t, "ALIGN_RATE", queries[0].Params["aggregation.perSeriesAligner"][0])
assert.Equal(t, "+60s", queries[0].Params["aggregation.alignmentPeriod"][0])
assert.Equal(t, "REDUCE_SUM", queries[0].Params["secondaryAggregation.crossSeriesReducer"][0])
assert.Equal(t, "REDUCE_MIN", queries[0].Params["secondaryAggregation.perSeriesAligner"][0])
assert.Equal(t, "+60s", queries[0].Params["secondaryAggregation.alignmentPeriod"][0])
})
t.Run("and query preprocessor is set to rate and group bys exist", func(t *testing.T) {
tsdbQuery := getBaseQuery()
tsdbQuery.Queries[0].Model = simplejson.NewFromAny(map[string]interface{}{
"metricType": "a/metric/type",
"crossSeriesReducer": "REDUCE_SUM",
"perSeriesAligner": "REDUCE_MIN",
"alignmentPeriod": "+60s",
"groupBys": []string{"labelname"},
"view": "FULL",
"preprocessor": "rate",
})
qes, err := executor.buildQueryExecutors(tsdbQuery)
require.NoError(t, err)
queries := getCloudMonitoringQueriesFromInterface(t, qes)
assert.Equal(t, 1, len(queries))
assert.Equal(t, "REDUCE_SUM", queries[0].Params["aggregation.crossSeriesReducer"][0])
assert.Equal(t, "ALIGN_RATE", queries[0].Params["aggregation.perSeriesAligner"][0])
assert.Equal(t, "+60s", queries[0].Params["aggregation.alignmentPeriod"][0])
assert.Equal(t, "labelname", queries[0].Params["aggregation.groupByFields"][0])
assert.Equal(t, "REDUCE_SUM", queries[0].Params["secondaryAggregation.crossSeriesReducer"][0])
assert.Equal(t, "REDUCE_MIN", queries[0].Params["secondaryAggregation.perSeriesAligner"][0])
assert.Equal(t, "+60s", queries[0].Params["secondaryAggregation.alignmentPeriod"][0])
assert.Equal(t, "labelname", queries[0].Params["secondaryAggregation.groupByFields"][0])
})
t.Run("and query preprocessor is set to delta and there's no group bys", func(t *testing.T) {
tsdbQuery := getBaseQuery()
tsdbQuery.Queries[0].Model = simplejson.NewFromAny(map[string]interface{}{
"metricType": "a/metric/type",
"crossSeriesReducer": "REDUCE_MIN",
"perSeriesAligner": "REDUCE_SUM",
"alignmentPeriod": "+60s",
"groupBys": []string{},
"view": "FULL",
"preprocessor": "delta",
})
qes, err := executor.buildQueryExecutors(tsdbQuery)
require.NoError(t, err)
queries := getCloudMonitoringQueriesFromInterface(t, qes)
assert.Equal(t, 1, len(queries))
assert.Equal(t, "REDUCE_NONE", queries[0].Params["aggregation.crossSeriesReducer"][0])
assert.Equal(t, "ALIGN_DELTA", queries[0].Params["aggregation.perSeriesAligner"][0])
assert.Equal(t, "+60s", queries[0].Params["aggregation.alignmentPeriod"][0])
assert.Equal(t, "REDUCE_MIN", queries[0].Params["secondaryAggregation.crossSeriesReducer"][0])
assert.Equal(t, "REDUCE_SUM", queries[0].Params["secondaryAggregation.perSeriesAligner"][0])
assert.Equal(t, "+60s", queries[0].Params["secondaryAggregation.alignmentPeriod"][0])
})
t.Run("and query preprocessor is set to delta and group bys exist", func(t *testing.T) {
tsdbQuery := getBaseQuery()
tsdbQuery.Queries[0].Model = simplejson.NewFromAny(map[string]interface{}{
"metricType": "a/metric/type",
"crossSeriesReducer": "REDUCE_MIN",
"perSeriesAligner": "REDUCE_SUM",
"alignmentPeriod": "+60s",
"groupBys": []string{"labelname"},
"view": "FULL",
"preprocessor": "delta",
})
qes, err := executor.buildQueryExecutors(tsdbQuery)
require.NoError(t, err)
queries := getCloudMonitoringQueriesFromInterface(t, qes)
assert.Equal(t, 1, len(queries))
assert.Equal(t, "REDUCE_MIN", queries[0].Params["aggregation.crossSeriesReducer"][0])
assert.Equal(t, "ALIGN_DELTA", queries[0].Params["aggregation.perSeriesAligner"][0])
assert.Equal(t, "+60s", queries[0].Params["aggregation.alignmentPeriod"][0])
assert.Equal(t, "labelname", queries[0].Params["aggregation.groupByFields"][0])
assert.Equal(t, "REDUCE_MIN", queries[0].Params["secondaryAggregation.crossSeriesReducer"][0])
assert.Equal(t, "REDUCE_SUM", queries[0].Params["secondaryAggregation.perSeriesAligner"][0])
assert.Equal(t, "+60s", queries[0].Params["secondaryAggregation.alignmentPeriod"][0])
assert.Equal(t, "labelname", queries[0].Params["secondaryAggregation.groupByFields"][0])
})
}
func loadTestFile(path string) (cloudMonitoringResponse, error) {

View File

@ -0,0 +1,22 @@
package cloudmonitoring
type preprocessorType int
const (
PreprocessorTypeNone preprocessorType = iota
PreprocessorTypeRate
PreprocessorTypeDelta
)
func toPreprocessorType(preprocessorTypeString string) preprocessorType {
switch preprocessorTypeString {
case "none":
return PreprocessorTypeNone
case "rate":
return PreprocessorTypeRate
case "delta":
return PreprocessorTypeDelta
default:
return PreprocessorTypeNone
}
}

View File

@ -95,6 +95,10 @@ func (timeSeriesFilter *cloudMonitoringTimeSeriesFilter) parseResponse(queryRes
frame.RefID = timeSeriesFilter.RefID
frame.Meta = &data.FrameMeta{
ExecutedQueryString: executedQueryString,
Custom: map[string]interface{}{
"alignmentPeriod": timeSeriesFilter.Params.Get("aggregation.alignmentPeriod"),
"perSeriesAligner": timeSeriesFilter.Params.Get("aggregation.perSeriesAligner"),
},
}
for key, value := range series.Metric.Labels {

View File

@ -56,6 +56,8 @@ type (
View string
EditorMode string
Query string
Preprocessor string
PreprocessorType preprocessorType
}
sloQuery struct {

View File

@ -1,7 +1,7 @@
import { isString } from 'lodash';
import { alignmentPeriods, MetricKind, selectors, ValueTypes } from './constants';
import { ALIGNMENT_PERIODS, SELECTORS } from './constants';
import CloudMonitoringDatasource from './datasource';
import { CloudMonitoringVariableQuery, MetricFindQueryTypes } from './types';
import { CloudMonitoringVariableQuery, MetricDescriptor, MetricFindQueryTypes, MetricKind, ValueTypes } from './types';
import { SelectableValue } from '@grafana/data';
import {
extractServicesFromMetricDescriptors,
@ -65,7 +65,7 @@ export default class CloudMonitoringMetricFindQuery {
async handleServiceQuery({ projectName }: CloudMonitoringVariableQuery) {
const metricDescriptors = await this.datasource.getMetricTypes(projectName);
const services: any[] = extractServicesFromMetricDescriptors(metricDescriptors);
const services: MetricDescriptor[] = extractServicesFromMetricDescriptors(metricDescriptors);
return services.map((s) => ({
text: s.serviceShortName,
value: s.service,
@ -79,7 +79,7 @@ export default class CloudMonitoringMetricFindQuery {
}
const metricDescriptors = await this.datasource.getMetricTypes(projectName);
return getMetricTypesByService(metricDescriptors, this.datasource.templateSrv.replace(selectedService)).map(
(s: any) => ({
(s) => ({
text: s.displayName,
value: s.type,
expandable: true,
@ -121,7 +121,7 @@ export default class CloudMonitoringMetricFindQuery {
}
const metricDescriptors = await this.datasource.getMetricTypes(projectName);
const descriptor = metricDescriptors.find(
(m: any) => m.type === this.datasource.templateSrv.replace(selectedMetricType)
(m) => m.type === this.datasource.templateSrv.replace(selectedMetricType)
);
if (!descriptor) {
@ -138,7 +138,7 @@ export default class CloudMonitoringMetricFindQuery {
const metricDescriptors = await this.datasource.getMetricTypes(projectName);
const descriptor = metricDescriptors.find(
(m: any) => m.type === this.datasource.templateSrv.replace(selectedMetricType)
(m) => m.type === this.datasource.templateSrv.replace(selectedMetricType)
);
if (!descriptor) {
@ -161,11 +161,11 @@ export default class CloudMonitoringMetricFindQuery {
}
async handleSelectorQuery() {
return selectors.map(this.toFindQueryResult);
return SELECTORS.map(this.toFindQueryResult);
}
handleAlignmentPeriodQuery() {
return alignmentPeriods.map(this.toFindQueryResult);
return ALIGNMENT_PERIODS.map(this.toFindQueryResult);
}
toFindQueryResult(x: any) {

View File

@ -2,10 +2,13 @@ import { AnnotationTarget } from './types';
export class CloudMonitoringAnnotationsQueryCtrl {
static templateUrl = 'partials/annotations.editor.html';
annotation: any;
declare annotation: any;
/** @ngInject */
constructor($scope: any) {
this.annotation = $scope.ctrl.annotation || {};
this.annotation.target = $scope.ctrl.annotation.target || {};
constructor() {
this.annotation.target = this.annotation.target || {};
this.onQueryChange = this.onQueryChange.bind(this);
}

View File

@ -1,9 +1,9 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { shallow } from 'enzyme';
import { Segment } from '@grafana/ui';
import { Aggregations, Props } from './Aggregations';
import { ValueTypes, MetricKind } from '../constants';
import { Select } from '@grafana/ui';
import { Aggregation, Props } from './Aggregation';
import { ValueTypes, MetricKind } from '../types';
import { TemplateSrvStub } from 'test/specs/helpers';
const props: Props = {
@ -16,16 +16,13 @@ const props: Props = {
} as any,
crossSeriesReducer: '',
groupBys: [],
children(renderProps) {
return <div />;
},
templateVariableOptions: [],
};
describe('Aggregations', () => {
describe('Aggregation', () => {
it('renders correctly', () => {
render(<Aggregations {...props} />);
expect(screen.getByTestId('aggregations')).toBeInTheDocument();
render(<Aggregation {...props} />);
expect(screen.getByTestId('cloud-monitoring-aggregation')).toBeInTheDocument();
});
describe('options', () => {
@ -39,8 +36,8 @@ describe('Aggregations', () => {
};
it('should not have the reduce values', () => {
const wrapper = shallow(<Aggregations {...nextProps} />);
const { options } = wrapper.find(Segment).props() as any;
const wrapper = shallow(<Aggregation {...nextProps} />);
const { options } = wrapper.find(Select).props() as any;
const [, aggGroup] = options;
expect(aggGroup.options.length).toEqual(11);
@ -60,8 +57,8 @@ describe('Aggregations', () => {
};
it('should have the reduce values', () => {
const wrapper = shallow(<Aggregations {...nextProps} />);
const { options } = wrapper.find(Segment).props() as any;
const wrapper = shallow(<Aggregation {...nextProps} />);
const { options } = wrapper.find(Select).props() as any;
const [, aggGroup] = options;
expect(aggGroup.options.length).toEqual(11);

View File

@ -0,0 +1,65 @@
import React, { FC, useMemo } from 'react';
import { SelectableValue } from '@grafana/data';
import { Select } from '@grafana/ui';
import { QueryEditorField } from '.';
import { getAggregationOptionsByMetric } from '../functions';
import { MetricDescriptor, ValueTypes, MetricKind } from '../types';
export interface Props {
onChange: (metricDescriptor: string) => void;
metricDescriptor?: MetricDescriptor;
crossSeriesReducer: string;
groupBys: string[];
templateVariableOptions: Array<SelectableValue<string>>;
}
export const Aggregation: FC<Props> = (props) => {
const aggOptions = useAggregationOptionsByMetric(props);
const selected = useSelectedFromOptions(aggOptions, props);
return (
<QueryEditorField labelWidth={18} label="Group by function" data-testid="cloud-monitoring-aggregation">
<Select
width={16}
onChange={({ value }) => props.onChange(value!)}
value={selected}
options={[
{
label: 'Template Variables',
options: props.templateVariableOptions,
},
{
label: 'Aggregations',
expanded: true,
options: aggOptions,
},
]}
placeholder="Select Reducer"
/>
</QueryEditorField>
);
};
const useAggregationOptionsByMetric = ({ metricDescriptor }: Props): Array<SelectableValue<string>> => {
const valueType = metricDescriptor?.valueType;
const metricKind = metricDescriptor?.metricKind;
return useMemo(() => {
if (!valueType || !metricKind) {
return [];
}
return getAggregationOptionsByMetric(valueType as ValueTypes, metricKind as MetricKind).map((a) => ({
...a,
label: a.text,
}));
}, [valueType, metricKind]);
};
const useSelectedFromOptions = (aggOptions: Array<SelectableValue<string>>, props: Props) => {
return useMemo(() => {
const allOptions = [...aggOptions, ...props.templateVariableOptions];
return allOptions.find((s) => s.value === props.crossSeriesReducer);
}, [aggOptions, props.crossSeriesReducer, props.templateVariableOptions]);
};

View File

@ -1,79 +0,0 @@
import React, { FC, useState, useMemo } from 'react';
import { SelectableValue } from '@grafana/data';
import { Segment, Icon } from '@grafana/ui';
import { getAggregationOptionsByMetric } from '../functions';
import { ValueTypes, MetricKind } from '../constants';
import { MetricDescriptor } from '../types';
export interface Props {
onChange: (metricDescriptor: string) => void;
metricDescriptor?: MetricDescriptor;
crossSeriesReducer: string;
groupBys: string[];
children: (displayAdvancedOptions: boolean) => React.ReactNode;
templateVariableOptions: Array<SelectableValue<string>>;
}
export const Aggregations: FC<Props> = (props) => {
const [displayAdvancedOptions, setDisplayAdvancedOptions] = useState(false);
const aggOptions = useAggregationOptionsByMetric(props);
const selected = useSelectedFromOptions(aggOptions, props);
return (
<div data-testid="aggregations">
<div className="gf-form-inline">
<label className="gf-form-label query-keyword width-9">Aggregation</label>
<Segment
onChange={({ value }) => props.onChange(value!)}
value={selected}
options={[
{
label: 'Template Variables',
options: props.templateVariableOptions,
},
{
label: 'Aggregations',
expanded: true,
options: aggOptions,
},
]}
placeholder="Select Reducer"
/>
<div className="gf-form gf-form--grow">
<label className="gf-form-label gf-form-label--grow">
<a onClick={() => setDisplayAdvancedOptions(!displayAdvancedOptions)}>
<>
<Icon name={displayAdvancedOptions ? 'angle-down' : 'angle-right'} /> Advanced Options
</>
</a>
</label>
</div>
</div>
{props.children(displayAdvancedOptions)}
</div>
);
};
const useAggregationOptionsByMetric = ({ metricDescriptor }: Props): Array<SelectableValue<string>> => {
const valueType = metricDescriptor?.valueType;
const metricKind = metricDescriptor?.metricKind;
return useMemo(() => {
if (!valueType || !metricKind) {
return [];
}
return getAggregationOptionsByMetric(valueType as ValueTypes, metricKind as MetricKind).map((a) => ({
...a,
label: a.text,
}));
}, [valueType, metricKind]);
};
const useSelectedFromOptions = (aggOptions: Array<SelectableValue<string>>, props: Props) => {
return useMemo(() => {
const allOptions = [...aggOptions, ...props.templateVariableOptions];
return allOptions.find((s) => s.value === props.crossSeriesReducer);
}, [aggOptions, props.crossSeriesReducer, props.templateVariableOptions]);
};

View File

@ -1,6 +1,8 @@
import React, { FunctionComponent, useState } from 'react';
import { debounce } from 'lodash';
import { QueryInlineField } from '.';
import { Input } from '@grafana/ui';
import { QueryEditorRow } from '.';
import { INPUT_WIDTH } from '../constants';
export interface Props {
onChange: (alias: any) => void;
@ -18,8 +20,8 @@ export const AliasBy: FunctionComponent<Props> = ({ value = '', onChange }) => {
};
return (
<QueryInlineField label="Alias By">
<input type="text" className="gf-form-input width-26" value={alias} onChange={onChange} />
</QueryInlineField>
<QueryEditorRow label="Alias by">
<Input width={INPUT_WIDTH} value={alias} onChange={onChange} />
</QueryEditorRow>
);
};

View File

@ -0,0 +1,34 @@
import React, { FC } from 'react';
import { SelectableValue } from '@grafana/data';
import { SELECT_WIDTH } from '../constants';
import { CustomMetaData, MetricQuery } from '../types';
import { AlignmentFunction, AlignmentPeriod, AlignmentPeriodLabel, QueryEditorField, QueryEditorRow } from '.';
import CloudMonitoringDatasource from '../datasource';
export interface Props {
onChange: (query: MetricQuery) => void;
query: MetricQuery;
templateVariableOptions: Array<SelectableValue<string>>;
customMetaData: CustomMetaData;
datasource: CloudMonitoringDatasource;
}
export const Alignment: FC<Props> = ({ templateVariableOptions, onChange, query, customMetaData, datasource }) => {
return (
<QueryEditorRow
label="Alignment function"
tooltip="The process of alignment consists of collecting all data points received in a fixed length of time, applying a function to combine those data points, and assigning a timestamp to the result."
fillComponent={<AlignmentPeriodLabel datasource={datasource} customMetaData={customMetaData} />}
>
<AlignmentFunction templateVariableOptions={templateVariableOptions} query={query} onChange={onChange} />
<QueryEditorField label="Alignment period">
<AlignmentPeriod
selectWidth={SELECT_WIDTH}
templateVariableOptions={templateVariableOptions}
query={query}
onChange={onChange}
/>
</QueryEditorField>
</QueryEditorRow>
);
};

View File

@ -0,0 +1,40 @@
import React, { FC, useMemo } from 'react';
import { SelectableValue } from '@grafana/data';
import { Select } from '@grafana/ui';
import { MetricQuery } from '../types';
import { getAlignmentPickerData } from '../functions';
import { SELECT_WIDTH } from '../constants';
export interface Props {
onChange: (query: MetricQuery) => void;
query: MetricQuery;
templateVariableOptions: Array<SelectableValue<string>>;
}
export const AlignmentFunction: FC<Props> = ({ query, templateVariableOptions, onChange }) => {
const { valueType, metricKind, perSeriesAligner: psa, preprocessor } = query;
const { perSeriesAligner, alignOptions } = useMemo(
() => getAlignmentPickerData(valueType, metricKind, psa, preprocessor),
[valueType, metricKind, psa, preprocessor]
);
return (
<Select
width={SELECT_WIDTH}
onChange={({ value }) => onChange({ ...query, perSeriesAligner: value! })}
value={[...alignOptions, ...templateVariableOptions].find((s) => s.value === perSeriesAligner)}
options={[
{
label: 'Template Variables',
options: templateVariableOptions,
},
{
label: 'Alignment options',
expanded: true,
options: alignOptions,
},
]}
placeholder="Select Alignment"
></Select>
);
};

View File

@ -0,0 +1,44 @@
import React, { FC, useMemo } from 'react';
import { SelectableValue } from '@grafana/data';
import { Select } from '@grafana/ui';
import { ALIGNMENT_PERIODS } from '../constants';
import { BaseQuery } from '../types';
export interface Props {
onChange: (query: BaseQuery) => void;
query: BaseQuery;
templateVariableOptions: Array<SelectableValue<string>>;
selectWidth?: number;
}
export const AlignmentPeriod: FC<Props> = ({ templateVariableOptions, onChange, query, selectWidth }) => {
const options = useMemo(
() =>
ALIGNMENT_PERIODS.map((ap) => ({
...ap,
label: ap.text,
})),
[]
);
const visibleOptions = useMemo(() => options.filter((ap) => !ap.hidden), [options]);
return (
<Select
width={selectWidth}
onChange={({ value }) => onChange({ ...query, alignmentPeriod: value! })}
value={[...options, ...templateVariableOptions].find((s) => s.value === query.alignmentPeriod)}
options={[
{
label: 'Template Variables',
options: templateVariableOptions,
},
{
label: 'Aggregations',
expanded: true,
options: visibleOptions,
},
]}
placeholder="Select Alignment"
></Select>
);
};

View File

@ -0,0 +1,26 @@
import React, { FC, useMemo } from 'react';
import { rangeUtil } from '@grafana/data';
import { ALIGNMENTS } from '../constants';
import CloudMonitoringDatasource from '../datasource';
import { CustomMetaData } from '../types';
export interface Props {
customMetaData: CustomMetaData;
datasource: CloudMonitoringDatasource;
}
export const AlignmentPeriodLabel: FC<Props> = ({ customMetaData, datasource }) => {
const { perSeriesAligner, alignmentPeriod } = customMetaData;
const formatAlignmentText = useMemo(() => {
if (!alignmentPeriod || !perSeriesAligner) {
return '';
}
const alignment = ALIGNMENTS.find((ap) => ap.value === datasource.templateSrv.replace(perSeriesAligner));
const seconds = parseInt(alignmentPeriod ?? ''.replace(/[^0-9]/g, ''), 10);
const hms = rangeUtil.secondsToHms(seconds);
return `${hms} interval (${alignment?.text ?? ''})`;
}, [datasource, perSeriesAligner, alignmentPeriod]);
return <label>{formatAlignmentText}</label>;
};

View File

@ -1,61 +0,0 @@
import React, { FC } from 'react';
import { TemplateSrv } from '@grafana/runtime';
import { SelectableValue, rangeUtil } from '@grafana/data';
import { Segment } from '@grafana/ui';
import { alignmentPeriods, alignOptions } from '../constants';
export interface Props {
onChange: (alignmentPeriod: string) => void;
templateSrv: TemplateSrv;
templateVariableOptions: Array<SelectableValue<string>>;
alignmentPeriod: string;
perSeriesAligner: string;
usedAlignmentPeriod?: number;
}
export const AlignmentPeriods: FC<Props> = ({
alignmentPeriod,
templateSrv,
templateVariableOptions,
onChange,
perSeriesAligner,
usedAlignmentPeriod,
}) => {
const alignment = alignOptions.find((ap) => ap.value === templateSrv.replace(perSeriesAligner));
const formatAlignmentText = usedAlignmentPeriod
? `${rangeUtil.secondsToHms(usedAlignmentPeriod)} interval (${alignment ? alignment.text : ''})`
: '';
const options = alignmentPeriods.map((ap) => ({
...ap,
label: ap.text,
}));
const visibleOptions = options.filter((ap) => !ap.hidden);
return (
<>
<div className="gf-form-inline">
<label className="gf-form-label query-keyword width-9">Alignment Period</label>
<Segment
onChange={({ value }) => onChange(value!)}
value={[...options, ...templateVariableOptions].find((s) => s.value === alignmentPeriod)}
options={[
{
label: 'Template Variables',
options: templateVariableOptions,
},
{
label: 'Aggregations',
expanded: true,
options: visibleOptions,
},
]}
placeholder="Select Alignment"
></Segment>
<div className="gf-form gf-form--grow">
{usedAlignmentPeriod && <label className="gf-form-label gf-form-label--grow">{formatAlignmentText}</label>}
</div>
</div>
</>
);
};

View File

@ -1,39 +0,0 @@
import React, { FC } from 'react';
import { SelectableValue } from '@grafana/data';
import { Segment } from '@grafana/ui';
export interface Props {
onChange: (perSeriesAligner: string) => void;
templateVariableOptions: Array<SelectableValue<string>>;
alignOptions: Array<SelectableValue<string>>;
perSeriesAligner: string;
}
export const Alignments: FC<Props> = ({ perSeriesAligner, templateVariableOptions, onChange, alignOptions }) => {
return (
<>
<div className="gf-form-inline">
<div className="gf-form offset-width-9">
<label className="gf-form-label query-keyword width-15">Aligner</label>
<Segment
onChange={({ value }) => onChange(value!)}
value={[...alignOptions, ...templateVariableOptions].find((s) => s.value === perSeriesAligner)}
options={[
{
label: 'Template Variables',
options: templateVariableOptions,
},
{
label: 'Alignment options',
expanded: true,
options: alignOptions,
},
]}
placeholder="Select Alignment"
></Segment>
</div>
</div>
</>
);
};

View File

@ -4,9 +4,9 @@ import { TemplateSrv } from '@grafana/runtime';
import { SelectableValue } from '@grafana/data';
import CloudMonitoringDatasource from '../datasource';
import { AnnotationsHelp, LabelFilter, Metrics, Project } from './';
import { AnnotationsHelp, LabelFilter, Metrics, Project, QueryEditorRow } from './';
import { toOption } from '../functions';
import { AnnotationTarget, EditorMode, MetricDescriptor } from '../types';
import { AnnotationTarget, EditorMode, MetricDescriptor, MetricKind } from '../types';
const { Input } = LegacyForms;
@ -30,7 +30,7 @@ const DefaultTarget: State = {
projects: [],
metricType: '',
filters: [],
metricKind: '',
metricKind: MetricKind.GAUGE,
valueType: '',
refId: 'annotationQuery',
title: '',
@ -122,29 +122,23 @@ export class AnnotationQueryEditor extends React.Component<Props, State> {
</>
)}
</Metrics>
<div className="gf-form gf-form-inline">
<div className="gf-form">
<span className="gf-form-label query-keyword width-9">Title</span>
<Input
type="text"
className="gf-form-input width-20"
value={title}
onChange={(e) => this.onChange('title', e.target.value)}
/>
</div>
<div className="gf-form">
<span className="gf-form-label query-keyword width-9">Text</span>
<Input
type="text"
className="gf-form-input width-20"
value={text}
onChange={(e) => this.onChange('text', e.target.value)}
/>
</div>
<div className="gf-form gf-form--grow">
<div className="gf-form-label gf-form-label--grow" />
</div>
</div>
<QueryEditorRow label="Title">
<Input
type="text"
className="gf-form-input width-20"
value={title}
onChange={(e) => this.onChange('title', e.target.value)}
/>
</QueryEditorRow>
<QueryEditorRow label="Text">
<Input
type="text"
className="gf-form-input width-20"
value={text}
onChange={(e) => this.onChange('text', e.target.value)}
/>
</QueryEditorRow>
<AnnotationsHelp />
</>

View File

@ -0,0 +1,72 @@
import React, { PureComponent } from 'react';
import { QueryEditorHelpProps } from '@grafana/data';
import { css } from '@emotion/css';
export default class CloudMonitoringCheatSheet extends PureComponent<QueryEditorHelpProps, { userExamples: string[] }> {
render() {
return (
<div>
<h2>Cloud Monitoring alias patterns</h2>
<div>
<p>
Format the legend keys any way you want by using alias patterns. Format the legend keys any way you want by
using alias patterns.
</p>
Example:
<code>{`${'{{metric.name}} - {{metric.label.instance_name}}'}`}</code>
<br />
Result: &nbsp;&nbsp;<code>cpu/usage_time - server1-europe-west-1</code>
<br />
<br />
<label>Patterns</label>
<br />
<ul
className={css`
list-style: none;
`}
>
<li>
<code>{`${'{{metric.type}}'}`}</code> = metric type e.g. compute.googleapis.com/instance/cpu/usage_time
</li>
<li>
<code>{`${'{{metric.name}}'}`}</code> = name part of metric e.g. instance/cpu/usage_time
</li>
<li>
<code>{`${'{{metric.service}}'}`}</code> = service part of metric e.g. compute
</li>
<li>
<code>{`${'{{metric.label.label_name}}'}`}</code> = Metric label metadata e.g. metric.label.instance_name
</li>
<li>
<code>{`${'{{resource.label.label_name}}'}`}</code> = Resource label metadata e.g. resource.label.zone
</li>
<li>
<code>{`${'{{metadata.system_labels.name}}'}`}</code> = Meta data system labels e.g.
metadata.system_labels.name. For this to work, the needs to be included in the group by
</li>
<li>
<code>{`${'{{metadata.user_labels.name}}'}`}</code> = Meta data user labels e.g.
metadata.user_labels.name. For this to work, the needs to be included in the group by
</li>
<li>
<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>
);
}
}

View File

@ -1,32 +1,8 @@
import React, { InputHTMLAttributes, FunctionComponent } from 'react';
import { InlineFormLabel, Select, InlineField } from '@grafana/ui';
import React, { FC } from 'react';
import { SelectableValue } from '@grafana/data';
export interface Props extends InputHTMLAttributes<HTMLInputElement> {
label: string;
tooltip?: string;
children?: React.ReactNode;
}
export const QueryField: FunctionComponent<Partial<Props>> = ({ label, tooltip, children }) => (
<>
<InlineFormLabel width={9} className="query-keyword" tooltip={tooltip}>
{label}
</InlineFormLabel>
{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>
);
};
import { HorizontalGroup, InlineLabel, PopoverContent, Select, InlineField } from '@grafana/ui';
import { css } from '@emotion/css';
import { INNER_LABEL_WIDTH, LABEL_WIDTH } from '../constants';
interface VariableQueryFieldProps {
onChange: (value: string) => void;
@ -36,7 +12,7 @@ interface VariableQueryFieldProps {
allowCustomValue?: boolean;
}
export const VariableQueryField: FunctionComponent<VariableQueryFieldProps> = ({
export const VariableQueryField: FC<VariableQueryFieldProps> = ({
label,
onChange,
value,
@ -55,3 +31,58 @@ export const VariableQueryField: FunctionComponent<VariableQueryFieldProps> = ({
</InlineField>
);
};
export interface Props {
children: React.ReactNode;
tooltip?: PopoverContent;
label?: React.ReactNode;
className?: string;
noFillEnd?: boolean;
labelWidth?: number;
fillComponent?: React.ReactNode;
}
export const QueryEditorRow: FC<Props> = ({
children,
label,
tooltip,
fillComponent,
noFillEnd = false,
labelWidth = LABEL_WIDTH,
...rest
}) => {
return (
<div className="gf-form" {...rest}>
{label && (
<InlineLabel width={labelWidth} tooltip={tooltip}>
{label}
</InlineLabel>
)}
<div
className={css`
margin-right: 4px;
`}
>
<HorizontalGroup spacing="xs" width="auto">
{children}
</HorizontalGroup>
</div>
<div className={'gf-form--grow'}>
{noFillEnd || <div className={'gf-form-label gf-form-label--grow'}>{fillComponent}</div>}
</div>
</div>
);
};
export const QueryEditorField: FC<Props> = ({ children, label, tooltip, labelWidth = INNER_LABEL_WIDTH, ...rest }) => {
return (
<>
{label && (
<InlineLabel width={labelWidth} tooltip={tooltip} {...rest}>
{label}
</InlineLabel>
)}
{children}
</>
);
};

View File

@ -0,0 +1,52 @@
import React, { FunctionComponent, useMemo } from 'react';
import { SelectableValue } from '@grafana/data';
import { MultiSelect } from '@grafana/ui';
import { labelsToGroupedOptions } from '../functions';
import { SYSTEM_LABELS, INPUT_WIDTH } from '../constants';
import { MetricDescriptor, MetricQuery } from '../types';
import { Aggregation, QueryEditorRow } from '.';
export interface Props {
variableOptionGroup: SelectableValue<string>;
labels: string[];
metricDescriptor?: MetricDescriptor;
onChange: (query: MetricQuery) => void;
query: MetricQuery;
}
export const GroupBy: FunctionComponent<Props> = ({
labels: groupBys = [],
query,
onChange,
variableOptionGroup,
metricDescriptor,
}) => {
const options = useMemo(() => [variableOptionGroup, ...labelsToGroupedOptions([...groupBys, ...SYSTEM_LABELS])], [
groupBys,
variableOptionGroup,
]);
return (
<QueryEditorRow
label="Group by"
tooltip="You can reduce the amount of data returned for a metric by combining different time series. To combine multiple time series, you can specify a grouping and a function. Grouping is done on the basis of labels. The grouping function is used to combine the time series in the group into a single time series."
>
<MultiSelect
width={INPUT_WIDTH}
placeholder="Choose label"
options={options}
value={query.groupBys ?? []}
onChange={(options) => {
onChange({ ...query, groupBys: options.map((o) => o.value!) });
}}
></MultiSelect>
<Aggregation
metricDescriptor={metricDescriptor}
templateVariableOptions={variableOptionGroup.options}
crossSeriesReducer={query.crossSeriesReducer}
groupBys={query.groupBys ?? []}
onChange={(crossSeriesReducer) => onChange({ ...query, crossSeriesReducer })}
></Aggregation>
</QueryEditorRow>
);
};

View File

@ -1,58 +0,0 @@
import React, { FunctionComponent } from 'react';
import { SelectableValue } from '@grafana/data';
import { Segment, Icon } from '@grafana/ui';
import { labelsToGroupedOptions } from '../functions';
import { systemLabels } from '../constants';
export interface Props {
values: string[];
onChange: (values: string[]) => void;
variableOptionGroup: SelectableValue<string>;
groupBys: string[];
}
const removeText = '-- remove group by --';
const removeOption: SelectableValue<string> = { label: removeText, value: removeText };
export const GroupBys: FunctionComponent<Props> = ({ groupBys = [], values = [], onChange, variableOptionGroup }) => {
const options = [removeOption, variableOptionGroup, ...labelsToGroupedOptions([...groupBys, ...systemLabels])];
return (
<div className="gf-form-inline">
<label className="gf-form-label query-keyword width-9">Group By</label>
{values &&
values.map((value, index) => (
<Segment
allowCustomValue
key={value + index}
value={value}
options={options}
onChange={({ value = '' }) =>
onChange(
value === removeText
? values.filter((_, i) => i !== index)
: values.map((v, i) => (i === index ? value : v))
)
}
/>
))}
{values.length !== groupBys.length && (
<Segment
Component={
<a className="gf-form-label query-part">
<Icon name="plus" />
</a>
}
allowCustomValue
onChange={({ value = '' }) => onChange([...values, value])}
options={[
variableOptionGroup,
...labelsToGroupedOptions([...groupBys.filter((groupBy) => !values.includes(groupBy)), ...systemLabels]),
]}
/>
)}
<div className="gf-form gf-form--grow">
<label className="gf-form-label gf-form-label--grow"></label>
</div>
</div>
);
};

View File

@ -1,139 +0,0 @@
import React from 'react';
import { MetricDescriptor } from '../types';
import { Icon } from '@grafana/ui';
export interface Props {
rawQuery: string;
lastQueryError?: string;
metricDescriptor?: MetricDescriptor;
}
interface State {
displayHelp: boolean;
displaRawQuery: boolean;
}
export class Help extends React.Component<Props, State> {
state: State = {
displayHelp: false,
displaRawQuery: false,
};
onHelpClicked = () => {
this.setState({ displayHelp: !this.state.displayHelp });
};
onRawQueryClicked = () => {
this.setState({ displaRawQuery: !this.state.displaRawQuery });
};
shouldComponentUpdate(nextProps: Props) {
return nextProps.metricDescriptor !== null;
}
render() {
const { displayHelp, displaRawQuery } = this.state;
const { rawQuery, lastQueryError } = this.props;
return (
<>
<div className="gf-form-inline">
<div className="gf-form" onClick={this.onHelpClicked}>
<label className="gf-form-label query-keyword pointer">
Show Help <Icon name={displayHelp ? 'angle-down' : 'angle-right'} />
</label>
</div>
{rawQuery && (
<div className="gf-form" onClick={this.onRawQueryClicked}>
<label className="gf-form-label query-keyword">
Raw query
<Icon
name={displaRawQuery ? 'angle-down' : 'angle-right'}
ng-show="ctrl.showHelp"
style={{ marginTop: '3px' }}
/>
</label>
</div>
)}
<div className="gf-form gf-form--grow">
<div className="gf-form-label gf-form-label--grow" />
</div>
</div>
{rawQuery && displaRawQuery && (
<div className="gf-form">
<pre className="gf-form-pre">{rawQuery}</pre>
</div>
)}
{displayHelp && (
<div className="gf-form grafana-info-box alert-info">
<div>
<h5>Alias Patterns</h5>Format the legend keys any way you want by using alias patterns. Format the legend
keys any way you want by using alias patterns.
<br /> <br />
Example:
<code>{`${'{{metric.name}} - {{metric.label.instance_name}}'}`}</code>
<br />
Result: &nbsp;&nbsp;<code>cpu/usage_time - server1-europe-west-1</code>
<br />
<br />
<strong>Patterns</strong>
<br />
<ul>
<li>
<code>{`${'{{metric.type}}'}`}</code> = metric type e.g.
compute.googleapis.com/instance/cpu/usage_time
</li>
<li>
<code>{`${'{{metric.name}}'}`}</code> = name part of metric e.g. instance/cpu/usage_time
</li>
<li>
<code>{`${'{{metric.service}}'}`}</code> = service part of metric e.g. compute
</li>
<li>
<code>{`${'{{metric.label.label_name}}'}`}</code> = Metric label metadata e.g.
metric.label.instance_name
</li>
<li>
<code>{`${'{{resource.label.label_name}}'}`}</code> = Resource label metadata e.g. resource.label.zone
</li>
<li>
<code>{`${'{{metadata.system_labels.name}}'}`}</code> = Meta data system labels e.g.
metadata.system_labels.name. For this to work, the needs to be included in the group by
</li>
<li>
<code>{`${'{{metadata.user_labels.name}}'}`}</code> = Meta data user labels e.g.
metadata.user_labels.name. For this to work, the needs to be included in the group by
</li>
<li>
<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>
)}
{lastQueryError && (
<div className="gf-form">
<pre className="gf-form-pre alert alert-error">{lastQueryError}</pre>
</div>
)}
</>
);
}
}

View File

@ -1,8 +1,13 @@
import React, { FunctionComponent, Fragment } from 'react';
import React, { FunctionComponent, useCallback, useMemo } from 'react';
import { flatten } from 'lodash';
import { SelectableValue } from '@grafana/data';
import { Segment, Icon } from '@grafana/ui';
import { labelsToGroupedOptions, filtersToStringArray, stringArrayToFilters, toOption } from '../functions';
import { CustomControlProps } from '@grafana/ui/src/components/Select/types';
import { Button, HorizontalGroup, Select, VerticalGroup } from '@grafana/ui';
import { labelsToGroupedOptions, stringArrayToFilters, toOption } from '../functions';
import { Filter } from '../types';
import { SELECT_WIDTH } from '../constants';
import { QueryEditorRow } from '.';
export interface Props {
labels: { [key: string]: string[] };
@ -11,82 +16,114 @@ export interface Props {
variableOptionGroup: SelectableValue<string>;
}
const removeText = '-- remove filter --';
const removeOption: SelectableValue<string> = { label: removeText, value: removeText, icon: 'times' };
const operators = ['=', '!=', '=~', '!=~'];
const FilterButton = React.forwardRef<HTMLButtonElement, CustomControlProps<string>>(
({ value, isOpen, invalid, ...rest }, ref) => {
return <Button ref={ref} {...rest} variant="secondary" icon="plus"></Button>;
}
);
FilterButton.displayName = 'FilterButton';
const OperatorButton = React.forwardRef<HTMLButtonElement, CustomControlProps<string>>(({ value, ...rest }, ref) => {
return (
<Button ref={ref} {...rest} variant="secondary">
<span className="query-segment-operator">{value?.label}</span>
</Button>
);
});
OperatorButton.displayName = 'OperatorButton';
export const LabelFilter: FunctionComponent<Props> = ({
labels = {},
filters: filterArray,
onChange,
variableOptionGroup,
}) => {
const filters = stringArrayToFilters(filterArray);
const filters = useMemo(() => stringArrayToFilters(filterArray), [filterArray]);
const options = useMemo(() => [variableOptionGroup, ...labelsToGroupedOptions(Object.keys(labels))], [
labels,
variableOptionGroup,
]);
const options = [removeOption, variableOptionGroup, ...labelsToGroupedOptions(Object.keys(labels))];
const filtersToStringArray = useCallback((filters: Filter[]) => {
const strArr = flatten(filters.map(({ key, operator, value, condition }) => [key, operator, value, condition!]));
return strArr.slice(0, strArr.length - 1);
}, []);
const AddFilter = () => {
return (
<Select
allowCustomValue
options={[variableOptionGroup, ...labelsToGroupedOptions(Object.keys(labels))]}
onChange={({ value: key = '' }) =>
onChange(filtersToStringArray([...filters, { key, operator: '=', condition: 'AND', value: '' }]))
}
menuPlacement="bottom"
renderControl={FilterButton}
/>
);
};
return (
<div className="gf-form-inline">
<label className="gf-form-label query-keyword width-9">Filter</label>
{filters.map(({ key, operator, value, condition }, index) => (
<Fragment key={index}>
<Segment
allowCustomValue
value={key}
options={options}
onChange={({ value: key = '' }) => {
if (key === removeText) {
onChange(filtersToStringArray(filters.filter((_, i) => i !== index)));
} else {
<QueryEditorRow
label="Filter"
tooltip={
'To reduce the amount of data charted, apply a filter. A filter has three components: a label, a comparison, and a value. The comparison can be an equality, inequality, or regular expression.'
}
noFillEnd={filters.length > 1}
>
<VerticalGroup spacing="xs" width="auto">
{filters.map(({ key, operator, value, condition }, index) => (
<HorizontalGroup key={index} spacing="xs" width="auto">
<Select
width={SELECT_WIDTH}
allowCustomValue
formatCreateLabel={(v) => `Use label key: ${v}`}
value={key}
options={options}
onChange={({ value: key = '' }) => {
onChange(
filtersToStringArray(
filters.map((f, i) => (i === index ? { key, operator, condition, value: '' } : f))
)
);
}}
/>
<Select
value={operator}
options={operators.map(toOption)}
onChange={({ value: operator = '=' }) =>
onChange(filtersToStringArray(filters.map((f, i) => (i === index ? { ...f, operator } : f))))
}
}}
/>
<Segment
value={operator}
className="gf-form-label query-segment-operator"
options={operators.map(toOption)}
onChange={({ value: operator = '=' }) =>
onChange(filtersToStringArray(filters.map((f, i) => (i === index ? { ...f, operator } : f))))
}
/>
<Segment
allowCustomValue
value={value}
placeholder="add filter value"
options={
labels.hasOwnProperty(key) ? [variableOptionGroup, ...labels[key].map(toOption)] : [variableOptionGroup]
}
onChange={({ value = '' }) =>
onChange(filtersToStringArray(filters.map((f, i) => (i === index ? { ...f, value } : f))))
}
/>
{filters.length > 1 && index + 1 !== filters.length && (
<label className="gf-form-label query-keyword">{condition}</label>
)}
</Fragment>
))}
{Object.values(filters).every(({ value }) => value) && (
<Segment
allowCustomValue
Component={
<a className="gf-form-label query-part">
<Icon name="plus" />
</a>
}
options={[variableOptionGroup, ...labelsToGroupedOptions(Object.keys(labels))]}
onChange={({ value: key = '' }) =>
onChange(filtersToStringArray([...filters, { key, operator: '=', condition: 'AND', value: '' } as Filter]))
}
/>
)}
<div className="gf-form gf-form--grow">
<label className="gf-form-label gf-form-label--grow"></label>
</div>
</div>
menuPlacement="bottom"
renderControl={OperatorButton}
/>
<Select
width={SELECT_WIDTH}
formatCreateLabel={(v) => `Use label value: ${v}`}
allowCustomValue
value={value}
placeholder="add filter value"
options={
labels.hasOwnProperty(key) ? [variableOptionGroup, ...labels[key].map(toOption)] : [variableOptionGroup]
}
onChange={({ value = '' }) =>
onChange(filtersToStringArray(filters.map((f, i) => (i === index ? { ...f, value } : f))))
}
/>
<Button
variant="secondary"
size="md"
icon="trash-alt"
aria-label="Remove"
onClick={() => onChange(filtersToStringArray(filters.filter((_, i) => i !== index)))}
></Button>
{index + 1 === filters.length && Object.values(filters).every(({ value }) => value) && <AddFilter />}
</HorizontalGroup>
))}
{!filters.length && <AddFilter />}
</VerticalGroup>
</QueryEditorRow>
);
};

View File

@ -1,14 +1,23 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useCallback } from 'react';
import { SelectableValue } from '@grafana/data';
import { Project, VisualMetricQueryEditor, AliasBy } from '.';
import { MetricQuery, MetricDescriptor, EditorMode } from '../types';
import {
MetricQuery,
MetricDescriptor,
EditorMode,
MetricKind,
PreprocessorType,
AlignmentTypes,
CustomMetaData,
ValueTypes,
} from '../types';
import { getAlignmentPickerData } from '../functions';
import CloudMonitoringDatasource from '../datasource';
import { SelectableValue } from '@grafana/data';
import { MQLQueryEditor } from './MQLQueryEditor';
export interface Props {
refId: string;
usedAlignmentPeriod?: number;
customMetaData: CustomMetaData;
variableOptionGroup: SelectableValue<string>;
onChange: (query: MetricQuery) => void;
onRunQuery: () => void;
@ -29,16 +38,16 @@ export const defaultQuery: (dataSource: CloudMonitoringDatasource) => MetricQuer
editorMode: EditorMode.Visual,
projectName: dataSource.getDefaultProject(),
metricType: '',
metricKind: '',
metricKind: MetricKind.GAUGE,
valueType: '',
unit: '',
crossSeriesReducer: 'REDUCE_MEAN',
alignmentPeriod: 'cloud-monitoring-auto',
perSeriesAligner: 'ALIGN_MEAN',
perSeriesAligner: AlignmentTypes.ALIGN_MEAN,
groupBys: [],
filters: [],
aliasBy: '',
query: '',
preprocessor: PreprocessorType.None,
});
function Editor({
@ -47,7 +56,7 @@ function Editor({
datasource,
onChange: onQueryChange,
onRunQuery,
usedAlignmentPeriod,
customMetaData,
variableOptionGroup,
}: React.PropsWithChildren<Props>) {
const [state, setState] = useState<State>(defaultState);
@ -61,22 +70,32 @@ function Editor({
}
}, [datasource, groupBys, metricType, projectName, refId]);
const onChange = (metricQuery: MetricQuery) => {
onQueryChange({ ...query, ...metricQuery });
onRunQuery();
};
const onChange = useCallback(
(metricQuery: MetricQuery) => {
onQueryChange({ ...query, ...metricQuery });
onRunQuery();
},
[onQueryChange, onRunQuery, query]
);
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 onMetricTypeChange = useCallback(
({ valueType, metricKind, type }: MetricDescriptor) => {
const preprocessor =
metricKind === MetricKind.GAUGE || valueType === ValueTypes.DISTRIBUTION
? PreprocessorType.None
: PreprocessorType.Rate;
const { perSeriesAligner } = getAlignmentPickerData(valueType, metricKind, state.perSeriesAligner, preprocessor);
onChange({
...query,
perSeriesAligner,
metricType: type,
valueType,
metricKind,
preprocessor,
});
},
[onChange, query, state]
);
return (
<>
@ -93,7 +112,7 @@ function Editor({
<VisualMetricQueryEditor
labels={state.labels}
variableOptionGroup={variableOptionGroup}
usedAlignmentPeriod={usedAlignmentPeriod}
customMetaData={customMetaData}
onMetricTypeChange={onMetricTypeChange}
onChange={onChange}
datasource={datasource}

View File

@ -1,10 +1,12 @@
import React, { useCallback, useEffect, useState } from 'react';
import { startCase, uniqBy } from 'lodash';
import { Select } from '@grafana/ui';
import { TemplateSrv } from '@grafana/runtime';
import { SelectableValue } from '@grafana/data';
import { QueryEditorRow, QueryEditorField } from '.';
import CloudMonitoringDatasource from '../datasource';
import { Segment } from '@grafana/ui';
import { INNER_LABEL_WIDTH, LABEL_WIDTH, SELECT_WIDTH } from '../constants';
import { MetricDescriptor } from '../types';
export interface Props {
@ -118,44 +120,39 @@ export function Metrics(props: Props) {
return (
<>
<div className="gf-form-inline">
<span className="gf-form-label width-9 query-keyword">Service</span>
<Segment
onChange={onServiceChange}
value={[...services, ...templateVariableOptions].find((s) => s.value === service)}
options={[
{
label: 'Template Variables',
options: templateVariableOptions,
},
...services,
]}
placeholder="Select Services"
></Segment>
<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>
<QueryEditorRow>
<QueryEditorField labelWidth={LABEL_WIDTH} label="Service">
<Select
width={SELECT_WIDTH}
onChange={onServiceChange}
value={[...services, ...templateVariableOptions].find((s) => s.value === service)}
options={[
{
label: 'Template Variables',
options: templateVariableOptions,
},
...services,
]}
placeholder="Select Services"
></Select>
</QueryEditorField>
<QueryEditorField label="Metric name" labelWidth={INNER_LABEL_WIDTH}>
<Select
width={SELECT_WIDTH}
onChange={onMetricTypeChange}
value={[...metrics, ...templateVariableOptions].find((s) => s.value === metricType)}
options={[
{
label: 'Template Variables',
options: templateVariableOptions,
},
...metrics,
]}
placeholder="Select Metric"
></Select>
</QueryEditorField>
</QueryEditorRow>
<Segment
className="query-part"
onChange={onMetricTypeChange}
value={[...metrics, ...templateVariableOptions].find((s) => s.value === metricType)}
options={[
{
label: 'Template Variables',
options: templateVariableOptions,
},
...metrics,
]}
placeholder="Select Metric"
></Segment>
<div className="gf-form gf-form--grow">
<div className="gf-form-label gf-form-label--grow" />
</div>
</div>
{children(state.metricDescriptor)}
</>
);

View File

@ -0,0 +1,65 @@
import React, { FunctionComponent, useMemo } from 'react';
import { SelectableValue } from '@grafana/data';
import { RadioButtonGroup } from '@grafana/ui';
import { MetricDescriptor, MetricKind, MetricQuery, PreprocessorType, ValueTypes } from '../types';
import { getAlignmentPickerData } from '../functions';
import { QueryEditorRow } from '.';
const NONE_OPTION = { label: 'None', value: PreprocessorType.None };
export interface Props {
metricDescriptor?: MetricDescriptor;
onChange: (query: MetricQuery) => void;
query: MetricQuery;
}
export const Preprocessor: FunctionComponent<Props> = ({ query, metricDescriptor, onChange }) => {
const options = useOptions(metricDescriptor);
return (
<QueryEditorRow
label="Pre-processing"
tooltip="Preprocessing options are displayed when the selected metric has a metric kind of delta or cumulative. The specific options available are determined by the metic's value type. If you select 'Rate', data points are aligned and converted to a rate per time series. If you select 'Delta', data points are aligned by their delta (difference) per time series"
>
<RadioButtonGroup
onChange={(value: PreprocessorType) => {
const { valueType, metricKind, perSeriesAligner: psa } = query;
const { perSeriesAligner } = getAlignmentPickerData(valueType, metricKind, psa, value);
onChange({ ...query, preprocessor: value, perSeriesAligner });
}}
value={query.preprocessor ?? PreprocessorType.None}
options={options}
></RadioButtonGroup>
</QueryEditorRow>
);
};
const useOptions = (metricDescriptor?: MetricDescriptor): Array<SelectableValue<string>> => {
const metricKind = metricDescriptor?.metricKind;
const valueType = metricDescriptor?.valueType;
return useMemo(() => {
if (!metricKind || metricKind === MetricKind.GAUGE || valueType === ValueTypes.DISTRIBUTION) {
return [NONE_OPTION];
}
const options = [
NONE_OPTION,
{
label: 'Rate',
value: PreprocessorType.Rate,
description: 'Data points are aligned and converted to a rate per time series',
},
];
return metricKind === MetricKind.CUMULATIVE
? [
...options,
{
label: 'Delta',
value: PreprocessorType.Delta,
description: 'Data points are aligned by their delta (difference) per time series',
},
]
: options;
}, [metricKind, valueType]);
};

View File

@ -1,7 +1,9 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import { SelectableValue } from '@grafana/data';
import { SegmentAsync } from '@grafana/ui';
import { Select } from '@grafana/ui';
import CloudMonitoringDatasource from '../datasource';
import { SELECT_WIDTH } from '../constants';
import { QueryEditorRow } from '.';
export interface Props {
datasource: CloudMonitoringDatasource;
@ -11,27 +13,30 @@ export interface Props {
}
export function Project({ projectName, datasource, onChange, templateVariableOptions }: Props) {
const [projects, setProjects] = useState<Array<SelectableValue<string>>>([]);
useEffect(() => {
datasource.getProjects().then((projects) =>
setProjects([
{
label: 'Template Variables',
options: templateVariableOptions,
},
...projects,
])
);
}, [datasource, templateVariableOptions]);
return (
<div className="gf-form-inline">
<span className="gf-form-label width-9 query-keyword">Project</span>
<SegmentAsync
<QueryEditorRow label="Project">
<Select
width={SELECT_WIDTH}
allowCustomValue
formatCreateLabel={(v) => `Use project: ${v}`}
onChange={({ value }) => onChange(value!)}
loadOptions={() =>
datasource.getProjects().then((projects) => [
{
label: 'Template Variables',
options: templateVariableOptions,
},
...projects,
])
}
value={projectName}
options={projects}
value={{ value: projectName, label: projectName }}
placeholder="Select Project"
/>
<div className="gf-form gf-form--grow">
<div className="gf-form-label gf-form-label--grow" />
</div>
</div>
</QueryEditorRow>
);
}

View File

@ -1,24 +1,18 @@
import React, { PureComponent } from 'react';
import appEvents from 'app/core/app_events';
import { CoreEvents } from 'app/types';
import { ExploreQueryFieldProps, SelectableValue } from '@grafana/data';
import { Segment } from '@grafana/ui';
import { Help, MetricQueryEditor, SLOQueryEditor } from './';
import { CloudMonitoringQuery, MetricQuery, QueryType, SLOQuery, queryTypes, EditorMode } from '../types';
import { css } from '@emotion/css';
import { ExploreQueryFieldProps } from '@grafana/data';
import { Button, Select } from '@grafana/ui';
import { MetricQueryEditor, SLOQueryEditor, QueryEditorRow } from './';
import { CloudMonitoringQuery, MetricQuery, QueryType, SLOQuery, EditorMode } from '../types';
import { SELECT_WIDTH, QUERY_TYPES } from '../constants';
import { defaultQuery } from './MetricQueryEditor';
import { defaultQuery as defaultSLOQuery } from './SLOQueryEditor';
import { formatCloudMonitoringError, toOption } from '../functions';
import { defaultQuery as defaultSLOQuery } from './SLO/SLOQueryEditor';
import { toOption } from '../functions';
import CloudMonitoringDatasource from '../datasource';
export type Props = ExploreQueryFieldProps<CloudMonitoringDatasource, CloudMonitoringQuery>;
interface State {
lastQueryError: string;
}
export class QueryEditor extends PureComponent<Props, State> {
state: State = { lastQueryError: '' };
export class QueryEditor extends PureComponent<Props> {
async UNSAFE_componentWillMount() {
const { datasource, query } = this.props;
@ -39,24 +33,6 @@ export class QueryEditor extends PureComponent<Props, State> {
}
}
componentDidMount() {
appEvents.on(CoreEvents.dsRequestError, this.onDataError.bind(this));
appEvents.on(CoreEvents.dsRequestResponse, this.onDataResponse.bind(this));
}
componentWillUnmount() {
appEvents.off(CoreEvents.dsRequestResponse, this.onDataResponse.bind(this));
appEvents.on(CoreEvents.dsRequestError, this.onDataError.bind(this));
}
onDataResponse() {
this.setState({ lastQueryError: '' });
}
onDataError(error: any) {
this.setState({ lastQueryError: formatCloudMonitoringError(error) });
}
onQueryChange(prop: string, value: any) {
this.props.onChange({ ...this.props.query, [prop]: value });
this.props.onRunQuery();
@ -68,7 +44,7 @@ export class QueryEditor extends PureComponent<Props, State> {
const sloQuery = { ...defaultSLOQuery(datasource), ...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;
const customMetaData = meta?.custom ?? {};
const variableOptionGroup = {
label: 'Template Variables',
expanded: false,
@ -77,48 +53,44 @@ export class QueryEditor extends PureComponent<Props, State> {
return (
<>
<div className="gf-form-inline">
<label className="gf-form-label query-keyword width-9">Query Type</label>
<Segment
value={[...queryTypes, ...variableOptionGroup.options].find((qt) => qt.value === queryType)}
options={[
...queryTypes,
{
label: 'Template Variables',
options: variableOptionGroup.options,
},
]}
onChange={({ value }: SelectableValue<QueryType>) => {
<QueryEditorRow
label="Query type"
fillComponent={
query.queryType !== QueryType.SLO && (
<Button
variant="secondary"
className={css`
margin-left: auto;
`}
icon="edit"
onClick={() =>
this.onQueryChange('metricQuery', {
...metricQuery,
editorMode: metricQuery.editorMode === EditorMode.MQL ? EditorMode.Visual : EditorMode.MQL,
})
}
>
{metricQuery.editorMode === EditorMode.MQL ? 'Switch to builder' : 'Edit MQL'}
</Button>
)
}
>
<Select
width={SELECT_WIDTH}
value={queryType}
options={QUERY_TYPES}
onChange={({ value }) => {
onChange({ ...query, sloQuery, queryType: value! });
onRunQuery();
}}
/>
{query.queryType !== QueryType.SLO && (
<button
className="gf-form-label "
onClick={() =>
this.onQueryChange('metricQuery', {
...metricQuery,
editorMode: metricQuery.editorMode === EditorMode.MQL ? EditorMode.Visual : EditorMode.MQL,
})
}
>
<span className="query-keyword">{'<>'}</span>&nbsp;&nbsp;
{metricQuery.editorMode === EditorMode.MQL ? 'Switch to builder' : 'Edit MQL'}
</button>
)}
<div className="gf-form gf-form--grow">
<label className="gf-form-label gf-form-label--grow"></label>
</div>
</div>
</QueryEditorRow>
{queryType === QueryType.METRICS && (
<MetricQueryEditor
refId={query.refId}
variableOptionGroup={variableOptionGroup}
usedAlignmentPeriod={usedAlignmentPeriod}
customMetaData={customMetaData}
onChange={(metricQuery: MetricQuery) => {
this.props.onChange({ ...this.props.query, metricQuery });
}}
@ -131,18 +103,13 @@ export class QueryEditor extends PureComponent<Props, State> {
{queryType === QueryType.SLO && (
<SLOQueryEditor
variableOptionGroup={variableOptionGroup}
usedAlignmentPeriod={usedAlignmentPeriod}
customMetaData={customMetaData}
onChange={(query: SLOQuery) => this.onQueryChange('sloQuery', query)}
onRunQuery={onRunQuery}
datasource={datasource}
query={sloQuery}
></SLOQueryEditor>
)}
<Help
rawQuery={decodeURIComponent(meta?.executedQueryString ?? '')}
lastQueryError={this.state.lastQueryError}
/>
</>
);
}

View File

@ -1,7 +1,8 @@
import React, { FunctionComponent } from 'react';
import { SelectableValue } from '@grafana/data';
import { Segment } from '@grafana/ui';
import { QueryType, queryTypes } from '../types';
import { QueryType } from '../types';
import { QUERY_TYPES } from '../constants';
export interface Props {
value: QueryType;
@ -14,9 +15,9 @@ export const QueryTypeSelector: FunctionComponent<Props> = ({ onChange, value, t
<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)}
value={[...QUERY_TYPES, ...templateVariableOptions].find((qt) => qt.value === value)}
options={[
...queryTypes,
...QUERY_TYPES,
{
label: 'Template Variables',
options: templateVariableOptions,

View File

@ -0,0 +1,52 @@
import React, { useEffect, useState } from 'react';
import { Select } from '@grafana/ui';
import { SelectableValue } from '@grafana/data';
import { QueryEditorRow } from '..';
import CloudMonitoringDatasource from '../../datasource';
import { SLOQuery } from '../../types';
import { SELECT_WIDTH } from '../../constants';
export interface Props {
onChange: (query: SLOQuery) => void;
query: SLOQuery;
templateVariableOptions: Array<SelectableValue<string>>;
datasource: CloudMonitoringDatasource;
}
export const SLO: React.FC<Props> = ({ query, templateVariableOptions, onChange, datasource }) => {
const [slos, setSLOs] = useState<Array<SelectableValue<string>>>([]);
const { projectName, serviceId } = query;
useEffect(() => {
if (!projectName || !serviceId) {
return;
}
datasource.getServiceLevelObjectives(projectName, serviceId).then((sloIds: Array<SelectableValue<string>>) => {
setSLOs([
{
label: 'Template Variables',
options: templateVariableOptions,
},
...sloIds,
]);
});
}, [datasource, projectName, serviceId, templateVariableOptions]);
return (
<QueryEditorRow label="SLO">
<Select
width={SELECT_WIDTH}
allowCustomValue
value={query?.sloId && { value: query?.sloId, label: query?.sloName || query?.sloId }}
placeholder="Select SLO"
options={slos}
onChange={async ({ value: sloId = '', label: sloName = '' }) => {
const slos = await datasource.getServiceLevelObjectives(projectName, serviceId);
const slo = slos.find(({ value }) => value === datasource.templateSrv.replace(sloId));
onChange({ ...query, sloId, sloName, goal: slo?.goal });
}}
/>
</QueryEditorRow>
);
};

View File

@ -0,0 +1,80 @@
import React from 'react';
import { SelectableValue } from '@grafana/data';
import { Project, AliasBy, AlignmentPeriod, AlignmentPeriodLabel, QueryEditorRow } from '..';
import { AlignmentTypes, CustomMetaData, SLOQuery } from '../../types';
import CloudMonitoringDatasource from '../../datasource';
import { Selector, Service, SLO } from '.';
import { SELECT_WIDTH } from '../../constants';
export interface Props {
customMetaData: CustomMetaData;
variableOptionGroup: SelectableValue<string>;
onChange: (query: SLOQuery) => void;
onRunQuery: () => void;
query: SLOQuery;
datasource: CloudMonitoringDatasource;
}
export const defaultQuery: (dataSource: CloudMonitoringDatasource) => SLOQuery = (dataSource) => ({
projectName: dataSource.getDefaultProject(),
alignmentPeriod: 'cloud-monitoring-auto',
perSeriesAligner: AlignmentTypes.ALIGN_MEAN,
aliasBy: '',
selectorName: 'select_slo_health',
serviceId: '',
serviceName: '',
sloId: '',
sloName: '',
});
export function SLOQueryEditor({
query,
datasource,
onChange,
variableOptionGroup,
customMetaData,
}: React.PropsWithChildren<Props>) {
return (
<>
<Project
templateVariableOptions={variableOptionGroup.options}
projectName={query.projectName}
datasource={datasource}
onChange={(projectName) => onChange({ ...query, projectName })}
/>
<Service
datasource={datasource}
templateVariableOptions={variableOptionGroup.options}
query={query}
onChange={onChange}
></Service>
<SLO
datasource={datasource}
templateVariableOptions={variableOptionGroup.options}
query={query}
onChange={onChange}
></SLO>
<Selector
datasource={datasource}
templateVariableOptions={variableOptionGroup.options}
query={query}
onChange={onChange}
></Selector>
<QueryEditorRow label="Alignment period">
<AlignmentPeriod
templateVariableOptions={variableOptionGroup.options}
query={{
...query,
perSeriesAligner: query.selectorName === 'select_slo_health' ? 'ALIGN_MEAN' : 'ALIGN_NEXT_OLDER',
}}
onChange={onChange}
selectWidth={SELECT_WIDTH}
/>
<AlignmentPeriodLabel datasource={datasource} customMetaData={customMetaData} />
</QueryEditorRow>
<AliasBy value={query.aliasBy} onChange={(aliasBy) => onChange({ ...query, aliasBy })} />
</>
);
}

View File

@ -0,0 +1,34 @@
import React from 'react';
import { Select } from '@grafana/ui';
import { SelectableValue } from '@grafana/data';
import { QueryEditorRow } from '..';
import CloudMonitoringDatasource from '../../datasource';
import { SLOQuery } from '../../types';
import { SELECT_WIDTH, SELECTORS } from '../../constants';
export interface Props {
onChange: (query: SLOQuery) => void;
query: SLOQuery;
templateVariableOptions: Array<SelectableValue<string>>;
datasource: CloudMonitoringDatasource;
}
export const Selector: React.FC<Props> = ({ query, templateVariableOptions, onChange, datasource }) => {
return (
<QueryEditorRow label="Selector">
<Select
width={SELECT_WIDTH}
allowCustomValue
value={[...SELECTORS, ...templateVariableOptions].find((s) => s.value === query?.selectorName ?? '')}
options={[
{
label: 'Template Variables',
options: templateVariableOptions,
},
...SELECTORS,
]}
onChange={({ value: selectorName }) => onChange({ ...query, selectorName: selectorName ?? '' })}
/>
</QueryEditorRow>
);
};

View File

@ -0,0 +1,50 @@
import React, { useEffect, useState } from 'react';
import { Select } from '@grafana/ui';
import { SelectableValue } from '@grafana/data';
import { QueryEditorRow } from '..';
import CloudMonitoringDatasource from '../../datasource';
import { SLOQuery } from '../../types';
import { SELECT_WIDTH } from '../../constants';
export interface Props {
onChange: (query: SLOQuery) => void;
query: SLOQuery;
templateVariableOptions: Array<SelectableValue<string>>;
datasource: CloudMonitoringDatasource;
}
export const Service: React.FC<Props> = ({ query, templateVariableOptions, onChange, datasource }) => {
const [services, setServices] = useState<Array<SelectableValue<string>>>([]);
const { projectName } = query;
useEffect(() => {
if (!projectName) {
return;
}
datasource.getSLOServices(projectName).then((services: Array<SelectableValue<string>>) => {
setServices([
{
label: 'Template Variables',
options: templateVariableOptions,
},
...services,
]);
});
}, [datasource, projectName, templateVariableOptions]);
return (
<QueryEditorRow label="Service">
<Select
width={SELECT_WIDTH}
allowCustomValue
value={query?.serviceId && { value: query?.serviceId, label: query?.serviceName || query?.serviceId }}
placeholder="Select service"
options={services}
onChange={({ value: serviceId = '', label: serviceName = '' }) =>
onChange({ ...query, serviceId, serviceName, sloId: '' })
}
/>
</QueryEditorRow>
);
};

View File

@ -0,0 +1,3 @@
export { Service } from './Service';
export { SLO } from './SLO';
export { Selector } from './Selector';

View File

@ -1,112 +0,0 @@
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 CloudMonitoringDatasource from '../datasource';
export interface Props {
usedAlignmentPeriod?: number;
variableOptionGroup: SelectableValue<string>;
onChange: (query: SLOQuery) => void;
onRunQuery: () => void;
query: SLOQuery;
datasource: CloudMonitoringDatasource;
}
export const defaultQuery: (dataSource: CloudMonitoringDatasource) => SLOQuery = (dataSource) => ({
projectName: dataSource.getDefaultProject(),
alignmentPeriod: 'cloud-monitoring-auto',
aliasBy: '',
selectorName: 'select_slo_health',
serviceId: '',
serviceName: '',
sloId: '',
sloName: '',
});
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={{ value: query?.serviceId, label: query?.serviceName || query?.serviceId }}
placeholder="Select service"
loadOptions={() =>
datasource.getSLOServices(query.projectName).then((services) => [
{
label: 'Template Variables',
options: variableOptionGroup.options,
},
...services,
])
}
onChange={({ value: serviceId = '', label: serviceName = '' }) =>
onChange({ ...query, serviceId, serviceName, sloId: '' })
}
/>
</QueryInlineField>
<QueryInlineField label="SLO">
<SegmentAsync
allowCustomValue
value={{ value: query?.sloId, label: query?.sloName || 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 = '', label: sloName = '' }) => {
const slos = await datasource.getServiceLevelObjectives(query.projectName, query.serviceId);
const slo = slos.find(({ value }) => value === datasource.templateSrv.replace(sloId));
onChange({ ...query, sloId, sloName, 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 })} />
</>
);
}

View File

@ -1,12 +1,11 @@
import React from 'react';
import { Aggregations, Metrics, LabelFilter, GroupBys, Alignments, AlignmentPeriods } from '.';
import { MetricQuery, MetricDescriptor } from '../types';
import { getAlignmentPickerData } from '../functions';
import CloudMonitoringDatasource from '../datasource';
import { SelectableValue } from '@grafana/data';
import { Metrics, LabelFilter, GroupBy, Preprocessor, Alignment } from '.';
import { MetricQuery, MetricDescriptor, CustomMetaData } from '../types';
import CloudMonitoringDatasource from '../datasource';
export interface Props {
usedAlignmentPeriod?: number;
customMetaData: CustomMetaData;
variableOptionGroup: SelectableValue<string>;
onMetricTypeChange: (query: MetricDescriptor) => void;
onChange: (query: MetricQuery) => void;
@ -21,11 +20,9 @@ function Editor({
datasource,
onChange,
onMetricTypeChange,
usedAlignmentPeriod,
customMetaData,
variableOptionGroup,
}: React.PropsWithChildren<Props>) {
const { perSeriesAligner, alignOptions } = getAlignmentPickerData(query, datasource.templateSrv);
return (
<Metrics
templateSrv={datasource.templateSrv}
@ -40,40 +37,23 @@ function Editor({
<LabelFilter
labels={labels}
filters={query.filters!}
onChange={(filters) => onChange({ ...query, filters })}
onChange={(filters: string[]) => onChange({ ...query, filters })}
variableOptionGroup={variableOptionGroup}
/>
<GroupBys
groupBys={Object.keys(labels)}
values={query.groupBys!}
onChange={(groupBys) => onChange({ ...query, groupBys })}
<Preprocessor metricDescriptor={metric} query={query} onChange={onChange} />
<GroupBy
labels={Object.keys(labels)}
query={query}
onChange={onChange}
variableOptionGroup={variableOptionGroup}
/>
<Aggregations
metricDescriptor={metric}
/>
<Alignment
datasource={datasource}
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 })}
query={query}
customMetaData={customMetaData}
onChange={onChange}
/>
</>
)}

View File

@ -1,16 +1,18 @@
export { Project } from './Project';
export { Metrics } from './Metrics';
export { Help } from './Help';
export { GroupBys } from './GroupBys';
export { GroupBy } from './GroupBy';
export { Alignment } from './Alignment';
export { LabelFilter } from './LabelFilter';
export { AnnotationsHelp } from './AnnotationsHelp';
export { Alignments } from './Alignments';
export { AlignmentPeriods } from './AlignmentPeriods';
export { AlignmentFunction } from './AlignmentFunction';
export { AlignmentPeriod } from './AlignmentPeriod';
export { AlignmentPeriodLabel } from './AlignmentPeriodLabel';
export { AliasBy } from './AliasBy';
export { Aggregations } from './Aggregations';
export { Aggregation } from './Aggregation';
export { MetricQueryEditor } from './MetricQueryEditor';
export { SLOQueryEditor } from './SLOQueryEditor';
export { SLOQueryEditor } from './SLO/SLOQueryEditor';
export { MQLQueryEditor } from './MQLQueryEditor';
export { QueryTypeSelector } from './QueryType';
export { QueryInlineField, QueryField, VariableQueryField } from './Fields';
export { VariableQueryField, QueryEditorRow, QueryEditorField } from './Fields';
export { VisualMetricQueryEditor } from './VisualMetricQueryEditor';
export { Preprocessor } from './Preprocessor';

View File

@ -1,21 +1,16 @@
export enum MetricKind {
METRIC_KIND_UNSPECIFIED = 'METRIC_KIND_UNSPECIFIED',
GAUGE = 'GAUGE',
DELTA = 'DELTA',
CUMULATIVE = 'CUMULATIVE',
}
import { AuthType, MetricKind, QueryType, ValueTypes } from './types';
export enum ValueTypes {
VALUE_TYPE_UNSPECIFIED = 'VALUE_TYPE_UNSPECIFIED',
BOOL = 'BOOL',
INT64 = 'INT64',
DOUBLE = 'DOUBLE',
STRING = 'STRING',
DISTRIBUTION = 'DISTRIBUTION',
MONEY = 'MONEY',
}
// not super excited about using uneven numbers, but this makes it align perfectly with rows that has two fields
export const INPUT_WIDTH = 71;
export const LABEL_WIDTH = 19;
export const INNER_LABEL_WIDTH = 14;
export const SELECT_WIDTH = 28;
export const AUTH_TYPES = [
{ value: 'Google JWT File', key: AuthType.JWT },
{ value: 'GCE Default Service Account', key: AuthType.GCE },
];
export const alignOptions = [
export const ALIGNMENTS = [
{
text: 'delta',
value: 'ALIGN_DELTA',
@ -134,7 +129,7 @@ export const alignOptions = [
},
];
export const aggOptions = [
export const AGGREGATIONS = [
{
text: 'none',
value: 'REDUCE_NONE',
@ -229,7 +224,7 @@ export const aggOptions = [
},
];
export const alignmentPeriods = [
export const ALIGNMENT_PERIODS = [
{ text: 'grafana auto', value: 'grafana-auto' },
{ text: 'stackdriver auto', value: 'stackdriver-auto', hidden: true },
{ text: 'cloud monitoring auto', value: 'cloud-monitoring-auto' },
@ -246,7 +241,7 @@ export const alignmentPeriods = [
{ text: '1w', value: '+604800s' },
];
export const systemLabels = [
export const SYSTEM_LABELS = [
'metadata.system_labels.cloud_account',
'metadata.system_labels.name',
'metadata.system_labels.region',
@ -259,8 +254,13 @@ export const systemLabels = [
'metadata.system_labels.container_image',
];
export const selectors = [
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' },
];
export const QUERY_TYPES = [
{ label: 'Metrics', value: QueryType.METRICS },
{ label: 'Service Level Objectives (SLO)', value: QueryType.SLO },
];

View File

@ -34,6 +34,7 @@ export default class CloudMonitoringDatasource extends DataSourceWithBackend<
this.authenticationType = instanceSettings.jsonData.authenticationType || 'jwt';
this.api = new API(`${instanceSettings.url!}/cloudmonitoring/v3/projects/`);
this.variables = new CloudMonitoringVariableSupport(this);
this.intervalMs = 0;
}
getVariables() {
@ -250,7 +251,7 @@ export default class CloudMonitoringDatasource extends DataSourceWithBackend<
}
return this.api.get(`${this.templateSrv.replace(projectName)}/metricDescriptors`, {
responseMap: (m: any) => {
responseMap: (m: MetricDescriptor) => {
const [service] = m.type.split('/');
const [serviceShortName] = service.split('.');
m.service = service;
@ -285,7 +286,7 @@ export default class CloudMonitoringDatasource extends DataSourceWithBackend<
});
}
getProjects() {
getProjects(): Promise<Array<SelectableValue<string>>> {
return this.api.get(`projects`, {
responseMap: ({ projectId, name }: { projectId: string; name: string }) => ({
value: projectId,
@ -346,7 +347,7 @@ export default class CloudMonitoringDatasource extends DataSourceWithBackend<
}
interpolateFilters(filters: string[], scopedVars: ScopedVars) {
const completeFilter = chunk(filters, 4)
const completeFilter: Filter[] = chunk(filters, 4)
.map(([key, operator, value, condition]) => ({
key,
operator,

View File

@ -1,5 +1,5 @@
import { getAlignmentOptionsByMetric } from './functions';
import { ValueTypes, MetricKind } from './constants';
import { ValueTypes, MetricKind } from './types';
describe('functions', () => {
let result: any;

View File

@ -1,9 +1,11 @@
import { chunk, flatten, initial, startCase, uniqBy } from 'lodash';
import { alignOptions, aggOptions, ValueTypes, MetricKind, systemLabels } from './constants';
import { ALIGNMENTS, AGGREGATIONS, SYSTEM_LABELS } from './constants';
import { SelectableValue } from '@grafana/data';
import CloudMonitoringDatasource from './datasource';
import { TemplateSrv } from '@grafana/runtime';
import { MetricDescriptor, Filter, MetricQuery } from './types';
import { TemplateSrv, getTemplateSrv } from '@grafana/runtime';
import { MetricDescriptor, ValueTypes, MetricKind, AlignmentTypes, PreprocessorType, Filter } from './types';
const templateSrv: TemplateSrv = getTemplateSrv();
export const extractServicesFromMetricDescriptors = (metricDescriptors: MetricDescriptor[]) =>
uniqBy(metricDescriptors, 'service');
@ -17,7 +19,7 @@ export const getMetricTypes = (
interpolatedMetricType: string,
selectedService: string
) => {
const metricTypes = getMetricTypesByService(metricDescriptors, selectedService).map((m: any) => ({
const metricTypes = getMetricTypesByService(metricDescriptors, selectedService).map((m) => ({
value: m.type,
name: m.displayName,
}));
@ -32,10 +34,18 @@ export const getMetricTypes = (
};
};
export const getAlignmentOptionsByMetric = (metricValueType: string, metricKind: string) => {
export const getAlignmentOptionsByMetric = (
metricValueType: string,
metricKind: string,
preprocessor?: PreprocessorType
) => {
if (preprocessor && preprocessor === PreprocessorType.Rate) {
metricKind = MetricKind.GAUGE;
}
return !metricValueType
? []
: alignOptions.filter((i) => {
: ALIGNMENTS.filter((i) => {
return (
i.valueTypes.indexOf(metricValueType as ValueTypes) !== -1 &&
i.metricKinds.indexOf(metricKind as MetricKind) !== -1
@ -46,7 +56,7 @@ export const getAlignmentOptionsByMetric = (metricValueType: string, metricKind:
export const getAggregationOptionsByMetric = (valueType: ValueTypes, metricKind: MetricKind) => {
return !metricKind
? []
: aggOptions.filter((i) => {
: AGGREGATIONS.filter((i) => {
return i.valueTypes.indexOf(valueType) !== -1 && i.metricKinds.indexOf(metricKind) !== -1;
});
};
@ -58,19 +68,21 @@ export const getLabelKeys = async (
) => {
const refId = 'handleLabelKeysQuery';
const labels = await datasource.getLabels(selectedMetricType, refId, projectName);
return [...Object.keys(labels), ...systemLabels];
return [...Object.keys(labels), ...SYSTEM_LABELS];
};
export const getAlignmentPickerData = (
{ valueType, metricKind, perSeriesAligner }: Partial<MetricQuery>,
templateSrv: TemplateSrv
valueType: string | undefined = ValueTypes.DOUBLE,
metricKind: string | undefined = MetricKind.GAUGE,
perSeriesAligner: string | undefined = AlignmentTypes.ALIGN_MEAN,
preprocessor?: PreprocessorType
) => {
const alignOptions = getAlignmentOptionsByMetric(valueType!, metricKind!).map((option) => ({
const alignOptions = getAlignmentOptionsByMetric(valueType!, metricKind!, preprocessor!).map((option) => ({
...option,
label: option.text,
}));
if (!alignOptions.some((o: { value: string }) => o.value === templateSrv.replace(perSeriesAligner!))) {
perSeriesAligner = alignOptions.length > 0 ? alignOptions[0].value : '';
if (!alignOptions.some((o: { value: string }) => o.value === templateSrv.replace(perSeriesAligner))) {
perSeriesAligner = alignOptions.length > 0 ? alignOptions[0].value : AlignmentTypes.ALIGN_MEAN;
}
return { alignOptions, perSeriesAligner };
};

View File

@ -3,11 +3,13 @@ import CloudMonitoringDatasource from './datasource';
import { QueryEditor } from './components/QueryEditor';
import { ConfigEditor } from './components/ConfigEditor/ConfigEditor';
import CloudMonitoringCheatSheet from './components/CloudMonitoringCheatSheet';
import { CloudMonitoringAnnotationsQueryCtrl } from './annotations_query_ctrl';
import { CloudMonitoringVariableQueryEditor } from './components/VariableQueryEditor';
import { CloudMonitoringQuery } from './types';
export const plugin = new DataSourcePlugin<CloudMonitoringDatasource, CloudMonitoringQuery>(CloudMonitoringDatasource)
.setQueryEditorHelp(CloudMonitoringCheatSheet)
.setQueryEditor(QueryEditor)
.setConfigEditor(ConfigEditor)
.setAnnotationQueryCtrl(CloudMonitoringAnnotationsQueryCtrl)

View File

@ -63,33 +63,71 @@ export enum EditorMode {
MQL = 'mql',
}
export const queryTypes = [
{ label: 'Metrics', value: QueryType.METRICS },
{ label: 'Service Level Objectives (SLO)', value: QueryType.SLO },
];
export enum PreprocessorType {
None = 'none',
Rate = 'rate',
Delta = 'delta',
}
export interface MetricQuery {
editorMode: EditorMode;
export enum MetricKind {
METRIC_KIND_UNSPECIFIED = 'METRIC_KIND_UNSPECIFIED',
GAUGE = 'GAUGE',
DELTA = 'DELTA',
CUMULATIVE = 'CUMULATIVE',
}
export enum ValueTypes {
VALUE_TYPE_UNSPECIFIED = 'VALUE_TYPE_UNSPECIFIED',
BOOL = 'BOOL',
INT64 = 'INT64',
DOUBLE = 'DOUBLE',
STRING = 'STRING',
DISTRIBUTION = 'DISTRIBUTION',
MONEY = 'MONEY',
}
export enum AlignmentTypes {
ALIGN_DELTA = 'ALIGN_DELTA',
ALIGN_RATE = 'ALIGN_RATE',
ALIGN_INTERPOLATE = 'ALIGN_INTERPOLATE',
ALIGN_NEXT_OLDER = 'ALIGN_NEXT_OLDER',
ALIGN_MIN = 'ALIGN_MIN',
ALIGN_MAX = 'ALIGN_MAX',
ALIGN_MEAN = 'ALIGN_MEAN',
ALIGN_COUNT = 'ALIGN_COUNT',
ALIGN_SUM = 'ALIGN_SUM',
ALIGN_STDDEV = 'ALIGN_STDDEV',
ALIGN_COUNT_TRUE = 'ALIGN_COUNT_TRUE',
ALIGN_COUNT_FALSE = 'ALIGN_COUNT_FALSE',
ALIGN_FRACTION_TRUE = 'ALIGN_FRACTION_TRUE',
ALIGN_PERCENTILE_99 = 'ALIGN_PERCENTILE_99',
ALIGN_PERCENTILE_95 = 'ALIGN_PERCENTILE_95',
ALIGN_PERCENTILE_50 = 'ALIGN_PERCENTILE_50',
ALIGN_PERCENTILE_05 = 'ALIGN_PERCENTILE_05',
ALIGN_PERCENT_CHANGE = 'ALIGN_PERCENT_CHANGE',
}
export interface BaseQuery {
projectName: string;
unit?: string;
perSeriesAligner?: string;
alignmentPeriod?: string;
aliasBy?: string;
}
export interface MetricQuery extends BaseQuery {
editorMode: EditorMode;
metricType: string;
crossSeriesReducer: string;
alignmentPeriod?: string;
perSeriesAligner?: string;
groupBys?: string[];
filters?: string[];
aliasBy?: string;
metricKind?: string;
metricKind?: MetricKind;
valueType?: string;
view?: string;
query: string;
preprocessor?: PreprocessorType;
}
export interface SLOQuery {
projectName: string;
alignmentPeriod?: string;
perSeriesAligner?: string;
aliasBy?: string;
export interface SLOQuery extends BaseQuery {
selectorName: string;
serviceId: string;
serviceName: string;
@ -124,7 +162,7 @@ export interface AnnotationTarget {
metricType: string;
refId: string;
filters: string[];
metricKind: string;
metricKind: MetricKind;
valueType: string;
title: string;
text: string;
@ -141,7 +179,7 @@ export interface QueryMeta {
export interface MetricDescriptor {
valueType: string;
metricKind: string;
metricKind: MetricKind;
type: string;
unit: string;
service: string;
@ -161,3 +199,8 @@ export interface Filter {
value: string;
condition?: string;
}
export interface CustomMetaData {
perSeriesAligner?: string;
alignmentPeriod?: string;
}