diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 5ba0b14..20d4788 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -22,6 +22,8 @@ jobs: - name: Checkout uses: actions/checkout@v4 - uses: actions/setup-go@v5 + with: + go-version-file: "go.mod" - name: Format run: diff -u <(echo -n) <(gofmt -d -s .) - name: Vet diff --git a/configuration/update.go b/configuration/update.go index d279132..bc1cf22 100644 --- a/configuration/update.go +++ b/configuration/update.go @@ -21,6 +21,7 @@ import ( ) var bannedLabels = []string{"__name__"} +var nativeQueryVariableRegex = regexp.MustCompile(`\$\{?([a-zA-Z_]\w+)\}?`) type ExcludeLabels struct { Regex *regexp.Regexp @@ -172,12 +173,20 @@ func (uc *updateCommand) validateNativeQueries(ctx context.Context) error { return nil } + newNativeQueries := make(map[string]metadata.NativeQuery) for key, nativeQuery := range uc.Config.Metadata.NativeOperations.Queries { if _, ok := uc.Config.Metadata.Metrics[key]; ok { return fmt.Errorf("duplicated native query name `%s`. That name may exist in the metrics collection", key) } slog.Debug(key, slog.String("type", "native_query"), slog.String("query", nativeQuery.Query)) + args, err := findNativeQueryVariables(nativeQuery) + if err != nil { + return fmt.Errorf("%s; query: %s", err, nativeQuery.Query) + } + nativeQuery.Arguments = args query := nativeQuery.Query + + // validate arguments and promQL syntaxes for k, v := range nativeQuery.Arguments { switch v.Type { case string(metadata.ScalarInt64), string(metadata.ScalarFloat64): @@ -188,12 +197,23 @@ func (uc *updateCommand) validateNativeQueries(ctx context.Context) error { return fmt.Errorf("invalid argument type `%s` in the native query `%s`", k, key) } } - _, err := uc.Client.FormatQuery(ctx, query) + _, err = uc.Client.FormatQuery(ctx, query) if err != nil { return fmt.Errorf("invalid native query %s: %s", key, err) } + + // format and replace $ to ${} + query, err = formatNativeQueryVariables(nativeQuery.Query, nativeQuery.Arguments) + if err != nil { + return err + } + + nativeQuery.Query = query + newNativeQueries[key] = nativeQuery } + uc.Config.Metadata.NativeOperations.Queries = newNativeQueries + return nil } @@ -231,8 +251,9 @@ var defaultConfiguration = metadata.Configuration{ NativeOperations: metadata.NativeOperations{}, }, Runtime: metadata.RuntimeSettings{ - Flat: false, - UnixTimeUnit: client.UnixTimeSecond, + Flat: false, + UnixTimeUnit: client.UnixTimeSecond, + ConcurrencyLimit: 5, Format: metadata.RuntimeFormatSettings{ Timestamp: metadata.TimestampUnix, Value: metadata.ValueFloat64, @@ -256,3 +277,66 @@ func validateRegularExpressions(patterns []*regexp.Regexp, input string) bool { } return false } + +func findNativeQueryVariables(nq metadata.NativeQuery) (map[string]metadata.NativeQueryArgumentInfo, error) { + result := map[string]metadata.NativeQueryArgumentInfo{} + matches := nativeQueryVariableRegex.FindAllStringSubmatchIndex(nq.Query, -1) + if len(matches) == 0 { + return result, nil + } + + queryLength := len(nq.Query) + for _, match := range matches { + if len(match) < 4 { + continue + } + name := nq.Query[match[2]:match[3]] + argumentInfo := metadata.NativeQueryArgumentInfo{} + + if match[0] > 0 && nq.Query[match[0]-1] == '[' { + // duration variables should be bounded by square brackets + if match[1] >= queryLength-1 || nq.Query[match[1]] != ']' { + return nil, fmt.Errorf("invalid promQL range syntax") + } + argumentInfo.Type = string(metadata.ScalarDuration) + } else if match[0] > 0 && nq.Query[match[0]-1] == '"' { + // duration variables should be bounded by double quotes + if match[1] >= queryLength-1 || nq.Query[match[1]] != '"' { + return nil, fmt.Errorf("invalid promQL string syntax") + } + argumentInfo.Type = string(metadata.ScalarString) + } + + if len(nq.Arguments) > 0 { + // merge the existing argument from the configuration file + arg, ok := nq.Arguments[name] + if ok { + argumentInfo.Description = arg.Description + if argumentInfo.Type == "" && arg.Type != "" { + argumentInfo.Type = arg.Type + } + } + } + if argumentInfo.Type == "" { + argumentInfo.Type = string(metadata.ScalarString) + } + + result[name] = argumentInfo + } + + return result, nil +} + +func formatNativeQueryVariables(queryInput string, variables map[string]metadata.NativeQueryArgumentInfo) (string, error) { + query := queryInput + for key := range variables { + rawPattern := fmt.Sprintf(`\$\{?%s\}?`, key) + rg, err := regexp.Compile(rawPattern) + if err != nil { + return "", fmt.Errorf("failed to compile regular expression %s, query: %s, error: %s", rawPattern, queryInput, err) + } + query = rg.ReplaceAllLiteralString(query, fmt.Sprintf("${%s}", key)) + } + + return query, nil +} diff --git a/configuration/update_test.go b/configuration/update_test.go new file mode 100644 index 0000000..0355133 --- /dev/null +++ b/configuration/update_test.go @@ -0,0 +1,121 @@ +package main + +import ( + "testing" + + "github.com/hasura/ndc-prometheus/connector/metadata" + "gotest.tools/v3/assert" +) + +func TestNativeQueryVariables(t *testing.T) { + testCases := []struct { + Input metadata.NativeQuery + ExpectedArguments map[string]metadata.NativeQueryArgumentInfo + ExpectedQuery string + ErrorMsg string + }{ + { + Input: metadata.NativeQuery{ + Query: "up", + }, + ExpectedArguments: map[string]metadata.NativeQueryArgumentInfo{}, + ExpectedQuery: "up", + }, + { + Input: metadata.NativeQuery{ + Query: `up{job="${job}", instance="$instance"}`, + }, + ExpectedArguments: map[string]metadata.NativeQueryArgumentInfo{ + "job": { + Type: string(metadata.ScalarString), + }, + "instance": { + Type: string(metadata.ScalarString), + }, + }, + ExpectedQuery: `up{job="${job}", instance="${instance}"}`, + }, + { + Input: metadata.NativeQuery{ + Query: `rate(up{job="${job}", instance="$instance"}[$range])`, + Arguments: map[string]metadata.NativeQueryArgumentInfo{}, + }, + ExpectedArguments: map[string]metadata.NativeQueryArgumentInfo{ + "job": { + Type: string(metadata.ScalarString), + }, + "instance": { + Type: string(metadata.ScalarString), + }, + "range": { + Type: "Duration", + }, + }, + ExpectedQuery: `rate(up{job="${job}", instance="${instance}"}[${range}])`, + }, + { + Input: metadata.NativeQuery{ + Query: `up{job="${job}"} > $value`, + Arguments: map[string]metadata.NativeQueryArgumentInfo{ + "value": { + Type: string(metadata.ScalarFloat64), + }, + }, + }, + ExpectedArguments: map[string]metadata.NativeQueryArgumentInfo{ + "job": { + Type: string(metadata.ScalarString), + }, + "value": { + Type: string(metadata.ScalarFloat64), + }, + }, + ExpectedQuery: `up{job="${job}"} > ${value}`, + }, + { + Input: metadata.NativeQuery{ + Query: `up{job="${job}"} > $value`, + Arguments: map[string]metadata.NativeQueryArgumentInfo{ + "value": {}, + }, + }, + ExpectedArguments: map[string]metadata.NativeQueryArgumentInfo{ + "job": { + Type: string(metadata.ScalarString), + }, + "value": { + Type: string(metadata.ScalarString), + }, + }, + ExpectedQuery: `up{job="${job}"} > ${value}`, + }, + { + Input: metadata.NativeQuery{ + Query: "up[$range", + }, + ErrorMsg: "invalid promQL range syntax", + }, + { + Input: metadata.NativeQuery{ + Query: `up{job="$job}`, + }, + ErrorMsg: "invalid promQL string syntax", + }, + } + + for _, tc := range testCases { + t.Run(tc.Input.Query, func(t *testing.T) { + arguments, err := findNativeQueryVariables(tc.Input) + if tc.ErrorMsg != "" { + assert.ErrorContains(t, err, tc.ErrorMsg) + return + } + + assert.NilError(t, err) + assert.DeepEqual(t, arguments, tc.ExpectedArguments) + query, err := formatNativeQueryVariables(tc.Input.Query, tc.ExpectedArguments) + assert.NilError(t, err) + assert.Equal(t, query, tc.ExpectedQuery) + }) + } +} diff --git a/connector/client/utils.go b/connector/client/utils.go index 894998b..84d99c8 100644 --- a/connector/client/utils.go +++ b/connector/client/utils.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "reflect" + "strings" "time" "github.com/hasura/ndc-sdk-go/utils" @@ -40,18 +41,8 @@ func evalStepFromRange(start time.Time, end time.Time) time.Duration { return time.Second case difference <= time.Hour: return time.Minute - case difference <= 3*time.Hour: - return 5 * time.Minute - case difference <= 6*time.Hour: - return 10 * time.Minute - case difference <= 12*time.Hour: - return 30 * time.Minute - case difference <= 24*time.Hour: - return time.Hour - case difference <= maxSteps*24*time.Hour: - return 24 * time.Hour default: - return 30 * 24 * time.Hour + return difference / 60 } } @@ -62,7 +53,11 @@ func ParseDuration(value any, unixTimeUnit UnixTimeUnit) (time.Duration, error) return 0, nil } - kind := reflectValue.Kind() + return parseDurationReflection(reflectValue, reflectValue.Kind(), unixTimeUnit) +} + +// parseDurationReflection parses duration from a reflection value +func parseDurationReflection(reflectValue reflect.Value, kind reflect.Kind, unixTimeUnit UnixTimeUnit) (time.Duration, error) { switch kind { case reflect.Invalid: return 0, nil @@ -85,6 +80,59 @@ func ParseDuration(value any, unixTimeUnit UnixTimeUnit) (time.Duration, error) } } +// RangeResolution represents the given range and resolution with format xx:xx +type RangeResolution struct { + Range model.Duration + Resolution model.Duration +} + +// String implements the fmt.Stringer interface +func (rr RangeResolution) String() string { + if rr.Resolution == 0 { + return rr.Range.String() + } + return fmt.Sprintf("%s:%s", rr.Range.String(), rr.Resolution.String()) +} + +// ParseRangeResolution parses the range resolution from a string +func ParseRangeResolution(input any, unixTimeUnit UnixTimeUnit) (*RangeResolution, error) { + reflectValue, ok := utils.UnwrapPointerFromReflectValue(reflect.ValueOf(input)) + if !ok { + return nil, nil + } + + kind := reflectValue.Kind() + if kind != reflect.String { + rng, err := parseDurationReflection(reflectValue, kind, unixTimeUnit) + if err != nil { + return nil, fmt.Errorf("invalid range resolution %v: %s", input, err) + } + return &RangeResolution{Range: model.Duration(rng)}, nil + } + + parts := strings.Split(reflectValue.String(), ":") + if parts[0] == "" { + return nil, fmt.Errorf("invalid range resolution %v", input) + } + + rng, err := model.ParseDuration(parts[0]) + if err != nil { + return nil, fmt.Errorf("invalid duration %s: %s", parts[0], err) + } + + result := &RangeResolution{ + Range: rng, + } + if len(parts) > 1 { + resolution, err := model.ParseDuration(parts[1]) + if err != nil { + return nil, fmt.Errorf("invalid resolution %s: %s", parts[1], err) + } + result.Resolution = resolution + } + return result, nil +} + // ParseTimestamp parses timestamp from an unknown value func ParseTimestamp(s any, unixTimeUnit UnixTimeUnit) (*time.Time, error) { reflectValue, ok := utils.UnwrapPointerFromReflectValue(reflect.ValueOf(s)) @@ -104,8 +152,10 @@ func ParseTimestamp(s any, unixTimeUnit UnixTimeUnit) (*time.Time, error) { return &now, nil } // Input timestamps may be provided either in RFC3339 format - if t, err := time.Parse(time.RFC3339, strValue); err == nil { - return &t, nil + for _, format := range []string{time.RFC3339, "2006-01-02T15:04:05Z0700", "2006-01-02T15:04:05-0700", time.RFC3339Nano, time.DateOnly} { + if t, err := time.Parse(format, strValue); err == nil { + return &t, nil + } } if d, err := time.ParseDuration(strValue); err == nil { result := time.Now().Add(-d) diff --git a/connector/internal/collection.go b/connector/internal/collection.go index 6126f25..930ed49 100644 --- a/connector/internal/collection.go +++ b/connector/internal/collection.go @@ -279,7 +279,7 @@ func (qce *QueryCollectionExecutor) buildQueryString(predicate *CollectionReques case metadata.HoltWinters: if !utils.IsNil(fn.Value) { var hw HoltWintersInput - if err := hw.FromValue(fn.Value); err != nil { + if err := hw.FromValue(fn.Value, qce.Runtime.UnixTimeUnit); err != nil { return "", false, fmt.Errorf("%s: %s", fn.Key, err) } query = fmt.Sprintf("%s(%s[%s], %f, %f)", fn.Key, query, hw.Range.String(), hw.Sf, hw.Tf) @@ -287,7 +287,7 @@ func (qce *QueryCollectionExecutor) buildQueryString(predicate *CollectionReques case metadata.PredictLinear: if !utils.IsNil(fn.Value) { var pli PredictLinearInput - if err := pli.FromValue(fn.Value); err != nil { + if err := pli.FromValue(fn.Value, qce.Runtime.UnixTimeUnit); err != nil { return "", false, fmt.Errorf("%s: %s", fn.Key, err) } query = fmt.Sprintf("%s(%s[%s], %f)", fn.Key, query, pli.Range.String(), pli.T) @@ -295,7 +295,7 @@ func (qce *QueryCollectionExecutor) buildQueryString(predicate *CollectionReques case metadata.QuantileOverTime: if !utils.IsNil(fn.Value) { var q QuantileOverTimeInput - if err := q.FromValue(fn.Value); err != nil { + if err := q.FromValue(fn.Value, qce.Runtime.UnixTimeUnit); err != nil { return "", false, fmt.Errorf("%s: %s", fn.Key, err) } query = fmt.Sprintf("%s(%f, %s[%s])", fn.Key, q.Quantile, query, q.Range.String()) @@ -343,7 +343,7 @@ func (qce *QueryCollectionExecutor) buildQueryString(predicate *CollectionReques query = fmt.Sprintf(`%s("%s", %s)`, fn.Key, *label, query) } case metadata.AbsentOverTime, metadata.Changes, metadata.Derivative, metadata.Delta, metadata.IDelta, metadata.Increase, metadata.IRate, metadata.Rate, metadata.Resets, metadata.AvgOverTime, metadata.MinOverTime, metadata.MaxOverTime, metadata.MadOverTime, metadata.SumOverTime, metadata.CountOverTime, metadata.StddevOverTime, metadata.StdvarOverTime, metadata.LastOverTime, metadata.PresentOverTime: - rng, err := ParseRangeResolution(fn.Value) + rng, err := client.ParseRangeResolution(fn.Value, qce.Runtime.UnixTimeUnit) if err != nil { return "", false, fmt.Errorf("%s: %s", fn.Key, err) } diff --git a/connector/internal/collection_test.go b/connector/internal/collection_test.go index 1a618e5..dc01043 100644 --- a/connector/internal/collection_test.go +++ b/connector/internal/collection_test.go @@ -547,7 +547,7 @@ var testCases = []struct { { "predict_linear": map[string]any{ "t": 0.1, - "range": "1m", + "range": 60, }, }, }).Encode(), @@ -559,7 +559,7 @@ var testCases = []struct { Functions: []KeyValue{ {Key: "predict_linear", Value: map[string]any{ "t": 0.1, - "range": "1m", + "range": 60, }}, }, }, diff --git a/connector/internal/native_query.go b/connector/internal/native_query.go index 440f2c7..bda8a34 100644 --- a/connector/internal/native_query.go +++ b/connector/internal/native_query.go @@ -69,7 +69,7 @@ func (nqe *NativeQueryExecutor) Explain(ctx context.Context) (*nativeQueryParame } argString = fmt.Sprint(argFloat) case metadata.ScalarDuration: - duration, err := ParseRangeResolution(arg) + duration, err := client.ParseRangeResolution(arg, nqe.Runtime.UnixTimeUnit) if err != nil { return nil, "", schema.UnprocessableContentError(err.Error(), nil) } diff --git a/connector/internal/promql.go b/connector/internal/promql.go index 917b56d..11e67d3 100644 --- a/connector/internal/promql.go +++ b/connector/internal/promql.go @@ -5,6 +5,7 @@ import ( "fmt" "strings" + "github.com/hasura/ndc-prometheus/connector/client" "github.com/hasura/ndc-sdk-go/utils" ) @@ -43,11 +44,11 @@ func (lri LabelReplaceInput) String() string { type HoltWintersInput struct { Sf float64 Tf float64 - Range RangeResolution + Range client.RangeResolution } // FromValue decodes data from any value -func (hwi *HoltWintersInput) FromValue(value any) error { +func (hwi *HoltWintersInput) FromValue(value any, unixTimeUnit client.UnixTimeUnit) error { m, ok := value.(map[string]any) if !ok { return fmt.Errorf("invalid HoltWintersInput value, expected map, got: %v", value) @@ -69,7 +70,7 @@ func (hwi *HoltWintersInput) FromValue(value any) error { return fmt.Errorf("invalid HoltWintersInput trend factor. Expected: 0 < tf < 1, got: %f", sf) } - rng, err := ParseRangeResolution(m["range"]) + rng, err := client.ParseRangeResolution(m["range"], unixTimeUnit) if err != nil { return fmt.Errorf("invalid HoltWintersInput range: %s", err) } @@ -87,11 +88,11 @@ func (hwi *HoltWintersInput) FromValue(value any) error { // PredictLinearInput represents input arguments of the predict_linear function type PredictLinearInput struct { T float64 - Range RangeResolution + Range client.RangeResolution } // FromValue decodes data from any value -func (pli *PredictLinearInput) FromValue(value any) error { +func (pli *PredictLinearInput) FromValue(value any, unixTimeUnit client.UnixTimeUnit) error { m, ok := value.(map[string]any) if !ok { return fmt.Errorf("invalid PredictLinearInput value, expected map, got: %v", value) @@ -101,7 +102,7 @@ func (pli *PredictLinearInput) FromValue(value any) error { return fmt.Errorf("invalid PredictLinearInput t: %s", err) } - rng, err := ParseRangeResolution(m["range"]) + rng, err := client.ParseRangeResolution(m["range"], unixTimeUnit) if err != nil { return fmt.Errorf("invalid PredictLinearInput range: %s", err) } @@ -118,11 +119,11 @@ func (pli *PredictLinearInput) FromValue(value any) error { // QuantileOverTimeInput represents input arguments of the quantile_over_time function type QuantileOverTimeInput struct { Quantile float64 - Range RangeResolution + Range client.RangeResolution } // FromValue decodes data from any value -func (qoti *QuantileOverTimeInput) FromValue(value any) error { +func (qoti *QuantileOverTimeInput) FromValue(value any, unixTimeUnit client.UnixTimeUnit) error { m, ok := value.(map[string]any) if !ok { return fmt.Errorf("invalid PredictLinearInput value, expected map, got: %v", value) @@ -136,7 +137,7 @@ func (qoti *QuantileOverTimeInput) FromValue(value any) error { return fmt.Errorf("quantile value should be between 0 and 1, got %f", quantile) } - rng, err := ParseRangeResolution(m["range"]) + rng, err := client.ParseRangeResolution(m["range"], unixTimeUnit) if err != nil { return fmt.Errorf("invalid PredictLinearInput range: %s", err) } diff --git a/connector/internal/range.go b/connector/internal/range.go deleted file mode 100644 index b9dc5b9..0000000 --- a/connector/internal/range.go +++ /dev/null @@ -1,55 +0,0 @@ -package internal - -import ( - "fmt" - "strings" - - "github.com/hasura/ndc-sdk-go/utils" - "github.com/prometheus/common/model" -) - -// RangeResolution represents the given range and resolution with format xx:xx -type RangeResolution struct { - Range model.Duration - Resolution model.Duration -} - -// String implements the fmt.Stringer interface -func (rr RangeResolution) String() string { - if rr.Resolution == 0 { - return rr.Range.String() - } - return fmt.Sprintf("%s:%s", rr.Range.String(), rr.Resolution.String()) -} - -// ParseRangeResolution parses the range resolution from a string -func ParseRangeResolution(input any) (*RangeResolution, error) { - str, err := utils.DecodeNullableString(input) - if err != nil { - return nil, fmt.Errorf("invalid range resolution %v: %s", input, err) - } - if str == nil { - return nil, nil - } - parts := strings.Split(*str, ":") - if parts[0] == "" { - return nil, fmt.Errorf("invalid range resolution %v", input) - } - - rng, err := model.ParseDuration(parts[0]) - if err != nil { - return nil, fmt.Errorf("invalid duration %s: %s", parts[0], err) - } - - result := &RangeResolution{ - Range: rng, - } - if len(parts) > 1 { - resolution, err := model.ParseDuration(parts[1]) - if err != nil { - return nil, fmt.Errorf("invalid resolution %s: %s", parts[1], err) - } - result.Resolution = resolution - } - return result, nil -} diff --git a/connector/metadata/configuration.go b/connector/metadata/configuration.go index d48c64f..f91a422 100644 --- a/connector/metadata/configuration.go +++ b/connector/metadata/configuration.go @@ -90,6 +90,8 @@ type RuntimeSettings struct { UnixTimeUnit client.UnixTimeUnit `json:"unix_time_unit" yaml:"unix_time_unit" jsonschema:"enum=s,enum=ms,default=s"` // The serialization format for response fields Format RuntimeFormatSettings `json:"format" yaml:"format"` + // The concurrency limit of queries if there are many variables in a single query + ConcurrencyLimit int `json:"concurrency_limit,omitempty" yaml:"concurrency_limit,omitempty"` } // ReadConfiguration reads the configuration from file diff --git a/connector/query.go b/connector/query.go index 31ce88b..27a1884 100644 --- a/connector/query.go +++ b/connector/query.go @@ -11,6 +11,7 @@ import ( "github.com/hasura/ndc-sdk-go/schema" "github.com/hasura/ndc-sdk-go/utils" "go.opentelemetry.io/otel/codes" + "golang.org/x/sync/errgroup" ) var histogramNameRegex = regexp.MustCompile(`^(\w+)_(sum|count|bucket)$`) @@ -21,6 +22,14 @@ func (c *PrometheusConnector) Query(ctx context.Context, configuration *metadata if len(requestVars) == 0 { requestVars = []schema.QueryRequestVariablesElem{make(schema.QueryRequestVariablesElem)} } + + if len(requestVars) == 1 || c.runtime.ConcurrencyLimit <= 1 { + return c.execQuerySync(ctx, state, request, requestVars) + } + return c.execQueryAsync(ctx, state, request, requestVars) +} + +func (c *PrometheusConnector) execQuerySync(ctx context.Context, state *metadata.State, request *schema.QueryRequest, requestVars []schema.QueryRequestVariablesElem) ([]schema.RowSet, error) { rowSets := make([]schema.RowSet, len(requestVars)) for i, requestVar := range requestVars { @@ -34,6 +43,31 @@ func (c *PrometheusConnector) Query(ctx context.Context, configuration *metadata return rowSets, nil } +func (c *PrometheusConnector) execQueryAsync(ctx context.Context, state *metadata.State, request *schema.QueryRequest, requestVars []schema.QueryRequestVariablesElem) ([]schema.RowSet, error) { + rowSets := make([]schema.RowSet, len(requestVars)) + + eg, ctx := errgroup.WithContext(ctx) + eg.SetLimit(c.runtime.ConcurrencyLimit) + + for i, requestVar := range requestVars { + func(index int, vars schema.QueryRequestVariablesElem) { + eg.Go(func() error { + result, err := c.execQuery(ctx, state, request, vars, index) + if err != nil { + return err + } + rowSets[index] = *result + return nil + }) + }(i, requestVar) + } + + if err := eg.Wait(); err != nil { + return nil, err + } + return rowSets, nil +} + func (c *PrometheusConnector) execQuery(ctx context.Context, state *metadata.State, request *schema.QueryRequest, variables map[string]any, index int) (*schema.RowSet, error) { ctx, span := state.Tracer.Start(ctx, fmt.Sprintf("Execute Query %d", index)) defer span.End() diff --git a/connector/testdata/query/process_cpu_seconds_total_variables/request.json b/connector/testdata/query/process_cpu_seconds_total_variables/request.json new file mode 100644 index 0000000..aabc07c --- /dev/null +++ b/connector/testdata/query/process_cpu_seconds_total_variables/request.json @@ -0,0 +1,45 @@ +{ + "collection": "process_cpu_seconds_total", + "query": { + "fields": { + "job": { "type": "column", "column": "job", "fields": null }, + "value": { "type": "column", "column": "value", "fields": null }, + "timestamp": { "type": "column", "column": "timestamp", "fields": null } + }, + "limit": 1, + "order_by": { + "elements": [ + { + "order_direction": "desc", + "target": { "type": "column", "name": "job", "path": [] } + } + ] + }, + "predicate": { + "type": "and", + "expressions": [ + { + "type": "binary_comparison_operator", + "column": { "type": "column", "name": "timestamp", "path": [] }, + "operator": "_gt", + "value": { "type": "scalar", "value": "2024-10-06T00:00:00Z" } + }, + { + "type": "binary_comparison_operator", + "column": { "type": "column", "name": "job", "path": [] }, + "operator": "_eq", + "value": { "type": "variable", "name": "$job" } + } + ] + } + }, + "arguments": { + "fn": { + "type": "literal", + "value": [{ "increase": "1m" }, { "sum": ["job"] }, { "avg": ["job"] }] + }, + "step": { "type": "literal", "value": "5m" } + }, + "collection_relationships": {}, + "variables": [{ "$job": "ndc-prometheus" }, { "$job": "node" }] +} diff --git a/go.mod b/go.mod index 95a831c..4d14b52 100644 --- a/go.mod +++ b/go.mod @@ -1,25 +1,26 @@ module github.com/hasura/ndc-prometheus -go 1.23.0 +go 1.23 require ( github.com/alecthomas/kong v1.2.1 github.com/go-viper/mapstructure/v2 v2.2.1 - github.com/hasura/ndc-sdk-go v1.4.1-0.20240929164022-df21222b1d21 + github.com/hasura/ndc-sdk-go v1.5.0 github.com/iancoleman/strcase v0.3.0 github.com/invopop/jsonschema v0.12.0 github.com/lmittmann/tint v1.0.5 github.com/prometheus/client_golang v1.20.4 - github.com/prometheus/common v0.59.1 + github.com/prometheus/common v0.60.0 go.opentelemetry.io/otel v1.30.0 go.opentelemetry.io/otel/trace v1.30.0 + golang.org/x/sync v0.8.0 google.golang.org/api v0.199.0 gopkg.in/yaml.v3 v3.0.1 gotest.tools/v3 v3.5.1 ) require ( - cloud.google.com/go/auth v0.9.5 // indirect + cloud.google.com/go/auth v0.9.7 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect cloud.google.com/go/compute/metadata v0.5.2 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect @@ -60,14 +61,14 @@ require ( go.opentelemetry.io/otel/sdk v1.30.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.30.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect - golang.org/x/crypto v0.27.0 // indirect - golang.org/x/net v0.29.0 // indirect + golang.org/x/crypto v0.28.0 // indirect + golang.org/x/net v0.30.0 // indirect golang.org/x/oauth2 v0.23.0 // indirect - golang.org/x/sys v0.25.0 // indirect - golang.org/x/text v0.18.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240924160255-9d4c2d233b61 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240924160255-9d4c2d233b61 // indirect - google.golang.org/grpc v1.67.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240930140551-af27646dc61f // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240930140551-af27646dc61f // indirect + google.golang.org/grpc v1.67.1 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index cf80b0e..f251f2e 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,6 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go/auth v0.9.5 h1:4CTn43Eynw40aFVr3GpPqsQponx2jv0BQpjvajsbbzw= -cloud.google.com/go/auth v0.9.5/go.mod h1:Xo0n7n66eHyOWWCnitop6870Ilwo3PiZyodVkkH1xWM= +cloud.google.com/go/auth v0.9.7 h1:ha65jNwOfI48YmUzNfMaUDfqt5ykuYIUnSartpU1+BA= +cloud.google.com/go/auth v0.9.7/go.mod h1:Xo0n7n66eHyOWWCnitop6870Ilwo3PiZyodVkkH1xWM= cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY= cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc= cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo= @@ -73,8 +73,8 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gT github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= -github.com/hasura/ndc-sdk-go v1.4.1-0.20240929164022-df21222b1d21 h1:BjX9Wg6v9h9A6rjOeD6URZR8kOTbcJz2Qqm9Abkwqss= -github.com/hasura/ndc-sdk-go v1.4.1-0.20240929164022-df21222b1d21/go.mod h1:oik0JrwuN5iZwZjZJzIRMw9uO2xDJbCXwhS1GgaRejk= +github.com/hasura/ndc-sdk-go v1.5.0 h1:KvfzbrWBDrs21oz1xA0BvWNhKRqAX0NqLCfPyKz+l/M= +github.com/hasura/ndc-sdk-go v1.5.0/go.mod h1:oik0JrwuN5iZwZjZJzIRMw9uO2xDJbCXwhS1GgaRejk= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= @@ -114,8 +114,8 @@ github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/j github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.59.1 h1:LXb1quJHWm1P6wq/U824uxYi4Sg0oGvNeUm1z5dJoX0= -github.com/prometheus/common v0.59.1/go.mod h1:GpWM7dewqmVYcd7SmRaiWVe9SSqjf0UrwnYnpEZNuT0= +github.com/prometheus/common v0.60.0 h1:+V9PAREWNvJMAuJ1x1BaWl9dewMW4YrHZQbx0sJNllA= +github.com/prometheus/common v0.60.0/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= @@ -165,8 +165,8 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= -golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -177,8 +177,8 @@ golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= -golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= @@ -191,12 +191,12 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= -golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -210,17 +210,17 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto/googleapis/api v0.0.0-20240924160255-9d4c2d233b61 h1:pAjq8XSSzXoP9ya73v/w+9QEAAJNluLrpmMq5qFJQNY= -google.golang.org/genproto/googleapis/api v0.0.0-20240924160255-9d4c2d233b61/go.mod h1:O6rP0uBq4k0mdi/b4ZEMAZjkhYWhS815kCvaMha4VN8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240924160255-9d4c2d233b61 h1:N9BgCIAUvn/M+p4NJccWPWb3BWh88+zyL0ll9HgbEeM= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240924160255-9d4c2d233b61/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/genproto/googleapis/api v0.0.0-20240930140551-af27646dc61f h1:jTm13A2itBi3La6yTGqn8bVSrc3ZZ1r8ENHlIXBfnRA= +google.golang.org/genproto/googleapis/api v0.0.0-20240930140551-af27646dc61f/go.mod h1:CLGoBuH1VHxAUXVPP8FfPwPEVJB6lz3URE5mY2SuayE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240930140551-af27646dc61f h1:cUMEy+8oS78BWIH9OWazBkzbr090Od9tWBNtZHkOhf0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240930140551-af27646dc61f/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.67.0 h1:IdH9y6PF5MPSdAntIcpjQ+tXO41pcQsfZV2RxtQgVcw= -google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= +google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= diff --git a/jsonschema/configuration.json b/jsonschema/configuration.json index 320fbfe..7d49e5a 100644 --- a/jsonschema/configuration.json +++ b/jsonschema/configuration.json @@ -464,6 +464,9 @@ }, "format": { "$ref": "#/$defs/RuntimeFormatSettings" + }, + "concurrency_limit": { + "type": "integer" } }, "additionalProperties": false, diff --git a/tests/configuration/configuration.yaml b/tests/configuration/configuration.yaml index 5466657..08ddfe2 100644 --- a/tests/configuration/configuration.yaml +++ b/tests/configuration/configuration.yaml @@ -211,6 +211,7 @@ metadata: runtime: flat: true unix_time_unit: s + concurrency_limit: 3 format: timestamp: rfc3339 value: float64 diff --git a/tests/engine/app/metadata/prometheus.hml b/tests/engine/app/metadata/prometheus.hml index 98b630c..08dfd6c 100644 --- a/tests/engine/app/metadata/prometheus.hml +++ b/tests/engine/app/metadata/prometheus.hml @@ -73,63 +73,64 @@ definition: representation: type: enum one_of: - - net_peer_port - - otel_scope_name + - job + - net_peer_name - le - http_method - - net_peer_name - - job - - otel_scope_version - http_status_code - instance + - net_peer_port + - otel_scope_name + - otel_scope_version aggregate_functions: {} comparison_operators: {} HttpClientDurationMillisecondsCountLabel: representation: type: enum one_of: + - otel_scope_name + - otel_scope_version - http_method - http_status_code - instance - job - net_peer_name - net_peer_port - - otel_scope_name - - otel_scope_version aggregate_functions: {} comparison_operators: {} HttpClientDurationMillisecondsSumLabel: representation: type: enum one_of: - - net_peer_name - - net_peer_port - - otel_scope_name - - otel_scope_version - http_method - http_status_code - instance - job + - net_peer_name + - net_peer_port + - otel_scope_name + - otel_scope_version aggregate_functions: {} comparison_operators: {} HttpClientRequestSizeBytesTotalLabel: representation: type: enum one_of: - - net_peer_port - - otel_scope_name - otel_scope_version - http_method - http_status_code - instance - job - net_peer_name + - net_peer_port + - otel_scope_name aggregate_functions: {} comparison_operators: {} HttpClientResponseSizeBytesTotalLabel: representation: type: enum one_of: + - http_status_code - instance - job - net_peer_name @@ -137,7 +138,6 @@ definition: - otel_scope_name - otel_scope_version - http_method - - http_status_code aggregate_functions: {} comparison_operators: {} Int64: @@ -159,12 +159,12 @@ definition: representation: type: enum one_of: - - status - collection - http_status - instance - job - otel_scope_name + - status aggregate_functions: {} comparison_operators: {} NdcPrometheusQueryTotalTimeBucketLabel: @@ -182,20 +182,20 @@ definition: representation: type: enum one_of: - - instance - - job - otel_scope_name - collection + - instance + - job aggregate_functions: {} comparison_operators: {} NdcPrometheusQueryTotalTimeSumLabel: representation: type: enum one_of: - - collection - - instance - job - otel_scope_name + - collection + - instance aggregate_functions: {} comparison_operators: {} NetConntrackDialerConnAttemptedTotalLabel: @@ -261,8 +261,8 @@ definition: representation: type: enum one_of: - - instance - job + - instance aggregate_functions: {} comparison_operators: {} ProcessNetworkTransmitBytesTotalLabel: @@ -285,16 +285,16 @@ definition: representation: type: enum one_of: - - job - instance + - job aggregate_functions: {} comparison_operators: {} ProcessStartTimeSecondsLabel: representation: type: enum one_of: - - job - instance + - job aggregate_functions: {} comparison_operators: {} ProcessVirtualMemoryBytesLabel: @@ -317,9 +317,9 @@ definition: representation: type: enum one_of: - - cause - instance - job + - cause aggregate_functions: {} comparison_operators: {} PromhttpMetricHandlerRequestsInFlightLabel: @@ -377,13 +377,13 @@ definition: representation: type: enum one_of: + - instance + - job - service_name - service_version - telemetry_sdk_language - telemetry_sdk_name - telemetry_sdk_version - - instance - - job aggregate_functions: {} comparison_operators: {} Timestamp: