diff --git a/docs/sources/datasources/google-cloud-monitoring/_index.md b/docs/sources/datasources/google-cloud-monitoring/_index.md index 1a98cd2e88d..19c90c54c56 100644 --- a/docs/sources/datasources/google-cloud-monitoring/_index.md +++ b/docs/sources/datasources/google-cloud-monitoring/_index.md @@ -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. diff --git a/pkg/tsdb/cloudmonitoring/cloudmonitoring.go b/pkg/tsdb/cloudmonitoring/cloudmonitoring.go index 868e89c804b..0b4ff46869d 100644 --- a/pkg/tsdb/cloudmonitoring/cloudmonitoring.go +++ b/pkg/tsdb/cloudmonitoring/cloudmonitoring.go @@ -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) diff --git a/pkg/tsdb/cloudmonitoring/cloudmonitoring_test.go b/pkg/tsdb/cloudmonitoring/cloudmonitoring_test.go index 4c251139979..fcc627c3c10 100644 --- a/pkg/tsdb/cloudmonitoring/cloudmonitoring_test.go +++ b/pkg/tsdb/cloudmonitoring/cloudmonitoring_test.go @@ -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) { diff --git a/pkg/tsdb/cloudmonitoring/preprocessor.go b/pkg/tsdb/cloudmonitoring/preprocessor.go new file mode 100644 index 00000000000..ca076df5641 --- /dev/null +++ b/pkg/tsdb/cloudmonitoring/preprocessor.go @@ -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 + } +} diff --git a/pkg/tsdb/cloudmonitoring/time_series_filter.go b/pkg/tsdb/cloudmonitoring/time_series_filter.go index ac63e0869aa..eeb6ed9769f 100644 --- a/pkg/tsdb/cloudmonitoring/time_series_filter.go +++ b/pkg/tsdb/cloudmonitoring/time_series_filter.go @@ -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 { diff --git a/pkg/tsdb/cloudmonitoring/types.go b/pkg/tsdb/cloudmonitoring/types.go index 96dc29cda52..25a911b9227 100644 --- a/pkg/tsdb/cloudmonitoring/types.go +++ b/pkg/tsdb/cloudmonitoring/types.go @@ -56,6 +56,8 @@ type ( View string EditorMode string Query string + Preprocessor string + PreprocessorType preprocessorType } sloQuery struct { diff --git a/public/app/plugins/datasource/cloud-monitoring/CloudMonitoringMetricFindQuery.ts b/public/app/plugins/datasource/cloud-monitoring/CloudMonitoringMetricFindQuery.ts index 4574325b34a..84b6cc59689 100644 --- a/public/app/plugins/datasource/cloud-monitoring/CloudMonitoringMetricFindQuery.ts +++ b/public/app/plugins/datasource/cloud-monitoring/CloudMonitoringMetricFindQuery.ts @@ -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) { diff --git a/public/app/plugins/datasource/cloud-monitoring/annotations_query_ctrl.ts b/public/app/plugins/datasource/cloud-monitoring/annotations_query_ctrl.ts index a5fd161172d..47478571660 100644 --- a/public/app/plugins/datasource/cloud-monitoring/annotations_query_ctrl.ts +++ b/public/app/plugins/datasource/cloud-monitoring/annotations_query_ctrl.ts @@ -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); } diff --git a/public/app/plugins/datasource/cloud-monitoring/components/Aggregations.test.tsx b/public/app/plugins/datasource/cloud-monitoring/components/Aggregation.test.tsx similarity index 72% rename from public/app/plugins/datasource/cloud-monitoring/components/Aggregations.test.tsx rename to public/app/plugins/datasource/cloud-monitoring/components/Aggregation.test.tsx index a6a9d1ad85d..aa5b3e16202 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/Aggregations.test.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/Aggregation.test.tsx @@ -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
; - }, templateVariableOptions: [], }; -describe('Aggregations', () => { +describe('Aggregation', () => { it('renders correctly', () => { - render(); - expect(screen.getByTestId('aggregations')).toBeInTheDocument(); + render(); + 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(); - const { options } = wrapper.find(Segment).props() as any; + const wrapper = shallow(); + 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(); - const { options } = wrapper.find(Segment).props() as any; + const wrapper = shallow(); + const { options } = wrapper.find(Select).props() as any; const [, aggGroup] = options; expect(aggGroup.options.length).toEqual(11); diff --git a/public/app/plugins/datasource/cloud-monitoring/components/Aggregation.tsx b/public/app/plugins/datasource/cloud-monitoring/components/Aggregation.tsx new file mode 100644 index 00000000000..7b94cb376f4 --- /dev/null +++ b/public/app/plugins/datasource/cloud-monitoring/components/Aggregation.tsx @@ -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>; +} + +export const Aggregation: FC = (props) => { + const aggOptions = useAggregationOptionsByMetric(props); + const selected = useSelectedFromOptions(aggOptions, props); + + return ( + + - + + + ); }; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/Alignment.tsx b/public/app/plugins/datasource/cloud-monitoring/components/Alignment.tsx new file mode 100644 index 00000000000..89968516891 --- /dev/null +++ b/public/app/plugins/datasource/cloud-monitoring/components/Alignment.tsx @@ -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>; + customMetaData: CustomMetaData; + datasource: CloudMonitoringDatasource; +} + +export const Alignment: FC = ({ templateVariableOptions, onChange, query, customMetaData, datasource }) => { + return ( + } + > + + + + + + ); +}; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/AlignmentFunction.tsx b/public/app/plugins/datasource/cloud-monitoring/components/AlignmentFunction.tsx new file mode 100644 index 00000000000..700564aa964 --- /dev/null +++ b/public/app/plugins/datasource/cloud-monitoring/components/AlignmentFunction.tsx @@ -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>; +} + +export const AlignmentFunction: FC = ({ query, templateVariableOptions, onChange }) => { + const { valueType, metricKind, perSeriesAligner: psa, preprocessor } = query; + const { perSeriesAligner, alignOptions } = useMemo( + () => getAlignmentPickerData(valueType, metricKind, psa, preprocessor), + [valueType, metricKind, psa, preprocessor] + ); + + return ( + + ); +}; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/AlignmentPeriod.tsx b/public/app/plugins/datasource/cloud-monitoring/components/AlignmentPeriod.tsx new file mode 100644 index 00000000000..ed472a76b94 --- /dev/null +++ b/public/app/plugins/datasource/cloud-monitoring/components/AlignmentPeriod.tsx @@ -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>; + selectWidth?: number; +} + +export const AlignmentPeriod: FC = ({ 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 ( + + ); +}; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/AlignmentPeriodLabel.tsx b/public/app/plugins/datasource/cloud-monitoring/components/AlignmentPeriodLabel.tsx new file mode 100644 index 00000000000..2a4d1651481 --- /dev/null +++ b/public/app/plugins/datasource/cloud-monitoring/components/AlignmentPeriodLabel.tsx @@ -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 = ({ 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 ; +}; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/AlignmentPeriods.tsx b/public/app/plugins/datasource/cloud-monitoring/components/AlignmentPeriods.tsx deleted file mode 100644 index d5bf169cbc6..00000000000 --- a/public/app/plugins/datasource/cloud-monitoring/components/AlignmentPeriods.tsx +++ /dev/null @@ -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>; - alignmentPeriod: string; - perSeriesAligner: string; - usedAlignmentPeriod?: number; -} - -export const AlignmentPeriods: FC = ({ - 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 ( - <> -
- - 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" - > -
- {usedAlignmentPeriod && } -
-
- - ); -}; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/Alignments.tsx b/public/app/plugins/datasource/cloud-monitoring/components/Alignments.tsx deleted file mode 100644 index 7c3fdcfe802..00000000000 --- a/public/app/plugins/datasource/cloud-monitoring/components/Alignments.tsx +++ /dev/null @@ -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>; - alignOptions: Array>; - perSeriesAligner: string; -} - -export const Alignments: FC = ({ perSeriesAligner, templateVariableOptions, onChange, alignOptions }) => { - return ( - <> -
-
- - 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" - > -
-
- - ); -}; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/AnnotationQueryEditor.tsx b/public/app/plugins/datasource/cloud-monitoring/components/AnnotationQueryEditor.tsx index cd9531cc871..a631672e04c 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/AnnotationQueryEditor.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/AnnotationQueryEditor.tsx @@ -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 { )} -
-
- Title - this.onChange('title', e.target.value)} - /> -
-
- Text - this.onChange('text', e.target.value)} - /> -
-
-
-
-
+ + + this.onChange('title', e.target.value)} + /> + + + this.onChange('text', e.target.value)} + /> + diff --git a/public/app/plugins/datasource/cloud-monitoring/components/CloudMonitoringCheatSheet.tsx b/public/app/plugins/datasource/cloud-monitoring/components/CloudMonitoringCheatSheet.tsx new file mode 100644 index 00000000000..3029092c818 --- /dev/null +++ b/public/app/plugins/datasource/cloud-monitoring/components/CloudMonitoringCheatSheet.tsx @@ -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 { + render() { + return ( +
+

Cloud Monitoring alias patterns

+
+

+ Format the legend keys any way you want by using alias patterns. Format the legend keys any way you want by + using alias patterns. +

+ Example: + {`${'{{metric.name}} - {{metric.label.instance_name}}'}`} +
+ Result:   cpu/usage_time - server1-europe-west-1 +
+
+ +
+
    +
  • + {`${'{{metric.type}}'}`} = metric type e.g. compute.googleapis.com/instance/cpu/usage_time +
  • +
  • + {`${'{{metric.name}}'}`} = name part of metric e.g. instance/cpu/usage_time +
  • +
  • + {`${'{{metric.service}}'}`} = service part of metric e.g. compute +
  • +
  • + {`${'{{metric.label.label_name}}'}`} = Metric label metadata e.g. metric.label.instance_name +
  • +
  • + {`${'{{resource.label.label_name}}'}`} = Resource label metadata e.g. resource.label.zone +
  • +
  • + {`${'{{metadata.system_labels.name}}'}`} = Meta data system labels e.g. + metadata.system_labels.name. For this to work, the needs to be included in the group by +
  • +
  • + {`${'{{metadata.user_labels.name}}'}`} = Meta data user labels e.g. + metadata.user_labels.name. For this to work, the needs to be included in the group by +
  • +
  • + {`${'{{bucket}}'}`} = bucket boundary for distribution metrics when using a heatmap in + Grafana +
  • +
  • + {`${'{{project}}'}`} = The project name that was specified in the query editor +
  • +
  • + {`${'{{service}}'}`} = The service id that was specified in the SLO query editor +
  • +
  • + {`${'{{slo}}'}`} = The SLO id that was specified in the SLO query editor +
  • +
  • + {`${'{{selector}}'}`} = The Selector function that was specified in the SLO query editor +
  • +
+
+
+ ); + } +} diff --git a/public/app/plugins/datasource/cloud-monitoring/components/Fields.tsx b/public/app/plugins/datasource/cloud-monitoring/components/Fields.tsx index a9a4756eb19..fc8d4aa43d0 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/Fields.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/Fields.tsx @@ -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 { - label: string; - tooltip?: string; - children?: React.ReactNode; -} - -export const QueryField: FunctionComponent> = ({ label, tooltip, children }) => ( - <> - - {label} - - {children} - -); - -export const QueryInlineField: FunctionComponent = ({ ...props }) => { - return ( -
- -
-
-
-
- ); -}; +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 = ({ +export const VariableQueryField: FC = ({ label, onChange, value, @@ -55,3 +31,58 @@ export const VariableQueryField: FunctionComponent = ({ ); }; + +export interface Props { + children: React.ReactNode; + tooltip?: PopoverContent; + label?: React.ReactNode; + className?: string; + noFillEnd?: boolean; + labelWidth?: number; + fillComponent?: React.ReactNode; +} + +export const QueryEditorRow: FC = ({ + children, + label, + tooltip, + fillComponent, + noFillEnd = false, + labelWidth = LABEL_WIDTH, + ...rest +}) => { + return ( +
+ {label && ( + + {label} + + )} +
+ + {children} + +
+
+ {noFillEnd ||
{fillComponent}
} +
+
+ ); +}; + +export const QueryEditorField: FC = ({ children, label, tooltip, labelWidth = INNER_LABEL_WIDTH, ...rest }) => { + return ( + <> + {label && ( + + {label} + + )} + {children} + + ); +}; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/GroupBy.tsx b/public/app/plugins/datasource/cloud-monitoring/components/GroupBy.tsx new file mode 100644 index 00000000000..adfdb66be03 --- /dev/null +++ b/public/app/plugins/datasource/cloud-monitoring/components/GroupBy.tsx @@ -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; + labels: string[]; + metricDescriptor?: MetricDescriptor; + onChange: (query: MetricQuery) => void; + query: MetricQuery; +} + +export const GroupBy: FunctionComponent = ({ + labels: groupBys = [], + query, + onChange, + variableOptionGroup, + metricDescriptor, +}) => { + const options = useMemo(() => [variableOptionGroup, ...labelsToGroupedOptions([...groupBys, ...SYSTEM_LABELS])], [ + groupBys, + variableOptionGroup, + ]); + + return ( + + { + onChange({ ...query, groupBys: options.map((o) => o.value!) }); + }} + > + onChange({ ...query, crossSeriesReducer })} + > + + ); +}; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/GroupBys.tsx b/public/app/plugins/datasource/cloud-monitoring/components/GroupBys.tsx deleted file mode 100644 index e1e361b61fd..00000000000 --- a/public/app/plugins/datasource/cloud-monitoring/components/GroupBys.tsx +++ /dev/null @@ -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; - groupBys: string[]; -} - -const removeText = '-- remove group by --'; -const removeOption: SelectableValue = { label: removeText, value: removeText }; - -export const GroupBys: FunctionComponent = ({ groupBys = [], values = [], onChange, variableOptionGroup }) => { - const options = [removeOption, variableOptionGroup, ...labelsToGroupedOptions([...groupBys, ...systemLabels])]; - return ( -
- - {values && - values.map((value, index) => ( - - onChange( - value === removeText - ? values.filter((_, i) => i !== index) - : values.map((v, i) => (i === index ? value : v)) - ) - } - /> - ))} - {values.length !== groupBys.length && ( - - - - } - allowCustomValue - onChange={({ value = '' }) => onChange([...values, value])} - options={[ - variableOptionGroup, - ...labelsToGroupedOptions([...groupBys.filter((groupBy) => !values.includes(groupBy)), ...systemLabels]), - ]} - /> - )} -
- -
-
- ); -}; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/Help.tsx b/public/app/plugins/datasource/cloud-monitoring/components/Help.tsx deleted file mode 100644 index 915a4fa7689..00000000000 --- a/public/app/plugins/datasource/cloud-monitoring/components/Help.tsx +++ /dev/null @@ -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 { - 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 ( - <> -
-
- -
- - {rawQuery && ( -
- -
- )} - -
-
-
-
- {rawQuery && displaRawQuery && ( -
-
{rawQuery}
-
- )} - - {displayHelp && ( -
-
-
Alias Patterns
Format the legend keys any way you want by using alias patterns. Format the legend - keys any way you want by using alias patterns. -

- Example: - {`${'{{metric.name}} - {{metric.label.instance_name}}'}`} -
- Result:   cpu/usage_time - server1-europe-west-1 -
-
- Patterns -
-
    -
  • - {`${'{{metric.type}}'}`} = metric type e.g. - compute.googleapis.com/instance/cpu/usage_time -
  • -
  • - {`${'{{metric.name}}'}`} = name part of metric e.g. instance/cpu/usage_time -
  • -
  • - {`${'{{metric.service}}'}`} = service part of metric e.g. compute -
  • -
  • - {`${'{{metric.label.label_name}}'}`} = Metric label metadata e.g. - metric.label.instance_name -
  • -
  • - {`${'{{resource.label.label_name}}'}`} = Resource label metadata e.g. resource.label.zone -
  • -
  • - {`${'{{metadata.system_labels.name}}'}`} = Meta data system labels e.g. - metadata.system_labels.name. For this to work, the needs to be included in the group by -
  • -
  • - {`${'{{metadata.user_labels.name}}'}`} = Meta data user labels e.g. - metadata.user_labels.name. For this to work, the needs to be included in the group by -
  • -
  • - {`${'{{bucket}}'}`} = bucket boundary for distribution metrics when using a heatmap in - Grafana -
  • -
  • - {`${'{{project}}'}`} = The project name that was specified in the query editor -
  • -
  • - {`${'{{service}}'}`} = The service id that was specified in the SLO query editor -
  • -
  • - {`${'{{slo}}'}`} = The SLO id that was specified in the SLO query editor -
  • -
  • - {`${'{{selector}}'}`} = The Selector function that was specified in the SLO query editor -
  • -
-
-
- )} - - {lastQueryError && ( -
-
{lastQueryError}
-
- )} - - ); - } -} diff --git a/public/app/plugins/datasource/cloud-monitoring/components/LabelFilter.tsx b/public/app/plugins/datasource/cloud-monitoring/components/LabelFilter.tsx index 67424a56d10..af1edf1663e 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/LabelFilter.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/LabelFilter.tsx @@ -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; } -const removeText = '-- remove filter --'; -const removeOption: SelectableValue = { label: removeText, value: removeText, icon: 'times' }; const operators = ['=', '!=', '=~', '!=~']; +const FilterButton = React.forwardRef>( + ({ value, isOpen, invalid, ...rest }, ref) => { + return ; + } +); +FilterButton.displayName = 'FilterButton'; + +const OperatorButton = React.forwardRef>(({ value, ...rest }, ref) => { + return ( + + ); +}); +OperatorButton.displayName = 'OperatorButton'; + export const LabelFilter: FunctionComponent = ({ 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 ( + `Use label key: ${v}`} + value={key} + options={options} + onChange={({ value: key = '' }) => { onChange( filtersToStringArray( filters.map((f, i) => (i === index ? { key, operator, condition, value: '' } : f)) ) ); + }} + /> + `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)))) + } + /> + + {index + 1 === filters.length && Object.values(filters).every(({ value }) => value) && } + + ))} + {!filters.length && } + + ); }; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/MetricQueryEditor.tsx b/public/app/plugins/datasource/cloud-monitoring/components/MetricQueryEditor.tsx index a0c35559957..0c49203e96e 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/MetricQueryEditor.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/MetricQueryEditor.tsx @@ -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; 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) { const [state, setState] = useState(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({ -
- Service - s.value === service)} - options={[ - { - label: 'Template Variables', - options: templateVariableOptions, - }, - ...services, - ]} - placeholder="Select Services" - > -
-
-
-
-
- Metric + + + + + + + + - s.value === metricType)} - options={[ - { - label: 'Template Variables', - options: templateVariableOptions, - }, - ...metrics, - ]} - placeholder="Select Metric" - > -
-
-
-
{children(state.metricDescriptor)} ); diff --git a/public/app/plugins/datasource/cloud-monitoring/components/Preprocessor.tsx b/public/app/plugins/datasource/cloud-monitoring/components/Preprocessor.tsx new file mode 100644 index 00000000000..601c9f233ce --- /dev/null +++ b/public/app/plugins/datasource/cloud-monitoring/components/Preprocessor.tsx @@ -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 = ({ query, metricDescriptor, onChange }) => { + const options = useOptions(metricDescriptor); + return ( + + { + 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} + > + + ); +}; + +const useOptions = (metricDescriptor?: MetricDescriptor): Array> => { + 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]); +}; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/Project.tsx b/public/app/plugins/datasource/cloud-monitoring/components/Project.tsx index 6e437d446ed..85bf1549be4 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/Project.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/Project.tsx @@ -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>>([]); + useEffect(() => { + datasource.getProjects().then((projects) => + setProjects([ + { + label: 'Template Variables', + options: templateVariableOptions, + }, + ...projects, + ]) + ); + }, [datasource, templateVariableOptions]); + return ( -
- Project - + { onChange({ ...query, sloQuery, queryType: value! }); onRunQuery(); }} /> - - {query.queryType !== QueryType.SLO && ( - - )} - -
- -
-
+ {queryType === QueryType.METRICS && ( { this.props.onChange({ ...this.props.query, metricQuery }); }} @@ -131,18 +103,13 @@ export class QueryEditor extends PureComponent { {queryType === QueryType.SLO && ( this.onQueryChange('sloQuery', query)} onRunQuery={onRunQuery} datasource={datasource} query={sloQuery} > )} - - ); } diff --git a/public/app/plugins/datasource/cloud-monitoring/components/QueryType.tsx b/public/app/plugins/datasource/cloud-monitoring/components/QueryType.tsx index 8ef4daa960e..c57c7fcfa7d 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/QueryType.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/QueryType.tsx @@ -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 = ({ onChange, value, t
qt.value === value)} + value={[...QUERY_TYPES, ...templateVariableOptions].find((qt) => qt.value === value)} options={[ - ...queryTypes, + ...QUERY_TYPES, { label: 'Template Variables', options: templateVariableOptions, diff --git a/public/app/plugins/datasource/cloud-monitoring/components/SLO/SLO.tsx b/public/app/plugins/datasource/cloud-monitoring/components/SLO/SLO.tsx new file mode 100644 index 00000000000..3399110a2a1 --- /dev/null +++ b/public/app/plugins/datasource/cloud-monitoring/components/SLO/SLO.tsx @@ -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>; + datasource: CloudMonitoringDatasource; +} + +export const SLO: React.FC = ({ query, templateVariableOptions, onChange, datasource }) => { + const [slos, setSLOs] = useState>>([]); + const { projectName, serviceId } = query; + + useEffect(() => { + if (!projectName || !serviceId) { + return; + } + + datasource.getServiceLevelObjectives(projectName, serviceId).then((sloIds: Array>) => { + setSLOs([ + { + label: 'Template Variables', + options: templateVariableOptions, + }, + ...sloIds, + ]); + }); + }, [datasource, projectName, serviceId, templateVariableOptions]); + + return ( + + s.value === query?.selectorName ?? '')} + options={[ + { + label: 'Template Variables', + options: templateVariableOptions, + }, + ...SELECTORS, + ]} + onChange={({ value: selectorName }) => onChange({ ...query, selectorName: selectorName ?? '' })} + /> + + ); +}; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/SLO/Service.tsx b/public/app/plugins/datasource/cloud-monitoring/components/SLO/Service.tsx new file mode 100644 index 00000000000..364b701d192 --- /dev/null +++ b/public/app/plugins/datasource/cloud-monitoring/components/SLO/Service.tsx @@ -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>; + datasource: CloudMonitoringDatasource; +} + +export const Service: React.FC = ({ query, templateVariableOptions, onChange, datasource }) => { + const [services, setServices] = useState>>([]); + const { projectName } = query; + + useEffect(() => { + if (!projectName) { + return; + } + + datasource.getSLOServices(projectName).then((services: Array>) => { + setServices([ + { + label: 'Template Variables', + options: templateVariableOptions, + }, + ...services, + ]); + }); + }, [datasource, projectName, templateVariableOptions]); + + return ( + +