diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 8d25176252..d1f3a0c988 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -19,7 +19,7 @@ jobs:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
- - uses: prometheus/promci@c0916f0a41f13444612a8f0f5e700ea34edd7c19 # v0.5.3
+ - uses: prometheus/promci@fc721ff8497a70a93a881cd552b71af7fb3a9d53 # v0.5.4
- uses: ./.github/promci/actions/setup_environment
with:
enable_npm: true
@@ -37,7 +37,7 @@ jobs:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
- - uses: prometheus/promci@c0916f0a41f13444612a8f0f5e700ea34edd7c19 # v0.5.3
+ - uses: prometheus/promci@fc721ff8497a70a93a881cd552b71af7fb3a9d53 # v0.5.4
- uses: ./.github/promci/actions/setup_environment
- run: go test --tags=dedupelabels ./...
- run: go test --tags=slicelabels -race ./cmd/prometheus ./model/textparse ./prompb/...
@@ -81,7 +81,7 @@ jobs:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
- - uses: prometheus/promci@c0916f0a41f13444612a8f0f5e700ea34edd7c19 # v0.5.3
+ - uses: prometheus/promci@fc721ff8497a70a93a881cd552b71af7fb3a9d53 # v0.5.4
- uses: ./.github/promci/actions/setup_environment
with:
enable_go: false
@@ -146,7 +146,7 @@ jobs:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
- - uses: prometheus/promci@c0916f0a41f13444612a8f0f5e700ea34edd7c19 # v0.5.3
+ - uses: prometheus/promci@fc721ff8497a70a93a881cd552b71af7fb3a9d53 # v0.5.4
- uses: ./.github/promci/actions/build
with:
promu_opts: "-p linux/amd64 -p windows/amd64 -p linux/arm64 -p darwin/amd64 -p darwin/arm64 -p linux/386"
@@ -173,7 +173,7 @@ jobs:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
- - uses: prometheus/promci@c0916f0a41f13444612a8f0f5e700ea34edd7c19 # v0.5.3
+ - uses: prometheus/promci@fc721ff8497a70a93a881cd552b71af7fb3a9d53 # v0.5.4
- uses: ./.github/promci/actions/build
with:
parallelism: 12
@@ -212,7 +212,7 @@ jobs:
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
- - uses: prometheus/promci@c0916f0a41f13444612a8f0f5e700ea34edd7c19 # v0.5.3
+ - uses: prometheus/promci@fc721ff8497a70a93a881cd552b71af7fb3a9d53 # v0.5.4
- uses: ./.github/promci/actions/setup_environment
with:
enable_npm: true
@@ -270,7 +270,7 @@ jobs:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
- - uses: prometheus/promci@c0916f0a41f13444612a8f0f5e700ea34edd7c19 # v0.5.3
+ - uses: prometheus/promci@fc721ff8497a70a93a881cd552b71af7fb3a9d53 # v0.5.4
- uses: ./.github/promci/actions/publish_main
with:
docker_hub_login: ${{ secrets.docker_hub_login }}
@@ -289,7 +289,7 @@ jobs:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
- - uses: prometheus/promci@c0916f0a41f13444612a8f0f5e700ea34edd7c19 # v0.5.3
+ - uses: prometheus/promci@fc721ff8497a70a93a881cd552b71af7fb3a9d53 # v0.5.4
- uses: ./.github/promci/actions/publish_release
with:
docker_hub_login: ${{ secrets.docker_hub_login }}
@@ -306,7 +306,7 @@ jobs:
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
- - uses: prometheus/promci@c0916f0a41f13444612a8f0f5e700ea34edd7c19 # v0.5.3
+ - uses: prometheus/promci@fc721ff8497a70a93a881cd552b71af7fb3a9d53 # v0.5.4
- name: Install nodejs
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
diff --git a/.github/workflows/fuzzing.yml b/.github/workflows/fuzzing.yml
index f9f7abafd6..55ab70dbac 100644
--- a/.github/workflows/fuzzing.yml
+++ b/.github/workflows/fuzzing.yml
@@ -1,30 +1,47 @@
-name: CIFuzz
+name: fuzzing
on:
workflow_call:
permissions:
contents: read
jobs:
- Fuzzing:
+ fuzzing:
+ name: Run Go Fuzz Tests
runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ fuzz_test: [FuzzParseMetricText, FuzzParseOpenMetric, FuzzParseMetricSelector, FuzzParseExpr]
steps:
- - name: Build Fuzzers
- id: build
- uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@4bf20ff8dfda18ad651583ebca9fb17a7ce1940a # master
+ - name: Checkout repository
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
- oss-fuzz-project-name: "prometheus"
- dry-run: false
- - name: Run Fuzzers
- uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@4bf20ff8dfda18ad651583ebca9fb17a7ce1940a # master
- # Note: Regularly check for updates to the pinned commit hash at:
- # https://github.com/google/oss-fuzz/tree/master/infra/cifuzz/actions/run_fuzzers
+ persist-credentials: false
+ - name: Install Go
+ uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
- oss-fuzz-project-name: "prometheus"
- fuzz-seconds: 600
- dry-run: false
- - name: Upload Crash
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
- if: failure() && steps.build.outcome == 'success'
+ go-version: 1.25.x
+ - name: Run Fuzzing
+ run: go test -fuzz=${{ matrix.fuzz_test }}$ -fuzztime=5m ./util/fuzzing
+ continue-on-error: true
+ id: fuzz
+ - name: Upload Crash Artifacts
+ uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
+ if: failure()
with:
- name: artifacts
- path: ./out/artifacts
+ name: fuzz-artifacts-${{ matrix.fuzz_test }}
+ path: util/fuzzing/testdata/fuzz/${{ matrix.fuzz_test }}
+ fuzzing_status:
+ # This status check aggregates the individual matrix jobs of the fuzzing
+ # step into a final status. Fails if a single matrix job fails, succeeds if
+ # all matrix jobs succeed.
+ name: Fuzzing
+ runs-on: ubuntu-latest
+ needs: [fuzzing]
+ if: always()
+ steps:
+ - name: Successful fuzzing
+ if: ${{ !(contains(needs.*.result, 'failure')) && !(contains(needs.*.result, 'cancelled')) }}
+ run: exit 0
+ - name: Failing or cancelled fuzzing
+ if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}
+ run: exit 1
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d43bb24720..a1afb0af59 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -54,7 +54,7 @@
* [FEATURE] OAuth2: support jwt-bearer grant-type (RFC7523 3.1). #17592
* [FEATURE] Dockerfile: Add OpenContainers spec labels to Dockerfile. #16483
* [FEATURE] SD: Add unified AWS service discovery for ec2, lightsail and ecs services. #17406
-* [FEATURE] Native histograms are now a stable, but optional feature, use the `scrape_native_histogram` config setting. #17232 #17315
+* [FEATURE] Native histograms are now a stable, but optional feature, use the `scrape_native_histograms` config setting. #17232 #17315
* [FEATURE] UI: Support anchored and smoothed keyword in promql editor. #17239
* [FEATURE] UI: Show detailed relabeling steps for each discovered target. #17337
* [FEATURE] Alerting: Add urlQueryEscape to template functions. #17403
diff --git a/Dockerfile b/Dockerfile
index 071e7441e3..98712d8f9c 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -9,7 +9,8 @@ LABEL org.opencontainers.image.authors="The Prometheus Authors" \
org.opencontainers.image.source="https://github.com/prometheus/prometheus" \
org.opencontainers.image.url="https://github.com/prometheus/prometheus" \
org.opencontainers.image.documentation="https://prometheus.io/docs" \
- org.opencontainers.image.licenses="Apache License 2.0"
+ org.opencontainers.image.licenses="Apache License 2.0" \
+ io.prometheus.image.variant="busybox"
ARG ARCH="amd64"
ARG OS="linux"
diff --git a/Dockerfile.distroless b/Dockerfile.distroless
new file mode 100644
index 0000000000..0ee184a91c
--- /dev/null
+++ b/Dockerfile.distroless
@@ -0,0 +1,29 @@
+ARG DISTROLESS_ARCH="amd64"
+
+# Use DISTROLESS_ARCH for base image selection (handles armv7->arm mapping).
+FROM gcr.io/distroless/static-debian13:nonroot-${DISTROLESS_ARCH}
+# Base image sets USER to 65532:65532 (nonroot user).
+
+ARG ARCH="amd64"
+ARG OS="linux"
+
+LABEL org.opencontainers.image.authors="The Prometheus Authors"
+LABEL org.opencontainers.image.vendor="Prometheus"
+LABEL org.opencontainers.image.title="Prometheus"
+LABEL org.opencontainers.image.description="The Prometheus monitoring system and time series database"
+LABEL org.opencontainers.image.source="https://github.com/prometheus/prometheus"
+LABEL org.opencontainers.image.url="https://github.com/prometheus/prometheus"
+LABEL org.opencontainers.image.documentation="https://prometheus.io/docs"
+LABEL org.opencontainers.image.licenses="Apache License 2.0"
+LABEL io.prometheus.image.variant="distroless"
+
+COPY documentation/examples/prometheus.yml /etc/prometheus/prometheus.yml
+COPY LICENSE NOTICE npm_licenses.tar.bz2 /
+COPY .build/${OS}-${ARCH}/prometheus /bin/prometheus
+COPY .build/${OS}-${ARCH}/promtool /bin/promtool
+
+WORKDIR /prometheus
+EXPOSE 9090
+ENTRYPOINT [ "/bin/prometheus" ]
+CMD [ "--config.file=/etc/prometheus/prometheus.yml", \
+ "--storage.tsdb.path=/prometheus" ]
diff --git a/Makefile b/Makefile
index 8c15ceb2e9..8bc4a3dcaa 100644
--- a/Makefile
+++ b/Makefile
@@ -220,3 +220,8 @@ check-node-version:
bump-go-version:
@echo ">> bumping Go minor version"
@./scripts/bump_go_version.sh
+
+.PHONY: generate-fuzzing-seed-corpus
+generate-fuzzing-seed-corpus:
+ @echo ">> Generating fuzzing seed corpus"
+ @$(GO) generate -tags fuzzing ./util/fuzzing/corpus_gen
diff --git a/Makefile.common b/Makefile.common
index 7beae6e58f..b8c9b3844c 100644
--- a/Makefile.common
+++ b/Makefile.common
@@ -82,11 +82,32 @@ endif
PREFIX ?= $(shell pwd)
BIN_DIR ?= $(shell pwd)
DOCKER_IMAGE_TAG ?= $(subst /,-,$(shell git rev-parse --abbrev-ref HEAD))
-DOCKERFILE_PATH ?= ./Dockerfile
DOCKERBUILD_CONTEXT ?= ./
DOCKER_REPO ?= prom
+# Check if deprecated DOCKERFILE_PATH is set
+ifdef DOCKERFILE_PATH
+$(error DOCKERFILE_PATH is deprecated. Use DOCKERFILE_VARIANTS ?= $(DOCKERFILE_PATH) in the Makefile)
+endif
+
DOCKER_ARCHS ?= amd64
+DOCKERFILE_VARIANTS ?= Dockerfile $(wildcard Dockerfile.*)
+
+# Function to extract variant from Dockerfile label.
+# Returns the variant name from io.prometheus.image.variant label, or "default" if not found.
+define dockerfile_variant
+$(strip $(or $(shell sed -n 's/.*io\.prometheus\.image\.variant="\([^"]*\)".*/\1/p' $(1)),default))
+endef
+
+# Check for duplicate variant names (including default for Dockerfiles without labels).
+DOCKERFILE_VARIANT_NAMES := $(foreach df,$(DOCKERFILE_VARIANTS),$(call dockerfile_variant,$(df)))
+DOCKERFILE_VARIANT_NAMES_SORTED := $(sort $(DOCKERFILE_VARIANT_NAMES))
+ifneq ($(words $(DOCKERFILE_VARIANT_NAMES)),$(words $(DOCKERFILE_VARIANT_NAMES_SORTED)))
+$(error Duplicate variant names found. Each Dockerfile must have a unique io.prometheus.image.variant label, and only one can be without a label (default))
+endif
+
+# Build variant:dockerfile pairs for shell iteration.
+DOCKERFILE_VARIANTS_WITH_NAMES := $(foreach df,$(DOCKERFILE_VARIANTS),$(call dockerfile_variant,$(df)):$(df))
BUILD_DOCKER_ARCHS = $(addprefix common-docker-,$(DOCKER_ARCHS))
PUBLISH_DOCKER_ARCHS = $(addprefix common-docker-publish-,$(DOCKER_ARCHS))
@@ -226,28 +247,110 @@ common-docker-repo-name:
.PHONY: common-docker $(BUILD_DOCKER_ARCHS)
common-docker: $(BUILD_DOCKER_ARCHS)
$(BUILD_DOCKER_ARCHS): common-docker-%:
- docker build -t "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" \
- -f $(DOCKERFILE_PATH) \
- --build-arg ARCH="$*" \
- --build-arg OS="linux" \
- $(DOCKERBUILD_CONTEXT)
+ @for variant in $(DOCKERFILE_VARIANTS_WITH_NAMES); do \
+ dockerfile=$${variant#*:}; \
+ variant_name=$${variant%%:*}; \
+ distroless_arch="$*"; \
+ if [ "$*" = "armv7" ]; then \
+ distroless_arch="arm"; \
+ fi; \
+ if [ "$$dockerfile" = "Dockerfile" ]; then \
+ echo "Building default variant ($$variant_name) for linux-$* using $$dockerfile"; \
+ docker build -t "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" \
+ -f $$dockerfile \
+ --build-arg ARCH="$*" \
+ --build-arg OS="linux" \
+ --build-arg DISTROLESS_ARCH="$$distroless_arch" \
+ $(DOCKERBUILD_CONTEXT); \
+ if [ "$$variant_name" != "default" ]; then \
+ echo "Tagging default variant with $$variant_name suffix"; \
+ docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" \
+ "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name"; \
+ fi; \
+ else \
+ echo "Building $$variant_name variant for linux-$* using $$dockerfile"; \
+ docker build -t "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name" \
+ -f $$dockerfile \
+ --build-arg ARCH="$*" \
+ --build-arg OS="linux" \
+ --build-arg DISTROLESS_ARCH="$$distroless_arch" \
+ $(DOCKERBUILD_CONTEXT); \
+ fi; \
+ done
.PHONY: common-docker-publish $(PUBLISH_DOCKER_ARCHS)
common-docker-publish: $(PUBLISH_DOCKER_ARCHS)
$(PUBLISH_DOCKER_ARCHS): common-docker-publish-%:
- docker push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)"
+ @for variant in $(DOCKERFILE_VARIANTS_WITH_NAMES); do \
+ dockerfile=$${variant#*:}; \
+ variant_name=$${variant%%:*}; \
+ if [ "$$dockerfile" != "Dockerfile" ] || [ "$$variant_name" != "default" ]; then \
+ echo "Pushing $$variant_name variant for linux-$*"; \
+ docker push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name"; \
+ fi; \
+ if [ "$$dockerfile" = "Dockerfile" ]; then \
+ echo "Pushing default variant ($$variant_name) for linux-$*"; \
+ docker push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)"; \
+ fi; \
+ if [ "$(DOCKER_IMAGE_TAG)" = "latest" ]; then \
+ if [ "$$dockerfile" != "Dockerfile" ] || [ "$$variant_name" != "default" ]; then \
+ echo "Pushing $$variant_name variant version tags for linux-$*"; \
+ docker push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:v$(DOCKER_MAJOR_VERSION_TAG)-$$variant_name"; \
+ fi; \
+ if [ "$$dockerfile" = "Dockerfile" ]; then \
+ echo "Pushing default variant version tag for linux-$*"; \
+ docker push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:v$(DOCKER_MAJOR_VERSION_TAG)"; \
+ fi; \
+ fi; \
+ done
DOCKER_MAJOR_VERSION_TAG = $(firstword $(subst ., ,$(shell cat VERSION)))
.PHONY: common-docker-tag-latest $(TAG_DOCKER_ARCHS)
common-docker-tag-latest: $(TAG_DOCKER_ARCHS)
$(TAG_DOCKER_ARCHS): common-docker-tag-latest-%:
- docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:latest"
- docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:v$(DOCKER_MAJOR_VERSION_TAG)"
+ @for variant in $(DOCKERFILE_VARIANTS_WITH_NAMES); do \
+ dockerfile=$${variant#*:}; \
+ variant_name=$${variant%%:*}; \
+ if [ "$$dockerfile" != "Dockerfile" ] || [ "$$variant_name" != "default" ]; then \
+ echo "Tagging $$variant_name variant for linux-$* as latest"; \
+ docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:latest-$$variant_name"; \
+ docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:v$(DOCKER_MAJOR_VERSION_TAG)-$$variant_name"; \
+ fi; \
+ if [ "$$dockerfile" = "Dockerfile" ]; then \
+ echo "Tagging default variant ($$variant_name) for linux-$* as latest"; \
+ docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:latest"; \
+ docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:v$(DOCKER_MAJOR_VERSION_TAG)"; \
+ fi; \
+ done
.PHONY: common-docker-manifest
common-docker-manifest:
- DOCKER_CLI_EXPERIMENTAL=enabled docker manifest create -a "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)" $(foreach ARCH,$(DOCKER_ARCHS),$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$(ARCH):$(SANITIZED_DOCKER_IMAGE_TAG))
- DOCKER_CLI_EXPERIMENTAL=enabled docker manifest push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)"
+ @for variant in $(DOCKERFILE_VARIANTS_WITH_NAMES); do \
+ dockerfile=$${variant#*:}; \
+ variant_name=$${variant%%:*}; \
+ if [ "$$dockerfile" != "Dockerfile" ] || [ "$$variant_name" != "default" ]; then \
+ echo "Creating manifest for $$variant_name variant"; \
+ DOCKER_CLI_EXPERIMENTAL=enabled docker manifest create -a "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name" $(foreach ARCH,$(DOCKER_ARCHS),$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$(ARCH):$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name); \
+ DOCKER_CLI_EXPERIMENTAL=enabled docker manifest push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name"; \
+ fi; \
+ if [ "$$dockerfile" = "Dockerfile" ]; then \
+ echo "Creating default variant ($$variant_name) manifest"; \
+ DOCKER_CLI_EXPERIMENTAL=enabled docker manifest create -a "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)" $(foreach ARCH,$(DOCKER_ARCHS),$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$(ARCH):$(SANITIZED_DOCKER_IMAGE_TAG)); \
+ DOCKER_CLI_EXPERIMENTAL=enabled docker manifest push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)"; \
+ fi; \
+ if [ "$(DOCKER_IMAGE_TAG)" = "latest" ]; then \
+ if [ "$$dockerfile" != "Dockerfile" ] || [ "$$variant_name" != "default" ]; then \
+ echo "Creating manifest for $$variant_name variant version tag"; \
+ DOCKER_CLI_EXPERIMENTAL=enabled docker manifest create -a "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):v$(DOCKER_MAJOR_VERSION_TAG)-$$variant_name" $(foreach ARCH,$(DOCKER_ARCHS),$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$(ARCH):v$(DOCKER_MAJOR_VERSION_TAG)-$$variant_name); \
+ DOCKER_CLI_EXPERIMENTAL=enabled docker manifest push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):v$(DOCKER_MAJOR_VERSION_TAG)-$$variant_name"; \
+ fi; \
+ if [ "$$dockerfile" = "Dockerfile" ]; then \
+ echo "Creating default variant version tag manifest"; \
+ DOCKER_CLI_EXPERIMENTAL=enabled docker manifest create -a "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):v$(DOCKER_MAJOR_VERSION_TAG)" $(foreach ARCH,$(DOCKER_ARCHS),$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$(ARCH):v$(DOCKER_MAJOR_VERSION_TAG)); \
+ DOCKER_CLI_EXPERIMENTAL=enabled docker manifest push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):v$(DOCKER_MAJOR_VERSION_TAG)"; \
+ fi; \
+ fi; \
+ done
.PHONY: promu
promu: $(PROMU)
diff --git a/RELEASE.md b/RELEASE.md
index c7375b35aa..5a8f8601ab 100644
--- a/RELEASE.md
+++ b/RELEASE.md
@@ -7,19 +7,20 @@ This page describes the release process and the currently planned schedule for u
Release cadence of first pre-releases being cut is 6 weeks.
Please see [the v2.55 RELEASE.md](https://github.com/prometheus/prometheus/blob/release-2.55/RELEASE.md) for the v2 release series schedule.
-| release series | date of first pre-release (year-month-day) | release shepherd |
-|----------------|--------------------------------------------|------------------------------------|
-| v3.0 | 2024-11-14 | Jan Fajerski (GitHub: @jan--f) |
-| v3.1 | 2024-12-17 | Bryan Boreham (GitHub: @bboreham) |
-| v3.2 | 2025-01-28 | Jan Fajerski (GitHub: @jan--f) |
-| v3.3 | 2025-03-11 | Ayoub Mrini (Github: @machine424) |
-| v3.4 | 2025-04-29 | Jan-Otto Kröpke (Github: @jkroepke)|
-| v3.5 LTS | 2025-06-03 | Bryan Boreham (GitHub: @bboreham) |
-| v3.6 | 2025-08-01 | Ayoub Mrini (Github: @machine424) |
-| v3.7 | 2025-09-25 | Arthur Sens and George Krajcsovits (Github: @ArthurSens and @krajorama)|
-| v3.8 | 2025-11-06 | Jan Fajerski (GitHub: @jan--f) |
-| v3.9 | 2025-12-18 | Bryan Boreham (GitHub: @bboreham) |
-| v3.10 | 2026-02-05 | **volunteer welcome** |
+| release series | date of first pre-release (year-month-day) | release shepherd |
+|----------------|--------------------------------------------|-------------------------------------------------------------------------|
+| v3.0 | 2024-11-14 | Jan Fajerski (GitHub: @jan--f) |
+| v3.1 | 2024-12-17 | Bryan Boreham (GitHub: @bboreham) |
+| v3.2 | 2025-01-28 | Jan Fajerski (GitHub: @jan--f) |
+| v3.3 | 2025-03-11 | Ayoub Mrini (Github: @machine424) |
+| v3.4 | 2025-04-29 | Jan-Otto Kröpke (Github: @jkroepke) |
+| v3.5 LTS | 2025-06-03 | Bryan Boreham (GitHub: @bboreham) |
+| v3.6 | 2025-08-01 | Ayoub Mrini (Github: @machine424) |
+| v3.7 | 2025-09-25 | Arthur Sens and George Krajcsovits (Github: @ArthurSens and @krajorama) |
+| v3.8 | 2025-11-06 | Jan Fajerski (GitHub: @jan--f) |
+| v3.9 | 2025-12-18 | Bryan Boreham (GitHub: @bboreham) |
+| v3.10 | 2026-02-05 | Ganesh Vernekar (Github: @codesome) |
+| v3.11 | 2026-03-19 | **volunteer welcome** |
If you are interested in volunteering please create a pull request against the [prometheus/prometheus](https://github.com/prometheus/prometheus) repository and propose yourself for the release series of your choice.
diff --git a/cmd/prometheus/main.go b/cmd/prometheus/main.go
index 0fa48c72b9..53584085ec 100644
--- a/cmd/prometheus/main.go
+++ b/cmd/prometheus/main.go
@@ -281,6 +281,9 @@ func (c *flagConfig) setFeatureListOptions(logger *slog.Logger) error {
case "promql-extended-range-selectors":
parser.EnableExtendedRangeSelectors = true
logger.Info("Experimental PromQL extended range selectors enabled.")
+ case "promql-binop-fill-modifiers":
+ parser.EnableBinopFillModifiers = true
+ logger.Info("Experimental PromQL binary operator fill modifiers enabled.")
case "":
continue
case "old-ui":
@@ -578,7 +581,7 @@ func main() {
a.Flag("scrape.discovery-reload-interval", "Interval used by scrape manager to throttle target groups updates.").
Hidden().Default("5s").SetValue(&cfg.scrape.DiscoveryReloadInterval)
- a.Flag("enable-feature", "Comma separated feature names to enable. Valid options: exemplar-storage, expand-external-labels, memory-snapshot-on-shutdown, promql-per-step-stats, promql-experimental-functions, extra-scrape-metrics, auto-gomaxprocs, created-timestamp-zero-ingestion, concurrent-rule-eval, delayed-compaction, old-ui, otlp-deltatocumulative, promql-duration-expr, use-uncached-io, promql-extended-range-selectors. See https://prometheus.io/docs/prometheus/latest/feature_flags/ for more details.").
+ a.Flag("enable-feature", "Comma separated feature names to enable. Valid options: exemplar-storage, expand-external-labels, memory-snapshot-on-shutdown, promql-per-step-stats, promql-experimental-functions, extra-scrape-metrics, auto-gomaxprocs, created-timestamp-zero-ingestion, concurrent-rule-eval, delayed-compaction, old-ui, otlp-deltatocumulative, promql-duration-expr, use-uncached-io, promql-extended-range-selectors, promql-binop-fill-modifiers. See https://prometheus.io/docs/prometheus/latest/feature_flags/ for more details.").
Default("").StringsVar(&cfg.featureList)
a.Flag("agent", "Run Prometheus in 'Agent mode'.").BoolVar(&agentMode)
@@ -1573,7 +1576,7 @@ func reloadConfig(filename string, enableExemplarStorage bool, logger *slog.Logg
logger.Error("Failed to apply configuration", "err", err)
failed = true
}
- timingsLogger = timingsLogger.With((rl.name), time.Since(rstart))
+ timingsLogger = timingsLogger.With(rl.name, time.Since(rstart))
}
if failed {
return fmt.Errorf("one or more errors occurred while applying the new configuration (--config.file=%q)", filename)
@@ -1747,6 +1750,14 @@ func (s *readyStorage) Appender(ctx context.Context) storage.Appender {
return notReadyAppender{}
}
+// AppenderV2 implements the Storage interface.
+func (s *readyStorage) AppenderV2(ctx context.Context) storage.AppenderV2 {
+ if x := s.get(); x != nil {
+ return x.AppenderV2(ctx)
+ }
+ return notReadyAppenderV2{}
+}
+
type notReadyAppender struct{}
// SetOptions does nothing in this appender implementation.
@@ -1780,6 +1791,15 @@ func (notReadyAppender) Commit() error { return tsdb.ErrNotReady }
func (notReadyAppender) Rollback() error { return tsdb.ErrNotReady }
+type notReadyAppenderV2 struct{}
+
+func (notReadyAppenderV2) Append(storage.SeriesRef, labels.Labels, int64, int64, float64, *histogram.Histogram, *histogram.FloatHistogram, storage.AOptions) (storage.SeriesRef, error) {
+ return 0, tsdb.ErrNotReady
+}
+func (notReadyAppenderV2) Commit() error { return tsdb.ErrNotReady }
+
+func (notReadyAppenderV2) Rollback() error { return tsdb.ErrNotReady }
+
// Close implements the Storage interface.
func (s *readyStorage) Close() error {
if x := s.get(); x != nil {
diff --git a/cmd/prometheus/testdata/features.json b/cmd/prometheus/testdata/features.json
index 145bb04d77..4c893daae2 100644
--- a/cmd/prometheus/testdata/features.json
+++ b/cmd/prometheus/testdata/features.json
@@ -28,6 +28,9 @@
"by": true,
"delayed_name_removal": false,
"duration_expr": false,
+ "fill": false,
+ "fill_left": false,
+ "fill_right": false,
"group_left": true,
"group_right": true,
"ignoring": true,
diff --git a/cmd/promtool/backfill.go b/cmd/promtool/backfill.go
index f04a76b0a5..e7a9a7f18a 100644
--- a/cmd/promtool/backfill.go
+++ b/cmd/promtool/backfill.go
@@ -27,7 +27,6 @@ import (
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/model/textparse"
"github.com/prometheus/prometheus/tsdb"
- tsdb_errors "github.com/prometheus/prometheus/tsdb/errors"
)
func getMinAndMaxTimestamps(p textparse.Parser) (int64, int64, error) {
@@ -94,7 +93,7 @@ func createBlocks(input []byte, mint, maxt, maxBlockDuration int64, maxSamplesIn
return err
}
defer func() {
- returnErr = tsdb_errors.NewMulti(returnErr, db.Close()).Err()
+ returnErr = errors.Join(returnErr, db.Close())
}()
var (
@@ -125,7 +124,7 @@ func createBlocks(input []byte, mint, maxt, maxBlockDuration int64, maxSamplesIn
return fmt.Errorf("block writer: %w", err)
}
defer func() {
- err = tsdb_errors.NewMulti(err, w.Close()).Err()
+ err = errors.Join(err, w.Close())
}()
ctx := context.Background()
diff --git a/cmd/promtool/rules.go b/cmd/promtool/rules.go
index 3960206f6b..bb45178e9c 100644
--- a/cmd/promtool/rules.go
+++ b/cmd/promtool/rules.go
@@ -15,6 +15,7 @@ package main
import (
"context"
+ "errors"
"fmt"
"log/slog"
"time"
@@ -28,7 +29,6 @@ import (
"github.com/prometheus/prometheus/rules"
"github.com/prometheus/prometheus/storage"
"github.com/prometheus/prometheus/tsdb"
- tsdb_errors "github.com/prometheus/prometheus/tsdb/errors"
)
const maxSamplesInMemory = 5000
@@ -143,7 +143,7 @@ func (importer *ruleImporter) importRule(ctx context.Context, ruleExpr, ruleName
var closed bool
defer func() {
if !closed {
- err = tsdb_errors.NewMulti(err, w.Close()).Err()
+ err = errors.Join(err, w.Close())
}
}()
app := newMultipleAppender(ctx, w)
@@ -181,7 +181,7 @@ func (importer *ruleImporter) importRule(ctx context.Context, ruleExpr, ruleName
if err := app.flushAndCommit(ctx); err != nil {
return fmt.Errorf("flush and commit: %w", err)
}
- err = tsdb_errors.NewMulti(err, w.Close()).Err()
+ err = errors.Join(err, w.Close())
closed = true
}
diff --git a/cmd/promtool/tsdb.go b/cmd/promtool/tsdb.go
index 9ccd1da714..d0016ec0aa 100644
--- a/cmd/promtool/tsdb.go
+++ b/cmd/promtool/tsdb.go
@@ -43,7 +43,6 @@ import (
"github.com/prometheus/prometheus/tsdb"
"github.com/prometheus/prometheus/tsdb/chunkenc"
"github.com/prometheus/prometheus/tsdb/chunks"
- tsdb_errors "github.com/prometheus/prometheus/tsdb/errors"
"github.com/prometheus/prometheus/tsdb/fileutil"
"github.com/prometheus/prometheus/tsdb/index"
)
@@ -339,7 +338,7 @@ func listBlocks(path string, humanReadable bool) error {
return err
}
defer func() {
- err = tsdb_errors.NewMulti(err, db.Close()).Err()
+ err = errors.Join(err, db.Close())
}()
blocks, err := db.Blocks()
if err != nil {
@@ -425,7 +424,7 @@ func analyzeBlock(ctx context.Context, path, blockID string, limit int, runExten
return err
}
defer func() {
- err = tsdb_errors.NewMulti(err, db.Close()).Err()
+ err = errors.Join(err, db.Close())
}()
meta := block.Meta()
@@ -625,7 +624,7 @@ func analyzeCompaction(ctx context.Context, block tsdb.BlockReader, indexr tsdb.
return err
}
defer func() {
- err = tsdb_errors.NewMulti(err, chunkr.Close()).Err()
+ err = errors.Join(err, chunkr.Close())
}()
totalChunks := 0
@@ -713,7 +712,7 @@ func dumpTSDBData(ctx context.Context, dbDir, sandboxDirRoot string, mint, maxt
return err
}
defer func() {
- err = tsdb_errors.NewMulti(err, db.Close()).Err()
+ err = errors.Join(err, db.Close())
}()
q, err := db.Querier(mint, maxt)
if err != nil {
@@ -743,7 +742,7 @@ func dumpTSDBData(ctx context.Context, dbDir, sandboxDirRoot string, mint, maxt
}
if ws := ss.Warnings(); len(ws) > 0 {
- return tsdb_errors.NewMulti(ws.AsErrors()...).Err()
+ return errors.Join(ws.AsErrors()...)
}
if ss.Err() != nil {
diff --git a/discovery/aws/aws.go b/discovery/aws/aws.go
index 1ac97b3c9e..be6b4dabbe 100644
--- a/discovery/aws/aws.go
+++ b/discovery/aws/aws.go
@@ -101,7 +101,8 @@ func (c *SDConfig) UnmarshalYAML(unmarshal func(any) error) error {
switch c.Role {
case RoleEC2:
if c.EC2SDConfig == nil {
- c.EC2SDConfig = &DefaultEC2SDConfig
+ ec2Config := DefaultEC2SDConfig
+ c.EC2SDConfig = &ec2Config
}
c.EC2SDConfig.HTTPClientConfig = c.HTTPClientConfig
if c.Region != "" {
@@ -133,7 +134,8 @@ func (c *SDConfig) UnmarshalYAML(unmarshal func(any) error) error {
}
case RoleECS:
if c.ECSSDConfig == nil {
- c.ECSSDConfig = &DefaultECSSDConfig
+ ecsConfig := DefaultECSSDConfig
+ c.ECSSDConfig = &ecsConfig
}
c.ECSSDConfig.HTTPClientConfig = c.HTTPClientConfig
if c.Region != "" {
@@ -165,7 +167,8 @@ func (c *SDConfig) UnmarshalYAML(unmarshal func(any) error) error {
}
case RoleLightsail:
if c.LightsailSDConfig == nil {
- c.LightsailSDConfig = &DefaultLightsailSDConfig
+ lightsailConfig := DefaultLightsailSDConfig
+ c.LightsailSDConfig = &lightsailConfig
}
c.LightsailSDConfig.HTTPClientConfig = c.HTTPClientConfig
if c.Region != "" {
diff --git a/discovery/aws/aws_test.go b/discovery/aws/aws_test.go
index a2f03a8b99..dc1f2044ec 100644
--- a/discovery/aws/aws_test.go
+++ b/discovery/aws/aws_test.go
@@ -20,7 +20,7 @@ import (
"github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
- "gopkg.in/yaml.v3"
+ "go.yaml.in/yaml/v3"
)
func TestRoleUnmarshalYAML(t *testing.T) {
@@ -177,3 +177,109 @@ port: 9300`,
})
}
}
+
+// TestMultipleSDConfigsDoNotShareState verifies that multiple AWS SD configs
+// don't share the same underlying configuration object. This was a bug where
+// all configs pointed to the same global default, causing port and other
+// settings from one job to overwrite settings in another job.
+func TestMultipleSDConfigsDoNotShareState(t *testing.T) {
+ tests := []struct {
+ name string
+ yaml string
+ validateFunc func(t *testing.T, cfg1, cfg2 *SDConfig)
+ }{
+ {
+ name: "EC2MultipleJobsDifferentPorts",
+ yaml: `
+- role: ec2
+ region: us-west-2
+ port: 9100
+ filters:
+ - name: tag:Name
+ values: [host-1]
+- role: ec2
+ region: us-west-2
+ port: 9101
+ filters:
+ - name: tag:Name
+ values: [host-2]`,
+ validateFunc: func(t *testing.T, cfg1, cfg2 *SDConfig) {
+ require.Equal(t, RoleEC2, cfg1.Role)
+ require.Equal(t, RoleEC2, cfg2.Role)
+ require.NotNil(t, cfg1.EC2SDConfig)
+ require.NotNil(t, cfg2.EC2SDConfig)
+
+ // Verify ports are different and not shared
+ require.Equal(t, 9100, cfg1.EC2SDConfig.Port)
+ require.Equal(t, 9101, cfg2.EC2SDConfig.Port)
+
+ // Verify filters are different and not shared
+ require.Len(t, cfg1.EC2SDConfig.Filters, 1)
+ require.Len(t, cfg2.EC2SDConfig.Filters, 1)
+ require.Equal(t, []string{"host-1"}, cfg1.EC2SDConfig.Filters[0].Values)
+ require.Equal(t, []string{"host-2"}, cfg2.EC2SDConfig.Filters[0].Values)
+
+ // Most importantly: verify they're not the same pointer
+ require.NotSame(t, cfg1.EC2SDConfig, cfg2.EC2SDConfig,
+ "EC2SDConfig objects should not share the same memory address")
+ },
+ },
+ {
+ name: "ECSMultipleJobsDifferentPorts",
+ yaml: `
+- role: ecs
+ region: us-east-1
+ port: 8080
+ clusters: [cluster-a]
+- role: ecs
+ region: us-east-1
+ port: 8081
+ clusters: [cluster-b]`,
+ validateFunc: func(t *testing.T, cfg1, cfg2 *SDConfig) {
+ require.Equal(t, RoleECS, cfg1.Role)
+ require.Equal(t, RoleECS, cfg2.Role)
+ require.NotNil(t, cfg1.ECSSDConfig)
+ require.NotNil(t, cfg2.ECSSDConfig)
+
+ require.Equal(t, 8080, cfg1.ECSSDConfig.Port)
+ require.Equal(t, 8081, cfg2.ECSSDConfig.Port)
+ require.Equal(t, []string{"cluster-a"}, cfg1.ECSSDConfig.Clusters)
+ require.Equal(t, []string{"cluster-b"}, cfg2.ECSSDConfig.Clusters)
+
+ require.NotSame(t, cfg1.ECSSDConfig, cfg2.ECSSDConfig,
+ "ECSSDConfig objects should not share the same memory address")
+ },
+ },
+ {
+ name: "LightsailMultipleJobsDifferentPorts",
+ yaml: `
+- role: lightsail
+ region: eu-west-1
+ port: 7070
+- role: lightsail
+ region: eu-west-1
+ port: 7071`,
+ validateFunc: func(t *testing.T, cfg1, cfg2 *SDConfig) {
+ require.Equal(t, RoleLightsail, cfg1.Role)
+ require.Equal(t, RoleLightsail, cfg2.Role)
+ require.NotNil(t, cfg1.LightsailSDConfig)
+ require.NotNil(t, cfg2.LightsailSDConfig)
+
+ require.Equal(t, 7070, cfg1.LightsailSDConfig.Port)
+ require.Equal(t, 7071, cfg2.LightsailSDConfig.Port)
+
+ require.NotSame(t, cfg1.LightsailSDConfig, cfg2.LightsailSDConfig,
+ "LightsailSDConfig objects should not share the same memory address")
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ var configs []SDConfig
+ require.NoError(t, yaml.Unmarshal([]byte(tt.yaml), &configs))
+ require.Len(t, configs, 2)
+ tt.validateFunc(t, &configs[0], &configs[1])
+ })
+ }
+}
diff --git a/docs/command-line/prometheus.md b/docs/command-line/prometheus.md
index d4a8cd4f20..251fdfd6a4 100644
--- a/docs/command-line/prometheus.md
+++ b/docs/command-line/prometheus.md
@@ -59,7 +59,7 @@ The Prometheus monitoring server
| --query.timeout | Maximum time a query may take before being aborted. Use with server mode only. | `2m` |
| --query.max-concurrency | Maximum number of queries executed concurrently. Use with server mode only. | `20` |
| --query.max-samples | Maximum number of samples a single query can load into memory. Note that queries will fail if they try to load more samples than this into memory, so this also limits the number of samples a query can return. Use with server mode only. | `50000000` |
-| --enable-feature ... | Comma separated feature names to enable. Valid options: exemplar-storage, expand-external-labels, memory-snapshot-on-shutdown, promql-per-step-stats, promql-experimental-functions, extra-scrape-metrics, auto-gomaxprocs, created-timestamp-zero-ingestion, concurrent-rule-eval, delayed-compaction, old-ui, otlp-deltatocumulative, promql-duration-expr, use-uncached-io, promql-extended-range-selectors. See https://prometheus.io/docs/prometheus/latest/feature_flags/ for more details. | |
+| --enable-feature ... | Comma separated feature names to enable. Valid options: exemplar-storage, expand-external-labels, memory-snapshot-on-shutdown, promql-per-step-stats, promql-experimental-functions, extra-scrape-metrics, auto-gomaxprocs, created-timestamp-zero-ingestion, concurrent-rule-eval, delayed-compaction, old-ui, otlp-deltatocumulative, promql-duration-expr, use-uncached-io, promql-extended-range-selectors, promql-binop-fill-modifiers. See https://prometheus.io/docs/prometheus/latest/feature_flags/ for more details. | |
| --agent | Run Prometheus in 'Agent mode'. | |
| --log.level | Only log messages with the given severity or above. One of: [debug, info, warn, error] | `info` |
| --log.format | Output format of log messages. One of: [logfmt, json] | `logfmt` |
diff --git a/docs/feature_flags.md b/docs/feature_flags.md
index af08eebb45..247941c5ce 100644
--- a/docs/feature_flags.md
+++ b/docs/feature_flags.md
@@ -67,12 +67,12 @@ Currently, Prometheus supports start timestamps on the
* `PrometheusProto`
* `OpenMetrics1.0.0`
-
+
From the above, Prometheus recommends `PrometheusProto`. This is because OpenMetrics 1.0 Start Timestamp information is shared as a `_created` metric and parsing those
are prone to errors and expensive (thus, adding an overhead). You also need to be careful to not pollute your Prometheus with extra `_created` metrics.
-
-Therefore, when `created-timestamp-zero-ingestion` is enabled Prometheus changes the global `scrape_protocols` default configuration option to
+
+Therefore, when `created-timestamp-zero-ingestion` is enabled Prometheus changes the global `scrape_protocols` default configuration option to
`[ PrometheusProto, OpenMetricsText1.0.0, OpenMetricsText0.0.1, PrometheusText0.0.4 ]`, resulting in negotiating the Prometheus Protobuf protocol first (unless the `scrape_protocols` option is set to a different value explicitly).
Besides enabling this feature in Prometheus, start timestamps need to be exposed by the application being scraped.
@@ -288,8 +288,8 @@ when wrong types are used on wrong functions, automatic renames, delta types and
### Behavior with metadata records
-When this feature is enabled and the metadata WAL records exists, in an unlikely situation when type or unit are different across those,
-the Prometheus outputs intends to prefer the `__type__` and `__unit__` labels values. For example on Remote Write 2.0,
+When this feature is enabled and the metadata WAL records exists, in an unlikely situation when type or unit are different across those,
+the Prometheus outputs intends to prefer the `__type__` and `__unit__` labels values. For example on Remote Write 2.0,
if the metadata record somehow (e.g. due to bug) says "counter", but `__type__="gauge"` the remote time series will be set to a gauge.
## Use Uncached IO
@@ -338,9 +338,25 @@ Example query:
> **Note for alerting and recording rules:**
> The `smoothed` modifier requires samples after the evaluation interval, so using it directly in alerting or recording rules will typically *under-estimate* the result, as future samples are not available at evaluation time.
-> To use `smoothed` safely in rules, you **must** apply a `query_offset` to the rule group (see [documentation](https://prometheus.io/docs/prometheus/latest/configuration/recording_rules/#rule_group)) to ensure the calculation window is fully in the past and all needed samples are available.
+> To use `smoothed` safely in rules, you **must** apply a `query_offset` to the rule group (see [documentation](https://prometheus.io/docs/prometheus/latest/configuration/recording_rules/#rule_group)) to ensure the calculation window is fully in the past and all needed samples are available.
> For critical alerting, set the offset to at least one scrape interval; for less critical or more resilient use cases, consider a larger offset (multiple scrape intervals) to tolerate missed scrapes.
For more details, see the [design doc](https://github.com/prometheus/proposals/blob/main/proposals/2025-04-04_extended-range-selectors-semantics.md).
**Note**: Extended Range Selectors are not supported for subqueries.
+
+## Binary operator fill modifiers
+
+`--enable-feature=promql-binop-fill-modifiers`
+
+Enables experimental `fill()`, `fill_left()`, and `fill_right()` modifiers for PromQL binary operators. These modifiers allow filling in missing matches on either side of a binary operation with a provided default sample value.
+
+Example query:
+
+```
+ rate(successful_requests[5m])
++ fill(0)
+ rate(failed_requests[5m])
+```
+
+See [the fill modifiers documentation](querying/operators.md#filling-in-missing-matches) for more details and examples.
diff --git a/docs/querying/operators.md b/docs/querying/operators.md
index b320d8e86e..b15c02aedc 100644
--- a/docs/querying/operators.md
+++ b/docs/querying/operators.md
@@ -47,9 +47,9 @@ special values like `NaN`, `+Inf`, and `-Inf`.
scalar that is the result of the operator applied to both scalar operands.
**Between an instant vector and a scalar**, the operator is applied to the
-value of every data sample in the vector.
+value of every data sample in the vector.
-If the data sample is a float, the operation is performed between that float and the scalar.
+If the data sample is a float, the operation is performed between that float and the scalar.
For example, if an instant vector of float samples is multiplied by 2,
the result is another vector of float samples in which every sample value of
the original vector is multiplied by 2.
@@ -81,8 +81,9 @@ following:
**Between two instant vectors**, a binary arithmetic operator is applied to
each entry in the LHS vector and its [matching element](#vector-matching) in
the RHS vector. The result is propagated into the result vector with the
-grouping labels becoming the output label set. Entries for which no matching
-entry in the right-hand vector can be found are not part of the result.
+grouping labels becoming the output label set. By default, series for which
+no matching entry in the opposite vector can be found are not part of the
+result. This behavior can be adjusted using [fill modifiers](#filling-in-missing-matches).
If two float samples are matched, the arithmetic operator is applied to the two
input values.
@@ -97,7 +98,7 @@ If two histogram samples are matched, only `+` and `-` are valid operations,
each adding or subtracting all matching bucket populations and the count and
the sum of observations. All other operations result in the removal of the
corresponding element from the output vector, flagged by an info-level
-annotation. The `+` and -` operations should generally only be applied to gauge
+annotation. The `+` and `-` operations should generally only be applied to gauge
histograms, but PromQL allows them for counter histograms, too, to cover
specific use cases, for which special attention is required to avoid problems
with unaligned counter resets. (Certain incompatibilities of counter resets can
@@ -106,7 +107,7 @@ two counter histograms results in a counter histogram. All other combination of
operands and all subtractions result in a gauge histogram.
**In any arithmetic binary operation involving vectors**, the metric name is
-dropped. This occurs even if `__name__` is explicitly mentioned in `on`
+dropped. This occurs even if `__name__` is explicitly mentioned in `on`
(see https://github.com/prometheus/prometheus/issues/16631 for further discussion).
**For any arithmetic binary operation that may result in a negative
@@ -156,9 +157,9 @@ info-level annotation.
applied to matching entries. Vector elements for which the expression is not
true or which do not find a match on the other side of the expression get
dropped from the result, while the others are propagated into a result vector
-with the grouping labels becoming the output label set.
+with the grouping labels becoming the output label set.
-Matches between two float samples work as usual.
+Matches between two float samples work as usual.
Matches between a float sample and a histogram sample are invalid, and the
corresponding element is removed from the result vector, flagged by an info-level
@@ -171,8 +172,8 @@ comparison binary operations are again invalid.
modifier changes the behavior in the following ways:
* Vector elements which find a match on the other side of the expression but for
- which the expression is false instead have the value `0` and vector elements
- that do find a match and for which the expression is true have the value `1`.
+ which the expression is false instead have the value `0`, and vector elements
+ that do find a match and for which the expression is true have the value `1`.
(Note that elements with no match or invalid operations involving histogram
samples still return no result rather than the value `0`.)
* The metric name is dropped.
@@ -216,11 +217,10 @@ matching behavior: One-to-one and many-to-one/one-to-many.
### Vector matching keywords
-These vector matching keywords allow for matching between series with different label sets
-providing:
+These vector matching keywords allow for matching between series with different label sets:
-* `on`
-* `ignoring`
+* `on()`: Only match on provided labels.
+* `ignoring()`: Ignore provided labels when matching.
Label lists provided to matching keywords will determine how vectors are combined. Examples
can be found in [One-to-one vector matches](#one-to-one-vector-matches) and in
@@ -230,8 +230,8 @@ can be found in [One-to-one vector matches](#one-to-one-vector-matches) and in
These group modifiers enable many-to-one/one-to-many vector matching:
-* `group_left`
-* `group_right`
+* `group_left`: Allow many-to-one matching, where the left vector has higher cardinality.
+* `group_right`: Allow one-to-many matching, where the right vector has higher cardinality.
Label lists can be provided to the group modifier which contain labels from the "one"-side to
be included in the result metrics.
@@ -239,11 +239,9 @@ be included in the result metrics.
_Many-to-one and one-to-many matching are advanced use cases that should be carefully considered.
Often a proper use of `ignoring()` provides the desired outcome._
-_Grouping modifiers can only be used for
-[comparison](#comparison-binary-operators) and
-[arithmetic](#arithmetic-binary-operators). Operations as `and`, `unless` and
-`or` operations match with all possible entries in the right vector by
-default._
+_Grouping modifiers can only be used for [comparison](#comparison-binary-operators),
+[arithmetic](#arithmetic-binary-operators), and [trigonometric](#trigonometric-binary-operators)
+operators. Set operators match with all possible entries on either side by default._
### One-to-one vector matches
@@ -311,6 +309,58 @@ left:
{method="post", code="500"} 0.05 // 6 / 120
{method="post", code="404"} 0.175 // 21 / 120
+### Filling in missing matches
+
+Fill modifiers are **experimental** and must be enabled with `--enable-feature=promql-binop-fill-modifiers`.
+
+By default, vector elements that do not find a match on the other side of a binary operation
+are not included in the result vector. Fill modifiers allow overriding this behavior by filling
+in missing series on either side of a binary operation with a provided default sample value:
+
+* `fill()`: Fill in missing matches on either side with `value`.
+* `fill_left()`: Fill in missing matches on the left side with `value`.
+* `fill_right()`: Fill in missing matches on the right side with `value`.
+
+`value` has to be a numeric literal representing a float sample. Histogram samples are not supported.
+
+Note that these modifiers can only fill in series that are missing on one side of the operation.
+If a series is missing on both sides, it cannot be created by these modifiers.
+
+The fill modifiers can be used in the following combinations:
+
+* `fill()`
+* `fill_left()`
+* `fill_right()`
+* `fill_left() fill_right()`
+* `fill_right() fill_left()`
+
+If other binary operator modifiers like `bool`, `on`, `ignoring`, `group_left`, or `group_right`
+are used, the fill modifiers must be provided last.
+
+When using fill modifiers in combination with `group_left` or `group_right`, they behave as follows:
+
+* If a fill modifier is used on the "many" side of a match, it will only fill in a single series
+ for the "many" side of each match group, using the group's matching labels as the series identity.
+* If a fill modifier is used on the "one" side of a match and the grouping modifier specifies
+ label names to include from the "one" side (e.g. `left_vector * on(instance, job) group_left(info_label) fill_right(1) right_vector`), those labels will not be filled in for missing
+ series, as there is no source for their values.
+
+Fill modifiers are not supported for set operators (`and`, `or`, `unless`), as the purpose of those
+operators is to filter series based on presence or absence in the other vector.
+
+Example query, filling in missing series on the either side with `0`:
+
+ method_code:http_errors:rate5m{status="500"} / ignoring(code) fill(0) method:http_requests:rate5m
+
+This returns a result vector containing the fraction of HTTP requests with status code
+of 500 for each method, as measured over the last 5 minutes. The entries with methods `put` and `del`
+are now included in the result with a filled-in default sample value of `0`, as they had no matching
+series on the respective other side:
+
+ {method="get"} 0.04 # 24 / 600
+ {method="put"} +Inf # 3 / 0 (missing right side filled in)
+ {method="del"} 0 # 0 / 34 (missing left side filled in)
+ {method="post"} 0.05 # 6 / 120
## Aggregation operators
@@ -357,7 +407,7 @@ identical between all elements of the vector.
#### `sum`
`sum(v)` sums up sample values in `v` in the same way as the `+` binary operator does
-between two values.
+between two values.
All sample values being aggregated into a single resulting vector element must either be
float samples or histogram samples. An aggregation of a mix of both is invalid,
@@ -393,7 +443,7 @@ vector, flagged by a warn-level annotation.
#### `min` and `max`
-`min(v)` and `max(v)` return the minimum or maximum value, respectively, in `v`.
+`min(v)` and `max(v)` return the minimum or maximum value, respectively, in `v`.
They only operate on float samples, following IEEE 754 floating
point arithmetic, which in particular implies that `NaN` is only ever
@@ -403,9 +453,9 @@ samples in the input vector are ignored, flagged by an info-level annotation.
#### `topk` and `bottomk`
`topk(k, v)` and `bottomk(k, v)` are different from other aggregators in that a subset of
-`k` values from the input samples, including the original labels, are returned in the result vector.
+`k` values from the input samples, including the original labels, are returned in the result vector.
-`by` and `without` are only used to bucket the input vector.
+`by` and `without` are only used to bucket the input vector.
Similar to `min` and `max`, they only operate on float samples, considering `NaN` values
to be farthest from the top or bottom, respectively. Histogram samples in the
@@ -415,7 +465,7 @@ If used in an instant query, `topk` and `bottomk` return series ordered by
value in descending or ascending order, respectively. If used with `by` or
`without`, then series within each bucket are sorted by value, and series in
the same bucket are returned consecutively, but there is no guarantee that
-buckets of series will be returned in any particular order.
+buckets of series will be returned in any particular order.
No sorting applies to range queries.
@@ -428,11 +478,11 @@ To get the 5 instances with the highest memory consumption across all instances
#### `limitk`
`limitk(k, v)` returns a subset of `k` input samples, including
-the original labels in the result vector.
+the original labels in the result vector.
The subset is selected in a deterministic pseudo-random way.
-This happens independent of the sample type.
-Therefore, it works for both float samples and histogram samples.
+This happens independent of the sample type.
+Therefore, it works for both float samples and histogram samples.
##### Example
@@ -470,8 +520,8 @@ The value may be a float or histogram sample.
#### `count_values`
-`count_values(l, v)` outputs one time series per unique sample value in `v`.
-Each series has an additional label, given by `l`, and the label value is the
+`count_values(l, v)` outputs one time series per unique sample value in `v`.
+Each series has an additional label, given by `l`, and the label value is the
unique sample value. The value of each time series is the number of times that sample value was present.
`count_values` works with both float samples and histogram samples. For the
@@ -486,7 +536,7 @@ To count the number of binaries running each build version we could write:
#### `stddev`
-`stddev(v)` returns the standard deviation of `v`.
+`stddev(v)` returns the standard deviation of `v`.
`stddev` only works with float samples, following IEEE 754 floating
point arithmetic. Histogram samples in the input vector are ignored, flagged by
@@ -494,7 +544,7 @@ an info-level annotation.
#### `stdvar`
-`stdvar(v)` returns the standard deviation of `v`.
+`stdvar(v)` returns the standard deviation of `v`.
`stdvar` only works with float samples, following IEEE 754 floating
point arithmetic. Histogram samples in the input vector are ignored, flagged by
@@ -510,12 +560,12 @@ are ignored, flagged by an info-level annotation.
`NaN` is considered the smallest possible value.
-For example, `quantile(0.5, ...)` calculates the median, `quantile(0.95, ...)` the 95th percentile.
+For example, `quantile(0.5, ...)` calculates the median, `quantile(0.95, ...)` the 95th percentile.
Special cases:
* For φ = `NaN`, `NaN` is returned.
-* For φ < 0, `-Inf` is returned.
+* For φ < 0, `-Inf` is returned.
* For φ > 1, `+Inf` is returned.
## Binary operator precedence
diff --git a/documentation/examples/remote_storage/go.mod b/documentation/examples/remote_storage/go.mod
index b77f248bf5..17076faddd 100644
--- a/documentation/examples/remote_storage/go.mod
+++ b/documentation/examples/remote_storage/go.mod
@@ -1,6 +1,6 @@
module github.com/prometheus/prometheus/documentation/examples/remote_storage
-go 1.24.9
+go 1.24.0
require (
github.com/alecthomas/kingpin/v2 v2.4.0
diff --git a/go.mod b/go.mod
index 61c555abc2..afc3f2740d 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
module github.com/prometheus/prometheus
-go 1.24.9
+go 1.24.0
require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0
@@ -34,7 +34,7 @@ require (
github.com/gogo/protobuf v1.3.2
github.com/golang/snappy v1.0.0
github.com/google/go-cmp v0.7.0
- github.com/google/pprof v0.0.0-20251213031049-b05bdaca462f
+ github.com/google/pprof v0.0.0-20260111202518-71be6bfdd440
github.com/google/uuid v1.6.0
github.com/gophercloud/gophercloud/v2 v2.9.0
github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853
@@ -71,7 +71,6 @@ require (
go.opentelemetry.io/collector/consumer v1.48.0
go.opentelemetry.io/collector/pdata v1.48.0
go.opentelemetry.io/collector/processor v1.48.0
- go.opentelemetry.io/collector/semconv v0.128.0
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.64.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0
go.opentelemetry.io/otel v1.39.0
@@ -84,8 +83,8 @@ require (
go.uber.org/atomic v1.11.0
go.uber.org/automaxprocs v1.6.0
go.uber.org/goleak v1.3.0
- go.uber.org/multierr v1.11.0
go.yaml.in/yaml/v2 v2.4.3
+ go.yaml.in/yaml/v3 v3.0.4
golang.org/x/oauth2 v0.34.0
golang.org/x/sync v0.19.0
golang.org/x/sys v0.39.0
@@ -94,7 +93,6 @@ require (
google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b
google.golang.org/grpc v1.78.0
google.golang.org/protobuf v1.36.11
- gopkg.in/yaml.v3 v3.0.1
k8s.io/api v0.34.3
k8s.io/apimachinery v0.34.3
k8s.io/client-go v0.34.3
@@ -115,7 +113,8 @@ require (
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
- go.yaml.in/yaml/v3 v3.0.4 // indirect
+ go.uber.org/multierr v1.11.0 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
)
diff --git a/go.sum b/go.sum
index b3333208dd..6ac2105275 100644
--- a/go.sum
+++ b/go.sum
@@ -236,8 +236,8 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX
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/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-20260111202518-71be6bfdd440 h1:oKBqR+eQXiIM7X8K1JEg9aoTEePLq/c6Awe484abOuA=
+github.com/google/pprof v0.0.0-20260111202518-71be6bfdd440/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
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=
@@ -571,8 +571,6 @@ go.opentelemetry.io/collector/processor/processortest v0.142.0 h1:wQnJeXDejBL6r8
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/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.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=
diff --git a/go.work b/go.work
index 5ec4aeab50..fbb73655e9 100644
--- a/go.work
+++ b/go.work
@@ -1,4 +1,4 @@
-go 1.24.9
+go 1.24.0
use (
.
diff --git a/internal/tools/go.mod b/internal/tools/go.mod
index a7a1ebec54..c8b62b5ca7 100644
--- a/internal/tools/go.mod
+++ b/internal/tools/go.mod
@@ -1,6 +1,6 @@
module github.com/prometheus/prometheus/internal/tools
-go 1.24.9
+go 1.24.0
require (
github.com/bufbuild/buf v1.62.1
diff --git a/model/labels/regexp.go b/model/labels/regexp.go
index a4bdf885ee..5f4f753419 100644
--- a/model/labels/regexp.go
+++ b/model/labels/regexp.go
@@ -71,6 +71,10 @@ func NewFastRegexMatcher(v string) (*FastRegexMatcher, error) {
if err != nil {
return nil, err
}
+
+ // Remove any capture operations before trying to optimize the remaining operations.
+ clearCapture(parsed)
+
if parsed.Op == syntax.OpConcat {
m.prefix, m.suffix, m.contains = optimizeConcatRegex(parsed)
}
diff --git a/model/labels/regexp_test.go b/model/labels/regexp_test.go
index 85cbe02a1f..d4385c7481 100644
--- a/model/labels/regexp_test.go
+++ b/model/labels/regexp_test.go
@@ -93,6 +93,7 @@ var (
"(.+)-(.+)-(.+)-(.+)-(.+)",
"((.*))(?i:f)((.*))o((.*))o((.*))",
"((.*))f((.*))(?i:o)((.*))o((.*))",
+ "(.*0.*)",
}
values = []string{
"foo", " foo bar", "bar", "buzz\nbar", "bar foo", "bfoo", "\n", "\nfoo", "foo\n", "hello foo world", "hello foo\n world", "",
diff --git a/model/rulefmt/rulefmt.go b/model/rulefmt/rulefmt.go
index 70541eb0d3..2cbfdf4cfc 100644
--- a/model/rulefmt/rulefmt.go
+++ b/model/rulefmt/rulefmt.go
@@ -24,7 +24,7 @@ import (
"time"
"github.com/prometheus/common/model"
- "gopkg.in/yaml.v3"
+ "go.yaml.in/yaml/v3"
"github.com/prometheus/prometheus/model/timestamp"
"github.com/prometheus/prometheus/promql"
diff --git a/model/rulefmt/rulefmt_test.go b/model/rulefmt/rulefmt_test.go
index ec16052bc0..ea8d09af0d 100644
--- a/model/rulefmt/rulefmt_test.go
+++ b/model/rulefmt/rulefmt_test.go
@@ -21,7 +21,7 @@ import (
"github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
- "gopkg.in/yaml.v3"
+ "go.yaml.in/yaml/v3"
)
func TestParseFileSuccess(t *testing.T) {
diff --git a/notifier/alertmanager_test.go b/notifier/alertmanager_test.go
index 668271d267..ca2bd2f771 100644
--- a/notifier/alertmanager_test.go
+++ b/notifier/alertmanager_test.go
@@ -14,11 +14,15 @@
package notifier
import (
+ "log/slog"
"testing"
+ "github.com/prometheus/client_golang/prometheus"
+ "github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
"github.com/prometheus/prometheus/config"
+ "github.com/prometheus/prometheus/discovery/targetgroup"
)
func TestPostPath(t *testing.T) {
@@ -60,3 +64,89 @@ func TestLabelSetNotReused(t *testing.T) {
// Target modified during alertmanager extraction
require.Equal(t, tg, makeInputTargetGroup())
}
+
+// TestAlertmanagerSetSync verifies that sync properly manages sendloop lifecycle:
+// - Starts sendloops for new alertmanagers.
+// - Stops sendloops for removed alertmanagers.
+// - Does NOT stop sendloops that are still in use.
+// - Does NOT stop sendloops that were just created.
+func TestAlertmanagerSetSync(t *testing.T) {
+ reg := prometheus.NewRegistry()
+ alertmanagersDiscoveredFunc := func() float64 { return 0 }
+ metrics := newAlertMetrics(reg, alertmanagersDiscoveredFunc)
+ logger := slog.New(slog.DiscardHandler)
+ opts := &Options{QueueCapacity: 10, MaxBatchSize: DefaultMaxBatchSize}
+
+ cfg := config.DefaultAlertmanagerConfig
+
+ // Create alertmanagerSet
+ ams, err := newAlertmanagerSet(&cfg, opts, logger, metrics)
+ require.NoError(t, err)
+
+ defer func() {
+ ams.sync([]*targetgroup.Group{})
+ require.Empty(t, ams.sendLoops, "All sendloops should be cleaned up")
+ }()
+
+ // First sync: Add AM1 and AM2
+ tgs1 := []*targetgroup.Group{
+ {
+ Targets: []model.LabelSet{
+ {model.AddressLabel: "am1.example.com:9093"},
+ {model.AddressLabel: "am2.example.com:9093"},
+ },
+ },
+ }
+
+ ams.sync(tgs1)
+
+ require.Len(t, ams.sendLoops, 2, "AM1 and AM2 sendloops should be created")
+ require.Contains(t, ams.sendLoops, "http://am1.example.com:9093/api/v2/alerts", "AM1 sendloop should be created")
+ require.Contains(t, ams.sendLoops, "http://am2.example.com:9093/api/v2/alerts", "AM2 sendloop should be created")
+
+ am1Loop := ams.sendLoops["http://am1.example.com:9093/api/v2/alerts"]
+ am2Loop := ams.sendLoops["http://am2.example.com:9093/api/v2/alerts"]
+ require.NotNil(t, am1Loop)
+ require.NotNil(t, am2Loop)
+
+ // Second sync: Keep AM2, remove AM1, add AM3
+ tgs2 := []*targetgroup.Group{
+ {
+ Targets: []model.LabelSet{
+ {model.AddressLabel: "am2.example.com:9093"},
+ {model.AddressLabel: "am3.example.com:9093"},
+ },
+ },
+ }
+
+ ams.sync(tgs2)
+
+ require.Len(t, ams.sendLoops, 2)
+ require.NotContains(t, ams.sendLoops, "http://am1.example.com:9093/api/v2/alerts", "AM1 sendloop should be removed")
+ require.Contains(t, ams.sendLoops, "http://am2.example.com:9093/api/v2/alerts", "AM2 sendloop should be kept")
+ require.Contains(t, ams.sendLoops, "http://am3.example.com:9093/api/v2/alerts", "AM3 sendloop should be created")
+
+ am2LoopAfter := ams.sendLoops["http://am2.example.com:9093/api/v2/alerts"]
+ require.Same(t, am2Loop, am2LoopAfter, "AM2 sendloop should not be recreated")
+
+ am3Loop := ams.sendLoops["http://am3.example.com:9093/api/v2/alerts"]
+ require.NotNil(t, am3Loop, "AM3 sendloop should be created")
+
+ // Third sync: Keep only AM3, remove AM2
+ tgs3 := []*targetgroup.Group{
+ {
+ Targets: []model.LabelSet{
+ {model.AddressLabel: "am3.example.com:9093"},
+ },
+ },
+ }
+
+ ams.sync(tgs3)
+
+ require.Len(t, ams.sendLoops, 1)
+ require.NotContains(t, ams.sendLoops, "http://am2.example.com:9093/api/v2/alerts", "AM2 sendloop should be removed")
+ require.Contains(t, ams.sendLoops, "http://am3.example.com:9093/api/v2/alerts", "AM3 sendloop should be kept")
+
+ am3LoopAfter := ams.sendLoops["http://am3.example.com:9093/api/v2/alerts"]
+ require.Same(t, am3Loop, am3LoopAfter, "AM3 sendloop should not be recreated")
+}
diff --git a/notifier/alertmanagerset.go b/notifier/alertmanagerset.go
index eca798e6f5..81565b5cf8 100644
--- a/notifier/alertmanagerset.go
+++ b/notifier/alertmanagerset.go
@@ -16,6 +16,7 @@ package notifier
import (
"crypto/md5"
"encoding/hex"
+ "fmt"
"log/slog"
"net/http"
"sync"
@@ -26,6 +27,7 @@ import (
"github.com/prometheus/prometheus/config"
"github.com/prometheus/prometheus/discovery/targetgroup"
+ "github.com/prometheus/prometheus/model/labels"
)
// alertmanagerSet contains a set of Alertmanagers discovered via a group of service
@@ -33,16 +35,19 @@ import (
type alertmanagerSet struct {
cfg *config.AlertmanagerConfig
client *http.Client
+ opts *Options
metrics *alertMetrics
mtx sync.RWMutex
ams []alertmanager
droppedAms []alertmanager
- logger *slog.Logger
+ sendLoops map[string]*sendLoop
+
+ logger *slog.Logger
}
-func newAlertmanagerSet(cfg *config.AlertmanagerConfig, logger *slog.Logger, metrics *alertMetrics) (*alertmanagerSet, error) {
+func newAlertmanagerSet(cfg *config.AlertmanagerConfig, opts *Options, logger *slog.Logger, metrics *alertMetrics) (*alertmanagerSet, error) {
client, err := config_util.NewClientFromConfig(cfg.HTTPClientConfig, "alertmanager")
if err != nil {
return nil, err
@@ -59,10 +64,12 @@ func newAlertmanagerSet(cfg *config.AlertmanagerConfig, logger *slog.Logger, met
client.Transport = t
s := &alertmanagerSet{
- client: client,
- cfg: cfg,
- logger: logger,
- metrics: metrics,
+ client: client,
+ cfg: cfg,
+ opts: opts,
+ sendLoops: make(map[string]*sendLoop),
+ logger: logger,
+ metrics: metrics,
}
return s, nil
}
@@ -86,36 +93,32 @@ func (s *alertmanagerSet) sync(tgs []*targetgroup.Group) {
s.mtx.Lock()
defer s.mtx.Unlock()
previousAms := s.ams
- // Set new Alertmanagers and deduplicate them along their unique URL.
s.ams = []alertmanager{}
s.droppedAms = []alertmanager{}
s.droppedAms = append(s.droppedAms, allDroppedAms...)
- seen := map[string]struct{}{}
+ // Deduplicate Alertmanagers and add sendloops for new Alertmanagers.
+ seen := map[string]struct{}{}
for _, am := range allAms {
us := am.url().String()
if _, ok := seen[us]; ok {
continue
}
- // This will initialize the Counters for the AM to 0.
- s.metrics.sent.WithLabelValues(us)
- s.metrics.errors.WithLabelValues(us)
-
seen[us] = struct{}{}
s.ams = append(s.ams, am)
}
- // Now remove counters for any removed Alertmanagers.
+ s.addSendLoops(s.ams)
+
+ // Populate a list of Alertmanagers to clean up,
+ // avoid cleaning up what we just added.
for _, am := range previousAms {
us := am.url().String()
if _, ok := seen[us]; ok {
continue
}
- s.metrics.latencySummary.DeleteLabelValues(us)
- s.metrics.latencyHistogram.DeleteLabelValues(us)
- s.metrics.sent.DeleteLabelValues(us)
- s.metrics.errors.DeleteLabelValues(us)
seen[us] = struct{}{}
+ s.cleanSendLoops(am)
}
}
@@ -127,3 +130,62 @@ func (s *alertmanagerSet) configHash() (string, error) {
hash := md5.Sum(b)
return hex.EncodeToString(hash[:]), nil
}
+
+func (s *alertmanagerSet) send(alerts ...*Alert) {
+ s.mtx.Lock()
+ defer s.mtx.Unlock()
+
+ if len(s.cfg.AlertRelabelConfigs) > 0 {
+ alerts = relabelAlerts(s.cfg.AlertRelabelConfigs, labels.Labels{}, alerts)
+ if len(alerts) == 0 {
+ return
+ }
+ }
+
+ for _, sendLoop := range s.sendLoops {
+ sendLoop.add(alerts...)
+ }
+}
+
+// addSendLoops creates and starts a send loop for newly discovered alertmanager.
+// This function expects the caller to acquire needed locks.
+func (s *alertmanagerSet) addSendLoops(ams []alertmanager) {
+ for _, am := range ams {
+ us := am.url().String()
+ // Only add if sendloop doesn't already exist
+ if loop, exists := s.sendLoops[us]; exists {
+ loop.logger.Debug("Alertmanager already has send loop running, skipping")
+ continue
+ }
+ sendLoop := newSendLoop(us, s.client, s.cfg, s.opts, s.logger.With("alertmanager", us), s.metrics)
+ go sendLoop.loop()
+ s.sendLoops[us] = sendLoop
+ }
+}
+
+// cleanSendLoops stops and cleans the send loops for each removed alertmanager.
+// This function expects the caller to acquire needed locks.
+func (s *alertmanagerSet) cleanSendLoops(ams ...alertmanager) {
+ for _, am := range ams {
+ us := am.url().String()
+ if sendLoop, ok := s.sendLoops[us]; ok {
+ sendLoop.stop()
+ delete(s.sendLoops, us)
+ }
+ }
+}
+
+// startSendLoops starts a send loop for newly discovered alertmanager.
+// This function expects the caller to acquire needed locks.
+// This is mainly needed for testing where the loops are added as part of the test setup.
+func (s *alertmanagerSet) startSendLoops(ams []alertmanager) {
+ for _, am := range ams {
+ us := am.url().String()
+
+ if l, ok := s.sendLoops[us]; ok {
+ go l.loop()
+ continue
+ }
+ panic(fmt.Sprintf("send loop not found for %s", us))
+ }
+}
diff --git a/notifier/manager.go b/notifier/manager.go
index a835cccffd..7eeed79b79 100644
--- a/notifier/manager.go
+++ b/notifier/manager.go
@@ -14,16 +14,12 @@
package notifier
import (
- "bytes"
"context"
- "encoding/json"
"fmt"
- "io"
"log/slog"
"net/http"
"net/url"
"sync"
- "time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/model"
@@ -55,13 +51,11 @@ var userAgent = version.PrometheusUserAgent()
// Manager is responsible for dispatching alert notifications to an
// alert manager service.
type Manager struct {
- queue []*Alert
- opts *Options
+ opts *Options
metrics *alertMetrics
- more chan struct{}
- mtx sync.RWMutex
+ mtx sync.RWMutex
stopOnce *sync.Once
stopRequested chan struct{}
@@ -114,23 +108,16 @@ func NewManager(o *Options, nameValidationScheme model.ValidationScheme, logger
}
n := &Manager{
- queue: make([]*Alert, 0, o.QueueCapacity),
- more: make(chan struct{}, 1),
stopRequested: make(chan struct{}),
stopOnce: &sync.Once{},
opts: o,
logger: logger,
}
- queueLenFunc := func() float64 { return float64(n.queueLen()) }
alertmanagersDiscoveredFunc := func() float64 { return float64(len(n.Alertmanagers())) }
- n.metrics = newAlertMetrics(
- o.Registerer,
- o.QueueCapacity,
- queueLenFunc,
- alertmanagersDiscoveredFunc,
- )
+ n.metrics = newAlertMetrics(o.Registerer, alertmanagersDiscoveredFunc)
+ n.metrics.queueCapacity.Set(float64(o.QueueCapacity))
return n
}
@@ -163,7 +150,7 @@ func (n *Manager) ApplyConfig(conf *config.Config) error {
}
for k, cfg := range conf.AlertingConfig.AlertmanagerConfigs.ToMap() {
- ams, err := newAlertmanagerSet(cfg, n.logger, n.metrics)
+ ams, err := newAlertmanagerSet(cfg, n.opts, n.logger, n.metrics)
if err != nil {
return err
}
@@ -176,86 +163,54 @@ func (n *Manager) ApplyConfig(conf *config.Config) error {
if oldAmSet, ok := configToAlertmanagers[hash]; ok {
ams.ams = oldAmSet.ams
ams.droppedAms = oldAmSet.droppedAms
+ // Only transfer sendLoops to the first new config with this hash.
+ // Subsequent configs with the same hash should not share the sendLoops
+ // map reference, as that would cause shared mutable state between
+ // alertmanagerSets (cleanup in one would affect the other).
+ oldAmSet.mtx.Lock()
+ if oldAmSet.sendLoops != nil {
+ ams.mtx.Lock()
+ ams.sendLoops = oldAmSet.sendLoops
+ oldAmSet.sendLoops = nil
+ ams.mtx.Unlock()
+ }
+ oldAmSet.mtx.Unlock()
}
amSets[k] = ams
}
+ // Clean up sendLoops that weren't transferred to new config.
+ // This happens when: (1) key was removed, or (2) key exists but hash changed.
+ // After the transfer loop above, any oldAmSet with non-nil sendLoops
+ // had its sendLoops NOT transferred (since we set it to nil on transfer).
+ for _, oldAmSet := range n.alertmanagers {
+ oldAmSet.mtx.Lock()
+ if oldAmSet.sendLoops != nil {
+ oldAmSet.cleanSendLoops(oldAmSet.ams...)
+ }
+ oldAmSet.mtx.Unlock()
+ }
+
n.alertmanagers = amSets
return nil
}
-func (n *Manager) queueLen() int {
- n.mtx.RLock()
- defer n.mtx.RUnlock()
-
- return len(n.queue)
-}
-
-func (n *Manager) nextBatch() []*Alert {
- n.mtx.Lock()
- defer n.mtx.Unlock()
-
- var alerts []*Alert
-
- if maxBatchSize := n.opts.MaxBatchSize; len(n.queue) > maxBatchSize {
- alerts = append(make([]*Alert, 0, maxBatchSize), n.queue[:maxBatchSize]...)
- n.queue = n.queue[maxBatchSize:]
- } else {
- alerts = append(make([]*Alert, 0, len(n.queue)), n.queue...)
- n.queue = n.queue[:0]
- }
-
- return alerts
-}
-
// Run dispatches notifications continuously, returning once Stop has been called and all
// pending notifications have been drained from the queue (if draining is enabled).
//
// Dispatching of notifications occurs in parallel to processing target updates to avoid one starving the other.
// Refer to https://github.com/prometheus/prometheus/issues/13676 for more details.
func (n *Manager) Run(tsets <-chan map[string][]*targetgroup.Group) {
- wg := sync.WaitGroup{}
- wg.Add(2)
+ n.targetUpdateLoop(tsets)
- go func() {
- defer wg.Done()
- n.targetUpdateLoop(tsets)
- }()
-
- go func() {
- defer wg.Done()
- n.sendLoop()
- n.drainQueue()
- }()
-
- wg.Wait()
- n.logger.Info("Notification manager stopped")
-}
-
-// sendLoop continuously consumes the notifications queue and sends alerts to
-// the configured Alertmanagers.
-func (n *Manager) sendLoop() {
- for {
- // If we've been asked to stop, that takes priority over sending any further notifications.
- select {
- case <-n.stopRequested:
- return
- default:
- select {
- case <-n.stopRequested:
- return
-
- case <-n.more:
- n.sendOneBatch()
-
- // If the queue still has items left, kick off the next iteration.
- if n.queueLen() > 0 {
- n.setMore()
- }
- }
- }
+ n.mtx.Lock()
+ defer n.mtx.Unlock()
+ for _, ams := range n.alertmanagers {
+ ams.mtx.Lock()
+ ams.cleanSendLoops(ams.ams...)
+ ams.mtx.Unlock()
}
}
@@ -280,33 +235,6 @@ func (n *Manager) targetUpdateLoop(tsets <-chan map[string][]*targetgroup.Group)
}
}
-func (n *Manager) sendOneBatch() {
- alerts := n.nextBatch()
-
- if !n.sendAll(alerts...) {
- n.metrics.dropped.Add(float64(len(alerts)))
- }
-}
-
-func (n *Manager) drainQueue() {
- if !n.opts.DrainOnShutdown {
- if n.queueLen() > 0 {
- n.logger.Warn("Draining remaining notifications on shutdown is disabled, and some notifications have been dropped", "count", n.queueLen())
- n.metrics.dropped.Add(float64(n.queueLen()))
- }
-
- return
- }
-
- n.logger.Info("Draining any remaining notifications...")
-
- for n.queueLen() > 0 {
- n.sendOneBatch()
- }
-
- n.logger.Info("Remaining notifications drained")
-}
-
func (n *Manager) reload(tgs map[string][]*targetgroup.Group) {
n.mtx.Lock()
defer n.mtx.Unlock()
@@ -324,44 +252,23 @@ func (n *Manager) reload(tgs map[string][]*targetgroup.Group) {
// Send queues the given notification requests for processing.
// Panics if called on a handler that is not running.
func (n *Manager) Send(alerts ...*Alert) {
- n.mtx.Lock()
- defer n.mtx.Unlock()
+ // If we've been asked to stop, that takes priority over accepting new alerts.
+ select {
+ case <-n.stopRequested:
+ return
+ default:
+ }
+
+ n.mtx.RLock()
+ defer n.mtx.RUnlock()
alerts = relabelAlerts(n.opts.RelabelConfigs, n.opts.ExternalLabels, alerts)
if len(alerts) == 0 {
return
}
- // Queue capacity should be significantly larger than a single alert
- // batch could be.
- if d := len(alerts) - n.opts.QueueCapacity; d > 0 {
- alerts = alerts[d:]
-
- n.logger.Warn("Alert batch larger than queue capacity, dropping alerts", "num_dropped", d)
- n.metrics.dropped.Add(float64(d))
- }
-
- // If the queue is full, remove the oldest alerts in favor
- // of newer ones.
- if d := (len(n.queue) + len(alerts)) - n.opts.QueueCapacity; d > 0 {
- n.queue = n.queue[d:]
-
- n.logger.Warn("Alert notification queue full, dropping alerts", "num_dropped", d)
- n.metrics.dropped.Add(float64(d))
- }
- n.queue = append(n.queue, alerts...)
-
- // Notify sending goroutine that there are alerts to be processed.
- n.setMore()
-}
-
-// setMore signals that the alert queue has items.
-func (n *Manager) setMore() {
- // If we cannot send on the channel, it means the signal already exists
- // and has not been consumed yet.
- select {
- case n.more <- struct{}{}:
- default:
+ for _, ams := range n.alertmanagers {
+ ams.send(alerts...)
}
}
@@ -403,158 +310,11 @@ func (n *Manager) DroppedAlertmanagers() []*url.URL {
return res
}
-// sendAll sends the alerts to all configured Alertmanagers concurrently.
-// It returns true if the alerts could be sent successfully to at least one Alertmanager.
-func (n *Manager) sendAll(alerts ...*Alert) bool {
- if len(alerts) == 0 {
- return true
- }
-
- begin := time.Now()
-
- // cachedPayload represent 'alerts' marshaled for Alertmanager API v2.
- // Marshaling happens below. Reference here is for caching between
- // for loop iterations.
- var cachedPayload []byte
-
- n.mtx.RLock()
- amSets := n.alertmanagers
- n.mtx.RUnlock()
-
- var (
- wg sync.WaitGroup
- amSetCovered sync.Map
- )
- for k, ams := range amSets {
- var (
- payload []byte
- err error
- amAlerts = alerts
- )
-
- ams.mtx.RLock()
-
- if len(ams.ams) == 0 {
- ams.mtx.RUnlock()
- continue
- }
-
- if len(ams.cfg.AlertRelabelConfigs) > 0 {
- amAlerts = relabelAlerts(ams.cfg.AlertRelabelConfigs, labels.Labels{}, alerts)
- if len(amAlerts) == 0 {
- ams.mtx.RUnlock()
- continue
- }
- // We can't use the cached values from previous iteration.
- cachedPayload = nil
- }
-
- switch ams.cfg.APIVersion {
- case config.AlertmanagerAPIVersionV2:
- {
- if cachedPayload == nil {
- openAPIAlerts := alertsToOpenAPIAlerts(amAlerts)
-
- cachedPayload, err = json.Marshal(openAPIAlerts)
- if err != nil {
- n.logger.Error("Encoding alerts for Alertmanager API v2 failed", "err", err)
- ams.mtx.RUnlock()
- return false
- }
- }
-
- payload = cachedPayload
- }
- default:
- {
- n.logger.Error(
- fmt.Sprintf("Invalid Alertmanager API version '%v', expected one of '%v'", ams.cfg.APIVersion, config.SupportedAlertmanagerAPIVersions),
- "err", err,
- )
- ams.mtx.RUnlock()
- return false
- }
- }
-
- if len(ams.cfg.AlertRelabelConfigs) > 0 {
- // We can't use the cached values on the next iteration.
- cachedPayload = nil
- }
-
- // Being here means len(ams.ams) > 0
- amSetCovered.Store(k, false)
- for _, am := range ams.ams {
- wg.Add(1)
-
- ctx, cancel := context.WithTimeout(context.Background(), time.Duration(ams.cfg.Timeout))
- defer cancel()
-
- go func(ctx context.Context, k string, client *http.Client, url string, payload []byte, count int) {
- err := n.sendOne(ctx, client, url, payload)
- if err != nil {
- n.logger.Error("Error sending alerts", "alertmanager", url, "count", count, "err", err)
- n.metrics.errors.WithLabelValues(url).Add(float64(count))
- } else {
- amSetCovered.CompareAndSwap(k, false, true)
- }
-
- durationSeconds := time.Since(begin).Seconds()
- n.metrics.latencySummary.WithLabelValues(url).Observe(durationSeconds)
- n.metrics.latencyHistogram.WithLabelValues(url).Observe(durationSeconds)
- n.metrics.sent.WithLabelValues(url).Add(float64(count))
-
- wg.Done()
- }(ctx, k, ams.client, am.url().String(), payload, len(amAlerts))
- }
-
- ams.mtx.RUnlock()
- }
-
- wg.Wait()
-
- // Return false if there are any sets which were attempted (e.g. not filtered
- // out) but have no successes.
- allAmSetsCovered := true
- amSetCovered.Range(func(_, value any) bool {
- if !value.(bool) {
- allAmSetsCovered = false
- return false
- }
- return true
- })
-
- return allAmSetsCovered
-}
-
-func (n *Manager) sendOne(ctx context.Context, c *http.Client, url string, b []byte) error {
- req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(b))
- if err != nil {
- return err
- }
- req.Header.Set("User-Agent", userAgent)
- req.Header.Set("Content-Type", contentTypeJSON)
- resp, err := n.opts.Do(ctx, c, req)
- if err != nil {
- return err
- }
- defer func() {
- io.Copy(io.Discard, resp.Body)
- resp.Body.Close()
- }()
-
- // Any HTTP status 2xx is OK.
- if resp.StatusCode/100 != 2 {
- return fmt.Errorf("bad response status %s", resp.Status)
- }
-
- return nil
-}
-
// Stop signals the notification manager to shut down and immediately returns.
//
// Run will return once the notification manager has successfully shut down.
//
-// The manager will optionally drain any queued notifications before shutting down.
+// The manager will optionally drain send loops before shutting down.
//
// Stop is safe to call multiple times.
func (n *Manager) Stop() {
diff --git a/notifier/manager_test.go b/notifier/manager_test.go
index 21ab0b28a1..ed224462ff 100644
--- a/notifier/manager_test.go
+++ b/notifier/manager_test.go
@@ -14,11 +14,11 @@
package notifier
import (
- "bytes"
"context"
"encoding/json"
"fmt"
"io"
+ "log/slog"
"net/http"
"net/http/httptest"
"net/url"
@@ -27,6 +27,7 @@ import (
"time"
"github.com/prometheus/client_golang/prometheus"
+ prom_testutil "github.com/prometheus/client_golang/prometheus/testutil"
config_util "github.com/prometheus/common/config"
"github.com/prometheus/common/model"
"github.com/prometheus/common/promslog"
@@ -42,25 +43,6 @@ import (
"github.com/prometheus/prometheus/model/relabel"
)
-const maxBatchSize = 256
-
-func TestHandlerNextBatch(t *testing.T) {
- h := NewManager(&Options{}, model.UTF8Validation, nil)
-
- for i := range make([]struct{}, 2*maxBatchSize+1) {
- h.queue = append(h.queue, &Alert{
- Labels: labels.FromStrings("alertname", strconv.Itoa(i)),
- })
- }
-
- expected := append([]*Alert{}, h.queue...)
-
- require.NoError(t, alertsEqual(expected[0:maxBatchSize], h.nextBatch()))
- require.NoError(t, alertsEqual(expected[maxBatchSize:2*maxBatchSize], h.nextBatch()))
- require.NoError(t, alertsEqual(expected[2*maxBatchSize:], h.nextBatch()))
- require.Empty(t, h.queue, "Expected queue to be empty but got %d alerts", len(h.queue))
-}
-
func alertsEqual(a, b []*Alert) error {
if len(a) != len(b) {
return fmt.Errorf("length mismatch: %v != %v", a, b)
@@ -108,10 +90,37 @@ func newTestHTTPServerBuilder(expected *[]*Alert, errc chan<- error, u, p string
}))
}
+func newTestAlertmanagerSet(
+ cfg *config.AlertmanagerConfig,
+ client *http.Client,
+ opts *Options,
+ metrics *alertMetrics,
+ alertmanagerURLs ...string,
+) *alertmanagerSet {
+ ams := make([]alertmanager, len(alertmanagerURLs))
+ for i, am := range alertmanagerURLs {
+ ams[i] = alertmanagerMock{urlf: func() string { return am }}
+ }
+ logger := slog.New(slog.DiscardHandler)
+ sendLoops := make(map[string]*sendLoop)
+ for _, am := range alertmanagerURLs {
+ sendLoops[am] = newSendLoop(am, client, cfg, opts, logger, metrics)
+ }
+ return &alertmanagerSet{
+ ams: ams,
+ cfg: cfg,
+ client: client,
+ logger: logger,
+ metrics: metrics,
+ opts: opts,
+ sendLoops: sendLoops,
+ }
+}
+
func TestHandlerSendAll(t *testing.T) {
var (
errc = make(chan error, 1)
- expected = make([]*Alert, 0, maxBatchSize)
+ expected = make([]*Alert, 0)
status1, status2, status3 atomic.Int32
)
status1.Store(int32(http.StatusOK))
@@ -125,7 +134,8 @@ func TestHandlerSendAll(t *testing.T) {
defer server2.Close()
defer server3.Close()
- h := NewManager(&Options{}, model.UTF8Validation, nil)
+ reg := prometheus.NewRegistry()
+ h := NewManager(&Options{Registerer: reg}, model.UTF8Validation, nil)
authClient, _ := config_util.NewClientFromConfig(
config_util.HTTPClientConfig{
@@ -146,35 +156,15 @@ func TestHandlerSendAll(t *testing.T) {
am3Cfg := config.DefaultAlertmanagerConfig
am3Cfg.Timeout = model.Duration(time.Second)
- h.alertmanagers["1"] = &alertmanagerSet{
- ams: []alertmanager{
- alertmanagerMock{
- urlf: func() string { return server1.URL },
- },
- },
- cfg: &am1Cfg,
- client: authClient,
- }
+ opts := &Options{Do: do, QueueCapacity: 10_000, MaxBatchSize: DefaultMaxBatchSize}
- h.alertmanagers["2"] = &alertmanagerSet{
- ams: []alertmanager{
- alertmanagerMock{
- urlf: func() string { return server2.URL },
- },
- alertmanagerMock{
- urlf: func() string { return server3.URL },
- },
- },
- cfg: &am2Cfg,
- }
+ h.alertmanagers["1"] = newTestAlertmanagerSet(&am1Cfg, authClient, opts, h.metrics, server1.URL)
+ h.alertmanagers["2"] = newTestAlertmanagerSet(&am2Cfg, nil, opts, h.metrics, server2.URL, server3.URL)
+ h.alertmanagers["3"] = newTestAlertmanagerSet(&am3Cfg, nil, opts, h.metrics)
- h.alertmanagers["3"] = &alertmanagerSet{
- ams: []alertmanager{}, // empty set
- cfg: &am3Cfg,
- }
-
- for i := range make([]struct{}, maxBatchSize) {
- h.queue = append(h.queue, &Alert{
+ var alerts []*Alert
+ for i := range DefaultMaxBatchSize {
+ alerts = append(alerts, &Alert{
Labels: labels.FromStrings("alertname", strconv.Itoa(i)),
})
expected = append(expected, &Alert{
@@ -191,34 +181,62 @@ func TestHandlerSendAll(t *testing.T) {
}
}
- // all ams in all sets are up
- require.True(t, h.sendAll(h.queue...), "all sends failed unexpectedly")
+ // Start send loops.
+ for _, ams := range h.alertmanagers {
+ ams.startSendLoops(ams.ams)
+ }
+ defer func() {
+ for _, ams := range h.alertmanagers {
+ ams.cleanSendLoops(ams.ams...)
+ }
+ }()
+
+ h.Send(alerts...)
+ require.Eventually(t, func() bool {
+ return prom_testutil.ToFloat64(h.metrics.sent.WithLabelValues(server1.URL)) == DefaultMaxBatchSize
+ }, time.Second*2, time.Millisecond*10)
checkNoErr()
- // the only am in set 1 is down
+ // The only am in set 1 is down.
status1.Store(int32(http.StatusNotFound))
- require.False(t, h.sendAll(h.queue...), "all sends failed unexpectedly")
+ h.Send(alerts...)
+ // Wait for all send loops to process before changing any status.
+ require.Eventually(t, func() bool {
+ return prom_testutil.ToFloat64(h.metrics.errors.WithLabelValues(server1.URL)) == DefaultMaxBatchSize &&
+ prom_testutil.ToFloat64(h.metrics.sent.WithLabelValues(server2.URL)) == DefaultMaxBatchSize*2 &&
+ prom_testutil.ToFloat64(h.metrics.sent.WithLabelValues(server3.URL)) == DefaultMaxBatchSize*2
+ }, time.Second*2, time.Millisecond*10)
checkNoErr()
- // reset it
+ // Fix the am.
status1.Store(int32(http.StatusOK))
- // only one of the ams in set 2 is down
+ // Only one of the ams in set 2 is down.
status2.Store(int32(http.StatusInternalServerError))
- require.True(t, h.sendAll(h.queue...), "all sends succeeded unexpectedly")
+ h.Send(alerts...)
+ // Wait for all send loops to either send or fail with errors depending on their status.
+ require.Eventually(t, func() bool {
+ return prom_testutil.ToFloat64(h.metrics.errors.WithLabelValues(server2.URL)) == DefaultMaxBatchSize &&
+ prom_testutil.ToFloat64(h.metrics.sent.WithLabelValues(server1.URL)) == DefaultMaxBatchSize*2 &&
+ prom_testutil.ToFloat64(h.metrics.sent.WithLabelValues(server3.URL)) == DefaultMaxBatchSize*3
+ }, time.Second*2, time.Millisecond*10)
checkNoErr()
- // both ams in set 2 are down
+ // Both ams in set 2 are down.
status3.Store(int32(http.StatusInternalServerError))
- require.False(t, h.sendAll(h.queue...), "all sends succeeded unexpectedly")
+ h.Send(alerts...)
+ require.Eventually(t, func() bool {
+ return prom_testutil.ToFloat64(h.metrics.errors.WithLabelValues(server2.URL)) == DefaultMaxBatchSize*2 &&
+ prom_testutil.ToFloat64(h.metrics.errors.WithLabelValues(server3.URL)) == DefaultMaxBatchSize
+ }, time.Second*2, time.Millisecond*10)
checkNoErr()
}
func TestHandlerSendAllRemapPerAm(t *testing.T) {
var (
errc = make(chan error, 1)
- expected1 = make([]*Alert, 0, maxBatchSize)
- expected2 = make([]*Alert, 0, maxBatchSize)
+ expected1 = make([]*Alert, 0)
+ expected2 = make([]*Alert, 0)
expected3 = make([]*Alert, 0)
status1, status2, status3 atomic.Int32
@@ -235,7 +253,8 @@ func TestHandlerSendAllRemapPerAm(t *testing.T) {
defer server2.Close()
defer server3.Close()
- h := NewManager(&Options{}, model.UTF8Validation, nil)
+ reg := prometheus.NewRegistry()
+ h := NewManager(&Options{QueueCapacity: 10_000, Registerer: reg}, model.UTF8Validation, nil)
h.alertmanagers = make(map[string]*alertmanagerSet)
am1Cfg := config.DefaultAlertmanagerConfig
@@ -263,43 +282,18 @@ func TestHandlerSendAllRemapPerAm(t *testing.T) {
},
}
- h.alertmanagers = map[string]*alertmanagerSet{
- // Drop no alerts.
- "1": {
- ams: []alertmanager{
- alertmanagerMock{
- urlf: func() string { return server1.URL },
- },
- },
- cfg: &am1Cfg,
- },
- // Drop only alerts with the "alertnamedrop" label.
- "2": {
- ams: []alertmanager{
- alertmanagerMock{
- urlf: func() string { return server2.URL },
- },
- },
- cfg: &am2Cfg,
- },
- // Drop all alerts.
- "3": {
- ams: []alertmanager{
- alertmanagerMock{
- urlf: func() string { return server3.URL },
- },
- },
- cfg: &am3Cfg,
- },
- // Empty list of Alertmanager endpoints.
- "4": {
- ams: []alertmanager{},
- cfg: &config.DefaultAlertmanagerConfig,
- },
- }
+ // Drop no alerts.
+ h.alertmanagers["1"] = newTestAlertmanagerSet(&am1Cfg, nil, h.opts, h.metrics, server1.URL)
+ // Drop only alerts with the "alertnamedrop" label.
+ h.alertmanagers["2"] = newTestAlertmanagerSet(&am2Cfg, nil, h.opts, h.metrics, server2.URL)
+ // Drop all alerts.
+ h.alertmanagers["3"] = newTestAlertmanagerSet(&am3Cfg, nil, h.opts, h.metrics, server3.URL)
+ // Empty list of Alertmanager endpoints.
+ h.alertmanagers["4"] = newTestAlertmanagerSet(&config.DefaultAlertmanagerConfig, nil, h.opts, h.metrics)
- for i := range make([]struct{}, maxBatchSize/2) {
- h.queue = append(h.queue,
+ var alerts []*Alert
+ for i := range make([]struct{}, DefaultMaxBatchSize/2) {
+ alerts = append(alerts,
&Alert{
Labels: labels.FromStrings("alertname", strconv.Itoa(i)),
},
@@ -330,63 +324,48 @@ func TestHandlerSendAllRemapPerAm(t *testing.T) {
}
}
- // all ams are up
- require.True(t, h.sendAll(h.queue...), "all sends failed unexpectedly")
+ // Start send loops.
+ for _, ams := range h.alertmanagers {
+ ams.startSendLoops(ams.ams)
+ }
+ defer func() {
+ // Stop send loops.
+ for _, ams := range h.alertmanagers {
+ ams.cleanSendLoops(ams.ams...)
+ }
+ }()
+
+ // All ams are up.
+ h.Send(alerts...)
+ require.Eventually(t, func() bool {
+ return prom_testutil.ToFloat64(h.metrics.sent.WithLabelValues(server1.URL)) == DefaultMaxBatchSize
+ }, time.Second*2, time.Millisecond*10)
checkNoErr()
- // the only am in set 1 goes down
+ // The only am in set 1 goes down.
status1.Store(int32(http.StatusInternalServerError))
- require.False(t, h.sendAll(h.queue...), "all sends failed unexpectedly")
+ h.Send(alerts...)
+ // Wait for metrics to update.
+ require.Eventually(t, func() bool {
+ return prom_testutil.ToFloat64(h.metrics.errors.WithLabelValues(server1.URL)) == DefaultMaxBatchSize
+ }, time.Second*2, time.Millisecond*10)
checkNoErr()
- // reset set 1
+ // Reset set 1.
status1.Store(int32(http.StatusOK))
- // set 3 loses its only am, but all alerts were dropped
- // so there was nothing to send, keeping sendAll true
+ // Set 3 loses its only am, but all alerts were dropped
+ // so there was nothing to send, keeping sendAll true.
status3.Store(int32(http.StatusInternalServerError))
- require.True(t, h.sendAll(h.queue...), "all sends failed unexpectedly")
+ h.Send(alerts...)
checkNoErr()
-
- // Verify that individual locks are released.
- for k := range h.alertmanagers {
- h.alertmanagers[k].mtx.Lock()
- h.alertmanagers[k].ams = nil
- h.alertmanagers[k].mtx.Unlock()
- }
-}
-
-func TestCustomDo(t *testing.T) {
- const testURL = "http://testurl.com/"
- const testBody = "testbody"
-
- var received bool
- h := NewManager(&Options{
- Do: func(_ context.Context, _ *http.Client, req *http.Request) (*http.Response, error) {
- received = true
- body, err := io.ReadAll(req.Body)
-
- require.NoError(t, err)
-
- require.Equal(t, testBody, string(body))
-
- require.Equal(t, testURL, req.URL.String())
-
- return &http.Response{
- Body: io.NopCloser(bytes.NewBuffer(nil)),
- }, nil
- },
- }, model.UTF8Validation, nil)
-
- h.sendOne(context.Background(), nil, testURL, []byte(testBody))
-
- require.True(t, received, "Expected to receive an alert, but didn't")
}
func TestExternalLabels(t *testing.T) {
+ reg := prometheus.NewRegistry()
h := NewManager(&Options{
- QueueCapacity: 3 * maxBatchSize,
- MaxBatchSize: maxBatchSize,
+ QueueCapacity: 3 * DefaultMaxBatchSize,
+ MaxBatchSize: DefaultMaxBatchSize,
ExternalLabels: labels.FromStrings("a", "b"),
RelabelConfigs: []*relabel.Config{
{
@@ -398,8 +377,14 @@ func TestExternalLabels(t *testing.T) {
NameValidationScheme: model.UTF8Validation,
},
},
+ Registerer: reg,
}, model.UTF8Validation, nil)
+ cfg := config.DefaultAlertmanagerConfig
+ h.alertmanagers = map[string]*alertmanagerSet{
+ "test": newTestAlertmanagerSet(&cfg, nil, h.opts, h.metrics, "test"),
+ }
+
// This alert should get the external label attached.
h.Send(&Alert{
Labels: labels.FromStrings("alertname", "test"),
@@ -416,13 +401,14 @@ func TestExternalLabels(t *testing.T) {
{Labels: labels.FromStrings("alertname", "externalrelabelthis", "a", "c")},
}
- require.NoError(t, alertsEqual(expected, h.queue))
+ require.NoError(t, alertsEqual(expected, h.alertmanagers["test"].sendLoops["test"].queue))
}
func TestHandlerRelabel(t *testing.T) {
+ reg := prometheus.NewRegistry()
h := NewManager(&Options{
- QueueCapacity: 3 * maxBatchSize,
- MaxBatchSize: maxBatchSize,
+ QueueCapacity: 3 * DefaultMaxBatchSize,
+ MaxBatchSize: DefaultMaxBatchSize,
RelabelConfigs: []*relabel.Config{
{
SourceLabels: model.LabelNames{"alertname"},
@@ -439,8 +425,14 @@ func TestHandlerRelabel(t *testing.T) {
NameValidationScheme: model.UTF8Validation,
},
},
+ Registerer: reg,
}, model.UTF8Validation, nil)
+ cfg := config.DefaultAlertmanagerConfig
+ h.alertmanagers = map[string]*alertmanagerSet{
+ "test": newTestAlertmanagerSet(&cfg, nil, h.opts, h.metrics, "test"),
+ }
+
// This alert should be dropped due to the configuration
h.Send(&Alert{
Labels: labels.FromStrings("alertname", "drop"),
@@ -455,7 +447,7 @@ func TestHandlerRelabel(t *testing.T) {
{Labels: labels.FromStrings("alertname", "renamed")},
}
- require.NoError(t, alertsEqual(expected, h.queue))
+ require.NoError(t, alertsEqual(expected, h.alertmanagers["test"].sendLoops["test"].queue))
}
func TestHandlerQueuing(t *testing.T) {
@@ -500,10 +492,12 @@ func TestHandlerQueuing(t *testing.T) {
server.Close()
}()
+ reg := prometheus.NewRegistry()
h := NewManager(
&Options{
- QueueCapacity: 3 * maxBatchSize,
- MaxBatchSize: maxBatchSize,
+ QueueCapacity: 3 * DefaultMaxBatchSize,
+ MaxBatchSize: DefaultMaxBatchSize,
+ Registerer: reg,
},
model.UTF8Validation,
nil,
@@ -513,20 +507,18 @@ func TestHandlerQueuing(t *testing.T) {
am1Cfg := config.DefaultAlertmanagerConfig
am1Cfg.Timeout = model.Duration(time.Second)
+ h.alertmanagers["1"] = newTestAlertmanagerSet(&am1Cfg, nil, h.opts, h.metrics, server.URL)
- h.alertmanagers["1"] = &alertmanagerSet{
- ams: []alertmanager{
- alertmanagerMock{
- urlf: func() string { return server.URL },
- },
- },
- cfg: &am1Cfg,
- }
go h.Run(nil)
defer h.Stop()
+ // Start send loops.
+ for _, ams := range h.alertmanagers {
+ ams.startSendLoops(ams.ams)
+ }
+
var alerts []*Alert
- for i := range make([]struct{}, 20*maxBatchSize) {
+ for i := range make([]struct{}, 20*DefaultMaxBatchSize) {
alerts = append(alerts, &Alert{
Labels: labels.FromStrings("alertname", strconv.Itoa(i)),
})
@@ -547,29 +539,22 @@ func TestHandlerQueuing(t *testing.T) {
}
}
- // If the batch is larger than the queue capacity, it should be truncated
- // from the front.
- h.Send(alerts[:4*maxBatchSize]...)
- for i := 1; i < 4; i++ {
- assertAlerts(alerts[i*maxBatchSize : (i+1)*maxBatchSize])
- }
-
// Send one batch, wait for it to arrive and block the server so the queue fills up.
- h.Send(alerts[:maxBatchSize]...)
+ h.Send(alerts[:DefaultMaxBatchSize]...)
<-called
// Send several batches while the server is still blocked so the queue
- // fills up to its maximum capacity (3*maxBatchSize). Then check that the
+ // fills up to its maximum capacity (3*DefaultMaxBatchSize). Then check that the
// queue is truncated in the front.
- h.Send(alerts[1*maxBatchSize : 2*maxBatchSize]...) // this batch should be dropped.
- h.Send(alerts[2*maxBatchSize : 3*maxBatchSize]...)
- h.Send(alerts[3*maxBatchSize : 4*maxBatchSize]...)
+ h.Send(alerts[1*DefaultMaxBatchSize : 2*DefaultMaxBatchSize]...) // This batch should be dropped.
+ h.Send(alerts[2*DefaultMaxBatchSize : 3*DefaultMaxBatchSize]...)
+ h.Send(alerts[3*DefaultMaxBatchSize : 4*DefaultMaxBatchSize]...)
// Send the batch that drops the first one.
- h.Send(alerts[4*maxBatchSize : 5*maxBatchSize]...)
+ h.Send(alerts[4*DefaultMaxBatchSize : 5*DefaultMaxBatchSize]...)
// Unblock the server.
- expectedc <- alerts[:maxBatchSize]
+ expectedc <- alerts[:DefaultMaxBatchSize]
select {
case err := <-errc:
require.NoError(t, err)
@@ -579,7 +564,7 @@ func TestHandlerQueuing(t *testing.T) {
// Verify that we receive the last 3 batches.
for i := 2; i < 5; i++ {
- assertAlerts(alerts[i*maxBatchSize : (i+1)*maxBatchSize])
+ assertAlerts(alerts[i*DefaultMaxBatchSize : (i+1)*DefaultMaxBatchSize])
}
}
@@ -715,7 +700,7 @@ func makeInputTargetGroup() *targetgroup.Group {
func TestHangingNotifier(t *testing.T) {
const (
batches = 100
- alertsCount = maxBatchSize * batches
+ alertsCount = DefaultMaxBatchSize * batches
)
var (
@@ -725,10 +710,6 @@ func TestHangingNotifier(t *testing.T) {
done = make(chan struct{})
)
- defer func() {
- close(done)
- }()
-
// Set up a faulty Alertmanager.
var faultyCalled atomic.Bool
faultyServer := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
@@ -738,21 +719,29 @@ func TestHangingNotifier(t *testing.T) {
case <-time.After(time.Hour):
}
}))
+ defer func() {
+ close(done)
+ }()
+
faultyURL, err := url.Parse(faultyServer.URL)
require.NoError(t, err)
+ faultyURL.Path = "/api/v2/alerts"
// Set up a functional Alertmanager.
var functionalCalled atomic.Bool
functionalServer := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
functionalCalled.Store(true)
}))
+ defer functionalServer.Close()
functionalURL, err := url.Parse(functionalServer.URL)
require.NoError(t, err)
+ functionalURL.Path = "/api/v2/alerts"
// Initialize the discovery manager
// This is relevant as the updates aren't sent continually in real life, but only each updatert.
// The old implementation of TestHangingNotifier didn't take that into account.
- ctx := t.Context()
+ ctx, cancelSdManager := context.WithCancel(t.Context())
+ defer cancelSdManager()
reg := prometheus.NewRegistry()
sdMetrics, err := discovery.RegisterSDMetrics(reg, discovery.NewRefreshMetrics(reg))
require.NoError(t, err)
@@ -770,6 +759,7 @@ func TestHangingNotifier(t *testing.T) {
notifier := NewManager(
&Options{
QueueCapacity: alertsCount,
+ Registerer: reg,
},
model.UTF8Validation,
nil,
@@ -777,18 +767,12 @@ func TestHangingNotifier(t *testing.T) {
notifier.alertmanagers = make(map[string]*alertmanagerSet)
amCfg := config.DefaultAlertmanagerConfig
amCfg.Timeout = model.Duration(sendTimeout)
- notifier.alertmanagers["config-0"] = &alertmanagerSet{
- ams: []alertmanager{
- alertmanagerMock{
- urlf: func() string { return faultyURL.String() },
- },
- alertmanagerMock{
- urlf: func() string { return functionalURL.String() },
- },
- },
- cfg: &amCfg,
- metrics: notifier.metrics,
+ notifier.alertmanagers["config-0"] = newTestAlertmanagerSet(&amCfg, nil, notifier.opts, notifier.metrics, faultyURL.String(), functionalURL.String())
+
+ for _, ams := range notifier.alertmanagers {
+ ams.startSendLoops(ams.ams)
}
+
go notifier.Run(sdManager.SyncCh())
defer notifier.Stop()
@@ -834,10 +818,6 @@ loop1:
}
require.NoError(t, sdManager.ApplyConfig(c))
- // The notifier should not wait until the alerts queue is empty to apply the discovery changes
- // A faulty Alertmanager could cause each alert sending cycle to take up to AlertmanagerConfig.Timeout
- // The queue may never be emptied, as the arrival rate could be larger than the departure rate
- // It could even overflow and alerts could be dropped.
timeout = time.After(batches * sendTimeout)
loop2:
for {
@@ -847,11 +827,10 @@ loop2:
default:
// The faulty alertmanager was dropped.
if len(notifier.Alertmanagers()) == 1 {
- // Prevent from TOCTOU.
- require.Positive(t, notifier.queueLen())
+ // The notifier should not wait until the alerts queue of the functional am is empty to apply the discovery changes.
+ require.NotZero(t, notifier.alertmanagers["config-0"].sendLoops[functionalURL.String()].queueLen())
break loop2
}
- require.Positive(t, notifier.queueLen(), "The faulty alertmanager wasn't dropped before the alerts queue was emptied.")
}
}
}
@@ -884,10 +863,12 @@ func TestStop_DrainingDisabled(t *testing.T) {
server.Close()
}()
+ reg := prometheus.NewRegistry()
m := NewManager(
&Options{
QueueCapacity: 10,
DrainOnShutdown: false,
+ Registerer: reg,
},
model.UTF8Validation,
nil,
@@ -897,14 +878,10 @@ func TestStop_DrainingDisabled(t *testing.T) {
am1Cfg := config.DefaultAlertmanagerConfig
am1Cfg.Timeout = model.Duration(time.Second)
+ m.alertmanagers["1"] = newTestAlertmanagerSet(&am1Cfg, nil, m.opts, m.metrics, server.URL)
- m.alertmanagers["1"] = &alertmanagerSet{
- ams: []alertmanager{
- alertmanagerMock{
- urlf: func() string { return server.URL },
- },
- },
- cfg: &am1Cfg,
+ for _, ams := range m.alertmanagers {
+ ams.startSendLoops(ams.ams)
}
notificationManagerStopped := make(chan struct{})
@@ -949,11 +926,11 @@ func TestStop_DrainingEnabled(t *testing.T) {
alertsReceived := atomic.NewInt64(0)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ var alerts []*Alert
+
// Let the test know we've received a request.
receiverReceivedRequest <- struct{}{}
- var alerts []*Alert
-
b, err := io.ReadAll(r.Body)
require.NoError(t, err)
@@ -971,10 +948,12 @@ func TestStop_DrainingEnabled(t *testing.T) {
server.Close()
}()
+ reg := prometheus.NewRegistry()
m := NewManager(
&Options{
QueueCapacity: 10,
DrainOnShutdown: true,
+ Registerer: reg,
},
model.UTF8Validation,
nil,
@@ -984,14 +963,10 @@ func TestStop_DrainingEnabled(t *testing.T) {
am1Cfg := config.DefaultAlertmanagerConfig
am1Cfg.Timeout = model.Duration(time.Second)
+ m.alertmanagers["1"] = newTestAlertmanagerSet(&am1Cfg, nil, m.opts, m.metrics, server.URL)
- m.alertmanagers["1"] = &alertmanagerSet{
- ams: []alertmanager{
- alertmanagerMock{
- urlf: func() string { return server.URL },
- },
- },
- cfg: &am1Cfg,
+ for _, ams := range m.alertmanagers {
+ ams.startSendLoops(ams.ams)
}
notificationManagerStopped := make(chan struct{})
@@ -1028,6 +1003,44 @@ func TestStop_DrainingEnabled(t *testing.T) {
require.Equal(t, int64(2), alertsReceived.Load())
}
+// TestQueuesDrainingOnApplyConfig ensures that when an alertmanagerSet disappears after an ApplyConfig(), its
+// sendLoops queues are drained only when DrainOnShutdown is set.
+func TestQueuesDrainingOnApplyConfig(t *testing.T) {
+ for _, drainOnShutDown := range []bool{false, true} {
+ t.Run(strconv.FormatBool(drainOnShutDown), func(t *testing.T) {
+ t.Parallel()
+ alertSent := make(chan struct{})
+
+ server := newImmediateAlertManager(alertSent)
+ defer server.Close()
+
+ h := NewManager(&Options{QueueCapacity: 10, DrainOnShutdown: drainOnShutDown}, model.UTF8Validation, nil)
+ h.alertmanagers = make(map[string]*alertmanagerSet)
+
+ amCfg := config.DefaultAlertmanagerConfig
+ amCfg.Timeout = model.Duration(time.Second)
+ h.alertmanagers["1"] = newTestAlertmanagerSet(&amCfg, nil, h.opts, h.metrics, server.URL)
+
+ // The send loops were not started, nothing will be sent.
+ h.Send([]*Alert{{Labels: labels.FromStrings("alertname", "foo")}}...)
+
+ // Remove the alertmanagerSet.
+ h.ApplyConfig(&config.Config{})
+
+ select {
+ case <-alertSent:
+ if !drainOnShutDown {
+ require.FailNow(t, "no alert should be sent")
+ }
+ case <-time.After(100 * time.Millisecond):
+ if drainOnShutDown {
+ require.FailNow(t, "alert wasn't received")
+ }
+ }
+ })
+ }
+}
+
func TestApplyConfig(t *testing.T) {
targetURL := "alertmanager:9093"
targetGroup := &targetgroup.Group{
@@ -1152,7 +1165,7 @@ func TestAlerstRelabelingIsIsolated(t *testing.T) {
defer server1.Close()
defer server2.Close()
- h := NewManager(&Options{}, model.UTF8Validation, nil)
+ h := NewManager(&Options{QueueCapacity: 10}, model.UTF8Validation, nil)
h.alertmanagers = make(map[string]*alertmanagerSet)
am1Cfg := config.DefaultAlertmanagerConfig
@@ -1172,34 +1185,29 @@ func TestAlerstRelabelingIsIsolated(t *testing.T) {
am2Cfg.Timeout = model.Duration(time.Second)
h.alertmanagers = map[string]*alertmanagerSet{
- "am1": {
- ams: []alertmanager{
- alertmanagerMock{
- urlf: func() string { return server1.URL },
- },
- },
- cfg: &am1Cfg,
- },
- "am2": {
- ams: []alertmanager{
- alertmanagerMock{
- urlf: func() string { return server2.URL },
- },
- },
- cfg: &am2Cfg,
- },
+ "am1": newTestAlertmanagerSet(&am1Cfg, nil, h.opts, h.metrics, server1.URL),
+ "am2": newTestAlertmanagerSet(&am2Cfg, nil, h.opts, h.metrics, server2.URL),
}
+ // Start send loops.
+ for _, ams := range h.alertmanagers {
+ ams.startSendLoops(ams.ams)
+ }
+ defer func() {
+ for _, ams := range h.alertmanagers {
+ ams.cleanSendLoops(ams.ams...)
+ }
+ }()
+
testAlert := &Alert{
Labels: labels.FromStrings("alertname", "test"),
}
- h.queue = []*Alert{testAlert}
expected1 = append(expected1, &Alert{
Labels: labels.FromStrings("alertname", "test", "parasite", "yes"),
})
- // am2 shouldn't get the parasite label.
+ // Am2 shouldn't get the parasite label.
expected2 = append(expected2, &Alert{
Labels: labels.FromStrings("alertname", "test"),
})
@@ -1213,6 +1221,363 @@ func TestAlerstRelabelingIsIsolated(t *testing.T) {
}
}
- require.True(t, h.sendAll(h.queue...))
+ h.Send(testAlert)
checkNoErr()
}
+
+// Regression test for https://github.com/prometheus/prometheus/issues/7676
+// The test creates a black hole alertmanager that never responds to any requests.
+// The alertmanager_config.timeout is set to infinite (1 year).
+// We check that the notifier does not hang and throughput is not affected.
+func TestNotifierQueueIndependentOfFailedAlertmanager(t *testing.T) {
+ stopBlackHole := make(chan struct{})
+ blackHoleAM := newBlackHoleAlertmanager(stopBlackHole)
+ defer func() {
+ close(stopBlackHole)
+ blackHoleAM.Close()
+ }()
+
+ doneAlertReceive := make(chan struct{})
+ immediateAM := newImmediateAlertManager(doneAlertReceive)
+ defer immediateAM.Close()
+
+ do := func(ctx context.Context, client *http.Client, req *http.Request) (*http.Response, error) {
+ return client.Do(req.WithContext(ctx))
+ }
+
+ reg := prometheus.NewRegistry()
+ h := NewManager(&Options{
+ Do: do,
+ QueueCapacity: 10,
+ MaxBatchSize: DefaultMaxBatchSize,
+ Registerer: reg,
+ }, model.UTF8Validation, nil)
+
+ h.alertmanagers = make(map[string]*alertmanagerSet)
+
+ amCfg := config.DefaultAlertmanagerConfig
+ amCfg.Timeout = model.Duration(time.Hour * 24 * 365)
+ h.alertmanagers["1"] = newTestAlertmanagerSet(&amCfg, http.DefaultClient, h.opts, h.metrics, blackHoleAM.URL)
+ h.alertmanagers["2"] = newTestAlertmanagerSet(&amCfg, http.DefaultClient, h.opts, h.metrics, immediateAM.URL)
+
+ doneSendAll := make(chan struct{})
+ for _, ams := range h.alertmanagers {
+ ams.startSendLoops(ams.ams)
+ }
+ defer func() {
+ for _, ams := range h.alertmanagers {
+ ams.cleanSendLoops(ams.ams...)
+ }
+ }()
+
+ go func() {
+ h.Send(&Alert{
+ Labels: labels.FromStrings("alertname", "test"),
+ })
+ close(doneSendAll)
+ }()
+
+ select {
+ case <-doneAlertReceive:
+ // This is the happy case, the alert was received by the immediate alertmanager.
+ case <-time.After(2 * time.Second):
+ t.Fatal("Timeout waiting for alert to be received by immediate alertmanager")
+ }
+
+ select {
+ case <-doneSendAll:
+ // This is the happy case, the sendAll function returned.
+ case <-time.After(2 * time.Second):
+ t.Fatal("Timeout waiting for sendAll to return")
+ }
+}
+
+// TestApplyConfigSendLoopsNotStoppedOnKeyChange reproduces a bug where sendLoops
+// are incorrectly stopped when the alertmanager config key changes but the config
+// content (and thus its hash) remains the same.
+//
+// The bug scenario:
+// 1. Old config has alertmanager set with key "config-0" and config hash X
+// 2. New config has TWO alertmanager sets where the SECOND one ("config-1") has hash X
+// 3. sendLoops are transferred from old "config-0" to new "config-1" (hash match)
+// 4. Cleanup checks if key "config-0" exists in new config — it does (different config)
+// 5. No cleanup happens for old "config-0", sendLoops work correctly
+//
+// However, there's a variant where the key disappears completely:
+// 1. Old config: "config-0" with hash X, "config-1" with hash Y
+// 2. New config: "config-0" with hash Y (was "config-1"), no "config-1"
+// 3. sendLoops from old "config-0" (hash X) have nowhere to go
+// 4. Cleanup sees "config-1" doesn't exist, tries to clean up old "config-1"
+//
+// This test verifies that when config keys change, sendLoops are correctly preserved.
+func TestApplyConfigSendLoopsNotStoppedOnKeyChange(t *testing.T) {
+ alertReceived := make(chan struct{}, 10)
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ select {
+ case alertReceived <- struct{}{}:
+ default:
+ }
+ }))
+ defer server.Close()
+
+ targetURL := server.Listener.Addr().String()
+ targetGroup := &targetgroup.Group{
+ Targets: []model.LabelSet{
+ {
+ "__address__": model.LabelValue(targetURL),
+ },
+ },
+ }
+
+ n := NewManager(&Options{QueueCapacity: 10}, model.UTF8Validation, nil)
+ cfg := &config.Config{}
+
+ // Initial config with TWO alertmanager configs.
+ // "config-0" uses file_sd_configs with foo.json (hash X)
+ // "config-1" uses file_sd_configs with bar.json (hash Y)
+ s := `
+alerting:
+ alertmanagers:
+ - file_sd_configs:
+ - files:
+ - foo.json
+ - file_sd_configs:
+ - files:
+ - bar.json
+`
+ require.NoError(t, yaml.UnmarshalStrict([]byte(s), cfg))
+ require.NoError(t, n.ApplyConfig(cfg))
+
+ // Reload with target groups to discover alertmanagers.
+ tgs := map[string][]*targetgroup.Group{
+ "config-0": {targetGroup},
+ "config-1": {targetGroup},
+ }
+ n.reload(tgs)
+ require.Len(t, n.Alertmanagers(), 2)
+
+ // Verify sendLoops exist for both configs.
+ require.Len(t, n.alertmanagers["config-0"].sendLoops, 1)
+ require.Len(t, n.alertmanagers["config-1"].sendLoops, 1)
+
+ // Start the send loops.
+ for _, ams := range n.alertmanagers {
+ ams.startSendLoops(ams.ams)
+ }
+ defer func() {
+ for _, ams := range n.alertmanagers {
+ ams.mtx.Lock()
+ ams.cleanSendLoops(ams.ams...)
+ ams.mtx.Unlock()
+ }
+ }()
+
+ // Send an alert and verify it's received (twice, once per alertmanager set).
+ n.Send(&Alert{Labels: labels.FromStrings("alertname", "test1")})
+ for range 2 {
+ select {
+ case <-alertReceived:
+ // Good, alert was sent.
+ case <-time.After(2 * time.Second):
+ require.FailNow(t, "timeout waiting for first alert")
+ }
+ }
+
+ // Apply a new config that REVERSES the order of alertmanager configs.
+ // Now "config-0" has hash Y (was bar.json) and "config-1" has hash X (was foo.json).
+ // The sendLoops should be transferred based on hash matching.
+ s = `
+alerting:
+ alertmanagers:
+ - file_sd_configs:
+ - files:
+ - bar.json
+ - file_sd_configs:
+ - files:
+ - foo.json
+`
+ require.NoError(t, yaml.UnmarshalStrict([]byte(s), cfg))
+ require.NoError(t, n.ApplyConfig(cfg))
+
+ // CRITICAL CHECK: After ApplyConfig but BEFORE reload, the sendLoops should
+ // have been transferred based on hash matching and NOT stopped.
+ // - Old "config-0" (foo.json, hash X) -> New "config-1" (foo.json, hash X)
+ // - Old "config-1" (bar.json, hash Y) -> New "config-0" (bar.json, hash Y)
+ // Both old keys exist in new config, so no cleanup should happen.
+ require.Len(t, n.alertmanagers["config-0"].sendLoops, 1, "sendLoops should be transferred to config-0")
+ require.Len(t, n.alertmanagers["config-1"].sendLoops, 1, "sendLoops should be transferred to config-1")
+
+ // Reload with target groups for the new config.
+ tgs = map[string][]*targetgroup.Group{
+ "config-0": {targetGroup},
+ "config-1": {targetGroup},
+ }
+ n.reload(tgs)
+
+ // The alertmanagers should still be discoverable.
+ require.Len(t, n.Alertmanagers(), 2)
+
+ // The critical test: send another alert and verify it's received by both.
+ n.Send(&Alert{Labels: labels.FromStrings("alertname", "test2")})
+ for range 2 {
+ select {
+ case <-alertReceived:
+ // Good, alert was sent - sendLoops are still working.
+ case <-time.After(2 * time.Second):
+ require.FailNow(t, "timeout waiting for second alert - sendLoops may have been incorrectly stopped")
+ }
+ }
+}
+
+// TestApplyConfigDuplicateHashSharesSendLoops tests a bug where multiple new
+// alertmanager configs with identical content (same hash) all receive the same
+// sendLoops map reference, causing shared mutable state between alertmanagerSets.
+//
+// Bug scenario:
+// 1. Old config: "config-0" with hash X
+// 2. New config: "config-0" AND "config-1" both with hash X (identical configs)
+// 3. Both new sets get `sendLoops = oldAmSet.sendLoops` (same map reference!)
+// 4. Now config-0 and config-1 share the same sendLoops map
+// 5. When config-1's alertmanager is removed via sync(), it cleans up the shared
+// sendLoops, breaking config-0's ability to send alerts
+func TestApplyConfigDuplicateHashSharesSendLoops(t *testing.T) {
+ n := NewManager(&Options{QueueCapacity: 10}, model.UTF8Validation, nil)
+ cfg := &config.Config{}
+
+ // Initial config with ONE alertmanager.
+ s := `
+alerting:
+ alertmanagers:
+ - file_sd_configs:
+ - files:
+ - foo.json
+`
+ require.NoError(t, yaml.UnmarshalStrict([]byte(s), cfg))
+ require.NoError(t, n.ApplyConfig(cfg))
+
+ targetGroup := &targetgroup.Group{
+ Targets: []model.LabelSet{
+ {"__address__": "alertmanager:9093"},
+ },
+ }
+ tgs := map[string][]*targetgroup.Group{"config-0": {targetGroup}}
+ n.reload(tgs)
+
+ require.Len(t, n.alertmanagers["config-0"].sendLoops, 1)
+
+ // Apply a new config with TWO IDENTICAL alertmanager configs.
+ // Both have the same hash, so both will receive sendLoops from the same old set.
+ s = `
+alerting:
+ alertmanagers:
+ - file_sd_configs:
+ - files:
+ - foo.json
+ - file_sd_configs:
+ - files:
+ - foo.json
+`
+ require.NoError(t, yaml.UnmarshalStrict([]byte(s), cfg))
+ require.NoError(t, n.ApplyConfig(cfg))
+
+ // Reload with target groups for both configs - same alertmanager URL for both.
+ tgs = map[string][]*targetgroup.Group{
+ "config-0": {targetGroup},
+ "config-1": {targetGroup},
+ }
+ n.reload(tgs)
+
+ // Both alertmanagerSets should have independent sendLoops.
+ sendLoops0 := n.alertmanagers["config-0"].sendLoops
+ sendLoops1 := n.alertmanagers["config-1"].sendLoops
+
+ require.Len(t, sendLoops0, 1, "config-0 should have sendLoops")
+ require.Len(t, sendLoops1, 1, "config-1 should have sendLoops")
+
+ // Verify that the two alertmanagerSets have INDEPENDENT sendLoops maps.
+ // They should NOT share the same sendLoop objects.
+ for k := range sendLoops0 {
+ if loop1, ok := sendLoops1[k]; ok {
+ require.NotSame(t, sendLoops0[k], loop1,
+ "config-0 and config-1 should have independent sendLoop instances, not shared references")
+ }
+ }
+}
+
+// TestApplyConfigHashChangeLeaksSendLoops tests a bug where sendLoops goroutines
+// are leaked when the config key remains the same but the config hash changes.
+//
+// Bug scenario:
+// 1. Old config has "config-0" with hash H1 and running sendLoops
+// 2. New config has "config-0" with hash H2 (modified config)
+// 3. Since hash differs, sendLoops are NOT transferred to the new alertmanagerSet
+// 4. Cleanup only checks if key exists in amSets - it does, so no cleanup
+// 5. Old sendLoops goroutines continue running and are never stopped
+func TestApplyConfigHashChangeLeaksSendLoops(t *testing.T) {
+ n := NewManager(&Options{QueueCapacity: 10}, model.UTF8Validation, nil)
+ cfg := &config.Config{}
+
+ // Initial config with one alertmanager.
+ s := `
+alerting:
+ alertmanagers:
+ - file_sd_configs:
+ - files:
+ - foo.json
+`
+ require.NoError(t, yaml.UnmarshalStrict([]byte(s), cfg))
+ require.NoError(t, n.ApplyConfig(cfg))
+
+ targetGroup := &targetgroup.Group{
+ Targets: []model.LabelSet{
+ {"__address__": "alertmanager:9093"},
+ },
+ }
+ tgs := map[string][]*targetgroup.Group{"config-0": {targetGroup}}
+ n.reload(tgs)
+
+ // Capture the old sendLoop.
+ oldSendLoops := n.alertmanagers["config-0"].sendLoops
+ require.Len(t, oldSendLoops, 1)
+ var oldSendLoop *sendLoop
+ for _, sl := range oldSendLoops {
+ oldSendLoop = sl
+ }
+
+ // Apply a new config with DIFFERENT hash (added path_prefix).
+ s = `
+alerting:
+ alertmanagers:
+ - file_sd_configs:
+ - files:
+ - foo.json
+ path_prefix: /changed
+`
+ require.NoError(t, yaml.UnmarshalStrict([]byte(s), cfg))
+ require.NoError(t, n.ApplyConfig(cfg))
+
+ // The old sendLoop should have been stopped since hash changed.
+ // Check that the stopped channel is closed.
+ select {
+ case <-oldSendLoop.stopped:
+ // Good - sendLoop was properly stopped
+ default:
+ t.Fatal("BUG: old sendLoop was not stopped when config hash changed - goroutine leak")
+ }
+}
+
+func newBlackHoleAlertmanager(stop <-chan struct{}) *httptest.Server {
+ return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+ // Do nothing, wait to be canceled.
+ <-stop
+ w.WriteHeader(http.StatusOK)
+ }))
+}
+
+func newImmediateAlertManager(done chan<- struct{}) *httptest.Server {
+ return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ close(done)
+ }))
+}
diff --git a/notifier/metric.go b/notifier/metric.go
index d10a02614c..a150331ab1 100644
--- a/notifier/metric.go
+++ b/notifier/metric.go
@@ -24,17 +24,13 @@ type alertMetrics struct {
latencyHistogram *prometheus.HistogramVec
errors *prometheus.CounterVec
sent *prometheus.CounterVec
- dropped prometheus.Counter
- queueLength prometheus.GaugeFunc
+ dropped *prometheus.CounterVec
+ queueLength *prometheus.GaugeVec
queueCapacity prometheus.Gauge
alertmanagersDiscovered prometheus.GaugeFunc
}
-func newAlertMetrics(
- r prometheus.Registerer,
- queueCap int,
- queueLen, alertmanagersDiscovered func() float64,
-) *alertMetrics {
+func newAlertMetrics(r prometheus.Registerer, alertmanagersDiscovered func() float64) *alertMetrics {
m := &alertMetrics{
latencySummary: prometheus.NewSummaryVec(prometheus.SummaryOpts{
Namespace: namespace,
@@ -74,18 +70,18 @@ func newAlertMetrics(
},
[]string{alertmanagerLabel},
),
- dropped: prometheus.NewCounter(prometheus.CounterOpts{
+ dropped: prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "dropped_total",
Help: "Total number of alerts dropped due to errors when sending to Alertmanager.",
- }),
- queueLength: prometheus.NewGaugeFunc(prometheus.GaugeOpts{
+ }, []string{alertmanagerLabel}),
+ queueLength: prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "queue_length",
Help: "The number of alert notifications in the queue.",
- }, queueLen),
+ }, []string{alertmanagerLabel}),
queueCapacity: prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: namespace,
Subsystem: subsystem,
@@ -98,8 +94,6 @@ func newAlertMetrics(
}, alertmanagersDiscovered),
}
- m.queueCapacity.Set(float64(queueCap))
-
if r != nil {
r.MustRegister(
m.latencySummary,
diff --git a/notifier/sendloop.go b/notifier/sendloop.go
new file mode 100644
index 0000000000..0413390265
--- /dev/null
+++ b/notifier/sendloop.go
@@ -0,0 +1,273 @@
+// Copyright 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 notifier
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "log/slog"
+ "net/http"
+ "sync"
+ "time"
+
+ "github.com/prometheus/prometheus/config"
+)
+
+type sendLoop struct {
+ alertmanagerURL string
+
+ cfg *config.AlertmanagerConfig
+ client *http.Client
+ opts *Options
+
+ metrics *alertMetrics
+
+ mtx sync.RWMutex
+ queue []*Alert
+ hasWork chan struct{}
+ stopped chan struct{}
+ stopOnce sync.Once
+
+ logger *slog.Logger
+}
+
+func newSendLoop(
+ alertmanagerURL string,
+ client *http.Client,
+ cfg *config.AlertmanagerConfig,
+ opts *Options,
+ logger *slog.Logger,
+ metrics *alertMetrics,
+) *sendLoop {
+ // This will initialize the Counters for the AM to 0 and set the static queue capacity gauge.
+ metrics.dropped.WithLabelValues(alertmanagerURL)
+ metrics.errors.WithLabelValues(alertmanagerURL)
+ metrics.sent.WithLabelValues(alertmanagerURL)
+ metrics.queueLength.WithLabelValues(alertmanagerURL)
+
+ return &sendLoop{
+ alertmanagerURL: alertmanagerURL,
+ client: client,
+ cfg: cfg,
+ opts: opts,
+ logger: logger,
+ metrics: metrics,
+ queue: make([]*Alert, 0, opts.QueueCapacity),
+ hasWork: make(chan struct{}, 1),
+ stopped: make(chan struct{}),
+ }
+}
+
+func (s *sendLoop) add(alerts ...*Alert) {
+ select {
+ case <-s.stopped:
+ return
+ default:
+ }
+
+ s.mtx.Lock()
+ defer s.mtx.Unlock()
+
+ var dropped int
+ // Queue capacity should be significantly larger than a single alert
+ // batch could be.
+ if d := len(alerts) - s.opts.QueueCapacity; d > 0 {
+ s.logger.Warn("Alert batch larger than queue capacity, dropping alerts", "count", d)
+ dropped += d
+ alerts = alerts[d:]
+ }
+
+ // If the queue is full, remove the oldest alerts in favor
+ // of newer ones.
+ if d := (len(s.queue) + len(alerts)) - s.opts.QueueCapacity; d > 0 {
+ s.logger.Warn("Alert notification queue full, dropping alerts", "count", d)
+ dropped += d
+ s.queue = s.queue[d:]
+ }
+
+ s.queue = append(s.queue, alerts...)
+
+ // Notify sending goroutine that there are alerts to be processed.
+ // If we cannot send on the channel, it means the signal already exists
+ // and has not been consumed yet.
+ s.notifyWork()
+
+ s.metrics.queueLength.WithLabelValues(s.alertmanagerURL).Set(float64(len(s.queue)))
+ if dropped > 0 {
+ s.metrics.dropped.WithLabelValues(s.alertmanagerURL).Add(float64(dropped))
+ }
+}
+
+func (s *sendLoop) notifyWork() {
+ select {
+ case <-s.stopped:
+ return
+ case s.hasWork <- struct{}{}:
+ default:
+ }
+}
+
+func (s *sendLoop) stop() {
+ s.stopOnce.Do(func() {
+ s.logger.Debug("Stopping send loop")
+ close(s.stopped)
+
+ if s.opts.DrainOnShutdown {
+ s.drainQueue()
+ } else {
+ ql := s.queueLen()
+ s.logger.Warn("Alert notification queue not drained on shutdown, dropping alerts", "count", ql)
+ s.metrics.dropped.WithLabelValues(s.alertmanagerURL).Add(float64(ql))
+ }
+
+ s.metrics.latencySummary.DeleteLabelValues(s.alertmanagerURL)
+ s.metrics.latencyHistogram.DeleteLabelValues(s.alertmanagerURL)
+ s.metrics.sent.DeleteLabelValues(s.alertmanagerURL)
+ s.metrics.dropped.DeleteLabelValues(s.alertmanagerURL)
+ s.metrics.errors.DeleteLabelValues(s.alertmanagerURL)
+ s.metrics.queueLength.DeleteLabelValues(s.alertmanagerURL)
+ })
+}
+
+func (s *sendLoop) drainQueue() {
+ for s.queueLen() > 0 {
+ s.sendOneBatch()
+ }
+}
+
+func (s *sendLoop) queueLen() int {
+ s.mtx.RLock()
+ defer s.mtx.RUnlock()
+
+ return len(s.queue)
+}
+
+func (s *sendLoop) nextBatch() []*Alert {
+ s.mtx.Lock()
+ defer s.mtx.Unlock()
+
+ var alerts []*Alert
+ if maxBatchSize := s.opts.MaxBatchSize; len(s.queue) > maxBatchSize {
+ alerts = append(make([]*Alert, 0, maxBatchSize), s.queue[:maxBatchSize]...)
+ s.queue = s.queue[maxBatchSize:]
+ } else {
+ alerts = append(make([]*Alert, 0, len(s.queue)), s.queue...)
+ s.queue = s.queue[:0]
+ }
+ s.metrics.queueLength.WithLabelValues(s.alertmanagerURL).Set(float64(len(s.queue)))
+
+ return alerts
+}
+
+func (s *sendLoop) sendOneBatch() {
+ alerts := s.nextBatch()
+
+ if !s.sendAll(alerts) {
+ s.metrics.dropped.WithLabelValues(s.alertmanagerURL).Add(float64(len(alerts)))
+ }
+}
+
+// loop continuously consumes the notifications queue and sends alerts to
+// the Alertmanager.
+func (s *sendLoop) loop() {
+ s.logger.Debug("Starting send loop")
+ for {
+ // If we've been asked to stop, that takes priority over sending any further notifications.
+ select {
+ case <-s.stopped:
+ return
+ default:
+ select {
+ case <-s.stopped:
+ return
+ case <-s.hasWork:
+ s.sendOneBatch()
+
+ // If the queue still has items left, kick off the next iteration.
+ if s.queueLen() > 0 {
+ s.notifyWork()
+ }
+ }
+ }
+ }
+}
+
+func (s *sendLoop) sendAll(alerts []*Alert) bool {
+ if len(alerts) == 0 {
+ return true
+ }
+
+ begin := time.Now()
+
+ var payload []byte
+ var err error
+ switch s.cfg.APIVersion {
+ case config.AlertmanagerAPIVersionV2:
+ openAPIAlerts := alertsToOpenAPIAlerts(alerts)
+ payload, err = json.Marshal(openAPIAlerts)
+ if err != nil {
+ s.logger.Error("Encoding alerts for Alertmanager API v2 failed", "err", err)
+ return false
+ }
+
+ default:
+ s.logger.Error(
+ fmt.Sprintf("Invalid Alertmanager API version '%v', expected one of '%v'", s.cfg.APIVersion, config.SupportedAlertmanagerAPIVersions),
+ "err", err,
+ )
+ return false
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), time.Duration(s.cfg.Timeout))
+ defer cancel()
+
+ if err := s.sendOne(ctx, s.client, s.alertmanagerURL, payload); err != nil {
+ s.logger.Error("Error sending alerts", "count", len(alerts), "err", err)
+ s.metrics.errors.WithLabelValues(s.alertmanagerURL).Add(float64(len(alerts)))
+ return false
+ }
+ durationSeconds := time.Since(begin).Seconds()
+ s.metrics.latencySummary.WithLabelValues(s.alertmanagerURL).Observe(durationSeconds)
+ s.metrics.latencyHistogram.WithLabelValues(s.alertmanagerURL).Observe(durationSeconds)
+ s.metrics.sent.WithLabelValues(s.alertmanagerURL).Add(float64(len(alerts)))
+
+ return true
+}
+
+func (s *sendLoop) sendOne(ctx context.Context, c *http.Client, url string, b []byte) error {
+ req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(b))
+ if err != nil {
+ return err
+ }
+ req.Header.Set("User-Agent", userAgent)
+ req.Header.Set("Content-Type", contentTypeJSON)
+ resp, err := s.opts.Do(ctx, c, req)
+ if err != nil {
+ return err
+ }
+ defer func() {
+ io.Copy(io.Discard, resp.Body)
+ resp.Body.Close()
+ }()
+
+ // Any HTTP status 2xx is OK.
+ if resp.StatusCode/100 != 2 {
+ return fmt.Errorf("bad response status %s", resp.Status)
+ }
+
+ return nil
+}
diff --git a/notifier/sendloop_test.go b/notifier/sendloop_test.go
new file mode 100644
index 0000000000..1e04c0d9a0
--- /dev/null
+++ b/notifier/sendloop_test.go
@@ -0,0 +1,187 @@
+// Copyright 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 notifier
+
+import (
+ "bytes"
+ "context"
+ "io"
+ "log/slog"
+ "net/http"
+ "strconv"
+ "testing"
+
+ "github.com/prometheus/client_golang/prometheus"
+ prom_testutil "github.com/prometheus/client_golang/prometheus/testutil"
+ "github.com/stretchr/testify/require"
+
+ "github.com/prometheus/prometheus/config"
+ "github.com/prometheus/prometheus/model/labels"
+)
+
+func TestCustomDo(t *testing.T) {
+ const testURL = "http://testurl.com/"
+ const testBody = "testbody"
+
+ var received bool
+ h := sendLoop{
+ opts: &Options{
+ Do: func(_ context.Context, _ *http.Client, req *http.Request) (*http.Response, error) {
+ received = true
+ body, err := io.ReadAll(req.Body)
+
+ require.NoError(t, err)
+
+ require.Equal(t, testBody, string(body))
+
+ require.Equal(t, testURL, req.URL.String())
+
+ return &http.Response{
+ Body: io.NopCloser(bytes.NewBuffer(nil)),
+ }, nil
+ },
+ },
+ }
+
+ h.sendOne(context.Background(), nil, testURL, []byte(testBody))
+
+ require.True(t, received)
+}
+
+func TestHandlerNextBatch(t *testing.T) {
+ sendLoop := newSendLoop("http://mock", nil, &config.DefaultAlertmanagerConfig, &Options{MaxBatchSize: DefaultMaxBatchSize}, slog.New(slog.DiscardHandler), newAlertMetrics(prometheus.NewRegistry(), nil))
+
+ for i := range make([]struct{}, 2*DefaultMaxBatchSize+1) {
+ sendLoop.queue = append(sendLoop.queue, &Alert{
+ Labels: labels.FromStrings("alertname", strconv.Itoa(i)),
+ })
+ }
+ expected := append([]*Alert{}, sendLoop.queue...)
+
+ require.NoError(t, alertsEqual(expected[0:DefaultMaxBatchSize], sendLoop.nextBatch()))
+ require.NoError(t, alertsEqual(expected[DefaultMaxBatchSize:2*DefaultMaxBatchSize], sendLoop.nextBatch()))
+ require.NoError(t, alertsEqual(expected[2*DefaultMaxBatchSize:], sendLoop.nextBatch()))
+ require.Empty(t, sendLoop.queue)
+}
+
+func TestAddAlertsToQueue(t *testing.T) {
+ alert1 := &Alert{Labels: labels.FromStrings("alertname", "existing1")}
+ alert2 := &Alert{Labels: labels.FromStrings("alertname", "existing2")}
+
+ s := newSendLoop("http://foo.bar/", nil, nil, &Options{QueueCapacity: 5}, slog.New(slog.DiscardHandler), newAlertMetrics(prometheus.NewRegistry(), nil))
+ s.add(alert1, alert2)
+ require.Equal(t, []*Alert{alert1, alert2}, s.queue)
+ require.Len(t, s.queue, 2)
+
+ alert3 := &Alert{Labels: labels.FromStrings("alertname", "new1")}
+ alert4 := &Alert{Labels: labels.FromStrings("alertname", "new2")}
+
+ // Add new alerts to the queue, expect 0 dropped
+ s.add(alert3, alert4)
+ require.Zero(t, prom_testutil.ToFloat64(s.metrics.dropped.WithLabelValues(s.alertmanagerURL)))
+
+ // Verify all new alerts were added to the queue
+ require.Equal(t, []*Alert{alert1, alert2, alert3, alert4}, s.queue)
+ require.Len(t, s.queue, 4)
+}
+
+func TestAddAlertsToQueueExceedingCapacity(t *testing.T) {
+ alert1 := &Alert{Labels: labels.FromStrings("alertname", "alert1")}
+ alert2 := &Alert{Labels: labels.FromStrings("alertname", "alert2")}
+
+ s := newSendLoop("http://foo.bar/", nil, nil, &Options{QueueCapacity: 3}, slog.New(slog.DiscardHandler), newAlertMetrics(prometheus.NewRegistry(), nil))
+ s.add(alert1, alert2)
+
+ alert3 := &Alert{Labels: labels.FromStrings("alertname", "alert3")}
+ alert4 := &Alert{Labels: labels.FromStrings("alertname", "alert4")}
+
+ // Add new alerts to queue, expect 1 dropped
+ s.add(alert3, alert4)
+ require.Equal(t, 1.0, prom_testutil.ToFloat64(s.metrics.dropped.WithLabelValues(s.alertmanagerURL)))
+
+ // Verify all new alerts were added to the queue
+ require.Equal(t, []*Alert{alert2, alert3, alert4}, s.queue)
+}
+
+func TestAddAlertsToQueueExceedingTotalCapacity(t *testing.T) {
+ alert1 := &Alert{Labels: labels.FromStrings("alertname", "alert1")}
+ alert2 := &Alert{Labels: labels.FromStrings("alertname", "alert2")}
+
+ s := newSendLoop("http://foo.bar/", nil, nil, &Options{QueueCapacity: 3}, slog.New(slog.DiscardHandler), newAlertMetrics(prometheus.NewRegistry(), nil))
+ s.add(alert1, alert2)
+
+ alert3 := &Alert{Labels: labels.FromStrings("alertname", "alert3")}
+ alert4 := &Alert{Labels: labels.FromStrings("alertname", "alert4")}
+ alert5 := &Alert{Labels: labels.FromStrings("alertname", "alert5")}
+ alert6 := &Alert{Labels: labels.FromStrings("alertname", "alert6")}
+
+ // Add new alerts to queue, expect 3 dropped: 1 from new batch + 2 from existing queued items
+ s.add(alert3, alert4, alert5, alert6)
+ require.Equal(t, 3.0, prom_testutil.ToFloat64(s.metrics.dropped.WithLabelValues(s.alertmanagerURL)))
+
+ // Verify all new alerts were added to the queue
+ require.Equal(t, []*Alert{alert4, alert5, alert6}, s.queue)
+}
+
+func TestNextBatchAlertsFromQueue(t *testing.T) {
+ s := newSendLoop("http://foo.bar/", nil, nil, &Options{QueueCapacity: 5, MaxBatchSize: 3}, slog.New(slog.DiscardHandler), newAlertMetrics(prometheus.NewRegistry(), nil))
+
+ alert1 := &Alert{Labels: labels.FromStrings("alertname", "alert1")}
+ alert2 := &Alert{Labels: labels.FromStrings("alertname", "alert2")}
+ alert3 := &Alert{Labels: labels.FromStrings("alertname", "alert3")}
+ s.add(alert1, alert2, alert3)
+
+ // Test batch-size alerts in the queue
+ require.Equal(t, []*Alert{alert1, alert2, alert3}, s.nextBatch())
+ require.Empty(t, s.nextBatch())
+
+ // Test full queue
+ alert4 := &Alert{Labels: labels.FromStrings("alertname", "alert4")}
+ alert5 := &Alert{Labels: labels.FromStrings("alertname", "alert5")}
+ s.add(alert1, alert2, alert3, alert4, alert5)
+ require.Equal(t, []*Alert{alert1, alert2, alert3}, s.nextBatch())
+ require.Equal(t, []*Alert{alert4, alert5}, s.nextBatch())
+ require.Empty(t, s.nextBatch())
+}
+
+func TestMetrics(t *testing.T) {
+ const alertmanagerURL = "http://alertmanager:9093"
+
+ // Use a single registry throughout the test - this is critical to catch registry conflicts
+ reg := prometheus.NewRegistry()
+ alertmanagersDiscoveredFunc := func() float64 { return 0 }
+ metrics := newAlertMetrics(reg, alertmanagersDiscoveredFunc)
+
+ logger := slog.New(slog.DiscardHandler)
+ opts := &Options{QueueCapacity: 10, MaxBatchSize: DefaultMaxBatchSize}
+
+ // Create first sendLoop - this initializes metrics with the alertmanager URL label
+ sendLoop1 := newSendLoop(alertmanagerURL, nil, &config.DefaultAlertmanagerConfig, opts, logger, metrics)
+
+ // Verify metrics are initialized
+ require.Equal(t, 0.0, prom_testutil.ToFloat64(metrics.dropped.WithLabelValues(alertmanagerURL)))
+ require.Equal(t, 0.0, prom_testutil.ToFloat64(metrics.sent.WithLabelValues(alertmanagerURL)))
+
+ // Stop the sendLoop - this should clean up all metrics
+ sendLoop1.stop()
+
+ // Create second sendLoop with the same URL - this should NOT panic or conflict
+ // because metrics were properly cleaned up
+ sendLoop2 := newSendLoop(alertmanagerURL, nil, &config.DefaultAlertmanagerConfig, opts, logger, metrics)
+ defer sendLoop2.stop()
+
+ // Verify metrics are re-initialized correctly
+ require.Equal(t, 0.0, prom_testutil.ToFloat64(metrics.dropped.WithLabelValues(alertmanagerURL)))
+ require.Equal(t, 0.0, prom_testutil.ToFloat64(metrics.sent.WithLabelValues(alertmanagerURL)))
+}
diff --git a/notifier/util_test.go b/notifier/util_test.go
index a9f0509ba1..78f45ba85c 100644
--- a/notifier/util_test.go
+++ b/notifier/util_test.go
@@ -15,6 +15,7 @@ package notifier
import (
"testing"
+ "time"
"github.com/prometheus/alertmanager/api/v2/models"
"github.com/stretchr/testify/require"
@@ -25,3 +26,99 @@ import (
func TestLabelsToOpenAPILabelSet(t *testing.T) {
require.Equal(t, models.LabelSet{"aaa": "111", "bbb": "222"}, labelsToOpenAPILabelSet(labels.FromStrings("aaa", "111", "bbb", "222")))
}
+
+// Edge case tests for utility functions
+
+func TestLabelsToOpenAPILabelSetEmpty(t *testing.T) {
+ result := labelsToOpenAPILabelSet(labels.EmptyLabels())
+ require.Empty(t, result)
+}
+
+func TestLabelsToOpenAPILabelSetSpecialCharacters(t *testing.T) {
+ result := labelsToOpenAPILabelSet(labels.FromStrings(
+ "special/chars", "value with spaces",
+ "unicode", "αβγ",
+ "empty", "",
+ ))
+
+ expected := models.LabelSet{
+ "special/chars": "value with spaces",
+ "unicode": "αβγ",
+ "empty": "",
+ }
+ require.Equal(t, expected, result)
+}
+
+func TestAlertsToOpenAPIAlertsEmpty(t *testing.T) {
+ result := alertsToOpenAPIAlerts([]*Alert{})
+ require.Empty(t, result)
+}
+
+func TestAlertsToOpenAPIAlertsNil(t *testing.T) {
+ result := alertsToOpenAPIAlerts(nil)
+ require.Empty(t, result)
+}
+
+func TestAlertsToOpenAPIAlertsSingle(t *testing.T) {
+ now := time.Now()
+ alert := &Alert{
+ Labels: labels.FromStrings("alertname", "test", "severity", "critical"),
+ Annotations: labels.FromStrings("summary", "Test alert"),
+ StartsAt: now,
+ EndsAt: now.Add(time.Hour),
+ GeneratorURL: "http://prometheus:9090/graph",
+ }
+
+ result := alertsToOpenAPIAlerts([]*Alert{alert})
+ require.Len(t, result, 1)
+
+ apiAlert := result[0]
+ require.Equal(t, "test", apiAlert.Labels["alertname"])
+ require.Equal(t, "critical", apiAlert.Labels["severity"])
+ require.Equal(t, "Test alert", apiAlert.Annotations["summary"])
+ require.Equal(t, "http://prometheus:9090/graph", string(apiAlert.GeneratorURL))
+}
+
+func TestAlertsToOpenAPIAlertsMultiple(t *testing.T) {
+ now := time.Now()
+ alerts := []*Alert{
+ {
+ Labels: labels.FromStrings("alertname", "alert1"),
+ Annotations: labels.FromStrings("desc", "First alert"),
+ StartsAt: now,
+ EndsAt: now.Add(time.Hour),
+ },
+ {
+ Labels: labels.FromStrings("alertname", "alert2"),
+ Annotations: labels.FromStrings("desc", "Second alert"),
+ StartsAt: now.Add(time.Minute),
+ EndsAt: now.Add(2 * time.Hour),
+ },
+ }
+
+ result := alertsToOpenAPIAlerts(alerts)
+ require.Len(t, result, 2)
+
+ require.Equal(t, "alert1", result[0].Labels["alertname"])
+ require.Equal(t, "alert2", result[1].Labels["alertname"])
+ require.Equal(t, "First alert", result[0].Annotations["desc"])
+ require.Equal(t, "Second alert", result[1].Annotations["desc"])
+}
+
+func TestAlertsToOpenAPIAlertsEmptyFields(t *testing.T) {
+ alert := &Alert{
+ Labels: labels.EmptyLabels(),
+ Annotations: labels.EmptyLabels(),
+ StartsAt: time.Time{},
+ EndsAt: time.Time{},
+ GeneratorURL: "",
+ }
+
+ result := alertsToOpenAPIAlerts([]*Alert{alert})
+ require.Len(t, result, 1)
+
+ apiAlert := result[0]
+ require.Empty(t, apiAlert.Labels)
+ require.Empty(t, apiAlert.Annotations)
+ require.Empty(t, string(apiAlert.GeneratorURL))
+}
diff --git a/promql/engine.go b/promql/engine.go
index 11a7ad22ec..b609dc4f0a 100644
--- a/promql/engine.go
+++ b/promql/engine.go
@@ -2862,7 +2862,8 @@ func (ev *evaluator) VectorBinop(op parser.ItemType, lhs, rhs Vector, matching *
if matching.Card == parser.CardManyToMany {
panic("many-to-many only allowed for set operators")
}
- if len(lhs) == 0 || len(rhs) == 0 {
+ if (len(lhs) == 0 && len(rhs) == 0) ||
+ ((len(lhs) == 0 || len(rhs) == 0) && matching.FillValues.RHS == nil && matching.FillValues.LHS == nil) {
return nil, nil // Short-circuit: nothing is going to match.
}
@@ -2910,17 +2911,9 @@ func (ev *evaluator) VectorBinop(op parser.ItemType, lhs, rhs Vector, matching *
}
matchedSigs := enh.matchedSigs
- // For all lhs samples find a respective rhs sample and perform
- // the binary operation.
var lastErr error
- for i, ls := range lhs {
- sigOrd := lhsh[i].sigOrdinal
-
- rs, found := rightSigs[sigOrd] // Look for a match in the rhs Vector.
- if !found {
- continue
- }
+ doBinOp := func(ls, rs Sample, sigOrd int) {
// Account for potentially swapped sidedness.
fl, fr := ls.F, rs.F
hl, hr := ls.H, rs.H
@@ -2931,7 +2924,7 @@ func (ev *evaluator) VectorBinop(op parser.ItemType, lhs, rhs Vector, matching *
floatValue, histogramValue, keep, info, err := vectorElemBinop(op, fl, fr, hl, hr, pos)
if err != nil {
lastErr = err
- continue
+ return
}
if info != nil {
lastErr = info
@@ -2971,7 +2964,7 @@ func (ev *evaluator) VectorBinop(op parser.ItemType, lhs, rhs Vector, matching *
}
if !keep && !returnBool {
- continue
+ return
}
enh.Out = append(enh.Out, Sample{
@@ -2981,6 +2974,43 @@ func (ev *evaluator) VectorBinop(op parser.ItemType, lhs, rhs Vector, matching *
DropName: returnBool,
})
}
+
+ // For all lhs samples, find a respective rhs sample and perform
+ // the binary operation.
+ for i, ls := range lhs {
+ sigOrd := lhsh[i].sigOrdinal
+
+ rs, found := rightSigs[sigOrd] // Look for a match in the rhs Vector.
+ if !found {
+ fill := matching.FillValues.RHS
+ if fill == nil {
+ continue
+ }
+ rs = Sample{
+ Metric: ls.Metric.MatchLabels(matching.On, matching.MatchingLabels...),
+ F: *fill,
+ }
+ }
+
+ doBinOp(ls, rs, sigOrd)
+ }
+
+ // For any rhs samples which have not been matched, check if we need to
+ // perform the operation with a fill value from the lhs.
+ if fill := matching.FillValues.LHS; fill != nil {
+ for sigOrd, rs := range rightSigs {
+ if _, matched := matchedSigs[sigOrd]; matched {
+ continue // Already matched.
+ }
+ ls := Sample{
+ Metric: rs.Metric.MatchLabels(matching.On, matching.MatchingLabels...),
+ F: *fill,
+ }
+
+ doBinOp(ls, rs, sigOrd)
+ }
+ }
+
return enh.Out, lastErr
}
@@ -4418,9 +4448,9 @@ func extendFloats(floats []FPoint, mint, maxt int64, smoothed bool) []FPoint {
lastSampleIndex--
}
- // TODO: Preallocate the length of the new list.
- out := make([]FPoint, 0)
- // Create the new floats list with the boundary samples and the inner samples.
+ count := max(lastSampleIndex-firstSampleIndex+1, 0)
+ out := make([]FPoint, 0, count+2)
+
out = append(out, FPoint{T: mint, F: left})
out = append(out, floats[firstSampleIndex:lastSampleIndex+1]...)
out = append(out, FPoint{T: maxt, F: right})
diff --git a/promql/engine_test.go b/promql/engine_test.go
index 7b7a67a54b..0eff93af4c 100644
--- a/promql/engine_test.go
+++ b/promql/engine_test.go
@@ -3747,12 +3747,12 @@ func TestHistogramRateWithFloatStaleness(t *testing.T) {
recoded bool
)
- newc, recoded, app, err = app.AppendHistogram(nil, 0, h1.Copy(), false)
+ newc, recoded, app, err = app.AppendHistogram(nil, 0, 0, h1.Copy(), false)
require.NoError(t, err)
require.False(t, recoded)
require.Nil(t, newc)
- newc, recoded, _, err = app.AppendHistogram(nil, 10, h1.Copy(), false)
+ newc, recoded, _, err = app.AppendHistogram(nil, 0, 10, h1.Copy(), false)
require.NoError(t, err)
require.False(t, recoded)
require.Nil(t, newc)
@@ -3762,7 +3762,7 @@ func TestHistogramRateWithFloatStaleness(t *testing.T) {
app, err = c2.Appender()
require.NoError(t, err)
- app.Append(20, math.Float64frombits(value.StaleNaN))
+ app.Append(0, 20, math.Float64frombits(value.StaleNaN))
// Make a chunk with two normal histograms that have zero value.
h2 := histogram.Histogram{
@@ -3773,12 +3773,12 @@ func TestHistogramRateWithFloatStaleness(t *testing.T) {
app, err = c3.Appender()
require.NoError(t, err)
- newc, recoded, app, err = app.AppendHistogram(nil, 30, h2.Copy(), false)
+ newc, recoded, app, err = app.AppendHistogram(nil, 0, 30, h2.Copy(), false)
require.NoError(t, err)
require.False(t, recoded)
require.Nil(t, newc)
- newc, recoded, _, err = app.AppendHistogram(nil, 40, h2.Copy(), false)
+ newc, recoded, _, err = app.AppendHistogram(nil, 0, 40, h2.Copy(), false)
require.NoError(t, err)
require.False(t, recoded)
require.Nil(t, newc)
diff --git a/promql/histogram_stats_iterator_test.go b/promql/histogram_stats_iterator_test.go
index cfea8a568e..d3a76820da 100644
--- a/promql/histogram_stats_iterator_test.go
+++ b/promql/histogram_stats_iterator_test.go
@@ -235,4 +235,6 @@ func (h *histogramIterator) AtFloatHistogram(*histogram.FloatHistogram) (int64,
func (*histogramIterator) AtT() int64 { return 0 }
+func (*histogramIterator) AtST() int64 { return 0 }
+
func (*histogramIterator) Err() error { return nil }
diff --git a/promql/info.go b/promql/info.go
index ab4250104d..c5b88e6af3 100644
--- a/promql/info.go
+++ b/promql/info.go
@@ -143,6 +143,23 @@ func (ev *evaluator) fetchInfoSeries(ctx context.Context, mat Matrix, ignoreSeri
}
}
if len(idLblValues) == 0 {
+ // Even when returning early, we need to remove __name__ from dataLabelMatchers
+ // since it's not a data label selector (it's used to select which info metrics
+ // to consider). Without this, combineWithInfoVector would incorrectly exclude
+ // series when only __name__ is specified in the selector.
+ for name, ms := range dataLabelMatchers {
+ for i, m := range ms {
+ if m.Name == labels.MetricName {
+ ms = slices.Delete(ms, i, i+1)
+ break
+ }
+ }
+ if len(ms) > 0 {
+ dataLabelMatchers[name] = ms
+ } else {
+ delete(dataLabelMatchers, name)
+ }
+ }
return nil, nil, nil
}
@@ -424,9 +441,10 @@ func (ev *evaluator) combineWithInfoVector(base, info Vector, ignoreSeries map[u
}
infoLbls := enh.lb.Labels()
- if infoLbls.Len() == 0 {
- // If there's at least one data label matcher not matching the empty string,
- // we have to ignore this series as there are no matching info series.
+ if len(seenInfoMetrics) == 0 {
+ // No info series matched this base series. If there's at least one data
+ // label matcher not matching the empty string, we have to ignore this
+ // series as there are no matching info series.
allMatchersMatchEmpty := true
for _, ms := range dataLabelMatchers {
for _, m := range ms {
diff --git a/promql/parser/ast.go b/promql/parser/ast.go
index 130f9aefb7..6496095287 100644
--- a/promql/parser/ast.go
+++ b/promql/parser/ast.go
@@ -318,6 +318,19 @@ type VectorMatching struct {
// Include contains additional labels that should be included in
// the result from the side with the lower cardinality.
Include []string
+ // Fill-in values to use when a series from one side does not find a match on the other side.
+ FillValues VectorMatchFillValues
+}
+
+// VectorMatchFillValues contains the fill values to use for Vector matching
+// when one side does not find a match on the other side.
+// When a fill value is nil, no fill is applied for that side, and there
+// is no output for the match group if there is no match.
+type VectorMatchFillValues struct {
+ // RHS is the fill value to use for the right-hand side.
+ RHS *float64
+ // LHS is the fill value to use for the left-hand side.
+ LHS *float64
}
// Visitor allows visiting a Node and its child nodes. The Visit method is
diff --git a/promql/parser/features.go b/promql/parser/features.go
index ec64678237..0df30e75c3 100644
--- a/promql/parser/features.go
+++ b/promql/parser/features.go
@@ -26,6 +26,8 @@ func RegisterFeatures(r features.Collector) {
switch keyword {
case "anchored", "smoothed":
r.Set(features.PromQL, keyword, EnableExtendedRangeSelectors)
+ case "fill", "fill_left", "fill_right":
+ r.Set(features.PromQL, keyword, EnableBinopFillModifiers)
default:
r.Enable(features.PromQL, keyword)
}
diff --git a/promql/parser/generated_parser.y b/promql/parser/generated_parser.y
index 47776f53d0..6e336e230b 100644
--- a/promql/parser/generated_parser.y
+++ b/promql/parser/generated_parser.y
@@ -139,6 +139,9 @@ BOOL
BY
GROUP_LEFT
GROUP_RIGHT
+FILL
+FILL_LEFT
+FILL_RIGHT
IGNORING
OFFSET
SMOOTHED
@@ -190,7 +193,7 @@ START_METRIC_SELECTOR
%type int
%type uint
%type number series_value signed_number signed_or_unsigned_number
-%type step_invariant_expr aggregate_expr aggregate_modifier bin_modifier binary_expr bool_modifier expr function_call function_call_args function_call_body group_modifiers label_matchers matrix_selector number_duration_literal offset_expr anchored_expr smoothed_expr on_or_ignoring paren_expr string_literal subquery_expr unary_expr vector_selector duration_expr paren_duration_expr positive_duration_expr offset_duration_expr
+%type step_invariant_expr aggregate_expr aggregate_modifier bin_modifier fill_modifiers binary_expr bool_modifier expr function_call function_call_args function_call_body group_modifiers fill_value label_matchers matrix_selector number_duration_literal offset_expr anchored_expr smoothed_expr on_or_ignoring paren_expr string_literal subquery_expr unary_expr vector_selector duration_expr paren_duration_expr positive_duration_expr offset_duration_expr
%start start
@@ -302,7 +305,7 @@ binary_expr : expr ADD bin_modifier expr { $$ = yylex.(*parser).newBinar
// Using left recursion for the modifier rules, helps to keep the parser stack small and
// reduces allocations.
-bin_modifier : group_modifiers;
+bin_modifier : fill_modifiers;
bool_modifier : /* empty */
{ $$ = &BinaryExpr{
@@ -346,6 +349,47 @@ group_modifiers: bool_modifier /* empty */
}
;
+fill_modifiers: group_modifiers /* empty */
+ /* Only fill() */
+ | group_modifiers FILL fill_value
+ {
+ $$ = $1
+ fill := $3.(*NumberLiteral).Val
+ $$.(*BinaryExpr).VectorMatching.FillValues.LHS = &fill
+ $$.(*BinaryExpr).VectorMatching.FillValues.RHS = &fill
+ }
+ /* Only fill_left() */
+ | group_modifiers FILL_LEFT fill_value
+ {
+ $$ = $1
+ fill := $3.(*NumberLiteral).Val
+ $$.(*BinaryExpr).VectorMatching.FillValues.LHS = &fill
+ }
+ /* Only fill_right() */
+ | group_modifiers FILL_RIGHT fill_value
+ {
+ $$ = $1
+ fill := $3.(*NumberLiteral).Val
+ $$.(*BinaryExpr).VectorMatching.FillValues.RHS = &fill
+ }
+ /* fill_left() fill_right() */
+ | group_modifiers FILL_LEFT fill_value FILL_RIGHT fill_value
+ {
+ $$ = $1
+ fill_left := $3.(*NumberLiteral).Val
+ fill_right := $5.(*NumberLiteral).Val
+ $$.(*BinaryExpr).VectorMatching.FillValues.LHS = &fill_left
+ $$.(*BinaryExpr).VectorMatching.FillValues.RHS = &fill_right
+ }
+ /* fill_right() fill_left() */
+ | group_modifiers FILL_RIGHT fill_value FILL_LEFT fill_value
+ {
+ fill_right := $3.(*NumberLiteral).Val
+ fill_left := $5.(*NumberLiteral).Val
+ $$.(*BinaryExpr).VectorMatching.FillValues.LHS = &fill_left
+ $$.(*BinaryExpr).VectorMatching.FillValues.RHS = &fill_right
+ }
+ ;
grouping_labels : LEFT_PAREN grouping_label_list RIGHT_PAREN
{ $$ = $2 }
@@ -387,6 +431,21 @@ grouping_label : maybe_label
{ yylex.(*parser).unexpected("grouping opts", "label"); $$ = Item{} }
;
+fill_value : LEFT_PAREN number_duration_literal RIGHT_PAREN
+ {
+ $$ = $2.(*NumberLiteral)
+ }
+ | LEFT_PAREN unary_op number_duration_literal RIGHT_PAREN
+ {
+ nl := $3.(*NumberLiteral)
+ if $2.Typ == SUB {
+ nl.Val *= -1
+ }
+ nl.PosRange.Start = $2.Pos
+ $$ = nl
+ }
+ ;
+
/*
* Function calls.
*/
@@ -697,7 +756,7 @@ metric : metric_identifier label_set
;
-metric_identifier: AVG | BOTTOMK | BY | COUNT | COUNT_VALUES | GROUP | IDENTIFIER | LAND | LOR | LUNLESS | MAX | METRIC_IDENTIFIER | MIN | OFFSET | QUANTILE | STDDEV | STDVAR | SUM | TOPK | WITHOUT | START | END | LIMITK | LIMIT_RATIO | STEP | RANGE | ANCHORED | SMOOTHED;
+metric_identifier: AVG | BOTTOMK | BY | COUNT | COUNT_VALUES | FILL | FILL_LEFT | FILL_RIGHT | GROUP | IDENTIFIER | LAND | LOR | LUNLESS | MAX | METRIC_IDENTIFIER | MIN | OFFSET | QUANTILE | STDDEV | STDVAR | SUM | TOPK | WITHOUT | START | END | LIMITK | LIMIT_RATIO | STEP | RANGE | ANCHORED | SMOOTHED;
label_set : LEFT_BRACE label_set_list RIGHT_BRACE
{ $$ = labels.New($2...) }
@@ -791,14 +850,15 @@ series_item : BLANK
// Histogram descriptions (part of unit testing).
| histogram_series_value
{
- $$ = []SequenceValue{{Histogram:$1}}
+ $$ = []SequenceValue{yylex.(*parser).newHistogramSequenceValue($1)}
}
| histogram_series_value TIMES uint
{
$$ = []SequenceValue{}
// Add an additional value for time 0, which we ignore in tests.
+ sv := yylex.(*parser).newHistogramSequenceValue($1)
for i:=uint64(0); i <= $3; i++{
- $$ = append($$, SequenceValue{Histogram:$1})
+ $$ = append($$, sv)
//$1 += $2
}
}
@@ -954,7 +1014,7 @@ counter_reset_hint : UNKNOWN_COUNTER_RESET | COUNTER_RESET | NOT_COUNTER_RESET |
aggregate_op : AVG | BOTTOMK | COUNT | COUNT_VALUES | GROUP | MAX | MIN | QUANTILE | STDDEV | STDVAR | SUM | TOPK | LIMITK | LIMIT_RATIO;
// Inside of grouping options label names can be recognized as keywords by the lexer. This is a list of keywords that could also be a label name.
-maybe_label : AVG | BOOL | BOTTOMK | BY | COUNT | COUNT_VALUES | GROUP | GROUP_LEFT | GROUP_RIGHT | IDENTIFIER | IGNORING | LAND | LOR | LUNLESS | MAX | METRIC_IDENTIFIER | MIN | OFFSET | ON | QUANTILE | STDDEV | STDVAR | SUM | TOPK | START | END | ATAN2 | LIMITK | LIMIT_RATIO | STEP | RANGE | ANCHORED | SMOOTHED;
+maybe_label : AVG | BOOL | BOTTOMK | BY | COUNT | COUNT_VALUES | GROUP | GROUP_LEFT | GROUP_RIGHT | FILL | FILL_LEFT | FILL_RIGHT | IDENTIFIER | IGNORING | LAND | LOR | LUNLESS | MAX | METRIC_IDENTIFIER | MIN | OFFSET | ON | QUANTILE | STDDEV | STDVAR | SUM | TOPK | START | END | ATAN2 | LIMITK | LIMIT_RATIO | STEP | RANGE | ANCHORED | SMOOTHED;
unary_op : ADD | SUB;
@@ -1162,7 +1222,7 @@ offset_duration_expr : number_duration_literal
}
| duration_expr
;
-
+
min_max: MIN | MAX ;
duration_expr : number_duration_literal
@@ -1277,14 +1337,14 @@ duration_expr : number_duration_literal
;
paren_duration_expr : LEFT_PAREN duration_expr RIGHT_PAREN
- {
+ {
yylex.(*parser).experimentalDurationExpr($2.(Expr))
if durationExpr, ok := $2.(*DurationExpr); ok {
durationExpr.Wrapped = true
$$ = durationExpr
break
}
- $$ = $2
+ $$ = $2
}
;
diff --git a/promql/parser/generated_parser.y.go b/promql/parser/generated_parser.y.go
index f5feec0b55..4b90d757cf 100644
--- a/promql/parser/generated_parser.y.go
+++ b/promql/parser/generated_parser.y.go
@@ -113,31 +113,34 @@ const BOOL = 57420
const BY = 57421
const GROUP_LEFT = 57422
const GROUP_RIGHT = 57423
-const IGNORING = 57424
-const OFFSET = 57425
-const SMOOTHED = 57426
-const ANCHORED = 57427
-const ON = 57428
-const WITHOUT = 57429
-const keywordsEnd = 57430
-const preprocessorStart = 57431
-const START = 57432
-const END = 57433
-const STEP = 57434
-const RANGE = 57435
-const preprocessorEnd = 57436
-const counterResetHintsStart = 57437
-const UNKNOWN_COUNTER_RESET = 57438
-const COUNTER_RESET = 57439
-const NOT_COUNTER_RESET = 57440
-const GAUGE_TYPE = 57441
-const counterResetHintsEnd = 57442
-const startSymbolsStart = 57443
-const START_METRIC = 57444
-const START_SERIES_DESCRIPTION = 57445
-const START_EXPRESSION = 57446
-const START_METRIC_SELECTOR = 57447
-const startSymbolsEnd = 57448
+const FILL = 57424
+const FILL_LEFT = 57425
+const FILL_RIGHT = 57426
+const IGNORING = 57427
+const OFFSET = 57428
+const SMOOTHED = 57429
+const ANCHORED = 57430
+const ON = 57431
+const WITHOUT = 57432
+const keywordsEnd = 57433
+const preprocessorStart = 57434
+const START = 57435
+const END = 57436
+const STEP = 57437
+const RANGE = 57438
+const preprocessorEnd = 57439
+const counterResetHintsStart = 57440
+const UNKNOWN_COUNTER_RESET = 57441
+const COUNTER_RESET = 57442
+const NOT_COUNTER_RESET = 57443
+const GAUGE_TYPE = 57444
+const counterResetHintsEnd = 57445
+const startSymbolsStart = 57446
+const START_METRIC = 57447
+const START_SERIES_DESCRIPTION = 57448
+const START_EXPRESSION = 57449
+const START_METRIC_SELECTOR = 57450
+const startSymbolsEnd = 57451
var yyToknames = [...]string{
"$end",
@@ -221,6 +224,9 @@ var yyToknames = [...]string{
"BY",
"GROUP_LEFT",
"GROUP_RIGHT",
+ "FILL",
+ "FILL_LEFT",
+ "FILL_RIGHT",
"IGNORING",
"OFFSET",
"SMOOTHED",
@@ -258,376 +264,403 @@ var yyExca = [...]int16{
-1, 1,
1, -1,
-2, 0,
- -1, 41,
- 1, 150,
- 10, 150,
- 24, 150,
+ -1, 44,
+ 1, 161,
+ 10, 161,
+ 24, 161,
-2, 0,
- -1, 72,
- 2, 193,
- 15, 193,
- 79, 193,
- 87, 193,
- -2, 107,
- -1, 73,
- 2, 194,
- 15, 194,
- 79, 194,
- 87, 194,
- -2, 108,
- -1, 74,
- 2, 195,
- 15, 195,
- 79, 195,
- 87, 195,
- -2, 110,
-1, 75,
- 2, 196,
- 15, 196,
- 79, 196,
- 87, 196,
- -2, 111,
- -1, 76,
- 2, 197,
- 15, 197,
- 79, 197,
- 87, 197,
- -2, 112,
- -1, 77,
- 2, 198,
- 15, 198,
- 79, 198,
- 87, 198,
- -2, 117,
- -1, 78,
- 2, 199,
- 15, 199,
- 79, 199,
- 87, 199,
- -2, 119,
- -1, 79,
- 2, 200,
- 15, 200,
- 79, 200,
- 87, 200,
- -2, 121,
- -1, 80,
- 2, 201,
- 15, 201,
- 79, 201,
- 87, 201,
- -2, 122,
- -1, 81,
- 2, 202,
- 15, 202,
- 79, 202,
- 87, 202,
- -2, 123,
- -1, 82,
- 2, 203,
- 15, 203,
- 79, 203,
- 87, 203,
- -2, 124,
- -1, 83,
2, 204,
15, 204,
79, 204,
- 87, 204,
- -2, 125,
- -1, 84,
+ 90, 204,
+ -2, 115,
+ -1, 76,
2, 205,
15, 205,
79, 205,
- 87, 205,
- -2, 129,
- -1, 85,
+ 90, 205,
+ -2, 116,
+ -1, 77,
2, 206,
15, 206,
79, 206,
- 87, 206,
+ 90, 206,
+ -2, 118,
+ -1, 78,
+ 2, 207,
+ 15, 207,
+ 79, 207,
+ 90, 207,
+ -2, 119,
+ -1, 79,
+ 2, 208,
+ 15, 208,
+ 79, 208,
+ 90, 208,
+ -2, 123,
+ -1, 80,
+ 2, 209,
+ 15, 209,
+ 79, 209,
+ 90, 209,
+ -2, 128,
+ -1, 81,
+ 2, 210,
+ 15, 210,
+ 79, 210,
+ 90, 210,
-2, 130,
- -1, 137,
- 41, 274,
- 42, 274,
- 52, 274,
- 53, 274,
- 57, 274,
+ -1, 82,
+ 2, 211,
+ 15, 211,
+ 79, 211,
+ 90, 211,
+ -2, 132,
+ -1, 83,
+ 2, 212,
+ 15, 212,
+ 79, 212,
+ 90, 212,
+ -2, 133,
+ -1, 84,
+ 2, 213,
+ 15, 213,
+ 79, 213,
+ 90, 213,
+ -2, 134,
+ -1, 85,
+ 2, 214,
+ 15, 214,
+ 79, 214,
+ 90, 214,
+ -2, 135,
+ -1, 86,
+ 2, 215,
+ 15, 215,
+ 79, 215,
+ 90, 215,
+ -2, 136,
+ -1, 87,
+ 2, 216,
+ 15, 216,
+ 79, 216,
+ 90, 216,
+ -2, 140,
+ -1, 88,
+ 2, 217,
+ 15, 217,
+ 79, 217,
+ 90, 217,
+ -2, 141,
+ -1, 140,
+ 41, 288,
+ 42, 288,
+ 52, 288,
+ 53, 288,
+ 57, 288,
-2, 22,
- -1, 251,
- 9, 259,
- 12, 259,
- 13, 259,
- 18, 259,
- 19, 259,
- 25, 259,
- 41, 259,
- 47, 259,
- 48, 259,
- 51, 259,
- 57, 259,
- 62, 259,
- 63, 259,
- 64, 259,
- 65, 259,
- 66, 259,
- 67, 259,
- 68, 259,
- 69, 259,
- 70, 259,
- 71, 259,
- 72, 259,
- 73, 259,
- 74, 259,
- 75, 259,
- 79, 259,
- 83, 259,
- 84, 259,
- 85, 259,
- 87, 259,
- 90, 259,
- 91, 259,
- 92, 259,
- 93, 259,
+ -1, 258,
+ 9, 273,
+ 12, 273,
+ 13, 273,
+ 18, 273,
+ 19, 273,
+ 25, 273,
+ 41, 273,
+ 47, 273,
+ 48, 273,
+ 51, 273,
+ 57, 273,
+ 62, 273,
+ 63, 273,
+ 64, 273,
+ 65, 273,
+ 66, 273,
+ 67, 273,
+ 68, 273,
+ 69, 273,
+ 70, 273,
+ 71, 273,
+ 72, 273,
+ 73, 273,
+ 74, 273,
+ 75, 273,
+ 79, 273,
+ 82, 273,
+ 83, 273,
+ 84, 273,
+ 86, 273,
+ 87, 273,
+ 88, 273,
+ 90, 273,
+ 93, 273,
+ 94, 273,
+ 95, 273,
+ 96, 273,
-2, 0,
- -1, 252,
- 9, 259,
- 12, 259,
- 13, 259,
- 18, 259,
- 19, 259,
- 25, 259,
- 41, 259,
- 47, 259,
- 48, 259,
- 51, 259,
- 57, 259,
- 62, 259,
- 63, 259,
- 64, 259,
- 65, 259,
- 66, 259,
- 67, 259,
- 68, 259,
- 69, 259,
- 70, 259,
- 71, 259,
- 72, 259,
- 73, 259,
- 74, 259,
- 75, 259,
- 79, 259,
- 83, 259,
- 84, 259,
- 85, 259,
- 87, 259,
- 90, 259,
- 91, 259,
- 92, 259,
- 93, 259,
+ -1, 259,
+ 9, 273,
+ 12, 273,
+ 13, 273,
+ 18, 273,
+ 19, 273,
+ 25, 273,
+ 41, 273,
+ 47, 273,
+ 48, 273,
+ 51, 273,
+ 57, 273,
+ 62, 273,
+ 63, 273,
+ 64, 273,
+ 65, 273,
+ 66, 273,
+ 67, 273,
+ 68, 273,
+ 69, 273,
+ 70, 273,
+ 71, 273,
+ 72, 273,
+ 73, 273,
+ 74, 273,
+ 75, 273,
+ 79, 273,
+ 82, 273,
+ 83, 273,
+ 84, 273,
+ 86, 273,
+ 87, 273,
+ 88, 273,
+ 90, 273,
+ 93, 273,
+ 94, 273,
+ 95, 273,
+ 96, 273,
-2, 0,
}
const yyPrivate = 57344
-const yyLast = 1050
+const yyLast = 1224
var yyAct = [...]int16{
- 58, 186, 413, 411, 341, 418, 286, 243, 197, 95,
- 189, 48, 355, 144, 70, 227, 93, 251, 252, 356,
- 159, 190, 65, 120, 17, 88, 127, 130, 128, 129,
- 22, 425, 426, 427, 428, 131, 249, 121, 124, 335,
- 250, 67, 132, 126, 408, 407, 377, 332, 125, 123,
- 331, 102, 126, 122, 336, 154, 324, 6, 397, 18,
- 19, 111, 112, 20, 135, 114, 137, 119, 101, 375,
- 337, 323, 375, 330, 11, 12, 14, 15, 16, 21,
- 23, 25, 26, 27, 28, 29, 33, 34, 43, 133,
- 329, 13, 116, 118, 117, 24, 38, 37, 146, 30,
- 402, 124, 31, 32, 35, 36, 130, 412, 138, 396,
- 194, 125, 123, 328, 131, 126, 365, 182, 239, 401,
- 193, 199, 204, 205, 206, 207, 208, 209, 177, 363,
- 362, 181, 200, 200, 200, 200, 200, 200, 200, 178,
- 120, 238, 223, 201, 201, 201, 201, 201, 201, 201,
- 212, 215, 134, 200, 136, 211, 210, 2, 3, 4,
- 5, 222, 233, 221, 201, 245, 235, 384, 333, 371,
- 228, 247, 229, 360, 370, 359, 246, 358, 188, 273,
- 140, 368, 114, 195, 119, 194, 277, 139, 62, 369,
- 268, 237, 229, 271, 185, 193, 441, 200, 61, 196,
- 367, 201, 273, 383, 155, 278, 279, 280, 201, 116,
- 118, 117, 231, 200, 236, 121, 124, 195, 382, 440,
- 86, 218, 230, 232, 201, 381, 125, 123, 276, 275,
- 126, 122, 231, 196, 274, 146, 87, 132, 439, 327,
- 429, 438, 230, 232, 248, 141, 184, 183, 419, 253,
- 254, 255, 256, 257, 258, 259, 260, 261, 262, 263,
- 264, 265, 266, 267, 334, 357, 191, 192, 214, 353,
- 354, 202, 203, 361, 121, 124, 88, 364, 283, 7,
- 39, 213, 282, 199, 200, 125, 123, 395, 200, 126,
- 122, 366, 10, 194, 200, 201, 394, 281, 393, 201,
- 392, 391, 90, 193, 390, 201, 160, 161, 162, 163,
- 164, 165, 166, 167, 168, 169, 170, 171, 172, 173,
- 174, 389, 194, 388, 120, 195, 373, 387, 386, 385,
- 153, 99, 193, 62, 442, 374, 376, 200, 378, 185,
- 56, 196, 40, 61, 379, 380, 89, 152, 201, 151,
- 1, 100, 102, 103, 195, 104, 105, 175, 71, 108,
- 109, 398, 111, 112, 113, 86, 114, 115, 119, 101,
- 196, 66, 200, 55, 9, 9, 54, 404, 8, 53,
- 406, 87, 41, 201, 52, 158, 410, 51, 414, 415,
- 416, 184, 183, 116, 118, 117, 421, 420, 423, 422,
- 417, 430, 50, 49, 289, 47, 156, 216, 147, 46,
- 431, 432, 200, 372, 299, 433, 202, 203, 145, 96,
- 305, 435, 157, 201, 403, 437, 326, 288, 147, 94,
- 436, 97, 45, 44, 57, 242, 434, 234, 145, 338,
- 443, 200, 97, 98, 121, 124, 143, 240, 284, 301,
- 302, 97, 201, 303, 91, 125, 123, 424, 187, 126,
- 122, 316, 287, 59, 290, 292, 294, 295, 296, 304,
- 306, 309, 310, 311, 312, 313, 317, 318, 142, 0,
- 291, 293, 297, 298, 300, 307, 322, 321, 308, 289,
- 96, 0, 314, 315, 319, 320, 226, 150, 405, 299,
- 94, 225, 149, 0, 0, 305, 0, 0, 92, 285,
- 0, 0, 288, 97, 224, 148, 62, 121, 124, 0,
- 0, 0, 272, 0, 0, 0, 61, 0, 125, 123,
- 0, 0, 126, 122, 301, 302, 0, 0, 303, 0,
- 0, 0, 0, 0, 0, 0, 316, 0, 86, 290,
- 292, 294, 295, 296, 304, 306, 309, 310, 311, 312,
- 313, 317, 318, 0, 87, 291, 293, 297, 298, 300,
- 307, 322, 321, 308, 184, 183, 0, 314, 315, 319,
- 320, 62, 0, 120, 60, 88, 0, 63, 0, 0,
- 22, 61, 0, 0, 217, 0, 0, 64, 0, 269,
- 270, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 100, 102, 0, 86, 0, 0, 0, 0, 0, 18,
- 19, 111, 112, 20, 0, 114, 115, 119, 101, 87,
- 0, 0, 0, 0, 72, 73, 74, 75, 76, 77,
- 78, 79, 80, 81, 82, 83, 84, 85, 0, 0,
- 400, 13, 116, 118, 117, 24, 38, 37, 399, 30,
- 0, 0, 31, 32, 68, 69, 62, 42, 0, 60,
- 88, 0, 63, 0, 0, 22, 61, 121, 124, 0,
- 0, 0, 64, 0, 121, 124, 0, 0, 125, 123,
- 0, 0, 126, 122, 0, 125, 123, 0, 86, 126,
- 122, 0, 0, 0, 18, 19, 0, 0, 20, 0,
- 0, 0, 0, 0, 87, 0, 0, 0, 0, 72,
- 73, 74, 75, 76, 77, 78, 79, 80, 81, 82,
- 83, 84, 85, 0, 0, 0, 13, 0, 0, 220,
- 24, 38, 37, 0, 30, 0, 325, 31, 32, 68,
- 69, 62, 0, 0, 60, 88, 0, 63, 121, 124,
- 22, 61, 0, 0, 0, 0, 0, 64, 0, 125,
- 123, 0, 0, 126, 122, 0, 0, 0, 0, 0,
- 121, 124, 0, 86, 0, 0, 0, 0, 0, 18,
- 19, 125, 123, 20, 0, 126, 122, 0, 0, 87,
- 0, 0, 0, 0, 72, 73, 74, 75, 76, 77,
- 78, 79, 80, 81, 82, 83, 84, 85, 17, 39,
- 0, 13, 0, 0, 22, 24, 38, 37, 0, 30,
- 340, 0, 31, 32, 68, 69, 0, 339, 0, 0,
- 0, 343, 344, 342, 349, 351, 348, 350, 345, 346,
- 347, 352, 241, 18, 19, 0, 194, 20, 0, 244,
- 0, 0, 0, 247, 0, 0, 193, 0, 11, 12,
- 14, 15, 16, 21, 23, 25, 26, 27, 28, 29,
- 33, 34, 0, 0, 120, 13, 0, 0, 195, 24,
- 38, 37, 219, 30, 0, 0, 31, 32, 35, 36,
- 0, 0, 0, 120, 196, 0, 0, 0, 0, 0,
- 0, 100, 102, 103, 0, 104, 105, 106, 107, 108,
- 109, 110, 111, 112, 113, 0, 114, 115, 119, 101,
- 100, 102, 103, 0, 104, 105, 106, 107, 108, 109,
- 110, 111, 112, 113, 198, 114, 115, 119, 101, 120,
- 0, 62, 0, 116, 118, 117, 0, 185, 176, 0,
- 0, 61, 0, 0, 0, 62, 0, 0, 0, 0,
- 0, 185, 116, 118, 117, 61, 100, 102, 103, 0,
- 104, 105, 106, 86, 108, 109, 110, 111, 112, 113,
- 0, 114, 115, 119, 101, 0, 0, 86, 0, 87,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 184,
- 183, 0, 0, 87, 0, 0, 0, 0, 116, 118,
- 117, 0, 0, 184, 183, 409, 0, 0, 0, 0,
- 0, 0, 0, 0, 202, 203, 343, 344, 342, 349,
- 351, 348, 350, 345, 346, 347, 352, 0, 179, 180,
+ 61, 363, 190, 429, 351, 436, 431, 293, 247, 201,
+ 98, 51, 147, 193, 369, 96, 231, 412, 413, 370,
+ 132, 133, 68, 130, 73, 163, 194, 131, 443, 444,
+ 445, 446, 134, 135, 256, 253, 254, 255, 257, 258,
+ 259, 129, 70, 426, 123, 425, 124, 127, 391, 342,
+ 157, 458, 223, 198, 447, 389, 415, 128, 126, 345,
+ 451, 129, 125, 197, 414, 465, 398, 138, 379, 140,
+ 6, 103, 105, 106, 346, 107, 108, 109, 110, 111,
+ 112, 113, 114, 115, 116, 199, 117, 118, 122, 104,
+ 347, 136, 343, 46, 124, 127, 389, 133, 334, 251,
+ 397, 200, 149, 377, 192, 128, 126, 199, 134, 129,
+ 125, 198, 141, 333, 420, 396, 119, 121, 120, 123,
+ 186, 197, 395, 200, 203, 208, 209, 210, 211, 212,
+ 213, 181, 376, 419, 430, 204, 204, 204, 204, 204,
+ 204, 204, 182, 199, 185, 227, 205, 205, 205, 205,
+ 205, 205, 205, 216, 219, 215, 204, 341, 214, 200,
+ 137, 117, 139, 122, 339, 385, 237, 205, 239, 464,
+ 384, 249, 226, 2, 3, 4, 5, 91, 290, 225,
+ 340, 123, 289, 280, 250, 383, 364, 338, 124, 127,
+ 284, 119, 121, 120, 275, 195, 196, 288, 218, 128,
+ 126, 204, 460, 129, 125, 205, 280, 278, 158, 105,
+ 374, 217, 205, 286, 287, 423, 243, 204, 241, 114,
+ 115, 124, 127, 117, 373, 122, 104, 372, 205, 222,
+ 143, 437, 128, 126, 124, 127, 129, 125, 65, 242,
+ 149, 240, 337, 142, 42, 128, 126, 418, 64, 129,
+ 125, 285, 252, 119, 121, 120, 365, 366, 260, 261,
+ 262, 263, 264, 265, 266, 267, 268, 269, 270, 271,
+ 272, 273, 274, 344, 371, 127, 367, 368, 198, 283,
+ 375, 124, 127, 282, 378, 128, 126, 281, 197, 129,
+ 203, 204, 128, 126, 135, 204, 129, 125, 198, 380,
+ 65, 204, 205, 144, 7, 409, 205, 408, 197, 407,
+ 64, 406, 205, 164, 165, 166, 167, 168, 169, 170,
+ 171, 172, 173, 174, 175, 176, 177, 178, 202, 232,
+ 199, 233, 89, 156, 417, 65, 387, 405, 463, 233,
+ 404, 189, 102, 224, 403, 64, 200, 204, 90, 388,
+ 390, 10, 392, 124, 127, 393, 394, 462, 205, 402,
+ 461, 93, 124, 127, 128, 126, 401, 89, 129, 125,
+ 400, 235, 399, 128, 126, 416, 410, 129, 125, 235,
+ 8, 234, 236, 90, 44, 59, 204, 411, 43, 234,
+ 236, 92, 422, 188, 187, 1, 179, 205, 424, 155,
+ 428, 154, 230, 432, 433, 434, 150, 229, 74, 335,
+ 439, 438, 441, 440, 449, 450, 148, 435, 58, 452,
+ 228, 206, 207, 448, 336, 57, 296, 56, 386, 100,
+ 204, 69, 453, 454, 9, 9, 309, 455, 99, 55,
+ 457, 205, 315, 124, 127, 162, 421, 150, 97, 295,
+ 99, 54, 459, 53, 128, 126, 238, 148, 129, 125,
+ 97, 100, 153, 204, 466, 146, 52, 152, 95, 50,
+ 100, 311, 312, 100, 205, 313, 160, 220, 49, 161,
+ 151, 48, 159, 326, 47, 60, 297, 299, 301, 302,
+ 303, 314, 316, 319, 320, 321, 322, 323, 327, 328,
+ 246, 456, 298, 300, 304, 305, 306, 307, 308, 310,
+ 317, 332, 331, 318, 296, 348, 101, 324, 325, 329,
+ 330, 245, 244, 291, 309, 198, 94, 442, 248, 191,
+ 315, 350, 251, 294, 292, 197, 62, 295, 349, 145,
+ 0, 0, 353, 354, 352, 359, 361, 358, 360, 355,
+ 356, 357, 362, 0, 0, 0, 0, 199, 0, 311,
+ 312, 0, 0, 313, 0, 0, 0, 0, 0, 0,
+ 0, 326, 0, 200, 297, 299, 301, 302, 303, 314,
+ 316, 319, 320, 321, 322, 323, 327, 328, 0, 0,
+ 298, 300, 304, 305, 306, 307, 308, 310, 317, 332,
+ 331, 318, 0, 0, 0, 324, 325, 329, 330, 65,
+ 0, 0, 63, 91, 0, 66, 427, 0, 25, 64,
+ 0, 0, 221, 0, 0, 67, 0, 353, 354, 352,
+ 359, 361, 358, 360, 355, 356, 357, 362, 0, 0,
+ 0, 89, 0, 0, 0, 0, 0, 21, 22, 0,
+ 0, 23, 0, 0, 0, 0, 0, 90, 0, 0,
+ 0, 0, 75, 76, 77, 78, 79, 80, 81, 82,
+ 83, 84, 85, 86, 87, 88, 0, 0, 0, 13,
+ 0, 0, 16, 17, 18, 0, 27, 41, 40, 0,
+ 33, 0, 0, 34, 35, 71, 72, 65, 45, 0,
+ 63, 91, 0, 66, 0, 0, 25, 64, 0, 0,
+ 0, 0, 0, 67, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 89,
+ 0, 0, 0, 0, 0, 21, 22, 0, 0, 23,
+ 0, 0, 0, 0, 0, 90, 0, 0, 0, 0,
+ 75, 76, 77, 78, 79, 80, 81, 82, 83, 84,
+ 85, 86, 87, 88, 0, 0, 0, 13, 0, 0,
+ 16, 17, 18, 0, 27, 41, 40, 0, 33, 0,
+ 0, 34, 35, 71, 72, 65, 0, 0, 63, 91,
+ 0, 66, 0, 0, 25, 64, 0, 0, 0, 0,
+ 0, 67, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 89, 0, 0,
+ 0, 0, 0, 21, 22, 0, 0, 23, 0, 0,
+ 0, 0, 0, 90, 0, 0, 0, 0, 75, 76,
+ 77, 78, 79, 80, 81, 82, 83, 84, 85, 86,
+ 87, 88, 0, 0, 0, 13, 0, 0, 16, 17,
+ 18, 0, 27, 41, 40, 0, 33, 20, 91, 34,
+ 35, 71, 72, 25, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 21, 22, 0, 0, 23, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 11, 12, 14,
+ 15, 19, 24, 26, 28, 29, 30, 31, 32, 36,
+ 37, 0, 0, 0, 13, 0, 0, 16, 17, 18,
+ 0, 27, 41, 40, 0, 33, 20, 42, 34, 35,
+ 38, 39, 25, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 21, 22, 0, 0, 23, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 11, 12, 14, 15,
+ 19, 24, 26, 28, 29, 30, 31, 32, 36, 37,
+ 123, 0, 0, 13, 0, 0, 16, 17, 18, 0,
+ 27, 41, 40, 0, 33, 0, 0, 34, 35, 38,
+ 39, 123, 0, 0, 0, 0, 0, 103, 105, 106,
+ 0, 107, 108, 109, 110, 111, 112, 113, 114, 115,
+ 116, 0, 117, 118, 122, 104, 0, 0, 103, 105,
+ 106, 0, 107, 108, 109, 0, 111, 112, 113, 114,
+ 115, 116, 382, 117, 118, 122, 104, 0, 0, 65,
+ 0, 123, 119, 121, 120, 189, 65, 0, 0, 64,
+ 0, 381, 189, 0, 0, 0, 64, 0, 0, 0,
+ 0, 0, 0, 119, 121, 120, 0, 0, 103, 105,
+ 106, 89, 107, 108, 0, 0, 111, 112, 89, 114,
+ 115, 116, 180, 117, 118, 122, 104, 90, 0, 65,
+ 0, 0, 0, 0, 90, 189, 65, 188, 187, 64,
+ 0, 0, 279, 0, 188, 187, 64, 123, 0, 0,
+ 0, 0, 0, 119, 121, 120, 0, 0, 0, 0,
+ 0, 89, 0, 0, 0, 206, 207, 0, 89, 0,
+ 0, 0, 206, 207, 103, 105, 0, 90, 0, 0,
+ 0, 0, 0, 0, 90, 114, 115, 188, 187, 117,
+ 118, 122, 104, 0, 188, 187, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 183, 184, 0, 0, 119,
+ 121, 120, 276, 277,
}
var yyPact = [...]int16{
- 55, 269, 806, 806, 657, 12, -1000, -1000, -1000, 267,
- -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000,
- -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000,
- -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, 488,
- -1000, 329, -1000, 889, -1000, -1000, -1000, -1000, -1000, -1000,
- -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -4, 27,
- 222, -1000, -1000, 742, -1000, 742, 263, -1000, 172, 165,
- 230, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000,
- -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, 426, -1000,
- -1000, 495, -1000, -1000, 345, 326, -1000, -1000, 31, -1000,
- -58, -58, -58, -58, -58, -58, -58, -58, -58, -58,
- -58, -58, -58, -58, -58, -58, 956, -1000, -1000, 176,
- 942, 324, 324, 324, 324, 324, 324, 222, -52, -1000,
- 266, 266, 572, -1000, 870, 717, 126, -13, -1000, 141,
- 139, 324, 494, -1000, -1000, 168, 188, -1000, -1000, 417,
- -1000, 189, -1000, 116, 847, 742, -1000, -46, -63, -1000,
- 742, 742, 742, 742, 742, 742, 742, 742, 742, 742,
- 742, 742, 742, 742, 742, -1000, -1000, -1000, 507, 219,
- 214, 213, -4, -1000, -1000, 324, -1000, 190, -1000, -1000,
- -1000, -1000, -1000, -1000, -1000, 101, 101, 276, -1000, -4,
- -1000, 324, 172, 165, 59, 59, -13, -13, -13, -13,
- -1000, -1000, -1000, 487, -1000, -1000, 49, -1000, 889, -1000,
- -1000, -1000, -1000, 739, -1000, 406, -1000, 88, -1000, -1000,
- -1000, -1000, -1000, 48, -1000, -1000, -1000, -1000, -1000, -1000,
- -1000, 21, 142, 13, -1000, -1000, -1000, 813, 9, 266,
- 266, 266, 266, 126, 126, 569, 569, 569, 310, 935,
- 569, 569, 310, 126, 126, 569, 126, 9, -1000, 162,
- 160, 158, 324, -13, 108, 107, 324, 717, 94, -1000,
- -1000, -1000, 179, -1000, 167, -1000, -1000, -1000, -1000, -1000,
+ 68, 294, 934, 934, 688, 855, -1000, -1000, -1000, 231,
-1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000,
-1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000,
-1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000,
- -1000, -1000, -1000, -1000, 742, 324, -1000, -1000, -1000, -1000,
- -1000, -1000, 53, 53, 20, 53, 155, 155, 201, 150,
- -1000, -1000, 323, 322, 321, 317, 315, 298, 295, 294,
- 292, 290, 281, -1000, -1000, -1000, -1000, -1000, 87, 36,
- 324, 636, -1000, -1000, 643, -1000, 98, -1000, -1000, -1000,
- 402, -1000, 889, 476, -1000, -1000, -1000, 53, -1000, 19,
- 18, 1008, -1000, -1000, -1000, 50, 284, 284, 284, 101,
- 234, 234, 50, 234, 50, -65, -1000, -1000, 233, -1000,
- 324, -1000, -1000, -1000, -1000, -1000, -1000, 53, 53, -1000,
- -1000, -1000, 53, -1000, -1000, -1000, -1000, -1000, -1000, 284,
- -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, 324,
- 403, -1000, -1000, -1000, 217, -1000, 174, -1000, 313, -1000,
- -1000, -1000, -1000, -1000,
+ -1000, -1000, 448, -1000, 340, -1000, 996, -1000, -1000, -1000,
+ -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000,
+ -1000, 5, 18, 279, -1000, -1000, 776, -1000, 776, 164,
+ -1000, 228, 215, 288, -1000, -1000, -1000, -1000, -1000, -1000,
+ -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000,
+ -1000, 445, -1000, -1000, 460, -1000, -1000, 397, 329, -1000,
+ -1000, 26, -1000, -53, -53, -53, -53, -53, -53, -53,
+ -53, -53, -53, -53, -53, -53, -53, -53, -53, 1120,
+ -1000, -1000, 102, 326, 1077, 1077, 1077, 1077, 1077, 1077,
+ 279, -58, -1000, 196, 196, 600, -1000, 30, 321, 105,
+ -15, -1000, 157, 150, 1077, 400, -1000, -1000, 327, 335,
+ -1000, -1000, 436, -1000, 216, -1000, 214, 516, 776, -1000,
+ -47, -51, -41, -1000, 776, 776, 776, 776, 776, 776,
+ 776, 776, 776, 776, 776, 776, 776, 776, 776, -1000,
+ -1000, -1000, 1127, 272, 268, 264, 5, -1000, -1000, 1077,
+ -1000, 236, -1000, -1000, -1000, -1000, -1000, -1000, -1000, 269,
+ 269, 176, -1000, 5, -1000, 1077, 228, 215, 233, 233,
+ -15, -15, -15, -15, -1000, -1000, -1000, 512, -1000, -1000,
+ 91, -1000, 996, -1000, -1000, -1000, -1000, 402, -1000, 404,
+ -1000, 162, -1000, -1000, -1000, -1000, -1000, 155, -1000, -1000,
+ -1000, -1000, -1000, -1000, -1000, 23, 66, 33, -1000, -1000,
+ -1000, 514, 167, 171, 171, 171, 196, 196, 196, 196,
+ 105, 105, 1133, 1133, 1133, 1067, 1017, 1133, 1133, 1067,
+ 105, 105, 1133, 105, 167, -1000, 212, 209, 195, 1077,
+ -15, 110, 81, 1077, 321, 46, -1000, -1000, -1000, 1070,
+ -1000, 163, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000,
+ -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000,
+ -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000,
+ -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000,
+ -1000, -1000, -1000, -1000, 776, 1077, -1000, -1000, -1000, -1000,
+ -1000, -1000, 36, 36, 22, 36, 83, 83, 98, 49,
+ -1000, -1000, 366, 364, 360, 353, 338, 334, 331, 305,
+ 303, 301, 299, -1000, 291, -67, -65, -1000, -1000, -1000,
+ -1000, -1000, 42, 34, 1077, 312, -1000, -1000, 240, -1000,
+ 112, -1000, -1000, -1000, 424, -1000, 996, 193, -1000, -1000,
+ -1000, 36, -1000, 19, 17, 599, -1000, -1000, -1000, 77,
+ 289, 289, 289, 269, 217, 217, 77, 217, 77, -71,
+ 32, 229, 171, 171, -1000, -1000, 53, -1000, 1077, -1000,
+ -1000, -1000, -1000, -1000, -1000, 36, 36, -1000, -1000, -1000,
+ 36, -1000, -1000, -1000, -1000, -1000, -1000, 289, -1000, -1000,
+ -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, 29, -1000,
+ -1000, 1077, 180, -1000, -1000, -1000, 336, -1000, -1000, 147,
+ -1000, 44, -1000, -1000, -1000, -1000, -1000,
}
var yyPgo = [...]int16{
- 0, 478, 13, 463, 6, 15, 462, 371, 22, 458,
- 9, 457, 14, 292, 378, 454, 16, 448, 19, 12,
- 447, 443, 7, 439, 4, 5, 436, 3, 2, 10,
- 435, 21, 1, 434, 433, 26, 204, 432, 422, 88,
- 409, 407, 28, 406, 41, 405, 11, 403, 402, 387,
- 385, 384, 379, 376, 373, 340, 0, 358, 8, 357,
- 350, 342,
+ 0, 539, 12, 536, 7, 16, 533, 431, 22, 529,
+ 10, 527, 24, 351, 380, 526, 15, 523, 19, 14,
+ 522, 516, 8, 515, 4, 5, 501, 3, 6, 13,
+ 500, 26, 2, 485, 484, 23, 208, 482, 481, 479,
+ 93, 478, 477, 27, 476, 1, 42, 469, 11, 466,
+ 453, 451, 445, 439, 427, 425, 418, 385, 0, 408,
+ 9, 396, 395, 388,
}
var yyR1 = [...]int8{
- 0, 60, 60, 60, 60, 60, 60, 60, 39, 39,
- 39, 39, 39, 39, 39, 39, 39, 39, 39, 39,
- 39, 39, 39, 34, 34, 34, 34, 35, 35, 37,
- 37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
- 37, 37, 37, 37, 37, 36, 38, 38, 50, 50,
- 43, 43, 43, 43, 18, 18, 18, 18, 17, 17,
- 17, 4, 4, 4, 40, 42, 42, 41, 41, 41,
- 51, 58, 47, 47, 48, 49, 33, 33, 33, 9,
- 9, 45, 53, 53, 53, 53, 53, 53, 54, 55,
- 55, 55, 44, 44, 44, 1, 1, 1, 2, 2,
- 2, 2, 2, 2, 2, 14, 14, 7, 7, 7,
+ 0, 62, 62, 62, 62, 62, 62, 62, 40, 40,
+ 40, 40, 40, 40, 40, 40, 40, 40, 40, 40,
+ 40, 40, 40, 34, 34, 34, 34, 35, 35, 38,
+ 38, 38, 38, 38, 38, 38, 38, 38, 38, 38,
+ 38, 38, 38, 38, 38, 36, 39, 39, 52, 52,
+ 44, 44, 44, 44, 37, 37, 37, 37, 37, 37,
+ 18, 18, 18, 18, 17, 17, 17, 4, 4, 4,
+ 45, 45, 41, 43, 43, 42, 42, 42, 53, 60,
+ 49, 49, 50, 51, 33, 33, 33, 9, 9, 47,
+ 55, 55, 55, 55, 55, 55, 56, 57, 57, 57,
+ 46, 46, 46, 1, 1, 1, 2, 2, 2, 2,
+ 2, 2, 2, 14, 14, 7, 7, 7, 7, 7,
7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
- 7, 7, 7, 7, 7, 13, 13, 13, 13, 15,
- 15, 15, 16, 16, 16, 16, 16, 16, 16, 61,
- 21, 21, 21, 21, 20, 20, 20, 20, 20, 20,
- 20, 20, 20, 30, 30, 30, 22, 22, 22, 22,
- 23, 23, 23, 24, 24, 24, 24, 24, 24, 24,
- 24, 24, 24, 24, 25, 25, 26, 26, 26, 11,
- 11, 11, 11, 3, 3, 3, 3, 3, 3, 3,
- 3, 3, 3, 3, 3, 3, 3, 6, 6, 6,
+ 7, 7, 7, 7, 7, 7, 13, 13, 13, 13,
+ 15, 15, 15, 16, 16, 16, 16, 16, 16, 16,
+ 63, 21, 21, 21, 21, 20, 20, 20, 20, 20,
+ 20, 20, 20, 20, 30, 30, 30, 22, 22, 22,
+ 22, 23, 23, 23, 24, 24, 24, 24, 24, 24,
+ 24, 24, 24, 24, 24, 25, 25, 26, 26, 26,
+ 11, 11, 11, 11, 3, 3, 3, 3, 3, 3,
+ 3, 3, 3, 3, 3, 3, 3, 3, 6, 6,
6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
- 8, 8, 5, 5, 5, 5, 46, 46, 29, 29,
- 31, 31, 32, 32, 28, 27, 27, 52, 10, 19,
- 19, 59, 59, 59, 59, 59, 59, 59, 59, 59,
- 59, 12, 12, 56, 56, 56, 56, 56, 56, 56,
- 56, 56, 56, 56, 56, 57,
+ 6, 6, 6, 6, 8, 8, 5, 5, 5, 5,
+ 48, 48, 29, 29, 31, 31, 32, 32, 28, 27,
+ 27, 54, 10, 19, 19, 61, 61, 61, 61, 61,
+ 61, 61, 61, 61, 61, 12, 12, 58, 58, 58,
+ 58, 58, 58, 58, 58, 58, 58, 58, 58, 59,
}
var yyR2 = [...]int8{
@@ -636,126 +669,131 @@ var yyR2 = [...]int8{
1, 1, 1, 3, 3, 2, 2, 2, 2, 4,
4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
4, 4, 4, 4, 4, 1, 0, 1, 3, 3,
- 1, 1, 3, 3, 3, 4, 2, 1, 3, 1,
- 2, 1, 1, 1, 2, 3, 2, 3, 1, 2,
- 3, 1, 3, 3, 2, 2, 3, 5, 3, 1,
- 1, 4, 6, 5, 6, 5, 4, 3, 2, 2,
- 1, 1, 3, 4, 2, 3, 1, 2, 3, 3,
- 1, 3, 3, 2, 1, 2, 1, 1, 1, 1,
+ 1, 1, 3, 3, 1, 3, 3, 3, 5, 5,
+ 3, 4, 2, 1, 3, 1, 2, 1, 1, 1,
+ 3, 4, 2, 3, 2, 3, 1, 2, 3, 1,
+ 3, 3, 2, 2, 3, 5, 3, 1, 1, 4,
+ 6, 5, 6, 5, 4, 3, 2, 2, 1, 1,
+ 3, 4, 2, 3, 1, 2, 3, 3, 1, 3,
+ 3, 2, 1, 2, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
- 1, 1, 1, 1, 1, 3, 4, 2, 0, 3,
- 1, 2, 3, 3, 1, 3, 3, 2, 1, 2,
- 0, 3, 2, 1, 1, 3, 1, 3, 4, 1,
- 3, 5, 5, 1, 1, 1, 4, 3, 3, 2,
- 3, 1, 2, 3, 3, 3, 3, 3, 3, 3,
- 3, 3, 3, 3, 4, 3, 3, 1, 2, 1,
+ 1, 1, 1, 1, 1, 1, 3, 4, 2, 0,
+ 3, 1, 2, 3, 3, 1, 3, 3, 2, 1,
+ 2, 0, 3, 2, 1, 1, 3, 1, 3, 4,
+ 1, 3, 5, 5, 1, 1, 1, 4, 3, 3,
+ 2, 3, 1, 2, 3, 3, 3, 3, 3, 3,
+ 3, 3, 3, 3, 3, 4, 3, 3, 1, 2,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
- 2, 2, 1, 1, 1, 2, 1, 1, 1, 0,
- 1, 1, 2, 3, 3, 4, 4, 6, 7, 4,
- 1, 1, 1, 1, 2, 3, 3, 3, 3, 3,
- 3, 3, 3, 6, 1, 3,
+ 1, 1, 1, 1, 2, 2, 1, 1, 1, 2,
+ 1, 1, 1, 0, 1, 1, 2, 3, 3, 4,
+ 4, 6, 7, 4, 1, 1, 1, 1, 2, 3,
+ 3, 3, 3, 3, 3, 3, 3, 6, 1, 3,
}
var yyChk = [...]int16{
- -1000, -60, 102, 103, 104, 105, 2, 10, -14, -7,
- -13, 62, 63, 79, 64, 65, 66, 12, 47, 48,
- 51, 67, 18, 68, 83, 69, 70, 71, 72, 73,
- 87, 90, 91, 74, 75, 92, 93, 85, 84, 13,
- -61, -14, 10, -39, -34, -37, -40, -45, -46, -47,
- -48, -49, -51, -52, -53, -54, -55, -33, -56, -3,
- 12, 19, 9, 15, 25, -8, -7, -44, 92, 93,
- -12, -57, 62, 63, 64, 65, 66, 67, 68, 69,
- 70, 71, 72, 73, 74, 75, 41, 57, 13, -55,
- -13, -15, 20, -16, 12, -10, 2, 25, -21, 2,
- 41, 59, 42, 43, 45, 46, 47, 48, 49, 50,
- 51, 52, 53, 54, 56, 57, 83, 85, 84, 58,
- 14, 41, 57, 53, 42, 52, 56, -35, -42, 2,
- 79, 87, 15, -42, -39, -56, -39, -56, -44, 15,
- 15, 15, -1, 20, -2, 12, -10, 2, 20, 7,
- 2, 4, 2, 4, 24, -36, -43, -38, -50, 78,
- -36, -36, -36, -36, -36, -36, -36, -36, -36, -36,
- -36, -36, -36, -36, -36, -59, 2, -46, -8, 92,
- 93, -12, -56, 68, 67, 15, -32, -9, 2, -29,
- -31, 90, 91, 19, 9, 41, 57, -58, 2, -56,
- -46, -8, 92, 93, -56, -56, -56, -56, -56, -56,
- -42, -35, -18, 15, 2, -18, -41, 22, -39, 22,
- 22, 22, 22, -56, 20, 7, 2, -5, 2, 4,
- 54, 44, 55, -5, 20, -16, 25, 2, 25, 2,
- -20, 5, -30, -22, 12, -29, -31, 16, -39, 82,
- 86, 80, 81, -39, -39, -39, -39, -39, -39, -39,
- -39, -39, -39, -39, -39, -39, -39, -39, -46, 92,
- 93, -12, 15, -56, 15, 15, 15, -56, 15, -29,
- -29, 21, 6, 2, -17, 22, -4, -6, 25, 2,
- 62, 78, 63, 79, 64, 65, 66, 80, 81, 12,
- 82, 47, 48, 51, 67, 18, 68, 83, 86, 69,
- 70, 71, 72, 73, 90, 91, 59, 74, 75, 92,
- 93, 85, 84, 22, 7, 7, 20, -2, 25, 2,
+ -1000, -62, 105, 106, 107, 108, 2, 10, -14, -7,
+ -13, 62, 63, 79, 64, 65, 82, 83, 84, 66,
+ 12, 47, 48, 51, 67, 18, 68, 86, 69, 70,
+ 71, 72, 73, 90, 93, 94, 74, 75, 95, 96,
+ 88, 87, 13, -63, -14, 10, -40, -34, -38, -41,
+ -47, -48, -49, -50, -51, -53, -54, -55, -56, -57,
+ -33, -58, -3, 12, 19, 9, 15, 25, -8, -7,
+ -46, 95, 96, -12, -59, 62, 63, 64, 65, 66,
+ 67, 68, 69, 70, 71, 72, 73, 74, 75, 41,
+ 57, 13, -57, -13, -15, 20, -16, 12, -10, 2,
+ 25, -21, 2, 41, 59, 42, 43, 45, 46, 47,
+ 48, 49, 50, 51, 52, 53, 54, 56, 57, 86,
+ 88, 87, 58, 14, 41, 57, 53, 42, 52, 56,
+ -35, -43, 2, 79, 90, 15, -43, -40, -58, -40,
+ -58, -46, 15, 15, 15, -1, 20, -2, 12, -10,
+ 2, 20, 7, 2, 4, 2, 4, 24, -36, -37,
+ -44, -39, -52, 78, -36, -36, -36, -36, -36, -36,
+ -36, -36, -36, -36, -36, -36, -36, -36, -36, -61,
+ 2, -48, -8, 95, 96, -12, -58, 68, 67, 15,
+ -32, -9, 2, -29, -31, 93, 94, 19, 9, 41,
+ 57, -60, 2, -58, -48, -8, 95, 96, -58, -58,
+ -58, -58, -58, -58, -43, -35, -18, 15, 2, -18,
+ -42, 22, -40, 22, 22, 22, 22, -58, 20, 7,
+ 2, -5, 2, 4, 54, 44, 55, -5, 20, -16,
+ 25, 2, 25, 2, -20, 5, -30, -22, 12, -29,
+ -31, 16, -40, 82, 83, 84, 85, 89, 80, 81,
+ -40, -40, -40, -40, -40, -40, -40, -40, -40, -40,
+ -40, -40, -40, -40, -40, -48, 95, 96, -12, 15,
+ -58, 15, 15, 15, -58, 15, -29, -29, 21, 6,
+ 2, -17, 22, -4, -6, 25, 2, 62, 78, 63,
+ 79, 64, 65, 66, 80, 81, 82, 83, 84, 12,
+ 85, 47, 48, 51, 67, 18, 68, 86, 89, 69,
+ 70, 71, 72, 73, 93, 94, 59, 74, 75, 95,
+ 96, 88, 87, 22, 7, 7, 20, -2, 25, 2,
25, 2, 26, 26, -31, 26, 41, 57, -23, 24,
17, -24, 30, 28, 29, 35, 36, 37, 33, 31,
- 34, 32, 38, -18, -18, -19, -18, -19, 15, 15,
- 15, -56, 22, 22, -56, 22, -58, 21, 2, 22,
- 7, 2, -39, -56, -28, 19, -28, 26, -28, -22,
- -22, 24, 17, 2, 17, 6, 6, 6, 6, 6,
- 6, 6, 6, 6, 6, 6, 22, 22, -56, 22,
- 7, 21, 2, 22, -4, 22, -28, 26, 26, 17,
- -24, -27, 57, -28, -32, -32, -32, -29, -25, 14,
- -25, -27, -25, -27, -11, 96, 97, 98, 99, 7,
- -56, -28, -28, -28, -26, -32, -56, 22, 24, 21,
- 2, 22, 21, -32,
+ 34, 32, 38, -45, 15, -45, -45, -18, -18, -19,
+ -18, -19, 15, 15, 15, -58, 22, 22, -58, 22,
+ -60, 21, 2, 22, 7, 2, -40, -58, -28, 19,
+ -28, 26, -28, -22, -22, 24, 17, 2, 17, 6,
+ 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
+ -48, -8, 84, 83, 22, 22, -58, 22, 7, 21,
+ 2, 22, -4, 22, -28, 26, 26, 17, -24, -27,
+ 57, -28, -32, -32, -32, -29, -25, 14, -25, -27,
+ -25, -27, -11, 99, 100, 101, 102, 22, -48, -45,
+ -45, 7, -58, -28, -28, -28, -26, -32, 22, -58,
+ 22, 24, 21, 2, 22, 21, -32,
}
var yyDef = [...]int16{
- 0, -2, 138, 138, 0, 0, 7, 6, 1, 138,
- 106, 107, 108, 109, 110, 111, 112, 113, 114, 115,
- 116, 117, 118, 119, 120, 121, 122, 123, 124, 125,
- 126, 127, 128, 129, 130, 131, 132, 133, 134, 0,
- 2, -2, 3, 4, 8, 9, 10, 11, 12, 13,
- 14, 15, 16, 17, 18, 19, 20, 21, 22, 0,
- 113, 246, 247, 0, 257, 0, 90, 91, 131, 132,
- 0, 284, -2, -2, -2, -2, -2, -2, -2, -2,
- -2, -2, -2, -2, -2, -2, 240, 241, 0, 5,
- 105, 0, 137, 140, 0, 144, 148, 258, 149, 153,
- 46, 46, 46, 46, 46, 46, 46, 46, 46, 46,
- 46, 46, 46, 46, 46, 46, 0, 74, 75, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 25, 26,
- 0, 0, 0, 64, 0, 22, 88, -2, 89, 0,
- 0, 0, 0, 94, 96, 0, 100, 104, 135, 0,
- 141, 0, 147, 0, 152, 0, 45, 50, 51, 47,
+ 0, -2, 149, 149, 0, 0, 7, 6, 1, 149,
+ 114, 115, 116, 117, 118, 119, 120, 121, 122, 123,
+ 124, 125, 126, 127, 128, 129, 130, 131, 132, 133,
+ 134, 135, 136, 137, 138, 139, 140, 141, 142, 143,
+ 144, 145, 0, 2, -2, 3, 4, 8, 9, 10,
+ 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
+ 21, 22, 0, 124, 260, 261, 0, 271, 0, 98,
+ 99, 142, 143, 0, 298, -2, -2, -2, -2, -2,
+ -2, -2, -2, -2, -2, -2, -2, -2, -2, 254,
+ 255, 0, 5, 113, 0, 148, 151, 0, 155, 159,
+ 272, 160, 164, 46, 46, 46, 46, 46, 46, 46,
+ 46, 46, 46, 46, 46, 46, 46, 46, 46, 0,
+ 82, 83, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 25, 26, 0, 0, 0, 72, 0, 22, 96,
+ -2, 97, 0, 0, 0, 0, 102, 104, 0, 108,
+ 112, 146, 0, 152, 0, 158, 0, 163, 0, 45,
+ 54, 50, 51, 47, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 80,
+ 81, 275, 0, 0, 0, 0, 284, 285, 286, 0,
+ 84, 0, 86, 266, 267, 87, 88, 262, 263, 0,
+ 0, 0, 95, 79, 287, 0, 0, 0, 289, 290,
+ 291, 292, 293, 294, 23, 24, 27, 0, 63, 28,
+ 0, 74, 76, 78, 299, 295, 296, 0, 100, 0,
+ 105, 0, 111, 256, 257, 258, 259, 0, 147, 150,
+ 153, 156, 154, 157, 162, 165, 167, 170, 174, 175,
+ 176, 0, 29, 0, 0, 0, 0, 0, -2, -2,
+ 30, 31, 32, 33, 34, 35, 36, 37, 38, 39,
+ 40, 41, 42, 43, 44, 276, 0, 0, 0, 0,
+ 288, 0, 0, 0, 0, 0, 264, 265, 89, 0,
+ 94, 0, 62, 65, 67, 68, 69, 218, 219, 220,
+ 221, 222, 223, 224, 225, 226, 227, 228, 229, 230,
+ 231, 232, 233, 234, 235, 236, 237, 238, 239, 240,
+ 241, 242, 243, 244, 245, 246, 247, 248, 249, 250,
+ 251, 252, 253, 73, 77, 0, 101, 103, 106, 110,
+ 107, 109, 0, 0, 0, 0, 0, 0, 0, 0,
+ 180, 182, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 55, 0, 56, 57, 48, 49, 52,
+ 274, 53, 0, 0, 0, 0, 277, 278, 0, 85,
+ 0, 91, 93, 60, 0, 66, 75, 0, 166, 268,
+ 168, 0, 171, 0, 0, 0, 178, 183, 179, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 72, 73, 261, 0, 0,
- 0, 0, 270, 271, 272, 0, 76, 0, 78, 252,
- 253, 79, 80, 248, 249, 0, 0, 0, 87, 71,
- 273, 0, 0, 0, 275, 276, 277, 278, 279, 280,
- 23, 24, 27, 0, 57, 28, 0, 66, 68, 70,
- 285, 281, 282, 0, 92, 0, 97, 0, 103, 242,
- 243, 244, 245, 0, 136, 139, 142, 145, 143, 146,
- 151, 154, 156, 159, 163, 164, 165, 0, 29, 0,
- 0, -2, -2, 30, 31, 32, 33, 34, 35, 36,
- 37, 38, 39, 40, 41, 42, 43, 44, 262, 0,
- 0, 0, 0, 274, 0, 0, 0, 0, 0, 250,
- 251, 81, 0, 86, 0, 56, 59, 61, 62, 63,
- 207, 208, 209, 210, 211, 212, 213, 214, 215, 216,
- 217, 218, 219, 220, 221, 222, 223, 224, 225, 226,
- 227, 228, 229, 230, 231, 232, 233, 234, 235, 236,
- 237, 238, 239, 65, 69, 0, 93, 95, 98, 102,
- 99, 101, 0, 0, 0, 0, 0, 0, 0, 0,
- 169, 171, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 48, 49, 52, 260, 53, 0, 0,
- 0, 0, 263, 264, 0, 77, 0, 83, 85, 54,
- 0, 60, 67, 0, 155, 254, 157, 0, 160, 0,
- 0, 0, 167, 172, 168, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 265, 266, 0, 269,
- 0, 82, 84, 55, 58, 283, 158, 0, 0, 166,
- 170, 173, 0, 256, 174, 175, 176, 177, 178, 0,
- 179, 180, 181, 182, 183, 189, 190, 191, 192, 0,
- 0, 161, 162, 255, 0, 187, 0, 267, 0, 185,
- 188, 268, 184, 186,
+ 0, 0, 0, 0, 279, 280, 0, 283, 0, 90,
+ 92, 61, 64, 297, 169, 0, 0, 177, 181, 184,
+ 0, 270, 185, 186, 187, 188, 189, 0, 190, 191,
+ 192, 193, 194, 200, 201, 202, 203, 70, 0, 58,
+ 59, 0, 0, 172, 173, 269, 0, 198, 71, 0,
+ 281, 0, 196, 199, 282, 195, 197,
}
var yyTok1 = [...]int8{
@@ -773,7 +811,7 @@ var yyTok2 = [...]int8{
72, 73, 74, 75, 76, 77, 78, 79, 80, 81,
82, 83, 84, 85, 86, 87, 88, 89, 90, 91,
92, 93, 94, 95, 96, 97, 98, 99, 100, 101,
- 102, 103, 104, 105, 106,
+ 102, 103, 104, 105, 106, 107, 108, 109,
}
var yyTok3 = [...]int8{
@@ -1298,44 +1336,83 @@ yydefault:
yyVAL.node.(*BinaryExpr).VectorMatching.Card = CardOneToMany
yyVAL.node.(*BinaryExpr).VectorMatching.Include = yyDollar[3].strings
}
- case 54:
+ case 55:
+ yyDollar = yyS[yypt-3 : yypt+1]
+ {
+ yyVAL.node = yyDollar[1].node
+ fill := yyDollar[3].node.(*NumberLiteral).Val
+ yyVAL.node.(*BinaryExpr).VectorMatching.FillValues.LHS = &fill
+ yyVAL.node.(*BinaryExpr).VectorMatching.FillValues.RHS = &fill
+ }
+ case 56:
+ yyDollar = yyS[yypt-3 : yypt+1]
+ {
+ yyVAL.node = yyDollar[1].node
+ fill := yyDollar[3].node.(*NumberLiteral).Val
+ yyVAL.node.(*BinaryExpr).VectorMatching.FillValues.LHS = &fill
+ }
+ case 57:
+ yyDollar = yyS[yypt-3 : yypt+1]
+ {
+ yyVAL.node = yyDollar[1].node
+ fill := yyDollar[3].node.(*NumberLiteral).Val
+ yyVAL.node.(*BinaryExpr).VectorMatching.FillValues.RHS = &fill
+ }
+ case 58:
+ yyDollar = yyS[yypt-5 : yypt+1]
+ {
+ yyVAL.node = yyDollar[1].node
+ fill_left := yyDollar[3].node.(*NumberLiteral).Val
+ fill_right := yyDollar[5].node.(*NumberLiteral).Val
+ yyVAL.node.(*BinaryExpr).VectorMatching.FillValues.LHS = &fill_left
+ yyVAL.node.(*BinaryExpr).VectorMatching.FillValues.RHS = &fill_right
+ }
+ case 59:
+ yyDollar = yyS[yypt-5 : yypt+1]
+ {
+ fill_right := yyDollar[3].node.(*NumberLiteral).Val
+ fill_left := yyDollar[5].node.(*NumberLiteral).Val
+ yyVAL.node.(*BinaryExpr).VectorMatching.FillValues.LHS = &fill_left
+ yyVAL.node.(*BinaryExpr).VectorMatching.FillValues.RHS = &fill_right
+ }
+ case 60:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.strings = yyDollar[2].strings
}
- case 55:
+ case 61:
yyDollar = yyS[yypt-4 : yypt+1]
{
yyVAL.strings = yyDollar[2].strings
}
- case 56:
+ case 62:
yyDollar = yyS[yypt-2 : yypt+1]
{
yyVAL.strings = []string{}
}
- case 57:
+ case 63:
yyDollar = yyS[yypt-1 : yypt+1]
{
yylex.(*parser).unexpected("grouping opts", "\"(\"")
yyVAL.strings = nil
}
- case 58:
+ case 64:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.strings = append(yyDollar[1].strings, yyDollar[3].item.Val)
}
- case 59:
+ case 65:
yyDollar = yyS[yypt-1 : yypt+1]
{
yyVAL.strings = []string{yyDollar[1].item.Val}
}
- case 60:
+ case 66:
yyDollar = yyS[yypt-2 : yypt+1]
{
yylex.(*parser).unexpected("grouping opts", "\",\" or \")\"")
yyVAL.strings = yyDollar[1].strings
}
- case 61:
+ case 67:
yyDollar = yyS[yypt-1 : yypt+1]
{
if !model.UTF8Validation.IsValidLabelName(yyDollar[1].item.Val) {
@@ -1343,7 +1420,7 @@ yydefault:
}
yyVAL.item = yyDollar[1].item
}
- case 62:
+ case 68:
yyDollar = yyS[yypt-1 : yypt+1]
{
unquoted := yylex.(*parser).unquoteString(yyDollar[1].item.Val)
@@ -1354,13 +1431,28 @@ yydefault:
yyVAL.item.Pos++
yyVAL.item.Val = unquoted
}
- case 63:
+ case 69:
yyDollar = yyS[yypt-1 : yypt+1]
{
yylex.(*parser).unexpected("grouping opts", "label")
yyVAL.item = Item{}
}
- case 64:
+ case 70:
+ yyDollar = yyS[yypt-3 : yypt+1]
+ {
+ yyVAL.node = yyDollar[2].node.(*NumberLiteral)
+ }
+ case 71:
+ yyDollar = yyS[yypt-4 : yypt+1]
+ {
+ nl := yyDollar[3].node.(*NumberLiteral)
+ if yyDollar[2].item.Typ == SUB {
+ nl.Val *= -1
+ }
+ nl.PosRange.Start = yyDollar[2].item.Pos
+ yyVAL.node = nl
+ }
+ case 72:
yyDollar = yyS[yypt-2 : yypt+1]
{
fn, exist := getFunction(yyDollar[1].item.Val, yylex.(*parser).functions)
@@ -1379,38 +1471,38 @@ yydefault:
},
}
}
- case 65:
+ case 73:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.node = yyDollar[2].node
}
- case 66:
+ case 74:
yyDollar = yyS[yypt-2 : yypt+1]
{
yyVAL.node = Expressions{}
}
- case 67:
+ case 75:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.node = append(yyDollar[1].node.(Expressions), yyDollar[3].node.(Expr))
}
- case 68:
+ case 76:
yyDollar = yyS[yypt-1 : yypt+1]
{
yyVAL.node = Expressions{yyDollar[1].node.(Expr)}
}
- case 69:
+ case 77:
yyDollar = yyS[yypt-2 : yypt+1]
{
yylex.(*parser).addParseErrf(yyDollar[2].item.PositionRange(), "trailing commas not allowed in function call args")
yyVAL.node = yyDollar[1].node
}
- case 70:
+ case 78:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.node = &ParenExpr{Expr: yyDollar[2].node.(Expr), PosRange: mergeRanges(&yyDollar[1].item, &yyDollar[3].item)}
}
- case 71:
+ case 79:
yyDollar = yyS[yypt-1 : yypt+1]
{
if numLit, ok := yyDollar[1].node.(*NumberLiteral); ok {
@@ -1424,7 +1516,7 @@ yydefault:
}
yyVAL.node = yyDollar[1].node
}
- case 72:
+ case 80:
yyDollar = yyS[yypt-3 : yypt+1]
{
if numLit, ok := yyDollar[3].node.(*NumberLiteral); ok {
@@ -1435,41 +1527,41 @@ yydefault:
yylex.(*parser).addOffsetExpr(yyDollar[1].node, yyDollar[3].node.(*DurationExpr))
yyVAL.node = yyDollar[1].node
}
- case 73:
+ case 81:
yyDollar = yyS[yypt-3 : yypt+1]
{
yylex.(*parser).unexpected("offset", "number, duration, step(), or range()")
yyVAL.node = yyDollar[1].node
}
- case 74:
+ case 82:
yyDollar = yyS[yypt-2 : yypt+1]
{
yylex.(*parser).setAnchored(yyDollar[1].node)
}
- case 75:
+ case 83:
yyDollar = yyS[yypt-2 : yypt+1]
{
yylex.(*parser).setSmoothed(yyDollar[1].node)
}
- case 76:
+ case 84:
yyDollar = yyS[yypt-3 : yypt+1]
{
yylex.(*parser).setTimestamp(yyDollar[1].node, yyDollar[3].float)
yyVAL.node = yyDollar[1].node
}
- case 77:
+ case 85:
yyDollar = yyS[yypt-5 : yypt+1]
{
yylex.(*parser).setAtModifierPreprocessor(yyDollar[1].node, yyDollar[3].item)
yyVAL.node = yyDollar[1].node
}
- case 78:
+ case 86:
yyDollar = yyS[yypt-3 : yypt+1]
{
yylex.(*parser).unexpected("@", "timestamp")
yyVAL.node = yyDollar[1].node
}
- case 81:
+ case 89:
yyDollar = yyS[yypt-4 : yypt+1]
{
var errMsg string
@@ -1499,7 +1591,7 @@ yydefault:
EndPos: yylex.(*parser).lastClosing,
}
}
- case 82:
+ case 90:
yyDollar = yyS[yypt-6 : yypt+1]
{
var rangeNl time.Duration
@@ -1521,7 +1613,7 @@ yydefault:
EndPos: yyDollar[6].item.Pos + 1,
}
}
- case 83:
+ case 91:
yyDollar = yyS[yypt-5 : yypt+1]
{
var rangeNl time.Duration
@@ -1536,31 +1628,31 @@ yydefault:
EndPos: yyDollar[5].item.Pos + 1,
}
}
- case 84:
+ case 92:
yyDollar = yyS[yypt-6 : yypt+1]
{
yylex.(*parser).unexpected("subquery selector", "\"]\"")
yyVAL.node = yyDollar[1].node
}
- case 85:
+ case 93:
yyDollar = yyS[yypt-5 : yypt+1]
{
yylex.(*parser).unexpected("subquery selector", "number, duration, step(), range(), or \"]\"")
yyVAL.node = yyDollar[1].node
}
- case 86:
+ case 94:
yyDollar = yyS[yypt-4 : yypt+1]
{
yylex.(*parser).unexpected("subquery or range", "\":\" or \"]\"")
yyVAL.node = yyDollar[1].node
}
- case 87:
+ case 95:
yyDollar = yyS[yypt-3 : yypt+1]
{
yylex.(*parser).unexpected("subquery or range selector", "number, duration, step(), or range()")
yyVAL.node = yyDollar[1].node
}
- case 88:
+ case 96:
yyDollar = yyS[yypt-2 : yypt+1]
{
if nl, ok := yyDollar[2].node.(*NumberLiteral); ok {
@@ -1573,7 +1665,7 @@ yydefault:
yyVAL.node = &UnaryExpr{Op: yyDollar[1].item.Typ, Expr: yyDollar[2].node.(Expr), StartPos: yyDollar[1].item.Pos}
}
}
- case 89:
+ case 97:
yyDollar = yyS[yypt-2 : yypt+1]
{
vs := yyDollar[2].node.(*VectorSelector)
@@ -1582,7 +1674,7 @@ yydefault:
yylex.(*parser).assembleVectorSelector(vs)
yyVAL.node = vs
}
- case 90:
+ case 98:
yyDollar = yyS[yypt-1 : yypt+1]
{
vs := &VectorSelector{
@@ -1593,14 +1685,14 @@ yydefault:
yylex.(*parser).assembleVectorSelector(vs)
yyVAL.node = vs
}
- case 91:
+ case 99:
yyDollar = yyS[yypt-1 : yypt+1]
{
vs := yyDollar[1].node.(*VectorSelector)
yylex.(*parser).assembleVectorSelector(vs)
yyVAL.node = vs
}
- case 92:
+ case 100:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.node = &VectorSelector{
@@ -1608,7 +1700,7 @@ yydefault:
PosRange: mergeRanges(&yyDollar[1].item, &yyDollar[3].item),
}
}
- case 93:
+ case 101:
yyDollar = yyS[yypt-4 : yypt+1]
{
yyVAL.node = &VectorSelector{
@@ -1616,7 +1708,7 @@ yydefault:
PosRange: mergeRanges(&yyDollar[1].item, &yyDollar[4].item),
}
}
- case 94:
+ case 102:
yyDollar = yyS[yypt-2 : yypt+1]
{
yyVAL.node = &VectorSelector{
@@ -1624,7 +1716,7 @@ yydefault:
PosRange: mergeRanges(&yyDollar[1].item, &yyDollar[2].item),
}
}
- case 95:
+ case 103:
yyDollar = yyS[yypt-3 : yypt+1]
{
if yyDollar[1].matchers != nil {
@@ -1633,144 +1725,144 @@ yydefault:
yyVAL.matchers = yyDollar[1].matchers
}
}
- case 96:
+ case 104:
yyDollar = yyS[yypt-1 : yypt+1]
{
yyVAL.matchers = []*labels.Matcher{yyDollar[1].matcher}
}
- case 97:
+ case 105:
yyDollar = yyS[yypt-2 : yypt+1]
{
yylex.(*parser).unexpected("label matching", "\",\" or \"}\"")
yyVAL.matchers = yyDollar[1].matchers
}
- case 98:
+ case 106:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.matcher = yylex.(*parser).newLabelMatcher(yyDollar[1].item, yyDollar[2].item, yyDollar[3].item)
}
- case 99:
+ case 107:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.matcher = yylex.(*parser).newLabelMatcher(yyDollar[1].item, yyDollar[2].item, yyDollar[3].item)
}
- case 100:
+ case 108:
yyDollar = yyS[yypt-1 : yypt+1]
{
yyVAL.matcher = yylex.(*parser).newMetricNameMatcher(yyDollar[1].item)
}
- case 101:
+ case 109:
yyDollar = yyS[yypt-3 : yypt+1]
{
yylex.(*parser).unexpected("label matching", "string")
yyVAL.matcher = nil
}
- case 102:
+ case 110:
yyDollar = yyS[yypt-3 : yypt+1]
{
yylex.(*parser).unexpected("label matching", "string")
yyVAL.matcher = nil
}
- case 103:
+ case 111:
yyDollar = yyS[yypt-2 : yypt+1]
{
yylex.(*parser).unexpected("label matching", "label matching operator")
yyVAL.matcher = nil
}
- case 104:
+ case 112:
yyDollar = yyS[yypt-1 : yypt+1]
{
yylex.(*parser).unexpected("label matching", "identifier or \"}\"")
yyVAL.matcher = nil
}
- case 105:
+ case 113:
yyDollar = yyS[yypt-2 : yypt+1]
{
b := labels.NewBuilder(yyDollar[2].labels)
b.Set(labels.MetricName, yyDollar[1].item.Val)
yyVAL.labels = b.Labels()
}
- case 106:
+ case 114:
yyDollar = yyS[yypt-1 : yypt+1]
{
yyVAL.labels = yyDollar[1].labels
}
- case 135:
+ case 146:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.labels = labels.New(yyDollar[2].lblList...)
}
- case 136:
+ case 147:
yyDollar = yyS[yypt-4 : yypt+1]
{
yyVAL.labels = labels.New(yyDollar[2].lblList...)
}
- case 137:
+ case 148:
yyDollar = yyS[yypt-2 : yypt+1]
{
yyVAL.labels = labels.New()
}
- case 138:
+ case 149:
yyDollar = yyS[yypt-0 : yypt+1]
{
yyVAL.labels = labels.New()
}
- case 139:
+ case 150:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.lblList = append(yyDollar[1].lblList, yyDollar[3].label)
}
- case 140:
+ case 151:
yyDollar = yyS[yypt-1 : yypt+1]
{
yyVAL.lblList = []labels.Label{yyDollar[1].label}
}
- case 141:
+ case 152:
yyDollar = yyS[yypt-2 : yypt+1]
{
yylex.(*parser).unexpected("label set", "\",\" or \"}\"")
yyVAL.lblList = yyDollar[1].lblList
}
- case 142:
+ case 153:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.label = labels.Label{Name: yyDollar[1].item.Val, Value: yylex.(*parser).unquoteString(yyDollar[3].item.Val)}
}
- case 143:
+ case 154:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.label = labels.Label{Name: yyDollar[1].item.Val, Value: yylex.(*parser).unquoteString(yyDollar[3].item.Val)}
}
- case 144:
+ case 155:
yyDollar = yyS[yypt-1 : yypt+1]
{
yyVAL.label = labels.Label{Name: labels.MetricName, Value: yyDollar[1].item.Val}
}
- case 145:
+ case 156:
yyDollar = yyS[yypt-3 : yypt+1]
{
yylex.(*parser).unexpected("label set", "string")
yyVAL.label = labels.Label{}
}
- case 146:
+ case 157:
yyDollar = yyS[yypt-3 : yypt+1]
{
yylex.(*parser).unexpected("label set", "string")
yyVAL.label = labels.Label{}
}
- case 147:
+ case 158:
yyDollar = yyS[yypt-2 : yypt+1]
{
yylex.(*parser).unexpected("label set", "\"=\"")
yyVAL.label = labels.Label{}
}
- case 148:
+ case 159:
yyDollar = yyS[yypt-1 : yypt+1]
{
yylex.(*parser).unexpected("label set", "identifier or \"}\"")
yyVAL.label = labels.Label{}
}
- case 149:
+ case 160:
yyDollar = yyS[yypt-2 : yypt+1]
{
yylex.(*parser).generatedParserResult = &seriesDescription{
@@ -1778,33 +1870,33 @@ yydefault:
values: yyDollar[2].series,
}
}
- case 150:
+ case 161:
yyDollar = yyS[yypt-0 : yypt+1]
{
yyVAL.series = []SequenceValue{}
}
- case 151:
+ case 162:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.series = append(yyDollar[1].series, yyDollar[3].series...)
}
- case 152:
+ case 163:
yyDollar = yyS[yypt-2 : yypt+1]
{
yyVAL.series = yyDollar[1].series
}
- case 153:
+ case 164:
yyDollar = yyS[yypt-1 : yypt+1]
{
yylex.(*parser).unexpected("series values", "")
yyVAL.series = nil
}
- case 154:
+ case 165:
yyDollar = yyS[yypt-1 : yypt+1]
{
yyVAL.series = []SequenceValue{{Omitted: true}}
}
- case 155:
+ case 166:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.series = []SequenceValue{}
@@ -1812,12 +1904,12 @@ yydefault:
yyVAL.series = append(yyVAL.series, SequenceValue{Omitted: true})
}
}
- case 156:
+ case 167:
yyDollar = yyS[yypt-1 : yypt+1]
{
yyVAL.series = []SequenceValue{{Value: yyDollar[1].float}}
}
- case 157:
+ case 168:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.series = []SequenceValue{}
@@ -1826,7 +1918,7 @@ yydefault:
yyVAL.series = append(yyVAL.series, SequenceValue{Value: yyDollar[1].float})
}
}
- case 158:
+ case 169:
yyDollar = yyS[yypt-4 : yypt+1]
{
yyVAL.series = []SequenceValue{}
@@ -1836,22 +1928,23 @@ yydefault:
yyDollar[1].float += yyDollar[2].float
}
}
- case 159:
+ case 170:
yyDollar = yyS[yypt-1 : yypt+1]
{
- yyVAL.series = []SequenceValue{{Histogram: yyDollar[1].histogram}}
+ yyVAL.series = []SequenceValue{yylex.(*parser).newHistogramSequenceValue(yyDollar[1].histogram)}
}
- case 160:
+ case 171:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.series = []SequenceValue{}
// Add an additional value for time 0, which we ignore in tests.
+ sv := yylex.(*parser).newHistogramSequenceValue(yyDollar[1].histogram)
for i := uint64(0); i <= yyDollar[3].uint; i++ {
- yyVAL.series = append(yyVAL.series, SequenceValue{Histogram: yyDollar[1].histogram})
+ yyVAL.series = append(yyVAL.series, sv)
//$1 += $2
}
}
- case 161:
+ case 172:
yyDollar = yyS[yypt-5 : yypt+1]
{
val, err := yylex.(*parser).histogramsIncreaseSeries(yyDollar[1].histogram, yyDollar[3].histogram, yyDollar[5].uint)
@@ -1860,7 +1953,7 @@ yydefault:
}
yyVAL.series = val
}
- case 162:
+ case 173:
yyDollar = yyS[yypt-5 : yypt+1]
{
val, err := yylex.(*parser).histogramsDecreaseSeries(yyDollar[1].histogram, yyDollar[3].histogram, yyDollar[5].uint)
@@ -1869,7 +1962,7 @@ yydefault:
}
yyVAL.series = val
}
- case 163:
+ case 174:
yyDollar = yyS[yypt-1 : yypt+1]
{
if yyDollar[1].item.Val != "stale" {
@@ -1877,130 +1970,130 @@ yydefault:
}
yyVAL.float = math.Float64frombits(value.StaleNaN)
}
- case 166:
+ case 177:
yyDollar = yyS[yypt-4 : yypt+1]
{
yyVAL.histogram = yylex.(*parser).buildHistogramFromMap(&yyDollar[2].descriptors)
}
- case 167:
+ case 178:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.histogram = yylex.(*parser).buildHistogramFromMap(&yyDollar[2].descriptors)
}
- case 168:
+ case 179:
yyDollar = yyS[yypt-3 : yypt+1]
{
m := yylex.(*parser).newMap()
yyVAL.histogram = yylex.(*parser).buildHistogramFromMap(&m)
}
- case 169:
+ case 180:
yyDollar = yyS[yypt-2 : yypt+1]
{
m := yylex.(*parser).newMap()
yyVAL.histogram = yylex.(*parser).buildHistogramFromMap(&m)
}
- case 170:
+ case 181:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.descriptors = *(yylex.(*parser).mergeMaps(&yyDollar[1].descriptors, &yyDollar[3].descriptors))
}
- case 171:
+ case 182:
yyDollar = yyS[yypt-1 : yypt+1]
{
yyVAL.descriptors = yyDollar[1].descriptors
}
- case 172:
+ case 183:
yyDollar = yyS[yypt-2 : yypt+1]
{
yylex.(*parser).unexpected("histogram description", "histogram description key, e.g. buckets:[5 10 7]")
}
- case 173:
+ case 184:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.descriptors = yylex.(*parser).newMap()
yyVAL.descriptors["schema"] = yyDollar[3].int
}
- case 174:
+ case 185:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.descriptors = yylex.(*parser).newMap()
yyVAL.descriptors["sum"] = yyDollar[3].float
}
- case 175:
+ case 186:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.descriptors = yylex.(*parser).newMap()
yyVAL.descriptors["count"] = yyDollar[3].float
}
- case 176:
+ case 187:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.descriptors = yylex.(*parser).newMap()
yyVAL.descriptors["z_bucket"] = yyDollar[3].float
}
- case 177:
+ case 188:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.descriptors = yylex.(*parser).newMap()
yyVAL.descriptors["z_bucket_w"] = yyDollar[3].float
}
- case 178:
+ case 189:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.descriptors = yylex.(*parser).newMap()
yyVAL.descriptors["custom_values"] = yyDollar[3].bucket_set
}
- case 179:
+ case 190:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.descriptors = yylex.(*parser).newMap()
yyVAL.descriptors["buckets"] = yyDollar[3].bucket_set
}
- case 180:
+ case 191:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.descriptors = yylex.(*parser).newMap()
yyVAL.descriptors["offset"] = yyDollar[3].int
}
- case 181:
+ case 192:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.descriptors = yylex.(*parser).newMap()
yyVAL.descriptors["n_buckets"] = yyDollar[3].bucket_set
}
- case 182:
+ case 193:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.descriptors = yylex.(*parser).newMap()
yyVAL.descriptors["n_offset"] = yyDollar[3].int
}
- case 183:
+ case 194:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.descriptors = yylex.(*parser).newMap()
yyVAL.descriptors["counter_reset_hint"] = yyDollar[3].item
}
- case 184:
+ case 195:
yyDollar = yyS[yypt-4 : yypt+1]
{
yyVAL.bucket_set = yyDollar[2].bucket_set
}
- case 185:
+ case 196:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.bucket_set = yyDollar[2].bucket_set
}
- case 186:
+ case 197:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.bucket_set = append(yyDollar[1].bucket_set, yyDollar[3].float)
}
- case 187:
+ case 198:
yyDollar = yyS[yypt-1 : yypt+1]
{
yyVAL.bucket_set = []float64{yyDollar[1].float}
}
- case 246:
+ case 260:
yyDollar = yyS[yypt-1 : yypt+1]
{
yyVAL.node = &NumberLiteral{
@@ -2008,7 +2101,7 @@ yydefault:
PosRange: yyDollar[1].item.PositionRange(),
}
}
- case 247:
+ case 261:
yyDollar = yyS[yypt-1 : yypt+1]
{
var err error
@@ -2023,12 +2116,12 @@ yydefault:
Duration: true,
}
}
- case 248:
+ case 262:
yyDollar = yyS[yypt-1 : yypt+1]
{
yyVAL.float = yylex.(*parser).number(yyDollar[1].item.Val)
}
- case 249:
+ case 263:
yyDollar = yyS[yypt-1 : yypt+1]
{
var err error
@@ -2039,17 +2132,17 @@ yydefault:
}
yyVAL.float = dur.Seconds()
}
- case 250:
+ case 264:
yyDollar = yyS[yypt-2 : yypt+1]
{
yyVAL.float = yyDollar[2].float
}
- case 251:
+ case 265:
yyDollar = yyS[yypt-2 : yypt+1]
{
yyVAL.float = -yyDollar[2].float
}
- case 254:
+ case 268:
yyDollar = yyS[yypt-1 : yypt+1]
{
var err error
@@ -2058,17 +2151,17 @@ yydefault:
yylex.(*parser).addParseErrf(yyDollar[1].item.PositionRange(), "invalid repetition in series values: %s", err)
}
}
- case 255:
+ case 269:
yyDollar = yyS[yypt-2 : yypt+1]
{
yyVAL.int = -int64(yyDollar[2].uint)
}
- case 256:
+ case 270:
yyDollar = yyS[yypt-1 : yypt+1]
{
yyVAL.int = int64(yyDollar[1].uint)
}
- case 257:
+ case 271:
yyDollar = yyS[yypt-1 : yypt+1]
{
yyVAL.node = &StringLiteral{
@@ -2076,7 +2169,7 @@ yydefault:
PosRange: yyDollar[1].item.PositionRange(),
}
}
- case 258:
+ case 272:
yyDollar = yyS[yypt-1 : yypt+1]
{
yyVAL.item = Item{
@@ -2085,12 +2178,12 @@ yydefault:
Val: yylex.(*parser).unquoteString(yyDollar[1].item.Val),
}
}
- case 259:
+ case 273:
yyDollar = yyS[yypt-0 : yypt+1]
{
yyVAL.strings = nil
}
- case 261:
+ case 275:
yyDollar = yyS[yypt-1 : yypt+1]
{
nl := yyDollar[1].node.(*NumberLiteral)
@@ -2101,7 +2194,7 @@ yydefault:
}
yyVAL.node = nl
}
- case 262:
+ case 276:
yyDollar = yyS[yypt-2 : yypt+1]
{
nl := yyDollar[2].node.(*NumberLiteral)
@@ -2116,7 +2209,7 @@ yydefault:
nl.PosRange.Start = yyDollar[1].item.Pos
yyVAL.node = nl
}
- case 263:
+ case 277:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.node = &DurationExpr{
@@ -2125,7 +2218,7 @@ yydefault:
EndPos: yyDollar[3].item.PositionRange().End,
}
}
- case 264:
+ case 278:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.node = &DurationExpr{
@@ -2134,7 +2227,7 @@ yydefault:
EndPos: yyDollar[3].item.PositionRange().End,
}
}
- case 265:
+ case 279:
yyDollar = yyS[yypt-4 : yypt+1]
{
yyVAL.node = &DurationExpr{
@@ -2147,7 +2240,7 @@ yydefault:
StartPos: yyDollar[1].item.Pos,
}
}
- case 266:
+ case 280:
yyDollar = yyS[yypt-4 : yypt+1]
{
yyVAL.node = &DurationExpr{
@@ -2160,7 +2253,7 @@ yydefault:
StartPos: yyDollar[1].item.Pos,
}
}
- case 267:
+ case 281:
yyDollar = yyS[yypt-6 : yypt+1]
{
yyVAL.node = &DurationExpr{
@@ -2171,7 +2264,7 @@ yydefault:
RHS: yyDollar[5].node.(Expr),
}
}
- case 268:
+ case 282:
yyDollar = yyS[yypt-7 : yypt+1]
{
yyVAL.node = &DurationExpr{
@@ -2187,7 +2280,7 @@ yydefault:
},
}
}
- case 269:
+ case 283:
yyDollar = yyS[yypt-4 : yypt+1]
{
de := yyDollar[3].node.(*DurationExpr)
@@ -2202,7 +2295,7 @@ yydefault:
}
yyVAL.node = yyDollar[3].node
}
- case 273:
+ case 287:
yyDollar = yyS[yypt-1 : yypt+1]
{
nl := yyDollar[1].node.(*NumberLiteral)
@@ -2213,7 +2306,7 @@ yydefault:
}
yyVAL.node = nl
}
- case 274:
+ case 288:
yyDollar = yyS[yypt-2 : yypt+1]
{
switch expr := yyDollar[2].node.(type) {
@@ -2246,25 +2339,25 @@ yydefault:
break
}
}
- case 275:
+ case 289:
yyDollar = yyS[yypt-3 : yypt+1]
{
yylex.(*parser).experimentalDurationExpr(yyDollar[1].node.(Expr))
yyVAL.node = &DurationExpr{Op: ADD, LHS: yyDollar[1].node.(Expr), RHS: yyDollar[3].node.(Expr)}
}
- case 276:
+ case 290:
yyDollar = yyS[yypt-3 : yypt+1]
{
yylex.(*parser).experimentalDurationExpr(yyDollar[1].node.(Expr))
yyVAL.node = &DurationExpr{Op: SUB, LHS: yyDollar[1].node.(Expr), RHS: yyDollar[3].node.(Expr)}
}
- case 277:
+ case 291:
yyDollar = yyS[yypt-3 : yypt+1]
{
yylex.(*parser).experimentalDurationExpr(yyDollar[1].node.(Expr))
yyVAL.node = &DurationExpr{Op: MUL, LHS: yyDollar[1].node.(Expr), RHS: yyDollar[3].node.(Expr)}
}
- case 278:
+ case 292:
yyDollar = yyS[yypt-3 : yypt+1]
{
yylex.(*parser).experimentalDurationExpr(yyDollar[1].node.(Expr))
@@ -2275,7 +2368,7 @@ yydefault:
}
yyVAL.node = &DurationExpr{Op: DIV, LHS: yyDollar[1].node.(Expr), RHS: yyDollar[3].node.(Expr)}
}
- case 279:
+ case 293:
yyDollar = yyS[yypt-3 : yypt+1]
{
yylex.(*parser).experimentalDurationExpr(yyDollar[1].node.(Expr))
@@ -2286,13 +2379,13 @@ yydefault:
}
yyVAL.node = &DurationExpr{Op: MOD, LHS: yyDollar[1].node.(Expr), RHS: yyDollar[3].node.(Expr)}
}
- case 280:
+ case 294:
yyDollar = yyS[yypt-3 : yypt+1]
{
yylex.(*parser).experimentalDurationExpr(yyDollar[1].node.(Expr))
yyVAL.node = &DurationExpr{Op: POW, LHS: yyDollar[1].node.(Expr), RHS: yyDollar[3].node.(Expr)}
}
- case 281:
+ case 295:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.node = &DurationExpr{
@@ -2301,7 +2394,7 @@ yydefault:
EndPos: yyDollar[3].item.PositionRange().End,
}
}
- case 282:
+ case 296:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.node = &DurationExpr{
@@ -2310,7 +2403,7 @@ yydefault:
EndPos: yyDollar[3].item.PositionRange().End,
}
}
- case 283:
+ case 297:
yyDollar = yyS[yypt-6 : yypt+1]
{
yyVAL.node = &DurationExpr{
@@ -2321,7 +2414,7 @@ yydefault:
RHS: yyDollar[5].node.(Expr),
}
}
- case 285:
+ case 299:
yyDollar = yyS[yypt-3 : yypt+1]
{
yylex.(*parser).experimentalDurationExpr(yyDollar[2].node.(Expr))
diff --git a/promql/parser/lex.go b/promql/parser/lex.go
index b3a82dc0c6..7149985767 100644
--- a/promql/parser/lex.go
+++ b/promql/parser/lex.go
@@ -137,6 +137,9 @@ var key = map[string]ItemType{
"ignoring": IGNORING,
"group_left": GROUP_LEFT,
"group_right": GROUP_RIGHT,
+ "fill": FILL,
+ "fill_left": FILL_LEFT,
+ "fill_right": FILL_RIGHT,
"bool": BOOL,
// Preprocessors.
@@ -1083,6 +1086,17 @@ Loop:
word := l.input[l.start:l.pos]
switch kw, ok := key[strings.ToLower(word)]; {
case ok:
+ // For fill/fill_left/fill_right, only treat as keyword if followed by '('
+ // This allows using these as metric names (e.g., "fill + fill").
+ // This could be done for other keywords as well, but for the new fill
+ // modifiers this is especially important so we don't break any existing
+ // queries.
+ if kw == FILL || kw == FILL_LEFT || kw == FILL_RIGHT {
+ if !l.peekFollowedByLeftParen() {
+ l.emit(IDENTIFIER)
+ break Loop
+ }
+ }
l.emit(kw)
case !strings.Contains(word, ":"):
l.emit(IDENTIFIER)
@@ -1098,6 +1112,23 @@ Loop:
return lexStatements
}
+// peekFollowedByLeftParen checks if the next non-whitespace character is '('.
+// This is used for context-sensitive keywords like fill/fill_left/fill_right
+// that should only be treated as keywords when followed by '('.
+func (l *Lexer) peekFollowedByLeftParen() bool {
+ pos := l.pos
+ for {
+ if int(pos) >= len(l.input) {
+ return false
+ }
+ r, w := utf8.DecodeRuneInString(l.input[pos:])
+ if !isSpace(r) {
+ return r == '('
+ }
+ pos += posrange.Pos(w)
+ }
+}
+
func isSpace(r rune) bool {
return r == ' ' || r == '\t' || r == '\n' || r == '\r'
}
diff --git a/promql/parser/parse.go b/promql/parser/parse.go
index 817e0d02d9..cefc627fda 100644
--- a/promql/parser/parse.go
+++ b/promql/parser/parse.go
@@ -45,6 +45,9 @@ var ExperimentalDurationExpr bool
// EnableExtendedRangeSelectors is a flag to enable experimental extended range selectors.
var EnableExtendedRangeSelectors bool
+// EnableBinopFillModifiers is a flag to enable experimental fill modifiers for binary operators.
+var EnableBinopFillModifiers bool
+
type Parser interface {
ParseExpr() (Expr, error)
Close()
@@ -67,6 +70,11 @@ type parser struct {
generatedParserResult any
parseErrors ParseErrors
+
+ // lastHistogramCounterResetHintSet is set to true when the most recently
+ // built histogram had a counter_reset_hint explicitly specified.
+ // This is used to populate CounterResetHintSet in SequenceValue.
+ lastHistogramCounterResetHintSet bool
}
type Opt func(p *parser)
@@ -234,6 +242,11 @@ type SequenceValue struct {
Value float64
Omitted bool
Histogram *histogram.FloatHistogram
+ // CounterResetHintSet is true if the counter reset hint was explicitly
+ // specified in the test file using counter_reset_hint:... syntax.
+ // This allows distinguishing between "no hint specified" (don't care)
+ // vs "counter_reset_hint:unknown" (verify it's unknown).
+ CounterResetHintSet bool
}
func (v SequenceValue) String() string {
@@ -413,13 +426,18 @@ func (p *parser) InjectItem(typ ItemType) {
p.injecting = true
}
-func (*parser) newBinaryExpression(lhs Node, op Item, modifiers, rhs Node) *BinaryExpr {
+func (p *parser) newBinaryExpression(lhs Node, op Item, modifiers, rhs Node) *BinaryExpr {
ret := modifiers.(*BinaryExpr)
ret.LHS = lhs.(Expr)
ret.RHS = rhs.(Expr)
ret.Op = op.Typ
+ if !EnableBinopFillModifiers && (ret.VectorMatching.FillValues.LHS != nil || ret.VectorMatching.FillValues.RHS != nil) {
+ p.addParseErrf(ret.PositionRange(), "binop fill modifiers are experimental and not enabled")
+ return ret
+ }
+
return ret
}
@@ -496,25 +514,30 @@ func (p *parser) mergeMaps(left, right *map[string]any) (ret *map[string]any) {
}
func (p *parser) histogramsIncreaseSeries(base, inc *histogram.FloatHistogram, times uint64) ([]SequenceValue, error) {
- return p.histogramsSeries(base, inc, times, func(a, b *histogram.FloatHistogram) (*histogram.FloatHistogram, error) {
+ // Capture the hint set flag immediately after inc histogram is built.
+ // The base histogram's hint set flag was already captured.
+ hintSet := p.lastHistogramCounterResetHintSet
+ return p.histogramsSeries(base, inc, times, hintSet, func(a, b *histogram.FloatHistogram) (*histogram.FloatHistogram, error) {
res, _, _, err := a.Add(b)
return res, err
})
}
func (p *parser) histogramsDecreaseSeries(base, inc *histogram.FloatHistogram, times uint64) ([]SequenceValue, error) {
- return p.histogramsSeries(base, inc, times, func(a, b *histogram.FloatHistogram) (*histogram.FloatHistogram, error) {
+ // Capture the hint set flag immediately after inc histogram is built.
+ hintSet := p.lastHistogramCounterResetHintSet
+ return p.histogramsSeries(base, inc, times, hintSet, func(a, b *histogram.FloatHistogram) (*histogram.FloatHistogram, error) {
res, _, _, err := a.Sub(b)
return res, err
})
}
-func (*parser) histogramsSeries(base, inc *histogram.FloatHistogram, times uint64,
+func (*parser) histogramsSeries(base, inc *histogram.FloatHistogram, times uint64, counterResetHintSet bool,
combine func(*histogram.FloatHistogram, *histogram.FloatHistogram) (*histogram.FloatHistogram, error),
) ([]SequenceValue, error) {
ret := make([]SequenceValue, times+1)
// Add an additional value (the base) for time 0, which we ignore in tests.
- ret[0] = SequenceValue{Histogram: base}
+ ret[0] = SequenceValue{Histogram: base, CounterResetHintSet: counterResetHintSet}
cur := base
for i := uint64(1); i <= times; i++ {
if cur.Schema > inc.Schema {
@@ -526,7 +549,7 @@ func (*parser) histogramsSeries(base, inc *histogram.FloatHistogram, times uint6
if err != nil {
return ret, err
}
- ret[i] = SequenceValue{Histogram: cur}
+ ret[i] = SequenceValue{Histogram: cur, CounterResetHintSet: counterResetHintSet}
}
return ret, nil
@@ -535,6 +558,8 @@ func (*parser) histogramsSeries(base, inc *histogram.FloatHistogram, times uint6
// buildHistogramFromMap is used in the grammar to take then individual parts of the histogram and complete it.
func (p *parser) buildHistogramFromMap(desc *map[string]any) *histogram.FloatHistogram {
output := &histogram.FloatHistogram{}
+ // Reset the flag for each new histogram being built.
+ p.lastHistogramCounterResetHintSet = false
val, ok := (*desc)["schema"]
if ok {
@@ -595,6 +620,8 @@ func (p *parser) buildHistogramFromMap(desc *map[string]any) *histogram.FloatHis
val, ok = (*desc)["counter_reset_hint"]
if ok {
+ // Mark that the counter reset hint was explicitly specified.
+ p.lastHistogramCounterResetHintSet = true
resetHint, ok := val.(Item)
if ok {
@@ -626,6 +653,16 @@ func (p *parser) buildHistogramFromMap(desc *map[string]any) *histogram.FloatHis
return output
}
+// newHistogramSequenceValue creates a SequenceValue for a histogram,
+// setting CounterResetHintSet based on whether counter_reset_hint was
+// explicitly specified in the histogram description.
+func (p *parser) newHistogramSequenceValue(h *histogram.FloatHistogram) SequenceValue {
+ return SequenceValue{
+ Histogram: h,
+ CounterResetHintSet: p.lastHistogramCounterResetHintSet,
+ }
+}
+
func (p *parser) buildHistogramBucketsAndSpans(desc *map[string]any, bucketsKey, offsetKey string,
) (buckets []float64, spans []histogram.Span) {
bucketCount := 0
@@ -768,6 +805,9 @@ func (p *parser) checkAST(node Node) (typ ValueType) {
if len(n.VectorMatching.MatchingLabels) > 0 {
p.addParseErrf(n.PositionRange(), "vector matching only allowed between instant vectors")
}
+ if n.VectorMatching.FillValues.LHS != nil || n.VectorMatching.FillValues.RHS != nil {
+ p.addParseErrf(n.PositionRange(), "filling in missing series only allowed between instant vectors")
+ }
n.VectorMatching = nil
case n.Op.IsSetOperator(): // Both operands are Vectors.
if n.VectorMatching.Card == CardOneToMany || n.VectorMatching.Card == CardManyToOne {
@@ -776,6 +816,9 @@ func (p *parser) checkAST(node Node) (typ ValueType) {
if n.VectorMatching.Card != CardManyToMany {
p.addParseErrf(n.PositionRange(), "set operations must always be many-to-many")
}
+ if n.VectorMatching.FillValues.LHS != nil || n.VectorMatching.FillValues.RHS != nil {
+ p.addParseErrf(n.PositionRange(), "filling in missing series not allowed for set operators")
+ }
}
if (lt == ValueTypeScalar || rt == ValueTypeScalar) && n.Op.IsSetOperator() {
diff --git a/promql/parser/printer.go b/promql/parser/printer.go
index 01e2c46c1b..44ca15e532 100644
--- a/promql/parser/printer.go
+++ b/promql/parser/printer.go
@@ -172,6 +172,19 @@ func (node *BinaryExpr) getMatchingStr() string {
b.WriteString(")")
matching += b.String()
}
+
+ if vm.FillValues.LHS != nil || vm.FillValues.RHS != nil {
+ if vm.FillValues.LHS == vm.FillValues.RHS {
+ matching += fmt.Sprintf(" fill (%v)", *vm.FillValues.LHS)
+ } else {
+ if vm.FillValues.LHS != nil {
+ matching += fmt.Sprintf(" fill_left (%v)", *vm.FillValues.LHS)
+ }
+ if vm.FillValues.RHS != nil {
+ matching += fmt.Sprintf(" fill_right (%v)", *vm.FillValues.RHS)
+ }
+ }
+ }
}
return matching
}
diff --git a/promql/parser/printer_test.go b/promql/parser/printer_test.go
index 4499fa7860..aee0d15137 100644
--- a/promql/parser/printer_test.go
+++ b/promql/parser/printer_test.go
@@ -23,8 +23,10 @@ import (
func TestExprString(t *testing.T) {
ExperimentalDurationExpr = true
+ EnableBinopFillModifiers = true
t.Cleanup(func() {
ExperimentalDurationExpr = false
+ EnableBinopFillModifiers = false
})
// A list of valid expressions that are expected to be
// returned as out when calling String(). If out is empty the output
@@ -113,6 +115,26 @@ func TestExprString(t *testing.T) {
in: `a - ignoring() group_left c`,
out: `a - ignoring () group_left () c`,
},
+ {
+ in: `a + fill(-23) b`,
+ out: `a + fill (-23) b`,
+ },
+ {
+ in: `a + fill_left(-23) b`,
+ out: `a + fill_left (-23) b`,
+ },
+ {
+ in: `a + fill_right(42) b`,
+ out: `a + fill_right (42) b`,
+ },
+ {
+ in: `a + fill_left(-23) fill_right(42) b`,
+ out: `a + fill_left (-23) fill_right (42) b`,
+ },
+ {
+ in: `a + on(b) group_left fill(-23) c`,
+ out: `a + on (b) group_left () fill (-23) c`,
+ },
{
in: `up > bool 0`,
},
diff --git a/promql/promqltest/test.go b/promql/promqltest/test.go
index 1c4226b461..7d48abb606 100644
--- a/promql/promqltest/test.go
+++ b/promql/promqltest/test.go
@@ -43,7 +43,6 @@ import (
"github.com/prometheus/prometheus/util/annotations"
"github.com/prometheus/prometheus/util/convertnhcb"
"github.com/prometheus/prometheus/util/teststorage"
- "github.com/prometheus/prometheus/util/testutil"
)
var (
@@ -72,7 +71,7 @@ var testStartTime = time.Unix(0, 0).UTC()
// LoadedStorage returns storage with generated data using the provided load statements.
// Non-load statements will cause test errors.
-func LoadedStorage(t testutil.T, input string) *teststorage.TestStorage {
+func LoadedStorage(t testing.TB, input string) *teststorage.TestStorage {
test, err := newTest(t, input, false, newTestStorage)
require.NoError(t, err)
@@ -113,21 +112,56 @@ func NewTestEngineWithOpts(tb testing.TB, opts promql.EngineOpts) *promql.Engine
return ng
}
+// GetBuiltInExprs returns all the eval statement expressions from the built-in test files.
+func GetBuiltInExprs() ([]string, error) {
+ files, err := fs.Glob(testsFs, "*/*.test")
+ if err != nil {
+ return nil, err
+ }
+
+ var exprs []string
+ for _, fn := range files {
+ content, err := fs.ReadFile(testsFs, fn)
+ if err != nil {
+ return nil, err
+ }
+
+ // Create a minimal test struct just for parsing
+ testInstance := &test{
+ cmds: []testCommand{},
+ }
+ if err := testInstance.parse(string(content)); err != nil {
+ return nil, err
+ }
+
+ // Extract expressions from eval commands
+ for _, cmd := range testInstance.cmds {
+ if evalCmd, ok := cmd.(*evalCmd); ok {
+ exprs = append(exprs, evalCmd.expr)
+ }
+ }
+ }
+
+ return exprs, nil
+}
+
// RunBuiltinTests runs an acceptance test suite against the provided engine.
func RunBuiltinTests(t TBRun, engine promql.QueryEngine) {
RunBuiltinTestsWithStorage(t, engine, newTestStorage)
}
// RunBuiltinTestsWithStorage runs an acceptance test suite against the provided engine and storage.
-func RunBuiltinTestsWithStorage(t TBRun, engine promql.QueryEngine, newStorage func(testutil.T) storage.Storage) {
+func RunBuiltinTestsWithStorage(t TBRun, engine promql.QueryEngine, newStorage func(testing.TB) storage.Storage) {
t.Cleanup(func() {
parser.EnableExperimentalFunctions = false
parser.ExperimentalDurationExpr = false
parser.EnableExtendedRangeSelectors = false
+ parser.EnableBinopFillModifiers = false
})
parser.EnableExperimentalFunctions = true
parser.ExperimentalDurationExpr = true
parser.EnableExtendedRangeSelectors = true
+ parser.EnableBinopFillModifiers = true
files, err := fs.Glob(testsFs, "*/*.test")
require.NoError(t, err)
@@ -142,22 +176,22 @@ func RunBuiltinTestsWithStorage(t TBRun, engine promql.QueryEngine, newStorage f
}
// RunTest parses and runs the test against the provided engine.
-func RunTest(t testutil.T, input string, engine promql.QueryEngine) {
+func RunTest(t testing.TB, input string, engine promql.QueryEngine) {
RunTestWithStorage(t, input, engine, newTestStorage)
}
// RunTestWithStorage parses and runs the test against the provided engine and storage.
-func RunTestWithStorage(t testutil.T, input string, engine promql.QueryEngine, newStorage func(testutil.T) storage.Storage) {
+func RunTestWithStorage(t testing.TB, input string, engine promql.QueryEngine, newStorage func(testing.TB) storage.Storage) {
require.NoError(t, runTest(t, input, engine, newStorage, false))
}
// testTest allows tests to be run in "test-the-test" mode (true for
// testingMode). This is a special mode for testing test code execution itself.
-func testTest(t testutil.T, input string, engine promql.QueryEngine) error {
+func testTest(t testing.TB, input string, engine promql.QueryEngine) error {
return runTest(t, input, engine, newTestStorage, true)
}
-func runTest(t testutil.T, input string, engine promql.QueryEngine, newStorage func(testutil.T) storage.Storage, testingMode bool) error {
+func runTest(t testing.TB, input string, engine promql.QueryEngine, newStorage func(testing.TB) storage.Storage, testingMode bool) error {
test, err := newTest(t, input, testingMode, newStorage)
// Why do this before checking err? newTest() can create the test storage and then return an error,
@@ -192,13 +226,14 @@ func runTest(t testutil.T, input string, engine promql.QueryEngine, newStorage f
// test is a sequence of read and write commands that are run
// against a test storage.
type test struct {
- testutil.T
+ testing.TB
+
// testingMode distinguishes between normal execution and test-execution mode.
testingMode bool
cmds []testCommand
- open func(testutil.T) storage.Storage
+ open func(testing.TB) storage.Storage
storage storage.Storage
context context.Context
@@ -206,9 +241,9 @@ type test struct {
}
// newTest returns an initialized empty Test.
-func newTest(t testutil.T, input string, testingMode bool, newStorage func(testutil.T) storage.Storage) (*test, error) {
+func newTest(t testing.TB, input string, testingMode bool, newStorage func(testing.TB) storage.Storage) (*test, error) {
test := &test{
- T: t,
+ TB: t,
cmds: []testCommand{},
testingMode: testingMode,
open: newStorage,
@@ -219,7 +254,7 @@ func newTest(t testutil.T, input string, testingMode bool, newStorage func(testu
return test, err
}
-func newTestStorage(t testutil.T) storage.Storage { return teststorage.New(t) }
+func newTestStorage(t testing.TB) storage.Storage { return teststorage.New(t) }
//go:embed testdata
var testsFs embed.FS
@@ -1012,7 +1047,12 @@ func (ev *evalCmd) compareResult(result parser.Value) error {
exp := ev.expected[hash]
var expectedFloats []promql.FPoint
- var expectedHistograms []promql.HPoint
+ // expectedHPoint wraps HPoint with CounterResetHintSet flag from SequenceValue.
+ type expectedHPoint struct {
+ promql.HPoint
+ CounterResetHintSet bool
+ }
+ var expectedHistograms []expectedHPoint
for i, e := range exp.vals {
ts := ev.start.Add(time.Duration(i) * ev.step)
@@ -1024,7 +1064,10 @@ func (ev *evalCmd) compareResult(result parser.Value) error {
t := ts.UnixNano() / int64(time.Millisecond/time.Nanosecond)
if e.Histogram != nil {
- expectedHistograms = append(expectedHistograms, promql.HPoint{T: t, H: e.Histogram})
+ expectedHistograms = append(expectedHistograms, expectedHPoint{
+ HPoint: promql.HPoint{T: t, H: e.Histogram},
+ CounterResetHintSet: e.CounterResetHintSet,
+ })
} else if !e.Omitted {
expectedFloats = append(expectedFloats, promql.FPoint{T: t, F: e.Value})
}
@@ -1053,7 +1096,7 @@ func (ev *evalCmd) compareResult(result parser.Value) error {
return fmt.Errorf("expected histogram value at index %v for %s to have timestamp %v, but it had timestamp %v (result has %s)", i, ev.metrics[hash], expected.T, actual.T, formatSeriesResult(s))
}
- if !compareNativeHistogram(expected.H.Compact(0), actual.H.Compact(0)) {
+ if !compareNativeHistogram(expected.H.Compact(0), actual.H.Compact(0), expected.CounterResetHintSet) {
return fmt.Errorf("expected histogram value at index %v (t=%v) for %s to be %v, but got %v (result has %s)", i, actual.T, ev.metrics[hash], expected.H.TestExpression(), actual.H.TestExpression(), formatSeriesResult(s))
}
}
@@ -1092,7 +1135,7 @@ func (ev *evalCmd) compareResult(result parser.Value) error {
if expH != nil && v.H == nil {
return fmt.Errorf("expected histogram %s for %s but got float value %v", HistogramTestExpression(expH), v.Metric, v.F)
}
- if expH != nil && !compareNativeHistogram(expH.Compact(0), v.H.Compact(0)) {
+ if expH != nil && !compareNativeHistogram(expH.Compact(0), v.H.Compact(0), exp0.CounterResetHintSet) {
return fmt.Errorf("expected %v for %s but got %s", HistogramTestExpression(expH), v.Metric, HistogramTestExpression(v.H))
}
if !almost.Equal(exp0.Value, v.F, defaultEpsilon) {
@@ -1130,7 +1173,9 @@ func (ev *evalCmd) compareResult(result parser.Value) error {
// compareNativeHistogram is helper function to compare two native histograms
// which can tolerate some differ in the field of float type, such as Count, Sum.
-func compareNativeHistogram(exp, cur *histogram.FloatHistogram) bool {
+// The counterResetHintSet parameter indicates whether the counter reset hint was
+// explicitly specified in the expected histogram (from the test file).
+func compareNativeHistogram(exp, cur *histogram.FloatHistogram, counterResetHintSet bool) bool {
if exp == nil || cur == nil {
return false
}
@@ -1166,6 +1211,15 @@ func compareNativeHistogram(exp, cur *histogram.FloatHistogram) bool {
return false
}
+ // Compare CounterResetHint only if explicitly specified in expected histogram.
+ // When counterResetHintSet is false, no hint was specified, meaning "don't care".
+ // When counterResetHintSet is true, the hint was explicitly specified and must match.
+ if counterResetHintSet {
+ if exp.CounterResetHint != cur.CounterResetHint {
+ return false
+ }
+ }
+
return true
}
@@ -1421,7 +1475,7 @@ func (t *test) execEval(cmd *evalCmd, engine promql.QueryEngine) error {
return do()
}
- if tt, ok := t.T.(*testing.T); ok {
+ if tt, ok := t.TB.(*testing.T); ok {
tt.Run(fmt.Sprintf("line %d/%s", cmd.line, cmd.expr), func(t *testing.T) {
require.NoError(t, do())
})
@@ -1589,12 +1643,12 @@ func assertMatrixSorted(m promql.Matrix) error {
func (t *test) clear() {
if t.storage != nil {
err := t.storage.Close()
- require.NoError(t.T, err, "Unexpected error while closing test storage.")
+ require.NoError(t.TB, err, "Unexpected error while closing test storage.")
}
if t.cancelCtx != nil {
t.cancelCtx()
}
- t.storage = t.open(t.T)
+ t.storage = t.open(t.TB)
t.context, t.cancelCtx = context.WithCancel(context.Background())
}
diff --git a/promql/promqltest/testdata/fill-modifier.test b/promql/promqltest/testdata/fill-modifier.test
new file mode 100644
index 0000000000..079a48cc99
--- /dev/null
+++ b/promql/promqltest/testdata/fill-modifier.test
@@ -0,0 +1,383 @@
+# ==================== fill / fill_left / fill_right modifier tests ====================
+
+# Test data for fill modifier tests: vectors with partial overlap.
+load 5m
+ left_vector{label="a"} 10
+ left_vector{label="b"} 20
+ left_vector{label="c"} 30
+ right_vector{label="a"} 100
+ right_vector{label="b"} 200
+ right_vector{label="d"} 400
+
+# ---------- Arithmetic operators with fill modifiers ----------
+
+# fill(0): Fill both sides with 0 for addition.
+eval instant at 0m left_vector + fill(0) right_vector
+ {label="a"} 110
+ {label="b"} 220
+ {label="c"} 30
+ {label="d"} 400
+
+# fill_left(0): Only fill left side with 0.
+eval instant at 0m left_vector + fill_left(0) right_vector
+ {label="a"} 110
+ {label="b"} 220
+ {label="d"} 400
+
+# fill_right(0): Only fill right side with 0.
+eval instant at 0m left_vector + fill_right(0) right_vector
+ {label="a"} 110
+ {label="b"} 220
+ {label="c"} 30
+
+# fill_left and fill_right with different values.
+eval instant at 0m left_vector + fill_left(5) fill_right(7) right_vector
+ {label="a"} 110
+ {label="b"} 220
+ {label="c"} 37
+ {label="d"} 405
+
+# fill with NaN.
+eval instant at 0m left_vector + fill(NaN) right_vector
+ {label="a"} 110
+ {label="b"} 220
+ {label="c"} NaN
+ {label="d"} NaN
+
+# fill with Inf.
+eval instant at 0m left_vector + fill(Inf) right_vector
+ {label="a"} 110
+ {label="b"} 220
+ {label="c"} +Inf
+ {label="d"} +Inf
+
+# fill with -Inf.
+eval instant at 0m left_vector + fill(-Inf) right_vector
+ {label="a"} 110
+ {label="b"} 220
+ {label="c"} -Inf
+ {label="d"} -Inf
+
+# ---------- Comparison operators with fill modifiers ----------
+
+# fill with equality comparison.
+eval instant at 0m left_vector == fill(30) right_vector
+ left_vector{label="c"} 30
+
+# fill with inequality comparison.
+eval instant at 0m left_vector != fill(30) right_vector
+ left_vector{label="a"} 10
+ left_vector{label="b"} 20
+ {label="d"} 30
+
+# fill with greater than.
+eval instant at 0m left_vector > fill(25) right_vector
+ left_vector{label="c"} 30
+
+# ---------- Comparison operators with bool modifier and fill ----------
+
+# fill with equality comparison and bool.
+eval instant at 0m left_vector == bool fill(30) right_vector
+ {label="a"} 0
+ {label="b"} 0
+ {label="c"} 1
+ {label="d"} 0
+
+# fill with inequality comparison and bool.
+eval instant at 0m left_vector != bool fill(30) right_vector
+ {label="a"} 1
+ {label="b"} 1
+ {label="c"} 0
+ {label="d"} 1
+
+# fill with greater than and bool.
+eval instant at 0m left_vector > bool fill(25) right_vector
+ {label="a"} 0
+ {label="b"} 0
+ {label="c"} 1
+ {label="d"} 0
+
+# ---------- fill with on() and ignoring() modifiers ----------
+
+clear
+
+load 5m
+ left_vector{job="foo", instance="a"} 10
+ left_vector{job="foo", instance="b"} 20
+ left_vector{job="bar", instance="a"} 30
+ right_vector{job="foo", instance="a"} 100
+ right_vector{job="foo", instance="c"} 300
+
+# fill with on().
+eval instant at 0m left_vector + on(job, instance) fill(0) right_vector
+ {job="foo", instance="a"} 110
+ {job="foo", instance="b"} 20
+ {job="bar", instance="a"} 30
+ {job="foo", instance="c"} 300
+
+# fill_right with on().
+eval instant at 0m left_vector + on(job, instance) fill_right(0) right_vector
+ {job="foo", instance="a"} 110
+ {job="foo", instance="b"} 20
+ {job="bar", instance="a"} 30
+
+# fill_left with on().
+eval instant at 0m left_vector + on(job, instance) fill_left(0) right_vector
+ {job="foo", instance="a"} 110
+ {job="foo", instance="c"} 300
+
+# fill with ignoring() - requires group_left since ignoring(job) creates many-to-one matching
+# when two left_vector series have same instance but different jobs.
+eval instant at 0m left_vector + ignoring(job) group_left fill(0) right_vector
+ {instance="a", job="foo"} 110
+ {instance="a", job="bar"} 130
+ {instance="b", job="foo"} 20
+ {instance="c"} 300
+
+# ---------- fill with group_left / group_right (many-to-one / one-to-many) ----------
+
+clear
+
+load 5m
+ requests{method="GET", status="200"} 100
+ requests{method="POST", status="200"} 200
+ requests{method="GET", status="500"} 10
+ requests{method="POST", status="500"} 20
+ limits{status="200"} 1000
+ limits{status="404"} 500
+ limits{status="500"} 50
+
+# group_left with fill_right: fill missing "one" side series.
+eval instant at 0m requests / on(status) group_left fill_right(1) limits
+ {method="GET", status="200"} 0.1
+ {method="POST", status="200"} 0.2
+ {method="GET", status="500"} 0.2
+ {method="POST", status="500"} 0.4
+
+# group_left with fill_left: fill missing "many" side series.
+# For status="404", there's no matching requests, so a single series with the match group's labels is filled
+eval instant at 0m requests + on(status) group_left fill_left(0) limits
+ {method="GET", status="200"} 1100
+ {method="POST", status="200"} 1200
+ {method="GET", status="500"} 60
+ {method="POST", status="500"} 70
+ {status="404"} 500
+
+# group_left with fill on both sides.
+eval instant at 0m requests + on(status) group_left fill(0) limits
+ {method="GET", status="200"} 1100
+ {method="POST", status="200"} 1200
+ {method="GET", status="500"} 60
+ {method="POST", status="500"} 70
+ {status="404"} 500
+
+# group_right with fill_left: fill missing "one" side series.
+clear
+
+load 5m
+ cpu_info{instance="a", cpu="0"} 1
+ cpu_info{instance="a", cpu="1"} 1
+ cpu_info{instance="b", cpu="0"} 1
+ node_meta{instance="a"} 100
+ node_meta{instance="c"} 300
+
+# fill_left fills the "one" side (node_meta) when missing for a "many" side series.
+eval instant at 0m node_meta * on(instance) group_right fill_left(1) cpu_info
+ {instance="a", cpu="0"} 100
+ {instance="a", cpu="1"} 100
+ {instance="c"} 300
+
+# group_right with fill_right: fill missing "many" side series.
+eval instant at 0m node_meta * on(instance) group_right fill_right(0) cpu_info
+ {instance="a", cpu="0"} 100
+ {instance="a", cpu="1"} 100
+ {instance="b", cpu="0"} 0
+
+# group_right with fill on both sides.
+eval instant at 0m node_meta * on(instance) group_right fill(1) cpu_info
+ {instance="a", cpu="0"} 100
+ {instance="a", cpu="1"} 100
+ {instance="b", cpu="0"} 1
+ {instance="c"} 300
+
+# ---------- fill with group_left/group_right and extra labels ----------
+
+clear
+
+load 5m
+ requests{method="GET", status="200"} 100
+ requests{method="POST", status="200"} 200
+ limits{status="200", owner="team-a"} 1000
+ limits{status="500", owner="team-b"} 50
+
+# group_left with extra label and fill_right.
+# Note: when filling the "one" side, the joined label cannot be filled.
+eval instant at 0m requests + on(status) group_left(owner) fill_right(0) limits
+ {method="GET", status="200", owner="team-a"} 1100
+ {method="POST", status="200", owner="team-a"} 1200
+
+# ---------- Edge cases ----------
+
+clear
+
+load 5m
+ only_left{label="a"} 10
+ only_left{label="b"} 20
+ only_right{label="c"} 30
+ only_right{label="d"} 40
+
+# No overlap at all - fill creates all results.
+eval instant at 0m only_left + fill(0) only_right
+ {label="a"} 10
+ {label="b"} 20
+ {label="c"} 30
+ {label="d"} 40
+
+# No overlap - fill_left only creates right side results.
+eval instant at 0m only_left + fill_left(0) only_right
+ {label="c"} 30
+ {label="d"} 40
+
+# No overlap - fill_right only creates left side results.
+eval instant at 0m only_left + fill_right(0) only_right
+ {label="a"} 10
+ {label="b"} 20
+
+# Complete overlap - fill has no effect.
+clear
+
+load 5m
+ complete_left{label="a"} 10
+ complete_left{label="b"} 20
+ complete_right{label="a"} 100
+ complete_right{label="b"} 200
+
+eval instant at 0m complete_left + fill(99) complete_right
+ {label="a"} 110
+ {label="b"} 220
+
+# ---------- fill with range queries ----------
+
+clear
+
+load 5m
+ range_left{label="a"} 1 2 3 4 5
+ range_left{label="b"} 10 20 30 40 50
+ range_right{label="a"} 100 200 300 400 500
+ range_right{label="c"} 1000 2000 3000 4000 5000
+
+eval range from 0 to 20m step 5m range_left + fill(0) range_right
+ {label="a"} 101 202 303 404 505
+ {label="b"} 10 20 30 40 50
+ {label="c"} 1000 2000 3000 4000 5000
+
+eval range from 0 to 20m step 5m range_left + fill_right(0) range_right
+ {label="a"} 101 202 303 404 505
+ {label="b"} 10 20 30 40 50
+
+eval range from 0 to 20m step 5m range_left + fill_left(0) range_right
+ {label="a"} 101 202 303 404 505
+ {label="c"} 1000 2000 3000 4000 5000
+
+# Range queries with intermittently present series.
+clear
+
+load 5m
+ intermittent_left{label="a"} 1 _ 3 _ 5
+ intermittent_left{label="b"} _ 20 _ 40 _
+ intermittent_right{label="a"} _ 200 _ 400 _
+ intermittent_right{label="b"} 100 _ 300 _ 500
+ intermittent_right{label="c"} 1000 _ _ 4000 5000
+
+# When both sides have the same label but are present at different times,
+# fill creates results at all timestamps where at least one side is present.
+eval range from 0 to 20m step 5m intermittent_left + fill(0) intermittent_right
+ {label="a"} 1 200 3 400 5
+ {label="b"} 100 20 300 40 500
+ {label="c"} 1000 _ _ 4000 5000
+
+# fill_right only fills the right side when it's missing.
+# Output only exists when left side is present (right side filled with 0 if missing).
+eval range from 0 to 20m step 5m intermittent_left + fill_right(0) intermittent_right
+ {label="a"} 1 _ 3 _ 5
+ {label="b"} _ 20 _ 40 _
+
+# fill_left only fills the left side when it's missing.
+# Output only exists when right side is present (left side filled with 0 if missing).
+eval range from 0 to 20m step 5m intermittent_left + fill_left(0) intermittent_right
+ {label="a"} _ 200 _ 400 _
+ {label="b"} 100 _ 300 _ 500
+ {label="c"} 1000 _ _ 4000 5000
+
+# ---------- fill with vectors where one side is empty ----------
+
+clear
+
+load 5m
+ non_empty{label="a"} 10
+ non_empty{label="b"} 20
+
+# Empty right side - fill_right has no effect (nothing to add).
+eval instant at 0m non_empty + fill_right(0) nonexistent
+ {label="a"} 10
+ {label="b"} 20
+
+# Empty right side - fill_left creates nothing (no right side labels to use).
+eval instant at 0m non_empty + fill_left(0) nonexistent
+
+# Empty left side - fill_left has no effect.
+eval instant at 0m nonexistent + fill_left(0) non_empty
+ {label="a"} 10
+ {label="b"} 20
+
+# Empty left side - fill_right creates nothing.
+eval instant at 0m nonexistent + fill_right(0) non_empty
+
+# fill both sides with one side empty.
+eval instant at 0m non_empty + fill(0) nonexistent
+ {label="a"} 10
+ {label="b"} 20
+
+eval instant at 0m nonexistent + fill(0) non_empty
+ {label="a"} 10
+ {label="b"} 20
+
+# ---------- Metric names that match fill modifier keywords ----------
+
+clear
+
+load 5m
+ fill{label="a"} 1
+ fill{label="b"} 2
+ fill_left{label="a"} 10
+ fill_left{label="c"} 30
+ fill_right{label="b"} 200
+ fill_right{label="d"} 400
+ other{label="a"} 1000
+ other{label="e"} 5000
+
+# Metric named "fill" on the left side.
+eval instant at 0m fill + fill(0) other
+ {label="a"} 1001
+ {label="b"} 2
+ {label="e"} 5000
+
+# Metric named "fill" on the right side without modifier.
+eval instant at 0m other + fill
+ {label="a"} 1001
+
+# Metric named "fill" on the right side with fill() modifier.
+eval instant at 0m other + fill(0) fill
+ {label="a"} 1001
+ {label="b"} 2
+ {label="e"} 5000
+
+# Metric named "fill_left" on the right side with fill_left() modifier.
+eval instant at 0m other + fill_left(0) fill_left
+ {label="a"} 1010
+ {label="c"} 30
+
+# Metric named "fill_right" on the right side with fill_right() modifier.
+eval instant at 0m other + fill_right(0) fill_right
+ {label="a"} 1000
+ {label="e"} 5000
diff --git a/promql/promqltest/testdata/info.test b/promql/promqltest/testdata/info.test
index 891e0eaa53..9bc4ed0fbc 100644
--- a/promql/promqltest/testdata/info.test
+++ b/promql/promqltest/testdata/info.test
@@ -34,6 +34,22 @@ eval range from 0m to 10m step 5m info(metric, {data=~".+", non_existent=~".*"})
eval range from 0m to 10m step 5m info(metric_with_overlapping_label)
metric_with_overlapping_label{data="base", instance="a", job="1", label="value", another_data="another info"} 0 1 2
+# Filtering by a label that exists on both base metric and target_info should work.
+# This is a regression test for https://github.com/prometheus/prometheus/issues/17813.
+# Note: data="base" on base metric, data="info" on target_info - the filter matches target_info.
+eval range from 0m to 10m step 5m info(metric_with_overlapping_label, {data="info"})
+ metric_with_overlapping_label{data="base", instance="a", job="1", label="value"} 0 1 2
+
+# Filtering by a label that exists on both base metric and target_info with regex should work.
+eval range from 0m to 10m step 5m info(metric_with_overlapping_label, {data=~".+"})
+ metric_with_overlapping_label{data="base", instance="a", job="1", label="value"} 0 1 2
+
+# Filtering by a label that exists on both base metric and target_info with same value.
+# The selector matches the target_info, and the join succeeds via identifying labels.
+# Note: Only the instance label is considered for inclusion, but it already exists on base.
+eval range from 0m to 10m step 5m info(metric_with_overlapping_label, {instance="a"})
+ metric_with_overlapping_label{data="base", instance="a", job="1", label="value"} 0 1 2
+
# Include data labels from target_info specifically.
eval range from 0m to 10m step 5m info(metric, {__name__="target_info"})
metric{data="info", instance="a", job="1", label="value", another_data="another info"} 0 1 2
@@ -54,7 +70,11 @@ eval range from 0m to 10m step 5m info(metric, {__name__=~".+_info"})
metric{instance="a", job="1", label="value", build_data="build", data="info", another_data="another info"} 0 1 2
# Info metrics themselves are ignored when it comes to enriching with info metric data labels.
-eval range from 0m to 10m step 5m info(build_info, {__name__=~".+_info", build_data=~".+"})
+eval range from 0m to 10m step 5m info(build_info, {__name__=~".+_info", another_data=~".+"})
+ build_info{instance="a", job="1", build_data="build"} 1 1 1
+
+# Info metrics themselves are ignored when it comes to enriching with info metric data labels.
+eval range from 0m to 10m step 5m info(build_info, {__name__=~".+_info"})
build_info{instance="a", job="1", build_data="build"} 1 1 1
clear
@@ -150,3 +170,35 @@ eval range from 0 to 2m step 1m info({job="work"}, {__name__="info_metric"})
data_metric{instance="a", job="work", state="running", label="new"} _ _ 30
info_metric{instance="b", job="work", state="stopped"} 1 1 1
info_metric{instance="a", job="work", state="running"} 1 1 1
+
+clear
+
+load 1m
+ data_metric{} 1 2 3
+
+eval range from 0 to 2m step 1m info(data_metric, {__name__="info_metric"})
+ data_metric{} 1 2 3
+
+clear
+
+load 1m
+ data_metric{} 1 2 3
+ data_metric{instance="a"} 4 5 6
+
+eval range from 0 to 2m step 1m info(data_metric, {__name__="info_metric"})
+ data_metric{} 1 2 3
+ data_metric{instance="a"} 4 5 6
+
+clear
+
+load 1m
+ data_metric{} 1 2 3
+ data_metric{instance="a"} 4 5 6
+ data_metric{job="1"} 7 8 9
+ data_metric{instance="a", job="1"} 10 20 30
+
+eval range from 0 to 2m step 1m info(data_metric, {__name__="info_metric"})
+ data_metric{} 1 2 3
+ data_metric{instance="a"} 4 5 6
+ data_metric{job="1"} 7 8 9
+ data_metric{instance="a", job="1"} 10 20 30
diff --git a/promql/promqltest/testdata/native_histograms.test b/promql/promqltest/testdata/native_histograms.test
index fd4b1f4178..d66400f787 100644
--- a/promql/promqltest/testdata/native_histograms.test
+++ b/promql/promqltest/testdata/native_histograms.test
@@ -1283,7 +1283,7 @@ eval instant at 12m sum_over_time(nhcb_metric[13m])
eval instant at 12m avg_over_time(nhcb_metric[13m])
expect no_warn
expect info msg: PromQL info: mismatched custom buckets were reconciled during aggregation
- {} {{schema:-53 count:1 sum:1 custom_values:[5] counter_reset_hint:gauge buckets:[1]}}
+ {} {{schema:-53 count:1 sum:1 custom_values:[5] buckets:[1]}}
eval instant at 12m last_over_time(nhcb_metric[13m])
expect no_warn
diff --git a/promql/value.go b/promql/value.go
index 02cb021024..17afdfc410 100644
--- a/promql/value.go
+++ b/promql/value.go
@@ -487,6 +487,11 @@ func (ssi *storageSeriesIterator) AtT() int64 {
return ssi.currT
}
+// TODO(krajorama): implement AtST.
+func (*storageSeriesIterator) AtST() int64 {
+ return 0
+}
+
func (ssi *storageSeriesIterator) Next() chunkenc.ValueType {
if ssi.currH != nil {
ssi.iHistograms++
diff --git a/rules/alerting_test.go b/rules/alerting_test.go
index a2c7abcd56..caf32e6472 100644
--- a/rules/alerting_test.go
+++ b/rules/alerting_test.go
@@ -697,12 +697,14 @@ func TestQueryForStateSeries(t *testing.T) {
{
selectMockFunction: func(bool, *storage.SelectHints, ...*labels.Matcher) storage.SeriesSet {
return storage.TestSeriesSet(storage.MockSeries(
+ nil,
[]int64{1, 2, 3},
[]float64{1, 2, 3},
[]string{"__name__", "ALERTS_FOR_STATE", "alertname", "TestRule", "severity", "critical"},
))
},
expectedSeries: storage.MockSeries(
+ nil,
[]int64{1, 2, 3},
[]float64{1, 2, 3},
[]string{"__name__", "ALERTS_FOR_STATE", "alertname", "TestRule", "severity", "critical"},
diff --git a/rules/manager_test.go b/rules/manager_test.go
index 0991e8198a..a716304b7a 100644
--- a/rules/manager_test.go
+++ b/rules/manager_test.go
@@ -45,6 +45,7 @@ import (
"github.com/prometheus/prometheus/promql/parser"
"github.com/prometheus/prometheus/promql/promqltest"
"github.com/prometheus/prometheus/storage"
+ "github.com/prometheus/prometheus/tsdb"
"github.com/prometheus/prometheus/tsdb/chunkenc"
"github.com/prometheus/prometheus/tsdb/tsdbutil"
"github.com/prometheus/prometheus/util/teststorage"
@@ -1201,7 +1202,9 @@ func TestRuleMovedBetweenGroups(t *testing.T) {
t.Skip("skipping test in short mode.")
}
- storage := teststorage.New(t, 600000)
+ storage := teststorage.New(t, func(opt *tsdb.Options) {
+ opt.OutOfOrderTimeWindow = 600000
+ })
defer storage.Close()
opts := promql.EngineOpts{
Logger: nil,
diff --git a/rules/recording_test.go b/rules/recording_test.go
index 1fee5ede72..29208b6392 100644
--- a/rules/recording_test.go
+++ b/rules/recording_test.go
@@ -111,7 +111,7 @@ var ruleEvalTestScenarios = []struct {
},
}
-func setUpRuleEvalTest(t require.TestingT) *teststorage.TestStorage {
+func setUpRuleEvalTest(t testing.TB) *teststorage.TestStorage {
return promqltest.LoadedStorage(t, `
load 1m
metric{label_a="1",label_b="3"} 1
diff --git a/scrape/helpers_test.go b/scrape/helpers_test.go
index dd5179b360..1db229561d 100644
--- a/scrape/helpers_test.go
+++ b/scrape/helpers_test.go
@@ -17,6 +17,7 @@ import (
"bytes"
"context"
"encoding/binary"
+ "fmt"
"net/http"
"testing"
"time"
@@ -38,15 +39,22 @@ import (
// For readability.
type sample = teststorage.Sample
+type compatAppendable interface {
+ storage.Appendable
+ storage.AppendableV2
+}
+
func withCtx(ctx context.Context) func(sl *scrapeLoop) {
return func(sl *scrapeLoop) {
sl.ctx = ctx
}
}
-func withAppendable(appendable storage.Appendable) func(sl *scrapeLoop) {
+func withAppendable(app compatAppendable, appV2 bool) func(sl *scrapeLoop) {
return func(sl *scrapeLoop) {
- sl.appendable = appendable
+ sa := selectAppendable(app, appV2)
+ sl.appendable = sa.V1()
+ sl.appendableV2 = sa.V2()
}
}
@@ -55,8 +63,7 @@ func withAppendable(appendable storage.Appendable) func(sl *scrapeLoop) {
//
// It's recommended to use withXYZ functions for simple option customizations, e.g:
//
-// appTest := teststorage.NewAppendable()
-// sl, _ := newTestScrapeLoop(t, withAppendable(appTest))
+// sl, _ := newTestScrapeLoop(t, withCtx(customCtx))
//
// However, when changing more than one scrapeLoop options it's more readable to have one explicit opt function:
//
@@ -64,7 +71,7 @@ func withAppendable(appendable storage.Appendable) func(sl *scrapeLoop) {
// appTest := teststorage.NewAppendable()
// sl, scraper := newTestScrapeLoop(t, func(sl *scrapeLoop) {
// sl.ctx = ctx
-// sl.appendable = appTest
+// sl.appendableV2 = appTest
// // Since we're writing samples directly below we need to provide a protocol fallback.
// sl.fallbackScrapeProtocol = "text/plain"
// })
@@ -84,8 +91,6 @@ func newTestScrapeLoop(t testing.TB, opts ...func(sl *scrapeLoop)) (_ *scrapeLoo
timeout: 1 * time.Hour,
sampleMutator: nopMutator,
reportSampleMutator: nopMutator,
-
- appendable: teststorage.NewAppendable(),
buffers: pool.New(1e3, 1e6, 3, func(sz int) any { return make([]byte, 0, sz) }),
metrics: metrics,
maxSchema: histogram.ExponentialSchemaMax,
@@ -98,6 +103,11 @@ func newTestScrapeLoop(t testing.TB, opts ...func(sl *scrapeLoop)) (_ *scrapeLoo
for _, o := range opts {
o(sl)
}
+
+ if sl.appendable != nil && sl.appendableV2 != nil {
+ t.Fatal("select the appendable to use, both were passed, likely a bug")
+ }
+
// Validate user opts for convenience.
require.Nil(t, sl.parentCtx, "newTestScrapeLoop does not support injecting non-nil parent context")
require.Nil(t, sl.appenderCtx, "newTestScrapeLoop does not support injecting non-nil appender context")
@@ -121,7 +131,8 @@ func newTestScrapeLoop(t testing.TB, opts ...func(sl *scrapeLoop)) (_ *scrapeLoo
return sl, scraper
}
-func newTestScrapePool(t *testing.T, injectNewLoop func(options scrapeLoopOptions) loop) *scrapePool {
+func newTestScrapePool(t *testing.T, app compatAppendable, appV2 bool, injectNewLoop func(options scrapeLoopOptions) loop) *scrapePool {
+ sa := selectAppendable(app, appV2)
return &scrapePool{
ctx: t.Context(),
cancel: func() {},
@@ -134,7 +145,8 @@ func newTestScrapePool(t *testing.T, injectNewLoop func(options scrapeLoopOption
loops: map[uint64]loop{},
injectTestNewLoop: injectNewLoop,
- appendable: teststorage.NewAppendable(),
+ appendable: sa.V1(), appendableV2: sa.V2(),
+
symbolTable: labels.NewSymbolTable(),
metrics: newTestScrapeMetrics(t),
}
@@ -158,3 +170,66 @@ func protoMarshalDelimited(t *testing.T, mf *dto.MetricFamily) []byte {
buf.Write(protoBuf)
return buf.Bytes()
}
+
+type selectedAppendable struct {
+ useV2 bool
+ app compatAppendable
+}
+
+// V1 returns Appendable if V1 is selected, otherwise nil.
+func (s selectedAppendable) V1() storage.Appendable {
+ if s.useV2 {
+ return nil
+ }
+ return s.app
+}
+
+// V2 returns AppendableV2 if V2 is selected, otherwise nil.
+func (s selectedAppendable) V2() storage.AppendableV2 {
+ if !s.useV2 {
+ return nil
+ }
+ return s.app
+}
+
+// selectAppendable allows to specify which appendable callers should use when the struct
+// implements both. This is how all callers are making the decision - if one appendable is nil, they
+// take another. selectAppendable allows to inject nil to e.g. storage.AppendableV2 when appV2 is false.
+func selectAppendable(app compatAppendable, appV2 bool) selectedAppendable {
+ s := selectedAppendable{
+ app: app,
+ useV2: appV2,
+ }
+ return s
+}
+
+func foreachAppendable(t *testing.T, f func(t *testing.T, appV2 bool)) {
+ for _, appV2 := range []bool{false, true} {
+ t.Run(fmt.Sprintf("appV2=%v", appV2), func(t *testing.T) {
+ f(t, appV2)
+ })
+ }
+}
+
+func TestSelectAppendable(t *testing.T) {
+ var i int
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ defer func() { i++ }()
+ switch i {
+ case 0:
+ require.False(t, appV2)
+
+ s := selectAppendable(teststorage.NewAppendable(), appV2)
+ require.NotNil(t, s.V1())
+ require.Nil(t, s.V2())
+ case 1:
+ require.True(t, appV2)
+
+ s := selectAppendable(teststorage.NewAppendable(), appV2)
+ require.Nil(t, s.V1())
+ require.NotNil(t, s.V2())
+ default:
+ t.Fatal("too many iterations")
+ }
+ })
+}
diff --git a/scrape/manager.go b/scrape/manager.go
index a2297aa824..ef226ad507 100644
--- a/scrape/manager.go
+++ b/scrape/manager.go
@@ -114,7 +114,8 @@ type Manager struct {
opts *Options
logger *slog.Logger
- appendable storage.Appendable
+ appendable storage.Appendable
+ appendableV2 storage.AppendableV2
graceShut chan struct{}
@@ -196,7 +197,7 @@ func (m *Manager) reload() {
continue
}
m.metrics.targetScrapePools.Inc()
- sp, err := newScrapePool(scrapeConfig, m.appendable, m.offsetSeed, m.logger.With("scrape_pool", setName), m.buffers, m.opts, m.metrics)
+ sp, err := newScrapePool(scrapeConfig, m.appendable, m.appendableV2, m.offsetSeed, m.logger.With("scrape_pool", setName), m.buffers, m.opts, m.metrics)
if err != nil {
m.metrics.targetScrapePoolsFailed.Inc()
m.logger.Error("error creating new scrape pool", "err", err, "scrape_pool", setName)
diff --git a/scrape/manager_test.go b/scrape/manager_test.go
index d4898eb996..8b289cb7e2 100644
--- a/scrape/manager_test.go
+++ b/scrape/manager_test.go
@@ -528,7 +528,7 @@ scrape_configs:
ch <- struct{}{}
return noopLoop()
}
- sp := newTestScrapePool(t, newLoop)
+ sp := newTestScrapePool(t, nil, false, newLoop)
sp.activeTargets[1] = &Target{}
sp.loops[1] = noopLoop()
sp.config = cfg1.ScrapeConfigs[0]
@@ -684,7 +684,7 @@ scrape_configs:
_, cancel := context.WithCancel(context.Background())
defer cancel()
- sp := newTestScrapePool(t, newLoop)
+ sp := newTestScrapePool(t, nil, false, newLoop)
sp.loops[1] = noopLoop()
sp.config = cfg1.ScrapeConfigs[0]
sp.metrics = scrapeManager.metrics
diff --git a/scrape/metrics.go b/scrape/metrics.go
index 4662a9fd9e..34f1e28dba 100644
--- a/scrape/metrics.go
+++ b/scrape/metrics.go
@@ -56,6 +56,7 @@ type scrapeMetrics struct {
targetScrapeExemplarOutOfOrder prometheus.Counter
targetScrapePoolExceededLabelLimits prometheus.Counter
targetScrapeNativeHistogramBucketLimit prometheus.Counter
+ targetScrapeDuration prometheus.Histogram
}
func newScrapeMetrics(reg prometheus.Registerer) (*scrapeMetrics, error) {
@@ -252,6 +253,15 @@ func newScrapeMetrics(reg prometheus.Registerer) (*scrapeMetrics, error) {
Help: "Total number of exemplar rejected due to not being out of the expected order.",
},
)
+ sm.targetScrapeDuration = prometheus.NewHistogram(
+ prometheus.HistogramOpts{
+ Name: "prometheus_target_scrape_duration_seconds",
+ Help: "Total duration of the scrape from start to commit completion in seconds.",
+ NativeHistogramBucketFactor: 1.1,
+ NativeHistogramMaxBucketNumber: 100,
+ NativeHistogramMinResetDuration: 1 * time.Hour,
+ },
+ )
for _, collector := range []prometheus.Collector{
// Used by Manager.
@@ -284,6 +294,7 @@ func newScrapeMetrics(reg prometheus.Registerer) (*scrapeMetrics, error) {
sm.targetScrapeExemplarOutOfOrder,
sm.targetScrapePoolExceededLabelLimits,
sm.targetScrapeNativeHistogramBucketLimit,
+ sm.targetScrapeDuration,
} {
err := reg.Register(collector)
if err != nil {
@@ -324,6 +335,7 @@ func (sm *scrapeMetrics) Unregister() {
sm.reg.Unregister(sm.targetScrapeExemplarOutOfOrder)
sm.reg.Unregister(sm.targetScrapePoolExceededLabelLimits)
sm.reg.Unregister(sm.targetScrapeNativeHistogramBucketLimit)
+ sm.reg.Unregister(sm.targetScrapeDuration)
}
type TargetsGatherer interface {
diff --git a/scrape/scrape.go b/scrape/scrape.go
index 1a99155d09..d5a9ba72b4 100644
--- a/scrape/scrape.go
+++ b/scrape/scrape.go
@@ -82,11 +82,12 @@ type FailureLogger interface {
// scrapePool manages scrapes for sets of targets.
type scrapePool struct {
- appendable storage.Appendable
- logger *slog.Logger
- ctx context.Context
- cancel context.CancelFunc
- options *Options
+ appendable storage.Appendable
+ appendableV2 storage.AppendableV2
+ logger *slog.Logger
+ ctx context.Context
+ cancel context.CancelFunc
+ options *Options
// mtx must not be taken after targetMtx.
mtx sync.Mutex
@@ -139,6 +140,7 @@ type scrapeLoopAppendAdapter interface {
func newScrapePool(
cfg *config.ScrapeConfig,
appendable storage.Appendable,
+ appendableV2 storage.AppendableV2,
offsetSeed uint64,
logger *slog.Logger,
buffers *pool.Pool,
@@ -171,6 +173,7 @@ func newScrapePool(
ctx, cancel := context.WithCancel(context.Background())
sp := &scrapePool{
appendable: appendable,
+ appendableV2: appendableV2,
logger: logger,
ctx: ctx,
cancel: cancel,
@@ -842,11 +845,12 @@ type scrapeLoop struct {
scraper scraper
// Static params per scrapePool.
- appendable storage.Appendable
- buffers *pool.Pool
- offsetSeed uint64
- symbolTable *labels.SymbolTable
- metrics *scrapeMetrics
+ appendable storage.Appendable
+ appendableV2 storage.AppendableV2
+ buffers *pool.Pool
+ offsetSeed uint64
+ symbolTable *labels.SymbolTable
+ metrics *scrapeMetrics
// Options from config.ScrapeConfig.
sampleLimit int
@@ -1190,11 +1194,12 @@ func newScrapeLoop(opts scrapeLoopOptions) *scrapeLoop {
scraper: opts.scraper,
// Static params per scrapePool.
- appendable: opts.sp.appendable,
- buffers: opts.sp.buffers,
- offsetSeed: opts.sp.offsetSeed,
- symbolTable: opts.sp.symbolTable,
- metrics: opts.sp.metrics,
+ appendable: opts.sp.appendable,
+ appendableV2: opts.sp.appendableV2,
+ buffers: opts.sp.buffers,
+ offsetSeed: opts.sp.offsetSeed,
+ symbolTable: opts.sp.symbolTable,
+ metrics: opts.sp.metrics,
// config.ScrapeConfig.
sampleLimit: int(opts.sp.config.SampleLimit),
@@ -1303,7 +1308,9 @@ mainLoop:
}
func (sl *scrapeLoop) appender() scrapeLoopAppendAdapter {
- // NOTE(bwplotka): Add AppenderV2 implementation, see https://github.com/prometheus/prometheus/issues/17632.
+ if sl.appendableV2 != nil {
+ return &scrapeLoopAppenderV2{scrapeLoop: sl, AppenderV2: sl.appendableV2.AppenderV2(sl.appenderCtx)}
+ }
return &scrapeLoopAppender{scrapeLoop: sl, Appender: sl.appendable.Appender(sl.appenderCtx)}
}
@@ -1335,6 +1342,11 @@ func (sl *scrapeLoop) scrapeAndReport(last, appendTime time.Time, errc chan<- er
return
}
err = app.Commit()
+ if sl.reportExtraMetrics {
+ totalDuration := time.Since(start)
+ // Record total scrape duration metric.
+ sl.metrics.targetScrapeDuration.Observe(totalDuration.Seconds())
+ }
if err != nil {
sl.l.Error("Scrape commit failed", "err", err)
}
@@ -1632,7 +1644,7 @@ loop:
break
}
switch et {
- // TODO(bwplotka): Consider changing parser to give metadata at once instead of type, help and unit in separation, ideally on `Series()/Histogram()
+ // TODO(bwplotka): Consider changing parser to give metadata at once instead of type, help and unit in separation, ideally on `Series()/Histogram()`
// otherwise we can expose metadata without series on metadata API.
case textparse.EntryType:
// TODO(bwplotka): Build meta entry directly instead of locking and updating the map. This will
@@ -1748,7 +1760,7 @@ loop:
}
}
- sampleAdded, err = sl.checkAddError(met, err, &sampleLimitErr, &bucketLimitErr, &appErrs)
+ sampleAdded, err = sl.checkAddError(met, nil, err, &sampleLimitErr, &bucketLimitErr, &appErrs)
if err != nil {
if !errors.Is(err, storage.ErrNotFound) {
sl.l.Debug("Unexpected error", "series", string(met), "err", err)
@@ -1824,7 +1836,7 @@ loop:
if !seriesCached || lastMeta.lastIterChange == sl.cache.iter {
// In majority cases we can trust that the current series/histogram is matching the lastMeta and lastMFName.
// However, optional TYPE etc metadata and broken OM text can break this, detect those cases here.
- // TODO(bwplotka): Consider moving this to parser as many parser users end up doing this (e.g. ST and NHCB parsing).
+ // TODO(https://github.com/prometheus/prometheus/issues/17900): Move this to text and OM parser.
if isSeriesPartOfFamily(lset.Get(model.MetricNameLabel), lastMFName, lastMeta.Type) {
if _, merr := app.UpdateMetadata(ref, lset, lastMeta.Metadata); merr != nil {
// No need to fail the scrape on errors appending metadata.
@@ -1866,6 +1878,7 @@ loop:
return total, added, seriesAdded, err
}
+// TODO(https://github.com/prometheus/prometheus/issues/17900): Move this to text and OM parser.
func isSeriesPartOfFamily(mName string, mfName []byte, typ model.MetricType) bool {
mfNameStr := yoloString(mfName)
if !strings.HasPrefix(mName, mfNameStr) { // Fast path.
@@ -1937,7 +1950,7 @@ func isSeriesPartOfFamily(mName string, mfName []byte, typ model.MetricType) boo
// during normal operation (e.g., accidental cardinality explosion, sudden traffic spikes).
// Current case ordering prevents exercising other cases when limits are exceeded.
// Remaining error cases typically occur only a few times, often during initial setup.
-func (sl *scrapeLoop) checkAddError(met []byte, err error, sampleLimitErr, bucketLimitErr *error, appErrs *appendErrors) (sampleAdded bool, _ error) {
+func (sl *scrapeLoop) checkAddError(met []byte, exemplars []exemplar.Exemplar, err error, sampleLimitErr, bucketLimitErr *error, appErrs *appendErrors) (sampleAdded bool, _ error) {
switch {
case err == nil:
return true, nil
@@ -1969,6 +1982,26 @@ func (sl *scrapeLoop) checkAddError(met []byte, err error, sampleLimitErr, bucke
case errors.Is(err, storage.ErrNotFound):
return false, storage.ErrNotFound
default:
+ // If nothing from the above, check for partial errors. Do this here to not alloc the pErr on a hot path.
+ var pErr *storage.AppendPartialError
+ if errors.As(err, &pErr) {
+ outOfOrderExemplars := 0
+ for _, e := range pErr.ExemplarErrors {
+ if errors.Is(e, storage.ErrOutOfOrderExemplar) {
+ outOfOrderExemplars++
+ }
+ // Since exemplar storage is still experimental, we don't fail or check other errors.
+ // Debug log is emitted in TSDB already.
+ }
+ if outOfOrderExemplars > 0 && outOfOrderExemplars == len(exemplars) {
+ // Only report out of order exemplars if all are out of order, otherwise this was a partial update
+ // to some existing set of exemplars.
+ appErrs.numExemplarOutOfOrder += outOfOrderExemplars
+ sl.l.Debug("Out of order exemplars", "count", outOfOrderExemplars, "latest", fmt.Sprintf("%+v", exemplars[len(exemplars)-1]))
+ sl.metrics.targetScrapeExemplarOutOfOrder.Add(float64(outOfOrderExemplars))
+ }
+ return true, nil
+ }
return false, err
}
}
diff --git a/scrape/scrape_append_v2.go b/scrape/scrape_append_v2.go
new file mode 100644
index 0000000000..64969707e1
--- /dev/null
+++ b/scrape/scrape_append_v2.go
@@ -0,0 +1,416 @@
+// Copyright 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 scrape
+
+import (
+ "errors"
+ "fmt"
+ "io"
+ "math"
+ "slices"
+ "time"
+
+ "github.com/prometheus/common/model"
+
+ "github.com/prometheus/prometheus/model/exemplar"
+ "github.com/prometheus/prometheus/model/histogram"
+ "github.com/prometheus/prometheus/model/labels"
+ "github.com/prometheus/prometheus/model/textparse"
+ "github.com/prometheus/prometheus/model/timestamp"
+ "github.com/prometheus/prometheus/model/value"
+ "github.com/prometheus/prometheus/storage"
+)
+
+// appenderWithLimits returns an appender with additional validation.
+func appenderV2WithLimits(app storage.AppenderV2, sampleLimit, bucketLimit int, maxSchema int32) storage.AppenderV2 {
+ app = &timeLimitAppenderV2{
+ AppenderV2: app,
+ maxTime: timestamp.FromTime(time.Now().Add(maxAheadTime)),
+ }
+
+ // The sampleLimit is applied after metrics are potentially dropped via relabeling.
+ if sampleLimit > 0 {
+ app = &limitAppenderV2{
+ AppenderV2: app,
+ limit: sampleLimit,
+ }
+ }
+
+ if bucketLimit > 0 {
+ app = &bucketLimitAppenderV2{
+ AppenderV2: app,
+ limit: bucketLimit,
+ }
+ }
+
+ if maxSchema < histogram.ExponentialSchemaMax {
+ app = &maxSchemaAppenderV2{
+ AppenderV2: app,
+ maxSchema: maxSchema,
+ }
+ }
+
+ return app
+}
+
+func (sl *scrapeLoop) updateStaleMarkersV2(app storage.AppenderV2, defTime int64) (err error) {
+ sl.cache.forEachStale(func(ref storage.SeriesRef, lset labels.Labels) bool {
+ // Series no longer exposed, mark it stale.
+ _, err = app.Append(ref, lset, 0, defTime, math.Float64frombits(value.StaleNaN), nil, nil, storage.AOptions{RejectOutOfOrder: true})
+ switch {
+ case errors.Is(err, storage.ErrOutOfOrderSample), errors.Is(err, storage.ErrDuplicateSampleForTimestamp):
+ // Do not count these in logging, as this is expected if a target
+ // goes away and comes back again with a new scrape loop.
+ err = nil
+ }
+ return err == nil
+ })
+ return err
+}
+
+type scrapeLoopAppenderV2 struct {
+ *scrapeLoop
+
+ storage.AppenderV2
+}
+
+var _ scrapeLoopAppendAdapter = &scrapeLoopAppenderV2{}
+
+func (sl *scrapeLoopAppenderV2) append(b []byte, contentType string, ts time.Time) (total, added, seriesAdded int, err error) {
+ defTime := timestamp.FromTime(ts)
+
+ if len(b) == 0 {
+ // Empty scrape. Just update the stale makers and swap the cache (but don't flush it).
+ err = sl.updateStaleMarkersV2(sl.AppenderV2, defTime)
+ sl.cache.iterDone(false)
+ return total, added, seriesAdded, err
+ }
+
+ p, err := textparse.New(b, contentType, sl.symbolTable, textparse.ParserOptions{
+ EnableTypeAndUnitLabels: sl.enableTypeAndUnitLabels,
+ IgnoreNativeHistograms: !sl.enableNativeHistogramScraping,
+ ConvertClassicHistogramsToNHCB: sl.convertClassicHistToNHCB,
+ KeepClassicOnClassicAndNativeHistograms: sl.alwaysScrapeClassicHist,
+ OpenMetricsSkipSTSeries: sl.enableSTZeroIngestion,
+ FallbackContentType: sl.fallbackScrapeProtocol,
+ })
+ if p == nil {
+ sl.l.Error(
+ "Failed to determine correct type of scrape target.",
+ "content_type", contentType,
+ "fallback_media_type", sl.fallbackScrapeProtocol,
+ "err", err,
+ )
+ return total, added, seriesAdded, err
+ }
+ if err != nil {
+ sl.l.Debug(
+ "Invalid content type on scrape, using fallback setting.",
+ "content_type", contentType,
+ "fallback_media_type", sl.fallbackScrapeProtocol,
+ "err", err,
+ )
+ }
+ var (
+ appErrs = appendErrors{}
+ sampleLimitErr error
+ bucketLimitErr error
+ lset labels.Labels // Escapes to heap so hoisted out of loop.
+ e exemplar.Exemplar // Escapes to heap so hoisted out of loop.
+ lastMeta *metaEntry
+ lastMFName []byte
+ )
+
+ exemplars := make([]exemplar.Exemplar, 0, 1)
+
+ // Take an appender with limits.
+ app := appenderV2WithLimits(sl.AppenderV2, sl.sampleLimit, sl.bucketLimit, sl.maxSchema)
+
+ defer func() {
+ if err != nil {
+ return
+ }
+ // Flush and swap the cache as the scrape was non-empty.
+ sl.cache.iterDone(true)
+ }()
+
+loop:
+ for {
+ var (
+ et textparse.Entry
+ sampleAdded, isHistogram bool
+ met []byte
+ parsedTimestamp *int64
+ val float64
+ h *histogram.Histogram
+ fh *histogram.FloatHistogram
+ )
+ if et, err = p.Next(); err != nil {
+ if errors.Is(err, io.EOF) {
+ err = nil
+ }
+ break
+ }
+ switch et {
+ // TODO(bwplotka): Consider changing parser to give metadata at once instead of type, help and unit in separation, ideally on `Series()/Histogram()
+ // otherwise we can expose metadata without series on metadata API.
+ case textparse.EntryType:
+ // TODO(bwplotka): Build meta entry directly instead of locking and updating the map. This will
+ // allow to properly update metadata when e.g unit was added, then removed;
+ lastMFName, lastMeta = sl.cache.setType(p.Type())
+ continue
+ case textparse.EntryHelp:
+ lastMFName, lastMeta = sl.cache.setHelp(p.Help())
+ continue
+ case textparse.EntryUnit:
+ lastMFName, lastMeta = sl.cache.setUnit(p.Unit())
+ continue
+ case textparse.EntryComment:
+ continue
+ case textparse.EntryHistogram:
+ isHistogram = true
+ default:
+ }
+ total++
+
+ t := defTime
+ if isHistogram {
+ met, parsedTimestamp, h, fh = p.Histogram()
+ } else {
+ met, parsedTimestamp, val = p.Series()
+ }
+ if !sl.honorTimestamps {
+ parsedTimestamp = nil
+ }
+ if parsedTimestamp != nil {
+ t = *parsedTimestamp
+ }
+
+ if sl.cache.getDropped(met) {
+ continue
+ }
+ ce, seriesCached, seriesAlreadyScraped := sl.cache.get(met)
+ var (
+ ref storage.SeriesRef
+ hash uint64
+ )
+
+ if seriesCached {
+ ref = ce.ref
+ lset = ce.lset
+ hash = ce.hash
+ } else {
+ p.Labels(&lset)
+ hash = lset.Hash()
+
+ // Hash label set as it is seen local to the target. Then add target labels
+ // and relabeling and store the final label set.
+ lset = sl.sampleMutator(lset)
+
+ // The label set may be set to empty to indicate dropping.
+ if lset.IsEmpty() {
+ sl.cache.addDropped(met)
+ continue
+ }
+
+ if !lset.Has(model.MetricNameLabel) {
+ err = errNameLabelMandatory
+ break loop
+ }
+ if !lset.IsValid(sl.validationScheme) {
+ err = fmt.Errorf("invalid metric name or label names: %s", lset.String())
+ break loop
+ }
+
+ // If any label limits is exceeded the scrape should fail.
+ if err = verifyLabelLimits(lset, sl.labelLimits); err != nil {
+ sl.metrics.targetScrapePoolExceededLabelLimits.Inc()
+ break loop
+ }
+ }
+
+ exemplars = exemplars[:0] // Reset and reuse the exemplar slice.
+
+ if seriesAlreadyScraped && parsedTimestamp == nil {
+ err = storage.ErrDuplicateSampleForTimestamp
+ } else {
+ // Double check we don't append float 0 for
+ // histogram case where parser returns bad data.
+ // This can only happen when parser has a bug.
+ if isHistogram && h == nil && fh == nil {
+ err = fmt.Errorf("parser returned nil histogram/float histogram for a histogram entry type for %v series; parser bug; aborting", lset.String())
+ break loop
+ }
+
+ st := int64(0)
+ if sl.enableSTZeroIngestion {
+ // p.StartTimestamp() tend to be expensive (e.g. OM1). Do it only if we care.
+ st = p.StartTimestamp()
+ }
+
+ for hasExemplar := p.Exemplar(&e); hasExemplar; hasExemplar = p.Exemplar(&e) {
+ if !e.HasTs {
+ if isHistogram {
+ // We drop exemplars for native histograms if they don't have a timestamp.
+ // Missing timestamps are deliberately not supported as we want to start
+ // enforcing timestamps for exemplars as otherwise proper deduplication
+ // is inefficient and purely based on heuristics: we cannot distinguish
+ // between repeated exemplars and new instances with the same values.
+ // This is done silently without logs as it is not an error but out of spec.
+ // This does not affect classic histograms so that behaviour is unchanged.
+ e = exemplar.Exemplar{} // Reset for the next fetch.
+ continue
+ }
+ e.Ts = t
+ }
+ exemplars = append(exemplars, e)
+ e = exemplar.Exemplar{} // Reset for the next fetch.
+ }
+
+ // Prepare append call.
+ appOpts := storage.AOptions{}
+ if len(exemplars) > 0 {
+ // Sort so that checking for duplicates / out of order is more efficient during validation.
+ slices.SortFunc(exemplars, exemplar.Compare)
+ appOpts.Exemplars = exemplars
+ }
+
+ // Metadata path mimicks the scrape appender V1 flow. Once we remove v2
+ // flow we should rename "appendMetadataToWAL" flag to "passMetadata" because for v2 flow
+ // the metadata storage detail is behind the appendableV2 contract. V2 also means we always pass the metadata,
+ // we don't check if it changed (that code can be removed).
+ //
+ // Long term, we should always attach the metadata without any flag. Unfortunately because of the limitation
+ // of the TEXT and OpenMetrics 1.0 (hopefully fixed in OpenMetrics 2.0) there are edge cases around unknown
+ // metadata + suffixes that is expensive (isSeriesPartOfFamily) or in some cases impossible to detect. For this
+ // reason metadata (appendMetadataToWAL=true) appender V2 flow scrape might taking ~3% more CPU in our benchmarks.
+ //
+ // TODO(https://github.com/prometheus/prometheus/issues/17900): Optimize this, notably move this check to parsers that require this (ensuring parser
+ // interface always yields correct metadata), deliver OpenMetrics 2.0 that removes suffixes.
+ if sl.appendMetadataToWAL && lastMeta != nil {
+ // In majority cases we can trust that the current series/histogram is matching the lastMeta and lastMFName.
+ // However, optional TYPE, etc metadata and broken OM text can break this, detect those cases here.
+ if !isSeriesPartOfFamily(lset.Get(model.MetricNameLabel), lastMFName, lastMeta.Type) {
+ lastMeta = nil // Don't pass knowingly broken metadata, now, nor on the next line.
+ }
+ if lastMeta != nil {
+ // Metric family name has the same source as metadata.
+ appOpts.MetricFamilyName = yoloString(lastMFName)
+ appOpts.Metadata = lastMeta.Metadata
+ }
+ }
+
+ // Append sample to the storage.
+ ref, err = app.Append(ref, lset, st, t, val, h, fh, appOpts)
+ }
+ sampleAdded, err = sl.checkAddError(met, exemplars, err, &sampleLimitErr, &bucketLimitErr, &appErrs)
+ if err != nil {
+ if !errors.Is(err, storage.ErrNotFound) {
+ sl.l.Debug("Unexpected error", "series", string(met), "err", err)
+ }
+ break loop
+ }
+ if (parsedTimestamp == nil || sl.trackTimestampsStaleness) && ce != nil {
+ sl.cache.trackStaleness(ce.ref, ce)
+ }
+
+ // If series wasn't cached (is new, not seen on previous scrape) we need to add it to the scrape cache.
+ // But we only do this for series that were appended to TSDB without errors.
+ // If a series was new, but we didn't append it due to sample_limit or other errors then we don't need
+ // it in the scrape cache because we don't need to emit StaleNaNs for it when it disappears.
+ if !seriesCached && sampleAdded {
+ ce = sl.cache.addRef(met, ref, lset, hash)
+ if ce != nil && (parsedTimestamp == nil || sl.trackTimestampsStaleness) {
+ // Bypass staleness logic if there is an explicit timestamp.
+ // But make sure we only do this if we have a cache entry (ce) for our series.
+ sl.cache.trackStaleness(ref, ce)
+ }
+ if sampleLimitErr == nil && bucketLimitErr == nil {
+ seriesAdded++
+ }
+ }
+
+ // Increment added even if there's an error so we correctly report the
+ // number of samples remaining after relabeling.
+ // We still report duplicated samples here since this number should be the exact number
+ // of time series exposed on a scrape after relabelling.
+ added++
+ }
+ if sampleLimitErr != nil {
+ if err == nil {
+ err = sampleLimitErr
+ }
+ // We only want to increment this once per scrape, so this is Inc'd outside the loop.
+ sl.metrics.targetScrapeSampleLimit.Inc()
+ }
+ if bucketLimitErr != nil {
+ if err == nil {
+ err = bucketLimitErr // If sample limit is hit, that error takes precedence.
+ }
+ // We only want to increment this once per scrape, so this is Inc'd outside the loop.
+ sl.metrics.targetScrapeNativeHistogramBucketLimit.Inc()
+ }
+ if appErrs.numOutOfOrder > 0 {
+ sl.l.Warn("Error on ingesting out-of-order samples", "num_dropped", appErrs.numOutOfOrder)
+ }
+ if appErrs.numDuplicates > 0 {
+ sl.l.Warn("Error on ingesting samples with different value but same timestamp", "num_dropped", appErrs.numDuplicates)
+ }
+ if appErrs.numOutOfBounds > 0 {
+ sl.l.Warn("Error on ingesting samples that are too old or are too far into the future", "num_dropped", appErrs.numOutOfBounds)
+ }
+ if appErrs.numExemplarOutOfOrder > 0 {
+ sl.l.Warn("Error on ingesting out-of-order exemplars", "num_dropped", appErrs.numExemplarOutOfOrder)
+ }
+ if err == nil {
+ err = sl.updateStaleMarkersV2(app, defTime)
+ }
+ return total, added, seriesAdded, err
+}
+
+func (sl *scrapeLoopAppenderV2) addReportSample(s reportSample, t int64, v float64, b *labels.Builder, rejectOOO bool) (err error) {
+ ce, ok, _ := sl.cache.get(s.name)
+ var ref storage.SeriesRef
+ var lset labels.Labels
+ if ok {
+ ref = ce.ref
+ lset = ce.lset
+ } else {
+ // The constants are suffixed with the invalid \xff unicode rune to avoid collisions
+ // with scraped metrics in the cache.
+ // We have to drop it when building the actual metric.
+ b.Reset(labels.EmptyLabels())
+ b.Set(model.MetricNameLabel, string(s.name[:len(s.name)-1]))
+ lset = sl.reportSampleMutator(b.Labels())
+ }
+
+ ref, err = sl.Append(ref, lset, 0, t, v, nil, nil, storage.AOptions{
+ MetricFamilyName: yoloString(s.name),
+ Metadata: s.Metadata,
+ RejectOutOfOrder: rejectOOO,
+ })
+ switch {
+ case err == nil:
+ if !ok {
+ sl.cache.addRef(s.name, ref, lset, lset.Hash())
+ }
+ return nil
+ case errors.Is(err, storage.ErrOutOfOrderSample), errors.Is(err, storage.ErrDuplicateSampleForTimestamp):
+ // Do not log here, as this is expected if a target goes away and comes back
+ // again with a new scrape loop.
+ return nil
+ default:
+ return err
+ }
+}
diff --git a/scrape/scrape_test.go b/scrape/scrape_test.go
index c2b2ae132c..9c12a31ab3 100644
--- a/scrape/scrape_test.go
+++ b/scrape/scrape_test.go
@@ -36,8 +36,6 @@ import (
"time"
"github.com/gogo/protobuf/proto"
- "github.com/google/go-cmp/cmp"
- "github.com/google/go-cmp/cmp/cmpopts"
"github.com/grafana/regexp"
"github.com/prometheus/client_golang/prometheus"
prom_testutil "github.com/prometheus/client_golang/prometheus/testutil"
@@ -65,6 +63,7 @@ import (
"github.com/prometheus/prometheus/model/timestamp"
"github.com/prometheus/prometheus/model/value"
"github.com/prometheus/prometheus/storage"
+ "github.com/prometheus/prometheus/tsdb"
"github.com/prometheus/prometheus/tsdb/chunkenc"
"github.com/prometheus/prometheus/util/pool"
"github.com/prometheus/prometheus/util/teststorage"
@@ -88,42 +87,67 @@ func newTestScrapeMetrics(t testing.TB) *scrapeMetrics {
}
func TestNewScrapePool(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testNewScrapePool(t, appV2)
+ })
+}
+
+func testNewScrapePool(t *testing.T, appV2 bool) {
var (
app = teststorage.NewAppendable()
+ sa = selectAppendable(app, appV2)
cfg = &config.ScrapeConfig{
MetricNameValidationScheme: model.UTF8Validation,
MetricNameEscapingScheme: model.AllowUTF8,
}
- sp, err = newScrapePool(cfg, app, 0, nil, nil, &Options{}, newTestScrapeMetrics(t))
+ sp, err = newScrapePool(cfg, sa.V1(), sa.V2(), 0, nil, nil, &Options{}, newTestScrapeMetrics(t))
)
require.NoError(t, err)
+ if appV2 {
+ a, ok := sp.appendableV2.(*teststorage.Appendable)
+ require.True(t, ok, "Failure to append.")
+ require.Equal(t, app, a, "Wrong sample AppenderV2.")
+ require.Equal(t, cfg, sp.config, "Wrong scrape config.")
+
+ require.Nil(t, sp.appendable)
+ return
+ }
a, ok := sp.appendable.(*teststorage.Appendable)
require.True(t, ok, "Failure to append.")
require.Equal(t, app, a, "Wrong sample appender.")
require.Equal(t, cfg, sp.config, "Wrong scrape config.")
+
+ require.Nil(t, sp.appendableV2)
}
func TestStorageHandlesOutOfOrderTimestamps(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testStorageHandlesOutOfOrderTimestamps(t, appV2)
+ })
+}
+
+func testStorageHandlesOutOfOrderTimestamps(t *testing.T, appV2 bool) {
// Test with default OutOfOrderTimeWindow (0)
t.Run("Out-Of-Order Sample Disabled", func(t *testing.T) {
s := teststorage.New(t)
t.Cleanup(func() { _ = s.Close() })
-
- runScrapeLoopTest(t, s, false)
+ runScrapeLoopTest(t, appV2, s, false)
})
// Test with specific OutOfOrderTimeWindow (600000)
t.Run("Out-Of-Order Sample Enabled", func(t *testing.T) {
- s := teststorage.New(t, 600000)
+ s := teststorage.New(t, func(opt *tsdb.Options) {
+ opt.OutOfOrderTimeWindow = 600000
+ })
t.Cleanup(func() { _ = s.Close() })
- runScrapeLoopTest(t, s, true)
+ runScrapeLoopTest(t, appV2, s, true)
})
}
-func runScrapeLoopTest(t *testing.T, s *teststorage.TestStorage, expectOutOfOrder bool) {
- sl, _ := newTestScrapeLoop(t, withAppendable(s))
+func runScrapeLoopTest(t *testing.T, appV2 bool, s *teststorage.TestStorage, expectOutOfOrder bool) {
+ sl, _ := newTestScrapeLoop(t, withAppendable(s, appV2))
// Current time for generating timestamps.
now := time.Now()
@@ -184,14 +208,20 @@ func runScrapeLoopTest(t *testing.T, s *teststorage.TestStorage, expectOutOfOrde
}
if expectOutOfOrder {
- require.NotEqual(t, want, results, "Expected results to include out-of-order sample:\n%s", results)
+ teststorage.RequireNotEqual(t, want, results, "Expected results to include out-of-order sample:\n%s", results)
} else {
- require.Equal(t, want, results, "Appended samples not as expected:\n%s", results)
+ teststorage.RequireEqual(t, want, results, "Appended samples not as expected:\n%s", results)
}
}
// Regression test against https://github.com/prometheus/prometheus/issues/15831.
func TestScrapeAppend_MetadataUpdate(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeAppendMetadataUpdate(t, appV2)
+ })
+}
+
+func testScrapeAppendMetadataUpdate(t *testing.T, appV2 bool) {
const (
scrape1 = `# TYPE test_metric counter
# HELP test_metric some help text
@@ -215,32 +245,40 @@ test_metric2{foo="bar"} 22
)
appTest := teststorage.NewAppendable()
- sl, _ := newTestScrapeLoop(t, withAppendable(appTest))
+ sl, _ := newTestScrapeLoop(t, withAppendable(appTest, appV2))
now := time.Now()
app := sl.appender()
_, _, _, err := app.append([]byte(scrape1), "application/openmetrics-text", now)
require.NoError(t, err)
require.NoError(t, app.Commit())
- testutil.RequireEqual(t, []sample{
+ teststorage.RequireEqual(t, []sample{
{L: labels.FromStrings("__name__", "test_metric_total"), M: metadata.Metadata{Type: "counter", Unit: "metric", Help: "some help text"}},
{L: labels.FromStrings("__name__", "test_metric2", "foo", "bar"), M: metadata.Metadata{Type: "gauge", Unit: "", Help: "other help text"}},
}, appTest.ResultMetadata())
appTest.ResultReset()
- // Next (the same) scrape should not new metadata entries.
app = sl.appender()
_, _, _, err = app.append([]byte(scrape1), "application/openmetrics-text", now.Add(15*time.Second))
require.NoError(t, err)
require.NoError(t, app.Commit())
- require.Empty(t, appTest.ResultMetadata())
+ if appV2 {
+ // Next (the same) scrape should pass new metadata entries as per always-on metadata Appendable V2 contract.
+ teststorage.RequireEqual(t, []sample{
+ {L: labels.FromStrings("__name__", "test_metric_total"), M: metadata.Metadata{Type: "counter", Unit: "metric", Help: "some help text"}},
+ {L: labels.FromStrings("__name__", "test_metric2", "foo", "bar"), M: metadata.Metadata{Type: "gauge", Unit: "", Help: "other help text"}},
+ }, appTest.ResultMetadata())
+ } else {
+ // Next (the same) scrape should not add new metadata entries.
+ require.Empty(t, appTest.ResultMetadata())
+ }
appTest.ResultReset()
app = sl.appender()
_, _, _, err = app.append([]byte(scrape2), "application/openmetrics-text", now.Add(15*time.Second))
require.NoError(t, err)
require.NoError(t, app.Commit())
- testutil.RequireEqual(t, []sample{
+ teststorage.RequireEqual(t, []sample{
{L: labels.FromStrings("__name__", "test_metric_total"), M: metadata.Metadata{Type: "counter", Unit: "metric", Help: "different help text"}}, // Here, technically we should have no unit, but it's a known limitation of the current implementation.
{L: labels.FromStrings("__name__", "test_metric2", "foo", "bar"), M: metadata.Metadata{Type: "gauge", Unit: "metric2", Help: "other help text"}},
}, appTest.ResultMetadata())
@@ -248,14 +286,20 @@ test_metric2{foo="bar"} 22
}
func TestScrapeReportMetadata(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeReportMetadata(t, appV2)
+ })
+}
+
+func testScrapeReportMetadata(t *testing.T, appV2 bool) {
appTest := teststorage.NewAppendable()
- sl, _ := newTestScrapeLoop(t, withAppendable(appTest))
+ sl, _ := newTestScrapeLoop(t, withAppendable(appTest, appV2))
app := sl.appender()
now := time.Now()
require.NoError(t, sl.report(app, now, 2*time.Second, 1, 1, 1, 512, nil))
require.NoError(t, app.Commit())
- testutil.RequireEqual(t, []sample{
+ teststorage.RequireEqual(t, []sample{
{L: labels.FromStrings("__name__", "up"), M: scrapeHealthMetric.Metadata},
{L: labels.FromStrings("__name__", "scrape_duration_seconds"), M: scrapeDurationMetric.Metadata},
{L: labels.FromStrings("__name__", "scrape_samples_scraped"), M: scrapeSamplesMetric.Metadata},
@@ -313,6 +357,12 @@ func TestIsSeriesPartOfFamily(t *testing.T) {
}
func TestDroppedTargetsList(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testDroppedTargetsList(t, appV2)
+ })
+}
+
+func testDroppedTargetsList(t *testing.T, appV2 bool) {
var (
app = teststorage.NewAppendable()
cfg = &config.ScrapeConfig{
@@ -337,7 +387,8 @@ func TestDroppedTargetsList(t *testing.T) {
},
},
}
- sp, _ = newScrapePool(cfg, app, 0, nil, nil, &Options{}, newTestScrapeMetrics(t))
+ sa = selectAppendable(app, appV2)
+ sp, _ = newScrapePool(cfg, sa.V1(), sa.V2(), 0, nil, nil, &Options{}, newTestScrapeMetrics(t))
expectedLabelSetString = "{__address__=\"127.0.0.1:9090\", __scrape_interval__=\"0s\", __scrape_timeout__=\"0s\", job=\"dropMe\"}"
expectedLength = 2
)
@@ -358,7 +409,7 @@ func TestDroppedTargetsList(t *testing.T) {
// TestDiscoveredLabelsUpdate checks that DiscoveredLabels are updated
// even when new labels don't affect the target `hash`.
func TestDiscoveredLabelsUpdate(t *testing.T) {
- sp := newTestScrapePool(t, nil)
+ sp := newTestScrapePool(t, nil, false, nil)
// These are used when syncing so need this to avoid a panic.
sp.config = &config.ScrapeConfig{
@@ -430,7 +481,7 @@ func (*testLoop) getCache() *scrapeCache {
func TestScrapePoolStop(t *testing.T) {
t.Parallel()
- sp := newTestScrapePool(t, nil)
+ sp := newTestScrapePool(t, nil, false, nil)
var mtx sync.Mutex
stopped := map[uint64]bool{}
@@ -530,7 +581,7 @@ func TestScrapePoolReload(t *testing.T) {
// Create test pool.
reg, metrics := newTestRegistryAndScrapeMetrics(t)
- sp := newTestScrapePool(t, newLoopCfg1)
+ sp := newTestScrapePool(t, nil, false, newLoopCfg1)
sp.metrics = metrics
// Prefill pool with 20 loops, simulating 20 scrape targets.
@@ -592,7 +643,7 @@ func TestScrapePoolReloadPreserveRelabeledIntervalTimeout(t *testing.T) {
return l
}
reg, metrics := newTestRegistryAndScrapeMetrics(t)
- sp := newTestScrapePool(t, newLoop)
+ sp := newTestScrapePool(t, nil, false, newLoop)
sp.activeTargets[1] = &Target{
labels: labels.FromStrings(model.ScrapeIntervalLabel, "5s", model.ScrapeTimeoutLabel, "3s"),
}
@@ -644,7 +695,7 @@ func TestScrapePoolTargetLimit(t *testing.T) {
return l
}
- sp := newTestScrapePool(t, newLoop)
+ sp := newTestScrapePool(t, nil, false, newLoop)
var tgs []*targetgroup.Group
for i := range 50 {
@@ -756,7 +807,9 @@ func TestScrapePoolAppenderWithLimits(t *testing.T) {
baseAppender := struct{ storage.Appender }{}
appendable := appendableFunc(func(context.Context) storage.Appender { return baseAppender })
- sl, _ := newTestScrapeLoop(t, withAppendable(appendable))
+ sl, _ := newTestScrapeLoop(t, func(sl *scrapeLoop) {
+ sl.appendable = appendable
+ })
wrapped := appenderWithLimits(sl.appendable.Appender(context.Background()), 0, 0, histogram.ExponentialSchemaMax)
tl, ok := wrapped.(*timeLimitAppender)
@@ -809,7 +862,77 @@ func TestScrapePoolAppenderWithLimits(t *testing.T) {
require.Equal(t, baseAppender, tl.Appender, "Expected base appender but got %T", tl.Appender)
}
+type appendableV2Func func(ctx context.Context) storage.AppenderV2
+
+func (a appendableV2Func) AppenderV2(ctx context.Context) storage.AppenderV2 { return a(ctx) }
+
+func TestScrapePoolAppenderWithLimits_AppendV2(t *testing.T) {
+ // Create a unique value, to validate the correct chain of appenders.
+ baseAppender := struct{ storage.AppenderV2 }{}
+ appendable := appendableV2Func(func(context.Context) storage.AppenderV2 { return baseAppender })
+
+ sl, _ := newTestScrapeLoop(t, func(sl *scrapeLoop) {
+ sl.appendableV2 = appendable
+ })
+ wrapped := appenderV2WithLimits(sl.appendableV2.AppenderV2(context.Background()), 0, 0, histogram.ExponentialSchemaMax)
+
+ tl, ok := wrapped.(*timeLimitAppenderV2)
+ require.True(t, ok, "Expected timeLimitAppenderV2 but got %T", wrapped)
+
+ require.Equal(t, baseAppender, tl.AppenderV2, "Expected base AppenderV2 but got %T", tl.AppenderV2)
+
+ sampleLimit := 100
+ sl, _ = newTestScrapeLoop(t, func(sl *scrapeLoop) {
+ sl.appendableV2 = appendable
+ sl.sampleLimit = sampleLimit
+ })
+ wrapped = appenderV2WithLimits(sl.appendableV2.AppenderV2(context.Background()), sampleLimit, 0, histogram.ExponentialSchemaMax)
+
+ la, ok := wrapped.(*limitAppenderV2)
+ require.True(t, ok, "Expected limitAppenderV2 but got %T", wrapped)
+
+ tl, ok = la.AppenderV2.(*timeLimitAppenderV2)
+ require.True(t, ok, "Expected timeLimitAppenderV2 but got %T", la.AppenderV2)
+
+ require.Equal(t, baseAppender, tl.AppenderV2, "Expected base AppenderV2 but got %T", tl.AppenderV2)
+
+ wrapped = appenderV2WithLimits(sl.appendableV2.AppenderV2(context.Background()), sampleLimit, 100, histogram.ExponentialSchemaMax)
+
+ bl, ok := wrapped.(*bucketLimitAppenderV2)
+ require.True(t, ok, "Expected bucketLimitAppenderV2 but got %T", wrapped)
+
+ la, ok = bl.AppenderV2.(*limitAppenderV2)
+ require.True(t, ok, "Expected limitAppenderV2 but got %T", bl)
+
+ tl, ok = la.AppenderV2.(*timeLimitAppenderV2)
+ require.True(t, ok, "Expected timeLimitAppenderV2 but got %T", la.AppenderV2)
+
+ require.Equal(t, baseAppender, tl.AppenderV2, "Expected base AppenderV2 but got %T", tl.AppenderV2)
+
+ wrapped = appenderV2WithLimits(sl.appendableV2.AppenderV2(context.Background()), sampleLimit, 100, 0)
+
+ ml, ok := wrapped.(*maxSchemaAppenderV2)
+ require.True(t, ok, "Expected maxSchemaAppenderV2 but got %T", wrapped)
+
+ bl, ok = ml.AppenderV2.(*bucketLimitAppenderV2)
+ require.True(t, ok, "Expected bucketLimitAppenderV2 but got %T", wrapped)
+
+ la, ok = bl.AppenderV2.(*limitAppenderV2)
+ require.True(t, ok, "Expected limitAppenderV2 but got %T", bl)
+
+ tl, ok = la.AppenderV2.(*timeLimitAppenderV2)
+ require.True(t, ok, "Expected timeLimitAppenderV2 but got %T", la.AppenderV2)
+
+ require.Equal(t, baseAppender, tl.AppenderV2, "Expected base AppenderV2 but got %T", tl.AppenderV2)
+}
+
func TestScrapePoolRaces(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapePoolRaces(t, appV2)
+ })
+}
+
+func testScrapePoolRaces(t *testing.T, appV2 bool) {
t.Parallel()
interval, _ := model.ParseDuration("1s")
timeout, _ := model.ParseDuration("500ms")
@@ -821,7 +944,8 @@ func TestScrapePoolRaces(t *testing.T) {
MetricNameEscapingScheme: model.AllowUTF8,
}
}
- sp, _ := newScrapePool(newConfig(), teststorage.NewAppendable(), 0, nil, nil, &Options{}, newTestScrapeMetrics(t))
+ sa := selectAppendable(teststorage.NewAppendable(), appV2)
+ sp, _ := newScrapePool(newConfig(), sa.V1(), sa.V2(), 0, nil, nil, &Options{}, newTestScrapeMetrics(t))
tgts := []*targetgroup.Group{
{
Targets: []model.LabelSet{
@@ -853,6 +977,12 @@ func TestScrapePoolRaces(t *testing.T) {
}
func TestScrapePoolScrapeLoopsStarted(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapePoolScrapeLoopsStarted(t, appV2)
+ })
+}
+
+func testScrapePoolScrapeLoopsStarted(t *testing.T, appV2 bool) {
var wg sync.WaitGroup
newLoop := func(scrapeLoopOptions) loop {
wg.Add(1)
@@ -864,7 +994,7 @@ func TestScrapePoolScrapeLoopsStarted(t *testing.T) {
}
return l
}
- sp := newTestScrapePool(t, newLoop)
+ sp := newTestScrapePool(t, teststorage.NewAppendable(), appV2, newLoop)
tgs := []*targetgroup.Group{
{
@@ -946,11 +1076,16 @@ func TestScrapeLoopStopBeforeRun(t *testing.T) {
func nopMutator(l labels.Labels) labels.Labels { return l }
func TestScrapeLoopStop(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopStop(t, appV2)
+ })
+}
+
+func testScrapeLoopStop(t *testing.T, appV2 bool) {
signal := make(chan struct{}, 1)
appTest := teststorage.NewAppendable()
- sl, scraper := newTestScrapeLoop(t, func(sl *scrapeLoop) {
- sl.appendable = appTest
+ sl, scraper := newTestScrapeLoop(t, withAppendable(appTest, appV2), func(sl *scrapeLoop) {
// Since we're writing samples directly below we need to provide a protocol fallback.
sl.fallbackScrapeProtocol = "text/plain"
})
@@ -1000,6 +1135,12 @@ func TestScrapeLoopStop(t *testing.T) {
}
func TestScrapeLoopRun(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopRun(t, appV2)
+ })
+}
+
+func testScrapeLoopRun(t *testing.T, appV2 bool) {
t.Parallel()
var (
signal = make(chan struct{}, 1)
@@ -1007,7 +1148,7 @@ func TestScrapeLoopRun(t *testing.T) {
)
ctx, cancel := context.WithCancel(t.Context())
- sl, scraper := newTestScrapeLoop(t, withCtx(ctx))
+ sl, scraper := newTestScrapeLoop(t, withCtx(ctx), withAppendable(teststorage.NewAppendable(), appV2))
// The loop must terminate during the initial offset if the context
// is canceled.
scraper.offsetDur = time.Hour
@@ -1030,7 +1171,7 @@ func TestScrapeLoopRun(t *testing.T) {
}
ctx, cancel = context.WithCancel(t.Context())
- sl, scraper = newTestScrapeLoop(t, func(sl *scrapeLoop) {
+ sl, scraper = newTestScrapeLoop(t, withAppendable(teststorage.NewAppendable(), appV2), func(sl *scrapeLoop) {
sl.ctx = ctx
sl.timeout = 100 * time.Millisecond
})
@@ -1076,13 +1217,19 @@ func TestScrapeLoopRun(t *testing.T) {
}
func TestScrapeLoopForcedErr(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopForcedErr(t, appV2)
+ })
+}
+
+func testScrapeLoopForcedErr(t *testing.T, appV2 bool) {
var (
signal = make(chan struct{}, 1)
errc = make(chan error)
)
ctx, cancel := context.WithCancel(t.Context())
- sl, scraper := newTestScrapeLoop(t, withCtx(ctx))
+ sl, scraper := newTestScrapeLoop(t, withCtx(ctx), withAppendable(teststorage.NewAppendable(), appV2))
forcedErr := errors.New("forced err")
sl.setForcedError(forcedErr)
@@ -1113,6 +1260,12 @@ func TestScrapeLoopForcedErr(t *testing.T) {
}
func TestScrapeLoopRun_ContextCancelTerminatesBlockedSend(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopRunContextCancelTerminatesBlockedSend(t, appV2)
+ })
+}
+
+func testScrapeLoopRunContextCancelTerminatesBlockedSend(t *testing.T, appV2 bool) {
// Regression test for issue #17553
defer goleak.VerifyNone(t)
@@ -1122,7 +1275,7 @@ func TestScrapeLoopRun_ContextCancelTerminatesBlockedSend(t *testing.T) {
)
ctx, cancel := context.WithCancel(t.Context())
- sl, scraper := newTestScrapeLoop(t, withCtx(ctx))
+ sl, scraper := newTestScrapeLoop(t, withCtx(ctx), withAppendable(teststorage.NewAppendable(), appV2))
forcedErr := errors.New("forced err")
sl.setForcedError(forcedErr)
@@ -1149,7 +1302,13 @@ func TestScrapeLoopRun_ContextCancelTerminatesBlockedSend(t *testing.T) {
}
func TestScrapeLoopMetadata(t *testing.T) {
- sl, _ := newTestScrapeLoop(t)
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopMetadata(t, appV2)
+ })
+}
+
+func testScrapeLoopMetadata(t *testing.T, appV2 bool) {
+ sl, _ := newTestScrapeLoop(t, withAppendable(teststorage.NewAppendable(), appV2))
app := sl.appender()
total, _, _, err := app.append([]byte(`# TYPE test_metric counter
@@ -1183,7 +1342,13 @@ test_metric_total 1
}
func TestScrapeLoopSeriesAdded(t *testing.T) {
- sl, _ := newTestScrapeLoop(t)
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopSeriesAdded(t, appV2)
+ })
+}
+
+func testScrapeLoopSeriesAdded(t *testing.T, appV2 bool) {
+ sl, _ := newTestScrapeLoop(t, withAppendable(teststorage.NewAppendable(), appV2))
app := sl.appender()
total, added, seriesAdded, err := app.append([]byte("test_metric 1\n"), "text/plain", time.Time{})
@@ -1203,6 +1368,12 @@ func TestScrapeLoopSeriesAdded(t *testing.T) {
}
func TestScrapeLoopFailWithInvalidLabelsAfterRelabel(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopFailWithInvalidLabelsAfterRelabel(t, appV2)
+ })
+}
+
+func testScrapeLoopFailWithInvalidLabelsAfterRelabel(t *testing.T, appV2 bool) {
target := &Target{
labels: labels.FromStrings("pod_label_invalid_012\xff", "test"),
}
@@ -1213,7 +1384,7 @@ func TestScrapeLoopFailWithInvalidLabelsAfterRelabel(t *testing.T) {
Replacement: "$1",
NameValidationScheme: model.UTF8Validation,
}}
- sl, _ := newTestScrapeLoop(t, func(sl *scrapeLoop) {
+ sl, _ := newTestScrapeLoop(t, withAppendable(teststorage.NewAppendable(), appV2), func(sl *scrapeLoop) {
sl.sampleMutator = func(l labels.Labels) labels.Labels {
return mutateSampleLabels(l, target, true, relabelConfig)
}
@@ -1229,7 +1400,13 @@ func TestScrapeLoopFailWithInvalidLabelsAfterRelabel(t *testing.T) {
}
func TestScrapeLoopFailLegacyUnderUTF8(t *testing.T) {
- sl, _ := newTestScrapeLoop(t, func(sl *scrapeLoop) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopFailLegacyUnderUTF8(t, appV2)
+ })
+}
+
+func testScrapeLoopFailLegacyUnderUTF8(t *testing.T, appV2 bool) {
+ sl, _ := newTestScrapeLoop(t, withAppendable(teststorage.NewAppendable(), appV2), func(sl *scrapeLoop) {
sl.validationScheme = model.LegacyValidation
})
@@ -1242,7 +1419,7 @@ func TestScrapeLoopFailLegacyUnderUTF8(t *testing.T) {
require.Equal(t, 0, seriesAdded)
// When scrapeloop has validation set to UTF-8, the metric is allowed.
- sl, _ = newTestScrapeLoop(t, func(sl *scrapeLoop) {
+ sl, _ = newTestScrapeLoop(t, withAppendable(teststorage.NewAppendable(), appV2), func(sl *scrapeLoop) {
sl.validationScheme = model.UTF8Validation
})
@@ -1275,13 +1452,50 @@ func makeTestGauges(n int) []byte {
return sb.Bytes()
}
+func makeTestHistogramsWithExemplars(n int) []byte {
+ sb := bytes.Buffer{}
+ for i := range n {
+ sb.WriteString(strings.ReplaceAll(`# HELP rpc_durations_histogram%d_seconds RPC latency distributions.
+# TYPE rpc_durations_histogram%d_seconds histogram
+rpc_durations_histogram%d_seconds_bucket{le="-0.00099"} 0
+rpc_durations_histogram%d_seconds_bucket{le="-0.00089"} 1 # {dummyID="1242"} -0.00091 1.7268398142239082e+09
+rpc_durations_histogram%d_seconds_bucket{le="-0.0007899999999999999"} 1 # {dummyID="17783"} -0.0003825067330956884 1.7268398142239082e+09
+rpc_durations_histogram%d_seconds_bucket{le="-0.0006899999999999999"} 2 # {dummyID="17783"} -0.0003825067330956884 1.7268398142239082e+09
+rpc_durations_histogram%d_seconds_bucket{le="-0.0005899999999999998"} 3 # {dummyID="17783"} -0.0003825067330956884 1.7268398142239082e+09
+rpc_durations_histogram%d_seconds_bucket{le="-0.0004899999999999998"} 4 # {dummyID="17783"} -0.0003825067330956884 1.7268398142239082e+09
+rpc_durations_histogram%d_seconds_bucket{le="-0.0003899999999999998"} 5 # {dummyID="17783"} -0.0003825067330956884 1.7268398142239082e+09
+rpc_durations_histogram%d_seconds_bucket{le="-0.0002899999999999998"} 6 # {dummyID="17783"} -0.0003825067330956884 1.7268398142239082e+09
+rpc_durations_histogram%d_seconds_bucket{le="-0.0001899999999999998"} 7 # {dummyID="84741"} -0.00020178290006788965 1.726839814829977e+09
+rpc_durations_histogram%d_seconds_bucket{le="-8.999999999999979e-05"} 7
+rpc_durations_histogram%d_seconds_bucket{le="1.0000000000000216e-05"} 8 # {dummyID="19206"} -4.6156147425468016e-05 1.7268398151337721e+09
+rpc_durations_histogram%d_seconds_bucket{le="0.00011000000000000022"} 9 # {dummyID="3974"} 9.528436760156754e-05 1.726839814526797e+09
+rpc_durations_histogram%d_seconds_bucket{le="0.00021000000000000023"} 11 # {dummyID="29640"} 0.00017459624183458996 1.7268398139220061e+09
+rpc_durations_histogram%d_seconds_bucket{le="0.0003100000000000002"} 15 # {dummyID="9818"} 0.0002791130914009552 1.7268398149821382e+09
+rpc_durations_histogram%d_seconds_bucket{le="0.0004100000000000002"} 15
+rpc_durations_histogram%d_seconds_bucket{le="0.0005100000000000003"} 15
+rpc_durations_histogram%d_seconds_bucket{le="0.0006100000000000003"} 15
+rpc_durations_histogram%d_seconds_bucket{le="0.0007100000000000003"} 15
+rpc_durations_histogram%d_seconds_bucket{le="0.0008100000000000004"} 15
+rpc_durations_histogram%d_seconds_bucket{le="0.0009100000000000004"} 15
+rpc_durations_histogram%d_seconds_bucket{le="+Inf"} 15
+rpc_durations_histogram%d_seconds_sum -8.452185437166741e-05
+rpc_durations_histogram%d_seconds_count 15
+rpc_durations_histogram%d_seconds_created 1.726839813016302e+09
+`, "%d", strconv.Itoa(i)))
+ }
+ sb.WriteString("# EOF\n")
+ return sb.Bytes()
+}
+
+// promTextToProto converts Prometheus text to proto.
+// Given expfmt decoding limitations, it does not support OpenMetrics fully (e.g. exemplars).
func promTextToProto(tb testing.TB, text []byte) []byte {
tb.Helper()
p := expfmt.NewTextParser(model.UTF8Validation)
fams, err := p.TextToMetricFamilies(bytes.NewReader(text))
if err != nil {
- tb.Fatal(err)
+ tb.Fatal("TextToMetricFamilies:", err)
}
// Order by name for the deterministic tests.
var names []string
@@ -1307,8 +1521,7 @@ func promTextToProto(tb testing.TB, text []byte) []byte {
func TestPromTextToProto(t *testing.T) {
metricsText := readTextParseTestMetrics(t)
- // TODO(bwplotka): Windows adds \r for new lines which is
- // not handled correctly in the expfmt parser, fix it.
+ // On windows \r is added when reading, but parsers do not support this. Kill it.
metricsText = bytes.ReplaceAll(metricsText, []byte("\r"), nil)
metricsProto := promTextToProto(t, metricsText)
@@ -1332,9 +1545,11 @@ func TestPromTextToProto(t *testing.T) {
require.Equal(t, "promhttp_metric_handler_requests_total", got[236])
}
-// BenchmarkScrapeLoopAppend benchmarks a core append function in a scrapeLoop
-// that creates a new parser and goes through a byte slice from a single scrape.
-// Benchmark compares append function run across 2 dimensions:
+// BenchmarkScrapeLoopAppend benchmarks scrape appends for typical cases.
+//
+// Benchmark compares append function run across 4 dimensions:
+// * `appV2`: appender V1 or V2
+// * `appendMetadataToWAL`: metadata-wal-records feature enabled or not
// *`data`: different sizes of metrics scraped e.g. one big gauge metric family
// with a thousand series and more realistic scenario with common types.
// *`fmt`: different scrape formats which will benchmark different parsers e.g.
@@ -1343,62 +1558,116 @@ func TestPromTextToProto(t *testing.T) {
// Recommended CLI invocation:
/*
export bench=append && go test ./scrape/... \
- -run '^$' -bench '^BenchmarkScrapeLoopAppend' \
- -benchtime 5s -count 6 -cpu 2 -timeout 999m \
+ -run '^$' -bench '^BenchmarkScrapeLoopAppend$' \
+ -benchtime 2s -count 6 -cpu 2 -timeout 999m \
| tee ${bench}.txt
*/
func BenchmarkScrapeLoopAppend(b *testing.B) {
- for _, data := range []struct {
- name string
- parsableText []byte
- }{
- {name: "1Fam1000Gauges", parsableText: makeTestGauges(2000)}, // ~68.1 KB, ~77.9 KB in proto.
- {name: "237FamsAllTypes", parsableText: readTextParseTestMetrics(b)}, // ~185.7 KB, ~70.6 KB in proto.
- } {
- b.Run(fmt.Sprintf("data=%v", data.name), func(b *testing.B) {
- metricsProto := promTextToProto(b, data.parsableText)
-
- for _, bcase := range []struct {
- name string
- contentType string
- parsable []byte
+ for _, appV2 := range []bool{false, true} {
+ for _, appendMetadataToWAL := range []bool{false, true} {
+ for _, data := range []struct {
+ name string
+ parsableText []byte
}{
- {name: "PromText", contentType: "text/plain", parsable: data.parsableText},
- {name: "OMText", contentType: "application/openmetrics-text", parsable: data.parsableText},
- {name: "PromProto", contentType: "application/vnd.google.protobuf", parsable: metricsProto},
+ {name: "1Fam1000Gauges", parsableText: makeTestGauges(2000)}, // ~68.1 KB, ~77.9 KB in proto.
+ {name: "237FamsAllTypes", parsableText: readTextParseTestMetrics(b)}, // ~185.7 KB, ~70.6 KB in proto.
} {
- b.Run(fmt.Sprintf("fmt=%v", bcase.name), func(b *testing.B) {
- // Need a full storage for correct Add/AddFast semantics.
- s := teststorage.New(b)
- b.Cleanup(func() { _ = s.Close() })
+ b.Run(fmt.Sprintf("appV2=%v/appendMetadataToWAL=%v/data=%v", appV2, appendMetadataToWAL, data.name), func(b *testing.B) {
+ metricsProto := promTextToProto(b, data.parsableText)
- sl, _ := newTestScrapeLoop(b, withAppendable(s))
- app := sl.appender()
- ts := time.Time{}
-
- b.ReportAllocs()
- b.ResetTimer()
- for b.Loop() {
- ts = ts.Add(time.Second)
- _, _, _, err := app.append(bcase.parsable, bcase.contentType, ts)
- if err != nil {
- b.Fatal(err)
- }
+ for _, bcase := range []struct {
+ name string
+ contentType string
+ parsable []byte
+ }{
+ {name: "PromText", contentType: "text/plain", parsable: data.parsableText},
+ {name: "OMText", contentType: "application/openmetrics-text", parsable: data.parsableText},
+ {name: "PromProto", contentType: "application/vnd.google.protobuf", parsable: metricsProto},
+ } {
+ b.Run(fmt.Sprintf("fmt=%v", bcase.name), func(b *testing.B) {
+ benchScrapeLoopAppend(b, appV2, bcase.parsable, bcase.contentType, appendMetadataToWAL, false)
+ })
}
})
}
+ }
+ }
+}
+
+func benchScrapeLoopAppend(
+ b *testing.B,
+ appV2 bool,
+ parsable []byte,
+ contentType string,
+ appendMetadataToWAL bool,
+ enableExemplarStorage bool,
+) {
+ // Need a full storage for correct Add/AddFast semantics.
+ s := teststorage.New(b, func(opt *tsdb.Options) {
+ opt.EnableMetadataWALRecords = appendMetadataToWAL
+ if enableExemplarStorage {
+ opt.EnableExemplarStorage = true
+ opt.MaxExemplars = 1e5
+ }
+ })
+ b.Cleanup(func() { _ = s.Close() })
+
+ sl, _ := newTestScrapeLoop(b, withAppendable(s, appV2), func(sl *scrapeLoop) {
+ sl.appendMetadataToWAL = appendMetadataToWAL
+ })
+ app := sl.appender()
+ ts := time.Time{}
+
+ b.ReportAllocs()
+ b.ResetTimer()
+ for b.Loop() {
+ ts = ts.Add(time.Second)
+ _, _, _, err := app.append(parsable, contentType, ts)
+ if err != nil {
+ b.Fatal(err)
+ }
+ // Reset the appender so it doesn't grow indefinitely, and it mimics what prod scrape will do.
+ // We do rollback, because it's cheaper than Commit.
+ if err := app.Rollback(); err != nil {
+ b.Fatal(err)
+ }
+ app = sl.appender()
+ }
+}
+
+// BenchmarkScrapeLoopAppend_HistogramsWithExemplars benchmarks OM scrapes with histograms full of exemplars.
+//
+// For e2e TSDB impact, we enable the TSDB exemplar storage
+//
+// Recommended CLI invocation:
+/*
+ export bench=appendHistWithExemplars && go test ./scrape/... \
+ -run '^$' -bench '^BenchmarkScrapeLoopAppend_HistogramsWithExemplars' \
+ -benchtime 5s -count 6 -cpu 2 -timeout 999m \
+ | tee ${bench}.txt
+*/
+func BenchmarkScrapeLoopAppend_HistogramsWithExemplars(b *testing.B) {
+ for _, appV2 := range []bool{false, true} {
+ b.Run(fmt.Sprintf("appV2=%v", appV2), func(b *testing.B) {
+ parsable := makeTestHistogramsWithExemplars(100) // ~255.8 KB in OM text.
+ benchScrapeLoopAppend(b, appV2, parsable, "application/openmetrics-text", false, true)
})
}
}
func TestScrapeLoopScrapeAndReport(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopScrapeAndReport(t, appV2)
+ })
+}
+
+func testScrapeLoopScrapeAndReport(t *testing.T, appV2 bool) {
parsableText := readTextParseTestMetrics(t)
// On windows \r is added when reading, but parsers do not support this. Kill it.
parsableText = bytes.ReplaceAll(parsableText, []byte("\r"), nil)
appTest := teststorage.NewAppendable()
- sl, scraper := newTestScrapeLoop(t, func(sl *scrapeLoop) {
- sl.appendable = appTest
+ sl, scraper := newTestScrapeLoop(t, withAppendable(appTest, appV2), func(sl *scrapeLoop) {
sl.fallbackScrapeProtocol = "application/openmetrics-text"
})
scraper.scrapeFunc = func(_ context.Context, writer io.Writer) error {
@@ -1418,38 +1687,49 @@ func TestScrapeLoopScrapeAndReport(t *testing.T) {
// Recommended CLI invocation:
/*
export bench=scrapeAndReport && go test ./scrape/... \
- -run '^$' -bench '^BenchmarkScrapeLoopScrapeAndReport' \
+ -run '^$' -bench '^BenchmarkScrapeLoopScrapeAndReport$' \
-benchtime 5s -count 6 -cpu 2 -timeout 999m \
| tee ${bench}.txt
*/
func BenchmarkScrapeLoopScrapeAndReport(b *testing.B) {
- parsableText := readTextParseTestMetrics(b)
+ for _, appV2 := range []bool{false, true} {
+ b.Run(fmt.Sprintf("appV2=%v", appV2), func(b *testing.B) {
+ parsableText := readTextParseTestMetrics(b)
- s := teststorage.New(b)
- b.Cleanup(func() { _ = s.Close() })
+ s := teststorage.New(b)
+ b.Cleanup(func() { _ = s.Close() })
- sl, scraper := newTestScrapeLoop(b, func(sl *scrapeLoop) {
- sl.appendable = s
- sl.fallbackScrapeProtocol = "application/openmetrics-text"
- })
- scraper.scrapeFunc = func(_ context.Context, writer io.Writer) error {
- _, err := writer.Write(parsableText)
- return err
- }
+ sl, scraper := newTestScrapeLoop(b, withAppendable(s, appV2), func(sl *scrapeLoop) {
+ sl.fallbackScrapeProtocol = "application/openmetrics-text"
+ })
+ scraper.scrapeFunc = func(_ context.Context, writer io.Writer) error {
+ _, err := writer.Write(parsableText)
+ return err
+ }
- ts := time.Time{}
+ ts := time.Time{}
- b.ReportAllocs()
- b.ResetTimer()
- for b.Loop() {
- ts = ts.Add(time.Second)
- sl.scrapeAndReport(time.Time{}, ts, nil)
- require.NoError(b, scraper.lastError)
+ b.ReportAllocs()
+ b.ResetTimer()
+ for b.Loop() {
+ ts = ts.Add(time.Second)
+ sl.scrapeAndReport(time.Time{}, ts, nil)
+ require.NoError(b, scraper.lastError)
+ }
+ })
}
}
func TestSetOptionsHandlingStaleness(t *testing.T) {
- s := teststorage.New(t, 600000)
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testSetOptionsHandlingStaleness(t, appV2)
+ })
+}
+
+func testSetOptionsHandlingStaleness(t *testing.T, appV2 bool) {
+ s := teststorage.New(t, func(opt *tsdb.Options) {
+ opt.OutOfOrderTimeWindow = 600000
+ })
t.Cleanup(func() { _ = s.Close() })
signal := make(chan struct{}, 1)
@@ -1458,9 +1738,8 @@ func TestSetOptionsHandlingStaleness(t *testing.T) {
// Function to run the scrape loop
runScrapeLoop := func(ctx context.Context, t *testing.T, cue int, action func(*scrapeLoop)) {
- sl, scraper := newTestScrapeLoop(t, func(sl *scrapeLoop) {
+ sl, scraper := newTestScrapeLoop(t, withAppendable(s, appV2), func(sl *scrapeLoop) {
sl.ctx = ctx
- sl.appendable = s
})
numScrapes := 0
@@ -1524,17 +1803,22 @@ func TestSetOptionsHandlingStaleness(t *testing.T) {
c++
}
}
- require.Equal(t, 0, c, "invalid count of staleness markers after stopping the engine")
+ require.Zero(t, c, "invalid count of staleness markers after stopping the engine")
}
func TestScrapeLoopRunCreatesStaleMarkersOnFailedScrape(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopRunCreatesStaleMarkersOnFailedScrape(t, appV2)
+ })
+}
+
+func testScrapeLoopRunCreatesStaleMarkersOnFailedScrape(t *testing.T, appV2 bool) {
signal := make(chan struct{}, 1)
ctx, cancel := context.WithCancel(t.Context())
appTest := teststorage.NewAppendable()
- sl, scraper := newTestScrapeLoop(t, func(sl *scrapeLoop) {
+ sl, scraper := newTestScrapeLoop(t, withAppendable(appTest, appV2), func(sl *scrapeLoop) {
sl.ctx = ctx
- sl.appendable = appTest
// Since we're writing samples directly below we need to provide a protocol fallback.
sl.fallbackScrapeProtocol = "text/plain"
})
@@ -1576,13 +1860,18 @@ func TestScrapeLoopRunCreatesStaleMarkersOnFailedScrape(t *testing.T) {
}
func TestScrapeLoopRunCreatesStaleMarkersOnParseFailure(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopRunCreatesStaleMarkersOnParseFailure(t, appV2)
+ })
+}
+
+func testScrapeLoopRunCreatesStaleMarkersOnParseFailure(t *testing.T, appV2 bool) {
signal := make(chan struct{}, 1)
ctx, cancel := context.WithCancel(t.Context())
appTest := teststorage.NewAppendable()
- sl, scraper := newTestScrapeLoop(t, func(sl *scrapeLoop) {
+ sl, scraper := newTestScrapeLoop(t, withAppendable(appTest, appV2), func(sl *scrapeLoop) {
sl.ctx = ctx
- sl.appendable = appTest
// Since we're writing samples directly below we need to provide a protocol fallback.
sl.fallbackScrapeProtocol = "text/plain"
})
@@ -1630,13 +1919,18 @@ func TestScrapeLoopRunCreatesStaleMarkersOnParseFailure(t *testing.T) {
// If we have a target with sample_limit set and scrape initially works, but then we hit the sample_limit error,
// then we don't expect to see any StaleNaNs appended for the series that disappeared due to sample_limit error.
func TestScrapeLoopRunCreatesStaleMarkersOnSampleLimit(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopRunCreatesStaleMarkersOnSampleLimit(t, appV2)
+ })
+}
+
+func testScrapeLoopRunCreatesStaleMarkersOnSampleLimit(t *testing.T, appV2 bool) {
signal := make(chan struct{}, 1)
ctx, cancel := context.WithCancel(t.Context())
appTest := teststorage.NewAppendable()
- sl, scraper := newTestScrapeLoop(t, func(sl *scrapeLoop) {
+ sl, scraper := newTestScrapeLoop(t, withAppendable(appTest, appV2), func(sl *scrapeLoop) {
sl.ctx = ctx
- sl.appendable = appTest
// Since we're writing samples directly below we need to provide a protocol fallback.
sl.fallbackScrapeProtocol = "text/plain"
sl.sampleLimit = 4
@@ -1700,6 +1994,12 @@ func TestScrapeLoopRunCreatesStaleMarkersOnSampleLimit(t *testing.T) {
}
func TestScrapeLoopCache(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopCache(t, appV2)
+ })
+}
+
+func testScrapeLoopCache(t *testing.T, appV2 bool) {
s := teststorage.New(t)
t.Cleanup(func() { _ = s.Close() })
@@ -1707,10 +2007,9 @@ func TestScrapeLoopCache(t *testing.T) {
ctx, cancel := context.WithCancel(t.Context())
appTest := teststorage.NewAppendable().Then(s)
- sl, scraper := newTestScrapeLoop(t, func(sl *scrapeLoop) {
+ sl, scraper := newTestScrapeLoop(t, withAppendable(appTest, appV2), func(sl *scrapeLoop) {
sl.ctx = ctx
sl.l = promslog.New(&promslog.Config{})
- sl.appendable = appTest
// Since we're writing samples directly below we need to provide a protocol fallback.
sl.fallbackScrapeProtocol = "text/plain"
// Decreasing the scrape interval could make the test fail, as multiple scrapes might be initiated at identical millisecond timestamps.
@@ -1765,13 +2064,19 @@ func TestScrapeLoopCache(t *testing.T) {
}
func TestScrapeLoopCacheMemoryExhaustionProtection(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopCacheMemoryExhaustionProtection(t, appV2)
+ })
+}
+
+func testScrapeLoopCacheMemoryExhaustionProtection(t *testing.T, appV2 bool) {
s := teststorage.New(t)
t.Cleanup(func() { _ = s.Close() })
signal := make(chan struct{}, 1)
ctx, cancel := context.WithCancel(t.Context())
- sl, scraper := newTestScrapeLoop(t, func(sl *scrapeLoop) {
+ sl, scraper := newTestScrapeLoop(t, withAppendable(teststorage.NewAppendable().Then(s), appV2), func(sl *scrapeLoop) {
sl.ctx = ctx
})
numScrapes := 0
@@ -1803,138 +2108,124 @@ func TestScrapeLoopCacheMemoryExhaustionProtection(t *testing.T) {
require.LessOrEqual(t, len(sl.cache.series), 2000, "More than 2000 series cached.")
}
-func TestScrapeLoopAppend(t *testing.T) {
- tests := []struct {
- title string
- honorLabels bool
- scrapeLabels string
- discoveryLabels []string
- expLset labels.Labels
- expValue float64
- }{
- {
- // When "honor_labels" is not set
- // label name collision is handler by adding a prefix.
- title: "Label name collision",
- honorLabels: false,
- scrapeLabels: `metric{n="1"} 0`,
- discoveryLabels: []string{"n", "2"},
- expLset: labels.FromStrings("__name__", "metric", "exported_n", "1", "n", "2"),
- expValue: 0,
- }, {
- // When "honor_labels" is not set
- // exported label from discovery don't get overwritten
- title: "Label name collision",
- honorLabels: false,
- scrapeLabels: `metric 0`,
- discoveryLabels: []string{"n", "2", "exported_n", "2"},
- expLset: labels.FromStrings("__name__", "metric", "n", "2", "exported_n", "2"),
- expValue: 0,
- }, {
- // Labels with no value need to be removed as these should not be ingested.
- title: "Delete Empty labels",
- honorLabels: false,
- scrapeLabels: `metric{n=""} 0`,
- discoveryLabels: nil,
- expLset: labels.FromStrings("__name__", "metric"),
- expValue: 0,
- }, {
- // Honor Labels should ignore labels with the same name.
- title: "Honor Labels",
- honorLabels: true,
- scrapeLabels: `metric{n1="1", n2="2"} 0`,
- discoveryLabels: []string{"n1", "0"},
- expLset: labels.FromStrings("__name__", "metric", "n1", "1", "n2", "2"),
- expValue: 0,
- }, {
- title: "Stale - NaN",
- honorLabels: false,
- scrapeLabels: `metric NaN`,
- discoveryLabels: nil,
- expLset: labels.FromStrings("__name__", "metric"),
- expValue: math.Float64frombits(value.NormalNaN),
- },
- }
-
- for _, test := range tests {
- discoveryLabels := &Target{
- labels: labels.FromStrings(test.discoveryLabels...),
- }
-
- appTest := teststorage.NewAppendable()
- sl, _ := newTestScrapeLoop(t, func(sl *scrapeLoop) {
- sl.appendable = appTest
- sl.sampleMutator = func(l labels.Labels) labels.Labels {
- return mutateSampleLabels(l, discoveryLabels, test.honorLabels, nil)
- }
- sl.reportSampleMutator = func(l labels.Labels) labels.Labels {
- return mutateReportSampleLabels(l, discoveryLabels)
- }
- })
-
- now := time.Now()
-
- app := sl.appender()
- _, _, _, err := app.append([]byte(test.scrapeLabels), "text/plain", now)
- require.NoError(t, err)
- require.NoError(t, app.Commit())
-
- expected := []sample{
- {
- L: test.expLset,
- T: timestamp.FromTime(now),
- V: test.expValue,
- },
- }
-
- t.Logf("Test:%s", test.title)
- requireEqual(t, expected, appTest.ResultSamples())
- }
+func TestScrapeLoopAppend_HonorLabels(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopAppendHonorLabels(t, appV2)
+ })
}
-func requireEqual(t *testing.T, expected, actual any, msgAndArgs ...any) {
- t.Helper()
- testutil.RequireEqualWithOptions(t, expected, actual,
- []cmp.Option{
- cmp.Comparer(func(a, b sample) bool { return a.Equals(b) }),
- // StaleNaN samples are generated by iterating over a map, which means that the order
- // of samples might be different on every test run. Sort series by label to avoid
- // test failures because of that.
- cmpopts.SortSlices(func(a, b sample) int {
- return labels.Compare(a.L, b.L)
- }),
+func testScrapeLoopAppendHonorLabels(t *testing.T, appV2 bool) {
+ for _, test := range []struct {
+ title string
+ honorLabels bool
+ scrapeText string
+ discoveryLabels []string
+ expLset labels.Labels
+ }{
+ {
+ // On label collision, when "honor_labels" is not set, prefix is added.
+ title: "HonorLabels=false",
+ scrapeText: `metric{n="1"} 1`,
+ discoveryLabels: []string{"n", "2"},
+ expLset: labels.FromStrings("__name__", "metric", "exported_n", "1", "n", "2"),
},
- msgAndArgs...)
+ {
+ // Case where SD already has the prefixed label - it shouldn't be overridden.
+ title: "HonorLabels=false;exported prefix already exists in SD",
+ scrapeText: `metric{n="1"} 1`,
+ discoveryLabels: []string{"n", "2", "exported_n", "2"},
+ expLset: labels.FromStrings("__name__", "metric", "n", "2", "exported_n", "2", "exported_exported_n", "1"),
+ },
+ {
+ // Labels with no value need to be removed as these should not be ingested.
+ title: "HonorLabels=false;empty label",
+ scrapeText: `metric{n=""} 1`,
+ discoveryLabels: nil,
+ expLset: labels.FromStrings("__name__", "metric"),
+ },
+ {
+ // On label collision, when "honor_labels" is true, label is overridden.
+ title: "HonorLabels=true",
+ honorLabels: true,
+ scrapeText: `metric{n="1"} 1`,
+ discoveryLabels: []string{"n", "2"},
+ expLset: labels.FromStrings("__name__", "metric", "n", "1"),
+ },
+ } {
+ t.Run(test.title, func(t *testing.T) {
+ discoveryLabels := &Target{
+ labels: labels.FromStrings(test.discoveryLabels...),
+ }
+
+ appTest := teststorage.NewAppendable()
+ sl, _ := newTestScrapeLoop(t, withAppendable(appTest, appV2), func(sl *scrapeLoop) {
+ sl.sampleMutator = func(l labels.Labels) labels.Labels {
+ return mutateSampleLabels(l, discoveryLabels, test.honorLabels, nil)
+ }
+ sl.reportSampleMutator = func(l labels.Labels) labels.Labels {
+ return mutateReportSampleLabels(l, discoveryLabels)
+ }
+ })
+
+ now := time.Now()
+
+ app := sl.appender()
+ _, _, _, err := app.append([]byte(test.scrapeText), "text/plain", now)
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+
+ expected := []sample{
+ {
+ L: test.expLset,
+ T: timestamp.FromTime(now),
+ V: 1,
+ },
+ }
+ teststorage.RequireEqual(t, expected, appTest.ResultSamples())
+ })
+ }
}
func TestScrapeLoopAppendForConflictingPrefixedLabels(t *testing.T) {
- testcases := map[string]struct {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopAppendForConflictingPrefixedLabels(t, appV2)
+ })
+}
+
+func testScrapeLoopAppendForConflictingPrefixedLabels(t *testing.T, appV2 bool) {
+ for _, tc := range []struct {
+ name string
targetLabels []string
exposedLabels string
expected []string
}{
- "One target label collides with existing label": {
+ {
+ name: "One target label collides with existing label",
targetLabels: []string{"foo", "2"},
exposedLabels: `metric{foo="1"} 0`,
expected: []string{"__name__", "metric", "exported_foo", "1", "foo", "2"},
},
- "One target label collides with existing label, plus target label already with prefix 'exported'": {
+ {
+ name: "One target label collides with existing label, plus target label already with prefix 'exported'",
targetLabels: []string{"foo", "2", "exported_foo", "3"},
exposedLabels: `metric{foo="1"} 0`,
expected: []string{"__name__", "metric", "exported_exported_foo", "1", "exported_foo", "3", "foo", "2"},
},
- "One target label collides with existing label, plus existing label already with prefix 'exported": {
+ {
+ name: "One target label collides with existing label, plus existing label already with prefix 'exported",
targetLabels: []string{"foo", "3"},
exposedLabels: `metric{foo="1", exported_foo="2"} 0`,
expected: []string{"__name__", "metric", "exported_exported_foo", "1", "exported_foo", "2", "foo", "3"},
},
- "One target label collides with existing label, both already with prefix 'exported'": {
+ {
+ name: "One target label collides with existing label, both already with prefix 'exported'",
targetLabels: []string{"exported_foo", "2"},
exposedLabels: `metric{exported_foo="1"} 0`,
expected: []string{"__name__", "metric", "exported_exported_foo", "1", "exported_foo", "2"},
},
- "Two target labels collide with existing labels, both with and without prefix 'exported'": {
+ {
+ name: "Two target labels collide with existing labels, both with and without prefix 'exported'",
targetLabels: []string{"foo", "3", "exported_foo", "4"},
exposedLabels: `metric{foo="1", exported_foo="2"} 0`,
expected: []string{
@@ -1942,7 +2233,8 @@ func TestScrapeLoopAppendForConflictingPrefixedLabels(t *testing.T) {
"2", "exported_foo", "4", "foo", "3",
},
},
- "Extreme example": {
+ {
+ name: "Extreme example",
targetLabels: []string{"foo", "0", "exported_exported_foo", "1", "exported_exported_exported_foo", "2"},
exposedLabels: `metric{foo="3", exported_foo="4", exported_exported_exported_foo="5"} 0`,
expected: []string{
@@ -1955,13 +2247,10 @@ func TestScrapeLoopAppendForConflictingPrefixedLabels(t *testing.T) {
"foo", "0",
},
},
- }
-
- for name, tc := range testcases {
- t.Run(name, func(t *testing.T) {
+ } {
+ t.Run(tc.name, func(t *testing.T) {
appTest := teststorage.NewAppendable()
- sl, _ := newTestScrapeLoop(t, func(sl *scrapeLoop) {
- sl.appendable = appTest
+ sl, _ := newTestScrapeLoop(t, withAppendable(appTest, appV2), func(sl *scrapeLoop) {
sl.sampleMutator = func(l labels.Labels) labels.Labels {
return mutateSampleLabels(l, &Target{labels: labels.FromStrings(tc.targetLabels...)}, false, nil)
}
@@ -1973,7 +2262,7 @@ func TestScrapeLoopAppendForConflictingPrefixedLabels(t *testing.T) {
require.NoError(t, app.Commit())
- requireEqual(t, []sample{
+ teststorage.RequireEqual(t, []sample{
{
L: labels.FromStrings(tc.expected...),
T: timestamp.FromTime(time.Date(2000, 1, 1, 1, 0, 0, 0, time.UTC)),
@@ -1985,8 +2274,14 @@ func TestScrapeLoopAppendForConflictingPrefixedLabels(t *testing.T) {
}
func TestScrapeLoopAppendCacheEntryButErrNotFound(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopAppendCacheEntryButErrNotFound(t, appV2)
+ })
+}
+
+func testScrapeLoopAppendCacheEntryButErrNotFound(t *testing.T, appV2 bool) {
appTest := teststorage.NewAppendable()
- sl, _ := newTestScrapeLoop(t, withAppendable(appTest))
+ sl, _ := newTestScrapeLoop(t, withAppendable(appTest, appV2))
fakeRef := storage.SeriesRef(1)
expValue := float64(1)
@@ -2017,8 +2312,7 @@ func TestScrapeLoopAppendCacheEntryButErrNotFound(t *testing.T) {
V: expValue,
},
}
-
- require.Equal(t, expected, appTest.ResultSamples())
+ teststorage.RequireEqual(t, expected, appTest.ResultSamples())
}
type appendableFunc func(ctx context.Context) storage.Appender
@@ -2026,12 +2320,26 @@ type appendableFunc func(ctx context.Context) storage.Appender
func (a appendableFunc) Appender(ctx context.Context) storage.Appender { return a(ctx) }
func TestScrapeLoopAppendSampleLimit(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopAppendSampleLimit(t, appV2)
+ })
+}
+
+func testScrapeLoopAppendSampleLimit(t *testing.T, appV2 bool) {
appTest := teststorage.NewAppendable()
sl, _ := newTestScrapeLoop(t, func(sl *scrapeLoop) {
- sl.appendable = appendableFunc(func(ctx context.Context) storage.Appender {
- // Chain appTest to verify what samples passed through.
- return &limitAppender{Appender: appTest.Appender(ctx), limit: 1}
- })
+ if appV2 {
+ sl.appendableV2 = appendableV2Func(func(ctx context.Context) storage.AppenderV2 {
+ // Chain appTest to verify what samples passed through.
+ return &limitAppenderV2{AppenderV2: appTest.AppenderV2(ctx), limit: 1}
+ })
+ } else {
+ sl.appendable = appendableFunc(func(ctx context.Context) storage.Appender {
+ // Chain appTest to verify what samples passed through.
+ return &limitAppender{Appender: appTest.Appender(ctx), limit: 1}
+ })
+ }
+
sl.sampleMutator = func(l labels.Labels) labels.Labels {
if l.Has("deleteme") {
return labels.EmptyLabels()
@@ -2075,7 +2383,7 @@ func TestScrapeLoopAppendSampleLimit(t *testing.T) {
V: 1,
},
}
- requireEqual(t, want, appTest.RolledbackSamples(), "Appended samples not as expected:\n%s", appTest)
+ teststorage.RequireEqual(t, want, appTest.RolledbackSamples(), "Appended samples not as expected:\n%s", appTest)
now = time.Now()
app = sl.appender()
@@ -2088,10 +2396,23 @@ func TestScrapeLoopAppendSampleLimit(t *testing.T) {
}
func TestScrapeLoop_HistogramBucketLimit(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopHistogramBucketLimit(t, appV2)
+ })
+}
+
+func testScrapeLoopHistogramBucketLimit(t *testing.T, appV2 bool) {
sl, _ := newTestScrapeLoop(t, func(sl *scrapeLoop) {
- sl.appendable = appendableFunc(func(ctx context.Context) storage.Appender {
- return &bucketLimitAppender{Appender: teststorage.NewAppendable().Appender(ctx), limit: 2}
- })
+ if appV2 {
+ sl.appendableV2 = appendableV2Func(func(ctx context.Context) storage.AppenderV2 {
+ return &bucketLimitAppenderV2{AppenderV2: teststorage.NewAppendable().AppenderV2(ctx), limit: 2}
+ })
+ } else {
+ sl.appendable = appendableFunc(func(ctx context.Context) storage.Appender {
+ return &bucketLimitAppender{Appender: teststorage.NewAppendable().Appender(ctx), limit: 2}
+ })
+ }
+
sl.enableNativeHistogramScraping = true
sl.sampleMutator = func(l labels.Labels) labels.Labels {
if l.Has("deleteme") {
@@ -2197,11 +2518,17 @@ func TestScrapeLoop_HistogramBucketLimit(t *testing.T) {
}
func TestScrapeLoop_ChangingMetricString(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopChangingMetricString(t, appV2)
+ })
+}
+
+func testScrapeLoopChangingMetricString(t *testing.T, appV2 bool) {
// This is a regression test for the scrape loop cache not properly maintaining
// IDs when the string representation of a metric changes across a scrape. Thus,
// we use a real storage appender here.
appTest := teststorage.NewAppendable()
- sl, _ := newTestScrapeLoop(t, withAppendable(appTest))
+ sl, _ := newTestScrapeLoop(t, withAppendable(appTest, appV2))
now := time.Now()
app := sl.appender()
@@ -2226,11 +2553,17 @@ func TestScrapeLoop_ChangingMetricString(t *testing.T) {
V: 2,
},
}
- require.Equal(t, want, appTest.ResultSamples(), "Appended samples not as expected:\n%s", appTest)
+ teststorage.RequireEqual(t, want, appTest.ResultSamples(), "Appended samples not as expected:\n%s", appTest)
}
func TestScrapeLoopAppendFailsWithNoContentType(t *testing.T) {
- sl, _ := newTestScrapeLoop(t, func(sl *scrapeLoop) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopAppendFailsWithNoContentType(t, appV2)
+ })
+}
+
+func testScrapeLoopAppendFailsWithNoContentType(t *testing.T, appV2 bool) {
+ sl, _ := newTestScrapeLoop(t, withAppendable(teststorage.NewAppendable(), appV2), func(sl *scrapeLoop) {
// Explicitly setting the lack of fallback protocol here to make it obvious.
sl.fallbackScrapeProtocol = ""
})
@@ -2244,7 +2577,13 @@ func TestScrapeLoopAppendFailsWithNoContentType(t *testing.T) {
// TestScrapeLoopAppendEmptyWithNoContentType ensures we there are no errors when we get a blank scrape or just want to append a stale marker.
func TestScrapeLoopAppendEmptyWithNoContentType(t *testing.T) {
- sl, _ := newTestScrapeLoop(t, func(sl *scrapeLoop) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopAppendEmptyWithNoContentType(t, appV2)
+ })
+}
+
+func testScrapeLoopAppendEmptyWithNoContentType(t *testing.T, appV2 bool) {
+ sl, _ := newTestScrapeLoop(t, withAppendable(teststorage.NewAppendable(), appV2), func(sl *scrapeLoop) {
// Explicitly setting the lack of fallback protocol here to make it obvious.
sl.fallbackScrapeProtocol = ""
})
@@ -2257,8 +2596,14 @@ func TestScrapeLoopAppendEmptyWithNoContentType(t *testing.T) {
}
func TestScrapeLoopAppendStaleness(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopAppendStaleness(t, appV2)
+ })
+}
+
+func testScrapeLoopAppendStaleness(t *testing.T, appV2 bool) {
appTest := teststorage.NewAppendable()
- sl, _ := newTestScrapeLoop(t, withAppendable(appTest))
+ sl, _ := newTestScrapeLoop(t, withAppendable(appTest, appV2))
now := time.Now()
app := sl.appender()
@@ -2283,12 +2628,18 @@ func TestScrapeLoopAppendStaleness(t *testing.T) {
V: math.Float64frombits(value.StaleNaN),
},
}
- requireEqual(t, want, appTest.ResultSamples(), "Appended samples not as expected:\n%s", appTest)
+ teststorage.RequireEqual(t, want, appTest.ResultSamples(), "Appended samples not as expected:\n%s", appTest)
}
func TestScrapeLoopAppendNoStalenessIfTimestamp(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopAppendNoStalenessIfTimestamp(t, appV2)
+ })
+}
+
+func testScrapeLoopAppendNoStalenessIfTimestamp(t *testing.T, appV2 bool) {
appTest := teststorage.NewAppendable()
- sl, _ := newTestScrapeLoop(t, withAppendable(appTest))
+ sl, _ := newTestScrapeLoop(t, withAppendable(appTest, appV2))
now := time.Now()
app := sl.appender()
_, _, _, err := app.append([]byte("metric_a 1 1000\n"), "text/plain", now)
@@ -2307,13 +2658,18 @@ func TestScrapeLoopAppendNoStalenessIfTimestamp(t *testing.T) {
V: 1,
},
}
- require.Equal(t, want, appTest.ResultSamples(), "Appended samples not as expected:\n%s", appTest)
+ teststorage.RequireEqual(t, want, appTest.ResultSamples(), "Appended samples not as expected:\n%s", appTest)
}
func TestScrapeLoopAppendStalenessIfTrackTimestampStaleness(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopAppendStalenessIfTrackTimestampStaleness(t, appV2)
+ })
+}
+
+func testScrapeLoopAppendStalenessIfTrackTimestampStaleness(t *testing.T, appV2 bool) {
appTest := teststorage.NewAppendable()
- sl, _ := newTestScrapeLoop(t, func(sl *scrapeLoop) {
- sl.appendable = appTest
+ sl, _ := newTestScrapeLoop(t, withAppendable(appTest, appV2), func(sl *scrapeLoop) {
sl.trackTimestampsStaleness = true
})
@@ -2340,11 +2696,18 @@ func TestScrapeLoopAppendStalenessIfTrackTimestampStaleness(t *testing.T) {
V: math.Float64frombits(value.StaleNaN),
},
}
- requireEqual(t, want, appTest.ResultSamples(), "Appended samples not as expected:\n%s", appTest)
+ teststorage.RequireEqual(t, want, appTest.ResultSamples(), "Appended samples not as expected:\n%s", appTest)
}
-func TestScrapeLoopAppendExemplar(t *testing.T) {
- tests := []struct {
+// TestScrapeLoopAppend is the main table test testing the scrape appends, including histograms, exemplar and metadata.
+func TestScrapeLoopAppend(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopAppend(t, appV2)
+ })
+}
+
+func testScrapeLoopAppend(t *testing.T, appV2 bool) {
+ for _, test := range []struct {
title string
alwaysScrapeClassicHist bool
enableNativeHistogramsIngestion bool
@@ -2353,6 +2716,15 @@ func TestScrapeLoopAppendExemplar(t *testing.T) {
discoveryLabels []string
samples []sample
}{
+ {
+ title: "Normal NaN scraped",
+ scrapeText: "metric_total{n=\"1\"} NaN\n# EOF",
+ contentType: "application/openmetrics-text",
+ samples: []sample{{
+ L: labels.FromStrings("__name__", "metric_total", "n", "1"),
+ V: math.Float64frombits(value.NormalNaN),
+ }},
+ },
{
title: "Metric without exemplars",
scrapeText: "metric_total{n=\"1\"} 0\n# EOF",
@@ -2612,22 +2984,6 @@ metric: <
alwaysScrapeClassicHist: true,
contentType: "application/vnd.google.protobuf",
samples: []sample{
- {L: labels.FromStrings("__name__", "test_histogram_count"), T: 1234568, V: 175},
- {L: labels.FromStrings("__name__", "test_histogram_sum"), T: 1234568, V: 0.0008280461746287094},
- {L: labels.FromStrings("__name__", "test_histogram_bucket", "le", "-0.0004899999999999998"), T: 1234568, V: 2},
- {
- L: labels.FromStrings("__name__", "test_histogram_bucket", "le", "-0.0003899999999999998"), T: 1234568, V: 4,
- ES: []exemplar.Exemplar{{Labels: labels.FromStrings("dummyID", "59727"), Value: -0.00039, Ts: 1625851155146, HasTs: true}},
- },
- {
- L: labels.FromStrings("__name__", "test_histogram_bucket", "le", "-0.0002899999999999998"), T: 1234568, V: 16,
- ES: []exemplar.Exemplar{{Labels: labels.FromStrings("dummyID", "5617"), Value: -0.00029, Ts: 1234568, HasTs: false}},
- },
- {
- L: labels.FromStrings("__name__", "test_histogram_bucket", "le", "-0.0001899999999999998"), T: 1234568, V: 32,
- ES: []exemplar.Exemplar{{Labels: labels.FromStrings("dummyID", "58215"), Value: -0.00019, Ts: 1625851055146, HasTs: true}},
- },
- {L: labels.FromStrings("__name__", "test_histogram_bucket", "le", "+Inf"), T: 1234568, V: 175},
{
T: 1234568,
L: labels.FromStrings("__name__", "test_histogram"),
@@ -2655,6 +3011,22 @@ metric: <
{Labels: labels.FromStrings("dummyID", "59727"), Value: -0.00039, Ts: 1625851155146, HasTs: true},
},
},
+ {L: labels.FromStrings("__name__", "test_histogram_count"), T: 1234568, V: 175},
+ {L: labels.FromStrings("__name__", "test_histogram_sum"), T: 1234568, V: 0.0008280461746287094},
+ {L: labels.FromStrings("__name__", "test_histogram_bucket", "le", "-0.0004899999999999998"), T: 1234568, V: 2},
+ {
+ L: labels.FromStrings("__name__", "test_histogram_bucket", "le", "-0.0003899999999999998"), T: 1234568, V: 4,
+ ES: []exemplar.Exemplar{{Labels: labels.FromStrings("dummyID", "59727"), Value: -0.00039, Ts: 1625851155146, HasTs: true}},
+ },
+ {
+ L: labels.FromStrings("__name__", "test_histogram_bucket", "le", "-0.0002899999999999998"), T: 1234568, V: 16,
+ ES: []exemplar.Exemplar{{Labels: labels.FromStrings("dummyID", "5617"), Value: -0.00029, Ts: 1234568, HasTs: false}},
+ },
+ {
+ L: labels.FromStrings("__name__", "test_histogram_bucket", "le", "-0.0001899999999999998"), T: 1234568, V: 32,
+ ES: []exemplar.Exemplar{{Labels: labels.FromStrings("dummyID", "58215"), Value: -0.00019, Ts: 1625851055146, HasTs: true}},
+ },
+ {L: labels.FromStrings("__name__", "test_histogram_bucket", "le", "+Inf"), T: 1234568, V: 175},
},
},
{
@@ -2836,17 +3208,14 @@ metric: <
{L: labels.FromStrings("__name__", "test_histogram_bucket", "le", "+Inf"), T: 1234568, V: 175},
},
},
- }
-
- for _, test := range tests {
+ } {
t.Run(test.title, func(t *testing.T) {
discoveryLabels := &Target{
labels: labels.FromStrings(test.discoveryLabels...),
}
appTest := teststorage.NewAppendable()
- sl, _ := newTestScrapeLoop(t, func(sl *scrapeLoop) {
- sl.appendable = appTest
+ sl, _ := newTestScrapeLoop(t, withAppendable(appTest, appV2), func(sl *scrapeLoop) {
sl.enableNativeHistogramScraping = test.enableNativeHistogramsIngestion
sl.sampleMutator = func(l labels.Labels) labels.Labels {
return mutateSampleLabels(l, discoveryLabels, false, nil)
@@ -2855,15 +3224,23 @@ metric: <
return mutateReportSampleLabels(l, discoveryLabels)
}
sl.alwaysScrapeClassicHist = test.alwaysScrapeClassicHist
- // This test does not care about metadata. Having this true would mean we need to add metadata to sample
+ // This test does not care about metadata.
+ // Having this true would mean we need to add metadata to sample
// expectations.
+ // TODO(bwplotka): Add cases for append metadata to WAL and pass metadata
sl.appendMetadataToWAL = false
})
app := sl.appender()
now := time.Now()
+ // Process expected samples.
for i := range test.samples {
+ if !appV2 && test.samples[i].MF != "" {
+ // AppenderV1 does not support metric family passing.
+ test.samples[i].MF = ""
+ }
+
if test.samples[i].T != 0 {
continue
}
@@ -2887,7 +3264,7 @@ metric: <
_, _, _, err := app.append(buf.Bytes(), test.contentType, now)
require.NoError(t, err)
require.NoError(t, app.Commit())
- requireEqual(t, test.samples, appTest.ResultSamples())
+ teststorage.RequireEqual(t, test.samples, appTest.ResultSamples())
})
}
}
@@ -2913,6 +3290,12 @@ func textToProto(text string, buf *bytes.Buffer) error {
}
func TestScrapeLoopAppendExemplarSeries(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopAppendExemplarSeries(t, appV2)
+ })
+}
+
+func testScrapeLoopAppendExemplarSeries(t *testing.T, appV2 bool) {
scrapeText := []string{`metric_total{n="1"} 1 # {t="1"} 1.0 10000
# EOF`, `metric_total{n="1"} 2 # {t="2"} 2.0 20000
# EOF`}
@@ -2934,8 +3317,7 @@ func TestScrapeLoopAppendExemplarSeries(t *testing.T) {
}
appTest := teststorage.NewAppendable()
- sl, _ := newTestScrapeLoop(t, func(sl *scrapeLoop) {
- sl.appendable = appTest
+ sl, _ := newTestScrapeLoop(t, withAppendable(appTest, appV2), func(sl *scrapeLoop) {
sl.sampleMutator = func(l labels.Labels) labels.Labels {
return mutateSampleLabels(l, discoveryLabels, false, nil)
}
@@ -2960,15 +3342,20 @@ func TestScrapeLoopAppendExemplarSeries(t *testing.T) {
require.NoError(t, app.Commit())
}
- requireEqual(t, samples, appTest.ResultSamples())
+ teststorage.RequireEqual(t, samples, appTest.ResultSamples())
}
func TestScrapeLoopRunReportsTargetDownOnScrapeError(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopRunReportsTargetDownOnScrapeError(t, appV2)
+ })
+}
+
+func testScrapeLoopRunReportsTargetDownOnScrapeError(t *testing.T, appV2 bool) {
ctx, cancel := context.WithCancel(t.Context())
appTest := teststorage.NewAppendable()
- sl, scraper := newTestScrapeLoop(t, func(sl *scrapeLoop) {
+ sl, scraper := newTestScrapeLoop(t, withAppendable(appTest, appV2), func(sl *scrapeLoop) {
sl.ctx = ctx
- sl.appendable = appTest
})
scraper.scrapeFunc = func(context.Context, io.Writer) error {
cancel()
@@ -2980,11 +3367,16 @@ func TestScrapeLoopRunReportsTargetDownOnScrapeError(t *testing.T) {
}
func TestScrapeLoopRunReportsTargetDownOnInvalidUTF8(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopRunReportsTargetDownOnInvalidUTF8(t, appV2)
+ })
+}
+
+func testScrapeLoopRunReportsTargetDownOnInvalidUTF8(t *testing.T, appV2 bool) {
ctx, cancel := context.WithCancel(t.Context())
appTest := teststorage.NewAppendable()
- sl, scraper := newTestScrapeLoop(t, func(sl *scrapeLoop) {
+ sl, scraper := newTestScrapeLoop(t, withAppendable(appTest, appV2), func(sl *scrapeLoop) {
sl.ctx = ctx
- sl.appendable = appTest
})
scraper.scrapeFunc = func(_ context.Context, w io.Writer) error {
cancel()
@@ -2997,6 +3389,12 @@ func TestScrapeLoopRunReportsTargetDownOnInvalidUTF8(t *testing.T) {
}
func TestScrapeLoopAppendGracefullyIfAmendOrOutOfOrderOrOutOfBounds(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopAppendGracefullyIfAmendOrOutOfOrderOrOutOfBounds(t, appV2)
+ })
+}
+
+func testScrapeLoopAppendGracefullyIfAmendOrOutOfOrderOrOutOfBounds(t *testing.T, appV2 bool) {
appTest := teststorage.NewAppendable().WithErrs(
func(ls labels.Labels) error {
switch ls.Get(model.MetricNameLabel) {
@@ -3010,7 +3408,7 @@ func TestScrapeLoopAppendGracefullyIfAmendOrOutOfOrderOrOutOfBounds(t *testing.T
return nil
}
}, nil, nil)
- sl, _ := newTestScrapeLoop(t, withAppendable(appTest))
+ sl, _ := newTestScrapeLoop(t, withAppendable(appTest, appV2))
now := time.Unix(1, 0)
app := sl.appender()
@@ -3025,21 +3423,36 @@ func TestScrapeLoopAppendGracefullyIfAmendOrOutOfOrderOrOutOfBounds(t *testing.T
V: 1,
},
}
- requireEqual(t, want, appTest.ResultSamples(), "Appended samples not as expected:\n%s", appTest)
+ teststorage.RequireEqual(t, want, appTest.ResultSamples(), "Appended samples not as expected:\n%s", appTest)
require.Equal(t, 4, total)
require.Equal(t, 4, added)
require.Equal(t, 1, seriesAdded)
}
func TestScrapeLoopOutOfBoundsTimeError(t *testing.T) {
- sl, _ := newTestScrapeLoop(t, withAppendable(
- appendableFunc(func(ctx context.Context) storage.Appender {
- return &timeLimitAppender{
- Appender: teststorage.NewAppendable().Appender(ctx),
- maxTime: timestamp.FromTime(time.Now().Add(10 * time.Minute)),
- }
- }),
- ))
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopOutOfBoundsTimeError(t, appV2)
+ })
+}
+
+func testScrapeLoopOutOfBoundsTimeError(t *testing.T, appV2 bool) {
+ sl, _ := newTestScrapeLoop(t, func(sl *scrapeLoop) {
+ if appV2 {
+ sl.appendableV2 = appendableV2Func(func(ctx context.Context) storage.AppenderV2 {
+ return &timeLimitAppenderV2{
+ AppenderV2: teststorage.NewAppendable().AppenderV2(ctx),
+ maxTime: timestamp.FromTime(time.Now().Add(10 * time.Minute)),
+ }
+ })
+ } else {
+ sl.appendable = appendableFunc(func(ctx context.Context) storage.Appender {
+ return &timeLimitAppender{
+ Appender: teststorage.NewAppendable().Appender(ctx),
+ maxTime: timestamp.FromTime(time.Now().Add(10 * time.Minute)),
+ }
+ })
+ }
+ })
now := time.Now().Add(20 * time.Minute)
app := sl.appender()
@@ -3461,11 +3874,17 @@ func (ts *testScraper) readResponse(ctx context.Context, _ *http.Response, w io.
}
func TestScrapeLoop_RespectTimestamps(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopRespectTimestamps(t, appV2)
+ })
+}
+
+func testScrapeLoopRespectTimestamps(t *testing.T, appV2 bool) {
s := teststorage.New(t)
t.Cleanup(func() { _ = s.Close() })
appTest := teststorage.NewAppendable().Then(s)
- sl, _ := newTestScrapeLoop(t, withAppendable(appTest))
+ sl, _ := newTestScrapeLoop(t, withAppendable(appTest, appV2))
now := time.Now()
app := sl.appender()
@@ -3480,16 +3899,21 @@ func TestScrapeLoop_RespectTimestamps(t *testing.T) {
V: 1,
},
}
- require.Equal(t, want, appTest.ResultSamples(), "Appended samples not as expected:\n%s", appTest)
+ teststorage.RequireEqual(t, want, appTest.ResultSamples(), "Appended samples not as expected:\n%s", appTest)
}
func TestScrapeLoop_DiscardTimestamps(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopDiscardTimestamps(t, appV2)
+ })
+}
+
+func testScrapeLoopDiscardTimestamps(t *testing.T, appV2 bool) {
s := teststorage.New(t)
t.Cleanup(func() { _ = s.Close() })
appTest := teststorage.NewAppendable().Then(s)
- sl, _ := newTestScrapeLoop(t, func(sl *scrapeLoop) {
- sl.appendable = appTest
+ sl, _ := newTestScrapeLoop(t, withAppendable(appTest, appV2), func(sl *scrapeLoop) {
sl.honorTimestamps = false
})
@@ -3506,15 +3930,21 @@ func TestScrapeLoop_DiscardTimestamps(t *testing.T) {
V: 1,
},
}
- require.Equal(t, want, appTest.ResultSamples(), "Appended samples not as expected:\n%s", appTest)
+ teststorage.RequireEqual(t, want, appTest.ResultSamples(), "Appended samples not as expected:\n%s", appTest)
}
func TestScrapeLoopDiscardDuplicateLabels(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopDiscardDuplicateLabels(t, appV2)
+ })
+}
+
+func testScrapeLoopDiscardDuplicateLabels(t *testing.T, appV2 bool) {
s := teststorage.New(t)
t.Cleanup(func() { _ = s.Close() })
appTest := teststorage.NewAppendable().Then(s)
- sl, _ := newTestScrapeLoop(t, withAppendable(appTest))
+ sl, _ := newTestScrapeLoop(t, withAppendable(appTest, appV2))
// We add a good and a bad metric to check that both are discarded.
app := sl.appender()
@@ -3546,12 +3976,17 @@ func TestScrapeLoopDiscardDuplicateLabels(t *testing.T) {
}
func TestScrapeLoopDiscardUnnamedMetrics(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopDiscardUnnamedMetrics(t, appV2)
+ })
+}
+
+func testScrapeLoopDiscardUnnamedMetrics(t *testing.T, appV2 bool) {
s := teststorage.New(t)
t.Cleanup(func() { _ = s.Close() })
appTest := teststorage.NewAppendable().Then(s)
- sl, _ := newTestScrapeLoop(t, func(sl *scrapeLoop) {
- sl.appendable = appTest
+ sl, _ := newTestScrapeLoop(t, withAppendable(appTest, appV2), func(sl *scrapeLoop) {
sl.sampleMutator = func(l labels.Labels) labels.Labels {
if l.Has("drop") {
return labels.FromStrings("no", "name") // This label set will trigger an error.
@@ -3641,6 +4076,12 @@ func TestReusableConfig(t *testing.T) {
}
func TestReuseScrapeCache(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testReuseScrapeCache(t, appV2)
+ })
+}
+
+func testReuseScrapeCache(t *testing.T, appV2 bool) {
var (
app = teststorage.NewAppendable()
cfg = &config.ScrapeConfig{
@@ -3651,7 +4092,8 @@ func TestReuseScrapeCache(t *testing.T) {
MetricNameValidationScheme: model.UTF8Validation,
MetricNameEscapingScheme: model.AllowUTF8,
}
- sp, _ = newScrapePool(cfg, app, 0, nil, nil, &Options{}, newTestScrapeMetrics(t))
+ sa = selectAppendable(app, appV2)
+ sp, _ = newScrapePool(cfg, sa.V1(), sa.V2(), 0, nil, nil, &Options{}, newTestScrapeMetrics(t))
t1 = &Target{
labels: labels.FromStrings("labelNew", "nameNew", "labelNew1", "nameNew1", "labelNew2", "nameNew2"),
scrapeConfig: &config.ScrapeConfig{
@@ -3825,10 +4267,16 @@ func TestReuseScrapeCache(t *testing.T) {
}
func TestScrapeAddFast(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeAddFast(t, appV2)
+ })
+}
+
+func testScrapeAddFast(t *testing.T, appV2 bool) {
s := teststorage.New(t)
t.Cleanup(func() { _ = s.Close() })
- sl, _ := newTestScrapeLoop(t, withAppendable(s))
+ sl, _ := newTestScrapeLoop(t, withAppendable(s, appV2))
app := sl.appender()
_, _, _, err := app.append([]byte("up 1\n"), "text/plain", time.Time{})
@@ -3848,6 +4296,12 @@ func TestScrapeAddFast(t *testing.T) {
}
func TestReuseCacheRace(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testReuseCacheRace(t, appV2)
+ })
+}
+
+func testReuseCacheRace(t *testing.T, appV2 bool) {
var (
cfg = &config.ScrapeConfig{
JobName: "Prometheus",
@@ -3858,7 +4312,8 @@ func TestReuseCacheRace(t *testing.T) {
MetricNameEscapingScheme: model.AllowUTF8,
}
buffers = pool.New(1e3, 100e6, 3, func(sz int) any { return make([]byte, 0, sz) })
- sp, _ = newScrapePool(cfg, teststorage.NewAppendable(), 0, nil, buffers, &Options{}, newTestScrapeMetrics(t))
+ sa = selectAppendable(teststorage.NewAppendable(), appV2)
+ sp, _ = newScrapePool(cfg, sa.V1(), sa.V2(), 0, nil, buffers, &Options{}, newTestScrapeMetrics(t))
t1 = &Target{
labels: labels.FromStrings("labelNew", "nameNew"),
scrapeConfig: &config.ScrapeConfig{},
@@ -3888,13 +4343,18 @@ func TestCheckAddError(t *testing.T) {
var appErrs appendErrors
sl, _ := newTestScrapeLoop(t)
// TODO: Check err etc
- _, _ = sl.checkAddError(nil, storage.ErrOutOfOrderSample, nil, nil, &appErrs)
+ _, _ = sl.checkAddError(nil, nil, storage.ErrOutOfOrderSample, nil, nil, &appErrs)
require.Equal(t, 1, appErrs.numOutOfOrder)
-
// TODO(bwplotka): Test partial error check and other cases
}
func TestScrapeReportSingleAppender(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeReportSingleAppender(t, appV2)
+ })
+}
+
+func testScrapeReportSingleAppender(t *testing.T, appV2 bool) {
t.Parallel()
s := teststorage.New(t)
t.Cleanup(func() { _ = s.Close() })
@@ -3902,9 +4362,8 @@ func TestScrapeReportSingleAppender(t *testing.T) {
signal := make(chan struct{}, 1)
ctx, cancel := context.WithCancel(t.Context())
- sl, scraper := newTestScrapeLoop(t, func(sl *scrapeLoop) {
+ sl, scraper := newTestScrapeLoop(t, withAppendable(s, appV2), func(sl *scrapeLoop) {
sl.ctx = ctx
- sl.appendable = s
// Since we're writing samples directly below we need to provide a protocol fallback.
sl.fallbackScrapeProtocol = "text/plain"
})
@@ -3951,6 +4410,12 @@ func TestScrapeReportSingleAppender(t *testing.T) {
}
func TestScrapeReportLimit(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeReportLimit(t, appV2)
+ })
+}
+
+func testScrapeReportLimit(t *testing.T, appV2 bool) {
s := teststorage.New(t)
t.Cleanup(func() { _ = s.Close() })
@@ -3967,7 +4432,8 @@ func TestScrapeReportLimit(t *testing.T) {
ts, scrapedTwice := newScrapableServer("metric_a 44\nmetric_b 44\nmetric_c 44\nmetric_d 44\n")
defer ts.Close()
- sp, err := newScrapePool(cfg, s, 0, nil, nil, &Options{}, newTestScrapeMetrics(t))
+ sa := selectAppendable(s, appV2)
+ sp, err := newScrapePool(cfg, sa.V1(), sa.V2(), 0, nil, nil, &Options{}, newTestScrapeMetrics(t))
require.NoError(t, err)
defer sp.stop()
@@ -4007,6 +4473,12 @@ func TestScrapeReportLimit(t *testing.T) {
}
func TestScrapeUTF8(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeUTF8(t, appV2)
+ })
+}
+
+func testScrapeUTF8(t *testing.T, appV2 bool) {
s := teststorage.New(t)
t.Cleanup(func() { _ = s.Close() })
@@ -4021,7 +4493,8 @@ func TestScrapeUTF8(t *testing.T) {
ts, scrapedTwice := newScrapableServer("{\"with.dots\"} 42\n")
defer ts.Close()
- sp, err := newScrapePool(cfg, s, 0, nil, nil, &Options{}, newTestScrapeMetrics(t))
+ sa := selectAppendable(s, appV2)
+ sp, err := newScrapePool(cfg, sa.V1(), sa.V2(), 0, nil, nil, &Options{}, newTestScrapeMetrics(t))
require.NoError(t, err)
defer sp.stop()
@@ -4051,7 +4524,13 @@ func TestScrapeUTF8(t *testing.T) {
}
func TestScrapeLoopLabelLimit(t *testing.T) {
- tests := []struct {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopLabelLimit(t, appV2)
+ })
+}
+
+func testScrapeLoopLabelLimit(t *testing.T, appV2 bool) {
+ for _, test := range []struct {
title string
scrapeLabels string
discoveryLabels []string
@@ -4113,14 +4592,12 @@ func TestScrapeLoopLabelLimit(t *testing.T) {
labelLimits: labelLimits{labelValueLengthLimit: 10},
expectErr: true,
},
- }
-
- for _, test := range tests {
+ } {
discoveryLabels := &Target{
labels: labels.FromStrings(test.discoveryLabels...),
}
- sl, _ := newTestScrapeLoop(t, func(sl *scrapeLoop) {
+ sl, _ := newTestScrapeLoop(t, withAppendable(teststorage.NewAppendable(), appV2), func(sl *scrapeLoop) {
sl.sampleMutator = func(l labels.Labels) labels.Labels {
return mutateSampleLabels(l, discoveryLabels, false, nil)
}
@@ -4144,6 +4621,12 @@ func TestScrapeLoopLabelLimit(t *testing.T) {
}
func TestTargetScrapeIntervalAndTimeoutRelabel(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testTargetScrapeIntervalAndTimeoutRelabel(t, appV2)
+ })
+}
+
+func testTargetScrapeIntervalAndTimeoutRelabel(t *testing.T, appV2 bool) {
interval, _ := model.ParseDuration("2s")
timeout, _ := model.ParseDuration("500ms")
cfg := &config.ScrapeConfig{
@@ -4170,7 +4653,9 @@ func TestTargetScrapeIntervalAndTimeoutRelabel(t *testing.T) {
},
},
}
- sp, _ := newScrapePool(cfg, teststorage.NewAppendable(), 0, nil, nil, &Options{}, newTestScrapeMetrics(t))
+
+ sa := selectAppendable(teststorage.NewAppendable(), appV2)
+ sp, _ := newScrapePool(cfg, sa.V1(), sa.V2(), 0, nil, nil, &Options{}, newTestScrapeMetrics(t))
tgts := []*targetgroup.Group{
{
Targets: []model.LabelSet{{model.AddressLabel: "127.0.0.1:9090"}},
@@ -4186,6 +4671,12 @@ func TestTargetScrapeIntervalAndTimeoutRelabel(t *testing.T) {
// Testing whether we can remove trailing .0 from histogram 'le' and summary 'quantile' labels.
func TestLeQuantileReLabel(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testLeQuantileReLabel(t, appV2)
+ })
+}
+
+func testLeQuantileReLabel(t *testing.T, appV2 bool) {
s := teststorage.New(t)
t.Cleanup(func() { _ = s.Close() })
@@ -4256,7 +4747,8 @@ test_summary_count 199
ts, scrapedTwice := newScrapableServer(metricsText)
defer ts.Close()
- sp, err := newScrapePool(cfg, s, 0, nil, nil, &Options{}, newTestScrapeMetrics(t))
+ sa := selectAppendable(s, appV2)
+ sp, err := newScrapePool(cfg, sa.V1(), sa.V2(), 0, nil, nil, &Options{}, newTestScrapeMetrics(t))
require.NoError(t, err)
defer sp.stop()
@@ -4305,6 +4797,12 @@ test_summary_count 199
// Testing whether we can automatically convert scraped classic histograms into native histograms with custom buckets.
func TestConvertClassicHistogramsToNHCB(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testConvertClassicHistogramsToNHCB(t, appV2)
+ })
+}
+
+func testConvertClassicHistogramsToNHCB(t *testing.T, appV2 bool) {
t.Parallel()
genTestCounterText := func(name string) string {
@@ -4709,8 +5207,7 @@ metric: <
s := teststorage.New(t)
t.Cleanup(func() { _ = s.Close() })
- sl, _ := newTestScrapeLoop(t, func(sl *scrapeLoop) {
- sl.appendable = s
+ sl, _ := newTestScrapeLoop(t, withAppendable(s, appV2), func(sl *scrapeLoop) {
sl.alwaysScrapeClassicHist = tc.alwaysScrapeClassicHistograms
sl.convertClassicHistToNHCB = tc.convertClassicHistToNHCB
sl.enableNativeHistogramScraping = true
@@ -4789,6 +5286,12 @@ metric: <
}
func TestTypeUnitReLabel(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testTypeUnitReLabel(t, appV2)
+ })
+}
+
+func testTypeUnitReLabel(t *testing.T, appV2 bool) {
s := teststorage.New(t)
t.Cleanup(func() { _ = s.Close() })
@@ -4837,7 +5340,8 @@ disk_usage_bytes 456
ts, scrapedTwice := newScrapableServer(metricsText)
defer ts.Close()
- sp, err := newScrapePool(cfg, s, 0, nil, nil, &Options{}, newTestScrapeMetrics(t))
+ sa := selectAppendable(s, appV2)
+ sp, err := newScrapePool(cfg, sa.V1(), sa.V2(), 0, nil, nil, &Options{}, newTestScrapeMetrics(t))
require.NoError(t, err)
defer sp.stop()
@@ -4875,13 +5379,18 @@ disk_usage_bytes 456
}
func TestScrapeLoopRunCreatesStaleMarkersOnFailedScrapeForTimestampedMetrics(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopRunCreatesStaleMarkersOnFailedScrapeForTimestampedMetrics(t, appV2)
+ })
+}
+
+func testScrapeLoopRunCreatesStaleMarkersOnFailedScrapeForTimestampedMetrics(t *testing.T, appV2 bool) {
signal := make(chan struct{}, 1)
ctx, cancel := context.WithCancel(t.Context())
appTest := teststorage.NewAppendable()
- sl, scraper := newTestScrapeLoop(t, func(sl *scrapeLoop) {
- sl.ctx = ctx
- sl.appendable = appTest // Since we're writing samples directly below we need to provide a protocol fallback.
+ sl, scraper := newTestScrapeLoop(t, withAppendable(appTest, appV2), func(sl *scrapeLoop) {
+ sl.ctx = ctx // Since we're writing samples directly below we need to provide a protocol fallback.
sl.fallbackScrapeProtocol = "text/plain"
sl.trackTimestampsStaleness = true
})
@@ -4922,6 +5431,12 @@ func TestScrapeLoopRunCreatesStaleMarkersOnFailedScrapeForTimestampedMetrics(t *
}
func TestScrapeLoopCompression(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopCompression(t, appV2)
+ })
+}
+
+func testScrapeLoopCompression(t *testing.T, appV2 bool) {
s := teststorage.New(t)
t.Cleanup(func() { _ = s.Close() })
@@ -4961,7 +5476,8 @@ func TestScrapeLoopCompression(t *testing.T) {
MetricNameEscapingScheme: model.AllowUTF8,
}
- sp, err := newScrapePool(cfg, s, 0, nil, nil, &Options{}, newTestScrapeMetrics(t))
+ sa := selectAppendable(s, appV2)
+ sp, err := newScrapePool(cfg, sa.V1(), sa.V2(), 0, nil, nil, &Options{}, newTestScrapeMetrics(t))
require.NoError(t, err)
defer sp.stop()
@@ -5133,7 +5649,13 @@ func BenchmarkTargetScraperGzip(b *testing.B) {
// When a scrape contains multiple instances for the same time series we should increment
// prometheus_target_scrapes_sample_duplicate_timestamp_total metric.
func TestScrapeLoopSeriesAddedDuplicates(t *testing.T) {
- sl, _ := newTestScrapeLoop(t)
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopSeriesAddedDuplicates(t, appV2)
+ })
+}
+
+func testScrapeLoopSeriesAddedDuplicates(t *testing.T, appV2 bool) {
+ sl, _ := newTestScrapeLoop(t, withAppendable(teststorage.NewAppendable(), appV2))
app := sl.appender()
total, added, seriesAdded, err := app.append([]byte("test_metric 1\ntest_metric 2\ntest_metric 3\n"), "text/plain", time.Time{})
@@ -5168,32 +5690,37 @@ func TestScrapeLoopSeriesAddedDuplicates(t *testing.T) {
// This tests running a full scrape loop and checking that the scrape option
// `native_histogram_min_bucket_factor` is used correctly.
func TestNativeHistogramMaxSchemaSet(t *testing.T) {
- testcases := map[string]struct {
- minBucketFactor string
- expectedSchema int32
- }{
- "min factor not specified": {
- minBucketFactor: "",
- expectedSchema: 3, // Factor 1.09.
- },
- "min factor 1": {
- minBucketFactor: "native_histogram_min_bucket_factor: 1",
- expectedSchema: 3, // Factor 1.09.
- },
- "min factor 2": {
- minBucketFactor: "native_histogram_min_bucket_factor: 2",
- expectedSchema: 0, // Factor 2.00.
- },
- }
- for name, tc := range testcases {
- t.Run(name, func(t *testing.T) {
- t.Parallel()
- testNativeHistogramMaxSchemaSet(t, tc.minBucketFactor, tc.expectedSchema)
- })
- }
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ for _, tc := range []struct {
+ name string
+ minBucketFactor string
+ expectedSchema int32
+ }{
+ {
+ name: "min factor not specified",
+ minBucketFactor: "",
+ expectedSchema: 3, // Factor 1.09.
+ },
+ {
+ name: "min factor 1",
+ minBucketFactor: "native_histogram_min_bucket_factor: 1",
+ expectedSchema: 3, // Factor 1.09.
+ },
+ {
+ name: "min factor 2",
+ minBucketFactor: "native_histogram_min_bucket_factor: 2",
+ expectedSchema: 0, // Factor 2.00.
+ },
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+ testNativeHistogramMaxSchemaSet(t, tc.minBucketFactor, tc.expectedSchema, appV2)
+ })
+ }
+ })
}
-func testNativeHistogramMaxSchemaSet(t *testing.T, minBucketFactor string, expectedSchema int32) {
+func testNativeHistogramMaxSchemaSet(t *testing.T, minBucketFactor string, expectedSchema int32, appV2 bool) {
// Create a ProtoBuf message to serve as a Prometheus metric.
nativeHistogram := prometheus.NewHistogram(
prometheus.HistogramOpts{
@@ -5246,6 +5773,12 @@ scrape_configs:
mng, err := NewManager(&Options{DiscoveryReloadInterval: model.Duration(10 * time.Millisecond)}, nil, nil, s, reg)
require.NoError(t, err)
+
+ if appV2 {
+ mng.appendableV2 = s
+ mng.appendable = nil
+ }
+
cfg, err := config.Load(configStr, promslog.NewNopLogger())
require.NoError(t, err)
require.NoError(t, mng.ApplyConfig(cfg))
@@ -5301,6 +5834,12 @@ scrape_configs:
}
func TestTargetScrapeConfigWithLabels(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testTargetScrapeConfigWithLabels(t, appV2)
+ })
+}
+
+func testTargetScrapeConfigWithLabels(t *testing.T, appV2 bool) {
t.Parallel()
const (
configTimeout = 1500 * time.Millisecond
@@ -5346,7 +5885,8 @@ func TestTargetScrapeConfigWithLabels(t *testing.T) {
}
}
- sp, err := newScrapePool(cfg, teststorage.NewAppendable(), 0, nil, nil, &Options{}, newTestScrapeMetrics(t))
+ sa := selectAppendable(teststorage.NewAppendable(), appV2)
+ sp, err := newScrapePool(cfg, sa.V1(), sa.V2(), 0, nil, nil, &Options{}, newTestScrapeMetrics(t))
require.NoError(t, err)
t.Cleanup(sp.stop)
@@ -5484,6 +6024,12 @@ func newScrapableServer(scrapeText string) (s *httptest.Server, scrapedTwice cha
// Regression test for the panic fixed in https://github.com/prometheus/prometheus/pull/15523.
func TestScrapePoolScrapeAfterReload(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapePoolScrapeAfterReload(t, appV2)
+ })
+}
+
+func testScrapePoolScrapeAfterReload(t *testing.T, appV2 bool) {
h := httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte{0x42, 0x42})
@@ -5509,7 +6055,8 @@ func TestScrapePoolScrapeAfterReload(t *testing.T) {
},
}
- p, err := newScrapePool(cfg, teststorage.NewAppendable(), 0, nil, nil, &Options{}, newTestScrapeMetrics(t))
+ sa := selectAppendable(teststorage.NewAppendable(), appV2)
+ p, err := newScrapePool(cfg, sa.V1(), sa.V2(), 0, nil, nil, &Options{}, newTestScrapeMetrics(t))
require.NoError(t, err)
t.Cleanup(p.stop)
@@ -5529,6 +6076,12 @@ func TestScrapePoolScrapeAfterReload(t *testing.T) {
// The first scrape fails with a parsing error, but the second should
// succeed and cause `metric_1=11` to appear in the appender.
func TestScrapeAppendWithParseError(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeAppendWithParseError(t, appV2)
+ })
+}
+
+func testScrapeAppendWithParseError(t *testing.T, appV2 bool) {
const (
scrape1 = `metric_a 1
`
@@ -5537,7 +6090,7 @@ func TestScrapeAppendWithParseError(t *testing.T) {
)
appTest := teststorage.NewAppendable()
- sl, _ := newTestScrapeLoop(t, withAppendable(appTest))
+ sl, _ := newTestScrapeLoop(t, withAppendable(appTest, appV2))
now := time.Now()
app := sl.appender()
@@ -5563,17 +6116,22 @@ func TestScrapeAppendWithParseError(t *testing.T) {
V: 11,
},
}
- requireEqual(t, want, appTest.ResultSamples(), "Appended samples not as expected:\n%s", appTest)
+ teststorage.RequireEqual(t, want, appTest.ResultSamples(), "Appended samples not as expected:\n%s", appTest)
}
// This test covers a case where there's a target with sample_limit set and some samples
// changes between scrapes.
func TestScrapeLoopAppendSampleLimitWithDisappearingSeries(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopAppendSampleLimitWithDisappearingSeries(t, appV2)
+ })
+}
+
+func testScrapeLoopAppendSampleLimitWithDisappearingSeries(t *testing.T, appV2 bool) {
const sampleLimit = 4
appTest := teststorage.NewAppendable()
- sl, _ := newTestScrapeLoop(t, func(sl *scrapeLoop) {
- sl.appendable = appTest
+ sl, _ := newTestScrapeLoop(t, withAppendable(appTest, appV2), func(sl *scrapeLoop) {
sl.sampleLimit = sampleLimit
})
@@ -5607,7 +6165,7 @@ func TestScrapeLoopAppendSampleLimitWithDisappearingSeries(t *testing.T) {
V: 1,
},
}
- requireEqual(t, want, appTest.ResultSamples(), "Appended samples not as expected:\n%s", app)
+ teststorage.RequireEqual(t, want, appTest.ResultSamples(), "Appended samples not as expected:\n%s", app)
now = now.Add(time.Minute)
app = sl.appender()
@@ -5622,7 +6180,7 @@ func TestScrapeLoopAppendSampleLimitWithDisappearingSeries(t *testing.T) {
require.Equal(t, 6, samplesScraped)
require.Equal(t, 6, samplesAfterRelabel)
require.Equal(t, 1, createdSeries) // We've added one series before hitting the limit.
- requireEqual(t, want, appTest.ResultSamples(), "Appended samples not as expected:\n%s", app)
+ testutil.RequireEqual(t, want, appTest.ResultSamples(), "Appended samples not as expected:\n%s", app)
sl.cache.iterDone(false)
now = now.Add(time.Minute)
@@ -5666,17 +6224,22 @@ func TestScrapeLoopAppendSampleLimitWithDisappearingSeries(t *testing.T) {
V: math.Float64frombits(value.StaleNaN),
},
}...)
- requireEqual(t, want, appTest.ResultSamples(), "Appended samples not as expected:\n%s", app)
+ teststorage.RequireEqual(t, want, appTest.ResultSamples(), "Appended samples not as expected:\n%s", appTest)
}
// This test covers a case where there's a target with sample_limit set and each scrape sees a completely
// different set of samples.
func TestScrapeLoopAppendSampleLimitReplaceAllSamples(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopAppendSampleLimitReplaceAllSamples(t, appV2)
+ })
+}
+
+func testScrapeLoopAppendSampleLimitReplaceAllSamples(t *testing.T, appV2 bool) {
const sampleLimit = 4
appTest := teststorage.NewAppendable()
- sl, _ := newTestScrapeLoop(t, func(sl *scrapeLoop) {
- sl.appendable = appTest
+ sl, _ := newTestScrapeLoop(t, withAppendable(appTest, appV2), func(sl *scrapeLoop) {
sl.sampleLimit = sampleLimit
})
@@ -5715,7 +6278,7 @@ func TestScrapeLoopAppendSampleLimitReplaceAllSamples(t *testing.T) {
V: 1,
},
}
- requireEqual(t, want, appTest.ResultSamples(), "Appended samples not as expected:\n%s", app)
+ teststorage.RequireEqual(t, want, appTest.ResultSamples(), "Appended samples not as expected:\n%s", app)
now = now.Add(time.Minute)
app = sl.appender()
@@ -5776,14 +6339,20 @@ func TestScrapeLoopAppendSampleLimitReplaceAllSamples(t *testing.T) {
V: math.Float64frombits(value.StaleNaN),
},
}...)
- requireEqual(t, want, appTest.ResultSamples(), "Appended samples not as expected:\n%s", app)
+ teststorage.RequireEqual(t, want, appTest.ResultSamples(), "Appended samples not as expected:\n%s", app)
}
func TestScrapeLoopDisableStalenessMarkerInjection(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopDisableStalenessMarkerInjection(t, appV2)
+ })
+}
+
+func testScrapeLoopDisableStalenessMarkerInjection(t *testing.T, appV2 bool) {
loopDone := atomic.NewBool(false)
appTest := teststorage.NewAppendable()
- sl, scraper := newTestScrapeLoop(t, withAppendable(appTest))
+ sl, scraper := newTestScrapeLoop(t, withAppendable(appTest, appV2))
scraper.scrapeFunc = func(ctx context.Context, w io.Writer) error {
if _, err := w.Write([]byte("metric_a 42\n")); err != nil {
return err
@@ -5832,6 +6401,7 @@ func BenchmarkScrapePoolRestartLoops(b *testing.B) {
ScrapeTimeout: model.Duration(1 * time.Hour),
},
nil,
+ nil,
0,
nil,
nil,
@@ -5857,6 +6427,12 @@ func BenchmarkScrapePoolRestartLoops(b *testing.B) {
// TestNewScrapeLoopHonorLabelsWiring verifies that newScrapeLoop correctly wires
// HonorLabels (not HonorTimestamps) to the sampleMutator.
func TestNewScrapeLoopHonorLabelsWiring(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testNewScrapeLoopHonorLabelsWiring(t, appV2)
+ })
+}
+
+func testNewScrapeLoopHonorLabelsWiring(t *testing.T, appV2 bool) {
// Scraped metric has label "lbl" with value "scraped".
// Discovery target has label "lbl" with value "discovery".
// With honor_labels=true, the scraped value should win.
@@ -5900,7 +6476,8 @@ func TestNewScrapeLoopHonorLabelsWiring(t *testing.T) {
MetricNameValidationScheme: model.UTF8Validation,
}
- sp, err := newScrapePool(cfg, s, 0, nil, nil, &Options{skipOffsetting: true}, newTestScrapeMetrics(t))
+ sa := selectAppendable(s, appV2)
+ sp, err := newScrapePool(cfg, sa.V1(), sa.V2(), 0, nil, nil, &Options{skipOffsetting: true}, newTestScrapeMetrics(t))
require.NoError(t, err)
defer sp.stop()
@@ -5934,6 +6511,12 @@ func TestNewScrapeLoopHonorLabelsWiring(t *testing.T) {
}
func TestDropsSeriesFromMetricRelabeling(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testDropsSeriesFromMetricRelabeling(t, appV2)
+ })
+}
+
+func testDropsSeriesFromMetricRelabeling(t *testing.T, appV2 bool) {
target := &Target{}
relabelConfig := []*relabel.Config{
{
@@ -5949,7 +6532,7 @@ func TestDropsSeriesFromMetricRelabeling(t *testing.T) {
NameValidationScheme: model.UTF8Validation,
},
}
- sl, _ := newTestScrapeLoop(t, func(sl *scrapeLoop) {
+ sl, _ := newTestScrapeLoop(t, withAppendable(teststorage.NewAppendable(), appV2), func(sl *scrapeLoop) {
sl.sampleMutator = func(l labels.Labels) labels.Labels {
return mutateSampleLabels(l, target, true, relabelConfig)
}
diff --git a/scrape/target.go b/scrape/target.go
index 4265f9e782..1040241bd3 100644
--- a/scrape/target.go
+++ b/scrape/target.go
@@ -454,6 +454,105 @@ func (app *maxSchemaAppender) AppendHistogram(ref storage.SeriesRef, lset labels
return ref, nil
}
+// limitAppender limits the number of total appended samples in a batch.
+type limitAppenderV2 struct {
+ storage.AppenderV2
+
+ limit int
+ i int
+}
+
+func (app *limitAppenderV2) Append(ref storage.SeriesRef, ls labels.Labels, st, t int64, v float64, h *histogram.Histogram, fh *histogram.FloatHistogram, opts storage.AOptions) (storage.SeriesRef, error) {
+ // Bypass sample_limit checks only if we have a staleness marker for a known series (ref value is non-zero).
+ // This ensures that if a series is already in TSDB then we always write the marker.
+ if ref == 0 || !value.IsStaleNaN(v) {
+ app.i++
+ if app.i > app.limit {
+ return 0, errSampleLimit
+ }
+ }
+ return app.AppenderV2.Append(ref, ls, st, t, v, h, fh, opts)
+}
+
+type timeLimitAppenderV2 struct {
+ storage.AppenderV2
+
+ maxTime int64
+}
+
+func (app *timeLimitAppenderV2) Append(ref storage.SeriesRef, ls labels.Labels, st, t int64, v float64, h *histogram.Histogram, fh *histogram.FloatHistogram, opts storage.AOptions) (storage.SeriesRef, error) {
+ if t > app.maxTime {
+ return 0, storage.ErrOutOfBounds
+ }
+
+ return app.AppenderV2.Append(ref, ls, st, t, v, h, fh, opts)
+}
+
+// bucketLimitAppender limits the number of total appended samples in a batch.
+type bucketLimitAppenderV2 struct {
+ storage.AppenderV2
+
+ limit int
+}
+
+func (app *bucketLimitAppenderV2) Append(ref storage.SeriesRef, ls labels.Labels, st, t int64, v float64, h *histogram.Histogram, fh *histogram.FloatHistogram, opts storage.AOptions) (_ storage.SeriesRef, err error) {
+ if h != nil {
+ // Return with an early error if the histogram has too many buckets and the
+ // schema is not exponential, in which case we can't reduce the resolution.
+ if len(h.PositiveBuckets)+len(h.NegativeBuckets) > app.limit && !histogram.IsExponentialSchema(h.Schema) {
+ return 0, errBucketLimit
+ }
+ for len(h.PositiveBuckets)+len(h.NegativeBuckets) > app.limit {
+ if h.Schema <= histogram.ExponentialSchemaMin {
+ return 0, errBucketLimit
+ }
+ if err = h.ReduceResolution(h.Schema - 1); err != nil {
+ return 0, err
+ }
+ }
+ }
+ if fh != nil {
+ // Return with an early error if the histogram has too many buckets and the
+ // schema is not exponential, in which case we can't reduce the resolution.
+ if len(fh.PositiveBuckets)+len(fh.NegativeBuckets) > app.limit && !histogram.IsExponentialSchema(fh.Schema) {
+ return 0, errBucketLimit
+ }
+ for len(fh.PositiveBuckets)+len(fh.NegativeBuckets) > app.limit {
+ if fh.Schema <= histogram.ExponentialSchemaMin {
+ return 0, errBucketLimit
+ }
+ if err = fh.ReduceResolution(fh.Schema - 1); err != nil {
+ return 0, err
+ }
+ }
+ }
+ return app.AppenderV2.Append(ref, ls, st, t, v, h, fh, opts)
+}
+
+type maxSchemaAppenderV2 struct {
+ storage.AppenderV2
+
+ maxSchema int32
+}
+
+func (app *maxSchemaAppenderV2) Append(ref storage.SeriesRef, ls labels.Labels, st, t int64, v float64, h *histogram.Histogram, fh *histogram.FloatHistogram, opts storage.AOptions) (_ storage.SeriesRef, err error) {
+ if h != nil {
+ if histogram.IsExponentialSchemaReserved(h.Schema) && h.Schema > app.maxSchema {
+ if err = h.ReduceResolution(app.maxSchema); err != nil {
+ return 0, err
+ }
+ }
+ }
+ if fh != nil {
+ if histogram.IsExponentialSchemaReserved(fh.Schema) && fh.Schema > app.maxSchema {
+ if err = fh.ReduceResolution(app.maxSchema); err != nil {
+ return 0, err
+ }
+ }
+ }
+ return app.AppenderV2.Append(ref, ls, st, t, v, h, fh, opts)
+}
+
// PopulateDiscoveredLabels sets base labels on lb from target and group labels and scrape configuration, before relabeling.
func PopulateDiscoveredLabels(lb *labels.Builder, cfg *config.ScrapeConfig, tLabels, tgLabels model.LabelSet) {
lb.Reset(labels.EmptyLabels())
diff --git a/scrape/target_test.go b/scrape/target_test.go
index 06227da816..ea0aa2009f 100644
--- a/scrape/target_test.go
+++ b/scrape/target_test.go
@@ -35,6 +35,7 @@ import (
"github.com/prometheus/prometheus/model/histogram"
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/model/timestamp"
+ "github.com/prometheus/prometheus/storage"
"github.com/prometheus/prometheus/util/teststorage"
)
@@ -610,37 +611,65 @@ func TestBucketLimitAppender(t *testing.T) {
},
}
- appTest := teststorage.NewAppendable()
-
for _, c := range cases {
for _, floatHisto := range []bool{true, false} {
t.Run(fmt.Sprintf("floatHistogram=%t", floatHisto), func(t *testing.T) {
- app := &bucketLimitAppender{Appender: appTest.Appender(t.Context()), limit: c.limit}
- ts := int64(10 * time.Minute / time.Millisecond)
- lbls := labels.FromStrings("__name__", "sparse_histogram_series")
- var err error
- if floatHisto {
- fh := c.h.Copy().ToFloat(nil)
- _, err = app.AppendHistogram(0, lbls, ts, nil, fh)
- if c.expectError {
- require.Error(t, err)
+ t.Run("appV2=false", func(t *testing.T) {
+ app := &bucketLimitAppender{Appender: teststorage.NewAppendable().Appender(t.Context()), limit: c.limit}
+ ts := int64(10 * time.Minute / time.Millisecond)
+ lbls := labels.FromStrings("__name__", "sparse_histogram_series")
+ var err error
+ if floatHisto {
+ fh := c.h.Copy().ToFloat(nil)
+ _, err = app.AppendHistogram(0, lbls, ts, nil, fh)
+ if c.expectError {
+ require.Error(t, err)
+ } else {
+ require.Equal(t, c.expectSchema, fh.Schema)
+ require.Equal(t, c.expectBucketCount, len(fh.NegativeBuckets)+len(fh.PositiveBuckets))
+ require.NoError(t, err)
+ }
} else {
- require.Equal(t, c.expectSchema, fh.Schema)
- require.Equal(t, c.expectBucketCount, len(fh.NegativeBuckets)+len(fh.PositiveBuckets))
- require.NoError(t, err)
+ h := c.h.Copy()
+ _, err = app.AppendHistogram(0, lbls, ts, h, nil)
+ if c.expectError {
+ require.Error(t, err)
+ } else {
+ require.Equal(t, c.expectSchema, h.Schema)
+ require.Equal(t, c.expectBucketCount, len(h.NegativeBuckets)+len(h.PositiveBuckets))
+ require.NoError(t, err)
+ }
}
- } else {
- h := c.h.Copy()
- _, err = app.AppendHistogram(0, lbls, ts, h, nil)
- if c.expectError {
- require.Error(t, err)
+ require.NoError(t, app.Commit())
+ })
+ t.Run("appV2=true", func(t *testing.T) {
+ app := &bucketLimitAppenderV2{AppenderV2: teststorage.NewAppendable().AppenderV2(t.Context()), limit: c.limit}
+ ts := int64(10 * time.Minute / time.Millisecond)
+ lbls := labels.FromStrings("__name__", "sparse_histogram_series")
+ var err error
+ if floatHisto {
+ fh := c.h.Copy().ToFloat(nil)
+ _, err = app.Append(0, lbls, 0, ts, 0, nil, fh, storage.AOptions{})
+ if c.expectError {
+ require.Error(t, err)
+ } else {
+ require.Equal(t, c.expectSchema, fh.Schema)
+ require.Equal(t, c.expectBucketCount, len(fh.NegativeBuckets)+len(fh.PositiveBuckets))
+ require.NoError(t, err)
+ }
} else {
- require.Equal(t, c.expectSchema, h.Schema)
- require.Equal(t, c.expectBucketCount, len(h.NegativeBuckets)+len(h.PositiveBuckets))
- require.NoError(t, err)
+ h := c.h.Copy()
+ _, err = app.Append(0, lbls, 0, ts, 0, h, nil, storage.AOptions{})
+ if c.expectError {
+ require.Error(t, err)
+ } else {
+ require.Equal(t, c.expectSchema, h.Schema)
+ require.Equal(t, c.expectBucketCount, len(h.NegativeBuckets)+len(h.PositiveBuckets))
+ require.NoError(t, err)
+ }
}
- }
- require.NoError(t, app.Commit())
+ require.NoError(t, app.Commit())
+ })
})
}
}
@@ -696,27 +725,45 @@ func TestMaxSchemaAppender(t *testing.T) {
},
}
- appTest := teststorage.NewAppendable()
-
for _, c := range cases {
for _, floatHisto := range []bool{true, false} {
t.Run(fmt.Sprintf("floatHistogram=%t", floatHisto), func(t *testing.T) {
- app := &maxSchemaAppender{Appender: appTest.Appender(t.Context()), maxSchema: c.maxSchema}
- ts := int64(10 * time.Minute / time.Millisecond)
- lbls := labels.FromStrings("__name__", "sparse_histogram_series")
- var err error
- if floatHisto {
- fh := c.h.Copy().ToFloat(nil)
- _, err = app.AppendHistogram(0, lbls, ts, nil, fh)
- require.Equal(t, c.expectSchema, fh.Schema)
- require.NoError(t, err)
- } else {
- h := c.h.Copy()
- _, err = app.AppendHistogram(0, lbls, ts, h, nil)
- require.Equal(t, c.expectSchema, h.Schema)
- require.NoError(t, err)
- }
- require.NoError(t, app.Commit())
+ t.Run("appV2=false", func(t *testing.T) {
+ app := &maxSchemaAppender{Appender: teststorage.NewAppendable().Appender(t.Context()), maxSchema: c.maxSchema}
+ ts := int64(10 * time.Minute / time.Millisecond)
+ lbls := labels.FromStrings("__name__", "sparse_histogram_series")
+ var err error
+ if floatHisto {
+ fh := c.h.Copy().ToFloat(nil)
+ _, err = app.AppendHistogram(0, lbls, ts, nil, fh)
+ require.Equal(t, c.expectSchema, fh.Schema)
+ require.NoError(t, err)
+ } else {
+ h := c.h.Copy()
+ _, err = app.AppendHistogram(0, lbls, ts, h, nil)
+ require.Equal(t, c.expectSchema, h.Schema)
+ require.NoError(t, err)
+ }
+ require.NoError(t, app.Commit())
+ })
+ t.Run("appV2=true", func(t *testing.T) {
+ app := &maxSchemaAppenderV2{AppenderV2: teststorage.NewAppendable().AppenderV2(t.Context()), maxSchema: c.maxSchema}
+ ts := int64(10 * time.Minute / time.Millisecond)
+ lbls := labels.FromStrings("__name__", "sparse_histogram_series")
+ var err error
+ if floatHisto {
+ fh := c.h.Copy().ToFloat(nil)
+ _, err = app.Append(0, lbls, 0, ts, 0, nil, fh, storage.AOptions{})
+ require.Equal(t, c.expectSchema, fh.Schema)
+ require.NoError(t, err)
+ } else {
+ h := c.h.Copy()
+ _, err = app.Append(0, lbls, 0, ts, 0, h, nil, storage.AOptions{})
+ require.Equal(t, c.expectSchema, h.Schema)
+ require.NoError(t, err)
+ }
+ require.NoError(t, app.Commit())
+ })
})
}
}
@@ -724,32 +771,65 @@ func TestMaxSchemaAppender(t *testing.T) {
// Test sample_limit when a scrape contains Native Histograms.
func TestAppendWithSampleLimitAndNativeHistogram(t *testing.T) {
- appTest := teststorage.NewAppendable()
-
now := time.Now()
- app := appenderWithLimits(appTest.Appender(t.Context()), 2, 0, histogram.ExponentialSchemaMax)
+ t.Run("appV2=false", func(t *testing.T) {
+ app := appenderWithLimits(teststorage.NewAppendable().Appender(t.Context()), 2, 0, histogram.ExponentialSchemaMax)
- // sample_limit is set to 2, so first two scrapes should work
- _, err := app.Append(0, labels.FromStrings(model.MetricNameLabel, "foo"), timestamp.FromTime(now), 1)
- require.NoError(t, err)
+ // sample_limit is set to 2, so first two scrapes should work
+ _, err := app.Append(0, labels.FromStrings(model.MetricNameLabel, "foo"), timestamp.FromTime(now), 1)
+ require.NoError(t, err)
- // Second sample, should be ok.
- _, err = app.AppendHistogram(
- 0,
- labels.FromStrings(model.MetricNameLabel, "my_histogram1"),
- timestamp.FromTime(now),
- &histogram.Histogram{},
- nil,
- )
- require.NoError(t, err)
+ // Second sample, should be ok.
+ _, err = app.AppendHistogram(
+ 0,
+ labels.FromStrings(model.MetricNameLabel, "my_histogram1"),
+ timestamp.FromTime(now),
+ &histogram.Histogram{},
+ nil,
+ )
+ require.NoError(t, err)
- // This is third sample with sample_limit=2, it should trigger errSampleLimit.
- _, err = app.AppendHistogram(
- 0,
- labels.FromStrings(model.MetricNameLabel, "my_histogram2"),
- timestamp.FromTime(now),
- &histogram.Histogram{},
- nil,
- )
- require.ErrorIs(t, err, errSampleLimit)
+ // This is third sample with sample_limit=2, it should trigger errSampleLimit.
+ _, err = app.AppendHistogram(
+ 0,
+ labels.FromStrings(model.MetricNameLabel, "my_histogram2"),
+ timestamp.FromTime(now),
+ &histogram.Histogram{},
+ nil,
+ )
+ require.ErrorIs(t, err, errSampleLimit)
+ })
+ t.Run("appV2=true", func(t *testing.T) {
+ app := appenderV2WithLimits(teststorage.NewAppendable().AppenderV2(t.Context()), 2, 0, histogram.ExponentialSchemaMax)
+
+ // sample_limit is set to 2, so first two scrapes should work
+ _, err := app.Append(0, labels.FromStrings(model.MetricNameLabel, "foo"), 0, timestamp.FromTime(now), 1, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+
+ // Second sample, should be ok.
+ _, err = app.Append(
+ 0,
+ labels.FromStrings(model.MetricNameLabel, "my_histogram1"),
+ 0,
+ timestamp.FromTime(now),
+ 0,
+ &histogram.Histogram{},
+ nil,
+ storage.AOptions{},
+ )
+ require.NoError(t, err)
+
+ // This is third sample with sample_limit=2, it should trigger errSampleLimit.
+ _, err = app.Append(
+ 0,
+ labels.FromStrings(model.MetricNameLabel, "my_histogram2"),
+ 0,
+ timestamp.FromTime(now),
+ 0,
+ &histogram.Histogram{},
+ nil,
+ storage.AOptions{},
+ )
+ require.ErrorIs(t, err, errSampleLimit)
+ })
}
diff --git a/scripts/check-go-mod-version.sh b/scripts/check-go-mod-version.sh
index d651a62036..96317de2e6 100755
--- a/scripts/check-go-mod-version.sh
+++ b/scripts/check-go-mod-version.sh
@@ -1,12 +1,12 @@
#!/usr/bin/env bash
-readarray -t mod_files < <(find . -type f -name go.mod)
+readarray -t mod_files < <(git ls-files go.mod go.work '*/go.mod' || find . -type f -name go.mod -or -name go.work)
echo "Checking files ${mod_files[@]}"
matches=$(awk '$1 == "go" {print $2}' "${mod_files[@]}" | sort -u | wc -l)
if [[ "${matches}" -ne 1 ]]; then
- echo 'Not all go.mod files have matching go versions'
+ echo 'Not all go.mod/go.work files have matching go versions'
exit 1
fi
diff --git a/storage/buffer.go b/storage/buffer.go
index 223c4fa42b..cdf8879f21 100644
--- a/storage/buffer.go
+++ b/storage/buffer.go
@@ -119,13 +119,16 @@ func (b *BufferedSeriesIterator) Next() chunkenc.ValueType {
return chunkenc.ValNone
case chunkenc.ValFloat:
t, f := b.it.At()
- b.buf.addF(fSample{t: t, f: f})
+ st := b.it.AtST()
+ b.buf.addF(fSample{st: st, t: t, f: f})
case chunkenc.ValHistogram:
t, h := b.it.AtHistogram(&b.hReader)
- b.buf.addH(hSample{t: t, h: h})
+ st := b.it.AtST()
+ b.buf.addH(hSample{st: st, t: t, h: h})
case chunkenc.ValFloatHistogram:
t, fh := b.it.AtFloatHistogram(&b.fhReader)
- b.buf.addFH(fhSample{t: t, fh: fh})
+ st := b.it.AtST()
+ b.buf.addFH(fhSample{st: st, t: t, fh: fh})
default:
panic(fmt.Errorf("BufferedSeriesIterator: unknown value type %v", b.valueType))
}
@@ -157,20 +160,29 @@ func (b *BufferedSeriesIterator) AtT() int64 {
return b.it.AtT()
}
+// AtST returns the current sample's start timestamp of the iterator.
+func (b *BufferedSeriesIterator) AtST() int64 {
+ return b.it.AtST()
+}
+
// Err returns the last encountered error.
func (b *BufferedSeriesIterator) Err() error {
return b.it.Err()
}
type fSample struct {
- t int64
- f float64
+ st, t int64
+ f float64
}
func (s fSample) T() int64 {
return s.t
}
+func (s fSample) ST() int64 {
+ return s.st
+}
+
func (s fSample) F() float64 {
return s.f
}
@@ -192,14 +204,18 @@ func (s fSample) Copy() chunks.Sample {
}
type hSample struct {
- t int64
- h *histogram.Histogram
+ st, t int64
+ h *histogram.Histogram
}
func (s hSample) T() int64 {
return s.t
}
+func (s hSample) ST() int64 {
+ return s.st
+}
+
func (hSample) F() float64 {
panic("F() called for hSample")
}
@@ -217,18 +233,22 @@ func (hSample) Type() chunkenc.ValueType {
}
func (s hSample) Copy() chunks.Sample {
- return hSample{t: s.t, h: s.h.Copy()}
+ return hSample{st: s.st, t: s.t, h: s.h.Copy()}
}
type fhSample struct {
- t int64
- fh *histogram.FloatHistogram
+ st, t int64
+ fh *histogram.FloatHistogram
}
func (s fhSample) T() int64 {
return s.t
}
+func (s fhSample) ST() int64 {
+ return s.st
+}
+
func (fhSample) F() float64 {
panic("F() called for fhSample")
}
@@ -246,7 +266,7 @@ func (fhSample) Type() chunkenc.ValueType {
}
func (s fhSample) Copy() chunks.Sample {
- return fhSample{t: s.t, fh: s.fh.Copy()}
+ return fhSample{st: s.st, t: s.t, fh: s.fh.Copy()}
}
type sampleRing struct {
@@ -329,6 +349,7 @@ func (r *sampleRing) iterator() *SampleRingIterator {
type SampleRingIterator struct {
r *sampleRing
i int
+ st int64
t int64
f float64
h *histogram.Histogram
@@ -350,21 +371,25 @@ func (it *SampleRingIterator) Next() chunkenc.ValueType {
switch it.r.bufInUse {
case fBuf:
s := it.r.atF(it.i)
+ it.st = s.st
it.t = s.t
it.f = s.f
return chunkenc.ValFloat
case hBuf:
s := it.r.atH(it.i)
+ it.st = s.st
it.t = s.t
it.h = s.h
return chunkenc.ValHistogram
case fhBuf:
s := it.r.atFH(it.i)
+ it.st = s.st
it.t = s.t
it.fh = s.fh
return chunkenc.ValFloatHistogram
}
s := it.r.at(it.i)
+ it.st = s.ST()
it.t = s.T()
switch s.Type() {
case chunkenc.ValHistogram:
@@ -410,6 +435,10 @@ func (it *SampleRingIterator) AtT() int64 {
return it.t
}
+func (it *SampleRingIterator) AtST() int64 {
+ return it.st
+}
+
func (r *sampleRing) at(i int) chunks.Sample {
j := (r.f + i) % len(r.iBuf)
return r.iBuf[j]
@@ -651,6 +680,7 @@ func addH(s hSample, buf []hSample, r *sampleRing) []hSample {
}
buf[r.i].t = s.t
+ buf[r.i].st = s.st
if buf[r.i].h == nil {
buf[r.i].h = s.h.Copy()
} else {
@@ -695,6 +725,7 @@ func addFH(s fhSample, buf []fhSample, r *sampleRing) []fhSample {
}
buf[r.i].t = s.t
+ buf[r.i].st = s.st
if buf[r.i].fh == nil {
buf[r.i].fh = s.fh.Copy()
} else {
diff --git a/storage/buffer_test.go b/storage/buffer_test.go
index fc6603d4a5..61d1601bc0 100644
--- a/storage/buffer_test.go
+++ b/storage/buffer_test.go
@@ -61,10 +61,9 @@ func TestSampleRing(t *testing.T) {
input := []fSample{}
for _, t := range c.input {
- input = append(input, fSample{
- t: t,
- f: float64(rand.Intn(100)),
- })
+ // Randomize start timestamp to make sure it does not affect the
+ // outcome.
+ input = append(input, fSample{st: rand.Int63(), t: t, f: float64(rand.Intn(100))})
}
for i, s := range input {
@@ -90,6 +89,24 @@ func TestSampleRing(t *testing.T) {
}
}
+func TestSampleRingFloatST(t *testing.T) {
+ r := newSampleRing(10, 5, chunkenc.ValNone)
+ require.Empty(t, r.fBuf)
+ require.Empty(t, r.hBuf)
+ require.Empty(t, r.fhBuf)
+ require.Empty(t, r.iBuf)
+
+ r.addF(fSample{st: 100, t: 11, f: 3.14})
+ it := r.iterator()
+
+ require.Equal(t, chunkenc.ValFloat, it.Next())
+ ts, f := it.At()
+ require.Equal(t, int64(11), ts)
+ require.Equal(t, 3.14, f)
+ require.Equal(t, int64(100), it.AtST())
+ require.Equal(t, chunkenc.ValNone, it.Next())
+}
+
func TestSampleRingMixed(t *testing.T) {
h1 := tsdbutil.GenerateTestHistogram(1)
h2 := tsdbutil.GenerateTestHistogram(2)
@@ -102,39 +119,43 @@ func TestSampleRingMixed(t *testing.T) {
require.Empty(t, r.iBuf)
// But then mixed adds should work as expected.
- r.addF(fSample{t: 1, f: 3.14})
- r.addH(hSample{t: 2, h: h1})
+ r.addF(fSample{st: 10, t: 11, f: 3.14})
+ r.addH(hSample{st: 20, t: 21, h: h1})
it := r.iterator()
require.Equal(t, chunkenc.ValFloat, it.Next())
ts, f := it.At()
- require.Equal(t, int64(1), ts)
+ require.Equal(t, int64(11), ts)
require.Equal(t, 3.14, f)
+ require.Equal(t, int64(10), it.AtST())
require.Equal(t, chunkenc.ValHistogram, it.Next())
var h *histogram.Histogram
ts, h = it.AtHistogram()
- require.Equal(t, int64(2), ts)
+ require.Equal(t, int64(21), ts)
require.Equal(t, h1, h)
+ require.Equal(t, int64(20), it.AtST())
require.Equal(t, chunkenc.ValNone, it.Next())
r.reset()
it = r.iterator()
require.Equal(t, chunkenc.ValNone, it.Next())
- r.addF(fSample{t: 3, f: 4.2})
- r.addH(hSample{t: 4, h: h2})
+ r.addF(fSample{st: 30, t: 31, f: 4.2})
+ r.addH(hSample{st: 40, t: 41, h: h2})
it = r.iterator()
require.Equal(t, chunkenc.ValFloat, it.Next())
ts, f = it.At()
- require.Equal(t, int64(3), ts)
+ require.Equal(t, int64(31), ts)
require.Equal(t, 4.2, f)
+ require.Equal(t, int64(30), it.AtST())
require.Equal(t, chunkenc.ValHistogram, it.Next())
ts, h = it.AtHistogram()
- require.Equal(t, int64(4), ts)
+ require.Equal(t, int64(41), ts)
require.Equal(t, h2, h)
+ require.Equal(t, int64(40), it.AtST())
require.Equal(t, chunkenc.ValNone, it.Next())
}
@@ -160,44 +181,50 @@ func TestSampleRingAtFloatHistogram(t *testing.T) {
it := r.iterator()
require.Equal(t, chunkenc.ValNone, it.Next())
- r.addFH(fhSample{t: 1, fh: fh1})
- r.addFH(fhSample{t: 2, fh: fh2})
+ r.addFH(fhSample{st: 10, t: 11, fh: fh1})
+ r.addFH(fhSample{st: 20, t: 21, fh: fh2})
it = r.iterator()
require.Equal(t, chunkenc.ValFloatHistogram, it.Next())
ts, fh = it.AtFloatHistogram(fh)
- require.Equal(t, int64(1), ts)
+ require.Equal(t, int64(11), ts)
require.Equal(t, fh1, fh)
+ require.Equal(t, int64(10), it.AtST())
require.Equal(t, chunkenc.ValFloatHistogram, it.Next())
ts, fh = it.AtFloatHistogram(fh)
- require.Equal(t, int64(2), ts)
+ require.Equal(t, int64(21), ts)
require.Equal(t, fh2, fh)
+ require.Equal(t, int64(20), it.AtST())
require.Equal(t, chunkenc.ValNone, it.Next())
r.reset()
it = r.iterator()
require.Equal(t, chunkenc.ValNone, it.Next())
- r.addH(hSample{t: 3, h: h1})
- r.addH(hSample{t: 4, h: h2})
+ r.addH(hSample{st: 30, t: 31, h: h1})
+ r.addH(hSample{st: 40, t: 41, h: h2})
it = r.iterator()
require.Equal(t, chunkenc.ValHistogram, it.Next())
ts, h = it.AtHistogram()
- require.Equal(t, int64(3), ts)
+ require.Equal(t, int64(31), ts)
require.Equal(t, h1, h)
+ require.Equal(t, int64(30), it.AtST())
ts, fh = it.AtFloatHistogram(fh)
- require.Equal(t, int64(3), ts)
+ require.Equal(t, int64(31), ts)
require.Equal(t, h1.ToFloat(nil), fh)
+ require.Equal(t, int64(30), it.AtST())
require.Equal(t, chunkenc.ValHistogram, it.Next())
ts, h = it.AtHistogram()
- require.Equal(t, int64(4), ts)
+ require.Equal(t, int64(41), ts)
require.Equal(t, h2, h)
+ require.Equal(t, int64(40), it.AtST())
ts, fh = it.AtFloatHistogram(fh)
- require.Equal(t, int64(4), ts)
+ require.Equal(t, int64(41), ts)
require.Equal(t, h2.ToFloat(nil), fh)
+ require.Equal(t, int64(40), it.AtST())
require.Equal(t, chunkenc.ValNone, it.Next())
}
@@ -209,59 +236,63 @@ func TestBufferedSeriesIterator(t *testing.T) {
bit := it.Buffer()
for bit.Next() == chunkenc.ValFloat {
t, f := bit.At()
- b = append(b, fSample{t: t, f: f})
+ st := bit.AtST()
+ b = append(b, fSample{st: st, t: t, f: f})
}
require.Equal(t, exp, b, "buffer mismatch")
}
- sampleEq := func(ets int64, ev float64) {
+ sampleEq := func(est, ets int64, ev float64) {
ts, v := it.At()
+ st := it.AtST()
+ require.Equal(t, est, st, "start timestamp mismatch")
require.Equal(t, ets, ts, "timestamp mismatch")
require.Equal(t, ev, v, "value mismatch")
}
- prevSampleEq := func(ets int64, ev float64, eok bool) {
+ prevSampleEq := func(est, ets int64, ev float64, eok bool) {
s, ok := it.PeekBack(1)
require.Equal(t, eok, ok, "exist mismatch")
+ require.Equal(t, est, s.ST(), "start timestamp mismatch")
require.Equal(t, ets, s.T(), "timestamp mismatch")
require.Equal(t, ev, s.F(), "value mismatch")
}
it = NewBufferIterator(NewListSeriesIterator(samples{
- fSample{t: 1, f: 2},
- fSample{t: 2, f: 3},
- fSample{t: 3, f: 4},
- fSample{t: 4, f: 5},
- fSample{t: 5, f: 6},
- fSample{t: 99, f: 8},
- fSample{t: 100, f: 9},
- fSample{t: 101, f: 10},
+ fSample{st: -1, t: 1, f: 2},
+ fSample{st: 1, t: 2, f: 3},
+ fSample{st: 2, t: 3, f: 4},
+ fSample{st: 3, t: 4, f: 5},
+ fSample{st: 3, t: 5, f: 6},
+ fSample{st: 50, t: 99, f: 8},
+ fSample{st: 99, t: 100, f: 9},
+ fSample{st: 100, t: 101, f: 10},
}), 2)
require.Equal(t, chunkenc.ValFloat, it.Seek(-123), "seek failed")
- sampleEq(1, 2)
- prevSampleEq(0, 0, false)
+ sampleEq(-1, 1, 2)
+ prevSampleEq(0, 0, 0, false)
bufferEq(nil)
require.Equal(t, chunkenc.ValFloat, it.Next(), "next failed")
- sampleEq(2, 3)
- prevSampleEq(1, 2, true)
- bufferEq([]fSample{{t: 1, f: 2}})
+ sampleEq(1, 2, 3)
+ prevSampleEq(-1, 1, 2, true)
+ bufferEq([]fSample{{st: -1, t: 1, f: 2}})
require.Equal(t, chunkenc.ValFloat, it.Next(), "next failed")
require.Equal(t, chunkenc.ValFloat, it.Next(), "next failed")
require.Equal(t, chunkenc.ValFloat, it.Next(), "next failed")
- sampleEq(5, 6)
- prevSampleEq(4, 5, true)
- bufferEq([]fSample{{t: 2, f: 3}, {t: 3, f: 4}, {t: 4, f: 5}})
+ sampleEq(3, 5, 6)
+ prevSampleEq(3, 4, 5, true)
+ bufferEq([]fSample{{st: 1, t: 2, f: 3}, {st: 2, t: 3, f: 4}, {st: 3, t: 4, f: 5}})
require.Equal(t, chunkenc.ValFloat, it.Seek(5), "seek failed")
- sampleEq(5, 6)
- prevSampleEq(4, 5, true)
- bufferEq([]fSample{{t: 2, f: 3}, {t: 3, f: 4}, {t: 4, f: 5}})
+ sampleEq(3, 5, 6)
+ prevSampleEq(3, 4, 5, true)
+ bufferEq([]fSample{{st: 1, t: 2, f: 3}, {st: 2, t: 3, f: 4}, {st: 3, t: 4, f: 5}})
require.Equal(t, chunkenc.ValFloat, it.Seek(101), "seek failed")
- sampleEq(101, 10)
- prevSampleEq(100, 9, true)
- bufferEq([]fSample{{t: 99, f: 8}, {t: 100, f: 9}})
+ sampleEq(100, 101, 10)
+ prevSampleEq(99, 100, 9, true)
+ bufferEq([]fSample{{st: 50, t: 99, f: 8}, {st: 99, t: 100, f: 9}})
require.Equal(t, chunkenc.ValNone, it.Next(), "next succeeded unexpectedly")
require.Equal(t, chunkenc.ValNone, it.Seek(1024), "seek succeeded unexpectedly")
@@ -402,6 +433,10 @@ func (*mockSeriesIterator) AtT() int64 {
return 0 // Not really mocked.
}
+func (*mockSeriesIterator) AtST() int64 {
+ return 0 // Not really mocked.
+}
+
type fakeSeriesIterator struct {
nsamples int64
step int64
@@ -428,6 +463,10 @@ func (it *fakeSeriesIterator) AtT() int64 {
return it.idx * it.step
}
+func (*fakeSeriesIterator) AtST() int64 {
+ return 0 // No start timestamps in this fake iterator.
+}
+
func (it *fakeSeriesIterator) Next() chunkenc.ValueType {
it.idx++
if it.idx >= it.nsamples {
diff --git a/storage/fanout.go b/storage/fanout.go
index 246a955b73..9baa31d9af 100644
--- a/storage/fanout.go
+++ b/storage/fanout.go
@@ -15,6 +15,7 @@ package storage
import (
"context"
+ "errors"
"log/slog"
"github.com/prometheus/common/model"
@@ -23,7 +24,6 @@ import (
"github.com/prometheus/prometheus/model/histogram"
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/model/metadata"
- tsdb_errors "github.com/prometheus/prometheus/tsdb/errors"
)
type fanout struct {
@@ -82,11 +82,14 @@ func (f *fanout) Querier(mint, maxt int64) (Querier, error) {
querier, err := storage.Querier(mint, maxt)
if err != nil {
// Close already open Queriers, append potential errors to returned error.
- errs := tsdb_errors.NewMulti(err, primary.Close())
- for _, q := range secondaries {
- errs.Add(q.Close())
+ errs := []error{
+ err,
+ primary.Close(),
}
- return nil, errs.Err()
+ for _, q := range secondaries {
+ errs = append(errs, q.Close())
+ }
+ return nil, errors.Join(errs...)
}
if _, ok := querier.(noopQuerier); !ok {
secondaries = append(secondaries, querier)
@@ -106,11 +109,14 @@ func (f *fanout) ChunkQuerier(mint, maxt int64) (ChunkQuerier, error) {
querier, err := storage.ChunkQuerier(mint, maxt)
if err != nil {
// Close already open Queriers, append potential errors to returned error.
- errs := tsdb_errors.NewMulti(err, primary.Close())
- for _, q := range secondaries {
- errs.Add(q.Close())
+ errs := []error{
+ err,
+ primary.Close(),
}
- return nil, errs.Err()
+ for _, q := range secondaries {
+ errs = append(errs, q.Close())
+ }
+ return nil, errors.Join(errs...)
}
secondaries = append(secondaries, querier)
}
@@ -130,13 +136,28 @@ func (f *fanout) Appender(ctx context.Context) Appender {
}
}
+func (f *fanout) AppenderV2(ctx context.Context) AppenderV2 {
+ primary := f.primary.AppenderV2(ctx)
+ secondaries := make([]AppenderV2, 0, len(f.secondaries))
+ for _, storage := range f.secondaries {
+ secondaries = append(secondaries, storage.AppenderV2(ctx))
+ }
+ return &fanoutAppenderV2{
+ logger: f.logger,
+ primary: primary,
+ secondaries: secondaries,
+ }
+}
+
// Close closes the storage and all its underlying resources.
func (f *fanout) Close() error {
- errs := tsdb_errors.NewMulti(f.primary.Close())
- for _, s := range f.secondaries {
- errs.Add(s.Close())
+ errs := []error{
+ f.primary.Close(),
}
- return errs.Err()
+ for _, s := range f.secondaries {
+ errs = append(errs, s.Close())
+ }
+ return errors.Join(errs...)
}
// fanoutAppender implements Appender.
@@ -268,5 +289,59 @@ func (f *fanoutAppender) Rollback() (err error) {
f.logger.Error("Squashed rollback error on rollback", "err", rollbackErr)
}
}
- return nil
+ return err
+}
+
+type fanoutAppenderV2 struct {
+ logger *slog.Logger
+
+ primary AppenderV2
+ secondaries []AppenderV2
+}
+
+func (f *fanoutAppenderV2) Append(ref SeriesRef, l labels.Labels, st, t int64, v float64, h *histogram.Histogram, fh *histogram.FloatHistogram, opts AOptions) (SeriesRef, error) {
+ ref, err := f.primary.Append(ref, l, st, t, v, h, fh, opts)
+ var partialErr AppendPartialError
+ if partialErr.Handle(err) != nil {
+ return ref, err
+ }
+
+ for _, appender := range f.secondaries {
+ if _, err := appender.Append(ref, l, st, t, v, h, fh, opts); err != nil {
+ if partialErr.Handle(err) != nil {
+ return ref, err
+ }
+ }
+ }
+ return ref, partialErr.ErrOrNil()
+}
+
+func (f *fanoutAppenderV2) Commit() (err error) {
+ err = f.primary.Commit()
+
+ for _, appender := range f.secondaries {
+ if err == nil {
+ err = appender.Commit()
+ } else {
+ if rollbackErr := appender.Rollback(); rollbackErr != nil {
+ f.logger.Error("Squashed rollback error on commit", "err", rollbackErr)
+ }
+ }
+ }
+ return err
+}
+
+func (f *fanoutAppenderV2) Rollback() (err error) {
+ err = f.primary.Rollback()
+
+ for _, appender := range f.secondaries {
+ rollbackErr := appender.Rollback()
+ switch {
+ case err == nil:
+ err = rollbackErr
+ case rollbackErr != nil:
+ f.logger.Error("Squashed rollback error on rollback", "err", rollbackErr)
+ }
+ }
+ return err
}
diff --git a/storage/fanout_test.go b/storage/fanout_test.go
index ed4cf17696..25f61341cd 100644
--- a/storage/fanout_test.go
+++ b/storage/fanout_test.go
@@ -21,11 +21,14 @@ import (
"github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
+ "github.com/prometheus/prometheus/model/exemplar"
"github.com/prometheus/prometheus/model/labels"
"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/teststorage"
+ "github.com/prometheus/prometheus/util/testutil"
)
func TestFanout_SelectSorted(t *testing.T) {
@@ -132,6 +135,115 @@ func TestFanout_SelectSorted(t *testing.T) {
})
}
+func TestFanout_SelectSorted_AppenderV2(t *testing.T) {
+ inputLabel := labels.FromStrings(model.MetricNameLabel, "a")
+ outputLabel := labels.FromStrings(model.MetricNameLabel, "a")
+
+ inputTotalSize := 0
+
+ priStorage := teststorage.New(t)
+ defer priStorage.Close()
+ app1 := priStorage.AppenderV2(t.Context())
+ _, err := app1.Append(0, inputLabel, 0, 0, 0, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ inputTotalSize++
+ _, err = app1.Append(0, inputLabel, 0, 1000, 1, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ inputTotalSize++
+ _, err = app1.Append(0, inputLabel, 0, 2000, 2, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ inputTotalSize++
+ require.NoError(t, app1.Commit())
+
+ remoteStorage1 := teststorage.New(t)
+ defer remoteStorage1.Close()
+ app2 := remoteStorage1.AppenderV2(t.Context())
+ _, err = app2.Append(0, inputLabel, 0, 3000, 3, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ inputTotalSize++
+ _, err = app2.Append(0, inputLabel, 0, 4000, 4, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ inputTotalSize++
+ _, err = app2.Append(0, inputLabel, 0, 5000, 5, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ inputTotalSize++
+ require.NoError(t, app2.Commit())
+
+ remoteStorage2 := teststorage.New(t)
+ defer remoteStorage2.Close()
+
+ app3 := remoteStorage2.AppenderV2(t.Context())
+ _, err = app3.Append(0, inputLabel, 0, 6000, 6, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ inputTotalSize++
+ _, err = app3.Append(0, inputLabel, 0, 7000, 7, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ inputTotalSize++
+ _, err = app3.Append(0, inputLabel, 0, 8000, 8, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ inputTotalSize++
+
+ require.NoError(t, app3.Commit())
+
+ fanoutStorage := storage.NewFanout(nil, priStorage, remoteStorage1, remoteStorage2)
+
+ t.Run("querier", func(t *testing.T) {
+ querier, err := fanoutStorage.Querier(0, 8000)
+ require.NoError(t, err)
+ defer querier.Close()
+
+ matcher, err := labels.NewMatcher(labels.MatchEqual, model.MetricNameLabel, "a")
+ require.NoError(t, err)
+
+ seriesSet := querier.Select(t.Context(), true, nil, matcher)
+
+ result := make(map[int64]float64)
+ var labelsResult labels.Labels
+ var iterator chunkenc.Iterator
+ for seriesSet.Next() {
+ series := seriesSet.At()
+ seriesLabels := series.Labels()
+ labelsResult = seriesLabels
+ iterator := series.Iterator(iterator)
+ for iterator.Next() == chunkenc.ValFloat {
+ timestamp, value := iterator.At()
+ result[timestamp] = value
+ }
+ }
+
+ require.Equal(t, labelsResult, outputLabel)
+ require.Len(t, result, inputTotalSize)
+ })
+ t.Run("chunk querier", func(t *testing.T) {
+ querier, err := fanoutStorage.ChunkQuerier(0, 8000)
+ require.NoError(t, err)
+ defer querier.Close()
+
+ matcher, err := labels.NewMatcher(labels.MatchEqual, model.MetricNameLabel, "a")
+ require.NoError(t, err)
+
+ seriesSet := storage.NewSeriesSetFromChunkSeriesSet(querier.Select(t.Context(), true, nil, matcher))
+
+ result := make(map[int64]float64)
+ var labelsResult labels.Labels
+ var iterator chunkenc.Iterator
+ for seriesSet.Next() {
+ series := seriesSet.At()
+ seriesLabels := series.Labels()
+ labelsResult = seriesLabels
+ iterator := series.Iterator(iterator)
+ for iterator.Next() == chunkenc.ValFloat {
+ timestamp, value := iterator.At()
+ result[timestamp] = value
+ }
+ }
+
+ require.NoError(t, seriesSet.Err())
+ require.Equal(t, labelsResult, outputLabel)
+ require.Len(t, result, inputTotalSize)
+ })
+}
+
func TestFanoutErrors(t *testing.T) {
workingStorage := teststorage.New(t)
defer workingStorage.Close()
@@ -224,9 +336,10 @@ type errChunkQuerier struct{ errQuerier }
func (errStorage) ChunkQuerier(_, _ int64) (storage.ChunkQuerier, error) {
return errChunkQuerier{}, nil
}
-func (errStorage) Appender(context.Context) storage.Appender { return nil }
-func (errStorage) StartTime() (int64, error) { return 0, nil }
-func (errStorage) Close() error { return nil }
+func (errStorage) Appender(context.Context) storage.Appender { return nil }
+func (errStorage) AppenderV2(context.Context) storage.AppenderV2 { return nil }
+func (errStorage) StartTime() (int64, error) { return 0, nil }
+func (errStorage) Close() error { return nil }
func (errQuerier) Select(context.Context, bool, *storage.SelectHints, ...*labels.Matcher) storage.SeriesSet {
return storage.ErrSeriesSet(errSelect)
@@ -245,3 +358,216 @@ func (errQuerier) Close() error { return nil }
func (errChunkQuerier) Select(context.Context, bool, *storage.SelectHints, ...*labels.Matcher) storage.ChunkSeriesSet {
return storage.ErrChunkSeriesSet(errSelect)
}
+
+type mockStorage struct {
+ app storage.Appendable
+ appV2 storage.AppendableV2
+ storage.Storage
+}
+
+func (m mockStorage) Appender(ctx context.Context) storage.Appender {
+ return m.app.Appender(ctx)
+}
+
+func (m mockStorage) AppenderV2(ctx context.Context) storage.AppenderV2 {
+ return m.appV2.AppenderV2(ctx)
+}
+
+type sample = teststorage.Sample
+
+func withoutExemplars(s []sample) (ret []sample) {
+ ret = make([]sample, len(s))
+ copy(ret, s)
+ for i := range ret {
+ ret[i].ES = nil
+ }
+ return ret
+}
+
+type fanoutAppenderTestCase struct {
+ name string
+ primary *teststorage.Appendable
+ secondary *teststorage.Appendable
+
+ expectAppendErr bool
+ expectExemplarError bool
+ expectCommitError bool
+
+ expectPrimarySamples []sample
+ expectSecondarySamples []sample
+}
+
+func fanoutAppenderTestCases(expected []sample) []fanoutAppenderTestCase {
+ appErr := errors.New("append test error")
+ exErr := errors.New("exemplar test error")
+ commitErr := errors.New("commit test error")
+
+ return []fanoutAppenderTestCase{
+ {
+ name: "both works",
+ primary: teststorage.NewAppendable(),
+ secondary: teststorage.NewAppendable(),
+
+ expectPrimarySamples: expected,
+ expectSecondarySamples: expected,
+ },
+ {
+ name: "primary errors",
+ primary: teststorage.NewAppendable().WithErrs(func(labels.Labels) error { return appErr }, exErr, commitErr),
+ secondary: teststorage.NewAppendable(),
+
+ expectAppendErr: true,
+ expectExemplarError: true,
+ expectCommitError: true,
+ },
+ {
+ name: "exemplar errors",
+ primary: teststorage.NewAppendable().WithErrs(func(labels.Labels) error { return nil }, exErr, nil),
+ secondary: teststorage.NewAppendable().WithErrs(func(labels.Labels) error { return nil }, exErr, nil),
+
+ expectAppendErr: false,
+ expectExemplarError: true,
+ expectCommitError: false,
+
+ expectPrimarySamples: withoutExemplars(expected),
+ expectSecondarySamples: withoutExemplars(expected),
+ },
+ {
+ name: "secondary errors",
+ primary: teststorage.NewAppendable(),
+ secondary: teststorage.NewAppendable().WithErrs(func(labels.Labels) error { return appErr }, exErr, commitErr),
+
+ expectAppendErr: true,
+ expectExemplarError: true,
+ expectCommitError: true,
+
+ expectPrimarySamples: expected,
+ },
+ }
+}
+
+func TestFanoutAppender(t *testing.T) {
+ h := tsdbutil.GenerateTestHistogram(0)
+ fh := tsdbutil.GenerateTestFloatHistogram(0)
+ ex := exemplar.Exemplar{Value: 1}
+
+ expected := []sample{
+ {L: labels.FromStrings(model.MetricNameLabel, "metric1"), V: 1, ES: []exemplar.Exemplar{ex}},
+ {L: labels.FromStrings(model.MetricNameLabel, "metric2"), T: 1, H: h},
+ {L: labels.FromStrings(model.MetricNameLabel, "metric3"), T: 2, FH: fh},
+ }
+ for _, tt := range fanoutAppenderTestCases(expected) {
+ t.Run(tt.name, func(t *testing.T) {
+ f := storage.NewFanout(nil, mockStorage{app: tt.primary}, mockStorage{app: tt.secondary})
+
+ app := f.Appender(t.Context())
+ ref, err := app.Append(0, labels.FromStrings(model.MetricNameLabel, "metric1"), 0, 1)
+ if tt.expectAppendErr {
+ require.Error(t, err)
+ } else {
+ require.NoError(t, err)
+ }
+
+ _, err = app.AppendExemplar(ref, labels.FromStrings(model.MetricNameLabel, "metric1"), ex)
+ if tt.expectExemplarError {
+ require.Error(t, err)
+ } else {
+ require.NoError(t, err)
+ }
+
+ _, err = app.AppendHistogram(0, labels.FromStrings(model.MetricNameLabel, "metric2"), 1, h, nil)
+ if tt.expectAppendErr {
+ require.Error(t, err)
+ } else {
+ require.NoError(t, err)
+ }
+
+ _, err = app.AppendHistogram(0, labels.FromStrings(model.MetricNameLabel, "metric3"), 2, nil, fh)
+ if tt.expectAppendErr {
+ require.Error(t, err)
+ } else {
+ require.NoError(t, err)
+ }
+
+ err = app.Commit()
+ if tt.expectCommitError {
+ require.Error(t, err)
+ } else {
+ require.NoError(t, err)
+ }
+
+ require.Nil(t, tt.primary.PendingSamples())
+ testutil.RequireEqual(t, tt.expectPrimarySamples, tt.primary.ResultSamples())
+ require.Nil(t, tt.primary.RolledbackSamples())
+
+ require.Nil(t, tt.secondary.PendingSamples())
+ testutil.RequireEqual(t, tt.expectSecondarySamples, tt.secondary.ResultSamples())
+ require.Nil(t, tt.secondary.RolledbackSamples())
+ })
+ }
+}
+
+func TestFanoutAppenderV2(t *testing.T) {
+ h := tsdbutil.GenerateTestHistogram(0)
+ fh := tsdbutil.GenerateTestFloatHistogram(0)
+ ex := exemplar.Exemplar{Value: 1}
+
+ expected := []sample{
+ {L: labels.FromStrings(model.MetricNameLabel, "metric1"), ST: -1, V: 1, ES: []exemplar.Exemplar{ex}},
+ {L: labels.FromStrings(model.MetricNameLabel, "metric2"), ST: -2, T: 1, H: h},
+ {L: labels.FromStrings(model.MetricNameLabel, "metric3"), ST: -3, T: 2, FH: fh},
+ }
+
+ for _, tt := range fanoutAppenderTestCases(expected) {
+ t.Run(tt.name, func(t *testing.T) {
+ f := storage.NewFanout(nil, mockStorage{appV2: tt.primary}, mockStorage{appV2: tt.secondary})
+
+ app := f.AppenderV2(t.Context())
+ _, err := app.Append(0, labels.FromStrings(model.MetricNameLabel, "metric1"), -1, 0, 1, nil, nil, storage.AOptions{
+ Exemplars: []exemplar.Exemplar{ex},
+ })
+ switch {
+ case tt.expectAppendErr:
+ require.Error(t, err)
+ case tt.expectExemplarError:
+ var pErr *storage.AppendPartialError
+ require.ErrorAs(t, err, &pErr)
+ // One for primary, one for secondary.
+ // This is because in V2 flow we must append sample even when first append partially failed with exemplars.
+ // Filtering out exemplars is neither feasible, nor important.
+ require.Len(t, pErr.ExemplarErrors, 2)
+ default:
+ require.NoError(t, err)
+ }
+
+ _, err = app.Append(0, labels.FromStrings(model.MetricNameLabel, "metric2"), -2, 1, 0, h, nil, storage.AOptions{})
+ if tt.expectAppendErr {
+ require.Error(t, err)
+ } else {
+ require.NoError(t, err)
+ }
+
+ _, err = app.Append(0, labels.FromStrings(model.MetricNameLabel, "metric3"), -3, 2, 0, nil, fh, storage.AOptions{})
+ if tt.expectAppendErr {
+ require.Error(t, err)
+ } else {
+ require.NoError(t, err)
+ }
+
+ err = app.Commit()
+ if tt.expectCommitError {
+ require.Error(t, err)
+ } else {
+ require.NoError(t, err)
+ }
+
+ require.Nil(t, tt.primary.PendingSamples())
+ testutil.RequireEqual(t, tt.expectPrimarySamples, tt.primary.ResultSamples())
+ require.Nil(t, tt.primary.RolledbackSamples())
+
+ require.Nil(t, tt.secondary.PendingSamples())
+ testutil.RequireEqual(t, tt.expectSecondarySamples, tt.secondary.ResultSamples())
+ require.Nil(t, tt.secondary.RolledbackSamples())
+ })
+ }
+}
diff --git a/storage/interface.go b/storage/interface.go
index 23b8b48a0c..d15ba547c8 100644
--- a/storage/interface.go
+++ b/storage/interface.go
@@ -61,7 +61,8 @@ type SeriesRef uint64
// Appendable allows creating Appender.
//
-// WARNING: Work AppendableV2 is in progress. Appendable will be removed soon (ETA: Q2 2026).
+// WARNING(bwplotka): Switch to AppendableV2 is in progress (https://github.com/prometheus/prometheus/issues/17632).
+// Appendable will be removed soon (ETA: Q2 2026).
type Appendable interface {
// Appender returns a new appender for the storage.
//
@@ -77,10 +78,16 @@ type SampleAndChunkQueryable interface {
}
// Storage ingests and manages samples, along with various indexes. All methods
-// are goroutine-safe. Storage implements storage.Appender.
+// are goroutine-safe.
type Storage interface {
SampleAndChunkQueryable
+
+ // Appendable allows appending to storage.
+ // WARNING(bwplotka): Switch to AppendableV2 is in progress (https://github.com/prometheus/prometheus/issues/17632).
+ // Appendable will be removed soon (ETA: Q2 2026).
Appendable
+ // AppendableV2 allows appending to storage.
+ AppendableV2
// StartTime returns the oldest timestamp stored in the storage.
StartTime() (int64, error)
@@ -261,7 +268,8 @@ func (f QueryableFunc) Querier(mint, maxt int64) (Querier, error) {
// AppendOptions provides options for implementations of the Appender interface.
//
-// WARNING: Work AppendableV2 is in progress. Appendable will be removed soon (ETA: Q2 2026).
+// WARNING(bwplotka): Switch to AppendableV2 is in progress (https://github.com/prometheus/prometheus/issues/17632).
+// AppendOptions will be removed soon (ETA: Q2 2026).
type AppendOptions struct {
// DiscardOutOfOrder tells implementation that this append should not be out
// of order. An OOO append MUST be rejected with storage.ErrOutOfOrderSample
@@ -278,7 +286,8 @@ type AppendOptions struct {
// I.e. timestamp order within batch is not validated, samples are not reordered per timestamp or by float/histogram
// type.
//
-// WARNING: Work AppendableV2 is in progress. Appendable will be removed soon (ETA: Q2 2026).
+// WARNING(bwplotka): Switch to AppendableV2 is in progress (https://github.com/prometheus/prometheus/issues/17632).
+// Appender will be removed soon (ETA: Q2 2026).
type Appender interface {
AppenderTransaction
@@ -315,7 +324,8 @@ type GetRef interface {
// ExemplarAppender provides an interface for adding samples to exemplar storage, which
// within Prometheus is in-memory only.
//
-// WARNING: Work AppendableV2 is in progress. Appendable will be removed soon (ETA: Q2 2026).
+// WARNING(bwplotka): Switch to AppendableV2 is in progress (https://github.com/prometheus/prometheus/issues/17632).
+// ExemplarAppender will be removed soon (ETA: Q2 2026).
type ExemplarAppender interface {
// AppendExemplar adds an exemplar for the given series labels.
// An optional reference number can be provided to accelerate calls.
@@ -333,7 +343,8 @@ type ExemplarAppender interface {
// HistogramAppender provides an interface for appending histograms to the storage.
//
-// WARNING: Work AppendableV2 is in progress. Appendable will be removed soon (ETA: Q2 2026).
+// WARNING(bwplotka): Switch to AppendableV2 is in progress (https://github.com/prometheus/prometheus/issues/17632).
+// HistogramAppender will be removed soon (ETA: Q2 2026).
type HistogramAppender interface {
// AppendHistogram adds a histogram for the given series labels. An
// optional reference number can be provided to accelerate calls. A
@@ -365,7 +376,8 @@ type HistogramAppender interface {
// MetadataUpdater provides an interface for associating metadata to stored series.
//
-// WARNING: Work AppendableV2 is in progress. Appendable will be removed soon (ETA: Q2 2026).
+// WARNING(bwplotka): Switch to AppendableV2 is in progress (https://github.com/prometheus/prometheus/issues/17632).
+// MetadataUpdater will be removed soon (ETA: Q2 2026).
type MetadataUpdater interface {
// UpdateMetadata updates a metadata entry for the given series and labels.
// A series reference number is returned which can be used to modify the
@@ -379,7 +391,8 @@ type MetadataUpdater interface {
// StartTimestampAppender provides an interface for appending ST to storage.
//
-// WARNING: Work AppendableV2 is in progress. Appendable will be removed soon (ETA: Q2 2026).
+// WARNING(bwplotka): Switch to AppendableV2 is in progress (https://github.com/prometheus/prometheus/issues/17632).
+// StartTimestampAppender will be removed soon (ETA: Q2 2026).
type StartTimestampAppender interface {
// AppendSTZeroSample adds synthetic zero sample for the given st timestamp,
// which will be associated with given series, labels and the incoming
@@ -473,9 +486,10 @@ type Series interface {
}
type mockSeries struct {
- timestamps []int64
- values []float64
- labelSet []string
+ startTimestamps []int64
+ timestamps []int64
+ values []float64
+ labelSet []string
}
func (s mockSeries) Labels() labels.Labels {
@@ -483,15 +497,19 @@ func (s mockSeries) Labels() labels.Labels {
}
func (s mockSeries) Iterator(chunkenc.Iterator) chunkenc.Iterator {
- return chunkenc.MockSeriesIterator(s.timestamps, s.values)
+ return chunkenc.MockSeriesIterator(s.startTimestamps, s.timestamps, s.values)
}
-// MockSeries returns a series with custom timestamps, values and labelSet.
-func MockSeries(timestamps []int64, values []float64, labelSet []string) Series {
+// MockSeries returns a series with custom start timestamp, timestamps, values,
+// and labelSet.
+// Start timestamps is optional, pass nil or empty slice to indicate no start
+// timestamps.
+func MockSeries(startTimestamps, timestamps []int64, values []float64, labelSet []string) Series {
return mockSeries{
- timestamps: timestamps,
- values: values,
- labelSet: labelSet,
+ startTimestamps: startTimestamps,
+ timestamps: timestamps,
+ values: values,
+ labelSet: labelSet,
}
}
diff --git a/storage/interface_append.go b/storage/interface_append.go
index cc7045dbd5..aa4ae84152 100644
--- a/storage/interface_append.go
+++ b/storage/interface_append.go
@@ -69,6 +69,7 @@ type AppendV2Options struct {
// Exemplars (optional) attached to the appended sample.
// Exemplar slice MUST be sorted by Exemplar.TS.
// Exemplar slice is unsafe for reuse.
+ // Duplicate exemplars errors MUST be ignored by implementations.
Exemplars []exemplar.Exemplar
// RejectOutOfOrder tells implementation that this append should not be out
@@ -96,6 +97,31 @@ func (e *AppendPartialError) Error() string {
return errs.Error()
}
+// ErrOrNil returns AppendPartialError as error, returning nil
+// if there are no errors.
+func (e *AppendPartialError) ErrOrNil() error {
+ if len(e.ExemplarErrors) == 0 {
+ return nil
+ }
+ return e
+}
+
+// Handle handles the given err that may be an AppendPartialError.
+// If the err is nil or not an AppendPartialError it returns err.
+// Otherwise, partial errors are aggregated.
+func (e *AppendPartialError) Handle(err error) error {
+ if err == nil {
+ return nil
+ }
+
+ var pErr *AppendPartialError
+ if !errors.As(err, &pErr) {
+ return err
+ }
+ e.ExemplarErrors = append(e.ExemplarErrors, pErr.ExemplarErrors...)
+ return nil
+}
+
var _ error = &AppendPartialError{}
// AppenderV2 provides appends against a storage for all types of samples.
diff --git a/storage/interface_test.go b/storage/interface_test.go
index d28e5177e3..3ea4b757e7 100644
--- a/storage/interface_test.go
+++ b/storage/interface_test.go
@@ -23,7 +23,7 @@ import (
)
func TestMockSeries(t *testing.T) {
- s := storage.MockSeries([]int64{1, 2, 3}, []float64{1, 2, 3}, []string{"__name__", "foo"})
+ s := storage.MockSeries(nil, []int64{1, 2, 3}, []float64{1, 2, 3}, []string{"__name__", "foo"})
it := s.Iterator(nil)
ts := []int64{}
vs := []float64{}
@@ -35,3 +35,20 @@ func TestMockSeries(t *testing.T) {
require.Equal(t, []int64{1, 2, 3}, ts)
require.Equal(t, []float64{1, 2, 3}, vs)
}
+
+func TestMockSeriesWithST(t *testing.T) {
+ s := storage.MockSeries([]int64{0, 1, 2}, []int64{1, 2, 3}, []float64{1, 2, 3}, []string{"__name__", "foo"})
+ it := s.Iterator(nil)
+ ts := []int64{}
+ vs := []float64{}
+ st := []int64{}
+ for it.Next() == chunkenc.ValFloat {
+ t, v := it.At()
+ ts = append(ts, t)
+ vs = append(vs, v)
+ st = append(st, it.AtST())
+ }
+ require.Equal(t, []int64{1, 2, 3}, ts)
+ require.Equal(t, []float64{1, 2, 3}, vs)
+ require.Equal(t, []int64{0, 1, 2}, st)
+}
diff --git a/storage/merge.go b/storage/merge.go
index a86a26891f..76bf0994e0 100644
--- a/storage/merge.go
+++ b/storage/merge.go
@@ -17,6 +17,7 @@ import (
"bytes"
"container/heap"
"context"
+ "errors"
"fmt"
"math"
"sync"
@@ -25,7 +26,6 @@ import (
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/tsdb/chunkenc"
"github.com/prometheus/prometheus/tsdb/chunks"
- tsdb_errors "github.com/prometheus/prometheus/tsdb/errors"
"github.com/prometheus/prometheus/util/annotations"
)
@@ -269,13 +269,13 @@ func (q *mergeGenericQuerier) LabelNames(ctx context.Context, hints *LabelHints,
// Close releases the resources of the generic querier.
func (q *mergeGenericQuerier) Close() error {
- errs := tsdb_errors.NewMulti()
+ var errs []error
for _, querier := range q.queriers {
if err := querier.Close(); err != nil {
- errs.Add(err)
+ errs = append(errs, err)
}
}
- return errs.Err()
+ return errors.Join(errs...)
}
func truncateToLimit(s []string, hints *LabelHints) []string {
@@ -599,6 +599,13 @@ func (c *chainSampleIterator) AtT() int64 {
return c.curr.AtT()
}
+func (c *chainSampleIterator) AtST() int64 {
+ if c.curr == nil {
+ panic("chainSampleIterator.AtST called before first .Next or after .Next returned false.")
+ }
+ return c.curr.AtST()
+}
+
func (c *chainSampleIterator) Next() chunkenc.ValueType {
var (
currT int64
@@ -679,11 +686,11 @@ func (c *chainSampleIterator) Next() chunkenc.ValueType {
}
func (c *chainSampleIterator) Err() error {
- errs := tsdb_errors.NewMulti()
+ var errs []error
for _, iter := range c.iterators {
- errs.Add(iter.Err())
+ errs = append(errs, iter.Err())
}
- return errs.Err()
+ return errors.Join(errs...)
}
type samplesIteratorHeap []chunkenc.Iterator
@@ -821,12 +828,12 @@ func (c *compactChunkIterator) Next() bool {
}
func (c *compactChunkIterator) Err() error {
- errs := tsdb_errors.NewMulti()
+ var errs []error
for _, iter := range c.iterators {
- errs.Add(iter.Err())
+ errs = append(errs, iter.Err())
}
- errs.Add(c.err)
- return errs.Err()
+ errs = append(errs, c.err)
+ return errors.Join(errs...)
}
type chunkIteratorHeap []chunks.Iterator
@@ -904,9 +911,9 @@ func (c *concatenatingChunkIterator) Next() bool {
}
func (c *concatenatingChunkIterator) Err() error {
- errs := tsdb_errors.NewMulti()
+ var errs []error
for _, iter := range c.iterators {
- errs.Add(iter.Err())
+ errs = append(errs, iter.Err())
}
- return errs.Err()
+ return errors.Join(errs...)
}
diff --git a/storage/merge_test.go b/storage/merge_test.go
index 6e2daaeb3a..e42a6a4ce1 100644
--- a/storage/merge_test.go
+++ b/storage/merge_test.go
@@ -66,116 +66,116 @@ func TestMergeQuerierWithChainMerger(t *testing.T) {
{
name: "one querier, two series",
querierSeries: [][]Series{{
- NewListSeries(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{1, 1}, fSample{2, 2}, fSample{3, 3}}),
- NewListSeries(labels.FromStrings("foo", "bar"), []chunks.Sample{fSample{0, 0}, fSample{1, 1}, fSample{2, 2}}),
+ NewListSeries(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 1, 1}, fSample{0, 2, 2}, fSample{0, 3, 3}}),
+ NewListSeries(labels.FromStrings("foo", "bar"), []chunks.Sample{fSample{0, 0, 0}, fSample{0, 1, 1}, fSample{0, 2, 2}}),
}},
expected: NewMockSeriesSet(
- NewListSeries(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{1, 1}, fSample{2, 2}, fSample{3, 3}}),
- NewListSeries(labels.FromStrings("foo", "bar"), []chunks.Sample{fSample{0, 0}, fSample{1, 1}, fSample{2, 2}}),
+ NewListSeries(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 1, 1}, fSample{0, 2, 2}, fSample{0, 3, 3}}),
+ NewListSeries(labels.FromStrings("foo", "bar"), []chunks.Sample{fSample{0, 0, 0}, fSample{0, 1, 1}, fSample{0, 2, 2}}),
),
},
{
name: "two queriers, one different series each",
querierSeries: [][]Series{{
- NewListSeries(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{1, 1}, fSample{2, 2}, fSample{3, 3}}),
+ NewListSeries(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 1, 1}, fSample{0, 2, 2}, fSample{0, 3, 3}}),
}, {
- NewListSeries(labels.FromStrings("foo", "bar"), []chunks.Sample{fSample{0, 0}, fSample{1, 1}, fSample{2, 2}}),
+ NewListSeries(labels.FromStrings("foo", "bar"), []chunks.Sample{fSample{0, 0, 0}, fSample{0, 1, 1}, fSample{0, 2, 2}}),
}},
expected: NewMockSeriesSet(
- NewListSeries(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{1, 1}, fSample{2, 2}, fSample{3, 3}}),
- NewListSeries(labels.FromStrings("foo", "bar"), []chunks.Sample{fSample{0, 0}, fSample{1, 1}, fSample{2, 2}}),
+ NewListSeries(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 1, 1}, fSample{0, 2, 2}, fSample{0, 3, 3}}),
+ NewListSeries(labels.FromStrings("foo", "bar"), []chunks.Sample{fSample{0, 0, 0}, fSample{0, 1, 1}, fSample{0, 2, 2}}),
),
},
{
name: "two time unsorted queriers, two series each",
querierSeries: [][]Series{{
- NewListSeries(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{5, 5}, fSample{6, 6}}),
- NewListSeries(labels.FromStrings("foo", "bar"), []chunks.Sample{fSample{0, 0}, fSample{1, 1}, fSample{2, 2}}),
+ NewListSeries(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 5, 5}, fSample{0, 6, 6}}),
+ NewListSeries(labels.FromStrings("foo", "bar"), []chunks.Sample{fSample{0, 0, 0}, fSample{0, 1, 1}, fSample{0, 2, 2}}),
}, {
- NewListSeries(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{1, 1}, fSample{2, 2}, fSample{3, 3}}),
- NewListSeries(labels.FromStrings("foo", "bar"), []chunks.Sample{fSample{3, 3}, fSample{4, 4}}),
+ NewListSeries(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 1, 1}, fSample{0, 2, 2}, fSample{0, 3, 3}}),
+ NewListSeries(labels.FromStrings("foo", "bar"), []chunks.Sample{fSample{0, 3, 3}, fSample{0, 4, 4}}),
}},
expected: NewMockSeriesSet(
NewListSeries(
labels.FromStrings("bar", "baz"),
- []chunks.Sample{fSample{1, 1}, fSample{2, 2}, fSample{3, 3}, fSample{5, 5}, fSample{6, 6}},
+ []chunks.Sample{fSample{0, 1, 1}, fSample{0, 2, 2}, fSample{0, 3, 3}, fSample{0, 5, 5}, fSample{0, 6, 6}},
),
NewListSeries(
labels.FromStrings("foo", "bar"),
- []chunks.Sample{fSample{0, 0}, fSample{1, 1}, fSample{2, 2}, fSample{3, 3}, fSample{4, 4}},
+ []chunks.Sample{fSample{0, 0, 0}, fSample{0, 1, 1}, fSample{0, 2, 2}, fSample{0, 3, 3}, fSample{0, 4, 4}},
),
),
},
{
name: "five queriers, only two queriers have two time unsorted series each",
querierSeries: [][]Series{{}, {}, {
- NewListSeries(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{5, 5}, fSample{6, 6}}),
- NewListSeries(labels.FromStrings("foo", "bar"), []chunks.Sample{fSample{0, 0}, fSample{1, 1}, fSample{2, 2}}),
+ NewListSeries(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 5, 5}, fSample{0, 6, 6}}),
+ NewListSeries(labels.FromStrings("foo", "bar"), []chunks.Sample{fSample{0, 0, 0}, fSample{0, 1, 1}, fSample{0, 2, 2}}),
}, {
- NewListSeries(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{1, 1}, fSample{2, 2}, fSample{3, 3}}),
- NewListSeries(labels.FromStrings("foo", "bar"), []chunks.Sample{fSample{3, 3}, fSample{4, 4}}),
+ NewListSeries(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 1, 1}, fSample{0, 2, 2}, fSample{0, 3, 3}}),
+ NewListSeries(labels.FromStrings("foo", "bar"), []chunks.Sample{fSample{0, 3, 3}, fSample{0, 4, 4}}),
}, {}},
expected: NewMockSeriesSet(
NewListSeries(
labels.FromStrings("bar", "baz"),
- []chunks.Sample{fSample{1, 1}, fSample{2, 2}, fSample{3, 3}, fSample{5, 5}, fSample{6, 6}},
+ []chunks.Sample{fSample{0, 1, 1}, fSample{0, 2, 2}, fSample{0, 3, 3}, fSample{0, 5, 5}, fSample{0, 6, 6}},
),
NewListSeries(
labels.FromStrings("foo", "bar"),
- []chunks.Sample{fSample{0, 0}, fSample{1, 1}, fSample{2, 2}, fSample{3, 3}, fSample{4, 4}},
+ []chunks.Sample{fSample{0, 0, 0}, fSample{0, 1, 1}, fSample{0, 2, 2}, fSample{0, 3, 3}, fSample{0, 4, 4}},
),
),
},
{
name: "two queriers, only two queriers have two time unsorted series each, with 3 noop and one nil querier together",
querierSeries: [][]Series{{}, {}, {
- NewListSeries(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{5, 5}, fSample{6, 6}}),
- NewListSeries(labels.FromStrings("foo", "bar"), []chunks.Sample{fSample{0, 0}, fSample{1, 1}, fSample{2, 2}}),
+ NewListSeries(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 5, 5}, fSample{0, 6, 6}}),
+ NewListSeries(labels.FromStrings("foo", "bar"), []chunks.Sample{fSample{0, 0, 0}, fSample{0, 1, 1}, fSample{0, 2, 2}}),
}, {
- NewListSeries(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{1, 1}, fSample{2, 2}, fSample{3, 3}}),
- NewListSeries(labels.FromStrings("foo", "bar"), []chunks.Sample{fSample{3, 3}, fSample{4, 4}}),
+ NewListSeries(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 1, 1}, fSample{0, 2, 2}, fSample{0, 3, 3}}),
+ NewListSeries(labels.FromStrings("foo", "bar"), []chunks.Sample{fSample{0, 3, 3}, fSample{0, 4, 4}}),
}, {}},
extraQueriers: []Querier{NoopQuerier(), NoopQuerier(), nil, NoopQuerier()},
expected: NewMockSeriesSet(
NewListSeries(
labels.FromStrings("bar", "baz"),
- []chunks.Sample{fSample{1, 1}, fSample{2, 2}, fSample{3, 3}, fSample{5, 5}, fSample{6, 6}},
+ []chunks.Sample{fSample{0, 1, 1}, fSample{0, 2, 2}, fSample{0, 3, 3}, fSample{0, 5, 5}, fSample{0, 6, 6}},
),
NewListSeries(
labels.FromStrings("foo", "bar"),
- []chunks.Sample{fSample{0, 0}, fSample{1, 1}, fSample{2, 2}, fSample{3, 3}, fSample{4, 4}},
+ []chunks.Sample{fSample{0, 0, 0}, fSample{0, 1, 1}, fSample{0, 2, 2}, fSample{0, 3, 3}, fSample{0, 4, 4}},
),
),
},
{
name: "two queriers, with two series, one is overlapping",
querierSeries: [][]Series{{}, {}, {
- NewListSeries(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{2, 21}, fSample{3, 31}, fSample{5, 5}, fSample{6, 6}}),
- NewListSeries(labels.FromStrings("foo", "bar"), []chunks.Sample{fSample{0, 0}, fSample{1, 1}, fSample{2, 2}}),
+ NewListSeries(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 2, 21}, fSample{0, 3, 31}, fSample{0, 5, 5}, fSample{0, 6, 6}}),
+ NewListSeries(labels.FromStrings("foo", "bar"), []chunks.Sample{fSample{0, 0, 0}, fSample{0, 1, 1}, fSample{0, 2, 2}}),
}, {
- NewListSeries(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{1, 1}, fSample{2, 22}, fSample{3, 32}}),
- NewListSeries(labels.FromStrings("foo", "bar"), []chunks.Sample{fSample{3, 3}, fSample{4, 4}}),
+ NewListSeries(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 1, 1}, fSample{0, 2, 22}, fSample{0, 3, 32}}),
+ NewListSeries(labels.FromStrings("foo", "bar"), []chunks.Sample{fSample{0, 3, 3}, fSample{0, 4, 4}}),
}, {}},
expected: NewMockSeriesSet(
NewListSeries(
labels.FromStrings("bar", "baz"),
- []chunks.Sample{fSample{1, 1}, fSample{2, 21}, fSample{3, 31}, fSample{5, 5}, fSample{6, 6}},
+ []chunks.Sample{fSample{0, 1, 1}, fSample{0, 2, 21}, fSample{0, 3, 31}, fSample{0, 5, 5}, fSample{0, 6, 6}},
),
NewListSeries(
labels.FromStrings("foo", "bar"),
- []chunks.Sample{fSample{0, 0}, fSample{1, 1}, fSample{2, 2}, fSample{3, 3}, fSample{4, 4}},
+ []chunks.Sample{fSample{0, 0, 0}, fSample{0, 1, 1}, fSample{0, 2, 2}, fSample{0, 3, 3}, fSample{0, 4, 4}},
),
),
},
{
name: "two queries, one with NaN samples series",
querierSeries: [][]Series{{
- NewListSeries(labels.FromStrings("foo", "bar"), []chunks.Sample{fSample{0, math.NaN()}}),
+ NewListSeries(labels.FromStrings("foo", "bar"), []chunks.Sample{fSample{0, 0, math.NaN()}}),
}, {
- NewListSeries(labels.FromStrings("foo", "bar"), []chunks.Sample{fSample{1, 1}}),
+ NewListSeries(labels.FromStrings("foo", "bar"), []chunks.Sample{fSample{0, 1, 1}}),
}},
expected: NewMockSeriesSet(
- NewListSeries(labels.FromStrings("foo", "bar"), []chunks.Sample{fSample{0, math.NaN()}, fSample{1, 1}}),
+ NewListSeries(labels.FromStrings("foo", "bar"), []chunks.Sample{fSample{0, 0, math.NaN()}, fSample{0, 1, 1}}),
),
},
} {
@@ -249,108 +249,108 @@ func TestMergeChunkQuerierWithNoVerticalChunkSeriesMerger(t *testing.T) {
{
name: "one querier, two series",
chkQuerierSeries: [][]ChunkSeries{{
- NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{1, 1}, fSample{2, 2}}, []chunks.Sample{fSample{3, 3}}),
- NewListChunkSeriesFromSamples(labels.FromStrings("foo", "bar"), []chunks.Sample{fSample{0, 0}, fSample{1, 1}}, []chunks.Sample{fSample{2, 2}}),
+ NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 1, 1}, fSample{0, 2, 2}}, []chunks.Sample{fSample{0, 3, 3}}),
+ NewListChunkSeriesFromSamples(labels.FromStrings("foo", "bar"), []chunks.Sample{fSample{0, 0, 0}, fSample{0, 1, 1}}, []chunks.Sample{fSample{0, 2, 2}}),
}},
expected: NewMockChunkSeriesSet(
- NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{1, 1}, fSample{2, 2}}, []chunks.Sample{fSample{3, 3}}),
- NewListChunkSeriesFromSamples(labels.FromStrings("foo", "bar"), []chunks.Sample{fSample{0, 0}, fSample{1, 1}}, []chunks.Sample{fSample{2, 2}}),
+ NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 1, 1}, fSample{0, 2, 2}}, []chunks.Sample{fSample{0, 3, 3}}),
+ NewListChunkSeriesFromSamples(labels.FromStrings("foo", "bar"), []chunks.Sample{fSample{0, 0, 0}, fSample{0, 1, 1}}, []chunks.Sample{fSample{0, 2, 2}}),
),
},
{
name: "two secondaries, one different series each",
chkQuerierSeries: [][]ChunkSeries{{
- NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{1, 1}, fSample{2, 2}}, []chunks.Sample{fSample{3, 3}}),
+ NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 1, 1}, fSample{0, 2, 2}}, []chunks.Sample{fSample{0, 3, 3}}),
}, {
- NewListChunkSeriesFromSamples(labels.FromStrings("foo", "bar"), []chunks.Sample{fSample{0, 0}, fSample{1, 1}}, []chunks.Sample{fSample{2, 2}}),
+ NewListChunkSeriesFromSamples(labels.FromStrings("foo", "bar"), []chunks.Sample{fSample{0, 0, 0}, fSample{0, 1, 1}}, []chunks.Sample{fSample{0, 2, 2}}),
}},
expected: NewMockChunkSeriesSet(
- NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{1, 1}, fSample{2, 2}}, []chunks.Sample{fSample{3, 3}}),
- NewListChunkSeriesFromSamples(labels.FromStrings("foo", "bar"), []chunks.Sample{fSample{0, 0}, fSample{1, 1}}, []chunks.Sample{fSample{2, 2}}),
+ NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 1, 1}, fSample{0, 2, 2}}, []chunks.Sample{fSample{0, 3, 3}}),
+ NewListChunkSeriesFromSamples(labels.FromStrings("foo", "bar"), []chunks.Sample{fSample{0, 0, 0}, fSample{0, 1, 1}}, []chunks.Sample{fSample{0, 2, 2}}),
),
},
{
name: "two secondaries, two not in time order series each",
chkQuerierSeries: [][]ChunkSeries{{
- NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{5, 5}}, []chunks.Sample{fSample{6, 6}}),
- NewListChunkSeriesFromSamples(labels.FromStrings("foo", "bar"), []chunks.Sample{fSample{0, 0}, fSample{1, 1}}, []chunks.Sample{fSample{2, 2}}),
+ NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 5, 5}}, []chunks.Sample{fSample{0, 6, 6}}),
+ NewListChunkSeriesFromSamples(labels.FromStrings("foo", "bar"), []chunks.Sample{fSample{0, 0, 0}, fSample{0, 1, 1}}, []chunks.Sample{fSample{0, 2, 2}}),
}, {
- NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{1, 1}, fSample{2, 2}}, []chunks.Sample{fSample{3, 3}}),
- NewListChunkSeriesFromSamples(labels.FromStrings("foo", "bar"), []chunks.Sample{fSample{3, 3}}, []chunks.Sample{fSample{4, 4}}),
+ NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 1, 1}, fSample{0, 2, 2}}, []chunks.Sample{fSample{0, 3, 3}}),
+ NewListChunkSeriesFromSamples(labels.FromStrings("foo", "bar"), []chunks.Sample{fSample{0, 3, 3}}, []chunks.Sample{fSample{0, 4, 4}}),
}},
expected: NewMockChunkSeriesSet(
NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"),
- []chunks.Sample{fSample{1, 1}, fSample{2, 2}},
- []chunks.Sample{fSample{3, 3}},
- []chunks.Sample{fSample{5, 5}},
- []chunks.Sample{fSample{6, 6}},
+ []chunks.Sample{fSample{0, 1, 1}, fSample{0, 2, 2}},
+ []chunks.Sample{fSample{0, 3, 3}},
+ []chunks.Sample{fSample{0, 5, 5}},
+ []chunks.Sample{fSample{0, 6, 6}},
),
NewListChunkSeriesFromSamples(labels.FromStrings("foo", "bar"),
- []chunks.Sample{fSample{0, 0}, fSample{1, 1}},
- []chunks.Sample{fSample{2, 2}},
- []chunks.Sample{fSample{3, 3}},
- []chunks.Sample{fSample{4, 4}},
+ []chunks.Sample{fSample{0, 0, 0}, fSample{0, 1, 1}},
+ []chunks.Sample{fSample{0, 2, 2}},
+ []chunks.Sample{fSample{0, 3, 3}},
+ []chunks.Sample{fSample{0, 4, 4}},
),
),
},
{
name: "five secondaries, only two have two not in time order series each",
chkQuerierSeries: [][]ChunkSeries{{}, {}, {
- NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{5, 5}}, []chunks.Sample{fSample{6, 6}}),
- NewListChunkSeriesFromSamples(labels.FromStrings("foo", "bar"), []chunks.Sample{fSample{0, 0}, fSample{1, 1}}, []chunks.Sample{fSample{2, 2}}),
+ NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 5, 5}}, []chunks.Sample{fSample{0, 6, 6}}),
+ NewListChunkSeriesFromSamples(labels.FromStrings("foo", "bar"), []chunks.Sample{fSample{0, 0, 0}, fSample{0, 1, 1}}, []chunks.Sample{fSample{0, 2, 2}}),
}, {
- NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{1, 1}, fSample{2, 2}}, []chunks.Sample{fSample{3, 3}}),
- NewListChunkSeriesFromSamples(labels.FromStrings("foo", "bar"), []chunks.Sample{fSample{3, 3}}, []chunks.Sample{fSample{4, 4}}),
+ NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 1, 1}, fSample{0, 2, 2}}, []chunks.Sample{fSample{0, 3, 3}}),
+ NewListChunkSeriesFromSamples(labels.FromStrings("foo", "bar"), []chunks.Sample{fSample{0, 3, 3}}, []chunks.Sample{fSample{0, 4, 4}}),
}, {}},
expected: NewMockChunkSeriesSet(
NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"),
- []chunks.Sample{fSample{1, 1}, fSample{2, 2}},
- []chunks.Sample{fSample{3, 3}},
- []chunks.Sample{fSample{5, 5}},
- []chunks.Sample{fSample{6, 6}},
+ []chunks.Sample{fSample{0, 1, 1}, fSample{0, 2, 2}},
+ []chunks.Sample{fSample{0, 3, 3}},
+ []chunks.Sample{fSample{0, 5, 5}},
+ []chunks.Sample{fSample{0, 6, 6}},
),
NewListChunkSeriesFromSamples(labels.FromStrings("foo", "bar"),
- []chunks.Sample{fSample{0, 0}, fSample{1, 1}},
- []chunks.Sample{fSample{2, 2}},
- []chunks.Sample{fSample{3, 3}},
- []chunks.Sample{fSample{4, 4}},
+ []chunks.Sample{fSample{0, 0, 0}, fSample{0, 1, 1}},
+ []chunks.Sample{fSample{0, 2, 2}},
+ []chunks.Sample{fSample{0, 3, 3}},
+ []chunks.Sample{fSample{0, 4, 4}},
),
),
},
{
name: "two secondaries, with two not in time order series each, with 3 noop queries and one nil together",
chkQuerierSeries: [][]ChunkSeries{{
- NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{5, 5}}, []chunks.Sample{fSample{6, 6}}),
- NewListChunkSeriesFromSamples(labels.FromStrings("foo", "bar"), []chunks.Sample{fSample{0, 0}, fSample{1, 1}}, []chunks.Sample{fSample{2, 2}}),
+ NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 5, 5}}, []chunks.Sample{fSample{0, 6, 6}}),
+ NewListChunkSeriesFromSamples(labels.FromStrings("foo", "bar"), []chunks.Sample{fSample{0, 0, 0}, fSample{0, 1, 1}}, []chunks.Sample{fSample{0, 2, 2}}),
}, {
- NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{1, 1}, fSample{2, 2}}, []chunks.Sample{fSample{3, 3}}),
- NewListChunkSeriesFromSamples(labels.FromStrings("foo", "bar"), []chunks.Sample{fSample{3, 3}}, []chunks.Sample{fSample{4, 4}}),
+ NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 1, 1}, fSample{0, 2, 2}}, []chunks.Sample{fSample{0, 3, 3}}),
+ NewListChunkSeriesFromSamples(labels.FromStrings("foo", "bar"), []chunks.Sample{fSample{0, 3, 3}}, []chunks.Sample{fSample{0, 4, 4}}),
}},
extraQueriers: []ChunkQuerier{NoopChunkedQuerier(), NoopChunkedQuerier(), nil, NoopChunkedQuerier()},
expected: NewMockChunkSeriesSet(
NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"),
- []chunks.Sample{fSample{1, 1}, fSample{2, 2}},
- []chunks.Sample{fSample{3, 3}},
- []chunks.Sample{fSample{5, 5}},
- []chunks.Sample{fSample{6, 6}},
+ []chunks.Sample{fSample{0, 1, 1}, fSample{0, 2, 2}},
+ []chunks.Sample{fSample{0, 3, 3}},
+ []chunks.Sample{fSample{0, 5, 5}},
+ []chunks.Sample{fSample{0, 6, 6}},
),
NewListChunkSeriesFromSamples(labels.FromStrings("foo", "bar"),
- []chunks.Sample{fSample{0, 0}, fSample{1, 1}},
- []chunks.Sample{fSample{2, 2}},
- []chunks.Sample{fSample{3, 3}},
- []chunks.Sample{fSample{4, 4}},
+ []chunks.Sample{fSample{0, 0, 0}, fSample{0, 1, 1}},
+ []chunks.Sample{fSample{0, 2, 2}},
+ []chunks.Sample{fSample{0, 3, 3}},
+ []chunks.Sample{fSample{0, 4, 4}},
),
),
},
{
name: "two queries, one with NaN samples series",
chkQuerierSeries: [][]ChunkSeries{{
- NewListChunkSeriesFromSamples(labels.FromStrings("foo", "bar"), []chunks.Sample{fSample{0, math.NaN()}}),
+ NewListChunkSeriesFromSamples(labels.FromStrings("foo", "bar"), []chunks.Sample{fSample{0, 0, math.NaN()}}),
}, {
- NewListChunkSeriesFromSamples(labels.FromStrings("foo", "bar"), []chunks.Sample{fSample{1, 1}}),
+ NewListChunkSeriesFromSamples(labels.FromStrings("foo", "bar"), []chunks.Sample{fSample{0, 1, 1}}),
}},
expected: NewMockChunkSeriesSet(
- NewListChunkSeriesFromSamples(labels.FromStrings("foo", "bar"), []chunks.Sample{fSample{0, math.NaN()}}, []chunks.Sample{fSample{1, 1}}),
+ NewListChunkSeriesFromSamples(labels.FromStrings("foo", "bar"), []chunks.Sample{fSample{0, 0, math.NaN()}}, []chunks.Sample{fSample{0, 1, 1}}),
),
},
} {
@@ -387,13 +387,13 @@ func TestMergeChunkQuerierWithNoVerticalChunkSeriesMerger(t *testing.T) {
func histogramSample(ts int64, hint histogram.CounterResetHint) hSample {
h := tsdbutil.GenerateTestHistogram(ts + 1)
h.CounterResetHint = hint
- return hSample{t: ts, h: h}
+ return hSample{st: -ts, t: ts, h: h}
}
func floatHistogramSample(ts int64, hint histogram.CounterResetHint) fhSample {
fh := tsdbutil.GenerateTestFloatHistogram(ts + 1)
fh.CounterResetHint = hint
- return fhSample{t: ts, fh: fh}
+ return fhSample{st: -ts, t: ts, fh: fh}
}
// Shorthands for counter reset hints.
@@ -431,9 +431,9 @@ func TestCompactingChunkSeriesMerger(t *testing.T) {
{
name: "single series",
input: []ChunkSeries{
- NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{1, 1}, fSample{2, 2}}, []chunks.Sample{fSample{3, 3}}),
+ NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 1, 1}, fSample{0, 2, 2}}, []chunks.Sample{fSample{0, 3, 3}}),
},
- expected: NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{1, 1}, fSample{2, 2}}, []chunks.Sample{fSample{3, 3}}),
+ expected: NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 1, 1}, fSample{0, 2, 2}}, []chunks.Sample{fSample{0, 3, 3}}),
},
{
name: "two empty series",
@@ -446,55 +446,55 @@ func TestCompactingChunkSeriesMerger(t *testing.T) {
{
name: "two non overlapping",
input: []ChunkSeries{
- NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{1, 1}, fSample{2, 2}}, []chunks.Sample{fSample{3, 3}, fSample{5, 5}}),
- NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{7, 7}, fSample{9, 9}}, []chunks.Sample{fSample{10, 10}}),
+ NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 1, 1}, fSample{0, 2, 2}}, []chunks.Sample{fSample{0, 3, 3}, fSample{0, 5, 5}}),
+ NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 7, 7}, fSample{0, 9, 9}}, []chunks.Sample{fSample{0, 10, 10}}),
},
- expected: NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{1, 1}, fSample{2, 2}}, []chunks.Sample{fSample{3, 3}, fSample{5, 5}}, []chunks.Sample{fSample{7, 7}, fSample{9, 9}}, []chunks.Sample{fSample{10, 10}}),
+ expected: NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 1, 1}, fSample{0, 2, 2}}, []chunks.Sample{fSample{0, 3, 3}, fSample{0, 5, 5}}, []chunks.Sample{fSample{0, 7, 7}, fSample{0, 9, 9}}, []chunks.Sample{fSample{0, 10, 10}}),
},
{
name: "two overlapping",
input: []ChunkSeries{
- NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{1, 1}, fSample{2, 2}}, []chunks.Sample{fSample{3, 3}, fSample{8, 8}}),
- NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{7, 7}, fSample{9, 9}}, []chunks.Sample{fSample{10, 10}}),
+ NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 1, 1}, fSample{0, 2, 2}}, []chunks.Sample{fSample{0, 3, 3}, fSample{0, 8, 8}}),
+ NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 7, 7}, fSample{0, 9, 9}}, []chunks.Sample{fSample{0, 10, 10}}),
},
- expected: NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{1, 1}, fSample{2, 2}}, []chunks.Sample{fSample{3, 3}, fSample{7, 7}, fSample{8, 8}, fSample{9, 9}}, []chunks.Sample{fSample{10, 10}}),
+ expected: NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 1, 1}, fSample{0, 2, 2}}, []chunks.Sample{fSample{0, 3, 3}, fSample{0, 7, 7}, fSample{0, 8, 8}, fSample{0, 9, 9}}, []chunks.Sample{fSample{0, 10, 10}}),
},
{
name: "two duplicated",
input: []ChunkSeries{
- NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{1, 1}, fSample{2, 2}, fSample{3, 3}, fSample{5, 5}}),
- NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{2, 2}, fSample{3, 3}, fSample{5, 5}}),
+ NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 1, 1}, fSample{0, 2, 2}, fSample{0, 3, 3}, fSample{0, 5, 5}}),
+ NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 2, 2}, fSample{0, 3, 3}, fSample{0, 5, 5}}),
},
- expected: NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{1, 1}, fSample{2, 2}, fSample{3, 3}, fSample{5, 5}}),
+ expected: NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 1, 1}, fSample{0, 2, 2}, fSample{0, 3, 3}, fSample{0, 5, 5}}),
},
{
name: "three overlapping",
input: []ChunkSeries{
- NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{1, 1}, fSample{2, 2}, fSample{3, 3}, fSample{5, 5}}),
- NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{2, 2}, fSample{3, 3}, fSample{6, 6}}),
- NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 0}, fSample{4, 4}}),
+ NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 1, 1}, fSample{0, 2, 2}, fSample{0, 3, 3}, fSample{0, 5, 5}}),
+ NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 2, 2}, fSample{0, 3, 3}, fSample{0, 6, 6}}),
+ NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 0, 0}, fSample{0, 4, 4}}),
},
- expected: NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 0}, fSample{1, 1}, fSample{2, 2}, fSample{3, 3}, fSample{4, 4}, fSample{5, 5}, fSample{6, 6}}),
+ expected: NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 0, 0}, fSample{0, 1, 1}, fSample{0, 2, 2}, fSample{0, 3, 3}, fSample{0, 4, 4}, fSample{0, 5, 5}, fSample{0, 6, 6}}),
},
{
name: "three in chained overlap",
input: []ChunkSeries{
- NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{1, 1}, fSample{2, 2}, fSample{3, 3}, fSample{5, 5}}),
- NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{4, 4}, fSample{6, 66}}),
- NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{6, 6}, fSample{10, 10}}),
+ NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 1, 1}, fSample{0, 2, 2}, fSample{0, 3, 3}, fSample{0, 5, 5}}),
+ NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 4, 4}, fSample{0, 6, 66}}),
+ NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 6, 6}, fSample{0, 10, 10}}),
},
- expected: NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{1, 1}, fSample{2, 2}, fSample{3, 3}, fSample{4, 4}, fSample{5, 5}, fSample{6, 66}, fSample{10, 10}}),
+ expected: NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 1, 1}, fSample{0, 2, 2}, fSample{0, 3, 3}, fSample{0, 4, 4}, fSample{0, 5, 5}, fSample{0, 6, 66}, fSample{0, 10, 10}}),
},
{
name: "three in chained overlap complex",
input: []ChunkSeries{
- NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 0}, fSample{5, 5}}, []chunks.Sample{fSample{10, 10}, fSample{15, 15}}),
- NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{2, 2}, fSample{20, 20}}, []chunks.Sample{fSample{25, 25}, fSample{30, 30}}),
- NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{18, 18}, fSample{26, 26}}, []chunks.Sample{fSample{31, 31}, fSample{35, 35}}),
+ NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 0, 0}, fSample{0, 5, 5}}, []chunks.Sample{fSample{0, 10, 10}, fSample{0, 15, 15}}),
+ NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 2, 2}, fSample{0, 20, 20}}, []chunks.Sample{fSample{0, 25, 25}, fSample{0, 30, 30}}),
+ NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 18, 18}, fSample{0, 26, 26}}, []chunks.Sample{fSample{0, 31, 31}, fSample{0, 35, 35}}),
},
expected: NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"),
- []chunks.Sample{fSample{0, 0}, fSample{2, 2}, fSample{5, 5}, fSample{10, 10}, fSample{15, 15}, fSample{18, 18}, fSample{20, 20}, fSample{25, 25}, fSample{26, 26}, fSample{30, 30}},
- []chunks.Sample{fSample{31, 31}, fSample{35, 35}},
+ []chunks.Sample{fSample{0, 0, 0}, fSample{0, 2, 2}, fSample{0, 5, 5}, fSample{0, 10, 10}, fSample{0, 15, 15}, fSample{0, 18, 18}, fSample{0, 20, 20}, fSample{0, 25, 25}, fSample{0, 26, 26}, fSample{0, 30, 30}},
+ []chunks.Sample{fSample{0, 31, 31}, fSample{0, 35, 35}},
),
},
{
@@ -534,13 +534,13 @@ func TestCompactingChunkSeriesMerger(t *testing.T) {
name: "histogram chunks overlapping with float chunks",
input: []ChunkSeries{
NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{histogramSample(0), histogramSample(5)}, []chunks.Sample{histogramSample(10), histogramSample(15)}),
- NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{1, 1}, fSample{12, 12}}, []chunks.Sample{fSample{14, 14}}),
+ NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 1, 1}, fSample{0, 12, 12}}, []chunks.Sample{fSample{0, 14, 14}}),
},
expected: NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"),
[]chunks.Sample{histogramSample(0)},
- []chunks.Sample{fSample{1, 1}},
+ []chunks.Sample{fSample{0, 1, 1}},
[]chunks.Sample{histogramSample(5), histogramSample(10)},
- []chunks.Sample{fSample{12, 12}, fSample{14, 14}},
+ []chunks.Sample{fSample{0, 12, 12}, fSample{0, 14, 14}},
[]chunks.Sample{histogramSample(15)},
),
},
@@ -560,13 +560,13 @@ func TestCompactingChunkSeriesMerger(t *testing.T) {
name: "float histogram chunks overlapping with float chunks",
input: []ChunkSeries{
NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{floatHistogramSample(0), floatHistogramSample(5)}, []chunks.Sample{floatHistogramSample(10), floatHistogramSample(15)}),
- NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{1, 1}, fSample{12, 12}}, []chunks.Sample{fSample{14, 14}}),
+ NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 1, 1}, fSample{0, 12, 12}}, []chunks.Sample{fSample{0, 14, 14}}),
},
expected: NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"),
[]chunks.Sample{floatHistogramSample(0)},
- []chunks.Sample{fSample{1, 1}},
+ []chunks.Sample{fSample{0, 1, 1}},
[]chunks.Sample{floatHistogramSample(5), floatHistogramSample(10)},
- []chunks.Sample{fSample{12, 12}, fSample{14, 14}},
+ []chunks.Sample{fSample{0, 12, 12}, fSample{0, 14, 14}},
[]chunks.Sample{floatHistogramSample(15)},
),
},
@@ -736,9 +736,9 @@ func TestConcatenatingChunkSeriesMerger(t *testing.T) {
{
name: "single series",
input: []ChunkSeries{
- NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{1, 1}, fSample{2, 2}}, []chunks.Sample{fSample{3, 3}}),
+ NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 1, 1}, fSample{0, 2, 2}}, []chunks.Sample{fSample{0, 3, 3}}),
},
- expected: NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{1, 1}, fSample{2, 2}}, []chunks.Sample{fSample{3, 3}}),
+ expected: NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 1, 1}, fSample{0, 2, 2}}, []chunks.Sample{fSample{0, 3, 3}}),
},
{
name: "two empty series",
@@ -751,70 +751,70 @@ func TestConcatenatingChunkSeriesMerger(t *testing.T) {
{
name: "two non overlapping",
input: []ChunkSeries{
- NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{1, 1}, fSample{2, 2}}, []chunks.Sample{fSample{3, 3}, fSample{5, 5}}),
- NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{7, 7}, fSample{9, 9}}, []chunks.Sample{fSample{10, 10}}),
+ NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 1, 1}, fSample{0, 2, 2}}, []chunks.Sample{fSample{0, 3, 3}, fSample{0, 5, 5}}),
+ NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 7, 7}, fSample{0, 9, 9}}, []chunks.Sample{fSample{0, 10, 10}}),
},
- expected: NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{1, 1}, fSample{2, 2}}, []chunks.Sample{fSample{3, 3}, fSample{5, 5}}, []chunks.Sample{fSample{7, 7}, fSample{9, 9}}, []chunks.Sample{fSample{10, 10}}),
+ expected: NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 1, 1}, fSample{0, 2, 2}}, []chunks.Sample{fSample{0, 3, 3}, fSample{0, 5, 5}}, []chunks.Sample{fSample{0, 7, 7}, fSample{0, 9, 9}}, []chunks.Sample{fSample{0, 10, 10}}),
},
{
name: "two overlapping",
input: []ChunkSeries{
- NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{1, 1}, fSample{2, 2}}, []chunks.Sample{fSample{3, 3}, fSample{8, 8}}),
- NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{7, 7}, fSample{9, 9}}, []chunks.Sample{fSample{10, 10}}),
+ NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 1, 1}, fSample{0, 2, 2}}, []chunks.Sample{fSample{0, 3, 3}, fSample{0, 8, 8}}),
+ NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 7, 7}, fSample{0, 9, 9}}, []chunks.Sample{fSample{0, 10, 10}}),
},
expected: NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"),
- []chunks.Sample{fSample{1, 1}, fSample{2, 2}}, []chunks.Sample{fSample{3, 3}, fSample{8, 8}},
- []chunks.Sample{fSample{7, 7}, fSample{9, 9}}, []chunks.Sample{fSample{10, 10}},
+ []chunks.Sample{fSample{0, 1, 1}, fSample{0, 2, 2}}, []chunks.Sample{fSample{0, 3, 3}, fSample{0, 8, 8}},
+ []chunks.Sample{fSample{0, 7, 7}, fSample{0, 9, 9}}, []chunks.Sample{fSample{0, 10, 10}},
),
},
{
name: "two duplicated",
input: []ChunkSeries{
- NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{1, 1}, fSample{2, 2}, fSample{3, 3}, fSample{5, 5}}),
- NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{2, 2}, fSample{3, 3}, fSample{5, 5}}),
+ NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 1, 1}, fSample{0, 2, 2}, fSample{0, 3, 3}, fSample{0, 5, 5}}),
+ NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 2, 2}, fSample{0, 3, 3}, fSample{0, 5, 5}}),
},
expected: NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"),
- []chunks.Sample{fSample{1, 1}, fSample{2, 2}, fSample{3, 3}, fSample{5, 5}},
- []chunks.Sample{fSample{2, 2}, fSample{3, 3}, fSample{5, 5}},
+ []chunks.Sample{fSample{0, 1, 1}, fSample{0, 2, 2}, fSample{0, 3, 3}, fSample{0, 5, 5}},
+ []chunks.Sample{fSample{0, 2, 2}, fSample{0, 3, 3}, fSample{0, 5, 5}},
),
},
{
name: "three overlapping",
input: []ChunkSeries{
- NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{1, 1}, fSample{2, 2}, fSample{3, 3}, fSample{5, 5}}),
- NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{2, 2}, fSample{3, 3}, fSample{6, 6}}),
- NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 0}, fSample{4, 4}}),
+ NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 1, 1}, fSample{0, 2, 2}, fSample{0, 3, 3}, fSample{0, 5, 5}}),
+ NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 2, 2}, fSample{0, 3, 3}, fSample{0, 6, 6}}),
+ NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 0, 0}, fSample{0, 4, 4}}),
},
expected: NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"),
- []chunks.Sample{fSample{1, 1}, fSample{2, 2}, fSample{3, 3}, fSample{5, 5}},
- []chunks.Sample{fSample{2, 2}, fSample{3, 3}, fSample{6, 6}},
- []chunks.Sample{fSample{0, 0}, fSample{4, 4}},
+ []chunks.Sample{fSample{0, 1, 1}, fSample{0, 2, 2}, fSample{0, 3, 3}, fSample{0, 5, 5}},
+ []chunks.Sample{fSample{0, 2, 2}, fSample{0, 3, 3}, fSample{0, 6, 6}},
+ []chunks.Sample{fSample{0, 0, 0}, fSample{0, 4, 4}},
),
},
{
name: "three in chained overlap",
input: []ChunkSeries{
- NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{1, 1}, fSample{2, 2}, fSample{3, 3}, fSample{5, 5}}),
- NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{4, 4}, fSample{6, 66}}),
- NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{6, 6}, fSample{10, 10}}),
+ NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 1, 1}, fSample{0, 2, 2}, fSample{0, 3, 3}, fSample{0, 5, 5}}),
+ NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 4, 4}, fSample{0, 6, 66}}),
+ NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 6, 6}, fSample{0, 10, 10}}),
},
expected: NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"),
- []chunks.Sample{fSample{1, 1}, fSample{2, 2}, fSample{3, 3}, fSample{5, 5}},
- []chunks.Sample{fSample{4, 4}, fSample{6, 66}},
- []chunks.Sample{fSample{6, 6}, fSample{10, 10}},
+ []chunks.Sample{fSample{0, 1, 1}, fSample{0, 2, 2}, fSample{0, 3, 3}, fSample{0, 5, 5}},
+ []chunks.Sample{fSample{0, 4, 4}, fSample{0, 6, 66}},
+ []chunks.Sample{fSample{0, 6, 6}, fSample{0, 10, 10}},
),
},
{
name: "three in chained overlap complex",
input: []ChunkSeries{
- NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 0}, fSample{5, 5}}, []chunks.Sample{fSample{10, 10}, fSample{15, 15}}),
- NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{2, 2}, fSample{20, 20}}, []chunks.Sample{fSample{25, 25}, fSample{30, 30}}),
- NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{18, 18}, fSample{26, 26}}, []chunks.Sample{fSample{31, 31}, fSample{35, 35}}),
+ NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 0, 0}, fSample{0, 5, 5}}, []chunks.Sample{fSample{0, 10, 10}, fSample{0, 15, 15}}),
+ NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 2, 2}, fSample{0, 20, 20}}, []chunks.Sample{fSample{0, 25, 25}, fSample{0, 30, 30}}),
+ NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"), []chunks.Sample{fSample{0, 18, 18}, fSample{0, 26, 26}}, []chunks.Sample{fSample{0, 31, 31}, fSample{0, 35, 35}}),
},
expected: NewListChunkSeriesFromSamples(labels.FromStrings("bar", "baz"),
- []chunks.Sample{fSample{0, 0}, fSample{5, 5}}, []chunks.Sample{fSample{10, 10}, fSample{15, 15}},
- []chunks.Sample{fSample{2, 2}, fSample{20, 20}}, []chunks.Sample{fSample{25, 25}, fSample{30, 30}},
- []chunks.Sample{fSample{18, 18}, fSample{26, 26}}, []chunks.Sample{fSample{31, 31}, fSample{35, 35}},
+ []chunks.Sample{fSample{0, 0, 0}, fSample{0, 5, 5}}, []chunks.Sample{fSample{0, 10, 10}, fSample{0, 15, 15}},
+ []chunks.Sample{fSample{0, 2, 2}, fSample{0, 20, 20}}, []chunks.Sample{fSample{0, 25, 25}, fSample{0, 30, 30}},
+ []chunks.Sample{fSample{0, 18, 18}, fSample{0, 26, 26}}, []chunks.Sample{fSample{0, 31, 31}, fSample{0, 35, 35}},
),
},
{
@@ -1059,7 +1059,7 @@ func (*mockChunkSeriesSet) Warnings() annotations.Annotations { return nil }
func TestChainSampleIterator(t *testing.T) {
for sampleType, sampleFunc := range map[string]func(int64) chunks.Sample{
- "float": func(ts int64) chunks.Sample { return fSample{ts, float64(ts)} },
+ "float": func(ts int64) chunks.Sample { return fSample{-ts, ts, float64(ts)} },
"histogram": func(ts int64) chunks.Sample { return histogramSample(ts, uk) },
"float histogram": func(ts int64) chunks.Sample { return floatHistogramSample(ts, uk) },
} {
@@ -1176,7 +1176,7 @@ func TestChainSampleIteratorHistogramCounterResetHint(t *testing.T) {
func TestChainSampleIteratorSeek(t *testing.T) {
for sampleType, sampleFunc := range map[string]func(int64) chunks.Sample{
- "float": func(ts int64) chunks.Sample { return fSample{ts, float64(ts)} },
+ "float": func(ts int64) chunks.Sample { return fSample{-ts, ts, float64(ts)} },
"histogram": func(ts int64) chunks.Sample { return histogramSample(ts, uk) },
"float histogram": func(ts int64) chunks.Sample { return floatHistogramSample(ts, uk) },
} {
@@ -1224,13 +1224,13 @@ func TestChainSampleIteratorSeek(t *testing.T) {
switch merged.Seek(tc.seek) {
case chunkenc.ValFloat:
t, f := merged.At()
- actual = append(actual, fSample{t, f})
+ actual = append(actual, fSample{merged.AtST(), t, f})
case chunkenc.ValHistogram:
t, h := merged.AtHistogram(nil)
- actual = append(actual, hSample{t, h})
+ actual = append(actual, hSample{merged.AtST(), t, h})
case chunkenc.ValFloatHistogram:
t, fh := merged.AtFloatHistogram(nil)
- actual = append(actual, fhSample{t, fh})
+ actual = append(actual, fhSample{merged.AtST(), t, fh})
}
s, err := ExpandSamples(merged, nil)
require.NoError(t, err)
@@ -1243,7 +1243,7 @@ func TestChainSampleIteratorSeek(t *testing.T) {
func TestChainSampleIteratorSeekFailingIterator(t *testing.T) {
merged := ChainSampleIteratorFromIterators(nil, []chunkenc.Iterator{
- NewListSeriesIterator(samples{fSample{0, 0.1}, fSample{1, 1.1}, fSample{2, 2.1}}),
+ NewListSeriesIterator(samples{fSample{0, 0, 0.1}, fSample{0, 1, 1.1}, fSample{0, 2, 2.1}}),
errIterator{errors.New("something went wrong")},
})
@@ -1253,7 +1253,7 @@ func TestChainSampleIteratorSeekFailingIterator(t *testing.T) {
func TestChainSampleIteratorNextImmediatelyFailingIterator(t *testing.T) {
merged := ChainSampleIteratorFromIterators(nil, []chunkenc.Iterator{
- NewListSeriesIterator(samples{fSample{0, 0.1}, fSample{1, 1.1}, fSample{2, 2.1}}),
+ NewListSeriesIterator(samples{fSample{0, 0, 0.1}, fSample{0, 1, 1.1}, fSample{0, 2, 2.1}}),
errIterator{errors.New("something went wrong")},
})
@@ -1263,7 +1263,7 @@ func TestChainSampleIteratorNextImmediatelyFailingIterator(t *testing.T) {
// Next() does some special handling for the first iterator, so make sure it handles the first iterator returning an error too.
merged = ChainSampleIteratorFromIterators(nil, []chunkenc.Iterator{
errIterator{errors.New("something went wrong")},
- NewListSeriesIterator(samples{fSample{0, 0.1}, fSample{1, 1.1}, fSample{2, 2.1}}),
+ NewListSeriesIterator(samples{fSample{0, 0, 0.1}, fSample{0, 1, 1.1}, fSample{0, 2, 2.1}}),
})
require.Equal(t, chunkenc.ValNone, merged.Next())
@@ -1310,13 +1310,13 @@ func TestChainSampleIteratorSeekHistogramCounterResetHint(t *testing.T) {
switch merged.Seek(tc.seek) {
case chunkenc.ValFloat:
t, f := merged.At()
- actual = append(actual, fSample{t, f})
+ actual = append(actual, fSample{merged.AtST(), t, f})
case chunkenc.ValHistogram:
t, h := merged.AtHistogram(nil)
- actual = append(actual, hSample{t, h})
+ actual = append(actual, hSample{merged.AtST(), t, h})
case chunkenc.ValFloatHistogram:
t, fh := merged.AtFloatHistogram(nil)
- actual = append(actual, fhSample{t, fh})
+ actual = append(actual, fhSample{merged.AtST(), t, fh})
}
s, err := ExpandSamples(merged, nil)
require.NoError(t, err)
@@ -1716,6 +1716,10 @@ func (errIterator) AtT() int64 {
return 0
}
+func (errIterator) AtST() int64 {
+ return 0
+}
+
func (e errIterator) Err() error {
return e.err
}
diff --git a/storage/remote/codec.go b/storage/remote/codec.go
index 9f0fb7d92a..c689a51164 100644
--- a/storage/remote/codec.go
+++ b/storage/remote/codec.go
@@ -564,6 +564,12 @@ func (c *concreteSeriesIterator) AtT() int64 {
return c.series.floats[c.floatsCur].Timestamp
}
+// TODO(krajorama): implement AtST. Maybe. concreteSeriesIterator is used
+// for turning query results into an iterable, but query results do not have ST.
+func (*concreteSeriesIterator) AtST() int64 {
+ return 0
+}
+
const noTS = int64(math.MaxInt64)
// Next implements chunkenc.Iterator.
@@ -832,6 +838,11 @@ func (it *chunkedSeriesIterator) AtT() int64 {
return it.cur.AtT()
}
+// TODO(krajorama): test AtST once we have a chunk format that provides ST.
+func (it *chunkedSeriesIterator) AtST() int64 {
+ return it.cur.AtST()
+}
+
func (it *chunkedSeriesIterator) Err() error {
return it.err
}
diff --git a/storage/remote/codec_test.go b/storage/remote/codec_test.go
index e6e7813c7b..5da8c8176c 100644
--- a/storage/remote/codec_test.go
+++ b/storage/remote/codec_test.go
@@ -1146,7 +1146,7 @@ func buildTestChunks(t *testing.T) []prompb.Chunk {
minTimeMs := time
for j := range numSamplesPerTestChunk {
- a.Append(time, float64(i+j))
+ a.Append(0, time, float64(i+j))
time += int64(1000)
}
diff --git a/storage/remote/otlptranslator/prometheusremotewrite/helper.go b/storage/remote/otlptranslator/prometheusremotewrite/helper.go
index 7e3c9d5021..11f2eec6fd 100644
--- a/storage/remote/otlptranslator/prometheusremotewrite/helper.go
+++ b/storage/remote/otlptranslator/prometheusremotewrite/helper.go
@@ -19,6 +19,7 @@ package prometheusremotewrite
import (
"context"
"encoding/hex"
+ "errors"
"fmt"
"log"
"math"
@@ -32,7 +33,7 @@ import (
"github.com/prometheus/otlptranslator"
"go.opentelemetry.io/collector/pdata/pcommon"
"go.opentelemetry.io/collector/pdata/pmetric"
- conventions "go.opentelemetry.io/collector/semconv/v1.6.1"
+ semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
"github.com/prometheus/prometheus/model/exemplar"
"github.com/prometheus/prometheus/model/labels"
@@ -63,15 +64,14 @@ const (
// createAttributes creates a slice of Prometheus Labels with OTLP attributes and pairs of string values.
// Unpaired string values are ignored. String pairs overwrite OTLP labels if collisions happen and
// if logOnOverwrite is true, the overwrite is logged. Resulting label names are sanitized.
-// If settings.PromoteResourceAttributes is not empty, it's a set of resource attributes that should be promoted to labels.
-func (c *PrometheusConverter) createAttributes(resource pcommon.Resource, attributes pcommon.Map, scope scope, settings Settings,
+//
+// This function requires for cached resource and scope labels to be set up first.
+func (c *PrometheusConverter) createAttributes(attributes pcommon.Map, settings Settings,
ignoreAttrs []string, logOnOverwrite bool, meta Metadata, extras ...string,
) (labels.Labels, error) {
- resourceAttrs := resource.Attributes()
- serviceName, haveServiceName := resourceAttrs.Get(conventions.AttributeServiceName)
- instance, haveInstanceID := resourceAttrs.Get(conventions.AttributeServiceInstanceID)
-
- promoteScope := settings.PromoteScopeMetadata && scope.name != ""
+ if c.resourceLabels == nil {
+ return labels.EmptyLabels(), errors.New("createAttributes called without initializing resource context")
+ }
// Ensure attributes are sorted by key for consistent merging of keys which
// collide when sanitized.
@@ -88,12 +88,6 @@ func (c *PrometheusConverter) createAttributes(resource pcommon.Resource, attrib
c.scratchBuilder.Sort()
sortedLabels := c.scratchBuilder.Labels()
- labelNamer := otlptranslator.LabelNamer{
- UTF8Allowed: settings.AllowUTF8,
- UnderscoreLabelSanitization: settings.LabelNameUnderscoreSanitization,
- PreserveMultipleUnderscores: settings.LabelNamePreserveMultipleUnderscores,
- }
-
if settings.AllowUTF8 {
// UTF8 is allowed, so conflicts aren't possible.
c.builder.Reset(sortedLabels)
@@ -106,7 +100,7 @@ func (c *PrometheusConverter) createAttributes(resource pcommon.Resource, attrib
if sortErr != nil {
return
}
- finalKey, err := labelNamer.Build(l.Name)
+ finalKey, err := c.buildLabelName(l.Name)
if err != nil {
sortErr = err
return
@@ -122,28 +116,36 @@ func (c *PrometheusConverter) createAttributes(resource pcommon.Resource, attrib
}
}
- err := settings.PromoteResourceAttributes.addPromotedAttributes(c.builder, resourceAttrs, labelNamer)
- if err != nil {
- return labels.EmptyLabels(), err
- }
- if promoteScope {
- var rangeErr error
- scope.attributes.Range(func(k string, v pcommon.Value) bool {
- name, err := labelNamer.Build("otel_scope_" + k)
- if err != nil {
- rangeErr = err
- return false
+ if settings.PromoteResourceAttributes != nil {
+ // Merge cached promoted resource labels.
+ c.resourceLabels.promotedLabels.Range(func(l labels.Label) {
+ if c.builder.Get(l.Name) == "" {
+ c.builder.Set(l.Name, l.Value)
}
- c.builder.Set(name, v.AsString())
- return true
})
- if rangeErr != nil {
- return labels.EmptyLabels(), rangeErr
+ }
+ // Merge cached job/instance labels.
+ if c.resourceLabels.jobLabel != "" {
+ c.builder.Set(model.JobLabel, c.resourceLabels.jobLabel)
+ }
+ if c.resourceLabels.instanceLabel != "" {
+ c.builder.Set(model.InstanceLabel, c.resourceLabels.instanceLabel)
+ }
+ // Merge cached external labels.
+ for key, value := range c.resourceLabels.externalLabels {
+ if c.builder.Get(key) == "" {
+ c.builder.Set(key, value)
}
- // Scope Name, Version and Schema URL are added after attributes to ensure they are not overwritten by attributes.
- c.builder.Set("otel_scope_name", scope.name)
- c.builder.Set("otel_scope_version", scope.version)
- c.builder.Set("otel_scope_schema_url", scope.schemaURL)
+ }
+
+ if c.scopeLabels != nil {
+ // Merge cached scope labels if scope promotion is enabled.
+ c.scopeLabels.scopeAttrs.Range(func(l labels.Label) {
+ c.builder.Set(l.Name, l.Value)
+ })
+ c.builder.Set("otel_scope_name", c.scopeLabels.scopeName)
+ c.builder.Set("otel_scope_version", c.scopeLabels.scopeVersion)
+ c.builder.Set("otel_scope_schema_url", c.scopeLabels.scopeSchemaURL)
}
if settings.EnableTypeAndUnitLabels {
@@ -156,27 +158,6 @@ func (c *PrometheusConverter) createAttributes(resource pcommon.Resource, attrib
}
}
- // Map service.name + service.namespace to job.
- if haveServiceName {
- val := serviceName.AsString()
- if serviceNamespace, ok := resourceAttrs.Get(conventions.AttributeServiceNamespace); ok {
- val = fmt.Sprintf("%s/%s", serviceNamespace.AsString(), val)
- }
- c.builder.Set(model.JobLabel, val)
- }
- // Map service.instance.id to instance.
- if haveInstanceID {
- c.builder.Set(model.InstanceLabel, instance.AsString())
- }
- for key, value := range settings.ExternalLabels {
- // External labels have already been sanitized.
- if existingValue := c.builder.Get(key); existingValue != "" {
- // Skip external labels if they are overridden by metric attributes.
- continue
- }
- c.builder.Set(key, value)
- }
-
for i := 0; i < len(extras); i += 2 {
if i+1 >= len(extras) {
break
@@ -189,7 +170,7 @@ func (c *PrometheusConverter) createAttributes(resource pcommon.Resource, attrib
// internal labels should be maintained.
if len(name) <= 4 || name[:2] != "__" || name[len(name)-2:] != "__" {
var err error
- name, err = labelNamer.Build(name)
+ name, err = c.buildLabelName(name)
if err != nil {
return labels.EmptyLabels(), err
}
@@ -223,7 +204,7 @@ func aggregationTemporality(metric pmetric.Metric) (pmetric.AggregationTemporali
// However, work is under way to resolve this shortcoming through a feature called native histograms custom buckets:
// https://github.com/prometheus/prometheus/issues/13485.
func (c *PrometheusConverter) addHistogramDataPoints(ctx context.Context, dataPoints pmetric.HistogramDataPointSlice,
- resource pcommon.Resource, settings Settings, scope scope, meta Metadata,
+ settings Settings, meta Metadata,
) error {
for x := 0; x < dataPoints.Len(); x++ {
if err := c.everyN.checkContext(ctx); err != nil {
@@ -233,7 +214,7 @@ func (c *PrometheusConverter) addHistogramDataPoints(ctx context.Context, dataPo
pt := dataPoints.At(x)
timestamp := convertTimeStamp(pt.Timestamp())
startTimestamp := convertTimeStamp(pt.StartTimestamp())
- baseLabels, err := c.createAttributes(resource, pt.Attributes(), scope, settings, nil, false, meta)
+ baseLabels, err := c.createAttributes(pt.Attributes(), settings, nil, false, meta)
if err != nil {
return err
}
@@ -424,8 +405,8 @@ func findMinAndMaxTimestamps(metric pmetric.Metric, minTimestamp, maxTimestamp p
return minTimestamp, maxTimestamp
}
-func (c *PrometheusConverter) addSummaryDataPoints(ctx context.Context, dataPoints pmetric.SummaryDataPointSlice, resource pcommon.Resource,
- settings Settings, scope scope, meta Metadata,
+func (c *PrometheusConverter) addSummaryDataPoints(ctx context.Context, dataPoints pmetric.SummaryDataPointSlice,
+ settings Settings, meta Metadata,
) error {
for x := 0; x < dataPoints.Len(); x++ {
if err := c.everyN.checkContext(ctx); err != nil {
@@ -435,7 +416,7 @@ func (c *PrometheusConverter) addSummaryDataPoints(ctx context.Context, dataPoin
pt := dataPoints.At(x)
timestamp := convertTimeStamp(pt.Timestamp())
startTimestamp := convertTimeStamp(pt.StartTimestamp())
- baseLabels, err := c.createAttributes(resource, pt.Attributes(), scope, settings, nil, false, meta)
+ baseLabels, err := c.createAttributes(pt.Attributes(), settings, nil, false, meta)
if err != nil {
return err
}
@@ -504,9 +485,9 @@ func (c *PrometheusConverter) addResourceTargetInfo(resource pcommon.Resource, s
attributes := resource.Attributes()
identifyingAttrs := []string{
- conventions.AttributeServiceNamespace,
- conventions.AttributeServiceName,
- conventions.AttributeServiceInstanceID,
+ string(semconv.ServiceNamespaceKey),
+ string(semconv.ServiceNameKey),
+ string(semconv.ServiceInstanceIDKey),
}
nonIdentifyingAttrsCount := attributes.Len()
for _, a := range identifyingAttrs {
@@ -538,7 +519,12 @@ func (c *PrometheusConverter) addResourceTargetInfo(resource pcommon.Resource, s
MetricFamilyName: name,
}
// TODO: should target info have the __type__ metadata label?
- lbls, err := c.createAttributes(resource, attributes, scope{}, settings, identifyingAttrs, false, Metadata{}, model.MetricNameLabel, name)
+ // target_info is a resource-level metric and should not include scope labels.
+ // Temporarily clear scope labels for this call.
+ savedScopeLabels := c.scopeLabels
+ c.scopeLabels = nil
+ lbls, err := c.createAttributes(attributes, settings, identifyingAttrs, false, Metadata{}, model.MetricNameLabel, name)
+ c.scopeLabels = savedScopeLabels
if err != nil {
return err
}
diff --git a/storage/remote/otlptranslator/prometheusremotewrite/helper_test.go b/storage/remote/otlptranslator/prometheusremotewrite/helper_test.go
index b06bf3d416..c549667dde 100644
--- a/storage/remote/otlptranslator/prometheusremotewrite/helper_test.go
+++ b/storage/remote/otlptranslator/prometheusremotewrite/helper_test.go
@@ -413,7 +413,11 @@ func TestCreateAttributes(t *testing.T) {
if tc.attrs != (pcommon.Map{}) {
testAttrs = tc.attrs
}
- lbls, err := c.createAttributes(testResource, testAttrs, tc.scope, settings, tc.ignoreAttrs, false, Metadata{}, model.MetricNameLabel, "test_metric")
+ // Initialize resource and scope context as FromMetrics would.
+ require.NoError(t, c.setResourceContext(testResource, settings))
+ require.NoError(t, c.setScopeContext(tc.scope, settings))
+
+ lbls, err := c.createAttributes(testAttrs, settings, tc.ignoreAttrs, false, Metadata{}, model.MetricNameLabel, "test_metric")
require.NoError(t, err)
testutil.RequireEqual(t, tc.expectedLabels, lbls)
@@ -643,15 +647,19 @@ func TestPrometheusConverter_AddSummaryDataPoints(t *testing.T) {
metric := tt.metric()
mockAppender := &mockCombinedAppender{}
converter := NewPrometheusConverter(mockAppender)
+ settings := Settings{
+ PromoteScopeMetadata: tt.promoteScope,
+ }
+ resource := pcommon.NewResource()
+
+ // Initialize resource and scope context as FromMetrics would.
+ require.NoError(t, converter.setResourceContext(resource, settings))
+ require.NoError(t, converter.setScopeContext(tt.scope, settings))
converter.addSummaryDataPoints(
context.Background(),
metric.Summary().DataPoints(),
- pcommon.NewResource(),
- Settings{
- PromoteScopeMetadata: tt.promoteScope,
- },
- tt.scope,
+ settings,
Metadata{
MetricFamilyName: metric.Name(),
},
@@ -806,15 +814,19 @@ func TestPrometheusConverter_AddHistogramDataPoints(t *testing.T) {
metric := tt.metric()
mockAppender := &mockCombinedAppender{}
converter := NewPrometheusConverter(mockAppender)
+ settings := Settings{
+ PromoteScopeMetadata: tt.promoteScope,
+ }
+ resource := pcommon.NewResource()
+
+ // Initialize resource and scope context as FromMetrics would.
+ require.NoError(t, converter.setResourceContext(resource, settings))
+ require.NoError(t, converter.setScopeContext(tt.scope, settings))
converter.addHistogramDataPoints(
context.Background(),
metric.Histogram().DataPoints(),
- pcommon.NewResource(),
- Settings{
- PromoteScopeMetadata: tt.promoteScope,
- },
- tt.scope,
+ settings,
Metadata{
MetricFamilyName: metric.Name(),
},
diff --git a/storage/remote/otlptranslator/prometheusremotewrite/histograms.go b/storage/remote/otlptranslator/prometheusremotewrite/histograms.go
index db7c0e1275..dd873c41bd 100644
--- a/storage/remote/otlptranslator/prometheusremotewrite/histograms.go
+++ b/storage/remote/otlptranslator/prometheusremotewrite/histograms.go
@@ -22,7 +22,6 @@ import (
"math"
"github.com/prometheus/common/model"
- "go.opentelemetry.io/collector/pdata/pcommon"
"go.opentelemetry.io/collector/pdata/pmetric"
"github.com/prometheus/prometheus/model/histogram"
@@ -35,8 +34,7 @@ const defaultZeroThreshold = 1e-128
// addExponentialHistogramDataPoints adds OTel exponential histogram data points to the corresponding time series
// as native histogram samples.
func (c *PrometheusConverter) addExponentialHistogramDataPoints(ctx context.Context, dataPoints pmetric.ExponentialHistogramDataPointSlice,
- resource pcommon.Resource, settings Settings, temporality pmetric.AggregationTemporality,
- scope scope, meta Metadata,
+ settings Settings, temporality pmetric.AggregationTemporality, meta Metadata,
) (annotations.Annotations, error) {
var annots annotations.Annotations
for x := 0; x < dataPoints.Len(); x++ {
@@ -53,9 +51,7 @@ func (c *PrometheusConverter) addExponentialHistogramDataPoints(ctx context.Cont
}
lbls, err := c.createAttributes(
- resource,
pt.Attributes(),
- scope,
settings,
nil,
true,
@@ -253,8 +249,7 @@ func convertBucketsLayout(bucketCounts []uint64, offset, scaleDown int32, adjust
}
func (c *PrometheusConverter) addCustomBucketsHistogramDataPoints(ctx context.Context, dataPoints pmetric.HistogramDataPointSlice,
- resource pcommon.Resource, settings Settings, temporality pmetric.AggregationTemporality,
- scope scope, meta Metadata,
+ settings Settings, temporality pmetric.AggregationTemporality, meta Metadata,
) (annotations.Annotations, error) {
var annots annotations.Annotations
@@ -272,9 +267,7 @@ func (c *PrometheusConverter) addCustomBucketsHistogramDataPoints(ctx context.Co
}
lbls, err := c.createAttributes(
- resource,
pt.Attributes(),
- scope,
settings,
nil,
true,
diff --git a/storage/remote/otlptranslator/prometheusremotewrite/histograms_test.go b/storage/remote/otlptranslator/prometheusremotewrite/histograms_test.go
index 644ec2e01b..f55aef2f36 100644
--- a/storage/remote/otlptranslator/prometheusremotewrite/histograms_test.go
+++ b/storage/remote/otlptranslator/prometheusremotewrite/histograms_test.go
@@ -861,15 +861,20 @@ func TestPrometheusConverter_addExponentialHistogramDataPoints(t *testing.T) {
}
name, err := namer.Build(TranslatorMetricFromOtelMetric(metric))
require.NoError(t, err)
+ settings := Settings{
+ PromoteScopeMetadata: tt.promoteScope,
+ }
+ resource := pcommon.NewResource()
+
+ // Initialize resource and scope context as FromMetrics would.
+ require.NoError(t, converter.setResourceContext(resource, settings))
+ require.NoError(t, converter.setScopeContext(tt.scope, settings))
+
annots, err := converter.addExponentialHistogramDataPoints(
context.Background(),
metric.ExponentialHistogram().DataPoints(),
- pcommon.NewResource(),
- Settings{
- PromoteScopeMetadata: tt.promoteScope,
- },
+ settings,
pmetric.AggregationTemporalityCumulative,
- tt.scope,
Metadata{
MetricFamilyName: name,
},
@@ -1334,16 +1339,21 @@ func TestPrometheusConverter_addCustomBucketsHistogramDataPoints(t *testing.T) {
}
name, err := namer.Build(TranslatorMetricFromOtelMetric(metric))
require.NoError(t, err)
+ settings := Settings{
+ ConvertHistogramsToNHCB: true,
+ PromoteScopeMetadata: tt.promoteScope,
+ }
+ resource := pcommon.NewResource()
+
+ // Initialize resource and scope context as FromMetrics would.
+ require.NoError(t, converter.setResourceContext(resource, settings))
+ require.NoError(t, converter.setScopeContext(tt.scope, settings))
+
annots, err := converter.addCustomBucketsHistogramDataPoints(
context.Background(),
metric.Histogram().DataPoints(),
- pcommon.NewResource(),
- Settings{
- ConvertHistogramsToNHCB: true,
- PromoteScopeMetadata: tt.promoteScope,
- },
+ settings,
pmetric.AggregationTemporalityCumulative,
- tt.scope,
Metadata{
MetricFamilyName: name,
},
diff --git a/storage/remote/otlptranslator/prometheusremotewrite/metrics_to_prw.go b/storage/remote/otlptranslator/prometheusremotewrite/metrics_to_prw.go
index 41de42548a..81e99a2f50 100644
--- a/storage/remote/otlptranslator/prometheusremotewrite/metrics_to_prw.go
+++ b/storage/remote/otlptranslator/prometheusremotewrite/metrics_to_prw.go
@@ -26,7 +26,7 @@ import (
"github.com/prometheus/otlptranslator"
"go.opentelemetry.io/collector/pdata/pcommon"
"go.opentelemetry.io/collector/pdata/pmetric"
- "go.uber.org/multierr"
+ semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
"github.com/prometheus/prometheus/config"
"github.com/prometheus/prometheus/model/labels"
@@ -62,6 +62,24 @@ type Settings struct {
LabelNamePreserveMultipleUnderscores bool
}
+// cachedResourceLabels holds precomputed labels constant for all datapoints in a ResourceMetrics.
+// These are computed once per ResourceMetrics boundary and reused for all datapoints.
+type cachedResourceLabels struct {
+ jobLabel string // from service.name + service.namespace.
+ instanceLabel string // from service.instance.id.
+ promotedLabels labels.Labels // promoted resource attributes.
+ externalLabels map[string]string
+}
+
+// cachedScopeLabels holds precomputed scope metadata labels.
+// These are computed once per ScopeMetrics boundary and reused for all datapoints.
+type cachedScopeLabels struct {
+ scopeName string
+ scopeVersion string
+ scopeSchemaURL string
+ scopeAttrs labels.Labels // otel_scope_* labels.
+}
+
// PrometheusConverter converts from OTel write format to Prometheus remote write format.
type PrometheusConverter struct {
everyN everyNTimes
@@ -70,6 +88,15 @@ type PrometheusConverter struct {
appender CombinedAppender
// seenTargetInfo tracks target_info samples within a batch to prevent duplicates.
seenTargetInfo map[targetInfoKey]struct{}
+
+ // Label caching for optimization - computed once per resource/scope boundary.
+ resourceLabels *cachedResourceLabels
+ scopeLabels *cachedScopeLabels
+ labelNamer otlptranslator.LabelNamer
+
+ // sanitizedLabels caches the results of label name sanitization within a request.
+ // This avoids repeated string allocations for the same label names.
+ sanitizedLabels map[string]string
}
// targetInfoKey uniquely identifies a target_info sample by its labelset and timestamp.
@@ -80,12 +107,27 @@ type targetInfoKey struct {
func NewPrometheusConverter(appender CombinedAppender) *PrometheusConverter {
return &PrometheusConverter{
- scratchBuilder: labels.NewScratchBuilder(0),
- builder: labels.NewBuilder(labels.EmptyLabels()),
- appender: appender,
+ scratchBuilder: labels.NewScratchBuilder(0),
+ builder: labels.NewBuilder(labels.EmptyLabels()),
+ appender: appender,
+ sanitizedLabels: make(map[string]string, 64), // Pre-size for typical label count.
}
}
+// buildLabelName returns a sanitized label name, using the cache to avoid repeated allocations.
+func (c *PrometheusConverter) buildLabelName(label string) (string, error) {
+ if sanitized, ok := c.sanitizedLabels[label]; ok {
+ return sanitized, nil
+ }
+
+ sanitized, err := c.labelNamer.Build(label)
+ if err != nil {
+ return "", err
+ }
+ c.sanitizedLabels[label] = sanitized
+ return sanitized, nil
+}
+
func TranslatorMetricFromOtelMetric(metric pmetric.Metric) otlptranslator.Metric {
m := otlptranslator.Metric{
Name: metric.Name(),
@@ -140,23 +182,33 @@ func (c *PrometheusConverter) FromMetrics(ctx context.Context, md pmetric.Metric
c.seenTargetInfo = make(map[targetInfoKey]struct{})
resourceMetricsSlice := md.ResourceMetrics()
- for i := 0; i < resourceMetricsSlice.Len(); i++ {
+ for i := range resourceMetricsSlice.Len() {
resourceMetrics := resourceMetricsSlice.At(i)
resource := resourceMetrics.Resource()
scopeMetricsSlice := resourceMetrics.ScopeMetrics()
+ if err := c.setResourceContext(resource, settings); err != nil {
+ errs = errors.Join(errs, err)
+ continue
+ }
+
// keep track of the earliest and latest timestamp in the ResourceMetrics for
// use with the "target" info metric
earliestTimestamp := pcommon.Timestamp(math.MaxUint64)
latestTimestamp := pcommon.Timestamp(0)
- for j := 0; j < scopeMetricsSlice.Len(); j++ {
+ for j := range scopeMetricsSlice.Len() {
scopeMetrics := scopeMetricsSlice.At(j)
scope := newScopeFromScopeMetrics(scopeMetrics)
+ if err := c.setScopeContext(scope, settings); err != nil {
+ errs = errors.Join(errs, err)
+ continue
+ }
+
metricSlice := scopeMetrics.Metrics()
// TODO: decide if instrumentation library information should be exported as labels
for k := 0; k < metricSlice.Len(); k++ {
if err := c.everyN.checkContext(ctx); err != nil {
- errs = multierr.Append(errs, err)
+ errs = errors.Join(errs, err)
return annots, errs
}
@@ -164,7 +216,7 @@ func (c *PrometheusConverter) FromMetrics(ctx context.Context, md pmetric.Metric
earliestTimestamp, latestTimestamp = findMinAndMaxTimestamps(metric, earliestTimestamp, latestTimestamp)
temporality, hasTemporality, err := aggregationTemporality(metric)
if err != nil {
- errs = multierr.Append(errs, err)
+ errs = errors.Join(errs, err)
continue
}
@@ -175,13 +227,13 @@ func (c *PrometheusConverter) FromMetrics(ctx context.Context, md pmetric.Metric
//nolint:staticcheck // QF1001 Applying De Morgan’s law would make the conditions harder to read.
!(temporality == pmetric.AggregationTemporalityCumulative ||
(settings.AllowDeltaTemporality && temporality == pmetric.AggregationTemporalityDelta)) {
- errs = multierr.Append(errs, fmt.Errorf("invalid temporality and type combination for metric %q", metric.Name()))
+ errs = errors.Join(errs, fmt.Errorf("invalid temporality and type combination for metric %q", metric.Name()))
continue
}
promName, err := namer.Build(TranslatorMetricFromOtelMetric(metric))
if err != nil {
- errs = multierr.Append(errs, err)
+ errs = errors.Join(errs, err)
continue
}
meta := Metadata{
@@ -199,11 +251,11 @@ func (c *PrometheusConverter) FromMetrics(ctx context.Context, md pmetric.Metric
case pmetric.MetricTypeGauge:
dataPoints := metric.Gauge().DataPoints()
if dataPoints.Len() == 0 {
- errs = multierr.Append(errs, fmt.Errorf("empty data points. %s is dropped", metric.Name()))
+ errs = errors.Join(errs, fmt.Errorf("empty data points. %s is dropped", metric.Name()))
break
}
- if err := c.addGaugeNumberDataPoints(ctx, dataPoints, resource, settings, scope, meta); err != nil {
- errs = multierr.Append(errs, err)
+ if err := c.addGaugeNumberDataPoints(ctx, dataPoints, settings, meta); err != nil {
+ errs = errors.Join(errs, err)
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return annots, errs
}
@@ -211,11 +263,11 @@ func (c *PrometheusConverter) FromMetrics(ctx context.Context, md pmetric.Metric
case pmetric.MetricTypeSum:
dataPoints := metric.Sum().DataPoints()
if dataPoints.Len() == 0 {
- errs = multierr.Append(errs, fmt.Errorf("empty data points. %s is dropped", metric.Name()))
+ errs = errors.Join(errs, fmt.Errorf("empty data points. %s is dropped", metric.Name()))
break
}
- if err := c.addSumNumberDataPoints(ctx, dataPoints, resource, settings, scope, meta); err != nil {
- errs = multierr.Append(errs, err)
+ if err := c.addSumNumberDataPoints(ctx, dataPoints, settings, meta); err != nil {
+ errs = errors.Join(errs, err)
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return annots, errs
}
@@ -223,23 +275,23 @@ func (c *PrometheusConverter) FromMetrics(ctx context.Context, md pmetric.Metric
case pmetric.MetricTypeHistogram:
dataPoints := metric.Histogram().DataPoints()
if dataPoints.Len() == 0 {
- errs = multierr.Append(errs, fmt.Errorf("empty data points. %s is dropped", metric.Name()))
+ errs = errors.Join(errs, fmt.Errorf("empty data points. %s is dropped", metric.Name()))
break
}
if settings.ConvertHistogramsToNHCB {
ws, err := c.addCustomBucketsHistogramDataPoints(
- ctx, dataPoints, resource, settings, temporality, scope, meta,
+ ctx, dataPoints, settings, temporality, meta,
)
annots.Merge(ws)
if err != nil {
- errs = multierr.Append(errs, err)
+ errs = errors.Join(errs, err)
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return annots, errs
}
}
} else {
- if err := c.addHistogramDataPoints(ctx, dataPoints, resource, settings, scope, meta); err != nil {
- errs = multierr.Append(errs, err)
+ if err := c.addHistogramDataPoints(ctx, dataPoints, settings, meta); err != nil {
+ errs = errors.Join(errs, err)
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return annots, errs
}
@@ -248,21 +300,19 @@ func (c *PrometheusConverter) FromMetrics(ctx context.Context, md pmetric.Metric
case pmetric.MetricTypeExponentialHistogram:
dataPoints := metric.ExponentialHistogram().DataPoints()
if dataPoints.Len() == 0 {
- errs = multierr.Append(errs, fmt.Errorf("empty data points. %s is dropped", metric.Name()))
+ errs = errors.Join(errs, fmt.Errorf("empty data points. %s is dropped", metric.Name()))
break
}
ws, err := c.addExponentialHistogramDataPoints(
ctx,
dataPoints,
- resource,
settings,
temporality,
- scope,
meta,
)
annots.Merge(ws)
if err != nil {
- errs = multierr.Append(errs, err)
+ errs = errors.Join(errs, err)
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return annots, errs
}
@@ -270,17 +320,17 @@ func (c *PrometheusConverter) FromMetrics(ctx context.Context, md pmetric.Metric
case pmetric.MetricTypeSummary:
dataPoints := metric.Summary().DataPoints()
if dataPoints.Len() == 0 {
- errs = multierr.Append(errs, fmt.Errorf("empty data points. %s is dropped", metric.Name()))
+ errs = errors.Join(errs, fmt.Errorf("empty data points. %s is dropped", metric.Name()))
break
}
- if err := c.addSummaryDataPoints(ctx, dataPoints, resource, settings, scope, meta); err != nil {
- errs = multierr.Append(errs, err)
+ if err := c.addSummaryDataPoints(ctx, dataPoints, settings, meta); err != nil {
+ errs = errors.Join(errs, err)
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return annots, errs
}
}
default:
- errs = multierr.Append(errs, errors.New("unsupported metric type"))
+ errs = errors.Join(errs, errors.New("unsupported metric type"))
}
}
}
@@ -288,7 +338,7 @@ func (c *PrometheusConverter) FromMetrics(ctx context.Context, md pmetric.Metric
// We have at least one metric sample for this resource.
// Generate a corresponding target_info series.
if err := c.addResourceTargetInfo(resource, settings, earliestTimestamp.AsTime(), latestTimestamp.AsTime()); err != nil {
- errs = multierr.Append(errs, err)
+ errs = errors.Join(errs, err)
}
}
}
@@ -311,8 +361,11 @@ func NewPromoteResourceAttributes(otlpCfg config.OTLPConfig) *PromoteResourceAtt
}
}
+// LabelNameBuilder is a function that builds/sanitizes label names.
+type LabelNameBuilder func(string) (string, error)
+
// addPromotedAttributes adds labels for promoted resourceAttributes to the builder.
-func (s *PromoteResourceAttributes) addPromotedAttributes(builder *labels.Builder, resourceAttributes pcommon.Map, labelNamer otlptranslator.LabelNamer) error {
+func (s *PromoteResourceAttributes) addPromotedAttributes(builder *labels.Builder, resourceAttributes pcommon.Map, buildLabelName LabelNameBuilder) error {
if s == nil {
return nil
}
@@ -322,13 +375,11 @@ func (s *PromoteResourceAttributes) addPromotedAttributes(builder *labels.Builde
resourceAttributes.Range(func(name string, value pcommon.Value) bool {
if _, exists := s.attrs[name]; !exists {
var normalized string
- normalized, err = labelNamer.Build(name)
+ normalized, err = buildLabelName(name)
if err != nil {
return false
}
- if builder.Get(normalized) == "" {
- builder.Set(normalized, value.AsString())
- }
+ builder.Set(normalized, value.AsString())
}
return true
})
@@ -338,15 +389,91 @@ func (s *PromoteResourceAttributes) addPromotedAttributes(builder *labels.Builde
resourceAttributes.Range(func(name string, value pcommon.Value) bool {
if _, exists := s.attrs[name]; exists {
var normalized string
- normalized, err = labelNamer.Build(name)
+ normalized, err = buildLabelName(name)
if err != nil {
return false
}
- if builder.Get(normalized) == "" {
- builder.Set(normalized, value.AsString())
- }
+ builder.Set(normalized, value.AsString())
}
return true
})
return err
}
+
+// setResourceContext precomputes and caches resource-level labels.
+// Called once per ResourceMetrics boundary, before processing any datapoints.
+// If an error is returned, resource level cache is reset.
+func (c *PrometheusConverter) setResourceContext(resource pcommon.Resource, settings Settings) error {
+ resourceAttrs := resource.Attributes()
+ c.resourceLabels = &cachedResourceLabels{
+ externalLabels: settings.ExternalLabels,
+ }
+
+ c.labelNamer = otlptranslator.LabelNamer{
+ UTF8Allowed: settings.AllowUTF8,
+ UnderscoreLabelSanitization: settings.LabelNameUnderscoreSanitization,
+ PreserveMultipleUnderscores: settings.LabelNamePreserveMultipleUnderscores,
+ }
+
+ if serviceName, ok := resourceAttrs.Get(string(semconv.ServiceNameKey)); ok {
+ val := serviceName.AsString()
+ if serviceNamespace, ok := resourceAttrs.Get(string(semconv.ServiceNamespaceKey)); ok {
+ val = serviceNamespace.AsString() + "/" + val
+ }
+ c.resourceLabels.jobLabel = val
+ }
+
+ if instance, ok := resourceAttrs.Get(string(semconv.ServiceInstanceIDKey)); ok {
+ c.resourceLabels.instanceLabel = instance.AsString()
+ }
+
+ if settings.PromoteResourceAttributes != nil {
+ c.builder.Reset(labels.EmptyLabels())
+ if err := settings.PromoteResourceAttributes.addPromotedAttributes(c.builder, resourceAttrs, c.buildLabelName); err != nil {
+ c.clearResourceContext()
+ return err
+ }
+ c.resourceLabels.promotedLabels = c.builder.Labels()
+ }
+ return nil
+}
+
+// setScopeContext precomputes and caches scope-level labels.
+// Called once per ScopeMetrics boundary, before processing any metrics.
+// If an error is returned, scope level cache is reset.
+func (c *PrometheusConverter) setScopeContext(scope scope, settings Settings) error {
+ if !settings.PromoteScopeMetadata || scope.name == "" {
+ c.scopeLabels = nil
+ return nil
+ }
+
+ c.scopeLabels = &cachedScopeLabels{
+ scopeName: scope.name,
+ scopeVersion: scope.version,
+ scopeSchemaURL: scope.schemaURL,
+ }
+ c.builder.Reset(labels.EmptyLabels())
+ var err error
+ scope.attributes.Range(func(k string, v pcommon.Value) bool {
+ var name string
+ name, err = c.buildLabelName("otel_scope_" + k)
+ if err != nil {
+ return false
+ }
+ c.builder.Set(name, v.AsString())
+ return true
+ })
+ if err != nil {
+ c.scopeLabels = nil
+ return err
+ }
+
+ c.scopeLabels.scopeAttrs = c.builder.Labels()
+ return nil
+}
+
+// clearResourceContext clears cached labels between ResourceMetrics.
+func (c *PrometheusConverter) clearResourceContext() {
+ c.resourceLabels = nil
+ c.scopeLabels = nil
+}
diff --git a/storage/remote/otlptranslator/prometheusremotewrite/metrics_to_prw_test.go b/storage/remote/otlptranslator/prometheusremotewrite/metrics_to_prw_test.go
index 8eb0029dd7..f90051e84d 100644
--- a/storage/remote/otlptranslator/prometheusremotewrite/metrics_to_prw_test.go
+++ b/storage/remote/otlptranslator/prometheusremotewrite/metrics_to_prw_test.go
@@ -31,6 +31,7 @@ import (
"go.opentelemetry.io/collector/pdata/pmetric"
"go.opentelemetry.io/collector/pdata/pmetric/pmetricotlp"
+ "github.com/prometheus/prometheus/config"
"github.com/prometheus/prometheus/model/exemplar"
"github.com/prometheus/prometheus/model/histogram"
"github.com/prometheus/prometheus/model/labels"
@@ -456,6 +457,211 @@ func TestFromMetrics(t *testing.T) {
},
}, targetInfoSamples)
})
+
+ t.Run("target_info should not include scope labels when PromoteScopeMetadata is enabled", func(t *testing.T) {
+ // Regression test: When PromoteScopeMetadata is enabled and a scope has a non-empty name,
+ // the cached scopeLabels should NOT be merged into target_info.
+ request := pmetricotlp.NewExportRequest()
+ rm := request.Metrics().ResourceMetrics().AppendEmpty()
+
+ // Set up resource attributes for job/instance labels.
+ rm.Resource().Attributes().PutStr("service.name", "test-service")
+ rm.Resource().Attributes().PutStr("service.instance.id", "instance-1")
+ generateAttributes(rm.Resource().Attributes(), "resource", 2)
+
+ // Create a scope with a non-empty name (this triggers scope label caching).
+ scopeMetrics := rm.ScopeMetrics().AppendEmpty()
+ scope := scopeMetrics.Scope()
+ scope.SetName("my-scope")
+ scope.SetVersion("1.0.0")
+ scope.Attributes().PutStr("scope-attr", "scope-value")
+
+ // Add a metric.
+ ts := pcommon.NewTimestampFromTime(time.Now())
+ m := scopeMetrics.Metrics().AppendEmpty()
+ m.SetEmptyGauge()
+ m.SetName("test_gauge")
+ m.SetDescription("test gauge")
+ point := m.Gauge().DataPoints().AppendEmpty()
+ point.SetTimestamp(ts)
+ point.SetDoubleValue(1.0)
+
+ mockAppender := &mockCombinedAppender{}
+ converter := NewPrometheusConverter(mockAppender)
+ annots, err := converter.FromMetrics(
+ context.Background(),
+ request.Metrics(),
+ Settings{
+ PromoteScopeMetadata: true,
+ LookbackDelta: defaultLookbackDelta,
+ },
+ )
+ require.NoError(t, err)
+ require.Empty(t, annots)
+ require.NoError(t, mockAppender.Commit())
+
+ // Find target_info samples.
+ var targetInfoSamples []combinedSample
+ for _, s := range mockAppender.samples {
+ if s.ls.Get(labels.MetricName) == "target_info" {
+ targetInfoSamples = append(targetInfoSamples, s)
+ }
+ }
+ require.NotEmpty(t, targetInfoSamples, "expected target_info samples")
+
+ // Verify target_info does NOT have scope labels.
+ for _, s := range targetInfoSamples {
+ require.Empty(t, s.ls.Get("otel_scope_name"), "target_info should not have otel_scope_name")
+ require.Empty(t, s.ls.Get("otel_scope_version"), "target_info should not have otel_scope_version")
+ require.Empty(t, s.ls.Get("otel_scope_schema_url"), "target_info should not have otel_scope_schema_url")
+ require.Empty(t, s.ls.Get("otel_scope_scope_attr"), "target_info should not have scope attributes")
+ }
+
+ // Verify the metric itself DOES have scope labels.
+ var metricSamples []combinedSample
+ for _, s := range mockAppender.samples {
+ if s.ls.Get(labels.MetricName) == "test_gauge" {
+ metricSamples = append(metricSamples, s)
+ }
+ }
+ require.NotEmpty(t, metricSamples, "expected metric samples")
+ require.Equal(t, "my-scope", metricSamples[0].ls.Get("otel_scope_name"), "metric should have otel_scope_name")
+ require.Equal(t, "1.0.0", metricSamples[0].ls.Get("otel_scope_version"), "metric should have otel_scope_version")
+ })
+
+ t.Run("target_info should include promoted resource attributes", func(t *testing.T) {
+ // Promoted resource attributes should appear on both metrics and target_info.
+ request := pmetricotlp.NewExportRequest()
+ rm := request.Metrics().ResourceMetrics().AppendEmpty()
+
+ // Set up resource attributes.
+ rm.Resource().Attributes().PutStr("service.name", "test-service")
+ rm.Resource().Attributes().PutStr("service.instance.id", "instance-1")
+ rm.Resource().Attributes().PutStr("custom.promoted.attr", "promoted-value")
+ rm.Resource().Attributes().PutStr("another.resource.attr", "another-value")
+
+ // Add a metric.
+ ts := pcommon.NewTimestampFromTime(time.Now())
+ scopeMetrics := rm.ScopeMetrics().AppendEmpty()
+ m := scopeMetrics.Metrics().AppendEmpty()
+ m.SetEmptyGauge()
+ m.SetName("test_gauge")
+ m.SetDescription("test gauge")
+ point := m.Gauge().DataPoints().AppendEmpty()
+ point.SetTimestamp(ts)
+ point.SetDoubleValue(1.0)
+
+ mockAppender := &mockCombinedAppender{}
+ converter := NewPrometheusConverter(mockAppender)
+ annots, err := converter.FromMetrics(
+ context.Background(),
+ request.Metrics(),
+ Settings{
+ PromoteResourceAttributes: NewPromoteResourceAttributes(config.OTLPConfig{
+ PromoteResourceAttributes: []string{"custom.promoted.attr"},
+ }),
+ LookbackDelta: defaultLookbackDelta,
+ },
+ )
+ require.NoError(t, err)
+ require.Empty(t, annots)
+ require.NoError(t, mockAppender.Commit())
+
+ // Find target_info samples.
+ var targetInfoSamples []combinedSample
+ for _, s := range mockAppender.samples {
+ if s.ls.Get(labels.MetricName) == "target_info" {
+ targetInfoSamples = append(targetInfoSamples, s)
+ }
+ }
+ require.NotEmpty(t, targetInfoSamples, "expected target_info samples")
+
+ // Verify target_info has the promoted resource attribute.
+ for _, s := range targetInfoSamples {
+ require.Equal(t, "promoted-value", s.ls.Get("custom_promoted_attr"), "target_info should have promoted resource attributes")
+ require.Equal(t, "another-value", s.ls.Get("another_resource_attr"), "target_info should have non-promoted resource attributes")
+ }
+
+ // Verify the metric also has the promoted resource attribute.
+ var metricSamples []combinedSample
+ for _, s := range mockAppender.samples {
+ if s.ls.Get(labels.MetricName) == "test_gauge" {
+ metricSamples = append(metricSamples, s)
+ }
+ }
+ require.NotEmpty(t, metricSamples, "expected metric samples")
+ require.Equal(t, "promoted-value", metricSamples[0].ls.Get("custom_promoted_attr"), "metric should have promoted resource attribute")
+ })
+
+ t.Run("target_info should include promoted attributes when KeepIdentifyingResourceAttributes is enabled", func(t *testing.T) {
+ // When both PromoteResourceAttributes and KeepIdentifyingResourceAttributes are configured,
+ // target_info should include both the promoted attributes and the identifying attributes.
+ request := pmetricotlp.NewExportRequest()
+ rm := request.Metrics().ResourceMetrics().AppendEmpty()
+
+ rm.Resource().Attributes().PutStr("service.name", "test-service")
+ rm.Resource().Attributes().PutStr("service.namespace", "test-namespace")
+ rm.Resource().Attributes().PutStr("service.instance.id", "instance-1")
+ rm.Resource().Attributes().PutStr("custom.promoted.attr", "promoted-value")
+ rm.Resource().Attributes().PutStr("another.resource.attr", "another-value")
+
+ // Add a metric.
+ ts := pcommon.NewTimestampFromTime(time.Now())
+ scopeMetrics := rm.ScopeMetrics().AppendEmpty()
+ m := scopeMetrics.Metrics().AppendEmpty()
+ m.SetEmptyGauge()
+ m.SetName("test_gauge")
+ m.SetDescription("test gauge")
+ point := m.Gauge().DataPoints().AppendEmpty()
+ point.SetTimestamp(ts)
+ point.SetDoubleValue(1.0)
+
+ mockAppender := &mockCombinedAppender{}
+ converter := NewPrometheusConverter(mockAppender)
+ annots, err := converter.FromMetrics(
+ context.Background(),
+ request.Metrics(),
+ Settings{
+ PromoteResourceAttributes: NewPromoteResourceAttributes(config.OTLPConfig{
+ PromoteResourceAttributes: []string{"custom.promoted.attr"},
+ }),
+ KeepIdentifyingResourceAttributes: true,
+ LookbackDelta: defaultLookbackDelta,
+ },
+ )
+ require.NoError(t, err)
+ require.Empty(t, annots)
+ require.NoError(t, mockAppender.Commit())
+
+ var targetInfoSamples []combinedSample
+ for _, s := range mockAppender.samples {
+ if s.ls.Get(labels.MetricName) == "target_info" {
+ targetInfoSamples = append(targetInfoSamples, s)
+ }
+ }
+ require.NotEmpty(t, targetInfoSamples, "expected target_info samples")
+
+ // Verify target_info has the promoted resource attribute.
+ for _, s := range targetInfoSamples {
+ require.Equal(t, "promoted-value", s.ls.Get("custom_promoted_attr"), "target_info should have promoted resource attributes")
+ // And it should have the identifying attributes (since KeepIdentifyingResourceAttributes is true).
+ require.Equal(t, "test-service", s.ls.Get("service_name"), "target_info should have service.name when KeepIdentifyingResourceAttributes is true")
+ require.Equal(t, "test-namespace", s.ls.Get("service_namespace"), "target_info should have service.namespace when KeepIdentifyingResourceAttributes is true")
+ require.Equal(t, "instance-1", s.ls.Get("service_instance_id"), "target_info should have service.instance.id when KeepIdentifyingResourceAttributes is true")
+ // And the non-promoted resource attribute.
+ require.Equal(t, "another-value", s.ls.Get("another_resource_attr"), "target_info should have non-promoted resource attributes")
+ }
+
+ // Verify the metric also has the promoted resource attribute.
+ var metricSamples []combinedSample
+ for _, s := range mockAppender.samples {
+ if s.ls.Get(labels.MetricName) == "test_gauge" {
+ metricSamples = append(metricSamples, s)
+ }
+ }
+ require.NotEmpty(t, metricSamples, "expected metric samples")
+ require.Equal(t, "promoted-value", metricSamples[0].ls.Get("custom_promoted_attr"), "metric should have promoted resource attribute")
+ })
}
func TestTemporality(t *testing.T) {
@@ -1067,7 +1273,7 @@ func BenchmarkPrometheusConverter_FromMetrics(b *testing.B) {
for b.Loop() {
app := &noOpAppender{}
- mockAppender := NewCombinedAppender(app, noOpLogger, false, false, appMetrics)
+ mockAppender := NewCombinedAppender(app, noOpLogger, false, true, appMetrics)
converter := NewPrometheusConverter(mockAppender)
annots, err := converter.FromMetrics(context.Background(), payload.Metrics(), settings)
require.NoError(b, err)
@@ -1323,3 +1529,276 @@ func generateExemplars(exemplars pmetric.ExemplarSlice, count int, ts pcommon.Ti
e.SetTraceID(pcommon.TraceID{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f})
}
}
+
+// createMultiScopeExportRequest creates an export request with multiple scopes per resource.
+// This is useful for benchmarking resource-level label caching, where cached resource labels
+// (job, instance, promoted attributes) should be computed once and reused across all scopes.
+func createMultiScopeExportRequest(
+ resourceAttributeCount int,
+ scopeCount int,
+ metricsPerScope int,
+ labelsPerMetric int,
+ scopeAttributeCount int,
+) pmetricotlp.ExportRequest {
+ request := pmetricotlp.NewExportRequest()
+ ts := pcommon.NewTimestampFromTime(time.Now())
+
+ rm := request.Metrics().ResourceMetrics().AppendEmpty()
+ generateAttributes(rm.Resource().Attributes(), "resource", resourceAttributeCount)
+
+ // Set service attributes for job/instance label generation
+ rm.Resource().Attributes().PutStr("service.name", "test-service")
+ rm.Resource().Attributes().PutStr("service.namespace", "test-namespace")
+ rm.Resource().Attributes().PutStr("service.instance.id", "instance-1")
+
+ for s := range scopeCount {
+ scopeMetrics := rm.ScopeMetrics().AppendEmpty()
+ scope := scopeMetrics.Scope()
+ scope.SetName(fmt.Sprintf("scope-%d", s))
+ scope.SetVersion("1.0.0")
+ generateAttributes(scope.Attributes(), "scope", scopeAttributeCount)
+
+ metrics := scopeMetrics.Metrics()
+ for m := range metricsPerScope {
+ metric := metrics.AppendEmpty()
+ metric.SetName(fmt.Sprintf("gauge_s%d_m%d", s, m))
+ metric.SetDescription("gauge metric")
+ metric.SetUnit("unit")
+ point := metric.SetEmptyGauge().DataPoints().AppendEmpty()
+ point.SetTimestamp(ts)
+ point.SetDoubleValue(float64(m))
+ generateAttributes(point.Attributes(), "series", labelsPerMetric)
+ }
+ }
+
+ return request
+}
+
+// createRepeatedLabelsExportRequest creates an export request where the same label names
+// appear repeatedly across many datapoints. This is useful for benchmarking the label
+// sanitization cache, which should reduce allocations when the same label names are seen multiple times.
+func createRepeatedLabelsExportRequest(
+ uniqueLabelNames int,
+ datapointCount int,
+ labelsPerDatapoint int,
+) pmetricotlp.ExportRequest {
+ request := pmetricotlp.NewExportRequest()
+ ts := pcommon.NewTimestampFromTime(time.Now())
+
+ rm := request.Metrics().ResourceMetrics().AppendEmpty()
+ rm.Resource().Attributes().PutStr("service.name", "test-service")
+ rm.Resource().Attributes().PutStr("service.instance.id", "instance-1")
+
+ metrics := rm.ScopeMetrics().AppendEmpty().Metrics()
+
+ // Pre-generate label names that will be reused.
+ labelNames := make([]string, uniqueLabelNames)
+ for i := range uniqueLabelNames {
+ labelNames[i] = fmt.Sprintf("label.name.%d", i)
+ }
+
+ for d := range datapointCount {
+ metric := metrics.AppendEmpty()
+ metric.SetName(fmt.Sprintf("gauge_%d", d))
+ metric.SetDescription("gauge metric")
+ metric.SetUnit("unit")
+ point := metric.SetEmptyGauge().DataPoints().AppendEmpty()
+ point.SetTimestamp(ts)
+ point.SetDoubleValue(float64(d))
+
+ // Add labels using the same label names (cycling through them).
+ for l := range labelsPerDatapoint {
+ labelName := labelNames[l%uniqueLabelNames]
+ point.Attributes().PutStr(labelName, fmt.Sprintf("value-%d-%d", d, l))
+ }
+ }
+
+ return request
+}
+
+// createMultiResourceExportRequest creates an export request with multiple ResourceMetrics.
+// This is useful for benchmarking the overhead of cache clearing between resources and
+// verifying that caching still helps within each resource.
+func createMultiResourceExportRequest(
+ resourceCount int,
+ resourceAttributeCount int,
+ metricsPerResource int,
+ labelsPerMetric int,
+) pmetricotlp.ExportRequest {
+ request := pmetricotlp.NewExportRequest()
+ ts := pcommon.NewTimestampFromTime(time.Now())
+
+ for r := range resourceCount {
+ rm := request.Metrics().ResourceMetrics().AppendEmpty()
+ generateAttributes(rm.Resource().Attributes(), "resource", resourceAttributeCount)
+
+ // Set unique service attributes per resource for job/instance label generation.
+ rm.Resource().Attributes().PutStr("service.name", fmt.Sprintf("service-%d", r))
+ rm.Resource().Attributes().PutStr("service.namespace", "test-namespace")
+ rm.Resource().Attributes().PutStr("service.instance.id", fmt.Sprintf("instance-%d", r))
+
+ metrics := rm.ScopeMetrics().AppendEmpty().Metrics()
+ for m := range metricsPerResource {
+ metric := metrics.AppendEmpty()
+ metric.SetName(fmt.Sprintf("gauge_r%d_m%d", r, m))
+ metric.SetDescription("gauge metric")
+ metric.SetUnit("unit")
+ point := metric.SetEmptyGauge().DataPoints().AppendEmpty()
+ point.SetTimestamp(ts)
+ point.SetDoubleValue(float64(m))
+ generateAttributes(point.Attributes(), "series", labelsPerMetric)
+ }
+ }
+
+ return request
+}
+
+// BenchmarkFromMetrics_LabelCaching_MultipleDatapointsPerResource benchmarks the resource-level
+// label caching optimization. With caching, resource labels (job, instance, promoted
+// attributes) should be computed once per ResourceMetrics and reused for all datapoints.
+func BenchmarkFromMetrics_LabelCaching_MultipleDatapointsPerResource(b *testing.B) {
+ const (
+ labelsPerMetric = 5
+ scopeAttributeCount = 3
+ )
+ for _, resourceAttrs := range []int{5, 50} {
+ for _, scopeCount := range []int{1, 10} {
+ for _, metricsPerScope := range []int{10, 100} {
+ b.Run(fmt.Sprintf("res_attrs=%d/scopes=%d/metrics=%d", resourceAttrs, scopeCount, metricsPerScope), func(b *testing.B) {
+ settings := Settings{
+ PromoteResourceAttributes: NewPromoteResourceAttributes(config.OTLPConfig{
+ PromoteAllResourceAttributes: true,
+ }),
+ }
+ payload := createMultiScopeExportRequest(
+ resourceAttrs,
+ scopeCount,
+ metricsPerScope,
+ labelsPerMetric,
+ scopeAttributeCount,
+ )
+ appMetrics := NewCombinedAppenderMetrics(prometheus.NewRegistry())
+ noOpLogger := promslog.NewNopLogger()
+ b.ReportAllocs()
+ b.ResetTimer()
+
+ for b.Loop() {
+ app := &noOpAppender{}
+ mockAppender := NewCombinedAppender(app, noOpLogger, false, false, appMetrics)
+ converter := NewPrometheusConverter(mockAppender)
+ _, err := converter.FromMetrics(context.Background(), payload.Metrics(), settings)
+ require.NoError(b, err)
+ }
+ })
+ }
+ }
+ }
+}
+
+// BenchmarkFromMetrics_LabelCaching_RepeatedLabelNames benchmarks the label sanitization cache.
+// When the same label names appear across many datapoints, the sanitization should
+// only happen once per unique label name within a ResourceMetrics.
+func BenchmarkFromMetrics_LabelCaching_RepeatedLabelNames(b *testing.B) {
+ const labelsPerDatapoint = 20
+ for _, uniqueLabels := range []int{5, 50} {
+ for _, datapoints := range []int{100, 1000} {
+ b.Run(fmt.Sprintf("unique_labels=%d/datapoints=%d", uniqueLabels, datapoints), func(b *testing.B) {
+ settings := Settings{}
+ payload := createRepeatedLabelsExportRequest(
+ uniqueLabels,
+ datapoints,
+ labelsPerDatapoint,
+ )
+ appMetrics := NewCombinedAppenderMetrics(prometheus.NewRegistry())
+ noOpLogger := promslog.NewNopLogger()
+ b.ReportAllocs()
+ b.ResetTimer()
+
+ for b.Loop() {
+ app := &noOpAppender{}
+ mockAppender := NewCombinedAppender(app, noOpLogger, false, false, appMetrics)
+ converter := NewPrometheusConverter(mockAppender)
+ _, err := converter.FromMetrics(context.Background(), payload.Metrics(), settings)
+ require.NoError(b, err)
+ }
+ })
+ }
+ }
+}
+
+// BenchmarkFromMetrics_LabelCaching_ScopeMetadata benchmarks scope-level label caching when
+// PromoteScopeMetadata is enabled. Scope metadata labels (otel_scope_name, version, etc.)
+// should be computed once per ScopeMetrics and reused for all metrics within that scope.
+func BenchmarkFromMetrics_LabelCaching_ScopeMetadata(b *testing.B) {
+ const (
+ resourceAttributeCount = 5
+ labelsPerMetric = 5
+ )
+ for _, scopeAttrs := range []int{0, 10} {
+ for _, metricsPerScope := range []int{10, 100} {
+ b.Run(fmt.Sprintf("scope_attrs=%d/metrics=%d", scopeAttrs, metricsPerScope), func(b *testing.B) {
+ settings := Settings{
+ PromoteScopeMetadata: true,
+ }
+ payload := createMultiScopeExportRequest(
+ resourceAttributeCount,
+ 1, // single scope to isolate scope caching benefit
+ metricsPerScope,
+ labelsPerMetric,
+ scopeAttrs,
+ )
+ appMetrics := NewCombinedAppenderMetrics(prometheus.NewRegistry())
+ noOpLogger := promslog.NewNopLogger()
+ b.ReportAllocs()
+ b.ResetTimer()
+
+ for b.Loop() {
+ app := &noOpAppender{}
+ mockAppender := NewCombinedAppender(app, noOpLogger, false, false, appMetrics)
+ converter := NewPrometheusConverter(mockAppender)
+ _, err := converter.FromMetrics(context.Background(), payload.Metrics(), settings)
+ require.NoError(b, err)
+ }
+ })
+ }
+ }
+}
+
+// BenchmarkFromMetrics_LabelCaching_MultipleResources benchmarks requests with multiple
+// ResourceMetrics. The label sanitization cache is cleared between resources, so this
+// measures the overhead of cache clearing and verifies caching helps within each resource.
+func BenchmarkFromMetrics_LabelCaching_MultipleResources(b *testing.B) {
+ const (
+ resourceAttributeCount = 10
+ labelsPerMetric = 10
+ )
+ for _, resourceCount := range []int{1, 10, 50} {
+ for _, metricsPerResource := range []int{10, 100} {
+ b.Run(fmt.Sprintf("resources=%d/metrics=%d", resourceCount, metricsPerResource), func(b *testing.B) {
+ settings := Settings{
+ PromoteResourceAttributes: NewPromoteResourceAttributes(config.OTLPConfig{
+ PromoteAllResourceAttributes: true,
+ }),
+ }
+ payload := createMultiResourceExportRequest(
+ resourceCount,
+ resourceAttributeCount,
+ metricsPerResource,
+ labelsPerMetric,
+ )
+ appMetrics := NewCombinedAppenderMetrics(prometheus.NewRegistry())
+ noOpLogger := promslog.NewNopLogger()
+ b.ReportAllocs()
+ b.ResetTimer()
+
+ for b.Loop() {
+ app := &noOpAppender{}
+ mockAppender := NewCombinedAppender(app, noOpLogger, false, false, appMetrics)
+ converter := NewPrometheusConverter(mockAppender)
+ _, err := converter.FromMetrics(context.Background(), payload.Metrics(), settings)
+ require.NoError(b, err)
+ }
+ })
+ }
+ }
+}
diff --git a/storage/remote/otlptranslator/prometheusremotewrite/number_data_points.go b/storage/remote/otlptranslator/prometheusremotewrite/number_data_points.go
index e3814ce095..d3860cb5d5 100644
--- a/storage/remote/otlptranslator/prometheusremotewrite/number_data_points.go
+++ b/storage/remote/otlptranslator/prometheusremotewrite/number_data_points.go
@@ -21,14 +21,13 @@ import (
"math"
"github.com/prometheus/common/model"
- "go.opentelemetry.io/collector/pdata/pcommon"
"go.opentelemetry.io/collector/pdata/pmetric"
"github.com/prometheus/prometheus/model/value"
)
func (c *PrometheusConverter) addGaugeNumberDataPoints(ctx context.Context, dataPoints pmetric.NumberDataPointSlice,
- resource pcommon.Resource, settings Settings, scope scope, meta Metadata,
+ settings Settings, meta Metadata,
) error {
for x := 0; x < dataPoints.Len(); x++ {
if err := c.everyN.checkContext(ctx); err != nil {
@@ -37,9 +36,7 @@ func (c *PrometheusConverter) addGaugeNumberDataPoints(ctx context.Context, data
pt := dataPoints.At(x)
labels, err := c.createAttributes(
- resource,
pt.Attributes(),
- scope,
settings,
nil,
true,
@@ -71,7 +68,7 @@ func (c *PrometheusConverter) addGaugeNumberDataPoints(ctx context.Context, data
}
func (c *PrometheusConverter) addSumNumberDataPoints(ctx context.Context, dataPoints pmetric.NumberDataPointSlice,
- resource pcommon.Resource, settings Settings, scope scope, meta Metadata,
+ settings Settings, meta Metadata,
) error {
for x := 0; x < dataPoints.Len(); x++ {
if err := c.everyN.checkContext(ctx); err != nil {
@@ -80,9 +77,7 @@ func (c *PrometheusConverter) addSumNumberDataPoints(ctx context.Context, dataPo
pt := dataPoints.At(x)
lbls, err := c.createAttributes(
- resource,
pt.Attributes(),
- scope,
settings,
nil,
true,
diff --git a/storage/remote/otlptranslator/prometheusremotewrite/number_data_points_test.go b/storage/remote/otlptranslator/prometheusremotewrite/number_data_points_test.go
index 77bc212c76..58a27c12e1 100644
--- a/storage/remote/otlptranslator/prometheusremotewrite/number_data_points_test.go
+++ b/storage/remote/otlptranslator/prometheusremotewrite/number_data_points_test.go
@@ -114,15 +114,19 @@ func TestPrometheusConverter_addGaugeNumberDataPoints(t *testing.T) {
metric := tt.metric()
mockAppender := &mockCombinedAppender{}
converter := NewPrometheusConverter(mockAppender)
+ settings := Settings{
+ PromoteScopeMetadata: tt.promoteScope,
+ }
+ resource := pcommon.NewResource()
+
+ // Initialize resource and scope context as FromMetrics would.
+ require.NoError(t, converter.setResourceContext(resource, settings))
+ require.NoError(t, converter.setScopeContext(tt.scope, settings))
converter.addGaugeNumberDataPoints(
context.Background(),
metric.Gauge().DataPoints(),
- pcommon.NewResource(),
- Settings{
- PromoteScopeMetadata: tt.promoteScope,
- },
- tt.scope,
+ settings,
Metadata{
MetricFamilyName: metric.Name(),
},
@@ -344,15 +348,19 @@ func TestPrometheusConverter_addSumNumberDataPoints(t *testing.T) {
metric := tt.metric()
mockAppender := &mockCombinedAppender{}
converter := NewPrometheusConverter(mockAppender)
+ settings := Settings{
+ PromoteScopeMetadata: tt.promoteScope,
+ }
+ resource := pcommon.NewResource()
+
+ // Initialize resource and scope context as FromMetrics would.
+ require.NoError(t, converter.setResourceContext(resource, settings))
+ require.NoError(t, converter.setScopeContext(tt.scope, settings))
converter.addSumNumberDataPoints(
context.Background(),
metric.Sum().DataPoints(),
- pcommon.NewResource(),
- Settings{
- PromoteScopeMetadata: tt.promoteScope,
- },
- tt.scope,
+ settings,
Metadata{
MetricFamilyName: metric.Name(),
},
diff --git a/storage/remote/storage.go b/storage/remote/storage.go
index f482597249..be75d23383 100644
--- a/storage/remote/storage.go
+++ b/storage/remote/storage.go
@@ -63,6 +63,8 @@ type Storage struct {
localStartTimeCallback startTimeCallback
}
+var _ storage.Storage = &Storage{}
+
// NewStorage returns a remote.Storage.
func NewStorage(l *slog.Logger, reg prometheus.Registerer, stCallback startTimeCallback, walDir string, flushDeadline time.Duration, sm ReadyScrapeManager, enableTypeAndUnitLabels bool) *Storage {
if l == nil {
@@ -193,6 +195,11 @@ func (s *Storage) Appender(ctx context.Context) storage.Appender {
return s.rws.Appender(ctx)
}
+// AppenderV2 implements storage.Storage.
+func (s *Storage) AppenderV2(ctx context.Context) storage.AppenderV2 {
+ return s.rws.AppenderV2(ctx)
+}
+
// LowestSentTimestamp returns the lowest sent timestamp across all queues.
func (s *Storage) LowestSentTimestamp() int64 {
return s.rws.LowestSentTimestamp()
diff --git a/storage/remote/write.go b/storage/remote/write.go
index 92f447d624..6a336dc06b 100644
--- a/storage/remote/write.go
+++ b/storage/remote/write.go
@@ -238,8 +238,20 @@ func (rws *WriteStorage) ApplyConfig(conf *config.Config) error {
// Appender implements storage.Storage.
func (rws *WriteStorage) Appender(context.Context) storage.Appender {
return ×tampTracker{
- writeStorage: rws,
- highestRecvTimestamp: rws.highestTimestamp,
+ baseTimestampTracker: baseTimestampTracker{
+ writeStorage: rws,
+ highestRecvTimestamp: rws.highestTimestamp,
+ },
+ }
+}
+
+// AppenderV2 implements storage.Storage.
+func (rws *WriteStorage) AppenderV2(context.Context) storage.AppenderV2 {
+ return ×tampTrackerV2{
+ baseTimestampTracker: baseTimestampTracker{
+ writeStorage: rws,
+ highestRecvTimestamp: rws.highestTimestamp,
+ },
}
}
@@ -282,9 +294,9 @@ func (rws *WriteStorage) Close() error {
return nil
}
-type timestampTracker struct {
- writeStorage *WriteStorage
- appendOptions *storage.AppendOptions
+type baseTimestampTracker struct {
+ writeStorage *WriteStorage
+
samples int64
exemplars int64
histograms int64
@@ -292,6 +304,12 @@ type timestampTracker struct {
highestRecvTimestamp *maxTimestamp
}
+type timestampTracker struct {
+ baseTimestampTracker
+
+ appendOptions *storage.AppendOptions
+}
+
func (t *timestampTracker) SetOptions(opts *storage.AppendOptions) {
t.appendOptions = opts
}
@@ -345,7 +363,7 @@ func (*timestampTracker) UpdateMetadata(storage.SeriesRef, labels.Labels, metada
}
// Commit implements storage.Appender.
-func (t *timestampTracker) Commit() error {
+func (t *baseTimestampTracker) Commit() error {
t.writeStorage.samplesIn.incr(t.samples + t.exemplars + t.histograms)
samplesIn.Add(float64(t.samples))
@@ -356,6 +374,25 @@ func (t *timestampTracker) Commit() error {
}
// Rollback implements storage.Appender.
-func (*timestampTracker) Rollback() error {
+func (*baseTimestampTracker) Rollback() error {
return nil
}
+
+type timestampTrackerV2 struct {
+ baseTimestampTracker
+}
+
+// Append implements storage.AppenderV2.
+func (t *timestampTrackerV2) Append(ref storage.SeriesRef, _ labels.Labels, _, ts int64, _ float64, h *histogram.Histogram, fh *histogram.FloatHistogram, opts storage.AOptions) (storage.SeriesRef, error) {
+ switch {
+ case fh != nil, h != nil:
+ t.histograms++
+ default:
+ t.samples++
+ }
+ if ts > t.highestTimestamp {
+ t.highestTimestamp = ts
+ }
+ t.exemplars += int64(len(opts.Exemplars))
+ return ref, nil
+}
diff --git a/storage/series.go b/storage/series.go
index ce989ef846..bf6df7db3e 100644
--- a/storage/series.go
+++ b/storage/series.go
@@ -138,6 +138,11 @@ func (it *listSeriesIterator) AtT() int64 {
return s.T()
}
+func (it *listSeriesIterator) AtST() int64 {
+ s := it.samples.Get(it.idx)
+ return s.ST()
+}
+
func (it *listSeriesIterator) Next() chunkenc.ValueType {
it.idx++
if it.idx >= it.samples.Len() {
@@ -355,18 +360,20 @@ func (s *seriesToChunkEncoder) Iterator(it chunks.Iterator) chunks.Iterator {
lastType = typ
var (
- t int64
- v float64
- h *histogram.Histogram
- fh *histogram.FloatHistogram
+ st, t int64
+ v float64
+ h *histogram.Histogram
+ fh *histogram.FloatHistogram
)
switch typ {
case chunkenc.ValFloat:
t, v = seriesIter.At()
- app.Append(t, v)
+ st = seriesIter.AtST()
+ app.Append(st, t, v)
case chunkenc.ValHistogram:
t, h = seriesIter.AtHistogram(nil)
- newChk, recoded, app, err = app.AppendHistogram(nil, t, h, false)
+ st = seriesIter.AtST()
+ newChk, recoded, app, err = app.AppendHistogram(nil, st, t, h, false)
if err != nil {
return errChunksIterator{err: err}
}
@@ -381,7 +388,8 @@ func (s *seriesToChunkEncoder) Iterator(it chunks.Iterator) chunks.Iterator {
}
case chunkenc.ValFloatHistogram:
t, fh = seriesIter.AtFloatHistogram(nil)
- newChk, recoded, app, err = app.AppendFloatHistogram(nil, t, fh, false)
+ st = seriesIter.AtST()
+ newChk, recoded, app, err = app.AppendFloatHistogram(nil, st, t, fh, false)
if err != nil {
return errChunksIterator{err: err}
}
@@ -440,25 +448,25 @@ func (e errChunksIterator) Err() error { return e.err }
// Optionally it takes samples constructor, useful when you want to compare sample slices with different
// sample implementations. if nil, sample type from this package will be used.
// For float sample, NaN values are replaced with -42.
-func ExpandSamples(iter chunkenc.Iterator, newSampleFn func(t int64, f float64, h *histogram.Histogram, fh *histogram.FloatHistogram) chunks.Sample) ([]chunks.Sample, error) {
+func ExpandSamples(iter chunkenc.Iterator, newSampleFn func(st, t int64, f float64, h *histogram.Histogram, fh *histogram.FloatHistogram) chunks.Sample) ([]chunks.Sample, error) {
return expandSamples(iter, true, newSampleFn)
}
// ExpandSamplesWithoutReplacingNaNs is same as ExpandSamples but it does not replace float sample NaN values with anything.
-func ExpandSamplesWithoutReplacingNaNs(iter chunkenc.Iterator, newSampleFn func(t int64, f float64, h *histogram.Histogram, fh *histogram.FloatHistogram) chunks.Sample) ([]chunks.Sample, error) {
+func ExpandSamplesWithoutReplacingNaNs(iter chunkenc.Iterator, newSampleFn func(st, t int64, f float64, h *histogram.Histogram, fh *histogram.FloatHistogram) chunks.Sample) ([]chunks.Sample, error) {
return expandSamples(iter, false, newSampleFn)
}
-func expandSamples(iter chunkenc.Iterator, replaceNaN bool, newSampleFn func(t int64, f float64, h *histogram.Histogram, fh *histogram.FloatHistogram) chunks.Sample) ([]chunks.Sample, error) {
+func expandSamples(iter chunkenc.Iterator, replaceNaN bool, newSampleFn func(st, t int64, f float64, h *histogram.Histogram, fh *histogram.FloatHistogram) chunks.Sample) ([]chunks.Sample, error) {
if newSampleFn == nil {
- newSampleFn = func(t int64, f float64, h *histogram.Histogram, fh *histogram.FloatHistogram) chunks.Sample {
+ newSampleFn = func(st, t int64, f float64, h *histogram.Histogram, fh *histogram.FloatHistogram) chunks.Sample {
switch {
case h != nil:
- return hSample{t, h}
+ return hSample{st, t, h}
case fh != nil:
- return fhSample{t, fh}
+ return fhSample{st, t, fh}
default:
- return fSample{t, f}
+ return fSample{st, t, f}
}
}
}
@@ -470,17 +478,20 @@ func expandSamples(iter chunkenc.Iterator, replaceNaN bool, newSampleFn func(t i
return result, iter.Err()
case chunkenc.ValFloat:
t, f := iter.At()
+ st := iter.AtST()
// NaNs can't be compared normally, so substitute for another value.
if replaceNaN && math.IsNaN(f) {
f = -42
}
- result = append(result, newSampleFn(t, f, nil, nil))
+ result = append(result, newSampleFn(st, t, f, nil, nil))
case chunkenc.ValHistogram:
t, h := iter.AtHistogram(nil)
- result = append(result, newSampleFn(t, 0, h, nil))
+ st := iter.AtST()
+ result = append(result, newSampleFn(st, t, 0, h, nil))
case chunkenc.ValFloatHistogram:
t, fh := iter.AtFloatHistogram(nil)
- result = append(result, newSampleFn(t, 0, nil, fh))
+ st := iter.AtST()
+ result = append(result, newSampleFn(st, t, 0, nil, fh))
}
}
}
diff --git a/storage/series_test.go b/storage/series_test.go
index 954d62f1b3..b33d6cb1b3 100644
--- a/storage/series_test.go
+++ b/storage/series_test.go
@@ -28,11 +28,11 @@ import (
func TestListSeriesIterator(t *testing.T) {
it := NewListSeriesIterator(samples{
- fSample{0, 0},
- fSample{1, 1},
- fSample{1, 1.5},
- fSample{2, 2},
- fSample{3, 3},
+ fSample{-10, 0, 0},
+ fSample{-9, 1, 1},
+ fSample{-8, 1, 1.5},
+ fSample{-7, 2, 2},
+ fSample{-6, 3, 3},
})
// Seek to the first sample with ts=1.
@@ -40,30 +40,35 @@ func TestListSeriesIterator(t *testing.T) {
ts, v := it.At()
require.Equal(t, int64(1), ts)
require.Equal(t, 1., v)
+ require.Equal(t, int64(-9), it.AtST())
// Seek one further, next sample still has ts=1.
require.Equal(t, chunkenc.ValFloat, it.Next())
ts, v = it.At()
require.Equal(t, int64(1), ts)
require.Equal(t, 1.5, v)
+ require.Equal(t, int64(-8), it.AtST())
// Seek again to 1 and make sure we stay where we are.
require.Equal(t, chunkenc.ValFloat, it.Seek(1))
ts, v = it.At()
require.Equal(t, int64(1), ts)
require.Equal(t, 1.5, v)
+ require.Equal(t, int64(-8), it.AtST())
// Another seek.
require.Equal(t, chunkenc.ValFloat, it.Seek(3))
ts, v = it.At()
require.Equal(t, int64(3), ts)
require.Equal(t, 3., v)
+ require.Equal(t, int64(-6), it.AtST())
// And we don't go back.
require.Equal(t, chunkenc.ValFloat, it.Seek(2))
ts, v = it.At()
require.Equal(t, int64(3), ts)
require.Equal(t, 3., v)
+ require.Equal(t, int64(-6), it.AtST())
// Seek beyond the end.
require.Equal(t, chunkenc.ValNone, it.Seek(5))
diff --git a/tsdb/agent/db.go b/tsdb/agent/db.go
index 7de2ed678f..1b29b223d7 100644
--- a/tsdb/agent/db.go
+++ b/tsdb/agent/db.go
@@ -37,7 +37,6 @@ import (
"github.com/prometheus/prometheus/storage/remote"
"github.com/prometheus/prometheus/tsdb"
"github.com/prometheus/prometheus/tsdb/chunks"
- tsdb_errors "github.com/prometheus/prometheus/tsdb/errors"
"github.com/prometheus/prometheus/tsdb/record"
"github.com/prometheus/prometheus/tsdb/tsdbutil"
"github.com/prometheus/prometheus/tsdb/wlog"
@@ -798,7 +797,7 @@ func (db *DB) Close() error {
db.metrics.Unregister()
- return tsdb_errors.NewMulti(db.locker.Release(), db.wal.Close()).Err()
+ return errors.Join(db.locker.Release(), db.wal.Close())
}
type appenderBase struct {
diff --git a/tsdb/block_test.go b/tsdb/block_test.go
index 855fa5638a..edd2df7415 100644
--- a/tsdb/block_test.go
+++ b/tsdb/block_test.go
@@ -176,7 +176,7 @@ func TestCorruptedChunk(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
tmpdir := t.TempDir()
- series := storage.NewListSeries(labels.FromStrings("a", "b"), []chunks.Sample{sample{1, 1, nil, nil}})
+ series := storage.NewListSeries(labels.FromStrings("a", "b"), []chunks.Sample{sample{0, 1, 1, nil, nil}})
blockDir := createBlock(t, tmpdir, []storage.Series{series})
files, err := sequenceFiles(chunkDir(blockDir))
require.NoError(t, err)
@@ -236,7 +236,7 @@ func TestLabelValuesWithMatchers(t *testing.T) {
seriesEntries = append(seriesEntries, storage.NewListSeries(labels.FromStrings(
"tens", fmt.Sprintf("value%d", i/10),
"unique", fmt.Sprintf("value%d", i),
- ), []chunks.Sample{sample{100, 0, nil, nil}}))
+ ), []chunks.Sample{sample{0, 100, 0, nil, nil}}))
}
blockDir := createBlock(t, tmpdir, seriesEntries)
@@ -319,7 +319,7 @@ func TestBlockQuerierReturnsSortedLabelValues(t *testing.T) {
for i := 100; i > 0; i-- {
seriesEntries = append(seriesEntries, storage.NewListSeries(labels.FromStrings(
"__name__", fmt.Sprintf("value%d", i),
- ), []chunks.Sample{sample{100, 0, nil, nil}}))
+ ), []chunks.Sample{sample{0, 100, 0, nil, nil}}))
}
blockDir := createBlock(t, tmpdir, seriesEntries)
@@ -436,7 +436,7 @@ func BenchmarkLabelValuesWithMatchers(b *testing.B) {
"a_unique", fmt.Sprintf("value%d", i),
"b_tens", fmt.Sprintf("value%d", i/(metricCount/10)),
"c_ninety", fmt.Sprintf("value%d", i/(metricCount/10)/9), // "0" for the first 90%, then "1"
- ), []chunks.Sample{sample{100, 0, nil, nil}}))
+ ), []chunks.Sample{sample{0, 100, 0, nil, nil}}))
}
blockDir := createBlock(b, tmpdir, seriesEntries)
@@ -472,13 +472,13 @@ func TestLabelNamesWithMatchers(t *testing.T) {
for i := range 100 {
seriesEntries = append(seriesEntries, storage.NewListSeries(labels.FromStrings(
"unique", fmt.Sprintf("value%d", i),
- ), []chunks.Sample{sample{100, 0, nil, nil}}))
+ ), []chunks.Sample{sample{0, 100, 0, nil, nil}}))
if i%10 == 0 {
seriesEntries = append(seriesEntries, storage.NewListSeries(labels.FromStrings(
"tens", fmt.Sprintf("value%d", i/10),
"unique", fmt.Sprintf("value%d", i),
- ), []chunks.Sample{sample{100, 0, nil, nil}}))
+ ), []chunks.Sample{sample{0, 100, 0, nil, nil}}))
}
if i%20 == 0 {
@@ -486,7 +486,7 @@ func TestLabelNamesWithMatchers(t *testing.T) {
"tens", fmt.Sprintf("value%d", i/10),
"twenties", fmt.Sprintf("value%d", i/20),
"unique", fmt.Sprintf("value%d", i),
- ), []chunks.Sample{sample{100, 0, nil, nil}}))
+ ), []chunks.Sample{sample{0, 100, 0, nil, nil}}))
}
}
@@ -542,7 +542,7 @@ func TestBlockIndexReader_PostingsForLabelMatching(t *testing.T) {
testPostingsForLabelMatching(t, 2, func(t *testing.T, series []labels.Labels) IndexReader {
var seriesEntries []storage.Series
for _, s := range series {
- seriesEntries = append(seriesEntries, storage.NewListSeries(s, []chunks.Sample{sample{100, 0, nil, nil}}))
+ seriesEntries = append(seriesEntries, storage.NewListSeries(s, []chunks.Sample{sample{0, 100, 0, nil, nil}}))
}
blockDir := createBlock(t, t.TempDir(), seriesEntries)
diff --git a/tsdb/chunkenc/chunk.go b/tsdb/chunkenc/chunk.go
index fed28c5701..711966ec39 100644
--- a/tsdb/chunkenc/chunk.go
+++ b/tsdb/chunkenc/chunk.go
@@ -99,9 +99,9 @@ type Iterable interface {
Iterator(Iterator) Iterator
}
-// Appender adds sample pairs to a chunk.
+// Appender adds sample with start timestamp, timestamp, and value to a chunk.
type Appender interface {
- Append(int64, float64)
+ Append(st, t int64, v float64)
// AppendHistogram and AppendFloatHistogram append a histogram sample to a histogram or float histogram chunk.
// Appending a histogram may require creating a completely new chunk or recoding (changing) the current chunk.
@@ -114,8 +114,8 @@ type Appender interface {
// The returned bool isRecoded can be used to distinguish between the new Chunk c being a completely new Chunk
// or the current Chunk recoded to a new Chunk.
// The Appender app that can be used for the next append is always returned.
- AppendHistogram(prev *HistogramAppender, t int64, h *histogram.Histogram, appendOnly bool) (c Chunk, isRecoded bool, app Appender, err error)
- AppendFloatHistogram(prev *FloatHistogramAppender, t int64, h *histogram.FloatHistogram, appendOnly bool) (c Chunk, isRecoded bool, app Appender, err error)
+ AppendHistogram(prev *HistogramAppender, st, t int64, h *histogram.Histogram, appendOnly bool) (c Chunk, isRecoded bool, app Appender, err error)
+ AppendFloatHistogram(prev *FloatHistogramAppender, st, t int64, h *histogram.FloatHistogram, appendOnly bool) (c Chunk, isRecoded bool, app Appender, err error)
}
// Iterator is a simple iterator that can only get the next value.
@@ -151,6 +151,10 @@ type Iterator interface {
// AtT returns the current timestamp.
// Before the iterator has advanced, the behaviour is unspecified.
AtT() int64
+ // AtST returns the current start timestamp.
+ // Returns 0 if the start timestamp is not implemented or not set.
+ // Before the iterator has advanced, the behaviour is unspecified.
+ AtST() int64
// Err returns the current error. It should be used only after the
// iterator is exhausted, i.e. `Next` or `Seek` have returned ValNone.
Err() error
@@ -208,25 +212,30 @@ func (v ValueType) NewChunk() (Chunk, error) {
}
}
-// MockSeriesIterator returns an iterator for a mock series with custom timeStamps and values.
-func MockSeriesIterator(timestamps []int64, values []float64) Iterator {
+// MockSeriesIterator returns an iterator for a mock series with custom
+// start timestamp, timestamps, and values.
+// Start timestamps is optional, pass nil or empty slice to indicate no start
+// timestamps.
+func MockSeriesIterator(startTimestamps, timestamps []int64, values []float64) Iterator {
return &mockSeriesIterator{
- timeStamps: timestamps,
- values: values,
- currIndex: -1,
+ startTimestamps: startTimestamps,
+ timestamps: timestamps,
+ values: values,
+ currIndex: -1,
}
}
type mockSeriesIterator struct {
- timeStamps []int64
- values []float64
- currIndex int
+ timestamps []int64
+ startTimestamps []int64
+ values []float64
+ currIndex int
}
func (*mockSeriesIterator) Seek(int64) ValueType { return ValNone }
func (it *mockSeriesIterator) At() (int64, float64) {
- return it.timeStamps[it.currIndex], it.values[it.currIndex]
+ return it.timestamps[it.currIndex], it.values[it.currIndex]
}
func (*mockSeriesIterator) AtHistogram(*histogram.Histogram) (int64, *histogram.Histogram) {
@@ -238,11 +247,18 @@ func (*mockSeriesIterator) AtFloatHistogram(*histogram.FloatHistogram) (int64, *
}
func (it *mockSeriesIterator) AtT() int64 {
- return it.timeStamps[it.currIndex]
+ return it.timestamps[it.currIndex]
+}
+
+func (it *mockSeriesIterator) AtST() int64 {
+ if len(it.startTimestamps) == 0 {
+ return 0
+ }
+ return it.startTimestamps[it.currIndex]
}
func (it *mockSeriesIterator) Next() ValueType {
- if it.currIndex < len(it.timeStamps)-1 {
+ if it.currIndex < len(it.timestamps)-1 {
it.currIndex++
return ValFloat
}
@@ -268,8 +284,9 @@ func (nopIterator) AtHistogram(*histogram.Histogram) (int64, *histogram.Histogra
func (nopIterator) AtFloatHistogram(*histogram.FloatHistogram) (int64, *histogram.FloatHistogram) {
return math.MinInt64, nil
}
-func (nopIterator) AtT() int64 { return math.MinInt64 }
-func (nopIterator) Err() error { return nil }
+func (nopIterator) AtT() int64 { return math.MinInt64 }
+func (nopIterator) AtST() int64 { return 0 }
+func (nopIterator) Err() error { return nil }
// Pool is used to create and reuse chunk references to avoid allocations.
type Pool interface {
diff --git a/tsdb/chunkenc/chunk_test.go b/tsdb/chunkenc/chunk_test.go
index d2d0e4c053..41bb23ddd1 100644
--- a/tsdb/chunkenc/chunk_test.go
+++ b/tsdb/chunkenc/chunk_test.go
@@ -65,7 +65,7 @@ func testChunk(t *testing.T, c Chunk) {
require.NoError(t, err)
}
- app.Append(ts, v)
+ app.Append(0, ts, v)
exp = append(exp, pair{t: ts, v: v})
}
@@ -226,7 +226,7 @@ func benchmarkIterator(b *testing.B, newChunk func() Chunk) {
if j > 250 {
break
}
- a.Append(p.t, p.v)
+ a.Append(0, p.t, p.v)
j++
}
}
@@ -303,7 +303,7 @@ func benchmarkAppender(b *testing.B, deltas func() (int64, float64), newChunk fu
b.Fatalf("get appender: %s", err)
}
for _, p := range exp {
- a.Append(p.t, p.v)
+ a.Append(0, p.t, p.v)
}
}
}
diff --git a/tsdb/chunkenc/float_histogram.go b/tsdb/chunkenc/float_histogram.go
index 797bc596b5..6af2fa68e2 100644
--- a/tsdb/chunkenc/float_histogram.go
+++ b/tsdb/chunkenc/float_histogram.go
@@ -195,7 +195,7 @@ func (a *FloatHistogramAppender) NumSamples() int {
// Append implements Appender. This implementation panics because normal float
// samples must never be appended to a histogram chunk.
-func (*FloatHistogramAppender) Append(int64, float64) {
+func (*FloatHistogramAppender) Append(int64, int64, float64) {
panic("appended a float sample to a histogram chunk")
}
@@ -682,11 +682,11 @@ func (*FloatHistogramAppender) recodeHistogram(
}
}
-func (*FloatHistogramAppender) AppendHistogram(*HistogramAppender, int64, *histogram.Histogram, bool) (Chunk, bool, Appender, error) {
+func (*FloatHistogramAppender) AppendHistogram(*HistogramAppender, int64, int64, *histogram.Histogram, bool) (Chunk, bool, Appender, error) {
panic("appended a histogram sample to a float histogram chunk")
}
-func (a *FloatHistogramAppender) AppendFloatHistogram(prev *FloatHistogramAppender, t int64, h *histogram.FloatHistogram, appendOnly bool) (Chunk, bool, Appender, error) {
+func (a *FloatHistogramAppender) AppendFloatHistogram(prev *FloatHistogramAppender, _, t int64, h *histogram.FloatHistogram, appendOnly bool) (Chunk, bool, Appender, error) {
if a.NumSamples() == 0 {
a.appendFloatHistogram(t, h)
if h.CounterResetHint == histogram.GaugeType {
@@ -938,6 +938,10 @@ func (it *floatHistogramIterator) AtT() int64 {
return it.t
}
+func (*floatHistogramIterator) AtST() int64 {
+ return 0
+}
+
func (it *floatHistogramIterator) Err() error {
return it.err
}
diff --git a/tsdb/chunkenc/float_histogram_test.go b/tsdb/chunkenc/float_histogram_test.go
index f27de97516..cbeb3171ce 100644
--- a/tsdb/chunkenc/float_histogram_test.go
+++ b/tsdb/chunkenc/float_histogram_test.go
@@ -63,7 +63,7 @@ func TestFirstFloatHistogramExplicitCounterReset(t *testing.T) {
chk := NewFloatHistogramChunk()
app, err := chk.Appender()
require.NoError(t, err)
- newChk, recoded, newApp, err := app.AppendFloatHistogram(nil, 0, h, false)
+ newChk, recoded, newApp, err := app.AppendFloatHistogram(nil, 0, 0, h, false)
require.NoError(t, err)
require.Nil(t, newChk)
require.False(t, recoded)
@@ -101,7 +101,7 @@ func TestFloatHistogramChunkSameBuckets(t *testing.T) {
},
NegativeBuckets: []int64{2, 1, -1, -1}, // counts: 2, 3, 2, 1 (total 8)
}
- chk, _, app, err := app.AppendFloatHistogram(nil, ts, h.ToFloat(nil), false)
+ chk, _, app, err := app.AppendFloatHistogram(nil, 0, ts, h.ToFloat(nil), false)
require.NoError(t, err)
require.Nil(t, chk)
exp = append(exp, floatResult{t: ts, h: h.ToFloat(nil)})
@@ -115,7 +115,7 @@ func TestFloatHistogramChunkSameBuckets(t *testing.T) {
h.Sum = 24.4
h.PositiveBuckets = []int64{5, -2, 1, -2} // counts: 5, 3, 4, 2 (total 14)
h.NegativeBuckets = []int64{4, -1, 1, -1} // counts: 4, 3, 4, 4 (total 15)
- chk, _, _, err = app.AppendFloatHistogram(nil, ts, h.ToFloat(nil), false)
+ chk, _, _, err = app.AppendFloatHistogram(nil, 0, ts, h.ToFloat(nil), false)
require.NoError(t, err)
require.Nil(t, chk)
expH := h.ToFloat(nil)
@@ -134,7 +134,7 @@ func TestFloatHistogramChunkSameBuckets(t *testing.T) {
h.Sum = 24.4
h.PositiveBuckets = []int64{6, 1, -3, 6} // counts: 6, 7, 4, 10 (total 27)
h.NegativeBuckets = []int64{5, 1, -2, 3} // counts: 5, 6, 4, 7 (total 22)
- chk, _, _, err = app.AppendFloatHistogram(nil, ts, h.ToFloat(nil), false)
+ chk, _, _, err = app.AppendFloatHistogram(nil, 0, ts, h.ToFloat(nil), false)
require.NoError(t, err)
require.Nil(t, chk)
expH = h.ToFloat(nil)
@@ -224,7 +224,7 @@ func TestFloatHistogramChunkBucketChanges(t *testing.T) {
NegativeBuckets: []int64{1},
}
- chk, _, app, err := app.AppendFloatHistogram(nil, ts1, h1.ToFloat(nil), false)
+ chk, _, app, err := app.AppendFloatHistogram(nil, 0, ts1, h1.ToFloat(nil), false)
require.NoError(t, err)
require.Nil(t, chk)
require.Equal(t, 1, c.NumSamples())
@@ -260,7 +260,7 @@ func TestFloatHistogramChunkBucketChanges(t *testing.T) {
require.True(t, ok) // Only new buckets came in.
require.False(t, cr)
c, app = hApp.recode(posInterjections, negInterjections, h2.PositiveSpans, h2.NegativeSpans)
- chk, _, _, err = app.AppendFloatHistogram(nil, ts2, h2.ToFloat(nil), false)
+ chk, _, _, err = app.AppendFloatHistogram(nil, 0, ts2, h2.ToFloat(nil), false)
require.NoError(t, err)
require.Nil(t, chk)
require.Equal(t, 2, c.NumSamples())
@@ -330,7 +330,7 @@ func TestFloatHistogramChunkAppendable(t *testing.T) {
ts := int64(1234567890)
- chk, _, app, err := app.AppendFloatHistogram(nil, ts, h.Copy(), false)
+ chk, _, app, err := app.AppendFloatHistogram(nil, 0, ts, h.Copy(), false)
require.NoError(t, err)
require.Nil(t, chk)
require.Equal(t, 1, c.NumSamples())
@@ -557,7 +557,7 @@ func TestFloatHistogramChunkAppendable(t *testing.T) {
nextChunk := NewFloatHistogramChunk()
app, err := nextChunk.Appender()
require.NoError(t, err)
- newChunk, recoded, newApp, err := app.AppendFloatHistogram(hApp, ts+1, h2, false)
+ newChunk, recoded, newApp, err := app.AppendFloatHistogram(hApp, 0, ts+1, h2, false)
require.NoError(t, err)
require.Nil(t, newChunk)
require.False(t, recoded)
@@ -575,7 +575,7 @@ func TestFloatHistogramChunkAppendable(t *testing.T) {
nextChunk := NewFloatHistogramChunk()
app, err := nextChunk.Appender()
require.NoError(t, err)
- newChunk, recoded, newApp, err := app.AppendFloatHistogram(hApp, ts+1, h2, false)
+ newChunk, recoded, newApp, err := app.AppendFloatHistogram(hApp, 0, ts+1, h2, false)
require.NoError(t, err)
require.Nil(t, newChunk)
require.False(t, recoded)
@@ -602,7 +602,7 @@ func TestFloatHistogramChunkAppendable(t *testing.T) {
nextChunk := NewFloatHistogramChunk()
app, err := nextChunk.Appender()
require.NoError(t, err)
- newChunk, recoded, newApp, err := app.AppendFloatHistogram(hApp, ts+1, h2, false)
+ newChunk, recoded, newApp, err := app.AppendFloatHistogram(hApp, 0, ts+1, h2, false)
require.NoError(t, err)
require.Nil(t, newChunk)
require.False(t, recoded)
@@ -717,7 +717,7 @@ func TestFloatHistogramChunkAppendable(t *testing.T) {
func assertNewFloatHistogramChunkOnAppend(t *testing.T, oldChunk Chunk, hApp *FloatHistogramAppender, ts int64, h *histogram.FloatHistogram, expectHeader CounterResetHeader, expectHint histogram.CounterResetHint) {
oldChunkBytes := oldChunk.Bytes()
- newChunk, recoded, newAppender, err := hApp.AppendFloatHistogram(nil, ts, h, false)
+ newChunk, recoded, newAppender, err := hApp.AppendFloatHistogram(nil, 0, ts, h, false)
require.Equal(t, oldChunkBytes, oldChunk.Bytes()) // Sanity check that previous chunk is untouched.
require.NoError(t, err)
require.NotNil(t, newChunk)
@@ -732,7 +732,7 @@ func assertNewFloatHistogramChunkOnAppend(t *testing.T, oldChunk Chunk, hApp *Fl
func assertNoNewFloatHistogramChunkOnAppend(t *testing.T, oldChunk Chunk, hApp *FloatHistogramAppender, ts int64, h *histogram.FloatHistogram, expectHeader CounterResetHeader) {
oldChunkBytes := oldChunk.Bytes()
- newChunk, recoded, newAppender, err := hApp.AppendFloatHistogram(nil, ts, h, false)
+ newChunk, recoded, newAppender, err := hApp.AppendFloatHistogram(nil, 0, ts, h, false)
require.Greater(t, len(oldChunk.Bytes()), len(oldChunkBytes)) // Check that current chunk is bigger than previously.
require.NoError(t, err)
require.Nil(t, newChunk)
@@ -745,7 +745,7 @@ func assertNoNewFloatHistogramChunkOnAppend(t *testing.T, oldChunk Chunk, hApp *
func assertRecodedFloatHistogramChunkOnAppend(t *testing.T, prevChunk Chunk, hApp *FloatHistogramAppender, ts int64, h *histogram.FloatHistogram, expectHeader CounterResetHeader) {
prevChunkBytes := prevChunk.Bytes()
- newChunk, recoded, newAppender, err := hApp.AppendFloatHistogram(nil, ts, h, false)
+ newChunk, recoded, newAppender, err := hApp.AppendFloatHistogram(nil, 0, ts, h, false)
require.Equal(t, prevChunkBytes, prevChunk.Bytes()) // Sanity check that previous chunk is untouched. This may change in the future if we implement in-place recoding.
require.NoError(t, err)
require.NotNil(t, newChunk)
@@ -959,7 +959,7 @@ func TestFloatHistogramChunkAppendableWithEmptySpan(t *testing.T) {
require.NoError(t, err)
require.Equal(t, 0, c.NumSamples())
- _, _, _, err = app.AppendFloatHistogram(nil, 1, tc.h1, true)
+ _, _, _, err = app.AppendFloatHistogram(nil, 0, 1, tc.h1, true)
require.NoError(t, err)
require.Equal(t, 1, c.NumSamples())
hApp, _ := app.(*FloatHistogramAppender)
@@ -1019,7 +1019,7 @@ func TestFloatHistogramChunkAppendableGauge(t *testing.T) {
ts := int64(1234567890)
- chk, _, app, err := app.AppendFloatHistogram(nil, ts, h.Copy(), false)
+ chk, _, app, err := app.AppendFloatHistogram(nil, 0, ts, h.Copy(), false)
require.NoError(t, err)
require.Nil(t, chk)
require.Equal(t, 1, c.NumSamples())
@@ -1259,7 +1259,7 @@ func TestFloatHistogramAppendOnlyErrors(t *testing.T) {
h := tsdbutil.GenerateTestFloatHistogram(0)
var isRecoded bool
- c, isRecoded, app, err = app.AppendFloatHistogram(nil, 1, h, true)
+ c, isRecoded, app, err = app.AppendFloatHistogram(nil, 0, 1, h, true)
require.Nil(t, c)
require.False(t, isRecoded)
require.NoError(t, err)
@@ -1267,7 +1267,7 @@ func TestFloatHistogramAppendOnlyErrors(t *testing.T) {
// Add erroring histogram.
h2 := h.Copy()
h2.Schema++
- c, isRecoded, _, err = app.AppendFloatHistogram(nil, 2, h2, true)
+ c, isRecoded, _, err = app.AppendFloatHistogram(nil, 0, 2, h2, true)
require.Nil(t, c)
require.False(t, isRecoded)
require.EqualError(t, err, "float histogram schema change")
@@ -1281,7 +1281,7 @@ func TestFloatHistogramAppendOnlyErrors(t *testing.T) {
h := tsdbutil.GenerateTestFloatHistogram(0)
var isRecoded bool
- c, isRecoded, app, err = app.AppendFloatHistogram(nil, 1, h, true)
+ c, isRecoded, app, err = app.AppendFloatHistogram(nil, 0, 1, h, true)
require.Nil(t, c)
require.False(t, isRecoded)
require.NoError(t, err)
@@ -1289,7 +1289,7 @@ func TestFloatHistogramAppendOnlyErrors(t *testing.T) {
// Add erroring histogram.
h2 := h.Copy()
h2.CounterResetHint = histogram.CounterReset
- c, isRecoded, _, err = app.AppendFloatHistogram(nil, 2, h2, true)
+ c, isRecoded, _, err = app.AppendFloatHistogram(nil, 0, 2, h2, true)
require.Nil(t, c)
require.False(t, isRecoded)
require.EqualError(t, err, "float histogram counter reset")
@@ -1303,7 +1303,7 @@ func TestFloatHistogramAppendOnlyErrors(t *testing.T) {
h := tsdbutil.GenerateTestCustomBucketsFloatHistogram(0)
var isRecoded bool
- c, isRecoded, app, err = app.AppendFloatHistogram(nil, 1, h, true)
+ c, isRecoded, app, err = app.AppendFloatHistogram(nil, 0, 1, h, true)
require.Nil(t, c)
require.False(t, isRecoded)
require.NoError(t, err)
@@ -1311,7 +1311,7 @@ func TestFloatHistogramAppendOnlyErrors(t *testing.T) {
// Add erroring histogram.
h2 := h.Copy()
h2.CustomValues = []float64{0, 1, 2, 3, 4, 5, 6, 7}
- c, isRecoded, _, err = app.AppendFloatHistogram(nil, 2, h2, true)
+ c, isRecoded, _, err = app.AppendFloatHistogram(nil, 0, 2, h2, true)
require.Nil(t, c)
require.False(t, isRecoded)
require.EqualError(t, err, "float histogram counter reset")
@@ -1344,10 +1344,10 @@ func TestFloatHistogramUniqueSpansAfterNext(t *testing.T) {
app, err := c.Appender()
require.NoError(t, err)
- _, _, _, err = app.AppendFloatHistogram(nil, 0, h1, false)
+ _, _, _, err = app.AppendFloatHistogram(nil, 0, 0, h1, false)
require.NoError(t, err)
- _, _, _, err = app.AppendFloatHistogram(nil, 1, h2, false)
+ _, _, _, err = app.AppendFloatHistogram(nil, 0, 1, h2, false)
require.NoError(t, err)
// Create an iterator and advance to the first histogram.
@@ -1390,10 +1390,10 @@ func TestFloatHistogramUniqueCustomValuesAfterNext(t *testing.T) {
app, err := c.Appender()
require.NoError(t, err)
- _, _, _, err = app.AppendFloatHistogram(nil, 0, h1, false)
+ _, _, _, err = app.AppendFloatHistogram(nil, 0, 0, h1, false)
require.NoError(t, err)
- _, _, _, err = app.AppendFloatHistogram(nil, 1, h2, false)
+ _, _, _, err = app.AppendFloatHistogram(nil, 0, 1, h2, false)
require.NoError(t, err)
// Create an iterator and advance to the first histogram.
@@ -1435,7 +1435,7 @@ func TestFloatHistogramEmptyBucketsWithGaps(t *testing.T) {
c := NewFloatHistogramChunk()
app, err := c.Appender()
require.NoError(t, err)
- _, _, _, err = app.AppendFloatHistogram(nil, 1, h1, false)
+ _, _, _, err = app.AppendFloatHistogram(nil, 0, 1, h1, false)
require.NoError(t, err)
h2 := &histogram.FloatHistogram{
@@ -1448,7 +1448,7 @@ func TestFloatHistogramEmptyBucketsWithGaps(t *testing.T) {
}
require.NoError(t, h2.Validate())
- newC, recoded, _, err := app.AppendFloatHistogram(nil, 2, h2, false)
+ newC, recoded, _, err := app.AppendFloatHistogram(nil, 0, 2, h2, false)
require.NoError(t, err)
require.True(t, recoded)
require.NotNil(t, newC)
@@ -1483,7 +1483,7 @@ func TestFloatHistogramIteratorFailIfSchemaInValid(t *testing.T) {
app, err := c.Appender()
require.NoError(t, err)
- _, _, _, err = app.AppendFloatHistogram(nil, 1, h, false)
+ _, _, _, err = app.AppendFloatHistogram(nil, 0, 1, h, false)
require.NoError(t, err)
it := c.Iterator(nil)
@@ -1512,7 +1512,7 @@ func TestFloatHistogramIteratorReduceSchema(t *testing.T) {
app, err := c.Appender()
require.NoError(t, err)
- _, _, _, err = app.AppendFloatHistogram(nil, 1, h, false)
+ _, _, _, err = app.AppendFloatHistogram(nil, 0, 1, h, false)
require.NoError(t, err)
it := c.Iterator(nil)
diff --git a/tsdb/chunkenc/histogram.go b/tsdb/chunkenc/histogram.go
index e05c49c81d..4e77f387d3 100644
--- a/tsdb/chunkenc/histogram.go
+++ b/tsdb/chunkenc/histogram.go
@@ -219,7 +219,7 @@ func (a *HistogramAppender) NumSamples() int {
// Append implements Appender. This implementation panics because normal float
// samples must never be appended to a histogram chunk.
-func (*HistogramAppender) Append(int64, float64) {
+func (*HistogramAppender) Append(int64, int64, float64) {
panic("appended a float sample to a histogram chunk")
}
@@ -734,11 +734,11 @@ func (a *HistogramAppender) writeSumDelta(v float64) {
xorWrite(a.b, v, a.sum, &a.leading, &a.trailing)
}
-func (*HistogramAppender) AppendFloatHistogram(*FloatHistogramAppender, int64, *histogram.FloatHistogram, bool) (Chunk, bool, Appender, error) {
+func (*HistogramAppender) AppendFloatHistogram(*FloatHistogramAppender, int64, int64, *histogram.FloatHistogram, bool) (Chunk, bool, Appender, error) {
panic("appended a float histogram sample to a histogram chunk")
}
-func (a *HistogramAppender) AppendHistogram(prev *HistogramAppender, t int64, h *histogram.Histogram, appendOnly bool) (Chunk, bool, Appender, error) {
+func (a *HistogramAppender) AppendHistogram(prev *HistogramAppender, _, t int64, h *histogram.Histogram, appendOnly bool) (Chunk, bool, Appender, error) {
if a.NumSamples() == 0 {
a.appendHistogram(t, h)
if h.CounterResetHint == histogram.GaugeType {
@@ -1075,6 +1075,10 @@ func (it *histogramIterator) AtT() int64 {
return it.t
}
+func (*histogramIterator) AtST() int64 {
+ return 0
+}
+
func (it *histogramIterator) Err() error {
return it.err
}
diff --git a/tsdb/chunkenc/histogram_test.go b/tsdb/chunkenc/histogram_test.go
index 38bbd58465..6ac8500e64 100644
--- a/tsdb/chunkenc/histogram_test.go
+++ b/tsdb/chunkenc/histogram_test.go
@@ -64,7 +64,7 @@ func TestFirstHistogramExplicitCounterReset(t *testing.T) {
chk := NewHistogramChunk()
app, err := chk.Appender()
require.NoError(t, err)
- newChk, recoded, newApp, err := app.AppendHistogram(nil, 0, h, false)
+ newChk, recoded, newApp, err := app.AppendHistogram(nil, 0, 0, h, false)
require.NoError(t, err)
require.Nil(t, newChk)
require.False(t, recoded)
@@ -102,7 +102,7 @@ func TestHistogramChunkSameBuckets(t *testing.T) {
},
NegativeBuckets: []int64{2, 1, -1, -1}, // counts: 2, 3, 2, 1 (total 8)
}
- chk, _, app, err := app.AppendHistogram(nil, ts, h, false)
+ chk, _, app, err := app.AppendHistogram(nil, 0, ts, h, false)
require.NoError(t, err)
require.Nil(t, chk)
exp = append(exp, result{t: ts, h: h, fh: h.ToFloat(nil)})
@@ -116,7 +116,7 @@ func TestHistogramChunkSameBuckets(t *testing.T) {
h.Sum = 24.4
h.PositiveBuckets = []int64{5, -2, 1, -2} // counts: 5, 3, 4, 2 (total 14)
h.NegativeBuckets = []int64{4, -1, 1, -1} // counts: 4, 3, 4, 4 (total 15)
- chk, _, _, err = app.AppendHistogram(nil, ts, h, false)
+ chk, _, _, err = app.AppendHistogram(nil, 0, ts, h, false)
require.NoError(t, err)
require.Nil(t, chk)
hExp := h.Copy()
@@ -135,7 +135,7 @@ func TestHistogramChunkSameBuckets(t *testing.T) {
h.Sum = 24.4
h.PositiveBuckets = []int64{6, 1, -3, 6} // counts: 6, 7, 4, 10 (total 27)
h.NegativeBuckets = []int64{5, 1, -2, 3} // counts: 5, 6, 4, 7 (total 22)
- chk, _, _, err = app.AppendHistogram(nil, ts, h, false)
+ chk, _, _, err = app.AppendHistogram(nil, 0, ts, h, false)
require.NoError(t, err)
require.Nil(t, chk)
hExp = h.Copy()
@@ -235,7 +235,7 @@ func TestHistogramChunkBucketChanges(t *testing.T) {
NegativeBuckets: []int64{1},
}
- chk, _, app, err := app.AppendHistogram(nil, ts1, h1, false)
+ chk, _, app, err := app.AppendHistogram(nil, 0, ts1, h1, false)
require.NoError(t, err)
require.Nil(t, chk)
require.Equal(t, 1, c.NumSamples())
@@ -271,7 +271,7 @@ func TestHistogramChunkBucketChanges(t *testing.T) {
require.True(t, ok) // Only new buckets came in.
require.Equal(t, NotCounterReset, cr)
c, app = hApp.recode(posInterjections, negInterjections, h2.PositiveSpans, h2.NegativeSpans)
- chk, _, _, err = app.AppendHistogram(nil, ts2, h2, false)
+ chk, _, _, err = app.AppendHistogram(nil, 0, ts2, h2, false)
require.NoError(t, err)
require.Nil(t, chk)
@@ -344,7 +344,7 @@ func TestHistogramChunkAppendable(t *testing.T) {
ts := int64(1234567890)
- chk, _, app, err := app.AppendHistogram(nil, ts, h.Copy(), false)
+ chk, _, app, err := app.AppendHistogram(nil, 0, ts, h.Copy(), false)
require.NoError(t, err)
require.Nil(t, chk)
require.Equal(t, 1, c.NumSamples())
@@ -581,7 +581,7 @@ func TestHistogramChunkAppendable(t *testing.T) {
nextChunk := NewHistogramChunk()
app, err := nextChunk.Appender()
require.NoError(t, err)
- newChunk, recoded, newApp, err := app.AppendHistogram(hApp, ts+1, h2, false)
+ newChunk, recoded, newApp, err := app.AppendHistogram(hApp, 0, ts+1, h2, false)
require.NoError(t, err)
require.Nil(t, newChunk)
require.False(t, recoded)
@@ -599,7 +599,7 @@ func TestHistogramChunkAppendable(t *testing.T) {
nextChunk := NewHistogramChunk()
app, err := nextChunk.Appender()
require.NoError(t, err)
- newChunk, recoded, newApp, err := app.AppendHistogram(hApp, ts+1, h2, false)
+ newChunk, recoded, newApp, err := app.AppendHistogram(hApp, 0, ts+1, h2, false)
require.NoError(t, err)
require.Nil(t, newChunk)
require.False(t, recoded)
@@ -629,7 +629,7 @@ func TestHistogramChunkAppendable(t *testing.T) {
nextChunk := NewHistogramChunk()
app, err := nextChunk.Appender()
require.NoError(t, err)
- newChunk, recoded, newApp, err := app.AppendHistogram(hApp, ts+1, h2, false)
+ newChunk, recoded, newApp, err := app.AppendHistogram(hApp, 0, ts+1, h2, false)
require.NoError(t, err)
require.Nil(t, newChunk)
require.False(t, recoded)
@@ -776,7 +776,7 @@ func TestHistogramChunkAppendable(t *testing.T) {
func assertNewHistogramChunkOnAppend(t *testing.T, oldChunk Chunk, hApp *HistogramAppender, ts int64, h *histogram.Histogram, expectHeader CounterResetHeader, expectHint histogram.CounterResetHint) {
oldChunkBytes := oldChunk.Bytes()
- newChunk, recoded, newAppender, err := hApp.AppendHistogram(nil, ts, h, false)
+ newChunk, recoded, newAppender, err := hApp.AppendHistogram(nil, 0, ts, h, false)
require.Equal(t, oldChunkBytes, oldChunk.Bytes()) // Sanity check that previous chunk is untouched.
require.NoError(t, err)
require.NotNil(t, newChunk)
@@ -791,7 +791,7 @@ func assertNewHistogramChunkOnAppend(t *testing.T, oldChunk Chunk, hApp *Histogr
func assertNoNewHistogramChunkOnAppend(t *testing.T, currChunk Chunk, hApp *HistogramAppender, ts int64, h *histogram.Histogram, expectHeader CounterResetHeader) {
prevChunkBytes := currChunk.Bytes()
- newChunk, recoded, newAppender, err := hApp.AppendHistogram(nil, ts, h, false)
+ newChunk, recoded, newAppender, err := hApp.AppendHistogram(nil, 0, ts, h, false)
require.Greater(t, len(currChunk.Bytes()), len(prevChunkBytes)) // Check that current chunk is bigger than previously.
require.NoError(t, err)
require.Nil(t, newChunk)
@@ -804,7 +804,7 @@ func assertNoNewHistogramChunkOnAppend(t *testing.T, currChunk Chunk, hApp *Hist
func assertRecodedHistogramChunkOnAppend(t *testing.T, prevChunk Chunk, hApp *HistogramAppender, ts int64, h *histogram.Histogram, expectHeader CounterResetHeader) {
prevChunkBytes := prevChunk.Bytes()
- newChunk, recoded, newAppender, err := hApp.AppendHistogram(nil, ts, h, false)
+ newChunk, recoded, newAppender, err := hApp.AppendHistogram(nil, 0, ts, h, false)
require.Equal(t, prevChunkBytes, prevChunk.Bytes()) // Sanity check that previous chunk is untouched. This may change in the future if we implement in-place recoding.
require.NoError(t, err)
require.NotNil(t, newChunk)
@@ -1029,7 +1029,7 @@ func TestHistogramChunkAppendableWithEmptySpan(t *testing.T) {
require.NoError(t, err)
require.Equal(t, 0, c.NumSamples())
- _, _, _, err = app.AppendHistogram(nil, 1, tc.h1, true)
+ _, _, _, err = app.AppendHistogram(nil, 1, 0, tc.h1, true)
require.NoError(t, err)
require.Equal(t, 1, c.NumSamples())
hApp, _ := app.(*HistogramAppender)
@@ -1172,7 +1172,7 @@ func TestAtFloatHistogram(t *testing.T) {
app, err := chk.Appender()
require.NoError(t, err)
for i := range input {
- newc, _, _, err := app.AppendHistogram(nil, int64(i), &input[i], false)
+ newc, _, _, err := app.AppendHistogram(nil, 0, int64(i), &input[i], false)
require.NoError(t, err)
require.Nil(t, newc)
}
@@ -1230,7 +1230,7 @@ func TestHistogramChunkAppendableGauge(t *testing.T) {
ts := int64(1234567890)
- chk, _, app, err := app.AppendHistogram(nil, ts, h.Copy(), false)
+ chk, _, app, err := app.AppendHistogram(nil, 0, ts, h.Copy(), false)
require.NoError(t, err)
require.Nil(t, chk)
require.Equal(t, 1, c.NumSamples())
@@ -1471,7 +1471,7 @@ func TestHistogramAppendOnlyErrors(t *testing.T) {
h := tsdbutil.GenerateTestHistogram(0)
var isRecoded bool
- c, isRecoded, app, err = app.AppendHistogram(nil, 1, h, true)
+ c, isRecoded, app, err = app.AppendHistogram(nil, 0, 1, h, true)
require.Nil(t, c)
require.False(t, isRecoded)
require.NoError(t, err)
@@ -1479,7 +1479,7 @@ func TestHistogramAppendOnlyErrors(t *testing.T) {
// Add erroring histogram.
h2 := h.Copy()
h2.Schema++
- c, isRecoded, _, err = app.AppendHistogram(nil, 2, h2, true)
+ c, isRecoded, _, err = app.AppendHistogram(nil, 0, 2, h2, true)
require.Nil(t, c)
require.False(t, isRecoded)
require.EqualError(t, err, "histogram schema change")
@@ -1493,7 +1493,7 @@ func TestHistogramAppendOnlyErrors(t *testing.T) {
h := tsdbutil.GenerateTestHistogram(0)
var isRecoded bool
- c, isRecoded, app, err = app.AppendHistogram(nil, 1, h, true)
+ c, isRecoded, app, err = app.AppendHistogram(nil, 0, 1, h, true)
require.Nil(t, c)
require.False(t, isRecoded)
require.NoError(t, err)
@@ -1501,7 +1501,7 @@ func TestHistogramAppendOnlyErrors(t *testing.T) {
// Add erroring histogram.
h2 := h.Copy()
h2.CounterResetHint = histogram.CounterReset
- c, isRecoded, _, err = app.AppendHistogram(nil, 2, h2, true)
+ c, isRecoded, _, err = app.AppendHistogram(nil, 0, 2, h2, true)
require.Nil(t, c)
require.False(t, isRecoded)
require.EqualError(t, err, "histogram counter reset")
@@ -1515,7 +1515,7 @@ func TestHistogramAppendOnlyErrors(t *testing.T) {
h := tsdbutil.GenerateTestCustomBucketsHistogram(0)
var isRecoded bool
- c, isRecoded, app, err = app.AppendHistogram(nil, 1, h, true)
+ c, isRecoded, app, err = app.AppendHistogram(nil, 0, 1, h, true)
require.Nil(t, c)
require.False(t, isRecoded)
require.NoError(t, err)
@@ -1523,7 +1523,7 @@ func TestHistogramAppendOnlyErrors(t *testing.T) {
// Add erroring histogram.
h2 := h.Copy()
h2.CustomValues = []float64{0, 1, 2, 3, 4, 5, 6, 7}
- c, isRecoded, _, err = app.AppendHistogram(nil, 2, h2, true)
+ c, isRecoded, _, err = app.AppendHistogram(nil, 0, 2, h2, true)
require.Nil(t, c)
require.False(t, isRecoded)
require.EqualError(t, err, "histogram counter reset")
@@ -1556,10 +1556,10 @@ func TestHistogramUniqueSpansAfterNextWithAtHistogram(t *testing.T) {
app, err := c.Appender()
require.NoError(t, err)
- _, _, _, err = app.AppendHistogram(nil, 0, h1, false)
+ _, _, _, err = app.AppendHistogram(nil, 0, 0, h1, false)
require.NoError(t, err)
- _, _, _, err = app.AppendHistogram(nil, 1, h2, false)
+ _, _, _, err = app.AppendHistogram(nil, 0, 1, h2, false)
require.NoError(t, err)
// Create an iterator and advance to the first histogram.
@@ -1607,10 +1607,10 @@ func TestHistogramUniqueSpansAfterNextWithAtFloatHistogram(t *testing.T) {
app, err := c.Appender()
require.NoError(t, err)
- _, _, _, err = app.AppendHistogram(nil, 0, h1, false)
+ _, _, _, err = app.AppendHistogram(nil, 0, 0, h1, false)
require.NoError(t, err)
- _, _, _, err = app.AppendHistogram(nil, 1, h2, false)
+ _, _, _, err = app.AppendHistogram(nil, 0, 1, h2, false)
require.NoError(t, err)
// Create an iterator and advance to the first histogram.
@@ -1653,10 +1653,10 @@ func TestHistogramCustomValuesInternedAfterNextWithAtHistogram(t *testing.T) {
app, err := c.Appender()
require.NoError(t, err)
- _, _, _, err = app.AppendHistogram(nil, 0, h1, false)
+ _, _, _, err = app.AppendHistogram(nil, 0, 0, h1, false)
require.NoError(t, err)
- _, _, _, err = app.AppendHistogram(nil, 1, h2, false)
+ _, _, _, err = app.AppendHistogram(nil, 0, 1, h2, false)
require.NoError(t, err)
// Create an iterator and advance to the first histogram.
@@ -1699,10 +1699,10 @@ func TestHistogramCustomValuesInternedAfterNextWithAtFloatHistogram(t *testing.T
app, err := c.Appender()
require.NoError(t, err)
- _, _, _, err = app.AppendHistogram(nil, 0, h1, false)
+ _, _, _, err = app.AppendHistogram(nil, 0, 0, h1, false)
require.NoError(t, err)
- _, _, _, err = app.AppendHistogram(nil, 1, h2, false)
+ _, _, _, err = app.AppendHistogram(nil, 0, 1, h2, false)
require.NoError(t, err)
// Create an iterator and advance to the first histogram.
@@ -1754,7 +1754,7 @@ func BenchmarkAppendable(b *testing.B) {
b.Fatal(err)
}
- _, _, _, err = app.AppendHistogram(nil, 1, h, true)
+ _, _, _, err = app.AppendHistogram(nil, 0, 1, h, true)
if err != nil {
b.Fatal(err)
}
@@ -1791,7 +1791,7 @@ func TestIntHistogramEmptyBucketsWithGaps(t *testing.T) {
c := NewHistogramChunk()
app, err := c.Appender()
require.NoError(t, err)
- _, _, _, err = app.AppendHistogram(nil, 1, h1, false)
+ _, _, _, err = app.AppendHistogram(nil, 0, 1, h1, false)
require.NoError(t, err)
h2 := &histogram.Histogram{
@@ -1804,7 +1804,7 @@ func TestIntHistogramEmptyBucketsWithGaps(t *testing.T) {
}
require.NoError(t, h2.Validate())
- newC, recoded, _, err := app.AppendHistogram(nil, 2, h2, false)
+ newC, recoded, _, err := app.AppendHistogram(nil, 0, 2, h2, false)
require.NoError(t, err)
require.True(t, recoded)
require.NotNil(t, newC)
@@ -1839,7 +1839,7 @@ func TestHistogramIteratorFailIfSchemaInValid(t *testing.T) {
app, err := c.Appender()
require.NoError(t, err)
- _, _, _, err = app.AppendHistogram(nil, 1, h, false)
+ _, _, _, err = app.AppendHistogram(nil, 0, 1, h, false)
require.NoError(t, err)
it := c.Iterator(nil)
@@ -1868,7 +1868,7 @@ func TestHistogramIteratorReduceSchema(t *testing.T) {
app, err := c.Appender()
require.NoError(t, err)
- _, _, _, err = app.AppendHistogram(nil, 1, h, false)
+ _, _, _, err = app.AppendHistogram(nil, 0, 1, h, false)
require.NoError(t, err)
it := c.Iterator(nil)
diff --git a/tsdb/chunkenc/xor.go b/tsdb/chunkenc/xor.go
index bbe12a893b..5a9a59dc22 100644
--- a/tsdb/chunkenc/xor.go
+++ b/tsdb/chunkenc/xor.go
@@ -158,7 +158,7 @@ type xorAppender struct {
trailing uint8
}
-func (a *xorAppender) Append(t int64, v float64) {
+func (a *xorAppender) Append(_, t int64, v float64) {
var tDelta uint64
num := binary.BigEndian.Uint16(a.b.bytes())
switch num {
@@ -225,11 +225,11 @@ func (a *xorAppender) writeVDelta(v float64) {
xorWrite(a.b, v, a.v, &a.leading, &a.trailing)
}
-func (*xorAppender) AppendHistogram(*HistogramAppender, int64, *histogram.Histogram, bool) (Chunk, bool, Appender, error) {
+func (*xorAppender) AppendHistogram(*HistogramAppender, int64, int64, *histogram.Histogram, bool) (Chunk, bool, Appender, error) {
panic("appended a histogram sample to a float chunk")
}
-func (*xorAppender) AppendFloatHistogram(*FloatHistogramAppender, int64, *histogram.FloatHistogram, bool) (Chunk, bool, Appender, error) {
+func (*xorAppender) AppendFloatHistogram(*FloatHistogramAppender, int64, int64, *histogram.FloatHistogram, bool) (Chunk, bool, Appender, error) {
panic("appended a float histogram sample to a float chunk")
}
@@ -277,6 +277,10 @@ func (it *xorIterator) AtT() int64 {
return it.t
}
+func (*xorIterator) AtST() int64 {
+ return 0
+}
+
func (it *xorIterator) Err() error {
return it.err
}
diff --git a/tsdb/chunkenc/xor_test.go b/tsdb/chunkenc/xor_test.go
index 904e536b49..b30c65283d 100644
--- a/tsdb/chunkenc/xor_test.go
+++ b/tsdb/chunkenc/xor_test.go
@@ -24,7 +24,7 @@ func BenchmarkXorRead(b *testing.B) {
app, err := c.Appender()
require.NoError(b, err)
for i := int64(0); i < 120*1000; i += 1000 {
- app.Append(i, float64(i)+float64(i)/10+float64(i)/100+float64(i)/1000)
+ app.Append(0, i, float64(i)+float64(i)/10+float64(i)/100+float64(i)/1000)
}
b.ReportAllocs()
diff --git a/tsdb/chunks/chunks.go b/tsdb/chunks/chunks.go
index 681fceb2fb..ce4c9d3d78 100644
--- a/tsdb/chunks/chunks.go
+++ b/tsdb/chunks/chunks.go
@@ -25,7 +25,6 @@ import (
"strconv"
"github.com/prometheus/prometheus/tsdb/chunkenc"
- tsdb_errors "github.com/prometheus/prometheus/tsdb/errors"
"github.com/prometheus/prometheus/tsdb/fileutil"
)
@@ -136,6 +135,7 @@ type Meta struct {
}
// ChunkFromSamples requires all samples to have the same type.
+// TODO(krajorama): test with ST when chunk formats support it.
func ChunkFromSamples(s []Sample) (Meta, error) {
return ChunkFromSamplesGeneric(SampleSlice(s))
}
@@ -165,9 +165,9 @@ func ChunkFromSamplesGeneric(s Samples) (Meta, error) {
for i := 0; i < s.Len(); i++ {
switch sampleType {
case chunkenc.ValFloat:
- ca.Append(s.Get(i).T(), s.Get(i).F())
+ ca.Append(s.Get(i).ST(), s.Get(i).T(), s.Get(i).F())
case chunkenc.ValHistogram:
- newChunk, _, ca, err = ca.AppendHistogram(nil, s.Get(i).T(), s.Get(i).H(), false)
+ newChunk, _, ca, err = ca.AppendHistogram(nil, s.Get(i).ST(), s.Get(i).T(), s.Get(i).H(), false)
if err != nil {
return emptyChunk, err
}
@@ -175,7 +175,7 @@ func ChunkFromSamplesGeneric(s Samples) (Meta, error) {
return emptyChunk, errors.New("did not expect to start a second chunk")
}
case chunkenc.ValFloatHistogram:
- newChunk, _, ca, err = ca.AppendFloatHistogram(nil, s.Get(i).T(), s.Get(i).FH(), false)
+ newChunk, _, ca, err = ca.AppendFloatHistogram(nil, s.Get(i).ST(), s.Get(i).T(), s.Get(i).FH(), false)
if err != nil {
return emptyChunk, err
}
@@ -431,13 +431,15 @@ func cutSegmentFile(dirFile *os.File, magicNumber uint32, chunksFormat byte, all
}
defer func() {
if returnErr != nil {
- errs := tsdb_errors.NewMulti(returnErr)
+ errs := []error{
+ returnErr,
+ }
if f != nil {
- errs.Add(f.Close())
+ errs = append(errs, f.Close())
}
// Calling RemoveAll on a non-existent file does not return error.
- errs.Add(os.RemoveAll(ptmp))
- returnErr = errs.Err()
+ errs = append(errs, os.RemoveAll(ptmp))
+ returnErr = errors.Join(errs...)
}
}()
if allocSize > 0 {
@@ -665,10 +667,10 @@ func NewDirReader(dir string, pool chunkenc.Pool) (*Reader, error) {
for _, fn := range files {
f, err := fileutil.OpenMmapFile(fn)
if err != nil {
- return nil, tsdb_errors.NewMulti(
+ return nil, errors.Join(
fmt.Errorf("mmap files: %w", err),
- tsdb_errors.CloseAll(cs),
- ).Err()
+ closeAll(cs),
+ )
}
cs = append(cs, f)
bs = append(bs, realByteSlice(f.Bytes()))
@@ -676,16 +678,16 @@ func NewDirReader(dir string, pool chunkenc.Pool) (*Reader, error) {
reader, err := newReader(bs, cs, pool)
if err != nil {
- return nil, tsdb_errors.NewMulti(
+ return nil, errors.Join(
err,
- tsdb_errors.CloseAll(cs),
- ).Err()
+ closeAll(cs),
+ )
}
return reader, nil
}
func (s *Reader) Close() error {
- return tsdb_errors.CloseAll(s.cs)
+ return closeAll(s.cs)
}
// Size returns the size of the chunks.
@@ -774,3 +776,12 @@ func sequenceFiles(dir string) ([]string, error) {
}
return res, nil
}
+
+// closeAll closes all given closers while recording error in MultiError.
+func closeAll(cs []io.Closer) error {
+ var errs []error
+ for _, c := range cs {
+ errs = append(errs, c.Close())
+ }
+ return errors.Join(errs...)
+}
diff --git a/tsdb/chunks/head_chunks.go b/tsdb/chunks/head_chunks.go
index ffe7e70fc6..809cd6b889 100644
--- a/tsdb/chunks/head_chunks.go
+++ b/tsdb/chunks/head_chunks.go
@@ -32,7 +32,6 @@ import (
"go.uber.org/atomic"
"github.com/prometheus/prometheus/tsdb/chunkenc"
- tsdb_errors "github.com/prometheus/prometheus/tsdb/errors"
"github.com/prometheus/prometheus/tsdb/fileutil"
)
@@ -304,7 +303,7 @@ func (cdm *ChunkDiskMapper) openMMapFiles() (returnErr error) {
cdm.closers = map[int]io.Closer{}
defer func() {
if returnErr != nil {
- returnErr = tsdb_errors.NewMulti(returnErr, closeAllFromMap(cdm.closers)).Err()
+ returnErr = errors.Join(returnErr, closeAllFromMap(cdm.closers))
cdm.mmappedChunkFiles = nil
cdm.closers = nil
@@ -614,7 +613,7 @@ func (cdm *ChunkDiskMapper) cut() (seq, offset int, returnErr error) {
// The file should not be closed if there is no error,
// its kept open in the ChunkDiskMapper.
if returnErr != nil {
- returnErr = tsdb_errors.NewMulti(returnErr, newFile.Close()).Err()
+ returnErr = errors.Join(returnErr, newFile.Close())
}
}()
@@ -970,7 +969,7 @@ func (cdm *ChunkDiskMapper) Truncate(fileNo uint32) error {
}
cdm.readPathMtx.RUnlock()
- errs := tsdb_errors.NewMulti()
+ var errs []error
// Cut a new file only if the current file has some chunks.
if cdm.curFileSize() > HeadChunkFileHeaderSize {
// There is a known race condition here because between the check of curFileSize() and the call to CutNewFile()
@@ -979,7 +978,7 @@ func (cdm *ChunkDiskMapper) Truncate(fileNo uint32) error {
cdm.CutNewFile()
}
pendingDeletes, err := cdm.deleteFiles(removedFiles)
- errs.Add(err)
+ errs = append(errs, err)
if len(chkFileIndices) == len(removedFiles) {
// All files were deleted. Reset the current sequence.
@@ -1003,7 +1002,7 @@ func (cdm *ChunkDiskMapper) Truncate(fileNo uint32) error {
cdm.evtlPosMtx.Unlock()
}
- return errs.Err()
+ return errors.Join(errs...)
}
// deleteFiles deletes the given file sequences in order of the sequence.
@@ -1098,23 +1097,23 @@ func (cdm *ChunkDiskMapper) Close() error {
}
cdm.closed = true
- errs := tsdb_errors.NewMulti(
+ errs := []error{
closeAllFromMap(cdm.closers),
cdm.finalizeCurFile(),
cdm.dir.Close(),
- )
+ }
cdm.mmappedChunkFiles = map[int]*mmappedChunkFile{}
cdm.closers = map[int]io.Closer{}
- return errs.Err()
+ return errors.Join(errs...)
}
func closeAllFromMap(cs map[int]io.Closer) error {
- errs := tsdb_errors.NewMulti()
+ var errs []error
for _, c := range cs {
- errs.Add(c.Close())
+ errs = append(errs, c.Close())
}
- return errs.Err()
+ return errors.Join(errs...)
}
const inBufferShards = 128 // 128 is a randomly chosen number.
diff --git a/tsdb/chunks/head_chunks_test.go b/tsdb/chunks/head_chunks_test.go
index 17efd44aa6..c3cbc5a618 100644
--- a/tsdb/chunks/head_chunks_test.go
+++ b/tsdb/chunks/head_chunks_test.go
@@ -559,7 +559,7 @@ func randomChunk(t *testing.T) chunkenc.Chunk {
app, err := chunk.Appender()
require.NoError(t, err)
for range length {
- app.Append(rand.Int63(), rand.Float64())
+ app.Append(0, rand.Int63(), rand.Float64())
}
return chunk
}
diff --git a/tsdb/chunks/samples.go b/tsdb/chunks/samples.go
index 8097bcd72b..280f2dd606 100644
--- a/tsdb/chunks/samples.go
+++ b/tsdb/chunks/samples.go
@@ -25,6 +25,7 @@ type Samples interface {
type Sample interface {
T() int64
+ ST() int64
F() float64
H() *histogram.Histogram
FH() *histogram.FloatHistogram
@@ -38,16 +39,20 @@ func (s SampleSlice) Get(i int) Sample { return s[i] }
func (s SampleSlice) Len() int { return len(s) }
type sample struct {
- t int64
- f float64
- h *histogram.Histogram
- fh *histogram.FloatHistogram
+ st, t int64
+ f float64
+ h *histogram.Histogram
+ fh *histogram.FloatHistogram
}
func (s sample) T() int64 {
return s.t
}
+func (s sample) ST() int64 {
+ return s.st
+}
+
func (s sample) F() float64 {
return s.f
}
diff --git a/tsdb/compact_test.go b/tsdb/compact_test.go
index 29b90d9bbc..6d2fbad91f 100644
--- a/tsdb/compact_test.go
+++ b/tsdb/compact_test.go
@@ -1452,9 +1452,6 @@ func TestHeadCompactionWithHistograms(t *testing.T) {
t.Run(fmt.Sprintf("float=%t", floatTest), func(t *testing.T) {
head, _ := newTestHead(t, DefaultBlockDuration, compression.None, false)
require.NoError(t, head.Init(0))
- t.Cleanup(func() {
- require.NoError(t, head.Close())
- })
minute := func(m int) int64 { return int64(m) * time.Minute.Milliseconds() }
ctx := context.Background()
@@ -1631,13 +1628,7 @@ func TestSparseHistogramSpaceSavings(t *testing.T) {
),
func(t *testing.T) {
oldHead, _ := newTestHead(t, DefaultBlockDuration, compression.None, false)
- t.Cleanup(func() {
- require.NoError(t, oldHead.Close())
- })
sparseHead, _ := newTestHead(t, DefaultBlockDuration, compression.None, false)
- t.Cleanup(func() {
- require.NoError(t, sparseHead.Close())
- })
var allSparseSeries []struct {
baseLabels labels.Labels
diff --git a/tsdb/db_append_v2_test.go b/tsdb/db_append_v2_test.go
index 344b1d6943..16134e8c93 100644
--- a/tsdb/db_append_v2_test.go
+++ b/tsdb/db_append_v2_test.go
@@ -372,7 +372,7 @@ func TestDeleteSimple_AppendV2(t *testing.T) {
expSamples := make([]chunks.Sample, 0, len(c.remaint))
for _, ts := range c.remaint {
- expSamples = append(expSamples, sample{ts, smpls[ts], nil, nil})
+ expSamples = append(expSamples, sample{0, ts, smpls[ts], nil, nil})
}
expss := newMockSeriesSet([]storage.Series{
@@ -507,7 +507,7 @@ func TestSkippingInvalidValuesInSameTxn_AppendV2(t *testing.T) {
ssMap := query(t, q, labels.MustNewMatcher(labels.MatchEqual, "a", "b"))
require.Equal(t, map[string][]chunks.Sample{
- labels.New(labels.Label{Name: "a", Value: "b"}).String(): {sample{0, 1, nil, nil}},
+ labels.New(labels.Label{Name: "a", Value: "b"}).String(): {sample{0, 0, 1, nil, nil}},
}, ssMap)
// Append Out of Order Value.
@@ -524,7 +524,7 @@ func TestSkippingInvalidValuesInSameTxn_AppendV2(t *testing.T) {
ssMap = query(t, q, labels.MustNewMatcher(labels.MatchEqual, "a", "b"))
require.Equal(t, map[string][]chunks.Sample{
- labels.New(labels.Label{Name: "a", Value: "b"}).String(): {sample{0, 1, nil, nil}, sample{10, 3, nil, nil}},
+ labels.New(labels.Label{Name: "a", Value: "b"}).String(): {sample{0, 0, 1, nil, nil}, sample{0, 10, 3, nil, nil}},
}, ssMap)
}
@@ -669,7 +669,7 @@ func TestDB_SnapshotWithDelete_AppendV2(t *testing.T) {
expSamples := make([]chunks.Sample, 0, len(c.remaint))
for _, ts := range c.remaint {
- expSamples = append(expSamples, sample{ts, smpls[ts], nil, nil})
+ expSamples = append(expSamples, sample{0, ts, smpls[ts], nil, nil})
}
expss := newMockSeriesSet([]storage.Series{
@@ -772,7 +772,7 @@ func TestDB_e2e_AppendV2(t *testing.T) {
for range numDatapoints {
v := rand.Float64()
- series = append(series, sample{ts, v, nil, nil})
+ series = append(series, sample{0, ts, v, nil, nil})
_, err := app.Append(0, lset, 0, ts, v, nil, nil, storage.AOptions{})
require.NoError(t, err)
@@ -1094,7 +1094,7 @@ func TestTombstoneClean_AppendV2(t *testing.T) {
expSamples := make([]chunks.Sample, 0, len(c.remaint))
for _, ts := range c.remaint {
- expSamples = append(expSamples, sample{ts, smpls[ts], nil, nil})
+ expSamples = append(expSamples, sample{0, ts, smpls[ts], nil, nil})
}
expss := newMockSeriesSet([]storage.Series{
@@ -2310,7 +2310,7 @@ func TestCompactHead_AppendV2(t *testing.T) {
val := rand.Float64()
_, err := app.Append(0, labels.FromStrings("a", "b"), 0, int64(i), val, nil, nil, storage.AOptions{})
require.NoError(t, err)
- expSamples = append(expSamples, sample{int64(i), val, nil, nil})
+ expSamples = append(expSamples, sample{0, int64(i), val, nil, nil})
}
require.NoError(t, app.Commit())
@@ -2337,7 +2337,7 @@ func TestCompactHead_AppendV2(t *testing.T) {
series = seriesSet.At().Iterator(series)
for series.Next() == chunkenc.ValFloat {
time, val := series.At()
- actSamples = append(actSamples, sample{time, val, nil, nil})
+ actSamples = append(actSamples, sample{0, time, val, nil, nil})
}
require.NoError(t, series.Err())
}
diff --git a/tsdb/db_test.go b/tsdb/db_test.go
index a55264c24e..dff403ab68 100644
--- a/tsdb/db_test.go
+++ b/tsdb/db_test.go
@@ -563,7 +563,7 @@ func TestDeleteSimple(t *testing.T) {
expSamples := make([]chunks.Sample, 0, len(c.remaint))
for _, ts := range c.remaint {
- expSamples = append(expSamples, sample{ts, smpls[ts], nil, nil})
+ expSamples = append(expSamples, sample{0, ts, smpls[ts], nil, nil})
}
expss := newMockSeriesSet([]storage.Series{
@@ -708,7 +708,7 @@ func TestSkippingInvalidValuesInSameTxn(t *testing.T) {
ssMap := query(t, q, labels.MustNewMatcher(labels.MatchEqual, "a", "b"))
require.Equal(t, map[string][]chunks.Sample{
- labels.New(labels.Label{Name: "a", Value: "b"}).String(): {sample{0, 1, nil, nil}},
+ labels.New(labels.Label{Name: "a", Value: "b"}).String(): {sample{0, 0, 1, nil, nil}},
}, ssMap)
// Append Out of Order Value.
@@ -725,7 +725,7 @@ func TestSkippingInvalidValuesInSameTxn(t *testing.T) {
ssMap = query(t, q, labels.MustNewMatcher(labels.MatchEqual, "a", "b"))
require.Equal(t, map[string][]chunks.Sample{
- labels.New(labels.Label{Name: "a", Value: "b"}).String(): {sample{0, 1, nil, nil}, sample{10, 3, nil, nil}},
+ labels.New(labels.Label{Name: "a", Value: "b"}).String(): {sample{0, 0, 1, nil, nil}, sample{0, 10, 3, nil, nil}},
}, ssMap)
}
@@ -870,7 +870,7 @@ func TestDB_SnapshotWithDelete(t *testing.T) {
expSamples := make([]chunks.Sample, 0, len(c.remaint))
for _, ts := range c.remaint {
- expSamples = append(expSamples, sample{ts, smpls[ts], nil, nil})
+ expSamples = append(expSamples, sample{0, ts, smpls[ts], nil, nil})
}
expss := newMockSeriesSet([]storage.Series{
@@ -973,7 +973,7 @@ func TestDB_e2e(t *testing.T) {
for range numDatapoints {
v := rand.Float64()
- series = append(series, sample{ts, v, nil, nil})
+ series = append(series, sample{0, ts, v, nil, nil})
_, err := app.Append(0, lset, ts, v)
require.NoError(t, err)
@@ -1295,7 +1295,7 @@ func TestTombstoneClean(t *testing.T) {
expSamples := make([]chunks.Sample, 0, len(c.remaint))
for _, ts := range c.remaint {
- expSamples = append(expSamples, sample{ts, smpls[ts], nil, nil})
+ expSamples = append(expSamples, sample{0, ts, smpls[ts], nil, nil})
}
expss := newMockSeriesSet([]storage.Series{
@@ -2880,11 +2880,11 @@ func assureChunkFromSamples(t *testing.T, samples []chunks.Sample) chunks.Meta {
// TestChunkWriter_ReadAfterWrite ensures that chunk segment are cut at the set segment size and
// that the resulted segments includes the expected chunks data.
func TestChunkWriter_ReadAfterWrite(t *testing.T) {
- chk1 := assureChunkFromSamples(t, []chunks.Sample{sample{1, 1, nil, nil}})
- chk2 := assureChunkFromSamples(t, []chunks.Sample{sample{1, 2, nil, nil}})
- chk3 := assureChunkFromSamples(t, []chunks.Sample{sample{1, 3, nil, nil}})
- chk4 := assureChunkFromSamples(t, []chunks.Sample{sample{1, 4, nil, nil}})
- chk5 := assureChunkFromSamples(t, []chunks.Sample{sample{1, 5, nil, nil}})
+ chk1 := assureChunkFromSamples(t, []chunks.Sample{sample{0, 1, 1, nil, nil}})
+ chk2 := assureChunkFromSamples(t, []chunks.Sample{sample{0, 1, 2, nil, nil}})
+ chk3 := assureChunkFromSamples(t, []chunks.Sample{sample{0, 1, 3, nil, nil}})
+ chk4 := assureChunkFromSamples(t, []chunks.Sample{sample{0, 1, 4, nil, nil}})
+ chk5 := assureChunkFromSamples(t, []chunks.Sample{sample{0, 1, 5, nil, nil}})
chunkSize := len(chk1.Chunk.Bytes()) + chunks.MaxChunkLengthFieldSize + chunks.ChunkEncodingSize + crc32.Size
tests := []struct {
@@ -3086,11 +3086,11 @@ func TestRangeForTimestamp(t *testing.T) {
func TestChunkReader_ConcurrentReads(t *testing.T) {
t.Parallel()
chks := []chunks.Meta{
- assureChunkFromSamples(t, []chunks.Sample{sample{1, 1, nil, nil}}),
- assureChunkFromSamples(t, []chunks.Sample{sample{1, 2, nil, nil}}),
- assureChunkFromSamples(t, []chunks.Sample{sample{1, 3, nil, nil}}),
- assureChunkFromSamples(t, []chunks.Sample{sample{1, 4, nil, nil}}),
- assureChunkFromSamples(t, []chunks.Sample{sample{1, 5, nil, nil}}),
+ assureChunkFromSamples(t, []chunks.Sample{sample{0, 1, 1, nil, nil}}),
+ assureChunkFromSamples(t, []chunks.Sample{sample{0, 1, 2, nil, nil}}),
+ assureChunkFromSamples(t, []chunks.Sample{sample{0, 1, 3, nil, nil}}),
+ assureChunkFromSamples(t, []chunks.Sample{sample{0, 1, 4, nil, nil}}),
+ assureChunkFromSamples(t, []chunks.Sample{sample{0, 1, 5, nil, nil}}),
}
tempDir := t.TempDir()
@@ -3150,7 +3150,7 @@ func TestCompactHead(t *testing.T) {
val := rand.Float64()
_, err := app.Append(0, labels.FromStrings("a", "b"), int64(i), val)
require.NoError(t, err)
- expSamples = append(expSamples, sample{int64(i), val, nil, nil})
+ expSamples = append(expSamples, sample{0, int64(i), val, nil, nil})
}
require.NoError(t, app.Commit())
@@ -3177,7 +3177,7 @@ func TestCompactHead(t *testing.T) {
series = seriesSet.At().Iterator(series)
for series.Next() == chunkenc.ValFloat {
time, val := series.At()
- actSamples = append(actSamples, sample{time, val, nil, nil})
+ actSamples = append(actSamples, sample{0, time, val, nil, nil})
}
require.NoError(t, series.Err())
}
diff --git a/tsdb/exemplar.go b/tsdb/exemplar.go
index b58976c911..36b0a7e660 100644
--- a/tsdb/exemplar.go
+++ b/tsdb/exemplar.go
@@ -327,9 +327,10 @@ func (ce *CircularExemplarStorage) grow(l int64) int {
{from: ce.nextIndex, to: oldSize},
{from: 0, to: ce.nextIndex},
}
- ce.nextIndex = copyExemplarRanges(ce.index, newSlice, ce.exemplars, ranges)
+ totalCopied, migrated := copyExemplarRanges(ce.index, newSlice, ce.exemplars, ranges)
+ ce.nextIndex = totalCopied
ce.exemplars = newSlice
- return oldSize
+ return migrated
}
// shrink the circular buffer by either trimming from the right or deleting the
@@ -353,6 +354,7 @@ func (ce *CircularExemplarStorage) shrink(l int64) (migrated int) {
newSlice := make([]circularBufferEntry, int(l))
+ var totalCopied int
switch {
case deleteStart == deleteEnd:
// The entire buffer was cleared (shrink to zero). Note that we don't have to
@@ -363,18 +365,18 @@ func (ce *CircularExemplarStorage) shrink(l int64) (migrated int) {
return 0
case deleteStart < deleteEnd:
// We delete an "inner" section of the circular buffer.
- migrated = copyExemplarRanges(ce.index, newSlice, ce.exemplars, []intRange{
+ totalCopied, migrated = copyExemplarRanges(ce.index, newSlice, ce.exemplars, []intRange{
{from: deleteEnd, to: oldSize},
{from: 0, to: deleteStart},
})
case deleteStart > deleteEnd:
// We keep an "inner" section of the circular buffer.
- migrated = copyExemplarRanges(ce.index, newSlice, ce.exemplars, []intRange{
+ totalCopied, migrated = copyExemplarRanges(ce.index, newSlice, ce.exemplars, []intRange{
{from: deleteEnd, to: deleteStart},
})
}
- ce.nextIndex = migrated % int(l)
+ ce.nextIndex = totalCopied % int(l)
ce.exemplars = newSlice
return migrated
}
@@ -405,8 +407,9 @@ func (ce *CircularExemplarStorage) AddExemplar(l labels.Labels, e exemplar.Exemp
// If we insert an out-of-order exemplar, we preemptively find the insertion
// index to check for duplicates.
var insertionIndex int
+ var outOfOrder bool
if indexExists {
- outOfOrder := e.Ts >= ce.exemplars[idx.oldest].exemplar.Ts && e.Ts < ce.exemplars[idx.newest].exemplar.Ts
+ outOfOrder = e.Ts >= ce.exemplars[idx.oldest].exemplar.Ts && e.Ts < ce.exemplars[idx.newest].exemplar.Ts
if outOfOrder {
insertionIndex = ce.findInsertionIndex(e, idx)
if ce.exemplars[insertionIndex].exemplar.Ts == e.Ts {
@@ -425,8 +428,7 @@ func (ce *CircularExemplarStorage) AddExemplar(l labels.Labels, e exemplar.Exemp
ce.index[string(seriesLabels)] = idx
}
- // Remove entries if the buffer is full. Note that this doesn't invalidate the
- // insertion index since out-of-order exemplars cannot be the oldest exemplar.
+ // Remove entries if the buffer is full.
if prev := &ce.exemplars[ce.nextIndex]; prev.ref != nil {
prevRef := prev.ref
if ce.removeExemplar(prev) {
@@ -436,6 +438,11 @@ func (ce *CircularExemplarStorage) AddExemplar(l labels.Labels, e exemplar.Exemp
} else {
ce.removeIndex(prevRef)
}
+ } else if outOfOrder && insertionIndex == ce.nextIndex && prevRef == idx {
+ // The entry we were going to insert after was removed from the same series.
+ // Recalculate the insertion point in the updated linked list to avoid
+ // creating a self-referencing loop.
+ insertionIndex = ce.findInsertionIndex(e, idx)
}
}
@@ -582,20 +589,21 @@ func (e intRange) contains(i int) bool {
}
// copyExemplarRanges copies non-overlapping ranges from src into dest and
-// adjusts list pointers in dest and index accordingly. Returns the number of
-// copied items.
+// adjusts list pointers in dest and index accordingly. Returns the total
+// number of slots copied (for nextIndex) and the number of non-empty entries
+// migrated.
func copyExemplarRanges(
index map[string]*indexEntry,
dest, src []circularBufferEntry,
ranges []intRange,
-) int {
+) (totalCopied, migratedEntries int) {
offsets := make([]int, len(ranges))
n := 0
for i, rng := range ranges {
offsets[i] = n - rng.from
n += copy(dest[n:], src[rng.from:rng.to])
}
- migratedEntries := n
+ migratedEntries = n
for di := range n {
e := &dest[di]
if e.ref == nil {
@@ -631,5 +639,5 @@ func copyExemplarRanges(
}
}
}
- return migratedEntries
+ return n, migratedEntries
}
diff --git a/tsdb/exemplar_test.go b/tsdb/exemplar_test.go
index 01ffeb9541..0d45f56b3e 100644
--- a/tsdb/exemplar_test.go
+++ b/tsdb/exemplar_test.go
@@ -190,6 +190,22 @@ func TestCircularExemplarStorage_AddExemplar(t *testing.T) {
{Labels: series1, Value: 0.3, Ts: 4},
},
},
+ {
+ name: "out-of-order insert where evicted entry is insertion point",
+ size: 3,
+ exemplars: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.2, Ts: 2}, // pos 0, linked list middle
+ {Labels: series1, Value: 0.1, Ts: 1}, // pos 1, linked list oldest
+ {Labels: series1, Value: 0.5, Ts: 5}, // pos 2, linked list newest
+ {Labels: series1, Value: 0.3, Ts: 3},
+ },
+ matcher: series1Matcher,
+ wantExemplars: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.1, Ts: 1},
+ {Labels: series1, Value: 0.3, Ts: 3},
+ {Labels: series1, Value: 0.5, Ts: 5},
+ },
+ },
{
name: "insert out of the OOO window",
size: 3,
@@ -390,7 +406,7 @@ func TestCircularExemplarStorage_Resize(t *testing.T) {
{Labels: series1, Value: 0.1, Ts: 1},
{Labels: series1, Value: 0.2, Ts: 2},
},
- wantNextIndex: 2,
+ wantNextIndex: 3,
},
{
name: "in-order, shrink",
@@ -431,7 +447,7 @@ func TestCircularExemplarStorage_Resize(t *testing.T) {
{Labels: series1, Value: 0.2, Ts: 2},
{Labels: series1, Value: 0.3, Ts: 3},
},
- wantNextIndex: 2,
+ wantNextIndex: 3,
},
{
name: "duplicate timestamps",
@@ -452,7 +468,7 @@ func TestCircularExemplarStorage_Resize(t *testing.T) {
exemplars: []exemplar.Exemplar{},
resize: 10,
wantExemplars: []exemplar.Exemplar{},
- wantNextIndex: 0,
+ wantNextIndex: 3,
},
{
name: "empty input, shrink",
@@ -507,7 +523,7 @@ func TestCircularExemplarStorage_Resize(t *testing.T) {
wantExemplars: []exemplar.Exemplar{
{Labels: series1, Value: 0.1, Ts: 1},
},
- wantNextIndex: 1,
+ wantNextIndex: 0,
},
}
@@ -660,6 +676,47 @@ func TestCircularExemplarStorage_Resize(t *testing.T) {
{Labels: series1, Value: 0.6, Ts: 6},
},
},
+ {
+ name: "grow non-full buffer then add entries",
+ addExemplars1: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.1, Ts: 1},
+ {Labels: series1, Value: 0.2, Ts: 2},
+ },
+ resize1: 10,
+ wantExemplars1: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.1, Ts: 1},
+ {Labels: series1, Value: 0.2, Ts: 2},
+ },
+ resize2: 10,
+ addExemplars2: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.3, Ts: 3},
+ {Labels: series1, Value: 0.4, Ts: 4},
+ },
+ wantExemplars2: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.1, Ts: 1},
+ {Labels: series1, Value: 0.2, Ts: 2},
+ {Labels: series1, Value: 0.3, Ts: 3},
+ {Labels: series1, Value: 0.4, Ts: 4},
+ },
+ },
+ {
+ name: "shrink non-full buffer then add entries",
+ addExemplars1: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.1, Ts: 1},
+ },
+ resize1: 2,
+ wantExemplars1: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.1, Ts: 1},
+ },
+ resize2: 2,
+ addExemplars2: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.2, Ts: 2},
+ },
+ wantExemplars2: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.1, Ts: 1},
+ {Labels: series1, Value: 0.2, Ts: 2},
+ },
+ },
}
for _, tc := range resizeTwiceCases {
diff --git a/tsdb/head.go b/tsdb/head.go
index 77b49ca1cb..3c3f642a4f 100644
--- a/tsdb/head.go
+++ b/tsdb/head.go
@@ -985,7 +985,7 @@ func (h *Head) loadMmappedChunks(refSeries map[chunks.HeadSeriesRef]*memSeries)
return nil
}); err != nil {
// secondLastRef because the lastRef caused an error.
- return nil, nil, secondLastRef, fmt.Errorf("iterate on on-disk chunks: %w", err)
+ return nil, nil, secondLastRef, fmt.Errorf("iterate on-disk chunks: %w", err)
}
return mmappedChunks, oooMmappedChunks, lastRef, nil
}
@@ -2339,17 +2339,20 @@ func (s *stripeSeries) postCreation(lset labels.Labels) {
}
type sample struct {
+ st int64
t int64
f float64
h *histogram.Histogram
fh *histogram.FloatHistogram
}
-func newSample(t int64, v float64, h *histogram.Histogram, fh *histogram.FloatHistogram) chunks.Sample {
- return sample{t, v, h, fh}
+func newSample(st, t int64, v float64, h *histogram.Histogram, fh *histogram.FloatHistogram) chunks.Sample {
+ return sample{st, t, v, h, fh}
}
-func (s sample) T() int64 { return s.t }
+func (s sample) T() int64 { return s.t }
+
+func (s sample) ST() int64 { return s.st }
func (s sample) F() float64 { return s.f }
func (s sample) H() *histogram.Histogram { return s.h }
func (s sample) FH() *histogram.FloatHistogram { return s.fh }
diff --git a/tsdb/head_append.go b/tsdb/head_append.go
index fceb80bd34..539884e74b 100644
--- a/tsdb/head_append.go
+++ b/tsdb/head_append.go
@@ -214,6 +214,9 @@ func (h *Head) getRefSeriesBuffer() []record.RefSeries {
}
func (h *Head) putRefSeriesBuffer(b []record.RefSeries) {
+ for i := range b { // Zero out to avoid retaining label data.
+ b[i].Labels = labels.EmptyLabels()
+ }
h.refSeriesPool.Put(b[:0])
}
@@ -257,6 +260,7 @@ func (h *Head) getHistogramBuffer() []record.RefHistogramSample {
}
func (h *Head) putHistogramBuffer(b []record.RefHistogramSample) {
+ clear(b)
h.histogramsPool.Put(b[:0])
}
@@ -269,6 +273,7 @@ func (h *Head) getFloatHistogramBuffer() []record.RefFloatHistogramSample {
}
func (h *Head) putFloatHistogramBuffer(b []record.RefFloatHistogramSample) {
+ clear(b)
h.floatHistogramsPool.Put(b[:0])
}
@@ -281,6 +286,7 @@ func (h *Head) getMetadataBuffer() []record.RefMetadata {
}
func (h *Head) putMetadataBuffer(b []record.RefMetadata) {
+ clear(b)
h.metadataPool.Put(b[:0])
}
@@ -1843,7 +1849,8 @@ func (s *memSeries) append(t int64, v float64, appendID uint64, o chunkOpts) (sa
if !sampleInOrder {
return sampleInOrder, chunkCreated
}
- s.app.Append(t, v)
+ // TODO(krajorama): pass ST.
+ s.app.Append(0, t, v)
c.maxTime = t
@@ -1885,7 +1892,8 @@ func (s *memSeries) appendHistogram(t int64, h *histogram.Histogram, appendID ui
prevApp = nil
}
- newChunk, recoded, s.app, _ = s.app.AppendHistogram(prevApp, t, h, false) // false=request a new chunk if needed
+ // TODO(krajorama): pass ST.
+ newChunk, recoded, s.app, _ = s.app.AppendHistogram(prevApp, 0, t, h, false) // false=request a new chunk if needed
s.lastHistogramValue = h
s.lastFloatHistogramValue = nil
@@ -1942,7 +1950,8 @@ func (s *memSeries) appendFloatHistogram(t int64, fh *histogram.FloatHistogram,
prevApp = nil
}
- newChunk, recoded, s.app, _ = s.app.AppendFloatHistogram(prevApp, t, fh, false) // False means request a new chunk if needed.
+ // TODO(krajorama): pass ST.
+ newChunk, recoded, s.app, _ = s.app.AppendFloatHistogram(prevApp, 0, t, fh, false) // False means request a new chunk if needed.
s.lastHistogramValue = nil
s.lastFloatHistogramValue = fh
diff --git a/tsdb/head_append_v2.go b/tsdb/head_append_v2.go
index 241fb42e97..4a62d56741 100644
--- a/tsdb/head_append_v2.go
+++ b/tsdb/head_append_v2.go
@@ -323,6 +323,7 @@ func (a *headAppenderV2) appendExemplars(s *memSeries, exemplar []exemplar.Exemp
if err := a.head.exemplars.ValidateExemplar(s.labels(), e); err != nil {
if !errors.Is(err, storage.ErrDuplicateExemplar) && !errors.Is(err, storage.ErrExemplarsDisabled) {
// Except duplicates, return partial errors.
+ // TODO(bwplotka): Add exemplar info into error.
errs = append(errs, err)
continue
}
diff --git a/tsdb/head_append_v2_test.go b/tsdb/head_append_v2_test.go
index 33bc3aec38..91f6ba81cc 100644
--- a/tsdb/head_append_v2_test.go
+++ b/tsdb/head_append_v2_test.go
@@ -312,8 +312,8 @@ func TestHeadAppenderV2_WALMultiRef(t *testing.T) {
// The samples before the new ref should be discarded since Head truncation
// happens only after compacting the Head.
require.Equal(t, map[string][]chunks.Sample{`{foo="bar"}`: {
- sample{1700, 3, nil, nil},
- sample{2000, 4, nil, nil},
+ sample{0, 1700, 3, nil, nil},
+ sample{0, 2000, 4, nil, nil},
}}, series)
}
@@ -352,7 +352,6 @@ func TestHeadAppenderV2_ActiveAppenders(t *testing.T) {
func TestHeadAppenderV2_RaceBetweenSeriesCreationAndGC(t *testing.T) {
head, _ := newTestHead(t, 1000, compression.None, false)
- t.Cleanup(func() { _ = head.Close() })
require.NoError(t, head.Init(0))
const totalSeries = 100_000
@@ -395,7 +394,6 @@ func TestHeadAppenderV2_CanGCSeriesCreatedWithoutSamples(t *testing.T) {
t.Run(op, func(t *testing.T) {
chunkRange := time.Hour.Milliseconds()
head, _ := newTestHead(t, chunkRange, compression.None, true)
- t.Cleanup(func() { _ = head.Close() })
require.NoError(t, head.Init(0))
@@ -605,7 +603,7 @@ func TestHeadAppenderV2_DeleteUntilCurrMax(t *testing.T) {
it = exps.Iterator(nil)
resSamples, err := storage.ExpandSamples(it, newSample)
require.NoError(t, err)
- require.Equal(t, []chunks.Sample{sample{11, 1, nil, nil}}, resSamples)
+ require.Equal(t, []chunks.Sample{sample{0, 11, 1, nil, nil}}, resSamples)
for res.Next() {
}
require.NoError(t, res.Err())
@@ -722,7 +720,7 @@ func TestHeadAppenderV2_Delete_e2e(t *testing.T) {
v := rand.Float64()
_, err := app.Append(0, ls, 0, ts, v, nil, nil, storage.AOptions{})
require.NoError(t, err)
- series = append(series, sample{ts, v, nil, nil})
+ series = append(series, sample{0, ts, v, nil, nil})
ts += rand.Int63n(timeInterval) + 1
}
seriesMap[labels.New(l...).String()] = series
@@ -1520,7 +1518,7 @@ func TestDataMissingOnQueryDuringCompaction_AppenderV2(t *testing.T) {
ref, err = app.Append(ref, labels.FromStrings("a", "b"), 0, ts, float64(i), nil, nil, storage.AOptions{})
require.NoError(t, err)
maxt = ts
- expSamples = append(expSamples, sample{ts, float64(i), nil, nil})
+ expSamples = append(expSamples, sample{0, ts, float64(i), nil, nil})
}
require.NoError(t, app.Commit())
@@ -1864,7 +1862,8 @@ func TestHeadAppenderV2_Append_Histogram(t *testing.T) {
func TestHistogramInWALAndMmapChunk_AppenderV2(t *testing.T) {
head, _ := newTestHead(t, 3000, compression.None, false)
t.Cleanup(func() {
- require.NoError(t, head.Close())
+ // Captures head by reference, so it closes the final head after restarts.
+ _ = head.Close()
})
require.NoError(t, head.Init(0))
@@ -2011,9 +2010,10 @@ func TestHistogramInWALAndMmapChunk_AppenderV2(t *testing.T) {
}
// Restart head.
+ walDir := head.wal.Dir()
require.NoError(t, head.Close())
startHead := func() {
- w, err := wlog.NewSize(nil, nil, head.wal.Dir(), 32768, compression.None)
+ w, err := wlog.NewSize(nil, nil, walDir, 32768, compression.None)
require.NoError(t, err)
head, err = NewHead(nil, nil, w, nil, head.opts, nil)
require.NoError(t, err)
@@ -2166,17 +2166,17 @@ func TestChunkSnapshot_AppenderV2(t *testing.T) {
aOpts.Exemplars = []exemplar.Exemplar{newExemplar(lbls, ts)}
}
val := rand.Float64()
- expSeries[lblStr] = append(expSeries[lblStr], sample{ts, val, nil, nil})
+ expSeries[lblStr] = append(expSeries[lblStr], sample{0, ts, val, nil, nil})
_, err := app.Append(0, lbls, 0, ts, val, nil, nil, aOpts)
require.NoError(t, err)
hist := histograms[int(ts)]
- expHist[lblsHistStr] = append(expHist[lblsHistStr], sample{ts, 0, hist, nil})
+ expHist[lblsHistStr] = append(expHist[lblsHistStr], sample{0, ts, 0, hist, nil})
_, err = app.Append(0, lblsHist, 0, ts, 0, hist, nil, storage.AOptions{})
require.NoError(t, err)
floatHist := floatHistogram[int(ts)]
- expFloatHist[lblsFloatHistStr] = append(expFloatHist[lblsFloatHistStr], sample{ts, 0, nil, floatHist})
+ expFloatHist[lblsFloatHistStr] = append(expFloatHist[lblsFloatHistStr], sample{0, ts, 0, nil, floatHist})
_, err = app.Append(0, lblsFloatHist, 0, ts, 0, nil, floatHist, storage.AOptions{})
require.NoError(t, err)
@@ -2244,17 +2244,17 @@ func TestChunkSnapshot_AppenderV2(t *testing.T) {
aOpts.Exemplars = []exemplar.Exemplar{newExemplar(lbls, ts)}
}
val := rand.Float64()
- expSeries[lblStr] = append(expSeries[lblStr], sample{ts, val, nil, nil})
+ expSeries[lblStr] = append(expSeries[lblStr], sample{0, ts, val, nil, nil})
_, err := app.Append(0, lbls, 0, ts, val, nil, nil, aOpts)
require.NoError(t, err)
hist := histograms[int(ts)]
- expHist[lblsHistStr] = append(expHist[lblsHistStr], sample{ts, 0, hist, nil})
+ expHist[lblsHistStr] = append(expHist[lblsHistStr], sample{0, ts, 0, hist, nil})
_, err = app.Append(0, lblsHist, 0, ts, 0, hist, nil, storage.AOptions{})
require.NoError(t, err)
floatHist := floatHistogram[int(ts)]
- expFloatHist[lblsFloatHistStr] = append(expFloatHist[lblsFloatHistStr], sample{ts, 0, nil, floatHist})
+ expFloatHist[lblsFloatHistStr] = append(expFloatHist[lblsFloatHistStr], sample{0, ts, 0, nil, floatHist})
_, err = app.Append(0, lblsFloatHist, 0, ts, 0, nil, floatHist, storage.AOptions{})
require.NoError(t, err)
@@ -4081,7 +4081,6 @@ func TestWALSampleAndExemplarOrder_AppenderV2(t *testing.T) {
func TestHeadAppenderV2_Append_FloatWithSameTimestampAsPreviousHistogram(t *testing.T) {
head, _ := newTestHead(t, DefaultBlockDuration, compression.None, false)
- t.Cleanup(func() { head.Close() })
ls := labels.FromStrings(labels.MetricName, "test")
@@ -4489,7 +4488,8 @@ func testHeadAppenderV2AppendHistogramAndCommitConcurrency(t *testing.T, appendF
func TestHeadAppenderV2_NumStaleSeries(t *testing.T) {
head, _ := newTestHead(t, 1000, compression.None, false)
t.Cleanup(func() {
- require.NoError(t, head.Close())
+ // Captures head by reference, so it closes the final head after restarts.
+ _ = head.Close()
})
require.NoError(t, head.Init(0))
diff --git a/tsdb/head_bench_test.go b/tsdb/head_bench_test.go
index dc0be0823a..d15f6cc310 100644
--- a/tsdb/head_bench_test.go
+++ b/tsdb/head_bench_test.go
@@ -230,7 +230,6 @@ func BenchmarkHeadAppender_AppendCommit(b *testing.B) {
opts := newTestHeadDefaultOptions(10000, false)
opts.EnableExemplarStorage = true // We benchmark with exemplars, benchmark with them.
h, _ := newTestHeadWithOptions(b, compression.None, opts)
- b.Cleanup(func() { require.NoError(b, h.Close()) })
ts := int64(1000)
diff --git a/tsdb/head_test.go b/tsdb/head_test.go
index ce4bb6d8e7..493f938860 100644
--- a/tsdb/head_test.go
+++ b/tsdb/head_test.go
@@ -84,6 +84,12 @@ func newTestHeadWithOptions(t testing.TB, compressWAL compression.Type, opts *He
h, err := NewHead(nil, nil, wal, nil, opts, nil)
require.NoError(t, err)
+ t.Cleanup(func() {
+ // Use _ = h.Close() instead of require.NoError because some tests
+ // explicitly close the head as part of their test logic (e.g., to
+ // restart/reopen the head), and we don't want to fail on double-close.
+ _ = h.Close()
+ })
require.NoError(t, h.chunkDiskMapper.IterateAllChunks(func(chunks.HeadSeriesRef, chunks.ChunkDiskMapperRef, int64, int64, uint16, chunkenc.Encoding, bool) error {
return nil
@@ -95,9 +101,6 @@ func newTestHeadWithOptions(t testing.TB, compressWAL compression.Type, opts *He
func BenchmarkCreateSeries(b *testing.B) {
series := genSeries(b.N, 10, 0, 0)
h, _ := newTestHead(b, 10000, compression.None, false)
- b.Cleanup(func() {
- require.NoError(b, h.Close())
- })
b.ReportAllocs()
b.ResetTimer()
@@ -473,9 +476,6 @@ func BenchmarkLoadRealWLs(b *testing.B) {
// returned results are correct.
func TestHead_HighConcurrencyReadAndWrite(t *testing.T) {
head, _ := newTestHead(t, DefaultBlockDuration, compression.None, false)
- defer func() {
- require.NoError(t, head.Close())
- }()
seriesCnt := 1000
readConcurrency := 2
@@ -703,9 +703,6 @@ func TestHead_ReadWAL(t *testing.T) {
}
head, w := newTestHead(t, 1000, compress, false)
- defer func() {
- require.NoError(t, head.Close())
- }()
populateTestWL(t, w, entries, nil)
@@ -745,7 +742,7 @@ func TestHead_ReadWAL(t *testing.T) {
// Verify samples and exemplar for series 10.
c, _, _, err := s10.chunk(0, head.chunkDiskMapper, &head.memChunkPool)
require.NoError(t, err)
- require.Equal(t, []sample{{100, 2, nil, nil}, {101, 5, nil, nil}}, expandChunk(c.chunk.Iterator(nil)))
+ require.Equal(t, []sample{{0, 100, 2, nil, nil}, {0, 101, 5, nil, nil}}, expandChunk(c.chunk.Iterator(nil)))
q, err := head.ExemplarQuerier(context.Background())
require.NoError(t, err)
@@ -758,14 +755,14 @@ func TestHead_ReadWAL(t *testing.T) {
// Verify samples for series 50
c, _, _, err = s50.chunk(0, head.chunkDiskMapper, &head.memChunkPool)
require.NoError(t, err)
- require.Equal(t, []sample{{101, 6, nil, nil}}, expandChunk(c.chunk.Iterator(nil)))
+ require.Equal(t, []sample{{0, 101, 6, nil, nil}}, expandChunk(c.chunk.Iterator(nil)))
// Verify records for series 100 and its duplicate, series 101.
// The samples before the new series record should be discarded since a duplicate record
// is only possible when old samples were compacted.
c, _, _, err = s100.chunk(0, head.chunkDiskMapper, &head.memChunkPool)
require.NoError(t, err)
- require.Equal(t, []sample{{101, 7, nil, nil}}, expandChunk(c.chunk.Iterator(nil)))
+ require.Equal(t, []sample{{0, 101, 7, nil, nil}}, expandChunk(c.chunk.Iterator(nil)))
q, err = head.ExemplarQuerier(context.Background())
require.NoError(t, err)
@@ -841,8 +838,8 @@ func TestHead_WALMultiRef(t *testing.T) {
// The samples before the new ref should be discarded since Head truncation
// happens only after compacting the Head.
require.Equal(t, map[string][]chunks.Sample{`{foo="bar"}`: {
- sample{1700, 3, nil, nil},
- sample{2000, 4, nil, nil},
+ sample{0, 1700, 3, nil, nil},
+ sample{0, 2000, 4, nil, nil},
}}, series)
}
@@ -1056,9 +1053,6 @@ func TestHead_WALCheckpointMultiRef(t *testing.T) {
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
h, w := newTestHead(t, 1000, compression.None, false)
- t.Cleanup(func() {
- require.NoError(t, h.Close())
- })
populateTestWL(t, w, tc.walEntries, nil)
first, _, err := wlog.Segments(w.Dir())
@@ -1134,9 +1128,6 @@ func TestHead_KeepSeriesInWALCheckpoint(t *testing.T) {
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
h, _ := newTestHead(t, 1000, compression.None, false)
- t.Cleanup(func() {
- require.NoError(t, h.Close())
- })
if tc.prepare != nil {
tc.prepare(t, h)
@@ -1152,7 +1143,6 @@ func TestHead_KeepSeriesInWALCheckpoint(t *testing.T) {
func TestHead_ActiveAppenders(t *testing.T) {
head, _ := newTestHead(t, 1000, compression.None, false)
- defer head.Close()
require.NoError(t, head.Init(0))
@@ -1185,7 +1175,6 @@ func TestHead_ActiveAppenders(t *testing.T) {
func TestHead_RaceBetweenSeriesCreationAndGC(t *testing.T) {
head, _ := newTestHead(t, 1000, compression.None, false)
- t.Cleanup(func() { _ = head.Close() })
require.NoError(t, head.Init(0))
const totalSeries = 100_000
@@ -1228,7 +1217,6 @@ func TestHead_CanGarbagecollectSeriesCreatedWithoutSamples(t *testing.T) {
t.Run(op, func(t *testing.T) {
chunkRange := time.Hour.Milliseconds()
head, _ := newTestHead(t, chunkRange, compression.None, true)
- t.Cleanup(func() { _ = head.Close() })
require.NoError(t, head.Init(0))
@@ -1267,7 +1255,6 @@ func TestHead_UnknownWALRecord(t *testing.T) {
head, w := newTestHead(t, 1000, compression.None, false)
w.Log([]byte{255, 42})
require.NoError(t, head.Init(0))
- require.NoError(t, head.Close())
}
// BenchmarkHead_Truncate is quite heavy, so consider running it with
@@ -1277,9 +1264,6 @@ func BenchmarkHead_Truncate(b *testing.B) {
prepare := func(b *testing.B, churn int) *Head {
h, _ := newTestHead(b, 1000, compression.None, false)
- b.Cleanup(func() {
- require.NoError(b, h.Close())
- })
h.initTime(0)
@@ -1346,9 +1330,6 @@ func BenchmarkHead_Truncate(b *testing.B) {
func TestHead_Truncate(t *testing.T) {
h, _ := newTestHead(t, 1000, compression.None, false)
- defer func() {
- require.NoError(t, h.Close())
- }()
h.initTime(0)
@@ -1671,9 +1652,6 @@ func TestHeadDeleteSeriesWithoutSamples(t *testing.T) {
},
}
head, w := newTestHead(t, 1000, compress, false)
- defer func() {
- require.NoError(t, head.Close())
- }()
populateTestWL(t, w, entries, nil)
@@ -1818,9 +1796,6 @@ func TestHeadDeleteSimple(t *testing.T) {
func TestDeleteUntilCurMax(t *testing.T) {
hb, _ := newTestHead(t, 1000000, compression.None, false)
- defer func() {
- require.NoError(t, hb.Close())
- }()
numSamples := int64(10)
app := hb.Appender(context.Background())
@@ -1859,7 +1834,7 @@ func TestDeleteUntilCurMax(t *testing.T) {
it = exps.Iterator(nil)
resSamples, err := storage.ExpandSamples(it, newSample)
require.NoError(t, err)
- require.Equal(t, []chunks.Sample{sample{11, 1, nil, nil}}, resSamples)
+ require.Equal(t, []chunks.Sample{sample{0, 11, 1, nil, nil}}, resSamples)
for res.Next() {
}
require.NoError(t, res.Err())
@@ -1963,9 +1938,6 @@ func TestDelete_e2e(t *testing.T) {
}
hb, _ := newTestHead(t, 100000, compression.None, false)
- defer func() {
- require.NoError(t, hb.Close())
- }()
app := hb.Appender(context.Background())
for _, l := range lbls {
@@ -1976,7 +1948,7 @@ func TestDelete_e2e(t *testing.T) {
v := rand.Float64()
_, err := app.Append(0, ls, ts, v)
require.NoError(t, err)
- series = append(series, sample{ts, v, nil, nil})
+ series = append(series, sample{0, ts, v, nil, nil})
ts += rand.Int63n(timeInterval) + 1
}
seriesMap[labels.New(l...).String()] = series
@@ -2331,9 +2303,6 @@ func TestGCChunkAccess(t *testing.T) {
// Put a chunk, select it. GC it and then access it.
const chunkRange = 1000
h, _ := newTestHead(t, chunkRange, compression.None, false)
- defer func() {
- require.NoError(t, h.Close())
- }()
cOpts := chunkOpts{
chunkDiskMapper: h.chunkDiskMapper,
@@ -2390,9 +2359,6 @@ func TestGCSeriesAccess(t *testing.T) {
// Put a series, select it. GC it and then access it.
const chunkRange = 1000
h, _ := newTestHead(t, chunkRange, compression.None, false)
- defer func() {
- require.NoError(t, h.Close())
- }()
cOpts := chunkOpts{
chunkDiskMapper: h.chunkDiskMapper,
@@ -2449,9 +2415,6 @@ func TestGCSeriesAccess(t *testing.T) {
func TestUncommittedSamplesNotLostOnTruncate(t *testing.T) {
h, _ := newTestHead(t, 1000, compression.None, false)
- defer func() {
- require.NoError(t, h.Close())
- }()
h.initTime(0)
@@ -2479,9 +2442,6 @@ func TestUncommittedSamplesNotLostOnTruncate(t *testing.T) {
func TestRemoveSeriesAfterRollbackAndTruncate(t *testing.T) {
h, _ := newTestHead(t, 1000, compression.None, false)
- defer func() {
- require.NoError(t, h.Close())
- }()
h.initTime(0)
@@ -2512,9 +2472,6 @@ func TestHead_LogRollback(t *testing.T) {
for _, compress := range []compression.Type{compression.None, compression.Snappy, compression.Zstd} {
t.Run(fmt.Sprintf("compress=%s", compress), func(t *testing.T) {
h, w := newTestHead(t, 1000, compress, false)
- defer func() {
- require.NoError(t, h.Close())
- }()
app := h.Appender(context.Background())
_, err := app.Append(0, labels.FromStrings("a", "b"), 1, 2)
@@ -2534,9 +2491,6 @@ func TestHead_LogRollback(t *testing.T) {
func TestHead_ReturnsSortedLabelValues(t *testing.T) {
h, _ := newTestHead(t, 1000, compression.None, false)
- defer func() {
- require.NoError(t, h.Close())
- }()
h.initTime(0)
@@ -2807,9 +2761,6 @@ func TestHeadReadWriterRepair(t *testing.T) {
func TestNewWalSegmentOnTruncate(t *testing.T) {
h, wal := newTestHead(t, 1000, compression.None, false)
- defer func() {
- require.NoError(t, h.Close())
- }()
add := func(ts int64) {
app := h.Appender(context.Background())
_, err := app.Append(0, labels.FromStrings("a", "b"), ts, 0)
@@ -2837,9 +2788,6 @@ func TestNewWalSegmentOnTruncate(t *testing.T) {
func TestAddDuplicateLabelName(t *testing.T) {
h, _ := newTestHead(t, 1000, compression.None, false)
- defer func() {
- require.NoError(t, h.Close())
- }()
add := func(labels labels.Labels, labelName string) {
app := h.Appender(context.Background())
@@ -3035,9 +2983,6 @@ func TestIsolationRollback(t *testing.T) {
// Rollback after a failed append and test if the low watermark has progressed anyway.
hb, _ := newTestHead(t, 1000, compression.None, false)
- defer func() {
- require.NoError(t, hb.Close())
- }()
app := hb.Appender(context.Background())
_, err := app.Append(0, labels.FromStrings("foo", "bar"), 0, 0)
@@ -3066,9 +3011,6 @@ func TestIsolationLowWatermarkMonotonous(t *testing.T) {
}
hb, _ := newTestHead(t, 1000, compression.None, false)
- defer func() {
- require.NoError(t, hb.Close())
- }()
app1 := hb.Appender(context.Background())
_, err := app1.Append(0, labels.FromStrings("foo", "bar"), 0, 0)
@@ -3103,9 +3045,6 @@ func TestIsolationAppendIDZeroIsNoop(t *testing.T) {
}
h, _ := newTestHead(t, 1000, compression.None, false)
- defer func() {
- require.NoError(t, h.Close())
- }()
h.initTime(0)
@@ -3135,9 +3074,6 @@ func TestIsolationWithoutAdd(t *testing.T) {
}
hb, _ := newTestHead(t, 1000, compression.None, false)
- defer func() {
- require.NoError(t, hb.Close())
- }()
app := hb.Appender(context.Background())
require.NoError(t, app.Commit())
@@ -3257,9 +3193,6 @@ func testOutOfOrderSamplesMetric(t *testing.T, scenario sampleTypeScenario, opti
func testHeadSeriesChunkRace(t *testing.T) {
h, _ := newTestHead(t, 1000, compression.None, false)
- defer func() {
- require.NoError(t, h.Close())
- }()
require.NoError(t, h.Init(0))
app := h.Appender(context.Background())
@@ -3292,9 +3225,6 @@ func testHeadSeriesChunkRace(t *testing.T) {
func TestHeadLabelNamesValuesWithMinMaxRange(t *testing.T) {
head, _ := newTestHead(t, 1000, compression.None, false)
- defer func() {
- require.NoError(t, head.Close())
- }()
const (
firstSeriesTimestamp int64 = 100
@@ -3353,7 +3283,6 @@ func TestHeadLabelNamesValuesWithMinMaxRange(t *testing.T) {
func TestHeadLabelValuesWithMatchers(t *testing.T) {
head, _ := newTestHead(t, 1000, compression.None, false)
- t.Cleanup(func() { require.NoError(t, head.Close()) })
ctx := context.Background()
@@ -3429,9 +3358,6 @@ func TestHeadLabelValuesWithMatchers(t *testing.T) {
func TestHeadLabelNamesWithMatchers(t *testing.T) {
head, _ := newTestHead(t, 1000, compression.None, false)
- defer func() {
- require.NoError(t, head.Close())
- }()
app := head.Appender(context.Background())
for i := range 100 {
@@ -3499,9 +3425,6 @@ func TestHeadShardedPostings(t *testing.T) {
headOpts := newTestHeadDefaultOptions(1000, false)
headOpts.EnableSharding = true
head, _ := newTestHeadWithOptions(t, compression.None, headOpts)
- defer func() {
- require.NoError(t, head.Close())
- }()
ctx := context.Background()
@@ -3562,9 +3485,6 @@ func TestHeadShardedPostings(t *testing.T) {
func TestErrReuseAppender(t *testing.T) {
head, _ := newTestHead(t, 1000, compression.None, false)
- defer func() {
- require.NoError(t, head.Close())
- }()
app := head.Appender(context.Background())
_, err := app.Append(0, labels.FromStrings("test", "test"), 0, 0)
@@ -3625,8 +3545,6 @@ func TestHeadMintAfterTruncation(t *testing.T) {
require.NoError(t, head.Truncate(7500))
require.Equal(t, int64(7500), head.MinTime())
require.Equal(t, int64(7500), head.minValidTime.Load())
-
- require.NoError(t, head.Close())
}
func TestHeadExemplars(t *testing.T) {
@@ -3648,13 +3566,11 @@ func TestHeadExemplars(t *testing.T) {
})
require.NoError(t, err)
require.NoError(t, app.Commit())
- require.NoError(t, head.Close())
}
func BenchmarkHeadLabelValuesWithMatchers(b *testing.B) {
chunkRange := int64(2000)
head, _ := newTestHead(b, chunkRange, compression.None, false)
- b.Cleanup(func() { require.NoError(b, head.Close()) })
ctx := context.Background()
@@ -3838,7 +3754,7 @@ func TestDataMissingOnQueryDuringCompaction(t *testing.T) {
ref, err = app.Append(ref, labels.FromStrings("a", "b"), ts, float64(i))
require.NoError(t, err)
maxt = ts
- expSamples = append(expSamples, sample{ts, float64(i), nil, nil})
+ expSamples = append(expSamples, sample{0, ts, float64(i), nil, nil})
}
require.NoError(t, app.Commit())
@@ -4100,9 +4016,6 @@ func TestAppendHistogram(t *testing.T) {
for _, numHistograms := range []int{1, 10, 150, 200, 250, 300} {
t.Run(strconv.Itoa(numHistograms), func(t *testing.T) {
head, _ := newTestHead(t, 1000, compression.None, false)
- t.Cleanup(func() {
- require.NoError(t, head.Close())
- })
require.NoError(t, head.Init(0))
ingestTs := int64(0)
@@ -4205,7 +4118,8 @@ func TestAppendHistogram(t *testing.T) {
func TestHistogramInWALAndMmapChunk(t *testing.T) {
head, _ := newTestHead(t, 3000, compression.None, false)
t.Cleanup(func() {
- require.NoError(t, head.Close())
+ // Captures head by reference, so it closes the final head after restarts.
+ _ = head.Close()
})
require.NoError(t, head.Init(0))
@@ -4352,9 +4266,10 @@ func TestHistogramInWALAndMmapChunk(t *testing.T) {
}
// Restart head.
+ walDir := head.wal.Dir()
require.NoError(t, head.Close())
startHead := func() {
- w, err := wlog.NewSize(nil, nil, head.wal.Dir(), 32768, compression.None)
+ w, err := wlog.NewSize(nil, nil, walDir, 32768, compression.None)
require.NoError(t, err)
head, err = NewHead(nil, nil, w, nil, head.opts, nil)
require.NoError(t, err)
@@ -4503,17 +4418,17 @@ func TestChunkSnapshot(t *testing.T) {
// 240 samples should m-map at least 1 chunk.
for ts := int64(1); ts <= 240; ts++ {
val := rand.Float64()
- expSeries[lblStr] = append(expSeries[lblStr], sample{ts, val, nil, nil})
+ expSeries[lblStr] = append(expSeries[lblStr], sample{0, ts, val, nil, nil})
ref, err := app.Append(0, lbls, ts, val)
require.NoError(t, err)
hist := histograms[int(ts)]
- expHist[lblsHistStr] = append(expHist[lblsHistStr], sample{ts, 0, hist, nil})
+ expHist[lblsHistStr] = append(expHist[lblsHistStr], sample{0, ts, 0, hist, nil})
_, err = app.AppendHistogram(0, lblsHist, ts, hist, nil)
require.NoError(t, err)
floatHist := floatHistogram[int(ts)]
- expFloatHist[lblsFloatHistStr] = append(expFloatHist[lblsFloatHistStr], sample{ts, 0, nil, floatHist})
+ expFloatHist[lblsFloatHistStr] = append(expFloatHist[lblsFloatHistStr], sample{0, ts, 0, nil, floatHist})
_, err = app.AppendHistogram(0, lblsFloatHist, ts, nil, floatHist)
require.NoError(t, err)
@@ -4577,17 +4492,17 @@ func TestChunkSnapshot(t *testing.T) {
// 240 samples should m-map at least 1 chunk.
for ts := int64(241); ts <= 480; ts++ {
val := rand.Float64()
- expSeries[lblStr] = append(expSeries[lblStr], sample{ts, val, nil, nil})
+ expSeries[lblStr] = append(expSeries[lblStr], sample{0, ts, val, nil, nil})
ref, err := app.Append(0, lbls, ts, val)
require.NoError(t, err)
hist := histograms[int(ts)]
- expHist[lblsHistStr] = append(expHist[lblsHistStr], sample{ts, 0, hist, nil})
+ expHist[lblsHistStr] = append(expHist[lblsHistStr], sample{0, ts, 0, hist, nil})
_, err = app.AppendHistogram(0, lblsHist, ts, hist, nil)
require.NoError(t, err)
floatHist := floatHistogram[int(ts)]
- expFloatHist[lblsFloatHistStr] = append(expFloatHist[lblsFloatHistStr], sample{ts, 0, nil, floatHist})
+ expFloatHist[lblsFloatHistStr] = append(expFloatHist[lblsFloatHistStr], sample{0, ts, 0, nil, floatHist})
_, err = app.AppendHistogram(0, lblsFloatHist, ts, nil, floatHist)
require.NoError(t, err)
@@ -5680,9 +5595,6 @@ func testOOOMmapReplay(t *testing.T, scenario sampleTypeScenario) {
func TestHeadInit_DiscardChunksWithUnsupportedEncoding(t *testing.T) {
h, _ := newTestHead(t, 1000, compression.None, false)
- defer func() {
- require.NoError(t, h.Close())
- }()
require.NoError(t, h.Init(0))
@@ -5727,6 +5639,9 @@ func TestHeadInit_DiscardChunksWithUnsupportedEncoding(t *testing.T) {
require.NoError(t, err)
h, err = NewHead(nil, nil, wal, nil, h.opts, nil)
require.NoError(t, err)
+ t.Cleanup(func() {
+ _ = h.Close()
+ })
require.NoError(t, h.Init(0))
series, created, err = h.getOrCreate(seriesLabels.Hash(), seriesLabels, false)
@@ -6367,9 +6282,6 @@ func TestCuttingNewHeadChunks(t *testing.T) {
for testName, tc := range testCases {
t.Run(testName, func(t *testing.T) {
h, _ := newTestHead(t, DefaultBlockDuration, compression.None, false)
- defer func() {
- require.NoError(t, h.Close())
- }()
a := h.Appender(context.Background())
@@ -6435,9 +6347,6 @@ func TestHeadDetectsDuplicateSampleAtSizeLimit(t *testing.T) {
baseTS := int64(1695209650)
h, _ := newTestHead(t, DefaultBlockDuration, compression.None, false)
- defer func() {
- require.NoError(t, h.Close())
- }()
a := h.Appender(context.Background())
var err error
@@ -6502,9 +6411,6 @@ func TestWALSampleAndExemplarOrder(t *testing.T) {
for testName, tc := range testcases {
t.Run(testName, func(t *testing.T) {
h, w := newTestHead(t, 1000, compression.None, false)
- defer func() {
- require.NoError(t, h.Close())
- }()
app := h.Appender(context.Background())
ref, err := tc.appendF(app, 10)
@@ -6552,7 +6458,6 @@ func TestHeadCompactionWhileAppendAndCommitExemplar(t *testing.T) {
require.NoError(t, err)
h.Truncate(10)
app.Commit()
- h.Close()
}
func labelsWithHashCollision() (labels.Labels, labels.Labels) {
@@ -6648,7 +6553,6 @@ func TestPostingsCardinalityStats(t *testing.T) {
func TestHeadAppender_AppendFloatWithSameTimestampAsPreviousHistogram(t *testing.T) {
head, _ := newTestHead(t, DefaultBlockDuration, compression.None, false)
- t.Cleanup(func() { head.Close() })
ls := labels.FromStrings(labels.MetricName, "test")
@@ -6872,9 +6776,6 @@ func TestHeadAppender_AppendST(t *testing.T) {
} {
t.Run(tc.name, func(t *testing.T) {
h, _ := newTestHead(t, DefaultBlockDuration, compression.None, false)
- defer func() {
- require.NoError(t, h.Close())
- }()
a := h.Appender(context.Background())
lbls := labels.FromStrings("foo", "bar")
for _, sample := range tc.appendableSamples {
@@ -6950,10 +6851,6 @@ func TestHeadAppender_AppendHistogramSTZeroSample(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
h, _ := newTestHead(t, DefaultBlockDuration, compression.None, false)
- defer func() {
- require.NoError(t, h.Close())
- }()
-
lbls := labels.FromStrings("foo", "bar")
var ref storage.SeriesRef
@@ -6979,9 +6876,6 @@ func TestHeadCompactableDoesNotCompactEmptyHead(t *testing.T) {
// would return true which is incorrect. This test verifies that we short-circuit
// the check when the head has not yet had any samples added.
head, _ := newTestHead(t, 1, compression.None, false)
- defer func() {
- require.NoError(t, head.Close())
- }()
require.False(t, head.compactable())
}
@@ -7021,9 +6915,6 @@ func TestHeadAppendHistogramAndCommitConcurrency(t *testing.T) {
func testHeadAppendHistogramAndCommitConcurrency(t *testing.T, appendFn func(storage.Appender, int) error) {
head, _ := newTestHead(t, 1000, compression.None, false)
- defer func() {
- require.NoError(t, head.Close())
- }()
wg := sync.WaitGroup{}
wg.Add(2)
@@ -7057,7 +6948,8 @@ func testHeadAppendHistogramAndCommitConcurrency(t *testing.T, appendFn func(sto
func TestHead_NumStaleSeries(t *testing.T) {
head, _ := newTestHead(t, 1000, compression.None, false)
t.Cleanup(func() {
- require.NoError(t, head.Close())
+ // Captures head by reference, so it closes the final head after restarts.
+ _ = head.Close()
})
require.NoError(t, head.Init(0))
@@ -7228,9 +7120,6 @@ func TestHistogramStalenessConversionMetrics(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
head, _ := newTestHead(t, 1000, compression.None, false)
- defer func() {
- require.NoError(t, head.Close())
- }()
lbls := labels.FromStrings("name", tc.name)
diff --git a/tsdb/index/index.go b/tsdb/index/index.go
index 8a76770821..493264b87f 100644
--- a/tsdb/index/index.go
+++ b/tsdb/index/index.go
@@ -33,7 +33,6 @@ import (
"github.com/prometheus/prometheus/storage"
"github.com/prometheus/prometheus/tsdb/chunks"
"github.com/prometheus/prometheus/tsdb/encoding"
- tsdb_errors "github.com/prometheus/prometheus/tsdb/errors"
"github.com/prometheus/prometheus/tsdb/fileutil"
)
@@ -1007,10 +1006,10 @@ func NewFileReader(path string, decoder PostingsDecoder) (*Reader, error) {
}
r, err := newReader(realByteSlice(f.Bytes()), f, decoder)
if err != nil {
- return nil, tsdb_errors.NewMulti(
+ return nil, errors.Join(
err,
f.Close(),
- ).Err()
+ )
}
return r, nil
diff --git a/tsdb/ooo_head.go b/tsdb/ooo_head.go
index c6ae924372..f9746c4c61 100644
--- a/tsdb/ooo_head.go
+++ b/tsdb/ooo_head.go
@@ -40,7 +40,8 @@ func (o *OOOChunk) Insert(t int64, v float64, h *histogram.Histogram, fh *histog
// try to append at the end first if the new timestamp is higher than the
// last known timestamp.
if len(o.samples) == 0 || t > o.samples[len(o.samples)-1].t {
- o.samples = append(o.samples, sample{t, v, h, fh})
+ // TODO(krajorama): pass ST.
+ o.samples = append(o.samples, sample{0, t, v, h, fh})
return true
}
@@ -49,7 +50,8 @@ func (o *OOOChunk) Insert(t int64, v float64, h *histogram.Histogram, fh *histog
if i >= len(o.samples) {
// none found. append it at the end
- o.samples = append(o.samples, sample{t, v, h, fh})
+ // TODO(krajorama): pass ST.
+ o.samples = append(o.samples, sample{0, t, v, h, fh})
return true
}
@@ -61,7 +63,8 @@ func (o *OOOChunk) Insert(t int64, v float64, h *histogram.Histogram, fh *histog
// Expand length by 1 to make room. use a zero sample, we will overwrite it anyway.
o.samples = append(o.samples, sample{})
copy(o.samples[i+1:], o.samples[i:])
- o.samples[i] = sample{t, v, h, fh}
+ // TODO(krajorama): pass ST.
+ o.samples[i] = sample{0, t, v, h, fh}
return true
}
@@ -125,7 +128,8 @@ func (o *OOOChunk) ToEncodedChunks(mint, maxt int64) (chks []memChunk, err error
}
switch encoding {
case chunkenc.EncXOR:
- app.Append(s.t, s.f)
+ // TODO(krajorama): pass ST.
+ app.Append(0, s.t, s.f)
case chunkenc.EncHistogram:
// Ignoring ok is ok, since we don't want to compare to the wrong previous appender anyway.
prevHApp, _ := prevApp.(*chunkenc.HistogramAppender)
@@ -133,7 +137,8 @@ func (o *OOOChunk) ToEncodedChunks(mint, maxt int64) (chks []memChunk, err error
newChunk chunkenc.Chunk
recoded bool
)
- newChunk, recoded, app, _ = app.AppendHistogram(prevHApp, s.t, s.h, false)
+ // TODO(krajorama): pass ST.
+ newChunk, recoded, app, _ = app.AppendHistogram(prevHApp, 0, s.t, s.h, false)
if newChunk != nil { // A new chunk was allocated.
if !recoded {
chks = append(chks, memChunk{chunk, cmint, cmaxt, nil})
@@ -148,7 +153,8 @@ func (o *OOOChunk) ToEncodedChunks(mint, maxt int64) (chks []memChunk, err error
newChunk chunkenc.Chunk
recoded bool
)
- newChunk, recoded, app, _ = app.AppendFloatHistogram(prevHApp, s.t, s.fh, false)
+ // TODO(krajorama): pass ST.
+ newChunk, recoded, app, _ = app.AppendFloatHistogram(prevHApp, 0, s.t, s.fh, false)
if newChunk != nil { // A new chunk was allocated.
if !recoded {
chks = append(chks, memChunk{chunk, cmint, cmaxt, nil})
diff --git a/tsdb/ooo_head_read_test.go b/tsdb/ooo_head_read_test.go
index 4ecaa51fec..f58ee3aada 100644
--- a/tsdb/ooo_head_read_test.go
+++ b/tsdb/ooo_head_read_test.go
@@ -301,9 +301,6 @@ func TestOOOHeadIndexReader_Series(t *testing.T) {
for _, headChunk := range []bool{false, true} {
t.Run(fmt.Sprintf("name=%s, permutation=%d, headChunk=%t", tc.name, perm, headChunk), func(t *testing.T) {
h, _ := newTestHead(t, 1000, compression.None, true)
- defer func() {
- require.NoError(t, h.Close())
- }()
require.NoError(t, h.Init(0))
s1, _, _ := h.getOrCreate(s1ID, s1Lset, false)
@@ -389,7 +386,6 @@ func TestOOOHeadChunkReader_LabelValues(t *testing.T) {
func testOOOHeadChunkReader_LabelValues(t *testing.T, scenario sampleTypeScenario) {
chunkRange := int64(2000)
head, _ := newTestHead(t, chunkRange, compression.None, true)
- t.Cleanup(func() { require.NoError(t, head.Close()) })
ctx := context.Background()
diff --git a/tsdb/querier.go b/tsdb/querier.go
index 4a487aa568..ce0292bf24 100644
--- a/tsdb/querier.go
+++ b/tsdb/querier.go
@@ -788,6 +788,11 @@ func (p *populateWithDelSeriesIterator) AtT() int64 {
return p.curr.AtT()
}
+// AtST TODO(krajorama): test AtST() when chunks support it.
+func (p *populateWithDelSeriesIterator) AtST() int64 {
+ return p.curr.AtST()
+}
+
func (p *populateWithDelSeriesIterator) Err() error {
if err := p.populateWithDelGenericSeriesIterator.Err(); err != nil {
return err
@@ -862,6 +867,7 @@ func (p *populateWithDelChunkSeriesIterator) Next() bool {
// populateCurrForSingleChunk sets the fields within p.currMetaWithChunk. This
// should be called if the samples in p.currDelIter only form one chunk.
+// TODO(krajorama): test ST when chunks support it.
func (p *populateWithDelChunkSeriesIterator) populateCurrForSingleChunk() bool {
valueType := p.currDelIter.Next()
if valueType == chunkenc.ValNone {
@@ -877,7 +883,7 @@ func (p *populateWithDelChunkSeriesIterator) populateCurrForSingleChunk() bool {
var (
newChunk chunkenc.Chunk
app chunkenc.Appender
- t int64
+ st, t int64
err error
)
switch valueType {
@@ -893,7 +899,8 @@ func (p *populateWithDelChunkSeriesIterator) populateCurrForSingleChunk() bool {
}
var h *histogram.Histogram
t, h = p.currDelIter.AtHistogram(nil)
- _, _, app, err = app.AppendHistogram(nil, t, h, true)
+ st = p.currDelIter.AtST()
+ _, _, app, err = app.AppendHistogram(nil, st, t, h, true)
if err != nil {
break
}
@@ -910,7 +917,8 @@ func (p *populateWithDelChunkSeriesIterator) populateCurrForSingleChunk() bool {
}
var v float64
t, v = p.currDelIter.At()
- app.Append(t, v)
+ st = p.currDelIter.AtST()
+ app.Append(st, t, v)
}
case chunkenc.ValFloatHistogram:
newChunk = chunkenc.NewFloatHistogramChunk()
@@ -924,7 +932,8 @@ func (p *populateWithDelChunkSeriesIterator) populateCurrForSingleChunk() bool {
}
var h *histogram.FloatHistogram
t, h = p.currDelIter.AtFloatHistogram(nil)
- _, _, app, err = app.AppendFloatHistogram(nil, t, h, true)
+ st = p.currDelIter.AtST()
+ _, _, app, err = app.AppendFloatHistogram(nil, st, t, h, true)
if err != nil {
break
}
@@ -950,6 +959,7 @@ func (p *populateWithDelChunkSeriesIterator) populateCurrForSingleChunk() bool {
// populateChunksFromIterable reads the samples from currDelIter to create
// chunks for chunksFromIterable. It also sets p.currMetaWithChunk to the first
// chunk.
+// TODO(krajorama): test ST when chunks support it.
func (p *populateWithDelChunkSeriesIterator) populateChunksFromIterable() bool {
p.chunksFromIterable = p.chunksFromIterable[:0]
p.chunksFromIterableIdx = -1
@@ -965,7 +975,7 @@ func (p *populateWithDelChunkSeriesIterator) populateChunksFromIterable() bool {
var (
// t is the timestamp for the current sample.
- t int64
+ st, t int64
cmint int64
cmaxt int64
@@ -1004,23 +1014,26 @@ func (p *populateWithDelChunkSeriesIterator) populateChunksFromIterable() bool {
{
var v float64
t, v = p.currDelIter.At()
- app.Append(t, v)
+ st = p.currDelIter.AtST()
+ app.Append(st, t, v)
}
case chunkenc.ValHistogram:
{
var v *histogram.Histogram
t, v = p.currDelIter.AtHistogram(nil)
+ st = p.currDelIter.AtST()
// No need to set prevApp as AppendHistogram will set the
// counter reset header for the appender that's returned.
- newChunk, recoded, app, err = app.AppendHistogram(nil, t, v, false)
+ newChunk, recoded, app, err = app.AppendHistogram(nil, st, t, v, false)
}
case chunkenc.ValFloatHistogram:
{
var v *histogram.FloatHistogram
t, v = p.currDelIter.AtFloatHistogram(nil)
+ st = p.currDelIter.AtST()
// No need to set prevApp as AppendHistogram will set the
// counter reset header for the appender that's returned.
- newChunk, recoded, app, err = app.AppendFloatHistogram(nil, t, v, false)
+ newChunk, recoded, app, err = app.AppendFloatHistogram(nil, st, t, v, false)
}
}
@@ -1202,6 +1215,11 @@ func (it *DeletedIterator) AtT() int64 {
return it.Iter.AtT()
}
+// AtST TODO(krajorama): test AtST() when chunks support it.
+func (it *DeletedIterator) AtST() int64 {
+ return it.Iter.AtST()
+}
+
func (it *DeletedIterator) Seek(t int64) chunkenc.ValueType {
if it.Iter.Err() != nil {
return chunkenc.ValNone
diff --git a/tsdb/querier_test.go b/tsdb/querier_test.go
index 6933aa617a..4387635959 100644
--- a/tsdb/querier_test.go
+++ b/tsdb/querier_test.go
@@ -141,7 +141,7 @@ func createIdxChkReaders(t *testing.T, tc []seriesSamples) (IndexReader, ChunkRe
app, _ := chunk.Appender()
for _, smpl := range chk {
require.NotNil(t, smpl.fh, "chunk can only contain one type of sample")
- _, _, _, err := app.AppendFloatHistogram(nil, smpl.t, smpl.fh, true)
+ _, _, _, err := app.AppendFloatHistogram(nil, 0, smpl.t, smpl.fh, true)
require.NoError(t, err, "chunk should be appendable")
}
chkReader[chunkRef] = chunk
@@ -150,7 +150,7 @@ func createIdxChkReaders(t *testing.T, tc []seriesSamples) (IndexReader, ChunkRe
app, _ := chunk.Appender()
for _, smpl := range chk {
require.NotNil(t, smpl.h, "chunk can only contain one type of sample")
- _, _, _, err := app.AppendHistogram(nil, smpl.t, smpl.h, true)
+ _, _, _, err := app.AppendHistogram(nil, 0, smpl.t, smpl.h, true)
require.NoError(t, err, "chunk should be appendable")
}
chkReader[chunkRef] = chunk
@@ -160,7 +160,7 @@ func createIdxChkReaders(t *testing.T, tc []seriesSamples) (IndexReader, ChunkRe
for _, smpl := range chk {
require.Nil(t, smpl.h, "chunk can only contain one type of sample")
require.Nil(t, smpl.fh, "chunk can only contain one type of sample")
- app.Append(smpl.t, smpl.f)
+ app.Append(0, smpl.t, smpl.f)
}
chkReader[chunkRef] = chunk
}
@@ -318,24 +318,24 @@ func TestBlockQuerier(t *testing.T) {
ms: []*labels.Matcher{labels.MustNewMatcher(labels.MatchRegexp, "a", ".*")},
exp: newMockSeriesSet([]storage.Series{
storage.NewListSeries(labels.FromStrings("a", "a"),
- []chunks.Sample{sample{1, 2, nil, nil}, sample{2, 3, nil, nil}, sample{3, 4, nil, nil}, sample{5, 2, nil, nil}, sample{6, 3, nil, nil}, sample{7, 4, nil, nil}},
+ []chunks.Sample{sample{0, 1, 2, nil, nil}, sample{0, 2, 3, nil, nil}, sample{0, 3, 4, nil, nil}, sample{0, 5, 2, nil, nil}, sample{0, 6, 3, nil, nil}, sample{0, 7, 4, nil, nil}},
),
storage.NewListSeries(labels.FromStrings("a", "a", "b", "b"),
- []chunks.Sample{sample{1, 1, nil, nil}, sample{2, 2, nil, nil}, sample{3, 3, nil, nil}, sample{5, 3, nil, nil}, sample{6, 6, nil, nil}},
+ []chunks.Sample{sample{0, 1, 1, nil, nil}, sample{0, 2, 2, nil, nil}, sample{0, 3, 3, nil, nil}, sample{0, 5, 3, nil, nil}, sample{0, 6, 6, nil, nil}},
),
storage.NewListSeries(labels.FromStrings("b", "b"),
- []chunks.Sample{sample{1, 3, nil, nil}, sample{2, 2, nil, nil}, sample{3, 6, nil, nil}, sample{5, 1, nil, nil}, sample{6, 7, nil, nil}, sample{7, 2, nil, nil}},
+ []chunks.Sample{sample{0, 1, 3, nil, nil}, sample{0, 2, 2, nil, nil}, sample{0, 3, 6, nil, nil}, sample{0, 5, 1, nil, nil}, sample{0, 6, 7, nil, nil}, sample{0, 7, 2, nil, nil}},
),
}),
expChks: newMockChunkSeriesSet([]storage.ChunkSeries{
storage.NewListChunkSeriesFromSamples(labels.FromStrings("a", "a"),
- []chunks.Sample{sample{1, 2, nil, nil}, sample{2, 3, nil, nil}, sample{3, 4, nil, nil}}, []chunks.Sample{sample{5, 2, nil, nil}, sample{6, 3, nil, nil}, sample{7, 4, nil, nil}},
+ []chunks.Sample{sample{0, 1, 2, nil, nil}, sample{0, 2, 3, nil, nil}, sample{0, 3, 4, nil, nil}}, []chunks.Sample{sample{0, 5, 2, nil, nil}, sample{0, 6, 3, nil, nil}, sample{0, 7, 4, nil, nil}},
),
storage.NewListChunkSeriesFromSamples(labels.FromStrings("a", "a", "b", "b"),
- []chunks.Sample{sample{1, 1, nil, nil}, sample{2, 2, nil, nil}, sample{3, 3, nil, nil}}, []chunks.Sample{sample{5, 3, nil, nil}, sample{6, 6, nil, nil}},
+ []chunks.Sample{sample{0, 1, 1, nil, nil}, sample{0, 2, 2, nil, nil}, sample{0, 3, 3, nil, nil}}, []chunks.Sample{sample{0, 5, 3, nil, nil}, sample{0, 6, 6, nil, nil}},
),
storage.NewListChunkSeriesFromSamples(labels.FromStrings("b", "b"),
- []chunks.Sample{sample{1, 3, nil, nil}, sample{2, 2, nil, nil}, sample{3, 6, nil, nil}}, []chunks.Sample{sample{5, 1, nil, nil}, sample{6, 7, nil, nil}, sample{7, 2, nil, nil}},
+ []chunks.Sample{sample{0, 1, 3, nil, nil}, sample{0, 2, 2, nil, nil}, sample{0, 3, 6, nil, nil}}, []chunks.Sample{sample{0, 5, 1, nil, nil}, sample{0, 6, 7, nil, nil}, sample{0, 7, 2, nil, nil}},
),
}),
},
@@ -345,18 +345,18 @@ func TestBlockQuerier(t *testing.T) {
ms: []*labels.Matcher{labels.MustNewMatcher(labels.MatchEqual, "a", "a")},
exp: newMockSeriesSet([]storage.Series{
storage.NewListSeries(labels.FromStrings("a", "a"),
- []chunks.Sample{sample{2, 3, nil, nil}, sample{3, 4, nil, nil}, sample{5, 2, nil, nil}, sample{6, 3, nil, nil}},
+ []chunks.Sample{sample{0, 2, 3, nil, nil}, sample{0, 3, 4, nil, nil}, sample{0, 5, 2, nil, nil}, sample{0, 6, 3, nil, nil}},
),
storage.NewListSeries(labels.FromStrings("a", "a", "b", "b"),
- []chunks.Sample{sample{2, 2, nil, nil}, sample{3, 3, nil, nil}, sample{5, 3, nil, nil}, sample{6, 6, nil, nil}},
+ []chunks.Sample{sample{0, 2, 2, nil, nil}, sample{0, 3, 3, nil, nil}, sample{0, 5, 3, nil, nil}, sample{0, 6, 6, nil, nil}},
),
}),
expChks: newMockChunkSeriesSet([]storage.ChunkSeries{
storage.NewListChunkSeriesFromSamples(labels.FromStrings("a", "a"),
- []chunks.Sample{sample{2, 3, nil, nil}, sample{3, 4, nil, nil}}, []chunks.Sample{sample{5, 2, nil, nil}, sample{6, 3, nil, nil}},
+ []chunks.Sample{sample{0, 2, 3, nil, nil}, sample{0, 3, 4, nil, nil}}, []chunks.Sample{sample{0, 5, 2, nil, nil}, sample{0, 6, 3, nil, nil}},
),
storage.NewListChunkSeriesFromSamples(labels.FromStrings("a", "a", "b", "b"),
- []chunks.Sample{sample{2, 2, nil, nil}, sample{3, 3, nil, nil}}, []chunks.Sample{sample{5, 3, nil, nil}, sample{6, 6, nil, nil}},
+ []chunks.Sample{sample{0, 2, 2, nil, nil}, sample{0, 3, 3, nil, nil}}, []chunks.Sample{sample{0, 5, 3, nil, nil}, sample{0, 6, 6, nil, nil}},
),
}),
},
@@ -369,20 +369,20 @@ func TestBlockQuerier(t *testing.T) {
ms: []*labels.Matcher{labels.MustNewMatcher(labels.MatchEqual, "a", "a")},
exp: newMockSeriesSet([]storage.Series{
storage.NewListSeries(labels.FromStrings("a", "a"),
- []chunks.Sample{sample{1, 2, nil, nil}, sample{2, 3, nil, nil}, sample{3, 4, nil, nil}, sample{5, 2, nil, nil}, sample{6, 3, nil, nil}, sample{7, 4, nil, nil}},
+ []chunks.Sample{sample{0, 1, 2, nil, nil}, sample{0, 2, 3, nil, nil}, sample{0, 3, 4, nil, nil}, sample{0, 5, 2, nil, nil}, sample{0, 6, 3, nil, nil}, sample{0, 7, 4, nil, nil}},
),
storage.NewListSeries(labels.FromStrings("a", "a", "b", "b"),
- []chunks.Sample{sample{1, 1, nil, nil}, sample{2, 2, nil, nil}, sample{3, 3, nil, nil}, sample{5, 3, nil, nil}, sample{6, 6, nil, nil}},
+ []chunks.Sample{sample{0, 1, 1, nil, nil}, sample{0, 2, 2, nil, nil}, sample{0, 3, 3, nil, nil}, sample{0, 5, 3, nil, nil}, sample{0, 6, 6, nil, nil}},
),
}),
expChks: newMockChunkSeriesSet([]storage.ChunkSeries{
storage.NewListChunkSeriesFromSamples(labels.FromStrings("a", "a"),
- []chunks.Sample{sample{1, 2, nil, nil}, sample{2, 3, nil, nil}, sample{3, 4, nil, nil}},
- []chunks.Sample{sample{5, 2, nil, nil}, sample{6, 3, nil, nil}, sample{7, 4, nil, nil}},
+ []chunks.Sample{sample{0, 1, 2, nil, nil}, sample{0, 2, 3, nil, nil}, sample{0, 3, 4, nil, nil}},
+ []chunks.Sample{sample{0, 5, 2, nil, nil}, sample{0, 6, 3, nil, nil}, sample{0, 7, 4, nil, nil}},
),
storage.NewListChunkSeriesFromSamples(labels.FromStrings("a", "a", "b", "b"),
- []chunks.Sample{sample{1, 1, nil, nil}, sample{2, 2, nil, nil}, sample{3, 3, nil, nil}},
- []chunks.Sample{sample{5, 3, nil, nil}, sample{6, 6, nil, nil}},
+ []chunks.Sample{sample{0, 1, 1, nil, nil}, sample{0, 2, 2, nil, nil}, sample{0, 3, 3, nil, nil}},
+ []chunks.Sample{sample{0, 5, 3, nil, nil}, sample{0, 6, 6, nil, nil}},
),
}),
},
@@ -395,18 +395,18 @@ func TestBlockQuerier(t *testing.T) {
ms: []*labels.Matcher{labels.MustNewMatcher(labels.MatchEqual, "a", "a")},
exp: newMockSeriesSet([]storage.Series{
storage.NewListSeries(labels.FromStrings("a", "a"),
- []chunks.Sample{sample{5, 2, nil, nil}, sample{6, 3, nil, nil}, sample{7, 4, nil, nil}},
+ []chunks.Sample{sample{0, 5, 2, nil, nil}, sample{0, 6, 3, nil, nil}, sample{0, 7, 4, nil, nil}},
),
storage.NewListSeries(labels.FromStrings("a", "a", "b", "b"),
- []chunks.Sample{sample{5, 3, nil, nil}, sample{6, 6, nil, nil}},
+ []chunks.Sample{sample{0, 5, 3, nil, nil}, sample{0, 6, 6, nil, nil}},
),
}),
expChks: newMockChunkSeriesSet([]storage.ChunkSeries{
storage.NewListChunkSeriesFromSamples(labels.FromStrings("a", "a"),
- []chunks.Sample{sample{5, 2, nil, nil}, sample{6, 3, nil, nil}, sample{7, 4, nil, nil}},
+ []chunks.Sample{sample{0, 5, 2, nil, nil}, sample{0, 6, 3, nil, nil}, sample{0, 7, 4, nil, nil}},
),
storage.NewListChunkSeriesFromSamples(labels.FromStrings("a", "a", "b", "b"),
- []chunks.Sample{sample{5, 3, nil, nil}, sample{6, 6, nil, nil}},
+ []chunks.Sample{sample{0, 5, 3, nil, nil}, sample{0, 6, 6, nil, nil}},
),
}),
},
@@ -454,24 +454,24 @@ func TestBlockQuerier_AgainstHeadWithOpenChunks(t *testing.T) {
ms: []*labels.Matcher{labels.MustNewMatcher(labels.MatchRegexp, "a", ".*")},
exp: newMockSeriesSet([]storage.Series{
storage.NewListSeries(labels.FromStrings("a", "a"),
- []chunks.Sample{sample{1, 2, nil, nil}, sample{2, 3, nil, nil}, sample{3, 4, nil, nil}, sample{5, 2, nil, nil}, sample{6, 3, nil, nil}, sample{7, 4, nil, nil}},
+ []chunks.Sample{sample{0, 1, 2, nil, nil}, sample{0, 2, 3, nil, nil}, sample{0, 3, 4, nil, nil}, sample{0, 5, 2, nil, nil}, sample{0, 6, 3, nil, nil}, sample{0, 7, 4, nil, nil}},
),
storage.NewListSeries(labels.FromStrings("a", "a", "b", "b"),
- []chunks.Sample{sample{1, 1, nil, nil}, sample{2, 2, nil, nil}, sample{3, 3, nil, nil}, sample{5, 3, nil, nil}, sample{6, 6, nil, nil}},
+ []chunks.Sample{sample{0, 1, 1, nil, nil}, sample{0, 2, 2, nil, nil}, sample{0, 3, 3, nil, nil}, sample{0, 5, 3, nil, nil}, sample{0, 6, 6, nil, nil}},
),
storage.NewListSeries(labels.FromStrings("b", "b"),
- []chunks.Sample{sample{1, 3, nil, nil}, sample{2, 2, nil, nil}, sample{3, 6, nil, nil}, sample{5, 1, nil, nil}, sample{6, 7, nil, nil}, sample{7, 2, nil, nil}},
+ []chunks.Sample{sample{0, 1, 3, nil, nil}, sample{0, 2, 2, nil, nil}, sample{0, 3, 6, nil, nil}, sample{0, 5, 1, nil, nil}, sample{0, 6, 7, nil, nil}, sample{0, 7, 2, nil, nil}},
),
}),
expChks: newMockChunkSeriesSet([]storage.ChunkSeries{
storage.NewListChunkSeriesFromSamples(labels.FromStrings("a", "a"),
- []chunks.Sample{sample{1, 2, nil, nil}, sample{2, 3, nil, nil}, sample{3, 4, nil, nil}, sample{5, 2, nil, nil}, sample{6, 3, nil, nil}, sample{7, 4, nil, nil}},
+ []chunks.Sample{sample{0, 1, 2, nil, nil}, sample{0, 2, 3, nil, nil}, sample{0, 3, 4, nil, nil}, sample{0, 5, 2, nil, nil}, sample{0, 6, 3, nil, nil}, sample{0, 7, 4, nil, nil}},
),
storage.NewListChunkSeriesFromSamples(labels.FromStrings("a", "a", "b", "b"),
- []chunks.Sample{sample{1, 1, nil, nil}, sample{2, 2, nil, nil}, sample{3, 3, nil, nil}, sample{5, 3, nil, nil}, sample{6, 6, nil, nil}},
+ []chunks.Sample{sample{0, 1, 1, nil, nil}, sample{0, 2, 2, nil, nil}, sample{0, 3, 3, nil, nil}, sample{0, 5, 3, nil, nil}, sample{0, 6, 6, nil, nil}},
),
storage.NewListChunkSeriesFromSamples(labels.FromStrings("b", "b"),
- []chunks.Sample{sample{1, 3, nil, nil}, sample{2, 2, nil, nil}, sample{3, 6, nil, nil}, sample{5, 1, nil, nil}, sample{6, 7, nil, nil}, sample{7, 2, nil, nil}},
+ []chunks.Sample{sample{0, 1, 3, nil, nil}, sample{0, 2, 2, nil, nil}, sample{0, 3, 6, nil, nil}, sample{0, 5, 1, nil, nil}, sample{0, 6, 7, nil, nil}, sample{0, 7, 2, nil, nil}},
),
}),
},
@@ -481,18 +481,18 @@ func TestBlockQuerier_AgainstHeadWithOpenChunks(t *testing.T) {
ms: []*labels.Matcher{labels.MustNewMatcher(labels.MatchEqual, "a", "a")},
exp: newMockSeriesSet([]storage.Series{
storage.NewListSeries(labels.FromStrings("a", "a"),
- []chunks.Sample{sample{2, 3, nil, nil}, sample{3, 4, nil, nil}, sample{5, 2, nil, nil}, sample{6, 3, nil, nil}},
+ []chunks.Sample{sample{0, 2, 3, nil, nil}, sample{0, 3, 4, nil, nil}, sample{0, 5, 2, nil, nil}, sample{0, 6, 3, nil, nil}},
),
storage.NewListSeries(labels.FromStrings("a", "a", "b", "b"),
- []chunks.Sample{sample{2, 2, nil, nil}, sample{3, 3, nil, nil}, sample{5, 3, nil, nil}, sample{6, 6, nil, nil}},
+ []chunks.Sample{sample{0, 2, 2, nil, nil}, sample{0, 3, 3, nil, nil}, sample{0, 5, 3, nil, nil}, sample{0, 6, 6, nil, nil}},
),
}),
expChks: newMockChunkSeriesSet([]storage.ChunkSeries{
storage.NewListChunkSeriesFromSamples(labels.FromStrings("a", "a"),
- []chunks.Sample{sample{2, 3, nil, nil}, sample{3, 4, nil, nil}, sample{5, 2, nil, nil}, sample{6, 3, nil, nil}},
+ []chunks.Sample{sample{0, 2, 3, nil, nil}, sample{0, 3, 4, nil, nil}, sample{0, 5, 2, nil, nil}, sample{0, 6, 3, nil, nil}},
),
storage.NewListChunkSeriesFromSamples(labels.FromStrings("a", "a", "b", "b"),
- []chunks.Sample{sample{2, 2, nil, nil}, sample{3, 3, nil, nil}, sample{5, 3, nil, nil}, sample{6, 6, nil, nil}},
+ []chunks.Sample{sample{0, 2, 2, nil, nil}, sample{0, 3, 3, nil, nil}, sample{0, 5, 3, nil, nil}, sample{0, 6, 6, nil, nil}},
),
}),
},
@@ -537,18 +537,18 @@ func TestBlockQuerier_TrimmingDoesNotModifyOriginalTombstoneIntervals(t *testing
ms: []*labels.Matcher{labels.MustNewMatcher(labels.MatchRegexp, "a", "a")},
exp: newMockSeriesSet([]storage.Series{
storage.NewListSeries(labels.FromStrings("a", "a"),
- []chunks.Sample{sample{3, 4, nil, nil}, sample{5, 2, nil, nil}, sample{6, 3, nil, nil}},
+ []chunks.Sample{sample{0, 3, 4, nil, nil}, sample{0, 5, 2, nil, nil}, sample{0, 6, 3, nil, nil}},
),
storage.NewListSeries(labels.FromStrings("a", "a", "b", "b"),
- []chunks.Sample{sample{3, 3, nil, nil}, sample{5, 3, nil, nil}, sample{6, 6, nil, nil}},
+ []chunks.Sample{sample{0, 3, 3, nil, nil}, sample{0, 5, 3, nil, nil}, sample{0, 6, 6, nil, nil}},
),
}),
expChks: newMockChunkSeriesSet([]storage.ChunkSeries{
storage.NewListChunkSeriesFromSamples(labels.FromStrings("a", "a"),
- []chunks.Sample{sample{3, 4, nil, nil}}, []chunks.Sample{sample{5, 2, nil, nil}, sample{6, 3, nil, nil}},
+ []chunks.Sample{sample{0, 3, 4, nil, nil}}, []chunks.Sample{sample{0, 5, 2, nil, nil}, sample{0, 6, 3, nil, nil}},
),
storage.NewListChunkSeriesFromSamples(labels.FromStrings("a", "a", "b", "b"),
- []chunks.Sample{sample{3, 3, nil, nil}}, []chunks.Sample{sample{5, 3, nil, nil}, sample{6, 6, nil, nil}},
+ []chunks.Sample{sample{0, 3, 3, nil, nil}}, []chunks.Sample{sample{0, 5, 3, nil, nil}, sample{0, 6, 6, nil, nil}},
),
}),
}
@@ -574,22 +574,22 @@ var testData = []seriesSamples{
{
lset: map[string]string{"a": "a"},
chunks: [][]sample{
- {{1, 2, nil, nil}, {2, 3, nil, nil}, {3, 4, nil, nil}},
- {{5, 2, nil, nil}, {6, 3, nil, nil}, {7, 4, nil, nil}},
+ {{0, 1, 2, nil, nil}, {0, 2, 3, nil, nil}, {0, 3, 4, nil, nil}},
+ {{0, 5, 2, nil, nil}, {0, 6, 3, nil, nil}, {0, 7, 4, nil, nil}},
},
},
{
lset: map[string]string{"a": "a", "b": "b"},
chunks: [][]sample{
- {{1, 1, nil, nil}, {2, 2, nil, nil}, {3, 3, nil, nil}},
- {{5, 3, nil, nil}, {6, 6, nil, nil}},
+ {{0, 1, 1, nil, nil}, {0, 2, 2, nil, nil}, {0, 3, 3, nil, nil}},
+ {{0, 5, 3, nil, nil}, {0, 6, 6, nil, nil}},
},
},
{
lset: map[string]string{"b": "b"},
chunks: [][]sample{
- {{1, 3, nil, nil}, {2, 2, nil, nil}, {3, 6, nil, nil}},
- {{5, 1, nil, nil}, {6, 7, nil, nil}, {7, 2, nil, nil}},
+ {{0, 1, 3, nil, nil}, {0, 2, 2, nil, nil}, {0, 3, 6, nil, nil}},
+ {{0, 5, 1, nil, nil}, {0, 6, 7, nil, nil}, {0, 7, 2, nil, nil}},
},
},
}
@@ -636,24 +636,24 @@ func TestBlockQuerierDelete(t *testing.T) {
ms: []*labels.Matcher{labels.MustNewMatcher(labels.MatchRegexp, "a", ".*")},
exp: newMockSeriesSet([]storage.Series{
storage.NewListSeries(labels.FromStrings("a", "a"),
- []chunks.Sample{sample{5, 2, nil, nil}, sample{6, 3, nil, nil}, sample{7, 4, nil, nil}},
+ []chunks.Sample{sample{0, 5, 2, nil, nil}, sample{0, 6, 3, nil, nil}, sample{0, 7, 4, nil, nil}},
),
storage.NewListSeries(labels.FromStrings("a", "a", "b", "b"),
- []chunks.Sample{sample{5, 3, nil, nil}},
+ []chunks.Sample{sample{0, 5, 3, nil, nil}},
),
storage.NewListSeries(labels.FromStrings("b", "b"),
- []chunks.Sample{sample{1, 3, nil, nil}, sample{2, 2, nil, nil}, sample{3, 6, nil, nil}, sample{5, 1, nil, nil}},
+ []chunks.Sample{sample{0, 1, 3, nil, nil}, sample{0, 2, 2, nil, nil}, sample{0, 3, 6, nil, nil}, sample{0, 5, 1, nil, nil}},
),
}),
expChks: newMockChunkSeriesSet([]storage.ChunkSeries{
storage.NewListChunkSeriesFromSamples(labels.FromStrings("a", "a"),
- []chunks.Sample{sample{5, 2, nil, nil}, sample{6, 3, nil, nil}, sample{7, 4, nil, nil}},
+ []chunks.Sample{sample{0, 5, 2, nil, nil}, sample{0, 6, 3, nil, nil}, sample{0, 7, 4, nil, nil}},
),
storage.NewListChunkSeriesFromSamples(labels.FromStrings("a", "a", "b", "b"),
- []chunks.Sample{sample{5, 3, nil, nil}},
+ []chunks.Sample{sample{0, 5, 3, nil, nil}},
),
storage.NewListChunkSeriesFromSamples(labels.FromStrings("b", "b"),
- []chunks.Sample{sample{1, 3, nil, nil}, sample{2, 2, nil, nil}, sample{3, 6, nil, nil}}, []chunks.Sample{sample{5, 1, nil, nil}},
+ []chunks.Sample{sample{0, 1, 3, nil, nil}, sample{0, 2, 2, nil, nil}, sample{0, 3, 6, nil, nil}}, []chunks.Sample{sample{0, 5, 1, nil, nil}},
),
}),
},
@@ -663,18 +663,18 @@ func TestBlockQuerierDelete(t *testing.T) {
ms: []*labels.Matcher{labels.MustNewMatcher(labels.MatchEqual, "a", "a")},
exp: newMockSeriesSet([]storage.Series{
storage.NewListSeries(labels.FromStrings("a", "a"),
- []chunks.Sample{sample{5, 2, nil, nil}, sample{6, 3, nil, nil}},
+ []chunks.Sample{sample{0, 5, 2, nil, nil}, sample{0, 6, 3, nil, nil}},
),
storage.NewListSeries(labels.FromStrings("a", "a", "b", "b"),
- []chunks.Sample{sample{5, 3, nil, nil}},
+ []chunks.Sample{sample{0, 5, 3, nil, nil}},
),
}),
expChks: newMockChunkSeriesSet([]storage.ChunkSeries{
storage.NewListChunkSeriesFromSamples(labels.FromStrings("a", "a"),
- []chunks.Sample{sample{5, 2, nil, nil}, sample{6, 3, nil, nil}},
+ []chunks.Sample{sample{0, 5, 2, nil, nil}, sample{0, 6, 3, nil, nil}},
),
storage.NewListChunkSeriesFromSamples(labels.FromStrings("a", "a", "b", "b"),
- []chunks.Sample{sample{5, 3, nil, nil}},
+ []chunks.Sample{sample{0, 5, 3, nil, nil}},
),
}),
},
@@ -790,6 +790,10 @@ func (it *mockSampleIterator) AtT() int64 {
return it.s[it.idx].T()
}
+func (it *mockSampleIterator) AtST() int64 {
+ return it.s[it.idx].ST()
+}
+
func (it *mockSampleIterator) Next() chunkenc.ValueType {
if it.idx < len(it.s)-1 {
it.idx++
@@ -871,15 +875,15 @@ func TestPopulateWithTombSeriesIterators(t *testing.T) {
{
name: "one chunk",
samples: [][]chunks.Sample{
- {sample{1, 2, nil, nil}, sample{2, 3, nil, nil}, sample{3, 5, nil, nil}, sample{6, 1, nil, nil}},
+ {sample{0, 1, 2, nil, nil}, sample{0, 2, 3, nil, nil}, sample{0, 3, 5, nil, nil}, sample{0, 6, 1, nil, nil}},
},
expected: []chunks.Sample{
- sample{1, 2, nil, nil}, sample{2, 3, nil, nil}, sample{3, 5, nil, nil}, sample{6, 1, nil, nil},
+ sample{0, 1, 2, nil, nil}, sample{0, 2, 3, nil, nil}, sample{0, 3, 5, nil, nil}, sample{0, 6, 1, nil, nil},
},
expectedChks: []chunks.Meta{
assureChunkFromSamples(t, []chunks.Sample{
- sample{1, 2, nil, nil}, sample{2, 3, nil, nil}, sample{3, 5, nil, nil}, sample{6, 1, nil, nil},
+ sample{0, 1, 2, nil, nil}, sample{0, 2, 3, nil, nil}, sample{0, 3, 5, nil, nil}, sample{0, 6, 1, nil, nil},
}),
},
expectedMinMaxTimes: []minMaxTimes{{1, 6}},
@@ -887,19 +891,19 @@ func TestPopulateWithTombSeriesIterators(t *testing.T) {
{
name: "two full chunks",
samples: [][]chunks.Sample{
- {sample{1, 2, nil, nil}, sample{2, 3, nil, nil}, sample{3, 5, nil, nil}, sample{6, 1, nil, nil}},
- {sample{7, 89, nil, nil}, sample{9, 8, nil, nil}},
+ {sample{0, 1, 2, nil, nil}, sample{0, 2, 3, nil, nil}, sample{0, 3, 5, nil, nil}, sample{0, 6, 1, nil, nil}},
+ {sample{0, 7, 89, nil, nil}, sample{0, 9, 8, nil, nil}},
},
expected: []chunks.Sample{
- sample{1, 2, nil, nil}, sample{2, 3, nil, nil}, sample{3, 5, nil, nil}, sample{6, 1, nil, nil}, sample{7, 89, nil, nil}, sample{9, 8, nil, nil},
+ sample{0, 1, 2, nil, nil}, sample{0, 2, 3, nil, nil}, sample{0, 3, 5, nil, nil}, sample{0, 6, 1, nil, nil}, sample{0, 7, 89, nil, nil}, sample{0, 9, 8, nil, nil},
},
expectedChks: []chunks.Meta{
assureChunkFromSamples(t, []chunks.Sample{
- sample{1, 2, nil, nil}, sample{2, 3, nil, nil}, sample{3, 5, nil, nil}, sample{6, 1, nil, nil},
+ sample{0, 1, 2, nil, nil}, sample{0, 2, 3, nil, nil}, sample{0, 3, 5, nil, nil}, sample{0, 6, 1, nil, nil},
}),
assureChunkFromSamples(t, []chunks.Sample{
- sample{7, 89, nil, nil}, sample{9, 8, nil, nil},
+ sample{0, 7, 89, nil, nil}, sample{0, 9, 8, nil, nil},
}),
},
expectedMinMaxTimes: []minMaxTimes{{1, 6}, {7, 9}},
@@ -907,23 +911,23 @@ func TestPopulateWithTombSeriesIterators(t *testing.T) {
{
name: "three full chunks",
samples: [][]chunks.Sample{
- {sample{1, 2, nil, nil}, sample{2, 3, nil, nil}, sample{3, 5, nil, nil}, sample{6, 1, nil, nil}},
- {sample{7, 89, nil, nil}, sample{9, 8, nil, nil}},
- {sample{10, 22, nil, nil}, sample{203, 3493, nil, nil}},
+ {sample{0, 1, 2, nil, nil}, sample{0, 2, 3, nil, nil}, sample{0, 3, 5, nil, nil}, sample{0, 6, 1, nil, nil}},
+ {sample{0, 7, 89, nil, nil}, sample{0, 9, 8, nil, nil}},
+ {sample{0, 10, 22, nil, nil}, sample{0, 203, 3493, nil, nil}},
},
expected: []chunks.Sample{
- sample{1, 2, nil, nil}, sample{2, 3, nil, nil}, sample{3, 5, nil, nil}, sample{6, 1, nil, nil}, sample{7, 89, nil, nil}, sample{9, 8, nil, nil}, sample{10, 22, nil, nil}, sample{203, 3493, nil, nil},
+ sample{0, 1, 2, nil, nil}, sample{0, 2, 3, nil, nil}, sample{0, 3, 5, nil, nil}, sample{0, 6, 1, nil, nil}, sample{0, 7, 89, nil, nil}, sample{0, 9, 8, nil, nil}, sample{0, 10, 22, nil, nil}, sample{0, 203, 3493, nil, nil},
},
expectedChks: []chunks.Meta{
assureChunkFromSamples(t, []chunks.Sample{
- sample{1, 2, nil, nil}, sample{2, 3, nil, nil}, sample{3, 5, nil, nil}, sample{6, 1, nil, nil},
+ sample{0, 1, 2, nil, nil}, sample{0, 2, 3, nil, nil}, sample{0, 3, 5, nil, nil}, sample{0, 6, 1, nil, nil},
}),
assureChunkFromSamples(t, []chunks.Sample{
- sample{7, 89, nil, nil}, sample{9, 8, nil, nil},
+ sample{0, 7, 89, nil, nil}, sample{0, 9, 8, nil, nil},
}),
assureChunkFromSamples(t, []chunks.Sample{
- sample{10, 22, nil, nil}, sample{203, 3493, nil, nil},
+ sample{0, 10, 22, nil, nil}, sample{0, 203, 3493, nil, nil},
}),
},
expectedMinMaxTimes: []minMaxTimes{{1, 6}, {7, 9}, {10, 203}},
@@ -939,8 +943,8 @@ func TestPopulateWithTombSeriesIterators(t *testing.T) {
{
name: "two chunks and seek beyond chunks",
samples: [][]chunks.Sample{
- {sample{1, 2, nil, nil}, sample{3, 5, nil, nil}, sample{6, 1, nil, nil}},
- {sample{7, 89, nil, nil}, sample{9, 8, nil, nil}},
+ {sample{0, 1, 2, nil, nil}, sample{0, 3, 5, nil, nil}, sample{0, 6, 1, nil, nil}},
+ {sample{0, 7, 89, nil, nil}, sample{0, 9, 8, nil, nil}},
},
seek: 10,
@@ -949,27 +953,27 @@ func TestPopulateWithTombSeriesIterators(t *testing.T) {
{
name: "two chunks and seek on middle of first chunk",
samples: [][]chunks.Sample{
- {sample{1, 2, nil, nil}, sample{3, 5, nil, nil}, sample{6, 1, nil, nil}},
- {sample{7, 89, nil, nil}, sample{9, 8, nil, nil}},
+ {sample{0, 1, 2, nil, nil}, sample{0, 3, 5, nil, nil}, sample{0, 6, 1, nil, nil}},
+ {sample{0, 7, 89, nil, nil}, sample{0, 9, 8, nil, nil}},
},
seek: 2,
seekSuccess: true,
expected: []chunks.Sample{
- sample{3, 5, nil, nil}, sample{6, 1, nil, nil}, sample{7, 89, nil, nil}, sample{9, 8, nil, nil},
+ sample{0, 3, 5, nil, nil}, sample{0, 6, 1, nil, nil}, sample{0, 7, 89, nil, nil}, sample{0, 9, 8, nil, nil},
},
},
{
name: "two chunks and seek before first chunk",
samples: [][]chunks.Sample{
- {sample{1, 2, nil, nil}, sample{3, 5, nil, nil}, sample{6, 1, nil, nil}},
- {sample{7, 89, nil, nil}, sample{9, 8, nil, nil}},
+ {sample{0, 1, 2, nil, nil}, sample{0, 3, 5, nil, nil}, sample{0, 6, 1, nil, nil}},
+ {sample{0, 7, 89, nil, nil}, sample{0, 9, 8, nil, nil}},
},
seek: -32,
seekSuccess: true,
expected: []chunks.Sample{
- sample{1, 2, nil, nil}, sample{3, 5, nil, nil}, sample{6, 1, nil, nil}, sample{7, 89, nil, nil}, sample{9, 8, nil, nil},
+ sample{0, 1, 2, nil, nil}, sample{0, 3, 5, nil, nil}, sample{0, 6, 1, nil, nil}, sample{0, 7, 89, nil, nil}, sample{0, 9, 8, nil, nil},
},
},
// Deletion / Trim cases.
@@ -981,20 +985,20 @@ func TestPopulateWithTombSeriesIterators(t *testing.T) {
{
name: "two chunks with trimmed first and last samples from edge chunks",
samples: [][]chunks.Sample{
- {sample{1, 2, nil, nil}, sample{2, 3, nil, nil}, sample{3, 5, nil, nil}, sample{6, 1, nil, nil}},
- {sample{7, 89, nil, nil}, sample{9, 8, nil, nil}},
+ {sample{0, 1, 2, nil, nil}, sample{0, 2, 3, nil, nil}, sample{0, 3, 5, nil, nil}, sample{0, 6, 1, nil, nil}},
+ {sample{0, 7, 89, nil, nil}, sample{0, 9, 8, nil, nil}},
},
intervals: tombstones.Intervals{{Mint: math.MinInt64, Maxt: 2}}.Add(tombstones.Interval{Mint: 9, Maxt: math.MaxInt64}),
expected: []chunks.Sample{
- sample{3, 5, nil, nil}, sample{6, 1, nil, nil}, sample{7, 89, nil, nil},
+ sample{0, 3, 5, nil, nil}, sample{0, 6, 1, nil, nil}, sample{0, 7, 89, nil, nil},
},
expectedChks: []chunks.Meta{
assureChunkFromSamples(t, []chunks.Sample{
- sample{3, 5, nil, nil}, sample{6, 1, nil, nil},
+ sample{0, 3, 5, nil, nil}, sample{0, 6, 1, nil, nil},
}),
assureChunkFromSamples(t, []chunks.Sample{
- sample{7, 89, nil, nil},
+ sample{0, 7, 89, nil, nil},
}),
},
expectedMinMaxTimes: []minMaxTimes{{3, 6}, {7, 7}},
@@ -1002,20 +1006,20 @@ func TestPopulateWithTombSeriesIterators(t *testing.T) {
{
name: "two chunks with trimmed middle sample of first chunk",
samples: [][]chunks.Sample{
- {sample{1, 2, nil, nil}, sample{2, 3, nil, nil}, sample{3, 5, nil, nil}, sample{6, 1, nil, nil}},
- {sample{7, 89, nil, nil}, sample{9, 8, nil, nil}},
+ {sample{0, 1, 2, nil, nil}, sample{0, 2, 3, nil, nil}, sample{0, 3, 5, nil, nil}, sample{0, 6, 1, nil, nil}},
+ {sample{0, 7, 89, nil, nil}, sample{0, 9, 8, nil, nil}},
},
intervals: tombstones.Intervals{{Mint: 2, Maxt: 3}},
expected: []chunks.Sample{
- sample{1, 2, nil, nil}, sample{6, 1, nil, nil}, sample{7, 89, nil, nil}, sample{9, 8, nil, nil},
+ sample{0, 1, 2, nil, nil}, sample{0, 6, 1, nil, nil}, sample{0, 7, 89, nil, nil}, sample{0, 9, 8, nil, nil},
},
expectedChks: []chunks.Meta{
assureChunkFromSamples(t, []chunks.Sample{
- sample{1, 2, nil, nil}, sample{6, 1, nil, nil},
+ sample{0, 1, 2, nil, nil}, sample{0, 6, 1, nil, nil},
}),
assureChunkFromSamples(t, []chunks.Sample{
- sample{7, 89, nil, nil}, sample{9, 8, nil, nil},
+ sample{0, 7, 89, nil, nil}, sample{0, 9, 8, nil, nil},
}),
},
expectedMinMaxTimes: []minMaxTimes{{1, 6}, {7, 9}},
@@ -1023,20 +1027,20 @@ func TestPopulateWithTombSeriesIterators(t *testing.T) {
{
name: "two chunks with deletion across two chunks",
samples: [][]chunks.Sample{
- {sample{1, 2, nil, nil}, sample{2, 3, nil, nil}, sample{3, 5, nil, nil}, sample{6, 1, nil, nil}},
- {sample{7, 89, nil, nil}, sample{9, 8, nil, nil}},
+ {sample{0, 1, 2, nil, nil}, sample{0, 2, 3, nil, nil}, sample{0, 3, 5, nil, nil}, sample{0, 6, 1, nil, nil}},
+ {sample{0, 7, 89, nil, nil}, sample{0, 9, 8, nil, nil}},
},
intervals: tombstones.Intervals{{Mint: 6, Maxt: 7}},
expected: []chunks.Sample{
- sample{1, 2, nil, nil}, sample{2, 3, nil, nil}, sample{3, 5, nil, nil}, sample{9, 8, nil, nil},
+ sample{0, 1, 2, nil, nil}, sample{0, 2, 3, nil, nil}, sample{0, 3, 5, nil, nil}, sample{0, 9, 8, nil, nil},
},
expectedChks: []chunks.Meta{
assureChunkFromSamples(t, []chunks.Sample{
- sample{1, 2, nil, nil}, sample{2, 3, nil, nil}, sample{3, 5, nil, nil},
+ sample{0, 1, 2, nil, nil}, sample{0, 2, 3, nil, nil}, sample{0, 3, 5, nil, nil},
}),
assureChunkFromSamples(t, []chunks.Sample{
- sample{9, 8, nil, nil},
+ sample{0, 9, 8, nil, nil},
}),
},
expectedMinMaxTimes: []minMaxTimes{{1, 3}, {9, 9}},
@@ -1044,17 +1048,17 @@ func TestPopulateWithTombSeriesIterators(t *testing.T) {
{
name: "two chunks with first chunk deleted",
samples: [][]chunks.Sample{
- {sample{1, 2, nil, nil}, sample{2, 3, nil, nil}, sample{3, 5, nil, nil}, sample{6, 1, nil, nil}},
- {sample{7, 89, nil, nil}, sample{9, 8, nil, nil}},
+ {sample{0, 1, 2, nil, nil}, sample{0, 2, 3, nil, nil}, sample{0, 3, 5, nil, nil}, sample{0, 6, 1, nil, nil}},
+ {sample{0, 7, 89, nil, nil}, sample{0, 9, 8, nil, nil}},
},
intervals: tombstones.Intervals{{Mint: 1, Maxt: 6}},
expected: []chunks.Sample{
- sample{7, 89, nil, nil}, sample{9, 8, nil, nil},
+ sample{0, 7, 89, nil, nil}, sample{0, 9, 8, nil, nil},
},
expectedChks: []chunks.Meta{
assureChunkFromSamples(t, []chunks.Sample{
- sample{7, 89, nil, nil}, sample{9, 8, nil, nil},
+ sample{0, 7, 89, nil, nil}, sample{0, 9, 8, nil, nil},
}),
},
expectedMinMaxTimes: []minMaxTimes{{7, 9}},
@@ -1063,22 +1067,22 @@ func TestPopulateWithTombSeriesIterators(t *testing.T) {
{
name: "two chunks with trimmed first and last samples from edge chunks, seek from middle of first chunk",
samples: [][]chunks.Sample{
- {sample{1, 2, nil, nil}, sample{2, 3, nil, nil}, sample{3, 5, nil, nil}, sample{6, 1, nil, nil}},
- {sample{7, 89, nil, nil}, sample{9, 8, nil, nil}},
+ {sample{0, 1, 2, nil, nil}, sample{0, 2, 3, nil, nil}, sample{0, 3, 5, nil, nil}, sample{0, 6, 1, nil, nil}},
+ {sample{0, 7, 89, nil, nil}, sample{0, 9, 8, nil, nil}},
},
intervals: tombstones.Intervals{{Mint: math.MinInt64, Maxt: 2}}.Add(tombstones.Interval{Mint: 9, Maxt: math.MaxInt64}),
seek: 3,
seekSuccess: true,
expected: []chunks.Sample{
- sample{3, 5, nil, nil}, sample{6, 1, nil, nil}, sample{7, 89, nil, nil},
+ sample{0, 3, 5, nil, nil}, sample{0, 6, 1, nil, nil}, sample{0, 7, 89, nil, nil},
},
},
{
name: "one chunk where all samples are trimmed",
samples: [][]chunks.Sample{
- {sample{2, 3, nil, nil}, sample{3, 5, nil, nil}, sample{6, 1, nil, nil}},
- {sample{7, 89, nil, nil}, sample{9, 8, nil, nil}},
+ {sample{0, 2, 3, nil, nil}, sample{0, 3, 5, nil, nil}, sample{0, 6, 1, nil, nil}},
+ {sample{0, 7, 89, nil, nil}, sample{0, 9, 8, nil, nil}},
},
intervals: tombstones.Intervals{{Mint: math.MinInt64, Maxt: 3}}.Add(tombstones.Interval{Mint: 4, Maxt: math.MaxInt64}),
@@ -1089,24 +1093,24 @@ func TestPopulateWithTombSeriesIterators(t *testing.T) {
name: "one histogram chunk",
samples: [][]chunks.Sample{
{
- sample{1, 0, tsdbutil.GenerateTestHistogram(1), nil},
- sample{2, 0, tsdbutil.GenerateTestHistogram(2), nil},
- sample{3, 0, tsdbutil.GenerateTestHistogram(3), nil},
- sample{6, 0, tsdbutil.GenerateTestHistogram(6), nil},
+ sample{0, 1, 0, tsdbutil.GenerateTestHistogram(1), nil},
+ sample{0, 2, 0, tsdbutil.GenerateTestHistogram(2), nil},
+ sample{0, 3, 0, tsdbutil.GenerateTestHistogram(3), nil},
+ sample{0, 6, 0, tsdbutil.GenerateTestHistogram(6), nil},
},
},
expected: []chunks.Sample{
- sample{1, 0, tsdbutil.GenerateTestHistogram(1), nil},
- sample{2, 0, tsdbutil.SetHistogramNotCounterReset(tsdbutil.GenerateTestHistogram(2)), nil},
- sample{3, 0, tsdbutil.SetHistogramNotCounterReset(tsdbutil.GenerateTestHistogram(3)), nil},
- sample{6, 0, tsdbutil.SetHistogramNotCounterReset(tsdbutil.GenerateTestHistogram(6)), nil},
+ sample{0, 1, 0, tsdbutil.GenerateTestHistogram(1), nil},
+ sample{0, 2, 0, tsdbutil.SetHistogramNotCounterReset(tsdbutil.GenerateTestHistogram(2)), nil},
+ sample{0, 3, 0, tsdbutil.SetHistogramNotCounterReset(tsdbutil.GenerateTestHistogram(3)), nil},
+ sample{0, 6, 0, tsdbutil.SetHistogramNotCounterReset(tsdbutil.GenerateTestHistogram(6)), nil},
},
expectedChks: []chunks.Meta{
assureChunkFromSamples(t, []chunks.Sample{
- sample{1, 0, tsdbutil.GenerateTestHistogram(1), nil},
- sample{2, 0, tsdbutil.SetHistogramNotCounterReset(tsdbutil.GenerateTestHistogram(2)), nil},
- sample{3, 0, tsdbutil.SetHistogramNotCounterReset(tsdbutil.GenerateTestHistogram(3)), nil},
- sample{6, 0, tsdbutil.SetHistogramNotCounterReset(tsdbutil.GenerateTestHistogram(6)), nil},
+ sample{0, 1, 0, tsdbutil.GenerateTestHistogram(1), nil},
+ sample{0, 2, 0, tsdbutil.SetHistogramNotCounterReset(tsdbutil.GenerateTestHistogram(2)), nil},
+ sample{0, 3, 0, tsdbutil.SetHistogramNotCounterReset(tsdbutil.GenerateTestHistogram(3)), nil},
+ sample{0, 6, 0, tsdbutil.SetHistogramNotCounterReset(tsdbutil.GenerateTestHistogram(6)), nil},
}),
},
expectedMinMaxTimes: []minMaxTimes{{1, 6}},
@@ -1115,21 +1119,21 @@ func TestPopulateWithTombSeriesIterators(t *testing.T) {
name: "one histogram chunk intersect with earlier deletion interval",
samples: [][]chunks.Sample{
{
- sample{1, 0, tsdbutil.GenerateTestHistogram(1), nil},
- sample{2, 0, tsdbutil.GenerateTestHistogram(2), nil},
- sample{3, 0, tsdbutil.GenerateTestHistogram(3), nil},
- sample{6, 0, tsdbutil.GenerateTestHistogram(6), nil},
+ sample{0, 1, 0, tsdbutil.GenerateTestHistogram(1), nil},
+ sample{0, 2, 0, tsdbutil.GenerateTestHistogram(2), nil},
+ sample{0, 3, 0, tsdbutil.GenerateTestHistogram(3), nil},
+ sample{0, 6, 0, tsdbutil.GenerateTestHistogram(6), nil},
},
},
intervals: tombstones.Intervals{{Mint: 1, Maxt: 2}},
expected: []chunks.Sample{
- sample{3, 0, tsdbutil.SetHistogramNotCounterReset(tsdbutil.GenerateTestHistogram(3)), nil},
- sample{6, 0, tsdbutil.SetHistogramNotCounterReset(tsdbutil.GenerateTestHistogram(6)), nil},
+ sample{0, 3, 0, tsdbutil.SetHistogramNotCounterReset(tsdbutil.GenerateTestHistogram(3)), nil},
+ sample{0, 6, 0, tsdbutil.SetHistogramNotCounterReset(tsdbutil.GenerateTestHistogram(6)), nil},
},
expectedChks: []chunks.Meta{
assureChunkFromSamples(t, []chunks.Sample{
- sample{3, 0, tsdbutil.SetHistogramNotCounterReset(tsdbutil.GenerateTestHistogram(3)), nil},
- sample{6, 0, tsdbutil.SetHistogramNotCounterReset(tsdbutil.GenerateTestHistogram(6)), nil},
+ sample{0, 3, 0, tsdbutil.SetHistogramNotCounterReset(tsdbutil.GenerateTestHistogram(3)), nil},
+ sample{0, 6, 0, tsdbutil.SetHistogramNotCounterReset(tsdbutil.GenerateTestHistogram(6)), nil},
}),
},
expectedMinMaxTimes: []minMaxTimes{{3, 6}},
@@ -1138,23 +1142,23 @@ func TestPopulateWithTombSeriesIterators(t *testing.T) {
name: "one histogram chunk intersect with later deletion interval",
samples: [][]chunks.Sample{
{
- sample{1, 0, tsdbutil.GenerateTestHistogram(1), nil},
- sample{2, 0, tsdbutil.GenerateTestHistogram(2), nil},
- sample{3, 0, tsdbutil.GenerateTestHistogram(3), nil},
- sample{6, 0, tsdbutil.GenerateTestHistogram(6), nil},
+ sample{0, 1, 0, tsdbutil.GenerateTestHistogram(1), nil},
+ sample{0, 2, 0, tsdbutil.GenerateTestHistogram(2), nil},
+ sample{0, 3, 0, tsdbutil.GenerateTestHistogram(3), nil},
+ sample{0, 6, 0, tsdbutil.GenerateTestHistogram(6), nil},
},
},
intervals: tombstones.Intervals{{Mint: 5, Maxt: 20}},
expected: []chunks.Sample{
- sample{1, 0, tsdbutil.GenerateTestHistogram(1), nil},
- sample{2, 0, tsdbutil.SetHistogramNotCounterReset(tsdbutil.GenerateTestHistogram(2)), nil},
- sample{3, 0, tsdbutil.SetHistogramNotCounterReset(tsdbutil.GenerateTestHistogram(3)), nil},
+ sample{0, 1, 0, tsdbutil.GenerateTestHistogram(1), nil},
+ sample{0, 2, 0, tsdbutil.SetHistogramNotCounterReset(tsdbutil.GenerateTestHistogram(2)), nil},
+ sample{0, 3, 0, tsdbutil.SetHistogramNotCounterReset(tsdbutil.GenerateTestHistogram(3)), nil},
},
expectedChks: []chunks.Meta{
assureChunkFromSamples(t, []chunks.Sample{
- sample{1, 0, tsdbutil.GenerateTestHistogram(1), nil},
- sample{2, 0, tsdbutil.SetHistogramNotCounterReset(tsdbutil.GenerateTestHistogram(2)), nil},
- sample{3, 0, tsdbutil.SetHistogramNotCounterReset(tsdbutil.GenerateTestHistogram(3)), nil},
+ sample{0, 1, 0, tsdbutil.GenerateTestHistogram(1), nil},
+ sample{0, 2, 0, tsdbutil.SetHistogramNotCounterReset(tsdbutil.GenerateTestHistogram(2)), nil},
+ sample{0, 3, 0, tsdbutil.SetHistogramNotCounterReset(tsdbutil.GenerateTestHistogram(3)), nil},
}),
},
expectedMinMaxTimes: []minMaxTimes{{1, 3}},
@@ -1163,24 +1167,24 @@ func TestPopulateWithTombSeriesIterators(t *testing.T) {
name: "one float histogram chunk",
samples: [][]chunks.Sample{
{
- sample{1, 0, nil, tsdbutil.GenerateTestFloatHistogram(1)},
- sample{2, 0, nil, tsdbutil.GenerateTestFloatHistogram(2)},
- sample{3, 0, nil, tsdbutil.GenerateTestFloatHistogram(3)},
- sample{6, 0, nil, tsdbutil.GenerateTestFloatHistogram(6)},
+ sample{0, 1, 0, nil, tsdbutil.GenerateTestFloatHistogram(1)},
+ sample{0, 2, 0, nil, tsdbutil.GenerateTestFloatHistogram(2)},
+ sample{0, 3, 0, nil, tsdbutil.GenerateTestFloatHistogram(3)},
+ sample{0, 6, 0, nil, tsdbutil.GenerateTestFloatHistogram(6)},
},
},
expected: []chunks.Sample{
- sample{1, 0, nil, tsdbutil.GenerateTestFloatHistogram(1)},
- sample{2, 0, nil, tsdbutil.SetFloatHistogramNotCounterReset(tsdbutil.GenerateTestFloatHistogram(2))},
- sample{3, 0, nil, tsdbutil.SetFloatHistogramNotCounterReset(tsdbutil.GenerateTestFloatHistogram(3))},
- sample{6, 0, nil, tsdbutil.SetFloatHistogramNotCounterReset(tsdbutil.GenerateTestFloatHistogram(6))},
+ sample{0, 1, 0, nil, tsdbutil.GenerateTestFloatHistogram(1)},
+ sample{0, 2, 0, nil, tsdbutil.SetFloatHistogramNotCounterReset(tsdbutil.GenerateTestFloatHistogram(2))},
+ sample{0, 3, 0, nil, tsdbutil.SetFloatHistogramNotCounterReset(tsdbutil.GenerateTestFloatHistogram(3))},
+ sample{0, 6, 0, nil, tsdbutil.SetFloatHistogramNotCounterReset(tsdbutil.GenerateTestFloatHistogram(6))},
},
expectedChks: []chunks.Meta{
assureChunkFromSamples(t, []chunks.Sample{
- sample{1, 0, nil, tsdbutil.GenerateTestFloatHistogram(1)},
- sample{2, 0, nil, tsdbutil.SetFloatHistogramNotCounterReset(tsdbutil.GenerateTestFloatHistogram(2))},
- sample{3, 0, nil, tsdbutil.SetFloatHistogramNotCounterReset(tsdbutil.GenerateTestFloatHistogram(3))},
- sample{6, 0, nil, tsdbutil.SetFloatHistogramNotCounterReset(tsdbutil.GenerateTestFloatHistogram(6))},
+ sample{0, 1, 0, nil, tsdbutil.GenerateTestFloatHistogram(1)},
+ sample{0, 2, 0, nil, tsdbutil.SetFloatHistogramNotCounterReset(tsdbutil.GenerateTestFloatHistogram(2))},
+ sample{0, 3, 0, nil, tsdbutil.SetFloatHistogramNotCounterReset(tsdbutil.GenerateTestFloatHistogram(3))},
+ sample{0, 6, 0, nil, tsdbutil.SetFloatHistogramNotCounterReset(tsdbutil.GenerateTestFloatHistogram(6))},
}),
},
expectedMinMaxTimes: []minMaxTimes{{1, 6}},
@@ -1189,21 +1193,21 @@ func TestPopulateWithTombSeriesIterators(t *testing.T) {
name: "one float histogram chunk intersect with earlier deletion interval",
samples: [][]chunks.Sample{
{
- sample{1, 0, nil, tsdbutil.GenerateTestFloatHistogram(1)},
- sample{2, 0, nil, tsdbutil.GenerateTestFloatHistogram(2)},
- sample{3, 0, nil, tsdbutil.GenerateTestFloatHistogram(3)},
- sample{6, 0, nil, tsdbutil.GenerateTestFloatHistogram(6)},
+ sample{0, 1, 0, nil, tsdbutil.GenerateTestFloatHistogram(1)},
+ sample{0, 2, 0, nil, tsdbutil.GenerateTestFloatHistogram(2)},
+ sample{0, 3, 0, nil, tsdbutil.GenerateTestFloatHistogram(3)},
+ sample{0, 6, 0, nil, tsdbutil.GenerateTestFloatHistogram(6)},
},
},
intervals: tombstones.Intervals{{Mint: 1, Maxt: 2}},
expected: []chunks.Sample{
- sample{3, 0, nil, tsdbutil.SetFloatHistogramNotCounterReset(tsdbutil.GenerateTestFloatHistogram(3))},
- sample{6, 0, nil, tsdbutil.SetFloatHistogramNotCounterReset(tsdbutil.GenerateTestFloatHistogram(6))},
+ sample{0, 3, 0, nil, tsdbutil.SetFloatHistogramNotCounterReset(tsdbutil.GenerateTestFloatHistogram(3))},
+ sample{0, 6, 0, nil, tsdbutil.SetFloatHistogramNotCounterReset(tsdbutil.GenerateTestFloatHistogram(6))},
},
expectedChks: []chunks.Meta{
assureChunkFromSamples(t, []chunks.Sample{
- sample{3, 0, nil, tsdbutil.SetFloatHistogramNotCounterReset(tsdbutil.GenerateTestFloatHistogram(3))},
- sample{6, 0, nil, tsdbutil.SetFloatHistogramNotCounterReset(tsdbutil.GenerateTestFloatHistogram(6))},
+ sample{0, 3, 0, nil, tsdbutil.SetFloatHistogramNotCounterReset(tsdbutil.GenerateTestFloatHistogram(3))},
+ sample{0, 6, 0, nil, tsdbutil.SetFloatHistogramNotCounterReset(tsdbutil.GenerateTestFloatHistogram(6))},
}),
},
expectedMinMaxTimes: []minMaxTimes{{3, 6}},
@@ -1212,23 +1216,23 @@ func TestPopulateWithTombSeriesIterators(t *testing.T) {
name: "one float histogram chunk intersect with later deletion interval",
samples: [][]chunks.Sample{
{
- sample{1, 0, nil, tsdbutil.GenerateTestFloatHistogram(1)},
- sample{2, 0, nil, tsdbutil.GenerateTestFloatHistogram(2)},
- sample{3, 0, nil, tsdbutil.GenerateTestFloatHistogram(3)},
- sample{6, 0, nil, tsdbutil.GenerateTestFloatHistogram(6)},
+ sample{0, 1, 0, nil, tsdbutil.GenerateTestFloatHistogram(1)},
+ sample{0, 2, 0, nil, tsdbutil.GenerateTestFloatHistogram(2)},
+ sample{0, 3, 0, nil, tsdbutil.GenerateTestFloatHistogram(3)},
+ sample{0, 6, 0, nil, tsdbutil.GenerateTestFloatHistogram(6)},
},
},
intervals: tombstones.Intervals{{Mint: 5, Maxt: 20}},
expected: []chunks.Sample{
- sample{1, 0, nil, tsdbutil.GenerateTestFloatHistogram(1)},
- sample{2, 0, nil, tsdbutil.SetFloatHistogramNotCounterReset(tsdbutil.GenerateTestFloatHistogram(2))},
- sample{3, 0, nil, tsdbutil.SetFloatHistogramNotCounterReset(tsdbutil.GenerateTestFloatHistogram(3))},
+ sample{0, 1, 0, nil, tsdbutil.GenerateTestFloatHistogram(1)},
+ sample{0, 2, 0, nil, tsdbutil.SetFloatHistogramNotCounterReset(tsdbutil.GenerateTestFloatHistogram(2))},
+ sample{0, 3, 0, nil, tsdbutil.SetFloatHistogramNotCounterReset(tsdbutil.GenerateTestFloatHistogram(3))},
},
expectedChks: []chunks.Meta{
assureChunkFromSamples(t, []chunks.Sample{
- sample{1, 0, nil, tsdbutil.GenerateTestFloatHistogram(1)},
- sample{2, 0, nil, tsdbutil.SetFloatHistogramNotCounterReset(tsdbutil.GenerateTestFloatHistogram(2))},
- sample{3, 0, nil, tsdbutil.SetFloatHistogramNotCounterReset(tsdbutil.GenerateTestFloatHistogram(3))},
+ sample{0, 1, 0, nil, tsdbutil.GenerateTestFloatHistogram(1)},
+ sample{0, 2, 0, nil, tsdbutil.SetFloatHistogramNotCounterReset(tsdbutil.GenerateTestFloatHistogram(2))},
+ sample{0, 3, 0, nil, tsdbutil.SetFloatHistogramNotCounterReset(tsdbutil.GenerateTestFloatHistogram(3))},
}),
},
expectedMinMaxTimes: []minMaxTimes{{1, 3}},
@@ -1237,24 +1241,24 @@ func TestPopulateWithTombSeriesIterators(t *testing.T) {
name: "one gauge histogram chunk",
samples: [][]chunks.Sample{
{
- sample{1, 0, tsdbutil.GenerateTestGaugeHistogram(1), nil},
- sample{2, 0, tsdbutil.GenerateTestGaugeHistogram(2), nil},
- sample{3, 0, tsdbutil.GenerateTestGaugeHistogram(3), nil},
- sample{6, 0, tsdbutil.GenerateTestGaugeHistogram(6), nil},
+ sample{0, 1, 0, tsdbutil.GenerateTestGaugeHistogram(1), nil},
+ sample{0, 2, 0, tsdbutil.GenerateTestGaugeHistogram(2), nil},
+ sample{0, 3, 0, tsdbutil.GenerateTestGaugeHistogram(3), nil},
+ sample{0, 6, 0, tsdbutil.GenerateTestGaugeHistogram(6), nil},
},
},
expected: []chunks.Sample{
- sample{1, 0, tsdbutil.GenerateTestGaugeHistogram(1), nil},
- sample{2, 0, tsdbutil.GenerateTestGaugeHistogram(2), nil},
- sample{3, 0, tsdbutil.GenerateTestGaugeHistogram(3), nil},
- sample{6, 0, tsdbutil.GenerateTestGaugeHistogram(6), nil},
+ sample{0, 1, 0, tsdbutil.GenerateTestGaugeHistogram(1), nil},
+ sample{0, 2, 0, tsdbutil.GenerateTestGaugeHistogram(2), nil},
+ sample{0, 3, 0, tsdbutil.GenerateTestGaugeHistogram(3), nil},
+ sample{0, 6, 0, tsdbutil.GenerateTestGaugeHistogram(6), nil},
},
expectedChks: []chunks.Meta{
assureChunkFromSamples(t, []chunks.Sample{
- sample{1, 0, tsdbutil.GenerateTestGaugeHistogram(1), nil},
- sample{2, 0, tsdbutil.GenerateTestGaugeHistogram(2), nil},
- sample{3, 0, tsdbutil.GenerateTestGaugeHistogram(3), nil},
- sample{6, 0, tsdbutil.GenerateTestGaugeHistogram(6), nil},
+ sample{0, 1, 0, tsdbutil.GenerateTestGaugeHistogram(1), nil},
+ sample{0, 2, 0, tsdbutil.GenerateTestGaugeHistogram(2), nil},
+ sample{0, 3, 0, tsdbutil.GenerateTestGaugeHistogram(3), nil},
+ sample{0, 6, 0, tsdbutil.GenerateTestGaugeHistogram(6), nil},
}),
},
expectedMinMaxTimes: []minMaxTimes{{1, 6}},
@@ -1263,21 +1267,21 @@ func TestPopulateWithTombSeriesIterators(t *testing.T) {
name: "one gauge histogram chunk intersect with earlier deletion interval",
samples: [][]chunks.Sample{
{
- sample{1, 0, tsdbutil.GenerateTestGaugeHistogram(1), nil},
- sample{2, 0, tsdbutil.GenerateTestGaugeHistogram(2), nil},
- sample{3, 0, tsdbutil.GenerateTestGaugeHistogram(3), nil},
- sample{6, 0, tsdbutil.GenerateTestGaugeHistogram(6), nil},
+ sample{0, 1, 0, tsdbutil.GenerateTestGaugeHistogram(1), nil},
+ sample{0, 2, 0, tsdbutil.GenerateTestGaugeHistogram(2), nil},
+ sample{0, 3, 0, tsdbutil.GenerateTestGaugeHistogram(3), nil},
+ sample{0, 6, 0, tsdbutil.GenerateTestGaugeHistogram(6), nil},
},
},
intervals: tombstones.Intervals{{Mint: 1, Maxt: 2}},
expected: []chunks.Sample{
- sample{3, 0, tsdbutil.GenerateTestGaugeHistogram(3), nil},
- sample{6, 0, tsdbutil.GenerateTestGaugeHistogram(6), nil},
+ sample{0, 3, 0, tsdbutil.GenerateTestGaugeHistogram(3), nil},
+ sample{0, 6, 0, tsdbutil.GenerateTestGaugeHistogram(6), nil},
},
expectedChks: []chunks.Meta{
assureChunkFromSamples(t, []chunks.Sample{
- sample{3, 0, tsdbutil.GenerateTestGaugeHistogram(3), nil},
- sample{6, 0, tsdbutil.GenerateTestGaugeHistogram(6), nil},
+ sample{0, 3, 0, tsdbutil.GenerateTestGaugeHistogram(3), nil},
+ sample{0, 6, 0, tsdbutil.GenerateTestGaugeHistogram(6), nil},
}),
},
expectedMinMaxTimes: []minMaxTimes{{3, 6}},
@@ -1286,23 +1290,23 @@ func TestPopulateWithTombSeriesIterators(t *testing.T) {
name: "one gauge histogram chunk intersect with later deletion interval",
samples: [][]chunks.Sample{
{
- sample{1, 0, tsdbutil.GenerateTestGaugeHistogram(1), nil},
- sample{2, 0, tsdbutil.GenerateTestGaugeHistogram(2), nil},
- sample{3, 0, tsdbutil.GenerateTestGaugeHistogram(3), nil},
- sample{6, 0, tsdbutil.GenerateTestGaugeHistogram(6), nil},
+ sample{0, 1, 0, tsdbutil.GenerateTestGaugeHistogram(1), nil},
+ sample{0, 2, 0, tsdbutil.GenerateTestGaugeHistogram(2), nil},
+ sample{0, 3, 0, tsdbutil.GenerateTestGaugeHistogram(3), nil},
+ sample{0, 6, 0, tsdbutil.GenerateTestGaugeHistogram(6), nil},
},
},
intervals: tombstones.Intervals{{Mint: 5, Maxt: 20}},
expected: []chunks.Sample{
- sample{1, 0, tsdbutil.GenerateTestGaugeHistogram(1), nil},
- sample{2, 0, tsdbutil.GenerateTestGaugeHistogram(2), nil},
- sample{3, 0, tsdbutil.GenerateTestGaugeHistogram(3), nil},
+ sample{0, 1, 0, tsdbutil.GenerateTestGaugeHistogram(1), nil},
+ sample{0, 2, 0, tsdbutil.GenerateTestGaugeHistogram(2), nil},
+ sample{0, 3, 0, tsdbutil.GenerateTestGaugeHistogram(3), nil},
},
expectedChks: []chunks.Meta{
assureChunkFromSamples(t, []chunks.Sample{
- sample{1, 0, tsdbutil.GenerateTestGaugeHistogram(1), nil},
- sample{2, 0, tsdbutil.GenerateTestGaugeHistogram(2), nil},
- sample{3, 0, tsdbutil.GenerateTestGaugeHistogram(3), nil},
+ sample{0, 1, 0, tsdbutil.GenerateTestGaugeHistogram(1), nil},
+ sample{0, 2, 0, tsdbutil.GenerateTestGaugeHistogram(2), nil},
+ sample{0, 3, 0, tsdbutil.GenerateTestGaugeHistogram(3), nil},
}),
},
expectedMinMaxTimes: []minMaxTimes{{1, 3}},
@@ -1311,24 +1315,24 @@ func TestPopulateWithTombSeriesIterators(t *testing.T) {
name: "one gauge float histogram",
samples: [][]chunks.Sample{
{
- sample{1, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(1)},
- sample{2, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(2)},
- sample{3, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(3)},
- sample{6, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(6)},
+ sample{0, 1, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(1)},
+ sample{0, 2, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(2)},
+ sample{0, 3, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(3)},
+ sample{0, 6, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(6)},
},
},
expected: []chunks.Sample{
- sample{1, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(1)},
- sample{2, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(2)},
- sample{3, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(3)},
- sample{6, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(6)},
+ sample{0, 1, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(1)},
+ sample{0, 2, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(2)},
+ sample{0, 3, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(3)},
+ sample{0, 6, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(6)},
},
expectedChks: []chunks.Meta{
assureChunkFromSamples(t, []chunks.Sample{
- sample{1, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(1)},
- sample{2, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(2)},
- sample{3, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(3)},
- sample{6, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(6)},
+ sample{0, 1, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(1)},
+ sample{0, 2, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(2)},
+ sample{0, 3, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(3)},
+ sample{0, 6, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(6)},
}),
},
expectedMinMaxTimes: []minMaxTimes{{1, 6}},
@@ -1337,21 +1341,21 @@ func TestPopulateWithTombSeriesIterators(t *testing.T) {
name: "one gauge float histogram chunk intersect with earlier deletion interval",
samples: [][]chunks.Sample{
{
- sample{1, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(1)},
- sample{2, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(2)},
- sample{3, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(3)},
- sample{6, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(6)},
+ sample{0, 1, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(1)},
+ sample{0, 2, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(2)},
+ sample{0, 3, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(3)},
+ sample{0, 6, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(6)},
},
},
intervals: tombstones.Intervals{{Mint: 1, Maxt: 2}},
expected: []chunks.Sample{
- sample{3, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(3)},
- sample{6, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(6)},
+ sample{0, 3, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(3)},
+ sample{0, 6, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(6)},
},
expectedChks: []chunks.Meta{
assureChunkFromSamples(t, []chunks.Sample{
- sample{3, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(3)},
- sample{6, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(6)},
+ sample{0, 3, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(3)},
+ sample{0, 6, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(6)},
}),
},
expectedMinMaxTimes: []minMaxTimes{{3, 6}},
@@ -1360,23 +1364,23 @@ func TestPopulateWithTombSeriesIterators(t *testing.T) {
name: "one gauge float histogram chunk intersect with later deletion interval",
samples: [][]chunks.Sample{
{
- sample{1, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(1)},
- sample{2, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(2)},
- sample{3, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(3)},
- sample{6, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(6)},
+ sample{0, 1, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(1)},
+ sample{0, 2, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(2)},
+ sample{0, 3, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(3)},
+ sample{0, 6, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(6)},
},
},
intervals: tombstones.Intervals{{Mint: 5, Maxt: 20}},
expected: []chunks.Sample{
- sample{1, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(1)},
- sample{2, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(2)},
- sample{3, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(3)},
+ sample{0, 1, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(1)},
+ sample{0, 2, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(2)},
+ sample{0, 3, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(3)},
},
expectedChks: []chunks.Meta{
assureChunkFromSamples(t, []chunks.Sample{
- sample{1, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(1)},
- sample{2, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(2)},
- sample{3, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(3)},
+ sample{0, 1, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(1)},
+ sample{0, 2, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(2)},
+ sample{0, 3, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(3)},
}),
},
expectedMinMaxTimes: []minMaxTimes{{1, 3}},
@@ -1384,31 +1388,31 @@ func TestPopulateWithTombSeriesIterators(t *testing.T) {
{
name: "three full mixed chunks",
samples: [][]chunks.Sample{
- {sample{1, 2, nil, nil}, sample{2, 3, nil, nil}, sample{3, 5, nil, nil}, sample{6, 1, nil, nil}},
+ {sample{0, 1, 2, nil, nil}, sample{0, 2, 3, nil, nil}, sample{0, 3, 5, nil, nil}, sample{0, 6, 1, nil, nil}},
{
- sample{7, 0, tsdbutil.GenerateTestGaugeHistogram(89), nil},
- sample{9, 0, tsdbutil.GenerateTestGaugeHistogram(8), nil},
+ sample{0, 7, 0, tsdbutil.GenerateTestGaugeHistogram(89), nil},
+ sample{0, 9, 0, tsdbutil.GenerateTestGaugeHistogram(8), nil},
},
{
- sample{10, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(22)},
- sample{203, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(3493)},
+ sample{0, 10, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(22)},
+ sample{0, 203, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(3493)},
},
},
expected: []chunks.Sample{
- sample{1, 2, nil, nil}, sample{2, 3, nil, nil}, sample{3, 5, nil, nil}, sample{6, 1, nil, nil}, sample{7, 0, tsdbutil.GenerateTestGaugeHistogram(89), nil}, sample{9, 0, tsdbutil.GenerateTestGaugeHistogram(8), nil}, sample{10, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(22)}, sample{203, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(3493)},
+ sample{0, 1, 2, nil, nil}, sample{0, 2, 3, nil, nil}, sample{0, 3, 5, nil, nil}, sample{0, 6, 1, nil, nil}, sample{0, 7, 0, tsdbutil.GenerateTestGaugeHistogram(89), nil}, sample{0, 9, 0, tsdbutil.GenerateTestGaugeHistogram(8), nil}, sample{0, 10, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(22)}, sample{0, 203, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(3493)},
},
expectedChks: []chunks.Meta{
assureChunkFromSamples(t, []chunks.Sample{
- sample{1, 2, nil, nil}, sample{2, 3, nil, nil}, sample{3, 5, nil, nil}, sample{6, 1, nil, nil},
+ sample{0, 1, 2, nil, nil}, sample{0, 2, 3, nil, nil}, sample{0, 3, 5, nil, nil}, sample{0, 6, 1, nil, nil},
}),
assureChunkFromSamples(t, []chunks.Sample{
- sample{7, 0, tsdbutil.GenerateTestGaugeHistogram(89), nil},
- sample{9, 0, tsdbutil.GenerateTestGaugeHistogram(8), nil},
+ sample{0, 7, 0, tsdbutil.GenerateTestGaugeHistogram(89), nil},
+ sample{0, 9, 0, tsdbutil.GenerateTestGaugeHistogram(8), nil},
}),
assureChunkFromSamples(t, []chunks.Sample{
- sample{10, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(22)},
- sample{203, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(3493)},
+ sample{0, 10, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(22)},
+ sample{0, 203, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(3493)},
}),
},
expectedMinMaxTimes: []minMaxTimes{{1, 6}, {7, 9}, {10, 203}},
@@ -1417,30 +1421,30 @@ func TestPopulateWithTombSeriesIterators(t *testing.T) {
name: "three full mixed chunks in different order",
samples: [][]chunks.Sample{
{
- sample{7, 0, tsdbutil.GenerateTestGaugeHistogram(89), nil},
- sample{9, 0, tsdbutil.GenerateTestGaugeHistogram(8), nil},
+ sample{0, 7, 0, tsdbutil.GenerateTestGaugeHistogram(89), nil},
+ sample{0, 9, 0, tsdbutil.GenerateTestGaugeHistogram(8), nil},
},
- {sample{11, 2, nil, nil}, sample{12, 3, nil, nil}, sample{13, 5, nil, nil}, sample{16, 1, nil, nil}},
+ {sample{0, 11, 2, nil, nil}, sample{0, 12, 3, nil, nil}, sample{0, 13, 5, nil, nil}, sample{0, 16, 1, nil, nil}},
{
- sample{100, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(22)},
- sample{203, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(3493)},
+ sample{0, 100, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(22)},
+ sample{0, 203, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(3493)},
},
},
expected: []chunks.Sample{
- sample{7, 0, tsdbutil.GenerateTestGaugeHistogram(89), nil}, sample{9, 0, tsdbutil.GenerateTestGaugeHistogram(8), nil}, sample{11, 2, nil, nil}, sample{12, 3, nil, nil}, sample{13, 5, nil, nil}, sample{16, 1, nil, nil}, sample{100, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(22)}, sample{203, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(3493)},
+ sample{0, 7, 0, tsdbutil.GenerateTestGaugeHistogram(89), nil}, sample{0, 9, 0, tsdbutil.GenerateTestGaugeHistogram(8), nil}, sample{0, 11, 2, nil, nil}, sample{0, 12, 3, nil, nil}, sample{0, 13, 5, nil, nil}, sample{0, 16, 1, nil, nil}, sample{0, 100, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(22)}, sample{0, 203, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(3493)},
},
expectedChks: []chunks.Meta{
assureChunkFromSamples(t, []chunks.Sample{
- sample{7, 0, tsdbutil.GenerateTestGaugeHistogram(89), nil},
- sample{9, 0, tsdbutil.GenerateTestGaugeHistogram(8), nil},
+ sample{0, 7, 0, tsdbutil.GenerateTestGaugeHistogram(89), nil},
+ sample{0, 9, 0, tsdbutil.GenerateTestGaugeHistogram(8), nil},
}),
assureChunkFromSamples(t, []chunks.Sample{
- sample{11, 2, nil, nil}, sample{12, 3, nil, nil}, sample{13, 5, nil, nil}, sample{16, 1, nil, nil},
+ sample{0, 11, 2, nil, nil}, sample{0, 12, 3, nil, nil}, sample{0, 13, 5, nil, nil}, sample{0, 16, 1, nil, nil},
}),
assureChunkFromSamples(t, []chunks.Sample{
- sample{100, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(22)},
- sample{203, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(3493)},
+ sample{0, 100, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(22)},
+ sample{0, 203, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(3493)},
}),
},
expectedMinMaxTimes: []minMaxTimes{{7, 9}, {11, 16}, {100, 203}},
@@ -1449,29 +1453,29 @@ func TestPopulateWithTombSeriesIterators(t *testing.T) {
name: "three full mixed chunks in different order intersect with deletion interval",
samples: [][]chunks.Sample{
{
- sample{7, 0, tsdbutil.GenerateTestGaugeHistogram(89), nil},
- sample{9, 0, tsdbutil.GenerateTestGaugeHistogram(8), nil},
+ sample{0, 7, 0, tsdbutil.GenerateTestGaugeHistogram(89), nil},
+ sample{0, 9, 0, tsdbutil.GenerateTestGaugeHistogram(8), nil},
},
- {sample{11, 2, nil, nil}, sample{12, 3, nil, nil}, sample{13, 5, nil, nil}, sample{16, 1, nil, nil}},
+ {sample{0, 11, 2, nil, nil}, sample{0, 12, 3, nil, nil}, sample{0, 13, 5, nil, nil}, sample{0, 16, 1, nil, nil}},
{
- sample{100, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(22)},
- sample{203, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(3493)},
+ sample{0, 100, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(22)},
+ sample{0, 203, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(3493)},
},
},
intervals: tombstones.Intervals{{Mint: 8, Maxt: 11}, {Mint: 15, Maxt: 150}},
expected: []chunks.Sample{
- sample{7, 0, tsdbutil.GenerateTestGaugeHistogram(89), nil}, sample{12, 3, nil, nil}, sample{13, 5, nil, nil}, sample{203, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(3493)},
+ sample{0, 7, 0, tsdbutil.GenerateTestGaugeHistogram(89), nil}, sample{0, 12, 3, nil, nil}, sample{0, 13, 5, nil, nil}, sample{0, 203, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(3493)},
},
expectedChks: []chunks.Meta{
assureChunkFromSamples(t, []chunks.Sample{
- sample{7, 0, tsdbutil.GenerateTestGaugeHistogram(89), nil},
+ sample{0, 7, 0, tsdbutil.GenerateTestGaugeHistogram(89), nil},
}),
assureChunkFromSamples(t, []chunks.Sample{
- sample{12, 3, nil, nil}, sample{13, 5, nil, nil},
+ sample{0, 12, 3, nil, nil}, sample{0, 13, 5, nil, nil},
}),
assureChunkFromSamples(t, []chunks.Sample{
- sample{203, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(3493)},
+ sample{0, 203, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(3493)},
}),
},
expectedMinMaxTimes: []minMaxTimes{{7, 7}, {12, 13}, {203, 203}},
@@ -1480,30 +1484,30 @@ func TestPopulateWithTombSeriesIterators(t *testing.T) {
name: "three full mixed chunks overlapping",
samples: [][]chunks.Sample{
{
- sample{7, 0, tsdbutil.GenerateTestGaugeHistogram(89), nil},
- sample{12, 0, tsdbutil.GenerateTestGaugeHistogram(8), nil},
+ sample{0, 7, 0, tsdbutil.GenerateTestGaugeHistogram(89), nil},
+ sample{0, 12, 0, tsdbutil.GenerateTestGaugeHistogram(8), nil},
},
- {sample{11, 2, nil, nil}, sample{12, 3, nil, nil}, sample{13, 5, nil, nil}, sample{16, 1, nil, nil}},
+ {sample{0, 11, 2, nil, nil}, sample{0, 12, 3, nil, nil}, sample{0, 13, 5, nil, nil}, sample{0, 16, 1, nil, nil}},
{
- sample{10, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(22)},
- sample{203, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(3493)},
+ sample{0, 10, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(22)},
+ sample{0, 203, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(3493)},
},
},
expected: []chunks.Sample{
- sample{7, 0, tsdbutil.GenerateTestGaugeHistogram(89), nil}, sample{12, 0, tsdbutil.GenerateTestGaugeHistogram(8), nil}, sample{11, 2, nil, nil}, sample{12, 3, nil, nil}, sample{13, 5, nil, nil}, sample{16, 1, nil, nil}, sample{10, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(22)}, sample{203, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(3493)},
+ sample{0, 7, 0, tsdbutil.GenerateTestGaugeHistogram(89), nil}, sample{0, 12, 0, tsdbutil.GenerateTestGaugeHistogram(8), nil}, sample{0, 11, 2, nil, nil}, sample{0, 12, 3, nil, nil}, sample{0, 13, 5, nil, nil}, sample{0, 16, 1, nil, nil}, sample{0, 10, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(22)}, sample{0, 203, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(3493)},
},
expectedChks: []chunks.Meta{
assureChunkFromSamples(t, []chunks.Sample{
- sample{7, 0, tsdbutil.GenerateTestGaugeHistogram(89), nil},
- sample{12, 0, tsdbutil.GenerateTestGaugeHistogram(8), nil},
+ sample{0, 7, 0, tsdbutil.GenerateTestGaugeHistogram(89), nil},
+ sample{0, 12, 0, tsdbutil.GenerateTestGaugeHistogram(8), nil},
}),
assureChunkFromSamples(t, []chunks.Sample{
- sample{11, 2, nil, nil}, sample{12, 3, nil, nil}, sample{13, 5, nil, nil}, sample{16, 1, nil, nil},
+ sample{0, 11, 2, nil, nil}, sample{0, 12, 3, nil, nil}, sample{0, 13, 5, nil, nil}, sample{0, 16, 1, nil, nil},
}),
assureChunkFromSamples(t, []chunks.Sample{
- sample{10, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(22)},
- sample{203, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(3493)},
+ sample{0, 10, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(22)},
+ sample{0, 203, 0, nil, tsdbutil.GenerateTestGaugeFloatHistogram(3493)},
}),
},
expectedMinMaxTimes: []minMaxTimes{{7, 12}, {11, 16}, {10, 203}},
@@ -1512,56 +1516,56 @@ func TestPopulateWithTombSeriesIterators(t *testing.T) {
name: "int histogram iterables with counter resets",
samples: [][]chunks.Sample{
{
- sample{7, 0, tsdbutil.GenerateTestHistogram(8), nil},
- sample{8, 0, tsdbutil.GenerateTestHistogram(9), nil},
+ sample{0, 7, 0, tsdbutil.GenerateTestHistogram(8), nil},
+ sample{0, 8, 0, tsdbutil.GenerateTestHistogram(9), nil},
// Counter reset should be detected when chunks are created from the iterable.
- sample{12, 0, tsdbutil.GenerateTestHistogram(5), nil},
- sample{15, 0, tsdbutil.GenerateTestHistogram(6), nil},
- sample{16, 0, tsdbutil.GenerateTestHistogram(7), nil},
+ sample{0, 12, 0, tsdbutil.GenerateTestHistogram(5), nil},
+ sample{0, 15, 0, tsdbutil.GenerateTestHistogram(6), nil},
+ sample{0, 16, 0, tsdbutil.GenerateTestHistogram(7), nil},
// Counter reset should be detected when chunks are created from the iterable.
- sample{17, 0, tsdbutil.GenerateTestHistogram(5), nil},
+ sample{0, 17, 0, tsdbutil.GenerateTestHistogram(5), nil},
},
{
- sample{18, 0, tsdbutil.GenerateTestHistogram(6), nil},
- sample{19, 0, tsdbutil.GenerateTestHistogram(7), nil},
+ sample{0, 18, 0, tsdbutil.GenerateTestHistogram(6), nil},
+ sample{0, 19, 0, tsdbutil.GenerateTestHistogram(7), nil},
// Counter reset should be detected when chunks are created from the iterable.
- sample{20, 0, tsdbutil.GenerateTestHistogram(5), nil},
- sample{21, 0, tsdbutil.GenerateTestHistogram(6), nil},
+ sample{0, 20, 0, tsdbutil.GenerateTestHistogram(5), nil},
+ sample{0, 21, 0, tsdbutil.GenerateTestHistogram(6), nil},
},
},
expected: []chunks.Sample{
- sample{7, 0, tsdbutil.GenerateTestHistogram(8), nil},
- sample{8, 0, tsdbutil.GenerateTestHistogram(9), nil},
- sample{12, 0, tsdbutil.GenerateTestHistogram(5), nil},
- sample{15, 0, tsdbutil.GenerateTestHistogram(6), nil},
- sample{16, 0, tsdbutil.GenerateTestHistogram(7), nil},
- sample{17, 0, tsdbutil.GenerateTestHistogram(5), nil},
- sample{18, 0, tsdbutil.GenerateTestHistogram(6), nil},
- sample{19, 0, tsdbutil.GenerateTestHistogram(7), nil},
- sample{20, 0, tsdbutil.GenerateTestHistogram(5), nil},
- sample{21, 0, tsdbutil.GenerateTestHistogram(6), nil},
+ sample{0, 7, 0, tsdbutil.GenerateTestHistogram(8), nil},
+ sample{0, 8, 0, tsdbutil.GenerateTestHistogram(9), nil},
+ sample{0, 12, 0, tsdbutil.GenerateTestHistogram(5), nil},
+ sample{0, 15, 0, tsdbutil.GenerateTestHistogram(6), nil},
+ sample{0, 16, 0, tsdbutil.GenerateTestHistogram(7), nil},
+ sample{0, 17, 0, tsdbutil.GenerateTestHistogram(5), nil},
+ sample{0, 18, 0, tsdbutil.GenerateTestHistogram(6), nil},
+ sample{0, 19, 0, tsdbutil.GenerateTestHistogram(7), nil},
+ sample{0, 20, 0, tsdbutil.GenerateTestHistogram(5), nil},
+ sample{0, 21, 0, tsdbutil.GenerateTestHistogram(6), nil},
},
expectedChks: []chunks.Meta{
assureChunkFromSamples(t, []chunks.Sample{
- sample{7, 0, tsdbutil.GenerateTestHistogram(8), nil},
- sample{8, 0, tsdbutil.SetHistogramNotCounterReset(tsdbutil.GenerateTestHistogram(9)), nil},
+ sample{0, 7, 0, tsdbutil.GenerateTestHistogram(8), nil},
+ sample{0, 8, 0, tsdbutil.SetHistogramNotCounterReset(tsdbutil.GenerateTestHistogram(9)), nil},
}),
assureChunkFromSamples(t, []chunks.Sample{
- sample{12, 0, tsdbutil.SetHistogramCounterReset(tsdbutil.GenerateTestHistogram(5)), nil},
- sample{15, 0, tsdbutil.SetHistogramNotCounterReset(tsdbutil.GenerateTestHistogram(6)), nil},
- sample{16, 0, tsdbutil.SetHistogramNotCounterReset(tsdbutil.GenerateTestHistogram(7)), nil},
+ sample{0, 12, 0, tsdbutil.SetHistogramCounterReset(tsdbutil.GenerateTestHistogram(5)), nil},
+ sample{0, 15, 0, tsdbutil.SetHistogramNotCounterReset(tsdbutil.GenerateTestHistogram(6)), nil},
+ sample{0, 16, 0, tsdbutil.SetHistogramNotCounterReset(tsdbutil.GenerateTestHistogram(7)), nil},
}),
assureChunkFromSamples(t, []chunks.Sample{
- sample{17, 0, tsdbutil.SetHistogramCounterReset(tsdbutil.GenerateTestHistogram(5)), nil},
+ sample{0, 17, 0, tsdbutil.SetHistogramCounterReset(tsdbutil.GenerateTestHistogram(5)), nil},
}),
assureChunkFromSamples(t, []chunks.Sample{
- sample{18, 0, tsdbutil.GenerateTestHistogram(6), nil},
- sample{19, 0, tsdbutil.SetHistogramNotCounterReset(tsdbutil.GenerateTestHistogram(7)), nil},
+ sample{0, 18, 0, tsdbutil.GenerateTestHistogram(6), nil},
+ sample{0, 19, 0, tsdbutil.SetHistogramNotCounterReset(tsdbutil.GenerateTestHistogram(7)), nil},
}),
assureChunkFromSamples(t, []chunks.Sample{
- sample{20, 0, tsdbutil.SetHistogramCounterReset(tsdbutil.GenerateTestHistogram(5)), nil},
- sample{21, 0, tsdbutil.SetHistogramNotCounterReset(tsdbutil.GenerateTestHistogram(6)), nil},
+ sample{0, 20, 0, tsdbutil.SetHistogramCounterReset(tsdbutil.GenerateTestHistogram(5)), nil},
+ sample{0, 21, 0, tsdbutil.SetHistogramNotCounterReset(tsdbutil.GenerateTestHistogram(6)), nil},
}),
},
expectedMinMaxTimes: []minMaxTimes{
@@ -1581,56 +1585,56 @@ func TestPopulateWithTombSeriesIterators(t *testing.T) {
name: "float histogram iterables with counter resets",
samples: [][]chunks.Sample{
{
- sample{7, 0, nil, tsdbutil.GenerateTestFloatHistogram(8)},
- sample{8, 0, nil, tsdbutil.GenerateTestFloatHistogram(9)},
+ sample{0, 7, 0, nil, tsdbutil.GenerateTestFloatHistogram(8)},
+ sample{0, 8, 0, nil, tsdbutil.GenerateTestFloatHistogram(9)},
// Counter reset should be detected when chunks are created from the iterable.
- sample{12, 0, nil, tsdbutil.GenerateTestFloatHistogram(5)},
- sample{15, 0, nil, tsdbutil.GenerateTestFloatHistogram(6)},
- sample{16, 0, nil, tsdbutil.GenerateTestFloatHistogram(7)},
+ sample{0, 12, 0, nil, tsdbutil.GenerateTestFloatHistogram(5)},
+ sample{0, 15, 0, nil, tsdbutil.GenerateTestFloatHistogram(6)},
+ sample{0, 16, 0, nil, tsdbutil.GenerateTestFloatHistogram(7)},
// Counter reset should be detected when chunks are created from the iterable.
- sample{17, 0, nil, tsdbutil.GenerateTestFloatHistogram(5)},
+ sample{0, 17, 0, nil, tsdbutil.GenerateTestFloatHistogram(5)},
},
{
- sample{18, 0, nil, tsdbutil.GenerateTestFloatHistogram(6)},
- sample{19, 0, nil, tsdbutil.GenerateTestFloatHistogram(7)},
+ sample{0, 18, 0, nil, tsdbutil.GenerateTestFloatHistogram(6)},
+ sample{0, 19, 0, nil, tsdbutil.GenerateTestFloatHistogram(7)},
// Counter reset should be detected when chunks are created from the iterable.
- sample{20, 0, nil, tsdbutil.GenerateTestFloatHistogram(5)},
- sample{21, 0, nil, tsdbutil.GenerateTestFloatHistogram(6)},
+ sample{0, 20, 0, nil, tsdbutil.GenerateTestFloatHistogram(5)},
+ sample{0, 21, 0, nil, tsdbutil.GenerateTestFloatHistogram(6)},
},
},
expected: []chunks.Sample{
- sample{7, 0, nil, tsdbutil.GenerateTestFloatHistogram(8)},
- sample{8, 0, nil, tsdbutil.GenerateTestFloatHistogram(9)},
- sample{12, 0, nil, tsdbutil.GenerateTestFloatHistogram(5)},
- sample{15, 0, nil, tsdbutil.GenerateTestFloatHistogram(6)},
- sample{16, 0, nil, tsdbutil.GenerateTestFloatHistogram(7)},
- sample{17, 0, nil, tsdbutil.GenerateTestFloatHistogram(5)},
- sample{18, 0, nil, tsdbutil.GenerateTestFloatHistogram(6)},
- sample{19, 0, nil, tsdbutil.GenerateTestFloatHistogram(7)},
- sample{20, 0, nil, tsdbutil.GenerateTestFloatHistogram(5)},
- sample{21, 0, nil, tsdbutil.GenerateTestFloatHistogram(6)},
+ sample{0, 7, 0, nil, tsdbutil.GenerateTestFloatHistogram(8)},
+ sample{0, 8, 0, nil, tsdbutil.GenerateTestFloatHistogram(9)},
+ sample{0, 12, 0, nil, tsdbutil.GenerateTestFloatHistogram(5)},
+ sample{0, 15, 0, nil, tsdbutil.GenerateTestFloatHistogram(6)},
+ sample{0, 16, 0, nil, tsdbutil.GenerateTestFloatHistogram(7)},
+ sample{0, 17, 0, nil, tsdbutil.GenerateTestFloatHistogram(5)},
+ sample{0, 18, 0, nil, tsdbutil.GenerateTestFloatHistogram(6)},
+ sample{0, 19, 0, nil, tsdbutil.GenerateTestFloatHistogram(7)},
+ sample{0, 20, 0, nil, tsdbutil.GenerateTestFloatHistogram(5)},
+ sample{0, 21, 0, nil, tsdbutil.GenerateTestFloatHistogram(6)},
},
expectedChks: []chunks.Meta{
assureChunkFromSamples(t, []chunks.Sample{
- sample{7, 0, nil, tsdbutil.GenerateTestFloatHistogram(8)},
- sample{8, 0, nil, tsdbutil.SetFloatHistogramNotCounterReset(tsdbutil.GenerateTestFloatHistogram(9))},
+ sample{0, 7, 0, nil, tsdbutil.GenerateTestFloatHistogram(8)},
+ sample{0, 8, 0, nil, tsdbutil.SetFloatHistogramNotCounterReset(tsdbutil.GenerateTestFloatHistogram(9))},
}),
assureChunkFromSamples(t, []chunks.Sample{
- sample{12, 0, nil, tsdbutil.SetFloatHistogramCounterReset(tsdbutil.GenerateTestFloatHistogram(5))},
- sample{15, 0, nil, tsdbutil.SetFloatHistogramNotCounterReset(tsdbutil.GenerateTestFloatHistogram(6))},
- sample{16, 0, nil, tsdbutil.SetFloatHistogramNotCounterReset(tsdbutil.GenerateTestFloatHistogram(7))},
+ sample{0, 12, 0, nil, tsdbutil.SetFloatHistogramCounterReset(tsdbutil.GenerateTestFloatHistogram(5))},
+ sample{0, 15, 0, nil, tsdbutil.SetFloatHistogramNotCounterReset(tsdbutil.GenerateTestFloatHistogram(6))},
+ sample{0, 16, 0, nil, tsdbutil.SetFloatHistogramNotCounterReset(tsdbutil.GenerateTestFloatHistogram(7))},
}),
assureChunkFromSamples(t, []chunks.Sample{
- sample{17, 0, nil, tsdbutil.SetFloatHistogramCounterReset(tsdbutil.GenerateTestFloatHistogram(5))},
+ sample{0, 17, 0, nil, tsdbutil.SetFloatHistogramCounterReset(tsdbutil.GenerateTestFloatHistogram(5))},
}),
assureChunkFromSamples(t, []chunks.Sample{
- sample{18, 0, nil, tsdbutil.GenerateTestFloatHistogram(6)},
- sample{19, 0, nil, tsdbutil.SetFloatHistogramNotCounterReset(tsdbutil.GenerateTestFloatHistogram(7))},
+ sample{0, 18, 0, nil, tsdbutil.GenerateTestFloatHistogram(6)},
+ sample{0, 19, 0, nil, tsdbutil.SetFloatHistogramNotCounterReset(tsdbutil.GenerateTestFloatHistogram(7))},
}),
assureChunkFromSamples(t, []chunks.Sample{
- sample{20, 0, nil, tsdbutil.SetFloatHistogramCounterReset(tsdbutil.GenerateTestFloatHistogram(5))},
- sample{21, 0, nil, tsdbutil.SetFloatHistogramNotCounterReset(tsdbutil.GenerateTestFloatHistogram(6))},
+ sample{0, 20, 0, nil, tsdbutil.SetFloatHistogramCounterReset(tsdbutil.GenerateTestFloatHistogram(5))},
+ sample{0, 21, 0, nil, tsdbutil.SetFloatHistogramNotCounterReset(tsdbutil.GenerateTestFloatHistogram(6))},
}),
},
expectedMinMaxTimes: []minMaxTimes{
@@ -1650,61 +1654,61 @@ func TestPopulateWithTombSeriesIterators(t *testing.T) {
name: "iterables with mixed encodings and counter resets",
samples: [][]chunks.Sample{
{
- sample{7, 0, tsdbutil.GenerateTestHistogram(8), nil},
- sample{8, 0, tsdbutil.GenerateTestHistogram(9), nil},
- sample{9, 0, nil, tsdbutil.GenerateTestFloatHistogram(10)},
- sample{10, 0, nil, tsdbutil.GenerateTestFloatHistogram(11)},
- sample{11, 0, nil, tsdbutil.GenerateTestFloatHistogram(12)},
- sample{12, 13, nil, nil},
- sample{13, 14, nil, nil},
- sample{14, 0, tsdbutil.GenerateTestHistogram(8), nil},
+ sample{0, 7, 0, tsdbutil.GenerateTestHistogram(8), nil},
+ sample{0, 8, 0, tsdbutil.GenerateTestHistogram(9), nil},
+ sample{0, 9, 0, nil, tsdbutil.GenerateTestFloatHistogram(10)},
+ sample{0, 10, 0, nil, tsdbutil.GenerateTestFloatHistogram(11)},
+ sample{0, 11, 0, nil, tsdbutil.GenerateTestFloatHistogram(12)},
+ sample{0, 12, 13, nil, nil},
+ sample{0, 13, 14, nil, nil},
+ sample{0, 14, 0, tsdbutil.GenerateTestHistogram(8), nil},
// Counter reset should be detected when chunks are created from the iterable.
- sample{15, 0, tsdbutil.GenerateTestHistogram(7), nil},
+ sample{0, 15, 0, tsdbutil.GenerateTestHistogram(7), nil},
},
{
- sample{18, 0, tsdbutil.GenerateTestHistogram(6), nil},
- sample{19, 45, nil, nil},
+ sample{0, 18, 0, tsdbutil.GenerateTestHistogram(6), nil},
+ sample{0, 19, 45, nil, nil},
},
},
expected: []chunks.Sample{
- sample{7, 0, tsdbutil.GenerateTestHistogram(8), nil},
- sample{8, 0, tsdbutil.GenerateTestHistogram(9), nil},
- sample{9, 0, nil, tsdbutil.GenerateTestFloatHistogram(10)},
- sample{10, 0, nil, tsdbutil.GenerateTestFloatHistogram(11)},
- sample{11, 0, nil, tsdbutil.GenerateTestFloatHistogram(12)},
- sample{12, 13, nil, nil},
- sample{13, 14, nil, nil},
- sample{14, 0, tsdbutil.GenerateTestHistogram(8), nil},
- sample{15, 0, tsdbutil.GenerateTestHistogram(7), nil},
- sample{18, 0, tsdbutil.GenerateTestHistogram(6), nil},
- sample{19, 45, nil, nil},
+ sample{0, 7, 0, tsdbutil.GenerateTestHistogram(8), nil},
+ sample{0, 8, 0, tsdbutil.GenerateTestHistogram(9), nil},
+ sample{0, 9, 0, nil, tsdbutil.GenerateTestFloatHistogram(10)},
+ sample{0, 10, 0, nil, tsdbutil.GenerateTestFloatHistogram(11)},
+ sample{0, 11, 0, nil, tsdbutil.GenerateTestFloatHistogram(12)},
+ sample{0, 12, 13, nil, nil},
+ sample{0, 13, 14, nil, nil},
+ sample{0, 14, 0, tsdbutil.GenerateTestHistogram(8), nil},
+ sample{0, 15, 0, tsdbutil.GenerateTestHistogram(7), nil},
+ sample{0, 18, 0, tsdbutil.GenerateTestHistogram(6), nil},
+ sample{0, 19, 45, nil, nil},
},
expectedChks: []chunks.Meta{
assureChunkFromSamples(t, []chunks.Sample{
- sample{7, 0, tsdbutil.GenerateTestHistogram(8), nil},
- sample{8, 0, tsdbutil.GenerateTestHistogram(9), nil},
+ sample{0, 7, 0, tsdbutil.GenerateTestHistogram(8), nil},
+ sample{0, 8, 0, tsdbutil.GenerateTestHistogram(9), nil},
}),
assureChunkFromSamples(t, []chunks.Sample{
- sample{9, 0, nil, tsdbutil.GenerateTestFloatHistogram(10)},
- sample{10, 0, nil, tsdbutil.GenerateTestFloatHistogram(11)},
- sample{11, 0, nil, tsdbutil.GenerateTestFloatHistogram(12)},
+ sample{0, 9, 0, nil, tsdbutil.GenerateTestFloatHistogram(10)},
+ sample{0, 10, 0, nil, tsdbutil.GenerateTestFloatHistogram(11)},
+ sample{0, 11, 0, nil, tsdbutil.GenerateTestFloatHistogram(12)},
}),
assureChunkFromSamples(t, []chunks.Sample{
- sample{12, 13, nil, nil},
- sample{13, 14, nil, nil},
+ sample{0, 12, 13, nil, nil},
+ sample{0, 13, 14, nil, nil},
}),
assureChunkFromSamples(t, []chunks.Sample{
- sample{14, 0, tsdbutil.GenerateTestHistogram(8), nil},
+ sample{0, 14, 0, tsdbutil.GenerateTestHistogram(8), nil},
}),
assureChunkFromSamples(t, []chunks.Sample{
- sample{15, 0, tsdbutil.SetHistogramCounterReset(tsdbutil.GenerateTestHistogram(7)), nil},
+ sample{0, 15, 0, tsdbutil.SetHistogramCounterReset(tsdbutil.GenerateTestHistogram(7)), nil},
}),
assureChunkFromSamples(t, []chunks.Sample{
- sample{18, 0, tsdbutil.GenerateTestHistogram(6), nil},
+ sample{0, 18, 0, tsdbutil.GenerateTestHistogram(6), nil},
}),
assureChunkFromSamples(t, []chunks.Sample{
- sample{19, 45, nil, nil},
+ sample{0, 19, 45, nil, nil},
}),
},
expectedMinMaxTimes: []minMaxTimes{
@@ -1845,8 +1849,8 @@ func TestPopulateWithDelSeriesIterator_DoubleSeek(t *testing.T) {
valType: chunkenc.ValFloat,
chks: [][]chunks.Sample{
{},
- {sample{1, 1, nil, nil}, sample{2, 2, nil, nil}, sample{3, 3, nil, nil}},
- {sample{4, 4, nil, nil}, sample{5, 5, nil, nil}},
+ {sample{0, 1, 1, nil, nil}, sample{0, 2, 2, nil, nil}, sample{0, 3, 3, nil, nil}},
+ {sample{0, 4, 4, nil, nil}, sample{0, 5, 5, nil, nil}},
},
},
{
@@ -1854,8 +1858,8 @@ func TestPopulateWithDelSeriesIterator_DoubleSeek(t *testing.T) {
valType: chunkenc.ValHistogram,
chks: [][]chunks.Sample{
{},
- {sample{1, 0, tsdbutil.GenerateTestHistogram(1), nil}, sample{2, 0, tsdbutil.GenerateTestHistogram(2), nil}, sample{3, 0, tsdbutil.GenerateTestHistogram(3), nil}},
- {sample{4, 0, tsdbutil.GenerateTestHistogram(4), nil}, sample{5, 0, tsdbutil.GenerateTestHistogram(5), nil}},
+ {sample{0, 1, 0, tsdbutil.GenerateTestHistogram(1), nil}, sample{0, 2, 0, tsdbutil.GenerateTestHistogram(2), nil}, sample{0, 3, 0, tsdbutil.GenerateTestHistogram(3), nil}},
+ {sample{0, 4, 0, tsdbutil.GenerateTestHistogram(4), nil}, sample{0, 5, 0, tsdbutil.GenerateTestHistogram(5), nil}},
},
},
{
@@ -1863,8 +1867,8 @@ func TestPopulateWithDelSeriesIterator_DoubleSeek(t *testing.T) {
valType: chunkenc.ValFloatHistogram,
chks: [][]chunks.Sample{
{},
- {sample{1, 0, nil, tsdbutil.GenerateTestFloatHistogram(1)}, sample{2, 0, nil, tsdbutil.GenerateTestFloatHistogram(2)}, sample{3, 0, nil, tsdbutil.GenerateTestFloatHistogram(3)}},
- {sample{4, 0, nil, tsdbutil.GenerateTestFloatHistogram(4)}, sample{5, 0, nil, tsdbutil.GenerateTestFloatHistogram(5)}},
+ {sample{0, 1, 0, nil, tsdbutil.GenerateTestFloatHistogram(1)}, sample{0, 2, 0, nil, tsdbutil.GenerateTestFloatHistogram(2)}, sample{0, 3, 0, nil, tsdbutil.GenerateTestFloatHistogram(3)}},
+ {sample{0, 4, 0, nil, tsdbutil.GenerateTestFloatHistogram(4)}, sample{0, 5, 0, nil, tsdbutil.GenerateTestFloatHistogram(5)}},
},
},
}
@@ -1898,7 +1902,7 @@ func TestPopulateWithDelSeriesIterator_SeekInCurrentChunk(t *testing.T) {
valType: chunkenc.ValFloat,
chks: [][]chunks.Sample{
{},
- {sample{1, 2, nil, nil}, sample{3, 4, nil, nil}, sample{5, 6, nil, nil}, sample{7, 8, nil, nil}},
+ {sample{0, 1, 2, nil, nil}, sample{0, 3, 4, nil, nil}, sample{0, 5, 6, nil, nil}, sample{0, 7, 8, nil, nil}},
{},
},
},
@@ -1907,7 +1911,7 @@ func TestPopulateWithDelSeriesIterator_SeekInCurrentChunk(t *testing.T) {
valType: chunkenc.ValHistogram,
chks: [][]chunks.Sample{
{},
- {sample{1, 0, tsdbutil.GenerateTestHistogram(2), nil}, sample{3, 0, tsdbutil.GenerateTestHistogram(4), nil}, sample{5, 0, tsdbutil.GenerateTestHistogram(6), nil}, sample{7, 0, tsdbutil.GenerateTestHistogram(8), nil}},
+ {sample{0, 1, 0, tsdbutil.GenerateTestHistogram(2), nil}, sample{0, 3, 0, tsdbutil.GenerateTestHistogram(4), nil}, sample{0, 5, 0, tsdbutil.GenerateTestHistogram(6), nil}, sample{0, 7, 0, tsdbutil.GenerateTestHistogram(8), nil}},
{},
},
},
@@ -1916,7 +1920,7 @@ func TestPopulateWithDelSeriesIterator_SeekInCurrentChunk(t *testing.T) {
valType: chunkenc.ValFloatHistogram,
chks: [][]chunks.Sample{
{},
- {sample{1, 0, nil, tsdbutil.GenerateTestFloatHistogram(2)}, sample{3, 0, nil, tsdbutil.GenerateTestFloatHistogram(4)}, sample{5, 0, nil, tsdbutil.GenerateTestFloatHistogram(6)}, sample{7, 0, nil, tsdbutil.GenerateTestFloatHistogram(8)}},
+ {sample{0, 1, 0, nil, tsdbutil.GenerateTestFloatHistogram(2)}, sample{0, 3, 0, nil, tsdbutil.GenerateTestFloatHistogram(4)}, sample{0, 5, 0, nil, tsdbutil.GenerateTestFloatHistogram(6)}, sample{0, 7, 0, nil, tsdbutil.GenerateTestFloatHistogram(8)}},
{},
},
},
@@ -1948,21 +1952,21 @@ func TestPopulateWithDelSeriesIterator_SeekWithMinTime(t *testing.T) {
name: "float",
valType: chunkenc.ValFloat,
chks: [][]chunks.Sample{
- {sample{1, 6, nil, nil}, sample{5, 6, nil, nil}, sample{6, 8, nil, nil}},
+ {sample{0, 1, 6, nil, nil}, sample{0, 5, 6, nil, nil}, sample{0, 6, 8, nil, nil}},
},
},
{
name: "histogram",
valType: chunkenc.ValHistogram,
chks: [][]chunks.Sample{
- {sample{1, 0, tsdbutil.GenerateTestHistogram(6), nil}, sample{5, 0, tsdbutil.GenerateTestHistogram(6), nil}, sample{6, 0, tsdbutil.GenerateTestHistogram(8), nil}},
+ {sample{0, 1, 0, tsdbutil.GenerateTestHistogram(6), nil}, sample{0, 5, 0, tsdbutil.GenerateTestHistogram(6), nil}, sample{0, 6, 0, tsdbutil.GenerateTestHistogram(8), nil}},
},
},
{
name: "float histogram",
valType: chunkenc.ValFloatHistogram,
chks: [][]chunks.Sample{
- {sample{1, 0, nil, tsdbutil.GenerateTestFloatHistogram(6)}, sample{5, 0, nil, tsdbutil.GenerateTestFloatHistogram(6)}, sample{6, 0, nil, tsdbutil.GenerateTestFloatHistogram(8)}},
+ {sample{0, 1, 0, nil, tsdbutil.GenerateTestFloatHistogram(6)}, sample{0, 5, 0, nil, tsdbutil.GenerateTestFloatHistogram(6)}, sample{0, 6, 0, nil, tsdbutil.GenerateTestFloatHistogram(8)}},
},
},
}
@@ -1991,21 +1995,21 @@ func TestPopulateWithDelSeriesIterator_NextWithMinTime(t *testing.T) {
name: "float",
valType: chunkenc.ValFloat,
chks: [][]chunks.Sample{
- {sample{1, 6, nil, nil}, sample{5, 6, nil, nil}, sample{7, 8, nil, nil}},
+ {sample{0, 1, 6, nil, nil}, sample{0, 5, 6, nil, nil}, sample{0, 7, 8, nil, nil}},
},
},
{
name: "histogram",
valType: chunkenc.ValHistogram,
chks: [][]chunks.Sample{
- {sample{1, 0, tsdbutil.GenerateTestHistogram(6), nil}, sample{5, 0, tsdbutil.GenerateTestHistogram(6), nil}, sample{7, 0, tsdbutil.GenerateTestHistogram(8), nil}},
+ {sample{0, 1, 0, tsdbutil.GenerateTestHistogram(6), nil}, sample{0, 5, 0, tsdbutil.GenerateTestHistogram(6), nil}, sample{0, 7, 0, tsdbutil.GenerateTestHistogram(8), nil}},
},
},
{
name: "float histogram",
valType: chunkenc.ValFloatHistogram,
chks: [][]chunks.Sample{
- {sample{1, 0, nil, tsdbutil.GenerateTestFloatHistogram(6)}, sample{5, 0, nil, tsdbutil.GenerateTestFloatHistogram(6)}, sample{7, 0, nil, tsdbutil.GenerateTestFloatHistogram(8)}},
+ {sample{0, 1, 0, nil, tsdbutil.GenerateTestFloatHistogram(6)}, sample{0, 5, 0, nil, tsdbutil.GenerateTestFloatHistogram(6)}, sample{0, 7, 0, nil, tsdbutil.GenerateTestFloatHistogram(8)}},
},
},
}
@@ -2096,7 +2100,7 @@ func TestDeletedIterator(t *testing.T) {
for i := range 1000 {
act[i].t = int64(i)
act[i].f = rand.Float64()
- app.Append(act[i].t, act[i].f)
+ app.Append(0, act[i].t, act[i].f)
}
cases := []struct {
@@ -2156,7 +2160,7 @@ func TestDeletedIterator_WithSeek(t *testing.T) {
for i := range 1000 {
act[i].t = int64(i)
act[i].f = float64(i)
- app.Append(act[i].t, act[i].f)
+ app.Append(0, act[i].t, act[i].f)
}
cases := []struct {
diff --git a/tsdb/tombstones/tombstones.go b/tsdb/tombstones/tombstones.go
index 25218782cd..b7bcd8801b 100644
--- a/tsdb/tombstones/tombstones.go
+++ b/tsdb/tombstones/tombstones.go
@@ -28,7 +28,6 @@ import (
"github.com/prometheus/prometheus/storage"
"github.com/prometheus/prometheus/tsdb/encoding"
- tsdb_errors "github.com/prometheus/prometheus/tsdb/errors"
"github.com/prometheus/prometheus/tsdb/fileutil"
)
@@ -128,7 +127,7 @@ func WriteFile(logger *slog.Logger, dir string, tr Reader) (int64, error) {
size += n
if err := f.Sync(); err != nil {
- return 0, tsdb_errors.NewMulti(err, f.Close()).Err()
+ return 0, errors.Join(err, f.Close())
}
if err = f.Close(); err != nil {
diff --git a/tsdb/tsdbutil/dir_locker.go b/tsdb/tsdbutil/dir_locker.go
index 45cabdd3d7..139e66859a 100644
--- a/tsdb/tsdbutil/dir_locker.go
+++ b/tsdb/tsdbutil/dir_locker.go
@@ -22,7 +22,6 @@ import (
"github.com/prometheus/client_golang/prometheus"
- tsdb_errors "github.com/prometheus/prometheus/tsdb/errors"
"github.com/prometheus/prometheus/tsdb/fileutil"
)
@@ -94,10 +93,9 @@ func (l *DirLocker) Release() error {
return nil
}
- errs := tsdb_errors.NewMulti()
- errs.Add(l.releaser.Release())
- errs.Add(os.Remove(l.path))
+ releaserErr := l.releaser.Release()
+ removeErr := os.Remove(l.path)
l.releaser = nil
- return errs.Err()
+ return errors.Join(releaserErr, removeErr)
}
diff --git a/tsdb/wlog/checkpoint.go b/tsdb/wlog/checkpoint.go
index 57c2faf23e..6742141fbc 100644
--- a/tsdb/wlog/checkpoint.go
+++ b/tsdb/wlog/checkpoint.go
@@ -28,7 +28,6 @@ import (
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/tsdb/chunks"
- tsdb_errors "github.com/prometheus/prometheus/tsdb/errors"
"github.com/prometheus/prometheus/tsdb/fileutil"
"github.com/prometheus/prometheus/tsdb/record"
"github.com/prometheus/prometheus/tsdb/tombstones"
@@ -71,14 +70,14 @@ func DeleteCheckpoints(dir string, maxIndex int) error {
return err
}
- errs := tsdb_errors.NewMulti()
+ var errs []error
for _, checkpoint := range checkpoints {
if checkpoint.index >= maxIndex {
break
}
- errs.Add(os.RemoveAll(filepath.Join(dir, checkpoint.name)))
+ errs = append(errs, os.RemoveAll(filepath.Join(dir, checkpoint.name)))
}
- return errs.Err()
+ return errors.Join(errs...)
}
// CheckpointPrefix is the prefix used for checkpoint files.
diff --git a/tsdb/wlog/reader_test.go b/tsdb/wlog/reader_test.go
index 788a2edfb9..971423e5cc 100644
--- a/tsdb/wlog/reader_test.go
+++ b/tsdb/wlog/reader_test.go
@@ -18,6 +18,7 @@ import (
"bytes"
"crypto/rand"
"encoding/binary"
+ "errors"
"fmt"
"hash/crc32"
"io"
@@ -32,7 +33,6 @@ import (
"github.com/prometheus/common/promslog"
"github.com/stretchr/testify/require"
- tsdb_errors "github.com/prometheus/prometheus/tsdb/errors"
"github.com/prometheus/prometheus/util/compression"
)
@@ -287,7 +287,7 @@ func (m *multiReadCloser) Read(p []byte) (n int, err error) {
}
func (m *multiReadCloser) Close() error {
- return tsdb_errors.NewMulti(tsdb_errors.CloseAll(m.closers)).Err()
+ return errors.Join(closeAll(m.closers))
}
func allSegments(dir string) (io.ReadCloser, error) {
@@ -549,3 +549,12 @@ func TestReaderData(t *testing.T) {
})
}
}
+
+// closeAll closes all given closers while recording error in MultiError.
+func closeAll(cs []io.Closer) error {
+ var errs []error
+ for _, c := range cs {
+ errs = append(errs, c.Close())
+ }
+ return errors.Join(errs...)
+}
diff --git a/util/fuzzing/.gitignore b/util/fuzzing/.gitignore
new file mode 100644
index 0000000000..539a5ec32d
--- /dev/null
+++ b/util/fuzzing/.gitignore
@@ -0,0 +1 @@
+Fuzz*_seed_corpus.zip
diff --git a/util/fuzzing/corpus.go b/util/fuzzing/corpus.go
new file mode 100644
index 0000000000..7f26271699
--- /dev/null
+++ b/util/fuzzing/corpus.go
@@ -0,0 +1,124 @@
+// Copyright 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 fuzzing
+
+import (
+ "github.com/prometheus/prometheus/promql/parser"
+ "github.com/prometheus/prometheus/promql/promqltest"
+)
+
+// GetCorpusForFuzzParseMetricText returns the seed corpus for FuzzParseMetricText.
+func GetCorpusForFuzzParseMetricText() [][]byte {
+ return [][]byte{
+ []byte(""),
+ []byte("metric_name 1.0"),
+ []byte("# HELP metric_name help text\n# TYPE metric_name counter\nmetric_name 1.0"),
+ []byte("o { quantile = \"1.0\", a = \"b\" } 8.3835e-05"),
+ []byte("# HELP api_http_request_count The total number of HTTP requests.\n# TYPE api_http_request_count counter\nhttp_request_count{method=\"post\",code=\"200\"} 1027 1395066363000"),
+ []byte("msdos_file_access_time_ms{path=\"C:\\\\DIR\\\\FILE.TXT\",error=\"Cannot find file:\\n\\\"FILE.TXT\\\"\"} 1.234e3"),
+ []byte("metric_without_timestamp_and_labels 12.47"),
+ []byte("something_weird{problem=\"division by zero\"} +Inf -3982045"),
+ []byte("http_request_duration_seconds_bucket{le=\"+Inf\"} 144320"),
+ []byte("go_gc_duration_seconds{ quantile=\"0.9\", a=\"b\"} 8.3835e-05"),
+ []byte("go_gc_duration_seconds{ quantile=\"1.0\", a=\"b\" } 8.3835e-05"),
+ []byte("go_gc_duration_seconds{ quantile = \"1.0\", a = \"b\" } 8.3835e-05"),
+ }
+}
+
+// GetCorpusForFuzzParseOpenMetric returns the seed corpus for FuzzParseOpenMetric.
+func GetCorpusForFuzzParseOpenMetric() [][]byte {
+ return [][]byte{
+ []byte(""),
+ []byte("# TYPE metric_name counter\nmetric_name_total 1.0"),
+ []byte("# HELP metric_name help text\n# TYPE metric_name counter\nmetric_name_total 1.0\n# EOF"),
+ }
+}
+
+// GetCorpusForFuzzParseMetricSelector returns the seed corpus for FuzzParseMetricSelector.
+func GetCorpusForFuzzParseMetricSelector() []string {
+ return []string{
+ "",
+ "metric_name",
+ `metric_name{label="value"}`,
+ `{label="value"}`,
+ `metric_name{label=~"val.*"}`,
+ }
+}
+
+// GetCorpusForFuzzParseExpr returns the seed corpus for FuzzParseExpr.
+func GetCorpusForFuzzParseExpr() ([]string, error) {
+ // Save original values and restore them after parsing test expressions.
+ defer func(funcs, durationExpr, rangeSelectors bool) {
+ parser.EnableExperimentalFunctions = funcs
+ parser.ExperimentalDurationExpr = durationExpr
+ parser.EnableExtendedRangeSelectors = rangeSelectors
+ }(parser.EnableExperimentalFunctions, parser.ExperimentalDurationExpr, parser.EnableExtendedRangeSelectors)
+
+ // Enable experimental features to parse all test expressions.
+ parser.EnableExperimentalFunctions = true
+ parser.ExperimentalDurationExpr = true
+ parser.EnableExtendedRangeSelectors = true
+
+ // Get built-in test expressions.
+ builtInExprs, err := promqltest.GetBuiltInExprs()
+ if err != nil {
+ return nil, err
+ }
+
+ // Add additional seed corpus.
+ additionalExprs := []string{
+ "",
+ "1",
+ "metric_name",
+ `"str"`,
+ // Numeric literals
+ ".5",
+ "5.",
+ "123.4567",
+ "5e3",
+ "5e-3",
+ "+5.5e-3",
+ "0xc",
+ "0755",
+ "-0755",
+ "+Inf",
+ "-Inf",
+ // Basic binary operations
+ "1 + 1",
+ "1 - 1",
+ "1 * 1",
+ "1 / 1",
+ "1 % 1",
+ // Comparison operators
+ "1 == 1",
+ "1 != 1",
+ "1 > 1",
+ "1 >= 1",
+ "1 < 1",
+ "1 <= 1",
+ // Operations with identifiers
+ "foo == 1",
+ "foo * bar",
+ "2.5 / bar",
+ "foo and bar",
+ "foo or bar",
+ // Complex expressions
+ "+1 + -2 * 1",
+ "1 + 2/(3*1)",
+ // Comment
+ "#comment",
+ }
+
+ return append(builtInExprs, additionalExprs...), nil
+}
diff --git a/util/fuzzing/corpus_gen/main.go b/util/fuzzing/corpus_gen/main.go
new file mode 100644
index 0000000000..aa38a79a48
--- /dev/null
+++ b/util/fuzzing/corpus_gen/main.go
@@ -0,0 +1,116 @@
+// Copyright 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.
+
+//go:build fuzzing
+
+//go:generate go run -tags fuzzing .
+
+package main
+
+import (
+ "archive/zip"
+ "fmt"
+ "os"
+ "path/filepath"
+ "sort"
+
+ "github.com/prometheus/prometheus/util/fuzzing"
+)
+
+func main() {
+ if err := run(); err != nil {
+ fmt.Fprintf(os.Stderr, "Error: %v\n", err)
+ os.Exit(1)
+ }
+ fmt.Println("Successfully generated all seed corpus ZIP files.")
+}
+
+func run() error {
+ // Generate FuzzParseExpr seed corpus.
+ exprs, err := fuzzing.GetCorpusForFuzzParseExpr()
+ if err != nil {
+ return fmt.Errorf("failed to get corpus for FuzzParseExpr: %w", err)
+ }
+ if err := generateZipFromStrings("fuzzParseExpr", exprs); err != nil {
+ return fmt.Errorf("failed to generate FuzzParseExpr_seed_corpus.zip: %w", err)
+ }
+ fmt.Printf("Generated fuzzParseExpr_seed_corpus.zip with %d entries.\n", len(exprs))
+
+ // Generate FuzzParseMetricSelector seed corpus.
+ selectors := fuzzing.GetCorpusForFuzzParseMetricSelector()
+ if err := generateZipFromStrings("fuzzParseMetricSelector", selectors); err != nil {
+ return fmt.Errorf("failed to generate FuzzParseMetricSelector_seed_corpus.zip: %w", err)
+ }
+ fmt.Printf("Generated fuzzParseMetricSelector_seed_corpus.zip with %d entries.\n", len(selectors))
+
+ // Generate FuzzParseMetricText seed corpus.
+ metrics := fuzzing.GetCorpusForFuzzParseMetricText()
+ if err := generateZipFromBytes("fuzzParseMetricText", metrics); err != nil {
+ return fmt.Errorf("failed to generate FuzzParseMetricText_seed_corpus.zip: %w", err)
+ }
+ fmt.Printf("Generated fuzzParseMetricText_seed_corpus.zip with %d entries.\n", len(metrics))
+
+ // Generate FuzzParseOpenMetric seed corpus.
+ openMetrics := fuzzing.GetCorpusForFuzzParseOpenMetric()
+ if err := generateZipFromBytes("fuzzParseOpenMetric", openMetrics); err != nil {
+ return fmt.Errorf("failed to generate FuzzParseOpenMetric_seed_corpus.zip: %w", err)
+ }
+ fmt.Printf("Generated fuzzParseOpenMetric_seed_corpus.zip with %d entries.\n", len(openMetrics))
+
+ return nil
+}
+
+// generateZipFromBytes creates a seed corpus ZIP file from a slice of byte slices.
+func generateZipFromBytes(fuzzName string, corpus [][]byte) error {
+ // Sort corpus deterministically.
+ sorted := make([][]byte, len(corpus))
+ copy(sorted, corpus)
+ sort.Slice(sorted, func(i, j int) bool {
+ return string(sorted[i]) < string(sorted[j])
+ })
+
+ // Create ZIP file in parent directory.
+ zipPath := filepath.Join("..", fuzzName+"_seed_corpus.zip")
+ zipFile, err := os.Create(zipPath)
+ if err != nil {
+ return fmt.Errorf("failed to create zip file: %w", err)
+ }
+ defer zipFile.Close()
+
+ zipWriter := zip.NewWriter(zipFile)
+ defer zipWriter.Close()
+
+ // Add each corpus entry as a file.
+ for i, entry := range sorted {
+ fileName := fmt.Sprintf("expr%d", i)
+ writer, err := zipWriter.Create(fileName)
+ if err != nil {
+ return fmt.Errorf("failed to create zip entry %s: %w", fileName, err)
+ }
+ if _, err := writer.Write(entry); err != nil {
+ return fmt.Errorf("failed to write zip entry %s: %w", fileName, err)
+ }
+ }
+
+ return nil
+}
+
+// generateZipFromStrings creates a seed corpus ZIP file from a slice of strings.
+func generateZipFromStrings(fuzzName string, corpus []string) error {
+ // Convert []string to [][]byte and delegate to generateZipFromBytes
+ byteCorpus := make([][]byte, len(corpus))
+ for i, s := range corpus {
+ byteCorpus[i] = []byte(s)
+ }
+ return generateZipFromBytes(fuzzName, byteCorpus)
+}
diff --git a/util/fuzzing/fuzz_test.go b/util/fuzzing/fuzz_test.go
new file mode 100644
index 0000000000..257b04bb60
--- /dev/null
+++ b/util/fuzzing/fuzz_test.go
@@ -0,0 +1,152 @@
+// Copyright 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 fuzzing
+
+import (
+ "errors"
+ "io"
+ "testing"
+
+ "github.com/prometheus/prometheus/model/labels"
+ "github.com/prometheus/prometheus/model/textparse"
+ "github.com/prometheus/prometheus/promql/parser"
+)
+
+const (
+ // Input size above which we know that Prometheus would consume too much
+ // memory. The recommended way to deal with it is check input size.
+ // https://google.github.io/oss-fuzz/getting-started/new-project-guide/#input-size
+ maxInputSize = 10240
+)
+
+// Use package-scope symbol table to avoid memory allocation on every fuzzing operation.
+var symbolTable = labels.NewSymbolTable()
+
+// FuzzParseMetricText fuzzes the metric parser with "text/plain" content type.
+//
+// Note that this is not the parser for the text-based exposition-format; that
+// lives in github.com/prometheus/client_golang/text.
+func FuzzParseMetricText(f *testing.F) {
+ // Add seed corpus
+ for _, corpus := range GetCorpusForFuzzParseMetricText() {
+ f.Add(corpus)
+ }
+
+ f.Fuzz(func(t *testing.T, in []byte) {
+ p, warning := textparse.New(in, "text/plain", symbolTable, textparse.ParserOptions{})
+ if p == nil || warning != nil {
+ // An invalid content type is being passed, which should not happen
+ // in this context.
+ t.Skip()
+ }
+
+ var err error
+ for {
+ _, err = p.Next()
+ if err != nil {
+ break
+ }
+ }
+ if errors.Is(err, io.EOF) {
+ err = nil
+ }
+
+ // We don't care about errors, just that we don't panic.
+ _ = err
+ })
+}
+
+// FuzzParseOpenMetric fuzzes the metric parser with "application/openmetrics-text" content type.
+func FuzzParseOpenMetric(f *testing.F) {
+ // Add seed corpus
+ for _, corpus := range GetCorpusForFuzzParseOpenMetric() {
+ f.Add(corpus)
+ }
+
+ f.Fuzz(func(t *testing.T, in []byte) {
+ p, warning := textparse.New(in, "application/openmetrics-text", symbolTable, textparse.ParserOptions{})
+ if p == nil || warning != nil {
+ // An invalid content type is being passed, which should not happen
+ // in this context.
+ t.Skip()
+ }
+
+ var err error
+ for {
+ _, err = p.Next()
+ if err != nil {
+ break
+ }
+ }
+ if errors.Is(err, io.EOF) {
+ err = nil
+ }
+
+ // We don't care about errors, just that we don't panic.
+ _ = err
+ })
+}
+
+// FuzzParseMetricSelector fuzzes the metric selector parser.
+func FuzzParseMetricSelector(f *testing.F) {
+ // Add seed corpus
+ for _, corpus := range GetCorpusForFuzzParseMetricSelector() {
+ f.Add(corpus)
+ }
+
+ f.Fuzz(func(t *testing.T, in string) {
+ if len(in) > maxInputSize {
+ t.Skip()
+ }
+ _, err := parser.ParseMetricSelector(in)
+ // We don't care about errors, just that we don't panic.
+ _ = err
+ })
+}
+
+// FuzzParseExpr fuzzes the expression parser.
+func FuzzParseExpr(f *testing.F) {
+ parser.EnableExperimentalFunctions = true
+ parser.ExperimentalDurationExpr = true
+ parser.EnableExtendedRangeSelectors = true
+ parser.EnableBinopFillModifiers = true
+ f.Cleanup(func() {
+ parser.EnableExperimentalFunctions = false
+ parser.ExperimentalDurationExpr = false
+ parser.EnableExtendedRangeSelectors = false
+ parser.EnableBinopFillModifiers = false
+ })
+
+ // Add seed corpus from built-in test expressions
+ corpus, err := GetCorpusForFuzzParseExpr()
+ if err != nil {
+ f.Fatal(err)
+ }
+ if len(corpus) < 1000 {
+ f.Fatalf("loading exprs is likely broken: got %d expressions, expected at least 1000", len(corpus))
+ }
+
+ for _, expr := range corpus {
+ f.Add(expr)
+ }
+
+ f.Fuzz(func(t *testing.T, in string) {
+ if len(in) > maxInputSize {
+ t.Skip()
+ }
+ _, err := parser.ParseExpr(in)
+ // We don't care about errors, just that we don't panic.
+ _ = err
+ })
+}
diff --git a/util/strutil/strconv_test.go b/util/strutil/strconv_test.go
index b4b87ee816..362fa79a6a 100644
--- a/util/strutil/strconv_test.go
+++ b/util/strutil/strconv_test.go
@@ -36,6 +36,26 @@ var linkTests = []linkTest{
"/graph?g0.expr=sum%28incoming_http_requests_total%7Bsystem%3D%22trackmetadata%22%7D%29&g0.tab=0",
"/graph?g0.expr=sum%28incoming_http_requests_total%7Bsystem%3D%22trackmetadata%22%7D%29&g0.tab=1",
},
+ {
+ "up",
+ "/graph?g0.expr=up&g0.tab=0",
+ "/graph?g0.expr=up&g0.tab=1",
+ },
+ {
+ "rate(http_requests_total[5m])",
+ "/graph?g0.expr=rate%28http_requests_total%5B5m%5D%29&g0.tab=0",
+ "/graph?g0.expr=rate%28http_requests_total%5B5m%5D%29&g0.tab=1",
+ },
+ {
+ "",
+ "/graph?g0.expr=&g0.tab=0",
+ "/graph?g0.expr=&g0.tab=1",
+ },
+ {
+ "metric_name{label=\"value with spaces\"}",
+ "/graph?g0.expr=metric_name%7Blabel%3D%22value+with+spaces%22%7D&g0.tab=0",
+ "/graph?g0.expr=metric_name%7Blabel%3D%22value+with+spaces%22%7D&g0.tab=1",
+ },
}
func TestLink(t *testing.T) {
@@ -51,29 +71,158 @@ func TestLink(t *testing.T) {
}
func TestSanitizeLabelName(t *testing.T) {
- actual := SanitizeLabelName("fooClientLABEL")
- expected := "fooClientLABEL"
- require.Equal(t, expected, actual, "SanitizeLabelName failed for label (%s)", expected)
+ tests := []struct {
+ name string
+ input string
+ expected string
+ }{
+ {
+ name: "valid label name",
+ input: "fooClientLABEL",
+ expected: "fooClientLABEL",
+ },
+ {
+ name: "label with special characters",
+ input: "barClient.LABEL$$##",
+ expected: "barClient_LABEL____",
+ },
+ {
+ name: "label starting with digit",
+ input: "123label",
+ expected: "123label",
+ },
+ {
+ name: "label with dashes",
+ input: "my-label-name",
+ expected: "my_label_name",
+ },
+ {
+ name: "label with spaces",
+ input: "my label name",
+ expected: "my_label_name",
+ },
+ {
+ name: "label with mixed case and numbers",
+ input: "Test123Label456",
+ expected: "Test123Label456",
+ },
+ {
+ name: "label with unicode characters",
+ input: "test-ñ-ü-label",
+ expected: "test_____label",
+ },
+ {
+ name: "empty string",
+ input: "",
+ expected: "",
+ },
+ {
+ name: "only underscores",
+ input: "___",
+ expected: "___",
+ },
+ {
+ name: "label with colons",
+ input: "namespace:metric_name",
+ expected: "namespace_metric_name",
+ },
+ }
- actual = SanitizeLabelName("barClient.LABEL$$##")
- expected = "barClient_LABEL____"
- require.Equal(t, expected, actual, "SanitizeLabelName failed for label (%s)", expected)
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ actual := SanitizeLabelName(tt.input)
+ require.Equal(t, tt.expected, actual, "SanitizeLabelName(%q) = %q, want %q", tt.input, actual, tt.expected)
+ })
+ }
}
func TestSanitizeFullLabelName(t *testing.T) {
- actual := SanitizeFullLabelName("fooClientLABEL")
- expected := "fooClientLABEL"
- require.Equal(t, expected, actual, "SanitizeFullLabelName failed for label (%s)", expected)
+ tests := []struct {
+ name string
+ input string
+ expected string
+ }{
+ {
+ name: "valid label name",
+ input: "fooClientLABEL",
+ expected: "fooClientLABEL",
+ },
+ {
+ name: "label with special characters",
+ input: "barClient.LABEL$$##",
+ expected: "barClient_LABEL____",
+ },
+ {
+ name: "label starting with digit",
+ input: "0zerothClient1LABEL",
+ expected: "_zerothClient1LABEL",
+ },
+ {
+ name: "empty string",
+ input: "",
+ expected: "_",
+ },
+ {
+ name: "label starting with multiple digits",
+ input: "123abc",
+ expected: "_23abc",
+ },
+ {
+ name: "label with dashes",
+ input: "my-label-name",
+ expected: "my_label_name",
+ },
+ {
+ name: "label with spaces",
+ input: "my label name",
+ expected: "my_label_name",
+ },
+ {
+ name: "label with numbers in middle",
+ input: "Test123Label456",
+ expected: "Test123Label456",
+ },
+ {
+ name: "single underscore",
+ input: "_",
+ expected: "_",
+ },
+ {
+ name: "label starting with underscore",
+ input: "_validLabel",
+ expected: "_validLabel",
+ },
+ {
+ name: "label with colons",
+ input: "namespace:metric_name",
+ expected: "namespace_metric_name",
+ },
+ {
+ name: "label with unicode characters",
+ input: "test-ñ-ü-label",
+ expected: "test_____label",
+ },
+ {
+ name: "only digits",
+ input: "12345",
+ expected: "_2345",
+ },
+ {
+ name: "label with mixed invalid characters at start",
+ input: "!@#test",
+ expected: "___test",
+ },
+ {
+ name: "label with consecutive digits at start",
+ input: "0123test",
+ expected: "_123test",
+ },
+ }
- actual = SanitizeFullLabelName("barClient.LABEL$$##")
- expected = "barClient_LABEL____"
- require.Equal(t, expected, actual, "SanitizeFullLabelName failed for label (%s)", expected)
-
- actual = SanitizeFullLabelName("0zerothClient1LABEL")
- expected = "_zerothClient1LABEL"
- require.Equal(t, expected, actual, "SanitizeFullLabelName failed for label (%s)", expected)
-
- actual = SanitizeFullLabelName("")
- expected = "_"
- require.Equal(t, expected, actual, "SanitizeFullLabelName failed for the empty label")
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ actual := SanitizeFullLabelName(tt.input)
+ require.Equal(t, tt.expected, actual, "SanitizeFullLabelName(%q) = %q, want %q", tt.input, actual, tt.expected)
+ })
+ }
}
diff --git a/util/teststorage/appender.go b/util/teststorage/appender.go
index 058a09561c..dc0825f98f 100644
--- a/util/teststorage/appender.go
+++ b/util/teststorage/appender.go
@@ -21,15 +21,20 @@ import (
"slices"
"strings"
"sync"
+ "testing"
+ "github.com/google/go-cmp/cmp"
"github.com/prometheus/common/model"
+ "github.com/stretchr/testify/require"
"go.uber.org/atomic"
"github.com/prometheus/prometheus/model/exemplar"
"github.com/prometheus/prometheus/model/histogram"
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/model/metadata"
+ "github.com/prometheus/prometheus/model/value"
"github.com/prometheus/prometheus/storage"
+ "github.com/prometheus/prometheus/util/testutil"
)
// Sample represents test, combined sample for mocking storage.AppenderV2.
@@ -65,13 +70,17 @@ func (s Sample) String() string {
// Print all value types on purpose, to catch bugs for appending multiple sample types at once.
h := ""
if s.H != nil {
- h = s.H.String()
+ h = " " + s.H.String()
}
fh := ""
if s.FH != nil {
- fh = s.FH.String()
+ fh = " " + s.FH.String()
}
- b.WriteString(fmt.Sprintf("%s %v%v%v st@%v t@%v\n", s.L.String(), s.V, h, fh, s.ST, s.T))
+ b.WriteString(fmt.Sprintf("%s %v%v%v st@%v t@%v", s.L.String(), s.V, h, fh, s.ST, s.T))
+ if len(s.ES) > 0 {
+ b.WriteString(fmt.Sprintf(" %v", s.ES))
+ }
+ b.WriteString("\n")
return b.String()
}
@@ -87,6 +96,88 @@ func (s Sample) Equals(other Sample) bool {
slices.EqualFunc(s.ES, other.ES, exemplar.Exemplar.Equals)
}
+// IsStale returns whether the sample represents a stale sample, according to
+// https://prometheus.io/docs/specs/native_histograms/#staleness-markers.
+func (s Sample) IsStale() bool {
+ switch {
+ case s.FH != nil:
+ return value.IsStaleNaN(s.FH.Sum)
+ case s.H != nil:
+ return value.IsStaleNaN(s.H.Sum)
+ default:
+ return value.IsStaleNaN(s.V)
+ }
+}
+
+var sampleComparer = cmp.Comparer(func(a, b Sample) bool {
+ return a.Equals(b)
+})
+
+// RequireEqual is a special require equal that correctly compare Prometheus structures.
+//
+// In comparison to testutil.RequireEqual, this function adds special logic for comparing []Samples.
+//
+// It also ignores ordering between consecutive stale samples to avoid false
+// negatives due to map iteration order in staleness tracking.
+func RequireEqual(t testing.TB, expected, got []Sample, msgAndArgs ...any) {
+ opts := []cmp.Option{sampleComparer}
+ expected = reorderExpectedForStaleness(expected, got)
+ testutil.RequireEqualWithOptions(t, expected, got, opts, msgAndArgs...)
+}
+
+// RequireNotEqual is the negation of RequireEqual.
+func RequireNotEqual(t testing.TB, expected, got []Sample, msgAndArgs ...any) {
+ t.Helper()
+
+ opts := []cmp.Option{cmp.Comparer(labels.Equal), sampleComparer}
+ expected = reorderExpectedForStaleness(expected, got)
+ if !cmp.Equal(expected, got, opts...) {
+ return
+ }
+ require.Fail(t, fmt.Sprintf("Equal, but expected not: \n"+
+ "a: %s\n"+
+ "b: %s", expected, got), msgAndArgs...)
+}
+
+func reorderExpectedForStaleness(expected, got []Sample) []Sample {
+ if len(expected) != len(got) || !includeStaleNaNs(expected) {
+ return expected
+ }
+ result := make([]Sample, len(expected))
+ copy(result, expected)
+
+ // Try to reorder only consecutive stale samples to avoid false negatives
+ // due to map iteration order in staleness tracking.
+ for i := range result {
+ if !result[i].IsStale() {
+ continue
+ }
+ if result[i].Equals(got[i]) {
+ continue
+ }
+ for j := i + 1; j < len(result); j++ {
+ if !result[j].IsStale() {
+ break
+ }
+ if result[j].Equals(got[i]) {
+ // Swap.
+ result[i], result[j] = result[j], result[i]
+ break
+ }
+ }
+ }
+ return result
+}
+
+func includeStaleNaNs(s []Sample) bool {
+ for _, e := range s {
+ if e.IsStale() {
+ return true
+ }
+ }
+ return false
+}
+
// Appendable is a storage.Appendable mock.
// It allows recording all samples that were added through the appender and injecting errors.
// Appendable will panic if more than one Appender is open.
@@ -104,7 +195,7 @@ type Appendable struct {
rolledbackSamples []Sample
// Optional chain (Appender will collect samples, then run next).
- next storage.Appendable
+ next compatAppendable
}
// NewAppendable returns mock Appendable.
@@ -112,8 +203,13 @@ func NewAppendable() *Appendable {
return &Appendable{}
}
-// Then chains another appender from the provided appendable for the Appender calls.
-func (a *Appendable) Then(appendable storage.Appendable) *Appendable {
+type compatAppendable interface {
+ storage.Appendable
+ storage.AppendableV2
+}
+
+// Then chains another appender from the provided Appendable for the Appender calls.
+func (a *Appendable) Then(appendable compatAppendable) *Appendable {
a.next = appendable
return a
}
@@ -130,6 +226,9 @@ func (a *Appendable) WithErrs(appendErrFn func(ls labels.Labels) error, appendEx
func (a *Appendable) PendingSamples() []Sample {
a.mtx.Lock()
defer a.mtx.Unlock()
+ if len(a.pendingSamples) == 0 {
+ return nil
+ }
ret := make([]Sample, len(a.pendingSamples))
copy(ret, a.pendingSamples)
@@ -140,6 +239,9 @@ func (a *Appendable) PendingSamples() []Sample {
func (a *Appendable) ResultSamples() []Sample {
a.mtx.Lock()
defer a.mtx.Unlock()
+ if len(a.resultSamples) == 0 {
+ return nil
+ }
ret := make([]Sample, len(a.resultSamples))
copy(ret, a.resultSamples)
@@ -150,6 +252,9 @@ func (a *Appendable) ResultSamples() []Sample {
func (a *Appendable) RolledbackSamples() []Sample {
a.mtx.Lock()
defer a.mtx.Unlock()
+ if len(a.rolledbackSamples) == 0 {
+ return nil
+ }
ret := make([]Sample, len(a.rolledbackSamples))
copy(ret, a.rolledbackSamples)
@@ -205,28 +310,76 @@ func (a *Appendable) String() string {
var errClosedAppender = errors.New("appender was already committed/rolledback")
-type appender struct {
- err error
- next storage.Appender
+type baseAppender struct {
+ err error
- a *Appendable
+ nextTr storage.AppenderTransaction
+ a *Appendable
}
-func (a *appender) checkErr() error {
+func (a *baseAppender) checkErr() error {
a.a.mtx.Lock()
defer a.a.mtx.Unlock()
return a.err
}
+func (a *baseAppender) Commit() error {
+ if err := a.checkErr(); err != nil {
+ return err
+ }
+ defer a.a.openAppenders.Dec()
+
+ if a.a.commitErr != nil {
+ return a.a.commitErr
+ }
+
+ a.a.mtx.Lock()
+ a.a.resultSamples = append(a.a.resultSamples, a.a.pendingSamples...)
+ a.a.pendingSamples = a.a.pendingSamples[:0]
+ a.err = errClosedAppender
+ a.a.mtx.Unlock()
+
+ if a.nextTr != nil {
+ return a.nextTr.Commit()
+ }
+ return nil
+}
+
+func (a *baseAppender) Rollback() error {
+ if err := a.checkErr(); err != nil {
+ return err
+ }
+ defer a.a.openAppenders.Dec()
+
+ a.a.mtx.Lock()
+ a.a.rolledbackSamples = append(a.a.rolledbackSamples, a.a.pendingSamples...)
+ a.a.pendingSamples = a.a.pendingSamples[:0]
+ a.err = errClosedAppender
+ a.a.mtx.Unlock()
+
+ if a.nextTr != nil {
+ return a.nextTr.Rollback()
+ }
+ return nil
+}
+
+type appender struct {
+ baseAppender
+
+ next storage.Appender
+}
+
func (a *Appendable) Appender(ctx context.Context) storage.Appender {
- ret := &appender{a: a}
+ ret := &appender{baseAppender: baseAppender{a: a}}
if a.openAppenders.Inc() > 1 {
ret.err = errors.New("teststorage.Appendable.Appender() concurrent use is not supported; attempted opening new Appender() without Commit/Rollback of the previous one. Extend the implementation if concurrent mock is needed")
+ return ret
}
if a.next != nil {
- ret.next = a.next.Appender(ctx)
+ app := a.next.Appender(ctx)
+ ret.next, ret.nextTr = app, app
}
return ret
}
@@ -263,8 +416,9 @@ func computeOrCheckRef(ref storage.SeriesRef, ls labels.Labels) (storage.SeriesR
}
if storage.SeriesRef(h) != ref {
- // Check for buggy ref while we at it.
- return 0, errors.New("teststorage.appender: found input ref not matching labels; potential bug in Appendable user")
+ // Check for buggy ref while we are at it. This only makes sense for cases without .Then*, because further appendable
+ // might have a different ref computation logic e.g. TSDB uses atomic increments.
+ return 0, errors.New("teststorage.appender: found input ref not matching labels; potential bug in Appendable usage")
}
return ref, nil
}
@@ -297,6 +451,7 @@ func (a *appender) AppendExemplar(ref storage.SeriesRef, l labels.Labels, e exem
if a.a.appendExemplarsError != nil {
return 0, a.a.appendExemplarsError
}
+ var appended bool
a.a.mtx.Lock()
// NOTE(bwplotka): Eventually exemplar has to be attached to a series and soon
@@ -304,13 +459,14 @@ func (a *appender) AppendExemplar(ref storage.SeriesRef, l labels.Labels, e exem
// with the naive attaching. See: https://github.com/prometheus/prometheus/issues/17632
i := len(a.a.pendingSamples) - 1
for ; i >= 0; i-- { // Attach exemplars to the last matching sample.
- if ref == storage.SeriesRef(a.a.pendingSamples[i].L.Hash()) {
+ if labels.Equal(l, a.a.pendingSamples[i].L) {
a.a.pendingSamples[i].ES = append(a.a.pendingSamples[i].ES, e)
+ appended = true
break
}
}
a.a.mtx.Unlock()
- if i < 0 {
+ if !appended {
return 0, fmt.Errorf("teststorage.appender: exemplar appender without series; ref %v; l %v; exemplar: %v", ref, l, e)
}
@@ -336,19 +492,22 @@ func (a *appender) UpdateMetadata(ref storage.SeriesRef, l labels.Labels, m meta
return 0, err
}
+ var updated bool
+
a.a.mtx.Lock()
// NOTE(bwplotka): Eventually metadata has to be attached to a series and soon
// the AppenderV2 will guarantee that for TSDB. Assume this from the mock perspective
// with the naive attaching. See: https://github.com/prometheus/prometheus/issues/17632
i := len(a.a.pendingSamples) - 1
for ; i >= 0; i-- { // Attach metadata to the last matching sample.
- if ref == storage.SeriesRef(a.a.pendingSamples[i].L.Hash()) {
+ if labels.Equal(l, a.a.pendingSamples[i].L) {
a.a.pendingSamples[i].M = m
+ updated = true
break
}
}
a.a.mtx.Unlock()
- if i < 0 {
+ if !updated {
return 0, fmt.Errorf("teststorage.appender: metadata update without series; ref %v; l %v; m: %v", ref, l, m)
}
@@ -358,42 +517,79 @@ func (a *appender) UpdateMetadata(ref storage.SeriesRef, l labels.Labels, m meta
return computeOrCheckRef(ref, l)
}
-func (a *appender) Commit() error {
- if err := a.checkErr(); err != nil {
- return err
- }
- defer a.a.openAppenders.Dec()
+type appenderV2 struct {
+ baseAppender
- if a.a.commitErr != nil {
- return a.a.commitErr
- }
-
- a.a.mtx.Lock()
- a.a.resultSamples = append(a.a.resultSamples, a.a.pendingSamples...)
- a.a.pendingSamples = a.a.pendingSamples[:0]
- a.err = errClosedAppender
- a.a.mtx.Unlock()
-
- if a.a.next != nil {
- return a.next.Commit()
- }
- return nil
+ next storage.AppenderV2
}
-func (a *appender) Rollback() error {
- if err := a.checkErr(); err != nil {
- return err
+func (a *Appendable) AppenderV2(ctx context.Context) storage.AppenderV2 {
+ ret := &appenderV2{baseAppender: baseAppender{a: a}}
+ if a.openAppenders.Inc() > 1 {
+ ret.err = errors.New("teststorage.Appendable.AppenderV2() concurrent use is not supported; attempted opening new AppenderV2() without Commit/Rollback of the previous one. Extend the implementation if concurrent mock is needed")
+ return ret
+ }
+
+ if a.next != nil {
+ app := a.next.AppenderV2(ctx)
+ ret.next, ret.nextTr = app, app
+ }
+ return ret
+}
+
+func (a *appenderV2) Append(ref storage.SeriesRef, ls labels.Labels, st, t int64, v float64, h *histogram.Histogram, fh *histogram.FloatHistogram, opts storage.AOptions) (_ storage.SeriesRef, err error) {
+ if err := a.checkErr(); err != nil {
+ return 0, err
+ }
+
+ if a.a.appendErrFn != nil {
+ if err := a.a.appendErrFn(ls); err != nil {
+ return 0, err
+ }
+ }
+
+ var (
+ es []exemplar.Exemplar
+ partialErr error
+ )
+
+ if len(opts.Exemplars) > 0 {
+ if a.a.appendExemplarsError != nil {
+ var exErrs []error
+ for range opts.Exemplars {
+ exErrs = append(exErrs, a.a.appendExemplarsError)
+ }
+ if len(exErrs) > 0 {
+ partialErr = &storage.AppendPartialError{ExemplarErrors: exErrs}
+ }
+ } else {
+ // As per AppenderV2 interface, opts.Exemplar slice is unsafe for reuse.
+ es = make([]exemplar.Exemplar, len(opts.Exemplars))
+ copy(es, opts.Exemplars)
+ }
}
- defer a.a.openAppenders.Dec()
a.a.mtx.Lock()
- a.a.rolledbackSamples = append(a.a.rolledbackSamples, a.a.pendingSamples...)
- a.a.pendingSamples = a.a.pendingSamples[:0]
- a.err = errClosedAppender
+ a.a.pendingSamples = append(a.a.pendingSamples, Sample{
+ MF: opts.MetricFamilyName,
+ M: opts.Metadata,
+ L: ls,
+ ST: st, T: t,
+ V: v, H: h, FH: fh,
+ ES: es,
+ })
a.a.mtx.Unlock()
if a.next != nil {
- return a.next.Rollback()
+ ref, err = a.next.Append(ref, ls, st, t, v, h, fh, opts)
+ if err != nil {
+ return 0, err
+ }
+ } else {
+ ref, err = computeOrCheckRef(ref, ls)
+ if err != nil {
+ return ref, err
+ }
}
- return nil
+ return ref, partialErr
}
diff --git a/util/teststorage/appender_test.go b/util/teststorage/appender_test.go
index 8c2a825c3a..41260ba43f 100644
--- a/util/teststorage/appender_test.go
+++ b/util/teststorage/appender_test.go
@@ -15,80 +15,220 @@ package teststorage
import (
"errors"
- "fmt"
+ "math"
"testing"
- "github.com/google/go-cmp/cmp"
+ "github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
"github.com/prometheus/prometheus/model/exemplar"
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/model/metadata"
+ "github.com/prometheus/prometheus/model/value"
+ "github.com/prometheus/prometheus/storage"
+ "github.com/prometheus/prometheus/tsdb/tsdbutil"
"github.com/prometheus/prometheus/util/testutil"
)
-// TestSample_RequireEqual ensures standard testutil.RequireEqual is enough for comparisons.
-// This is thanks to the fact metadata has now Equals method.
+func testAppendableV1(t *testing.T, appTest *Appendable, a storage.Appendable) {
+ for _, commit := range []bool{true, false} {
+ appTest.ResultReset()
+
+ app := a.Appender(t.Context())
+
+ ref1, err := app.Append(0, labels.FromStrings(model.MetricNameLabel, "test_metric1", "app", "v1"), 1, 2)
+ require.NoError(t, err)
+
+ h := tsdbutil.GenerateTestHistogram(0)
+ _, err = app.AppendHistogram(0, labels.FromStrings(model.MetricNameLabel, "test_metric2", "app", "v1"), 2, h, nil)
+ require.NoError(t, err)
+
+ fh := tsdbutil.GenerateTestFloatHistogram(0)
+ _, err = app.AppendHistogram(0, labels.FromStrings(model.MetricNameLabel, "test_metric3", "app", "v1"), 3, nil, fh)
+ require.NoError(t, err)
+
+ // Update meta of first series.
+ m1 := metadata.Metadata{Type: "gauge", Unit: "", Help: "other help text"}
+ _, err = app.UpdateMetadata(ref1, labels.FromStrings(model.MetricNameLabel, "test_metric1", "app", "v1"), m1)
+ require.NoError(t, err)
+
+ // Add exemplars to the first series.
+ e1 := exemplar.Exemplar{Labels: labels.FromStrings(model.MetricNameLabel, "yolo"), HasTs: true, Ts: 1}
+ _, err = app.AppendExemplar(ref1, labels.FromStrings(model.MetricNameLabel, "test_metric1", "app", "v1"), e1)
+ require.NoError(t, err)
+
+ exp := []Sample{
+ {L: labels.FromStrings(model.MetricNameLabel, "test_metric1", "app", "v1"), M: m1, T: 1, V: 2, ES: []exemplar.Exemplar{e1}},
+ {L: labels.FromStrings(model.MetricNameLabel, "test_metric2", "app", "v1"), T: 2, H: h},
+ {L: labels.FromStrings(model.MetricNameLabel, "test_metric3", "app", "v1"), T: 3, FH: fh},
+ }
+ testutil.RequireEqual(t, exp, appTest.PendingSamples())
+ require.Nil(t, appTest.ResultSamples())
+ require.Nil(t, appTest.RolledbackSamples())
+
+ if commit {
+ require.NoError(t, app.Commit())
+ require.Nil(t, appTest.PendingSamples())
+ testutil.RequireEqual(t, exp, appTest.ResultSamples())
+ require.Nil(t, appTest.RolledbackSamples())
+ break
+ }
+
+ require.NoError(t, app.Rollback())
+ require.Nil(t, appTest.PendingSamples())
+ require.Nil(t, appTest.ResultSamples())
+ testutil.RequireEqual(t, exp, appTest.RolledbackSamples())
+ }
+}
+
+func testAppendableV2(t *testing.T, appTest *Appendable, a storage.AppendableV2) {
+ for _, commit := range []bool{true, false} {
+ appTest.ResultReset()
+
+ app := a.AppenderV2(t.Context())
+
+ m1 := metadata.Metadata{Type: "gauge", Unit: "", Help: "other help text"}
+ e1 := exemplar.Exemplar{Labels: labels.FromStrings(model.MetricNameLabel, "yolo"), HasTs: true, Ts: 1}
+ _, err := app.Append(0, labels.FromStrings(model.MetricNameLabel, "test_metric1", "app", "v2"), -1, 1, 2, nil, nil, storage.AOptions{
+ MetricFamilyName: "test_metric1",
+ Metadata: m1,
+ Exemplars: []exemplar.Exemplar{e1},
+ })
+ require.NoError(t, err)
+
+ h := tsdbutil.GenerateTestHistogram(0)
+ _, err = app.Append(0, labels.FromStrings(model.MetricNameLabel, "test_metric2", "app", "v2"), -2, 2, 0, h, nil, storage.AOptions{})
+ require.NoError(t, err)
+
+ fh := tsdbutil.GenerateTestFloatHistogram(0)
+ _, err = app.Append(0, labels.FromStrings(model.MetricNameLabel, "test_metric3", "app", "v2"), -3, 3, 0, nil, fh, storage.AOptions{})
+ require.NoError(t, err)
+
+ exp := []Sample{
+ {L: labels.FromStrings(model.MetricNameLabel, "test_metric1", "app", "v2"), MF: "test_metric1", M: m1, ST: -1, T: 1, V: 2, ES: []exemplar.Exemplar{e1}},
+ {L: labels.FromStrings(model.MetricNameLabel, "test_metric2", "app", "v2"), ST: -2, T: 2, H: h},
+ {L: labels.FromStrings(model.MetricNameLabel, "test_metric3", "app", "v2"), ST: -3, T: 3, FH: fh},
+ }
+ testutil.RequireEqual(t, exp, appTest.PendingSamples())
+ require.Nil(t, appTest.ResultSamples())
+ require.Nil(t, appTest.RolledbackSamples())
+
+ if commit {
+ require.NoError(t, app.Commit())
+ require.Nil(t, appTest.PendingSamples())
+ testutil.RequireEqual(t, exp, appTest.ResultSamples())
+ require.Nil(t, appTest.RolledbackSamples())
+ break
+ }
+
+ require.NoError(t, app.Rollback())
+ require.Nil(t, appTest.PendingSamples())
+ require.Nil(t, appTest.ResultSamples())
+ testutil.RequireEqual(t, exp, appTest.RolledbackSamples())
+ }
+}
+
+func TestAppendable(t *testing.T) {
+ appTest := NewAppendable()
+ testAppendableV1(t, appTest, appTest)
+ testAppendableV2(t, appTest, appTest)
+}
+
+func TestAppendable_Then(t *testing.T) {
+ nextAppTest := NewAppendable()
+ app := NewAppendable().Then(nextAppTest)
+
+ // Ensure next mock record all the appends when appending to app.
+ testAppendableV1(t, nextAppTest, app)
+ // Ensure next mock record all the appends when appending to app.
+ testAppendableV2(t, nextAppTest, app)
+}
+
+// TestSample_RequireEqual.
func TestSample_RequireEqual(t *testing.T) {
a := []Sample{
{},
- {L: labels.FromStrings("__name__", "test_metric_total"), M: metadata.Metadata{Type: "counter", Unit: "metric", Help: "some help text"}},
- {L: labels.FromStrings("__name__", "test_metric2", "foo", "bar"), M: metadata.Metadata{Type: "gauge", Unit: "", Help: "other help text"}, V: 123.123},
- {ES: []exemplar.Exemplar{{Labels: labels.FromStrings("__name__", "yolo")}}},
+ {L: labels.FromStrings(model.MetricNameLabel, "test_metric_total"), M: metadata.Metadata{Type: "counter", Unit: "metric", Help: "some help text"}},
+ {L: labels.FromStrings(model.MetricNameLabel, "test_metric2", "foo", "bar"), M: metadata.Metadata{Type: "gauge", Unit: "", Help: "other help text"}, V: 123.123},
+ {ES: []exemplar.Exemplar{{Labels: labels.FromStrings(model.MetricNameLabel, "yolo")}}},
}
- testutil.RequireEqual(t, a, a)
+ RequireEqual(t, a, a)
b1 := []Sample{
{},
- {L: labels.FromStrings("__name__", "test_metric_total"), M: metadata.Metadata{Type: "counter", Unit: "metric", Help: "some help text"}},
- {L: labels.FromStrings("__name__", "test_metric2_diff", "foo", "bar"), M: metadata.Metadata{Type: "gauge", Unit: "", Help: "other help text"}, V: 123.123}, // test_metric2_diff is different.
- {ES: []exemplar.Exemplar{{Labels: labels.FromStrings("__name__", "yolo")}}},
+ {L: labels.FromStrings(model.MetricNameLabel, "test_metric_total"), M: metadata.Metadata{Type: "counter", Unit: "metric", Help: "some help text"}},
+ {L: labels.FromStrings(model.MetricNameLabel, "test_metric2_diff", "foo", "bar"), M: metadata.Metadata{Type: "gauge", Unit: "", Help: "other help text"}, V: 123.123}, // test_metric2_diff is different.
+ {ES: []exemplar.Exemplar{{Labels: labels.FromStrings(model.MetricNameLabel, "yolo")}}},
}
- requireNotEqual(t, a, b1)
+ RequireNotEqual(t, a, b1)
b2 := []Sample{
{},
- {L: labels.FromStrings("__name__", "test_metric_total"), M: metadata.Metadata{Type: "counter", Unit: "metric", Help: "some help text"}},
- {L: labels.FromStrings("__name__", "test_metric2", "foo", "bar"), M: metadata.Metadata{Type: "gauge", Unit: "", Help: "other help text"}, V: 123.123},
- {ES: []exemplar.Exemplar{{Labels: labels.FromStrings("__name__", "yolo2")}}}, // exemplar is different.
+ {L: labels.FromStrings(model.MetricNameLabel, "test_metric_total"), M: metadata.Metadata{Type: "counter", Unit: "metric", Help: "some help text"}},
+ {L: labels.FromStrings(model.MetricNameLabel, "test_metric2", "foo", "bar"), M: metadata.Metadata{Type: "gauge", Unit: "", Help: "other help text"}, V: 123.123},
+ {ES: []exemplar.Exemplar{{Labels: labels.FromStrings(model.MetricNameLabel, "yolo2")}}}, // exemplar is different.
}
- requireNotEqual(t, a, b2)
+ RequireNotEqual(t, a, b2)
b3 := []Sample{
{},
- {L: labels.FromStrings("__name__", "test_metric_total"), M: metadata.Metadata{Type: "counter", Unit: "metric", Help: "some help text"}},
- {L: labels.FromStrings("__name__", "test_metric2", "foo", "bar"), M: metadata.Metadata{Type: "gauge", Unit: "", Help: "other help text"}, V: 123.123, T: 123}, // Timestamp is different.
- {ES: []exemplar.Exemplar{{Labels: labels.FromStrings("__name__", "yolo")}}},
+ {L: labels.FromStrings(model.MetricNameLabel, "test_metric_total"), M: metadata.Metadata{Type: "counter", Unit: "metric", Help: "some help text"}},
+ {L: labels.FromStrings(model.MetricNameLabel, "test_metric2", "foo", "bar"), M: metadata.Metadata{Type: "gauge", Unit: "", Help: "other help text"}, V: 123.123, T: 123}, // Timestamp is different.
+ {ES: []exemplar.Exemplar{{Labels: labels.FromStrings(model.MetricNameLabel, "yolo")}}},
}
- requireNotEqual(t, a, b3)
+ RequireNotEqual(t, a, b3)
b4 := []Sample{
{},
- {L: labels.FromStrings("__name__", "test_metric_total"), M: metadata.Metadata{Type: "counter", Unit: "metric", Help: "some help text"}},
- {L: labels.FromStrings("__name__", "test_metric2", "foo", "bar"), M: metadata.Metadata{Type: "gauge", Unit: "", Help: "other help text"}, V: 456.456}, // Value is different.
- {ES: []exemplar.Exemplar{{Labels: labels.FromStrings("__name__", "yolo")}}},
+ {L: labels.FromStrings(model.MetricNameLabel, "test_metric_total"), M: metadata.Metadata{Type: "counter", Unit: "metric", Help: "some help text"}},
+ {L: labels.FromStrings(model.MetricNameLabel, "test_metric2", "foo", "bar"), M: metadata.Metadata{Type: "gauge", Unit: "", Help: "other help text"}, V: 456.456}, // Value is different.
+ {ES: []exemplar.Exemplar{{Labels: labels.FromStrings(model.MetricNameLabel, "yolo")}}},
}
- requireNotEqual(t, a, b4)
+ RequireNotEqual(t, a, b4)
b5 := []Sample{
{},
- {L: labels.FromStrings("__name__", "test_metric_total"), M: metadata.Metadata{Type: "counter2", Unit: "metric", Help: "some help text"}}, // Different type.
- {L: labels.FromStrings("__name__", "test_metric2", "foo", "bar"), M: metadata.Metadata{Type: "gauge", Unit: "", Help: "other help text"}, V: 123.123},
- {ES: []exemplar.Exemplar{{Labels: labels.FromStrings("__name__", "yolo")}}},
+ {L: labels.FromStrings(model.MetricNameLabel, "test_metric_total"), M: metadata.Metadata{Type: "counter2", Unit: "metric", Help: "some help text"}}, // Different type.
+ {L: labels.FromStrings(model.MetricNameLabel, "test_metric2", "foo", "bar"), M: metadata.Metadata{Type: "gauge", Unit: "", Help: "other help text"}, V: 123.123},
+ {ES: []exemplar.Exemplar{{Labels: labels.FromStrings(model.MetricNameLabel, "yolo")}}},
}
- requireNotEqual(t, a, b5)
-}
+ RequireNotEqual(t, a, b5)
-// TODO(bwplotka): While this mimick testutil.RequireEqual just making it negative, this does not literally test
-// testutil.RequireEqual. Either build test suita that mocks `testing.TB` or get rid of testutil.RequireEqual somehow.
-func requireNotEqual(t testing.TB, a, b any) {
- t.Helper()
- if !cmp.Equal(a, b, cmp.Comparer(labels.Equal)) {
- return
+ // NaN comparison.
+ a = []Sample{
+ {},
+ {L: labels.FromStrings(model.MetricNameLabel, "test_metric_total"), M: metadata.Metadata{Type: "counter", Unit: "metric", Help: "some help text"}},
+ {L: labels.FromStrings(model.MetricNameLabel, "test_metric2", "foo", "bar"), M: metadata.Metadata{Type: "gauge", Unit: "", Help: "other help text"}, V: math.Float64frombits(value.StaleNaN)},
+ {ES: []exemplar.Exemplar{{Labels: labels.FromStrings(model.MetricNameLabel, "yolo")}}},
}
- require.Fail(t, fmt.Sprintf("Equal, but expected not: \n"+
- "a: %s\n"+
- "b: %s", a, b))
+ RequireEqual(t, a, a)
+
+ // NaN comparison with different order.
+ a = []Sample{
+ {},
+ {L: labels.FromStrings(model.MetricNameLabel, "test_metric_total"), M: metadata.Metadata{Type: "counter", Unit: "metric", Help: "some help text"}},
+ {L: labels.FromStrings(model.MetricNameLabel, "test_metric10", "foo", "bar"), M: metadata.Metadata{Type: "gauge", Unit: "", Help: "other help text"}, V: math.Float64frombits(value.StaleNaN)},
+ {L: labels.FromStrings(model.MetricNameLabel, "test_metric2", "foo", "bar"), M: metadata.Metadata{Type: "gauge", Unit: "", Help: "other help text"}, V: math.Float64frombits(value.StaleNaN)},
+ {ES: []exemplar.Exemplar{{Labels: labels.FromStrings(model.MetricNameLabel, "yolo")}}},
+ }
+ b6 := []Sample{
+ {},
+ {L: labels.FromStrings(model.MetricNameLabel, "test_metric_total"), M: metadata.Metadata{Type: "counter", Unit: "metric", Help: "some help text"}},
+ {L: labels.FromStrings(model.MetricNameLabel, "test_metric2", "foo", "bar"), M: metadata.Metadata{Type: "gauge", Unit: "", Help: "other help text"}, V: math.Float64frombits(value.StaleNaN)},
+ {L: labels.FromStrings(model.MetricNameLabel, "test_metric10", "foo", "bar"), M: metadata.Metadata{Type: "gauge", Unit: "", Help: "other help text"}, V: math.Float64frombits(value.StaleNaN)},
+ {ES: []exemplar.Exemplar{{Labels: labels.FromStrings(model.MetricNameLabel, "yolo")}}},
+ }
+ RequireEqual(t, a, b6)
+
+ // Not equal with NaNs.
+ b7 := []Sample{
+ {},
+ {L: labels.FromStrings(model.MetricNameLabel, "test_metric_total"), M: metadata.Metadata{Type: "counter", Unit: "metric", Help: "some help text"}},
+ {L: labels.FromStrings(model.MetricNameLabel, "test_metric10", "foo", "bar"), M: metadata.Metadata{Type: "gauge", Unit: "", Help: "other help text"}, V: math.Float64frombits(value.StaleNaN)},
+ {L: labels.FromStrings(model.MetricNameLabel, "test_metric2", "foo", "bar"), M: metadata.Metadata{Type: "gauge2", Unit: "", Help: "other help text"}, V: math.Float64frombits(value.StaleNaN)}, // metadata different
+ {ES: []exemplar.Exemplar{{Labels: labels.FromStrings(model.MetricNameLabel, "yolo")}}},
+ }
+ RequireNotEqual(t, a, b7)
}
func TestConcurrentAppender_ReturnsErrAppender(t *testing.T) {
@@ -129,3 +269,145 @@ func TestConcurrentAppender_ReturnsErrAppender(t *testing.T) {
require.Error(t, app.Commit())
require.Error(t, app.Rollback())
}
+
+func TestConcurrentAppenderV2_ReturnsErrAppender(t *testing.T) {
+ a := NewAppendable()
+
+ // Non-concurrent multiple use if fine.
+ app := a.AppenderV2(t.Context())
+ require.Equal(t, int32(1), a.openAppenders.Load())
+ require.NoError(t, app.Commit())
+ // Repeated commit fails.
+ require.Error(t, app.Commit())
+
+ app = a.AppenderV2(t.Context())
+ require.NoError(t, app.Rollback())
+ // Commit after rollback fails.
+ require.Error(t, app.Commit())
+
+ a.WithErrs(
+ nil,
+ nil,
+ errors.New("commit err"),
+ )
+ app = a.AppenderV2(t.Context())
+ require.Error(t, app.Commit())
+
+ a.WithErrs(nil, nil, nil)
+ app = a.AppenderV2(t.Context())
+ require.NoError(t, app.Commit())
+ require.Equal(t, int32(0), a.openAppenders.Load())
+
+ // Concurrent use should return appender that errors.
+ _ = a.AppenderV2(t.Context())
+ app = a.AppenderV2(t.Context())
+ _, err := app.Append(0, labels.EmptyLabels(), 0, 0, 0, nil, nil, storage.AOptions{})
+ require.Error(t, err)
+ require.Error(t, app.Commit())
+ require.Error(t, app.Rollback())
+}
+
+func TestReorderExpectedForStaleness(t *testing.T) {
+ testcases := []struct {
+ name string
+ inExpected []Sample
+ inGot []Sample
+ expected []Sample
+ }{
+ {
+ name: "no staleness markers",
+ inExpected: []Sample{
+ {L: labels.FromStrings("a", "1"), T: 1, V: 1},
+ {L: labels.FromStrings("a", "2"), T: 1, V: 2},
+ },
+ inGot: []Sample{
+ {L: labels.FromStrings("a", "2"), T: 1, V: 2},
+ {L: labels.FromStrings("a", "1"), T: 1, V: 1},
+ },
+ },
+ {
+ name: "with staleness markers",
+ inExpected: []Sample{
+ {L: labels.FromStrings("a", "1"), T: 1, V: 1},
+ {L: labels.FromStrings("a", "2"), T: 2, V: 2},
+ {L: labels.FromStrings("a", "3"), T: 3, V: math.Float64frombits(value.StaleNaN)},
+ {L: labels.FromStrings("a", "4"), T: 4, V: math.Float64frombits(value.StaleNaN)},
+ },
+ inGot: []Sample{
+ {L: labels.FromStrings("a", "1"), T: 1, V: 1},
+ {L: labels.FromStrings("a", "2"), T: 2, V: 2},
+ {L: labels.FromStrings("a", "3"), T: 3, V: math.Float64frombits(value.StaleNaN)},
+ {L: labels.FromStrings("a", "4"), T: 4, V: math.Float64frombits(value.StaleNaN)},
+ },
+ },
+ {
+ name: "with staleness markers wrong order",
+ inExpected: []Sample{
+ {L: labels.FromStrings("a", "1"), T: 1, V: 1},
+ {L: labels.FromStrings("a", "2"), T: 2, V: 2},
+ {L: labels.FromStrings("a", "3"), T: 3, V: math.Float64frombits(value.StaleNaN)},
+ {L: labels.FromStrings("a", "4"), T: 4, V: math.Float64frombits(value.StaleNaN)},
+ },
+ inGot: []Sample{
+ {L: labels.FromStrings("a", "2"), T: 2, V: 2},
+ {L: labels.FromStrings("a", "1"), T: 1, V: 1},
+ {L: labels.FromStrings("a", "4"), T: 4, V: math.Float64frombits(value.StaleNaN)},
+ {L: labels.FromStrings("a", "3"), T: 3, V: math.Float64frombits(value.StaleNaN)},
+ },
+ expected: []Sample{
+ {L: labels.FromStrings("a", "1"), T: 1, V: 1},
+ {L: labels.FromStrings("a", "2"), T: 2, V: 2},
+ {L: labels.FromStrings("a", "4"), T: 4, V: math.Float64frombits(value.StaleNaN)},
+ {L: labels.FromStrings("a", "3"), T: 3, V: math.Float64frombits(value.StaleNaN)},
+ },
+ },
+ {
+ name: "with staleness markers wrong order but not consecutive",
+ inExpected: []Sample{
+ {L: labels.FromStrings("a", "1"), T: 1, V: 1},
+ {L: labels.FromStrings("a", "3"), T: 3, V: math.Float64frombits(value.StaleNaN)},
+ {L: labels.FromStrings("a", "2"), T: 2, V: 2},
+ {L: labels.FromStrings("a", "4"), T: 4, V: math.Float64frombits(value.StaleNaN)},
+ },
+ inGot: []Sample{
+ {L: labels.FromStrings("a", "2"), T: 2, V: 2},
+ {L: labels.FromStrings("a", "1"), T: 1, V: 1},
+ {L: labels.FromStrings("a", "4"), T: 4, V: math.Float64frombits(value.StaleNaN)},
+ {L: labels.FromStrings("a", "3"), T: 3, V: math.Float64frombits(value.StaleNaN)},
+ },
+ expected: []Sample{
+ {L: labels.FromStrings("a", "1"), T: 1, V: 1},
+ {L: labels.FromStrings("a", "3"), T: 3, V: math.Float64frombits(value.StaleNaN)},
+ {L: labels.FromStrings("a", "2"), T: 2, V: 2},
+ {L: labels.FromStrings("a", "4"), T: 4, V: math.Float64frombits(value.StaleNaN)},
+ },
+ },
+ }
+ for _, tc := range testcases {
+ t.Run(tc.name, func(t *testing.T) {
+ if tc.expected == nil {
+ tc.expected = tc.inExpected
+ }
+ RequireEqual(t, tc.expected, reorderExpectedForStaleness(tc.inExpected, tc.inGot))
+ })
+ }
+}
+
+func TestSampleIsStale(t *testing.T) {
+ s1 := Sample{V: 1}
+ require.False(t, s1.IsStale())
+ s2 := Sample{V: math.Float64frombits(value.StaleNaN)}
+ require.True(t, s2.IsStale())
+ h := tsdbutil.GenerateTestHistogram(0)
+ h1 := Sample{V: math.Float64frombits(value.StaleNaN), H: h}
+ require.False(t, h1.IsStale()) // Histogram takes precedence over V.
+ h.Sum = math.Float64frombits(value.StaleNaN)
+ h2 := Sample{V: 1, H: h}
+ require.True(t, h2.IsStale())
+ fh := tsdbutil.GenerateTestFloatHistogram(0)
+ fh1 := Sample{V: math.Float64frombits(value.StaleNaN), H: h, FH: fh}
+ require.False(t, fh1.IsStale()) // FloatHistogram takes precedence over all.
+ fh.Sum = math.Float64frombits(value.StaleNaN)
+ fh2 := Sample{V: 1, H: tsdbutil.GenerateTestHistogram(1), FH: fh}
+ require.True(t, fh2.IsStale())
+}
diff --git a/util/teststorage/storage.go b/util/teststorage/storage.go
index 17efdda77d..dd83ff8763 100644
--- a/util/teststorage/storage.go
+++ b/util/teststorage/storage.go
@@ -16,6 +16,7 @@ package teststorage
import (
"fmt"
"os"
+ "testing"
"time"
"github.com/prometheus/client_golang/prometheus"
@@ -25,37 +26,36 @@ import (
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/storage"
"github.com/prometheus/prometheus/tsdb"
- "github.com/prometheus/prometheus/util/testutil"
)
+type Option func(opt *tsdb.Options)
+
// New returns a new TestStorage for testing purposes
// that removes all associated files on closing.
-func New(t testutil.T, outOfOrderTimeWindow ...int64) *TestStorage {
- stor, err := NewWithError(outOfOrderTimeWindow...)
+func New(t testing.TB, o ...Option) *TestStorage {
+ s, err := NewWithError(o...)
require.NoError(t, err)
- return stor
+ return s
}
// NewWithError returns a new TestStorage for user facing tests, which reports
// errors directly.
-func NewWithError(outOfOrderTimeWindow ...int64) (*TestStorage, error) {
- dir, err := os.MkdirTemp("", "test_storage")
- if err != nil {
- return nil, fmt.Errorf("opening test directory: %w", err)
- }
-
+func NewWithError(o ...Option) (*TestStorage, error) {
// Tests just load data for a series sequentially. Thus we
// need a long appendable window.
opts := tsdb.DefaultOptions()
opts.MinBlockDuration = int64(24 * time.Hour / time.Millisecond)
opts.MaxBlockDuration = int64(24 * time.Hour / time.Millisecond)
opts.RetentionDuration = 0
+ opts.OutOfOrderTimeWindow = 0
- // Set OutOfOrderTimeWindow if provided, otherwise use default (0)
- if len(outOfOrderTimeWindow) > 0 {
- opts.OutOfOrderTimeWindow = outOfOrderTimeWindow[0]
- } else {
- opts.OutOfOrderTimeWindow = 0 // Default value is zero
+ for _, opt := range o {
+ opt(opts)
+ }
+
+ dir, err := os.MkdirTemp("", "test_storage")
+ if err != nil {
+ return nil, fmt.Errorf("opening test directory: %w", err)
}
db, err := tsdb.Open(dir, nil, nil, opts, tsdb.NewDBStats())
diff --git a/util/testutil/directory.go b/util/testutil/directory.go
index 706007d322..b65a3f4fa0 100644
--- a/util/testutil/directory.go
+++ b/util/testutil/directory.go
@@ -60,21 +60,12 @@ type (
// their interactions.
temporaryDirectory struct {
path string
- tester T
+ tester testing.TB
}
callbackCloser struct {
fn func()
}
-
- // T implements the needed methods of testing.TB so that we do not need
- // to actually import testing (which has the side effect of adding all
- // the test flags, which we do not want in non-test binaries even if
- // they make use of these utilities for some reason).
- T interface {
- Errorf(format string, args ...any)
- FailNow()
- }
)
func (nilCloser) Close() {
@@ -113,7 +104,7 @@ func (t temporaryDirectory) Path() string {
// NewTemporaryDirectory creates a new temporary directory for transient POSIX
// activities.
-func NewTemporaryDirectory(name string, t T) (handler TemporaryDirectory) {
+func NewTemporaryDirectory(name string, t testing.TB) (handler TemporaryDirectory) {
var (
directory string
err error
diff --git a/web/api/v1/translate_ast.go b/web/api/v1/translate_ast.go
index 3cce0583f9..3c2bc09943 100644
--- a/web/api/v1/translate_ast.go
+++ b/web/api/v1/translate_ast.go
@@ -47,6 +47,10 @@ func translateAST(node parser.Expr) any {
"labels": sanitizeList(m.MatchingLabels),
"on": m.On,
"include": sanitizeList(m.Include),
+ "fillValues": map[string]*float64{
+ "lhs": m.FillValues.LHS,
+ "rhs": m.FillValues.RHS,
+ },
}
}
diff --git a/web/ui/mantine-ui/src/pages/query/ExplainViews/BinaryExpr/VectorVector.tsx b/web/ui/mantine-ui/src/pages/query/ExplainViews/BinaryExpr/VectorVector.tsx
index e70b7a3f3e..5c10357561 100644
--- a/web/ui/mantine-ui/src/pages/query/ExplainViews/BinaryExpr/VectorVector.tsx
+++ b/web/ui/mantine-ui/src/pages/query/ExplainViews/BinaryExpr/VectorVector.tsx
@@ -8,6 +8,7 @@ import {
MatchErrorType,
computeVectorVectorBinOp,
filteredSampleValue,
+ MaybeFilledInstantSample,
} from "../../../../promql/binOp";
import { formatNode, labelNameList } from "../../../../promql/format";
import {
@@ -177,11 +178,10 @@ const explanationText = (node: BinaryExpr): React.ReactNode => {
) : (
-
- group_{manySide}({labelNameList(matching.include)})
-
- : {matching.card} match. Each series from the {oneSide}-hand side is
- allowed to match with multiple series from the {manySide}-hand side.
+ group_{manySide}
+ ({labelNameList(matching.include)}) : {matching.card} match. Each
+ series from the {oneSide}-hand side is allowed to match with
+ multiple series from the {manySide}-hand side.
{matching.include.length !== 0 && (
<>
{" "}
@@ -192,6 +192,55 @@ const explanationText = (node: BinaryExpr): React.ReactNode => {
)}
)}
+ {(matching.fillValues.lhs !== null ||
+ matching.fillValues.rhs !== null) &&
+ (matching.fillValues.lhs === matching.fillValues.rhs ? (
+
+ fill (
+
+ {matching.fillValues.lhs}
+
+ ) : For series on either side missing a match, fill in the sample
+ value{" "}
+
+ {matching.fillValues.lhs}
+
+ .
+
+ ) : (
+ <>
+ {matching.fillValues.lhs !== null && (
+
+ fill_left (
+
+ {matching.fillValues.lhs}
+
+ ) : For series on the left-hand side missing a match, fill in
+ the sample value{" "}
+
+ {matching.fillValues.lhs}
+
+ .
+
+ )}
+
+ {matching.fillValues.rhs !== null && (
+
+ fill_right
+ (
+
+ {matching.fillValues.rhs}
+
+ ) : For series on the right-hand side missing a match, fill in
+ the sample value{" "}
+
+ {matching.fillValues.rhs}
+
+ .
+
+ )}
+ >
+ ))}
{node.bool && (
bool : Instead of
@@ -239,7 +288,12 @@ const explainError = (
matching: {
...(binOp.matching
? binOp.matching
- : { labels: [], on: false, include: [] }),
+ : {
+ labels: [],
+ on: false,
+ include: [],
+ fillValues: { lhs: null, rhs: null },
+ }),
card:
err.dupeSide === "left"
? vectorMatchCardinality.manyToOne
@@ -403,7 +457,7 @@ const VectorVectorBinaryExprExplainView: FC<
);
const matchGroupTable = (
- series: InstantSample[],
+ series: MaybeFilledInstantSample[],
seriesCount: number,
color: string,
colorOffset?: number
@@ -458,6 +512,11 @@ const VectorVectorBinaryExprExplainView: FC<
)}
format={true}
/>
+ {s.filled && (
+
+ no match, filling in default value
+
+ )}
{showSampleValues && (
diff --git a/web/ui/mantine-ui/src/pages/query/urlStateEncoding.test.ts b/web/ui/mantine-ui/src/pages/query/urlStateEncoding.test.ts
new file mode 100644
index 0000000000..aef8369cd5
--- /dev/null
+++ b/web/ui/mantine-ui/src/pages/query/urlStateEncoding.test.ts
@@ -0,0 +1,648 @@
+import {
+ parseTime,
+ formatTime,
+ decodePanelOptionsFromURLParams,
+ encodePanelOptionsToURLParams,
+} from "./urlStateEncoding";
+import { GraphDisplayMode, Panel } from "../../state/queryPageSlice";
+
+describe("parseTime", () => {
+ test("parses ISO date string correctly", () => {
+ expect(parseTime("2024-01-15 12:30:45")).toBe(1705321845000);
+ });
+
+ test("parses date-only string correctly", () => {
+ expect(parseTime("2024-01-01 00:00:00")).toBe(1704067200000);
+ });
+
+ test("parses date with different time values", () => {
+ expect(parseTime("2024-06-15 23:59:59")).toBe(1718495999000);
+ });
+});
+
+describe("formatTime", () => {
+ test("formats timestamp to expected string format", () => {
+ expect(formatTime(1705321845000)).toBe("2024-01-15 12:30:45");
+ });
+
+ test("formats midnight correctly", () => {
+ expect(formatTime(1704067200000)).toBe("2024-01-01 00:00:00");
+ });
+
+ test("formats end of day correctly", () => {
+ expect(formatTime(1718495999000)).toBe("2024-06-15 23:59:59");
+ });
+});
+
+describe("parseTime and formatTime roundtrip", () => {
+ test("roundtrip preserves time", () => {
+ const original = "2024-03-20 15:45:30";
+ const timestamp = parseTime(original);
+ expect(formatTime(timestamp)).toBe(original);
+ });
+});
+
+describe("decodePanelOptionsFromURLParams", () => {
+ test("returns empty array for empty query string", () => {
+ expect(decodePanelOptionsFromURLParams("")).toEqual([]);
+ });
+
+ test("returns empty array when no expr parameter exists", () => {
+ expect(decodePanelOptionsFromURLParams("?foo=bar")).toEqual([]);
+ });
+
+ test("decodes single panel with expr only", () => {
+ const panels = decodePanelOptionsFromURLParams("g0.expr=up");
+ expect(panels).toHaveLength(1);
+ expect(panels[0].expr).toBe("up");
+ });
+
+ test("decodes URL-encoded expression", () => {
+ const panels = decodePanelOptionsFromURLParams(
+ "g0.expr=rate(http_requests_total%5B5m%5D)"
+ );
+ expect(panels).toHaveLength(1);
+ expect(panels[0].expr).toBe("rate(http_requests_total[5m])");
+ });
+
+ test("decodes multiple panels", () => {
+ const panels = decodePanelOptionsFromURLParams(
+ "g0.expr=up&g1.expr=node_cpu_seconds_total"
+ );
+ expect(panels).toHaveLength(2);
+ expect(panels[0].expr).toBe("up");
+ expect(panels[1].expr).toBe("node_cpu_seconds_total");
+ });
+
+ test("decodes show_tree parameter", () => {
+ const panelsWithTree = decodePanelOptionsFromURLParams(
+ "g0.expr=up&g0.show_tree=1"
+ );
+ expect(panelsWithTree[0].showTree).toBe(true);
+
+ const panelsWithoutTree = decodePanelOptionsFromURLParams(
+ "g0.expr=up&g0.show_tree=0"
+ );
+ expect(panelsWithoutTree[0].showTree).toBe(false);
+ });
+
+ describe("tab parameter", () => {
+ test("decodes numeric tab value 0 as graph", () => {
+ const panels = decodePanelOptionsFromURLParams("g0.expr=up&g0.tab=0");
+ expect(panels[0].visualizer.activeTab).toBe("graph");
+ });
+
+ test("decodes numeric tab value 1 as table", () => {
+ const panels = decodePanelOptionsFromURLParams("g0.expr=up&g0.tab=1");
+ expect(panels[0].visualizer.activeTab).toBe("table");
+ });
+
+ test("decodes string tab value graph", () => {
+ const panels = decodePanelOptionsFromURLParams("g0.expr=up&g0.tab=graph");
+ expect(panels[0].visualizer.activeTab).toBe("graph");
+ });
+
+ test("decodes string tab value table", () => {
+ const panels = decodePanelOptionsFromURLParams("g0.expr=up&g0.tab=table");
+ expect(panels[0].visualizer.activeTab).toBe("table");
+ });
+
+ test("decodes string tab value explain", () => {
+ const panels = decodePanelOptionsFromURLParams(
+ "g0.expr=up&g0.tab=explain"
+ );
+ expect(panels[0].visualizer.activeTab).toBe("explain");
+ });
+ });
+
+ describe("display_mode parameter", () => {
+ test("decodes lines display mode", () => {
+ const panels = decodePanelOptionsFromURLParams(
+ "g0.expr=up&g0.display_mode=lines"
+ );
+ expect(panels[0].visualizer.displayMode).toBe(GraphDisplayMode.Lines);
+ });
+
+ test("decodes stacked display mode", () => {
+ const panels = decodePanelOptionsFromURLParams(
+ "g0.expr=up&g0.display_mode=stacked"
+ );
+ expect(panels[0].visualizer.displayMode).toBe(GraphDisplayMode.Stacked);
+ });
+
+ test("decodes heatmap display mode", () => {
+ const panels = decodePanelOptionsFromURLParams(
+ "g0.expr=up&g0.display_mode=heatmap"
+ );
+ expect(panels[0].visualizer.displayMode).toBe(GraphDisplayMode.Heatmap);
+ });
+ });
+
+ describe("legacy stacked parameter", () => {
+ test("decodes stacked=1 as stacked display mode", () => {
+ const panels = decodePanelOptionsFromURLParams("g0.expr=up&g0.stacked=1");
+ expect(panels[0].visualizer.displayMode).toBe(GraphDisplayMode.Stacked);
+ });
+
+ test("decodes stacked=0 as lines display mode", () => {
+ const panels = decodePanelOptionsFromURLParams("g0.expr=up&g0.stacked=0");
+ expect(panels[0].visualizer.displayMode).toBe(GraphDisplayMode.Lines);
+ });
+ });
+
+ test("decodes y_axis_min parameter", () => {
+ const panels = decodePanelOptionsFromURLParams(
+ "g0.expr=up&g0.y_axis_min=10.5"
+ );
+ expect(panels[0].visualizer.yAxisMin).toBe(10.5);
+ });
+
+ test("decodes empty y_axis_min as null", () => {
+ const panels = decodePanelOptionsFromURLParams("g0.expr=up&g0.y_axis_min=");
+ expect(panels[0].visualizer.yAxisMin).toBeNull();
+ });
+
+ test("decodes show_exemplars parameter", () => {
+ const panelsWithExemplars = decodePanelOptionsFromURLParams(
+ "g0.expr=up&g0.show_exemplars=1"
+ );
+ expect(panelsWithExemplars[0].visualizer.showExemplars).toBe(true);
+
+ const panelsWithoutExemplars = decodePanelOptionsFromURLParams(
+ "g0.expr=up&g0.show_exemplars=0"
+ );
+ expect(panelsWithoutExemplars[0].visualizer.showExemplars).toBe(false);
+ });
+
+ test("decodes range_input parameter", () => {
+ const panels = decodePanelOptionsFromURLParams(
+ "g0.expr=up&g0.range_input=2h"
+ );
+ expect(panels[0].visualizer.range).toBe(7200000); // 2 hours in ms
+ });
+
+ test("decodes end_input parameter", () => {
+ const panels = decodePanelOptionsFromURLParams(
+ "g0.expr=up&g0.end_input=2024-01-15%2012%3A30%3A45"
+ );
+ expect(panels[0].visualizer.endTime).toBe(1705321845000);
+ });
+
+ test("decodes moment_input parameter", () => {
+ const panels = decodePanelOptionsFromURLParams(
+ "g0.expr=up&g0.moment_input=2024-01-15%2012%3A30%3A45"
+ );
+ expect(panels[0].visualizer.endTime).toBe(1705321845000);
+ });
+
+ describe("legacy step_input parameter", () => {
+ test("decodes positive step_input as custom resolution", () => {
+ const panels = decodePanelOptionsFromURLParams(
+ "g0.expr=up&g0.step_input=15"
+ );
+ expect(panels[0].visualizer.resolution).toEqual({
+ type: "custom",
+ step: 15000,
+ });
+ });
+
+ test("ignores non-positive step_input", () => {
+ const panels = decodePanelOptionsFromURLParams(
+ "g0.expr=up&g0.step_input=0"
+ );
+ expect(panels[0].visualizer.resolution).toEqual({
+ type: "auto",
+ density: "medium",
+ });
+ });
+ });
+
+ describe("resolution parameters", () => {
+ test("decodes auto resolution with low density", () => {
+ const panels = decodePanelOptionsFromURLParams(
+ "g0.expr=up&g0.res_type=auto&g0.res_density=low"
+ );
+ expect(panels[0].visualizer.resolution).toEqual({
+ type: "auto",
+ density: "low",
+ });
+ });
+
+ test("decodes auto resolution with medium density", () => {
+ const panels = decodePanelOptionsFromURLParams(
+ "g0.expr=up&g0.res_type=auto&g0.res_density=medium"
+ );
+ expect(panels[0].visualizer.resolution).toEqual({
+ type: "auto",
+ density: "medium",
+ });
+ });
+
+ test("decodes auto resolution with high density", () => {
+ const panels = decodePanelOptionsFromURLParams(
+ "g0.expr=up&g0.res_type=auto&g0.res_density=high"
+ );
+ expect(panels[0].visualizer.resolution).toEqual({
+ type: "auto",
+ density: "high",
+ });
+ });
+
+ test("decodes fixed resolution", () => {
+ const panels = decodePanelOptionsFromURLParams(
+ "g0.expr=up&g0.res_type=fixed&g0.res_step=30"
+ );
+ expect(panels[0].visualizer.resolution).toEqual({
+ type: "fixed",
+ step: 30000,
+ });
+ });
+
+ test("decodes custom resolution", () => {
+ const panels = decodePanelOptionsFromURLParams(
+ "g0.expr=up&g0.res_type=custom&g0.res_step=60"
+ );
+ expect(panels[0].visualizer.resolution).toEqual({
+ type: "custom",
+ step: 60000,
+ });
+ });
+ });
+
+ test("decodes complex panel with all parameters", () => {
+ const queryString =
+ "g0.expr=rate(http_requests_total%5B5m%5D)" +
+ "&g0.show_tree=1" +
+ "&g0.tab=graph" +
+ "&g0.display_mode=stacked" +
+ "&g0.y_axis_min=0" +
+ "&g0.show_exemplars=1" +
+ "&g0.range_input=1h" +
+ "&g0.end_input=2024-01-15%2012%3A30%3A45" +
+ "&g0.res_type=fixed" +
+ "&g0.res_step=15";
+
+ const panels = decodePanelOptionsFromURLParams(queryString);
+ expect(panels).toHaveLength(1);
+ expect(panels[0].expr).toBe("rate(http_requests_total[5m])");
+ expect(panels[0].showTree).toBe(true);
+ expect(panels[0].visualizer.activeTab).toBe("graph");
+ expect(panels[0].visualizer.displayMode).toBe(GraphDisplayMode.Stacked);
+ expect(panels[0].visualizer.yAxisMin).toBe(0);
+ expect(panels[0].visualizer.showExemplars).toBe(true);
+ expect(panels[0].visualizer.range).toBe(3600000);
+ expect(panels[0].visualizer.endTime).toBe(1705321845000);
+ expect(panels[0].visualizer.resolution).toEqual({
+ type: "fixed",
+ step: 15000,
+ });
+ });
+});
+
+describe("encodePanelOptionsToURLParams", () => {
+ const createPanel = (overrides: Partial = {}): Panel => ({
+ id: "test-id",
+ expr: "up",
+ showTree: false,
+ showMetricsExplorer: false,
+ visualizer: {
+ activeTab: "table",
+ endTime: null,
+ range: 3600000,
+ resolution: { type: "auto", density: "medium" },
+ displayMode: GraphDisplayMode.Lines,
+ showExemplars: false,
+ yAxisMin: null,
+ },
+ ...overrides,
+ });
+
+ test("encodes single panel with basic settings", () => {
+ const panel = createPanel();
+ const params = encodePanelOptionsToURLParams([panel]);
+
+ expect(params.get("g0.expr")).toBe("up");
+ expect(params.get("g0.show_tree")).toBe("0");
+ expect(params.get("g0.tab")).toBe("table");
+ expect(params.get("g0.range_input")).toBe("1h");
+ expect(params.get("g0.display_mode")).toBe("lines");
+ expect(params.get("g0.show_exemplars")).toBe("0");
+ });
+
+ test("encodes multiple panels", () => {
+ const panel1 = createPanel({ expr: "up" });
+ const panel2 = createPanel({ expr: "node_cpu_seconds_total" });
+ const params = encodePanelOptionsToURLParams([panel1, panel2]);
+
+ expect(params.get("g0.expr")).toBe("up");
+ expect(params.get("g1.expr")).toBe("node_cpu_seconds_total");
+ });
+
+ test("encodes show_tree as 1 when true", () => {
+ const panel = createPanel({ showTree: true });
+ const params = encodePanelOptionsToURLParams([panel]);
+
+ expect(params.get("g0.show_tree")).toBe("1");
+ });
+
+ test("encodes different tab values", () => {
+ const graphPanel = createPanel({
+ visualizer: {
+ ...createPanel().visualizer,
+ activeTab: "graph",
+ },
+ });
+ const tablePanel = createPanel({
+ visualizer: {
+ ...createPanel().visualizer,
+ activeTab: "table",
+ },
+ });
+ const explainPanel = createPanel({
+ visualizer: {
+ ...createPanel().visualizer,
+ activeTab: "explain",
+ },
+ });
+
+ expect(encodePanelOptionsToURLParams([graphPanel]).get("g0.tab")).toBe(
+ "graph"
+ );
+ expect(encodePanelOptionsToURLParams([tablePanel]).get("g0.tab")).toBe(
+ "table"
+ );
+ expect(encodePanelOptionsToURLParams([explainPanel]).get("g0.tab")).toBe(
+ "explain"
+ );
+ });
+
+ test("encodes endTime when set", () => {
+ const panel = createPanel({
+ visualizer: {
+ ...createPanel().visualizer,
+ endTime: 1705321845000,
+ },
+ });
+ const params = encodePanelOptionsToURLParams([panel]);
+
+ expect(params.get("g0.end_input")).toBe("2024-01-15 12:30:45");
+ expect(params.get("g0.moment_input")).toBe("2024-01-15 12:30:45");
+ });
+
+ test("does not encode endTime when null", () => {
+ const panel = createPanel({
+ visualizer: {
+ ...createPanel().visualizer,
+ endTime: null,
+ },
+ });
+ const params = encodePanelOptionsToURLParams([panel]);
+
+ expect(params.has("g0.end_input")).toBe(false);
+ expect(params.has("g0.moment_input")).toBe(false);
+ });
+
+ test("encodes range_input in Prometheus duration format", () => {
+ const panel = createPanel({
+ visualizer: {
+ ...createPanel().visualizer,
+ range: 7200000, // 2 hours
+ },
+ });
+ const params = encodePanelOptionsToURLParams([panel]);
+
+ expect(params.get("g0.range_input")).toBe("2h");
+ });
+
+ describe("resolution encoding", () => {
+ test("encodes auto resolution with density", () => {
+ const panel = createPanel({
+ visualizer: {
+ ...createPanel().visualizer,
+ resolution: { type: "auto", density: "high" },
+ },
+ });
+ const params = encodePanelOptionsToURLParams([panel]);
+
+ expect(params.get("g0.res_type")).toBe("auto");
+ expect(params.get("g0.res_density")).toBe("high");
+ });
+
+ test("encodes fixed resolution with step", () => {
+ const panel = createPanel({
+ visualizer: {
+ ...createPanel().visualizer,
+ resolution: { type: "fixed", step: 30000 },
+ },
+ });
+ const params = encodePanelOptionsToURLParams([panel]);
+
+ expect(params.get("g0.res_type")).toBe("fixed");
+ expect(params.get("g0.res_step")).toBe("30");
+ });
+
+ test("encodes custom resolution with step", () => {
+ const panel = createPanel({
+ visualizer: {
+ ...createPanel().visualizer,
+ resolution: { type: "custom", step: 60000 },
+ },
+ });
+ const params = encodePanelOptionsToURLParams([panel]);
+
+ expect(params.get("g0.res_type")).toBe("custom");
+ expect(params.get("g0.res_step")).toBe("60");
+ });
+ });
+
+ test("encodes display_mode", () => {
+ const linesPanel = createPanel({
+ visualizer: {
+ ...createPanel().visualizer,
+ displayMode: GraphDisplayMode.Lines,
+ },
+ });
+ const stackedPanel = createPanel({
+ visualizer: {
+ ...createPanel().visualizer,
+ displayMode: GraphDisplayMode.Stacked,
+ },
+ });
+ const heatmapPanel = createPanel({
+ visualizer: {
+ ...createPanel().visualizer,
+ displayMode: GraphDisplayMode.Heatmap,
+ },
+ });
+
+ expect(
+ encodePanelOptionsToURLParams([linesPanel]).get("g0.display_mode")
+ ).toBe("lines");
+ expect(
+ encodePanelOptionsToURLParams([stackedPanel]).get("g0.display_mode")
+ ).toBe("stacked");
+ expect(
+ encodePanelOptionsToURLParams([heatmapPanel]).get("g0.display_mode")
+ ).toBe("heatmap");
+ });
+
+ test("encodes y_axis_min when set", () => {
+ const panel = createPanel({
+ visualizer: {
+ ...createPanel().visualizer,
+ yAxisMin: 10.5,
+ },
+ });
+ const params = encodePanelOptionsToURLParams([panel]);
+
+ expect(params.get("g0.y_axis_min")).toBe("10.5");
+ });
+
+ test("does not encode y_axis_min when null", () => {
+ const panel = createPanel({
+ visualizer: {
+ ...createPanel().visualizer,
+ yAxisMin: null,
+ },
+ });
+ const params = encodePanelOptionsToURLParams([panel]);
+
+ expect(params.has("g0.y_axis_min")).toBe(false);
+ });
+
+ test("encodes show_exemplars", () => {
+ const panelWithExemplars = createPanel({
+ visualizer: {
+ ...createPanel().visualizer,
+ showExemplars: true,
+ },
+ });
+ const panelWithoutExemplars = createPanel({
+ visualizer: {
+ ...createPanel().visualizer,
+ showExemplars: false,
+ },
+ });
+
+ expect(
+ encodePanelOptionsToURLParams([panelWithExemplars]).get(
+ "g0.show_exemplars"
+ )
+ ).toBe("1");
+ expect(
+ encodePanelOptionsToURLParams([panelWithoutExemplars]).get(
+ "g0.show_exemplars"
+ )
+ ).toBe("0");
+ });
+
+ test("encodes empty panels array", () => {
+ const params = encodePanelOptionsToURLParams([]);
+ expect(params.toString()).toBe("");
+ });
+});
+
+describe("encode and decode roundtrip", () => {
+ const createPanel = (overrides: Partial = {}): Panel => ({
+ id: "test-id",
+ expr: "up",
+ showTree: false,
+ showMetricsExplorer: false,
+ visualizer: {
+ activeTab: "table",
+ endTime: null,
+ range: 3600000,
+ resolution: { type: "auto", density: "medium" },
+ displayMode: GraphDisplayMode.Lines,
+ showExemplars: false,
+ yAxisMin: null,
+ },
+ ...overrides,
+ });
+
+ test("roundtrip preserves basic panel settings", () => {
+ const original = createPanel({
+ expr: "rate(http_requests_total[5m])",
+ showTree: true,
+ });
+ const encoded = encodePanelOptionsToURLParams([original]);
+ const decoded = decodePanelOptionsFromURLParams(encoded.toString());
+
+ expect(decoded).toHaveLength(1);
+ expect(decoded[0].expr).toBe(original.expr);
+ expect(decoded[0].showTree).toBe(original.showTree);
+ });
+
+ test("roundtrip preserves visualizer settings", () => {
+ const original = createPanel({
+ visualizer: {
+ activeTab: "graph",
+ endTime: 1705321845000,
+ range: 7200000,
+ resolution: { type: "fixed", step: 30000 },
+ displayMode: GraphDisplayMode.Stacked,
+ showExemplars: true,
+ yAxisMin: 0,
+ },
+ });
+ const encoded = encodePanelOptionsToURLParams([original]);
+ const decoded = decodePanelOptionsFromURLParams(encoded.toString());
+
+ expect(decoded).toHaveLength(1);
+ expect(decoded[0].visualizer.activeTab).toBe(original.visualizer.activeTab);
+ expect(decoded[0].visualizer.endTime).toBe(original.visualizer.endTime);
+ expect(decoded[0].visualizer.range).toBe(original.visualizer.range);
+ expect(decoded[0].visualizer.resolution).toEqual(
+ original.visualizer.resolution
+ );
+ expect(decoded[0].visualizer.displayMode).toBe(
+ original.visualizer.displayMode
+ );
+ expect(decoded[0].visualizer.showExemplars).toBe(
+ original.visualizer.showExemplars
+ );
+ expect(decoded[0].visualizer.yAxisMin).toBe(original.visualizer.yAxisMin);
+ });
+
+ test("roundtrip preserves multiple panels", () => {
+ const panels = [
+ createPanel({ expr: "up" }),
+ createPanel({ expr: "node_cpu_seconds_total", showTree: true }),
+ createPanel({
+ expr: "rate(http_requests_total[5m])",
+ visualizer: {
+ ...createPanel().visualizer,
+ activeTab: "graph",
+ displayMode: GraphDisplayMode.Heatmap,
+ },
+ }),
+ ];
+ const encoded = encodePanelOptionsToURLParams(panels);
+ const decoded = decodePanelOptionsFromURLParams(encoded.toString());
+
+ expect(decoded).toHaveLength(3);
+ expect(decoded[0].expr).toBe("up");
+ expect(decoded[1].expr).toBe("node_cpu_seconds_total");
+ expect(decoded[1].showTree).toBe(true);
+ expect(decoded[2].expr).toBe("rate(http_requests_total[5m])");
+ expect(decoded[2].visualizer.displayMode).toBe(GraphDisplayMode.Heatmap);
+ });
+
+ test("roundtrip preserves auto resolution with all densities", () => {
+ for (const density of ["low", "medium", "high"] as const) {
+ const original = createPanel({
+ visualizer: {
+ ...createPanel().visualizer,
+ resolution: { type: "auto", density },
+ },
+ });
+ const encoded = encodePanelOptionsToURLParams([original]);
+ const decoded = decodePanelOptionsFromURLParams(encoded.toString());
+
+ expect(decoded[0].visualizer.resolution).toEqual({
+ type: "auto",
+ density,
+ });
+ }
+ });
+});
diff --git a/web/ui/mantine-ui/src/pages/query/urlStateEncoding.ts b/web/ui/mantine-ui/src/pages/query/urlStateEncoding.ts
index 18b63d9ed4..a20a6fae36 100644
--- a/web/ui/mantine-ui/src/pages/query/urlStateEncoding.ts
+++ b/web/ui/mantine-ui/src/pages/query/urlStateEncoding.ts
@@ -64,7 +64,7 @@ export const decodePanelOptionsFromURLParams = (query: string): Panel[] => {
value === "1" ? GraphDisplayMode.Stacked : GraphDisplayMode.Lines;
});
decodeSetting("y_axis_min", (value) => {
- panel.visualizer.yAxisMin = value === null ? null : parseFloat(value);
+ panel.visualizer.yAxisMin = value === "" ? null : parseFloat(value);
});
decodeSetting("show_exemplars", (value) => {
panel.visualizer.showExemplars = value === "1";
@@ -174,11 +174,11 @@ export const encodePanelOptionsToURLParams = (
}
addParam(idx, "display_mode", p.visualizer.displayMode);
- addParam(
- idx,
- "y_axis_min",
- p.visualizer.yAxisMin === null ? "" : p.visualizer.yAxisMin.toString()
- );
+
+ if (p.visualizer.yAxisMin !== null) {
+ addParam(idx, "y_axis_min", p.visualizer.yAxisMin.toString());
+ }
+
addParam(idx, "show_exemplars", p.visualizer.showExemplars ? "1" : "0");
});
diff --git a/web/ui/mantine-ui/src/promql/ast.ts b/web/ui/mantine-ui/src/promql/ast.ts
index 94872c6db0..9f8c5cb102 100644
--- a/web/ui/mantine-ui/src/promql/ast.ts
+++ b/web/ui/mantine-ui/src/promql/ast.ts
@@ -104,11 +104,16 @@ export interface LabelMatcher {
value: string;
}
+export interface FillValues {
+ lhs: number | null;
+ rhs: number | null;
+}
export interface VectorMatching {
card: vectorMatchCardinality;
labels: string[];
on: boolean;
include: string[];
+ fillValues: FillValues;
}
export type StartOrEnd = "start" | "end" | null;
diff --git a/web/ui/mantine-ui/src/promql/binOp.test.ts b/web/ui/mantine-ui/src/promql/binOp.test.ts
index 72ef16947b..76dd24fa79 100644
--- a/web/ui/mantine-ui/src/promql/binOp.test.ts
+++ b/web/ui/mantine-ui/src/promql/binOp.test.ts
@@ -81,6 +81,7 @@ const testCases: TestCase[] = [
on: false,
include: [],
labels: [],
+ fillValues: { lhs: null, rhs: null },
},
lhs: testMetricA,
rhs: testMetricB,
@@ -247,6 +248,7 @@ const testCases: TestCase[] = [
on: true,
include: [],
labels: ["label1", "label2"],
+ fillValues: { lhs: null, rhs: null },
},
lhs: testMetricA,
rhs: testMetricB,
@@ -413,6 +415,7 @@ const testCases: TestCase[] = [
on: false,
include: [],
labels: ["same"],
+ fillValues: { lhs: null, rhs: null },
},
lhs: testMetricA,
rhs: testMetricB,
@@ -579,6 +582,7 @@ const testCases: TestCase[] = [
on: false,
include: [],
labels: [],
+ fillValues: { lhs: null, rhs: null },
},
lhs: testMetricB,
rhs: testMetricC,
@@ -701,6 +705,7 @@ const testCases: TestCase[] = [
on: true,
include: [],
labels: ["label1"],
+ fillValues: { lhs: null, rhs: null },
},
lhs: testMetricB,
rhs: testMetricC,
@@ -791,6 +796,7 @@ const testCases: TestCase[] = [
on: true,
include: [],
labels: ["label1"],
+ fillValues: { lhs: null, rhs: null },
},
lhs: testMetricB,
rhs: testMetricC,
@@ -905,6 +911,7 @@ const testCases: TestCase[] = [
on: true,
include: [],
labels: ["label1"],
+ fillValues: { lhs: null, rhs: null },
},
lhs: testMetricC,
rhs: testMetricB,
@@ -1019,6 +1026,7 @@ const testCases: TestCase[] = [
on: true,
include: [],
labels: ["label1"],
+ fillValues: { lhs: null, rhs: null },
},
lhs: testMetricC,
rhs: testMetricB,
@@ -1107,6 +1115,7 @@ const testCases: TestCase[] = [
on: true,
include: [],
labels: ["label1"],
+ fillValues: { lhs: null, rhs: null },
},
lhs: testMetricA,
rhs: testMetricB,
@@ -1223,6 +1232,7 @@ const testCases: TestCase[] = [
on: false,
include: [],
labels: [],
+ fillValues: { lhs: null, rhs: null },
},
lhs: testMetricA,
rhs: testMetricB,
@@ -1409,6 +1419,7 @@ const testCases: TestCase[] = [
on: false,
include: [],
labels: [],
+ fillValues: { lhs: null, rhs: null },
},
lhs: testMetricA,
rhs: testMetricB,
@@ -1596,6 +1607,7 @@ const testCases: TestCase[] = [
on: false,
include: [],
labels: [],
+ fillValues: { lhs: null, rhs: null },
},
lhs: testMetricA,
rhs: testMetricB,
@@ -1763,6 +1775,7 @@ const testCases: TestCase[] = [
on: false,
include: [],
labels: [],
+ fillValues: { lhs: null, rhs: null },
},
lhs: testMetricA,
rhs: testMetricB,
@@ -1929,6 +1942,7 @@ const testCases: TestCase[] = [
on: false,
include: [],
labels: [],
+ fillValues: { lhs: null, rhs: null },
},
lhs: testMetricA,
rhs: testMetricB,
@@ -2022,6 +2036,7 @@ const testCases: TestCase[] = [
on: true,
include: [],
labels: ["label1"],
+ fillValues: { lhs: null, rhs: null },
},
lhs: testMetricB,
rhs: testMetricC,
@@ -2105,6 +2120,7 @@ const testCases: TestCase[] = [
on: true,
include: [],
labels: ["label1"],
+ fillValues: { lhs: null, rhs: null },
},
lhs: testMetricB,
rhs: testMetricC,
@@ -2147,6 +2163,437 @@ const testCases: TestCase[] = [
numGroups: 2,
},
},
+ {
+ // metric_a - fill(0) metric_b
+ desc: "subtraction with fill(0) but no missing series",
+ op: binaryOperatorType.sub,
+ matching: {
+ card: vectorMatchCardinality.oneToOne,
+ on: false,
+ include: [],
+ labels: [],
+ fillValues: { lhs: 0, rhs: 0 },
+ },
+ lhs: testMetricA,
+ rhs: testMetricB,
+ result: {
+ groups: {
+ [fnv1a(["a", "x", "same"])]: {
+ groupLabels: { label1: "a", label2: "x", same: "same" },
+ lhs: [
+ {
+ metric: {
+ __name__: "metric_a",
+ label1: "a",
+ label2: "x",
+ same: "same",
+ },
+ value: [0, "1"],
+ },
+ ],
+ lhsCount: 1,
+ rhs: [
+ {
+ metric: {
+ __name__: "metric_b",
+ label1: "a",
+ label2: "x",
+ same: "same",
+ },
+ value: [0, "10"],
+ },
+ ],
+ rhsCount: 1,
+ result: [
+ {
+ sample: {
+ metric: { label1: "a", label2: "x", same: "same" },
+ value: [0, "-9"],
+ },
+ manySideIdx: 0,
+ },
+ ],
+ error: null,
+ },
+ [fnv1a(["a", "y", "same"])]: {
+ groupLabels: { label1: "a", label2: "y", same: "same" },
+ lhs: [
+ {
+ metric: {
+ __name__: "metric_a",
+ label1: "a",
+ label2: "y",
+ same: "same",
+ },
+ value: [0, "2"],
+ },
+ ],
+ lhsCount: 1,
+ rhs: [
+ {
+ metric: {
+ __name__: "metric_b",
+ label1: "a",
+ label2: "y",
+ same: "same",
+ },
+ value: [0, "20"],
+ },
+ ],
+ rhsCount: 1,
+ result: [
+ {
+ sample: {
+ metric: { label1: "a", label2: "y", same: "same" },
+ value: [0, "-18"],
+ },
+ manySideIdx: 0,
+ },
+ ],
+ error: null,
+ },
+ [fnv1a(["b", "x", "same"])]: {
+ groupLabels: { label1: "b", label2: "x", same: "same" },
+ lhs: [
+ {
+ metric: {
+ __name__: "metric_a",
+ label1: "b",
+ label2: "x",
+ same: "same",
+ },
+ value: [0, "3"],
+ },
+ ],
+ lhsCount: 1,
+ rhs: [
+ {
+ metric: {
+ __name__: "metric_b",
+ label1: "b",
+ label2: "x",
+ same: "same",
+ },
+ value: [0, "30"],
+ },
+ ],
+ rhsCount: 1,
+ result: [
+ {
+ sample: {
+ metric: { label1: "b", label2: "x", same: "same" },
+ value: [0, "-27"],
+ },
+ manySideIdx: 0,
+ },
+ ],
+ error: null,
+ },
+ [fnv1a(["b", "y", "same"])]: {
+ groupLabels: { label1: "b", label2: "y", same: "same" },
+ lhs: [
+ {
+ metric: {
+ __name__: "metric_a",
+ label1: "b",
+ label2: "y",
+ same: "same",
+ },
+ value: [0, "4"],
+ },
+ ],
+ lhsCount: 1,
+ rhs: [
+ {
+ metric: {
+ __name__: "metric_b",
+ label1: "b",
+ label2: "y",
+ same: "same",
+ },
+ value: [0, "40"],
+ },
+ ],
+ rhsCount: 1,
+ result: [
+ {
+ sample: {
+ metric: { label1: "b", label2: "y", same: "same" },
+ value: [0, "-36"],
+ },
+ manySideIdx: 0,
+ },
+ ],
+ error: null,
+ },
+ },
+ numGroups: 4,
+ },
+ },
+ {
+ // metric_a[0..2] - fill_left(23) fill_right(42) metric_b[1...3]
+ desc: "subtraction with different fill values and missing series on each side",
+ op: binaryOperatorType.sub,
+ matching: {
+ card: vectorMatchCardinality.oneToOne,
+ on: false,
+ include: [],
+ labels: [],
+ fillValues: { lhs: 23, rhs: 42 },
+ },
+ lhs: testMetricA.slice(0, 3),
+ rhs: testMetricB.slice(1, 4),
+ result: {
+ groups: {
+ [fnv1a(["a", "x", "same"])]: {
+ groupLabels: { label1: "a", label2: "x", same: "same" },
+ lhs: [
+ {
+ metric: {
+ __name__: "metric_a",
+ label1: "a",
+ label2: "x",
+ same: "same",
+ },
+ value: [0, "1"],
+ },
+ ],
+ lhsCount: 1,
+ rhs: [
+ {
+ metric: {
+ label1: "a",
+ label2: "x",
+ same: "same",
+ },
+ value: [0, "42"],
+ filled: true,
+ },
+ ],
+ rhsCount: 1,
+ result: [
+ {
+ sample: {
+ metric: { label1: "a", label2: "x", same: "same" },
+ value: [0, "-41"],
+ },
+ manySideIdx: 0,
+ },
+ ],
+ error: null,
+ },
+ [fnv1a(["a", "y", "same"])]: {
+ groupLabels: { label1: "a", label2: "y", same: "same" },
+ lhs: [
+ {
+ metric: {
+ __name__: "metric_a",
+ label1: "a",
+ label2: "y",
+ same: "same",
+ },
+ value: [0, "2"],
+ },
+ ],
+ lhsCount: 1,
+ rhs: [
+ {
+ metric: {
+ __name__: "metric_b",
+ label1: "a",
+ label2: "y",
+ same: "same",
+ },
+ value: [0, "20"],
+ },
+ ],
+ rhsCount: 1,
+ result: [
+ {
+ sample: {
+ metric: { label1: "a", label2: "y", same: "same" },
+ value: [0, "-18"],
+ },
+ manySideIdx: 0,
+ },
+ ],
+ error: null,
+ },
+ [fnv1a(["b", "x", "same"])]: {
+ groupLabels: { label1: "b", label2: "x", same: "same" },
+ lhs: [
+ {
+ metric: {
+ __name__: "metric_a",
+ label1: "b",
+ label2: "x",
+ same: "same",
+ },
+ value: [0, "3"],
+ },
+ ],
+ lhsCount: 1,
+ rhs: [
+ {
+ metric: {
+ __name__: "metric_b",
+ label1: "b",
+ label2: "x",
+ same: "same",
+ },
+ value: [0, "30"],
+ },
+ ],
+ rhsCount: 1,
+ result: [
+ {
+ sample: {
+ metric: { label1: "b", label2: "x", same: "same" },
+ value: [0, "-27"],
+ },
+ manySideIdx: 0,
+ },
+ ],
+ error: null,
+ },
+ [fnv1a(["b", "y", "same"])]: {
+ groupLabels: { label1: "b", label2: "y", same: "same" },
+ lhs: [
+ {
+ metric: {
+ label1: "b",
+ label2: "y",
+ same: "same",
+ },
+ filled: true,
+ value: [0, "23"],
+ },
+ ],
+ lhsCount: 1,
+ rhs: [
+ {
+ metric: {
+ __name__: "metric_b",
+ label1: "b",
+ label2: "y",
+ same: "same",
+ },
+ value: [0, "40"],
+ },
+ ],
+ rhsCount: 1,
+ result: [
+ {
+ sample: {
+ metric: { label1: "b", label2: "y", same: "same" },
+ value: [0, "-17"],
+ },
+ manySideIdx: 0,
+ },
+ ],
+ error: null,
+ },
+ },
+ numGroups: 4,
+ },
+ },
+ {
+ // metric_b[0...1] - on(label1) group_left fill(0) metric_c
+ desc: "many-to-one matching with matching labels specified, group_left, and fill specified",
+ op: binaryOperatorType.sub,
+ matching: {
+ card: vectorMatchCardinality.manyToOne,
+ on: true,
+ include: [],
+ labels: ["label1"],
+ fillValues: { lhs: 0, rhs: 0 },
+ },
+ lhs: testMetricB.slice(0, 2),
+ rhs: testMetricC,
+ result: {
+ groups: {
+ [fnv1a(["a"])]: {
+ groupLabels: { label1: "a" },
+ lhs: [
+ {
+ metric: {
+ __name__: "metric_b",
+ label1: "a",
+ label2: "x",
+ same: "same",
+ },
+ value: [0, "10"],
+ },
+ {
+ metric: {
+ __name__: "metric_b",
+ label1: "a",
+ label2: "y",
+ same: "same",
+ },
+ value: [0, "20"],
+ },
+ ],
+ lhsCount: 2,
+ rhs: [
+ {
+ metric: { __name__: "metric_c", label1: "a" },
+ value: [0, "100"],
+ },
+ ],
+ rhsCount: 1,
+ result: [
+ {
+ sample: {
+ metric: { label1: "a", label2: "x", same: "same" },
+ value: [0, "-90"],
+ },
+ manySideIdx: 0,
+ },
+ {
+ sample: {
+ metric: { label1: "a", label2: "y", same: "same" },
+ value: [0, "-80"],
+ },
+ manySideIdx: 1,
+ },
+ ],
+ error: null,
+ },
+ [fnv1a(["b"])]: {
+ groupLabels: { label1: "b" },
+ lhs: [
+ {
+ metric: {
+ label1: "b",
+ },
+ filled: true,
+ value: [0, "0"],
+ },
+ ],
+ lhsCount: 1,
+ rhs: [
+ {
+ metric: { __name__: "metric_c", label1: "b" },
+ value: [0, "200"],
+ },
+ ],
+ rhsCount: 1,
+ result: [
+ {
+ sample: {
+ metric: { label1: "b" },
+ value: [0, "-200"],
+ },
+ manySideIdx: 0,
+ },
+ ],
+ error: null,
+ },
+ },
+ numGroups: 2,
+ },
+ },
{
// metric_a and metric b
desc: "and operator with no matching labels and matching groups",
@@ -2156,6 +2603,7 @@ const testCases: TestCase[] = [
on: false,
include: [],
labels: [],
+ fillValues: { lhs: null, rhs: null },
},
lhs: testMetricA,
rhs: testMetricB,
@@ -2342,6 +2790,7 @@ const testCases: TestCase[] = [
on: true,
include: [],
labels: ["label1"],
+ fillValues: { lhs: null, rhs: null },
},
lhs: testMetricA.slice(0, 3),
rhs: testMetricB.slice(1, 4),
@@ -2474,6 +2923,7 @@ const testCases: TestCase[] = [
on: true,
include: [],
labels: ["label1"],
+ fillValues: { lhs: null, rhs: null },
},
lhs: testMetricA.slice(0, 3),
rhs: testMetricB.slice(1, 4),
@@ -2568,6 +3018,7 @@ const testCases: TestCase[] = [
on: true,
include: [],
labels: ["label1"],
+ fillValues: { lhs: null, rhs: null },
},
lhs: testMetricA.slice(0, 3),
rhs: testMetricB.slice(1, 4),
@@ -2700,6 +3151,7 @@ const testCases: TestCase[] = [
on: false,
include: [],
labels: [],
+ fillValues: { lhs: null, rhs: null },
},
lhs: testMetricA.slice(0, 3),
rhs: testMetricB.slice(1, 4),
@@ -2886,6 +3338,7 @@ describe("binOp", () => {
on: true,
labels: ["label1"],
include: [],
+ fillValues: { lhs: null, rhs: null },
};
const result = resultMetric(lhs, rhs, op, matching);
@@ -2911,6 +3364,7 @@ describe("binOp", () => {
on: true,
labels: ["label1"],
include: [],
+ fillValues: { lhs: null, rhs: null },
};
const result = resultMetric(lhs, rhs, op, matching);
@@ -2931,6 +3385,7 @@ describe("binOp", () => {
on: true,
labels: ["label1"],
include: ["label2"],
+ fillValues: { lhs: null, rhs: null },
};
const result = resultMetric(lhs, rhs, op, matching);
diff --git a/web/ui/mantine-ui/src/promql/binOp.ts b/web/ui/mantine-ui/src/promql/binOp.ts
index dbfa64be2c..9ebee90f64 100644
--- a/web/ui/mantine-ui/src/promql/binOp.ts
+++ b/web/ui/mantine-ui/src/promql/binOp.ts
@@ -45,13 +45,18 @@ export type VectorMatchError =
| MultipleMatchesOnBothSidesError
| MultipleMatchesOnOneSideError;
+export type MaybeFilledInstantSample = InstantSample & {
+ // If the sample was filled in via a fill(...) modifier, this is true.
+ filled?: boolean;
+};
+
// A single match group as produced by a vector-to-vector binary operation, with all of its
// left-hand side and right-hand side series, as well as a result and error, if applicable.
export type BinOpMatchGroup = {
groupLabels: Metric;
- rhs: InstantSample[];
+ rhs: MaybeFilledInstantSample[];
rhsCount: number; // Number of samples before applying limits.
- lhs: InstantSample[];
+ lhs: MaybeFilledInstantSample[];
lhsCount: number; // Number of samples before applying limits.
result: {
sample: InstantSample;
@@ -338,6 +343,26 @@ export const computeVectorVectorBinOp = (
groups[sig].lhsCount++;
});
+ // Check for any LHS / RHS with no series and fill in default values, if specified.
+ Object.values(groups).forEach((mg) => {
+ if (mg.lhs.length === 0 && matching.fillValues.lhs !== null) {
+ mg.lhs.push({
+ metric: mg.groupLabels,
+ value: [0, formatPrometheusFloat(matching.fillValues.lhs as number)],
+ filled: true,
+ });
+ mg.lhsCount = 1;
+ }
+ if (mg.rhs.length === 0 && matching.fillValues.rhs !== null) {
+ mg.rhs.push({
+ metric: mg.groupLabels,
+ value: [0, formatPrometheusFloat(matching.fillValues.rhs as number)],
+ filled: true,
+ });
+ mg.rhsCount = 1;
+ }
+ });
+
// Annotate the match groups with errors (if any) and populate the results.
Object.values(groups).forEach((mg) => {
switch (matching.card) {
diff --git a/web/ui/mantine-ui/src/promql/format.tsx b/web/ui/mantine-ui/src/promql/format.tsx
index 75b1965b35..8602c65a82 100644
--- a/web/ui/mantine-ui/src/promql/format.tsx
+++ b/web/ui/mantine-ui/src/promql/format.tsx
@@ -265,6 +265,7 @@ const formatNodeInternal = (
case nodeType.binaryExpr: {
let matching = <>>;
let grouping = <>>;
+ let fill = <>>;
const vm = node.matching;
if (vm !== null) {
if (
@@ -305,6 +306,45 @@ const formatNodeInternal = (
>
);
}
+
+ const lfill = vm.fillValues.lhs;
+ const rfill = vm.fillValues.rhs;
+ if (lfill !== null || rfill !== null) {
+ if (lfill === rfill) {
+ fill = (
+ <>
+ {" "}
+ fill
+ (
+ {lfill}
+ )
+ >
+ );
+ } else {
+ fill = (
+ <>
+ {lfill !== null && (
+ <>
+ {" "}
+ fill_left
+ (
+ {lfill}
+ )
+ >
+ )}
+ {rfill !== null && (
+ <>
+ {" "}
+ fill_right
+ (
+ {rfill}
+ )
+ >
+ )}
+ >
+ );
+ }
+ }
}
return (
@@ -327,7 +367,8 @@ const formatNodeInternal = (
>
)}
{matching}
- {grouping}{" "}
+ {grouping}
+ {fill}{" "}
{showChildren &&
formatNode(
maybeParenthesizeBinopChild(node.op, node.rhs),
diff --git a/web/ui/mantine-ui/src/promql/serialize.ts b/web/ui/mantine-ui/src/promql/serialize.ts
index 584e1ae9ff..50c32c49e4 100644
--- a/web/ui/mantine-ui/src/promql/serialize.ts
+++ b/web/ui/mantine-ui/src/promql/serialize.ts
@@ -135,6 +135,7 @@ const serializeNode = (
case nodeType.binaryExpr: {
let matching = "";
let grouping = "";
+ let fill = "";
const vm = node.matching;
if (vm !== null) {
if (
@@ -152,11 +153,26 @@ const serializeNode = (
) {
grouping = ` group_${vm.card === vectorMatchCardinality.manyToOne ? "left" : "right"}(${labelNameList(vm.include)})`;
}
+
+ const lfill = vm.fillValues.lhs;
+ const rfill = vm.fillValues.rhs;
+ if (lfill !== null || rfill !== null) {
+ if (lfill === rfill) {
+ fill = ` fill(${lfill})`;
+ } else {
+ if (lfill !== null) {
+ fill += ` fill_left(${lfill})`;
+ }
+ if (rfill !== null) {
+ fill += ` fill_right(${rfill})`;
+ }
+ }
+ }
}
return `${serializeNode(maybeParenthesizeBinopChild(node.op, node.lhs), childIndent, pretty)}${childSeparator}${ind}${
node.op
- }${node.bool ? " bool" : ""}${matching}${grouping}${childSeparator}${serializeNode(
+ }${node.bool ? " bool" : ""}${matching}${grouping}${fill}${childSeparator}${serializeNode(
maybeParenthesizeBinopChild(node.op, node.rhs),
childIndent,
pretty
diff --git a/web/ui/mantine-ui/src/promql/serializeAndFormat.test.ts b/web/ui/mantine-ui/src/promql/serializeAndFormat.test.ts
index a3734d311f..f9ff039882 100644
--- a/web/ui/mantine-ui/src/promql/serializeAndFormat.test.ts
+++ b/web/ui/mantine-ui/src/promql/serializeAndFormat.test.ts
@@ -658,6 +658,7 @@ describe("serializeNode and formatNode", () => {
labels: [],
on: false,
include: [],
+ fillValues: { lhs: null, rhs: null },
},
bool: false,
},
@@ -677,6 +678,7 @@ describe("serializeNode and formatNode", () => {
labels: [],
on: true,
include: [],
+ fillValues: { lhs: null, rhs: null },
},
bool: false,
},
@@ -696,6 +698,7 @@ describe("serializeNode and formatNode", () => {
labels: ["label1", "label2"],
on: true,
include: [],
+ fillValues: { lhs: null, rhs: null },
},
bool: false,
},
@@ -715,6 +718,7 @@ describe("serializeNode and formatNode", () => {
labels: ["label1", "label2"],
on: false,
include: [],
+ fillValues: { lhs: null, rhs: null },
},
bool: false,
},
@@ -735,6 +739,7 @@ describe("serializeNode and formatNode", () => {
labels: [],
on: false,
include: [],
+ fillValues: { lhs: null, rhs: null },
},
bool: false,
},
@@ -755,6 +760,7 @@ describe("serializeNode and formatNode", () => {
labels: [],
on: false,
include: ["__name__"],
+ fillValues: { lhs: null, rhs: null },
},
bool: false,
},
@@ -774,6 +780,7 @@ describe("serializeNode and formatNode", () => {
labels: ["label1", "label2"],
on: true,
include: [],
+ fillValues: { lhs: null, rhs: null },
},
bool: false,
},
@@ -793,6 +800,7 @@ describe("serializeNode and formatNode", () => {
labels: ["label1", "label2"],
on: true,
include: ["label3"],
+ fillValues: { lhs: null, rhs: null },
},
bool: false,
},
@@ -812,6 +820,7 @@ describe("serializeNode and formatNode", () => {
labels: ["label1", "label2"],
on: true,
include: [],
+ fillValues: { lhs: null, rhs: null },
},
bool: false,
},
@@ -831,6 +840,7 @@ describe("serializeNode and formatNode", () => {
labels: ["label1", "label2"],
on: true,
include: ["label3"],
+ fillValues: { lhs: null, rhs: null },
},
bool: false,
},
@@ -864,6 +874,7 @@ describe("serializeNode and formatNode", () => {
labels: ["label1", "label2"],
on: true,
include: ["label3"],
+ fillValues: { lhs: null, rhs: null },
},
bool: true,
},
@@ -911,6 +922,7 @@ describe("serializeNode and formatNode", () => {
include: ["c", "ü"],
labels: ["b", "ö"],
on: true,
+ fillValues: { lhs: null, rhs: null },
},
op: binaryOperatorType.div,
rhs: {
@@ -948,6 +960,7 @@ describe("serializeNode and formatNode", () => {
include: [],
labels: ["e", "ö"],
on: false,
+ fillValues: { lhs: null, rhs: null },
},
op: binaryOperatorType.add,
rhs: {
diff --git a/web/ui/mantine-ui/src/promql/tools/go.mod b/web/ui/mantine-ui/src/promql/tools/go.mod
index 32b64019e9..a3abc881e2 100644
--- a/web/ui/mantine-ui/src/promql/tools/go.mod
+++ b/web/ui/mantine-ui/src/promql/tools/go.mod
@@ -1,6 +1,6 @@
module github.com/prometheus/prometheus/web/ui/mantine-ui/src/promql/tools
-go 1.24.9
+go 1.24.0
require (
github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853
diff --git a/web/ui/module/codemirror-promql/src/complete/promql.terms.ts b/web/ui/module/codemirror-promql/src/complete/promql.terms.ts
index d356268d74..3670fffff7 100644
--- a/web/ui/module/codemirror-promql/src/complete/promql.terms.ts
+++ b/web/ui/module/codemirror-promql/src/complete/promql.terms.ts
@@ -39,6 +39,10 @@ export const binOpModifierTerms = [
{ label: 'ignoring', info: 'Ignore specified labels for matching', type: 'keyword' },
{ label: 'group_left', info: 'Allow many-to-one matching', type: 'keyword' },
{ label: 'group_right', info: 'Allow one-to-many matching', type: 'keyword' },
+ { label: 'bool', info: 'Return boolean result (0 or 1) instead of filtering', type: 'keyword' },
+ { label: 'fill', info: 'Fill in missing series on both sides', type: 'keyword' },
+ { label: 'fill_left', info: 'Fill in missing series on the left side', type: 'keyword' },
+ { label: 'fill_right', info: 'Fill in missing series on the right side', type: 'keyword' },
];
export const atModifierTerms = [
diff --git a/web/ui/module/codemirror-promql/src/parser/vector.test.ts b/web/ui/module/codemirror-promql/src/parser/vector.test.ts
index f628206538..c6eeb930ab 100644
--- a/web/ui/module/codemirror-promql/src/parser/vector.test.ts
+++ b/web/ui/module/codemirror-promql/src/parser/vector.test.ts
@@ -15,29 +15,31 @@ import { buildVectorMatching } from './vector';
import { createEditorState } from '../test/utils-test';
import { BinaryExpr } from '@prometheus-io/lezer-promql';
import { syntaxTree } from '@codemirror/language';
-import { VectorMatchCardinality } from '../types';
+import { VectorMatchCardinality, VectorMatching } from '../types';
+
+const noFill = { fill: { lhs: null, rhs: null } };
describe('buildVectorMatching test', () => {
- const testCases = [
+ const testCases: { binaryExpr: string; expectedVectorMatching: VectorMatching }[] = [
{
binaryExpr: 'foo * bar',
- expectedVectorMatching: { card: VectorMatchCardinality.CardOneToOne, matchingLabels: [], on: false, include: [] },
+ expectedVectorMatching: { card: VectorMatchCardinality.CardOneToOne, matchingLabels: [], on: false, include: [], ...noFill },
},
{
binaryExpr: 'foo * sum',
- expectedVectorMatching: { card: VectorMatchCardinality.CardOneToOne, matchingLabels: [], on: false, include: [] },
+ expectedVectorMatching: { card: VectorMatchCardinality.CardOneToOne, matchingLabels: [], on: false, include: [], ...noFill },
},
{
binaryExpr: 'foo == 1',
- expectedVectorMatching: { card: VectorMatchCardinality.CardOneToOne, matchingLabels: [], on: false, include: [] },
+ expectedVectorMatching: { card: VectorMatchCardinality.CardOneToOne, matchingLabels: [], on: false, include: [], ...noFill },
},
{
binaryExpr: 'foo == bool 1',
- expectedVectorMatching: { card: VectorMatchCardinality.CardOneToOne, matchingLabels: [], on: false, include: [] },
+ expectedVectorMatching: { card: VectorMatchCardinality.CardOneToOne, matchingLabels: [], on: false, include: [], ...noFill },
},
{
binaryExpr: '2.5 / bar',
- expectedVectorMatching: { card: VectorMatchCardinality.CardOneToOne, matchingLabels: [], on: false, include: [] },
+ expectedVectorMatching: { card: VectorMatchCardinality.CardOneToOne, matchingLabels: [], on: false, include: [], ...noFill },
},
{
binaryExpr: 'foo and bar',
@@ -46,6 +48,7 @@ describe('buildVectorMatching test', () => {
matchingLabels: [],
on: false,
include: [],
+ ...noFill,
},
},
{
@@ -55,6 +58,7 @@ describe('buildVectorMatching test', () => {
matchingLabels: [],
on: false,
include: [],
+ ...noFill,
},
},
{
@@ -64,6 +68,7 @@ describe('buildVectorMatching test', () => {
matchingLabels: [],
on: false,
include: [],
+ ...noFill,
},
},
{
@@ -75,6 +80,7 @@ describe('buildVectorMatching test', () => {
matchingLabels: [],
on: false,
include: [],
+ ...noFill,
},
},
{
@@ -86,6 +92,7 @@ describe('buildVectorMatching test', () => {
matchingLabels: [],
on: false,
include: [],
+ ...noFill,
},
},
{
@@ -95,6 +102,7 @@ describe('buildVectorMatching test', () => {
matchingLabels: ['test', 'blub'],
on: true,
include: [],
+ ...noFill,
},
},
{
@@ -104,6 +112,7 @@ describe('buildVectorMatching test', () => {
matchingLabels: ['test', 'blub'],
on: true,
include: [],
+ ...noFill,
},
},
{
@@ -113,6 +122,7 @@ describe('buildVectorMatching test', () => {
matchingLabels: ['test', 'blub'],
on: true,
include: [],
+ ...noFill,
},
},
{
@@ -122,6 +132,7 @@ describe('buildVectorMatching test', () => {
matchingLabels: [],
on: true,
include: [],
+ ...noFill,
},
},
{
@@ -131,6 +142,7 @@ describe('buildVectorMatching test', () => {
matchingLabels: ['test', 'blub'],
on: false,
include: [],
+ ...noFill,
},
},
{
@@ -140,6 +152,7 @@ describe('buildVectorMatching test', () => {
matchingLabels: [],
on: false,
include: [],
+ ...noFill,
},
},
{
@@ -149,6 +162,7 @@ describe('buildVectorMatching test', () => {
matchingLabels: ['bar'],
on: true,
include: [],
+ ...noFill,
},
},
{
@@ -158,6 +172,7 @@ describe('buildVectorMatching test', () => {
matchingLabels: ['test', 'blub'],
on: true,
include: ['bar'],
+ ...noFill,
},
},
{
@@ -167,6 +182,7 @@ describe('buildVectorMatching test', () => {
matchingLabels: ['test', 'blub'],
on: false,
include: ['blub'],
+ ...noFill,
},
},
{
@@ -176,6 +192,7 @@ describe('buildVectorMatching test', () => {
matchingLabels: ['test', 'blub'],
on: false,
include: ['bar'],
+ ...noFill,
},
},
{
@@ -185,6 +202,7 @@ describe('buildVectorMatching test', () => {
matchingLabels: ['test', 'blub'],
on: true,
include: ['bar', 'foo'],
+ ...noFill,
},
},
{
@@ -194,6 +212,57 @@ describe('buildVectorMatching test', () => {
matchingLabels: ['test', 'blub'],
on: false,
include: ['bar', 'foo'],
+ ...noFill,
+ },
+ },
+ {
+ binaryExpr: 'foo + fill(23) bar',
+ expectedVectorMatching: {
+ card: VectorMatchCardinality.CardOneToOne,
+ matchingLabels: [],
+ on: false,
+ include: [],
+ fill: { lhs: 23, rhs: 23 },
+ },
+ },
+ {
+ binaryExpr: 'foo + fill_left(23) bar',
+ expectedVectorMatching: {
+ card: VectorMatchCardinality.CardOneToOne,
+ matchingLabels: [],
+ on: false,
+ include: [],
+ fill: { lhs: 23, rhs: null },
+ },
+ },
+ {
+ binaryExpr: 'foo + fill_right(23) bar',
+ expectedVectorMatching: {
+ card: VectorMatchCardinality.CardOneToOne,
+ matchingLabels: [],
+ on: false,
+ include: [],
+ fill: { lhs: null, rhs: 23 },
+ },
+ },
+ {
+ binaryExpr: 'foo + fill_left(23) fill_right(42) bar',
+ expectedVectorMatching: {
+ card: VectorMatchCardinality.CardOneToOne,
+ matchingLabels: [],
+ on: false,
+ include: [],
+ fill: { lhs: 23, rhs: 42 },
+ },
+ },
+ {
+ binaryExpr: 'foo + fill_right(23) fill_left(42) bar',
+ expectedVectorMatching: {
+ card: VectorMatchCardinality.CardOneToOne,
+ matchingLabels: [],
+ on: false,
+ include: [],
+ fill: { lhs: 42, rhs: 23 },
},
},
];
@@ -203,7 +272,7 @@ describe('buildVectorMatching test', () => {
const node = syntaxTree(state).topNode.getChild(BinaryExpr);
expect(node).toBeTruthy();
if (node) {
- expect(value.expectedVectorMatching).toEqual(buildVectorMatching(state, node));
+ expect(buildVectorMatching(state, node)).toEqual(value.expectedVectorMatching);
}
});
});
diff --git a/web/ui/module/codemirror-promql/src/parser/vector.ts b/web/ui/module/codemirror-promql/src/parser/vector.ts
index c47ca1fb76..9fc31bf5c6 100644
--- a/web/ui/module/codemirror-promql/src/parser/vector.ts
+++ b/web/ui/module/codemirror-promql/src/parser/vector.ts
@@ -24,6 +24,11 @@ import {
On,
Or,
Unless,
+ NumberDurationLiteral,
+ FillModifier,
+ FillClause,
+ FillLeftClause,
+ FillRightClause,
} from '@prometheus-io/lezer-promql';
import { VectorMatchCardinality, VectorMatching } from '../types';
import { containsAtLeastOneChild } from './path-finder';
@@ -37,6 +42,10 @@ export function buildVectorMatching(state: EditorState, binaryNode: SyntaxNode):
matchingLabels: [],
on: false,
include: [],
+ fill: {
+ lhs: null,
+ rhs: null,
+ },
};
const modifierClause = binaryNode.getChild(MatchingModifierClause);
if (modifierClause) {
@@ -60,6 +69,32 @@ export function buildVectorMatching(state: EditorState, binaryNode: SyntaxNode):
}
}
+ const fillModifier = binaryNode.getChild(FillModifier);
+ if (fillModifier) {
+ const fill = fillModifier.getChild(FillClause);
+ const fillLeft = fillModifier.getChild(FillLeftClause);
+ const fillRight = fillModifier.getChild(FillRightClause);
+
+ const getFillValue = (node: SyntaxNode) => {
+ const valueNode = node.getChild(NumberDurationLiteral);
+ return valueNode ? parseFloat(state.sliceDoc(valueNode.from, valueNode.to)) : null;
+ };
+
+ if (fill) {
+ const value = getFillValue(fill);
+ result.fill.lhs = value;
+ result.fill.rhs = value;
+ }
+
+ if (fillLeft) {
+ result.fill.lhs = getFillValue(fillLeft);
+ }
+
+ if (fillRight) {
+ result.fill.rhs = getFillValue(fillRight);
+ }
+ }
+
const isSetOperator = containsAtLeastOneChild(binaryNode, And, Or, Unless);
if (isSetOperator && result.card === VectorMatchCardinality.CardOneToOne) {
result.card = VectorMatchCardinality.CardManyToMany;
diff --git a/web/ui/module/codemirror-promql/src/types/vector.ts b/web/ui/module/codemirror-promql/src/types/vector.ts
index 4e7a4f4c45..709b0b76d6 100644
--- a/web/ui/module/codemirror-promql/src/types/vector.ts
+++ b/web/ui/module/codemirror-promql/src/types/vector.ts
@@ -18,6 +18,11 @@ export enum VectorMatchCardinality {
CardManyToMany = 'many-to-many',
}
+export interface FillValues {
+ lhs: number | null;
+ rhs: number | null;
+}
+
export interface VectorMatching {
// The cardinality of the two Vectors.
card: VectorMatchCardinality;
@@ -30,4 +35,6 @@ export interface VectorMatching {
// Include contains additional labels that should be included in
// the result from the side with the lower cardinality.
include: string[];
+ // Fill contains optional fill values for missing elements.
+ fill: FillValues;
}
diff --git a/web/ui/module/lezer-promql/src/promql.grammar b/web/ui/module/lezer-promql/src/promql.grammar
index 5fe8d4d025..9308ad01be 100644
--- a/web/ui/module/lezer-promql/src/promql.grammar
+++ b/web/ui/module/lezer-promql/src/promql.grammar
@@ -101,11 +101,30 @@ MatchingModifierClause {
((GroupLeft | GroupRight) (!group GroupingLabels)?)?
}
+FillClause {
+ Fill "(" NumberDurationLiteral ")"
+}
+
+FillLeftClause {
+ FillLeft "(" NumberDurationLiteral ")"
+}
+
+FillRightClause {
+ FillRight "(" NumberDurationLiteral ")"
+}
+
+FillModifier {
+ (FillClause | FillLeftClause | FillRightClause) |
+ (FillLeftClause FillRightClause) |
+ (FillRightClause FillLeftClause)
+}
+
BoolModifier { Bool }
binModifiers {
BoolModifier?
MatchingModifierClause?
+ FillModifier?
}
GroupingLabels {
@@ -366,7 +385,10 @@ NumberDurationLiteralInDurationContext {
Start,
End,
Smoothed,
- Anchored
+ Anchored,
+ Fill,
+ FillLeft,
+ FillRight
}
@external propSource promQLHighLight from "./highlight"
diff --git a/web/ui/module/lezer-promql/src/tokens.js b/web/ui/module/lezer-promql/src/tokens.js
index 523c306ae9..6fd681f1f8 100644
--- a/web/ui/module/lezer-promql/src/tokens.js
+++ b/web/ui/module/lezer-promql/src/tokens.js
@@ -12,82 +12,88 @@
// limitations under the License.
import {
- And,
- Avg,
- Atan2,
- Bool,
- Bottomk,
- By,
- Count,
- CountValues,
- End,
- Group,
- GroupLeft,
- GroupRight,
- Ignoring,
- inf,
- Max,
- Min,
- nan,
- Offset,
- On,
- Or,
- Quantile,
- LimitK,
- LimitRatio,
- Start,
- Stddev,
- Stdvar,
- Sum,
- Topk,
- Unless,
- Without,
- Smoothed,
- Anchored,
-} from './parser.terms.js';
+ And,
+ Avg,
+ Atan2,
+ Bool,
+ Bottomk,
+ By,
+ Count,
+ CountValues,
+ End,
+ Group,
+ GroupLeft,
+ GroupRight,
+ Ignoring,
+ inf,
+ Max,
+ Min,
+ nan,
+ Offset,
+ On,
+ Or,
+ Quantile,
+ LimitK,
+ LimitRatio,
+ Start,
+ Stddev,
+ Stdvar,
+ Sum,
+ Topk,
+ Unless,
+ Without,
+ Smoothed,
+ Anchored,
+ Fill,
+ FillLeft,
+ FillRight,
+} from "./parser.terms.js";
const keywordTokens = {
- inf: inf,
- nan: nan,
- bool: Bool,
- ignoring: Ignoring,
- on: On,
- group_left: GroupLeft,
- group_right: GroupRight,
- offset: Offset,
+ inf: inf,
+ nan: nan,
+ bool: Bool,
+ ignoring: Ignoring,
+ on: On,
+ group_left: GroupLeft,
+ group_right: GroupRight,
+ offset: Offset,
};
export const specializeIdentifier = (value, stack) => {
- return keywordTokens[value.toLowerCase()] || -1;
+ return keywordTokens[value.toLowerCase()] || -1;
};
const contextualKeywordTokens = {
- avg: Avg,
- atan2: Atan2,
- bottomk: Bottomk,
- count: Count,
- count_values: CountValues,
- group: Group,
- max: Max,
- min: Min,
- quantile: Quantile,
- limitk: LimitK,
- limit_ratio: LimitRatio,
- stddev: Stddev,
- stdvar: Stdvar,
- sum: Sum,
- topk: Topk,
- by: By,
- without: Without,
- and: And,
- or: Or,
- unless: Unless,
- start: Start,
- end: End,
- smoothed: Smoothed,
- anchored: Anchored,
+ avg: Avg,
+ atan2: Atan2,
+ bottomk: Bottomk,
+ count: Count,
+ count_values: CountValues,
+ group: Group,
+ max: Max,
+ min: Min,
+ quantile: Quantile,
+ limitk: LimitK,
+ limit_ratio: LimitRatio,
+ stddev: Stddev,
+ stdvar: Stdvar,
+ sum: Sum,
+ topk: Topk,
+ by: By,
+ without: Without,
+ and: And,
+ or: Or,
+ unless: Unless,
+ start: Start,
+ end: End,
+ smoothed: Smoothed,
+ anchored: Anchored,
+ fill: Fill,
+ fill_left: FillLeft,
+ fill_right: FillRight,
};
export const extendIdentifier = (value, stack) => {
- return contextualKeywordTokens[value.toLowerCase()] || -1;
+ return contextualKeywordTokens[value.toLowerCase()] || -1;
};
diff --git a/web/web.go b/web/web.go
index afe78e4255..4df447be64 100644
--- a/web/web.go
+++ b/web/web.go
@@ -634,8 +634,8 @@ func (h *Handler) testReady(f http.HandlerFunc) http.HandlerFunc {
case Ready:
f(w, r)
case NotReady:
- w.WriteHeader(http.StatusServiceUnavailable)
w.Header().Set("X-Prometheus-Stopping", "false")
+ w.WriteHeader(http.StatusServiceUnavailable)
fmt.Fprintf(w, "Service Unavailable")
case Stopping:
w.Header().Set("X-Prometheus-Stopping", "true")
diff --git a/web/web_test.go b/web/web_test.go
index ae7d532f1f..ce682912a9 100644
--- a/web/web_test.go
+++ b/web/web_test.go
@@ -140,11 +140,32 @@ func TestReadyAndHealthy(t *testing.T) {
resp, err = http.Get(u)
require.NoError(t, err)
require.Equal(t, http.StatusServiceUnavailable, resp.StatusCode)
+ require.Equal(t, "false", resp.Header.Get("X-Prometheus-Stopping"))
cleanupTestResponse(t, resp)
resp, err = http.Head(u)
require.NoError(t, err)
require.Equal(t, http.StatusServiceUnavailable, resp.StatusCode)
+ require.Equal(t, "false", resp.Header.Get("X-Prometheus-Stopping"))
+ cleanupTestResponse(t, resp)
+ }
+
+ // Set to stopping
+ webHandler.SetReady(Stopping)
+
+ for _, u := range []string{
+ baseURL + "/-/ready",
+ } {
+ resp, err = http.Get(u)
+ require.NoError(t, err)
+ require.Equal(t, http.StatusServiceUnavailable, resp.StatusCode)
+ require.Equal(t, "true", resp.Header.Get("X-Prometheus-Stopping"))
+ cleanupTestResponse(t, resp)
+
+ resp, err = http.Head(u)
+ require.NoError(t, err)
+ require.Equal(t, http.StatusServiceUnavailable, resp.StatusCode)
+ require.Equal(t, "true", resp.Header.Get("X-Prometheus-Stopping"))
cleanupTestResponse(t, resp)
}