diff --git a/pkg/promlib/healthcheck.go b/pkg/promlib/healthcheck.go index c3849398684..c38a9297431 100644 --- a/pkg/promlib/healthcheck.go +++ b/pkg/promlib/healthcheck.go @@ -8,6 +8,7 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/backend" + sdkapi "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1" "github.com/grafana/grafana/pkg/promlib/models" ) @@ -55,8 +56,8 @@ func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthReque func healthcheck(ctx context.Context, req *backend.CheckHealthRequest, i *instance) (*backend.CheckHealthResult, error) { qm := models.QueryModel{ UtcOffsetSec: 0, - CommonQueryProperties: models.CommonQueryProperties{ - RefId: refID, + CommonQueryProperties: sdkapi.CommonQueryProperties{ + RefID: refID, }, PrometheusQueryProperties: models.PrometheusQueryProperties{ Expr: "1+1", diff --git a/pkg/promlib/models/query.go b/pkg/promlib/models/query.go index b2bcd0d02d7..d9cb65a571f 100644 --- a/pkg/promlib/models/query.go +++ b/pkg/promlib/models/query.go @@ -1,6 +1,7 @@ package models import ( + "embed" "encoding/json" "math" "strconv" @@ -9,6 +10,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/gtime" + sdkapi "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1" "github.com/prometheus/prometheus/model/labels" "github.com/grafana/grafana/pkg/promlib/intervalv2" @@ -119,8 +121,8 @@ var safeResolution = 11000 // QueryModel includes both the common and specific values type QueryModel struct { - PrometheusQueryProperties `json:",inline"` - CommonQueryProperties `json:",inline"` + PrometheusQueryProperties `json:",inline"` + sdkapi.CommonQueryProperties `json:",inline"` // The following properties may be part of the request payload, however they are not saved in panel JSON // Timezone offset to align start & end time on backend @@ -128,13 +130,6 @@ type QueryModel struct { Interval string `json:"interval,omitempty"` } -// CommonQueryProperties is properties applied to all queries -// NOTE: this will soon be replaced with a struct from the SDK -type CommonQueryProperties struct { - RefId string `json:"refId,omitempty"` - IntervalMs int64 `json:"intervalMs,omitempty"` -} - type TimeRange struct { Start time.Time End time.Time @@ -167,7 +162,7 @@ func Parse(query backend.DataQuery, dsScrapeInterval string, intervalCalculator } // Final step value for prometheus - calculatedStep, err := calculatePrometheusInterval(model.Interval, dsScrapeInterval, model.IntervalMs, model.IntervalFactor, query, intervalCalculator) + calculatedStep, err := calculatePrometheusInterval(model.Interval, dsScrapeInterval, int64(model.IntervalMS), model.IntervalFactor, query, intervalCalculator) if err != nil { return nil, err } @@ -368,3 +363,11 @@ func AlignTimeRange(t time.Time, step time.Duration, offset int64) time.Time { stepNano := float64(step.Nanoseconds()) return time.Unix(0, int64(math.Floor((float64(t.UnixNano())+offsetNano)/stepNano)*stepNano-offsetNano)).UTC() } + +//go:embed query.types.json +var f embed.FS + +// QueryTypeDefinitionsJSON returns the query type definitions +func QueryTypeDefinitionsJSON() (json.RawMessage, error) { + return f.ReadFile("query.types.json") +} diff --git a/pkg/promlib/models/query.panel.example.json b/pkg/promlib/models/query.panel.example.json new file mode 100644 index 00000000000..bf2157c2a42 --- /dev/null +++ b/pkg/promlib/models/query.panel.example.json @@ -0,0 +1,13 @@ +{ + "type": "table", + "targets": [ + { + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "TheUID" + }, + "expr": "1+1" + } + ] +} \ No newline at end of file diff --git a/pkg/promlib/models/query.panel.schema.json b/pkg/promlib/models/query.panel.schema.json new file mode 100644 index 00000000000..ffd2328f1de --- /dev/null +++ b/pkg/promlib/models/query.panel.schema.json @@ -0,0 +1,229 @@ +{ + "type": "object", + "required": [ + "targets", + "type" + ], + "properties": { + "targets": { + "type": "array", + "items": { + "description": "PrometheusQueryProperties defines the specific properties used for prometheus", + "type": "object", + "required": [ + "expr" + ], + "properties": { + "datasource": { + "description": "The datasource", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "description": "The datasource plugin type", + "type": "string", + "pattern": "^prometheus$" + }, + "uid": { + "description": "Datasource UID", + "type": "string" + } + }, + "additionalProperties": false + }, + "editorMode": { + "description": "what we should show in the editor\n\n\nPossible enum values:\n - `\"builder\"` \n - `\"code\"` ", + "type": "string", + "enum": [ + "builder", + "code" + ], + "x-enum-description": {} + }, + "exemplar": { + "description": "Execute an additional query to identify interesting raw samples relevant for the given expr", + "type": "boolean" + }, + "expr": { + "description": "The actual expression/query that will be evaluated by Prometheus", + "type": "string" + }, + "format": { + "description": "The response format\n\n\nPossible enum values:\n - `\"time_series\"` \n - `\"table\"` \n - `\"heatmap\"` ", + "type": "string", + "enum": [ + "time_series", + "table", + "heatmap" + ], + "x-enum-description": {} + }, + "hide": { + "description": "true if query is disabled (ie should not be returned to the dashboard)\nNOTE: this does not always imply that the query should not be executed since\nthe results from a hidden query may be used as the input to other queries (SSE etc)", + "type": "boolean" + }, + "instant": { + "description": "Returns only the latest value that Prometheus has scraped for the requested time series", + "type": "boolean" + }, + "intervalFactor": { + "description": "Used to specify how many times to divide max data points by. We use max data points under query options\nSee https://github.com/grafana/grafana/issues/48081\nDeprecated: use interval", + "type": "integer" + }, + "intervalMs": { + "description": "Interval is the suggested duration between time points in a time series query.\nNOTE: the values for intervalMs is not saved in the query model. It is typically calculated\nfrom the interval required to fill a pixels in the visualization", + "type": "number" + }, + "legendFormat": { + "description": "Series name override or template. Ex. {{hostname}} will be replaced with label value for hostname", + "type": "string" + }, + "maxDataPoints": { + "description": "MaxDataPoints is the maximum number of data points that should be returned from a time series query.\nNOTE: the values for maxDataPoints is not saved in the query model. It is typically calculated\nfrom the number of pixels visible in a visualization", + "type": "integer" + }, + "queryType": { + "description": "QueryType is an optional identifier for the type of query.\nIt can be used to distinguish different types of queries.", + "type": "string" + }, + "range": { + "description": "Returns a Range vector, comprised of a set of time series containing a range of data points over time for each time series", + "type": "boolean" + }, + "refId": { + "description": "RefID is the unique identifier of the query, set by the frontend call.", + "type": "string" + }, + "resultAssertions": { + "description": "Optionally define expected query result behavior", + "type": "object", + "required": [ + "typeVersion" + ], + "properties": { + "maxFrames": { + "description": "Maximum frame count", + "type": "integer" + }, + "type": { + "description": "Type asserts that the frame matches a known type structure.\n\n\nPossible enum values:\n - `\"\"` \n - `\"timeseries-wide\"` \n - `\"timeseries-long\"` \n - `\"timeseries-many\"` \n - `\"timeseries-multi\"` \n - `\"directory-listing\"` \n - `\"table\"` \n - `\"numeric-wide\"` \n - `\"numeric-multi\"` \n - `\"numeric-long\"` \n - `\"log-lines\"` ", + "type": "string", + "enum": [ + "", + "timeseries-wide", + "timeseries-long", + "timeseries-many", + "timeseries-multi", + "directory-listing", + "table", + "numeric-wide", + "numeric-multi", + "numeric-long", + "log-lines" + ], + "x-enum-description": {} + }, + "typeVersion": { + "description": "TypeVersion is the version of the Type property. Versions greater than 0.0 correspond to the dataplane\ncontract documentation https://grafana.github.io/dataplane/contract/.", + "type": "array", + "maxItems": 2, + "minItems": 2, + "items": { + "type": "integer" + } + } + }, + "additionalProperties": false + }, + "scope": { + "description": "???", + "type": "object", + "required": [ + "title", + "type", + "description", + "category", + "filters" + ], + "properties": { + "category": { + "type": "string" + }, + "description": { + "type": "string" + }, + "filters": { + "type": "array", + "items": { + "description": "ScopeFilter is a hand copy of the ScopeFilter struct from pkg/apis/scope/v0alpha1/types.go to avoid import (temp fix)", + "type": "object", + "required": [ + "key", + "value", + "operator" + ], + "properties": { + "key": { + "type": "string" + }, + "operator": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "title": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "additionalProperties": false + }, + "timeRange": { + "description": "TimeRange represents the query range\nNOTE: unlike generic /ds/query, we can now send explicit time values in each query\nNOTE: the values for timeRange are not saved in a dashboard, they are constructed on the fly", + "type": "object", + "required": [ + "from", + "to" + ], + "properties": { + "from": { + "description": "From is the start time of the query.", + "type": "string", + "default": "now-6h", + "examples": [ + "now-1h" + ] + }, + "to": { + "description": "To is the end time of the query.", + "type": "string", + "default": "now", + "examples": [ + "now" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false, + "$schema": "https://json-schema.org/draft-04/schema#" + } + }, + "type": { + "description": "the panel type", + "type": "string" + } + }, + "additionalProperties": true, + "$schema": "https://json-schema.org/draft-04/schema#" +} \ No newline at end of file diff --git a/pkg/promlib/models/query.request.example.json b/pkg/promlib/models/query.request.example.json new file mode 100644 index 00000000000..85946db6aef --- /dev/null +++ b/pkg/promlib/models/query.request.example.json @@ -0,0 +1,12 @@ +{ + "from": "now-1h", + "to": "now", + "queries": [ + { + "refId": "A", + "maxDataPoints": 1000, + "intervalMs": 5, + "expr": "1+1" + } + ] +} \ No newline at end of file diff --git a/pkg/promlib/models/query.request.schema.json b/pkg/promlib/models/query.request.schema.json new file mode 100644 index 00000000000..c8e797d1f38 --- /dev/null +++ b/pkg/promlib/models/query.request.schema.json @@ -0,0 +1,239 @@ +{ + "type": "object", + "required": [ + "queries" + ], + "properties": { + "$schema": { + "description": "helper", + "type": "string" + }, + "debug": { + "type": "boolean" + }, + "from": { + "description": "From Start time in epoch timestamps in milliseconds or relative using Grafana time units.", + "type": "string" + }, + "queries": { + "type": "array", + "items": { + "description": "PrometheusQueryProperties defines the specific properties used for prometheus", + "type": "object", + "required": [ + "expr" + ], + "properties": { + "datasource": { + "description": "The datasource", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "description": "The datasource plugin type", + "type": "string", + "pattern": "^prometheus$" + }, + "uid": { + "description": "Datasource UID", + "type": "string" + } + }, + "additionalProperties": false + }, + "editorMode": { + "description": "what we should show in the editor\n\n\nPossible enum values:\n - `\"builder\"` \n - `\"code\"` ", + "type": "string", + "enum": [ + "builder", + "code" + ], + "x-enum-description": {} + }, + "exemplar": { + "description": "Execute an additional query to identify interesting raw samples relevant for the given expr", + "type": "boolean" + }, + "expr": { + "description": "The actual expression/query that will be evaluated by Prometheus", + "type": "string" + }, + "format": { + "description": "The response format\n\n\nPossible enum values:\n - `\"time_series\"` \n - `\"table\"` \n - `\"heatmap\"` ", + "type": "string", + "enum": [ + "time_series", + "table", + "heatmap" + ], + "x-enum-description": {} + }, + "hide": { + "description": "true if query is disabled (ie should not be returned to the dashboard)\nNOTE: this does not always imply that the query should not be executed since\nthe results from a hidden query may be used as the input to other queries (SSE etc)", + "type": "boolean" + }, + "instant": { + "description": "Returns only the latest value that Prometheus has scraped for the requested time series", + "type": "boolean" + }, + "intervalFactor": { + "description": "Used to specify how many times to divide max data points by. We use max data points under query options\nSee https://github.com/grafana/grafana/issues/48081\nDeprecated: use interval", + "type": "integer" + }, + "intervalMs": { + "description": "Interval is the suggested duration between time points in a time series query.\nNOTE: the values for intervalMs is not saved in the query model. It is typically calculated\nfrom the interval required to fill a pixels in the visualization", + "type": "number" + }, + "legendFormat": { + "description": "Series name override or template. Ex. {{hostname}} will be replaced with label value for hostname", + "type": "string" + }, + "maxDataPoints": { + "description": "MaxDataPoints is the maximum number of data points that should be returned from a time series query.\nNOTE: the values for maxDataPoints is not saved in the query model. It is typically calculated\nfrom the number of pixels visible in a visualization", + "type": "integer" + }, + "queryType": { + "description": "QueryType is an optional identifier for the type of query.\nIt can be used to distinguish different types of queries.", + "type": "string" + }, + "range": { + "description": "Returns a Range vector, comprised of a set of time series containing a range of data points over time for each time series", + "type": "boolean" + }, + "refId": { + "description": "RefID is the unique identifier of the query, set by the frontend call.", + "type": "string" + }, + "resultAssertions": { + "description": "Optionally define expected query result behavior", + "type": "object", + "required": [ + "typeVersion" + ], + "properties": { + "maxFrames": { + "description": "Maximum frame count", + "type": "integer" + }, + "type": { + "description": "Type asserts that the frame matches a known type structure.\n\n\nPossible enum values:\n - `\"\"` \n - `\"timeseries-wide\"` \n - `\"timeseries-long\"` \n - `\"timeseries-many\"` \n - `\"timeseries-multi\"` \n - `\"directory-listing\"` \n - `\"table\"` \n - `\"numeric-wide\"` \n - `\"numeric-multi\"` \n - `\"numeric-long\"` \n - `\"log-lines\"` ", + "type": "string", + "enum": [ + "", + "timeseries-wide", + "timeseries-long", + "timeseries-many", + "timeseries-multi", + "directory-listing", + "table", + "numeric-wide", + "numeric-multi", + "numeric-long", + "log-lines" + ], + "x-enum-description": {} + }, + "typeVersion": { + "description": "TypeVersion is the version of the Type property. Versions greater than 0.0 correspond to the dataplane\ncontract documentation https://grafana.github.io/dataplane/contract/.", + "type": "array", + "maxItems": 2, + "minItems": 2, + "items": { + "type": "integer" + } + } + }, + "additionalProperties": false + }, + "scope": { + "description": "???", + "type": "object", + "required": [ + "title", + "type", + "description", + "category", + "filters" + ], + "properties": { + "category": { + "type": "string" + }, + "description": { + "type": "string" + }, + "filters": { + "type": "array", + "items": { + "description": "ScopeFilter is a hand copy of the ScopeFilter struct from pkg/apis/scope/v0alpha1/types.go to avoid import (temp fix)", + "type": "object", + "required": [ + "key", + "value", + "operator" + ], + "properties": { + "key": { + "type": "string" + }, + "operator": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "title": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "additionalProperties": false + }, + "timeRange": { + "description": "TimeRange represents the query range\nNOTE: unlike generic /ds/query, we can now send explicit time values in each query\nNOTE: the values for timeRange are not saved in a dashboard, they are constructed on the fly", + "type": "object", + "required": [ + "from", + "to" + ], + "properties": { + "from": { + "description": "From is the start time of the query.", + "type": "string", + "default": "now-6h", + "examples": [ + "now-1h" + ] + }, + "to": { + "description": "To is the end time of the query.", + "type": "string", + "default": "now", + "examples": [ + "now" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false, + "$schema": "https://json-schema.org/draft-04/schema#" + } + }, + "to": { + "description": "To end time in epoch timestamps in milliseconds or relative using Grafana time units.", + "type": "string" + } + }, + "additionalProperties": false, + "$schema": "https://json-schema.org/draft-04/schema#" +} \ No newline at end of file diff --git a/pkg/promlib/models/query.types.json b/pkg/promlib/models/query.types.json new file mode 100644 index 00000000000..7ae0e10374e --- /dev/null +++ b/pkg/promlib/models/query.types.json @@ -0,0 +1,130 @@ +{ + "kind": "QueryTypeDefinitionList", + "apiVersion": "query.grafana.app/v0alpha1", + "metadata": { + "resourceVersion": "1711372744903" + }, + "items": [ + { + "metadata": { + "name": "default", + "resourceVersion": "1711374012365", + "creationTimestamp": "2024-03-25T13:19:04Z" + }, + "spec": { + "schema": { + "$schema": "https://json-schema.org/draft-04/schema", + "additionalProperties": false, + "description": "PrometheusQueryProperties defines the specific properties used for prometheus", + "properties": { + "editorMode": { + "description": "what we should show in the editor\n\n\nPossible enum values:\n - `\"builder\"` \n - `\"code\"` ", + "enum": [ + "builder", + "code" + ], + "type": "string", + "x-enum-description": {} + }, + "exemplar": { + "description": "Execute an additional query to identify interesting raw samples relevant for the given expr", + "type": "boolean" + }, + "expr": { + "description": "The actual expression/query that will be evaluated by Prometheus", + "type": "string" + }, + "format": { + "description": "The response format\n\n\nPossible enum values:\n - `\"time_series\"` \n - `\"table\"` \n - `\"heatmap\"` ", + "enum": [ + "time_series", + "table", + "heatmap" + ], + "type": "string", + "x-enum-description": {} + }, + "instant": { + "description": "Returns only the latest value that Prometheus has scraped for the requested time series", + "type": "boolean" + }, + "intervalFactor": { + "description": "Used to specify how many times to divide max data points by. We use max data points under query options\nSee https://github.com/grafana/grafana/issues/48081\nDeprecated: use interval", + "type": "integer" + }, + "legendFormat": { + "description": "Series name override or template. Ex. {{hostname}} will be replaced with label value for hostname", + "type": "string" + }, + "range": { + "description": "Returns a Range vector, comprised of a set of time series containing a range of data points over time for each time series", + "type": "boolean" + }, + "scope": { + "additionalProperties": false, + "description": "???", + "properties": { + "category": { + "type": "string" + }, + "description": { + "type": "string" + }, + "filters": { + "items": { + "additionalProperties": false, + "description": "ScopeFilter is a hand copy of the ScopeFilter struct from pkg/apis/scope/v0alpha1/types.go to avoid import (temp fix)", + "properties": { + "key": { + "type": "string" + }, + "operator": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "key", + "value", + "operator" + ], + "type": "object" + }, + "type": "array" + }, + "title": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "required": [ + "title", + "type", + "description", + "category", + "filters" + ], + "type": "object" + } + }, + "required": [ + "expr" + ], + "type": "object" + }, + "examples": [ + { + "name": "simple health check", + "saveModel": { + "expr": "1+1" + } + } + ] + } + } + ] +} \ No newline at end of file diff --git a/pkg/promlib/models/query_test.go b/pkg/promlib/models/query_test.go index 085352f3f1a..3bba6eb78d9 100644 --- a/pkg/promlib/models/query_test.go +++ b/pkg/promlib/models/query_test.go @@ -7,6 +7,8 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/backend" + sdkapi "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1" + "github.com/grafana/grafana-plugin-sdk-go/experimental/schemabuilder" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/promlib/intervalv2" @@ -782,3 +784,38 @@ func TestAlignTimeRange(t *testing.T) { }) } } + +func TestQueryTypeDefinitions(t *testing.T) { + builder, err := schemabuilder.NewSchemaBuilder( + schemabuilder.BuilderOptions{ + PluginID: []string{"prometheus"}, + ScanCode: []schemabuilder.CodePaths{{ + BasePackage: "github.com/grafana/grafana/pkg/promlib/models", + CodePath: "./", + }}, + Enums: []reflect.Type{ + reflect.TypeOf(models.PromQueryFormatTimeSeries), // pick an example value (not the root) + reflect.TypeOf(models.QueryEditorModeBuilder), + }, + }) + require.NoError(t, err) + err = builder.AddQueries( + schemabuilder.QueryTypeInfo{ + Name: "default", + GoType: reflect.TypeOf(&models.PrometheusQueryProperties{}), + Examples: []sdkapi.QueryExample{ + { + Name: "simple health check", + SaveModel: sdkapi.AsUnstructured( + models.PrometheusQueryProperties{ + Expr: "1+1", + }, + ), + }, + }, + }, + ) + + require.NoError(t, err) + builder.UpdateQueryDefinition(t, "./") +} diff --git a/pkg/promlib/querydata/framing_test.go b/pkg/promlib/querydata/framing_test.go index 0d194f189ff..3668438ded4 100644 --- a/pkg/promlib/querydata/framing_test.go +++ b/pkg/promlib/querydata/framing_test.go @@ -14,6 +14,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/experimental" + sdkapi "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/promlib/models" @@ -113,8 +114,8 @@ func loadStoredQuery(fileName string) (*backend.QueryDataRequest, error) { Expr: sq.Expr, LegendFormat: sq.LegendFormat, }, - CommonQueryProperties: models.CommonQueryProperties{ - IntervalMs: sq.Step * 1000, + CommonQueryProperties: sdkapi.CommonQueryProperties{ + IntervalMS: float64(sq.Step * 1000), }, Interval: fmt.Sprintf("%ds", sq.Step), }