diff --git a/Dockerfile.invoca b/Dockerfile.invoca new file mode 100644 index 00000000000..a4473ad0408 --- /dev/null +++ b/Dockerfile.invoca @@ -0,0 +1,10 @@ +FROM golang:1.26 as build + +WORKDIR $GOPATH/src/github.com/thanos-io/thanos +COPY . $GOPATH/src/github.com/thanos-io/thanos +RUN git update-index --refresh; make build + +FROM invocaops/base:master +COPY --from=build /go/bin/thanos /bin/thanos + +ENTRYPOINT ["/bin/thanos"] diff --git a/cmd/thanos/query.go b/cmd/thanos/query.go index bc2689bc868..8e42471ec6a 100644 --- a/cmd/thanos/query.go +++ b/cmd/thanos/query.go @@ -21,7 +21,6 @@ import ( "github.com/prometheus/common/route" "github.com/prometheus/prometheus/model/labels" "github.com/prometheus/prometheus/promql" - "github.com/prometheus/prometheus/promql/parser" apiv1 "github.com/thanos-io/thanos/pkg/api/query" "github.com/thanos-io/thanos/pkg/api/query/querypb" @@ -34,6 +33,7 @@ import ( "github.com/thanos-io/thanos/pkg/extgrpc" "github.com/thanos-io/thanos/pkg/extkingpin" "github.com/thanos-io/thanos/pkg/extprom" + "github.com/thanos-io/thanos/pkg/extpromql" extpromhttp "github.com/thanos-io/thanos/pkg/extprom/http" "github.com/thanos-io/thanos/pkg/gate" "github.com/thanos-io/thanos/pkg/info" @@ -224,7 +224,7 @@ func registerQuery(app *extkingpin.App) { for _, feature := range *featureList { if feature == promqlExperimentalFunctions { - parser.EnableExperimentalFunctions = true + extpromql.SetEnableExperimentalFunctions(true) level.Info(logger).Log("msg", "Experimental PromQL functions enabled.", "option", promqlExperimentalFunctions) } if feature == promqlAtModifier { diff --git a/cmd/thanos/query_frontend.go b/cmd/thanos/query_frontend.go index 0022d0a5eed..de9bb6650aa 100644 --- a/cmd/thanos/query_frontend.go +++ b/cmd/thanos/query_frontend.go @@ -32,6 +32,7 @@ import ( "github.com/thanos-io/thanos/pkg/exthttp" "github.com/thanos-io/thanos/pkg/extkingpin" "github.com/thanos-io/thanos/pkg/extprom" + "github.com/thanos-io/thanos/pkg/extpromql" extpromhttp "github.com/thanos-io/thanos/pkg/extprom/http" "github.com/thanos-io/thanos/pkg/logging" "github.com/thanos-io/thanos/pkg/prober" @@ -310,7 +311,7 @@ func runQueryFrontend( if len(cfg.EnableFeatures) > 0 { for _, feature := range cfg.EnableFeatures { if feature == promqlExperimentalFunctions { - parser.EnableExperimentalFunctions = true + extpromql.SetEnableExperimentalFunctions(true) level.Info(logger).Log("msg", "Experimental PromQL functions enabled.", "option", promqlExperimentalFunctions) } } diff --git a/cmd/thanos/rule.go b/cmd/thanos/rule.go index 14f3f2f45c6..b9ed8373650 100644 --- a/cmd/thanos/rule.go +++ b/cmd/thanos/rule.go @@ -62,8 +62,8 @@ import ( "github.com/thanos-io/thanos/pkg/extgrpc" "github.com/thanos-io/thanos/pkg/extkingpin" "github.com/thanos-io/thanos/pkg/extprom" - extpromhttp "github.com/thanos-io/thanos/pkg/extprom/http" "github.com/thanos-io/thanos/pkg/extpromql" + extpromhttp "github.com/thanos-io/thanos/pkg/extprom/http" "github.com/thanos-io/thanos/pkg/info" "github.com/thanos-io/thanos/pkg/info/infopb" "github.com/thanos-io/thanos/pkg/logging" @@ -602,7 +602,7 @@ func runRule( if len(conf.EnableFeatures) > 0 { for _, feature := range conf.EnableFeatures { if feature == promqlExperimentalFunctions { - parser.EnableExperimentalFunctions = true + extpromql.SetEnableExperimentalFunctions(true) level.Info(logger).Log("msg", "Experimental PromQL functions enabled.", "option", promqlExperimentalFunctions) } } diff --git a/go.mod b/go.mod index 1a69c9e3e4e..11a695fceb8 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.26.0 require ( capnproto.org/go/capnp/v3 v3.1.0-alpha.1 - cloud.google.com/go/trace v1.11.6 + cloud.google.com/go/trace v1.11.7 github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.27.0 github.com/KimMachineGun/automemlimit v0.7.5 github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b @@ -15,7 +15,7 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89 github.com/chromedp/chromedp v0.9.2 - github.com/coreos/go-systemd/v22 v22.6.0 + github.com/coreos/go-systemd/v22 v22.7.0 github.com/cortexproject/promqlsmith v0.0.0-20250407233056-90db95b1a4e4 github.com/cristalhq/hedgedhttp v0.9.1 github.com/dustin/go-humanize v1.0.1 @@ -26,9 +26,9 @@ require ( github.com/fatih/structtag v1.2.0 github.com/felixge/fgprof v0.9.5 github.com/fortytw2/leaktest v1.3.0 - github.com/fsnotify/fsnotify v1.9.0 + github.com/fsnotify/fsnotify v1.10.1 github.com/go-kit/log v0.2.1 - github.com/go-openapi/strfmt v0.25.0 + github.com/go-openapi/strfmt v0.26.3 github.com/gogo/protobuf v1.3.2 github.com/gogo/status v1.1.1 github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 @@ -42,35 +42,34 @@ require ( github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/jpillora/backoff v1.0.0 github.com/json-iterator/go v1.1.12 - github.com/klauspost/compress v1.18.2 + github.com/klauspost/compress v1.18.6 github.com/leanovate/gopter v0.2.9 github.com/lightstep/lightstep-tracer-go v0.26.0 github.com/lovoo/gcloud-opentracing v0.3.0 - github.com/miekg/dns v1.1.69 + github.com/miekg/dns v1.1.72 github.com/minio/sha256-simd v1.0.1 github.com/mitchellh/go-ps v1.0.0 github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f github.com/oklog/run v1.2.0 - github.com/oklog/ulid v1.3.1 // indirect github.com/olekukonko/tablewriter v0.0.5 - github.com/onsi/gomega v1.36.2 + github.com/onsi/gomega v1.38.2 github.com/opentracing/basictracer-go v1.1.0 github.com/opentracing/opentracing-go v1.2.0 github.com/pkg/errors v0.9.1 - github.com/prometheus-community/prom-label-proxy v0.11.1 - github.com/prometheus/alertmanager v0.30.0 + github.com/prometheus-community/prom-label-proxy v0.14.0 + github.com/prometheus/alertmanager v0.33.0 github.com/prometheus/client_golang v1.23.2 github.com/prometheus/client_model v0.6.2 - github.com/prometheus/common v0.67.5 - github.com/prometheus/exporter-toolkit v0.15.0 + github.com/prometheus/common v0.69.0 + github.com/prometheus/exporter-toolkit v0.16.0 // Prometheus maps version 3.x.y to tags v0.30x.y. - github.com/prometheus/prometheus v0.309.1 + github.com/prometheus/prometheus v0.312.0 github.com/redis/rueidis v1.0.61 github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 github.com/sony/gobreaker v1.0.0 github.com/stretchr/testify v1.11.1 github.com/thanos-io/objstore v0.0.0-20250804093838-71d60dfee488 - github.com/thanos-io/promql-engine v0.0.0-20260513095632-c5f6038078e3 + github.com/thanos-io/promql-engine v0.0.0-20260707114442-fa2cb843e6ba github.com/uber/jaeger-client-go v2.30.0+incompatible github.com/vimeo/galaxycache v1.3.1 github.com/weaveworks/common v0.0.0-20230728070032-dd9e68f319d5 @@ -78,14 +77,14 @@ require ( go.elastic.co/apm/module/apmot v1.15.0 go.opentelemetry.io/contrib/propagators/autoprop v0.61.0 go.opentelemetry.io/contrib/samplers/jaegerremote v0.30.0 - go.opentelemetry.io/otel v1.43.0 + go.opentelemetry.io/otel v1.44.0 go.opentelemetry.io/otel/bridge/opentracing v1.36.0 go.opentelemetry.io/otel/exporters/jaeger v1.17.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 - go.opentelemetry.io/otel/sdk v1.43.0 - go.opentelemetry.io/otel/trace v1.43.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0 + go.opentelemetry.io/otel/sdk v1.44.0 + go.opentelemetry.io/otel/trace v1.44.0 go.uber.org/atomic v1.11.0 go.uber.org/goleak v1.3.0 go4.org/intern v0.0.0-20230525184215-6c62f75575cb @@ -93,7 +92,7 @@ require ( golang.org/x/net v0.56.0 golang.org/x/sync v0.21.0 golang.org/x/text v0.38.0 - golang.org/x/time v0.14.0 + golang.org/x/time v0.15.0 google.golang.org/grpc v1.81.1 google.golang.org/grpc/examples v0.0.0-20250407062114-b368379ef8f6 google.golang.org/protobuf v1.36.11 @@ -102,17 +101,17 @@ require ( ) require ( - cloud.google.com/go v0.120.0 // indirect - cloud.google.com/go/auth v0.17.0 // indirect + cloud.google.com/go v0.121.6 // indirect + cloud.google.com/go/auth v0.20.0 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect - cloud.google.com/go/iam v1.5.2 // indirect - cloud.google.com/go/storage v1.50.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 // indirect - github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect + cloud.google.com/go/iam v1.5.3 // indirect + cloud.google.com/go/storage v1.56.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.1 // indirect - github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2 // indirect ) require ( @@ -121,7 +120,7 @@ require ( github.com/oklog/ulid/v2 v2.1.1 github.com/prometheus/otlptranslator v1.0.0 github.com/tjhop/slog-gokit v0.1.4 - go.opentelemetry.io/collector/pdata v1.48.0 + go.opentelemetry.io/collector/pdata v1.60.0 go.opentelemetry.io/collector/semconv v0.128.0 go.opentelemetry.io/proto/otlp v1.10.0 ) @@ -133,9 +132,9 @@ require ( github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 // indirect github.com/elastic/go-licenser v0.4.2 // indirect github.com/go-ini/ini v1.67.0 // indirect - github.com/go-openapi/runtime v0.29.2 // indirect + github.com/go-openapi/runtime v0.32.3 // indirect github.com/goccy/go-json v0.10.5 // indirect - github.com/golang-jwt/jwt/v5 v5.3.0 // indirect + github.com/golang-jwt/jwt/v5 v5.3.1 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/huaweicloud/huaweicloud-sdk-go-obs v3.25.4+incompatible // indirect github.com/jcchavezs/porto v0.7.0 // indirect @@ -147,104 +146,106 @@ require ( github.com/onsi/ginkgo v1.16.5 // indirect github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect github.com/sercand/kuberesolver/v4 v4.0.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 // indirect go.opentelemetry.io/contrib/propagators/ot v1.36.0 // indirect go4.org/unsafe/assume-no-moving-gc v0.0.0-20231121144256-b99613f794b6 // indirect golang.org/x/lint v0.0.0-20241112194109-818c5a804067 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260615183401-62b3387ff324 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260615183401-62b3387ff324 // indirect - k8s.io/apimachinery v0.34.3 // indirect - k8s.io/client-go v0.34.3 // indirect - k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect + k8s.io/apimachinery v0.35.3 // indirect + k8s.io/client-go v0.35.3 // indirect + k8s.io/klog/v2 v2.140.0 // indirect + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect ) require ( cel.dev/expr v0.25.1 // indirect - cloud.google.com/go/monitoring v1.24.2 // indirect + cloud.google.com/go/monitoring v1.24.3 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.50.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.52.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 // indirect github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible // indirect github.com/armon/go-radix v1.0.0 // indirect - github.com/aws/aws-sdk-go-v2 v1.41.0 // indirect - github.com/aws/aws-sdk-go-v2/config v1.32.6 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.19.6 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 // indirect - github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 // indirect - github.com/aws/smithy-go v1.24.0 // indirect + github.com/aws/aws-sdk-go-v2 v1.42.0 // indirect + github.com/aws/aws-sdk-go-v2/config v1.32.25 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.19.24 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.2.0 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.31.3 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.43.3 // indirect + github.com/aws/smithy-go v1.27.2 // indirect + github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/baidubce/bce-sdk-go v0.9.230 // indirect github.com/beorn7/perks v1.0.1 // indirect + github.com/buger/jsonparser v1.1.2 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/chromedp/sysutil v1.0.0 // indirect github.com/clbanning/mxj v1.8.4 // indirect github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dennwc/varint v1.0.0 // indirect - github.com/edsrzf/mmap-go v1.2.0 // indirect + github.com/edsrzf/mmap-go v1.2.1-0.20241212181136-fad1cd13edbd // indirect github.com/elastic/go-sysinfo v1.15.3 // indirect github.com/elastic/go-windows v1.0.2 // indirect github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect - github.com/fatih/color v1.18.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-openapi/analysis v0.24.1 // indirect - github.com/go-openapi/errors v0.22.4 // indirect - github.com/go-openapi/jsonpointer v0.22.1 // indirect - github.com/go-openapi/jsonreference v0.21.3 // indirect - github.com/go-openapi/loads v0.23.2 // indirect - github.com/go-openapi/spec v0.22.1 // indirect - github.com/go-openapi/swag v0.25.4 // indirect - github.com/go-openapi/swag/cmdutils v0.25.4 // indirect - github.com/go-openapi/swag/conv v0.25.4 // indirect - github.com/go-openapi/swag/fileutils v0.25.4 // indirect - github.com/go-openapi/swag/jsonname v0.25.4 // indirect - github.com/go-openapi/swag/jsonutils v0.25.4 // indirect - github.com/go-openapi/swag/loading v0.25.4 // indirect - github.com/go-openapi/swag/mangling v0.25.4 // indirect - github.com/go-openapi/swag/netutils v0.25.4 // indirect - github.com/go-openapi/swag/stringutils v0.25.4 // indirect - github.com/go-openapi/swag/typeutils v0.25.4 // indirect - github.com/go-openapi/swag/yamlutils v0.25.4 // indirect - github.com/go-openapi/validate v0.25.1 // indirect - github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/go-openapi/analysis v0.25.2 // indirect + github.com/go-openapi/errors v0.22.7 // indirect + github.com/go-openapi/jsonpointer v0.23.1 // indirect + github.com/go-openapi/jsonreference v0.21.6 // indirect + github.com/go-openapi/loads v0.23.3 // indirect + github.com/go-openapi/runtime/server-middleware v0.30.0 // indirect + github.com/go-openapi/spec v0.22.5 // indirect + github.com/go-openapi/swag v0.26.0 // indirect + github.com/go-openapi/swag/cmdutils v0.26.0 // indirect + github.com/go-openapi/swag/conv v0.26.0 // indirect + github.com/go-openapi/swag/fileutils v0.26.0 // indirect + github.com/go-openapi/swag/jsonname v0.26.0 // indirect + github.com/go-openapi/swag/jsonutils v0.26.0 // indirect + github.com/go-openapi/swag/loading v0.26.0 // indirect + github.com/go-openapi/swag/mangling v0.26.0 // indirect + github.com/go-openapi/swag/netutils v0.26.0 // indirect + github.com/go-openapi/swag/stringutils v0.26.0 // indirect + github.com/go-openapi/swag/typeutils v0.26.0 // indirect + github.com/go-openapi/swag/yamlutils v0.26.0 // indirect + github.com/go-openapi/validate v0.25.3 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/ws v1.2.1 // indirect github.com/gofrs/flock v0.12.1 // indirect github.com/gogo/googleapis v1.4.1 // indirect - github.com/google/go-querystring v1.1.0 // indirect - github.com/google/pprof v0.0.0-20251213031049-b05bdaca462f // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect - github.com/googleapis/gax-go/v2 v2.15.0 // indirect + github.com/google/go-querystring v1.2.0 // indirect + github.com/google/pprof v0.0.0-20260604005048-7023385849c0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.15 // indirect + github.com/googleapis/gax-go/v2 v2.22.0 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect - github.com/hashicorp/go-version v1.8.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect + github.com/hashicorp/go-version v1.9.0 // indirect github.com/jaegertracing/jaeger-idl v0.6.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/julienschmidt/httprouter v1.3.0 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/knadh/koanf/maps v0.1.2 // indirect github.com/knadh/koanf/providers/confmap v1.0.0 // indirect - github.com/knadh/koanf/v2 v2.3.0 // indirect + github.com/knadh/koanf/v2 v2.3.5 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20210210170715-a8dfcb80d3a7 // indirect github.com/mailru/easyjson v0.9.0 // indirect - github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/minio/crc64nvme v1.0.1 // indirect github.com/minio/md5-simd v1.1.2 // indirect @@ -256,20 +257,23 @@ require ( github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/mozillazg/go-httpheader v0.4.0 // indirect github.com/ncw/swift v1.0.53 // indirect - github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics v0.142.0 // indirect - github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.142.0 // indirect - github.com/open-telemetry/opentelemetry-collector-contrib/processor/deltatocumulativeprocessor v0.142.0 // indirect + github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics v0.154.0 // indirect + github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.154.0 // indirect + github.com/open-telemetry/opentelemetry-collector-contrib/processor/deltatocumulativeprocessor v0.154.0 // indirect github.com/opentracing-contrib/go-grpc v0.1.2 // indirect github.com/opentracing-contrib/go-stdlib v1.1.0 // indirect github.com/oracle/oci-go-sdk/v65 v65.93.1 // indirect + github.com/pb33f/jsonpath v0.8.2 // indirect + github.com/pb33f/libopenapi v0.37.2 // indirect + github.com/pb33f/ordered-map/v2 v2.3.1 // indirect github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/client_golang/exp v0.0.0-20251212205219-7ba246a648ca // indirect + github.com/prometheus/client_golang/exp v0.0.0-20260602051030-3537b20ac86b // indirect github.com/prometheus/procfs v0.16.1 // indirect - github.com/prometheus/sigv4 v0.3.0 // indirect - github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect + github.com/prometheus/sigv4 v0.4.1 // indirect + github.com/puzpuzpuz/xsync/v4 v4.5.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rs/xid v1.6.0 // indirect github.com/santhosh-tekuri/jsonschema v1.2.4 // indirect @@ -280,43 +284,52 @@ require ( github.com/tinylib/msgp v1.3.0 // indirect github.com/uber/jaeger-lib v2.4.1+incompatible // indirect github.com/weaveworks/promrus v1.2.0 // indirect + github.com/x448/float16 v0.8.4 // indirect github.com/xhit/go-str2duration/v2 v2.1.0 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect github.com/yuin/gopher-lua v1.1.1 // indirect go.elastic.co/apm/module/apmhttp v1.15.0 // indirect go.elastic.co/fastjson v1.5.1 // indirect - go.mongodb.org/mongo-driver v1.17.6 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/collector/component v1.48.0 // indirect - go.opentelemetry.io/collector/confmap v1.48.0 // indirect - go.opentelemetry.io/collector/confmap/xconfmap v0.142.0 // indirect - go.opentelemetry.io/collector/consumer v1.48.0 // indirect - go.opentelemetry.io/collector/featuregate v1.48.0 // indirect - go.opentelemetry.io/collector/pipeline v1.48.0 // indirect - go.opentelemetry.io/collector/processor v1.48.0 // indirect + go.opentelemetry.io/collector/component v1.60.0 // indirect + go.opentelemetry.io/collector/confmap v1.60.0 // indirect + go.opentelemetry.io/collector/confmap/xconfmap v0.154.0 // indirect + go.opentelemetry.io/collector/consumer v1.60.0 // indirect + go.opentelemetry.io/collector/featuregate v1.60.0 // indirect + go.opentelemetry.io/collector/internal/componentalias v0.154.0 // indirect + go.opentelemetry.io/collector/pipeline v1.60.0 // indirect + go.opentelemetry.io/collector/processor v1.60.0 // indirect go.opentelemetry.io/contrib/detectors/gcp v1.42.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.64.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.69.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 // indirect go.opentelemetry.io/contrib/propagators/aws v1.36.0 // indirect go.opentelemetry.io/contrib/propagators/b3 v1.36.0 // indirect go.opentelemetry.io/contrib/propagators/jaeger v1.36.0 // indirect - go.opentelemetry.io/otel/metric v1.43.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect + go.opentelemetry.io/otel/metric v1.44.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.27.1 // indirect - go.yaml.in/yaml/v2 v2.4.3 // indirect + go.uber.org/zap v1.28.0 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/exp v0.0.0-20250808145144-a408d31f581a // indirect + go.yaml.in/yaml/v4 v4.0.0-rc.5 // indirect + golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect golang.org/x/mod v0.36.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sys v0.46.0 // indirect golang.org/x/telemetry v0.0.0-20260508192327-42602be52be6 // indirect + golang.org/x/term v0.44.0 // indirect golang.org/x/tools v0.45.0 // indirect gonum.org/v1/gonum v0.17.0 // indirect - google.golang.org/api v0.257.0 // indirect - google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect + google.golang.org/api v0.278.0 // indirect + google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect howett.net/plist v1.0.1 // indirect + k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect zenhack.net/go/util v0.0.0-20230414204917-531d38494cf5 // indirect ) @@ -328,9 +341,15 @@ replace ( // Required by Cortex https://github.com/cortexproject/cortex/pull/3051. github.com/bradfitz/gomemcache => github.com/themihai/gomemcache v0.0.0-20180902122335-24332e2d58ab + github.com/prometheus/prometheus => github.com/Invoca/prometheus v1.8.2-0.20260709045841-ec2ecb44fa29 + // Pin kuberesolver/v5 to support new grpc version. Need to upgrade kuberesolver version on weaveworks/common. github.com/sercand/kuberesolver/v4 => github.com/sercand/kuberesolver/v5 v5.1.1 + // Patched for Prometheus 3.13 BucketQuantile / histogram_quantile annotation API. + // Drop once upstream promql-engine catches up. + github.com/thanos-io/promql-engine => ./internal/promql-engine + github.com/vimeo/galaxycache => github.com/thanos-community/galaxycache v0.0.0-20211122094458-3a32041a1f1e // Overriding to use latest commit. diff --git a/go.sum b/go.sum index 1bbf3c207b1..3bf2a81c9b6 100644 --- a/go.sum +++ b/go.sum @@ -40,8 +40,8 @@ cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRY cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM= cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I= cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY= -cloud.google.com/go v0.120.0 h1:wc6bgG9DHyKqF5/vQvX1CiZrtHnxJjBlKUyF9nP6meA= -cloud.google.com/go v0.120.0/go.mod h1:/beW32s8/pGRuj4IILWQNd4uuebeT4dkOhKmkfit64Q= +cloud.google.com/go v0.121.6 h1:waZiuajrI28iAf40cWgycWNgaXPO06dupuS+sgibK6c= +cloud.google.com/go v0.121.6/go.mod h1:coChdst4Ea5vUpiALcYKXEpR1S9ZgXbhEzzMcMR66vI= cloud.google.com/go/accessapproval v1.4.0/go.mod h1:zybIuC3KpDOvotz59lFe5qxRZx6C75OtwbisN56xYB4= cloud.google.com/go/accessapproval v1.5.0/go.mod h1:HFy3tuiGvMdcd/u+Cu5b9NkO1pEICJ46IR82PoUdplw= cloud.google.com/go/accessapproval v1.6.0/go.mod h1:R0EiYnwV5fsRFiKZkPHr6mwyk2wxUJ30nL4j2pcFY2E= @@ -88,8 +88,8 @@ cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVo cloud.google.com/go/assuredworkloads v1.8.0/go.mod h1:AsX2cqyNCOvEQC8RMPnoc0yEarXQk6WEKkxYfL6kGIo= cloud.google.com/go/assuredworkloads v1.9.0/go.mod h1:kFuI1P78bplYtT77Tb1hi0FMxM0vVpRC7VVoJC3ZoT0= cloud.google.com/go/assuredworkloads v1.10.0/go.mod h1:kwdUQuXcedVdsIaKgKTp9t0UJkE5+PAVNhdQm4ZVq2E= -cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4= -cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ= +cloud.google.com/go/auth v0.20.0 h1:kXTssoVb4azsVDoUiF8KvxAqrsQcQtB53DcSgta74CA= +cloud.google.com/go/auth v0.20.0/go.mod h1:942/yi/itH1SsmpyrbnTMDgGfdy2BUqIKyd0cyYLc5Q= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0= @@ -277,8 +277,8 @@ cloud.google.com/go/iam v0.7.0/go.mod h1:H5Br8wRaDGNc8XP3keLc4unfUUZeyH3Sfl9XpQE cloud.google.com/go/iam v0.8.0/go.mod h1:lga0/y3iH6CX7sYqypWJ33hf7kkfXJag67naqGESjkE= cloud.google.com/go/iam v0.11.0/go.mod h1:9PiLDanza5D+oWFZiH1uG+RnRCfEGKoyl6yo4cgWZGY= cloud.google.com/go/iam v0.12.0/go.mod h1:knyHGviacl11zrtZUoDuYpDgLjvr28sLQaG0YB2GYAY= -cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8= -cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE= +cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc= +cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU= cloud.google.com/go/iap v1.4.0/go.mod h1:RGFwRJdihTINIe4wZ2iCP0zF/qu18ZwyKxrhMhygBEc= cloud.google.com/go/iap v1.5.0/go.mod h1:UH/CGgKd4KyohZL5Pt0jSKE4m3FR51qg6FKQ/z/Ix9A= cloud.google.com/go/iap v1.6.0/go.mod h1:NSuvI9C/j7UdjGjIde7t7HBz+QTwBcapPE07+sSRcLk= @@ -302,13 +302,13 @@ cloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6 cloud.google.com/go/lifesciences v0.8.0/go.mod h1:lFxiEOMqII6XggGbOnKiyZ7IBwoIqA84ClvoezaA/bo= cloud.google.com/go/logging v1.6.1/go.mod h1:5ZO0mHHbvm8gEmeEUHrmDlTDSu5imF6MUP9OfilNXBw= cloud.google.com/go/logging v1.7.0/go.mod h1:3xjP2CjkM3ZkO73aj4ASA5wRPGGCRrPIAeNqVNkzY8M= -cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc= -cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA= +cloud.google.com/go/logging v1.13.2 h1:qqlHCBvieJT9Cdq4QqYx1KPadCQ2noD4FK02eNqHAjA= +cloud.google.com/go/logging v1.13.2/go.mod h1:zaybliM3yun1J8mU2dVQ1/qDzjbOqEijZCn6hSBtKak= cloud.google.com/go/longrunning v0.1.1/go.mod h1:UUFxuDWkv22EuY93jjmDMFT5GPQKeFVJBIF6QlTqdsE= cloud.google.com/go/longrunning v0.3.0/go.mod h1:qth9Y41RRSUE69rDcOn6DdK3HfQfsUI0YSmW3iIlLJc= cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo= -cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE= -cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY= +cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8= +cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk= cloud.google.com/go/managedidentities v1.3.0/go.mod h1:UzlW3cBOiPrzucO5qWkNkh0w33KFtBJU281hacNvsdE= cloud.google.com/go/managedidentities v1.4.0/go.mod h1:NWSBYbEMgqmbZsLIyKvxrYbtqOsxY1ZrGM+9RgDqInM= cloud.google.com/go/managedidentities v1.5.0/go.mod h1:+dWcZ0JlUmpuxpIDfyP5pP5y0bLdRwOS4Lp7gMni/LA= @@ -330,8 +330,8 @@ cloud.google.com/go/metastore v1.10.0/go.mod h1:fPEnH3g4JJAk+gMRnrAnoqyv2lpUCqJP cloud.google.com/go/monitoring v1.7.0/go.mod h1:HpYse6kkGo//7p6sT0wsIC6IBDET0RhIsnmlA53dvEk= cloud.google.com/go/monitoring v1.8.0/go.mod h1:E7PtoMJ1kQXWxPjB6mv2fhC5/15jInuulFdYYtlcvT4= cloud.google.com/go/monitoring v1.12.0/go.mod h1:yx8Jj2fZNEkL/GYZyTLS4ZtZEZN8WtDEiEqG4kLK50w= -cloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM= -cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U= +cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE= +cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI= cloud.google.com/go/networkconnectivity v1.4.0/go.mod h1:nOl7YL8odKyAOtzNX73/M5/mGZgqqMeryi6UPZTk/rA= cloud.google.com/go/networkconnectivity v1.5.0/go.mod h1:3GzqJx7uhtlM3kln0+x5wyFvuVH1pIBJjhCpjzSt75o= cloud.google.com/go/networkconnectivity v1.6.0/go.mod h1:OJOoEXW+0LAxHh89nXd64uGG+FbQoeH8DtxCHVOMlaM= @@ -473,8 +473,8 @@ cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc= cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s= cloud.google.com/go/storage v1.28.1/go.mod h1:Qnisd4CqDdo6BGs2AD5LLnEsmSQ80wQ5ogcBBKhU86Y= -cloud.google.com/go/storage v1.50.0 h1:3TbVkzTooBvnZsk7WaAQfOsNrdoM8QHusXA1cpk6QJs= -cloud.google.com/go/storage v1.50.0/go.mod h1:l7XeiD//vx5lfqE3RavfmU9yvk5Pp0Zhcv482poyafY= +cloud.google.com/go/storage v1.56.0 h1:iixmq2Fse2tqxMbWhLWC9HfBj1qdxqAmiK8/eqtsLxI= +cloud.google.com/go/storage v1.56.0/go.mod h1:Tpuj6t4NweCLzlNbw9Z9iwxEkrSem20AetIeH/shgVU= cloud.google.com/go/storagetransfer v1.5.0/go.mod h1:dxNzUopWy7RQevYFHewchb29POFv3/AaBgnhqzqiK0w= cloud.google.com/go/storagetransfer v1.6.0/go.mod h1:y77xm4CQV/ZhFZH75PLEXY0ROiS7Gh6pSKrM8dJyg6I= cloud.google.com/go/storagetransfer v1.7.0/go.mod h1:8Giuj1QNb1kfLAiWM1bN6dHzfdlDAVC9rv9abHot2W4= @@ -492,8 +492,8 @@ cloud.google.com/go/tpu v1.5.0/go.mod h1:8zVo1rYDFuW2l4yZVY0R0fb/v44xLh3llq7RuV6 cloud.google.com/go/trace v1.3.0/go.mod h1:FFUE83d9Ca57C+K8rDl/Ih8LwOzWIV1krKgxg6N0G28= cloud.google.com/go/trace v1.4.0/go.mod h1:UG0v8UBqzusp+z63o7FK74SdFE+AXpCLdFb1rshXG+Y= cloud.google.com/go/trace v1.8.0/go.mod h1:zH7vcsbAhklH8hWFig58HvxcxyQbaIqMarMg9hn5ECA= -cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4= -cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI= +cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U= +cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s= cloud.google.com/go/translate v1.3.0/go.mod h1:gzMUwRjvOqj5i69y/LYLd8RrNQk+hOmIXTi9+nb3Djs= cloud.google.com/go/translate v1.4.0/go.mod h1:06Dn/ppvLD6WvA5Rhdp029IX2Mi3Mn7fpMRLPvXT5Wg= cloud.google.com/go/translate v1.6.0/go.mod h1:lMGRudH1pu7I3n3PETiOB2507gf3HnfLV8qlkHZEyos= @@ -535,14 +535,14 @@ cloud.google.com/go/workflows v1.10.0/go.mod h1:fZ8LmRmZQWacon9UCX1r/g/DfAXx5VcP dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 h1:JXg2dwJUmPB9JmtVmdEB16APJ7jurfbY5jnfXpJoRMc= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0= -github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= -github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0 h1:aokoqcHvaGjiM3VpjKDfMMnF/8epJ+Q1HLJ7CudztqE= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0/go.mod h1:/WYEx9pcM9Y+Dd/APJaNlSvVSvzl54rrMdZT5+Oi2LM= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.0 h1:CU4+EJeJi3TKYWEcYuSdWsjzw0nVsK/H0MSQOiPcymU= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.0/go.mod h1:q0+UTSRvShwUCrR/s5HtyInYphN7Wvxb7snFM3u+SLA= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.4.0 h1:xFaZZ+IubdftrDHnGGwZ6QvQ3KHTtWl2MCK+GMt2vxs= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.4.0/go.mod h1:mCBhUhlMjLLJKr5aqw2TNS/VqJOie8MzWq3DAMJeKso= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 h1:fhqpLE3UEXi9lPaBRpQ6XuRW0nU7hgg4zlmZZa+a9q4= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0/go.mod h1:7dCRMLwisfRH3dBupKeNCioWYUZ4SS09Z14H+7i8ZoY= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5 v5.7.0 h1:LkHbJbgF3YyvC53aqYGR+wWQDn2Rdp9AQdGndf9QvY4= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5 v5.7.0/go.mod h1:QyiQdW4f4/BIfB8ZutZ2s+28RAgfa/pT+zS++ZHyM1I= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v4 v4.3.0 h1:bXwSugBiSbgtz7rOtbfGf+woewp4f06orW9OP5BjHLA= @@ -553,8 +553,8 @@ github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.1 h1:lhZdRq7TIx0GJQvSy github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.1/go.mod h1:8cl44BDmi+effbARHMQjgOKA2AYvcohNm7KEt42mSV8= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= -github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= -github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= +github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2 h1:RHK7bS+HQMslb1sZpAokUt+zTVmue0hKSs2C791hhzU= +github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/Code-Hex/go-generics-cache v1.5.1 h1:6vhZGc5M7Y/YD8cIUcY8kcuQLB4cHR7U+0KMqAA0KcU= @@ -562,19 +562,23 @@ github.com/Code-Hex/go-generics-cache v1.5.1/go.mod h1:qxcC9kRVrct9rHeiYpFWSoW1v github.com/DATA-DOG/go-sqlmock v1.4.1/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.50.0 h1:5IT7xOdq17MtcdtL/vtl6mGfzhaq4m4vpollPRmlsBQ= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.50.0/go.mod h1:ZV4VOm0/eHR06JLrXWe09068dHpr3TRpY9Uo7T+anuA= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 h1:owcC2UnmsZycprQ5RfRgjydWhuoxg71LUfyiQdijZuM= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0/go.mod h1:ZPpqegjbE99EPKsu3iUWV22A04wzGPcAY/ziSIQEEgs= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.27.0 h1:Jtr816GUk6+I2ox9L/v+VcOwN6IyGOEDTSNHfD6m9sY= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.27.0/go.mod h1:E05RN++yLx9W4fXPtX978OLo9P0+fBacauUdET1BckA= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0 h1:OqVGm6Ei3x5+yZmSJG1Mh2NwHvpVmZ08CB5qJhT9Nuk= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0/go.mod h1:SZiPHWGOOk3bl8tkevxkoiwPgsIl6CwrWcbwjfHZpdM= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.52.0 h1:wbMd4eG/fOhsCa6+IP8uEDvWF5vl7rNoUWmP5f72Tbs= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.52.0/go.mod h1:gdIm9TxRk5soClCwuB0FtdXsbqtw0aqPwBEurK9tPkw= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0 h1:4LP6hvB4I5ouTbGgWtixJhgED6xdf67twf9PoY96Tbg= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0/go.mod h1:jUZ5LYlw40WMd07qxcQJD5M40aUxrfwqQX1g7zxYnrQ= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 h1:Ron4zCA/yk6U7WOBXhTJcDpsUBG9npumK6xw2auFltQ= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0/go.mod h1:cSgYe11MCNYunTnRXrKiR/tHc0eoKjICUuWpNZoVCOo= github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM= github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= +github.com/Invoca/prometheus v1.8.2-0.20260709045841-ec2ecb44fa29 h1:KOsHG4EK1pqjd3OfdGCnRXmO19BW0qZLRiu9t6E+gFY= +github.com/Invoca/prometheus v1.8.2-0.20260709045841-ec2ecb44fa29/go.mod h1:Kq9A+EPun2WyVusbQxO7Tx1RxKqLKFclfiBGJA1mFkk= github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk= github.com/KimMachineGun/automemlimit v0.7.5 h1:RkbaC0MwhjL1ZuBKunGDjE/ggwAX43DwZrJqVwyveTk= github.com/KimMachineGun/automemlimit v0.7.5/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= @@ -611,42 +615,54 @@ github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4= -github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= -github.com/aws/aws-sdk-go-v2/config v1.32.6 h1:hFLBGUKjmLAekvi1evLi5hVvFQtSo3GYwi+Bx4lpJf8= -github.com/aws/aws-sdk-go-v2/config v1.32.6/go.mod h1:lcUL/gcd8WyjCrMnxez5OXkO3/rwcNmvfno62tnXNcI= -github.com/aws/aws-sdk-go-v2/credentials v1.19.6 h1:F9vWao2TwjV2MyiyVS+duza0NIRtAslgLUM0vTA1ZaE= -github.com/aws/aws-sdk-go-v2/credentials v1.19.6/go.mod h1:SgHzKjEVsdQr6Opor0ihgWtkWdfRAIwxYzSJ8O85VHY= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 h1:80+uETIWS1BqjnN9uJ0dBUaETh+P1XwFy5vwHwK5r9k= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16/go.mod h1:wOOsYuxYuB/7FlnVtzeBYRcjSRtQpAW0hCP7tIULMwo= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 h1:rgGwPzb82iBYSvHMHXc8h9mRoOUBZIGFgKb9qniaZZc= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16/go.mod h1:L/UxsGeKpGoIj6DxfhOWHWQ/kGKcd4I1VncE4++IyKA= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 h1:1jtGzuV7c82xnqOVfx2F0xmJcOw5374L7N6juGW6x6U= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16/go.mod h1:M2E5OQf+XLe+SZGmmpaI2yy+J326aFf6/+54PoxSANc= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= -github.com/aws/aws-sdk-go-v2/service/ec2 v1.277.0 h1:RHJSkRXDGkAKrV4CTEsZsZkOmSpxXKO4aKx4rXd94K4= -github.com/aws/aws-sdk-go-v2/service/ec2 v1.277.0/go.mod h1:Wg68QRgy2gEGGdmTPU/UbVpdv8sM14bUZmF64KFwAsY= -github.com/aws/aws-sdk-go-v2/service/ecs v1.69.5 h1:5nkhwt0d/gjuT3AQ2LUK0aFRNB3MGlzB2elqy/ZsKP4= -github.com/aws/aws-sdk-go-v2/service/ecs v1.69.5/go.mod h1:LQMlcWBoiFVD3vUVEz42ST0yTiaDujv2dRE6sXt1yPE= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 h1:oHjJHeUy0ImIV0bsrX0X91GkV5nJAyv1l1CC9lnO0TI= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16/go.mod h1:iRSNGgOYmiYwSCXxXaKb9HfOEj40+oTKn8pTxMlYkRM= -github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.10 h1:MQuZZ6Tq1qQabPlkVxrCMdyVl70Ogl4AERZKo+y9Wzo= -github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.10/go.mod h1:U5C3JME1ibKESmpzBAqlRpTYZfVbTqrb5ICJm+sVVd8= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 h1:HpI7aMmJ+mm1wkSHIA2t5EaFFv5EFYXePW30p1EIrbQ= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.4/go.mod h1:C5RdGMYGlfM0gYq/tifqgn4EbyX99V15P2V3R+VHbQU= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 h1:aM/Q24rIlS3bRAhTyFurowU8A0SMyGDtEOY/l/s/1Uw= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.8/go.mod h1:+fWt2UHSb4kS7Pu8y+BMBvJF0EWx+4H0hzNwtDNRTrg= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 h1:AHDr0DaHIAo8c9t1emrzAlVDFp+iMMKnPdYy6XO4MCE= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12/go.mod h1:GQ73XawFFiWxyWXMHWfhiomvP3tXtdNar/fi8z18sx0= -github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX1s+lFTg4+4DOy70= -github.com/aws/aws-sdk-go-v2/service/sts v1.41.5/go.mod h1:iW40X4QBmUxdP+fZNOpfmkdMZqsovezbAeO+Ubiv2pk= -github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk= -github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= +github.com/aws/aws-sdk-go v1.55.8 h1:JRmEUbU52aJQZ2AjX4q4Wu7t4uZjOu71uyNmaWlUkJQ= +github.com/aws/aws-sdk-go v1.55.8/go.mod h1:ZkViS9AqA6otK+JBBNH2++sx1sgxrPKcSzPPvQkUtXk= +github.com/aws/aws-sdk-go-v2 v1.42.0 h1:XvXMJTkFQtpBKIWZnmr9ZEOc2InWM2yldjXEJ/bymhA= +github.com/aws/aws-sdk-go-v2 v1.42.0/go.mod h1:27+ACypSLljLAEKsCYOmrjKh83vuTRkuAe9Uv/3A4bg= +github.com/aws/aws-sdk-go-v2/config v1.32.25 h1:ACCejvStYoilgwrfegSt5ZntCbPrk52qfwyNcnl3omM= +github.com/aws/aws-sdk-go-v2/config v1.32.25/go.mod h1:LJyU8sDRbXUxFn8xMJIGP+v9QYYwveNLI8a/giAOiAs= +github.com/aws/aws-sdk-go-v2/credentials v1.19.24 h1:2hQqYCV9yqyePQ9o6dCrZc/zO8U3TwPr9mIKlZnPu/I= +github.com/aws/aws-sdk-go-v2/credentials v1.19.24/go.mod h1:IDwpACtwqHLISdzfwUUNq4P9DsB/h5BLg4FwJPNfqFY= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29 h1:r6qZHbT+wxgWO/e9vYNUEtg7lv5+UN3pRqKhLXvnArg= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29/go.mod h1:QRnaRcTVGKPGRy8w78HMQtKUGRYcnMZAANATkeVA6Mo= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 h1:f3vKqSo13fhTYb+JEcXwXefZQE26I1FB5eTSniU67ko= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29/go.mod h1:MzoLFUArKGpGD+ukmPiTPG1X5x4o6M2kq4v2dr1FiEc= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29 h1:RdwIf/CuUsvJX3RgJagbOyotl/cxoLY4xviKuE7p2GY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29/go.mod h1:71wt8W2EgswdZy9Mf9KNnzxZ3TiZlv4caKghPktDOkA= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30 h1:VTGy885W5DKBxWRUJbym9hytNaYzsyaPkCHGRRMAOhU= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30/go.mod h1:AS0HycUvJRFvTt613AYDOgO2jzw+00cVSMny8XB3yMY= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.307.0 h1:ZQMhFWDFhwJbq3xCggO0gh3AW+yu65QtcT9F5HfdZhY= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.307.0/go.mod h1:8mrDF7OtbuL0QpwP4YCvLuoOE4/5lL7D33MXgp069/Y= +github.com/aws/aws-sdk-go-v2/service/ecs v1.83.0 h1:LQKIHuVHqdbU9LUt5c2G9f+CcQAzolxQmAch3RTORMc= +github.com/aws/aws-sdk-go-v2/service/ecs v1.83.0/go.mod h1:0vahPCh3slyORHbSuAP8YDyJKLEUQAMX7+bzYGxEnVI= +github.com/aws/aws-sdk-go-v2/service/elasticache v1.54.3 h1:KZDlMf8V5riU8xBCMJLWhfa+RP/MIagz2qJFwRg/b1g= +github.com/aws/aws-sdk-go-v2/service/elasticache v1.54.3/go.mod h1:nsMdHtF/ned4F5GCAfoerJaa/Q6cx+G+WYNsb/TFN7Q= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12 h1:ZD2+BSw9vFsNlKYIasSNt3uDbjqqXIBcM13UJv/Lx2k= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12/go.mod h1:Ms4zlcVBbXbiP7EVLhl+lgjvA/a7YphqQ3Ih3174EmI= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29 h1:DRebniUGZ2MqiiIVmQJ04vIXr918hubdHMnarSLEWyU= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29/go.mod h1:LfRkPCD8YHDM2E5eTkos2UpwYeZnBcVarTa8L59bJHA= +github.com/aws/aws-sdk-go-v2/service/kafka v1.52.6 h1:1Cn7pNj5Knye9dx2KFY0UmSdXM+DZdzQaeBx72QHgSQ= +github.com/aws/aws-sdk-go-v2/service/kafka v1.52.6/go.mod h1:5SCWP3gW59x0gRYHuwzXoj/ZuxEoa+j9/OeynrJd/sk= +github.com/aws/aws-sdk-go-v2/service/lightsail v1.56.1 h1:bbOZEcMgnUQocfDoaaU2f148Te/MpUk6FkOGtJyfwlg= +github.com/aws/aws-sdk-go-v2/service/lightsail v1.56.1/go.mod h1:428ttHou5n2J4/oQAQS9EmOU6LrBv48F2bGk+Ta7EF4= +github.com/aws/aws-sdk-go-v2/service/rds v1.119.3 h1:SIGdk+wA+xGXgN+L7Jr3Ot83Mjh3jpjyJIwZd3DqAnU= +github.com/aws/aws-sdk-go-v2/service/rds v1.119.3/go.mod h1:zCRPUdp05FEZG3OO7LmJq9xkSDjMEhkiVrZV0oJs2a0= +github.com/aws/aws-sdk-go-v2/service/signin v1.2.0 h1:3nXpRcFwRCW8n7HgO2QGy0Dc20eQNfBuUemGQhpF8m8= +github.com/aws/aws-sdk-go-v2/service/signin v1.2.0/go.mod h1:LxYujSTLPRlp2vTtcUO/+1ilrew8ytt6SvQyOgejzFQ= +github.com/aws/aws-sdk-go-v2/service/sso v1.31.3 h1:ey1XLTYXb9PcLt4535632o5kCGXNXEhNb620Dqwuylo= +github.com/aws/aws-sdk-go-v2/service/sso v1.31.3/go.mod h1:Lk7PlmoTYryQmyBG0EXqj5BcUbj3whXdU2s3yGI3EAc= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6 h1:yLr03zQE/5Eu5l3QU0Si+xMbLMbSDF2YXsigqXngs6g= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6/go.mod h1:Q5N6icH+KJZDLh+ESNwzdv6cZ6vLFF/egy3IOxWhmz4= +github.com/aws/aws-sdk-go-v2/service/sts v1.43.3 h1:VrIhKRCSK1umelSgB9RghvA9RTUYeQffyAS5ApXehNI= +github.com/aws/aws-sdk-go-v2/service/sts v1.43.3/go.mod h1:r8wkDOuLaaMFqFiYAb8dGY2A3gJCOujMc6CFOVC4Zhc= +github.com/aws/smithy-go v1.27.2 h1:y9NPmSE6am6LjEFPfqHqG/jJk7AauQvhCJONKh7kpzk= +github.com/aws/smithy-go v1.27.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/baidubce/bce-sdk-go v0.9.230 h1:HzELBKiD7QAgYqZ1qHZexoI2A3Lo/6zYGQFvcUbS5cA= github.com/baidubce/bce-sdk-go v0.9.230/go.mod h1:zbYJMQwE4IZuyrJiFO8tO8NbtYiKTFTbwh4eIsqjVdg= +github.com/basgys/goxml2json v1.1.1-0.20231018121955-e66ee54ceaad h1:3swAvbzgfaI6nKuDDU7BiKfZRdF+h2ZwKgMHd8Ha4t8= +github.com/basgys/goxml2json v1.1.1-0.20231018121955-e66ee54ceaad/go.mod h1:9+nBLYNWkvPcq9ep0owWUsPTLgL9ZXTsZWcCSVGGLJ0= github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 h1:6df1vn4bBlDDo4tARvBm7l6KA9iVMnE3NWizDeWSrps= github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3/go.mod h1:CIWtjkly68+yqLPbvwwR/fjNJA/idrtULjZWh2v1ys0= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= @@ -659,6 +675,8 @@ github.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw= github.com/bluele/gcache v0.0.2/go.mod h1:m15KV+ECjptwSPxKhOhQoAFQVtUFjTVkc3H8o0t/fp0= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk= +github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/caio/go-tdigest v3.1.0+incompatible h1:uoVMJ3Q5lXmVLCCqaMGHLBWnbGoN6Lpu7OAUPR60cds= github.com/caio/go-tdigest v3.1.0+incompatible/go.mod h1:sHQM/ubZStBUmF1WbB8FAm8q9GjDajLC5T7ydxE3JHI= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= @@ -708,8 +726,8 @@ github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/coreos/go-systemd/v22 v22.4.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo= -github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU= +github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA= +github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w= github.com/cortexproject/promqlsmith v0.0.0-20250407233056-90db95b1a4e4 h1:dpo7kQ24uFSV6Zgm9/kB34TIUWjGmadlbKrM6fNfQko= github.com/cortexproject/promqlsmith v0.0.0-20250407233056-90db95b1a4e4/go.mod h1:jh6POgN18lXU133HBMfwr/1TjvBp8e5kL4ZtRsAPvGY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -724,22 +742,20 @@ github.com/dennwc/varint v1.0.0/go.mod h1:hnItb35rvZvJrbTALZtY/iQfDs48JKRG1RPpgz github.com/dgryski/go-metro v0.0.0-20200812162917-85c65e2d0165/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw= github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 h1:ucRHb6/lvW/+mTEIGbvhcYU3S8+uSNkuMjx/qZFfhtM= github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw= -github.com/digitalocean/godo v1.171.0 h1:QwpkwWKr3v7yxc8D4NQG973NoR9APCEWjYnLOQeXVpQ= -github.com/digitalocean/godo v1.171.0/go.mod h1:xQsWpVCCbkDrWisHA72hPzPlnC+4W5w/McZY5ij9uvU= -github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= -github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= -github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= -github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/digitalocean/godo v1.196.0 h1:32bkla5iESoGaCHmXD2+fUXAepR23wWwbzPjwenIhik= +github.com/digitalocean/godo v1.196.0/go.mod h1:xQsWpVCCbkDrWisHA72hPzPlnC+4W5w/McZY5ij9uvU= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= +github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/edsrzf/mmap-go v1.2.0 h1:hXLYlkbaPzt1SaQk+anYwKSRNhufIDCchSPkUD6dD84= -github.com/edsrzf/mmap-go v1.2.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q= +github.com/edsrzf/mmap-go v1.2.1-0.20241212181136-fad1cd13edbd h1:I4PrRZuNMeDP3VbFrak4QsqwO5tWkQf0tqrrr1L2DsU= +github.com/edsrzf/mmap-go v1.2.1-0.20241212181136-fad1cd13edbd/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q= github.com/efficientgo/core v1.0.0-rc.3 h1:X6CdgycYWDcbYiJr1H1+lQGzx13o7bq3EUkbB9DsSPc= github.com/efficientgo/core v1.0.0-rc.3/go.mod h1:FfGdkzWarkuzOlY04VY+bGfb1lWrjaL6x/GLcQ4vJps= github.com/efficientgo/e2e v0.14.1-0.20260204162810-8c75b1e33ef9 h1:ubLDovsdOv7n27IV6J+ITPW+VjkbJs/DfAbVqfIFkzQ= @@ -798,8 +814,8 @@ github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHqu github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= -github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= -github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= +github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= github.com/fullstorydev/emulators/storage v0.0.0-20240401123056-edc69752f474 h1:TufioMBjkJ6/Oqmlye/ReuxHFS35HyLmypj/BNy/8GY= github.com/fullstorydev/emulators/storage v0.0.0-20240401123056-edc69752f474/go.mod h1:PQwxF4UU8wuL+srGxr3BOhIW5zXqgucwVlO/nPZLsxw= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= @@ -836,65 +852,67 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-openapi/analysis v0.24.1 h1:Xp+7Yn/KOnVWYG8d+hPksOYnCYImE3TieBa7rBOesYM= -github.com/go-openapi/analysis v0.24.1/go.mod h1:dU+qxX7QGU1rl7IYhBC8bIfmWQdX4Buoea4TGtxXY84= -github.com/go-openapi/errors v0.22.4 h1:oi2K9mHTOb5DPW2Zjdzs/NIvwi2N3fARKaTJLdNabaM= -github.com/go-openapi/errors v0.22.4/go.mod h1:z9S8ASTUqx7+CP1Q8dD8ewGH/1JWFFLX/2PmAYNQLgk= -github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk= -github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM= -github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc= -github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4= -github.com/go-openapi/loads v0.23.2 h1:rJXAcP7g1+lWyBHC7iTY+WAF0rprtM+pm8Jxv1uQJp4= -github.com/go-openapi/loads v0.23.2/go.mod h1:IEVw1GfRt/P2Pplkelxzj9BYFajiWOtY2nHZNj4UnWY= -github.com/go-openapi/runtime v0.29.2 h1:UmwSGWNmWQqKm1c2MGgXVpC2FTGwPDQeUsBMufc5Yj0= -github.com/go-openapi/runtime v0.29.2/go.mod h1:biq5kJXRJKBJxTDJXAa00DOTa/anflQPhT0/wmjuy+0= -github.com/go-openapi/spec v0.22.1 h1:beZMa5AVQzRspNjvhe5aG1/XyBSMeX1eEOs7dMoXh/k= -github.com/go-openapi/spec v0.22.1/go.mod h1:c7aeIQT175dVowfp7FeCvXXnjN/MrpaONStibD2WtDA= -github.com/go-openapi/strfmt v0.25.0 h1:7R0RX7mbKLa9EYCTHRcCuIPcaqlyQiWNPTXwClK0saQ= -github.com/go-openapi/strfmt v0.25.0/go.mod h1:nNXct7OzbwrMY9+5tLX4I21pzcmE6ccMGXl3jFdPfn8= -github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU= -github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ= -github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4= -github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= -github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= -github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= -github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y= -github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk= -github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= -github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= -github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= -github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM= -github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= -github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= -github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48= -github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg= -github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0= -github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg= -github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= -github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= -github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= -github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= -github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= -github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= -github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4= -github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= -github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= -github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= -github.com/go-openapi/validate v0.25.1 h1:sSACUI6Jcnbo5IWqbYHgjibrhhmt3vR6lCzKZnmAgBw= -github.com/go-openapi/validate v0.25.1/go.mod h1:RMVyVFYte0gbSTaZ0N4KmTn6u/kClvAFp+mAVfS/DQc= +github.com/go-openapi/analysis v0.25.2 h1:I0vy4n3alz+DHTiN1PRhCb7QZxkK6g5YmswZKv2TKuw= +github.com/go-openapi/analysis v0.25.2/go.mod h1:Uhs1t/2XR10EnwONYILGEzw8gcfGIG5Xk5K2AxnhqDo= +github.com/go-openapi/errors v0.22.7 h1:JLFBGC0Apwdzw3484MmBqspjPbwa2SHvpDm0u5aGhUA= +github.com/go-openapi/errors v0.22.7/go.mod h1://QW6SD9OsWtH6gHllUCddOXDL0tk0ZGNYHwsw4sW3w= +github.com/go-openapi/jsonpointer v0.23.1 h1:1HBACs7XIwR2RcmItfdSFlALhGbe6S92p0ry4d1GWg4= +github.com/go-openapi/jsonpointer v0.23.1/go.mod h1:iWRmZTrGn7XwYhtPt/fvdSFj1OfNBngqRT2UG3BxSqY= +github.com/go-openapi/jsonreference v0.21.6 h1:NZ5nGfnaM1n4I43Xjm1e5/M2GjOwQwndQz22uhxwD+Y= +github.com/go-openapi/jsonreference v0.21.6/go.mod h1:xzbgtQ3ZbWxvET3AxdzCJlJt6vkovbf+IfSPJjD0tUY= +github.com/go-openapi/loads v0.23.3 h1:g5Xap1JfwKkUnZdn+S0L3SzBDpcTIYzZ5Qaag0YDkKQ= +github.com/go-openapi/loads v0.23.3/go.mod h1:NOH07zLajXo8y55hom0omlHWDVVvCwBM/S+csCK8LqA= +github.com/go-openapi/runtime v0.32.3 h1:J7Ycy5DJmhhP1By3NifhRUjnkXTrk21qbeqSULjwX8U= +github.com/go-openapi/runtime v0.32.3/go.mod h1:/WTQi0fa5DiGnnCXQKsTkSm15OzJp8Uz3H2t+67TBr4= +github.com/go-openapi/runtime/server-middleware v0.30.0 h1:8rPoJ/xv7JL8BsovaqboKETlpWBArVh8n+0L/GyePog= +github.com/go-openapi/runtime/server-middleware v0.30.0/go.mod h1:OYNT/TxNvB/VK5oe4htM2jDTwlEXuejVJmu0DVZfAMs= +github.com/go-openapi/spec v0.22.5 h1:KhO7RBlKQfonUWX2WzQCoLIXVA6AcNqDGZ3a1Dutdlo= +github.com/go-openapi/spec v0.22.5/go.mod h1:vxpOtMya5TXtENXKE5bKqv5NjocVhyhxHrlZfvKnZ74= +github.com/go-openapi/strfmt v0.26.3 h1:rzmslHarJgBbf2qfGge+X3htclQfmXqBZMm0Too0HhU= +github.com/go-openapi/strfmt v0.26.3/go.mod h1:a5nsUw0oRpQzZeOwx8bi6cKbzFZslpbCKt1LEot+KnQ= +github.com/go-openapi/swag v0.26.0 h1:GVDXCmfvhfu1BxiHo8/FA+BbKmhecHnG3varjON5/RI= +github.com/go-openapi/swag v0.26.0/go.mod h1:82g3193sZJRbocs7bNCqGfIgq8pkuwVwCfhKIRlEQF0= +github.com/go-openapi/swag/cmdutils v0.26.0 h1:iowihOcvq7y4egO8cOq0dmfohz6wfeQ63U1EnuhO2TU= +github.com/go-openapi/swag/cmdutils v0.26.0/go.mod h1:Sm1MVFMkF6guJJ+pQqHnQA3N0j9qALV3NxzDSv6bETM= +github.com/go-openapi/swag/conv v0.26.0 h1:5yGGsPYI1ZCva93U0AoKi/iZrNhaJEjr324YVsiD89I= +github.com/go-openapi/swag/conv v0.26.0/go.mod h1:tpAmIL7X58VPnHHiSO4uE3jBeRamGsFsfdDeDtb5ECE= +github.com/go-openapi/swag/fileutils v0.26.0 h1:WJoPRvsA7QRiiWluowkLJa9jaYR7FCuxmDvnCgaRRxU= +github.com/go-openapi/swag/fileutils v0.26.0/go.mod h1:0WDJ7lp67eNjPMO50wAWYlKvhOb6CQ37rzR7wrgI8Tc= +github.com/go-openapi/swag/jsonname v0.26.0 h1:gV1NFX9M8avo0YSpmWogqfQISigCmpaiNci8cGECU5w= +github.com/go-openapi/swag/jsonname v0.26.0/go.mod h1:urBBR8bZNoDYGr653ynhIx+gTeIz0ARZxHkAPktJK2M= +github.com/go-openapi/swag/jsonutils v0.26.0 h1:FawFML2iAXsPqmERscuMPIHmFsoP1tOqWkxBaKNMsnA= +github.com/go-openapi/swag/jsonutils v0.26.0/go.mod h1:2VmA0CJlyFqgawOaPI9psnjFDqzyivIqLYN34t9p91E= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0 h1:apqeINu/ICHouqiRZbyFvuDge5jCmmLTqGQ9V95EaOM= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0/go.mod h1:AyM6QT8uz5IdKxk5akv0y6u4QvcL9GWERt0Jx/F/R8Y= +github.com/go-openapi/swag/loading v0.26.0 h1:Apg6zaKhCJurpJer0DCxq99qwmhFddBhaMX7kilDcko= +github.com/go-openapi/swag/loading v0.26.0/go.mod h1:dBxQ/6V2uBaAQdevN18VELE6xSpJWZxLX4txe12JwDg= +github.com/go-openapi/swag/mangling v0.26.0 h1:Du2YC4YLA/Y5m/YKQd7AnY5qq0wRKSFZTTt8ktFaXcQ= +github.com/go-openapi/swag/mangling v0.26.0/go.mod h1:jifS7W9vbg+pw63bT+GI53otluMQL3CeemuyCHKwVx0= +github.com/go-openapi/swag/netutils v0.26.0 h1:CmZp+ZT7HrmFwrC3GdGsXBq2+42T1bjKBapcqVpIs3c= +github.com/go-openapi/swag/netutils v0.26.0/go.mod h1:5iK+Ok3ZohWWex1C50BFTPexi03UaPwjW4Oj8kgrpwo= +github.com/go-openapi/swag/stringutils v0.26.0 h1:qZQngLxs5s7SLijc3N2ZO+fUq2o8LjuWAASSrJuh+xg= +github.com/go-openapi/swag/stringutils v0.26.0/go.mod h1:sWn5uY+QIIspwPhvgnqJsH8xqFT2ZbYcvbcFanRyhFE= +github.com/go-openapi/swag/typeutils v0.26.0 h1:2kdEwdiNWy+JJdOvu5MA2IIg2SylWAFuuyQIKYybfq4= +github.com/go-openapi/swag/typeutils v0.26.0/go.mod h1:oovDuIUvTrEHVMqWilQzKzV4YlSKgyZmFh7AlfABNVE= +github.com/go-openapi/swag/yamlutils v0.26.0 h1:H7O8l/8NJJQ/oiReEN+oMpnGMyt8G0hl460nRZxhLMQ= +github.com/go-openapi/swag/yamlutils v0.26.0/go.mod h1:1evKEGAtP37Pkwcc7EWMF0hedX0/x3Rkvei2wtG/TbU= +github.com/go-openapi/testify/enable/yaml/v2 v2.5.1 h1:q9NtHwK4qHF7yZziBPvZyv7zWAIk8ok88Gh2mR6Jpc8= +github.com/go-openapi/testify/enable/yaml/v2 v2.5.1/go.mod h1:JW0MXIotCYps/XsgJnG3a8Q7rE5xAiBwoOD5OfaIQBk= +github.com/go-openapi/testify/v2 v2.5.1 h1:TMdhCaw8fUNraVSf3Omoob1dO/AzBfhtFAPW0an6sBo= +github.com/go-openapi/testify/v2 v2.5.1/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= +github.com/go-openapi/validate v0.25.3 h1:4nzAIavcJ7WveHK2+V1UAkZK3kWcjzxZCzjfZAfavKs= +github.com/go-openapi/validate v0.25.3/go.mod h1:GemfuGMyYpIaBoKpX3z8sLywrmxpzWVOoJ7R0VeAVuk= github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= -github.com/go-resty/resty/v2 v2.17.1 h1:x3aMpHK1YM9e4va/TMDRlusDDoZiQ+ViDu/WpA6xTM4= -github.com/go-resty/resty/v2 v2.17.1/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA= +github.com/go-resty/resty/v2 v2.17.2 h1:FQW5oHYcIlkCNrMD2lloGScxcHJ0gkjshV3qcQAyHQk= +github.com/go-resty/resty/v2 v2.17.2/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= -github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= -github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-zookeeper/zk v1.0.4 h1:DPzxraQx7OrPyXq2phlGlNSIyWEsAox0RJmjTseMV6I= github.com/go-zookeeper/zk v1.0.4/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= @@ -925,8 +943,8 @@ github.com/gogo/status v1.0.3/go.mod h1:SavQ51ycCLnc7dGyJxp8YAmudx8xqiVrRf+6IXRs github.com/gogo/status v1.1.1 h1:DuHXlSFHNKqTQ+/ACf5Vs6r4X/dH2EgIzR9Vr+H65kg= github.com/gogo/status v1.1.1/go.mod h1:jpG3dM5QPcqu19Hg8lkUhBFBa3TcLs1DG7+2Jqci7oU= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= -github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= -github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= @@ -991,11 +1009,12 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= -github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= -github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0= +github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= @@ -1021,8 +1040,8 @@ github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= -github.com/google/pprof v0.0.0-20251213031049-b05bdaca462f h1:HU1RgM6NALf/KW9HEY6zry3ADbDKcmpQ+hJedoNGQYQ= -github.com/google/pprof v0.0.0-20251213031049-b05bdaca462f/go.mod h1:67FPmZWbr+KDT/VlpWtw6sO9XSjpJmLuHpoLmWiTGgY= +github.com/google/pprof v0.0.0-20260604005048-7023385849c0 h1:h1QTMDl6q9wDvDCJVpKQSjgleGFYnd2fOxmg2K+6BGE= +github.com/google/pprof v0.0.0-20260604005048-7023385849c0/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= @@ -1036,8 +1055,8 @@ github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= github.com/googleapis/enterprise-certificate-proxy v0.2.1/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= -github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ= -github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/enterprise-certificate-proxy v0.3.15 h1:xolVQTEXusUcAA5UgtyRLjelpFFHWlPQ4XfWGc7MBas= +github.com/googleapis/enterprise-certificate-proxy v0.3.15/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= github.com/googleapis/gax-go v2.0.2+incompatible h1:silFMLAnr330+NRuag/VjIGF7TLp/LBrV2CJKFLWEww= github.com/googleapis/gax-go v2.0.2+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= @@ -1050,12 +1069,12 @@ github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo= github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY= github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8= -github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= -github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= +github.com/googleapis/gax-go/v2 v2.22.0 h1:PjIWBpgGIVKGoCXuiCoP64altEJCj3/Ei+kSU5vlZD4= +github.com/googleapis/gax-go/v2 v2.22.0/go.mod h1:irWBbALSr0Sk3qlqb9SyJ1h68WjgeFuiOzI4Rqw5+aY= github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= -github.com/gophercloud/gophercloud/v2 v2.9.0 h1:Y9OMrwKF9EDERcHFSOTpf/6XGoAI0yOxmsLmQki4LPM= -github.com/gophercloud/gophercloud/v2 v2.9.0/go.mod h1:Ki/ILhYZr/5EPebrPL9Ej+tUg4lqx71/YH2JWVeU+Qk= +github.com/gophercloud/gophercloud/v2 v2.12.0 h1:Gxmc/Bog1UDKkxTcQW7MSPTDviJXpLeEgVeN5KrxoCo= +github.com/gophercloud/gophercloud/v2 v2.12.0/go.mod h1:H7TTOxbLy8RIaHSNhI2GCrWIzw4Xpw8Xn2mBhCUT5kA= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= @@ -1070,8 +1089,8 @@ github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2/go.mod h1:wd1YpapPLivG6nQ github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs= github.com/hashicorp/consul/api v1.32.1 h1:0+osr/3t/aZNAdJX558crU3PEjVrG4x6715aZHRgceE= github.com/hashicorp/consul/api v1.32.1/go.mod h1:mXUWLnxftwTmDv4W3lzxYCPD199iNLLUyLfLGFJbtl4= github.com/hashicorp/cronexpr v1.1.3 h1:rl5IkxXN2m681EfivTlccqIryzYJSXRGRNa0xeG7NA4= @@ -1086,24 +1105,24 @@ github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJ github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= -github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= +github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= +github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= -github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4= -github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.9.0 h1:CeOIz6k+LoN3qX9Z0tyQrPtiB1DFYRPfCIBtaXPSCnA= +github.com/hashicorp/go-version v1.9.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.6.0 h1:uL2shRDx7RTrOrTCUZEGP/wJUFiUI8QT6E7z5o8jga4= github.com/hashicorp/golang-lru v0.6.0/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= -github.com/hashicorp/nomad/api v0.0.0-20251216171439-1dee0671280e h1:wGl06iy/H90NSbWjfXWeRwk9SJOks0u4voIryeJFlSA= -github.com/hashicorp/nomad/api v0.0.0-20251216171439-1dee0671280e/go.mod h1:sldFTIgs+FsUeKU3LwVjviAIuksxD8TzDOn02MYwslE= +github.com/hashicorp/nomad/api v0.0.0-20260616181215-ea1ca2d932bf h1:pU9wD+K2z1mY8ypEmMlfnuxPURG6Vf/OCZsyuWP/3AE= +github.com/hashicorp/nomad/api v0.0.0-20260616181215-ea1ca2d932bf/go.mod h1:Kr8imJwigbQ/50BqVae2+JL+AyX+FnzbnuCoIFb6iYg= github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY= github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= -github.com/hetznercloud/hcloud-go/v2 v2.32.0 h1:BRe+k7ESdYv3xQLBGdKUfk+XBFRJNGKzq70nJI24ciM= -github.com/hetznercloud/hcloud-go/v2 v2.32.0/go.mod h1:hAanyyfn9M0cMmZ68CXzPCF54KRb9EXd8eiE2FHKGIE= +github.com/hetznercloud/hcloud-go/v2 v2.43.0 h1:soqEUxJJqbf8UICQmDXfUwY/khfROAk0fi1s0bnBtd8= +github.com/hetznercloud/hcloud-go/v2 v2.43.0/go.mod h1:d0s2WLe7jSoStamv3eHoWgBSOxc/K17tYSXsqUkbse0= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huaweicloud/huaweicloud-sdk-go-obs v3.25.4+incompatible h1:yNjwdvn9fwuN6Ouxr0xHM0cVu03YMUWUyFmu2van/Yc= github.com/huaweicloud/huaweicloud-sdk-go-obs v3.25.4+incompatible/go.mod h1:l7VUhRbTKCzdOacdT4oWCwATKyvZqUOlOqr0Ous3k4s= @@ -1111,8 +1130,8 @@ github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= -github.com/ionos-cloud/sdk-go/v6 v6.3.5 h1:6fHArdV1lf50iRhCkCP7wkvGwWzVwi+l9w1t5mwkOa8= -github.com/ionos-cloud/sdk-go/v6 v6.3.5/go.mod h1:nUGHP4kZHAZngCVr4v6C8nuargFrtvt7GrzH/hqn7c4= +github.com/ionos-cloud/sdk-go/v6 v6.3.8 h1:CUZzrNciLM2IlmZtnclIznjST29tAYQbtQ8epiX5RUo= +github.com/ionos-cloud/sdk-go/v6 v6.3.8/go.mod h1:nUGHP4kZHAZngCVr4v6C8nuargFrtvt7GrzH/hqn7c4= github.com/jaegertracing/jaeger-idl v0.6.0 h1:LOVQfVby9ywdMPI9n3hMwKbyLVV3BL1XH2QqsP5KTMk= github.com/jaegertracing/jaeger-idl v0.6.0/go.mod h1:mpW0lZfG907/+o5w5OlnNnig7nHJGT3SfKmRqC42HGQ= github.com/jcchavezs/porto v0.1.0/go.mod h1:fESH0gzDHiutHRdX2hv27ojnOVFco37hg1W6E9EZF4A= @@ -1120,6 +1139,8 @@ github.com/jcchavezs/porto v0.7.0 h1:VncK84yxV7QZD4GdvoslzjnieSuruztGxLCmFi/Eu28 github.com/jcchavezs/porto v0.7.0/go.mod h1:tQ1cJ85cNzzZg/58VuZWOLbmrjcH1wPxkWgeBjvOq5o= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901/go.mod h1:Z86h9688Y0wesXCyonoVr47MasHilkuLMqGhRZ4Hpak= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= @@ -1146,8 +1167,8 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= -github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= -github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= +github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= @@ -1156,8 +1177,8 @@ github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpb github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= github.com/knadh/koanf/providers/confmap v1.0.0 h1:mHKLJTE7iXEys6deO5p6olAiZdG5zwp8Aebir+/EaRE= github.com/knadh/koanf/providers/confmap v1.0.0/go.mod h1:txHYHiI2hAtF0/0sCmcuol4IDcuQbKTybiB1nOcUo1A= -github.com/knadh/koanf/v2 v2.3.0 h1:Qg076dDRFHvqnKG97ZEsi9TAg2/nFTa9hCdcSa1lvlM= -github.com/knadh/koanf/v2 v2.3.0/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28= +github.com/knadh/koanf/v2 v2.3.5 h1:2dXJUYaKGm4SGYeoAtBviq9+02JZo/pxQ2ssOd60rJg= +github.com/knadh/koanf/v2 v2.3.5/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28= github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b h1:udzkj9S/zlT5X367kqJis0QP7YMxobob6zhzq6Yre00= github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b/go.mod h1:pcaDhQK0/NJZEvtCO0qQPPropqV0sJOJ6YW7X+9kRwM= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -1185,8 +1206,8 @@ github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20210210170715-a github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20210210170715-a8dfcb80d3a7/go.mod h1:Spd59icnvRxSKuyijbbwe5AemzvcyXAUBgApa7VybMw= github.com/lightstep/lightstep-tracer-go v0.26.0 h1:ZOw8meo7+7SvvUWrL0c4IRr3bd4YIGRtrAgDBaRH6ro= github.com/lightstep/lightstep-tracer-go v0.26.0/go.mod h1:+H6HJI7VlzXOAyxt5a/ZhsOUFbBU89BTMrBFEWSWGoY= -github.com/linode/linodego v1.63.0 h1:MdjizfXNJDVJU6ggoJmMO5O9h4KGPGivNX0fzrAnstk= -github.com/linode/linodego v1.63.0/go.mod h1:GoiwLVuLdBQcAebxAVKVL3mMYUgJZR/puOUSla04xBE= +github.com/linode/linodego v1.69.1 h1:f45N2MHR/oece2/ktTTCYmrlfse4//k3NgwcF5zbGZ0= +github.com/linode/linodego v1.69.1/go.mod h1:Fha0NYsQSx5VZK1HQNJY/z/dIxxkFp+vb5veawbmAUw= github.com/lovoo/gcloud-opentracing v0.3.0 h1:nAeKG70rIsog0TelcEtt6KU0Y1s5qXtsDLnHp0urPLU= github.com/lovoo/gcloud-opentracing v0.3.0/go.mod h1:ZFqk2y38kMDDikZPAK7ynTTGuyt17nSPdS3K5e+ZTBY= github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= @@ -1216,8 +1237,8 @@ github.com/mdlayher/vsock v1.2.1/go.mod h1:NRfCibel++DgeMD8z/hP+PPTjlNJsdPOmxcnE github.com/metalmatze/signal v0.0.0-20210307161603-1c9aa721a97a h1:0usWxe5SGXKQovz3p+BiQ81Jy845xSMu2CWKuXsXuUM= github.com/metalmatze/signal v0.0.0-20210307161603-1c9aa721a97a/go.mod h1:3OETvrxfELvGsU2RoGGWercfeZ4bCL3+SOwzIWtJH/Q= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= -github.com/miekg/dns v1.1.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc= -github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g= +github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= +github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE= github.com/minio/crc64nvme v1.0.1 h1:DHQPrYPdqK7jQG/Ls5CTBZWeex/2FMS3G5XGkycuFrY= @@ -1241,6 +1262,10 @@ github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zx github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/moby/api v1.54.2 h1:wiat9QAhnDQjA7wk1kh/TqHz2I1uUA7M7t9SAl/JNXg= +github.com/moby/moby/api v1.54.2/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= +github.com/moby/moby/client v0.4.1 h1:DMQgisVoMkmMs7fp3ROSdiBnoAu8+vo3GggFl06M/wY= +github.com/moby/moby/client v0.4.1/go.mod h1:z52C9O2POPOsnxZAy//WtKcQ32P+jT/NGeXu/7nfjGQ= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -1265,8 +1290,6 @@ github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/oklog/run v1.2.0 h1:O8x3yXwah4A73hJdlrwo/2X6J62gE5qTMusH0dvz60E= github.com/oklog/run v1.2.0/go.mod h1:mgDbKRSwPhJfesJ4PntqFUbKQRZ50NgmZTSPlFA0YFk= -github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= -github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= @@ -1276,23 +1299,23 @@ github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/ginkgo/v2 v2.22.1 h1:QW7tbJAUDyVDVOM5dFa7qaybo+CRfR7bemlQUN6Z8aM= -github.com/onsi/ginkgo/v2 v2.22.1/go.mod h1:S6aTpoRsSq2cZOd+pssHAlKW/Q/jZt6cPrPlnj4a1xM= +github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= +github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= -github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= -github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics v0.142.0 h1:agYk41V3eIfV6aIMxIeRQ7SFhfaW5k2O96HEebpmPwM= -github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics v0.142.0/go.mod h1:ZmMdcBia20ih8NYia5b4dNhfNLT68xHgaqF+fNW+TLM= -github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.142.0 h1:bLp+Ii1UQ9cNr+Dm1jKzbcklhd0eBnPuIFQY6NPzkZ0= -github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.142.0/go.mod h1:6N36UrFd9Yiz2aYpXM5xiK7Eqp2RyAr3O8lUE+wK2Y8= -github.com/open-telemetry/opentelemetry-collector-contrib/processor/deltatocumulativeprocessor v0.142.0 h1:fL8LBVeje+nbts2VIInvRa4T5LlsC0BZCI60wNGoS+Y= -github.com/open-telemetry/opentelemetry-collector-contrib/processor/deltatocumulativeprocessor v0.142.0/go.mod h1:fSnKuTN91I68Ou1Lgfwe3Mt6BGl9kcA8PYCpnGkPnsY= +github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= +github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= +github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics v0.154.0 h1:WS8HkUa6p8iVJ2v0mmGEK1a9R2b+Uro6tSG+4IfX6rk= +github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics v0.154.0/go.mod h1:9QPTx+XgZE7ktvh5jT5TvSisIkh2Fwc7mrfuf6+j2/U= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.154.0 h1:Kda+8F8o5QATBLP5K2MKmI2t7ddr7sBaV0EhZpjlvB0= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.154.0/go.mod h1:iVnoGSVXYhnyuQ6TQNhBIHqtu7h0LTXbSyWy584eBjg= +github.com/open-telemetry/opentelemetry-collector-contrib/processor/deltatocumulativeprocessor v0.154.0 h1:U/MRkEeVwZ3zl8hOlUBP/Q/RMgLfMbTHQoATlLXhI4I= +github.com/open-telemetry/opentelemetry-collector-contrib/processor/deltatocumulativeprocessor v0.154.0/go.mod h1:dFTV2c6rjph2ZMtkq9xHN5QuYbUSQ+o/25UQfIY3QUQ= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= -github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/opentracing-contrib/go-grpc v0.0.0-20180928155321-4b5a12d3ff02/go.mod h1:JNdpVEzCpXBgIiv4ds+TzhN1hrtxq6ClLrTlT9OQRSc= github.com/opentracing-contrib/go-grpc v0.1.2 h1:MP16Ozc59kqqwn1v18aQxpeGZhsBanJ2iurZYaQSZ+g= github.com/opentracing-contrib/go-grpc v0.1.2/go.mod h1:glU6rl1Fhfp9aXUHkE36K2mR4ht8vih0ekOVlWKEUHM= @@ -1309,8 +1332,18 @@ github.com/oracle/oci-go-sdk/v65 v65.93.1 h1:lIvy/6aQOUenQI+cxXH1wDBJeXFPO9Du3Ca github.com/oracle/oci-go-sdk/v65 v65.93.1/go.mod h1:u6XRPsw9tPziBh76K7GrrRXPa8P8W3BQeqJ6ZZt9VLA= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= +github.com/outscale/osc-sdk-go/v2 v2.34.0 h1:hHH5W9Fmgt6b8nGUmDyu4vVP+zqJ+W0zflzjgsGEGUQ= +github.com/outscale/osc-sdk-go/v2 v2.34.0/go.mod h1:6J8WRznaSIEXXVHhhTXisGJQgvE5fYzbf8hAw7YIGfQ= github.com/ovh/go-ovh v1.9.0 h1:6K8VoL3BYjVV3In9tPJUdT7qMx9h0GExN9EXx1r2kKE= github.com/ovh/go-ovh v1.9.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c= +github.com/pb33f/jsonpath v0.8.2 h1:Ou4C7zjYClBm97dfZjDCjdZGusJoynv/vrtiEKNfj2Y= +github.com/pb33f/jsonpath v0.8.2/go.mod h1:zBV5LJW4OQOPatmQE2QdKpGQJvhDTlE5IEj6ASaRNTo= +github.com/pb33f/libopenapi v0.37.2 h1:4Kb4w/h2BVKb099oYIZqeDxEBhUioWA+z6WJhBOk2r8= +github.com/pb33f/libopenapi v0.37.2/go.mod h1:MsDdUlQ1CdrIDO5v26JfgBxQs7kcaOUEpMP3EqU6bI4= +github.com/pb33f/libopenapi-validator v0.13.8 h1:3t/5hUq8EYIWe6Uwj5RA7xFSWkleDbqsueA78/tIAo8= +github.com/pb33f/libopenapi-validator v0.13.8/go.mod h1:mWwedExRS1L8gjK6BGE1oDA5wXLuxgU7EheNz703MCg= +github.com/pb33f/ordered-map/v2 v2.3.1 h1:5319HDO0aw4DA4gzi+zv4FXU9UlSs3xGZ40wcP1nBjY= +github.com/pb33f/ordered-map/v2 v2.3.1/go.mod h1:qxFQgd0PkVUtOMCkTapqotNgzRhMPL7VvaHKbd1HnmQ= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= @@ -1334,10 +1367,10 @@ github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus-community/prom-label-proxy v0.11.1 h1:jX+m+BQCNM0z3/P6V6jVxbiDKgugvk91SaICD6bVhT4= -github.com/prometheus-community/prom-label-proxy v0.11.1/go.mod h1:uTeQW+wZ/VPV1LL3IPfvUE++wR2nPLex+Y4RE38Cpis= -github.com/prometheus/alertmanager v0.30.0 h1:E4dnxSFXK8V2Bb8iqudlisTmaIrF3hRJSWnliG08tBM= -github.com/prometheus/alertmanager v0.30.0/go.mod h1:93PBumcTLr/gNtNtM0m7BcCffbvYP5bKuLBWiOnISaA= +github.com/prometheus-community/prom-label-proxy v0.14.0 h1:5P99yZ0stwrwqpu73xDYg7d4HjJ7iNDSdDYeVrNy66w= +github.com/prometheus-community/prom-label-proxy v0.14.0/go.mod h1:SrmZpQsimziUAqvmAdaHUT+gLFH/FBKz6mkee8Wfhzk= +github.com/prometheus/alertmanager v0.33.0 h1:AAVa3wpCsaDxisTUUPXx+1qhnA2mx0f8Cc+smpAtN7w= +github.com/prometheus/alertmanager v0.33.0/go.mod h1:V06Uc8EZ5X5wLOJRGhtXx+EE2LgrinFIADbKWMVm1RY= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= @@ -1349,8 +1382,8 @@ github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQg github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= -github.com/prometheus/client_golang/exp v0.0.0-20251212205219-7ba246a648ca h1:BOxmsLoL2ymn8lXJtorca7N/m+2vDQUDoEtPjf0iAxA= -github.com/prometheus/client_golang/exp v0.0.0-20251212205219-7ba246a648ca/go.mod h1:gndBHh3ZdjBozGcGrjUYjN3UJLRS3l2drALtu4lUt+k= +github.com/prometheus/client_golang/exp v0.0.0-20260602051030-3537b20ac86b h1:633sracZPrB7O7T6r5skFtwqXDOrXlQkE9Wr5DnYVJE= +github.com/prometheus/client_golang/exp v0.0.0-20260602051030-3537b20ac86b/go.mod h1:7hAEIbflIgnK0HubVroVy6UgJYYKryF6p3mP/dcyay8= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -1365,11 +1398,11 @@ github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9 github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= -github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= -github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= +github.com/prometheus/common v0.69.0 h1:OA85nJQS/T/MaYh/Q2CcgDKSGWqNIgrBDvDH85CuiNk= +github.com/prometheus/common v0.69.0/go.mod h1:ZzL3f6u94qUxh9p+tJTrF+FvBS1XXbbRAZCQkytAL0Y= github.com/prometheus/exporter-toolkit v0.8.2/go.mod h1:00shzmJL7KxcsabLWcONwpyNEuWhREOnFqZW7vadFS0= -github.com/prometheus/exporter-toolkit v0.15.0 h1:Pcle5sSViwR1x0gdPd0wtYrPQENBieQAM7TmT0qtb2U= -github.com/prometheus/exporter-toolkit v0.15.0/go.mod h1:OyRWd2iTo6Xge9Kedvv0IhCrJSBu36JCfJ2yVniRIYk= +github.com/prometheus/exporter-toolkit v0.16.0 h1:xT/j7L2XKF+VJd6B4fpUw6xWabHrSmsUf6mYmFqyu0s= +github.com/prometheus/exporter-toolkit v0.16.0/go.mod h1:d1EL8Z9674xQe/iWhwP2wDyCEoBPbXVeqDbqAUsgJWY= github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos= github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= @@ -1383,12 +1416,10 @@ github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0ua github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= -github.com/prometheus/prometheus v0.309.1 h1:jutK6eCYDpWdPTUbVbkcQsNCMO9CCkSwjQRMLds4jSo= -github.com/prometheus/prometheus v0.309.1/go.mod h1:d+dOGiVhuNDa4MaFXHVdnUBy/CzqlcNTooR8oM1wdTU= -github.com/prometheus/sigv4 v0.3.0 h1:QIG7nTbu0JTnNidGI1Uwl5AGVIChWUACxn2B/BQ1kms= -github.com/prometheus/sigv4 v0.3.0/go.mod h1:fKtFYDus2M43CWKMNtGvFNHGXnAJJEGZbiYCmVp/F8I= -github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg= -github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= +github.com/prometheus/sigv4 v0.4.1 h1:EIc3j+8NBea9u1iV6O5ZAN8uvPq2xOIUPcqCTivHuXs= +github.com/prometheus/sigv4 v0.4.1/go.mod h1:eu+ZbRvsc5TPiHwqh77OWuCnWK73IdkETYY46P4dXOU= +github.com/puzpuzpuz/xsync/v4 v4.5.0 h1:vOSWu6b57/emh+L/Cw0BeQfvxa/cogFywXHeGUxQxAg= +github.com/puzpuzpuz/xsync/v4 v4.5.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo= github.com/redis/rueidis v1.0.61 h1:AkbCMeTyjFSQraGaNYncg3unMCTYGr6Y8WOqGhDOQu4= github.com/redis/rueidis v1.0.61/go.mod h1:Lkhr2QTgcoYBhxARU7kJRO8SyVlgUuEkcJO1Y8MCluA= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= @@ -1407,8 +1438,10 @@ github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfF github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk= github.com/santhosh-tekuri/jsonschema v1.2.4 h1:hNhW8e7t+H1vgY+1QeEQpveR6D4+OwKPXCfD2aieJis= github.com/santhosh-tekuri/jsonschema v1.2.4/go.mod h1:TEAUOeZSmIxTTuHatJzrvARHiuO9LYd+cIxzgEHCQI4= -github.com/scaleway/scaleway-sdk-go v1.0.0-beta.35 h1:8xfn1RzeI9yoCUuEwDy08F+No6PcKZGEDOQ6hrRyLts= -github.com/scaleway/scaleway-sdk-go v1.0.0-beta.35/go.mod h1:47B1d/YXmSAxlJxUJxClzHR6b3T4M1WyCvwENPQNBWc= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= +github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 h1:ObX9hZmK+VmijreZO/8x9pQ8/P/ToHD/bdSb4Eg4tUo= +github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36/go.mod h1:LEsDu4BubxK7/cWhtlQWfuxwL4rf/2UEpxXz1o1EMtM= github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 h1:emzAzMZ1L9iaKCTxdy3Em8Wv4ChIAGnfiz18Cda70g4= github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg= github.com/sercand/kuberesolver/v5 v5.1.1 h1:CYH+d67G0sGBj7q5wLK61yzqJJ8gLLC8aeprPTHb6yY= @@ -1426,12 +1459,12 @@ github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasO github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= -github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= -github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= -github.com/stackitcloud/stackit-sdk-go/core v0.20.1 h1:odiuhhRXmxvEvnVTeZSN9u98edvw2Cd3DcnkepncP3M= -github.com/stackitcloud/stackit-sdk-go/core v0.20.1/go.mod h1:fqto7M82ynGhEnpZU6VkQKYWYoFG5goC076JWXTUPRQ= +github.com/stackitcloud/stackit-sdk-go/core v0.26.0 h1:jQEb9gkehfp6VCP6TcYk7BI10cz4l0KM2L6hqYBH2QA= +github.com/stackitcloud/stackit-sdk-go/core v0.26.0/go.mod h1:WU1hhxnjXw2EV7CYa1nlEvNpMiRY6CvmIOaHuL3pOaA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= @@ -1459,8 +1492,6 @@ github.com/thanos-community/galaxycache v0.0.0-20211122094458-3a32041a1f1e h1:f1 github.com/thanos-community/galaxycache v0.0.0-20211122094458-3a32041a1f1e/go.mod h1:jXcofnrSln/cLI6/dhlBxPQZEEQHVPCcFaH75M+nSzM= github.com/thanos-io/objstore v0.0.0-20250804093838-71d60dfee488 h1:khBsQLLRoF1KzXgTlwFZa6mC32bwYUUAu/AeP49V7UM= github.com/thanos-io/objstore v0.0.0-20250804093838-71d60dfee488/go.mod h1:uDHLkMKOGDAnlN75EAz8VrRzob1+VbgYSuUleatWuF0= -github.com/thanos-io/promql-engine v0.0.0-20260513095632-c5f6038078e3 h1:vO9SBpSzzp7jlDaWqJ34Y7zumD5OrPTcCZcWgDgzC6M= -github.com/thanos-io/promql-engine v0.0.0-20260513095632-c5f6038078e3/go.mod h1:uzn40oZHPXvfdP498h+MiRL2fN7RF519gNaV3LyhChc= github.com/themihai/gomemcache v0.0.0-20180902122335-24332e2d58ab h1:7ZR3hmisBWw77ZpO1/o86g+JV3VKlk3d48jopJxzTjU= github.com/themihai/gomemcache v0.0.0-20180902122335-24332e2d58ab/go.mod h1:eheTFp954zcWZXCU8d0AT76ftsQOTo4DTqkN/h3k1MY= github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww= @@ -1473,8 +1504,8 @@ github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMW github.com/uber/jaeger-lib v2.2.0+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg= github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= -github.com/vultr/govultr/v2 v2.17.2 h1:gej/rwr91Puc/tgh+j33p/BLR16UrIPnSr+AIwYWZQs= -github.com/vultr/govultr/v2 v2.17.2/go.mod h1:ZFOKGWmgjytfyjeyAdhQlSWwTjh2ig+X49cAp50dzXI= +github.com/vultr/govultr/v3 v3.31.2 h1:2l3/KDvfemG+4azw4LLquJoh9mFOAVEdBXtPPzix3ac= +github.com/vultr/govultr/v3 v3.31.2/go.mod h1:2zyUw9yADQaGwKnwDesmIOlBNLrm7edsCfWHFJpWKf8= github.com/weaveworks/common v0.0.0-20230728070032-dd9e68f319d5 h1:nORobjToZAvi54wcuUXLq+XG2Rsr0XEizy5aHBHvqWQ= github.com/weaveworks/common v0.0.0-20230728070032-dd9e68f319d5/go.mod h1:rgbeLfJUtEr+G74cwFPR1k/4N0kDeaeSv/qhUNE4hm8= github.com/weaveworks/promrus v1.2.0 h1:jOLf6pe6/vss4qGHjXmGz4oDJQA+AOCqEL3FvvZGz7M= @@ -1508,8 +1539,6 @@ go.elastic.co/apm/module/apmot v1.15.0/go.mod h1:BjFz2KOlnjXdnSo0p6nhDDaIEYYX8c6 go.elastic.co/fastjson v1.1.0/go.mod h1:boNGISWMjQsUPy/t6yqt2/1Wx4YNPSe+mZjlyw9vKKI= go.elastic.co/fastjson v1.5.1 h1:zeh1xHrFH79aQ6Xsw7YxixvnOdAl3OSv0xch/jRDzko= go.elastic.co/fastjson v1.5.1/go.mod h1:WtvH5wz8z9pDOPqNYSYKoLLv/9zCWZLeejHWuvdL/EM= -go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss= -go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= @@ -1521,50 +1550,52 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/collector/component v1.48.0 h1:0hZKOvT6fIlXoE+6t40UXbXOH7r/h9jyE3eIt0W19Qg= -go.opentelemetry.io/collector/component v1.48.0/go.mod h1:Kmc9Z2CT53M2oRRf+WXHUHHgjCC+ADbiqfPO5mgZe3g= -go.opentelemetry.io/collector/component/componentstatus v0.142.0 h1:a1KkLCtShI5SfhO2ga75VqWjjBRGgrerelt/2JXWLBI= -go.opentelemetry.io/collector/component/componentstatus v0.142.0/go.mod h1:IRWKvFcUrFrkz1gJEV+cKAdE2ZBT128gk1sHt0OzKI4= -go.opentelemetry.io/collector/component/componenttest v0.142.0 h1:a8XclEutO5dv4AnzThHK8dfqR4lDWjJKLtRNM2aVUFM= -go.opentelemetry.io/collector/component/componenttest v0.142.0/go.mod h1:JhX/zKaEbjhFcsiV2ha2spzo24A6RL/jqNBS0svURD0= -go.opentelemetry.io/collector/confmap v1.48.0 h1:vGhg25NEUX5DiYziJEw2siwdzsvtXBRZVuYyLVinFR8= -go.opentelemetry.io/collector/confmap v1.48.0/go.mod h1:8tJHJowmvUkJ8AHzZ6SaH61dcWbdfRE9Sd/hwsKLgRE= -go.opentelemetry.io/collector/confmap/xconfmap v0.142.0 h1:SNfuFP8TA0PmUkx6ryY63uNjLN2HMh5VeGO++IYdPgA= -go.opentelemetry.io/collector/confmap/xconfmap v0.142.0/go.mod h1:FXuX6B8b7Ub7qkLqloWKanmPhADL18EEkaFptcd4eDQ= -go.opentelemetry.io/collector/consumer v1.48.0 h1:g1uroz2AA0cqnEsjqFTSZG+y8uH1gQBqqyzk8kd3QiM= -go.opentelemetry.io/collector/consumer v1.48.0/go.mod h1:lC6PnVXBwI456SV5WtvJqE7vjCNN6DAUc8xjFQ9wUV4= -go.opentelemetry.io/collector/consumer/consumertest v0.142.0 h1:TRt8zR57Vk1PTjtqjHOwOAMbIl+IeloHxWAuF8sWdRw= -go.opentelemetry.io/collector/consumer/consumertest v0.142.0/go.mod h1:yq2dhMxFUlCFkRN7LES3fzsTmUDw9VaunyRAka2TEaY= -go.opentelemetry.io/collector/consumer/xconsumer v0.142.0 h1:qOoQnLZXQ9sRLexTkkmBx3qfaOmEgco9VBPmryg5UhA= -go.opentelemetry.io/collector/consumer/xconsumer v0.142.0/go.mod h1:oPN0yJzEpovwlWvmSaiYgtDqGuOmMMLmmg352sqZdsE= -go.opentelemetry.io/collector/featuregate v1.48.0 h1:jiGRcl93yzUFgZVDuskMAftFraE21jANdxXTQfSQScc= -go.opentelemetry.io/collector/featuregate v1.48.0/go.mod h1:/1bclXgP91pISaEeNulRxzzmzMTm4I5Xih2SnI4HRSo= -go.opentelemetry.io/collector/internal/testutil v0.142.0 h1:MHnAVRimQdsfYqYHC3YuJRkIUap4VmSpJkkIT2N7jJA= -go.opentelemetry.io/collector/internal/testutil v0.142.0/go.mod h1:YAD9EAkwh/l5asZNbEBEUCqEjoL1OKMjAMoPjPqH76c= -go.opentelemetry.io/collector/pdata v1.48.0 h1:CKZ+9v/lGTX/cTGx2XVp8kp0E8R//60kHFCBdZudrTg= -go.opentelemetry.io/collector/pdata v1.48.0/go.mod h1:jaf2JQGpfUreD1TOtGBPsq00ecOqM66NG15wALmdxKA= -go.opentelemetry.io/collector/pdata/pprofile v0.142.0 h1:Ivyw7WY8SIIWqzXsnNmjEgz3ysVs/OkIf0KIpJUnuuo= -go.opentelemetry.io/collector/pdata/pprofile v0.142.0/go.mod h1:94GAph54K4WDpYz9xirhroHB3ptNLuPiY02k8fyoNUI= -go.opentelemetry.io/collector/pdata/testdata v0.142.0 h1:+jf9RyLWl8WyhIVjpg7yuH+bRdQH4mW20cPtCMlY1cI= -go.opentelemetry.io/collector/pdata/testdata v0.142.0/go.mod h1:kgAu5ZLEcVuPH3RFiHDg23RGitgm1M0cUAVwiGX4SB8= -go.opentelemetry.io/collector/pipeline v1.48.0 h1:E4zyQ7+4FTGvdGS4pruUnItuyRTGhN0Qqk1CN71lfW0= -go.opentelemetry.io/collector/pipeline v1.48.0/go.mod h1:xUrAqiebzYbrgxyoXSkk6/Y3oi5Sy3im2iCA51LwUAI= -go.opentelemetry.io/collector/processor v1.48.0 h1:3Kttw79mnrf463QKJGoGZzFfiNzQuMWK0p2nHuvOhaQ= -go.opentelemetry.io/collector/processor v1.48.0/go.mod h1:A3OsW6ga+a48J1mrnVNH5L5kB0v+n9nVFlmOQB5/Jwk= -go.opentelemetry.io/collector/processor/processortest v0.142.0 h1:wQnJeXDejBL6r8ov66AYAGf8Q0/JspjuqAjPVBdCUoI= -go.opentelemetry.io/collector/processor/processortest v0.142.0/go.mod h1:QU5SWj0L+92MSvQxZDjwWCsKssNDm+nD6SHn7IvviUE= -go.opentelemetry.io/collector/processor/xprocessor v0.142.0 h1:7a1Crxrd5iBMVnebTxkcqxVkRHAlOBUUmNTUVUTnlCU= -go.opentelemetry.io/collector/processor/xprocessor v0.142.0/go.mod h1:LY/GS2DiJILJKS3ynU3eOLLWSP8CmN1FtdpAMsVV8AU= +go.opentelemetry.io/collector/component v1.60.0 h1:LpIjHMn7OOjUsFR84ROc2kqPbP1xnKyDCGi7ZVqEaKU= +go.opentelemetry.io/collector/component v1.60.0/go.mod h1:Rag+NNgiGIkcGYlcTfJtMh2l0T5XS1KNv9Wjw9yofAk= +go.opentelemetry.io/collector/component/componentstatus v0.154.0 h1:4ifSCy2Y332iZ5AldHt9ujVjY6XKxhVe/hND4TSDarg= +go.opentelemetry.io/collector/component/componentstatus v0.154.0/go.mod h1:ZsBIax7tvvODn0XqTyhTfKZjm96zVKnLUKvlN8SHFjo= +go.opentelemetry.io/collector/component/componenttest v0.154.0 h1:uH06tUatG4S45A/f3sFENMMAMzWURmgxKK3MAbVZAUI= +go.opentelemetry.io/collector/component/componenttest v0.154.0/go.mod h1:SQ1JRosjFAZ7kN2yNHNcNakOliqrP0QxglKcYyUrUpQ= +go.opentelemetry.io/collector/confmap v1.60.0 h1:TEBi/N3kac/JI4VTEq9LjqRCFdF2JS2MHOCEiHq8GSM= +go.opentelemetry.io/collector/confmap v1.60.0/go.mod h1:Z693ETewV4n8JsOO2jp/iLe1PGGpFCIzuNsF1xLeiSY= +go.opentelemetry.io/collector/confmap/xconfmap v0.154.0 h1:tarvY9S02jkYNYW/4+yD02RRatwJAojMD430Bs4JD/4= +go.opentelemetry.io/collector/confmap/xconfmap v0.154.0/go.mod h1:zcVRrY1gS8qVwBrTrhzVI67tMAUu5BONTsIXzjXu1Ho= +go.opentelemetry.io/collector/consumer v1.60.0 h1:SWP/0HvDnWiiy/4S366CiatAZ4gFl410UmggrZEcWVg= +go.opentelemetry.io/collector/consumer v1.60.0/go.mod h1:nkp1NBtKQzme7WFF7fkgRgDlQLs49VIMOn8rO0jfmYU= +go.opentelemetry.io/collector/consumer/consumertest v0.154.0 h1:G9gFP86ZsglC3mTLA6cqOrW5lvdcEBJrVgHtThE+Sc4= +go.opentelemetry.io/collector/consumer/consumertest v0.154.0/go.mod h1:FRLGgy8gFYjm3A+yby1bctz5ZIAn6EUOpuV49KnKbFY= +go.opentelemetry.io/collector/consumer/xconsumer v0.154.0 h1:I3rB+S5ORE1XLzqopFXvP6UmYrsj5n1tFlcEAPg96Zw= +go.opentelemetry.io/collector/consumer/xconsumer v0.154.0/go.mod h1:WNT9BoyLE/nE5N6WEL4c1GXcfGcRUmSTCSr6e/tyfO4= +go.opentelemetry.io/collector/featuregate v1.60.0 h1:/HxHB8hq4N5Fhq5N0C8G6xbXTHxnGcWIryyJzmP7pdc= +go.opentelemetry.io/collector/featuregate v1.60.0/go.mod h1:4ga1QBMPEejXXmpyJS8lmaRpknJ3Lb9Bvk6e420bUFU= +go.opentelemetry.io/collector/internal/componentalias v0.154.0 h1:g0y8F/qez9cbsgF5+/uU6YC6l5oXVkccIhsXVHmF3xQ= +go.opentelemetry.io/collector/internal/componentalias v0.154.0/go.mod h1:F2tudJ/Zcm8w8b768sU65nZc4q2rgY1MhfX5FxDeUgA= +go.opentelemetry.io/collector/internal/testutil v0.154.0 h1:iUYHOM8+wONW01A4jFnzauanOYGVBGchKWWtm51is6c= +go.opentelemetry.io/collector/internal/testutil v0.154.0/go.mod h1:Jkjs6rkqs973LqgZ0Fe3zrokQRKULYXPIf4HuqStiEE= +go.opentelemetry.io/collector/pdata v1.60.0 h1:YcGMHzeJucHen41AoR4mxHro8reUr9SVqt7P0KacKzQ= +go.opentelemetry.io/collector/pdata v1.60.0/go.mod h1:Ca8VgZX2wOr6wW4nihPWaCpkJVvzeo6Txa7BJ7/WO90= +go.opentelemetry.io/collector/pdata/pprofile v0.154.0 h1:dWrHnKBzzMhkZXfKmSuFpGVAApSUcrQ+mBFzAsO6/8s= +go.opentelemetry.io/collector/pdata/pprofile v0.154.0/go.mod h1:BE9oOmAEHVqE+yHRe5Z3qz7co+2SU249DIxVGPRsYf8= +go.opentelemetry.io/collector/pdata/testdata v0.154.0 h1:PSc3gogHpJoVHenvMhcxkOPTnEKpaykURxtSNyVXYK4= +go.opentelemetry.io/collector/pdata/testdata v0.154.0/go.mod h1:zIT+sag/xmSM6VAMhv2tnEzlQF9n266OcQm4V6roWdU= +go.opentelemetry.io/collector/pipeline v1.60.0 h1:ZLk/8K/Xzz+JRBWLmqLlVMwEWVnQvmly6nWeKs+lh6s= +go.opentelemetry.io/collector/pipeline v1.60.0/go.mod h1:RD90NG3Jbk965Xaqym3JyHkuol4uZJjQVUkD9ddXJIs= +go.opentelemetry.io/collector/processor v1.60.0 h1:B3YgiKa+4tMuJ6v4bSaKUtTCwNRzugbEDei8j7jiPpI= +go.opentelemetry.io/collector/processor v1.60.0/go.mod h1:ZRNUW8FHZ+0CW+HoIG0/h+fQq8aYjMz9ccy2w2jguag= +go.opentelemetry.io/collector/processor/processortest v0.154.0 h1:2Lu7JGqH3fzg9BE0rmzBwCQB7oRWzM8fs+X5SSZO/4M= +go.opentelemetry.io/collector/processor/processortest v0.154.0/go.mod h1:E813PIbkBcwgoDnZ9cjuw70MUNmqxAHIvmDC8gOZiP8= +go.opentelemetry.io/collector/processor/xprocessor v0.154.0 h1:ert+SRk5DPSqIxqpOEnywrwVLYSvqEvXwy60F94VtFE= +go.opentelemetry.io/collector/processor/xprocessor v0.154.0/go.mod h1:93XyfiqPYokF1i8NQvWsKggt5Si5qZvOcZ2P0l+uxII= go.opentelemetry.io/collector/semconv v0.128.0 h1:MzYOz7Vgb3Kf5D7b49pqqgeUhEmOCuT10bIXb/Cc+k4= go.opentelemetry.io/collector/semconv v0.128.0/go.mod h1:OPXer4l43X23cnjLXIZnRj/qQOjSuq4TgBLI76P9hns= go.opentelemetry.io/contrib/detectors/gcp v1.42.0 h1:kpt2PEJuOuqYkPcktfJqWWDjTEd/FNgrxcniL7kQrXQ= go.opentelemetry.io/contrib/detectors/gcp v1.42.0/go.mod h1:W9zQ439utxymRrXsUOzZbFX4JhLxXU4+ZnCt8GG7yA8= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= -go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.64.0 h1:OXSUzgmIFkcC4An+mv+lqqZSndTffXpjAyoR+1f8k/A= -go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.64.0/go.mod h1:1A4GVLFIm54HFqVdOpWmukap7rgb0frrE3zWXohLPdM= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.69.0 h1:MCcYL7J6Vt/X0kjqbMZkekCmwsurbQRbL69vkiye2lk= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.69.0/go.mod h1:3jnStNwSufK+f5ktjL4EPcwtig4rtd81NS70lqHuXl8= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI= go.opentelemetry.io/contrib/propagators/autoprop v0.61.0 h1:cxOVDJ30qfzV27G5p9WMtJUB/3cXC0iL+u9EV1fSOws= go.opentelemetry.io/contrib/propagators/autoprop v0.61.0/go.mod h1:Y+xiUbWetg65vAroDZcIzJ5wyPNWRH32EoIV9rIaa0g= go.opentelemetry.io/contrib/propagators/aws v1.36.0 h1:Txhy/1LZIbbnutftc5pdU8Y9vOQuAkuIOFXuLsdDejs= @@ -1577,39 +1608,41 @@ go.opentelemetry.io/contrib/propagators/ot v1.36.0 h1:UBoZjbx483GslNKYK2YpfvePTJ go.opentelemetry.io/contrib/propagators/ot v1.36.0/go.mod h1:adDDRry19/n9WoA7mSCMjoVJcmzK/bZYzX9SR+g2+W4= go.opentelemetry.io/contrib/samplers/jaegerremote v0.30.0 h1:bQ1Gvah4Sp8z7epSkgJaNTuZm7sutfA6Fji2/7cKFMc= go.opentelemetry.io/contrib/samplers/jaegerremote v0.30.0/go.mod h1:9b8Q9rH52NgYH3ShiTFB5wf18Vt3RTH/VMB7LDcC1ug= -go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= -go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= go.opentelemetry.io/otel/bridge/opentracing v1.36.0 h1:GWGmcYhMCu6+K/Yz5KWSETU/esd/mkVGx+77uKtLjpk= go.opentelemetry.io/otel/bridge/opentracing v1.36.0/go.mod h1:bW7xTHgtWSNqY8QjhqXzloXBkw3iQIa8uBqCF/0EUbc= go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4= go.opentelemetry.io/otel/exporters/jaeger v1.17.0/go.mod h1:nPCqOnEH9rNLKqH/+rrUjiMzHJdV1BlpKcTwRTyKkKI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 h1:RAE+JPfvEmvy+0LzyUA25/SGawPwIUbZ6u0Wug54sLc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0/go.mod h1:AGmbycVGEsRx9mXMZ75CsOyhSP6MFIcj/6dnG+vhVjk= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0 h1:WDdP9acbMYjbKIyJUhTvtzj601sVJOqgWdUxSdR/Ysc= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0/go.mod h1:BLbf7zbNIONBLPwvFnwNHGj4zge8uTCM/UPIVW1Mq2I= -go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= -go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= -go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= -go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= -go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= -go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= -go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= -go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 h1:4YsVu3B8+3qtWYYrsUYgn0OG78pN0rnNPRGX4SbokQI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0/go.mod h1:+wnlSn0mD1ADVMe3v9Z/WIaiz6q6gL2J/ejaAmdmv80= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0 h1:qazEJlUOQzhCpzQpFETGby7EdqjI1wsd0W+6Gg1SCTU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0/go.mod h1:fOD2Yefuxixkx3ahVNf0O/PERb6r4OlbxfATVnYvzCo= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0 h1:lgh3PiVrRUWMLOVSkQicxzZll5NjF1r+AtsX1XRIHw0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0/go.mod h1:5Cnhth3m/AgOeTgE3ex12pPmiu/gGtZit03kSzx9X7s= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 h1:rixTyDGXFxRy1xzhKrotaHy3/KXdPhlWARrCgK+eqUY= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= +go.opentelemetry.io/otel/metric/x v0.66.0 h1:YkCrx1zLOChi9ZcZ6euupOcsgzbVlec7D/xoEU1+cTA= +go.opentelemetry.io/otel/metric/x v0.66.0/go.mod h1:d1+BDj9t96do0/1LoU1ayfCv79ZgNE41qbhBvnMOBZk= +go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= +go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= +go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= +go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= -go.opentelemetry.io/proto/slim/otlp v1.9.0 h1:fPVMv8tP3TrsqlkH1HWYUpbCY9cAIemx184VGkS6vlE= -go.opentelemetry.io/proto/slim/otlp v1.9.0/go.mod h1:xXdeJJ90Gqyll+orzUkY4bOd2HECo5JofeoLpymVqdI= -go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.2.0 h1:o13nadWDNkH/quoDomDUClnQBpdQQ2Qqv0lQBjIXjE8= -go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.2.0/go.mod h1:Gyb6Xe7FTi/6xBHwMmngGoHqL0w29Y4eW8TGFzpefGA= -go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.2.0 h1:EiUYvtwu6PMrMHVjcPfnsG3v+ajPkbUeH+IL93+QYyk= -go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.2.0/go.mod h1:mUUHKFiN2SST3AhJ8XhJxEoeVW12oqfXog0Bo8W3Ec4= +go.opentelemetry.io/proto/slim/otlp v1.10.0 h1:iR97Vs/ZDR+y9TfuP9b1XBtdPWeC+OMslIBmhcLU7jM= +go.opentelemetry.io/proto/slim/otlp v1.10.0/go.mod h1:lV9250stpjYLPNA5viFabIgP2QlUGRT1GdTgAf8SIUk= +go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.3.0 h1:RUF5rO0hAlgiJt1fzQVzcVs3vZVNHIcMLgOgG4rWNcQ= +go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.3.0/go.mod h1:I89cynRj8y+383o7tEQVg2SVA6SRgDVIouWPUVXjx0U= +go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.3.0 h1:CQvJSldHRUN6Z8jsUeYv8J0lXRvygALXIzsmAeCcZE0= +go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.3.0/go.mod h1:xSQ+mEfJe/GjK1LXEyVOoSI1N9JV9ZI923X5kup43W4= go.uber.org/atomic v1.5.1/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= @@ -1617,12 +1650,14 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= -go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= -go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= +go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +go.yaml.in/yaml/v4 v4.0.0-rc.5 h1:JVliQq9EGOYaTgMi+k8BhUJyqcGk4ZqeuiN1Cirba9c= +go.yaml.in/yaml/v4 v4.0.0-rc.5/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= go4.org/intern v0.0.0-20230525184215-6c62f75575cb h1:ae7kzL5Cfdmcecbh22ll7lYP3iuUdnfnhiPcSaDgH/8= go4.org/intern v0.0.0-20230525184215-6c62f75575cb/go.mod h1:Ycrt6raEcnF5FTsLiLKkhBTO6DPX3RCUCUVnks3gFJU= go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E= @@ -1658,8 +1693,8 @@ golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= -golang.org/x/exp v0.0.0-20250808145144-a408d31f581a h1:Y+7uR/b1Mw2iSXZ3G//1haIiSElDQZ8KWh0h+sZPG90= -golang.org/x/exp v0.0.0-20250808145144-a408d31f581a/go.mod h1:rT6SFzZ7oxADUDx58pcaKFTcZ+inxAa9fTrYx/uVYwg= +golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0= +golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= @@ -1961,8 +1996,8 @@ golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= -golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -2105,8 +2140,8 @@ google.golang.org/api v0.106.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/ google.golang.org/api v0.107.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= google.golang.org/api v0.108.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60ca7iI= -google.golang.org/api v0.257.0 h1:8Y0lzvHlZps53PEaw+G29SsQIkuKrumGWs9puiexNAA= -google.golang.org/api v0.257.0/go.mod h1:4eJrr+vbVaZSqs7vovFd1Jb/A6ml6iw2e6FBYf3GAO4= +google.golang.org/api v0.278.0 h1:W7jiRvRi53VYFfZ/HoZjQBtJk7gOFbHD8ot1RzVZU6E= +google.golang.org/api v0.278.0/go.mod h1:B9TqLBwJqVjp1mtt7WeoQwWRwvu/400y5lETOql+giQ= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -2241,10 +2276,10 @@ google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc/go.mod h1:RGgjbofJ google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44/go.mod h1:8B0gmkoRebU8ukX6HP+4wrVQUY1+6PkQ44BSyIlflHA= google.golang.org/genproto v0.0.0-20230222225845-10f96fb3dbec/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw= google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= -google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= -google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s= -google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= -google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= +google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0= +google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I= +google.golang.org/genproto/googleapis/api v0.0.0-20260615183401-62b3387ff324 h1:g0RAkxK/smSu/iRwC/KIX1mwUoVJtk2OjbgaeS4DmUM= +google.golang.org/genproto/googleapis/api v0.0.0-20260615183401-62b3387ff324/go.mod h1:Z4WJ5pJOYWFWcHEQUelD5QaZDknIQkpIL/+fyJOT9+A= google.golang.org/genproto/googleapis/rpc v0.0.0-20260615183401-62b3387ff324 h1:9HZDLIdYBJXAnaFOr9WHrKVycfpY+75s9HGadC0305A= google.golang.org/genproto/googleapis/rpc v0.0.0-20260615183401-62b3387ff324/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= @@ -2319,13 +2354,13 @@ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= -gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.67.2 h1:JtOSMb9OuaCZKr7h5D/h6iii14sK0hLbplTc6frx4Ss= +gopkg.in/ini.v1 v1.67.2/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= @@ -2354,18 +2389,18 @@ honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0= howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM= howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= -k8s.io/api v0.34.3 h1:D12sTP257/jSH2vHV2EDYrb16bS7ULlHpdNdNhEw2S4= -k8s.io/api v0.34.3/go.mod h1:PyVQBF886Q5RSQZOim7DybQjAbVs8g7gwJNhGtY5MBk= -k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE= -k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= -k8s.io/client-go v0.34.3 h1:wtYtpzy/OPNYf7WyNBTj3iUA0XaBHVqhv4Iv3tbrF5A= -k8s.io/client-go v0.34.3/go.mod h1:OxxeYagaP9Kdf78UrKLa3YZixMCfP6bgPwPwNBQBzpM= -k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= -k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA= -k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts= -k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= -k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/api v0.35.3 h1:pA2fiBc6+N9PDf7SAiluKGEBuScsTzd2uYBkA5RzNWQ= +k8s.io/api v0.35.3/go.mod h1:9Y9tkBcFwKNq2sxwZTQh1Njh9qHl81D0As56tu42GA4= +k8s.io/apimachinery v0.35.3 h1:MeaUwQCV3tjKP4bcwWGgZ/cp/vpsRnQzqO6J6tJyoF8= +k8s.io/apimachinery v0.35.3/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= +k8s.io/client-go v0.35.3 h1:s1lZbpN4uI6IxeTM2cpdtrwHcSOBML1ODNTCCfsP1pg= +k8s.io/client-go v0.35.3/go.mod h1:RzoXkc0mzpWIDvBrRnD+VlfXP+lRzqQjCmKtiwZ8Q9c= +k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= +k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= @@ -2404,8 +2439,8 @@ rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8 rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= -sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= diff --git a/internal/cortex/querier/series/series_set.go b/internal/cortex/querier/series/series_set.go index a870b866a00..86863889078 100644 --- a/internal/cortex/querier/series/series_set.go +++ b/internal/cortex/querier/series/series_set.go @@ -137,6 +137,10 @@ func (c *concreteSeriesIterator) AtT() int64 { return t } +func (c *concreteSeriesIterator) AtST() int64 { + return 0 +} + func (c *concreteSeriesIterator) Next() chunkenc.ValueType { c.cur++ @@ -185,6 +189,10 @@ func (errIterator) AtT() int64 { return 0 } +func (errIterator) AtST() int64 { + return 0 +} + func (e errIterator) Err() error { return e.err } @@ -264,6 +272,10 @@ func (d DeletedSeriesIterator) AtT() int64 { return t } +func (d DeletedSeriesIterator) AtST() int64 { + return d.itr.AtST() +} + func (d DeletedSeriesIterator) Next() chunkenc.ValueType { for valueType := d.itr.Next(); valueType != chunkenc.ValNone; valueType = d.itr.Next() { ts, _ := d.itr.At() diff --git a/internal/cortex/util/metrics_helper.go b/internal/cortex/util/metrics_helper.go index 27d13207a71..5e5f39e676f 100644 --- a/internal/cortex/util/metrics_helper.go +++ b/internal/cortex/util/metrics_helper.go @@ -14,7 +14,7 @@ import ( "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" "github.com/prometheus/prometheus/model/labels" - tsdb_errors "github.com/prometheus/prometheus/tsdb/errors" + tsdb_errors "github.com/thanos-io/thanos/internal/tsdberrors" util_log "github.com/thanos-io/thanos/internal/cortex/util/log" ) diff --git a/internal/promql-engine/.github/dependabot.yaml b/internal/promql-engine/.github/dependabot.yaml new file mode 100644 index 00000000000..79e177b4355 --- /dev/null +++ b/internal/promql-engine/.github/dependabot.yaml @@ -0,0 +1,12 @@ +--- +version: 2 +updates: + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 20 + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: weekly \ No newline at end of file diff --git a/internal/promql-engine/.github/workflows/docs.yaml b/internal/promql-engine/.github/workflows/docs.yaml new file mode 100644 index 00000000000..3278e02732f --- /dev/null +++ b/internal/promql-engine/.github/workflows/docs.yaml @@ -0,0 +1,27 @@ +name: docs + +on: + push: + branches: + - main + tags: + - '*' + pull_request: + +jobs: + check: + runs-on: ubuntu-latest + name: Documentation check + env: + GOBIN: /tmp/.bin + steps: + - name: Checkout code into the Go module directory. + uses: actions/checkout@v4 + + - name: Install Go + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + with: + go-version-file: go.mod + + - name: Check docs + run: make check-docs diff --git a/internal/promql-engine/.github/workflows/test.yaml b/internal/promql-engine/.github/workflows/test.yaml new file mode 100644 index 00000000000..51490c72b89 --- /dev/null +++ b/internal/promql-engine/.github/workflows/test.yaml @@ -0,0 +1,113 @@ +name: Test + +on: + push: + branches: [ main ] + pull_request: + branches: [ main, update-prometheus-3.0 ] + +jobs: + skip-check: + name: Skip check + continue-on-error: true + runs-on: ubuntu-latest + outputs: + should_skip: ${{ steps.skip-check.outputs.should_skip }} + permissions: + actions: write + contents: read + steps: + - id: skip-check + uses: fkirc/skip-duplicate-actions@v4 + with: + do_not_skip: '["schedule", "workflow_dispatch"]' + paths: |- + [ + "**.go", + ".github/workflows/test.yml", + "go.mod", + "go.sum" + ] + skip_after_successful_duplicate: false + + lint: + runs-on: ubuntu-latest + name: Linters (Static Analysis) for Go + env: + GOBIN: /tmp/.bin + steps: + - name: Checkout code into the Go module directory. + uses: actions/checkout@v4 + + - name: Install Go + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + with: + go-version-file: go.mod + cache: true + + - name: Cache binary dependencies + uses: actions/cache@v4 + with: + path: /tmp/.bin + key: ${{ runner.os }}-binaries-${{ hashFiles('**/go.sum', '.bingo/**/*.sum') }} + restore-keys: | + ${{ runner.os }}-binaries- + + - name: Format + run: make format + + - name: Lint + run: make lint + test: + runs-on: ubuntu-latest + name: Run tests + env: + GOBIN: /tmp/.bin + steps: + - name: Check out code into the Go module directory. + uses: actions/checkout@v4 + + - name: Install Go. + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + with: + go-version-file: go.mod + cache: true + + - name: Run unit tests + run: make test + + test-tag-slicelabels: + runs-on: ubuntu-latest + name: Run tests --tags=slicelabels + env: + GOBIN: /tmp/.bin + steps: + - name: Check out code into the Go module directory. + uses: actions/checkout@v4 + + - name: Install Go. + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + with: + go-version-file: go.mod + cache: true + + - name: Run unit tests + run: make test-slicelabels + + fuzz: + runs-on: ubuntu-latest + name: Run fuzz + env: + GOBIN: /tmp/.bin + steps: + - name: Check out code into the Go module directory. + uses: actions/checkout@v4 + + - name: Install Go. + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + with: + go-version-file: go.mod + cache: true + + - name: Run fuzzing + run: make fuzz diff --git a/internal/promql-engine/.gitignore b/internal/promql-engine/.gitignore new file mode 100644 index 00000000000..0d8136f8913 --- /dev/null +++ b/internal/promql-engine/.gitignore @@ -0,0 +1,17 @@ +.envrc +.bin + +/vendor/ + +/.idea +/*.iml + +tmp/ +examples/tmp/ + +# Ignore the MacOS Trash (DS-Store) +.DS_Store + +# Ignore benchmarking output +benchmarks/ +engine.test diff --git a/internal/promql-engine/.golangci.yml b/internal/promql-engine/.golangci.yml new file mode 100644 index 00000000000..d41cb167739 --- /dev/null +++ b/internal/promql-engine/.golangci.yml @@ -0,0 +1,77 @@ +# This file contains all available configuration options +# with their default values. + +# options for analysis running +run: + # timeout for analysis, e.g. 30s, 5m, default is 1m + timeout: 5m + + # exit code when at least one issue was found, default is 1 + issues-exit-code: 1 + +# output configuration options +output: + # colored-line-number|line-number|json|tab|checkstyle, default is "colored-line-number" + formats: + - format: colored-line-number + + # print lines of code with issue, default is true + print-issued-lines: true + + # print linter name in the end of issue text, default is true + print-linter-name: true + +linters: + enable: + # Sorted alphabetically. + - errcheck + - godot + - gofmt + - gci + - gosimple + - govet + - ineffassign + - misspell + - staticcheck + - unparam + - unused + - copyloopvar + - promlinter + +linters-settings: + errcheck: + exclude-functions: + - (github.com/go-kit/log.Logger).Log + - fmt.Fprintln + - fmt.Fprint + misspell: + locale: US + staticcheck: + checks: + - "all" + - "-SA1019" # Ignore deprecated warnings (labels.MetricName, LabelName.IsValid, etc.) + gci: + sections: + - standard + - prefix(github.com/thanos-io) + - default + - blank + - dot + skip-generated: false + custom-order: true + +issues: + exclude-rules: + # We don't check metrics naming in the tests. + - path: _test\.go + linters: + - promlinter + + # which dirs to skip: they won't be analyzed; + # can use regexp here: generated.*, regexp is applied on full path; + # default value is empty list, but next dirs are always skipped independently + # from this option's value: + # vendor$, third_party$, testdata$, examples$, Godeps$, builtin$ + exclude-dirs: + - vendor + - parser diff --git a/internal/promql-engine/.mdox.validate.yaml b/internal/promql-engine/.mdox.validate.yaml new file mode 100644 index 00000000000..15afd4eab77 --- /dev/null +++ b/internal/promql-engine/.mdox.validate.yaml @@ -0,0 +1,25 @@ +version: 1 + +validators: + # Validators to skip checking PR/issue links of Thanos, Prometheus and Cortex. + - regex: '(^http[s]?:\/\/)(www\.)?(github\.com\/)thanos-io\/thanos(\/pull\/|\/issues\/)' + type: 'githubPullsIssues' + - regex: '(^http[s]?:\/\/)(www\.)?(github\.com\/)prometheus\/prometheus(\/pull\/|\/issues\/)' + type: 'githubPullsIssues' + - regex: '(^http[s]?:\/\/)(www\.)?(github\.com\/)cortexproject\/cortex(\/pull\/|\/issues\/)' + type: 'githubPullsIssues' + # Ignore Thanos release links. + - regex: '(^http[s]?:\/\/)(www\.)?(github\.com\/)thanos-io\/thanos(\/releases\/)' + type: 'ignore' + # Causes http stream errors with statuscode 0 sometimes. But is safe to skip. + - regex: 'slack\.cncf\.io' + type: 'ignore' + # 301 errors even when curl-ed. + - regex: 'envoyproxy\.io' + type: 'ignore' + # couldn't reach even when curl-ed. + - regex: 'cloud\.baidu\.com' + type: 'ignore' + # 403 when curl-ed from GitHub actions, though not from a developer machine. Likely due to secondary rate limits. + - regex: 'docs\.github\.com' + type: 'ignore' diff --git a/internal/promql-engine/COPYRIGHT b/internal/promql-engine/COPYRIGHT new file mode 100644 index 00000000000..8bd4235c369 --- /dev/null +++ b/internal/promql-engine/COPYRIGHT @@ -0,0 +1,2 @@ +Copyright (c) The Thanos Community Authors. +Licensed under the Apache License 2.0. \ No newline at end of file diff --git a/internal/promql-engine/LICENSE b/internal/promql-engine/LICENSE new file mode 100644 index 00000000000..8dada3edaf5 --- /dev/null +++ b/internal/promql-engine/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/internal/promql-engine/Makefile b/internal/promql-engine/Makefile new file mode 100644 index 00000000000..157f3b71fee --- /dev/null +++ b/internal/promql-engine/Makefile @@ -0,0 +1,133 @@ +FILES_TO_FMT ?= $(shell find . -path ./vendor -prune -o -name '*.go' -print) +MDOX_VALIDATE_CONFIG ?= .mdox.validate.yaml + +# if macos, use gsed +SED ?= $(shell which gsed 2>/dev/null || which sed) +BENCHSTAT = go tool -modfile go.tools.mod benchstat +MDOX = go tool -modfile go.tools.mod mdox +GCI = go tool -modfile go.tools.mod gci +FAILLINT = go tool -modfile go.tools.mod faillint +GOLANGCI_LINT = go tool -modfile go.tools.mod golangci-lint +MODERNIZE = go tool -modfile go.tools.mod modernize +COPYRIGHT = go run github.com/efficientgo/tools/copyright@v0.0.0-20220225185207-fe763185946b + +GOMODULES = $(shell go list ./...) + +define require_clean_work_tree + @git update-index -q --ignore-submodules --refresh + + @if ! git diff-files --quiet --ignore-submodules --; then \ + echo >&2 "cannot $1: you have unstaged changes."; \ + git diff-files --name-status -r --ignore-submodules -- >&2; \ + echo >&2 "Please commit or stash them."; \ + exit 1; \ + fi + + @if ! git diff-index --cached --quiet HEAD --ignore-submodules --; then \ + echo >&2 "cannot $1: your index contains uncommitted changes."; \ + git diff-index --cached --name-status -r --ignore-submodules HEAD -- >&2; \ + echo >&2 "Please commit or stash them."; \ + exit 1; \ + fi +endef + +help: ## Displays help. + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n\nTargets:\n"} /^[a-z0-9A-Z_-]+:.*?##/ { printf " \033[36m%-10s\033[0m %s\n", $$1, $$2 }' $(MAKEFILE_LIST) + +.PHONY: test +test: ## Runs all Go unit tests. + @echo ">> running unit tests (without cache)" + @rm -rf /tmp/engine-cache + @GORACE=atexit_sleep_ms=0 GOCACHE=/tmp/engine-cache go test -race -timeout=10m $(GOMODULES); + +.PHONY: test-fast +test-fast: ## Runs all Go unit tests without race detector. + @echo ">> running unit tests without race detection (with cache)" + @go test -count=1 -timeout=10m $(GOMODULES); + +.PHONY: test-slicelabels +test-slicelabels: ## Runs all Go unit tests with slicelabels flag. + @export GOCACHE=/tmp/cache + @echo ">> running unit tests with slicelabels flag (without cache)" + @rm -rf $(GOCACHE) + @go test -race --tags=slicelabels -timeout=10m $(GOMODULES); + +.PHONY: fuzz +fuzz: ## Runs selected fuzzing tests + @export GOCACHE=/tmp/cache + @echo ">> running fuzz tests (without cache)" + @rm -rf $(GOCACHE) + @go test github.com/thanos-io/promql-engine/engine -run None -fuzz FuzzEnginePromQLSmithInstantQuery -fuzztime=90s -fuzzminimizetime 0x; + @go test github.com/thanos-io/promql-engine/engine -run None -fuzz FuzzNativeHistogramQuery -fuzztime=90s -fuzzminimizetime 0x; + @go test github.com/thanos-io/promql-engine/logicalplan -run None -fuzz FuzzNodesMarshalJSON -fuzztime=30s -fuzzminimizetime 0x; + +.PHONY: deps +deps: ## Ensures fresh go.mod and go.sum. + @go mod tidy + @go mod verify + +.PHONY: docs +docs: ## Generates docs for all thanos commands, localise links, ensure GitHub format. +docs: + @echo ">> generating docs" + $(MDOX) fmt README.md + $(MAKE) white-noise-cleanup + +.PHONY: check-docs +check-docs: ## Checks docs against discrepancy with flags, links, white noise. +check-docs: + @echo ">> checking docs" + $(MDOX) fmt -l --links.validate.config-file=$(MDOX_VALIDATE_CONFIG) README.md + $(MAKE) white-noise-cleanup + $(call require_clean_work_tree,'run make docs and commit changes') + +.PHONY: format +format: + @echo ">> formatting promql tests" + @go run scripts/testvet/main.go -json -fix ./... + @echo ">> formatting imports" + @$(GCI) write $(shell find . -name "*.go") -s "standard" -s "prefix(github.com/thanos-io)" -s "default" -s "blank" -s "dot" --custom-order + @$(MODERNIZE) -fix ./... + +.PHONY:lint +lint: format deps docs + $(call require_clean_work_tree,'detected not clean work tree before running lint, previous job changed something?') + @echo ">> verifying modules being imported" + @# TODO(bwplotka): Add, Printf, DefaultRegisterer, NewGaugeFunc and MustRegister once exception are accepted. + @$(FAILLINT) -paths "errors=github.com/efficientgo/core/errors,\ +fmt.{Errorf}=github.com/efficientgo/core/errors.{Wrap,Wrapf},\ +github.com/prometheus/prometheus/pkg/testutils=github.com/efficientgo/core/testutil,\ +github.com/stretchr/testify=github.com/efficientgo/core/testutil" $(GOMODULES) + @$(FAILLINT) -paths "fmt.{Print,Println,Errorf}" -ignore-tests $(GOMODULES) + @echo ">> linting all of the Go files GOGC=${GOGC}" + @$(GOLANGCI_LINT) run + @echo ">> ensuring Copyright headers" + @$(COPYRIGHT) $(shell find . -name "*.go") + @echo ">> ensuring modern go style" + @$(MODERNIZE) -test ./... + $(call require_clean_work_tree,'detected files without copyright, run make lint and commit changes') + +.PHONY: white-noise-cleanup +white-noise-cleanup: ## Cleans up white noise in docs. +white-noise-cleanup: + @echo ">> cleaning up white noise" + @find . -type f \( -name "*.md" \) | SED_BIN="$(SED)" xargs scripts/cleanup-white-noise.sh + +benchmarks: + @mkdir -p benchmarks + +.PHONY: bench-old +bench-old: benchmarks + @echo "Benchmarking old engine" + @go test ./... -bench 'BenchmarkRangeQuery/.*/old_engine' -run none -count 5 | sed -u 's/\/old_engine//' > benchmarks/old.out + @go test ./... -bench 'BenchmarkNativeHistograms/.*/old_engine' -run none -count 5 | sed -u 's/\/old_engine//' >> benchmarks/old.out + +.PHONY: bench-new +bench-new: benchmarks + @echo "Benchmarking new engine" + @go test ./... -bench 'BenchmarkRangeQuery/.*/new_engine' -run none -count 5 | sed -u 's/\/new_engine//' > benchmarks/new.out + @go test ./... -bench 'BenchmarkNativeHistograms/.*/new_engine' -run none -count 5 | sed -u 's/\/new_engine//' >> benchmarks/new.out + +.PHONY: benchmark +benchmark: bench-old bench-new + @$(BENCHSTAT) benchmarks/old.out benchmarks/new.out diff --git a/internal/promql-engine/README.md b/internal/promql-engine/README.md new file mode 100644 index 00000000000..3a8da25cdad --- /dev/null +++ b/internal/promql-engine/README.md @@ -0,0 +1,342 @@ +# PromQL Query Engine + +A multi-threaded implementation of a PromQL Query Engine based on the [Volcano/Iterator model](https://paperhub.s3.amazonaws.com/dace52a42c07f7f8348b08dc2b186061.pdf). + +The project is currently under active development. + +## Roadmap + +The engine intends to have full compatibility with the original engine used in Prometheus. Since implementing the full specification will take time, we aim to add support for most commonly used expressions. Instructions on using the engine will be added after we have enough confidence in its correctness. If the engine encounters an expression it does not support it will return an error that can be tested with `engine.IsUnimplemented(err)`, the calling code is expected to handle this fallback. + +The following table shows operations which are currently supported by the engine + +| Type | Supported | Priority | +|------------------------|-------------------------------------------------------------------------|----------| +| Binary expressions | Full support | | +| Histograms | Full support | | +| Subqueries | Full support | | +| Aggregations | Full support | | +| Aggregations over time | Full support except for `quantile_over_time` with non-constant argument | Medium | +| Functions | Full support except for `predict_linear` with non-constant argument | Medium | + +## Design + +At the beginning of a PromQL query execution, the query engine computes a physical plan consisting of multiple independent operators, each responsible for calculating one part of the query expression. + +Operators are assembled in a tree-like structure with every operator calling `Next()` on its dependents until there is no more data to be returned. The result of the `Next()` function is a *column vector* (also called a *step vector*) with elements in the vector representing samples with the same timestamp from different time series. + +

+ +

+ +This model allows for samples from individual time series to flow one execution step at a time from the left-most operators to the one at the very right. Since most PromQL expressions are aggregations, samples are reduced in number as they are pulled by the operators on the right. Because of this, samples from the original timeseries can be decoded and kept in memory in batches instead of being fully expanded. + +In addition to operators that have a one-to-one mapping with PromQL constructs, the Volcano model also describes so-called Exchange operators which can be used for flow control and optimizations, such as concurrency or batched selects. An example of an *Exchange* operator is described in the [Intra-operator parallelism](#intra-operator-parallelism) section. + +### Inter-operator parallelism + +Since operators are independent and rely on a common interface for pulling data, they can be run in parallel to each other. As soon as one operator has processed data from an evaluation step, it can pass the result onward so that its upstream can immediately start working on it. + +

+ +

+ +### Intra-operator parallelism + +Parallelism can also be added within individual operators, using a parallel coalesce exchange operator. Such exchange operators are indistinguishable from regular operators to their upstreams since they respect the same `Next()` interface. + +

+ +

+ +### Memory management + +#### Step vector allocations + +One challenge with the streamed execution model is knowing how much memory to allocate in each operator for each step. + +To work around this issue, operators expose a `Series()` method which returns the labels for all time series that they will ever produce (for all `Next()` calls). Operators at the very bottom of the tree, like vector and matrix selectors, have this information since they are responsible for loading data from storage. Other operators can then call `Series()` on the downstream operator and pre-compute all possible outputs. + +Even though this might look like an expensive operation, its cost is identical to just one evaluation step. Knowing sizes of input and output vectors also allows us to: +* allocate memory very precisely by properly sizing vector pools (see section below), +* use arrays instead of maps for indexing data, leading to faster execution times due to having less allocations and using index-based lookups, and +* use tight loops in operators by eliminating conditional statements associated with maps. + +#### Vector pools + +Since time series are decoded one step at a time, vectors between execution steps can be recycled manually instead of relying on the garbage collector. Each operator has its own pool that it uses to allocate new step vectors and send results to its upstream. Whenever the upstream operator is finished with processing a step vector, it will return that vector to the pool of its downstream so that it can be reused again for subsequent steps. + +#### Memory limits + +There are currently no mechanisms to apply memory limits to queries within the engine. This is a highly desirable feature, and we would like to explore ways in which we can support it. + +### Concurrency control + +The current implementation uses goroutines very liberally which means the query will use as many cores as possible. Limiting the number of cores which a query can use is not yet implemented but we would eventually like to have support for it. + +### Plan optimization + +Each PromQL query is initially treated as a declarative (logical) plan and is optimized before execution. The engine currently supports several optimizers, some of which are enabled by default and others need to be explicitly opted-into. Optimizers implement the [Optimizer](https://pkg.go.dev/github.com/thanos-io/promql-engine/logicalplan#Optimizer) interface and all implementations can be found in the [logicalplan](https://pkg.go.dev/github.com/thanos-io/promql-engine/logicalplan) package. + +### Extensibility + +The engine can be extended through custom optimizers which can be injected at instantiation. These optimizers can be used to either rearrange the logical nodes into a new plan or to inject new nodes altogether. + +It is also possible to modify the actual execution of a query by injecting a node implementing the `UserDefinedOperator` interface. This node type has a `MakeExecutionOperator` method which can be used to control which execution operator should be instantiated for the logical node. + +## Distributed execution mode + +The engine supports a distributed mode where aggregations can be delegated to multiple remote engines, each responsible for an independent dataset. This mode is currently implemented through an optimizer which rewrites a query as a combination of multiple remote and one local aggregation. For example, when two remote engines are available, a query like: + +``` +sum(rate(http_request_total[4m])) +``` + +would be rewritten as + +``` +sum( + coalesce( + sum(rate(http_request_total[4m])) # remote engine 1 + sum(rate(http_request_total[4m])) # remote engine 2 + ) +) +``` + +The inner aggregations are forwarded to remote engines and the global result is completed in memory. + +An engine using the distributed mode can be created through the `NewDistributedEngine` function. The user is expected to pass an implementation of `RemoteEndpoints` which has a single `Engines()` method. When invoked, `Engines()` should return all remote engines that can be used for a single query. The `Engines()` method is called separately for each individual query which allows the `RemoteEndpoints` implementation to do continuous service discovery and inject engines as they become available. + +The interfaces used for remote execution can be found in [api](https://pkg.go.dev/github.com/thanos-io/promql-engine/api) package. Note that the `RemoteEngine` interface has a `NewRangeQuery` method, similar to the one in the Prometheus [v1.QueryEngine](https://pkg.go.dev/github.com/prometheus/prometheus@v0.42.0/web/api/v1#QueryEngine) interface. It is up to the user of the library to implement this method as they see fit. An example implementation could be to forward the query to an HTTP `/api/v1/query_range` endpoint of a Prometheus instance. In Thanos, this method is implemented as a gRPC call to a Thanos Querier. + +For more details on the overall design, please refer to the [proposal](https://github.com/thanos-io/thanos/blob/main/docs/proposals-done/202301-distributed-query-execution.md) in the Thanos project. + +## Differences from Prometheus PromQL Engine + +The Thanos PromQL engine follows the same PromQL specification but may differ in some implementation details and edge case behaviors. These differences are often intentional and stem from design choices optimized for performance and flexibility. + +| Feature/Function | Prometheus Engine Behavior | Thanos Engine Behavior | +|------------------|---------------------------------------|---------------------------------------------------------------------------------------------------------------------------------| +| `sort()` | Filters out native histogram samples. | Retains native histogram samples. `sort()` is treated as a presentation-layer operation and does not alter the underlying data. | + +## Continuous benchmark + +If you are interested in the benchmark results captured by continuous benchmark, please check [here](https://thanos-io.github.io/promql-engine/dev/bench/). + +## Latest benchmarks + +These are the latest benchmarks captured on an Apple M1 Pro processor. + +Note that memory usage is higher when executing a query with parallelism greater than 1. This is due to the fact that the engine is able to execute multiple operations at once (e.g. decode chunks from multiple series at the same time), which requires using independent buffers for each parallel operation. + +Single core benchmarks + +```markdown +name old time/op new time/op delta +RangeQuery/vector_selector 33.5ms ± 3% 43.4ms ± 3% +29.59% (p=0.008 n=5+5) +RangeQuery/sum 46.6ms ± 1% 34.3ms ± 2% -26.37% (p=0.008 n=5+5) +RangeQuery/sum_by_pod 145ms ± 1% 46ms ± 3% -68.36% (p=0.008 n=5+5) +RangeQuery/topk 46.7ms ± 2% 37.0ms ± 6% -20.79% (p=0.008 n=5+5) +RangeQuery/bottomk 46.9ms ± 1% 35.3ms ± 7% -24.72% (p=0.008 n=5+5) +RangeQuery/rate 65.7ms ± 1% 72.2ms ± 2% +9.88% (p=0.008 n=5+5) +RangeQuery/sum_rate 76.6ms ± 1% 61.9ms ± 1% -19.16% (p=0.008 n=5+5) +RangeQuery/sum_by_rate 180ms ± 3% 74ms ± 7% -58.94% (p=0.008 n=5+5) +RangeQuery/quantile_with_variable_parameter 263ms ± 6% 99ms ± 3% -62.38% (p=0.008 n=5+5) +RangeQuery/binary_operation_with_one_to_one 119ms ± 1% 31ms ± 3% -74.20% (p=0.008 n=5+5) +RangeQuery/binary_operation_with_many_to_one 396ms ± 1% 69ms ± 1% -82.52% (p=0.008 n=5+5) +RangeQuery/binary_operation_with_vector_and_scalar 241ms ± 1% 51ms ± 1% -78.85% (p=0.008 n=5+5) +RangeQuery/unary_negation 35.6ms ± 2% 46.6ms ± 5% +31.00% (p=0.008 n=5+5) +RangeQuery/vector_and_scalar_comparison 205ms ± 3% 57ms ± 4% -72.48% (p=0.008 n=5+5) +RangeQuery/positive_offset_vector 33.2ms ±10% 43.2ms ± 3% +30.20% (p=0.008 n=5+5) +RangeQuery/at_modifier_ 18.7ms ± 3% 18.0ms ± 5% ~ (p=0.095 n=5+5) +RangeQuery/at_modifier_with_positive_offset_vector 17.9ms ± 2% 17.2ms ± 2% -3.78% (p=0.008 n=5+5) +RangeQuery/clamp 252ms ± 7% 62ms ± 4% -75.28% (p=0.008 n=5+5) +RangeQuery/clamp_min 253ms ± 5% 59ms ± 3% -76.75% (p=0.008 n=5+5) +RangeQuery/complex_func_query 455ms ± 2% 68ms ± 3% -85.06% (p=0.008 n=5+5) +RangeQuery/func_within_func_query 265ms ± 3% 89ms ± 3% -66.33% (p=0.008 n=5+5) +RangeQuery/aggr_within_func_query 273ms ± 1% 91ms ± 2% -66.43% (p=0.008 n=5+5) +RangeQuery/histogram_quantile 579ms ± 2% 204ms ± 5% -64.68% (p=0.008 n=5+5) +RangeQuery/sort 299ms ± 1% 43ms ± 2% -85.57% (p=0.008 n=5+5) +RangeQuery/sort_desc 294ms ± 2% 44ms ± 3% -84.97% (p=0.008 n=5+5) +NativeHistograms/selector 620ms ± 1% 662ms ± 5% +6.79% (p=0.008 n=5+5) +NativeHistograms/sum 1.21s ± 7% 1.01s ± 1% -16.42% (p=0.008 n=5+5) +NativeHistograms/rate 4.57s ± 3% 4.49s ± 1% ~ (p=0.310 n=5+5) +NativeHistograms/sum_rate 5.04s ± 1% 4.79s ± 1% -4.99% (p=0.008 n=5+5) +NativeHistograms/histogram_sum 930ms ± 2% 1068ms ± 6% +14.77% (p=0.008 n=5+5) +NativeHistograms/histogram_count 980ms ± 7% 1059ms ± 7% ~ (p=0.095 n=5+5) +NativeHistograms/histogram_quantile 1.20s ± 1% 1.02s ± 4% -14.80% (p=0.008 n=5+5) + +name old alloc/op new alloc/op delta +RangeQuery/vector_selector 24.5MB ± 0% 38.1MB ± 0% +55.28% (p=0.008 n=5+5) +RangeQuery/sum 7.13MB ± 0% 10.20MB ± 0% +43.11% (p=0.008 n=5+5) +RangeQuery/sum_by_pod 79.9MB ± 0% 22.7MB ± 0% -71.61% (p=0.008 n=5+5) +RangeQuery/topk 7.38MB ± 0% 12.68MB ± 0% +71.84% (p=0.008 n=5+5) +RangeQuery/bottomk 7.44MB ± 0% 12.72MB ± 0% +71.02% (p=0.029 n=4+4) +RangeQuery/rate 25.6MB ± 0% 41.0MB ± 0% +60.30% (p=0.008 n=5+5) +RangeQuery/sum_rate 8.19MB ± 0% 13.09MB ± 0% +59.73% (p=0.016 n=5+4) +RangeQuery/sum_by_rate 80.7MB ± 0% 25.5MB ± 0% -68.43% (p=0.008 n=5+5) +RangeQuery/quantile_with_variable_parameter 174MB ± 0% 39MB ± 0% -77.60% (p=0.016 n=5+4) +RangeQuery/binary_operation_with_one_to_one 16.5MB ± 0% 21.6MB ± 0% +30.83% (p=0.008 n=5+5) +RangeQuery/binary_operation_with_many_to_one 72.0MB ± 0% 55.8MB ± 0% -22.54% (p=0.008 n=5+5) +RangeQuery/binary_operation_with_vector_and_scalar 39.1MB ± 0% 40.2MB ± 0% +2.80% (p=0.008 n=5+5) +RangeQuery/unary_negation 25.6MB ± 0% 39.4MB ± 0% +54.13% (p=0.008 n=5+5) +RangeQuery/vector_and_scalar_comparison 37.7MB ± 0% 39.9MB ± 0% +5.63% (p=0.008 n=5+5) +RangeQuery/positive_offset_vector 23.0MB ± 0% 36.6MB ± 0% +58.83% (p=0.008 n=5+5) +RangeQuery/at_modifier_ 39.8MB ± 0% 33.1MB ± 0% -16.75% (p=0.008 n=5+5) +RangeQuery/at_modifier_with_positive_offset_vector 39.6MB ± 0% 32.9MB ± 0% -16.83% (p=0.016 n=4+5) +RangeQuery/clamp 39.2MB ± 0% 38.5MB ± 0% -1.69% (p=0.016 n=5+4) +RangeQuery/clamp_min 39.1MB ± 0% 38.5MB ± 0% -1.75% (p=0.008 n=5+5) +RangeQuery/complex_func_query 53.8MB ± 0% 40.6MB ± 0% -24.54% (p=0.016 n=5+4) +RangeQuery/func_within_func_query 40.2MB ± 0% 41.1MB ± 0% +2.18% (p=0.008 n=5+5) +RangeQuery/aggr_within_func_query 40.2MB ± 0% 41.1MB ± 0% +2.19% (p=0.008 n=5+5) +RangeQuery/histogram_quantile 47.5MB ± 0% 57.9MB ± 0% +21.88% (p=0.016 n=5+4) +RangeQuery/sort 37.8MB ± 0% 38.1MB ± 0% +0.67% (p=0.008 n=5+5) +RangeQuery/sort_desc 37.8MB ± 0% 38.1MB ± 0% +0.67% (p=0.008 n=5+5) +NativeHistograms/selector 761MB ± 0% 774MB ± 0% +1.72% (p=0.016 n=4+5) +NativeHistograms/sum 943MB ± 0% 931MB ± 0% -1.21% (p=0.008 n=5+5) +NativeHistograms/rate 2.86GB ± 0% 2.87GB ± 0% +0.53% (p=0.029 n=4+4) +NativeHistograms/sum_rate 3.04GB ± 0% 3.03GB ± 0% -0.41% (p=0.016 n=4+5) +NativeHistograms/histogram_sum 786MB ± 0% 775MB ± 0% -1.42% (p=0.008 n=5+5) +NativeHistograms/histogram_count 787MB ± 0% 774MB ± 0% -1.63% (p=0.016 n=5+4) +NativeHistograms/histogram_quantile 942MB ± 0% 932MB ± 0% -1.14% (p=0.008 n=5+5) + +name old allocs/op new allocs/op delta +RangeQuery/vector_selector 99.1k ± 0% 111.9k ± 0% +12.96% (p=0.016 n=5+4) +RangeQuery/sum 103k ± 0% 107k ± 0% +3.41% (p=0.016 n=5+4) +RangeQuery/sum_by_pod 598k ± 0% 202k ± 0% -66.28% (p=0.008 n=5+5) +RangeQuery/topk 108k ± 0% 114k ± 0% +5.18% (p=0.008 n=5+5) +RangeQuery/bottomk 109k ± 0% 116k ± 0% +6.20% (p=0.008 n=5+5) +RangeQuery/rate 111k ± 0% 136k ± 0% +22.33% (p=0.016 n=4+5) +RangeQuery/sum_rate 115k ± 0% 131k ± 0% +13.45% (p=0.008 n=5+5) +RangeQuery/sum_by_rate 608k ± 0% 226k ± 0% -62.89% (p=0.008 n=5+5) +RangeQuery/quantile_with_variable_parameter 1.67M ± 0% 0.58M ± 0% -65.23% (p=0.008 n=5+5) +RangeQuery/binary_operation_with_one_to_one 75.3k ± 0% 89.2k ± 0% +18.55% (p=0.008 n=5+5) +RangeQuery/binary_operation_with_many_to_one 637k ± 0% 173k ± 0% -72.86% (p=0.008 n=5+5) +RangeQuery/binary_operation_with_vector_and_scalar 117k ± 0% 116k ± 0% -1.24% (p=0.008 n=5+5) +RangeQuery/unary_negation 111k ± 0% 124k ± 0% +11.82% (p=0.008 n=5+5) +RangeQuery/vector_and_scalar_comparison 105k ± 0% 113k ± 0% +7.22% (p=0.008 n=5+5) +RangeQuery/positive_offset_vector 73.1k ± 0% 86.0k ± 0% +17.69% (p=0.008 n=5+5) +RangeQuery/at_modifier_ 74.1k ± 0% 62.6k ± 0% -15.53% (p=0.000 n=5+4) +RangeQuery/at_modifier_with_positive_offset_vector 68.1k ± 0% 56.6k ± 0% -16.90% (p=0.000 n=5+4) +RangeQuery/clamp 118k ± 0% 116k ± 0% -1.69% (p=0.016 n=5+4) +RangeQuery/clamp_min 117k ± 0% 115k ± 0% -1.75% (p=0.008 n=5+5) +RangeQuery/complex_func_query 136k ± 0% 120k ± 0% -11.97% (p=0.016 n=5+4) +RangeQuery/func_within_func_query 130k ± 0% 137k ± 0% +5.40% (p=0.008 n=5+5) +RangeQuery/aggr_within_func_query 130k ± 0% 137k ± 0% +5.40% (p=0.008 n=5+5) +RangeQuery/histogram_quantile 617k ± 0% 656k ± 0% +6.29% (p=0.016 n=5+4) +RangeQuery/sort 106k ± 0% 112k ± 0% +6.10% (p=0.008 n=5+5) +RangeQuery/sort_desc 106k ± 0% 112k ± 0% +6.10% (p=0.008 n=5+5) +NativeHistograms/selector 9.63M ± 0% 9.64M ± 0% +0.14% (p=0.016 n=4+5) +NativeHistograms/sum 11.1M ± 0% 11.1M ± 0% +0.02% (p=0.008 n=5+5) +NativeHistograms/rate 34.1M ± 0% 34.1M ± 0% +0.08% (p=0.016 n=5+4) +NativeHistograms/sum_rate 35.6M ± 0% 35.6M ± 0% +0.05% (p=0.008 n=5+5) +NativeHistograms/histogram_sum 9.65M ± 0% 9.64M ± 0% -0.04% (p=0.008 n=5+5) +NativeHistograms/histogram_count 9.65M ± 0% 9.64M ± 0% -0.04% (p=0.008 n=5+5) +NativeHistograms/histogram_quantile 11.1M ± 0% 11.1M ± 0% +0.02% (p=0.008 n=5+5) +``` + +Multi-core (8 core) benchmarks + +```markdown +name old time/op new time/op delta +RangeQuery/vector_selector-8 31.1ms ± 1% 14.7ms ± 1% -52.66% (p=0.008 n=5+5) +RangeQuery/sum-8 49.3ms ± 2% 11.0ms ± 0% -77.74% (p=0.008 n=5+5) +RangeQuery/sum_by_pod-8 138ms ± 4% 15ms ± 0% -89.16% (p=0.016 n=5+4) +RangeQuery/topk-8 47.7ms ± 4% 11.0ms ± 0% -77.03% (p=0.008 n=5+5) +RangeQuery/bottomk-8 48.3ms ± 3% 11.1ms ± 5% -76.95% (p=0.008 n=5+5) +RangeQuery/rate-8 61.4ms ± 2% 21.1ms ± 3% -65.63% (p=0.008 n=5+5) +RangeQuery/sum_rate-8 77.9ms ± 1% 19.0ms ± 4% -75.63% (p=0.008 n=5+5) +RangeQuery/sum_by_rate-8 165ms ± 1% 22ms ± 3% -86.80% (p=0.008 n=5+5) +RangeQuery/quantile_with_variable_parameter-8 234ms ± 3% 25ms ± 1% -89.17% (p=0.008 n=5+5) +RangeQuery/binary_operation_with_one_to_one-8 121ms ± 2% 14ms ± 1% -88.53% (p=0.008 n=5+5) +RangeQuery/binary_operation_with_many_to_one-8 405ms ± 2% 30ms ± 1% -92.49% (p=0.008 n=5+5) +RangeQuery/binary_operation_with_vector_and_scalar-8 245ms ± 2% 20ms ± 0% -91.88% (p=0.008 n=5+5) +RangeQuery/unary_negation-8 32.3ms ± 3% 15.5ms ± 2% -52.10% (p=0.008 n=5+5) +RangeQuery/vector_and_scalar_comparison-8 206ms ± 2% 21ms ± 2% -89.78% (p=0.008 n=5+5) +RangeQuery/positive_offset_vector-8 27.6ms ± 1% 13.9ms ± 4% -49.83% (p=0.008 n=5+5) +RangeQuery/at_modifier_-8 12.6ms ± 2% 10.0ms ± 2% -20.88% (p=0.008 n=5+5) +RangeQuery/at_modifier_with_positive_offset_vector-8 12.0ms ± 3% 9.6ms ± 1% -19.73% (p=0.008 n=5+5) +RangeQuery/clamp-8 246ms ± 4% 31ms ± 4% -87.26% (p=0.008 n=5+5) +RangeQuery/clamp_min-8 251ms ± 4% 27ms ±17% -89.10% (p=0.008 n=5+5) +RangeQuery/complex_func_query-8 480ms ± 5% 38ms ± 5% -92.15% (p=0.008 n=5+5) +RangeQuery/func_within_func_query-8 279ms ± 1% 32ms ± 1% -88.59% (p=0.008 n=5+5) +RangeQuery/aggr_within_func_query-8 274ms ± 6% 32ms ± 2% -88.28% (p=0.008 n=5+5) +RangeQuery/histogram_quantile-8 545ms ± 5% 97ms ± 1% -82.15% (p=0.008 n=5+5) +RangeQuery/sort-8 301ms ± 7% 15ms ± 3% -94.92% (p=0.008 n=5+5) +RangeQuery/sort_desc-8 295ms ± 3% 15ms ± 1% -94.88% (p=0.008 n=5+5) +NativeHistograms/selector-8 417ms ± 3% 217ms ± 3% -47.95% (p=0.008 n=5+5) +NativeHistograms/sum-8 897ms ± 1% 271ms ± 2% -69.74% (p=0.008 n=5+5) +NativeHistograms/rate-8 3.76s ± 1% 1.27s ± 2% -66.30% (p=0.008 n=5+5) +NativeHistograms/sum_rate-8 4.24s ± 3% 1.27s ± 4% -70.12% (p=0.008 n=5+5) +NativeHistograms/histogram_sum-8 683ms ± 1% 429ms ± 2% -37.23% (p=0.008 n=5+5) +NativeHistograms/histogram_count-8 681ms ± 1% 423ms ± 1% -37.97% (p=0.008 n=5+5) +NativeHistograms/histogram_quantile-8 903ms ± 3% 268ms ± 2% -70.32% (p=0.008 n=5+5) + +name old alloc/op new alloc/op delta +RangeQuery/vector_selector-8 24.5MB ± 0% 38.6MB ± 0% +57.55% (p=0.008 n=5+5) +RangeQuery/sum-8 7.12MB ± 0% 9.89MB ± 0% +39.06% (p=0.008 n=5+5) +RangeQuery/sum_by_pod-8 79.9MB ± 0% 23.7MB ± 0% -70.36% (p=0.008 n=5+5) +RangeQuery/topk-8 7.43MB ± 0% 10.95MB ± 0% +47.39% (p=0.008 n=5+5) +RangeQuery/bottomk-8 7.46MB ± 0% 10.94MB ± 1% +46.61% (p=0.008 n=5+5) +RangeQuery/rate-8 25.6MB ± 0% 41.4MB ± 0% +61.73% (p=0.008 n=5+5) +RangeQuery/sum_rate-8 8.18MB ± 0% 12.67MB ± 0% +54.81% (p=0.008 n=5+5) +RangeQuery/sum_by_rate-8 80.7MB ± 0% 25.3MB ± 1% -68.68% (p=0.008 n=5+5) +RangeQuery/quantile_with_variable_parameter-8 174MB ± 0% 41MB ± 0% -76.60% (p=0.008 n=5+5) +RangeQuery/binary_operation_with_one_to_one-8 16.5MB ± 0% 22.4MB ± 0% +35.63% (p=0.008 n=5+5) +RangeQuery/binary_operation_with_many_to_one-8 72.0MB ± 0% 56.9MB ± 0% -20.99% (p=0.008 n=5+5) +RangeQuery/binary_operation_with_vector_and_scalar-8 39.1MB ± 0% 41.6MB ± 0% +6.39% (p=0.008 n=5+5) +RangeQuery/unary_negation-8 25.6MB ± 0% 40.0MB ± 0% +56.65% (p=0.008 n=5+5) +RangeQuery/vector_and_scalar_comparison-8 37.7MB ± 0% 41.3MB ± 0% +9.45% (p=0.008 n=5+5) +RangeQuery/positive_offset_vector-8 23.0MB ± 0% 37.1MB ± 0% +61.21% (p=0.008 n=5+5) +RangeQuery/at_modifier_-8 39.8MB ± 0% 33.1MB ± 0% -16.67% (p=0.008 n=5+5) +RangeQuery/at_modifier_with_positive_offset_vector-8 39.6MB ± 0% 33.0MB ± 0% -16.74% (p=0.008 n=5+5) +RangeQuery/clamp-8 39.1MB ± 0% 39.1MB ± 0% -0.22% (p=0.008 n=5+5) +RangeQuery/clamp_min-8 39.1MB ± 0% 39.0MB ± 0% -0.27% (p=0.008 n=5+5) +RangeQuery/complex_func_query-8 53.8MB ± 0% 41.9MB ± 0% -22.09% (p=0.008 n=5+5) +RangeQuery/func_within_func_query-8 40.2MB ± 0% 41.7MB ± 0% +3.68% (p=0.008 n=5+5) +RangeQuery/aggr_within_func_query-8 40.2MB ± 0% 41.6MB ± 0% +3.66% (p=0.008 n=5+5) +RangeQuery/histogram_quantile-8 47.5MB ± 0% 60.5MB ± 0% +27.26% (p=0.008 n=5+5) +RangeQuery/sort-8 37.8MB ± 0% 38.6MB ± 0% +2.12% (p=0.008 n=5+5) +RangeQuery/sort_desc-8 37.8MB ± 0% 38.6MB ± 0% +2.12% (p=0.016 n=4+5) +NativeHistograms/selector-8 761MB ± 0% 775MB ± 0% +1.90% (p=0.008 n=5+5) +NativeHistograms/sum-8 940MB ± 0% 933MB ± 0% -0.67% (p=0.008 n=5+5) +NativeHistograms/rate-8 2.86GB ± 0% 2.88GB ± 0% +0.61% (p=0.008 n=5+5) +NativeHistograms/sum_rate-8 3.04GB ± 0% 3.04GB ± 0% -0.27% (p=0.008 n=5+5) +NativeHistograms/histogram_sum-8 786MB ± 0% 777MB ± 0% -1.24% (p=0.008 n=5+5) +NativeHistograms/histogram_count-8 787MB ± 0% 777MB ± 0% -1.29% (p=0.008 n=5+5) +NativeHistograms/histogram_quantile-8 940MB ± 0% 934MB ± 0% -0.69% (p=0.008 n=5+5) + +name old allocs/op new allocs/op delta +RangeQuery/vector_selector-8 98.8k ± 0% 113.6k ± 0% +14.99% (p=0.008 n=5+5) +RangeQuery/sum-8 103k ± 0% 108k ± 0% +5.03% (p=0.008 n=5+5) +RangeQuery/sum_by_pod-8 598k ± 0% 204k ± 0% -65.95% (p=0.008 n=5+5) +RangeQuery/topk-8 109k ± 0% 116k ± 0% +6.76% (p=0.008 n=5+5) +RangeQuery/bottomk-8 110k ± 0% 115k ± 0% +4.46% (p=0.008 n=5+5) +RangeQuery/rate-8 111k ± 0% 138k ± 0% +24.06% (p=0.008 n=5+5) +RangeQuery/sum_rate-8 115k ± 0% 132k ± 0% +14.87% (p=0.016 n=4+5) +RangeQuery/sum_by_rate-8 608k ± 0% 227k ± 0% -62.64% (p=0.008 n=5+5) +RangeQuery/quantile_with_variable_parameter-8 1.66M ± 0% 0.58M ± 0% -64.99% (p=0.008 n=5+5) +RangeQuery/binary_operation_with_one_to_one-8 75.2k ± 0% 93.4k ± 0% +24.19% (p=0.008 n=5+5) +RangeQuery/binary_operation_with_many_to_one-8 637k ± 0% 177k ± 0% -72.24% (p=0.008 n=5+5) +RangeQuery/binary_operation_with_vector_and_scalar-8 117k ± 0% 118k ± 0% +0.65% (p=0.016 n=4+5) +RangeQuery/unary_negation-8 111k ± 0% 126k ± 0% +13.71% (p=0.008 n=5+5) +RangeQuery/vector_and_scalar_comparison-8 105k ± 0% 115k ± 0% +9.37% (p=0.008 n=5+5) +RangeQuery/positive_offset_vector-8 72.9k ± 0% 87.8k ± 0% +20.37% (p=0.029 n=4+4) +RangeQuery/at_modifier_-8 74.0k ± 0% 62.6k ± 0% -15.40% (p=0.008 n=5+5) +RangeQuery/at_modifier_with_positive_offset_vector-8 68.0k ± 0% 56.6k ± 0% -16.77% (p=0.008 n=5+5) +RangeQuery/clamp-8 117k ± 0% 117k ± 0% +0.08% (p=0.008 n=5+5) +RangeQuery/clamp_min-8 117k ± 0% 117k ± 0% ~ (p=0.159 n=4+5) +RangeQuery/complex_func_query-8 136k ± 0% 122k ± 0% -10.31% (p=0.008 n=5+5) +RangeQuery/func_within_func_query-8 129k ± 0% 139k ± 0% +7.04% (p=0.008 n=5+5) +RangeQuery/aggr_within_func_query-8 129k ± 0% 139k ± 0% +7.03% (p=0.008 n=5+5) +RangeQuery/histogram_quantile-8 617k ± 0% 658k ± 0% +6.64% (p=0.008 n=5+5) +RangeQuery/sort-8 105k ± 0% 114k ± 0% +7.98% (p=0.008 n=5+5) +RangeQuery/sort_desc-8 105k ± 0% 114k ± 0% +7.98% (p=0.016 n=4+5) +NativeHistograms/selector-8 9.63M ± 0% 9.64M ± 0% +0.16% (p=0.008 n=5+5) +NativeHistograms/sum-8 11.1M ± 0% 11.1M ± 0% +0.04% (p=0.008 n=5+5) +NativeHistograms/rate-8 34.1M ± 0% 34.2M ± 0% +0.09% (p=0.008 n=5+5) +NativeHistograms/sum_rate-8 35.6M ± 0% 35.6M ± 0% +0.06% (p=0.008 n=5+5) +NativeHistograms/histogram_sum-8 9.65M ± 0% 9.65M ± 0% -0.01% (p=0.008 n=5+5) +NativeHistograms/histogram_count-8 9.65M ± 0% 9.65M ± 0% -0.01% (p=0.008 n=5+5) +NativeHistograms/histogram_quantile-8 11.1M ± 0% 11.1M ± 0% +0.05% (p=0.008 n=5+5) +``` diff --git a/internal/promql-engine/api/remote.go b/internal/promql-engine/api/remote.go new file mode 100644 index 00000000000..21b7c27245d --- /dev/null +++ b/internal/promql-engine/api/remote.go @@ -0,0 +1,93 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package api + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/promql" +) + +type RemoteQuery interface { + fmt.Stringer +} + +// RemoteEndpoints returns remote engines. +// +// Implementations should use mint and maxt to prune engine metadata +// (e.g., filter TSDBInfos to only those overlapping the time range), +// reducing unnecessary computations in subsequent calls to methods like +// RemoteEngine.LabelSets(). +// +// All available engines should be returned regardless of pruning. +type RemoteEndpoints interface { + // Engines returns remote engines. + // + // If mint and/or maxt of the query is unknown, the caller must pass + // math.MinInt64 and math.MaxInt64 respectively to retrieve unpruned engines. + Engines(mint, maxt int64) []RemoteEngine +} + +type RemoteEngine interface { + MaxT() int64 + MinT() int64 + + // The external labels of the remote engine. These are used to limit fanout. The engine uses these to + // not distribute into remote engines that would return empty responses because their labelset is not matching. + LabelSets() []labels.Labels + + // The external labels of the remote engine that form a logical partition. This is expected to be + // a subset of the result of "LabelSets()". The engine uses these to compute how to distribute a query. + // It is important that, for a given set of remote engines, these labels do not overlap meaningfully. + PartitionLabelSets() []labels.Labels + + NewRangeQuery(ctx context.Context, opts promql.QueryOpts, plan RemoteQuery, start, end time.Time, interval time.Duration) (promql.Query, error) +} + +type staticEndpoints struct { + engines []RemoteEngine +} + +func (m staticEndpoints) Engines(mint, maxt int64) []RemoteEngine { + return m.engines +} + +func NewStaticEndpoints(engines []RemoteEngine) RemoteEndpoints { + return &staticEndpoints{engines: engines} +} + +type cachedEndpoints struct { + endpoints RemoteEndpoints + + enginesOnce sync.Once + engines []RemoteEngine +} + +func (l *cachedEndpoints) Engines(mint, maxt int64) []RemoteEngine { + l.enginesOnce.Do(func() { + l.engines = l.endpoints.Engines(mint, maxt) + }) + return l.engines +} + +// NewCachedEndpoints returns an endpoints wrapper that +// resolves and caches engines on first access. +// +// All subsequent Engines calls return cached engines, ignoring any query +// parameters. +func NewCachedEndpoints(endpoints RemoteEndpoints) RemoteEndpoints { + if endpoints == nil { + panic("api.NewCachedEndpoints: endpoints is nil") + } + + if le, ok := endpoints.(*cachedEndpoints); ok { + return le + } + + return &cachedEndpoints{endpoints: endpoints} +} diff --git a/internal/promql-engine/api/remote_test.go b/internal/promql-engine/api/remote_test.go new file mode 100644 index 00000000000..b4bd4502d6d --- /dev/null +++ b/internal/promql-engine/api/remote_test.go @@ -0,0 +1,72 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package api + +import ( + "testing" + + "github.com/efficientgo/core/testutil" + "github.com/prometheus/prometheus/model/labels" +) + +func TestCachedEndpoints(t *testing.T) { + engines := remoteEndpointsFunc(func(mint, maxt int64) []RemoteEngine { + testutil.Equals(t, int64(10), mint) + testutil.Equals(t, int64(20), maxt) + return []RemoteEngine{newEngineMock(0, 1, nil)} + }) + endpoints := NewCachedEndpoints(engines) + + es := endpoints.Engines(10, 20) + testutil.Equals(t, 1, len(es)) +} + +func TestCachedEndpointsCachesEngines(t *testing.T) { + var calls int + engines := remoteEndpointsFunc(func(mint, maxt int64) []RemoteEngine { + calls++ + return []RemoteEngine{ + newEngineMock(100*int64(calls), 1000*int64(calls), nil), + newEngineMock(200*int64(calls), 2000*int64(calls), nil), + } + }) + endpoints := NewCachedEndpoints(engines) + + es1 := endpoints.Engines(10, 10000) + testutil.Equals(t, 2, len(es1)) + + es2 := endpoints.Engines(20, 20000) + testutil.Equals(t, 2, len(es2)) + + testutil.Equals(t, 1, calls) + testutil.Equals(t, es1, es2) + + // Engines must be mutable. + es1[0].(*engineMock).maxT = 1337 + testutil.Equals(t, int64(1337), es1[0].MaxT()) + testutil.Equals(t, int64(1337), es2[0].MaxT()) +} + +type remoteEndpointsFunc func(mint, maxt int64) []RemoteEngine + +func (f remoteEndpointsFunc) Engines(mint, maxt int64) []RemoteEngine { + return f(mint, maxt) +} + +type engineMock struct { + RemoteEngine + minT int64 + maxT int64 + labelSets []labels.Labels + partitionLabelSets []labels.Labels +} + +func (e engineMock) MaxT() int64 { return e.maxT } +func (e engineMock) MinT() int64 { return e.minT } +func (e engineMock) LabelSets() []labels.Labels { return e.labelSets } +func (e engineMock) PartitionLabelSets() []labels.Labels { return e.partitionLabelSets } + +func newEngineMock(mint, maxt int64, labelSets []labels.Labels) *engineMock { + return &engineMock{minT: mint, maxT: maxt, labelSets: labelSets, partitionLabelSets: labelSets} +} diff --git a/internal/promql-engine/compute/aggregators.go b/internal/promql-engine/compute/aggregators.go new file mode 100644 index 00000000000..a7c1efc45bd --- /dev/null +++ b/internal/promql-engine/compute/aggregators.go @@ -0,0 +1,964 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package compute + +import ( + "math" + "sort" + + "github.com/thanos-io/promql-engine/warnings" + + "github.com/prometheus/prometheus/model/histogram" + "gonum.org/v1/gonum/floats" +) + +type ValueType int + +const ( + NoValue ValueType = iota + SingleTypeValue + MixedTypeValue +) + +// CounterResetState tracks which counter reset hints have been seen during aggregation. +// Used to detect collisions between CounterReset and NotCounterReset hints. +type CounterResetState uint8 + +const ( + SeenCounterReset CounterResetState = 1 << iota // histogram with CounterReset hint was seen + SeenNotCounterReset // histogram with NotCounterReset hint was seen +) + +// HasCollision returns true if both CounterReset and NotCounterReset hints were seen. +func (s CounterResetState) HasCollision() bool { + return s&SeenCounterReset != 0 && s&SeenNotCounterReset != 0 +} + +// Accumulators map prometheus behavior for aggregations, either operators or +// "[...]_over_time" functions. The caller is responsible to add all errors +// returned by Add as annotations. +// The Warnings method returns a bitset of warning conditions that occurred +// during accumulation (e.g., ignored histograms, mixed types). +type Accumulator interface { + Add(v float64, h *histogram.FloatHistogram) error + Value() (float64, *histogram.FloatHistogram) + ValueType() ValueType + Warnings() warnings.Warnings + Reset(float64) +} + +// VectorAccumulator is like Accumulator but accepts batches of values. +type VectorAccumulator interface { + AddVector(vs []float64, hs []*histogram.FloatHistogram) error + Value() (float64, *histogram.FloatHistogram) + ValueType() ValueType + Warnings() warnings.Warnings + Reset(float64) +} + +type SumAcc struct { + value float64 + compensation float64 + histSum *histogram.FloatHistogram + hasFloatVal bool + hasError bool // histogram error occurred; accumulator becomes no-op + warn warnings.Warnings + counterResetState CounterResetState +} + +func NewSumAcc() *SumAcc { + return &SumAcc{} +} + +func (s *SumAcc) AddVector(float64s []float64, histograms []*histogram.FloatHistogram) error { + if s.hasError { + return nil + } + if len(float64s) > 0 { + s.value, s.compensation = KahanSumInc(compensatedSum(float64s), s.value, s.compensation) + s.hasFloatVal = true + } + + if len(histograms) > 0 { + // Track counter reset hints for collision detection. + for _, h := range histograms { + switch h.CounterResetHint { + case histogram.CounterReset: + s.counterResetState |= SeenCounterReset + case histogram.NotCounterReset: + s.counterResetState |= SeenNotCounterReset + } + } + + var ( + err error + warn warnings.Warnings + ) + s.histSum, warn, err = histogramSum(s.histSum, histograms) + s.warn |= warn + if err != nil { + s.hasError = true + return err + } + } + return nil +} + +func (s *SumAcc) Add(v float64, h *histogram.FloatHistogram) error { + if s.hasError { + return nil + } + if h == nil { + s.hasFloatVal = true + s.value, s.compensation = KahanSumInc(v, s.value, s.compensation) + return nil + } + return s.addHistogram(h) +} + +func (s *SumAcc) addHistogram(h *histogram.FloatHistogram) error { + // Track counter reset hints for collision detection. + switch h.CounterResetHint { + case histogram.CounterReset: + s.counterResetState |= SeenCounterReset + case histogram.NotCounterReset: + s.counterResetState |= SeenNotCounterReset + } + + if s.histSum == nil { + s.histSum = h.Copy() + return nil + } + // The histogram being added must have an equal or larger schema. + // https://github.com/prometheus/prometheus/blob/57bcbf18880f7554ae34c5b341d52fc53f059a97/promql/engine.go#L2448-L2456 + var ( + err error + nhcbBoundsReconciled bool + ) + if h.Schema >= s.histSum.Schema { + s.histSum, _, nhcbBoundsReconciled, err = s.histSum.Add(h) + } else { + t := h.Copy() + if s.histSum, _, nhcbBoundsReconciled, err = t.Add(s.histSum); err == nil { + s.histSum = t + } + } + if nhcbBoundsReconciled { + s.warn |= warnings.WarnNHCBBoundsReconciledAgg + } + if err != nil { + s.histSum = nil + s.hasError = true + return warnings.ConvertHistogramError(err) + } + return nil +} + +func (s *SumAcc) Value() (float64, *histogram.FloatHistogram) { + if s.histSum != nil { + s.histSum.Compact(0) + } + return s.value + s.compensation, s.histSum +} + +func (s *SumAcc) ValueType() ValueType { + if s.hasFloatVal && s.histSum != nil { + return MixedTypeValue + } + if s.hasFloatVal || s.histSum != nil { + return SingleTypeValue + } + return NoValue +} + +func (s *SumAcc) Warnings() warnings.Warnings { + warn := s.warn + if s.ValueType() == MixedTypeValue { + warn |= warnings.WarnMixedFloatsHistograms + } + // Detect counter reset collision: if we've seen both CounterReset and NotCounterReset hints. + if s.counterResetState.HasCollision() { + warn |= warnings.WarnCounterResetCollision + } + return warn +} + +func (s *SumAcc) Reset(_ float64) { + s.histSum = nil + s.hasFloatVal = false + s.hasError = false + s.warn = 0 + s.value = 0 + s.compensation = 0 + s.counterResetState = 0 +} + +func NewMaxAcc() *MaxAcc { + return &MaxAcc{} +} + +type MaxAcc struct { + value float64 + hasValue bool + warn warnings.Warnings +} + +func (c *MaxAcc) AddVector(vs []float64, hs []*histogram.FloatHistogram) error { + if len(hs) > 0 { + c.warn |= warnings.WarnHistogramIgnoredInAggregation + } + if len(vs) == 0 { + return nil + } + + fst, rem := vs[0], vs[1:] + _ = c.Add(fst, nil) + if len(rem) > 0 { + _ = c.Add(floats.Max(rem), nil) + } + return nil +} + +func (c *MaxAcc) Add(v float64, h *histogram.FloatHistogram) error { + if h != nil { + c.warn |= warnings.WarnHistogramIgnoredInAggregation + return nil + } + c.addFloat(v) + return nil +} + +func (c *MaxAcc) Warnings() warnings.Warnings { + return c.warn +} + +func (c *MaxAcc) addFloat(v float64) { + if !c.hasValue { + c.value = v + c.hasValue = true + return + } + if c.value < v || math.IsNaN(c.value) { + c.value = v + } +} + +func (c *MaxAcc) Value() (float64, *histogram.FloatHistogram) { + return c.value, nil +} + +func (c *MaxAcc) ValueType() ValueType { + if c.hasValue { + return SingleTypeValue + } else { + return NoValue + } +} + +func (c *MaxAcc) Reset(_ float64) { + c.hasValue = false + c.warn = 0 + c.value = 0 +} + +func NewMinAcc() *MinAcc { + return &MinAcc{} +} + +type MinAcc struct { + value float64 + hasValue bool + warn warnings.Warnings +} + +func (c *MinAcc) AddVector(vs []float64, hs []*histogram.FloatHistogram) error { + if len(hs) > 0 { + c.warn |= warnings.WarnHistogramIgnoredInAggregation + } + if len(vs) == 0 { + return nil + } + + fst, rem := vs[0], vs[1:] + _ = c.Add(fst, nil) + if len(rem) > 0 { + _ = c.Add(floats.Min(rem), nil) + } + return nil +} + +func (c *MinAcc) Add(v float64, h *histogram.FloatHistogram) error { + if h != nil { + c.warn |= warnings.WarnHistogramIgnoredInAggregation + return nil + } + c.addFloat(v) + return nil +} + +func (c *MinAcc) Warnings() warnings.Warnings { + return c.warn +} + +func (c *MinAcc) addFloat(v float64) { + if !c.hasValue { + c.value = v + c.hasValue = true + return + } + if c.value > v || math.IsNaN(c.value) { + c.value = v + } +} + +func (c *MinAcc) Value() (float64, *histogram.FloatHistogram) { + return c.value, nil +} + +func (c *MinAcc) ValueType() ValueType { + if c.hasValue { + return SingleTypeValue + } else { + return NoValue + } +} + +func (c *MinAcc) Reset(_ float64) { + c.hasValue = false + c.warn = 0 + c.value = 0 +} + +func NewGroupAcc() *GroupAcc { + return &GroupAcc{} +} + +type GroupAcc struct { + value float64 + hasValue bool +} + +func (c *GroupAcc) AddVector(vs []float64, hs []*histogram.FloatHistogram) error { + if len(vs) == 0 && len(hs) == 0 { + return nil + } + c.hasValue = true + c.value = 1 + return nil +} + +func (c *GroupAcc) Add(v float64, h *histogram.FloatHistogram) error { + c.hasValue = true + c.value = 1 + return nil +} + +func (c *GroupAcc) Value() (float64, *histogram.FloatHistogram) { + return c.value, nil +} + +func (c *GroupAcc) ValueType() ValueType { + if c.hasValue { + return SingleTypeValue + } else { + return NoValue + } +} + +func (c *GroupAcc) Warnings() warnings.Warnings { + return 0 +} + +func (c *GroupAcc) Reset(_ float64) { + c.hasValue = false + c.value = 0 +} + +type CountAcc struct { + value float64 + hasValue bool +} + +func NewCountAcc() *CountAcc { + return &CountAcc{} +} + +func (c *CountAcc) AddVector(vs []float64, hs []*histogram.FloatHistogram) error { + if len(vs) > 0 || len(hs) > 0 { + c.hasValue = true + c.value += float64(len(vs)) + float64(len(hs)) + } + return nil +} + +func (c *CountAcc) Add(v float64, h *histogram.FloatHistogram) error { + c.hasValue = true + c.value += 1 + return nil +} + +func (c *CountAcc) Value() (float64, *histogram.FloatHistogram) { + return c.value, nil +} + +func (c *CountAcc) ValueType() ValueType { + if c.hasValue { + return SingleTypeValue + } else { + return NoValue + } +} +func (c *CountAcc) Warnings() warnings.Warnings { + return 0 +} + +func (c *CountAcc) Reset(_ float64) { + c.hasValue = false + c.value = 0 +} + +type AvgAcc struct { + kahanSum float64 + kahanC float64 + avg float64 + incremental bool + count int64 + hasValue bool + hasError bool // histogram error occurred; accumulator becomes no-op + + histSum *histogram.FloatHistogram + histScratch *histogram.FloatHistogram + histSumScratch *histogram.FloatHistogram + histCount float64 + warn warnings.Warnings + counterResetState CounterResetState +} + +func NewAvgAcc() *AvgAcc { + return &AvgAcc{} +} + +func (a *AvgAcc) Add(v float64, h *histogram.FloatHistogram) error { + if a.hasError { + return nil + } + if h == nil { + return a.addFloat(v) + } + return a.addHistogram(h) +} + +func (a *AvgAcc) addHistogram(h *histogram.FloatHistogram) error { + // Track counter reset hints for collision detection. + switch h.CounterResetHint { + case histogram.CounterReset: + a.counterResetState |= SeenCounterReset + case histogram.NotCounterReset: + a.counterResetState |= SeenNotCounterReset + } + + a.histCount++ + if a.histSum == nil { + a.histSum = h.Copy() + a.histScratch = &histogram.FloatHistogram{} + a.histSumScratch = &histogram.FloatHistogram{} + return nil + } + + var ( + err error + nhcbBoundsReconciled bool + ) + h.CopyTo(a.histScratch) + left := a.histScratch.Div(a.histCount) + a.histSum.CopyTo(a.histSumScratch) + right := a.histSumScratch.Div(a.histCount) + toAdd, _, nhcbBoundsReconciled, err := left.Sub(right) + if nhcbBoundsReconciled { + a.warn |= warnings.WarnNHCBBoundsReconciledAgg + } + if err == nil { + var nbr bool + a.histSum, _, nbr, err = a.histSum.Add(toAdd) + if nbr { + a.warn |= warnings.WarnNHCBBoundsReconciledAgg + } + } + if err != nil { + a.histSum = nil + a.histCount = 0 + a.hasError = true + return warnings.ConvertHistogramError(err) + } + return nil +} + +func (a *AvgAcc) addFloat(v float64) error { + a.count++ + if !a.hasValue { + a.hasValue = true + a.kahanSum = v + return nil + } + + a.hasValue = true + + if !a.incremental { + newSum, newC := KahanSumInc(v, a.kahanSum, a.kahanC) + + if !math.IsInf(newSum, 0) { + // The sum doesn't overflow, so we propagate it to the + // group struct and continue with the regular + // calculation of the mean value. + a.kahanSum, a.kahanC = newSum, newC + return nil + } + + // If we are here, we know that the sum _would_ overflow. So + // instead of continue to sum up, we revert to incremental + // calculation of the mean value from here on. + a.incremental = true + a.avg = a.kahanSum / float64(a.count-1) + a.kahanC /= float64(a.count) - 1 + } + + if math.IsInf(a.avg, 0) { + if math.IsInf(v, 0) && (a.avg > 0) == (v > 0) { + // The `floatMean` and `s.F` values are `Inf` of the same sign. They + // can't be subtracted, but the value of `floatMean` is correct + // already. + return nil + } + if !math.IsInf(v, 0) && !math.IsNaN(v) { + // At this stage, the mean is an infinite. If the added + // value is neither an Inf or a Nan, we can keep that mean + // value. + // This is required because our calculation below removes + // the mean value, which would look like Inf += x - Inf and + // end up as a NaN. + return nil + } + } + currentMean := a.avg + a.kahanC + a.avg, a.kahanC = KahanSumInc( + // Divide each side of the `-` by `group.groupCount` to avoid float64 overflows. + v/float64(a.count)-currentMean/float64(a.count), + a.avg, + a.kahanC, + ) + return nil +} + +func (a *AvgAcc) AddVector(vs []float64, hs []*histogram.FloatHistogram) error { + if a.hasError { + return nil + } + for _, v := range vs { + if err := a.Add(v, nil); err != nil { + return err + } + } + for _, h := range hs { + if err := a.Add(0, h); err != nil { + return err + } + } + return nil +} + +func (a *AvgAcc) Value() (float64, *histogram.FloatHistogram) { + if a.histSum != nil { + a.histSum.Compact(0) + } + if a.incremental { + return a.avg + a.kahanC, a.histSum + } + return (a.kahanSum + a.kahanC) / float64(a.count), a.histSum +} + +func (a *AvgAcc) ValueType() ValueType { + hasFloat := a.count > 0 + hasHist := a.histCount > 0 + + if hasFloat && hasHist { + return MixedTypeValue + } + if hasFloat || hasHist { + return SingleTypeValue + } + return NoValue +} + +func (a *AvgAcc) Warnings() warnings.Warnings { + warn := a.warn + if a.ValueType() == MixedTypeValue { + warn |= warnings.WarnMixedFloatsHistograms + } + // Detect counter reset collision: if we've seen both CounterReset and NotCounterReset hints. + if a.counterResetState.HasCollision() { + warn |= warnings.WarnCounterResetCollision + } + return warn +} + +func (a *AvgAcc) Reset(_ float64) { + a.hasValue = false + a.hasError = false + a.incremental = false + a.kahanSum = 0 + a.kahanC = 0 + a.count = 0 + + a.histCount = 0 + a.histSum = nil + a.warn = 0 + a.counterResetState = 0 +} + +type statAcc struct { + count float64 + mean float64 + cMean float64 + value float64 + cValue float64 + hasValue bool + hasNaN bool + warn warnings.Warnings +} + +func (s *statAcc) ValueType() ValueType { + if s.hasValue { + return SingleTypeValue + } + return NoValue +} + +func (s *statAcc) Warnings() warnings.Warnings { + return s.warn +} + +func (s *statAcc) Reset(_ float64) { + s.hasValue = false + s.hasNaN = false + s.warn = 0 + s.count = 0 + s.mean = 0 + s.cMean = 0 + s.value = 0 + s.cValue = 0 +} + +func (s *statAcc) add(v float64) { + s.hasValue = true + s.count++ + if math.IsNaN(v) || math.IsInf(v, 0) { + s.hasNaN = true + return + } + delta := v - (s.mean + s.cMean) + s.mean, s.cMean = KahanSumInc(delta/s.count, s.mean, s.cMean) + s.value, s.cValue = KahanSumInc(delta*(v-(s.mean+s.cMean)), s.value, s.cValue) +} + +func (s *statAcc) variance() float64 { + if s.hasNaN { + return math.NaN() + } + return (s.value + s.cValue) / s.count +} + +type StdDevAcc struct { + statAcc +} + +func NewStdDevAcc() *StdDevAcc { + return &StdDevAcc{} +} + +func (s *StdDevAcc) Add(v float64, h *histogram.FloatHistogram) error { + if h != nil { + s.warn |= warnings.WarnHistogramIgnoredInAggregation + return nil + } + s.add(v) + return nil +} + +func (s *StdDevAcc) Value() (float64, *histogram.FloatHistogram) { + return math.Sqrt(s.variance()), nil +} + +type StdVarAcc struct { + statAcc +} + +func NewStdVarAcc() *StdVarAcc { + return &StdVarAcc{} +} + +func (s *StdVarAcc) Add(v float64, h *histogram.FloatHistogram) error { + if h != nil { + s.warn |= warnings.WarnHistogramIgnoredInAggregation + return nil + } + s.add(v) + return nil +} + +func (s *StdVarAcc) Value() (float64, *histogram.FloatHistogram) { + return s.variance(), nil +} + +type QuantileAcc struct { + arg float64 + points []float64 + hasValue bool + warn warnings.Warnings +} + +func NewQuantileAcc() Accumulator { + return &QuantileAcc{} +} + +func (q *QuantileAcc) Add(v float64, h *histogram.FloatHistogram) error { + if h != nil { + q.warn |= warnings.WarnHistogramIgnoredInAggregation + return nil + } + + q.hasValue = true + q.points = append(q.points, v) + return nil +} + +func (q *QuantileAcc) Value() (float64, *histogram.FloatHistogram) { + return Quantile(q.arg, q.points), nil +} + +func (q *QuantileAcc) ValueType() ValueType { + if q.hasValue { + return SingleTypeValue + } else { + return NoValue + } +} + +func (q *QuantileAcc) Warnings() warnings.Warnings { + return q.warn +} + +func (q *QuantileAcc) Reset(f float64) { + q.hasValue = false + q.warn = 0 + q.arg = f + q.points = q.points[:0] +} + +type HistogramAvgAcc struct { + sum *histogram.FloatHistogram + count int64 + hasFloat bool +} + +func NewHistogramAvgAcc() *HistogramAvgAcc { + return &HistogramAvgAcc{ + sum: &histogram.FloatHistogram{}, + } +} + +func (acc *HistogramAvgAcc) Add(v float64, h *histogram.FloatHistogram) error { + if h == nil { + acc.hasFloat = true + } + if acc.count == 0 { + h.CopyTo(acc.sum) + } + var err error + if h.Schema >= acc.sum.Schema { + if acc.sum, _, _, err = acc.sum.Add(h); err != nil { + return err + } + } else { + t := h.Copy() + if _, _, _, err = t.Add(acc.sum); err != nil { + return err + } + acc.sum = t + } + acc.count++ + return nil +} + +func (acc *HistogramAvgAcc) Value() (float64, *histogram.FloatHistogram) { + return 0, acc.sum.Mul(1 / float64(acc.count)) +} + +func (acc *HistogramAvgAcc) ValueType() ValueType { + if acc.count > 0 && !acc.hasFloat { + return SingleTypeValue + } + return NoValue +} + +func (acc *HistogramAvgAcc) Warnings() warnings.Warnings { + return 0 +} + +func (acc *HistogramAvgAcc) Reset(f float64) { + acc.count = 0 +} + +// LastAcc tracks the last value seen. Used for last_over_time. +type LastAcc struct { + value float64 + hist *histogram.FloatHistogram + hasValue bool +} + +func NewLastAcc() *LastAcc { + return &LastAcc{} +} + +func (l *LastAcc) Add(v float64, h *histogram.FloatHistogram) error { + l.hasValue = true + if h != nil { + l.value = 0 + if l.hist == nil { + l.hist = h.Copy() + } else { + h.CopyTo(l.hist) + } + } else { + l.value = v + l.hist = nil + } + return nil +} + +func (l *LastAcc) Value() (float64, *histogram.FloatHistogram) { + if l.hist != nil { + return 0, l.hist.Copy() + } + return l.value, nil +} + +func (l *LastAcc) ValueType() ValueType { + if l.hasValue { + return SingleTypeValue + } + return NoValue +} + +func (l *LastAcc) Warnings() warnings.Warnings { + return 0 +} + +func (l *LastAcc) Reset(_ float64) { + l.hasValue = false + l.value = 0 + l.hist = nil +} + +// KahanSumInc implements kahan summation, see https://en.wikipedia.org/wiki/Kahan_summation_algorithm. +func KahanSumInc(inc, sum, c float64) (newSum, newC float64) { + t := sum + inc + switch { + case math.IsInf(t, 0): + c = 0 + + // Using Neumaier improvement, swap if next term larger than sum. + case math.Abs(sum) >= math.Abs(inc): + c += (sum - t) + inc + default: + c += (inc - t) + sum + } + return t, c +} + +func Quantile(q float64, points []float64) float64 { + if len(points) == 0 || math.IsNaN(q) { + return math.NaN() + } + if q < 0 { + return math.Inf(-1) + } + if q > 1 { + return math.Inf(+1) + } + sort.Float64s(points) + + n := float64(len(points)) + // When the quantile lies between two samples, + // we use a weighted average of the two samples. + rank := q * (n - 1) + + lowerIndex := math.Max(0, math.Floor(rank)) + upperIndex := math.Min(n-1, lowerIndex+1) + + weight := rank - math.Floor(rank) + return points[int(lowerIndex)]*(1-weight) + points[int(upperIndex)]*weight +} + +func histogramSum(current *histogram.FloatHistogram, histograms []*histogram.FloatHistogram) (*histogram.FloatHistogram, warnings.Warnings, error) { + if len(histograms) == 0 { + return current, 0, nil + } + if current == nil && len(histograms) == 1 { + return histograms[0].Copy(), 0, nil + } + var histSum *histogram.FloatHistogram + if current != nil { + histSum = current.Copy() + } else { + histSum = histograms[0].Copy() + histograms = histograms[1:] + } + + var ( + err error + warn warnings.Warnings + nhcbBoundsReconciled bool + ) + for i := range histograms { + if histograms[i].Schema >= histSum.Schema { + histSum, _, nhcbBoundsReconciled, err = histSum.Add(histograms[i]) + } else { + t := histograms[i].Copy() + histSum, _, nhcbBoundsReconciled, err = t.Add(histSum) + } + if nhcbBoundsReconciled { + warn |= warnings.WarnNHCBBoundsReconciledAgg + } + if err != nil { + return nil, warn, warnings.ConvertHistogramError(err) + } + } + return histSum, warn, nil +} + +// compensatedSum returns the sum of the elements of the slice calculated with greater +// accuracy than Sum at the expense of additional computation. +func compensatedSum(s []float64) float64 { + // compensatedSum uses an improved version of Kahan's compensated + // summation algorithm proposed by Neumaier. + // See https://en.wikipedia.org/wiki/Kahan_summation_algorithm for details. + var sum, c float64 + for _, x := range s { + // This type conversion is here to prevent a sufficiently smart compiler + // from optimizing away these operations. + t := sum + x + switch { + case math.IsInf(t, 0): + c = 0 + + // Using Neumaier improvement, swap if next term larger than sum. + case math.Abs(sum) >= math.Abs(x): + c += (sum - t) + x + default: + c += (x - t) + sum + } + sum = t + } + return sum + c +} diff --git a/internal/promql-engine/docs/assets/design.png b/internal/promql-engine/docs/assets/design.png new file mode 100644 index 00000000000..b7688d3200f Binary files /dev/null and b/internal/promql-engine/docs/assets/design.png differ diff --git a/internal/promql-engine/docs/assets/parallel-coalesce.png b/internal/promql-engine/docs/assets/parallel-coalesce.png new file mode 100644 index 00000000000..79f01633091 Binary files /dev/null and b/internal/promql-engine/docs/assets/parallel-coalesce.png differ diff --git a/internal/promql-engine/docs/assets/promql-pipeline.png b/internal/promql-engine/docs/assets/promql-pipeline.png new file mode 100644 index 00000000000..db6efcd4c05 Binary files /dev/null and b/internal/promql-engine/docs/assets/promql-pipeline.png differ diff --git a/internal/promql-engine/engine/bench_test.go b/internal/promql-engine/engine/bench_test.go new file mode 100644 index 00000000000..36306f6aa9c --- /dev/null +++ b/internal/promql-engine/engine/bench_test.go @@ -0,0 +1,902 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package engine_test + +import ( + "context" + "fmt" + "runtime" + "strconv" + "strings" + "testing" + "time" + + "github.com/thanos-io/promql-engine/engine" + "github.com/thanos-io/promql-engine/logicalplan" + + "github.com/efficientgo/core/testutil" + "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/promql" + "github.com/prometheus/prometheus/promql/promqltest" + "github.com/prometheus/prometheus/tsdb" + "github.com/prometheus/prometheus/tsdb/chunkenc" + "github.com/prometheus/prometheus/util/teststorage" +) + +func BenchmarkChunkDecoding(b *testing.B) { + storage := setupStorage(b, 1000, 3, 720) + defer storage.Close() + + ctx := context.Background() + start := time.Unix(0, 0) + end := start.Add(6 * time.Hour) + step := time.Second * 30 + + querier, err := storage.Querier(start.UnixMilli(), end.UnixMilli()) + testutil.Ok(b, err) + + matcher, err := labels.NewMatcher(labels.MatchEqual, labels.MetricName, "http_requests_total") + testutil.Ok(b, err) + + b.Run("iterate by series", func(b *testing.B) { + b.ResetTimer() + for b.Loop() { + numIterations := 0 + + ss := querier.Select(ctx, false, nil, matcher) + series := make([]chunkenc.Iterator, 0) + for ss.Next() { + series = append(series, ss.At().Iterator(nil)) + } + for i := range series { + for ts := start.UnixMilli(); ts <= end.UnixMilli(); ts += step.Milliseconds() { + numIterations++ + if val := series[i].Seek(ts); val == chunkenc.ValNone { + break + } + } + } + } + }) + b.Run("iterate by time", func(b *testing.B) { + b.ResetTimer() + for b.Loop() { + numIterations := 0 + ss := querier.Select(ctx, false, nil, matcher) + series := make([]chunkenc.Iterator, 0) + for ss.Next() { + series = append(series, ss.At().Iterator(nil)) + } + stepCount := 10 + ts := start.UnixMilli() + for ts <= end.UnixMilli() { + for i := range series { + seriesTs := ts + for currStep := 0; currStep < stepCount && seriesTs <= end.UnixMilli(); currStep++ { + numIterations++ + if valType := series[i].Seek(seriesTs); valType == chunkenc.ValNone { + break + } + seriesTs += step.Milliseconds() + } + } + ts += step.Milliseconds() * int64(stepCount) + } + } + }) +} + +func BenchmarkSingleQuery(b *testing.B) { + + memProfileRate := runtime.MemProfileRate + runtime.MemProfileRate = 0 + + test := setupStorage(b, 5000, 3, 720) + defer test.Close() + + start := time.Unix(0, 0) + end := start.Add(6 * time.Hour) + step := time.Second * 30 + + query := "sum(rate(http_requests_total[2m]))" + opts := engine.Opts{ + EngineOpts: promql.EngineOpts{Timeout: 100 * time.Second}, + SelectorBatchSize: 256, + } + b.ReportAllocs() + + runtime.MemProfileRate = memProfileRate + for b.Loop() { + result := executeRangeQuery(b, query, test, start, end, step, opts) + testutil.Ok(b, result.Err) + } +} + +func BenchmarkRangeQuery(b *testing.B) { + samplesPerHour := 60 * 2 + sixHourDataset := setupStorage(b, 1000, 3, 6*samplesPerHour) + defer sixHourDataset.Close() + + /* + largeSixHourDataset := setupStorage(b, 10000, 10, 6*samplesPerHour) + defer largeSixHourDataset.Close() + + sevenDaysAndTwoHoursDataset := setupStorage(b, 1000, 3, (7*24+2)*samplesPerHour) + defer sevenDaysAndTwoHoursDataset.Close() + */ + + start := time.Unix(0, 0) + end := start.Add(2 * time.Hour) + step := time.Second * 30 + + cases := []struct { + name string + query string + step time.Duration + storage *teststorage.TestStorage + }{ + { + name: "vector selector", + query: `http_requests_total`, + storage: sixHourDataset, + }, + { + name: "sum", + query: `sum(http_requests_total)`, + storage: sixHourDataset, + }, + { + name: "sum by pod", + query: `sum by (pod) (http_requests_total)`, + storage: sixHourDataset, + }, + { + name: "topk", + query: `topk(2, http_requests_total)`, + storage: sixHourDataset, + }, + { + name: "bottomk", + query: `bottomk(2, http_requests_total)`, + storage: sixHourDataset, + }, + { + name: "limitk", + query: `limitk(2, http_requests_total)`, + storage: sixHourDataset, + }, + { + name: "limit_ratio", + query: `limit_ratio(0.2, http_requests_total)`, + storage: sixHourDataset, + }, + { + name: "rate", + query: `rate(http_requests_total[1m])`, + storage: sixHourDataset, + }, + { + name: "rate with longer window", + query: `rate(http_requests_total[10m])`, + storage: sixHourDataset, + step: 5 * time.Minute, + }, + { + name: "subquery", + query: `sum_over_time(rate(http_requests_total[1m])[10m:1m])`, + storage: sixHourDataset, + }, + /* + { + name: "rate with large range selection", + query: "rate(http_requests_total[7d])", + storage: sevenDaysAndTwoHoursDataset, + }, + { + name: "rate with large number of series, 1m range", + query: "rate(http_requests_total[1m])", + storage: largeSixHourDataset, + }, + { + name: "rate with large number of series, 5m range", + query: "rate(http_requests_total[5m])", + storage: largeSixHourDataset, + }, + */ + { + name: "sum rate", + query: `sum(rate(http_requests_total[1m]))`, + storage: sixHourDataset, + }, + { + name: "sum by rate", + query: `sum by (pod) (rate(http_requests_total[1m]))`, + storage: sixHourDataset, + }, + { + name: "quantile with variable parameter", + query: `quantile by (pod) (scalar(min(http_requests_total)), http_requests_total)`, + storage: sixHourDataset, + }, + { + name: "binary operation with one to one", + query: `http_requests_total{container="c1"} / ignoring (container) http_responses_total`, + storage: sixHourDataset, + }, + { + name: "binary operation with many to one", + query: `http_requests_total / on (pod) group_left () http_responses_total`, + storage: sixHourDataset, + }, + { + name: "binary operation with vector and scalar", + query: `http_requests_total * 10`, + storage: sixHourDataset, + }, + { + name: "unary negation", + query: `-http_requests_total`, + storage: sixHourDataset, + }, + { + name: "vector and scalar comparison", + query: `http_requests_total > 10`, + storage: sixHourDataset, + }, + { + name: "positive offset vector", + query: `http_requests_total offset 5m`, + storage: sixHourDataset, + }, + { + name: "at modifier ", + query: `http_requests_total @ 600.000`, + storage: sixHourDataset, + }, + { + name: "at modifier with positive offset vector", + query: `http_requests_total @ 600.000 offset 5m`, + storage: sixHourDataset, + }, + { + name: "clamp", + query: `clamp(http_requests_total, 5, 10)`, + storage: sixHourDataset, + }, + { + name: "clamp_min", + query: `clamp_min(http_requests_total, 10)`, + storage: sixHourDataset, + }, + { + name: "complex func query", + query: `clamp(1 - http_requests_total, 10 - 5, 10)`, + storage: sixHourDataset, + }, + { + name: "func within func query", + query: `clamp(irate(http_requests_total[30s]), 10 - 5, 10)`, + storage: sixHourDataset, + }, + { + name: "aggr within func query", + query: `clamp(rate(http_requests_total[30s]), 10 - 5, 10)`, + storage: sixHourDataset, + }, + { + name: "histogram_quantile", + query: `histogram_quantile(0.9, http_response_seconds_bucket)`, + storage: sixHourDataset, + }, + { + name: "sort", + query: `sort(http_requests_total)`, + storage: sixHourDataset, + }, + { + name: "sort_desc", + query: `sort_desc(http_requests_total)`, + storage: sixHourDataset, + }, + { + name: "absent and exists", + query: `absent(http_requests_total)`, + storage: sixHourDataset, + }, + { + name: "absent and doesnt exist", + query: `absent(nonexistent)`, + storage: sixHourDataset, + }, + { + name: "double exponential smoothing", + query: `double_exponential_smoothing(http_requests_total[1m], 0.1, 0.1)`, + storage: sixHourDataset, + }, + // over_time functions + { + name: "count_over_time_5m", + query: `count_over_time(http_requests_total[5m])`, + storage: sixHourDataset, + }, + { + name: "count_over_time_1h", + query: `count_over_time(http_requests_total[1h])`, + storage: sixHourDataset, + }, + { + name: "count_over_time_6h", + query: `count_over_time(http_requests_total[6h])`, + storage: sixHourDataset, + }, + { + name: "sum_over_time_5m", + query: `sum_over_time(http_requests_total[5m])`, + storage: sixHourDataset, + }, + { + name: "sum_over_time_1h", + query: `sum_over_time(http_requests_total[1h])`, + storage: sixHourDataset, + }, + { + name: "sum_over_time_6h", + query: `sum_over_time(http_requests_total[6h])`, + storage: sixHourDataset, + }, + + { + name: "avg_over_time_5m", + query: `avg_over_time(http_requests_total[5m])`, + storage: sixHourDataset, + }, + { + name: "avg_over_time_1h", + query: `avg_over_time(http_requests_total[1h])`, + storage: sixHourDataset, + }, + { + name: "avg_over_time_6h", + query: `avg_over_time(http_requests_total[6h])`, + storage: sixHourDataset, + }, + { + name: "min_over_time_5m", + query: `min_over_time(http_requests_total[5m])`, + storage: sixHourDataset, + }, + { + name: "min_over_time_1h", + query: `min_over_time(http_requests_total[1h])`, + storage: sixHourDataset, + }, + { + name: "min_over_time_6h", + query: `min_over_time(http_requests_total[6h])`, + storage: sixHourDataset, + }, + { + name: "max_over_time_5m", + query: `max_over_time(http_requests_total[5m])`, + storage: sixHourDataset, + }, + { + name: "max_over_time_1h", + query: `max_over_time(http_requests_total[1h])`, + storage: sixHourDataset, + }, + { + name: "max_over_time_6h", + query: `max_over_time(http_requests_total[6h])`, + storage: sixHourDataset, + }, + { + name: "stddev_over_time_5m", + query: `stddev_over_time(http_requests_total[5m])`, + storage: sixHourDataset, + }, + { + name: "stddev_over_time_1h", + query: `stddev_over_time(http_requests_total[1h])`, + storage: sixHourDataset, + }, + { + name: "stddev_over_time_6h", + query: `stddev_over_time(http_requests_total[6h])`, + storage: sixHourDataset, + }, + { + name: "stdvar_over_time", + query: `stdvar_over_time(http_requests_total[5m])`, + storage: sixHourDataset, + }, + { + name: "last_over_time", + query: `last_over_time(http_requests_total[5m])`, + storage: sixHourDataset, + }, + { + name: "present_over_time", + query: `present_over_time(http_requests_total[5m])`, + storage: sixHourDataset, + }, + } + + opts := engine.Opts{ + EngineOpts: promql.EngineOpts{ + Logger: nil, + Reg: nil, + MaxSamples: 50000000, + Timeout: 100 * time.Second, + EnableAtModifier: true, + EnableNegativeOffset: true, + }, + SelectorBatchSize: 256, + } + + for _, tc := range cases { + testStep := step + if tc.step != 0 { + testStep = tc.step + } + b.Run(tc.name, func(b *testing.B) { + b.ReportAllocs() + b.Run("old_engine", func(b *testing.B) { + + promEngine := promql.NewEngine(opts.EngineOpts) + + b.ResetTimer() + b.ReportAllocs() + for b.Loop() { + qry, err := promEngine.NewRangeQuery(context.Background(), tc.storage, nil, tc.query, start, end, testStep) + testutil.Ok(b, err) + + oldResult := qry.Exec(context.Background()) + testutil.Ok(b, oldResult.Err) + } + }) + b.Run("new_engine", func(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + + for b.Loop() { + newResult := executeRangeQuery(b, tc.query, tc.storage, start, end, testStep, opts) + testutil.Ok(b, newResult.Err) + } + }) + }) + } +} + +func BenchmarkNativeHistograms(b *testing.B) { + storage := teststorage.New(b) + defer storage.Close() + + app := storage.Appender(context.TODO()) + testutil.Ok(b, generateNativeHistogramSeries(app, 3000, false)) + testutil.Ok(b, app.Commit()) + + start := time.Unix(0, 0) + end := start.Add(2 * time.Hour) + step := time.Second * 30 + + cases := []struct { + name string + query string + step time.Duration + }{ + { + name: "selector", + query: `native_histogram_series`, + }, + { + name: "sum", + query: `sum(native_histogram_series)`, + }, + { + name: "rate", + query: `rate(native_histogram_series[1m])`, + }, + { + name: "rate with longer window", + query: `rate(native_histogram_series[10m])`, + step: 5 * time.Minute, + }, + { + name: "sum rate", + query: `sum(rate(native_histogram_series[1m]))`, + }, + { + name: "histogram_sum", + query: `histogram_sum(native_histogram_series)`, + }, + { + name: "histogram_count with rate", + query: `histogram_count(rate(native_histogram_series[1m]))`, + }, + { + name: "histogram_count", + query: `histogram_count(native_histogram_series)`, + }, + { + name: "histogram_count with sum and rate", + query: `histogram_count(sum(rate(native_histogram_series[1m])))`, + }, + { + name: "histogram_avg", + query: `histogram_avg(native_histogram_series)`, + }, + { + name: "histogram_avg with sum and rate", + query: `histogram_avg(sum(rate(native_histogram_series[1m])))`, + }, + { + name: "histogram_quantile", + query: `histogram_quantile(0.9, sum(native_histogram_series))`, + }, + { + name: "histogram scalar binop", + query: `sum(native_histogram_series * 60)`, + }, + { + name: "histogram_stdvar", + query: `histogram_stdvar(native_histogram_series)`, + }, + { + name: "histogram_stddev", + query: `histogram_stddev(native_histogram_series)`, + }, + } + + opts := promql.EngineOpts{ + Logger: nil, + Reg: nil, + MaxSamples: 50000000, + Timeout: 100 * time.Second, + EnableAtModifier: true, + EnableNegativeOffset: true, + } + for _, tc := range cases { + b.Run(tc.name, func(b *testing.B) { + testStep := step + if tc.step != 0 { + testStep = tc.step + } + b.Run("old_engine", func(b *testing.B) { + engine := promql.NewEngine(opts) + + b.ResetTimer() + b.ReportAllocs() + for b.Loop() { + qry, err := engine.NewRangeQuery(context.Background(), storage, nil, tc.query, start, end, testStep) + testutil.Ok(b, err) + + oldResult := qry.Exec(context.Background()) + testutil.Ok(b, oldResult.Err) + } + }) + b.Run("new_engine", func(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + + for b.Loop() { + ng := engine.New(engine.Opts{ + EngineOpts: opts, + }) + + qry, err := ng.NewRangeQuery(context.Background(), storage, nil, tc.query, start, end, testStep) + testutil.Ok(b, err) + + newResult := qry.Exec(context.Background()) + testutil.Ok(b, newResult.Err) + } + }) + }) + } +} + +func BenchmarkInstantQuery(b *testing.B) { + storage := setupStorage(b, 1000, 3, 720) + defer storage.Close() + + // 6 hour dataset at 30s intervals for long range queries + sixHourStorage := setupStorage(b, 1000, 3, 6*60*2) + defer sixHourStorage.Close() + + queryTime := time.Unix(50, 0) + sixHourQueryTime := time.Unix(6*60*60, 0) // End of 6h dataset + + cases := []struct { + name string + query string + }{ + { + name: "vector selector", + query: `http_requests_total`, + }, + { + name: "count", + query: `count(http_requests_total)`, + }, + { + name: "count_values", + query: `count_values("val", http_requests_total)`, + }, + { + name: "round", + query: `round(http_requests_total)`, + }, + { + name: "round with argument", + query: `round(http_requests_total, 0.5)`, + }, + { + name: "avg", + query: `avg(http_requests_total)`, + }, + { + name: "sum", + query: `sum(http_requests_total)`, + }, + { + name: "sum by pod", + query: `sum by (pod) (http_requests_total)`, + }, + { + name: "rate", + query: `rate(http_requests_total[1m])`, + }, + { + name: "rate with long window", + query: `rate(http_requests_total[1h])`, + }, + { + name: "sum rate", + query: `sum(rate(http_requests_total[1m]))`, + }, + { + name: "sum by rate", + query: `sum by (pod) (rate(http_requests_total[1m]))`, + }, + { + name: "binary operation with many to one", + query: `http_requests_total / on (pod) group_left () http_responses_total`, + }, + { + name: "unary negation", + query: `-http_requests_total`, + }, + { + name: "vector and scalar comparison", + query: `http_requests_total > 10`, + }, + { + name: "sort", + query: `sort(http_requests_total)`, + }, + { + name: "sort_desc", + query: `sort_desc(http_requests_total)`, + }, + { + name: "subquery sum_over_time", + query: `sum_over_time(count(http_requests_total)[1h:10s])`, + }, + { + name: "double exponential smoothing", + query: `double_exponential_smoothing(http_requests_total[1m], 0.1, 0.1)`, + }, + } + + // Long range instant query cases - these benefit from OverTimeBuffer + longRangeCases := []struct { + name string + query string + }{ + { + name: "count_over_time 6h", + query: `count_over_time(http_requests_total[6h])`, + }, + { + name: "sum_over_time 6h", + query: `sum_over_time(http_requests_total[6h])`, + }, + { + name: "avg_over_time 6h", + query: `avg_over_time(http_requests_total[6h])`, + }, + { + name: "min_over_time 6h", + query: `min_over_time(http_requests_total[6h])`, + }, + { + name: "max_over_time 6h", + query: `max_over_time(http_requests_total[6h])`, + }, + { + name: "stddev_over_time 6h", + query: `stddev_over_time(http_requests_total[6h])`, + }, + { + name: "stdvar_over_time 6h", + query: `stdvar_over_time(http_requests_total[6h])`, + }, + { + name: "present_over_time 6h", + query: `present_over_time(http_requests_total[6h])`, + }, + { + name: "last_over_time 6h", + query: `last_over_time(http_requests_total[6h])`, + }, + } + + for _, tc := range longRangeCases { + b.Run(tc.name, func(b *testing.B) { + b.Run("new_engine", func(b *testing.B) { + ng := engine.New(engine.Opts{ + EngineOpts: promql.EngineOpts{Timeout: 100 * time.Second}, + }) + b.ResetTimer() + b.ReportAllocs() + + for b.Loop() { + qry, err := ng.NewInstantQuery(context.Background(), sixHourStorage, nil, tc.query, sixHourQueryTime) + testutil.Ok(b, err) + + res := qry.Exec(context.Background()) + testutil.Ok(b, res.Err) + } + }) + }) + } + + for _, tc := range cases { + b.Run(tc.name, func(b *testing.B) { + b.Run("old_engine", func(b *testing.B) { + opts := promql.EngineOpts{ + Logger: nil, + Reg: nil, + MaxSamples: 50000000, + Timeout: 100 * time.Second, + EnableAtModifier: true, + EnableNegativeOffset: true, + } + engine := promql.NewEngine(opts) + + b.ResetTimer() + b.ReportAllocs() + for b.Loop() { + qry, err := engine.NewInstantQuery(context.Background(), storage, nil, tc.query, queryTime) + testutil.Ok(b, err) + + res := qry.Exec(context.Background()) + testutil.Ok(b, res.Err) + } + }) + b.Run("new_engine", func(b *testing.B) { + ng := engine.New(engine.Opts{ + EngineOpts: promql.EngineOpts{Timeout: 100 * time.Second}, + }) + b.ResetTimer() + b.ReportAllocs() + + for b.Loop() { + qry, err := ng.NewInstantQuery(context.Background(), storage, nil, tc.query, queryTime) + testutil.Ok(b, err) + + res := qry.Exec(context.Background()) + testutil.Ok(b, res.Err) + } + }) + }) + } +} + +func BenchmarkMergeSelectorsOptimizer(b *testing.B) { + db := createRequestsMetricBlock(b, 10000, 9900) + defer db.Close() + + start := time.Unix(0, 0) + end := start.Add(6 * time.Hour) + step := time.Second * 30 + + query := `sum(http_requests_total{code="200"}) / sum(http_requests_total)` + b.Run("withoutOptimizers", func(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + for b.Loop() { + opts := engine.Opts{ + LogicalOptimizers: logicalplan.NoOptimizers, + EngineOpts: promql.EngineOpts{Timeout: 100 * time.Second}, + } + ng := engine.New(opts) + ctx := context.Background() + qry, err := ng.NewRangeQuery(ctx, db, nil, query, start, end, step) + testutil.Ok(b, err) + + res := qry.Exec(ctx) + testutil.Ok(b, res.Err) + } + }) + b.Run("withOptimizers", func(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + for b.Loop() { + ng := engine.New(engine.Opts{EngineOpts: promql.EngineOpts{Timeout: 100 * time.Second}}) + ctx := context.Background() + qry, err := ng.NewRangeQuery(ctx, db, nil, query, start, end, step) + testutil.Ok(b, err) + + res := qry.Exec(ctx) + testutil.Ok(b, res.Err) + } + }) + +} + +func executeRangeQuery(b *testing.B, q string, storage *teststorage.TestStorage, start time.Time, end time.Time, step time.Duration, opts engine.Opts) *promql.Result { + return executeRangeQueryWithOpts(b, q, storage, start, end, step, opts) +} + +func executeRangeQueryWithOpts(b *testing.B, q string, storage *teststorage.TestStorage, start time.Time, end time.Time, step time.Duration, opts engine.Opts) *promql.Result { + ng := engine.New(opts) + ctx := context.Background() + qry, err := ng.NewRangeQuery(ctx, storage, nil, q, start, end, step) + testutil.Ok(b, err) + + return qry.Exec(ctx) +} + +// nolint: unparam +func setupStorage(b *testing.B, numLabelsA int, numLabelsB int, numSteps int) *teststorage.TestStorage { + load := synthesizeLoad(numLabelsA, numLabelsB, numSteps) + return promqltest.LoadedStorage(b, load) +} + +func createRequestsMetricBlock(b *testing.B, numRequests int, numSuccess int) *tsdb.DB { + dir := b.TempDir() + + db, err := tsdb.Open(dir, nil, nil, tsdb.DefaultOptions(), nil) + testutil.Ok(b, err) + appender := db.Appender(context.Background()) + + sixHours := int64(6 * 60 * 2) + + for i := range numRequests { + for t := int64(0); t < sixHours; t += 30 { + code := "200" + if numSuccess < i { + code = "500" + } + lbls := labels.FromStrings(labels.MetricName, "http_requests_total", "code", code, "pod", strconv.Itoa(i)) + _, err = appender.Append(0, lbls, t, 1) + testutil.Ok(b, err) + } + } + + testutil.Ok(b, appender.Commit()) + + return db +} + +func synthesizeLoad(numPods, numContainers, numSteps int) string { + var sb strings.Builder + sb.WriteString("load 30s\n") + for i := range numPods { + for j := range numContainers { + sb.WriteString(fmt.Sprintf(`http_requests_total{pod="p%d", container="c%d"} %d+%dx%d%s`, i, j, i, j, numSteps, "\n")) + } + sb.WriteString(fmt.Sprintf(`http_responses_total{pod="p%d"} %dx%d%s`, i, i, numSteps, "\n")) + } + + for i := range numPods { + for j := range 10 { + sb.WriteString(fmt.Sprintf(`http_response_seconds_bucket{pod="p%d", le="%d"} %d+%dx%d%s`, i, j, i, j, numSteps, "\n")) + } + sb.WriteString(fmt.Sprintf(`http_response_seconds_bucket{pod="p%d", le="+Inf"} %d+%dx%d%s`, i, i, i, numSteps, "\n")) + } + + return sb.String() +} diff --git a/internal/promql-engine/engine/distributed.go b/internal/promql-engine/engine/distributed.go new file mode 100644 index 00000000000..c9fa1c6916c --- /dev/null +++ b/internal/promql-engine/engine/distributed.go @@ -0,0 +1,148 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package engine + +import ( + "context" + "time" + + "github.com/thanos-io/promql-engine/api" + "github.com/thanos-io/promql-engine/logicalplan" + + "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/promql" + "github.com/prometheus/prometheus/storage" +) + +type remoteEngine struct { + q storage.Queryable + engine *Engine + labelSets []labels.Labels + maxt int64 + mint int64 +} + +func NewRemoteEngine(opts Opts, q storage.Queryable, mint, maxt int64, labelSets []labels.Labels) *remoteEngine { + return &remoteEngine{ + q: q, + labelSets: labelSets, + maxt: maxt, + mint: mint, + engine: New(opts), + } +} + +func (l remoteEngine) MaxT() int64 { + return l.maxt +} + +func (l remoteEngine) MinT() int64 { + return l.mint +} + +func (l remoteEngine) LabelSets() []labels.Labels { + return l.labelSets +} + +func (l remoteEngine) PartitionLabelSets() []labels.Labels { + return l.labelSets +} + +func (l remoteEngine) NewRangeQuery(ctx context.Context, opts promql.QueryOpts, plan api.RemoteQuery, start, end time.Time, interval time.Duration) (promql.Query, error) { + return l.engine.NewRangeQuery(ctx, l.q, opts, plan.String(), start, end, interval) +} + +type DistributedEngine struct { + engine *Engine +} + +func NewDistributedEngine(opts Opts) *DistributedEngine { + return &DistributedEngine{ + engine: New(opts), + } +} + +func (l DistributedEngine) MakeInstantQueryFromPlan(ctx context.Context, q storage.Queryable, e api.RemoteEndpoints, opts promql.QueryOpts, plan logicalplan.Node, ts time.Time) (promql.Query, error) { + // Truncate milliseconds to avoid mismatch in timestamps between remote and local engines. + // Some clients might only support second precision when executing queries. + ts = ts.Truncate(time.Second) + + // Cache engines to give optimizers a consistent view of Engines(). + // Some RemoteEndpoints implementations also compute and cache + // MinT() / MaxT() / LabelSets() on the fly, so the cache prevents + // recomputing those fields in each optimizer. + e = api.NewCachedEndpoints(e) + + qOpts := fromPromQLOpts(opts) + qOpts.LogicalOptimizers = []logicalplan.Optimizer{ + logicalplan.PassthroughOptimizer{Endpoints: e}, + logicalplan.DistributedExecutionOptimizer{Endpoints: e}, + } + + return l.engine.MakeInstantQueryFromPlan(ctx, q, qOpts, plan, ts) +} + +func (l DistributedEngine) MakeRangeQueryFromPlan(ctx context.Context, q storage.Queryable, e api.RemoteEndpoints, opts promql.QueryOpts, plan logicalplan.Node, start, end time.Time, interval time.Duration) (promql.Query, error) { + // Truncate milliseconds to avoid mismatch in timestamps between remote and local engines. + // Some clients might only support second precision when executing queries. + start = start.Truncate(time.Second) + end = end.Truncate(time.Second) + interval = interval.Truncate(time.Second) + + // Cache engines to give optimizers a consistent view of Engines(). + // Some RemoteEndpoints implementations also compute and cache + // MinT() / MaxT() / LabelSets() on the fly, so the cache prevents + // recomputing those fields in each optimizer. + e = api.NewCachedEndpoints(e) + + qOpts := fromPromQLOpts(opts) + qOpts.LogicalOptimizers = []logicalplan.Optimizer{ + logicalplan.PassthroughOptimizer{Endpoints: e}, + logicalplan.DistributedExecutionOptimizer{Endpoints: e}, + } + + return l.engine.MakeRangeQueryFromPlan(ctx, q, qOpts, plan, start, end, interval) +} + +func (l DistributedEngine) MakeInstantQuery(ctx context.Context, q storage.Queryable, e api.RemoteEndpoints, opts promql.QueryOpts, qs string, ts time.Time) (promql.Query, error) { + // Truncate milliseconds to avoid mismatch in timestamps between remote and local engines. + // Some clients might only support second precision when executing queries. + ts = ts.Truncate(time.Second) + + // Cache engines to give optimizers a consistent view of Engines(). + // Some RemoteEndpoints implementations also compute and cache + // MinT() / MaxT() / LabelSets() on the fly, so the cache prevents + // recomputing those fields in each optimizer. + e = api.NewCachedEndpoints(e) + + qOpts := fromPromQLOpts(opts) + qOpts.LogicalOptimizers = []logicalplan.Optimizer{ + logicalplan.PassthroughOptimizer{Endpoints: e}, + logicalplan.DistributedExecutionOptimizer{Endpoints: e}, + } + + return l.engine.MakeInstantQuery(ctx, q, qOpts, qs, ts) +} + +func (l DistributedEngine) MakeRangeQuery(ctx context.Context, q storage.Queryable, e api.RemoteEndpoints, opts promql.QueryOpts, qs string, start, end time.Time, interval time.Duration) (promql.Query, error) { + // Truncate milliseconds to avoid mismatch in timestamps between remote and local engines. + // Some clients might only support second precision when executing queries. + start = start.Truncate(time.Second) + end = end.Truncate(time.Second) + interval = interval.Truncate(time.Second) + + // Cache engines to give optimizers a consistent view of Engines(). + // Some RemoteEndpoints implementations also compute and cache + // MinT() / MaxT() / LabelSets() on the fly, so the cache prevents + // recomputing those fields in each optimizer. + e = api.NewCachedEndpoints(e) + + qOpts := fromPromQLOpts(opts) + qOpts.LogicalOptimizers = []logicalplan.Optimizer{ + logicalplan.PassthroughOptimizer{Endpoints: e}, + logicalplan.DistributedExecutionOptimizer{Endpoints: e}, + } + + return l.engine.MakeRangeQuery(ctx, q, qOpts, qs, start, end, interval) +} diff --git a/internal/promql-engine/engine/distributed_test.go b/internal/promql-engine/engine/distributed_test.go new file mode 100644 index 00000000000..747f1947d47 --- /dev/null +++ b/internal/promql-engine/engine/distributed_test.go @@ -0,0 +1,453 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package engine_test + +import ( + "context" + "fmt" + "math" + "testing" + "time" + + "github.com/thanos-io/promql-engine/api" + "github.com/thanos-io/promql-engine/engine" + + "github.com/efficientgo/core/errors" + "github.com/efficientgo/core/testutil" + "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/promql" + "github.com/prometheus/prometheus/storage" + "github.com/prometheus/prometheus/util/annotations" +) + +type partition struct { + series []*mockSeries + extLset []labels.Labels +} + +func (p partition) maxt() int64 { + var maxt int64 = math.MinInt64 + for _, s := range p.series { + ts := s.timestamps[len(s.timestamps)-1] + if ts > maxt { + maxt = ts + } + } + + return maxt +} + +func (p partition) mint() int64 { + mint := p.series[0].timestamps[0] + for _, s := range p.series { + ts := s.timestamps[0] + if ts > mint { + mint = ts + } + } + + return mint +} + +func TestDistributedAggregations(t *testing.T) { + t.Parallel() + + instantTSs := []time.Time{ + time.Unix(75, 0), + time.Unix(121, 0), + time.Unix(600, 0), + } + rangeStart := time.Unix(0, 0) + rangeEnd := time.Unix(120, 0) + rangeStep := time.Second * 30 + + makeSeries := func(zone, pod string) []string { + return []string{labels.MetricName, "bar", "zone", zone, "pod", pod} + } + + makeSeriesWithName := func(name, zone, pod string) []string { + return []string{labels.MetricName, name, "zone", zone, "pod", pod} + } + + tests := []struct { + name string + seriesSets []partition + timeOverlap partition + rangeEnd time.Time + }{ + { + name: "base case", + seriesSets: []partition{{ + extLset: []labels.Labels{labels.FromStrings("zone", "east-1")}, + series: []*mockSeries{ + newMockSeries(makeSeries("east-1", "nginx-1"), []int64{30, 60, 90, 120}, []float64{2, 3, 4, 5}), + newMockSeries(makeSeries("east-1", "nginx-2"), []int64{30, 60, 90, 120}, []float64{3, 4, 5, 6}), + }, + }, { + extLset: []labels.Labels{ + labels.FromStrings("zone", "west-1"), + labels.FromStrings("zone", "west-2"), + }, + series: []*mockSeries{ + newMockSeries(makeSeries("west-1", "nginx-1"), []int64{30, 60, 90, 120}, []float64{4, 5, 6, 7}), + newMockSeries(makeSeries("west-1", "nginx-2"), []int64{30, 60, 90, 120}, []float64{5, 6, 7, 8}), + newMockSeries(makeSeries("west-2", "nginx-1"), []int64{30, 60, 90, 120}, []float64{6, 7, 8, 9}), + }, + }}, + timeOverlap: partition{ + extLset: []labels.Labels{ + labels.FromStrings("zone", "east-1"), + labels.FromStrings("zone", "west-1"), + labels.FromStrings("zone", "west-2"), + }, + series: []*mockSeries{ + newMockSeries(makeSeries("east-1", "nginx-1"), []int64{30, 60}, []float64{2, 3}), + newMockSeries(makeSeries("east-1", "nginx-2"), []int64{30, 60}, []float64{3, 4}), + newMockSeries(makeSeries("west-1", "nginx-1"), []int64{30, 60}, []float64{4, 5}), + newMockSeries(makeSeries("west-1", "nginx-2"), []int64{30, 60}, []float64{5, 6}), + newMockSeries(makeSeries("west-2", "nginx-1"), []int64{30, 60}, []float64{6, 7}), + }, + }, + }, + { + // Repro for https://github.com/thanos-io/promql-engine/issues/187. + name: "series with different ranges in a newer engine", + seriesSets: []partition{{ + extLset: []labels.Labels{labels.FromStrings("zone", "east-1"), labels.FromStrings("zone", "east-1")}, + series: []*mockSeries{ + newMockSeries(makeSeries("east-1", "nginx-1"), []int64{60, 90, 120}, []float64{3, 4, 5}), + newMockSeries(makeSeries("east-2", "nginx-1"), []int64{30, 60, 90, 120}, []float64{3, 4, 5, 6}), + }}, + }, + timeOverlap: partition{ + extLset: []labels.Labels{labels.FromStrings("zone", "east-1"), labels.FromStrings("zone", "east-2")}, + series: []*mockSeries{ + newMockSeries(makeSeries("east-1", "nginx-1"), []int64{30, 60}, []float64{2, 3}), + newMockSeries(makeSeries("east-2", "nginx-1"), []int64{30, 60}, []float64{3, 4}), + }, + }, + }, + { + name: "verify double lookback is not applied", + seriesSets: []partition{{ + extLset: []labels.Labels{labels.FromStrings("zone", "east-2")}, + series: []*mockSeries{ + newMockSeries(makeSeries("east-2", "nginx-1"), []int64{30, 60, 90, 120}, []float64{3, 4, 5, 6}), + }}, + }, + timeOverlap: partition{ + extLset: []labels.Labels{labels.FromStrings("zone", "east-2")}, + series: []*mockSeries{ + newMockSeries(makeSeries("east-2", "nginx-1"), []int64{30, 60}, []float64{3, 4}), + }, + }, + rangeEnd: time.Unix(15000, 0), + }, + { + name: "count by __name__ label", + seriesSets: []partition{{ + extLset: []labels.Labels{labels.FromStrings("zone", "east-2")}, + series: []*mockSeries{ + newMockSeries(makeSeriesWithName("foo", "east-2", "nginx-1"), []int64{30, 60, 90, 120}, []float64{3, 4, 5, 6}), + newMockSeries(makeSeriesWithName("bar", "east-2", "nginx-1"), []int64{30, 60, 90, 120}, []float64{3, 4, 5, 6}), + }, + }, { + extLset: []labels.Labels{labels.FromStrings("zone", "east-2"), labels.FromStrings("zone", "west-1")}, + series: []*mockSeries{ + newMockSeries(makeSeriesWithName("xyz", "east-2", "nginx-1"), []int64{30, 60, 90, 120}, []float64{3, 4, 5, 6}), + }, + }}, + timeOverlap: partition{ + series: []*mockSeries{ + newMockSeries(makeSeriesWithName("foo", "east-2", "nginx-1"), []int64{30, 60}, []float64{3, 4}), + newMockSeries(makeSeriesWithName("bar", "east-2", "nginx-1"), []int64{30, 60}, []float64{3, 4}), + newMockSeries(makeSeriesWithName("xyz", "east-2", "nginx-1"), []int64{30, 60}, []float64{3, 4}), + }, + }, + }, + { + name: "engines with different retentions", + seriesSets: []partition{{ + extLset: []labels.Labels{labels.FromStrings("zone", "us-east1")}, + series: []*mockSeries{ + newMockSeries(makeSeries("us-east1", "nginx-1"), []int64{30, 60, 90, 120, 150}, []float64{3, 4, 5, 6, 9}), + }}, { + extLset: []labels.Labels{labels.FromStrings("zone", "us-east2")}, + series: []*mockSeries{ + newMockSeries(makeSeries("us-east2", "nginx-2"), []int64{90, 120, 150}, []float64{7, 9, 11}), + }, + }}, + timeOverlap: partition{ + extLset: []labels.Labels{ + labels.FromStrings("zone", "us-east1"), + labels.FromStrings("zone", "us-east2"), + }, + series: []*mockSeries{ + newMockSeries(makeSeries("us-east1", "nginx-1"), []int64{30, 60, 90}, []float64{3, 4, 5}), + newMockSeries(makeSeries("us-east2", "nginx-2"), []int64{30, 60, 90, 120}, []float64{2, 6, 7, 9}), + }, + }, + rangeEnd: time.Unix(180, 0), + }, + { + // Single engine with two non-overlapping labelsets with same labels + name: "single engine with multiple labelsets", + seriesSets: []partition{{ + extLset: []labels.Labels{ + labels.FromStrings("zone", "east-1"), + labels.FromStrings("zone", "west-1"), + }, + series: []*mockSeries{ + newMockSeries(makeSeries("east-1", "nginx-1"), []int64{30, 60, 90, 120}, []float64{2, 3, 4, 5}), + newMockSeries(makeSeries("east-1", "nginx-2"), []int64{30, 60, 90, 120}, []float64{3, 4, 5, 6}), + newMockSeries(makeSeries("west-1", "nginx-1"), []int64{30, 60, 90, 120}, []float64{4, 5, 6, 7}), + newMockSeries(makeSeries("west-1", "nginx-2"), []int64{30, 60, 90, 120}, []float64{5, 6, 7, 8}), + }, + }}, + }, + } + + queries := []struct { + name string + query string + rangeStart time.Time + }{ + {name: "binop with selector and constant series", query: `bar or on () vector(0)`}, + {name: "binop with aggregation and constant series", query: `sum(bar) or on () vector(0)`}, + {name: "sum", query: `sum by (pod) (bar)`}, + {name: "sum by __name__", query: `sum by (__name__) ({__name__=~".+"})`}, + {name: "parenthesis", query: `sum by (pod) ((bar))`}, + {name: "avg", query: `avg(bar)`}, + {name: "avg by __name__", query: `avg by (__name__) ({__name__=~".+"})`}, + {name: "avg with by-grouping", query: `avg by (pod) (bar)`}, + {name: "avg with without-grouping", query: `avg without (pod) (bar)`}, + {name: "label_replace", query: `max by (instance) (label_replace(bar, "instance", "$1", "pod", ".*"))`}, + {name: "label_replace to ext label before aggregation", query: `max(sum(label_replace(bar, "zone", "hardcoded-zone", "zone", "(.*)")))`}, + {name: "label_replace to ext label after aggregation", query: `max(label_replace(sum by (zone) (bar), "zone", "hardcoded-zone", "zone", ".*"))`}, + {name: "count", query: `count by (pod) (bar)`}, + {name: "count by __name__", query: `count by (__name__) ({__name__=~".+"})`}, + {name: "group", query: `group by (pod) (bar)`}, + {name: "topk", query: `topk by (pod) (1, bar)`}, + {name: "bottomk", query: `bottomk by (pod) (1, bar)`}, + {name: "label based pruning with no match", query: `sum by (pod) (bar{zone="north-2"})`}, + {name: "label based pruning with one match", query: `sum by (pod) (bar{zone="east-1"})`}, + {name: "double aggregation", query: `max by (pod) (sum by (pod) (bar))`}, + {name: "aggregation with function operand", query: `sum by (pod) (rate(bar[1m]))`}, + {name: "binary expression with constant operand", query: `sum by (region) (bar * 60)`}, + {name: "binary expression with distributable pairing", query: `sum by (pod) (bar * bar)`}, + {name: "binary expression with non-distributable pairing", query: `sum by (pod) (foo * ignoring (region, zone) xyz)`}, + {name: "binary aggregation", query: `sum by (region) (bar) / sum by (pod) (bar)`}, + {name: "binary nested with constants", query: `(1 + 2) + (1 atan2 (-1 % -1))`}, + {name: "binary nested with functions", query: `(1 + exp(vector(1))) + (1 atan2 (-1 % -1))`}, + {name: "filtered selector interaction", query: `sum by (region) (bar{region="east"}) / sum by (region) (bar)`}, + {name: "unsupported aggregation", query: `count_values("pod", bar)`}, + {name: "absent_over_time for non-existing metric", query: `absent_over_time(foo[2m])`}, + {name: "absent_over_time for existing metric", query: `absent_over_time(bar{pod="nginx-1"}[2m])`}, + {name: "absent for non-existing metric", query: `absent(foo)`}, + {name: "absent for existing metric with aggregation", query: `sum(absent(foo))`}, + {name: "absent for existing metric", query: `absent(bar{pod="nginx-1"})`}, + {name: "absent for existing metric with aggregation", query: `sum(absent(bar{pod="nginx-1"}))`}, + {name: "subquery with sum/count", query: `max_over_time((sum(bar) / count(bar))[30s:15s])`}, + {name: "subquery with avg", query: `max_over_time(avg(bar)[30s:15s])`}, + {name: "subquery with window within engine range", query: `max_over_time(sum_over_time(bar[30s])[30s:15s])`}, + {name: "subquery with window outside of engine range", query: `max_over_time(sum_over_time(bar[1m])[10m:1m])`}, + {name: "subquery with misaligned ranges", rangeStart: time.Unix(7, 0), query: `max_over_time(sum(bar)[30s:15s])`}, + {name: "subquery with misaligned ranges", rangeStart: time.Unix(7, 0), query: `max_over_time(sum(sum(bar))[30s:15s])`}, + {name: "nested subqueries", + rangeStart: time.Unix(7, 0), + query: `max_over_time(min_over_time(sum(bar)[15s:15s])[15s:15s])`, + }, + {name: "subquery over distributed binary expression", query: `max_over_time((bar / bar)[30s:15s])`}, + {name: "timestamp", query: `timestamp(bar)`}, + {name: "timestamp - step invariant", query: `timestamp(bar @ 6000.000)`}, + {name: "query with @start() absolute timestamp", query: `sum(bar @ start())`}, + {name: "query with @end() timestamp", query: `sum(bar @ end())`}, + {name: "query with numeric timestamp", query: `sum(bar @ 140.000)`}, + {name: "query with range and @end() timestamp", query: `sum(count_over_time(bar[1h] @ end()))`}, + {name: `subquery with @end() timestamp`, query: `bar @ 100.000 - bar @ 150.000`}, + {name: "limitk", query: `limitk by (pod,zone) (1, bar)`}, // this is more of a condition where o/p will be consistent with prometheus. In engine, first 'k' samples are chosen based on + // increasing order of sample/histogram ids which are internal to each leaf querier, thus in some rare cases when order of output series is inconsistent, + // the samples will differ from prometheus as root querier can't determine which sample would have occurred first in sequential execution of prometheus, + // this behavior won't be an obstacle as limitk was proposed for an easier way to inspect labels in high cardinality metrics. + {name: "limit_ratio", query: `limit_ratio by (pod) (1, bar)`}, + {name: "or with default matching", query: `bar{pod="nginx-1"} or bar{pod="nginx-2"}`}, + {name: "or with on() including partition", query: `bar{pod="nginx-1"} or on (zone) bar{pod="nginx-2"}`}, + {name: "or with on() excluding partition", query: `bar{pod="nginx-1"} or on (pod) bar{pod="nginx-2"}`}, + {name: "or with ignoring() excluding partition", query: `bar{pod="nginx-1"} or ignoring (pod) bar{pod="nginx-2"}`}, + {name: "or with ignoring() including partition", query: `bar{pod="nginx-1"} or ignoring (zone) bar{pod="nginx-2"}`}, + {name: "sum over or", query: `sum by (zone) (bar{pod="nginx-1"} or bar{pod="nginx-2"})`}, + {name: "topk over or by partition", query: `topk(2, bar{pod="nginx-1"} or on (zone) bar{pod="nginx-2"})`}, + {name: "or between aggregations", query: `sum by (zone) (bar{pod="nginx-1"}) or sum by (zone) (bar{pod="nginx-2"})`}, + {name: "or with partial metric coverage", query: `foo or bar`}, + {name: "or with partial metric coverage and aggregation", query: `sum by (zone) (foo or bar)`}, + {name: "or with partial metric coverage sum", query: `sum(foo or bar)`}, + {name: "unless with default matching", query: `bar{pod="nginx-1"} unless bar{pod="nginx-2"}`}, + {name: "unless with on() including partition", query: `bar{pod="nginx-1"} unless on (zone) bar{pod="nginx-2"}`}, + {name: "unless with on() excluding partition", query: `bar{pod="nginx-1"} unless on (pod) bar{pod="nginx-2"}`}, + {name: "unless with ignoring() excluding partition", query: `bar{pod="nginx-1"} unless ignoring (pod) bar{pod="nginx-2"}`}, + {name: "unless with ignoring() including partition", query: `bar{pod="nginx-1"} unless ignoring (zone) bar{pod="nginx-2"}`}, + {name: "sum over unless", query: `sum by (zone) (bar{pod="nginx-1"} unless bar{pod="nginx-2"})`}, + {name: "topk over unless by partition", query: `topk(2, bar{pod="nginx-1"} unless on (zone) bar{pod="nginx-2"})`}, + {name: "unless between aggregations", query: `sum by (zone) (bar{pod="nginx-1"}) unless sum by (zone) (bar{pod="nginx-2"})`}, + {name: "or with partition-specific selector on RHS", query: `bar or bar{zone="east-1"}`}, + {name: "or with partition-specific selector on LHS", query: `bar{zone="east-1"} or bar`}, + {name: "unless with partition-specific selector on RHS", query: `bar unless bar{zone="east-1"}`}, + {name: "unless with partition-specific selector on LHS", query: `bar{zone="east-1"} unless bar{zone="west-1"}`}, + {name: "group_left with partition label in include", query: `bar * on (pod) group_left (zone) bar{pod="nginx-1"}`}, + {name: "group_right with partition label in include", query: `bar{pod="nginx-1"} * on (pod) group_right (zone) bar`}, + {name: "group_left without partition label", query: `bar * on (zone) group_left (pod) bar{zone="east-1"}`}, + {name: "group_right without partition label", query: `bar{zone="east-1"} * on (zone) group_right (pod) bar`}, + {name: "max over sum by partition", query: `max(sum by (zone, pod) (bar))`}, + {name: "max over max by partition", query: `max(max by (zone) (bar))`}, + {name: "min over sum by partition", query: `min(sum by (zone, pod) (bar))`}, + {name: "min over min by partition", query: `min(min by (zone) (bar))`}, + {name: "count over sum by partition", query: `count(sum by (zone, pod) (bar))`}, + {name: "sum over max by partition", query: `sum(max by (zone) (bar))`}, + {name: "min over count by partition", query: `min(count by (zone) (bar))`}, + {name: "sum over max by zone", query: `sum(max by (zone) (bar))`}, + {name: "max over sum over rate by partition", query: `max(sum by (zone) (rate(bar[1m])))`}, + {name: "min over avg by partition", query: `min(avg by (zone) (bar))`}, + {name: "max over max over sum by partition", query: `max(max(sum by (zone) (bar)))`}, + {name: "sum over min over max by partition", query: `sum(min(max by (zone, pod) (bar)))`}, + {name: "max over sum without partition", query: `max(sum by (pod) (bar))`}, + {name: "min over max without partition", query: `min(max(bar))`}, + {name: "max over binary op by partition", query: `max(sum by (zone) (bar) / count by (zone) (bar))`}, + {name: "count over max by partition", query: `count(max by (zone) (bar))`}, + {name: "group over sum by partition", query: `group(sum by (zone) (bar))`}, + {name: "max over binary with on() by partition", query: `max(bar * on (zone, pod) bar)`}, + {name: "max over sum with without() by partition", query: `max(sum without (pod) (bar))`}, + {name: "max over sum with without() without partition", query: `max(sum without (zone) (bar))`}, + {name: "sum over binary with nested group", query: `sum(bar * group by (zone) (bar))`}, + {name: "max over binary with nested sum", query: `max(bar + sum by (zone, pod) (bar))`}, + {name: "count over binary with nested max", query: `count(bar / max by (zone) (bar))`}, + {name: "sum over complex binary with nested aggregations", query: `sum(bar * sum by (zone, pod) (bar) + bar)`}, + {name: "min over binary with nested count", query: `min(bar - count by (zone) (bar))`}, + {name: "avg nested inside binary", query: `sum(bar * avg by (zone) (bar))`}, + {name: "avg nested inside sum", query: `sum(avg by (pod) (bar))`}, + {name: "max over avg", query: `max(avg by (zone) (bar))`}, + {name: "avg with binary operand", query: `avg by (zone) (bar * bar)`}, + {name: "multiple nested aggregations", query: `max(sum by (zone) (bar) + count by (zone) (bar))`}, + } + + lookbackDeltas := []time.Duration{0, 30 * time.Second, 5 * time.Minute} + allQueryOpts := []promql.QueryOpts{nil} + for _, l := range lookbackDeltas { + allQueryOpts = append(allQueryOpts, promql.NewPrometheusQueryOpts(false, l)) + } + + for _, query := range queries { + t.Run(query.name, func(t *testing.T) { + t.Parallel() + for _, test := range tests { + var allSeries []*mockSeries + remoteEngines := make([]api.RemoteEngine, 0, len(test.seriesSets)+1) + for _, s := range test.seriesSets { + allSeries = append(allSeries, s.series...) + } + if len(test.timeOverlap.series) > 0 { + allSeries = append(allSeries, test.timeOverlap.series...) + } + completeSeriesSet := storageWithSeries(mergeWithSampleDedup(allSeries)...) + t.Run(test.name, func(t *testing.T) { + for _, lookbackDelta := range lookbackDeltas { + opts := engine.Opts{ + EngineOpts: promql.EngineOpts{ + Timeout: 1 * time.Hour, + MaxSamples: 1e10, + EnableNegativeOffset: true, + EnableAtModifier: true, + LookbackDelta: lookbackDelta, + }, + } + + for _, s := range test.seriesSets { + remoteEngines = append(remoteEngines, engine.NewRemoteEngine( + opts, + storageWithMockSeries(s.series...), + s.mint(), + s.maxt(), + s.extLset, + )) + } + if len(test.timeOverlap.series) > 0 { + remoteEngines = append(remoteEngines, engine.NewRemoteEngine( + opts, + storageWithMockSeries(test.timeOverlap.series...), + test.timeOverlap.mint(), + test.timeOverlap.maxt(), + test.timeOverlap.extLset, + )) + } + endpoints := api.NewStaticEndpoints(remoteEngines) + for _, queryOpts := range allQueryOpts { + ctx := context.Background() + for _, instantTS := range instantTSs { + t.Run(fmt.Sprintf("instant/ts=%d", instantTS.Unix()), func(t *testing.T) { + distEngine := engine.NewDistributedEngine(opts) + distQry, err := distEngine.MakeInstantQuery(ctx, completeSeriesSet, endpoints, queryOpts, query.query, instantTS) + testutil.Ok(t, err) + + distResult := distQry.Exec(ctx) + promEngine := promql.NewEngine(opts.EngineOpts) + promQry, err := promEngine.NewInstantQuery(ctx, completeSeriesSet, queryOpts, query.query, instantTS) + testutil.Ok(t, err) + promResult := promQry.Exec(ctx) + + testutil.WithGoCmp(comparer).Equals(t, promResult, distResult, queryExplanation(distQry)) + }) + } + + t.Run("range", func(t *testing.T) { + if query.rangeStart.IsZero() { + query.rangeStart = rangeStart + } + if test.rangeEnd.IsZero() { + test.rangeEnd = rangeEnd + } + distEngine := engine.NewDistributedEngine(opts) + distQry, err := distEngine.MakeRangeQuery(ctx, completeSeriesSet, endpoints, queryOpts, query.query, query.rangeStart, test.rangeEnd, rangeStep) + testutil.Ok(t, err) + + distResult := distQry.Exec(ctx) + promEngine := promql.NewEngine(opts.EngineOpts) + promQry, err := promEngine.NewRangeQuery(ctx, completeSeriesSet, queryOpts, query.query, query.rangeStart, test.rangeEnd, rangeStep) + testutil.Ok(t, err) + promResult := promQry.Exec(ctx) + + testutil.WithGoCmp(comparer).Equals(t, promResult, distResult, queryExplanation(distQry)) + }) + } + } + }) + } + }) + } +} + +func TestDistributedEngineWarnings(t *testing.T) { + t.Parallel() + + opts := engine.Opts{ + EngineOpts: promql.EngineOpts{ + MaxSamples: math.MaxInt64, + Timeout: 1 * time.Minute, + }, + } + + querier := &storage.MockQueryable{ + MockQuerier: &storage.MockQuerier{ + SelectMockFunction: func(sortSeries bool, hints *storage.SelectHints, matchers ...*labels.Matcher) storage.SeriesSet { + return newWarningsSeriesSet(annotations.New().Add(errors.New("test warning"))) + }, + }, + } + remote := engine.NewRemoteEngine(opts, querier, math.MinInt64, math.MaxInt64, nil) + endpoints := api.NewStaticEndpoints([]api.RemoteEngine{remote}) + ng := engine.NewDistributedEngine(opts) + q, err := ng.MakeInstantQuery(context.Background(), querier, endpoints, nil, "test", time.UnixMilli(0)) + testutil.Ok(t, err) + + res := q.Exec(context.Background()) + testutil.Equals(t, 1, len(res.Warnings)) +} diff --git a/internal/promql-engine/engine/engine.go b/internal/promql-engine/engine/engine.go new file mode 100644 index 00000000000..8390da1531f --- /dev/null +++ b/internal/promql-engine/engine/engine.go @@ -0,0 +1,754 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package engine + +import ( + "context" + "log/slog" + "maps" + "math" + "runtime" + "slices" + "sort" + "time" + + "github.com/thanos-io/promql-engine/execution" + "github.com/thanos-io/promql-engine/execution/model" + "github.com/thanos-io/promql-engine/execution/parse" + "github.com/thanos-io/promql-engine/execution/telemetry" + "github.com/thanos-io/promql-engine/extlabels" + "github.com/thanos-io/promql-engine/logicalplan" + "github.com/thanos-io/promql-engine/query" + engstorage "github.com/thanos-io/promql-engine/storage" + promstorage "github.com/thanos-io/promql-engine/storage/prometheus" + "github.com/thanos-io/promql-engine/warnings" + + "github.com/efficientgo/core/errors" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "github.com/prometheus/common/promslog" + "github.com/prometheus/prometheus/promql" + "github.com/prometheus/prometheus/promql/parser" + "github.com/prometheus/prometheus/storage" + "github.com/prometheus/prometheus/util/annotations" + "github.com/prometheus/prometheus/util/stats" +) + +type QueryType int + +type engineMetrics struct { + currentQueries prometheus.Gauge + totalQueries prometheus.Counter +} + +const ( + namespace string = "thanos" + subsystem string = "engine" + InstantQuery QueryType = 1 + RangeQuery QueryType = 2 + stepsBatch = 10 +) + +func IsUnimplemented(err error) bool { + return errors.Is(err, parse.ErrNotSupportedExpr) || errors.Is(err, parse.ErrNotImplemented) +} + +type Opts struct { + promql.EngineOpts + + // LogicalOptimizers are optimizers that are run if the value is not nil. If it is nil then the default optimizers are run. Default optimizer list is available in the logicalplan package. + LogicalOptimizers []logicalplan.Optimizer + + // ExtLookbackDelta specifies what time range to use to determine valid previous sample for extended range functions. + // Defaults to 1 hour if not specified. + ExtLookbackDelta time.Duration + + // DecodingConcurrency is the maximum number of goroutines that can be used to decode samples. Defaults to GOMAXPROCS / 2. + DecodingConcurrency int + + // SelectorBatchSize specifies the maximum number of samples to be returned by selectors in a single batch. + SelectorBatchSize int64 + + // EnableXFunctions enables custom xRate, xIncrease and xDelta functions. + // This will default to false. + EnableXFunctions bool + + // EnableAnalysis enables query analysis. + EnableAnalysis bool + + // The Prometheus engine has internal check for duplicate labels produced by functions, aggregations or binary operators. + // This check can produce false positives when querying time-series data which does not conform to the Prometheus data model, + // and can be disabled if it leads to false positives. + DisableDuplicateLabelChecks bool +} + +// QueryOpts implements promql.QueryOpts but allows to override more engine default options. +type QueryOpts struct { + // These values are used to implement promql.QueryOpts, they have weird "Param" suffix because + // they are accessed by methods of the same name. + LookbackDeltaParam time.Duration + EnablePerStepStatsParam bool + + // DecodingConcurrency can be used to override the DecodingConcurrency engine setting. + DecodingConcurrency int + + // SelectorBatchSize can be used to override the SelectorBatchSize engine setting. + SelectorBatchSize int64 + + // LogicalOptimizers can be used to override the LogicalOptimizers engine setting. + LogicalOptimizers []logicalplan.Optimizer +} + +func (opts QueryOpts) LookbackDelta() time.Duration { return opts.LookbackDeltaParam } +func (opts QueryOpts) EnablePerStepStats() bool { return opts.EnablePerStepStatsParam } + +func fromPromQLOpts(opts promql.QueryOpts) *QueryOpts { + if opts == nil { + return &QueryOpts{} + } + return &QueryOpts{ + LookbackDeltaParam: opts.LookbackDelta(), + EnablePerStepStatsParam: opts.EnablePerStepStats(), + } +} + +// New creates a new query engine with the given options. The query engine will +// use the storage passed in NewInstantQuery and NewRangeQuery for retrieving +// data when executing queries. +func New(opts Opts) *Engine { + return NewWithScanners(opts, nil) +} + +// NewWithScanners creates a new query engine with the given options and storage.Scanners. +// When executing queries, the engine will create scanner operators using the storage.Scanners and will ignore the +// Prometheus storage passed in NewInstantQuery and NewRangeQuery. +// This method is useful when the data being queried does not easily fit into the Prometheus storage model. +func NewWithScanners(opts Opts, scanners engstorage.Scanners) *Engine { + if opts.Logger == nil { + opts.Logger = promslog.NewNopLogger() + } + if opts.LookbackDelta == 0 { + opts.LookbackDelta = 5 * time.Minute + opts.Logger.Debug("lookback delta is zero, setting to default value", "value", 5*time.Minute) + } + if opts.ExtLookbackDelta == 0 { + opts.ExtLookbackDelta = 1 * time.Hour + opts.Logger.Debug("external lookback delta is zero, setting to default value", "value", 1*time.Hour) + } + if len(opts.LogicalOptimizers) == 0 { + opts.LogicalOptimizers = append( + opts.LogicalOptimizers, logicalplan.DefaultOptimizers..., + ) + } + + functions := make(map[string]*parser.Function, len(parser.Functions)) + maps.Copy(functions, parser.Functions) + if opts.EnableXFunctions { + maps.Copy(functions, parse.XFunctions) + } + + metrics := &engineMetrics{ + currentQueries: promauto.With(opts.Reg).NewGauge( + prometheus.GaugeOpts{ + Namespace: namespace, + Subsystem: subsystem, + Name: "queries", + Help: "The current number of queries being executed or waiting.", + }, + ), + totalQueries: promauto.With(opts.Reg).NewCounter( + prometheus.CounterOpts{ + Namespace: namespace, + Subsystem: subsystem, + Name: "queries_total", + Help: "Number of PromQL queries.", + }, + ), + } + + decodingConcurrency := opts.DecodingConcurrency + if opts.DecodingConcurrency < 1 { + decodingConcurrency = max(runtime.GOMAXPROCS(0)/2, 1) + } + selectorBatchSize := opts.SelectorBatchSize + + var queryTracker promql.QueryTracker = nopQueryTracker{} + if opts.ActiveQueryTracker != nil { + queryTracker = opts.ActiveQueryTracker + } + + return &Engine{ + functions: functions, + scanners: scanners, + activeQueryTracker: queryTracker, + parser: opts.Parser, + + disableDuplicateLabelChecks: opts.DisableDuplicateLabelChecks, + + logger: opts.Logger, + lookbackDelta: opts.LookbackDelta, + enablePerStepStats: opts.EnablePerStepStats, + logicalOptimizers: opts.LogicalOptimizers, + timeout: opts.Timeout, + metrics: metrics, + extLookbackDelta: opts.ExtLookbackDelta, + enableAnalysis: opts.EnableAnalysis, + noStepSubqueryIntervalFn: func(d time.Duration) time.Duration { + return time.Duration(opts.NoStepSubqueryIntervalFn(d.Milliseconds()) * 1000000) + }, + decodingConcurrency: decodingConcurrency, + selectorBatchSize: selectorBatchSize, + maxSamplesPerQuery: opts.MaxSamples, + } +} + +var ( + // Duplicate label checking logic uses a bitmap with 64 bits currently. + // As long as we use this method we need to have batches that are smaller + // then 64 steps. + ErrStepsBatchTooLarge = errors.New("'StepsBatch' must be less than 64") +) + +type Engine struct { + functions map[string]*parser.Function + scanners engstorage.Scanners + activeQueryTracker promql.QueryTracker + parser parser.Parser + + disableDuplicateLabelChecks bool + + logger *slog.Logger + lookbackDelta time.Duration + enablePerStepStats bool + logicalOptimizers []logicalplan.Optimizer + timeout time.Duration + metrics *engineMetrics + + extLookbackDelta time.Duration + decodingConcurrency int + selectorBatchSize int64 + enableAnalysis bool + noStepSubqueryIntervalFn func(time.Duration) time.Duration + maxSamplesPerQuery int +} + +func (e *Engine) parseExpr(qs string) (parser.Expr, error) { + orig := parser.Functions + parser.Functions = e.functions + defer func() { parser.Functions = orig }() + + p := e.parser + if p == nil { + p = parser.NewParser(parser.Options{}) + } + return p.ParseExpr(qs) +} + +func (e *Engine) MakeInstantQuery(ctx context.Context, q storage.Queryable, opts *QueryOpts, qs string, ts time.Time) (promql.Query, error) { + idx, err := e.activeQueryTracker.Insert(ctx, qs) + if err != nil { + return nil, err + } + defer e.activeQueryTracker.Delete(idx) + + expr, err := e.parseExpr(qs) + if err != nil { + return nil, err + } + // determine sorting order before optimizers run, we do this by looking for "sort" + // and "sort_desc" and optimize them away afterwards since they are only needed at + // the presentation layer and not when computing the results. + resultSort := newResultSort(expr) + + qOpts := e.makeQueryOpts(ts, ts, 0, opts) + if qOpts.StepsBatch > 64 { + return nil, ErrStepsBatchTooLarge + } + + planOpts := logicalplan.PlanOptions{ + DisableDuplicateLabelCheck: e.disableDuplicateLabelChecks, + } + initialPlan, err := logicalplan.NewFromAST(expr, qOpts, planOpts) + if err != nil { + return nil, errors.Wrap(err, "creating plan") + } + optimizedPlan, warns := initialPlan.Optimize(e.getLogicalOptimizers(opts)) + + ctx = warnings.NewContext(ctx) + defer func() { warns.Merge(warnings.FromContext(ctx)) }() + + scanners, err := e.storageScanners(q, qOpts, optimizedPlan) + if err != nil { + return nil, errors.Wrap(err, "creating storage scanners") + } + + exec, err := execution.New(ctx, optimizedPlan.Root(), scanners, qOpts) + if err != nil { + return nil, err + } + e.metrics.totalQueries.Inc() + return &compatibilityQuery{ + Query: &Query{exec: exec, opts: qOpts}, + engine: e, + plan: optimizedPlan, + warns: warns, + ts: ts, + t: InstantQuery, + resultSort: resultSort, + scanners: scanners, + }, nil +} + +func (e *Engine) MakeInstantQueryFromPlan(ctx context.Context, q storage.Queryable, opts *QueryOpts, root logicalplan.Node, ts time.Time) (promql.Query, error) { + idx, err := e.activeQueryTracker.Insert(ctx, root.String()) + if err != nil { + return nil, err + } + defer e.activeQueryTracker.Delete(idx) + + qOpts := e.makeQueryOpts(ts, ts, 0, opts) + if qOpts.StepsBatch > 64 { + return nil, ErrStepsBatchTooLarge + } + planOpts := logicalplan.PlanOptions{ + DisableDuplicateLabelCheck: e.disableDuplicateLabelChecks, + } + lplan, warns := logicalplan.New(root, qOpts, planOpts).Optimize(e.getLogicalOptimizers(opts)) + + ctx = warnings.NewContext(ctx) + defer func() { warns.Merge(warnings.FromContext(ctx)) }() + + scnrs, err := e.storageScanners(q, qOpts, lplan) + if err != nil { + return nil, errors.Wrap(err, "creating storage scanners") + } + + exec, err := execution.New(ctx, lplan.Root(), scnrs, qOpts) + if err != nil { + return nil, err + } + e.metrics.totalQueries.Inc() + + return &compatibilityQuery{ + Query: &Query{exec: exec, opts: qOpts}, + engine: e, + plan: lplan, + warns: warns, + ts: ts, + t: InstantQuery, + // TODO(fpetkovski): Infer the sort order from the plan, ideally without copying the newResultSort function. + resultSort: noSortResultSort{}, + scanners: scnrs, + }, nil +} + +func (e *Engine) MakeRangeQuery(ctx context.Context, q storage.Queryable, opts *QueryOpts, qs string, start, end time.Time, step time.Duration) (promql.Query, error) { + idx, err := e.activeQueryTracker.Insert(ctx, qs) + if err != nil { + return nil, err + } + defer e.activeQueryTracker.Delete(idx) + + expr, err := e.parseExpr(qs) + if err != nil { + return nil, err + } + + // Use same check as Prometheus for range queries. + if expr.Type() != parser.ValueTypeVector && expr.Type() != parser.ValueTypeScalar { + return nil, errors.Newf("invalid expression type %q for range query, must be Scalar or instant Vector", parser.DocumentedType(expr.Type())) + } + qOpts := e.makeQueryOpts(start, end, step, opts) + if qOpts.StepsBatch > 64 { + return nil, ErrStepsBatchTooLarge + } + planOpts := logicalplan.PlanOptions{ + DisableDuplicateLabelCheck: e.disableDuplicateLabelChecks, + } + + initialPlan, err := logicalplan.NewFromAST(expr, qOpts, planOpts) + if err != nil { + return nil, errors.Wrap(err, "creating plan") + } + optimizedPlan, warns := initialPlan.Optimize(e.getLogicalOptimizers(opts)) + + ctx = warnings.NewContext(ctx) + defer func() { warns.Merge(warnings.FromContext(ctx)) }() + + scnrs, err := e.storageScanners(q, qOpts, optimizedPlan) + if err != nil { + return nil, errors.Wrap(err, "creating storage scanners") + } + + exec, err := execution.New(ctx, optimizedPlan.Root(), scnrs, qOpts) + if err != nil { + return nil, err + } + e.metrics.totalQueries.Inc() + + return &compatibilityQuery{ + Query: &Query{exec: exec, opts: qOpts}, + engine: e, + plan: optimizedPlan, + warns: warns, + t: RangeQuery, + scanners: scnrs, + }, nil +} + +func (e *Engine) MakeRangeQueryFromPlan(ctx context.Context, q storage.Queryable, opts *QueryOpts, root logicalplan.Node, start, end time.Time, step time.Duration) (promql.Query, error) { + idx, err := e.activeQueryTracker.Insert(ctx, root.String()) + if err != nil { + return nil, err + } + defer e.activeQueryTracker.Delete(idx) + + qOpts := e.makeQueryOpts(start, end, step, opts) + if qOpts.StepsBatch > 64 { + return nil, ErrStepsBatchTooLarge + } + planOpts := logicalplan.PlanOptions{ + DisableDuplicateLabelCheck: e.disableDuplicateLabelChecks, + } + lplan, warns := logicalplan.New(root, qOpts, planOpts).Optimize(e.getLogicalOptimizers(opts)) + + scnrs, err := e.storageScanners(q, qOpts, lplan) + if err != nil { + return nil, errors.Wrap(err, "creating storage scanners") + } + + ctx = warnings.NewContext(ctx) + defer func() { warns.Merge(warnings.FromContext(ctx)) }() + exec, err := execution.New(ctx, lplan.Root(), scnrs, qOpts) + if err != nil { + return nil, err + } + e.metrics.totalQueries.Inc() + + return &compatibilityQuery{ + Query: &Query{exec: exec, opts: qOpts}, + engine: e, + plan: lplan, + warns: warns, + t: RangeQuery, + scanners: scnrs, + }, nil +} + +// PromQL compatibility constructors + +// NewInstantQuery implements the promql.Engine interface. +func (e *Engine) NewInstantQuery(ctx context.Context, q storage.Queryable, opts promql.QueryOpts, qs string, ts time.Time) (promql.Query, error) { + return e.MakeInstantQuery(ctx, q, fromPromQLOpts(opts), qs, ts) +} + +// NewRangeQuery implements the promql.Engine interface. +func (e *Engine) NewRangeQuery(ctx context.Context, q storage.Queryable, opts promql.QueryOpts, qs string, start, end time.Time, step time.Duration) (promql.Query, error) { + return e.MakeRangeQuery(ctx, q, fromPromQLOpts(opts), qs, start, end, step) +} + +func (e *Engine) makeQueryOpts(start time.Time, end time.Time, step time.Duration, opts *QueryOpts) *query.Options { + res := &query.Options{ + Start: start, + End: end, + Step: step, + StepsBatch: stepsBatch, + LookbackDelta: e.lookbackDelta, + EnablePerStepStats: e.enablePerStepStats, + ExtLookbackDelta: e.extLookbackDelta, + EnableAnalysis: e.enableAnalysis, + NoStepSubqueryIntervalFn: e.noStepSubqueryIntervalFn, + DecodingConcurrency: e.decodingConcurrency, + SampleTracker: query.NewSampleTracker(e.maxSamplesPerQuery), + } + + if opts == nil { + return res + } + + if opts.LookbackDelta() > 0 { + res.LookbackDelta = opts.LookbackDelta() + } + if opts.EnablePerStepStats() { + res.EnablePerStepStats = opts.EnablePerStepStats() + } + + if opts.DecodingConcurrency != 0 { + res.DecodingConcurrency = opts.DecodingConcurrency + } + + return res +} + +func (e *Engine) getLogicalOptimizers(opts *QueryOpts) []logicalplan.Optimizer { + var optimizers []logicalplan.Optimizer + if len(opts.LogicalOptimizers) != 0 { + optimizers = slices.Clone(opts.LogicalOptimizers) + } else { + optimizers = slices.Clone(e.logicalOptimizers) + } + selectorBatchSize := e.selectorBatchSize + if opts.SelectorBatchSize != 0 { + selectorBatchSize = opts.SelectorBatchSize + } + return append(optimizers, logicalplan.SelectorBatchSize{Size: selectorBatchSize}) +} + +func (e *Engine) storageScanners(queryable storage.Queryable, qOpts *query.Options, lplan logicalplan.Plan) (engstorage.Scanners, error) { + if e.scanners == nil { + return promstorage.NewPrometheusScanners(queryable, qOpts, lplan) + } + return e.scanners, nil +} + +type Query struct { + exec model.VectorOperator + opts *query.Options +} + +// Explain returns human-readable explanation of the created executor. +func (q *Query) Explain() *ExplainOutputNode { + // TODO(bwplotka): Explain plan and steps. + return explainVector(q.exec) +} + +func (q *Query) Analyze() *AnalyzeOutputNode { + return analyzeQuery(q.exec) +} + +type compatibilityQuery struct { + *Query + engine *Engine + plan logicalplan.Plan + ts time.Time // Empty for range queries. + warns annotations.Annotations + + t QueryType + resultSort resultSorter + cancel context.CancelFunc + + scanners engstorage.Scanners +} + +func (q *compatibilityQuery) Exec(ctx context.Context) (ret *promql.Result) { + idx, err := q.engine.activeQueryTracker.Insert(ctx, q.String()) + if err != nil { + return &promql.Result{Err: err} + } + defer q.engine.activeQueryTracker.Delete(idx) + + ctx = warnings.NewContext(ctx) + warnings.MergeToContext(q.warns, ctx) + + // Handle case with strings early on as this does not need us to process samples. + switch e := q.plan.Root().(type) { + case *logicalplan.StringLiteral: + return &promql.Result{Value: promql.String{V: e.Val, T: q.ts.UnixMilli()}} + } + ret = &promql.Result{ + Value: promql.Vector{}, + } + defer recoverEngine(q.engine.logger, q.plan, &ret.Err) + + q.engine.metrics.currentQueries.Inc() + defer q.engine.metrics.currentQueries.Dec() + + ctx, cancel := context.WithTimeout(ctx, q.engine.timeout) + defer cancel() + q.cancel = cancel + + resultSeries, err := q.Query.exec.Series(ctx) + if err != nil { + return newErrResult(ret, err) + } + + totalSteps := q.opts.TotalSteps() + series := make([]promql.Series, len(resultSeries)) + for i, s := range resultSeries { + series[i].Metric = s + } + + buf := make([]model.StepVector, q.opts.StepsBatch) +loop: + for { + select { + case <-ctx.Done(): + return newErrResult(ret, ctx.Err()) + default: + n, err := q.Query.exec.Next(ctx, buf) + if err != nil { + return newErrResult(ret, err) + } + if n == 0 { + break loop + } + + // Case where Series call might return nil, but samples are present. + // For example scalar(http_request_total) where http_request_total has multiple values. + if len(series) == 0 && n > 0 { + series = make([]promql.Series, len(buf[0].Samples)) + } + + for i := range n { + vector := &buf[i] + for j, s := range vector.SampleIDs { + if series[s].Floats == nil { + series[s].Floats = make([]promql.FPoint, 0, totalSteps) + } + series[s].Floats = append(series[s].Floats, promql.FPoint{ + T: vector.T, + F: vector.Samples[j], + }) + } + for j, s := range vector.HistogramIDs { + if series[s].Histograms == nil { + series[s].Histograms = make([]promql.HPoint, 0, totalSteps) + } + series[s].Histograms = append(series[s].Histograms, promql.HPoint{ + T: vector.T, + H: vector.Histograms[j], + }) + } + } + } + } + + // For range Query we expect always a Matrix value type. + if q.t == RangeQuery { + matrix := make(promql.Matrix, 0, len(series)) + for _, s := range series { + if len(s.Floats)+len(s.Histograms) == 0 { + continue + } + matrix = append(matrix, s) + } + sort.Sort(matrix) + ret.Value = matrix + ret.Warnings = warnings.FromContext(ctx) + if matrix.ContainsSameLabelset() { + return newErrResult(ret, extlabels.ErrDuplicateLabelSet) + } + return ret + } + + var result parser.Value + switch q.plan.Root().ReturnType() { + case parser.ValueTypeMatrix: + result = promql.Matrix(series) + case parser.ValueTypeVector: + // Convert matrix with one value per series into vector. + vector := make(promql.Vector, 0, len(resultSeries)) + for i := range series { + if len(series[i].Floats)+len(series[i].Histograms) == 0 { + continue + } + // Point might have a different timestamp, force it to the evaluation + // timestamp as that is when we ran the evaluation. + if len(series[i].Floats) > 0 { + vector = append(vector, promql.Sample{ + Metric: series[i].Metric, + F: series[i].Floats[0].F, + T: q.ts.UnixMilli(), + }) + } else { + vector = append(vector, promql.Sample{ + Metric: series[i].Metric, + H: series[i].Histograms[0].H, + T: q.ts.UnixMilli(), + }) + } + } + + if !q.resultSort.keepHistograms() { + vector = filterFloats(vector) + } + sort.Slice(vector, q.resultSort.comparer(&vector)) + if vector.ContainsSameLabelset() { + return newErrResult(ret, extlabels.ErrDuplicateLabelSet) + } + result = vector + case parser.ValueTypeScalar: + v := math.NaN() + if len(series) != 0 { + v = series[0].Floats[0].F + } + result = promql.Scalar{V: v, T: q.ts.UnixMilli()} + default: + panic(errors.Newf("new.Engine.exec: unexpected expression type %q", q.plan.Root().ReturnType())) + } + + ret.Value = result + ret.Warnings = warnings.FromContext(ctx) + return ret +} + +func newErrResult(r *promql.Result, err error) *promql.Result { + if r == nil { + r = &promql.Result{} + } + if r.Err == nil && err != nil { + r.Err = err + } + return r +} + +func (q *compatibilityQuery) Statement() parser.Statement { return nil } + +// Stats always returns empty query stats for now to avoid panic. +func (q *compatibilityQuery) Stats() *stats.Statistics { + enablePerStepStats := q.opts.EnablePerStepStats + + analysis := q.Analyze() + samples := stats.NewQuerySamples(enablePerStepStats) + if enablePerStepStats { + samples.InitStepTracking(q.opts.Start.UnixMilli(), q.opts.End.UnixMilli(), telemetry.StepTrackingInterval(q.opts.Step)) + } + + if analysis != nil { + samples.PeakSamples = int(analysis.PeakSamples()) + samples.TotalSamples = analysis.TotalSamples() + samples.TotalSamplesPerStep = analysis.TotalSamplesPerStep() + } + + return &stats.Statistics{Timers: stats.NewQueryTimers(), Samples: samples} +} + +func (q *compatibilityQuery) Close() { + if err := q.scanners.Close(); err != nil { + q.engine.logger.Warn("error closing storage scanners, some memory might have leaked", "err", err) + } +} + +func (q *compatibilityQuery) String() string { return q.plan.Root().String() } + +func (q *compatibilityQuery) Cancel() { + if q.cancel != nil { + q.cancel() + q.cancel = nil + } +} + +type nopQueryTracker struct{} + +func (n nopQueryTracker) GetMaxConcurrent() int { return -1 } +func (n nopQueryTracker) Insert(ctx context.Context, query string) (int, error) { return 0, nil } +func (n nopQueryTracker) Delete(insertIndex int) {} +func (n nopQueryTracker) Close() error { return nil } + +func recoverEngine(logger *slog.Logger, plan logicalplan.Plan, errp *error) { + e := recover() + if e == nil { + return + } + + switch err := e.(type) { + case runtime.Error: + // Print the stack trace but do not inhibit the running application. + buf := make([]byte, 64<<10) + buf = buf[:runtime.Stack(buf, false)] + + logger.Error("runtime panic in engine", "expr", plan.Root().String(), "err", e, "stacktrace", string(buf)) + *errp = errors.Wrap(err, "unexpected error") + } +} diff --git a/internal/promql-engine/engine/engine_test.go b/internal/promql-engine/engine/engine_test.go new file mode 100644 index 00000000000..9889b8f2ccc --- /dev/null +++ b/internal/promql-engine/engine/engine_test.go @@ -0,0 +1,6780 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package engine_test + +import ( + "bytes" + "context" + "fmt" + "io" + "math" + "os" + "reflect" + "runtime" + "runtime/pprof" + "slices" + "sort" + "strconv" + "strings" + "sync" + "testing" + "time" + + "github.com/thanos-io/promql-engine/engine" + "github.com/thanos-io/promql-engine/execution/model" + "github.com/thanos-io/promql-engine/extlabels" + "github.com/thanos-io/promql-engine/logicalplan" + "github.com/thanos-io/promql-engine/query" + "github.com/thanos-io/promql-engine/storage/prometheus" + "github.com/thanos-io/promql-engine/warnings" + + "github.com/efficientgo/core/errors" + "github.com/efficientgo/core/testutil" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/prometheus/common/promslog" + "github.com/prometheus/prometheus/model/histogram" + "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/model/timestamp" + "github.com/prometheus/prometheus/promql" + "github.com/prometheus/prometheus/promql/parser" + "github.com/prometheus/prometheus/promql/promqltest" + "github.com/prometheus/prometheus/storage" + "github.com/prometheus/prometheus/tsdb/chunkenc" + "github.com/prometheus/prometheus/tsdb/tsdbutil" + "github.com/prometheus/prometheus/util/annotations" + "github.com/prometheus/prometheus/util/stats" + "github.com/prometheus/prometheus/util/teststorage" + "github.com/stretchr/testify/require" + "go.uber.org/goleak" + "golang.org/x/exp/maps" +) + +func TestMain(m *testing.M) { + parser.EnableExperimentalFunctions = true + goleak.VerifyTestMain(m, + // https://github.com/census-instrumentation/opencensus-go/blob/d7677d6af5953e0506ac4c08f349c62b917a443a/stats/view/worker.go#L34 + goleak.IgnoreTopFunction("go.opencensus.io/stats/view.(*worker).start"), + ) +} + +type skipTest struct { + skipTests []string + promqltest.TBRun +} + +func (s *skipTest) Run(name string, t func(*testing.T)) bool { + if slices.Contains(s.skipTests, name) { + return true + } + + return s.TBRun.Run(name, t) +} + +func TestPromqlAcceptance(t *testing.T) { + // promql acceptance tests disable experimental functions again + // since we use them in our tests too we need to enable them afterwards again + t.Cleanup(func() { parser.EnableExperimentalFunctions = true }) + + engine := engine.New(engine.Opts{ + EngineOpts: promql.EngineOpts{ + Logger: promslog.NewNopLogger(), + EnableAtModifier: true, + EnableNegativeOffset: true, + MaxSamples: 5e10, + Timeout: 1 * time.Hour, + NoStepSubqueryIntervalFn: func(rangeMillis int64) int64 { return 30 * time.Second.Milliseconds() }, + }}) + + st := &skipTest{ + skipTests: []string{ + "testdata/name_label_dropping.test", // feature unsupported + "testdata/type_and_unit.test", // feature unsupported + "testdata/extended_vectors.test", // experimental anchored/smoothed modifiers unsupported + "testdata/info.test", // info() function unsupported + "testdata/literals.test", // string literal expressions as query results unsupported + "testdata/range_queries.test", // matrix selector as instant query result unsupported + }, // TODO(sungjin1212): change to test whole cases + TBRun: t, + } + + promqltest.RunBuiltinTests(st, engine) +} + +func TestVectorSelectorWithGaps(t *testing.T) { + t.Parallel() + opts := promql.EngineOpts{ + Timeout: 1 * time.Hour, + MaxSamples: 1e10, + EnableNegativeOffset: true, + EnableAtModifier: true, + } + + series := storage.MockSeries( + []int64{240, 270, 300, 600, 630, 660}, + []float64{1, 2, 3, 4, 5, 6}, + []string{labels.MetricName, "foo"}, + ) + + query := "foo" + start := time.Unix(0, 0) + end := time.Unix(1000, 0) + + ctx := context.Background() + newEngine := engine.New(engine.Opts{EngineOpts: opts}) + q1, err := newEngine.NewRangeQuery(ctx, storageWithSeries(series), nil, query, start, end, 30*time.Second) + testutil.Ok(t, err) + defer q1.Close() + + newResult := q1.Exec(ctx) + testutil.Ok(t, newResult.Err) + + oldEngine := promql.NewEngine(opts) + q2, err := oldEngine.NewRangeQuery(ctx, storageWithSeries(series), nil, query, start, end, 30*time.Second) + testutil.Ok(t, err) + defer q2.Close() + + oldResult := q2.Exec(context.Background()) + testutil.Ok(t, oldResult.Err) + + testutil.WithGoCmp(comparer).Equals(t, oldResult, newResult, queryExplanation(q1)) +} + +type queryableCloseChecker struct { + closed bool + + storage.Queryable +} + +func (q *queryableCloseChecker) Querier(mint, maxt int64) (storage.Querier, error) { + qr, err := q.Queryable.Querier(mint, maxt) + if err != nil { + return nil, err + } + return &querierCloseChecker{Querier: qr, closed: &q.closed}, nil +} + +type querierCloseChecker struct { + storage.Querier + + closed *bool +} + +func (q *querierCloseChecker) Close() error { + *q.closed = true + return q.Querier.Close() +} + +// TestQuerierClosedAfterQueryClosed tests that the querier is only closed +// after the query is closed. +func TestQuerierClosedAfterQueryClosed(t *testing.T) { + t.Parallel() + opts := promql.EngineOpts{ + Timeout: 1 * time.Hour, + MaxSamples: 1e10, + EnableNegativeOffset: true, + EnableAtModifier: true, + } + + load := `load 30s + http_requests_total{pod="nginx-1", route="/"} 41.00+0.20x40 + http_requests_total{pod="nginx-2", route="/"} 51+21.71x40` + + storage := promqltest.LoadedStorage(t, load) + defer storage.Close() + + optimizers := logicalplan.AllOptimizers + newEngine := engine.New(engine.Opts{ + EngineOpts: opts, + LogicalOptimizers: optimizers, + // Set to 1 to make sure batching is tested. + SelectorBatchSize: 1, + }) + ctx := context.Background() + qr := &queryableCloseChecker{ + Queryable: storage, + } + q1, err := newEngine.NewInstantQuery(ctx, qr, nil, "sum(http_requests_total)", time.Unix(0, 0)) + testutil.Ok(t, err) + _ = q1.Exec(ctx) + + require.Equal(t, false, qr.closed) + q1.Close() + + require.Equal(t, true, qr.closed) +} + +func TestQueriesAgainstOldEngine(t *testing.T) { + t.Parallel() + start := time.Unix(0, 0) + end := time.Unix(1800, 0) + step := time.Second * 30 + // Negative offset and at modifier are enabled by default + // since Prometheus v2.33.0 so we also enable them. + opts := promql.EngineOpts{ + Timeout: 1 * time.Hour, + MaxSamples: 1e10, + EnableNegativeOffset: true, + EnableAtModifier: true, + } + + cases := []struct { + load string + name string + query string + start time.Time + end time.Time + step time.Duration + }{ + { + name: "fuzz parser crash", + load: `load 30s + http_requests_total{pod="nginx-1", route="/"} 46.00+13.00x40 + http_requests_total{pod="nginx-2", route="/"} 2+5.25x40`, + query: ` +avg( + {__name__="http_requests_total",route!="/"} offset -4m43s + ^ + {__name__="http_requests_total",route!="/"} +)`, + }, + { + name: "fuzz", + load: `load 30s + http_requests_total{pod="nginx-1", route="/"} 46.00+13.00x40 + http_requests_total{pod="nginx-2", route="/"} 2+5.25x40`, + query: `sum(quantile by (route) (-0.5044968945760265, {__name__="http_requests_total",route="/"}))`, + }, + { + name: "fuzz -0", + load: `load 30s + http_requests_total{pod="nginx-1", route="/"} 46.00+1.00x40 + http_requests_total{pod="nginx-2", route="/"} -6+4.20x40`, + query: `count_values without (pod, route) ("value", -1 * http_requests_total * 0)`, + }, + { + name: "predict_linear fuzz", + load: `load 30s + http_requests_total{pod="nginx-1", route="/"} 48.00+9.17x40 + http_requests_total{pod="nginx-2", route="/"} -108+173.00x40`, + query: `predict_linear(http_requests_total{route="/"}[1h:1m] offset 1m, 60)`, + }, + { + name: "duplicate label fuzz", + load: `load 30s + http_requests_total{pod="nginx-1", route="/"} 41.00+0.20x40 + http_requests_total{pod="nginx-2", route="/"} 51+21.71x40`, + query: ` +-avg by (__name__) ( + (-group({__name__="http_requests_total"} @ 54.013) or {__name__="http_requests_total"} offset 1m32s) +)`, + }, + { + name: "timestamp fuzz 1", + load: `load 30s + http_requests_total{pod="nginx-1", route="/"} 0.20+9.00x40 + http_requests_total{pod="nginx-2", route="/"} 6+60.00x40`, + query: `timestamp(last_over_time(http_requests_total{route="/"}[1h]))`, + }, + { + name: "timestamp fuzz 2", + load: `load 30s + http_requests_total{pod="nginx-1", route="/"} 8.00+9.17x40 + http_requests_total{pod="nginx-2", route="/"} -12+103.00x40`, + query: ` +timestamp( + http_requests_total{pod="nginx-1"} >= bool (http_requests_total < 2 * http_requests_total) +)`, + }, + { + name: "timestamp with multiple parenthesis", + load: `load 30s + http_requests_total{pod="nginx-1", route="/"} 8.00+9.17x40 + http_requests_total{pod="nginx-2", route="/"} -12+103.00x40`, + query: `timestamp((http_requests_total))`, + }, + { + name: "subqueries in binary expression", + load: `load 30s + http_requests_total{pod="nginx-1", route="/"} 1.00+0.20x40 + http_requests_total{pod="nginx-2", route="/"} -44+2.00x40`, + query: ` + absent_over_time(http_requests_total @ end()[1h:1m]) +or + avg_over_time(http_requests_total @ end()[1h:1m])`, + }, + + { + name: "nested unary negation", + query: `1 / (-(2 * 2))`, + }, + { + name: "stddev with large values", + load: `load 30s + http_requests_total{pod="nginx-1", route="/"} 1e+181 + http_requests_total{pod="nginx-2", route="/"} 1e+80`, + query: `stddev(http_requests_total)`, + }, + { + name: "stddev with NaN 1", + load: `load 30s + http_requests_total{pod="nginx-1", route="/"} NaN + http_requests_total{pod="nginx-2", route="/"} 1`, + query: `stddev by (route) (http_requests_total)`, + }, + { + name: "stddev with NaN 2", + load: `load 30s + http_requests_total{pod="nginx-1", route="/"} NaN + http_requests_total{pod="nginx-2", route="/"} 1`, + query: `stddev by (pod) (http_requests_total)`, + }, + { + name: "aggregate without", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1.1x1 + http_requests_total{pod="nginx-2"} 2+2.3x1`, + start: time.Unix(0, 0), + end: time.Unix(60, 0), + step: 30 * time.Second, + query: `avg without (pod) (http_requests_total)`, + }, + { + name: "avg fuzz", + load: `load 30s + http_requests_total{pod="nginx-1", route="/"} NaN NaN NaN NaN NaN 0.000053234+0.000003x10 NaN NaN + http_requests_total{pod="nginx-2", route="/"} NaN NaN NaN NaN NaN 0.00004123412+0.000004x10 NaN NaN`, + query: `avg(stdvar_over_time(http_requests_total[2m:1m]))`, + start: time.Unix(0, 0), + end: time.Unix(300, 0), + }, + { + name: "func with scalar arg that selects storage, checks whether same series handled correctly", + load: `load 30s + thanos_cache_redis_hits_total{name="caching-bucket",service="thanos-store"} 1+1x30`, + query: ` + clamp_min(thanos_cache_redis_hits_total, scalar(max by (service) (thanos_cache_redis_hits_total))) ++ + clamp_min(thanos_cache_redis_hits_total, scalar(max by (service) (thanos_cache_redis_hits_total)))`, + }, + { + name: "sum + rate divided by itself", + load: `load 30s + thanos_cache_redis_hits_total{name="caching-bucket",service="thanos-store"} 1+1x30`, + query: ` + (sum by (service) (rate(thanos_cache_redis_hits_total{name="caching-bucket"}[2m]))) +/ + (sum by (service) (rate(thanos_cache_redis_hits_total{name="caching-bucket"}[2m])))`, + }, + { + name: "stddev_over_time", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `stddev_over_time(http_requests_total[30s])`, + }, + { + name: "stdvar_over_time", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `stdvar_over_time(http_requests_total[30s])`, + }, + { + name: "quantile_over_time", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `quantile_over_time(0.9, http_requests_total[1m])`, + }, + { + name: "quantile_over_time with subquery", + load: `load 30s + http_requests_total{pod="nginx-1"} 41.00+0.20x40 + http_requests_total{pod="nginx-2"} 51+21.71x40`, + query: `quantile_over_time(0.5, http_requests_total{pod="nginx-1"}[5m:1m])`, + start: start, + end: end, + }, + { + name: "quantile_over_time with subquery and non-constant param", + load: `load 30s + http_requests_total{pod="nginx-1"} 41.00+0.20x40 + http_requests_total{pod="nginx-2"} 51+21.71x40 + param_series 0+0.01x40`, + query: `quantile_over_time(scalar(param_series), http_requests_total{pod="nginx-1"}[5m:1m])`, + start: start, + end: end, + }, + { + name: "predict_linear with subquery and non-constant param", + load: `load 30s + http_requests_total{pod="nginx-1"} 41.00+0.20x40 + http_requests_total{pod="nginx-2"} 51+21.71x40 + param_series 1+1x40`, + query: `predict_linear(http_requests_total{pod="nginx-1"}[5m:1m], scalar(param_series))`, + start: start, + end: end, + }, + { + name: "predict_linear with subquery and non-existing param series", + load: `load 30s + http_requests_total{pod="nginx-1"} 41.00+0.20x40 + http_requests_total{pod="nginx-2"} 51+21.71x40`, + query: `predict_linear(http_requests_total{pod="nginx-1"}[5m:1m], scalar(non_existent))`, + start: start, + end: end, + }, + { + name: "changes", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18 + http_requests_total{pod="nginx-2"} 1+2x18 + http_requests_total{pod="nginx-2"} 1+2x18 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `changes(http_requests_total[30s])`, + }, + { + name: "deriv", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18 + http_requests_total{pod="nginx-2"} 1+2x18 + http_requests_total{pod="nginx-2"} 1+2x18 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `deriv(http_requests_total[30s])`, + }, + { + name: "abs", + load: `load 30s + http_requests_total{pod="nginx-1"} -5+1x15 + http_requests_total{pod="nginx-2"} -5+2x18`, + query: `abs(http_requests_total)`, + }, + { + name: "ceil", + load: `load 30s + http_requests_total{pod="nginx-1"} -5.5+1x15 + http_requests_total{pod="nginx-2"} -5.5+2x18`, + query: `ceil(http_requests_total)`, + }, + { + name: "exp", + load: `load 30s + http_requests_total{pod="nginx-1"} -5.5+1x15 + http_requests_total{pod="nginx-2"} -5.5+2x18`, + query: `exp(http_requests_total)`, + }, + { + name: "floor", + load: `load 30s + http_requests_total{pod="nginx-1"} -5.5+1x15 + http_requests_total{pod="nginx-2"} -5.5+2x18`, + query: `floor(http_requests_total)`, + }, + { + name: "floor with a filter", + load: `load 30s + http_requests_total{pod="nginx-1", route="/"} 1 + http_requests_total{pod="nginx-2", route="/"} 2`, + query: `floor(http_requests_total{pod="nginx-2"}) / http_requests_total`, + }, + { + name: "sqrt", + load: `load 30s + http_requests_total{pod="nginx-1"} 5.5+1x15 + http_requests_total{pod="nginx-2"} 5.5+2x18`, + query: `sqrt(http_requests_total)`, + }, + { + name: "ln", + load: `load 30s + http_requests_total{pod="nginx-1"} 5.5+1x15 + http_requests_total{pod="nginx-2"} 5.5+2x18`, + query: `ln(http_requests_total)`, + }, + { + name: "log2", + load: `load 30s + http_requests_total{pod="nginx-1"} 5.5+1x15 + http_requests_total{pod="nginx-2"} 5.5+2x18`, + query: `log2(http_requests_total)`, + }, + { + name: "log10", + load: `load 30s + http_requests_total{pod="nginx-1"} 5.5+1x15 + http_requests_total{pod="nginx-2"} 5.5+2x18`, + query: `log10(http_requests_total)`, + }, + { + name: "sin", + load: `load 30s + http_requests_total{pod="nginx-1"} 5.5+1x15 + http_requests_total{pod="nginx-2"} 5.5+2x18`, + query: `sin(http_requests_total)`, + }, + { + name: "cos", + load: `load 30s + http_requests_total{pod="nginx-1"} 5.5+1x15 + http_requests_total{pod="nginx-2"} 5.5+2x18`, + query: `cos(http_requests_total)`, + }, + { + name: "tan", + load: `load 30s + http_requests_total{pod="nginx-1"} 5.5+1x15 + http_requests_total{pod="nginx-2"} 5.5+2x18`, + query: `tan(http_requests_total)`, + }, + { + name: "asin", + load: `load 30s + http_requests_total{pod="nginx-1"} 0 + http_requests_total{pod="nginx-2"} 1`, + query: `asin(http_requests_total)`, + }, + { + name: "acos", + load: `load 30s + http_requests_total{pod="nginx-1"} 0 + http_requests_total{pod="nginx-2"} 1`, + query: `acos(http_requests_total)`, + }, + { + name: "atan", + load: `load 30s + http_requests_total{pod="nginx-1"} 0 + http_requests_total{pod="nginx-2"} 1`, + query: `atan(http_requests_total)`, + }, + { + name: "sinh", + load: `load 30s + http_requests_total{pod="nginx-1"} 0 + http_requests_total{pod="nginx-2"} 1`, + query: `sinh(http_requests_total)`, + }, + { + name: "cosh", + load: `load 30s + http_requests_total{pod="nginx-1"} 0 + http_requests_total{pod="nginx-2"} 1`, + query: `cosh(http_requests_total)`, + }, + { + name: "tanh", + load: `load 30s + http_requests_total{pod="nginx-1"} 5.5+1x15 + http_requests_total{pod="nginx-2"} 5.5+2x18`, + query: `tanh(http_requests_total)`, + }, + { + name: "asinh", + load: `load 30s + http_requests_total{pod="nginx-1"} 5.5+1x15 + http_requests_total{pod="nginx-2"} 5.5+2x18`, + query: `asinh(http_requests_total)`, + }, + { + name: "acosh", + load: `load 30s + http_requests_total{pod="nginx-1"} 5.5+1x15 + http_requests_total{pod="nginx-2"} 5.5+2x18`, + query: `acosh(http_requests_total)`, + }, + { + name: "atanh", + load: `load 30s + http_requests_total{pod="nginx-1"} 0 + http_requests_total{pod="nginx-2"} 1`, + query: `atanh(http_requests_total)`, + }, + { + name: "rad", + load: `load 30s + http_requests_total{pod="nginx-1"} 5.5+1x15 + http_requests_total{pod="nginx-2"} 5.5+2x18`, + query: `rad(http_requests_total)`, + }, + { + name: "deg", + load: `load 30s + http_requests_total{pod="nginx-1"} 5.5+1x15 + http_requests_total{pod="nginx-2"} 5.5+2x18`, + query: `deg(http_requests_total)`, + }, + { + name: "pi", + load: ``, + query: `pi()`, + }, + { + name: "sum", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `sum(http_requests_total)`, + }, + { + name: "sum_over_time", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `sum_over_time(http_requests_total[30s])`, + }, + { + name: "count", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `count(http_requests_total)`, + }, + { + name: "count_over_time", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `count_over_time(http_requests_total[30s])`, + }, + { + name: "average", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `avg(http_requests_total)`, + }, + { + name: "avg_over_time", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `avg_over_time(http_requests_total[30s])`, + }, + { + name: "abs", + load: `load 30s + http_requests_total{pod="nginx-1"} -10+1x15 + http_requests_total{pod="nginx-2"} -10+2x18`, + query: `abs(http_requests_total)`, + }, + { + name: "max", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `max(http_requests_total)`, + }, + { + name: "max with only 1 sample", + load: `load 30s + http_requests_total{pod="nginx-1"} -1 + http_requests_total{pod="nginx-2"} 1`, + query: `max by (pod) (http_requests_total)`, + }, + { + name: "max_over_time", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `max_over_time(http_requests_total[30s])`, + }, + { + name: "min", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `min(http_requests_total)`, + }, + { + name: "min with only 1 sample", + load: `load 30s + http_requests_total{pod="nginx-1"} -1 + http_requests_total{pod="nginx-2"} 1`, + query: `min by (pod) (http_requests_total)`, + }, + { + name: "min_over_time", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `min_over_time(http_requests_total[30s])`, + }, + { + name: "count_over_time", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `count_over_time(http_requests_total[30s])`, + }, + { + name: "sum by pod", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18 + http_requests_total{pod="nginx-3"} 1+2x20 + http_requests_total{pod="nginx-4"} 1+2x50`, + query: `sum by (pod) (http_requests_total)`, + }, + { + name: "multi label grouping by", + load: `load 30s + http_requests_total{pod="nginx-1", ns="a"} 1+1x15 + http_requests_total{pod="nginx-2", ns="a"} 1+1x15`, + query: `avg by (pod, ns) (avg_over_time(http_requests_total[2m]))`, + }, + { + name: "multi label grouping without", + load: `load 30s + http_requests_total{pod="nginx-1", ns="a"} 1+1x15 + http_requests_total{pod="nginx-2", ns="a"} 1+1x15`, + query: `avg without (pod, ns) (avg_over_time(http_requests_total[2m]))`, + }, + { + name: "query in the future", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `sum by (pod) (http_requests_total)`, + start: time.Unix(400, 0), + end: time.Unix(3000, 0), + }, + { + name: "count_over_time", + load: `load 30s + http_requests_total{pod="nginx-1"} 1 + http_requests_total{pod="nginx-1"} 1+1x30 + http_requests_total{pod="nginx-2"} 1+2x600`, + query: `count_over_time(http_requests_total[10m])`, + start: time.Unix(60, 0), + end: time.Unix(600, 0), + }, + { + name: "rate", + load: `load 30s + http_requests_total{pod="nginx-1", series="1"} 1+1.1x40 + http_requests_total{pod="nginx-2", series="2"} 2+2.3x50 + http_requests_total{pod="nginx-4", series="3"} 5+2.4x50 + http_requests_total{pod="nginx-5", series="1"} 8.4+2.3x50 + http_requests_total{pod="nginx-6", series="2"} 2.3+2.3x50`, + query: `rate(http_requests_total[1m])`, + start: time.Unix(0, 0), + end: time.Unix(3000, 0), + step: 2 * time.Second, + }, + { + name: "rate with counter reset and step larger than window", + load: `load 30s + http_requests_total{pod="nginx-1", series="1"} 0+1x3 0+1x2 0+1x3`, + query: `rate(http_requests_total[1m])`, + start: time.Unix(0, 0), + end: time.Unix(200, 0), + step: 90 * time.Second, + }, + { + name: "rate with counter reset and step equal to window", + load: `load 30s + http_requests_total{pod="nginx-1", series="1"} 5 1 6 4`, + query: `rate(http_requests_total[1m])`, + start: time.Unix(-60, 0), + end: time.Unix(120, 0), + step: 60 * time.Second, + }, + { + name: "native histogram rate with counter reset and step equal to window", + load: `load 30s + some_metric {{schema:0 sum:1 count:1 buckets:[1]}} {{schema:0 sum:0 count:0 buckets:[1]}} {{schema:0 sum:5 count:4 buckets:[1 2 1]}} {{schema:0 sum:1 count:1 buckets:[1]}}`, + query: `rate(some_metric[1m])`, + start: time.Unix(-60, 0), + end: time.Unix(120, 0), + step: 60 * time.Second, + }, + { + name: "sum rate", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x4 + http_requests_total{pod="nginx-2"} 1+2x4`, + query: `sum(rate(http_requests_total[1m]))`, + }, + { + name: "sum rate with stale series", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x40 + http_requests_total{pod="nginx-2"} 1+2x50 + http_requests_total{pod="nginx-4"} 1+2x50 + http_requests_total{pod="nginx-5"} 1+2x50 + http_requests_total{pod="nginx-6"} 1+2x50`, + query: `sum(rate(http_requests_total[1m]))`, + start: time.Unix(421, 0), + end: time.Unix(3230, 0), + step: 28 * time.Second, + }, + { + name: "delta", + load: `load 30s + http_requests_total{pod="nginx-1", series="1"} 1+1.1x40 + http_requests_total{pod="nginx-2", series="2"} 2+2.3x50 + http_requests_total{pod="nginx-4", series="3"} 5+2.4x50 + http_requests_total{pod="nginx-5", series="1"} 8.4+2.3x50 + http_requests_total{pod="nginx-6", series="2"} 2.3+2.3x50`, + query: `delta(http_requests_total[1m])`, + start: time.Unix(0, 0), + end: time.Unix(3000, 0), + step: 2 * time.Second, + }, + { + name: "increase", + load: `load 30s + http_requests_total{pod="nginx-1", series="1"} 1+1.1x40 + http_requests_total{pod="nginx-2", series="2"} 2+2.3x50 + http_requests_total{pod="nginx-4", series="3"} 5+2.4x50 + http_requests_total{pod="nginx-5", series="1"} 8.4+2.3x50 + http_requests_total{pod="nginx-6", series="2"} 2.3+2.3x50`, + query: `increase(http_requests_total[1m])`, + start: time.Unix(0, 0), + end: time.Unix(3000, 0), + step: 2 * time.Second, + }, + { + name: "irate", + load: `load 30s + http_requests_total{pod="nginx-1", series="1"} 1+1.1x40 + http_requests_total{pod="nginx-2", series="2"} 2+2.3x50 + http_requests_total{pod="nginx-4", series="3"} 5+2.4x50 + http_requests_total{pod="nginx-5", series="1"} 8.4+2.3x50 + http_requests_total{pod="nginx-6", series="2"} 2.3+2.3x50`, + query: `irate(http_requests_total[1m])`, + start: time.Unix(0, 0), + end: time.Unix(3000, 0), + step: 2 * time.Second, + }, + { + name: "idelta", + load: `load 30s + http_requests_total{pod="nginx-1", series="1"} 1+1.1x40 + http_requests_total{pod="nginx-2", series="2"} 2+2.3x50 + http_requests_total{pod="nginx-4", series="3"} 5+2.4x50 + http_requests_total{pod="nginx-5", series="1"} 8.4+2.3x50 + http_requests_total{pod="nginx-6", series="2"} 2.3+2.3x50`, + query: `idelta(http_requests_total[1m])`, + start: time.Unix(0, 0), + end: time.Unix(3000, 0), + step: 2 * time.Second, + }, + { + name: "number literal", + load: "", + query: `34`, + }, + { + name: "vector", + load: "", + query: `vector(24)`, + }, + { + name: "binary operation atan2", + load: `load 30s + foo{} 10 + bar{} 2`, + query: `foo atan2 bar`, + }, + { + name: "binary operation atan2 with NaN", + load: `load 30s + foo{} 10 + bar{} NaN`, + query: `foo atan2 bar`, + }, + { + name: "binary operation with one-to-one matching", + load: `load 30s + foo{method="get", code="500"} 1+1x1 + foo{method="get", code="404"} 2+1x2 + foo{method="put", code="501"} 3+1x3 + foo{method="put", code="500"} 1+1x4 + foo{method="post", code="500"} 4+1x4 + foo{method="post", code="404"} 5+1x5 + bar{method="get"} 1+1x1 + bar{method="del"} 2+1x2 + bar{method="post"} 3+1x3`, + query: `foo{code="500"} + ignoring (code) bar`, + start: time.Unix(0, 0), + end: time.Unix(600, 0), + }, + { + // Example from https://prometheus.io/docs/prometheus/latest/querying/operators/#many-to-one-and-one-to-many-vector-matches + name: "binary operation with group_left", + load: `load 30s + foo{method="get", code="500", path="/"} 1+1.1x30 + foo{method="get", code="404", path="/"} 1+2.2x20 + foo{method="put", code="501", path="/"} 4+3.4x60 + foo{method="post", code="500", path="/"} 1+5.1x40 + foo{method="post", code="404", path="/"} 2+3.7x40 + bar{method="get", path="/a"} 3+7.4x10 + bar{method="del", path="/b"} 8+6.1x30 + bar{method="post", path="/c"} 1+2.1x40`, + query: `foo * ignoring (path, code) group_left () bar`, + start: time.Unix(0, 0), + end: time.Unix(600, 0), + }, + { + // Example from https://prometheus.io/docs/prometheus/latest/querying/operators/#many-to-one-and-one-to-many-vector-matches + name: "binary operation with group_right", + load: `load 30s + foo{method="get", code="500"} 1+1.1x30 + foo{method="get", code="404"} 1+2.2x20 + foo{method="put", code="501"} 4+3.4x60 + foo{method="post", code="500"} 1+5.1x40 + foo{method="post", code="404"} 2+3.7x40 + bar{method="get", path="/a"} 3+7.4x10 + bar{method="del", path="/b"} 8+6.1x30 + bar{method="post", path="/c"} 1+2.1x40`, + query: `bar * ignoring (code, path) group_right () foo`, + start: time.Unix(0, 0), + end: time.Unix(3000, 0), + step: 2 * time.Second, + }, + { + name: "binary operation with group_left and included labels", + load: `load 30s + foo{method="get", code="500"} 1+1.1x30 + foo{method="get", code="404"} 1+2.2x20 + foo{method="put", code="501"} 4+3.4x60 + foo{method="post", code="500"} 1+5.1x40 + foo{method="post", code="404"} 2+3.7x40 + bar{method="get", path="/a"} 3+7.4x10 + bar{method="del", path="/b"} 8+6.1x30 + bar{method="post", path="/c"} 1+2.1x40`, + query: `foo * ignoring (code, path) group_left (path) bar`, + start: time.Unix(0, 0), + end: time.Unix(600, 0), + }, + { + name: "binary operation with group_right and included labels", + load: `load 30s + foo{method="get", code="500"} 1+1.1x30 + foo{method="get", code="404"} 1+2.2x20 + foo{method="put", code="501"} 4+3.4x60 + foo{method="post", code="500"} 1+5.1x40 + foo{method="post", code="404"} 2+3.7x40 + bar{method="get", path="/a"} 3+7.4x10 + bar{method="del", path="/b"} 8+6.1x30 + bar{method="post", path="/c"} 1+2.1x40`, + query: `bar * ignoring (code, path) group_right (path) foo`, + start: time.Unix(0, 0), + end: time.Unix(600, 0), + }, + { + name: "binary operation with vector and scalar on the right", + load: `load 30s + foo{method="get", code="500"} 1+1.1x30 + foo{method="get", code="404"} 1+2.2x20`, + query: `sum(foo) * 2`, + }, + { + name: "binary operation with vector and scalar on the left", + load: `load 30s + foo{method="get", code="500"} 1+1.1x30 + foo{method="get", code="404"} 1+2.2x20`, + query: `2 * sum(foo)`, + }, + { + name: "complex binary operation", + load: `load 30s + foo{method="get", code="500"} 1+1.1x30 + foo{method="get", code="404"} 1+2.2x20`, + query: `1 - (100 * sum(foo{method="get"}) / sum(foo))`, + }, + { + name: "binary operation with many-to-many matching", + load: `load 30s + foo{code="200", method="get"} 1+1x20 + foo{code="200", method="post"} 1+1x20 + bar{code="200", method="get"} 1+1x20 + bar{code="200", method="post"} 1+1x20`, + query: `foo + on (code) bar`, + }, + { + name: "binary operation with many-to-many matching lhs high card", + load: `load 30s + foo{code="200", method="get"} 1+1x20 + foo{code="200", method="post"} 1+1x20 + bar{code="200", method="get"} 1+1x20 + bar{code="200", method="post"} 1+1x20`, + query: `foo + on (code) group_left () bar`, + }, + { + name: "binary operation with many-to-many matching rhs high card", + load: `load 30s + foo{code="200", method="get"} 1+1x20 + foo{code="200", method="post"} 1+1x20 + bar{code="200", method="get"} 1+1x20 + bar{code="200", method="post"} 1+1x20`, + query: `foo + on (code) group_right () bar`, + }, + { + name: "vector binary op ==", + load: `load 30s + foo{method="get", code="500"} 1+1x40 + bar{method="get", code="404"} 1+1.1x30`, + query: `sum by (method) (foo) == sum by (method) (bar)`, + }, + { + name: "vector binary op !=", + load: `load 30s + foo{method="get", code="500"} 1+1x40 + bar{method="get", code="404"} 1+1.1x30`, + query: `sum by (method) (foo) != sum by (method) (bar)`, + }, + { + name: "vector binary op >", + load: `load 30s + foo{method="get", code="500"} 1+1x40 + bar{method="get", code="404"} 1+1.1x30`, + query: `sum by (method) (foo) > sum by (method) (bar)`, + }, + { + name: "vector binary op with name <", + load: `load 30s + foo{method="get", code="500"} 1+1x40 + bar{method="get", code="500"} 1+1.1x30`, + query: `foo < bar`, + }, + { + name: "vector binary op with name < scalar", + load: `load 30s + foo{method="get", code="500"} 1+1x40 + bar{method="get", code="500"} 1+1.1x30`, + query: `foo < 10`, + }, + { + name: "vector binary op with name < scalar and bool modifier", + load: `load 30s + foo{method="get", code="500"} 1+1x40 + bar{method="get", code="500"} 1+1.1x30`, + query: `foo < bool 10`, + }, + { + name: "vector binary op > scalar", + load: `load 30s + foo{method="get", code="500"} 1+2x40 + bar{method="get", code="404"} 1+1x30`, + query: `sum by (method) (foo) > 10`, + }, + { + name: "vector binary op > scalar and bool modifier", + load: `load 30s + foo{method="get", code="500"} 1+2x40 + bar{method="get", code="404"} 1+1x30`, + query: `sum by (method) (foo) > bool 10`, + }, + { + name: "scalar < vector binary op", + load: `load 30s + foo{method="get", code="500"} 1+2x40 + bar{method="get", code="404"} 1+1x30`, + query: `10 < sum by (method) (foo)`, + }, + { + name: "vector binary op <", + load: `load 30s + foo{method="get", code="500"} 1+1x40 + bar{method="get", code="404"} 1+1.1x30`, + query: `sum by (method) (foo) < sum by (method) (bar)`, + }, + { + name: "vector binary op >=", + load: `load 30s + foo{method="get", code="500"} 1+1x40 + bar{method="get", code="404"} 1+1.1x30`, + query: `sum by (method) (foo) >= sum by (method) (bar)`, + }, + { + name: "vector binary op <=", + load: `load 30s + foo{method="get", code="500"} 1+1x40 + bar{method="get", code="404"} 1+1.1x30`, + query: `sum by (method) (foo) <= sum by (method) (bar)`, + }, + { + name: "vector binary op ^", + load: `load 30s + foo{method="get", code="500"} 1+1x40 + bar{method="get", code="404"} 1+1.1x30`, + query: `sum by (method) (foo) ^ sum by (method) (bar)`, + }, + { + name: "vector binary op %", + load: `load 30s + foo{method="get", code="500"} 1+2x40 + bar{method="get", code="404"} 1+1x30`, + query: `sum by (method) (foo) % sum by (method) (bar)`, + }, + { + name: "vector/vector binary op", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18 + http_requests_total{pod="nginx-2"} 1+2x18 + http_requests_total{pod="nginx-2"} 1+2x18 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `(1 + rate(http_requests_total[30s])) > bool rate(http_requests_total[30s])`, + }, + { + name: "vector/scalar binary op with a complicated expression on LHS", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18 + http_requests_total{pod="nginx-2"} 1+2x18 + http_requests_total{pod="nginx-2"} 1+2x18 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `rate(http_requests_total[30s]) > bool 0`, + }, + { + name: "vector/scalar binary op with a complicated expression on RHS", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18 + http_requests_total{pod="nginx-2"} 1+2x18 + http_requests_total{pod="nginx-2"} 1+2x18 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `0 < bool rate(http_requests_total[30s])`, + }, + { + name: "scalar binary op == true", + load: ``, + query: `1 == bool 1`, + }, + { + name: "scalar binary op == false", + load: ``, + query: `1 != bool 2`, + }, + { + name: "scalar binary op !=", + load: ``, + query: `1 != bool 1`, + }, + { + name: "scalar binary op >", + load: ``, + query: `1 > bool 0`, + }, + { + name: "scalar binary op <", + load: ``, + query: `1 < bool 2`, + }, + { + name: "scalar binary op >=", + load: ``, + query: `1 >= bool 0`, + }, + { + name: "scalar binary op <=", + load: ``, + query: `1 <= bool 2`, + }, + { + name: "scalar binary op % 0", + load: ``, + query: `2 % 2`, + }, + { + name: "scalar binary op % 1", + load: ``, + query: `1 % 2`, + }, + { + name: "scalar binary op ^", + load: ``, + query: `2 ^ 2`, + }, + { + name: "empty series", + load: "", + query: `http_requests_total`, + }, + { + name: "time function", + load: "", + query: `time()`, + }, + { + name: "time function in binary expression", + load: "", + query: `time() - 10`, + }, + { + name: "empty series with func", + load: "", + query: `sum(http_requests_total)`, + }, + { + name: "empty result", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `http_requests_total{pod="nginx-3"}`, + }, + { + name: "last_over_time", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `last_over_time(http_requests_total[30s])`, + }, + { + name: "group", + load: `load 30s + http_requests_total{pod="nginx-1"} 2+1x15 + http_requests_total{pod="nginx-2"} 2+2x18`, + query: `group(http_requests_total)`, + }, + { + name: "group by ", + load: `load 30s + http_requests_total{pod="nginx-1"} 2+1x15 + http_requests_total{pod="nginx-2"} 2+2x18`, + query: `group by (pod) (http_requests_total)`, + }, + { + // Issue https://github.com/thanos-io/promql-engine/issues/326. + name: "group by with NaN values", + load: `load 30s + http_requests_total{pod="nginx-1", route="/"} 1.00+1.00x4 + http_requests_total{pod="nginx-2", route="/"} 1+2.00x4`, + query: `group by (pod, route) (atanh(-{__name__="http_requests_total"} offset -3m4s))`, + }, + { + name: "resets", + load: `load 30s + http_requests_total{pod="nginx-1"} 100-1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `resets(http_requests_total[5m])`, + }, + { + name: "present_over_time", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `present_over_time(http_requests_total[30s])`, + }, + { + name: "complex binary with aggregation", + load: `load 30s + grpc_server_handled_total{pod="nginx-1", grpc_method="Series", grpc_code="105"} 1+1x15 + grpc_server_handled_total{pod="nginx-2", grpc_method="Series", grpc_code="105"} 1+1x15 + grpc_server_handled_total{pod="nginx-3", grpc_method="Series", grpc_code="105"} 1+1x15 + prometheus_tsdb_head_samples_appended_total{pod="nginx-1", tenant="tenant-1"} 1+2x18 + prometheus_tsdb_head_samples_appended_total{pod="nginx-2", tenant="tenant-2"} 1+2x18 + prometheus_tsdb_head_samples_appended_total{pod="nginx-3", tenant="tenant-3"} 1+2x18`, + query: ` +sum by (grpc_method, grpc_code) ( + sum by (pod, grpc_method, grpc_code) ( + rate(grpc_server_handled_total{grpc_method="Series",pod=~".+"}[1m]) + ) + + on (pod) group_left () + max by (pod) (prometheus_tsdb_head_samples_appended_total{pod=~".+"}) +)`, + }, + { + name: "unary sub operation for scalar", + load: ``, + query: `-(1 + 5)`, + }, + { + name: "unary sub operation for vector", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `-http_requests_total`, + }, + { + name: "unary add operation for vector", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `+http_requests_total`, + }, + { + name: "vector positive offset", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `http_requests_total offset 30s`, + start: time.Unix(600, 0), + end: time.Unix(1200, 0), + }, + { + name: "vector negative offset", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `http_requests_total offset -30s`, + start: time.Unix(600, 0), + end: time.Unix(1200, 0), + }, + { + name: "matrix negative offset with sum_over_time", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x25 + http_requests_total{pod="nginx-2"} 1+2x28`, + query: `sum_over_time(http_requests_total[5m] offset 5m)`, + start: time.Unix(600, 0), + end: time.Unix(6000, 0), + }, + { + name: "matrix negative offset with count_over_time", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `count_over_time(http_requests_total[5m] offset -2m)`, + start: time.Unix(600, 0), + end: time.Unix(6000, 0), + }, + { + name: "@ vector time 10s", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `http_requests_total @ 10.000`, + }, + { + name: "@ vector time 120s", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `http_requests_total @ 120.000`, + }, + { + name: "@ vector time 360s", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `http_requests_total @ 360.000`, + }, + { + name: "@ vector start", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `http_requests_total @ start()`, + }, + { + name: "@ vector end", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `http_requests_total @ end()`, + }, + { + name: "count_over_time @ start", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `count_over_time(http_requests_total[5m] @ start())`, + }, + { + name: "sum_over_time @ end", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `sum_over_time(http_requests_total[5m] @ start())`, + }, + { + name: "avg_over_time @ 180s", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `avg_over_time(http_requests_total[4m] @ 180.000)`, + }, + { + name: "@ vector 240s offset 2m", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `http_requests_total @ 240.000 offset 2m`, + }, + { + name: "avg_over_time @ 120s offset -2m", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `http_requests_total @ 120.000 offset -2m`, + }, + { + name: "sum_over_time @ 180s offset 2m", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `sum_over_time(http_requests_total[5m] @ 180.000 offset 2m)`, + }, + { + name: "binop with @ end() modifier inside query range", + load: `load 30s + http_requests_total 2+3x100 + http_responses_total 2+4x100`, + query: `max(http_requests_total @ end()) / max(http_responses_total)`, + end: time.Unix(600, 0), + }, + { + name: "binop with @ end() modifier outside of query range", + load: `load 30s + http_requests_total 2+3x100 + http_responses_total 2+4x100`, + query: `max(http_requests_total @ end()) / max(http_responses_total)`, + end: time.Unix(60000, 0), + }, + { + name: "days_in_month with input", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `days_in_month(http_requests_total)`, + }, + { + name: "days_in_month without input", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `days_in_month()`, + }, + { + name: "day_of_month with input", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `day_of_month(http_requests_total)`, + }, + { + name: "day_of_month without input", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `day_of_month()`, + }, + { + name: "day_of_week with input", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `days_in_month(http_requests_total)`, + }, + { + name: "day_of_week without input", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `days_in_month()`, + }, + { + name: "day_of_year with input", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `day_of_year(http_requests_total)`, + }, + { + name: "day_of_year without input", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `day_of_year()`, + }, + { + name: "hour with input", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `hour(http_requests_total)`, + }, + { + name: "hour without input", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `hour()`, + }, + { + name: "minute with input", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `minute(http_requests_total)`, + }, + { + name: "minute without input", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `minute()`, + }, + { + name: "month with input", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `month(http_requests_total)`, + }, + { + name: "month without input", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `month()`, + }, + { + name: "year with input", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `year(http_requests_total)`, + }, + { + name: "year without input", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `year()`, + }, + { + name: "selector merge", + load: `load 30s + http_requests_total{pod="nginx-1", ns="nginx"} 1+1x15 + http_requests_total{pod="nginx-2", ns="nginx"} 1+2x18 + http_requests_total{pod="nginx-3", ns="nginx"} 1+2x21`, + query: ` + http_requests_total{ns="nginx",pod=~"nginx-1"} +/ on () group_left () + sum(http_requests_total{ns="nginx"})`, + }, + { + name: "selector merge with different ranges", + load: `load 30s + http_requests_total{pod="nginx-1", ns="nginx"} 2+2x16 + http_requests_total{pod="nginx-2", ns="nginx"} 2+4x18 + http_requests_total{pod="nginx-3", ns="nginx"} 2+6x20`, + query: ` + rate(http_requests_total{ns="nginx",pod=~"nginx-1"}[2m]) ++ on () group_left () + sum(http_requests_total{ns="nginx"})`, + }, + { + name: "binop with positive matcher using regex, only one side has data", + load: `load 30s + metric{} 1+2x5 + metric{} 1+2x20`, + query: `sum(rate(metric{err=~".+"}[5m])) / sum(rate(metric[5m]))`, + }, + { + name: "binop with positive matcher using regex, both sides have data", + load: `load 30s + metric{} 1+2x5 + metric{err="FooBarKey"} 1+2x20`, + query: `sum(rate(metric{err=~".+"}[5m])) / sum(rate(metric[5m]))`, + }, + { + name: "binop with negative matcher using regex, only one side has data", + load: `load 30s + metric{} 1+2x5 + metric{} 1+2x20`, + query: `sum(rate(metric{err!~".+"}[5m])) / sum(rate(metric[5m]))`, + }, + { + name: "binop with negative matcher using regex, both sides have data", + load: `load 30s + metric{} 1+2x5 + metric{err="FooBarKey"} 1+2x20`, + query: `sum(rate(metric{err!~".+"}[5m])) / sum(rate(metric[5m]))`, + }, + { + name: "scalar func with NaN", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} NaN`, + query: `scalar(http_requests_total)`, + }, + { + name: "scalar func with aggr", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `scalar(max(http_requests_total))`, + }, + { + name: "scalar func with aggr and number on right", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `scalar(max(http_requests_total)) + 10`, + }, + { + name: "scalar func with aggr and number on left", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `10 + scalar(max(http_requests_total))`, + }, + { + name: "quantile with param series", + load: `load 30s + http_requests_total{pod="nginx-1", series="1"} 1+1.1x40 + http_requests_total{pod="nginx-2", series="2"} 2+2.3x50 + http_requests_total{pod="nginx-4", series="3"} 5+2.4x50 + http_requests_total{pod="nginx-5", series="1"} 8.4+2.3x50 + http_requests_total{pod="nginx-6", series="2"} 2.3+2.3x50 + param_series 0+0.1x50`, + query: `quantile(scalar(param_series), rate(http_requests_total[1m]))`, + }, + { + name: "quantile with param series that evaluates to NaN", + load: `load 30s + http_requests_total{pod="nginx-1", series="1"} 1+1.1x40 + http_requests_total{pod="nginx-2", series="2"} 2+2.3x50 + http_requests_total{pod="nginx-4", series="3"} 5+2.4x50 + http_requests_total{pod="nginx-5", series="1"} 8.4+2.3x50 + http_requests_total{pod="nginx-6", series="2"} 2.3+2.3x50 + param_series NaN+0x50`, + query: `quantile(scalar(param_series), rate(http_requests_total[1m]))`, + }, + { + name: "quantile with non-existing param series", + load: `load 30s + http_requests_total{pod="nginx-1", series="1"} 1+1.1x40 + http_requests_total{pod="nginx-2", series="2"} 2+2.3x50 + http_requests_total{pod="nginx-4", series="3"} 5+2.4x50 + http_requests_total{pod="nginx-5", series="1"} 8.4+2.3x50 + http_requests_total{pod="nginx-6", series="2"} 2.3+2.3x50`, + query: `quantile(scalar(non_existent), rate(http_requests_total[1m]))`, + }, + { + name: "clamp", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `clamp(http_requests_total, 5, 10)`, + }, + { + name: "clamp_min", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `clamp_min(http_requests_total, 10)`, + }, + { + name: "complex func query", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `clamp(1 - http_requests_total, 10 - 5, 10)`, + }, + { + name: "func within func query", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `clamp(irate(http_requests_total[30s]), 10 - 5, 10)`, + }, + { + name: "aggr within func query", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `clamp(rate(http_requests_total[30s]), 10 - 5, 10)`, + }, + { + name: "func with scalar arg that selects storage", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `clamp_min(http_requests_total, scalar(max(http_requests_total)))`, + }, + { + name: "func with scalar arg that selects storage + number", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `clamp_min(http_requests_total, scalar(max(http_requests_total)) + 10)`, + }, + { + name: "histogram quantile", + load: `load 30s + http_requests_total{pod="nginx-1", le="1"} 1+3x10 + http_requests_total{pod="nginx-2", le="1"} 2+3x10 + http_requests_total{pod="nginx-1", le="2"} 1+2x10 + http_requests_total{pod="nginx-2", le="2"} 2+2x10 + http_requests_total{pod="nginx-2", le="5"} 3+2x10 + http_requests_total{pod="nginx-1", le="+Inf"} 1+1x10 + http_requests_total{pod="nginx-2", le="+Inf"} 4+1x10`, + query: `histogram_quantile(0.9, http_requests_total)`, + }, + { + name: "histogram quantile on malformed data", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+3x10 + http_requests_total{pod="nginx-2"} 2+3x10`, + query: `histogram_quantile(0.9, http_requests_total)`, + }, + { + name: "histogram quantile on partially malformed data", + load: `load 30s + http_requests_total{pod="nginx-1", le="1"} 1+3x10 + http_requests_total{pod="nginx-2", le="2"} 2+3x10 + http_requests_total{pod="nginx-3"} 3+3x10 + http_requests_total{pod="nginx-4"} 4+3x10`, + query: `histogram_quantile(0.9, http_requests_total)`, + }, + { + name: "histogram quantile on malformed, interleaved data", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+3x10 + http_requests_total{pod="nginx-2"} 2+3x10 + http_requests_total{pod="nginx-3", le="0.05"} 2+3x10 + http_requests_total{pod="nginx-4", le="0.1"} 2+3x10`, + query: `histogram_quantile(0.9, http_requests_total)`, + }, + { + name: "histogram quantile on malformed, interleaved data 2", + load: `load 30s + http_requests_total{pod="nginx-1", le="0.01"} 1+3x10 + http_requests_total{pod="nginx-2", le="0.02"} 2+3x10 + http_requests_total{pod="nginx-3"} 2+3x10 + http_requests_total{pod="nginx-4"} 2+3x10`, + query: `histogram_quantile(0.9, http_requests_total)`, + }, + { + name: "histogram quantile on malformed, interleaved data 3", + load: `load 30s + http_requests_total{pod="nginx-1", le="0.01"} 1+3x10 + http_requests_total{pod="nginx-2"} 2+3x10 + http_requests_total{pod="nginx-3"} 2+3x10 + http_requests_total{pod="nginx-4", le="0.03"} 2+3x10`, + query: `histogram_quantile(0.9, http_requests_total)`, + }, + { + name: "histogram quantile on malformed, interleaved data 4", + load: `load 30s + http_requests_total{pod="nginx-1", le="0.01"} 1+3x10 + http_requests_total{pod="nginx-2"} 2+3x10 + http_requests_total{pod="nginx-2", le="0.05"} 2+3x10 + http_requests_total{pod="nginx-2", le="0.2"} 2+3x10 + http_requests_total{pod="nginx-3"} 2+3x10 + http_requests_total{pod="nginx-4", le="0.03"} 2+3x10`, + query: `histogram_quantile(0.9, http_requests_total)`, + }, + { + name: "histogram quantile with sum", + load: `load 30s + http_requests_total{pod="nginx-1", le="1"} 1+3x10 + http_requests_total{pod="nginx-2", le="1"} 2+3x10 + http_requests_total{pod="nginx-1", le="2"} 1+2x10 + http_requests_total{pod="nginx-2", le="2"} 2+2x10 + http_requests_total{pod="nginx-2", le="5"} 3+2x10 + http_requests_total{pod="nginx-1", le="+Inf"} 1+1x10 + http_requests_total{pod="nginx-2", le="+Inf"} 4+1x10`, + query: `histogram_quantile(0.9, sum by (pod, le) (rate(http_requests_total[2m])))`, + }, + { + name: "histogram quantile with scalar operator", + load: `load 30s + quantile{pod="nginx-1", le="1"} 1+1x2 + http_requests_total{pod="nginx-1", le="1"} 1+3x10 + http_requests_total{pod="nginx-2", le="1"} 2+3x10 + http_requests_total{pod="nginx-1", le="2"} 1+2x10 + http_requests_total{pod="nginx-2", le="2"} 2+2x10 + http_requests_total{pod="nginx-2", le="5"} 3+2x10 + http_requests_total{pod="nginx-1", le="+Inf"} 1+1x10 + http_requests_total{pod="nginx-2", le="+Inf"} 4+1x10`, + query: `histogram_quantile(scalar(max(quantile)), http_requests_total)`, + }, + { + name: "topk", + load: `load 30s + http_requests_total{pod="nginx-1", series="1"} 1+1.1x40 + http_requests_total{pod="nginx-2", series="1"} 2+2.3x50 + http_requests_total{pod="nginx-4", series="2"} 5+2.4x50 + http_requests_total{pod="nginx-5", series="2"} 8.4+2.3x50 + http_requests_total{pod="nginx-6", series="2"} 2.3+2.3x50`, + query: `topk(2, http_requests_total)`, + start: time.Unix(0, 0), + end: time.Unix(3000, 0), + step: 2 * time.Second, + }, + { + name: "topk with float64 parameter", + load: `load 30s + http_requests_total{pod="nginx-1", series="1"} 1+1.1x40 + http_requests_total{pod="nginx-2", series="1"} 2+2.3x50 + http_requests_total{pod="nginx-4", series="2"} 5+2.4x50 + http_requests_total{pod="nginx-5", series="2"} 8.4+2.3x50 + http_requests_total{pod="nginx-6", series="2"} 2.3+2.3x50`, + query: `topk(3.5, http_requests_total)`, + start: time.Unix(0, 0), + end: time.Unix(3000, 0), + step: 2 * time.Second, + }, + { + name: "topk with float64 parameter that gets truncated to 0", + load: `load 30s + http_requests_total{pod="nginx-1", series="1"} 1+1.1x40 + http_requests_total{pod="nginx-2", series="1"} 2+2.3x50 + http_requests_total{pod="nginx-4", series="2"} 5+2.4x50 + http_requests_total{pod="nginx-5", series="2"} 8.4+2.3x50 + http_requests_total{pod="nginx-6", series="2"} 2.3+2.3x50`, + query: `topk(0.5, http_requests_total)`, + start: time.Unix(0, 0), + end: time.Unix(3000, 0), + step: 2 * time.Second, + }, + { + name: "topk with float64 parameter that does not fit int64", + load: `load 30s + http_requests_total{pod="nginx-1", series="1"} 1+1.1x40 + http_requests_total{pod="nginx-2", series="1"} 2+2.3x50 + http_requests_total{pod="nginx-4", series="2"} 5+2.4x50 + http_requests_total{pod="nginx-5", series="2"} 8.4+2.3x50 + http_requests_total{pod="nginx-6", series="2"} 2.3+2.3x50`, + query: ` +topk( + 1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000, + http_requests_total +)`, + start: time.Unix(0, 0), + end: time.Unix(3000, 0), + step: 2 * time.Second, + }, + { + name: "topk with NaN", + load: `load 30s + http_requests_total{pod="nginx-1", series="1"} 1+1.1x40 + http_requests_total{pod="nginx-2", series="1"} 2+2.3x50 + http_requests_total{pod="nginx-4", series="2"} 5+2.4x50 + http_requests_total{pod="nginx-5", series="2"} 8.4+2.3x50 + http_requests_total{pod="nginx-6", series="2"} 2.3+2.3x50`, + query: `topk(NaN, http_requests_total)`, + start: time.Unix(0, 0), + end: time.Unix(3000, 0), + step: 2 * time.Second, + }, + { + name: "topk with NaN and no matching series", + query: `topk(NaN, not_there)`, + start: time.Unix(0, 0), + end: time.Unix(3000, 0), + step: 2 * time.Second, + }, + { + name: "topk with NaN comparison", + load: `load 30s + http_requests_total{pod="nginx-1", route="/"} NaN + http_requests_total{pod="nginx-2", route="/"} NaN`, + query: `topk by (route) (1, http_requests_total)`, + }, + { + name: "nested topk error that should not be skipped", + load: `load 30s + X 1+1x50`, + query: `topk(0, topk(NaN, X))`, + start: time.Unix(0, 0), + end: time.Unix(3000, 0), + step: 2 * time.Second, + }, + { + name: "topk wrapped by another aggregate", + load: `load 30s + http_requests_total{pod="nginx-1", series="1"} 1+1.1x40 + http_requests_total{pod="nginx-2", series="1"} 2+2.3x50 + http_requests_total{pod="nginx-4", series="2"} 5+2.4x50 + http_requests_total{pod="nginx-5", series="2"} 8.4+2.3x50 + http_requests_total{pod="nginx-6", series="2"} 2.3+2.3x50`, + query: `max(topk by (series) (2, http_requests_total))`, + end: time.Unix(3000, 0), + }, + { + name: "topk on empty result", + load: `load 30s + metric_a 1+1x2`, + query: `topk(2, histogram_quantile(0.1, metric_b))`, + }, + { + name: "topk by", + load: `load 30s + http_requests_total{pod="nginx-1", series="1"} 1+1.1x40 + http_requests_total{pod="nginx-2", series="1"} 2+2.3x50 + http_requests_total{pod="nginx-4", series="2"} 5+2.4x50 + http_requests_total{pod="nginx-5", series="2"} 8.4+2.3x50 + http_requests_total{pod="nginx-6", series="2"} 2.3+2.3x50`, + query: `topk by (series) (2, http_requests_total)`, + start: time.Unix(0, 0), + end: time.Unix(3000, 0), + step: 2 * time.Second, + }, + { + name: "topk with simple expression", + load: `load 30s + http_requests_total{pod="nginx-1", series="1"} 1+1.1x40 + http_requests_total{pod="nginx-2", series="1"} 2+2.3x50 + http_requests_total{pod="nginx-4", series="2"} 5+2.4x50 + http_requests_total{pod="nginx-5", series="2"} 8.4+2.3x50 + http_requests_total{pod="nginx-6", series="2"} 2.3+2.3x50`, + query: `topk by (series) (2 - 1, http_requests_total)`, + start: time.Unix(0, 0), + end: time.Unix(3000, 0), + step: 2 * time.Second, + }, + { + name: "topk with expression", + load: `load 30s + http_requests_total{pod="nginx-1", series="1"} 1+1.1x40 + http_requests_total{pod="nginx-2", series="1"} 2+2.3x50 + http_requests_total{pod="nginx-4", series="2"} 5+2.4x50 + http_requests_total{pod="nginx-5", series="2"} 8.4+2.3x50 + http_requests_total{pod="nginx-6", series="2"} 2.3+2.3x50`, + query: `topk by (series) (scalar(min(http_requests_total)), http_requests_total)`, + start: time.Unix(0, 0), + end: time.Unix(500, 0), + step: 2 * time.Second, + }, + { + name: "topk with expression as argument not returning any value", + load: `load 30s + http_requests_total{pod="nginx-1", series="1"} 1+1.1x40 + http_requests_total{pod="nginx-2", series="1"} 2+2.3x50 + http_requests_total{pod="nginx-4", series="2"} 5+2.4x50 + http_requests_total{pod="nginx-5", series="2"} 8.4+2.3x50 + http_requests_total{pod="nginx-6", series="2"} 2.3+2.3x50`, + query: `topk by (series) (scalar(min(non_existent_metric)), http_requests_total)`, + start: time.Unix(0, 0), + end: time.Unix(500, 0), + step: 2 * time.Second, + }, + { + name: "bottomK", + load: `load 30s + http_requests_total{pod="nginx-1", series="1"} 1+1.1x40 + http_requests_total{pod="nginx-2", series="1"} 2+2.3x50 + http_requests_total{pod="nginx-4", series="2"} 5+2.4x50 + http_requests_total{pod="nginx-5", series="2"} 8.4+2.3x50 + http_requests_total{pod="nginx-6", series="2"} 2.3+2.3x50`, + query: `bottomk(2, http_requests_total)`, + start: time.Unix(0, 0), + end: time.Unix(3000, 0), + step: 2 * time.Second, + }, + { + name: "bottomK by", + load: `load 30s + http_requests_total{pod="nginx-1", series="1"} 1+1.1x40 + http_requests_total{pod="nginx-2", series="1"} 2+2.3x50 + http_requests_total{pod="nginx-4", series="2"} 5+2.4x50 + http_requests_total{pod="nginx-5", series="2"} 8.4+2.3x50 + http_requests_total{pod="nginx-6", series="2"} 2.3+2.3x50`, + query: `bottomk by (series) (2, http_requests_total)`, + start: time.Unix(0, 0), + end: time.Unix(3000, 0), + step: 2 * time.Second, + }, + { + name: "limitK", + load: `load 30s + http_requests_total{pod="nginx-1", series="1"} 1+1.1x50 + http_requests_total{pod="nginx-2", series="1"} 2+2.3x50 + http_requests_total{pod="nginx-4", series="2"} 5+2.4x50 + http_requests_total{pod="nginx-5", series="2"} 8.4+2.3x50 + http_requests_total{pod="nginx-6", series="2"} 2.3+2.3x50`, + query: `limitk(2, http_requests_total)`, + start: time.Unix(0, 0), + end: time.Unix(3000, 0), + step: 2 * time.Second, + }, + { + name: "limitK with negative value as param", + load: `load 30s + http_requests_total{pod="nginx-1", series="1"} 1+1.1x40 + http_requests_total{pod="nginx-2", series="1"} 2+2.3x50 + http_requests_total{pod="nginx-4", series="2"} 5+2.4x50`, + query: `limitk(-2, http_requests_total)`, + start: time.Unix(0, 0), + end: time.Unix(3000, 0), + step: 2 * time.Second, + }, + { + name: "limitK by (series)", + load: `load 30s + http_requests_total{pod="nginx-1", series="1"} 1+1.1x50 + http_requests_total{pod="nginx-2", series="1"} 2+2.3x50 + http_requests_total{pod="nginx-4", series="2"} 5+2.4x50 + http_requests_total{pod="nginx-5", series="2"} 8.4+2.3x50 + http_requests_total{pod="nginx-6", series="2"} 2.3+2.3x50`, + query: `limitk(2, http_requests_total) by (pod)`, + start: time.Unix(0, 0), + end: time.Unix(3000, 0), + step: 2 * time.Second, + }, + { + name: "limitK with returning all samples", + load: `load 30s + http_requests_total{pod="nginx-1", series="1"} 1+1.1x50 + http_requests_total{pod="nginx-2", series="1"} 2+2.3x50 + http_requests_total{pod="nginx-4", series="2"} 5+2.4x50 + http_requests_total{pod="nginx-5", series="2"} 8.4+2.3x50 + http_requests_total{pod="nginx-6", series="2"} 2.3+2.3x50`, + query: `count(limitk(100, http_requests_total) by (pod))`, + start: time.Unix(0, 0), + end: time.Unix(3000, 0), + step: 2 * time.Second, + }, + { + name: "limitK but a sample might not present at last few timestamps", + load: `load 30s + http_requests_total{pod="nginx-1", series="1"} 1+1.1x50 + http_requests_total{pod="nginx-2", series="1"} 2+2.3x40 + http_requests_total{pod="nginx-4", series="2"} 5+2.4x50 + http_requests_total{pod="nginx-5", series="2"} 8.4+2.3x50 + http_requests_total{pod="nginx-6", series="2"} 2.3+2.3x50`, + query: `limitk(2, http_requests_total)`, + start: time.Unix(0, 0), + end: time.Unix(3000, 0), + step: 2 * time.Second, + }, + { + name: "limit_ratio", + load: `load 30s + http_requests_total{pod="nginx-1", series="1"} 1+1.1x50 + http_requests_total{pod="nginx-2", series="1"} 2+2.3x50 + http_requests_total{pod="nginx-4", series="2"} 5+2.4x50 + http_requests_total{pod="nginx-5", series="2"} 8.4+2.3x50 + http_requests_total{pod="nginx-6", series="2"} 2.3+2.3x50`, + query: `limit_ratio(0.65, http_requests_total)`, + start: time.Unix(0, 0), + end: time.Unix(3000, 0), + step: 2 * time.Second, + }, + { + name: "limit_ratio by (series)", + load: `load 30s + http_requests_total{pod="nginx-1", series="1"} 1+2.1x50 + http_requests_total{pod="nginx-5", series="1"} 2+1.3x40 + http_requests_total{pod="nginx-3", series="2"} 5+3.4x50 + http_requests_total{pod="nginx-7", series="2"} 8.4+2.3x50 + http_requests_total{pod="nginx-4", series="2"} 2.5+2.3x50`, + query: `limit_ratio(0.3, http_requests_total) by (series)`, + start: time.Unix(0, 0), + end: time.Unix(3000, 0), + step: 2 * time.Second, + }, + { + name: "limit_ratio with ratio exceeding ratio limit", // in limit_ratio exceeded ratio limit should be capped to -1 or 1 (whichever is relatively closer) + load: `load 30s + http_requests_total{pod="nginx-1", series="1"} 1+2.1x50 + http_requests_total{pod="nginx-6", series="1"} 2+1.3x40 + http_requests_total{pod="nginx-4", series="2"} 5+3.4x30 + http_requests_total{pod="nginx-9", series="2"} 8.4+2.3x50 + http_requests_total{pod="nginx-2", series="2"} 2.5+2.3x50`, + query: `limit_ratio(23456, http_requests_total) and limit_ratio(-4567, http_requests_total)`, + start: time.Unix(0, 0), + end: time.Unix(3000, 0), + step: 2 * time.Second, + }, + { + name: "limit_ratio with NaN value as ratio param", + load: `load 30s + http_requests_total{pod="nginx-1", series="1"} 1+2.1x50 + http_requests_total{pod="nginx-5", series="1"} 2+1.3x40 + http_requests_total{pod="nginx-3", series="2"} 5+3.4x50 + http_requests_total{pod="nginx-7", series="2"} 8.4+2.3x50 + http_requests_total{pod="nginx-4", series="2"} 2.5+2.3x50`, + query: `limit_ratio(NaN, http_requests_total)`, + start: time.Unix(0, 0), + end: time.Unix(3000, 0), + step: 2 * time.Second, + }, + { + name: "combined kaggregates", + load: `load 30s + http_requests_total{pod="nginx-1", series="1"} 1+2.1x50 + http_requests_total{pod="nginx-5", series="1"} 2+1.3x50 + http_requests_total{pod="nginx-3", series="2"} 5+3.4x50 + http_requests_total{pod="nginx-7", series="2"} 8.4+2.3x50 + http_requests_total{pod="nginx-4", series="4"} 2.5+2.3x50`, + query: `limitk(5, topk(3, limit_ratio(0.8, http_requests_total)) or bottomk(3, limit_ratio(-0.2, http_requests_total)))`, + start: time.Unix(0, 0), + end: time.Unix(3000, 0), + step: 2 * time.Second, + }, + { + name: "sgn", + load: `load 30s + http_requests_total{pod="nginx-1", series="1"} 1+1.1x40 + http_requests_total{pod="nginx-2", series="1"} -10+1x50`, + query: `sgn(http_requests_total)`, + start: time.Unix(0, 0), + end: time.Unix(3000, 0), + step: 2 * time.Second, + }, + { + name: "sort_desc", + load: `load 30s + http_requests_total{pod="nginx-1", series="1"} 1+1.1x40 + http_requests_total{pod="nginx-2", series="1"} 2+2.3x50 + http_requests_total{pod="nginx-4", series="2"} 5+2.4x50 + http_requests_total{pod="nginx-5", series="2"} 8.4+2.3x50 + http_requests_total{pod="nginx-6", series="2"} 2.3+2.3x50`, + query: `sort_desc(http_requests_total)`, + }, + { + name: "count by __name__ label", + load: `load 30s + foo 1+1x5 + bar 2+2x5`, + query: `count by (__name__) ({__name__=~".+"})`, + }, + { + name: "scalar with bool", + load: `load 30s + http_requests_total{pod="nginx-1", series="1"} 1+1.1x40 + http_requests_total{pod="nginx-2", series="2"} 2+2.3x50 + http_requests_total{pod="nginx-3", series="3"} 6+0.8x60 + http_requests_total{pod="nginx-4", series="3"} 5+2.4x50 + http_requests_total{pod="nginx-5", series="1"} 8.4+2.3x50 + http_requests_total{pod="nginx-6", series="2"} 2.3+2.3x50`, + query: `scalar(avg_over_time({__name__="http_requests_total"}[3m])) > bool 0.9464749352949011`, + }, + { + name: "repro https://github.com/thanos-io/promql-engine/issues/239", + load: `load 30s + storage_used{storage_index="1010"} 65x20 + storage_used{storage_index="1011"} 125x20 + storage_used{storage_index="1012"} 0x20 + storage_used{storage_index="20"} 2290380x20 + storage_used{storage_index="30"} 397304x20 + storage_used{storage_index="40"} 5590832x20 + storage_used{storage_index="41"} 65559832x20 + storage_used{storage_index="42"} 3516400x20 + storage_info{storage_info="Config", storage_index="40"} 1x20 + storage_info{storage_info="Log", storage_index="41"} 1x20 + storage_info{storage_info="Mem", storage_index="20"} 1x20 + storage_info{storage_info="Root", storage_index="42"} 1x20 + storage_info{storage_info="Swap", storage_index="30"} 1x20`, + query: ` +avg by (storage_info) ( + storage_used + * on (instance, storage_index) group_left (storage_info) + (sum by (instance, storage_index, storage_info) (storage_info)) +)`, + }, + { + name: "absent with partial data in range", + load: `load 30s + existent{job="myjob"} 1 1 1`, + query: `absent(existent{job="myjob"})`, + }, + { + name: "absent with no data in range", + load: `load 30s`, + query: `absent(nonexistent{job="myjob"})`, + }, + { + name: "absent_over_time with no data in range", + query: `absent_over_time(non_existent[10m])`, + }, + { + name: "absent_over_time with data in range", + load: `load 30s + X{a="b"} 1x10`, + query: `absent_over_time(X{a="b"}[10m])`, + }, + { + name: "absent_over_time - present but out of range", + load: `load 30s + X{a="b"} 1x10`, + query: `absent_over_time(X{a="b"}[1m])`, + start: time.Unix(600, 0), + }, + { + name: "absent_over_time - absent because of label", + load: `load 30s + X{a="b"} 1x10`, + query: `absent_over_time(X{a!="b"}[1m])`, + }, + { + name: "subquery in binary expression", + load: `load 60s + http_requests_total{pod="nginx-1", series="1"} 1+1x40`, + query: `http_requests_total * (sum_over_time(http_requests_total[5m:1m]) > 0)`, + }, + { + name: "sum_over_time subquery with outer step larger than inner step", + load: `load 60s + http_requests_total{pod="nginx-1", series="1"} 1+1x40`, + query: `sum_over_time(sum_over_time(http_requests_total[2m])[5m:1m])`, + }, + { + name: "sum_over_time subquery with outer step equal to inner step", + load: `load 60s + http_requests_total{pod="nginx-1", series="1"} 1+1x40`, + query: `sum_over_time(sum_over_time(http_requests_total[2m])[5m:30s])`, + }, + { + name: "sum_over_time subquery with outer step smaller than inner step", + load: `load 60s + http_requests_total{pod="nginx-1", series="1"} 1+1x40`, + query: `sum_over_time(sum_over_time(http_requests_total[2m])[5m:15s])`, + }, + { + name: "sum_over_time subquery with aggregation", + load: `load 10s + http_requests_total{pod="nginx-1", series="1"} 1+1x40 + http_requests_total{pod="nginx-2", series="2"} 2+2x50`, + query: `sum_over_time(sum by (pod) (http_requests_total)[5m:1m])`, + }, + { + name: "rate subquery with outer @ modifier", + load: `load 30s + http_requests_total{pod="nginx-1", series="1"} 1+1x40 + http_requests_total{pod="nginx-2", series="2"} 2+2x50`, + query: `rate(http_requests_total[20s:10s] @ 100.000)`, + }, + { + name: "rate subquery with offset", + load: `load 10s + http_requests_total{pod="nginx-1", series="1"} 1+2x40`, + query: `rate(http_requests_total[20s:10s] offset 20s)`, + }, + { + name: "fuzz absent_over_time empty matcher", + load: `load 30s + http_requests_total{pod="nginx-1", route="/"} 41.00+0.20x40 + http_requests_total{pod="nginx-2", route="/"} 1+2.41x40`, + query: `absent_over_time({__name__="http_requests_total",route=""}[4m])`, + start: time.UnixMilli(170000), + end: time.UnixMilli(170000), + }, + { + name: "predict_linear", + load: `load 1m + native_histogram {{sum:100 count:100}} {{sum:103 count:103}} {{sum:106 count:106}} {{sum:109 count:109}} {{sum:112 count:112}} {{sum:3 count:3 counter_reset_hint:reset}} {{sum:6 count:6}}+{{sum:3 count:3}}x5`, + query: `increase(native_histogram[10m:3m])`, + start: time.UnixMilli(10 * 60 * 1000), + }, + { + name: "utf-8 label", + load: `load 10s + http_requests_total{"label:name"="labelvalue"} 1+2x40`, + query: `rate(http_requests_total{"label:name"="labelvalue"}[20s:10s] offset 20s)`, + }, + { + name: "native histogram sum compact", + load: `load 2m + http_request_duration_seconds{pod="nginx-1"} {{schema:0 count:3 sum:14.00 buckets:[1 2]}}+{{schema:0 count:4 buckets:[1 2 1]}}x20 + http_request_duration_seconds{pod="nginx-2"} {{schema:0 count:2 sum:14.00 buckets:[2]}}+{{schema:0 count:6 buckets:[2 2 2]}}x20`, + query: `--sum by (pod) ({__name__="http_request_duration_seconds"})`, + start: time.UnixMilli(0), + end: time.UnixMilli(0), + step: 0, + }, + { + name: "native histogram scalar compact", + load: `load 2m + http_request_duration_seconds{pod="nginx-1"} {{schema:0 count:3 sum:14.00 buckets:[1 2]}}+{{schema:0 count:20 buckets:[1 2 17]}}x20 + http_request_duration_seconds{pod="nginx-2"} {{schema:0 count:2 sum:14.00 buckets:[2]}}+{{schema:0 count:38 buckets:[2 2 34]}}x20`, + query: `({__name__="http_request_duration_seconds"} offset -2s * pi())`, + start: time.UnixMilli(0), + end: time.UnixMilli(300000), + step: 15 * time.Second, + }, + { + name: "native histogram nested binary#1", + load: `load 2m + http_request_duration_seconds{pod="nginx-1"} {{schema:0 count:3 sum:14.00 buckets:[1 2]}}+{{schema:0 count:4 buckets:[1 2 1]}}x20 + http_request_duration_seconds{pod="nginx-2"} {{schema:0 count:2 sum:14.00 buckets:[2]}}+{{schema:0 count:6 buckets:[2 2 2]}}x20 + http_request_duration_seconds{pod="nginx-3"} {{schema:0 count:2 sum:14.00 buckets:[2]}}+{{schema:0 count:6 buckets:[2 2 2]}}x20`, + query: ` + avg(http_request_duration_seconds) +or + (http_request_duration_seconds + http_request_duration_seconds{pod!="nginx-1"})`, + start: time.UnixMilli(0), + end: time.UnixMilli(60), + step: 15 * time.Second, + }, + { + name: "native histogram nested binary#2", + load: `load 2m + http_request_duration_seconds{pod="nginx-1"} {{schema:0 count:3 sum:14.00 buckets:[1 2]}}+{{schema:0 count:4 buckets:[1 2 1]}}x20 + http_request_duration_seconds{pod="nginx-2"} {{schema:0 count:2 sum:14.00 buckets:[2]}}+{{schema:0 count:6 buckets:[2 2 2]}}x20 + http_request_duration_seconds{pod="nginx-3"} {{schema:0 count:2 sum:14.00 buckets:[2]}}+{{schema:0 count:6 buckets:[2 2 2]}}x20`, + query: ` + count(http_request_duration_seconds offset -3m3s) +* + -( + group by (pod) (http_request_duration_seconds) + or + avg by (__name__) (http_request_duration_seconds{pod=~"ngi.*"} @ end()) + )`, + start: time.UnixMilli(0), + end: time.UnixMilli(124000), + step: 15 * time.Second, + }, + { + name: "fuzz native histogram approx float comparison", + load: `load 2m + http_request_duration_seconds{pod="nginx-1"} {{schema:0 count:30 sum:14.00 buckets:[27 2 1]}}+{{schema:0 count:30 buckets:[27 2 1]}}x20 + http_request_duration_seconds{pod="nginx-2"} {{schema:-2 count:58 sum:4368.00 buckets:[54 2 2]}}+{{schema:-2 count:58 buckets:[54 2 2]}}x30`, + query: ` +-( + -{__name__="http_request_duration_seconds"} + / + histogram_stdvar({__name__="http_request_duration_seconds"}) +)`, + start: time.UnixMilli(83000), + end: time.UnixMilli(160000), + step: time.Minute + 16*time.Second, + }, + { + // The matching low-card series for the pod changes its included (ns) + // label across the query window. Each output label set must become + // its own series, matching Prometheus, rather than collapsing onto a + // single representative series. + name: "group_left with included label changing across the window", + load: `load 30s + metric_a{pod="x"} 1 1 1 1 1 + metric_b{pod="x", ns="a"} 1 1 _ _ _ + metric_b{pod="x", ns="b"} _ _ 1 1 1`, + query: `metric_a * on (pod) group_left (ns) metric_b`, + start: time.Unix(0, 0), + end: time.Unix(120, 0), + step: 30 * time.Second, + }, + } + + disableOptimizerOpts := []bool{true, false} + lookbackDeltas := []time.Duration{0, 30 * time.Second, time.Minute, 5 * time.Minute, 10 * time.Minute} + for _, lookbackDelta := range lookbackDeltas { + opts.LookbackDelta = lookbackDelta + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + storage := promqltest.LoadedStorage(t, tc.load) + defer storage.Close() + + if tc.start.Equal(time.Time{}) { + tc.start = start + } + if tc.end.Equal(time.Time{}) { + tc.end = end + } + if tc.step == 0 { + tc.step = step + } + for _, disableOptimizers := range disableOptimizerOpts { + t.Run(fmt.Sprintf("disableOptimizers=%v", disableOptimizers), func(t *testing.T) { + optimizers := logicalplan.AllOptimizers + if disableOptimizers { + optimizers = logicalplan.NoOptimizers + } + newEngine := engine.New(engine.Opts{ + EngineOpts: opts, + LogicalOptimizers: optimizers, + // Set to 1 to make sure batching is tested. + SelectorBatchSize: 1, + }) + ctx := context.Background() + q1, err := newEngine.NewRangeQuery(ctx, storage, nil, tc.query, tc.start, tc.end, tc.step) + testutil.Ok(t, err) + defer q1.Close() + newResult := q1.Exec(ctx) + + oldEngine := promql.NewEngine(opts) + q2, err := oldEngine.NewRangeQuery(ctx, storage, nil, tc.query, tc.start, tc.end, tc.step) + testutil.Ok(t, err) + defer q2.Close() + oldResult := q2.Exec(ctx) + + testutil.WithGoCmp(comparer).Equals(t, oldResult, newResult, queryExplanation(q1)) + }) + } + }) + } + } +} + +// mergeWithSampleDedup merges samples from series with the same labels, +// removing samples with identical timestamps. +func mergeWithSampleDedup(series []*mockSeries) []storage.Series { + index := make(map[uint64]*mockSeries) + for _, s := range series { + hash := s.Labels().Hash() + existing, ok := index[hash] + if !ok { + // Make a copy to avoid modifying the original series + // when merging samples. + index[hash] = &mockSeries{ + labels: s.labels, + timestamps: s.timestamps, + values: s.values, + } + continue + } + existing.timestamps = append(existing.timestamps, s.timestamps...) + existing.values = append(existing.values, s.values...) + } + + for _, s := range index { + sort.Sort(byTimestamps(*s)) + // Remove exact timestamp duplicates. + i := 1 + for i < len(s.timestamps) { + if s.timestamps[i] == s.timestamps[i-1] { + s.timestamps = slices.Delete(s.timestamps, i, i+1) + s.values = slices.Delete(s.values, i, i+1) + } else { + i++ + } + } + } + + sset := make([]storage.Series, 0, len(index)) + for _, s := range index { + sset = append(sset, s) + } + return sset +} + +func TestWarnings(t *testing.T) { + querier := &storage.MockQueryable{ + MockQuerier: &storage.MockQuerier{ + SelectMockFunction: func(sortSeries bool, hints *storage.SelectHints, matchers ...*labels.Matcher) storage.SeriesSet { + return newWarningsSeriesSet(annotations.New().Add(errors.New("test warning"))) + }, + }, + } + + var ( + start = time.UnixMilli(0) + end = time.UnixMilli(600) + step = 30 * time.Second + ) + + cases := []struct { + name string + query string + expectedWarns annotations.Annotations + }{ + { + name: "single select call", + query: `http_requests_total`, + expectedWarns: annotations.New().Add( + errors.New("test warning"), + ), + }, + { + name: "multiple select calls", + query: `sum(http_requests_total) / sum(http_responses_total)`, + expectedWarns: annotations.New().Add( + errors.New("test warning"), + ), + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + newEngine := engine.New(engine.Opts{EngineOpts: promql.EngineOpts{Timeout: 1 * time.Hour}}) + q1, err := newEngine.NewRangeQuery(context.Background(), querier, nil, tc.query, start, end, step) + testutil.Ok(t, err) + + res := q1.Exec(context.Background()) + testutil.Ok(t, res.Err) + testutil.WithGoCmp(cmp.Comparer(func(err1, err2 error) bool { + return err1.Error() == err2.Error() + })).Equals(t, tc.expectedWarns, res.Warnings) + }) + } +} + +type scannersWithWarns struct { + warn error + promScanners *prometheus.Scanners +} + +func newScannersWithWarns(warn error, qOpts *query.Options, lplan logicalplan.Plan) (*scannersWithWarns, error) { + scanners, err := prometheus.NewPrometheusScanners(&storage.MockQueryable{ + MockQuerier: storage.NoopQuerier(), + }, qOpts, lplan) + if err != nil { + return nil, err + } + return &scannersWithWarns{ + warn: warn, + promScanners: scanners, + }, nil +} + +func (s *scannersWithWarns) Close() error { return nil } + +func (s scannersWithWarns) NewVectorSelector(ctx context.Context, opts *query.Options, hints storage.SelectHints, selector logicalplan.VectorSelector) (model.VectorOperator, error) { + warnings.AddToContext(s.warn, ctx) + return s.promScanners.NewVectorSelector(ctx, opts, hints, selector) +} + +func (s scannersWithWarns) NewMatrixSelector(ctx context.Context, opts *query.Options, hints storage.SelectHints, selector logicalplan.MatrixSelector, call logicalplan.FunctionCall) (model.VectorOperator, error) { + warnings.AddToContext(s.warn, ctx) + return s.promScanners.NewMatrixSelector(ctx, opts, hints, selector, call) +} + +func TestWarningsPlanCreation(t *testing.T) { + var ( + opts = engine.Opts{EngineOpts: promql.EngineOpts{Timeout: 1 * time.Hour}} + expectedWarn = errors.New("test warning") + ) + + scnrs, err := newScannersWithWarns(expectedWarn, &query.Options{}, nil) + testutil.Ok(t, err) + newEngine := engine.NewWithScanners(opts, scnrs) + q1, err := newEngine.NewRangeQuery(context.Background(), nil, nil, "http_requests_total", time.UnixMilli(0), time.UnixMilli(600), 30*time.Second) + testutil.Ok(t, err) + + res := q1.Exec(context.Background()) + testutil.Ok(t, res.Err) + testutil.WithGoCmp(cmp.Comparer(func(err1, err2 error) bool { + return err1.Error() == err2.Error() + })).Equals(t, annotations.New().Add(expectedWarn), res.Warnings) + +} + +func TestEdgeCases(t *testing.T) { + t.Parallel() + testCases := []struct { + name string + series []storage.Series + query string + start time.Time + end time.Time + }{ + { + name: "binop edge case", + series: []storage.Series{ + newMockSeries( + []string{labels.MetricName, "foo"}, + []int64{0, 30, 60, 1200, 1500, 1800}, + []float64{1, 2, 3, 4, 5, 6}, + ), + newMockSeries( + []string{labels.MetricName, "bar", "id", "1"}, + []int64{0, 30}, + []float64{1, 2}, + ), + newMockSeries( + []string{labels.MetricName, "bar", "id", "2"}, + []int64{1200, 1500}, + []float64{3, 4}, + ), + }, + query: `foo * on () group_left () bar`, + start: time.Unix(0, 0), + end: time.Unix(30000, 0), + }, + { + name: "absent with gaps in series", + series: []storage.Series{ + newMockSeries( + []string{labels.MetricName, "foo"}, + []int64{30, 300, 3000, 6000, 12000, 18000}, + []float64{1, 2, 3, 4, 5, 6}, + ), + }, + query: `absent(foo)`, + start: time.Unix(0, 0), + end: time.Unix(30000, 0), + }, + } + + opts := promql.EngineOpts{ + Timeout: 1 * time.Hour, + MaxSamples: 1e10, + EnableNegativeOffset: true, + EnableAtModifier: true, + } + step := time.Second * 30 + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + ctx := context.Background() + oldEngine := promql.NewEngine(opts) + q1, err := oldEngine.NewRangeQuery(ctx, storageWithSeries(tc.series...), nil, tc.query, tc.start, tc.end, step) + testutil.Ok(t, err) + + newEngine := engine.New(engine.Opts{EngineOpts: opts}) + q2, err := newEngine.NewRangeQuery(ctx, storageWithSeries(tc.series...), nil, tc.query, tc.start, tc.end, step) + testutil.Ok(t, err) + + oldResult := q1.Exec(ctx) + newResult := q2.Exec(ctx) + + testutil.WithGoCmp(comparer).Equals(t, oldResult, newResult, queryExplanation(q1)) + }) + } +} + +func TestXFunctionsRangeQuery(t *testing.T) { + // Negative offset and at modifier are enabled by default + // since Prometheus v2.33.0, so we also enable them. + opts := promql.EngineOpts{ + Timeout: 1 * time.Hour, + MaxSamples: 1e10, + EnableNegativeOffset: true, + EnableAtModifier: true, + } + + cases := []struct { + name string + load string + query string + startTime time.Time + endTime time.Time + step time.Duration + + expected promql.Matrix + }{ + { + name: "gaps between steps", + load: `load 10s + http_requests 1 5 10 20 _ 40`, + query: "xincrease(http_requests[10s])", + + startTime: time.Unix(0, 0), + endTime: time.Unix(60, 0), + step: 20 * time.Second, + + expected: promql.Matrix{ + promql.Series{ + Metric: labels.New(), + Floats: []promql.FPoint{ + {T: 00_000, F: 1}, + {T: 20_000, F: 9}, // TODO: this seems odd, feels like it should be 5 + {T: 40_000, F: 0}, + {T: 60_000, F: 0}, + }, + }, + }, + }, + { + name: "back to back steps", + load: `load 10s + http_requests 1 5 10 20 _ 40`, + query: "xincrease(http_requests[10s])", + + startTime: time.Unix(0, 0), + endTime: time.Unix(60, 0), + step: 10 * time.Second, + + expected: promql.Matrix{ + promql.Series{ + Metric: labels.New(), + Floats: []promql.FPoint{ + {T: 00_000, F: 1}, + {T: 10_000, F: 4}, + {T: 20_000, F: 5}, + {T: 30_000, F: 10}, + {T: 40_000, F: 0}, + {T: 50_000, F: 20}, + {T: 60_000, F: 0}, + }, + }, + }, + }, + { + name: "overlapping steps", + load: `load 10s + http_requests 1 5 10 20 _ 40`, + query: "xincrease(http_requests[20s])", + + startTime: time.Unix(0, 0), + endTime: time.Unix(60, 0), + step: 10 * time.Second, + + expected: promql.Matrix{ + promql.Series{ + Metric: labels.New(), + Floats: []promql.FPoint{ + {T: 00_000, F: 1}, + {T: 10_000, F: 4}, + {T: 20_000, F: 9}, + {T: 30_000, F: 15}, + {T: 40_000, F: 10}, + {T: 50_000, F: 20}, + {T: 60_000, F: 20}, + }, + }, + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + storage := promqltest.LoadedStorage(t, tc.load) + defer storage.Close() + + ctx := context.Background() + newEngine := engine.New(engine.Opts{ + EngineOpts: opts, + LogicalOptimizers: logicalplan.AllOptimizers, + EnableXFunctions: true, + }) + query, err := newEngine.NewRangeQuery(ctx, storage, nil, tc.query, tc.startTime, tc.endTime, tc.step) + testutil.Ok(t, err) + defer query.Close() + + engineResult := query.Exec(ctx) + testutil.Ok(t, engineResult.Err) + + gotMatrix, err := engineResult.Matrix() + require.NoError(t, err) + + for i := range tc.expected { + testutil.WithGoCmp(comparer).Equals(t, tc.expected[i].Floats, gotMatrix[i].Floats, queryExplanation(query)) + } + }) + } +} + +func TestXFunctionsWithNativeHistograms(t *testing.T) { + defaultQueryTime := time.Unix(50, 0) + + expr := "sum(xincrease(native_histogram_series[50s]))" + + // Negative offset and at modifier are enabled by default + // since Prometheus v2.33.0, so we also enable them. + opts := promql.EngineOpts{ + Timeout: 1 * time.Hour, + MaxSamples: 1e10, + EnableNegativeOffset: true, + EnableAtModifier: true, + } + + lStorage := teststorage.New(t) + defer lStorage.Close() + + app := lStorage.Appender(context.TODO()) + testutil.Ok(t, generateFloatHistogramSeries(app, 3000, false)) + testutil.Ok(t, app.Commit()) + + optimizers := logicalplan.AllOptimizers + + ctx := context.Background() + newEngine := engine.New(engine.Opts{ + EngineOpts: opts, + LogicalOptimizers: optimizers, + EnableXFunctions: true, + }) + query, err := newEngine.NewInstantQuery(ctx, lStorage, nil, expr, defaultQueryTime) + testutil.Ok(t, err) + defer query.Close() + + engineResult := query.Exec(ctx) + require.Error(t, engineResult.Err) +} + +func TestXFunctions(t *testing.T) { + defaultQueryTime := time.Unix(50, 0) + // Negative offset and at modifier are enabled by default + // since Prometheus v2.33.0, so we also enable them. + opts := promql.EngineOpts{ + Timeout: 1 * time.Hour, + MaxSamples: 1e10, + EnableNegativeOffset: true, + EnableAtModifier: true, + } + + defaultLoad := `load 5s + http_requests{path="/foo"} 0+10x10 + http_requests{path="/bar"} 0+10x5 0+10x4` + + xDeltaLoad := `load 5m + http_requests{path="/foo"} 0 50 300 150 200 + http_requests{path="/bar"} 200 150 300 50 0` + + cases := []struct { + name string + load string + query string + queryTime time.Time + expected []promql.Sample + rangeQuery bool + startTime time.Time + endTime time.Time + }{ + // Tests for xIncrease + { + name: "eval instant at 50s xincrease, with 50s lookback", + load: defaultLoad, + query: "xincrease(http_requests[50s])", + expected: []promql.Sample{ + createSample(defaultQueryTime.UnixMilli(), 100, labels.FromStrings("path", "/foo")), + createSample(defaultQueryTime.UnixMilli(), 90, labels.FromStrings("path", "/bar")), + }, + }, + { + name: "eval instant at 50s xincrease, with 5s lookback", + load: defaultLoad, + query: "xincrease(http_requests[5s])", + expected: []promql.Sample{ + createSample(defaultQueryTime.UnixMilli(), 10, labels.FromStrings("path", "/foo")), + createSample(defaultQueryTime.UnixMilli(), 10, labels.FromStrings("path", "/bar")), + }, + }, + { + name: "eval instant at 50s xincrease, with 10s lookback", + load: defaultLoad, + query: "xincrease(http_requests[10s])", + expected: []promql.Sample{ + createSample(defaultQueryTime.UnixMilli(), 20, labels.FromStrings("path", "/foo")), + createSample(defaultQueryTime.UnixMilli(), 20, labels.FromStrings("path", "/bar")), + }, + }, + { + name: "eval instant at 50s xincrease, with 3s lookback", + load: defaultLoad, + query: "xincrease(http_requests[3s])", + expected: []promql.Sample{ + createSample(defaultQueryTime.UnixMilli(), 10, labels.FromStrings("path", "/foo")), + createSample(defaultQueryTime.UnixMilli(), 10, labels.FromStrings("path", "/bar")), + }, + }, + // Additional tests + { + name: "eval instant at 17s xincrease, with 5s lookback", + load: defaultLoad, + query: "xincrease(http_requests[5s])", + queryTime: time.Unix(17, 0), + expected: []promql.Sample{ + createSample(17000, 10, labels.FromStrings("path", "/foo")), + createSample(17000, 10, labels.FromStrings("path", "/bar")), + }, + }, + { + name: "eval instant at 17s xincrease, with 10s lookback", + load: defaultLoad, + query: "xincrease(http_requests[10s])", + queryTime: time.Unix(17, 0), + expected: []promql.Sample{ + createSample(17000, 20, labels.FromStrings("path", "/foo")), + createSample(17000, 20, labels.FromStrings("path", "/bar")), + }, + }, + { + name: "eval instant at 50s xrate, with 50s lookback", + load: defaultLoad, + query: "xrate(http_requests[50s])", + expected: []promql.Sample{ + createSample(defaultQueryTime.UnixMilli(), 2, labels.FromStrings("path", "/foo")), + createSample(defaultQueryTime.UnixMilli(), 1.8, labels.FromStrings("path", "/bar")), + }, + }, + { + name: "eval instant at 50s xrate, with 100s lookback", + load: defaultLoad, + query: "xrate(http_requests[100s])", + expected: []promql.Sample{ + createSample(defaultQueryTime.UnixMilli(), 1, labels.FromStrings("path", "/foo")), + createSample(defaultQueryTime.UnixMilli(), 0.9, labels.FromStrings("path", "/bar")), + }, + }, + // Test zero series injection. + { + name: "eval instant xincrease with only one point", + load: `load 5m + http_requests{path="/foo"} stale stale stale 5`, + query: "xincrease(http_requests[1h15m])", + queryTime: time.Unix(1*60*60+15*60, 0), + expected: []promql.Sample{ + createSample(time.Unix(1*60*60+15*60, 0).UnixMilli(), 5, labels.FromStrings("path", "/foo")), + }, + }, + { + name: "eval instant at 50s xrate, with 5s lookback", + load: defaultLoad, + query: "xrate(http_requests[5s])", + expected: []promql.Sample{ + createSample(defaultQueryTime.UnixMilli(), 2, labels.FromStrings("path", "/foo")), + createSample(defaultQueryTime.UnixMilli(), 2, labels.FromStrings("path", "/bar")), + }, + }, + { + name: "eval instant at 50s xrate, with 3s lookback", + load: defaultLoad, + query: "xrate(http_requests[3s])", + expected: []promql.Sample{ + createSample(defaultQueryTime.UnixMilli(), 2, labels.FromStrings("path", "/foo")), + createSample(defaultQueryTime.UnixMilli(), 2, labels.FromStrings("path", "/bar")), + }, + }, + // # Test for increase()/xincrease with counter reset. + // # When the counter is reset, it always starts at 0. + // # So the sequence 3 2 (decreasing counter = reset) is interpreted the same as 3 0 1 2. + // # Prometheus assumes it missed the intermediate values 0 and 1. + { + name: "eval instant at 30m increase(http_requests[30m])", + load: `load 5m + http_requests{path="/foo"} 0 1 2 3 2 3 4`, + query: `increase(http_requests[30m])`, + queryTime: time.Unix(1800, 0), + expected: []promql.Sample{ + createSample(1800000, 7, labels.FromStrings("path", "/foo")), + }, + }, + { + name: "eval instant at 30m xincrease(http_requests[30m])", + load: `load 5m + http_requests{path="/foo"} 0 1 2 3 2 3 4`, + query: "xincrease(http_requests[30m])", + queryTime: time.Unix(1800, 0), + expected: []promql.Sample{ + createSample(1800000, 7, labels.FromStrings("path", "/foo")), + }, + }, + // Tests for xDelta + { + name: "eval instant at 20m xdelta(http_requests[20m]), with 20m lookback", + load: xDeltaLoad, + query: "xdelta(http_requests[20m])", + queryTime: time.Unix(1200, 0), + expected: []promql.Sample{ + createSample(1200000, 200, labels.FromStrings("path", "/foo")), + createSample(1200000, -200, labels.FromStrings("path", "/bar")), + }, + }, + { + name: "eval instant at 20m xdelta(http_requests[19m]), with 19m lookback", + load: xDeltaLoad, + query: "xdelta(http_requests[19m])", + queryTime: time.Unix(1200, 0), + expected: []promql.Sample{ + createSample(1200000, 190, labels.FromStrings("path", "/foo")), + createSample(1200000, -190, labels.FromStrings("path", "/bar")), + }, + }, + { + name: "eval instant at 20m xdelta(http_requests[1m]), with 1m lookback", + load: xDeltaLoad, + query: "xdelta(http_requests[1m])", + queryTime: time.Unix(1200, 0), + expected: []promql.Sample{ + createSample(1200000, 10, labels.FromStrings("path", "/foo")), + createSample(1200000, -10, labels.FromStrings("path", "/bar")), + }, + }, + { + name: "eval instant at 4m xincrease(http_requests[2m1s]), with 1m lookback", + load: `load 30s + http_requests 0 0 0 0 1 1 1 1`, + query: "xincrease(http_requests[2m1s])", + queryTime: time.Unix(240, 0), + expected: []promql.Sample{ + createSample(240000, 1, labels.Labels{}), + }, + }, + { + name: "eval instant at 4m xincrease(http_requests[2m]), with 1m lookback", + load: `load 30s + http_requests 0 0 0 0 1 1 1 1`, + query: "xincrease(http_requests[2m])", + queryTime: time.Unix(240, 0), + expected: []promql.Sample{ + createSample(240000, 0, labels.Labels{}), + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + storage := promqltest.LoadedStorage(t, tc.load) + defer storage.Close() + + queryTime := defaultQueryTime + if tc.queryTime != (time.Time{}) { + queryTime = tc.queryTime + } + + optimizers := logicalplan.AllOptimizers + + ctx := context.Background() + newEngine := engine.New(engine.Opts{ + EngineOpts: opts, + LogicalOptimizers: optimizers, + EnableXFunctions: true, + }) + query, err := newEngine.NewInstantQuery(ctx, storage, nil, tc.query, queryTime) + testutil.Ok(t, err) + defer query.Close() + + engineResult := query.Exec(ctx) + testutil.Ok(t, engineResult.Err) + expectedResult := createVectorResult(tc.expected) + + testutil.WithGoCmp(comparer).Equals(t, expectedResult, engineResult, queryExplanation(query)) + }) + } +} + +func TestXFunctionsWhenDisabled(t *testing.T) { + var ( + query = "xincrease(http_requests[50s])" + start = time.Unix(0, 0) + end = time.Unix(100, 0) + step = time.Second * 10 + ) + ng := engine.New(engine.Opts{}) + _, err := ng.NewRangeQuery(context.Background(), nil, nil, query, start, end, step) + testutil.NotOk(t, err) + testutil.Equals(t, `1:1: parse error: unknown function with name "xincrease"`, err.Error()) + + _, err = ng.NewInstantQuery(context.Background(), nil, nil, query, start) + testutil.NotOk(t, err) + testutil.Equals(t, `1:1: parse error: unknown function with name "xincrease"`, err.Error()) +} + +func TestRateVsXRate(t *testing.T) { + defaultQueryTime := time.Unix(25, 0) + // Negative offset and at modifier are enabled by default + // since Prometheus v2.33.0, so we also enable them. + opts := promql.EngineOpts{ + Timeout: 1 * time.Hour, + MaxSamples: 1e10, + EnableNegativeOffset: true, + EnableAtModifier: true, + } + + defaultLoad := `load 5s + http_requests{path="/foo"} 1 1 1 2 2 2 2 2 3 3 3 + http_requests{path="/bar"} 1 2 3 4 5 6 7 8 9 10 11` + + cases := []struct { + name string + load string + query string + queryTime time.Time + expected promql.Vector + rangeQuery bool + startTime time.Time + endTime time.Time + }{ + // ### Timeseries starts insice range, (presumably) goes on after range end. ### + // 1. Reference eval + { + name: "eval instant at 25s rate, with 50s lookback", + query: `rate(http_requests[50s])`, + queryTime: time.Unix(25, 0), + expected: []promql.Sample{ + createSample(defaultQueryTime.UnixMilli(), 0.022, labels.FromStrings("path", "/foo")), + createSample(defaultQueryTime.UnixMilli(), 0.11000000000000001, labels.FromStrings("path", "/bar")), + }, + }, + { + name: "eval instant at 25s xrate, with 50s lookback", + query: "xrate(http_requests[50s])", + queryTime: time.Unix(25, 0), + expected: []promql.Sample{ + createSample(defaultQueryTime.UnixMilli(), 0.02, labels.FromStrings("path", "/foo")), + createSample(defaultQueryTime.UnixMilli(), 0.1, labels.FromStrings("path", "/bar")), + }, + }, + // 2. Eval 1 second earlier compared to (1). + // * path="/foo" rate should be same or fractionally higher ("shorter" sample, same actual increase); + // * path="/bar" rate should be same or fractionally lower (80% the increase, 80/96% range covered by sample). + // XXX Seeing ~20% jump for path="/foo" + { + name: "eval instant at 24s rate(http_requests[50s]), with 50s lookback", + query: `rate(http_requests[50s])`, + queryTime: time.Unix(24, 0), + expected: []promql.Sample{ + createSample(24000, 0.0265, labels.FromStrings("path", "/foo")), + createSample(24000, 0.106, labels.FromStrings("path", "/bar")), + }, + }, + { + name: "eval instant at 24s xrate(http_requests[50s]), with 50s lookback", + query: "xrate(http_requests[50s])", + queryTime: time.Unix(24, 0), + expected: []promql.Sample{ + createSample(24000, 0.02, labels.FromStrings("path", "/foo")), + createSample(24000, 0.08, labels.FromStrings("path", "/bar")), + }, + }, + // 3. Eval 1 second later compared to (1) + // * path="/foo" rate should be same or fractionally lower ("longer" sample, same actual increase). + // * path="/bar" rate should be same or fractionally lower ("longer" sample, same actual increase). + // XXX Higher instead of lower for both. + { + name: "eval instant at 26s rate(http_requests[50s]), with 50s lookback", + query: `rate(http_requests[50s])`, + queryTime: time.Unix(26, 0), + expected: []promql.Sample{ + createSample(26000, 0.022799999999999997, labels.FromStrings("path", "/foo")), + createSample(26000, 0.11399999999999999, labels.FromStrings("path", "/bar")), + }, + }, + { + name: "eval instant at 26s xrate(http_requests[50s]), with 50s lookback", + query: "xrate(http_requests[50s])", + queryTime: time.Unix(26, 0), + expected: []promql.Sample{ + createSample(26000, 0.02, labels.FromStrings("path", "/foo")), + createSample(26000, 0.1, labels.FromStrings("path", "/bar")), + }, + }, + // ### Timeseries starts before range, ends within range. ### + // 4. Reference eval + { + name: "eval instant at 75s rate(http_requests[50s]), with 50s lookback", + query: `rate(http_requests[50s])`, + queryTime: time.Unix(75, 0), + expected: []promql.Sample{ + createSample(75000, 0.0275, labels.FromStrings("path", "/foo")), + createSample(75000, 0.11, labels.FromStrings("path", "/bar")), + }, + }, + { + name: "eval instant at 75s xrate(http_requests[51s]), with 50s lookback", + query: "xrate(http_requests[50s])", + queryTime: time.Unix(75, 0), + expected: []promql.Sample{ + createSample(75000, 0.02, labels.FromStrings("path", "/foo")), + createSample(75000, 0.1, labels.FromStrings("path", "/bar")), + }, + }, + // 5. Eval 1s earlier compared to (4) + // * path="/foo" rate should be same or fractionally lower ("longer" sample, same actual increase). + // * path="/bar" rate should be same or fractionally lower ("longer" sample, same actual increase). + // # XXX Higher instead of lower for both. + { + name: "eval instant at 74s rate(http_requests[50s]), with 50s lookback", + query: `rate(http_requests[50s])`, + queryTime: time.Unix(74, 0), + expected: []promql.Sample{ + createSample(74000, 0.02279999999, labels.FromStrings("path", "/foo")), + createSample(74000, 0.11399999999, labels.FromStrings("path", "/bar")), + }, + }, + { + name: "eval instant at 74s xrate(http_requests[50s]), with 50s lookback", + query: "xrate(http_requests[50s])", + queryTime: time.Unix(74, 0), + expected: []promql.Sample{ + createSample(74000, 0.02, labels.FromStrings("path", "/foo")), + createSample(74000, 0.12, labels.FromStrings("path", "/bar")), + }, + }, + // 6. Eval 1s later compared to (4). Rate/increase (should be) fractionally smaller. + // * path="/foo" rate should be same or fractionally higher ("shorter" sample, same actual increase) + // * path="/bar" rate should be same or fractionally lower (80% the increase, 80/96% range covered by sample). + // XXX Seeing ~20% jump for path="/foo", decrease instead of increase for path="/bar". + { + name: "eval instant at 76s rate(http_requests[50s]), with 50s lookback", + query: `rate(http_requests[50s])`, + queryTime: time.Unix(76, 0), + expected: []promql.Sample{ + createSample(76000, 0.0265, labels.FromStrings("path", "/foo")), + createSample(76000, 0.106, labels.FromStrings("path", "/bar")), + }, + }, + { + name: "eval instant at 76s xrate(http_requests[50s]), with 50s lookback", + query: "xrate(http_requests[50s])", + queryTime: time.Unix(76, 0), + expected: []promql.Sample{ + createSample(76000, 0.02, labels.FromStrings("path", "/foo")), + createSample(76000, 0.1, labels.FromStrings("path", "/bar")), + }, + }, + // Evaluation of 10 second rate every 10 seconds + { + name: "eval instant at 9s rate(http_requests[10s]), with 10s lookback", + query: `rate(http_requests[10s])`, + queryTime: time.Unix(9, 0), + expected: []promql.Sample{ + createSample(9000, 0, labels.FromStrings("path", "/foo")), + createSample(9000, 0.2, labels.FromStrings("path", "/bar")), + }, + }, + { + name: "eval instant at 19s rate(http_requests[10s]), with 10s lookback", + query: `rate(http_requests[10s])`, + queryTime: time.Unix(19, 0), + expected: []promql.Sample{ + createSample(19000, 0.2, labels.FromStrings("path", "/foo")), + createSample(19000, 0.2, labels.FromStrings("path", "/bar")), + }, + }, + { + name: "eval instant at 29s rate(http_requests[10s]), with 10s lookback", + query: `rate(http_requests[10s])`, + queryTime: time.Unix(29, 0), + expected: []promql.Sample{ + createSample(29000, 0, labels.FromStrings("path", "/foo")), + createSample(29000, 0.2, labels.FromStrings("path", "/bar")), + }, + }, + { + name: "eval instant at 39s rate(http_requests[10s]), with 10s lookback", + query: `rate(http_requests[10s])`, + queryTime: time.Unix(39, 0), + expected: []promql.Sample{ + createSample(39000, 0, labels.FromStrings("path", "/foo")), + createSample(39000, 0.2, labels.FromStrings("path", "/bar")), + }, + }, + // XXX Missed an increase in path="/foo" between timestamps 35 and 40 (both in this eval and the one before). + { + name: "eval instant at 49s rate(http_requests[10s]), with 10s lookback", + query: `rate(http_requests[10s])`, + queryTime: time.Unix(49, 0), + expected: []promql.Sample{ + createSample(49000, 0, labels.FromStrings("path", "/foo")), + createSample(49000, 0.2, labels.FromStrings("path", "/bar")), + }, + }, + { + name: "eval instant at 9s xrate(http_requests[50s]), with 10s lookback", + query: "xrate(http_requests[10s])", + queryTime: time.Unix(9, 0), + expected: []promql.Sample{ + createSample(9000, 0, labels.FromStrings("path", "/foo")), + createSample(9000, 0.1, labels.FromStrings("path", "/bar")), + }, + }, + { + name: "eval instant at 19s xrate(http_requests[50s]), with 10s lookback", + query: "xrate(http_requests[10s])", + queryTime: time.Unix(19, 0), + expected: []promql.Sample{ + createSample(19000, 0.1, labels.FromStrings("path", "/foo")), + createSample(19000, 0.2, labels.FromStrings("path", "/bar")), + }, + }, + { + name: "eval instant at 29s xrate(http_requests[50s]), with 10s lookback", + query: "xrate(http_requests[10s])", + queryTime: time.Unix(29, 0), + expected: []promql.Sample{ + createSample(29000, 0, labels.FromStrings("path", "/foo")), + createSample(29000, 0.2, labels.FromStrings("path", "/bar")), + }, + }, + { + name: "eval instant at 39s xrate(http_requests[50s]), with 10s lookback", + query: "xrate(http_requests[10s])", + queryTime: time.Unix(39, 0), + expected: []promql.Sample{ + createSample(39000, 0, labels.FromStrings("path", "/foo")), + createSample(39000, 0.2, labels.FromStrings("path", "/bar")), + }, + }, + // Sees the increase in path="/foo" between timestamps 35 and 40. + { + name: "eval instant at 49s xrate(http_requests[50s]), with 10s lookback", + query: "xrate(http_requests[10s])", + queryTime: time.Unix(49, 0), + expected: []promql.Sample{ + createSample(49000, 0.1, labels.FromStrings("path", "/foo")), + createSample(49000, 0.2, labels.FromStrings("path", "/bar")), + }, + }, + // xincrease injects a zero if there is only one sample in the given timerange. + { + name: "eval instant at 1s xincrease(http_requests[50s]), with 5s lookback", + query: "xincrease(http_requests[5s])", + queryTime: time.Unix(1, 0), + expected: []promql.Sample{ + createSample(1000, 1, labels.FromStrings("path", "/foo")), + createSample(1000, 1, labels.FromStrings("path", "/bar")), + }, + }, + // xincrease injects a zero if there is only one sample in the given timerange. + { + name: "eval instant at 1s xincrease(http_requests[50s]), with 5s lookback", + query: "xincrease(http_requests[5s])", + queryTime: time.Unix(1, 0), + expected: []promql.Sample{ + createSample(1000, 1, labels.FromStrings("path", "/foo")), + createSample(1000, 1, labels.FromStrings("path", "/bar")), + }, + }, + // xincrease does not inject anything at the end of the given timerange if there are two or more samples. + { + name: "eval instant at 55s xincrease(http_requests[10s]), with 10s lookback", + query: "xincrease(http_requests[10s])", + queryTime: time.Unix(55, 0), + expected: []promql.Sample{ + createSample(55000, 0, labels.FromStrings("path", "/foo")), + createSample(55000, 1, labels.FromStrings("path", "/bar")), + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + load := defaultLoad + if tc.load != "" { + load = tc.load + } + + storage := promqltest.LoadedStorage(t, load) + defer storage.Close() + + queryTime := defaultQueryTime + if tc.queryTime != (time.Time{}) { + queryTime = tc.queryTime + } + + optimizers := logicalplan.AllOptimizers + + newEngine := engine.New(engine.Opts{ + EngineOpts: opts, + LogicalOptimizers: optimizers, + EnableXFunctions: true, + }) + query, err := newEngine.NewInstantQuery(context.Background(), storage, nil, tc.query, queryTime) + testutil.Ok(t, err) + defer query.Close() + + engineResult := query.Exec(context.Background()) + expectedResult := createVectorResult(tc.expected) + + testutil.WithGoCmp(comparer).Equals(t, expectedResult, engineResult, queryExplanation(query)) + }) + } +} + +func createSample(t int64, v float64, metric labels.Labels) promql.Sample { + return promql.Sample{ + T: t, + F: v, + H: nil, + Metric: metric, + } +} + +func createVectorResult(vector promql.Vector) *promql.Result { + return &promql.Result{ + Err: nil, + Value: vector, + Warnings: nil, + } +} + +func TestInstantQuery(t *testing.T) { + t.Parallel() + + defaultQueryTime := time.Unix(50, 0) + cases := []struct { + load string + name string + query string + queryTime time.Time + }{ + { + name: "eval instant at 2m ts_of_min_over_time, with 2m lookback", + load: `load 5s + http_requests{path="/foo"} 0+10x10 + http_requests{path="/bar"} 0+10x5 0+10x4`, + queryTime: time.Unix(120, 0), + query: "ts_of_min_over_time(http_requests[2m])", + }, + { + name: "eval instant at 2m ts_of_max_over_time, with 2m lookback", + load: `load 5s + http_requests{path="/foo"} 0+10x10 + http_requests{path="/bar"} 0+10x5 0+10x4`, + queryTime: time.Unix(120, 0), + query: "ts_of_max_over_time(http_requests[2m])", + }, + { + name: "eval instant at 2m ts_of_max_over_time, with subquery", + load: `load 5s + http_requests{path="/foo"} 0+10x10 + http_requests{path="/bar"} 0+10x5 0+10x4`, + queryTime: time.Unix(120, 0), + query: "ts_of_max_over_time(rate(http_requests[30s])[2m:5s])", + }, + { + name: "count_values fuzz", + load: `load 30s + http_requests_total{pod="nginx-1", route="/"} 51.00+1.00x40 + http_requests_total{pod="nginx-2", route="/"} -74+14.00x40`, + query: ` +count_values without () ( + "value", + (atanh(http_requests_total{pod="nginx-1"}) > tanh(http_requests_total{route="/"})) + or + avg by (pod, __name__) (http_requests_total{route="/"}) +)`, + }, + { + name: "sum evaluates to -0 fuzz", + load: `load 30s + http_requests_total{pod="nginx-2", route="/"} 0`, + query: `sum by (pod) (-http_requests_total) atan2 -0`, + queryTime: time.Unix(0, 0), + }, + { + name: "count_values", + load: `load 30s + version{foo="bar"} 1 + version{foo="baz"} 1 + version{foo="quz"} 2`, + query: `count_values("val", version)`, + queryTime: time.Unix(0, 0), + }, + { + name: "binary pairing early exit fuzz", + load: `load 30s + http_requests_total{pod="nginx-1", route="/"} 33.00+1.00x40 + http_requests_total{pod="nginx-2", route="/"} 1+2.00x40`, + query: ` + avg without (route) (avg(http_requests_total) / http_requests_total) +<= + sum by (__name__) (http_requests_total or avg(http_requests_total))`, + queryTime: time.Unix(0, 0), + }, + { + name: "offset and @ modifiers", + load: `load 30s + http_requests_total{pod="nginx-0", route="/"} 1+1x30`, + query: `http_requests_total @ end() offset 2m`, + queryTime: time.Unix(300, 0), + }, + { + name: "timestamp - offset modifier", + load: `load 30s + http_requests_total{pod="nginx-0", route="/"} 0x30`, + query: `timestamp(http_requests_total offset 2m)`, + queryTime: time.Unix(300, 0), + }, + { + name: "timestamp - @ modifier", + load: `load 30s + http_requests_total{pod="nginx-0", route="/"} 0x30`, + query: `timestamp(http_requests_total @ 60.000)`, + queryTime: time.Unix(300, 0), + }, + { + name: "timestamp - nested functions with offset", + load: `load 30s + http_requests_total{pod="nginx-0", route="/"} 0x30`, + query: `timestamp(timestamp(http_requests_total offset 2m))`, + queryTime: time.Unix(300, 0), + }, + { + name: "timestamp - nested functions without any scan", + query: `timestamp(vector(1))`, + queryTime: time.Unix(300, 0), + }, + { + name: "timestamp - aggregation", + load: `load 30s + http_requests_total{pod="nginx-0", route="/"} 0x30`, + query: `timestamp(sum(http_requests_total))`, + queryTime: time.Unix(300, 0), + }, + { + name: "timestamp - fuzzing failure", + load: `load 30s + http_requests_total{pod="nginx-1"} 1.00+1.00x15 + http_requests_total{pod="nginx-2"} 1+2.00x21`, + query: `timestamp(http_requests_total @ end() offset -2m23s)`, + queryTime: time.Unix(300, 0), + }, + { + name: "fuzz - min with NaN", + load: `load 30s + http_requests_total{pod="nginx-1", route="/"} 0 + http_requests_total{pod="nginx-2", route="/"} NaN`, + query: `min without (__name__, pod) (http_requests_total)`, + queryTime: time.Unix(0, 0), + }, + { + name: "fuzz - max with NaN", + load: `load 30s + http_requests_total{pod="nginx-1", route="/"} 0 + http_requests_total{pod="nginx-2", route="/"} NaN`, + query: `max without (__name__, pod) (http_requests_total)`, + queryTime: time.Unix(0, 0), + }, + { + name: "fuzz - min with NaN", + load: `load 30s + http_requests_total{pod="nginx-1", route="/"} 124.00+1.00x40 + http_requests_total{pod="nginx-2", route="/"} 0+0.29x40`, + query: `min by (route, pod) (sqrt(-http_requests_total))`, + }, + { + name: "fuzz - min with Inf", + load: `load 30s + http_requests_total{pod="nginx-1", route="/"} 483.00+6035.00x40 + http_requests_total{pod="nginx-2", route="/"} 2+47.14x40`, + query: ` +min without () ( + ( + {__name__="http_requests_total"} @ start() offset -2m40s + ^ + {__name__="http_requests_total"} @ start() offset -2m49s + ) +)`, + }, + /* + This is a known issue, we lose the signed 0 in the sum because we add to a + default element. Prometheus assigns the first element to the sum and preserves + the sign. + { + name: "fuzz - signed zero", + query: `1/sum(-(absent(X)-1))`, + }, + */ + { + name: "sum_over_time with subquery", + load: `load 10s + http_requests_total{pod="nginx-1", series="1"} 1+1x40 + http_requests_total{pod="nginx-2", series="2"} 2+2x50 + http_requests_total{pod="nginx-4", series="3"} 5+2x50 + http_requests_total{pod="nginx-5", series="1"} 8+4x50 + http_requests_total{pod="nginx-6", series="2"} 2+3x50`, + queryTime: time.Unix(600, 0), + query: `sum_over_time(sum by (series) (http_requests_total)[5m:1m])`, + }, + { + name: "sum_over_time with subquery with default step", + load: `load 10s + http_requests_total{pod="nginx-1", series="1"} 1+1x40 + http_requests_total{pod="nginx-2", series="2"} 2+2x50 + http_requests_total{pod="nginx-4", series="3"} 5+2x50 + http_requests_total{pod="nginx-5", series="1"} 8+4x50 + http_requests_total{pod="nginx-6", series="2"} 2+3x50`, + queryTime: time.Unix(600, 0), + query: `sum_over_time(sum by (series) (http_requests_total)[5m:])`, + }, + { + name: "sum_over_time with subquery with resolution that doesnt divide step length", + load: `load 10s + http_requests_total{pod="nginx-1", series="1"} 1+1x40 + http_requests_total{pod="nginx-2", series="2"} 2+2x50 + http_requests_total{pod="nginx-4", series="3"} 5+2x50 + http_requests_total{pod="nginx-5", series="1"} 8+4x50 + http_requests_total{pod="nginx-6", series="2"} 2+3x50`, + queryTime: time.Unix(600, 0), + query: `sum_over_time(sum by (series) (http_requests_total)[5m:22s])`, + }, + { + name: "sum_over_time with subquery with offset", + load: `load 10s + http_requests_total{pod="nginx-1", series="1"} 1+1x40 + http_requests_total{pod="nginx-2", series="2"} 2+2x50 + http_requests_total{pod="nginx-4", series="3"} 5+2x50 + http_requests_total{pod="nginx-5", series="1"} 8+4x50 + http_requests_total{pod="nginx-6", series="2"} 2+3x50`, + queryTime: time.Unix(600, 0), + query: `sum_over_time(sum by (series) (http_requests_total)[5m:1m] offset 1m)`, + }, + { + name: "sum_over_time with subquery with inner offset", + load: `load 10s + http_requests_total{pod="nginx-1", series="1"} 1+1x40 + http_requests_total{pod="nginx-2", series="2"} 2+2x50 + http_requests_total{pod="nginx-4", series="3"} 5+2x50 + http_requests_total{pod="nginx-5", series="1"} 8+4x50 + http_requests_total{pod="nginx-6", series="2"} 2+3x50`, + queryTime: time.Unix(600, 0), + query: `sum_over_time(sum by (series) (http_requests_total offset 1m)[5m:1m])`, + }, + { + name: "sum_over_time with subquery with inner @ modifier", + load: `load 10s + http_requests_total{pod="nginx-1", series="1"} 1+1x40 + http_requests_total{pod="nginx-2", series="2"} 2+2x50 + http_requests_total{pod="nginx-4", series="3"} 5+2x50 + http_requests_total{pod="nginx-5", series="1"} 8+4x50 + http_requests_total{pod="nginx-6", series="2"} 2+3x50`, + queryTime: time.Unix(600, 0), + query: `sum_over_time(sum by (series) (http_requests_total @ 10.000)[5m:1m])`, + }, + { + name: "sum_over_time with nested subqueries with inner @ modifier", + load: `load 10s + http_requests_total{pod="nginx-1", series="1"} 1+1x40 + http_requests_total{pod="nginx-2", series="2"} 2+2x50 + http_requests_total{pod="nginx-4", series="3"} 5+2x50 + http_requests_total{pod="nginx-5", series="1"} 8+4x50 + http_requests_total{pod="nginx-6", series="2"} 2+3x50`, + queryTime: time.Unix(600, 0), + query: `sum_over_time(rate(sum by (series) (http_requests_total @ 10.000)[5m:1m] @ 0.000)[10m:1m])`, + }, + { + name: "sum_over_time with subquery should drop name label", + load: `load 10s + http_requests_total{pod="nginx-1", series="1"} 1+1x40 + http_requests_total{pod="nginx-2", series="1"} 2+2x50`, + queryTime: time.Unix(0, 0), + query: `sum_over_time(http_requests_total{series="1"} offset 7s[1h:1m] @ 119.800)`, + }, + { + name: "duplicate label set", + load: `load 5m + testmetric1{src="a",dst="b"} 0 + testmetric2{src="a",dst="b"} 1`, + query: `changes({__name__=~"testmetric1|testmetric2"}[5m])`, + }, + { + name: "scalar", + load: ``, + queryTime: time.Unix(160, 0), + query: `12 + 1`, + }, + { + name: "string literal", + load: ``, + queryTime: time.Unix(160, 0), + query: `test - string - literal`, + }, + { + name: "increase plus offset", + load: `load 1s + http_requests_total{pod="nginx-1"} 1+1x180`, + queryTime: time.Unix(160, 0), + query: `increase(http_requests_total[1m] offset 1m)`, + }, + { + name: "round", + load: `load 1s + http_requests_total{pod="nginx-1", series="1"} 1+1.1x40 + http_requests_total{pod="nginx-2", series="2"} 2+2.3x50 + http_requests_total{pod="nginx-4", series="3"} 5+2.4x50 + http_requests_total{pod="nginx-5", series="1"} 8.4+2.3x50 + http_requests_total{pod="nginx-6", series="2"} 2.3+2.3x50`, + queryTime: time.Unix(0, 0), + query: `round(http_requests_total)`, + }, + { + name: "round with argument", + load: `load 1s + http_requests_total{pod="nginx-1", series="1"} 1+1.1x40 + http_requests_total{pod="nginx-2", series="2"} 2+2.3x50 + http_requests_total{pod="nginx-4", series="3"} 5+2.4x50 + http_requests_total{pod="nginx-5", series="1"} 8.4+2.3x50 + http_requests_total{pod="nginx-6", series="2"} 2.3+2.3x50`, + queryTime: time.Unix(0, 0), + query: `round(http_requests_total, 0.5)`, + }, + { + name: "sort", + load: `load 1s + http_requests_total{pod="nginx-1", series="1"} 1+1.1x40 + http_requests_total{pod="nginx-2", series="2"} 2+2.3x50 + http_requests_total{pod="nginx-4", series="3"} 5+2.4x50 + http_requests_total{pod="nginx-5", series="1"} 8.4+2.3x50 + http_requests_total{pod="nginx-6", series="2"} 2.3+2.3x50`, + queryTime: time.Unix(0, 0), + query: `sort(http_requests_total)`, + }, + { + name: "sort_desc", + load: `load 1s + http_requests_total{pod="nginx-1", series="1"} 1+1.1x40 + http_requests_total{pod="nginx-2", series="2"} 2+2.3x50 + http_requests_total{pod="nginx-4", series="3"} 5+2.4x50 + http_requests_total{pod="nginx-5", series="1"} 8.4+2.3x50 + http_requests_total{pod="nginx-6", series="2"} 2.3+2.3x50`, + queryTime: time.Unix(0, 0), + query: `sort_desc(http_requests_total)`, + }, + { + name: "histogram_quantile with mock duplicate labels", + load: `load 30s + http_requests_total{pod="nginx-2", route="/"} 0+0.14x40`, + queryTime: time.Unix(600, 0), + query: `histogram_quantile(10, -http_requests_total or http_requests_total)`, + }, + { + name: "quantile by pod", + load: `load 30s + http_requests_total{pod="nginx-1", series="1"} 1+1.1x40 + http_requests_total{pod="nginx-2", series="2"} 2+2.3x50 + http_requests_total{pod="nginx-4", series="3"} 5+2.4x50 + http_requests_total{pod="nginx-5", series="1"} 8.4+2.3x50 + http_requests_total{pod="nginx-6", series="2"} 2.3+2.3x50`, + query: `quantile by (pod) (0.9, rate(http_requests_total[1m]))`, + }, + { + name: "quantile by pod with binary", + load: `load 30s + http_requests_total{pod="nginx-1", series="1"} 1+1.1x40 + http_requests_total{pod="nginx-2", series="2"} 2+2.3x50 + http_requests_total{pod="nginx-4", series="3"} 5+2.4x50 + http_requests_total{pod="nginx-5", series="1"} 8.4+2.3x50 + http_requests_total{pod="nginx-6", series="2"} 2.3+2.3x50`, + query: `quantile by (pod) (1 - 0.1, rate(http_requests_total[1m]))`, + }, + { + name: "quantile by pod with expression", + load: `load 30s + http_requests_total{pod="nginx-1", series="1"} 1+1.1x40 + http_requests_total{pod="nginx-2", series="2"} 2+2.3x50 + http_requests_total{pod="nginx-4", series="3"} 5+2.4x50 + http_requests_total{pod="nginx-5", series="1"} 8.4+2.3x50 + http_requests_total{pod="nginx-6", series="2"} 2.3+2.3x50`, + query: `quantile by (pod) (scalar(min(http_requests_total)), rate(http_requests_total[1m]))`, + }, + { + name: "quantile", + load: `load 30s + http_requests_total{pod="nginx-1", series="1"} 1+1.1x40 + http_requests_total{pod="nginx-2", series="2"} 2+2.3x50 + http_requests_total{pod="nginx-4", series="3"} 5+2.4x50 + http_requests_total{pod="nginx-5", series="1"} 8.4+2.3x50 + http_requests_total{pod="nginx-6", series="2"} 2.3+2.3x50`, + query: `quantile(0.9, rate(http_requests_total[1m]))`, + }, + { + name: "stdvar", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x4 + http_requests_total{pod="nginx-2"} 1+2x4`, + query: `stdvar(http_requests_total)`, + }, + { + name: "stdvar by pod", + load: `load 30s + http_requests_total{pod="nginx-1"} 1 + http_requests_total{pod="nginx-2"} 2 + http_requests_total{pod="nginx-3"} 8 + http_requests_total{pod="nginx-4"} 6`, + query: `stdvar by (pod) (http_requests_total)`, + }, + { + name: "stddev", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x4 + http_requests_total{pod="nginx-2"} 1+2x4`, + query: `stddev(http_requests_total)`, + }, + { + name: "stddev by pod", + load: `load 30s + http_requests_total{pod="nginx-1"} 1 + http_requests_total{pod="nginx-2"} 2 + http_requests_total{pod="nginx-3"} 8 + http_requests_total{pod="nginx-4"} 6`, + query: `stddev by (pod) (http_requests_total)`, + }, + { + name: "sum by pod", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x4 + http_requests_total{pod="nginx-2"} 1+2x4`, + query: `sum by (pod) (http_requests_total)`, + }, + { + name: "count", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `count(http_requests_total)`, + }, + { + name: "average", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `avg(http_requests_total)`, + }, + { + name: "label_join", + load: `load 30s + http_requests_total{pod="nginx-1", series="1"} 1+1.1x40 + http_requests_total{pod="nginx-2", series="2"} 2+2.3x50 + http_requests_total{pod="nginx-4", series="3"} 5+2.4x50`, + queryTime: time.Unix(160, 0), + query: `label_join(http_requests_total, "label", "-", "pod", "series")`, + }, + { + name: "label_join with non-existing src labels", + load: `load 30s + http_requests_total{pod="nginx-1", series="1"} 1+1.1x40 + http_requests_total{pod="nginx-2", series="2"} 2+2.3x50 + http_requests_total{pod="nginx-4", series="3"} 5+2.4x50`, + queryTime: time.Unix(160, 0), + query: `label_join(http_requests_total, "label", "-", "test", "fake")`, + }, + { + name: "label_join with overwrite dst label if exists", + load: `load 30s + http_requests_total{pod="nginx-1", series="1", label="test-1"} 1+1.1x40 + http_requests_total{pod="nginx-2", series="2", label="test-2"} 2+2.3x50 + http_requests_total{pod="nginx-4", series="3", label="test-3"} 5+2.4x50`, + queryTime: time.Unix(160, 0), + query: `label_join(http_requests_total, "label", "-", "pod", "series")`, + }, + { + name: "label_join with no src labels provided", + load: `load 30s + http_requests_total{pod="nginx-1", series="1"} 1+1.1x40 + http_requests_total{pod="nginx-2", series="2"} 2+2.3x50 + http_requests_total{pod="nginx-4", series="3"} 5+2.4x50`, + queryTime: time.Unix(160, 0), + query: `label_join(http_requests_total, "label", "-")`, + }, + { + name: "label_replace", + load: `load 30s + http_requests_total{pod="nginx-1", series="1"} 1+1.1x40 + http_requests_total{pod="nginx-2", series="2"} 2+2.3x50 + http_requests_total{pod="nginx-4", series="3"} 5+2.4x50`, + queryTime: time.Unix(160, 0), + query: `label_replace(http_requests_total, "foo", "$1", "series", ".*")`, + }, + { + name: "label_replace with bad regular expression", + load: `load 30s + http_requests_total{pod="nginx-1", series="1"} 1+1.1x40 + http_requests_total{pod="nginx-2", series="2"} 2+2.3x50 + http_requests_total{pod="nginx-4", series="3"} 5+2.4x50`, + queryTime: time.Unix(160, 0), + query: `label_replace(http_requests_total, "foo", "$1", "series", "]]")`, + }, + { + name: "label_replace non-existing src label", + load: `load 30s + http_requests_total{pod="nginx-1", series="1"} 1+1.1x40 + http_requests_total{pod="nginx-2", series="2"} 2+2.3x50 + http_requests_total{pod="nginx-4", series="3"} 5+2.4x50`, + queryTime: time.Unix(160, 0), + query: `label_replace(http_requests_total, "foo", "$1", "bar", ".*")`, + }, + { + name: "topk", + load: `load 30s + http_requests_total{pod="nginx-1", series="1"} 1 + http_requests_total{pod="nginx-2", series="1"} 2 + http_requests_total{pod="nginx-3", series="1"} 8 + http_requests_total{pod="nginx-4", series="2"} 6 + http_requests_total{pod="nginx-5", series="2"} 8 + http_requests_total{pod="nginx-6", series="3"} 15 + http_requests_total{pod="nginx-7", series="3"} 11 + http_requests_total{pod="nginx-8", series="4"} 22 + http_requests_total{pod="nginx-9", series="4"} 89`, + query: `topk(2, http_requests_total)`, + }, + { + name: "topk by series", + load: `load 30s + http_requests_total{pod="nginx-1", series="1"} 1 + http_requests_total{pod="nginx-2", series="1"} 2 + http_requests_total{pod="nginx-3", series="1"} 8 + http_requests_total{pod="nginx-4", series="2"} 6 + http_requests_total{pod="nginx-5", series="2"} 8 + http_requests_total{pod="nginx-6", series="3"} 15 + http_requests_total{pod="nginx-7", series="3"} 11 + http_requests_total{pod="nginx-8", series="4"} 22 + http_requests_total{pod="nginx-9", series="4"} 89`, + query: `topk by (series) (2, http_requests_total)`, + }, + { + name: "bottomK", + load: `load 30s + http_requests_total{pod="nginx-1", series="1"} 1 + http_requests_total{pod="nginx-2", series="1"} 2 + http_requests_total{pod="nginx-3", series="1"} 8 + http_requests_total{pod="nginx-4", series="2"} 6 + http_requests_total{pod="nginx-5", series="2"} 8 + http_requests_total{pod="nginx-6", series="3"} 15 + http_requests_total{pod="nginx-7", series="3"} 11 + http_requests_total{pod="nginx-8", series="4"} 22 + http_requests_total{pod="nginx-9", series="4"} 89`, + query: `bottomk(2, http_requests_total)`, + }, + { + name: "bottomk by series", + load: `load 30s + http_requests_total{pod="nginx-1", series="1"} 1 + http_requests_total{pod="nginx-2", series="1"} 2 + http_requests_total{pod="nginx-3", series="1"} 8 + http_requests_total{pod="nginx-4", series="2"} 6 + http_requests_total{pod="nginx-5", series="2"} 8 + http_requests_total{pod="nginx-6", series="3"} 15 + http_requests_total{pod="nginx-7", series="3"} 11 + http_requests_total{pod="nginx-8", series="4"} 22 + http_requests_total{pod="nginx-9", series="4"} 89`, + query: `bottomk by (series) (2, http_requests_total)`, + }, + { + name: "limitK", + load: `load 30s + http_requests_total{pod="nginx-1", series="1"} 1 + http_requests_total{pod="nginx-2", series="3"} 2 + http_requests_total{pod="nginx-3", series="2"} 8 + http_requests_total{pod="nginx-4", series="5"} 6 + http_requests_total{pod="nginx-5", series="4"} 8 + http_requests_total{pod="nginx-6", series="7"} 15 + http_requests_total{pod="nginx-7", series="6"} 11 + http_requests_total{pod="nginx-8", series="9"} 22 + http_requests_total{pod="nginx-9", series="8"} 89`, + query: `limitk(2, http_requests_total)`, + }, + { + name: "limitk by (pod)", + load: `load 30s + http_requests_total{pod="nginx-1", series="2"} 89 + http_requests_total{pod="nginx-1", series="1"} 49 + http_requests_total{pod="nginx-1", series="3"} 19 + http_requests_total{pod="nginx-2", series="2"} 12 + http_requests_total{pod="nginx-2", series="1"} 24 + http_requests_total{pod="nginx-3", series="3"} 8 + http_requests_total{pod="nginx-3", series="1"} 22 + http_requests_total{pod="nginx-3", series="2"} 1`, + query: "limitk(2, http_requests_total) by (pod)", + }, + { + name: "limitk(x, many-to-many join)", + load: `load 30s + http_requests_total{pod="nginx-1", series="2"} 89 + http_requests_total{pod="nginx-1", series="1"} 49 + http_requests_total{pod="nginx-1", series="3"} 19 + http_requests_total{pod="nginx-2", series="2"} 12 + http_requests_total{pod="nginx-2", series="1"} 24 + http_requests_total{pod="nginx-3", series="3"} 8 + http_requests_total{pod="nginx-3", series="1"} 22 + http_requests_total{pod="nginx-3", series="2"} 1`, + query: "limitk(2, http_requests_total or limitk(2, sum without (series) (http_requests_total))) by (pod)", + }, + { + name: "limit_ratio", + load: `load 30s + http_requests_total{pod="nginx-1", series="3"} 2 + http_requests_total{pod="nginx-3", series="2"} 4 + http_requests_total{pod="nginx-5", series="1"} 8 + http_requests_total{pod="nginx-7", series="1"} 6 + http_requests_total{pod="nginx-9", series="2"} 12 + http_requests_total{pod="nginx-11", series="3"} 10 + http_requests_total{pod="nginx-13", series="2"} 16 + http_requests_total{pod="nginx-15", series="2"} 12 + http_requests_total{pod="nginx-17", series="5"} 14`, + query: `limit_ratio(0.5, http_requests_total)`, + }, + { + name: "limit_ratio by (series)", + load: `load 30s + http_requests_total{pod="nginx-2", series="1"} 1 + http_requests_total{pod="nginx-4", series="3"} 3 + http_requests_total{pod="nginx-6", series="2"} 7 + http_requests_total{pod="nginx-8", series="1"} 5 + http_requests_total{pod="nginx-10", series="4"} 11 + http_requests_total{pod="nginx-12", series="1"} 9 + http_requests_total{pod="nginx-14", series="2"} 17 + http_requests_total{pod="nginx-16", series="3"} 13 + http_requests_total{pod="nginx-18", series="2"} 15`, + query: `limit_ratio by (series) (0.1, http_requests_total) `, + }, + { + name: "limitk(limit_ratios returning all samples)", + load: `load 30s + http_requests_total{pod="nginx-1", series="3"} 1 + http_requests_total{pod="nginx-3", series="1"} 1 + http_requests_total{pod="nginx-2", series="3"} 2 + http_requests_total{pod="nginx-4", series="1"} 3 + http_requests_total{pod="nginx-6", series="5"} 5 + http_requests_total{pod="nginx-5", series="1"} 8 + http_requests_total{pod="nginx-10", series="2"} 13 + http_requests_total{pod="nginx-12", series="3"} 21 + http_requests_total{pod="nginx-7", series="2"} 34`, + query: `limitk(9, limit_ratio(0.5, http_requests_total) or limit_ratio(-0.5, http_requests_total))`, + }, + { + name: "max", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `max(http_requests_total)`, + }, + { + name: "max with only 1 sample", + load: `load 30s + http_requests_total{pod="nginx-1"} -1 + http_requests_total{pod="nginx-2"} 1`, + query: `max by (pod) (http_requests_total)`, + }, + { + name: "min", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `min(http_requests_total)`, + }, + { + name: "min with only 1 sample", + load: `load 30s + http_requests_total{pod="nginx-1"} -1 + http_requests_total{pod="nginx-2"} 1`, + query: `min by (pod) (http_requests_total)`, + }, + { + name: "rate", + load: `load 30s + http_requests_total{pod="nginx-1", series="1"} 1+1.1x40 + http_requests_total{pod="nginx-2", series="2"} 2+2.3x50 + http_requests_total{pod="nginx-4", series="3"} 5+2.4x50 + http_requests_total{pod="nginx-5", series="1"} 8.4+2.3x50 + http_requests_total{pod="nginx-6", series="2"} 2.3+2.3x50`, + query: `rate(http_requests_total[1m])`, + }, + { + name: "sum rate", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x4 + http_requests_total{pod="nginx-2"} 1+2x4`, + query: `sum(rate(http_requests_total[1m]))`, + }, + { + name: "sum rate with single sample series", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x4 + http_requests_total{pod="nginx-2"} 1+2x4 + http_requests_total{pod="nginx-3"} 0`, + query: `sum by (pod) (rate(http_requests_total[1m]))`, + }, + { + name: "sum rate with stale series", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x4 + http_requests_total{pod="nginx-2"} 1+2x20`, + query: `sum(rate(http_requests_total[1m]))`, + }, + { + name: "delta", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x4 + http_requests_total{pod="nginx-2"} 1+2x4`, + query: `delta(http_requests_total[1m])`, + }, + { + name: "increase", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x4 + http_requests_total{pod="nginx-2"} 1+2x4`, + query: `increase(http_requests_total[1m])`, + }, + + { + name: "sum irate", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x4 + http_requests_total{pod="nginx-2"} 1+2x4`, + query: `sum(irate(http_requests_total[1m]))`, + }, + { + name: "sum irate with stale series", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x4 + http_requests_total{pod="nginx-2"} 1+2x20`, + query: `sum(irate(http_requests_total[1m]))`, + }, + { + name: "number literal", + load: "", + query: `34`, + }, + { + name: "vector", + load: "", + query: `vector(24)`, + }, + { + name: "binary operation with vector and scalar on the right", + load: `load 30s + foo{method="get", code="500"} 1+1.1x30 + foo{method="get", code="404"} 1+2.2x20`, + query: `foo * 2`, + }, + { + name: "binary operation with vector and scalar on the left", + load: `load 30s + foo{method="get", code="500"} 1+1.1x30 + foo{method="get", code="404"} 1+2.2x20`, + query: `2 * foo`, + }, + { + name: "complex binary operation", + load: `load 30s + foo{method="get", code="500"} 1+1.1x30 + foo{method="get", code="404"} 1+2.2x20`, + query: `1 - (100 * sum(foo{method="get"}) / sum(foo))`, + }, + { + name: "vector binary op ==", + load: `load 30s + foo{method="get", code="500"} 1+1x40 + bar{method="get", code="404"} 1+1.1x30`, + query: `sum by (method) (foo) == sum by (method) (bar)`, + }, + { + name: "vector binary op !=", + load: `load 30s + foo{method="get", code="500"} 1+1x40 + bar{method="get", code="404"} 1+1.1x30`, + query: `sum by (method) (foo) != sum by (method) (bar)`, + }, + { + name: "vector binary op >", + load: `load 30s + foo{method="get", code="500"} 1+1x40 + bar{method="get", code="404"} 1+1.1x30`, + query: `sum by (method) (foo) > sum by (method) (bar)`, + }, + { + name: "vector binary op <", + load: `load 30s + foo{method="get", code="500"} 1+1x40 + bar{method="get", code="404"} 1+1.1x30`, + query: `sum by (method) (foo) < sum by (method) (bar)`, + }, + { + name: "vector binary op >=", + load: `load 30s + foo{method="get", code="500"} 1+1x40 + bar{method="get", code="404"} 1+1.1x30`, + query: `sum by (method) (foo) >= sum by (method) (bar)`, + }, + { + name: "vector binary op <=", + load: `load 30s + foo{method="get", code="500"} 1+1x40 + bar{method="get", code="404"} 1+1.1x30`, + query: `sum by (method) (foo) <= sum by (method) (bar)`, + }, + { + name: "vector binary op ^", + load: `load 30s + foo{method="get", code="500"} 1+1x40 + bar{method="get", code="404"} 1+1.1x30`, + query: `sum by (method) (foo) ^ sum by (method) (bar)`, + }, + { + name: "vector binary op %", + load: `load 30s + foo{method="get", code="500"} 1+2x40 + bar{method="get", code="404"} 1+1x30`, + query: `sum by (method) (foo) % sum by (method) (bar)`, + }, + { + name: "vector binary op and 1", + load: `load 30s + foo{method="get", code="500"} 1+2x40 + bar{method="get", code="404"} 1+1x30`, + query: `sum by (method) (foo) and sum by (method) (bar)`, + }, + { + name: "vector binary op and 2", + load: `load 30s + foo{method="get", code="500"} 1+2x40 + bar{method="get", code="404"} 1+1x30`, + query: `sum by (code) (foo) and sum by (code) (bar)`, + }, + { + name: "vector binary op unless 1", + load: `load 30s + foo{method="get", code="500"} 1+2x40 + bar{method="get", code="404"} 1+1x30`, + query: `sum by (method) (foo) unless sum by (method) (bar)`, + }, + { + name: "vector binary op unless 2", + load: `load 30s + foo{method="get", code="500"} 1+2x40 + bar{method="get", code="404"} 1+1x30`, + query: `sum by (code) (foo) unless sum by (code) (bar)`, + }, + { + name: "vector binary op unless 3", + load: `load 30s + foo{method="get", code="500"} 1+2x40`, + query: `sum by (code) (foo) unless nonexistent`, + }, + { + name: "vector binary op or 1", + load: `load 30s + foo{A="1"} 1+1x40 + foo{A="2"} 2+2x40`, + query: `sinh(foo or exp(foo))`, + }, + { + name: "vector binary op one-to-one left multiple matches", + load: `load 30s + foo{method="get", code="500"} 1 + foo{method="get", code="200"} 1 + bar{method="get", code="200"} 1`, + query: `foo / ignoring (code) bar`, + }, + { + name: "vector binary operation with many-to-many matching rhs high card", + load: `load 30s + foo{code="200", method="get"} 1+1x20 + foo{code="200", method="post"} 1+1x20 + bar{code="200", method="get"} 1+1x20 + bar{code="200", method="post"} 1+1x20`, + query: `foo + on (code) group_right () bar`, + }, + { + name: "vector binary op > scalar", + load: `load 30s + foo{method="get", code="500"} 1+2x40 + bar{method="get", code="404"} 1+1x30`, + query: `sum by (method) (foo) > 10`, + }, + { + name: "scalar < vector binary op", + load: `load 30s + foo{method="get", code="500"} 1+2x40 + bar{method="get", code="404"} 1+1x30`, + query: `10 < sum by (method) (foo)`, + }, + { + name: "scalar binary op == true", + load: ``, + query: `1 == bool 1`, + }, + { + name: "scalar binary op == false", + load: ``, + query: `1 != bool 2`, + }, + { + name: "scalar binary op !=", + load: ``, + query: `1 != bool 1`, + }, + { + name: "scalar binary op >", + load: ``, + query: `1 > bool 0`, + }, + { + name: "scalar binary op <", + load: ``, + query: `1 > bool 2`, + }, + { + name: "scalar binary op >=", + load: ``, + query: `1 >= bool 0`, + }, + { + name: "scalar binary op <=", + load: ``, + query: `1 <= bool 2`, + }, + { + name: "scalar binary op % 0", + load: ``, + query: `2 % 2`, + }, + { + name: "scalar binary op % 1", + load: ``, + query: `1 % 2`, + }, + { + name: "scalar binary op ^", + load: ``, + query: `2 ^ 2`, + }, + { + name: "empty series", + load: "", + query: `http_requests_total`, + }, + { + name: "empty series with func", + load: "", + query: `sum(http_requests_total)`, + }, + { + name: "empty result", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `http_requests_total{pod="nginx-3"}`, + }, + { + name: "last_over_time", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `last_over_time(http_requests_total[30s])`, + }, + { + name: "group", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `group(http_requests_total)`, + }, + { + name: "reset", + load: `load 30s + http_requests_total{pod="nginx-1"} 100-1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `resets(http_requests_total[5m])`, + }, + { + name: "present_over_time", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `present_over_time(http_requests_total[30s])`, + }, + { + name: "unary sub operation for scalar", + load: ``, + query: `-(1 + 5)`, + }, + { + name: "unary sub operation for vector", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `-http_requests_total`, + }, + { + name: "unary add operation for vector", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `+http_requests_total`, + }, + { + name: "vector positive offset", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `http_requests_total offset 30s`, + }, + { + name: "vector negative offset", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `http_requests_total offset -30s`, + }, + { + name: "matrix negative offset with sum_over_time", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x25 + http_requests_total{pod="nginx-2"} 1+2x28`, + query: `sum_over_time(http_requests_total[5m] offset 5m)`, + }, + { + name: "matrix negative offset with count_over_time", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `count_over_time(http_requests_total[5m] offset -2m)`, + }, + { + name: "@ vector time 10s", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `http_requests_total @ 10.000`, + }, + { + name: "@ vector time 120s", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `http_requests_total @ 120.000`, + }, + { + name: "@ vector time 360s", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `http_requests_total @ 360.000`, + }, + { + name: "@ vector start", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `http_requests_total @ start()`, + }, + { + name: "@ vector end", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `http_requests_total @ end()`, + }, + { + name: "count_over_time @ start", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `count_over_time(http_requests_total[5m] @ start())`, + }, + { + name: "sum_over_time @ end", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `sum_over_time(http_requests_total[5m] @ start())`, + }, + { + name: "avg_over_time @ 180s", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `avg_over_time(http_requests_total[4m] @ 180.000)`, + }, + { + name: "@ vector 240s offset 2m", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `http_requests_total @ 240.000 offset 2m`, + }, + { + name: "avg_over_time @ 120s offset -2m", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `http_requests_total @ 120.000 offset -2m`, + }, + { + name: "sum_over_time @ 180s offset 2m", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `sum_over_time(http_requests_total[5m] @ 180.000 offset 2m)`, + }, + { + name: "scalar with nested binary operator with step invariant", + load: `load 30s + http_requests_total{pod="nginx-1", route="/"} 53.33+56.00x40 + http_requests_total{pod="nginx-2", route="/"} -26+2.00x40`, + query: `vector(scalar((http_requests_total @ end() offset 5m > http_requests_total)))`, + }, + { + name: "scalar func with non existent metric in scalar comparison", + query: `scalar(non_existent_metric) < bool 0`, + }, + { + name: "scalar func with NaN", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `scalar(http_requests_total)`, + }, + { + name: "scalar func with aggr", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `scalar(max(http_requests_total))`, + }, + { + name: "scalar func with aggr and number on right", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `scalar(max(http_requests_total)) + 10`, + }, + { + name: "scalar func with aggr and number on left", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `10 + scalar(max(http_requests_total))`, + }, + { + name: "clamp", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `clamp(http_requests_total, 5, 10)`, + }, + { + name: "clamp_min", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `clamp_min(http_requests_total, 10)`, + }, + { + name: "complex func query", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `clamp(1 - http_requests_total, 10 - 5, 10)`, + }, + { + name: "func within func query", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `clamp(irate(http_requests_total[30s]), 10 - 5, 10)`, + }, + { + name: "aggr within func query", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `clamp(rate(http_requests_total[30s]), 10 - 5, 10)`, + }, + { + name: "func with scalar arg that selects storage", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `clamp_min(http_requests_total, scalar(max(http_requests_total)))`, + }, + { + name: "func with scalar arg that selects storage + number", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `clamp_min(http_requests_total, scalar(max(http_requests_total)) + 10)`, + }, + { + name: "sgn", + load: `load 30s + http_requests_total{pod="nginx-1", series="1"} 1+1.1x40 + http_requests_total{pod="nginx-2", series="1"} -10+1x50 + http_requests_total{pod="nginx-3", series="1"} NaN`, + query: `sgn(http_requests_total)`, + }, + { + name: "absent and series does not exist", + load: `load 30s`, + query: `absent(nonexistent{job="myjob"})`, + }, + { + name: "absent and series exists", + load: `load 30s + existent{job="myjob"} 1`, + query: `absent(existent{job="myjob"})`, + }, + { + name: "absent and regex matcher", + load: `load 30s`, + query: `absent(nonexistent{instance=~".*",job="myjob"})`, + }, + { + name: "absent and duplicate matchers", + load: `load 30s`, + query: `absent(nonexistent{foo="bar",job="myjob",job="yourjob"})`, + }, + { + name: "absent and nested function", + load: `load 30s`, + query: `absent(sum(nonexistent{job="myjob"}))`, + }, + { + name: "absent and nested absent with existing series", + load: `load 30s + existent{job="myjob"} 1`, + query: `absent(absent(existent{job="myjob"}))`, + }, + { + name: "absent_over_time with subquery - present data", + load: `load 30s + X{a="b"} 1x10`, + query: `absent_over_time(sum_over_time(X{a="b"}[1m])[1m:30s])`, + }, + { + name: "absent_over_time with subquery - missing data", + load: `load 30s + X{a="b"} 1x10`, + query: `absent_over_time(sum_over_time(X{a!="b"}[1m])[1m:30s])`, + }, + { + name: "absent_over_time with subquery and fixed offset", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x10 + http_requests_total{pod="nginx-2"} 1+2x10`, + query: `absent_over_time(http_requests_total @ start()[1h:1m])`, + }, + { + name: "absent_over_time fuzzer findings", + load: `load 30s + http_requests_total{pod="nginx-1", route="/"} 0.02+1.00x40 + http_requests_total{pod="nginx-2", route="/"} -24+0.67x40`, + query: ` + count without (route, pod) ({__name__="http_requests_total"} @ 153.689) +> + absent_over_time({__name__="http_requests_total",route="/"}[3m] offset 1m45s)`, + }, + { + name: "sort_by_label", + load: `load 30s + http_requests{job="api-server", instance="0", group="production"} 0+10x10 + http_requests{job="api-server", instance="1", group="production"} 0+20x10 + http_requests{job="api-server", instance="0", group="canary"} 0+30x10 + http_requests{job="api-server", instance="1", group="canary"} 0+40x10 + http_requests{job="api-server", instance="2", group="canary"} NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN + http_requests{job="app-server", instance="0", group="production"} 0+50x10 + http_requests{job="app-server", instance="1", group="production"} 0+60x10 + http_requests{job="app-server", instance="0", group="canary"} 0+70x10 + http_requests{job="app-server", instance="1", group="canary"} 0+80x10 + http_requests{job="api-server", instance="2", group="production"} 0+10x10`, + query: `sort_by_label_desc(http_requests, "instance")`, + }, + } + + disableOptimizerOpts := []bool{true, false} + lookbackDeltas := []time.Duration{0, 30 * time.Second, time.Minute, 5 * time.Minute, 10 * time.Minute} + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + testStorage := promqltest.LoadedStorage(t, tc.load) + defer testStorage.Close() + for _, disableOptimizers := range disableOptimizerOpts { + t.Run(fmt.Sprintf("disableOptimizers=%t", disableOptimizers), func(t *testing.T) { + for _, lookbackDelta := range lookbackDeltas { + // Negative offset and at modifier are enabled by default + // since Prometheus v2.33.0, so we also enable them. + opts := promql.EngineOpts{ + Timeout: 1 * time.Hour, + MaxSamples: 1e10, + EnableNegativeOffset: true, + EnableAtModifier: true, + NoStepSubqueryIntervalFn: func(rangeMillis int64) int64 { return 30 * time.Second.Milliseconds() }, + LookbackDelta: lookbackDelta, + } + + var queryTime time.Time = defaultQueryTime + if tc.queryTime != (time.Time{}) { + queryTime = tc.queryTime + } + + optimizers := logicalplan.AllOptimizers + if disableOptimizers { + optimizers = logicalplan.NoOptimizers + } + newEngine := engine.New(engine.Opts{ + EngineOpts: opts, + LogicalOptimizers: optimizers, + }) + + ctx := context.Background() + q1, err := newEngine.NewInstantQuery(ctx, testStorage, nil, tc.query, queryTime) + testutil.Ok(t, err) + defer q1.Close() + + newResult := q1.Exec(ctx) + + oldEngine := promql.NewEngine(opts) + q2, err := oldEngine.NewInstantQuery(ctx, testStorage, nil, tc.query, queryTime) + testutil.Ok(t, err) + defer q2.Close() + + oldResult := q2.Exec(ctx) + testutil.WithGoCmp(comparer).Equals(t, oldResult, newResult, queryExplanation(q1)) + } + }) + } + }) + } +} + +func TestQueryCancellation(t *testing.T) { + twelveHours := int64(12 * time.Hour.Seconds()) + + start := time.Unix(0, 0) + end := time.Unix(twelveHours, 0) + step := time.Second * 30 + query := `sum(rate(http_requests_total{pod="nginx-1"}[10s]))` + + querier := &storage.MockQueryable{ + MockQuerier: &storage.MockQuerier{ + SelectMockFunction: func(sortSeries bool, hints *storage.SelectHints, matchers ...*labels.Matcher) storage.SeriesSet { + return newTestSeriesSet(&slowSeries{}) + }, + }, + } + + ctx := context.Background() + newEngine := engine.New(engine.Opts{EngineOpts: promql.EngineOpts{Timeout: 1 * time.Hour}}) + q1, err := newEngine.NewRangeQuery(ctx, querier, nil, query, start, end, step) + testutil.Ok(t, err) + + ctx, cancel := context.WithCancel(ctx) + go func() { + <-time.After(1000 * time.Millisecond) + cancel() + }() + + newResult := q1.Exec(ctx) + testutil.Equals(t, context.Canceled, newResult.Err) +} + +func TestQueryConcurrency(t *testing.T) { + const storageDelay = 200 * time.Millisecond + queryable := &storage.MockQueryable{ + MockQuerier: &storage.MockQuerier{ + SelectMockFunction: func(sortSeries bool, hints *storage.SelectHints, matchers ...*labels.Matcher) storage.SeriesSet { + return newSlowSeriesSet(storageDelay) + }, + }, + } + + var ( + ctx = context.Background() + logger = promslog.New(&promslog.Config{Writer: os.Stdout}) + concurrency = 2 + maxQueries = 4 + responseChan = make(chan struct{}, maxQueries) + ) + newEngine := engine.New(engine.Opts{ + EngineOpts: promql.EngineOpts{ + Timeout: 1 * time.Hour, + MaxSamples: math.MaxInt64, + ActiveQueryTracker: promql.NewActiveQueryTracker(t.TempDir(), concurrency, logger), + }}, + ) + for range maxQueries { + go func() { + qry, err := newEngine.NewRangeQuery(ctx, queryable, nil, `count(metric)`, time.Unix(0, 0), time.Unix(300, 0), time.Second*30) + testutil.Ok(t, err) + + resp := qry.Exec(ctx) + testutil.Ok(t, resp.Err) + + responseChan <- struct{}{} + }() + } + + var ( + i = 0 + gracePeriod = storageDelay + 10*time.Millisecond + ) + for i < concurrency { + select { + case <-time.After(gracePeriod): + t.Errorf("expected query to complete within %f seconds", gracePeriod.Seconds()) + case <-responseChan: + } + i++ + } + select { + case <-responseChan: + t.Error("Expected to block on a query but did not") + case <-time.After(10 * time.Millisecond): + break + } + for i < maxQueries { + select { + case <-time.After(gracePeriod): + t.Errorf("expected query to complete within %f seconds", gracePeriod.Seconds()) + case <-responseChan: + } + i++ + } +} + +func TestQueryTimeout(t *testing.T) { + end := time.Unix(120, 0) + query := `http_requests_total{pod="nginx-1"}` + load := `load 30s + http_requests_total{pod="nginx-1"} 1+1x1 + http_requests_total{pod="nginx-2"} 1+2x1` + + opts := promql.EngineOpts{ + Timeout: 1 * time.Microsecond, + MaxSamples: math.MaxInt64, + } + + storage := promqltest.LoadedStorage(t, load) + defer storage.Close() + + newEngine := engine.New(engine.Opts{EngineOpts: opts}) + + q, err := newEngine.NewInstantQuery(context.Background(), storage, nil, query, end) + testutil.Ok(t, err) + + res := q.Exec(context.Background()) + testutil.NotOk(t, res.Err, "expected timeout error but got none") + testutil.Equals(t, context.DeadlineExceeded, res.Err) +} + +func TestMaxSamples(t *testing.T) { + t.Parallel() + + t.Run("max_samples with rate function", func(t *testing.T) { + t.Parallel() + storage := teststorage.New(t) + defer storage.Close() + + app := storage.Appender(context.Background()) + // Create 1000 series with samples every 15s for 5 minutes + for i := range 1000 { + for ts := int64(0); ts <= 300; ts += 15 { + _, err := app.Append(0, labels.FromStrings(labels.MetricName, "test_metric", "series", strconv.Itoa(i)), ts*1000, float64(ts)) + require.NoError(t, err) + } + } + require.NoError(t, app.Commit()) + + // With 1000 series and a 2m window, rate() will keep ~8 samples per series in memory + // = ~8000 samples total + query := `rate(test_metric[2m])` + start := time.Unix(120, 0) + end := time.Unix(300, 0) + step := 30 * time.Second + + t.Run("exceeds limit", func(t *testing.T) { + ng := engine.New(engine.Opts{ + EngineOpts: promql.EngineOpts{ + Timeout: 1 * time.Hour, + MaxSamples: 5000, // Lower than ~8000 expected + }, + }) + q, err := ng.NewRangeQuery(context.Background(), storage, nil, query, start, end, step) + require.NoError(t, err) + res := q.Exec(context.Background()) + require.Error(t, res.Err, "expected max_samples error") + require.Contains(t, res.Err.Error(), "query processing would load too many samples into memory") + }) + + t.Run("within limit", func(t *testing.T) { + ng := engine.New(engine.Opts{ + EngineOpts: promql.EngineOpts{ + Timeout: 1 * time.Hour, + MaxSamples: 50000, // Higher than ~8000 expected + }, + }) + q, err := ng.NewRangeQuery(context.Background(), storage, nil, query, start, end, step) + require.NoError(t, err) + res := q.Exec(context.Background()) + require.NoError(t, res.Err) + }) + }) + + t.Run("max_samples with vector selector", func(t *testing.T) { + t.Parallel() + storage := teststorage.New(t) + defer storage.Close() + + app := storage.Appender(context.Background()) + // 10000 series, each step will have 10000 samples in memory + for i := range 10000 { + for ts := int64(0); ts <= 300; ts += 30 { + _, err := app.Append(0, labels.FromStrings(labels.MetricName, "test_metric", "series", strconv.Itoa(i)), ts*1000, float64(ts)) + require.NoError(t, err) + } + } + require.NoError(t, app.Commit()) + + query := `test_metric` + start := time.Unix(0, 0) + end := time.Unix(60, 0) + step := 30 * time.Second + + ng := engine.New(engine.Opts{ + EngineOpts: promql.EngineOpts{ + Timeout: 1 * time.Hour, + MaxSamples: 5000, // Lower than 10000 series per step + }, + }) + q, err := ng.NewRangeQuery(context.Background(), storage, nil, query, start, end, step) + require.NoError(t, err) + res := q.Exec(context.Background()) + require.Error(t, res.Err) + require.Contains(t, res.Err.Error(), "query processing would load too many samples into memory") + }) + + t.Run("max_samples with subquery", func(t *testing.T) { + t.Parallel() + storage := teststorage.New(t) + defer storage.Close() + + app := storage.Appender(context.Background()) + // 1000 series with subquery that accumulates samples + for i := range 1000 { + for ts := int64(0); ts <= 600; ts += 15 { + _, err := app.Append(0, labels.FromStrings(labels.MetricName, "test_metric", "series", strconv.Itoa(i)), ts*1000, float64(ts)) + require.NoError(t, err) + } + } + require.NoError(t, app.Commit()) + + // Subquery with 2m range and 30s step = 5 steps per evaluation + // With 1000 series, that's ~5000 samples in ring buffer + query := `sum_over_time(test_metric[2m:30s])` + start := time.Unix(120, 0) + end := time.Unix(300, 0) + step := 60 * time.Second + + ng := engine.New(engine.Opts{ + EngineOpts: promql.EngineOpts{ + Timeout: 1 * time.Hour, + MaxSamples: 1000, // Lower than expected + }, + }) + q, err := ng.NewRangeQuery(context.Background(), storage, nil, query, start, end, step) + require.NoError(t, err) + res := q.Exec(context.Background()) + require.Error(t, res.Err) + require.Contains(t, res.Err.Error(), "query processing would load too many samples into memory") + }) + + t.Run("max_samples disabled by default", func(t *testing.T) { + t.Parallel() + storage := teststorage.New(t) + defer storage.Close() + + app := storage.Appender(context.Background()) + for i := range 100 { + for ts := int64(0); ts < 300; ts += 30 { + _, err := app.Append(0, labels.FromStrings(labels.MetricName, "test_metric", "series", strconv.Itoa(i)), ts*1000, float64(ts)) + require.NoError(t, err) + } + } + require.NoError(t, app.Commit()) + + query := `rate(test_metric[1m])` + start := time.Unix(0, 0) + end := time.Unix(300, 0) + step := 30 * time.Second + + ng := engine.New(engine.Opts{ + EngineOpts: promql.EngineOpts{Timeout: 1 * time.Hour}, + }) + q, err := ng.NewRangeQuery(context.Background(), storage, nil, query, start, end, step) + require.NoError(t, err) + res := q.Exec(context.Background()) + require.NoError(t, res.Err) + }) +} + +type hintRecordingQuerier struct { + storage.Querier + mux sync.Mutex + hints []*storage.SelectHints +} + +func (h *hintRecordingQuerier) Close() error { return nil } + +func (h *hintRecordingQuerier) Select(_ context.Context, sortSeries bool, hints *storage.SelectHints, matchers ...*labels.Matcher) storage.SeriesSet { + h.mux.Lock() + defer h.mux.Unlock() + h.hints = append(h.hints, hints) + return storage.EmptySeriesSet() +} + +func TestSelectHintsSetCorrectly(t *testing.T) { + for _, tc := range []struct { + query string + + // All times are in milliseconds. + start int64 + end int64 + + // TODO(bwplotka): Add support for better hints when subquerying. + expected []*storage.SelectHints + }{ + { + query: `foo`, start: 10000, + expected: []*storage.SelectHints{ + {Start: 5000 + 1, End: 10000}, + }, + }, { + query: `foo @ 15.000`, start: 10000, + expected: []*storage.SelectHints{ + {Start: 10000 + 1, End: 15000}, + }, + }, { + query: `foo @ 1.000`, start: 10000, + expected: []*storage.SelectHints{ + {Start: -4000 + 1, End: 1000}, + }, + }, { + query: `rate(foo[2m])`, start: 200000, + expected: []*storage.SelectHints{ + {Start: 80000 + 1, End: 200000, Range: 120000, Func: "rate"}, + }, + }, { + query: `rate(foo[2m] @ 180.000)`, start: 200000, + expected: []*storage.SelectHints{ + {Start: 60000 + 1, End: 180000, Range: 120000, Func: "rate"}, + }, + }, { + query: `rate(foo[2m] @ 300.000)`, start: 200000, + expected: []*storage.SelectHints{ + {Start: 180000 + 1, End: 300000, Range: 120000, Func: "rate"}, + }, + }, { + query: `rate(foo[2m] @ 60.000)`, start: 200000, + expected: []*storage.SelectHints{ + {Start: -60000 + 1, End: 60000, Range: 120000, Func: "rate"}, + }, + }, { + query: `rate(foo[2m] offset 2m)`, start: 300000, + expected: []*storage.SelectHints{ + {Start: 60000 + 1, End: 180000, Range: 120000, Func: "rate"}, + }, + }, { + query: `rate(foo[2m] @ 200.000 offset 2m)`, start: 300000, + expected: []*storage.SelectHints{ + {Start: -40000 + 1, End: 80000, Range: 120000, Func: "rate"}, + }, + }, { + query: `rate(foo[2m:1s])`, start: 300000, + expected: []*storage.SelectHints{ + {Start: 175000 + 1, End: 300000, Step: 1000, Func: "rate"}, + }, + }, { + query: `count_over_time(foo[2m:1s])`, start: 300000, + expected: []*storage.SelectHints{ + {Start: 175000 + 1, End: 300000, Func: "count_over_time", Step: 1000}, + }, + }, { + query: `count_over_time(foo[2m:1s] @ 300.000)`, start: 200000, + expected: []*storage.SelectHints{ + {Start: 175000 + 1, End: 300000, Func: "count_over_time", Step: 1000}, + }, + }, { + query: `count_over_time(foo[2m:1s] @ 200.000)`, start: 200000, + expected: []*storage.SelectHints{ + {Start: 75000 + 1, End: 200000, Func: "count_over_time", Step: 1000}, + }, + }, { + query: `count_over_time(foo[2m:1s] @ 100.000)`, start: 200000, + expected: []*storage.SelectHints{ + {Start: -25000 + 1, End: 100000, Func: "count_over_time", Step: 1000}, + }, + }, { + query: `count_over_time(foo[2m:1s] offset 10s)`, start: 300000, + expected: []*storage.SelectHints{ + {Start: 165000 + 1, End: 290000, Func: "count_over_time", Step: 1000}, + }, + }, { + query: `count_over_time((foo offset 10s)[2m:1s] offset 10s)`, start: 300000, + expected: []*storage.SelectHints{ + {Start: 155000 + 1, End: 280000, Func: "count_over_time", Step: 1000}, + }, + }, { + // When the @ is on the vector selector, the enclosing subquery parameters + // don't affect the hint ranges. + query: `count_over_time((foo @ 200.000 offset 10s)[2m:1s] offset 10s)`, start: 300000, + expected: []*storage.SelectHints{ + {Start: 185000 + 1, End: 190000, Func: "count_over_time", Step: 1000}, + }, + }, { + // When the @ is on the vector selector, the enclosing subquery parameters + // don't affect the hint ranges. + query: `count_over_time((foo @ 200.000 offset 10s)[2m:1s] @ 100.000 offset 10s)`, start: 300000, + expected: []*storage.SelectHints{ + {Start: 185000 + 1, End: 190000, Func: "count_over_time", Step: 1000}, + }, + }, { + query: `count_over_time((foo offset 10s)[2m:1s] @ 100.000 offset 10s)`, start: 300000, + expected: []*storage.SelectHints{ + {Start: -45000 + 1, End: 80000, Func: "count_over_time", Step: 1000}, + }, + }, { + query: `foo`, start: 10000, end: 20000, + expected: []*storage.SelectHints{ + {Start: 5000 + 1, End: 20000, Step: 1000}, + }, + }, { + query: `foo @ 15.000`, start: 10000, end: 20000, + expected: []*storage.SelectHints{ + {Start: 10000 + 1, End: 15000, Step: 1000}, + }, + }, { + query: `foo @ 1.000`, start: 10000, end: 20000, + expected: []*storage.SelectHints{ + {Start: -4000 + 1, End: 1000, Step: 1000}, + }, + }, { + query: `rate(foo[2m] @ 180.000)`, start: 200000, end: 500000, + expected: []*storage.SelectHints{ + {Start: 60000 + 1, End: 180000, Range: 120000, Func: "rate", Step: 1000}, + }, + }, { + query: `rate(foo[2m] @ 300.000)`, start: 200000, end: 500000, + expected: []*storage.SelectHints{ + {Start: 180000 + 1, End: 300000, Range: 120000, Func: "rate", Step: 1000}, + }, + }, { + query: `rate(foo[2m] @ 60.000)`, start: 200000, end: 500000, + expected: []*storage.SelectHints{ + {Start: -60000 + 1, End: 60000, Range: 120000, Func: "rate", Step: 1000}, + }, + }, { + query: `rate(foo[2m])`, start: 200000, end: 500000, + expected: []*storage.SelectHints{ + {Start: 80000 + 1, End: 500000, Range: 120000, Func: "rate", Step: 1000}, + }, + }, { + query: `rate(foo[2m] offset 2m)`, start: 300000, end: 500000, + expected: []*storage.SelectHints{ + {Start: 60000 + 1, End: 380000, Range: 120000, Func: "rate", Step: 1000}, + }, + }, { + query: `rate(foo[2m:1s])`, start: 300000, end: 500000, + expected: []*storage.SelectHints{ + {Start: 175000 + 1, End: 500000, Func: "rate", Step: 1000}, + }, + }, { + query: `count_over_time(foo[2m:1s])`, start: 300000, end: 500000, + expected: []*storage.SelectHints{ + {Start: 175000 + 1, End: 500000, Func: "count_over_time", Step: 1000}, + }, + }, { + query: `count_over_time(foo[2m:1s] offset 10s)`, start: 300000, end: 500000, + expected: []*storage.SelectHints{ + {Start: 165000 + 1, End: 490000, Func: "count_over_time", Step: 1000}, + }, + }, { + query: `count_over_time(foo[2m:1s] @ 300.000)`, start: 200000, end: 500000, + expected: []*storage.SelectHints{ + {Start: 175000 + 1, End: 300000, Func: "count_over_time", Step: 1000}, + }, + }, { + query: `count_over_time(foo[2m:1s] @ 200.000)`, start: 200000, end: 500000, + expected: []*storage.SelectHints{ + {Start: 75000 + 1, End: 200000, Func: "count_over_time", Step: 1000}, + }, + }, { + query: `count_over_time(foo[2m:1s] @ 100.000)`, start: 200000, end: 500000, + expected: []*storage.SelectHints{ + {Start: -25000 + 1, End: 100000, Func: "count_over_time", Step: 1000}, + }, + }, { + query: `count_over_time((foo offset 10s)[2m:1s] offset 10s)`, start: 300000, end: 500000, + expected: []*storage.SelectHints{ + {Start: 155000 + 1, End: 480000, Func: "count_over_time", Step: 1000}, + }, + }, { + // When the @ is on the vector selector, the enclosing subquery parameters + // don't affect the hint ranges. + query: `count_over_time((foo @ 200.000 offset 10s)[2m:1s] offset 10s)`, start: 300000, end: 500000, + expected: []*storage.SelectHints{ + {Start: 185000 + 1, End: 190000, Func: "count_over_time", Step: 1000}, + }, + }, { + // When the @ is on the vector selector, the enclosing subquery parameters + // don't affect the hint ranges. + query: `count_over_time((foo @ 200.000 offset 10s)[2m:1s] @ 100.000 offset 10s)`, start: 300000, end: 500000, + expected: []*storage.SelectHints{ + {Start: 185000 + 1, End: 190000, Func: "count_over_time", Step: 1000}, + }, + }, { + query: `count_over_time((foo offset 10s)[2m:1s] @ 100.000 offset 10s)`, start: 300000, end: 500000, + expected: []*storage.SelectHints{ + {Start: -45000 + 1, End: 80000, Func: "count_over_time", Step: 1000}, + }, + }, { + query: `sum by (dim1) (foo)`, start: 10000, + expected: []*storage.SelectHints{ + {Start: 5000 + 1, End: 10000, Func: "sum", By: true, Grouping: []string{"dim1"}}, + }, + }, { + query: `sum without (dim1) (foo)`, start: 10000, + expected: []*storage.SelectHints{ + {Start: 5000 + 1, End: 10000, Func: "sum", Grouping: []string{"dim1"}}, + }, + }, { + query: `sum by (dim1) (avg_over_time(foo[1s]))`, start: 10000, + expected: []*storage.SelectHints{ + {Start: 9000 + 1, End: 10000, Func: "avg_over_time", Range: 1000}, + }, + }, { + query: `sum by (dim1) (max by (dim2) (foo))`, start: 10000, + expected: []*storage.SelectHints{ + {Start: 5000 + 1, End: 10000, Func: "max", By: true, Grouping: []string{"dim2"}}, + }, + }, { + query: `max_over_time((max by (dim1) (foo))[5s:1s])`, start: 10000, + expected: []*storage.SelectHints{ + {Start: 0 + 1, End: 10000, Func: "max", By: true, Grouping: []string{"dim1"}, Step: 1000}, + }, + }, { + query: "max_over_time((sum(http_requests{group=~\"p.*\"})+max(http_requests{group=~\"c.*\"}))[20s:5s])", start: 120000, + expected: []*storage.SelectHints{ + {Start: 95000 + 1, End: 120000, Func: "sum", By: true, Step: 5000}, + {Start: 95000 + 1, End: 120000, Func: "max", By: true, Step: 5000}, + }, + }, { + query: `foo @ 50.000 + bar @ 250.000 + baz @ 900.000`, start: 100000, end: 500000, + expected: []*storage.SelectHints{ + {Start: 45000 + 1, End: 50000, Step: 1000}, + {Start: 245000 + 1, End: 250000, Step: 1000}, + {Start: 895000 + 1, End: 900000, Step: 1000}, + }, + }, { + query: `foo @ 50.000 + bar + baz @ 900.000`, start: 100000, end: 500000, + expected: []*storage.SelectHints{ + {Start: 45000 + 1, End: 50000, Step: 1000}, + {Start: 95000 + 1, End: 500000, Step: 1000}, + {Start: 895000 + 1, End: 900000, Step: 1000}, + }, + }, { + query: `rate(foo[2s] @ 50.000) + bar @ 250.000 + baz @ 900.000`, start: 100000, end: 500000, + expected: []*storage.SelectHints{ + {Start: 48000 + 1, End: 50000, Step: 1000, Func: "rate", Range: 2000}, + {Start: 245000 + 1, End: 250000, Step: 1000}, + {Start: 895000 + 1, End: 900000, Step: 1000}, + }, + }, { + query: `rate(foo[2s:1s] @ 50.000) + bar + baz`, start: 100000, end: 500000, + expected: []*storage.SelectHints{ + {Start: 43000 + 1, End: 50000, Step: 1000, Func: "rate"}, + {Start: 95000 + 1, End: 500000, Step: 1000}, + {Start: 95000 + 1, End: 500000, Step: 1000}, + }, + }, { + query: `rate(foo[2s:1s] @ 50.000) + bar + rate(baz[2m:1s] @ 900.000 offset 2m)`, start: 100000, end: 500000, + expected: []*storage.SelectHints{ + {Start: 43000 + 1, End: 50000, Step: 1000, Func: "rate"}, + {Start: 95000 + 1, End: 500000, Step: 1000}, + {Start: 655000 + 1, End: 780000, Step: 1000, Func: "rate"}, + }, + }, { // Hints are based on the inner most subquery timestamp. + query: ` +sum_over_time( +sum_over_time(sum_over_time(metric{job="1"}[1m40s])[1m40s:25s] @ 50.000)[3s:1s] @ 3000.000 +)`, start: 100000, + expected: []*storage.SelectHints{ + {Start: -150000 + 1, End: 50000, Range: 100000, Func: "sum_over_time", Step: 25000}, + }, + }, { // Hints are based on the inner most subquery timestamp. + query: ` +sum_over_time( +sum_over_time(sum_over_time(metric{job="1"}[1m40s])[1m40s:25s] @ 3000.000)[3s:1s] @ 50.000 +)`, + expected: []*storage.SelectHints{ + {Start: 2800000 + 1, End: 3000000, Range: 100000, Func: "sum_over_time", Step: 25000}, + }, + }, + } { + t.Run(tc.query, func(t *testing.T) { + t.Parallel() + opts := promql.EngineOpts{ + Logger: nil, + Reg: nil, + MaxSamples: 10, + Timeout: 10 * time.Second, + LookbackDelta: 5 * time.Second, + EnableAtModifier: true, + } + + ng := engine.New(engine.Opts{EngineOpts: opts}) + hintsRecorder := &hintRecordingQuerier{} + queryable := &storage.MockQueryable{MockQuerier: hintsRecorder} + ctx := context.Background() + + var ( + query promql.Query + err error + ) + if tc.end == 0 { + query, err = ng.NewInstantQuery(ctx, queryable, nil, tc.query, timestamp.Time(tc.start)) + } else { + query, err = ng.NewRangeQuery(ctx, queryable, nil, tc.query, timestamp.Time(tc.start), timestamp.Time(tc.end), time.Second) + } + testutil.Ok(t, err) + + res := query.Exec(context.Background()) + testutil.Ok(t, res.Err) + + // Selects are done in parallel so check that all hints are + // present, but order does not matter. + testutil.Equals(t, len(tc.expected), len(hintsRecorder.hints)) + for _, expected := range tc.expected { + contains := false + for _, hint := range hintsRecorder.hints { + if reflect.DeepEqual(expected, hint) { + contains = true + } + } + testutil.Assert(t, contains, "hints did not contain contain %#v", expected) + } + }) + } +} + +func TestQueryStats(t *testing.T) { + cases := []struct { + name string + load string + query string + start time.Time + end time.Time + step time.Duration + }{ + { + name: "nested subquery", + load: `load 15s + http_requests_total{pod="nginx-1"} 1+2x1000 + http_requests_total{pod="nginx-2"} 1+3x10`, + query: `sum_over_time(deriv(rate(http_requests_total[30s])[1m:30s])[2m:])`, + start: time.Unix(0, 0), + end: time.Unix(3600, 0), + step: time.Second * 10, + }, + { + name: "subquery", + load: `load 15s + http_requests_total{pod="nginx-1"} 1+2x200 + http_requests_total{pod="nginx-2"} 1+3x200`, + query: `max_over_time(sum(http_requests_total)[30s:15s])`, + start: time.Unix(0, 0), + end: time.Unix(1500, 0), + step: time.Second * 30, + }, + { + name: "subquery different time range", + load: `load 15s + http_requests_total{pod="nginx-1"} 1+2x200 + http_requests_total{pod="nginx-2"} 1+3x200`, + query: `max_over_time(sum(http_requests_total)[30s:15s])`, + start: time.Unix(60, 0), + end: time.Unix(1000, 0), + step: time.Second * 30, + }, + { + name: "vector selector", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x100 + http_requests_total{pod="nginx-2"} 1+2x100`, + query: `http_requests_total{pod="nginx-1"}`, + start: time.Unix(0, 0), + end: time.Unix(1800, 0), + step: time.Second * 30, + }, + { + name: "vector selector sparse", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x100 + http_requests_total{pod="nginx-2"} 1+2x20`, + query: `rate(http_requests_total{pod="nginx-2"}[10s])`, + start: time.Unix(0, 0), + end: time.Unix(1800, 0), + step: time.Second * 30, + }, + { + name: "sum", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x100 + http_requests_total{pod="nginx-2"} 1+2x100`, + query: `sum(http_requests_total)`, + start: time.Unix(0, 0), + end: time.Unix(1200, 0), + step: time.Second * 30, + }, + { + name: "sum rate", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x100 + http_requests_total{pod="nginx-2"} 1+2x100`, + query: `sum(rate(http_requests_total[1m]))`, + start: time.Unix(0, 0), + end: time.Unix(1800, 0), + step: time.Second * 30, + }, + { + name: "sum rate large window", + load: `load 2m + http_requests_total{pod="nginx-1"} 1+1x100 + http_requests_total{pod="nginx-2"} 1+2x100`, + query: `sum(rate(http_requests_total[1m]))`, + start: time.Unix(0, 0), + end: time.Unix(1800, 0), + step: time.Second * 30, + }, + { + name: "sum rate sparse", + load: `load 2m + http_requests_total{pod="nginx-1"} 1+1x5 + http_requests_total{pod="nginx-2"} 1+2x5`, + query: `sum(rate(http_requests_total[1m]))`, + start: time.Unix(0, 0), + end: time.Unix(1800, 0), + step: time.Second * 30, + }, + { + name: "label_replace", + load: `load 2m + http_requests_total{pod="nginx-1"} 1+1x5 + http_requests_total{pod="nginx-2"} 1+2x5`, + query: `label_replace(http_requests_total, "replace", "$1", "pod", "(.*)")`, + start: time.Unix(1, 0), + end: time.Unix(1800, 0), + step: time.Second * 30, + }, + { + name: "step invariant with samples", + load: `load 5m + http_requests_total{pod="nginx-1"} 1+1x5 + http_requests_total{pod="nginx-2"} 1+2x5`, + query: `sum without (__name__) (http_requests_total @ end())`, + start: time.Unix(1, 0), + end: time.Unix(600, 0), + step: time.Second * 34, + }, + { + name: "step invariant without samples", + load: `load 30s + http_requests_total{pod="nginx-1"} 1.00+1.00x15 + http_requests_total{pod="nginx-2"} 1+2.00x21`, + query: `pi()`, + start: time.UnixMilli(0), + end: time.UnixMilli(120000), + step: time.Second * 30, + }, + { + name: "fuzz subquery without enough samples", + load: `load 30s + http_requests_total{pod="nginx-1"} 1.00+1.00x15 + http_requests_total{pod="nginx-2"} 1+2.00x21`, + query: `rate({__name__="http_requests_total"} offset -6s[1h:1m] offset 1m29s)`, + start: time.UnixMilli(0), + end: time.UnixMilli(120000), + step: time.Second * 30, + }, + { + name: "native histogram sum compact", + load: `load 2m + http_request_duration_seconds{pod="nginx-1"} {{schema:0 count:3 sum:14.00 buckets:[1 2]}}+{{schema:0 count:4 buckets:[1 2 1]}}x20 + http_request_duration_seconds{pod="nginx-2"} {{schema:0 count:2 sum:14.00 buckets:[2]}}+{{schema:0 count:6 buckets:[2 2 2]}}x20`, + query: `--sum by (pod) ({__name__="http_request_duration_seconds"})`, + start: time.UnixMilli(0), + end: time.UnixMilli(2400000), + step: time.Second * 30, + }, + { + name: "native histogram rate with counter reset and step equal to window", + load: `load 30s + some_metric {{schema:0 sum:1 count:1 buckets:[1]}} {{schema:0 sum:0 count:0 buckets:[1]}} {{schema:0 sum:5 count:4 buckets:[1 2 1]}} {{schema:0 sum:1 count:1 buckets:[1]}}`, + query: `rate(some_metric[1m])`, + start: time.Unix(-60, 0), + end: time.Unix(120, 0), + step: time.Second * 30, + }, + { + name: "native histogram histogram_quantile", + load: `load 2m + http_request_duration_seconds{pod="nginx-1"} {{schema:0 count:3 sum:14.00 buckets:[1 2]}}+{{schema:0 count:4 buckets:[1 2 1]}}x20 + http_request_duration_seconds{pod="nginx-2"} {{schema:0 count:2 sum:14.00 buckets:[2]}}+{{schema:0 count:6 buckets:[2 2 2]}}x20`, + query: `histogram_quantile(0.9, {__name__="http_request_duration_seconds"})`, + start: time.UnixMilli(0), + end: time.UnixMilli(2400000), + step: time.Second * 30, + }, + { + name: "fuzz aggregation with scalar param", + load: `load 30s + http_requests_total{pod="nginx-1"} -77.00+1.00x15 + http_requests_total{pod="nginx-2"} 1+0.67x21`, + query: ` +quantile without (pod) ( + scalar({__name__="http_requests_total"} offset 2m58s), + {__name__="http_requests_total"} +)`, + start: time.UnixMilli(0), + end: time.UnixMilli(221000), + step: time.Second * 30, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + opts := promql.EngineOpts{ + Timeout: 300 * time.Second, + MaxSamples: math.MaxInt64, + EnablePerStepStats: true, + EnableAtModifier: true, + EnableNegativeOffset: true, + NoStepSubqueryIntervalFn: func(rangeMillis int64) int64 { return 30 * time.Second.Milliseconds() }, + } + qOpts := promql.NewPrometheusQueryOpts(true, 5*time.Minute) + + storage := promqltest.LoadedStorage(t, tc.load) + defer storage.Close() + + ctx := context.Background() + + oldEngine := promql.NewEngine(opts) + newEngine := engine.New(engine.Opts{EnableAnalysis: true, EngineOpts: opts}) + + // Instant query + oldQ, err := oldEngine.NewInstantQuery(ctx, storage, qOpts, tc.query, tc.end) + testutil.Ok(t, err) + oldResult := oldQ.Exec(ctx) + oldStats := oldQ.Stats() + stats.NewQueryStats(oldStats) + + newQ, err := newEngine.NewInstantQuery(ctx, storage, qOpts, tc.query, tc.end) + testutil.Ok(t, err) + newResult := newQ.Exec(ctx) + newStats := newQ.Stats() + stats.NewQueryStats(newStats) + + testutil.WithGoCmp(comparer).Equals(t, oldResult, newResult) + if oldResult.Err == nil { + testutil.WithGoCmp(samplesComparer).Equals(t, oldStats.Samples, newStats.Samples) + } + + // Range query + oldQ, err = oldEngine.NewRangeQuery(ctx, storage, qOpts, tc.query, tc.start, tc.end, tc.step) + testutil.Ok(t, err) + oldResult = oldQ.Exec(ctx) + oldStats = oldQ.Stats() + stats.NewQueryStats(oldStats) + + newQ, err = newEngine.NewRangeQuery(ctx, storage, qOpts, tc.query, tc.start, tc.end, tc.step) + testutil.Ok(t, err) + newResult = newQ.Exec(ctx) + newStats = newQ.Stats() + stats.NewQueryStats(newStats) + + testutil.WithGoCmp(comparer).Equals(t, oldResult, newResult) + if oldResult.Err == nil { + testutil.WithGoCmp(samplesComparer).Equals(t, oldStats.Samples, newStats.Samples) + } + }) + } +} + +func storageWithMockSeries(mockSeries ...*mockSeries) *storage.MockQueryable { + series := make([]storage.Series, 0, len(mockSeries)) + for _, mock := range mockSeries { + series = append(series, storage.Series(mock)) + } + return storageWithSeries(series...) +} + +func storageWithSeries(series ...storage.Series) *storage.MockQueryable { + return &storage.MockQueryable{ + MockQuerier: &storage.MockQuerier{ + SelectMockFunction: func(sortSeries bool, hints *storage.SelectHints, matchers ...*labels.Matcher) storage.SeriesSet { + result := make([]storage.Series, 0) + loopSeries: + for _, s := range series { + for _, m := range matchers { + lbl := s.Labels().Get(m.Name) + if !m.Matches(lbl) { + continue loopSeries + } + } + result = append(result, s) + } + return newTestSeriesSet(result...) + }, + }, + } +} + +type byTimestamps mockSeries + +func (b byTimestamps) Len() int { + return len(b.timestamps) +} + +func (b byTimestamps) Less(i, j int) bool { + return b.timestamps[i] < b.timestamps[j] +} + +func (b byTimestamps) Swap(i, j int) { + b.timestamps[i], b.timestamps[j] = b.timestamps[j], b.timestamps[i] + b.values[i], b.values[j] = b.values[j], b.values[i] +} + +type mockSeries struct { + labels []string + timestamps []int64 + values []float64 +} + +func newMockSeries(labels []string, timestamps []int64, values []float64) *mockSeries { + for i := range timestamps { + timestamps[i] = timestamps[i] * 1000 + } + return &mockSeries{labels: labels, timestamps: timestamps, values: values} +} + +func (m mockSeries) Labels() labels.Labels { + return labels.FromStrings(m.labels...) +} + +func (m mockSeries) Iterator(chunkenc.Iterator) chunkenc.Iterator { + return &mockIterator{ + i: -1, + timestamps: m.timestamps, + values: m.values, + } +} + +type mockIterator struct { + i int + timestamps []int64 + values []float64 +} + +func (m *mockIterator) Next() chunkenc.ValueType { + m.i++ + if m.i >= len(m.values) { + return chunkenc.ValNone + } + + return chunkenc.ValFloat +} + +func (m *mockIterator) Seek(t int64) chunkenc.ValueType { + if m.i > -1 && m.i < len(m.timestamps) { + currentTS := m.timestamps[m.i] + if currentTS >= t { + return chunkenc.ValFloat + } + } + for { + next := m.Next() + if next == chunkenc.ValNone { + return chunkenc.ValNone + } + + if m.AtT() >= t { + return next + } + } +} + +func (m *mockIterator) At() (int64, float64) { + return m.timestamps[m.i], m.values[m.i] +} + +func (m *mockIterator) AtHistogram(_ *histogram.Histogram) (int64, *histogram.Histogram) { + return 0, nil +} + +func (m *mockIterator) AtFloatHistogram(_ *histogram.FloatHistogram) (int64, *histogram.FloatHistogram) { + return 0, nil +} + +func (m *mockIterator) AtT() int64 { return m.timestamps[m.i] } + +func (m *mockIterator) Err() error { return nil } + +type slowSeriesSet struct { + empty bool + delay time.Duration +} + +func newSlowSeriesSet(delay time.Duration) *slowSeriesSet { + return &slowSeriesSet{delay: delay} +} + +func (s *slowSeriesSet) Next() bool { + if s.empty { + return false + } + s.empty = true + <-time.After(s.delay) + return true +} + +func (s slowSeriesSet) At() storage.Series { + return storage.MockSeries([]int64{0}, []float64{0}, nil) +} + +func (s slowSeriesSet) Err() error { return nil } + +func (s slowSeriesSet) Warnings() annotations.Annotations { return nil } + +type testSeriesSet struct { + i int + series []storage.Series + warns annotations.Annotations + err error +} + +func newTestSeriesSet(series ...storage.Series) storage.SeriesSet { + return &testSeriesSet{ + i: -1, + series: series, + } +} + +func newWarningsSeriesSet(warns annotations.Annotations) storage.SeriesSet { + return &testSeriesSet{ + i: -1, + warns: warns, + } +} + +func (s *testSeriesSet) Next() bool { s.i++; return s.i < len(s.series) } +func (s *testSeriesSet) At() storage.Series { return s.series[s.i] } +func (s *testSeriesSet) Err() error { return s.err } +func (s *testSeriesSet) Warnings() annotations.Annotations { return s.warns } + +type slowSeries struct{} + +func (d slowSeries) Labels() labels.Labels { return labels.FromStrings("foo", "bar") } +func (d slowSeries) Iterator(chunkenc.Iterator) chunkenc.Iterator { return &slowIterator{} } + +type slowIterator struct { + ts int64 +} + +func (d *slowIterator) AtHistogram(_ *histogram.Histogram) (int64, *histogram.Histogram) { + panic("not implemented") +} + +func (d *slowIterator) AtFloatHistogram(_ *histogram.FloatHistogram) (int64, *histogram.FloatHistogram) { + panic("not implemented") +} + +func (d *slowIterator) AtT() int64 { + return d.ts +} + +func (d *slowIterator) At() (int64, float64) { + return d.ts, 1 +} + +func (d *slowIterator) Next() chunkenc.ValueType { + <-time.After(10 * time.Millisecond) + d.ts += 30 * 1000 + return chunkenc.ValFloat +} + +func (d *slowIterator) Seek(t int64) chunkenc.ValueType { + <-time.After(10 * time.Millisecond) + d.ts = t + return chunkenc.ValFloat +} +func (d *slowIterator) Err() error { return nil } + +type mockRuntimeErr struct{} + +func (m *mockRuntimeErr) Error() string { + return "panic!" +} + +func (m *mockRuntimeErr) RuntimeError() { +} + +func TestEngineRecoversFromPanic(t *testing.T) { + t.Parallel() + + querier := &storage.MockQueryable{ + MockQuerier: &storage.MockQuerier{ + SelectMockFunction: func(sortSeries bool, hints *storage.SelectHints, matchers ...*labels.Matcher) storage.SeriesSet { + panic(runtime.Error(&mockRuntimeErr{})) + }, + }, + } + t.Run("instant", func(t *testing.T) { + newEngine := engine.New(engine.Opts{}) + ctx := context.Background() + q, err := newEngine.NewInstantQuery(ctx, querier, nil, "somequery", time.Time{}) + testutil.Ok(t, err) + + r := q.Exec(ctx) + testutil.Assert(t, r.Err.Error() == "unexpected panic: panic!") + }) + + t.Run("range", func(t *testing.T) { + newEngine := engine.New(engine.Opts{}) + ctx := context.Background() + q, err := newEngine.NewRangeQuery(ctx, querier, nil, "somequery", time.Time{}, time.Time{}, 42) + testutil.Ok(t, err) + + r := q.Exec(ctx) + testutil.Assert(t, r.Err.Error() == "unexpected panic: panic!") + }) +} + +func TestNativeHistogramRateWithNaN(t *testing.T) { + type HPoint struct { + T int64 + H *histogram.FloatHistogram + } + + testStorage := teststorage.New(t) + defer testStorage.Close() + + app := testStorage.Appender(t.Context()) + points := []HPoint{ + {T: 5574708, H: tsdbutil.GenerateTestFloatHistogram(1)}, + {T: 5604708, H: tsdbutil.GenerateTestFloatHistogram(2)}, + {T: 5634708, H: tsdbutil.GenerateTestFloatHistogram(3)}, + + {T: 6146221, H: &histogram.FloatHistogram{Sum: math.NaN()}}, + {T: 6176221, H: tsdbutil.GenerateTestFloatHistogram(1)}, + {T: 6206221, H: tsdbutil.GenerateTestFloatHistogram(1)}, + {T: 6236221, H: tsdbutil.GenerateTestFloatHistogram(1)}, + } + for _, point := range points { + _, err := app.AppendHistogram(0, labels.FromStrings(labels.MetricName, "test_metric"), point.T, nil, point.H) + require.NoError(t, err) + } + require.NoError(t, app.Commit()) + + var ( + opts = engine.Opts{ + EngineOpts: promql.EngineOpts{ + Timeout: 1 * time.Hour, + MaxSamples: 1e16, + EnableNegativeOffset: true, + EnableAtModifier: true, + }, + } + start = time.UnixMilli(6146221) + end = time.UnixMilli(6236221) + + step = 60 * time.Second + ) + execQuery := func(ng promql.QueryEngine) *promql.Result { + qry, err := ng.NewRangeQuery(context.TODO(), testStorage, nil, "histogram_count(rate(test_metric[10m]))", start, end, step) + require.NoError(t, err) + return qry.Exec(context.Background()) + } + + promResult := execQuery(promql.NewEngine(opts.EngineOpts)) + newResult := execQuery(engine.New(opts)) + testutil.WithGoCmp(comparer).Equals(t, promResult, newResult) +} + +type histogramTestCase struct { + name string + query string + start time.Time + wantEmptyForMixedTypes bool +} + +type histogramGeneratorFunc func(app storage.Appender, numSeries int, withMixedTypes bool) error + +func TestNativeHistograms(t *testing.T) { + t.Parallel() + opts := promql.EngineOpts{ + Timeout: 1 * time.Hour, + MaxSamples: 1e16, + EnableNegativeOffset: true, + EnableAtModifier: true, + } + + cases := []histogramTestCase{ + { + name: "count_over_time() with different start time", + query: `count_over_time(native_histogram_series[1m15s])`, + start: time.Unix(400, 0), + }, + { + name: "irate()", + query: `irate(native_histogram_series[1m])`, + }, + { + name: "rate()", + query: `rate(native_histogram_series[1m])`, + }, + { + name: "increase()", + query: `increase(native_histogram_series[1m])`, + }, + { + name: "delta()", + query: `delta(native_histogram_series[1m])`, + }, + { + name: "sum()", + query: `sum(native_histogram_series)`, + wantEmptyForMixedTypes: true, + }, + { + name: "sum by (foo)", + query: `sum by (foo) (native_histogram_series)`, + wantEmptyForMixedTypes: true, + }, + { + name: "avg()", + query: `avg(native_histogram_series)`, + wantEmptyForMixedTypes: true, + }, + { + name: "avg by (foo)", + query: `avg by (foo) (native_histogram_series)`, + wantEmptyForMixedTypes: true, + }, + { + name: "count", + query: `count(native_histogram_series)`, + }, + { + name: "count by (foo)", + query: `count by (foo) (native_histogram_series)`, + }, + { + name: "max", + query: `max(native_histogram_series)`, + }, + { + name: "max by (foo)", + query: `max by (foo) (native_histogram_series)`, + }, + { + name: "min", + query: `min(native_histogram_series)`, + }, + { + name: "min by (foo)", + query: `min by (foo) (native_histogram_series)`, + }, + { + name: "absent", + query: `absent(native_histogram_series)`, + }, + { + name: "histogram_sum", + query: `histogram_sum(native_histogram_series)`, + }, + { + name: "histogram_count", + query: `histogram_count(native_histogram_series)`, + }, + { + name: "histogram_avg", + query: `histogram_avg(native_histogram_series)`, + }, + { + name: "histogram_count of histogram product", + query: `histogram_count(native_histogram_series * native_histogram_series)`, + }, + { + name: "histogram_sum / histogram_count", + query: `histogram_sum(native_histogram_series) / histogram_count(native_histogram_series)`, + }, + { + name: "histogram_sum over histogram_quantile", + query: `histogram_sum(scalar(histogram_quantile(1, sum(native_histogram_series))) * native_histogram_series)`, + }, + { + name: "histogram_sum over histogram_fraction", + query: ` +histogram_sum( + scalar(histogram_fraction(-Inf, +Inf, sum(native_histogram_series))) * native_histogram_series +)`, + }, + { + name: "histogram_quantile", + query: `histogram_quantile(0.7, native_histogram_series)`, + }, + { + // Test strange query with a mix of histogram functions. + name: "histogram_quantile(histogram_sum)", + query: `histogram_quantile(0.7, histogram_sum(native_histogram_series))`, + }, + { + name: "histogram_count * histogram aggregation", + query: `scalar(histogram_count(sum(native_histogram_series))) * sum(native_histogram_series)`, + }, + { + name: "histogram_fraction", + query: `histogram_fraction(0, 0.2, native_histogram_series)`, + }, + { + name: "histogram_stdvar", + query: `histogram_stdvar(native_histogram_series)`, + }, + { + name: "histogram_stddev", + query: `histogram_stddev(native_histogram_series)`, + }, + { + name: "lhs multiplication", + query: `native_histogram_series * 3`, + }, + { + name: "rhs multiplication", + query: `3 * native_histogram_series`, + }, + { + name: "lhs division", + query: `native_histogram_series / 2`, + }, + { + name: "subqueries", + query: `increase(rate(native_histogram_series[2m])[2m:15s])`, + }, + { + name: "Binary OR", + query: ` + native_histogram_series +or + (histogram_quantile(0.7, native_histogram_series) or rate(native_histogram_series[2m]))`, + }, + { + name: "Mixed Binary OR", + query: `sum(native_histogram_series) or native_histogram_series`, // sum will be a single float value, float series on lhs of 'or' + }, + { + name: "Binary AND", + query: ` + (rate(native_histogram_series[2m]) and histogram_quantile(0.7, native_histogram_series)) +and + native_histogram_series`, + }, + { + name: "Mixed Binary AND", + query: `native_histogram_series and count(native_histogram_series)`, // count will be a single float value, float series on 'rhs' of 'and' + }, + { + name: "many-to-many join Unless", + query: `sum without (foo) (native_histogram_series) unless native_histogram_series / 2`, + }, + { + name: "Mixed many-to-many join Unless", + query: `native_histogram_series * 3 unless avg(native_histogram_series)`, + }, + { + name: "Limitk aggregation", + query: `limitk(2, native_histogram_series)`, + }, + { + name: "limitk by", + query: `limitk(2, native_histogram_series) by (foo) and native_histogram_series`, + }, + { + name: "Limit_ratio aggregation", + query: `limit_ratio(0.4, native_histogram_series)`, + }, + { + name: "limit_ratio by", + query: `limit_ratio(0.33, native_histogram_series) by (foo) or native_histogram_series`, + }, + } + + defer pprof.StopCPUProfile() + t.Run("integer_histograms", func(t *testing.T) { + t.Parallel() + testNativeHistograms(t, cases, opts, generateNativeHistogramSeries) + }) + t.Run("float_histograms", func(t *testing.T) { + t.Parallel() + testNativeHistograms(t, cases, opts, generateFloatHistogramSeries) + }) +} + +func testNativeHistograms(t *testing.T, cases []histogramTestCase, opts promql.EngineOpts, generateHistograms histogramGeneratorFunc) { + numHistograms := 10 + mixedTypesOpts := []bool{false, true} + var ( + queryStart = time.Unix(50, 0) + queryEnd = time.Unix(600, 0) + queryStep = 30 * time.Second + ) + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + for _, withMixedTypes := range mixedTypesOpts { + t.Run(fmt.Sprintf("mixedTypes=%t", withMixedTypes), func(t *testing.T) { + storage := teststorage.New(t) + defer storage.Close() + + app := storage.Appender(context.TODO()) + err := generateHistograms(app, numHistograms, withMixedTypes) + testutil.Ok(t, err) + testutil.Ok(t, app.Commit()) + + promEngine := promql.NewEngine(opts) + thanosEngine := engine.New(engine.Opts{ + EngineOpts: opts, + LogicalOptimizers: logicalplan.AllOptimizers, + }) + + t.Run("instant", func(t *testing.T) { + ctx := context.Background() + q1, err := thanosEngine.NewInstantQuery(ctx, storage, nil, tc.query, time.Unix(50, 0)) + testutil.Ok(t, err) + newResult := q1.Exec(ctx) + testutil.Ok(t, newResult.Err) + + q2, err := promEngine.NewInstantQuery(ctx, storage, nil, tc.query, time.Unix(50, 0)) + testutil.Ok(t, err) + promResult := q2.Exec(ctx) + testutil.Ok(t, promResult.Err) + promVector, err := promResult.Vector() + testutil.Ok(t, err) + + // Make sure we're not getting back empty results. + if withMixedTypes && tc.wantEmptyForMixedTypes { + testutil.Assert(t, len(promVector) == 0) + testutil.Equals(t, len(promResult.Warnings), len(newResult.Warnings)) + } + + testutil.WithGoCmp(comparer).Equals(t, promResult, newResult, queryExplanation(q1)) + }) + + t.Run("range", func(t *testing.T) { + if tc.start == (time.Time{}) { + tc.start = queryStart + } + ctx := context.Background() + q1, err := thanosEngine.NewRangeQuery(ctx, storage, nil, tc.query, tc.start, queryEnd, queryStep) + testutil.Ok(t, err) + newResult := q1.Exec(ctx) + testutil.Ok(t, newResult.Err) + + q2, err := promEngine.NewRangeQuery(ctx, storage, nil, tc.query, tc.start, queryEnd, queryStep) + testutil.Ok(t, err) + promResult := q2.Exec(ctx) + testutil.Ok(t, promResult.Err) + promMatrix, err := promResult.Matrix() + testutil.Ok(t, err) + + // Make sure we're not getting back empty results. + if withMixedTypes && tc.wantEmptyForMixedTypes { + testutil.Assert(t, len(promMatrix) == 0) + testutil.Equals(t, len(promResult.Warnings), len(newResult.Warnings)) + testutil.Equals(t, "PromQL warning: encountered a mix of histograms and floats for aggregation", newResult.Warnings.AsErrors()[0].Error()) + } + testutil.WithGoCmp(comparer).Equals(t, promResult, newResult, queryExplanation(q1)) + }) + }) + } + }) + } +} + +func generateNativeHistogramSeries(app storage.Appender, numSeries int, withMixedTypes bool) error { + commonLabels := []string{labels.MetricName, "native_histogram_series", "foo", "bar"} + series := make([][]*histogram.Histogram, numSeries) + for i := range series { + series[i] = tsdbutil.GenerateTestHistograms(2000) + } + higherSchemaHist := &histogram.Histogram{ + Schema: 3, + PositiveSpans: []histogram.Span{ + {Offset: -5, Length: 2}, // -5 -4 + {Offset: 2, Length: 3}, // -1 0 1 + {Offset: 2, Length: 2}, // 4 5 + }, + PositiveBuckets: []int64{1, 2, -2, 1, -1, 0, 3}, + Count: 13, + } + + for sid, histograms := range series { + lbls := append(commonLabels, "h", strconv.Itoa(sid)) + for i := range histograms { + ts := time.Unix(int64(i*15), 0).UnixMilli() + if i == 0 { + // Inject a histogram with a higher schema. + // Regression test for: + // * https://github.com/thanos-io/promql-engine/pull/182 + // * https://github.com/thanos-io/promql-engine/pull/183. + if _, err := app.AppendHistogram(0, labels.FromStrings(lbls...), ts, higherSchemaHist, nil); err != nil { + return err + } + } + if _, err := app.AppendHistogram(0, labels.FromStrings(lbls...), ts, histograms[i], nil); err != nil { + return err + } + if withMixedTypes { + if _, err := app.Append(0, labels.FromStrings(append(lbls, "classic", "1", "le", "1")...), ts, float64(i)); err != nil { + return err + } + if _, err := app.Append(0, labels.FromStrings(append(lbls, "classic", "1", "le", "+Inf")...), ts, float64(i*2)); err != nil { + return err + } + } + } + } + + return nil +} + +func generateFloatHistogramSeries(app storage.Appender, numSeries int, withMixedTypes bool) error { + lbls := []string{labels.MetricName, "native_histogram_series", "foo", "bar"} + h1 := tsdbutil.GenerateTestFloatHistograms(numSeries) + h2 := tsdbutil.GenerateTestFloatHistograms(numSeries) + for i := range h1 { + ts := time.Unix(int64(i*15), 0).UnixMilli() + if withMixedTypes { + if _, err := app.Append(0, labels.FromStrings(append(lbls, "le", "1")...), ts, float64(i)); err != nil { + return err + } + if _, err := app.Append(0, labels.FromStrings(append(lbls, "le", "+Inf")...), ts, float64(i*2)); err != nil { + return err + } + } + if _, err := app.AppendHistogram(0, labels.FromStrings(append(lbls, "h", "1")...), ts, nil, h1[i]); err != nil { + return err + } + if _, err := app.AppendHistogram(0, labels.FromStrings(append(lbls, "h", "2")...), ts, nil, h2[i]); err != nil { + return err + } + } + return nil +} + +func TestMixedNativeHistogramTypes(t *testing.T) { + t.Parallel() + histograms := tsdbutil.GenerateTestHistograms(2) + + storage := teststorage.New(t) + defer storage.Close() + + lbls := []string{labels.MetricName, "native_histogram_series"} + + app := storage.Appender(context.TODO()) + _, err := app.AppendHistogram(0, labels.FromStrings(lbls...), 0, nil, histograms[0].ToFloat(nil)) + testutil.Ok(t, err) + testutil.Ok(t, app.Commit()) + + app = storage.Appender(context.TODO()) + _, err = app.AppendHistogram(0, labels.FromStrings(lbls...), 30_000, histograms[1], nil) + testutil.Ok(t, err) + testutil.Ok(t, app.Commit()) + + opts := promql.EngineOpts{ + Timeout: 1 * time.Hour, + MaxSamples: 1e10, + EnableNegativeOffset: true, + EnableAtModifier: true, + } + + engine := engine.New(engine.Opts{ + EngineOpts: opts, + LogicalOptimizers: logicalplan.AllOptimizers, + }) + + ctx := context.Background() + + t.Run("vector_select", func(t *testing.T) { + qry, err := engine.NewInstantQuery(ctx, storage, nil, "sum(native_histogram_series)", time.Unix(30, 0)) + testutil.Ok(t, err) + res := qry.Exec(context.Background()) + testutil.Ok(t, res.Err) + actual, err := res.Vector() + testutil.Ok(t, err) + + testutil.Equals(t, 1, len(actual), "expected vector with 1 element") + expected := histograms[1].ToFloat(nil) + expected.CounterResetHint = histogram.UnknownCounterReset + testutil.Equals(t, expected, actual[0].H) + }) + + t.Run("matrix_select", func(t *testing.T) { + qry, err := engine.NewRangeQuery(ctx, storage, nil, "rate(native_histogram_series[1m1s])", time.Unix(0, 0), time.Unix(60, 0), 60*time.Second) + testutil.Ok(t, err) + res := qry.Exec(context.Background()) + testutil.Ok(t, res.Err) + actual, err := res.Matrix() + testutil.Ok(t, err) + + testutil.Equals(t, 1, len(actual), "expected 1 series") + testutil.Equals(t, 1, len(actual[0].Histograms), "expected 1 point") + + diff, _, _, err := histograms[1].ToFloat(nil).Sub(histograms[0].ToFloat(nil)) + testutil.Ok(t, err) + expected := diff.Mul(1 / float64(30)) + expected.CounterResetHint = histogram.GaugeType + testutil.Equals(t, expected, actual[0].Histograms[0].H) + }) +} + +type seriesByLabels []promql.Series + +func (b seriesByLabels) Len() int { return len(b) } +func (b seriesByLabels) Swap(i, j int) { b[i], b[j] = b[j], b[i] } +func (b seriesByLabels) Less(i, j int) bool { return labels.Compare(b[i].Metric, b[j].Metric) < 0 } + +type samplesByLabels []promql.Sample + +func (b samplesByLabels) Len() int { return len(b) } +func (b samplesByLabels) Swap(i, j int) { b[i], b[j] = b[j], b[i] } +func (b samplesByLabels) Less(i, j int) bool { return labels.Compare(b[i].Metric, b[j].Metric) < 0 } + +const epsilon = 1e-6 +const fraction = 1e-10 + +func floatsMatch(f1, f2 []float64) bool { + if len(f1) != len(f2) { + return false + } + for i, f := range f1 { + if !cmp.Equal(f, f2[i], cmpopts.EquateNaNs(), cmpopts.EquateApprox(fraction, epsilon)) { + return false + } + } + return true +} + +// spansMatch returns true if both spans represent the same bucket layout +// after combining zero length spans with the next non-zero length span. +// Copied from: https://github.com/prometheus/prometheus/blob/3d245e31d31774f62ff18c36039315fa55fe252c/model/histogram/histogram.go#L287 +func spansMatch(s1, s2 []histogram.Span) bool { + if len(s1) == 0 && len(s2) == 0 { + return true + } + + s1idx, s2idx := 0, 0 + for { + if s1idx >= len(s1) { + return allEmptySpans(s2[s2idx:]) + } + if s2idx >= len(s2) { + return allEmptySpans(s1[s1idx:]) + } + + currS1, currS2 := s1[s1idx], s2[s2idx] + s1idx++ + s2idx++ + if currS1.Length == 0 { + // This span is zero length, so we add consecutive such spans + // until we find a non-zero span. + for ; s1idx < len(s1) && s1[s1idx].Length == 0; s1idx++ { + currS1.Offset += s1[s1idx].Offset + } + if s1idx < len(s1) { + currS1.Offset += s1[s1idx].Offset + currS1.Length = s1[s1idx].Length + s1idx++ + } + } + if currS2.Length == 0 { + // This span is zero length, so we add consecutive such spans + // until we find a non-zero span. + for ; s2idx < len(s2) && s2[s2idx].Length == 0; s2idx++ { + currS2.Offset += s2[s2idx].Offset + } + if s2idx < len(s2) { + currS2.Offset += s2[s2idx].Offset + currS2.Length = s2[s2idx].Length + s2idx++ + } + } + + if currS1.Length == 0 && currS2.Length == 0 { + // The last spans of both set are zero length. Previous spans match. + return true + } + + if currS1.Offset != currS2.Offset || currS1.Length != currS2.Length { + return false + } + } +} + +func allEmptySpans(s []histogram.Span) bool { + for _, ss := range s { + if ss.Length > 0 { + return false + } + } + return true +} + +var ( + // comparer should be used to compare promql results between engines. + comparer = cmp.Comparer(func(x, y *promql.Result) bool { + compareFloats := func(l, r float64) bool { + return cmp.Equal(l, r, cmpopts.EquateNaNs(), cmpopts.EquateApprox(fraction, epsilon)) + } + compareHistograms := func(l, r *histogram.FloatHistogram) bool { + if l == nil && r == nil { + return true + } + + if l == nil && r != nil { + return false + } + + // Copied from https://github.com/prometheus/prometheus/blob/3d245e31d31774f62ff18c36039315fa55fe252c/model/histogram/float_histogram.go#L471 + // and extended to use approx comparison instead of exact match. + if l.Schema != r.Schema || !compareFloats(l.Count, r.Count) || !compareFloats(l.Sum, r.Sum) { + return false + } + + if l.UsesCustomBuckets() { + if !floatsMatch(l.CustomValues, r.CustomValues) { + return false + } + } + + if l.ZeroThreshold != r.ZeroThreshold || !compareFloats(l.ZeroCount, r.ZeroCount) { + return false + } + + if !spansMatch(l.NegativeSpans, r.NegativeSpans) { + return false + } + + if !floatsMatch(l.NegativeBuckets, r.NegativeBuckets) { + return false + } + + if !spansMatch(l.PositiveSpans, r.PositiveSpans) { + return false + } + + if !floatsMatch(l.PositiveBuckets, r.PositiveBuckets) { + return false + } + + return true + } + compareAnnotations := func(l, r annotations.Annotations) bool { + // TODO: discard promql annotations for now, once we support them we should add them back + discardPromqlAnnotations := func(k string, _ error) bool { + hasInfoPrefix := strings.HasPrefix(k, annotations.PromQLInfo.Error()) + hasWarnPrefix := strings.HasPrefix(k, annotations.PromQLWarning.Error()) + return hasInfoPrefix || hasWarnPrefix + } + maps.DeleteFunc(l, discardPromqlAnnotations) + maps.DeleteFunc(r, discardPromqlAnnotations) + + if len(l) != len(r) { + return false + } + for k, v := range l { + if !cmp.Equal(r[k], v) { + return false + } + } + for k, v := range r { + if !cmp.Equal(l[k], v) { + return false + } + } + return true + } + compareValueMetrics := func(l, r labels.Labels) (valueMetric bool, equals bool) { + // For count_value() float values embedded in the labels should be extracted out and compared separately from other labels. + lLabels := l.Copy() + rLabels := r.Copy() + var ( + lVal, rVal string + lFloat, rFloat float64 + err error + ) + + if lVal = lLabels.Get("value"); lVal == "" { + return false, false + } + + if rVal = rLabels.Get("value"); rVal == "" { + return false, false + } + + if lFloat, err = strconv.ParseFloat(lVal, 64); err != nil { + return false, false + } + if rFloat, err = strconv.ParseFloat(rVal, 64); err != nil { + return false, false + } + + // Exclude the value label in comparison. + lLabels = lLabels.MatchLabels(false, "value") + rLabels = rLabels.MatchLabels(false, "value") + + if !labels.Equal(lLabels, rLabels) { + return false, false + } + + return true, compareFloats(lFloat, rFloat) + } + compareMetrics := func(l, r labels.Labels) bool { + if valueMetric, equals := compareValueMetrics(l, r); valueMetric { + return equals + } + return l.Hash() == r.Hash() + } + + compareErrors := func(l, r error) (stop bool, result bool) { + if l == nil && r == nil { + return false, true + } + // If both have errors, consider them equal - error messages may differ + // between engines (e.g., remote exec wrapper, different series ordering) + // but what matters is that both produced an error. + if l != nil && r != nil { + return true, true + } + err := l + if err == nil { + err = r + } + // Thanos engine handles duplicate label check differently than Prometheus engine. + return true, err.Error() == extlabels.ErrDuplicateLabelSet.Error() + } + + if stop, result := compareErrors(x.Err, y.Err); stop { + return result + } + + if !compareAnnotations(x.Warnings, y.Warnings) { + return false + } + + vx, xvec := x.Value.(promql.Vector) + vy, yvec := y.Value.(promql.Vector) + + if xvec && yvec { + if len(vx) != len(vy) { + return false + } + + // Sort vector before comparing. + sort.Sort(samplesByLabels(vx)) + sort.Sort(samplesByLabels(vy)) + + for i := range vx { + if !compareMetrics(vx[i].Metric, vy[i].Metric) { + return false + } + if vx[i].T != vy[i].T { + return false + } + if !compareFloats(vx[i].F, vy[i].F) { + return false + } + if !compareHistograms(vx[i].H, vy[i].H) { + return false + } + } + return true + } + + mx, xmat := x.Value.(promql.Matrix) + my, ymat := y.Value.(promql.Matrix) + + if xmat && ymat { + if len(mx) != len(my) { + return false + } + // Sort matrix before comparing. + sort.Sort(seriesByLabels(mx)) + sort.Sort(seriesByLabels(my)) + for i := range mx { + mxs := mx[i] + mys := my[i] + + if !compareMetrics(mxs.Metric, mys.Metric) { + return false + } + + xps := mxs.Floats + yps := mys.Floats + + if len(xps) != len(yps) { + return false + } + for j := range xps { + if xps[j].T != yps[j].T { + return false + } + if !compareFloats(xps[j].F, yps[j].F) { + return false + } + } + xph := mxs.Histograms + yph := mys.Histograms + + if len(xph) != len(yph) { + return false + } + for j := range xph { + if xph[j].T != yph[j].T { + return false + } + if !compareHistograms(xph[j].H, yph[j].H) { + return false + } + } + } + return true + } + + sx, xscalar := x.Value.(promql.Scalar) + sy, yscalar := y.Value.(promql.Scalar) + if xscalar && yscalar { + if sx.T != sy.T { + return false + } + return compareFloats(sx.V, sy.V) + } + return false + }) + + samplesComparer = cmp.Comparer(func(x, y *stats.QuerySamples) bool { + if x == nil && y == nil { + return true + } + if x.TotalSamples != y.TotalSamples { + return false + } + + if !cmp.Equal(x.TotalSamplesPerStep, y.TotalSamplesPerStep) { + return false + } + + if !cmp.Equal(x.TotalSamplesPerStepMap(), y.TotalSamplesPerStepMap()) { + return false + } + return true + }) +) + +func queryExplanation(q promql.Query) string { + eq, ok := q.(engine.ExplainableQuery) + if !ok { + return "" + } + + var explain func(w io.Writer, n engine.ExplainOutputNode, indent, indentNext string) + + explain = func(w io.Writer, n engine.ExplainOutputNode, indent, indentNext string) { + next := n.Children + me := n.OperatorName + + _, _ = w.Write([]byte(indent)) + _, _ = w.Write([]byte(me)) + if len(next) == 0 { + _, _ = w.Write([]byte("\n")) + return + } + + if me == "[*CancellableOperator]" { + _, _ = w.Write([]byte(": ")) + explain(w, next[0], "", indentNext) + return + } + _, _ = w.Write([]byte(":\n")) + + for i, n := range next { + if i == len(next)-1 { + explain(w, n, indentNext+"└──", indentNext+" ") + } else { + explain(w, n, indentNext+"├──", indentNext+"│ ") + } + } + } + + var b bytes.Buffer + explain(&b, *eq.Explain(), "", "") + + return fmt.Sprintf("Query: %s\nExplanation:\n%s\n", q.String(), b.String()) +} + +// Adapted from: https://github.com/prometheus/prometheus/blob/906f6a33b60cec2596018ac8cc97ac41b16b06b7/promql/promqltest/testdata/functions.test#L814 +func TestDoubleExponentialSmoothing(t *testing.T) { + t.Parallel() + + const ( + testTimeout = 1 * time.Hour + testMaxSamples = math.MaxInt64 + testQueryStart = 0 + testQueryEnd = 3600 + testQueryStep = 30 + ) + + defaultStart := time.Unix(testQueryStart, 0) + defaultEnd := time.Unix(testQueryEnd, 0) + defaultStep := testQueryStep * time.Second + + cases := []struct { + name string + + load string + query string + + start time.Time + end time.Time + step time.Duration + }{ + { + name: "double exponential smoothing basic", + load: `load 30s + http_requests_total{pod="nginx-1"} 1+1x15 + http_requests_total{pod="nginx-2"} 1+2x18`, + query: `double_exponential_smoothing(http_requests_total[5m], 0.1, 0.1)`, + }, + { + name: "double exponential smoothing with positive trend", + load: `load 10s + http_requests{job="api-server", instance="0", group="production"} 0+10x1000 100+30x1000 + http_requests{job="api-server", instance="1", group="production"} 0+20x1000 200+30x1000`, + query: `double_exponential_smoothing(http_requests[5m], 0.01, 0.1)`, + }, + { + name: "double exponential smoothing with negative trend", + load: `load 10s + http_requests{job="api-server", instance="0", group="production"} 8000-10x1000 + http_requests{job="api-server", instance="1", group="production"} 0-20x1000`, + query: `double_exponential_smoothing(http_requests[5m], 0.01, 0.1)`, + }, + { + name: "double exponential smoothing with mixed histogram data", + load: `load 30s + http_requests_mix{job="api-server", instance="0"} 0+10x1000 100+30x1000 {{schema:0 count:1 sum:2}}x1000 + http_requests_mix{job="api-server", instance="1"} 0+20x1000 200+30x1000 {{schema:0 count:1 sum:2}}x1000`, + query: `double_exponential_smoothing(http_requests_mix[5m], 0.01, 0.1)`, + }, + { + name: "double exponential smoothing with pure histogram data", + load: `load 30s + http_requests_histogram{job="api-server", instance="1"} {{schema:0 count:1 sum:2}}x1000`, + query: `double_exponential_smoothing(http_requests_histogram[5m], 0.01, 0.1)`, + }, + } + + for _, tcase := range cases { + t.Run(tcase.name, func(t *testing.T) { + t.Parallel() + + storage := promqltest.LoadedStorage(t, tcase.load) + defer storage.Close() + + opts := promql.EngineOpts{ + Timeout: testTimeout, + MaxSamples: testMaxSamples, + EnableNegativeOffset: true, + EnableAtModifier: true, + } + + start := defaultStart + if !tcase.start.IsZero() { + start = tcase.start + } + end := defaultEnd + if !tcase.end.IsZero() { + end = tcase.end + } + step := defaultStep + if tcase.step != 0 { + step = tcase.step + } + + ctx := context.Background() + oldEngine := promql.NewEngine(opts) + q1, err := oldEngine.NewRangeQuery(ctx, storage, nil, tcase.query, start, end, step) + testutil.Ok(t, errors.Wrap(err, "create old engine range query")) + oldResult := q1.Exec(ctx) + + newEngine := engine.New(engine.Opts{EngineOpts: opts}) + q2, err := newEngine.NewRangeQuery(ctx, storage, nil, tcase.query, start, end, step) + testutil.Ok(t, errors.Wrap(err, "create new engine range query")) + newResult := q2.Exec(ctx) + + testutil.WithGoCmp(comparer).Equals(t, oldResult, newResult, queryExplanation(q2)) + }) + } +} diff --git a/internal/promql-engine/engine/enginefuzz_test.go b/internal/promql-engine/engine/enginefuzz_test.go new file mode 100644 index 00000000000..3557612daf5 --- /dev/null +++ b/internal/promql-engine/engine/enginefuzz_test.go @@ -0,0 +1,785 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package engine_test + +import ( + "context" + "fmt" + "math" + "math/rand" + "slices" + "strings" + "testing" + "time" + + "github.com/thanos-io/promql-engine/api" + "github.com/thanos-io/promql-engine/engine" + "github.com/thanos-io/promql-engine/logicalplan" + + "github.com/cortexproject/promqlsmith" + "github.com/efficientgo/core/errors" + "github.com/efficientgo/core/testutil" + "github.com/google/go-cmp/cmp" + "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/promql" + "github.com/prometheus/prometheus/promql/parser" + "github.com/prometheus/prometheus/promql/promqltest" + "github.com/prometheus/prometheus/storage" + "github.com/prometheus/prometheus/util/stats" + "github.com/stretchr/testify/require" +) + +const testRuns = 100 + +type testCase struct { + query string + loads []string + oldRes, newRes *promql.Result + oldStats, newStats *stats.Statistics + start, end time.Time + interval time.Duration + validateSamples bool +} + +type testType int + +const ( + testTypeFloat testType = iota // 0 + testTypeNativeHistogram // 1 +) + +// shouldValidateSamples checks if the samples can be compared for the expr. +// For certain known cases, Thanos engine returns less samples than Prometheus engine due to optimizations. +func shouldValidateSamples(expr parser.Expr) bool { + valid := true + + parser.Inspect(expr, func(node parser.Node, path []parser.Node) error { + switch n := node.(type) { + case *parser.VectorSelector: + if n.Timestamp != nil || n.StartOrEnd != 0 { + // The Thanos engine's step invariant operator caches the result of the + // first evaluation and replays it for subsequent steps without re-counting + // samples. This leads to fewer total samples compared to Prometheus which + // counts samples at every step. + valid = false + return errors.New("error") + } + case *parser.Call: + switch n.Func.Name { + case "scalar": + // Optimized to step_invariant in Thanos engine. + valid = false + return errors.New("error") + case "histogram_count", "histogram_sum", "histogram_avg": + // Optimized using DetectHistogramStatsOptimizer and will return smaller samples than Prometheus engine. + valid = false + return errors.New("error") + } + } + return nil + }) + return valid +} + +// validateExpr checks if the given expression is valid for fuzz tests. +// For certain known cases Thanos engine results do not match with Prometheus engine. +func validateExpr(expr parser.Expr, testType testType) bool { + expr, _ = promql.PreprocessExpr(expr, time.Unix(0, 0), time.Unix(0, 0), 0) + valid := true + + parser.Inspect(expr, func(node parser.Node, path []parser.Node) error { + switch n := node.(type) { + case *parser.AggregateExpr: + if n.Op == parser.COUNT_VALUES { + // count_values converts float values to string labels. Tiny floating point + // precision differences between engines (e.g. 61.24999999999997 vs 61.24999999999998) + // produce different label values, causing result mismatches. + valid = false + return errors.New("error") + } + case *parser.Call: + switch n.Func.Name { + case "sort", "sort_desc", "sort_by_label", "sort_by_label_desc": + if testType == testTypeNativeHistogram { + // Prometheus engine filters out native histograms in nested sort(). + // Thanos engine implements sorting only at the presentation time and ignores nested sort(). + // See: https://github.com/thanos-io/promql-engine/pull/595 + valid = false + return errors.New("error") + } + case "predict_linear": + switch t := n.Args[0].(type) { + case *parser.StepInvariantExpr: + // Thanos engine cannot correctly handle a MatrixSelector wrapped by StepInvariant. + // eg: predict_linear({__name__="http_request_duration_seconds"}[5m] @ end(), 0.5) + // See: https://github.com/thanos-io/promql-engine/pull/527 + if _, ok := t.Expr.(*parser.MatrixSelector); ok { + valid = false + return errors.New("error") + } + } + case "timestamp": + if testType == testTypeNativeHistogram { + // TODO(johrry): Remove after merging https://github.com/thanos-io/promql-engine/pull/598 + valid = false + return errors.New("error") + } + } + } + return nil + }) + return valid +} + +func FuzzEnginePromQLSmithRangeQuery(f *testing.F) { + f.Add(int64(0), uint32(0), uint32(120), uint32(30), 1.0, 1.0, 1.0, 2.0, 30) + + f.Fuzz(func(t *testing.T, seed int64, startTS, endTS, intervalSeconds uint32, initialVal1, initialVal2, inc1, inc2 float64, stepRange int) { + if math.IsNaN(initialVal1) || math.IsNaN(initialVal2) || math.IsNaN(inc1) || math.IsNaN(inc2) { + return + } + if math.IsInf(initialVal1, 0) || math.IsInf(initialVal2, 0) || math.IsInf(inc1, 0) || math.IsInf(inc2, 0) { + return + } + if inc1 < 0 || inc2 < 0 || stepRange <= 0 || intervalSeconds <= 0 || endTS < startTS { + return + } + rnd := rand.New(rand.NewSource(seed)) + + load := fmt.Sprintf(`load 30s + http_requests_total{pod="nginx-1"} %.2f+%.2fx15 + http_requests_total{pod="nginx-2"} %2.f+%.2fx21`, initialVal1, inc1, initialVal2, inc2) + + opts := promql.EngineOpts{ + Timeout: 1 * time.Hour, + MaxSamples: 1e10, + EnableNegativeOffset: true, + EnableAtModifier: true, + EnablePerStepStats: true, + } + qOpts := promql.NewPrometheusQueryOpts(true, 0) + + storage := promqltest.LoadedStorage(t, load) + defer storage.Close() + + start := time.Unix(int64(startTS), 0) + end := time.Unix(int64(endTS), 0) + interval := time.Duration(intervalSeconds) * time.Second + + seriesSet, err := getSeries(context.Background(), storage, "http_requests_total") + require.NoError(t, err) + psOpts := []promqlsmith.Option{ + promqlsmith.WithEnableOffset(true), + promqlsmith.WithEnableAtModifier(true), + // bottomk and topk sometimes lead to random failures since their result on equal values is essentially random + promqlsmith.WithEnabledAggrs([]parser.ItemType{parser.SUM, parser.MIN, parser.MAX, parser.AVG, parser.GROUP, parser.COUNT, parser.COUNT_VALUES, parser.QUANTILE}), + } + ps := promqlsmith.New(rnd, seriesSet, psOpts...) + + newEngine := engine.New(engine.Opts{EngineOpts: opts, EnableAnalysis: true}) + oldEngine := promql.NewEngine(opts) + + var ( + q1 promql.Query + query string + validateSamples bool + ) + cases := make([]*testCase, testRuns) + for i := range testRuns { + for { + expr := ps.WalkRangeQuery() + if !validateExpr(expr, testTypeFloat) { + continue + } + validateSamples = shouldValidateSamples(expr) + + query = expr.Pretty(0) + q1, err = newEngine.NewRangeQuery(context.Background(), storage, qOpts, query, start, end, interval) + if engine.IsUnimplemented(err) || errors.As(err, &parser.ParseErrors{}) { + continue + } else { + break + } + } + + testutil.Ok(t, err) + newResult := q1.Exec(context.Background()) + newStats := q1.Stats() + stats.NewQueryStats(newStats) + + q2, err := oldEngine.NewRangeQuery(context.Background(), storage, qOpts, query, start, end, interval) + testutil.Ok(t, err) + + oldResult := q2.Exec(context.Background()) + oldStats := q2.Stats() + stats.NewQueryStats(oldStats) + + cases[i] = &testCase{ + query: query, + newRes: newResult, + newStats: newStats, + oldRes: oldResult, + oldStats: oldStats, + loads: []string{load}, + start: start, + end: end, + interval: interval, + validateSamples: validateSamples, + } + } + validateTestCases(t, cases) + }) +} + +func FuzzEnginePromQLSmithInstantQuery(f *testing.F) { + f.Add(int64(0), uint32(0), 1.0, 1.0, 1.0, 2.0) + + f.Fuzz(func(t *testing.T, seed int64, ts uint32, initialVal1, initialVal2, inc1, inc2 float64) { + t.Parallel() + if inc1 < 0 || inc2 < 0 { + return + } + rnd := rand.New(rand.NewSource(seed)) + + load := fmt.Sprintf(`load 30s + http_requests_total{pod="nginx-1", route="/"} %.2f+%.2fx40 + http_requests_total{pod="nginx-2", route="/"} %2.f+%.2fx40`, initialVal1, inc1, initialVal2, inc2) + + opts := promql.EngineOpts{ + Timeout: 1 * time.Hour, + MaxSamples: 1e10, + EnableNegativeOffset: true, + EnableAtModifier: true, + EnablePerStepStats: true, + } + qOpts := promql.NewPrometheusQueryOpts(true, 0) + + storage := promqltest.LoadedStorage(t, load) + defer storage.Close() + + queryTime := time.Unix(int64(ts), 0) + newEngine := engine.New(engine.Opts{ + EngineOpts: opts, + LogicalOptimizers: logicalplan.AllOptimizers, + EnableAnalysis: true, + }) + oldEngine := promql.NewEngine(opts) + + seriesSet, err := getSeries(context.Background(), storage, "http_requests_total") + require.NoError(t, err) + psOpts := []promqlsmith.Option{ + promqlsmith.WithEnableOffset(true), + promqlsmith.WithEnableAtModifier(true), + promqlsmith.WithAtModifierMaxTimestamp(180 * 1000), + // bottomk and topk sometimes lead to random failures since their result on equal values is essentially random + promqlsmith.WithEnabledAggrs([]parser.ItemType{parser.SUM, parser.MIN, parser.MAX, parser.AVG, parser.GROUP, parser.COUNT, parser.COUNT_VALUES, parser.QUANTILE}), + } + ps := promqlsmith.New(rnd, seriesSet, psOpts...) + + var ( + q1 promql.Query + query string + validateSamples bool + ) + cases := make([]*testCase, testRuns) + for i := range testRuns { + // Since we disabled fallback, keep trying until we find a query + // that can be natively execute by the engine. + // Parsing experimental function, like mad_over_time, will lead to a parser.ParseErrors, so we also ignore those. + for { + expr := ps.WalkInstantQuery() + if !validateExpr(expr, testTypeFloat) { + continue + } + validateSamples = shouldValidateSamples(expr) + query = expr.Pretty(0) + q1, err = newEngine.NewInstantQuery(context.Background(), storage, qOpts, query, queryTime) + if engine.IsUnimplemented(err) || errors.As(err, &parser.ParseErrors{}) { + continue + } else { + break + } + } + + testutil.Ok(t, err) + newResult := q1.Exec(context.Background()) + newStats := q1.Stats() + stats.NewQueryStats(newStats) + + q2, err := oldEngine.NewInstantQuery(context.Background(), storage, qOpts, query, queryTime) + testutil.Ok(t, err) + + oldResult := q2.Exec(context.Background()) + oldStats := q2.Stats() + stats.NewQueryStats(oldStats) + + cases[i] = &testCase{ + query: query, + newRes: newResult, + newStats: newStats, + oldRes: oldResult, + oldStats: oldStats, + loads: []string{load}, + start: queryTime, + end: queryTime, + validateSamples: validateSamples, + } + } + validateTestCases(t, cases) + }) +} + +func getSeries(ctx context.Context, q storage.Queryable, query string) ([]labels.Labels, error) { + querier, err := q.Querier(0, time.Now().Unix()) + if err != nil { + return nil, err + } + res := make([]labels.Labels, 0) + ss := querier.Select(ctx, false, &storage.SelectHints{Func: "series"}, labels.MustNewMatcher(labels.MatchEqual, "__name__", query)) + for ss.Next() { + lbls := ss.At().Labels() + res = append(res, lbls) + } + if err := ss.Err(); err != nil { + return nil, err + } + return res, nil +} + +func validateTestCases(t *testing.T, cases []*testCase) { + failures := 0 + logQuery := func(c *testCase) { + for _, load := range c.loads { + t.Log(load) + } + t.Logf("query: %s, start: %d, end: %d, interval: %v", c.query, c.start.UnixMilli(), c.end.UnixMilli(), c.interval) + } + for i, c := range cases { + if !cmp.Equal(c.oldRes, c.newRes, comparer) { + if isAcceptableDuplicateDetectionDifference(c.newRes, c.oldRes) { + continue + } + logQuery(c) + t.Logf("case %d error mismatch.\nnew result: %s\nold result: %s\n", i, c.newRes.String(), c.oldRes.String()) + failures++ + continue + } + if !c.validateSamples || c.oldRes.Err != nil { + // Skip sample comparison + continue + } + if !cmp.Equal(c.oldStats.Samples, c.newStats.Samples, samplesComparer) { + logQuery(c) + t.Logf("case: %d, samples mismatch. total samples: old: %v, new: %v. samples per step: old: %v, new: %v", i, c.oldStats.Samples.TotalSamples, c.newStats.Samples.TotalSamples, c.oldStats.Samples.TotalSamplesPerStep, c.newStats.Samples.TotalSamplesPerStep) + failures++ + } + } + if failures > 0 { + t.Fatalf("failed %d test cases", failures) + } +} + +func normalizeBuckets(a, b, c uint64) []uint64 { + // Ensure strictly increasing positive values (cumulative form). + buckets := []uint64{a, b, c} + slices.Sort(buckets) + + // Avoid zero counts + for i := range buckets { + if buckets[i] == 0 { + buckets[i] = 1 + } + } + + return buckets +} + +func sumBuckets(buckets []uint64) uint64 { + var sum uint64 + for _, b := range buckets { + sum += b + } + return sum +} + +// Produces a realistic sum based on schema and bucket index. +func estimateSum(schema int8, buckets []uint64) float64 { + base := math.Pow(2, math.Pow(2, float64(-schema))) + var sum float64 + for i, count := range buckets { + value := math.Pow(base, float64(i+1)) + sum += float64(count) * value + } + return sum +} + +func FuzzNativeHistogramQuery(f *testing.F) { + f.Add(int64(0), uint32(0), uint32(60), uint32(120), int8(0), int8(0), uint64(1), uint64(2), uint64(1)) + + f.Fuzz(func(t *testing.T, seed int64, startTS, endTS, intervalSeconds uint32, schema1 int8, schema2 int8, b1, b2, b3 uint64) { + t.Parallel() + if endTS < startTS || intervalSeconds <= 0 { + return + } + + if schema1 < -4 || schema1 > 8 || schema2 < -4 || schema2 > 8 { + return + } + + // Ensure positive, increasing bucket counts (mimicking cumulative). + bucket1 := normalizeBuckets(b1, b2, b3) + bucket2 := normalizeBuckets(b1*2, b2+5, b3*2) + + count1 := sumBuckets(bucket1) + count2 := sumBuckets(bucket2) + + if count1 == 0 || count2 == 0 { + return + } + + sum1 := estimateSum(schema1, bucket1) + sum2 := estimateSum(schema2, bucket2) + + load := fmt.Sprintf(`load 2m + http_request_duration_seconds{pod="nginx-1"} {{schema:%d count:%d sum:%.2f buckets:%v}}+{{schema:%d count:%d buckets:%v}}x20 + http_request_duration_seconds{pod="nginx-2"} {{schema:%d count:%d sum:%.2f buckets:%v}}+{{schema:%d count:%d buckets:%v}}x30`, + schema1, count1, sum1, bucket1, + schema1, count1, bucket1, + schema2, count2, sum2, bucket2, + schema2, count2, bucket2, + ) + + opts := promql.EngineOpts{ + Timeout: 1 * time.Hour, + MaxSamples: 1e10, + EnableNegativeOffset: true, + EnableAtModifier: true, + EnablePerStepStats: true, + } + + qOpts := promql.NewPrometheusQueryOpts(true, 0) + queryable := promqltest.LoadedStorage(t, load) + t.Cleanup(func() { + queryable.Close() + }) + + startTime := time.Unix(int64(startTS), 0) + endTime := time.Unix(int64(endTS), 0) + interval := time.Duration(intervalSeconds) * time.Second + + seriesSet, err := getSeries(context.Background(), queryable, "http_request_duration_seconds") + require.NoError(t, err) + + psOpts := []promqlsmith.Option{ + promqlsmith.WithEnableOffset(true), + promqlsmith.WithEnableAtModifier(true), + promqlsmith.WithEnabledAggrs([]parser.ItemType{ + parser.SUM, parser.MIN, parser.MAX, parser.AVG, parser.GROUP, parser.COUNT, parser.COUNT_VALUES, parser.QUANTILE, + }), + } + + rnd := rand.New(rand.NewSource(seed)) + ps := promqlsmith.New(rnd, seriesSet, psOpts...) + newEngine := engine.New(engine.Opts{EngineOpts: opts, EnableAnalysis: true}) + oldEngine := promql.NewEngine(opts) + + instantCases := make([]*testCase, 0, testRuns/2) + rangeCases := make([]*testCase, 0, testRuns/2) + + var allQueries []promql.Query + t.Cleanup(func() { + for _, q := range allQueries { + if q != nil { + q.Close() + } + } + }) + + for range testRuns / 2 { + var ( + qInstant promql.Query + qRange promql.Query + instantQuery string + rangeQuery string + validateSamplesForInstantQuery bool + validateSamplesForRangeQuery bool + ) + + for { + expr := ps.WalkInstantQuery() + if !validateExpr(expr, testTypeNativeHistogram) { + continue + } + + validateSamplesForInstantQuery = shouldValidateSamples(expr) + instantQuery = expr.Pretty(0) + + qInstant, err = newEngine.NewInstantQuery(context.Background(), queryable, qOpts, instantQuery, startTime) + allQueries = append(allQueries, qInstant) + if engine.IsUnimplemented(err) || errors.As(err, &parser.ParseErrors{}) { + continue + } else { + break + } + } + testutil.Ok(t, err) + + for { + expr := ps.WalkRangeQuery() + if !validateExpr(expr, testTypeNativeHistogram) { + continue + } + + validateSamplesForRangeQuery = shouldValidateSamples(expr) + rangeQuery = expr.Pretty(0) + + qRange, err = newEngine.NewRangeQuery(context.Background(), queryable, qOpts, rangeQuery, startTime, endTime, interval) + allQueries = append(allQueries, qRange) + if engine.IsUnimplemented(err) || errors.As(err, &parser.ParseErrors{}) { + continue + } else { + break + } + } + testutil.Ok(t, err) + + // Instant query + newInstantResult := qInstant.Exec(context.Background()) + newInstantStats := qInstant.Stats() + stats.NewQueryStats(newInstantStats) + + q2Instant, err := oldEngine.NewInstantQuery(context.Background(), queryable, qOpts, instantQuery, startTime) + testutil.Ok(t, err) + allQueries = append(allQueries, q2Instant) + + oldInstantResult := q2Instant.Exec(context.Background()) + oldInstantStats := q2Instant.Stats() + stats.NewQueryStats(oldInstantStats) + + // Range query + newRangeResult := qRange.Exec(context.Background()) + newRangeStats := qRange.Stats() + stats.NewQueryStats(newRangeStats) + + q2Range, err := oldEngine.NewRangeQuery(context.Background(), queryable, qOpts, rangeQuery, startTime, endTime, interval) + testutil.Ok(t, err) + allQueries = append(allQueries, q2Range) + + oldRangeResult := q2Range.Exec(context.Background()) + oldRangeStats := q2Range.Stats() + stats.NewQueryStats(oldRangeStats) + + instantCases = append(instantCases, &testCase{ + query: instantQuery, + newRes: newInstantResult, + newStats: newInstantStats, + oldRes: oldInstantResult, + oldStats: oldInstantStats, + loads: []string{load}, + start: startTime, + end: startTime, + interval: 0, + validateSamples: validateSamplesForInstantQuery, + }) + + rangeCases = append(rangeCases, &testCase{ + query: rangeQuery, + newRes: newRangeResult, + newStats: newRangeStats, + oldRes: oldRangeResult, + oldStats: oldRangeStats, + loads: []string{load}, + start: startTime, + end: endTime, + interval: interval, + validateSamples: validateSamplesForRangeQuery, + }) + } + + validateTestCases(t, instantCases) + validateTestCases(t, rangeCases) + }) +} + +// isAcceptableDuplicateDetectionDifference checks if the result mismatch is due to +// different duplicate/cardinality detection behavior between distributed and Prometheus. +// +// Case 1: Distributed returns empty, Prometheus errors on duplicates +// Prometheus checks for duplicate series on each side BEFORE matching, and errors +// even if those duplicates would never participate in a match. For example: +// +// bar{zone="east-1"} <= on (zone) bar{zone!="east-1"} +// +// LHS has zone=east-1, RHS has zone=west-1/west-2. These will NEVER match because +// zones differ. But Prometheus errors because RHS has duplicate match keys (multiple +// series with zone=west-1). The distributed engine returns empty instead, which is +// arguably more correct - why error about duplicates that don't affect the result? +// +// Case 2: Distributed errors on many-to-one, Prometheus returns empty +// promql-engine is stricter than Prometheus about many-to-one matching detection +// for comparison operators. When selectors filter to different partitions, +// promql-engine may detect cardinality issues that Prometheus doesn't flag. For example: +// +// bar{zone=~"east.*"} <= on (zone) bar{zone="west-1"} +// +// LHS has zone=east-1/east-2, RHS has zone=west-1. These will NEVER match because +// zones differ. But promql-engine errors with "many-to-one" because it detects the +// cardinality mismatch, while Prometheus returns empty since the match never happens. +// +// We accept both directions of this mismatch for duplicate/cardinality errors. +func isAcceptableDuplicateDetectionDifference(newRes, oldRes *promql.Result) bool { + isDuplicateOrCardinalityError := func(r *promql.Result) bool { + if r.Err == nil { + return false + } + errStr := r.Err.Error() + return strings.Contains(errStr, "duplicate series") || + strings.Contains(errStr, "many-to-many") || + strings.Contains(errStr, "many-to-one") || + strings.Contains(errStr, "grouping labels must ensure unique matches") + } + + // Accept either direction: one has error, other doesn't + newHasError := isDuplicateOrCardinalityError(newRes) + oldHasError := isDuplicateOrCardinalityError(oldRes) + + return (newRes.Err == nil && oldHasError) || (oldRes.Err == nil && newHasError) +} + +func FuzzDistributedEngineQuery(f *testing.F) { + f.Add(int64(0), uint32(0), uint32(120), uint32(30), 1.0, 1.0, 1.0, 2.0) + + f.Fuzz(func(t *testing.T, seed int64, startTS, endTS, intervalSeconds uint32, initialVal1, initialVal2, inc1, inc2 float64) { + if math.IsNaN(initialVal1) || math.IsNaN(initialVal2) || math.IsNaN(inc1) || math.IsNaN(inc2) { + return + } + if math.IsInf(initialVal1, 0) || math.IsInf(initialVal2, 0) || math.IsInf(inc1, 0) || math.IsInf(inc2, 0) { + return + } + if inc1 < 0 || inc2 < 0 || intervalSeconds <= 0 || endTS <= startTS { + return + } + + rnd := rand.New(rand.NewSource(seed)) + + load1 := fmt.Sprintf(`load 30s + http_requests_total{pod="nginx-1", zone="east"} %.2f+%.2fx15 + http_requests_total{pod="nginx-2", zone="east"} %.2f+%.2fx15`, initialVal1, inc1, initialVal2, inc2) + + load2 := fmt.Sprintf(`load 30s + http_requests_total{pod="nginx-1", zone="west"} %.2f+%.2fx15 + http_requests_total{pod="nginx-2", zone="west"} %.2f+%.2fx15`, initialVal1*2, inc1, initialVal2*2, inc2) + + combinedLoad := fmt.Sprintf(`load 30s + http_requests_total{pod="nginx-1", zone="east"} %.2f+%.2fx15 + http_requests_total{pod="nginx-2", zone="east"} %.2f+%.2fx15 + http_requests_total{pod="nginx-1", zone="west"} %.2f+%.2fx15 + http_requests_total{pod="nginx-2", zone="west"} %.2f+%.2fx15`, initialVal1, inc1, initialVal2, inc2, initialVal1*2, inc1, initialVal2*2, inc2) + + opts := promql.EngineOpts{ + Timeout: 1 * time.Hour, + MaxSamples: 1e10, + EnableNegativeOffset: true, + EnableAtModifier: true, + } + + storage1 := promqltest.LoadedStorage(t, load1) + defer storage1.Close() + storage2 := promqltest.LoadedStorage(t, load2) + defer storage2.Close() + combinedStorage := promqltest.LoadedStorage(t, combinedLoad) + defer combinedStorage.Close() + + // Truncate to second precision + start := time.Unix(int64(startTS), 0) + end := time.Unix(int64(endTS), 0) + interval := time.Duration(intervalSeconds) * time.Second + + engineOpts := engine.Opts{EngineOpts: opts} + + // Create remote engines for each partition + remoteEngine1 := engine.NewRemoteEngine( + engineOpts, + storage1, + 0, + math.MaxInt64, + []labels.Labels{labels.FromStrings("zone", "east")}, + ) + remoteEngine2 := engine.NewRemoteEngine( + engineOpts, + storage2, + 0, + math.MaxInt64, + []labels.Labels{labels.FromStrings("zone", "west")}, + ) + + endpoints := api.NewStaticEndpoints([]api.RemoteEngine{remoteEngine1, remoteEngine2}) + + seriesSet, err := getSeries(context.Background(), combinedStorage, "http_requests_total") + require.NoError(t, err) + + psOpts := []promqlsmith.Option{ + promqlsmith.WithEnableOffset(false), + promqlsmith.WithEnableAtModifier(false), + promqlsmith.WithEnabledAggrs([]parser.ItemType{ + parser.SUM, parser.MIN, parser.MAX, parser.AVG, parser.GROUP, parser.COUNT, + }), + promqlsmith.WithEnableVectorMatching(true), + } + ps := promqlsmith.New(rnd, seriesSet, psOpts...) + + distEngine := engine.NewDistributedEngine(engineOpts) + promEngine := promql.NewEngine(opts) + + cases := make([]*testCase, 0, testRuns) + + for range testRuns { + var ( + query string + expr parser.Expr + ) + + for { + expr = ps.WalkRangeQuery() + if !validateExpr(expr, testTypeFloat) { + continue + } + query = expr.Pretty(0) + + _, err := distEngine.MakeRangeQuery(context.Background(), combinedStorage, endpoints, nil, query, start, end, interval) + if err == nil { + break + } + if engine.IsUnimplemented(err) || errors.As(err, &parser.ParseErrors{}) { + continue + } + continue + } + + distQry, err := distEngine.MakeRangeQuery(context.Background(), combinedStorage, endpoints, nil, query, start, end, interval) + if err != nil { + continue + } + distResult := distQry.Exec(context.Background()) + + promQry, err := promEngine.NewRangeQuery(context.Background(), combinedStorage, nil, query, start, end, interval) + if err != nil { + continue + } + promResult := promQry.Exec(context.Background()) + + cases = append(cases, &testCase{ + query: query, + newRes: distResult, + oldRes: promResult, + loads: []string{combinedLoad}, + start: start, + end: end, + interval: interval, + }) + } + + validateTestCases(t, cases) + }) +} diff --git a/internal/promql-engine/engine/existing_test.go b/internal/promql-engine/engine/existing_test.go new file mode 100644 index 00000000000..45f2f168e81 --- /dev/null +++ b/internal/promql-engine/engine/existing_test.go @@ -0,0 +1,155 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package engine_test + +import ( + "context" + "testing" + "time" + + "github.com/thanos-io/promql-engine/engine" + + "github.com/efficientgo/core/testutil" + "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/promql" + "github.com/prometheus/prometheus/promql/parser" + "github.com/prometheus/prometheus/promql/promqltest" +) + +func TestRangeQuery(t *testing.T) { + cases := []struct { + Name string + Load string + Query string + Result parser.Value + Start time.Time + End time.Time + Interval time.Duration + }{ + { + Name: "sum_over_time with all values", + Load: `load 30s + bar 0 1 10 100 1000`, + Query: `sum_over_time(bar[30s])`, + Result: promql.Matrix{ + promql.Series{ + Floats: []promql.FPoint{{F: 0, T: 0}, {F: 10, T: 60000}, {F: 1000, T: 120000}}, + Metric: labels.Labels{}, + }, + }, + Start: time.Unix(0, 0), + End: time.Unix(120, 0), + Interval: 60 * time.Second, + }, + { + Name: "sum_over_time with all values", + Load: `load 30s + bar 0 1 10 100 1000`, + Query: `sum_over_time(bar[45s])`, + Result: promql.Matrix{ + promql.Series{ + Floats: []promql.FPoint{{F: 0, T: 0}, {F: 11, T: 60000}, {F: 1100, T: 120000}}, + Metric: labels.Labels{}, + }, + }, + Start: time.Unix(0, 0), + End: time.Unix(120, 0), + Interval: 60 * time.Second, + }, + { + Name: "sum_over_time with trailing values", + Load: `load 30s + bar 0 1 10 100 1000 0 0 0 0`, + Query: `sum_over_time(bar[45s])`, + Result: promql.Matrix{ + promql.Series{ + Floats: []promql.FPoint{{F: 0, T: 0}, {F: 11, T: 60000}, {F: 1100, T: 120000}}, + Metric: labels.Labels{}, + }, + }, + Start: time.Unix(0, 0), + End: time.Unix(120, 0), + Interval: 60 * time.Second, + }, + { + Name: "sum_over_time with all values long", + Load: `load 30s + bar 0 1 10 100 1000 10000 100000 1000000 10000000`, + Query: `sum_over_time(bar[45s])`, + Result: promql.Matrix{ + promql.Series{ + Floats: []promql.FPoint{{F: 0, T: 0}, {F: 11, T: 60000}, {F: 1100, T: 120000}, {F: 110000, T: 180000}, {F: 11000000, T: 240000}}, + Metric: labels.Labels{}, + }, + }, + Start: time.Unix(0, 0), + End: time.Unix(240, 0), + Interval: 60 * time.Second, + }, + { + Name: "sum_over_time with all values random", + Load: `load 30s + bar 5 17 42 2 7 905 51`, + Query: `sum_over_time(bar[45s])`, + Result: promql.Matrix{ + promql.Series{ + Floats: []promql.FPoint{{F: 5, T: 0}, {F: 59, T: 60000}, {F: 9, T: 120000}, {F: 956, T: 180000}}, + Metric: labels.Labels{}, + }, + }, + Start: time.Unix(0, 0), + End: time.Unix(180, 0), + Interval: 60 * time.Second, + }, + { + Name: "metric query", + Load: `load 30s + metric 1+1x4`, + Query: `metric`, + Result: promql.Matrix{ + promql.Series{ + Floats: []promql.FPoint{{F: 1, T: 0}, {F: 3, T: 60000}, {F: 5, T: 120000}}, + Metric: labels.New(labels.Label{Name: "__name__", Value: "metric"}), + }, + }, + Start: time.Unix(0, 0), + End: time.Unix(120, 0), + Interval: 1 * time.Minute, + }, + { + Name: "metric query with trailing values", + Load: `load 30s + metric 1+1x8`, + Query: `metric`, + Result: promql.Matrix{ + promql.Series{ + Floats: []promql.FPoint{{F: 1, T: 0}, {F: 3, T: 60000}, {F: 5, T: 120000}}, + Metric: labels.New(labels.Label{Name: "__name__", Value: "metric"}), + }, + }, + Start: time.Unix(0, 0), + End: time.Unix(120, 0), + Interval: 1 * time.Minute, + }, + } + + opts := promql.EngineOpts{ + Timeout: 1 * time.Hour, + } + ng := engine.New(engine.Opts{EngineOpts: opts}) + for _, c := range cases { + t.Run(c.Name, func(t *testing.T) { + t.Parallel() + storage := promqltest.LoadedStorage(t, c.Load) + defer storage.Close() + + qry, err := ng.NewRangeQuery(context.Background(), storage, nil, c.Query, c.Start, c.End, c.Interval) + testutil.Ok(t, err) + + res := qry.Exec(context.Background()) + testutil.Ok(t, res.Err) + testutil.Equals(t, c.Result, res.Value) + }) + } +} diff --git a/internal/promql-engine/engine/explain.go b/internal/promql-engine/engine/explain.go new file mode 100644 index 00000000000..003593dad07 --- /dev/null +++ b/internal/promql-engine/engine/explain.go @@ -0,0 +1,125 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package engine + +import ( + "sync" + + "github.com/thanos-io/promql-engine/execution/model" + "github.com/thanos-io/promql-engine/execution/telemetry" + "github.com/thanos-io/promql-engine/logicalplan" + + "github.com/prometheus/prometheus/promql" +) + +type ExplainableQuery interface { + promql.Query + + Explain() *ExplainOutputNode + Analyze() *AnalyzeOutputNode +} + +type AnalyzeOutputNode struct { + OperatorTelemetry telemetry.OperatorTelemetry `json:"telemetry,omitempty"` + OperatorID *uint64 `json:"operatorId,omitempty"` + Children []*AnalyzeOutputNode `json:"children,omitempty"` + + once sync.Once + totalSamples int64 + peakSamples int64 + totalSamplesPerStep []int64 +} + +type ExplainOutputNode struct { + OperatorName string `json:"name,omitempty"` + Children []ExplainOutputNode `json:"children,omitempty"` +} + +var _ ExplainableQuery = &compatibilityQuery{} + +func (a *AnalyzeOutputNode) TotalSamples() int64 { + a.aggregateSamples() + return a.totalSamples +} + +func (a *AnalyzeOutputNode) TotalSamplesPerStep() []int64 { + a.aggregateSamples() + return a.totalSamplesPerStep +} + +func (a *AnalyzeOutputNode) PeakSamples() int64 { + a.aggregateSamples() + return a.peakSamples +} + +func (a *AnalyzeOutputNode) aggregateSamples() { + a.once.Do(func() { + if nodeSamples := a.OperatorTelemetry.Samples(); nodeSamples != nil { + a.totalSamples += nodeSamples.TotalSamples + a.peakSamples += int64(nodeSamples.PeakSamples) + a.totalSamplesPerStep = nodeSamples.TotalSamplesPerStep + } + + for _, child := range a.Children { + childPeak := child.PeakSamples() + a.peakSamples = max(a.peakSamples, childPeak) + + switch a.OperatorTelemetry.LogicalNode().(type) { + case *logicalplan.Subquery: + // Skip aggregating samples for subquery + case *logicalplan.StepInvariantExpr: + childSamples := child.TotalSamples() + for i := range a.totalSamplesPerStep { + a.totalSamples += childSamples + a.totalSamplesPerStep[i] += childSamples + } + default: + a.totalSamples += child.TotalSamples() + for i, s := range child.TotalSamplesPerStep() { + a.totalSamplesPerStep[i] += s + } + } + } + }) +} + +func analyzeQuery(op model.VectorOperator) *AnalyzeOutputNode { + var operatorID *uint64 + if ider, ok := op.(model.OperatorIDer); ok { + id := ider.OperatorID() + operatorID = &id + } + obsv, ok := model.Unwrap(op).(telemetry.ObservableVectorOperator) + if !ok { + return nil + } + + children := obsv.Explain() + var childTelemetry []*AnalyzeOutputNode + for _, child := range children { + if node := analyzeQuery(child); node != nil { + childTelemetry = append(childTelemetry, node) + } + } + + return &AnalyzeOutputNode{ + OperatorTelemetry: obsv, + OperatorID: operatorID, + Children: childTelemetry, + } +} + +func explainVector(v model.VectorOperator) *ExplainOutputNode { + vectors := v.Explain() + + var children []ExplainOutputNode + for _, vector := range vectors { + children = append(children, *explainVector(vector)) + } + + return &ExplainOutputNode{ + OperatorName: v.String(), + Children: children, + } +} diff --git a/internal/promql-engine/engine/explain_test.go b/internal/promql-engine/engine/explain_test.go new file mode 100644 index 00000000000..e5402d5702d --- /dev/null +++ b/internal/promql-engine/engine/explain_test.go @@ -0,0 +1,439 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package engine_test + +import ( + "context" + "fmt" + "runtime" + "strings" + "testing" + "time" + + "github.com/thanos-io/promql-engine/engine" + + "github.com/efficientgo/core/testutil" + "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/promql" + "github.com/prometheus/prometheus/promql/promqltest" + "github.com/prometheus/prometheus/storage" + "github.com/stretchr/testify/require" +) + +func TestQueryExplain(t *testing.T) { + t.Parallel() + opts := promql.EngineOpts{Timeout: 1 * time.Hour} + series := storage.MockSeries( + []int64{240, 270, 300, 600, 630, 660}, + []float64{1, 2, 3, 4, 5, 6}, + []string{labels.MetricName, "foo"}, + ) + + start := time.Unix(0, 0) + end := time.Unix(1000, 0) + + // Calculate concurrencyOperators according to max available CPUs. + totalOperators := runtime.GOMAXPROCS(0) / 2 + var concurrencyOperators []engine.ExplainOutputNode + for i := range totalOperators { + concurrencyOperators = append(concurrencyOperators, engine.ExplainOutputNode{ + OperatorName: "[concurrent(buff=2)]", Children: []engine.ExplainOutputNode{ + {OperatorName: fmt.Sprintf("[vectorSelector] {[__name__=\"foo\"]} %d mod %d", i, totalOperators)}, + }, + }) + } + + for _, tc := range []struct { + query string + expected *engine.ExplainOutputNode + }{ + { + query: `time()`, + expected: &engine.ExplainOutputNode{OperatorName: "[duplicateLabelCheck]", Children: []engine.ExplainOutputNode{ + { + OperatorName: "[noArgFunction]", + Children: nil, + }, + }}, + }, + { + query: `foo`, + expected: &engine.ExplainOutputNode{OperatorName: "[coalesce]", Children: concurrencyOperators}, + }, + { + query: `sum by (job) (foo)`, + expected: &engine.ExplainOutputNode{ + OperatorName: "[duplicateLabelCheck]", + Children: []engine.ExplainOutputNode{ + { + OperatorName: "[concurrent(buff=2)]", Children: []engine.ExplainOutputNode{ + { + OperatorName: "[aggregate] sum by ([job])", Children: []engine.ExplainOutputNode{ + { + OperatorName: "[coalesce]", + Children: concurrencyOperators, + }, + }, + }, + }, + }, + }, + }, + }, + } { + { + t.Run(tc.query, func(t *testing.T) { + ng := engine.New(engine.Opts{EngineOpts: opts}) + ctx := context.Background() + + var ( + query promql.Query + err error + ) + + query, err = ng.NewInstantQuery(ctx, storageWithSeries(series), nil, tc.query, start) + testutil.Ok(t, err) + + explainableQuery := query.(engine.ExplainableQuery) + testutil.Equals(t, tc.expected, explainableQuery.Explain()) + + query, err = ng.NewRangeQuery(ctx, storageWithSeries(series), nil, tc.query, start, end, 30*time.Second) + testutil.Ok(t, err) + + explainableQuery = query.(engine.ExplainableQuery) + testutil.Equals(t, tc.expected, explainableQuery.Explain()) + }) + } + } +} + +func TestQueryAnalyzeOperatorID(t *testing.T) { + t.Parallel() + opts := promql.EngineOpts{Timeout: 1 * time.Hour} + series := storage.MockSeries( + []int64{240, 270, 300, 600, 630, 660}, + []float64{1, 2, 3, 4, 5, 6}, + []string{`__name__`, "foo"}, + ) + start := time.Unix(0, 0) + + for _, tc := range []struct { + query string + expectedIDs []uint64 + }{ + { + query: `foo`, + expectedIDs: []uint64{9607318204070194689}, + }, + { + query: `sum by (job) (foo)`, + expectedIDs: []uint64{17184185013747611877}, + }, + { + query: `time()`, + expectedIDs: nil, + }, + } { + t.Run(tc.query, func(t *testing.T) { + ng := engine.New(engine.Opts{EngineOpts: opts, EnableAnalysis: true}) + ctx := context.Background() + query, err := ng.NewInstantQuery(ctx, storageWithSeries(series), nil, tc.query, start) + testutil.Ok(t, err) + testutil.Ok(t, query.Exec(ctx).Err) + + var ids []uint64 + level := []*engine.AnalyzeOutputNode{query.(engine.ExplainableQuery).Analyze()} + for len(level) > 0 { + var next []*engine.AnalyzeOutputNode + for _, node := range level { + if node.OperatorID != nil { + ids = append(ids, *node.OperatorID) + } + next = append(next, node.Children...) + } + level = next + } + testutil.Equals(t, tc.expectedIDs, ids) + }) + } +} + +func assertExecutionTimeNonZero(t *testing.T, got *engine.AnalyzeOutputNode) bool { + if got != nil { + if got.OperatorTelemetry.ExecutionTimeTaken() <= 0 { + t.Errorf("expected non-zero ExecutionTime for Operator, got %s ", got.OperatorTelemetry.ExecutionTimeTaken()) + return false + } + for i := range got.Children { + child := got.Children[i] + return got.OperatorTelemetry.ExecutionTimeTaken() > 0 && assertExecutionTimeNonZero(t, child) + } + } + return true +} + +func assertSeriesExecutionTimeNonZero(t *testing.T, got *engine.AnalyzeOutputNode) bool { + if got != nil { + if got.OperatorTelemetry.SeriesExecutionTime() <= 0 { + t.Errorf("expected non-zero SeriesExecutionTime for Operator, got %s ", got.OperatorTelemetry.SeriesExecutionTime()) + return false + } + for i := range got.Children { + child := got.Children[i] + return got.OperatorTelemetry.SeriesExecutionTime() > 0 && assertSeriesExecutionTimeNonZero(t, child) + } + } + return true +} + +func assertNextExecutionTimeNonZero(t *testing.T, got *engine.AnalyzeOutputNode) bool { + if got != nil { + if got.OperatorTelemetry.NextExecutionTime() <= 0 { + t.Errorf("expected non-zero NextExecutionTime for Operator, got %s ", got.OperatorTelemetry.NextExecutionTime()) + return false + } + for i := range got.Children { + child := got.Children[i] + return got.OperatorTelemetry.NextExecutionTime() > 0 && assertNextExecutionTimeNonZero(t, child) + } + } + return true +} + +// getMaxSeriesCount gets the max series count from the explain output node tree. +func getMaxSeriesCount(got *engine.AnalyzeOutputNode) int { + maxSeriesCount := 0 + if got != nil { + maxSeriesCount = got.OperatorTelemetry.MaxSeriesCount() + for i := range got.Children { + child := got.Children[i] + maxSeriesCount = max(maxSeriesCount, getMaxSeriesCount(child)) + } + } + return maxSeriesCount +} + +func TestQueryAnalyze(t *testing.T) { + opts := promql.EngineOpts{Timeout: 1 * time.Hour} + seriesList := []storage.Series{ + storage.MockSeries( + []int64{240, 270, 300, 600, 630, 660}, + []float64{1, 2, 3, 4, 5, 6}, + []string{labels.MetricName, "foo"}, + ), + storage.MockSeries( + []int64{240, 270, 300, 600, 630, 660}, + []float64{1, 2, 3, 4, 5, 6}, + []string{labels.MetricName, "http_requests_total", "pod", "nginx-1"}, + ), + storage.MockSeries( + []int64{240, 270, 300, 600, 630, 660}, + []float64{1, 2, 3, 4, 5, 6}, + []string{labels.MetricName, "http_requests_total", "pod", "nginx-2"}, + ), + storage.MockSeries( + []int64{240, 270, 300, 600, 630, 660}, + []float64{1, 2, 3, 4, 5, 6}, + []string{labels.MetricName, "http_requests_total", "pod", "nginx-3"}, + ), + } + + start := time.Unix(0, 0) + end := time.Unix(1000, 0) + + for _, tc := range []struct { + query string + maxSeriesCount int + }{ + { + query: `foo`, + maxSeriesCount: 1, + }, + { + query: `time()`, + maxSeriesCount: 0, + }, + { + query: `sum by (job) (foo)`, + maxSeriesCount: 1, + }, + { + query: `rate(http_requests_total[30s]) > bool 0`, + maxSeriesCount: 3, + }, + } { + { + t.Run(tc.query, func(t *testing.T) { + t.Parallel() + ng := engine.New(engine.Opts{EngineOpts: opts, EnableAnalysis: true}) + ctx := context.Background() + + var ( + query promql.Query + err error + ) + + query, err = ng.NewInstantQuery(ctx, storageWithSeries(seriesList...), nil, tc.query, start) + testutil.Ok(t, err) + + queryResults := query.Exec(context.Background()) + testutil.Ok(t, queryResults.Err) + + explainableQuery := query.(engine.ExplainableQuery) + + testutil.Assert(t, assertExecutionTimeNonZero(t, explainableQuery.Analyze())) + testutil.Assert(t, assertSeriesExecutionTimeNonZero(t, explainableQuery.Analyze())) + testutil.Assert(t, assertNextExecutionTimeNonZero(t, explainableQuery.Analyze())) + + testutil.Equals(t, tc.maxSeriesCount, getMaxSeriesCount(explainableQuery.Analyze())) + + query, err = ng.NewRangeQuery(ctx, storageWithSeries(seriesList...), nil, tc.query, start, end, 30*time.Second) + testutil.Ok(t, err) + + queryResults = query.Exec(context.Background()) + testutil.Ok(t, queryResults.Err) + + explainableQuery = query.(engine.ExplainableQuery) + testutil.Assert(t, assertExecutionTimeNonZero(t, explainableQuery.Analyze())) + testutil.Assert(t, assertSeriesExecutionTimeNonZero(t, explainableQuery.Analyze())) + testutil.Assert(t, assertNextExecutionTimeNonZero(t, explainableQuery.Analyze())) + }) + } + } +} +func TestAnalyzeOutputNode_Samples(t *testing.T) { + t.Parallel() + ng := engine.New(engine.Opts{EngineOpts: promql.EngineOpts{Timeout: 1 * time.Hour}, EnableAnalysis: true, DecodingConcurrency: 2}) + ctx := context.Background() + + load := `load 30s + http_requests_total{pod="nginx-1"} 1+1x100 + http_requests_total{pod="nginx-2"} 1+1x100` + + tstorage := promqltest.LoadedStorage(t, load) + defer tstorage.Close() + minT := tstorage.Head().Meta().MinTime + maxT := tstorage.Head().Meta().MaxTime + + query, err := ng.NewInstantQuery(ctx, tstorage, nil, "http_requests_total", time.Unix(0, 0)) + testutil.Ok(t, err) + queryResults := query.Exec(context.Background()) + testutil.Ok(t, queryResults.Err) + explainableQuery := query.(engine.ExplainableQuery) + analyzeOutput := explainableQuery.Analyze() + require.Greater(t, analyzeOutput.PeakSamples(), int64(0)) + require.Greater(t, analyzeOutput.TotalSamples(), int64(0)) + + rangeQry, err := ng.NewRangeQuery( + ctx, + tstorage, + promql.NewPrometheusQueryOpts(false, 0), + "sum(rate(http_requests_total[10m])) by (pod)", // Increase range to 60 minutes + time.Unix(minT, 0), + time.Unix(maxT, 0), + 60*time.Second, + ) + testutil.Ok(t, err) + queryResults = rangeQry.Exec(context.Background()) + testutil.Ok(t, queryResults.Err) + + explainableQuery = rangeQry.(engine.ExplainableQuery) + analyzeOutput = explainableQuery.Analyze() + require.Greater(t, analyzeOutput.PeakSamples(), int64(0)) + require.Greater(t, analyzeOutput.TotalSamples(), int64(0)) + result := renderAnalysisTree(analyzeOutput, 0) + expected := `[duplicateLabelCheck]: max_series: 2 total_samples: 0 peak_samples: 0 +|---[concurrent(buff=2)]: max_series: 2 total_samples: 0 peak_samples: 0 +| |---[aggregate] sum by ([pod]): max_series: 2 total_samples: 0 peak_samples: 0 +| | |---[duplicateLabelCheck]: max_series: 2 total_samples: 0 peak_samples: 0 +| | | |---[coalesce]: max_series: 2 total_samples: 0 peak_samples: 0 +| | | | |---[concurrent(buff=2)]: max_series: 1 total_samples: 0 peak_samples: 0 +| | | | | |---[matrixSelector] rate({[__name__="http_requests_total"]}[10m0s] 0 mod 2): max_series: 1 total_samples: 1010 peak_samples: 200 +| | | | |---[concurrent(buff=2)]: max_series: 1 total_samples: 0 peak_samples: 0 +| | | | | |---[matrixSelector] rate({[__name__="http_requests_total"]}[10m0s] 1 mod 2): max_series: 1 total_samples: 1010 peak_samples: 200 +` + require.EqualValues(t, expected, result) +} + +func renderAnalysisTree(node *engine.AnalyzeOutputNode, level int) string { + var result strings.Builder + + totalSamples := int64(0) + seriesCount := node.OperatorTelemetry.MaxSeriesCount() + samples := node.OperatorTelemetry.Samples() + if samples != nil { + totalSamples = samples.TotalSamples + } + + peakSamples := int64(0) + if samples != nil { + peakSamples = int64(samples.PeakSamples) + } + + if level > 0 { + result.WriteString(strings.Repeat("| ", level-1) + "|---") + } + + result.WriteString(fmt.Sprintf("%s: max_series: %d total_samples: %d peak_samples: %d\n", node.OperatorTelemetry.String(), seriesCount, totalSamples, peakSamples)) + for _, child := range node.Children { + result.WriteString(renderAnalysisTree(child, level+1)) + } + + return result.String() +} + +func TestAnalyzPeak(t *testing.T) { + t.Parallel() + ng := engine.New(engine.Opts{EngineOpts: promql.EngineOpts{Timeout: 1 * time.Hour}, EnableAnalysis: true, DecodingConcurrency: 2}) + ctx := context.Background() + load := `load 30s + http_requests_total{pod="nginx-1"} 1+1x100 + http_requests_total{pod="nginx-2"} 1+1x100` + + tstorage := promqltest.LoadedStorage(t, load) + defer tstorage.Close() + minT := tstorage.Head().Meta().MinTime + maxT := tstorage.Head().Meta().MaxTime + + query, err := ng.NewInstantQuery(ctx, tstorage, nil, "http_requests_total", time.Unix(0, 0)) + testutil.Ok(t, err) + queryResults := query.Exec(context.Background()) + testutil.Ok(t, queryResults.Err) + explainableQuery := query.(engine.ExplainableQuery) + analyzeOutput := explainableQuery.Analyze() + require.Greater(t, analyzeOutput.PeakSamples(), int64(0)) + require.Greater(t, analyzeOutput.TotalSamples(), int64(0)) + + rangeQry, err := ng.NewRangeQuery( + ctx, + tstorage, + promql.NewPrometheusQueryOpts(false, 0), + "sum(rate(http_requests_total[10m])) by (pod)", + time.Unix(minT, 0), + time.Unix(maxT, 0), + 60*time.Second, + ) + testutil.Ok(t, err) + queryResults = rangeQry.Exec(context.Background()) + testutil.Ok(t, queryResults.Err) + + explainableQuery = rangeQry.(engine.ExplainableQuery) + analyzeOutput = explainableQuery.Analyze() + + t.Logf("value of peak = %v", analyzeOutput.PeakSamples()) + require.Equal(t, int64(200), analyzeOutput.PeakSamples()) + + result := renderAnalysisTree(analyzeOutput, 0) + expected := `[duplicateLabelCheck]: max_series: 2 total_samples: 0 peak_samples: 0 +|---[concurrent(buff=2)]: max_series: 2 total_samples: 0 peak_samples: 0 +| |---[aggregate] sum by ([pod]): max_series: 2 total_samples: 0 peak_samples: 0 +| | |---[duplicateLabelCheck]: max_series: 2 total_samples: 0 peak_samples: 0 +| | | |---[coalesce]: max_series: 2 total_samples: 0 peak_samples: 0 +| | | | |---[concurrent(buff=2)]: max_series: 1 total_samples: 0 peak_samples: 0 +| | | | | |---[matrixSelector] rate({[__name__="http_requests_total"]}[10m0s] 0 mod 2): max_series: 1 total_samples: 1010 peak_samples: 200 +| | | | |---[concurrent(buff=2)]: max_series: 1 total_samples: 0 peak_samples: 0 +| | | | | |---[matrixSelector] rate({[__name__="http_requests_total"]}[10m0s] 1 mod 2): max_series: 1 total_samples: 1010 peak_samples: 200 +` + require.EqualValues(t, expected, result) +} diff --git a/internal/promql-engine/engine/projection_test.go b/internal/promql-engine/engine/projection_test.go new file mode 100644 index 00000000000..caf90cd499d --- /dev/null +++ b/internal/promql-engine/engine/projection_test.go @@ -0,0 +1,289 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package engine_test + +import ( + "context" + "fmt" + "math/rand" + "slices" + "strconv" + "testing" + "time" + + "github.com/thanos-io/promql-engine/engine" + "github.com/thanos-io/promql-engine/logicalplan" + + "github.com/cortexproject/promqlsmith" + "github.com/efficientgo/core/errors" + "github.com/efficientgo/core/testutil" + "github.com/google/go-cmp/cmp" + "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/promql" + "github.com/prometheus/prometheus/promql/parser" + "github.com/prometheus/prometheus/promql/promqltest" + "github.com/prometheus/prometheus/storage" + "github.com/prometheus/prometheus/tsdb/chunkenc" + "github.com/prometheus/prometheus/util/annotations" +) + +type projectionQuerier struct { + storage.Querier +} + +type projectionSeriesSet struct { + storage.SeriesSet + hints *storage.SelectHints +} + +func (m projectionSeriesSet) Next() bool { return m.SeriesSet.Next() } +func (m projectionSeriesSet) At() storage.Series { + // Get the original series + originalSeries := m.SeriesSet.At() + if originalSeries == nil { + return nil + } + // If no projection hints, return the original series + if m.hints == nil { + return originalSeries + } + if !m.hints.ProjectionInclude && len(m.hints.ProjectionLabels) == 0 { + return originalSeries + } + + // Apply projection based on hints + originalLabels := originalSeries.Labels() + var projectedLabels labels.Labels + + if m.hints.ProjectionInclude { + // Include mode: only keep the labels in the projection labels + builder := labels.NewBuilder(labels.EmptyLabels()) + originalLabels.Range(func(l labels.Label) { + if slices.Contains(m.hints.ProjectionLabels, l.Name) { + builder.Set(l.Name, l.Value) + } + }) + builder.Set("__series_hash__", strconv.FormatUint(originalLabels.Hash(), 10)) + projectedLabels = builder.Labels() + } else { + // Exclude mode: keep all labels except those in the projection labels + excludeMap := make(map[string]struct{}) + for _, groupLabel := range m.hints.ProjectionLabels { + excludeMap[groupLabel] = struct{}{} + } + + builder := labels.NewBuilder(labels.EmptyLabels()) + originalLabels.Range(func(l labels.Label) { + if _, excluded := excludeMap[l.Name]; !excluded { + builder.Set(l.Name, l.Value) + } + }) + builder.Set("__series_hash__", strconv.FormatUint(originalLabels.Hash(), 10)) + projectedLabels = builder.Labels() + } + + // Return a projected series that wraps the original but with filtered labels + return &projectedSeries{ + Series: originalSeries, + lset: projectedLabels, + } +} + +// projectedSeries wraps a storage.Series but returns projected labels. +type projectedSeries struct { + storage.Series + lset labels.Labels +} + +func (s *projectedSeries) Labels() labels.Labels { + return s.lset +} + +func (s *projectedSeries) Iterator(iter chunkenc.Iterator) chunkenc.Iterator { + return s.Series.Iterator(iter) +} + +func (m projectionSeriesSet) Err() error { return m.SeriesSet.Err() } +func (m projectionSeriesSet) Warnings() annotations.Annotations { return m.SeriesSet.Warnings() } + +// Implement the Querier interface methods. +func (m *projectionQuerier) Select(ctx context.Context, sortSeries bool, hints *storage.SelectHints, matchers ...*labels.Matcher) storage.SeriesSet { + return projectionSeriesSet{ + SeriesSet: m.Querier.Select(ctx, sortSeries, hints, matchers...), + hints: hints, + } +} +func (m *projectionQuerier) LabelValues(ctx context.Context, name string, _ *storage.LabelHints, matchers ...*labels.Matcher) ([]string, annotations.Annotations, error) { + return nil, nil, nil +} +func (m *projectionQuerier) LabelNames(ctx context.Context, _ *storage.LabelHints, matchers ...*labels.Matcher) ([]string, annotations.Annotations, error) { + return nil, nil, nil +} +func (m *projectionQuerier) Close() error { return nil } + +// projectionQueryable is a storage.Queryable that applies projection to the querier. +type projectionQueryable struct { + storage.Queryable +} + +func (q *projectionQueryable) Querier(mint, maxt int64) (storage.Querier, error) { + querier, err := q.Queryable.Querier(mint, maxt) + if err != nil { + return nil, err + } + return &projectionQuerier{ + Querier: querier, + }, nil +} + +func TestProjectionWithFuzz(t *testing.T) { + t.Parallel() + + // Define test parameters + seed := time.Now().UnixNano() + rnd := rand.New(rand.NewSource(seed)) + testRuns := 10000 + + // Create test data + load := `load 30s + http_requests_total{pod="nginx-1", job="app", env="prod", instance="1"} 1+1x40 + http_requests_total{pod="nginx-2", job="app", env="dev", instance="2"} 2+2x40 + http_requests_total{pod="nginx-3", job="api", env="prod", instance="3"} 3+3x40 + http_requests_total{pod="nginx-4", job="api", env="dev", instance="4"} 4+4x40 + http_requests_duration_seconds_bucket{pod="nginx-1", job="app", env="prod", instance="1", le="0.1"} 1+1x40 + http_requests_duration_seconds_bucket{pod="nginx-1", job="app", env="prod", instance="1", le="0.2"} 2+2x40 + http_requests_duration_seconds_bucket{pod="nginx-1", job="app", env="prod", instance="1", le="0.5"} 3+2x40 + http_requests_duration_seconds_bucket{pod="nginx-1", job="app", env="prod", instance="1", le="+Inf"} 4+2x40 + http_requests_duration_seconds_bucket{pod="nginx-2", job="api", env="dev", instance="2", le="0.1"} 1+1x40 + http_requests_duration_seconds_bucket{pod="nginx-2", job="api", env="dev", instance="2", le="0.2"} 2+2x40 + http_requests_duration_seconds_bucket{pod="nginx-2", job="api", env="dev", instance="2", le="0.5"} 3+2x40 + http_requests_duration_seconds_bucket{pod="nginx-2", job="api", env="dev", instance="2", le="+Inf"} 4+2x40 + errors_total{pod="nginx-1", job="app", env="prod", instance="1", cluster="us-west-2"} 0.5+0.5x40 + errors_total{pod="nginx-2", job="app", env="dev", instance="2", cluster="us-west-2"} 1+1x40 + errors_total{pod="nginx-3", job="api", env="prod", instance="3", cluster="us-east-2"} 1.5+1.5x40 + errors_total{pod="nginx-4", job="api", env="dev", instance="4", cluster="us-east-1"} 2+2x40` + + storage := promqltest.LoadedStorage(t, load) + defer storage.Close() + + // Get series for PromQLSmith + seriesSet, err := getSeries(context.Background(), storage, "http_requests_total") + testutil.Ok(t, err) + + // Configure PromQLSmith + psOpts := []promqlsmith.Option{ + promqlsmith.WithEnableOffset(false), + promqlsmith.WithEnableAtModifier(false), + // Focus on aggregations that benefit from projection pushdown + promqlsmith.WithEnabledAggrs([]parser.ItemType{ + parser.SUM, parser.MIN, parser.MAX, parser.AVG, parser.COUNT, parser.TOPK, parser.BOTTOMK, + }), + promqlsmith.WithEnableVectorMatching(true), + } + ps := promqlsmith.New(rnd, seriesSet, psOpts...) + + // Engine options + engineOpts := promql.EngineOpts{ + Timeout: 1 * time.Hour, + MaxSamples: 1e10, + EnableNegativeOffset: true, + EnableAtModifier: true, + } + + normalEngine := engine.New(engine.Opts{ + EngineOpts: engineOpts, + LogicalOptimizers: logicalplan.AllOptimizers, + DisableDuplicateLabelChecks: false, + }) + + projectionEngine := engine.New(engine.Opts{ + EngineOpts: engineOpts, + // projection optimizer doesn't support merge selects optimizer + // so disable it for now. + LogicalOptimizers: []logicalplan.Optimizer{ + logicalplan.SortMatchers{}, + logicalplan.ProjectionOptimizer{SeriesHashLabel: "__series_hash__"}, + logicalplan.DetectHistogramStatsOptimizer{}, + logicalplan.MergeSelectsOptimizer{}, + }, + DisableDuplicateLabelChecks: false, + }) + + ctx := context.Background() + queryTime := time.Unix(600, 0) + + t.Logf("Running %d fuzzy tests with seed %d", testRuns, seed) + for i := range testRuns { + var expr parser.Expr + var query string + + // Generate a query that can be executed by the engine + for { + expr = ps.WalkInstantQuery() + query = expr.Pretty(0) + + // Skip queries that don't benefit from projection pushdown + if !containsProjectionExprs(expr) { + continue + } + + // Try to parse the query and see if it is valid. + _, err := normalEngine.NewInstantQuery(ctx, storage, nil, query, queryTime) + if err != nil { + continue + } + break + } + + t.Run(fmt.Sprintf("Query_%d", i), func(t *testing.T) { + // Create projection querier that wraps the original querier + projectionStorage := &projectionQueryable{ + Queryable: storage, + } + + normalQuery, err := normalEngine.NewInstantQuery(ctx, storage, &engine.QueryOpts{}, query, queryTime) + testutil.Ok(t, err) + defer normalQuery.Close() + normalResult := normalQuery.Exec(ctx) + if normalResult.Err != nil { + // Something wrong with the generated query so it even failed without projection pushdown, skipping. + return + } + testutil.Ok(t, normalResult.Err, "query: %s", query) + + projectionQuery, err := projectionEngine.MakeInstantQuery(ctx, projectionStorage, &engine.QueryOpts{}, query, queryTime) + testutil.Ok(t, err) + + defer projectionQuery.Close() + projectionResult := projectionQuery.Exec(ctx) + testutil.Ok(t, projectionResult.Err, "query: %s", query) + + if diff := cmp.Diff(normalResult, projectionResult, comparer); diff != "" { + t.Errorf("Results differ for query %s: %s", query, diff) + } + }) + } +} + +// containsProjectionExprs checks if the expression contains any expressions that might benefit from projection pushdown. +func containsProjectionExprs(expr parser.Expr) bool { + found := false + parser.Inspect(expr, func(node parser.Node, _ []parser.Node) error { + switch n := node.(type) { + case *parser.Call: + if n.Func.Name == "histogram_quantile" || n.Func.Name == "absent_over_time" || n.Func.Name == "absent" || n.Func.Name == "scalar" { + found = true + return errors.New("found") + } + case *parser.AggregateExpr: + found = true + return errors.New("found") + case *parser.BinaryExpr: + found = true + return errors.New("found") + } + return nil + }) + return found +} diff --git a/internal/promql-engine/engine/propagate_selector_test.go b/internal/promql-engine/engine/propagate_selector_test.go new file mode 100644 index 00000000000..d1fd33b124b --- /dev/null +++ b/internal/promql-engine/engine/propagate_selector_test.go @@ -0,0 +1,186 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package engine_test + +import ( + "context" + "fmt" + "math/rand" + "testing" + "time" + + "github.com/thanos-io/promql-engine/engine" + "github.com/thanos-io/promql-engine/logicalplan" + + "github.com/cortexproject/promqlsmith" + "github.com/efficientgo/core/testutil" + "github.com/google/go-cmp/cmp" + "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/promql" + "github.com/prometheus/prometheus/promql/parser" + "github.com/prometheus/prometheus/promql/promqltest" +) + +func TestPropagateMatchers(t *testing.T) { + t.Parallel() + + // Define test parameters + seed := time.Now().UnixNano() + rnd := rand.New(rand.NewSource(seed)) + testRuns := 10000 + + // Create test data + load := `load 30s + http_requests_total{pod="nginx-1", job="app", env="prod", instance="1"} 1+1x40 + http_requests_total{pod="nginx-2", job="app", env="dev", instance="2"} 2+2x40 + http_requests_total{pod="nginx-3", job="api", env="prod", instance="3"} 3+3x40 + http_requests_total{pod="nginx-4", job="api", env="dev", instance="4"} 4+4x40 + http_requests_total2{pod="nginx-1", job="app", env="prod", instance="1"} 1+1x40 + http_requests_total2{pod="nginx-2", job="app", env="dev", instance="2"} 2+2x40 + http_requests_total2{pod="nginx-3", job="api", env="prod", instance="3"} 3+3x40 + http_requests_total2{pod="nginx-4", job="api", env="dev", instance="5"} 5+3x40 + http_requests_total3{pod="nginx-1", job="app", env="prod", cluster="us-west-2"} 1+1x40 + http_requests_total3{pod="nginx-2", job="app", env="dev", cluster="us-east-1"} 2+2x40 + ` + + storage := promqltest.LoadedStorage(t, load) + defer storage.Close() + + // Get series for PromQLSmith + seriesSet, err := getSeries(context.Background(), storage, "http_requests_total") + testutil.Ok(t, err) + + // Configure PromQLSmith + psOpts := []promqlsmith.Option{ + promqlsmith.WithEnableOffset(false), + promqlsmith.WithEnableAtModifier(false), + promqlsmith.WithEnabledExprs([]promqlsmith.ExprType{promqlsmith.BinaryExpr, promqlsmith.VectorSelector}), + promqlsmith.WithMaxDepth(3), + promqlsmith.WithEnableVectorMatching(true), + } + ps := promqlsmith.New(rnd, seriesSet, psOpts...) + + // Engine options + engineOpts := promql.EngineOpts{ + Timeout: 1 * time.Hour, + MaxSamples: 1e10, + EnableNegativeOffset: true, + EnableAtModifier: true, + } + + normalEngine := engine.New(engine.Opts{ + EngineOpts: engineOpts, + LogicalOptimizers: logicalplan.NoOptimizers, + DisableDuplicateLabelChecks: false, + }) + + optimizedEngine := engine.New(engine.Opts{ + EngineOpts: engineOpts, + LogicalOptimizers: []logicalplan.Optimizer{ + logicalplan.SortMatchers{}, + logicalplan.MergeSelectsOptimizer{}, + logicalplan.PropagateMatchersOptimizer{}, + }, + DisableDuplicateLabelChecks: false, + }) + + ctx := context.Background() + queryTime := time.Unix(600, 0) + metricNameMapping := map[string]string{ + "http_requests_total": "http_requests_total2", + "http_requests_total2": "http_requests_total", + "http_requests_total3": "http_requests_total", + } + + t.Logf("Running %d fuzzy tests with seed %d", testRuns, seed) + for i := range testRuns { + var expr parser.Expr + var query string + + // Generate a query that can be executed by the engine + for { + expr = ps.WalkInstantQuery() + expr = logicalplan.UnwrapParens(expr) + if _, ok := expr.(*parser.BinaryExpr); !ok { + continue + } + + // Binary expression generated by promqlsmith uses the same metric name. + // Manually replace the metric name from one side + replaceMetricNameIfEqual(expr, metricNameMapping) + query = expr.Pretty(0) + + // Try to parse the query and see if it is valid. + _, err := normalEngine.NewInstantQuery(ctx, storage, nil, query, queryTime) + if err != nil { + continue + } + break + } + + t.Run(fmt.Sprintf("Query_%d", i), func(t *testing.T) { + normalQuery, err := normalEngine.NewInstantQuery(ctx, storage, &engine.QueryOpts{}, query, queryTime) + testutil.Ok(t, err) + defer normalQuery.Close() + normalResult := normalQuery.Exec(ctx) + if normalResult.Err != nil { + // Something wrong with the generated query so it even failed without projection pushdown, skipping. + return + } + testutil.Ok(t, normalResult.Err, "query: %s", query) + + optimizedQuery, err := optimizedEngine.MakeInstantQuery(ctx, storage, &engine.QueryOpts{}, query, queryTime) + testutil.Ok(t, err) + + defer optimizedQuery.Close() + optimizedQueryResult := optimizedQuery.Exec(ctx) + testutil.Ok(t, optimizedQueryResult.Err, "query: %s", query) + + if diff := cmp.Diff(normalResult, optimizedQueryResult, comparer); diff != "" { + t.Errorf("Results differ for query %s: %s", query, diff) + } + }) + } +} + +func replaceMetricNameIfEqual(node parser.Expr, metricNameMapping map[string]string) { + parser.Inspect(node, func(node parser.Node, nodes []parser.Node) error { + binOp, ok := (node).(*parser.BinaryExpr) + if !ok { + return nil + } + LHS, ok := binOp.LHS.(*parser.VectorSelector) + if !ok { + return nil + } + RHS, ok := binOp.LHS.(*parser.VectorSelector) + if !ok { + return nil + } + lhsName := extractMetricName(LHS.LabelMatchers) + rhsName := extractMetricName(RHS.LabelMatchers) + if lhsName != rhsName { + return nil + } + mappedMetricName, ok := metricNameMapping[lhsName] + if !ok { + return nil + } + for _, matcher := range RHS.LabelMatchers { + if matcher.Name == labels.MetricName { + matcher.Value = mappedMetricName + } + } + return nil + }) +} + +func extractMetricName(matchers []*labels.Matcher) string { + for _, matcher := range matchers { + if matcher.Name == labels.MetricName { + return matcher.Value + } + } + return "" +} diff --git a/internal/promql-engine/engine/sort.go b/internal/promql-engine/engine/sort.go new file mode 100644 index 00000000000..bb22a44b424 --- /dev/null +++ b/internal/promql-engine/engine/sort.go @@ -0,0 +1,188 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package engine + +import ( + "math" + + "github.com/facette/natsort" + "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/promql" + "github.com/prometheus/prometheus/promql/parser" +) + +type sortOrder bool + +const ( + sortOrderAsc sortOrder = false + sortOrderDesc sortOrder = true +) + +type resultSorter interface { + comparer(samples *promql.Vector) func(i, j int) bool + keepHistograms() bool +} + +type sortFuncResultSort struct { + sortOrder sortOrder +} + +func (s sortFuncResultSort) keepHistograms() bool { + return false +} + +type sortByLabelFuncResult struct { + sortingLabels []string + + sortOrder sortOrder +} + +func (s sortByLabelFuncResult) keepHistograms() bool { + return false +} + +type aggregateResultSort struct { + sortingLabels []string + groupBy bool + + sortOrder sortOrder +} + +type noSortResultSort struct { +} + +func (a aggregateResultSort) keepHistograms() bool { + return true +} + +func (s noSortResultSort) keepHistograms() bool { + return true +} + +func extractSortingLabels(f *parser.Call) []string { + args := f.Args[1:] + + res := make([]string, 0) + for i := range args { + res = append(res, args[i].(*parser.StringLiteral).Val) + } + return res +} + +func newResultSort(expr parser.Expr) resultSorter { + switch texpr := expr.(type) { + case *parser.Call: + switch texpr.Func.Name { + case "sort": + return sortFuncResultSort{sortOrder: sortOrderAsc} + case "sort_desc": + return sortFuncResultSort{sortOrder: sortOrderDesc} + case "sort_by_label": + return sortByLabelFuncResult{sortOrder: sortOrderAsc, sortingLabels: extractSortingLabels(texpr)} + case "sort_by_label_desc": + return sortByLabelFuncResult{sortOrder: sortOrderDesc, sortingLabels: extractSortingLabels(texpr)} + } + case *parser.AggregateExpr: + switch texpr.Op { + case parser.TOPK: + return aggregateResultSort{ + sortingLabels: texpr.Grouping, + sortOrder: sortOrderDesc, + groupBy: !texpr.Without, + } + case parser.BOTTOMK: + return aggregateResultSort{ + sortingLabels: texpr.Grouping, + sortOrder: sortOrderAsc, + groupBy: !texpr.Without, + } + case parser.LIMITK, parser.LIMIT_RATIO: + return aggregateResultSort{ + sortingLabels: texpr.Grouping, + groupBy: !texpr.Without, + } + } + } + return noSortResultSort{} +} + +func (s noSortResultSort) comparer(samples *promql.Vector) func(i, j int) bool { + return func(i, j int) bool { return i < j } +} + +func valueCompare(order sortOrder, l, r float64) bool { + if math.IsNaN(r) { + return true + } + if order == sortOrderAsc { + return l < r + } + return l > r +} + +// filterFloats filters out histogram samples from the vector in-place. +func filterFloats(v promql.Vector) promql.Vector { + floats := v[:0] + for _, s := range v { + if s.H == nil { + floats = append(floats, s) + } + } + return floats +} + +func (s sortFuncResultSort) comparer(samples *promql.Vector) func(i, j int) bool { + return func(i, j int) bool { + return valueCompare(s.sortOrder, (*samples)[i].F, (*samples)[j].F) + } +} + +func (s sortByLabelFuncResult) comparer(samples *promql.Vector) func(i, j int) bool { + return func(i, j int) bool { + iLb := labels.NewBuilder((*samples)[i].Metric) + jLb := labels.NewBuilder((*samples)[j].Metric) + + for _, label := range s.sortingLabels { + lv1 := iLb.Get(label) + lv2 := jLb.Get(label) + + if lv1 == lv2 { + continue + } + if natsort.Compare(lv1, lv2) { + return s.sortOrder == sortOrderAsc + } else { + return s.sortOrder == sortOrderDesc + } + } + // If all labels provided as arguments were equal, sort by the full label set. This ensures a consistent ordering. + if lblsCmp := labels.Compare(iLb.Labels(), jLb.Labels()); lblsCmp < 0 { + return s.sortOrder == sortOrderAsc + } else { + return s.sortOrder == sortOrderDesc + } + } +} + +func (s aggregateResultSort) comparer(samples *promql.Vector) func(i, j int) bool { + return func(i int, j int) bool { + var iLbls labels.Labels + var jLbls labels.Labels + iLb := labels.NewBuilder((*samples)[i].Metric) + jLb := labels.NewBuilder((*samples)[j].Metric) + if s.groupBy { + iLbls = iLb.Keep(s.sortingLabels...).Labels() + jLbls = jLb.Keep(s.sortingLabels...).Labels() + } else { + iLbls = iLb.Del(s.sortingLabels...).Labels() + jLbls = jLb.Del(s.sortingLabels...).Labels() + } + + lblsCmp := labels.Compare(iLbls, jLbls) + if lblsCmp != 0 { + return lblsCmp < 0 + } + return valueCompare(s.sortOrder, (*samples)[i].F, (*samples)[j].F) + } +} diff --git a/internal/promql-engine/engine/testdata/fuzz/FuzzDistributedEngineQuery/08ce8e8f11c659af b/internal/promql-engine/engine/testdata/fuzz/FuzzDistributedEngineQuery/08ce8e8f11c659af new file mode 100644 index 00000000000..274f36c26cf --- /dev/null +++ b/internal/promql-engine/engine/testdata/fuzz/FuzzDistributedEngineQuery/08ce8e8f11c659af @@ -0,0 +1,9 @@ +go test fuzz v1 +int64(-1) +uint32(0) +uint32(215) +uint32(8) +float64(-79) +float64(0.028571428571428574) +float64(37) +float64(2) diff --git a/internal/promql-engine/engine/testdata/fuzz/FuzzDistributedEngineQuery/21d9c723c3bec3a2 b/internal/promql-engine/engine/testdata/fuzz/FuzzDistributedEngineQuery/21d9c723c3bec3a2 new file mode 100644 index 00000000000..0799ad45a31 --- /dev/null +++ b/internal/promql-engine/engine/testdata/fuzz/FuzzDistributedEngineQuery/21d9c723c3bec3a2 @@ -0,0 +1,9 @@ +go test fuzz v1 +int64(-103) +uint32(0) +uint32(120) +uint32(114) +float64(96) +float64(0.3333333333333333) +float64(0.14285714285714285) +float64(26) diff --git a/internal/promql-engine/engine/testdata/fuzz/FuzzDistributedEngineQuery/b3e7a10fa9abab8e b/internal/promql-engine/engine/testdata/fuzz/FuzzDistributedEngineQuery/b3e7a10fa9abab8e new file mode 100644 index 00000000000..e7b81f35f17 --- /dev/null +++ b/internal/promql-engine/engine/testdata/fuzz/FuzzDistributedEngineQuery/b3e7a10fa9abab8e @@ -0,0 +1,9 @@ +go test fuzz v1 +int64(-163) +uint32(0) +uint32(200) +uint32(30) +float64(-472) +float64(1) +float64(0.16666666666666666) +float64(16) diff --git a/internal/promql-engine/engine/testdata/fuzz/FuzzEnginePromQLSmithInstantQuery/010a0568eb7e1f6d b/internal/promql-engine/engine/testdata/fuzz/FuzzEnginePromQLSmithInstantQuery/010a0568eb7e1f6d new file mode 100644 index 00000000000..8f9f128740b --- /dev/null +++ b/internal/promql-engine/engine/testdata/fuzz/FuzzEnginePromQLSmithInstantQuery/010a0568eb7e1f6d @@ -0,0 +1,7 @@ +go test fuzz v1 +int64(61) +uint32(2) +float64(12.166666666666666) +float64(5) +float64(1) +float64(0.4) diff --git a/internal/promql-engine/engine/testdata/fuzz/FuzzEnginePromQLSmithInstantQuery/0341fed135d1ad03 b/internal/promql-engine/engine/testdata/fuzz/FuzzEnginePromQLSmithInstantQuery/0341fed135d1ad03 new file mode 100644 index 00000000000..244a71fa307 --- /dev/null +++ b/internal/promql-engine/engine/testdata/fuzz/FuzzEnginePromQLSmithInstantQuery/0341fed135d1ad03 @@ -0,0 +1,7 @@ +go test fuzz v1 +int64(26) +uint32(170) +float64(41) +float64(1) +float64(0.2) +float64(21.714285714285715) diff --git a/internal/promql-engine/engine/testdata/fuzz/FuzzEnginePromQLSmithInstantQuery/08b1cd7987127c3f b/internal/promql-engine/engine/testdata/fuzz/FuzzEnginePromQLSmithInstantQuery/08b1cd7987127c3f new file mode 100644 index 00000000000..4a2fa234a94 --- /dev/null +++ b/internal/promql-engine/engine/testdata/fuzz/FuzzEnginePromQLSmithInstantQuery/08b1cd7987127c3f @@ -0,0 +1,7 @@ +go test fuzz v1 +int64(-203) +uint32(10) +float64(33) +float64(1) +float64(1) +float64(2) diff --git a/internal/promql-engine/engine/testdata/fuzz/FuzzEnginePromQLSmithInstantQuery/0f7a5c8026659ee3 b/internal/promql-engine/engine/testdata/fuzz/FuzzEnginePromQLSmithInstantQuery/0f7a5c8026659ee3 new file mode 100644 index 00000000000..27a4b5b52ec --- /dev/null +++ b/internal/promql-engine/engine/testdata/fuzz/FuzzEnginePromQLSmithInstantQuery/0f7a5c8026659ee3 @@ -0,0 +1,7 @@ +go test fuzz v1 +int64(-84) +uint32(4) +float64(8) +float64(-12) +float64(9.166666666666666) +float64(103) diff --git a/internal/promql-engine/engine/testdata/fuzz/FuzzEnginePromQLSmithInstantQuery/1a5331aef5ebd0a3 b/internal/promql-engine/engine/testdata/fuzz/FuzzEnginePromQLSmithInstantQuery/1a5331aef5ebd0a3 new file mode 100644 index 00000000000..a801a6778e2 --- /dev/null +++ b/internal/promql-engine/engine/testdata/fuzz/FuzzEnginePromQLSmithInstantQuery/1a5331aef5ebd0a3 @@ -0,0 +1,7 @@ +go test fuzz v1 +int64(63) +uint32(33) +float64(46) +float64(0.2) +float64(1) +float64(21) diff --git a/internal/promql-engine/engine/testdata/fuzz/FuzzEnginePromQLSmithInstantQuery/259abf7357a0eac3 b/internal/promql-engine/engine/testdata/fuzz/FuzzEnginePromQLSmithInstantQuery/259abf7357a0eac3 new file mode 100644 index 00000000000..9e46af0a203 --- /dev/null +++ b/internal/promql-engine/engine/testdata/fuzz/FuzzEnginePromQLSmithInstantQuery/259abf7357a0eac3 @@ -0,0 +1,7 @@ +go test fuzz v1 +int64(26) +uint32(170) +float64(41) +float64(51) +float64(0.2) +float64(21.714285714285715) diff --git a/internal/promql-engine/engine/testdata/fuzz/FuzzEnginePromQLSmithInstantQuery/277074908c5a5f75 b/internal/promql-engine/engine/testdata/fuzz/FuzzEnginePromQLSmithInstantQuery/277074908c5a5f75 new file mode 100644 index 00000000000..aa6edcbd825 --- /dev/null +++ b/internal/promql-engine/engine/testdata/fuzz/FuzzEnginePromQLSmithInstantQuery/277074908c5a5f75 @@ -0,0 +1,7 @@ +go test fuzz v1 +int64(37) +uint32(44) +float64(600) +float64(8) +float64(10) +float64(28) diff --git a/internal/promql-engine/engine/testdata/fuzz/FuzzEnginePromQLSmithInstantQuery/61a4f8d0ac706041 b/internal/promql-engine/engine/testdata/fuzz/FuzzEnginePromQLSmithInstantQuery/61a4f8d0ac706041 new file mode 100644 index 00000000000..2209ea7ab0f --- /dev/null +++ b/internal/promql-engine/engine/testdata/fuzz/FuzzEnginePromQLSmithInstantQuery/61a4f8d0ac706041 @@ -0,0 +1,7 @@ +go test fuzz v1 +int64(166) +uint32(7) +float64(60) +float64(8) +float64(11) +float64(2) diff --git a/internal/promql-engine/engine/testdata/fuzz/FuzzEnginePromQLSmithInstantQuery/95a37f4fde1b21a5 b/internal/promql-engine/engine/testdata/fuzz/FuzzEnginePromQLSmithInstantQuery/95a37f4fde1b21a5 new file mode 100644 index 00000000000..c2fb9ca9449 --- /dev/null +++ b/internal/promql-engine/engine/testdata/fuzz/FuzzEnginePromQLSmithInstantQuery/95a37f4fde1b21a5 @@ -0,0 +1,7 @@ +go test fuzz v1 +int64(63) +uint32(33) +float64(46) +float64(2) +float64(1) +float64(21) diff --git a/internal/promql-engine/engine/testdata/fuzz/FuzzEnginePromQLSmithInstantQuery/ec52f38b0d05e53d b/internal/promql-engine/engine/testdata/fuzz/FuzzEnginePromQLSmithInstantQuery/ec52f38b0d05e53d new file mode 100644 index 00000000000..c815497318d --- /dev/null +++ b/internal/promql-engine/engine/testdata/fuzz/FuzzEnginePromQLSmithInstantQuery/ec52f38b0d05e53d @@ -0,0 +1,7 @@ +go test fuzz v1 +int64(-143) +uint32(41) +float64(0.2) +float64(6) +float64(9) +float64(60) diff --git a/internal/promql-engine/engine/testdata/fuzz/FuzzEnginePromQLSmithInstantQuery/f3b6f63d9f0c6c39 b/internal/promql-engine/engine/testdata/fuzz/FuzzEnginePromQLSmithInstantQuery/f3b6f63d9f0c6c39 new file mode 100644 index 00000000000..d183e2410e5 --- /dev/null +++ b/internal/promql-engine/engine/testdata/fuzz/FuzzEnginePromQLSmithInstantQuery/f3b6f63d9f0c6c39 @@ -0,0 +1,7 @@ +go test fuzz v1 +int64(-123) +uint32(35) +float64(51) +float64(-74) +float64(1) +float64(2) diff --git a/internal/promql-engine/engine/testdata/fuzz/FuzzEnginePromQLSmithRangeQuery/010afeeed13bbcc2 b/internal/promql-engine/engine/testdata/fuzz/FuzzEnginePromQLSmithRangeQuery/010afeeed13bbcc2 new file mode 100644 index 00000000000..45f14e77341 --- /dev/null +++ b/internal/promql-engine/engine/testdata/fuzz/FuzzEnginePromQLSmithRangeQuery/010afeeed13bbcc2 @@ -0,0 +1,10 @@ +go test fuzz v1 +int64(20) +uint32(0) +uint32(120) +uint32(109) +float64(73) +float64(10) +float64(7) +float64(2) +int(71) diff --git a/internal/promql-engine/engine/testdata/fuzz/FuzzEnginePromQLSmithRangeQuery/59c2955b78c86bab b/internal/promql-engine/engine/testdata/fuzz/FuzzEnginePromQLSmithRangeQuery/59c2955b78c86bab new file mode 100644 index 00000000000..ae1855a2708 --- /dev/null +++ b/internal/promql-engine/engine/testdata/fuzz/FuzzEnginePromQLSmithRangeQuery/59c2955b78c86bab @@ -0,0 +1,10 @@ +go test fuzz v1 +int64(-7) +uint32(14) +uint32(147) +uint32(4) +float64(7.3) +float64(16.703703703703706) +float64(7) +float64(20) +int(71) diff --git a/internal/promql-engine/engine/testdata/fuzz/FuzzNativeHistogramQuery/04c926f3bdeff7b0 b/internal/promql-engine/engine/testdata/fuzz/FuzzNativeHistogramQuery/04c926f3bdeff7b0 new file mode 100644 index 00000000000..929c4d43058 --- /dev/null +++ b/internal/promql-engine/engine/testdata/fuzz/FuzzNativeHistogramQuery/04c926f3bdeff7b0 @@ -0,0 +1,10 @@ +go test fuzz v1 +int64(-134) +uint32(0) +uint32(197) +uint32(4) +int8(0) +int8(0) +uint64(1) +uint64(2) +uint64(63) diff --git a/internal/promql-engine/engine/testdata/fuzz/FuzzNativeHistogramQuery/07a2aa0f5786055d b/internal/promql-engine/engine/testdata/fuzz/FuzzNativeHistogramQuery/07a2aa0f5786055d new file mode 100644 index 00000000000..86858f4e431 --- /dev/null +++ b/internal/promql-engine/engine/testdata/fuzz/FuzzNativeHistogramQuery/07a2aa0f5786055d @@ -0,0 +1,10 @@ +go test fuzz v1 +int64(-134) +uint32(0) +uint32(197) +uint32(4) +int8(0) +int8(0) +uint64(1) +uint64(1) +uint64(63) diff --git a/internal/promql-engine/engine/testdata/fuzz/FuzzNativeHistogramQuery/313842d99fde393f b/internal/promql-engine/engine/testdata/fuzz/FuzzNativeHistogramQuery/313842d99fde393f new file mode 100644 index 00000000000..b40dffe9802 --- /dev/null +++ b/internal/promql-engine/engine/testdata/fuzz/FuzzNativeHistogramQuery/313842d99fde393f @@ -0,0 +1,10 @@ +go test fuzz v1 +int64(-134) +uint32(0) +uint32(172) +uint32(4) +int8(0) +int8(0) +uint64(1) +uint64(1) +uint64(63) diff --git a/internal/promql-engine/engine/testdata/fuzz/FuzzNativeHistogramQuery/531dcf7d644da227 b/internal/promql-engine/engine/testdata/fuzz/FuzzNativeHistogramQuery/531dcf7d644da227 new file mode 100644 index 00000000000..0294c3ec751 --- /dev/null +++ b/internal/promql-engine/engine/testdata/fuzz/FuzzNativeHistogramQuery/531dcf7d644da227 @@ -0,0 +1,10 @@ +go test fuzz v1 +int64(-44) +uint32(0) +uint32(144) +uint32(3) +int8(0) +int8(0) +uint64(0) +uint64(14) +uint64(95) diff --git a/internal/promql-engine/engine/testdata/fuzz/FuzzNativeHistogramQuery/aea9c02480c8be8a b/internal/promql-engine/engine/testdata/fuzz/FuzzNativeHistogramQuery/aea9c02480c8be8a new file mode 100644 index 00000000000..a7e56c4f682 --- /dev/null +++ b/internal/promql-engine/engine/testdata/fuzz/FuzzNativeHistogramQuery/aea9c02480c8be8a @@ -0,0 +1,10 @@ +go test fuzz v1 +int64(-2) +uint32(83) +uint32(160) +uint32(76) +int8(0) +int8(-2) +uint64(27) +uint64(2) +uint64(1) diff --git a/internal/promql-engine/engine/testdata/fuzz/FuzzNativeHistogramQuery/b8509c2646902b89 b/internal/promql-engine/engine/testdata/fuzz/FuzzNativeHistogramQuery/b8509c2646902b89 new file mode 100644 index 00000000000..24f54c37237 --- /dev/null +++ b/internal/promql-engine/engine/testdata/fuzz/FuzzNativeHistogramQuery/b8509c2646902b89 @@ -0,0 +1,10 @@ +go test fuzz v1 +int64(22) +uint32(0) +uint32(96) +uint32(113) +int8(0) +int8(-4) +uint64(80) +uint64(2) +uint64(1) diff --git a/internal/promql-engine/engine/testdata/fuzz/FuzzNativeHistogramQuery/d50044a9f219c24a b/internal/promql-engine/engine/testdata/fuzz/FuzzNativeHistogramQuery/d50044a9f219c24a new file mode 100644 index 00000000000..8b154cc1183 --- /dev/null +++ b/internal/promql-engine/engine/testdata/fuzz/FuzzNativeHistogramQuery/d50044a9f219c24a @@ -0,0 +1,10 @@ +go test fuzz v1 +int64(-101) +uint32(0) +uint32(61) +uint32(120) +int8(0) +int8(0) +uint64(22) +uint64(1) +uint64(3) diff --git a/internal/promql-engine/engine/testdata/fuzz/FuzzNativeHistogramQuery/df60c8694f1c9759 b/internal/promql-engine/engine/testdata/fuzz/FuzzNativeHistogramQuery/df60c8694f1c9759 new file mode 100644 index 00000000000..e3baa6eb3b2 --- /dev/null +++ b/internal/promql-engine/engine/testdata/fuzz/FuzzNativeHistogramQuery/df60c8694f1c9759 @@ -0,0 +1,10 @@ +go test fuzz v1 +int64(-57) +uint32(27) +uint32(248) +uint32(12) +int8(0) +int8(0) +uint64(1) +uint64(1) +uint64(16) diff --git a/internal/promql-engine/engine/testdata/fuzz/FuzzNativeHistogramQuery/fccf25699b4fc3f7 b/internal/promql-engine/engine/testdata/fuzz/FuzzNativeHistogramQuery/fccf25699b4fc3f7 new file mode 100644 index 00000000000..4f2249230d8 --- /dev/null +++ b/internal/promql-engine/engine/testdata/fuzz/FuzzNativeHistogramQuery/fccf25699b4fc3f7 @@ -0,0 +1,10 @@ +go test fuzz v1 +int64(-134) +uint32(0) +uint32(79) +uint32(3) +int8(3) +int8(0) +uint64(132) +uint64(79) +uint64(70) diff --git a/internal/promql-engine/engine/user_defined_test.go b/internal/promql-engine/engine/user_defined_test.go new file mode 100644 index 00000000000..915938d0469 --- /dev/null +++ b/internal/promql-engine/engine/user_defined_test.go @@ -0,0 +1,131 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package engine_test + +import ( + "context" + "slices" + "testing" + "time" + + "github.com/thanos-io/promql-engine/engine" + "github.com/thanos-io/promql-engine/execution/model" + "github.com/thanos-io/promql-engine/logicalplan" + "github.com/thanos-io/promql-engine/query" + + "github.com/efficientgo/core/testutil" + "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/promql" + "github.com/prometheus/prometheus/promql/promqltest" + "github.com/prometheus/prometheus/storage" + "github.com/prometheus/prometheus/util/annotations" +) + +func TestUserDefinedOperators(t *testing.T) { + opts := promql.EngineOpts{ + Timeout: 1 * time.Hour, + MaxSamples: 1e10, + } + + load := ` +load 30s + http_requests_total{container="a"} 1x30 + http_requests_total{container="b"} 2x30` + + storage := promqltest.LoadedStorage(t, load) + defer storage.Close() + + newEngine := engine.New(engine.Opts{ + EngineOpts: opts, + LogicalOptimizers: append(slices.Clone(logicalplan.DefaultOptimizers), &injectVectorSelector{}), + }) + query := "sum(http_requests_total)" + qry, err := newEngine.NewRangeQuery(context.Background(), storage, nil, query, time.Unix(0, 0), time.Unix(90, 0), 30*time.Second) + testutil.Ok(t, err) + + result := qry.Exec(context.Background()) + testutil.Ok(t, result.Err) + + expected := promql.Matrix{ + promql.Series{ + Metric: labels.EmptyLabels(), + Floats: []promql.FPoint{{T: 0, F: 14}, {T: 30000, F: 14}, {T: 60000, F: 14}, {T: 90000, F: 14}}, + }, + } + mat, err := result.Matrix() + testutil.Ok(t, err) + testutil.Equals(t, expected, mat) +} + +type injectVectorSelector struct{} + +func (i injectVectorSelector) Optimize(plan logicalplan.Node, _ *query.Options) (logicalplan.Node, annotations.Annotations) { + logicalplan.TraverseBottomUp(nil, &plan, func(_, current *logicalplan.Node) bool { + switch t := (*current).(type) { + case *logicalplan.VectorSelector: + *current = &logicalVectorSelector{ + VectorSelector: t, + } + } + return false + }) + return plan, nil +} + +type logicalVectorSelector struct { + *logicalplan.VectorSelector +} + +func (c logicalVectorSelector) MakeExecutionOperator(_ context.Context, opts *query.Options, _ storage.SelectHints) (model.VectorOperator, error) { + oper := &vectorSelectorOperator{ + stepsBatch: opts.StepsBatch, + + mint: opts.Start.UnixMilli(), + maxt: opts.End.UnixMilli(), + step: opts.Step.Milliseconds(), + currentStep: opts.Start.UnixMilli(), + } + + return oper, nil +} + +func (c vectorSelectorOperator) String() string { + return "logicalVectorSelector" +} + +type vectorSelectorOperator struct { + stepsBatch int + + mint int64 + maxt int64 + step int64 + currentStep int64 +} + +func (c *vectorSelectorOperator) Next(ctx context.Context, buf []model.StepVector) (int, error) { + if c.currentStep > c.maxt { + return 0, nil + } + + n := 0 + for i := 0; i < c.stepsBatch && c.currentStep <= c.maxt && n < len(buf); i++ { + buf[n].Reset(c.currentStep) + buf[n].AppendSample(1, 7) + buf[n].AppendSample(2, 7) + c.currentStep += c.step + n++ + } + return n, nil +} + +func (c *vectorSelectorOperator) Series(ctx context.Context) ([]labels.Labels, error) { + return []labels.Labels{ + labels.FromStrings(labels.MetricName, "http_requests_total", "container", "a"), + labels.FromStrings(labels.MetricName, "http_requests_total", "container", "b"), + }, nil +} + +func (c *vectorSelectorOperator) Explain() (next []model.VectorOperator) { + return nil +} diff --git a/internal/promql-engine/execution/aggregate/count_values.go b/internal/promql-engine/execution/aggregate/count_values.go new file mode 100644 index 00000000000..57b31b72c4c --- /dev/null +++ b/internal/promql-engine/execution/aggregate/count_values.go @@ -0,0 +1,208 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package aggregate + +import ( + "context" + "fmt" + "slices" + "strconv" + "sync" + + "github.com/thanos-io/promql-engine/execution/model" + "github.com/thanos-io/promql-engine/execution/telemetry" + "github.com/thanos-io/promql-engine/query" + + "github.com/efficientgo/core/errors" + prommodel "github.com/prometheus/common/model" + "github.com/prometheus/prometheus/model/labels" +) + +type countValuesOperator struct { + next model.VectorOperator + param string + + by bool + grouping []string + + stepsBatch int + curStep int + + ts []int64 + counts []map[int]int + series []labels.Labels + + once sync.Once + tempBuf []model.StepVector +} + +func NewCountValues(next model.VectorOperator, param string, by bool, grouping []string, opts *query.Options) model.VectorOperator { + // Grouping labels need to be sorted in order for metric hashing to work. + // https://github.com/prometheus/prometheus/blob/8ed39fdab1ead382a354e45ded999eb3610f8d5f/model/labels/labels.go#L162-L181 + slices.Sort(grouping) + + op := &countValuesOperator{ + next: next, + param: param, + stepsBatch: opts.StepsBatch, + by: by, + grouping: grouping, + } + return telemetry.NewOperator(telemetry.NewTelemetry(op, opts), op) +} + +func (c *countValuesOperator) Explain() []model.VectorOperator { + return []model.VectorOperator{c.next} +} + +func (c *countValuesOperator) String() string { + if c.by { + return fmt.Sprintf("[countValues] by (%v) - param (%v)", c.grouping, c.param) + } + return fmt.Sprintf("[countValues] without (%v) - param (%v)", c.grouping, c.param) +} + +func (c *countValuesOperator) Series(ctx context.Context) ([]labels.Labels, error) { + var err error + c.once.Do(func() { err = c.initSeriesOnce(ctx) }) + return c.series, err +} + +func (c *countValuesOperator) Next(ctx context.Context, buf []model.StepVector) (int, error) { + select { + case <-ctx.Done(): + return 0, ctx.Err() + default: + } + + var err error + c.once.Do(func() { err = c.initSeriesOnce(ctx) }) + if err != nil { + return 0, err + } + + if c.curStep >= len(c.ts) { + return 0, nil + } + + n := 0 + maxSteps := min(c.stepsBatch, len(buf)) + + for range maxSteps { + if c.curStep >= len(c.ts) { + break + } + buf[n] = model.StepVector{T: c.ts[c.curStep]} + for id, v := range c.counts[c.curStep] { + buf[n].AppendSample(uint64(id), float64(v)) + } + c.curStep++ + n++ + } + return n, nil +} + +func (c *countValuesOperator) initSeriesOnce(ctx context.Context) error { + if !prommodel.UTF8Validation.IsValidLabelName(c.param) { + return errors.Newf("invalid label name %q", c.param) + } + + nextSeries, err := c.next.Series(ctx) + if err != nil { + return err + } + + // Allocate outer slice for buffer; inner slices will be allocated by child operators + // or grow on demand. + c.tempBuf = make([]model.StepVector, c.stepsBatch) + + var ( + inputIdToHashBucket = make(map[int]uint64) + hashToBucketLabels = make(map[uint64]labels.Labels) + hashToOutputId = make(map[uint64]int) + + hashingBuf = make([]byte, 1024) + builder labels.ScratchBuilder + labelsMap = make(map[string]struct{}) + ) + for _, lblName := range c.grouping { + labelsMap[lblName] = struct{}{} + } + for i := range nextSeries { + hash, lbls := hashMetric(builder, nextSeries[i], !c.by, c.grouping, labelsMap, hashingBuf) + inputIdToHashBucket[i] = hash + if _, ok := hashToBucketLabels[hash]; !ok { + hashToBucketLabels[hash] = lbls + } + } + + ts := make([]int64, 0) + counts := make([]map[int]int, 0) + series := make([]labels.Labels, 0) + + b := labels.NewBuilder(labels.EmptyLabels()) + for { + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + + n, err := c.next.Next(ctx, c.tempBuf) + if err != nil { + return err + } + if n == 0 { + break + } + in := c.tempBuf[:n] + for i := range in { + ts = append(ts, in[i].T) + countPerHashbucket := make(map[uint64]map[string]int, len(inputIdToHashBucket)) + for j := range in[i].Samples { + hash := inputIdToHashBucket[int(in[i].SampleIDs[j])] + if _, ok := countPerHashbucket[hash]; !ok { + countPerHashbucket[hash] = make(map[string]int) + } + // Using string as the key to the map so that -0 and 0 are treated as separate values. + fStr := strconv.FormatFloat(in[i].Samples[j], 'f', -1, 64) + countPerHashbucket[hash][fStr]++ + } + + for j := range in[i].Histograms { + hash := inputIdToHashBucket[int(in[i].HistogramIDs[j])] + if _, ok := countPerHashbucket[hash]; !ok { + countPerHashbucket[hash] = make(map[string]int) + } + // Using string as the key to the map so that -0 and 0 are treated as separate values. + fStr := in[i].Histograms[j].String() + countPerHashbucket[hash][fStr]++ + } + + countsPerOutputId := make(map[int]int) + for hash, counts := range countPerHashbucket { + b.Reset(hashToBucketLabels[hash]) + for f, count := range counts { + // TODO: Probably we should issue a warning if we override a label here + lbls := b.Set(c.param, f).Labels() + hash := lbls.Hash() + outputId, ok := hashToOutputId[hash] + if !ok { + series = append(series, lbls) + outputId = len(series) - 1 + hashToOutputId[hash] = outputId + } + countsPerOutputId[outputId] += count + } + } + counts = append(counts, countsPerOutputId) + } + } + + c.ts = ts + c.counts = counts + c.series = series + + return nil +} diff --git a/internal/promql-engine/execution/aggregate/hashaggregate.go b/internal/promql-engine/execution/aggregate/hashaggregate.go new file mode 100644 index 00000000000..05af86ddb98 --- /dev/null +++ b/internal/promql-engine/execution/aggregate/hashaggregate.go @@ -0,0 +1,274 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package aggregate + +import ( + "context" + "fmt" + "math" + "sync" + + "github.com/thanos-io/promql-engine/execution/model" + "github.com/thanos-io/promql-engine/execution/parse" + "github.com/thanos-io/promql-engine/execution/telemetry" + "github.com/thanos-io/promql-engine/query" + "github.com/thanos-io/promql-engine/warnings" + + "github.com/efficientgo/core/errors" + "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/promql/parser" + "github.com/prometheus/prometheus/promql/parser/posrange" + "github.com/prometheus/prometheus/util/annotations" + "golang.org/x/exp/slices" +) + +type aggregate struct { + next model.VectorOperator + paramOp model.VectorOperator + by bool + labels []string + aggregation parser.ItemType + stepsBatch int + + once sync.Once + series []labels.Labels + tables []aggregateTable + params []float64 + + lastBatch []model.StepVector + tempBuf []model.StepVector + paramBuf []model.StepVector + lastBatchBuf []model.StepVector + inputSeriesCount int +} + +func NewHashAggregate( + next model.VectorOperator, + paramOp model.VectorOperator, + aggregation parser.ItemType, + by bool, + labels []string, + opts *query.Options, +) (model.VectorOperator, error) { + // Verify that the aggregation is supported. + if _, err := newScalarAccumulator(aggregation); err != nil { + return nil, err + } + + // Grouping labels need to be sorted in order for metric hashing to work. + // https://github.com/prometheus/prometheus/blob/8ed39fdab1ead382a354e45ded999eb3610f8d5f/model/labels/labels.go#L162-L181 + slices.Sort(labels) + a := &aggregate{ + next: next, + paramOp: paramOp, + by: by, + labels: labels, + aggregation: aggregation, + stepsBatch: opts.StepsBatch, + params: make([]float64, opts.StepsBatch), + } + + return telemetry.NewOperator(telemetry.NewTelemetry(a, opts), a), nil +} + +func (a *aggregate) String() string { + if a.by { + return fmt.Sprintf("[aggregate] %v by (%v)", a.aggregation.String(), a.labels) + } + return fmt.Sprintf("[aggregate] %v without (%v)", a.aggregation.String(), a.labels) +} + +func (a *aggregate) Explain() (next []model.VectorOperator) { + switch a.aggregation { + case parser.QUANTILE: + return []model.VectorOperator{a.paramOp, a.next} + default: + return []model.VectorOperator{a.next} + } +} + +func (a *aggregate) Series(ctx context.Context) ([]labels.Labels, error) { + var err error + a.once.Do(func() { err = a.initializeTables(ctx) }) + if err != nil { + return nil, err + } + return a.series, nil +} + +func (a *aggregate) Next(ctx context.Context, buf []model.StepVector) (int, error) { + select { + case <-ctx.Done(): + return 0, ctx.Err() + default: + } + + var err error + a.once.Do(func() { err = a.initializeTables(ctx) }) + if err != nil { + return 0, err + } + + if a.paramOp != nil { + n, err := a.paramOp.Next(ctx, a.paramBuf) + if err != nil { + return 0, err + } + for i := range n { + a.params[i] = a.paramBuf[i].Samples[0] + if sample := a.params[i]; math.IsNaN(sample) || sample < 0 || sample > 1 { + warnings.AddToContext(annotations.NewInvalidQuantileWarning(sample, posrange.PositionRange{}), ctx) + } + } + } + + for i, p := range a.params { + a.tables[i].reset(p) + } + + // Track how many tables are populated during aggregation. + numTables := 0 + if a.lastBatch != nil { + numTables = len(a.lastBatch) + if warn := a.aggregate(a.lastBatch); warn != nil { + warnings.AddToContext(warn, ctx) + } + a.lastBatch = nil + } + + for { + n, err := a.next.Next(ctx, a.tempBuf) + if err != nil { + return 0, err + } + if n == 0 { + break + } + next := a.tempBuf[:n] + // Keep aggregating samples as long as timestamps of batches are equal. + currentTs := a.tables[0].timestamp() + if currentTs == math.MinInt64 || next[0].T == currentTs { + numTables = n + if warn := a.aggregate(next); warn != nil { + warnings.AddToContext(warn, ctx) + } + continue + } + a.lastBatch = a.lastBatchBuf[:n] + copy(a.lastBatch, next) + break + } + + n := min(numTables, len(buf)) + for i := range n { + buf[i].Reset(a.tables[i].timestamp()) + a.tables[i].populateVector(ctx, &buf[i]) + } + return n, nil +} + +func (a *aggregate) aggregate(in []model.StepVector) error { + var err error + for i, vector := range in { + err = warnings.Coalesce(err, a.tables[i].aggregate(vector)) + } + return err +} + +func (a *aggregate) initializeTables(ctx context.Context) error { + var ( + tables []aggregateTable + series []labels.Labels + err error + ) + + if a.by && len(a.labels) == 0 { + tables, series, err = a.initializeVectorizedTables(ctx) + } else { + tables, series, err = a.initializeScalarTables(ctx) + } + if err != nil { + return err + } + a.tables = tables + a.series = series + + // Allocate outer slice for buffers; inner slices will be allocated by child operators + // or grow on demand. This avoids over-allocation when aggregating many series to few. + a.tempBuf = make([]model.StepVector, a.stepsBatch) + a.lastBatchBuf = make([]model.StepVector, a.stepsBatch) + if a.paramOp != nil { + a.paramBuf = make([]model.StepVector, len(a.params)) + } + + return nil +} + +func (a *aggregate) initializeVectorizedTables(ctx context.Context) ([]aggregateTable, []labels.Labels, error) { + // perform initialization of the underlying operator even if we are aggregating the labels away + series, err := a.next.Series(ctx) + if err != nil { + return nil, nil, err + } + a.inputSeriesCount = len(series) + tables, err := newVectorizedTables(a.stepsBatch, a.aggregation) + if errors.Is(err, parse.ErrNotSupportedExpr) { + return a.initializeScalarTables(ctx) + } + + if err != nil { + return nil, nil, err + } + + return tables, []labels.Labels{{}}, nil +} + +func (a *aggregate) initializeScalarTables(ctx context.Context) ([]aggregateTable, []labels.Labels, error) { + series, err := a.next.Series(ctx) + if err != nil { + return nil, nil, err + } + a.inputSeriesCount = len(series) + var ( + // inputCache is an index from input seriesID to output seriesID. + inputCache = make([]uint64, len(series)) + // outputMap is used to map from the hash of an input series to an output series. + outputMap = make(map[uint64]*model.Series) + // outputCache is an index from output seriesID to output series. + outputCache = make([]*model.Series, 0) + // hashingBuf is a reusable buffer for hashing input series. + hashingBuf = make([]byte, 1024) + // builder is a reusable labels builder for output series. + builder labels.ScratchBuilder + ) + labelsMap := make(map[string]struct{}) + for _, lblName := range a.labels { + labelsMap[lblName] = struct{}{} + } + for i := range series { + hash, lbls := hashMetric(builder, series[i], !a.by, a.labels, labelsMap, hashingBuf) + output, ok := outputMap[hash] + if !ok { + output = &model.Series{ + Metric: lbls, + ID: uint64(len(outputCache)), + } + outputMap[hash] = output + outputCache = append(outputCache, output) + } + + inputCache[i] = output.ID + } + tables, err := newScalarTables(a.stepsBatch, inputCache, outputCache, a.aggregation) + if err != nil { + return nil, nil, err + } + + series = make([]labels.Labels, len(outputCache)) + for i := range outputCache { + series[i] = outputCache[i].Metric + } + + return tables, series, nil +} diff --git a/internal/promql-engine/execution/aggregate/khashaggregate.go b/internal/promql-engine/execution/aggregate/khashaggregate.go new file mode 100644 index 00000000000..e1c825a08fe --- /dev/null +++ b/internal/promql-engine/execution/aggregate/khashaggregate.go @@ -0,0 +1,408 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package aggregate + +import ( + "container/heap" + "context" + "fmt" + "math" + "sort" + "sync" + + "github.com/thanos-io/promql-engine/execution/model" + "github.com/thanos-io/promql-engine/execution/telemetry" + "github.com/thanos-io/promql-engine/query" + "github.com/thanos-io/promql-engine/warnings" + + "github.com/efficientgo/core/errors" + "github.com/prometheus/prometheus/model/histogram" + "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/promql/parser" + "github.com/prometheus/prometheus/promql/parser/posrange" + "github.com/prometheus/prometheus/util/annotations" + "golang.org/x/exp/slices" +) + +type kAggregate struct { + next model.VectorOperator + paramOp model.VectorOperator + by bool + labels []string + aggregation parser.ItemType + stepsBatch int + compare func(float64, float64) bool + + once sync.Once + series []labels.Labels + inputToHeap []*samplesHeap + heaps []*samplesHeap + params []float64 + + tempBuf []model.StepVector + paramBuf []model.StepVector +} + +func NewKHashAggregate( + next model.VectorOperator, + paramOp model.VectorOperator, + aggregation parser.ItemType, + by bool, + labels []string, + opts *query.Options, +) (model.VectorOperator, error) { + var compare func(float64, float64) bool + + if aggregation == parser.TOPK { + compare = func(f float64, s float64) bool { + return f < s + } + } else if aggregation == parser.BOTTOMK { + compare = func(f float64, s float64) bool { + return s < f + } + } else if aggregation != parser.LIMITK && aggregation != parser.LIMIT_RATIO { + return nil, errors.Newf("Unsupported aggregate expression: %v", aggregation) + } + // Grouping labels need to be sorted in order for metric hashing to work. + // https://github.com/prometheus/prometheus/blob/8ed39fdab1ead382a354e45ded999eb3610f8d5f/model/labels/labels.go#L162-L181 + slices.Sort(labels) + + op := &kAggregate{ + next: next, + by: by, + aggregation: aggregation, + labels: labels, + paramOp: paramOp, + compare: compare, + params: make([]float64, opts.StepsBatch), + stepsBatch: opts.StepsBatch, + } + + return telemetry.NewOperator(telemetry.NewTelemetry(op, opts), op), nil +} + +func (a *kAggregate) Next(ctx context.Context, buf []model.StepVector) (int, error) { + select { + case <-ctx.Done(): + return 0, ctx.Err() + default: + } + + var err error + a.once.Do(func() { err = a.init(ctx) }) + if err != nil { + return 0, err + } + + nIn, err := a.next.Next(ctx, a.tempBuf) + if err != nil { + return 0, err + } + if nIn == 0 { + return 0, nil + } + in := a.tempBuf[:nIn] + + nParam, err := a.paramOp.Next(ctx, a.paramBuf) + if err != nil { + return 0, err + } + + for i := range nParam { + a.params[i] = a.paramBuf[i].Samples[0] + val := a.params[i] + + switch a.aggregation { + case parser.TOPK, parser.BOTTOMK, parser.LIMITK: + if math.IsNaN(val) { + return 0, errors.New("Parameter value is NaN") + } + if val > math.MaxInt64 { + return 0, errors.Newf("Scalar value %v overflows int64", val) + } + if val < math.MinInt64 { + return 0, errors.Newf("Scalar value %v underflows int64", val) + } + case parser.LIMIT_RATIO: + if math.IsNaN(val) { + return 0, errors.Newf("Ratio value is NaN") + } + switch { + case val < -1.0: + val = -1.0 + warnings.AddToContext(annotations.NewInvalidRatioWarning(a.params[i], val, posrange.PositionRange{}), ctx) + case val > 1.0: + val = 1.0 + warnings.AddToContext(annotations.NewInvalidRatioWarning(a.params[i], val, posrange.PositionRange{}), ctx) + } + a.params[i] = val + } + } + + n := 0 + for i := 0; i < nIn && n < len(buf); i++ { + vector := in[i] + // Skip steps where the argument is less than or equal to 0, limit_ratio is an exception. + if (a.aggregation != parser.LIMIT_RATIO && int(a.params[i]) <= 0) || (a.aggregation == parser.LIMIT_RATIO && a.params[i] == 0) { + buf[n] = model.StepVector{T: vector.T} + n++ + continue + } + if a.aggregation != parser.LIMITK && a.aggregation != parser.LIMIT_RATIO && len(vector.Histograms) > 0 { + warnings.AddToContext(annotations.NewHistogramIgnoredInAggregationInfo(a.aggregation.String(), posrange.PositionRange{}), ctx) + } + + var k int + var ratio float64 + + if a.aggregation == parser.LIMIT_RATIO { + ratio = a.params[i] + } else { + k = int(a.params[i]) + } + + buf[n].Reset(vector.T) + a.aggregate(&buf[n], k, ratio, vector.SampleIDs, vector.Samples, vector.HistogramIDs, vector.Histograms) + n++ + } + + return n, nil +} + +func (a *kAggregate) Series(ctx context.Context) ([]labels.Labels, error) { + var err error + a.once.Do(func() { err = a.init(ctx) }) + if err != nil { + return nil, err + } + return a.series, nil +} + +func (a *kAggregate) String() string { + if a.by { + return fmt.Sprintf("[kaggregate] %v by (%v)", a.aggregation.String(), a.labels) + } + return fmt.Sprintf("[kaggregate] %v without (%v)", a.aggregation.String(), a.labels) +} + +func (a *kAggregate) Explain() (next []model.VectorOperator) { + return []model.VectorOperator{a.paramOp, a.next} +} + +func (a *kAggregate) init(ctx context.Context) error { + series, err := a.next.Series(ctx) + if err != nil { + return err + } + var ( + // heapsHash is a map of hash of the series to output samples heap for that series. + heapsHash = make(map[uint64]*samplesHeap) + // hashingBuf is a buffer used for metric hashing. + hashingBuf = make([]byte, 1024) + // builder is a scratch builder used for creating output series. + builder labels.ScratchBuilder + ) + labelsMap := make(map[string]struct{}) + for _, lblName := range a.labels { + labelsMap[lblName] = struct{}{} + } + for i := range series { + hash, _ := hashMetric(builder, series[i], !a.by, a.labels, labelsMap, hashingBuf) + h, ok := heapsHash[hash] + if !ok { + h = &samplesHeap{compare: a.compare} + heapsHash[hash] = h + a.heaps = append(a.heaps, h) + } + a.inputToHeap = append(a.inputToHeap, h) + } + a.series = series + + // Allocate outer slice for buffers; inner slices will be allocated by child operators + // or grow on demand. This avoids over-allocation when aggregating many series to few. + a.tempBuf = make([]model.StepVector, a.stepsBatch) + a.paramBuf = make([]model.StepVector, a.stepsBatch) + + return nil +} + +// aggregates based on the given parameter k (or ratio for limit_ratio) and timeseries, supported aggregation are +// topk: gives the 'k' largest element based on the sample values +// bottomk: gives the 'k' smallest element based on the sample values +// limitk: samples the first 'k' element from the given timeseries (has native histogram support) +// limit_ratio: deterministically samples out the 'ratio' amount of the samples from the given timeseries (also has native histogram support). +func (a *kAggregate) aggregate(out *model.StepVector, k int, ratio float64, sampleIDs []uint64, samples []float64, histogramIDs []uint64, histograms []*histogram.FloatHistogram) { + groupsRemaining := len(a.heaps) + + switch a.aggregation { + case parser.TOPK, parser.BOTTOMK: + for i, sId := range sampleIDs { + sampleHeap := a.inputToHeap[sId] + switch { + case sampleHeap.Len() < k: + heap.Push(sampleHeap, &entry{sId: sId, total: samples[i]}) + + case sampleHeap.compare(sampleHeap.entries[0].total, samples[i]) || (math.IsNaN(sampleHeap.entries[0].total) && !math.IsNaN(samples[i])): + sampleHeap.entries[0].sId = sId + sampleHeap.entries[0].total = samples[i] + + if k > 1 { + heap.Fix(sampleHeap, 0) + } + } + } + + case parser.LIMITK: + if len(histogramIDs) == 0 { + for i, sId := range sampleIDs { + sampleHeap := a.inputToHeap[sId] + if sampleHeap.Len() < k { + heap.Push(sampleHeap, &entry{sId: sId, total: samples[i]}) + + if sampleHeap.Len() == k { + groupsRemaining-- + } + + if groupsRemaining == 0 { + break + } + } + } + } else { + histogramIndex := 0 + sampleIndex := 0 + + // pick the first 'k' samples based on the increasing order of their ids + for histogramIndex < len(histogramIDs) || sampleIndex < len(sampleIDs) { + var currentID uint64 + haveSample := sampleIndex < len(sampleIDs) + haveHistogram := histogramIndex < len(histogramIDs) + + if haveSample && haveHistogram { + currentID = uint64(min(sampleIDs[sampleIndex], histogramIDs[histogramIndex])) + } else if haveHistogram { + currentID = histogramIDs[histogramIndex] + } else { + currentID = sampleIDs[sampleIndex] + } + + sampleHeap := a.inputToHeap[currentID] + + if sampleHeap.Len() < k { + if haveHistogram && histogramIDs[histogramIndex] == currentID { + heap.Push(sampleHeap, &entry{histId: currentID, histogramSample: histograms[histogramIndex]}) + histogramIndex++ + } else if haveSample && sampleIDs[sampleIndex] == currentID { + heap.Push(sampleHeap, &entry{sId: currentID, total: samples[sampleIndex]}) + sampleIndex++ + } + + if sampleHeap.Len() == k { + groupsRemaining-- + } + + if groupsRemaining == 0 { + break + } + } else { + if haveHistogram && histogramIDs[histogramIndex] == currentID { + histogramIndex++ + } else if haveSample && sampleIDs[sampleIndex] == currentID { + sampleIndex++ + } + } + } + } + case parser.LIMIT_RATIO: + for i, sId := range sampleIDs { + sampleHeap := a.inputToHeap[sId] + + if addRatioSample(ratio, a.series[sId]) { + heap.Push(sampleHeap, &entry{sId: sId, total: samples[i]}) + } + } + + for i, histId := range histogramIDs { + sampleHeap := a.inputToHeap[histId] + + if addRatioSample(ratio, a.series[histId]) { + heap.Push(sampleHeap, &entry{histId: histId, histogramSample: histograms[i]}) + } + } + } + + // Add results from all heaps to the output step vector. + inputSize := len(sampleIDs) + len(histogramIDs) + hint := inputSize + if k > 0 && k*len(a.heaps) < inputSize { + hint = k * len(a.heaps) + } else if ratio != 0 { + estimated := int(float64(inputSize) * math.Abs(ratio)) + if estimated < hint { + hint = estimated + } + } + for _, sampleHeap := range a.heaps { + // for topk and bottomk the heap keeps the lowest value on top, so reverse it. + if a.aggregation == parser.TOPK || a.aggregation == parser.BOTTOMK { + sort.Sort(sort.Reverse(sampleHeap)) + } + sampleHeap.addSamplesToPool(out, hint) + } +} + +type entry struct { + sId uint64 + histId uint64 + total float64 + histogramSample *histogram.FloatHistogram +} + +type samplesHeap struct { + entries []entry + compare func(float64, float64) bool +} + +func (s samplesHeap) Len() int { + return len(s.entries) +} + +func (s *samplesHeap) addSamplesToPool(stepVector *model.StepVector, hint int) { + for _, e := range s.entries { + if e.histogramSample == nil { + stepVector.AppendSampleWithSizeHint(e.sId, e.total, hint) + } else { + stepVector.AppendHistogramWithSizeHint(e.histId, e.histogramSample, hint) + } + } + s.entries = s.entries[:0] +} + +func (s samplesHeap) Less(i, j int) bool { + if math.IsNaN(s.entries[i].total) { + return true + } + if s.compare == nil { // this is case for limitk as it doesn't require any sorting logic + return false + } + + return s.compare(s.entries[i].total, s.entries[j].total) +} + +func (s samplesHeap) Swap(i, j int) { + s.entries[i], s.entries[j] = s.entries[j], s.entries[i] +} + +func (s *samplesHeap) Push(x any) { + s.entries = append(s.entries, *(x.(*entry))) +} + +func (s *samplesHeap) Pop() any { + old := (*s).entries + n := len(old) + el := old[n-1] + (*s).entries = old[0 : n-1] + return el +} diff --git a/internal/promql-engine/execution/aggregate/scalar_table.go b/internal/promql-engine/execution/aggregate/scalar_table.go new file mode 100644 index 00000000000..0e182e0e383 --- /dev/null +++ b/internal/promql-engine/execution/aggregate/scalar_table.go @@ -0,0 +1,230 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package aggregate + +import ( + "context" + "fmt" + "math" + + "github.com/thanos-io/promql-engine/compute" + "github.com/thanos-io/promql-engine/execution/model" + "github.com/thanos-io/promql-engine/execution/parse" + "github.com/thanos-io/promql-engine/warnings" + + "github.com/efficientgo/core/errors" + "github.com/prometheus/prometheus/model/histogram" + "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/promql/parser" + "github.com/prometheus/prometheus/promql/parser/posrange" + "github.com/prometheus/prometheus/util/annotations" +) + +// aggregateTable is a table that aggregates input samples into +// output samples for a single step. +type aggregateTable interface { + // timestamp returns the timestamp of the table. + // If the table is empty, it returns math.MinInt64. + timestamp() int64 + // aggregate aggregates the given vector into the table. + aggregate(vector model.StepVector) error + // populateVector writes out the accumulated result into the provided vector. + populateVector(ctx context.Context, vec *model.StepVector) + // reset resets the table with a new aggregation argument. + // The argument is currently used for quantile aggregation. + reset(arg float64) +} + +type scalarTable struct { + ts int64 + inputs []uint64 + outputs []*model.Series + accumulators []compute.Accumulator +} + +func newScalarTables(stepsBatch int, inputCache []uint64, outputCache []*model.Series, aggregation parser.ItemType) ([]aggregateTable, error) { + tables := make([]aggregateTable, stepsBatch) + for i := range tables { + table, err := newScalarTable(inputCache, outputCache, aggregation) + if err != nil { + return nil, err + } + tables[i] = table + } + return tables, nil +} + +func (t *scalarTable) timestamp() int64 { + return t.ts +} + +func newScalarTable(inputSampleIDs []uint64, outputs []*model.Series, aggregation parser.ItemType) (*scalarTable, error) { + accumulators := make([]compute.Accumulator, len(outputs)) + for i := range accumulators { + acc, err := newScalarAccumulator(aggregation) + if err != nil { + return nil, err + } + accumulators[i] = acc + } + return &scalarTable{ + ts: math.MinInt64, + inputs: inputSampleIDs, + outputs: outputs, + accumulators: accumulators, + }, nil +} + +func (t *scalarTable) aggregate(vector model.StepVector) error { + t.ts = vector.T + + var err error + for i := range vector.Samples { + err = warnings.Coalesce(err, t.addSample(vector.SampleIDs[i], vector.Samples[i])) + } + for i := range vector.Histograms { + err = warnings.Coalesce(err, t.addHistogram(vector.HistogramIDs[i], vector.Histograms[i])) + } + return err +} + +func (t *scalarTable) addSample(sampleID uint64, sample float64) error { + outputSampleID := t.inputs[sampleID] + output := t.outputs[outputSampleID] + + return t.accumulators[output.ID].Add(sample, nil) +} + +func (t *scalarTable) addHistogram(sampleID uint64, h *histogram.FloatHistogram) error { + outputSampleID := t.inputs[sampleID] + output := t.outputs[outputSampleID] + + return t.accumulators[output.ID].Add(0, h) +} + +func (t *scalarTable) reset(arg float64) { + for i := range t.outputs { + t.accumulators[i].Reset(arg) + } + t.ts = math.MinInt64 +} + +func (t *scalarTable) populateVector(ctx context.Context, vec *model.StepVector) { + hint := len(t.outputs) + for i, v := range t.outputs { + acc := t.accumulators[i] + emitAccumulatorWarnings(ctx, acc.Warnings()) + switch acc.ValueType() { + case compute.NoValue, compute.MixedTypeValue: + // MixedTypeValue: warning already emitted by emitAccumulatorWarnings + // for accumulators that track mixed floats/histograms. + continue + case compute.SingleTypeValue: + f, h := acc.Value() + if h == nil { + vec.AppendSampleWithSizeHint(v.ID, f, hint) + } else { + vec.AppendHistogramWithSizeHint(v.ID, h, hint) + } + } + } +} + +// emitAccumulatorWarnings converts accumulator warning flags to annotations and adds them to context. +func emitAccumulatorWarnings(ctx context.Context, warn warnings.Warnings) { + if warn == 0 { + return + } + if warn&warnings.WarnHistogramIgnoredInAggregation != 0 { + warnings.AddToContext(annotations.HistogramIgnoredInAggregationInfo, ctx) + } + if warn&warnings.WarnMixedFloatsHistograms != 0 { + warnings.AddToContext(warnings.MixedFloatsHistogramsAggWarning, ctx) + } + if warn&warnings.WarnCounterResetCollision != 0 { + warnings.AddToContext(annotations.NewHistogramCounterResetCollisionWarning(posrange.PositionRange{}, annotations.HistogramAgg), ctx) + } + if warn&warnings.WarnNHCBBoundsReconciledAgg != 0 { + warnings.AddToContext(annotations.NewMismatchedCustomBucketsHistogramsInfo(posrange.PositionRange{}, annotations.HistogramAgg), ctx) + } +} + +func hashMetric( + builder labels.ScratchBuilder, + metric labels.Labels, + without bool, + grouping []string, + groupingSet map[string]struct{}, + buf []byte, +) (uint64, labels.Labels) { + buf = buf[:0] + builder.Reset() + + if without { + metric.Range(func(lbl labels.Label) { + if lbl.Name == labels.MetricName { + return + } + if _, ok := groupingSet[lbl.Name]; ok { + return + } + builder.Add(lbl.Name, lbl.Value) + }) + key, _ := metric.HashWithoutLabels(buf, grouping...) + return key, builder.Labels() + } + + if len(grouping) == 0 { + return 0, labels.Labels{} + } + + metric.Range(func(lbl labels.Label) { + if _, ok := groupingSet[lbl.Name]; !ok { + return + } + builder.Add(lbl.Name, lbl.Value) + }) + key, _ := metric.HashForLabels(buf, grouping...) + return key, builder.Labels() +} + +// doing it the prometheus way +// https://github.com/prometheus/prometheus/blob/f379e2eac7134dea12ae1d93ebdcb8109db3a5ef/promql/engine.go#L3809C1-L3833C2 +// if ratioLimit > 0 and sampleOffset turns out to be < ratioLimit add sample to the result +// else if ratioLimit < 0 then do ratioLimit+1(switch to positive axis), therefore now we will be taking those samples whose sampleOffset >= 1+ratioLimit (inverting the logic from previous case). +func addRatioSample(ratioLimit float64, series labels.Labels) bool { + sampleOffset := float64(series.Hash()) / float64(math.MaxUint64) + + return (ratioLimit >= 0 && sampleOffset < ratioLimit) || + (ratioLimit < 0 && sampleOffset >= (1.0+ratioLimit)) +} + +func newScalarAccumulator(expr parser.ItemType) (compute.Accumulator, error) { + t := parser.ItemTypeStr[expr] + switch t { + case "sum": + return compute.NewSumAcc(), nil + case "max": + return compute.NewMaxAcc(), nil + case "min": + return compute.NewMinAcc(), nil + case "count": + return compute.NewCountAcc(), nil + case "avg": + return compute.NewAvgAcc(), nil + case "group": + return compute.NewGroupAcc(), nil + case "stddev": + return compute.NewStdDevAcc(), nil + case "stdvar": + return compute.NewStdVarAcc(), nil + case "quantile": + return compute.NewQuantileAcc(), nil + case "histogram_avg": + return compute.NewHistogramAvgAcc(), nil + } + + msg := fmt.Sprintf("unknown aggregation function %s", t) + return nil, errors.Wrap(parse.ErrNotSupportedExpr, msg) +} diff --git a/internal/promql-engine/execution/aggregate/vector_table.go b/internal/promql-engine/execution/aggregate/vector_table.go new file mode 100644 index 00000000000..d5deec49e28 --- /dev/null +++ b/internal/promql-engine/execution/aggregate/vector_table.go @@ -0,0 +1,92 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package aggregate + +import ( + "context" + "fmt" + "math" + + "github.com/thanos-io/promql-engine/compute" + "github.com/thanos-io/promql-engine/execution/model" + "github.com/thanos-io/promql-engine/execution/parse" + + "github.com/efficientgo/core/errors" + "github.com/prometheus/prometheus/promql/parser" +) + +type vectorTable struct { + ts int64 + accumulator compute.VectorAccumulator +} + +func newVectorizedTables(stepsBatch int, a parser.ItemType) ([]aggregateTable, error) { + tables := make([]aggregateTable, stepsBatch) + for i := range tables { + acc, err := newVectorAccumulator(a) + if err != nil { + return nil, err + } + tables[i] = newVectorizedTable(acc) + } + + return tables, nil +} + +func newVectorizedTable(a compute.VectorAccumulator) *vectorTable { + return &vectorTable{ + ts: math.MinInt64, + accumulator: a, + } +} + +func (t *vectorTable) timestamp() int64 { + return t.ts +} + +func (t *vectorTable) aggregate(vector model.StepVector) error { + t.ts = vector.T + return t.accumulator.AddVector(vector.Samples, vector.Histograms) +} + +func (t *vectorTable) populateVector(ctx context.Context, vec *model.StepVector) { + emitAccumulatorWarnings(ctx, t.accumulator.Warnings()) + switch t.accumulator.ValueType() { + case compute.NoValue, compute.MixedTypeValue: + // MixedTypeValue: warning already emitted by emitAccumulatorWarnings + return + case compute.SingleTypeValue: + v, h := t.accumulator.Value() + if h == nil { + vec.AppendSample(0, v) + } else { + vec.AppendHistogram(0, h) + } + } +} + +func (t *vectorTable) reset(p float64) { + t.ts = math.MinInt64 + t.accumulator.Reset(p) +} + +func newVectorAccumulator(expr parser.ItemType) (compute.VectorAccumulator, error) { + t := parser.ItemTypeStr[expr] + switch t { + case "sum": + return compute.NewSumAcc(), nil + case "max": + return compute.NewMaxAcc(), nil + case "min": + return compute.NewMinAcc(), nil + case "count": + return compute.NewCountAcc(), nil + case "avg": + return compute.NewAvgAcc(), nil + case "group": + return compute.NewGroupAcc(), nil + } + msg := fmt.Sprintf("unknown aggregation function %s", t) + return nil, errors.Wrap(parse.ErrNotSupportedExpr, msg) +} diff --git a/internal/promql-engine/execution/binary/scalar.go b/internal/promql-engine/execution/binary/scalar.go new file mode 100644 index 00000000000..7ab8301d138 --- /dev/null +++ b/internal/promql-engine/execution/binary/scalar.go @@ -0,0 +1,255 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package binary + +import ( + "context" + "fmt" + "sync" + + "github.com/thanos-io/promql-engine/execution/model" + "github.com/thanos-io/promql-engine/execution/telemetry" + "github.com/thanos-io/promql-engine/extlabels" + "github.com/thanos-io/promql-engine/query" + "github.com/thanos-io/promql-engine/warnings" + + "github.com/efficientgo/core/errors" + "github.com/prometheus/prometheus/model/histogram" + "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/promql/parser" +) + +// scalarOperator evaluates expressions where one operand is a scalarOperator. +type scalarOperator struct { + lhs model.VectorOperator + rhs model.VectorOperator + lhsType parser.ValueType + rhsType parser.ValueType + opType parser.ItemType + returnBool bool + stepsBatch int + + once sync.Once + series []labels.Labels + + lhsBuf []model.StepVector + rhsBuf []model.StepVector +} + +func NewScalar( + lhs model.VectorOperator, + rhs model.VectorOperator, + lhsType parser.ValueType, + rhsType parser.ValueType, + opType parser.ItemType, + returnBool bool, + opts *query.Options, +) (model.VectorOperator, error) { + op := &scalarOperator{ + lhs: lhs, + rhs: rhs, + lhsType: lhsType, + rhsType: rhsType, + opType: opType, + returnBool: returnBool, + stepsBatch: opts.StepsBatch, + } + + return telemetry.NewOperator(telemetry.NewTelemetry(op, opts), op), nil +} + +func (o *scalarOperator) Explain() (next []model.VectorOperator) { + return []model.VectorOperator{o.lhs, o.rhs} +} + +func (o *scalarOperator) Series(ctx context.Context) ([]labels.Labels, error) { + var err error + o.once.Do(func() { err = o.loadSeries(ctx) }) + if err != nil { + return nil, err + } + return o.series, nil +} + +func (o *scalarOperator) String() string { + return fmt.Sprintf("[vectorScalarBinary] %s", parser.ItemTypeStr[o.opType]) +} + +func (o *scalarOperator) Next(ctx context.Context, buf []model.StepVector) (int, error) { + select { + case <-ctx.Done(): + return 0, ctx.Err() + default: + } + + var err error + o.once.Do(func() { err = o.loadSeries(ctx) }) + if err != nil { + return 0, err + } + + var lhsN int + var lerrChan = make(chan error, 1) + go func() { + defer func() { + if r := recover(); r != nil { + lerrChan <- errors.Newf("unexpected panic: %v", r) + } + close(lerrChan) + }() + var err error + lhsN, err = o.lhs.Next(ctx, o.lhsBuf) + if err != nil { + lerrChan <- err + } + }() + + rhsN, rerr := o.rhs.Next(ctx, o.rhsBuf) + lerr := <-lerrChan + if rerr != nil { + return 0, rerr + } + if lerr != nil { + return 0, lerr + } + + // TODO(fpetkovski): When one operator becomes empty, + // we might want to drain or close the other one. + // We don't have a concept of closing an operator yet. + if lhsN == 0 || rhsN == 0 { + return 0, nil + } + + n := 0 + minN := min(rhsN, lhsN) + + for i := 0; i < minN && n < len(buf); i++ { + o.execBinaryOperation(ctx, o.lhsBuf[i], o.rhsBuf[i], &buf[n]) + n++ + } + + return n, nil +} + +func (o *scalarOperator) loadSeries(ctx context.Context) error { + vectorSide := o.lhs + if o.lhsType == parser.ValueTypeScalar { + vectorSide = o.rhs + } + vectorSeries, err := vectorSide.Series(ctx) + if err != nil { + return err + } + + series := make([]labels.Labels, len(vectorSeries)) + var b labels.ScratchBuilder + for i := range vectorSeries { + if !vectorSeries[i].IsEmpty() { + lbls := vectorSeries[i] + if shouldDropMetricName(o.opType, o.returnBool) { + lbls = extlabels.DropReserved(lbls, b) + } + series[i] = lbls + } else { + series[i] = vectorSeries[i] + } + } + + o.series = series + + // Pre-allocate buffers with appropriate inner slice capacities. + // One side is a scalar (1 sample), the other is a vector (len(vectorSeries) samples). + o.lhsBuf = make([]model.StepVector, o.stepsBatch) + o.rhsBuf = make([]model.StepVector, o.stepsBatch) + + var lhsSeriesCount, rhsSeriesCount int + if o.lhsType == parser.ValueTypeScalar { + lhsSeriesCount = 1 + rhsSeriesCount = len(vectorSeries) + } else { + lhsSeriesCount = len(vectorSeries) + rhsSeriesCount = 1 + } + + // Pre-allocate float sample slices; histogram slices will grow on demand. + for i := range o.lhsBuf { + o.lhsBuf[i].SampleIDs = make([]uint64, 0, lhsSeriesCount) + o.lhsBuf[i].Samples = make([]float64, 0, lhsSeriesCount) + } + for i := range o.rhsBuf { + o.rhsBuf[i].SampleIDs = make([]uint64, 0, rhsSeriesCount) + o.rhsBuf[i].Samples = make([]float64, 0, rhsSeriesCount) + } + + return nil +} + +func (o *scalarOperator) execBinaryOperation(ctx context.Context, lhs, rhs model.StepVector, step *model.StepVector) { + ts := lhs.T + step.Reset(ts) + + scalar, other := lhs, rhs + if o.lhsType != parser.ValueTypeScalar { + scalar, other = rhs, lhs + } + + var ( + v float64 + h *histogram.FloatHistogram + keep bool + err error + ) + var warn warnings.Warnings + sampleHint := len(other.Samples) + for i, otherVal := range other.Samples { + scalarVal := scalar.Samples[0] + + if o.lhsType == parser.ValueTypeScalar { + v, _, keep, warn, err = binOp(o.opType, scalarVal, otherVal, nil, nil) + } else { + v, _, keep, warn, err = binOp(o.opType, otherVal, scalarVal, nil, nil) + } + if err != nil { + warnings.AddToContext(err, ctx) + continue + } + if warn != 0 { + emitBinaryOpWarnings(ctx, warn, o.opType) + } + // in comparison operations between scalars and vectors, the vectors are filtered, regardless if lhs or rhs + if keep && o.opType.IsComparisonOperator() && (o.lhsType == parser.ValueTypeVector || o.rhsType == parser.ValueTypeVector) { + v = otherVal + } + if o.returnBool { + v = 0.0 + if keep { + v = 1.0 + } + } else if !keep { + continue + } + step.AppendSampleWithSizeHint(other.SampleIDs[i], v, sampleHint) + } + histogramHint := len(other.Histograms) + for i, otherVal := range other.Histograms { + scalarVal := scalar.Samples[0] + + if o.lhsType == parser.ValueTypeScalar { + _, h, keep, warn, err = binOp(o.opType, scalarVal, 0., nil, otherVal) + } else { + _, h, keep, warn, err = binOp(o.opType, 0., scalarVal, otherVal, nil) + } + if err != nil { + warnings.AddToContext(err, ctx) + continue + } + if warn != 0 { + emitBinaryOpWarnings(ctx, warn, o.opType) + } + if !keep { + continue + } + step.AppendHistogramWithSizeHint(other.HistogramIDs[i], h, histogramHint) + } +} diff --git a/internal/promql-engine/execution/binary/utils.go b/internal/promql-engine/execution/binary/utils.go new file mode 100644 index 00000000000..db086930809 --- /dev/null +++ b/internal/promql-engine/execution/binary/utils.go @@ -0,0 +1,201 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package binary + +import ( + "context" + "fmt" + "math" + + "github.com/thanos-io/promql-engine/warnings" + + "github.com/prometheus/prometheus/model/histogram" + "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/promql/parser" + "github.com/prometheus/prometheus/promql/parser/posrange" + "github.com/prometheus/prometheus/util/annotations" +) + +type binOpSide string + +const ( + lhBinOpSide binOpSide = "left" + rhBinOpSide binOpSide = "right" +) + +type errManyToManyMatch struct { + matching *parser.VectorMatching + side binOpSide + + original, duplicate labels.Labels +} + +func newManyToManyMatchError(matching *parser.VectorMatching, original, duplicate labels.Labels, side binOpSide) *errManyToManyMatch { + return &errManyToManyMatch{ + original: original, + duplicate: duplicate, + matching: matching, + side: side, + } +} + +func (e *errManyToManyMatch) Error() string { + group := e.original.MatchLabels(e.matching.On, e.matching.MatchingLabels...) + // The choice of which series is reported as "original" vs "duplicate" is + // driven by upstream StepVector / map iteration order and is therefore + // non-deterministic across runs. Sort the rendered label-set strings so + // the error message is stable; downstream consumers (e.g. Cortex, + // see cortexproject/cortex#7546) compare these messages. + original, duplicate := e.original.String(), e.duplicate.String() + if duplicate < original { + original, duplicate = duplicate, original + } + msg := "found duplicate series for the match group %s on the %s hand-side of the operation: [%s, %s]" + + ";many-to-many matching not allowed: matching labels must be unique on one side" + return fmt.Sprintf(msg, group, e.side, original, duplicate) +} + +func shouldDropMetricName(op parser.ItemType, returnBool bool) bool { + switch op { + case parser.ADD, parser.SUB, parser.MUL, parser.DIV, parser.MOD, parser.POW, parser.ATAN2: + return true + default: + return op.IsComparisonOperator() && returnBool + } +} + +// binOp evaluates a binary operation between two values. +// Returns: value, histogram, keep, warnings, error. +func binOp(op parser.ItemType, lhs, rhs float64, hlhs, hrhs *histogram.FloatHistogram) (float64, *histogram.FloatHistogram, bool, warnings.Warnings, error) { + switch { + case hlhs == nil && hrhs == nil: + { + switch op { + case parser.ADD: + return lhs + rhs, nil, true, 0, nil + case parser.SUB: + return lhs - rhs, nil, true, 0, nil + case parser.MUL: + return lhs * rhs, nil, true, 0, nil + case parser.DIV: + return lhs / rhs, nil, true, 0, nil + case parser.POW: + return math.Pow(lhs, rhs), nil, true, 0, nil + case parser.MOD: + return math.Mod(lhs, rhs), nil, true, 0, nil + case parser.EQLC: + return lhs, nil, lhs == rhs, 0, nil + case parser.NEQ: + return lhs, nil, lhs != rhs, 0, nil + case parser.GTR: + return lhs, nil, lhs > rhs, 0, nil + case parser.LSS: + return lhs, nil, lhs < rhs, 0, nil + case parser.GTE: + return lhs, nil, lhs >= rhs, 0, nil + case parser.LTE: + return lhs, nil, lhs <= rhs, 0, nil + case parser.ATAN2: + return math.Atan2(lhs, rhs), nil, true, 0, nil + } + } + case hlhs == nil && hrhs != nil: + { + switch op { + case parser.MUL: + return 0, hrhs.Copy().Mul(lhs).Compact(0), true, 0, nil + case parser.ADD, parser.SUB, parser.DIV, parser.POW, parser.MOD, parser.EQLC, parser.NEQ, parser.GTR, parser.LSS, parser.GTE, parser.LTE, parser.ATAN2: + return 0, nil, false, warnings.WarnIncompatibleTypesInBinOp, nil + } + } + case hlhs != nil && hrhs == nil: + { + switch op { + case parser.MUL: + return 0, hlhs.Copy().Mul(rhs).Compact(0), true, 0, nil + case parser.DIV: + return 0, hlhs.Copy().Div(rhs).Compact(0), true, 0, nil + case parser.ADD, parser.SUB, parser.POW, parser.MOD, parser.EQLC, parser.NEQ, parser.GTR, parser.LSS, parser.GTE, parser.LTE, parser.ATAN2: + return 0, nil, false, warnings.WarnIncompatibleTypesInBinOp, nil + } + } + case hlhs != nil && hrhs != nil: + { + switch op { + case parser.ADD: + res, counterResetCollision, nhcbBoundsReconciled, err := hlhs.Copy().Add(hrhs) + if err != nil { + return 0, nil, false, 0, err + } + var warn warnings.Warnings + if counterResetCollision { + warn |= warnings.WarnCounterResetCollision + } + if nhcbBoundsReconciled { + warn |= warnings.WarnNHCBBoundsReconciled + } + return 0, res.Compact(0), true, warn, nil + case parser.SUB: + res, counterResetCollision, nhcbBoundsReconciled, err := hlhs.Copy().Sub(hrhs) + if err != nil { + return 0, nil, false, 0, err + } + var warn warnings.Warnings + if counterResetCollision { + warn |= warnings.WarnCounterResetCollision + } + if nhcbBoundsReconciled { + warn |= warnings.WarnNHCBBoundsReconciled + } + return 0, res.Compact(0), true, warn, nil + case parser.EQLC: + // This operation expects that both histograms are compacted. + return 0, hlhs, hlhs.Equals(hrhs), 0, nil + case parser.NEQ: + // This operation expects that both histograms are compacted. + return 0, hlhs, !hlhs.Equals(hrhs), 0, nil + case parser.MUL, parser.DIV, parser.POW, parser.MOD, parser.GTR, parser.LSS, parser.GTE, parser.LTE, parser.ATAN2: + return 0, nil, false, warnings.WarnIncompatibleTypesInBinOp, nil + } + } + } + return 0, nil, false, 0, nil +} + +// emitBinaryOpWarnings emits warnings for binary operation side effects. +func emitBinaryOpWarnings(ctx context.Context, warn warnings.Warnings, opType parser.ItemType) { + if warn == 0 { + return + } + if warn&warnings.WarnMixedExponentialCustomBuckets != 0 { + warnings.AddToContext(annotations.NewMixedExponentialCustomHistogramsWarning("", posrange.PositionRange{}), ctx) + } + if warn&warnings.WarnCounterResetCollision != 0 { + var op annotations.HistogramOperation + switch opType { + case parser.ADD: + op = annotations.HistogramAdd + case parser.SUB: + op = annotations.HistogramSub + default: + return + } + warnings.AddToContext(annotations.NewHistogramCounterResetCollisionWarning(posrange.PositionRange{}, op), ctx) + } + if warn&warnings.WarnNHCBBoundsReconciled != 0 { + var op annotations.HistogramOperation + switch opType { + case parser.ADD: + op = annotations.HistogramAdd + case parser.SUB: + op = annotations.HistogramSub + default: + return + } + warnings.AddToContext(annotations.NewMismatchedCustomBucketsHistogramsInfo(posrange.PositionRange{}, op), ctx) + } + if warn&warnings.WarnIncompatibleTypesInBinOp != 0 { + warnings.AddToContext(annotations.IncompatibleTypesInBinOpInfo, ctx) + } +} diff --git a/internal/promql-engine/execution/binary/utils_test.go b/internal/promql-engine/execution/binary/utils_test.go new file mode 100644 index 00000000000..e8098a1b3d7 --- /dev/null +++ b/internal/promql-engine/execution/binary/utils_test.go @@ -0,0 +1,96 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package binary + +import ( + "strings" + "testing" + + "github.com/efficientgo/core/testutil" + "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/promql/parser" +) + +// TestErrManyToManyMatchDeterministicOrder verifies that errManyToManyMatch.Error() +// produces a byte-identical error string regardless of which of the two duplicate +// series happened to land in the "original" vs "duplicate" slot. +// +// Background: the choice of which series becomes original vs duplicate is driven +// by upstream StepVector ordering, which is in turn fed by map iteration (e.g. +// in execution/scan/subquery.go). That made the error message non-deterministic +// across runs and across parallel engine workers, breaking error-string equality +// checks in downstream consumers (see cortexproject/cortex#7546). +// +// Fix: lexicographically sort the two rendered label-set strings before +// formatting the message. +func TestErrManyToManyMatchDeterministicOrder(t *testing.T) { + matching := &parser.VectorMatching{ + On: true, + MatchingLabels: []string{"job"}, + } + + for _, tc := range []struct { + name string + side binOpSide + a labels.Labels + b labels.Labels + }{ + { + name: "simple labels right side", + side: rhBinOpSide, + a: labels.FromStrings("__name__", "requests_total", "instance", "a", "job", "api"), + b: labels.FromStrings("__name__", "requests_total", "instance", "b", "job", "api"), + }, + { + name: "simple labels left side", + side: lhBinOpSide, + a: labels.FromStrings("zone", "us-east-1", "job", "api"), + b: labels.FromStrings("zone", "us-west-2", "job", "api"), + }, + { + // Adversarial values that include the same punctuation we use to + // frame the rendered list ([, ], comma, braces, quotes). Sorting + // the rendered label-set strings — not the labels themselves — + // keeps the comparison robust against these characters. + name: "adversarial label values", + side: rhBinOpSide, + a: labels.FromStrings( + "detail", `left[,{}"]`, + "instance", `a[,{}"]`, + "job", `api[,{}"]`, + ), + b: labels.FromStrings( + "detail", `right[,{}"]`, + "instance", `b[,{}"]`, + "job", `api[,{}"]`, + ), + }, + } { + t.Run(tc.name, func(t *testing.T) { + aFirst := (&errManyToManyMatch{ + matching: matching, + side: tc.side, + original: tc.a, + duplicate: tc.b, + }).Error() + bFirst := (&errManyToManyMatch{ + matching: matching, + side: tc.side, + original: tc.b, + duplicate: tc.a, + }).Error() + + testutil.Equals(t, aFirst, bFirst) + + // Sanity-check that both rendered label sets are still present and + // that the existing prefix/format expected by downstream consumers + // (e.g. Prometheus-compatible error matchers) is preserved. + testutil.Assert(t, strings.Contains(aFirst, tc.a.String()), "expected %q to contain %q", aFirst, tc.a.String()) + testutil.Assert(t, strings.Contains(aFirst, tc.b.String()), "expected %q to contain %q", aFirst, tc.b.String()) + testutil.Assert(t, strings.Contains(aFirst, "found duplicate series for the match group"), "missing expected prefix in %q", aFirst) + testutil.Assert(t, strings.Contains(aFirst, "many-to-many matching not allowed"), "missing expected suffix in %q", aFirst) + testutil.Assert(t, strings.Contains(aFirst, string(tc.side)+" hand-side"), "missing expected side marker in %q", aFirst) + }) + } +} diff --git a/internal/promql-engine/execution/binary/vector.go b/internal/promql-engine/execution/binary/vector.go new file mode 100644 index 00000000000..b0791a7ff23 --- /dev/null +++ b/internal/promql-engine/execution/binary/vector.go @@ -0,0 +1,672 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package binary + +import ( + "context" + "fmt" + "sync" + + "github.com/thanos-io/promql-engine/execution/model" + "github.com/thanos-io/promql-engine/execution/telemetry" + "github.com/thanos-io/promql-engine/extlabels" + "github.com/thanos-io/promql-engine/query" + "github.com/thanos-io/promql-engine/warnings" + + "github.com/cespare/xxhash/v2" + "github.com/efficientgo/core/errors" + "github.com/prometheus/prometheus/model/histogram" + "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/promql/parser" + "golang.org/x/exp/slices" +) + +type joinBucket struct { + ats, bts int64 + sid uint64 + val float64 + histogramVal *histogram.FloatHistogram +} + +// vectorOperator evaluates an expression between two step vectors. +type vectorOperator struct { + lhs model.VectorOperator + rhs model.VectorOperator + matching *parser.VectorMatching + opType parser.ItemType + returnBool bool + stepsBatch int + sigFunc func(labels.Labels) uint64 + + once sync.Once + series []labels.Labels + lhsSampleIDs []labels.Labels + rhsSampleIDs []labels.Labels + + // output series indices, keyed by input series id. + // arithmetic: output is hcOutputBase[hc]+lcOutputOffset[lc] (offset is 0 + // without group_left/group_right includes). "or": rhs uses lcOutputBase. + hcOutputBase []uint64 + lcOutputBase []uint64 + lcOutputOffset []uint64 + + lcJoinBuckets []*joinBucket + hcJoinBuckets []*joinBucket + + lhsBuf []model.StepVector + rhsBuf []model.StepVector +} + +func NewVectorOperator( + lhs model.VectorOperator, + rhs model.VectorOperator, + matching *parser.VectorMatching, + opType parser.ItemType, + returnBool bool, + opts *query.Options, +) (model.VectorOperator, error) { + op := &vectorOperator{ + lhs: lhs, + rhs: rhs, + matching: matching, + opType: opType, + returnBool: returnBool, + sigFunc: signatureFunc(matching.On, matching.MatchingLabels...), + stepsBatch: opts.StepsBatch, + } + + return telemetry.NewOperator(telemetry.NewTelemetry(op, opts), op), nil +} + +func (o *vectorOperator) String() string { + if o.matching.On { + return fmt.Sprintf("[vectorBinary] %s - %v, on: %v, group: %v", parser.ItemTypeStr[o.opType], o.matching.Card.String(), o.matching.MatchingLabels, o.matching.Include) + } + return fmt.Sprintf("[vectorBinary] %s - %v, ignoring: %v, group: %v", parser.ItemTypeStr[o.opType], o.matching.Card.String(), o.matching.On, o.matching.Include) +} + +func (o *vectorOperator) Explain() (next []model.VectorOperator) { + return []model.VectorOperator{o.lhs, o.rhs} +} + +func (o *vectorOperator) Series(ctx context.Context) ([]labels.Labels, error) { + if err := o.initOnce(ctx); err != nil { + return nil, err + } + return o.series, nil +} + +func (o *vectorOperator) Next(ctx context.Context, buf []model.StepVector) (int, error) { + select { + case <-ctx.Done(): + return 0, ctx.Err() + default: + } + + // Some operators do not call Series of all their children. + if err := o.initOnce(ctx); err != nil { + return 0, err + } + + var lhsN int + var lerrChan = make(chan error, 1) + go func() { + defer func() { + if r := recover(); r != nil { + lerrChan <- errors.Newf("unexpected panic: %v", r) + } + close(lerrChan) + }() + var err error + lhsN, err = o.lhs.Next(ctx, o.lhsBuf) + if err != nil { + lerrChan <- err + } + }() + + rhsN, rerr := o.rhs.Next(ctx, o.rhsBuf) + lerr := <-lerrChan + if rerr != nil { + return 0, rerr + } + if lerr != nil { + return 0, lerr + } + + // TODO(fpetkovski): When one operator becomes empty, + // we might want to drain or close the other one. + // We don't have a concept of closing an operator yet. + if lhsN == 0 || rhsN == 0 { + return 0, nil + } + + n := 0 + minN := min(rhsN, lhsN) + + for i := 0; i < minN && n < len(buf); i++ { + if err := o.execBinaryOperation(ctx, o.lhsBuf[i], o.rhsBuf[i], &buf[n]); err != nil { + return 0, err + } + n++ + } + + return n, nil +} + +func (o *vectorOperator) initOnce(ctx context.Context) error { + var err error + o.once.Do(func() { err = o.init(ctx) }) + return err +} + +func (o *vectorOperator) init(ctx context.Context) error { + var highCardSide []labels.Labels + var errChan = make(chan error, 1) + go func() { + defer func() { + if r := recover(); r != nil { + errChan <- errors.Newf("unexpected panic: %v", r) + } + close(errChan) + }() + var err error + highCardSide, err = o.lhs.Series(ctx) + if err != nil { + errChan <- err + } + }() + + lowCardSide, err := o.rhs.Series(ctx) + if err != nil { + return err + } + if err := <-errChan; err != nil { + return err + } + o.lhsSampleIDs = highCardSide + o.rhsSampleIDs = lowCardSide + + if o.matching.Card == parser.CardOneToMany { + highCardSide, lowCardSide = lowCardSide, highCardSide + } + + o.initJoinTables(highCardSide, lowCardSide) + + // Pre-allocate buffers with appropriate inner slice capacities + // based on series counts from each side. + lhsSeriesCount := len(o.lhsSampleIDs) + rhsSeriesCount := len(o.rhsSampleIDs) + + o.lhsBuf = make([]model.StepVector, o.stepsBatch) + o.rhsBuf = make([]model.StepVector, o.stepsBatch) + + // Pre-allocate float sample slices; histogram slices will grow on demand. + for i := range o.lhsBuf { + o.lhsBuf[i].SampleIDs = make([]uint64, 0, lhsSeriesCount) + o.lhsBuf[i].Samples = make([]float64, 0, lhsSeriesCount) + } + for i := range o.rhsBuf { + o.rhsBuf[i].SampleIDs = make([]uint64, 0, rhsSeriesCount) + o.rhsBuf[i].Samples = make([]float64, 0, rhsSeriesCount) + } + + return nil +} + +func (o *vectorOperator) execBinaryOperation(ctx context.Context, lhs, rhs model.StepVector, step *model.StepVector) error { + switch o.opType { + case parser.LAND: + return o.execBinaryAnd(lhs, rhs, step) + case parser.LOR: + return o.execBinaryOr(lhs, rhs, step) + case parser.LUNLESS: + return o.execBinaryUnless(lhs, rhs, step) + default: + return o.execBinaryArithmetic(ctx, lhs, rhs, step) + } +} + +func (o *vectorOperator) execBinaryAnd(lhs, rhs model.StepVector, step *model.StepVector) error { + ts := lhs.T + step.Reset(ts) + + for _, sampleID := range rhs.SampleIDs { + jp := o.lcJoinBuckets[sampleID] + jp.ats = ts + } + + for _, histogramID := range rhs.HistogramIDs { + jp := o.lcJoinBuckets[histogramID] + jp.ats = ts + } + + sampleHint := len(lhs.Samples) + for i, sampleID := range lhs.SampleIDs { + if jp := o.hcJoinBuckets[sampleID]; jp.ats == ts { + step.AppendSampleWithSizeHint(o.hcOutputBase[sampleID], lhs.Samples[i], sampleHint) + } + } + + histogramHint := len(lhs.Histograms) + for i, histogramID := range lhs.HistogramIDs { + if jp := o.hcJoinBuckets[histogramID]; jp.ats == ts { + step.AppendHistogramWithSizeHint(o.hcOutputBase[histogramID], lhs.Histograms[i], histogramHint) + } + } + return nil +} + +func (o *vectorOperator) execBinaryOr(lhs, rhs model.StepVector, step *model.StepVector) error { + ts := lhs.T + step.Reset(ts) + + sampleHint := len(lhs.Samples) + len(rhs.Samples) + for i, sampleID := range lhs.SampleIDs { + jp := o.hcJoinBuckets[sampleID] + jp.ats = ts + step.AppendSampleWithSizeHint(o.hcOutputBase[sampleID], lhs.Samples[i], sampleHint) + } + + histogramHint := len(lhs.Histograms) + len(rhs.Histograms) + for i, histogramID := range lhs.HistogramIDs { + jp := o.hcJoinBuckets[histogramID] + jp.ats = ts + step.AppendHistogramWithSizeHint(o.hcOutputBase[histogramID], lhs.Histograms[i], histogramHint) + } + + for i, sampleID := range rhs.SampleIDs { + if jp := o.lcJoinBuckets[sampleID]; jp.ats != ts { + step.AppendSampleWithSizeHint(o.lcOutputBase[sampleID], rhs.Samples[i], sampleHint) + } + } + + for i, histogramID := range rhs.HistogramIDs { + if jp := o.lcJoinBuckets[histogramID]; jp.ats != ts { + step.AppendHistogramWithSizeHint(o.lcOutputBase[histogramID], rhs.Histograms[i], histogramHint) + } + } + + return nil +} + +func (o *vectorOperator) execBinaryUnless(lhs, rhs model.StepVector, step *model.StepVector) error { + ts := lhs.T + step.Reset(ts) + + for _, sampleID := range rhs.SampleIDs { + jp := o.lcJoinBuckets[sampleID] + jp.ats = ts + } + for _, histogramID := range rhs.HistogramIDs { + jp := o.lcJoinBuckets[histogramID] + jp.ats = ts + } + + sampleHint := len(lhs.Samples) + for i, sampleID := range lhs.SampleIDs { + if jp := o.hcJoinBuckets[sampleID]; jp.ats != ts { + step.AppendSampleWithSizeHint(o.hcOutputBase[sampleID], lhs.Samples[i], sampleHint) + } + } + histogramHint := len(lhs.Histograms) + for i, histogramID := range lhs.HistogramIDs { + if jp := o.hcJoinBuckets[histogramID]; jp.ats != ts { + step.AppendHistogramWithSizeHint(o.hcOutputBase[histogramID], lhs.Histograms[i], histogramHint) + } + } + return nil +} + +func (o *vectorOperator) computeBinaryPairing(hval, lval float64, hlhs, hrhs *histogram.FloatHistogram) (float64, *histogram.FloatHistogram, bool, warnings.Warnings, error) { + // operand is not commutative so we need to address potential swapping + if o.matching.Card == parser.CardOneToMany { + return binOp(o.opType, lval, hval, hlhs, hrhs) + } + return binOp(o.opType, hval, lval, hlhs, hrhs) +} + +func (o *vectorOperator) execBinaryArithmetic(ctx context.Context, lhs, rhs model.StepVector, step *model.StepVector) error { + ts := lhs.T + step.Reset(ts) + + var ( + hcs, lcs model.StepVector + h *histogram.FloatHistogram + keep bool + err error + ) + + switch o.matching.Card { + case parser.CardManyToOne, parser.CardOneToOne: + hcs, lcs = lhs, rhs + case parser.CardOneToMany: + hcs, lcs = rhs, lhs + default: + return errors.Newf("Unexpected matching cardinality: %s", o.matching.Card.String()) + } + + // shortcut: if we have no samples and histograms on the high card side we cannot compute pairings + if len(hcs.Samples) == 0 && len(hcs.Histograms) == 0 { + return nil + } + for i, sampleID := range lcs.SampleIDs { + jp := o.lcJoinBuckets[sampleID] + // Hash collisions on the low-card-side would imply a many-to-many relation. + if jp.ats == ts { + return o.newManyToManyMatchErrorOnLowCardSide(jp.sid, sampleID) + } + jp.sid = sampleID + jp.val = lcs.Samples[i] + jp.ats = ts + } + + for i, histogramID := range lcs.HistogramIDs { + jp := o.lcJoinBuckets[histogramID] + // Hash collisions on the low-card-side would imply a many-to-many relation. + if jp.ats == ts { + return o.newManyToManyMatchErrorOnLowCardSide(jp.sid, histogramID) + } + jp.sid = histogramID + jp.histogramVal = lcs.Histograms[i] + jp.ats = ts + } + + sampleHint := len(hcs.Samples) + len(hcs.Histograms) + histogramHint := len(hcs.Samples) + len(hcs.Histograms) + + for i, histogramID := range hcs.HistogramIDs { + jp := o.hcJoinBuckets[histogramID] + if jp.ats != ts { + continue + } + // Hash collisions on the high card side are expected except if a one-to-one + // matching was requested and we have an implicit many-to-one match instead. + if jp.bts == ts && o.matching.Card == parser.CardOneToOne { + return o.newImplicitManyToOneError() + } + jp.bts = ts + + var warn warnings.Warnings + if jp.histogramVal != nil { + _, h, keep, warn, err = o.computeBinaryPairing(0, 0, hcs.Histograms[i], jp.histogramVal) + } else { + _, h, keep, warn, err = o.computeBinaryPairing(0, jp.val, hcs.Histograms[i], nil) + } + if err != nil { + warnings.AddToContext(err, ctx) + continue + } + if warn != 0 { + emitBinaryOpWarnings(ctx, warn, o.opType) + // For incompatible types, skip entirely - don't produce any output + if warn&warnings.WarnIncompatibleTypesInBinOp != 0 { + continue + } + } + + switch { + case o.returnBool: + h = nil + if keep { + step.AppendSampleWithSizeHint(o.hcOutputBase[histogramID]+o.lcOutputOffset[jp.sid], 1.0, sampleHint) + } else { + step.AppendSampleWithSizeHint(o.hcOutputBase[histogramID]+o.lcOutputOffset[jp.sid], 0.0, sampleHint) + } + case !keep: + continue + } + + if h != nil { + step.AppendHistogramWithSizeHint(o.hcOutputBase[histogramID]+o.lcOutputOffset[jp.sid], h, histogramHint) + } + } + + for i, sampleID := range hcs.SampleIDs { + jp := o.hcJoinBuckets[sampleID] + if jp.ats != ts { + continue + } + // Hash collisions on the high card side are expected except if a one-to-one + // matching was requested and we have an implicit many-to-one match instead. + if jp.bts == ts && o.matching.Card == parser.CardOneToOne { + return o.newImplicitManyToOneError() + } + jp.bts = ts + var val float64 + var warn warnings.Warnings + + if jp.histogramVal != nil { + _, h, keep, warn, err = o.computeBinaryPairing(hcs.Samples[i], 0, nil, jp.histogramVal) + if err != nil { + warnings.AddToContext(err, ctx) + continue + } + if warn != 0 { + emitBinaryOpWarnings(ctx, warn, o.opType) + if warn&warnings.WarnIncompatibleTypesInBinOp != 0 { + continue + } + } + if !keep { + continue + } + step.AppendHistogramWithSizeHint(o.hcOutputBase[sampleID]+o.lcOutputOffset[jp.sid], h, histogramHint) + } else { + val, _, keep, warn, err = o.computeBinaryPairing(hcs.Samples[i], jp.val, nil, nil) + if err != nil { + warnings.AddToContext(err, ctx) + continue + } + if warn != 0 { + emitBinaryOpWarnings(ctx, warn, o.opType) + } + if o.returnBool { + val = 0 + if keep { + val = 1 + } + } else if !keep { + continue + } + step.AppendSampleWithSizeHint(o.hcOutputBase[sampleID]+o.lcOutputOffset[jp.sid], val, sampleHint) + } + } + return nil +} + +func (o *vectorOperator) newManyToManyMatchErrorOnLowCardSide(originalSampleId, duplicateSampleId uint64) error { + side := rhBinOpSide + labels := o.rhsSampleIDs + + if o.matching.Card == parser.CardOneToMany { + side = lhBinOpSide + labels = o.lhsSampleIDs + } + return newManyToManyMatchError(o.matching, labels[duplicateSampleId], labels[originalSampleId], side) +} + +func (o *vectorOperator) newImplicitManyToOneError() error { + return errors.New("multiple matches for labels: many-to-one matching must be explicit (group_left/group_right)") +} + +func (o *vectorOperator) includedLabelsHash(lset labels.Labels, b *labels.ScratchBuilder) uint64 { + b.Reset() + for _, name := range o.matching.Include { + if v := lset.Get(name); v != "" { + b.Add(name, v) + } + } + b.Sort() + return b.Labels().Hash() +} + +func (o *vectorOperator) initJoinTables(highCardSide, lowCardSide []labels.Labels) { + var ( + joinBucketsByHash = make(map[uint64]*joinBucket) + lcJoinBuckets = make([]*joinBucket, len(lowCardSide)) + hcJoinBuckets = make([]*joinBucket, len(highCardSide)) + lcHashToSeriesIDs = make(map[uint64][]uint64, len(lowCardSide)) + lcSampleIdToSignature = make([]uint64, len(lowCardSide)) + hcSampleIdToSignature = make([]uint64, len(highCardSide)) + ) + + // initialize join bucket mappings + for i := range lowCardSide { + sig := o.sigFunc(lowCardSide[i]) + lcSampleIdToSignature[i] = sig + lcHashToSeriesIDs[sig] = append(lcHashToSeriesIDs[sig], uint64(i)) + if jb, ok := joinBucketsByHash[sig]; ok { + lcJoinBuckets[i] = jb + } else { + jb := joinBucket{ats: -1, bts: -1} + joinBucketsByHash[sig] = &jb + lcJoinBuckets[i] = &jb + } + } + for i := range highCardSide { + sig := o.sigFunc(highCardSide[i]) + hcSampleIdToSignature[i] = sig + if jb, ok := joinBucketsByHash[sig]; ok { + hcJoinBuckets[i] = jb + } else { + jb := joinBucket{ats: -1, bts: -1} + joinBucketsByHash[sig] = &jb + hcJoinBuckets[i] = &jb + } + } + + // initialize series + h := &joinHelper{seen: make(map[uint64]int)} + hcOutputBase := make([]uint64, len(highCardSide)) + switch o.opType { + case parser.LAND: + // "and" can only have matches if lhs and rhs have collision, so we only need to populate + // the output index for lhs series that have corresponding hash collision + for i := range highCardSide { + if len(lcHashToSeriesIDs[hcSampleIdToSignature[i]]) == 0 { + continue + } + hcOutputBase[i] = uint64(h.append(highCardSide[i])) + } + case parser.LOR: + for i := range highCardSide { + hcOutputBase[i] = uint64(h.append(highCardSide[i])) + } + lcOutputBase := make([]uint64, len(lowCardSide)) + for i := range lowCardSide { + lcOutputBase[i] = uint64(h.append(lowCardSide[i])) + } + o.lcOutputBase = lcOutputBase + case parser.LUNLESS: + for i := range highCardSide { + hcOutputBase[i] = uint64(h.append(highCardSide[i])) + } + default: + // rank low card series sharing a signature by their distinct included + // labels (a single rank 0 without includes), then give each high card + // series a contiguous output block indexed by that rank. + b := labels.NewBuilder(labels.EmptyLabels()) + lcOutputOffset := make([]uint64, len(lowCardSide)) + rankByHashBySig := make(map[uint64]map[uint64]uint64, len(lcHashToSeriesIDs)) + repsBySig := make(map[uint64][]uint64, len(lcHashToSeriesIDs)) + var includeLabels labels.ScratchBuilder + for i := range lowCardSide { + sig := lcSampleIdToSignature[i] + rankByHash, ok := rankByHashBySig[sig] + if !ok { + rankByHash = make(map[uint64]uint64) + rankByHashBySig[sig] = rankByHash + } + includeHash := o.includedLabelsHash(lowCardSide[i], &includeLabels) + rank, ok := rankByHash[includeHash] + if !ok { + rank = uint64(len(repsBySig[sig])) + rankByHash[includeHash] = rank + repsBySig[sig] = append(repsBySig[sig], uint64(i)) + } + lcOutputOffset[i] = rank + } + + for i := range highCardSide { + reps, ok := repsBySig[hcSampleIdToSignature[i]] + if !ok { + continue + } + // append consecutively so ranks land at base, base+1, ... + hcOutputBase[i] = uint64(h.append(o.resultMetric(b, highCardSide[i], lowCardSide[reps[0]]))) + for _, lc := range reps[1:] { + h.append(o.resultMetric(b, highCardSide[i], lowCardSide[lc])) + } + } + o.lcOutputOffset = lcOutputOffset + } + o.series = h.ls + o.hcOutputBase = hcOutputBase + o.lcJoinBuckets = lcJoinBuckets + o.hcJoinBuckets = hcJoinBuckets +} + +type joinHelper struct { + seen map[uint64]int + ls []labels.Labels + n int +} + +func (h *joinHelper) append(ls labels.Labels) int { + hash := ls.Hash() + if n, ok := h.seen[hash]; ok { + return n + } + h.ls = append(h.ls, ls) + h.seen[hash] = h.n + h.n++ + + return h.n - 1 +} + +func (o *vectorOperator) resultMetric(b *labels.Builder, highCard, lowCard labels.Labels) labels.Labels { + b.Reset(highCard) + + if shouldDropMetricName(o.opType, o.returnBool) { + b.Del(labels.MetricName) + b.Del(extlabels.MetricType) + b.Del(extlabels.MetricUnit) + } + + if o.matching.Card == parser.CardOneToOne { + if o.matching.On { + b.Keep(o.matching.MatchingLabels...) + } else { + b.Del(o.matching.MatchingLabels...) + } + } + for _, ln := range o.matching.Include { + if v := lowCard.Get(ln); v != "" { + b.Set(ln, v) + } else { + b.Del(ln) + } + } + if o.returnBool { + b.Del(labels.MetricName) + b.Del(extlabels.MetricType) + b.Del(extlabels.MetricUnit) + } + return b.Labels() +} + +func signatureFunc(on bool, names ...string) func(labels.Labels) uint64 { + b := make([]byte, 256) + if on { + slices.Sort(names) + return func(lset labels.Labels) uint64 { + return xxhash.Sum64(lset.BytesWithLabels(b, names...)) + } + } + names = append([]string{labels.MetricName}, names...) + slices.Sort(names) + return func(lset labels.Labels) uint64 { + return xxhash.Sum64(lset.BytesWithoutLabels(b, names...)) + } +} diff --git a/internal/promql-engine/execution/exchange/coalesce.go b/internal/promql-engine/execution/exchange/coalesce.go new file mode 100644 index 00000000000..d7bdc8186dd --- /dev/null +++ b/internal/promql-engine/execution/exchange/coalesce.go @@ -0,0 +1,262 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package exchange + +import ( + "context" + "math" + "sync" + "sync/atomic" + + "github.com/thanos-io/promql-engine/execution/model" + "github.com/thanos-io/promql-engine/execution/telemetry" + "github.com/thanos-io/promql-engine/query" + + "github.com/efficientgo/core/errors" + "github.com/prometheus/prometheus/model/histogram" + "github.com/prometheus/prometheus/model/labels" +) + +type errorChan chan error + +func (c errorChan) getError() error { + for err := range c { + if err != nil { + return err + } + } + + return nil +} + +// coalesce is a model.VectorOperator that merges input vectors from multiple downstream operators +// into a single output vector. +// coalesce guarantees that samples from different input vectors will be added to the output in the same order +// as the input vectors themselves are provided in NewCoalesce. +type coalesce struct { + once sync.Once + series []labels.Labels + + wg sync.WaitGroup + operators []model.VectorOperator + batchSize int64 + + // inVectors is an internal per-step cache for references to input vectors. + inVectors [][]model.StepVector + // sampleOffsets holds per-operator offsets needed to map an input sample ID to an output sample ID. + sampleOffsets []uint64 + // seriesCounts holds the number of series per operator for pre-allocation. + seriesCounts []int + // tempBufs are reusable buffers for reading from operators + tempBufs [][]model.StepVector +} + +func NewCoalesce(opts *query.Options, batchSize int64, operators ...model.VectorOperator) model.VectorOperator { + if len(operators) == 1 { + return operators[0] + } + oper := &coalesce{ + sampleOffsets: make([]uint64, len(operators)), + operators: operators, + inVectors: make([][]model.StepVector, len(operators)), + batchSize: batchSize, + } + + return telemetry.NewOperator(telemetry.NewTelemetry(oper, opts), oper) +} + +func (c *coalesce) Explain() (next []model.VectorOperator) { + return c.operators +} + +func (c *coalesce) String() string { + return "[coalesce]" +} + +func (c *coalesce) Series(ctx context.Context) ([]labels.Labels, error) { + var err error + c.once.Do(func() { err = c.loadSeries(ctx) }) + if err != nil { + return nil, err + } + return c.series, nil +} + +func (c *coalesce) Next(ctx context.Context, buf []model.StepVector) (int, error) { + select { + case <-ctx.Done(): + return 0, ctx.Err() + default: + } + + var err error + c.once.Do(func() { err = c.loadSeries(ctx) }) + if err != nil { + return 0, err + } + + // Allocate temporary buffers on first use. + // Inner slices will be lazily pre-allocated by child operators when they append data. + if c.tempBufs == nil { + c.tempBufs = make([][]model.StepVector, len(c.operators)) + for i := range c.tempBufs { + c.tempBufs[i] = make([]model.StepVector, len(buf)) + } + } + + var mu sync.Mutex + var minTs int64 = math.MaxInt64 + var errChan = make(errorChan, len(c.operators)) + vectorCounts := make([]int, len(c.operators)) + + for idx, o := range c.operators { + // We already have a batch from the previous iteration. + if c.inVectors[idx] != nil { + mu.Lock() + if len(c.inVectors[idx]) > 0 { + minTs = min(minTs, c.inVectors[idx][0].T) + } + mu.Unlock() + continue + } + + c.wg.Add(1) + go func(opIdx int, o model.VectorOperator) { + defer c.wg.Done() + defer func() { + if r := recover(); r != nil { + errChan <- errors.Newf("unexpected panic: %v", r) + } + }() + + n, err := o.Next(ctx, c.tempBufs[opIdx]) + if err != nil { + errChan <- err + return + } + vectorCounts[opIdx] = n + + // Map input IDs to output IDs. + for i := range n { + vector := &c.tempBufs[opIdx][i] + for j := range vector.SampleIDs { + vector.SampleIDs[j] = vector.SampleIDs[j] + c.sampleOffsets[opIdx] + } + for j := range vector.HistogramIDs { + vector.HistogramIDs[j] = vector.HistogramIDs[j] + c.sampleOffsets[opIdx] + } + } + + if n > 0 { + c.inVectors[opIdx] = c.tempBufs[opIdx][:n] + mu.Lock() + minTs = min(minTs, c.tempBufs[opIdx][0].T) + mu.Unlock() + } else { + c.inVectors[opIdx] = nil + } + }(idx, o) + } + c.wg.Wait() + close(errChan) + + if err := errChan.getError(); err != nil { + return 0, err + } + + // Count vectors with minTs and prepare output + n := 0 + for opIdx, vectors := range c.inVectors { + if len(vectors) == 0 || vectors[0].T != minTs { + continue + } + + // Initialize output vectors if needed + if n == 0 { + maxSteps := min(len(vectors), len(buf)) + for i := range maxSteps { + buf[i].Reset(vectors[i].T) + // Ensure sufficient capacity for float samples. + // Histogram slices will grow on demand since most queries don't use them. + totalSamples := 0 + totalHistograms := 0 + for _, v := range c.inVectors { + if len(v) > i { + totalSamples += len(v[i].SampleIDs) + totalHistograms += len(v[i].HistogramIDs) + } + } + if cap(buf[i].SampleIDs) < totalSamples { + buf[i].SampleIDs = make([]uint64, 0, totalSamples) + buf[i].Samples = make([]float64, 0, totalSamples) + } + if totalHistograms > 0 && cap(buf[i].HistogramIDs) < totalHistograms { + buf[i].HistogramIDs = make([]uint64, 0, totalHistograms) + buf[i].Histograms = make([]*histogram.FloatHistogram, 0, totalHistograms) + } + } + n = maxSteps + } + + // Append samples from this operator + for i := 0; i < n && i < len(vectors); i++ { + buf[i].AppendSamples(vectors[i].SampleIDs, vectors[i].Samples) + buf[i].AppendHistograms(vectors[i].HistogramIDs, vectors[i].Histograms) + } + + // Keep remaining vectors for next iteration + if n < len(vectors) { + c.inVectors[opIdx] = vectors[n:] + } else { + c.inVectors[opIdx] = nil + } + } + + return n, nil +} + +func (c *coalesce) loadSeries(ctx context.Context) error { + var wg sync.WaitGroup + var numSeries uint64 + allSeries := make([][]labels.Labels, len(c.operators)) + errChan := make(errorChan, len(c.operators)) + for i := range c.operators { + wg.Add(1) + go func(i int) { + defer wg.Done() + defer func() { + if r := recover(); r != nil { + errChan <- errors.Newf("unexpected panic: %v", r) + } + }() + series, err := c.operators[i].Series(ctx) + if err != nil { + errChan <- err + return + } + + allSeries[i] = series + atomic.AddUint64(&numSeries, uint64(len(series))) + }(i) + } + wg.Wait() + close(errChan) + if err := errChan.getError(); err != nil { + return err + } + + c.sampleOffsets = make([]uint64, len(c.operators)) + c.seriesCounts = make([]int, len(c.operators)) + c.series = make([]labels.Labels, 0, numSeries) + for i, series := range allSeries { + c.sampleOffsets[i] = uint64(len(c.series)) + c.seriesCounts[i] = len(series) + c.series = append(c.series, series...) + } + + if c.batchSize == 0 || c.batchSize > int64(len(c.series)) { + c.batchSize = int64(len(c.series)) + } + return nil +} diff --git a/internal/promql-engine/execution/exchange/concurrent.go b/internal/promql-engine/execution/exchange/concurrent.go new file mode 100644 index 00000000000..cfbddbb0643 --- /dev/null +++ b/internal/promql-engine/execution/exchange/concurrent.go @@ -0,0 +1,171 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package exchange + +import ( + "context" + "fmt" + "sync" + + "github.com/thanos-io/promql-engine/execution/model" + "github.com/thanos-io/promql-engine/execution/telemetry" + "github.com/thanos-io/promql-engine/query" + + "github.com/efficientgo/core/errors" + "github.com/prometheus/prometheus/model/labels" +) + +type maybeStepVector struct { + err error + vectors []model.StepVector // The actual buffer with data + n int +} + +type concurrencyOperator struct { + once sync.Once + seriesOnce sync.Once + next model.VectorOperator + buffer chan maybeStepVector + bufferSize int + opts *query.Options + + // Buffer management for zero-copy swapping + // We maintain a pool of buffers that get swapped between producer and consumer + returnChan chan []model.StepVector // Channel to return buffers for reuse + + // seriesCount is used to pre-allocate inner slices of StepVectors + seriesCount int +} + +func NewConcurrent(next model.VectorOperator, bufferSize int, opts *query.Options) model.VectorOperator { + oper := &concurrencyOperator{ + next: next, + buffer: make(chan maybeStepVector, bufferSize), + bufferSize: bufferSize, + opts: opts, + returnChan: make(chan []model.StepVector, bufferSize+2), + } + + return telemetry.NewOperator(telemetry.NewTelemetry(oper, opts), oper) +} + +func (c *concurrencyOperator) Explain() (next []model.VectorOperator) { + return []model.VectorOperator{c.next} +} + +func (c *concurrencyOperator) String() string { + return fmt.Sprintf("[concurrent(buff=%v)]", c.bufferSize) +} + +func (c *concurrencyOperator) Series(ctx context.Context) ([]labels.Labels, error) { + series, err := c.next.Series(ctx) + if err != nil { + return nil, err + } + + // Initialize buffers. Inner slices will be allocated by the child operator + // which knows the actual batch size for pre-allocation. + c.seriesOnce.Do(func() { + c.seriesCount = len(series) + for i := 0; i < c.bufferSize+1; i++ { + c.returnChan <- make([]model.StepVector, c.opts.StepsBatch) + } + }) + + return series, nil +} + +func (c *concurrencyOperator) Next(ctx context.Context, buf []model.StepVector) (int, error) { + select { + case <-ctx.Done(): + return 0, ctx.Err() + default: + } + + // Ensure buffers are initialized (in case Series() wasn't called first) + c.seriesOnce.Do(func() { + // Fallback: create buffers without pre-sized inner slices + for i := 0; i < c.bufferSize+1; i++ { + c.returnChan <- make([]model.StepVector, c.opts.StepsBatch) + } + }) + + c.once.Do(func() { + go c.pull(ctx) + go c.drainBufferOnCancel(ctx) + }) + + r, ok := <-c.buffer + if !ok { + return 0, nil + } + if r.err != nil { + return 0, r.err + } + + // Zero-copy swap: move data from internal buffer to caller's buffer + // by swapping the slice contents directly + n := min(r.n, len(buf)) + for i := range n { + // Swap the step vector contents (this is just pointer/slice header swaps, not data copy) + buf[i], r.vectors[i] = r.vectors[i], buf[i] + } + + // Return the (now empty) buffer for reuse by the producer + c.returnChan <- r.vectors + + return n, nil +} + +func (c *concurrencyOperator) pull(ctx context.Context) { + defer func() { + if r := recover(); r != nil { + c.buffer <- maybeStepVector{err: errors.Newf("unexpected panic: %v", r)} + } + close(c.buffer) + }() + + for { + select { + case <-ctx.Done(): + c.buffer <- maybeStepVector{err: ctx.Err()} + return + default: + // Get an available buffer from the return channel + var readBuf []model.StepVector + select { + case readBuf = <-c.returnChan: + case <-ctx.Done(): + c.buffer <- maybeStepVector{err: ctx.Err()} + return + } + + n, err := c.next.Next(ctx, readBuf) + if err != nil { + // Return the buffer + c.returnChan <- readBuf + c.buffer <- maybeStepVector{err: err} + return + } + if n == 0 { + // Return the buffer + c.returnChan <- readBuf + return + } + + // Send the buffer with data + c.buffer <- maybeStepVector{vectors: readBuf, n: n} + } + } +} + +func (c *concurrencyOperator) drainBufferOnCancel(ctx context.Context) { + <-ctx.Done() + for r := range c.buffer { + if r.vectors != nil { + // Return the buffer + c.returnChan <- r.vectors + } + } +} diff --git a/internal/promql-engine/execution/exchange/dedup.go b/internal/promql-engine/execution/exchange/dedup.go new file mode 100644 index 00000000000..f07ce4b667a --- /dev/null +++ b/internal/promql-engine/execution/exchange/dedup.go @@ -0,0 +1,160 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package exchange + +import ( + "context" + "sync" + + "github.com/thanos-io/promql-engine/execution/model" + "github.com/thanos-io/promql-engine/execution/telemetry" + "github.com/thanos-io/promql-engine/query" + + "github.com/cespare/xxhash/v2" + "github.com/prometheus/prometheus/model/histogram" + "github.com/prometheus/prometheus/model/labels" +) + +type dedupSample struct { + t int64 + v float64 + h *histogram.FloatHistogram +} + +// The dedupCache is an internal cache used to deduplicate samples inside a single step vector. +type dedupCache []dedupSample + +// dedupOperator is a model.VectorOperator that deduplicates samples with +// same IDs inside a single model.StepVector. +// Deduplication is done using a last-sample-wins strategy, which means that +// if multiple samples with the same ID are present in a StepVector, dedupOperator +// will keep the last sample in that vector. +type dedupOperator struct { + once sync.Once + series []labels.Labels + + next model.VectorOperator + // outputIndex is a slice that is used as an index from input sample ID to output sample ID. + outputIndex []uint64 + dedupCache dedupCache +} + +func NewDedupOperator(next model.VectorOperator, opts *query.Options) model.VectorOperator { + oper := &dedupOperator{ + next: next, + } + return telemetry.NewOperator(telemetry.NewTelemetry(oper, opts), oper) +} + +func (d *dedupOperator) Next(ctx context.Context, buf []model.StepVector) (int, error) { + var err error + d.once.Do(func() { err = d.loadSeries(ctx) }) + if err != nil { + return 0, err + } + + n, err := d.next.Next(ctx, buf) + if err != nil { + return 0, err + } + if n == 0 { + return 0, nil + } + + // Process each input vector and overwrite it with the deduplicated output + for idx := range n { + vector := &buf[idx] + + // Update dedup cache with all samples from this vector + for i, inputSampleID := range vector.SampleIDs { + outputSampleID := d.outputIndex[inputSampleID] + d.dedupCache[outputSampleID].t = vector.T + d.dedupCache[outputSampleID].v = vector.Samples[i] + } + + for i, inputSampleID := range vector.HistogramIDs { + outputSampleID := d.outputIndex[inputSampleID] + d.dedupCache[outputSampleID].t = vector.T + d.dedupCache[outputSampleID].h = vector.Histograms[i] + } + + // Clear the vector and rebuild it with deduplicated data + t := vector.T + buf[idx].Reset(t) + + hint := len(d.series) + for outputSampleID, sample := range d.dedupCache { + // To avoid clearing the dedup cache for each step vector, we use the `t` field + // to detect whether a sample for the current step should be mapped to the output. + // If the timestamp of the sample does not match the input vector timestamp, it means that + // the sample was added in a previous iteration and should be skipped. + if sample.t == t { + if sample.h == nil { + buf[idx].AppendSampleWithSizeHint(uint64(outputSampleID), sample.v, hint) + } else { + buf[idx].AppendHistogramWithSizeHint(uint64(outputSampleID), sample.h, hint) + } + } + } + } + + return n, nil +} + +func (d *dedupOperator) Series(ctx context.Context) ([]labels.Labels, error) { + var err error + d.once.Do(func() { err = d.loadSeries(ctx) }) + if err != nil { + return nil, err + } + return d.series, nil +} + +func (d *dedupOperator) Explain() (next []model.VectorOperator) { + return []model.VectorOperator{d.next} +} + +func (d *dedupOperator) String() string { + return "[dedup]" +} + +func (d *dedupOperator) loadSeries(ctx context.Context) error { + series, err := d.next.Series(ctx) + if err != nil { + return err + } + + outputIndex := make(map[uint64]uint64) + inputIndex := make([]uint64, len(series)) + hashBuf := make([]byte, 0, 128) + for inputSeriesID, inputSeries := range series { + hash := hashSeries(hashBuf, inputSeries) + + inputIndex[inputSeriesID] = hash + outputSeriesID, ok := outputIndex[hash] + if !ok { + outputSeriesID = uint64(len(d.series)) + d.series = append(d.series, inputSeries) + } + outputIndex[hash] = outputSeriesID + } + + d.outputIndex = make([]uint64, len(inputIndex)) + for inputSeriesID, hash := range inputIndex { + outputSeriesID := outputIndex[hash] + d.outputIndex[inputSeriesID] = outputSeriesID + } + d.dedupCache = make(dedupCache, len(outputIndex)) + for i := range d.dedupCache { + d.dedupCache[i].t = -1 + } + + return nil +} + +func hashSeries(hashBuf []byte, inputSeries labels.Labels) uint64 { + hashBuf = hashBuf[:0] + hash := xxhash.Sum64(inputSeries.Bytes(hashBuf)) + return hash +} diff --git a/internal/promql-engine/execution/exchange/duplicate_label.go b/internal/promql-engine/execution/exchange/duplicate_label.go new file mode 100644 index 00000000000..b18fb35c465 --- /dev/null +++ b/internal/promql-engine/execution/exchange/duplicate_label.go @@ -0,0 +1,121 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package exchange + +import ( + "context" + "sync" + + "github.com/thanos-io/promql-engine/execution/model" + "github.com/thanos-io/promql-engine/execution/telemetry" + "github.com/thanos-io/promql-engine/extlabels" + "github.com/thanos-io/promql-engine/query" + + "github.com/prometheus/prometheus/model/labels" +) + +type pair struct{ a, b int } + +type duplicateLabelCheckOperator struct { + once sync.Once + next model.VectorOperator + + p []pair + c []uint64 +} + +func NewDuplicateLabelCheck(next model.VectorOperator, opts *query.Options) model.VectorOperator { + oper := &duplicateLabelCheckOperator{ + next: next, + } + return telemetry.NewOperator(telemetry.NewTelemetry(oper, opts), oper) +} + +func (d *duplicateLabelCheckOperator) Next(ctx context.Context, buf []model.StepVector) (int, error) { + select { + case <-ctx.Done(): + return 0, ctx.Err() + default: + } + + if err := d.init(ctx); err != nil { + return 0, err + } + + n, err := d.next.Next(ctx, buf) + if err != nil { + return 0, err + } + if n == 0 { + return 0, nil + } + + // TODO: currently there is a bug, we need to reset 'd.c's state + // if the current timestamp changes. With configured BatchSize we + // dont see all samples for a timestamp in the same batch, but this + // logic relies on that. + if len(d.p) > 0 { + for i := range d.p { + d.c[d.p[i].a] = 0 + d.c[d.p[i].b] = 0 + } + for i := range n { + sv := &buf[i] + for _, sid := range sv.SampleIDs { + d.c[sid] |= 2 << i + } + } + for i := range d.p { + if d.c[d.p[i].a]&d.c[d.p[i].b] > 0 { + return 0, extlabels.ErrDuplicateLabelSet + } + } + } + + return n, nil +} + +func (d *duplicateLabelCheckOperator) Series(ctx context.Context) ([]labels.Labels, error) { + if err := d.init(ctx); err != nil { + return nil, err + } + series, err := d.next.Series(ctx) + if err != nil { + return nil, err + } + return series, nil +} + +func (d *duplicateLabelCheckOperator) Explain() (next []model.VectorOperator) { + return []model.VectorOperator{d.next} +} + +func (d *duplicateLabelCheckOperator) String() string { + return "[duplicateLabelCheck]" +} + +func (d *duplicateLabelCheckOperator) init(ctx context.Context) error { + var err error + d.once.Do(func() { + series, seriesErr := d.next.Series(ctx) + if seriesErr != nil { + err = seriesErr + return + } + m := make(map[uint64]int, len(series)) + p := make([]pair, 0) + c := make([]uint64, len(series)) + for i := range series { + h := series[i].Hash() + if j, ok := m[h]; ok { + p = append(p, pair{a: i, b: j}) + } else { + m[h] = i + } + } + d.p = p + d.c = c + }) + return err +} diff --git a/internal/promql-engine/execution/execution.go b/internal/promql-engine/execution/execution.go new file mode 100644 index 00000000000..8df9332d8dd --- /dev/null +++ b/internal/promql-engine/execution/execution.go @@ -0,0 +1,427 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +// Copyright 2013 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package execution + +import ( + "context" + "sort" + "time" + + "github.com/thanos-io/promql-engine/execution/aggregate" + "github.com/thanos-io/promql-engine/execution/binary" + "github.com/thanos-io/promql-engine/execution/exchange" + "github.com/thanos-io/promql-engine/execution/function" + "github.com/thanos-io/promql-engine/execution/model" + "github.com/thanos-io/promql-engine/execution/noop" + "github.com/thanos-io/promql-engine/execution/parse" + "github.com/thanos-io/promql-engine/execution/remote" + "github.com/thanos-io/promql-engine/execution/scan" + "github.com/thanos-io/promql-engine/execution/step_invariant" + "github.com/thanos-io/promql-engine/execution/unary" + "github.com/thanos-io/promql-engine/logicalplan" + "github.com/thanos-io/promql-engine/query" + "github.com/thanos-io/promql-engine/storage" + + "github.com/efficientgo/core/errors" + "github.com/prometheus/prometheus/promql" + "github.com/prometheus/prometheus/promql/parser" + promstorage "github.com/prometheus/prometheus/storage" +) + +// New creates new physical query execution for a given query expression which represents logical plan. +// TODO(bwplotka): Add definition (could be parameters for each execution operator) we can optimize - it would represent physical plan. +func New(ctx context.Context, expr logicalplan.Node, storage storage.Scanners, opts *query.Options) (model.VectorOperator, error) { + hints := promstorage.SelectHints{ + Start: opts.Start.UnixMilli(), + End: opts.End.UnixMilli(), + Step: opts.Step.Milliseconds(), + } + return newOperator(ctx, expr, storage, opts, hints) +} + +func newOperator(ctx context.Context, expr logicalplan.Node, storage storage.Scanners, opts *query.Options, hints promstorage.SelectHints) (model.VectorOperator, error) { + switch e := expr.(type) { + case *logicalplan.NumberLiteral: + return scan.NewNumberLiteralSelector(opts, e.Val), nil + case *logicalplan.VectorSelector: + return newVectorSelector(ctx, e, storage, opts, hints) + case *logicalplan.FunctionCall: + return newCall(ctx, e, storage, opts, hints) + case *logicalplan.Aggregation: + return newAggregateExpression(ctx, e, storage, opts, hints) + case *logicalplan.Binary: + return newBinaryExpression(ctx, e, storage, opts, hints) + case *logicalplan.Parens: + return newOperator(ctx, e.Expr, storage, opts, hints) + case *logicalplan.Unary: + return newUnaryExpression(ctx, e, storage, opts, hints) + case *logicalplan.StepInvariantExpr: + return newStepInvariantExpression(ctx, e, storage, opts, hints) + case logicalplan.Deduplicate: + return newDeduplication(ctx, e, storage, opts, hints) + case logicalplan.RemoteExecution: + return newRemoteExecution(ctx, e, opts, hints) + case *logicalplan.CheckDuplicateLabels: + return newDuplicateLabelCheck(ctx, e, storage, opts, hints) + case logicalplan.Noop: + return noop.NewOperator(opts), nil + case logicalplan.UserDefinedExpr: + return e.MakeExecutionOperator(ctx, opts, hints) + default: + return nil, errors.Wrapf(parse.ErrNotSupportedExpr, "got: %s (%T)", e, e) + } +} + +func newVectorSelector(ctx context.Context, e *logicalplan.VectorSelector, scanners storage.Scanners, opts *query.Options, hints promstorage.SelectHints) (model.VectorOperator, error) { + start, end := getTimeRangesForVectorSelector(e, opts, 0) + hints.Start = start + hints.End = end + op, err := scanners.NewVectorSelector(ctx, opts, hints, *e) + if err != nil { + return nil, err + } + return model.WithID(op, logicalplan.NodeFingerprint(e)), nil +} + +func newCall(ctx context.Context, e *logicalplan.FunctionCall, scanners storage.Scanners, opts *query.Options, hints promstorage.SelectHints) (model.VectorOperator, error) { + hints.Func = e.Func.Name + hints.Grouping = nil + hints.By = false + + if e.Func.Name == "absent_over_time" { + return newAbsentOverTimeOperator(ctx, e, scanners, opts, hints) + } + if e.Func.Name == "timestamp" { + switch arg := e.Args[0].(type) { + case *logicalplan.VectorSelector: + arg.SelectTimestamp = true + return newVectorSelector(ctx, arg, scanners, opts, hints) + case *logicalplan.StepInvariantExpr: + // Step invariant expressions on vector selectors need to be unwrapped so that we + // can return the original timestamp rather than the step invariant one. + switch vs := arg.Expr.(type) { + case *logicalplan.VectorSelector: + // Prometheus weirdness. + if vs.Timestamp != nil { + vs.OriginalOffset = 0 + } + vs.SelectTimestamp = true + return newVectorSelector(ctx, vs, scanners, opts, hints) + } + return newInstantVectorFunction(ctx, e, scanners, opts, hints) + } + return newInstantVectorFunction(ctx, e, scanners, opts, hints) + } + + // TODO(saswatamcode): Range vector result might need new operator + // before it can be non-nested. https://github.com/thanos-io/promql-engine/issues/39 + for i := range e.Args { + switch t := e.Args[i].(type) { + case *logicalplan.Subquery: + return newSubqueryFunction(ctx, e, t, scanners, opts, hints) + case *logicalplan.MatrixSelector: + return newRangeVectorFunction(ctx, e, t, scanners, opts, hints) + } + } + return newInstantVectorFunction(ctx, e, scanners, opts, hints) +} + +func newAbsentOverTimeOperator(ctx context.Context, call *logicalplan.FunctionCall, scanners storage.Scanners, opts *query.Options, hints promstorage.SelectHints) (model.VectorOperator, error) { + switch arg := call.Args[0].(type) { + case *logicalplan.Subquery: + matrixCall := &logicalplan.FunctionCall{ + Func: parser.Function{Name: "last_over_time"}, + } + argOp, err := newSubqueryFunction(ctx, matrixCall, arg, scanners, opts, hints) + if err != nil { + return nil, err + } + f := &logicalplan.FunctionCall{ + Func: parser.Function{Name: "absent"}, + Args: []logicalplan.Node{matrixCall}, + } + return function.NewFunctionOperator(f, []model.VectorOperator{argOp}, opts.StepsBatch, opts) + case *logicalplan.MatrixSelector: + matrixCall := &logicalplan.FunctionCall{ + Func: parser.Function{Name: "last_over_time"}, + Args: call.Args, + } + argOp, err := newRangeVectorFunction(ctx, matrixCall, arg, scanners, opts, hints) + if err != nil { + return nil, err + } + f := &logicalplan.FunctionCall{ + Func: parser.Function{Name: "absent"}, + Args: []logicalplan.Node{&logicalplan.MatrixSelector{ + VectorSelector: arg.VectorSelector, + Range: arg.Range, + OriginalString: arg.String(), + }}, + } + return function.NewFunctionOperator(f, []model.VectorOperator{argOp}, opts.StepsBatch, opts) + default: + return nil, parse.ErrNotSupportedExpr + } +} + +func newRangeVectorFunction(ctx context.Context, e *logicalplan.FunctionCall, t *logicalplan.MatrixSelector, scanners storage.Scanners, opts *query.Options, hints promstorage.SelectHints) (model.VectorOperator, error) { + // TODO(saswatamcode): Range vector result might need new operator + // before it can be non-nested. https://github.com/thanos-io/promql-engine/issues/39 + milliSecondRange := t.Range.Milliseconds() + if parse.IsExtFunction(e.Func.Name) { + milliSecondRange += opts.ExtLookbackDelta.Milliseconds() + } + + start, end := getTimeRangesForVectorSelector(t.VectorSelector, opts, milliSecondRange) + hints.Start = start + hints.End = end + hints.Range = milliSecondRange + op, err := scanners.NewMatrixSelector(ctx, opts, hints, *t, *e) + if err != nil { + return nil, err + } + return model.WithID(op, logicalplan.NodeFingerprint(t)), nil +} + +func newSubqueryFunction(ctx context.Context, e *logicalplan.FunctionCall, t *logicalplan.Subquery, storage storage.Scanners, opts *query.Options, hints promstorage.SelectHints) (model.VectorOperator, error) { + // TODO: We dont implement ext functions + if parse.IsExtFunction(e.Func.Name) { + return nil, parse.ErrNotImplemented + } + + nOpts := query.NestedOptionsForSubquery(opts, t.Step, t.Range, t.Offset) + + hints.Start = nOpts.Start.UnixMilli() + hints.End = nOpts.End.UnixMilli() + hints.Step = nOpts.Step.Milliseconds() + + inner, err := newOperator(ctx, t.Expr, storage, nOpts, hints) + if err != nil { + return nil, err + } + + outerOpts := *opts + if t.Timestamp != nil { + outerOpts.Start = time.UnixMilli(*t.Timestamp) + outerOpts.End = time.UnixMilli(*t.Timestamp) + } + + var scalarArg model.VectorOperator + var scalarArg2 model.VectorOperator + switch e.Func.Name { + case "quantile_over_time": + // quantile_over_time(scalar, range-vector) + scalarArg, err = newOperator(ctx, e.Args[0], storage, opts, hints) + if err != nil { + return nil, err + } + case "predict_linear": + // predict_linear(range-vector, scalar) + scalarArg, err = newOperator(ctx, e.Args[1], storage, opts, hints) + if err != nil { + return nil, err + } + case "double_exponential_smoothing": + // double_exponential_smoothing(range-vector, scalar, scalar) + scalarArg, err = newOperator(ctx, e.Args[1], storage, opts, hints) + if err != nil { + return nil, err + } + scalarArg2, err = newOperator(ctx, e.Args[2], storage, opts, hints) + if err != nil { + return nil, err + } + } + + return scan.NewSubqueryOperator(inner, scalarArg, scalarArg2, &outerOpts, e, t) +} + +func newInstantVectorFunction(ctx context.Context, e *logicalplan.FunctionCall, storage storage.Scanners, opts *query.Options, hints promstorage.SelectHints) (model.VectorOperator, error) { + nextOperators := make([]model.VectorOperator, 0, len(e.Args)) + for i := range e.Args { + // Strings don't need an operator + if e.Args[i].ReturnType() == parser.ValueTypeString { + continue + } + next, err := newOperator(ctx, e.Args[i], storage, opts, hints) + if err != nil { + return nil, err + } + nextOperators = append(nextOperators, next) + } + + return function.NewFunctionOperator(e, nextOperators, opts.StepsBatch, opts) +} + +func newAggregateExpression(ctx context.Context, e *logicalplan.Aggregation, scanners storage.Scanners, opts *query.Options, hints promstorage.SelectHints) (model.VectorOperator, error) { + hints.Func = e.Op.String() + hints.Grouping = e.Grouping + hints.By = !e.Without + + next, err := newOperator(ctx, e.Expr, scanners, opts, hints) + if err != nil { + return nil, err + } + if e.Op == parser.COUNT_VALUES { + param := logicalplan.UnsafeUnwrapString(e.Param) + return aggregate.NewCountValues(next, param, !e.Without, e.Grouping, opts), nil + } + + // parameter is only required for count_values, quantile, topk, bottomk, limitk, and limit_ratio. + var paramOp model.VectorOperator + switch e.Op { + case parser.QUANTILE, parser.TOPK, parser.BOTTOMK, parser.LIMITK, parser.LIMIT_RATIO: + paramOp, err = newOperator(ctx, e.Param, scanners, opts, hints) + if err != nil { + return nil, err + } + } + if e.Op == parser.TOPK || e.Op == parser.BOTTOMK || e.Op == parser.LIMITK || e.Op == parser.LIMIT_RATIO { + next, err = aggregate.NewKHashAggregate(next, paramOp, e.Op, !e.Without, e.Grouping, opts) + } else { + next, err = aggregate.NewHashAggregate(next, paramOp, e.Op, !e.Without, e.Grouping, opts) + } + if err != nil { + return nil, err + } + + return exchange.NewConcurrent(next, 2, opts), nil +} + +func newBinaryExpression(ctx context.Context, e *logicalplan.Binary, scanners storage.Scanners, opts *query.Options, hints promstorage.SelectHints) (model.VectorOperator, error) { + if e.LHS.ReturnType() == parser.ValueTypeScalar || e.RHS.ReturnType() == parser.ValueTypeScalar { + return newScalarBinaryOperator(ctx, e, scanners, opts, hints) + } + return newVectorBinaryOperator(ctx, e, scanners, opts, hints) +} + +func newVectorBinaryOperator(ctx context.Context, e *logicalplan.Binary, storage storage.Scanners, opts *query.Options, hints promstorage.SelectHints) (model.VectorOperator, error) { + leftOperator, err := newOperator(ctx, e.LHS, storage, opts, hints) + if err != nil { + return nil, err + } + rightOperator, err := newOperator(ctx, e.RHS, storage, opts, hints) + if err != nil { + return nil, err + } + return binary.NewVectorOperator(leftOperator, rightOperator, e.VectorMatching, e.Op, e.ReturnBool, opts) +} + +func newScalarBinaryOperator(ctx context.Context, e *logicalplan.Binary, storage storage.Scanners, opts *query.Options, hints promstorage.SelectHints) (model.VectorOperator, error) { + lhs, err := newOperator(ctx, e.LHS, storage, opts, hints) + if err != nil { + return nil, err + } + rhs, err := newOperator(ctx, e.RHS, storage, opts, hints) + if err != nil { + return nil, err + } + + return binary.NewScalar(lhs, rhs, e.LHS.ReturnType(), e.RHS.ReturnType(), e.Op, e.ReturnBool, opts) +} + +func newUnaryExpression(ctx context.Context, e *logicalplan.Unary, scanners storage.Scanners, opts *query.Options, hints promstorage.SelectHints) (model.VectorOperator, error) { + next, err := newOperator(ctx, e.Expr, scanners, opts, hints) + if err != nil { + return nil, err + } + switch e.Op { + case parser.ADD: + return next, nil + case parser.SUB: + return unary.NewUnaryNegation(next, opts) + default: + // This shouldn't happen as Op was validated when parsing already + // https://github.com/prometheus/prometheus/blob/v2.38.0/promql/parser/parse.go#L573. + return nil, errors.Wrapf(parse.ErrNotSupportedExpr, "got: %s", e) + } +} + +func newStepInvariantExpression(ctx context.Context, e *logicalplan.StepInvariantExpr, scanners storage.Scanners, opts *query.Options, hints promstorage.SelectHints) (model.VectorOperator, error) { + switch t := e.Expr.(type) { + case *logicalplan.NumberLiteral: + return scan.NewNumberLiteralSelector(opts, t.Val), nil + } + next, err := newOperator(ctx, e.Expr, scanners, opts.WithEndTime(opts.Start), hints) + if err != nil { + return nil, err + } + return step_invariant.NewStepInvariantOperator(next, e.Expr, opts) +} + +func newDeduplication(ctx context.Context, e logicalplan.Deduplicate, scanners storage.Scanners, opts *query.Options, hints promstorage.SelectHints) (model.VectorOperator, error) { + // The Deduplicate operator will deduplicate samples using a last-sample-wins strategy. + // Sorting engines by MaxT ensures that samples produced due to + // staleness will be overwritten and corrected by samples coming from + // engines with a higher max time. + sort.Slice(e.Expressions, func(i, j int) bool { + return e.Expressions[i].Engine.MaxT() < e.Expressions[j].Engine.MaxT() + }) + + operators := make([]model.VectorOperator, len(e.Expressions)) + for i, expr := range e.Expressions { + operator, err := newOperator(ctx, expr, scanners, opts, hints) + if err != nil { + return nil, err + } + operators[i] = operator + } + coalesce := exchange.NewCoalesce(opts, 0, operators...) + dedup := exchange.NewDedupOperator(coalesce, opts) + return exchange.NewConcurrent(dedup, 2, opts), nil +} + +func newRemoteExecution(ctx context.Context, e logicalplan.RemoteExecution, opts *query.Options, hints promstorage.SelectHints) (model.VectorOperator, error) { + // Create a new remote query scoped to the calculated start time. + qry, err := e.Engine.NewRangeQuery(ctx, promql.NewPrometheusQueryOpts(false, opts.LookbackDelta), e.Query, e.QueryRangeStart, e.QueryRangeEnd, opts.Step) + if err != nil { + return nil, err + } + + // The selector uses the original query time to make sure that steps from different + // operators have the same timestamps. + // We need to set the lookback for the selector to 0 since the remote query already applies one lookback. + selectorOpts := *opts + selectorOpts.LookbackDelta = 0 + remoteExec := remote.NewExecution(qry, e.QueryRangeStart, e.QueryRangeEnd, e.Engine.LabelSets(), &selectorOpts, hints) + return exchange.NewConcurrent(model.WithID(remoteExec, logicalplan.NodeFingerprint(e)), 2, opts), nil +} + +func newDuplicateLabelCheck(ctx context.Context, e *logicalplan.CheckDuplicateLabels, storage storage.Scanners, opts *query.Options, hints promstorage.SelectHints) (model.VectorOperator, error) { + op, err := newOperator(ctx, e.Expr, storage, opts, hints) + if err != nil { + return nil, err + } + return exchange.NewDuplicateLabelCheck(op, opts), nil +} + +// Copy from https://github.com/prometheus/prometheus/blob/v2.39.1/promql/engine.go#L791. +func getTimeRangesForVectorSelector(n *logicalplan.VectorSelector, opts *query.Options, evalRange int64) (int64, int64) { + start := opts.Start.UnixMilli() + end := opts.End.UnixMilli() + if n.Timestamp != nil { + start = *n.Timestamp + end = *n.Timestamp + } + if evalRange == 0 { + start -= opts.LookbackDelta.Milliseconds() - 1 + } else { + start -= evalRange - 1 + } + offset := n.OriginalOffset.Milliseconds() + return start - offset, end - offset +} diff --git a/internal/promql-engine/execution/function/absent.go b/internal/promql-engine/execution/function/absent.go new file mode 100644 index 00000000000..deb3e7a503d --- /dev/null +++ b/internal/promql-engine/execution/function/absent.go @@ -0,0 +1,106 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package function + +import ( + "context" + "sync" + + "github.com/thanos-io/promql-engine/execution/model" + "github.com/thanos-io/promql-engine/execution/telemetry" + "github.com/thanos-io/promql-engine/logicalplan" + "github.com/thanos-io/promql-engine/query" + + "github.com/prometheus/prometheus/model/labels" +) + +type absentOperator struct { + once sync.Once + funcExpr *logicalplan.FunctionCall + series []labels.Labels + next model.VectorOperator +} + +func newAbsentOperator( + funcExpr *logicalplan.FunctionCall, + next model.VectorOperator, + opts *query.Options, +) model.VectorOperator { + oper := &absentOperator{ + funcExpr: funcExpr, + next: next, + } + return telemetry.NewOperator(telemetry.NewTelemetry(oper, opts), oper) +} + +func (o *absentOperator) String() string { + return "[absent]" +} + +func (o *absentOperator) Explain() (next []model.VectorOperator) { + return []model.VectorOperator{o.next} +} + +func (o *absentOperator) Series(_ context.Context) ([]labels.Labels, error) { + o.loadSeries() + return o.series, nil +} + +func (o *absentOperator) loadSeries() { + // we need to put the filtered labels back for absent to compute its series properly + o.once.Do(func() { + // https://github.com/prometheus/prometheus/blob/df1b4da348a7c2f8c0b294ffa1f05db5f6641278/promql/functions.go#L1857 + var lm []*labels.Matcher + switch n := o.funcExpr.Args[0].(type) { + case *logicalplan.VectorSelector: + lm = append(n.LabelMatchers, n.Filters...) + case *logicalplan.MatrixSelector: + v := n.VectorSelector + lm = append(v.LabelMatchers, v.Filters...) + default: + o.series = []labels.Labels{labels.EmptyLabels()} + return + } + + has := make(map[string]bool) + b := labels.NewBuilder(labels.EmptyLabels()) + for _, l := range lm { + if l.Name == labels.MetricName { + continue + } + if l.Type == labels.MatchEqual && !has[l.Name] { + b.Set(l.Name, l.Value) + has[l.Name] = true + } else { + b.Del(l.Name) + } + } + o.series = []labels.Labels{b.Labels()} + }) +} + +func (o *absentOperator) Next(ctx context.Context, buf []model.StepVector) (int, error) { + select { + case <-ctx.Done(): + return 0, ctx.Err() + default: + } + + o.loadSeries() + + n, err := o.next.Next(ctx, buf) + if err != nil { + return 0, err + } + + for i := range n { + vector := &buf[i] + isEmpty := len(vector.Samples) == 0 && len(vector.Histograms) == 0 + vector.Reset(vector.T) + if isEmpty { + vector.AppendSample(0, 1) + } + } + return n, nil +} diff --git a/internal/promql-engine/execution/function/functions.go b/internal/promql-engine/execution/function/functions.go new file mode 100644 index 00000000000..ace2707d442 --- /dev/null +++ b/internal/promql-engine/execution/function/functions.go @@ -0,0 +1,329 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package function + +import ( + "math" + "time" + + "github.com/thanos-io/promql-engine/compute" + + "github.com/prometheus/prometheus/model/histogram" +) + +type functionCall func(f float64, h *histogram.FloatHistogram, vargs ...float64) (float64, bool) + +var instantVectorFuncs = map[string]functionCall{ + "abs": simpleFunc(math.Abs), + "ceil": simpleFunc(math.Ceil), + "exp": simpleFunc(math.Exp), + "floor": simpleFunc(math.Floor), + "sqrt": simpleFunc(math.Sqrt), + "ln": simpleFunc(math.Log), + "log2": simpleFunc(math.Log2), + "log10": simpleFunc(math.Log10), + "sin": simpleFunc(math.Sin), + "cos": simpleFunc(math.Cos), + "tan": simpleFunc(math.Tan), + "asin": simpleFunc(math.Asin), + "acos": simpleFunc(math.Acos), + "atan": simpleFunc(math.Atan), + "sinh": simpleFunc(math.Sinh), + "cosh": simpleFunc(math.Cosh), + "tanh": simpleFunc(math.Tanh), + "asinh": simpleFunc(math.Asinh), + "acosh": simpleFunc(math.Acosh), + "atanh": simpleFunc(math.Atanh), + "rad": simpleFunc(func(v float64) float64 { + return v * math.Pi / 180 + }), + "deg": simpleFunc(func(v float64) float64 { + return v * 180 / math.Pi + }), + "sgn": simpleFunc(func(v float64) float64 { + var sign float64 + if v > 0 { + sign = 1 + } else if v < 0 { + sign = -1 + } + if math.IsNaN(v) { + sign = math.NaN() + } + return sign + }), + "round": func(f float64, h *histogram.FloatHistogram, vargs ...float64) (float64, bool) { + if h != nil { + return 0., false + } + + if len(vargs) > 1 { + return 0., false + } + + toNearest := 1.0 + if len(vargs) > 0 { + toNearest = vargs[0] + } + toNearestInverse := 1.0 / toNearest + return math.Floor(f*toNearestInverse+0.5) / toNearestInverse, true + }, + "pi": func(float64, *histogram.FloatHistogram, ...float64) (float64, bool) { + return math.Pi, true + }, + "vector": func(f float64, h *histogram.FloatHistogram, vargs ...float64) (float64, bool) { + return f, true + }, + "clamp": func(f float64, h *histogram.FloatHistogram, vargs ...float64) (float64, bool) { + if h != nil { + return 0., false + } + + if len(vargs) != 2 { + return 0., false + } + + v := f + min := vargs[0] + max := vargs[1] + + if max < min { + return 0., false + } + + return math.Max(min, math.Min(max, v)), true + }, + "clamp_min": func(f float64, h *histogram.FloatHistogram, vargs ...float64) (float64, bool) { + if h != nil { + return 0., false + } + + if len(vargs) != 1 { + return 0., false + } + + v := f + min := vargs[0] + + return math.Max(min, v), true + }, + "clamp_max": func(f float64, h *histogram.FloatHistogram, vargs ...float64) (float64, bool) { + if h != nil { + return 0., false + } + + if len(vargs) != 1 { + return 0., false + } + + v := f + max := vargs[0] + + return math.Min(max, v), true + }, + "histogram_sum": func(f float64, h *histogram.FloatHistogram, vargs ...float64) (float64, bool) { + if h == nil { + return 0., false + } + return h.Sum, true + }, + "histogram_count": func(f float64, h *histogram.FloatHistogram, vargs ...float64) (float64, bool) { + if h == nil { + return 0., false + } + return h.Count, true + }, + "histogram_avg": func(f float64, h *histogram.FloatHistogram, vargs ...float64) (float64, bool) { + if h == nil { + return 0., false + } + return h.Sum / h.Count, true + }, + "histogram_stddev": func(f float64, h *histogram.FloatHistogram, vargs ...float64) (float64, bool) { + if h == nil { + return 0., false + } + return histogramStdDev(h), true + }, + "histogram_stdvar": func(f float64, h *histogram.FloatHistogram, vargs ...float64) (float64, bool) { + if h == nil { + return 0., false + } + return histogramStdVar(h), true + }, + // variants of date time functions with an argument + "days_in_month": dateTimeFunc(daysInMonth), + "day_of_month": dateTimeFunc(dayOfMonth), + "day_of_week": dateTimeFunc(dayOfWeek), + "day_of_year": dateTimeFunc(dayOfYear), + "hour": dateTimeFunc(hour), + "minute": dateTimeFunc(minute), + "month": dateTimeFunc(month), + "year": dateTimeFunc(year), + // hack we only have sort functions as argument for "timestamp" possibly so they dont actually + // need to sort anything. This is only for compatibility to prometheus as this sort of query does + // not make too much sense. + "sort": simpleFunc(func(v float64) float64 { + return v + }), + "sort_desc": simpleFunc(func(v float64) float64 { + return v + }), + "sort_by_label": simpleFunc(func(v float64) float64 { + return v + }), + "sort_by_label_desc": simpleFunc(func(v float64) float64 { + return v + }), +} + +type noArgFunctionCall func(t int64) float64 + +var noArgFuncs = map[string]noArgFunctionCall{ + "pi": func(_ int64) float64 { + return math.Pi + }, + "time": func(t int64) float64 { + return float64(t) / 1000 + }, + // variants of date time functions with no argument + "days_in_month": dateTimeNoArgFunc(daysInMonth), + "day_of_month": dateTimeNoArgFunc(dayOfMonth), + "day_of_week": dateTimeNoArgFunc(dayOfWeek), + "day_of_year": dateTimeNoArgFunc(dayOfYear), + "hour": dateTimeNoArgFunc(hour), + "minute": dateTimeNoArgFunc(minute), + "month": dateTimeNoArgFunc(month), + "year": dateTimeNoArgFunc(year), +} + +func simpleFunc(f func(float64) float64) functionCall { + return func(v float64, h *histogram.FloatHistogram, vargs ...float64) (float64, bool) { + if h != nil { + return 0., false + } + return f(v), true + } +} + +func dateTimeFunc(f func(time.Time) float64) functionCall { + return func(v float64, h *histogram.FloatHistogram, vargs ...float64) (float64, bool) { + if h != nil { + return 0., false + } + return f(dateFromSampleValue(v)), true + } +} + +func dateTimeNoArgFunc(f func(time.Time) float64) noArgFunctionCall { + return func(t int64) float64 { + return f(dateFromStepTime(t)) + } +} + +func dateFromSampleValue(f float64) time.Time { + return time.Unix(int64(f), 0).UTC() +} + +func dateFromStepTime(t int64) time.Time { + return time.Unix(t/1000, 0).UTC() +} + +func daysInMonth(t time.Time) float64 { + return float64(32 - time.Date(t.Year(), t.Month(), 32, 0, 0, 0, 0, time.UTC).Day()) +} + +func dayOfMonth(t time.Time) float64 { + return float64(t.Day()) +} + +func dayOfWeek(t time.Time) float64 { + return float64(t.Weekday()) +} + +func dayOfYear(t time.Time) float64 { + return float64(t.YearDay()) +} + +func hour(t time.Time) float64 { + return float64(t.Hour()) +} + +func minute(t time.Time) float64 { + return float64(t.Minute()) +} + +func month(t time.Time) float64 { + return float64(t.Month()) +} + +func year(t time.Time) float64 { + return float64(t.Year()) +} + +// TODO: import from prometheus once exported there. +func histogramStdDev(h *histogram.FloatHistogram) float64 { + mean := h.Sum / h.Count + var variance, cVariance float64 + it := h.AllBucketIterator() + for it.Next() { + bucket := it.At() + if bucket.Count == 0 { + continue + } + var val float64 + switch { + case h.UsesCustomBuckets(): + // Use arithmetic mean in case of custom buckets. + val = (bucket.Upper + bucket.Lower) / 2.0 + case bucket.Lower <= 0 && bucket.Upper >= 0: + // Use zero (effectively the arithmetic mean) in the zero bucket of a standard exponential histogram. + val = 0 + default: + // Use geometric mean in case of standard exponential buckets. + val = math.Sqrt(bucket.Upper * bucket.Lower) + if bucket.Upper < 0 { + val = -val + } + } + delta := val - mean + variance, cVariance = compute.KahanSumInc(bucket.Count*delta*delta, variance, cVariance) + } + variance += cVariance + variance /= h.Count + return math.Sqrt(variance) +} + +// TODO: import from prometheus once exported there. +func histogramStdVar(h *histogram.FloatHistogram) float64 { + mean := h.Sum / h.Count + var variance, cVariance float64 + it := h.AllBucketIterator() + for it.Next() { + bucket := it.At() + if bucket.Count == 0 { + continue + } + var val float64 + switch { + case h.UsesCustomBuckets(): + // Use arithmetic mean in case of custom buckets. + val = (bucket.Upper + bucket.Lower) / 2.0 + case bucket.Lower <= 0 && bucket.Upper >= 0: + // Use zero (effectively the arithmetic mean) in the zero bucket of a standard exponential histogram. + val = 0 + default: + // Use geometric mean in case of standard exponential buckets. + val = math.Sqrt(bucket.Upper * bucket.Lower) + if bucket.Upper < 0 { + val = -val + } + } + delta := val - mean + variance, cVariance = compute.KahanSumInc(bucket.Count*delta*delta, variance, cVariance) + } + variance += cVariance + variance /= h.Count + return variance +} diff --git a/internal/promql-engine/execution/function/histogram.go b/internal/promql-engine/execution/function/histogram.go new file mode 100644 index 00000000000..e3df8508ac2 --- /dev/null +++ b/internal/promql-engine/execution/function/histogram.go @@ -0,0 +1,368 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package function + +import ( + "context" + "fmt" + "math" + "strconv" + "sync" + + "github.com/thanos-io/promql-engine/execution/model" + "github.com/thanos-io/promql-engine/execution/telemetry" + "github.com/thanos-io/promql-engine/extlabels" + "github.com/thanos-io/promql-engine/logicalplan" + "github.com/thanos-io/promql-engine/query" + "github.com/thanos-io/promql-engine/warnings" + + "github.com/cespare/xxhash/v2" + "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/promql" + "github.com/prometheus/prometheus/promql/parser/posrange" + "github.com/prometheus/prometheus/util/annotations" +) + +type histogramSeries struct { + outputID int + upperBound float64 + hasBucketValue bool + bucketLabelValue string // original bucket label value for use in warnings +} + +// histogramOperator is a function operator that calculates percentiles. +type histogramOperator struct { + once sync.Once + series []labels.Labels + + funcName string + funcArgs logicalplan.Nodes + stepsBatch int + vectorOp model.VectorOperator + scalar1Op model.VectorOperator + scalar2Op model.VectorOperator + + // scalarPoints is a reusable buffer for points from the first argument of histogram_quantile. + scalar1Points []float64 + scalar2Points []float64 + + // outputIndex is a mapping from input series ID to the output series ID and its upper boundary value + // parsed from the le label. + // If outputIndex[i] is nil then series[i] has no valid `le` label. + outputIndex []*histogramSeries + + // needed to compile warnings on mixed histograms + inputSeriesNames []string + + // seriesBuckets are the buckets for each individual conventional histogram series. + seriesBuckets []promql.Buckets + + // badBucketWarned tracks which series have already emitted bad bucket label warnings. + badBucketWarned map[uint64]bool + + vectorBuf []model.StepVector + scalar1Buf []model.StepVector + scalar2Buf []model.StepVector +} + +func newHistogramOperator( + call *logicalplan.FunctionCall, + nextOps []model.VectorOperator, + stepsBatch int, + opts *query.Options, +) model.VectorOperator { + o := &histogramOperator{ + funcName: call.Func.Name, + funcArgs: call.Args, + stepsBatch: stepsBatch, + } + + switch o.funcName { + case "histogram_quantile": + o.scalar1Op = nextOps[0] + o.vectorOp = nextOps[1] + o.scalar1Points = make([]float64, stepsBatch) + case "histogram_fraction": + o.scalar1Op = nextOps[0] + o.scalar2Op = nextOps[1] + o.vectorOp = nextOps[2] + o.scalar1Points = make([]float64, stepsBatch) + o.scalar2Points = make([]float64, stepsBatch) + default: + panic("unsupported function passed") + } + return telemetry.NewOperator(telemetry.NewTelemetry(o, opts), o) +} + +func (o *histogramOperator) String() string { + return fmt.Sprintf("[%s](%v)", o.funcName, o.funcArgs) +} + +func (o *histogramOperator) Explain() (next []model.VectorOperator) { + switch o.funcName { + case "histogram_quantile": + return []model.VectorOperator{o.scalar1Op, o.vectorOp} + case "histogram_fraction": + return []model.VectorOperator{o.scalar1Op, o.scalar2Op, o.vectorOp} + } + return nil +} + +func (o *histogramOperator) Series(ctx context.Context) ([]labels.Labels, error) { + var err error + o.once.Do(func() { err = o.loadSeries(ctx) }) + if err != nil { + return nil, err + } + return o.series, nil +} + +func (o *histogramOperator) Next(ctx context.Context, buf []model.StepVector) (int, error) { + select { + case <-ctx.Done(): + return 0, ctx.Err() + default: + } + + var err error + o.once.Do(func() { err = o.loadSeries(ctx) }) + if err != nil { + return 0, err + } + + // First get vector data to know how many steps we need + vectorN, err := o.vectorOp.Next(ctx, o.vectorBuf) + if err != nil { + return 0, err + } + if vectorN == 0 { + return 0, nil + } + + // Now get scalar data for the same number of steps + switch o.funcName { + case "histogram_quantile": + scalar1N, err := o.scalar1Op.Next(ctx, o.scalar1Buf[:vectorN]) + if err != nil { + return 0, err + } + + if scalar1N == 0 { + return 0, nil + } + + o.scalar1Points = o.scalar1Points[:0] + for i := range scalar1N { + scalar := o.scalar1Buf[i] + if len(scalar.Samples) > 0 { + sample := scalar.Samples[0] + if math.IsNaN(sample) || sample < 0 || sample > 1 { + warnings.AddToContext(annotations.NewInvalidQuantileWarning(sample, posrange.PositionRange{}), ctx) + } + o.scalar1Points = append(o.scalar1Points, sample) + } + } + case "histogram_fraction": + scalar1N, err := o.scalar1Op.Next(ctx, o.scalar1Buf[:vectorN]) + if err != nil { + return 0, err + } + + if scalar1N == 0 { + return 0, nil + } + + o.scalar1Points = o.scalar1Points[:0] + for i := range scalar1N { + scalar := o.scalar1Buf[i] + if len(scalar.Samples) > 0 { + sample := scalar.Samples[0] + o.scalar1Points = append(o.scalar1Points, sample) + } + } + + scalar2N, err := o.scalar2Op.Next(ctx, o.scalar2Buf[:vectorN]) + if err != nil { + return 0, err + } + + if scalar2N == 0 { + return 0, nil + } + + o.scalar2Points = o.scalar2Points[:0] + for i := range scalar2N { + scalar := o.scalar2Buf[i] + if len(scalar.Samples) > 0 { + sample := scalar.Samples[0] + o.scalar2Points = append(o.scalar2Points, sample) + } + } + } + + // Process the vector data and write to output buffer + vectors := o.vectorBuf[:vectorN] + n, err := o.processInputSeries(ctx, vectors, buf) + if err != nil { + return 0, err + } + + return n, nil +} + +// nolint: unparam +func (o *histogramOperator) processInputSeries(ctx context.Context, vectors []model.StepVector, buf []model.StepVector) (int, error) { + n := 0 + for stepIndex, vector := range vectors { + if n >= len(buf) { + break + } + o.resetBuckets() + for i, seriesID := range vector.SampleIDs { + outputSeries := o.outputIndex[seriesID] + // This means that it has an invalid `le` label. + if outputSeries == nil || !outputSeries.hasBucketValue { + // Emit warning for invalid bucket label (only once per series). + if outputSeries != nil && !o.badBucketWarned[uint64(seriesID)] { + o.badBucketWarned[uint64(seriesID)] = true + metricName := o.inputSeriesNames[seriesID] + warnings.AddToContext(annotations.NewBadBucketLabelWarning(metricName, outputSeries.bucketLabelValue, posrange.PositionRange{}), ctx) + } + continue + } + + outputSeriesID := outputSeries.outputID + bucket := promql.Bucket{ + UpperBound: outputSeries.upperBound, + Count: vector.Samples[i], + } + o.seriesBuckets[outputSeriesID] = append(o.seriesBuckets[outputSeriesID], bucket) + } + + buf[n].Reset(vector.T) + for i, seriesID := range vector.HistogramIDs { + outputSeriesID := o.outputIndex[seriesID].outputID + // We need to check if there is a conventional histogram mapped to this output series ID. + // If that is the case, it means we have mixed data types for a single step and this behavior is undefined. + // In that case, we reset the conventional buckets to avoid emitting a sample. + if len(o.seriesBuckets[outputSeriesID]) == 0 { + var annos annotations.Annotations + var v float64 + switch o.funcName { + case "histogram_quantile": + v, annos = promql.HistogramQuantile(o.scalar1Points[stepIndex], vector.Histograms[i], o.inputSeriesNames[seriesID], posrange.PositionRange{}) + buf[n].AppendSample(uint64(outputSeriesID), v) + case "histogram_fraction": + v, annos = promql.HistogramFraction(o.scalar1Points[stepIndex], o.scalar2Points[stepIndex], vector.Histograms[i], o.inputSeriesNames[seriesID], posrange.PositionRange{}) + buf[n].AppendSample(uint64(outputSeriesID), v) + } + warnings.MergeToContext(annos, ctx) + } else { + warnings.AddToContext(annotations.NewMixedClassicNativeHistogramsWarning(o.inputSeriesNames[seriesID], posrange.PositionRange{}), ctx) + o.seriesBuckets[outputSeriesID] = o.seriesBuckets[outputSeriesID][:0] + } + } + + for i, stepBuckets := range o.seriesBuckets { + // It could be zero if multiple input series map to the same output series ID. + if len(stepBuckets) == 0 { + continue + } + // If we are after how many scalar points we have then it needs to be NaN. + if stepIndex >= len(o.scalar1Points) { + buf[n].AppendSample(uint64(i), math.NaN()) + continue + } + switch o.funcName { + case "histogram_quantile": + // histogram_quantile needs at least 2 buckets. + if len(stepBuckets) == 1 { + buf[n].AppendSample(uint64(i), math.NaN()) + continue + } + v, forcedMonotonicity, _, minBucket, maxBucket, maxDiff := promql.BucketQuantile(o.scalar1Points[stepIndex], stepBuckets) + buf[n].AppendSample(uint64(i), v) + if forcedMonotonicity { + warnings.AddToContext(annotations.NewHistogramQuantileForcedMonotonicityInfo(o.inputSeriesNames[i], posrange.PositionRange{}, vector.T, minBucket, maxBucket, maxDiff), ctx) + } + case "histogram_fraction": + // BucketFraction handles single bucket and other edge cases properly. + v := promql.BucketFraction(o.scalar1Points[stepIndex], o.scalar2Points[stepIndex], stepBuckets) + buf[n].AppendSample(uint64(i), v) + } + } + n++ + } + + return n, nil +} + +func (o *histogramOperator) loadSeries(ctx context.Context) error { + + o.vectorBuf = make([]model.StepVector, o.stepsBatch) + o.scalar1Buf = make([]model.StepVector, o.stepsBatch) + if o.scalar2Op != nil { + o.scalar2Buf = make([]model.StepVector, o.stepsBatch) + } + + series, err := o.vectorOp.Series(ctx) + if err != nil { + return err + } + + var ( + hashBuf = make([]byte, 0, 256) + hasher = xxhash.New() + seriesHashes = make(map[uint64]int, len(series)) + ) + + o.series = make([]labels.Labels, 0) + o.inputSeriesNames = make([]string, len(series)) + o.outputIndex = make([]*histogramSeries, len(series)) + b := labels.ScratchBuilder{} + for i, s := range series { + hasBucketValue := true + lbls, bucketLabel := extlabels.DropBucketLabel(s, b) + value, err := strconv.ParseFloat(bucketLabel.Value, 64) + if err != nil { + hasBucketValue = false + } + + hasher.Reset() + hashBuf = lbls.Bytes(hashBuf) + if _, err := hasher.Write(hashBuf); err != nil { + return err + } + + // We check for duplicate series after dropped labels when + // showing the result of the query. Series that are equal after + // dropping name should not hash to the same bucket here. + lbls = extlabels.DropReserved(lbls, b) + + seriesHash := hasher.Sum64() + seriesID, ok := seriesHashes[seriesHash] + if !ok { + o.series = append(o.series, lbls) + seriesID = len(o.series) - 1 + seriesHashes[seriesHash] = seriesID + } + + o.inputSeriesNames[i] = s.Get(labels.MetricName) + o.outputIndex[i] = &histogramSeries{ + outputID: seriesID, + upperBound: value, + hasBucketValue: hasBucketValue, + bucketLabelValue: bucketLabel.Value, + } + } + o.seriesBuckets = make([]promql.Buckets, len(o.series)) + o.badBucketWarned = make(map[uint64]bool) + return nil +} + +func (o *histogramOperator) resetBuckets() { + for i := range o.seriesBuckets { + o.seriesBuckets[i] = o.seriesBuckets[i][:0] + } +} diff --git a/internal/promql-engine/execution/function/noarg.go b/internal/promql-engine/execution/function/noarg.go new file mode 100644 index 00000000000..3758d90754f --- /dev/null +++ b/internal/promql-engine/execution/function/noarg.go @@ -0,0 +1,61 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package function + +import ( + "context" + + "github.com/thanos-io/promql-engine/execution/model" + "github.com/thanos-io/promql-engine/logicalplan" + + "github.com/prometheus/prometheus/model/labels" +) + +type noArgFunctionOperator struct { + mint int64 + maxt int64 + step int64 + currentStep int64 + stepsBatch int + funcExpr *logicalplan.FunctionCall + call noArgFunctionCall + series []labels.Labels + sampleIDs []uint64 +} + +func (o *noArgFunctionOperator) Explain() (next []model.VectorOperator) { + return nil +} + +func (o *noArgFunctionOperator) String() string { + return "[noArgFunction]" +} + +func (o *noArgFunctionOperator) Series(_ context.Context) ([]labels.Labels, error) { + return o.series, nil +} + +func (o *noArgFunctionOperator) Next(ctx context.Context, buf []model.StepVector) (int, error) { + select { + case <-ctx.Done(): + return 0, ctx.Err() + default: + } + + if o.currentStep > o.maxt { + return 0, nil + } + + n := 0 + maxSteps := min(o.stepsBatch, len(buf)) + + for i := 0; i < maxSteps && o.currentStep <= o.maxt; i++ { + buf[n].Reset(o.currentStep) + buf[n].AppendSample(o.sampleIDs[0], o.call(o.currentStep)) + n++ + o.currentStep += o.step + } + + return n, nil +} diff --git a/internal/promql-engine/execution/function/operator.go b/internal/promql-engine/execution/function/operator.go new file mode 100644 index 00000000000..d5d0641b9c9 --- /dev/null +++ b/internal/promql-engine/execution/function/operator.go @@ -0,0 +1,245 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package function + +import ( + "context" + "fmt" + "math" + "sync" + + "github.com/thanos-io/promql-engine/execution/model" + "github.com/thanos-io/promql-engine/execution/parse" + "github.com/thanos-io/promql-engine/execution/telemetry" + "github.com/thanos-io/promql-engine/extlabels" + "github.com/thanos-io/promql-engine/logicalplan" + "github.com/thanos-io/promql-engine/query" + + "github.com/efficientgo/core/errors" + "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/promql/parser" +) + +func NewFunctionOperator(funcExpr *logicalplan.FunctionCall, nextOps []model.VectorOperator, stepsBatch int, opts *query.Options) (model.VectorOperator, error) { + // Some functions need to be handled in special operators + switch funcExpr.Func.Name { + case "scalar": + return newScalarOperator(nextOps[0], opts), nil + case "timestamp": + return newTimestampOperator(nextOps[0], opts), nil + case "label_join", "label_replace": + return newRelabelOperator(nextOps[0], funcExpr, opts), nil + case "absent": + return newAbsentOperator(funcExpr, nextOps[0], opts), nil + case "histogram_quantile", "histogram_fraction": + return newHistogramOperator(funcExpr, nextOps, stepsBatch, opts), nil + } + + // Short-circuit functions that take no args. Their only input is the step's timestamp. + if len(nextOps) == 0 { + return newNoArgsFunctionOperator(funcExpr, stepsBatch, opts) + } + // All remaining functions + return newInstantVectorFunctionOperator(funcExpr, nextOps, stepsBatch, opts) +} + +func newNoArgsFunctionOperator(funcExpr *logicalplan.FunctionCall, stepsBatch int, opts *query.Options) (model.VectorOperator, error) { + call, ok := noArgFuncs[funcExpr.Func.Name] + if !ok { + return nil, parse.UnknownFunctionError(funcExpr.Func.Name) + } + + interval := opts.Step.Milliseconds() + // We set interval to be at least 1. + if interval == 0 { + interval = 1 + } + + op := &noArgFunctionOperator{ + currentStep: opts.Start.UnixMilli(), + mint: opts.Start.UnixMilli(), + maxt: opts.End.UnixMilli(), + step: interval, + stepsBatch: stepsBatch, + funcExpr: funcExpr, + call: call, + } + + switch funcExpr.Func.Name { + case "pi", "time": + op.sampleIDs = []uint64{0} + default: + // Other functions require non-nil labels. + op.series = []labels.Labels{{}} + op.sampleIDs = []uint64{0} + } + + return telemetry.NewOperator(telemetry.NewTelemetry(op, opts), op), nil +} + +// functionOperator returns []model.StepVector after processing input with desired function. +type functionOperator struct { + funcExpr *logicalplan.FunctionCall + series []labels.Labels + once sync.Once + + vectorIndex int + nextOps []model.VectorOperator + stepsBatch int + + call functionCall + scalarPoints [][]float64 + scalarBuf []model.StepVector +} + +func newInstantVectorFunctionOperator(funcExpr *logicalplan.FunctionCall, nextOps []model.VectorOperator, stepsBatch int, opts *query.Options) (model.VectorOperator, error) { + call, ok := instantVectorFuncs[funcExpr.Func.Name] + if !ok { + return nil, parse.UnknownFunctionError(funcExpr.Func.Name) + } + + scalarPoints := make([][]float64, stepsBatch) + for i := range stepsBatch { + scalarPoints[i] = make([]float64, len(nextOps)-1) + } + f := &functionOperator{ + nextOps: nextOps, + call: call, + funcExpr: funcExpr, + vectorIndex: 0, + stepsBatch: stepsBatch, + scalarPoints: scalarPoints, + } + + for i := range funcExpr.Args { + if funcExpr.Args[i].ReturnType() == parser.ValueTypeVector { + f.vectorIndex = i + break + } + } + + // Check selector type. + switch funcExpr.Args[f.vectorIndex].ReturnType() { + case parser.ValueTypeVector, parser.ValueTypeScalar: + return telemetry.NewOperator(telemetry.NewTelemetry(f, opts), f), nil + default: + return nil, errors.Wrapf(parse.ErrNotImplemented, "got %s:", funcExpr.String()) + } +} + +func (o *functionOperator) Explain() (next []model.VectorOperator) { + return o.nextOps +} + +func (o *functionOperator) String() string { + return fmt.Sprintf("[function] %v(%v)", o.funcExpr.Func.Name, o.funcExpr.Args) +} + +func (o *functionOperator) Series(ctx context.Context) ([]labels.Labels, error) { + if err := o.loadSeries(ctx); err != nil { + return nil, err + } + return o.series, nil +} + +func (o *functionOperator) Next(ctx context.Context, buf []model.StepVector) (int, error) { + select { + case <-ctx.Done(): + return 0, ctx.Err() + default: + } + if err := o.loadSeries(ctx); err != nil { + return 0, err + } + + // Process non-variadic single/multi-arg instant vector and scalar input functions. + // Call next on vector input. + n, err := o.nextOps[o.vectorIndex].Next(ctx, buf) + if err != nil { + return 0, err + } + + if n == 0 { + return 0, nil + } + + scalarIndex := 0 + for i := range o.nextOps { + if i == o.vectorIndex { + continue + } + + scalarN, err := o.nextOps[i].Next(ctx, o.scalarBuf) + if err != nil { + return 0, err + } + + for batchIndex := range n { + val := math.NaN() + if batchIndex < scalarN && len(o.scalarBuf[batchIndex].Samples) > 0 { + val = o.scalarBuf[batchIndex].Samples[0] + } + o.scalarPoints[batchIndex][scalarIndex] = val + } + scalarIndex++ + } + + for batchIndex := range n { + vector := &buf[batchIndex] + i := 0 + for i < len(vector.Samples) { + if v, ok := o.call(vector.Samples[i], nil, o.scalarPoints[batchIndex]...); ok { + vector.Samples[i] = v + i++ + } else { + // This operator modifies samples directly in the input vector to avoid allocations. + // In case of an invalid output sample, we need to do an in-place removal of the input sample. + vector.RemoveSample(i) + } + } + + i = 0 + for i < len(vector.Histograms) { + v, ok := o.call(0., vector.Histograms[i], o.scalarPoints[batchIndex]...) + // This operator modifies samples directly in the input vector to avoid allocations. + // All current functions for histograms produce a float64 sample. It's therefore safe to + // always remove the input histogram so that it does not propagate to the output. + sampleID := vector.HistogramIDs[i] + vector.RemoveHistogram(i) + if ok { + vector.AppendSample(sampleID, v) + } + } + } + + return n, nil +} + +func (o *functionOperator) loadSeries(ctx context.Context) error { + var err error + o.once.Do(func() { + + o.scalarBuf = make([]model.StepVector, o.stepsBatch) + + if o.funcExpr.Func.Name == "vector" { + o.series = []labels.Labels{labels.New()} + return + } + + series, loadErr := o.nextOps[o.vectorIndex].Series(ctx) + if loadErr != nil { + err = loadErr + return + } + o.series = make([]labels.Labels, len(series)) + + var b labels.ScratchBuilder + for i, s := range series { + lbls := extlabels.DropReserved(s, b) + o.series[i] = lbls + } + }) + + return err +} diff --git a/internal/promql-engine/execution/function/relabel.go b/internal/promql-engine/execution/function/relabel.go new file mode 100644 index 00000000000..e32d4d9e929 --- /dev/null +++ b/internal/promql-engine/execution/function/relabel.go @@ -0,0 +1,158 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package function + +import ( + "context" + "regexp" + "strings" + "sync" + + "github.com/thanos-io/promql-engine/execution/model" + "github.com/thanos-io/promql-engine/execution/telemetry" + "github.com/thanos-io/promql-engine/logicalplan" + "github.com/thanos-io/promql-engine/query" + + "github.com/efficientgo/core/errors" + prommodel "github.com/prometheus/common/model" + "github.com/prometheus/prometheus/model/labels" +) + +type relabelOperator struct { + next model.VectorOperator + funcExpr *logicalplan.FunctionCall + once sync.Once + series []labels.Labels +} + +func newRelabelOperator( + next model.VectorOperator, + funcExpr *logicalplan.FunctionCall, + opts *query.Options, +) model.VectorOperator { + oper := &relabelOperator{ + next: next, + funcExpr: funcExpr, + } + return telemetry.NewOperator(telemetry.NewTelemetry(oper, opts), oper) +} + +func (o *relabelOperator) String() string { + return "[relabel]" +} + +func (o *relabelOperator) Explain() (next []model.VectorOperator) { + return []model.VectorOperator{o.next} +} + +func (o *relabelOperator) Series(ctx context.Context) ([]labels.Labels, error) { + var err error + o.once.Do(func() { err = o.loadSeries(ctx) }) + return o.series, err +} + +func (o *relabelOperator) Next(ctx context.Context, buf []model.StepVector) (int, error) { + return o.next.Next(ctx, buf) +} + +func (o *relabelOperator) loadSeries(ctx context.Context) (err error) { + series, err := o.next.Series(ctx) + if err != nil { + return err + } + o.series = make([]labels.Labels, len(series)) + + switch o.funcExpr.Func.Name { + case "label_join": + err = o.loadSeriesForLabelJoin(series) + case "label_replace": + err = o.loadSeriesForLabelReplace(series) + default: + err = errors.Newf("invalid function name for relabel operator: %s", o.funcExpr.Func.Name) + } + return err +} + +func (o *relabelOperator) loadSeriesForLabelJoin(series []labels.Labels) error { + labelJoinDst, err := logicalplan.UnwrapString(o.funcExpr.Args[1]) + if err != nil { + return errors.Wrap(err, "unable to unwrap string argument") + } + if !prommodel.UTF8Validation.IsValidLabelName(labelJoinDst) { + return errors.Newf("invalid destination label name in label_join: %s", labelJoinDst) + } + + var labelJoinSrcLabels []string + labelJoinSep, err := logicalplan.UnwrapString(o.funcExpr.Args[2]) + if err != nil { + return errors.Wrap(err, "unable to unwrap string argument") + } + for j := 3; j < len(o.funcExpr.Args); j++ { + srcLabel, err := logicalplan.UnwrapString(o.funcExpr.Args[j]) + if err != nil { + return errors.Wrap(err, "unable to unwrap string argument") + } + labelJoinSrcLabels = append(labelJoinSrcLabels, srcLabel) + } + for i, s := range series { + lbls := s + srcVals := make([]string, len(labelJoinSrcLabels)) + + for j, src := range labelJoinSrcLabels { + srcVals[j] = lbls.Get(src) + } + lb := labels.NewBuilder(lbls) + if strval := strings.Join(srcVals, labelJoinSep); strval == "" { + lb.Del(labelJoinDst) + } else { + lb.Set(labelJoinDst, strval) + } + o.series[i] = lb.Labels() + } + return nil +} +func (o *relabelOperator) loadSeriesForLabelReplace(series []labels.Labels) error { + labelReplaceDst, err := logicalplan.UnwrapString(o.funcExpr.Args[1]) + if err != nil { + return errors.Wrap(err, "unable to unwrap string argument") + } + if !prommodel.UTF8Validation.IsValidLabelName(labelReplaceDst) { + return errors.Newf("invalid destination label name in label_replace: %s", labelReplaceDst) + } + labelReplaceRepl, err := logicalplan.UnwrapString(o.funcExpr.Args[2]) + if err != nil { + return errors.Wrap(err, "unable to unwrap string argument") + } + labelReplaceSrc, err := logicalplan.UnwrapString(o.funcExpr.Args[3]) + if err != nil { + return errors.Wrap(err, "unable to unwrap string argument") + } + labelReplaceRegexVal, err := logicalplan.UnwrapString(o.funcExpr.Args[4]) + if err != nil { + return errors.Wrap(err, "unable to unwrap string argument") + } + labelReplaceRegex, err := regexp.Compile("^(?:" + labelReplaceRegexVal + ")$") + if err != nil { + return errors.Newf("invalid regular expression in label_replace(): %s", labelReplaceRegexVal) + } + + for i, s := range series { + lbls := s + + srcVal := lbls.Get(labelReplaceSrc) + matches := labelReplaceRegex.FindStringSubmatchIndex(srcVal) + if len(matches) == 0 { + o.series[i] = lbls + continue + } + res := labelReplaceRegex.ExpandString([]byte{}, labelReplaceRepl, srcVal, matches) + lb := labels.NewBuilder(lbls).Del(labelReplaceDst) + if len(res) > 0 { + lb.Set(labelReplaceDst, string(res)) + } + o.series[i] = lb.Labels() + } + + return nil +} diff --git a/internal/promql-engine/execution/function/scalar.go b/internal/promql-engine/execution/function/scalar.go new file mode 100644 index 00000000000..16d25d3774b --- /dev/null +++ b/internal/promql-engine/execution/function/scalar.go @@ -0,0 +1,66 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package function + +import ( + "context" + "math" + + "github.com/thanos-io/promql-engine/execution/model" + "github.com/thanos-io/promql-engine/execution/telemetry" + "github.com/thanos-io/promql-engine/query" + + "github.com/prometheus/prometheus/model/labels" +) + +type scalarOperator struct { + next model.VectorOperator +} + +func newScalarOperator(next model.VectorOperator, opts *query.Options) model.VectorOperator { + oper := &scalarOperator{ + next: next, + } + + return telemetry.NewOperator(telemetry.NewTelemetry(oper, opts), oper) +} + +func (o *scalarOperator) String() string { + return "[scalar]" +} + +func (o *scalarOperator) Explain() (next []model.VectorOperator) { + return []model.VectorOperator{o.next} +} + +func (o *scalarOperator) Series(ctx context.Context) ([]labels.Labels, error) { + return nil, nil +} + +func (o *scalarOperator) Next(ctx context.Context, buf []model.StepVector) (int, error) { + select { + case <-ctx.Done(): + return 0, ctx.Err() + default: + } + + n, err := o.next.Next(ctx, buf) + if err != nil { + return 0, err + } + + for i := range n { + vector := &buf[i] + var val float64 + if len(vector.Samples) == 1 { + val = vector.Samples[0] + } else { + val = math.NaN() + } + vector.Reset(vector.T) + vector.AppendSample(0, val) + } + + return n, nil +} diff --git a/internal/promql-engine/execution/function/timestamp.go b/internal/promql-engine/execution/function/timestamp.go new file mode 100644 index 00000000000..514cfadb49c --- /dev/null +++ b/internal/promql-engine/execution/function/timestamp.go @@ -0,0 +1,85 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package function + +import ( + "context" + "sync" + + "github.com/thanos-io/promql-engine/execution/model" + "github.com/thanos-io/promql-engine/execution/telemetry" + "github.com/thanos-io/promql-engine/extlabels" + "github.com/thanos-io/promql-engine/query" + + "github.com/prometheus/prometheus/model/labels" +) + +type timestampOperator struct { + next model.VectorOperator + + series []labels.Labels + once sync.Once +} + +func newTimestampOperator(next model.VectorOperator, opts *query.Options) model.VectorOperator { + oper := ×tampOperator{ + next: next, + } + return telemetry.NewOperator(telemetry.NewTelemetry(oper, opts), oper) +} + +func (o *timestampOperator) Explain() (next []model.VectorOperator) { + return []model.VectorOperator{o.next} +} + +func (o *timestampOperator) Series(ctx context.Context) ([]labels.Labels, error) { + if err := o.loadSeries(ctx); err != nil { + return nil, err + } + return o.series, nil +} + +func (o *timestampOperator) String() string { + return "[timestamp]" +} + +func (o *timestampOperator) loadSeries(ctx context.Context) error { + var err error + o.once.Do(func() { + series, loadErr := o.next.Series(ctx) + if loadErr != nil { + err = loadErr + return + } + o.series = make([]labels.Labels, len(series)) + + var b labels.ScratchBuilder + for i, s := range series { + lbls := extlabels.DropReserved(s, b) + o.series[i] = lbls + } + }) + + return err +} + +func (o *timestampOperator) Next(ctx context.Context, buf []model.StepVector) (int, error) { + select { + case <-ctx.Done(): + return 0, ctx.Err() + default: + } + + n, err := o.next.Next(ctx, buf) + if err != nil { + return 0, err + } + for i := range n { + vector := &buf[i] + for j := range vector.Samples { + vector.Samples[j] = float64(vector.T / 1000) + } + } + return n, nil +} diff --git a/internal/promql-engine/execution/model/identified.go b/internal/promql-engine/execution/model/identified.go new file mode 100644 index 00000000000..ad968a61bc8 --- /dev/null +++ b/internal/promql-engine/execution/model/identified.go @@ -0,0 +1,55 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package model + +import ( + "context" + "sync" + + "github.com/prometheus/prometheus/model/labels" +) + +// identifiedOperator wraps a VectorOperator with a deterministic fingerprint. +type identifiedOperator struct { + VectorOperator + id uint64 + enrichedCtx context.Context + once sync.Once +} + +// WithID wraps op so that it implements OperatorIDer. +func WithID(op VectorOperator, id uint64) VectorOperator { + return &identifiedOperator{VectorOperator: op, id: id} +} + +func (o *identifiedOperator) OperatorID() uint64 { return o.id } + +func (o *identifiedOperator) enriched(ctx context.Context) context.Context { + o.once.Do(func() { o.enrichedCtx = ContextWithOperatorID(ctx, o.id) }) + return o.enrichedCtx +} + +func (o *identifiedOperator) Series(ctx context.Context) ([]labels.Labels, error) { + return o.VectorOperator.Series(o.enriched(ctx)) +} + +func (o *identifiedOperator) Next(ctx context.Context, buf []StepVector) (int, error) { + return o.VectorOperator.Next(o.enriched(ctx), buf) +} + +func (o *identifiedOperator) Unwrap() VectorOperator { return o.VectorOperator } + +type Unwrapper interface { + Unwrap() VectorOperator +} + +func Unwrap(op VectorOperator) VectorOperator { + for { + u, ok := op.(Unwrapper) + if !ok { + return op + } + op = u.Unwrap() + } +} diff --git a/internal/promql-engine/execution/model/operator.go b/internal/promql-engine/execution/model/operator.go new file mode 100644 index 00000000000..85c19c97229 --- /dev/null +++ b/internal/promql-engine/execution/model/operator.go @@ -0,0 +1,51 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package model + +import ( + "context" + "fmt" + + "github.com/prometheus/prometheus/model/labels" +) + +// OperatorIDer is an optional interface for operators that carry a +// deterministic fingerprint derived from their logical plan subtree. +// Only data-fetching operators implement this; pure-computation operators +// do not. +type OperatorIDer interface { + OperatorID() uint64 +} + +type operatorIDKey struct{} + +// ContextWithOperatorID returns a copy of ctx carrying the given operator ID. +func ContextWithOperatorID(ctx context.Context, id uint64) context.Context { + return context.WithValue(ctx, operatorIDKey{}, id) +} + +// OperatorIDFromContext returns the operator ID stored in ctx, if any. +func OperatorIDFromContext(ctx context.Context) (uint64, bool) { + id, ok := ctx.Value(operatorIDKey{}).(uint64) + return id, ok +} + +// VectorOperator performs operations on series in step by step fashion. +type VectorOperator interface { + // Next yields vectors of samples from all series for one or more execution steps. + // The caller provides a buffer (buf) to be filled with StepVectors. + // Returns the number of StepVectors written to buf and any error encountered. + // A return value of 0 indicates no more data is available. + Next(ctx context.Context, buf []StepVector) (int, error) + + // Series returns all series that the operator will process during Next results. + // The result can be used by upstream operators to allocate output tables and buffers + // before starting to process samples. + Series(ctx context.Context) ([]labels.Labels, error) + + // Explain returns human-readable explanation of the current operator and optional nested operators. + Explain() (next []VectorOperator) + + fmt.Stringer +} diff --git a/internal/promql-engine/execution/model/vector.go b/internal/promql-engine/execution/model/vector.go new file mode 100644 index 00000000000..185717e732b --- /dev/null +++ b/internal/promql-engine/execution/model/vector.go @@ -0,0 +1,112 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package model + +import ( + "slices" + + "github.com/prometheus/prometheus/model/histogram" + "github.com/prometheus/prometheus/model/labels" +) + +type Series struct { + // ID is a numerical, zero-based identifier for a series. + // It allows using slices instead of maps for fast lookups. + ID uint64 + Metric labels.Labels +} + +type StepVector struct { + T int64 + SampleIDs []uint64 + Samples []float64 + + HistogramIDs []uint64 + Histograms []*histogram.FloatHistogram +} + +// Reset resets the StepVector to the given timestamp while preserving slice capacity. +func (s *StepVector) Reset(t int64) { + s.T = t + if s.SampleIDs != nil { + s.SampleIDs = s.SampleIDs[:0] + } + if s.Samples != nil { + s.Samples = s.Samples[:0] + } + if s.HistogramIDs != nil { + s.HistogramIDs = s.HistogramIDs[:0] + } + if s.Histograms != nil { + s.Histograms = s.Histograms[:0] + } +} + +func (s *StepVector) AppendSample(id uint64, val float64) { + s.SampleIDs = append(s.SampleIDs, id) + s.Samples = append(s.Samples, val) +} + +// AppendSampleWithSizeHint appends a sample and lazily pre-allocates capacity if needed. +// Use this when you know the expected number of samples to avoid repeated slice growth. +func (s *StepVector) AppendSampleWithSizeHint(id uint64, val float64, hint int) { + if s.SampleIDs == nil || cap(s.SampleIDs) < hint { + newSampleIDs := make([]uint64, len(s.SampleIDs), hint) + copy(newSampleIDs, s.SampleIDs) + s.SampleIDs = newSampleIDs + + newSamples := make([]float64, len(s.Samples), hint) + copy(newSamples, s.Samples) + s.Samples = newSamples + } + s.SampleIDs = append(s.SampleIDs, id) + s.Samples = append(s.Samples, val) +} + +func (s *StepVector) AppendSamples(ids []uint64, vals []float64) { + if len(ids) == 0 && len(vals) == 0 { + return + } + s.SampleIDs = append(s.SampleIDs, ids...) + s.Samples = append(s.Samples, vals...) +} + +func (s *StepVector) RemoveSample(index int) { + s.Samples = slices.Delete(s.Samples, index, index+1) + s.SampleIDs = slices.Delete(s.SampleIDs, index, index+1) +} + +func (s *StepVector) AppendHistogram(histogramID uint64, h *histogram.FloatHistogram) { + s.HistogramIDs = append(s.HistogramIDs, histogramID) + s.Histograms = append(s.Histograms, h) +} + +// AppendHistogramWithSizeHint appends a histogram and lazily pre-allocates capacity if needed. +// Use this when you know the expected number of histograms to avoid repeated slice growth. +func (s *StepVector) AppendHistogramWithSizeHint(histogramID uint64, h *histogram.FloatHistogram, hint int) { + if s.HistogramIDs == nil || cap(s.HistogramIDs) < hint { + newHistogramIDs := make([]uint64, len(s.HistogramIDs), hint) + copy(newHistogramIDs, s.HistogramIDs) + s.HistogramIDs = newHistogramIDs + + newHistograms := make([]*histogram.FloatHistogram, len(s.Histograms), hint) + copy(newHistograms, s.Histograms) + s.Histograms = newHistograms + } + s.HistogramIDs = append(s.HistogramIDs, histogramID) + s.Histograms = append(s.Histograms, h) +} + +func (s *StepVector) AppendHistograms(histogramIDs []uint64, hs []*histogram.FloatHistogram) { + if len(histogramIDs) == 0 && len(hs) == 0 { + return + } + s.HistogramIDs = append(s.HistogramIDs, histogramIDs...) + s.Histograms = append(s.Histograms, hs...) +} + +func (s *StepVector) RemoveHistogram(index int) { + s.Histograms = slices.Delete(s.Histograms, index, index+1) + s.HistogramIDs = slices.Delete(s.HistogramIDs, index, index+1) +} diff --git a/internal/promql-engine/execution/noop/operator.go b/internal/promql-engine/execution/noop/operator.go new file mode 100644 index 00000000000..5b4f03dc352 --- /dev/null +++ b/internal/promql-engine/execution/noop/operator.go @@ -0,0 +1,41 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package noop + +import ( + "context" + + "github.com/thanos-io/promql-engine/execution/model" + "github.com/thanos-io/promql-engine/query" + "github.com/thanos-io/promql-engine/storage/prometheus" + + "github.com/prometheus/prometheus/model/labels" +) + +type operator struct { + model.VectorOperator +} + +func NewOperator(opts *query.Options) model.VectorOperator { + scanner := prometheus.NewVectorSelector( + noopSelector{}, + opts, + 0, // offset + 0, // batchSize + false, // selectTimestamp + 0, // shard + 1, // numShards + ) + return &operator{VectorOperator: scanner} +} + +func (o operator) String() string { return "[noop]" } +func (o operator) Explain() (next []model.VectorOperator) { return nil } + +type noopSelector struct{} + +func (n noopSelector) Matchers() []*labels.Matcher { return nil } +func (n noopSelector) GetSeries(ctx context.Context, shard, numShards int) ([]prometheus.SignedSeries, error) { + return nil, nil +} diff --git a/internal/promql-engine/execution/parse/errors.go b/internal/promql-engine/execution/parse/errors.go new file mode 100644 index 00000000000..c5e29e1ca14 --- /dev/null +++ b/internal/promql-engine/execution/parse/errors.go @@ -0,0 +1,21 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package parse + +import ( + "fmt" + + "github.com/efficientgo/core/errors" + "github.com/prometheus/prometheus/promql/parser" +) + +var ErrNotSupportedExpr = errors.New("unsupported expression") + +func UnsupportedOperationErr(op parser.ItemType) error { + t := parser.ItemTypeStr[op] + msg := fmt.Sprintf("operation not supported: %s", t) + return errors.Wrap(ErrNotSupportedExpr, msg) +} + +var ErrNotImplemented = errors.New("expression not implemented") diff --git a/internal/promql-engine/execution/parse/functions.go b/internal/promql-engine/execution/parse/functions.go new file mode 100644 index 00000000000..0ba8173df69 --- /dev/null +++ b/internal/promql-engine/execution/parse/functions.go @@ -0,0 +1,44 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package parse + +import ( + "fmt" + + "github.com/efficientgo/core/errors" + "github.com/prometheus/prometheus/promql/parser" +) + +var XFunctions = map[string]*parser.Function{ + "xdelta": { + Name: "xdelta", + ArgTypes: []parser.ValueType{parser.ValueTypeMatrix}, + ReturnType: parser.ValueTypeVector, + }, + "xincrease": { + Name: "xincrease", + ArgTypes: []parser.ValueType{parser.ValueTypeMatrix}, + ReturnType: parser.ValueTypeVector, + }, + "xrate": { + Name: "xrate", + ArgTypes: []parser.ValueType{parser.ValueTypeMatrix}, + ReturnType: parser.ValueTypeVector, + }, +} + +// IsExtFunction is a convenience function to determine whether extended range calculations are required. +func IsExtFunction(functionName string) bool { + _, ok := XFunctions[functionName] + return ok +} + +func UnknownFunctionError(name string) error { + msg := fmt.Sprintf("unknown function: %s", name) + if _, ok := parser.Functions[name]; ok { + return errors.Wrap(ErrNotImplemented, msg) + } + + return errors.Wrap(ErrNotSupportedExpr, msg) +} diff --git a/internal/promql-engine/execution/remote/operator.go b/internal/promql-engine/execution/remote/operator.go new file mode 100644 index 00000000000..fa71bef077c --- /dev/null +++ b/internal/promql-engine/execution/remote/operator.go @@ -0,0 +1,150 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package remote + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/thanos-io/promql-engine/execution/model" + "github.com/thanos-io/promql-engine/execution/telemetry" + "github.com/thanos-io/promql-engine/query" + promstorage "github.com/thanos-io/promql-engine/storage/prometheus" + "github.com/thanos-io/promql-engine/warnings" + + "github.com/efficientgo/core/errors" + "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/promql" + "github.com/prometheus/prometheus/storage" + "github.com/prometheus/prometheus/util/stats" +) + +type Execution struct { + storage *storageAdapter + query promql.Query + opts *query.Options + queryRangeStart time.Time + queryRangeEnd time.Time + + vectorSelector model.VectorOperator +} + +func NewExecution(query promql.Query, queryRangeStart, queryRangeEnd time.Time, engineLabels []labels.Labels, opts *query.Options, _ storage.SelectHints) model.VectorOperator { + storage := newStorageFromQuery(query, opts, engineLabels) + oper := &Execution{ + storage: storage, + query: query, + opts: opts, + queryRangeStart: queryRangeStart, + queryRangeEnd: queryRangeEnd, + vectorSelector: promstorage.NewVectorSelector(storage, opts, 0, 0, false, 0, 1), + } + + return telemetry.NewOperator(telemetry.NewTelemetry(oper, opts), oper) +} + +func (e *Execution) Series(ctx context.Context) ([]labels.Labels, error) { + series, err := e.vectorSelector.Series(ctx) + if err != nil { + return nil, err + } + return series, nil +} + +func (e *Execution) String() string { + return fmt.Sprintf("[remoteExec] %s (%d, %d)", e.query, e.queryRangeStart.Unix(), e.queryRangeEnd.Unix()) +} + +func (e *Execution) Next(ctx context.Context, buf []model.StepVector) (int, error) { + n, err := e.vectorSelector.Next(ctx, buf) + if n == 0 { + // Closing the storage prematurely can lead to results from the query + // engine to be recycled. Because of this, we close the storage only + // when we are done with processing all samples returned by the query. + e.storage.Close() + } + return n, err +} + +func (e *Execution) Explain() (next []model.VectorOperator) { + return nil +} + +func (e *Execution) Samples() *stats.QuerySamples { + if s := e.storage.query.Stats(); s != nil { + return s.Samples + } + + return nil +} + +type storageAdapter struct { + query promql.Query + opts *query.Options + lbls []labels.Labels + + once sync.Once + err error + series []promstorage.SignedSeries +} + +func newStorageFromQuery(query promql.Query, opts *query.Options, lbls []labels.Labels) *storageAdapter { + return &storageAdapter{ + query: query, + opts: opts, + lbls: lbls, + } +} + +func (s *storageAdapter) Matchers() []*labels.Matcher { return nil } + +func (s *storageAdapter) GetSeries(ctx context.Context, _, _ int) ([]promstorage.SignedSeries, error) { + s.once.Do(func() { s.executeQuery(ctx) }) + if s.err != nil { + return nil, s.err + } + + return s.series, nil +} + +func (s *storageAdapter) executeQuery(ctx context.Context) { + result := s.query.Exec(ctx) + for _, w := range result.Warnings { + warnings.AddToContext(w, ctx) + } + if result.Err != nil { + s.err = errors.Wrapf(result.Err, "remote exec error [%s]", s.lbls) + return + } + switch val := result.Value.(type) { + case promql.Matrix: + s.series = make([]promstorage.SignedSeries, len(val)) + for i, series := range val { + s.series[i] = promstorage.SignedSeries{ + Signature: uint64(i), + Series: promql.NewStorageSeries(series), + } + } + case promql.Vector: + s.series = make([]promstorage.SignedSeries, len(val)) + for i, sample := range val { + series := promql.Series{Metric: sample.Metric} + if sample.H == nil { + series.Floats = []promql.FPoint{{T: sample.T, F: sample.F}} + } else { + series.Histograms = []promql.HPoint{{T: sample.T, H: sample.H}} + } + s.series[i] = promstorage.SignedSeries{ + Signature: uint64(i), + Series: promql.NewStorageSeries(series), + } + } + } +} + +func (s *storageAdapter) Close() { + s.query.Close() +} diff --git a/internal/promql-engine/execution/scan/literal_selector.go b/internal/promql-engine/execution/scan/literal_selector.go new file mode 100644 index 00000000000..7b8aba70b9f --- /dev/null +++ b/internal/promql-engine/execution/scan/literal_selector.go @@ -0,0 +1,94 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package scan + +import ( + "context" + "fmt" + "sync" + + "github.com/thanos-io/promql-engine/execution/model" + "github.com/thanos-io/promql-engine/execution/telemetry" + "github.com/thanos-io/promql-engine/query" + + "github.com/prometheus/prometheus/model/labels" +) + +// numberLiteralSelector returns []model.StepVector with same sample value across time range. +type numberLiteralSelector struct { + numSteps int + mint int64 + maxt int64 + step int64 + currentStep int64 + series []labels.Labels + once sync.Once + + val float64 +} + +func NewNumberLiteralSelector(opts *query.Options, val float64) model.VectorOperator { + oper := &numberLiteralSelector{ + numSteps: opts.NumStepsPerBatch(), + mint: opts.Start.UnixMilli(), + maxt: opts.End.UnixMilli(), + step: opts.Step.Milliseconds(), + currentStep: opts.Start.UnixMilli(), + val: val, + } + + return telemetry.NewOperator(telemetry.NewTelemetry(oper, opts), oper) +} + +func (o *numberLiteralSelector) Explain() (next []model.VectorOperator) { + return nil +} + +func (o *numberLiteralSelector) String() string { + return fmt.Sprintf("[numberLiteral] %v", o.val) +} + +func (o *numberLiteralSelector) Series(context.Context) ([]labels.Labels, error) { + o.loadSeries() + return o.series, nil +} + +func (o *numberLiteralSelector) Next(ctx context.Context, buf []model.StepVector) (int, error) { + select { + case <-ctx.Done(): + return 0, ctx.Err() + default: + } + + if o.currentStep > o.maxt { + return 0, nil + } + + o.loadSeries() + + ts := o.currentStep + n := 0 + for n < len(buf) && n < o.numSteps && ts <= o.maxt { + buf[n].Reset(ts) + buf[n].AppendSample(0, o.val) + + ts += o.step + n++ + } + + // For instant queries, set the step to a positive value + // so that the operator can terminate. + if o.step == 0 { + o.step = 1 + } + o.currentStep += o.step * int64(n) + + return n, nil +} + +func (o *numberLiteralSelector) loadSeries() { + o.once.Do(func() { + o.series = []labels.Labels{{}} + }) +} diff --git a/internal/promql-engine/execution/scan/subquery.go b/internal/promql-engine/execution/scan/subquery.go new file mode 100644 index 00000000000..8ebe4bbae84 --- /dev/null +++ b/internal/promql-engine/execution/scan/subquery.go @@ -0,0 +1,342 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package scan + +import ( + "context" + "fmt" + "math" + "sync" + + "github.com/thanos-io/promql-engine/execution/model" + "github.com/thanos-io/promql-engine/execution/telemetry" + "github.com/thanos-io/promql-engine/extlabels" + "github.com/thanos-io/promql-engine/logicalplan" + "github.com/thanos-io/promql-engine/query" + "github.com/thanos-io/promql-engine/ringbuffer" + + "github.com/prometheus/prometheus/model/histogram" + "github.com/prometheus/prometheus/model/labels" +) + +const sampleLimitCheckPercentage = 0.05 + +type subqueryOperator struct { + next model.VectorOperator + paramOp model.VectorOperator + paramOp2 model.VectorOperator + call ringbuffer.FunctionCall + telemetry telemetry.OperatorTelemetry + funcExpr *logicalplan.FunctionCall + subQuery *logicalplan.Subquery + opts *query.Options + + mint int64 + maxt int64 + currentStep int64 + step int64 + stepsBatch int + + onceSeries sync.Once + series []labels.Labels + + lastVectors []model.StepVector + lastCollected int + buffers []*ringbuffer.GenericRingBuffer + + // params holds the function parameter for each step. + // quantile_over time and predict_linear use one parameter (params) + // double_exponential_smoothing uses two (params, params2) for (sf, tf) + params []float64 + params2 []float64 + + paramBuf []model.StepVector + param2Buf []model.StepVector + tempBuf []model.StepVector + + currentTrackedSamples int + lastTrackedSamples int +} + +func NewSubqueryOperator(next, paramOp, paramOp2 model.VectorOperator, opts *query.Options, funcExpr *logicalplan.FunctionCall, subQuery *logicalplan.Subquery) (model.VectorOperator, error) { + call, err := ringbuffer.NewRangeVectorFunc(funcExpr.Func.Name) + if err != nil { + return nil, err + } + step := opts.Step.Milliseconds() + if step == 0 { + step = 1 + } + + o := &subqueryOperator{ + next: next, + paramOp: paramOp, + paramOp2: paramOp2, + call: call, + funcExpr: funcExpr, + subQuery: subQuery, + opts: opts, + mint: opts.Start.UnixMilli(), + maxt: opts.End.UnixMilli(), + currentStep: opts.Start.UnixMilli(), + step: step, + stepsBatch: opts.StepsBatch, + lastCollected: -1, + params: make([]float64, opts.StepsBatch), + params2: make([]float64, opts.StepsBatch), + } + o.telemetry = telemetry.NewSubqueryTelemetry(o, opts) + return telemetry.NewOperator(o.telemetry, o), nil +} + +func (o *subqueryOperator) String() string { + return fmt.Sprintf("[subquery] %v()", o.funcExpr.Func.Name) +} + +func (o *subqueryOperator) Explain() (next []model.VectorOperator) { + switch o.funcExpr.Func.Name { + case "quantile_over_time", "predict_linear": + return []model.VectorOperator{o.paramOp, o.next} + case "double_exponential_smoothing": + return []model.VectorOperator{o.paramOp, o.paramOp2, o.next} + default: + return []model.VectorOperator{o.next} + } +} + +func (o *subqueryOperator) Next(ctx context.Context, buf []model.StepVector) (int, error) { + select { + case <-ctx.Done(): + return 0, ctx.Err() + default: + } + if o.currentStep > o.maxt { + return 0, nil + } + if err := o.initSeries(ctx); err != nil { + return 0, err + } + + if o.paramOp != nil { + n, err := o.paramOp.Next(ctx, o.paramBuf) + if err != nil { + return 0, err + } + for i := range n { + o.params[i] = math.NaN() + if len(o.paramBuf[i].Samples) == 1 { + o.params[i] = o.paramBuf[i].Samples[0] + } + + } + } + + if o.paramOp2 != nil { // double_exponential_smoothing + n, err := o.paramOp2.Next(ctx, o.param2Buf) + if err != nil { + return 0, err + } + for i := range n { + o.params2[i] = math.NaN() + if len(o.param2Buf[i].Samples) == 1 { + o.params2[i] = o.param2Buf[i].Samples[0] + } + + } + } + + n := 0 + maxSteps := min(o.stepsBatch, len(buf)) + + for i := 0; o.currentStep <= o.maxt && i < maxSteps; i++ { + mint := o.currentStep - o.subQuery.Range.Milliseconds() - o.subQuery.OriginalOffset.Milliseconds() + 1 + maxt := o.currentStep - o.subQuery.OriginalOffset.Milliseconds() + for _, b := range o.buffers { + b.Reset(mint, maxt+o.subQuery.Offset.Milliseconds()) + } + o.currentTrackedSamples = 0 + o.lastTrackedSamples = 0 + checkSampleLimitCounter := 0 + if len(o.lastVectors) > 0 { + for _, v := range o.lastVectors[o.lastCollected+1:] { + if v.T > maxt { + break + } + o.collect(v, mint) + o.lastCollected++ + } + if o.lastCollected == len(o.lastVectors)-1 { + + o.lastVectors = nil + o.lastCollected = -1 + } + } + + ACC: + for len(o.lastVectors) == 0 { + vecN, err := o.next.Next(ctx, o.tempBuf) + if err != nil { + return 0, err + } + if vecN == 0 { + break ACC + } + vectors := o.tempBuf[:vecN] + for j, vector := range vectors { + if vector.T > maxt { + o.lastVectors = vectors + o.lastCollected = j - 1 + break ACC + } + o.collect(vector, mint) + } + + checkSampleLimitCounter++ + if o.shouldCheckSampleLimit(checkSampleLimitCounter) { + if err := o.checkSampleLimit(); err != nil { + return 0, err + } + checkSampleLimitCounter = 0 + } + } + if checkSampleLimitCounter > 0 { + if err := o.checkSampleLimit(); err != nil { + return 0, err + } + } + + buf[n].Reset(o.currentStep) + hint := len(o.buffers) + for sampleId, rangeSamples := range o.buffers { + f, h, ok, _, err := rangeSamples.Eval(ctx, o.params[i], o.params2[i], math.MinInt64) + if err != nil { + return 0, err + } + // Note: warnings from subqueries are currently ignored since we don't have metric names here + if ok { + if h != nil { + buf[n].AppendHistogramWithSizeHint(uint64(sampleId), h, hint) + } else { + buf[n].AppendSampleWithSizeHint(uint64(sampleId), f, hint) + } + } + o.telemetry.IncrementSamplesAtTimestamp(rangeSamples.SampleCount(), buf[n].T) + } + n++ + o.currentStep += o.step + } + + return n, nil +} + +func (o *subqueryOperator) checkSampleLimit() error { + delta := o.currentTrackedSamples - o.lastTrackedSamples + if delta > 0 { + o.opts.SampleTracker.Add(delta) + } + o.lastTrackedSamples = o.currentTrackedSamples + return o.opts.SampleTracker.CheckLimit() +} + +func (o *subqueryOperator) collect(v model.StepVector, mint int64) { + if v.T < mint { + return + } + for i, s := range v.Samples { + buffer := o.buffers[v.SampleIDs[i]] + if !ringbuffer.Empty(buffer) && v.T <= buffer.MaxT() { + continue + } + buffer.Push(v.T, ringbuffer.Value{F: s}) + o.currentTrackedSamples++ + } + for i, s := range v.Histograms { + buffer := o.buffers[v.HistogramIDs[i]] + if !ringbuffer.Empty(buffer) && v.T < buffer.MaxT() { + continue + } + // Set any "NotCounterReset" and "CounterReset" hints in native + // histograms to "UnknownCounterReset" because we might + // otherwise miss a counter reset happening in samples not + // returned by the subquery, or we might over-detect counter + // resets if the sample with a counter reset is returned + // multiple times by a high-res subquery. This intentionally + // does not attempt to be clever (like detecting if we are + // really missing underlying samples or returning underlying + // samples multiple times) because subqueries on counters are + // inherently problematic WRT counter reset handling, so we + // cannot really solve the problem for good. We only want to + // avoid problems that happen due to the explicitly set counter + // reset hints and go back to the behavior we already know from + // float samples. + switch s.CounterResetHint { + case histogram.NotCounterReset, histogram.CounterReset: + s.CounterResetHint = histogram.UnknownCounterReset + } + buffer.Push(v.T, ringbuffer.Value{H: s}) + o.currentTrackedSamples += telemetry.CalculateHistogramSampleCount(s) + } + +} + +func (o *subqueryOperator) Series(ctx context.Context) ([]labels.Labels, error) { + if err := o.initSeries(ctx); err != nil { + return nil, err + } + return o.series, nil +} + +func (o *subqueryOperator) initSeries(ctx context.Context) error { + var err error + o.onceSeries.Do(func() { + + o.tempBuf = make([]model.StepVector, o.stepsBatch) + if o.paramOp != nil { + o.paramBuf = make([]model.StepVector, o.stepsBatch) + } + if o.paramOp2 != nil { + o.param2Buf = make([]model.StepVector, o.stepsBatch) + } + + var series []labels.Labels + series, err = o.next.Series(ctx) + if err != nil { + return + } + + o.series = make([]labels.Labels, len(series)) + o.buffers = make([]*ringbuffer.GenericRingBuffer, len(series)) + for i := range o.buffers { + o.buffers[i] = ringbuffer.New(ctx, 8, o.subQuery.Range.Milliseconds(), o.subQuery.Offset.Milliseconds(), o.call) + } + var b labels.ScratchBuilder + for i, s := range series { + lbls := s + if o.funcExpr.Func.Name != "last_over_time" { + lbls = extlabels.DropReserved(s, b) + } + o.series[i] = lbls + } + + }) + return err +} + +func (o *subqueryOperator) shouldCheckSampleLimit(checkSampleLimitCounter int) bool { + if len(o.series) == 0 { + return checkSampleLimitCounter >= 1 + } + + limit := o.opts.SampleTracker.Limit() + targetSamplesPerCheck := int(float64(limit) * sampleLimitCheckPercentage) + + maxSamplesPerCall := len(o.series) * o.stepsBatch + if maxSamplesPerCall == 0 { + return checkSampleLimitCounter >= 1 + } + + interval := max(targetSamplesPerCheck/maxSamplesPerCall, 1) + + return checkSampleLimitCounter >= interval +} diff --git a/internal/promql-engine/execution/step_invariant/step_invariant.go b/internal/promql-engine/execution/step_invariant/step_invariant.go new file mode 100644 index 00000000000..1c80c4b9411 --- /dev/null +++ b/internal/promql-engine/execution/step_invariant/step_invariant.go @@ -0,0 +1,144 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package step_invariant + +import ( + "context" + "sync" + + "github.com/thanos-io/promql-engine/execution/model" + "github.com/thanos-io/promql-engine/execution/telemetry" + "github.com/thanos-io/promql-engine/logicalplan" + "github.com/thanos-io/promql-engine/query" + + "github.com/efficientgo/core/errors" + "github.com/prometheus/prometheus/model/labels" +) + +type stepInvariantOperator struct { + next model.VectorOperator + cacheResult bool + + seriesOnce sync.Once + series []labels.Labels + cacheVectorOnce sync.Once + cachedVector model.StepVector + + mint int64 + maxt int64 + step int64 + currentStep int64 + stepsBatch int +} + +func (u *stepInvariantOperator) Explain() (next []model.VectorOperator) { + return []model.VectorOperator{u.next} +} + +func (u *stepInvariantOperator) String() string { + return "[stepInvariant]" +} + +func NewStepInvariantOperator( + next model.VectorOperator, + expr logicalplan.Node, + opts *query.Options, +) (model.VectorOperator, error) { + // We set interval to be at least 1. + u := &stepInvariantOperator{ + next: next, + currentStep: opts.Start.UnixMilli(), + mint: opts.Start.UnixMilli(), + maxt: opts.End.UnixMilli(), + step: opts.Step.Milliseconds(), + stepsBatch: opts.StepsBatch, + cacheResult: true, + } + if u.step == 0 { + u.step = 1 + } + // We do not duplicate results for range selectors since result is a matrix + // with their unique timestamps which does not depend on the step. + switch expr.(type) { + case *logicalplan.MatrixSelector, *logicalplan.Subquery: + u.cacheResult = false + } + + return telemetry.NewOperator(telemetry.NewStepInvariantTelemetry(u, opts), u), nil +} + +func (u *stepInvariantOperator) Series(ctx context.Context) ([]labels.Labels, error) { + var err error + u.seriesOnce.Do(func() { + u.series, err = u.next.Series(ctx) + }) + if err != nil { + return nil, err + } + return u.series, nil +} + +func (u *stepInvariantOperator) Next(ctx context.Context, buf []model.StepVector) (int, error) { + select { + case <-ctx.Done(): + return 0, ctx.Err() + default: + } + + if u.currentStep > u.maxt { + return 0, nil + } + + if !u.cacheResult { + return u.next.Next(ctx, buf) + } + + if err := u.cacheInputVector(ctx); err != nil { + return 0, err + } + + n := 0 + maxSteps := min(u.stepsBatch, len(buf)) + + for i := 0; i < maxSteps && u.currentStep <= u.maxt; i++ { + buf[n].Reset(u.currentStep) + buf[n].AppendSamples(u.cachedVector.SampleIDs, u.cachedVector.Samples) + buf[n].AppendHistograms(u.cachedVector.HistogramIDs, u.cachedVector.Histograms) + n++ + u.currentStep += u.step + } + + return n, nil +} + +func (u *stepInvariantOperator) cacheInputVector(ctx context.Context) error { + var err error + u.cacheVectorOnce.Do(func() { + // Create a temporary buffer for reading one vector + tempBuf := make([]model.StepVector, 1) + n, readErr := u.next.Next(ctx, tempBuf) + if readErr != nil { + err = readErr + return + } + + if n == 0 || (len(tempBuf[0].Samples) == 0 && len(tempBuf[0].Histograms) == 0) { + return + } + + // Make sure we only have exactly one step vector. + if n != 1 { + err = errors.New("unexpected number of samples") + return + } + + // Copy the evaluated step vector. + // The timestamp of the vector is not relevant since we will produce + // new output vectors with the current step's timestamp. + u.cachedVector = model.StepVector{T: 0} + u.cachedVector.AppendSamples(tempBuf[0].SampleIDs, tempBuf[0].Samples) + u.cachedVector.AppendHistograms(tempBuf[0].HistogramIDs, tempBuf[0].Histograms) + }) + return err +} diff --git a/internal/promql-engine/execution/telemetry/telemetry.go b/internal/promql-engine/execution/telemetry/telemetry.go new file mode 100644 index 00000000000..10cb1c8b850 --- /dev/null +++ b/internal/promql-engine/execution/telemetry/telemetry.go @@ -0,0 +1,241 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package telemetry + +import ( + "context" + "fmt" + "time" + + "github.com/thanos-io/promql-engine/execution/model" + "github.com/thanos-io/promql-engine/logicalplan" + "github.com/thanos-io/promql-engine/query" + + "github.com/prometheus/prometheus/model/histogram" + "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/util/stats" +) + +type OperatorTelemetry interface { + fmt.Stringer + + MaxSeriesCount() int + SetMaxSeriesCount(count int) + ExecutionTimeTaken() time.Duration + AddSeriesExecutionTime(time.Duration) + SeriesExecutionTime() time.Duration + AddNextExecutionTime(time.Duration) + NextExecutionTime() time.Duration + IncrementSamplesAtTimestamp(samples int, t int64) + Samples() *stats.QuerySamples + LogicalNode() logicalplan.Node + UpdatePeak(count int) +} + +func NewTelemetry(operator fmt.Stringer, opts *query.Options) OperatorTelemetry { + if opts.EnableAnalysis { + return NewTrackedTelemetry(operator, opts, nil) + } + return NewNoopTelemetry(operator) +} + +func NewSubqueryTelemetry(operator fmt.Stringer, opts *query.Options) OperatorTelemetry { + if opts.EnableAnalysis { + return NewTrackedTelemetry(operator, opts, &logicalplan.Subquery{}) + } + return NewNoopTelemetry(operator) +} + +func NewStepInvariantTelemetry(operator fmt.Stringer, opts *query.Options) OperatorTelemetry { + if opts.EnableAnalysis { + return NewTrackedTelemetry(operator, opts, &logicalplan.StepInvariantExpr{}) + } + return NewNoopTelemetry(operator) +} + +type NoopTelemetry struct { + fmt.Stringer +} + +func NewNoopTelemetry(operator fmt.Stringer) *NoopTelemetry { + return &NoopTelemetry{Stringer: operator} +} + +func (tm *NoopTelemetry) AddExecutionTimeTaken(t time.Duration) {} + +func (tm *NoopTelemetry) ExecutionTimeTaken() time.Duration { + return time.Duration(0) +} + +func (tm *NoopTelemetry) AddSeriesExecutionTime(t time.Duration) {} + +func (tm *NoopTelemetry) SeriesExecutionTime() time.Duration { + return time.Duration(0) +} + +func (tm *NoopTelemetry) AddNextExecutionTime(t time.Duration) {} + +func (tm *NoopTelemetry) NextExecutionTime() time.Duration { + return time.Duration(0) +} + +func (tm *NoopTelemetry) IncrementSamplesAtTimestamp(_ int, _ int64) {} + +func (tm *NoopTelemetry) Samples() *stats.QuerySamples { return nil } + +func (tm *NoopTelemetry) MaxSeriesCount() int { return 0 } + +func (tm *NoopTelemetry) SetMaxSeriesCount(_ int) {} + +func (tm *NoopTelemetry) LogicalNode() logicalplan.Node { + return nil +} + +func (tm *NoopTelemetry) UpdatePeak(_ int) {} + +type TrackedTelemetry struct { + fmt.Stringer + + Series int + ExecutionTime time.Duration + SeriesTime time.Duration + NextTime time.Duration + LoadedSamples *stats.QuerySamples + logicalNode logicalplan.Node +} + +func NewTrackedTelemetry(operator fmt.Stringer, opts *query.Options, logicalPlanNode logicalplan.Node) *TrackedTelemetry { + ss := stats.NewQuerySamples(opts.EnablePerStepStats) + ss.InitStepTracking(opts.Start.UnixMilli(), opts.End.UnixMilli(), StepTrackingInterval(opts.Step)) + return &TrackedTelemetry{ + Stringer: operator, + LoadedSamples: ss, + logicalNode: logicalPlanNode, + } +} + +func StepTrackingInterval(step time.Duration) int64 { + if step == 0 { + return 1 + } + return int64(step / (time.Millisecond / time.Nanosecond)) +} + +func (ti *TrackedTelemetry) AddExecutionTimeTaken(t time.Duration) { ti.ExecutionTime += t } + +func (ti *TrackedTelemetry) ExecutionTimeTaken() time.Duration { + return ti.ExecutionTime +} + +func (ti *TrackedTelemetry) AddSeriesExecutionTime(t time.Duration) { + ti.SeriesTime += t + ti.ExecutionTime += t +} + +func (ti *TrackedTelemetry) SeriesExecutionTime() time.Duration { + return ti.SeriesTime +} + +func (ti *TrackedTelemetry) AddNextExecutionTime(t time.Duration) { + ti.NextTime += t + ti.ExecutionTime += t +} + +func (ti *TrackedTelemetry) NextExecutionTime() time.Duration { + return ti.NextTime +} + +func (ti *TrackedTelemetry) IncrementSamplesAtTimestamp(samples int, t int64) { + ti.LoadedSamples.IncrementSamplesAtTimestamp(t, int64(samples)) +} + +func (ti *TrackedTelemetry) LogicalNode() logicalplan.Node { + return ti.logicalNode +} + +func (ti *TrackedTelemetry) Samples() *stats.QuerySamples { return ti.LoadedSamples } + +func (ti *TrackedTelemetry) MaxSeriesCount() int { return ti.Series } + +func (ti *TrackedTelemetry) SetMaxSeriesCount(count int) { ti.Series = count } + +func (ti *TrackedTelemetry) UpdatePeak(count int) { + ti.Samples().UpdatePeak(count) +} + +type ObservableVectorOperator interface { + model.VectorOperator + OperatorTelemetry +} + +// CalculateHistogramSampleCount returns the size of the FloatHistogram compared to the size of a Float. +// The total size is calculated considering the histogram timestamp (p.T - 8 bytes), +// and then a number of bytes in the histogram. +// This sum is divided by 16, as samples are 16 bytes. +// See: https://github.com/prometheus/prometheus/blob/2bf6f4c9dcbb1ad2e8fef70c6a48d8fc44a7f57c/promql/value.go#L178 +func CalculateHistogramSampleCount(h *histogram.FloatHistogram) int { + return (h.Size() + 8) / 16 +} + +func NewOperator(telemetry OperatorTelemetry, inner model.VectorOperator) model.VectorOperator { + op := &Operator{ + inner: inner, + } + op.OperatorTelemetry = telemetry + return op +} + +// Operator wraps other inner operator to track its telemetry. +type Operator struct { + OperatorTelemetry + inner model.VectorOperator +} + +func (t *Operator) Series(ctx context.Context) ([]labels.Labels, error) { + start := time.Now() + defer func() { t.OperatorTelemetry.AddSeriesExecutionTime(time.Since(start)) }() + s, err := t.inner.Series(ctx) + if err != nil { + return nil, err + } + t.OperatorTelemetry.SetMaxSeriesCount(len(s)) + return s, err +} + +func (t *Operator) Next(ctx context.Context, buf []model.StepVector) (int, error) { + start := time.Now() + var totalSamplesBeforeCount int64 + totalSamplesBefore := t.OperatorTelemetry.Samples() + if totalSamplesBefore != nil { + totalSamplesBeforeCount = totalSamplesBefore.TotalSamples + } else { + totalSamplesBeforeCount = 0 + } + + defer func() { t.OperatorTelemetry.AddNextExecutionTime(time.Since(start)) }() + n, err := t.inner.Next(ctx, buf) + if err != nil { + return 0, err + } + + var totalSamplesAfter int64 + totalSamplesAfterSamples := t.OperatorTelemetry.Samples() + if totalSamplesAfterSamples != nil { + totalSamplesAfter = totalSamplesAfterSamples.TotalSamples + } else { + totalSamplesAfter = 0 + } + + t.OperatorTelemetry.UpdatePeak(int(totalSamplesAfter) - int(totalSamplesBeforeCount)) + + return n, err +} + +func (t *Operator) Explain() []model.VectorOperator { + return t.inner.Explain() +} + +func (t *Operator) String() string { + return t.inner.String() +} diff --git a/internal/promql-engine/execution/unary/unary.go b/internal/promql-engine/execution/unary/unary.go new file mode 100644 index 00000000000..29e9736448f --- /dev/null +++ b/internal/promql-engine/execution/unary/unary.go @@ -0,0 +1,92 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package unary + +import ( + "context" + "sync" + + "github.com/thanos-io/promql-engine/execution/model" + "github.com/thanos-io/promql-engine/execution/telemetry" + "github.com/thanos-io/promql-engine/extlabels" + "github.com/thanos-io/promql-engine/query" + + "github.com/prometheus/prometheus/model/histogram" + "github.com/prometheus/prometheus/model/labels" + "gonum.org/v1/gonum/floats" +) + +type unaryNegation struct { + next model.VectorOperator + once sync.Once + + series []labels.Labels +} + +func NewUnaryNegation(next model.VectorOperator, opts *query.Options) (model.VectorOperator, error) { + u := &unaryNegation{ + next: next, + } + return telemetry.NewOperator(telemetry.NewTelemetry(u, opts), u), nil +} + +func (u *unaryNegation) Explain() (next []model.VectorOperator) { + return []model.VectorOperator{u.next} +} + +func (u *unaryNegation) String() string { + return "[unaryNegation]" +} + +func (u *unaryNegation) Series(ctx context.Context) ([]labels.Labels, error) { + if err := u.loadSeries(ctx); err != nil { + return nil, err + } + return u.series, nil +} + +func (u *unaryNegation) loadSeries(ctx context.Context) error { + var err error + u.once.Do(func() { + var series []labels.Labels + series, err = u.next.Series(ctx) + if err != nil { + return + } + u.series = make([]labels.Labels, len(series)) + var b labels.ScratchBuilder + for i := range series { + lbls := extlabels.DropReserved(series[i], b) + u.series[i] = lbls + } + }) + return err +} + +func (u *unaryNegation) Next(ctx context.Context, buf []model.StepVector) (int, error) { + select { + case <-ctx.Done(): + return 0, ctx.Err() + default: + } + + n, err := u.next.Next(ctx, buf) + if err != nil { + return 0, err + } + if n == 0 { + return 0, nil + } + for i := range n { + floats.Scale(-1, buf[i].Samples) + negateHistograms(buf[i].Histograms) + } + return n, nil +} + +func negateHistograms(hists []*histogram.FloatHistogram) { + for i := range hists { + hists[i] = hists[i].Copy().Mul(-1) + } +} diff --git a/internal/promql-engine/extlabels/labels.go b/internal/promql-engine/extlabels/labels.go new file mode 100644 index 00000000000..c602d8ab1c9 --- /dev/null +++ b/internal/promql-engine/extlabels/labels.go @@ -0,0 +1,68 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package extlabels + +import ( + "github.com/efficientgo/core/errors" + "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/schema" +) + +var ( + ErrDuplicateLabelSet = errors.New("vector cannot contain metrics with the same labelset") +) + +const ( + MetricType = "__type__" + MetricUnit = "__unit__" +) + +// DropReserved removes all reserved labels (__name__, __type__, __unit__) and returns the remaining labels. +func DropReserved(l labels.Labels, b labels.ScratchBuilder) labels.Labels { + return DropLabels(l, schema.IsMetadataLabel, b) +} + +// DropBucketLabel removes the le label and returns the dropped name and remaining labels. +func DropBucketLabel(l labels.Labels, b labels.ScratchBuilder) (labels.Labels, labels.Label) { + return DropLabel(l, labels.BucketLabel, b) +} + +// DropLabel removes the label with name from l and returns the dropped label. +func DropLabel(l labels.Labels, name string, b labels.ScratchBuilder) (labels.Labels, labels.Label) { + var ret labels.Label + + if l.IsEmpty() { + return l, labels.Label{} + } + + b.Reset() + + l.Range(func(l labels.Label) { + if l.Name == name { + ret = l + return + } + + b.Add(l.Name, l.Value) + }) + + return b.Labels(), ret +} + +// DropLabels removes labels from l based on the shouldDrop function and returns the remaining labels. +func DropLabels(l labels.Labels, shouldDrop func(name string) bool, b labels.ScratchBuilder) labels.Labels { + if l.IsEmpty() { + return l + } + + b.Reset() + + l.Range(func(lbl labels.Label) { + if !shouldDrop(lbl.Name) { + b.Add(lbl.Name, lbl.Value) + } + }) + + return b.Labels() +} diff --git a/internal/promql-engine/go.mod b/internal/promql-engine/go.mod new file mode 100644 index 00000000000..c8247b21232 --- /dev/null +++ b/internal/promql-engine/go.mod @@ -0,0 +1,108 @@ +module github.com/thanos-io/promql-engine + +go 1.24.0 + +toolchain go1.24.4 + +require ( + github.com/cespare/xxhash/v2 v2.3.0 + github.com/cortexproject/promqlsmith v0.0.0-20250407233056-90db95b1a4e4 + github.com/efficientgo/core v1.0.0-rc.2 + github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb + github.com/google/go-cmp v0.7.0 + github.com/prometheus/client_golang v1.23.2 + github.com/prometheus/common v0.67.4 + github.com/prometheus/prometheus v0.308.0 + github.com/stretchr/testify v1.11.1 + go.uber.org/goleak v1.3.0 + golang.org/x/exp v0.0.0-20250808145144-a408d31f581a + golang.org/x/tools v0.37.0 + gonum.org/v1/gonum v0.16.0 +) + +require ( + github.com/googleapis/gax-go/v2 v2.15.0 // indirect + github.com/prometheus/sigv4 v0.3.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect +) + +require ( + cloud.google.com/go/auth v0.17.0 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect + cloud.google.com/go/compute/metadata v0.9.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.12.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0 // indirect + github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect + github.com/aws/aws-sdk-go-v2 v1.39.6 // indirect + github.com/aws/aws-sdk-go-v2/config v1.31.17 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.18.21 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.39.1 // indirect + github.com/aws/smithy-go v1.23.2 // indirect + github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dennwc/varint v1.0.0 // indirect + github.com/edsrzf/mmap-go v1.2.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect + github.com/golang/snappy v1.0.0 // indirect + github.com/google/s2a-go v0.1.9 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect + github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 // indirect + github.com/jpillora/backoff v1.0.0 // indirect + github.com/klauspost/compress v1.18.1 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect + github.com/oklog/ulid/v2 v2.1.1 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_golang/exp v0.0.0-20250914183048-a974e0d45e0a // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/otlptranslator v1.0.0 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect + go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect + go.uber.org/atomic v1.11.0 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + golang.org/x/crypto v0.43.0 // indirect + golang.org/x/mod v0.28.0 // indirect + golang.org/x/net v0.46.0 // indirect + golang.org/x/oauth2 v0.32.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/text v0.30.0 // indirect + golang.org/x/time v0.13.0 // indirect + google.golang.org/api v0.252.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect + google.golang.org/grpc v1.76.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/apimachinery v0.34.1 // indirect + k8s.io/client-go v0.34.1 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect +) + +exclude ( + // Exclude erronous modules that cause go mod tidy with go 1.19.1 to fail with + // ambiguous import: found package cloud.google.com/go/compute/metadata in multiple modules. + cloud.google.com/go v0.34.0 + cloud.google.com/go v0.65.0 +) diff --git a/internal/promql-engine/go.sum b/internal/promql-engine/go.sum new file mode 100644 index 00000000000..2b917b6ac4d --- /dev/null +++ b/internal/promql-engine/go.sum @@ -0,0 +1,452 @@ +cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4= +cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 h1:5YTBM8QDVIBN3sxBil89WfdAAqDZbyJTgh688DSxX5w= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.12.0 h1:wL5IEG5zb7BVv1Kv0Xm92orq+5hB5Nipn3B5tn4Rqfk= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.12.0/go.mod h1:J7MUC/wtRpfGVbQ5sIItY5/FuVWmvzlY21WAOfQnq/I= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5 v5.7.0 h1:LkHbJbgF3YyvC53aqYGR+wWQDn2Rdp9AQdGndf9QvY4= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5 v5.7.0/go.mod h1:QyiQdW4f4/BIfB8ZutZ2s+28RAgfa/pT+zS++ZHyM1I= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v4 v4.3.0 h1:bXwSugBiSbgtz7rOtbfGf+woewp4f06orW9OP5BjHLA= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v4 v4.3.0/go.mod h1:Y/HgrePTmGy9HjdSGTqZNa+apUpTVIEVKXJyARP2lrk= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= +github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0 h1:XkkQbfMyuH2jTSjQjSoihryI8GINRcs4xp8lNawg0FI= +github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= +github.com/Code-Hex/go-generics-cache v1.5.1 h1:6vhZGc5M7Y/YD8cIUcY8kcuQLB4cHR7U+0KMqAA0KcU= +github.com/Code-Hex/go-generics-cache v1.5.1/go.mod h1:qxcC9kRVrct9rHeiYpFWSoW1vxyillCVzX13KZG8dl4= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0= +github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs= +github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA= +github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= +github.com/aws/aws-sdk-go-v2 v1.39.6 h1:2JrPCVgWJm7bm83BDwY5z8ietmeJUbh3O2ACnn+Xsqk= +github.com/aws/aws-sdk-go-v2 v1.39.6/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE= +github.com/aws/aws-sdk-go-v2/config v1.31.17 h1:QFl8lL6RgakNK86vusim14P2k8BFSxjvUkcWLDjgz9Y= +github.com/aws/aws-sdk-go-v2/config v1.31.17/go.mod h1:V8P7ILjp/Uef/aX8TjGk6OHZN6IKPM5YW6S78QnRD5c= +github.com/aws/aws-sdk-go-v2/credentials v1.18.21 h1:56HGpsgnmD+2/KpG0ikvvR8+3v3COCwaF4r+oWwOeNA= +github.com/aws/aws-sdk-go-v2/credentials v1.18.21/go.mod h1:3YELwedmQbw7cXNaII2Wywd+YY58AmLPwX4LzARgmmA= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 h1:T1brd5dR3/fzNFAQch/iBKeX07/ffu/cLu+q+RuzEWk= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13/go.mod h1:Peg/GBAQ6JDt+RoBf4meB1wylmAipb7Kg2ZFakZTlwk= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 h1:a+8/MLcWlIxo1lF9xaGt3J/u3yOZx+CdSveSNwjhD40= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13/go.mod h1:oGnKwIYZ4XttyU2JWxFrwvhF6YKiK/9/wmE3v3Iu9K8= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13 h1:HBSI2kDkMdWz4ZM7FjwE7e/pWDEZ+nR95x8Ztet1ooY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13/go.mod h1:YE94ZoDArI7awZqJzBAZ3PDD2zSfuP7w6P2knOzIn8M= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.262.0 h1:5qBb1XV/D18qtCHd3bmmxoVglI+fZ4QWuS/EB8kIXYQ= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.262.0/go.mod h1:NDdDLLW5PtLLXN661gKcvJvqAH5OBXsfhMlmKVu1/pY= +github.com/aws/aws-sdk-go-v2/service/ecs v1.67.2 h1:oeICOX/+D0XXV1aMYJPXVe3CO37zYr7fB6HFgxchleU= +github.com/aws/aws-sdk-go-v2/service/ecs v1.67.2/go.mod h1:rrhqfkXfa2DSNq0RyFhnnFEAyI+yJB4+2QlZKeJvMjs= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 h1:x2Ibm/Af8Fi+BH+Hsn9TXGdT+hKbDd5XOTZxTMxDk7o= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3/go.mod h1:IW1jwyrQgMdhisceG8fQLmQIydcT/jWY21rFhzgaKwo= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 h1:kDqdFvMY4AtKoACfzIGD8A0+hbT41KTKF//gq7jITfM= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13/go.mod h1:lmKuogqSU3HzQCwZ9ZtcqOc5XGMqtDK7OIc2+DxiUEg= +github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.4 h1:/1o2AYwHJojUDeMvQNyJiKZwcWCc3e4kQuTXqRLuThc= +github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.4/go.mod h1:Nn2xx6HojGuNMtUFxxz/nyNLSS+tHMRsMhe3+W3wB5k= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 h1:0JPwLz1J+5lEOfy/g0SURC9cxhbQ1lIMHMa+AHZSzz0= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.1/go.mod h1:fKvyjJcz63iL/ftA6RaM8sRCtN4r4zl4tjL3qw5ec7k= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 h1:OWs0/j2UYR5LOGi88sD5/lhN6TDLG6SfA7CqsQO9zF0= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5/go.mod h1:klO+ejMvYsB4QATfEOIXk8WAEwN4N0aBfJpvC+5SZBo= +github.com/aws/aws-sdk-go-v2/service/sts v1.39.1 h1:mLlUgHn02ue8whiR4BmxxGJLR2gwU6s6ZzJ5wDamBUs= +github.com/aws/aws-sdk-go-v2/service/sts v1.39.1/go.mod h1:E19xDjpzPZC7LS2knI9E6BaRFDK43Eul7vd6rSq2HWk= +github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM= +github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= +github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 h1:6df1vn4bBlDDo4tARvBm7l6KA9iVMnE3NWizDeWSrps= +github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3/go.mod h1:CIWtjkly68+yqLPbvwwR/fjNJA/idrtULjZWh2v1ys0= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls= +github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/cortexproject/promqlsmith v0.0.0-20250407233056-90db95b1a4e4 h1:dpo7kQ24uFSV6Zgm9/kB34TIUWjGmadlbKrM6fNfQko= +github.com/cortexproject/promqlsmith v0.0.0-20250407233056-90db95b1a4e4/go.mod h1:jh6POgN18lXU133HBMfwr/1TjvBp8e5kL4ZtRsAPvGY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dennwc/varint v1.0.0 h1:kGNFFSSw8ToIy3obO/kKr8U9GZYUAxQEVuix4zfDWzE= +github.com/dennwc/varint v1.0.0/go.mod h1:hnItb35rvZvJrbTALZtY/iQfDs48JKRG1RPpgziApxA= +github.com/digitalocean/godo v1.168.0 h1:mlORtUcPD91LQeJoznrH3XvfvgK3t8Wvrpph9giUT/Q= +github.com/digitalocean/godo v1.168.0/go.mod h1:xQsWpVCCbkDrWisHA72hPzPlnC+4W5w/McZY5ij9uvU= +github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= +github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= +github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/edsrzf/mmap-go v1.2.0 h1:hXLYlkbaPzt1SaQk+anYwKSRNhufIDCchSPkUD6dD84= +github.com/edsrzf/mmap-go v1.2.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q= +github.com/efficientgo/core v1.0.0-rc.2 h1:7j62qHLnrZqO3V3UA0AqOGd5d5aXV3AX6m/NZBHp78I= +github.com/efficientgo/core v1.0.0-rc.2/go.mod h1:FfGdkzWarkuzOlY04VY+bGfb1lWrjaL6x/GLcQ4vJps= +github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= +github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= +github.com/envoyproxy/go-control-plane/envoy v1.35.0 h1:ixjkELDE+ru6idPxcHLj8LBVc2bFP7iBytj353BoHUo= +github.com/envoyproxy/go-control-plane/envoy v1.35.0/go.mod h1:09qwbGVuSWWAyN5t/b3iyVfz5+z8QWGrzkoqm/8SbEs= +github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= +github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= +github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb h1:IT4JYU7k4ikYg1SCxNI1/Tieq/NFvh6dzLdgi7eu0tM= +github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb/go.mod h1:bH6Xx7IW64qjjJq8M2u4dxNaBiDfKK+z/3eGDpXEQhc= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM= +github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-zookeeper/zk v1.0.4 h1:DPzxraQx7OrPyXq2phlGlNSIyWEsAox0RJmjTseMV6I= +github.com/go-zookeeper/zk v1.0.4/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= +github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= +github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= +github.com/gophercloud/gophercloud/v2 v2.8.0 h1:of2+8tT6+FbEYHfYC8GBu8TXJNsXYSNm9KuvpX7Neqo= +github.com/gophercloud/gophercloud/v2 v2.8.0/go.mod h1:Ki/ILhYZr/5EPebrPL9Ej+tUg4lqx71/YH2JWVeU+Qk= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= +github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 h1:cLN4IBkmkYZNnk7EAJ0BHIethd+J6LqxFNw5mSiI2bM= +github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk= +github.com/hashicorp/consul/api v1.32.0 h1:5wp5u780Gri7c4OedGEPzmlUEzi0g2KyiPphSr6zjVg= +github.com/hashicorp/consul/api v1.32.0/go.mod h1:Z8YgY0eVPukT/17ejW+l+C7zJmKwgPHtjU1q16v/Y40= +github.com/hashicorp/cronexpr v1.1.3 h1:rl5IkxXN2m681EfivTlccqIryzYJSXRGRNa0xeG7NA4= +github.com/hashicorp/cronexpr v1.1.3/go.mod h1:P4wA0KBl9C5q2hABiMO7cp6jcIg96CDh1Efb3g1PWA4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= +github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= +github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/golang-lru v0.6.0 h1:uL2shRDx7RTrOrTCUZEGP/wJUFiUI8QT6E7z5o8jga4= +github.com/hashicorp/golang-lru v0.6.0/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/nomad/api v0.0.0-20250930071859-eaa0fe0e27af h1:ScAYf8O+9xTqTJPZH8MIlUfO+ak8cb31rW1aYJgS+jE= +github.com/hashicorp/nomad/api v0.0.0-20250930071859-eaa0fe0e27af/go.mod h1:sldFTIgs+FsUeKU3LwVjviAIuksxD8TzDOn02MYwslE= +github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY= +github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= +github.com/hetznercloud/hcloud-go/v2 v2.29.0 h1:LzNFw5XLBfftyu3WM1sdSLjOZBlWORtz2hgGydHaYV8= +github.com/hetznercloud/hcloud-go/v2 v2.29.0/go.mod h1:XBU4+EDH2KVqu2KU7Ws0+ciZcX4ygukQl/J0L5GS8P8= +github.com/ionos-cloud/sdk-go/v6 v6.3.4 h1:jTvGl4LOF8v8OYoEIBNVwbFoqSGAFqn6vGE7sp7/BqQ= +github.com/ionos-cloud/sdk-go/v6 v6.3.4/go.mod h1:wCVwNJ/21W29FWFUv+fNawOTMlFoP1dS3L+ZuztFW48= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= +github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= +github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= +github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo= +github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= +github.com/knadh/koanf/providers/confmap v1.0.0 h1:mHKLJTE7iXEys6deO5p6olAiZdG5zwp8Aebir+/EaRE= +github.com/knadh/koanf/providers/confmap v1.0.0/go.mod h1:txHYHiI2hAtF0/0sCmcuol4IDcuQbKTybiB1nOcUo1A= +github.com/knadh/koanf/v2 v2.3.0 h1:Qg076dDRFHvqnKG97ZEsi9TAg2/nFTa9hCdcSa1lvlM= +github.com/knadh/koanf/v2 v2.3.0/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28= +github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b h1:udzkj9S/zlT5X367kqJis0QP7YMxobob6zhzq6Yre00= +github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b/go.mod h1:pcaDhQK0/NJZEvtCO0qQPPropqV0sJOJ6YW7X+9kRwM= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/linode/linodego v1.60.0 h1:SgsebJFRCi+lSmYy+C40wmKZeJllGGm+W12Qw4+yVdI= +github.com/linode/linodego v1.60.0/go.mod h1:1+Bt0oTz5rBnDOJbGhccxn7LYVytXTIIfAy7QYmijDs= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA= +github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= +github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= +github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics v0.139.0 h1:D5aGQCErSCb4sKIHoZhgR4El6AzgviTRYlHUpbSFqDo= +github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics v0.139.0/go.mod h1:ZjeRsA5oaVk89fg5D+iXStx2QncmhAvtGbdSumT07H4= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.139.0 h1:6/j0Ta8ZJnmAFVEoC3aZ1Hs19RB4fHzlN6kOZhsBJqM= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.139.0/go.mod h1:VfA8xHz4xg7Fyj5bBsCDbOO3iVYzDn9wP/QFsjcAE5c= +github.com/open-telemetry/opentelemetry-collector-contrib/processor/deltatocumulativeprocessor v0.139.0 h1:iRNX/ueuad1psOVgnNkxuQmXxvF3ze5ZZCP66xKFk/w= +github.com/open-telemetry/opentelemetry-collector-contrib/processor/deltatocumulativeprocessor v0.139.0/go.mod h1:bW09lo3WgHsPsZ1mgsJvby9wCefT5o13patM5phdfIU= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= +github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/ovh/go-ovh v1.9.0 h1:6K8VoL3BYjVV3In9tPJUdT7qMx9h0GExN9EXx1r2kKE= +github.com/ovh/go-ovh v1.9.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c= +github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_golang/exp v0.0.0-20250914183048-a974e0d45e0a h1:RF1vfKM34/3DbGNis22BGd6sDDY3XBi0eM7pYqmOEO0= +github.com/prometheus/client_golang/exp v0.0.0-20250914183048-a974e0d45e0a/go.mod h1:FGJuwvfcPY0V5enm+w8zF1RNS062yugQtPPQp1c4Io4= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= +github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= +github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos= +github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/prometheus/prometheus v0.308.0 h1:kVh/5m1n6m4cSK9HYTDEbMxzuzCWyEdPdKSxFRxXj04= +github.com/prometheus/prometheus v0.308.0/go.mod h1:xXYKzScyqyFHihpS0UsXpC2F3RA/CygOs7wb4mpdusE= +github.com/prometheus/sigv4 v0.3.0 h1:QIG7nTbu0JTnNidGI1Uwl5AGVIChWUACxn2B/BQ1kms= +github.com/prometheus/sigv4 v0.3.0/go.mod h1:fKtFYDus2M43CWKMNtGvFNHGXnAJJEGZbiYCmVp/F8I= +github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg= +github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/scaleway/scaleway-sdk-go v1.0.0-beta.35 h1:8xfn1RzeI9yoCUuEwDy08F+No6PcKZGEDOQ6hrRyLts= +github.com/scaleway/scaleway-sdk-go v1.0.0-beta.35/go.mod h1:47B1d/YXmSAxlJxUJxClzHR6b3T4M1WyCvwENPQNBWc= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stackitcloud/stackit-sdk-go/core v0.17.3 h1:GsZGmRRc/3GJLmCUnsZswirr5wfLRrwavbnL/renOqg= +github.com/stackitcloud/stackit-sdk-go/core v0.17.3/go.mod h1:HBCXJGPgdRulplDzhrmwC+Dak9B/x0nzNtmOpu+1Ahg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/vultr/govultr/v2 v2.17.2 h1:gej/rwr91Puc/tgh+j33p/BLR16UrIPnSr+AIwYWZQs= +github.com/vultr/govultr/v2 v2.17.2/go.mod h1:ZFOKGWmgjytfyjeyAdhQlSWwTjh2ig+X49cAp50dzXI= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/collector/component v1.45.0 h1:gGFfVdbQ+1YuyUkJjWo85I7euu3H/CiupuzCHv8OgHA= +go.opentelemetry.io/collector/component v1.45.0/go.mod h1:xoNFnRKE8Iv6gmlqAKgjayWraRnDcYLLgrPt9VgyO2g= +go.opentelemetry.io/collector/confmap v1.45.0 h1:7M7TTlpzX4r+mIzP/ARdxZBAvI4N+1V96phDane+akU= +go.opentelemetry.io/collector/confmap v1.45.0/go.mod h1:AE1dnkjv0T9gptsh5+mTX0XFGdXx0n7JS4b7CcPfJ6Q= +go.opentelemetry.io/collector/confmap/xconfmap v0.139.0 h1:uQGpFuWnTCXqdMbI3gDSvkwU66/kF/aoC0kVMrit1EM= +go.opentelemetry.io/collector/confmap/xconfmap v0.139.0/go.mod h1:d0ucaeNq2rojFRSQsCHF/gkT3cgBx5H2bVkPQMj57ck= +go.opentelemetry.io/collector/consumer v1.45.0 h1:TtqXxgW+1GSCwdoohq0fzqnfqrZBKbfo++1XRj8mrEA= +go.opentelemetry.io/collector/consumer v1.45.0/go.mod h1:pJzqTWBubwLt8mVou+G4/Hs23b3m425rVmld3LqOYpY= +go.opentelemetry.io/collector/featuregate v1.45.0 h1:D06hpf1F2KzKC+qXLmVv5e8IZpgCyZVeVVC8iOQxVmw= +go.opentelemetry.io/collector/featuregate v1.45.0/go.mod h1:d0tiRzVYrytB6LkcYgz2ESFTv7OktRPQe0QEQcPt1L4= +go.opentelemetry.io/collector/pdata v1.45.0 h1:q4XaISpeX640BcwXwb2mKOVw/gb67r22HjGWl8sbWsk= +go.opentelemetry.io/collector/pdata v1.45.0/go.mod h1:5q2f001YhwMQO8QvpFhCOa4Cq/vtwX9W4HRMsXkU/nE= +go.opentelemetry.io/collector/pipeline v1.45.0 h1:sn9JJAEBe3XABTkWechMk0eH60QMBjjNe5V+ccBl+Uo= +go.opentelemetry.io/collector/pipeline v1.45.0/go.mod h1:xUrAqiebzYbrgxyoXSkk6/Y3oi5Sy3im2iCA51LwUAI= +go.opentelemetry.io/collector/processor v1.45.0 h1:GH5km9BkDQOoz7MR0jzTnzB1Kb5vtKzPwa/wDmRg2dQ= +go.opentelemetry.io/collector/processor v1.45.0/go.mod h1:wdlaTTC3wqlZIJP9R9/SLc2q7h+MFGARsxfjgPtwbes= +go.opentelemetry.io/collector/semconv v0.128.0 h1:MzYOz7Vgb3Kf5D7b49pqqgeUhEmOCuT10bIXb/Cc+k4= +go.opentelemetry.io/collector/semconv v0.128.0/go.mod h1:OPXer4l43X23cnjLXIZnRj/qQOjSuq4TgBLI76P9hns= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0 h1:2pn7OzMewmYRiNtv1doZnLo3gONcnMHlFnmOR8Vgt+8= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0/go.mod h1:rjbQTDEPQymPE0YnRQp9/NuPwwtL0sesz/fnqRW/v84= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/exp v0.0.0-20250808145144-a408d31f581a h1:Y+7uR/b1Mw2iSXZ3G//1haIiSElDQZ8KWh0h+sZPG90= +golang.org/x/exp v0.0.0-20250808145144-a408d31f581a/go.mod h1:rT6SFzZ7oxADUDx58pcaKFTcZ+inxAa9fTrYx/uVYwg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= +golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= +golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +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.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= +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.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= +golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= +golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/api v0.252.0 h1:xfKJeAJaMwb8OC9fesr369rjciQ704AjU/psjkKURSI= +google.golang.org/api v0.252.0/go.mod h1:dnHOv81x5RAmumZ7BWLShB/u7JZNeyalImxHmtTHxqw= +google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= +google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 h1:8XJ4pajGwOlasW+L13MnEGA8W4115jJySQtVfS2/IBU= +google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4/go.mod h1:NnuHhy+bxcg30o7FnVAZbXsPHUDQ9qKWAQKCD7VxFtk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= +google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= +google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM= +k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk= +k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4= +k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY= +k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA= +k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/internal/promql-engine/go.tools.mod b/internal/promql-engine/go.tools.mod new file mode 100644 index 00000000000..07f95d32557 --- /dev/null +++ b/internal/promql-engine/go.tools.mod @@ -0,0 +1,304 @@ +module github.com/thanos-io/promql-engine + +go 1.24.4 + +tool ( + github.com/bwplotka/mdox + github.com/daixiang0/gci + github.com/fatih/faillint + github.com/golangci/golangci-lint/cmd/golangci-lint + golang.org/x/perf/cmd/benchstat + golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize +) + +require ( + github.com/cespare/xxhash/v2 v2.3.0 + github.com/cortexproject/promqlsmith v0.0.0-20250203072244-cbb5738d00ca + github.com/efficientgo/core v1.0.0-rc.3 + github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb + github.com/google/go-cmp v0.7.0 + github.com/prometheus/client_golang v1.21.1 + github.com/prometheus/common v0.63.0 + github.com/prometheus/prometheus v0.302.1 + github.com/stretchr/testify v1.10.0 + go.uber.org/goleak v1.3.0 + golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 + golang.org/x/tools v0.34.0 + gonum.org/v1/gonum v0.16.0 +) + +require ( + 4d63.com/gocheckcompilerdirectives v1.3.0 // indirect + 4d63.com/gochecknoglobals v0.2.2 // indirect + cloud.google.com/go/auth v0.15.0 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect + cloud.google.com/go/compute/metadata v0.6.0 // indirect + dmitri.shuralyov.com/go/generated v0.0.0-20170818220700-b1254a446363 // indirect + github.com/4meepo/tagalign v1.4.2 // indirect + github.com/Abirdcfly/dupword v0.1.3 // indirect + github.com/Antonboom/errname v1.0.0 // indirect + github.com/Antonboom/nilnil v1.0.1 // indirect + github.com/Antonboom/testifylint v1.5.2 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 // indirect + github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect + github.com/Crocmagnon/fatcontext v0.7.1 // indirect + github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 // indirect + github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.1 // indirect + github.com/Kunde21/markdownfmt/v2 v2.1.1-0.20210810103848-727f02f4c51c // indirect + github.com/Masterminds/semver/v3 v3.3.0 // indirect + github.com/OpenPeeDeeP/depguard/v2 v2.2.1 // indirect + github.com/PuerkitoBio/goquery v1.5.1 // indirect + github.com/aclements/go-moremath v0.0.0-20210112150236-f10218a38794 // indirect + github.com/alecthomas/chroma v0.8.2 // indirect + github.com/alecthomas/go-check-sumtype v0.3.1 // indirect + github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect + github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect + github.com/alexkohler/nakedret/v2 v2.0.5 // indirect + github.com/alexkohler/prealloc v1.0.0 // indirect + github.com/alingse/asasalint v0.0.11 // indirect + github.com/alingse/nilnesserr v0.1.2 // indirect + github.com/andybalholm/cascadia v1.2.0 // indirect + github.com/antchfx/htmlquery v1.2.3 // indirect + github.com/antchfx/xmlquery v1.3.4 // indirect + github.com/antchfx/xpath v1.1.10 // indirect + github.com/ashanbrown/forbidigo v1.6.0 // indirect + github.com/ashanbrown/makezero v1.2.0 // indirect + github.com/aws/aws-sdk-go v1.55.6 // indirect + github.com/aymerick/douceur v0.2.0 // indirect + github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/bkielbasa/cyclop v1.2.3 // indirect + github.com/blizzy78/varnamelen v0.8.0 // indirect + github.com/bombsimon/wsl/v4 v4.5.0 // indirect + github.com/breml/bidichk v0.3.2 // indirect + github.com/breml/errchkjson v0.4.0 // indirect + github.com/butuzov/ireturn v0.3.1 // indirect + github.com/butuzov/mirror v1.3.0 // indirect + github.com/bwplotka/mdox v0.9.0 // indirect + github.com/catenacyber/perfsprint v0.8.2 // indirect + github.com/ccojocar/zxcvbn-go v1.0.2 // indirect + github.com/charithe/durationcheck v0.0.10 // indirect + github.com/charmbracelet/glamour v0.3.0 // indirect + github.com/chavacava/garif v0.1.0 // indirect + github.com/ckaznocha/intrange v0.3.0 // indirect + github.com/curioswitch/go-reassign v0.3.0 // indirect + github.com/daixiang0/gci v0.13.6 // indirect + github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/denis-tingaikin/go-header v0.5.0 // indirect + github.com/dennwc/varint v1.0.0 // indirect + github.com/dlclark/regexp2 v1.11.0 // indirect + github.com/edsrzf/mmap-go v1.2.0 // indirect + github.com/efficientgo/tools/core v0.0.0-20210609125236-d73259166f20 // indirect + github.com/efficientgo/tools/extkingpin v0.0.0-20210609125236-d73259166f20 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.32.3 // indirect + github.com/ettle/strcase v0.2.0 // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/fatih/faillint v1.15.0 // indirect + github.com/fatih/structtag v1.2.0 // indirect + github.com/felixge/fgprof v0.9.1 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/firefart/nonamedreturns v1.0.5 // indirect + github.com/fsnotify/fsnotify v1.8.0 // indirect + github.com/fzipp/gocyclo v0.6.0 // indirect + github.com/ghostiam/protogetter v0.3.9 // indirect + github.com/go-critic/go-critic v0.12.0 // indirect + github.com/go-kit/kit v0.10.0 // indirect + github.com/go-logfmt/logfmt v0.6.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-toolsmith/astcast v1.1.0 // indirect + github.com/go-toolsmith/astcopy v1.1.0 // indirect + github.com/go-toolsmith/astequal v1.2.0 // indirect + github.com/go-toolsmith/astfmt v1.1.0 // indirect + github.com/go-toolsmith/astp v1.1.0 // indirect + github.com/go-toolsmith/strparse v1.1.0 // indirect + github.com/go-toolsmith/typep v1.1.0 // indirect + github.com/go-viper/mapstructure/v2 v2.2.1 // indirect + github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect + github.com/gobwas/glob v0.2.3 // indirect + github.com/gocolly/colly/v2 v2.1.1-0.20201013153555-8252c346cfb0 // indirect + github.com/gofrs/flock v0.12.1 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/gohugoio/hugo v0.74.3 // indirect + github.com/golang-jwt/jwt/v5 v5.2.1 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 // indirect + github.com/golangci/go-printf-func-name v0.1.0 // indirect + github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d // indirect + github.com/golangci/golangci-lint v1.64.8 // indirect + github.com/golangci/misspell v0.6.0 // indirect + github.com/golangci/plugin-module-register v0.1.1 // indirect + github.com/golangci/revgrep v0.8.0 // indirect + github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed // indirect + github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect + github.com/google/s2a-go v0.1.9 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect + github.com/googleapis/gax-go/v2 v2.14.1 // indirect + github.com/gordonklaus/ineffassign v0.1.0 // indirect + github.com/gorilla/css v1.0.1 // indirect + github.com/gostaticanalysis/analysisutil v0.7.1 // indirect + github.com/gostaticanalysis/comment v1.5.0 // indirect + github.com/gostaticanalysis/forcetypeassert v0.2.0 // indirect + github.com/gostaticanalysis/nilerr v0.1.1 // indirect + github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect + github.com/hashicorp/go-immutable-radix/v2 v2.1.0 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/hexops/gotextdiff v1.0.3 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jgautheron/goconst v1.7.1 // indirect + github.com/jingyugao/rowserrcheck v1.1.1 // indirect + github.com/jjti/go-spancheck v0.6.4 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/jpillora/backoff v1.0.0 // indirect + github.com/julz/importas v0.2.0 // indirect + github.com/karamaru-alpha/copyloopvar v1.2.1 // indirect + github.com/kennygrant/sanitize v1.2.4 // indirect + github.com/kisielk/errcheck v1.9.0 // indirect + github.com/kkHAIKE/contextcheck v1.1.6 // indirect + github.com/klauspost/compress v1.17.11 // indirect + github.com/kulti/thelper v0.6.3 // indirect + github.com/kunwardeep/paralleltest v1.0.10 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect + github.com/lasiar/canonicalheader v1.1.2 // indirect + github.com/ldez/exptostd v0.4.2 // indirect + github.com/ldez/gomoddirectives v0.6.1 // indirect + github.com/ldez/grignotin v0.9.0 // indirect + github.com/ldez/tagliatelle v0.7.1 // indirect + github.com/ldez/usetesting v0.4.2 // indirect + github.com/leonklingele/grouper v1.1.2 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/macabu/inamedparam v0.1.3 // indirect + github.com/magiconair/properties v1.8.6 // indirect + github.com/maratori/testableexamples v1.0.0 // indirect + github.com/maratori/testpackage v1.1.1 // indirect + github.com/matoous/godox v1.1.0 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mattn/go-shellwords v1.0.10 // indirect + github.com/mgechev/revive v1.7.0 // indirect + github.com/microcosm-cc/bluemonday v1.0.27 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/moricho/tparallel v0.3.2 // indirect + github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68 // indirect + github.com/muesli/termenv v0.8.1 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect + github.com/nakabonne/nestif v0.3.1 // indirect + github.com/niklasfasching/go-org v1.3.0 // indirect + github.com/nishanths/exhaustive v0.12.0 // indirect + github.com/nishanths/predeclared v0.2.2 // indirect + github.com/nunnatsa/ginkgolinter v0.19.1 // indirect + github.com/oklog/run v1.1.0 // indirect + github.com/oklog/ulid v1.3.1 // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/polyfloyd/go-errorlint v1.7.1 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/prometheus/sigv4 v0.1.1 // indirect + github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1 // indirect + github.com/quasilyte/go-ruleguard/dsl v0.3.22 // indirect + github.com/quasilyte/gogrep v0.5.0 // indirect + github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect + github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect + github.com/raeperd/recvcheck v0.2.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/ryancurrah/gomodguard v1.3.5 // indirect + github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect + github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca // indirect + github.com/sanposhiho/wastedassign/v2 v2.1.0 // indirect + github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 // indirect + github.com/sashamelentyev/interfacebloat v1.1.0 // indirect + github.com/sashamelentyev/usestdlibvars v1.28.0 // indirect + github.com/securego/gosec/v2 v2.22.2 // indirect + github.com/sergi/go-diff v1.0.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/sivchari/containedctx v1.0.3 // indirect + github.com/sivchari/tenv v1.12.1 // indirect + github.com/sonatard/noctx v0.1.0 // indirect + github.com/sourcegraph/go-diff v0.7.0 // indirect + github.com/spf13/afero v1.12.0 // indirect + github.com/spf13/cast v1.5.0 // indirect + github.com/spf13/cobra v1.9.1 // indirect + github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/spf13/pflag v1.0.6 // indirect + github.com/spf13/viper v1.12.0 // indirect + github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect + github.com/stbenjam/no-sprintf-host-port v0.2.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect + github.com/subosito/gotenv v1.4.1 // indirect + github.com/tdakkota/asciicheck v0.4.1 // indirect + github.com/temoto/robotstxt v1.1.1 // indirect + github.com/tetafro/godot v1.5.0 // indirect + github.com/theckman/yacspin v0.8.0 // indirect + github.com/timakin/bodyclose v0.0.0-20241017074812-ed6a65f985e3 // indirect + github.com/timonwong/loggercheck v0.10.1 // indirect + github.com/tomarrell/wrapcheck/v2 v2.10.0 // indirect + github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect + github.com/ultraware/funlen v0.2.0 // indirect + github.com/ultraware/whitespace v0.2.0 // indirect + github.com/uudashr/gocognit v1.2.0 // indirect + github.com/uudashr/iface v1.3.1 // indirect + github.com/xen0n/gosmopolitan v1.2.2 // indirect + github.com/yagipy/maintidx v1.0.0 // indirect + github.com/yeya24/promlinter v0.3.0 // indirect + github.com/ykadowak/zerologlint v0.1.5 // indirect + github.com/yuin/goldmark v1.4.13 // indirect + github.com/yuin/goldmark-emoji v1.0.1 // indirect + gitlab.com/bosi/decorder v0.4.2 // indirect + go-simpler.org/musttag v0.13.0 // indirect + go-simpler.org/sloglint v0.9.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect + go.opentelemetry.io/otel v1.34.0 // indirect + go.opentelemetry.io/otel/metric v1.34.0 // indirect + go.opentelemetry.io/otel/trace v1.34.0 // indirect + go.uber.org/atomic v1.11.0 // indirect + go.uber.org/automaxprocs v1.6.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + golang.org/x/crypto v0.39.0 // indirect + golang.org/x/exp/typeparams v0.0.0-20250218142911-aa4b98e5adaa // indirect + golang.org/x/mod v0.25.0 // indirect + golang.org/x/net v0.41.0 // indirect + golang.org/x/oauth2 v0.30.0 // indirect + golang.org/x/perf v0.0.0-20250605212013-b481878a17be // indirect + golang.org/x/sync v0.15.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/telemetry v0.0.0-20250417124945-06ef541f3fa3 // indirect + golang.org/x/text v0.26.0 // indirect + golang.org/x/time v0.10.0 // indirect + golang.org/x/tools/gopls v0.19.0-pre.2 // indirect + google.golang.org/api v0.223.0 // indirect + google.golang.org/appengine v1.6.8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2 // indirect + google.golang.org/grpc v1.70.0 // indirect + google.golang.org/protobuf v1.36.5 // indirect + gopkg.in/alecthomas/kingpin.v2 v2.2.6 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + honnef.co/go/tools v0.7.0-0.dev.0.20250523013057-bbc2f4dd71ea // indirect + k8s.io/apimachinery v0.31.3 // indirect + k8s.io/client-go v0.31.3 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect + mvdan.cc/gofumpt v0.7.0 // indirect + mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f // indirect +) diff --git a/internal/promql-engine/go.tools.sum b/internal/promql-engine/go.tools.sum new file mode 100644 index 00000000000..64c1026367f --- /dev/null +++ b/internal/promql-engine/go.tools.sum @@ -0,0 +1,1756 @@ +4d63.com/gocheckcompilerdirectives v1.3.0 h1:Ew5y5CtcAAQeTVKUVFrE7EwHMrTO6BggtEj8BZSjZ3A= +4d63.com/gocheckcompilerdirectives v1.3.0/go.mod h1:ofsJ4zx2QAuIP/NO/NAh1ig6R1Fb18/GI7RVMwz7kAY= +4d63.com/gochecknoglobals v0.2.2 h1:H1vdnwnMaZdQW/N+NrkT1SZMTBmcwHe9Vq8lJcYYTtU= +4d63.com/gochecknoglobals v0.2.2/go.mod h1:lLxwTQjL5eIesRbvnzIP3jZtG140FnTdz+AlMa+ogt0= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7hw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.39.0/go.mod h1:rVLT6fkc8chs9sfPtFc1SBH6em7n+ZoXaG+87tDISts= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go/auth v0.15.0 h1:Ly0u4aA5vG/fsSsxu98qCQBemXtAtJf+95z9HK+cxps= +cloud.google.com/go/auth v0.15.0/go.mod h1:WJDGqZ1o9E9wKIL+IwStfyn/+s59zl4Bi+1KQNVXLZ8= +cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M= +cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= +cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +contrib.go.opencensus.io/exporter/aws v0.0.0-20181029163544-2befc13012d0/go.mod h1:uu1P0UCM/6RbsMrgPa98ll8ZcHM858i/AD06a9aLRCA= +contrib.go.opencensus.io/exporter/ocagent v0.4.12/go.mod h1:450APlNTSR6FrvC3CTRqYosuDstRB9un7SOx2k/9ckA= +contrib.go.opencensus.io/exporter/stackdriver v0.11.0/go.mod h1:hA7rlmtavV03FGxzWXAPBUnZeZBhWN/QYQAuMtxc9Bk= +contrib.go.opencensus.io/integrations/ocsql v0.1.4/go.mod h1:8DsSdjz3F+APR+0z0WkU1aRorQCFfRxvqjUUPMbF3fE= +contrib.go.opencensus.io/resource v0.0.0-20190131005048-21591786a5e0/go.mod h1:F361eGI91LCmW1I/Saf+rX0+OFcigGlFvXwEGEnkRLA= +dmitri.shuralyov.com/go/generated v0.0.0-20170818220700-b1254a446363 h1:o4lAkfETerCnr1kF9/qwkwjICnU+YLHNDCM8h2xj7as= +dmitri.shuralyov.com/go/generated v0.0.0-20170818220700-b1254a446363/go.mod h1:WG7q7swWsS2f9PYpt5DoEP/EBYWx8We5UoRltn9vJl8= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/4meepo/tagalign v1.4.2 h1:0hcLHPGMjDyM1gHG58cS73aQF8J4TdVR96TZViorO9E= +github.com/4meepo/tagalign v1.4.2/go.mod h1:+p4aMyFM+ra7nb41CnFG6aSDXqRxU/w1VQqScKqDARI= +github.com/Abirdcfly/dupword v0.1.3 h1:9Pa1NuAsZvpFPi9Pqkd93I7LIYRURj+A//dFd5tgBeE= +github.com/Abirdcfly/dupword v0.1.3/go.mod h1:8VbB2t7e10KRNdwTVoxdBaxla6avbhGzb8sCTygUMhw= +github.com/Antonboom/errname v1.0.0 h1:oJOOWR07vS1kRusl6YRSlat7HFnb3mSfMl6sDMRoTBA= +github.com/Antonboom/errname v1.0.0/go.mod h1:gMOBFzK/vrTiXN9Oh+HFs+e6Ndl0eTFbtsRTSRdXyGI= +github.com/Antonboom/nilnil v1.0.1 h1:C3Tkm0KUxgfO4Duk3PM+ztPncTFlOf0b2qadmS0s4xs= +github.com/Antonboom/nilnil v1.0.1/go.mod h1:CH7pW2JsRNFgEh8B2UaPZTEPhCMuFowP/e8Udp9Nnb0= +github.com/Antonboom/testifylint v1.5.2 h1:4s3Xhuv5AvdIgbd8wOOEeo0uZG7PbDKQyKY5lGoQazk= +github.com/Antonboom/testifylint v1.5.2/go.mod h1:vxy8VJ0bc6NavlYqjZfmp6EfqXMtBgQ4+mhCojwC1P8= +github.com/Azure/azure-amqp-common-go v1.1.3/go.mod h1:FhZtXirFANw40UXI2ntweO+VOkfaw8s6vZxUiRhLYW8= +github.com/Azure/azure-amqp-common-go v1.1.4/go.mod h1:FhZtXirFANw40UXI2ntweO+VOkfaw8s6vZxUiRhLYW8= +github.com/Azure/azure-pipeline-go v0.1.8/go.mod h1:XA1kFWRVhSK+KNFiOhfv83Fv8L9achrP7OxIzeTn1Yg= +github.com/Azure/azure-pipeline-go v0.1.9/go.mod h1:XA1kFWRVhSK+KNFiOhfv83Fv8L9achrP7OxIzeTn1Yg= +github.com/Azure/azure-sdk-for-go v21.3.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/azure-sdk-for-go v27.3.0+incompatible h1:i+ROfG3CsZUPoVAnhK06T3R6PmBzKB9ds+lHBpN7Mzo= +github.com/Azure/azure-sdk-for-go v27.3.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 h1:g0EZJwz7xkXQiZAI5xi9f3WWFYBlX1CPTrR+NDToRkQ= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0/go.mod h1:XCW7KnZet0Opnr7HccfUw1PLc4CjHqpcaxW8DHklNkQ= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.1 h1:1mvYtZfWQAnwNah/C+Z+Jb9rQH95LPE2vlmMuWAHJk8= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.1/go.mod h1:75I/mXtme1JyWFtz8GocPHVFyH421IBoZErnO16dd0k= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.1 h1:Bk5uOhSAenHyR5P61D/NzeQCv+4fEVV8mOkJ82NqpWw= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.1/go.mod h1:QZ4pw3or1WPmRBxf0cHd1tknzrT54WPBOQoGutCPvSU= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5 v5.7.0 h1:LkHbJbgF3YyvC53aqYGR+wWQDn2Rdp9AQdGndf9QvY4= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5 v5.7.0/go.mod h1:QyiQdW4f4/BIfB8ZutZ2s+28RAgfa/pT+zS++ZHyM1I= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v4 v4.3.0 h1:bXwSugBiSbgtz7rOtbfGf+woewp4f06orW9OP5BjHLA= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v4 v4.3.0/go.mod h1:Y/HgrePTmGy9HjdSGTqZNa+apUpTVIEVKXJyARP2lrk= +github.com/Azure/azure-service-bus-go v0.4.1/go.mod h1:d9ho9e/06euiTwGpKxmlbpPhFUsfCsq6a4tZ68r51qI= +github.com/Azure/azure-storage-blob-go v0.6.0/go.mod h1:oGfmITT1V6x//CswqY2gtAHND+xIP64/qL7a5QJix0Y= +github.com/Azure/go-autorest v11.0.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest v11.1.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest v11.1.2+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest/tracing v0.1.0/go.mod h1:ROEEAFwXycQw7Sn3DXNtEedEvdeRAgDr0izn4z5Ij88= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= +github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 h1:kYRSnvJju5gYVyhkij+RTJ/VR6QIUaCfWeaFm2ycsjQ= +github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69/go.mod h1:L1AbZdiDllfyYH5l5OkAaZtk7VkWe89bPJFmnDBNHxg= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs= +github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Code-Hex/go-generics-cache v1.5.1 h1:6vhZGc5M7Y/YD8cIUcY8kcuQLB4cHR7U+0KMqAA0KcU= +github.com/Code-Hex/go-generics-cache v1.5.1/go.mod h1:qxcC9kRVrct9rHeiYpFWSoW1vxyillCVzX13KZG8dl4= +github.com/Crocmagnon/fatcontext v0.7.1 h1:SC/VIbRRZQeQWj/TcQBS6JmrXcfA+BU4OGSVUt54PjM= +github.com/Crocmagnon/fatcontext v0.7.1/go.mod h1:1wMvv3NXEBJucFGfwOJBxSVWcoIO6emV215SMkW9MFU= +github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 h1:sHglBQTwgx+rWPdisA5ynNEsoARbiCBOyGcJM4/OzsM= +github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs= +github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.1 h1:Sz1JIXEcSfhz7fUi7xHnhpIE0thVASYjvosApmHuD2k= +github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.1/go.mod h1:n/LSCXNuIYqVfBlVXyHfMQkZDdp1/mmxfSjADd3z1Zg= +github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0= +github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0= +github.com/GoogleCloudPlatform/cloudsql-proxy v0.0.0-20190418212003-6ac0b49e7197/go.mod h1:aJ4qN3TfrelA6NZ6AXsXRfmEVaYin3EDbSPJrKS8OXo= +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= +github.com/Kunde21/markdownfmt/v2 v2.1.1-0.20210810103848-727f02f4c51c h1:rnouiLtDKeaWKnxRViK454oCI8jkhWv5fItCvZ9nJOU= +github.com/Kunde21/markdownfmt/v2 v2.1.1-0.20210810103848-727f02f4c51c/go.mod h1:LFJueuHZej/Z7Xhqh/XgClfkDjZiiEBOLVTt1Duq1r0= +github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= +github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/OpenPeeDeeP/depguard/v2 v2.2.1 h1:vckeWVESWp6Qog7UZSARNqfu/cZqvki8zsuj3piCMx4= +github.com/OpenPeeDeeP/depguard/v2 v2.2.1/go.mod h1:q4DKzC4UcVaAvcfd41CZh0PWpGgzrVxUYBlgKNGquUo= +github.com/PuerkitoBio/goquery v1.5.1 h1:PSPBGne8NIUWw+/7vFBV+kG2J/5MOjbzc7154OaKCSE= +github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= +github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= +github.com/aclements/go-moremath v0.0.0-20210112150236-f10218a38794 h1:xlwdaKcTNVW4PtpQb8aKA4Pjy0CdJHEqvFbAnvR5m2g= +github.com/aclements/go-moremath v0.0.0-20210112150236-f10218a38794/go.mod h1:7e+I0LQFUI9AXWxOfsQROs9xPhoJtbsyWcjJqDd4KPY= +github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= +github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= +github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U= +github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI= +github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= +github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/chroma v0.7.2-0.20200305040604-4f3623dce67a/go.mod h1:fv5SzZPFJbwp2NXJWpFIX7DZS4HgV1K4ew4Pc2OZD9s= +github.com/alecthomas/chroma v0.7.3/go.mod h1:sko8vR34/90zvl5QdcUdvzL3J8NKjAUx9va9jPuFNoM= +github.com/alecthomas/chroma v0.8.2 h1:x3zkuE2lUk/RIekyAJ3XRqSCP4zwWDfcw/YJCuCAACg= +github.com/alecthomas/chroma v0.8.2/go.mod h1:sko8vR34/90zvl5QdcUdvzL3J8NKjAUx9va9jPuFNoM= +github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 h1:JHZL0hZKJ1VENNfmXvHbgYlbUOvpzYzvy2aZU5gXVeo= +github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0= +github.com/alecthomas/go-check-sumtype v0.3.1 h1:u9aUvbGINJxLVXiFvHUlPEaD7VDULsrxJb4Aq31NLkU= +github.com/alecthomas/go-check-sumtype v0.3.1/go.mod h1:A8TSiN3UPRw3laIgWEUOHHLPa6/r9MtoigdlP5h3K/E= +github.com/alecthomas/kong v0.1.17-0.20190424132513-439c674f7ae0/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI= +github.com/alecthomas/kong v0.2.1-0.20190708041108-0548c6b1afae/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI= +github.com/alecthomas/kong v0.2.4/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE= +github.com/alecthomas/kong-hcl v0.1.8-0.20190615233001-b21fea9723c8/go.mod h1:MRgZdU3vrFd05IQ89AxUZ0aYdF39BYoNFa324SodPCA= +github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= +github.com/alecthomas/repr v0.0.0-20181024024818-d37bc2a10ba1/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= +github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= +github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= +github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0= +github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs= +github.com/alexkohler/nakedret/v2 v2.0.5 h1:fP5qLgtwbx9EJE8dGEERT02YwS8En4r9nnZ71RK+EVU= +github.com/alexkohler/nakedret/v2 v2.0.5/go.mod h1:bF5i0zF2Wo2o4X4USt9ntUWve6JbFv02Ff4vlkmS/VU= +github.com/alexkohler/prealloc v1.0.0 h1:Hbq0/3fJPQhNkN0dR95AVrr6R7tou91y0uHG5pOcUuw= +github.com/alexkohler/prealloc v1.0.0/go.mod h1:VetnK3dIgFBBKmg0YnD9F9x6Icjd+9cvfHR56wJVlKE= +github.com/alingse/asasalint v0.0.11 h1:SFwnQXJ49Kx/1GghOFz1XGqHYKp21Kq1nHad/0WQRnw= +github.com/alingse/asasalint v0.0.11/go.mod h1:nCaoMhw7a9kSJObvQyVzNTPBDbNpdocqrSP7t/cW5+I= +github.com/alingse/nilnesserr v0.1.2 h1:Yf8Iwm3z2hUUrP4muWfW83DF4nE3r1xZ26fGWUKCZlo= +github.com/alingse/nilnesserr v0.1.2/go.mod h1:1xJPrXonEtX7wyTq8Dytns5P2hNzoWymVUIaKm4HNFg= +github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= +github.com/andybalholm/cascadia v1.2.0 h1:vuRCkM5Ozh/BfmsaTm26kbjm0mIOM3yS5Ek/F5h18aE= +github.com/andybalholm/cascadia v1.2.0/go.mod h1:YCyR8vOZT9aZ1CHEd8ap0gMVm2aFgxBp0T0eFw1RUQY= +github.com/antchfx/htmlquery v1.2.3 h1:sP3NFDneHx2stfNXCKbhHFo8XgNjCACnU/4AO5gWz6M= +github.com/antchfx/htmlquery v1.2.3/go.mod h1:B0ABL+F5irhhMWg54ymEZinzMSi0Kt3I2if0BLYa3V0= +github.com/antchfx/xmlquery v1.2.4/go.mod h1:KQQuESaxSlqugE2ZBcM/qn+ebIpt+d+4Xx7YcSGAIrM= +github.com/antchfx/xmlquery v1.3.4 h1:RuhsI4AA5Ma4XoXhaAr2VjJxU0Xp0W2zy/f9ZIpsF4s= +github.com/antchfx/xmlquery v1.3.4/go.mod h1:64w0Xesg2sTaawIdNqMB+7qaW/bSqkQm+ssPaCMWNnc= +github.com/antchfx/xpath v1.1.6/go.mod h1:Yee4kTMuNiPYJ7nSNorELQMr1J33uOpXDMByNYhvtNk= +github.com/antchfx/xpath v1.1.8/go.mod h1:Yee4kTMuNiPYJ7nSNorELQMr1J33uOpXDMByNYhvtNk= +github.com/antchfx/xpath v1.1.10 h1:cJ0pOvEdN/WvYXxvRrzQH9x5QWKpzHacYO8qzCcDYAg= +github.com/antchfx/xpath v1.1.10/go.mod h1:Yee4kTMuNiPYJ7nSNorELQMr1J33uOpXDMByNYhvtNk= +github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA= +github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= +github.com/ashanbrown/forbidigo v1.6.0 h1:D3aewfM37Yb3pxHujIPSpTf6oQk9sc9WZi8gerOIVIY= +github.com/ashanbrown/forbidigo v1.6.0/go.mod h1:Y8j9jy9ZYAEHXdu723cUlraTqbzjKF1MUyfOKL+AjcU= +github.com/ashanbrown/makezero v1.2.0 h1:/2Lp1bypdmK9wDIq7uWBlDF1iMUpIIS4A+pF6C9IEUU= +github.com/ashanbrown/makezero v1.2.0/go.mod h1:dxlPhHbDMC6N6xICzFBSK+4njQDdK8euNO0qjQMtGY4= +github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= +github.com/aws/aws-sdk-go v1.15.27/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= +github.com/aws/aws-sdk-go v1.18.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go v1.19.16/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go v1.27.1/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go v1.55.6 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk= +github.com/aws/aws-sdk-go v1.55.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= +github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 h1:6df1vn4bBlDDo4tARvBm7l6KA9iVMnE3NWizDeWSrps= +github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3/go.mod h1:CIWtjkly68+yqLPbvwwR/fjNJA/idrtULjZWh2v1ys0= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bep/debounce v1.2.0/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= +github.com/bep/gitmap v1.1.2/go.mod h1:g9VRETxFUXNWzMiuxOwcudo6DfZkW9jOsOW0Ft4kYaY= +github.com/bep/golibsass v0.6.0/go.mod h1:DL87K8Un/+pWUS75ggYv41bliGiolxzDKWJAq3eJ1MA= +github.com/bep/tmc v0.5.1/go.mod h1:tGYHN8fS85aJPhDLgXETVKp+PR382OvFi2+q2GkGsq0= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bkielbasa/cyclop v1.2.3 h1:faIVMIGDIANuGPWH031CZJTi2ymOQBULs9H21HSMa5w= +github.com/bkielbasa/cyclop v1.2.3/go.mod h1:kHTwA9Q0uZqOADdupvcFJQtp/ksSnytRMe8ztxG8Fuo= +github.com/blizzy78/varnamelen v0.8.0 h1:oqSblyuQvFsW1hbBHh1zfwrKe3kcSj0rnXkKzsQ089M= +github.com/blizzy78/varnamelen v0.8.0/go.mod h1:V9TzQZ4fLJ1DSrjVDfl89H7aMnTvKkApdHeyESmyR7k= +github.com/bombsimon/wsl/v4 v4.5.0 h1:iZRsEvDdyhd2La0FVi5k6tYehpOR/R7qIUjmKk7N74A= +github.com/bombsimon/wsl/v4 v4.5.0/go.mod h1:NOQ3aLF4nD7N5YPXMruR6ZXDOAqLoM0GEpLwTdvmOSc= +github.com/breml/bidichk v0.3.2 h1:xV4flJ9V5xWTqxL+/PMFF6dtJPvZLPsyixAoPe8BGJs= +github.com/breml/bidichk v0.3.2/go.mod h1:VzFLBxuYtT23z5+iVkamXO386OB+/sVwZOpIj6zXGos= +github.com/breml/errchkjson v0.4.0 h1:gftf6uWZMtIa/Is3XJgibewBm2ksAQSY/kABDNFTAdk= +github.com/breml/errchkjson v0.4.0/go.mod h1:AuBOSTHyLSaaAFlWsRSuRBIroCh3eh7ZHh5YeelDIk8= +github.com/butuzov/ireturn v0.3.1 h1:mFgbEI6m+9W8oP/oDdfA34dLisRFCj2G6o/yiI1yZrY= +github.com/butuzov/ireturn v0.3.1/go.mod h1:ZfRp+E7eJLC0NQmk1Nrm1LOrn/gQlOykv+cVPdiXH5M= +github.com/butuzov/mirror v1.3.0 h1:HdWCXzmwlQHdVhwvsfBb2Au0r3HyINry3bDWLYXiKoc= +github.com/butuzov/mirror v1.3.0/go.mod h1:AEij0Z8YMALaq4yQj9CPPVYOyJQyiexpQEQgihajRfI= +github.com/bwplotka/mdox v0.9.0 h1:NRrJNWGDUjA2dWeWiJv/4WNQZp5+OcVYIHz94T6ksxs= +github.com/bwplotka/mdox v0.9.0/go.mod h1:d9tSrvKyHqgb/IuvvhncvLzbV1SfSfhH56ct1TOYncw= +github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= +github.com/catenacyber/perfsprint v0.8.2 h1:+o9zVmCSVa7M4MvabsWvESEhpsMkhfE7k0sHNGL95yw= +github.com/catenacyber/perfsprint v0.8.2/go.mod h1:q//VWC2fWbcdSLEY1R3l8n0zQCDPdE4IjZwyY1HMunM= +github.com/ccojocar/zxcvbn-go v1.0.2 h1:na/czXU8RrhXO4EZme6eQJLR4PzcGsahsBOAwU6I3Vg= +github.com/ccojocar/zxcvbn-go v1.0.2/go.mod h1:g1qkXtUSvHP8lhHp5GrSmTz6uWALGRMQdw6Qnz/hi60= +github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/charithe/durationcheck v0.0.10 h1:wgw73BiocdBDQPik+zcEoBG/ob8uyBHf2iyoHGPf5w4= +github.com/charithe/durationcheck v0.0.10/go.mod h1:bCWXb7gYRysD1CU3C+u4ceO49LoGOY1C1L6uouGNreQ= +github.com/charmbracelet/glamour v0.3.0 h1:3H+ZrKlSg8s+WU6V7eF2eRVYt8lCueffbi7r2+ffGkc= +github.com/charmbracelet/glamour v0.3.0/go.mod h1:TzF0koPZhqq0YVBNL100cPHznAAjVj7fksX2RInwjGw= +github.com/chavacava/garif v0.1.0 h1:2JHa3hbYf5D9dsgseMKAmc/MZ109otzgNFk5s87H9Pc= +github.com/chavacava/garif v0.1.0/go.mod h1:XMyYCkEL58DF0oyW4qDjjnPWONs2HBqYKI+UIPD+Gww= +github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927/go.mod h1:h/aW8ynjgkuj+NQRlZcDbAbM1ORAbXjXX77sX7T289U= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/ckaznocha/intrange v0.3.0 h1:VqnxtK32pxgkhJgYQEeOArVidIPg+ahLP7WBOXZd5ZY= +github.com/ckaznocha/intrange v0.3.0/go.mod h1:+I/o2d2A1FBHgGELbGxzIcyd3/9l9DuwjM8FsbSS3Lo= +github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 h1:QVw89YDxXxEe+l8gU8ETbOasdwEV+avkR75ZzsVV9WI= +github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cortexproject/promqlsmith v0.0.0-20250203072244-cbb5738d00ca h1:TvKuPFRUQ39O07xv3b+TO6GBRhchYKyBCCXrlmmDE3Y= +github.com/cortexproject/promqlsmith v0.0.0-20250203072244-cbb5738d00ca/go.mod h1:xbYQa0KX6Eh6YWbTBfZ9kK3N4hRxX+ZPIfVIY2U/y00= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/curioswitch/go-reassign v0.3.0 h1:dh3kpQHuADL3cobV/sSGETA8DOv457dwl+fbBAhrQPs= +github.com/curioswitch/go-reassign v0.3.0/go.mod h1:nApPCCTtqLJN/s8HfItCcKV0jIPwluBOvZP+dsJGA88= +github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E= +github.com/daixiang0/gci v0.13.6 h1:RKuEOSkGpSadkGbvZ6hJ4ddItT3cVZ9Vn9Rybk6xjl8= +github.com/daixiang0/gci v0.13.6/go.mod h1:12etP2OniiIdP4q+kjUGrC/rUagga7ODbqsom5Eo5Yk= +github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ= +github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk= +github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/denis-tingaikin/go-header v0.5.0 h1:SRdnP5ZKvcO9KKRP1KJrhFR3RrlGuD+42t4429eC9k8= +github.com/denis-tingaikin/go-header v0.5.0/go.mod h1:mMenU5bWrok6Wl2UsZjy+1okegmwQ3UgWl4V1D8gjlY= +github.com/dennwc/varint v1.0.0 h1:kGNFFSSw8ToIy3obO/kKr8U9GZYUAxQEVuix4zfDWzE= +github.com/dennwc/varint v1.0.0/go.mod h1:hnItb35rvZvJrbTALZtY/iQfDs48JKRG1RPpgziApxA= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/digitalocean/godo v1.132.0 h1:n0x6+ZkwbyQBtIU1wwBhv26EINqHg0wWQiBXlwYg/HQ= +github.com/digitalocean/godo v1.132.0/go.mod h1:PU8JB6I1XYkQIdHFop8lLAY9ojp6M0XcU0TWaQSxbrc= +github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8= +github.com/disintegration/gift v1.2.1/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI= +github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= +github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/dlclark/regexp2 v1.1.6/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= +github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/docker/docker v27.4.1+incompatible h1:ZJvcY7gfwHn1JF48PfbyXg7Jyt9ZCWDW+GGXOIxEwp4= +github.com/docker/docker v27.4.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= +github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/edsrzf/mmap-go v1.2.0 h1:hXLYlkbaPzt1SaQk+anYwKSRNhufIDCchSPkUD6dD84= +github.com/edsrzf/mmap-go v1.2.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q= +github.com/efficientgo/core v1.0.0-rc.3 h1:X6CdgycYWDcbYiJr1H1+lQGzx13o7bq3EUkbB9DsSPc= +github.com/efficientgo/core v1.0.0-rc.3/go.mod h1:FfGdkzWarkuzOlY04VY+bGfb1lWrjaL6x/GLcQ4vJps= +github.com/efficientgo/tools/core v0.0.0-20210609125236-d73259166f20 h1:3wGt9Bx/h2D/0yOlYIhWB7GmsFBPG9vWDg90UUGgn8c= +github.com/efficientgo/tools/core v0.0.0-20210609125236-d73259166f20/go.mod h1:OmVcnJopJL8d3X3sSXTiypGoUSgFq1aDGmlrdi9dn/M= +github.com/efficientgo/tools/extkingpin v0.0.0-20210609125236-d73259166f20 h1:kM/ALyvAnTrwSB+nlKqoKaDnZbInp1YImZvW+gtHwc8= +github.com/efficientgo/tools/extkingpin v0.0.0-20210609125236-d73259166f20/go.mod h1:ZV0utlglOczUWv3ih2AbqPSoLoFzdplUYxwV62eZi6Q= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane/envoy v1.32.3 h1:hVEaommgvzTjTd4xCaFd+kEQ2iYBtGxP6luyLrx6uOk= +github.com/envoyproxy/go-control-plane/envoy v1.32.3/go.mod h1:F6hWupPfh75TBXGKA++MCT/CZHFq5r9/uwt/kQYkZfE= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= +github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= +github.com/ettle/strcase v0.2.0 h1:fGNiVF21fHXpX1niBgk0aROov1LagYsOwV/xqKDKR/Q= +github.com/ettle/strcase v0.2.0/go.mod h1:DajmHElDSaX76ITe3/VHVyMin4LWSJN5Z909Wp+ED1A= +github.com/evanw/esbuild v0.6.5/go.mod h1:mptxmSXIzBIKKCe4jo9A5SToEd1G+AKZ9JmY85dYRJ0= +github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb h1:IT4JYU7k4ikYg1SCxNI1/Tieq/NFvh6dzLdgi7eu0tM= +github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb/go.mod h1:bH6Xx7IW64qjjJq8M2u4dxNaBiDfKK+z/3eGDpXEQhc= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/fatih/faillint v1.15.0 h1:GBsqRgd/uPvo7AdunUoM4oEBVCE7KCxz77J0f9IKr2w= +github.com/fatih/faillint v1.15.0/go.mod h1:xg4qfUwfXviSkDlZqJ7cDaf0JyOgR2Hx+a977rJt5Rc= +github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= +github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= +github.com/felixge/fgprof v0.9.1 h1:E6FUJ2Mlv043ipLOCFqo8+cHo9MhQ203E2cdEK/isEs= +github.com/felixge/fgprof v0.9.1/go.mod h1:7/HK6JFtFaARhIljgP2IV8rJLIoHDoOYoUphsnGvqxE= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/firefart/nonamedreturns v1.0.5 h1:tM+Me2ZaXs8tfdDw3X6DOX++wMCOqzYUho6tUTYIdRA= +github.com/firefart/nonamedreturns v1.0.5/go.mod h1:gHJjDqhGM4WyPt639SOZs+G89Ko7QKH5R5BhnO6xJhw= +github.com/fortytw2/leaktest v1.2.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= +github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= +github.com/frankban/quicktest v1.4.1/go.mod h1:36zfPVQyHxymz4cH7wlDmVwDrJuljRB60qkgn7rorfQ= +github.com/frankban/quicktest v1.7.2/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o= +github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo= +github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA= +github.com/getkin/kin-openapi v0.14.0/go.mod h1:WGRs2ZMM1Q8LR1QBEwUxC6RJEfaBcD0s+pcEVXFuAjw= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/ghostiam/protogetter v0.3.9 h1:j+zlLLWzqLay22Cz/aYwTHKQ88GE2DQ6GkWSYFOI4lQ= +github.com/ghostiam/protogetter v0.3.9/go.mod h1:WZ0nw9pfzsgxuRsPOFQomgDVSWtDLJRfQJEhsGbmQMA= +github.com/go-critic/go-critic v0.12.0 h1:iLosHZuye812wnkEz1Xu3aBwn5ocCPfc9yqmFG9pa6w= +github.com/go-critic/go-critic v0.12.0/go.mod h1:DpE0P6OVc6JzVYzmM5gq5jMU31zLr4am5mB/VfFK64w= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.10.0 h1:dXFJfIHVvUcpSgDOV+Ne6t7jXri8Tfv2uOLHUZ2XNuo= +github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= +github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= +github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= +github.com/go-resty/resty/v2 v2.16.3 h1:zacNT7lt4b8M/io2Ahj6yPypL7bqx9n1iprfQuodV+E= +github.com/go-resty/resty/v2 v2.16.3/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/go-toolsmith/astcast v1.1.0 h1:+JN9xZV1A+Re+95pgnMgDboWNVnIMMQXwfBwLRPgSC8= +github.com/go-toolsmith/astcast v1.1.0/go.mod h1:qdcuFWeGGS2xX5bLM/c3U9lewg7+Zu4mr+xPwZIB4ZU= +github.com/go-toolsmith/astcopy v1.1.0 h1:YGwBN0WM+ekI/6SS6+52zLDEf8Yvp3n2seZITCUBt5s= +github.com/go-toolsmith/astcopy v1.1.0/go.mod h1:hXM6gan18VA1T/daUEHCFcYiW8Ai1tIwIzHY6srfEAw= +github.com/go-toolsmith/astequal v1.0.3/go.mod h1:9Ai4UglvtR+4up+bAD4+hCj7iTo4m/OXVTSLnCyTAx4= +github.com/go-toolsmith/astequal v1.1.0/go.mod h1:sedf7VIdCL22LD8qIvv7Nn9MuWJruQA/ysswh64lffQ= +github.com/go-toolsmith/astequal v1.2.0 h1:3Fs3CYZ1k9Vo4FzFhwwewC3CHISHDnVUPC4x0bI2+Cw= +github.com/go-toolsmith/astequal v1.2.0/go.mod h1:c8NZ3+kSFtFY/8lPso4v8LuJjdJiUFVnSuU3s0qrrDY= +github.com/go-toolsmith/astfmt v1.1.0 h1:iJVPDPp6/7AaeLJEruMsBUlOYCmvg0MoCfJprsOmcco= +github.com/go-toolsmith/astfmt v1.1.0/go.mod h1:OrcLlRwu0CuiIBp/8b5PYF9ktGVZUjlNMV634mhwuQ4= +github.com/go-toolsmith/astp v1.1.0 h1:dXPuCl6u2llURjdPLLDxJeZInAeZ0/eZwFJmqZMnpQA= +github.com/go-toolsmith/astp v1.1.0/go.mod h1:0T1xFGz9hicKs8Z5MfAqSUitoUYS30pDMsRVIDHs8CA= +github.com/go-toolsmith/pkgload v1.2.2 h1:0CtmHq/02QhxcF7E9N5LIFcYFsMR5rdovfqTtRKkgIk= +github.com/go-toolsmith/pkgload v1.2.2/go.mod h1:R2hxLNRKuAsiXCo2i5J6ZQPhnPMOVtU+f0arbFPWCus= +github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8= +github.com/go-toolsmith/strparse v1.1.0 h1:GAioeZUK9TGxnLS+qfdqNbA4z0SSm5zVNtCQiyP2Bvw= +github.com/go-toolsmith/strparse v1.1.0/go.mod h1:7ksGy58fsaQkGQlY8WVoBFNyEPMGuJin1rfoPS4lBSQ= +github.com/go-toolsmith/typep v1.1.0 h1:fIRYDyF+JywLfqzyhdiHzRop/GQDxxNhLGQ6gFUNHus= +github.com/go-toolsmith/typep v1.1.0/go.mod h1:fVIw+7zjdsMxDA3ITWnH1yOiw1rnTQKCsF/sk2H/qig= +github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= +github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-xmlfmt/xmlfmt v1.1.3 h1:t8Ey3Uy7jDSEisW2K3somuMKIpzktkWptA0iFCnRUWY= +github.com/go-xmlfmt/xmlfmt v1.1.3/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= +github.com/go-zookeeper/zk v1.0.4 h1:DPzxraQx7OrPyXq2phlGlNSIyWEsAox0RJmjTseMV6I= +github.com/go-zookeeper/zk v1.0.4/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/gocolly/colly v1.2.0/go.mod h1:Hof5T3ZswNVsOHYmba1u03W65HDWgpV5HifSuueE0EA= +github.com/gocolly/colly/v2 v2.1.1-0.20201013153555-8252c346cfb0 h1:f+kHjWsqjft+/nCpQ6TcV3Lgs4lc+6rvBt2sfL4XsbE= +github.com/gocolly/colly/v2 v2.1.1-0.20201013153555-8252c346cfb0/go.mod h1:I2MuhsLjQ+Ex+IzK3afNS8/1qP3AedHOusRPcRdC5o0= +github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= +github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/gohugoio/hugo v0.74.3 h1:fAMS4JYV+T9YiHj+be4mrYDFzNgx91UY+Gok/FdbSzU= +github.com/gohugoio/hugo v0.74.3/go.mod h1:qTy3SQXdyeRLfUMMdGZeySMGMzvi3D31prjuIbAwImk= +github.com/gohugoio/testmodBuilder/mods v0.0.0-20190520184928-c56af20f2e95/go.mod h1:bOlVlCa1/RajcHpXkrUXPSHB/Re1UnlXxD1Qp8SKOd8= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 h1:WUvBfQL6EW/40l6OmeSBYQJNSif4O11+bmWEz+C7FYw= +github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32/go.mod h1:NUw9Zr2Sy7+HxzdjIULge71wI6yEg1lWQr7Evcu8K0E= +github.com/golangci/go-printf-func-name v0.1.0 h1:dVokQP+NMTO7jwO4bwsRwLWeudOVUPPyAKJuzv8pEJU= +github.com/golangci/go-printf-func-name v0.1.0/go.mod h1:wqhWFH5mUdJQhweRnldEywnR5021wTdZSNgwYceV14s= +github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d h1:viFft9sS/dxoYY0aiOTsLKO2aZQAPT4nlQCsimGcSGE= +github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d/go.mod h1:ivJ9QDg0XucIkmwhzCDsqcnxxlDStoTl89jDMIoNxKY= +github.com/golangci/golangci-lint v1.64.8 h1:y5TdeVidMtBGG32zgSC7ZXTFNHrsJkDnpO4ItB3Am+I= +github.com/golangci/golangci-lint v1.64.8/go.mod h1:5cEsUQBSr6zi8XI8OjmcY2Xmliqc4iYL7YoPrL+zLJ4= +github.com/golangci/misspell v0.6.0 h1:JCle2HUTNWirNlDIAUO44hUsKhOFqGPoC4LZxlaSXDs= +github.com/golangci/misspell v0.6.0/go.mod h1:keMNyY6R9isGaSAu+4Q8NMBwMPkh15Gtc8UCVoDtAWo= +github.com/golangci/plugin-module-register v0.1.1 h1:TCmesur25LnyJkpsVrupv1Cdzo+2f7zX0H6Jkw1Ol6c= +github.com/golangci/plugin-module-register v0.1.1/go.mod h1:TTpqoB6KkwOJMV8u7+NyXMrkwwESJLOkfl9TxR1DGFc= +github.com/golangci/revgrep v0.8.0 h1:EZBctwbVd0aMeRnNUsFogoyayvKHyxlV3CdUA46FX2s= +github.com/golangci/revgrep v0.8.0/go.mod h1:U4R/s9dlXZsg8uJmaR1GrloUr14D7qDl8gi2iPXJH8k= +github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed h1:IURFTjxeTfNFP0hTEi1YKjB/ub8zkpaOqFFMApi2EAs= +github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed/go.mod h1:XLXN8bNw4CGRPaqgl3bv/lhz7bsGPh4/xSaMTbo2vkQ= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.2-0.20191028172631-481baca67f93/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian v2.1.1-0.20190517191504-25dcb96d9e51+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200615235658-03e1cf38a040/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg= +github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/wire v0.2.2/go.mod h1:7FHVg6mFpFQrjeUZrm+BaD50N5jnDKm50uVPTpyYOmU= +github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= +github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= +github.com/googleapis/gax-go v2.0.2+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= +github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= +github.com/gophercloud/gophercloud/v2 v2.4.0 h1:XhP5tVEH3ni66NSNK1+0iSO6kaGPH/6srtx6Cr+8eCg= +github.com/gophercloud/gophercloud/v2 v2.4.0/go.mod h1:uJWNpTgJPSl2gyzJqcU/pIAhFUWvIkp8eE8M15n9rs4= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gordonklaus/ineffassign v0.1.0 h1:y2Gd/9I7MdY1oEIt+n+rowjBNDcLQq3RsH5hwJd0f9s= +github.com/gordonklaus/ineffassign v0.1.0/go.mod h1:Qcp2HIAYhR7mNUVSIxZww3Guk4it82ghYcEXIAk+QT0= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/csrf v1.6.0/go.mod h1:7tSf8kmjNYr7IWDCYhd3U8Ck34iQ/Yw5CJu7bAkHEGI= +github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= +github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= +github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= +github.com/gorilla/handlers v1.4.1/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= +github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gostaticanalysis/analysisutil v0.7.1 h1:ZMCjoue3DtDWQ5WyU16YbjbQEQ3VuzwxALrpYd+HeKk= +github.com/gostaticanalysis/analysisutil v0.7.1/go.mod h1:v21E3hY37WKMGSnbsw2S/ojApNWb6C1//mXO48CXbVc= +github.com/gostaticanalysis/comment v1.4.1/go.mod h1:ih6ZxzTHLdadaiSnF5WY3dxUoXfXAlTaRzuaNDlSado= +github.com/gostaticanalysis/comment v1.4.2/go.mod h1:KLUTGDv6HOCotCH8h2erHKmpci2ZoR8VPu34YA2uzdM= +github.com/gostaticanalysis/comment v1.5.0 h1:X82FLl+TswsUMpMh17srGRuKaaXprTaytmEpgnKIDu8= +github.com/gostaticanalysis/comment v1.5.0/go.mod h1:V6eb3gpCv9GNVqb6amXzEUX3jXLVK/AdA+IrAMSqvEc= +github.com/gostaticanalysis/forcetypeassert v0.2.0 h1:uSnWrrUEYDr86OCxWa4/Tp2jeYDlogZiZHzGkWFefTk= +github.com/gostaticanalysis/forcetypeassert v0.2.0/go.mod h1:M5iPavzE9pPqWyeiVXSFghQjljW1+l/Uke3PXHS6ILY= +github.com/gostaticanalysis/nilerr v0.1.1 h1:ThE+hJP0fEp4zWLkWHWcRyI2Od0p7DlgYG3Uqrmrcpk= +github.com/gostaticanalysis/nilerr v0.1.1/go.mod h1:wZYb6YI5YAxxq0i1+VJbY0s2YONW0HU0GPE3+5PWN4A= +github.com/gostaticanalysis/testutil v0.3.1-0.20210208050101-bfb5c8eec0e4/go.mod h1:D+FIZ+7OahH3ePw/izIEeH5I06eKs1IKI4Xr64/Am3M= +github.com/gostaticanalysis/testutil v0.5.0 h1:Dq4wT1DdTwTGCQQv3rl3IvD5Ld0E6HiY+3Zh0sUGqw8= +github.com/gostaticanalysis/testutil v0.5.0/go.mod h1:OLQSbuM6zw2EvCcXTz1lVq5unyoNft372msDY0nY5Hs= +github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248= +github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= +github.com/hashicorp/consul/api v1.31.0 h1:32BUNLembeSRek0G/ZAM6WNfdEwYdYo8oQ4+JoqGkNQ= +github.com/hashicorp/consul/api v1.31.0/go.mod h1:2ZGIiXM3A610NmDULmCHd/aqBJj8CkMfOhswhOafxRg= +github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/cronexpr v1.1.2 h1:wG/ZYIKT+RT3QkOdgYc+xsKWVRgnxJ1OJtjjy84fJ9A= +github.com/hashicorp/cronexpr v1.1.2/go.mod h1:P4wA0KBl9C5q2hABiMO7cp6jcIg96CDh1Efb3g1PWA4= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix/v2 v2.1.0 h1:CUW5RYIcysz+D3B+l1mDeXrQ7fUvGGCwJfdASSzbrfo= +github.com/hashicorp/go-immutable-radix/v2 v2.1.0/go.mod h1:hgdqLXA4f6NIjRVisM1TJ9aOJVNRqKZj+xDGF6m7PBw= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= +github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.6.0 h1:uL2shRDx7RTrOrTCUZEGP/wJUFiUI8QT6E7z5o8jga4= +github.com/hashicorp/golang-lru v0.6.0/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/nomad/api v0.0.0-20241218080744-e3ac00f30eec h1:+YBzb977VrmffaCX/OBm17dEVJUcWn5dW+eqs3aIJ/A= +github.com/hashicorp/nomad/api v0.0.0-20241218080744-e3ac00f30eec/go.mod h1:svtxn6QnrQ69P23VvIWMR34tg3vmwLz4UdUzm1dSCgE= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY= +github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= +github.com/hetznercloud/hcloud-go/v2 v2.18.0 h1:BemrVGeWI8Kn/pvaC1jBsHZxQMnRqOydS7Ju4BERB4Q= +github.com/hetznercloud/hcloud-go/v2 v2.18.0/go.mod h1:r5RTzv+qi8IbLcDIskTzxkFIji7Ovc8yNgepQR9M+UA= +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/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= +github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= +github.com/ionos-cloud/sdk-go/v6 v6.3.2 h1:2mUmrZZz6cPyT9IRX0T8fBLc/7XU/eTxP2Y5tS7/09k= +github.com/ionos-cloud/sdk-go/v6 v6.3.2/go.mod h1:SXrO9OGyWjd2rZhAhEpdYN6VUAODzzqRdqA9BCviQtI= +github.com/jawher/mow.cli v1.1.0/go.mod h1:aNaQlc7ozF3vw6IJ2dHjp2ZFiA4ozMIYY6PyuRJwlUg= +github.com/jdkato/prose v1.1.1/go.mod h1:jkF0lkxaX5PFSlk9l4Gh9Y+T57TqUZziWT7uZbW5ADg= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jgautheron/goconst v1.7.1 h1:VpdAG7Ca7yvvJk5n8dMwQhfEZJh95kl/Hl9S1OI5Jkk= +github.com/jgautheron/goconst v1.7.1/go.mod h1:aAosetZ5zaeC/2EfMeRswtxUFBpe2Hr7HzkgX4fanO4= +github.com/jingyugao/rowserrcheck v1.1.1 h1:zibz55j/MJtLsjP1OF4bSdgXxwL1b+Vn7Tjzq7gFzUs= +github.com/jingyugao/rowserrcheck v1.1.1/go.mod h1:4yvlZSDb3IyDTUZJUmpZfm2Hwok+Dtp+nu2qOq+er9c= +github.com/jjti/go-spancheck v0.6.4 h1:Tl7gQpYf4/TMU7AT84MN83/6PutY21Nb9fuQjFTpRRc= +github.com/jjti/go-spancheck v0.6.4/go.mod h1:yAEYdKJ2lRkDA8g7X+oKUHXOWVAXSBJRv04OhF+QUjk= +github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/julz/importas v0.2.0 h1:y+MJN/UdL63QbFJHws9BVC5RpA2iq0kpjrFajTGivjQ= +github.com/julz/importas v0.2.0/go.mod h1:pThlt589EnCYtMnmhmRYY/qn9lCf/frPOK+WMx3xiJY= +github.com/karamaru-alpha/copyloopvar v1.2.1 h1:wmZaZYIjnJ0b5UoKDjUHrikcV0zuPyyxI4SVplLd2CI= +github.com/karamaru-alpha/copyloopvar v1.2.1/go.mod h1:nFmMlFNlClC2BPvNaHMdkirmTJxVCY0lhxBtlfOypMM= +github.com/kennygrant/sanitize v1.2.4 h1:gN25/otpP5vAsO2djbMhF/LQX6R7+O1TB4yv8NzpJ3o= +github.com/kennygrant/sanitize v1.2.4/go.mod h1:LGsjYYtgxbetdg5owWB2mpgUL6e2nfw2eObZ0u0qvak= +github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 h1:IsMZxCuZqKuao2vNdfD82fjjgPLfyHLpR41Z88viRWs= +github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6/go.mod h1:3VeWNIJaW+O5xpRQbPp0Ybqu1vJd/pm7s2F473HRrkw= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/errcheck v1.9.0 h1:9xt1zI9EBfcYBvdU1nVrzMzzUPUtPKs9bVSIM3TAb3M= +github.com/kisielk/errcheck v1.9.0/go.mod h1:kQxWMMVZgIkDq7U8xtG/n2juOjbLgZtedi0D+/VL/i8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kkHAIKE/contextcheck v1.1.6 h1:7HIyRcnyzxL9Lz06NGhiKvenXq7Zw6Q0UQu/ttjfJCE= +github.com/kkHAIKE/contextcheck v1.1.6/go.mod h1:3dDbMRNBFaq8HFXWC1JyvDSPm43CmE6IuHam8Wr0rkg= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b h1:udzkj9S/zlT5X367kqJis0QP7YMxobob6zhzq6Yre00= +github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b/go.mod h1:pcaDhQK0/NJZEvtCO0qQPPropqV0sJOJ6YW7X+9kRwM= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kulti/thelper v0.6.3 h1:ElhKf+AlItIu+xGnI990no4cE2+XaSu1ULymV2Yulxs= +github.com/kulti/thelper v0.6.3/go.mod h1:DsqKShOvP40epevkFrvIwkCMNYxMeTNjdWL4dqWHZ6I= +github.com/kunwardeep/paralleltest v1.0.10 h1:wrodoaKYzS2mdNVnc4/w31YaXFtsc21PCTdvWJ/lDDs= +github.com/kunwardeep/paralleltest v1.0.10/go.mod h1:2C7s65hONVqY7Q5Efj5aLzRCNLjw2h4eMc9EcypGjcY= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/kyokomi/emoji v2.2.1+incompatible/go.mod h1:mZ6aGCD7yk8j6QY6KICwnZ2pxoszVseX1DNoGtU2tBA= +github.com/lasiar/canonicalheader v1.1.2 h1:vZ5uqwvDbyJCnMhmFYimgMZnJMjwljN5VGY0VKbMXb4= +github.com/lasiar/canonicalheader v1.1.2/go.mod h1:qJCeLFS0G/QlLQ506T+Fk/fWMa2VmBUiEI2cuMK4djI= +github.com/ldez/exptostd v0.4.2 h1:l5pOzHBz8mFOlbcifTxzfyYbgEmoUqjxLFHZkjlbHXs= +github.com/ldez/exptostd v0.4.2/go.mod h1:iZBRYaUmcW5jwCR3KROEZ1KivQQp6PHXbDPk9hqJKCQ= +github.com/ldez/gomoddirectives v0.6.1 h1:Z+PxGAY+217f/bSGjNZr/b2KTXcyYLgiWI6geMBN2Qc= +github.com/ldez/gomoddirectives v0.6.1/go.mod h1:cVBiu3AHR9V31em9u2kwfMKD43ayN5/XDgr+cdaFaKs= +github.com/ldez/grignotin v0.9.0 h1:MgOEmjZIVNn6p5wPaGp/0OKWyvq42KnzAt/DAb8O4Ow= +github.com/ldez/grignotin v0.9.0/go.mod h1:uaVTr0SoZ1KBii33c47O1M8Jp3OP3YDwhZCmzT9GHEk= +github.com/ldez/tagliatelle v0.7.1 h1:bTgKjjc2sQcsgPiT902+aadvMjCeMHrY7ly2XKFORIk= +github.com/ldez/tagliatelle v0.7.1/go.mod h1:3zjxUpsNB2aEZScWiZTHrAXOl1x25t3cRmzfK1mlo2I= +github.com/ldez/usetesting v0.4.2 h1:J2WwbrFGk3wx4cZwSMiCQQ00kjGR0+tuuyW0Lqm4lwA= +github.com/ldez/usetesting v0.4.2/go.mod h1:eEs46T3PpQ+9RgN9VjpY6qWdiw2/QmfiDeWmdZdrjIQ= +github.com/leonklingele/grouper v1.1.2 h1:o1ARBDLOmmasUaNDesWqWCIFH3u7hoFlM84YrjT3mIY= +github.com/leonklingele/grouper v1.1.2/go.mod h1:6D0M/HVkhs2yRKRFZUoGjeDy7EZTfFBE9gl4kjmIGkA= +github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= +github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= +github.com/linode/linodego v1.46.0 h1:+uOG4SD2MIrhbrLrvOD5HrbdLN3D19Wgn3MgdUNQjeU= +github.com/linode/linodego v1.46.0/go.mod h1:vyklQRzZUWhFVBZdYx4dcYJU/gG9yKB9VUcUs6ub0Lk= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= +github.com/macabu/inamedparam v0.1.3 h1:2tk/phHkMlEL/1GNe/Yf6kkR/hkcUdAEY3L0hjYV1Mk= +github.com/macabu/inamedparam v0.1.3/go.mod h1:93FLICAIk/quk7eaPPQvbzihUdn/QkGDwIZEoLtpH6I= +github.com/magefile/mage v1.9.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= +github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/maratori/testableexamples v1.0.0 h1:dU5alXRrD8WKSjOUnmJZuzdxWOEQ57+7s93SLMxb2vI= +github.com/maratori/testableexamples v1.0.0/go.mod h1:4rhjL1n20TUTT4vdh3RDqSizKLyXp7K2u6HgraZCGzE= +github.com/maratori/testpackage v1.1.1 h1:S58XVV5AD7HADMmD0fNnziNHqKvSdDuEKdPD1rNTU04= +github.com/maratori/testpackage v1.1.1/go.mod h1:s4gRK/ym6AMrqpOa/kEbQTV4Q4jb7WeLZzVhVVVOQMc= +github.com/markbates/inflect v1.0.0/go.mod h1:oTeZL2KHA7CUX6X+fovmK9OvIOFuqu0TwdQrZjLTh88= +github.com/matoous/godox v1.1.0 h1:W5mqwbyWrwZv6OQ5Z1a/DHGMOvXYCBP3+Ht7KMoJhq4= +github.com/matoous/godox v1.1.0/go.mod h1:jgE/3fUXiTurkdHOLT5WEkThTSuE7yxHv5iWPa80afs= +github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= +github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2/go.mod h1:0KeJpeMD6o+O4hW7qJOT7vyQPKrWmj26uf5wMc/IiIs= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-shellwords v1.0.10 h1:Y7Xqm8piKOO3v10Thp7Z36h4FYFjt5xB//6XvOrs2Gw= +github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mgechev/revive v1.7.0 h1:JyeQ4yO5K8aZhIKf5rec56u0376h8AlKNQEmjfkjKlY= +github.com/mgechev/revive v1.7.0/go.mod h1:qZnwcNhoguE58dfi96IJeSTPeZQejNeoMQLUZGi4SW4= +github.com/microcosm-cc/bluemonday v1.0.6/go.mod h1:HOT/6NaBlR0f9XlxD3zolN6Z3N8Lp4pvhp+jLS5ihnI= +github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= +github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY= +github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs= +github.com/miekg/mmark v1.3.6/go.mod h1:w7r9mkTvpS55jlfyn22qJ618itLryxXBhA7Jp3FIlkw= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/hashstructure v1.0.0/go.mod h1:QjSHrPWS+BGUVBYkbTZWEnOh3G1DutKwClXU/ABz6AQ= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.2.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/moricho/tparallel v0.3.2 h1:odr8aZVFA3NZrNybggMkYO3rgPRcqjeQUlBBFVxKHTI= +github.com/moricho/tparallel v0.3.2/go.mod h1:OQ+K3b4Ln3l2TZveGCywybl68glfLEwFGqvnjok8b+U= +github.com/muesli/reflow v0.2.0/go.mod h1:qT22vjVmM9MIUeLgsVYe/Ye7eZlbv9dZjL3dVhUqLX8= +github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68 h1:y1p/ycavWjGT9FnmSjdbWUlLGvcxrY0Rw3ATltrxOhk= +github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ= +github.com/muesli/smartcrop v0.3.0/go.mod h1:i2fCI/UorTfgEpPPLWiFBv4pye+YAG78RwcQLUkocpI= +github.com/muesli/termenv v0.8.1 h1:9q230czSP3DHVpkaPDXGp0TOfAwyjyYwXlUCQxQSaBk= +github.com/muesli/termenv v0.8.1/go.mod h1:kzt/D/4a88RoheZmwfqorY3A+tnsSMA9HJC/fQSFKo0= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nakabonne/nestif v0.3.1 h1:wm28nZjhQY5HyYPx+weN3Q65k6ilSBxDb8v5S81B81U= +github.com/nakabonne/nestif v0.3.1/go.mod h1:9EtoZochLn5iUprVDmDjqGKPofoUEBL8U4Ngq6aY7OE= +github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= +github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= +github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= +github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= +github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= +github.com/nicksnyder/go-i18n v1.10.0/go.mod h1:HrK7VCrbOvQoUAQ7Vpy7i87N7JZZZ7R2xBGjv0j365Q= +github.com/niklasfasching/go-org v1.3.0 h1:X8Ob7WOF61iP4KnisZo1eVVei8/GzmNz3ymJrYLnIic= +github.com/niklasfasching/go-org v1.3.0/go.mod h1:AsLD6X7djzRIz4/RFZu8vwRL0VGjUvGZCCH1Nz0VdrU= +github.com/nishanths/exhaustive v0.12.0 h1:vIY9sALmw6T/yxiASewa4TQcFsVYZQQRUQJhKRf3Swg= +github.com/nishanths/exhaustive v0.12.0/go.mod h1:mEZ95wPIZW+x8kC4TgC+9YCUgiST7ecevsVDTgc2obs= +github.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm/w98Vk= +github.com/nishanths/predeclared v0.2.2/go.mod h1:RROzoN6TnGQupbC+lqggsOlcgysk3LMK/HI84Mp280c= +github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E= +github.com/nunnatsa/ginkgolinter v0.19.1 h1:mjwbOlDQxZi9Cal+KfbEJTCz327OLNfwNvoZ70NJ+c4= +github.com/nunnatsa/ginkgolinter v0.19.1/go.mod h1:jkQ3naZDmxaZMXPWaS9rblH+i+GWXQCaS/JFIWcOH2s= +github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= +github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU= +github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= +github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics v0.116.0 h1:Kxk5Ral+Dc6VB9UmTketVjs+rbMZP8JxQ4SXDx4RivQ= +github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics v0.116.0/go.mod h1:ctT6oQmGmWGGGgUIKyx2fDwqz77N9+04gqKkDyAzKCg= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.116.0 h1:jwnZYRBuPJnsKXE5H6ZvTEm91bXW5VP8+tLewzl54eg= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.116.0/go.mod h1:NT3Ag+DdnIAZQfD7l7OHwlYqnaAJ19SoPZ0nhD9yx4s= +github.com/open-telemetry/opentelemetry-collector-contrib/processor/deltatocumulativeprocessor v0.116.0 h1:ZBmLuipJv7BT9fho/2yAFsS8AtMsCOCe4ON8oqkX3n8= +github.com/open-telemetry/opentelemetry-collector-contrib/processor/deltatocumulativeprocessor v0.116.0/go.mod h1:f0GdYWGxUunyRZ088gHnoX78pc/gZc3dQlRtidiGXzg= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= +github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= +github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= +github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= +github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= +github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= +github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU= +github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w= +github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= +github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= +github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= +github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= +github.com/ovh/go-ovh v1.6.0 h1:ixLOwxQdzYDx296sXcgS35TOPEahJkpjMGtzPadCjQI= +github.com/ovh/go-ovh v1.6.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c= +github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= +github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= +github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= +github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/polyfloyd/go-errorlint v1.7.1 h1:RyLVXIbosq1gBdk/pChWA8zWYLsq9UEw7a1L5TVMCnA= +github.com/polyfloyd/go-errorlint v1.7.1/go.mod h1:aXjNb1x2TNhoLsk26iv1yl7a+zTnXPhwEMtEXukiLR8= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= +github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk= +github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/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.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.30.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= +github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +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/prometheus/prometheus v0.302.1 h1:xqVdrwrB4WNpdgJqxsz5loqFWNUZitsK8myqLuSZ6Ag= +github.com/prometheus/prometheus v0.302.1/go.mod h1:YcyCoTbUR/TM8rY3Aoeqr0AWTu/pu1Ehh+trpX3eRzg= +github.com/prometheus/sigv4 v0.1.1 h1:UJxjOqVcXctZlwDjpUpZ2OiMWJdFijgSofwLzO1Xk0Q= +github.com/prometheus/sigv4 v0.1.1/go.mod h1:RAmWVKqx0bwi0Qm4lrKMXFM0nhpesBcenfCtz9qRyH8= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1 h1:+Wl/0aFp0hpuHM3H//KMft64WQ1yX9LdJY64Qm/gFCo= +github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1/go.mod h1:GJLgqsLeo4qgavUoL8JeGFNS7qcisx3awV/w9eWTmNI= +github.com/quasilyte/go-ruleguard/dsl v0.3.22 h1:wd8zkOhSNr+I+8Qeciml08ivDt1pSXe60+5DqOpCjPE= +github.com/quasilyte/go-ruleguard/dsl v0.3.22/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU= +github.com/quasilyte/gogrep v0.5.0 h1:eTKODPXbI8ffJMN+W2aE0+oL0z/nh8/5eNdiO34SOAo= +github.com/quasilyte/gogrep v0.5.0/go.mod h1:Cm9lpz9NZjEoL1tgZ2OgeUKPIxL1meE7eo60Z6Sk+Ng= +github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 h1:TCg2WBOl980XxGFEZSS6KlBGIV0diGdySzxATTWoqaU= +github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0= +github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 h1:M8mH9eK4OUR4lu7Gd+PU1fV2/qnDNfzT635KRSObncs= +github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567/go.mod h1:DWNGW8A4Y+GyBgPuaQJuWiy0XYftx4Xm/y5Jqk9I6VQ= +github.com/raeperd/recvcheck v0.2.0 h1:GnU+NsbiCqdC2XX5+vMZzP+jAJC5fht7rcVTAhX74UI= +github.com/raeperd/recvcheck v0.2.0/go.mod h1:n04eYkwIR0JbgD73wT8wL4JjPC3wm0nFtzBnWNocnYU= +github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E= +github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.5.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday v1.5.3-0.20200218234912-41c5fccfd6f6/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= +github.com/ryancurrah/gomodguard v1.3.5 h1:cShyguSwUEeC0jS7ylOiG/idnd1TpJ1LfHGpV3oJmPU= +github.com/ryancurrah/gomodguard v1.3.5/go.mod h1:MXlEPQRxgfPQa62O8wzK3Ozbkv9Rkqr+wKjSxTdsNJE= +github.com/ryanrolds/sqlclosecheck v0.5.1 h1:dibWW826u0P8jNLsLN+En7+RqWWTYrjCB9fJfSfdyCU= +github.com/ryanrolds/sqlclosecheck v0.5.1/go.mod h1:2g3dUjoS6AL4huFdv6wn55WpLIDjY7ZgUR4J8HOO/XQ= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca h1:NugYot0LIVPxTvN8n+Kvkn6TrbMyxQiuvKdEwFdR9vI= +github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU= +github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= +github.com/sanity-io/litter v1.2.0/go.mod h1:JF6pZUFgu2Q0sBZ+HSV35P8TVPI1TTzEwyu9FXAw2W4= +github.com/sanposhiho/wastedassign/v2 v2.1.0 h1:crurBF7fJKIORrV85u9UUpePDYGWnwvv3+A96WvwXT0= +github.com/sanposhiho/wastedassign/v2 v2.1.0/go.mod h1:+oSmSC+9bQ+VUAxA66nBb0Z7N8CK7mscKTDYC6aIek4= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 h1:PKK9DyHxif4LZo+uQSgXNqs0jj5+xZwwfKHgph2lxBw= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.1/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= +github.com/sashamelentyev/interfacebloat v1.1.0 h1:xdRdJp0irL086OyW1H/RTZTr1h/tMEOsumirXcOJqAw= +github.com/sashamelentyev/interfacebloat v1.1.0/go.mod h1:+Y9yU5YdTkrNvoX0xHc84dxiN1iBi9+G8zZIhPVoNjQ= +github.com/sashamelentyev/usestdlibvars v1.28.0 h1:jZnudE2zKCtYlGzLVreNp5pmCdOxXUzwsMDBkR21cyQ= +github.com/sashamelentyev/usestdlibvars v1.28.0/go.mod h1:9nl0jgOfHKWNFS43Ojw0i7aRoS4j6EBye3YBhmAIRF8= +github.com/scaleway/scaleway-sdk-go v1.0.0-beta.30 h1:yoKAVkEVwAqbGbR8n87rHQ1dulL25rKloGadb3vm770= +github.com/scaleway/scaleway-sdk-go v1.0.0-beta.30/go.mod h1:sH0u6fq6x4R5M7WxkoQFY/o7UaiItec0o1LinLCJNq8= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/securego/gosec/v2 v2.22.2 h1:IXbuI7cJninj0nRpZSLCUlotsj8jGusohfONMrHoF6g= +github.com/securego/gosec/v2 v2.22.2/go.mod h1:UEBGA+dSKb+VqM6TdehR7lnQtIIMorYJ4/9CW1KVQBE= +github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= +github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sivchari/containedctx v1.0.3 h1:x+etemjbsh2fB5ewm5FeLNi5bUjK0V8n0RB+Wwfd0XE= +github.com/sivchari/containedctx v1.0.3/go.mod h1:c1RDvCbnJLtH4lLcYD/GqwiBSSf4F5Qk0xld2rBqzJ4= +github.com/sivchari/tenv v1.12.1 h1:+E0QzjktdnExv/wwsnnyk4oqZBUfuh89YMQT1cyuvSY= +github.com/sivchari/tenv v1.12.1/go.mod h1:1LjSOUCc25snIr5n3DtGGrENhX3LuWefcplwVGC24mw= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/sonatard/noctx v0.1.0 h1:JjqOc2WN16ISWAjAk8M5ej0RfExEXtkEyExl2hLW+OM= +github.com/sonatard/noctx v0.1.0/go.mod h1:0RvBxqY8D4j9cTTTWE8ylt2vqj2EPI8fHmrxHdsaZ2c= +github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= +github.com/sourcegraph/go-diff v0.7.0 h1:9uLlrd5T46OXs5qpp8L/MTltk0zikUGi0sNNyCpA8G0= +github.com/sourcegraph/go-diff v0.7.0/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= +github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= +github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/fsync v0.9.0/go.mod h1:fNtJEfG3HiltN3y4cPOz6MLjos9+2pIEqLIgszqhp/0= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/spf13/viper v1.6.1/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k= +github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ= +github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI= +github.com/ssgreg/nlreturn/v2 v2.2.1 h1:X4XDI7jstt3ySqGU86YGAURbxw3oTDPK9sPEi6YEwQ0= +github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I= +github.com/stbenjam/no-sprintf-host-port v0.2.0 h1:i8pxvGrt1+4G0czLr/WnmyH7zbZ8Bg8etvARQ1rpyl4= +github.com/stbenjam/no-sprintf-host-port v0.2.0/go.mod h1:eL0bQ9PasS0hsyTyfTjjG+E80QIyPnBVQbYZyv20Jfk= +github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= +github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/tdakkota/asciicheck v0.4.1 h1:bm0tbcmi0jezRA2b5kg4ozmMuGAFotKI3RZfrhfovg8= +github.com/tdakkota/asciicheck v0.4.1/go.mod h1:0k7M3rCfRXb0Z6bwgvkEIMleKH3kXNz9UqJ9Xuqopr8= +github.com/tdewolff/minify/v2 v2.6.2/go.mod h1:BkDSm8aMMT0ALGmpt7j3Ra7nLUgZL0qhyrAHXwxcy5w= +github.com/tdewolff/parse/v2 v2.4.2/go.mod h1:WzaJpRSbwq++EIQHYIRTpbYKNA3gn9it1Ik++q4zyho= +github.com/tdewolff/test v1.0.6/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE= +github.com/temoto/robotstxt v1.1.1 h1:Gh8RCs8ouX3hRSxxK7B1mO5RFByQ4CmJZDwgom++JaA= +github.com/temoto/robotstxt v1.1.1/go.mod h1:+1AmkuG3IYkh1kv0d2qEB9Le88ehNO0zwOr3ujewlOo= +github.com/tenntenn/modver v1.0.1 h1:2klLppGhDgzJrScMpkj9Ujy3rXPUspSjAcev9tSEBgA= +github.com/tenntenn/modver v1.0.1/go.mod h1:bePIyQPb7UeioSRkw3Q0XeMhYZSMx9B8ePqg6SAMGH0= +github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3 h1:f+jULpRQGxTSkNYKJ51yaw6ChIqO+Je8UqsTKN/cDag= +github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3/go.mod h1:ON8b8w4BN/kE1EOhwT0o+d62W65a6aPw1nouo9LMgyY= +github.com/tetafro/godot v1.5.0 h1:aNwfVI4I3+gdxjMgYPus9eHmoBeJIbnajOyqZYStzuw= +github.com/tetafro/godot v1.5.0/go.mod h1:2oVxTBSftRTh4+MVfUaUXR6bn2GDXCaMcOG4Dk3rfio= +github.com/theckman/yacspin v0.8.0 h1:9LA2kUol1/+eH5m/ptlbYCrnCEfLCaX4Xn+5tK/AprI= +github.com/theckman/yacspin v0.8.0/go.mod h1:K1H1naXCpDytqETpvmlxWzAq8BbOMy3Wrd0iy0ZNzRI= +github.com/tidwall/pretty v0.0.0-20190325153808-1166b9ac2b65/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/timakin/bodyclose v0.0.0-20241017074812-ed6a65f985e3 h1:y4mJRFlM6fUyPhoXuFg/Yu02fg/nIPFMOY8tOqppoFg= +github.com/timakin/bodyclose v0.0.0-20241017074812-ed6a65f985e3/go.mod h1:mkjARE7Yr8qU23YcGMSALbIxTQ9r9QBVahQOBRfU460= +github.com/timonwong/loggercheck v0.10.1 h1:uVZYClxQFpw55eh+PIoqM7uAOHMrhVcDoWDery9R8Lg= +github.com/timonwong/loggercheck v0.10.1/go.mod h1:HEAWU8djynujaAVX7QI65Myb8qgfcZ1uKbdpg3ZzKl8= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tomarrell/wrapcheck/v2 v2.10.0 h1:SzRCryzy4IrAH7bVGG4cK40tNUhmVmMDuJujy4XwYDg= +github.com/tomarrell/wrapcheck/v2 v2.10.0/go.mod h1:g9vNIyhb5/9TQgumxQyOEqDHsmGYcGsVMOx/xGkqdMo= +github.com/tommy-muehle/go-mnd/v2 v2.5.1 h1:NowYhSdyE/1zwK9QCLeRb6USWdoif80Ie+v+yU8u1Zw= +github.com/tommy-muehle/go-mnd/v2 v2.5.1/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw= +github.com/uber-go/atomic v1.3.2/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g= +github.com/uber/jaeger-client-go v2.15.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= +github.com/uber/jaeger-lib v1.5.0/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/ultraware/funlen v0.2.0 h1:gCHmCn+d2/1SemTdYMiKLAHFYxTYz7z9VIDRaTGyLkI= +github.com/ultraware/funlen v0.2.0/go.mod h1:ZE0q4TsJ8T1SQcjmkhN/w+MceuatI6pBFSxxyteHIJA= +github.com/ultraware/whitespace v0.2.0 h1:TYowo2m9Nfj1baEQBjuHzvMRbp19i+RCcRYrSWoFa+g= +github.com/ultraware/whitespace v0.2.0/go.mod h1:XcP1RLD81eV4BW8UhQlpaR+SDc2givTvyI8a586WjW8= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/uudashr/gocognit v1.2.0 h1:3BU9aMr1xbhPlvJLSydKwdLN3tEUUrzPSSM8S4hDYRA= +github.com/uudashr/gocognit v1.2.0/go.mod h1:k/DdKPI6XBZO1q7HgoV2juESI2/Ofj9AcHPZhBBdrTU= +github.com/uudashr/iface v1.3.1 h1:bA51vmVx1UIhiIsQFSNq6GZ6VPTk3WNMZgRiCe9R29U= +github.com/uudashr/iface v1.3.1/go.mod h1:4QvspiRd3JLPAEXBQ9AiZpLbJlrWWgRChOKDJEuQTdg= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= +github.com/vultr/govultr/v2 v2.17.2 h1:gej/rwr91Puc/tgh+j33p/BLR16UrIPnSr+AIwYWZQs= +github.com/vultr/govultr/v2 v2.17.2/go.mod h1:ZFOKGWmgjytfyjeyAdhQlSWwTjh2ig+X49cAp50dzXI= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= +github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= +github.com/xen0n/gosmopolitan v1.2.2 h1:/p2KTnMzwRexIW8GlKawsTWOxn7UHA+jCMF/V8HHtvU= +github.com/xen0n/gosmopolitan v1.2.2/go.mod h1:7XX7Mj61uLYrj0qmeN0zi7XDon9JRAEhYQqAPLVNTeg= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yagipy/maintidx v1.0.0 h1:h5NvIsCz+nRDapQ0exNv4aJ0yXSI0420omVANTv3GJM= +github.com/yagipy/maintidx v1.0.0/go.mod h1:0qNf/I/CCZXSMhsRsrEPDZ+DkekpKLXAJfsTACwgXLk= +github.com/yeya24/promlinter v0.3.0 h1:JVDbMp08lVCP7Y6NP3qHroGAO6z2yGKQtS5JsjqtoFs= +github.com/yeya24/promlinter v0.3.0/go.mod h1:cDfJQQYv9uYciW60QT0eeHlFodotkYZlL+YcPQN+mW4= +github.com/ykadowak/zerologlint v0.1.5 h1:Gy/fMz1dFQN9JZTPjv1hxEk+sRWm05row04Yoolgdiw= +github.com/ykadowak/zerologlint v0.1.5/go.mod h1:KaUskqF3e/v59oPmdq1U1DnKcuHokl2/K1U4pmIELKg= +github.com/yuin/goldmark v1.1.22/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.31/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.3/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/goldmark-emoji v1.0.1 h1:ctuWEyzGBwiucEqxzwe0SOYDXPAucOrE9NQC18Wa1os= +github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ= +github.com/yuin/goldmark-highlighting v0.0.0-20200307114337-60d527fdb691/go.mod h1:YLF3kDffRfUH/bTxOxHhV6lxwIB3Vfj91rEwNMS9MXo= +gitlab.com/bosi/decorder v0.4.2 h1:qbQaV3zgwnBZ4zPMhGLW4KZe7A7NwxEhJx39R3shffo= +gitlab.com/bosi/decorder v0.4.2/go.mod h1:muuhHoaJkA9QLcYHq4Mj8FJUwDZ+EirSHRiaTcTf6T8= +go-simpler.org/assert v0.9.0 h1:PfpmcSvL7yAnWyChSjOz6Sp6m9j5lyK8Ok9pEL31YkQ= +go-simpler.org/assert v0.9.0/go.mod h1:74Eqh5eI6vCK6Y5l3PI8ZYFXG4Sa+tkr70OIPJAUr28= +go-simpler.org/musttag v0.13.0 h1:Q/YAW0AHvaoaIbsPj3bvEI5/QFP7w696IMUpnKXQfCE= +go-simpler.org/musttag v0.13.0/go.mod h1:FTzIGeK6OkKlUDVpj0iQUXZLUO1Js9+mvykDQy9C5yM= +go-simpler.org/sloglint v0.9.0 h1:/40NQtjRx9txvsB/RN022KsUJU+zaaSb/9q9BSefSrE= +go-simpler.org/sloglint v0.9.0/go.mod h1:G/OrAF6uxj48sHahCzrbarVMptL2kjWTaUeC8+fOGww= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= +go.mongodb.org/mongo-driver v1.0.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.opencensus.io v0.15.0/go.mod h1:UffZAU+4sDEINUGP/B7UfBBkq4fqLu9zXAX7ke6CHW0= +go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/collector/component v0.118.0 h1:sSO/ObxJ+yH77Z4DmT1mlSuxhbgUmY1ztt7xCA1F/8w= +go.opentelemetry.io/collector/component v0.118.0/go.mod h1:LUJ3AL2b+tmFr3hZol3hzKzCMvNdqNq0M5CF3SWdv4M= +go.opentelemetry.io/collector/config/configtelemetry v0.118.0 h1:UlN46EViG2X42odWtXgWaqY7Y01ZKpsnswSwXTWx5mM= +go.opentelemetry.io/collector/config/configtelemetry v0.118.0/go.mod h1:SlBEwQg0qly75rXZ6W1Ig8jN25KBVBkFIIAUI1GiAAE= +go.opentelemetry.io/collector/consumer v1.24.0 h1:7DeyBm9qdr1EPuCfPjWyChPK16DbVc0wZeSa9LZprFU= +go.opentelemetry.io/collector/consumer v1.24.0/go.mod h1:0G6jvZprIp4dpKMD1ZxCjriiP9GdFvFMObsQEtTk71s= +go.opentelemetry.io/collector/pdata v1.24.0 h1:D6j92eAzmAbQgivNBUnt8r9juOl8ugb+ihYynoFZIEg= +go.opentelemetry.io/collector/pdata v1.24.0/go.mod h1:cf3/W9E/uIvPS4MR26SnMFJhraUCattzzM6qusuONuc= +go.opentelemetry.io/collector/pipeline v0.118.0 h1:RI1DMe7L0+5hGkx0EDGxG00TaJoh96MEQppgOlGx1Oc= +go.opentelemetry.io/collector/pipeline v0.118.0/go.mod h1:qE3DmoB05AW0C3lmPvdxZqd/H4po84NPzd5MrqgtL74= +go.opentelemetry.io/collector/processor v0.118.0 h1:NlqWiTTpPP+EPbrqTcNP9nh/4O4/9U9RGWVB49xo4ws= +go.opentelemetry.io/collector/processor v0.118.0/go.mod h1:Y8OD7wk51oPuBqrbn1qXIK91AbprRHP76hlvEzC24U4= +go.opentelemetry.io/collector/semconv v0.118.0 h1:V4vlMIK7TIaemrrn2VawvQPwruIKpj7Xgw9P5+BL56w= +go.opentelemetry.io/collector/semconv v0.118.0/go.mod h1:N6XE8Q0JKgBN2fAhkUQtqK9LT7rEGR6+Wu/Rtbal1iI= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.59.0 h1:iQZYNQ7WwIcYXzOPR46FQv9O0dS1PW16RjvR0TjDOe8= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.59.0/go.mod h1:54CaSNqYEXvpzDh8KPjiMVoWm60t5R0dZRt0leEPgAs= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I= +go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= +go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= +go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= +go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= +go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU= +go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ= +go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= +go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= +go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= +go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +gocloud.dev v0.15.0/go.mod h1:ShXCyJaGrJu9y/7a6+DSCyBb9MFGZ1P5wwPa0Wu6w34= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181001203147-e3636079e1a4/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190422183909-d864b10871cd/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= +golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= +golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= +golang.org/x/exp/typeparams v0.0.0-20250218142911-aa4b98e5adaa h1:Br3+0EZZohShrmVVc85znGpxw7Ca8hsUJlrdT/JQGw8= +golang.org/x/exp/typeparams v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:LKZHyeOpPuZcMgxeHjJp4p5yvxrCX1xDvH10zYHhjjQ= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20191214001246-9130b4cfad52/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +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= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190322120337-addf6b3196f6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190420063019-afa5a82059c6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190424112056-4829fb13d2c6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190319182350-c85d3e98c914/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190523182746-aaccbc9213b0/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/perf v0.0.0-20250605212013-b481878a17be h1:Oh/azZqQQCNCOOki6pNDJt6WOp5zqqEjBklun7f8Tqo= +golang.org/x/perf v0.0.0-20250605212013-b481878a17be/go.mod h1:nWcMYnRm5u4K1K1uHna2Dr0Ly7nKHQZa2dpXixO3BZ8= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181031143558-9b800f95dbbc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/telemetry v0.0.0-20250417124945-06ef541f3fa3 h1:RXY2+rSHXvxO2Y+gKrPjYVaEoGOqh3VEXFhnWAt1Irg= +golang.org/x/telemetry v0.0.0-20250417124945-06ef541f3fa3/go.mod h1:RoaXAWDwS90j6FxVKwJdBV+0HCU+llrKUGgJaxiKl6M= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= +golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +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= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200324003944-a576cf524670/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200329025819-fd4102a86c65/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200724022722-7017fd6b1305/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200820010801-b793a1359eac/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20201020161133-226fd2f889ca/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= +golang.org/x/tools v0.0.0-20201023174141-c8cfbd0f21e6/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.1-0.20210205202024-ef80cdb6ec6d/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU= +golang.org/x/tools v0.1.1-0.20210302220138-2ac05c832e1a/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= +golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= +golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= +golang.org/x/tools/gopls v0.19.0-pre.2 h1:CUjqNCFGKHPMRZMd/tkBvtkae+eJuGNHOYsGpKxQNj4= +golang.org/x/tools/gopls v0.19.0-pre.2/go.mod h1:978AwBntyFmG+swdND5BfSs81hQdJQHR3jSUYtKpg0s= +golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= +google.golang.org/api v0.3.2/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.5.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.223.0 h1:JUTaWEriXmEy5AhvdMgksGGPEFsYfUKaPEYXd4c3Wvc= +google.golang.org/api v0.223.0/go.mod h1:C+RS7Z+dDwds2b+zoAk5hN/eSfsiCn0UDrYof/M4d2M= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190508193815-b515fa19cec8/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190522204451-c2c4e71fbf69/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= +google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD4Q5w+vfEnPvPpuTwedCNVohYJfNk= +google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f h1:gap6+3Gk41EItBuyi4XX/bp4oqJ3UwuIMl25yGinuAA= +google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:Ic02D47M+zbarjYYUlK57y316f2MoN0gjAwI3f2S95o= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2 h1:DMTIbak9GhdaSxEjvVzAeNZvyc03I61duqNbnm3SU0M= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.19.1/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= +google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= +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= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.7.0-0.dev.0.20250523013057-bbc2f4dd71ea h1:fj8r9irJSpolAGUdZBxJIRY3lLc4jH2Dt4lwnWyWwpw= +honnef.co/go/tools v0.7.0-0.dev.0.20250523013057-bbc2f4dd71ea/go.mod h1:EPDDhEZqVHhWuPI5zPAsjU0U7v9xNIWjoOVyZ5ZcniQ= +k8s.io/api v0.31.3 h1:umzm5o8lFbdN/hIXbrK9oRpOproJO62CV1zqxXrLgk8= +k8s.io/api v0.31.3/go.mod h1:UJrkIp9pnMOI9K2nlL6vwpxRzzEX5sWgn8kGQe92kCE= +k8s.io/apimachinery v0.31.3 h1:6l0WhcYgasZ/wk9ktLq5vLaoXJJr5ts6lkaQzgeYPq4= +k8s.io/apimachinery v0.31.3/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= +k8s.io/client-go v0.31.3 h1:CAlZuM+PH2cm+86LOBemaJI/lQ5linJ6UFxKX/SoG+4= +k8s.io/client-go v0.31.3/go.mod h1:2CgjPUTpv3fE5dNygAr2NcM8nhHzXvxB8KL5gYc3kJs= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= +k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= +k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= +k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +mvdan.cc/gofumpt v0.7.0 h1:bg91ttqXmi9y2xawvkuMXyvAA/1ZGJqYAEGjXuP0JXU= +mvdan.cc/gofumpt v0.7.0/go.mod h1:txVFJy/Sc/mvaycET54pV8SW8gWxTlUuGHVEcncmNUo= +mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f h1:lMpcwN6GxNbWtbpI1+xzFLSW8XzX0u72NttUGVFjO3U= +mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f/go.mod h1:RSLa7mKKCNeTTMHBw5Hsy2rfJmd6O2ivt9Dw9ZqCQpQ= +pack.ag/amqp v0.8.0/go.mod h1:4/cbmt4EJXSKlG6LCfWHoqmN0uFdy5i/+YFz+fTfhV4= +pack.ag/amqp v0.11.0/go.mod h1:4/cbmt4EJXSKlG6LCfWHoqmN0uFdy5i/+YFz+fTfhV4= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= diff --git a/internal/promql-engine/logicalplan/codec.go b/internal/promql-engine/logicalplan/codec.go new file mode 100644 index 00000000000..d57098443e4 --- /dev/null +++ b/internal/promql-engine/logicalplan/codec.go @@ -0,0 +1,224 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package logicalplan + +import ( + "bytes" + "encoding/json" + "math" + + "github.com/prometheus/prometheus/model/labels" +) + +const ( + nanVal = `"NaN"` + infVal = `"+Inf"` + negInfVal = `"-Inf"` +) + +type jsonNode struct { + Type NodeType `json:"type"` + Data json.RawMessage `json:"data"` + Children []json.RawMessage `json:"children,omitempty"` +} + +func Marshal(node Node) ([]byte, error) { + clone := node.Clone() + return marshalNode(clone) +} + +func marshalNode(node Node) ([]byte, error) { + children := make([]json.RawMessage, 0, len(node.Children())) + for _, c := range node.Children() { + childData, err := marshalNode(*c) + if err != nil { + return nil, err + } + children = append(children, childData) + } + var data json.RawMessage = nil + // Special handling for -Inf/+Inf values. + if n, ok := node.(*NumberLiteral); ok { + if math.IsInf(n.Val, 1) { + data = json.RawMessage(infVal) + } + if math.IsInf(n.Val, -1) { + data = json.RawMessage(negInfVal) + } + if math.IsNaN(n.Val) { + data = json.RawMessage(nanVal) + } + } + if data == nil { + var err error + data, err = json.Marshal(node) + if err != nil { + return nil, err + } + } + return json.Marshal(jsonNode{ + Type: node.Type(), + Data: data, + Children: children, + }) +} + +func Unmarshal(data []byte) (Node, error) { + return unmarshalNode(data) +} + +func unmarshalNode(data []byte) (Node, error) { + t := jsonNode{} + if err := json.Unmarshal(data, &t); err != nil { + return nil, err + } + + switch t.Type { + case VectorSelectorNode: + v := &VectorSelector{} + if err := json.Unmarshal(t.Data, v); err != nil { + return nil, err + } + var err error + for i, m := range v.LabelMatchers { + v.LabelMatchers[i], err = labels.NewMatcher(m.Type, m.Name, m.Value) + if err != nil { + return nil, err + } + } + return v, nil + case MatrixSelectorNode: + m := &MatrixSelector{} + if err := json.Unmarshal(t.Data, m); err != nil { + return nil, err + } + vs, err := unmarshalNode(t.Children[0]) + if err != nil { + return nil, err + } + m.VectorSelector = vs.(*VectorSelector) + return m, nil + case AggregationNode: + a := &Aggregation{} + if err := json.Unmarshal(t.Data, a); err != nil { + return nil, err + } + var err error + a.Expr, err = unmarshalNode(t.Children[0]) + if err != nil { + return nil, err + } + if len(t.Children) > 1 { + a.Param, err = unmarshalNode(t.Children[1]) + if err != nil { + return nil, err + } + } + return a, nil + case BinaryNode: + b := &Binary{} + if err := json.Unmarshal(t.Data, b); err != nil { + return nil, err + } + var err error + b.LHS, err = unmarshalNode(t.Children[0]) + if err != nil { + return nil, err + } + b.RHS, err = unmarshalNode(t.Children[1]) + if err != nil { + return nil, err + } + return b, nil + case FunctionNode: + f := &FunctionCall{} + if err := json.Unmarshal(t.Data, f); err != nil { + return nil, err + } + for _, c := range t.Children { + child, err := unmarshalNode(c) + if err != nil { + return nil, err + } + f.Args = append(f.Args, child) + } + return f, nil + case NumberLiteralNode: + n := &NumberLiteral{} + if bytes.Equal(t.Data, []byte(infVal)) { + n.Val = math.Inf(1) + } else if bytes.Equal(t.Data, []byte(negInfVal)) { + n.Val = math.Inf(-1) + } else if bytes.Equal(t.Data, []byte(nanVal)) { + n.Val = math.NaN() + } else { + if err := json.Unmarshal(t.Data, n); err != nil { + return nil, err + } + } + return n, nil + case StringLiteralNode: + s := &StringLiteral{} + if err := json.Unmarshal(t.Data, s); err != nil { + return nil, err + } + return s, nil + case SubqueryNode: + s := &Subquery{} + if err := json.Unmarshal(t.Data, s); err != nil { + return nil, err + } + var err error + s.Expr, err = unmarshalNode(t.Children[0]) + if err != nil { + return nil, err + } + return s, nil + case CheckDuplicateNode: + c := &CheckDuplicateLabels{} + if err := json.Unmarshal(t.Data, c); err != nil { + return nil, err + } + var err error + c.Expr, err = unmarshalNode(t.Children[0]) + if err != nil { + return nil, err + } + return c, nil + case StepInvariantNode: + s := &StepInvariantExpr{} + if err := json.Unmarshal(t.Data, s); err != nil { + return nil, err + } + var err error + s.Expr, err = unmarshalNode(t.Children[0]) + if err != nil { + return nil, err + } + return s, nil + case ParensNode: + p := &Parens{} + if err := json.Unmarshal(t.Data, p); err != nil { + return nil, err + } + var err error + p.Expr, err = unmarshalNode(t.Children[0]) + if err != nil { + return nil, err + } + return p, nil + case UnaryNode: + u := &Unary{} + if err := json.Unmarshal(t.Data, u); err != nil { + return nil, err + } + var err error + u.Expr, err = unmarshalNode(t.Children[0]) + if err != nil { + return nil, err + } + return u, nil + } + return nil, nil +} diff --git a/internal/promql-engine/logicalplan/codec_test.go b/internal/promql-engine/logicalplan/codec_test.go new file mode 100644 index 00000000000..766ba2a9cb3 --- /dev/null +++ b/internal/promql-engine/logicalplan/codec_test.go @@ -0,0 +1,116 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package logicalplan + +import ( + "math/rand" + "testing" + + "github.com/thanos-io/promql-engine/query" + + "github.com/cortexproject/promqlsmith" + "github.com/efficientgo/core/testutil" + "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/promql/parser" +) + +const testRuns = 100 + +func TestNodesMarshalJSON(t *testing.T) { + var cases = []struct { + name string + query string + }{ + { + name: "complex query", + query: ` +sum( + max_over_time(sum by (pod) (2 * -(rate(http_requests_total[1h])))[2m:1m]) + + + http_requests_total{job="api-server"} @ end() + + label_replace(metric, "new_label", "$1", "label", ".*") +)`, + }, + { + name: "+Inf", + query: "clamp_max(metric, +Inf)", + }, + { + name: "NaN", + query: "clamp_max(metric, NaN)", + }, + { + name: "-Inf", + query: "clamp_max(metric, -Inf)", + }, + } + for _, tcase := range cases { + t.Run(tcase.name, func(t *testing.T) { + ast, err := parser.ParseExpr(tcase.query) + testutil.Ok(t, err) + original, _ := NewFromAST(ast, &query.Options{}, PlanOptions{}) + original, _ = original.Optimize(DefaultOptimizers) + + bytes, err := Marshal(original.Root()) + testutil.Ok(t, err) + + clone, err := Unmarshal(bytes) + testutil.Ok(t, err) + testutil.Equals(t, original.Root().String(), clone.String()) + }) + } +} + +func TestUnmarshalMatchers(t *testing.T) { + expr := `metric{name=~"value"}` + ast, err := parser.ParseExpr(expr) + testutil.Ok(t, err) + + original, _ := NewFromAST(ast, &query.Options{}, PlanOptions{}) + bytes, err := Marshal(original.Root()) + testutil.Ok(t, err) + clone, err := Unmarshal(bytes) + testutil.Ok(t, err) + testutil.Equals(t, original.Root().String(), clone.String()) + + vs, ok := clone.(*VectorSelector) + testutil.Assert(t, true, ok) + testutil.Assert(t, true, vs.LabelMatchers[0].Matches("value")) +} + +func FuzzNodesMarshalJSON(f *testing.F) { + f.Add(int64(0)) + f.Fuzz(func(t *testing.T, seed int64) { + lbls := []labels.Labels{ + labels.FromStrings("__name__", "http_requests_total"), + } + opts := []promqlsmith.Option{ + promqlsmith.WithEnableOffset(true), + promqlsmith.WithEnableAtModifier(true), + } + rnd := rand.New(rand.NewSource(seed)) + pqSmith := promqlsmith.New(rnd, lbls, opts...) + for range testRuns { + qry := pqSmith.WalkRangeQuery() + parser.Inspect(qry, func(node parser.Node, nodes []parser.Node) error { + switch vs := (node).(type) { + case *parser.VectorSelector: + vs.Series = nil + vs.UnexpandedSeriesSet = nil + } + return nil + }) + + original, _ := NewFromAST(qry, &query.Options{}, PlanOptions{}) + original, _ = original.Optimize(DefaultOptimizers) + + bytes, err := Marshal(original.Root()) + testutil.Ok(t, err) + + clone, err := Unmarshal(bytes) + testutil.Ok(t, err) + testutil.Equals(t, original.Root().String(), clone.String()) + } + }) +} diff --git a/internal/promql-engine/logicalplan/distribute.go b/internal/promql-engine/logicalplan/distribute.go new file mode 100644 index 00000000000..821475eb708 --- /dev/null +++ b/internal/promql-engine/logicalplan/distribute.go @@ -0,0 +1,929 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package logicalplan + +import ( + "fmt" + "math" + "slices" + "sort" + "strings" + "time" + + "github.com/thanos-io/promql-engine/api" + "github.com/thanos-io/promql-engine/query" + + "github.com/efficientgo/core/errors" + "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/promql/parser" + "github.com/prometheus/prometheus/util/annotations" +) + +var ( + RewrittenExternalLabelWarning = errors.Newf("%s: rewriting an external label with label_replace can disable distributed query execution", annotations.PromQLWarning.Error()) +) + +type timeRange struct { + start time.Time + end time.Time +} + +type timeRanges []timeRange + +// minOverlap returns the smallest overlap between consecutive time ranges that overlap the interval [mint, maxt]. +func (trs timeRanges) minOverlap(mint, maxt int64) time.Duration { + var minEngineOverlap time.Duration = math.MaxInt64 + if len(trs) == 1 { + return minEngineOverlap + } + + for i := 1; i < len(trs); i++ { + if trs[i].end.UnixMilli() < mint || trs[i].start.UnixMilli() > maxt { + continue + } + minEngineOverlap = min(minEngineOverlap, trs[i-1].end.Sub(trs[i].start)) + } + return minEngineOverlap +} + +type labelSetRanges map[string]timeRanges + +func (lrs labelSetRanges) addRange(key string, tr timeRange) { + lrs[key] = append(lrs[key], tr) +} + +// minOverlap returns the smallest overlap between all label set ranges that overlap the interval [mint, maxt]. +func (lrs labelSetRanges) minOverlap(mint, maxt int64) time.Duration { + var minLabelsetOverlap time.Duration = math.MaxInt64 + for _, lr := range lrs { + minLabelsetOverlap = min(minLabelsetOverlap, lr.minOverlap(mint, maxt)) + } + + return minLabelsetOverlap +} + +type RemoteExecutions []RemoteExecution + +func (rs RemoteExecutions) String() string { + parts := make([]string, len(rs)) + for i, r := range rs { + parts[i] = r.String() + } + return strings.Join(parts, ", ") +} + +// RemoteExecution is a logical plan that describes a +// remote execution of a Query against the given PromQL Engine. +type RemoteExecution struct { + LeafNode + Engine api.RemoteEngine + Query Node + QueryRangeStart time.Time + QueryRangeEnd time.Time +} + +func (r RemoteExecution) Clone() Node { + clone := r + clone.Query = r.Query.Clone() + return clone +} + +func (r RemoteExecution) String() string { + if r.QueryRangeStart.UnixMilli() == 0 { + return fmt.Sprintf("remote(%s)", r.Query) + } + return fmt.Sprintf("remote(%s) [%s, %s]", r.Query, r.QueryRangeStart.UTC().String(), r.QueryRangeEnd.UTC().String()) +} + +func (r RemoteExecution) Type() NodeType { return RemoteExecutionNode } + +func (r RemoteExecution) ReturnType() parser.ValueType { return r.Query.ReturnType() } + +// Deduplicate is a logical plan which deduplicates samples from multiple RemoteExecutions. +type Deduplicate struct { + Expressions RemoteExecutions +} + +func (r Deduplicate) Children() []*Node { + children := make([]*Node, len(r.Expressions)) + for i := range r.Expressions { + var n Node = r.Expressions[i] + children[i] = &n + } + return children +} + +func (r Deduplicate) Clone() Node { + clone := r + clone.Expressions = make(RemoteExecutions, len(r.Expressions)) + for i, e := range r.Expressions { + clone.Expressions[i] = e.Clone().(RemoteExecution) + } + return clone +} + +func (r Deduplicate) String() string { + return fmt.Sprintf("dedup(%s)", r.Expressions.String()) +} + +func (r Deduplicate) ReturnType() parser.ValueType { return r.Expressions[0].ReturnType() } + +func (r Deduplicate) Type() NodeType { return DeduplicateNode } + +type Noop struct { + LeafNode +} + +func (r Noop) Clone() Node { return r } + +func (r Noop) String() string { return "noop" } + +func (r Noop) ReturnType() parser.ValueType { return parser.ValueTypeVector } + +func (r Noop) Type() NodeType { return NoopNode } + +// DistributedExecutionOptimizer produces a logical plan suitable for +// distributed Query execution. +type DistributedExecutionOptimizer struct { + Endpoints api.RemoteEndpoints + SkipBinaryPushdown bool +} + +func (m DistributedExecutionOptimizer) Optimize(plan Node, opts *query.Options) (Node, annotations.Annotations) { + engines := m.Endpoints.Engines(MinMaxTime(plan, opts)) + sort.Slice(engines, func(i, j int) bool { + return engines[i].MinT() < engines[j].MinT() + }) + + labelRanges := make(labelSetRanges) + engineLabels := make(map[string]struct{}) + for _, e := range engines { + for _, lset := range e.PartitionLabelSets() { + lsetKey := lset.String() + labelRanges.addRange(lsetKey, timeRange{ + start: time.UnixMilli(e.MinT()), + end: time.UnixMilli(e.MaxT()), + }) + lset.Range(func(lbl labels.Label) { + engineLabels[lbl.Name] = struct{}{} + }) + } + } + + warns := annotations.New() + + parents := computeParents(&plan) + distributionPoints := m.computeDistributionPoints(&plan, parents, engineLabels, warns) + + TraverseBottomUp(nil, &plan, func(parent, current *Node) (stop bool) { + if _, distributeNow := distributionPoints[current]; !distributeNow { + return false + } + + if isAvgAggregation(current) && !preservesPartitionLabels(*current, engineLabels) { + // avg without partition labels: rewrite as sum/count. + *current = m.distributeAvg(*current, engines, m.subqueryOpts(parents, current, opts), labelRanges) + return true + } + + if isAbsent(current) { + *current = m.distributeAbsent(*current, engines, calculateStartOffset(current, opts.LookbackDelta), m.subqueryOpts(parents, current, opts)) + return true + } + + if isAggregation(current) { + if preservesPartitionLabels(*current, engineLabels) { + // Partition-preserving aggregation: push as-is since each engine + // computes over disjoint partition values. + *current = m.distributeQuery(current, engines, m.subqueryOpts(parents, current, opts), labelRanges) + } else { + // Distributive aggregation that drops partition labels: use a + // two-level split with local_agg(remote_agg(X)). + *current = m.distributeAggregation((*current).(*Aggregation), engines, m.subqueryOpts(parents, current, opts), labelRanges) + } + return true + } + + *current = m.distributeQuery(current, engines, m.subqueryOpts(parents, current, opts), labelRanges) + return true + }) + return plan, *warns +} + +func (m DistributedExecutionOptimizer) distributeAggregation(aggr *Aggregation, engines []api.RemoteEngine, opts *query.Options, labelRanges labelSetRanges) Node { + localAggregation := aggr.Op + if aggr.Op == parser.COUNT { + localAggregation = parser.SUM + } + remoteAggregation := newRemoteAggregation(aggr, engines) + subQueries := m.distributeQuery(&remoteAggregation, engines, opts, labelRanges) + return &Aggregation{ + Op: localAggregation, + Expr: subQueries, + Param: aggr.Param, + Grouping: aggr.Grouping, + Without: aggr.Without, + } +} + +func computeParents(plan *Node) map[*Node]*Node { + parents := make(map[*Node]*Node) + TraverseBottomUp(nil, plan, func(parent, current *Node) (stop bool) { + parents[current] = parent + return false + }) + return parents +} + +func (m DistributedExecutionOptimizer) computeDistributionPoints(plan *Node, parents map[*Node]*Node, engineLabels map[string]struct{}, warns *annotations.Annotations) map[*Node]struct{} { + marks := make(map[*Node]struct{}) + + // First pass: mark distribution points (aggregations, absent functions). + Traverse(plan, func(current *Node) { + if isAbsent(current) { + if m.isDistributive(current, engineLabels, warns) { + marks[current] = struct{}{} + } + return + } + if isAggregation(current) { + // Non-distributive aggregations that don't preserve partition labels + // cannot be distributed, except for avg which gets rewritten as sum/count. + if !m.isDistributive(current, engineLabels, warns) { + if isAvgAggregation(current) { + marks[current] = struct{}{} + } + return + } + // Distributive aggregations (standard or partition-preserving): + // defer to ancestor if possible. + if preservesPartitionLabels(*current, engineLabels) { + if m.hasDistributiveAncestor(parents, current, engineLabels, warns) { + return + } + } + marks[current] = struct{}{} + } + }) + + // Second pass: for nodes whose siblings have marks, mark them too so both + // sides of a binary expression get distributed. + Traverse(plan, func(current *Node) { + if _, ok := marks[current]; ok { + return + } + if subtreeHasMark(current, marks) { + return + } + if !m.isDistributive(current, engineLabels, warns) { + return + } + parent := parents[current] + if parent != nil && (m.isDistributive(parent, engineLabels, warns) || isAvgAggregation(parent)) { + if !subtreeHasMark(parent, marks) { + return + } + } + marks[current] = struct{}{} + }) + + return marks +} + +func subtreeHasMark(node *Node, marks map[*Node]struct{}) bool { + for _, child := range (*node).Children() { + if _, ok := marks[child]; ok { + return true + } + if subtreeHasMark(child, marks) { + return true + } + } + return false +} + +func (m DistributedExecutionOptimizer) subqueryOpts(parents map[*Node]*Node, current *Node, opts *query.Options) *query.Options { + subqueryParents := make([]*Subquery, 0, len(parents)) + for p := parents[current]; p != nil; p = parents[p] { + if subquery, ok := (*p).(*Subquery); ok { + subqueryParents = append(subqueryParents, subquery) + } + } + for i := len(subqueryParents) - 1; i >= 0; i-- { + opts = query.NestedOptionsForSubquery( + opts, + subqueryParents[i].Step, + subqueryParents[i].Range, + subqueryParents[i].Offset, + ) + } + return opts +} + +func newRemoteAggregation(rootAggregation *Aggregation, engines []api.RemoteEngine) Node { + groupingSet := make(map[string]struct{}) + for _, lbl := range rootAggregation.Grouping { + groupingSet[lbl] = struct{}{} + } + + for _, engine := range engines { + for _, lbls := range engine.PartitionLabelSets() { + lbls.Range(func(lbl labels.Label) { + if rootAggregation.Without { + delete(groupingSet, lbl.Name) + } else { + groupingSet[lbl.Name] = struct{}{} + } + }) + } + } + + groupingLabels := make([]string, 0, len(groupingSet)) + for lbl := range groupingSet { + groupingLabels = append(groupingLabels, lbl) + } + sort.Strings(groupingLabels) + + remoteAggregation := *rootAggregation + remoteAggregation.Grouping = groupingLabels + return &remoteAggregation +} + +// distributeQuery takes a PromQL expression in the form of *parser.Expr and a set of remote engines. +// For each engine which matches the time range of the query, it creates a RemoteExecution scoped to the range of the engine. +// All remote executions are wrapped in a Deduplicate logical node to make sure that results from overlapping engines are deduplicated. +func (m DistributedExecutionOptimizer) distributeQuery(expr *Node, engines []api.RemoteEngine, opts *query.Options, labelRanges labelSetRanges) Node { + startOffset := calculateStartOffset(expr, opts.LookbackDelta) + allowedStartOffset := labelRanges.minOverlap(opts.Start.UnixMilli()-startOffset.Milliseconds(), opts.End.UnixMilli()) + + if allowedStartOffset < startOffset { + return *expr + } + if IsConstantExpr(*expr) { + return *expr + } + + // Selectors in queries can be scoped to a single timestamp. This case is hard to + // distribute properly and can lead to flaky results. + // We only do it if all engines have sufficient scope for the full range of the query, + // adjusted for the timestamp. + // Otherwise, we fall back to the default mode of not executing the query remotely. + if timestamps := getQueryTimestamps(expr); len(timestamps) > 0 { + for _, e := range engines { + for _, ts := range timestamps { + if e.MinT() > ts-startOffset.Milliseconds() || e.MaxT() < ts { + return *expr + } + } + } + } + + var globalMinT int64 = math.MaxInt64 + for _, e := range engines { + if e.MinT() < globalMinT { + globalMinT = e.MinT() + } + } + + remoteQueries := make(RemoteExecutions, 0, len(engines)) + for _, e := range engines { + if !matchesExternalLabelSet(*expr, e.LabelSets()) { + continue + } + if e.MinT() > opts.End.UnixMilli() { + continue + } + if e.MaxT() < opts.Start.UnixMilli()-startOffset.Milliseconds() { + continue + } + + start, keep := getStartTimeForEngine(e, opts, startOffset, globalMinT) + if !keep { + continue + } + + remoteQueries = append(remoteQueries, RemoteExecution{ + Engine: e, + Query: (*expr).Clone(), + QueryRangeStart: start, + QueryRangeEnd: opts.End, + }) + } + + if len(remoteQueries) == 0 { + return Noop{} + } + + return Deduplicate{ + Expressions: remoteQueries, + } +} + +func (m DistributedExecutionOptimizer) distributeAbsent(expr Node, engines []api.RemoteEngine, startOffset time.Duration, opts *query.Options) Node { + queries := make(RemoteExecutions, 0, len(engines)) + for i, e := range engines { + if e.MaxT() < opts.Start.UnixMilli()-startOffset.Milliseconds() { + continue + } + if e.MinT() > opts.End.UnixMilli() { + continue + } + queries = append(queries, RemoteExecution{ + Engine: engines[i], + Query: expr.Clone(), + QueryRangeStart: opts.Start, + QueryRangeEnd: opts.End, + }) + } + // We need to make sure that absent is at least evaluated against one engine. + // Otherwise, we will end up with an empty result (not absent) when no engine matches the query. + // For practicality, we choose the latest one since it likely has data in memory or on disk. + // TODO(fpetkovski): This could also solved by a synthetic node which acts as a number literal but has specific labels. + if len(queries) == 0 && len(engines) > 0 { + return RemoteExecution{ + Engine: engines[len(engines)-1], + Query: expr, + QueryRangeStart: opts.Start, + QueryRangeEnd: opts.End, + } + } + + var rootExpr Node = queries[0] + for i := 1; i < len(queries); i++ { + rootExpr = &Binary{ + Op: parser.MUL, + LHS: rootExpr, + RHS: queries[i], + VectorMatching: &parser.VectorMatching{}, + } + } + + return rootExpr +} + +func isAbsent(expr *Node) bool { + if expr == nil { + return false + } + call, ok := (*expr).(*FunctionCall) + if !ok { + return false + } + return call.Func.Name == "absent" || call.Func.Name == "absent_over_time" +} + +// distributeAvg distributes an avg() aggregation by rewriting it as sum()/count() +// where each side is distributed independently. This is necessary because averaging +// averages gives incorrect results - we must sum all values and count all values +// separately, then divide. +func (m DistributedExecutionOptimizer) distributeAvg(expr Node, engines []api.RemoteEngine, opts *query.Options, labelRanges labelSetRanges) Node { + aggr := expr.(*Aggregation) + + sumAggr := *aggr + sumAggr.Op = parser.SUM + sumRemote := newRemoteAggregation(&sumAggr, engines) + sumSubQueries := m.distributeQuery(&sumRemote, engines, opts, labelRanges) + distributedSum := &Aggregation{ + Op: parser.SUM, + Expr: sumSubQueries, + Param: aggr.Param, + Grouping: aggr.Grouping, + Without: aggr.Without, + } + + countAggr := *aggr + countAggr.Op = parser.COUNT + countAggr.Expr = aggr.Expr.Clone() + countRemote := newRemoteAggregation(&countAggr, engines) + countSubQueries := m.distributeQuery(&countRemote, engines, opts, labelRanges) + distributedCount := &Aggregation{ + Op: parser.SUM, + Expr: countSubQueries, + Param: aggr.Param, + Grouping: aggr.Grouping, + Without: aggr.Without, + } + + return &Binary{ + Op: parser.DIV, + LHS: distributedSum, + RHS: distributedCount, + VectorMatching: &parser.VectorMatching{ + Include: aggr.Grouping, + MatchingLabels: aggr.Grouping, + On: !aggr.Without, + }, + } +} + +func getStartTimeForEngine(e api.RemoteEngine, opts *query.Options, offset time.Duration, globalMinT int64) (time.Time, bool) { + if e.MinT() > opts.End.UnixMilli() { + return time.Time{}, false + } + + // Do not adjust start time for oldest engine since there is no engine to backfill from. + if e.MinT() == globalMinT { + return opts.Start, true + } + + // A remote engine needs to have sufficient scope to do a look-back from the start of the query range. + engineMinTime := time.UnixMilli(e.MinT()) + requiredMinTime := opts.Start.Add(-offset) + + // Do not adjust the start time for instant queries since it would lead to + // changing the user-provided timestamp and sending a result for a different time. + if opts.IsInstantQuery() { + keep := engineMinTime.Before(requiredMinTime) + return opts.Start, keep + } + + // If an engine's min time is before the start time of the query, + // scope the query for this engine to the start of the range + the required offset. + if engineMinTime.After(requiredMinTime) { + engineMinTime = calculateStepAlignedStart(opts, engineMinTime.Add(offset)) + } + + start := calculateStepAlignedStart(opts, maxTime(engineMinTime, opts.Start)) + // Step alignment can push the start time past the end of the query range, + // which would produce an invalid range (QueryRangeStart > QueryRangeEnd). + // In that case the engine has no data to contribute, so skip it. + if start.After(opts.End) { + return time.Time{}, false + } + + return start, true +} + +// calculateStepAlignedStart returns a start time for the query based on the +// engine min time and the query step size. +// The purpose of this alignment is to make sure that the steps for the remote query +// have the same timestamps as the ones for the central query. +func calculateStepAlignedStart(opts *query.Options, engineMinTime time.Time) time.Time { + originalSteps := numSteps(opts.Start, opts.End, opts.Step) + remoteQuerySteps := numSteps(engineMinTime, opts.End, opts.Step) + + stepsToSkip := originalSteps - remoteQuerySteps + stepAlignedStartTime := opts.Start.UnixMilli() + stepsToSkip*opts.Step.Milliseconds() + + return time.UnixMilli(stepAlignedStartTime) +} + +// calculateStartOffset returns the offset that needs to be added to the start time +// for each remote query. It is calculated by taking the maximum between +// the range of a matrix selector (if present in a query) and the lookback configured +// in the query engine. +// Applying an offset is necessary to make sure that a remote engine has sufficient +// scope to calculate results for the first several steps of a range. +// +// For example, for a query like sum_over_time(metric[1h]), an engine with a time range of +// 6h can correctly evaluate only the last 5h of the range. +// The first 1 hour of data cannot be correctly calculated since the range selector in the engine +// will not be able to gather enough points. +func calculateStartOffset(expr *Node, lookbackDelta time.Duration) time.Duration { + if expr == nil { + return lookbackDelta + } + + var ( + selectRange time.Duration + offset time.Duration + ) + Traverse(expr, func(node *Node) { + switch n := (*node).(type) { + case *Subquery: + selectRange += n.Range + case *MatrixSelector: + selectRange += n.Range + case *VectorSelector: + offset = n.Offset + } + }) + return maxDuration(offset+selectRange, lookbackDelta) +} + +func getQueryTimestamps(expr *Node) []int64 { + var timestamps []int64 + Traverse(expr, func(node *Node) { + switch n := (*node).(type) { + case *Subquery: + if n.Timestamp != nil { + timestamps = append(timestamps, *n.Timestamp) + return + } + case *VectorSelector: + if n.Timestamp != nil { + timestamps = append(timestamps, *n.Timestamp) + return + } + } + }) + return timestamps +} + +func numSteps(start, end time.Time, step time.Duration) int64 { + return (end.UnixMilli()-start.UnixMilli())/step.Milliseconds() + 1 +} + +// preservesPartitionLabels checks if an expression preserves all partition labels. +// An expression preserves partition labels if the output series will still have +// those labels, meaning results from different engines won't overlap and can be +// coalesced without deduplication. +// +// This enables pushing more operations to remote engines. For example: +// +// topk(10, sum by (P, instance) (X)) +// +// If P is a partition label, the sum preserves P, so topk can also be pushed +// down since each engine's top 10 won't overlap with other engines' top 10. +func preservesPartitionLabels(expr Node, partitionLabels map[string]struct{}) bool { + if len(partitionLabels) == 0 { + return false + } + + switch e := expr.(type) { + case *VectorSelector, *MatrixSelector, *NumberLiteral, *StringLiteral: + return true + case *Aggregation: + for lbl := range partitionLabels { + if slices.Contains(e.Grouping, lbl) == e.Without { + return false + } + } + return true + case *Binary: + if e.VectorMatching != nil { + for lbl := range partitionLabels { + inMatching := slices.Contains(e.VectorMatching.MatchingLabels, lbl) + inInclude := slices.Contains(e.VectorMatching.Include, lbl) + if !inInclude && inMatching != e.VectorMatching.On { + return false + } + } + } + return preservesPartitionLabels(e.LHS, partitionLabels) && + preservesPartitionLabels(e.RHS, partitionLabels) + case *FunctionCall: + if e.Func.Name == "label_replace" || e.Func.Name == "label_join" { + if _, ok := partitionLabels[UnsafeUnwrapString(e.Args[1])]; ok { + return false + } + } + for _, arg := range e.Args { + if arg.ReturnType() == parser.ValueTypeVector || arg.ReturnType() == parser.ValueTypeMatrix { + if !preservesPartitionLabels(arg, partitionLabels) { + return false + } + } + } + return true + case *Unary: + return preservesPartitionLabels(e.Expr, partitionLabels) + case *Parens: + return preservesPartitionLabels(e.Expr, partitionLabels) + case *StepInvariantExpr: + return preservesPartitionLabels(e.Expr, partitionLabels) + case *CheckDuplicateLabels: + return preservesPartitionLabels(e.Expr, partitionLabels) + case *Subquery: + return preservesPartitionLabels(e.Expr, partitionLabels) + default: + return false + } +} + +func (m DistributedExecutionOptimizer) isDistributive(expr *Node, engineLabels map[string]struct{}, warns *annotations.Annotations) bool { + if expr == nil { + return false + } + + switch e := (*expr).(type) { + case Deduplicate, RemoteExecution: + return false + case *Binary: + if isBinaryExpressionWithOneScalarSide(e) { + return true + } + return !m.SkipBinaryPushdown && + isBinaryExpressionWithDistributableMatching(e, engineLabels) && + m.isDistributive(&e.LHS, engineLabels, warns) && + m.isDistributive(&e.RHS, engineLabels, warns) + case *Aggregation: + switch e.Op { + // Mathematically distributive: can be split into local_agg(remote_agg(X)) + // regardless of partition labels. + case parser.SUM, parser.MIN, parser.MAX, parser.GROUP, parser.COUNT, + parser.TOPK, parser.BOTTOMK, parser.LIMITK: + // Non-distributive: can only be pushed as-is when they preserve + // partition labels (each engine computes over disjoint data). + case parser.AVG, parser.QUANTILE, parser.STDDEV, parser.STDVAR, + parser.COUNT_VALUES, parser.LIMIT_RATIO: + if !preservesPartitionLabels(e, engineLabels) { + return false + } + default: + return false + } + case *FunctionCall: + if e.Func.Name == "label_replace" || e.Func.Name == "label_join" { + targetLabel := UnsafeUnwrapString(e.Args[1]) + if _, ok := engineLabels[targetLabel]; ok { + warns.Add(RewrittenExternalLabelWarning) + return false + } + } + // scalar() returns NaN if the vector selector returns nothing + // so it's not possible to know which result is correct. Hence, + // it is not distributive. + if e.Func.Name == "scalar" { + return false + } + } + + return true +} + +func isBinaryExpressionWithOneScalarSide(expr *Binary) bool { + lhsConstant := IsConstantScalarExpr(expr.LHS) + rhsConstant := IsConstantScalarExpr(expr.RHS) + return lhsConstant || rhsConstant +} + +func isBinaryExpressionWithDistributableMatching(expr *Binary, engineLabels map[string]struct{}) bool { + if expr.VectorMatching == nil { + return false + } + + isSetOperation := expr.Op == parser.LOR || expr.Op == parser.LUNLESS + + // For set operations (or/unless) with a constant expression on either side, + // distribution is not safe because the constant will be evaluated by each + // engine and cause duplicates. For example, `bar or on () vector(0)` would + // have vector(0) returned by every engine. + if isSetOperation && (IsConstantExpr(expr.LHS) || IsConstantExpr(expr.RHS)) { + return false + } + + // Default matching (no explicit on() or ignoring()) matches on all labels. + // For this to be safe, both sides must preserve partition labels so that the + // matching will include them. If only one side has partition labels, the matching + // behavior differs per partition. + if len(expr.VectorMatching.MatchingLabels) == 0 && !expr.VectorMatching.On { + // For or/unless with default matching, we can distribute if: + // 1. Both sides preserve partition labels (matching will include them), OR + // 2. Both sides have the same partition label scope (both global, or both + // filtered to the same partition values) + // + // Case 2 is important because it allows queries like: + // metric_a or metric_b (both global) + // metric_a{zone="east"} or metric_b{zone="east"} (same partition) + if isSetOperation { + lhsMatchers := getPartitionMatchers(expr.LHS, engineLabels) + rhsMatchers := getPartitionMatchers(expr.RHS, engineLabels) + return partitionMatchersEqual(lhsMatchers, rhsMatchers) + } + return true + } + + for lbl := range engineLabels { + inMatching := slices.Contains(expr.VectorMatching.MatchingLabels, lbl) + inInclude := slices.Contains(expr.VectorMatching.Include, lbl) + // If a partition label is in group_left/group_right (Include), distribution + // changes match cardinality semantics. Each partition only sees one value for + // that label, so what's many-to-many globally may become one-to-one per partition, + // producing results instead of errors (or vice versa). + if inInclude || inMatching != expr.VectorMatching.On { + return false + } + } + return true +} + +// getPartitionMatchers extracts matchers for partition labels from all selectors in the expression. +// Returns a map of partition label name to a list of matchers found across all selectors. +// If a selector has no matcher for a partition label, it's considered "global" for that label. +func getPartitionMatchers(expr Node, partitionLabels map[string]struct{}) map[string][]*labels.Matcher { + result := make(map[string][]*labels.Matcher) + for lbl := range partitionLabels { + result[lbl] = nil + } + + Traverse(&expr, func(current *Node) { + vs, ok := (*current).(*VectorSelector) + if !ok { + return + } + for _, m := range vs.LabelMatchers { + if _, isPartition := partitionLabels[m.Name]; isPartition { + result[m.Name] = append(result[m.Name], m) + } + } + }) + + return result +} + +// partitionMatchersEqual checks if two sets of partition matchers are equivalent. +func partitionMatchersEqual(a, b map[string][]*labels.Matcher) bool { + for lbl := range a { + aMatchers := a[lbl] + bMatchers := b[lbl] + + // Both global (no matchers) for this label + if len(aMatchers) == 0 && len(bMatchers) == 0 { + continue + } + + // One has matchers, other doesn't - not equal + if len(aMatchers) != len(bMatchers) { + return false + } + + // Compare matchers - they should be identical + // Sort by name+type+value for comparison + aSet := make(map[string]struct{}) + for _, m := range aMatchers { + key := fmt.Sprintf("%s:%d:%s", m.Name, m.Type, m.Value) + aSet[key] = struct{}{} + } + for _, m := range bMatchers { + key := fmt.Sprintf("%s:%d:%s", m.Name, m.Type, m.Value) + if _, ok := aSet[key]; !ok { + return false + } + } + } + return true +} + +// matchesExternalLabels returns false if given matchers are not matching external labels. +func matchesExternalLabelSet(expr Node, externalLabelSet []labels.Labels) bool { + if len(externalLabelSet) == 0 { + return true + } + var selectorSet [][]*labels.Matcher + Traverse(&expr, func(current *Node) { + vs, ok := (*current).(*VectorSelector) + if ok { + selectorSet = append(selectorSet, vs.LabelMatchers) + } + }) + + for _, selectors := range selectorSet { + hasMatch := false + for _, externalLabels := range externalLabelSet { + hasMatch = hasMatch || matchesExternalLabels(selectors, externalLabels) + } + if !hasMatch { + return false + } + } + + return true +} + +// matchesExternalLabels returns false if given matchers are not matching external labels. +func matchesExternalLabels(ms []*labels.Matcher, externalLabels labels.Labels) bool { + if externalLabels.Len() == 0 { + return true + } + + for _, matcher := range ms { + extValue := externalLabels.Get(matcher.Name) + if extValue != "" && !matcher.Matches(extValue) { + return false + } + } + return true +} + +// hasDistributiveAncestor checks if there's a distributive node somewhere up the +// parent chain from the current node that can handle distribution. +// We must have an unbroken chain of distributive nodes to the ancestor for it to +// be able to handle distribution on our behalf. +func (m DistributedExecutionOptimizer) hasDistributiveAncestor(parents map[*Node]*Node, current *Node, engineLabels map[string]struct{}, warns *annotations.Annotations) bool { + for p := parents[current]; p != nil; p = parents[p] { + if !m.isDistributive(p, engineLabels, warns) { + // We hit a non-distributive node, so we can't push through it. + // No ancestor can help us distribute. + return false + } + } + // All ancestors are distributive, so the root (or the point where we + // stop traversing) can handle distribution. + return parents[current] != nil +} + +func maxTime(a, b time.Time) time.Time { + if a.After(b) { + return a + } + return b +} + +func maxDuration(a, b time.Duration) time.Duration { + if a > b { + return a + } + return b +} diff --git a/internal/promql-engine/logicalplan/distribute_test.go b/internal/promql-engine/logicalplan/distribute_test.go new file mode 100644 index 00000000000..497b69fce5b --- /dev/null +++ b/internal/promql-engine/logicalplan/distribute_test.go @@ -0,0 +1,1537 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package logicalplan + +import ( + "context" + "math" + "math/rand" + "regexp" + "testing" + "time" + + "github.com/thanos-io/promql-engine/api" + "github.com/thanos-io/promql-engine/query" + + "github.com/cortexproject/promqlsmith" + "github.com/efficientgo/core/testutil" + "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/promql" + "github.com/prometheus/prometheus/promql/parser" + "github.com/prometheus/prometheus/promql/promqltest" +) + +var replacements = map[string]*regexp.Regexp{ + " ": spaces, + "(": openParenthesis, + ")": closedParenthesis, +} + +func TestDistributedExecution(t *testing.T) { + t.Parallel() + cases := []struct { + name string + expr string + skipBinopPushdown bool + expectWarn bool + expected string + }{ + { + name: "binary with aggregation distributes entire expression", + expr: `sum(node_uname_info * group by (region) (server_running))`, + expected: ` +sum(dedup( + remote(sum by (region) (node_uname_info * group by (region) (server_running))), + remote(sum by (region) (node_uname_info * group by (region) (server_running)))))`, + }, + { + name: "nested binary with multiple aggregations", + expr: `sum(metric_a * sum by (region, pod) (metric_b) + metric_c)`, + expected: ` +sum(dedup( + remote(sum by (region) (metric_a * sum by (region, pod) (metric_b) + metric_c)), + remote(sum by (region) (metric_a * sum by (region, pod) (metric_b) + metric_c))))`, + }, + { + name: "max over binary with nested group", + expr: `max(metric_a / group by (region) (metric_b))`, + expected: ` +max(dedup( + remote(max by (region) (metric_a / group by (region) (metric_b))), + remote(max by (region) (metric_a / group by (region) (metric_b)))))`, + }, + { + name: "count over binary with nested sum", + expr: `count(metric_a + sum by (region, pod) (metric_b))`, + expected: ` +sum(dedup( + remote(count by (region) (metric_a + sum by (region, pod) (metric_b))), + remote(count by (region) (metric_a + sum by (region, pod) (metric_b)))))`, + }, + { + name: "avg nested inside sum does not defer", + expr: `sum(avg by (pod) (metric_a))`, + expected: ` +sum( + sum by (pod) (dedup( + remote(sum by (pod, region) (metric_a)), + remote(sum by (pod, region) (metric_a)))) + / on (pod) + sum by (pod) (dedup( + remote(count by (pod, region) (metric_a)), + remote(count by (pod, region) (metric_a)))))`, + }, + { + name: "avg in binary expression with outer sum pushes entire expression", + expr: `sum(metric_a * avg by (region) (metric_b))`, + expected: ` +sum( + dedup( + remote(sum by (region) (metric_a * avg by (region) (metric_b))), + remote(sum by (region) (metric_a * avg by (region) (metric_b)))))`, + }, + { + name: "selector", + expr: `http_requests_total`, + expected: `dedup(remote(http_requests_total), remote(http_requests_total))`, + }, + { + name: "parentheses", + expr: `(http_requests_total)`, + expected: `dedup(remote((http_requests_total)), remote((http_requests_total)))`, + }, + { + name: "scalar", + expr: `scalar(redis::shard_price_per_month)`, + expected: `scalar(dedup(remote(redis::shard_price_per_month), remote(redis::shard_price_per_month)))`, + }, + { + name: "rate", + expr: `rate(http_requests_total[5m])`, + expected: `dedup(remote(rate(http_requests_total[5m])), remote(rate(http_requests_total[5m])))`, + }, + { + name: "sum-rate", + expr: `sum by (pod) (rate(http_requests_total[5m]))`, + expected: ` +sum by (pod) (dedup( + remote(sum by (pod, region) (rate(http_requests_total[5m]))), + remote(sum by (pod, region) (rate(http_requests_total[5m])))))`, + }, + { + name: "sum-rate without labels preserves engine labels", + expr: `sum without (pod, region) (rate(http_requests_total[5m]))`, + expected: ` +sum without (pod, region) ( + dedup( + remote(sum without (pod) (rate(http_requests_total[5m]))), + remote(sum without (pod) (rate(http_requests_total[5m]))) + ) +)`, + }, + { + name: "avg", + expr: `avg(http_requests_total)`, + expected: ` +sum( + dedup( + remote(sum by (region) (http_requests_total)), + remote(sum by (region) (http_requests_total)) + ) +) / on () +sum( + dedup( + remote(count by (region) (http_requests_total)), + remote(count by (region) (http_requests_total)) + ) +)`, + }, + { + name: "avg with by-grouping", + expr: `avg by (pod) (http_requests_total)`, + expected: ` +sum by (pod) ( + dedup( + remote(sum by (pod, region) (http_requests_total)), + remote(sum by (pod, region) (http_requests_total)) + ) +) / on (pod) +sum by (pod) ( + dedup( + remote(count by (pod, region) (http_requests_total)), + remote(count by (pod, region) (http_requests_total)) + ) +)`, + }, + { + name: "avg with without-grouping preserving partition labels", + expr: `avg without (pod) (http_requests_total)`, + expected: ` +dedup( + remote(avg without (pod) (http_requests_total)), + remote(avg without (pod) (http_requests_total)))`, + }, + { + name: "avg with prior aggregation", + expr: `avg by (pod) (sum by (pod) (http_requests_total))`, + expected: ` +avg by (pod) ( + sum by (pod) ( + dedup( + remote(sum by (pod, region) (http_requests_total)), + remote(sum by (pod, region) (http_requests_total)) + ) + ) +)`, + }, + { + name: "avg with prior binary expression", + expr: `avg by (pod) (metric_a / metric_b)`, + expected: ` +sum by (pod) ( + dedup( + remote(sum by (pod, region) (metric_a / metric_b)), + remote(sum by (pod, region) (metric_a / metric_b)) + ) +) +/ on (pod) +sum by (pod) ( + dedup( + remote(count by (pod, region) (metric_a / metric_b)), + remote(count by (pod, region) (metric_a / metric_b)) + ) +)`, + }, + { + name: "avg by partition label pushes as-is", + expr: `avg by (region) (http_requests_total)`, + expected: ` +dedup( + remote(avg by (region) (http_requests_total)), + remote(avg by (region) (http_requests_total)))`, + }, + { + name: "avg by partition label defers to distributive ancestor", + expr: `max(avg by (region) (http_requests_total))`, + expected: ` +max( + dedup( + remote(max by (region) (avg by (region) (http_requests_total))), + remote(max by (region) (avg by (region) (http_requests_total)))))`, + }, + { + name: "avg over subquery with inner aggregations pushes entire expression", + expr: `avg by (region) (quantile_over_time(0.9, (sum by (region) (rate(metric_a[2m])) / sum by (region) (metric_b))[1h:1m]))`, + expected: ` +dedup( + remote(avg by (region) (quantile_over_time(0.9, (sum by (region) (rate(metric_a[2m])) / sum by (region) (metric_b))[1h:1m]))), + remote(avg by (region) (quantile_over_time(0.9, (sum by (region) (rate(metric_a[2m])) / sum by (region) (metric_b))[1h:1m]))))`, + }, + { + name: "quantile by partition label pushes as-is", + expr: `quantile by (region) (0.9, http_requests_total)`, + expected: ` +dedup( + remote(quantile by (region) (0.9, http_requests_total)), + remote(quantile by (region) (0.9, http_requests_total)))`, + }, + { + name: "quantile by non-partition label is not distributed", + expr: `quantile by (pod) (0.9, http_requests_total)`, + expected: `quantile by (pod) (0.9, dedup(remote(http_requests_total), remote(http_requests_total)))`, + }, + { + name: "stddev by partition label pushes as-is", + expr: `stddev by (region) (http_requests_total)`, + expected: ` +dedup( + remote(stddev by (region) (http_requests_total)), + remote(stddev by (region) (http_requests_total)))`, + }, + { + name: "stddev by non-partition label is not distributed", + expr: `stddev by (pod) (http_requests_total)`, + expected: `stddev by (pod) (dedup(remote(http_requests_total), remote(http_requests_total)))`, + }, + { + name: "two-level aggregation", + expr: `max by (pod) (sum by (pod) (http_requests_total))`, + expected: ` +max by (pod) ( + sum by (pod) ( + dedup( + remote(sum by (pod, region) (http_requests_total)), + remote(sum by (pod, region) (http_requests_total)) + ) + ) +)`, + }, + { + name: "aggregation of binary expression", + expr: `max by (pod) (metric_a / metric_b)`, + expected: ` +max by (pod) ( + dedup( + remote(max by (pod, region) (metric_a / metric_b)), + remote(max by (pod, region) (metric_a / metric_b)) + ) +) +`, + }, + { + name: "unsupported aggregation in the operand path", + expr: `max by (pod) (sort(quantile(0.9, http_requests_total)))`, + expected: ` +max by (pod) (quantile(0.9, + dedup( + remote(http_requests_total), + remote(http_requests_total) + ) +))`, + }, + { + name: "label replace", + expr: `label_replace(http_requests_total, "pod", "$1", "instance", "(.*)")`, + expected: ` +dedup( + remote(label_replace(http_requests_total, "pod", "$1", "instance", "(.*)")), + remote(label_replace(http_requests_total, "pod", "$1", "instance", "(.*)")) +)`, + }, + { + name: "label replace to internal label before an aggregation", + expr: `max by (instance) (label_replace(http_requests_total, "pod", "$1", "instance", "(.*)"))`, + expected: ` +max by (instance) ( + dedup( + remote(max by (instance, region) (label_replace(http_requests_total, "pod", "$1", "instance", "(.*)"))), + remote(max by (instance, region) (label_replace(http_requests_total, "pod", "$1", "instance", "(.*)"))) + ) +)`, + }, + { + name: "label replace to internal label before an aggregation", + expr: `max by (location) (label_replace(http_requests_total, "zone", "$1", "location", "(.*)"))`, + expected: ` +max by (location) (dedup( + remote(max by (location, region) (label_replace(http_requests_total, "zone", "$1", "location", "(.*)"))), + remote(max by (location, region) (label_replace(http_requests_total, "zone", "$1", "location", "(.*)"))) +))`, + }, + { + name: "label replace to external label before an aggregation", + expr: `max by (location) (label_replace(http_requests_total, "region", "$1", "location", "(.*)"))`, + expected: `max by (location) (label_replace(dedup(remote(http_requests_total), remote(http_requests_total)), "region", "$1", "location", "(.*)"))`, + expectWarn: true, + }, + { + name: "label replace to external label before an avg", + expr: `avg by (location) (label_replace(http_requests_total, "region", "$1", "location", "(.*)"))`, + expected: `avg by (location) (label_replace(dedup(remote(http_requests_total), remote(http_requests_total)), "region", "$1", "location", "(.*)"))`, + expectWarn: true, + }, + { + name: "label replace to internal label before an avg", + expr: `avg by (location) (label_replace(http_requests_total, "zone", "$1", "location", "(.*)"))`, + expected: ` +sum by (location) ( + dedup( + remote(sum by (location, region) (label_replace(http_requests_total, "zone", "$1", "location", "(.*)"))), + remote(sum by (location, region) (label_replace(http_requests_total, "zone", "$1", "location", "(.*)"))))) + / on (location) +sum by (location) ( + dedup( + remote(count by (location, region) (label_replace(http_requests_total, "zone", "$1", "location", "(.*)"))), + remote(count by (location, region) (label_replace(http_requests_total, "zone", "$1", "location", "(.*)"))))) +`, + }, + { + name: "label replace after an aggregation", + expr: `label_replace(max by (location) (http_requests_total), "region", "$1", "location", "(.*)")`, + expected: ` +label_replace(max by (location) (dedup( + remote(max by (location, region) (http_requests_total)), + remote(max by (location, region) (http_requests_total)) +)), "region", "$1", "location", "(.*)")`, + expectWarn: true, + }, + { + name: "label join targeting non-partition label distributes", + expr: `label_join(http_requests_total, "zone", ",", "pod")`, + expected: ` +dedup( + remote(label_join(http_requests_total, "zone", ",", "pod")), + remote(label_join(http_requests_total, "zone", ",", "pod")))`, + }, + { + name: "label join targeting partition label does not distribute", + expr: `max by (location) (label_join(http_requests_total, "region", ",", "pod"))`, + expected: `max by (location) (label_join(dedup(remote(http_requests_total), remote(http_requests_total)), "region", ",", "pod"))`, + expectWarn: true, + }, + { + name: "binary operation in the operand path", + expr: `max by (pod) (metric_a / metric_b)`, + expected: ` +max by (pod) ( + dedup( + remote(max by (pod, region) (metric_a / metric_b)), + remote(max by (pod, region) (metric_a / metric_b)) + ) +) +`, + }, + { + name: "binary operation with aggregations", + expr: `sum by (pod) (metric_a) / sum by (pod) (metric_b)`, + expected: ` +sum by (pod) (dedup( + remote(sum by (pod, region) (metric_a)), + remote(sum by (pod, region) (metric_a))) +) +/ +sum by (pod) (dedup( + remote(sum by (pod, region) (metric_b)), + remote(sum by (pod, region) (metric_b)) +))`, + }, + { + name: "binary operation with aggregations preserving partition labels distributes entire expression", + expr: `sum by (region, pod) (metric_a) / count by (region, pod) (metric_b)`, + expected: ` +dedup( + remote(sum by (region, pod) (metric_a) / count by (region, pod) (metric_b)), + remote(sum by (region, pod) (metric_a) / count by (region, pod) (metric_b)) +)`, + }, + { + name: "function sharding", + expr: `rate(http_requests_total[2m])`, + expected: ` +dedup( + remote(rate(http_requests_total[2m])), + remote(rate(http_requests_total[2m])) +)`, + }, + { + name: "top level function with no args", + expr: `pi()`, + expected: `pi()`, + }, + { + name: "binary expression with no arg functions", + expr: `time() - pi()`, + expected: `time() - pi()`, + }, + { + name: `histogram quantile`, + expr: `histogram_quantile(0.5, sum by (le) (rate(coredns_dns_request_duration_seconds_bucket[5m])))`, + expected: ` +histogram_quantile(0.5, sum by (le) (dedup( + remote(sum by (le, region) (rate(coredns_dns_request_duration_seconds_bucket[5m]))), + remote(sum by (le, region) (rate(coredns_dns_request_duration_seconds_bucket[5m]))) +)))`, + }, + { + name: "binary expression with time", + expr: `time() - max by (foo) (bar)`, + expected: `time() - max by (foo) (dedup(remote(max by (foo, region) (bar)), remote(max by (foo, region) (bar))))`, + }, + { + name: "number literal", + expr: `1`, + expected: `1`, + }, + { + name: "aggregation with number literal", + expr: `max(foo) - 1`, + expected: `max(dedup(remote(max by (region) (foo)), remote(max by (region) (foo)))) - 1`, + }, + { + name: "absent", + expr: `absent(foo)`, + expected: `remote(absent(foo)) * remote(absent(foo))`, + }, + { + name: "absent with aggregation", + expr: `sum(absent(foo))`, + expected: `sum(remote(absent(foo)) * remote(absent(foo)))`, + }, + { + name: "binary expression with constant", + expr: `sum by (pod) (rate(http_requests_total[2m]) * 60)`, + expected: `sum by (pod) (dedup( +remote(sum by (pod, region) (rate(http_requests_total[2m]) * 60)), +remote(sum by (pod, region) (rate(http_requests_total[2m]) * 60))))`, + }, + { + name: "binary expression with no arg function", + expr: `time() - last_update_timestamp`, + expected: `time() - dedup(remote(last_update_timestamp), remote(last_update_timestamp))`, + }, + { + name: "subquery", + expr: `sum_over_time(http_requests_total[5m:1m])`, + expected: `dedup(remote(sum_over_time(http_requests_total[5m:1m])), remote(sum_over_time(http_requests_total[5m:1m])))`, + }, + { + name: "subquery over range function", + expr: `sum_over_time(rate(http_requests_total[5m])[5m:1m])`, + expected: `dedup(remote(sum_over_time(rate(http_requests_total[5m])[5m:1m])), remote(sum_over_time(rate(http_requests_total[5m])[5m:1m])))`, + }, + { + name: "subquery over range aggregation", + expr: `sum_over_time(max(http_requests_total)[5m:1m])`, + expected: ` +sum_over_time(max(dedup( + remote(max by (region) (http_requests_total)) [1969-12-31 23:55:00 +0000 UTC, 1970-01-01 00:00:00 +0000 UTC], + remote(max by (region) (http_requests_total)) [1969-12-31 23:55:00 +0000 UTC, 1970-01-01 00:00:00 +0000 UTC]) +)[5m:1m])`, + }, + { + name: "label based pruning matches one engine", + expr: `sum by (pod) (rate(http_requests_total{region="west"}[2m]))`, + expected: `sum by (pod) (dedup(remote(sum by (pod, region) (rate(http_requests_total{region="west"}[2m])))))`, + }, + { + name: "label based pruning matches no engines", + expr: `http_requests_total{region="north"}`, + expected: `noop`, + }, + { + name: "label based pruning with grouping matches no engines", + expr: `sum by (pod) (rate(http_requests_total{region="north"}[2m]))`, + expected: `sum by (pod) (noop)`, + }, + { + name: "label based pruning with grouping matches single engine", + expr: `sum by (pod) (rate(http_requests_total{region="south"}[2m]))`, + expected: `sum by (pod) (dedup(remote(sum by (pod, region) (rate(http_requests_total{region="south"}[2m])))))`, + }, + { + name: "binary matching where hash contains partitioning label with on", + expr: `X * on (region) Y`, + expected: `dedup(remote(X * on (region) Y), remote(X * on (region) Y))`, + }, + { + name: "binary matching where hash contains partitioning label with ignoring", + expr: `X * ignoring (foo) Y`, + expected: `dedup(remote(X * ignoring (foo) Y), remote(X * ignoring (foo) Y))`, + }, + { + name: "binary matching where hash doesnt contain partitioning label with ignoring", + expr: `X * ignoring (region) Y`, + expected: `dedup(remote(X), remote(X)) * ignoring (region) dedup(remote(Y), remote(Y))`, + }, + { + name: "binary matching where hash doesnt contain partitioning label with on", + expr: `X * on (foo) Y`, + expected: `dedup(remote(X), remote(X)) * on (foo) dedup(remote(Y), remote(Y))`, + }, + + { + name: "binary matching and label replace with local label", + expr: ` +count by (cluster) ( + label_replace(up, "ns", "$0", "namespace", ".*") + * on(region) group_left(project) label_replace(k8s_cluster_info, "k8s_cluster", "$0", "cluster", ".*") +)`, + expected: ` +sum by (cluster) (dedup( + remote(count by (cluster, region) (label_replace(up, "ns", "$0", "namespace", ".*") * on (region) group_left (project) label_replace(k8s_cluster_info, "k8s_cluster", "$0", "cluster", ".*"))), + remote(count by (cluster, region) (label_replace(up, "ns", "$0", "namespace", ".*") * on (region) group_left (project) label_replace(k8s_cluster_info, "k8s_cluster", "$0", "cluster", ".*")))) +)`, + }, + { + name: "binary matching and label replace with engine label", + expr: ` +count by (cluster) ( + label_replace(up, "region", "$0", "k8s_region", ".*") + * on(region) group_left(project) label_replace(k8s_cluster_info, "k8s_cluster", "$0", "cluster", ".*"))`, + expected: ` +count by (cluster) ( + label_replace(dedup(remote(up), remote(up)), "region", "$0", "k8s_region", ".*") + * on (region) group_left (project) dedup( + remote(label_replace(k8s_cluster_info, "k8s_cluster", "$0", "cluster", ".*")), + remote(label_replace(k8s_cluster_info, "k8s_cluster", "$0", "cluster", ".*")) + ) +)`, + expectWarn: true, + }, + { + name: "skip binary pushdown when configured", + expr: `metric_a / metric_b`, + expected: `dedup(remote(metric_a), remote(metric_a)) / dedup(remote(metric_b), remote(metric_b))`, + skipBinopPushdown: true, + }, + { + name: "skip binary pushdown with nested aggregation", + expr: `sum(metric_a * group by (region) (metric_b))`, + expected: `sum(dedup(remote(metric_a), remote(metric_a)) * dedup(remote(group by (region) (metric_b)), remote(group by (region) (metric_b))))`, + skipBinopPushdown: true, + }, + { + name: "skip binary pushdown with outer aggregation", + expr: `max(metric_a + sum by (region, pod) (metric_b))`, + expected: `max(dedup(remote(metric_a), remote(metric_a)) + dedup(remote(sum by (region, pod) (metric_b)), remote(sum by (region, pod) (metric_b))))`, + skipBinopPushdown: true, + }, + { + // When the RHS of unless has an aggregation that drops the partition label, + // both sides should still be distributed independently. + name: "unless with aggregation that drops partition label distributes both sides", + expr: `group by (region, instance) (metric_a unless on (region, instance) max by (instance) (metric_b))`, + expected: `group by (region, instance) (dedup(remote(metric_a), remote(metric_a)) unless on (region, instance) max by (instance) (dedup(remote(max by (instance, region) (metric_b)), remote(max by (instance, region) (metric_b)))))`, + }, + { + // group_left/group_right with partition label cannot be distributed because + // match cardinality changes when each partition only sees one value for that label. + name: "binary with group_left including partition label does not distribute", + expr: `max(metric_a * on (pod) group_left(region) metric_b)`, + expected: `max(dedup(remote(metric_a), remote(metric_a)) * on (pod) group_left (region) dedup(remote(metric_b), remote(metric_b)))`, + }, + { + name: "binary with group_right including partition label does not distribute", + expr: `max(metric_a * on (pod) group_right(region) metric_b)`, + expected: `max(dedup(remote(metric_a), remote(metric_a)) * on (pod) group_right (region) dedup(remote(metric_b), remote(metric_b)))`, + }, + { + name: "or distributes with default matching when both sides are global", + expr: `metric_a or metric_b`, + expected: `dedup(remote(metric_a or metric_b), remote(metric_a or metric_b))`, + }, + { + name: "unless distributes with default matching when both sides are global", + expr: `metric_a unless metric_b`, + expected: `dedup(remote(metric_a unless metric_b), remote(metric_a unless metric_b))`, + }, + { + name: "or distributes with on(partition_label)", + expr: `metric_a or on(region) metric_b`, + expected: `dedup(remote(metric_a or on (region) metric_b), remote(metric_a or on (region) metric_b))`, + }, + { + name: "unless distributes with on(partition_label)", + expr: `metric_a unless on(region) metric_b`, + expected: `dedup(remote(metric_a unless on (region) metric_b), remote(metric_a unless on (region) metric_b))`, + }, + { + name: "or does not distribute when on() excludes partition label", + expr: `metric_a or on(pod) metric_b`, + expected: `dedup(remote(metric_a), remote(metric_a)) or on (pod) dedup(remote(metric_b), remote(metric_b))`, + }, + { + name: "unless does not distribute when on() excludes partition label", + expr: `metric_a unless on(pod) metric_b`, + expected: `dedup(remote(metric_a), remote(metric_a)) unless on (pod) dedup(remote(metric_b), remote(metric_b))`, + }, + { + name: "or distributes with ignoring(non_partition_label)", + expr: `metric_a or ignoring(pod) metric_b`, + expected: `dedup(remote(metric_a or ignoring (pod) metric_b), remote(metric_a or ignoring (pod) metric_b))`, + }, + { + name: "or does not distribute when ignoring(partition_label)", + expr: `metric_a or ignoring(region) metric_b`, + expected: `dedup(remote(metric_a), remote(metric_a)) or ignoring (region) dedup(remote(metric_b), remote(metric_b))`, + }, + { + name: "or does not distribute with cross-partition selectors", + expr: `metric_a{region="east"} or metric_b{region="west"}`, + expected: `dedup(remote(metric_a{region="east"})) or dedup(remote(metric_b{region="west"}))`, + }, + { + name: "or distributes when both sides have same partition selector", + expr: `metric_a{region="east"} or metric_b{region="east"}`, + expected: `dedup(remote(metric_a{region="east"} or metric_b{region="east"}))`, + }, + { + name: "unless does not distribute with cross-partition selectors", + expr: `metric_a{region="east"} unless metric_b{region="west"}`, + expected: `dedup(remote(metric_a{region="east"})) unless dedup(remote(metric_b{region="west"}))`, + }, + { + name: "or does not distribute when one side is global and other has partition selector", + expr: `metric_a or metric_b{region="east"}`, + expected: `dedup(remote(metric_a), remote(metric_a)) or dedup(remote(metric_b{region="east"}))`, + }, + { + name: "or does not distribute with constant expression on right side", + expr: `metric_a or on () vector(0)`, + expected: `dedup(remote(metric_a), remote(metric_a)) or on () vector(0)`, + }, + { + name: "or does not distribute with constant expression on left side", + expr: `vector(1) or metric_b`, + expected: `vector(1) or dedup(remote(metric_b), remote(metric_b))`, + }, + { + name: "unless does not distribute with constant expression", + expr: `metric_a unless on () vector(0)`, + expected: `dedup(remote(metric_a), remote(metric_a)) unless on () vector(0)`, + }, + { + name: "max over sum by partition", + expr: `max(sum by (region, instance) (http_requests_total))`, + expected: `max(dedup(remote(max by (region) (sum by (region, instance) (http_requests_total))), remote(max by (region) (sum by (region, instance) (http_requests_total)))))`, + }, + { + name: "min over max by partition", + expr: `min(max by (region, pod) (cpu_usage))`, + expected: `min(dedup(remote(min by (region) (max by (region, pod) (cpu_usage))), remote(min by (region) (max by (region, pod) (cpu_usage)))))`, + }, + { + name: "max over sum without partition", + expr: `max(sum by (instance) (http_requests_total))`, + expected: `max(sum by (instance) (dedup(remote(sum by (instance, region) (http_requests_total)), remote(sum by (instance, region) (http_requests_total)))))`, + }, + { + name: "count over sum by partition", + expr: `count(sum by (region, pod) (http_requests_total))`, + expected: `sum(dedup(remote(count by (region) (sum by (region, pod) (http_requests_total))), remote(count by (region) (sum by (region, pod) (http_requests_total)))))`, + }, + { + name: "max over binary with on() including partition", + expr: `max(metric_a * on (region, pod) metric_b)`, + expected: `max(dedup(remote(max by (region) (metric_a * on (region, pod) metric_b)), remote(max by (region) (metric_a * on (region, pod) metric_b))))`, + }, + { + name: "max over binary with on() excluding partition", + expr: `max(metric_a * on (pod) metric_b)`, + expected: `max(dedup(remote(metric_a), remote(metric_a)) * on (pod) dedup(remote(metric_b), remote(metric_b)))`, + }, + { + name: "max over binary with ignoring() excluding partition", + expr: `max(metric_a * ignoring (pod) metric_b)`, + expected: `max(dedup(remote(max by (region) (metric_a * ignoring (pod) metric_b)), remote(max by (region) (metric_a * ignoring (pod) metric_b))))`, + }, + { + name: "max over binary with ignoring() including partition", + expr: `max(metric_a * ignoring (region) metric_b)`, + expected: `max(dedup(remote(metric_a), remote(metric_a)) * ignoring (region) dedup(remote(metric_b), remote(metric_b)))`, + }, + { + name: "max over sum with without() excluding partition", + expr: `max(sum without (pod) (metric_a))`, + expected: `max(dedup(remote(max by (region) (sum without (pod) (metric_a))), remote(max by (region) (sum without (pod) (metric_a)))))`, + }, + { + name: "max over sum with without() including partition", + expr: `max(sum without (region) (metric_a))`, + expected: `max(sum without (region) (dedup(remote(sum without () (metric_a)), remote(sum without () (metric_a)))))`, + }, + } + + engines := []api.RemoteEngine{ + newEngineMock(math.MinInt64, math.MaxInt64, []labels.Labels{labels.FromStrings("region", "east"), labels.FromStrings("region", "south")}), + newEngineMock(math.MinInt64, math.MaxInt64, []labels.Labels{labels.FromStrings("region", "west")}), + } + for _, tcase := range cases { + t.Run(tcase.name, func(t *testing.T) { + optimizers := []Optimizer{ + DistributedExecutionOptimizer{ + Endpoints: api.NewStaticEndpoints(engines), + SkipBinaryPushdown: tcase.skipBinopPushdown, + }, + } + + expr, err := parser.ParseExpr(tcase.expr) + testutil.Ok(t, err) + + plan, _ := NewFromAST(expr, &query.Options{Start: time.Unix(0, 0), End: time.Unix(0, 0)}, PlanOptions{}) + optimizedPlan, warns := plan.Optimize(optimizers) + expectedPlan := cleanUp(replacements, tcase.expected) + testutil.Equals(t, expectedPlan, optimizedPlan.Root().String()) + if tcase.expectWarn { + testutil.Assert(t, len(warns) > 0, "expected warnings, got none") + } else { + testutil.Assert(t, len(warns) == 0, "expected no warnings, got some") + } + }) + } +} + +type engineOpts struct { + minTime time.Time + maxTime time.Time +} + +func (o engineOpts) mint() int64 { + return o.minTime.UnixMilli() +} + +func (o engineOpts) maxt() int64 { + return o.maxTime.UnixMilli() +} + +func TestDistributedExecutionWithLongSelectorRanges(t *testing.T) { + sixHours := 6 * time.Hour + eightHours := 8 * time.Hour + twelveHours := 12 * time.Hour + + queryStart := time.Unix(0, 0) + queryEnd := time.Unix(0, 0).Add(twelveHours) + queryStep := time.Minute + + cases := []struct { + name string + expr string + expected string + firstEngineOpts engineOpts + secondEngineOpts engineOpts + }{ + { + name: "sum over 5m with non overlapping engine but second engine is not intersecting query range", + firstEngineOpts: engineOpts{ + minTime: queryEnd.Add(sixHours), + maxTime: queryEnd.Add(eightHours), + }, + secondEngineOpts: engineOpts{ + minTime: queryStart, + maxTime: queryEnd, + }, + expr: `sum_over_time(metric[5m])`, + expected: ` +dedup( + remote(sum_over_time(metric[5m])) +)`, + }, + { + name: "sum over 5m adds a 5 minute offset to latest engine", + firstEngineOpts: engineOpts{ + minTime: queryStart, + maxTime: time.Unix(0, 0).Add(eightHours), + }, + secondEngineOpts: engineOpts{ + minTime: time.Unix(0, 0).Add(sixHours), + maxTime: queryEnd, + }, + expr: `sum_over_time(metric[5m])`, + expected: ` +dedup( + remote(sum_over_time(metric[5m])), + remote(sum_over_time(metric[5m])) [1970-01-01 06:05:00 +0000 UTC, 1970-01-01 12:00:00 +0000 UTC] +)`, + }, + { + name: "sum over 2h adds a 2 hour offset to latest engine", + firstEngineOpts: engineOpts{ + minTime: queryStart, + maxTime: time.Unix(0, 0).Add(eightHours), + }, + secondEngineOpts: engineOpts{ + minTime: time.Unix(0, 0).Add(sixHours), + maxTime: queryEnd, + }, + expr: `sum_over_time(metric[2h])`, + expected: ` +dedup( + remote(sum_over_time(metric[2h])), + remote(sum_over_time(metric[2h])) [1970-01-01 08:00:00 +0000 UTC, 1970-01-01 12:00:00 +0000 UTC] +)`, + }, + { + name: "subquery with a total 2h range is distributed with proper offsets", + firstEngineOpts: engineOpts{ + minTime: queryStart, + maxTime: time.Unix(0, 0).Add(eightHours), + }, + secondEngineOpts: engineOpts{ + minTime: time.Unix(0, 0).Add(sixHours), + maxTime: queryEnd, + }, + expr: `sum_over_time(sum_over_time(metric[1h])[1h:30m])`, + expected: ` +dedup( + remote(sum_over_time(sum_over_time(metric[1h])[1h:30m])), + remote(sum_over_time(sum_over_time(metric[1h])[1h:30m])) [1970-01-01 08:00:00 +0000 UTC, 1970-01-01 12:00:00 +0000 UTC] +)`, + }, + { + name: "multiple subqueries with a total 90m range get distributed with proper offsets", + firstEngineOpts: engineOpts{ + minTime: queryStart, + maxTime: time.Unix(0, 0).Add(eightHours), + }, + secondEngineOpts: engineOpts{ + minTime: time.Unix(0, 0).Add(sixHours), + maxTime: queryEnd, + }, + expr: `max_over_time(sum_over_time(sum_over_time(metric[5m])[45m:10m])[15m:15m])`, + expected: `dedup( + remote(max_over_time(sum_over_time(sum_over_time(metric[5m])[45m:10m])[15m:15m])), + remote(max_over_time(sum_over_time(sum_over_time(metric[5m])[45m:10m])[15m:15m])) [1970-01-01 07:05:00 +0000 UTC, 1970-01-01 12:00:00 +0000 UTC])`, + }, + { + name: "subquery with a total 4h range is cannot be distributed", + firstEngineOpts: engineOpts{ + minTime: queryStart, + maxTime: time.Unix(0, 0).Add(eightHours), + }, + secondEngineOpts: engineOpts{ + minTime: time.Unix(0, 0).Add(sixHours), + maxTime: queryEnd, + }, + expr: `sum_over_time(sum_over_time(metric[2h])[2h:30m])`, + expected: `sum_over_time(sum_over_time(metric[2h])[2h:30m])`, + }, + { + name: "sum over 3h does not distribute the query due to insufficient engine overlap", + firstEngineOpts: engineOpts{ + minTime: queryStart, + maxTime: time.Unix(0, 0).Add(eightHours), + }, + secondEngineOpts: engineOpts{ + minTime: time.Unix(0, 0).Add(sixHours), + maxTime: queryEnd, + }, + expr: `sum_over_time(metric[3h])`, + expected: `sum_over_time(metric[3h])`, + }, + { + name: "distribute queries with timestamp", + firstEngineOpts: engineOpts{ + minTime: queryStart, + maxTime: time.Unix(0, 0).Add(eightHours), + }, + secondEngineOpts: engineOpts{ + minTime: time.Unix(0, 0).Add(sixHours), + maxTime: queryEnd, + }, + expr: `sum(metric @ 25200)`, + expected: ` +sum(dedup( + remote(sum by (region) (metric @ 25200.000)), + remote(sum by (region) (metric @ 25200.000)) [1970-01-01 06:00:00 +0000 UTC, 1970-01-01 12:00:00 +0000 UTC] +))`, + }, + { + name: "skip distributing queries with timestamps outside of the range of an engine", + firstEngineOpts: engineOpts{ + minTime: queryStart, + maxTime: time.Unix(0, 0).Add(eightHours), + }, + secondEngineOpts: engineOpts{ + minTime: time.Unix(0, 0).Add(sixHours), + maxTime: queryEnd, + }, + expr: `sum(metric @ 18000)`, + expected: `sum(sum by (region) (metric @ 18000.000))`, + }, + } + + for _, tcase := range cases { + t.Run(tcase.name, func(t *testing.T) { + engines := []api.RemoteEngine{ + newEngineMock(tcase.firstEngineOpts.mint(), tcase.firstEngineOpts.maxt(), []labels.Labels{labels.FromStrings("region", "east")}), + newEngineMock(tcase.secondEngineOpts.mint(), tcase.secondEngineOpts.maxt(), []labels.Labels{labels.FromStrings("region", "east")}), + } + optimizers := []Optimizer{ + DistributedExecutionOptimizer{Endpoints: api.NewStaticEndpoints(engines)}, + } + + expr, err := parser.ParseExpr(tcase.expr) + testutil.Ok(t, err) + + plan, _ := NewFromAST(expr, &query.Options{Start: queryStart, End: queryEnd, Step: queryStep}, PlanOptions{}) + optimizedPlan, _ := plan.Optimize(optimizers) + expectedPlan := cleanUp(replacements, tcase.expected) + testutil.Equals(t, expectedPlan, optimizedPlan.Root().String()) + }) + } +} + +func TestDistributedExecutionPruningByTime(t *testing.T) { + firstEngineOpts := engineOpts{ + minTime: time.Unix(0, 0), + maxTime: time.Unix(0, 0).Add(6 * time.Hour), + } + secondEngineOpts := engineOpts{ + minTime: time.Unix(0, 0).Add(4 * time.Hour), + maxTime: time.Unix(0, 0).Add(8 * time.Hour), + } + + cases := []struct { + name string + expr string + expected string + queryStart time.Time + queryEnd time.Time + }{ + { + name: "1 hour query at the end of the range prunes the first engine", + expr: `sum(metric)`, + queryStart: time.Unix(0, 0).Add(7 * time.Hour), + queryEnd: time.Unix(0, 0).Add(8 * time.Hour), + expected: `sum(dedup(remote(sum by (region) (metric)) [1970-01-01 07:00:00 +0000 UTC, 1970-01-01 08:00:00 +0000 UTC]))`, + }, + { + name: "1 hour range query at the start of the range prunes the second engine", + expr: `sum(metric)`, + queryStart: time.Unix(0, 0).Add(1 * time.Hour), + queryEnd: time.Unix(0, 0).Add(2 * time.Hour), + expected: `sum(dedup(remote(sum by (region) (metric)) [1970-01-01 01:00:00 +0000 UTC, 1970-01-01 02:00:00 +0000 UTC]))`, + }, + { + name: "instant query in the overlapping range queries both engines", + expr: `sum(metric)`, + queryStart: time.Unix(0, 0).Add(6 * time.Hour), + queryEnd: time.Unix(0, 0).Add(6 * time.Hour), + expected: ` +sum( + dedup( + remote(sum by (region) (metric)) [1970-01-01 06:00:00 +0000 UTC, 1970-01-01 06:00:00 +0000 UTC], + remote(sum by (region) (metric)) [1970-01-01 06:00:00 +0000 UTC, 1970-01-01 06:00:00 +0000 UTC] + ) +)`, + }, + } + + for _, tcase := range cases { + t.Run(tcase.name, func(t *testing.T) { + engines := []api.RemoteEngine{ + newEngineMock(firstEngineOpts.mint(), firstEngineOpts.maxt(), []labels.Labels{labels.FromStrings("region", "east")}), + newEngineMock(secondEngineOpts.mint(), secondEngineOpts.maxt(), []labels.Labels{labels.FromStrings("region", "east")}), + } + optimizers := []Optimizer{ + DistributedExecutionOptimizer{Endpoints: api.NewStaticEndpoints(engines)}, + } + + expr, err := parser.ParseExpr(tcase.expr) + testutil.Ok(t, err) + + plan, _ := NewFromAST(expr, &query.Options{Start: tcase.queryStart, End: tcase.queryEnd, Step: time.Minute}, PlanOptions{}) + optimizedPlan, _ := plan.Optimize(optimizers) + expectedPlan := cleanUp(replacements, tcase.expected) + testutil.Equals(t, expectedPlan, renderExprTree(optimizedPlan.Root())) + }) + } +} + +func TestDistributedExecutionPruningByLabelset(t *testing.T) { + cases := []struct { + name string + expr string + expected string + }{ + { + name: "querying by labelsets restricts to partition that matches that labelset", + expr: `sum by (pod) (rate(http_requests_total{region="west"}[2m]))`, + expected: `sum by (pod) (dedup(remote(sum by (datacenter, pod) (rate(http_requests_total{region="west"}[2m])))))`, + }, + { + name: "querying by labelsets restricts to partition that matches that labelset", + expr: `sum by (pod) (rate(http_requests_total{region="east"}[2m]))`, + expected: ` +sum by (pod) (dedup( + remote(sum by (datacenter, pod) (rate(http_requests_total{region="east"}[2m]))), + remote(sum by (datacenter, pod) (rate(http_requests_total{region="east"}[2m]))) +))`, + }, + } + + for _, tcase := range cases { + t.Run(tcase.name, func(t *testing.T) { + // We are partitioned by datacenter but also have a "region" label that we can target + engines := []api.RemoteEngine{ + newEngineMockWithExplicitPartition( + math.MinInt64, + math.MaxInt64, + []labels.Labels{labels.FromStrings("region", "east", "datacenter", "east-1")}, + []labels.Labels{labels.FromStrings("datacenter", "east-1")}, + ), + newEngineMockWithExplicitPartition( + math.MinInt64, + math.MaxInt64, + []labels.Labels{labels.FromStrings("region", "east", "datacenter", "east-2")}, + []labels.Labels{labels.FromStrings("datacenter", "east-2")}, + ), + newEngineMockWithExplicitPartition( + math.MinInt64, + math.MaxInt64, + []labels.Labels{labels.FromStrings("region", "west", "datacenter", "west-1")}, + []labels.Labels{labels.FromStrings("datacenter", "west-1")}, + ), + } + optimizers := []Optimizer{ + DistributedExecutionOptimizer{Endpoints: api.NewStaticEndpoints(engines)}, + } + + expr, err := parser.ParseExpr(tcase.expr) + testutil.Ok(t, err) + + plan, err := NewFromAST(expr, &query.Options{Start: time.Unix(0, 0), End: time.Unix(0, 0)}, PlanOptions{}) + testutil.Ok(t, err) + optimizedPlan, _ := plan.Optimize(optimizers) + expectedPlan := cleanUp(replacements, tcase.expected) + testutil.Equals(t, expectedPlan, renderExprTree(optimizedPlan.Root())) + }) + } +} + +func TestDistributedExecutionMultiplePartitionLabels(t *testing.T) { + // Engines partitioned by both region and datacenter. + engines := []api.RemoteEngine{ + newEngineMock(math.MinInt64, math.MaxInt64, []labels.Labels{labels.FromStrings("region", "east", "datacenter", "dc1")}), + newEngineMock(math.MinInt64, math.MaxInt64, []labels.Labels{labels.FromStrings("region", "west", "datacenter", "dc2")}), + } + optimizers := []Optimizer{ + DistributedExecutionOptimizer{Endpoints: api.NewStaticEndpoints(engines)}, + } + + cases := []struct { + name string + expr string + expected string + }{ + { + name: "on() must include all partition labels to distribute", + expr: `metric_a + on (region) metric_b`, + expected: ` +dedup(remote(metric_a), remote(metric_a)) ++ on (region) +dedup(remote(metric_b), remote(metric_b))`, + }, + { + name: "on() with all partition labels distributes the binary", + expr: `metric_a + on (region, datacenter) metric_b`, + expected: ` +dedup( + remote(metric_a + on (region, datacenter) metric_b), + remote(metric_a + on (region, datacenter) metric_b))`, + }, + { + name: "ignoring() must not include any partition label to distribute", + expr: `metric_a + ignoring (region) metric_b`, + expected: ` +dedup(remote(metric_a), remote(metric_a)) ++ ignoring (region) +dedup(remote(metric_b), remote(metric_b))`, + }, + { + name: "ignoring() non-partition label distributes the binary", + expr: `metric_a + ignoring (pod) metric_b`, + expected: ` +dedup( + remote(metric_a + ignoring (pod) metric_b), + remote(metric_a + ignoring (pod) metric_b))`, + }, + { + name: "sum must include all partition labels to preserve", + expr: `sum by (region) (metric_a)`, + expected: ` +sum by (region) ( + dedup( + remote(sum by (datacenter, region) (metric_a)), + remote(sum by (datacenter, region) (metric_a))))`, + }, + { + name: "sum by all partition labels preserves", + expr: `max(sum by (region, datacenter) (metric_a))`, + expected: ` +max( + dedup( + remote(max by (datacenter, region) (sum by (region, datacenter) (metric_a))), + remote(max by (datacenter, region) (sum by (region, datacenter) (metric_a)))))`, + }, + } + + for _, tcase := range cases { + t.Run(tcase.name, func(t *testing.T) { + expr, err := parser.ParseExpr(tcase.expr) + testutil.Ok(t, err) + + plan, err := NewFromAST(expr, &query.Options{Start: time.Unix(0, 0), End: time.Unix(0, 0)}, PlanOptions{}) + testutil.Ok(t, err) + optimizedPlan, _ := plan.Optimize(optimizers) + expectedPlan := cleanUp(replacements, tcase.expected) + testutil.Equals(t, expectedPlan, renderExprTree(optimizedPlan.Root())) + }) + } +} + +func TestDistributedExecutionClonesNodes(t *testing.T) { + var ( + start = time.Unix(0, 0) + end = time.Unix(0, 0).Add(6 * time.Hour) + step = time.Second + expected = ` +sum(dedup( + remote(sum by (region) (metric{region="east"})), + remote(sum by (region) (metric{region="east"})) +))` + ) + expr, err := parser.ParseExpr(`sum(metric{region="east"})`) + testutil.Ok(t, err) + + engines := []api.RemoteEngine{ + newEngineMock(math.MinInt64, math.MaxInt64, []labels.Labels{labels.FromStrings("region", "east")}), + newEngineMock(math.MinInt64, math.MaxInt64, []labels.Labels{labels.FromStrings("region", "east")}), + } + + lplan, _ := NewFromAST(expr, &query.Options{Start: start, End: end, Step: step}, PlanOptions{}) + optimizedPlan, _ := lplan.Optimize([]Optimizer{ + DistributedExecutionOptimizer{Endpoints: api.NewStaticEndpoints(engines)}, + }) + + newMatcher := labels.MustNewMatcher(labels.MatchEqual, "region", "west") + // Modify the original expression to ensure that changes to not leak into the optimized plan. + originalVS := expr.(*parser.AggregateExpr).Expr.(*parser.VectorSelector) + originalVS.LabelMatchers = append(originalVS.LabelMatchers, newMatcher) + + expectedPlan := cleanUp(replacements, expected) + testutil.Equals(t, expectedPlan, renderExprTree(optimizedPlan.Root())) + + getSelector := func(i int) *VectorSelector { + return optimizedPlan.Root().(*CheckDuplicateLabels).Expr.(*Aggregation).Expr.(Deduplicate).Expressions[i].Query.(*Aggregation).Expr.(*VectorSelector) + } + + // Assert that modifying one subquery does not affect the other one. + vs0 := getSelector(0) + vs0.LabelMatchers = append(vs0.LabelMatchers, newMatcher) + + vs1 := getSelector(1) + testutil.Assert(t, len(vs1.LabelMatchers) == len(vs0.LabelMatchers)-1, "expected %d label matchers, got %d", len(vs0.LabelMatchers)-1, len(vs1.LabelMatchers)) +} + +type engineMock struct { + api.RemoteEngine + minT int64 + maxT int64 + labelSets []labels.Labels + partitionLabelSets []labels.Labels +} + +func (e engineMock) MaxT() int64 { + return e.maxT +} + +func (e engineMock) MinT() int64 { + return e.minT +} + +func (e engineMock) LabelSets() []labels.Labels { + return e.labelSets +} + +func (e engineMock) PartitionLabelSets() []labels.Labels { + return e.partitionLabelSets +} + +func newEngineMock(mint, maxt int64, labelSets []labels.Labels) *engineMock { + return &engineMock{minT: mint, maxT: maxt, labelSets: labelSets, partitionLabelSets: labelSets} +} + +func newEngineMockWithExplicitPartition(mint, maxt int64, labelSets, partitionLabelSets []labels.Labels) *engineMock { + return &engineMock{minT: mint, maxT: maxt, labelSets: labelSets, partitionLabelSets: partitionLabelSets} +} + +func TestPreservesPartitionLabels(t *testing.T) { + partitionLabels := map[string]struct{}{"region": {}} + + parse := func(t *testing.T, expr string) Node { + t.Helper() + parsed, err := parser.ParseExpr(expr) + testutil.Ok(t, err) + plan, err := NewFromAST(parsed, &query.Options{ + Start: time.Unix(0, 0), + End: time.Unix(0, 0), + }, PlanOptions{}) + testutil.Ok(t, err) + return plan.Root() + } + + cases := []struct { + name string + expr string + partitionLabels map[string]struct{} + expected bool + }{ + { + name: "vector selector preserves", + expr: `metric`, + expected: true, + }, + { + name: "number literal preserves", + expr: `1`, + expected: true, + }, + { + name: "sum by partition label preserves", + expr: `sum by (region) (metric)`, + expected: true, + }, + { + name: "sum by non-partition label does not preserve", + expr: `sum by (pod) (metric)`, + expected: false, + }, + { + name: "sum by both labels preserves", + expr: `sum by (pod, region) (metric)`, + expected: true, + }, + { + name: "sum without partition label does not preserve", + expr: `sum without (region) (metric)`, + expected: false, + }, + { + name: "sum without non-partition label preserves", + expr: `sum without (pod) (metric)`, + expected: true, + }, + { + name: "sum with no grouping does not preserve", + expr: `sum(metric)`, + expected: false, + }, + { + name: "binary with on(partition) preserves", + expr: `metric_a + on (region) metric_b`, + expected: true, + }, + { + name: "binary with on(non-partition) does not preserve", + expr: `metric_a + on (pod) metric_b`, + expected: false, + }, + { + name: "binary with ignoring(partition) does not preserve", + expr: `metric_a + ignoring (region) metric_b`, + expected: false, + }, + { + name: "binary with ignoring(non-partition) preserves", + expr: `metric_a + ignoring (pod) metric_b`, + expected: true, + }, + { + name: "binary with default matching preserves", + expr: `metric_a + metric_b`, + expected: true, + }, + { + name: "binary with partition in group_left include preserves", + expr: `metric_a * on (pod) group_left(region) metric_b`, + expected: true, + }, + { + name: "unary preserves", + expr: `-metric`, + expected: true, + }, + { + name: "subquery preserves", + expr: `max_over_time(metric[5m:1m])`, + expected: true, + }, + { + name: "label_replace targeting partition label does not preserve", + expr: `label_replace(metric, "region", "$1", "pod", "(.*)")`, + expected: false, + }, + { + name: "label_replace targeting non-partition label preserves", + expr: `label_replace(metric, "zone", "$1", "pod", "(.*)")`, + expected: true, + }, + { + name: "label_join targeting partition label does not preserve", + expr: `label_join(metric, "region", ",", "pod")`, + expected: false, + }, + { + name: "label_join targeting non-partition label preserves", + expr: `label_join(metric, "zone", ",", "pod")`, + expected: true, + }, + { + name: "rate preserves", + expr: `rate(metric[5m])`, + expected: true, + }, + { + name: "nested sum by(region)(sum by(pod)(X)) preserves at top level", + expr: `sum by (region) (sum by (pod) (metric))`, + expected: true, + }, + { + name: "nested sum by(pod)(sum by(region)(X)) does not preserve", + expr: `sum by (pod) (sum by (region) (metric))`, + expected: false, + }, + { + name: "binary with scalar preserves", + expr: `metric / 1000`, + expected: true, + }, + { + name: "avg by partition label preserves", + expr: `avg by (region) (metric)`, + expected: true, + }, + { + name: "avg by non-partition label does not preserve", + expr: `avg by (pod) (metric)`, + expected: false, + }, + { + name: "empty partition labels returns false", + expr: `metric`, + partitionLabels: map[string]struct{}{}, + expected: false, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + pl := partitionLabels + if tc.partitionLabels != nil { + pl = tc.partitionLabels + } + result := preservesPartitionLabels(parse(t, tc.expr), pl) + testutil.Equals(t, tc.expected, result) + }) + } +} + +func FuzzDistributedExecutionPreservesPartitionLabels(f *testing.F) { + f.Add(int64(0)) + f.Fuzz(func(t *testing.T, seed int64) { + rnd := rand.New(rand.NewSource(seed)) + + load := `load 30s + http_requests_total{pod="nginx-1", region="east"} 1+1x15 + http_requests_total{pod="nginx-2", region="east"} 2+2x15 + http_requests_total{pod="nginx-1", region="west"} 3+1x15 + http_requests_total{pod="nginx-2", region="west"} 4+2x15` + + testStorage := promqltest.LoadedStorage(t, load) + defer testStorage.Close() + + engines := []api.RemoteEngine{ + newEngineMock(math.MinInt64, math.MaxInt64, []labels.Labels{labels.FromStrings("region", "east")}), + newEngineMock(math.MinInt64, math.MaxInt64, []labels.Labels{labels.FromStrings("region", "west")}), + } + optimizers := []Optimizer{ + DistributedExecutionOptimizer{Endpoints: api.NewStaticEndpoints(engines)}, + } + + lbls := []labels.Labels{ + labels.FromStrings("__name__", "http_requests_total", "pod", "nginx-1", "region", "east"), + labels.FromStrings("__name__", "http_requests_total", "pod", "nginx-2", "region", "west"), + } + + // Exclude functions that produce unlabeled series by design. + enabledFunctions := make([]*parser.Function, 0, len(parser.Functions)) + for _, f := range parser.Functions { + switch f.Name { + case "vector", "absent", "absent_over_time": + continue + } + enabledFunctions = append(enabledFunctions, f) + } + + psOpts := []promqlsmith.Option{ + promqlsmith.WithEnableOffset(false), + promqlsmith.WithEnableAtModifier(false), + promqlsmith.WithEnabledAggrs([]parser.ItemType{ + parser.SUM, parser.MIN, parser.MAX, parser.AVG, parser.GROUP, + parser.COUNT, parser.QUANTILE, parser.STDDEV, parser.STDVAR, + parser.COUNT_VALUES, parser.TOPK, parser.BOTTOMK, + }), + promqlsmith.WithEnabledFunctions(enabledFunctions), + promqlsmith.WithEnableVectorMatching(true), + } + ps := promqlsmith.New(rnd, lbls, psOpts...) + + ng := promql.NewEngine(promql.EngineOpts{ + Timeout: 1 * time.Hour, + MaxSamples: 1e10, + EnableNegativeOffset: true, + EnableAtModifier: true, + }) + + start := time.Unix(60, 0) + end := time.Unix(120, 0) + step := 30 * time.Second + + opts := &query.Options{ + Start: start, + End: end, + Step: step, + } + for range testRuns { + expr := ps.WalkRangeQuery() + exprStr := expr.Pretty(0) + + parsed, err := parser.ParseExpr(exprStr) + if err != nil { + continue + } + + plan, err := NewFromAST(parsed, opts, PlanOptions{}) + if err != nil { + continue + } + + optimizedPlan, _ := plan.Optimize(optimizers) + root := optimizedPlan.Root() + + // For each remote query in the optimized plan, execute it + // against the test storage and verify that all result series + // still have the partition label "region". + Traverse(&root, func(node *Node) { + remote, ok := (*node).(RemoteExecution) + if !ok { + return + } + + // Skip remote queries that don't touch real series data + // (e.g. scalar parameters to aggregations like quantile). + hasSelector := false + var remoteNode Node = remote.Query + Traverse(&remoteNode, func(n *Node) { + switch (*n).(type) { + case *VectorSelector, *MatrixSelector: + hasSelector = true + } + }) + if !hasSelector { + return + } + + remoteQuery := remote.Query.String() + qry, err := ng.NewRangeQuery(context.Background(), testStorage, nil, remoteQuery, start, end, step) + if err != nil { + return + } + result := qry.Exec(context.Background()) + if result.Err != nil { + return + } + + matrix, err := result.Matrix() + if err != nil { + return + } + + for _, series := range matrix { + if !series.Metric.Has("region") { + t.Errorf( + "remote query result series missing partition label 'region'\n"+ + " original: %s\n"+ + " optimized: %s\n"+ + " remote query: %s\n"+ + " series: %s", + exprStr, root.String(), remoteQuery, series.Metric.String(), + ) + } + } + }) + } + }) +} diff --git a/internal/promql-engine/logicalplan/exprutil.go b/internal/promql-engine/logicalplan/exprutil.go new file mode 100644 index 00000000000..7cb4a8d4674 --- /dev/null +++ b/internal/promql-engine/logicalplan/exprutil.go @@ -0,0 +1,94 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package logicalplan + +import ( + "github.com/efficientgo/core/errors" + "github.com/prometheus/prometheus/promql/parser" +) + +// UnwrapString recursively unwraps a parser.Expr until it reaches an StringLiteral. +func UnwrapString(expr Node) (string, error) { + switch texpr := expr.(type) { + case *StringLiteral: + return texpr.Val, nil + case *Parens: + return UnwrapString(texpr.Expr) + case *StepInvariantExpr: + return UnwrapString(texpr.Expr) + default: + return "", errors.Newf("unexpected type: %T", texpr) + } +} + +// UnsafeUnwrapString is like UnwrapString but should only be used in cases where the parser +// guarantees success by already only allowing strings wrapped in parentheses. +func UnsafeUnwrapString(expr Node) string { + v, _ := UnwrapString(expr) + return v +} + +// UnwrapFloat recursively unwraps a parser.Expr until it reaches an NumberLiteral. +func UnwrapFloat(expr Node) (float64, error) { + switch texpr := expr.(type) { + case *NumberLiteral: + return texpr.Val, nil + case *Parens: + return UnwrapFloat(texpr.Expr) + case *StepInvariantExpr: + return UnwrapFloat(texpr.Expr) + default: + return 0, errors.Newf("unexpected type: %T", texpr) + } +} + +// UnwrapParens recursively unwraps a parser.ParenExpr. +func UnwrapParens(expr parser.Expr) parser.Expr { + switch t := expr.(type) { + case *parser.ParenExpr: + return UnwrapParens(t.Expr) + default: + return t + } +} + +// IsConstantExpr reports if the expression evaluates to a constant. +func IsConstantExpr(expr Node) bool { + // TODO: there are more possibilities for constant expressions + switch texpr := expr.(type) { + case *NumberLiteral, *StringLiteral: + return true + case *StepInvariantExpr: + return IsConstantExpr(texpr.Expr) + case *Parens: + return IsConstantExpr(texpr.Expr) + case *FunctionCall: + constArgs := true + for _, arg := range texpr.Args { + constArgs = constArgs && IsConstantExpr(arg) + } + return constArgs + case *Binary: + return IsConstantExpr(texpr.LHS) && IsConstantExpr(texpr.RHS) + default: + return false + } +} + +// IsConstantScalarExpr reports if the expression evaluates to a scalar. +func IsConstantScalarExpr(expr Node) bool { + // TODO: there are more possibilities for constant expressions + switch texpr := expr.(type) { + case *NumberLiteral, *StringLiteral: + return true + case *StepInvariantExpr: + return IsConstantScalarExpr(texpr.Expr) + case *Parens: + return IsConstantScalarExpr(texpr.Expr) + case *Binary: + return IsConstantScalarExpr(texpr.LHS) && IsConstantScalarExpr(texpr.RHS) + default: + return false + } +} diff --git a/internal/promql-engine/logicalplan/fingerprint.go b/internal/promql-engine/logicalplan/fingerprint.go new file mode 100644 index 00000000000..e74d5d06e85 --- /dev/null +++ b/internal/promql-engine/logicalplan/fingerprint.go @@ -0,0 +1,34 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package logicalplan + +import ( + enc "encoding/binary" + "hash/fnv" + "io" +) + +// NodeFingerprint returns a deterministic 64-bit fingerprint for the subtree +// rooted at n. Structurally identical subtrees always produce the same value. +func NodeFingerprint(n Node) uint64 { + h := fnv.New64a() + data, err := Marshal(n) + if err != nil { + // Marshal does not support distributed-execution nodes because they hold + // runtime Engine references. Fall back to type + String() + children. + _, _ = io.WriteString(h, string(n.Type())) + _, _ = io.WriteString(h, n.String()) + for _, child := range n.Children() { + if child == nil || *child == nil { + continue + } + var buf [8]byte + enc.LittleEndian.PutUint64(buf[:], NodeFingerprint(*child)) + h.Write(buf[:]) + } + } else { + h.Write(data) + } + return h.Sum64() +} diff --git a/internal/promql-engine/logicalplan/histogram_stats.go b/internal/promql-engine/logicalplan/histogram_stats.go new file mode 100644 index 00000000000..806925c3e8e --- /dev/null +++ b/internal/promql-engine/logicalplan/histogram_stats.go @@ -0,0 +1,56 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package logicalplan + +import ( + "github.com/thanos-io/promql-engine/query" + + "github.com/prometheus/prometheus/util/annotations" +) + +type DetectHistogramStatsOptimizer struct{} + +func (d DetectHistogramStatsOptimizer) Optimize(plan Node, _ *query.Options) (Node, annotations.Annotations) { + return d.optimize(plan, false) +} + +func (d DetectHistogramStatsOptimizer) optimize(plan Node, decodeStats bool) (Node, annotations.Annotations) { + var stop bool + Traverse(&plan, func(node *Node) { + if stop { + return + } + switch n := (*node).(type) { + case *VectorSelector: + n.DecodeNativeHistogramStats = decodeStats + case *Subquery: + // Do not propagate decodeStats through subqueries. + // Subqueries may apply functions like increase/rate that need + // full histogram bucket data for proper counter reset detection. + n.Expr, _ = d.optimize(n.Expr, false) + stop = true + return + case *FunctionCall: + switch n.Func.Name { + case "histogram_count", "histogram_sum", "histogram_avg": + n.Args[0], _ = d.optimize(n.Args[0], true) + stop = true + return + case "histogram_quantile": + n.Args[1], _ = d.optimize(n.Args[1], false) + stop = true + return + case "histogram_fraction": + n.Args[2], _ = d.optimize(n.Args[2], false) + stop = true + return + case "histogram_stddev", "histogram_stdvar": + n.Args[0], _ = d.optimize(n.Args[0], false) + stop = true + return + } + } + }) + return plan, nil +} diff --git a/internal/promql-engine/logicalplan/logical_nodes.go b/internal/promql-engine/logicalplan/logical_nodes.go new file mode 100644 index 00000000000..181d80aca7c --- /dev/null +++ b/internal/promql-engine/logicalplan/logical_nodes.go @@ -0,0 +1,589 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package logicalplan + +import ( + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/prometheus/common/model" + "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/promql/parser" +) + +type NodeType string + +const ( + VectorSelectorNode = "vector_selector" + MatrixSelectorNode = "matrix_selector" + AggregationNode = "aggregation" + BinaryNode = "binary" + FunctionNode = "function" + NumberLiteralNode = "number_literal" + StringLiteralNode = "string_literal" + SubqueryNode = "subquery" + CheckDuplicateNode = "check_duplicate" + StepInvariantNode = "step_invariant" + ParensNode = "parens" + UnaryNode = "unary" + + RemoteExecutionNode = "remote_exec" + DeduplicateNode = "dedup" + NoopNode = "noop" +) + +type Cloneable interface { + Clone() Node +} + +type Traversable interface { + Children() []*Node +} + +type LeafNode struct{} + +func (l LeafNode) Children() []*Node { return nil } + +type Node interface { + fmt.Stringer + Cloneable + Traversable + Type() NodeType + ReturnType() parser.ValueType +} + +type Nodes []Node + +// Projection has information on which series labels should be selected from storage. +type Projection struct { + // Labels is a list of labels to be included or excluded from the selection result, depending on the value of Include. + Labels []string + // Include is true if only the provided list of labels should be retrieved from storage. + // When set to false, the provided list of labels should be excluded from selection. + Include bool +} + +// VectorSelector is vector selector with additional configuration set by optimizers. +type VectorSelector struct { + *parser.VectorSelector + LeafNode + Filters []*labels.Matcher + BatchSize int64 + SelectTimestamp bool + Projection *Projection + // When set, histogram iterators can return objects which only have their + // CounterResetHint, Count and Sum values populated. Histogram buckets and spans + // will not be used during query evaluation. + DecodeNativeHistogramStats bool +} + +func (f *VectorSelector) Clone() Node { + clone := *f + vsClone := *f.VectorSelector + clone.VectorSelector = &vsClone + + clone.Filters = shallowCloneSlice(f.Filters) + clone.LabelMatchers = shallowCloneSlice(f.LabelMatchers) + if f.Projection != nil { + clone.Projection = &Projection{} + clone.Projection.Labels = shallowCloneSlice(f.Projection.Labels) + clone.Projection.Include = f.Projection.Include + } + + if f.VectorSelector.Timestamp != nil { + ts := *f.VectorSelector.Timestamp + clone.Timestamp = &ts + } + + return &clone +} + +func (f *VectorSelector) Type() NodeType { return VectorSelectorNode } + +func (f *VectorSelector) String() string { + if f.SelectTimestamp { + // If we pushed down timestamp into the vector selector we need to render the proper + // PromQL again. + return fmt.Sprintf("timestamp(%s)", f.VectorSelector.String()) + } + return f.VectorSelector.String() +} + +func (f *VectorSelector) ReturnType() parser.ValueType { return parser.ValueTypeVector } + +// MatrixSelector is matrix selector with additional configuration set by optimizers. +// It is used so we can get rid of VectorSelector in distributed mode too. +type MatrixSelector struct { + VectorSelector *VectorSelector `json:"-"` + Range time.Duration + + // Needed because this operator is used in the distributed mode + OriginalString string +} + +func (f *MatrixSelector) Clone() Node { + clone := *f + clone.VectorSelector = f.VectorSelector.Clone().(*VectorSelector) + return &clone +} + +func (f *MatrixSelector) Children() []*Node { + var vs Node = f.VectorSelector + return []*Node{&vs} +} + +func (f *MatrixSelector) String() string { + return f.OriginalString +} + +func (f *MatrixSelector) ReturnType() parser.ValueType { return parser.ValueTypeVector } + +func (f *MatrixSelector) Type() NodeType { return MatrixSelectorNode } + +// CheckDuplicateLabels is a logical node that checks for duplicate labels in the same timestamp. +type CheckDuplicateLabels struct { + Expr Node `json:"-"` +} + +func (c *CheckDuplicateLabels) Clone() Node { + clone := *c + clone.Expr = c.Expr.Clone() + return &clone +} + +func (c *CheckDuplicateLabels) Children() []*Node { return []*Node{&c.Expr} } +func (c *CheckDuplicateLabels) String() string { return c.Expr.String() } +func (c *CheckDuplicateLabels) ReturnType() parser.ValueType { return c.Expr.ReturnType() } +func (c *CheckDuplicateLabels) Type() NodeType { return CheckDuplicateNode } + +// StringLiteral is a logical node representing a literal string. +type StringLiteral struct { + LeafNode + Val string +} + +func (c *StringLiteral) Clone() Node { return &StringLiteral{Val: c.Val} } +func (c *StringLiteral) String() string { return fmt.Sprintf("%q", c.Val) } +func (c *StringLiteral) ReturnType() parser.ValueType { return parser.ValueTypeString } +func (c *StringLiteral) Type() NodeType { return StringLiteralNode } + +// NumberLiteral is a logical node representing a literal number. +type NumberLiteral struct { + LeafNode + Val float64 +} + +func (c *NumberLiteral) Clone() Node { return &NumberLiteral{Val: c.Val} } +func (c *NumberLiteral) String() string { + // Wrap negative numbers in parentheses to preserve parsing behavior. + // Without parens, "-1.5 ^ 2" parses as "-(1.5 ^ 2)" due to operator precedence. + if c.Val < 0 { + return fmt.Sprintf("(%v)", c.Val) + } + return fmt.Sprint(c.Val) +} +func (c *NumberLiteral) ReturnType() parser.ValueType { return parser.ValueTypeScalar } +func (c *NumberLiteral) Type() NodeType { return NumberLiteralNode } + +// StepInvariantExpr is a logical node that expresses that the child expression +// returns the same value at every step in the evaluation. +type StepInvariantExpr struct { + Expr Node `json:"-"` +} + +func (c *StepInvariantExpr) Clone() Node { + clone := *c + clone.Expr = c.Expr.Clone() + return &clone +} + +func (c *StepInvariantExpr) Children() []*Node { return []*Node{&c.Expr} } +func (c *StepInvariantExpr) String() string { return c.Expr.String() } +func (c *StepInvariantExpr) ReturnType() parser.ValueType { return c.Expr.ReturnType() } +func (c *StepInvariantExpr) Type() NodeType { return StepInvariantNode } + +// FunctionCall represents a PromQL function. +type FunctionCall struct { + // The function that was called. + Func parser.Function + // Arguments passed into the function. + Args []Node `json:"-"` +} + +func (f *FunctionCall) Clone() Node { + clone := *f + clone.Args = make([]Node, 0, len(f.Args)) + for _, arg := range f.Args { + clone.Args = append(clone.Args, arg.Clone()) + } + return &clone +} + +func (f *FunctionCall) Children() []*Node { + args := make([]*Node, 0, len(f.Args)) + for i := range f.Args { + args = append(args, &f.Args[i]) + } + return args +} + +func (f *FunctionCall) String() string { + args := make([]string, 0, len(f.Args)) + for _, arg := range f.Args { + args = append(args, arg.String()) + } + return fmt.Sprintf("%s(%s)", f.Func.Name, strings.Join(args, ", ")) +} + +func (f *FunctionCall) ReturnType() parser.ValueType { return f.Func.ReturnType } +func (f *FunctionCall) Type() NodeType { return FunctionNode } + +type Parens struct { + Expr Node `json:"-"` +} + +func (p *Parens) Clone() Node { return &Parens{Expr: p.Expr.Clone()} } +func (p *Parens) Children() []*Node { return []*Node{&p.Expr} } +func (p *Parens) String() string { return fmt.Sprintf("(%s)", p.Expr.String()) } +func (p *Parens) ReturnType() parser.ValueType { return p.Expr.ReturnType() } +func (p *Parens) Type() NodeType { return ParensNode } + +type Unary struct { + Op parser.ItemType + Expr Node `json:"-"` +} + +func (p *Unary) Clone() Node { return &Unary{Op: p.Op, Expr: p.Expr.Clone()} } +func (p *Unary) Children() []*Node { return []*Node{&p.Expr} } +func (p *Unary) String() string { return fmt.Sprintf("%s%s", p.Op.String(), p.Expr.String()) } +func (p *Unary) ReturnType() parser.ValueType { return p.Expr.ReturnType() } +func (p *Unary) Type() NodeType { return UnaryNode } + +type unary struct{ Op string } + +func (p *Unary) MarshalJSON() ([]byte, error) { + return json.Marshal(unary{ + Op: p.Op.String(), + }) +} + +func (p *Unary) UnmarshalJSON(data []byte) error { + a := unary{} + if err := json.Unmarshal(data, &a); err != nil { + return err + } + lexer := parser.Lex(a.Op) + var opItem parser.Item + lexer.NextItem(&opItem) + p.Op = opItem.Typ + return nil +} + +// Aggregation represents a PromQL aggregation. +type Aggregation struct { + Op parser.ItemType + Expr Node `json:"-"` + Param Node `json:"-"` + Grouping []string + Without bool +} + +func (f *Aggregation) Clone() Node { + clone := *f + clone.Expr = f.Expr.Clone() + if clone.Param != nil { + clone.Param = f.Param.Clone() + } + clone.Grouping = shallowCloneSlice(f.Grouping) + return &clone +} + +func (f *Aggregation) Children() []*Node { + children := []*Node{&f.Expr} + if f.Param != nil { + children = append(children, &f.Param) + } + return children +} + +func (f *Aggregation) String() string { + aggrString := f.getAggOpStr() + aggrString += "(" + if f.Op.IsAggregatorWithParam() { + aggrString += fmt.Sprintf("%s, ", f.Param) + } + aggrString += fmt.Sprintf("%s)", f.Expr) + + return aggrString +} + +func (f *Aggregation) ReturnType() parser.ValueType { return parser.ValueTypeVector } +func (f *Aggregation) Type() NodeType { return AggregationNode } + +func (f *Aggregation) getAggOpStr() string { + aggrString := f.Op.String() + + switch { + case f.Without: + aggrString += fmt.Sprintf(" without (%s) ", strings.Join(f.Grouping, ", ")) + case len(f.Grouping) > 0: + aggrString += fmt.Sprintf(" by (%s) ", strings.Join(f.Grouping, ", ")) + } + + return aggrString +} + +type aggregation struct { + Op string + Grouping []string + Without bool +} + +func (f *Aggregation) MarshalJSON() ([]byte, error) { + return json.Marshal(aggregation{ + Op: f.Op.String(), + Grouping: f.Grouping, + Without: f.Without, + }) +} + +func (f *Aggregation) UnmarshalJSON(data []byte) error { + a := aggregation{} + if err := json.Unmarshal(data, &a); err != nil { + return err + } + lexer := parser.Lex(a.Op) + var opItem parser.Item + lexer.NextItem(&opItem) + f.Op = opItem.Typ + f.Grouping = a.Grouping + f.Without = a.Without + return nil +} + +type Binary struct { + Op parser.ItemType // The operation of the expression. + LHS, RHS Node `json:"-"` // The operands on the respective sides of the operator. + + // The matching behavior for the operation if both operands are Vectors. + // If they are not this field is nil. + VectorMatching *parser.VectorMatching + + // If a comparison operator, return 0/1 rather than filtering. + ReturnBool bool + + ValueType parser.ValueType +} + +func (b *Binary) Clone() Node { + clone := *b + clone.LHS = b.LHS.Clone() + clone.RHS = b.RHS.Clone() + if b.VectorMatching != nil { + vm := *b.VectorMatching + clone.VectorMatching = &vm + } + return &clone +} + +func (b *Binary) Children() []*Node { return []*Node{&b.LHS, &b.RHS} } + +func (b *Binary) ReturnType() parser.ValueType { + if b.LHS.ReturnType() == parser.ValueTypeScalar && b.RHS.ReturnType() == parser.ValueTypeScalar { + return parser.ValueTypeScalar + } + return parser.ValueTypeVector +} + +func (b *Binary) Type() NodeType { return BinaryNode } + +func (b *Binary) String() string { + returnBool := "" + if b.ReturnBool { + returnBool = " bool" + } + + matching := b.getMatchingStr() + return fmt.Sprintf("%s %s%s%s %s", b.LHS, b.Op, returnBool, matching, b.RHS) +} + +func (b *Binary) getMatchingStr() string { + matching := "" + vm := b.VectorMatching + if vm != nil && (len(vm.MatchingLabels) > 0 || vm.On) { + vmTag := "ignoring" + if vm.On { + vmTag = "on" + } + matching = fmt.Sprintf(" %s (%s)", vmTag, strings.Join(vm.MatchingLabels, ", ")) + + if vm.Card == parser.CardManyToOne || vm.Card == parser.CardOneToMany { + vmCard := "right" + if vm.Card == parser.CardManyToOne { + vmCard = "left" + } + matching += fmt.Sprintf(" group_%s (%s)", vmCard, strings.Join(vm.Include, ", ")) + } + } + return matching +} + +type binary struct { + Op string + VectorMatching *parser.VectorMatching + ReturnBool bool + ValueType parser.ValueType +} + +func (b *Binary) MarshalJSON() ([]byte, error) { + return json.Marshal(binary{ + Op: b.Op.String(), + VectorMatching: b.VectorMatching, + ReturnBool: b.ReturnBool, + ValueType: b.ValueType, + }) +} + +func (b *Binary) UnmarshalJSON(data []byte) error { + a := binary{} + if err := json.Unmarshal(data, &a); err != nil { + return err + } + lexer := parser.Lex(a.Op) + var opItem parser.Item + lexer.NextItem(&opItem) + b.Op = opItem.Typ + b.VectorMatching = a.VectorMatching + b.ReturnBool = a.ReturnBool + b.ValueType = a.ValueType + + return nil +} + +type Subquery struct { + Expr Node `json:"-"` + Range time.Duration + // OriginalOffset is the actual offset that was set in the query. + // This never changes. + OriginalOffset time.Duration + // Offset is the offset used during the query execution + // which is calculated using the original offset, at modifier time, + // eval time, and subquery offsets in the AST tree. + Offset time.Duration + Timestamp *int64 + Step time.Duration + + StartOrEnd parser.ItemType +} + +func (s *Subquery) Clone() Node { + clone := *s + clone.Expr = s.Expr.Clone() + + if s.Timestamp != nil { + ts := *s.Timestamp + clone.Timestamp = &ts + } + + return &clone +} + +func (s *Subquery) Children() []*Node { return []*Node{&s.Expr} } + +func (s *Subquery) String() string { + return fmt.Sprintf("%s%s", s.Expr.String(), s.getSubqueryTimeSuffix()) +} + +func (s *Subquery) ReturnType() parser.ValueType { return s.Expr.ReturnType() } + +func (s *Subquery) Type() NodeType { return SubqueryNode } + +func (s *Subquery) getSubqueryTimeSuffix() any { + step := "" + if s.Step != 0 { + step = model.Duration(s.Step).String() + } + offset := "" + switch { + case s.OriginalOffset > time.Duration(0): + offset = fmt.Sprintf(" offset %s", model.Duration(s.OriginalOffset)) + case s.OriginalOffset < time.Duration(0): + offset = fmt.Sprintf(" offset -%s", model.Duration(-s.OriginalOffset)) + } + at := "" + switch { + case s.Timestamp != nil: + at = fmt.Sprintf(" @ %.3f", float64(*s.Timestamp)/1000.0) + case s.StartOrEnd == parser.START: + at = " @ start()" + case s.StartOrEnd == parser.END: + at = " @ end()" + } + return fmt.Sprintf("[%s:%s]%s%s", model.Duration(s.Range), step, at, offset) +} + +type subquery struct { + Range time.Duration + OriginalOffset time.Duration + Offset time.Duration + Timestamp *int64 + Step time.Duration + StartOrEnd string +} + +func (s *Subquery) MarshalJSON() ([]byte, error) { + return json.Marshal(subquery{ + Range: s.Range, + OriginalOffset: s.OriginalOffset, + Offset: s.Offset, + Timestamp: s.Timestamp, + Step: s.Step, + StartOrEnd: s.StartOrEnd.String(), + }) +} + +func (s *Subquery) UnmarshalJSON(data []byte) error { + a := subquery{} + if err := json.Unmarshal(data, &a); err != nil { + return err + } + lexer := parser.Lex(a.StartOrEnd) + var opItem parser.Item + lexer.NextItem(&opItem) + + s.Range = a.Range + s.OriginalOffset = a.OriginalOffset + s.Offset = a.Offset + s.Timestamp = a.Timestamp + s.Step = a.Step + s.StartOrEnd = opItem.Typ + return nil +} + +func shallowCloneSlice[T any](s []T) []T { + if s == nil { + return nil + } + clone := make([]T, len(s)) + copy(clone, s) + return clone +} + +func isAggregation(expr *Node) bool { + if expr == nil { + return false + } + _, ok := (*expr).(*Aggregation) + return ok +} + +func isAvgAggregation(expr *Node) bool { + if expr == nil { + return false + } + aggr, ok := (*expr).(*Aggregation) + return ok && aggr.Op == parser.AVG +} diff --git a/internal/promql-engine/logicalplan/merge_selects.go b/internal/promql-engine/logicalplan/merge_selects.go new file mode 100644 index 00000000000..4cc2f7cf2a7 --- /dev/null +++ b/internal/promql-engine/logicalplan/merge_selects.go @@ -0,0 +1,175 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package logicalplan + +import ( + "slices" + + "github.com/thanos-io/promql-engine/query" + + "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/util/annotations" +) + +// MergeSelectsOptimizer optimizes a binary expression where +// one select is a superset of the other select. +// For example, the expression: +// +// metric{a="b", c="d"} / scalar(metric{a="b"}) becomes: +// Filter(c="d", metric{a="b"}) / scalar(metric{a="b"}). +// +// The engine can then cache the result of `metric{a="b"}` +// and apply an additional filter for {c="d"}. +type MergeSelectsOptimizer struct{} + +func (m MergeSelectsOptimizer) Optimize(plan Node, _ *query.Options) (Node, annotations.Annotations) { + heap := make(matcherHeap) + extractSelectors(heap, plan) + replaceMatchers(heap, &plan) + + return plan, nil +} + +func extractSelectors(selectors matcherHeap, expr Node) { + Traverse(&expr, func(node *Node) { + e, ok := (*node).(*VectorSelector) + if !ok { + return + } + if !emptyProjection(e) { + return + } + for _, l := range e.LabelMatchers { + if l.Name == labels.MetricName { + selectors.add(l.Value, e.LabelMatchers) + } + } + }) +} + +func replaceMatchers(selectors matcherHeap, expr *Node) { + Traverse(expr, func(node *Node) { + var matchers []*labels.Matcher + switch e := (*node).(type) { + case *VectorSelector: + if !emptyProjection(e) { + return + } + matchers = e.LabelMatchers + default: + return + } + + for _, l := range matchers { + if l.Name != labels.MetricName { + continue + } + replacement, found := selectors.findReplacement(l.Value, matchers) + if !found { + continue + } + + // Make a copy of the original selectors to avoid modifying them while + // trimming filters. + filters := make([]*labels.Matcher, len(matchers)) + copy(filters, matchers) + + // Drop filters which are already present as matchers in the replacement selector including metric name selector. + filters = dropMatcher(replacement, filters) + + switch e := (*node).(type) { + case *VectorSelector: + e.LabelMatchers = replacement + e.Filters = filters + } + + return + } + }) +} + +func dropMatcher(toDrop []*labels.Matcher, original []*labels.Matcher) []*labels.Matcher { + res := slices.Clone(original) + i := 0 + for i < len(res) { + l := res[i] + remove := false + for _, m := range toDrop { + if l.Name == m.Name && l.Type == m.Type && l.Value == m.Value { + remove = true + break + } + } + if remove { + res = slices.Delete(res, i, i+1) + } else { + i++ + } + } + return res +} + +func matcherToMap(matchers []*labels.Matcher) map[string]*labels.Matcher { + r := make(map[string]*labels.Matcher, len(matchers)) + for i := range matchers { + r[matchers[i].Name] = matchers[i] + } + return r +} + +// matcherHeap is a set of the most selective label matchers +// for each metrics discovered in a PromQL expression. +// The selectivity of a matcher is defined by how many series are +// matched by it. Since matchers in PromQL are open, selectors +// with the least amount of matchers are typically the most selective ones. +type matcherHeap map[string][]*labels.Matcher + +func (m matcherHeap) add(metricName string, lessSelective []*labels.Matcher) { + moreSelective, ok := m[metricName] + if !ok { + m[metricName] = lessSelective + return + } + + if len(lessSelective) < len(moreSelective) { + moreSelective = lessSelective + } + + m[metricName] = moreSelective +} + +func (m matcherHeap) findReplacement(metricName string, matcher []*labels.Matcher) ([]*labels.Matcher, bool) { + top, ok := m[metricName] + if !ok { + return nil, false + } + + matcherSet := matcherToMap(matcher) + topSet := matcherToMap(top) + for k, v := range topSet { + m, ok := matcherSet[k] + if !ok { + return nil, false + } + + equals := v.Name == m.Name && v.Type == m.Type && v.Value == m.Value + if !equals { + return nil, false + } + } + + // The top matcher and input matcher are equal. No replacement needed. + if len(topSet) == len(matcherSet) { + return nil, false + } + + return top, true +} + +func emptyProjection(vs *VectorSelector) bool { + if vs.Projection == nil { + return true + } + return !vs.Projection.Include && len(vs.Projection.Labels) == 0 +} diff --git a/internal/promql-engine/logicalplan/merge_selects_test.go b/internal/promql-engine/logicalplan/merge_selects_test.go new file mode 100644 index 00000000000..fcb4e07fa9a --- /dev/null +++ b/internal/promql-engine/logicalplan/merge_selects_test.go @@ -0,0 +1,176 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package logicalplan + +import ( + "testing" + + "github.com/thanos-io/promql-engine/query" + + "github.com/efficientgo/core/testutil" + "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/promql/parser" +) + +func TestMergeSelects(t *testing.T) { + cases := []struct { + expr string + expected string + }{ + { + expr: `X{a="b"}/X`, + expected: `filter([a="b"], X) / X`, + }, + { + expr: `floor(X{a="b"})/X`, + expected: `floor(filter([a="b"], X)) / X`, + }, + { + expr: `X/floor(X{a="b"})`, + expected: `X / floor(filter([a="b"], X))`, + }, + { + expr: `X{a="b"}/floor(X)`, + expected: `filter([a="b"], X) / floor(X)`, + }, + { + expr: `X{a!~"b",a=~"b",c="d"}/X{a=~"b"}`, + expected: `filter([a!~"b" c="d"], X{a=~"b"}) / X{a=~"b"}`, + }, + { + expr: `quantile by (pod) (scalar(min(http_requests_total)), http_requests_total)`, + expected: `quantile by (pod) (scalar(min(http_requests_total)), http_requests_total)`, + }, + } + optimizers := []Optimizer{MergeSelectsOptimizer{}} + for _, tcase := range cases { + t.Run(tcase.expr, func(t *testing.T) { + expr, err := parser.ParseExpr(tcase.expr) + testutil.Ok(t, err) + + plan, _ := NewFromAST(expr, &query.Options{}, PlanOptions{}) + optimizedPlan, _ := plan.Optimize(optimizers) + testutil.Equals(t, tcase.expected, renderExprTree(optimizedPlan.Root())) + }) + } +} + +func TestMergeSelectsWithProjections(t *testing.T) { + cases := []struct { + name string + plan Node + expected string + }{ + { + name: "no merge when left has projection", + plan: &Binary{ + Op: parser.DIV, + LHS: &VectorSelector{ + VectorSelector: &parser.VectorSelector{ + Name: "X", + LabelMatchers: []*labels.Matcher{ + {Type: labels.MatchEqual, Name: labels.MetricName, Value: "X"}, + {Type: labels.MatchEqual, Name: "a", Value: "b"}, + }, + }, + Projection: &Projection{Include: true, Labels: []string{"a"}}, + }, + RHS: &VectorSelector{ + VectorSelector: &parser.VectorSelector{ + Name: "X", + LabelMatchers: []*labels.Matcher{ + {Type: labels.MatchEqual, Name: labels.MetricName, Value: "X"}, + }, + }, + }, + }, + expected: `X{a="b"}[projection=include(a)] / X`, + }, + { + name: "no merge when right has projection", + plan: &Binary{ + Op: parser.DIV, + LHS: &VectorSelector{ + VectorSelector: &parser.VectorSelector{ + Name: "X", + LabelMatchers: []*labels.Matcher{ + {Type: labels.MatchEqual, Name: labels.MetricName, Value: "X"}, + }, + }, + }, + RHS: &VectorSelector{ + VectorSelector: &parser.VectorSelector{ + Name: "X", + LabelMatchers: []*labels.Matcher{ + {Type: labels.MatchEqual, Name: labels.MetricName, Value: "X"}, + + {Type: labels.MatchEqual, Name: "a", Value: "b"}, + }, + }, + Projection: &Projection{Include: true, Labels: []string{"a"}}, + }, + }, + expected: `X / X{a="b"}[projection=include(a)]`, + }, + { + name: "no merge when both have projections", + plan: &Binary{ + Op: parser.DIV, + LHS: &VectorSelector{ + VectorSelector: &parser.VectorSelector{ + Name: "X", + LabelMatchers: []*labels.Matcher{ + {Type: labels.MatchEqual, Name: labels.MetricName, Value: "X"}, + }, + }, + Projection: &Projection{Include: true, Labels: []string{"a"}}, + }, + RHS: &VectorSelector{ + VectorSelector: &parser.VectorSelector{ + Name: "X", + LabelMatchers: []*labels.Matcher{ + {Type: labels.MatchEqual, Name: labels.MetricName, Value: "X"}, + {Type: labels.MatchEqual, Name: "c", Value: "d"}, + }, + }, + Projection: &Projection{Include: true, Labels: []string{"c"}}, + }, + }, + expected: `X[projection=include(a)] / X{c="d"}[projection=include(c)]`, + }, + { + name: "merge if empty projection", + plan: &Binary{ + Op: parser.DIV, + LHS: &VectorSelector{ + VectorSelector: &parser.VectorSelector{ + Name: "X", + LabelMatchers: []*labels.Matcher{ + {Type: labels.MatchEqual, Name: labels.MetricName, Value: "X"}, + {Type: labels.MatchEqual, Name: "a", Value: "b"}, + }, + }, + Projection: &Projection{}, + }, + RHS: &VectorSelector{ + VectorSelector: &parser.VectorSelector{ + Name: "X", + LabelMatchers: []*labels.Matcher{ + {Type: labels.MatchEqual, Name: labels.MetricName, Value: "X"}, + }, + }, + }, + }, + expected: `filter([a="b"], X) / X`, + }, + } + + optimizer := MergeSelectsOptimizer{} + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + optimizedPlan, _ := optimizer.Optimize(tc.plan, &query.Options{}) + testutil.Equals(t, tc.expected, renderExprTree(optimizedPlan)) + }) + } +} diff --git a/internal/promql-engine/logicalplan/passthrough.go b/internal/promql-engine/logicalplan/passthrough.go new file mode 100644 index 00000000000..db04d465505 --- /dev/null +++ b/internal/promql-engine/logicalplan/passthrough.go @@ -0,0 +1,97 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package logicalplan + +import ( + "maps" + "slices" + + "github.com/thanos-io/promql-engine/api" + "github.com/thanos-io/promql-engine/query" + + "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/util/annotations" +) + +// PassthroughOptimizer optimizes queries which can be simply passed +// through to a RemoteEngine. +type PassthroughOptimizer struct { + Endpoints api.RemoteEndpoints +} + +// labelSetsMatch returns false if all label-set do not match the matchers (aka: OR is between all label-sets). +func labelSetsMatch(matchers []*labels.Matcher, lset ...labels.Labels) bool { + if len(lset) == 0 { + return true + } + + for _, ls := range lset { + notMatched := false + for _, m := range matchers { + if lv := ls.Get(m.Name); ls.Has(m.Name) && !m.Matches(lv) { + notMatched = true + break + } + } + if !notMatched { + return true + } + } + return false +} + +func (m PassthroughOptimizer) Optimize(plan Node, opts *query.Options) (Node, annotations.Annotations) { + mint, maxt := MinMaxTime(plan, opts) + engines := m.Endpoints.Engines(mint, maxt) + if len(engines) == 0 { + return plan, nil + } + var ( + hasSelector bool + matchingEngineSet = make(map[api.RemoteEngine]struct{}) + ) + TraverseBottomUp(nil, &plan, func(parent, current *Node) (stop bool) { + if vs, ok := (*current).(*VectorSelector); ok { + hasSelector = true + for _, e := range engines { + if !labelSetsMatch(vs.LabelMatchers, e.LabelSets()...) { + continue + } + matchingEngineSet[e] = struct{}{} + if len(matchingEngineSet) > 1 { + return true + } + } + } + return false + }) + + matchingEngines := slices.Collect(maps.Keys(matchingEngineSet)) + if len(matchingEngines) == 0 { + if !hasSelector && matchingEngineTime(engines[0], mint, maxt) { + return RemoteExecution{ + Engine: engines[0], + Query: plan.Clone(), + QueryRangeStart: opts.Start, + QueryRangeEnd: opts.End, + }, nil + } + return plan, nil + } + + if len(matchingEngines) == 1 && matchingEngineTime(matchingEngines[0], mint, maxt) { + return RemoteExecution{ + Engine: matchingEngines[0], + Query: plan.Clone(), + QueryRangeStart: opts.Start, + QueryRangeEnd: opts.End, + }, nil + } + + return plan, nil +} + +func matchingEngineTime(e api.RemoteEngine, minTime, maxTime int64) bool { + return !(minTime > e.MaxT() || maxTime < e.MinT()) +} diff --git a/internal/promql-engine/logicalplan/passthrough_test.go b/internal/promql-engine/logicalplan/passthrough_test.go new file mode 100644 index 00000000000..de42531cdfb --- /dev/null +++ b/internal/promql-engine/logicalplan/passthrough_test.go @@ -0,0 +1,156 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package logicalplan + +import ( + "math" + "testing" + "time" + + "github.com/thanos-io/promql-engine/api" + "github.com/thanos-io/promql-engine/query" + + "github.com/efficientgo/core/testutil" + "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/promql/parser" +) + +func TestPassthrough(t *testing.T) { + expr, err := parser.ParseExpr(`time()`) + testutil.Ok(t, err) + + t.Run("optimized with one engine, in bounds", func(t *testing.T) { + engines := []api.RemoteEngine{ + newEngineMock(math.MinInt64, math.MaxInt64, []labels.Labels{labels.FromStrings("region", "east"), labels.FromStrings("region", "south")}), + } + optimizers := []Optimizer{PassthroughOptimizer{Endpoints: api.NewStaticEndpoints(engines)}} + + plan, _ := NewFromAST(expr, &query.Options{Start: time.Unix(0, 0), End: time.Unix(0, 0)}, PlanOptions{}) + optimizedPlan, _ := plan.Optimize(optimizers) + + testutil.Equals(t, "remote(time())", renderExprTree(optimizedPlan.Root())) + }) + + t.Run("not optimized with two engines", func(t *testing.T) { + engines := []api.RemoteEngine{ + newEngineMock(math.MinInt64, math.MinInt64, []labels.Labels{labels.FromStrings("region", "east"), labels.FromStrings("region", "south")}), + newEngineMock(math.MinInt64, math.MinInt64, []labels.Labels{labels.FromStrings("region", "west")}), + } + optimizers := []Optimizer{PassthroughOptimizer{Endpoints: api.NewStaticEndpoints(engines)}} + + plan, _ := NewFromAST(expr, &query.Options{Start: time.Unix(0, 0), End: time.Unix(0, 0)}, PlanOptions{}) + optimizedPlan, _ := plan.Optimize(optimizers) + + testutil.Equals(t, "time()", renderExprTree(optimizedPlan.Root())) + }) + + t.Run("not optimized with one out of bound engine", func(t *testing.T) { + engines := []api.RemoteEngine{ + newEngineMock(math.MinInt64, math.MinInt64, []labels.Labels{labels.FromStrings("region", "east"), labels.FromStrings("region", "south")}), + } + optimizers := []Optimizer{PassthroughOptimizer{Endpoints: api.NewStaticEndpoints(engines)}} + + plan, _ := NewFromAST(expr, &query.Options{Start: time.Unix(0, 0), End: time.Unix(0, 0)}, PlanOptions{}) + optimizedPlan, _ := plan.Optimize(optimizers) + + testutil.Equals(t, "time()", renderExprTree(optimizedPlan.Root())) + }) + + t.Run("optimized with matching labels", func(t *testing.T) { + selectorExpr, err := parser.ParseExpr(`{region="east"}`) + testutil.Ok(t, err) + + engines := []api.RemoteEngine{ + newEngineMock(math.MinInt64, math.MaxInt64, []labels.Labels{labels.FromStrings("region", "east"), labels.FromStrings("region", "south")}), + newEngineMock(math.MinInt64, math.MaxInt64, []labels.Labels{labels.FromStrings("region", "west")}), + } + optimizers := []Optimizer{PassthroughOptimizer{Endpoints: api.NewStaticEndpoints(engines)}} + + plan, _ := NewFromAST(selectorExpr, &query.Options{Start: time.Unix(0, 0), End: time.Unix(0, 0)}, PlanOptions{}) + optimizedPlan, _ := plan.Optimize(optimizers) + + testutil.Equals(t, `remote({region="east"})`, renderExprTree(optimizedPlan.Root())) + }) + + t.Run("not optimized due to multiple engines", func(t *testing.T) { + selectorExpr, err := parser.ParseExpr(`{region=~"east|west"}`) + testutil.Ok(t, err) + + engines := []api.RemoteEngine{ + newEngineMock(math.MinInt64, math.MaxInt64, []labels.Labels{labels.FromStrings("region", "east"), labels.FromStrings("region", "south")}), + newEngineMock(math.MinInt64, math.MaxInt64, []labels.Labels{labels.FromStrings("region", "west")}), + } + optimizers := []Optimizer{PassthroughOptimizer{Endpoints: api.NewStaticEndpoints(engines)}} + + plan, _ := NewFromAST(selectorExpr, &query.Options{Start: time.Unix(0, 0), End: time.Unix(0, 0)}, PlanOptions{}) + optimizedPlan, _ := plan.Optimize(optimizers) + + testutil.Equals(t, `{region=~"east|west"}`, renderExprTree(optimizedPlan.Root())) + }) + + t.Run("optimized with matching labels on matrix selector", func(t *testing.T) { + selectorExpr, err := parser.ParseExpr(`{region="east"}[5m]`) + testutil.Ok(t, err) + + engines := []api.RemoteEngine{ + newEngineMock(math.MinInt64, math.MaxInt64, []labels.Labels{labels.FromStrings("region", "east"), labels.FromStrings("region", "south")}), + newEngineMock(math.MinInt64, math.MaxInt64, []labels.Labels{labels.FromStrings("region", "west")}), + } + optimizers := []Optimizer{PassthroughOptimizer{Endpoints: api.NewStaticEndpoints(engines)}} + + plan, _ := NewFromAST(selectorExpr, &query.Options{Start: time.Unix(0, 0), End: time.Unix(0, 0)}, PlanOptions{}) + optimizedPlan, _ := plan.Optimize(optimizers) + + testutil.Equals(t, `remote({region="east"}[5m])`, renderExprTree(optimizedPlan.Root())) + }) + + t.Run("optimized with multiple selectors matching the same engine", func(t *testing.T) { + multiSelectorExpr, err := parser.ParseExpr(`rate({region="east",__name__="metric_a"}[5m]) / {region="east",__name__="metric_b"}`) + testutil.Ok(t, err) + + engines := []api.RemoteEngine{ + newEngineMock(math.MinInt64, math.MaxInt64, []labels.Labels{labels.FromStrings("region", "east")}), + newEngineMock(math.MinInt64, math.MaxInt64, []labels.Labels{labels.FromStrings("region", "west")}), + } + optimizers := []Optimizer{PassthroughOptimizer{Endpoints: api.NewStaticEndpoints(engines)}} + + plan, _ := NewFromAST(multiSelectorExpr, &query.Options{Start: time.Unix(0, 0), End: time.Unix(0, 0)}, PlanOptions{}) + optimizedPlan, _ := plan.Optimize(optimizers) + + testutil.Equals(t, `remote(rate({__name__="metric_a",region="east"}[5m]) / {__name__="metric_b",region="east"})`, renderExprTree(optimizedPlan.Root())) + }) + + t.Run("not optimized with selectors matching different engines", func(t *testing.T) { + crossPartitionExpr, err := parser.ParseExpr(`{region="east",__name__="metric_a"} / {region="west",__name__="metric_b"}`) + testutil.Ok(t, err) + + engines := []api.RemoteEngine{ + newEngineMock(math.MinInt64, math.MaxInt64, []labels.Labels{labels.FromStrings("region", "east")}), + newEngineMock(math.MinInt64, math.MaxInt64, []labels.Labels{labels.FromStrings("region", "west")}), + } + optimizers := []Optimizer{PassthroughOptimizer{Endpoints: api.NewStaticEndpoints(engines)}} + + plan, _ := NewFromAST(crossPartitionExpr, &query.Options{Start: time.Unix(0, 0), End: time.Unix(0, 0)}, PlanOptions{}) + optimizedPlan, _ := plan.Optimize(optimizers) + + testutil.Equals(t, `{__name__="metric_a",region="east"} / {__name__="metric_b",region="west"}`, renderExprTree(optimizedPlan.Root())) + }) + + t.Run("not optimized with matching labels but not matching time", func(t *testing.T) { + selectorExpr, err := parser.ParseExpr(`{region="east"}`) + testutil.Ok(t, err) + + engines := []api.RemoteEngine{ + newEngineMock(math.MinInt64, math.MinInt64, []labels.Labels{labels.FromStrings("region", "east"), labels.FromStrings("region", "south")}), + newEngineMock(math.MinInt64, math.MaxInt64, []labels.Labels{labels.FromStrings("region", "west")}), + } + optimizers := []Optimizer{PassthroughOptimizer{Endpoints: api.NewStaticEndpoints(engines)}} + + plan, _ := NewFromAST(selectorExpr, &query.Options{Start: time.Unix(0, 0), End: time.Unix(0, 0)}, PlanOptions{}) + optimizedPlan, _ := plan.Optimize(optimizers) + + testutil.Equals(t, `{region="east"}`, renderExprTree(optimizedPlan.Root())) + }) + +} diff --git a/internal/promql-engine/logicalplan/plan.go b/internal/promql-engine/logicalplan/plan.go new file mode 100644 index 00000000000..3f6753301bb --- /dev/null +++ b/internal/promql-engine/logicalplan/plan.go @@ -0,0 +1,556 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package logicalplan + +import ( + "math" + "strings" + "time" + + "github.com/thanos-io/promql-engine/execution/parse" + "github.com/thanos-io/promql-engine/query" + + "github.com/prometheus/prometheus/promql" + "github.com/prometheus/prometheus/promql/parser" + "github.com/prometheus/prometheus/util/annotations" +) + +var ( + NoOptimizers = []Optimizer{} + AllOptimizers = append(DefaultOptimizers, PropagateMatchersOptimizer{}) +) + +var DefaultOptimizers = []Optimizer{ + SortMatchers{}, + MergeSelectsOptimizer{}, + DetectHistogramStatsOptimizer{}, +} + +type Plan interface { + Optimize([]Optimizer) (Plan, annotations.Annotations) + Root() Node +} + +type Optimizer interface { + Optimize(plan Node, opts *query.Options) (Node, annotations.Annotations) +} + +type plan struct { + expr Node + opts *query.Options + planOpts PlanOptions +} + +type PlanOptions struct { + DisableDuplicateLabelCheck bool +} + +// New creates a new logical plan from logical node. +func New(root Node, queryOpts *query.Options, planOpts PlanOptions) Plan { + return &plan{ + expr: root, + opts: queryOpts, + planOpts: planOpts, + } +} + +func NewFromAST(ast parser.Expr, queryOpts *query.Options, planOpts PlanOptions) (Plan, error) { + ast, err := promql.PreprocessExpr(ast, queryOpts.Start, queryOpts.End, queryOpts.Step) + if err != nil { + return nil, err + } + setOffsetForAtModifier(queryOpts.Start.UnixMilli(), ast) + setOffsetForInnerSubqueries(ast, queryOpts) + + // replace scanners by our logical nodes + expr := replacePrometheusNodes(ast) + + // the engine handles sorting at the presentation layer + expr = trimSorts(expr) + + // best effort evaluate constant expressions + expr = reduceConstantExpressions(expr) + + return &plan{ + expr: expr, + opts: queryOpts, + planOpts: planOpts, + }, nil +} + +// NewFromBytes creates a new logical plan from a byte slice created with Marshal. +// This method is used to deserialize a logical plan which has been sent over the wire. +func NewFromBytes(bytes []byte, queryOpts *query.Options, planOpts PlanOptions) (Plan, error) { + root, err := Unmarshal(bytes) + if err != nil { + return nil, err + } + // the engine handles sorting at the presentation layer + root = trimSorts(root) + + return &plan{ + expr: root, + opts: queryOpts, + planOpts: planOpts, + }, nil +} + +func getTimeRangesForSelector(qOpts *query.Options, n *parser.VectorSelector, parents []*Node, evalRange time.Duration) (int64, int64) { + start, end := qOpts.Start.UnixMilli(), qOpts.End.UnixMilli() + subqOffset, subqRange, subqTs := logicalSubqueryTimes(parents) + + if subqTs != nil { + // The timestamp on the subquery overrides the eval statement time ranges. + start = *subqTs + end = *subqTs + } + + if n.Timestamp != nil { + // The timestamp on the selector overrides everything. + start = *n.Timestamp + end = *n.Timestamp + } else { + offsetMilliseconds := subqOffset.Milliseconds() + start = start - offsetMilliseconds - subqRange.Milliseconds() + end -= offsetMilliseconds + } + + if evalRange == 0 { + start -= qOpts.LookbackDelta.Milliseconds() + } else { + start -= evalRange.Milliseconds() + } + + start -= n.OriginalOffset.Milliseconds() + end -= n.OriginalOffset.Milliseconds() + + if parse.IsExtFunction(extractFuncFromPath(parents)) { + // Buffer more so that we could reasonably + // inject a zero if there is only one point. + start -= int64(qOpts.ExtLookbackDelta.Milliseconds()) + } + + return start, end +} + +func extractFuncFromPath(p []*Node) string { + if len(p) == 0 { + return "" + } + switch n := (*(p[len(p)-1])).(type) { + case *Aggregation: + return n.Op.String() + case *FunctionCall: + return n.Func.Name + case *Binary: + // If we hit a binary expression we terminate since we only care about functions + // or aggregations over a single metric. + return "" + } + return extractFuncFromPath(p[:len(p)-1]) +} + +func (p *plan) Root() Node { + return p.expr +} + +// MinMaxTime returns the min and max timestamp that any selector in the query +// can read. +func MinMaxTime(root Node, qOpts *query.Options) (int64, int64) { + var minTimestamp, maxTimestamp int64 = math.MaxInt64, math.MinInt64 + // Whenever a MatrixSelector is evaluated, evalRange is set to the corresponding range. + // The evaluation of the VectorSelector inside then evaluates the given range and unsets + // the variable. + var evalRange time.Duration + + TraverseWithParents(nil, &root, func(parents []*Node, node *Node) { + switch n := (*node).(type) { + case *VectorSelector: + start, end := getTimeRangesForSelector(qOpts, n.VectorSelector, parents, evalRange) + if start < minTimestamp { + minTimestamp = start + } + if end > maxTimestamp { + maxTimestamp = end + } + evalRange = 0 + case *MatrixSelector: + evalRange = n.Range + } + }) + + if maxTimestamp == math.MinInt64 { + // This happens when there was no selector. Hence no time range to select. + minTimestamp = 0 + maxTimestamp = 0 + } + + return minTimestamp, maxTimestamp +} + +func (p *plan) Optimize(optimizers []Optimizer) (Plan, annotations.Annotations) { + annos := annotations.New() + for _, o := range optimizers { + var a annotations.Annotations + p.expr, a = o.Optimize(p.expr, p.opts) + annos.Merge(a) + } + // parens are just annoying and getting rid of them doesn't change the query + // NOTE: we need to do this here to not break the distributed optimizer since + // rendering subqueries String() method depends on parens sometimes. + expr := trimParens(p.expr) + + if !p.planOpts.DisableDuplicateLabelCheck { + expr = insertDuplicateLabelChecks(expr) + } + + return &plan{expr: expr, opts: p.opts}, *annos +} + +func Traverse(expr *Node, transform func(*Node)) { + children := (*expr).Children() + transform(expr) + for _, c := range children { + Traverse(c, transform) + } +} + +func TraverseWithParents(parents []*Node, current *Node, transform func(parents []*Node, node *Node)) { + children := (*current).Children() + transform(parents, current) + for _, c := range children { + TraverseWithParents(append(parents, current), c, transform) + } +} + +func TraverseBottomUp(parent *Node, current *Node, transform func(parent *Node, node *Node) bool) bool { + var stop bool + for _, c := range (*current).Children() { + stop = TraverseBottomUp(current, c, transform) || stop + } + return stop || transform(parent, current) +} + +func replacePrometheusNodes(plan parser.Expr) Node { + switch t := (plan).(type) { + case *parser.StringLiteral: + return &StringLiteral{Val: t.Val} + case *parser.NumberLiteral: + return &NumberLiteral{Val: t.Val} + case *parser.StepInvariantExpr: + // We expect functions to be pushed down into matrix selectors. This means that + // parents of matrixselector nodes are always expected to be functions, not step invariant + // operators. + if m, ok := t.Expr.(*parser.MatrixSelector); ok { + return replacePrometheusNodes(m) + } + return &StepInvariantExpr{Expr: replacePrometheusNodes(t.Expr)} + case *parser.MatrixSelector: + return &MatrixSelector{ + VectorSelector: &VectorSelector{ + VectorSelector: t.VectorSelector.(*parser.VectorSelector), + }, + Range: t.Range, + OriginalString: t.String(), + } + case *parser.VectorSelector: + return &VectorSelector{VectorSelector: t} + + // TODO: we dont yet have logical nodes for these, keep traversing here but set fields in-place + case *parser.Call: + if t.Func.Name == "timestamp" { + // pushed-down timestamp function + switch v := UnwrapParens(t.Args[0]).(type) { + case *parser.VectorSelector: + return &VectorSelector{VectorSelector: v, SelectTimestamp: true} + case *parser.StepInvariantExpr: + vs, ok := UnwrapParens(v.Expr).(*parser.VectorSelector) + if ok { + // Prometheus weirdness + if vs.Timestamp != nil { + vs.OriginalOffset = 0 + } + return &StepInvariantExpr{ + Expr: &VectorSelector{VectorSelector: vs, SelectTimestamp: true}, + } + } + } + } + args := make([]Node, len(t.Args)) + // nested timestamp functions + for i, arg := range t.Args { + args[i] = replacePrometheusNodes(arg) + } + return &FunctionCall{ + Func: *t.Func, + Args: args, + } + case *parser.ParenExpr: + return &Parens{ + Expr: replacePrometheusNodes(t.Expr), + } + case *parser.UnaryExpr: + return &Unary{ + Op: t.Op, + Expr: replacePrometheusNodes(t.Expr), + } + case *parser.AggregateExpr: + return &Aggregation{ + Op: t.Op, + Expr: replacePrometheusNodes(t.Expr), + Param: replacePrometheusNodes(t.Param), + Grouping: t.Grouping, + Without: t.Without, + } + case *parser.BinaryExpr: + return &Binary{ + Op: t.Op, + LHS: replacePrometheusNodes(t.LHS), + RHS: replacePrometheusNodes(t.RHS), + VectorMatching: t.VectorMatching, + ReturnBool: t.ReturnBool, + } + case *parser.SubqueryExpr: + return &Subquery{ + Expr: replacePrometheusNodes(t.Expr), + Range: t.Range, + OriginalOffset: t.OriginalOffset, + Offset: t.Offset, + Timestamp: t.Timestamp, + Step: t.Step, + StartOrEnd: t.StartOrEnd, + } + case nil: + return nil + } + panic("Unrecognized AST node") +} + +func trimSorts(expr Node) Node { + canTrimSorts := true + // We cannot trim inner sort if its an argument to a timestamp function. + // If we would do it we could transform "timestamp(sort(X))" into "timestamp(X)" + // Which might return actual timestamps of samples instead of query execution timestamp. + TraverseBottomUp(nil, &expr, func(parent, current *Node) bool { + if current == nil || parent == nil { + return true + } + e, pok := (*parent).(*FunctionCall) + f, cok := (*current).(*FunctionCall) + + if pok && cok { + if e.Func.Name == "timestamp" && strings.HasPrefix(f.Func.Name, "sort") { + canTrimSorts = false + return true + } + } + return false + }) + if !canTrimSorts { + return expr + } + TraverseBottomUp(nil, &expr, func(parent, current *Node) bool { + if current == nil || parent == nil { + return true + } + switch e := (*parent).(type) { + case *FunctionCall: + switch e.Func.Name { + case "sort", "sort_desc", "sort_by_label", "sort_by_label_desc": + *parent = *current + } + } + return false + }) + return expr +} + +func reduceConstantExpressions(expr Node) Node { + TraverseBottomUp(nil, &expr, func(parent, current *Node) bool { + if current == nil || parent == nil { + return true + } + switch tparent := (*parent).(type) { + case *Parens: + num, err := UnwrapFloat(tparent.Expr) + if err != nil { + return false + } + *parent = &NumberLiteral{Val: num} + case *Unary: + num, err := UnwrapFloat(tparent.Expr) + if err != nil { + return false + } + switch tparent.Op { + case parser.ADD: + *parent = &NumberLiteral{Val: num} + case parser.SUB: + *parent = &NumberLiteral{Val: -num} + } + case *Binary: + lnum, err := UnwrapFloat(tparent.LHS) + if err != nil { + return false + } + rnum, err := UnwrapFloat(tparent.RHS) + if err != nil { + return false + } + switch tparent.Op { + case parser.ADD: + *parent = &NumberLiteral{Val: lnum + rnum} + case parser.SUB: + *parent = &NumberLiteral{Val: lnum - rnum} + case parser.MUL: + *parent = &NumberLiteral{Val: lnum * rnum} + case parser.DIV: + *parent = &NumberLiteral{Val: lnum / rnum} + case parser.POW: + *parent = &NumberLiteral{Val: math.Pow(lnum, rnum)} + case parser.MOD: + *parent = &NumberLiteral{Val: math.Mod(lnum, rnum)} + default: + return false + } + } + return false + }) + return expr +} + +func trimParens(expr Node) Node { + TraverseBottomUp(nil, &expr, func(parent, current *Node) bool { + if current == nil || parent == nil { + return true + } + switch (*parent).(type) { + case *Parens: + *parent = *current + } + return false + }) + return expr +} + +func insertDuplicateLabelChecks(expr Node) Node { + Traverse(&expr, func(node *Node) { + switch t := (*node).(type) { + case *CheckDuplicateLabels: + return + case *Aggregation, *Unary, *Binary, *FunctionCall: + *node = &CheckDuplicateLabels{Expr: t} + case *VectorSelector: + if t.SelectTimestamp { + *node = &CheckDuplicateLabels{Expr: t} + } + } + }) + return expr +} + +// https://github.com/prometheus/prometheus/blob/dfae954dc1137568f33564e8cffda321f2867925/promql/engine.go#L754 +// subqueryTimes returns the sum of offsets and ranges of all subqueries in the path. +// If the @ modifier is used, then the offset and range is w.r.t. that timestamp +// (i.e. the sum is reset when we have @ modifier). +// The returned *int64 is the closest timestamp that was seen. nil for no @ modifier. +func subqueryTimes(path []parser.Node) (time.Duration, time.Duration, *int64) { + var ( + subqOffset, subqRange time.Duration + ts int64 = math.MaxInt64 + ) + for _, node := range path { + if n, ok := node.(*parser.SubqueryExpr); ok { + subqOffset += n.OriginalOffset + subqRange += n.Range + if n.Timestamp != nil { + // The @ modifier on subquery invalidates all the offset and + // range till now. Hence resetting it here. + subqOffset = n.OriginalOffset + subqRange = n.Range + ts = *n.Timestamp + } + } + } + var tsp *int64 + if ts != math.MaxInt64 { + tsp = &ts + } + return subqOffset, subqRange, tsp +} + +// Copy from https://github.com/prometheus/prometheus/blob/v2.39.1/promql/engine.go#L2658. +func setOffsetForAtModifier(evalTime int64, expr parser.Expr) { + getOffset := func(ts *int64, originalOffset time.Duration, path []parser.Node) time.Duration { + if ts == nil { + return originalOffset + } + subqOffset, _, subqTs := subqueryTimes(path) + if subqTs != nil { + subqOffset += time.Duration(evalTime-*subqTs) * time.Millisecond + } + + offsetForTs := time.Duration(evalTime-*ts) * time.Millisecond + offsetDiff := offsetForTs - subqOffset + return originalOffset + offsetDiff + } + + parser.Inspect(expr, func(node parser.Node, path []parser.Node) error { + switch n := node.(type) { + case *parser.VectorSelector: + n.Offset = getOffset(n.Timestamp, n.OriginalOffset, path) + + case *parser.MatrixSelector: + vs := n.VectorSelector.(*parser.VectorSelector) + vs.Offset = getOffset(vs.Timestamp, vs.OriginalOffset, path) + + case *parser.SubqueryExpr: + n.Offset = getOffset(n.Timestamp, n.OriginalOffset, path) + } + return nil + }) +} + +// logicalSubqueryTimes returns the sum of offsets and ranges of all subqueries in the path. +// If the @ modifier is used, then the offset and range is w.r.t. that timestamp +// (i.e. the sum is reset when we have @ modifier). +// The returned *int64 is the closest timestamp that was seen. nil for no @ modifier. +func logicalSubqueryTimes(path []*Node) (time.Duration, time.Duration, *int64) { + var ( + subqOffset, subqRange time.Duration + ts int64 = math.MaxInt64 + ) + for _, node := range path { + switch n := (*node).(type) { + case *Subquery: + subqOffset += n.OriginalOffset + subqRange += n.Range + if n.Timestamp != nil { + // The @ modifier on subquery invalidates all the offset and + // range till now. Hence resetting it here. + subqOffset = n.OriginalOffset + subqRange = n.Range + ts = *n.Timestamp + } + } + } + var tsp *int64 + if ts != math.MaxInt64 { + tsp = &ts + } + return subqOffset, subqRange, tsp +} + +func setOffsetForInnerSubqueries(expr parser.Expr, opts *query.Options) { + switch n := expr.(type) { + case *parser.SubqueryExpr: + nOpts := query.NestedOptionsForSubquery(opts, n.Step, n.Range, n.Offset) + setOffsetForAtModifier(nOpts.Start.UnixMilli(), n.Expr) + setOffsetForInnerSubqueries(n.Expr, nOpts) + default: + for _, c := range parser.Children(n) { + setOffsetForInnerSubqueries(c.(parser.Expr), opts) + } + } +} diff --git a/internal/promql-engine/logicalplan/plan_test.go b/internal/promql-engine/logicalplan/plan_test.go new file mode 100644 index 00000000000..9cd1605d5af --- /dev/null +++ b/internal/promql-engine/logicalplan/plan_test.go @@ -0,0 +1,435 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package logicalplan + +import ( + "fmt" + "regexp" + "sort" + "strings" + "testing" + "time" + + "github.com/thanos-io/promql-engine/query" + + "github.com/efficientgo/core/testutil" + "github.com/prometheus/prometheus/promql/parser" +) + +var spaces = regexp.MustCompile(`\s+`) +var openParenthesis = regexp.MustCompile(`\(\s+`) +var closedParenthesis = regexp.MustCompile(`\s+\)`) + +// renderExprTree renders the expression into a string. It is useful +// in tests to use strings for assertions in cases where the "String()" +// method might not yield enough information or would panic because of +// internal logical expression types. Implementations were largeley taken +// from upstream prometheus. +// +// TODO: maybe its better to Traverse the expression here and inject +// new nodes with prepared String methods? Like replacing MatrixSelector +// by testMatrixSelector that has a overridden string method? +func renderExprTree(expr Node) string { + switch t := expr.(type) { + case *NumberLiteral: + return fmt.Sprint(t.Val) + case *VectorSelector: + var b strings.Builder + base := t.VectorSelector.String() + if t.BatchSize > 0 { + base += fmt.Sprintf("[batch=%d]", t.BatchSize) + } + if t.Projection != nil { + sort.Strings(t.Projection.Labels) + if t.Projection.Include { + base += fmt.Sprintf("[projection=include(%s)]", strings.Join(t.Projection.Labels, ",")) + } else if len(t.Projection.Labels) > 0 { + base += fmt.Sprintf("[projection=exclude(%s)]", strings.Join(t.Projection.Labels, ",")) + } + } + if len(t.Filters) > 0 { + b.WriteString("filter(") + b.WriteString(fmt.Sprintf("%s", t.Filters)) + b.WriteString(", ") + b.WriteString(base) + b.WriteRune(')') + return b.String() + } + return base + case *MatrixSelector: + // Render the inner vector selector first + vsStr := renderExprTree(t.VectorSelector) + // Then add the range + return fmt.Sprintf("%s[%s]", vsStr, t.Range.String()) + case *Binary: + var b strings.Builder + b.WriteString(renderExprTree(t.LHS)) + b.WriteString(" ") + b.WriteString(t.Op.String()) + b.WriteString(" ") + if vm := t.VectorMatching; vm != nil && (len(vm.MatchingLabels) > 0 || vm.On) { + vmTag := "ignoring" + if vm.On { + vmTag = "on" + } + matching := fmt.Sprintf("%s (%s)", vmTag, strings.Join(vm.MatchingLabels, ", ")) + + if vm.Card == parser.CardManyToOne || vm.Card == parser.CardOneToMany { + vmCard := "right" + if vm.Card == parser.CardManyToOne { + vmCard = "left" + } + matching += fmt.Sprintf(" group_%s (%s)", vmCard, strings.Join(vm.Include, ", ")) + } + b.WriteString(matching) + b.WriteString(" ") + } + b.WriteString(renderExprTree(t.RHS)) + return b.String() + case *FunctionCall: + var b strings.Builder + b.Write([]byte(t.Func.Name)) + b.WriteRune('(') + for i := range t.Args { + if i > 0 { + b.WriteString(", ") + } + b.WriteString(renderExprTree(t.Args[i])) + } + b.WriteRune(')') + return b.String() + case *Aggregation: + var b strings.Builder + b.Write([]byte(t.Op.String())) + switch { + case t.Without: + b.WriteString(fmt.Sprintf(" without (%s) ", strings.Join(t.Grouping, ", "))) + case len(t.Grouping) > 0: + b.WriteString(fmt.Sprintf(" by (%s) ", strings.Join(t.Grouping, ", "))) + } + b.WriteRune('(') + if t.Param != nil { + b.WriteString(renderExprTree(t.Param)) + b.WriteString(", ") + } + b.WriteString(renderExprTree(t.Expr)) + b.WriteRune(')') + return b.String() + case *StepInvariantExpr: + return renderExprTree(t.Expr) + case *CheckDuplicateLabels: + return renderExprTree(t.Expr) + case *Subquery: + var b strings.Builder + + // Render the inner expression + innerExpr := renderExprTree(t.Expr) + b.WriteString(innerExpr) + + // Add the subquery range and step + b.WriteString(fmt.Sprintf("[%s:%s]", t.Range.String(), t.Step.String())) + return b.String() + default: + return t.String() + } +} + +func TestDefaultOptimizers(t *testing.T) { + cases := []struct { + name string + expr string + expected string + }{ + { + name: "common selectors", + expr: `sum(metric{a="b", c="d"}) / sum(metric{a="b"})`, + expected: `sum(filter([c="d"], metric{a="b"})) / sum(metric{a="b"})`, + }, + { + name: "common selectors with duplicate matchers", + expr: `sum(metric{a="b", c="d", a="b"}) / sum(metric{a="b"})`, + expected: `sum(filter([c="d"], metric{a="b"})) / sum(metric{a="b"})`, + }, + { + name: "common selectors with different operators", + expr: `sum(metric{a="b"}) / sum(metric{a=~"b"})`, + expected: `sum(metric{a="b"}) / sum(metric{a=~"b"})`, + }, + { + name: "common selectors with regex", + expr: `http_requests_total / on () group_left sum(http_requests_total{pod=~"p1.+"})`, + expected: `http_requests_total / on () group_left () sum(filter([pod=~"p1.+"], http_requests_total))`, + }, + { + name: "common selectors in different metrics", + expr: ` + sum(metric_1{a="b", c="d"}) / sum(metric_1{a="b"}) + + sum(metric_2{a="b", c="d"}) / sum(metric_2{a="b"}) +`, + expected: ` + sum(filter([c="d"], metric_1{a="b"})) / sum(metric_1{a="b"}) + + sum(filter([c="d"], metric_2{a="b"})) / sum(metric_2{a="b"})`, + }, + { + name: "different selectors", + expr: `sum(metric{a="b"}) / sum(metric{c="d"})`, + expected: `sum(metric{a="b"}) / sum(metric{c="d"})`, + }, + { + name: "different operator", + expr: `sum(metric{a="b"}) / sum(metric{a=~"b"})`, + expected: `sum(metric{a="b"}) / sum(metric{a=~"b"})`, + }, + { + name: "different metrics", + expr: `sum(metric_1{a="b"}) / sum(metric_2{a="b"})`, + expected: `sum(metric_1{a="b"}) / sum(metric_2{a="b"})`, + }, + { + name: "duplicate matchers", + expr: `metric_1{a="1", b="2", a="1"} / metric_2{a="1", b="2", a="1"}`, + expected: `metric_1{a="1",a="1",b="2"} / metric_2{a="1",a="1",b="2"}`, + }, + { + name: "duplicate matchers", + expr: `metric_1{a="1", b="2", a="1", e="f"} / metric_1{a="1", b="2", a="1"}`, + expected: `filter([e="f"], metric_1{a="1",a="1",b="2"}) / metric_1{a="1",a="1",b="2"}`, + }, + } + + for _, tcase := range cases { + t.Run(tcase.name, func(t *testing.T) { + expr, err := parser.ParseExpr(tcase.expr) + testutil.Ok(t, err) + + plan, _ := NewFromAST(expr, &query.Options{Start: time.Unix(0, 0), End: time.Unix(0, 0)}, PlanOptions{}) + optimizedPlan, _ := plan.Optimize(DefaultOptimizers) + expectedPlan := strings.Trim(spaces.ReplaceAllString(tcase.expected, " "), " ") + testutil.Equals(t, expectedPlan, renderExprTree(optimizedPlan.Root())) + }) + } +} + +func TestMatcherPropagation(t *testing.T) { + cases := []struct { + name string + expr string + expected string + }{ + { + name: "common matchers with same metric", + expr: `node_filesystem_files{host="$host", mountpoint="/"} - node_filesystem_files`, + expected: `node_filesystem_files{host="$host",mountpoint="/"} - node_filesystem_files`, + }, + { + name: "common matchers with same overlapping selectors", + expr: `node_filesystem_files{host="$host", mountpoint="/"} - node_filesystem_files{host!="$host"}`, + expected: `node_filesystem_files{host="$host",mountpoint="/"} - node_filesystem_files{host!="$host"}`, + }, + { + name: "common matchers with many-to-one", + expr: `node_filesystem_files{host="$host",mountpoint="/"} - on () group_left () node_filesystem_files_free`, + expected: `node_filesystem_files{host="$host",mountpoint="/"} - on () group_left () node_filesystem_files_free`, + }, + { + name: "common matchers", + expr: `node_filesystem_files{host="$host", mountpoint="/"} - node_filesystem_files_free`, + expected: `node_filesystem_files{host="$host",mountpoint="/"} - node_filesystem_files_free{host="$host",mountpoint="/"}`, + }, + { + name: "vector matching on specific labels", + expr: `node_filesystem_files{host="$host", mountpoint="/"} - on(host) node_filesystem_files_free`, + expected: `node_filesystem_files{host="$host",mountpoint="/"} - on (host) node_filesystem_files_free{host="$host"}`, + }, + { + name: "vector matching ignoring specific labels", + expr: `node_filesystem_files{host="$host", mountpoint="/"} - ignoring(mountpoint) node_filesystem_files_free`, + expected: `node_filesystem_files{host="$host",mountpoint="/"} - ignoring (mountpoint) node_filesystem_files_free{host="$host"}`, + }, + { + name: "vector matching on metric name", + expr: `node_filesystem_files{host="$host"} - on(__name__, host) node_filesystem_files_free`, + expected: `node_filesystem_files{host="$host"} - on (__name__, host) node_filesystem_files_free`, + }, + { + name: "OR operation with common labels", + expr: `node_filesystem_files{host="$host", mountpoint="/"} or node_filesystem_files_free`, + expected: `node_filesystem_files{host="$host",mountpoint="/"} or node_filesystem_files_free`, + }, + { + name: "OR operation with conflicting labels", + expr: `node_filesystem_files{host="host1"} or node_filesystem_files{host="host2"}`, + expected: `node_filesystem_files{host="host1"} or node_filesystem_files{host="host2"}`, + }, + { + name: "UNLESS operation with common labels", + expr: `node_filesystem_files{host="$host", mountpoint="/"} unless node_filesystem_files_free`, + expected: `node_filesystem_files{host="$host",mountpoint="/"} unless node_filesystem_files_free`, + }, + { + name: "one-to-many with group_right", + expr: `node_filesystem_files - on(host) group_right(mountpoint) node_filesystem_files_free{host="$host"}`, + expected: `node_filesystem_files{host="$host"} - on (host) group_right (mountpoint) node_filesystem_files_free{host="$host"}`, + }, + { + name: "many-to-one with group_left", + expr: `node_filesystem_files{host="$host",mountpoint="/"} - on(host) group_left(device) node_filesystem_files_free`, + expected: `node_filesystem_files{host="$host",mountpoint="/"} - on (host) group_left (device) node_filesystem_files_free{host="$host"}`, + }, + { + name: "non-mergeable matchers with regex", + expr: `node_filesystem_files{host=~"host1.*"} - node_filesystem_files_free{host=~"host2.*"}`, + expected: `node_filesystem_files{host=~"host1.*"} - node_filesystem_files_free{host=~"host2.*"}`, + }, + { + name: "overlapping matchers with different types", + expr: `node_filesystem_files{host=~"host.*",env="prod"} - node_filesystem_files_free{host="host1",env!="dev"}`, + expected: `node_filesystem_files{env="prod",host="host1"} - node_filesystem_files_free{env="prod",host="host1"}`, + }, + { + name: "complex vector matching with multiple labels", + expr: `node_filesystem_files{host="$host",env="prod",dc="us-east"} - on(host,dc) group_left(device) node_filesystem_files_free`, + expected: `node_filesystem_files{dc="us-east",env="prod",host="$host"} - on (host, dc) group_left (device) node_filesystem_files_free{dc="us-east",host="$host"}`, + }, + { + name: "OR with vector matching", + expr: `node_filesystem_files{host="$host"} - on(host) group_left() node_filesystem_files_free or node_filesystem_files_reserved`, + expected: `node_filesystem_files{host="$host"} - on (host) group_left () node_filesystem_files_free{host="$host"} or node_filesystem_files_reserved`, + }, + { + name: "UNLESS with vector matching", + expr: `node_filesystem_files{host="$host"} unless on(host,mountpoint) node_filesystem_files_free`, + expected: `node_filesystem_files{host="$host"} unless on (host, mountpoint) node_filesystem_files_free`, + }, + { + name: "mixed operations with common labels", + expr: `node_filesystem_files{host="$host"} - node_filesystem_files_free or node_filesystem_files_reserved`, + expected: `node_filesystem_files{host="$host"} - node_filesystem_files_free{host="$host"} or node_filesystem_files_reserved`, + }, + } + + optimizers := []Optimizer{PropagateMatchersOptimizer{}} + for _, tcase := range cases { + t.Run(tcase.name, func(t *testing.T) { + t.Parallel() + expr, err := parser.ParseExpr(tcase.expr) + testutil.Ok(t, err) + + plan, _ := NewFromAST(expr, &query.Options{Start: time.Unix(0, 0), End: time.Unix(0, 0)}, PlanOptions{}) + optimizedPlan, _ := plan.Optimize(optimizers) + expectedPlan := strings.Trim(spaces.ReplaceAllString(tcase.expected, " "), " ") + testutil.Equals(t, expectedPlan, renderExprTree(optimizedPlan.Root())) + }) + } +} + +func TestTrimSorts(t *testing.T) { + cases := []struct { + name string + expr string + expected string + }{ + // this test case is ok since the engine determines sorting order + // before running optimziers + { + name: "simple sort", + expr: "sort(X)", + expected: "X", + }, + { + name: "sort", + expr: "sum(sort(X))", + expected: "sum(X)", + }, + { + name: "nested", + expr: "sum(sort(rate(X[1m])))", + expected: "sum(rate(X[1m]))", + }, + { + name: "weirdly nested", + expr: "sum(sort(sqrt(sort(X))))", + expected: "sum(sqrt(X))", + }, + { + name: "sort in binary expression", + expr: "sort(sort(sqrt(X))/sort(sqrt(Y)))", + expected: "sqrt(X) / sqrt(Y)", + }, + { + name: "sort in argument to timestamp function", + expr: "timestamp(sort(X))", + expected: "timestamp(sort(X))", + }, + } + for _, tcase := range cases { + t.Run(tcase.name, func(t *testing.T) { + expr, err := parser.ParseExpr(tcase.expr) + testutil.Ok(t, err) + + plan, _ := NewFromAST(expr, &query.Options{}, PlanOptions{}) + testutil.Equals(t, tcase.expected, plan.Root().String()) + }) + } +} + +func TestReduceConstantExpressions(t *testing.T) { + cases := []struct { + name string + expr string + expected string + }{ + { + name: "binary add", + expr: "5+3", + expected: "8", + }, + { + name: "binary pow", + expr: "2^8", + expected: "256", + }, + { + name: "binary mod", + expr: "12%5", + expected: "2", + }, + { + name: "unary negation", + expr: "2+(-5)", + expected: "(-3)", + }, + { + name: "function", + expr: "predict_linear(X[1h], 24*60)", + expected: "predict_linear(X[1h], 1440)", + }, + { + name: "function and parens", + expr: "predict_linear(X[1h], (2*12)*60)", + expected: "predict_linear(X[1h], 1440)", + }, + { + name: "aggregation", + expr: "topk((3), X)", + expected: "topk(3, X)", + }, + } + for _, tcase := range cases { + t.Run(tcase.name, func(t *testing.T) { + expr, err := parser.ParseExpr(tcase.expr) + testutil.Ok(t, err) + + plan, _ := NewFromAST(expr, &query.Options{}, PlanOptions{}) + testutil.Equals(t, tcase.expected, plan.Root().String()) + }) + } +} + +func cleanUp(replacements map[string]*regexp.Regexp, expr string) string { + for replacement, match := range replacements { + expr = match.ReplaceAllString(expr, replacement) + } + return strings.Trim(expr, " ") +} diff --git a/internal/promql-engine/logicalplan/projection.go b/internal/promql-engine/logicalplan/projection.go new file mode 100644 index 00000000000..cbca7c983bb --- /dev/null +++ b/internal/promql-engine/logicalplan/projection.go @@ -0,0 +1,273 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package logicalplan + +import ( + "maps" + "slices" + + "github.com/thanos-io/promql-engine/query" + + "github.com/prometheus/prometheus/promql/parser" + "github.com/prometheus/prometheus/util/annotations" +) + +type ProjectionOptimizer struct { + SeriesHashLabel string +} + +func (p ProjectionOptimizer) Optimize(plan Node, _ *query.Options) (Node, annotations.Annotations) { + p.pushProjection(&plan, nil) + return plan, nil +} + +// pushProjection recursively traverses the tree and pushes projection information down. +func (p ProjectionOptimizer) pushProjection(node *Node, projection *Projection) { + switch n := (*node).(type) { + case *VectorSelector: + if projection != nil { + n.Projection = projection + } else { + // Set dummy projection. + n.Projection = &Projection{} + } + + case *Aggregation: + // Special handling for aggregation functions that need all labels + // regardless of grouping (topk, bottomk, limitk, limit_ratio) + switch n.Op { + case parser.TOPK, parser.BOTTOMK, parser.LIMITK, parser.LIMIT_RATIO: + // These functions need all labels, so clear any requirements + p.pushProjection(&n.Expr, nil) + return + } + + // For aggregations, we directly use the grouping labels + grouping := n.Grouping + groupingProjection := &Projection{ + Labels: grouping, + Include: !n.Without, + } + // Note that we don't push projection to Aggregation.Param as they are not + // selecting data for the aggregation. + p.pushProjection(&n.Expr, groupingProjection) + + if p.SeriesHashLabel != "" && n.Without { + n.Grouping = append(grouping, p.SeriesHashLabel) + } + + case *Binary: + var highCard, lowCard = n.LHS, n.RHS + + if n.VectorMatching == nil || (!n.VectorMatching.On && len(n.VectorMatching.MatchingLabels) == 0) { + if IsConstantExpr(lowCard) { + p.pushProjection(&highCard, projection) + } else { + p.pushProjection(&highCard, nil) + } + + if IsConstantExpr(highCard) { + p.pushProjection(&lowCard, projection) + } else { + p.pushProjection(&lowCard, nil) + } + return + } + + if n.VectorMatching.Card == parser.CardOneToOne { + proj := &Projection{ + Labels: n.VectorMatching.MatchingLabels, + Include: n.VectorMatching.On, + } + + for _, child := range n.Children() { + p.pushProjection(child, proj) + } + + if !n.VectorMatching.On && p.SeriesHashLabel != "" { + n.VectorMatching.MatchingLabels = append(n.VectorMatching.MatchingLabels, p.SeriesHashLabel) + } + return + } + + if n.VectorMatching.Card == parser.CardOneToMany { + highCard, lowCard = lowCard, highCard + } + + // Handle high card side projection. Only ignoring mode is supported. + hcProjection := &Projection{} + // Only push projection for high card side if there is an outer projection available + // to remove series hash + if projection != nil && projection.Include { + // Include labels are from low card side so we don't need to fetch + // them from high card side if include labels are not used as join keys. + hcProjection.Labels = n.VectorMatching.Include + if !n.VectorMatching.On { + hcProjection.Labels = intersect(hcProjection.Labels, n.VectorMatching.MatchingLabels) + } + } + if len(hcProjection.Labels) > 1 { + p.pushProjection(&highCard, hcProjection) + } else { + // If there is only 1 label to project then it is not worth to push projection + // down to high card side as calculating hash might be more expensive. + p.pushProjection(&highCard, nil) + } + + // Handle low card side projection. + lcProjection := extendProjection(Projection{ + Include: n.VectorMatching.On, + Labels: n.VectorMatching.MatchingLabels, + }, n.VectorMatching.Include) + p.pushProjection(&lowCard, &lcProjection) + + if !n.VectorMatching.On && p.SeriesHashLabel != "" { + n.VectorMatching.MatchingLabels = append(n.VectorMatching.MatchingLabels, p.SeriesHashLabel) + } + return + + case *FunctionCall: + // Handle function-specific label requirements. + updatedProjection := getFunctionLabelRequirements(n.Func.Name, n.Args, projection) + for _, child := range n.Children() { + p.pushProjection(child, updatedProjection) + } + + default: + // For other node types, propagate to children + for _, child := range (*node).Children() { + p.pushProjection(child, projection) + } + } +} + +func extendProjection(projection Projection, lbls []string) Projection { + var extendedLabels []string + if projection.Include { + extendedLabels = union(projection.Labels, lbls) + } else { + extendedLabels = subtract(projection.Labels, lbls) + } + return Projection{ + Include: projection.Include, + Labels: extendedLabels, + } +} + +// unwrapStepInvariantExpr recursively unwraps step invariant expressions to get to the underlying node. +func unwrapStepInvariantExpr(node Node) Node { + if stepInvariant, ok := node.(*StepInvariantExpr); ok { + return unwrapStepInvariantExpr(stepInvariant.Expr) + } + return node +} + +// getFunctionLabelRequirements returns an updated projection based on function-specific requirements. +func getFunctionLabelRequirements(funcName string, args []Node, projection *Projection) *Projection { + if projection == nil { + projection = &Projection{} + } + result := &Projection{ + Labels: make([]string, len(projection.Labels)), + Include: projection.Include, + } + copy(result.Labels, projection.Labels) + + // Add function-specific required labels + switch funcName { + case "absent_over_time", "absent", "scalar": + return &Projection{ + Labels: []string{}, + Include: true, + } + case "histogram_quantile": + // Unsafe to push projection down for histogram_quantile as it requires le label. + return nil + case "label_replace": + dstArg := unwrapStepInvariantExpr(args[1]) + if dstLit, ok := dstArg.(*StringLiteral); ok { + dstLabel := dstLit.Val + needed := slices.Contains(result.Labels, dstLabel) + needSourceLabels := (result.Include && needed) || (!result.Include && !needed) + if !needSourceLabels { + return result + } + + srcArg := unwrapStepInvariantExpr(args[3]) + if strLit, ok := srcArg.(*StringLiteral); ok { + if result.Include && needed { + result.Labels = append(result.Labels, strLit.Val) + } else { + result.Labels = slices.DeleteFunc(result.Labels, func(s string) bool { + return s == strLit.Val + }) + } + } + } + case "label_join": + dstArg := unwrapStepInvariantExpr(args[1]) + if dstLit, ok := dstArg.(*StringLiteral); ok { + dstLabel := dstLit.Val + needed := slices.Contains(result.Labels, dstLabel) + needSourceLabels := (result.Include && needed) || (!result.Include && !needed) + if !needSourceLabels { + return result + } + + // Only if the destination label is needed, we need the source labels + for i := 3; i < len(args); i++ { + srcArg := unwrapStepInvariantExpr(args[i]) + if strLit, ok := srcArg.(*StringLiteral); ok { + if result.Include && needed { + result.Labels = append(result.Labels, strLit.Val) + } else { + result.Labels = slices.DeleteFunc(result.Labels, func(s string) bool { + return s == strLit.Val + }) + } + } + } + } + } + + return result +} + +// union returns the union of two string slices. +func union(l1 []string, l2 []string) []string { + m := make(map[string]struct{}) + for _, s := range l1 { + m[s] = struct{}{} + } + for _, s := range l2 { + m[s] = struct{}{} + } + return slices.Collect(maps.Keys(m)) +} + +// subtract returns the intersection of two string slices. +func subtract(l1 []string, l2 []string) []string { + m := make(map[string]struct{}) + for _, s := range l1 { + m[s] = struct{}{} + } + for _, s := range l2 { + delete(m, s) + } + return slices.Collect(maps.Keys(m)) +} + +func intersect(l1 []string, l2 []string) []string { + m := make(map[string]struct{}) + for _, s := range l1 { + m[s] = struct{}{} + } + var result []string + for _, s := range l2 { + if _, ok := m[s]; ok { + result = append(result, s) + } + } + return result +} diff --git a/internal/promql-engine/logicalplan/projection_test.go b/internal/promql-engine/logicalplan/projection_test.go new file mode 100644 index 00000000000..d8f8834de93 --- /dev/null +++ b/internal/promql-engine/logicalplan/projection_test.go @@ -0,0 +1,502 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package logicalplan + +import ( + "slices" + "sort" + "testing" + "time" + + "github.com/thanos-io/promql-engine/query" + + "github.com/efficientgo/core/testutil" + "github.com/prometheus/prometheus/promql/parser" +) + +func TestProjectionOptimizer(t *testing.T) { + cases := []struct { + name string + expr string + expected string + }{ + { + name: "simple aggregation by no labels", + expr: `sum (metric{instance="a", job="b", env="c"})`, + expected: `sum(metric{env="c",instance="a",job="b"}[projection=include()])`, + }, + { + name: "simple aggregation without no labels", + expr: `sum without() (metric{instance="a", job="b", env="c"})`, + expected: `sum without (__series_hash__) (metric{env="c",instance="a",job="b"})`, + }, + { + name: "simple aggregation", + expr: `sum by (job) (metric{instance="a", job="b", env="c"})`, + expected: `sum by (job) (metric{env="c",instance="a",job="b"}[projection=include(job)])`, + }, + { + name: "multiple aggregations", + expr: `sum by (job) (metric{instance="a", job="b", env="c"}) / count by (job) (metric{instance="a", job="b", env="c"})`, + expected: `sum by (job) (metric{env="c",instance="a",job="b"}[projection=include(job)]) / count by (job) (metric{env="c",instance="a",job="b"}[projection=include(job)])`, + }, + { + name: "binary operation with vector matching", + expr: `metric{instance="a", job="b"} * on(job) metric{instance="c", job="b"}`, + expected: `metric{instance="a",job="b"}[projection=include(job)] * on (job) metric{instance="c",job="b"}[projection=include(job)]`, + }, + { + name: "function call", + expr: `rate(metric{instance="a", job="b"}[5m])`, + expected: `rate(metric{instance="a",job="b"}[5m0s])`, + }, + { + name: "complex query with multiple operations", + expr: `sum by (job) (rate(test_metric{instance="a", job="b", env="c"}[5m])) / on(job) group_left count by (job) (metric{instance="d", job="b", env="e"})`, + expected: `sum by (job) (rate(test_metric{env="c",instance="a",job="b"}[projection=include(job)][5m0s])) / on (job) group_left () count by (job) (metric{env="e",instance="d",job="b"}[projection=include(job)])`, + }, + { + name: "aggregation with without", + expr: `sum without (instance) (metric{instance="a", job="b", env="c"})`, + expected: `sum without (instance, __series_hash__) (metric{env="c",instance="a",job="b"}[projection=exclude(instance)])`, + }, + { + name: "subquery with aggregation", + expr: `sum by (job) (count_over_time(up{job="prometheus"}[30m:1m]))`, + expected: `sum by (job) (count_over_time(up{job="prometheus"}[projection=include(job)][30m0s:1m0s]))`, + }, + { + name: "label_replace with required destination label", + expr: `sum by (new_job) (label_replace(metric{instance="a", job="b", env="c"}, "new_job", "$1", "job", "(.+)"))`, + expected: `sum by (new_job) (label_replace(metric{env="c",instance="a",job="b"}[projection=include(job,new_job)], "new_job", "$1", "job", "(.+)"))`, + }, + { + name: "label_replace with unrequired destination label", + expr: `sum by (instance) (label_replace(metric{instance="a", job="b", env="c"}, "new_job", "$1", "job", "(.+)"))`, + expected: `sum by (instance) (label_replace(metric{env="c",instance="a",job="b"}[projection=include(instance)], "new_job", "$1", "job", "(.+)"))`, + }, + { + name: "label_join with required destination label", + expr: `sum by (combined) (label_join(metric{instance="a", job="b", env="c"}, "combined", "-", "job", "env"))`, + expected: `sum by (combined) (label_join(metric{env="c",instance="a",job="b"}[projection=include(combined,env,job)], "combined", "-", "job", "env"))`, + }, + { + name: "label_join with unrequired destination label", + expr: `sum by (instance) (label_join(metric{instance="a", job="b", env="c"}, "combined", "-", "job", "env"))`, + expected: `sum by (instance) (label_join(metric{env="c",instance="a",job="b"}[projection=include(instance)], "combined", "-", "job", "env"))`, + }, + { + name: "histogram_quantile with aggregation inside", + expr: `histogram_quantile(0.9, sum by (le, job) (rate(http_request_duration_seconds_bucket{job="api-server", instance="localhost:9090"}[5m])))`, + expected: `histogram_quantile(0.9, sum by (le, job) (rate(http_request_duration_seconds_bucket{instance="localhost:9090",job="api-server"}[projection=include(job,le)][5m0s])))`, + }, + { + name: "label_replace with grouping on original label", + expr: `sum by (job) (label_replace(metric{instance="a", job="b", env="c"}, "new_job", "$1", "env", "(.+)"))`, + expected: `sum by (job) (label_replace(metric{env="c",instance="a",job="b"}[projection=include(job)], "new_job", "$1", "env", "(.+)"))`, + }, + { + name: "label_replace with grouping on different label", + expr: `sum by (instance) (label_replace(metric{instance="a", job="b", env="c"}, "new_job", "$1", "job", "(.+)"))`, + expected: `sum by (instance) (label_replace(metric{env="c",instance="a",job="b"}[projection=include(instance)], "new_job", "$1", "job", "(.+)"))`, + }, + { + name: "label_join with grouping on original label", + expr: `sum by (job) (label_join(metric{instance="a", job="b", env="c"}, "combined", "-", "env", "instance"))`, + expected: `sum by (job) (label_join(metric{env="c",instance="a",job="b"}[projection=include(job)], "combined", "-", "env", "instance"))`, + }, + { + name: "label_join with grouping on different label", + expr: `sum by (env) (label_join(metric{instance="a", job="b", env="c"}, "combined", "-", "job", "instance"))`, + expected: `sum by (env) (label_join(metric{env="c",instance="a",job="b"}[projection=include(env)], "combined", "-", "job", "instance"))`, + }, + { + name: "binary operation with ignoring", + expr: `metric{instance="a", job="b", env="c"} * ignoring(instance) metric{instance="d", job="b", env="c"}`, + expected: `metric{env="c",instance="a",job="b"}[projection=exclude(instance)] * ignoring (instance, __series_hash__) metric{env="c",instance="d",job="b"}[projection=exclude(instance)]`, + }, + { + name: "binary operation with ignoring and group_left", + expr: `metric{instance="a", job="b", env="c"} * ignoring(instance) group_left(env) metric{instance="d", job="b"}`, + expected: `metric{env="c",instance="a",job="b"} * ignoring (instance, __series_hash__) group_left (env) metric{instance="d",job="b"}[projection=exclude(instance)]`, + }, + { + name: "binary operation with ignoring and group_right", + expr: `metric{instance="a", job="b"} * ignoring(job) group_right(instance) metric{instance="d", job="b", env="e"}`, + expected: `metric{instance="a",job="b"}[projection=exclude(job)] * ignoring (job, __series_hash__) group_right (instance) metric{env="e",instance="d",job="b"}`, + }, + { + name: "aggregation with binary operation using on", + expr: `sum(metric1{instance="a", job="b", env="c"} * on(job) metric2{instance="d", job="b", env="e"})`, + expected: `sum(metric1{env="c",instance="a",job="b"}[projection=include(job)] * on (job) metric2{env="e",instance="d",job="b"}[projection=include(job)])`, + }, + { + name: "aggregation with binary operation using ignoring", + expr: `sum(metric1{instance="a", job="b", env="c"} * ignoring(instance) metric2{instance="d", job="b", env="c"})`, + expected: `sum(metric1{env="c",instance="a",job="b"}[projection=exclude(instance)] * ignoring (instance, __series_hash__) metric2{env="c",instance="d",job="b"}[projection=exclude(instance)])`, + }, + { + name: "aggregation by label with binary operation using on and group_left", + expr: `sum by (job) (metric1{instance="a", job="b", env="c"} * on(job) group_left(env) metric2{instance="d", job="b"})`, + expected: `sum by (job) (metric1{env="c",instance="a",job="b"} * on (job) group_left (env) metric2{instance="d",job="b"}[projection=include(env,job)])`, + }, + { + name: "aggregation by label with binary operation using on and group_right", + expr: `sum by (job) (metric1{instance="a", job="b"} * on(job) group_right(instance) metric2{instance="d", job="b", env="e"})`, + expected: `sum by (job) (metric1{instance="a",job="b"}[projection=include(instance,job)] * on (job) group_right (instance) metric2{env="e",instance="d",job="b"})`, + }, + { + name: "aggregation by label with binary operation using ignoring and group_left", + expr: `sum by (job) (metric1{instance="a", job="b", env="c"} * ignoring(instance) group_left(env) metric2{instance="d", job="b"})`, + expected: `sum by (job) (metric1{env="c",instance="a",job="b"} * ignoring (instance, __series_hash__) group_left (env) metric2{instance="d",job="b"}[projection=exclude(instance)])`, + }, + { + name: "aggregation by label with binary operation using ignoring and group_right", + expr: `sum by (job) (metric1{instance="a", job="b"} * ignoring(instance) group_right(env) metric2{instance="d", job="b", env="e"})`, + expected: `sum by (job) (metric1{instance="a",job="b"}[projection=exclude(instance)] * ignoring (instance, __series_hash__) group_right (env) metric2{env="e",instance="d",job="b"})`, + }, + { + name: "aggregation without label with binary operation using on", + expr: `sum without (instance) (metric1{instance="a", job="b", env="c"} * on(job) metric2{instance="d", job="b", env="e"})`, + expected: `sum without (instance, __series_hash__) (metric1{env="c",instance="a",job="b"}[projection=include(job)] * on (job) metric2{env="e",instance="d",job="b"}[projection=include(job)])`, + }, + { + name: "aggregation without label with binary operation using ignoring", + expr: `sum without (instance) (metric1{instance="a", job="b", env="c"} * ignoring(instance) metric2{instance="d", job="b", env="c"})`, + expected: `sum without (instance, __series_hash__) (metric1{env="c",instance="a",job="b"}[projection=exclude(instance)] * ignoring (instance, __series_hash__) metric2{env="c",instance="d",job="b"}[projection=exclude(instance)])`, + }, + { + name: "binary operation with on", + expr: `metric{instance="a", job="b", env="c"} * on(job) metric{instance="d", job="b", env="e"}`, + expected: `metric{env="c",instance="a",job="b"}[projection=include(job)] * on (job) metric{env="e",instance="d",job="b"}[projection=include(job)]`, + }, + { + name: "binary operation with on and group_right", + expr: `metric{instance="a", job="b"} * on(job) group_right(instance) metric{instance="d", job="b", env="e"}`, + expected: `metric{instance="a",job="b"}[projection=include(instance,job)] * on (job) group_right (instance) metric{env="e",instance="d",job="b"}`, + }, + { + name: "nested aggregation", + expr: `sum by (job) (avg by (job, instance) (metric{instance="a", job="b", env="c"}))`, + expected: `sum by (job) (avg by (job, instance) (metric{env="c",instance="a",job="b"}[projection=include(instance,job)]))`, + }, + { + name: "nested aggregation with without", + expr: `sum without (instance) (avg without (env) (metric{instance="a", job="b", env="c"}))`, + expected: `sum without (instance, __series_hash__) (avg without (env, __series_hash__) (metric{env="c",instance="a",job="b"}[projection=exclude(env)]))`, + }, + { + name: "nested aggregation with outer by and inner without", + expr: `sum by (job) (avg without (env) (metric{instance="a", job="b", env="c"}))`, + expected: `sum by (job) (avg without (env, __series_hash__) (metric{env="c",instance="a",job="b"}[projection=exclude(env)]))`, + }, + { + name: "nested aggregation with outer without and inner by", + expr: `sum without (env) (avg by (job, instance) (metric{instance="a", job="b", env="c"}))`, + expected: `sum without (env, __series_hash__) (avg by (job, instance) (metric{env="c",instance="a",job="b"}[projection=include(instance,job)]))`, + }, + { + name: "triple nested aggregation with mixed by and without", + expr: `sum by (job) (count without (env) (avg by (job, instance) (metric{instance="a", job="b", env="c"})))`, + expected: `sum by (job) (count without (env, __series_hash__) (avg by (job, instance) (metric{env="c",instance="a",job="b"}[projection=include(instance,job)])))`, + }, + { + name: "nested aggregation with different by labels", + expr: `sum by (job) (avg by (instance) (metric{instance="a", job="b", env="c"}))`, + expected: `sum by (job) (avg by (instance) (metric{env="c",instance="a",job="b"}[projection=include(instance)]))`, + }, + { + name: "aggregation with histogram_quantile", + expr: `sum by (job) (histogram_quantile(0.9, rate(http_request_duration_seconds_bucket{instance="a", job="b"}[5m])))`, + expected: `sum by (job) (histogram_quantile(0.9, rate(http_request_duration_seconds_bucket{instance="a",job="b"}[5m0s])))`, + }, + { + name: "topk aggregation", + expr: `topk(3, metric{instance="a", job="b", env="c"})`, + expected: `topk(3, metric{env="c",instance="a",job="b"})`, + }, + { + name: "bottomk aggregation", + expr: `bottomk(5, metric{instance="a", job="b", env="c"})`, + expected: `bottomk(5, metric{env="c",instance="a",job="b"})`, + }, + { + name: "topk with by clause", + expr: `topk by (job) (3, metric{instance="a", job="b", env="c"})`, + expected: `topk by (job) (3, metric{env="c",instance="a",job="b"})`, + }, + { + name: "bottomk with by clause", + expr: `bottomk by (job) (5, metric{instance="a", job="b", env="c"})`, + expected: `bottomk by (job) (5, metric{env="c",instance="a",job="b"})`, + }, + { + name: "topk with outer aggregation", + expr: `sum by (job) (topk(3, metric{instance="a", job="b", env="c"}))`, + expected: `sum by (job) (topk(3, metric{env="c",instance="a",job="b"}))`, + }, + { + name: "scalar function", + expr: `scalar(metric{instance="a", job="b", env="c"})`, + expected: `scalar(metric{env="c",instance="a",job="b"})`, + }, + { + name: "absent function", + expr: `absent(metric{instance="a", job="b", env="c"})`, + expected: `absent(metric{env="c",instance="a",job="b"})`, + }, + { + name: "absent_over_time function", + expr: `absent_over_time(metric{instance="a", job="b", env="c"}[5m])`, + expected: `absent_over_time(metric{env="c",instance="a",job="b"}[5m0s])`, + }, + { + name: "scalar function with aggregation", + expr: `sum by (job) (scalar(metric{instance="a", job="b", env="c"}))`, + expected: `sum by (job) (scalar(metric{env="c",instance="a",job="b"}))`, + }, + { + name: "absent function with aggregation", + expr: `sum by (job) (absent(metric{instance="a", job="b", env="c"}))`, + expected: `sum by (job) (absent(metric{env="c",instance="a",job="b"}))`, + }, + { + name: "absent_over_time function with aggregation", + expr: `sum by (job) (absent_over_time(metric{instance="a", job="b", env="c"}[5m]))`, + expected: `sum by (job) (absent_over_time(metric{env="c",instance="a",job="b"}[5m0s]))`, + }, + } + + for _, tc := range cases { + if tc.name != "simple aggregation by no labels" { + continue + } + t.Run(tc.name, func(t *testing.T) { + expr, err := parser.ParseExpr(tc.expr) + testutil.Ok(t, err) + + plan, err := NewFromAST(expr, &query.Options{Start: time.Unix(0, 0), End: time.Unix(0, 0)}, PlanOptions{}) + testutil.Ok(t, err) + optimizer := ProjectionOptimizer{SeriesHashLabel: "__series_hash__"} + optimizedPlan, _ := optimizer.Optimize(plan.Root(), nil) + + result := renderExprTree(optimizedPlan) + testutil.Equals(t, tc.expected, result) + }) + } +} + +func TestGetFunctionLabelRequirements(t *testing.T) { + tests := []struct { + name string + funcName string + args []Node + projection *Projection + expected *Projection + }{ + { + name: "label_replace with destination label needed", + funcName: "label_replace", + args: []Node{ + &VectorSelector{}, + &StringLiteral{Val: "new_label"}, + &StringLiteral{Val: "replacement"}, + &StringLiteral{Val: "src_label"}, + &StringLiteral{Val: "regex"}, + }, + projection: &Projection{ + Labels: []string{"new_label"}, + Include: true, + }, + expected: &Projection{ + Labels: []string{"new_label", "src_label"}, + Include: true, + }, + }, + { + name: "label_replace with destination label not needed", + funcName: "label_replace", + args: []Node{ + &VectorSelector{}, + &StringLiteral{Val: "new_label"}, + &StringLiteral{Val: "replacement"}, + &StringLiteral{Val: "src_label"}, + &StringLiteral{Val: "regex"}, + }, + projection: &Projection{ + Labels: []string{"other_label"}, + Include: true, + }, + expected: &Projection{ + Labels: []string{"other_label"}, + Include: true, + }, + }, + { + name: "label_replace with without clause", + funcName: "label_replace", + args: []Node{ + &VectorSelector{}, + &StringLiteral{Val: "new_label"}, + &StringLiteral{Val: "replacement"}, + &StringLiteral{Val: "src_label"}, + &StringLiteral{Val: "regex"}, + }, + projection: &Projection{ + Labels: []string{"other_label"}, + Include: false, + }, + expected: &Projection{ + Labels: []string{"other_label"}, + Include: false, + }, + }, + { + name: "label_join with destination label needed", + funcName: "label_join", + args: []Node{ + &VectorSelector{}, + &StringLiteral{Val: "new_label"}, + &StringLiteral{Val: "separator"}, + &StringLiteral{Val: "src_label1"}, + &StringLiteral{Val: "src_label2"}, + }, + projection: &Projection{ + Labels: []string{"new_label"}, + Include: true, + }, + expected: &Projection{ + Labels: []string{"new_label", "src_label1", "src_label2"}, + Include: true, + }, + }, + { + name: "label_join with without clause", + funcName: "label_join", + args: []Node{ + &VectorSelector{}, + &StringLiteral{Val: "new_label"}, + &StringLiteral{Val: "separator"}, + &StringLiteral{Val: "src_label1"}, + &StringLiteral{Val: "src_label2"}, + }, + projection: &Projection{ + Labels: []string{"new_label"}, + Include: false, + }, + expected: &Projection{ + Labels: []string{"new_label"}, + Include: false, + }, + }, + { + name: "scalar function returns empty projection", + funcName: "scalar", + args: []Node{ + &VectorSelector{}, + }, + projection: &Projection{ + Labels: []string{"label1"}, + Include: true, + }, + expected: &Projection{ + Labels: []string{}, + Include: true, + }, + }, + { + name: "absent function returns empty projection", + funcName: "absent", + args: []Node{ + &VectorSelector{}, + }, + projection: &Projection{ + Labels: []string{"label1"}, + Include: true, + }, + expected: &Projection{ + Labels: []string{}, + Include: true, + }, + }, + { + name: "absent_over_time function returns empty projection", + funcName: "absent_over_time", + args: []Node{ + &MatrixSelector{}, + }, + projection: &Projection{ + Labels: []string{"label1"}, + Include: true, + }, + expected: &Projection{ + Labels: []string{}, + Include: true, + }, + }, + { + name: "histogram_quantile function returns nil projection", + funcName: "histogram_quantile", + args: []Node{ + &NumberLiteral{Val: 0.9}, + &VectorSelector{}, + }, + projection: &Projection{ + Labels: []string{"label1"}, + Include: true, + }, + expected: nil, + }, + { + name: "unknown function returns original labels", + funcName: "unknown_function", + args: []Node{}, + projection: &Projection{ + Labels: []string{"label1"}, + Include: true, + }, + expected: &Projection{ + Labels: []string{"label1"}, + Include: true, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := getFunctionLabelRequirements(tt.funcName, tt.args, tt.projection) + + // Check if result is nil when expected is nil + if tt.expected == nil { + if result != nil { + t.Errorf("expected nil result, got %v", result) + } + return + } + + // Sort labels for consistent comparison + sort.Strings(result.Labels) + sort.Strings(tt.expected.Labels) + + // Check if Include matches + if result.Include != tt.expected.Include { + t.Errorf("expected Include=%v, got %v", tt.expected.Include, result.Include) + } + + // Check if all expected labels are in the result + for _, label := range tt.expected.Labels { + if !slices.Contains(result.Labels, label) { + t.Errorf("expected label %s to be in result, but it wasn't", label) + } + } + + // Check if result doesn't have unexpected labels + for _, label := range result.Labels { + if !slices.Contains(tt.expected.Labels, label) { + t.Errorf("unexpected label %s in result", label) + } + } + }) + } +} diff --git a/internal/promql-engine/logicalplan/propagate_selectors.go b/internal/promql-engine/logicalplan/propagate_selectors.go new file mode 100644 index 00000000000..dd43a4f1d75 --- /dev/null +++ b/internal/promql-engine/logicalplan/propagate_selectors.go @@ -0,0 +1,267 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package logicalplan + +import ( + "slices" + "sort" + "strings" + + "github.com/thanos-io/promql-engine/query" + + "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/promql/parser" + "github.com/prometheus/prometheus/util/annotations" +) + +// PropagateMatchersOptimizer implements matcher propagation between +// two vector selectors in a binary expression. +type PropagateMatchersOptimizer struct{} + +func (m PropagateMatchersOptimizer) Optimize(plan Node, _ *query.Options) (Node, annotations.Annotations) { + Traverse(&plan, func(expr *Node) { + binOp, ok := (*expr).(*Binary) + if !ok { + return + } + + // The optimizer cannot be applied to comparison operations or 'atan2'. + if binOp.Op.IsComparisonOperator() || binOp.Op.String() == "atan2" { + return + } + // Skip OR and UNLESS as their JOIN logic is different from AND and + // other binary arithmetic operations. + if binOp.Op == parser.LOR || binOp.Op == parser.LUNLESS { + return + } + + vm := binOp.VectorMatching + if vm == nil { + propagateMatchers(binOp) + return + } + + // Skip matching on metric name for now. + if vm.On && slices.Contains(vm.MatchingLabels, labels.MetricName) { + return + } + + propagateMatchers(binOp) + }) + + return plan, nil +} + +func propagateMatchers(binOp *Binary) { + lhSelector, ok := binOp.LHS.(*VectorSelector) + if !ok { + return + } + rhSelector, ok := binOp.RHS.(*VectorSelector) + if !ok { + return + } + // Only handle vector selectors with equal metric name matcher now. + lhMetricNameMatcher := extractMetricNameMatcher(lhSelector.LabelMatchers) + if lhMetricNameMatcher == nil || lhMetricNameMatcher.Type != labels.MatchEqual { + return + } + rhMetricNameMatcher := extractMetricNameMatcher(rhSelector.LabelMatchers) + if rhMetricNameMatcher == nil || rhMetricNameMatcher.Type != labels.MatchEqual { + return + } + + // There are cases where VectorSelector.Name is empty when the metric name is + // specified using {__name__="http_requests_total"} instead of http_requests_total{}. + if lhSelector.Name == "" { + lhSelector.Name = lhMetricNameMatcher.Value + } + if rhSelector.Name == "" { + rhSelector.Name = rhMetricNameMatcher.Value + } + // This case is handled by MergeSelectsOptimizer. + if lhSelector.Name == rhSelector.Name { + return + } + + vm := binOp.VectorMatching + labelRequired := func(label string) bool { + if label == labels.MetricName { + return false + } + if vm == nil { + return true + } + if !vm.On && len(vm.MatchingLabels) == 0 { + return true + } + + if vm.On && slices.Contains(vm.MatchingLabels, label) { + return true + } + if !vm.On && !slices.Contains(vm.MatchingLabels, label) { + return true + } + return false + } + + lhMatchers := toMatcherMap(lhSelector.LabelMatchers) + rhMatchers := toMatcherMap(rhSelector.LabelMatchers) + union, stop := makeUnion(lhMatchers, rhMatchers, labelRequired) + if stop || len(union) == 0 { + return + } + + // Matchers to add or replace. + matchersToChange := toSlice(union) + updateSelectorMatchers(lhSelector, matchersToChange) + updateSelectorMatchers(rhSelector, matchersToChange) +} + +func toSlice(union map[string]*labels.Matcher) []*labels.Matcher { + finalMatchers := make([]*labels.Matcher, 0, len(union)) + for _, m := range union { + finalMatchers = append(finalMatchers, m) + } + + sort.Slice(finalMatchers, func(i, j int) bool { return finalMatchers[i].Name < finalMatchers[j].Name }) + return finalMatchers +} + +func makeUnion(lhMatchers map[string]*labels.Matcher, rhMatchers map[string]*labels.Matcher, labelRequired func(label string) bool) (map[string]*labels.Matcher, bool) { + union := make(map[string]*labels.Matcher) + for _, m := range lhMatchers { + if !labelRequired(m.Name) { + continue + } + // Add every label required from left side to the union. + union[m.Name] = m + } + + for _, m := range rhMatchers { + if !labelRequired(m.Name) { + continue + } + existing, ok := union[m.Name] + if !ok { + union[m.Name] = m + continue + } + newMatcher, stop := mergeMatcher(existing, m) + // Matchers unable to be merged, stop early. + if stop { + return nil, true + } + union[m.Name] = newMatcher + } + return union, false +} + +func toMatcherMap(matchers []*labels.Matcher) map[string]*labels.Matcher { + lhMatchers := make(map[string]*labels.Matcher) + for _, m := range matchers { + lhMatchers[m.Name] = m + } + return lhMatchers +} + +func extractMetricNameMatcher(matchers []*labels.Matcher) *labels.Matcher { + for _, matcher := range matchers { + if matcher.Name == labels.MetricName { + return matcher + } + } + return nil +} + +func updateSelectorMatchers(selector *VectorSelector, matchers []*labels.Matcher) { + selectorMatchersMap := toMatcherMap(selector.LabelMatchers) + for _, m := range matchers { + selectorMatchersMap[m.Name] = m + } + selectorMatchers := make([]*labels.Matcher, 0, len(selectorMatchersMap)) + for _, m := range selectorMatchersMap { + selectorMatchers = append(selectorMatchers, m) + } + selector.LabelMatchers = selectorMatchers + sort.Slice(selector.LabelMatchers, func(i, j int) bool { + return selector.LabelMatchers[i].Name < selector.LabelMatchers[j].Name + }) +} + +func matcherEqual(a, b *labels.Matcher) bool { + return a.Name == b.Name && a.Type == b.Type && a.Value == b.Value +} + +func mergeMatcher(existingMatcher *labels.Matcher, newMatcher *labels.Matcher) (*labels.Matcher, bool) { + // Same matcher. + if matcherEqual(existingMatcher, newMatcher) { + return existingMatcher, false + } + // Different matchers. + + // As long as one matcher matches empty value or nothing, it overrides any other matcher + // that matches value. + if matchesEmptyValue(existingMatcher) || matchesNothing(existingMatcher) { + return existingMatcher, false + } + if matchesEmptyValue(newMatcher) || matchesNothing(newMatcher) { + return newMatcher, false + } + if matchesAllValues(existingMatcher) || matchesEverything(existingMatcher) { + return newMatcher, false + } + if matchesAllValues(newMatcher) || matchesEverything(newMatcher) { + return existingMatcher, false + } + + if existingMatcher.Type == labels.MatchNotEqual && newMatcher.Type == labels.MatchNotEqual { + return labels.MustNewMatcher(labels.MatchNotRegexp, existingMatcher.Name, strings.Join([]string{existingMatcher.Value, newMatcher.Value}, "|")), false + } + // One equal matcher with another matcher type. Always use equal matcher to scope down. + if existingMatcher.Type == labels.MatchEqual || newMatcher.Type == labels.MatchEqual { + var equalMatcher *labels.Matcher + var otherMatcher *labels.Matcher + if existingMatcher.Type == labels.MatchEqual { + equalMatcher = existingMatcher + otherMatcher = newMatcher + } else { + equalMatcher = newMatcher + otherMatcher = existingMatcher + } + if otherMatcher.Matches(equalMatcher.Value) { + return equalMatcher, false + } + } + + return nil, true +} + +func matchesEmptyValue(matcher *labels.Matcher) bool { + if matcher.Value == "" && (matcher.Type == labels.MatchEqual || matcher.Type == labels.MatchRegexp) { + return true + } + if matcher.Value == ".+" && matcher.Type == labels.MatchNotRegexp { + return true + } + return false +} + +func matchesAllValues(matcher *labels.Matcher) bool { + if matcher.Value == ".+" && matcher.Type == labels.MatchRegexp { + return true + } + if matcher.Value == "" && (matcher.Type == labels.MatchNotEqual || matcher.Type == labels.MatchNotRegexp) { + return true + } + return false +} + +func matchesEverything(matcher *labels.Matcher) bool { + return matcher.Value == ".*" && matcher.Type == labels.MatchRegexp +} + +func matchesNothing(matcher *labels.Matcher) bool { + return matcher.Value == ".*" && matcher.Type == labels.MatchNotRegexp +} diff --git a/internal/promql-engine/logicalplan/propagate_selectors_test.go b/internal/promql-engine/logicalplan/propagate_selectors_test.go new file mode 100644 index 00000000000..c80154bf37e --- /dev/null +++ b/internal/promql-engine/logicalplan/propagate_selectors_test.go @@ -0,0 +1,537 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package logicalplan + +import ( + "sort" + "testing" + + "github.com/efficientgo/core/testutil" + "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/promql/parser" +) + +func TestMergeMatcher(t *testing.T) { + cases := []struct { + name string + existingMatcher *labels.Matcher + newMatcher *labels.Matcher + expected *labels.Matcher + shouldStop bool + }{ + { + name: "same matchers", + existingMatcher: labels.MustNewMatcher(labels.MatchEqual, "label", "value"), + newMatcher: labels.MustNewMatcher(labels.MatchEqual, "label", "value"), + expected: labels.MustNewMatcher(labels.MatchEqual, "label", "value"), + shouldStop: false, + }, + { + name: "existing matcher matches empty value", + existingMatcher: labels.MustNewMatcher(labels.MatchEqual, "label", ""), + newMatcher: labels.MustNewMatcher(labels.MatchEqual, "label", "value"), + expected: labels.MustNewMatcher(labels.MatchEqual, "label", ""), + shouldStop: false, + }, + { + name: "new matcher matches empty value", + existingMatcher: labels.MustNewMatcher(labels.MatchEqual, "label", "value"), + newMatcher: labels.MustNewMatcher(labels.MatchEqual, "label", ""), + expected: labels.MustNewMatcher(labels.MatchEqual, "label", ""), + shouldStop: false, + }, + { + name: "existing matcher matches nothing", + existingMatcher: labels.MustNewMatcher(labels.MatchNotRegexp, "label", ".*"), + newMatcher: labels.MustNewMatcher(labels.MatchEqual, "label", "value"), + expected: labels.MustNewMatcher(labels.MatchNotRegexp, "label", ".*"), + shouldStop: false, + }, + { + name: "new matcher matches nothing", + existingMatcher: labels.MustNewMatcher(labels.MatchEqual, "label", "value"), + newMatcher: labels.MustNewMatcher(labels.MatchNotRegexp, "label", ".*"), + expected: labels.MustNewMatcher(labels.MatchNotRegexp, "label", ".*"), + shouldStop: false, + }, + { + name: "existing matcher matches all values", + existingMatcher: labels.MustNewMatcher(labels.MatchRegexp, "label", ".+"), + newMatcher: labels.MustNewMatcher(labels.MatchEqual, "label", "value"), + expected: labels.MustNewMatcher(labels.MatchEqual, "label", "value"), + shouldStop: false, + }, + { + name: "new matcher matches all values", + existingMatcher: labels.MustNewMatcher(labels.MatchEqual, "label", "value"), + newMatcher: labels.MustNewMatcher(labels.MatchRegexp, "label", ".+"), + expected: labels.MustNewMatcher(labels.MatchEqual, "label", "value"), + shouldStop: false, + }, + { + name: "existing matcher matches everything", + existingMatcher: labels.MustNewMatcher(labels.MatchRegexp, "label", ".*"), + newMatcher: labels.MustNewMatcher(labels.MatchEqual, "label", "value"), + expected: labels.MustNewMatcher(labels.MatchEqual, "label", "value"), + shouldStop: false, + }, + { + name: "new matcher matches everything", + existingMatcher: labels.MustNewMatcher(labels.MatchEqual, "label", "value"), + newMatcher: labels.MustNewMatcher(labels.MatchRegexp, "label", ".*"), + expected: labels.MustNewMatcher(labels.MatchEqual, "label", "value"), + shouldStop: false, + }, + { + name: "both not equal matchers", + existingMatcher: labels.MustNewMatcher(labels.MatchNotEqual, "label", "value1"), + newMatcher: labels.MustNewMatcher(labels.MatchNotEqual, "label", "value2"), + expected: labels.MustNewMatcher(labels.MatchNotRegexp, "label", "value1|value2"), + shouldStop: false, + }, + { + name: "equal matcher with regexp that matches", + existingMatcher: labels.MustNewMatcher(labels.MatchEqual, "label", "value"), + newMatcher: labels.MustNewMatcher(labels.MatchRegexp, "label", "val.*"), + expected: labels.MustNewMatcher(labels.MatchEqual, "label", "value"), + shouldStop: false, + }, + { + name: "equal matcher with regexp that doesn't match", + existingMatcher: labels.MustNewMatcher(labels.MatchEqual, "label", "value"), + newMatcher: labels.MustNewMatcher(labels.MatchRegexp, "label", "foo.*"), + expected: nil, + shouldStop: true, + }, + { + name: "equal matcher with not equal that matches", + existingMatcher: labels.MustNewMatcher(labels.MatchEqual, "label", "value1"), + newMatcher: labels.MustNewMatcher(labels.MatchNotEqual, "label", "value2"), + expected: labels.MustNewMatcher(labels.MatchEqual, "label", "value1"), + shouldStop: false, + }, + { + name: "equal matcher with not equal that doesn't match", + existingMatcher: labels.MustNewMatcher(labels.MatchEqual, "label", "value"), + newMatcher: labels.MustNewMatcher(labels.MatchNotEqual, "label", "value"), + expected: nil, + shouldStop: true, + }, + { + name: "not equal with equal matcher that matches", + existingMatcher: labels.MustNewMatcher(labels.MatchNotEqual, "label", "value2"), + newMatcher: labels.MustNewMatcher(labels.MatchEqual, "label", "value1"), + expected: labels.MustNewMatcher(labels.MatchEqual, "label", "value1"), + shouldStop: false, + }, + { + name: "not equal with equal matcher that doesn't match", + existingMatcher: labels.MustNewMatcher(labels.MatchNotEqual, "label", "value"), + newMatcher: labels.MustNewMatcher(labels.MatchEqual, "label", "value"), + expected: nil, + shouldStop: true, + }, + { + name: "equal matcher with not regexp that matches", + existingMatcher: labels.MustNewMatcher(labels.MatchEqual, "label", "value1"), + newMatcher: labels.MustNewMatcher(labels.MatchNotRegexp, "label", "value2|value3"), + expected: labels.MustNewMatcher(labels.MatchEqual, "label", "value1"), + shouldStop: false, + }, + { + name: "equal matcher with not regexp that doesn't match", + existingMatcher: labels.MustNewMatcher(labels.MatchEqual, "label", "value"), + newMatcher: labels.MustNewMatcher(labels.MatchNotRegexp, "label", "val.*"), + expected: nil, + shouldStop: true, + }, + { + name: "not regexp with equal matcher that matches", + existingMatcher: labels.MustNewMatcher(labels.MatchNotRegexp, "label", "foo.*|bar.*"), + newMatcher: labels.MustNewMatcher(labels.MatchEqual, "label", "value"), + expected: labels.MustNewMatcher(labels.MatchEqual, "label", "value"), + shouldStop: false, + }, + { + name: "not regexp with equal matcher that doesn't match", + existingMatcher: labels.MustNewMatcher(labels.MatchNotRegexp, "label", "val.*"), + newMatcher: labels.MustNewMatcher(labels.MatchEqual, "label", "value"), + expected: nil, + shouldStop: true, + }, + { + name: "incompatible matchers", + existingMatcher: labels.MustNewMatcher(labels.MatchEqual, "label", "value1"), + newMatcher: labels.MustNewMatcher(labels.MatchEqual, "label", "value2"), + expected: nil, + shouldStop: true, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + result, stop := mergeMatcher(tc.existingMatcher, tc.newMatcher) + testutil.Equals(t, tc.shouldStop, stop) + if tc.expected == nil { + testutil.Equals(t, tc.expected, result) + } else { + testutil.Assert(t, matcherEqual(tc.expected, result)) + } + }) + } +} + +func TestPropagateMatchers(t *testing.T) { + cases := []struct { + name string + binOp *Binary + expected *Binary + }{ + { + name: "non vector selector LHS", + binOp: &Binary{ + Op: parser.ADD, + LHS: &NumberLiteral{Val: 1}, + RHS: &VectorSelector{ + VectorSelector: &parser.VectorSelector{ + Name: "metric", + LabelMatchers: []*labels.Matcher{ + labels.MustNewMatcher(labels.MatchEqual, labels.MetricName, "metric"), + }, + }, + }, + }, + expected: &Binary{ + Op: parser.ADD, + LHS: &NumberLiteral{Val: 1}, + RHS: &VectorSelector{ + VectorSelector: &parser.VectorSelector{ + Name: "metric", + LabelMatchers: []*labels.Matcher{ + labels.MustNewMatcher(labels.MatchEqual, labels.MetricName, "metric"), + }, + }, + }, + }, + }, + { + name: "non vector selector RHS", + binOp: &Binary{ + Op: parser.ADD, + LHS: &VectorSelector{ + VectorSelector: &parser.VectorSelector{ + Name: "metric", + LabelMatchers: []*labels.Matcher{ + labels.MustNewMatcher(labels.MatchEqual, labels.MetricName, "metric"), + }, + }, + }, + RHS: &NumberLiteral{Val: 1}, + }, + expected: &Binary{ + Op: parser.ADD, + LHS: &VectorSelector{ + VectorSelector: &parser.VectorSelector{ + Name: "metric", + LabelMatchers: []*labels.Matcher{ + labels.MustNewMatcher(labels.MatchEqual, labels.MetricName, "metric"), + }, + }, + }, + RHS: &NumberLiteral{Val: 1}, + }, + }, + { + name: "non equal metric name matcher", + binOp: &Binary{ + Op: parser.ADD, + LHS: &VectorSelector{ + VectorSelector: &parser.VectorSelector{ + LabelMatchers: []*labels.Matcher{ + labels.MustNewMatcher(labels.MatchRegexp, labels.MetricName, "metric.*"), + }, + }, + }, + RHS: &VectorSelector{ + VectorSelector: &parser.VectorSelector{ + Name: "metric2", + LabelMatchers: []*labels.Matcher{ + labels.MustNewMatcher(labels.MatchEqual, labels.MetricName, "metric2"), + }, + }, + }, + }, + expected: &Binary{ + Op: parser.ADD, + LHS: &VectorSelector{ + VectorSelector: &parser.VectorSelector{ + LabelMatchers: []*labels.Matcher{ + labels.MustNewMatcher(labels.MatchRegexp, labels.MetricName, "metric.*"), + }, + }, + }, + RHS: &VectorSelector{ + VectorSelector: &parser.VectorSelector{ + Name: "metric2", + LabelMatchers: []*labels.Matcher{ + labels.MustNewMatcher(labels.MatchEqual, labels.MetricName, "metric2"), + }, + }, + }, + }, + }, + { + name: "empty metric names", + binOp: &Binary{ + Op: parser.ADD, + LHS: &VectorSelector{ + VectorSelector: &parser.VectorSelector{ + LabelMatchers: []*labels.Matcher{ + labels.MustNewMatcher(labels.MatchEqual, labels.MetricName, "metric1"), + labels.MustNewMatcher(labels.MatchEqual, "label", "value1"), + }, + }, + }, + RHS: &VectorSelector{ + VectorSelector: &parser.VectorSelector{ + LabelMatchers: []*labels.Matcher{ + labels.MustNewMatcher(labels.MatchEqual, labels.MetricName, "metric2"), + labels.MustNewMatcher(labels.MatchEqual, "label", "value2"), + }, + }, + }, + }, + expected: &Binary{ + Op: parser.ADD, + LHS: &VectorSelector{ + VectorSelector: &parser.VectorSelector{ + Name: "metric1", + LabelMatchers: []*labels.Matcher{ + labels.MustNewMatcher(labels.MatchEqual, labels.MetricName, "metric1"), + labels.MustNewMatcher(labels.MatchEqual, "label", "value1"), + }, + }, + }, + RHS: &VectorSelector{ + VectorSelector: &parser.VectorSelector{ + Name: "metric2", + LabelMatchers: []*labels.Matcher{ + labels.MustNewMatcher(labels.MatchEqual, labels.MetricName, "metric2"), + labels.MustNewMatcher(labels.MatchEqual, "label", "value2"), + }, + }, + }, + }, + }, + { + name: "same metric names - should skip", + binOp: &Binary{ + Op: parser.ADD, + LHS: &VectorSelector{ + VectorSelector: &parser.VectorSelector{ + Name: "metric", + LabelMatchers: []*labels.Matcher{ + labels.MustNewMatcher(labels.MatchEqual, labels.MetricName, "metric"), + labels.MustNewMatcher(labels.MatchEqual, "label", "value1"), + }, + }, + }, + RHS: &VectorSelector{ + VectorSelector: &parser.VectorSelector{ + Name: "metric", + LabelMatchers: []*labels.Matcher{ + labels.MustNewMatcher(labels.MatchEqual, labels.MetricName, "metric"), + labels.MustNewMatcher(labels.MatchEqual, "label", "value2"), + }, + }, + }, + }, + expected: &Binary{ + Op: parser.ADD, + LHS: &VectorSelector{ + VectorSelector: &parser.VectorSelector{ + Name: "metric", + LabelMatchers: []*labels.Matcher{ + labels.MustNewMatcher(labels.MatchEqual, labels.MetricName, "metric"), + labels.MustNewMatcher(labels.MatchEqual, "label", "value1"), + }, + }, + }, + RHS: &VectorSelector{ + VectorSelector: &parser.VectorSelector{ + Name: "metric", + LabelMatchers: []*labels.Matcher{ + labels.MustNewMatcher(labels.MatchEqual, labels.MetricName, "metric"), + labels.MustNewMatcher(labels.MatchEqual, "label", "value2"), + }, + }, + }, + }, + }, + { + name: "vector matching on labels", + binOp: &Binary{ + Op: parser.ADD, + LHS: &VectorSelector{ + VectorSelector: &parser.VectorSelector{ + Name: "metric1", + LabelMatchers: []*labels.Matcher{ + labels.MustNewMatcher(labels.MatchEqual, labels.MetricName, "metric1"), + labels.MustNewMatcher(labels.MatchEqual, "label1", "value1"), + labels.MustNewMatcher(labels.MatchEqual, "label2", "value2"), + }, + }, + }, + RHS: &VectorSelector{ + VectorSelector: &parser.VectorSelector{ + Name: "metric2", + LabelMatchers: []*labels.Matcher{ + labels.MustNewMatcher(labels.MatchEqual, labels.MetricName, "metric2"), + labels.MustNewMatcher(labels.MatchEqual, "label1", "value1"), + labels.MustNewMatcher(labels.MatchEqual, "label3", "value3"), + }, + }, + }, + VectorMatching: &parser.VectorMatching{ + On: true, + MatchingLabels: []string{"label1"}, + }, + }, + expected: &Binary{ + Op: parser.ADD, + LHS: &VectorSelector{ + VectorSelector: &parser.VectorSelector{ + Name: "metric1", + LabelMatchers: []*labels.Matcher{ + labels.MustNewMatcher(labels.MatchEqual, labels.MetricName, "metric1"), + labels.MustNewMatcher(labels.MatchEqual, "label1", "value1"), + labels.MustNewMatcher(labels.MatchEqual, "label2", "value2"), + }, + }, + }, + RHS: &VectorSelector{ + VectorSelector: &parser.VectorSelector{ + Name: "metric2", + LabelMatchers: []*labels.Matcher{ + labels.MustNewMatcher(labels.MatchEqual, labels.MetricName, "metric2"), + labels.MustNewMatcher(labels.MatchEqual, "label1", "value1"), + labels.MustNewMatcher(labels.MatchEqual, "label3", "value3"), + }, + }, + }, + VectorMatching: &parser.VectorMatching{ + On: true, + MatchingLabels: []string{"label1"}, + }, + }, + }, + { + name: "vector matching ignoring labels", + binOp: &Binary{ + Op: parser.ADD, + LHS: &VectorSelector{ + VectorSelector: &parser.VectorSelector{ + Name: "metric1", + LabelMatchers: []*labels.Matcher{ + labels.MustNewMatcher(labels.MatchEqual, labels.MetricName, "metric1"), + labels.MustNewMatcher(labels.MatchEqual, "label1", "value1"), + labels.MustNewMatcher(labels.MatchEqual, "label2", "value2"), + }, + }, + }, + RHS: &VectorSelector{ + VectorSelector: &parser.VectorSelector{ + Name: "metric2", + LabelMatchers: []*labels.Matcher{ + labels.MustNewMatcher(labels.MatchEqual, labels.MetricName, "metric2"), + labels.MustNewMatcher(labels.MatchEqual, "label1", "value1"), + labels.MustNewMatcher(labels.MatchEqual, "label3", "value3"), + }, + }, + }, + VectorMatching: &parser.VectorMatching{ + On: false, + MatchingLabels: []string{"label2", "label3"}, + }, + }, + expected: &Binary{ + Op: parser.ADD, + LHS: &VectorSelector{ + VectorSelector: &parser.VectorSelector{ + Name: "metric1", + LabelMatchers: []*labels.Matcher{ + labels.MustNewMatcher(labels.MatchEqual, labels.MetricName, "metric1"), + labels.MustNewMatcher(labels.MatchEqual, "label1", "value1"), + labels.MustNewMatcher(labels.MatchEqual, "label2", "value2"), + }, + }, + }, + RHS: &VectorSelector{ + VectorSelector: &parser.VectorSelector{ + Name: "metric2", + LabelMatchers: []*labels.Matcher{ + labels.MustNewMatcher(labels.MatchEqual, labels.MetricName, "metric2"), + labels.MustNewMatcher(labels.MatchEqual, "label1", "value1"), + labels.MustNewMatcher(labels.MatchEqual, "label3", "value3"), + }, + }, + }, + VectorMatching: &parser.VectorMatching{ + On: false, + MatchingLabels: []string{"label2", "label3"}, + }, + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + propagateMatchers(tc.binOp) + + // Compare LHS + lhs1, ok1 := tc.binOp.LHS.(*VectorSelector) + lhs2, ok2 := tc.expected.LHS.(*VectorSelector) + testutil.Equals(t, ok1, ok2) + if ok1 { + testutil.Equals(t, lhs2.Name, lhs1.Name) + testutil.Assert(t, matchersEqual(lhs2.LabelMatchers, lhs1.LabelMatchers)) + } else { + testutil.Equals(t, tc.expected.LHS, tc.binOp.LHS) + } + + // Compare RHS + rhs1, ok1 := tc.binOp.RHS.(*VectorSelector) + rhs2, ok2 := tc.expected.RHS.(*VectorSelector) + testutil.Equals(t, ok1, ok2) + if ok1 { + testutil.Equals(t, rhs2.Name, rhs1.Name) + testutil.Assert(t, matchersEqual(rhs2.LabelMatchers, rhs1.LabelMatchers)) + } else { + testutil.Equals(t, tc.expected.RHS, tc.binOp.RHS) + } + + // Compare VectorMatching + if tc.expected.VectorMatching == nil { + testutil.Equals(t, tc.expected.VectorMatching, tc.binOp.VectorMatching) + } else { + testutil.Equals(t, tc.expected.VectorMatching.On, tc.binOp.VectorMatching.On) + testutil.Equals(t, tc.expected.VectorMatching.MatchingLabels, tc.binOp.VectorMatching.MatchingLabels) + } + }) + } +} + +func matchersEqual(m1, m2 []*labels.Matcher) bool { + if len(m1) != len(m2) { + return false + } + sort.Slice(m1, func(i, j int) bool { return m1[i].Name < m1[j].Name }) + sort.Slice(m2, func(i, j int) bool { return m2[i].Name < m2[j].Name }) + for i := range m1 { + if !matcherEqual(m1[i], m2[i]) { + return false + } + } + return true +} diff --git a/internal/promql-engine/logicalplan/set_batch_size.go b/internal/promql-engine/logicalplan/set_batch_size.go new file mode 100644 index 00000000000..ffc666f780f --- /dev/null +++ b/internal/promql-engine/logicalplan/set_batch_size.go @@ -0,0 +1,48 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package logicalplan + +import ( + "github.com/thanos-io/promql-engine/query" + + "github.com/prometheus/prometheus/promql/parser" + "github.com/prometheus/prometheus/util/annotations" +) + +// SelectorBatchSize configures the batch size of selector based on +// aggregates present in the plan. +type SelectorBatchSize struct { + Size int64 +} + +// Optimize configures the batch size of selector based on the query plan. +// If any aggregate is present in the plan, the batch size is set to the configured value. +// The two exceptions where this cannot be done is if the aggregate is quantile, or +// when a binary expression precedes the aggregate. +func (m SelectorBatchSize) Optimize(plan Node, _ *query.Options) (Node, annotations.Annotations) { + canBatch := false + Traverse(&plan, func(current *Node) { + switch e := (*current).(type) { + case *FunctionCall: + //TODO: calls can reduce the labelset of the input; think histogram_quantile reducing + // multiple "le" labels into one output. We cannot handle this in batching. Revisit + // what is safe here. + canBatch = false + case *Binary: + canBatch = false + case *Aggregation: + if e.Op == parser.QUANTILE || e.Op == parser.TOPK || e.Op == parser.BOTTOMK || e.Op == parser.LIMITK || e.Op == parser.LIMIT_RATIO { + canBatch = false + return + } + canBatch = true + case *VectorSelector: + if canBatch { + e.BatchSize = m.Size + } + canBatch = false + } + }) + return plan, nil +} diff --git a/internal/promql-engine/logicalplan/set_batch_size_test.go b/internal/promql-engine/logicalplan/set_batch_size_test.go new file mode 100644 index 00000000000..15733eac55c --- /dev/null +++ b/internal/promql-engine/logicalplan/set_batch_size_test.go @@ -0,0 +1,105 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package logicalplan + +import ( + "testing" + + "github.com/thanos-io/promql-engine/query" + + "github.com/efficientgo/core/testutil" + "github.com/prometheus/prometheus/promql/parser" +) + +func TestSetBatchSize(t *testing.T) { + cases := []struct { + name string + expr string + expected string + }{ + { + name: "selector", + expr: `http_requests_total`, + expected: `http_requests_total`, + }, + { + name: "rate", + expr: `rate(http_requests_total[5m])`, + expected: `rate(http_requests_total[5m0s])`, + }, + { + name: "sum", + expr: `sum(http_requests_total)`, + expected: `sum(http_requests_total[batch=10])`, + }, + { + name: "quantile", + expr: `quantile(0.9, http_requests_total)`, + expected: `quantile(0.9, http_requests_total)`, + }, + { + name: "two-level aggregation", + expr: `max by (pod) (sum by (pod) (http_requests_total))`, + expected: `max by (pod) (sum by (pod) (http_requests_total[batch=10]))`, + }, + { + name: "aggregation of binary expression", + expr: `max by (pod) (metric_a / metric_b)`, + expected: `max by (pod) (metric_a / metric_b)`, + }, + { + name: "binary operation of aggregations", + expr: `max(metric_a) / max(metric_b)`, + expected: `max(metric_a[batch=10]) / max(metric_b[batch=10])`, + }, + { + name: "binary operation with same metric aggregations", + expr: `max(metric_a) / max(metric_a{code="foo"})`, + expected: `max(metric_a[batch=10]) / max(filter([code="foo"], metric_a[batch=10]))`, + }, + { + name: `histogram quantile`, + expr: `histogram_quantile(0.5, metric_bucket)`, + expected: `histogram_quantile(0.5, metric_bucket)`, + }, + { + name: "binary expression with time", + expr: `time() - max by (foo) (bar)`, + expected: `time() - max by (foo) (bar[batch=10])`, + }, + { + name: "binary expression with single aggregation", + expr: `metric_a - max by (foo) (bar)`, + expected: `metric_a - max by (foo) (bar[batch=10])`, + }, + { + name: "number literal", + expr: `1`, + expected: `1`, + }, + { + name: "absent", + expr: `absent(foo)`, + expected: `absent(foo)`, + }, + { + name: "histogram quantile with aggregation", + expr: `histogram_quantile(scalar(max(quantile)), http_requests_total)`, + expected: `histogram_quantile(scalar(max(quantile[batch=10])), http_requests_total)`, + }, + } + + optimizers := append([]Optimizer{SelectorBatchSize{Size: 10}}, DefaultOptimizers...) + for _, tcase := range cases { + t.Run(tcase.expr, func(t *testing.T) { + t.Parallel() + expr, err := parser.ParseExpr(tcase.expr) + testutil.Ok(t, err) + + plan, _ := NewFromAST(expr, &query.Options{}, PlanOptions{}) + optimizedPlan, _ := plan.Optimize(optimizers) + testutil.Equals(t, tcase.expected, renderExprTree(optimizedPlan.Root())) + }) + } +} diff --git a/internal/promql-engine/logicalplan/sort_matchers.go b/internal/promql-engine/logicalplan/sort_matchers.go new file mode 100644 index 00000000000..b3a204374e3 --- /dev/null +++ b/internal/promql-engine/logicalplan/sort_matchers.go @@ -0,0 +1,31 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package logicalplan + +import ( + "sort" + + "github.com/thanos-io/promql-engine/query" + + "github.com/prometheus/prometheus/util/annotations" +) + +// SortMatchers sorts all matchers in a selector so that +// all subsequent optimizers, both in the logical and physical plan, +// can rely on this property. +type SortMatchers struct{} + +func (m SortMatchers) Optimize(plan Node, _ *query.Options) (Node, annotations.Annotations) { + Traverse(&plan, func(node *Node) { + e, ok := (*node).(*VectorSelector) + if !ok { + return + } + + sort.Slice(e.LabelMatchers, func(i, j int) bool { + return e.LabelMatchers[i].Name < e.LabelMatchers[j].Name + }) + }) + return plan, nil +} diff --git a/internal/promql-engine/logicalplan/user_defined.go b/internal/promql-engine/logicalplan/user_defined.go new file mode 100644 index 00000000000..6f19f21fdee --- /dev/null +++ b/internal/promql-engine/logicalplan/user_defined.go @@ -0,0 +1,23 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package logicalplan + +import ( + "context" + + "github.com/thanos-io/promql-engine/execution/model" + "github.com/thanos-io/promql-engine/query" + + "github.com/prometheus/prometheus/storage" +) + +// UserDefinedExpr is an extension point which allows users to define their execution operators. +type UserDefinedExpr interface { + Node + MakeExecutionOperator( + ctx context.Context, + opts *query.Options, + hints storage.SelectHints, + ) (model.VectorOperator, error) +} diff --git a/internal/promql-engine/query/options.go b/internal/promql-engine/query/options.go new file mode 100644 index 00000000000..e83cd51ae4c --- /dev/null +++ b/internal/promql-engine/query/options.go @@ -0,0 +1,76 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package query + +import ( + "time" +) + +type Options struct { + Start time.Time + End time.Time + Step time.Duration + StepsBatch int + LookbackDelta time.Duration + EnablePerStepStats bool + ExtLookbackDelta time.Duration + NoStepSubqueryIntervalFn func(time.Duration) time.Duration + EnableAnalysis bool + DecodingConcurrency int + SampleTracker SampleTracker // Tracks current samples in memory +} + +// TotalSteps returns the total number of steps in the query, regardless of batching. +// This is useful for pre-allocating result slices. +func (o *Options) TotalSteps() int { + // Instant evaluation is executed as a range evaluation with one step. + if o.Step.Milliseconds() == 0 { + return 1 + } + return int((o.End.UnixMilli()-o.Start.UnixMilli())/o.Step.Milliseconds() + 1) +} + +func (o *Options) NumStepsPerBatch() int { + totalSteps := o.TotalSteps() + if o.StepsBatch < totalSteps { + return o.StepsBatch + } + return totalSteps +} + +func (o *Options) IsInstantQuery() bool { + return o.TotalSteps() == 1 +} + +func (o *Options) WithEndTime(end time.Time) *Options { + result := *o + result.End = end + return &result +} + +func NestedOptionsForSubquery(opts *Options, step, queryRange, offset time.Duration) *Options { + nOpts := &Options{ + End: opts.End.Add(-offset), + LookbackDelta: opts.LookbackDelta, + StepsBatch: opts.StepsBatch, + ExtLookbackDelta: opts.ExtLookbackDelta, + NoStepSubqueryIntervalFn: opts.NoStepSubqueryIntervalFn, + EnableAnalysis: opts.EnableAnalysis, + DecodingConcurrency: opts.DecodingConcurrency, + SampleTracker: opts.SampleTracker, + } + if nOpts.SampleTracker == nil { + nOpts.SampleTracker = NewSampleTracker(0) + } + if step != 0 { + nOpts.Step = step + } else { + nOpts.Step = opts.NoStepSubqueryIntervalFn(queryRange) + } + nOpts.Start = time.UnixMilli(nOpts.Step.Milliseconds() * (opts.Start.Add(-offset-queryRange).UnixMilli() / nOpts.Step.Milliseconds())) + if nOpts.Start.Before(opts.Start.Add(-offset - queryRange)) { + nOpts.Start = nOpts.Start.Add(nOpts.Step) + } + return nOpts +} diff --git a/internal/promql-engine/query/sample_tracker.go b/internal/promql-engine/query/sample_tracker.go new file mode 100644 index 00000000000..2c0455c7841 --- /dev/null +++ b/internal/promql-engine/query/sample_tracker.go @@ -0,0 +1,67 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package query + +import ( + "fmt" + "math" + "sync/atomic" +) + +type SampleTracker interface { + Add(count int) + Remove(count int) + CheckLimit() error + Limit() int64 +} + +type sampleTracker struct { + current atomic.Int64 + limit int64 +} + +func NewSampleTracker(maxSamples int) SampleTracker { + if maxSamples <= 0 { + return nopSampleTracker{} + } + return &sampleTracker{ + limit: int64(maxSamples), + } +} + +func (st *sampleTracker) Add(count int) { + st.current.Add(int64(count)) +} + +func (st *sampleTracker) Remove(count int) { + st.current.Add(-int64(count)) +} + +func (st *sampleTracker) CheckLimit() error { + current := st.current.Load() + if current > st.limit { + return ErrMaxSamplesExceeded{Current: current, Limit: st.limit} + } + return nil +} + +func (st *sampleTracker) Limit() int64 { + return st.limit +} + +type nopSampleTracker struct{} + +func (nopSampleTracker) Add(int) {} +func (nopSampleTracker) Remove(int) {} +func (nopSampleTracker) CheckLimit() error { return nil } +func (nopSampleTracker) Limit() int64 { return math.MaxInt64 } + +type ErrMaxSamplesExceeded struct { + Current int64 + Limit int64 +} + +func (e ErrMaxSamplesExceeded) Error() string { + return fmt.Sprintf("query processing would load too many samples into memory: current=%d, limit=%d", e.Current, e.Limit) +} diff --git a/internal/promql-engine/query/sample_tracker_test.go b/internal/promql-engine/query/sample_tracker_test.go new file mode 100644 index 00000000000..d99ac21d504 --- /dev/null +++ b/internal/promql-engine/query/sample_tracker_test.go @@ -0,0 +1,52 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package query + +import ( + "testing" +) + +func TestSampleTracker_WithLimit(t *testing.T) { + tracker := NewSampleTracker(100) + + tracker.Add(50) + if err := tracker.CheckLimit(); err != nil { + t.Errorf("unexpected error: %v", err) + } + + tracker.Add(60) + if err := tracker.CheckLimit(); err == nil { + t.Error("expected error when exceeding limit") + } + + if tracker.Limit() != 100 { + t.Errorf("expected limit 100, got %d", tracker.Limit()) + } +} + +func TestSampleTracker_NoLimit(t *testing.T) { + tracker := NewSampleTracker(0) + + tracker.Add(1000000) + if err := tracker.CheckLimit(); err != nil { + t.Errorf("nop tracker should never error: %v", err) + } + + tracker.Add(1000000) + if err := tracker.CheckLimit(); err != nil { + t.Errorf("nop tracker should never error: %v", err) + } +} + +func TestSampleTracker_Remove(t *testing.T) { + tracker := NewSampleTracker(100) + + tracker.Add(90) + tracker.Remove(40) + tracker.Add(40) + + if err := tracker.CheckLimit(); err != nil { + t.Errorf("unexpected error after remove: %v", err) + } +} diff --git a/internal/promql-engine/ringbuffer/functions.go b/internal/promql-engine/ringbuffer/functions.go new file mode 100644 index 00000000000..bbf6c054a1b --- /dev/null +++ b/internal/promql-engine/ringbuffer/functions.go @@ -0,0 +1,1219 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package ringbuffer + +import ( + "math" + "sort" + + "github.com/thanos-io/promql-engine/compute" + "github.com/thanos-io/promql-engine/execution/parse" + "github.com/thanos-io/promql-engine/warnings" + + "github.com/efficientgo/core/errors" + "github.com/prometheus/prometheus/model/histogram" +) + +type SamplesBuffer GenericRingBuffer + +type FunctionArgs struct { + Samples []Sample + StepTime int64 + SelectRange int64 + Offset int64 + MetricAppearedTs int64 + + // quantile_over_time and predict_linear use one, so we only use one here. + ScalarPoint float64 + ScalarPoint2 float64 // only for double_exponential_smoothing (trend factor) +} + +type FunctionCall func(f FunctionArgs) (float64, *histogram.FloatHistogram, bool, warnings.Warnings, error) + +func instantValue(samples []Sample, isRate bool) (float64, *histogram.FloatHistogram, bool, warnings.Warnings, error) { + var ( + ss = make([]Sample, 0, 2) + warn warnings.Warnings + ) + + if len(samples) < 2 { + return 0, nil, false, 0, nil + } + + for i := range samples { + if samples[i].V.H != nil { + continue + } + if len(ss) == 2 { + ss[0] = ss[1] + ss[1] = samples[i] + continue + } + ss = append(ss, samples[i]) + } + + histSeen := 0 + for i := len(samples) - 1; i >= 0 && histSeen < 2; i-- { + if samples[i].V.H == nil { + continue + } + s := samples[i] + switch { + case len(ss) == 0: + ss = append(ss, s) + case len(ss) == 1: + if s.T < ss[0].T { + ss = append([]Sample{s}, ss...) + } else { + ss = append(ss, s) + } + case s.T < ss[0].T: + // s is older than 1st, so discard it. + case s.T > ss[1].T: + // s is newest, so add it as 2nd and make the old 2nd the new 1st. + ss[0] = ss[1] + ss[1] = s + default: + // In all other cases, we just make s the new 1st. + // This establishes a correct order, even in the (irregular) + // case of equal timestamps. + ss[0] = s + } + + histSeen++ + } + + sampledInterval := ss[1].T - ss[0].T + if sampledInterval == 0 { + // Avoid dividing by 0. + return 0, nil, false, 0, nil + } + + resultSample := ss[1] + switch { + case ss[1].V.H == nil && ss[0].V.H == nil: + if !isRate || !(ss[1].V.F < ss[0].V.F) { + // Gauge, or counter without reset, or counter with NaN value. + resultSample.V.F = ss[1].V.F - ss[0].V.F + } + + // In case of a counter reset, we leave resultSample at + // its current value, which is already ss[1]. + case ss[1].V.H != nil && ss[0].V.H != nil: + resultSample.V.H = ss[1].V.H.Copy() + // irate should only be applied to counters. + if isRate && (ss[1].V.H.CounterResetHint == histogram.GaugeType || ss[0].V.H.CounterResetHint == histogram.GaugeType) { + warn |= warnings.WarnNotCounter + } + // idelta should only be applied to gauges. + if !isRate && (ss[1].V.H.CounterResetHint != histogram.GaugeType || ss[0].V.H.CounterResetHint != histogram.GaugeType) { + warn |= warnings.WarnNotGauge + } + + if !isRate || !ss[1].V.H.DetectReset(ss[0].V.H) { + _, _, nhcbBoundsReconciled, err := resultSample.V.H.Sub(ss[0].V.H) + if err != nil { + // Convert incompatible schema error to warning + if errors.Is(err, histogram.ErrHistogramsIncompatibleSchema) { + warn |= warnings.WarnMixedExponentialCustomBuckets + return 0, nil, false, warn, nil + } + return 0, nil, false, warn, err + } + if nhcbBoundsReconciled { + warn |= warnings.WarnNHCBBoundsReconciled + } + } + + resultSample.V.H.CounterResetHint = histogram.GaugeType + resultSample.V.H.Compact(0) + default: + // Mix of a float and a histogram. + return 0, nil, false, warnings.WarnMixedFloatsHistograms, nil + } + + if isRate { + // Convert to per-second. + if resultSample.V.H == nil { + resultSample.V.F /= float64(sampledInterval) / 1000 + } else { + resultSample.V.H.Div(float64(sampledInterval) / 1000) + } + } + + return resultSample.V.F, resultSample.V.H, true, warn, nil +} + +var rangeVectorFuncs = map[string]FunctionCall{ + "sum_over_time": func(f FunctionArgs) (float64, *histogram.FloatHistogram, bool, warnings.Warnings, error) { + if len(f.Samples) == 0 { + return 0., nil, false, 0, nil + } + return sumOverTime(f.Samples) + }, + "avg_over_time": func(f FunctionArgs) (float64, *histogram.FloatHistogram, bool, warnings.Warnings, error) { + if len(f.Samples) == 0 { + return 0., nil, false, 0, nil + } + return avgOverTime(f.Samples) + }, + "mad_over_time": func(f FunctionArgs) (float64, *histogram.FloatHistogram, bool, warnings.Warnings, error) { + if len(f.Samples) == 0 { + return 0., nil, false, 0, nil + } + val, ok, warn := madOverTime(f.Samples) + return val, nil, ok, warn, nil + }, + "max_over_time": func(f FunctionArgs) (float64, *histogram.FloatHistogram, bool, warnings.Warnings, error) { + if len(f.Samples) == 0 { + return 0., nil, false, 0, nil + } + v, _, ok, warn := maxOverTime(f.Samples) + return v, nil, ok, warn, nil + }, + "min_over_time": func(f FunctionArgs) (float64, *histogram.FloatHistogram, bool, warnings.Warnings, error) { + if len(f.Samples) == 0 { + return 0., nil, false, 0, nil + } + v, _, ok, warn := minOverTime(f.Samples) + return v, nil, ok, warn, nil + }, + "ts_of_max_over_time": func(f FunctionArgs) (float64, *histogram.FloatHistogram, bool, warnings.Warnings, error) { + if len(f.Samples) == 0 { + return 0., nil, false, 0, nil + } + _, t, ok, warn := maxOverTime(f.Samples) + return float64(t) / 1000, nil, ok, warn, nil + }, + "ts_of_min_over_time": func(f FunctionArgs) (float64, *histogram.FloatHistogram, bool, warnings.Warnings, error) { + if len(f.Samples) == 0 { + return 0., nil, false, 0, nil + } + _, t, ok, warn := minOverTime(f.Samples) + return float64(t) / 1000, nil, ok, warn, nil + }, + "ts_of_last_over_time": func(f FunctionArgs) (float64, *histogram.FloatHistogram, bool, warnings.Warnings, error) { + if len(f.Samples) == 0 { + return 0., nil, false, 0, nil + } + + var t int64 + for _, s := range f.Samples { + t = max(t, s.T) + } + return float64(t) / 1000, nil, true, 0, nil + }, + "ts_of_first_over_time": func(f FunctionArgs) (float64, *histogram.FloatHistogram, bool, warnings.Warnings, error) { + if len(f.Samples) == 0 { + return 0., nil, false, 0, nil + } + + t := f.Samples[0].T + for _, s := range f.Samples[1:] { + t = min(t, s.T) + } + return float64(t) / 1000, nil, true, 0, nil + }, + "stddev_over_time": func(f FunctionArgs) (float64, *histogram.FloatHistogram, bool, warnings.Warnings, error) { + if len(f.Samples) == 0 { + return 0., nil, false, 0, nil + } + v, ok, warn := stddevOverTime(f.Samples) + return v, nil, ok, warn, nil + }, + "stdvar_over_time": func(f FunctionArgs) (float64, *histogram.FloatHistogram, bool, warnings.Warnings, error) { + if len(f.Samples) == 0 { + return 0., nil, false, 0, nil + } + v, ok, warn := stdvarOverTime(f.Samples) + return v, nil, ok, warn, nil + }, + "count_over_time": func(f FunctionArgs) (float64, *histogram.FloatHistogram, bool, warnings.Warnings, error) { + if len(f.Samples) == 0 { + return 0., nil, false, 0, nil + } + return countOverTime(f.Samples), nil, true, 0, nil + }, + "last_over_time": func(f FunctionArgs) (float64, *histogram.FloatHistogram, bool, warnings.Warnings, error) { + if len(f.Samples) == 0 { + return 0., nil, false, 0, nil + } + + var fi, hi int = -1, -1 + for i, s := range f.Samples { + if s.V.H != nil { + hi = i + continue + } + fi = i + } + + if hi == -1 { + return f.Samples[len(f.Samples)-1].V.F, nil, true, 0, nil + } + if fi == -1 { + return 0, f.Samples[hi].V.H.Copy(), true, 0, nil + } + + if f.Samples[hi].T > f.Samples[fi].T { + return 0, f.Samples[hi].V.H.Copy(), true, 0, nil + } + return f.Samples[fi].V.F, nil, true, 0, nil + }, + "first_over_time": func(f FunctionArgs) (float64, *histogram.FloatHistogram, bool, warnings.Warnings, error) { + if len(f.Samples) == 0 { + return 0., nil, false, 0, nil + } + + var fi, hi int = -1, -1 + var ft, ht int64 = math.MaxInt64, math.MaxInt64 + for i, s := range f.Samples { + if s.V.H != nil { + if s.T < ht { + ht = s.T + hi = i + } + } else { + if s.T < ft { + ft = s.T + fi = i + } + } + } + + if hi == -1 { + return f.Samples[fi].V.F, nil, true, 0, nil + } + if fi == -1 { + return 0, f.Samples[hi].V.H.Copy(), true, 0, nil + } + + if ht < ft { + return 0, f.Samples[hi].V.H.Copy(), true, 0, nil + } + return f.Samples[fi].V.F, nil, true, 0, nil + }, + "present_over_time": func(f FunctionArgs) (float64, *histogram.FloatHistogram, bool, warnings.Warnings, error) { + if len(f.Samples) == 0 { + return 0., nil, false, 0, nil + } + return 1., nil, true, 0, nil + }, + "quantile_over_time": func(f FunctionArgs) (float64, *histogram.FloatHistogram, bool, warnings.Warnings, error) { + if len(f.Samples) == 0 { + return 0., nil, false, 0, nil + } + floats := make([]float64, 0, len(f.Samples)) + + var warn warnings.Warnings + for _, sample := range f.Samples { + if sample.V.H != nil { + if len(floats) > 0 { + warn |= warnings.WarnHistogramIgnoredInMixedRange + } + continue + } + floats = append(floats, sample.V.F) + } + + if len(floats) == 0 { + return 0, nil, false, warn, nil + } + return compute.Quantile(f.ScalarPoint, floats), nil, true, warn, nil + }, + "changes": func(f FunctionArgs) (float64, *histogram.FloatHistogram, bool, warnings.Warnings, error) { + if len(f.Samples) == 0 { + return 0., nil, false, 0, nil + } + return changes(f.Samples), nil, true, 0, nil + }, + "resets": func(f FunctionArgs) (float64, *histogram.FloatHistogram, bool, warnings.Warnings, error) { + if len(f.Samples) == 0 { + return 0., nil, false, 0, nil + } + return resets(f.Samples), nil, true, 0, nil + }, + "deriv": func(f FunctionArgs) (float64, *histogram.FloatHistogram, bool, warnings.Warnings, error) { + if len(f.Samples) < 2 { + return 0., nil, false, 0, nil + } + v, ok, warn := deriv(f.Samples) + return v, nil, ok, warn, nil + }, + "irate": func(f FunctionArgs) (float64, *histogram.FloatHistogram, bool, warnings.Warnings, error) { + v, fh, ok, warn, err := instantValue(f.Samples, true) + if err != nil { + return 0., nil, false, warn, err + } + if !ok { + return 0., nil, false, warn, nil + } + return v, fh, true, warn, nil + }, + "idelta": func(f FunctionArgs) (float64, *histogram.FloatHistogram, bool, warnings.Warnings, error) { + v, fh, ok, warn, err := instantValue(f.Samples, false) + if err != nil { + return 0., nil, false, warn, err + } + if !ok { + return 0., nil, false, warn, nil + } + return v, fh, true, warn, nil + }, + "rate": func(f FunctionArgs) (float64, *histogram.FloatHistogram, bool, warnings.Warnings, error) { + if len(f.Samples) < 2 { + return 0., nil, false, 0, nil + } + return extrapolatedRate(f.Samples, len(f.Samples), true, true, f.StepTime, f.SelectRange, f.Offset) + }, + "delta": func(f FunctionArgs) (float64, *histogram.FloatHistogram, bool, warnings.Warnings, error) { + if len(f.Samples) < 2 { + return 0., nil, false, 0, nil + } + return extrapolatedRate(f.Samples, len(f.Samples), false, false, f.StepTime, f.SelectRange, f.Offset) + }, + "increase": func(f FunctionArgs) (float64, *histogram.FloatHistogram, bool, warnings.Warnings, error) { + if len(f.Samples) < 2 { + return 0., nil, false, 0, nil + } + return extrapolatedRate(f.Samples, len(f.Samples), true, false, f.StepTime, f.SelectRange, f.Offset) + }, + "xrate": func(f FunctionArgs) (float64, *histogram.FloatHistogram, bool, warnings.Warnings, error) { + if len(f.Samples) == 0 { + return 0., nil, false, 0, nil + } + if f.MetricAppearedTs == math.MinInt64 { + panic("BUG: we got some Samples but metric still hasn't appeared") + } + v, h := extendedRate(f.Samples, true, true, f.StepTime, f.SelectRange, f.Offset, f.MetricAppearedTs) + return v, h, true, 0, nil + }, + "xdelta": func(f FunctionArgs) (float64, *histogram.FloatHistogram, bool, warnings.Warnings, error) { + if len(f.Samples) == 0 { + return 0., nil, false, 0, nil + } + if f.MetricAppearedTs == math.MinInt64 { + panic("BUG: we got some Samples but metric still hasn't appeared") + } + v, h := extendedRate(f.Samples, false, false, f.StepTime, f.SelectRange, f.Offset, f.MetricAppearedTs) + return v, h, true, 0, nil + }, + "xincrease": func(f FunctionArgs) (float64, *histogram.FloatHistogram, bool, warnings.Warnings, error) { + if len(f.Samples) == 0 { + return 0., nil, false, 0, nil + } + if f.MetricAppearedTs == math.MinInt64 { + panic("BUG: we got some Samples but metric still hasn't appeared") + } + v, h := extendedRate(f.Samples, true, false, f.StepTime, f.SelectRange, f.Offset, f.MetricAppearedTs) + return v, h, true, 0, nil + }, + "predict_linear": func(f FunctionArgs) (float64, *histogram.FloatHistogram, bool, warnings.Warnings, error) { + v, ok, warn := predictLinear(f.Samples, f.ScalarPoint, f.StepTime) + return v, nil, ok, warn, nil + }, + "double_exponential_smoothing": func(f FunctionArgs) (float64, *histogram.FloatHistogram, bool, warnings.Warnings, error) { + floats, numHistograms := filterFloatOnlySamples(f.Samples) + var warn warnings.Warnings + if numHistograms > 0 && len(floats) > 0 { + warn |= warnings.WarnHistogramIgnoredInMixedRange + } + + if len(floats) < 2 { + return 0, nil, false, warn, nil + } + + sf := f.ScalarPoint // smoothing factor or alpha + tf := f.ScalarPoint2 // trend factor argument or beta + + v, ok := doubleExponentialSmoothing(floats, sf, tf) + return v, nil, ok, warn, nil + }, +} + +func NewRangeVectorFunc(name string) (FunctionCall, error) { + call, ok := rangeVectorFuncs[name] + if !ok { + return nil, parse.UnknownFunctionError(name) + } + return call, nil +} + +// extrapolatedRate is a utility function for rate/increase/delta. +// It calculates the rate (allowing for counter resets if isCounter is true), +// extrapolates if the first/last sample is close to the boundary, and returns +// the result as either per-second (if isRate is true) or overall. +func extrapolatedRate(samples []Sample, numSamples int, isCounter, isRate bool, stepTime int64, selectRange int64, offset int64) (f float64, h *histogram.FloatHistogram, ok bool, warn warnings.Warnings, err error) { + var ( + rangeStart = stepTime - (selectRange + offset) + rangeEnd = stepTime - offset + resultValue float64 + resultHistogram *histogram.FloatHistogram + ) + + var fd, hd bool + for _, s := range samples { + hd = hd || s.V.H != nil + fd = fd || s.V.H == nil + } + if fd && hd { + return 0, nil, false, warnings.WarnMixedFloatsHistograms, nil + } + + if samples[0].V.H != nil { + var err error + resultHistogram, warn, err = histogramRate(samples, isCounter) + if err != nil { + return 0, nil, false, warn, err + } + } else { + resultValue = samples[len(samples)-1].V.F - samples[0].V.F + if isCounter { + var lastValue float64 + for _, sample := range samples { + if sample.V.F < lastValue { + resultValue += lastValue + } + lastValue = sample.V.F + } + } + } + + // Duration between first/last Samples and boundary of range. + durationToStart := float64(samples[0].T-rangeStart) / 1000 + durationToEnd := float64(rangeEnd-samples[len(samples)-1].T) / 1000 + + sampledInterval := float64(samples[len(samples)-1].T-samples[0].T) / 1000 + averageDurationBetweenSamples := sampledInterval / float64(numSamples-1) + + // If samples are close enough to the (lower or upper) boundary of the + // range, we extrapolate the rate all the way to the boundary in + // question. "Close enough" is defined as "up to 10% more than the + // average duration between samples within the range", see + // extrapolationThreshold below. Essentially, we are assuming a more or + // less regular spacing between samples, and if we don't see a sample + // where we would expect one, we assume the series does not cover the + // whole range, but starts and/or ends within the range. We still + // extrapolate the rate in this case, but not all the way to the + // boundary, but only by half of the average duration between samples + // (which is our guess for where the series actually starts or ends). + + extrapolationThreshold := averageDurationBetweenSamples * 1.1 + + if durationToStart >= extrapolationThreshold { + durationToStart = averageDurationBetweenSamples / 2 + } + if isCounter { + // Counters cannot be negative. If we have any slope at + // all (i.e. resultValue went up), we can extrapolate + // the zero point of the counter. If the duration to the + // zero point is shorter than the durationToStart, we + // take the zero point as the start of the series, + // thereby avoiding extrapolation to negative counter + // values. + durationToZero := durationToStart + + if resultValue > 0 && + len(samples) > 0 && + samples[0].V.F >= 0 { + durationToZero = sampledInterval * (samples[0].V.F / resultValue) + } else if resultHistogram != nil && + resultHistogram.Count > 0 && + len(samples) > 0 && + samples[0].V.H.Count >= 0 { + durationToZero = sampledInterval * (samples[0].V.H.Count / resultHistogram.Count) + } + if durationToZero < durationToStart { + durationToStart = durationToZero + } + } + + if durationToEnd >= extrapolationThreshold { + durationToEnd = averageDurationBetweenSamples / 2 + } + + factor := (sampledInterval + durationToStart + durationToEnd) / sampledInterval + if isRate { + factor /= float64(selectRange) / 1000 + } + if resultHistogram == nil { + resultValue *= factor + } else { + resultHistogram.Mul(factor) + } + + if samples[0].V.H != nil && resultHistogram == nil { + // to prevent appending sample with 0 + return 0, nil, false, warn, nil + } + + return resultValue, resultHistogram, true, warn, nil +} + +// extendedRate is a utility function for xrate/xincrease/xdelta. +// It calculates the rate (allowing for counter resets if isCounter is true), +// taking into account the last sample before the range start, and returns +// the result as either per-second (if isRate is true) or overall. +func extendedRate(samples []Sample, isCounter, isRate bool, stepTime int64, selectRange int64, offset int64, metricAppearedTs int64) (float64, *histogram.FloatHistogram) { + var ( + rangeStart = stepTime - (selectRange + offset) + rangeEnd = stepTime - offset + resultValue float64 + resultHistogram *histogram.FloatHistogram + ) + + if samples[0].V.H != nil { + // TODO - support extended rate for histograms + resultHistogram, _, _ = histogramRate(samples, isCounter) + return resultValue, resultHistogram + } + + sameVals := true + for i := range samples { + if i > 0 && samples[i-1].V.F != samples[i].V.F { + sameVals = false + break + } + } + + // This effectively injects a "zero" series for xincrease if we only have one sample. + // Only do it for some time when the metric appears the first time. + until := selectRange + metricAppearedTs + if isCounter && !isRate && sameVals { + // Make sure we are not at the end of the range. + if stepTime-offset <= until { + return samples[0].V.F, nil + } + } + + sampledInterval := float64(samples[len(samples)-1].T - samples[0].T) + averageDurationBetweenSamples := sampledInterval / float64(len(samples)-1) + + firstPoint := 0 + // Only do this for not xincrease + if !(isCounter && !isRate) { + // If the point before the range is too far from rangeStart, drop it. + if float64(rangeStart-samples[0].T) > averageDurationBetweenSamples { + if len(samples) < 3 { + return resultValue, nil + } + firstPoint = 1 + sampledInterval = float64(samples[len(samples)-1].T - samples[1].T) + averageDurationBetweenSamples = sampledInterval / float64(len(samples)-2) + } + } + + var ( + counterCorrection float64 + lastValue float64 + ) + if isCounter { + for i := firstPoint; i < len(samples); i++ { + sample := samples[i] + if sample.V.F < lastValue { + counterCorrection += lastValue + } + lastValue = sample.V.F + } + } + resultValue = samples[len(samples)-1].V.F - samples[firstPoint].V.F + counterCorrection + + // Duration between last sample and boundary of range. + durationToEnd := float64(rangeEnd - samples[len(samples)-1].T) + // If the points cover the whole range (i.e. they start just before the + // range start and end just before the range end) adjust the value from + // the sampled range to the requested range. + // Only do this for not xincrease. + if !(isCounter && !isRate) { + if samples[firstPoint].T <= rangeStart && durationToEnd < averageDurationBetweenSamples { + adjustToRange := float64(selectRange / 1000) + resultValue = resultValue * (adjustToRange / (sampledInterval / 1000)) + } + } + + if isRate { + resultValue = resultValue / float64(selectRange/1000) + } + + return resultValue, nil +} + +// histogramRate is a helper function for extrapolatedRate. It requires +// points[0] to be a histogram. It returns nil if any other Point in points is +// not a histogram. +func histogramRate(points []Sample, isCounter bool) (*histogram.FloatHistogram, warnings.Warnings, error) { + // Calculating a rate on a single sample is not defined. + if len(points) < 2 { + return nil, 0, nil + } + var ( + prev = points[0].V.H + usingCustomBuckets = prev.UsesCustomBuckets() + last = points[len(points)-1].V.H + warn warnings.Warnings + ) + if last == nil { + return nil, warnings.WarnMixedFloatsHistograms, nil // Range contains a mix of histograms and floats. + } + + // We check for gauge type histograms in the loop below, but the loop + // below does not run on the first and last point, so check the first + // and last point now. + if isCounter && (prev.CounterResetHint == histogram.GaugeType || last.CounterResetHint == histogram.GaugeType) { + warn |= warnings.WarnNotCounter + } + + // Null out the 1st sample if there is a counter reset between the 1st + // and 2nd. In this case, we want to ignore any incompatibility in the + // bucket layout of the 1st sample because we do not need to look at it. + if isCounter && len(points) > 1 { + second := points[1].V.H + if second != nil && second.DetectReset(prev) { + prev = &histogram.FloatHistogram{} + prev.Schema = second.Schema + prev.CustomValues = second.CustomValues + usingCustomBuckets = second.UsesCustomBuckets() + } + } + + if last.UsesCustomBuckets() != usingCustomBuckets { + return nil, warnings.WarnMixedExponentialCustomBuckets, nil + } + + minSchema := min(last.Schema, prev.Schema) + + if last.UsesCustomBuckets() != usingCustomBuckets { + return nil, warnings.WarnMixedExponentialCustomBuckets, nil + } + + // https://github.com/prometheus/prometheus/blob/ccea61c7bf1e6bce2196ba8189a209945a204c5b/promql/functions.go#L183 + // First iteration to find out two things: + // - What's the smallest relevant schema? + // - Are all data points histograms? + // []FloatPoint and a []HistogramPoint separately. + for _, currPoint := range points[1 : len(points)-1] { + curr := currPoint.V.H + if curr == nil { + return nil, warn | warnings.WarnMixedFloatsHistograms, nil // Range contains a mix of histograms and floats. + } + if !isCounter { + continue + } + if curr.CounterResetHint == histogram.GaugeType { + warn |= warnings.WarnNotCounter + } + if curr.Schema < minSchema { + minSchema = curr.Schema + } + if curr.UsesCustomBuckets() != usingCustomBuckets { + return nil, warn | warnings.WarnMixedExponentialCustomBuckets, nil + } + } + + h := last.CopyToSchema(minSchema) + if _, _, nhcbBoundsReconciled, err := h.Sub(prev); err != nil { + return nil, warn, err + } else if nhcbBoundsReconciled { + warn |= warnings.WarnNHCBBoundsReconciled + } + + if isCounter { + // Second iteration to deal with counter resets. + for _, currPoint := range points[1:] { + curr := currPoint.V.H + if curr.DetectReset(prev) { + if _, _, _, err := h.Add(prev); err != nil { + return nil, warn, err + } + } + prev = curr + } + } else if points[0].V.H.CounterResetHint != histogram.GaugeType || points[len(points)-1].V.H.CounterResetHint != histogram.GaugeType { + warn |= warnings.WarnNotGauge + } + + h.CounterResetHint = histogram.GaugeType + return h.Compact(0), warn, nil +} + +func madOverTime(points []Sample) (float64, bool, warnings.Warnings) { + values := make([]float64, 0, len(points)) + var floatsDetected bool + var warn warnings.Warnings + for _, f := range points { + if f.V.H != nil { + if floatsDetected { + warn |= warnings.WarnHistogramIgnoredInMixedRange + } + continue + } else { + floatsDetected = true + + } + values = append(values, f.V.F) + } + sort.Float64s(values) + + if len(values) == 0 { + return 0, false, warn + } + median := compute.Quantile(0.5, values) + + for i, f := range values { + values[i] = math.Abs(f - median) + } + sort.Float64s(values) + + return compute.Quantile(0.5, values), true, warn +} + +func maxOverTime(points []Sample) (float64, int64, bool, warnings.Warnings) { + resv := points[0].V.F + rest := points[0].T + + var foundFloat, foundHist bool + for _, v := range points { + if v.V.H != nil { + foundHist = true + } else { + foundFloat = true + } + if v.V.F >= resv || math.IsNaN(resv) { + resv = v.V.F + rest = v.T + } + } + + if !foundFloat { + return 0, 0, false, 0 + } + var warn warnings.Warnings + if foundHist { + warn = warnings.WarnHistogramIgnoredInMixedRange + } + return resv, rest, true, warn +} + +func minOverTime(points []Sample) (float64, int64, bool, warnings.Warnings) { + resv := points[0].V.F + rest := points[0].T + + var foundFloat, foundHist bool + for _, v := range points { + if v.V.H != nil { + foundHist = true + } else { + foundFloat = true + } + if v.V.F <= resv || math.IsNaN(resv) { + resv = v.V.F + rest = v.T + } + } + + if !foundFloat { + return 0, 0, false, 0 + } + var warn warnings.Warnings + if foundHist { + warn = warnings.WarnHistogramIgnoredInMixedRange + } + return resv, rest, true, warn +} + +func countOverTime(points []Sample) float64 { + return float64(len(points)) +} + +func avgOverTime(points []Sample) (float64, *histogram.FloatHistogram, bool, warnings.Warnings, error) { + // we sniffed a histogram average + if points[0].V.H != nil { + mean := points[0].V.H.Copy() + for i, sample := range points[1:] { + if sample.V.H == nil { + return 0, nil, false, warnings.WarnMixedFloatsHistograms, nil + } + count := float64(i + 2) + left := sample.V.H.Copy().Div(count) + right := mean.Copy().Div(count) + toAdd, _, _, err := left.Sub(right) + if err != nil { + return 0, nil, false, 0, err + } + if _, _, _, err = mean.Add(toAdd); err != nil { + return 0, nil, false, 0, err + } + } + return 0, mean, true, 0, nil + } + + // we sniffed a float average + var ( + // Pre-set the 1st sample to start the loop with the 2nd. + sum, count = points[0].V.F, 1. + mean, kahanC float64 + incrementalMean bool + ) + for i, p := range points[1:] { + if p.V.H != nil { + return 0, nil, false, warnings.WarnMixedFloatsHistograms, nil + } + count = float64(i + 2) + if !incrementalMean { + newSum, newC := compute.KahanSumInc(p.V.F, sum, kahanC) + // Perform regular mean calculation as long as + // the sum doesn't overflow. + if !math.IsInf(newSum, 0) { + sum, kahanC = newSum, newC + continue + } + // Handle overflow by reverting to incremental + // calculation of the mean value. + incrementalMean = true + mean = sum / (count - 1) + kahanC /= count - 1 + } + q := (count - 1) / count + mean, kahanC = compute.KahanSumInc(p.V.F/count, q*mean, q*kahanC) + } + if incrementalMean { + return mean + kahanC, nil, true, 0, nil + } + return sum/count + kahanC/count, nil, true, 0, nil +} + +func sumOverTime(points []Sample) (float64, *histogram.FloatHistogram, bool, warnings.Warnings, error) { + // we sniffed a histogram sum + if points[0].V.H != nil { + res := points[0].V.H.Copy() + for _, v := range points[1:] { + if v.V.H == nil { + return 0, nil, false, warnings.WarnMixedFloatsHistograms, nil + } + if _, _, _, err := res.Add(v.V.H); err != nil { + return 0, nil, false, 0, err + } + } + return 0, res, true, 0, nil + } + + // we sniffed a float sum + res, c := points[0].V.F, 0. + for _, v := range points[1:] { + if v.V.H != nil { + return 0, nil, false, warnings.WarnMixedFloatsHistograms, nil + } + res, c = compute.KahanSumInc(v.V.F, res, c) + } + if math.IsInf(res, 0) { + return res, nil, true, 0, nil + } + return res + c, nil, true, 0, nil + +} + +func stddevOverTime(points []Sample) (float64, bool, warnings.Warnings) { + var count float64 + var mean, cMean float64 + var aux, cAux float64 + + var foundFloat bool + var warn warnings.Warnings + for _, v := range points { + if v.V.H == nil { + foundFloat = true + } else if foundFloat && v.V.H != nil { + warn |= warnings.WarnHistogramIgnoredInMixedRange + continue + } + count++ + delta := v.V.F - (mean + cMean) + mean, cMean = compute.KahanSumInc(delta/count, mean, cMean) + aux, cAux = compute.KahanSumInc(delta*(v.V.F-(mean+cMean)), aux, cAux) + } + + if !foundFloat { + return 0, false, warn + } + return math.Sqrt((aux + cAux) / count), true, warn +} + +func stdvarOverTime(points []Sample) (float64, bool, warnings.Warnings) { + var count float64 + var mean, cMean float64 + var aux, cAux float64 + + var foundFloat bool + var warn warnings.Warnings + for _, v := range points { + if v.V.H == nil { + foundFloat = true + } else if foundFloat && v.V.H != nil { + warn |= warnings.WarnHistogramIgnoredInMixedRange + continue + } + count++ + delta := v.V.F - (mean + cMean) + mean, cMean = compute.KahanSumInc(delta/count, mean, cMean) + aux, cAux = compute.KahanSumInc(delta*(v.V.F-(mean+cMean)), aux, cAux) + } + + if !foundFloat { + return 0, false, warn + } + return ((aux + cAux) / count), true, warn +} + +func changes(points []Sample) float64 { + count := 0. + + prevSample := points[0] + for _, curSample := range points[1:] { + switch { + case prevSample.V.H == nil && curSample.V.H == nil: + if curSample.V.F != prevSample.V.F && !(math.IsNaN(curSample.V.F) && math.IsNaN(prevSample.V.F)) { + count++ + } + case prevSample.V.H != nil && curSample.V.H == nil, prevSample.V.H == nil && curSample.V.H != nil: + count++ + case prevSample.V.H != nil && curSample.V.H != nil: + if !curSample.V.H.Equals(prevSample.V.H) { + count++ + } + } + prevSample = curSample + } + return count +} + +func deriv(points []Sample) (float64, bool, warnings.Warnings) { + var floats int + var warn warnings.Warnings + + for _, p := range points { + if p.V.H == nil { + floats++ + } + + if floats > 0 && p.V.H != nil { + warn |= warnings.WarnHistogramIgnoredInMixedRange + } + } + + if floats < 2 { + return 0, false, warn + } + + fp := make([]Sample, 0, floats) + for _, p := range points { + if p.V.H == nil { + fp = append(fp, p) + } + } + // We pass in an arbitrary timestamp that is near the values in use + // to avoid floating point accuracy issues, see + // https://github.com/prometheus/prometheus/issues/2674 + slope, _ := linearRegression(fp, fp[0].T) + + return slope, true, warn +} + +func predictLinear(points []Sample, duration float64, stepTime int64) (float64, bool, warnings.Warnings) { + var floats int + var warn warnings.Warnings + + for _, p := range points { + if p.V.H == nil { + floats++ + } + + if floats > 0 && p.V.H != nil { + warn |= warnings.WarnHistogramIgnoredInMixedRange + } + } + + if floats < 2 { + return 0, false, warn + } + + fp := make([]Sample, 0, floats) + for _, p := range points { + if p.V.H == nil { + fp = append(fp, p) + } + } + slope, intercept := linearRegression(fp, stepTime) + return slope*duration + intercept, true, warn +} + +// Based on https://github.com/prometheus/prometheus/blob/8baad1a73e471bd3cf3175a1608199e27484f179/promql/functions.go#L438 +// doubleExponentialSmoothing calculates the smoothed out value for the given series. +// It is similar to a weighted moving average, where historical data has exponentially less influence on the current data. +// It also accounts for trends in data. The smoothing factor (0 < sf < 1), aka "alpha", affects how historical data will affect the current data. +// A lower smoothing factor increases the influence of historical data. +// The trend factor (0 < tf < 1), aka "beta", affects how trends in historical data will affect the current data. +// A higher trend factor increases the influence of trends. +// Algorithm taken from https://en.wikipedia.org/wiki/Exponential_smoothing +func doubleExponentialSmoothing(points []Sample, sf, tf float64) (float64, bool) { + // Check that the input parameters are valid + if sf <= 0 || sf >= 1 || tf <= 0 || tf >= 1 { + return 0, false + } + + // Can't do the smoothing operation with less than two points + if len(points) < 2 { + return 0, false + } + + // Check for histograms in the samples + for _, s := range points { + if s.V.H != nil { + return 0, false + } + } + + var s0, s1, b float64 + // Set initial values + s1 = points[0].V.F + b = points[1].V.F - points[0].V.F + + // Run the smoothing operation + for i := 1; i < len(points); i++ { + // Scale the raw value against the smoothing factor + x := sf * points[i].V.F + // Scale the last smoothed value with the trend at this point + b = calcTrendValue(i-1, tf, s0, s1, b) + y := (1 - sf) * (s1 + b) + s0, s1 = s1, x+y + } + + return s1, true +} + +// calcTrendValue calculates the trend value at the given index i. +// This is somewhat analogous to the slope of the trend at the given index. +// The argument "tf" is the trend factor. +// The argument "s0" is the previous smoothed value. +// The argument "s1" is the current smoothed value. +// The argument "b" is the previous trend value. +func calcTrendValue(i int, tf, s0, s1, b float64) float64 { + if i == 0 { + return b + } + x := tf * (s1 - s0) + y := (1 - tf) * b + return x + y +} + +func resets(points []Sample) float64 { + var histogramPoints []Sample + var floatPoints []Sample + + for _, p := range points { + if p.V.H != nil { + histogramPoints = append(histogramPoints, p) + } else { + floatPoints = append(floatPoints, p) + } + } + + count := 0 + var prevSample, curSample Sample + for iFloat, iHistogram := 0, 0; iFloat < len(floatPoints) || iHistogram < len(histogramPoints); { + switch { + // Process a float sample if no histogram sample remains or its timestamp is earlier. + // Process a histogram sample if no float sample remains or its timestamp is earlier. + case iHistogram >= len(histogramPoints) || iFloat < len(floatPoints) && floatPoints[iFloat].T < histogramPoints[iHistogram].T: + curSample.V.F = floatPoints[iFloat].V.F + curSample.V.H = nil + iFloat++ + case iFloat >= len(floatPoints) || iHistogram < len(histogramPoints) && floatPoints[iFloat].T > histogramPoints[iHistogram].T: + curSample.V.H = histogramPoints[iHistogram].V.H + iHistogram++ + } + // Skip the comparison for the first sample, just initialize prevSample. + if iFloat+iHistogram == 1 { + prevSample = curSample + continue + } + switch { + case prevSample.V.H == nil && curSample.V.H == nil: + if curSample.V.F < prevSample.V.F { + count++ + } + case prevSample.V.H != nil && curSample.V.H == nil, prevSample.V.H == nil && curSample.V.H != nil: + count++ + case prevSample.V.H != nil && curSample.V.H != nil: + if curSample.V.H.DetectReset(prevSample.V.H) { + count++ + } + } + prevSample = curSample + } + + return float64(count) +} + +func linearRegression(Samples []Sample, interceptTime int64) (slope, intercept float64) { + var ( + n float64 + sumX, cX float64 + sumY, cY float64 + sumXY, cXY float64 + sumX2, cX2 float64 + initY float64 + constY bool + ) + initY = Samples[0].V.F + constY = true + for i, sample := range Samples { + if sample.V.H != nil { + // should ignore histograms + continue + } + + // Set constY to false if any new y values are encountered. + if constY && i > 0 && sample.V.F != initY { + constY = false + } + n += 1.0 + x := float64(sample.T-interceptTime) / 1e3 + sumX, cX = compute.KahanSumInc(x, sumX, cX) + sumY, cY = compute.KahanSumInc(sample.V.F, sumY, cY) + sumXY, cXY = compute.KahanSumInc(x*sample.V.F, sumXY, cXY) + sumX2, cX2 = compute.KahanSumInc(x*x, sumX2, cX2) + } + if constY { + if math.IsInf(initY, 0) { + return math.NaN(), math.NaN() + } + return 0, initY + } + sumX = sumX + cX + sumY = sumY + cY + sumXY = sumXY + cXY + sumX2 = sumX2 + cX2 + + covXY := sumXY - sumX*sumY/n + varX := sumX2 - sumX*sumX/n + + slope = covXY / varX + intercept = sumY/n - slope*sumX/n + return slope, intercept +} + +func filterFloatOnlySamples(samples []Sample) ([]Sample, int) { + i := 0 + histograms := 0 + for _, sample := range samples { + if sample.V.H == nil { + samples[i] = sample + i++ + } else { + histograms++ + } + } + samples = samples[:i] + return samples, histograms +} diff --git a/internal/promql-engine/ringbuffer/generic.go b/internal/promql-engine/ringbuffer/generic.go new file mode 100644 index 00000000000..5357f823739 --- /dev/null +++ b/internal/promql-engine/ringbuffer/generic.go @@ -0,0 +1,153 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package ringbuffer + +import ( + "context" + "math" + + "github.com/thanos-io/promql-engine/execution/telemetry" + "github.com/thanos-io/promql-engine/warnings" + + "github.com/prometheus/prometheus/model/histogram" +) + +type Buffer interface { + MaxT() int64 + Push(t int64, v Value) + Reset(mint int64, evalt int64) + Eval(ctx context.Context, _, _ float64, _ int64) (float64, *histogram.FloatHistogram, bool, warnings.Warnings, error) + SampleCount() int + + // to handle extlookback properly, only used by buffers that implement xincrease or xrate + ReadIntoLast(f func(*Sample)) +} + +func Empty(b Buffer) bool { return b.MaxT() == math.MinInt64 } + +type Value struct { + F float64 + H *histogram.FloatHistogram +} + +type Sample struct { + T int64 + V Value +} + +type GenericRingBuffer struct { + ctx context.Context + items []Sample + tail []Sample + + currentStep int64 + offset int64 + selectRange int64 + extLookback int64 + call FunctionCall +} + +func New(ctx context.Context, size int, selectRange, offset int64, call FunctionCall) *GenericRingBuffer { + return NewWithExtLookback(ctx, size, selectRange, offset, 0, call) +} + +func NewWithExtLookback(ctx context.Context, size int, selectRange, offset, extLookback int64, call FunctionCall) *GenericRingBuffer { + return &GenericRingBuffer{ + ctx: ctx, + items: make([]Sample, 0, size), + selectRange: selectRange, + offset: offset, + extLookback: extLookback, + call: call, + } +} + +func (r *GenericRingBuffer) SampleCount() int { + c := 0 + for _, s := range r.items { + if s.V.H != nil { + c += telemetry.CalculateHistogramSampleCount(s.V.H) + continue + } + c++ + } + return c +} + +// MaxT returns the maximum timestamp of the ring buffer. +// If the ring buffer is empty, it returns math.MinInt64. +func (r *GenericRingBuffer) MaxT() int64 { + if len(r.items) == 0 { + return math.MinInt64 + } + return r.items[len(r.items)-1].T +} + +// ReadIntoLast reads a sample into the last slot in the buffer, replacing the existing sample. +func (r *GenericRingBuffer) ReadIntoLast(f func(*Sample)) { + f(&r.items[len(r.items)-1]) +} + +// Push adds a new sample to the buffer. +func (r *GenericRingBuffer) Push(t int64, v Value) { + n := len(r.items) + if n < cap(r.items) { + r.items = r.items[:n+1] + } else { + r.items = append(r.items, Sample{}) + } + + r.items[n].T = t + r.items[n].V.F = v.F + if v.H != nil { + if r.items[n].V.H == nil { + h := v.H.Copy() + r.items[n].V.H = h + } else { + v.H.CopyTo(r.items[n].V.H) + } + } else { + r.items[n].V.H = nil + } +} + +func (r *GenericRingBuffer) Reset(mint int64, evalt int64) { + r.currentStep = evalt + if r.extLookback == 0 && (len(r.items) == 0 || r.items[len(r.items)-1].T < mint) { + r.items = r.items[:0] + return + } + var drop int + for drop = 0; drop < len(r.items) && r.items[drop].T <= mint; drop++ { + } + if r.extLookback > 0 && drop > 0 && r.items[drop-1].T >= mint-r.extLookback { + drop-- + } + + keep := len(r.items) - drop + r.tail = resize(r.tail, drop) + copy(r.tail, r.items[:drop]) + copy(r.items, r.items[drop:]) + copy(r.items[keep:], r.tail) + r.items = r.items[:keep] +} + +func (r *GenericRingBuffer) Eval(ctx context.Context, scalarArg float64, scalarArg2 float64, metricAppearedTs int64) (float64, *histogram.FloatHistogram, bool, warnings.Warnings, error) { + return r.call(FunctionArgs{ + Samples: r.items, + StepTime: r.currentStep, + SelectRange: r.selectRange, + Offset: r.offset, + ScalarPoint: scalarArg, + ScalarPoint2: scalarArg2, // only for double_exponential_smoothing + MetricAppearedTs: metricAppearedTs, + }) +} + +func resize(s []Sample, n int) []Sample { + if cap(s) >= n { + return s[:n] + } + return make([]Sample, n) +} diff --git a/internal/promql-engine/ringbuffer/overtime.go b/internal/promql-engine/ringbuffer/overtime.go new file mode 100644 index 00000000000..baf09db0236 --- /dev/null +++ b/internal/promql-engine/ringbuffer/overtime.go @@ -0,0 +1,222 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package ringbuffer + +import ( + "context" + "math" + + "github.com/thanos-io/promql-engine/compute" + "github.com/thanos-io/promql-engine/execution/telemetry" + "github.com/thanos-io/promql-engine/query" + "github.com/thanos-io/promql-engine/warnings" + + "github.com/prometheus/prometheus/model/histogram" +) + +// If we use $__interval as steps and $__rate_interval for the sliding window +// we usually have an overlap of 4 steps here. This should ensure we use the +// optimized streaming approach normally, but wont regress if a user wants a very +// high overlap. +const maxStreamingStepOverlap = 5 + +// overlapSteps calculates the number of evaluation steps that a range window overlaps. +// This is the number of steps where a single sample contributes to the result. +func overlapSteps(opts query.Options, selectRange int64) int64 { + step := max(1, opts.Step.Milliseconds()) + return min( + (selectRange-1)/step+1, + querySteps(opts), + ) +} + +func UseStreamingRingBuffers(opts query.Options, selectRange int64) bool { + return overlapSteps(opts, selectRange) <= maxStreamingStepOverlap +} + +// OverTimeBuffer is a Buffer which can calculate [agg]_over_time for a series in a +// streaming manner, calculating the value incrementally for each step where the sample is used. +type OverTimeBuffer struct { + // stepRanges contain the bounds and number of samples for each evaluation step. + stepRanges []stepRange + // stepStates contains the aggregation state for the corresponding stepRange + stepStates []stepState + // firstTimestamps contains the timestamp of the first sample for each evaluation step. + firstTimestamps []int64 + + // lastTimestamp is the timestamp of the lsat sample in the current evaluation step + lastTimestamp int64 + + step int64 +} + +type stepState struct { + acc compute.Accumulator + warn error +} + +func newOverTimeBuffer(opts query.Options, selectRange, offset int64, accMaker func() compute.Accumulator) *OverTimeBuffer { + var ( + step = max(1, opts.Step.Milliseconds()) + numSteps = overlapSteps(opts, selectRange) + + current = opts.Start.UnixMilli() + firstTimestamps = make([]int64, 0, numSteps) + stepRanges = make([]stepRange, 0, numSteps) + stepStates = make([]stepState, 0, numSteps) + ) + for range int(numSteps) { + var ( + maxt = current - offset + mint = maxt - selectRange + ) + stepRanges = append(stepRanges, stepRange{mint: mint, maxt: maxt}) + stepStates = append(stepStates, stepState{acc: accMaker()}) + firstTimestamps = append(firstTimestamps, math.MaxInt64) + + current += step + } + + return &OverTimeBuffer{ + step: step, + stepRanges: stepRanges, + stepStates: stepStates, + firstTimestamps: firstTimestamps, + lastTimestamp: math.MinInt64, + } +} + +func NewCountOverTimeBuffer(opts query.Options, selectRange, offset int64) *OverTimeBuffer { + return newOverTimeBuffer(opts, selectRange, offset, func() compute.Accumulator { return compute.NewCountAcc() }) +} + +func NewMaxOverTimeBuffer(opts query.Options, selectRange, offset int64) *OverTimeBuffer { + return newOverTimeBuffer(opts, selectRange, offset, func() compute.Accumulator { return compute.NewMaxAcc() }) +} + +func NewMinOverTimeBuffer(opts query.Options, selectRange, offset int64) *OverTimeBuffer { + return newOverTimeBuffer(opts, selectRange, offset, func() compute.Accumulator { return compute.NewMinAcc() }) +} + +func NewSumOverTimeBuffer(opts query.Options, selectRange, offset int64) *OverTimeBuffer { + return newOverTimeBuffer(opts, selectRange, offset, func() compute.Accumulator { return compute.NewSumAcc() }) +} + +func NewAvgOverTimeBuffer(opts query.Options, selectRange, offset int64) *OverTimeBuffer { + return newOverTimeBuffer(opts, selectRange, offset, func() compute.Accumulator { return compute.NewAvgAcc() }) +} + +func NewStdDevOverTimeBuffer(opts query.Options, selectRange, offset int64) *OverTimeBuffer { + return newOverTimeBuffer(opts, selectRange, offset, func() compute.Accumulator { return compute.NewStdDevAcc() }) +} + +func NewStdVarOverTimeBuffer(opts query.Options, selectRange, offset int64) *OverTimeBuffer { + return newOverTimeBuffer(opts, selectRange, offset, func() compute.Accumulator { return compute.NewStdVarAcc() }) +} + +func NewPresentOverTimeBuffer(opts query.Options, selectRange, offset int64) *OverTimeBuffer { + return newOverTimeBuffer(opts, selectRange, offset, func() compute.Accumulator { return compute.NewGroupAcc() }) +} + +func NewLastOverTimeBuffer(opts query.Options, selectRange, offset int64) *OverTimeBuffer { + return newOverTimeBuffer(opts, selectRange, offset, func() compute.Accumulator { return compute.NewLastAcc() }) +} + +func (r *OverTimeBuffer) SampleCount() int { + return r.stepRanges[0].sampleCount +} + +func (r *OverTimeBuffer) MaxT() int64 { return r.lastTimestamp } + +func (r *OverTimeBuffer) Push(t int64, v Value) { + // Set the lastSample sample for the current evaluation step. + r.lastTimestamp = t + + // Set the first sample for each evaluation step where the currently read sample is used. + for i := 0; i < len(r.stepRanges) && t > r.stepRanges[i].mint && t <= r.stepRanges[i].maxt; i++ { + r.stepRanges[i].numSamples++ + if v.H != nil { + r.stepRanges[i].sampleCount += telemetry.CalculateHistogramSampleCount(v.H) + } else { + r.stepRanges[i].sampleCount++ + } + + // Aggregate the sample to the current step. + // Accumulators track error state internally and become no-ops after an error. + // Float-only accumulators skip histograms and track via HasIgnoredHistograms(). + if err := r.stepStates[i].acc.Add(v.F, v.H); err != nil { + r.stepStates[i].warn = err + } + + if fts := r.firstTimestamps[i]; t >= fts { + continue + } + r.firstTimestamps[i] = t + } +} + +func (r *OverTimeBuffer) Reset(mint int64, evalt int64) { + if r.stepRanges[0].mint == mint { + return + } + + lastSample := len(r.stepRanges) - 1 + var ( + nextMint = r.stepRanges[lastSample].mint + r.step + nextMaxt = r.stepRanges[lastSample].maxt + r.step + ) + nextStepRange := r.stepRanges[0] + copy(r.stepRanges, r.stepRanges[1:]) + r.stepRanges[lastSample] = nextStepRange + r.stepRanges[lastSample].mint = nextMint + r.stepRanges[lastSample].maxt = nextMaxt + r.stepRanges[lastSample].sampleCount = 0 + r.stepRanges[lastSample].numSamples = 0 + + nextFirstState := r.stepStates[0] + copy(r.stepStates, r.stepStates[1:]) + r.stepStates[lastSample] = nextFirstState + r.stepStates[lastSample].acc.Reset(0) + r.stepStates[lastSample].warn = nil + + nextFirstTimestamp := r.firstTimestamps[0] + copy(r.firstTimestamps, r.firstTimestamps[1:]) + r.firstTimestamps[lastSample] = nextFirstTimestamp + r.firstTimestamps[lastSample] = math.MaxInt64 +} + +func (r *OverTimeBuffer) ReadIntoLast(func(*Sample)) {} + +func (r *OverTimeBuffer) Eval(ctx context.Context, _, _ float64, _ int64) (float64, *histogram.FloatHistogram, bool, warnings.Warnings, error) { + var warn warnings.Warnings + + if r.stepStates[0].warn != nil { + return 0, nil, false, warn, r.stepStates[0].warn + } + + if r.firstTimestamps[0] == math.MaxInt64 { + return 0, nil, false, warn, nil + } + + acc := r.stepStates[0].acc + f, h := acc.Value() + + // Include accumulator warnings (mixed types, ignored histograms) + accWarn := acc.Warnings() + + if acc.ValueType() == compute.MixedTypeValue { + warn |= accWarn + return 0, nil, false, warn, nil + } + + // For _over_time functions returning a float value, translate WarnHistogramIgnoredInAggregation + // to WarnHistogramIgnoredInMixedRange (which indicates histograms were ignored in a mixed range). + // Only do this when we actually have a float result, not when returning no value. + if acc.ValueType() == compute.SingleTypeValue && h == nil && accWarn&warnings.WarnHistogramIgnoredInAggregation != 0 { + accWarn = (accWarn &^ warnings.WarnHistogramIgnoredInAggregation) | warnings.WarnHistogramIgnoredInMixedRange + } + warn |= accWarn + + return f, h, acc.ValueType() == compute.SingleTypeValue, warn, nil +} diff --git a/internal/promql-engine/ringbuffer/rate.go b/internal/promql-engine/ringbuffer/rate.go new file mode 100644 index 00000000000..a3f021d9992 --- /dev/null +++ b/internal/promql-engine/ringbuffer/rate.go @@ -0,0 +1,205 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package ringbuffer + +import ( + "context" + "math" + "slices" + + "github.com/thanos-io/promql-engine/execution/telemetry" + "github.com/thanos-io/promql-engine/query" + "github.com/thanos-io/promql-engine/warnings" + + "github.com/prometheus/prometheus/model/histogram" +) + +// RateBuffer is a Buffer which can calculate rate, increase and delta for a +// series in a streaming manner, calculating the value incrementally for each +// step where the sample is used. +type RateBuffer struct { + ctx context.Context + // stepRanges contain the bounds and number of samples for each evaluation step. + stepRanges []stepRange + // firstSamples contains the first sample for each evaluation step. + firstSamples []Sample + // resets contains all samples which are detected as a counter reset. + resets []Sample + // rateBuffer is the buffer passed to the rate function. This is a scratch buffer + // used to avoid allocating a new slice each time we need to calculate the rate. + rateBuffer []Sample + // lastSample is the lastSample sample in the current evaluation step. + lastSample Sample + + currentMint int64 + selectRange int64 + step int64 + offset int64 + isCounter bool + isRate bool + + evalTs int64 +} + +type stepRange struct { + mint int64 + maxt int64 + numSamples int + sampleCount int +} + +// NewRateBuffer creates a new RateBuffer. +func NewRateBuffer(ctx context.Context, opts query.Options, isCounter, isRate bool, selectRange, offset int64) *RateBuffer { + var ( + step = max(1, opts.Step.Milliseconds()) + numSteps = min( + (selectRange-1)/step+1, + querySteps(opts), + ) + + current = opts.Start.UnixMilli() + firstSamples = make([]Sample, 0, numSteps) + stepRanges = make([]stepRange, 0, numSteps) + ) + for range int(numSteps) { + var ( + maxt = current - offset + mint = maxt - selectRange + ) + stepRanges = append(stepRanges, stepRange{mint: mint, maxt: maxt}) + firstSamples = append(firstSamples, Sample{T: math.MaxInt64}) + current += step + } + + return &RateBuffer{ + ctx: ctx, + isCounter: isCounter, + isRate: isRate, + selectRange: selectRange, + step: step, + offset: offset, + stepRanges: stepRanges, + firstSamples: firstSamples, + lastSample: Sample{T: math.MinInt64}, + currentMint: math.MaxInt64, + } +} + +func (r *RateBuffer) SampleCount() int { + return r.stepRanges[0].sampleCount +} + +func (r *RateBuffer) MaxT() int64 { return r.lastSample.T } + +func (r *RateBuffer) Push(t int64, v Value) { + // Detect resets and store the current and previous sample so that + // the rate is properly adjusted. + if r.lastSample.T > r.currentMint && v.H != nil && r.lastSample.V.H != nil { + if v.H.DetectReset(r.lastSample.V.H) { + r.resets = append(r.resets, Sample{ + T: r.lastSample.T, + V: Value{H: r.lastSample.V.H.Copy()}, + }) + r.resets = append(r.resets, Sample{ + T: t, + V: Value{H: v.H.Copy()}, + }) + } + } else if r.lastSample.T > r.currentMint && r.lastSample.V.F > v.F { + r.resets = append(r.resets, Sample{T: r.lastSample.T, V: Value{F: r.lastSample.V.F}}) + r.resets = append(r.resets, Sample{T: t, V: Value{F: v.F}}) + } + + // Set the lastSample sample for the current evaluation step. + r.lastSample.T, r.lastSample.V.F = t, v.F + if v.H != nil { + if r.lastSample.V.H == nil { + r.lastSample.V.H = v.H.Copy() + } else { + v.H.CopyTo(r.lastSample.V.H) + } + } else { + r.lastSample.V.H = nil + } + + // Set the first sample for each evaluation step where the currently read sample is used. + for i := 0; i < len(r.stepRanges) && t > r.stepRanges[i].mint && t <= r.stepRanges[i].maxt; i++ { + r.stepRanges[i].numSamples++ + if v.H != nil { + r.stepRanges[i].sampleCount += telemetry.CalculateHistogramSampleCount(v.H) + } else { + r.stepRanges[i].sampleCount++ + } + sample := &r.firstSamples[i] + if t >= sample.T { + continue + } + sample.T, sample.V.F = t, v.F + if v.H != nil { + if sample.V.H == nil { + sample.V.H = v.H.Copy() + } else { + v.H.CopyTo(sample.V.H) + } + } else { + sample.V.H = nil + } + } +} + +func (r *RateBuffer) Reset(mint int64, evalt int64) { + r.currentMint, r.evalTs = mint, evalt + if r.stepRanges[0].mint == mint { + return + } + dropResets := 0 + for ; dropResets < len(r.resets) && r.resets[dropResets].T <= mint; dropResets++ { + } + r.resets = r.resets[dropResets:] + + lastSample := len(r.stepRanges) - 1 + var ( + nextMint = r.stepRanges[lastSample].mint + r.step + nextMaxt = r.stepRanges[lastSample].maxt + r.step + ) + + nextStepRange := r.stepRanges[0] + copy(r.stepRanges, r.stepRanges[1:]) + r.stepRanges[lastSample] = nextStepRange + r.stepRanges[lastSample].mint = nextMint + r.stepRanges[lastSample].maxt = nextMaxt + r.stepRanges[lastSample].sampleCount = 0 + r.stepRanges[lastSample].numSamples = 0 + + nextSample := r.firstSamples[0] + copy(r.firstSamples, r.firstSamples[1:]) + r.firstSamples[lastSample] = nextSample + r.firstSamples[lastSample].T = math.MaxInt64 +} + +func (r *RateBuffer) Eval(ctx context.Context, _, _ float64, _ int64) (float64, *histogram.FloatHistogram, bool, warnings.Warnings, error) { + if r.firstSamples[0].T == math.MaxInt64 || r.firstSamples[0].T == r.lastSample.T { + return 0, nil, false, 0, nil + } + + r.rateBuffer = append(append( + append(r.rateBuffer[:0], r.firstSamples[0]), + r.resets...), + r.lastSample, + ) + r.rateBuffer = slices.CompactFunc(r.rateBuffer, func(s1 Sample, s2 Sample) bool { return s1.T == s2.T }) + numSamples := r.stepRanges[0].numSamples + return extrapolatedRate(r.rateBuffer, numSamples, r.isCounter, r.isRate, r.evalTs, r.selectRange, r.offset) +} + +func (r *RateBuffer) ReadIntoLast(func(*Sample)) {} + +func querySteps(o query.Options) int64 { + // Instant evaluation is executed as a range evaluation with one step. + if o.Step.Milliseconds() == 0 { + return 1 + } + + return (o.End.UnixMilli()-o.Start.UnixMilli())/o.Step.Milliseconds() + 1 +} diff --git a/internal/promql-engine/scripts/cleanup-white-noise.sh b/internal/promql-engine/scripts/cleanup-white-noise.sh new file mode 100755 index 00000000000..9cd366b33dd --- /dev/null +++ b/internal/promql-engine/scripts/cleanup-white-noise.sh @@ -0,0 +1,4 @@ +#!/bin/bash +SED_BIN=${SED_BIN:-sed} + +${SED_BIN} -i 's/[ \t]*$//' "$@" diff --git a/internal/promql-engine/scripts/testvet/main.go b/internal/promql-engine/scripts/testvet/main.go new file mode 100644 index 00000000000..27dc0225276 --- /dev/null +++ b/internal/promql-engine/scripts/testvet/main.go @@ -0,0 +1,187 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package main + +import ( + "bufio" + "bytes" + "fmt" + "go/ast" + "go/token" + "io" + "os" + "strings" + "unicode" + + "github.com/prometheus/prometheus/promql/parser" + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/singlechecker" +) + +func main() { + singlechecker.Main(loadAnalyzer) +} + +var loadAnalyzer = &analysis.Analyzer{ + Name: "loadvet", + Doc: "reports ill-formatted prometheus load directives or PromQL expressions", + Run: run, +} + +func run(pass *analysis.Pass) (any, error) { + for _, file := range pass.Files { + if !strings.HasSuffix(file.Name.String(), "_test") { + continue + } + var stack []ast.Node + ast.Inspect(file, func(n ast.Node) bool { + defer func() { + if n == nil { + stack = stack[:len(stack)-1] + } else { + stack = append(stack, n) + } + }() + if n == nil || len(stack) == 0 { + return true + } + // only format expressions that are behind "query" or "load" keys + parent, ok := stack[len(stack)-1].(*ast.KeyValueExpr) + if !ok { + return true + } + p, ok := parent.Key.(*ast.Ident) + if !ok { + return true + } + switch strings.ToLower(p.Name) { + case "query", "load": + default: + return true + } + + s, ok := n.(*ast.BasicLit) + if !ok { + return true + } + if s.Kind != token.STRING { + return true + } + position := pass.Fset.Position(s.Pos()) + lineAtPosition, err := readLine(position.Filename, position.Line) + if err != nil { + return true + } + whiteSpace := leadingWhitespace(lineAtPosition) + cont := s.Value[1 : len(s.Value)-1] + // for consistency and ease of replacement, we replace the quotes with ` here + quote := s.Value[0] + switch { + case looksLikeLoadStmt(cont): + if formatted := formatLoadDirective(cont, whiteSpace); cont != formatted || quote != '`' { + pass.Report(analysis.Diagnostic{ + Pos: s.Pos(), + Message: "ill-formatted load directive found", + SuggestedFixes: []analysis.SuggestedFix{ + { + Message: fmt.Sprintf("Should replace '%s' with '%s'", cont, formatted), + TextEdits: []analysis.TextEdit{ + { + Pos: s.Pos(), + End: s.End(), + NewText: fmt.Appendf(nil, "%c%s%c", '`', formatted, '`'), + }, + }, + }, + }, + }) + return false + } + case looksLikePromQL(cont): + if formatted := formatPromQL(cont); cont != formatted || quote != '`' { + pass.Report(analysis.Diagnostic{ + Pos: s.Pos(), + Message: "ill-formatted promql found", + SuggestedFixes: []analysis.SuggestedFix{ + { + Message: fmt.Sprintf("Should replace '%s' with '%s'", cont, formatted), + TextEdits: []analysis.TextEdit{ + { + Pos: s.Pos(), + End: s.End(), + NewText: fmt.Appendf(nil, "%c%s%c", '`', formatted, '`'), + }, + }, + }, + }, + }) + return false + } + } + return true + }) + } + return nil, nil +} + +func looksLikeLoadStmt(cont string) bool { + return strings.HasPrefix(strings.TrimSpace(cont), "load") +} +func looksLikePromQL(cont string) bool { + _, err := parser.ParseExpr(cont) + return err == nil +} + +func formatLoadDirective(load, whiteSpace string) string { + var res strings.Builder + + sc := bufio.NewScanner(strings.NewReader(strings.TrimSpace(load))) + for i := 0; sc.Scan(); i++ { + if i == 0 { + res.WriteString(sc.Text()) + } else { + res.WriteString(whiteSpace) + res.WriteString(strings.Repeat(" ", 4)) + res.WriteString(strings.TrimSpace(sc.Text())) + } + res.WriteByte('\n') + } + + return strings.TrimSpace(res.String()) +} + +func formatPromQL(exprStr string) string { + expr, _ := parser.ParseExpr(exprStr) + pretty := expr.Pretty(0) + if strings.Count(pretty, "\n") > 0 { + return "\n" + pretty + } + return pretty +} + +func readLine(filename string, line int) (string, error) { + content, err := os.ReadFile(filename) + if err != nil { + return "", err + } + sc := bufio.NewScanner(bytes.NewReader(content)) + for i := 0; sc.Scan(); i++ { + if i == line-1 { + return sc.Text(), sc.Err() + } + } + return "", io.EOF +} + +func leadingWhitespace(line string) string { + var res strings.Builder + for _, r := range line { + if unicode.IsSpace(r) { + res.WriteRune(r) + } else { + break + } + } + return res.String() +} diff --git a/internal/promql-engine/storage/prometheus/filter.go b/internal/promql-engine/storage/prometheus/filter.go new file mode 100644 index 00000000000..b5428a3c87e --- /dev/null +++ b/internal/promql-engine/storage/prometheus/filter.go @@ -0,0 +1,60 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package prometheus + +import ( + "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/storage" +) + +type Filter interface { + Matches(series storage.Series) bool + Matchers() []*labels.Matcher +} + +type nopFilter struct{} + +func (n nopFilter) Matchers() []*labels.Matcher { return nil } + +func (n nopFilter) Matches(storage.Series) bool { return true } + +type filter struct { + matchers []*labels.Matcher + matcherSet map[string][]*labels.Matcher +} + +func NewFilter(matchers []*labels.Matcher) Filter { + if len(matchers) == 0 { + return &nopFilter{} + } + + matcherSet := make(map[string][]*labels.Matcher) + for _, m := range matchers { + matcherSet[m.Name] = append(matcherSet[m.Name], m) + } + return &filter{ + matchers: matchers, + matcherSet: matcherSet, + } +} + +func (f filter) Matchers() []*labels.Matcher { return f.matchers } + +func (f filter) Matches(series storage.Series) bool { + if len(f.matcherSet) == 0 { + return true + } + + for name, matchers := range f.matcherSet { + label := series.Labels().Get(name) + + for _, m := range matchers { + if !m.Matches(label) { + return false + } + } + } + + return true +} diff --git a/internal/promql-engine/storage/prometheus/filter_test.go b/internal/promql-engine/storage/prometheus/filter_test.go new file mode 100644 index 00000000000..69eda95f968 --- /dev/null +++ b/internal/promql-engine/storage/prometheus/filter_test.go @@ -0,0 +1,100 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package prometheus_test + +import ( + "testing" + + storage "github.com/thanos-io/promql-engine/storage/prometheus" + + "github.com/prometheus/prometheus/model/labels" + promstg "github.com/prometheus/prometheus/storage" + "github.com/prometheus/prometheus/tsdb/chunkenc" + "github.com/stretchr/testify/require" +) + +func TestFilter_MultipleMatcherWithSameName(t *testing.T) { + f := storage.NewFilter([]*labels.Matcher{ + labels.MustNewMatcher(labels.MatchNotEqual, "phase", "Running"), + labels.MustNewMatcher(labels.MatchNotEqual, "phase", "Succeeded"), + }) + + require.Equal(t, false, f.Matches(&mockLabelSeries{labels: labels.FromStrings("phase", "Running")})) +} + +func TestFilter_Matches(t *testing.T) { + t.Parallel() + testCases := []struct { + name string + matchers []*labels.Matcher + series promstg.Series + expected bool + }{ + { + name: "empty matchers", + matchers: []*labels.Matcher{}, + series: &mockLabelSeries{labels: labels.FromStrings("foo", "bar")}, + expected: true, + }, + { + name: "no match", + matchers: []*labels.Matcher{labels.MustNewMatcher(labels.MatchEqual, "foo", "bar")}, + series: &mockLabelSeries{labels: labels.FromStrings("foo", "baz")}, + }, + { + name: "regex match", + matchers: []*labels.Matcher{labels.MustNewMatcher(labels.MatchRegexp, "foo", "ba.")}, + series: &mockLabelSeries{labels: labels.FromStrings("foo", "bar")}, + expected: true, + }, + { + name: "regex no match", + matchers: []*labels.Matcher{labels.MustNewMatcher(labels.MatchRegexp, "foo", "ba.")}, + series: &mockLabelSeries{labels: labels.FromStrings("foo", "nope")}, + }, + { + name: "multiple matchers", + matchers: []*labels.Matcher{labels.MustNewMatcher(labels.MatchEqual, "foo", "bar"), labels.MustNewMatcher(labels.MatchEqual, "baz", "qux")}, + series: &mockLabelSeries{labels: labels.FromStrings("foo", "bar", "baz", "qux")}, + expected: true, + }, + { + name: "single regex matcher, with label name not present", + matchers: []*labels.Matcher{labels.MustNewMatcher(labels.MatchRegexp, "foo", ".*")}, + series: &mockLabelSeries{labels: labels.FromStrings("bar", "baz")}, + expected: true, + }, + { + name: "single regex matcher, with label name not present, negative regex", + matchers: []*labels.Matcher{labels.MustNewMatcher(labels.MatchNotRegexp, "foo", ".*")}, + series: &mockLabelSeries{labels: labels.FromStrings("bar", "baz")}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + f := storage.NewFilter(tc.matchers) + if got := f.Matches(tc.series); got != tc.expected { + if tc.expected { + t.Errorf("expected %s to match %s, but it did not.", tc.series.Labels().String(), tc.matchers) + } else { + t.Errorf("expected %s to not match %s, but it did.", tc.series.Labels().String(), tc.matchers) + } + } + }) + } +} + +type mockLabelSeries struct { + labels labels.Labels +} + +func (s *mockLabelSeries) Labels() labels.Labels { + return s.labels +} + +func (s *mockLabelSeries) Iterator(chunkenc.Iterator) chunkenc.Iterator { + return nil +} diff --git a/internal/promql-engine/storage/prometheus/filtered_selector.go b/internal/promql-engine/storage/prometheus/filtered_selector.go new file mode 100644 index 00000000000..061c4744fef --- /dev/null +++ b/internal/promql-engine/storage/prometheus/filtered_selector.go @@ -0,0 +1,61 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package prometheus + +import ( + "context" + "sync" + + "github.com/prometheus/prometheus/model/labels" +) + +type filteredSelector struct { + selector *seriesSelector + filter Filter + + once sync.Once + series []SignedSeries +} + +func NewFilteredSelector(selector *seriesSelector, filter Filter) SeriesSelector { + return &filteredSelector{ + selector: selector, + filter: filter, + } +} + +func (f *filteredSelector) Matchers() []*labels.Matcher { + return append(f.selector.matchers, f.filter.Matchers()...) +} + +func (f *filteredSelector) GetSeries(ctx context.Context, shard, numShards int) ([]SignedSeries, error) { + var err error + f.once.Do(func() { err = f.loadSeries(ctx) }) + if err != nil { + return nil, err + } + + return seriesShard(f.series, shard, numShards), nil +} + +func (f *filteredSelector) loadSeries(ctx context.Context) error { + series, err := f.selector.GetSeries(ctx, 0, 1) + if err != nil { + return err + } + + var i uint64 + f.series = make([]SignedSeries, 0, len(series)) + for _, s := range series { + if f.filter.Matches(s) { + f.series = append(f.series, SignedSeries{ + Series: s.Series, + Signature: i, + }) + i++ + } + } + + return nil +} diff --git a/internal/promql-engine/storage/prometheus/matrix_selector.go b/internal/promql-engine/storage/prometheus/matrix_selector.go new file mode 100644 index 00000000000..1b876c24359 --- /dev/null +++ b/internal/promql-engine/storage/prometheus/matrix_selector.go @@ -0,0 +1,474 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package prometheus + +import ( + "context" + "fmt" + "math" + "strings" + "sync" + "time" + + "github.com/thanos-io/promql-engine/execution/model" + "github.com/thanos-io/promql-engine/execution/parse" + "github.com/thanos-io/promql-engine/execution/telemetry" + "github.com/thanos-io/promql-engine/extlabels" + "github.com/thanos-io/promql-engine/query" + "github.com/thanos-io/promql-engine/ringbuffer" + "github.com/thanos-io/promql-engine/warnings" + + "github.com/efficientgo/core/errors" + "github.com/prometheus/prometheus/model/histogram" + "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/model/value" + "github.com/prometheus/prometheus/promql/parser/posrange" + "github.com/prometheus/prometheus/tsdb/chunkenc" + "github.com/prometheus/prometheus/util/annotations" +) + +type matrixScanner struct { + labels labels.Labels + metricName string + signature uint64 + + buffer ringbuffer.Buffer + iterator chunkenc.Iterator + lastSample ringbuffer.Sample + metricAppearedTs int64 +} + +type matrixSelector struct { + telemetry telemetry.OperatorTelemetry + + storage SeriesSelector + scalarArg float64 + scalarArg2 float64 + scanners []matrixScanner + series []labels.Labels + once sync.Once + + functionName string + call ringbuffer.FunctionCall + fhReader *histogram.FloatHistogram + opts *query.Options + + numSteps int + mint int64 + maxt int64 + step int64 + selectRange int64 + offset int64 + isExtFunction bool + + currentStep int64 + currentSeries int64 + seriesBatchSize int64 + + shard int + numShards int + + // Lookback delta for extended range functions. + extLookbackDelta int64 + + nonCounterMetric string + hasFloats bool +} + +var ErrNativeHistogramsNotSupported = errors.New("native histograms are not supported in extended range functions") + +const sampleLimitCheckInterval = 500 + +// NewMatrixSelector creates operator which selects vector of series over time. +func NewMatrixSelector( + selector SeriesSelector, + functionName string, + arg float64, + arg2 float64, + opts *query.Options, + selectRange, offset time.Duration, + batchSize int64, + shard, numShard int, +) (model.VectorOperator, error) { + call, err := ringbuffer.NewRangeVectorFunc(functionName) + if err != nil { + return nil, err + } + m := &matrixSelector{ + storage: selector, + call: call, + functionName: functionName, + scalarArg: arg, + scalarArg2: arg2, + fhReader: &histogram.FloatHistogram{}, + + opts: opts, + numSteps: opts.NumStepsPerBatch(), + mint: opts.Start.UnixMilli(), + maxt: opts.End.UnixMilli(), + step: opts.Step.Milliseconds(), + isExtFunction: parse.IsExtFunction(functionName), + + selectRange: selectRange.Milliseconds(), + offset: offset.Milliseconds(), + currentStep: opts.Start.UnixMilli(), + seriesBatchSize: batchSize, + + shard: shard, + numShards: numShard, + + extLookbackDelta: opts.ExtLookbackDelta.Milliseconds(), + } + + // For instant queries, set the step to a positive value + // so that the operator can terminate. + if m.step == 0 { + m.step = 1 + } + + m.telemetry = telemetry.NewTelemetry(m, opts) + return telemetry.NewOperator(m.telemetry, m), nil +} + +func (o *matrixSelector) Explain() []model.VectorOperator { + return nil +} + +func (o *matrixSelector) Series(ctx context.Context) ([]labels.Labels, error) { + if err := o.loadSeries(ctx); err != nil { + return nil, err + } + return o.series, nil +} + +func (o *matrixSelector) Next(ctx context.Context, buf []model.StepVector) (int, error) { + select { + case <-ctx.Done(): + return 0, ctx.Err() + default: + } + + if o.currentStep > o.maxt { + if o.nonCounterMetric != "" && o.hasFloats { + warnings.AddToContext(annotations.NewPossibleNonCounterInfo(o.nonCounterMetric, posrange.PositionRange{}), ctx) + } + + return 0, nil + } + if err := o.loadSeries(ctx); err != nil { + return 0, err + } + + ts := o.currentStep + n := 0 + maxSteps := min(o.numSteps, len(buf)) + + // Calculate expected samples per step: the actual number of series we'll process this batch. + // This is min(seriesBatchSize, remaining series to process). + remainingSeries := int64(len(o.scanners)) - o.currentSeries + expectedSamples := int(min(o.seriesBatchSize, remainingSeries)) + if expectedSamples <= 0 { + expectedSamples = len(o.scanners) + } + + for currStep := 0; currStep < maxSteps && ts <= o.maxt; currStep++ { + buf[n].Reset(ts) + n++ + ts += o.step + } + + // Reset the current timestamp. + ts = o.currentStep + firstSeries := o.currentSeries + batchSamplesDelta := 0 + for ; o.currentSeries-firstSeries < o.seriesBatchSize && o.currentSeries < int64(len(o.scanners)); o.currentSeries++ { + var ( + scanner = &o.scanners[o.currentSeries] + seriesTs = ts + ) + + sampleCountBefore := scanner.buffer.SampleCount() + + for currStep := 0; currStep < n && seriesTs <= o.maxt; currStep++ { + maxt := seriesTs - o.offset + mint := maxt - o.selectRange + + if err := scanner.selectPoints(mint, maxt, seriesTs, o.fhReader, o.isExtFunction); err != nil { + return 0, err + } + // TODO(saswatamcode): Handle multi-arg functions for matrixSelectors. + // Also, allow operator to exist independently without being nested + // under parser.Call by implementing new data model. + // https://github.com/thanos-io/promql-engine/issues/39 + f, h, ok, warn, err := scanner.buffer.Eval(ctx, o.scalarArg, o.scalarArg2, scanner.metricAppearedTs) + if err != nil { + return 0, err + } + if warn != 0 { + emitRingbufferWarnings(ctx, warn, scanner.metricName) + } + if ok { + buf[currStep].T = seriesTs + if h != nil { + // Lazy pre-allocate histogram slices only when we actually have histograms + buf[currStep].AppendHistogramWithSizeHint(scanner.signature, h, expectedSamples) + } else { + // Lazy pre-allocate sample slices with capacity hint + buf[currStep].AppendSampleWithSizeHint(scanner.signature, f, expectedSamples) + o.hasFloats = true + } + } + o.telemetry.IncrementSamplesAtTimestamp(scanner.buffer.SampleCount(), seriesTs) + seriesTs += o.step + } + + sampleCountAfter := scanner.buffer.SampleCount() + batchSamplesDelta += sampleCountAfter - sampleCountBefore + + if o.shouldCheckSampleLimit(firstSeries) { + if err := o.updateSampleTracker(batchSamplesDelta); err != nil { + return 0, err + } + batchSamplesDelta = 0 + } + } + + if o.currentSeries == int64(len(o.scanners)) { + o.currentStep += o.step * int64(n) + o.currentSeries = 0 + } + return n, nil +} + +func (o *matrixSelector) updateSampleTracker(delta int) error { + if delta > 0 { + o.opts.SampleTracker.Add(delta) + return o.opts.SampleTracker.CheckLimit() + } else if delta < 0 { + o.opts.SampleTracker.Remove(-delta) + } + return nil +} + +func (o *matrixSelector) loadSeries(ctx context.Context) error { + var err error + o.once.Do(func() { + series, loadErr := o.storage.GetSeries(ctx, o.shard, o.numShards) + if loadErr != nil { + err = loadErr + return + } + + o.scanners = make([]matrixScanner, len(series)) + o.series = make([]labels.Labels, len(series)) + var b labels.ScratchBuilder + + for i, s := range series { + origLbls := s.Labels() + lbls := origLbls + if o.functionName != "last_over_time" && o.functionName != "first_over_time" { + lbls = extlabels.DropReserved(lbls, b) + } + o.scanners[i] = matrixScanner{ + labels: lbls, + metricName: origLbls.Get(labels.MetricName), + signature: s.Signature, + iterator: s.Iterator(nil), + lastSample: ringbuffer.Sample{T: math.MinInt64}, + buffer: o.newBuffer(ctx), + metricAppearedTs: math.MinInt64, + } + o.series[i] = lbls + } + numSeries := int64(len(o.series)) + if o.seriesBatchSize == 0 || numSeries < o.seriesBatchSize { + o.seriesBatchSize = numSeries + } + + // Add a warning if rate or increase is applied on metrics which are not named like counters. + if o.functionName == "rate" || o.functionName == "increase" { + if len(series) > 0 { + metricName := series[0].Labels().Get(labels.MetricName) + if metricName != "" && + !strings.HasSuffix(metricName, "_total") && + !strings.HasSuffix(metricName, "_sum") && + !strings.HasSuffix(metricName, "_count") && + !strings.HasSuffix(metricName, "_bucket") { + o.nonCounterMetric = metricName + } + } + } + }) + return err +} + +func (o *matrixSelector) shouldCheckSampleLimit(firstSeries int64) bool { + seriesProcessed := o.currentSeries + 1 - firstSeries + + if seriesProcessed%sampleLimitCheckInterval == 0 { + return true + } + + isEndOfBatch := seriesProcessed >= o.seriesBatchSize + isLastSeries := o.currentSeries+1 >= int64(len(o.scanners)) + + return isEndOfBatch || isLastSeries +} + +func (o *matrixSelector) newBuffer(ctx context.Context) ringbuffer.Buffer { + if ringbuffer.UseStreamingRingBuffers(*o.opts, o.selectRange) { + switch o.functionName { + case "rate": + return ringbuffer.NewRateBuffer(ctx, *o.opts, true, true, o.selectRange, o.offset) + case "increase": + return ringbuffer.NewRateBuffer(ctx, *o.opts, true, false, o.selectRange, o.offset) + case "delta": + return ringbuffer.NewRateBuffer(ctx, *o.opts, false, false, o.selectRange, o.offset) + case "count_over_time": + return ringbuffer.NewCountOverTimeBuffer(*o.opts, o.selectRange, o.offset) + case "max_over_time": + return ringbuffer.NewMaxOverTimeBuffer(*o.opts, o.selectRange, o.offset) + case "min_over_time": + return ringbuffer.NewMinOverTimeBuffer(*o.opts, o.selectRange, o.offset) + case "sum_over_time": + return ringbuffer.NewSumOverTimeBuffer(*o.opts, o.selectRange, o.offset) + case "avg_over_time": + return ringbuffer.NewAvgOverTimeBuffer(*o.opts, o.selectRange, o.offset) + case "stddev_over_time": + return ringbuffer.NewStdDevOverTimeBuffer(*o.opts, o.selectRange, o.offset) + case "stdvar_over_time": + return ringbuffer.NewStdVarOverTimeBuffer(*o.opts, o.selectRange, o.offset) + case "present_over_time": + return ringbuffer.NewPresentOverTimeBuffer(*o.opts, o.selectRange, o.offset) + case "last_over_time": + return ringbuffer.NewLastOverTimeBuffer(*o.opts, o.selectRange, o.offset) + } + } + + if o.isExtFunction { + return ringbuffer.NewWithExtLookback(ctx, 8, o.selectRange, o.offset, o.opts.ExtLookbackDelta.Milliseconds()-1, o.call) + } + return ringbuffer.New(ctx, 8, o.selectRange, o.offset, o.call) + +} + +func (o *matrixSelector) String() string { + r := time.Duration(o.selectRange) * time.Millisecond + if o.call != nil { + return fmt.Sprintf("[matrixSelector] %v({%v}[%s] %v mod %v)", o.functionName, o.storage.Matchers(), r, o.shard, o.numShards) + } + return fmt.Sprintf("[matrixSelector] {%v}[%s] %v mod %v", o.storage.Matchers(), r, o.shard, o.numShards) +} + +// matrixIterSlice populates a matrix vector covering the requested range for a +// single time series, with points retrieved from an iterator. +// +// As an optimization, the matrix vector may already contain points of the same +// time series from the evaluation of an earlier step (with lower mint and maxt +// values). Any such points falling before mint are discarded; points that fall +// into the [mint, maxt] range are retained; only points with later timestamps +// are populated from the iterator. +// TODO(fpetkovski): Add max samples limit. +func (m *matrixScanner) selectPoints( + mint, maxt, evalt int64, + fh *histogram.FloatHistogram, + isExtFunction bool, +) error { + m.buffer.Reset(mint, evalt) + if m.lastSample.T > maxt { + return nil + } + + if bufMaxt := m.buffer.MaxT() + 1; bufMaxt > mint { + mint = bufMaxt + } + mint = max(mint, m.buffer.MaxT()+1) + if m.lastSample.T > mint { + m.buffer.Push(m.lastSample.T, m.lastSample.V) + m.lastSample.T = math.MinInt64 + mint = max(mint, m.buffer.MaxT()+1) + } + + appendedPointBeforeMint := !ringbuffer.Empty(m.buffer) + for valType := m.iterator.Next(); valType != chunkenc.ValNone; valType = m.iterator.Next() { + switch valType { + case chunkenc.ValHistogram, chunkenc.ValFloatHistogram: + if isExtFunction { + return ErrNativeHistogramsNotSupported + } + var t int64 + t, fh = m.iterator.AtFloatHistogram(fh) + if value.IsStaleNaN(fh.Sum) || t < mint { + continue + } + if t > maxt { + m.lastSample.T = t + if m.lastSample.V.H == nil { + m.lastSample.V.H = fh.Copy() + } else { + fh.CopyTo(m.lastSample.V.H) + } + return nil + } + if t > mint { + m.buffer.Push(t, ringbuffer.Value{H: fh}) + } + case chunkenc.ValFloat: + t, v := m.iterator.At() + if value.IsStaleNaN(v) { + continue + } + if m.metricAppearedTs == math.MinInt64 { + m.metricAppearedTs = t + } + if t > maxt { + m.lastSample.T, m.lastSample.V.F, m.lastSample.V.H = t, v, nil + return nil + } + if isExtFunction { + if t > mint || !appendedPointBeforeMint { + m.buffer.Push(t, ringbuffer.Value{F: v}) + appendedPointBeforeMint = true + } else { + m.buffer.ReadIntoLast(func(s *ringbuffer.Sample) { + s.T, s.V.F, s.V.H = t, v, nil + }) + } + } else { + if t > mint { + m.buffer.Push(t, ringbuffer.Value{F: v}) + } + } + } + } + return m.iterator.Err() +} + +// emitRingbufferWarnings converts warnings.Warnings flags to proper annotations with metric names. +func emitRingbufferWarnings(ctx context.Context, warn warnings.Warnings, metricName string) { + if warn&warnings.WarnNotCounter != 0 { + warnings.AddToContext(annotations.NewNativeHistogramNotCounterWarning(metricName, posrange.PositionRange{}), ctx) + } + if warn&warnings.WarnNotGauge != 0 { + warnings.AddToContext(annotations.NewNativeHistogramNotGaugeWarning(metricName, posrange.PositionRange{}), ctx) + } + if warn&warnings.WarnMixedFloatsHistograms != 0 { + warnings.AddToContext(annotations.NewMixedFloatsHistogramsWarning(metricName, posrange.PositionRange{}), ctx) + } + if warn&warnings.WarnMixedExponentialCustomBuckets != 0 { + warnings.AddToContext(annotations.NewMixedExponentialCustomHistogramsWarning(metricName, posrange.PositionRange{}), ctx) + } + if warn&warnings.WarnHistogramIgnoredInMixedRange != 0 { + warnings.AddToContext(annotations.NewHistogramIgnoredInMixedRangeInfo(metricName, posrange.PositionRange{}), ctx) + } + if warn&warnings.WarnCounterResetCollision != 0 { + warnings.AddToContext(annotations.NewHistogramCounterResetCollisionWarning(posrange.PositionRange{}, annotations.HistogramAgg), ctx) + } + if warn&warnings.WarnNHCBBoundsReconciled != 0 { + warnings.AddToContext(annotations.NewMismatchedCustomBucketsHistogramsInfo(posrange.PositionRange{}, annotations.HistogramSub), ctx) + } + if warn&warnings.WarnNHCBBoundsReconciledAgg != 0 { + warnings.AddToContext(annotations.NewMismatchedCustomBucketsHistogramsInfo(posrange.PositionRange{}, annotations.HistogramAgg), ctx) + } +} diff --git a/internal/promql-engine/storage/prometheus/pool.go b/internal/promql-engine/storage/prometheus/pool.go new file mode 100644 index 00000000000..9706b334081 --- /dev/null +++ b/internal/promql-engine/storage/prometheus/pool.go @@ -0,0 +1,84 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package prometheus + +import ( + "strconv" + "strings" + + "github.com/cespare/xxhash/v2" + "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/storage" +) + +var sep = []byte{'\xff'} + +type SelectorPool struct { + selectors map[uint64]*seriesSelector + + querier storage.Querier +} + +func NewSelectorPool(querier storage.Querier) *SelectorPool { + return &SelectorPool{ + selectors: make(map[uint64]*seriesSelector), + querier: querier, + } +} + +func (p *SelectorPool) GetSelector(mint, maxt, step int64, matchers []*labels.Matcher, hints storage.SelectHints) SeriesSelector { + key := hashMatchers(matchers, mint, maxt, hints) + if _, ok := p.selectors[key]; !ok { + p.selectors[key] = newSeriesSelector(p.querier, matchers, hints) + } + return p.selectors[key] +} + +func (p *SelectorPool) GetFilteredSelector(mint, maxt, step int64, matchers, filters []*labels.Matcher, hints storage.SelectHints) SeriesSelector { + key := hashMatchers(matchers, mint, maxt, hints) + if _, ok := p.selectors[key]; !ok { + p.selectors[key] = newSeriesSelector(p.querier, matchers, hints) + } + + return NewFilteredSelector(p.selectors[key], NewFilter(filters)) +} + +func hashMatchers(matchers []*labels.Matcher, mint, maxt int64, hints storage.SelectHints) uint64 { + sb := xxhash.New() + for _, m := range matchers { + writeMatcher(sb, m) + } + writeInt64(sb, mint) + writeInt64(sb, maxt) + writeInt64(sb, hints.Step) + writeString(sb, hints.Func) + writeString(sb, strings.Join(hints.Grouping, ";")) + writeBool(sb, hints.By) + writeString(sb, strings.Join(hints.ProjectionLabels, ";")) + writeBool(sb, hints.ProjectionInclude) + + key := sb.Sum64() + return key +} + +func writeMatcher(sb *xxhash.Digest, m *labels.Matcher) { + writeString(sb, m.Name) + writeString(sb, strconv.Itoa(int(m.Type))) + writeString(sb, m.Value) +} + +func writeInt64(sb *xxhash.Digest, val int64) { + _, _ = sb.WriteString(strconv.FormatInt(val, 10)) + _, _ = sb.Write(sep) +} + +func writeString(sb *xxhash.Digest, val string) { + _, _ = sb.WriteString(val) + _, _ = sb.Write(sep) +} + +func writeBool(sb *xxhash.Digest, val bool) { + _, _ = sb.WriteString(strconv.FormatBool(val)) + _, _ = sb.Write(sep) +} diff --git a/internal/promql-engine/storage/prometheus/scanners.go b/internal/promql-engine/storage/prometheus/scanners.go new file mode 100644 index 00000000000..a655151e9a3 --- /dev/null +++ b/internal/promql-engine/storage/prometheus/scanners.go @@ -0,0 +1,191 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package prometheus + +import ( + "context" + "math" + + "github.com/thanos-io/promql-engine/execution/exchange" + "github.com/thanos-io/promql-engine/execution/model" + "github.com/thanos-io/promql-engine/execution/parse" + "github.com/thanos-io/promql-engine/logicalplan" + "github.com/thanos-io/promql-engine/query" + "github.com/thanos-io/promql-engine/warnings" + + "github.com/efficientgo/core/errors" + "github.com/prometheus/prometheus/promql" + "github.com/prometheus/prometheus/promql/parser/posrange" + "github.com/prometheus/prometheus/storage" + "github.com/prometheus/prometheus/tsdb/chunkenc" + "github.com/prometheus/prometheus/util/annotations" +) + +type Scanners struct { + selectors *SelectorPool + + querier storage.Querier +} + +func (s *Scanners) Close() error { + return s.querier.Close() +} + +func NewPrometheusScanners(queryable storage.Queryable, qOpts *query.Options, lplan logicalplan.Plan) (*Scanners, error) { + var min, max int64 + if lplan != nil { + min, max = logicalplan.MinMaxTime(lplan.Root(), qOpts) + } else { + min, max = qOpts.Start.UnixMilli(), qOpts.End.UnixMilli() + } + + querier, err := queryable.Querier(min, max) + if err != nil { + return nil, errors.Wrap(err, "create querier") + } + return &Scanners{querier: querier, selectors: NewSelectorPool(querier)}, nil +} + +func (p Scanners) NewVectorSelector( + _ context.Context, + opts *query.Options, + hints storage.SelectHints, + logicalNode logicalplan.VectorSelector, +) (model.VectorOperator, error) { + // Update hints with projection information if available + if logicalNode.Projection != nil { + hints.ProjectionLabels = logicalNode.Projection.Labels + hints.ProjectionInclude = logicalNode.Projection.Include + } + + selector := p.selectors.GetFilteredSelector(hints.Start, hints.End, opts.Step.Milliseconds(), logicalNode.VectorSelector.LabelMatchers, logicalNode.Filters, hints) + if logicalNode.DecodeNativeHistogramStats { + selector = newHistogramStatsSelector(selector) + } + + operators := make([]model.VectorOperator, 0, opts.DecodingConcurrency) + for i := range opts.DecodingConcurrency { + operator := exchange.NewConcurrent( + NewVectorSelector( + selector, + opts, + logicalNode.Offset, + logicalNode.BatchSize, + logicalNode.SelectTimestamp, + i, + opts.DecodingConcurrency, + ), 2, opts) + operators = append(operators, operator) + } + + return exchange.NewCoalesce(opts, logicalNode.BatchSize*int64(opts.DecodingConcurrency), operators...), nil +} + +func (p Scanners) NewMatrixSelector( + ctx context.Context, + opts *query.Options, + hints storage.SelectHints, + logicalNode logicalplan.MatrixSelector, + call logicalplan.FunctionCall, +) (model.VectorOperator, error) { + arg := 0.0 + arg2 := 0.0 + switch call.Func.Name { + case "quantile_over_time": + unwrap, err := logicalplan.UnwrapFloat(call.Args[0]) + if err != nil { + return nil, errors.Wrapf(parse.ErrNotSupportedExpr, "quantile_over_time with expression as first argument is not supported") + } + arg = unwrap + if math.IsNaN(unwrap) || unwrap < 0 || unwrap > 1 { + warnings.AddToContext(annotations.NewInvalidQuantileWarning(unwrap, posrange.PositionRange{}), ctx) + } + case "predict_linear": + unwrap, err := logicalplan.UnwrapFloat(call.Args[1]) + if err != nil { + return nil, errors.Wrapf(parse.ErrNotSupportedExpr, "predict_linear with expression as second argument is not supported") + } + arg = unwrap + case "double_exponential_smoothing": + sf, err := logicalplan.UnwrapFloat(call.Args[1]) + if err != nil { + return nil, errors.Wrapf(parse.ErrNotSupportedExpr, "double_exponential_smoothing with expression as second argument is not supported") + } + + tf, err := logicalplan.UnwrapFloat(call.Args[2]) + if err != nil { + return nil, errors.Wrapf(parse.ErrNotSupportedExpr, "double_exponential_smoothing with expression as third argument is not supported") + } + + if sf <= 0 || sf >= 1 || tf <= 0 || tf >= 1 { + return nil, nil + } + arg = sf + arg2 = tf + } + + vs := logicalNode.VectorSelector + if vs.Projection != nil { + hints.ProjectionLabels = vs.Projection.Labels + hints.ProjectionInclude = vs.Projection.Include + } + + selector := p.selectors.GetFilteredSelector(hints.Start, hints.End, opts.Step.Milliseconds(), vs.LabelMatchers, vs.Filters, hints) + if logicalNode.VectorSelector.DecodeNativeHistogramStats { + selector = newHistogramStatsSelector(selector) + } + + operators := make([]model.VectorOperator, 0, opts.DecodingConcurrency) + for i := range opts.DecodingConcurrency { + operator, err := NewMatrixSelector( + selector, + call.Func.Name, + arg, + arg2, + opts, + logicalNode.Range, + vs.Offset, + vs.BatchSize, + i, + opts.DecodingConcurrency, + ) + if err != nil { + return nil, err + } + operators = append(operators, exchange.NewConcurrent(operator, 2, opts)) + } + + return exchange.NewCoalesce(opts, vs.BatchSize*int64(opts.DecodingConcurrency), operators...), nil +} + +type histogramStatsSelector struct { + SeriesSelector +} + +func newHistogramStatsSelector(seriesSelector SeriesSelector) histogramStatsSelector { + return histogramStatsSelector{SeriesSelector: seriesSelector} +} + +func (h histogramStatsSelector) GetSeries(ctx context.Context, shard, numShards int) ([]SignedSeries, error) { + series, err := h.SeriesSelector.GetSeries(ctx, shard, numShards) + if err != nil { + return nil, err + } + for i := range series { + series[i].Series = newHistogramStatsSeries(series[i].Series) + } + return series, nil +} + +type histogramStatsSeries struct { + storage.Series +} + +func newHistogramStatsSeries(series storage.Series) histogramStatsSeries { + return histogramStatsSeries{Series: series} +} + +func (h histogramStatsSeries) Iterator(it chunkenc.Iterator) chunkenc.Iterator { + return promql.NewHistogramStatsIterator(h.Series.Iterator(it)) +} diff --git a/internal/promql-engine/storage/prometheus/scanners_test.go b/internal/promql-engine/storage/prometheus/scanners_test.go new file mode 100644 index 00000000000..127f762db63 --- /dev/null +++ b/internal/promql-engine/storage/prometheus/scanners_test.go @@ -0,0 +1,89 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package prometheus + +import ( + "testing" + "time" + + "github.com/thanos-io/promql-engine/logicalplan" + "github.com/thanos-io/promql-engine/query" + + "github.com/prometheus/prometheus/promql/parser" + "github.com/stretchr/testify/require" +) + +func TestScannersMinMaxTime(t *testing.T) { + for _, tcase := range []struct { + expr string + start, end time.Time + step time.Duration + min, max int64 + }{ + { + expr: "foo offset 5m", + start: time.Unix(200, 0), + end: time.Unix(200, 0), + step: time.Second, + + min: -400000, + max: -100000, + }, + { + expr: `absent_over_time(http_requests_total @ 1800.000[1h:1m])`, + start: time.Unix(200, 0), + end: time.Unix(200, 0), + step: time.Second, + + min: 1500000, + max: 1800000, + }, + { + expr: `rate(testcounter_zero_cutoff[20m])`, + start: time.Unix(200, 0), + end: time.Unix(200, 0), + step: time.Second, + + min: -1000000, + max: 200000, + }, + { + expr: `rate(testcounter_zero_cutoff[20m])`, + start: time.Unix(200, 0), + end: time.Unix(400, 0), + step: time.Second, + + min: -1000000, + max: 400000, + }, + { + expr: "foo @ 20", + start: time.Unix(200, 0), + end: time.Unix(200, 0), + step: time.Second, + + min: -280000, + max: 20000, + }, + } { + t.Run(tcase.expr, func(t *testing.T) { + p, err := parser.ParseExpr(tcase.expr) + require.NoError(t, err) + + qOpts := &query.Options{ + Start: tcase.start, + End: tcase.end, + Step: tcase.step, + LookbackDelta: 5 * time.Duration(time.Minute), + } + + plan, _ := logicalplan.NewFromAST(p, qOpts, logicalplan.PlanOptions{}) + + min, max := logicalplan.MinMaxTime(plan.Root(), qOpts) + + require.Equal(t, tcase.min, min) + require.Equal(t, tcase.max, max) + }) + } +} diff --git a/internal/promql-engine/storage/prometheus/series_selector.go b/internal/promql-engine/storage/prometheus/series_selector.go new file mode 100644 index 00000000000..5e11e1ae525 --- /dev/null +++ b/internal/promql-engine/storage/prometheus/series_selector.go @@ -0,0 +1,87 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package prometheus + +import ( + "context" + "sync" + + "github.com/thanos-io/promql-engine/warnings" + + "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/storage" +) + +type SeriesSelector interface { + GetSeries(ctx context.Context, shard, numShards int) ([]SignedSeries, error) + Matchers() []*labels.Matcher +} + +type SignedSeries struct { + storage.Series + Signature uint64 +} + +type seriesSelector struct { + storage storage.Querier + matchers []*labels.Matcher + hints storage.SelectHints + + once sync.Once + series []SignedSeries +} + +func newSeriesSelector(storage storage.Querier, matchers []*labels.Matcher, hints storage.SelectHints) *seriesSelector { + return &seriesSelector{ + storage: storage, + matchers: matchers, + hints: hints, + } +} + +func (o *seriesSelector) Matchers() []*labels.Matcher { + return o.matchers +} + +func (o *seriesSelector) GetSeries(ctx context.Context, shard int, numShards int) ([]SignedSeries, error) { + var err error + o.once.Do(func() { err = o.loadSeries(ctx) }) + if err != nil { + return nil, err + } + + return seriesShard(o.series, shard, numShards), nil +} + +func (o *seriesSelector) loadSeries(ctx context.Context) error { + seriesSet := o.storage.Select(ctx, false, &o.hints, o.matchers...) + i := 0 + for seriesSet.Next() { + s := seriesSet.At() + o.series = append(o.series, SignedSeries{ + Series: s, + Signature: uint64(i), + }) + i++ + } + + for _, w := range seriesSet.Warnings() { + warnings.AddToContext(w, ctx) + } + return seriesSet.Err() +} + +func seriesShard(series []SignedSeries, index int, numShards int) []SignedSeries { + start := index * len(series) / numShards + end := min((index+1)*len(series)/numShards, len(series)) + + slice := series[start:end] + shard := make([]SignedSeries, len(slice)) + copy(shard, slice) + + for i := range shard { + shard[i].Signature = uint64(i) + } + return shard +} diff --git a/internal/promql-engine/storage/prometheus/vector_selector.go b/internal/promql-engine/storage/prometheus/vector_selector.go new file mode 100644 index 00000000000..915c06c3b4a --- /dev/null +++ b/internal/promql-engine/storage/prometheus/vector_selector.go @@ -0,0 +1,288 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package prometheus + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/thanos-io/promql-engine/execution/model" + "github.com/thanos-io/promql-engine/execution/telemetry" + "github.com/thanos-io/promql-engine/extlabels" + "github.com/thanos-io/promql-engine/query" + + "github.com/efficientgo/core/errors" + "github.com/prometheus/prometheus/model/histogram" + "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/model/value" + "github.com/prometheus/prometheus/storage" + "github.com/prometheus/prometheus/tsdb/chunkenc" +) + +type vectorScanner struct { + labels labels.Labels + signature uint64 + samples *storage.MemoizedSeriesIterator +} + +type vectorSelector struct { + telemetry telemetry.OperatorTelemetry + + storage SeriesSelector + scanners []vectorScanner + series []labels.Labels + + once sync.Once + + numSteps int + mint int64 + maxt int64 + lookbackDelta int64 + step int64 + offset int64 + seriesBatchSize int64 + + currentSeries int64 + currentStep int64 + + shard int + numShards int + + selectTimestamp bool + + opts *query.Options + lastTrackedSamples int +} + +// NewVectorSelector creates operator which selects vector of series. +func NewVectorSelector( + selector SeriesSelector, + queryOpts *query.Options, + offset time.Duration, + batchSize int64, + selectTimestamp bool, + shard, numShards int, +) model.VectorOperator { + o := &vectorSelector{ + storage: selector, + + mint: queryOpts.Start.UnixMilli(), + maxt: queryOpts.End.UnixMilli(), + step: queryOpts.Step.Milliseconds(), + currentStep: queryOpts.Start.UnixMilli(), + lookbackDelta: queryOpts.LookbackDelta.Milliseconds(), + offset: offset.Milliseconds(), + numSteps: queryOpts.NumStepsPerBatch(), + seriesBatchSize: batchSize, + + shard: shard, + numShards: numShards, + + selectTimestamp: selectTimestamp, + + opts: queryOpts, + } + + // For instant queries, set the step to a positive value + // so that the operator can terminate. + if o.step == 0 { + o.step = 1 + } + + o.telemetry = telemetry.NewTelemetry(o, queryOpts) + return telemetry.NewOperator(o.telemetry, o) +} + +func (o *vectorSelector) String() string { + return fmt.Sprintf("[vectorSelector] {%v} %v mod %v", o.storage.Matchers(), o.shard, o.numShards) +} + +func (o *vectorSelector) Explain() (next []model.VectorOperator) { + return nil +} + +func (o *vectorSelector) Series(ctx context.Context) ([]labels.Labels, error) { + if err := o.loadSeries(ctx); err != nil { + return nil, err + } + return o.series, nil +} + +func (o *vectorSelector) Next(ctx context.Context, buf []model.StepVector) (int, error) { + select { + case <-ctx.Done(): + return 0, ctx.Err() + default: + } + if o.currentStep > o.maxt { + return 0, nil + } + + if err := o.loadSeries(ctx); err != nil { + return 0, err + } + + ts := o.currentStep + n := 0 + maxSteps := min(o.numSteps, len(buf)) + + // Calculate expected samples per step: the actual number of series we'll process this batch. + // This is min(seriesBatchSize, remaining series to process). + remainingSeries := int64(len(o.scanners)) - o.currentSeries + expectedSamples := int(min(o.seriesBatchSize, remainingSeries)) + if expectedSamples <= 0 { + expectedSamples = len(o.scanners) + } + + for currStep := 0; currStep < maxSteps && ts <= o.maxt; currStep++ { + buf[n].Reset(ts) + n++ + ts += o.step + } + + var currStepSamples int + var totalSamples int + // Reset the current timestamp. + ts = o.currentStep + fromSeries := o.currentSeries + + for ; o.currentSeries-fromSeries < o.seriesBatchSize && o.currentSeries < int64(len(o.scanners)); o.currentSeries++ { + var ( + series = o.scanners[o.currentSeries] + seriesTs = ts + ) + for currStep := 0; currStep < n && seriesTs <= o.maxt; currStep++ { + currStepSamples = 0 + t, v, h, ok, err := selectPoint(series.samples, seriesTs, o.lookbackDelta, o.offset) + if err != nil { + return 0, err + } + if o.selectTimestamp { + v = float64(t) / 1000 + } + if ok { + if h != nil && !o.selectTimestamp { + // Lazy pre-allocate histogram slices only when we actually have histograms + buf[currStep].AppendHistogramWithSizeHint(series.signature, h, expectedSamples) + currStepSamples += telemetry.CalculateHistogramSampleCount(h) + } else { + // Lazy pre-allocate sample slices with capacity hint + buf[currStep].AppendSampleWithSizeHint(series.signature, v, expectedSamples) + currStepSamples++ + } + totalSamples += currStepSamples + } + o.telemetry.IncrementSamplesAtTimestamp(currStepSamples, seriesTs) + seriesTs += o.step + } + + if o.shouldCheckSampleLimit(fromSeries) { + if err := o.updateSampleTracker(totalSamples); err != nil { + return 0, err + } + } + } + + if o.currentSeries == int64(len(o.scanners)) { + o.currentStep += o.step * int64(n) + o.currentSeries = 0 + } + return n, nil +} + +func (o *vectorSelector) loadSeries(ctx context.Context) error { + var err error + o.once.Do(func() { + series, loadErr := o.storage.GetSeries(ctx, o.shard, o.numShards) + if loadErr != nil { + err = loadErr + return + } + + b := labels.NewBuilder(labels.EmptyLabels()) + o.scanners = make([]vectorScanner, len(series)) + o.series = make([]labels.Labels, len(series)) + for i, s := range series { + o.scanners[i] = vectorScanner{ + labels: s.Labels(), + signature: s.Signature, + samples: storage.NewMemoizedIterator(s.Iterator(nil), o.lookbackDelta), + } + b.Reset(s.Labels()) + // if we have pushed down a timestamp function into the scan we need to drop + // the reserved labels (__name__, __type__, __unit__) + if o.selectTimestamp { + b.Del(labels.MetricName) + b.Del(extlabels.MetricType) + b.Del(extlabels.MetricUnit) + } + o.series[i] = b.Labels() + } + + numSeries := int64(len(o.series)) + if o.seriesBatchSize == 0 || numSeries < o.seriesBatchSize { + o.seriesBatchSize = numSeries + } + }) + return err +} + +func (o *vectorSelector) updateSampleTracker(totalSamples int) error { + if o.lastTrackedSamples > 0 { + o.opts.SampleTracker.Remove(o.lastTrackedSamples) + } + if totalSamples > 0 { + o.opts.SampleTracker.Add(totalSamples) + } + o.lastTrackedSamples = totalSamples + return o.opts.SampleTracker.CheckLimit() +} + +func (o *vectorSelector) shouldCheckSampleLimit(fromSeries int64) bool { + seriesProcessed := o.currentSeries + 1 - fromSeries + + if seriesProcessed%sampleLimitCheckInterval == 0 { + return true + } + + isEndOfBatch := seriesProcessed >= o.seriesBatchSize + isLastSeries := o.currentSeries+1 >= int64(len(o.scanners)) + + return isEndOfBatch || isLastSeries +} + +// TODO(fpetkovski): Add max samples limit. +func selectPoint(it *storage.MemoizedSeriesIterator, ts, lookbackDelta, offset int64) (int64, float64, *histogram.FloatHistogram, bool, error) { + refTime := ts - offset + var t int64 + var v float64 + var fh *histogram.FloatHistogram + + valueType := it.Seek(refTime) + switch valueType { + case chunkenc.ValNone: + if it.Err() != nil { + return 0, 0, nil, false, it.Err() + } + case chunkenc.ValFloatHistogram, chunkenc.ValHistogram: + t, fh = it.AtFloatHistogram() + case chunkenc.ValFloat: + t, v = it.At() + default: + panic(errors.Newf("unknown value type %v", valueType)) + } + if valueType == chunkenc.ValNone || t > refTime { + var ok bool + t, v, fh, ok = it.PeekPrev() + if !ok || t <= refTime-lookbackDelta { + return 0, 0, nil, false, nil + } + } + if value.IsStaleNaN(v) || (fh != nil && value.IsStaleNaN(fh.Sum)) { + return 0, 0, nil, false, nil + } + return t, v, fh, true, nil +} diff --git a/internal/promql-engine/storage/scanners.go b/internal/promql-engine/storage/scanners.go new file mode 100644 index 00000000000..fba38b00122 --- /dev/null +++ b/internal/promql-engine/storage/scanners.go @@ -0,0 +1,20 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package storage + +import ( + "context" + + "github.com/thanos-io/promql-engine/execution/model" + "github.com/thanos-io/promql-engine/logicalplan" + "github.com/thanos-io/promql-engine/query" + + "github.com/prometheus/prometheus/storage" +) + +type Scanners interface { + Close() error + NewVectorSelector(ctx context.Context, opts *query.Options, hints storage.SelectHints, selector logicalplan.VectorSelector) (model.VectorOperator, error) + NewMatrixSelector(ctx context.Context, opts *query.Options, hints storage.SelectHints, selector logicalplan.MatrixSelector, call logicalplan.FunctionCall) (model.VectorOperator, error) +} diff --git a/internal/promql-engine/warnings/coalesce.go b/internal/promql-engine/warnings/coalesce.go new file mode 100644 index 00000000000..83180195d83 --- /dev/null +++ b/internal/promql-engine/warnings/coalesce.go @@ -0,0 +1,11 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package warnings + +func Coalesce(a, b error) error { + if a != nil { + return a + } + return b +} diff --git a/internal/promql-engine/warnings/context.go b/internal/promql-engine/warnings/context.go new file mode 100644 index 00000000000..3d9b7c01f9a --- /dev/null +++ b/internal/promql-engine/warnings/context.go @@ -0,0 +1,109 @@ +// Copyright (c) The Thanos Community Authors. +// Licensed under the Apache License 2.0. + +package warnings + +import ( + "context" + "fmt" + "maps" + "sync" + + "github.com/efficientgo/core/errors" + "github.com/prometheus/prometheus/model/histogram" + "github.com/prometheus/prometheus/util/annotations" +) + +// MixedFloatsHistogramsAggWarning is used when an aggregation encounters both floats and histograms. +// We define this here because Prometheus's NewMixedFloatsHistogramsAggWarning requires a posrange +// which we don't have at the accumulator level. +// +//lint:ignore faillint We need fmt.Errorf to match Prometheus error format exactly. +var MixedFloatsHistogramsAggWarning = fmt.Errorf("%w aggregation", annotations.MixedFloatsHistogramsWarning) + +// Warnings is a bitset of warning flags that can be returned by functions +// to indicate warning conditions. The actual warning messages with metric +// names are emitted by operators that have access to series labels. +type Warnings uint32 + +const ( + WarnNotCounter Warnings = 1 << iota + WarnNotGauge + WarnMixedFloatsHistograms + WarnMixedExponentialCustomBuckets + WarnHistogramIgnoredInMixedRange // for _over_time functions, only when both floats and histograms + WarnHistogramIgnoredInAggregation // for aggregations (max, min, stddev, etc.), always when histograms ignored + WarnCounterResetCollision + WarnNHCBBoundsReconciled // for subtraction operations (rate, irate, delta) + WarnNHCBBoundsReconciledAgg // for aggregation operations (sum, avg, sum_over_time, avg_over_time) + WarnIncompatibleTypesInBinOp +) + +type warningKey string + +const key warningKey = "promql-warnings" + +type warnings struct { + mu sync.Mutex + warns annotations.Annotations +} + +func newWarnings() *warnings { + return &warnings{warns: annotations.Annotations{}} +} + +func (w *warnings) add(warns error) { + w.mu.Lock() + defer w.mu.Unlock() + w.warns = w.warns.Add(warns) +} + +func (w *warnings) get() annotations.Annotations { + w.mu.Lock() + defer w.mu.Unlock() + return w.warns +} + +func (w *warnings) merge(anno annotations.Annotations) { + w.mu.Lock() + defer w.mu.Unlock() + w.warns = w.warns.Merge(anno) +} + +func NewContext(ctx context.Context) context.Context { + return context.WithValue(ctx, key, newWarnings()) +} + +func AddToContext(warn error, ctx context.Context) { + w, ok := ctx.Value(key).(*warnings) + if !ok { + return + } + w.add(warn) +} + +func MergeToContext(annos annotations.Annotations, ctx context.Context) { + w, ok := ctx.Value(key).(*warnings) + if !ok { + return + } + w.merge(annos) +} + +func FromContext(ctx context.Context) annotations.Annotations { + warns := ctx.Value(key).(*warnings).get() + + return maps.Clone(warns) +} + +// ConvertHistogramError converts histogram operation errors to appropriate annotation warnings. +// Returns nil if the error is not a histogram error. +func ConvertHistogramError(err error) error { + if err == nil { + return nil + } + if errors.Is(err, histogram.ErrHistogramsIncompatibleSchema) { + return annotations.MixedExponentialCustomHistogramsWarning + } + return err +} diff --git a/internal/tsdberrors/errors.go b/internal/tsdberrors/errors.go new file mode 100644 index 00000000000..2297577d30d --- /dev/null +++ b/internal/tsdberrors/errors.go @@ -0,0 +1,88 @@ +// Copyright (c) The Thanos Authors. +// Licensed under the Apache License 2.0. + +// Package tsdberrors provides multi-error helpers removed from Prometheus 3.13 +// (prometheus/prometheus#17768). Thanos still uses these until upstream adapts. +package tsdberrors + +import ( + "bytes" + "errors" + "fmt" + "io" +) + +type multiError []error + +// NewMulti returns multiError with provided errors added if not nil. +func NewMulti(errs ...error) multiError { + m := multiError{} + m.Add(errs...) + return m +} + +// Add adds single or many errors to the error list. Each error is added only if not nil. +func (es *multiError) Add(errs ...error) { + for _, err := range errs { + if err == nil { + continue + } + var merr nonNilMultiError + if errors.As(err, &merr) { + *es = append(*es, merr.errs...) + continue + } + *es = append(*es, err) + } +} + +// Err returns the error list as an error or nil if it is empty. +func (es multiError) Err() error { + if len(es) == 0 { + return nil + } + return nonNilMultiError{errs: es} +} + +type nonNilMultiError struct { + errs multiError +} + +func (es nonNilMultiError) Error() string { + var buf bytes.Buffer + + if len(es.errs) > 1 { + fmt.Fprintf(&buf, "%d errors: ", len(es.errs)) + } + + for i, err := range es.errs { + if i != 0 { + buf.WriteString("; ") + } + buf.WriteString(err.Error()) + } + + return buf.String() +} + +func (es nonNilMultiError) Is(target error) bool { + for _, err := range es.errs { + if errors.Is(err, target) { + return true + } + } + return false +} + +func (es nonNilMultiError) Unwrap() []error { + return es.errs +} + +// CloseAll closes all given closers while recording error in MultiError. +func CloseAll(cs []io.Closer) error { + errs := NewMulti() + for _, c := range cs { + errs.Add(c.Close()) + } + return errs.Err() +} diff --git a/pkg/alert/alert.go b/pkg/alert/alert.go index ee5389b0b64..26becf76b14 100644 --- a/pkg/alert/alert.go +++ b/pkg/alert/alert.go @@ -159,8 +159,8 @@ func (q *Queue) Push(alerts []*notifier.Alert) { q.toAddLset.Range(func(l labels.Label) { b.Set(l.Name, l.Value) }) - if lset, keep := relabel.Process(b.Labels(), q.alertRelabelConfigs...); keep { - a.Labels = lset + if relabel.ProcessBuilder(b, q.alertRelabelConfigs...) { + a.Labels = b.Labels() relabeledAlerts = append(relabeledAlerts, a) } } diff --git a/pkg/api/query/engine.go b/pkg/api/query/engine.go index 2f778c43055..372bfa03931 100644 --- a/pkg/api/query/engine.go +++ b/pkg/api/query/engine.go @@ -27,12 +27,14 @@ import ( "github.com/go-kit/log" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/prometheus/promql" + "github.com/prometheus/prometheus/promql/parser" "github.com/prometheus/prometheus/storage" "github.com/thanos-io/promql-engine/api" "github.com/thanos-io/promql-engine/engine" "github.com/thanos-io/promql-engine/logicalplan" "github.com/thanos-io/thanos/pkg/extprom" + "github.com/thanos-io/thanos/pkg/extpromql" "github.com/thanos-io/thanos/pkg/logutil" ) @@ -107,10 +109,14 @@ func NewQueryFactory( }, EnableNegativeOffset: true, EnableAtModifier: true, + Parser: parser.NewParser(extpromql.ParserOptions()), }, EnableXFunctions: enableXFunctions, EnableAnalysis: true, } + if opts.Parser == nil { + opts.Parser = parser.NewParser(extpromql.ParserOptions()) + } if activeQueryTracker != nil { opts.ActiveQueryTracker = activeQueryTracker } diff --git a/pkg/block/fetcher.go b/pkg/block/fetcher.go index fd7d51941b6..9f0664c5f0b 100644 --- a/pkg/block/fetcher.go +++ b/pkg/block/fetcher.go @@ -813,7 +813,8 @@ func (f *LabelShardedMetaFilter) Filter(_ context.Context, metas map[ulid.ULID]* b.Set(k, v) } - if processedLabels, _ := relabel.Process(b.Labels(), f.relabelConfig...); processedLabels.IsEmpty() { + relabel.ProcessBuilder(&b, f.relabelConfig...) + if b.Labels().IsEmpty() { synced.WithLabelValues(labelExcludedMeta).Inc() delete(metas, id) } diff --git a/pkg/block/writer.go b/pkg/block/writer.go index c7c999784ad..74f291c34ae 100644 --- a/pkg/block/writer.go +++ b/pkg/block/writer.go @@ -16,7 +16,7 @@ import ( "github.com/prometheus/prometheus/storage" "github.com/prometheus/prometheus/tsdb" "github.com/prometheus/prometheus/tsdb/chunks" - tsdb_errors "github.com/prometheus/prometheus/tsdb/errors" + tsdb_errors "github.com/thanos-io/thanos/internal/tsdberrors" "github.com/prometheus/prometheus/tsdb/fileutil" "github.com/prometheus/prometheus/tsdb/index" ) diff --git a/pkg/compact/downsample/downsample.go b/pkg/compact/downsample/downsample.go index 043a419f4de..6989677b2a0 100644 --- a/pkg/compact/downsample/downsample.go +++ b/pkg/compact/downsample/downsample.go @@ -440,10 +440,10 @@ func minSchema(samples []sample) int32 { func downsampleFloatBatch(batch []sample, resolution int64) chunks.Meta { ab := newAggrChunkBuilder() // Encode first raw value; see ApplyCounterResetsSeriesIterator. - ab.apps[AggrCounter].Append(batch[0].t, batch[0].v) + ab.apps[AggrCounter].Append(batch[0].t, batch[0].t, batch[0].v) lastT := downsampleBatch(batch, resolution, &floatAggregator{}, ab.add) // Encode last raw value; see ApplyCounterResetsSeriesIterator. - ab.apps[AggrCounter].Append(lastT, batch[len(batch)-1].v) + ab.apps[AggrCounter].Append(lastT, lastT, batch[len(batch)-1].v) return ab.encode() } @@ -480,7 +480,7 @@ func (b *aggrChunkBuilder) addHistogram(t int64, a sampleAggregator) { } b.appendFloatHistogram(AggrCounter, t, aggr.counter) b.appendFloatHistogram(AggrSum, t, aggr.sum) - b.apps[AggrCount].Append(t, float64(aggr.count)) + b.apps[AggrCount].Append(t, t, float64(aggr.count)) b.added++ } @@ -490,7 +490,7 @@ func (b *aggrChunkBuilder) addHistogram(t int64, a sampleAggregator) { func (b *aggrChunkBuilder) appendFloatHistogram(t AggrType, ts int64, fh *histogram.FloatHistogram) { app := b.apps[t].(*chunkenc.FloatHistogramAppender) - ch, _, cApp, err := b.apps[t].AppendFloatHistogram(app, ts, fh, false) + ch, _, cApp, err := b.apps[t].AppendFloatHistogram(app, ts, ts, fh, false) if err != nil { panic("unexpected error: " + err.Error()) } @@ -613,11 +613,11 @@ func (b *aggrChunkBuilder) add(t int64, a sampleAggregator) { } aggr := mustGetFloatAggregator(a) - b.apps[AggrSum].Append(t, aggr.sum) - b.apps[AggrMin].Append(t, aggr.min) - b.apps[AggrMax].Append(t, aggr.max) - b.apps[AggrCount].Append(t, float64(aggr.count)) - b.apps[AggrCounter].Append(t, aggr.counter) + b.apps[AggrSum].Append(t, t, aggr.sum) + b.apps[AggrMin].Append(t, t, aggr.min) + b.apps[AggrMax].Append(t, t, aggr.max) + b.apps[AggrCount].Append(t, t, float64(aggr.count)) + b.apps[AggrCounter].Append(t, t, aggr.counter) b.added++ } @@ -813,7 +813,7 @@ func genericAggregate( if t > maxt { maxt = t } - ab.apps[at].Append(t, f(a)) + ab.apps[at].Append(t, t, f(a)) }) return mint, maxt, nil @@ -1083,7 +1083,7 @@ func downsampleFloatAggrBatch(chks []*AggrChunk, buf *[]sample, resolution int64 ab.apps[AggrCounter], _ = ab.chunks[AggrCounter].Appender() // Retain first raw value; see ApplyCounterResetsSeriesIterator. - ab.apps[AggrCounter].Append((*buf)[0].t, (*buf)[0].v) + ab.apps[AggrCounter].Append((*buf)[0].t, (*buf)[0].t, (*buf)[0].v) lastT := downsampleBatch(*buf, resolution, &floatAggregator{}, func(t int64, a sampleAggregator) { if t < mint { @@ -1092,11 +1092,11 @@ func downsampleFloatAggrBatch(chks []*AggrChunk, buf *[]sample, resolution int64 if t > maxt { maxt = t } - ab.apps[AggrCounter].Append(t, mustGetFloatAggregator(a).counter) + ab.apps[AggrCounter].Append(t, t, mustGetFloatAggregator(a).counter) }) // Retain last raw value; see ApplyCounterResetsSeriesIterator. - ab.apps[AggrCounter].Append(lastT, it.lastV) + ab.apps[AggrCounter].Append(lastT, lastT, it.lastV) ab.mint = mint ab.maxt = maxt @@ -1208,6 +1208,13 @@ func (it *ApplyCounterResetsSeriesIterator) AtT() int64 { return it.lastT } +func (it *ApplyCounterResetsSeriesIterator) AtST() int64 { + if it.i >= len(it.chks) { + return 0 + } + return it.chks[it.i].AtST() +} + func (it *ApplyCounterResetsSeriesIterator) Seek(x int64) chunkenc.ValueType { // Don't use underlying Seek, but iterate over next to not miss counter resets. for { @@ -1306,6 +1313,10 @@ func (it *AverageChunkIterator) AtT() int64 { return it.t } +func (it *AverageChunkIterator) AtST() int64 { + return 0 +} + func (it *AverageChunkIterator) Err() error { if it.cntIt.Err() != nil { return it.cntIt.Err() diff --git a/pkg/compactv2/chunk_series_set.go b/pkg/compactv2/chunk_series_set.go index 1f70427984b..39bea93d8cd 100644 --- a/pkg/compactv2/chunk_series_set.go +++ b/pkg/compactv2/chunk_series_set.go @@ -98,6 +98,7 @@ func (e errChunkIterator) AtFloatHistogram(*histogram.FloatHistogram) (int64, *h panic("not implemented") } func (e errChunkIterator) AtT() int64 { return 0 } +func (e errChunkIterator) AtST() int64 { return 0 } func (e errChunkIterator) Next() chunkenc.ValueType { return chunkenc.ValNone } func (e errChunkIterator) Err() error { return e.err } diff --git a/pkg/compactv2/compactor.go b/pkg/compactv2/compactor.go index c79601d4e81..63f6f6b702a 100644 --- a/pkg/compactv2/compactor.go +++ b/pkg/compactv2/compactor.go @@ -15,7 +15,7 @@ import ( "github.com/prometheus/prometheus/storage" "github.com/prometheus/prometheus/tsdb" "github.com/prometheus/prometheus/tsdb/chunkenc" - tsdb_errors "github.com/prometheus/prometheus/tsdb/errors" + tsdb_errors "github.com/thanos-io/thanos/internal/tsdberrors" "github.com/prometheus/prometheus/tsdb/index" "github.com/thanos-io/thanos/pkg/block" diff --git a/pkg/compactv2/modifiers.go b/pkg/compactv2/modifiers.go index 67537a009c6..1c47460379d 100644 --- a/pkg/compactv2/modifiers.go +++ b/pkg/compactv2/modifiers.go @@ -290,6 +290,13 @@ func (p *delSeriesIterator) AtT() int64 { return t } +func (p *delSeriesIterator) AtST() int64 { + if p.curr == nil { + return 0 + } + return p.curr.AtST() +} + func (p *delSeriesIterator) Err() error { if err := p.delGenericSeriesIterator.Err(); err != nil { return err @@ -337,11 +344,11 @@ func (p *delChunkSeriesIterator) Next() bool { t, v := p.currDelIter.At() p.curr.MinTime = t - app.Append(t, v) + app.Append(t, t, v) for p.currDelIter.Next() != chunkenc.ValNone { t, v = p.currDelIter.At() - app.Append(t, v) + app.Append(t, t, v) } if err := p.currDelIter.Err(); err != nil { p.err = errors.Wrap(err, "iterate chunk while re-encoding") @@ -373,9 +380,10 @@ func (d *RelabelModifier) Modify(_ index.StringIter, set storage.ChunkSeriesSet, lbls := s.Labels() chksIter := s.Iterator(nil) - // The labels have to be copied because `relabel.Process` is now overwriting the original - // labels to same memory. This happens since Prometheus v2.39.0. - if processedLabels, _ := relabel.Process(lbls.Copy(), d.relabels...); processedLabels.IsEmpty() { + lb := labels.NewBuilder(lbls.Copy()) + relabel.ProcessBuilder(lb, d.relabels...) + processedLabels := lb.Labels() + if processedLabels.IsEmpty() { // Special case: Delete whole series if no labels are present. var ( minT int64 = math.MaxInt64 diff --git a/pkg/dedup/chunk_iter.go b/pkg/dedup/chunk_iter.go index 111dedd379e..6a9737d21cd 100644 --- a/pkg/dedup/chunk_iter.go +++ b/pkg/dedup/chunk_iter.go @@ -345,7 +345,7 @@ func (a *aggrChunkIterator) toChunk(at downsample.AggrType, minTime, maxTime int ) for it.Next() != chunkenc.ValNone { lastT, lastV = it.At() - appender.Append(lastT, lastV) + appender.Append(lastT, lastT, lastV) } if err := it.Err(); err != nil { return nil, err @@ -358,7 +358,7 @@ func (a *aggrChunkIterator) toChunk(at downsample.AggrType, minTime, maxTime int // Encode last sample for AggrCounter. if at == downsample.AggrCounter { - appender.Append(lastT, lastV) + appender.Append(lastT, lastT, lastV) } return &chunks.Meta{ diff --git a/pkg/dedup/iter.go b/pkg/dedup/iter.go index a8eddfdd023..3c43e4bc241 100644 --- a/pkg/dedup/iter.go +++ b/pkg/dedup/iter.go @@ -439,6 +439,13 @@ func (it *dedupSeriesIterator) AtT() int64 { return t } +func (it *dedupSeriesIterator) AtST() int64 { + if it.useA { + return it.a.AtST() + } + return it.b.AtST() +} + func (it *dedupSeriesIterator) Err() error { if it.a.Err() != nil { return it.a.Err() @@ -483,6 +490,10 @@ func (it *boundedSeriesIterator) AtT() int64 { return it.it.AtT() } +func (it *boundedSeriesIterator) AtST() int64 { + return it.it.AtST() +} + func (it *boundedSeriesIterator) Next() chunkenc.ValueType { valueType := it.it.Next() if valueType == chunkenc.ValNone { diff --git a/pkg/extpromql/parser.go b/pkg/extpromql/parser.go index 4f3fd950bc4..edaf8233b5b 100644 --- a/pkg/extpromql/parser.go +++ b/pkg/extpromql/parser.go @@ -7,6 +7,7 @@ import ( "fmt" "maps" "strings" + "sync" "github.com/pkg/errors" "github.com/prometheus/prometheus/model/labels" @@ -15,14 +16,37 @@ import ( "github.com/thanos-io/promql-engine/execution/parse" ) +var enableExperimentalFunctions bool + +// SetEnableExperimentalFunctions toggles parsing of experimental PromQL functions. +func SetEnableExperimentalFunctions(v bool) { + enableExperimentalFunctions = v +} + +// ParserOptions returns PromQL parser options for Thanos components. +func ParserOptions() parser.Options { + return parser.Options{ + EnableExperimentalFunctions: enableExperimentalFunctions, + } +} + +var parseMu sync.Mutex + // ParseExpr parses the input PromQL expression and returns the parsed representation. func ParseExpr(input string) (parser.Expr, error) { allFuncs := make(map[string]*parser.Function, len(parse.XFunctions)+len(parser.Functions)) maps.Copy(allFuncs, parser.Functions) maps.Copy(allFuncs, parse.XFunctions) - p := parser.NewParser(input, parser.WithFunctions(allFuncs)) - defer p.Close() - return p.ParseExpr() + + parseMu.Lock() + orig := parser.Functions + parser.Functions = allFuncs + defer func() { + parser.Functions = orig + parseMu.Unlock() + }() + + return parser.NewParser(ParserOptions()).ParseExpr(input) } // ParseMetricSelector parses the provided textual metric selector into a list of diff --git a/pkg/query/iter.go b/pkg/query/iter.go index e5276ec79bf..3736ba068bd 100644 --- a/pkg/query/iter.go +++ b/pkg/query/iter.go @@ -210,6 +210,7 @@ func (errSeriesIterator) AtFloatHistogram(*histogram.FloatHistogram) (int64, *hi return 0, nil } func (errSeriesIterator) AtT() int64 { return 0 } +func (errSeriesIterator) AtST() int64 { return 0 } func (it errSeriesIterator) Err() error { return it.err } // chunkSeriesIterator implements a series iterator on top @@ -261,6 +262,10 @@ func (it *chunkSeriesIterator) AtT() int64 { return it.cur.AtT() } +func (it *chunkSeriesIterator) AtST() int64 { + return it.cur.AtST() +} + func (it *chunkSeriesIterator) Next() chunkenc.ValueType { lastT := it.AtT() diff --git a/pkg/query/remote_engine.go b/pkg/query/remote_engine.go index 1c75d7c4942..3339b83687f 100644 --- a/pkg/query/remote_engine.go +++ b/pkg/query/remote_engine.go @@ -105,12 +105,14 @@ func NewRemoteEndpoints(logger log.Logger, getClients func() []Client, opts Opts } } -func (r remoteEndpoints) Engines() []api.RemoteEngine { +func (r remoteEndpoints) Engines(mint, maxt int64) []api.RemoteEngine { clients := r.getClients() engines := make([]api.RemoteEngine, len(clients)) for i := range clients { engines[i] = NewRemoteEngine(r.logger, clients[i], r.opts) } + _ = mint + _ = maxt return engines } diff --git a/pkg/receive/expandedpostingscache/tsdb.go b/pkg/receive/expandedpostingscache/tsdb.go index b1085fb6078..6163529231d 100644 --- a/pkg/receive/expandedpostingscache/tsdb.go +++ b/pkg/receive/expandedpostingscache/tsdb.go @@ -15,7 +15,7 @@ import ( "github.com/prometheus/prometheus/model/labels" "github.com/prometheus/prometheus/storage" prom_tsdb "github.com/prometheus/prometheus/tsdb" - tsdb_errors "github.com/prometheus/prometheus/tsdb/errors" + tsdb_errors "github.com/thanos-io/thanos/internal/tsdberrors" "github.com/prometheus/prometheus/tsdb/index" "github.com/prometheus/prometheus/tsdb/tombstones" "github.com/prometheus/prometheus/util/annotations" diff --git a/pkg/receive/handler.go b/pkg/receive/handler.go index 1e60f2d8edf..b0aca8584be 100644 --- a/pkg/receive/handler.go +++ b/pkg/receive/handler.go @@ -1160,12 +1160,11 @@ func (h *Handler) relabel(wreq *prompb.WriteRequest) { } timeSeries := make([]prompb.TimeSeries, 0, len(wreq.Timeseries)) for _, ts := range wreq.Timeseries { - var keep bool - lbls, keep := relabel.Process(labelpb.ZLabelsToPromLabels(ts.Labels), h.options.RelabelConfigs...) - if !keep { + b := labels.NewBuilder(labelpb.ZLabelsToPromLabels(ts.Labels)) + if !relabel.ProcessBuilder(b, h.options.RelabelConfigs...) { continue } - ts.Labels = labelpb.ZLabelsFromPromLabels(lbls) + ts.Labels = labelpb.ZLabelsFromPromLabels(b.Labels()) timeSeries = append(timeSeries, ts) } wreq.Timeseries = timeSeries diff --git a/pkg/rules/manager.go b/pkg/rules/manager.go index 4326f9c80f6..88b1aae9303 100644 --- a/pkg/rules/manager.go +++ b/pkg/rules/manager.go @@ -20,11 +20,13 @@ import ( "github.com/prometheus/common/model" "github.com/prometheus/prometheus/model/labels" "github.com/prometheus/prometheus/model/rulefmt" + "github.com/prometheus/prometheus/promql/parser" "github.com/prometheus/prometheus/rules" "gopkg.in/yaml.v2" "github.com/thanos-io/thanos/pkg/errutil" "github.com/thanos-io/thanos/pkg/extprom" + "github.com/thanos-io/thanos/pkg/extpromql" "github.com/thanos-io/thanos/pkg/rules/rulespb" "github.com/thanos-io/thanos/pkg/store/labelpb" "github.com/thanos-io/thanos/pkg/store/storepb" @@ -264,7 +266,7 @@ func (g configRuleAdapter) validate() (errs []error) { set[g.group.Name] = struct{}{} for i, r := range g.group.Rules { - for _, node := range r.Validate(rulefmt.RuleNode{}, model.UTF8Validation) { + for _, node := range r.Validate(rulefmt.RuleNode{}, model.UTF8Validation, parser.NewParser(extpromql.ParserOptions())) { var ruleName string if r.Alert != "" { ruleName = r.Alert diff --git a/pkg/store/prometheus.go b/pkg/store/prometheus.go index 7e56885e4a3..15b52da6465 100644 --- a/pkg/store/prometheus.go +++ b/pkg/store/prometheus.go @@ -534,7 +534,7 @@ func (p *PrometheusStore) encodeChunk(ss []prompb.Sample) (storepb.Chunk_Encodin return 0, nil, err } for _, s := range ss { - a.Append(s.Timestamp, s.Value) + a.Append(s.Timestamp, s.Timestamp, s.Value) } return storepb.Chunk_XOR, c.Bytes(), nil } diff --git a/pkg/store/tsdb_selector.go b/pkg/store/tsdb_selector.go index 761aa46737e..17be299b4bd 100644 --- a/pkg/store/tsdb_selector.go +++ b/pkg/store/tsdb_selector.go @@ -46,11 +46,12 @@ func (sr *TSDBSelector) MatchLabelSets(labelSets ...labels.Labels) (bool, []labe func (sr *TSDBSelector) runRelabelRules(labelSets []labels.Labels) []labels.Labels { result := make([]labels.Labels, 0) for _, labelSet := range labelSets { - if _, keep := relabel.Process(labelSet, sr.relabelConfig...); !keep { + b := labels.NewBuilder(labelSet) + if !relabel.ProcessBuilder(b, sr.relabelConfig...) { continue } - result = append(result, labelSet) + result = append(result, b.Labels()) } return result