diff --git a/.dockerignore b/.dockerignore
index 5eca8e1b80..c528ea1189 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -7,3 +7,4 @@ data/
!.build/linux-arm64/
!.build/linux-ppc64le/
!.build/linux-s390x/
+!.build/linux-riscv64/
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000000..432caee6f7
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+web/api/v1/testdata/openapi_golden.yaml linguist-generated
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
deleted file mode 100644
index 7f7cec9cda..0000000000
--- a/.github/CODEOWNERS
+++ /dev/null
@@ -1,10 +0,0 @@
-/web/ui @juliusv
-/web/ui/module @juliusv @nexucis
-/storage/remote @cstyan @bwplotka @tomwilkie
-/storage/remote/otlptranslator @aknuds1 @jesusvazquez
-/discovery/kubernetes @brancz
-/tsdb @jesusvazquez
-/promql @roidelapluie
-/cmd/promtool @dgl
-/documentation/prometheus-mixin @metalmatze
-
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index ec4eef8dae..7873822f26 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -28,6 +28,7 @@ If no, just write "NONE" in the release-notes block below.
Otherwise, please describe what should be mentioned in the CHANGELOG. Use the following prefixes:
[FEATURE] [ENHANCEMENT] [PERF] [BUGFIX] [SECURITY] [CHANGE]
Refer to the existing CHANGELOG for inspiration: https://github.com/prometheus/prometheus/blob/main/CHANGELOG.md
+A concrete example may look as follows (be sure to leave out the surrounding quotes): "[FEATURE] API: Add /api/v1/features for clients to understand which features are supported".
If you need help formulating your entries, consult the reviewer(s).
-->
```release-notes
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
deleted file mode 100644
index 191e07ffac..0000000000
--- a/.github/dependabot.yml
+++ /dev/null
@@ -1,27 +0,0 @@
-version: 2
-updates:
- - package-ecosystem: "docker"
- directory: "/"
- schedule:
- interval: "monthly"
- - package-ecosystem: "github-actions"
- directories:
- - "/"
- - "/scripts"
- schedule:
- interval: "monthly"
- - package-ecosystem: "gomod"
- directories:
- - "/"
- - "/documentation/examples/remote_storage"
- - "/internal/tools"
- schedule:
- interval: "monthly"
- groups:
- k8s.io:
- patterns:
- - "k8s.io/*"
- go.opentelemetry.io:
- patterns:
- - "go.opentelemetry.io/*"
- open-pull-requests-limit: 20
diff --git a/.github/workflows/automerge-dependabot.yml b/.github/workflows/automerge-dependabot.yml
index 616e4ee8b6..8b07f4df95 100644
--- a/.github/workflows/automerge-dependabot.yml
+++ b/.github/workflows/automerge-dependabot.yml
@@ -19,7 +19,7 @@ jobs:
steps:
- name: Dependabot metadata
id: metadata
- uses: dependabot/fetch-metadata@08eff52bf64351f401fb50d4972fa95b9f2c2d1b # v2.4.0
+ uses: dependabot/fetch-metadata@21025c705c08248db411dc16f3619e6b5f9ea21a # v2.5.0
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"
- name: Enable auto-merge for Dependabot PRs
diff --git a/.github/workflows/buf-lint.yml b/.github/workflows/buf-lint.yml
index 4e942f1f3b..b5d6ad3864 100644
--- a/.github/workflows/buf-lint.yml
+++ b/.github/workflows/buf-lint.yml
@@ -12,7 +12,7 @@ jobs:
name: lint
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: bufbuild/buf-setup-action@a47c93e0b1648d5651a065437926377d060baa99 # v1.50.0
diff --git a/.github/workflows/buf.yml b/.github/workflows/buf.yml
index add72cc89c..58a4c87b96 100644
--- a/.github/workflows/buf.yml
+++ b/.github/workflows/buf.yml
@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
if: github.repository_owner == 'prometheus'
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: bufbuild/buf-setup-action@a47c93e0b1648d5651a065437926377d060baa99 # v1.50.0
@@ -25,7 +25,7 @@ jobs:
with:
input: 'prompb'
against: 'https://github.com/prometheus/prometheus.git#branch=main,ref=HEAD~1,subdir=prompb'
- - uses: bufbuild/buf-push-action@a654ff18effe4641ebea4a4ce242c49800728459 # v1.1.1
+ - uses: bufbuild/buf-push-action@a654ff18effe4641ebea4a4ce242c49800728459 # v1.2.0
with:
input: 'prompb'
buf_token: ${{ secrets.BUF_TOKEN }}
diff --git a/.github/workflows/check_release_notes.yml b/.github/workflows/check_release_notes.yml
index b8381aff07..cfc4264602 100644
--- a/.github/workflows/check_release_notes.yml
+++ b/.github/workflows/check_release_notes.yml
@@ -20,7 +20,7 @@ jobs:
# Don't run it on dependabot PRs either as humans would take control in case a bump introduces a breaking change.
if: (github.repository_owner == 'prometheus' || github.repository_owner == 'prometheus-community') && github.event.pull_request.user.login != 'dependabot[bot]'
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- env:
PR_DESCRIPTION: ${{ github.event.pull_request.body }}
run: |
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ed4cfbf356..2482055fa2 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -3,6 +3,8 @@ name: CI
on:
pull_request:
push:
+ branches: [main, 'release-*']
+ tags: ['v*']
permissions:
contents: read
@@ -16,10 +18,10 @@ jobs:
# should also be updated.
image: quay.io/prometheus/golang-builder:1.25-base
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- - uses: prometheus/promci@443c7fc2397e946bc9f5029e313a9c3441b9b86d # v0.4.7
+ - uses: prometheus/promci@fc721ff8497a70a93a881cd552b71af7fb3a9d53 # v0.5.4
- uses: ./.github/promci/actions/setup_environment
with:
enable_npm: true
@@ -34,10 +36,10 @@ jobs:
container:
image: quay.io/prometheus/golang-builder:1.25-base
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- - uses: prometheus/promci@443c7fc2397e946bc9f5029e313a9c3441b9b86d # v0.4.7
+ - 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/...
@@ -57,9 +59,9 @@ jobs:
GOEXPERIMENT: synctest
container:
# The go version in this image should be N-1 wrt test_go.
- image: quay.io/prometheus/golang-builder:1.24-base
+ image: quay.io/prometheus/golang-builder:1.25-base
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- run: make build
@@ -78,10 +80,10 @@ jobs:
image: quay.io/prometheus/golang-builder:1.25-base
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- - uses: prometheus/promci@443c7fc2397e946bc9f5029e313a9c3441b9b86d # v0.4.7
+ - uses: prometheus/promci@fc721ff8497a70a93a881cd552b71af7fb3a9d53 # v0.5.4
- uses: ./.github/promci/actions/setup_environment
with:
enable_go: false
@@ -97,10 +99,10 @@ jobs:
name: Go tests on Windows
runs-on: windows-latest
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
+ - uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
with:
go-version: 1.25.x
- run: |
@@ -116,7 +118,7 @@ jobs:
container:
image: quay.io/prometheus/golang-builder:1.25-base
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- run: go install ./cmd/promtool/.
@@ -128,6 +130,27 @@ jobs:
- run: make -C documentation/prometheus-mixin
- run: git diff --exit-code
+ test-compliance:
+ name: Compliance testing
+ runs-on: ubuntu-latest
+ container:
+ # Whenever the Go version is updated here, .promu.yml
+ # should also be updated.
+ image: quay.io/prometheus/golang-builder:1.25-base
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ persist-credentials: false
+ - uses: prometheus/promci@fc721ff8497a70a93a881cd552b71af7fb3a9d53 # v0.5.4
+ - uses: ./.github/promci/actions/setup_environment
+ with:
+ enable_npm: false
+ # NOTE: Those tests are based on https://github.com/prometheus/compliance and
+ # are executed against the ./cmd/prometheus main package.
+ - run: go test -skip ${SKIP_TESTS} -v --tags=compliance ./compliance/...
+ env:
+ SKIP_TESTS: "TestRemoteWriteSender/prometheus/samples/rw2/start_timestamp*" # TODO(bwplotka): PROM-60
+
build:
name: Build Prometheus for common architectures
runs-on: ubuntu-latest
@@ -143,10 +166,10 @@ jobs:
matrix:
thread: [ 0, 1, 2 ]
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- - uses: prometheus/promci@443c7fc2397e946bc9f5029e313a9c3441b9b86d # v0.4.7
+ - 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"
@@ -170,10 +193,10 @@ jobs:
# Whenever the Go version is updated here, .promu.yml
# should also be updated.
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- - uses: prometheus/promci@443c7fc2397e946bc9f5029e313a9c3441b9b86d # v0.4.7
+ - uses: prometheus/promci@fc721ff8497a70a93a881cd552b71af7fb3a9d53 # v0.5.4
- uses: ./.github/promci/actions/build
with:
parallelism: 12
@@ -202,30 +225,32 @@ jobs:
if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}
run: exit 1
check_generated_parser:
+ # Checks generated parser and UI functions list. Not renaming as it is a required check.
name: Check generated parser
runs-on: ubuntu-latest
+ container:
+ image: quay.io/prometheus/golang-builder:1.25-base
steps:
- name: Checkout repository
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- - name: Install Go
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
+ - uses: prometheus/promci@fc721ff8497a70a93a881cd552b71af7fb3a9d53 # v0.5.4
+ - uses: ./.github/promci/actions/setup_environment
with:
- cache: false
- go-version: 1.25.x
- - name: Run goyacc and check for diff
- run: make install-goyacc check-generated-parser
+ enable_npm: true
+ - run: make install-goyacc check-generated-parser
+ - run: make check-generated-promql-functions
golangci:
name: golangci-lint
runs-on: ubuntu-latest
steps:
- name: Checkout repository
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Install Go
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
+ uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
with:
go-version: 1.25.x
- name: Install snmp_exporter/generator dependencies
@@ -235,21 +260,27 @@ jobs:
id: golangci-lint-version
run: echo "version=$(make print-golangci-lint-version)" >> $GITHUB_OUTPUT
- name: Lint
- uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0
+ uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0
with:
args: --verbose
version: ${{ steps.golangci-lint-version.outputs.version }}
- name: Lint with slicelabels
- uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0
+ uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0
with:
# goexperiment.synctest to ensure we don't miss files that depend on it.
args: --verbose --build-tags=slicelabels,goexperiment.synctest
version: ${{ steps.golangci-lint-version.outputs.version }}
- name: Lint with dedupelabels
- uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0
+ uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0
with:
args: --verbose --build-tags=dedupelabels
version: ${{ steps.golangci-lint-version.outputs.version }}
+ - name: Lint in documentation/examples/remote_storage
+ uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0
+ with:
+ args: --verbose
+ working-directory: documentation/examples/remote_storage
+ version: ${{ steps.golangci-lint-version.outputs.version }}
fuzzing:
uses: ./.github/workflows/fuzzing.yml
if: github.event_name == 'pull_request'
@@ -265,10 +296,10 @@ jobs:
needs: [test_ui, test_go, test_go_more, test_go_oldest, test_windows, golangci, codeql, build_all]
if: github.event_name == 'push' && github.event.ref == 'refs/heads/main'
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- - uses: prometheus/promci@443c7fc2397e946bc9f5029e313a9c3441b9b86d # v0.4.7
+ - uses: prometheus/promci@fc721ff8497a70a93a881cd552b71af7fb3a9d53 # v0.5.4
- uses: ./.github/promci/actions/publish_main
with:
docker_hub_login: ${{ secrets.docker_hub_login }}
@@ -284,10 +315,10 @@ jobs:
||
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v3.'))
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- - uses: prometheus/promci@443c7fc2397e946bc9f5029e313a9c3441b9b86d # v0.4.7
+ - uses: prometheus/promci@fc721ff8497a70a93a881cd552b71af7fb3a9d53 # v0.5.4
- uses: ./.github/promci/actions/publish_release
with:
docker_hub_login: ${{ secrets.docker_hub_login }}
@@ -301,16 +332,16 @@ jobs:
needs: [test_ui, codeql]
steps:
- name: Checkout
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- - uses: prometheus/promci@443c7fc2397e946bc9f5029e313a9c3441b9b86d # v0.4.7
+ - uses: prometheus/promci@fc721ff8497a70a93a881cd552b71af7fb3a9d53 # v0.5.4
- name: Install nodejs
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
+ uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version-file: "web/ui/.nvmrc"
registry-url: "https://registry.npmjs.org"
- - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
+ - uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 2e2143f4c8..21539d15d4 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -24,17 +24,17 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Initialize CodeQL
- uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
+ uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
with:
languages: ${{ matrix.language }}
- name: Autobuild
- uses: github/codeql-action/autobuild@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
+ uses: github/codeql-action/autobuild@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
+ uses: github/codeql-action/analyze@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
diff --git a/.github/workflows/container_description.yml b/.github/workflows/container_description.yml
index 7de8bb8da7..d7b879f9c7 100644
--- a/.github/workflows/container_description.yml
+++ b/.github/workflows/container_description.yml
@@ -18,7 +18,7 @@ jobs:
if: github.repository_owner == 'prometheus' || github.repository_owner == 'prometheus-community' # Don't run this workflow on forks.
steps:
- name: git checkout
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set docker hub repo name
@@ -42,7 +42,7 @@ jobs:
if: github.repository_owner == 'prometheus' || github.repository_owner == 'prometheus-community' # Don't run this workflow on forks.
steps:
- name: git checkout
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set quay.io org name
diff --git a/.github/workflows/fuzzing.yml b/.github/workflows/fuzzing.yml
index 3d3aa82d1c..f19e0ba609 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@cafd7a0eb8ecb4e007c56897996a9b65c49c972f # master
+ - name: Checkout repository
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
- oss-fuzz-project-name: "prometheus"
- dry-run: false
- - name: Run Fuzzers
- uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@cafd7a0eb8ecb4e007c56897996a9b65c49c972f # 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@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
with:
- oss-fuzz-project-name: "prometheus"
- fuzz-seconds: 600
- dry-run: false
- - name: Upload Crash
+ 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() && steps.build.outcome == 'success'
+ 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/.github/workflows/lock.yml b/.github/workflows/lock.yml
index e7e813e3b6..8f34aad204 100644
--- a/.github/workflows/lock.yml
+++ b/.github/workflows/lock.yml
@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
if: github.repository_owner == 'prometheus'
steps:
- - uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1
+ - uses: dessant/lock-threads@7266a7ce5c1df01b1c6db85bf8cd86c737dadbe7 # v6.0.0
with:
process-only: 'issues'
issue-inactive-days: '180'
diff --git a/.github/workflows/prombench.yml b/.github/workflows/prombench.yml
index 65d1d71917..5eb9becc0a 100644
--- a/.github/workflows/prombench.yml
+++ b/.github/workflows/prombench.yml
@@ -38,8 +38,8 @@ jobs:
uses: docker://prominfra/prombench:master
with:
args: >-
- until make all_nodes_deleted; do echo "waiting for nodepools to be deleted"; sleep 10; done;
make deploy;
+ until make all_nodes_running; do echo "waiting for nodepools to be created"; sleep 10; done;
- name: Update status to failure
if: failure()
run: >-
@@ -73,8 +73,8 @@ jobs:
uses: docker://prominfra/prombench:master
with:
args: >-
- until make all_nodes_running; do echo "waiting for nodepools to be created"; sleep 10; done;
make clean;
+ until make all_nodes_deleted; do echo "waiting for nodepools to be deleted"; sleep 10; done;
- name: Update status to failure
if: failure()
run: >-
@@ -108,10 +108,10 @@ jobs:
uses: docker://prominfra/prombench:master
with:
args: >-
- until make all_nodes_running; do echo "waiting for nodepools to be created"; sleep 10; done;
make clean;
until make all_nodes_deleted; do echo "waiting for nodepools to be deleted"; sleep 10; done;
make deploy;
+ until make all_nodes_running; do echo "waiting for nodepools to be created"; sleep 10; done;
- name: Update status to failure
if: failure()
run: >-
diff --git a/.github/workflows/repo_sync.yml b/.github/workflows/repo_sync.yml
index fea1422fdc..9752c98b51 100644
--- a/.github/workflows/repo_sync.yml
+++ b/.github/workflows/repo_sync.yml
@@ -14,7 +14,7 @@ jobs:
container:
image: quay.io/prometheus/golang-builder
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- run: ./scripts/sync_repo_files.sh
diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml
index 658e140f27..b54be91620 100644
--- a/.github/workflows/scorecards.yml
+++ b/.github/workflows/scorecards.yml
@@ -21,7 +21,7 @@ jobs:
steps:
- name: "Checkout code"
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # tag=v4.2.2
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
@@ -37,7 +37,7 @@ jobs:
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab.
- name: "Upload artifact"
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # tag=v5.0.0
+ uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # tag=v6.0.0
with:
name: SARIF file
path: results.sarif
@@ -45,6 +45,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
- uses: github/codeql-action/upload-sarif@0499de31b99561a6d14a36a5f662c2a54f91beee # tag=v4.31.2
+ uses: github/codeql-action/upload-sarif@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
with:
sarif_file: results.sarif
diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml
index 86deb94097..b29097c400 100644
--- a/.github/workflows/stale.yml
+++ b/.github/workflows/stale.yml
@@ -11,7 +11,7 @@ jobs:
if: github.repository_owner == 'prometheus' || github.repository_owner == 'prometheus-community' # Don't run this workflow on forks.
runs-on: ubuntu-latest
steps:
- - uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
+ - uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
# opt out of defaults to avoid marking issues as stale and closing them
diff --git a/.gitignore b/.gitignore
index 0d99305f69..f64f775993 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,6 +26,7 @@ npm_licenses.tar.bz2
/vendor
/.build
+/go.work.sum
/**/node_modules
diff --git a/.golangci.yml b/.golangci.yml
index 22c89a6beb..ff37050211 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -31,6 +31,7 @@ linters:
- govet
- loggercheck
- misspell
+ - modernize
- nilnesserr
# TODO(bwplotka): Enable once https://github.com/golangci/golangci-lint/issues/3228 is fixed.
# - nolintlint
@@ -38,6 +39,7 @@ linters:
- predeclared
- revive
- sloglint
+ - staticcheck
- testifylint
- unconvert
- unused
@@ -101,6 +103,10 @@ linters:
desc: "Use github.com/klauspost/compress instead of zlib"
- pkg: "golang.org/x/exp/slices"
desc: "Use 'slices' instead."
+ - pkg: "gopkg.in/yaml.v2"
+ desc: "Use go.yaml.in/yaml/v2 instead of gopkg.in/yaml.v2"
+ - pkg: "gopkg.in/yaml.v3"
+ desc: "Use go.yaml.in/yaml/v3 instead of gopkg.in/yaml.v3"
errcheck:
exclude-functions:
# Don't flag lines such as "io.Copy(io.Discard, resp.Body)".
@@ -117,6 +123,12 @@ linters:
- shadow
- fieldalignment
enable-all: true
+ modernize:
+ disable:
+ # Suggest replacing omitempty with omitzero for struct fields.
+ # Disable this check for now since it introduces too many changes in our existing codebase.
+ # See https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_omitzero for more details.
+ - omitzero
perfsprint:
# Optimizes even if it requires an int or uint type cast.
int-conversion: true
@@ -175,6 +187,11 @@ linters:
- name: unused-receiver
- name: var-declaration
- name: var-naming
+ # TODO(SuperQ): See: https://github.com/prometheus/prometheus/issues/17766
+ arguments:
+ - []
+ - []
+ - - skip-package-name-checks: true
testifylint:
disable:
- float-compare
diff --git a/.yamllint b/.yamllint
index 8d09c375fd..b329f464fb 100644
--- a/.yamllint
+++ b/.yamllint
@@ -2,6 +2,7 @@
extends: default
ignore: |
**/node_modules
+ web/api/v1/testdata/openapi_*_golden.yaml
rules:
braces:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3976ecbf81..a1afb0af59 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,9 +1,87 @@
# Changelog
-## main / unreleased
+## 3.9.1 / 2026-01-07
-* [BUGFIX] Discovery/Consul: Fix filter parameter not being applied to health service endpoint, causing Node and Node.Meta filters to be ignored. #16087
-* [FEATURE] Templates: Add urlQueryEscape to template functions. #17403
+ - [BUGFIX] Agent: fix crash shortly after startup from invalid type of object. #17802
+ - [BUGFIX] Scraping: fix relabel keep/drop not working. #17807
+
+## 3.9.0 / 2026-01-06
+
+- [CHANGE] Native Histograms are no longer experimental! Make the `native-histogram` feature flag a no-op. Use `scrape_native_histograms` config option instead. #17528
+- [CHANGE] API: Add maximum limit of 10,000 sets of statistics to TSDB status endpoint. #17647
+- [FEATURE] API: Add /api/v1/features for clients to understand which features are supported. #17427
+- [FEATURE] Promtool: Add `start_timestamp` field for unit tests. #17636
+- [FEATURE] Promtool: Add `--format seriesjson` option to `tsdb dump` to output just series labels in JSON format. #13409
+- [FEATURE] Add `--storage.tsdb.delay-compact-file.path` flag for better interoperability with Thanos. #17435
+- [FEATURE] UI: Add an option on the query drop-down menu to duplicate that query panel. #17714
+- [ENHANCEMENT]: TSDB: add flag `--storage.tsdb.block-reload-interval` to configure TSDB Block Reload Interval. #16728
+- [ENHANCEMENT] UI: Add graph option to start the chart's Y axis at zero. #17565
+- [ENHANCEMENT] Scraping: Classic protobuf format no longer requires the unit in the metric name. #16834
+- [ENHANCEMENT] PromQL, Rules, SD, Scraping: Add native histograms to complement existing summaries. #17374
+- [ENHANCEMENT] Notifications: Add a histogram `prometheus_notifications_latency_histogram_seconds` to complement the existing summary. #16637
+- [ENHANCEMENT] Remote-write: Add custom scope support for AzureAD authentication. #17483
+- [ENHANCEMENT] SD: add a `config` label with job name for most `prometheus_sd_refresh` metrics. #17138
+- [ENHANCEMENT] TSDB: New histogram `prometheus_tsdb_sample_ooo_delta`, the distribution of out-of-order samples in seconds. Collected for all samples, accepted or not. #17477
+- [ENHANCEMENT] Remote-read: Validate histograms received via remote-read. #17561
+- [PERF] TSDB: Small optimizations to postings index. #17439
+- [PERF] Scraping: Speed up relabelling of series. #17530
+- [PERF] PromQL: Small optimisations in binary operators. #17524, #17519.
+- [BUGFIX] UI: PromQL autocomplete now shows the correct type and HELP text for OpenMetrics counters whose samples end in `_total`. #17682
+- [BUGFIX] UI: Fixed codemirror-promql incorrectly showing label completion suggestions after the closing curly brace of a vector selector. #17602
+- [BUGFIX] UI: Query editor no longer suggests a duration unit if one is already present after a number. #17605
+- [BUGFIX] PromQL: Fix some "vector cannot contain metrics with the same labelset" errors when experimental delayed name removal is enabled. #17678
+- [BUGFIX] PromQL: Fix possible corruption of PromQL text if the query had an empty `ignoring()` and non-empty grouping. #17643
+- [BUGFIX] PromQL: Fix resets/changes to return empty results for anchored selectors when all samples are outside the range. #17479
+- [BUGFIX] PromQL: Check more consistently for many-to-one matching in filter binary operators. #17668
+- [BUGFIX] PromQL: Fix collision in unary negation with non-overlapping series. #17708
+- [BUGFIX] PromQL: Fix collision in label_join and label_replace with non-overlapping series. #17703
+- [BUGFIX] PromQL: Fix bug with inconsistent results for queries with OR expression when experimental delayed name removal is enabled. #17161
+- [BUGFIX] PromQL: Ensure that `rate`/`increase`/`delta` of histograms results in a gauge histogram. #17608
+- [BUGFIX] PromQL: Do not panic while iterating over invalid histograms. #17559
+- [BUGFIX] TSDB: Reject chunk files whose encoded chunk length overflows int. #17533
+- [BUGFIX] TSDB: Do not panic during resolution reduction of invalid histograms. #17561
+- [BUGFIX] Remote-write Receive: Avoid duplicate labels when experimental type-and-unit-label feature is enabled. #17546
+- [BUGFIX] OTLP Receiver: Only write metadata to disk when experimental metadata-wal-records feature is enabled. #17472
+
+## 3.8.1 / 2025-12-16
+
+* [BUGFIX] remote: Fix Remote Write receiver, so it does not send wrong response headers for v1 flow and cause Prometheus senders to emit false partial error log and metrics. #17683
+
+## 3.8.0 / 2025-11-28
+
+* [CHANGE] Remote-write: Update receiving to [2.0-rc.4 spec](https://github.com/prometheus/docs/blob/60c24e450010df38cfcb4f65df874f6f9b26dbcb/docs/specs/prw/remote_write_spec_2_0.md). "created timestamp" (CT) is now called "start timestamp" (ST). #17411
+* [CHANGE] TSDB: Native Histogram Custom Bounds with a NaN threshold are now rejected. #17287
+* [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_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
+* [FEATURE] Promtool: Add Remote-Write 2.0 support to `promtool push metrics` via the `--protobuf_message` flag. #17417
+* [ENHANCEMENT] Clarify the docs about handling negative native histograms. #17249
+* [ENHANCEMENT] Mixin: Add static UID to the remote-write dashboard. #17256
+* [ENHANCEMENT] PromQL: Reconcile mismatched NHCB bounds in `Add` and `Sub`. #17278
+* [ENHANCEMENT] Alerting: Add "unknown" state for alerting rules that haven't been evaluated yet. #17282
+* [ENHANCEMENT] Scrape: Allow simultaneous use of classic histogram → NHCB conversion and zero-timestamp ingestion. #17305
+* [ENHANCEMENT] UI: Add smoothed/anchored in explain. #17334
+* [ENHANCEMENT] OTLP: De-duplicate any `target_info` samples with the same timestamp for the same series. #17400
+* [ENHANCEMENT] Document `use_fips_sts_endpoint` in `sigv4` config sections. #17304
+* [ENHANCEMENT] Document Prometheus Agent. #14519
+* [PERF] PromQL: Speed up parsing of variadic functions. #17316
+* [PERF] UI: Speed up alerts/rules/... pages by not rendering collapsed content. #17485
+* [PERF] UI: Performance improvement when getting label name and values in promql editor. #17194
+* [PERF] UI: Speed up /alerts for many firing alerts via virtual scrolling. #17254
+* [BUGFIX] PromQL: Fix slice indexing bug in info function on churning series. #17199
+* [BUGFIX] API: Reduce lock contention on `/api/v1/targets`. #17306
+* [BUGFIX] PromQL: Consistent handling of gauge vs. counter histograms in aggregations. #17312
+* [BUGFIX] TSDB: Allow NHCB with -Inf as the first custom value. #17320
+* [BUGFIX] UI: Fix duplicate loading of data from the API speed up rendering of some pages. #17357
+* [BUGFIX] Old UI: Fix createExpressionLink to correctly build /graph URLs so links from Alerts/Rules work again. #17365
+* [BUGFIX] PromQL: Avoid panic when parsing malformed `info` call. #17379
+* [BUGFIX] PromQL: Include histograms when enforcing sample_limit. #17390
+* [BUGFIX] Config: Fix panic if TLS CA file is absent. #17418
+* [BUGFIX] PromQL: Fix `histogram_fraction` for classic histograms and NHCB if lower bound is in the first bucket. #17424
## 3.7.3 / 2025-10-29
@@ -201,7 +279,7 @@
## 3.2.1 / 2025-02-25
-* [BUGFIX] Don't send Accept` header `escape=allow-utf-8` when `metric_name_validation_scheme: legacy` is configured. #16061
+* [BUGFIX] Don't send `Accept` header `escape=allow-utf-8` when `metric_name_validation_scheme: legacy` is configured. #16061
## 3.2.0 / 2025-02-17
@@ -212,10 +290,10 @@
* [ENHANCEMENT] scrape: Add metadata for automatic metrics to WAL for `metadata-wal-records` feature. #15837
* [ENHANCEMENT] promtool: Support linting of scrape interval, through lint option `too-long-scrape-interval`. #15719
* [ENHANCEMENT] promtool: Add --ignore-unknown-fields option. #15706
-* [ENHANCEMENT] ui: Make "hide empty rules" and hide empty rules" persistent #15807
+* [ENHANCEMENT] ui: Make "hide empty rules" and "hide empty rules" persistent #15807
* [ENHANCEMENT] web/api: Add a limit parameter to `/query` and `/query_range`. #15552
* [ENHANCEMENT] api: Add fields Node and ServerTime to `/status`. #15784
-* [PERF] Scraping: defer computing labels for dropped targets until they are needed by the UI. #15261
+* [PERF] Scraping: defer computing labels for dropped targets until they are needed by the UI. #15261
* [BUGFIX] remotewrite2: Fix invalid metadata bug for metrics without metadata. #15829
* [BUGFIX] remotewrite2: Fix the unit field propagation. #15825
* [BUGFIX] scrape: Fix WAL metadata for histograms and summaries. #15832
@@ -232,9 +310,9 @@
* [ENHANCEMENT] TSDB: Improve calculation of space used by labels. #13880
* [ENHANCEMENT] Rules: new metric rule_group_last_rule_duration_sum_seconds. #15672
* [ENHANCEMENT] Observability: Export 'go_sync_mutex_wait_total_seconds_total' metric. #15339
- * [ENHANCEMEN] Remote-Write: optionally use a DNS resolver that picks a random IP. #15329
+ * [ENHANCEMENT] Remote-Write: optionally use a DNS resolver that picks a random IP. #15329
* [PERF] Optimize `l=~".+"` matcher. #15474, #15684
- * [PERF] TSDB: Cache all symbols for compaction . #15455
+ * [PERF] TSDB: Cache all symbols for compaction. #15455
* [PERF] TSDB: MemPostings: keep a map of label values slices. #15426
* [PERF] Remote-Write: Remove interning hook. #15456
* [PERF] Scrape: optimize string manipulation for experimental native histograms with custom buckets. #15453
diff --git a/CODEOWNERS b/CODEOWNERS
index c5b7f25349..2c5dedbffa 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -1,10 +1,29 @@
+#
+# Please keep this file in sync with the MAINTAINERS.md file!
+#
+
# Prometheus team members are members of the "default maintainers" github team.
# They are code owners by default for the whole repo.
* @prometheus/default-maintainers
-# Example adding a dedicated maintainer for AWS SD, and also "default
-# maintainers" so that they do not need to bypass codeowners check to merge
-# something.
-# Example comes from
-# https://github.com/prometheus/prometheus/pull/17105#issuecomment-3248209452
-# /discovery/aws/ @matt-gp @prometheus/default-maintainers
+# Subsystems.
+/Makefile @prometheus/default-maintainers @simonpasquier @SuperQ
+/cmd/promtool @prometheus/default-maintainers @dgl
+/documentation/prometheus-mixin @prometheus/default-maintainers @metalmatze
+/model/histogram @prometheus/default-maintainers @beorn7 @krajorama
+/web/ui @prometheus/default-maintainers @juliusv
+/web/ui/module @prometheus/default-maintainers @juliusv @nexucis
+/promql @prometheus/default-maintainers @roidelapluie
+/storage/remote @prometheus/default-maintainers @cstyan @bwplotka @tomwilkie @alexgreenbank
+/storage/remote/otlptranslator @prometheus/default-maintainers @aknuds1 @jesusvazquez @ArthurSens
+/tsdb @prometheus/default-maintainers @jesusvazquez @codesome @bwplotka @krajorama
+
+# Service discovery.
+/discovery/kubernetes @prometheus/default-maintainers @brancz
+/discovery/stackit @prometheus/default-maintainers @jkroepke
+/discovery/aws/ @prometheus/default-maintainers @matt-gp @sysadmind
+# Pending
+# https://github.com/prometheus/prometheus/pull/15212#issuecomment-3575225179
+# /discovery/aliyun @prometheus/default-maintainers @KeyOfSpectator
+# https://github.com/prometheus/prometheus/pull/14108#issuecomment-2639515421
+# /discovery/nomad @prometheus/default-maintainers @jaloren @jrasell
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 9b1b286ccf..cfb346e4d0 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -14,7 +14,7 @@ Prometheus uses GitHub to manage reviews of pull requests.
of inspiration. Also please see our [non-goals issue](https://github.com/prometheus/docs/issues/149) on areas that the Prometheus community doesn't plan to work on.
* Relevant coding style guidelines are the [Go Code Review
- Comments](https://code.google.com/p/go-wiki/wiki/CodeReviewComments)
+ Comments](https://go.dev/wiki/CodeReviewComments)
and the _Formatting and style_ section of Peter Bourgon's [Go: Best
Practices for Production
Environments](https://peter.bourgon.org/go-in-production/#formatting-and-style).
@@ -78,8 +78,7 @@ go get example.com/some/module/pkg@vX.Y.Z
Tidy up the `go.mod` and `go.sum` files:
```bash
-# The GO111MODULE variable can be omitted when the code isn't located in GOPATH.
-GO111MODULE=on go mod tidy
+go mod tidy
```
You have to commit the changes to `go.mod` and `go.sum` before submitting the pull request.
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/MAINTAINERS.md b/MAINTAINERS.md
index d36f82ca61..ae61059af5 100644
--- a/MAINTAINERS.md
+++ b/MAINTAINERS.md
@@ -1,9 +1,12 @@
# Maintainers
+## Please keep this file in sync with the CODEOWNERS file!
+
General maintainers:
* Bryan Boreham (bjboreham@gmail.com / @bboreham)
* Ayoub Mrini (ayoubmrini424@gmail.com / @machine424)
* Julien Pivotto (roidelapluie@prometheus.io / @roidelapluie)
+* György Krajcsovits ( / @krajorama)
Maintainers for specific parts of the codebase:
* `cmd`
@@ -13,15 +16,13 @@ Maintainers for specific parts of the codebase:
* `stackit`: Jan-Otto Kröpke ( / @jkroepke)
* `documentation`
* `prometheus-mixin`: Matthias Loibl ( / @metalmatze)
-* `model/histogram` and other code related to native histograms: Björn Rabenstein ( / @beorn7),
-George Krajcsovits ( / @krajorama)
* `storage`
- * `remote`: Callum Styan ( / @cstyan), Bartłomiej Płotka ( / @bwplotka), Tom Wilkie (tom.wilkie@gmail.com / @tomwilkie), Nicolás Pazos ( / @npazosmendez), Alex Greenbank ( / @alexgreenbank)
+ * `remote`: Callum Styan ( / @cstyan), Bartłomiej Płotka ( / @bwplotka), Tom Wilkie (tom.wilkie@gmail.com / @tomwilkie), Alex Greenbank ( / @alexgreenbank)
* `otlptranslator`: Arthur Silva Sens ( / @ArthurSens), Arve Knudsen ( / @aknuds1), Jesús Vázquez ( / @jesusvazquez)
* `tsdb`: Ganesh Vernekar ( / @codesome), Bartłomiej Płotka ( / @bwplotka), Jesús Vázquez ( / @jesusvazquez)
* `web`
* `ui`: Julius Volz ( / @juliusv)
- * `module`: Augustin Husson ( @nexucis)
+ * `module`: Augustin Husson ( / @nexucis)
* `Makefile` and related build configuration: Simon Pasquier ( / @simonpasquier), Ben Kochie ( / @SuperQ)
For the sake of brevity, not all subtrees are explicitly listed. Due to the
diff --git a/Makefile b/Makefile
index 43020998ef..ad4b90f020 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-# Copyright 2018 The Prometheus Authors
+# 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
@@ -12,7 +12,7 @@
# limitations under the License.
# Needs to be defined before including Makefile.common to auto-generate targets
-DOCKER_ARCHS ?= amd64 armv7 arm64 ppc64le s390x
+DOCKER_ARCHS ?= amd64 armv7 arm64 ppc64le riscv64 s390x
UI_PATH = web/ui
UI_NODE_MODULES_PATH = $(UI_PATH)/node_modules
@@ -79,6 +79,20 @@ ui-lint:
# new Mantine-based UI is fully integrated and the old app can be removed.
cd $(UI_PATH)/react-app && npm run lint
+.PHONY: generate-promql-functions
+generate-promql-functions: ui-install
+ @echo ">> generating PromQL function signatures"
+ @cd $(UI_PATH)/mantine-ui/src/promql/tools && $(GO) run ./gen_functions_list > ../functionSignatures.ts
+ @echo ">> generating PromQL function documentation"
+ @cd $(UI_PATH)/mantine-ui/src/promql/tools && $(GO) run ./gen_functions_docs $(CURDIR)/docs/querying/functions.md > ../functionDocs.tsx
+ @echo ">> formatting generated files"
+ @cd $(UI_PATH)/mantine-ui && npx prettier --write --print-width 120 src/promql/functionSignatures.ts src/promql/functionDocs.tsx
+
+.PHONY: check-generated-promql-functions
+check-generated-promql-functions: generate-promql-functions
+ @echo ">> checking generated PromQL functions"
+ @git diff --exit-code -- $(UI_PATH)/mantine-ui/src/promql/functionSignatures.ts $(UI_PATH)/mantine-ui/src/promql/functionDocs.tsx || (echo "Generated PromQL function files are out of date. Please run 'make generate-promql-functions' and commit the changes." && false)
+
.PHONY: assets
ifndef SKIP_UI_BUILD
assets: check-node-version ui-install ui-build
@@ -152,15 +166,8 @@ tarball: npm_licenses common-tarball
.PHONY: docker
docker: npm_licenses common-docker
-plugins/plugins.go: plugins.yml plugins/generate.go
- @echo ">> creating plugins list"
- $(GO) generate -tags plugins ./plugins
-
-.PHONY: plugins
-plugins: plugins/plugins.go
-
.PHONY: build
-build: assets npm_licenses assets-compress plugins common-build
+build: assets npm_licenses assets-compress common-build
.PHONY: bench_tsdb
bench_tsdb: $(PROMU)
@@ -184,14 +191,26 @@ check-go-mod-version:
@echo ">> checking go.mod version matching"
@./scripts/check-go-mod-version.sh
+.PHONY: update-features-testdata
+update-features-testdata:
+ @echo ">> updating features testdata"
+ @$(GO) test ./cmd/prometheus -run TestFeaturesAPI -update-features
+
+GO_SUBMODULE_DIRS := documentation/examples/remote_storage internal/tools web/ui/mantine-ui/src/promql/tools
+
.PHONY: update-all-go-deps
-update-all-go-deps:
- @$(MAKE) update-go-deps
- @echo ">> updating Go dependencies in ./documentation/examples/remote_storage/"
- @cd ./documentation/examples/remote_storage/ && for m in $$($(GO) list -mod=readonly -m -f '{{ if and (not .Indirect) (not .Main)}}{{.Path}}{{end}}' all); do \
+update-all-go-deps: update-go-deps
+ $(foreach dir,$(GO_SUBMODULE_DIRS),$(MAKE) update-go-deps-in-dir DIR=$(dir);)
+ @echo ">> syncing Go workspace"
+ @$(GO) work sync
+
+.PHONY: update-go-deps-in-dir
+update-go-deps-in-dir:
+ @echo ">> updating Go dependencies in ./$(DIR)/"
+ @cd ./$(DIR) && for m in $$($(GO) list -mod=readonly -m -f '{{ if and (not .Indirect) (not .Main)}}{{.Path}}{{end}}' all); do \
$(GO) get $$m; \
done
- @cd ./documentation/examples/remote_storage/ && $(GO) mod tidy
+ @cd ./$(DIR) && $(GO) mod tidy
.PHONY: check-node-version
check-node-version:
@@ -201,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 143bf03fbc..d19d390d37 100644
--- a/Makefile.common
+++ b/Makefile.common
@@ -1,4 +1,4 @@
-# Copyright 2018 The Prometheus Authors
+# 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
@@ -55,13 +55,13 @@ ifneq ($(shell command -v gotestsum 2> /dev/null),)
endif
endif
-PROMU_VERSION ?= 0.17.0
+PROMU_VERSION ?= 0.18.0
PROMU_URL := https://github.com/prometheus/promu/releases/download/v$(PROMU_VERSION)/promu-$(PROMU_VERSION).$(GO_BUILD_PLATFORM).tar.gz
SKIP_GOLANGCI_LINT :=
GOLANGCI_LINT :=
GOLANGCI_LINT_OPTS ?=
-GOLANGCI_LINT_VERSION ?= v2.6.0
+GOLANGCI_LINT_VERSION ?= v2.10.1
GOLANGCI_FMT_OPTS ?=
# golangci-lint only supports linux, darwin and windows platforms on i386/amd64/arm64.
# windows isn't included here because of the path separator being different.
@@ -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))
@@ -112,7 +133,7 @@ common-all: precheck style check_license lint yamllint unused build test
.PHONY: common-style
common-style:
@echo ">> checking code style"
- @fmtRes=$$($(GOFMT) -d $$(find . -path ./vendor -prune -o -name '*.go' -print)); \
+ @fmtRes=$$($(GOFMT) -d $$(git ls-files '*.go' ':!:vendor/*' || find . -path ./vendor -prune -o -name '*.go' -print)); \
if [ -n "$${fmtRes}" ]; then \
echo "gofmt checking failed!"; echo "$${fmtRes}"; echo; \
echo "Please ensure you are using $$($(GO) version) for formatting code."; \
@@ -122,13 +143,19 @@ common-style:
.PHONY: common-check_license
common-check_license:
@echo ">> checking license header"
- @licRes=$$(for file in $$(find . -type f -iname '*.go' ! -path './vendor/*') ; do \
+ @licRes=$$(for file in $$(git ls-files '*.go' ':!:vendor/*' || find . -path ./vendor -prune -o -type f -iname '*.go' -print) ; do \
awk 'NR<=3' $$file | grep -Eq "(Copyright|generated|GENERATED)" || echo $$file; \
done); \
if [ -n "$${licRes}" ]; then \
echo "license header checking failed:"; echo "$${licRes}"; \
exit 1; \
fi
+ @echo ">> checking for copyright years 2026 or later"
+ @futureYearRes=$$(git grep -E 'Copyright (202[6-9]|20[3-9][0-9])' -- '*.go' ':!:vendor/*' || true); \
+ if [ -n "$${futureYearRes}" ]; then \
+ echo "Files with copyright year 2026 or later found (should use 'Copyright The Prometheus Authors'):"; echo "$${futureYearRes}"; \
+ exit 1; \
+ fi
.PHONY: common-deps
common-deps:
@@ -220,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/README.md b/README.md
index 1743c5a4b8..030a827952 100644
--- a/README.md
+++ b/README.md
@@ -69,7 +69,7 @@ To build Prometheus from source code, You need:
* Go: Version specified in [go.mod](./go.mod) or greater.
* NodeJS: Version specified in [.nvmrc](./web/ui/.nvmrc) or greater.
-* npm: Version 8 or greater (check with `npm --version` and [here](https://www.npmjs.com/)).
+* npm: Version 10 or greater (check with `npm --version` and [here](https://www.npmjs.com/)).
Start by cloning the repository:
@@ -82,15 +82,15 @@ You can use the `go` tool to build and install the `prometheus`
and `promtool` binaries into your `GOPATH`:
```bash
-GO111MODULE=on go install github.com/prometheus/prometheus/cmd/...
+go install github.com/prometheus/prometheus/cmd/...
prometheus --config.file=your_config.yml
```
*However*, when using `go install` to build Prometheus, Prometheus will expect to be able to
-read its web assets from local filesystem directories under `web/ui/static` and
-`web/ui/templates`. In order for these assets to be found, you will have to run Prometheus
-from the root of the cloned repository. Note also that these directories do not include the
-React UI unless it has been built explicitly using `make assets` or `make build`.
+read its web assets from local filesystem directories under `web/ui/static`. In order for
+these assets to be found, you will have to run Prometheus from the root of the cloned
+repository. Note also that this directory does not include the React UI unless it has been
+built explicitly using `make assets` or `make build`.
An example of the above configuration file can be found [here.](https://github.com/prometheus/prometheus/blob/main/documentation/examples/prometheus.yml)
@@ -113,16 +113,31 @@ The Makefile provides several targets:
### Service discovery plugins
-Prometheus is bundled with many service discovery plugins.
-When building Prometheus from source, you can edit the [plugins.yml](./plugins.yml)
-file to disable some service discoveries. The file is a yaml-formatted list of go
-import path that will be built into the Prometheus binary.
+Prometheus is bundled with many service discovery plugins. You can customize
+which service discoveries are included in your build using Go build tags.
-After you have changed the file, you
-need to run `make build` again.
+To exclude service discoveries when building with `make build`, add the desired
+tags to the `.promu.yml` file under `build.tags.all`:
-If you are using another method to compile Prometheus, `make plugins` will
-generate the plugins file accordingly.
+```yaml
+build:
+ tags:
+ all:
+ - netgo
+ - builtinassets
+ - remove_all_sd # Exclude all optional SDs
+ - enable_kubernetes_sd # Re-enable only kubernetes
+```
+
+Then run `make build` as usual. Alternatively, when using `go build` directly:
+
+```bash
+go build -tags "remove_all_sd,enable_kubernetes_sd" ./cmd/prometheus
+```
+
+Available build tags:
+* `remove_all_sd` - Exclude all optional service discoveries (keeps file_sd, static_sd, and http_sd)
+* `enable__sd` - Re-enable a specific SD when using `remove_all_sd`
If you add out-of-tree plugins, which we do not endorse at the moment,
additional steps might be needed to adjust the `go.mod` and `go.sum` files. As
@@ -144,6 +159,15 @@ produce a fully working image when run locally.
## Using Prometheus as a Go Library
+Within the Prometheus project, repositories such as [prometheus/common](https://github.com/prometheus/common) and
+[prometheus/client-golang](https://github.com/prometheus/client-golang) are designed as re-usable libraries.
+
+The [prometheus/prometheus](https://github.com/prometheus/prometheus) repository builds a stand-alone program and is not
+designed for use as a library. We are aware that people do use parts as such,
+and we do not put any deliberate inconvenience in the way, but we want you to be
+aware that no care has been taken to make it work well as a library. For instance,
+you may encounter errors that only surface when used as a library.
+
### Remote Write
We are publishing our Remote Write protobuf independently at
diff --git a/RELEASE.md b/RELEASE.md
index 952f9f010d..5a8f8601ab 100644
--- a/RELEASE.md
+++ b/RELEASE.md
@@ -7,18 +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 | **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/VERSION b/VERSION
index c1e43e6d45..6bd10744ae 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-3.7.3
+3.9.1
diff --git a/cmd/prometheus/features_test.go b/cmd/prometheus/features_test.go
new file mode 100644
index 0000000000..5907c87247
--- /dev/null
+++ b/cmd/prometheus/features_test.go
@@ -0,0 +1,125 @@
+// 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 main
+
+import (
+ "bytes"
+ "encoding/json"
+ "flag"
+ "fmt"
+ "io"
+ "net/http"
+ "os"
+ "path/filepath"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/prometheus/prometheus/util/testutil"
+)
+
+var updateFeatures = flag.Bool("update-features", false, "update features.json golden file")
+
+func TestFeaturesAPI(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping test in short mode.")
+ }
+ t.Parallel()
+
+ tmpDir := t.TempDir()
+ configFile := filepath.Join(tmpDir, "prometheus.yml")
+ require.NoError(t, os.WriteFile(configFile, []byte{}, 0o644))
+
+ port := testutil.RandomUnprivilegedPort(t)
+ prom := prometheusCommandWithLogging(
+ t,
+ configFile,
+ port,
+ fmt.Sprintf("--storage.tsdb.path=%s", tmpDir),
+ )
+ require.NoError(t, prom.Start())
+
+ baseURL := fmt.Sprintf("http://127.0.0.1:%d", port)
+
+ // Wait for Prometheus to be ready.
+ require.Eventually(t, func() bool {
+ resp, err := http.Get(baseURL + "/-/ready")
+ if err != nil {
+ return false
+ }
+ defer resp.Body.Close()
+ return resp.StatusCode == http.StatusOK
+ }, 10*time.Second, 100*time.Millisecond, "Prometheus didn't become ready in time")
+
+ // Fetch features from the API.
+ resp, err := http.Get(baseURL + "/api/v1/features")
+ require.NoError(t, err)
+ defer resp.Body.Close()
+ require.Equal(t, http.StatusOK, resp.StatusCode)
+
+ body, err := io.ReadAll(resp.Body)
+ require.NoError(t, err)
+
+ // Parse API response.
+ var apiResponse struct {
+ Status string `json:"status"`
+ Data map[string]map[string]bool `json:"data"`
+ }
+ require.NoError(t, json.Unmarshal(body, &apiResponse))
+ require.Equal(t, "success", apiResponse.Status)
+
+ goldenPath := filepath.Join("testdata", "features.json")
+
+ // If update flag is set, write the current features to the golden file.
+ if *updateFeatures {
+ var buf bytes.Buffer
+ encoder := json.NewEncoder(&buf)
+ encoder.SetEscapeHTML(false)
+ encoder.SetIndent("", " ")
+ require.NoError(t, encoder.Encode(apiResponse.Data))
+ // Ensure testdata directory exists.
+ require.NoError(t, os.MkdirAll(filepath.Dir(goldenPath), 0o755))
+ require.NoError(t, os.WriteFile(goldenPath, buf.Bytes(), 0o644))
+ t.Logf("Updated golden file: %s", goldenPath)
+ return
+ }
+
+ // Load golden file.
+ goldenData, err := os.ReadFile(goldenPath)
+ require.NoError(t, err, "Failed to read golden file %s. Run 'make update-features-testdata' to generate it.", goldenPath)
+
+ var expectedFeatures map[string]map[string]bool
+ require.NoError(t, json.Unmarshal(goldenData, &expectedFeatures))
+
+ // The labels implementation depends on build tags (stringlabels, slicelabels, or dedupelabels).
+ // We need to update the expected features to match the current build.
+ if prometheusFeatures, ok := expectedFeatures["prometheus"]; ok {
+ // Remove all label implementation features from expected.
+ delete(prometheusFeatures, "stringlabels")
+ delete(prometheusFeatures, "slicelabels")
+ delete(prometheusFeatures, "dedupelabels")
+ // Add the current implementation.
+ if actualPrometheus, ok := apiResponse.Data["prometheus"]; ok {
+ for _, impl := range []string{"stringlabels", "slicelabels", "dedupelabels"} {
+ if actualPrometheus[impl] {
+ prometheusFeatures[impl] = true
+ }
+ }
+ }
+ }
+
+ // Compare the features data with the golden file.
+ require.Equal(t, expectedFeatures, apiResponse.Data, "Features mismatch. Run 'make update-features-testdata' to update the golden file.")
+}
diff --git a/cmd/prometheus/main.go b/cmd/prometheus/main.go
index d108e4c7a2..1835fa1eff 100644
--- a/cmd/prometheus/main.go
+++ b/cmd/prometheus/main.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
@@ -16,6 +16,7 @@ package main
import (
"context"
+ "encoding/json"
"errors"
"fmt"
"log/slog"
@@ -72,11 +73,13 @@ import (
"github.com/prometheus/prometheus/scrape"
"github.com/prometheus/prometheus/storage"
"github.com/prometheus/prometheus/storage/remote"
+ "github.com/prometheus/prometheus/template"
"github.com/prometheus/prometheus/tracing"
"github.com/prometheus/prometheus/tsdb"
"github.com/prometheus/prometheus/tsdb/agent"
"github.com/prometheus/prometheus/util/compression"
"github.com/prometheus/prometheus/util/documentcli"
+ "github.com/prometheus/prometheus/util/features"
"github.com/prometheus/prometheus/util/logging"
"github.com/prometheus/prometheus/util/notifications"
prom_runtime "github.com/prometheus/prometheus/util/runtime"
@@ -215,6 +218,8 @@ type flagConfig struct {
promqlEnableDelayedNameRemoval bool
+ parserOpts parser.Options
+
promslogConfig promslog.Config
}
@@ -230,11 +235,14 @@ func (c *flagConfig) setFeatureListOptions(logger *slog.Logger) error {
c.tsdb.EnableMemorySnapshotOnShutdown = true
logger.Info("Experimental memory snapshot on shutdown enabled")
case "extra-scrape-metrics":
- c.scrape.ExtraMetrics = true
- logger.Info("Experimental additional scrape metrics enabled")
+ t := true
+ config.DefaultConfig.GlobalConfig.ExtraScrapeMetrics = &t
+ config.DefaultGlobalConfig.ExtraScrapeMetrics = &t
+ logger.Warn("This option for --enable-feature is being phased out. It currently changes the default for the extra_scrape_metrics config setting to true, but will become a no-op in a future version. Stop using this option and set extra_scrape_metrics in the config instead.", "option", o)
case "metadata-wal-records":
c.scrape.AppendMetadata = true
c.web.AppendMetadata = true
+ features.Enable(features.TSDB, "metadata_wal_records")
logger.Info("Experimental metadata records in WAL enabled")
case "promql-per-step-stats":
c.enablePerStepStats = true
@@ -249,26 +257,36 @@ func (c *flagConfig) setFeatureListOptions(logger *slog.Logger) error {
c.enableConcurrentRuleEval = true
logger.Info("Experimental concurrent rule evaluation enabled.")
case "promql-experimental-functions":
- parser.EnableExperimentalFunctions = true
+ c.parserOpts.EnableExperimentalFunctions = true
logger.Info("Experimental PromQL functions enabled.")
case "promql-duration-expr":
- parser.ExperimentalDurationExpr = true
+ c.parserOpts.ExperimentalDurationExpr = true
logger.Info("Experimental duration expression parsing enabled.")
case "native-histograms":
- // Change relevant global variables. Hacky, but it's hard to pass a new option or default to unmarshallers.
- t := true
- config.DefaultConfig.GlobalConfig.ScrapeNativeHistograms = &t
- config.DefaultGlobalConfig.ScrapeNativeHistograms = &t
- logger.Warn("This option for --enable-feature is being phased out. It currently changes the default for the scrape_native_histograms scrape config setting to true, but will become a no-op in v3.9+. Stop using this option and set scrape_native_histograms in the scrape config instead.", "option", o)
+ logger.Warn("This option for --enable-feature is a no-op. To scrape native histograms, set the scrape_native_histograms scrape config setting to true.", "option", o)
case "ooo-native-histograms":
logger.Warn("This option for --enable-feature is now permanently enabled and therefore a no-op.", "option", o)
case "created-timestamp-zero-ingestion":
- c.scrape.EnableCreatedTimestampZeroIngestion = true
- c.web.CTZeroIngestionEnabled = true
+ // NOTE(bwplotka): Once AppendableV1 is removed, there will be only the TSDB and agent flags.
+ c.scrape.EnableStartTimestampZeroIngestion = true
+ c.web.STZeroIngestionEnabled = true
+ c.tsdb.EnableSTAsZeroSample = true
+ c.agent.EnableSTAsZeroSample = true
+
// Change relevant global variables. Hacky, but it's hard to pass a new option or default to unmarshallers.
+ // This is to widen the ST support surface.
config.DefaultConfig.GlobalConfig.ScrapeProtocols = config.DefaultProtoFirstScrapeProtocols
config.DefaultGlobalConfig.ScrapeProtocols = config.DefaultProtoFirstScrapeProtocols
- logger.Info("Experimental created timestamp zero ingestion enabled. Changed default scrape_protocols to prefer PrometheusProto format.", "global.scrape_protocols", fmt.Sprintf("%v", config.DefaultGlobalConfig.ScrapeProtocols))
+ logger.Info("Experimental start timestamp zero ingestion enabled. Changed default scrape_protocols to prefer PrometheusProto format.", "global.scrape_protocols", fmt.Sprintf("%v", config.DefaultGlobalConfig.ScrapeProtocols))
+ case "st-storage":
+ // TODO(bwplotka): Implement ST Storage as per PROM-60 and document this hidden feature flag.
+ c.tsdb.EnableSTStorage = true
+ c.agent.EnableSTStorage = true
+
+ // Change relevant global variables. Hacky, but it's hard to pass a new option or default to unmarshallers. This is to widen the ST support surface.
+ config.DefaultConfig.GlobalConfig.ScrapeProtocols = config.DefaultProtoFirstScrapeProtocols
+ config.DefaultGlobalConfig.ScrapeProtocols = config.DefaultProtoFirstScrapeProtocols
+ logger.Info("Experimental start timestamp storage enabled. Changed default scrape_protocols to prefer PrometheusProto format.", "global.scrape_protocols", fmt.Sprintf("%v", config.DefaultGlobalConfig.ScrapeProtocols))
case "delayed-compaction":
c.tsdb.EnableDelayedCompaction = true
logger.Info("Experimental delayed compaction is enabled.")
@@ -276,8 +294,11 @@ func (c *flagConfig) setFeatureListOptions(logger *slog.Logger) error {
c.promqlEnableDelayedNameRemoval = true
logger.Info("Experimental PromQL delayed name removal enabled.")
case "promql-extended-range-selectors":
- parser.EnableExtendedRangeSelectors = true
+ c.parserOpts.EnableExtendedRangeSelectors = true
logger.Info("Experimental PromQL extended range selectors enabled.")
+ case "promql-binop-fill-modifiers":
+ c.parserOpts.EnableBinopFillModifiers = true
+ logger.Info("Experimental PromQL binary operator fill modifiers enabled.")
case "":
continue
case "old-ui":
@@ -345,10 +366,14 @@ func main() {
Registerer: prometheus.DefaultRegisterer,
},
web: web.Options{
- Registerer: prometheus.DefaultRegisterer,
- Gatherer: prometheus.DefaultGatherer,
+ Registerer: prometheus.DefaultRegisterer,
+ Gatherer: prometheus.DefaultGatherer,
+ FeatureRegistry: features.DefaultRegistry,
},
promslogConfig: promslog.Config{},
+ scrape: scrape.Options{
+ FeatureRegistry: features.DefaultRegistry,
+ },
}
a := kingpin.New(filepath.Base(os.Args[0]), "The Prometheus monitoring server").UsageWriter(os.Stdout)
@@ -460,8 +485,9 @@ func main() {
Default("true").Hidden().BoolVar(&cfg.tsdb.EnableOverlappingCompaction)
var (
- tsdbWALCompression bool
- tsdbWALCompressionType string
+ tsdbWALCompression bool
+ tsdbWALCompressionType string
+ tsdbDelayCompactFilePath string
)
serverOnlyFlag(a, "storage.tsdb.wal-compression", "Compress the tsdb WAL. If false, the --storage.tsdb.wal-compression-type flag is ignored.").
Hidden().Default("true").BoolVar(&tsdbWALCompression)
@@ -478,6 +504,12 @@ func main() {
serverOnlyFlag(a, "storage.tsdb.delayed-compaction.max-percent", "Sets the upper limit for the random compaction delay, specified as a percentage of the head chunk range. 100 means the compaction can be delayed by up to the entire head chunk range. Only effective when the delayed-compaction feature flag is enabled.").
Default("10").Hidden().IntVar(&cfg.tsdb.CompactionDelayMaxPercent)
+ serverOnlyFlag(a, "storage.tsdb.delay-compact-file.path", "Path to a JSON file with uploaded TSDB blocks e.g. Thanos shipper meta file. If set TSDB will only compact 1 level blocks that are marked as uploaded in that file, improving external storage integrations e.g. with Thanos sidecar. 1+ level compactions won't be delayed.").
+ Default("").StringVar(&tsdbDelayCompactFilePath)
+
+ serverOnlyFlag(a, "storage.tsdb.block-reload-interval", "Interval at which to check for new or removed blocks in storage. Users who manually backfill or drop blocks must wait up to this duration before changes become available.").
+ Default("1m").Hidden().SetValue(&cfg.tsdb.BlockReloadInterval)
+
agentOnlyFlag(a, "storage.agent.path", "Base path for metrics storage.").
Default("data-agent/").StringVar(&cfg.agentStoragePath)
@@ -564,7 +596,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, native-histograms, 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)
@@ -600,6 +632,8 @@ func main() {
os.Exit(1)
}
+ promqlParser := parser.NewParser(cfg.parserOpts)
+
if agentMode && len(serverOnlyFlags) > 0 {
fmt.Fprintf(os.Stderr, "The following flag(s) can not be used in agent mode: %q", serverOnlyFlags)
os.Exit(3)
@@ -654,7 +688,7 @@ func main() {
}
// Parse rule files to verify they exist and contain valid rules.
- if err := rules.ParseFiles(cfgFile.RuleFiles, cfgFile.GlobalConfig.MetricNameValidationScheme); err != nil {
+ if err := rules.ParseFiles(cfgFile.RuleFiles, cfgFile.GlobalConfig.MetricNameValidationScheme, promqlParser); err != nil {
absPath, pathErr := filepath.Abs(cfg.configFile)
if pathErr != nil {
absPath = cfg.configFile
@@ -669,8 +703,13 @@ func main() {
}
cfg.tsdb.MaxExemplars = cfgFile.StorageConfig.ExemplarsConfig.MaxExemplars
}
+ if cfg.tsdb.BlockReloadInterval < model.Duration(1*time.Second) {
+ logger.Warn("The option --storage.tsdb.block-reload-interval is set to a value less than 1s. Setting it to 1s to avoid overload.")
+ cfg.tsdb.BlockReloadInterval = model.Duration(1 * time.Second)
+ }
if cfgFile.StorageConfig.TSDBConfig != nil {
cfg.tsdb.OutOfOrderTimeWindow = cfgFile.StorageConfig.TSDBConfig.OutOfOrderTimeWindow
+ cfg.tsdb.StaleSeriesCompactionThreshold = cfgFile.StorageConfig.TSDBConfig.StaleSeriesCompactionThreshold
if cfgFile.StorageConfig.TSDBConfig.Retention != nil {
if cfgFile.StorageConfig.TSDBConfig.Retention.Time > 0 {
cfg.tsdb.RetentionDuration = cfgFile.StorageConfig.TSDBConfig.Retention.Time
@@ -707,6 +746,12 @@ func main() {
}
}
+ if tsdbDelayCompactFilePath != "" {
+ logger.Info("Compactions will be delayed for blocks not marked as uploaded in the file tracking uploads", "path", tsdbDelayCompactFilePath)
+ cfg.tsdb.BlockCompactionExcludeFunc = exludeBlocksPendingUpload(
+ logger, tsdbDelayCompactFilePath)
+ }
+
// Now that the validity of the config is established, set the config
// success metrics accordingly, although the config isn't really loaded
// yet. This will happen later (including setting these metrics again),
@@ -790,6 +835,12 @@ func main() {
"vm_limits", prom_runtime.VMLimits(),
)
+ features.Set(features.Prometheus, "agent_mode", agentMode)
+ features.Set(features.Prometheus, "server_mode", !agentMode)
+ features.Set(features.Prometheus, "auto_reload_config", cfg.enableAutoReload)
+ features.Enable(features.Prometheus, labels.ImplementationName)
+ template.RegisterFeatures(features.DefaultRegistry)
+
var (
localStorage = &readyStorage{stats: tsdb.NewDBStats()}
scraper = &readyScrapeManager{}
@@ -826,13 +877,13 @@ func main() {
os.Exit(1)
}
- discoveryManagerScrape = discovery.NewManager(ctxScrape, logger.With("component", "discovery manager scrape"), prometheus.DefaultRegisterer, sdMetrics, discovery.Name("scrape"))
+ discoveryManagerScrape = discovery.NewManager(ctxScrape, logger.With("component", "discovery manager scrape"), prometheus.DefaultRegisterer, sdMetrics, discovery.Name("scrape"), discovery.FeatureRegistry(features.DefaultRegistry))
if discoveryManagerScrape == nil {
logger.Error("failed to create a discovery manager scrape")
os.Exit(1)
}
- discoveryManagerNotify = discovery.NewManager(ctxNotify, logger.With("component", "discovery manager notify"), prometheus.DefaultRegisterer, sdMetrics, discovery.Name("notify"))
+ discoveryManagerNotify = discovery.NewManager(ctxNotify, logger.With("component", "discovery manager notify"), prometheus.DefaultRegisterer, sdMetrics, discovery.Name("notify"), discovery.FeatureRegistry(features.DefaultRegistry))
if discoveryManagerNotify == nil {
logger.Error("failed to create a discovery manager notify")
os.Exit(1)
@@ -842,7 +893,7 @@ func main() {
&cfg.scrape,
logger.With("component", "scrape manager"),
logging.NewJSONFileLogger,
- fanoutStorage,
+ nil, fanoutStorage,
prometheus.DefaultRegisterer,
)
if err != nil {
@@ -873,6 +924,8 @@ func main() {
EnablePerStepStats: cfg.enablePerStepStats,
EnableDelayedNameRemoval: cfg.promqlEnableDelayedNameRemoval,
EnableTypeAndUnitLabels: cfg.scrape.EnableTypeAndUnitLabels,
+ FeatureRegistry: features.DefaultRegistry,
+ Parser: promqlParser,
}
queryEngine = promql.NewEngine(opts)
@@ -895,6 +948,8 @@ func main() {
DefaultRuleQueryOffset: func() time.Duration {
return time.Duration(cfgFile.GlobalConfig.RuleQueryOffset)
},
+ FeatureRegistry: features.DefaultRegistry,
+ Parser: promqlParser,
})
}
@@ -914,6 +969,7 @@ func main() {
cfg.web.LookbackDelta = time.Duration(cfg.lookbackDelta)
cfg.web.IsAgent = agentMode
cfg.web.AppName = modeAppName
+ cfg.web.Parser = promqlParser
cfg.web.Version = &web.PrometheusVersion{
Version: version.Version,
@@ -1135,9 +1191,11 @@ func main() {
func() error {
<-reloadReady.C
ruleManager.Run()
+ logger.Info("Rule manager stopped")
return nil
},
func(error) {
+ logger.Info("Stopping rule manager manager...")
ruleManager.Stop()
},
)
@@ -1172,9 +1230,11 @@ func main() {
func() error {
<-reloadReady.C
tracingManager.Run()
+ logger.Info("Tracing manager stopped")
return nil
},
func(error) {
+ logger.Info("Stopping tracing manager...")
tracingManager.Stop()
},
)
@@ -1251,6 +1311,7 @@ func main() {
checksum = currentChecksum
}
case <-cancel:
+ logger.Info("Reloaders stopped")
return nil
}
}
@@ -1258,6 +1319,7 @@ func main() {
func(error) {
// Wait for any in-progress reloads to complete to avoid
// reloading things after they have been shutdown.
+ logger.Info("Stopping reloaders...")
cancel <- struct{}{}
},
)
@@ -1331,6 +1393,9 @@ func main() {
"RetentionDuration", cfg.tsdb.RetentionDuration,
"WALSegmentSize", cfg.tsdb.WALSegmentSize,
"WALCompressionType", cfg.tsdb.WALCompressionType,
+ "BlockReloadInterval", cfg.tsdb.BlockReloadInterval,
+ "EnableSTAsZeroSample", cfg.tsdb.EnableSTAsZeroSample,
+ "EnableSTStorage", cfg.tsdb.EnableSTStorage,
)
startTimeMargin := int64(2 * time.Duration(cfg.tsdb.MinBlockDuration).Seconds() * 1000)
@@ -1338,9 +1403,11 @@ func main() {
db.SetWriteNotified(remoteStorage)
close(dbOpen)
<-cancel
+ logger.Info("TSDB stopped")
return nil
},
func(error) {
+ logger.Info("Stopping storage...")
if err := fanoutStorage.Close(); err != nil {
logger.Error("Error stopping storage", "err", err)
}
@@ -1387,15 +1454,19 @@ func main() {
"MinWALTime", cfg.agent.MinWALTime,
"MaxWALTime", cfg.agent.MaxWALTime,
"OutOfOrderTimeWindow", cfg.agent.OutOfOrderTimeWindow,
+ "EnableSTAsZeroSample", cfg.agent.EnableSTAsZeroSample,
+ "EnableSTStorage", cfg.tsdb.EnableSTStorage,
)
localStorage.Set(db, 0)
db.SetWriteNotified(remoteStorage)
close(dbOpen)
<-cancel
+ logger.Info("Agent WAL storage stopped")
return nil
},
func(error) {
+ logger.Info("Stopping agent WAL storage...")
if err := fanoutStorage.Close(); err != nil {
logger.Error("Error stopping storage", "err", err)
}
@@ -1410,9 +1481,11 @@ func main() {
if err := webHandler.Run(ctxWeb, listeners, *webConfig); err != nil {
return fmt.Errorf("error starting web server: %w", err)
}
+ logger.Info("Web handler stopped")
return nil
},
func(error) {
+ logger.Info("Stopping web handler...")
cancelWeb()
},
)
@@ -1435,6 +1508,7 @@ func main() {
return nil
},
func(error) {
+ logger.Info("Stopping notifier manager...")
notifierManager.Stop()
},
)
@@ -1538,7 +1612,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)
@@ -1712,6 +1786,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.
@@ -1729,7 +1811,7 @@ func (notReadyAppender) AppendHistogram(storage.SeriesRef, labels.Labels, int64,
return 0, tsdb.ErrNotReady
}
-func (notReadyAppender) AppendHistogramCTZeroSample(storage.SeriesRef, labels.Labels, int64, int64, *histogram.Histogram, *histogram.FloatHistogram) (storage.SeriesRef, error) {
+func (notReadyAppender) AppendHistogramSTZeroSample(storage.SeriesRef, labels.Labels, int64, int64, *histogram.Histogram, *histogram.FloatHistogram) (storage.SeriesRef, error) {
return 0, tsdb.ErrNotReady
}
@@ -1737,7 +1819,7 @@ func (notReadyAppender) UpdateMetadata(storage.SeriesRef, labels.Labels, metadat
return 0, tsdb.ErrNotReady
}
-func (notReadyAppender) AppendCTZeroSample(storage.SeriesRef, labels.Labels, int64, int64) (storage.SeriesRef, error) {
+func (notReadyAppender) AppendSTZeroSample(storage.SeriesRef, labels.Labels, int64, int64) (storage.SeriesRef, error) {
return 0, tsdb.ErrNotReady
}
@@ -1745,6 +1827,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 {
@@ -1887,6 +1978,11 @@ type tsdbOptions struct {
CompactionDelayMaxPercent int
EnableOverlappingCompaction bool
UseUncachedIO bool
+ BlockCompactionExcludeFunc tsdb.BlockExcludeFilterFunc
+ BlockReloadInterval model.Duration
+ EnableSTAsZeroSample bool
+ EnableSTStorage bool
+ StaleSeriesCompactionThreshold float64
}
func (opts tsdbOptions) ToTSDBOptions() tsdb.Options {
@@ -1910,6 +2006,12 @@ func (opts tsdbOptions) ToTSDBOptions() tsdb.Options {
CompactionDelayMaxPercent: opts.CompactionDelayMaxPercent,
EnableOverlappingCompaction: opts.EnableOverlappingCompaction,
UseUncachedIO: opts.UseUncachedIO,
+ BlockCompactionExcludeFunc: opts.BlockCompactionExcludeFunc,
+ BlockReloadInterval: time.Duration(opts.BlockReloadInterval),
+ FeatureRegistry: features.DefaultRegistry,
+ EnableSTAsZeroSample: opts.EnableSTAsZeroSample,
+ EnableSTStorage: opts.EnableSTStorage,
+ StaleSeriesCompactionThreshold: opts.StaleSeriesCompactionThreshold,
}
}
@@ -1922,7 +2024,9 @@ type agentOptions struct {
TruncateFrequency model.Duration
MinWALTime, MaxWALTime model.Duration
NoLockfile bool
- OutOfOrderTimeWindow int64
+ OutOfOrderTimeWindow int64 // TODO(bwplotka): Unused option, fix it or remove.
+ EnableSTAsZeroSample bool
+ EnableSTStorage bool
}
func (opts agentOptions) ToAgentOptions(outOfOrderTimeWindow int64) agent.Options {
@@ -1938,6 +2042,8 @@ func (opts agentOptions) ToAgentOptions(outOfOrderTimeWindow int64) agent.Option
MaxWALTime: durationToInt64Millis(time.Duration(opts.MaxWALTime)),
NoLockfile: opts.NoLockfile,
OutOfOrderTimeWindow: outOfOrderTimeWindow,
+ EnableSTAsZeroSample: opts.EnableSTAsZeroSample,
+ EnableSTStorage: opts.EnableSTStorage,
}
}
@@ -1974,3 +2080,48 @@ func (p *rwProtoMsgFlagParser) Set(opt string) error {
*p.msgs = append(*p.msgs, t)
return nil
}
+
+type UploadMeta struct {
+ Uploaded []string `json:"uploaded"`
+}
+
+// Cache the last read UploadMeta.
+var (
+ tsdbDelayCompactLastMeta *UploadMeta // The content of uploadMetaPath from the last time we've opened it.
+ tsdbDelayCompactLastMetaTime time.Time // The timestamp at which we stored tsdbDelayCompactLastMeta last time.
+)
+
+func exludeBlocksPendingUpload(logger *slog.Logger, uploadMetaPath string) tsdb.BlockExcludeFilterFunc {
+ return func(meta *tsdb.BlockMeta) bool {
+ if meta.Compaction.Level > 1 {
+ // Blocks with level > 1 are assumed to be not uploaded, thus no need to delay those.
+ // See `storage.tsdb.delay-compact-file.path` flag for detail.
+ return false
+ }
+
+ // If we have cached uploadMetaPath content that was stored in the last minute the use it.
+ if tsdbDelayCompactLastMeta != nil &&
+ tsdbDelayCompactLastMetaTime.After(time.Now().UTC().Add(time.Minute*-1)) {
+ return !slices.Contains(tsdbDelayCompactLastMeta.Uploaded, meta.ULID.String())
+ }
+
+ // We don't have anything cached or it's older than a minute. Try to open and parse the uploadMetaPath path.
+ data, err := os.ReadFile(uploadMetaPath)
+ if err != nil {
+ logger.Warn("cannot open TSDB upload meta file", slog.String("path", uploadMetaPath), slog.Any("err", err))
+ return false
+ }
+
+ var uploadMeta UploadMeta
+ if err = json.Unmarshal(data, &uploadMeta); err != nil {
+ logger.Warn("cannot parse TSDB upload meta file", slog.String("path", uploadMetaPath), slog.Any("err", err))
+ return false
+ }
+
+ // We have parsed the uploadMetaPath file, cache it.
+ tsdbDelayCompactLastMeta = &uploadMeta
+ tsdbDelayCompactLastMetaTime = time.Now().UTC()
+
+ return !slices.Contains(uploadMeta.Uploaded, meta.ULID.String())
+ }
+}
diff --git a/cmd/prometheus/main_test.go b/cmd/prometheus/main_test.go
index ccc9151492..38dfd3f2da 100644
--- a/cmd/prometheus/main_test.go
+++ b/cmd/prometheus/main_test.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
@@ -395,6 +395,7 @@ func TestTimeMetrics(t *testing.T) {
}
func getCurrentGaugeValuesFor(t *testing.T, reg prometheus.Gatherer, metricNames ...string) map[string]float64 {
+ t.Helper()
f, err := reg.Gather()
require.NoError(t, err)
@@ -426,7 +427,7 @@ func TestAgentSuccessfulStartup(t *testing.T) {
go func() { done <- prom.Wait() }()
select {
case err := <-done:
- t.Logf("prometheus agent should be still running: %v", err)
+ t.Logf("prometheus agent exited early: %v", err)
actualExitStatus = prom.ProcessState.ExitCode()
case <-time.After(startupTime):
prom.Process.Kill()
@@ -571,12 +572,7 @@ func TestDocumentation(t *testing.T) {
var stdout bytes.Buffer
cmd.Stdout = &stdout
- if err := cmd.Run(); err != nil {
- var exitError *exec.ExitError
- if errors.As(err, &exitError) && exitError.ExitCode() != 0 {
- fmt.Println("Command failed with non-zero exit code")
- }
- }
+ require.NoError(t, cmd.Run(), "failed to generate CLI documentation via --write-documentation")
generatedContent := strings.ReplaceAll(stdout.String(), filepath.Base(promPath), strings.TrimSuffix(filepath.Base(promPath), ".test"))
@@ -753,7 +749,7 @@ global:
configFile := filepath.Join(tmpDir, "prometheus.yml")
port := testutil.RandomUnprivilegedPort(t)
- os.WriteFile(configFile, []byte(tc.config), 0o777)
+ require.NoError(t, os.WriteFile(configFile, []byte(tc.config), 0o777))
prom := prometheusCommandWithLogging(
t,
configFile,
@@ -801,7 +797,7 @@ global:
newConfig := `
runtime:
gogc: 99`
- os.WriteFile(configFile, []byte(newConfig), 0o777)
+ require.NoError(t, os.WriteFile(configFile, []byte(newConfig), 0o777))
reloadPrometheusConfig(t, reloadURL)
ensureGOGCValue(99.0)
})
@@ -834,7 +830,7 @@ scrape_configs:
static_configs:
- targets: ['localhost:%d']
`, port, port)
- os.WriteFile(configFile, []byte(config), 0o777)
+ require.NoError(t, os.WriteFile(configFile, []byte(config), 0o777))
prom := prometheusCommandWithLogging(
t,
@@ -968,7 +964,18 @@ remote_write:
// TestRemoteWrite_ReshardingWithoutDeadlock ensures that resharding (scaling up) doesn't block when the shards are full.
// See: https://github.com/prometheus/prometheus/issues/17384.
+//
+// The following shows key resharding metrics before and after the fix.
+// In v3.7.0, the deadlock prevented the resharding logic from observing the true incoming data rate.
+//
+// | Metric | v3.7.0 | after the fix |
+// |---------------------|---------------|---------------------|
+// | dataInRate | 0.6 | 307.2 |
+// | dataPendingRate | 0.2 | 306.8 |
+// | dataPending | 0 | 1228.8 |
+// | desiredShards | 0.6 | 369.2 |.
func TestRemoteWrite_ReshardingWithoutDeadlock(t *testing.T) {
+ t.Skip("flaky test, see https://github.com/prometheus/prometheus/issues/17489")
t.Parallel()
tmpDir := t.TempDir()
@@ -983,7 +990,8 @@ func TestRemoteWrite_ReshardingWithoutDeadlock(t *testing.T) {
config := fmt.Sprintf(`
global:
- scrape_interval: 100ms
+ # Using a smaller interval may cause the scrape to time out.
+ scrape_interval: 1s
scrape_configs:
- job_name: 'self'
static_configs:
@@ -994,6 +1002,8 @@ remote_write:
queue_config:
# Speed up the queue being full.
capacity: 1
+ # Helps keep the “time to send one sample” low so it doesn’t influence the resharding logic.
+ max_samples_per_send: 1
`, port, server.URL)
require.NoError(t, os.WriteFile(configFile, []byte(config), 0o777))
@@ -1002,36 +1012,52 @@ remote_write:
configFile,
port,
fmt.Sprintf("--storage.tsdb.path=%s", tmpDir),
+ "--log.level=debug",
)
require.NoError(t, prom.Start())
- var checkInitialDesiredShardsOnce sync.Once
- require.Eventually(t, func() bool {
+ const desiredShardsMetric = "prometheus_remote_storage_shards_desired"
+ getMetrics := func() ([]byte, error) {
r, err := http.Get(fmt.Sprintf("http://127.0.0.1:%d/metrics", port))
if err != nil {
- return false
+ return nil, err
}
defer r.Body.Close()
if r.StatusCode != http.StatusOK {
- return false
+ return nil, fmt.Errorf("unexpected status code: %d", r.StatusCode)
}
metrics, err := io.ReadAll(r.Body)
+ if err != nil {
+ return nil, err
+ }
+ return metrics, nil
+ }
+
+ // Ensure the initial desired shards is 1.
+ require.Eventually(t, func() bool {
+ metrics, err := getMetrics()
if err != nil {
return false
}
+ initialDesiredShards, err := getMetricValue(t, bytes.NewReader(metrics), model.MetricTypeGauge, desiredShardsMetric)
+ if err != nil {
+ return false
+ }
+ return initialDesiredShards == 1.0
+ }, 10*time.Second, 100*time.Millisecond)
- checkInitialDesiredShardsOnce.Do(func() {
- s, err := getMetricValue(t, bytes.NewReader(metrics), model.MetricTypeGauge, "prometheus_remote_storage_shards_desired")
- require.NoError(t, err)
- require.Equal(t, 1.0, s)
- })
-
- desiredShards, err := getMetricValue(t, bytes.NewReader(metrics), model.MetricTypeGauge, "prometheus_remote_storage_shards_desired")
- if err != nil || desiredShards <= 1 {
+ // Ensure scaling up is triggered after some time.
+ require.Eventually(t, func() bool {
+ metrics, err := getMetrics()
+ if err != nil {
+ return false
+ }
+ desiredShards, err := getMetricValue(t, bytes.NewReader(metrics), model.MetricTypeGauge, desiredShardsMetric)
+ if err != nil || desiredShards <= 1.0 {
return false
}
return true
// 3*shardUpdateDuration to allow for the resharding logic to run.
- }, 30*time.Second, 1*time.Second)
+ }, 30*time.Second, time.Second)
}
diff --git a/cmd/prometheus/main_unix_test.go b/cmd/prometheus/main_unix_test.go
index 66bfe9b60a..ea130b3bf9 100644
--- a/cmd/prometheus/main_unix_test.go
+++ b/cmd/prometheus/main_unix_test.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
diff --git a/cmd/prometheus/query_log_test.go b/cmd/prometheus/query_log_test.go
index 645ac31145..e410f836a9 100644
--- a/cmd/prometheus/query_log_test.go
+++ b/cmd/prometheus/query_log_test.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
@@ -334,7 +334,8 @@ func (p *queryLogTest) run(t *testing.T) {
p.query(t)
- ql := readQueryLog(t, queryLogFile.Name())
+ // Wait for query log entry to be written (avoid race with file I/O).
+ ql := waitForQueryLog(t, queryLogFile.Name(), 1)
qc := len(ql)
if p.exactQueryCount() {
require.Equal(t, 1, qc)
@@ -361,7 +362,8 @@ func (p *queryLogTest) run(t *testing.T) {
p.query(t)
qc++
- ql = readQueryLog(t, queryLogFile.Name())
+ // Wait for query log entry to be written (avoid race with file I/O).
+ ql = waitForQueryLog(t, queryLogFile.Name(), qc)
if p.exactQueryCount() {
require.Len(t, ql, qc)
} else {
@@ -392,7 +394,8 @@ func (p *queryLogTest) run(t *testing.T) {
qc++
- ql = readQueryLog(t, newFile.Name())
+ // Wait for query log entry to be written (avoid race with file I/O).
+ ql = waitForQueryLog(t, newFile.Name(), qc)
if p.exactQueryCount() {
require.Len(t, ql, qc)
} else {
@@ -404,7 +407,8 @@ func (p *queryLogTest) run(t *testing.T) {
p.query(t)
- ql = readQueryLog(t, queryLogFile.Name())
+ // Wait for query log entry to be written (avoid race with file I/O).
+ ql = waitForQueryLog(t, queryLogFile.Name(), 1)
qc = len(ql)
if p.exactQueryCount() {
require.Equal(t, 1, qc)
@@ -446,6 +450,18 @@ func readQueryLog(t *testing.T, path string) []queryLogLine {
return ql
}
+// waitForQueryLog waits for the query log to contain at least minEntries entries,
+// polling at regular intervals until the timeout is reached.
+func waitForQueryLog(t *testing.T, path string, minEntries int) []queryLogLine {
+ t.Helper()
+ var ql []queryLogLine
+ require.Eventually(t, func() bool {
+ ql = readQueryLog(t, path)
+ return len(ql) >= minEntries
+ }, 5*time.Second, 100*time.Millisecond, "timed out waiting for query log to have at least %d entries, got %d", minEntries, len(ql))
+ return ql
+}
+
func TestQueryLog(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode.")
diff --git a/cmd/prometheus/reload_test.go b/cmd/prometheus/reload_test.go
index 6feb2bf3a5..bbe108c9a6 100644
--- a/cmd/prometheus/reload_test.go
+++ b/cmd/prometheus/reload_test.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Prometheus Authors
+// 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
diff --git a/cmd/prometheus/scrape_failure_log_test.go b/cmd/prometheus/scrape_failure_log_test.go
index f35cb7bee6..c3f459f601 100644
--- a/cmd/prometheus/scrape_failure_log_test.go
+++ b/cmd/prometheus/scrape_failure_log_test.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Prometheus Authors
+// 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
diff --git a/cmd/prometheus/testdata/features.json b/cmd/prometheus/testdata/features.json
new file mode 100644
index 0000000000..ce7dbbaebe
--- /dev/null
+++ b/cmd/prometheus/testdata/features.json
@@ -0,0 +1,257 @@
+{
+ "api": {
+ "admin": false,
+ "exclude_alerts": true,
+ "label_values_match": true,
+ "lifecycle": false,
+ "openapi_3.1": true,
+ "openapi_3.2": true,
+ "otlp_write_receiver": false,
+ "query_stats": true,
+ "query_warnings": true,
+ "remote_write_receiver": false,
+ "time_range_labels": true,
+ "time_range_series": true
+ },
+ "otlp_receiver": {
+ "delta_conversion": false,
+ "native_delta_ingestion": false
+ },
+ "prometheus": {
+ "agent_mode": false,
+ "auto_reload_config": false,
+ "server_mode": true,
+ "stringlabels": true
+ },
+ "promql": {
+ "anchored": false,
+ "at_modifier": true,
+ "bool": true,
+ "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,
+ "negative_offset": true,
+ "offset": true,
+ "on": true,
+ "per_query_lookback_delta": true,
+ "per_step_stats": false,
+ "smoothed": false,
+ "subqueries": true,
+ "type_and_unit_labels": false,
+ "without": true
+ },
+ "promql_functions": {
+ "abs": true,
+ "absent": true,
+ "absent_over_time": true,
+ "acos": true,
+ "acosh": true,
+ "asin": true,
+ "asinh": true,
+ "atan": true,
+ "atanh": true,
+ "avg_over_time": true,
+ "ceil": true,
+ "changes": true,
+ "clamp": true,
+ "clamp_max": true,
+ "clamp_min": true,
+ "cos": true,
+ "cosh": true,
+ "count_over_time": true,
+ "day_of_month": true,
+ "day_of_week": true,
+ "day_of_year": true,
+ "days_in_month": true,
+ "deg": true,
+ "delta": true,
+ "deriv": true,
+ "double_exponential_smoothing": false,
+ "exp": true,
+ "first_over_time": false,
+ "floor": true,
+ "histogram_avg": true,
+ "histogram_count": true,
+ "histogram_fraction": true,
+ "histogram_quantile": true,
+ "histogram_quantiles": false,
+ "histogram_stddev": true,
+ "histogram_stdvar": true,
+ "histogram_sum": true,
+ "hour": true,
+ "idelta": true,
+ "increase": true,
+ "info": false,
+ "irate": true,
+ "label_join": true,
+ "label_replace": true,
+ "last_over_time": true,
+ "ln": true,
+ "log10": true,
+ "log2": true,
+ "mad_over_time": false,
+ "max_over_time": true,
+ "min_over_time": true,
+ "minute": true,
+ "month": true,
+ "pi": true,
+ "predict_linear": true,
+ "present_over_time": true,
+ "quantile_over_time": true,
+ "rad": true,
+ "rate": true,
+ "resets": true,
+ "round": true,
+ "scalar": true,
+ "sgn": true,
+ "sin": true,
+ "sinh": true,
+ "sort": true,
+ "sort_by_label": false,
+ "sort_by_label_desc": false,
+ "sort_desc": true,
+ "sqrt": true,
+ "stddev_over_time": true,
+ "stdvar_over_time": true,
+ "sum_over_time": true,
+ "tan": true,
+ "tanh": true,
+ "time": true,
+ "timestamp": true,
+ "ts_of_first_over_time": false,
+ "ts_of_last_over_time": false,
+ "ts_of_max_over_time": false,
+ "ts_of_min_over_time": false,
+ "vector": true,
+ "year": true
+ },
+ "promql_operators": {
+ "!=": true,
+ "!~": true,
+ "%": true,
+ "*": true,
+ "+": true,
+ "-": true,
+ "/": true,
+ "<": true,
+ "<=": true,
+ "==": true,
+ "=~": true,
+ ">": true,
+ ">=": true,
+ "@": true,
+ "^": true,
+ "and": true,
+ "atan2": true,
+ "avg": true,
+ "bottomk": true,
+ "count": true,
+ "count_values": true,
+ "group": true,
+ "limit_ratio": false,
+ "limitk": false,
+ "max": true,
+ "min": true,
+ "or": true,
+ "quantile": true,
+ "stddev": true,
+ "stdvar": true,
+ "sum": true,
+ "topk": true,
+ "unless": true
+ },
+ "rules": {
+ "concurrent_rule_eval": false,
+ "keep_firing_for": true,
+ "query_offset": true
+ },
+ "scrape": {
+ "extra_scrape_metrics": true,
+ "start_timestamp_zero_ingestion": false,
+ "type_and_unit_labels": false
+ },
+ "service_discovery_providers": {
+ "aws": true,
+ "azure": true,
+ "consul": true,
+ "digitalocean": true,
+ "dns": true,
+ "docker": true,
+ "dockerswarm": true,
+ "ec2": true,
+ "ecs": true,
+ "elasticache": true,
+ "eureka": true,
+ "file": true,
+ "gce": true,
+ "hetzner": true,
+ "http": true,
+ "ionos": true,
+ "kubernetes": true,
+ "kuma": true,
+ "lightsail": true,
+ "linode": true,
+ "marathon": true,
+ "msk": true,
+ "nerve": true,
+ "nomad": true,
+ "openstack": true,
+ "ovhcloud": true,
+ "puppetdb": true,
+ "scaleway": true,
+ "serverset": true,
+ "stackit": true,
+ "static": true,
+ "triton": true,
+ "uyuni": true,
+ "vultr": true
+ },
+ "templating_functions": {
+ "args": true,
+ "externalURL": true,
+ "first": true,
+ "graphLink": true,
+ "humanize": true,
+ "humanize1024": true,
+ "humanizeDuration": true,
+ "humanizePercentage": true,
+ "humanizeTimestamp": true,
+ "label": true,
+ "match": true,
+ "now": true,
+ "parseDuration": true,
+ "pathPrefix": true,
+ "query": true,
+ "reReplaceAll": true,
+ "safeHtml": true,
+ "sortByLabel": true,
+ "stripDomain": true,
+ "stripPort": true,
+ "strvalue": true,
+ "tableLink": true,
+ "title": true,
+ "toDuration": true,
+ "toLower": true,
+ "toTime": true,
+ "toUpper": true,
+ "urlQueryEscape": true,
+ "value": true
+ },
+ "tsdb": {
+ "delayed_compaction": false,
+ "exemplar_storage": false,
+ "isolation": true,
+ "native_histograms": true,
+ "use_uncached_io": false
+ },
+ "ui": {
+ "ui_v2": false,
+ "ui_v3": true
+ }
+}
diff --git a/cmd/prometheus/upload_test.go b/cmd/prometheus/upload_test.go
new file mode 100644
index 0000000000..97a98351a7
--- /dev/null
+++ b/cmd/prometheus/upload_test.go
@@ -0,0 +1,144 @@
+// 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 main
+
+import (
+ "encoding/json"
+ "os"
+ "path"
+ "testing"
+ "time"
+
+ "github.com/oklog/ulid/v2"
+ "github.com/prometheus/common/promslog"
+ "github.com/stretchr/testify/require"
+
+ "github.com/prometheus/prometheus/tsdb"
+)
+
+func TestBlockExcludeFilter(t *testing.T) {
+ for _, test := range []struct {
+ summary string // Description of the test case.
+ uploaded []ulid.ULID // List of blocks marked as uploaded inside the shipper file.
+ setupFn func(string) // Optional function to run before the test, takes the path to the shipper file.
+ meta tsdb.BlockMeta // Meta of the block we're checking.
+ isExcluded bool // What do we expect to be returned.
+ }{
+ {
+ summary: "missing file",
+ setupFn: func(path string) {
+ // Delete shipper file to test error handling.
+ os.Remove(path)
+ },
+ meta: tsdb.BlockMeta{ULID: ulid.MustNew(1, nil)},
+ isExcluded: false,
+ },
+ {
+ summary: "corrupt file",
+ setupFn: func(path string) {
+ // Overwrite the shipper file content with invalid JSON.
+ os.WriteFile(path, []byte("{["), 0o644)
+ },
+ meta: tsdb.BlockMeta{ULID: ulid.MustNew(1, nil)},
+ isExcluded: false,
+ },
+ {
+ summary: "empty uploaded list",
+ uploaded: []ulid.ULID{},
+ meta: tsdb.BlockMeta{ULID: ulid.MustNew(1, nil)},
+ isExcluded: true,
+ },
+ {
+ summary: "block meta not present in the uploaded list, level=1",
+ uploaded: []ulid.ULID{ulid.MustNew(1, nil), ulid.MustNew(3, nil)},
+ meta: tsdb.BlockMeta{
+ ULID: ulid.MustNew(2, nil),
+ Compaction: tsdb.BlockMetaCompaction{Level: 1},
+ },
+ isExcluded: true,
+ },
+ {
+ summary: "block meta not present in the uploaded list, level=2",
+ uploaded: []ulid.ULID{ulid.MustNew(1, nil), ulid.MustNew(3, nil)},
+ meta: tsdb.BlockMeta{
+ ULID: ulid.MustNew(2, nil),
+ Compaction: tsdb.BlockMetaCompaction{Level: 2},
+ },
+ isExcluded: false,
+ },
+ {
+ summary: "block meta present in the uploaded list",
+ uploaded: []ulid.ULID{ulid.MustNew(1, nil), ulid.MustNew(2, nil), ulid.MustNew(3, nil)},
+ meta: tsdb.BlockMeta{ULID: ulid.MustNew(2, nil)},
+ isExcluded: false,
+ },
+ {
+ summary: "don't read the file if there's valid cache",
+ setupFn: func(path string) {
+ // Remove the shipper file, cache should be used instead.
+ require.NoError(t, os.Remove(path))
+ // Set cached values
+ tsdbDelayCompactLastMeta = &UploadMeta{
+ Uploaded: []string{
+ ulid.MustNew(1, nil).String(),
+ ulid.MustNew(2, nil).String(),
+ ulid.MustNew(3, nil).String(),
+ },
+ }
+ tsdbDelayCompactLastMetaTime = time.Now().UTC().Add(time.Second * -1)
+ },
+ uploaded: []ulid.ULID{},
+ meta: tsdb.BlockMeta{ULID: ulid.MustNew(2, nil)},
+ isExcluded: false,
+ },
+ {
+ summary: "read the file if there's cache but expired",
+ setupFn: func(_ string) {
+ // Set the cache but make it too old
+ tsdbDelayCompactLastMeta = &UploadMeta{
+ Uploaded: []string{},
+ }
+ tsdbDelayCompactLastMetaTime = time.Now().UTC().Add(time.Second * -61)
+ },
+ uploaded: []ulid.ULID{ulid.MustNew(1, nil), ulid.MustNew(2, nil), ulid.MustNew(3, nil)},
+ meta: tsdb.BlockMeta{ULID: ulid.MustNew(2, nil)},
+ isExcluded: false,
+ },
+ } {
+ t.Run(test.summary, func(t *testing.T) {
+ dir := t.TempDir()
+ shipperPath := path.Join(dir, "shipper.json")
+
+ uploaded := make([]string, 0, len(test.uploaded))
+ for _, ul := range test.uploaded {
+ uploaded = append(uploaded, ul.String())
+ }
+ ts := UploadMeta{Uploaded: uploaded}
+ data, err := json.Marshal(ts)
+ require.NoError(t, err, "failed to marshall upload meta file")
+ require.NoError(t, os.WriteFile(shipperPath, data, 0o644), "failed to write upload meta file")
+
+ tsdbDelayCompactLastMeta = nil
+ tsdbDelayCompactLastMetaTime = time.Time{}
+
+ if test.setupFn != nil {
+ test.setupFn(shipperPath)
+ }
+
+ fn := exludeBlocksPendingUpload(promslog.NewNopLogger(), shipperPath)
+ isExcluded := fn(&test.meta)
+ require.Equal(t, test.isExcluded, isExcluded)
+ })
+ }
+}
diff --git a/cmd/promtool/analyze.go b/cmd/promtool/analyze.go
index aea72a193b..a725772f5d 100644
--- a/cmd/promtool/analyze.go
+++ b/cmd/promtool/analyze.go
@@ -1,4 +1,4 @@
-// Copyright 2023 The Prometheus Authors
+// 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
diff --git a/cmd/promtool/analyze_test.go b/cmd/promtool/analyze_test.go
index 3de4283a15..d2e81da2c8 100644
--- a/cmd/promtool/analyze_test.go
+++ b/cmd/promtool/analyze_test.go
@@ -1,4 +1,4 @@
-// Copyright 2023 The Prometheus Authors
+// 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
diff --git a/cmd/promtool/archive.go b/cmd/promtool/archive.go
index 7b565c57cc..23baea2700 100644
--- a/cmd/promtool/archive.go
+++ b/cmd/promtool/archive.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
diff --git a/cmd/promtool/backfill.go b/cmd/promtool/backfill.go
index 47de3b5c1c..e7a9a7f18a 100644
--- a/cmd/promtool/backfill.go
+++ b/cmd/promtool/backfill.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
@@ -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/backfill_test.go b/cmd/promtool/backfill_test.go
index 8a599510a9..499b90e99a 100644
--- a/cmd/promtool/backfill_test.go
+++ b/cmd/promtool/backfill_test.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
diff --git a/cmd/promtool/debug.go b/cmd/promtool/debug.go
index 6383aaface..b6e82ef981 100644
--- a/cmd/promtool/debug.go
+++ b/cmd/promtool/debug.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
diff --git a/cmd/promtool/main.go b/cmd/promtool/main.go
index 460e47fd25..4dc6c7615f 100644
--- a/cmd/promtool/main.go
+++ b/cmd/promtool/main.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
@@ -61,7 +61,10 @@ import (
"github.com/prometheus/prometheus/util/documentcli"
)
-var promqlEnableDelayedNameRemoval = false
+var (
+ promqlEnableDelayedNameRemoval = false
+ promtoolParserOpts parser.Options
+)
func init() {
// This can be removed when the legacy global mode is fully deprecated.
@@ -162,7 +165,11 @@ func main() {
checkRulesIgnoreUnknownFields := checkRulesCmd.Flag("ignore-unknown-fields", "Ignore unknown fields in the rule files. This is useful when you want to extend rule files with custom metadata. Ensure that those fields are removed before loading them into the Prometheus server as it performs strict checks by default.").Default("false").Bool()
checkMetricsCmd := checkCmd.Command("metrics", checkMetricsUsage)
- checkMetricsExtended := checkCmd.Flag("extended", "Print extended information related to the cardinality of the metrics.").Bool()
+ checkMetricsExtended := checkMetricsCmd.Flag("extended", "Print extended information related to the cardinality of the metrics.").Bool()
+ checkMetricsLint := checkMetricsCmd.Flag(
+ "lint",
+ "Linting checks to apply for metrics. Available options are: all, none. Use --lint=none to disable metrics linting.",
+ ).Default(lintOptionAll).String()
agentMode := checkConfigCmd.Flag("agent", "Check config file for Prometheus in Agent mode.").Bool()
queryCmd := app.Command("query", "Run query against a Prometheus server.")
@@ -257,12 +264,13 @@ func main() {
listHumanReadable := tsdbListCmd.Flag("human-readable", "Print human readable values.").Short('r').Bool()
listPath := tsdbListCmd.Arg("db path", "Database path (default is "+defaultDBPath+").").Default(defaultDBPath).String()
- tsdbDumpCmd := tsdbCmd.Command("dump", "Dump samples from a TSDB.")
+ tsdbDumpCmd := tsdbCmd.Command("dump", "Dump data (series+samples or optionally just series) from a TSDB.")
dumpPath := tsdbDumpCmd.Arg("db path", "Database path (default is "+defaultDBPath+").").Default(defaultDBPath).String()
dumpSandboxDirRoot := tsdbDumpCmd.Flag("sandbox-dir-root", "Root directory where a sandbox directory will be created, this sandbox is used in case WAL replay generates chunks (default is the database path). The sandbox is cleaned up at the end.").String()
dumpMinTime := tsdbDumpCmd.Flag("min-time", "Minimum timestamp to dump, in milliseconds since the Unix epoch.").Default(strconv.FormatInt(math.MinInt64, 10)).Int64()
dumpMaxTime := tsdbDumpCmd.Flag("max-time", "Maximum timestamp to dump, in milliseconds since the Unix epoch.").Default(strconv.FormatInt(math.MaxInt64, 10)).Int64()
dumpMatch := tsdbDumpCmd.Flag("match", "Series selector. Can be specified multiple times.").Default("{__name__=~'(?s:.*)'}").Strings()
+ dumpFormat := tsdbDumpCmd.Flag("format", "Output format of the dump (prom (default) or seriesjson).").Default("prom").Enum("prom", "seriesjson")
tsdbDumpOpenMetricsCmd := tsdbCmd.Command("dump-openmetrics", "[Experimental] Dump samples from a TSDB into OpenMetrics text format, excluding native histograms and staleness markers, which are not representable in OpenMetrics.")
dumpOpenMetricsPath := tsdbDumpOpenMetricsCmd.Arg("db path", "Database path (default is "+defaultDBPath+").").Default(defaultDBPath).String()
@@ -309,7 +317,7 @@ func main() {
promQLLabelsDeleteQuery := promQLLabelsDeleteCmd.Arg("query", "PromQL query.").Required().String()
promQLLabelsDeleteName := promQLLabelsDeleteCmd.Arg("name", "Name of the label to delete.").Required().String()
- featureList := app.Flag("enable-feature", "Comma separated feature names to enable. Valid options: promql-experimental-functions, promql-delayed-name-removal. See https://prometheus.io/docs/prometheus/latest/feature_flags/ for more details").Default("").Strings()
+ featureList := app.Flag("enable-feature", "Comma separated feature names to enable. Valid options: promql-experimental-functions, promql-delayed-name-removal, promql-duration-expr, promql-extended-range-selectors. See https://prometheus.io/docs/prometheus/latest/feature_flags/ for more details").Default("").Strings()
documentationCmd := app.Command("write-documentation", "Generate command line documentation. Internal use.").Hidden()
@@ -343,9 +351,13 @@ func main() {
for o := range strings.SplitSeq(f, ",") {
switch o {
case "promql-experimental-functions":
- parser.EnableExperimentalFunctions = true
+ promtoolParserOpts.EnableExperimentalFunctions = true
case "promql-delayed-name-removal":
promqlEnableDelayedNameRemoval = true
+ case "promql-duration-expr":
+ promtoolParserOpts.ExperimentalDurationExpr = true
+ case "promql-extended-range-selectors":
+ promtoolParserOpts.EnableExtendedRangeSelectors = true
case "":
continue
default:
@@ -353,13 +365,14 @@ func main() {
}
}
}
+ promtoolParser := parser.NewParser(promtoolParserOpts)
switch parsedCmd {
case sdCheckCmd.FullCommand():
os.Exit(CheckSD(*sdConfigFile, *sdJobName, *sdTimeout, prometheus.DefaultRegisterer))
case checkConfigCmd.FullCommand():
- os.Exit(CheckConfig(*agentMode, *checkConfigSyntaxOnly, newConfigLintConfig(*checkConfigLint, *checkConfigLintFatal, *checkConfigIgnoreUnknownFields, model.UTF8Validation, model.Duration(*checkLookbackDelta)), *configFiles...))
+ os.Exit(CheckConfig(*agentMode, *checkConfigSyntaxOnly, newConfigLintConfig(*checkConfigLint, *checkConfigLintFatal, *checkConfigIgnoreUnknownFields, model.UTF8Validation, model.Duration(*checkLookbackDelta)), promtoolParser, *configFiles...))
case checkServerHealthCmd.FullCommand():
os.Exit(checkErr(CheckServerStatus(serverURL, checkHealth, httpRoundTripper)))
@@ -371,10 +384,10 @@ func main() {
os.Exit(CheckWebConfig(*webConfigFiles...))
case checkRulesCmd.FullCommand():
- os.Exit(CheckRules(newRulesLintConfig(*checkRulesLint, *checkRulesLintFatal, *checkRulesIgnoreUnknownFields, model.UTF8Validation), *ruleFiles...))
+ os.Exit(CheckRules(newRulesLintConfig(*checkRulesLint, *checkRulesLintFatal, *checkRulesIgnoreUnknownFields, model.UTF8Validation), promtoolParser, *ruleFiles...))
case checkMetricsCmd.FullCommand():
- os.Exit(CheckMetrics(*checkMetricsExtended))
+ os.Exit(CheckMetrics(*checkMetricsExtended, *checkMetricsLint))
case pushMetricsCmd.FullCommand():
os.Exit(PushMetrics(remoteWriteURL, httpRoundTripper, *pushMetricsHeaders, *pushMetricsTimeout, *pushMetricsProtoMsg, *pushMetricsLabels, *metricFiles...))
@@ -411,6 +424,7 @@ func main() {
EnableNegativeOffset: true,
EnableDelayedNameRemoval: promqlEnableDelayedNameRemoval,
},
+ promtoolParser,
*testRulesRun,
*testRulesDiff,
*testRulesDebug,
@@ -422,15 +436,20 @@ func main() {
os.Exit(checkErr(benchmarkWrite(*benchWriteOutPath, *benchSamplesFile, *benchWriteNumMetrics, *benchWriteNumScrapes)))
case tsdbAnalyzeCmd.FullCommand():
- os.Exit(checkErr(analyzeBlock(ctx, *analyzePath, *analyzeBlockID, *analyzeLimit, *analyzeRunExtended, *analyzeMatchers)))
+ os.Exit(checkErr(analyzeBlock(ctx, *analyzePath, *analyzeBlockID, *analyzeLimit, *analyzeRunExtended, *analyzeMatchers, promtoolParser)))
case tsdbListCmd.FullCommand():
os.Exit(checkErr(listBlocks(*listPath, *listHumanReadable)))
case tsdbDumpCmd.FullCommand():
- os.Exit(checkErr(dumpSamples(ctx, *dumpPath, *dumpSandboxDirRoot, *dumpMinTime, *dumpMaxTime, *dumpMatch, formatSeriesSet)))
+ format := formatSeriesSet
+ if *dumpFormat == "seriesjson" {
+ format = formatSeriesSetLabelsToJSON
+ }
+ os.Exit(checkErr(dumpTSDBData(ctx, *dumpPath, *dumpSandboxDirRoot, *dumpMinTime, *dumpMaxTime, *dumpMatch, format, promtoolParser)))
+
case tsdbDumpOpenMetricsCmd.FullCommand():
- os.Exit(checkErr(dumpSamples(ctx, *dumpOpenMetricsPath, *dumpOpenMetricsSandboxDirRoot, *dumpOpenMetricsMinTime, *dumpOpenMetricsMaxTime, *dumpOpenMetricsMatch, formatSeriesSetOpenMetrics)))
+ os.Exit(checkErr(dumpTSDBData(ctx, *dumpOpenMetricsPath, *dumpOpenMetricsSandboxDirRoot, *dumpOpenMetricsMinTime, *dumpOpenMetricsMaxTime, *dumpOpenMetricsMatch, formatSeriesSetOpenMetrics, promtoolParser)))
// TODO(aSquare14): Work on adding support for custom block size.
case openMetricsImportCmd.FullCommand():
os.Exit(backfillOpenMetrics(*importFilePath, *importDBPath, *importHumanReadable, *importQuiet, *maxBlockDuration, *openMetricsLabels))
@@ -446,15 +465,15 @@ func main() {
case promQLFormatCmd.FullCommand():
checkExperimental(*experimental)
- os.Exit(checkErr(formatPromQL(*promQLFormatQuery)))
+ os.Exit(checkErr(formatPromQL(*promQLFormatQuery, promtoolParser)))
case promQLLabelsSetCmd.FullCommand():
checkExperimental(*experimental)
- os.Exit(checkErr(labelsSetPromQL(*promQLLabelsSetQuery, *promQLLabelsSetType, *promQLLabelsSetName, *promQLLabelsSetValue)))
+ os.Exit(checkErr(labelsSetPromQL(*promQLLabelsSetQuery, *promQLLabelsSetType, *promQLLabelsSetName, *promQLLabelsSetValue, promtoolParser)))
case promQLLabelsDeleteCmd.FullCommand():
checkExperimental(*experimental)
- os.Exit(checkErr(labelsDeletePromQL(*promQLLabelsDeleteQuery, *promQLLabelsDeleteName)))
+ os.Exit(checkErr(labelsDeletePromQL(*promQLLabelsDeleteQuery, *promQLLabelsDeleteName, promtoolParser)))
}
}
@@ -579,7 +598,7 @@ func CheckServerStatus(serverURL *url.URL, checkEndpoint string, roundTripper ht
}
// CheckConfig validates configuration files.
-func CheckConfig(agentMode, checkSyntaxOnly bool, lintSettings configLintConfig, files ...string) int {
+func CheckConfig(agentMode, checkSyntaxOnly bool, lintSettings configLintConfig, p parser.Parser, files ...string) int {
failed := false
hasErrors := false
@@ -600,7 +619,7 @@ func CheckConfig(agentMode, checkSyntaxOnly bool, lintSettings configLintConfig,
if !checkSyntaxOnly {
scrapeConfigsFailed := lintScrapeConfigs(scrapeConfigs, lintSettings)
failed = failed || scrapeConfigsFailed
- rulesFailed, rulesHaveErrors := checkRules(ruleFiles, lintSettings.rulesLintConfig)
+ rulesFailed, rulesHaveErrors := checkRules(ruleFiles, lintSettings.rulesLintConfig, p)
failed = failed || rulesFailed
hasErrors = hasErrors || rulesHaveErrors
}
@@ -827,13 +846,13 @@ func checkSDFile(filename string) ([]*targetgroup.Group, error) {
}
// CheckRules validates rule files.
-func CheckRules(ls rulesLintConfig, files ...string) int {
+func CheckRules(ls rulesLintConfig, p parser.Parser, files ...string) int {
failed := false
hasErrors := false
if len(files) == 0 {
- failed, hasErrors = checkRulesFromStdin(ls)
+ failed, hasErrors = checkRulesFromStdin(ls, p)
} else {
- failed, hasErrors = checkRules(files, ls)
+ failed, hasErrors = checkRules(files, ls, p)
}
if failed && hasErrors {
@@ -847,7 +866,7 @@ func CheckRules(ls rulesLintConfig, files ...string) int {
}
// checkRulesFromStdin validates rule from stdin.
-func checkRulesFromStdin(ls rulesLintConfig) (bool, bool) {
+func checkRulesFromStdin(ls rulesLintConfig, p parser.Parser) (bool, bool) {
failed := false
hasErrors := false
fmt.Println("Checking standard input")
@@ -856,7 +875,7 @@ func checkRulesFromStdin(ls rulesLintConfig) (bool, bool) {
fmt.Fprintln(os.Stderr, " FAILED:", err)
return true, true
}
- rgs, errs := rulefmt.Parse(data, ls.ignoreUnknownFields, ls.nameValidationScheme)
+ rgs, errs := rulefmt.Parse(data, ls.ignoreUnknownFields, ls.nameValidationScheme, p)
if errs != nil {
failed = true
fmt.Fprintln(os.Stderr, " FAILED:")
@@ -885,12 +904,12 @@ func checkRulesFromStdin(ls rulesLintConfig) (bool, bool) {
}
// checkRules validates rule files.
-func checkRules(files []string, ls rulesLintConfig) (bool, bool) {
+func checkRules(files []string, ls rulesLintConfig, p parser.Parser) (bool, bool) {
failed := false
hasErrors := false
for _, f := range files {
fmt.Println("Checking", f)
- rgs, errs := rulefmt.ParseFile(f, ls.ignoreUnknownFields, ls.nameValidationScheme)
+ rgs, errs := rulefmt.ParseFile(f, ls.ignoreUnknownFields, ls.nameValidationScheme, p)
if errs != nil {
failed = true
fmt.Fprintln(os.Stderr, " FAILED:")
@@ -929,11 +948,11 @@ func checkRuleGroups(rgs *rulefmt.RuleGroups, lintSettings rulesLintConfig) (int
dRules := checkDuplicates(rgs.Groups)
if len(dRules) != 0 {
var errMessage strings.Builder
- errMessage.WriteString(fmt.Sprintf("%d duplicate rule(s) found.\n", len(dRules)))
+ fmt.Fprintf(&errMessage, "%d duplicate rule(s) found.\n", len(dRules))
for _, n := range dRules {
- errMessage.WriteString(fmt.Sprintf("Metric: %s\nLabel(s):\n", n.metric))
+ fmt.Fprintf(&errMessage, "Metric: %s\nLabel(s):\n", n.metric)
n.label.Range(func(l labels.Label) {
- errMessage.WriteString(fmt.Sprintf("\t%s: %s\n", l.Name, l.Value))
+ fmt.Fprintf(&errMessage, "\t%s: %s\n", l.Name, l.Value)
})
}
errMessage.WriteString("Might cause inconsistency while recording expressions")
@@ -1012,36 +1031,53 @@ func ruleMetric(rule rulefmt.Rule) string {
}
var checkMetricsUsage = strings.TrimSpace(`
-Pass Prometheus metrics over stdin to lint them for consistency and correctness.
+Pass Prometheus metrics over stdin to lint them for consistency and correctness, and optionally perform cardinality analysis.
examples:
$ cat metrics.prom | promtool check metrics
-$ curl -s http://localhost:9090/metrics | promtool check metrics
+$ curl -s http://localhost:9090/metrics | promtool check metrics --extended
+
+$ curl -s http://localhost:9100/metrics | promtool check metrics --extended --lint=none
`)
// CheckMetrics performs a linting pass on input metrics.
-func CheckMetrics(extended bool) int {
- var buf bytes.Buffer
- tee := io.TeeReader(os.Stdin, &buf)
- l := promlint.New(tee)
- problems, err := l.Lint()
- if err != nil {
- fmt.Fprintln(os.Stderr, "error while linting:", err)
+func CheckMetrics(extended bool, lint string) int {
+ // Validate that at least one feature is enabled.
+ if !extended && lint == lintOptionNone {
+ fmt.Fprintln(os.Stderr, "error: at least one of --extended or linting must be enabled")
+ fmt.Fprintln(os.Stderr, "Use --extended for cardinality analysis, or remove --lint=none to enable linting")
return failureExitCode
}
- for _, p := range problems {
- fmt.Fprintln(os.Stderr, p.Metric, p.Text)
+ var buf bytes.Buffer
+ var (
+ problems []promlint.Problem
+ reader io.Reader
+ err error
+ )
+
+ if lint != lintOptionNone {
+ tee := io.TeeReader(os.Stdin, &buf)
+ l := promlint.New(tee)
+ problems, err = l.Lint()
+ if err != nil {
+ fmt.Fprintln(os.Stderr, "error while linting:", err)
+ return failureExitCode
+ }
+ for _, p := range problems {
+ fmt.Fprintln(os.Stderr, p.Metric, p.Text)
+ }
+ reader = &buf
+ } else {
+ reader = os.Stdin
}
- if len(problems) > 0 {
- return lintErrExitCode
- }
+ hasLintProblems := len(problems) > 0
if extended {
- stats, total, err := checkMetricsExtended(&buf)
+ stats, total, err := checkMetricsExtended(reader)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return failureExitCode
@@ -1055,6 +1091,10 @@ func CheckMetrics(extended bool) int {
w.Flush()
}
+ if hasLintProblems {
+ return lintErrExitCode
+ }
+
return successExitCode
}
@@ -1310,8 +1350,8 @@ func checkTargetGroupsForScrapeConfig(targetGroups []*targetgroup.Group, scfg *c
return nil
}
-func formatPromQL(query string) error {
- expr, err := parser.ParseExpr(query)
+func formatPromQL(query string, p parser.Parser) error {
+ expr, err := p.ParseExpr(query)
if err != nil {
return err
}
@@ -1320,8 +1360,8 @@ func formatPromQL(query string) error {
return nil
}
-func labelsSetPromQL(query, labelMatchType, name, value string) error {
- expr, err := parser.ParseExpr(query)
+func labelsSetPromQL(query, labelMatchType, name, value string, p parser.Parser) error {
+ expr, err := p.ParseExpr(query)
if err != nil {
return err
}
@@ -1365,8 +1405,8 @@ func labelsSetPromQL(query, labelMatchType, name, value string) error {
return nil
}
-func labelsDeletePromQL(query, name string) error {
- expr, err := parser.ParseExpr(query)
+func labelsDeletePromQL(query, name string, p parser.Parser) error {
+ expr, err := p.ParseExpr(query)
if err != nil {
return err
}
diff --git a/cmd/promtool/main_test.go b/cmd/promtool/main_test.go
index a9a54f6d5f..297dd35d70 100644
--- a/cmd/promtool/main_test.go
+++ b/cmd/promtool/main_test.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Prometheus Authors
+// 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
@@ -18,6 +18,7 @@ import (
"context"
"errors"
"fmt"
+ "io"
"net/http"
"net/http/httptest"
"net/url"
@@ -36,6 +37,7 @@ import (
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/model/rulefmt"
+ "github.com/prometheus/prometheus/promql/parser"
"github.com/prometheus/prometheus/promql/promqltest"
)
@@ -186,7 +188,7 @@ func TestCheckDuplicates(t *testing.T) {
c := test
t.Run(c.name, func(t *testing.T) {
t.Parallel()
- rgs, err := rulefmt.ParseFile(c.ruleFile, false, model.UTF8Validation)
+ rgs, err := rulefmt.ParseFile(c.ruleFile, false, model.UTF8Validation, parser.NewParser(parser.Options{}))
require.Empty(t, err)
dups := checkDuplicates(rgs.Groups)
require.Equal(t, c.expectedDups, dups)
@@ -195,7 +197,7 @@ func TestCheckDuplicates(t *testing.T) {
}
func BenchmarkCheckDuplicates(b *testing.B) {
- rgs, err := rulefmt.ParseFile("./testdata/rules_large.yml", false, model.UTF8Validation)
+ rgs, err := rulefmt.ParseFile("./testdata/rules_large.yml", false, model.UTF8Validation, parser.NewParser(parser.Options{}))
require.Empty(b, err)
for b.Loop() {
@@ -402,6 +404,99 @@ func TestCheckMetricsExtended(t *testing.T) {
}, stats)
}
+func TestCheckMetricsLintOptions(t *testing.T) {
+ if runtime.GOOS == "windows" {
+ t.Skip("Skipping on windows")
+ }
+
+ const testMetrics = `
+# HELP testMetric_CamelCase A test metric with camelCase
+# TYPE testMetric_CamelCase gauge
+testMetric_CamelCase{label="value1"} 1
+`
+
+ tests := []struct {
+ name string
+ lint string
+ extended bool
+ wantErrCode int
+ wantLint bool
+ wantCard bool
+ }{
+ {
+ name: "default_all_with_extended",
+ lint: lintOptionAll,
+ extended: true,
+ wantErrCode: lintErrExitCode,
+ wantLint: true,
+ wantCard: true,
+ },
+ {
+ name: "lint_none_with_extended",
+ lint: lintOptionNone,
+ extended: true,
+ wantErrCode: successExitCode,
+ wantLint: false,
+ wantCard: true,
+ },
+ {
+ name: "both_disabled_fails",
+ lint: lintOptionNone,
+ extended: false,
+ wantErrCode: failureExitCode,
+ wantLint: false,
+ wantCard: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ r, w, err := os.Pipe()
+ require.NoError(t, err)
+ _, err = w.WriteString(testMetrics)
+ require.NoError(t, err)
+ w.Close()
+
+ oldStdin := os.Stdin
+ os.Stdin = r
+ defer func() { os.Stdin = oldStdin }()
+
+ oldStdout := os.Stdout
+ oldStderr := os.Stderr
+ rOut, wOut, err := os.Pipe()
+ require.NoError(t, err)
+ rErr, wErr, err := os.Pipe()
+ require.NoError(t, err)
+ os.Stdout = wOut
+ os.Stderr = wErr
+
+ code := CheckMetrics(tt.extended, tt.lint)
+
+ wOut.Close()
+ wErr.Close()
+ os.Stdout = oldStdout
+ os.Stderr = oldStderr
+
+ var outBuf, errBuf bytes.Buffer
+ _, _ = io.Copy(&outBuf, rOut)
+ _, _ = io.Copy(&errBuf, rErr)
+
+ require.Equal(t, tt.wantErrCode, code)
+ if tt.wantLint {
+ require.Contains(t, errBuf.String(), "testMetric_CamelCase")
+ } else {
+ require.NotContains(t, errBuf.String(), "testMetric_CamelCase")
+ }
+
+ if tt.wantCard {
+ require.Contains(t, outBuf.String(), "Cardinality")
+ } else {
+ require.NotContains(t, outBuf.String(), "Cardinality")
+ }
+ })
+ }
+}
+
func TestExitCodes(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode.")
@@ -508,7 +603,7 @@ func TestCheckRules(t *testing.T) {
defer func(v *os.File) { os.Stdin = v }(os.Stdin)
os.Stdin = r
- exitCode := CheckRules(newRulesLintConfig(lintOptionDuplicateRules, false, false, model.UTF8Validation))
+ exitCode := CheckRules(newRulesLintConfig(lintOptionDuplicateRules, false, false, model.UTF8Validation), parser.NewParser(parser.Options{}))
require.Equal(t, successExitCode, exitCode)
})
@@ -530,7 +625,7 @@ func TestCheckRules(t *testing.T) {
defer func(v *os.File) { os.Stdin = v }(os.Stdin)
os.Stdin = r
- exitCode := CheckRules(newRulesLintConfig(lintOptionDuplicateRules, false, false, model.UTF8Validation))
+ exitCode := CheckRules(newRulesLintConfig(lintOptionDuplicateRules, false, false, model.UTF8Validation), parser.NewParser(parser.Options{}))
require.Equal(t, failureExitCode, exitCode)
})
@@ -552,7 +647,7 @@ func TestCheckRules(t *testing.T) {
defer func(v *os.File) { os.Stdin = v }(os.Stdin)
os.Stdin = r
- exitCode := CheckRules(newRulesLintConfig(lintOptionDuplicateRules, true, false, model.UTF8Validation))
+ exitCode := CheckRules(newRulesLintConfig(lintOptionDuplicateRules, true, false, model.UTF8Validation), parser.NewParser(parser.Options{}))
require.Equal(t, lintErrExitCode, exitCode)
})
}
@@ -561,7 +656,7 @@ func TestCheckRulesWithFeatureFlag(t *testing.T) {
// As opposed to TestCheckRules calling CheckRules directly we run promtool
// so the feature flag parsing can be tested.
- args := []string{"-test.main", "--enable-feature=promql-experimental-functions", "check", "rules", "testdata/features.yml"}
+ args := []string{"-test.main", "--enable-feature=promql-experimental-functions", "--enable-feature=promql-duration-expr", "--enable-feature=promql-extended-range-selectors", "check", "rules", "testdata/features.yml"}
tool := exec.Command(promtoolPath, args...)
err := tool.Run()
require.NoError(t, err)
@@ -570,19 +665,19 @@ func TestCheckRulesWithFeatureFlag(t *testing.T) {
func TestCheckRulesWithRuleFiles(t *testing.T) {
t.Run("rules-good", func(t *testing.T) {
t.Parallel()
- exitCode := CheckRules(newRulesLintConfig(lintOptionDuplicateRules, false, false, model.UTF8Validation), "./testdata/rules.yml")
+ exitCode := CheckRules(newRulesLintConfig(lintOptionDuplicateRules, false, false, model.UTF8Validation), parser.NewParser(parser.Options{}), "./testdata/rules.yml")
require.Equal(t, successExitCode, exitCode)
})
t.Run("rules-bad", func(t *testing.T) {
t.Parallel()
- exitCode := CheckRules(newRulesLintConfig(lintOptionDuplicateRules, false, false, model.UTF8Validation), "./testdata/rules-bad.yml")
+ exitCode := CheckRules(newRulesLintConfig(lintOptionDuplicateRules, false, false, model.UTF8Validation), parser.NewParser(parser.Options{}), "./testdata/rules-bad.yml")
require.Equal(t, failureExitCode, exitCode)
})
t.Run("rules-lint-fatal", func(t *testing.T) {
t.Parallel()
- exitCode := CheckRules(newRulesLintConfig(lintOptionDuplicateRules, true, false, model.UTF8Validation), "./testdata/prometheus-rules.lint.yml")
+ exitCode := CheckRules(newRulesLintConfig(lintOptionDuplicateRules, true, false, model.UTF8Validation), parser.NewParser(parser.Options{}), "./testdata/prometheus-rules.lint.yml")
require.Equal(t, lintErrExitCode, exitCode)
})
}
@@ -611,20 +706,21 @@ func TestCheckScrapeConfigs(t *testing.T) {
} {
t.Run(tc.name, func(t *testing.T) {
// Non-fatal linting.
- code := CheckConfig(false, false, newConfigLintConfig(lintOptionTooLongScrapeInterval, false, false, model.UTF8Validation, tc.lookbackDelta), "./testdata/prometheus-config.lint.too_long_scrape_interval.yml")
+ p := parser.NewParser(parser.Options{})
+ code := CheckConfig(false, false, newConfigLintConfig(lintOptionTooLongScrapeInterval, false, false, model.UTF8Validation, tc.lookbackDelta), p, "./testdata/prometheus-config.lint.too_long_scrape_interval.yml")
require.Equal(t, successExitCode, code, "Non-fatal linting should return success")
// Fatal linting.
- code = CheckConfig(false, false, newConfigLintConfig(lintOptionTooLongScrapeInterval, true, false, model.UTF8Validation, tc.lookbackDelta), "./testdata/prometheus-config.lint.too_long_scrape_interval.yml")
+ code = CheckConfig(false, false, newConfigLintConfig(lintOptionTooLongScrapeInterval, true, false, model.UTF8Validation, tc.lookbackDelta), p, "./testdata/prometheus-config.lint.too_long_scrape_interval.yml")
if tc.expectError {
require.Equal(t, lintErrExitCode, code, "Fatal linting should return error")
} else {
require.Equal(t, successExitCode, code, "Fatal linting should return success when there are no problems")
}
// Check syntax only, no linting.
- code = CheckConfig(false, true, newConfigLintConfig(lintOptionTooLongScrapeInterval, true, false, model.UTF8Validation, tc.lookbackDelta), "./testdata/prometheus-config.lint.too_long_scrape_interval.yml")
+ code = CheckConfig(false, true, newConfigLintConfig(lintOptionTooLongScrapeInterval, true, false, model.UTF8Validation, tc.lookbackDelta), p, "./testdata/prometheus-config.lint.too_long_scrape_interval.yml")
require.Equal(t, successExitCode, code, "Fatal linting should return success when checking syntax only")
// Lint option "none" should disable linting.
- code = CheckConfig(false, false, newConfigLintConfig(lintOptionNone+","+lintOptionTooLongScrapeInterval, true, false, model.UTF8Validation, tc.lookbackDelta), "./testdata/prometheus-config.lint.too_long_scrape_interval.yml")
+ code = CheckConfig(false, false, newConfigLintConfig(lintOptionNone+","+lintOptionTooLongScrapeInterval, true, false, model.UTF8Validation, tc.lookbackDelta), p, "./testdata/prometheus-config.lint.too_long_scrape_interval.yml")
require.Equal(t, successExitCode, code, `Fatal linting should return success when lint option "none" is specified`)
})
}
@@ -640,7 +736,6 @@ func TestTSDBDumpCommand(t *testing.T) {
load 1m
metric{foo="bar"} 1 2 3
`)
- t.Cleanup(func() { storage.Close() })
for _, c := range []struct {
name string
diff --git a/cmd/promtool/metrics.go b/cmd/promtool/metrics.go
index c21ef15fd8..b1a2beb72e 100644
--- a/cmd/promtool/metrics.go
+++ b/cmd/promtool/metrics.go
@@ -1,4 +1,4 @@
-// Copyright 2023 The Prometheus Authors
+// 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
diff --git a/cmd/promtool/metrics_test.go b/cmd/promtool/metrics_test.go
index 938f1cadfd..d5a3bf63cc 100644
--- a/cmd/promtool/metrics_test.go
+++ b/cmd/promtool/metrics_test.go
@@ -1,4 +1,4 @@
-// Copyright 2025 The Prometheus Authors
+// 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
diff --git a/cmd/promtool/query.go b/cmd/promtool/query.go
index 0d7cb12cf4..1342f148f8 100644
--- a/cmd/promtool/query.go
+++ b/cmd/promtool/query.go
@@ -1,4 +1,4 @@
-// Copyright 2023 The Prometheus Authors
+// 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
diff --git a/cmd/promtool/rules.go b/cmd/promtool/rules.go
index 98f2c38b58..bb45178e9c 100644
--- a/cmd/promtool/rules.go
+++ b/cmd/promtool/rules.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
@@ -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/rules_test.go b/cmd/promtool/rules_test.go
index 6fe7d8c5a1..678e2b4d50 100644
--- a/cmd/promtool/rules_test.go
+++ b/cmd/promtool/rules_test.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
diff --git a/cmd/promtool/sd.go b/cmd/promtool/sd.go
index 884864205c..6b844c699a 100644
--- a/cmd/promtool/sd.go
+++ b/cmd/promtool/sd.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
diff --git a/cmd/promtool/sd_test.go b/cmd/promtool/sd_test.go
index e41c9893b2..9f43764f55 100644
--- a/cmd/promtool/sd_test.go
+++ b/cmd/promtool/sd_test.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
diff --git a/cmd/promtool/testdata/dump-series-1.prom b/cmd/promtool/testdata/dump-series-1.prom
new file mode 100644
index 0000000000..5e44c0bf1b
--- /dev/null
+++ b/cmd/promtool/testdata/dump-series-1.prom
@@ -0,0 +1,3 @@
+{"__name__":"heavy_metric","foo":"bar"}
+{"__name__":"heavy_metric","foo":"foo"}
+{"__name__":"metric","baz":"abc","foo":"bar"}
diff --git a/cmd/promtool/testdata/dump-series-2.prom b/cmd/promtool/testdata/dump-series-2.prom
new file mode 100644
index 0000000000..fefefa6d1b
--- /dev/null
+++ b/cmd/promtool/testdata/dump-series-2.prom
@@ -0,0 +1,2 @@
+{"__name__":"heavy_metric","foo":"foo"}
+{"__name__":"metric","baz":"abc","foo":"bar"}
diff --git a/cmd/promtool/testdata/dump-series-3.prom b/cmd/promtool/testdata/dump-series-3.prom
new file mode 100644
index 0000000000..dd98e8707d
--- /dev/null
+++ b/cmd/promtool/testdata/dump-series-3.prom
@@ -0,0 +1 @@
+{"__name__":"metric","baz":"abc","foo":"bar"}
diff --git a/cmd/promtool/testdata/features.yml b/cmd/promtool/testdata/features.yml
index 769f8362bf..946e07d0d7 100644
--- a/cmd/promtool/testdata/features.yml
+++ b/cmd/promtool/testdata/features.yml
@@ -1,6 +1,10 @@
groups:
- name: features
rules:
- - record: x
- # We don't expect anything from this, just want to check the function parses.
+ # We don't expect anything from these, just want to check the syntax parses.
+ - record: promql-experimental-functions
expr: sort_by_label(up, "instance")
+ - record: promql-duration-expr
+ expr: rate(up[1m * 2])
+ - record: promql-extended-range-selectors
+ expr: rate(up[1m] anchored)
diff --git a/cmd/promtool/testdata/start-time-test.yml b/cmd/promtool/testdata/start-time-test.yml
new file mode 100644
index 0000000000..b7365366f4
--- /dev/null
+++ b/cmd/promtool/testdata/start-time-test.yml
@@ -0,0 +1,76 @@
+rule_files:
+ - rules.yml
+
+evaluation_interval: 1m
+
+tests:
+ # Test with default start_time (0 / Unix epoch).
+ - name: default_start_time
+ interval: 1m
+ promql_expr_test:
+ - expr: time()
+ eval_time: 0m
+ exp_samples:
+ - value: 0
+ - expr: time()
+ eval_time: 5m
+ exp_samples:
+ - value: 300
+
+ # Test with RFC3339 start_timestamp.
+ - name: rfc3339_start_timestamp
+ interval: 1m
+ start_timestamp: "2024-01-01T00:00:00Z"
+ promql_expr_test:
+ - expr: time()
+ eval_time: 0m
+ exp_samples:
+ - value: 1704067200
+ - expr: time()
+ eval_time: 5m
+ exp_samples:
+ - value: 1704067500
+
+ # Test with Unix timestamp start_timestamp.
+ - name: unix_timestamp_start_timestamp
+ interval: 1m
+ start_timestamp: 1609459200
+ input_series:
+ - series: test_metric
+ values: "1 1 1"
+ promql_expr_test:
+ - expr: time()
+ eval_time: 0m
+ exp_samples:
+ - value: 1609459200
+ - expr: time()
+ eval_time: 10m
+ exp_samples:
+ - value: 1609459800
+
+ # Test that input series samples are correctly timestamped with custom start_timestamp.
+ - name: samples_with_start_timestamp
+ interval: 1m
+ start_timestamp: "2024-01-01T00:00:00Z"
+ input_series:
+ - series: 'my_metric{label="test"}'
+ values: "10+10x15"
+ promql_expr_test:
+ # Query at absolute timestamp (start_timestamp = 1704067200).
+ - expr: my_metric@1704067200
+ eval_time: 5m
+ exp_samples:
+ - labels: 'my_metric{label="test"}'
+ value: 10
+ # Query at 2 minutes after start_timestamp (1704067200 + 120 = 1704067320).
+ - expr: my_metric@1704067320
+ eval_time: 5m
+ exp_samples:
+ - labels: 'my_metric{label="test"}'
+ value: 30
+ # Verify timestamp() function returns the absolute timestamp.
+ - expr: timestamp(my_metric)
+ eval_time: 5m
+ exp_samples:
+ - labels: '{label="test"}'
+ value: 1704067500
diff --git a/cmd/promtool/tsdb.go b/cmd/promtool/tsdb.go
index 1da95e862c..f43da0e1d0 100644
--- a/cmd/promtool/tsdb.go
+++ b/cmd/promtool/tsdb.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
@@ -17,6 +17,7 @@ import (
"bufio"
"bytes"
"context"
+ "encoding/json"
"errors"
"fmt"
"io"
@@ -42,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"
)
@@ -159,17 +159,14 @@ func (b *writeBenchmark) ingestScrapes(lbls []labels.Labels, scrapeCount int) (u
batch := lbls[:l]
lbls = lbls[l:]
- wg.Add(1)
- go func() {
- defer wg.Done()
-
+ wg.Go(func() {
n, err := b.ingestScrapesShard(batch, 100, int64(timeDelta*i))
if err != nil {
// exitWithError(err)
fmt.Println(" err", err)
}
total.Add(n)
- }()
+ })
}
wg.Wait()
}
@@ -338,7 +335,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 {
@@ -408,13 +405,13 @@ func openBlock(path, blockID string) (*tsdb.DBReadOnly, tsdb.BlockReader, error)
return db, b, nil
}
-func analyzeBlock(ctx context.Context, path, blockID string, limit int, runExtended bool, matchers string) error {
+func analyzeBlock(ctx context.Context, path, blockID string, limit int, runExtended bool, matchers string, p parser.Parser) error {
var (
selectors []*labels.Matcher
err error
)
if len(matchers) > 0 {
- selectors, err = parser.ParseMetricSelector(matchers)
+ selectors, err = p.ParseMetricSelector(matchers)
if err != nil {
return err
}
@@ -424,7 +421,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()
@@ -478,24 +475,24 @@ func analyzeBlock(ctx context.Context, path, blockID string, limit int, runExten
labelpairsCount := map[string]uint64{}
entries := 0
var (
- p index.Postings
- refs []storage.SeriesRef
+ postings index.Postings
+ refs []storage.SeriesRef
)
if len(matchers) > 0 {
- p, err = tsdb.PostingsForMatchers(ctx, ir, selectors...)
+ postings, err = tsdb.PostingsForMatchers(ctx, ir, selectors...)
if err != nil {
return err
}
// Expand refs first and cache in memory.
// So later we don't have to expand again.
- refs, err = index.ExpandPostings(p)
+ refs, err = index.ExpandPostings(postings)
if err != nil {
return err
}
fmt.Printf("Matched series: %d\n", len(refs))
- p = index.NewListPostings(refs)
+ postings = index.NewListPostings(refs)
} else {
- p, err = ir.Postings(ctx, "", "") // The special all key.
+ postings, err = ir.Postings(ctx, "", "") // The special all key.
if err != nil {
return err
}
@@ -503,8 +500,8 @@ func analyzeBlock(ctx context.Context, path, blockID string, limit int, runExten
chks := []chunks.Meta{}
builder := labels.ScratchBuilder{}
- for p.Next() {
- if err = ir.Series(p.At(), &builder, &chks); err != nil {
+ for postings.Next() {
+ if err = ir.Series(postings.At(), &builder, &chks); err != nil {
return err
}
// Amount of the block time range not covered by this series.
@@ -517,8 +514,8 @@ func analyzeBlock(ctx context.Context, path, blockID string, limit int, runExten
entries++
})
}
- if p.Err() != nil {
- return p.Err()
+ if postings.Err() != nil {
+ return postings.Err()
}
fmt.Printf("Postings (unique label pairs): %d\n", len(labelpairsUncovered))
fmt.Printf("Postings entries (total label pairs): %d\n", entries)
@@ -624,7 +621,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
@@ -706,13 +703,13 @@ func analyzeCompaction(ctx context.Context, block tsdb.BlockReader, indexr tsdb.
type SeriesSetFormatter func(series storage.SeriesSet) error
-func dumpSamples(ctx context.Context, dbDir, sandboxDirRoot string, mint, maxt int64, match []string, formatter SeriesSetFormatter) (err error) {
+func dumpTSDBData(ctx context.Context, dbDir, sandboxDirRoot string, mint, maxt int64, match []string, formatter SeriesSetFormatter, p parser.Parser) (err error) {
db, err := tsdb.OpenDBReadOnly(dbDir, sandboxDirRoot, nil)
if err != nil {
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 {
@@ -720,7 +717,7 @@ func dumpSamples(ctx context.Context, dbDir, sandboxDirRoot string, mint, maxt i
}
defer q.Close()
- matcherSets, err := parser.ParseMetricSelectors(match)
+ matcherSets, err := p.ParseMetricSelectors(match)
if err != nil {
return err
}
@@ -742,7 +739,7 @@ func dumpSamples(ctx context.Context, dbDir, sandboxDirRoot string, mint, maxt i
}
if ws := ss.Warnings(); len(ws) > 0 {
- return tsdb_errors.NewMulti(ws.AsErrors()...).Err()
+ return errors.Join(ws.AsErrors()...)
}
if ss.Err() != nil {
@@ -794,6 +791,30 @@ func CondensedString(ls labels.Labels) string {
return b.String()
}
+func formatSeriesSetLabelsToJSON(ss storage.SeriesSet) error {
+ seriesCache := make(map[string]struct{})
+ for ss.Next() {
+ series := ss.At()
+ lbs := series.Labels()
+
+ b, err := json.Marshal(lbs)
+ if err != nil {
+ return err
+ }
+
+ if len(b) == 0 {
+ continue
+ }
+
+ s := string(b)
+ if _, ok := seriesCache[s]; !ok {
+ fmt.Println(s)
+ seriesCache[s] = struct{}{}
+ }
+ }
+ return nil
+}
+
func formatSeriesSetOpenMetrics(ss storage.SeriesSet) error {
for ss.Next() {
series := ss.At()
diff --git a/cmd/promtool/tsdb_posix_test.go b/cmd/promtool/tsdb_posix_test.go
index 8a83aead70..9d0034844f 100644
--- a/cmd/promtool/tsdb_posix_test.go
+++ b/cmd/promtool/tsdb_posix_test.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
diff --git a/cmd/promtool/tsdb_test.go b/cmd/promtool/tsdb_test.go
index e745a3fe7a..86d7c67d77 100644
--- a/cmd/promtool/tsdb_test.go
+++ b/cmd/promtool/tsdb_test.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
@@ -27,6 +27,7 @@ import (
"github.com/stretchr/testify/require"
+ "github.com/prometheus/prometheus/promql/parser"
"github.com/prometheus/prometheus/promql/promqltest"
"github.com/prometheus/prometheus/tsdb"
)
@@ -63,7 +64,7 @@ func getDumpedSamples(t *testing.T, databasePath, sandboxDirRoot string, mint, m
r, w, _ := os.Pipe()
os.Stdout = w
- err := dumpSamples(
+ err := dumpTSDBData(
context.Background(),
databasePath,
sandboxDirRoot,
@@ -71,6 +72,7 @@ func getDumpedSamples(t *testing.T, databasePath, sandboxDirRoot string, mint, m
maxt,
match,
formatter,
+ parser.NewParser(parser.Options{}),
)
require.NoError(t, err)
@@ -97,7 +99,6 @@ func TestTSDBDump(t *testing.T) {
heavy_metric{foo="bar"} 5 4 3 2 1
heavy_metric{foo="foo"} 5 4 3 2 1
`)
- t.Cleanup(func() { storage.Close() })
tests := []struct {
name string
@@ -106,13 +107,15 @@ func TestTSDBDump(t *testing.T) {
sandboxDirRoot string
match []string
expectedDump string
+ expectedSeries string
}{
{
- name: "default match",
- mint: math.MinInt64,
- maxt: math.MaxInt64,
- match: []string{"{__name__=~'(?s:.*)'}"},
- expectedDump: "testdata/dump-test-1.prom",
+ name: "default match",
+ mint: math.MinInt64,
+ maxt: math.MaxInt64,
+ match: []string{"{__name__=~'(?s:.*)'}"},
+ expectedDump: "testdata/dump-test-1.prom",
+ expectedSeries: "testdata/dump-series-1.prom",
},
{
name: "default match with sandbox dir root set",
@@ -121,41 +124,47 @@ func TestTSDBDump(t *testing.T) {
sandboxDirRoot: t.TempDir(),
match: []string{"{__name__=~'(?s:.*)'}"},
expectedDump: "testdata/dump-test-1.prom",
+ expectedSeries: "testdata/dump-series-1.prom",
},
{
- name: "same matcher twice",
- mint: math.MinInt64,
- maxt: math.MaxInt64,
- match: []string{"{foo=~'.+'}", "{foo=~'.+'}"},
- expectedDump: "testdata/dump-test-1.prom",
+ name: "same matcher twice",
+ mint: math.MinInt64,
+ maxt: math.MaxInt64,
+ match: []string{"{foo=~'.+'}", "{foo=~'.+'}"},
+ expectedDump: "testdata/dump-test-1.prom",
+ expectedSeries: "testdata/dump-series-1.prom",
},
{
- name: "no duplication",
- mint: math.MinInt64,
- maxt: math.MaxInt64,
- match: []string{"{__name__=~'(?s:.*)'}", "{baz='abc'}"},
- expectedDump: "testdata/dump-test-1.prom",
+ name: "no duplication",
+ mint: math.MinInt64,
+ maxt: math.MaxInt64,
+ match: []string{"{__name__=~'(?s:.*)'}", "{baz='abc'}"},
+ expectedDump: "testdata/dump-test-1.prom",
+ expectedSeries: "testdata/dump-series-1.prom",
},
{
- name: "well merged",
- mint: math.MinInt64,
- maxt: math.MaxInt64,
- match: []string{"{__name__='heavy_metric'}", "{baz='abc'}"},
- expectedDump: "testdata/dump-test-1.prom",
+ name: "well merged",
+ mint: math.MinInt64,
+ maxt: math.MaxInt64,
+ match: []string{"{__name__='heavy_metric'}", "{baz='abc'}"},
+ expectedDump: "testdata/dump-test-1.prom",
+ expectedSeries: "testdata/dump-series-1.prom",
},
{
- name: "multi matchers",
- mint: math.MinInt64,
- maxt: math.MaxInt64,
- match: []string{"{__name__='heavy_metric',foo='foo'}", "{__name__='metric'}"},
- expectedDump: "testdata/dump-test-2.prom",
+ name: "multi matchers",
+ mint: math.MinInt64,
+ maxt: math.MaxInt64,
+ match: []string{"{__name__='heavy_metric',foo='foo'}", "{__name__='metric'}"},
+ expectedDump: "testdata/dump-test-2.prom",
+ expectedSeries: "testdata/dump-series-2.prom",
},
{
- name: "with reduced mint and maxt",
- mint: int64(60000),
- maxt: int64(120000),
- match: []string{"{__name__='metric'}"},
- expectedDump: "testdata/dump-test-3.prom",
+ name: "with reduced mint and maxt",
+ mint: int64(60000),
+ maxt: int64(120000),
+ match: []string{"{__name__='metric'}"},
+ expectedDump: "testdata/dump-test-3.prom",
+ expectedSeries: "testdata/dump-series-3.prom",
},
}
for _, tt := range tests {
@@ -166,6 +175,12 @@ func TestTSDBDump(t *testing.T) {
expectedMetrics = normalizeNewLine(expectedMetrics)
// Sort both, because Prometheus does not guarantee the output order.
require.Equal(t, sortLines(string(expectedMetrics)), sortLines(dumpedMetrics))
+
+ dumpedSeries := getDumpedSamples(t, storage.Dir(), tt.sandboxDirRoot, tt.mint, tt.maxt, tt.match, formatSeriesSetLabelsToJSON)
+ expectedSeries, err := os.ReadFile(tt.expectedSeries)
+ require.NoError(t, err)
+ expectedSeries = normalizeNewLine(expectedSeries)
+ require.Equal(t, sortLines(string(expectedSeries)), sortLines(dumpedSeries))
})
}
}
@@ -182,7 +197,6 @@ func TestTSDBDumpOpenMetrics(t *testing.T) {
my_counter{foo="bar", baz="abc"} 1 2 3 4 5
my_gauge{bar="foo", abc="baz"} 9 8 0 4 7
`)
- t.Cleanup(func() { storage.Close() })
tests := []struct {
name string
diff --git a/cmd/promtool/unittest.go b/cmd/promtool/unittest.go
index 15b5171645..dab452af64 100644
--- a/cmd/promtool/unittest.go
+++ b/cmd/promtool/unittest.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Prometheus Authors
+// 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
@@ -47,11 +47,11 @@ import (
// RulesUnitTest does unit testing of rules based on the unit testing files provided.
// More info about the file format can be found in the docs.
-func RulesUnitTest(queryOpts promqltest.LazyLoaderOpts, runStrings []string, diffFlag, debug, ignoreUnknownFields bool, files ...string) int {
- return RulesUnitTestResult(io.Discard, queryOpts, runStrings, diffFlag, debug, ignoreUnknownFields, files...)
+func RulesUnitTest(queryOpts promqltest.LazyLoaderOpts, p parser.Parser, runStrings []string, diffFlag, debug, ignoreUnknownFields bool, files ...string) int {
+ return RulesUnitTestResult(io.Discard, queryOpts, p, runStrings, diffFlag, debug, ignoreUnknownFields, files...)
}
-func RulesUnitTestResult(results io.Writer, queryOpts promqltest.LazyLoaderOpts, runStrings []string, diffFlag, debug, ignoreUnknownFields bool, files ...string) int {
+func RulesUnitTestResult(results io.Writer, queryOpts promqltest.LazyLoaderOpts, p parser.Parser, runStrings []string, diffFlag, debug, ignoreUnknownFields bool, files ...string) int {
failed := false
junit := &junitxml.JUnitXML{}
@@ -61,7 +61,7 @@ func RulesUnitTestResult(results io.Writer, queryOpts promqltest.LazyLoaderOpts,
}
for _, f := range files {
- if errs := ruleUnitTest(f, queryOpts, run, diffFlag, debug, ignoreUnknownFields, junit.Suite(f)); errs != nil {
+ if errs := ruleUnitTest(f, queryOpts, p, run, diffFlag, debug, ignoreUnknownFields, junit.Suite(f)); errs != nil {
fmt.Fprintln(os.Stderr, " FAILED:")
for _, e := range errs {
fmt.Fprintln(os.Stderr, e.Error())
@@ -83,7 +83,7 @@ func RulesUnitTestResult(results io.Writer, queryOpts promqltest.LazyLoaderOpts,
return successExitCode
}
-func ruleUnitTest(filename string, queryOpts promqltest.LazyLoaderOpts, run *regexp.Regexp, diffFlag, debug, ignoreUnknownFields bool, ts *junitxml.TestSuite) []error {
+func ruleUnitTest(filename string, queryOpts promqltest.LazyLoaderOpts, p parser.Parser, run *regexp.Regexp, diffFlag, debug, ignoreUnknownFields bool, ts *junitxml.TestSuite) []error {
b, err := os.ReadFile(filename)
if err != nil {
ts.Abort(err)
@@ -132,6 +132,7 @@ func ruleUnitTest(filename string, queryOpts promqltest.LazyLoaderOpts, run *reg
if t.Interval == 0 {
t.Interval = unitTestInp.EvaluationInterval
}
+ t.parser = p
ers := t.test(testname, evalInterval, groupOrderMap, queryOpts, diffFlag, debug, ignoreUnknownFields, unitTestInp.FuzzyCompare, unitTestInp.RuleFiles...)
if ers != nil {
for _, e := range ers {
@@ -188,15 +189,39 @@ func resolveAndGlobFilepaths(baseDir string, utf *unitTestFile) error {
return nil
}
+// testStartTimestamp wraps time.Time to support custom YAML unmarshaling.
+// It can parse both RFC3339 timestamps and Unix timestamps.
+type testStartTimestamp struct {
+ time.Time
+}
+
+// UnmarshalYAML implements custom YAML unmarshaling for testStartTimestamp.
+// It accepts both RFC3339 formatted strings and numeric Unix timestamps.
+func (t *testStartTimestamp) UnmarshalYAML(unmarshal func(any) error) error {
+ var s string
+ if err := unmarshal(&s); err != nil {
+ return err
+ }
+ parsed, err := parseTime(s)
+ if err != nil {
+ return err
+ }
+ t.Time = parsed
+ return nil
+}
+
// testGroup is a group of input series and tests associated with it.
type testGroup struct {
- Interval model.Duration `yaml:"interval"`
- InputSeries []series `yaml:"input_series"`
- AlertRuleTests []alertTestCase `yaml:"alert_rule_test,omitempty"`
- PromqlExprTests []promqlTestCase `yaml:"promql_expr_test,omitempty"`
- ExternalLabels labels.Labels `yaml:"external_labels,omitempty"`
- ExternalURL string `yaml:"external_url,omitempty"`
- TestGroupName string `yaml:"name,omitempty"`
+ Interval model.Duration `yaml:"interval"`
+ InputSeries []series `yaml:"input_series"`
+ AlertRuleTests []alertTestCase `yaml:"alert_rule_test,omitempty"`
+ PromqlExprTests []promqlTestCase `yaml:"promql_expr_test,omitempty"`
+ ExternalLabels labels.Labels `yaml:"external_labels,omitempty"`
+ ExternalURL string `yaml:"external_url,omitempty"`
+ TestGroupName string `yaml:"name,omitempty"`
+ StartTimestamp testStartTimestamp `yaml:"start_timestamp,omitempty"`
+
+ parser parser.Parser `yaml:"-"`
}
// test performs the unit tests.
@@ -209,6 +234,8 @@ func (tg *testGroup) test(testname string, evalInterval time.Duration, groupOrde
}()
}
// Setup testing suite.
+ // Set the start time from the test group.
+ queryOpts.StartTime = tg.StartTimestamp.Time
suite, err := promqltest.NewLazyLoader(tg.seriesLoadingString(), queryOpts)
if err != nil {
return []error{err}
@@ -228,6 +255,7 @@ func (tg *testGroup) test(testname string, evalInterval time.Duration, groupOrde
Context: context.Background(),
NotifyFunc: func(context.Context, string, ...*rules.Alert) {},
Logger: promslog.NewNopLogger(),
+ Parser: tg.parser,
}
m := rules.NewManager(opts)
groupsMap, ers := m.LoadGroups(time.Duration(tg.Interval), tg.ExternalLabels, tg.ExternalURL, nil, ignoreUnknownFields, ruleFiles...)
@@ -237,7 +265,12 @@ func (tg *testGroup) test(testname string, evalInterval time.Duration, groupOrde
groups := orderedGroups(groupsMap, groupOrderMap)
// Bounds for evaluating the rules.
- mint := time.Unix(0, 0).UTC()
+ var mint time.Time
+ if tg.StartTimestamp.IsZero() {
+ mint = time.Unix(0, 0).UTC()
+ } else {
+ mint = tg.StartTimestamp.Time
+ }
maxt := mint.Add(tg.maxEvalTime())
// Optional floating point compare fuzzing.
@@ -453,10 +486,10 @@ Outer:
var expSamples []parsedSample
for _, s := range testCase.ExpSamples {
- lb, err := parser.ParseMetric(s.Labels)
+ lb, err := tg.parser.ParseMetric(s.Labels)
var hist *histogram.FloatHistogram
if err == nil && s.Histogram != "" {
- _, values, parseErr := parser.ParseSeriesDesc("{} " + s.Histogram)
+ _, values, parseErr := tg.parser.ParseSeriesDesc("{} " + s.Histogram)
switch {
case parseErr != nil:
err = parseErr
@@ -528,9 +561,9 @@ Outer:
// seriesLoadingString returns the input series in PromQL notation.
func (tg *testGroup) seriesLoadingString() string {
var result strings.Builder
- result.WriteString(fmt.Sprintf("load %v\n", shortDuration(tg.Interval)))
+ fmt.Fprintf(&result, "load %v\n", shortDuration(tg.Interval))
for _, is := range tg.InputSeries {
- result.WriteString(fmt.Sprintf(" %v %v\n", is.Series, is.Values))
+ fmt.Fprintf(&result, " %v %v\n", is.Series, is.Values)
}
return result.String()
}
@@ -631,13 +664,14 @@ func (la labelsAndAnnotations) String() string {
if len(la) == 0 {
return "[]"
}
- s := "[\n0:" + indentLines("\n"+la[0].String(), " ")
+ var s strings.Builder
+ s.WriteString("[\n0:" + indentLines("\n"+la[0].String(), " "))
for i, l := range la[1:] {
- s += ",\n" + strconv.Itoa(i+1) + ":" + indentLines("\n"+l.String(), " ")
+ s.WriteString(",\n" + strconv.Itoa(i+1) + ":" + indentLines("\n"+l.String(), " "))
}
- s += "\n]"
+ s.WriteString("\n]")
- return s
+ return s.String()
}
type labelAndAnnotation struct {
@@ -688,11 +722,12 @@ func parsedSamplesString(pss []parsedSample) string {
if len(pss) == 0 {
return "nil"
}
- s := pss[0].String()
+ var s strings.Builder
+ s.WriteString(pss[0].String())
for _, ps := range pss[1:] {
- s += ", " + ps.String()
+ s.WriteString(", " + ps.String())
}
- return s
+ return s.String()
}
func (ps *parsedSample) String() string {
diff --git a/cmd/promtool/unittest_test.go b/cmd/promtool/unittest_test.go
index 566e0acbc6..ce317e5e41 100644
--- a/cmd/promtool/unittest_test.go
+++ b/cmd/promtool/unittest_test.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Prometheus Authors
+// 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
@@ -21,6 +21,7 @@ import (
"github.com/stretchr/testify/require"
+ "github.com/prometheus/prometheus/promql/parser"
"github.com/prometheus/prometheus/promql/promqltest"
"github.com/prometheus/prometheus/util/junitxml"
)
@@ -129,6 +130,16 @@ func TestRulesUnitTest(t *testing.T) {
},
want: 0,
},
+ {
+ name: "Start time tests",
+ args: args{
+ files: []string{"./testdata/start-time-test.yml"},
+ },
+ queryOpts: promqltest.LazyLoaderOpts{
+ EnableAtModifier: true,
+ },
+ want: 0,
+ },
}
reuseFiles := []string{}
reuseCount := [2]int{}
@@ -143,7 +154,7 @@ func TestRulesUnitTest(t *testing.T) {
}
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
- if got := RulesUnitTest(tt.queryOpts, nil, false, false, false, tt.args.files...); got != tt.want {
+ if got := RulesUnitTest(tt.queryOpts, parser.NewParser(parser.Options{}), nil, false, false, false, tt.args.files...); got != tt.want {
t.Errorf("RulesUnitTest() = %v, want %v", got, tt.want)
}
})
@@ -151,7 +162,7 @@ func TestRulesUnitTest(t *testing.T) {
t.Run("Junit xml output ", func(t *testing.T) {
t.Parallel()
var buf bytes.Buffer
- if got := RulesUnitTestResult(&buf, promqltest.LazyLoaderOpts{}, nil, false, false, false, reuseFiles...); got != 1 {
+ if got := RulesUnitTestResult(&buf, promqltest.LazyLoaderOpts{}, parser.NewParser(parser.Options{}), nil, false, false, false, reuseFiles...); got != 1 {
t.Errorf("RulesUnitTestResults() = %v, want 1", got)
}
var test junitxml.JUnitXML
@@ -267,7 +278,7 @@ func TestRulesUnitTestRun(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
- got := RulesUnitTest(tt.queryOpts, tt.args.run, false, false, tt.ignoreUnknownFields, tt.args.files...)
+ got := RulesUnitTest(tt.queryOpts, parser.NewParser(parser.Options{}), tt.args.run, false, false, tt.ignoreUnknownFields, tt.args.files...)
require.Equal(t, tt.want, got)
})
}
diff --git a/compliance/go.mod b/compliance/go.mod
new file mode 100644
index 0000000000..54adc20b6c
--- /dev/null
+++ b/compliance/go.mod
@@ -0,0 +1,26 @@
+module compliance
+
+go 1.25.5
+
+require github.com/prometheus/compliance/remotewrite v0.0.0-20260220101514-bccaa3a70275
+
+require (
+ github.com/cespare/xxhash/v2 v2.3.0 // indirect
+ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
+ github.com/gogo/protobuf v1.3.2 // indirect
+ github.com/golang/snappy v1.0.0 // indirect
+ github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 // indirect
+ github.com/klauspost/compress v1.18.1 // indirect
+ github.com/kr/pretty v0.3.1 // indirect
+ github.com/oklog/run v1.2.0 // indirect
+ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
+ github.com/prometheus/client_golang/exp v0.0.0-20250914183048-a974e0d45e0a // indirect
+ github.com/prometheus/client_model v0.6.2 // indirect
+ github.com/prometheus/common v0.67.2 // indirect
+ github.com/prometheus/prometheus v0.307.4-0.20251119130332-1174b0ce4f1f // indirect
+ github.com/stretchr/testify v1.11.1 // indirect
+ go.yaml.in/yaml/v2 v2.4.3 // indirect
+ golang.org/x/text v0.30.0 // indirect
+ google.golang.org/protobuf v1.36.10 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+)
diff --git a/compliance/go.sum b/compliance/go.sum
new file mode 100644
index 0000000000..6f273f49bd
--- /dev/null
+++ b/compliance/go.sum
@@ -0,0 +1,79 @@
+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
+github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 h1:cLN4IBkmkYZNnk7EAJ0BHIethd+J6LqxFNw5mSiI2bM=
+github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk=
+github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
+github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/oklog/run v1.2.0 h1:O8x3yXwah4A73hJdlrwo/2X6J62gE5qTMusH0dvz60E=
+github.com/oklog/run v1.2.0/go.mod h1:mgDbKRSwPhJfesJ4PntqFUbKQRZ50NgmZTSPlFA0YFk=
+github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_golang/exp v0.0.0-20250914183048-a974e0d45e0a h1:RF1vfKM34/3DbGNis22BGd6sDDY3XBi0eM7pYqmOEO0=
+github.com/prometheus/client_golang/exp v0.0.0-20250914183048-a974e0d45e0a/go.mod h1:FGJuwvfcPY0V5enm+w8zF1RNS062yugQtPPQp1c4Io4=
+github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
+github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
+github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8=
+github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko=
+github.com/prometheus/compliance/remotewrite v0.0.0-20260220101514-bccaa3a70275 h1:NLTtFqM00EuqtisYX9P+hQkjoxNxsR2oUQWDluyD2Xw=
+github.com/prometheus/compliance/remotewrite v0.0.0-20260220101514-bccaa3a70275/go.mod h1:VEPZGvpSBbzTKc5acnBj9ng4gfo1DZ4qBsCQnoNFiSc=
+github.com/prometheus/prometheus v0.307.4-0.20251119130332-1174b0ce4f1f h1:ERPCnBglv9Z4IjkEBTNbcHmZPlryMldXVWLkk7TeBIY=
+github.com/prometheus/prometheus v0.307.4-0.20251119130332-1174b0ce4f1f/go.mod h1:7hcXiGf9AXIKW2ehWWzxkvRYJTGmc2StUIJ8mprfxjg=
+github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
+github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
+github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
+go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
+golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
+google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/compliance/remote_write_sender_test.go b/compliance/remote_write_sender_test.go
new file mode 100644
index 0000000000..6840132bd3
--- /dev/null
+++ b/compliance/remote_write_sender_test.go
@@ -0,0 +1,93 @@
+// 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 compliance
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "html/template"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/prometheus/compliance/remotewrite/sender"
+)
+
+const (
+ scrapeConfigTemplate = `
+global:
+ scrape_interval: 1s
+
+remote_write:
+ - url: "{{.RemoteWriteEndpointURL}}"
+ protobuf_message: "{{.RemoteWriteMessage}}"
+ send_exemplars: true
+ queue_config:
+ retry_on_http_429: true
+ metadata_config:
+ send: true
+
+scrape_configs:
+ - job_name: "{{.ScrapeTargetJobName}}"
+ scrape_interval: 1s
+ scrape_protocols:
+ - PrometheusProto
+ - OpenMetricsText1.0.0
+ - PrometheusText0.0.4
+ static_configs:
+ - targets: ["{{.ScrapeTargetHostPort}}"]
+`
+)
+
+var scrapeConfigTmpl = template.Must(template.New("config").Parse(scrapeConfigTemplate))
+
+type internalPrometheus struct{}
+
+func (p internalPrometheus) Name() string { return "internal-prometheus" }
+
+// Run runs a cmd/prometheus main package as a test sender target, until ctx is done.
+func (p internalPrometheus) Run(ctx context.Context, opts sender.Options) error {
+ var buf bytes.Buffer
+ if err := scrapeConfigTmpl.Execute(&buf, opts); err != nil {
+ return fmt.Errorf("failed to execute config template: %w", err)
+ }
+
+ dir, err := os.MkdirTemp("", "test-*")
+ if err != nil {
+ return err
+ }
+ configFile := filepath.Join(dir, "config.yaml")
+ if err := os.WriteFile(configFile, buf.Bytes(), 0o600); err != nil {
+ return err
+ }
+ defer os.RemoveAll(dir)
+
+ return sender.RunCommand(ctx, "../cmd/prometheus", nil,
+ "go", "run", ".",
+ "--web.listen-address=0.0.0.0:0",
+ fmt.Sprintf("--storage.tsdb.path=%v", dir),
+ fmt.Sprintf("--config.file=%s", configFile),
+ // Set important flags for the full remote write compliance:
+ "--enable-feature=st-storage",
+ )
+}
+
+var _ sender.Sender = internalPrometheus{}
+
+// TestRemoteWriteSender runs remote write sender compliance tests defined in
+// https://github.com/prometheus/compliance/tree/main/remotewrite/sender
+func TestRemoteWriteSender(t *testing.T) {
+ sender.RunTests(t, internalPrometheus{}, sender.ComplianceTests())
+}
diff --git a/config/config.go b/config/config.go
index 30c8a8ed21..d721d7fb86 100644
--- a/config/config.go
+++ b/config/config.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
@@ -149,6 +149,10 @@ func LoadFile(filename string, agentMode bool, logger *slog.Logger) (*Config, er
return cfg, nil
}
+func boolPtr(b bool) *bool {
+ return &b
+}
+
// The defaults applied before parsing the respective config sections.
var (
// DefaultConfig is the default top-level configuration.
@@ -158,7 +162,6 @@ var (
OTLPConfig: DefaultOTLPConfig,
}
- f bool
// DefaultGlobalConfig is the default global configuration.
DefaultGlobalConfig = GlobalConfig{
ScrapeInterval: model.Duration(1 * time.Minute),
@@ -173,9 +176,10 @@ var (
ScrapeProtocols: nil,
// When the native histogram feature flag is enabled,
// ScrapeNativeHistograms default changes to true.
- ScrapeNativeHistograms: &f,
+ ScrapeNativeHistograms: boolPtr(false),
ConvertClassicHistogramsToNHCB: false,
AlwaysScrapeClassicHistograms: false,
+ ExtraScrapeMetrics: boolPtr(false),
MetricNameValidationScheme: model.UTF8Validation,
MetricNameEscapingScheme: model.AllowUTF8,
}
@@ -513,6 +517,10 @@ type GlobalConfig struct {
ConvertClassicHistogramsToNHCB bool `yaml:"convert_classic_histograms_to_nhcb,omitempty"`
// Whether to scrape a classic histogram, even if it is also exposed as a native histogram.
AlwaysScrapeClassicHistograms bool `yaml:"always_scrape_classic_histograms,omitempty"`
+ // Whether to enable additional scrape metrics.
+ // When enabled, Prometheus stores samples for scrape_timeout_seconds,
+ // scrape_sample_limit, and scrape_body_size_bytes.
+ ExtraScrapeMetrics *bool `yaml:"extra_scrape_metrics,omitempty"`
}
// ScrapeProtocol represents supported protocol for scraping metrics.
@@ -652,6 +660,9 @@ func (c *GlobalConfig) UnmarshalYAML(unmarshal func(any) error) error {
if gc.ScrapeNativeHistograms == nil {
gc.ScrapeNativeHistograms = DefaultGlobalConfig.ScrapeNativeHistograms
}
+ if gc.ExtraScrapeMetrics == nil {
+ gc.ExtraScrapeMetrics = DefaultGlobalConfig.ExtraScrapeMetrics
+ }
if gc.ScrapeProtocols == nil {
if DefaultGlobalConfig.ScrapeProtocols != nil {
// This is the case where the defaults are set due to a feature flag.
@@ -687,7 +698,17 @@ func (c *GlobalConfig) isZero() bool {
c.ScrapeProtocols == nil &&
c.ScrapeNativeHistograms == nil &&
!c.ConvertClassicHistogramsToNHCB &&
- !c.AlwaysScrapeClassicHistograms
+ !c.AlwaysScrapeClassicHistograms &&
+ c.BodySizeLimit == 0 &&
+ c.SampleLimit == 0 &&
+ c.TargetLimit == 0 &&
+ c.LabelLimit == 0 &&
+ c.LabelNameLengthLimit == 0 &&
+ c.LabelValueLengthLimit == 0 &&
+ c.KeepDroppedTargets == 0 &&
+ c.MetricNameValidationScheme == model.UnsetValidation &&
+ c.MetricNameEscapingScheme == "" &&
+ c.ExtraScrapeMetrics == nil
}
const DefaultGoGCPercentage = 75
@@ -796,6 +817,11 @@ type ScrapeConfig struct {
// blank in config files but must have a value if a ScrapeConfig is created
// programmatically.
MetricNameEscapingScheme string `yaml:"metric_name_escaping_scheme,omitempty"`
+ // Whether to enable additional scrape metrics.
+ // When enabled, Prometheus stores samples for scrape_timeout_seconds,
+ // scrape_sample_limit, and scrape_body_size_bytes.
+ // If not set (nil), inherits the value from the global configuration.
+ ExtraScrapeMetrics *bool `yaml:"extra_scrape_metrics,omitempty"`
// We cannot do proper Go type embedding below as the parser will then parse
// values arbitrarily into the overflow maps of further-down types.
@@ -897,6 +923,9 @@ func (c *ScrapeConfig) Validate(globalConfig GlobalConfig) error {
if c.ScrapeNativeHistograms == nil {
c.ScrapeNativeHistograms = globalConfig.ScrapeNativeHistograms
}
+ if c.ExtraScrapeMetrics == nil {
+ c.ExtraScrapeMetrics = globalConfig.ExtraScrapeMetrics
+ }
if c.ScrapeProtocols == nil {
switch {
@@ -1022,7 +1051,7 @@ func ToEscapingScheme(s string, v model.ValidationScheme) (model.EscapingScheme,
case model.LegacyValidation:
return model.UnderscoreEscaping, nil
case model.UnsetValidation:
- return model.NoEscaping, fmt.Errorf("v is unset: %s", v)
+ return model.NoEscaping, fmt.Errorf("ValidationScheme is unset: %s", v)
default:
panic(fmt.Errorf("unhandled validation scheme: %s", v))
}
@@ -1045,6 +1074,11 @@ func (c *ScrapeConfig) AlwaysScrapeClassicHistogramsEnabled() bool {
return c.AlwaysScrapeClassicHistograms != nil && *c.AlwaysScrapeClassicHistograms
}
+// ExtraScrapeMetricsEnabled returns whether to enable extra scrape metrics.
+func (c *ScrapeConfig) ExtraScrapeMetricsEnabled() bool {
+ return c.ExtraScrapeMetrics != nil && *c.ExtraScrapeMetrics
+}
+
// StorageConfig configures runtime reloadable configuration options.
type StorageConfig struct {
TSDBConfig *TSDBConfig `yaml:"tsdb,omitempty"`
@@ -1073,6 +1107,10 @@ type TSDBConfig struct {
// This should not be used directly and must be converted into OutOfOrderTimeWindow.
OutOfOrderTimeWindowFlag model.Duration `yaml:"out_of_order_time_window,omitempty"`
+ // StaleSeriesCompactionThreshold is a number between 0.0-1.0 indicating the % of stale series in
+ // the in-memory Head block. If the % of stale series crosses this threshold, stale series compaction is run immediately.
+ StaleSeriesCompactionThreshold float64 `yaml:"stale_series_compaction_threshold,omitempty"`
+
Retention *TSDBRetentionConfig `yaml:"retention,omitempty"`
}
diff --git a/config/config_default_test.go b/config/config_default_test.go
index e5f43e1f50..91c290ae4e 100644
--- a/config/config_default_test.go
+++ b/config/config_default_test.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
diff --git a/config/config_test.go b/config/config_test.go
index 28c8f2196d..968b563e1e 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
@@ -74,10 +74,6 @@ func mustParseURL(u string) *config.URL {
return &config.URL{URL: parsed}
}
-func boolPtr(b bool) *bool {
- return &b
-}
-
const (
globBodySizeLimit = 15 * units.MiB
globSampleLimit = 1500
@@ -109,6 +105,7 @@ var expectedConf = &Config{
ScrapeNativeHistograms: boolPtr(false),
AlwaysScrapeClassicHistograms: false,
ConvertClassicHistogramsToNHCB: false,
+ ExtraScrapeMetrics: boolPtr(false),
MetricNameValidationScheme: model.UTF8Validation,
},
@@ -236,6 +233,7 @@ var expectedConf = &Config{
ScrapeNativeHistograms: boolPtr(false),
AlwaysScrapeClassicHistograms: boolPtr(false),
ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
MetricsPath: DefaultScrapeConfig.MetricsPath,
Scheme: DefaultScrapeConfig.Scheme,
@@ -360,6 +358,7 @@ var expectedConf = &Config{
ScrapeNativeHistograms: boolPtr(false),
AlwaysScrapeClassicHistograms: boolPtr(false),
ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
HTTPClientConfig: config.HTTPClientConfig{
BasicAuth: &config.BasicAuth{
@@ -470,6 +469,7 @@ var expectedConf = &Config{
ScrapeNativeHistograms: boolPtr(false),
AlwaysScrapeClassicHistograms: boolPtr(false),
ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
MetricsPath: DefaultScrapeConfig.MetricsPath,
Scheme: DefaultScrapeConfig.Scheme,
@@ -532,6 +532,7 @@ var expectedConf = &Config{
ScrapeNativeHistograms: boolPtr(false),
AlwaysScrapeClassicHistograms: boolPtr(false),
ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
MetricsPath: "/metrics",
Scheme: "http",
@@ -571,6 +572,7 @@ var expectedConf = &Config{
ScrapeNativeHistograms: boolPtr(false),
AlwaysScrapeClassicHistograms: boolPtr(false),
ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
MetricsPath: DefaultScrapeConfig.MetricsPath,
Scheme: DefaultScrapeConfig.Scheme,
@@ -616,6 +618,7 @@ var expectedConf = &Config{
ScrapeNativeHistograms: boolPtr(false),
AlwaysScrapeClassicHistograms: boolPtr(false),
ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
MetricsPath: DefaultScrapeConfig.MetricsPath,
Scheme: DefaultScrapeConfig.Scheme,
@@ -661,6 +664,7 @@ var expectedConf = &Config{
ScrapeNativeHistograms: boolPtr(false),
AlwaysScrapeClassicHistograms: boolPtr(false),
ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
MetricsPath: DefaultScrapeConfig.MetricsPath,
Scheme: DefaultScrapeConfig.Scheme,
@@ -696,6 +700,7 @@ var expectedConf = &Config{
ScrapeNativeHistograms: boolPtr(false),
AlwaysScrapeClassicHistograms: boolPtr(false),
ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
MetricsPath: DefaultScrapeConfig.MetricsPath,
Scheme: DefaultScrapeConfig.Scheme,
@@ -739,6 +744,7 @@ var expectedConf = &Config{
ScrapeNativeHistograms: boolPtr(false),
AlwaysScrapeClassicHistograms: boolPtr(false),
ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
MetricsPath: DefaultScrapeConfig.MetricsPath,
Scheme: DefaultScrapeConfig.Scheme,
@@ -779,6 +785,7 @@ var expectedConf = &Config{
ScrapeNativeHistograms: boolPtr(false),
AlwaysScrapeClassicHistograms: boolPtr(false),
ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
MetricsPath: DefaultScrapeConfig.MetricsPath,
Scheme: DefaultScrapeConfig.Scheme,
@@ -826,6 +833,7 @@ var expectedConf = &Config{
ScrapeNativeHistograms: boolPtr(false),
AlwaysScrapeClassicHistograms: boolPtr(false),
ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
MetricsPath: DefaultScrapeConfig.MetricsPath,
Scheme: DefaultScrapeConfig.Scheme,
@@ -863,6 +871,7 @@ var expectedConf = &Config{
ScrapeNativeHistograms: boolPtr(false),
AlwaysScrapeClassicHistograms: boolPtr(false),
ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
MetricsPath: DefaultScrapeConfig.MetricsPath,
Scheme: DefaultScrapeConfig.Scheme,
@@ -903,6 +912,7 @@ var expectedConf = &Config{
ScrapeNativeHistograms: boolPtr(false),
AlwaysScrapeClassicHistograms: boolPtr(false),
ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
MetricsPath: DefaultScrapeConfig.MetricsPath,
Scheme: DefaultScrapeConfig.Scheme,
@@ -936,6 +946,7 @@ var expectedConf = &Config{
ScrapeNativeHistograms: boolPtr(false),
AlwaysScrapeClassicHistograms: boolPtr(false),
ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
MetricsPath: DefaultScrapeConfig.MetricsPath,
Scheme: DefaultScrapeConfig.Scheme,
@@ -972,6 +983,7 @@ var expectedConf = &Config{
ScrapeNativeHistograms: boolPtr(false),
AlwaysScrapeClassicHistograms: boolPtr(false),
ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
MetricsPath: "/federate",
Scheme: DefaultScrapeConfig.Scheme,
@@ -1008,6 +1020,7 @@ var expectedConf = &Config{
ScrapeNativeHistograms: boolPtr(false),
AlwaysScrapeClassicHistograms: boolPtr(false),
ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
MetricsPath: DefaultScrapeConfig.MetricsPath,
Scheme: DefaultScrapeConfig.Scheme,
@@ -1044,6 +1057,7 @@ var expectedConf = &Config{
ScrapeNativeHistograms: boolPtr(false),
AlwaysScrapeClassicHistograms: boolPtr(false),
ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
MetricsPath: DefaultScrapeConfig.MetricsPath,
Scheme: DefaultScrapeConfig.Scheme,
@@ -1077,6 +1091,7 @@ var expectedConf = &Config{
ScrapeNativeHistograms: boolPtr(false),
AlwaysScrapeClassicHistograms: boolPtr(false),
ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
MetricsPath: DefaultScrapeConfig.MetricsPath,
Scheme: DefaultScrapeConfig.Scheme,
@@ -1118,6 +1133,7 @@ var expectedConf = &Config{
ScrapeNativeHistograms: boolPtr(false),
AlwaysScrapeClassicHistograms: boolPtr(false),
ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
MetricsPath: DefaultScrapeConfig.MetricsPath,
Scheme: DefaultScrapeConfig.Scheme,
@@ -1158,6 +1174,7 @@ var expectedConf = &Config{
ScrapeNativeHistograms: boolPtr(false),
AlwaysScrapeClassicHistograms: boolPtr(false),
ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
MetricsPath: DefaultScrapeConfig.MetricsPath,
Scheme: DefaultScrapeConfig.Scheme,
@@ -1195,6 +1212,7 @@ var expectedConf = &Config{
ScrapeNativeHistograms: boolPtr(false),
AlwaysScrapeClassicHistograms: boolPtr(false),
ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
MetricsPath: DefaultScrapeConfig.MetricsPath,
Scheme: DefaultScrapeConfig.Scheme,
@@ -1231,6 +1249,7 @@ var expectedConf = &Config{
ScrapeNativeHistograms: boolPtr(false),
AlwaysScrapeClassicHistograms: boolPtr(false),
ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
MetricsPath: DefaultScrapeConfig.MetricsPath,
Scheme: DefaultScrapeConfig.Scheme,
@@ -1271,6 +1290,7 @@ var expectedConf = &Config{
ScrapeNativeHistograms: boolPtr(false),
AlwaysScrapeClassicHistograms: boolPtr(false),
ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
MetricsPath: DefaultScrapeConfig.MetricsPath,
Scheme: DefaultScrapeConfig.Scheme,
@@ -1314,6 +1334,7 @@ var expectedConf = &Config{
ScrapeNativeHistograms: boolPtr(true),
AlwaysScrapeClassicHistograms: boolPtr(false),
ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
MetricsPath: DefaultScrapeConfig.MetricsPath,
Scheme: DefaultScrapeConfig.Scheme,
@@ -1377,6 +1398,7 @@ var expectedConf = &Config{
ScrapeNativeHistograms: boolPtr(false),
AlwaysScrapeClassicHistograms: boolPtr(false),
ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
MetricsPath: DefaultScrapeConfig.MetricsPath,
Scheme: DefaultScrapeConfig.Scheme,
@@ -1410,6 +1432,7 @@ var expectedConf = &Config{
ScrapeNativeHistograms: boolPtr(false),
AlwaysScrapeClassicHistograms: boolPtr(false),
ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
HTTPClientConfig: config.DefaultHTTPClientConfig,
MetricsPath: DefaultScrapeConfig.MetricsPath,
@@ -1454,6 +1477,7 @@ var expectedConf = &Config{
ScrapeNativeHistograms: boolPtr(false),
AlwaysScrapeClassicHistograms: boolPtr(false),
ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
HTTPClientConfig: config.DefaultHTTPClientConfig,
MetricsPath: DefaultScrapeConfig.MetricsPath,
@@ -1504,6 +1528,7 @@ var expectedConf = &Config{
ScrapeNativeHistograms: boolPtr(false),
AlwaysScrapeClassicHistograms: boolPtr(false),
ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
MetricsPath: DefaultScrapeConfig.MetricsPath,
Scheme: DefaultScrapeConfig.Scheme,
@@ -1544,6 +1569,7 @@ var expectedConf = &Config{
ScrapeNativeHistograms: boolPtr(false),
AlwaysScrapeClassicHistograms: boolPtr(false),
ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
MetricsPath: DefaultScrapeConfig.MetricsPath,
Scheme: DefaultScrapeConfig.Scheme,
@@ -1585,6 +1611,7 @@ var expectedConf = &Config{
ScrapeNativeHistograms: boolPtr(false),
AlwaysScrapeClassicHistograms: boolPtr(false),
ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
HTTPClientConfig: config.DefaultHTTPClientConfig,
MetricsPath: DefaultScrapeConfig.MetricsPath,
@@ -1621,6 +1648,7 @@ var expectedConf = &Config{
ScrapeNativeHistograms: boolPtr(false),
AlwaysScrapeClassicHistograms: boolPtr(false),
ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
MetricsPath: DefaultScrapeConfig.MetricsPath,
Scheme: DefaultScrapeConfig.Scheme,
@@ -1659,6 +1687,7 @@ var expectedConf = &Config{
ScrapeNativeHistograms: boolPtr(false),
AlwaysScrapeClassicHistograms: boolPtr(false),
ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
MetricsPath: DefaultScrapeConfig.MetricsPath,
Scheme: DefaultScrapeConfig.Scheme,
@@ -1704,8 +1733,9 @@ var expectedConf = &Config{
},
StorageConfig: StorageConfig{
TSDBConfig: &TSDBConfig{
- OutOfOrderTimeWindow: 30 * time.Minute.Milliseconds(),
- OutOfOrderTimeWindowFlag: model.Duration(30 * time.Minute),
+ OutOfOrderTimeWindow: 30 * time.Minute.Milliseconds(),
+ OutOfOrderTimeWindowFlag: model.Duration(30 * time.Minute),
+ StaleSeriesCompactionThreshold: 0.5,
Retention: &TSDBRetentionConfig{
Time: model.Duration(24 * time.Hour),
Size: 1 * units.GiB,
@@ -2663,12 +2693,87 @@ func TestAgentMode(t *testing.T) {
)
}
-func TestEmptyGlobalBlock(t *testing.T) {
- c, err := Load("global:\n", promslog.NewNopLogger())
- require.NoError(t, err)
- exp := DefaultConfig
- exp.loaded = true
- require.Equal(t, exp, *c)
+func TestGlobalConfig(t *testing.T) {
+ t.Run("empty block restores defaults", func(t *testing.T) {
+ c, err := Load("global:\n", promslog.NewNopLogger())
+ require.NoError(t, err)
+ exp := DefaultConfig
+ exp.loaded = true
+ require.Equal(t, exp, *c)
+ })
+
+ // Verify that isZero() correctly identifies non-zero configurations for all
+ // fields in GlobalConfig. This is important because isZero() is used during
+ // YAML unmarshaling to detect empty global blocks that should be replaced
+ // with defaults.
+ t.Run("isZero", func(t *testing.T) {
+ for _, tc := range []struct {
+ name string
+ config GlobalConfig
+ expectZero bool
+ }{
+ {
+ name: "empty GlobalConfig",
+ config: GlobalConfig{},
+ expectZero: true,
+ },
+ {
+ name: "ScrapeInterval set",
+ config: GlobalConfig{ScrapeInterval: model.Duration(30 * time.Second)},
+ expectZero: false,
+ },
+ {
+ name: "BodySizeLimit set",
+ config: GlobalConfig{BodySizeLimit: 1 * units.MiB},
+ expectZero: false,
+ },
+ {
+ name: "SampleLimit set",
+ config: GlobalConfig{SampleLimit: 1000},
+ expectZero: false,
+ },
+ {
+ name: "TargetLimit set",
+ config: GlobalConfig{TargetLimit: 500},
+ expectZero: false,
+ },
+ {
+ name: "LabelLimit set",
+ config: GlobalConfig{LabelLimit: 100},
+ expectZero: false,
+ },
+ {
+ name: "LabelNameLengthLimit set",
+ config: GlobalConfig{LabelNameLengthLimit: 50},
+ expectZero: false,
+ },
+ {
+ name: "LabelValueLengthLimit set",
+ config: GlobalConfig{LabelValueLengthLimit: 200},
+ expectZero: false,
+ },
+ {
+ name: "KeepDroppedTargets set",
+ config: GlobalConfig{KeepDroppedTargets: 10},
+ expectZero: false,
+ },
+ {
+ name: "MetricNameValidationScheme set",
+ config: GlobalConfig{MetricNameValidationScheme: model.LegacyValidation},
+ expectZero: false,
+ },
+ {
+ name: "MetricNameEscapingScheme set",
+ config: GlobalConfig{MetricNameEscapingScheme: model.EscapeUnderscores},
+ expectZero: false,
+ },
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ result := tc.config.isZero()
+ require.Equal(t, tc.expectZero, result)
+ })
+ }
+ })
}
// ScrapeConfigOptions contains options for creating a scrape config.
@@ -2680,6 +2785,7 @@ type ScrapeConfigOptions struct {
ScrapeNativeHistograms bool
AlwaysScrapeClassicHistograms bool
ConvertClassicHistToNHCB bool
+ ExtraScrapeMetrics bool
}
func TestGetScrapeConfigs(t *testing.T) {
@@ -2713,6 +2819,7 @@ func TestGetScrapeConfigs(t *testing.T) {
ScrapeNativeHistograms: boolPtr(opts.ScrapeNativeHistograms),
AlwaysScrapeClassicHistograms: boolPtr(opts.AlwaysScrapeClassicHistograms),
ConvertClassicHistogramsToNHCB: boolPtr(opts.ConvertClassicHistToNHCB),
+ ExtraScrapeMetrics: boolPtr(opts.ExtraScrapeMetrics),
}
if opts.ScrapeProtocols == nil {
sc.ScrapeProtocols = DefaultScrapeProtocols
@@ -2796,6 +2903,7 @@ func TestGetScrapeConfigs(t *testing.T) {
ScrapeNativeHistograms: boolPtr(false),
AlwaysScrapeClassicHistograms: boolPtr(false),
ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
MetricsPath: DefaultScrapeConfig.MetricsPath,
Scheme: DefaultScrapeConfig.Scheme,
@@ -2834,6 +2942,7 @@ func TestGetScrapeConfigs(t *testing.T) {
ScrapeNativeHistograms: boolPtr(false),
AlwaysScrapeClassicHistograms: boolPtr(false),
ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
HTTPClientConfig: config.HTTPClientConfig{
TLSConfig: config.TLSConfig{
@@ -2946,6 +3055,26 @@ func TestGetScrapeConfigs(t *testing.T) {
configFile: "testdata/global_scrape_protocols_and_local_disable_scrape_native_hist.good.yml",
expectedResult: []*ScrapeConfig{sc(ScrapeConfigOptions{JobName: "prometheus", ScrapeInterval: model.Duration(60 * time.Second), ScrapeTimeout: model.Duration(10 * time.Second), ScrapeNativeHistograms: false, ScrapeProtocols: []ScrapeProtocol{PrometheusText0_0_4}})},
},
+ {
+ name: "A global config that enables extra scrape metrics",
+ configFile: "testdata/global_enable_extra_scrape_metrics.good.yml",
+ expectedResult: []*ScrapeConfig{sc(ScrapeConfigOptions{JobName: "prometheus", ScrapeInterval: model.Duration(60 * time.Second), ScrapeTimeout: model.Duration(10 * time.Second), ExtraScrapeMetrics: true})},
+ },
+ {
+ name: "A global config that disables extra scrape metrics",
+ configFile: "testdata/global_disable_extra_scrape_metrics.good.yml",
+ expectedResult: []*ScrapeConfig{sc(ScrapeConfigOptions{JobName: "prometheus", ScrapeInterval: model.Duration(60 * time.Second), ScrapeTimeout: model.Duration(10 * time.Second), ExtraScrapeMetrics: false})},
+ },
+ {
+ name: "A global config that disables extra scrape metrics and scrape config that enables it",
+ configFile: "testdata/local_enable_extra_scrape_metrics.good.yml",
+ expectedResult: []*ScrapeConfig{sc(ScrapeConfigOptions{JobName: "prometheus", ScrapeInterval: model.Duration(60 * time.Second), ScrapeTimeout: model.Duration(10 * time.Second), ExtraScrapeMetrics: true})},
+ },
+ {
+ name: "A global config that enables extra scrape metrics and scrape config that disables it",
+ configFile: "testdata/local_disable_extra_scrape_metrics.good.yml",
+ expectedResult: []*ScrapeConfig{sc(ScrapeConfigOptions{JobName: "prometheus", ScrapeInterval: model.Duration(60 * time.Second), ScrapeTimeout: model.Duration(10 * time.Second), ExtraScrapeMetrics: false})},
+ },
}
for _, tc := range testCases {
@@ -2962,6 +3091,99 @@ func TestGetScrapeConfigs(t *testing.T) {
}
}
+func TestExtraScrapeMetrics(t *testing.T) {
+ tests := []struct {
+ name string
+ config string
+ expectGlobal *bool
+ expectEnabled bool
+ }{
+ {
+ name: "default values (not set)",
+ config: `
+scrape_configs:
+ - job_name: test
+ static_configs:
+ - targets: ['localhost:9090']
+`,
+ expectGlobal: boolPtr(false), // inherits from DefaultGlobalConfig
+ expectEnabled: false,
+ },
+ {
+ name: "global enabled",
+ config: `
+global:
+ extra_scrape_metrics: true
+scrape_configs:
+ - job_name: test
+ static_configs:
+ - targets: ['localhost:9090']
+`,
+ expectGlobal: boolPtr(true),
+ expectEnabled: true,
+ },
+ {
+ name: "global disabled",
+ config: `
+global:
+ extra_scrape_metrics: false
+scrape_configs:
+ - job_name: test
+ static_configs:
+ - targets: ['localhost:9090']
+`,
+ expectGlobal: boolPtr(false),
+ expectEnabled: false,
+ },
+ {
+ name: "scrape override enabled",
+ config: `
+global:
+ extra_scrape_metrics: false
+scrape_configs:
+ - job_name: test
+ extra_scrape_metrics: true
+ static_configs:
+ - targets: ['localhost:9090']
+`,
+ expectGlobal: boolPtr(false),
+ expectEnabled: true,
+ },
+ {
+ name: "scrape override disabled",
+ config: `
+global:
+ extra_scrape_metrics: true
+scrape_configs:
+ - job_name: test
+ extra_scrape_metrics: false
+ static_configs:
+ - targets: ['localhost:9090']
+`,
+ expectGlobal: boolPtr(true),
+ expectEnabled: false,
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ cfg, err := Load(tc.config, promslog.NewNopLogger())
+ require.NoError(t, err)
+
+ // Check global config
+ require.Equal(t, tc.expectGlobal, cfg.GlobalConfig.ExtraScrapeMetrics)
+
+ // Check scrape config
+ scfgs, err := cfg.GetScrapeConfigs()
+ require.NoError(t, err)
+ require.Len(t, scfgs, 1)
+
+ // Check the effective value via the helper method
+ require.Equal(t, tc.expectEnabled, scfgs[0].ExtraScrapeMetricsEnabled())
+ })
+ }
+}
+
func kubernetesSDHostURL() config.URL {
tURL, _ := url.Parse("https://localhost:1234")
return config.URL{URL: tURL}
diff --git a/config/config_windows_test.go b/config/config_windows_test.go
index 9d338b99e7..72a56ff41a 100644
--- a/config/config_windows_test.go
+++ b/config/config_windows_test.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
diff --git a/config/reload.go b/config/reload.go
index 07a077a6a9..a250693169 100644
--- a/config/reload.go
+++ b/config/reload.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Prometheus Authors
+// 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
diff --git a/config/reload_test.go b/config/reload_test.go
index 3e77260ab3..cb60d47651 100644
--- a/config/reload_test.go
+++ b/config/reload_test.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Prometheus Authors
+// 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
diff --git a/config/testdata/conf.good.yml b/config/testdata/conf.good.yml
index 7aa53b3b74..96bf9e2b33 100644
--- a/config/testdata/conf.good.yml
+++ b/config/testdata/conf.good.yml
@@ -453,6 +453,7 @@ alerting:
storage:
tsdb:
out_of_order_time_window: 30m
+ stale_series_compaction_threshold: 0.5
retention:
time: 1d
size: 1GB
diff --git a/config/testdata/global_disable_extra_scrape_metrics.good.yml b/config/testdata/global_disable_extra_scrape_metrics.good.yml
new file mode 100644
index 0000000000..26c6e4b8b5
--- /dev/null
+++ b/config/testdata/global_disable_extra_scrape_metrics.good.yml
@@ -0,0 +1,6 @@
+global:
+ extra_scrape_metrics: false
+scrape_configs:
+ - job_name: prometheus
+ static_configs:
+ - targets: ['localhost:8080']
diff --git a/config/testdata/global_enable_extra_scrape_metrics.good.yml b/config/testdata/global_enable_extra_scrape_metrics.good.yml
new file mode 100644
index 0000000000..1d7ea2db1c
--- /dev/null
+++ b/config/testdata/global_enable_extra_scrape_metrics.good.yml
@@ -0,0 +1,6 @@
+global:
+ extra_scrape_metrics: true
+scrape_configs:
+ - job_name: prometheus
+ static_configs:
+ - targets: ['localhost:8080']
diff --git a/config/testdata/local_disable_extra_scrape_metrics.good.yml b/config/testdata/local_disable_extra_scrape_metrics.good.yml
new file mode 100644
index 0000000000..a1b7c646fa
--- /dev/null
+++ b/config/testdata/local_disable_extra_scrape_metrics.good.yml
@@ -0,0 +1,7 @@
+global:
+ extra_scrape_metrics: true
+scrape_configs:
+ - job_name: prometheus
+ static_configs:
+ - targets: ['localhost:8080']
+ extra_scrape_metrics: false
diff --git a/config/testdata/local_enable_extra_scrape_metrics.good.yml b/config/testdata/local_enable_extra_scrape_metrics.good.yml
new file mode 100644
index 0000000000..a1c8b2808e
--- /dev/null
+++ b/config/testdata/local_enable_extra_scrape_metrics.good.yml
@@ -0,0 +1,7 @@
+global:
+ extra_scrape_metrics: false
+scrape_configs:
+ - job_name: prometheus
+ static_configs:
+ - targets: ['localhost:8080']
+ extra_scrape_metrics: true
diff --git a/discovery/README.md b/discovery/README.md
index d5418e7fb1..5d1adcf145 100644
--- a/discovery/README.md
+++ b/discovery/README.md
@@ -50,7 +50,7 @@ file for use with `file_sd`.
The general principle with SD is to extract all the potentially useful
information we can out of the SD, and let the user choose what they need of it
using
-[relabelling](https://prometheus.io/docs/operating/configuration/#).
+[relabelling](https://prometheus.io/docs/operating/configuration/#relabel_config).
This information is generally termed metadata.
Metadata is exposed as a set of key/value pairs (labels) per target. The keys
diff --git a/discovery/aws/aws.go b/discovery/aws/aws.go
new file mode 100644
index 0000000000..f0f9c3d4df
--- /dev/null
+++ b/discovery/aws/aws.go
@@ -0,0 +1,336 @@
+// 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 aws
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "time"
+
+ awsConfig "github.com/aws/aws-sdk-go-v2/config"
+ "github.com/aws/aws-sdk-go-v2/feature/ec2/imds"
+ "github.com/prometheus/client_golang/prometheus"
+ "github.com/prometheus/common/config"
+ "github.com/prometheus/common/model"
+
+ "github.com/prometheus/prometheus/discovery"
+)
+
+// DefaultSDConfig is the default AWS SD configuration.
+var DefaultSDConfig = SDConfig{
+ RefreshInterval: model.Duration(60 * time.Second),
+ HTTPClientConfig: config.DefaultHTTPClientConfig,
+}
+
+func init() {
+ discovery.RegisterConfig(&SDConfig{})
+}
+
+// Role is role of the service in AWS.
+type Role string
+
+// The valid options for Role.
+const (
+ RoleEC2 Role = "ec2"
+ RoleECS Role = "ecs"
+ RoleElasticache Role = "elasticache"
+ RoleLightsail Role = "lightsail"
+ RoleMSK Role = "msk"
+)
+
+// UnmarshalYAML implements the yaml.Unmarshaler interface.
+func (c *Role) UnmarshalYAML(unmarshal func(any) error) error {
+ if err := unmarshal((*string)(c)); err != nil {
+ return err
+ }
+ switch *c {
+ case RoleEC2, RoleECS, RoleElasticache, RoleLightsail, RoleMSK:
+ return nil
+ default:
+ return fmt.Errorf("unknown AWS SD role %q", *c)
+ }
+}
+
+func (c Role) String() string {
+ return string(c)
+}
+
+// SDConfig is the configuration for AWS service discovery.
+type SDConfig struct {
+ Role Role `yaml:"role"`
+ Region string `yaml:"region,omitempty"`
+ Endpoint string `yaml:"endpoint,omitempty"`
+ AccessKey string `yaml:"access_key,omitempty"`
+ SecretKey config.Secret `yaml:"secret_key,omitempty"`
+ Profile string `yaml:"profile,omitempty"`
+ RoleARN string `yaml:"role_arn,omitempty"`
+ RefreshInterval model.Duration `yaml:"refresh_interval,omitempty"`
+ Port int `yaml:"port,omitempty"`
+ HTTPClientConfig config.HTTPClientConfig `yaml:",inline"`
+
+ // ec2 specific
+ Filters []*EC2Filter `yaml:"filters,omitempty"`
+
+ // ecs, msk specific
+ Clusters []string `yaml:"clusters,omitempty"`
+
+ // Embedded sub-configs (internal use only, not serialized)
+ *EC2SDConfig `yaml:"-"`
+ *ECSSDConfig `yaml:"-"`
+ *ElasticacheSDConfig `yaml:"-"`
+ *LightsailSDConfig `yaml:"-"`
+ *MSKSDConfig `yaml:"-"`
+}
+
+// UnmarshalYAML implements the yaml.Unmarshaler interface for SDConfig.
+func (c *SDConfig) UnmarshalYAML(unmarshal func(any) error) error {
+ // Alias to avoid recursion
+ type plain SDConfig
+ var aux plain
+ // Unmarshal into aux
+ if err := unmarshal(&aux); err != nil {
+ return err
+ }
+ *c = SDConfig(aux)
+
+ var err error
+ c.Region, err = loadRegion(context.Background(), c.Region)
+ if err != nil {
+ return fmt.Errorf("could not determine AWS region: %w", err)
+ }
+
+ switch c.Role {
+ case RoleEC2:
+ if c.EC2SDConfig == nil {
+ ec2Config := DefaultEC2SDConfig
+ c.EC2SDConfig = &ec2Config
+ }
+ c.EC2SDConfig.HTTPClientConfig = c.HTTPClientConfig
+ c.EC2SDConfig.Region = c.Region
+ if c.Endpoint != "" {
+ c.EC2SDConfig.Endpoint = c.Endpoint
+ }
+ if c.AccessKey != "" {
+ c.EC2SDConfig.AccessKey = c.AccessKey
+ }
+ if c.SecretKey != "" {
+ c.EC2SDConfig.SecretKey = c.SecretKey
+ }
+ if c.Profile != "" {
+ c.EC2SDConfig.Profile = c.Profile
+ }
+ if c.RoleARN != "" {
+ c.EC2SDConfig.RoleARN = c.RoleARN
+ }
+ if c.Port != 0 {
+ c.EC2SDConfig.Port = c.Port
+ }
+ if c.RefreshInterval != 0 {
+ c.EC2SDConfig.RefreshInterval = c.RefreshInterval
+ }
+ if c.Filters != nil {
+ c.EC2SDConfig.Filters = c.Filters
+ }
+ case RoleECS:
+ if c.ECSSDConfig == nil {
+ ecsConfig := DefaultECSSDConfig
+ c.ECSSDConfig = &ecsConfig
+ }
+ c.ECSSDConfig.HTTPClientConfig = c.HTTPClientConfig
+ c.ECSSDConfig.Region = c.Region
+ if c.Endpoint != "" {
+ c.ECSSDConfig.Endpoint = c.Endpoint
+ }
+ if c.AccessKey != "" {
+ c.ECSSDConfig.AccessKey = c.AccessKey
+ }
+ if c.SecretKey != "" {
+ c.ECSSDConfig.SecretKey = c.SecretKey
+ }
+ if c.Profile != "" {
+ c.ECSSDConfig.Profile = c.Profile
+ }
+ if c.RoleARN != "" {
+ c.ECSSDConfig.RoleARN = c.RoleARN
+ }
+ if c.Port != 0 {
+ c.ECSSDConfig.Port = c.Port
+ }
+ if c.RefreshInterval != 0 {
+ c.ECSSDConfig.RefreshInterval = c.RefreshInterval
+ }
+ if c.Clusters != nil {
+ c.ECSSDConfig.Clusters = c.Clusters
+ }
+ case RoleElasticache:
+ if c.ElasticacheSDConfig == nil {
+ elasticacheConfig := DefaultElasticacheSDConfig
+ c.ElasticacheSDConfig = &elasticacheConfig
+ }
+ c.ElasticacheSDConfig.HTTPClientConfig = c.HTTPClientConfig
+ c.ElasticacheSDConfig.Region = c.Region
+ if c.Endpoint != "" {
+ c.ElasticacheSDConfig.Endpoint = c.Endpoint
+ }
+ if c.AccessKey != "" {
+ c.ElasticacheSDConfig.AccessKey = c.AccessKey
+ }
+ if c.SecretKey != "" {
+ c.ElasticacheSDConfig.SecretKey = c.SecretKey
+ }
+ if c.Profile != "" {
+ c.ElasticacheSDConfig.Profile = c.Profile
+ }
+ if c.RoleARN != "" {
+ c.ElasticacheSDConfig.RoleARN = c.RoleARN
+ }
+ if c.Port != 0 {
+ c.ElasticacheSDConfig.Port = c.Port
+ }
+ if c.RefreshInterval != 0 {
+ c.ElasticacheSDConfig.RefreshInterval = c.RefreshInterval
+ }
+ if c.Clusters != nil {
+ c.ElasticacheSDConfig.Clusters = c.Clusters
+ }
+ case RoleLightsail:
+ if c.LightsailSDConfig == nil {
+ lightsailConfig := DefaultLightsailSDConfig
+ c.LightsailSDConfig = &lightsailConfig
+ }
+ c.LightsailSDConfig.HTTPClientConfig = c.HTTPClientConfig
+ c.LightsailSDConfig.Region = c.Region
+ if c.Endpoint != "" {
+ c.LightsailSDConfig.Endpoint = c.Endpoint
+ }
+ if c.AccessKey != "" {
+ c.LightsailSDConfig.AccessKey = c.AccessKey
+ }
+ if c.SecretKey != "" {
+ c.LightsailSDConfig.SecretKey = c.SecretKey
+ }
+ if c.Profile != "" {
+ c.LightsailSDConfig.Profile = c.Profile
+ }
+ if c.RoleARN != "" {
+ c.LightsailSDConfig.RoleARN = c.RoleARN
+ }
+ if c.Port != 0 {
+ c.LightsailSDConfig.Port = c.Port
+ }
+ if c.RefreshInterval != 0 {
+ c.LightsailSDConfig.RefreshInterval = c.RefreshInterval
+ }
+ case RoleMSK:
+ if c.MSKSDConfig == nil {
+ mskConfig := DefaultMSKSDConfig
+ c.MSKSDConfig = &mskConfig
+ }
+ c.MSKSDConfig.HTTPClientConfig = c.HTTPClientConfig
+ c.MSKSDConfig.Region = c.Region
+ if c.Endpoint != "" {
+ c.MSKSDConfig.Endpoint = c.Endpoint
+ }
+ if c.AccessKey != "" {
+ c.MSKSDConfig.AccessKey = c.AccessKey
+ }
+ if c.SecretKey != "" {
+ c.MSKSDConfig.SecretKey = c.SecretKey
+ }
+ if c.Profile != "" {
+ c.MSKSDConfig.Profile = c.Profile
+ }
+ if c.RoleARN != "" {
+ c.MSKSDConfig.RoleARN = c.RoleARN
+ }
+ if c.Port != 0 {
+ c.MSKSDConfig.Port = c.Port
+ }
+ if c.RefreshInterval != 0 {
+ c.MSKSDConfig.RefreshInterval = c.RefreshInterval
+ }
+ if c.Clusters != nil {
+ c.MSKSDConfig.Clusters = c.Clusters
+ }
+ default:
+ return fmt.Errorf("unknown AWS SD role %q", c.Role)
+ }
+ return nil
+}
+
+// Name returns the name of the AWS Config.
+func (*SDConfig) Name() string { return "aws" }
+
+// NewDiscovererMetrics implements discovery.Config.
+func (*SDConfig) NewDiscovererMetrics(_ prometheus.Registerer, rmi discovery.RefreshMetricsInstantiator) discovery.DiscovererMetrics {
+ return &awsMetrics{refreshMetrics: rmi}
+}
+
+// NewDiscoverer returns a Discoverer for the AWS Config.
+func (c *SDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) {
+ awsMetrics, ok := opts.Metrics.(*awsMetrics)
+ if !ok {
+ return nil, errors.New("invalid discovery metrics type for AWS SD")
+ }
+
+ switch c.Role {
+ case RoleEC2:
+ opts.Metrics = &ec2Metrics{refreshMetrics: awsMetrics.refreshMetrics}
+ return NewEC2Discovery(c.EC2SDConfig, opts)
+ case RoleECS:
+ opts.Metrics = &ecsMetrics{refreshMetrics: awsMetrics.refreshMetrics}
+ return NewECSDiscovery(c.ECSSDConfig, opts)
+ case RoleElasticache:
+ opts.Metrics = &elasticacheMetrics{refreshMetrics: awsMetrics.refreshMetrics}
+ return NewElasticacheDiscovery(c.ElasticacheSDConfig, opts)
+ case RoleLightsail:
+ opts.Metrics = &lightsailMetrics{refreshMetrics: awsMetrics.refreshMetrics}
+ return NewLightsailDiscovery(c.LightsailSDConfig, opts)
+ case RoleMSK:
+ opts.Metrics = &mskMetrics{refreshMetrics: awsMetrics.refreshMetrics}
+ return NewMSKDiscovery(c.MSKSDConfig, opts)
+ default:
+ return nil, fmt.Errorf("unknown AWS SD role %q", c.Role)
+ }
+}
+
+// loadRegion finds the region in order: AWS config/env vars ->IMDS.
+func loadRegion(ctx context.Context, specifiedRegion string) (string, error) {
+ if specifiedRegion != "" {
+ return specifiedRegion, nil
+ }
+
+ cfg, err := awsConfig.LoadDefaultConfig(ctx)
+ if err != nil {
+ return "", fmt.Errorf("failed to load AWS config: %w", err)
+ }
+
+ if cfg.Region != "" {
+ return cfg.Region, nil
+ }
+
+ // Fallback (may fail in non-AWS environments)
+ imdsClient := imds.NewFromConfig(cfg)
+ region, err := imdsClient.GetRegion(ctx, &imds.GetRegionInput{})
+ if err != nil {
+ return "", fmt.Errorf("failed to get region from IMDS: %w", err)
+ }
+
+ if region.Region == "" {
+ return "", errors.New("region not found in AWS config or IMDS")
+ }
+
+ return region.Region, nil
+}
diff --git a/discovery/aws/aws_test.go b/discovery/aws/aws_test.go
new file mode 100644
index 0000000000..d1ec7b2282
--- /dev/null
+++ b/discovery/aws/aws_test.go
@@ -0,0 +1,489 @@
+// 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 aws
+
+import (
+ "context"
+ "errors"
+ "math/rand/v2"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "path/filepath"
+ "testing"
+ "time"
+
+ "github.com/prometheus/common/model"
+ "github.com/stretchr/testify/require"
+ "go.yaml.in/yaml/v3"
+)
+
+func TestRoleUnmarshalYAML(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ expected Role
+ wantErr bool
+ }{
+ {
+ name: "EC2Role",
+ input: "ec2",
+ expected: RoleEC2,
+ wantErr: false,
+ },
+ {
+ name: "LightsailRole",
+ input: "lightsail",
+ expected: RoleLightsail,
+ wantErr: false,
+ },
+ {
+ name: "ECSRole",
+ input: "ecs",
+ expected: RoleECS,
+ wantErr: false,
+ },
+ {
+ name: "InvalidRole",
+ input: "invalid",
+ expected: "invalid",
+ wantErr: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ var r Role
+ err := r.UnmarshalYAML(func(v any) error {
+ ptr, ok := v.(*string)
+ if !ok {
+ return errors.New("not a string pointer")
+ }
+ *ptr = tt.input
+ return nil
+ })
+ if tt.wantErr {
+ require.Error(t, err, "expected error for input %q", tt.input)
+ } else {
+ require.NoError(t, err, "unexpected error for input %q", tt.input)
+ require.Equal(t, tt.expected, r, "unexpected role for input %q", tt.input)
+ }
+ })
+ }
+}
+
+func TestRoleString(t *testing.T) {
+ tests := []struct {
+ name string
+ role Role
+ expected string
+ }{
+ {
+ name: "EC2",
+ role: RoleEC2,
+ expected: "ec2",
+ },
+ {
+ name: "Lightsail",
+ role: RoleLightsail,
+ expected: "lightsail",
+ },
+ {
+ name: "ECS",
+ role: RoleECS,
+ expected: "ecs",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ require.Equal(t, tt.expected, tt.role.String())
+ })
+ }
+}
+
+func TestSDConfigName(t *testing.T) {
+ cfg := &SDConfig{}
+ require.Equal(t, "aws", cfg.Name())
+}
+
+func TestDefaultSDConfig(t *testing.T) {
+ require.Equal(t, Role(""), DefaultSDConfig.Role)
+ require.Equal(t, model.Duration(60*time.Second), DefaultSDConfig.RefreshInterval)
+}
+
+func TestSDConfigUnmarshalYAML(t *testing.T) {
+ tests := []struct {
+ name string
+ yaml string
+ validateFunc func(t *testing.T, cfg *SDConfig)
+ }{
+ {
+ name: "EC2WithFlatFields",
+ yaml: `role: ec2
+region: us-west-2
+port: 9100
+filters:
+ - name: instance-state-name
+ values: [running]`,
+ validateFunc: func(t *testing.T, cfg *SDConfig) {
+ require.Equal(t, RoleEC2, cfg.Role)
+ require.NotNil(t, cfg.EC2SDConfig)
+ require.Equal(t, "us-west-2", cfg.EC2SDConfig.Region)
+ require.Equal(t, 9100, cfg.EC2SDConfig.Port)
+ require.Len(t, cfg.EC2SDConfig.Filters, 1)
+ require.Equal(t, "instance-state-name", cfg.EC2SDConfig.Filters[0].Name)
+ require.Equal(t, []string{"running"}, cfg.EC2SDConfig.Filters[0].Values)
+ },
+ },
+ {
+ name: "ECSWithFlatFields",
+ yaml: `role: ecs
+region: us-east-1
+port: 9200
+clusters: ["some-cluster"]`,
+ validateFunc: func(t *testing.T, cfg *SDConfig) {
+ require.Equal(t, RoleECS, cfg.Role)
+ require.NotNil(t, cfg.ECSSDConfig)
+ require.Equal(t, "us-east-1", cfg.ECSSDConfig.Region)
+ require.Equal(t, 9200, cfg.ECSSDConfig.Port)
+ require.Equal(t, []string{"some-cluster"}, cfg.ECSSDConfig.Clusters)
+ },
+ },
+ {
+ name: "LightsailWithFlatFields",
+ yaml: `role: lightsail
+region: eu-central-1
+port: 9300`,
+ validateFunc: func(t *testing.T, cfg *SDConfig) {
+ require.Equal(t, RoleLightsail, cfg.Role)
+ require.NotNil(t, cfg.LightsailSDConfig)
+ require.Equal(t, "eu-central-1", cfg.LightsailSDConfig.Region)
+ require.Equal(t, 9300, cfg.LightsailSDConfig.Port)
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ var cfg SDConfig
+ require.NoError(t, yaml.Unmarshal([]byte(tt.yaml), &cfg))
+ tt.validateFunc(t, &cfg)
+ })
+ }
+}
+
+// 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")
+ },
+ },
+ {
+ name: "MSKMultipleJobsDifferentPorts",
+ yaml: `
+- role: msk
+ region: ap-south-1
+ port: 6060
+ clusters: ["cluster-1"]
+- role: msk
+ region: ap-south-1
+ port: 6061
+ clusters: ["cluster-2"]`,
+ validateFunc: func(t *testing.T, cfg1, cfg2 *SDConfig) {
+ require.Equal(t, RoleMSK, cfg1.Role)
+ require.Equal(t, RoleMSK, cfg2.Role)
+ require.NotNil(t, cfg1.MSKSDConfig)
+ require.NotNil(t, cfg2.MSKSDConfig)
+
+ require.Equal(t, 6060, cfg1.MSKSDConfig.Port)
+ require.Equal(t, []string{"cluster-1"}, cfg1.MSKSDConfig.Clusters)
+ require.Equal(t, 6061, cfg2.MSKSDConfig.Port)
+ require.Equal(t, []string{"cluster-2"}, cfg2.MSKSDConfig.Clusters)
+
+ require.NotSame(t, cfg1.MSKSDConfig, cfg2.MSKSDConfig,
+ "MSKSDConfig 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])
+ })
+ }
+}
+
+// getRandomRegion is a helper to return a pseudo-random AWS region for testing.
+func getRandomRegion() string {
+ regions := []string{
+ "us-east-1",
+ "us-east-2",
+ "us-west-1",
+ "us-west-2",
+ "eu-west-1",
+ "eu-west-2",
+ "ap-southeast-1",
+ "ap-southeast-2",
+ "ap-northeast-1",
+ "ap-northeast-2",
+ }
+
+ return regions[rand.IntN(len(regions))]
+}
+
+func TestLoadRegion(t *testing.T) {
+ t.Run("with_env_region", func(t *testing.T) {
+ randomRegion := getRandomRegion()
+ t.Setenv("AWS_REGION", randomRegion)
+ t.Setenv("AWS_ACCESS_KEY_ID", "dummy")
+ t.Setenv("AWS_SECRET_ACCESS_KEY", "dummy")
+ t.Setenv("AWS_CONFIG_FILE", "") // Ensure no config file is used
+ t.Setenv("AWS_PROFILE", "") // Ensure no profile file is used
+
+ region, err := loadRegion(context.Background(), "")
+ require.NoError(t, err)
+ require.Equal(t, randomRegion, region)
+ })
+
+ t.Run("with_config_file_default_profile", func(t *testing.T) {
+ randomRegion := getRandomRegion()
+
+ // Create a temporary AWS config file
+ tmpDir := t.TempDir()
+ configFile := filepath.Join(tmpDir, "config")
+
+ configContent := `[default]
+region = ` + randomRegion + `
+`
+
+ err := os.WriteFile(configFile, []byte(configContent), 0o644)
+ require.NoError(t, err)
+ defer os.Remove(configFile)
+
+ // Set up environment to use the config file
+ t.Setenv("AWS_CONFIG_FILE", configFile)
+ t.Setenv("AWS_ACCESS_KEY_ID", "dummy")
+ t.Setenv("AWS_SECRET_ACCESS_KEY", "dummy")
+ // Clear any region environment variables to force config file usage
+ t.Setenv("AWS_REGION", "")
+ t.Setenv("AWS_PROFILE", "") // Ensure no profile file is used
+ t.Setenv("AWS_DEFAULT_REGION", "")
+
+ region, err := loadRegion(context.Background(), "")
+ require.NoError(t, err)
+ require.Equal(t, randomRegion, region)
+ })
+
+ t.Run("with_config_file_named_profile", func(t *testing.T) {
+ randomRegion := getRandomRegion()
+
+ // Create a temporary AWS config file
+ tmpDir := t.TempDir()
+ configFile := filepath.Join(tmpDir, "config")
+
+ configContent := `[default]
+region = ` + getRandomRegion() + `
+
+[profile ` + randomRegion + `-profile]
+region = ` + randomRegion + `
+`
+
+ err := os.WriteFile(configFile, []byte(configContent), 0o644)
+ require.NoError(t, err)
+ defer os.Remove(configFile)
+
+ // Set up environment to use the config file
+ t.Setenv("AWS_CONFIG_FILE", configFile)
+ t.Setenv("AWS_PROFILE", randomRegion+"-profile")
+ t.Setenv("AWS_ACCESS_KEY_ID", "dummy")
+ t.Setenv("AWS_SECRET_ACCESS_KEY", "dummy")
+ // Clear any region environment variables to force config file usage
+ t.Setenv("AWS_REGION", "")
+ t.Setenv("AWS_DEFAULT_REGION", "")
+
+ region, err := loadRegion(context.Background(), "")
+ require.NoError(t, err)
+ require.Equal(t, randomRegion, region)
+ })
+
+ t.Run("with_specified_region", func(t *testing.T) {
+ specifiedRegion := getRandomRegion()
+
+ // Even with environment region set differently, specified region should take precedence
+ t.Setenv("AWS_REGION", getRandomRegion())
+ t.Setenv("AWS_ACCESS_KEY_ID", "dummy")
+ t.Setenv("AWS_SECRET_ACCESS_KEY", "dummy")
+
+ region, err := loadRegion(context.Background(), specifiedRegion)
+ require.NoError(t, err)
+ require.Equal(t, specifiedRegion, region)
+ })
+
+ t.Run("imds_fallback", func(t *testing.T) {
+ randomRegion := getRandomRegion()
+
+ // Mock IMDS server that returns a region
+ mockIMDS := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ // Handle instance identity document (contains region info)
+ if r.URL.Path == "/latest/dynamic/instance-identity/document" {
+ imdsPayload := `{"region": "` + randomRegion + `"}`
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte(imdsPayload))
+ return
+ }
+ w.WriteHeader(http.StatusNotFound)
+ }))
+ defer mockIMDS.Close()
+
+ // Set up environment with no region but valid credentials
+ // This will force fallback to IMDS
+ t.Setenv("AWS_ACCESS_KEY_ID", "dummy")
+ t.Setenv("AWS_SECRET_ACCESS_KEY", "dummy")
+ // Unset any existing region
+ t.Setenv("AWS_REGION", "")
+ t.Setenv("AWS_DEFAULT_REGION", "")
+ t.Setenv("AWS_CONFIG_FILE", "") // Ensure no config file is used
+ t.Setenv("AWS_PROFILE", "") // Ensure no profile file is used
+ // Point IMDS to our mock server
+ t.Setenv("AWS_EC2_METADATA_SERVICE_ENDPOINT", mockIMDS.URL)
+
+ region, err := loadRegion(context.Background(), "")
+ require.NoError(t, err)
+ require.Equal(t, randomRegion, region)
+ })
+
+ t.Run("imds_empty_region", func(t *testing.T) {
+ // Mock IMDS server that returns empty region
+ mockIMDS := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ // Handle instance identity document with empty region
+ if r.URL.Path == "/latest/dynamic/instance-identity/document" {
+ imdsPayload := `{"region": ""}`
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte(imdsPayload))
+ return
+ }
+ w.WriteHeader(http.StatusNotFound)
+ }))
+ defer mockIMDS.Close()
+
+ // Set up environment with no region but valid credentials
+ t.Setenv("AWS_ACCESS_KEY_ID", "dummy")
+ t.Setenv("AWS_SECRET_ACCESS_KEY", "dummy")
+ // Unset any existing region
+ t.Setenv("AWS_REGION", "")
+ t.Setenv("AWS_DEFAULT_REGION", "")
+ t.Setenv("AWS_CONFIG_FILE", "") // Ensure no config file is used
+ t.Setenv("AWS_PROFILE", "") // Ensure no profile file is used
+ // Point IMDS to our mock server
+ t.Setenv("AWS_EC2_METADATA_SERVICE_ENDPOINT", mockIMDS.URL)
+
+ _, err := loadRegion(context.Background(), "")
+ require.Error(t, err)
+ require.Contains(t, err.Error(), "failed to get region from IMDS")
+ })
+}
diff --git a/discovery/aws/ec2.go b/discovery/aws/ec2.go
index 0386d9bb1c..48ab411d72 100644
--- a/discovery/aws/ec2.go
+++ b/discovery/aws/ec2.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
@@ -27,7 +27,6 @@ import (
awsConfig "github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/credentials/stscreds"
- "github.com/aws/aws-sdk-go-v2/feature/ec2/imds"
"github.com/aws/aws-sdk-go-v2/service/ec2"
ec2Types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
"github.com/aws/aws-sdk-go-v2/service/sts"
@@ -113,7 +112,7 @@ func (*EC2SDConfig) Name() string { return "ec2" }
// NewDiscoverer returns a Discoverer for the EC2 Config.
func (c *EC2SDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) {
- return NewEC2Discovery(c, opts.Logger, opts.Metrics)
+ return NewEC2Discovery(c, opts)
}
// UnmarshalYAML implements the yaml.Unmarshaler interface for the EC2 Config.
@@ -125,31 +124,10 @@ func (c *EC2SDConfig) UnmarshalYAML(unmarshal func(any) error) error {
return err
}
- if c.Region == "" {
- cfg, err := awsConfig.LoadDefaultConfig(context.Background())
- if err != nil {
- return err
- }
-
- if cfg.Region != "" {
- // If the region is already set in the config, use it.
- // This can happen if the user has set the region in the AWS config file or environment variables.
- c.Region = cfg.Region
- }
-
- if c.Region == "" {
- // Try to get the region from the instance metadata service (IMDS).
- imdsClient := imds.NewFromConfig(cfg)
- region, err := imdsClient.GetRegion(context.Background(), &imds.GetRegionInput{})
- if err != nil {
- return err
- }
- c.Region = region.Region
- }
- }
-
- if c.Region == "" {
- return errors.New("EC2 SD configuration requires a region")
+ // Check if the region is set, if not attempt to load it from the AWS SDK.
+ c.Region, err = loadRegion(context.Background(), c.Region)
+ if err != nil {
+ return fmt.Errorf("could not determine AWS region: %w", err)
}
for _, f := range c.Filters {
@@ -180,23 +158,24 @@ type EC2Discovery struct {
}
// NewEC2Discovery returns a new EC2Discovery which periodically refreshes its targets.
-func NewEC2Discovery(conf *EC2SDConfig, logger *slog.Logger, metrics discovery.DiscovererMetrics) (*EC2Discovery, error) {
- m, ok := metrics.(*ec2Metrics)
+func NewEC2Discovery(conf *EC2SDConfig, opts discovery.DiscovererOptions) (*EC2Discovery, error) {
+ m, ok := opts.Metrics.(*ec2Metrics)
if !ok {
return nil, errors.New("invalid discovery metrics type")
}
- if logger == nil {
- logger = promslog.NewNopLogger()
+ if opts.Logger == nil {
+ opts.Logger = promslog.NewNopLogger()
}
d := &EC2Discovery{
- logger: logger,
+ logger: opts.Logger,
cfg: conf,
}
d.Discovery = refresh.NewDiscovery(
refresh.Options{
- Logger: logger,
+ Logger: opts.Logger,
Mech: "ec2",
+ SetName: opts.SetName,
Interval: time.Duration(d.cfg.RefreshInterval),
RefreshF: d.refresh,
MetricsInstantiator: m.refreshMetrics,
@@ -245,7 +224,12 @@ func (d *EC2Discovery) ec2Client(ctx context.Context) (ec2Client, error) {
cfg.Credentials = aws.NewCredentialsCache(assumeProvider)
}
- d.ec2 = ec2.NewFromConfig(cfg)
+ d.ec2 = ec2.NewFromConfig(cfg, func(options *ec2.Options) {
+ if d.cfg.Endpoint != "" {
+ options.BaseEndpoint = &d.cfg.Endpoint
+ }
+ options.HTTPClient = httpClient
+ })
return d.ec2, nil
}
@@ -255,8 +239,15 @@ func (d *EC2Discovery) refreshAZIDs(ctx context.Context) error {
if err != nil {
return err
}
+ if azs.AvailabilityZones == nil {
+ d.azToAZID = make(map[string]string)
+ return nil
+ }
d.azToAZID = make(map[string]string, len(azs.AvailabilityZones))
for _, az := range azs.AvailabilityZones {
+ if az.ZoneName == nil || az.ZoneId == nil {
+ continue
+ }
d.azToAZID[*az.ZoneName] = *az.ZoneId
}
return nil
diff --git a/discovery/aws/ec2_test.go b/discovery/aws/ec2_test.go
index 46ab8e771d..bd1047ffc0 100644
--- a/discovery/aws/ec2_test.go
+++ b/discovery/aws/ec2_test.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Prometheus Authors
+// 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
diff --git a/discovery/aws/ecs.go b/discovery/aws/ecs.go
new file mode 100644
index 0000000000..18d2746cb6
--- /dev/null
+++ b/discovery/aws/ecs.go
@@ -0,0 +1,995 @@
+// 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 aws
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "log/slog"
+ "net"
+ "slices"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/aws/aws-sdk-go-v2/aws"
+ awsConfig "github.com/aws/aws-sdk-go-v2/config"
+ "github.com/aws/aws-sdk-go-v2/credentials"
+ "github.com/aws/aws-sdk-go-v2/credentials/stscreds"
+ "github.com/aws/aws-sdk-go-v2/service/ec2"
+ "github.com/aws/aws-sdk-go-v2/service/ecs"
+ "github.com/aws/aws-sdk-go-v2/service/ecs/types"
+ "github.com/aws/aws-sdk-go-v2/service/sts"
+ "github.com/prometheus/client_golang/prometheus"
+ "github.com/prometheus/common/config"
+ "github.com/prometheus/common/model"
+ "github.com/prometheus/common/promslog"
+ "golang.org/x/sync/errgroup"
+
+ "github.com/prometheus/prometheus/discovery"
+ "github.com/prometheus/prometheus/discovery/refresh"
+ "github.com/prometheus/prometheus/discovery/targetgroup"
+ "github.com/prometheus/prometheus/util/strutil"
+)
+
+const (
+ ecsLabel = model.MetaLabelPrefix + "ecs_"
+ ecsLabelCluster = ecsLabel + "cluster"
+ ecsLabelClusterARN = ecsLabel + "cluster_arn"
+ ecsLabelService = ecsLabel + "service"
+ ecsLabelServiceARN = ecsLabel + "service_arn"
+ ecsLabelServiceStatus = ecsLabel + "service_status"
+ ecsLabelTaskGroup = ecsLabel + "task_group"
+ ecsLabelTaskARN = ecsLabel + "task_arn"
+ ecsLabelTaskDefinition = ecsLabel + "task_definition"
+ ecsLabelRegion = ecsLabel + "region"
+ ecsLabelAvailabilityZone = ecsLabel + "availability_zone"
+ ecsLabelSubnetID = ecsLabel + "subnet_id"
+ ecsLabelIPAddress = ecsLabel + "ip_address"
+ ecsLabelLaunchType = ecsLabel + "launch_type"
+ ecsLabelDesiredStatus = ecsLabel + "desired_status"
+ ecsLabelLastStatus = ecsLabel + "last_status"
+ ecsLabelHealthStatus = ecsLabel + "health_status"
+ ecsLabelPlatformFamily = ecsLabel + "platform_family"
+ ecsLabelPlatformVersion = ecsLabel + "platform_version"
+ ecsLabelTag = ecsLabel + "tag_"
+ ecsLabelTagCluster = ecsLabelTag + "cluster_"
+ ecsLabelTagService = ecsLabelTag + "service_"
+ ecsLabelTagTask = ecsLabelTag + "task_"
+ ecsLabelTagEC2 = ecsLabelTag + "ec2_"
+ ecsLabelNetworkMode = ecsLabel + "network_mode"
+ ecsLabelContainerInstanceARN = ecsLabel + "container_instance_arn"
+ ecsLabelEC2InstanceID = ecsLabel + "ec2_instance_id"
+ ecsLabelEC2InstanceType = ecsLabel + "ec2_instance_type"
+ ecsLabelEC2InstancePrivateIP = ecsLabel + "ec2_instance_private_ip"
+ ecsLabelEC2InstancePublicIP = ecsLabel + "ec2_instance_public_ip"
+ ecsLabelPublicIP = ecsLabel + "public_ip"
+)
+
+// DefaultECSSDConfig is the default ECS SD configuration.
+var DefaultECSSDConfig = ECSSDConfig{
+ Port: 80,
+ RefreshInterval: model.Duration(60 * time.Second),
+ RequestConcurrency: 20, // Aligned with AWS ECS API sustained rate limits (20 req/sec)
+ HTTPClientConfig: config.DefaultHTTPClientConfig,
+}
+
+func init() {
+ discovery.RegisterConfig(&ECSSDConfig{})
+}
+
+// ECSSDConfig is the configuration for ECS based service discovery.
+type ECSSDConfig struct {
+ Region string `yaml:"region"`
+ Endpoint string `yaml:"endpoint"`
+ AccessKey string `yaml:"access_key,omitempty"`
+ SecretKey config.Secret `yaml:"secret_key,omitempty"`
+ Profile string `yaml:"profile,omitempty"`
+ RoleARN string `yaml:"role_arn,omitempty"`
+ Clusters []string `yaml:"clusters,omitempty"`
+ Port int `yaml:"port"`
+ RefreshInterval model.Duration `yaml:"refresh_interval,omitempty"`
+
+ // RequestConcurrency controls the maximum number of concurrent ECS API requests.
+ // Default is 20, which aligns with AWS ECS sustained rate limits:
+ // - Cluster read actions (DescribeClusters, ListClusters): 20 req/sec sustained
+ // - Service read actions (DescribeServices, ListServices): 20 req/sec sustained
+ // - Cluster resource read actions (DescribeTasks, ListTasks): 20 req/sec sustained
+ // See: https://docs.aws.amazon.com/AmazonECS/latest/APIReference/request-throttling.html
+ RequestConcurrency int `yaml:"request_concurrency,omitempty"`
+
+ HTTPClientConfig config.HTTPClientConfig `yaml:",inline"`
+}
+
+// NewDiscovererMetrics implements discovery.Config.
+func (*ECSSDConfig) NewDiscovererMetrics(_ prometheus.Registerer, rmi discovery.RefreshMetricsInstantiator) discovery.DiscovererMetrics {
+ return &ecsMetrics{
+ refreshMetrics: rmi,
+ }
+}
+
+// Name returns the name of the ECS Config.
+func (*ECSSDConfig) Name() string { return "ecs" }
+
+// NewDiscoverer returns a Discoverer for the EC2 Config.
+func (c *ECSSDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) {
+ return NewECSDiscovery(c, opts)
+}
+
+// UnmarshalYAML implements the yaml.Unmarshaler interface for the ECS Config.
+func (c *ECSSDConfig) UnmarshalYAML(unmarshal func(any) error) error {
+ *c = DefaultECSSDConfig
+ type plain ECSSDConfig
+ err := unmarshal((*plain)(c))
+ if err != nil {
+ return err
+ }
+
+ c.Region, err = loadRegion(context.Background(), c.Region)
+ if err != nil {
+ return fmt.Errorf("could not determine AWS region: %w", err)
+ }
+
+ return c.HTTPClientConfig.Validate()
+}
+
+type ecsClient interface {
+ ListClusters(context.Context, *ecs.ListClustersInput, ...func(*ecs.Options)) (*ecs.ListClustersOutput, error)
+ DescribeClusters(context.Context, *ecs.DescribeClustersInput, ...func(*ecs.Options)) (*ecs.DescribeClustersOutput, error)
+ ListServices(context.Context, *ecs.ListServicesInput, ...func(*ecs.Options)) (*ecs.ListServicesOutput, error)
+ DescribeServices(context.Context, *ecs.DescribeServicesInput, ...func(*ecs.Options)) (*ecs.DescribeServicesOutput, error)
+ ListTasks(context.Context, *ecs.ListTasksInput, ...func(*ecs.Options)) (*ecs.ListTasksOutput, error)
+ DescribeTasks(context.Context, *ecs.DescribeTasksInput, ...func(*ecs.Options)) (*ecs.DescribeTasksOutput, error)
+ DescribeContainerInstances(context.Context, *ecs.DescribeContainerInstancesInput, ...func(*ecs.Options)) (*ecs.DescribeContainerInstancesOutput, error)
+}
+
+type ecsEC2Client interface {
+ DescribeInstances(context.Context, *ec2.DescribeInstancesInput, ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error)
+ DescribeNetworkInterfaces(context.Context, *ec2.DescribeNetworkInterfacesInput, ...func(*ec2.Options)) (*ec2.DescribeNetworkInterfacesOutput, error)
+}
+
+// ECSDiscovery periodically performs ECS-SD requests. It implements
+// the Discoverer interface.
+type ECSDiscovery struct {
+ *refresh.Discovery
+ logger *slog.Logger
+ cfg *ECSSDConfig
+ ecs ecsClient
+ ec2 ecsEC2Client
+}
+
+// NewECSDiscovery returns a new ECSDiscovery which periodically refreshes its targets.
+func NewECSDiscovery(conf *ECSSDConfig, opts discovery.DiscovererOptions) (*ECSDiscovery, error) {
+ m, ok := opts.Metrics.(*ecsMetrics)
+ if !ok {
+ return nil, errors.New("invalid discovery metrics type")
+ }
+
+ if opts.Logger == nil {
+ opts.Logger = promslog.NewNopLogger()
+ }
+ d := &ECSDiscovery{
+ logger: opts.Logger,
+ cfg: conf,
+ }
+ d.Discovery = refresh.NewDiscovery(
+ refresh.Options{
+ Logger: opts.Logger,
+ Mech: "ecs",
+ Interval: time.Duration(d.cfg.RefreshInterval),
+ RefreshF: d.refresh,
+ MetricsInstantiator: m.refreshMetrics,
+ },
+ )
+ return d, nil
+}
+
+func (d *ECSDiscovery) initEcsClient(ctx context.Context) error {
+ if d.ecs != nil && d.ec2 != nil {
+ return nil
+ }
+
+ if d.cfg.Region == "" {
+ return errors.New("region must be set for ECS service discovery")
+ }
+
+ // Build the HTTP client from the provided HTTPClientConfig.
+ client, err := config.NewClientFromConfig(d.cfg.HTTPClientConfig, "ecs_sd")
+ if err != nil {
+ return err
+ }
+
+ // Build the AWS config with the provided region.
+ var configOptions []func(*awsConfig.LoadOptions) error
+ configOptions = append(configOptions, awsConfig.WithRegion(d.cfg.Region))
+ configOptions = append(configOptions, awsConfig.WithHTTPClient(client))
+
+ // Only set static credentials if both access key and secret key are provided
+ // Otherwise, let AWS SDK use its default credential chain
+ if d.cfg.AccessKey != "" && d.cfg.SecretKey != "" {
+ credProvider := credentials.NewStaticCredentialsProvider(d.cfg.AccessKey, string(d.cfg.SecretKey), "")
+ configOptions = append(configOptions, awsConfig.WithCredentialsProvider(credProvider))
+ }
+
+ if d.cfg.Profile != "" {
+ configOptions = append(configOptions, awsConfig.WithSharedConfigProfile(d.cfg.Profile))
+ }
+
+ cfg, err := awsConfig.LoadDefaultConfig(ctx, configOptions...)
+ if err != nil {
+ d.logger.Error("Failed to create AWS config", "error", err)
+ return fmt.Errorf("could not create aws config: %w", err)
+ }
+
+ // If the role ARN is set, assume the role to get credentials and set the credentials provider in the config.
+ if d.cfg.RoleARN != "" {
+ assumeProvider := stscreds.NewAssumeRoleProvider(sts.NewFromConfig(cfg), d.cfg.RoleARN)
+ cfg.Credentials = aws.NewCredentialsCache(assumeProvider)
+ }
+
+ d.ecs = ecs.NewFromConfig(cfg, func(options *ecs.Options) {
+ if d.cfg.Endpoint != "" {
+ options.BaseEndpoint = &d.cfg.Endpoint
+ }
+ options.HTTPClient = client
+ })
+
+ d.ec2 = ec2.NewFromConfig(cfg, func(options *ec2.Options) {
+ options.HTTPClient = client
+ })
+
+ // Test credentials by making a simple API call
+ testCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
+ defer cancel()
+
+ _, err = d.ecs.DescribeClusters(testCtx, &ecs.DescribeClustersInput{})
+ if err != nil {
+ d.logger.Error("Failed to test ECS credentials", "error", err)
+ return fmt.Errorf("ECS credential test failed: %w", err)
+ }
+
+ return nil
+}
+
+// listClusterARNs returns a slice of cluster arns.
+// This method does not use concurrency as it's a simple paginated call.
+func (d *ECSDiscovery) listClusterARNs(ctx context.Context) ([]string, error) {
+ var (
+ clusterARNs []string
+ nextToken *string
+ )
+ for {
+ resp, err := d.ecs.ListClusters(ctx, &ecs.ListClustersInput{
+ NextToken: nextToken,
+ MaxResults: aws.Int32(100),
+ })
+ if err != nil {
+ return nil, fmt.Errorf("could not list clusters: %w", err)
+ }
+
+ clusterARNs = append(clusterARNs, resp.ClusterArns...)
+
+ if resp.NextToken == nil {
+ break
+ }
+ nextToken = resp.NextToken
+ }
+
+ return clusterARNs, nil
+}
+
+// describeClusters returns a map of cluster ARN to a slice of clusters.
+// Uses concurrent requests limited by RequestConcurrency to respect AWS API throttling.
+// Clusters are described in batches of 100 to respect AWS API limits (DescribeClusters allows up to 100 clusters per call).
+func (d *ECSDiscovery) describeClusters(ctx context.Context, clusters []string) (map[string]types.Cluster, error) {
+ mu := sync.Mutex{}
+ clusterMap := make(map[string]types.Cluster)
+ errg, ectx := errgroup.WithContext(ctx)
+ errg.SetLimit(d.cfg.RequestConcurrency)
+ for batch := range slices.Chunk(clusters, 100) {
+ errg.Go(func() error {
+ resp, err := d.ecs.DescribeClusters(ectx, &ecs.DescribeClustersInput{
+ Clusters: batch,
+ Include: []types.ClusterField{"TAGS"},
+ })
+ if err != nil {
+ d.logger.Error("Failed to describe clusters", "clusters", batch, "error", err)
+ return fmt.Errorf("could not describe clusters %v: %w", batch, err)
+ }
+
+ for _, cluster := range resp.Clusters {
+ if cluster.ClusterArn != nil {
+ mu.Lock()
+ clusterMap[*cluster.ClusterArn] = cluster
+ mu.Unlock()
+ }
+ }
+ return nil
+ })
+ }
+
+ return clusterMap, errg.Wait()
+}
+
+// listServiceARNs returns a map of cluster ARN to a slice of service ARNs.
+// Uses concurrent requests limited by RequestConcurrency to respect AWS API throttling.
+// Services are listed in batches of 100 to respect AWS API limits (ListServices allows up to 100 services per call).
+func (d *ECSDiscovery) listServiceARNs(ctx context.Context, clusters []string) (map[string][]string, error) {
+ mu := sync.Mutex{}
+ services := make(map[string][]string)
+ errg, ectx := errgroup.WithContext(ctx)
+ errg.SetLimit(d.cfg.RequestConcurrency)
+ for _, clusterARN := range clusters {
+ errg.Go(func() error {
+ var nextToken *string
+ var serviceARNs []string
+ for {
+ resp, err := d.ecs.ListServices(ectx, &ecs.ListServicesInput{
+ Cluster: aws.String(clusterARN),
+ NextToken: nextToken,
+ MaxResults: aws.Int32(100),
+ })
+ if err != nil {
+ return fmt.Errorf("could not list services for cluster %q: %w", clusterARN, err)
+ }
+
+ serviceARNs = append(serviceARNs, resp.ServiceArns...)
+
+ if resp.NextToken == nil {
+ break
+ }
+ nextToken = resp.NextToken
+ }
+
+ mu.Lock()
+ services[clusterARN] = serviceARNs
+ mu.Unlock()
+ return nil
+ })
+ }
+
+ return services, errg.Wait()
+}
+
+// describeServices returns a map of service name to service.
+// Uses concurrent requests limited by RequestConcurrency to respect AWS API throttling.
+// Services are described in batches of 10 to respect AWS API limits (DescribeServices allows up to 10 services per call).
+func (d *ECSDiscovery) describeServices(ctx context.Context, clusterARN string, serviceARNS []string) (map[string]types.Service, error) {
+ mu := sync.Mutex{}
+ services := make(map[string]types.Service)
+ errg, ectx := errgroup.WithContext(ctx)
+ errg.SetLimit(d.cfg.RequestConcurrency)
+ for batch := range slices.Chunk(serviceARNS, 10) {
+ errg.Go(func() error {
+ resp, err := d.ecs.DescribeServices(ectx, &ecs.DescribeServicesInput{
+ Cluster: aws.String(clusterARN),
+ Services: batch,
+ Include: []types.ServiceField{"TAGS"},
+ })
+ if err != nil {
+ d.logger.Error("Failed to describe services", "cluster", clusterARN, "batch", batch, "error", err)
+ return fmt.Errorf("could not describe services for cluster %q: batch %v: %w", clusterARN, batch, err)
+ }
+
+ for _, service := range resp.Services {
+ if service.ServiceArn != nil {
+ mu.Lock()
+ services[*service.ServiceName] = service
+ mu.Unlock()
+ }
+ }
+ return nil
+ })
+ }
+
+ return services, errg.Wait()
+}
+
+// listTaskARNs returns a map of clustersARN to a slice of task ARNs.
+// Uses concurrent requests limited by RequestConcurrency to respect AWS API throttling.
+// Tasks are listed in batches of 100 to respect AWS API limits (ListTasks allows up to 100 tasks per call).
+// This method also uses pagination to handle cases where there are more than 100 tasks in a cluster.
+func (d *ECSDiscovery) listTaskARNs(ctx context.Context, clusterARNs []string) (map[string][]string, error) {
+ mu := sync.Mutex{}
+ tasks := make(map[string][]string)
+ errg, ectx := errgroup.WithContext(ctx)
+ errg.SetLimit(d.cfg.RequestConcurrency)
+ for _, clusterARN := range clusterARNs {
+ errg.Go(func() error {
+ var (
+ nextToken *string
+ taskARNs []string
+ )
+ for {
+ resp, err := d.ecs.ListTasks(ectx, &ecs.ListTasksInput{
+ Cluster: aws.String(clusterARN),
+ NextToken: nextToken,
+ MaxResults: aws.Int32(100),
+ })
+ if err != nil {
+ return fmt.Errorf("could not list tasks for cluster %q: %w", clusterARN, err)
+ }
+
+ taskARNs = append(taskARNs, resp.TaskArns...)
+
+ if resp.NextToken == nil {
+ break
+ }
+ nextToken = resp.NextToken
+ }
+
+ mu.Lock()
+ tasks[clusterARN] = taskARNs
+ mu.Unlock()
+ return nil
+ })
+ }
+
+ return tasks, errg.Wait()
+}
+
+// describeTasks returns a slice of tasks.
+// Uses concurrent requests limited by RequestConcurrency to respect AWS API throttling.
+// Tasks are described in batches of 100 to respect AWS API limits (DescribeTasks allows up to 100 tasks per call).
+func (d *ECSDiscovery) describeTasks(ctx context.Context, clusterARN string, taskARNs []string) ([]types.Task, error) {
+ mu := sync.Mutex{}
+ var tasks []types.Task
+ errg, ectx := errgroup.WithContext(ctx)
+ errg.SetLimit(d.cfg.RequestConcurrency)
+ for batch := range slices.Chunk(taskARNs, 100) {
+ errg.Go(func() error {
+ resp, err := d.ecs.DescribeTasks(ectx, &ecs.DescribeTasksInput{
+ Cluster: aws.String(clusterARN),
+ Tasks: batch,
+ Include: []types.TaskField{"TAGS"},
+ })
+ if err != nil {
+ d.logger.Error("Failed to describe tasks", "cluster", clusterARN, "batch", batch, "error", err)
+ return fmt.Errorf("could not describe tasks in cluster %q: batch %v: %w", clusterARN, batch, err)
+ }
+
+ mu.Lock()
+ tasks = append(tasks, resp.Tasks...)
+ mu.Unlock()
+ return nil
+ })
+ }
+
+ return tasks, errg.Wait()
+}
+
+// describeContainerInstances returns a map of container instance ARN to EC2 instance ID
+// Uses concurrent requests limited by RequestConcurrency to respect AWS API throttling.
+// Container instances are described in batches of 100 to respect AWS API limits (DescribeContainerInstances allows up to 100 container instances per call).
+func (d *ECSDiscovery) describeContainerInstances(ctx context.Context, clusterARN string, tasks []types.Task) (map[string]string, error) {
+ containerInstanceARNs := make([]string, 0, len(tasks))
+ for _, task := range tasks {
+ if task.ContainerInstanceArn != nil {
+ containerInstanceARNs = append(containerInstanceARNs, *task.ContainerInstanceArn)
+ }
+ }
+
+ if len(containerInstanceARNs) == 0 {
+ return make(map[string]string), nil
+ }
+
+ mu := sync.Mutex{}
+ containerInstToEC2 := make(map[string]string)
+ errg, ectx := errgroup.WithContext(ctx)
+ errg.SetLimit(d.cfg.RequestConcurrency)
+ for batch := range slices.Chunk(containerInstanceARNs, 100) {
+ errg.Go(func() error {
+ resp, err := d.ecs.DescribeContainerInstances(ectx, &ecs.DescribeContainerInstancesInput{
+ Cluster: aws.String(clusterARN),
+ ContainerInstances: batch,
+ })
+ if err != nil {
+ return fmt.Errorf("could not describe container instances: %w", err)
+ }
+
+ for _, ci := range resp.ContainerInstances {
+ if ci.ContainerInstanceArn != nil && ci.Ec2InstanceId != nil {
+ mu.Lock()
+ containerInstToEC2[*ci.ContainerInstanceArn] = *ci.Ec2InstanceId
+ mu.Unlock()
+ }
+ }
+ return nil
+ })
+ }
+
+ return containerInstToEC2, errg.Wait()
+}
+
+// ec2InstanceInfo holds information retrieved from EC2 DescribeInstances.
+type ec2InstanceInfo struct {
+ privateIP string
+ publicIP string
+ subnetID string
+ instanceType string
+ tags map[string]string
+}
+
+// describeEC2Instances returns a map of EC2 instance ID to instance information.
+// Uses concurrent requests limited by RequestConcurrency to respect AWS API throttling.
+// This method does not use concurrency as it's a simple paginated call.
+func (d *ECSDiscovery) describeEC2Instances(ctx context.Context, instanceIDs []string) (map[string]ec2InstanceInfo, error) {
+ if len(instanceIDs) == 0 {
+ return make(map[string]ec2InstanceInfo), nil
+ }
+
+ instanceInfo := make(map[string]ec2InstanceInfo)
+ var nextToken *string
+
+ for {
+ resp, err := d.ec2.DescribeInstances(ctx, &ec2.DescribeInstancesInput{
+ InstanceIds: instanceIDs,
+ NextToken: nextToken,
+ })
+ if err != nil {
+ return nil, fmt.Errorf("could not describe EC2 instances: %w", err)
+ }
+
+ for _, reservation := range resp.Reservations {
+ for _, instance := range reservation.Instances {
+ if instance.InstanceId != nil && instance.PrivateIpAddress != nil {
+ info := ec2InstanceInfo{
+ privateIP: *instance.PrivateIpAddress,
+ tags: make(map[string]string),
+ }
+ if instance.PublicIpAddress != nil {
+ info.publicIP = *instance.PublicIpAddress
+ }
+ if instance.SubnetId != nil {
+ info.subnetID = *instance.SubnetId
+ }
+ if instance.InstanceType != "" {
+ info.instanceType = string(instance.InstanceType)
+ }
+ // Collect EC2 instance tags
+ for _, tag := range instance.Tags {
+ if tag.Key != nil && tag.Value != nil {
+ info.tags[*tag.Key] = *tag.Value
+ }
+ }
+ instanceInfo[*instance.InstanceId] = info
+ }
+ }
+ }
+
+ if resp.NextToken == nil {
+ break
+ }
+ nextToken = resp.NextToken
+ }
+
+ return instanceInfo, nil
+}
+
+// describeNetworkInterfaces returns a map of ENI ID to public IP address.
+// This is needed to get the public IP for tasks using awsvpc network mode, as the ENI is what gets the public IP, not the EC2 instance.
+// This method does not use concurrency as it's a simple paginated call.
+func (d *ECSDiscovery) describeNetworkInterfaces(ctx context.Context, tasks []types.Task) (map[string]string, error) {
+ eniIDs := make([]string, 0, len(tasks))
+
+ for _, task := range tasks {
+ for _, attachment := range task.Attachments {
+ if attachment.Type != nil && *attachment.Type == "ElasticNetworkInterface" {
+ for _, detail := range attachment.Details {
+ if detail.Name != nil && *detail.Name == "networkInterfaceId" && detail.Value != nil {
+ eniIDs = append(eniIDs, *detail.Value)
+ break
+ }
+ }
+ break
+ }
+ }
+ }
+
+ if len(eniIDs) == 0 {
+ return make(map[string]string), nil
+ }
+
+ eniToPublicIP := make(map[string]string)
+ var nextToken *string
+
+ for {
+ resp, err := d.ec2.DescribeNetworkInterfaces(ctx, &ec2.DescribeNetworkInterfacesInput{
+ NetworkInterfaceIds: eniIDs,
+ NextToken: nextToken,
+ })
+ if err != nil {
+ return nil, fmt.Errorf("could not describe network interfaces: %w", err)
+ }
+
+ for _, eni := range resp.NetworkInterfaces {
+ if eni.NetworkInterfaceId != nil && eni.Association != nil && eni.Association.PublicIp != nil {
+ eniToPublicIP[*eni.NetworkInterfaceId] = *eni.Association.PublicIp
+ }
+ }
+
+ if resp.NextToken == nil {
+ break
+ }
+ nextToken = resp.NextToken
+ }
+
+ return eniToPublicIP, nil
+}
+
+func (d *ECSDiscovery) refresh(ctx context.Context) ([]*targetgroup.Group, error) {
+ err := d.initEcsClient(ctx)
+ if err != nil {
+ return nil, err
+ }
+
+ var clusters []string
+ if len(d.cfg.Clusters) == 0 {
+ clusters, err = d.listClusterARNs(ctx)
+ if err != nil {
+ return nil, err
+ }
+ } else {
+ clusters = d.cfg.Clusters
+ }
+
+ if len(clusters) == 0 {
+ return []*targetgroup.Group{
+ {
+ Source: d.cfg.Region,
+ },
+ }, nil
+ }
+
+ tg := &targetgroup.Group{
+ Source: d.cfg.Region,
+ }
+
+ // Fetch cluster details, service ARNs, and task ARNs in parallel
+ var (
+ clusterMap map[string]types.Cluster
+ serviceMap map[string][]string
+ taskMap map[string][]string
+ )
+
+ clusterErrg, clusterCtx := errgroup.WithContext(ctx)
+ clusterErrg.Go(func() error {
+ var err error
+ clusterMap, err = d.describeClusters(clusterCtx, clusters)
+ return err
+ })
+ clusterErrg.Go(func() error {
+ var err error
+ serviceMap, err = d.listServiceARNs(clusterCtx, clusters)
+ return err
+ })
+ clusterErrg.Go(func() error {
+ var err error
+ taskMap, err = d.listTaskARNs(clusterCtx, clusters)
+ return err
+ })
+
+ if err := clusterErrg.Wait(); err != nil {
+ return nil, err
+ }
+
+ // Use goroutines to process clusters in parallel
+ var (
+ clusterWg sync.WaitGroup
+ clusterMu sync.Mutex
+ clusterTargets []model.LabelSet
+ )
+
+ for clusterARN, taskARNs := range taskMap {
+ if len(taskARNs) == 0 {
+ continue
+ }
+
+ clusterWg.Add(1)
+
+ go func(cluster types.Cluster, serviceARNs, taskARNs []string) {
+ defer clusterWg.Done()
+
+ // Fetch services and tasks in parallel (they're independent)
+ var (
+ services map[string]types.Service
+ tasks []types.Task
+ )
+
+ resourceErrg, resourceCtx := errgroup.WithContext(ctx)
+ resourceErrg.Go(func() error {
+ var err error
+ services, err = d.describeServices(resourceCtx, *cluster.ClusterArn, serviceARNs)
+ if err != nil {
+ d.logger.Error("Failed to describe services for cluster", "cluster", *cluster.ClusterArn, "error", err)
+ }
+ return err
+ })
+ resourceErrg.Go(func() error {
+ var err error
+ tasks, err = d.describeTasks(resourceCtx, *cluster.ClusterArn, taskARNs)
+ if err != nil {
+ d.logger.Error("Failed to describe tasks for cluster", "cluster", *cluster.ClusterArn, "error", err)
+ }
+ return err
+ })
+
+ if err := resourceErrg.Wait(); err != nil {
+ return
+ }
+
+ // Fetch container instances and network interfaces in parallel (both depend on tasks)
+ var (
+ containerInstances map[string]string
+ eniToPublicIP map[string]string
+ )
+
+ instanceErrg, instanceCtx := errgroup.WithContext(ctx)
+ instanceErrg.Go(func() error {
+ var err error
+ containerInstances, err = d.describeContainerInstances(instanceCtx, *cluster.ClusterArn, tasks)
+ if err != nil {
+ d.logger.Error("Failed to describe container instances for cluster", "cluster", *cluster.ClusterArn, "error", err)
+ }
+ return err
+ })
+ instanceErrg.Go(func() error {
+ var err error
+ eniToPublicIP, err = d.describeNetworkInterfaces(instanceCtx, tasks)
+ if err != nil {
+ d.logger.Error("Failed to describe network interfaces for cluster", "cluster", *cluster.ClusterArn, "error", err)
+ }
+ return err
+ })
+
+ if err := instanceErrg.Wait(); err != nil {
+ return
+ }
+
+ ec2Instances := make(map[string]ec2InstanceInfo)
+ if len(containerInstances) > 0 {
+ // Deduplicate EC2 instance IDs (multiple tasks can share the same instance)
+ ec2InstanceIDSet := make(map[string]struct{})
+ for _, ec2ID := range containerInstances {
+ ec2InstanceIDSet[ec2ID] = struct{}{}
+ }
+ ec2InstanceIDs := make([]string, 0, len(ec2InstanceIDSet))
+ for ec2ID := range ec2InstanceIDSet {
+ ec2InstanceIDs = append(ec2InstanceIDs, ec2ID)
+ }
+ ec2Instances, err = d.describeEC2Instances(ctx, ec2InstanceIDs)
+ if err != nil {
+ d.logger.Error("Failed to describe EC2 instances for cluster", "cluster", *cluster.ClusterArn, "error", err)
+ return
+ }
+ }
+
+ var (
+ taskWg sync.WaitGroup
+ taskMu sync.Mutex
+ taskTargets []model.LabelSet
+ )
+
+ for _, task := range tasks {
+ taskWg.Add(1)
+
+ go func(cluster types.Cluster, services map[string]types.Service, task types.Task, containerInstances map[string]string, ec2Instances map[string]ec2InstanceInfo, eniToPublicIP map[string]string) {
+ defer taskWg.Done()
+
+ var (
+ ipAddress, subnetID, publicIP string
+ networkMode string
+ ec2InstanceID, ec2InstanceType, ec2InstancePrivateIP, ec2InstancePublicIP string
+ )
+
+ // Try to get IP from ENI attachment (awsvpc mode)
+ var eniAttachment *types.Attachment
+ for _, attachment := range task.Attachments {
+ if attachment.Type != nil && *attachment.Type == "ElasticNetworkInterface" {
+ eniAttachment = &attachment
+ break
+ }
+ }
+
+ if eniAttachment != nil {
+ // awsvpc networking mode - get IP from ENI
+ networkMode = "awsvpc"
+ var eniID string
+ for _, detail := range eniAttachment.Details {
+ switch *detail.Name {
+ case "privateIPv4Address":
+ ipAddress = *detail.Value
+ case "subnetId":
+ subnetID = *detail.Value
+ case "networkInterfaceId":
+ eniID = *detail.Value
+ }
+ }
+ // Get public IP from ENI if available
+ if eniID != "" {
+ if pub, ok := eniToPublicIP[eniID]; ok {
+ publicIP = pub
+ }
+ }
+ } else if task.ContainerInstanceArn != nil {
+ // bridge/host networking mode - need to get EC2 instance IP and subnet
+ networkMode = "bridge"
+ var ok bool
+ ec2InstanceID, ok = containerInstances[*task.ContainerInstanceArn]
+ if ok {
+ info, ok := ec2Instances[ec2InstanceID]
+ if ok {
+ ipAddress = info.privateIP
+ publicIP = info.publicIP
+ subnetID = info.subnetID
+ ec2InstanceType = info.instanceType
+ ec2InstancePrivateIP = info.privateIP
+ ec2InstancePublicIP = info.publicIP
+ } else {
+ d.logger.Debug("EC2 instance info not found", "instance", ec2InstanceID, "task", *task.TaskArn)
+ }
+ } else {
+ d.logger.Debug("Container instance not found in map", "arn", *task.ContainerInstanceArn, "task", *task.TaskArn)
+ }
+ }
+
+ // Get EC2 instance metadata for awsvpc tasks running on EC2
+ // We want the instance type and the host IPs for advanced use cases
+ if networkMode == "awsvpc" && task.ContainerInstanceArn != nil {
+ var ok bool
+ ec2InstanceID, ok = containerInstances[*task.ContainerInstanceArn]
+ if ok {
+ info, ok := ec2Instances[ec2InstanceID]
+ if ok {
+ ec2InstanceType = info.instanceType
+ ec2InstancePrivateIP = info.privateIP
+ ec2InstancePublicIP = info.publicIP
+ }
+ }
+ }
+
+ if ipAddress == "" {
+ return
+ }
+
+ labels := model.LabelSet{
+ ecsLabelClusterARN: model.LabelValue(*cluster.ClusterArn),
+ ecsLabelCluster: model.LabelValue(*cluster.ClusterName),
+ ecsLabelTaskGroup: model.LabelValue(*task.Group),
+ ecsLabelTaskARN: model.LabelValue(*task.TaskArn),
+ ecsLabelTaskDefinition: model.LabelValue(*task.TaskDefinitionArn),
+ ecsLabelIPAddress: model.LabelValue(ipAddress),
+ ecsLabelRegion: model.LabelValue(d.cfg.Region),
+ ecsLabelLaunchType: model.LabelValue(task.LaunchType),
+ ecsLabelAvailabilityZone: model.LabelValue(*task.AvailabilityZone),
+ ecsLabelDesiredStatus: model.LabelValue(*task.DesiredStatus),
+ ecsLabelLastStatus: model.LabelValue(*task.LastStatus),
+ ecsLabelHealthStatus: model.LabelValue(task.HealthStatus),
+ ecsLabelNetworkMode: model.LabelValue(networkMode),
+ }
+
+ // Add subnet ID when available (awsvpc mode from ENI, bridge/host from EC2 instance)
+ if subnetID != "" {
+ labels[ecsLabelSubnetID] = model.LabelValue(subnetID)
+ }
+
+ // Add container instance and EC2 instance info for EC2 launch type
+ if task.ContainerInstanceArn != nil {
+ labels[ecsLabelContainerInstanceARN] = model.LabelValue(*task.ContainerInstanceArn)
+ }
+ if ec2InstanceID != "" {
+ labels[ecsLabelEC2InstanceID] = model.LabelValue(ec2InstanceID)
+ }
+ if ec2InstanceType != "" {
+ labels[ecsLabelEC2InstanceType] = model.LabelValue(ec2InstanceType)
+ }
+ if ec2InstancePrivateIP != "" {
+ labels[ecsLabelEC2InstancePrivateIP] = model.LabelValue(ec2InstancePrivateIP)
+ }
+ if ec2InstancePublicIP != "" {
+ labels[ecsLabelEC2InstancePublicIP] = model.LabelValue(ec2InstancePublicIP)
+ }
+ if publicIP != "" {
+ labels[ecsLabelPublicIP] = model.LabelValue(publicIP)
+ }
+
+ if task.PlatformFamily != nil {
+ labels[ecsLabelPlatformFamily] = model.LabelValue(*task.PlatformFamily)
+ }
+ if task.PlatformVersion != nil {
+ labels[ecsLabelPlatformVersion] = model.LabelValue(*task.PlatformVersion)
+ }
+
+ labels[model.AddressLabel] = model.LabelValue(net.JoinHostPort(ipAddress, strconv.Itoa(d.cfg.Port)))
+
+ // Add cluster tags
+ for _, clusterTag := range cluster.Tags {
+ if clusterTag.Key != nil && clusterTag.Value != nil {
+ labels[model.LabelName(ecsLabelTagCluster+strutil.SanitizeLabelName(*clusterTag.Key))] = model.LabelValue(*clusterTag.Value)
+ }
+ }
+
+ // If this is not a standalone task, add service information and tags
+ if !isStandaloneTask(task) {
+ service, ok := services[getServiceNameFromTaskGroup(task)]
+ if !ok {
+ d.logger.Debug("Service not found for task", "task", *task.TaskArn, "service", getServiceNameFromTaskGroup(task))
+ }
+ if service.ServiceName != nil {
+ labels[ecsLabelService] = model.LabelValue(*service.ServiceName)
+ }
+ if service.ServiceArn != nil {
+ labels[ecsLabelServiceARN] = model.LabelValue(*service.ServiceArn)
+ }
+ if service.Status != nil {
+ labels[ecsLabelServiceStatus] = model.LabelValue(*service.Status)
+ }
+
+ // Add service tags
+ for _, serviceTag := range service.Tags {
+ if serviceTag.Key != nil && serviceTag.Value != nil {
+ labels[model.LabelName(ecsLabelTagService+strutil.SanitizeLabelName(*serviceTag.Key))] = model.LabelValue(*serviceTag.Value)
+ }
+ }
+ }
+
+ // Add task tags
+ for _, taskTag := range task.Tags {
+ if taskTag.Key != nil && taskTag.Value != nil {
+ labels[model.LabelName(ecsLabelTagTask+strutil.SanitizeLabelName(*taskTag.Key))] = model.LabelValue(*taskTag.Value)
+ }
+ }
+
+ // Add EC2 instance tags (if running on EC2)
+ if ec2InstanceID != "" {
+ if info, ok := ec2Instances[ec2InstanceID]; ok {
+ for tagKey, tagValue := range info.tags {
+ labels[model.LabelName(ecsLabelTagEC2+strutil.SanitizeLabelName(tagKey))] = model.LabelValue(tagValue)
+ }
+ }
+ }
+
+ taskMu.Lock()
+ taskTargets = append(taskTargets, labels)
+ taskMu.Unlock()
+ }(cluster, services, task, containerInstances, ec2Instances, eniToPublicIP)
+ }
+
+ taskWg.Wait()
+
+ // Add this cluster's task targets to the overall collection
+ clusterMu.Lock()
+ clusterTargets = append(clusterTargets, taskTargets...)
+ clusterMu.Unlock()
+ }(clusterMap[clusterARN], serviceMap[clusterARN], taskARNs)
+ }
+
+ clusterWg.Wait()
+
+ // Set all targets to the target group
+ tg.Targets = clusterTargets
+
+ return []*targetgroup.Group{tg}, nil
+}
+
+func isStandaloneTask(task types.Task) bool {
+ // A standalone task will have a group of "family:task-def-name"
+ return task.Group != nil && strings.HasPrefix(*task.Group, "family:")
+}
+
+func getServiceNameFromTaskGroup(task types.Task) string {
+ return strings.Split(*task.Group, ":")[1]
+}
diff --git a/discovery/aws/ecs_test.go b/discovery/aws/ecs_test.go
new file mode 100644
index 0000000000..bb1f96a28e
--- /dev/null
+++ b/discovery/aws/ecs_test.go
@@ -0,0 +1,1807 @@
+// 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 aws
+
+import (
+ "context"
+ "testing"
+
+ "github.com/aws/aws-sdk-go-v2/service/ec2"
+ ec2Types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
+ "github.com/aws/aws-sdk-go-v2/service/ecs"
+ ecsTypes "github.com/aws/aws-sdk-go-v2/service/ecs/types"
+ "github.com/prometheus/common/model"
+ "github.com/stretchr/testify/require"
+
+ "github.com/prometheus/prometheus/discovery/targetgroup"
+)
+
+// Struct for test data.
+type ecsDataStore struct {
+ region string
+
+ clusters []ecsTypes.Cluster
+ services []ecsTypes.Service
+ tasks []ecsTypes.Task
+ containerInstances []ecsTypes.ContainerInstance
+ ec2Instances map[string]ec2InstanceInfo // EC2 instance ID to instance info
+ eniPublicIPs map[string]string // ENI ID to public IP
+}
+
+func TestECSDiscoveryListClusterARNs(t *testing.T) {
+ ctx := context.Background()
+
+ // iterate through the test cases
+ for _, tt := range []struct {
+ name string
+ ecsData *ecsDataStore
+ expected []string
+ }{
+ {
+ name: "MultipleClusters",
+ ecsData: &ecsDataStore{
+ region: "us-west-2",
+ clusters: []ecsTypes.Cluster{
+ {
+ ClusterName: strptr("test-cluster"),
+ ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"),
+ Status: strptr("ACTIVE"),
+ },
+ {
+ ClusterName: strptr("prod-cluster"),
+ ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/prod-cluster"),
+ Status: strptr("ACTIVE"),
+ },
+ },
+ },
+ expected: []string{
+ "arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster",
+ "arn:aws:ecs:us-west-2:123456789012:cluster/prod-cluster",
+ },
+ },
+ {
+ name: "SingleCluster",
+ ecsData: &ecsDataStore{
+ region: "us-east-1",
+ clusters: []ecsTypes.Cluster{
+ {
+ ClusterName: strptr("single-cluster"),
+ ClusterArn: strptr("arn:aws:ecs:us-east-1:123456789012:cluster/single-cluster"),
+ Status: strptr("ACTIVE"),
+ },
+ },
+ },
+ expected: []string{
+ "arn:aws:ecs:us-east-1:123456789012:cluster/single-cluster",
+ },
+ },
+ {
+ name: "NoClusters",
+ ecsData: &ecsDataStore{
+ region: "us-east-1",
+ clusters: []ecsTypes.Cluster{},
+ },
+ expected: nil,
+ },
+ } {
+ t.Run(tt.name, func(t *testing.T) {
+ client := newMockECSClient(tt.ecsData)
+
+ d := &ECSDiscovery{
+ ecs: client,
+ cfg: &ECSSDConfig{
+ Region: tt.ecsData.region,
+ RequestConcurrency: 10,
+ },
+ }
+
+ clusters, err := d.listClusterARNs(ctx)
+ require.NoError(t, err)
+ require.Equal(t, tt.expected, clusters)
+ })
+ }
+}
+
+func TestECSDiscoveryDescribeClusters(t *testing.T) {
+ ctx := context.Background()
+
+ // iterate through the test cases
+ for _, tt := range []struct {
+ name string
+ ecsData *ecsDataStore
+ clusterARNs []string
+ expected map[string]ecsTypes.Cluster
+ }{
+ {
+ name: "SingleClusterWithTags",
+ ecsData: &ecsDataStore{
+ region: "us-west-2",
+ clusters: []ecsTypes.Cluster{
+ {
+ ClusterName: strptr("test-cluster"),
+ ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"),
+ Status: strptr("ACTIVE"),
+ Tags: []ecsTypes.Tag{
+ {Key: strptr("Environment"), Value: strptr("test")},
+ {Key: strptr("Team"), Value: strptr("backend")},
+ },
+ },
+ },
+ },
+ clusterARNs: []string{"arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"},
+ expected: map[string]ecsTypes.Cluster{
+ "arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster": {
+ ClusterName: strptr("test-cluster"),
+ ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"),
+ Status: strptr("ACTIVE"),
+ Tags: []ecsTypes.Tag{
+ {Key: strptr("Environment"), Value: strptr("test")},
+ {Key: strptr("Team"), Value: strptr("backend")},
+ },
+ },
+ },
+ },
+ {
+ name: "MultipleClusters",
+ ecsData: &ecsDataStore{
+ region: "us-east-1",
+ clusters: []ecsTypes.Cluster{
+ {
+ ClusterName: strptr("cluster-1"),
+ ClusterArn: strptr("arn:aws:ecs:us-east-1:123456789012:cluster/cluster-1"),
+ Status: strptr("ACTIVE"),
+ },
+ {
+ ClusterName: strptr("cluster-2"),
+ ClusterArn: strptr("arn:aws:ecs:us-east-1:123456789012:cluster/cluster-2"),
+ Status: strptr("DRAINING"),
+ Tags: []ecsTypes.Tag{
+ {Key: strptr("Stage"), Value: strptr("prod")},
+ },
+ },
+ },
+ },
+ clusterARNs: []string{
+ "arn:aws:ecs:us-east-1:123456789012:cluster/cluster-1",
+ "arn:aws:ecs:us-east-1:123456789012:cluster/cluster-2",
+ },
+ expected: map[string]ecsTypes.Cluster{
+ "arn:aws:ecs:us-east-1:123456789012:cluster/cluster-1": {
+ ClusterName: strptr("cluster-1"),
+ ClusterArn: strptr("arn:aws:ecs:us-east-1:123456789012:cluster/cluster-1"),
+ Status: strptr("ACTIVE"),
+ },
+ "arn:aws:ecs:us-east-1:123456789012:cluster/cluster-2": {
+ ClusterName: strptr("cluster-2"),
+ ClusterArn: strptr("arn:aws:ecs:us-east-1:123456789012:cluster/cluster-2"),
+ Status: strptr("DRAINING"),
+ Tags: []ecsTypes.Tag{
+ {Key: strptr("Stage"), Value: strptr("prod")},
+ },
+ },
+ },
+ },
+ } {
+ t.Run(tt.name, func(t *testing.T) {
+ client := newMockECSClient(tt.ecsData)
+
+ d := &ECSDiscovery{
+ ecs: client,
+ cfg: &ECSSDConfig{
+ Region: tt.ecsData.region,
+ RequestConcurrency: 10,
+ },
+ }
+
+ clusterMap, err := d.describeClusters(ctx, tt.clusterARNs)
+ require.NoError(t, err)
+ require.Equal(t, tt.expected, clusterMap)
+ })
+ }
+}
+
+func TestECSDiscoveryListServiceARNs(t *testing.T) {
+ ctx := context.Background()
+
+ for _, tt := range []struct {
+ name string
+ ecsData *ecsDataStore
+ clusterARNs []string
+ expected map[string][]string
+ }{
+ {
+ name: "SingleClusterWithServices",
+ ecsData: &ecsDataStore{
+ region: "us-west-2",
+ services: []ecsTypes.Service{
+ {
+ ServiceName: strptr("web-service"),
+ ServiceArn: strptr("arn:aws:ecs:us-west-2:123456789012:service/test-cluster/web-service"),
+ ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"),
+ Status: strptr("ACTIVE"),
+ },
+ {
+ ServiceName: strptr("api-service"),
+ ServiceArn: strptr("arn:aws:ecs:us-west-2:123456789012:service/test-cluster/api-service"),
+ ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"),
+ Status: strptr("ACTIVE"),
+ },
+ },
+ },
+ clusterARNs: []string{"arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"},
+ expected: map[string][]string{
+ "arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster": {
+ "arn:aws:ecs:us-west-2:123456789012:service/test-cluster/web-service",
+ "arn:aws:ecs:us-west-2:123456789012:service/test-cluster/api-service",
+ },
+ },
+ },
+ {
+ name: "MultipleClusters",
+ ecsData: &ecsDataStore{
+ region: "us-west-2",
+ services: []ecsTypes.Service{
+ {
+ ServiceName: strptr("web-service"),
+ ServiceArn: strptr("arn:aws:ecs:us-west-2:123456789012:service/cluster-1/web-service"),
+ ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/cluster-1"),
+ Status: strptr("ACTIVE"),
+ },
+ {
+ ServiceName: strptr("api-service"),
+ ServiceArn: strptr("arn:aws:ecs:us-west-2:123456789012:service/cluster-2/api-service"),
+ ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/cluster-2"),
+ Status: strptr("ACTIVE"),
+ },
+ },
+ },
+ clusterARNs: []string{
+ "arn:aws:ecs:us-west-2:123456789012:cluster/cluster-1",
+ "arn:aws:ecs:us-west-2:123456789012:cluster/cluster-2",
+ },
+ expected: map[string][]string{
+ "arn:aws:ecs:us-west-2:123456789012:cluster/cluster-1": {
+ "arn:aws:ecs:us-west-2:123456789012:service/cluster-1/web-service",
+ },
+ "arn:aws:ecs:us-west-2:123456789012:cluster/cluster-2": {
+ "arn:aws:ecs:us-west-2:123456789012:service/cluster-2/api-service",
+ },
+ },
+ },
+ {
+ name: "EmptyCluster",
+ ecsData: &ecsDataStore{
+ region: "us-west-2",
+ services: []ecsTypes.Service{},
+ },
+ clusterARNs: []string{"arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"},
+ expected: map[string][]string{
+ "arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster": nil,
+ },
+ },
+ } {
+ t.Run(tt.name, func(t *testing.T) {
+ client := newMockECSClient(tt.ecsData)
+
+ d := &ECSDiscovery{
+ ecs: client,
+ cfg: &ECSSDConfig{
+ Region: tt.ecsData.region,
+ RequestConcurrency: 2,
+ },
+ }
+
+ serviceMap, err := d.listServiceARNs(ctx, tt.clusterARNs)
+ require.NoError(t, err)
+ require.Equal(t, tt.expected, serviceMap)
+ })
+ }
+}
+
+func TestECSDiscoveryDescribeServices(t *testing.T) {
+ ctx := context.Background()
+
+ for _, tt := range []struct {
+ name string
+ ecsData *ecsDataStore
+ clusterARN string
+ serviceARNs []string
+ expected map[string]ecsTypes.Service
+ }{
+ {
+ name: "ServicesWithTags",
+ ecsData: &ecsDataStore{
+ region: "us-west-2",
+ services: []ecsTypes.Service{
+ {
+ ServiceName: strptr("web-service"),
+ ServiceArn: strptr("arn:aws:ecs:us-west-2:123456789012:service/test-cluster/web-service"),
+ ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"),
+ Status: strptr("ACTIVE"),
+ Tags: []ecsTypes.Tag{
+ {Key: strptr("Environment"), Value: strptr("production")},
+ {Key: strptr("Team"), Value: strptr("platform")},
+ },
+ },
+ {
+ ServiceName: strptr("api-service"),
+ ServiceArn: strptr("arn:aws:ecs:us-west-2:123456789012:service/test-cluster/api-service"),
+ ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"),
+ Status: strptr("ACTIVE"),
+ Tags: []ecsTypes.Tag{
+ {Key: strptr("Environment"), Value: strptr("staging")},
+ },
+ },
+ },
+ },
+ clusterARN: "arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster",
+ serviceARNs: []string{
+ "arn:aws:ecs:us-west-2:123456789012:service/test-cluster/web-service",
+ "arn:aws:ecs:us-west-2:123456789012:service/test-cluster/api-service",
+ },
+ expected: map[string]ecsTypes.Service{
+ "web-service": {
+ ServiceName: strptr("web-service"),
+ ServiceArn: strptr("arn:aws:ecs:us-west-2:123456789012:service/test-cluster/web-service"),
+ ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"),
+ Status: strptr("ACTIVE"),
+ Tags: []ecsTypes.Tag{
+ {Key: strptr("Environment"), Value: strptr("production")},
+ {Key: strptr("Team"), Value: strptr("platform")},
+ },
+ },
+ "api-service": {
+ ServiceName: strptr("api-service"),
+ ServiceArn: strptr("arn:aws:ecs:us-west-2:123456789012:service/test-cluster/api-service"),
+ ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"),
+ Status: strptr("ACTIVE"),
+ Tags: []ecsTypes.Tag{
+ {Key: strptr("Environment"), Value: strptr("staging")},
+ },
+ },
+ },
+ },
+ {
+ name: "EmptyServiceList",
+ ecsData: &ecsDataStore{
+ region: "us-west-2",
+ services: []ecsTypes.Service{},
+ },
+ clusterARN: "arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster",
+ serviceARNs: []string{},
+ expected: map[string]ecsTypes.Service{},
+ },
+ } {
+ t.Run(tt.name, func(t *testing.T) {
+ client := newMockECSClient(tt.ecsData)
+
+ d := &ECSDiscovery{
+ ecs: client,
+ cfg: &ECSSDConfig{
+ Region: tt.ecsData.region,
+ RequestConcurrency: 2,
+ },
+ }
+
+ services, err := d.describeServices(ctx, tt.clusterARN, tt.serviceARNs)
+ require.NoError(t, err)
+ require.Equal(t, tt.expected, services)
+ })
+ }
+}
+
+func TestECSDiscoveryDescribeContainerInstances(t *testing.T) {
+ ctx := context.Background()
+
+ for _, tt := range []struct {
+ name string
+ ecsData *ecsDataStore
+ clusterARN string
+ tasks []ecsTypes.Task
+ expected map[string]string
+ }{
+ {
+ name: "EC2Tasks",
+ ecsData: &ecsDataStore{
+ region: "us-west-2",
+ containerInstances: []ecsTypes.ContainerInstance{
+ {
+ ContainerInstanceArn: strptr("arn:aws:ecs:us-west-2:123456789012:container-instance/test-cluster/abc123"),
+ Ec2InstanceId: strptr("i-1234567890abcdef0"),
+ },
+ {
+ ContainerInstanceArn: strptr("arn:aws:ecs:us-west-2:123456789012:container-instance/test-cluster/xyz789"),
+ Ec2InstanceId: strptr("i-0987654321fedcba0"),
+ },
+ },
+ },
+ clusterARN: "arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster",
+ tasks: []ecsTypes.Task{
+ {
+ TaskArn: strptr("arn:aws:ecs:us-west-2:123456789012:task/test-cluster/task-1"),
+ ContainerInstanceArn: strptr("arn:aws:ecs:us-west-2:123456789012:container-instance/test-cluster/abc123"),
+ LaunchType: ecsTypes.LaunchTypeEc2,
+ },
+ {
+ TaskArn: strptr("arn:aws:ecs:us-west-2:123456789012:task/test-cluster/task-2"),
+ ContainerInstanceArn: strptr("arn:aws:ecs:us-west-2:123456789012:container-instance/test-cluster/xyz789"),
+ LaunchType: ecsTypes.LaunchTypeEc2,
+ },
+ },
+ expected: map[string]string{
+ "arn:aws:ecs:us-west-2:123456789012:container-instance/test-cluster/abc123": "i-1234567890abcdef0",
+ "arn:aws:ecs:us-west-2:123456789012:container-instance/test-cluster/xyz789": "i-0987654321fedcba0",
+ },
+ },
+ {
+ name: "FargateTasks",
+ ecsData: &ecsDataStore{
+ region: "us-west-2",
+ containerInstances: []ecsTypes.ContainerInstance{},
+ },
+ clusterARN: "arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster",
+ tasks: []ecsTypes.Task{
+ {
+ TaskArn: strptr("arn:aws:ecs:us-west-2:123456789012:task/test-cluster/task-1"),
+ LaunchType: ecsTypes.LaunchTypeFargate,
+ },
+ },
+ expected: map[string]string{},
+ },
+ {
+ name: "MixedTasks",
+ ecsData: &ecsDataStore{
+ region: "us-west-2",
+ containerInstances: []ecsTypes.ContainerInstance{
+ {
+ ContainerInstanceArn: strptr("arn:aws:ecs:us-west-2:123456789012:container-instance/test-cluster/abc123"),
+ Ec2InstanceId: strptr("i-1234567890abcdef0"),
+ },
+ },
+ },
+ clusterARN: "arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster",
+ tasks: []ecsTypes.Task{
+ {
+ TaskArn: strptr("arn:aws:ecs:us-west-2:123456789012:task/test-cluster/task-ec2"),
+ ContainerInstanceArn: strptr("arn:aws:ecs:us-west-2:123456789012:container-instance/test-cluster/abc123"),
+ LaunchType: ecsTypes.LaunchTypeEc2,
+ },
+ {
+ TaskArn: strptr("arn:aws:ecs:us-west-2:123456789012:task/test-cluster/task-fargate"),
+ LaunchType: ecsTypes.LaunchTypeFargate,
+ },
+ },
+ expected: map[string]string{
+ "arn:aws:ecs:us-west-2:123456789012:container-instance/test-cluster/abc123": "i-1234567890abcdef0",
+ },
+ },
+ } {
+ t.Run(tt.name, func(t *testing.T) {
+ client := newMockECSClient(tt.ecsData)
+
+ d := &ECSDiscovery{
+ ecs: client,
+ cfg: &ECSSDConfig{
+ Region: tt.ecsData.region,
+ RequestConcurrency: 2,
+ },
+ }
+
+ containerInstances, err := d.describeContainerInstances(ctx, tt.clusterARN, tt.tasks)
+ require.NoError(t, err)
+ require.Equal(t, tt.expected, containerInstances)
+ })
+ }
+}
+
+func TestECSDiscoveryDescribeEC2Instances(t *testing.T) {
+ ctx := context.Background()
+
+ for _, tt := range []struct {
+ name string
+ ecsData *ecsDataStore
+ instanceIDs []string
+ expected map[string]ec2InstanceInfo
+ }{
+ {
+ name: "InstancesWithTags",
+ ecsData: &ecsDataStore{
+ region: "us-west-2",
+ ec2Instances: map[string]ec2InstanceInfo{
+ "i-1234567890abcdef0": {
+ privateIP: "10.0.1.50",
+ publicIP: "54.1.2.3",
+ subnetID: "subnet-12345",
+ instanceType: "t3.medium",
+ tags: map[string]string{
+ "Name": "ecs-host-1",
+ "Environment": "production",
+ },
+ },
+ "i-0987654321fedcba0": {
+ privateIP: "10.0.1.75",
+ publicIP: "54.2.3.4",
+ subnetID: "subnet-67890",
+ instanceType: "t3.large",
+ tags: map[string]string{
+ "Name": "ecs-host-2",
+ "Team": "platform",
+ },
+ },
+ },
+ },
+ instanceIDs: []string{"i-1234567890abcdef0", "i-0987654321fedcba0"},
+ expected: map[string]ec2InstanceInfo{
+ "i-1234567890abcdef0": {
+ privateIP: "10.0.1.50",
+ publicIP: "54.1.2.3",
+ subnetID: "subnet-12345",
+ instanceType: "t3.medium",
+ tags: map[string]string{
+ "Name": "ecs-host-1",
+ "Environment": "production",
+ },
+ },
+ "i-0987654321fedcba0": {
+ privateIP: "10.0.1.75",
+ publicIP: "54.2.3.4",
+ subnetID: "subnet-67890",
+ instanceType: "t3.large",
+ tags: map[string]string{
+ "Name": "ecs-host-2",
+ "Team": "platform",
+ },
+ },
+ },
+ },
+ {
+ name: "EmptyList",
+ ecsData: &ecsDataStore{
+ region: "us-west-2",
+ ec2Instances: map[string]ec2InstanceInfo{},
+ },
+ instanceIDs: []string{},
+ expected: map[string]ec2InstanceInfo{},
+ },
+ {
+ name: "InstanceWithoutPublicIP",
+ ecsData: &ecsDataStore{
+ region: "us-west-2",
+ ec2Instances: map[string]ec2InstanceInfo{
+ "i-privateonly": {
+ privateIP: "10.0.1.100",
+ publicIP: "",
+ subnetID: "subnet-private",
+ instanceType: "t3.micro",
+ tags: map[string]string{},
+ },
+ },
+ },
+ instanceIDs: []string{"i-privateonly"},
+ expected: map[string]ec2InstanceInfo{
+ "i-privateonly": {
+ privateIP: "10.0.1.100",
+ publicIP: "",
+ subnetID: "subnet-private",
+ instanceType: "t3.micro",
+ tags: map[string]string{},
+ },
+ },
+ },
+ } {
+ t.Run(tt.name, func(t *testing.T) {
+ ec2Client := newMockECSEC2Client(tt.ecsData.ec2Instances, nil)
+
+ d := &ECSDiscovery{
+ ec2: ec2Client,
+ cfg: &ECSSDConfig{
+ Region: tt.ecsData.region,
+ RequestConcurrency: 2,
+ },
+ }
+
+ instances, err := d.describeEC2Instances(ctx, tt.instanceIDs)
+ require.NoError(t, err)
+ require.Equal(t, tt.expected, instances)
+ })
+ }
+}
+
+func TestECSDiscoveryDescribeNetworkInterfaces(t *testing.T) {
+ ctx := context.Background()
+
+ for _, tt := range []struct {
+ name string
+ ecsData *ecsDataStore
+ tasks []ecsTypes.Task
+ expected map[string]string
+ }{
+ {
+ name: "AwsvpcTasksWithPublicIPs",
+ ecsData: &ecsDataStore{
+ region: "us-west-2",
+ eniPublicIPs: map[string]string{
+ "eni-12345": "52.1.2.3",
+ "eni-67890": "52.2.3.4",
+ },
+ },
+ tasks: []ecsTypes.Task{
+ {
+ TaskArn: strptr("arn:aws:ecs:us-west-2:123456789012:task/test-cluster/task-1"),
+ LaunchType: ecsTypes.LaunchTypeFargate,
+ Attachments: []ecsTypes.Attachment{
+ {
+ Type: strptr("ElasticNetworkInterface"),
+ Details: []ecsTypes.KeyValuePair{
+ {Name: strptr("networkInterfaceId"), Value: strptr("eni-12345")},
+ {Name: strptr("privateIPv4Address"), Value: strptr("10.0.1.100")},
+ },
+ },
+ },
+ },
+ {
+ TaskArn: strptr("arn:aws:ecs:us-west-2:123456789012:task/test-cluster/task-2"),
+ LaunchType: ecsTypes.LaunchTypeFargate,
+ Attachments: []ecsTypes.Attachment{
+ {
+ Type: strptr("ElasticNetworkInterface"),
+ Details: []ecsTypes.KeyValuePair{
+ {Name: strptr("networkInterfaceId"), Value: strptr("eni-67890")},
+ {Name: strptr("privateIPv4Address"), Value: strptr("10.0.1.200")},
+ },
+ },
+ },
+ },
+ },
+ expected: map[string]string{
+ "eni-12345": "52.1.2.3",
+ "eni-67890": "52.2.3.4",
+ },
+ },
+ {
+ name: "AwsvpcTasksWithoutPublicIPs",
+ ecsData: &ecsDataStore{
+ region: "us-west-2",
+ eniPublicIPs: map[string]string{},
+ },
+ tasks: []ecsTypes.Task{
+ {
+ TaskArn: strptr("arn:aws:ecs:us-west-2:123456789012:task/test-cluster/task-1"),
+ LaunchType: ecsTypes.LaunchTypeFargate,
+ Attachments: []ecsTypes.Attachment{
+ {
+ Type: strptr("ElasticNetworkInterface"),
+ Details: []ecsTypes.KeyValuePair{
+ {Name: strptr("networkInterfaceId"), Value: strptr("eni-private")},
+ {Name: strptr("privateIPv4Address"), Value: strptr("10.0.1.100")},
+ },
+ },
+ },
+ },
+ },
+ expected: map[string]string{},
+ },
+ {
+ name: "BridgeTasksNoENI",
+ ecsData: &ecsDataStore{
+ region: "us-west-2",
+ eniPublicIPs: map[string]string{},
+ },
+ tasks: []ecsTypes.Task{
+ {
+ TaskArn: strptr("arn:aws:ecs:us-west-2:123456789012:task/test-cluster/task-1"),
+ LaunchType: ecsTypes.LaunchTypeEc2,
+ // No ENI attachment for bridge networking
+ Attachments: []ecsTypes.Attachment{},
+ },
+ },
+ expected: map[string]string{},
+ },
+ {
+ name: "MixedTasks",
+ ecsData: &ecsDataStore{
+ region: "us-west-2",
+ eniPublicIPs: map[string]string{
+ "eni-fargate": "52.1.2.3",
+ },
+ },
+ tasks: []ecsTypes.Task{
+ {
+ TaskArn: strptr("arn:aws:ecs:us-west-2:123456789012:task/test-cluster/task-fargate"),
+ LaunchType: ecsTypes.LaunchTypeFargate,
+ Attachments: []ecsTypes.Attachment{
+ {
+ Type: strptr("ElasticNetworkInterface"),
+ Details: []ecsTypes.KeyValuePair{
+ {Name: strptr("networkInterfaceId"), Value: strptr("eni-fargate")},
+ {Name: strptr("privateIPv4Address"), Value: strptr("10.0.1.100")},
+ },
+ },
+ },
+ },
+ {
+ TaskArn: strptr("arn:aws:ecs:us-west-2:123456789012:task/test-cluster/task-bridge"),
+ LaunchType: ecsTypes.LaunchTypeEc2,
+ Attachments: []ecsTypes.Attachment{},
+ },
+ },
+ expected: map[string]string{
+ "eni-fargate": "52.1.2.3",
+ },
+ },
+ } {
+ t.Run(tt.name, func(t *testing.T) {
+ ec2Client := newMockECSEC2Client(nil, tt.ecsData.eniPublicIPs)
+
+ d := &ECSDiscovery{
+ ec2: ec2Client,
+ cfg: &ECSSDConfig{
+ Region: tt.ecsData.region,
+ RequestConcurrency: 2,
+ },
+ }
+
+ eniMap, err := d.describeNetworkInterfaces(ctx, tt.tasks)
+ require.NoError(t, err)
+ require.Equal(t, tt.expected, eniMap)
+ })
+ }
+}
+
+func TestECSDiscoveryListTaskARNs(t *testing.T) {
+ ctx := context.Background()
+
+ // iterate through the test cases
+ for _, tt := range []struct {
+ name string
+ ecsData *ecsDataStore
+ clusterARNs []string
+ expected map[string][]string
+ }{
+ {
+ name: "TasksInCluster",
+ ecsData: &ecsDataStore{
+ region: "us-west-2",
+ tasks: []ecsTypes.Task{
+ {
+ TaskArn: strptr("arn:aws:ecs:us-west-2:123456789012:task/test-cluster/task-1"),
+ ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"),
+ Group: strptr("service:web-service"),
+ TaskDefinitionArn: strptr("arn:aws:ecs:us-west-2:123456789012:task-definition/web-task:1"),
+ },
+ {
+ TaskArn: strptr("arn:aws:ecs:us-west-2:123456789012:task/test-cluster/task-2"),
+ ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"),
+ Group: strptr("service:web-service"),
+ TaskDefinitionArn: strptr("arn:aws:ecs:us-west-2:123456789012:task-definition/web-task:1"),
+ },
+ {
+ TaskArn: strptr("arn:aws:ecs:us-west-2:123456789012:task/test-cluster/task-3"),
+ ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"),
+ Group: strptr("service:api-service"),
+ TaskDefinitionArn: strptr("arn:aws:ecs:us-west-2:123456789012:task-definition/api-task:2"),
+ },
+ },
+ },
+ clusterARNs: []string{"arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"},
+ expected: map[string][]string{
+ "arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster": {
+ "arn:aws:ecs:us-west-2:123456789012:task/test-cluster/task-1",
+ "arn:aws:ecs:us-west-2:123456789012:task/test-cluster/task-2",
+ "arn:aws:ecs:us-west-2:123456789012:task/test-cluster/task-3",
+ },
+ },
+ },
+ {
+ name: "EmptyCluster",
+ ecsData: &ecsDataStore{
+ region: "us-west-2",
+ tasks: []ecsTypes.Task{},
+ },
+ clusterARNs: []string{"arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"},
+ expected: map[string][]string{
+ "arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster": nil,
+ },
+ },
+ } {
+ t.Run(tt.name, func(t *testing.T) {
+ client := newMockECSClient(tt.ecsData)
+
+ d := &ECSDiscovery{
+ ecs: client,
+ cfg: &ECSSDConfig{
+ Region: tt.ecsData.region,
+ RequestConcurrency: 1,
+ },
+ }
+
+ taskMap, err := d.listTaskARNs(ctx, tt.clusterARNs)
+ require.NoError(t, err)
+ require.Equal(t, tt.expected, taskMap)
+ })
+ }
+}
+
+func TestECSDiscoveryDescribeTasks(t *testing.T) {
+ ctx := context.Background()
+
+ // iterate through the test cases
+ for _, tt := range []struct {
+ name string
+ ecsData *ecsDataStore
+ clusterARN string
+ taskARNs []string
+ expected []ecsTypes.Task
+ }{
+ {
+ name: "TasksInCluster",
+ ecsData: &ecsDataStore{
+ region: "us-west-2",
+ tasks: []ecsTypes.Task{
+ {
+ TaskArn: strptr("arn:aws:ecs:us-west-2:123456789012:task/test-cluster/task-1"),
+ ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"),
+ Group: strptr("service:web-service"),
+ TaskDefinitionArn: strptr("arn:aws:ecs:us-west-2:123456789012:task-definition/web-task:1"),
+ LastStatus: strptr("RUNNING"),
+ Tags: []ecsTypes.Tag{
+ {Key: strptr("Environment"), Value: strptr("production")},
+ },
+ },
+ {
+ TaskArn: strptr("arn:aws:ecs:us-west-2:123456789012:task/test-cluster/task-2"),
+ ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"),
+ Group: strptr("service:api-service"),
+ TaskDefinitionArn: strptr("arn:aws:ecs:us-west-2:123456789012:task-definition/api-task:2"),
+ LastStatus: strptr("RUNNING"),
+ },
+ },
+ },
+ clusterARN: "arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster",
+ taskARNs: []string{
+ "arn:aws:ecs:us-west-2:123456789012:task/test-cluster/task-1",
+ "arn:aws:ecs:us-west-2:123456789012:task/test-cluster/task-2",
+ },
+ expected: []ecsTypes.Task{
+ {
+ TaskArn: strptr("arn:aws:ecs:us-west-2:123456789012:task/test-cluster/task-1"),
+ ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"),
+ Group: strptr("service:web-service"),
+ TaskDefinitionArn: strptr("arn:aws:ecs:us-west-2:123456789012:task-definition/web-task:1"),
+ LastStatus: strptr("RUNNING"),
+ Tags: []ecsTypes.Tag{
+ {Key: strptr("Environment"), Value: strptr("production")},
+ },
+ },
+ {
+ TaskArn: strptr("arn:aws:ecs:us-west-2:123456789012:task/test-cluster/task-2"),
+ ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"),
+ Group: strptr("service:api-service"),
+ TaskDefinitionArn: strptr("arn:aws:ecs:us-west-2:123456789012:task-definition/api-task:2"),
+ LastStatus: strptr("RUNNING"),
+ },
+ },
+ },
+ {
+ name: "EmptyTaskList",
+ ecsData: &ecsDataStore{
+ region: "us-west-2",
+ tasks: []ecsTypes.Task{},
+ },
+ clusterARN: "arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster",
+ taskARNs: []string{},
+ expected: nil,
+ },
+ } {
+ t.Run(tt.name, func(t *testing.T) {
+ client := newMockECSClient(tt.ecsData)
+
+ d := &ECSDiscovery{
+ ecs: client,
+ cfg: &ECSSDConfig{
+ Region: tt.ecsData.region,
+ RequestConcurrency: 1,
+ },
+ }
+
+ tasks, err := d.describeTasks(ctx, tt.clusterARN, tt.taskARNs)
+ require.NoError(t, err)
+ require.Equal(t, tt.expected, tasks)
+ })
+ }
+}
+
+func TestECSDiscoveryRefresh(t *testing.T) {
+ ctx := context.Background()
+
+ tests := []struct {
+ name string
+ ecsData *ecsDataStore
+ expected []*targetgroup.Group
+ }{
+ {
+ name: "SingleClusterWithTasks",
+ ecsData: &ecsDataStore{
+ region: "us-west-2",
+ clusters: []ecsTypes.Cluster{
+ {
+ ClusterName: strptr("test-cluster"),
+ ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"),
+ Status: strptr("ACTIVE"),
+ Tags: []ecsTypes.Tag{
+ {Key: strptr("Environment"), Value: strptr("test")},
+ },
+ },
+ },
+ services: []ecsTypes.Service{
+ {
+ ServiceName: strptr("web-service"),
+ ServiceArn: strptr("arn:aws:ecs:us-west-2:123456789012:service/test-cluster/web-service"),
+ ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"),
+ Status: strptr("ACTIVE"),
+ Tags: []ecsTypes.Tag{
+ {Key: strptr("App"), Value: strptr("web")},
+ },
+ },
+ },
+ tasks: []ecsTypes.Task{
+ {
+ TaskArn: strptr("arn:aws:ecs:us-west-2:123456789012:task/test-cluster/task-1"),
+ ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"),
+ TaskDefinitionArn: strptr("arn:aws:ecs:us-west-2:123456789012:task-definition/web-task:1"),
+ Group: strptr("service:web-service"),
+ LaunchType: ecsTypes.LaunchTypeFargate,
+ LastStatus: strptr("RUNNING"),
+ DesiredStatus: strptr("RUNNING"),
+ HealthStatus: ecsTypes.HealthStatusHealthy,
+ AvailabilityZone: strptr("us-west-2a"),
+ PlatformFamily: strptr("Linux"),
+ PlatformVersion: strptr("1.4.0"),
+ Attachments: []ecsTypes.Attachment{
+ {
+ Type: strptr("ElasticNetworkInterface"),
+ Details: []ecsTypes.KeyValuePair{
+ {Name: strptr("subnetId"), Value: strptr("subnet-12345")},
+ {Name: strptr("privateIPv4Address"), Value: strptr("10.0.1.100")},
+ {Name: strptr("networkInterfaceId"), Value: strptr("eni-fargate-123")},
+ },
+ },
+ },
+ Tags: []ecsTypes.Tag{
+ {Key: strptr("Version"), Value: strptr("v1.0")},
+ },
+ },
+ },
+ eniPublicIPs: map[string]string{
+ "eni-fargate-123": "52.1.2.3",
+ },
+ },
+ expected: []*targetgroup.Group{
+ {
+ Source: "us-west-2",
+ Targets: []model.LabelSet{
+ {
+ model.AddressLabel: model.LabelValue("10.0.1.100:80"),
+ "__meta_ecs_cluster": model.LabelValue("test-cluster"),
+ "__meta_ecs_cluster_arn": model.LabelValue("arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"),
+ "__meta_ecs_service": model.LabelValue("web-service"),
+ "__meta_ecs_service_arn": model.LabelValue("arn:aws:ecs:us-west-2:123456789012:service/test-cluster/web-service"),
+ "__meta_ecs_service_status": model.LabelValue("ACTIVE"),
+ "__meta_ecs_task_group": model.LabelValue("service:web-service"),
+ "__meta_ecs_task_arn": model.LabelValue("arn:aws:ecs:us-west-2:123456789012:task/test-cluster/task-1"),
+ "__meta_ecs_task_definition": model.LabelValue("arn:aws:ecs:us-west-2:123456789012:task-definition/web-task:1"),
+ "__meta_ecs_region": model.LabelValue("us-west-2"),
+ "__meta_ecs_availability_zone": model.LabelValue("us-west-2a"),
+ "__meta_ecs_subnet_id": model.LabelValue("subnet-12345"),
+ "__meta_ecs_ip_address": model.LabelValue("10.0.1.100"),
+ "__meta_ecs_launch_type": model.LabelValue("FARGATE"),
+ "__meta_ecs_desired_status": model.LabelValue("RUNNING"),
+ "__meta_ecs_last_status": model.LabelValue("RUNNING"),
+ "__meta_ecs_health_status": model.LabelValue("HEALTHY"),
+ "__meta_ecs_platform_family": model.LabelValue("Linux"),
+ "__meta_ecs_platform_version": model.LabelValue("1.4.0"),
+ "__meta_ecs_network_mode": model.LabelValue("awsvpc"),
+ "__meta_ecs_public_ip": model.LabelValue("52.1.2.3"),
+ "__meta_ecs_tag_cluster_Environment": model.LabelValue("test"),
+ "__meta_ecs_tag_service_App": model.LabelValue("web"),
+ "__meta_ecs_tag_task_Version": model.LabelValue("v1.0"),
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "NoTasks",
+ ecsData: &ecsDataStore{
+ region: "us-east-1",
+ clusters: []ecsTypes.Cluster{
+ {
+ ClusterName: strptr("empty-cluster"),
+ ClusterArn: strptr("arn:aws:ecs:us-east-1:123456789012:cluster/empty-cluster"),
+ Status: strptr("ACTIVE"),
+ },
+ },
+ services: []ecsTypes.Service{
+ {
+ ServiceName: strptr("empty-service"),
+ ServiceArn: strptr("arn:aws:ecs:us-east-1:123456789012:service/empty-cluster/empty-service"),
+ ClusterArn: strptr("arn:aws:ecs:us-east-1:123456789012:cluster/empty-cluster"),
+ Status: strptr("ACTIVE"),
+ },
+ },
+ tasks: []ecsTypes.Task{},
+ },
+ expected: []*targetgroup.Group{
+ {
+ Source: "us-east-1",
+ },
+ },
+ },
+ {
+ name: "TaskWithoutENI",
+ ecsData: &ecsDataStore{
+ region: "us-west-2",
+ clusters: []ecsTypes.Cluster{
+ {
+ ClusterName: strptr("test-cluster"),
+ ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"),
+ Status: strptr("ACTIVE"),
+ },
+ },
+ services: []ecsTypes.Service{
+ {
+ ServiceName: strptr("service-1"),
+ ServiceArn: strptr("arn:aws:ecs:us-west-2:123456789012:service/test-cluster/service-1"),
+ ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"),
+ Status: strptr("ACTIVE"),
+ },
+ },
+ tasks: []ecsTypes.Task{
+ {
+ TaskArn: strptr("arn:aws:ecs:us-west-2:123456789012:task/test-cluster/task-1"),
+ ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"),
+ TaskDefinitionArn: strptr("arn:aws:ecs:us-west-2:123456789012:task-definition/task-def:1"),
+ Group: strptr("service:service-1"),
+ LaunchType: ecsTypes.LaunchTypeEc2,
+ LastStatus: strptr("RUNNING"),
+ DesiredStatus: strptr("RUNNING"),
+ HealthStatus: ecsTypes.HealthStatusHealthy,
+ AvailabilityZone: strptr("us-west-2a"),
+ // No attachments - should be skipped
+ Attachments: []ecsTypes.Attachment{},
+ },
+ },
+ },
+ expected: []*targetgroup.Group{
+ {
+ Source: "us-west-2",
+ },
+ },
+ },
+ {
+ name: "StandaloneTaskNoService",
+ ecsData: &ecsDataStore{
+ region: "us-west-2",
+ clusters: []ecsTypes.Cluster{
+ {
+ ClusterName: strptr("standalone-cluster"),
+ ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/standalone-cluster"),
+ Status: strptr("ACTIVE"),
+ },
+ },
+ services: []ecsTypes.Service{},
+ tasks: []ecsTypes.Task{
+ {
+ TaskArn: strptr("arn:aws:ecs:us-west-2:123456789012:task/standalone-cluster/task-standalone"),
+ ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/standalone-cluster"),
+ TaskDefinitionArn: strptr("arn:aws:ecs:us-west-2:123456789012:task-definition/standalone-task:1"),
+ Group: strptr("family:standalone-task"),
+ LaunchType: ecsTypes.LaunchTypeFargate,
+ LastStatus: strptr("RUNNING"),
+ DesiredStatus: strptr("RUNNING"),
+ HealthStatus: ecsTypes.HealthStatusHealthy,
+ AvailabilityZone: strptr("us-west-2a"),
+ Attachments: []ecsTypes.Attachment{
+ {
+ Type: strptr("ElasticNetworkInterface"),
+ Details: []ecsTypes.KeyValuePair{
+ {Name: strptr("subnetId"), Value: strptr("subnet-standalone-1")},
+ {Name: strptr("privateIPv4Address"), Value: strptr("10.0.4.10")},
+ {Name: strptr("networkInterfaceId"), Value: strptr("eni-standalone-123")},
+ },
+ },
+ },
+ Tags: []ecsTypes.Tag{
+ {Key: strptr("Role"), Value: strptr("batch")},
+ },
+ },
+ },
+ eniPublicIPs: map[string]string{
+ "eni-standalone-123": "52.4.5.6",
+ },
+ },
+ expected: []*targetgroup.Group{
+ {
+ Source: "us-west-2",
+ Targets: []model.LabelSet{
+ {
+ model.AddressLabel: model.LabelValue("10.0.4.10:80"),
+ "__meta_ecs_cluster": model.LabelValue("standalone-cluster"),
+ "__meta_ecs_cluster_arn": model.LabelValue("arn:aws:ecs:us-west-2:123456789012:cluster/standalone-cluster"),
+ "__meta_ecs_task_group": model.LabelValue("family:standalone-task"),
+ "__meta_ecs_task_arn": model.LabelValue("arn:aws:ecs:us-west-2:123456789012:task/standalone-cluster/task-standalone"),
+ "__meta_ecs_task_definition": model.LabelValue("arn:aws:ecs:us-west-2:123456789012:task-definition/standalone-task:1"),
+ "__meta_ecs_region": model.LabelValue("us-west-2"),
+ "__meta_ecs_availability_zone": model.LabelValue("us-west-2a"),
+ "__meta_ecs_subnet_id": model.LabelValue("subnet-standalone-1"),
+ "__meta_ecs_ip_address": model.LabelValue("10.0.4.10"),
+ "__meta_ecs_launch_type": model.LabelValue("FARGATE"),
+ "__meta_ecs_desired_status": model.LabelValue("RUNNING"),
+ "__meta_ecs_last_status": model.LabelValue("RUNNING"),
+ "__meta_ecs_health_status": model.LabelValue("HEALTHY"),
+ "__meta_ecs_network_mode": model.LabelValue("awsvpc"),
+ "__meta_ecs_public_ip": model.LabelValue("52.4.5.6"),
+ "__meta_ecs_tag_task_Role": model.LabelValue("batch"),
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "TaskWithBridgeNetworking",
+ ecsData: &ecsDataStore{
+ region: "us-west-2",
+ clusters: []ecsTypes.Cluster{
+ {
+ ClusterName: strptr("test-cluster"),
+ ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"),
+ Status: strptr("ACTIVE"),
+ },
+ },
+ services: []ecsTypes.Service{
+ {
+ ServiceName: strptr("bridge-service"),
+ ServiceArn: strptr("arn:aws:ecs:us-west-2:123456789012:service/test-cluster/bridge-service"),
+ ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"),
+ Status: strptr("ACTIVE"),
+ },
+ },
+ tasks: []ecsTypes.Task{
+ {
+ TaskArn: strptr("arn:aws:ecs:us-west-2:123456789012:task/test-cluster/task-bridge"),
+ ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"),
+ TaskDefinitionArn: strptr("arn:aws:ecs:us-west-2:123456789012:task-definition/bridge-task:1"),
+ Group: strptr("service:bridge-service"),
+ LaunchType: ecsTypes.LaunchTypeEc2,
+ LastStatus: strptr("RUNNING"),
+ DesiredStatus: strptr("RUNNING"),
+ HealthStatus: ecsTypes.HealthStatusHealthy,
+ AvailabilityZone: strptr("us-west-2a"),
+ ContainerInstanceArn: strptr("arn:aws:ecs:us-west-2:123456789012:container-instance/test-cluster/abc123"),
+ Attachments: []ecsTypes.Attachment{},
+ },
+ },
+ containerInstances: []ecsTypes.ContainerInstance{
+ {
+ ContainerInstanceArn: strptr("arn:aws:ecs:us-west-2:123456789012:container-instance/test-cluster/abc123"),
+ Ec2InstanceId: strptr("i-1234567890abcdef0"),
+ Status: strptr("ACTIVE"),
+ },
+ },
+ ec2Instances: map[string]ec2InstanceInfo{
+ "i-1234567890abcdef0": {
+ privateIP: "10.0.1.50",
+ publicIP: "54.1.2.3",
+ subnetID: "subnet-bridge-1",
+ instanceType: "t3.medium",
+ tags: map[string]string{
+ "Name": "ecs-host-1",
+ "Environment": "production",
+ },
+ },
+ },
+ },
+ expected: []*targetgroup.Group{
+ {
+ Source: "us-west-2",
+ Targets: []model.LabelSet{
+ {
+ model.AddressLabel: model.LabelValue("10.0.1.50:80"),
+ "__meta_ecs_cluster": model.LabelValue("test-cluster"),
+ "__meta_ecs_cluster_arn": model.LabelValue("arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"),
+ "__meta_ecs_service": model.LabelValue("bridge-service"),
+ "__meta_ecs_service_arn": model.LabelValue("arn:aws:ecs:us-west-2:123456789012:service/test-cluster/bridge-service"),
+ "__meta_ecs_service_status": model.LabelValue("ACTIVE"),
+ "__meta_ecs_task_group": model.LabelValue("service:bridge-service"),
+ "__meta_ecs_task_arn": model.LabelValue("arn:aws:ecs:us-west-2:123456789012:task/test-cluster/task-bridge"),
+ "__meta_ecs_task_definition": model.LabelValue("arn:aws:ecs:us-west-2:123456789012:task-definition/bridge-task:1"),
+ "__meta_ecs_region": model.LabelValue("us-west-2"),
+ "__meta_ecs_availability_zone": model.LabelValue("us-west-2a"),
+ "__meta_ecs_ip_address": model.LabelValue("10.0.1.50"),
+ "__meta_ecs_subnet_id": model.LabelValue("subnet-bridge-1"),
+ "__meta_ecs_launch_type": model.LabelValue("EC2"),
+ "__meta_ecs_desired_status": model.LabelValue("RUNNING"),
+ "__meta_ecs_last_status": model.LabelValue("RUNNING"),
+ "__meta_ecs_health_status": model.LabelValue("HEALTHY"),
+ "__meta_ecs_network_mode": model.LabelValue("bridge"),
+ "__meta_ecs_container_instance_arn": model.LabelValue("arn:aws:ecs:us-west-2:123456789012:container-instance/test-cluster/abc123"),
+ "__meta_ecs_ec2_instance_id": model.LabelValue("i-1234567890abcdef0"),
+ "__meta_ecs_ec2_instance_type": model.LabelValue("t3.medium"),
+ "__meta_ecs_ec2_instance_private_ip": model.LabelValue("10.0.1.50"),
+ "__meta_ecs_ec2_instance_public_ip": model.LabelValue("54.1.2.3"),
+ "__meta_ecs_public_ip": model.LabelValue("54.1.2.3"),
+ "__meta_ecs_tag_ec2_Name": model.LabelValue("ecs-host-1"),
+ "__meta_ecs_tag_ec2_Environment": model.LabelValue("production"),
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "MixedNetworkingModes",
+ ecsData: &ecsDataStore{
+ region: "us-west-2",
+ clusters: []ecsTypes.Cluster{
+ {
+ ClusterName: strptr("mixed-cluster"),
+ ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/mixed-cluster"),
+ Status: strptr("ACTIVE"),
+ },
+ },
+ services: []ecsTypes.Service{
+ {
+ ServiceName: strptr("mixed-service"),
+ ServiceArn: strptr("arn:aws:ecs:us-west-2:123456789012:service/mixed-cluster/mixed-service"),
+ ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/mixed-cluster"),
+ Status: strptr("ACTIVE"),
+ },
+ },
+ tasks: []ecsTypes.Task{
+ {
+ TaskArn: strptr("arn:aws:ecs:us-west-2:123456789012:task/mixed-cluster/task-awsvpc"),
+ ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/mixed-cluster"),
+ TaskDefinitionArn: strptr("arn:aws:ecs:us-west-2:123456789012:task-definition/awsvpc-task:1"),
+ Group: strptr("service:mixed-service"),
+ LaunchType: ecsTypes.LaunchTypeFargate,
+ LastStatus: strptr("RUNNING"),
+ DesiredStatus: strptr("RUNNING"),
+ HealthStatus: ecsTypes.HealthStatusHealthy,
+ AvailabilityZone: strptr("us-west-2a"),
+ Attachments: []ecsTypes.Attachment{
+ {
+ Type: strptr("ElasticNetworkInterface"),
+ Details: []ecsTypes.KeyValuePair{
+ {Name: strptr("subnetId"), Value: strptr("subnet-12345")},
+ {Name: strptr("privateIPv4Address"), Value: strptr("10.0.2.100")},
+ {Name: strptr("networkInterfaceId"), Value: strptr("eni-mixed-awsvpc")},
+ },
+ },
+ },
+ },
+ {
+ TaskArn: strptr("arn:aws:ecs:us-west-2:123456789012:task/mixed-cluster/task-bridge"),
+ ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/mixed-cluster"),
+ TaskDefinitionArn: strptr("arn:aws:ecs:us-west-2:123456789012:task-definition/bridge-task:1"),
+ Group: strptr("service:mixed-service"),
+ LaunchType: ecsTypes.LaunchTypeEc2,
+ LastStatus: strptr("RUNNING"),
+ DesiredStatus: strptr("RUNNING"),
+ HealthStatus: ecsTypes.HealthStatusHealthy,
+ AvailabilityZone: strptr("us-west-2b"),
+ ContainerInstanceArn: strptr("arn:aws:ecs:us-west-2:123456789012:container-instance/mixed-cluster/xyz789"),
+ Attachments: []ecsTypes.Attachment{},
+ },
+ },
+ containerInstances: []ecsTypes.ContainerInstance{
+ {
+ ContainerInstanceArn: strptr("arn:aws:ecs:us-west-2:123456789012:container-instance/mixed-cluster/xyz789"),
+ Ec2InstanceId: strptr("i-0987654321fedcba0"),
+ Status: strptr("ACTIVE"),
+ },
+ },
+ ec2Instances: map[string]ec2InstanceInfo{
+ "i-0987654321fedcba0": {
+ privateIP: "10.0.1.75",
+ publicIP: "54.2.3.4",
+ subnetID: "subnet-bridge-2",
+ instanceType: "t3.large",
+ tags: map[string]string{
+ "Name": "mixed-host",
+ "Team": "platform",
+ },
+ },
+ },
+ eniPublicIPs: map[string]string{
+ "eni-mixed-awsvpc": "52.2.3.4",
+ },
+ },
+ expected: []*targetgroup.Group{
+ {
+ Source: "us-west-2",
+ Targets: []model.LabelSet{
+ {
+ model.AddressLabel: model.LabelValue("10.0.2.100:80"),
+ "__meta_ecs_cluster": model.LabelValue("mixed-cluster"),
+ "__meta_ecs_cluster_arn": model.LabelValue("arn:aws:ecs:us-west-2:123456789012:cluster/mixed-cluster"),
+ "__meta_ecs_service": model.LabelValue("mixed-service"),
+ "__meta_ecs_service_arn": model.LabelValue("arn:aws:ecs:us-west-2:123456789012:service/mixed-cluster/mixed-service"),
+ "__meta_ecs_service_status": model.LabelValue("ACTIVE"),
+ "__meta_ecs_task_group": model.LabelValue("service:mixed-service"),
+ "__meta_ecs_task_arn": model.LabelValue("arn:aws:ecs:us-west-2:123456789012:task/mixed-cluster/task-awsvpc"),
+ "__meta_ecs_task_definition": model.LabelValue("arn:aws:ecs:us-west-2:123456789012:task-definition/awsvpc-task:1"),
+ "__meta_ecs_region": model.LabelValue("us-west-2"),
+ "__meta_ecs_availability_zone": model.LabelValue("us-west-2a"),
+ "__meta_ecs_ip_address": model.LabelValue("10.0.2.100"),
+ "__meta_ecs_subnet_id": model.LabelValue("subnet-12345"),
+ "__meta_ecs_launch_type": model.LabelValue("FARGATE"),
+ "__meta_ecs_desired_status": model.LabelValue("RUNNING"),
+ "__meta_ecs_last_status": model.LabelValue("RUNNING"),
+ "__meta_ecs_health_status": model.LabelValue("HEALTHY"),
+ "__meta_ecs_network_mode": model.LabelValue("awsvpc"),
+ "__meta_ecs_public_ip": model.LabelValue("52.2.3.4"),
+ },
+ {
+ model.AddressLabel: model.LabelValue("10.0.1.75:80"),
+ "__meta_ecs_cluster": model.LabelValue("mixed-cluster"),
+ "__meta_ecs_cluster_arn": model.LabelValue("arn:aws:ecs:us-west-2:123456789012:cluster/mixed-cluster"),
+ "__meta_ecs_service": model.LabelValue("mixed-service"),
+ "__meta_ecs_service_arn": model.LabelValue("arn:aws:ecs:us-west-2:123456789012:service/mixed-cluster/mixed-service"),
+ "__meta_ecs_service_status": model.LabelValue("ACTIVE"),
+ "__meta_ecs_task_group": model.LabelValue("service:mixed-service"),
+ "__meta_ecs_task_arn": model.LabelValue("arn:aws:ecs:us-west-2:123456789012:task/mixed-cluster/task-bridge"),
+ "__meta_ecs_task_definition": model.LabelValue("arn:aws:ecs:us-west-2:123456789012:task-definition/bridge-task:1"),
+ "__meta_ecs_region": model.LabelValue("us-west-2"),
+ "__meta_ecs_availability_zone": model.LabelValue("us-west-2b"),
+ "__meta_ecs_ip_address": model.LabelValue("10.0.1.75"),
+ "__meta_ecs_subnet_id": model.LabelValue("subnet-bridge-2"),
+ "__meta_ecs_launch_type": model.LabelValue("EC2"),
+ "__meta_ecs_desired_status": model.LabelValue("RUNNING"),
+ "__meta_ecs_last_status": model.LabelValue("RUNNING"),
+ "__meta_ecs_health_status": model.LabelValue("HEALTHY"),
+ "__meta_ecs_network_mode": model.LabelValue("bridge"),
+ "__meta_ecs_container_instance_arn": model.LabelValue("arn:aws:ecs:us-west-2:123456789012:container-instance/mixed-cluster/xyz789"),
+ "__meta_ecs_ec2_instance_id": model.LabelValue("i-0987654321fedcba0"),
+ "__meta_ecs_ec2_instance_type": model.LabelValue("t3.large"),
+ "__meta_ecs_ec2_instance_private_ip": model.LabelValue("10.0.1.75"),
+ "__meta_ecs_ec2_instance_public_ip": model.LabelValue("54.2.3.4"),
+ "__meta_ecs_public_ip": model.LabelValue("54.2.3.4"),
+ "__meta_ecs_tag_ec2_Name": model.LabelValue("mixed-host"),
+ "__meta_ecs_tag_ec2_Team": model.LabelValue("platform"),
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "EC2WithAwsvpcNetworking",
+ ecsData: &ecsDataStore{
+ region: "us-west-2",
+ clusters: []ecsTypes.Cluster{
+ {
+ ClusterName: strptr("ec2-awsvpc-cluster"),
+ ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/ec2-awsvpc-cluster"),
+ Status: strptr("ACTIVE"),
+ },
+ },
+ services: []ecsTypes.Service{
+ {
+ ServiceName: strptr("ec2-awsvpc-service"),
+ ServiceArn: strptr("arn:aws:ecs:us-west-2:123456789012:service/ec2-awsvpc-cluster/ec2-awsvpc-service"),
+ ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/ec2-awsvpc-cluster"),
+ Status: strptr("ACTIVE"),
+ },
+ },
+ tasks: []ecsTypes.Task{
+ {
+ TaskArn: strptr("arn:aws:ecs:us-west-2:123456789012:task/ec2-awsvpc-cluster/task-ec2-awsvpc"),
+ ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/ec2-awsvpc-cluster"),
+ TaskDefinitionArn: strptr("arn:aws:ecs:us-west-2:123456789012:task-definition/ec2-awsvpc-task:1"),
+ Group: strptr("service:ec2-awsvpc-service"),
+ LaunchType: ecsTypes.LaunchTypeEc2,
+ LastStatus: strptr("RUNNING"),
+ DesiredStatus: strptr("RUNNING"),
+ HealthStatus: ecsTypes.HealthStatusHealthy,
+ AvailabilityZone: strptr("us-west-2c"),
+ ContainerInstanceArn: strptr("arn:aws:ecs:us-west-2:123456789012:container-instance/ec2-awsvpc-cluster/def456"),
+ // Has BOTH ENI attachment AND container instance ARN - should use ENI
+ Attachments: []ecsTypes.Attachment{
+ {
+ Type: strptr("ElasticNetworkInterface"),
+ Details: []ecsTypes.KeyValuePair{
+ {Name: strptr("subnetId"), Value: strptr("subnet-99999")},
+ {Name: strptr("privateIPv4Address"), Value: strptr("10.0.3.200")},
+ {Name: strptr("networkInterfaceId"), Value: strptr("eni-ec2-awsvpc")},
+ },
+ },
+ },
+ },
+ },
+ eniPublicIPs: map[string]string{
+ "eni-ec2-awsvpc": "52.3.4.5",
+ },
+ // Container instance data - IP should NOT be used, but instance type SHOULD be used
+ containerInstances: []ecsTypes.ContainerInstance{
+ {
+ ContainerInstanceArn: strptr("arn:aws:ecs:us-west-2:123456789012:container-instance/ec2-awsvpc-cluster/def456"),
+ Ec2InstanceId: strptr("i-ec2awsvpcinstance"),
+ Status: strptr("ACTIVE"),
+ },
+ },
+ ec2Instances: map[string]ec2InstanceInfo{
+ "i-ec2awsvpcinstance": {
+ privateIP: "10.0.9.99", // This IP should NOT be used (ENI IP is used instead)
+ publicIP: "54.3.4.5", // This public IP SHOULD be exposed
+ subnetID: "subnet-wrong", // This subnet should NOT be used (ENI subnet is used instead)
+ instanceType: "c5.2xlarge", // This instance type SHOULD be used
+ tags: map[string]string{
+ "Name": "ec2-awsvpc-host",
+ "Owner": "team-a",
+ },
+ },
+ },
+ },
+ expected: []*targetgroup.Group{
+ {
+ Source: "us-west-2",
+ Targets: []model.LabelSet{
+ {
+ model.AddressLabel: model.LabelValue("10.0.3.200:80"),
+ "__meta_ecs_cluster": model.LabelValue("ec2-awsvpc-cluster"),
+ "__meta_ecs_cluster_arn": model.LabelValue("arn:aws:ecs:us-west-2:123456789012:cluster/ec2-awsvpc-cluster"),
+ "__meta_ecs_service": model.LabelValue("ec2-awsvpc-service"),
+ "__meta_ecs_service_arn": model.LabelValue("arn:aws:ecs:us-west-2:123456789012:service/ec2-awsvpc-cluster/ec2-awsvpc-service"),
+ "__meta_ecs_service_status": model.LabelValue("ACTIVE"),
+ "__meta_ecs_task_group": model.LabelValue("service:ec2-awsvpc-service"),
+ "__meta_ecs_task_arn": model.LabelValue("arn:aws:ecs:us-west-2:123456789012:task/ec2-awsvpc-cluster/task-ec2-awsvpc"),
+ "__meta_ecs_task_definition": model.LabelValue("arn:aws:ecs:us-west-2:123456789012:task-definition/ec2-awsvpc-task:1"),
+ "__meta_ecs_region": model.LabelValue("us-west-2"),
+ "__meta_ecs_availability_zone": model.LabelValue("us-west-2c"),
+ "__meta_ecs_ip_address": model.LabelValue("10.0.3.200"),
+ "__meta_ecs_subnet_id": model.LabelValue("subnet-99999"),
+ "__meta_ecs_launch_type": model.LabelValue("EC2"),
+ "__meta_ecs_desired_status": model.LabelValue("RUNNING"),
+ "__meta_ecs_last_status": model.LabelValue("RUNNING"),
+ "__meta_ecs_health_status": model.LabelValue("HEALTHY"),
+ "__meta_ecs_network_mode": model.LabelValue("awsvpc"),
+ "__meta_ecs_container_instance_arn": model.LabelValue("arn:aws:ecs:us-west-2:123456789012:container-instance/ec2-awsvpc-cluster/def456"),
+ "__meta_ecs_ec2_instance_id": model.LabelValue("i-ec2awsvpcinstance"),
+ "__meta_ecs_ec2_instance_type": model.LabelValue("c5.2xlarge"),
+ "__meta_ecs_ec2_instance_private_ip": model.LabelValue("10.0.9.99"),
+ "__meta_ecs_ec2_instance_public_ip": model.LabelValue("54.3.4.5"),
+ "__meta_ecs_public_ip": model.LabelValue("52.3.4.5"),
+ "__meta_ecs_tag_ec2_Name": model.LabelValue("ec2-awsvpc-host"),
+ "__meta_ecs_tag_ec2_Owner": model.LabelValue("team-a"),
+ },
+ },
+ },
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ ecsClient := newMockECSClient(tt.ecsData)
+ ec2Client := newMockECSEC2Client(tt.ecsData.ec2Instances, tt.ecsData.eniPublicIPs)
+
+ d := &ECSDiscovery{
+ ecs: ecsClient,
+ ec2: ec2Client,
+ cfg: &ECSSDConfig{
+ Region: tt.ecsData.region,
+ Port: 80,
+ RequestConcurrency: 1,
+ },
+ }
+
+ groups, err := d.refresh(ctx)
+ require.NoError(t, err)
+ if tt.name == "MixedNetworkingModes" {
+ // Use ElementsMatch for tests with multiple tasks as goroutines can affect order
+ require.Len(t, groups, len(tt.expected))
+ require.Equal(t, tt.expected[0].Source, groups[0].Source)
+ require.ElementsMatch(t, tt.expected[0].Targets, groups[0].Targets)
+ } else {
+ require.Equal(t, tt.expected, groups)
+ }
+ })
+ }
+}
+
+// ECS client mock.
+type mockECSClient struct {
+ ecsData ecsDataStore
+}
+
+func newMockECSClient(ecsData *ecsDataStore) *mockECSClient {
+ client := mockECSClient{
+ ecsData: *ecsData,
+ }
+ return &client
+}
+
+func (m *mockECSClient) ListClusters(_ context.Context, _ *ecs.ListClustersInput, _ ...func(*ecs.Options)) (*ecs.ListClustersOutput, error) {
+ clusterArns := make([]string, 0, len(m.ecsData.clusters))
+ for _, cluster := range m.ecsData.clusters {
+ clusterArns = append(clusterArns, *cluster.ClusterArn)
+ }
+
+ return &ecs.ListClustersOutput{
+ ClusterArns: clusterArns,
+ }, nil
+}
+
+func (m *mockECSClient) DescribeClusters(_ context.Context, input *ecs.DescribeClustersInput, _ ...func(*ecs.Options)) (*ecs.DescribeClustersOutput, error) {
+ var clusters []ecsTypes.Cluster
+ for _, clusterArn := range input.Clusters {
+ for _, cluster := range m.ecsData.clusters {
+ if *cluster.ClusterArn == clusterArn {
+ clusters = append(clusters, cluster)
+ break
+ }
+ }
+ }
+
+ return &ecs.DescribeClustersOutput{
+ Clusters: clusters,
+ }, nil
+}
+
+func (m *mockECSClient) ListServices(_ context.Context, input *ecs.ListServicesInput, _ ...func(*ecs.Options)) (*ecs.ListServicesOutput, error) {
+ var serviceArns []string
+ for _, service := range m.ecsData.services {
+ if *service.ClusterArn == *input.Cluster {
+ serviceArns = append(serviceArns, *service.ServiceArn)
+ }
+ }
+
+ return &ecs.ListServicesOutput{
+ ServiceArns: serviceArns,
+ }, nil
+}
+
+func (m *mockECSClient) DescribeServices(_ context.Context, input *ecs.DescribeServicesInput, _ ...func(*ecs.Options)) (*ecs.DescribeServicesOutput, error) {
+ var services []ecsTypes.Service
+ for _, serviceArn := range input.Services {
+ for _, service := range m.ecsData.services {
+ if *service.ServiceArn == serviceArn && *service.ClusterArn == *input.Cluster {
+ services = append(services, service)
+ break
+ }
+ }
+ }
+
+ return &ecs.DescribeServicesOutput{
+ Services: services,
+ }, nil
+}
+
+func (m *mockECSClient) ListTasks(_ context.Context, input *ecs.ListTasksInput, _ ...func(*ecs.Options)) (*ecs.ListTasksOutput, error) {
+ var taskArns []string
+ for _, task := range m.ecsData.tasks {
+ if *task.ClusterArn == *input.Cluster {
+ // If ServiceName is specified, filter by service
+ if input.ServiceName != nil {
+ expectedGroup := "service:" + *input.ServiceName
+ if task.Group != nil && *task.Group == expectedGroup {
+ taskArns = append(taskArns, *task.TaskArn)
+ }
+ } else {
+ taskArns = append(taskArns, *task.TaskArn)
+ }
+ }
+ }
+
+ return &ecs.ListTasksOutput{
+ TaskArns: taskArns,
+ }, nil
+}
+
+func (m *mockECSClient) DescribeTasks(_ context.Context, input *ecs.DescribeTasksInput, _ ...func(*ecs.Options)) (*ecs.DescribeTasksOutput, error) {
+ var tasks []ecsTypes.Task
+ for _, taskArn := range input.Tasks {
+ for _, task := range m.ecsData.tasks {
+ if *task.TaskArn == taskArn && *task.ClusterArn == *input.Cluster {
+ tasks = append(tasks, task)
+ break
+ }
+ }
+ }
+
+ return &ecs.DescribeTasksOutput{
+ Tasks: tasks,
+ }, nil
+}
+
+func (m *mockECSClient) DescribeContainerInstances(_ context.Context, input *ecs.DescribeContainerInstancesInput, _ ...func(*ecs.Options)) (*ecs.DescribeContainerInstancesOutput, error) {
+ var containerInstances []ecsTypes.ContainerInstance
+ for _, ciArn := range input.ContainerInstances {
+ for _, ci := range m.ecsData.containerInstances {
+ if *ci.ContainerInstanceArn == ciArn {
+ containerInstances = append(containerInstances, ci)
+ break
+ }
+ }
+ }
+
+ return &ecs.DescribeContainerInstancesOutput{
+ ContainerInstances: containerInstances,
+ }, nil
+}
+
+// Mock EC2 client wrapper for ECS tests.
+type mockECSEC2Client struct {
+ ec2Instances map[string]ec2InstanceInfo
+ eniPublicIPs map[string]string
+}
+
+func newMockECSEC2Client(ec2Instances map[string]ec2InstanceInfo, eniPublicIPs map[string]string) *mockECSEC2Client {
+ return &mockECSEC2Client{
+ ec2Instances: ec2Instances,
+ eniPublicIPs: eniPublicIPs,
+ }
+}
+
+func (m *mockECSEC2Client) DescribeInstances(_ context.Context, input *ec2.DescribeInstancesInput, _ ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error) {
+ var reservations []ec2Types.Reservation
+
+ for _, instanceID := range input.InstanceIds {
+ if info, ok := m.ec2Instances[instanceID]; ok {
+ instance := ec2Types.Instance{
+ InstanceId: &instanceID,
+ PrivateIpAddress: &info.privateIP,
+ }
+ if info.publicIP != "" {
+ instance.PublicIpAddress = &info.publicIP
+ }
+ if info.subnetID != "" {
+ instance.SubnetId = &info.subnetID
+ }
+ if info.instanceType != "" {
+ instance.InstanceType = ec2Types.InstanceType(info.instanceType)
+ }
+ // Add tags
+ for tagKey, tagValue := range info.tags {
+ instance.Tags = append(instance.Tags, ec2Types.Tag{
+ Key: &tagKey,
+ Value: &tagValue,
+ })
+ }
+ reservation := ec2Types.Reservation{
+ Instances: []ec2Types.Instance{instance},
+ }
+ reservations = append(reservations, reservation)
+ }
+ }
+
+ return &ec2.DescribeInstancesOutput{
+ Reservations: reservations,
+ }, nil
+}
+
+func (m *mockECSEC2Client) DescribeNetworkInterfaces(_ context.Context, input *ec2.DescribeNetworkInterfacesInput, _ ...func(*ec2.Options)) (*ec2.DescribeNetworkInterfacesOutput, error) {
+ var networkInterfaces []ec2Types.NetworkInterface
+
+ for _, eniID := range input.NetworkInterfaceIds {
+ if publicIP, ok := m.eniPublicIPs[eniID]; ok {
+ eni := ec2Types.NetworkInterface{
+ NetworkInterfaceId: &eniID,
+ }
+ if publicIP != "" {
+ eni.Association = &ec2Types.NetworkInterfaceAssociation{
+ PublicIp: &publicIP,
+ }
+ }
+ networkInterfaces = append(networkInterfaces, eni)
+ }
+ }
+
+ return &ec2.DescribeNetworkInterfacesOutput{
+ NetworkInterfaces: networkInterfaces,
+ }, nil
+}
+
+func TestIsStandaloneTask(t *testing.T) {
+ tests := []struct {
+ name string
+ task ecsTypes.Task
+ expected bool
+ }{
+ {
+ name: "StandaloneTask",
+ task: ecsTypes.Task{
+ Group: strptr("family:my-task-definition"),
+ },
+ expected: true,
+ },
+ {
+ name: "ServiceTask",
+ task: ecsTypes.Task{
+ Group: strptr("service:my-service"),
+ },
+ expected: false,
+ },
+ {
+ name: "ServiceTaskWithColon",
+ task: ecsTypes.Task{
+ Group: strptr("service:my:service:name"),
+ },
+ expected: false,
+ },
+ {
+ name: "NilGroup",
+ task: ecsTypes.Task{
+ Group: nil,
+ },
+ expected: false,
+ },
+ {
+ name: "EmptyGroup",
+ task: ecsTypes.Task{
+ Group: strptr(""),
+ },
+ expected: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result := isStandaloneTask(tt.task)
+ require.Equal(t, tt.expected, result)
+ })
+ }
+}
+
+func TestGetServiceNameFromTaskGroup(t *testing.T) {
+ tests := []struct {
+ name string
+ task ecsTypes.Task
+ expected string
+ }{
+ {
+ name: "SimpleServiceName",
+ task: ecsTypes.Task{
+ Group: strptr("service:my-service"),
+ },
+ expected: "my-service",
+ },
+ {
+ name: "ServiceNameWithHyphens",
+ task: ecsTypes.Task{
+ Group: strptr("service:web-api-service"),
+ },
+ expected: "web-api-service",
+ },
+ {
+ name: "ServiceNameWithColons",
+ task: ecsTypes.Task{
+ Group: strptr("service:my:service:name"),
+ },
+ expected: "my",
+ },
+ {
+ name: "FamilyGroup",
+ task: ecsTypes.Task{
+ Group: strptr("family:my-task-def"),
+ },
+ expected: "my-task-def",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result := getServiceNameFromTaskGroup(tt.task)
+ require.Equal(t, tt.expected, result)
+ })
+ }
+}
diff --git a/discovery/aws/elasticache.go b/discovery/aws/elasticache.go
new file mode 100644
index 0000000000..7ed598e294
--- /dev/null
+++ b/discovery/aws/elasticache.go
@@ -0,0 +1,907 @@
+// 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 aws
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "log/slog"
+ "maps"
+ "net"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/aws/aws-sdk-go-v2/aws"
+ awsConfig "github.com/aws/aws-sdk-go-v2/config"
+ "github.com/aws/aws-sdk-go-v2/credentials"
+ "github.com/aws/aws-sdk-go-v2/credentials/stscreds"
+ "github.com/aws/aws-sdk-go-v2/service/elasticache"
+ "github.com/aws/aws-sdk-go-v2/service/elasticache/types"
+ "github.com/aws/aws-sdk-go-v2/service/sts"
+ "github.com/prometheus/client_golang/prometheus"
+ "github.com/prometheus/common/config"
+ "github.com/prometheus/common/model"
+ "github.com/prometheus/common/promslog"
+ "golang.org/x/sync/errgroup"
+
+ "github.com/prometheus/prometheus/discovery"
+ "github.com/prometheus/prometheus/discovery/refresh"
+ "github.com/prometheus/prometheus/discovery/targetgroup"
+ "github.com/prometheus/prometheus/util/strutil"
+)
+
+const (
+ elasticacheLabel = model.MetaLabelPrefix + "elasticache_"
+ elasticacheLabelDeploymentOption = elasticacheLabel + "deployment_option"
+
+ // cache cluster.
+ elasticacheLabelCacheCluster = elasticacheLabel + "cache_cluster_"
+ elasticacheLabelCacheClusterARN = elasticacheLabelCacheCluster + "arn"
+ elasticacheLabelCacheClusterAtRestEncryptionEnabled = elasticacheLabelCacheCluster + "at_rest_encryption_enabled"
+ elasticacheLabelCacheClusterAuthTokenEnabled = elasticacheLabelCacheCluster + "auth_token_enabled"
+ elasticacheLabelCacheClusterAuthTokenLastModified = elasticacheLabelCacheCluster + "auth_token_last_modified"
+ elasticacheLabelCacheClusterAutoMinorVersionUpgrade = elasticacheLabelCacheCluster + "auto_minor_version_upgrade"
+ elasticacheLabelCacheClusterCreateTime = elasticacheLabelCacheCluster + "cache_cluster_create_time"
+ elasticacheLabelCacheClusterID = elasticacheLabelCacheCluster + "cache_cluster_id"
+ elasticacheLabelCacheClusterStatus = elasticacheLabelCacheCluster + "cache_cluster_status"
+ elasticacheLabelCacheClusterNodeType = elasticacheLabelCacheCluster + "cache_node_type"
+ elasticacheLabelCacheClusterParameterGroup = elasticacheLabelCacheCluster + "cache_parameter_group"
+ elasticacheLabelCacheClusterSubnetGroupName = elasticacheLabelCacheCluster + "cache_subnet_group_name"
+ elasticacheLabelCacheClusterClientDownloadLandingPage = elasticacheLabelCacheCluster + "client_download_landing_page"
+ elasticacheLabelCacheClusterEngine = elasticacheLabelCacheCluster + "engine"
+ elasticacheLabelCacheClusterEngineVersion = elasticacheLabelCacheCluster + "engine_version"
+ elasticacheLabelCacheClusterIPDiscovery = elasticacheLabelCacheCluster + "ip_discovery"
+ elasticacheLabelCacheClusterNetworkType = elasticacheLabelCacheCluster + "network_type"
+ elasticacheLabelCacheClusterNumCacheNodes = elasticacheLabelCacheCluster + "num_cache_nodes"
+ elasticacheLabelCacheClusterPreferredAvailabilityZone = elasticacheLabelCacheCluster + "preferred_availability_zone"
+ elasticacheLabelCacheClusterPreferredMaintenanceWindow = elasticacheLabelCacheCluster + "preferred_maintenance_window"
+ elasticacheLabelCacheClusterPreferredOutpostARN = elasticacheLabelCacheCluster + "preferred_outpost_arn"
+ elasticacheLabelCacheClusterReplicationGroupID = elasticacheLabelCacheCluster + "replication_group_id"
+ elasticacheLabelCacheClusterReplicationGroupLogDeliveryEnabled = elasticacheLabelCacheCluster + "replication_group_log_delivery_enabled"
+ elasticacheLabelCacheClusterSnapshotRetentionLimit = elasticacheLabelCacheCluster + "snapshot_retention_limit"
+ elasticacheLabelCacheClusterSnapshotWindow = elasticacheLabelCacheCluster + "snapshot_window"
+ elasticacheLabelCacheClusterTransitEncryptionEnabled = elasticacheLabelCacheCluster + "transit_encryption_enabled"
+ elasticacheLabelCacheClusterTransitEncryptionMode = elasticacheLabelCacheCluster + "transit_encryption_mode"
+
+ // configuration endpoint.
+ elasticacheLabelCacheClusterConfigurationEndpoint = elasticacheLabelCacheCluster + "configuration_endpoint_"
+ elasticacheLabelCacheClusterConfigurationEndpointAddress = elasticacheLabelCacheClusterConfigurationEndpoint + "address"
+ elasticacheLabelCacheClusterConfigurationEndpointPort = elasticacheLabelCacheClusterConfigurationEndpoint + "port"
+
+ // notification.
+ elasticacheLabelCacheClusterNotification = elasticacheLabelCacheCluster + "notification_"
+ elasticacheLabelCacheClusterNotificationTopicARN = elasticacheLabelCacheClusterNotification + "topic_arn"
+ elasticacheLabelCacheClusterNotificationTopicStatus = elasticacheLabelCacheClusterNotification + "topic_status"
+
+ // log delivery configuration (slice - use with index).
+ elasticacheLabelCacheClusterLogDeliveryConfiguration = elasticacheLabelCacheCluster + "log_delivery_configuration_"
+ elasticacheLabelCacheClusterLogDeliveryConfigurationDestinationType = elasticacheLabelCacheClusterLogDeliveryConfiguration + "destination_type"
+ elasticacheLabelCacheClusterLogDeliveryConfigurationLogFormat = elasticacheLabelCacheClusterLogDeliveryConfiguration + "log_format"
+ elasticacheLabelCacheClusterLogDeliveryConfigurationLogType = elasticacheLabelCacheClusterLogDeliveryConfiguration + "log_type"
+ elasticacheLabelCacheClusterLogDeliveryConfigurationStatus = elasticacheLabelCacheClusterLogDeliveryConfiguration + "status"
+ elasticacheLabelCacheClusterLogDeliveryConfigurationMessage = elasticacheLabelCacheClusterLogDeliveryConfiguration + "message"
+ elasticacheLabelCacheClusterLogDeliveryConfigurationLogGroup = elasticacheLabelCacheClusterLogDeliveryConfiguration + "log_group"
+ elasticacheLabelCacheClusterLogDeliveryConfigurationDeliveryStream = elasticacheLabelCacheClusterLogDeliveryConfiguration + "delivery_stream"
+
+ // pending modified values.
+ elasticacheLabelCacheClusterPendingModifiedValues = elasticacheLabelCacheCluster + "pending_modified_values_"
+ elasticacheLabelCacheClusterPendingModifiedValuesAuthTokenStatus = elasticacheLabelCacheClusterPendingModifiedValues + "auth_token_status"
+ elasticacheLabelCacheClusterPendingModifiedValuesCacheNodeType = elasticacheLabelCacheClusterPendingModifiedValues + "cache_node_type"
+ elasticacheLabelCacheClusterPendingModifiedValuesEngineVersion = elasticacheLabelCacheClusterPendingModifiedValues + "engine_version"
+ elasticacheLabelCacheClusterPendingModifiedValuesNumCacheNodes = elasticacheLabelCacheClusterPendingModifiedValues + "num_cache_nodes"
+ elasticacheLabelCacheClusterPendingModifiedValuesTransitEncryptionEnabled = elasticacheLabelCacheClusterPendingModifiedValues + "transit_encryption_enabled"
+ elasticacheLabelCacheClusterPendingModifiedValuesTransitEncryptionMode = elasticacheLabelCacheClusterPendingModifiedValues + "transit_encryption_mode"
+ elasticacheLabelCacheClusterPendingModifiedValuesCacheNodeIDsToRemove = elasticacheLabelCacheClusterPendingModifiedValues + "cache_node_ids_to_remove"
+
+ // security group membership (slice - use with index).
+ elasticacheLabelCacheClusterSecurityGroupMembership = elasticacheLabelCacheCluster + "security_group_membership_"
+ elasticacheLabelCacheClusterSecurityGroupMembershipID = elasticacheLabelCacheClusterSecurityGroupMembership + "id"
+ elasticacheLabelCacheClusterSecurityGroupMembershipStatus = elasticacheLabelCacheClusterSecurityGroupMembership + "status"
+
+ // tags - create one label per tag key, with the format: elasticache_cache_cluster_tag_.
+ elasticacheLabelCacheClusterTag = elasticacheLabelCacheCluster + "tag_"
+
+ // node.
+ elasticacheLabelCacheClusterNode = elasticacheLabelCacheCluster + "node_"
+ elasticacheLabelCacheClusterNodeCreateTime = elasticacheLabelCacheClusterNode + "create_time"
+ elasticacheLabelCacheClusterNodeID = elasticacheLabelCacheClusterNode + "id"
+ elasticacheLabelCacheClusterNodeStatus = elasticacheLabelCacheClusterNode + "status"
+ elasticacheLabelCacheClusterNodeAZ = elasticacheLabelCacheClusterNode + "availability_zone"
+ elasticacheLabelCacheClusterNodeCustomerOutpostARN = elasticacheLabelCacheClusterNode + "customer_outpost_arn"
+ elasticacheLabelCacheClusterNodeSourceCacheNodeID = elasticacheLabelCacheClusterNode + "source_cache_node_id"
+ elasticacheLabelCacheClusterNodeParameterGroupStatus = elasticacheLabelCacheClusterNode + "parameter_group_status"
+
+ // endpoint.
+ elasticacheLabelCacheClusterNodeEndpoint = elasticacheLabelCacheClusterNode + "endpoint_"
+ elasticacheLabelCacheClusterNodeEndpointAddress = elasticacheLabelCacheClusterNodeEndpoint + "address"
+ elasticacheLabelCacheClusterNodeEndpointPort = elasticacheLabelCacheClusterNodeEndpoint + "port"
+
+ // serverless cache.
+ elasticacheLabelServerlessCache = elasticacheLabel + "serverless_cache_"
+ elasticacheLabelServerlessCacheARN = elasticacheLabelServerlessCache + "arn"
+ elasticacheLabelServerlessCacheName = elasticacheLabelServerlessCache + "name"
+ elasticacheLabelServerlessCacheCreateTime = elasticacheLabelServerlessCache + "create_time"
+ elasticacheLabelServerlessCacheDescription = elasticacheLabelServerlessCache + "description"
+ elasticacheLabelServerlessCacheEngine = elasticacheLabelServerlessCache + "engine"
+ elasticacheLabelServerlessCacheFullEngineVersion = elasticacheLabelServerlessCache + "full_engine_version"
+ elasticacheLabelServerlessCacheMajorEngineVersion = elasticacheLabelServerlessCache + "major_engine_version"
+ elasticacheLabelServerlessCacheStatus = elasticacheLabelServerlessCache + "status"
+ elasticacheLabelServerlessCacheKmsKeyID = elasticacheLabelServerlessCache + "kms_key_id"
+ elasticacheLabelServerlessCacheUserGroupID = elasticacheLabelServerlessCache + "user_group_id"
+ elasticacheLabelServerlessCacheDailySnapshotTime = elasticacheLabelServerlessCache + "daily_snapshot_time"
+ elasticacheLabelServerlessCacheSnapshotRetentionLimit = elasticacheLabelServerlessCache + "snapshot_retention_limit"
+
+ // endpoint.
+ elasticacheLabelServerlessCacheEndpoint = elasticacheLabelServerlessCache + "endpoint_"
+ elasticacheLabelServerlessCacheEndpointAddress = elasticacheLabelServerlessCacheEndpoint + "address"
+ elasticacheLabelServerlessCacheEndpointPort = elasticacheLabelServerlessCacheEndpoint + "port"
+ elasticacheLabelServerlessCacheReaderEndpointAddress = elasticacheLabelServerlessCacheEndpoint + "reader_address"
+ elasticacheLabelServerlessCacheReaderEndpointPort = elasticacheLabelServerlessCacheEndpoint + "reader_port"
+
+ // security group membership (slice - use with index).
+ elasticacheLabelServerlessCacheSecurityGroupID = elasticacheLabelServerlessCache + "security_group_id"
+
+ // Subnet group membership (slice - use with index).
+ elasticacheLabelServerlessCacheSubnetID = elasticacheLabelServerlessCache + "subnet_id"
+
+ // cache usage limits.
+ elasticacheLabelServerlessCacheCacheUsageLimit = elasticacheLabelServerlessCache + "cache_usage_limit_"
+ elasticacheLabelServerlessCacheCacheUsageLimitCacheDataStorage = elasticacheLabelServerlessCacheCacheUsageLimit + "data_storage"
+ elasticacheLabelServerlessCacheCacheUsageLimitCacheDataStorageMaximum = elasticacheLabelServerlessCacheCacheUsageLimitCacheDataStorage + "maximum"
+ elasticacheLabelServerlessCacheCacheUsageLimitCacheDataStorageMinimum = elasticacheLabelServerlessCacheCacheUsageLimitCacheDataStorage + "minimum"
+ elasticacheLabelServerlessCacheCacheUsageLimitCacheDataStorageUnit = elasticacheLabelServerlessCacheCacheUsageLimitCacheDataStorage + "unit"
+ elasticacheLabelServerlessCacheCacheUsageLimitECPUPerSecond = elasticacheLabelServerlessCacheCacheUsageLimit + "ecpu_per_second"
+ elasticacheLabelServerlessCacheCacheUsageLimitECPUPerSecondMaximum = elasticacheLabelServerlessCacheCacheUsageLimitECPUPerSecond + "maximum"
+ elasticacheLabelServerlessCacheCacheUsageLimitECPUPerSecondMinimum = elasticacheLabelServerlessCacheCacheUsageLimitECPUPerSecond + "minimum"
+
+ // tags - create one label per tag key, with the format: elasticache_serverless_cache_tag_.
+ elasticacheLabelServerlessCacheTag = elasticacheLabelServerlessCache + "tag_"
+)
+
+// DefaultElasticacheSDConfig is the default Elasticache SD configuration.
+var DefaultElasticacheSDConfig = ElasticacheSDConfig{
+ Port: 80,
+ RefreshInterval: model.Duration(60 * time.Second),
+ RequestConcurrency: 10,
+ HTTPClientConfig: config.DefaultHTTPClientConfig,
+}
+
+func init() {
+ discovery.RegisterConfig(&ElasticacheSDConfig{})
+}
+
+// ElasticacheSDConfig is the configuration for Elasticache based service discovery.
+type ElasticacheSDConfig struct {
+ Region string `yaml:"region"`
+ Endpoint string `yaml:"endpoint"`
+ AccessKey string `yaml:"access_key,omitempty"`
+ SecretKey config.Secret `yaml:"secret_key,omitempty"`
+ Profile string `yaml:"profile,omitempty"`
+ RoleARN string `yaml:"role_arn,omitempty"`
+ Clusters []string `yaml:"clusters,omitempty"`
+ Port int `yaml:"port"`
+ RefreshInterval model.Duration `yaml:"refresh_interval,omitempty"`
+
+ // RequestConcurrency controls the maximum number of concurrent Elasticache API requests.
+ RequestConcurrency int `yaml:"request_concurrency,omitempty"`
+
+ HTTPClientConfig config.HTTPClientConfig `yaml:",inline"`
+}
+
+// NewDiscovererMetrics implements discovery.Config.
+func (*ElasticacheSDConfig) NewDiscovererMetrics(_ prometheus.Registerer, rmi discovery.RefreshMetricsInstantiator) discovery.DiscovererMetrics {
+ return &elasticacheMetrics{
+ refreshMetrics: rmi,
+ }
+}
+
+// Name returns the name of the Elasticache Config.
+func (*ElasticacheSDConfig) Name() string { return "elasticache" }
+
+// NewDiscoverer returns a Discoverer for the Elasticache Config.
+func (c *ElasticacheSDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) {
+ return NewElasticacheDiscovery(c, opts)
+}
+
+// UnmarshalYAML implements the yaml.Unmarshaler interface for the Elasticache Config.
+func (c *ElasticacheSDConfig) UnmarshalYAML(unmarshal func(any) error) error {
+ *c = DefaultElasticacheSDConfig
+ type plain ElasticacheSDConfig
+ err := unmarshal((*plain)(c))
+ if err != nil {
+ return err
+ }
+
+ c.Region, err = loadRegion(context.Background(), c.Region)
+ if err != nil {
+ return fmt.Errorf("could not determine AWS region: %w", err)
+ }
+
+ return c.HTTPClientConfig.Validate()
+}
+
+type elasticacheClient interface {
+ DescribeServerlessCaches(ctx context.Context, params *elasticache.DescribeServerlessCachesInput, optFns ...func(*elasticache.Options)) (*elasticache.DescribeServerlessCachesOutput, error)
+ DescribeCacheClusters(ctx context.Context, params *elasticache.DescribeCacheClustersInput, optFns ...func(*elasticache.Options)) (*elasticache.DescribeCacheClustersOutput, error)
+ ListTagsForResource(ctx context.Context, params *elasticache.ListTagsForResourceInput, optFns ...func(*elasticache.Options)) (*elasticache.ListTagsForResourceOutput, error)
+}
+
+// ElasticacheDiscovery periodically performs Elasticache-SD requests.
+// It implements the Discoverer interface.
+type ElasticacheDiscovery struct {
+ *refresh.Discovery
+ logger *slog.Logger
+ cfg *ElasticacheSDConfig
+ elasticacheClient elasticacheClient
+}
+
+// NewElasticacheDiscovery returns a new ElasticacheDiscovery which periodically refreshes its targets.
+func NewElasticacheDiscovery(conf *ElasticacheSDConfig, opts discovery.DiscovererOptions) (*ElasticacheDiscovery, error) {
+ m, ok := opts.Metrics.(*elasticacheMetrics)
+ if !ok {
+ return nil, errors.New("invalid discovery metrics type")
+ }
+
+ if opts.Logger == nil {
+ opts.Logger = promslog.NewNopLogger()
+ }
+ d := &ElasticacheDiscovery{
+ logger: opts.Logger,
+ cfg: conf,
+ }
+ d.Discovery = refresh.NewDiscovery(
+ refresh.Options{
+ Logger: opts.Logger,
+ Mech: "elasticache",
+ Interval: time.Duration(d.cfg.RefreshInterval),
+ RefreshF: d.refresh,
+ MetricsInstantiator: m.refreshMetrics,
+ },
+ )
+ return d, nil
+}
+
+func (d *ElasticacheDiscovery) initElasticacheClient(ctx context.Context) error {
+ if d.elasticacheClient != nil {
+ return nil
+ }
+
+ if d.cfg.Region == "" {
+ return errors.New("region must be set for Elasticache service discovery")
+ }
+
+ // Build the HTTP client from the provided HTTPClientConfig.
+ client, err := config.NewClientFromConfig(d.cfg.HTTPClientConfig, "elasticache_sd")
+ if err != nil {
+ return err
+ }
+
+ // Build the AWS config with the provided region.
+ var configOptions []func(*awsConfig.LoadOptions) error
+ configOptions = append(configOptions, awsConfig.WithRegion(d.cfg.Region))
+ configOptions = append(configOptions, awsConfig.WithHTTPClient(client))
+
+ // Only set static credentials if both access key and secret key are provided
+ // Otherwise, let AWS SDK use its default credential chain
+ if d.cfg.AccessKey != "" && d.cfg.SecretKey != "" {
+ credProvider := credentials.NewStaticCredentialsProvider(d.cfg.AccessKey, string(d.cfg.SecretKey), "")
+ configOptions = append(configOptions, awsConfig.WithCredentialsProvider(credProvider))
+ }
+
+ if d.cfg.Profile != "" {
+ configOptions = append(configOptions, awsConfig.WithSharedConfigProfile(d.cfg.Profile))
+ }
+
+ cfg, err := awsConfig.LoadDefaultConfig(ctx, configOptions...)
+ if err != nil {
+ d.logger.Error("Failed to create AWS config", "error", err)
+ return fmt.Errorf("could not create aws config: %w", err)
+ }
+
+ // If the role ARN is set, assume the role to get credentials and set the credentials provider in the config.
+ if d.cfg.RoleARN != "" {
+ assumeProvider := stscreds.NewAssumeRoleProvider(sts.NewFromConfig(cfg), d.cfg.RoleARN)
+ cfg.Credentials = aws.NewCredentialsCache(assumeProvider)
+ }
+
+ d.elasticacheClient = elasticache.NewFromConfig(cfg, func(options *elasticache.Options) {
+ if d.cfg.Endpoint != "" {
+ options.BaseEndpoint = &d.cfg.Endpoint
+ }
+ options.HTTPClient = client
+ })
+
+ // Test credentials by making a simple API call
+ testCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
+ defer cancel()
+
+ _, err = d.elasticacheClient.DescribeCacheClusters(testCtx, &elasticache.DescribeCacheClustersInput{})
+ if err != nil {
+ d.logger.Error("Failed to test Elasticache credentials", "error", err)
+ return fmt.Errorf("elasticache credential test failed: %w", err)
+ }
+
+ return nil
+}
+
+// describeServerlessCaches calls DescribeServerlessCaches API for the given cache IDs (or all caches if no IDs are provided) and returns the list of serverless caches.
+func (d *ElasticacheDiscovery) describeServerlessCaches(ctx context.Context, caches []string) ([]types.ServerlessCache, error) {
+ mu := &sync.Mutex{}
+ errg, ectx := errgroup.WithContext(ctx)
+ errg.SetLimit(d.cfg.RequestConcurrency)
+ var serverlessCaches []types.ServerlessCache
+ if len(caches) == 0 {
+ errg.Go(func() error {
+ var nextToken *string
+ for {
+ output, err := d.elasticacheClient.DescribeServerlessCaches(ectx, &elasticache.DescribeServerlessCachesInput{
+ MaxResults: aws.Int32(50),
+ NextToken: nextToken,
+ })
+ if err != nil {
+ return fmt.Errorf("failed to describe serverless caches: %w", err)
+ }
+ mu.Lock()
+ serverlessCaches = append(serverlessCaches, output.ServerlessCaches...)
+ mu.Unlock()
+ if output.NextToken == nil {
+ break
+ }
+ nextToken = output.NextToken
+ }
+ return nil
+ })
+ } else {
+ for _, cacheID := range caches {
+ errg.Go(func() error {
+ output, err := d.elasticacheClient.DescribeServerlessCaches(ectx, &elasticache.DescribeServerlessCachesInput{
+ MaxResults: aws.Int32(50),
+ NextToken: nil,
+ ServerlessCacheName: aws.String(cacheID),
+ })
+ if err != nil {
+ return fmt.Errorf("failed to describe serverless cache %s: %w", cacheID, err)
+ }
+ mu.Lock()
+ serverlessCaches = append(serverlessCaches, output.ServerlessCaches...)
+ mu.Unlock()
+ return nil
+ })
+ }
+ }
+
+ return serverlessCaches, errg.Wait()
+}
+
+// describeCacheClusters calls DescribeCacheClusters API for the given cache cluster IDs (or all cache clusters if no IDs are provided) and returns the list of cache clusters.
+func (d *ElasticacheDiscovery) describeCacheClusters(ctx context.Context, caches []string) ([]types.CacheCluster, error) {
+ mu := &sync.Mutex{}
+ errg, ectx := errgroup.WithContext(ctx)
+ errg.SetLimit(d.cfg.RequestConcurrency)
+ showCacheClustersNotInReplicationGroupsBools := []bool{false, true}
+ var cacheClusters []types.CacheCluster
+ if len(caches) == 0 {
+ for _, showCacheClustersNotInReplicationGroupsBool := range showCacheClustersNotInReplicationGroupsBools {
+ errg.Go(func() error {
+ var nextToken *string
+ for {
+ output, err := d.elasticacheClient.DescribeCacheClusters(ectx, &elasticache.DescribeCacheClustersInput{
+ MaxRecords: aws.Int32(100),
+ Marker: nextToken,
+ ShowCacheNodeInfo: aws.Bool(true),
+ ShowCacheClustersNotInReplicationGroups: aws.Bool(showCacheClustersNotInReplicationGroupsBool),
+ })
+ if err != nil {
+ return fmt.Errorf("failed to describe cache clusters: %w", err)
+ }
+ mu.Lock()
+ cacheClusters = append(cacheClusters, output.CacheClusters...)
+ mu.Unlock()
+ if output.Marker == nil {
+ break
+ }
+ nextToken = output.Marker
+ }
+ return nil
+ })
+ }
+ } else {
+ for _, cacheID := range caches {
+ for _, showCacheClustersNotInReplicationGroupsBool := range showCacheClustersNotInReplicationGroupsBools {
+ errg.Go(func() error {
+ output, err := d.elasticacheClient.DescribeCacheClusters(ectx, &elasticache.DescribeCacheClustersInput{
+ MaxRecords: aws.Int32(100),
+ Marker: nil,
+ ShowCacheNodeInfo: aws.Bool(true),
+ ShowCacheClustersNotInReplicationGroups: aws.Bool(showCacheClustersNotInReplicationGroupsBool),
+ CacheClusterId: aws.String(cacheID),
+ })
+ if err != nil {
+ return fmt.Errorf("failed to describe cache cluster %s: %w", cacheID, err)
+ }
+ mu.Lock()
+ cacheClusters = append(cacheClusters, output.CacheClusters...)
+ mu.Unlock()
+ return nil
+ })
+ }
+ }
+ }
+
+ return cacheClusters, errg.Wait()
+}
+
+// listTagsForResource calls ListTagsForResource API for the given resource ARNs and returns a map of resource ARN to list of tags.
+func (d *ElasticacheDiscovery) listTagsForResource(ctx context.Context, resourceARNs []string) (map[string][]types.Tag, error) {
+ mu := &sync.Mutex{}
+ errg, ectx := errgroup.WithContext(ctx)
+ errg.SetLimit(d.cfg.RequestConcurrency)
+ tagsByResourceARN := make(map[string][]types.Tag)
+ for _, resourceARN := range resourceARNs {
+ errg.Go(func() error {
+ output, err := d.elasticacheClient.ListTagsForResource(ectx, &elasticache.ListTagsForResourceInput{
+ ResourceName: aws.String(resourceARN),
+ })
+ if err != nil {
+ return fmt.Errorf("failed to list tags for resource %s: %w", resourceARN, err)
+ }
+ mu.Lock()
+ tagsByResourceARN[resourceARN] = output.TagList
+ mu.Unlock()
+ return nil
+ })
+ }
+ return tagsByResourceARN, errg.Wait()
+}
+
+func (d *ElasticacheDiscovery) refresh(ctx context.Context) ([]*targetgroup.Group, error) {
+ err := d.initElasticacheClient(ctx)
+ if err != nil {
+ return nil, err
+ }
+
+ var clusters []string
+ clustersMu := sync.Mutex{}
+ serverlessCacheIDs, cacheClusterIDs := splitCacheDeploymentOptions(d.cfg.Clusters)
+
+ clusterErrg, clusterCtx := errgroup.WithContext(ctx)
+ clusterErrg.Go(func() error {
+ caches, err := d.describeServerlessCaches(clusterCtx, serverlessCacheIDs)
+ if err != nil {
+ return fmt.Errorf("failed to describe serverless caches: %w", err)
+ }
+ for _, cache := range caches {
+ clustersMu.Lock()
+ clusters = append(clusters, *cache.ARN)
+ clustersMu.Unlock()
+ }
+ return nil
+ })
+
+ clusterErrg.Go(func() error {
+ cacheClusters, err := d.describeCacheClusters(clusterCtx, cacheClusterIDs)
+ if err != nil {
+ return fmt.Errorf("failed to describe cache clusters: %w", err)
+ }
+ for _, cluster := range cacheClusters {
+ clustersMu.Lock()
+ clusters = append(clusters, *cluster.ARN)
+ clustersMu.Unlock()
+ }
+ return nil
+ })
+
+ if err := clusterErrg.Wait(); err != nil {
+ return nil, err
+ }
+
+ tagsByResourceARN, err := d.listTagsForResource(ctx, clusters)
+ if err != nil {
+ return nil, fmt.Errorf("failed to list tags for resources: %w", err)
+ }
+
+ tg := &targetgroup.Group{
+ Source: d.cfg.Region,
+ }
+
+ errg, ectx := errgroup.WithContext(ctx)
+ errg.Go(func() error {
+ caches, err := d.describeServerlessCaches(ectx, serverlessCacheIDs)
+ if err != nil {
+ return fmt.Errorf("failed to describe serverless caches: %w", err)
+ }
+ for _, cache := range caches {
+ addServerlessCacheTargets(tg, &cache, tagsByResourceARN[*cache.ARN])
+ }
+ return nil
+ })
+
+ errg.Go(func() error {
+ cacheClusters, err := d.describeCacheClusters(ectx, cacheClusterIDs)
+ if err != nil {
+ return fmt.Errorf("failed to describe cache clusters: %w", err)
+ }
+ for _, cluster := range cacheClusters {
+ addCacheClusterTargets(tg, &cluster, tagsByResourceARN[*cluster.ARN])
+ }
+ return nil
+ })
+
+ if err := errg.Wait(); err != nil {
+ return nil, err
+ }
+
+ return []*targetgroup.Group{tg}, nil
+}
+
+// splitCacheTypes takes a list of cache ARNs and splits them into serverless cache IDs and cache cluster IDs based on their format.
+// Serverless caches are in the format arn:aws:elasticache:::serverlesscache:
+// Cache clusters are in the format arn:aws:elasticache:::replicationgroup:.
+func splitCacheDeploymentOptions(caches []string) (serverlessCacheIDs, cacheClusterIDs []string) {
+ for _, cacheARN := range caches {
+ if len(cacheARN) == 0 {
+ continue
+ }
+ parts := strings.Split(cacheARN, ":")
+ if len(parts) < 6 {
+ continue
+ }
+ resourceType := parts[5]
+ resourceID := parts[6]
+ switch resourceType {
+ case "serverlesscache":
+ serverlessCacheIDs = append(serverlessCacheIDs, resourceID)
+ case "replicationgroup":
+ cacheClusterIDs = append(cacheClusterIDs, resourceID)
+ default:
+ continue
+ }
+ }
+ return serverlessCacheIDs, cacheClusterIDs
+}
+
+// addServerlessCacheTargets adds targets for a serverless cache to the target group.
+func addServerlessCacheTargets(tg *targetgroup.Group, cache *types.ServerlessCache, tags []types.Tag) {
+ labels := model.LabelSet{
+ elasticacheLabelDeploymentOption: model.LabelValue("serverless"),
+ elasticacheLabelServerlessCacheARN: model.LabelValue(*cache.ARN),
+ elasticacheLabelServerlessCacheName: model.LabelValue(*cache.ServerlessCacheName),
+ elasticacheLabelServerlessCacheStatus: model.LabelValue(*cache.Status),
+ elasticacheLabelServerlessCacheEngine: model.LabelValue(*cache.Engine),
+ elasticacheLabelServerlessCacheFullEngineVersion: model.LabelValue(*cache.FullEngineVersion),
+ elasticacheLabelServerlessCacheMajorEngineVersion: model.LabelValue(*cache.MajorEngineVersion),
+ }
+
+ if cache.Description != nil {
+ labels[elasticacheLabelServerlessCacheDescription] = model.LabelValue(*cache.Description)
+ }
+
+ if cache.CreateTime != nil {
+ labels[elasticacheLabelServerlessCacheCreateTime] = model.LabelValue(cache.CreateTime.Format(time.RFC3339))
+ }
+
+ if cache.KmsKeyId != nil {
+ labels[elasticacheLabelServerlessCacheKmsKeyID] = model.LabelValue(*cache.KmsKeyId)
+ }
+
+ if cache.UserGroupId != nil {
+ labels[elasticacheLabelServerlessCacheUserGroupID] = model.LabelValue(*cache.UserGroupId)
+ }
+
+ if cache.DailySnapshotTime != nil {
+ labels[elasticacheLabelServerlessCacheDailySnapshotTime] = model.LabelValue(*cache.DailySnapshotTime)
+ }
+
+ if cache.SnapshotRetentionLimit != nil {
+ labels[elasticacheLabelServerlessCacheSnapshotRetentionLimit] = model.LabelValue(strconv.Itoa(int(*cache.SnapshotRetentionLimit)))
+ }
+
+ if cache.Endpoint != nil {
+ if cache.Endpoint.Address != nil {
+ labels[elasticacheLabelServerlessCacheEndpointAddress] = model.LabelValue(*cache.Endpoint.Address)
+ }
+ if cache.Endpoint.Port != nil {
+ labels[elasticacheLabelServerlessCacheEndpointPort] = model.LabelValue(strconv.Itoa(int(*cache.Endpoint.Port)))
+ }
+ }
+
+ if cache.ReaderEndpoint != nil {
+ if cache.ReaderEndpoint.Address != nil {
+ labels[elasticacheLabelServerlessCacheReaderEndpointAddress] = model.LabelValue(*cache.ReaderEndpoint.Address)
+ }
+ if cache.ReaderEndpoint.Port != nil {
+ labels[elasticacheLabelServerlessCacheReaderEndpointPort] = model.LabelValue(strconv.Itoa(int(*cache.ReaderEndpoint.Port)))
+ }
+ }
+
+ for i, sgID := range cache.SecurityGroupIds {
+ labels[model.LabelName(fmt.Sprintf("%s_%d", elasticacheLabelServerlessCacheSecurityGroupID, i))] = model.LabelValue(sgID)
+ }
+
+ for i, subnetID := range cache.SubnetIds {
+ labels[model.LabelName(fmt.Sprintf("%s_%d", elasticacheLabelServerlessCacheSubnetID, i))] = model.LabelValue(subnetID)
+ }
+
+ if cache.CacheUsageLimits != nil {
+ if cache.CacheUsageLimits.DataStorage != nil {
+ if cache.CacheUsageLimits.DataStorage.Maximum != nil {
+ labels[elasticacheLabelServerlessCacheCacheUsageLimitCacheDataStorageMaximum] = model.LabelValue(strconv.Itoa(int(*cache.CacheUsageLimits.DataStorage.Maximum)))
+ }
+ if cache.CacheUsageLimits.DataStorage.Minimum != nil {
+ labels[elasticacheLabelServerlessCacheCacheUsageLimitCacheDataStorageMinimum] = model.LabelValue(strconv.Itoa(int(*cache.CacheUsageLimits.DataStorage.Minimum)))
+ }
+ labels[elasticacheLabelServerlessCacheCacheUsageLimitCacheDataStorageUnit] = model.LabelValue(cache.CacheUsageLimits.DataStorage.Unit)
+ }
+ if cache.CacheUsageLimits.ECPUPerSecond != nil {
+ if cache.CacheUsageLimits.ECPUPerSecond.Maximum != nil {
+ labels[elasticacheLabelServerlessCacheCacheUsageLimitECPUPerSecondMaximum] = model.LabelValue(strconv.Itoa(int(*cache.CacheUsageLimits.ECPUPerSecond.Maximum)))
+ }
+ if cache.CacheUsageLimits.ECPUPerSecond.Minimum != nil {
+ labels[elasticacheLabelServerlessCacheCacheUsageLimitECPUPerSecondMinimum] = model.LabelValue(strconv.Itoa(int(*cache.CacheUsageLimits.ECPUPerSecond.Minimum)))
+ }
+ }
+ }
+
+ for _, tag := range tags {
+ if tag.Key != nil && tag.Value != nil {
+ labels[model.LabelName(elasticacheLabelServerlessCacheTag+strutil.SanitizeLabelName(*tag.Key))] = model.LabelValue(*tag.Value)
+ }
+ }
+
+ // Set the address label using the endpoint
+ if cache.Endpoint != nil && cache.Endpoint.Address != nil && cache.Endpoint.Port != nil {
+ labels[model.AddressLabel] = model.LabelValue(net.JoinHostPort(*cache.Endpoint.Address, strconv.Itoa(int(*cache.Endpoint.Port))))
+ }
+
+ tg.Targets = append(tg.Targets, labels)
+}
+
+// addCacheClusterTargets adds targets for a cache cluster to the target group.
+// Creates one target per cache node for individual scraping.
+func addCacheClusterTargets(tg *targetgroup.Group, cluster *types.CacheCluster, tags []types.Tag) {
+ // Build common labels that apply to all nodes in this cluster
+ commonLabels := model.LabelSet{
+ elasticacheLabelDeploymentOption: model.LabelValue("node"),
+ elasticacheLabelCacheClusterARN: model.LabelValue(*cluster.ARN),
+ elasticacheLabelCacheClusterID: model.LabelValue(*cluster.CacheClusterId),
+ elasticacheLabelCacheClusterStatus: model.LabelValue(*cluster.CacheClusterStatus),
+ }
+
+ if cluster.AtRestEncryptionEnabled != nil {
+ commonLabels[elasticacheLabelCacheClusterAtRestEncryptionEnabled] = model.LabelValue(strconv.FormatBool(*cluster.AtRestEncryptionEnabled))
+ }
+
+ if cluster.AuthTokenEnabled != nil {
+ commonLabels[elasticacheLabelCacheClusterAuthTokenEnabled] = model.LabelValue(strconv.FormatBool(*cluster.AuthTokenEnabled))
+ }
+
+ if cluster.AuthTokenLastModifiedDate != nil {
+ commonLabels[elasticacheLabelCacheClusterAuthTokenLastModified] = model.LabelValue(cluster.AuthTokenLastModifiedDate.Format(time.RFC3339))
+ }
+
+ if cluster.AutoMinorVersionUpgrade != nil {
+ commonLabels[elasticacheLabelCacheClusterAutoMinorVersionUpgrade] = model.LabelValue(strconv.FormatBool(*cluster.AutoMinorVersionUpgrade))
+ }
+
+ if cluster.CacheClusterCreateTime != nil {
+ commonLabels[elasticacheLabelCacheClusterCreateTime] = model.LabelValue(cluster.CacheClusterCreateTime.Format(time.RFC3339))
+ }
+
+ if cluster.CacheNodeType != nil {
+ commonLabels[elasticacheLabelCacheClusterNodeType] = model.LabelValue(*cluster.CacheNodeType)
+ }
+
+ if cluster.CacheParameterGroup != nil && cluster.CacheParameterGroup.CacheParameterGroupName != nil {
+ commonLabels[elasticacheLabelCacheClusterParameterGroup] = model.LabelValue(*cluster.CacheParameterGroup.CacheParameterGroupName)
+ }
+
+ if cluster.CacheSubnetGroupName != nil {
+ commonLabels[elasticacheLabelCacheClusterSubnetGroupName] = model.LabelValue(*cluster.CacheSubnetGroupName)
+ }
+
+ if cluster.ClientDownloadLandingPage != nil {
+ commonLabels[elasticacheLabelCacheClusterClientDownloadLandingPage] = model.LabelValue(*cluster.ClientDownloadLandingPage)
+ }
+
+ if cluster.ConfigurationEndpoint != nil {
+ if cluster.ConfigurationEndpoint.Address != nil {
+ commonLabels[elasticacheLabelCacheClusterConfigurationEndpointAddress] = model.LabelValue(*cluster.ConfigurationEndpoint.Address)
+ }
+ if cluster.ConfigurationEndpoint.Port != nil {
+ commonLabels[elasticacheLabelCacheClusterConfigurationEndpointPort] = model.LabelValue(strconv.Itoa(int(*cluster.ConfigurationEndpoint.Port)))
+ }
+ }
+
+ if cluster.Engine != nil {
+ commonLabels[elasticacheLabelCacheClusterEngine] = model.LabelValue(*cluster.Engine)
+ }
+
+ if cluster.EngineVersion != nil {
+ commonLabels[elasticacheLabelCacheClusterEngineVersion] = model.LabelValue(*cluster.EngineVersion)
+ }
+
+ if len(cluster.IpDiscovery) > 0 {
+ commonLabels[elasticacheLabelCacheClusterIPDiscovery] = model.LabelValue(cluster.IpDiscovery)
+ }
+
+ if len(cluster.NetworkType) > 0 {
+ commonLabels[elasticacheLabelCacheClusterNetworkType] = model.LabelValue(cluster.NetworkType)
+ }
+
+ if cluster.NotificationConfiguration != nil {
+ if cluster.NotificationConfiguration.TopicArn != nil {
+ commonLabels[elasticacheLabelCacheClusterNotificationTopicARN] = model.LabelValue(*cluster.NotificationConfiguration.TopicArn)
+ }
+ if cluster.NotificationConfiguration.TopicStatus != nil {
+ commonLabels[elasticacheLabelCacheClusterNotificationTopicStatus] = model.LabelValue(*cluster.NotificationConfiguration.TopicStatus)
+ }
+ }
+
+ if cluster.NumCacheNodes != nil {
+ commonLabels[elasticacheLabelCacheClusterNumCacheNodes] = model.LabelValue(strconv.Itoa(int(*cluster.NumCacheNodes)))
+ }
+
+ if cluster.PreferredAvailabilityZone != nil {
+ commonLabels[elasticacheLabelCacheClusterPreferredAvailabilityZone] = model.LabelValue(*cluster.PreferredAvailabilityZone)
+ }
+
+ if cluster.PreferredMaintenanceWindow != nil {
+ commonLabels[elasticacheLabelCacheClusterPreferredMaintenanceWindow] = model.LabelValue(*cluster.PreferredMaintenanceWindow)
+ }
+
+ if cluster.PreferredOutpostArn != nil {
+ commonLabels[elasticacheLabelCacheClusterPreferredOutpostARN] = model.LabelValue(*cluster.PreferredOutpostArn)
+ }
+
+ if cluster.ReplicationGroupId != nil {
+ commonLabels[elasticacheLabelCacheClusterReplicationGroupID] = model.LabelValue(*cluster.ReplicationGroupId)
+ }
+
+ if cluster.ReplicationGroupLogDeliveryEnabled != nil {
+ commonLabels[elasticacheLabelCacheClusterReplicationGroupLogDeliveryEnabled] = model.LabelValue(strconv.FormatBool(*cluster.ReplicationGroupLogDeliveryEnabled))
+ }
+
+ if cluster.SnapshotRetentionLimit != nil {
+ commonLabels[elasticacheLabelCacheClusterSnapshotRetentionLimit] = model.LabelValue(strconv.Itoa(int(*cluster.SnapshotRetentionLimit)))
+ }
+
+ if cluster.SnapshotWindow != nil {
+ commonLabels[elasticacheLabelCacheClusterSnapshotWindow] = model.LabelValue(*cluster.SnapshotWindow)
+ }
+
+ if cluster.TransitEncryptionEnabled != nil {
+ commonLabels[elasticacheLabelCacheClusterTransitEncryptionEnabled] = model.LabelValue(strconv.FormatBool(*cluster.TransitEncryptionEnabled))
+ }
+
+ if len(cluster.TransitEncryptionMode) > 0 {
+ commonLabels[elasticacheLabelCacheClusterTransitEncryptionMode] = model.LabelValue(cluster.TransitEncryptionMode)
+ }
+
+ // Log delivery configurations (slice)
+ for i, logDelivery := range cluster.LogDeliveryConfigurations {
+ if len(logDelivery.DestinationType) > 0 {
+ commonLabels[model.LabelName(fmt.Sprintf("%s_%d", elasticacheLabelCacheClusterLogDeliveryConfigurationDestinationType, i))] = model.LabelValue(logDelivery.DestinationType)
+ }
+ if len(logDelivery.LogFormat) > 0 {
+ commonLabels[model.LabelName(fmt.Sprintf("%s_%d", elasticacheLabelCacheClusterLogDeliveryConfigurationLogFormat, i))] = model.LabelValue(logDelivery.LogFormat)
+ }
+ if len(logDelivery.LogType) > 0 {
+ commonLabels[model.LabelName(fmt.Sprintf("%s_%d", elasticacheLabelCacheClusterLogDeliveryConfigurationLogType, i))] = model.LabelValue(logDelivery.LogType)
+ }
+ if len(logDelivery.Status) > 0 {
+ commonLabels[model.LabelName(fmt.Sprintf("%s_%d", elasticacheLabelCacheClusterLogDeliveryConfigurationStatus, i))] = model.LabelValue(logDelivery.Status)
+ }
+ if logDelivery.Message != nil {
+ commonLabels[model.LabelName(fmt.Sprintf("%s_%d", elasticacheLabelCacheClusterLogDeliveryConfigurationMessage, i))] = model.LabelValue(*logDelivery.Message)
+ }
+ if logDelivery.DestinationDetails != nil {
+ if logDelivery.DestinationDetails.CloudWatchLogsDetails != nil && logDelivery.DestinationDetails.CloudWatchLogsDetails.LogGroup != nil {
+ commonLabels[model.LabelName(fmt.Sprintf("%s_%d", elasticacheLabelCacheClusterLogDeliveryConfigurationLogGroup, i))] = model.LabelValue(*logDelivery.DestinationDetails.CloudWatchLogsDetails.LogGroup)
+ }
+ if logDelivery.DestinationDetails.KinesisFirehoseDetails != nil && logDelivery.DestinationDetails.KinesisFirehoseDetails.DeliveryStream != nil {
+ commonLabels[model.LabelName(fmt.Sprintf("%s_%d", elasticacheLabelCacheClusterLogDeliveryConfigurationDeliveryStream, i))] = model.LabelValue(*logDelivery.DestinationDetails.KinesisFirehoseDetails.DeliveryStream)
+ }
+ }
+ }
+
+ // Pending modified values
+ if cluster.PendingModifiedValues != nil {
+ if len(cluster.PendingModifiedValues.AuthTokenStatus) > 0 {
+ commonLabels[elasticacheLabelCacheClusterPendingModifiedValuesAuthTokenStatus] = model.LabelValue(cluster.PendingModifiedValues.AuthTokenStatus)
+ }
+ if cluster.PendingModifiedValues.CacheNodeType != nil {
+ commonLabels[elasticacheLabelCacheClusterPendingModifiedValuesCacheNodeType] = model.LabelValue(*cluster.PendingModifiedValues.CacheNodeType)
+ }
+ if cluster.PendingModifiedValues.EngineVersion != nil {
+ commonLabels[elasticacheLabelCacheClusterPendingModifiedValuesEngineVersion] = model.LabelValue(*cluster.PendingModifiedValues.EngineVersion)
+ }
+ if cluster.PendingModifiedValues.NumCacheNodes != nil {
+ commonLabels[elasticacheLabelCacheClusterPendingModifiedValuesNumCacheNodes] = model.LabelValue(strconv.Itoa(int(*cluster.PendingModifiedValues.NumCacheNodes)))
+ }
+ if cluster.PendingModifiedValues.TransitEncryptionEnabled != nil {
+ commonLabels[elasticacheLabelCacheClusterPendingModifiedValuesTransitEncryptionEnabled] = model.LabelValue(strconv.FormatBool(*cluster.PendingModifiedValues.TransitEncryptionEnabled))
+ }
+ if len(cluster.PendingModifiedValues.TransitEncryptionMode) > 0 {
+ commonLabels[elasticacheLabelCacheClusterPendingModifiedValuesTransitEncryptionMode] = model.LabelValue(cluster.PendingModifiedValues.TransitEncryptionMode)
+ }
+ if len(cluster.PendingModifiedValues.CacheNodeIdsToRemove) > 0 {
+ commonLabels[elasticacheLabelCacheClusterPendingModifiedValuesCacheNodeIDsToRemove] = model.LabelValue(strings.Join(cluster.PendingModifiedValues.CacheNodeIdsToRemove, ","))
+ }
+ }
+
+ // Security group membership (slice)
+ for i, sg := range cluster.SecurityGroups {
+ if sg.SecurityGroupId != nil {
+ commonLabels[model.LabelName(fmt.Sprintf("%s_%d", elasticacheLabelCacheClusterSecurityGroupMembershipID, i))] = model.LabelValue(*sg.SecurityGroupId)
+ }
+ if sg.Status != nil {
+ commonLabels[model.LabelName(fmt.Sprintf("%s_%d", elasticacheLabelCacheClusterSecurityGroupMembershipStatus, i))] = model.LabelValue(*sg.Status)
+ }
+ }
+
+ // Tags
+ for _, tag := range tags {
+ if tag.Key != nil && tag.Value != nil {
+ commonLabels[model.LabelName(elasticacheLabelCacheClusterTag+strutil.SanitizeLabelName(*tag.Key))] = model.LabelValue(*tag.Value)
+ }
+ }
+
+ // Create one target per cache node
+ for _, node := range cluster.CacheNodes {
+ // Clone common labels for this node
+ labels := make(model.LabelSet, len(commonLabels))
+ maps.Copy(labels, commonLabels)
+
+ // Add node-specific labels
+ if node.CacheNodeId != nil {
+ labels[elasticacheLabelCacheClusterNodeID] = model.LabelValue(*node.CacheNodeId)
+ }
+ if node.CacheNodeStatus != nil {
+ labels[elasticacheLabelCacheClusterNodeStatus] = model.LabelValue(*node.CacheNodeStatus)
+ }
+ if node.CacheNodeCreateTime != nil {
+ labels[elasticacheLabelCacheClusterNodeCreateTime] = model.LabelValue(node.CacheNodeCreateTime.Format(time.RFC3339))
+ }
+ if node.CustomerAvailabilityZone != nil {
+ labels[elasticacheLabelCacheClusterNodeAZ] = model.LabelValue(*node.CustomerAvailabilityZone)
+ }
+ if node.CustomerOutpostArn != nil {
+ labels[elasticacheLabelCacheClusterNodeCustomerOutpostARN] = model.LabelValue(*node.CustomerOutpostArn)
+ }
+ if node.SourceCacheNodeId != nil {
+ labels[elasticacheLabelCacheClusterNodeSourceCacheNodeID] = model.LabelValue(*node.SourceCacheNodeId)
+ }
+ if node.ParameterGroupStatus != nil {
+ labels[elasticacheLabelCacheClusterNodeParameterGroupStatus] = model.LabelValue(*node.ParameterGroupStatus)
+ }
+ if node.Endpoint != nil {
+ if node.Endpoint.Address != nil {
+ labels[elasticacheLabelCacheClusterNodeEndpointAddress] = model.LabelValue(*node.Endpoint.Address)
+ }
+ if node.Endpoint.Port != nil {
+ labels[elasticacheLabelCacheClusterNodeEndpointPort] = model.LabelValue(strconv.Itoa(int(*node.Endpoint.Port)))
+ }
+
+ // Set the address label to this node's endpoint
+ if node.Endpoint.Address != nil && node.Endpoint.Port != nil {
+ labels[model.AddressLabel] = model.LabelValue(net.JoinHostPort(*node.Endpoint.Address, strconv.Itoa(int(*node.Endpoint.Port))))
+ }
+ }
+
+ tg.Targets = append(tg.Targets, labels)
+ }
+}
diff --git a/discovery/aws/elasticache_test.go b/discovery/aws/elasticache_test.go
new file mode 100644
index 0000000000..4611f33059
--- /dev/null
+++ b/discovery/aws/elasticache_test.go
@@ -0,0 +1,615 @@
+// 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 aws
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ "github.com/aws/aws-sdk-go-v2/aws"
+ "github.com/aws/aws-sdk-go-v2/service/elasticache"
+ "github.com/aws/aws-sdk-go-v2/service/elasticache/types"
+ "github.com/prometheus/common/model"
+ "github.com/stretchr/testify/require"
+
+ "github.com/prometheus/prometheus/discovery/targetgroup"
+)
+
+// Struct for test data.
+type elasticacheDataStore struct {
+ region string
+ serverlessCaches []types.ServerlessCache
+ cacheClusters []types.CacheCluster
+ tags map[string][]types.Tag // keyed by cache ARN
+}
+
+func TestElasticacheDiscoveryDescribeServerlessCaches(t *testing.T) {
+ ctx := context.Background()
+
+ for _, tt := range []struct {
+ name string
+ ecData *elasticacheDataStore
+ cacheNames []string
+ expectedCount int
+ }{
+ {
+ name: "MultipleCaches",
+ ecData: &elasticacheDataStore{
+ region: "us-west-2",
+ serverlessCaches: []types.ServerlessCache{
+ {
+ ServerlessCacheName: strptr("test-cache"),
+ ARN: strptr("arn:aws:elasticache:us-west-2:123456789012:serverlesscache:test-cache"),
+ Status: strptr("available"),
+ Engine: strptr("redis"),
+ FullEngineVersion: strptr("7.1"),
+ CreateTime: aws.Time(time.Now()),
+ Endpoint: &types.Endpoint{
+ Address: strptr("test-cache.serverless.use1.cache.amazonaws.com"),
+ Port: aws.Int32(6379),
+ },
+ },
+ {
+ ServerlessCacheName: strptr("prod-cache"),
+ ARN: strptr("arn:aws:elasticache:us-west-2:123456789012:serverlesscache:prod-cache"),
+ Status: strptr("available"),
+ Engine: strptr("valkey"),
+ FullEngineVersion: strptr("7.2"),
+ CreateTime: aws.Time(time.Now()),
+ Endpoint: &types.Endpoint{
+ Address: strptr("prod-cache.serverless.use1.cache.amazonaws.com"),
+ Port: aws.Int32(6379),
+ },
+ },
+ },
+ },
+ cacheNames: []string{},
+ expectedCount: 2,
+ },
+ {
+ name: "SingleCache",
+ ecData: &elasticacheDataStore{
+ region: "us-east-1",
+ serverlessCaches: []types.ServerlessCache{
+ {
+ ServerlessCacheName: strptr("single-cache"),
+ ARN: strptr("arn:aws:elasticache:us-east-1:123456789012:serverlesscache:single-cache"),
+ Status: strptr("available"),
+ Engine: strptr("redis"),
+ FullEngineVersion: strptr("7.1"),
+ CreateTime: aws.Time(time.Now()),
+ },
+ },
+ },
+ cacheNames: []string{"single-cache"},
+ expectedCount: 1,
+ },
+ {
+ name: "NoCaches",
+ ecData: &elasticacheDataStore{
+ region: "us-east-1",
+ serverlessCaches: []types.ServerlessCache{},
+ },
+ cacheNames: []string{},
+ expectedCount: 0,
+ },
+ } {
+ t.Run(tt.name, func(t *testing.T) {
+ client := newMockElasticacheClient(tt.ecData)
+
+ d := &ElasticacheDiscovery{
+ elasticacheClient: client,
+ cfg: &ElasticacheSDConfig{
+ Region: tt.ecData.region,
+ RequestConcurrency: 10,
+ },
+ }
+
+ caches, err := d.describeServerlessCaches(ctx, tt.cacheNames)
+ require.NoError(t, err)
+ require.Len(t, caches, tt.expectedCount)
+ })
+ }
+}
+
+func TestElasticacheDiscoveryDescribeCacheClusters(t *testing.T) {
+ ctx := context.Background()
+
+ for _, tt := range []struct {
+ name string
+ ecData *elasticacheDataStore
+ clusterIDs []string
+ expectedCount int
+ skipTest bool
+ }{
+ {
+ name: "MockValidation",
+ ecData: &elasticacheDataStore{
+ region: "us-west-2",
+ cacheClusters: []types.CacheCluster{
+ {
+ CacheClusterId: strptr("test-cluster-001"),
+ ARN: strptr("arn:aws:elasticache:us-west-2:123456789012:cluster:test-cluster-001"),
+ CacheClusterStatus: strptr("available"),
+ Engine: strptr("redis"),
+ EngineVersion: strptr("7.1"),
+ CacheNodeType: strptr("cache.t3.micro"),
+ NumCacheNodes: aws.Int32(1),
+ ConfigurationEndpoint: &types.Endpoint{
+ Address: strptr("test-cluster.abc123.cfg.use1.cache.amazonaws.com"),
+ Port: aws.Int32(6379),
+ },
+ },
+ },
+ },
+ clusterIDs: []string{},
+ expectedCount: 1,
+ skipTest: false,
+ },
+ {
+ name: "NoClusters",
+ ecData: &elasticacheDataStore{
+ region: "us-east-1",
+ cacheClusters: []types.CacheCluster{},
+ },
+ clusterIDs: []string{},
+ expectedCount: 0,
+ skipTest: false,
+ },
+ } {
+ t.Run(tt.name, func(t *testing.T) {
+ if tt.skipTest {
+ t.Skip("Skipping complex test with concurrency")
+ }
+ client := newMockElasticacheClient(tt.ecData)
+
+ // Verify mock returns expected data
+ output, err := client.DescribeCacheClusters(ctx, &elasticache.DescribeCacheClustersInput{})
+ require.NoError(t, err)
+ require.Len(t, output.CacheClusters, tt.expectedCount)
+ })
+ }
+}
+
+func TestAddServerlessCacheTargets(t *testing.T) {
+ testTime := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
+
+ tests := []struct {
+ name string
+ cache *types.ServerlessCache
+ tags []types.Tag
+ expectedLabels model.LabelSet
+ }{
+ {
+ name: "ServerlessCacheWithEndpoint",
+ cache: &types.ServerlessCache{
+ ServerlessCacheName: strptr("my-cache"),
+ ARN: strptr("arn:aws:elasticache:us-east-1:123456789012:serverlesscache:my-cache"),
+ Status: strptr("available"),
+ Engine: strptr("redis"),
+ FullEngineVersion: strptr("7.1"),
+ MajorEngineVersion: strptr("7"),
+ CreateTime: aws.Time(testTime),
+ Endpoint: &types.Endpoint{
+ Address: strptr("my-cache.serverless.use1.cache.amazonaws.com"),
+ Port: aws.Int32(6379),
+ },
+ ReaderEndpoint: &types.Endpoint{
+ Address: strptr("my-cache-ro.serverless.use1.cache.amazonaws.com"),
+ Port: aws.Int32(6379),
+ },
+ SecurityGroupIds: []string{"sg-12345"},
+ SubnetIds: []string{"subnet-abcdef"},
+ CacheUsageLimits: &types.CacheUsageLimits{
+ DataStorage: &types.DataStorage{
+ Maximum: aws.Int32(10),
+ Minimum: aws.Int32(1),
+ Unit: types.DataStorageUnitGb,
+ },
+ ECPUPerSecond: &types.ECPUPerSecond{
+ Maximum: aws.Int32(5000),
+ Minimum: aws.Int32(1000),
+ },
+ },
+ },
+ tags: []types.Tag{
+ {Key: strptr("Environment"), Value: strptr("test")},
+ },
+ expectedLabels: model.LabelSet{
+ "__meta_elasticache_deployment_option": "serverless",
+ "__meta_elasticache_serverless_cache_arn": "arn:aws:elasticache:us-east-1:123456789012:serverlesscache:my-cache",
+ "__meta_elasticache_serverless_cache_name": "my-cache",
+ "__meta_elasticache_serverless_cache_status": "available",
+ "__meta_elasticache_serverless_cache_engine": "redis",
+ "__meta_elasticache_serverless_cache_full_engine_version": "7.1",
+ "__meta_elasticache_serverless_cache_major_engine_version": "7",
+ "__meta_elasticache_serverless_cache_create_time": "2024-01-01T00:00:00Z",
+ "__meta_elasticache_serverless_cache_endpoint_address": "my-cache.serverless.use1.cache.amazonaws.com",
+ "__meta_elasticache_serverless_cache_endpoint_port": "6379",
+
+ "__meta_elasticache_serverless_cache_security_group_id_0": "sg-12345",
+ "__meta_elasticache_serverless_cache_subnet_id_0": "subnet-abcdef",
+
+ "__address__": "my-cache.serverless.use1.cache.amazonaws.com:6379",
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ tg := &targetgroup.Group{
+ Source: "test",
+ }
+
+ addServerlessCacheTargets(tg, tt.cache, tt.tags)
+
+ require.Len(t, tg.Targets, 1)
+ labels := tg.Targets[0]
+
+ // Check that all expected labels are present with correct values
+ for k, v := range tt.expectedLabels {
+ actualValue, exists := labels[k]
+ require.True(t, exists, "label %s should exist", k)
+ require.Equal(t, v, actualValue, "label %s mismatch", k)
+ }
+ })
+ }
+}
+
+func TestAddCacheClusterTargets(t *testing.T) {
+ testTime := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
+
+ tests := []struct {
+ name string
+ cluster *types.CacheCluster
+ tags []types.Tag
+ expectedTargetCount int
+ expectedLabels []model.LabelSet // One per node
+ }{
+ {
+ name: "CacheClusterWithMultipleNodes",
+ cluster: &types.CacheCluster{
+ CacheClusterId: strptr("my-cluster-001"),
+ ARN: strptr("arn:aws:elasticache:us-east-1:123456789012:cluster:my-cluster-001"),
+ CacheClusterStatus: strptr("available"),
+ Engine: strptr("redis"),
+ EngineVersion: strptr("7.1"),
+ CacheNodeType: strptr("cache.t3.micro"),
+ NumCacheNodes: aws.Int32(2),
+ CacheClusterCreateTime: aws.Time(testTime),
+ ConfigurationEndpoint: &types.Endpoint{
+ Address: strptr("my-cluster.abc123.cfg.use1.cache.amazonaws.com"),
+ Port: aws.Int32(6379),
+ },
+ AtRestEncryptionEnabled: aws.Bool(true),
+ TransitEncryptionEnabled: aws.Bool(true),
+ AuthTokenEnabled: aws.Bool(true),
+ AutoMinorVersionUpgrade: aws.Bool(true),
+ CacheSubnetGroupName: strptr("my-subnet-group"),
+ PreferredAvailabilityZone: strptr("us-east-1a"),
+ SecurityGroups: []types.SecurityGroupMembership{
+ {
+ SecurityGroupId: strptr("sg-12345"),
+ Status: strptr("active"),
+ },
+ },
+ CacheNodes: []types.CacheNode{
+ {
+ CacheNodeId: strptr("0001"),
+ CacheNodeStatus: strptr("available"),
+ CacheNodeCreateTime: aws.Time(testTime),
+ CustomerAvailabilityZone: strptr("us-east-1a"),
+ Endpoint: &types.Endpoint{
+ Address: strptr("my-cluster-001.abc123.0001.use1.cache.amazonaws.com"),
+ Port: aws.Int32(6379),
+ },
+ },
+ {
+ CacheNodeId: strptr("0002"),
+ CacheNodeStatus: strptr("available"),
+ CacheNodeCreateTime: aws.Time(testTime),
+ CustomerAvailabilityZone: strptr("us-east-1b"),
+ Endpoint: &types.Endpoint{
+ Address: strptr("my-cluster-001.abc123.0002.use1.cache.amazonaws.com"),
+ Port: aws.Int32(6379),
+ },
+ },
+ },
+ },
+ tags: []types.Tag{
+ {Key: strptr("Environment"), Value: strptr("production")},
+ {Key: strptr("Application"), Value: strptr("web-app")},
+ },
+ expectedTargetCount: 2,
+ expectedLabels: []model.LabelSet{
+ {
+ "__meta_elasticache_deployment_option": "node",
+ "__meta_elasticache_cache_cluster_arn": "arn:aws:elasticache:us-east-1:123456789012:cluster:my-cluster-001",
+ "__meta_elasticache_cache_cluster_cache_cluster_id": "my-cluster-001",
+ "__meta_elasticache_cache_cluster_cache_cluster_status": "available",
+ "__meta_elasticache_cache_cluster_engine": "redis",
+ "__meta_elasticache_cache_cluster_engine_version": "7.1",
+ "__meta_elasticache_cache_cluster_cache_node_type": "cache.t3.micro",
+ "__meta_elasticache_cache_cluster_num_cache_nodes": "2",
+ "__meta_elasticache_cache_cluster_cache_cluster_create_time": "2024-01-01T00:00:00Z",
+ "__meta_elasticache_cache_cluster_configuration_endpoint_address": "my-cluster.abc123.cfg.use1.cache.amazonaws.com",
+ "__meta_elasticache_cache_cluster_configuration_endpoint_port": "6379",
+ "__meta_elasticache_cache_cluster_at_rest_encryption_enabled": "true",
+ "__meta_elasticache_cache_cluster_transit_encryption_enabled": "true",
+ "__meta_elasticache_cache_cluster_auth_token_enabled": "true",
+ "__meta_elasticache_cache_cluster_auto_minor_version_upgrade": "true",
+ "__meta_elasticache_cache_cluster_cache_subnet_group_name": "my-subnet-group",
+ "__meta_elasticache_cache_cluster_preferred_availability_zone": "us-east-1a",
+ "__meta_elasticache_cache_cluster_security_group_membership_id_0": "sg-12345",
+ "__meta_elasticache_cache_cluster_security_group_membership_status_0": "active",
+ "__meta_elasticache_cache_cluster_tag_Environment": "production",
+ "__meta_elasticache_cache_cluster_tag_Application": "web-app",
+ "__meta_elasticache_cache_cluster_node_id": "0001",
+ "__meta_elasticache_cache_cluster_node_status": "available",
+ "__meta_elasticache_cache_cluster_node_create_time": "2024-01-01T00:00:00Z",
+ "__meta_elasticache_cache_cluster_node_availability_zone": "us-east-1a",
+ "__meta_elasticache_cache_cluster_node_endpoint_address": "my-cluster-001.abc123.0001.use1.cache.amazonaws.com",
+ "__meta_elasticache_cache_cluster_node_endpoint_port": "6379",
+ "__address__": "my-cluster-001.abc123.0001.use1.cache.amazonaws.com:6379",
+ },
+ {
+ "__meta_elasticache_deployment_option": "node",
+ "__meta_elasticache_cache_cluster_arn": "arn:aws:elasticache:us-east-1:123456789012:cluster:my-cluster-001",
+ "__meta_elasticache_cache_cluster_cache_cluster_id": "my-cluster-001",
+ "__meta_elasticache_cache_cluster_cache_cluster_status": "available",
+ "__meta_elasticache_cache_cluster_engine": "redis",
+ "__meta_elasticache_cache_cluster_engine_version": "7.1",
+ "__meta_elasticache_cache_cluster_cache_node_type": "cache.t3.micro",
+ "__meta_elasticache_cache_cluster_num_cache_nodes": "2",
+ "__meta_elasticache_cache_cluster_cache_cluster_create_time": "2024-01-01T00:00:00Z",
+ "__meta_elasticache_cache_cluster_configuration_endpoint_address": "my-cluster.abc123.cfg.use1.cache.amazonaws.com",
+ "__meta_elasticache_cache_cluster_configuration_endpoint_port": "6379",
+ "__meta_elasticache_cache_cluster_at_rest_encryption_enabled": "true",
+ "__meta_elasticache_cache_cluster_transit_encryption_enabled": "true",
+ "__meta_elasticache_cache_cluster_auth_token_enabled": "true",
+ "__meta_elasticache_cache_cluster_auto_minor_version_upgrade": "true",
+ "__meta_elasticache_cache_cluster_cache_subnet_group_name": "my-subnet-group",
+ "__meta_elasticache_cache_cluster_preferred_availability_zone": "us-east-1a",
+ "__meta_elasticache_cache_cluster_security_group_membership_id_0": "sg-12345",
+ "__meta_elasticache_cache_cluster_security_group_membership_status_0": "active",
+ "__meta_elasticache_cache_cluster_tag_Environment": "production",
+ "__meta_elasticache_cache_cluster_tag_Application": "web-app",
+ "__meta_elasticache_cache_cluster_node_id": "0002",
+ "__meta_elasticache_cache_cluster_node_status": "available",
+ "__meta_elasticache_cache_cluster_node_create_time": "2024-01-01T00:00:00Z",
+ "__meta_elasticache_cache_cluster_node_availability_zone": "us-east-1b",
+ "__meta_elasticache_cache_cluster_node_endpoint_address": "my-cluster-001.abc123.0002.use1.cache.amazonaws.com",
+ "__meta_elasticache_cache_cluster_node_endpoint_port": "6379",
+ "__address__": "my-cluster-001.abc123.0002.use1.cache.amazonaws.com:6379",
+ },
+ },
+ },
+ {
+ name: "CacheClusterWithSingleNode",
+ cluster: &types.CacheCluster{
+ CacheClusterId: strptr("node-cluster-001"),
+ ARN: strptr("arn:aws:elasticache:us-east-1:123456789012:cluster:node-cluster-001"),
+ CacheClusterStatus: strptr("available"),
+ Engine: strptr("redis"),
+ EngineVersion: strptr("6.2"),
+ CacheNodeType: strptr("cache.r6g.large"),
+ NumCacheNodes: aws.Int32(1),
+ CacheNodes: []types.CacheNode{
+ {
+ CacheNodeId: strptr("0001"),
+ CacheNodeStatus: strptr("available"),
+ CacheNodeCreateTime: aws.Time(testTime),
+ CustomerAvailabilityZone: strptr("us-east-1a"),
+ Endpoint: &types.Endpoint{
+ Address: strptr("node-cluster-001.abc123.0001.use1.cache.amazonaws.com"),
+ Port: aws.Int32(6379),
+ },
+ },
+ },
+ },
+ tags: []types.Tag{},
+ expectedTargetCount: 1,
+ expectedLabels: []model.LabelSet{
+ {
+ "__meta_elasticache_deployment_option": "node",
+ "__meta_elasticache_cache_cluster_arn": "arn:aws:elasticache:us-east-1:123456789012:cluster:node-cluster-001",
+ "__meta_elasticache_cache_cluster_cache_cluster_id": "node-cluster-001",
+ "__meta_elasticache_cache_cluster_cache_cluster_status": "available",
+ "__meta_elasticache_cache_cluster_engine": "redis",
+ "__meta_elasticache_cache_cluster_engine_version": "6.2",
+ "__meta_elasticache_cache_cluster_cache_node_type": "cache.r6g.large",
+ "__meta_elasticache_cache_cluster_num_cache_nodes": "1",
+ "__meta_elasticache_cache_cluster_node_id": "0001",
+ "__meta_elasticache_cache_cluster_node_status": "available",
+ "__meta_elasticache_cache_cluster_node_create_time": "2024-01-01T00:00:00Z",
+ "__meta_elasticache_cache_cluster_node_availability_zone": "us-east-1a",
+ "__meta_elasticache_cache_cluster_node_endpoint_address": "node-cluster-001.abc123.0001.use1.cache.amazonaws.com",
+ "__meta_elasticache_cache_cluster_node_endpoint_port": "6379",
+ "__address__": "node-cluster-001.abc123.0001.use1.cache.amazonaws.com:6379",
+ },
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ tg := &targetgroup.Group{
+ Source: "test",
+ }
+
+ addCacheClusterTargets(tg, tt.cluster, tt.tags)
+
+ require.Len(t, tg.Targets, tt.expectedTargetCount)
+
+ // Check each target
+ for i, expectedLabels := range tt.expectedLabels {
+ labels := tg.Targets[i]
+
+ // Check that all expected labels are present with correct values
+ for k, v := range expectedLabels {
+ actualValue, exists := labels[k]
+ require.True(t, exists, "label %s should exist in target %d", k, i)
+ require.Equal(t, v, actualValue, "label %s mismatch in target %d", k, i)
+ }
+ }
+ })
+ }
+}
+
+// Mock Elasticache client.
+type mockElasticacheClient struct {
+ data *elasticacheDataStore
+}
+
+func newMockElasticacheClient(data *elasticacheDataStore) *mockElasticacheClient {
+ return &mockElasticacheClient{data: data}
+}
+
+func (m *mockElasticacheClient) DescribeServerlessCaches(_ context.Context, input *elasticache.DescribeServerlessCachesInput, _ ...func(*elasticache.Options)) (*elasticache.DescribeServerlessCachesOutput, error) {
+ if input.ServerlessCacheName != nil {
+ // Filter by name
+ for _, cache := range m.data.serverlessCaches {
+ if cache.ServerlessCacheName != nil && *cache.ServerlessCacheName == *input.ServerlessCacheName {
+ return &elasticache.DescribeServerlessCachesOutput{
+ ServerlessCaches: []types.ServerlessCache{cache},
+ }, nil
+ }
+ }
+ return &elasticache.DescribeServerlessCachesOutput{
+ ServerlessCaches: []types.ServerlessCache{},
+ }, nil
+ }
+
+ return &elasticache.DescribeServerlessCachesOutput{
+ ServerlessCaches: m.data.serverlessCaches,
+ }, nil
+}
+
+func (m *mockElasticacheClient) DescribeCacheClusters(_ context.Context, input *elasticache.DescribeCacheClustersInput, _ ...func(*elasticache.Options)) (*elasticache.DescribeCacheClustersOutput, error) {
+ if input.CacheClusterId != nil {
+ // Single cluster lookup
+ for _, cluster := range m.data.cacheClusters {
+ if cluster.CacheClusterId != nil && *cluster.CacheClusterId == *input.CacheClusterId {
+ return &elasticache.DescribeCacheClustersOutput{
+ CacheClusters: []types.CacheCluster{cluster},
+ }, nil
+ }
+ }
+ return &elasticache.DescribeCacheClustersOutput{
+ CacheClusters: []types.CacheCluster{},
+ }, nil
+ }
+
+ return &elasticache.DescribeCacheClustersOutput{
+ CacheClusters: m.data.cacheClusters,
+ }, nil
+}
+
+func (m *mockElasticacheClient) ListTagsForResource(_ context.Context, input *elasticache.ListTagsForResourceInput, _ ...func(*elasticache.Options)) (*elasticache.ListTagsForResourceOutput, error) {
+ if input.ResourceName != nil {
+ if tags, ok := m.data.tags[*input.ResourceName]; ok {
+ return &elasticache.ListTagsForResourceOutput{
+ TagList: tags,
+ }, nil
+ }
+ }
+
+ return &elasticache.ListTagsForResourceOutput{
+ TagList: []types.Tag{},
+ }, nil
+}
+
+func TestSplitCacheDeploymentOptions(t *testing.T) {
+ tests := []struct {
+ name string
+ caches []string
+ expectedServerlessCacheIDs []string
+ expectedCacheClusterIDs []string
+ }{
+ {
+ name: "MixedARNs",
+ caches: []string{
+ "arn:aws:elasticache:us-east-1:123456789012:serverlesscache:my-serverless-cache",
+ "arn:aws:elasticache:us-east-1:123456789012:replicationgroup:my-replication-group",
+ "arn:aws:elasticache:us-west-2:123456789012:serverlesscache:prod-cache",
+ },
+ expectedServerlessCacheIDs: []string{"my-serverless-cache", "prod-cache"},
+ expectedCacheClusterIDs: []string{"my-replication-group"},
+ },
+ {
+ name: "OnlyServerlessCaches",
+ caches: []string{
+ "arn:aws:elasticache:us-east-1:123456789012:serverlesscache:cache-1",
+ "arn:aws:elasticache:us-east-1:123456789012:serverlesscache:cache-2",
+ },
+ expectedServerlessCacheIDs: []string{"cache-1", "cache-2"},
+ expectedCacheClusterIDs: nil,
+ },
+ {
+ name: "OnlyReplicationGroups",
+ caches: []string{
+ "arn:aws:elasticache:us-east-1:123456789012:replicationgroup:cluster-1",
+ "arn:aws:elasticache:us-east-1:123456789012:replicationgroup:cluster-2",
+ },
+ expectedServerlessCacheIDs: nil,
+ expectedCacheClusterIDs: []string{"cluster-1", "cluster-2"},
+ },
+ {
+ name: "EmptyInput",
+ caches: []string{},
+ expectedServerlessCacheIDs: nil,
+ expectedCacheClusterIDs: nil,
+ },
+ {
+ name: "InvalidARNs",
+ caches: []string{
+ "not-an-arn",
+ "arn:aws:elasticache:us-east-1",
+ "",
+ },
+ expectedServerlessCacheIDs: nil,
+ expectedCacheClusterIDs: nil,
+ },
+ {
+ name: "UnknownResourceType",
+ caches: []string{
+ "arn:aws:elasticache:us-east-1:123456789012:unknown:resource-id",
+ },
+ expectedServerlessCacheIDs: nil,
+ expectedCacheClusterIDs: nil,
+ },
+ {
+ name: "MixedWithInvalidARNs",
+ caches: []string{
+ "arn:aws:elasticache:us-east-1:123456789012:serverlesscache:valid-cache",
+ "invalid-arn",
+ "arn:aws:elasticache:us-east-1:123456789012:replicationgroup:valid-cluster",
+ "",
+ "arn:aws:elasticache:us-east-1:123456789012:unknown:ignored",
+ },
+ expectedServerlessCacheIDs: []string{"valid-cache"},
+ expectedCacheClusterIDs: []string{"valid-cluster"},
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ serverlessCacheIDs, cacheClusterIDs := splitCacheDeploymentOptions(tt.caches)
+
+ require.Equal(t, tt.expectedServerlessCacheIDs, serverlessCacheIDs, "serverless cache IDs mismatch")
+ require.Equal(t, tt.expectedCacheClusterIDs, cacheClusterIDs, "cache cluster IDs mismatch")
+ })
+ }
+}
diff --git a/discovery/aws/lightsail.go b/discovery/aws/lightsail.go
index 5441e510b9..39e4716957 100644
--- a/discovery/aws/lightsail.go
+++ b/discovery/aws/lightsail.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
@@ -17,7 +17,6 @@ import (
"context"
"errors"
"fmt"
- "log/slog"
"net"
"strconv"
"strings"
@@ -27,7 +26,6 @@ import (
awsConfig "github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/credentials/stscreds"
- "github.com/aws/aws-sdk-go-v2/feature/ec2/imds"
"github.com/aws/aws-sdk-go-v2/service/lightsail"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/aws/smithy-go"
@@ -95,7 +93,7 @@ func (*LightsailSDConfig) Name() string { return "lightsail" }
// NewDiscoverer returns a Discoverer for the Lightsail Config.
func (c *LightsailSDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) {
- return NewLightsailDiscovery(c, opts.Logger, opts.Metrics)
+ return NewLightsailDiscovery(c, opts)
}
// UnmarshalYAML implements the yaml.Unmarshaler interface for the Lightsail Config.
@@ -107,30 +105,9 @@ func (c *LightsailSDConfig) UnmarshalYAML(unmarshal func(any) error) error {
return err
}
- if c.Region == "" {
- cfg, err := awsConfig.LoadDefaultConfig(context.Background())
- if err != nil {
- return err
- }
-
- if cfg.Region != "" {
- // Use the region from the AWS config. It will load environment variables and shared config files.
- c.Region = cfg.Region
- }
-
- if c.Region == "" {
- // Try to get the region from the instance metadata service (IMDS).
- imdsClient := imds.NewFromConfig(cfg)
- region, err := imdsClient.GetRegion(context.Background(), &imds.GetRegionInput{})
- if err != nil {
- return err
- }
- c.Region = region.Region
- }
- }
-
- if c.Region == "" {
- return errors.New("lightsail SD configuration requires a region")
+ c.Region, err = loadRegion(context.Background(), c.Region)
+ if err != nil {
+ return fmt.Errorf("could not determine AWS region: %w", err)
}
return c.HTTPClientConfig.Validate()
@@ -145,14 +122,14 @@ type LightsailDiscovery struct {
}
// NewLightsailDiscovery returns a new LightsailDiscovery which periodically refreshes its targets.
-func NewLightsailDiscovery(conf *LightsailSDConfig, logger *slog.Logger, metrics discovery.DiscovererMetrics) (*LightsailDiscovery, error) {
- m, ok := metrics.(*lightsailMetrics)
+func NewLightsailDiscovery(conf *LightsailSDConfig, opts discovery.DiscovererOptions) (*LightsailDiscovery, error) {
+ m, ok := opts.Metrics.(*lightsailMetrics)
if !ok {
return nil, errors.New("invalid discovery metrics type")
}
- if logger == nil {
- logger = promslog.NewNopLogger()
+ if opts.Logger == nil {
+ opts.Logger = promslog.NewNopLogger()
}
d := &LightsailDiscovery{
@@ -160,8 +137,9 @@ func NewLightsailDiscovery(conf *LightsailSDConfig, logger *slog.Logger, metrics
}
d.Discovery = refresh.NewDiscovery(
refresh.Options{
- Logger: logger,
+ Logger: opts.Logger,
Mech: "lightsail",
+ SetName: opts.SetName,
Interval: time.Duration(d.cfg.RefreshInterval),
RefreshF: d.refresh,
MetricsInstantiator: m.refreshMetrics,
@@ -210,7 +188,12 @@ func (d *LightsailDiscovery) lightsailClient(ctx context.Context) (*lightsail.Cl
cfg.Credentials = aws.NewCredentialsCache(assumeProvider)
}
- d.lightsail = lightsail.NewFromConfig(cfg)
+ d.lightsail = lightsail.NewFromConfig(cfg, func(options *lightsail.Options) {
+ if d.cfg.Endpoint != "" {
+ options.BaseEndpoint = &d.cfg.Endpoint
+ }
+ options.HTTPClient = httpClient
+ })
return d.lightsail, nil
}
diff --git a/discovery/aws/metrics_aws.go b/discovery/aws/metrics_aws.go
new file mode 100644
index 0000000000..4cb9b25041
--- /dev/null
+++ b/discovery/aws/metrics_aws.go
@@ -0,0 +1,32 @@
+// 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 aws
+
+import (
+ "github.com/prometheus/prometheus/discovery"
+)
+
+type awsMetrics struct {
+ refreshMetrics discovery.RefreshMetricsInstantiator
+}
+
+var _ discovery.DiscovererMetrics = (*awsMetrics)(nil)
+
+// Register implements discovery.DiscovererMetrics.
+func (*awsMetrics) Register() error {
+ return nil
+}
+
+// Unregister implements discovery.DiscovererMetrics.
+func (*awsMetrics) Unregister() {}
diff --git a/discovery/aws/metrics_ec2.go b/discovery/aws/metrics_ec2.go
index 45227c3534..1a37347b40 100644
--- a/discovery/aws/metrics_ec2.go
+++ b/discovery/aws/metrics_ec2.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
diff --git a/discovery/aws/metrics_ecs.go b/discovery/aws/metrics_ecs.go
new file mode 100644
index 0000000000..dde3483c06
--- /dev/null
+++ b/discovery/aws/metrics_ecs.go
@@ -0,0 +1,32 @@
+// 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 aws
+
+import (
+ "github.com/prometheus/prometheus/discovery"
+)
+
+type ecsMetrics struct {
+ refreshMetrics discovery.RefreshMetricsInstantiator
+}
+
+var _ discovery.DiscovererMetrics = (*ecsMetrics)(nil)
+
+// Register implements discovery.DiscovererMetrics.
+func (*ecsMetrics) Register() error {
+ return nil
+}
+
+// Unregister implements discovery.DiscovererMetrics.
+func (*ecsMetrics) Unregister() {}
diff --git a/discovery/aws/metrics_elasticache.go b/discovery/aws/metrics_elasticache.go
new file mode 100644
index 0000000000..7ecfcb4b72
--- /dev/null
+++ b/discovery/aws/metrics_elasticache.go
@@ -0,0 +1,32 @@
+// 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 aws
+
+import (
+ "github.com/prometheus/prometheus/discovery"
+)
+
+type elasticacheMetrics struct {
+ refreshMetrics discovery.RefreshMetricsInstantiator
+}
+
+var _ discovery.DiscovererMetrics = (*elasticacheMetrics)(nil)
+
+// Register implements discovery.DiscovererMetrics.
+func (*elasticacheMetrics) Register() error {
+ return nil
+}
+
+// Unregister implements discovery.DiscovererMetrics.
+func (*elasticacheMetrics) Unregister() {}
diff --git a/discovery/aws/metrics_lightsail.go b/discovery/aws/metrics_lightsail.go
index 4dfe14c60c..40f7639459 100644
--- a/discovery/aws/metrics_lightsail.go
+++ b/discovery/aws/metrics_lightsail.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
diff --git a/discovery/aws/metrics_msk.go b/discovery/aws/metrics_msk.go
new file mode 100644
index 0000000000..fc69f57aa1
--- /dev/null
+++ b/discovery/aws/metrics_msk.go
@@ -0,0 +1,32 @@
+// Copyright 2015 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 aws
+
+import (
+ "github.com/prometheus/prometheus/discovery"
+)
+
+type mskMetrics struct {
+ refreshMetrics discovery.RefreshMetricsInstantiator
+}
+
+var _ discovery.DiscovererMetrics = (*mskMetrics)(nil)
+
+// Register implements discovery.DiscovererMetrics.
+func (*mskMetrics) Register() error {
+ return nil
+}
+
+// Unregister implements discovery.DiscovererMetrics.
+func (*mskMetrics) Unregister() {}
diff --git a/discovery/aws/msk.go b/discovery/aws/msk.go
new file mode 100644
index 0000000000..3ecc1e6235
--- /dev/null
+++ b/discovery/aws/msk.go
@@ -0,0 +1,451 @@
+// 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 aws
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "log/slog"
+ "net"
+ "strconv"
+ "sync"
+ "time"
+
+ "github.com/aws/aws-sdk-go-v2/aws"
+ awsConfig "github.com/aws/aws-sdk-go-v2/config"
+ "github.com/aws/aws-sdk-go-v2/credentials"
+ "github.com/aws/aws-sdk-go-v2/credentials/stscreds"
+ "github.com/aws/aws-sdk-go-v2/service/kafka"
+ "github.com/aws/aws-sdk-go-v2/service/kafka/types"
+ "github.com/aws/aws-sdk-go-v2/service/sts"
+ "github.com/prometheus/client_golang/prometheus"
+ "github.com/prometheus/common/config"
+ "github.com/prometheus/common/model"
+ "github.com/prometheus/common/promslog"
+ "golang.org/x/sync/errgroup"
+
+ "github.com/prometheus/prometheus/discovery"
+ "github.com/prometheus/prometheus/discovery/refresh"
+ "github.com/prometheus/prometheus/discovery/targetgroup"
+ "github.com/prometheus/prometheus/util/strutil"
+)
+
+type NodeType string
+
+const (
+ NodeTypeBroker NodeType = "BROKER"
+ NodeTypeController NodeType = "CONTROLLER"
+)
+
+const (
+ mskLabel = model.MetaLabelPrefix + "msk_"
+
+ // Cluster labels.
+ mskLabelCluster = mskLabel + "cluster_"
+ mskLabelClusterName = mskLabelCluster + "name"
+ mskLabelClusterARN = mskLabelCluster + "arn"
+ mskLabelClusterState = mskLabelCluster + "state"
+ mskLabelClusterType = mskLabelCluster + "type"
+ mskLabelClusterVersion = mskLabelCluster + "version"
+ mskLabelClusterJmxExporterEnabled = mskLabelCluster + "jmx_exporter_enabled"
+ mskLabelClusterConfigurationARN = mskLabelCluster + "configuration_arn"
+ mskLabelClusterConfigurationRevision = mskLabelCluster + "configuration_revision"
+ mskLabelClusterKafkaVersion = mskLabelCluster + "kafka_version"
+ mskLabelClusterTags = mskLabelCluster + "tag_"
+
+ // Node labels.
+ mskLabelNode = mskLabel + "node_"
+ mskLabelNodeType = mskLabelNode + "type"
+ mskLabelNodeARN = mskLabelNode + "arn"
+ mskLabelNodeAddedTime = mskLabelNode + "added_time"
+ mskLabelNodeInstanceType = mskLabelNode + "instance_type"
+ mskLabelNodeAttachedENI = mskLabelNode + "attached_eni"
+
+ // Broker labels.
+ mskLabelBroker = mskLabel + "broker_"
+ mskLabelBrokerEndpointIndex = mskLabelBroker + "endpoint_index"
+ mskLabelBrokerID = mskLabelBroker + "id"
+ mskLabelBrokerClientSubnet = mskLabelBroker + "client_subnet"
+ mskLabelBrokerClientVPCIP = mskLabelBroker + "client_vpc_ip"
+ mskLabelBrokerNodeExporterEnabled = mskLabelBroker + "node_exporter_enabled"
+
+ // Controller labels.
+ mskLabelController = mskLabel + "controller_"
+ mskLabelControllerEndpointIndex = mskLabelController + "endpoint_index"
+)
+
+// DefaultMSKSDConfig is the default MSK SD configuration.
+var DefaultMSKSDConfig = MSKSDConfig{
+ Port: 80,
+ RefreshInterval: model.Duration(60 * time.Second),
+ RequestConcurrency: 10,
+ HTTPClientConfig: config.DefaultHTTPClientConfig,
+}
+
+func init() {
+ discovery.RegisterConfig(&MSKSDConfig{})
+}
+
+// MSKSDConfig is the configuration for MSK based service discovery.
+type MSKSDConfig struct {
+ Region string `yaml:"region"`
+ Endpoint string `yaml:"endpoint"`
+ AccessKey string `yaml:"access_key,omitempty"`
+ SecretKey config.Secret `yaml:"secret_key,omitempty"`
+ Profile string `yaml:"profile,omitempty"`
+ RoleARN string `yaml:"role_arn,omitempty"`
+ Clusters []string `yaml:"clusters,omitempty"`
+ Port int `yaml:"port"`
+ RefreshInterval model.Duration `yaml:"refresh_interval,omitempty"`
+
+ RequestConcurrency int `yaml:"request_concurrency,omitempty"`
+ HTTPClientConfig config.HTTPClientConfig `yaml:",inline"`
+}
+
+// NewDiscovererMetrics implements discovery.Config.
+func (*MSKSDConfig) NewDiscovererMetrics(_ prometheus.Registerer, rmi discovery.RefreshMetricsInstantiator) discovery.DiscovererMetrics {
+ return &mskMetrics{
+ refreshMetrics: rmi,
+ }
+}
+
+// Name returns the name of the MSK Config.
+func (*MSKSDConfig) Name() string { return "msk" }
+
+// NewDiscoverer returns a Discoverer for the MSK Config.
+func (c *MSKSDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) {
+ return NewMSKDiscovery(c, opts)
+}
+
+// UnmarshalYAML implements the yaml.Unmarshaler interface for the MSK Config.
+func (c *MSKSDConfig) UnmarshalYAML(unmarshal func(any) error) error {
+ *c = DefaultMSKSDConfig
+ type plain MSKSDConfig
+ err := unmarshal((*plain)(c))
+ if err != nil {
+ return err
+ }
+
+ c.Region, err = loadRegion(context.Background(), c.Region)
+ if err != nil {
+ return fmt.Errorf("could not determine AWS region: %w", err)
+ }
+
+ return c.HTTPClientConfig.Validate()
+}
+
+type mskClient interface {
+ DescribeClusterV2(context.Context, *kafka.DescribeClusterV2Input, ...func(*kafka.Options)) (*kafka.DescribeClusterV2Output, error)
+ ListClustersV2(context.Context, *kafka.ListClustersV2Input, ...func(*kafka.Options)) (*kafka.ListClustersV2Output, error)
+ ListNodes(context.Context, *kafka.ListNodesInput, ...func(*kafka.Options)) (*kafka.ListNodesOutput, error)
+}
+
+// MSKDiscovery periodically performs MSK-SD requests. It implements
+// the Discoverer interface.
+type MSKDiscovery struct {
+ *refresh.Discovery
+ logger *slog.Logger
+ cfg *MSKSDConfig
+ msk mskClient
+}
+
+// NewMSKDiscovery returns a new MSKDiscovery which periodically refreshes its targets.
+func NewMSKDiscovery(conf *MSKSDConfig, opts discovery.DiscovererOptions) (*MSKDiscovery, error) {
+ m, ok := opts.Metrics.(*mskMetrics)
+ if !ok {
+ return nil, errors.New("invalid discovery metrics type")
+ }
+
+ if opts.Logger == nil {
+ opts.Logger = promslog.NewNopLogger()
+ }
+ d := &MSKDiscovery{
+ logger: opts.Logger,
+ cfg: conf,
+ }
+ d.Discovery = refresh.NewDiscovery(
+ refresh.Options{
+ Logger: opts.Logger,
+ Mech: "msk",
+ Interval: time.Duration(d.cfg.RefreshInterval),
+ RefreshF: d.refresh,
+ MetricsInstantiator: m.refreshMetrics,
+ },
+ )
+ return d, nil
+}
+
+func (d *MSKDiscovery) initMskClient(ctx context.Context) error {
+ if d.msk != nil {
+ return nil
+ }
+
+ if d.cfg.Region == "" {
+ return errors.New("region must be set for MSK service discovery")
+ }
+
+ // Build the HTTP client from the provided HTTPClientConfig.
+ client, err := config.NewClientFromConfig(d.cfg.HTTPClientConfig, "msk_sd")
+ if err != nil {
+ return err
+ }
+
+ // Build the AWS config with the provided region.
+ var configOptions []func(*awsConfig.LoadOptions) error
+ configOptions = append(configOptions, awsConfig.WithRegion(d.cfg.Region))
+ configOptions = append(configOptions, awsConfig.WithHTTPClient(client))
+
+ // Only set static credentials if both access key and secret key are provided
+ // Otherwise, let AWS SDK use its default credential chain
+ if d.cfg.AccessKey != "" && d.cfg.SecretKey != "" {
+ credProvider := credentials.NewStaticCredentialsProvider(d.cfg.AccessKey, string(d.cfg.SecretKey), "")
+ configOptions = append(configOptions, awsConfig.WithCredentialsProvider(credProvider))
+ }
+
+ if d.cfg.Profile != "" {
+ configOptions = append(configOptions, awsConfig.WithSharedConfigProfile(d.cfg.Profile))
+ }
+
+ cfg, err := awsConfig.LoadDefaultConfig(ctx, configOptions...)
+ if err != nil {
+ d.logger.Error("Failed to create AWS config", "error", err)
+ return fmt.Errorf("could not create aws config: %w", err)
+ }
+
+ // If the role ARN is set, assume the role to get credentials and set the credentials provider in the config.
+ if d.cfg.RoleARN != "" {
+ assumeProvider := stscreds.NewAssumeRoleProvider(sts.NewFromConfig(cfg), d.cfg.RoleARN)
+ cfg.Credentials = aws.NewCredentialsCache(assumeProvider)
+ }
+
+ d.msk = kafka.NewFromConfig(cfg, func(options *kafka.Options) {
+ if d.cfg.Endpoint != "" {
+ options.BaseEndpoint = &d.cfg.Endpoint
+ }
+ options.HTTPClient = client
+ })
+
+ // Test credentials by making a simple API call
+ testCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
+ defer cancel()
+
+ _, err = d.msk.ListClustersV2(testCtx, &kafka.ListClustersV2Input{})
+ if err != nil {
+ d.logger.Error("Failed to test MSK credentials", "error", err)
+ return fmt.Errorf("MSK credential test failed: %w", err)
+ }
+
+ return nil
+}
+
+// describeClusters describes the clusters with the given ARNs and returns their details.
+func (d *MSKDiscovery) describeClusters(ctx context.Context, clusterARNs []string) ([]types.Cluster, error) {
+ var (
+ clusters []types.Cluster
+ mu sync.Mutex
+ )
+ errg, ectx := errgroup.WithContext(ctx)
+ errg.SetLimit(d.cfg.RequestConcurrency)
+ for _, clusterARN := range clusterARNs {
+ errg.Go(func() error {
+ cluster, err := d.msk.DescribeClusterV2(ectx, &kafka.DescribeClusterV2Input{
+ ClusterArn: aws.String(clusterARN),
+ })
+ if err != nil {
+ return fmt.Errorf("could not describe cluster %v: %w", clusterARN, err)
+ }
+ mu.Lock()
+ clusters = append(clusters, *cluster.ClusterInfo)
+ mu.Unlock()
+ return nil
+ })
+ }
+
+ return clusters, errg.Wait()
+}
+
+// listClusters lists all MSK clusters in the configured region and returns their details.
+func (d *MSKDiscovery) listClusters(ctx context.Context) ([]types.Cluster, error) {
+ var (
+ clusters []types.Cluster
+ nextToken *string
+ )
+ for {
+ listClustersInput := kafka.ListClustersV2Input{
+ ClusterTypeFilter: aws.String("PROVISIONED"),
+ MaxResults: aws.Int32(100),
+ NextToken: nextToken,
+ }
+
+ resp, err := d.msk.ListClustersV2(ctx, &listClustersInput)
+ if err != nil {
+ return nil, fmt.Errorf("could not list clusters: %w", err)
+ }
+
+ clusters = append(clusters, resp.ClusterInfoList...)
+ if resp.NextToken == nil {
+ break
+ }
+ nextToken = resp.NextToken
+ }
+
+ return clusters, nil
+}
+
+// listNodes lists all nodes for the given clusters and returns a map of cluster ARN to its nodes.
+func (d *MSKDiscovery) listNodes(ctx context.Context, clusters []types.Cluster) (map[string][]types.NodeInfo, error) {
+ clusterNodeMap := make(map[string][]types.NodeInfo)
+ mu := sync.Mutex{}
+ errg, ectx := errgroup.WithContext(ctx)
+ errg.SetLimit(d.cfg.RequestConcurrency)
+ for _, cluster := range clusters {
+ clusterARN := aws.ToString(cluster.ClusterArn)
+ errg.Go(func() error {
+ var clusterNodes []types.NodeInfo
+ var nextToken *string
+ for {
+ resp, err := d.msk.ListNodes(ectx, &kafka.ListNodesInput{
+ ClusterArn: aws.String(clusterARN),
+ MaxResults: aws.Int32(100),
+ NextToken: nextToken,
+ })
+ if err != nil {
+ return fmt.Errorf("could not list nodes for cluster %v: %w", clusterARN, err)
+ }
+
+ clusterNodes = append(clusterNodes, resp.NodeInfoList...)
+ if resp.NextToken == nil {
+ break
+ }
+ nextToken = resp.NextToken
+ }
+
+ mu.Lock()
+ clusterNodeMap[clusterARN] = clusterNodes
+ mu.Unlock()
+ return nil
+ })
+ }
+
+ return clusterNodeMap, errg.Wait()
+}
+
+func (d *MSKDiscovery) refresh(ctx context.Context) ([]*targetgroup.Group, error) {
+ err := d.initMskClient(ctx)
+ if err != nil {
+ return nil, err
+ }
+
+ tg := &targetgroup.Group{
+ Source: d.cfg.Region,
+ }
+
+ var clusters []types.Cluster
+ if len(d.cfg.Clusters) > 0 {
+ clusters, err = d.describeClusters(ctx, d.cfg.Clusters)
+ if err != nil {
+ return nil, err
+ }
+ } else {
+ clusters, err = d.listClusters(ctx)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ clusterNodeMap, err := d.listNodes(ctx, clusters)
+ if err != nil {
+ return nil, err
+ }
+
+ var (
+ targetsMu sync.Mutex
+ wg sync.WaitGroup
+ )
+ for _, cluster := range clusters {
+ wg.Add(1)
+
+ go func(cluster types.Cluster, nodes []types.NodeInfo) {
+ defer wg.Done()
+ for _, node := range nodes {
+ labels := model.LabelSet{
+ mskLabelClusterName: model.LabelValue(aws.ToString(cluster.ClusterName)),
+ mskLabelClusterARN: model.LabelValue(aws.ToString(cluster.ClusterArn)),
+ mskLabelClusterState: model.LabelValue(string(cluster.State)),
+ mskLabelClusterType: model.LabelValue(string(cluster.ClusterType)),
+ mskLabelClusterVersion: model.LabelValue(aws.ToString(cluster.CurrentVersion)),
+ mskLabelNodeARN: model.LabelValue(aws.ToString(node.NodeARN)),
+ mskLabelNodeAddedTime: model.LabelValue(aws.ToString(node.AddedToClusterTime)),
+ mskLabelNodeInstanceType: model.LabelValue(aws.ToString(node.InstanceType)),
+ mskLabelClusterJmxExporterEnabled: model.LabelValue(strconv.FormatBool(*cluster.Provisioned.OpenMonitoring.Prometheus.JmxExporter.EnabledInBroker)),
+ mskLabelClusterConfigurationARN: model.LabelValue(aws.ToString(cluster.Provisioned.CurrentBrokerSoftwareInfo.ConfigurationArn)),
+ mskLabelClusterConfigurationRevision: model.LabelValue(strconv.FormatInt(*cluster.Provisioned.CurrentBrokerSoftwareInfo.ConfigurationRevision, 10)),
+ mskLabelClusterKafkaVersion: model.LabelValue(aws.ToString(cluster.Provisioned.CurrentBrokerSoftwareInfo.KafkaVersion)),
+ }
+
+ for key, value := range cluster.Tags {
+ labels[model.LabelName(mskLabelClusterTags+strutil.SanitizeLabelName(key))] = model.LabelValue(value)
+ }
+
+ switch nodeType(node) {
+ case NodeTypeBroker:
+ labels[mskLabelNodeType] = model.LabelValue(NodeTypeBroker)
+ labels[mskLabelNodeAttachedENI] = model.LabelValue(aws.ToString(node.BrokerNodeInfo.AttachedENIId))
+ labels[mskLabelBrokerID] = model.LabelValue(fmt.Sprintf("%.0f", aws.ToFloat64(node.BrokerNodeInfo.BrokerId)))
+ labels[mskLabelBrokerClientSubnet] = model.LabelValue(aws.ToString(node.BrokerNodeInfo.ClientSubnet))
+ labels[mskLabelBrokerClientVPCIP] = model.LabelValue(aws.ToString(node.BrokerNodeInfo.ClientVpcIpAddress))
+ labels[mskLabelBrokerNodeExporterEnabled] = model.LabelValue(strconv.FormatBool(*cluster.Provisioned.OpenMonitoring.Prometheus.NodeExporter.EnabledInBroker))
+
+ for idx, endpoint := range node.BrokerNodeInfo.Endpoints {
+ endpointLabels := labels.Clone()
+ endpointLabels[mskLabelBrokerEndpointIndex] = model.LabelValue(strconv.Itoa(idx))
+ endpointLabels[model.AddressLabel] = model.LabelValue(net.JoinHostPort(endpoint, strconv.Itoa(d.cfg.Port)))
+
+ targetsMu.Lock()
+ tg.Targets = append(tg.Targets, endpointLabels)
+ targetsMu.Unlock()
+ }
+
+ case NodeTypeController:
+ labels[mskLabelNodeType] = model.LabelValue(NodeTypeController)
+
+ for idx, endpoint := range node.ControllerNodeInfo.Endpoints {
+ endpointLabels := labels.Clone()
+ endpointLabels[mskLabelControllerEndpointIndex] = model.LabelValue(strconv.Itoa(idx))
+ endpointLabels[model.AddressLabel] = model.LabelValue(net.JoinHostPort(endpoint, strconv.Itoa(d.cfg.Port)))
+
+ targetsMu.Lock()
+ tg.Targets = append(tg.Targets, endpointLabels)
+ targetsMu.Unlock()
+ }
+ default:
+ continue
+ }
+ }
+ }(cluster, clusterNodeMap[aws.ToString(cluster.ClusterArn)])
+ }
+ wg.Wait()
+
+ return []*targetgroup.Group{tg}, nil
+}
+
+func nodeType(node types.NodeInfo) NodeType {
+ if node.BrokerNodeInfo != nil {
+ return NodeTypeBroker
+ } else if node.ControllerNodeInfo != nil {
+ return NodeTypeController
+ }
+ return ""
+}
diff --git a/discovery/aws/msk_test.go b/discovery/aws/msk_test.go
new file mode 100644
index 0000000000..b1d48a7ea6
--- /dev/null
+++ b/discovery/aws/msk_test.go
@@ -0,0 +1,1131 @@
+// 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 aws
+
+import (
+ "context"
+ "fmt"
+ "sort"
+ "testing"
+
+ "github.com/aws/aws-sdk-go-v2/aws"
+ "github.com/aws/aws-sdk-go-v2/service/kafka"
+ "github.com/aws/aws-sdk-go-v2/service/kafka/types"
+ "github.com/prometheus/common/model"
+ "github.com/stretchr/testify/require"
+
+ "github.com/prometheus/prometheus/discovery/targetgroup"
+)
+
+// Struct for test data.
+type mskDataStore struct {
+ region string
+ clusters []types.Cluster
+ nodes map[string][]types.NodeInfo // keyed by cluster ARN
+}
+
+func TestMSKDiscoveryListClusters(t *testing.T) {
+ ctx := context.Background()
+
+ for _, tt := range []struct {
+ name string
+ mskData *mskDataStore
+ expected []types.Cluster
+ }{
+ {
+ name: "MultipleClusters",
+ mskData: &mskDataStore{
+ region: "us-west-2",
+ clusters: []types.Cluster{
+ {
+ ClusterName: strptr("test-cluster"),
+ ClusterArn: strptr("arn:aws:kafka:us-west-2:123456789012:cluster/test-cluster/abc-123"),
+ State: types.ClusterStateActive,
+ ClusterType: types.ClusterTypeProvisioned,
+ },
+ {
+ ClusterName: strptr("prod-cluster"),
+ ClusterArn: strptr("arn:aws:kafka:us-west-2:123456789012:cluster/prod-cluster/def-456"),
+ State: types.ClusterStateActive,
+ ClusterType: types.ClusterTypeProvisioned,
+ },
+ },
+ },
+ expected: []types.Cluster{
+ {
+ ClusterName: strptr("test-cluster"),
+ ClusterArn: strptr("arn:aws:kafka:us-west-2:123456789012:cluster/test-cluster/abc-123"),
+ State: types.ClusterStateActive,
+ ClusterType: types.ClusterTypeProvisioned,
+ },
+ {
+ ClusterName: strptr("prod-cluster"),
+ ClusterArn: strptr("arn:aws:kafka:us-west-2:123456789012:cluster/prod-cluster/def-456"),
+ State: types.ClusterStateActive,
+ ClusterType: types.ClusterTypeProvisioned,
+ },
+ },
+ },
+ {
+ name: "SingleCluster",
+ mskData: &mskDataStore{
+ region: "us-east-1",
+ clusters: []types.Cluster{
+ {
+ ClusterName: strptr("single-cluster"),
+ ClusterArn: strptr("arn:aws:kafka:us-east-1:123456789012:cluster/single-cluster/xyz-789"),
+ State: types.ClusterStateActive,
+ ClusterType: types.ClusterTypeProvisioned,
+ },
+ },
+ },
+ expected: []types.Cluster{
+ {
+ ClusterName: strptr("single-cluster"),
+ ClusterArn: strptr("arn:aws:kafka:us-east-1:123456789012:cluster/single-cluster/xyz-789"),
+ State: types.ClusterStateActive,
+ ClusterType: types.ClusterTypeProvisioned,
+ },
+ },
+ },
+ {
+ name: "NoClusters",
+ mskData: &mskDataStore{
+ region: "us-east-1",
+ clusters: []types.Cluster{},
+ },
+ expected: nil,
+ },
+ } {
+ t.Run(tt.name, func(t *testing.T) {
+ client := newMockMSKClient(tt.mskData)
+
+ d := &MSKDiscovery{
+ msk: client,
+ cfg: &MSKSDConfig{
+ Region: tt.mskData.region,
+ },
+ }
+
+ clusters, err := d.listClusters(ctx)
+ require.NoError(t, err)
+ require.Equal(t, tt.expected, clusters)
+ })
+ }
+}
+
+func TestMSKDiscoveryDescribeClusters(t *testing.T) {
+ ctx := context.Background()
+
+ for _, tt := range []struct {
+ name string
+ mskData *mskDataStore
+ clusterARNs []string
+ expected []types.Cluster
+ }{
+ {
+ name: "SingleCluster",
+ mskData: &mskDataStore{
+ region: "us-west-2",
+ clusters: []types.Cluster{
+ {
+ ClusterName: strptr("test-cluster"),
+ ClusterArn: strptr("arn:aws:kafka:us-west-2:123456789012:cluster/test-cluster/abc-123"),
+ State: types.ClusterStateActive,
+ ClusterType: types.ClusterTypeProvisioned,
+ CurrentVersion: strptr("1.2.3"),
+ Tags: map[string]string{
+ "Environment": "production",
+ "Team": "platform",
+ },
+ },
+ },
+ },
+ clusterARNs: []string{"arn:aws:kafka:us-west-2:123456789012:cluster/test-cluster/abc-123"},
+ expected: []types.Cluster{
+ {
+ ClusterName: strptr("test-cluster"),
+ ClusterArn: strptr("arn:aws:kafka:us-west-2:123456789012:cluster/test-cluster/abc-123"),
+ State: types.ClusterStateActive,
+ ClusterType: types.ClusterTypeProvisioned,
+ CurrentVersion: strptr("1.2.3"),
+ Tags: map[string]string{
+ "Environment": "production",
+ "Team": "platform",
+ },
+ },
+ },
+ },
+ {
+ name: "MultipleClusters",
+ mskData: &mskDataStore{
+ region: "us-east-1",
+ clusters: []types.Cluster{
+ {
+ ClusterName: strptr("cluster-1"),
+ ClusterArn: strptr("arn:aws:kafka:us-east-1:123456789012:cluster/cluster-1/xyz-789"),
+ State: types.ClusterStateActive,
+ ClusterType: types.ClusterTypeProvisioned,
+ },
+ {
+ ClusterName: strptr("cluster-2"),
+ ClusterArn: strptr("arn:aws:kafka:us-east-1:123456789012:cluster/cluster-2/def-456"),
+ State: types.ClusterStateActive,
+ ClusterType: types.ClusterTypeProvisioned,
+ Tags: map[string]string{
+ "Stage": "prod",
+ },
+ },
+ },
+ },
+ clusterARNs: []string{
+ "arn:aws:kafka:us-east-1:123456789012:cluster/cluster-1/xyz-789",
+ "arn:aws:kafka:us-east-1:123456789012:cluster/cluster-2/def-456",
+ },
+ expected: []types.Cluster{
+ {
+ ClusterName: strptr("cluster-1"),
+ ClusterArn: strptr("arn:aws:kafka:us-east-1:123456789012:cluster/cluster-1/xyz-789"),
+ State: types.ClusterStateActive,
+ ClusterType: types.ClusterTypeProvisioned,
+ },
+ {
+ ClusterName: strptr("cluster-2"),
+ ClusterArn: strptr("arn:aws:kafka:us-east-1:123456789012:cluster/cluster-2/def-456"),
+ State: types.ClusterStateActive,
+ ClusterType: types.ClusterTypeProvisioned,
+ Tags: map[string]string{
+ "Stage": "prod",
+ },
+ },
+ },
+ },
+ } {
+ t.Run(tt.name, func(t *testing.T) {
+ client := newMockMSKClient(tt.mskData)
+
+ d := &MSKDiscovery{
+ msk: client,
+ cfg: &MSKSDConfig{
+ Region: tt.mskData.region,
+ RequestConcurrency: 10,
+ },
+ }
+
+ clusters, err := d.describeClusters(ctx, tt.clusterARNs)
+ require.NoError(t, err)
+
+ // Sort clusters by ARN to handle non-deterministic ordering from goroutines
+ sort.Slice(clusters, func(i, j int) bool {
+ return aws.ToString(clusters[i].ClusterArn) < aws.ToString(clusters[j].ClusterArn)
+ })
+ sort.Slice(tt.expected, func(i, j int) bool {
+ return aws.ToString(tt.expected[i].ClusterArn) < aws.ToString(tt.expected[j].ClusterArn)
+ })
+
+ require.Equal(t, tt.expected, clusters)
+ })
+ }
+}
+
+func TestMSKDiscoveryListNodes(t *testing.T) {
+ ctx := context.Background()
+
+ for _, tt := range []struct {
+ name string
+ mskData *mskDataStore
+ clusters []types.Cluster
+ expected map[string][]types.NodeInfo
+ }{
+ {
+ name: "ClusterWithBrokers",
+ mskData: &mskDataStore{
+ region: "us-west-2",
+ nodes: map[string][]types.NodeInfo{
+ "arn:aws:kafka:us-west-2:123456789012:cluster/test-cluster/abc-123": {
+ {
+ NodeARN: strptr("arn:aws:kafka:us-west-2:123456789012:node/broker-1"),
+ AddedToClusterTime: strptr("2023-01-01T00:00:00Z"),
+ InstanceType: strptr("kafka.m5.large"),
+ BrokerNodeInfo: &types.BrokerNodeInfo{
+ BrokerId: aws.Float64(1),
+ ClientSubnet: strptr("subnet-12345"),
+ ClientVpcIpAddress: strptr("10.0.1.100"),
+ Endpoints: []string{"b-1.test-cluster.abc123.kafka.us-west-2.amazonaws.com"},
+ AttachedENIId: strptr("eni-12345"),
+ },
+ },
+ {
+ NodeARN: strptr("arn:aws:kafka:us-west-2:123456789012:node/broker-2"),
+ AddedToClusterTime: strptr("2023-01-01T00:00:00Z"),
+ InstanceType: strptr("kafka.m5.large"),
+ BrokerNodeInfo: &types.BrokerNodeInfo{
+ BrokerId: aws.Float64(2),
+ ClientSubnet: strptr("subnet-67890"),
+ ClientVpcIpAddress: strptr("10.0.1.101"),
+ Endpoints: []string{"b-2.test-cluster.abc123.kafka.us-west-2.amazonaws.com"},
+ AttachedENIId: strptr("eni-67890"),
+ },
+ },
+ },
+ },
+ },
+ clusters: []types.Cluster{
+ {
+ ClusterArn: strptr("arn:aws:kafka:us-west-2:123456789012:cluster/test-cluster/abc-123"),
+ },
+ },
+ expected: map[string][]types.NodeInfo{
+ "arn:aws:kafka:us-west-2:123456789012:cluster/test-cluster/abc-123": {
+ {
+ NodeARN: strptr("arn:aws:kafka:us-west-2:123456789012:node/broker-1"),
+ AddedToClusterTime: strptr("2023-01-01T00:00:00Z"),
+ InstanceType: strptr("kafka.m5.large"),
+ BrokerNodeInfo: &types.BrokerNodeInfo{
+ BrokerId: aws.Float64(1),
+ ClientSubnet: strptr("subnet-12345"),
+ ClientVpcIpAddress: strptr("10.0.1.100"),
+ Endpoints: []string{"b-1.test-cluster.abc123.kafka.us-west-2.amazonaws.com"},
+ AttachedENIId: strptr("eni-12345"),
+ },
+ },
+ {
+ NodeARN: strptr("arn:aws:kafka:us-west-2:123456789012:node/broker-2"),
+ AddedToClusterTime: strptr("2023-01-01T00:00:00Z"),
+ InstanceType: strptr("kafka.m5.large"),
+ BrokerNodeInfo: &types.BrokerNodeInfo{
+ BrokerId: aws.Float64(2),
+ ClientSubnet: strptr("subnet-67890"),
+ ClientVpcIpAddress: strptr("10.0.1.101"),
+ Endpoints: []string{"b-2.test-cluster.abc123.kafka.us-west-2.amazonaws.com"},
+ AttachedENIId: strptr("eni-67890"),
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "ClusterWithNoNodes",
+ mskData: &mskDataStore{
+ region: "us-west-2",
+ nodes: map[string][]types.NodeInfo{
+ "arn:aws:kafka:us-west-2:123456789012:cluster/empty-cluster/xyz-789": {},
+ },
+ },
+ clusters: []types.Cluster{
+ {
+ ClusterArn: strptr("arn:aws:kafka:us-west-2:123456789012:cluster/empty-cluster/xyz-789"),
+ },
+ },
+ expected: map[string][]types.NodeInfo{
+ "arn:aws:kafka:us-west-2:123456789012:cluster/empty-cluster/xyz-789": nil,
+ },
+ },
+ {
+ name: "MultipleClusters",
+ mskData: &mskDataStore{
+ region: "us-west-2",
+ nodes: map[string][]types.NodeInfo{
+ "arn:aws:kafka:us-west-2:123456789012:cluster/cluster-1/abc-123": {
+ {
+ NodeARN: strptr("arn:aws:kafka:us-west-2:123456789012:node/broker-1"),
+ InstanceType: strptr("kafka.m5.large"),
+ BrokerNodeInfo: &types.BrokerNodeInfo{
+ BrokerId: aws.Float64(1),
+ },
+ },
+ },
+ "arn:aws:kafka:us-west-2:123456789012:cluster/cluster-2/def-456": {
+ {
+ NodeARN: strptr("arn:aws:kafka:us-west-2:123456789012:node/broker-2"),
+ InstanceType: strptr("kafka.m5.xlarge"),
+ BrokerNodeInfo: &types.BrokerNodeInfo{
+ BrokerId: aws.Float64(2),
+ },
+ },
+ },
+ },
+ },
+ clusters: []types.Cluster{
+ {
+ ClusterArn: strptr("arn:aws:kafka:us-west-2:123456789012:cluster/cluster-1/abc-123"),
+ },
+ {
+ ClusterArn: strptr("arn:aws:kafka:us-west-2:123456789012:cluster/cluster-2/def-456"),
+ },
+ },
+ expected: map[string][]types.NodeInfo{
+ "arn:aws:kafka:us-west-2:123456789012:cluster/cluster-1/abc-123": {
+ {
+ NodeARN: strptr("arn:aws:kafka:us-west-2:123456789012:node/broker-1"),
+ InstanceType: strptr("kafka.m5.large"),
+ BrokerNodeInfo: &types.BrokerNodeInfo{
+ BrokerId: aws.Float64(1),
+ },
+ },
+ },
+ "arn:aws:kafka:us-west-2:123456789012:cluster/cluster-2/def-456": {
+ {
+ NodeARN: strptr("arn:aws:kafka:us-west-2:123456789012:node/broker-2"),
+ InstanceType: strptr("kafka.m5.xlarge"),
+ BrokerNodeInfo: &types.BrokerNodeInfo{
+ BrokerId: aws.Float64(2),
+ },
+ },
+ },
+ },
+ },
+ } {
+ t.Run(tt.name, func(t *testing.T) {
+ client := newMockMSKClient(tt.mskData)
+
+ d := &MSKDiscovery{
+ msk: client,
+ cfg: &MSKSDConfig{
+ Region: tt.mskData.region,
+ RequestConcurrency: 10,
+ },
+ }
+
+ nodes, err := d.listNodes(ctx, tt.clusters)
+ require.NoError(t, err)
+ require.Equal(t, tt.expected, nodes)
+ })
+ }
+}
+
+func TestMSKDiscoveryRefresh(t *testing.T) {
+ ctx := context.Background()
+
+ tests := []struct {
+ name string
+ mskData *mskDataStore
+ config *MSKSDConfig
+ expected []*targetgroup.Group
+ }{
+ {
+ name: "ClusterWithBrokersUsingClustersConfig",
+ mskData: &mskDataStore{
+ region: "us-west-2",
+ clusters: []types.Cluster{
+ {
+ ClusterName: strptr("test-cluster"),
+ ClusterArn: strptr("arn:aws:kafka:us-west-2:123456789012:cluster/test-cluster/abc-123"),
+ State: types.ClusterStateActive,
+ ClusterType: types.ClusterTypeProvisioned,
+ CurrentVersion: strptr("1.2.3"),
+ Tags: map[string]string{
+ "Environment": "production",
+ "Team": "platform",
+ },
+ Provisioned: &types.Provisioned{
+ CurrentBrokerSoftwareInfo: &types.BrokerSoftwareInfo{
+ ConfigurationArn: strptr("arn:aws:kafka:us-west-2:123456789012:configuration/my-config/abc-123"),
+ ConfigurationRevision: aws.Int64(1),
+ KafkaVersion: strptr("2.8.1"),
+ },
+ OpenMonitoring: &types.OpenMonitoringInfo{
+ Prometheus: &types.PrometheusInfo{
+ JmxExporter: &types.JmxExporterInfo{
+ EnabledInBroker: aws.Bool(true),
+ },
+ NodeExporter: &types.NodeExporterInfo{
+ EnabledInBroker: aws.Bool(true),
+ },
+ },
+ },
+ },
+ },
+ },
+ nodes: map[string][]types.NodeInfo{
+ "arn:aws:kafka:us-west-2:123456789012:cluster/test-cluster/abc-123": {
+ {
+ NodeARN: strptr("arn:aws:kafka:us-west-2:123456789012:node/broker-1"),
+ AddedToClusterTime: strptr("2023-01-01T00:00:00Z"),
+ InstanceType: strptr("kafka.m5.large"),
+ BrokerNodeInfo: &types.BrokerNodeInfo{
+ BrokerId: aws.Float64(1),
+ ClientSubnet: strptr("subnet-12345"),
+ ClientVpcIpAddress: strptr("10.0.1.100"),
+ Endpoints: []string{"b-1.test-cluster.abc123.kafka.us-west-2.amazonaws.com"},
+ AttachedENIId: strptr("eni-12345"),
+ },
+ },
+ },
+ },
+ },
+ config: &MSKSDConfig{
+ Region: "us-west-2",
+ Port: 80,
+ RequestConcurrency: 10,
+ Clusters: []string{"arn:aws:kafka:us-west-2:123456789012:cluster/test-cluster/abc-123"},
+ },
+ expected: []*targetgroup.Group{
+ {
+ Source: "us-west-2",
+ Targets: []model.LabelSet{
+ {
+ model.AddressLabel: model.LabelValue("b-1.test-cluster.abc123.kafka.us-west-2.amazonaws.com:80"),
+ "__meta_msk_cluster_name": model.LabelValue("test-cluster"),
+ "__meta_msk_cluster_arn": model.LabelValue("arn:aws:kafka:us-west-2:123456789012:cluster/test-cluster/abc-123"),
+ "__meta_msk_cluster_state": model.LabelValue("ACTIVE"),
+ "__meta_msk_cluster_type": model.LabelValue("PROVISIONED"),
+ "__meta_msk_cluster_version": model.LabelValue("1.2.3"),
+ "__meta_msk_cluster_jmx_exporter_enabled": model.LabelValue("true"),
+ "__meta_msk_cluster_configuration_arn": model.LabelValue("arn:aws:kafka:us-west-2:123456789012:configuration/my-config/abc-123"),
+ "__meta_msk_cluster_configuration_revision": model.LabelValue("1"),
+ "__meta_msk_cluster_kafka_version": model.LabelValue("2.8.1"),
+ "__meta_msk_cluster_tag_Environment": model.LabelValue("production"),
+ "__meta_msk_cluster_tag_Team": model.LabelValue("platform"),
+ "__meta_msk_node_type": model.LabelValue("BROKER"),
+ "__meta_msk_node_arn": model.LabelValue("arn:aws:kafka:us-west-2:123456789012:node/broker-1"),
+ "__meta_msk_node_added_time": model.LabelValue("2023-01-01T00:00:00Z"),
+ "__meta_msk_node_instance_type": model.LabelValue("kafka.m5.large"),
+ "__meta_msk_node_attached_eni": model.LabelValue("eni-12345"),
+ "__meta_msk_broker_id": model.LabelValue("1"),
+ "__meta_msk_broker_client_subnet": model.LabelValue("subnet-12345"),
+ "__meta_msk_broker_client_vpc_ip": model.LabelValue("10.0.1.100"),
+ "__meta_msk_broker_node_exporter_enabled": model.LabelValue("true"),
+ "__meta_msk_broker_endpoint_index": model.LabelValue("0"),
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "NoClustersWithEmptyClustersConfig",
+ mskData: &mskDataStore{
+ region: "us-east-1",
+ clusters: []types.Cluster{},
+ },
+ config: &MSKSDConfig{
+ Region: "us-east-1",
+ Port: 80,
+ RequestConcurrency: 10,
+ Clusters: []string{}, // Empty clusters list uses listClusters
+ },
+ expected: []*targetgroup.Group{
+ {
+ Source: "us-east-1",
+ },
+ },
+ },
+ {
+ name: "ClusterWithBrokersUsingListClusters",
+ mskData: &mskDataStore{
+ region: "us-west-2",
+ clusters: []types.Cluster{
+ {
+ ClusterName: strptr("auto-discovered-cluster"),
+ ClusterArn: strptr("arn:aws:kafka:us-west-2:123456789012:cluster/auto-discovered-cluster/xyz-123"),
+ State: types.ClusterStateActive,
+ ClusterType: types.ClusterTypeProvisioned,
+ CurrentVersion: strptr("1.0.0"),
+ Provisioned: &types.Provisioned{
+ CurrentBrokerSoftwareInfo: &types.BrokerSoftwareInfo{
+ ConfigurationArn: strptr("arn:aws:kafka:us-west-2:123456789012:configuration/config/xyz"),
+ ConfigurationRevision: aws.Int64(1),
+ KafkaVersion: strptr("3.3.1"),
+ },
+ OpenMonitoring: &types.OpenMonitoringInfo{
+ Prometheus: &types.PrometheusInfo{
+ JmxExporter: &types.JmxExporterInfo{
+ EnabledInBroker: aws.Bool(true),
+ },
+ NodeExporter: &types.NodeExporterInfo{
+ EnabledInBroker: aws.Bool(true),
+ },
+ },
+ },
+ },
+ },
+ },
+ nodes: map[string][]types.NodeInfo{
+ "arn:aws:kafka:us-west-2:123456789012:cluster/auto-discovered-cluster/xyz-123": {
+ {
+ NodeARN: strptr("arn:aws:kafka:us-west-2:123456789012:node/broker-auto"),
+ AddedToClusterTime: strptr("2023-01-01T00:00:00Z"),
+ InstanceType: strptr("kafka.m5.large"),
+ BrokerNodeInfo: &types.BrokerNodeInfo{
+ BrokerId: aws.Float64(1),
+ ClientSubnet: strptr("subnet-auto"),
+ ClientVpcIpAddress: strptr("10.0.1.200"),
+ Endpoints: []string{"b-auto.cluster.kafka.us-west-2.amazonaws.com"},
+ AttachedENIId: strptr("eni-auto"),
+ },
+ },
+ },
+ },
+ },
+ config: &MSKSDConfig{
+ Region: "us-west-2",
+ Port: 80,
+ RequestConcurrency: 10,
+ Clusters: nil, // nil clusters list uses listClusters (backward compatibility)
+ },
+ expected: []*targetgroup.Group{
+ {
+ Source: "us-west-2",
+ Targets: []model.LabelSet{
+ {
+ model.AddressLabel: model.LabelValue("b-auto.cluster.kafka.us-west-2.amazonaws.com:80"),
+ "__meta_msk_cluster_name": model.LabelValue("auto-discovered-cluster"),
+ "__meta_msk_cluster_arn": model.LabelValue("arn:aws:kafka:us-west-2:123456789012:cluster/auto-discovered-cluster/xyz-123"),
+ "__meta_msk_cluster_state": model.LabelValue("ACTIVE"),
+ "__meta_msk_cluster_type": model.LabelValue("PROVISIONED"),
+ "__meta_msk_cluster_version": model.LabelValue("1.0.0"),
+ "__meta_msk_cluster_jmx_exporter_enabled": model.LabelValue("true"),
+ "__meta_msk_cluster_configuration_arn": model.LabelValue("arn:aws:kafka:us-west-2:123456789012:configuration/config/xyz"),
+ "__meta_msk_cluster_configuration_revision": model.LabelValue("1"),
+ "__meta_msk_cluster_kafka_version": model.LabelValue("3.3.1"),
+ "__meta_msk_node_type": model.LabelValue("BROKER"),
+ "__meta_msk_node_arn": model.LabelValue("arn:aws:kafka:us-west-2:123456789012:node/broker-auto"),
+ "__meta_msk_node_added_time": model.LabelValue("2023-01-01T00:00:00Z"),
+ "__meta_msk_node_instance_type": model.LabelValue("kafka.m5.large"),
+ "__meta_msk_node_attached_eni": model.LabelValue("eni-auto"),
+ "__meta_msk_broker_id": model.LabelValue("1"),
+ "__meta_msk_broker_client_subnet": model.LabelValue("subnet-auto"),
+ "__meta_msk_broker_client_vpc_ip": model.LabelValue("10.0.1.200"),
+ "__meta_msk_broker_node_exporter_enabled": model.LabelValue("true"),
+ "__meta_msk_broker_endpoint_index": model.LabelValue("0"),
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "ClusterWithBrokersAndControllersUsingClustersConfig",
+ mskData: &mskDataStore{
+ region: "us-west-2",
+ clusters: []types.Cluster{
+ {
+ ClusterName: strptr("kraft-cluster"),
+ ClusterArn: strptr("arn:aws:kafka:us-west-2:123456789012:cluster/kraft-cluster/xyz-789"),
+ State: types.ClusterStateActive,
+ ClusterType: types.ClusterTypeProvisioned,
+ CurrentVersion: strptr("1.0.0"),
+ Tags: map[string]string{
+ "Type": "kraft",
+ },
+ Provisioned: &types.Provisioned{
+ CurrentBrokerSoftwareInfo: &types.BrokerSoftwareInfo{
+ ConfigurationArn: strptr("arn:aws:kafka:us-west-2:123456789012:configuration/config/xyz"),
+ ConfigurationRevision: aws.Int64(2),
+ KafkaVersion: strptr("3.3.1"),
+ },
+ OpenMonitoring: &types.OpenMonitoringInfo{
+ Prometheus: &types.PrometheusInfo{
+ JmxExporter: &types.JmxExporterInfo{
+ EnabledInBroker: aws.Bool(true),
+ },
+ NodeExporter: &types.NodeExporterInfo{
+ EnabledInBroker: aws.Bool(false),
+ },
+ },
+ },
+ },
+ },
+ },
+ nodes: map[string][]types.NodeInfo{
+ "arn:aws:kafka:us-west-2:123456789012:cluster/kraft-cluster/xyz-789": {
+ {
+ NodeARN: strptr("arn:aws:kafka:us-west-2:123456789012:node/broker-1"),
+ AddedToClusterTime: strptr("2023-06-01T00:00:00Z"),
+ InstanceType: strptr("kafka.m5.large"),
+ BrokerNodeInfo: &types.BrokerNodeInfo{
+ BrokerId: aws.Float64(1),
+ ClientSubnet: strptr("subnet-abc123"),
+ ClientVpcIpAddress: strptr("10.0.2.100"),
+ Endpoints: []string{"b-1.kraft-cluster.xyz789.kafka.us-west-2.amazonaws.com"},
+ AttachedENIId: strptr("eni-broker-1"),
+ },
+ },
+ {
+ NodeARN: strptr("arn:aws:kafka:us-west-2:123456789012:node/broker-2"),
+ AddedToClusterTime: strptr("2023-06-01T00:00:00Z"),
+ InstanceType: strptr("kafka.m5.large"),
+ BrokerNodeInfo: &types.BrokerNodeInfo{
+ BrokerId: aws.Float64(2),
+ ClientSubnet: strptr("subnet-abc124"),
+ ClientVpcIpAddress: strptr("10.0.2.101"),
+ Endpoints: []string{"b-2.kraft-cluster.xyz789.kafka.us-west-2.amazonaws.com"},
+ AttachedENIId: strptr("eni-broker-2"),
+ },
+ },
+ {
+ NodeARN: strptr("arn:aws:kafka:us-west-2:123456789012:node/controller-1"),
+ AddedToClusterTime: strptr("2023-06-01T00:00:00Z"),
+ InstanceType: strptr("kafka.m5.large"),
+ ControllerNodeInfo: &types.ControllerNodeInfo{
+ Endpoints: []string{"c-1.kraft-cluster.xyz789.kafka.us-west-2.amazonaws.com"},
+ },
+ },
+ {
+ NodeARN: strptr("arn:aws:kafka:us-west-2:123456789012:node/controller-2"),
+ AddedToClusterTime: strptr("2023-06-01T00:00:00Z"),
+ InstanceType: strptr("kafka.m5.large"),
+ ControllerNodeInfo: &types.ControllerNodeInfo{
+ Endpoints: []string{"c-2.kraft-cluster.xyz789.kafka.us-west-2.amazonaws.com"},
+ },
+ },
+ },
+ },
+ },
+ config: &MSKSDConfig{
+ Region: "us-west-2",
+ Port: 80,
+ RequestConcurrency: 10,
+ Clusters: []string{"arn:aws:kafka:us-west-2:123456789012:cluster/kraft-cluster/xyz-789"},
+ },
+ expected: []*targetgroup.Group{
+ {
+ Source: "us-west-2",
+ Targets: []model.LabelSet{
+ {
+ model.AddressLabel: model.LabelValue("b-1.kraft-cluster.xyz789.kafka.us-west-2.amazonaws.com:80"),
+ "__meta_msk_cluster_name": model.LabelValue("kraft-cluster"),
+ "__meta_msk_cluster_arn": model.LabelValue("arn:aws:kafka:us-west-2:123456789012:cluster/kraft-cluster/xyz-789"),
+ "__meta_msk_cluster_state": model.LabelValue("ACTIVE"),
+ "__meta_msk_cluster_type": model.LabelValue("PROVISIONED"),
+ "__meta_msk_cluster_version": model.LabelValue("1.0.0"),
+ "__meta_msk_cluster_jmx_exporter_enabled": model.LabelValue("true"),
+ "__meta_msk_cluster_configuration_arn": model.LabelValue("arn:aws:kafka:us-west-2:123456789012:configuration/config/xyz"),
+ "__meta_msk_cluster_configuration_revision": model.LabelValue("2"),
+ "__meta_msk_cluster_kafka_version": model.LabelValue("3.3.1"),
+ "__meta_msk_cluster_tag_Type": model.LabelValue("kraft"),
+ "__meta_msk_node_type": model.LabelValue("BROKER"),
+ "__meta_msk_node_arn": model.LabelValue("arn:aws:kafka:us-west-2:123456789012:node/broker-1"),
+ "__meta_msk_node_added_time": model.LabelValue("2023-06-01T00:00:00Z"),
+ "__meta_msk_node_instance_type": model.LabelValue("kafka.m5.large"),
+ "__meta_msk_node_attached_eni": model.LabelValue("eni-broker-1"),
+ "__meta_msk_broker_id": model.LabelValue("1"),
+ "__meta_msk_broker_client_subnet": model.LabelValue("subnet-abc123"),
+ "__meta_msk_broker_client_vpc_ip": model.LabelValue("10.0.2.100"),
+ "__meta_msk_broker_node_exporter_enabled": model.LabelValue("false"),
+ "__meta_msk_broker_endpoint_index": model.LabelValue("0"),
+ },
+ {
+ model.AddressLabel: model.LabelValue("b-2.kraft-cluster.xyz789.kafka.us-west-2.amazonaws.com:80"),
+ "__meta_msk_cluster_name": model.LabelValue("kraft-cluster"),
+ "__meta_msk_cluster_arn": model.LabelValue("arn:aws:kafka:us-west-2:123456789012:cluster/kraft-cluster/xyz-789"),
+ "__meta_msk_cluster_state": model.LabelValue("ACTIVE"),
+ "__meta_msk_cluster_type": model.LabelValue("PROVISIONED"),
+ "__meta_msk_cluster_version": model.LabelValue("1.0.0"),
+ "__meta_msk_cluster_jmx_exporter_enabled": model.LabelValue("true"),
+ "__meta_msk_cluster_configuration_arn": model.LabelValue("arn:aws:kafka:us-west-2:123456789012:configuration/config/xyz"),
+ "__meta_msk_cluster_configuration_revision": model.LabelValue("2"),
+ "__meta_msk_cluster_kafka_version": model.LabelValue("3.3.1"),
+ "__meta_msk_cluster_tag_Type": model.LabelValue("kraft"),
+ "__meta_msk_node_type": model.LabelValue("BROKER"),
+ "__meta_msk_node_arn": model.LabelValue("arn:aws:kafka:us-west-2:123456789012:node/broker-2"),
+ "__meta_msk_node_added_time": model.LabelValue("2023-06-01T00:00:00Z"),
+ "__meta_msk_node_instance_type": model.LabelValue("kafka.m5.large"),
+ "__meta_msk_node_attached_eni": model.LabelValue("eni-broker-2"),
+ "__meta_msk_broker_id": model.LabelValue("2"),
+ "__meta_msk_broker_client_subnet": model.LabelValue("subnet-abc124"),
+ "__meta_msk_broker_client_vpc_ip": model.LabelValue("10.0.2.101"),
+ "__meta_msk_broker_node_exporter_enabled": model.LabelValue("false"),
+ "__meta_msk_broker_endpoint_index": model.LabelValue("0"),
+ },
+ {
+ model.AddressLabel: model.LabelValue("c-1.kraft-cluster.xyz789.kafka.us-west-2.amazonaws.com:80"),
+ "__meta_msk_cluster_name": model.LabelValue("kraft-cluster"),
+ "__meta_msk_cluster_arn": model.LabelValue("arn:aws:kafka:us-west-2:123456789012:cluster/kraft-cluster/xyz-789"),
+ "__meta_msk_cluster_state": model.LabelValue("ACTIVE"),
+ "__meta_msk_cluster_type": model.LabelValue("PROVISIONED"),
+ "__meta_msk_cluster_version": model.LabelValue("1.0.0"),
+ "__meta_msk_cluster_jmx_exporter_enabled": model.LabelValue("true"),
+ "__meta_msk_cluster_configuration_arn": model.LabelValue("arn:aws:kafka:us-west-2:123456789012:configuration/config/xyz"),
+ "__meta_msk_cluster_configuration_revision": model.LabelValue("2"),
+ "__meta_msk_cluster_kafka_version": model.LabelValue("3.3.1"),
+ "__meta_msk_cluster_tag_Type": model.LabelValue("kraft"),
+ "__meta_msk_node_type": model.LabelValue("CONTROLLER"),
+ "__meta_msk_node_arn": model.LabelValue("arn:aws:kafka:us-west-2:123456789012:node/controller-1"),
+ "__meta_msk_node_added_time": model.LabelValue("2023-06-01T00:00:00Z"),
+ "__meta_msk_node_instance_type": model.LabelValue("kafka.m5.large"),
+ "__meta_msk_controller_endpoint_index": model.LabelValue("0"),
+ },
+ {
+ model.AddressLabel: model.LabelValue("c-2.kraft-cluster.xyz789.kafka.us-west-2.amazonaws.com:80"),
+ "__meta_msk_cluster_name": model.LabelValue("kraft-cluster"),
+ "__meta_msk_cluster_arn": model.LabelValue("arn:aws:kafka:us-west-2:123456789012:cluster/kraft-cluster/xyz-789"),
+ "__meta_msk_cluster_state": model.LabelValue("ACTIVE"),
+ "__meta_msk_cluster_type": model.LabelValue("PROVISIONED"),
+ "__meta_msk_cluster_version": model.LabelValue("1.0.0"),
+ "__meta_msk_cluster_jmx_exporter_enabled": model.LabelValue("true"),
+ "__meta_msk_cluster_configuration_arn": model.LabelValue("arn:aws:kafka:us-west-2:123456789012:configuration/config/xyz"),
+ "__meta_msk_cluster_configuration_revision": model.LabelValue("2"),
+ "__meta_msk_cluster_kafka_version": model.LabelValue("3.3.1"),
+ "__meta_msk_cluster_tag_Type": model.LabelValue("kraft"),
+ "__meta_msk_node_type": model.LabelValue("CONTROLLER"),
+ "__meta_msk_node_arn": model.LabelValue("arn:aws:kafka:us-west-2:123456789012:node/controller-2"),
+ "__meta_msk_node_added_time": model.LabelValue("2023-06-01T00:00:00Z"),
+ "__meta_msk_node_instance_type": model.LabelValue("kafka.m5.large"),
+ "__meta_msk_controller_endpoint_index": model.LabelValue("0"),
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "NodesWithMultipleEndpointsUsingClustersConfig",
+ mskData: &mskDataStore{
+ region: "us-east-1",
+ clusters: []types.Cluster{
+ {
+ ClusterName: strptr("multi-endpoint-cluster"),
+ ClusterArn: strptr("arn:aws:kafka:us-east-1:123456789012:cluster/multi-endpoint-cluster/abc-999"),
+ State: types.ClusterStateActive,
+ ClusterType: types.ClusterTypeProvisioned,
+ CurrentVersion: strptr("2.0.0"),
+ Provisioned: &types.Provisioned{
+ CurrentBrokerSoftwareInfo: &types.BrokerSoftwareInfo{
+ ConfigurationArn: strptr("arn:aws:kafka:us-east-1:123456789012:configuration/config/abc"),
+ ConfigurationRevision: aws.Int64(1),
+ KafkaVersion: strptr("3.4.0"),
+ },
+ OpenMonitoring: &types.OpenMonitoringInfo{
+ Prometheus: &types.PrometheusInfo{
+ JmxExporter: &types.JmxExporterInfo{
+ EnabledInBroker: aws.Bool(true),
+ },
+ NodeExporter: &types.NodeExporterInfo{
+ EnabledInBroker: aws.Bool(true),
+ },
+ },
+ },
+ },
+ },
+ },
+ nodes: map[string][]types.NodeInfo{
+ "arn:aws:kafka:us-east-1:123456789012:cluster/multi-endpoint-cluster/abc-999": {
+ {
+ NodeARN: strptr("arn:aws:kafka:us-east-1:123456789012:node/broker-multi"),
+ AddedToClusterTime: strptr("2023-08-01T00:00:00Z"),
+ InstanceType: strptr("kafka.m5.xlarge"),
+ BrokerNodeInfo: &types.BrokerNodeInfo{
+ BrokerId: aws.Float64(3),
+ ClientSubnet: strptr("subnet-multi-1"),
+ ClientVpcIpAddress: strptr("10.0.3.50"),
+ // Multiple endpoints for this broker
+ Endpoints: []string{"b-3-1.cluster.kafka.us-east-1.amazonaws.com", "b-3-2.cluster.kafka.us-east-1.amazonaws.com", "b-3-3.cluster.kafka.us-east-1.amazonaws.com"},
+ AttachedENIId: strptr("eni-multi-broker"),
+ },
+ },
+ {
+ NodeARN: strptr("arn:aws:kafka:us-east-1:123456789012:node/controller-multi"),
+ AddedToClusterTime: strptr("2023-08-01T00:00:00Z"),
+ InstanceType: strptr("kafka.m5.large"),
+ ControllerNodeInfo: &types.ControllerNodeInfo{
+ // Multiple endpoints for this controller
+ Endpoints: []string{"c-1-1.cluster.kafka.us-east-1.amazonaws.com", "c-1-2.cluster.kafka.us-east-1.amazonaws.com", "c-1-3.cluster.kafka.us-east-1.amazonaws.com", "c-1-4.cluster.kafka.us-east-1.amazonaws.com"},
+ },
+ },
+ },
+ },
+ },
+ config: &MSKSDConfig{
+ Region: "us-east-1",
+ Port: 80,
+ RequestConcurrency: 10,
+ Clusters: []string{"arn:aws:kafka:us-east-1:123456789012:cluster/multi-endpoint-cluster/abc-999"},
+ },
+ expected: []*targetgroup.Group{
+ {
+ Source: "us-east-1",
+ Targets: []model.LabelSet{
+ // Broker with 3 endpoints - creates 3 targets with different endpoint indices
+ {
+ model.AddressLabel: model.LabelValue("b-3-1.cluster.kafka.us-east-1.amazonaws.com:80"),
+ "__meta_msk_cluster_name": model.LabelValue("multi-endpoint-cluster"),
+ "__meta_msk_cluster_arn": model.LabelValue("arn:aws:kafka:us-east-1:123456789012:cluster/multi-endpoint-cluster/abc-999"),
+ "__meta_msk_cluster_state": model.LabelValue("ACTIVE"),
+ "__meta_msk_cluster_type": model.LabelValue("PROVISIONED"),
+ "__meta_msk_cluster_version": model.LabelValue("2.0.0"),
+ "__meta_msk_cluster_jmx_exporter_enabled": model.LabelValue("true"),
+ "__meta_msk_cluster_configuration_arn": model.LabelValue("arn:aws:kafka:us-east-1:123456789012:configuration/config/abc"),
+ "__meta_msk_cluster_configuration_revision": model.LabelValue("1"),
+ "__meta_msk_cluster_kafka_version": model.LabelValue("3.4.0"),
+ "__meta_msk_node_type": model.LabelValue("BROKER"),
+ "__meta_msk_node_arn": model.LabelValue("arn:aws:kafka:us-east-1:123456789012:node/broker-multi"),
+ "__meta_msk_node_added_time": model.LabelValue("2023-08-01T00:00:00Z"),
+ "__meta_msk_node_instance_type": model.LabelValue("kafka.m5.xlarge"),
+ "__meta_msk_node_attached_eni": model.LabelValue("eni-multi-broker"),
+ "__meta_msk_broker_id": model.LabelValue("3"),
+ "__meta_msk_broker_client_subnet": model.LabelValue("subnet-multi-1"),
+ "__meta_msk_broker_client_vpc_ip": model.LabelValue("10.0.3.50"),
+ "__meta_msk_broker_node_exporter_enabled": model.LabelValue("true"),
+ "__meta_msk_broker_endpoint_index": model.LabelValue("0"),
+ },
+ {
+ model.AddressLabel: model.LabelValue("b-3-2.cluster.kafka.us-east-1.amazonaws.com:80"),
+ "__meta_msk_cluster_name": model.LabelValue("multi-endpoint-cluster"),
+ "__meta_msk_cluster_arn": model.LabelValue("arn:aws:kafka:us-east-1:123456789012:cluster/multi-endpoint-cluster/abc-999"),
+ "__meta_msk_cluster_state": model.LabelValue("ACTIVE"),
+ "__meta_msk_cluster_type": model.LabelValue("PROVISIONED"),
+ "__meta_msk_cluster_version": model.LabelValue("2.0.0"),
+ "__meta_msk_cluster_jmx_exporter_enabled": model.LabelValue("true"),
+ "__meta_msk_cluster_configuration_arn": model.LabelValue("arn:aws:kafka:us-east-1:123456789012:configuration/config/abc"),
+ "__meta_msk_cluster_configuration_revision": model.LabelValue("1"),
+ "__meta_msk_cluster_kafka_version": model.LabelValue("3.4.0"),
+ "__meta_msk_node_type": model.LabelValue("BROKER"),
+ "__meta_msk_node_arn": model.LabelValue("arn:aws:kafka:us-east-1:123456789012:node/broker-multi"),
+ "__meta_msk_node_added_time": model.LabelValue("2023-08-01T00:00:00Z"),
+ "__meta_msk_node_instance_type": model.LabelValue("kafka.m5.xlarge"),
+ "__meta_msk_node_attached_eni": model.LabelValue("eni-multi-broker"),
+ "__meta_msk_broker_id": model.LabelValue("3"),
+ "__meta_msk_broker_client_subnet": model.LabelValue("subnet-multi-1"),
+ "__meta_msk_broker_client_vpc_ip": model.LabelValue("10.0.3.50"),
+ "__meta_msk_broker_node_exporter_enabled": model.LabelValue("true"),
+ "__meta_msk_broker_endpoint_index": model.LabelValue("1"),
+ },
+ {
+ model.AddressLabel: model.LabelValue("b-3-3.cluster.kafka.us-east-1.amazonaws.com:80"),
+ "__meta_msk_cluster_name": model.LabelValue("multi-endpoint-cluster"),
+ "__meta_msk_cluster_arn": model.LabelValue("arn:aws:kafka:us-east-1:123456789012:cluster/multi-endpoint-cluster/abc-999"),
+ "__meta_msk_cluster_state": model.LabelValue("ACTIVE"),
+ "__meta_msk_cluster_type": model.LabelValue("PROVISIONED"),
+ "__meta_msk_cluster_version": model.LabelValue("2.0.0"),
+ "__meta_msk_cluster_jmx_exporter_enabled": model.LabelValue("true"),
+ "__meta_msk_cluster_configuration_arn": model.LabelValue("arn:aws:kafka:us-east-1:123456789012:configuration/config/abc"),
+ "__meta_msk_cluster_configuration_revision": model.LabelValue("1"),
+ "__meta_msk_cluster_kafka_version": model.LabelValue("3.4.0"),
+ "__meta_msk_node_type": model.LabelValue("BROKER"),
+ "__meta_msk_node_arn": model.LabelValue("arn:aws:kafka:us-east-1:123456789012:node/broker-multi"),
+ "__meta_msk_node_added_time": model.LabelValue("2023-08-01T00:00:00Z"),
+ "__meta_msk_node_instance_type": model.LabelValue("kafka.m5.xlarge"),
+ "__meta_msk_node_attached_eni": model.LabelValue("eni-multi-broker"),
+ "__meta_msk_broker_id": model.LabelValue("3"),
+ "__meta_msk_broker_client_subnet": model.LabelValue("subnet-multi-1"),
+ "__meta_msk_broker_client_vpc_ip": model.LabelValue("10.0.3.50"),
+ "__meta_msk_broker_node_exporter_enabled": model.LabelValue("true"),
+ "__meta_msk_broker_endpoint_index": model.LabelValue("2"),
+ },
+ // Controller with 4 endpoints - creates 4 targets with different endpoint indices
+ {
+ model.AddressLabel: model.LabelValue("c-1-1.cluster.kafka.us-east-1.amazonaws.com:80"),
+ "__meta_msk_cluster_name": model.LabelValue("multi-endpoint-cluster"),
+ "__meta_msk_cluster_arn": model.LabelValue("arn:aws:kafka:us-east-1:123456789012:cluster/multi-endpoint-cluster/abc-999"),
+ "__meta_msk_cluster_state": model.LabelValue("ACTIVE"),
+ "__meta_msk_cluster_type": model.LabelValue("PROVISIONED"),
+ "__meta_msk_cluster_version": model.LabelValue("2.0.0"),
+ "__meta_msk_cluster_jmx_exporter_enabled": model.LabelValue("true"),
+ "__meta_msk_cluster_configuration_arn": model.LabelValue("arn:aws:kafka:us-east-1:123456789012:configuration/config/abc"),
+ "__meta_msk_cluster_configuration_revision": model.LabelValue("1"),
+ "__meta_msk_cluster_kafka_version": model.LabelValue("3.4.0"),
+ "__meta_msk_node_type": model.LabelValue("CONTROLLER"),
+ "__meta_msk_node_arn": model.LabelValue("arn:aws:kafka:us-east-1:123456789012:node/controller-multi"),
+ "__meta_msk_node_added_time": model.LabelValue("2023-08-01T00:00:00Z"),
+ "__meta_msk_node_instance_type": model.LabelValue("kafka.m5.large"),
+ "__meta_msk_controller_endpoint_index": model.LabelValue("0"),
+ },
+ {
+ model.AddressLabel: model.LabelValue("c-1-2.cluster.kafka.us-east-1.amazonaws.com:80"),
+ "__meta_msk_cluster_name": model.LabelValue("multi-endpoint-cluster"),
+ "__meta_msk_cluster_arn": model.LabelValue("arn:aws:kafka:us-east-1:123456789012:cluster/multi-endpoint-cluster/abc-999"),
+ "__meta_msk_cluster_state": model.LabelValue("ACTIVE"),
+ "__meta_msk_cluster_type": model.LabelValue("PROVISIONED"),
+ "__meta_msk_cluster_version": model.LabelValue("2.0.0"),
+ "__meta_msk_cluster_jmx_exporter_enabled": model.LabelValue("true"),
+ "__meta_msk_cluster_configuration_arn": model.LabelValue("arn:aws:kafka:us-east-1:123456789012:configuration/config/abc"),
+ "__meta_msk_cluster_configuration_revision": model.LabelValue("1"),
+ "__meta_msk_cluster_kafka_version": model.LabelValue("3.4.0"),
+ "__meta_msk_node_type": model.LabelValue("CONTROLLER"),
+ "__meta_msk_node_arn": model.LabelValue("arn:aws:kafka:us-east-1:123456789012:node/controller-multi"),
+ "__meta_msk_node_added_time": model.LabelValue("2023-08-01T00:00:00Z"),
+ "__meta_msk_node_instance_type": model.LabelValue("kafka.m5.large"),
+ "__meta_msk_controller_endpoint_index": model.LabelValue("1"),
+ },
+ {
+ model.AddressLabel: model.LabelValue("c-1-3.cluster.kafka.us-east-1.amazonaws.com:80"),
+ "__meta_msk_cluster_name": model.LabelValue("multi-endpoint-cluster"),
+ "__meta_msk_cluster_arn": model.LabelValue("arn:aws:kafka:us-east-1:123456789012:cluster/multi-endpoint-cluster/abc-999"),
+ "__meta_msk_cluster_state": model.LabelValue("ACTIVE"),
+ "__meta_msk_cluster_type": model.LabelValue("PROVISIONED"),
+ "__meta_msk_cluster_version": model.LabelValue("2.0.0"),
+ "__meta_msk_cluster_jmx_exporter_enabled": model.LabelValue("true"),
+ "__meta_msk_cluster_configuration_arn": model.LabelValue("arn:aws:kafka:us-east-1:123456789012:configuration/config/abc"),
+ "__meta_msk_cluster_configuration_revision": model.LabelValue("1"),
+ "__meta_msk_cluster_kafka_version": model.LabelValue("3.4.0"),
+ "__meta_msk_node_type": model.LabelValue("CONTROLLER"),
+ "__meta_msk_node_arn": model.LabelValue("arn:aws:kafka:us-east-1:123456789012:node/controller-multi"),
+ "__meta_msk_node_added_time": model.LabelValue("2023-08-01T00:00:00Z"),
+ "__meta_msk_node_instance_type": model.LabelValue("kafka.m5.large"),
+ "__meta_msk_controller_endpoint_index": model.LabelValue("2"),
+ },
+ {
+ model.AddressLabel: model.LabelValue("c-1-4.cluster.kafka.us-east-1.amazonaws.com:80"),
+ "__meta_msk_cluster_name": model.LabelValue("multi-endpoint-cluster"),
+ "__meta_msk_cluster_arn": model.LabelValue("arn:aws:kafka:us-east-1:123456789012:cluster/multi-endpoint-cluster/abc-999"),
+ "__meta_msk_cluster_state": model.LabelValue("ACTIVE"),
+ "__meta_msk_cluster_type": model.LabelValue("PROVISIONED"),
+ "__meta_msk_cluster_version": model.LabelValue("2.0.0"),
+ "__meta_msk_cluster_jmx_exporter_enabled": model.LabelValue("true"),
+ "__meta_msk_cluster_configuration_arn": model.LabelValue("arn:aws:kafka:us-east-1:123456789012:configuration/config/abc"),
+ "__meta_msk_cluster_configuration_revision": model.LabelValue("1"),
+ "__meta_msk_cluster_kafka_version": model.LabelValue("3.4.0"),
+ "__meta_msk_node_type": model.LabelValue("CONTROLLER"),
+ "__meta_msk_node_arn": model.LabelValue("arn:aws:kafka:us-east-1:123456789012:node/controller-multi"),
+ "__meta_msk_node_added_time": model.LabelValue("2023-08-01T00:00:00Z"),
+ "__meta_msk_node_instance_type": model.LabelValue("kafka.m5.large"),
+ "__meta_msk_controller_endpoint_index": model.LabelValue("3"),
+ },
+ },
+ },
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ client := newMockMSKClient(tt.mskData)
+
+ config := tt.config
+ if config == nil {
+ // Default config for backward compatibility
+ config = &MSKSDConfig{
+ Region: tt.mskData.region,
+ Port: 80,
+ RequestConcurrency: 10,
+ }
+ }
+
+ d := &MSKDiscovery{
+ msk: client,
+ cfg: config,
+ }
+
+ groups, err := d.refresh(ctx)
+ require.NoError(t, err)
+
+ // Sort targets within each group by address to handle non-deterministic ordering from goroutines
+ for _, group := range groups {
+ if len(group.Targets) > 0 {
+ sort.Slice(group.Targets, func(i, j int) bool {
+ return string(group.Targets[i][model.AddressLabel]) < string(group.Targets[j][model.AddressLabel])
+ })
+ }
+ }
+ for _, group := range tt.expected {
+ if len(group.Targets) > 0 {
+ sort.Slice(group.Targets, func(i, j int) bool {
+ return string(group.Targets[i][model.AddressLabel]) < string(group.Targets[j][model.AddressLabel])
+ })
+ }
+ }
+
+ require.Equal(t, tt.expected, groups)
+ })
+ }
+}
+
+func TestNodeType(t *testing.T) {
+ tests := []struct {
+ name string
+ node types.NodeInfo
+ expected NodeType
+ }{
+ {
+ name: "BrokerNode",
+ node: types.NodeInfo{
+ BrokerNodeInfo: &types.BrokerNodeInfo{},
+ },
+ expected: NodeTypeBroker,
+ },
+ {
+ name: "ControllerNode",
+ node: types.NodeInfo{
+ ControllerNodeInfo: &types.ControllerNodeInfo{},
+ },
+ expected: NodeTypeController,
+ },
+ {
+ name: "UnknownNode",
+ node: types.NodeInfo{},
+ expected: "",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result := nodeType(tt.node)
+ require.Equal(t, tt.expected, result)
+ })
+ }
+}
+
+// MSK client mock.
+type mockMSKClient struct {
+ mskData mskDataStore
+}
+
+func newMockMSKClient(mskData *mskDataStore) *mockMSKClient {
+ return &mockMSKClient{
+ mskData: *mskData,
+ }
+}
+
+func (m *mockMSKClient) DescribeClusterV2(_ context.Context, input *kafka.DescribeClusterV2Input, _ ...func(*kafka.Options)) (*kafka.DescribeClusterV2Output, error) {
+ inputARN := aws.ToString(input.ClusterArn)
+ for i := range m.mskData.clusters {
+ cluster := &m.mskData.clusters[i]
+ if aws.ToString(cluster.ClusterArn) == inputARN {
+ return &kafka.DescribeClusterV2Output{
+ ClusterInfo: cluster,
+ }, nil
+ }
+ }
+
+ return nil, fmt.Errorf("cluster not found: %s", inputARN)
+}
+
+func (m *mockMSKClient) ListClustersV2(_ context.Context, input *kafka.ListClustersV2Input, _ ...func(*kafka.Options)) (*kafka.ListClustersV2Output, error) {
+ var clusters []types.Cluster
+
+ for _, cluster := range m.mskData.clusters {
+ // Apply cluster name filter if specified
+ if input.ClusterNameFilter != nil && *input.ClusterNameFilter != "" {
+ if cluster.ClusterName != nil && *cluster.ClusterName != *input.ClusterNameFilter {
+ continue
+ }
+ }
+
+ // Apply cluster type filter if specified
+ if input.ClusterTypeFilter != nil && *input.ClusterTypeFilter != "" {
+ if string(cluster.ClusterType) != *input.ClusterTypeFilter {
+ continue
+ }
+ }
+
+ clusters = append(clusters, cluster)
+ }
+
+ return &kafka.ListClustersV2Output{
+ ClusterInfoList: clusters,
+ }, nil
+}
+
+func (m *mockMSKClient) ListNodes(_ context.Context, input *kafka.ListNodesInput, _ ...func(*kafka.Options)) (*kafka.ListNodesOutput, error) {
+ clusterARN := aws.ToString(input.ClusterArn)
+ nodes, exists := m.mskData.nodes[clusterARN]
+ if !exists {
+ return &kafka.ListNodesOutput{
+ NodeInfoList: nil,
+ }, nil
+ }
+
+ return &kafka.ListNodesOutput{
+ NodeInfoList: nodes,
+ }, nil
+}
diff --git a/discovery/azure/azure.go b/discovery/azure/azure.go
index bed4861787..32fc97fdfa 100644
--- a/discovery/azure/azure.go
+++ b/discovery/azure/azure.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
@@ -127,7 +127,7 @@ func (*SDConfig) Name() string { return "azure" }
// NewDiscoverer returns a Discoverer for the Config.
func (c *SDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) {
- return NewDiscovery(c, opts.Logger, opts.Metrics)
+ return NewDiscovery(c, opts)
}
func validateAuthParam(param, name string) error {
@@ -178,28 +178,29 @@ type Discovery struct {
}
// NewDiscovery returns a new AzureDiscovery which periodically refreshes its targets.
-func NewDiscovery(cfg *SDConfig, logger *slog.Logger, metrics discovery.DiscovererMetrics) (*Discovery, error) {
- m, ok := metrics.(*azureMetrics)
+func NewDiscovery(cfg *SDConfig, opts discovery.DiscovererOptions) (*Discovery, error) {
+ m, ok := opts.Metrics.(*azureMetrics)
if !ok {
return nil, errors.New("invalid discovery metrics type")
}
- if logger == nil {
- logger = promslog.NewNopLogger()
+ if opts.Logger == nil {
+ opts.Logger = promslog.NewNopLogger()
}
l := cache.New(cache.AsLRU[string, *armnetwork.Interface](lru.WithCapacity(5000)))
d := &Discovery{
cfg: cfg,
port: cfg.Port,
- logger: logger,
+ logger: opts.Logger,
cache: l,
metrics: m,
}
d.Discovery = refresh.NewDiscovery(
refresh.Options{
- Logger: logger,
+ Logger: opts.Logger,
Mech: "azure",
+ SetName: opts.SetName,
Interval: time.Duration(cfg.RefreshInterval),
RefreshF: d.refresh,
MetricsInstantiator: m.refreshMetrics,
diff --git a/discovery/azure/azure_test.go b/discovery/azure/azure_test.go
index a88738da9f..23c120ac6b 100644
--- a/discovery/azure/azure_test.go
+++ b/discovery/azure/azure_test.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
@@ -659,7 +659,11 @@ func TestAzureRefresh(t *testing.T) {
refreshMetrics := discovery.NewRefreshMetrics(reg)
metrics := azureSDConfig.NewDiscovererMetrics(reg, refreshMetrics)
- sd, err := NewDiscovery(azureSDConfig, nil, metrics)
+ sd, err := NewDiscovery(azureSDConfig, discovery.DiscovererOptions{
+ Logger: nil,
+ Metrics: metrics,
+ SetName: "azure",
+ })
require.NoError(t, err)
tg, err := sd.refreshAzureClient(context.Background(), azureClient)
diff --git a/discovery/azure/metrics.go b/discovery/azure/metrics.go
index 3e3dbdbfbb..dc0291cdb8 100644
--- a/discovery/azure/metrics.go
+++ b/discovery/azure/metrics.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
diff --git a/discovery/consul/consul.go b/discovery/consul/consul.go
index 600bd274a4..4cc4003b2c 100644
--- a/discovery/consul/consul.go
+++ b/discovery/consul/consul.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
diff --git a/discovery/consul/consul_test.go b/discovery/consul/consul_test.go
index b813146089..32394dcc00 100644
--- a/discovery/consul/consul_test.go
+++ b/discovery/consul/consul_test.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
diff --git a/discovery/consul/metrics.go b/discovery/consul/metrics.go
index b49509bd8f..903fba5cef 100644
--- a/discovery/consul/metrics.go
+++ b/discovery/consul/metrics.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
diff --git a/discovery/digitalocean/digitalocean.go b/discovery/digitalocean/digitalocean.go
index 5c9795440d..0a185c2915 100644
--- a/discovery/digitalocean/digitalocean.go
+++ b/discovery/digitalocean/digitalocean.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
@@ -17,7 +17,6 @@ import (
"context"
"errors"
"fmt"
- "log/slog"
"net"
"net/http"
"strconv"
@@ -84,7 +83,7 @@ func (*SDConfig) Name() string { return "digitalocean" }
// NewDiscoverer returns a Discoverer for the Config.
func (c *SDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) {
- return NewDiscovery(c, opts.Logger, opts.Metrics)
+ return NewDiscovery(c, opts)
}
// SetDirectory joins any relative file paths with dir.
@@ -112,8 +111,8 @@ type Discovery struct {
}
// NewDiscovery returns a new Discovery which periodically refreshes its targets.
-func NewDiscovery(conf *SDConfig, logger *slog.Logger, metrics discovery.DiscovererMetrics) (*Discovery, error) {
- m, ok := metrics.(*digitaloceanMetrics)
+func NewDiscovery(conf *SDConfig, opts discovery.DiscovererOptions) (*Discovery, error) {
+ m, ok := opts.Metrics.(*digitaloceanMetrics)
if !ok {
return nil, errors.New("invalid discovery metrics type")
}
@@ -140,8 +139,9 @@ func NewDiscovery(conf *SDConfig, logger *slog.Logger, metrics discovery.Discove
d.Discovery = refresh.NewDiscovery(
refresh.Options{
- Logger: logger,
+ Logger: opts.Logger,
Mech: "digitalocean",
+ SetName: opts.SetName,
Interval: time.Duration(conf.RefreshInterval),
RefreshF: d.refresh,
MetricsInstantiator: m.refreshMetrics,
diff --git a/discovery/digitalocean/digitalocean_test.go b/discovery/digitalocean/digitalocean_test.go
index a282225ac2..560d8d533a 100644
--- a/discovery/digitalocean/digitalocean_test.go
+++ b/discovery/digitalocean/digitalocean_test.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
@@ -57,7 +57,11 @@ func TestDigitalOceanSDRefresh(t *testing.T) {
defer metrics.Unregister()
defer refreshMetrics.Unregister()
- d, err := NewDiscovery(&cfg, promslog.NewNopLogger(), metrics)
+ d, err := NewDiscovery(&cfg, discovery.DiscovererOptions{
+ Logger: promslog.NewNopLogger(),
+ Metrics: metrics,
+ SetName: "digitalocean",
+ })
require.NoError(t, err)
endpoint, err := url.Parse(sdmock.Mock.Endpoint())
require.NoError(t, err)
diff --git a/discovery/digitalocean/metrics.go b/discovery/digitalocean/metrics.go
index 7f68b39e56..4b11b825e5 100644
--- a/discovery/digitalocean/metrics.go
+++ b/discovery/digitalocean/metrics.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
diff --git a/discovery/digitalocean/mock_test.go b/discovery/digitalocean/mock_test.go
index 62d963c3b3..d5703d7702 100644
--- a/discovery/digitalocean/mock_test.go
+++ b/discovery/digitalocean/mock_test.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
diff --git a/discovery/discoverer_metrics_noop.go b/discovery/discoverer_metrics_noop.go
index 4321204b6c..b75474dfec 100644
--- a/discovery/discoverer_metrics_noop.go
+++ b/discovery/discoverer_metrics_noop.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
diff --git a/discovery/discovery.go b/discovery/discovery.go
index 2157b820b9..c4f8c8d458 100644
--- a/discovery/discovery.go
+++ b/discovery/discovery.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
@@ -54,19 +54,23 @@ type DiscovererOptions struct {
// Extra HTTP client options to expose to Discoverers. This field may be
// ignored; Discoverer implementations must opt-in to reading it.
HTTPClientOptions []config.HTTPClientOption
+
+ // SetName identifies this discoverer set.
+ SetName string
}
// RefreshMetrics are used by the "refresh" package.
// We define them here in the "discovery" package in order to avoid a cyclic dependency between
// "discovery" and "refresh".
type RefreshMetrics struct {
- Failures prometheus.Counter
- Duration prometheus.Observer
+ Failures prometheus.Counter
+ Duration prometheus.Observer
+ DurationHistogram prometheus.Observer
}
// RefreshMetricsInstantiator instantiates the metrics used by the "refresh" package.
type RefreshMetricsInstantiator interface {
- Instantiate(mech string) *RefreshMetrics
+ Instantiate(mech, setName string) *RefreshMetrics
}
// RefreshMetricsManager is an interface for registering, unregistering, and
diff --git a/discovery/discovery_test.go b/discovery/discovery_test.go
index 116095fd62..53539b6d40 100644
--- a/discovery/discovery_test.go
+++ b/discovery/discovery_test.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Prometheus Authors
+// 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
diff --git a/discovery/dns/dns.go b/discovery/dns/dns.go
index 24af8f65d9..4d9200d734 100644
--- a/discovery/dns/dns.go
+++ b/discovery/dns/dns.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The Prometheus Authors
+// 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
@@ -78,7 +78,7 @@ func (*SDConfig) Name() string { return "dns" }
// NewDiscoverer returns a Discoverer for the Config.
func (c *SDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) {
- return NewDiscovery(*c, opts.Logger, opts.Metrics)
+ return NewDiscovery(*c, opts)
}
// UnmarshalYAML implements the yaml.Unmarshaler interface.
@@ -118,14 +118,14 @@ type Discovery struct {
}
// NewDiscovery returns a new Discovery which periodically refreshes its targets.
-func NewDiscovery(conf SDConfig, logger *slog.Logger, metrics discovery.DiscovererMetrics) (*Discovery, error) {
- m, ok := metrics.(*dnsMetrics)
+func NewDiscovery(conf SDConfig, opts discovery.DiscovererOptions) (*Discovery, error) {
+ m, ok := opts.Metrics.(*dnsMetrics)
if !ok {
return nil, errors.New("invalid discovery metrics type")
}
- if logger == nil {
- logger = promslog.NewNopLogger()
+ if opts.Logger == nil {
+ opts.Logger = promslog.NewNopLogger()
}
qtype := dns.TypeSRV
@@ -145,15 +145,16 @@ func NewDiscovery(conf SDConfig, logger *slog.Logger, metrics discovery.Discover
names: conf.Names,
qtype: qtype,
port: conf.Port,
- logger: logger,
+ logger: opts.Logger,
lookupFn: lookupWithSearchPath,
metrics: m,
}
d.Discovery = refresh.NewDiscovery(
refresh.Options{
- Logger: logger,
+ Logger: opts.Logger,
Mech: "dns",
+ SetName: opts.SetName,
Interval: time.Duration(conf.RefreshInterval),
RefreshF: d.refresh,
MetricsInstantiator: m.refreshMetrics,
diff --git a/discovery/dns/dns_test.go b/discovery/dns/dns_test.go
index eb37f1a98e..eeb1137878 100644
--- a/discovery/dns/dns_test.go
+++ b/discovery/dns/dns_test.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The Prometheus Authors
+// 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
@@ -259,7 +259,11 @@ func TestDNS(t *testing.T) {
metrics := tc.config.NewDiscovererMetrics(reg, refreshMetrics)
require.NoError(t, metrics.Register())
- sd, err := NewDiscovery(tc.config, nil, metrics)
+ sd, err := NewDiscovery(tc.config, discovery.DiscovererOptions{
+ Logger: nil,
+ Metrics: metrics,
+ SetName: "dns",
+ })
require.NoError(t, err)
sd.lookupFn = tc.lookup
diff --git a/discovery/dns/metrics.go b/discovery/dns/metrics.go
index 27c96b53e0..b65db5e6c0 100644
--- a/discovery/dns/metrics.go
+++ b/discovery/dns/metrics.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
diff --git a/discovery/eureka/client.go b/discovery/eureka/client.go
index e4b54faae6..252b152637 100644
--- a/discovery/eureka/client.go
+++ b/discovery/eureka/client.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
diff --git a/discovery/eureka/client_test.go b/discovery/eureka/client_test.go
index f85409a11e..19812b1f5d 100644
--- a/discovery/eureka/client_test.go
+++ b/discovery/eureka/client_test.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
diff --git a/discovery/eureka/eureka.go b/discovery/eureka/eureka.go
index 11e83359cf..0d46667437 100644
--- a/discovery/eureka/eureka.go
+++ b/discovery/eureka/eureka.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
@@ -16,7 +16,6 @@ package eureka
import (
"context"
"errors"
- "log/slog"
"net"
"net/http"
"net/url"
@@ -88,7 +87,7 @@ func (*SDConfig) Name() string { return "eureka" }
// NewDiscoverer returns a Discoverer for the Config.
func (c *SDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) {
- return NewDiscovery(c, opts.Logger, opts.Metrics)
+ return NewDiscovery(c, opts)
}
// SetDirectory joins any relative file paths with dir.
@@ -125,8 +124,8 @@ type Discovery struct {
}
// NewDiscovery creates a new Eureka discovery for the given role.
-func NewDiscovery(conf *SDConfig, logger *slog.Logger, metrics discovery.DiscovererMetrics) (*Discovery, error) {
- m, ok := metrics.(*eurekaMetrics)
+func NewDiscovery(conf *SDConfig, opts discovery.DiscovererOptions) (*Discovery, error) {
+ m, ok := opts.Metrics.(*eurekaMetrics)
if !ok {
return nil, errors.New("invalid discovery metrics type")
}
@@ -142,8 +141,9 @@ func NewDiscovery(conf *SDConfig, logger *slog.Logger, metrics discovery.Discove
}
d.Discovery = refresh.NewDiscovery(
refresh.Options{
- Logger: logger,
+ Logger: opts.Logger,
Mech: "eureka",
+ SetName: opts.SetName,
Interval: time.Duration(conf.RefreshInterval),
RefreshF: d.refresh,
MetricsInstantiator: m.refreshMetrics,
diff --git a/discovery/eureka/eureka_test.go b/discovery/eureka/eureka_test.go
index 5ea9a6c74e..69612fedb7 100644
--- a/discovery/eureka/eureka_test.go
+++ b/discovery/eureka/eureka_test.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
@@ -47,7 +47,11 @@ func testUpdateServices(respHandler http.HandlerFunc) ([]*targetgroup.Group, err
defer metrics.Unregister()
defer refreshMetrics.Unregister()
- md, err := NewDiscovery(&conf, nil, metrics)
+ md, err := NewDiscovery(&conf, discovery.DiscovererOptions{
+ Logger: nil,
+ Metrics: metrics,
+ SetName: "eureka",
+ })
if err != nil {
return nil, err
}
diff --git a/discovery/eureka/metrics.go b/discovery/eureka/metrics.go
index 72cfe47096..5a0720a8d5 100644
--- a/discovery/eureka/metrics.go
+++ b/discovery/eureka/metrics.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
diff --git a/discovery/file/file.go b/discovery/file/file.go
index e0225891ce..c654297e0a 100644
--- a/discovery/file/file.go
+++ b/discovery/file/file.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
diff --git a/discovery/file/file_test.go b/discovery/file/file_test.go
index c80744f8c3..d8a36df399 100644
--- a/discovery/file/file_test.go
+++ b/discovery/file/file_test.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The Prometheus Authors
+// 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
diff --git a/discovery/file/metrics.go b/discovery/file/metrics.go
index 3e3df7bbf6..0371338d46 100644
--- a/discovery/file/metrics.go
+++ b/discovery/file/metrics.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
diff --git a/discovery/gce/gce.go b/discovery/gce/gce.go
index f5d20fb740..96eed2b27b 100644
--- a/discovery/gce/gce.go
+++ b/discovery/gce/gce.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
@@ -17,7 +17,6 @@ import (
"context"
"errors"
"fmt"
- "log/slog"
"net/http"
"strconv"
"strings"
@@ -94,7 +93,7 @@ func (*SDConfig) Name() string { return "gce" }
// NewDiscoverer returns a Discoverer for the Config.
func (c *SDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) {
- return NewDiscovery(*c, opts.Logger, opts.Metrics)
+ return NewDiscovery(*c, opts)
}
// UnmarshalYAML implements the yaml.Unmarshaler interface.
@@ -129,8 +128,8 @@ type Discovery struct {
}
// NewDiscovery returns a new Discovery which periodically refreshes its targets.
-func NewDiscovery(conf SDConfig, logger *slog.Logger, metrics discovery.DiscovererMetrics) (*Discovery, error) {
- m, ok := metrics.(*gceMetrics)
+func NewDiscovery(conf SDConfig, opts discovery.DiscovererOptions) (*Discovery, error) {
+ m, ok := opts.Metrics.(*gceMetrics)
if !ok {
return nil, errors.New("invalid discovery metrics type")
}
@@ -155,8 +154,9 @@ func NewDiscovery(conf SDConfig, logger *slog.Logger, metrics discovery.Discover
d.Discovery = refresh.NewDiscovery(
refresh.Options{
- Logger: logger,
+ Logger: opts.Logger,
Mech: "gce",
+ SetName: opts.SetName,
Interval: time.Duration(conf.RefreshInterval),
RefreshF: d.refresh,
MetricsInstantiator: m.refreshMetrics,
diff --git a/discovery/gce/metrics.go b/discovery/gce/metrics.go
index 7ea69b1a89..c4020f0a53 100644
--- a/discovery/gce/metrics.go
+++ b/discovery/gce/metrics.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
diff --git a/discovery/hetzner/hcloud.go b/discovery/hetzner/hcloud.go
index 88fe09bd3e..7fe55ffded 100644
--- a/discovery/hetzner/hcloud.go
+++ b/discovery/hetzner/hcloud.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
@@ -98,13 +98,13 @@ func (d *hcloudDiscovery) refresh(ctx context.Context) ([]*targetgroup.Group, er
hetznerLabelRole: model.LabelValue(HetznerRoleHcloud),
hetznerLabelServerID: model.LabelValue(strconv.FormatInt(server.ID, 10)),
hetznerLabelServerName: model.LabelValue(server.Name),
- hetznerLabelDatacenter: model.LabelValue(server.Datacenter.Name),
+ hetznerLabelDatacenter: model.LabelValue(server.Datacenter.Name), //nolint:staticcheck // server.Datacenter is deprecated but kept for backwards compatibility until the next minor release
hetznerLabelPublicIPv4: model.LabelValue(server.PublicNet.IPv4.IP.String()),
hetznerLabelPublicIPv6Network: model.LabelValue(server.PublicNet.IPv6.Network.String()),
hetznerLabelServerStatus: model.LabelValue(server.Status),
- hetznerLabelHcloudDatacenterLocation: model.LabelValue(server.Datacenter.Location.Name),
- hetznerLabelHcloudDatacenterLocationNetworkZone: model.LabelValue(server.Datacenter.Location.NetworkZone),
+ hetznerLabelHcloudDatacenterLocation: model.LabelValue(server.Datacenter.Location.Name), //nolint:staticcheck // server.Datacenter is deprecated but kept for backwards compatibility until the next minor release
+ hetznerLabelHcloudDatacenterLocationNetworkZone: model.LabelValue(server.Datacenter.Location.NetworkZone), //nolint:staticcheck // server.Datacenter is deprecated but kept for backwards compatibility until the next minor release
hetznerLabelHcloudType: model.LabelValue(server.ServerType.Name),
hetznerLabelHcloudCPUCores: model.LabelValue(strconv.Itoa(server.ServerType.Cores)),
hetznerLabelHcloudCPUType: model.LabelValue(server.ServerType.CPUType),
diff --git a/discovery/hetzner/hcloud_test.go b/discovery/hetzner/hcloud_test.go
index fa8291625a..3f20bcb86c 100644
--- a/discovery/hetzner/hcloud_test.go
+++ b/discovery/hetzner/hcloud_test.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
diff --git a/discovery/hetzner/hetzner.go b/discovery/hetzner/hetzner.go
index 5c5252d3d7..932cfc8c93 100644
--- a/discovery/hetzner/hetzner.go
+++ b/discovery/hetzner/hetzner.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
@@ -78,7 +78,7 @@ func (*SDConfig) Name() string { return "hetzner" }
// NewDiscoverer returns a Discoverer for the Config.
func (c *SDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) {
- return NewDiscovery(c, opts.Logger, opts.Metrics)
+ return NewDiscovery(c, opts)
}
type refresher interface {
@@ -138,21 +138,22 @@ type Discovery struct {
}
// NewDiscovery returns a new Discovery which periodically refreshes its targets.
-func NewDiscovery(conf *SDConfig, logger *slog.Logger, metrics discovery.DiscovererMetrics) (*refresh.Discovery, error) {
- m, ok := metrics.(*hetznerMetrics)
+func NewDiscovery(conf *SDConfig, opts discovery.DiscovererOptions) (*refresh.Discovery, error) {
+ m, ok := opts.Metrics.(*hetznerMetrics)
if !ok {
return nil, errors.New("invalid discovery metrics type")
}
- r, err := newRefresher(conf, logger)
+ r, err := newRefresher(conf, opts.Logger)
if err != nil {
return nil, err
}
return refresh.NewDiscovery(
refresh.Options{
- Logger: logger,
+ Logger: opts.Logger,
Mech: "hetzner",
+ SetName: opts.SetName,
Interval: time.Duration(conf.RefreshInterval),
RefreshF: r.refresh,
MetricsInstantiator: m.refreshMetrics,
diff --git a/discovery/hetzner/metrics.go b/discovery/hetzner/metrics.go
index 0023018194..cab1d66a3e 100644
--- a/discovery/hetzner/metrics.go
+++ b/discovery/hetzner/metrics.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
diff --git a/discovery/hetzner/mock_test.go b/discovery/hetzner/mock_test.go
index d192a4eae9..5f1e9c036b 100644
--- a/discovery/hetzner/mock_test.go
+++ b/discovery/hetzner/mock_test.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
diff --git a/discovery/hetzner/robot.go b/discovery/hetzner/robot.go
index 33aa2abcd8..ef5de1a30c 100644
--- a/discovery/hetzner/robot.go
+++ b/discovery/hetzner/robot.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
diff --git a/discovery/hetzner/robot_test.go b/discovery/hetzner/robot_test.go
index 2618bd097c..0e8b7954cc 100644
--- a/discovery/hetzner/robot_test.go
+++ b/discovery/hetzner/robot_test.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
diff --git a/discovery/http/http.go b/discovery/http/http.go
index bbaf4038c8..fa9c7208fa 100644
--- a/discovery/http/http.go
+++ b/discovery/http/http.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
@@ -19,7 +19,6 @@ import (
"errors"
"fmt"
"io"
- "log/slog"
"net/http"
"net/url"
"strconv"
@@ -69,7 +68,7 @@ func (*SDConfig) Name() string { return "http" }
// NewDiscoverer returns a Discoverer for the Config.
func (c *SDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) {
- return NewDiscovery(c, opts.Logger, opts.HTTPClientOptions, opts.Metrics)
+ return NewDiscovery(c, opts)
}
// SetDirectory joins any relative file paths with dir.
@@ -115,17 +114,17 @@ type Discovery struct {
}
// NewDiscovery returns a new HTTP discovery for the given config.
-func NewDiscovery(conf *SDConfig, logger *slog.Logger, clientOpts []config.HTTPClientOption, metrics discovery.DiscovererMetrics) (*Discovery, error) {
- m, ok := metrics.(*httpMetrics)
+func NewDiscovery(conf *SDConfig, opts discovery.DiscovererOptions) (*Discovery, error) {
+ m, ok := opts.Metrics.(*httpMetrics)
if !ok {
return nil, errors.New("invalid discovery metrics type")
}
- if logger == nil {
- logger = promslog.NewNopLogger()
+ if opts.Logger == nil {
+ opts.Logger = promslog.NewNopLogger()
}
- client, err := config.NewClientFromConfig(conf.HTTPClientConfig, "http", clientOpts...)
+ client, err := config.NewClientFromConfig(conf.HTTPClientConfig, "http", opts.HTTPClientOptions...)
if err != nil {
return nil, err
}
@@ -140,8 +139,9 @@ func NewDiscovery(conf *SDConfig, logger *slog.Logger, clientOpts []config.HTTPC
d.Discovery = refresh.NewDiscovery(
refresh.Options{
- Logger: logger,
+ Logger: opts.Logger,
Mech: "http",
+ SetName: opts.SetName,
Interval: time.Duration(conf.RefreshInterval),
RefreshF: d.Refresh,
MetricsInstantiator: m.refreshMetrics,
diff --git a/discovery/http/http_test.go b/discovery/http/http_test.go
index 3af9e4e504..50a5800fc6 100644
--- a/discovery/http/http_test.go
+++ b/discovery/http/http_test.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
@@ -49,7 +49,12 @@ func TestHTTPValidRefresh(t *testing.T) {
require.NoError(t, metrics.Register())
defer metrics.Unregister()
- d, err := NewDiscovery(&cfg, promslog.NewNopLogger(), nil, metrics)
+ d, err := NewDiscovery(&cfg, discovery.DiscovererOptions{
+ Logger: promslog.NewNopLogger(),
+ HTTPClientOptions: nil,
+ Metrics: metrics,
+ SetName: "http",
+ })
require.NoError(t, err)
ctx := context.Background()
@@ -94,7 +99,12 @@ func TestHTTPInvalidCode(t *testing.T) {
require.NoError(t, metrics.Register())
defer metrics.Unregister()
- d, err := NewDiscovery(&cfg, promslog.NewNopLogger(), nil, metrics)
+ d, err := NewDiscovery(&cfg, discovery.DiscovererOptions{
+ Logger: promslog.NewNopLogger(),
+ HTTPClientOptions: nil,
+ Metrics: metrics,
+ SetName: "http",
+ })
require.NoError(t, err)
ctx := context.Background()
@@ -123,7 +133,12 @@ func TestHTTPInvalidFormat(t *testing.T) {
require.NoError(t, metrics.Register())
defer metrics.Unregister()
- d, err := NewDiscovery(&cfg, promslog.NewNopLogger(), nil, metrics)
+ d, err := NewDiscovery(&cfg, discovery.DiscovererOptions{
+ Logger: promslog.NewNopLogger(),
+ HTTPClientOptions: nil,
+ Metrics: metrics,
+ SetName: "http",
+ })
require.NoError(t, err)
ctx := context.Background()
@@ -442,7 +457,12 @@ func TestSourceDisappeared(t *testing.T) {
require.NoError(t, metrics.Register())
defer metrics.Unregister()
- d, err := NewDiscovery(&cfg, promslog.NewNopLogger(), nil, metrics)
+ d, err := NewDiscovery(&cfg, discovery.DiscovererOptions{
+ Logger: promslog.NewNopLogger(),
+ HTTPClientOptions: nil,
+ Metrics: metrics,
+ SetName: "http",
+ })
require.NoError(t, err)
for _, test := range cases {
ctx := context.Background()
diff --git a/discovery/http/metrics.go b/discovery/http/metrics.go
index b1f8b84433..57fbcac15a 100644
--- a/discovery/http/metrics.go
+++ b/discovery/http/metrics.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
diff --git a/discovery/install/install.go b/discovery/install/install.go
index 9c397f9d36..05598347c1 100644
--- a/discovery/install/install.go
+++ b/discovery/install/install.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
diff --git a/discovery/ionos/ionos.go b/discovery/ionos/ionos.go
index 021986395b..93d57654e8 100644
--- a/discovery/ionos/ionos.go
+++ b/discovery/ionos/ionos.go
@@ -1,4 +1,4 @@
-// Copyright 2022 The Prometheus Authors
+// 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
@@ -15,7 +15,6 @@ package ionos
import (
"errors"
- "log/slog"
"time"
"github.com/prometheus/client_golang/prometheus"
@@ -42,8 +41,8 @@ func init() {
type Discovery struct{}
// NewDiscovery returns a new refresh.Discovery for IONOS Cloud.
-func NewDiscovery(conf *SDConfig, logger *slog.Logger, metrics discovery.DiscovererMetrics) (*refresh.Discovery, error) {
- m, ok := metrics.(*ionosMetrics)
+func NewDiscovery(conf *SDConfig, opts discovery.DiscovererOptions) (*refresh.Discovery, error) {
+ m, ok := opts.Metrics.(*ionosMetrics)
if !ok {
return nil, errors.New("invalid discovery metrics type")
}
@@ -52,15 +51,16 @@ func NewDiscovery(conf *SDConfig, logger *slog.Logger, metrics discovery.Discove
conf.ionosEndpoint = "https://api.ionos.com"
}
- d, err := newServerDiscovery(conf, logger)
+ d, err := newServerDiscovery(conf, opts.Logger)
if err != nil {
return nil, err
}
return refresh.NewDiscovery(
refresh.Options{
- Logger: logger,
+ Logger: opts.Logger,
Mech: "ionos",
+ SetName: opts.SetName,
Interval: time.Duration(conf.RefreshInterval),
RefreshF: d.refresh,
MetricsInstantiator: m.refreshMetrics,
@@ -101,8 +101,8 @@ func (SDConfig) Name() string {
}
// NewDiscoverer returns a new discovery.Discoverer for IONOS Cloud.
-func (c SDConfig) NewDiscoverer(options discovery.DiscovererOptions) (discovery.Discoverer, error) {
- return NewDiscovery(&c, options.Logger, options.Metrics)
+func (c SDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) {
+ return NewDiscovery(&c, opts)
}
// UnmarshalYAML implements the yaml.Unmarshaler interface.
diff --git a/discovery/ionos/metrics.go b/discovery/ionos/metrics.go
index e79bded695..7fc78fdfa5 100644
--- a/discovery/ionos/metrics.go
+++ b/discovery/ionos/metrics.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
diff --git a/discovery/ionos/server.go b/discovery/ionos/server.go
index 81bb497277..bd351625db 100644
--- a/discovery/ionos/server.go
+++ b/discovery/ionos/server.go
@@ -1,4 +1,4 @@
-// Copyright 2022 The Prometheus Authors
+// 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
diff --git a/discovery/ionos/server_test.go b/discovery/ionos/server_test.go
index 30f358e325..28fd285f67 100644
--- a/discovery/ionos/server_test.go
+++ b/discovery/ionos/server_test.go
@@ -1,4 +1,4 @@
-// Copyright 2022 The Prometheus Authors
+// 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
diff --git a/discovery/kubernetes/endpoints.go b/discovery/kubernetes/endpoints.go
index 21c401da2c..4edcf9d4fa 100644
--- a/discovery/kubernetes/endpoints.go
+++ b/discovery/kubernetes/endpoints.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The Prometheus Authors
+// 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
diff --git a/discovery/kubernetes/endpoints_test.go b/discovery/kubernetes/endpoints_test.go
index aa0e432bfd..0ac472324d 100644
--- a/discovery/kubernetes/endpoints_test.go
+++ b/discovery/kubernetes/endpoints_test.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The Prometheus Authors
+// 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
diff --git a/discovery/kubernetes/endpointslice.go b/discovery/kubernetes/endpointslice.go
index 85b579438f..a6cfb0706a 100644
--- a/discovery/kubernetes/endpointslice.go
+++ b/discovery/kubernetes/endpointslice.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
diff --git a/discovery/kubernetes/endpointslice_test.go b/discovery/kubernetes/endpointslice_test.go
index cfd6be709e..b4dc0c36ce 100644
--- a/discovery/kubernetes/endpointslice_test.go
+++ b/discovery/kubernetes/endpointslice_test.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The Prometheus Authors
+// 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
diff --git a/discovery/kubernetes/ingress.go b/discovery/kubernetes/ingress.go
index 551453e513..985cc8f138 100644
--- a/discovery/kubernetes/ingress.go
+++ b/discovery/kubernetes/ingress.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The Prometheus Authors
+// 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
diff --git a/discovery/kubernetes/ingress_test.go b/discovery/kubernetes/ingress_test.go
index 76c9ff9036..15fa28002a 100644
--- a/discovery/kubernetes/ingress_test.go
+++ b/discovery/kubernetes/ingress_test.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The Prometheus Authors
+// 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
diff --git a/discovery/kubernetes/kubernetes.go b/discovery/kubernetes/kubernetes.go
index 1a6f965ecd..678f287ef5 100644
--- a/discovery/kubernetes/kubernetes.go
+++ b/discovery/kubernetes/kubernetes.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The Prometheus Authors
+// 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
diff --git a/discovery/kubernetes/kubernetes_test.go b/discovery/kubernetes/kubernetes_test.go
index f8edec23cb..b4bba381a4 100644
--- a/discovery/kubernetes/kubernetes_test.go
+++ b/discovery/kubernetes/kubernetes_test.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Prometheus Authors
+// 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
@@ -17,6 +17,7 @@ import (
"context"
"encoding/json"
"errors"
+ "os"
"testing"
"time"
@@ -42,6 +43,14 @@ import (
)
func TestMain(m *testing.M) {
+ // Disable the WatchListClient feature gate that is enabled by default in
+ // client-go v0.35.0+. The WatchList flow requires the server to support
+ // SendInitialEvents and to send a bookmark event with the
+ // "k8s.io/initial-events-end" annotation. The fake clientset used in tests
+ // does not support this protocol, causing informers to hang indefinitely
+ // waiting for the bookmark. Disabling this feature restores the traditional
+ // List+Watch flow which is compatible with the fake clientset.
+ os.Setenv("KUBE_FEATURE_WatchListClient", "false")
testutil.TolerantVerifyLeak(m)
}
@@ -52,7 +61,7 @@ func makeDiscovery(role Role, nsDiscovery NamespaceDiscovery, objects ...runtime
// makeDiscoveryWithVersion creates a kubernetes.Discovery instance with the specified kubernetes version for testing.
func makeDiscoveryWithVersion(role Role, nsDiscovery NamespaceDiscovery, k8sVer string, objects ...runtime.Object) (*Discovery, kubernetes.Interface) {
- clientset := fake.NewSimpleClientset(objects...)
+ clientset := fake.NewClientset(objects...)
fakeDiscovery, _ := clientset.Discovery().(*fakediscovery.FakeDiscovery)
fakeDiscovery.FakedServerVersion = &version.Info{GitVersion: k8sVer}
diff --git a/discovery/kubernetes/metrics.go b/discovery/kubernetes/metrics.go
index ba3cb1d32a..cdf158a032 100644
--- a/discovery/kubernetes/metrics.go
+++ b/discovery/kubernetes/metrics.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
diff --git a/discovery/kubernetes/node.go b/discovery/kubernetes/node.go
index 131cdcc9e7..cbc69dd0ca 100644
--- a/discovery/kubernetes/node.go
+++ b/discovery/kubernetes/node.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The Prometheus Authors
+// 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
diff --git a/discovery/kubernetes/node_test.go b/discovery/kubernetes/node_test.go
index bc17efdc01..9e56b95bb9 100644
--- a/discovery/kubernetes/node_test.go
+++ b/discovery/kubernetes/node_test.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The Prometheus Authors
+// 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
diff --git a/discovery/kubernetes/pod.go b/discovery/kubernetes/pod.go
index 03089e39d4..1fed78b3a7 100644
--- a/discovery/kubernetes/pod.go
+++ b/discovery/kubernetes/pod.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The Prometheus Authors
+// 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
diff --git a/discovery/kubernetes/pod_test.go b/discovery/kubernetes/pod_test.go
index 2cf336774a..db5db546d0 100644
--- a/discovery/kubernetes/pod_test.go
+++ b/discovery/kubernetes/pod_test.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The Prometheus Authors
+// 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
diff --git a/discovery/kubernetes/service.go b/discovery/kubernetes/service.go
index d676490d6c..ac2d42fc7c 100644
--- a/discovery/kubernetes/service.go
+++ b/discovery/kubernetes/service.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The Prometheus Authors
+// 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
diff --git a/discovery/kubernetes/service_test.go b/discovery/kubernetes/service_test.go
index 43c2b7922d..56a785d9c2 100644
--- a/discovery/kubernetes/service_test.go
+++ b/discovery/kubernetes/service_test.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The Prometheus Authors
+// 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
diff --git a/discovery/linode/linode.go b/discovery/linode/linode.go
index fe61e122e4..a5f05600c1 100644
--- a/discovery/linode/linode.go
+++ b/discovery/linode/linode.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
@@ -17,7 +17,6 @@ import (
"context"
"errors"
"fmt"
- "log/slog"
"net"
"net/http"
"strconv"
@@ -103,7 +102,7 @@ func (*SDConfig) Name() string { return "linode" }
// NewDiscoverer returns a Discoverer for the Config.
func (c *SDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) {
- return NewDiscovery(c, opts.Logger, opts.Metrics)
+ return NewDiscovery(c, opts)
}
// SetDirectory joins any relative file paths with dir.
@@ -138,8 +137,8 @@ type Discovery struct {
}
// NewDiscovery returns a new Discovery which periodically refreshes its targets.
-func NewDiscovery(conf *SDConfig, logger *slog.Logger, metrics discovery.DiscovererMetrics) (*Discovery, error) {
- m, ok := metrics.(*linodeMetrics)
+func NewDiscovery(conf *SDConfig, opts discovery.DiscovererOptions) (*Discovery, error) {
+ m, ok := opts.Metrics.(*linodeMetrics)
if !ok {
return nil, errors.New("invalid discovery metrics type")
}
@@ -170,8 +169,9 @@ func NewDiscovery(conf *SDConfig, logger *slog.Logger, metrics discovery.Discove
d.Discovery = refresh.NewDiscovery(
refresh.Options{
- Logger: logger,
+ Logger: opts.Logger,
Mech: "linode",
+ SetName: opts.SetName,
Interval: time.Duration(conf.RefreshInterval),
RefreshF: d.refresh,
MetricsInstantiator: m.refreshMetrics,
diff --git a/discovery/linode/linode_test.go b/discovery/linode/linode_test.go
index 7bcaa05ba4..d795d29698 100644
--- a/discovery/linode/linode_test.go
+++ b/discovery/linode/linode_test.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
@@ -238,7 +238,11 @@ func TestLinodeSDRefresh(t *testing.T) {
defer metrics.Unregister()
defer refreshMetrics.Unregister()
- d, err := NewDiscovery(&cfg, promslog.NewNopLogger(), metrics)
+ d, err := NewDiscovery(&cfg, discovery.DiscovererOptions{
+ Logger: promslog.NewNopLogger(),
+ Metrics: metrics,
+ SetName: "linode",
+ })
require.NoError(t, err)
endpoint, err := url.Parse(sdmock.Endpoint())
require.NoError(t, err)
diff --git a/discovery/linode/metrics.go b/discovery/linode/metrics.go
index 8f81389226..5bc805a60e 100644
--- a/discovery/linode/metrics.go
+++ b/discovery/linode/metrics.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
diff --git a/discovery/linode/mock_test.go b/discovery/linode/mock_test.go
index 50f0572ecd..b8094ec211 100644
--- a/discovery/linode/mock_test.go
+++ b/discovery/linode/mock_test.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
diff --git a/discovery/manager.go b/discovery/manager.go
index 6688152da9..3f2b2db652 100644
--- a/discovery/manager.go
+++ b/discovery/manager.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The Prometheus Authors
+// 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
@@ -27,6 +27,7 @@ import (
"github.com/prometheus/common/promslog"
"github.com/prometheus/prometheus/discovery/targetgroup"
+ "github.com/prometheus/prometheus/util/features"
)
type poolKey struct {
@@ -111,6 +112,13 @@ func NewManager(ctx context.Context, logger *slog.Logger, registerer prometheus.
}
mgr.metrics = metrics
+ // Register all available service discovery providers with the feature registry.
+ if mgr.featureRegistry != nil {
+ for _, sdName := range RegisteredConfigNames() {
+ mgr.featureRegistry.Enable(features.ServiceDiscoveryProviders, sdName)
+ }
+ }
+
return mgr
}
@@ -141,6 +149,15 @@ func HTTPClientOptions(opts ...config.HTTPClientOption) func(*Manager) {
}
}
+// FeatureRegistry sets the feature registry for the manager.
+func FeatureRegistry(fr features.Collector) func(*Manager) {
+ return func(m *Manager) {
+ m.mtx.Lock()
+ defer m.mtx.Unlock()
+ m.featureRegistry = fr
+ }
+}
+
// Manager maintains a set of discovery providers and sends each update to a map channel.
// Targets are grouped by the target set name.
type Manager struct {
@@ -175,6 +192,9 @@ type Manager struct {
metrics *Metrics
sdMetrics map[string]DiscovererMetrics
+
+ // featureRegistry is used to track which service discovery providers are configured.
+ featureRegistry features.Collector
}
// Providers returns the currently configured SD providers.
@@ -479,6 +499,7 @@ func (m *Manager) registerProviders(cfgs Configs, setName string) int {
Logger: m.logger.With("discovery", typ, "config", setName),
HTTPClientOptions: m.httpOpts,
Metrics: m.sdMetrics[typ],
+ SetName: setName,
})
if err != nil {
m.logger.Error("Cannot create service discovery", "err", err, "type", typ, "config", setName)
diff --git a/discovery/manager_test.go b/discovery/manager_test.go
index 5d34cb7ac0..8a49005100 100644
--- a/discovery/manager_test.go
+++ b/discovery/manager_test.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The Prometheus Authors
+// 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
@@ -1562,11 +1562,9 @@ func TestConfigReloadAndShutdownRace(t *testing.T) {
discoveryManager.updatert = 100 * time.Millisecond
var wgDiscovery sync.WaitGroup
- wgDiscovery.Add(1)
- go func() {
+ wgDiscovery.Go(func() {
discoveryManager.Run()
- wgDiscovery.Done()
- }()
+ })
time.Sleep(time.Millisecond * 200)
var wgBg sync.WaitGroup
@@ -1588,11 +1586,9 @@ func TestConfigReloadAndShutdownRace(t *testing.T) {
discoveryManager.ApplyConfig(c)
delete(c, "prometheus")
- wgBg.Add(1)
- go func() {
+ wgBg.Go(func() {
discoveryManager.ApplyConfig(c)
- wgBg.Done()
- }()
+ })
mgrCancel()
wgDiscovery.Wait()
diff --git a/discovery/marathon/marathon.go b/discovery/marathon/marathon.go
index cae040ca98..878d404373 100644
--- a/discovery/marathon/marathon.go
+++ b/discovery/marathon/marathon.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The Prometheus Authors
+// 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
@@ -19,7 +19,6 @@ import (
"errors"
"fmt"
"io"
- "log/slog"
"math/rand"
"net"
"net/http"
@@ -91,7 +90,7 @@ func (*SDConfig) Name() string { return "marathon" }
// NewDiscoverer returns a Discoverer for the Config.
func (c *SDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) {
- return NewDiscovery(*c, opts.Logger, opts.Metrics)
+ return NewDiscovery(*c, opts)
}
// SetDirectory joins any relative file paths with dir.
@@ -140,8 +139,8 @@ type Discovery struct {
}
// NewDiscovery returns a new Marathon Discovery.
-func NewDiscovery(conf SDConfig, logger *slog.Logger, metrics discovery.DiscovererMetrics) (*Discovery, error) {
- m, ok := metrics.(*marathonMetrics)
+func NewDiscovery(conf SDConfig, opts discovery.DiscovererOptions) (*Discovery, error) {
+ m, ok := opts.Metrics.(*marathonMetrics)
if !ok {
return nil, errors.New("invalid discovery metrics type")
}
@@ -168,8 +167,9 @@ func NewDiscovery(conf SDConfig, logger *slog.Logger, metrics discovery.Discover
}
d.Discovery = refresh.NewDiscovery(
refresh.Options{
- Logger: logger,
+ Logger: opts.Logger,
Mech: "marathon",
+ SetName: opts.SetName,
Interval: time.Duration(conf.RefreshInterval),
RefreshF: d.refresh,
MetricsInstantiator: m.refreshMetrics,
diff --git a/discovery/marathon/marathon_test.go b/discovery/marathon/marathon_test.go
index 588532d218..71c7d73d7e 100644
--- a/discovery/marathon/marathon_test.go
+++ b/discovery/marathon/marathon_test.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
@@ -51,7 +51,11 @@ func testUpdateServices(client appListClient) ([]*targetgroup.Group, error) {
defer metrics.Unregister()
defer refreshMetrics.Unregister()
- md, err := NewDiscovery(cfg, nil, metrics)
+ md, err := NewDiscovery(cfg, discovery.DiscovererOptions{
+ Logger: nil,
+ Metrics: metrics,
+ SetName: "marathon",
+ })
if err != nil {
return nil, err
}
@@ -132,7 +136,11 @@ func TestMarathonSDRemoveApp(t *testing.T) {
defer metrics.Unregister()
defer refreshMetrics.Unregister()
- md, err := NewDiscovery(cfg, nil, metrics)
+ md, err := NewDiscovery(cfg, discovery.DiscovererOptions{
+ Logger: nil,
+ Metrics: metrics,
+ SetName: "marathon",
+ })
require.NoError(t, err)
md.appsClient = func(context.Context, *http.Client, string) (*appList, error) {
diff --git a/discovery/marathon/metrics.go b/discovery/marathon/metrics.go
index 40e2ade558..3d3d57d9ae 100644
--- a/discovery/marathon/metrics.go
+++ b/discovery/marathon/metrics.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
diff --git a/discovery/metrics.go b/discovery/metrics.go
index 356be1ddcb..2a3734fb2d 100644
--- a/discovery/metrics.go
+++ b/discovery/metrics.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The Prometheus Authors
+// 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
diff --git a/discovery/metrics_k8s_client.go b/discovery/metrics_k8s_client.go
index 19dfd4e247..3642eac568 100644
--- a/discovery/metrics_k8s_client.go
+++ b/discovery/metrics_k8s_client.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Prometheus Authors
+// 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
diff --git a/discovery/metrics_refresh.go b/discovery/metrics_refresh.go
index ef49e591a3..11092d9f96 100644
--- a/discovery/metrics_refresh.go
+++ b/discovery/metrics_refresh.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
@@ -14,6 +14,8 @@
package discovery
import (
+ "time"
+
"github.com/prometheus/client_golang/prometheus"
)
@@ -21,8 +23,9 @@ import (
// We define them here in the "discovery" package in order to avoid a cyclic dependency between
// "discovery" and "refresh".
type RefreshMetricsVecs struct {
- failuresVec *prometheus.CounterVec
- durationVec *prometheus.SummaryVec
+ failuresVec *prometheus.CounterVec
+ durationVec *prometheus.SummaryVec
+ durationHistVec *prometheus.HistogramVec
metricRegisterer MetricRegisterer
}
@@ -36,13 +39,23 @@ func NewRefreshMetrics(reg prometheus.Registerer) RefreshMetricsManager {
Name: "prometheus_sd_refresh_failures_total",
Help: "Number of refresh failures for the given SD mechanism.",
},
- []string{"mechanism"}),
+ []string{"mechanism", "config"}),
durationVec: prometheus.NewSummaryVec(
prometheus.SummaryOpts{
Name: "prometheus_sd_refresh_duration_seconds",
Help: "The duration of a refresh in seconds for the given SD mechanism.",
Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
},
+ []string{"mechanism", "config"}),
+ durationHistVec: prometheus.NewHistogramVec(
+ prometheus.HistogramOpts{
+ Name: "prometheus_sd_refresh_duration_histogram_seconds",
+ Help: "The duration of a refresh for the given SD mechanism.",
+ Buckets: []float64{.01, .1, 1, 10},
+ NativeHistogramBucketFactor: 1.1,
+ NativeHistogramMaxBucketNumber: 100,
+ NativeHistogramMinResetDuration: 1 * time.Hour,
+ },
[]string{"mechanism"}),
}
@@ -51,16 +64,18 @@ func NewRefreshMetrics(reg prometheus.Registerer) RefreshMetricsManager {
m.metricRegisterer = NewMetricRegisterer(reg, []prometheus.Collector{
m.failuresVec,
m.durationVec,
+ m.durationHistVec,
})
return m
}
-// Instantiate returns metrics out of metric vectors.
-func (m *RefreshMetricsVecs) Instantiate(mech string) *RefreshMetrics {
+// Instantiate returns metrics out of metric vectors for a given mechanism and config.
+func (m *RefreshMetricsVecs) Instantiate(mech, config string) *RefreshMetrics {
return &RefreshMetrics{
- Failures: m.failuresVec.WithLabelValues(mech),
- Duration: m.durationVec.WithLabelValues(mech),
+ Failures: m.failuresVec.WithLabelValues(mech, config),
+ Duration: m.durationVec.WithLabelValues(mech, config),
+ DurationHistogram: m.durationHistVec.WithLabelValues(mech),
}
}
diff --git a/discovery/moby/docker.go b/discovery/moby/docker.go
index cb5577a131..aa1cd2eb42 100644
--- a/discovery/moby/docker.go
+++ b/discovery/moby/docker.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
@@ -17,7 +17,6 @@ import (
"context"
"errors"
"fmt"
- "log/slog"
"net"
"net/http"
"net/url"
@@ -94,7 +93,7 @@ func (*DockerSDConfig) Name() string { return "docker" }
// NewDiscoverer returns a Discoverer for the Config.
func (c *DockerSDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) {
- return NewDockerDiscovery(c, opts.Logger, opts.Metrics)
+ return NewDockerDiscovery(c, opts)
}
// SetDirectory joins any relative file paths with dir.
@@ -129,8 +128,8 @@ type DockerDiscovery struct {
}
// NewDockerDiscovery returns a new DockerDiscovery which periodically refreshes its targets.
-func NewDockerDiscovery(conf *DockerSDConfig, logger *slog.Logger, metrics discovery.DiscovererMetrics) (*DockerDiscovery, error) {
- m, ok := metrics.(*dockerMetrics)
+func NewDockerDiscovery(conf *DockerSDConfig, opts discovery.DiscovererOptions) (*DockerDiscovery, error) {
+ m, ok := opts.Metrics.(*dockerMetrics)
if !ok {
return nil, errors.New("invalid discovery metrics type")
}
@@ -146,7 +145,7 @@ func NewDockerDiscovery(conf *DockerSDConfig, logger *slog.Logger, metrics disco
return nil, err
}
- opts := []client.Opt{
+ clientOpts := []client.Opt{
client.WithHost(conf.Host),
client.WithAPIVersionNegotiation(),
}
@@ -166,7 +165,7 @@ func NewDockerDiscovery(conf *DockerSDConfig, logger *slog.Logger, metrics disco
if err != nil {
return nil, err
}
- opts = append(opts,
+ clientOpts = append(clientOpts,
client.WithHTTPClient(&http.Client{
Transport: rt,
Timeout: time.Duration(conf.RefreshInterval),
@@ -178,15 +177,16 @@ func NewDockerDiscovery(conf *DockerSDConfig, logger *slog.Logger, metrics disco
)
}
- d.client, err = client.NewClientWithOpts(opts...)
+ d.client, err = client.NewClientWithOpts(clientOpts...)
if err != nil {
return nil, fmt.Errorf("error setting up docker client: %w", err)
}
d.Discovery = refresh.NewDiscovery(
refresh.Options{
- Logger: logger,
+ Logger: opts.Logger,
Mech: "docker",
+ SetName: opts.SetName,
Interval: time.Duration(conf.RefreshInterval),
RefreshF: d.refresh,
MetricsInstantiator: m.refreshMetrics,
diff --git a/discovery/moby/docker_test.go b/discovery/moby/docker_test.go
index 430669c113..effdf90b36 100644
--- a/discovery/moby/docker_test.go
+++ b/discovery/moby/docker_test.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
@@ -48,7 +48,11 @@ host: %s
defer metrics.Unregister()
defer refreshMetrics.Unregister()
- d, err := NewDockerDiscovery(&cfg, promslog.NewNopLogger(), metrics)
+ d, err := NewDockerDiscovery(&cfg, discovery.DiscovererOptions{
+ Logger: promslog.NewNopLogger(),
+ Metrics: metrics,
+ SetName: "docker_swarm",
+ })
require.NoError(t, err)
ctx := context.Background()
@@ -226,7 +230,11 @@ host: %s
require.NoError(t, metrics.Register())
defer metrics.Unregister()
defer refreshMetrics.Unregister()
- d, err := NewDockerDiscovery(&cfg, promslog.NewNopLogger(), metrics)
+ d, err := NewDockerDiscovery(&cfg, discovery.DiscovererOptions{
+ Logger: promslog.NewNopLogger(),
+ Metrics: metrics,
+ SetName: "docker_swarm",
+ })
require.NoError(t, err)
ctx := context.Background()
diff --git a/discovery/moby/dockerswarm.go b/discovery/moby/dockerswarm.go
index 44abb0ab25..5cb12279d8 100644
--- a/discovery/moby/dockerswarm.go
+++ b/discovery/moby/dockerswarm.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
@@ -17,7 +17,6 @@ import (
"context"
"errors"
"fmt"
- "log/slog"
"net/http"
"net/url"
"time"
@@ -81,7 +80,7 @@ func (*DockerSwarmSDConfig) Name() string { return "dockerswarm" }
// NewDiscoverer returns a Discoverer for the Config.
func (c *DockerSwarmSDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) {
- return NewDiscovery(c, opts.Logger, opts.Metrics)
+ return NewDiscovery(c, opts)
}
// SetDirectory joins any relative file paths with dir.
@@ -124,8 +123,8 @@ type Discovery struct {
}
// NewDiscovery returns a new Discovery which periodically refreshes its targets.
-func NewDiscovery(conf *DockerSwarmSDConfig, logger *slog.Logger, metrics discovery.DiscovererMetrics) (*Discovery, error) {
- m, ok := metrics.(*dockerswarmMetrics)
+func NewDiscovery(conf *DockerSwarmSDConfig, opts discovery.DiscovererOptions) (*Discovery, error) {
+ m, ok := opts.Metrics.(*dockerswarmMetrics)
if !ok {
return nil, errors.New("invalid discovery metrics type")
}
@@ -140,7 +139,7 @@ func NewDiscovery(conf *DockerSwarmSDConfig, logger *slog.Logger, metrics discov
return nil, err
}
- opts := []client.Opt{
+ clientOpts := []client.Opt{
client.WithHost(conf.Host),
client.WithAPIVersionNegotiation(),
}
@@ -160,7 +159,7 @@ func NewDiscovery(conf *DockerSwarmSDConfig, logger *slog.Logger, metrics discov
if err != nil {
return nil, err
}
- opts = append(opts,
+ clientOpts = append(clientOpts,
client.WithHTTPClient(&http.Client{
Transport: rt,
Timeout: time.Duration(conf.RefreshInterval),
@@ -172,15 +171,16 @@ func NewDiscovery(conf *DockerSwarmSDConfig, logger *slog.Logger, metrics discov
)
}
- d.client, err = client.NewClientWithOpts(opts...)
+ d.client, err = client.NewClientWithOpts(clientOpts...)
if err != nil {
return nil, fmt.Errorf("error setting up docker swarm client: %w", err)
}
d.Discovery = refresh.NewDiscovery(
refresh.Options{
- Logger: logger,
+ Logger: opts.Logger,
Mech: "dockerswarm",
+ SetName: opts.SetName,
Interval: time.Duration(conf.RefreshInterval),
RefreshF: d.refresh,
MetricsInstantiator: m.refreshMetrics,
diff --git a/discovery/moby/metrics_docker.go b/discovery/moby/metrics_docker.go
index 716f52b60a..8c2518a75e 100644
--- a/discovery/moby/metrics_docker.go
+++ b/discovery/moby/metrics_docker.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
diff --git a/discovery/moby/metrics_dockerswarm.go b/discovery/moby/metrics_dockerswarm.go
index 17dd30d1b3..e4682b032a 100644
--- a/discovery/moby/metrics_dockerswarm.go
+++ b/discovery/moby/metrics_dockerswarm.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
diff --git a/discovery/moby/mock_test.go b/discovery/moby/mock_test.go
index 2450ca4436..e43319494d 100644
--- a/discovery/moby/mock_test.go
+++ b/discovery/moby/mock_test.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
diff --git a/discovery/moby/network.go b/discovery/moby/network.go
index ea1ca66bc7..02db2b8a12 100644
--- a/discovery/moby/network.go
+++ b/discovery/moby/network.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
diff --git a/discovery/moby/nodes.go b/discovery/moby/nodes.go
index a11afeee25..76e090c803 100644
--- a/discovery/moby/nodes.go
+++ b/discovery/moby/nodes.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
diff --git a/discovery/moby/nodes_test.go b/discovery/moby/nodes_test.go
index 35676a3a8d..1f97016297 100644
--- a/discovery/moby/nodes_test.go
+++ b/discovery/moby/nodes_test.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
@@ -48,7 +48,11 @@ host: %s
defer metrics.Unregister()
defer refreshMetrics.Unregister()
- d, err := NewDiscovery(&cfg, promslog.NewNopLogger(), metrics)
+ d, err := NewDiscovery(&cfg, discovery.DiscovererOptions{
+ Logger: promslog.NewNopLogger(),
+ Metrics: metrics,
+ SetName: "docker_swarm",
+ })
require.NoError(t, err)
ctx := context.Background()
diff --git a/discovery/moby/services.go b/discovery/moby/services.go
index 0698c01e6a..558d544e25 100644
--- a/discovery/moby/services.go
+++ b/discovery/moby/services.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
diff --git a/discovery/moby/services_test.go b/discovery/moby/services_test.go
index af6ce842d1..eb5c75c71e 100644
--- a/discovery/moby/services_test.go
+++ b/discovery/moby/services_test.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
@@ -48,7 +48,11 @@ host: %s
defer metrics.Unregister()
defer refreshMetrics.Unregister()
- d, err := NewDiscovery(&cfg, promslog.NewNopLogger(), metrics)
+ d, err := NewDiscovery(&cfg, discovery.DiscovererOptions{
+ Logger: promslog.NewNopLogger(),
+ Metrics: metrics,
+ SetName: "moby",
+ })
require.NoError(t, err)
ctx := context.Background()
@@ -349,7 +353,11 @@ filters:
defer metrics.Unregister()
defer refreshMetrics.Unregister()
- d, err := NewDiscovery(&cfg, promslog.NewNopLogger(), metrics)
+ d, err := NewDiscovery(&cfg, discovery.DiscovererOptions{
+ Logger: promslog.NewNopLogger(),
+ Metrics: metrics,
+ SetName: "moby",
+ })
require.NoError(t, err)
ctx := context.Background()
diff --git a/discovery/moby/tasks.go b/discovery/moby/tasks.go
index 8a3dbe8101..d4e3678ee5 100644
--- a/discovery/moby/tasks.go
+++ b/discovery/moby/tasks.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
diff --git a/discovery/moby/tasks_test.go b/discovery/moby/tasks_test.go
index afb19abbee..60453990c4 100644
--- a/discovery/moby/tasks_test.go
+++ b/discovery/moby/tasks_test.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
@@ -48,7 +48,11 @@ host: %s
defer metrics.Unregister()
defer refreshMetrics.Unregister()
- d, err := NewDiscovery(&cfg, promslog.NewNopLogger(), metrics)
+ d, err := NewDiscovery(&cfg, discovery.DiscovererOptions{
+ Logger: promslog.NewNopLogger(),
+ Metrics: metrics,
+ SetName: "docker_swarm",
+ })
require.NoError(t, err)
ctx := context.Background()
diff --git a/discovery/nomad/metrics.go b/discovery/nomad/metrics.go
index 9707153d91..0e5dca4723 100644
--- a/discovery/nomad/metrics.go
+++ b/discovery/nomad/metrics.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
diff --git a/discovery/nomad/nomad.go b/discovery/nomad/nomad.go
index e204b740f7..da558f54d9 100644
--- a/discovery/nomad/nomad.go
+++ b/discovery/nomad/nomad.go
@@ -1,4 +1,4 @@
-// Copyright 2022 The Prometheus Authors
+// 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
@@ -17,7 +17,6 @@ import (
"context"
"errors"
"fmt"
- "log/slog"
"net"
"strconv"
"strings"
@@ -84,7 +83,7 @@ func (*SDConfig) Name() string { return "nomad" }
// NewDiscoverer returns a Discoverer for the Config.
func (c *SDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) {
- return NewDiscovery(c, opts.Logger, opts.Metrics)
+ return NewDiscovery(c, opts)
}
// SetDirectory joins any relative file paths with dir.
@@ -121,8 +120,8 @@ type Discovery struct {
}
// NewDiscovery returns a new Discovery which periodically refreshes its targets.
-func NewDiscovery(conf *SDConfig, logger *slog.Logger, metrics discovery.DiscovererMetrics) (*Discovery, error) {
- m, ok := metrics.(*nomadMetrics)
+func NewDiscovery(conf *SDConfig, opts discovery.DiscovererOptions) (*Discovery, error) {
+ m, ok := opts.Metrics.(*nomadMetrics)
if !ok {
return nil, errors.New("invalid discovery metrics type")
}
@@ -157,8 +156,9 @@ func NewDiscovery(conf *SDConfig, logger *slog.Logger, metrics discovery.Discove
d.Discovery = refresh.NewDiscovery(
refresh.Options{
- Logger: logger,
+ Logger: opts.Logger,
Mech: "nomad",
+ SetName: opts.SetName,
Interval: time.Duration(conf.RefreshInterval),
RefreshF: d.refresh,
MetricsInstantiator: m.refreshMetrics,
diff --git a/discovery/nomad/nomad_test.go b/discovery/nomad/nomad_test.go
index a73b45785d..3a4963e24b 100644
--- a/discovery/nomad/nomad_test.go
+++ b/discovery/nomad/nomad_test.go
@@ -1,4 +1,4 @@
-// Copyright 2022 The Prometheus Authors
+// 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
@@ -150,7 +150,11 @@ func TestConfiguredService(t *testing.T) {
require.NoError(t, metrics.Register())
defer metrics.Unregister()
- _, err := NewDiscovery(conf, nil, metrics)
+ _, err := NewDiscovery(conf, discovery.DiscovererOptions{
+ Logger: nil,
+ Metrics: metrics,
+ SetName: "nomad",
+ })
if tc.acceptedURL {
require.NoError(t, err)
} else {
@@ -178,7 +182,11 @@ func TestNomadSDRefresh(t *testing.T) {
defer metrics.Unregister()
defer refreshMetrics.Unregister()
- d, err := NewDiscovery(&cfg, promslog.NewNopLogger(), metrics)
+ d, err := NewDiscovery(&cfg, discovery.DiscovererOptions{
+ Logger: promslog.NewNopLogger(),
+ Metrics: metrics,
+ SetName: "nomad",
+ })
require.NoError(t, err)
tgs, err := d.refresh(context.Background())
diff --git a/discovery/openstack/hypervisor.go b/discovery/openstack/hypervisor.go
index e7a6362052..141b77c706 100644
--- a/discovery/openstack/hypervisor.go
+++ b/discovery/openstack/hypervisor.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
diff --git a/discovery/openstack/hypervisor_test.go b/discovery/openstack/hypervisor_test.go
index e4a97f32cf..afba84af2d 100644
--- a/discovery/openstack/hypervisor_test.go
+++ b/discovery/openstack/hypervisor_test.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
diff --git a/discovery/openstack/instance.go b/discovery/openstack/instance.go
index 58bf154555..2a6a777e9a 100644
--- a/discovery/openstack/instance.go
+++ b/discovery/openstack/instance.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
diff --git a/discovery/openstack/instance_test.go b/discovery/openstack/instance_test.go
index 0933b57067..aa202cddff 100644
--- a/discovery/openstack/instance_test.go
+++ b/discovery/openstack/instance_test.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
diff --git a/discovery/openstack/loadbalancer.go b/discovery/openstack/loadbalancer.go
index 254b713cdd..3b2def0d6a 100644
--- a/discovery/openstack/loadbalancer.go
+++ b/discovery/openstack/loadbalancer.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
diff --git a/discovery/openstack/loadbalancer_test.go b/discovery/openstack/loadbalancer_test.go
index eee21b9831..68be323a5a 100644
--- a/discovery/openstack/loadbalancer_test.go
+++ b/discovery/openstack/loadbalancer_test.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
diff --git a/discovery/openstack/metrics.go b/discovery/openstack/metrics.go
index 664f5ea6bc..01e7ab3add 100644
--- a/discovery/openstack/metrics.go
+++ b/discovery/openstack/metrics.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
diff --git a/discovery/openstack/mock_test.go b/discovery/openstack/mock_test.go
index 34e09c710f..c44dadfbc0 100644
--- a/discovery/openstack/mock_test.go
+++ b/discovery/openstack/mock_test.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
diff --git a/discovery/openstack/openstack.go b/discovery/openstack/openstack.go
index 7f23757297..ce365e6cd0 100644
--- a/discovery/openstack/openstack.go
+++ b/discovery/openstack/openstack.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
@@ -78,7 +78,7 @@ func (*SDConfig) Name() string { return "openstack" }
// NewDiscoverer returns a Discoverer for the Config.
func (c *SDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) {
- return NewDiscovery(c, opts.Logger, opts.Metrics)
+ return NewDiscovery(c, opts)
}
// SetDirectory joins any relative file paths with dir.
@@ -145,20 +145,21 @@ type refresher interface {
}
// NewDiscovery returns a new OpenStack Discoverer which periodically refreshes its targets.
-func NewDiscovery(conf *SDConfig, l *slog.Logger, metrics discovery.DiscovererMetrics) (*refresh.Discovery, error) {
- m, ok := metrics.(*openstackMetrics)
+func NewDiscovery(conf *SDConfig, opts discovery.DiscovererOptions) (*refresh.Discovery, error) {
+ m, ok := opts.Metrics.(*openstackMetrics)
if !ok {
return nil, errors.New("invalid discovery metrics type")
}
- r, err := newRefresher(conf, l)
+ r, err := newRefresher(conf, opts.Logger)
if err != nil {
return nil, err
}
return refresh.NewDiscovery(
refresh.Options{
- Logger: l,
+ Logger: opts.Logger,
Mech: "openstack",
+ SetName: opts.SetName,
Interval: time.Duration(conf.RefreshInterval),
RefreshF: r.refresh,
MetricsInstantiator: m.refreshMetrics,
diff --git a/discovery/ovhcloud/dedicated_server.go b/discovery/ovhcloud/dedicated_server.go
index 2035e92c91..e892607c34 100644
--- a/discovery/ovhcloud/dedicated_server.go
+++ b/discovery/ovhcloud/dedicated_server.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
diff --git a/discovery/ovhcloud/dedicated_server_test.go b/discovery/ovhcloud/dedicated_server_test.go
index 686fa7ef3f..84fa2c4c12 100644
--- a/discovery/ovhcloud/dedicated_server_test.go
+++ b/discovery/ovhcloud/dedicated_server_test.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
diff --git a/discovery/ovhcloud/metrics.go b/discovery/ovhcloud/metrics.go
index 18492c0ab4..dbcfe130e9 100644
--- a/discovery/ovhcloud/metrics.go
+++ b/discovery/ovhcloud/metrics.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
diff --git a/discovery/ovhcloud/ovhcloud.go b/discovery/ovhcloud/ovhcloud.go
index 69c7cd6004..863fcfeaf9 100644
--- a/discovery/ovhcloud/ovhcloud.go
+++ b/discovery/ovhcloud/ovhcloud.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
@@ -100,8 +100,8 @@ func createClient(config *SDConfig) (*ovh.Client, error) {
}
// NewDiscoverer returns a Discoverer for the Config.
-func (c *SDConfig) NewDiscoverer(options discovery.DiscovererOptions) (discovery.Discoverer, error) {
- return NewDiscovery(c, options.Logger, options.Metrics)
+func (c *SDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) {
+ return NewDiscovery(c, opts)
}
func init() {
@@ -148,21 +148,22 @@ func newRefresher(conf *SDConfig, logger *slog.Logger) (refresher, error) {
}
// NewDiscovery returns a new OVHcloud Discoverer which periodically refreshes its targets.
-func NewDiscovery(conf *SDConfig, logger *slog.Logger, metrics discovery.DiscovererMetrics) (*refresh.Discovery, error) {
- m, ok := metrics.(*ovhcloudMetrics)
+func NewDiscovery(conf *SDConfig, opts discovery.DiscovererOptions) (*refresh.Discovery, error) {
+ m, ok := opts.Metrics.(*ovhcloudMetrics)
if !ok {
return nil, errors.New("invalid discovery metrics type")
}
- r, err := newRefresher(conf, logger)
+ r, err := newRefresher(conf, opts.Logger)
if err != nil {
return nil, err
}
return refresh.NewDiscovery(
refresh.Options{
- Logger: logger,
+ Logger: opts.Logger,
Mech: "ovhcloud",
+ SetName: opts.SetName,
Interval: time.Duration(conf.RefreshInterval),
RefreshF: r.refresh,
MetricsInstantiator: m.refreshMetrics,
diff --git a/discovery/ovhcloud/ovhcloud_test.go b/discovery/ovhcloud/ovhcloud_test.go
index 8f2272b746..acb1c43fad 100644
--- a/discovery/ovhcloud/ovhcloud_test.go
+++ b/discovery/ovhcloud/ovhcloud_test.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
diff --git a/discovery/ovhcloud/vps.go b/discovery/ovhcloud/vps.go
index 4e71a877bc..4023c4ff49 100644
--- a/discovery/ovhcloud/vps.go
+++ b/discovery/ovhcloud/vps.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
diff --git a/discovery/ovhcloud/vps_test.go b/discovery/ovhcloud/vps_test.go
index 051d52e85e..d997f2bb0e 100644
--- a/discovery/ovhcloud/vps_test.go
+++ b/discovery/ovhcloud/vps_test.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
diff --git a/discovery/puppetdb/metrics.go b/discovery/puppetdb/metrics.go
index 83e7975ed5..5a8e9736c2 100644
--- a/discovery/puppetdb/metrics.go
+++ b/discovery/puppetdb/metrics.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
diff --git a/discovery/puppetdb/puppetdb.go b/discovery/puppetdb/puppetdb.go
index a5163addb0..52a1cf73c6 100644
--- a/discovery/puppetdb/puppetdb.go
+++ b/discovery/puppetdb/puppetdb.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
@@ -20,7 +20,6 @@ import (
"errors"
"fmt"
"io"
- "log/slog"
"net"
"net/http"
"net/url"
@@ -93,7 +92,7 @@ func (*SDConfig) Name() string { return "puppetdb" }
// NewDiscoverer returns a Discoverer for the Config.
func (c *SDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) {
- return NewDiscovery(c, opts.Logger, opts.Metrics)
+ return NewDiscovery(c, opts)
}
// SetDirectory joins any relative file paths with dir.
@@ -140,14 +139,14 @@ type Discovery struct {
}
// NewDiscovery returns a new PuppetDB discovery for the given config.
-func NewDiscovery(conf *SDConfig, logger *slog.Logger, metrics discovery.DiscovererMetrics) (*Discovery, error) {
- m, ok := metrics.(*puppetdbMetrics)
+func NewDiscovery(conf *SDConfig, opts discovery.DiscovererOptions) (*Discovery, error) {
+ m, ok := opts.Metrics.(*puppetdbMetrics)
if !ok {
return nil, errors.New("invalid discovery metrics type")
}
- if logger == nil {
- logger = promslog.NewNopLogger()
+ if opts.Logger == nil {
+ opts.Logger = promslog.NewNopLogger()
}
client, err := config.NewClientFromConfig(conf.HTTPClientConfig, "http")
@@ -172,8 +171,9 @@ func NewDiscovery(conf *SDConfig, logger *slog.Logger, metrics discovery.Discove
d.Discovery = refresh.NewDiscovery(
refresh.Options{
- Logger: logger,
+ Logger: opts.Logger,
Mech: "puppetdb",
+ SetName: opts.SetName,
Interval: time.Duration(conf.RefreshInterval),
RefreshF: d.refresh,
MetricsInstantiator: m.refreshMetrics,
diff --git a/discovery/puppetdb/puppetdb_test.go b/discovery/puppetdb/puppetdb_test.go
index 57e198e131..b12835b47c 100644
--- a/discovery/puppetdb/puppetdb_test.go
+++ b/discovery/puppetdb/puppetdb_test.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
@@ -70,7 +70,11 @@ func TestPuppetSlashInURL(t *testing.T) {
metrics := cfg.NewDiscovererMetrics(reg, refreshMetrics)
require.NoError(t, metrics.Register())
- d, err := NewDiscovery(&cfg, promslog.NewNopLogger(), metrics)
+ d, err := NewDiscovery(&cfg, discovery.DiscovererOptions{
+ Logger: promslog.NewNopLogger(),
+ Metrics: metrics,
+ SetName: "puppetdb",
+ })
require.NoError(t, err)
require.Equal(t, apiURL, d.url)
@@ -94,7 +98,11 @@ func TestPuppetDBRefresh(t *testing.T) {
metrics := cfg.NewDiscovererMetrics(reg, refreshMetrics)
require.NoError(t, metrics.Register())
- d, err := NewDiscovery(&cfg, promslog.NewNopLogger(), metrics)
+ d, err := NewDiscovery(&cfg, discovery.DiscovererOptions{
+ Logger: promslog.NewNopLogger(),
+ Metrics: metrics,
+ SetName: "puppetdb",
+ })
require.NoError(t, err)
ctx := context.Background()
@@ -142,7 +150,11 @@ func TestPuppetDBRefreshWithParameters(t *testing.T) {
metrics := cfg.NewDiscovererMetrics(reg, refreshMetrics)
require.NoError(t, metrics.Register())
- d, err := NewDiscovery(&cfg, promslog.NewNopLogger(), metrics)
+ d, err := NewDiscovery(&cfg, discovery.DiscovererOptions{
+ Logger: promslog.NewNopLogger(),
+ Metrics: metrics,
+ SetName: "puppetdb",
+ })
require.NoError(t, err)
ctx := context.Background()
@@ -201,7 +213,11 @@ func TestPuppetDBInvalidCode(t *testing.T) {
metrics := cfg.NewDiscovererMetrics(reg, refreshMetrics)
require.NoError(t, metrics.Register())
- d, err := NewDiscovery(&cfg, promslog.NewNopLogger(), metrics)
+ d, err := NewDiscovery(&cfg, discovery.DiscovererOptions{
+ Logger: promslog.NewNopLogger(),
+ Metrics: metrics,
+ SetName: "puppetdb",
+ })
require.NoError(t, err)
ctx := context.Background()
@@ -229,7 +245,11 @@ func TestPuppetDBInvalidFormat(t *testing.T) {
metrics := cfg.NewDiscovererMetrics(reg, refreshMetrics)
require.NoError(t, metrics.Register())
- d, err := NewDiscovery(&cfg, promslog.NewNopLogger(), metrics)
+ d, err := NewDiscovery(&cfg, discovery.DiscovererOptions{
+ Logger: promslog.NewNopLogger(),
+ Metrics: metrics,
+ SetName: "puppetdb",
+ })
require.NoError(t, err)
ctx := context.Background()
diff --git a/discovery/puppetdb/resources.go b/discovery/puppetdb/resources.go
index 487c471c1b..09aa43a776 100644
--- a/discovery/puppetdb/resources.go
+++ b/discovery/puppetdb/resources.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
diff --git a/discovery/refresh/refresh.go b/discovery/refresh/refresh.go
index 31646c0e4c..3e766d1c84 100644
--- a/discovery/refresh/refresh.go
+++ b/discovery/refresh/refresh.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The Prometheus Authors
+// 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
@@ -28,6 +28,7 @@ import (
type Options struct {
Logger *slog.Logger
Mech string
+ SetName string
Interval time.Duration
RefreshF func(ctx context.Context) ([]*targetgroup.Group, error)
MetricsInstantiator discovery.RefreshMetricsInstantiator
@@ -43,7 +44,7 @@ type Discovery struct {
// NewDiscovery returns a Discoverer function that calls a refresh() function at every interval.
func NewDiscovery(opts Options) *Discovery {
- m := opts.MetricsInstantiator.Instantiate(opts.Mech)
+ m := opts.MetricsInstantiator.Instantiate(opts.Mech, opts.SetName)
var logger *slog.Logger
if opts.Logger == nil {
@@ -107,6 +108,7 @@ func (d *Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error) {
now := time.Now()
defer func() {
d.metrics.Duration.Observe(time.Since(now).Seconds())
+ d.metrics.DurationHistogram.Observe(time.Since(now).Seconds())
}()
tgs, err := d.refreshf(ctx)
diff --git a/discovery/refresh/refresh_test.go b/discovery/refresh/refresh_test.go
index a6213db6c8..e227d0abc9 100644
--- a/discovery/refresh/refresh_test.go
+++ b/discovery/refresh/refresh_test.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The Prometheus Authors
+// 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
@@ -76,6 +76,7 @@ func TestRefresh(t *testing.T) {
Options{
Logger: nil,
Mech: "test",
+ SetName: "test-refresh",
Interval: interval,
RefreshF: refresh,
MetricsInstantiator: metrics,
diff --git a/discovery/registry.go b/discovery/registry.go
index 33938cef3e..04145e72e4 100644
--- a/discovery/registry.go
+++ b/discovery/registry.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
@@ -280,3 +280,13 @@ func RegisterSDMetrics(registerer prometheus.Registerer, rmm RefreshMetricsManag
}
return metrics, nil
}
+
+// RegisteredConfigNames returns the names of all registered service discovery providers.
+func RegisteredConfigNames() []string {
+ names := make([]string, 0, len(configNames))
+ for name := range configNames {
+ names = append(names, name)
+ }
+ sort.Strings(names)
+ return names
+}
diff --git a/discovery/scaleway/baremetal.go b/discovery/scaleway/baremetal.go
index 06f13532df..347ed40bab 100644
--- a/discovery/scaleway/baremetal.go
+++ b/discovery/scaleway/baremetal.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
diff --git a/discovery/scaleway/instance.go b/discovery/scaleway/instance.go
index 162a75e407..c0ed5853b3 100644
--- a/discovery/scaleway/instance.go
+++ b/discovery/scaleway/instance.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
diff --git a/discovery/scaleway/instance_test.go b/discovery/scaleway/instance_test.go
index b67b858ae0..2d0f7a67ff 100644
--- a/discovery/scaleway/instance_test.go
+++ b/discovery/scaleway/instance_test.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
diff --git a/discovery/scaleway/metrics.go b/discovery/scaleway/metrics.go
index d7a4e78556..5871f7e31b 100644
--- a/discovery/scaleway/metrics.go
+++ b/discovery/scaleway/metrics.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
diff --git a/discovery/scaleway/scaleway.go b/discovery/scaleway/scaleway.go
index d617e01905..f8ef6c706c 100644
--- a/discovery/scaleway/scaleway.go
+++ b/discovery/scaleway/scaleway.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
@@ -17,7 +17,6 @@ import (
"context"
"errors"
"fmt"
- "log/slog"
"net/http"
"os"
"strings"
@@ -167,8 +166,8 @@ func (c *SDConfig) UnmarshalYAML(unmarshal func(any) error) error {
return c.HTTPClientConfig.Validate()
}
-func (c SDConfig) NewDiscoverer(options discovery.DiscovererOptions) (discovery.Discoverer, error) {
- return NewDiscovery(&c, options.Logger, options.Metrics)
+func (c SDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) {
+ return NewDiscovery(&c, opts)
}
// SetDirectory joins any relative file paths with dir.
@@ -185,8 +184,8 @@ func init() {
// the Discoverer interface.
type Discovery struct{}
-func NewDiscovery(conf *SDConfig, logger *slog.Logger, metrics discovery.DiscovererMetrics) (*refresh.Discovery, error) {
- m, ok := metrics.(*scalewayMetrics)
+func NewDiscovery(conf *SDConfig, opts discovery.DiscovererOptions) (*refresh.Discovery, error) {
+ m, ok := opts.Metrics.(*scalewayMetrics)
if !ok {
return nil, errors.New("invalid discovery metrics type")
}
@@ -198,8 +197,9 @@ func NewDiscovery(conf *SDConfig, logger *slog.Logger, metrics discovery.Discove
return refresh.NewDiscovery(
refresh.Options{
- Logger: logger,
+ Logger: opts.Logger,
Mech: "scaleway",
+ SetName: opts.SetName,
Interval: time.Duration(conf.RefreshInterval),
RefreshF: r.refresh,
MetricsInstantiator: m.refreshMetrics,
diff --git a/discovery/stackit/metrics.go b/discovery/stackit/metrics.go
index 5ba565eb9c..a44d0728e3 100644
--- a/discovery/stackit/metrics.go
+++ b/discovery/stackit/metrics.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
diff --git a/discovery/stackit/mock_test.go b/discovery/stackit/mock_test.go
index 59641ce2bc..d1366508a3 100644
--- a/discovery/stackit/mock_test.go
+++ b/discovery/stackit/mock_test.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
diff --git a/discovery/stackit/server.go b/discovery/stackit/server.go
index 1be834a689..c553d9b3f3 100644
--- a/discovery/stackit/server.go
+++ b/discovery/stackit/server.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
diff --git a/discovery/stackit/server_test.go b/discovery/stackit/server_test.go
index 117fbdd66d..afb9460851 100644
--- a/discovery/stackit/server_test.go
+++ b/discovery/stackit/server_test.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
diff --git a/discovery/stackit/stackit.go b/discovery/stackit/stackit.go
index 351526e016..bae76c8897 100644
--- a/discovery/stackit/stackit.go
+++ b/discovery/stackit/stackit.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
@@ -87,7 +87,7 @@ func (*SDConfig) Name() string { return "stackit" }
// NewDiscoverer returns a Discoverer for the Config.
func (c *SDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) {
- return NewDiscovery(c, opts.Logger, opts.Metrics)
+ return NewDiscovery(c, opts)
}
type refresher interface {
@@ -126,21 +126,22 @@ type Discovery struct {
}
// NewDiscovery returns a new Discovery which periodically refreshes its targets.
-func NewDiscovery(conf *SDConfig, logger *slog.Logger, metrics discovery.DiscovererMetrics) (*refresh.Discovery, error) {
- m, ok := metrics.(*stackitMetrics)
+func NewDiscovery(conf *SDConfig, opts discovery.DiscovererOptions) (*refresh.Discovery, error) {
+ m, ok := opts.Metrics.(*stackitMetrics)
if !ok {
return nil, errors.New("invalid discovery metrics type")
}
- r, err := newRefresher(conf, logger)
+ r, err := newRefresher(conf, opts.Logger)
if err != nil {
return nil, err
}
return refresh.NewDiscovery(
refresh.Options{
- Logger: logger,
+ Logger: opts.Logger,
Mech: "stackit",
+ SetName: opts.SetName,
Interval: time.Duration(conf.RefreshInterval),
RefreshF: r.refresh,
MetricsInstantiator: m.refreshMetrics,
diff --git a/discovery/stackit/types.go b/discovery/stackit/types.go
index 84b7d0266c..575acbbe56 100644
--- a/discovery/stackit/types.go
+++ b/discovery/stackit/types.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
diff --git a/discovery/targetgroup/targetgroup.go b/discovery/targetgroup/targetgroup.go
index 5c3b67d6e8..4b1670ae1b 100644
--- a/discovery/targetgroup/targetgroup.go
+++ b/discovery/targetgroup/targetgroup.go
@@ -1,4 +1,4 @@
-// Copyright 2013 The Prometheus Authors
+// 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
diff --git a/discovery/targetgroup/targetgroup_test.go b/discovery/targetgroup/targetgroup_test.go
index d68e29644a..1c1583d33d 100644
--- a/discovery/targetgroup/targetgroup_test.go
+++ b/discovery/targetgroup/targetgroup_test.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Prometheus Authors
+// 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
diff --git a/discovery/triton/metrics.go b/discovery/triton/metrics.go
index ea98eae452..2d4193ee1f 100644
--- a/discovery/triton/metrics.go
+++ b/discovery/triton/metrics.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
diff --git a/discovery/triton/triton.go b/discovery/triton/triton.go
index 9300753015..b21beef9d0 100644
--- a/discovery/triton/triton.go
+++ b/discovery/triton/triton.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
@@ -19,7 +19,6 @@ import (
"errors"
"fmt"
"io"
- "log/slog"
"net/http"
"net/url"
"strings"
@@ -82,7 +81,7 @@ func (*SDConfig) Name() string { return "triton" }
// NewDiscoverer returns a Discoverer for the Config.
func (c *SDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) {
- return New(opts.Logger, c, opts.Metrics)
+ return New(c, opts)
}
// SetDirectory joins any relative file paths with dir.
@@ -146,8 +145,8 @@ type Discovery struct {
}
// New returns a new Discovery which periodically refreshes its targets.
-func New(logger *slog.Logger, conf *SDConfig, metrics discovery.DiscovererMetrics) (*Discovery, error) {
- m, ok := metrics.(*tritonMetrics)
+func New(conf *SDConfig, opts discovery.DiscovererOptions) (*Discovery, error) {
+ m, ok := opts.Metrics.(*tritonMetrics)
if !ok {
return nil, errors.New("invalid discovery metrics type")
}
@@ -173,8 +172,9 @@ func New(logger *slog.Logger, conf *SDConfig, metrics discovery.DiscovererMetric
}
d.Discovery = refresh.NewDiscovery(
refresh.Options{
- Logger: logger,
+ Logger: opts.Logger,
Mech: "triton",
+ SetName: opts.SetName,
Interval: time.Duration(conf.RefreshInterval),
RefreshF: d.refresh,
MetricsInstantiator: m.refreshMetrics,
diff --git a/discovery/triton/triton_test.go b/discovery/triton/triton_test.go
index b0dccbf898..f2b6398bc8 100644
--- a/discovery/triton/triton_test.go
+++ b/discovery/triton/triton_test.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The Prometheus Authors
+// 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
@@ -83,14 +83,17 @@ var (
func newTritonDiscovery(c SDConfig) (*Discovery, discovery.DiscovererMetrics, error) {
reg := prometheus.NewRegistry()
refreshMetrics := discovery.NewRefreshMetrics(reg)
- // TODO(ptodev): Add the ability to unregister refresh metrics.
metrics := c.NewDiscovererMetrics(reg, refreshMetrics)
err := metrics.Register()
if err != nil {
return nil, nil, err
}
- d, err := New(nil, &c, metrics)
+ d, err := New(&c, discovery.DiscovererOptions{
+ Logger: nil,
+ Metrics: metrics,
+ SetName: "triton",
+ })
if err != nil {
return nil, nil, err
}
diff --git a/discovery/util.go b/discovery/util.go
index 4e2a088518..064a5312a7 100644
--- a/discovery/util.go
+++ b/discovery/util.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
diff --git a/discovery/uyuni/metrics.go b/discovery/uyuni/metrics.go
index 85ea9d73d2..e1a9fd4db0 100644
--- a/discovery/uyuni/metrics.go
+++ b/discovery/uyuni/metrics.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
diff --git a/discovery/uyuni/uyuni.go b/discovery/uyuni/uyuni.go
index 6419d8d365..6f29fa130c 100644
--- a/discovery/uyuni/uyuni.go
+++ b/discovery/uyuni/uyuni.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
@@ -124,7 +124,7 @@ func (*SDConfig) Name() string { return "uyuni" }
// NewDiscoverer returns a Discoverer for the Config.
func (c *SDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) {
- return NewDiscovery(c, opts.Logger, opts.Metrics)
+ return NewDiscovery(c, opts)
}
// SetDirectory joins any relative file paths with dir.
@@ -213,8 +213,8 @@ func getEndpointInfoForSystems(
}
// NewDiscovery returns a uyuni discovery for the given configuration.
-func NewDiscovery(conf *SDConfig, logger *slog.Logger, metrics discovery.DiscovererMetrics) (*Discovery, error) {
- m, ok := metrics.(*uyuniMetrics)
+func NewDiscovery(conf *SDConfig, opts discovery.DiscovererOptions) (*Discovery, error) {
+ m, ok := opts.Metrics.(*uyuniMetrics)
if !ok {
return nil, errors.New("invalid discovery metrics type")
}
@@ -238,13 +238,14 @@ func NewDiscovery(conf *SDConfig, logger *slog.Logger, metrics discovery.Discove
entitlement: conf.Entitlement,
separator: conf.Separator,
interval: time.Duration(conf.RefreshInterval),
- logger: logger,
+ logger: opts.Logger,
}
d.Discovery = refresh.NewDiscovery(
refresh.Options{
- Logger: logger,
+ Logger: opts.Logger,
Mech: "uyuni",
+ SetName: opts.SetName,
Interval: time.Duration(conf.RefreshInterval),
RefreshF: d.refresh,
MetricsInstantiator: m.refreshMetrics,
diff --git a/discovery/uyuni/uyuni_test.go b/discovery/uyuni/uyuni_test.go
index 46567587a8..71f1c5afb1 100644
--- a/discovery/uyuni/uyuni_test.go
+++ b/discovery/uyuni/uyuni_test.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
@@ -47,7 +47,11 @@ func testUpdateServices(respHandler http.HandlerFunc) ([]*targetgroup.Group, err
defer metrics.Unregister()
defer refreshMetrics.Unregister()
- md, err := NewDiscovery(&conf, nil, metrics)
+ md, err := NewDiscovery(&conf, discovery.DiscovererOptions{
+ Logger: nil,
+ Metrics: metrics,
+ SetName: "uyuni",
+ })
if err != nil {
return nil, err
}
@@ -127,7 +131,11 @@ func TestUyuniSDSkipLogin(t *testing.T) {
defer metrics.Unregister()
defer refreshMetrics.Unregister()
- md, err := NewDiscovery(&conf, nil, metrics)
+ md, err := NewDiscovery(&conf, discovery.DiscovererOptions{
+ Logger: nil,
+ Metrics: metrics,
+ SetName: "uyuni",
+ })
if err != nil {
t.Error(err)
}
diff --git a/discovery/vultr/metrics.go b/discovery/vultr/metrics.go
index 65b15eae2f..823fe4bdc0 100644
--- a/discovery/vultr/metrics.go
+++ b/discovery/vultr/metrics.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
diff --git a/discovery/vultr/mock_test.go b/discovery/vultr/mock_test.go
index bfc24d06fb..03e5952dd0 100644
--- a/discovery/vultr/mock_test.go
+++ b/discovery/vultr/mock_test.go
@@ -1,4 +1,4 @@
-// Copyright 2022 The Prometheus Authors
+// 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
diff --git a/discovery/vultr/vultr.go b/discovery/vultr/vultr.go
index 79a7a0179f..b2f6bde52a 100644
--- a/discovery/vultr/vultr.go
+++ b/discovery/vultr/vultr.go
@@ -1,4 +1,4 @@
-// Copyright 2022 The Prometheus Authors
+// 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
@@ -16,7 +16,6 @@ package vultr
import (
"context"
"errors"
- "log/slog"
"net"
"net/http"
"strconv"
@@ -86,7 +85,7 @@ func (*SDConfig) Name() string { return "vultr" }
// NewDiscoverer returns a Discoverer for the Config.
func (c *SDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) {
- return NewDiscovery(c, opts.Logger, opts.Metrics)
+ return NewDiscovery(c, opts)
}
// SetDirectory joins any relative file paths with dir.
@@ -114,8 +113,8 @@ type Discovery struct {
}
// NewDiscovery returns a new Discovery which periodically refreshes its targets.
-func NewDiscovery(conf *SDConfig, logger *slog.Logger, metrics discovery.DiscovererMetrics) (*Discovery, error) {
- m, ok := metrics.(*vultrMetrics)
+func NewDiscovery(conf *SDConfig, opts discovery.DiscovererOptions) (*Discovery, error) {
+ m, ok := opts.Metrics.(*vultrMetrics)
if !ok {
return nil, errors.New("invalid discovery metrics type")
}
@@ -138,8 +137,9 @@ func NewDiscovery(conf *SDConfig, logger *slog.Logger, metrics discovery.Discove
d.Discovery = refresh.NewDiscovery(
refresh.Options{
- Logger: logger,
+ Logger: opts.Logger,
Mech: "vultr",
+ SetName: opts.SetName,
Interval: time.Duration(conf.RefreshInterval),
RefreshF: d.refresh,
MetricsInstantiator: m.refreshMetrics,
diff --git a/discovery/vultr/vultr_test.go b/discovery/vultr/vultr_test.go
index 00ef21e38c..d116c419b7 100644
--- a/discovery/vultr/vultr_test.go
+++ b/discovery/vultr/vultr_test.go
@@ -1,4 +1,4 @@
-// Copyright 2022 The Prometheus Authors
+// 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
@@ -57,7 +57,11 @@ func TestVultrSDRefresh(t *testing.T) {
defer metrics.Unregister()
defer refreshMetrics.Unregister()
- d, err := NewDiscovery(&cfg, promslog.NewNopLogger(), metrics)
+ d, err := NewDiscovery(&cfg, discovery.DiscovererOptions{
+ Logger: promslog.NewNopLogger(),
+ Metrics: metrics,
+ SetName: "vultr",
+ })
require.NoError(t, err)
endpoint, err := url.Parse(sdMock.Mock.Endpoint())
require.NoError(t, err)
diff --git a/discovery/xds/client.go b/discovery/xds/client.go
index a27e060fbd..59485ffcba 100644
--- a/discovery/xds/client.go
+++ b/discovery/xds/client.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
diff --git a/discovery/xds/client_test.go b/discovery/xds/client_test.go
index 7e3cd85b6c..e663902161 100644
--- a/discovery/xds/client_test.go
+++ b/discovery/xds/client_test.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
diff --git a/discovery/xds/kuma.go b/discovery/xds/kuma.go
index 82ca8f2c9a..34bebe7765 100644
--- a/discovery/xds/kuma.go
+++ b/discovery/xds/kuma.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
diff --git a/discovery/xds/kuma_mads.pb.go b/discovery/xds/kuma_mads.pb.go
index 210a5343a4..d234241453 100644
--- a/discovery/xds/kuma_mads.pb.go
+++ b/discovery/xds/kuma_mads.pb.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
diff --git a/discovery/xds/kuma_test.go b/discovery/xds/kuma_test.go
index 533a31dcf3..6620f9fac6 100644
--- a/discovery/xds/kuma_test.go
+++ b/discovery/xds/kuma_test.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
@@ -111,7 +111,6 @@ func getKumaMadsV1DiscoveryResponse(resources ...*MonitoringAssignment) (*v3.Dis
func newKumaTestHTTPDiscovery(c KumaSDConfig) (*fetchDiscovery, error) {
reg := prometheus.NewRegistry()
refreshMetrics := discovery.NewRefreshMetrics(reg)
- // TODO(ptodev): Add the ability to unregister refresh metrics.
metrics := c.NewDiscovererMetrics(reg, refreshMetrics)
err := metrics.Register()
if err != nil {
diff --git a/discovery/xds/metrics.go b/discovery/xds/metrics.go
index bdc9598f2c..7e5be89bd3 100644
--- a/discovery/xds/metrics.go
+++ b/discovery/xds/metrics.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
diff --git a/discovery/xds/xds.go b/discovery/xds/xds.go
index db55a2b6f7..29da7b7c89 100644
--- a/discovery/xds/xds.go
+++ b/discovery/xds/xds.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
diff --git a/discovery/xds/xds_test.go b/discovery/xds/xds_test.go
index 5a2e9d737b..c11cdd2c05 100644
--- a/discovery/xds/xds_test.go
+++ b/discovery/xds/xds_test.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
diff --git a/discovery/zookeeper/zookeeper.go b/discovery/zookeeper/zookeeper.go
index d5239324cb..6ac9b25cd6 100644
--- a/discovery/zookeeper/zookeeper.go
+++ b/discovery/zookeeper/zookeeper.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
diff --git a/discovery/zookeeper/zookeeper_test.go b/discovery/zookeeper/zookeeper_test.go
index de0d1f4924..ae2d23e607 100644
--- a/discovery/zookeeper/zookeeper_test.go
+++ b/discovery/zookeeper/zookeeper_test.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Prometheus Authors
+// 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
diff --git a/docs/command-line/prometheus.md b/docs/command-line/prometheus.md
index 0396f90bee..251fdfd6a4 100644
--- a/docs/command-line/prometheus.md
+++ b/docs/command-line/prometheus.md
@@ -38,6 +38,7 @@ The Prometheus monitoring server
| --storage.tsdb.retention.size | [DEPRECATED] Maximum number of bytes that can be stored for blocks. A unit is required, supported units: B, KB, MB, GB, TB, PB, EB. Ex: "512MB". Based on powers-of-2, so 1KB is 1024B. This flag has been deprecated, use the storage.tsdb.retention.size field in the config file instead. Use with server mode only. | |
| --storage.tsdb.no-lockfile | Do not create lockfile in data directory. Use with server mode only. | `false` |
| --storage.tsdb.head-chunks-write-queue-size | Size of the queue through which head chunks are written to the disk to be m-mapped, 0 disables the queue completely. Experimental. Use with server mode only. | `0` |
+| --storage.tsdb.delay-compact-file.path | Path to a JSON file with uploaded TSDB blocks e.g. Thanos shipper meta file. If set TSDB will only compact 1 level blocks that are marked as uploaded in that file, improving external storage integrations e.g. with Thanos sidecar. 1+ level compactions won't be delayed. Use with server mode only. | |
| --storage.agent.path | Base path for metrics storage. Use with agent mode only. | `data-agent/` |
| --storage.agent.wal-compression | Compress the agent WAL. If false, the --storage.agent.wal-compression-type flag is ignored. Use with agent mode only. | `true` |
| --storage.agent.retention.min-time | Minimum age samples may be before being considered for deletion when the WAL is truncated Use with agent mode only. | |
@@ -58,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, native-histograms, 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/command-line/promtool.md b/docs/command-line/promtool.md
index 69fcd265f2..e8ffa75aaa 100644
--- a/docs/command-line/promtool.md
+++ b/docs/command-line/promtool.md
@@ -12,7 +12,7 @@ Tooling for the Prometheus monitoring system.
| -h, --help | Show context-sensitive help (also try --help-long and --help-man). |
| --version | Show application version. |
| --experimental | Enable experimental commands. |
-| --enable-feature ... | Comma separated feature names to enable. Valid options: promql-experimental-functions, promql-delayed-name-removal. See https://prometheus.io/docs/prometheus/latest/feature_flags/ for more details |
+| --enable-feature ... | Comma separated feature names to enable. Valid options: promql-experimental-functions, promql-delayed-name-removal, promql-duration-expr, promql-extended-range-selectors. See https://prometheus.io/docs/prometheus/latest/feature_flags/ for more details |
@@ -59,7 +59,6 @@ Check the resources for validity.
| Flag | Description | Default |
| --- | --- | --- |
| --query.lookback-delta | The server's maximum query lookback duration. | `5m` |
-| --extended | Print extended information related to the cardinality of the metrics. | |
@@ -192,13 +191,25 @@ Check if the rule files are valid or not.
##### `promtool check metrics`
-Pass Prometheus metrics over stdin to lint them for consistency and correctness.
+Pass Prometheus metrics over stdin to lint them for consistency and correctness, and optionally perform cardinality analysis.
examples:
$ cat metrics.prom | promtool check metrics
-$ curl -s http://localhost:9090/metrics | promtool check metrics
+$ curl -s http://localhost:9090/metrics | promtool check metrics `--extended`
+
+$ curl -s http://localhost:9100/metrics | promtool check metrics `--extended` `--lint`=none
+
+
+
+###### Flags
+
+| Flag | Description | Default |
+| --- | --- | --- |
+| --extended | Print extended information related to the cardinality of the metrics. | |
+| --lint | Linting checks to apply for metrics. Available options are: all, none. Use --lint=none to disable metrics linting. | `all` |
+
@@ -570,7 +581,7 @@ List tsdb blocks.
##### `promtool tsdb dump`
-Dump samples from a TSDB.
+Dump data (series+samples or optionally just series) from a TSDB.
@@ -582,6 +593,7 @@ Dump samples from a TSDB.
| --min-time | Minimum timestamp to dump, in milliseconds since the Unix epoch. | `-9223372036854775808` |
| --max-time | Maximum timestamp to dump, in milliseconds since the Unix epoch. | `9223372036854775807` |
| --match ... | Series selector. Can be specified multiple times. | `{__name__=~'(?s:.*)'}` |
+| --format | Output format of the dump (prom (default) or seriesjson). | `prom` |
diff --git a/docs/configuration/configuration.md b/docs/configuration/configuration.md
index 7eab633987..ff4eb4c2cf 100644
--- a/docs/configuration/configuration.md
+++ b/docs/configuration/configuration.md
@@ -159,6 +159,12 @@ global:
# native histogram with custom buckets.
[ always_scrape_classic_histograms: | default = false ]
+ # When enabled, Prometheus stores additional time series for each scrape:
+ # scrape_timeout_seconds, scrape_sample_limit, and scrape_body_size_bytes.
+ # These metrics help monitor how close targets are to their configured limits.
+ # This option can be overridden per scrape config.
+ [ extra_scrape_metrics: | default = false ]
+
# The following explains the various combinations of the last three options
# in various exposition cases.
#
@@ -395,6 +401,10 @@ params:
# authorization), proxy configurations, TLS options, custom HTTP headers, etc.
[ ]
+# List of AWS service discovery configurations.
+aws_sd_configs:
+ [ - ... ]
+
# List of Azure service discovery configurations.
azure_sd_configs:
[ - ... ]
@@ -643,6 +653,12 @@ metric_relabel_configs:
# native histogram with custom buckets.
[ always_scrape_classic_histograms: | default = ]
+# When enabled, Prometheus stores additional time series for this scrape job:
+# scrape_timeout_seconds, scrape_sample_limit, and scrape_body_size_bytes.
+# These metrics help monitor how close targets are to their configured limits.
+# If not set, inherits the value from the global configuration.
+[ extra_scrape_metrics: | default = ]
+
# See global configuration above for further explanations of how the last three
# options combine their effects.
@@ -757,16 +773,56 @@ A `tls_config` allows configuring TLS connections.
OAuth 2.0 authentication using the client credentials or password grant type.
Prometheus fetches an access token from the specified endpoint with
-the given client access and secret keys.
+the given client access and credentials.
```yaml
client_id:
+
+# OAuth2 grant type to use. It can be one of
+# "client_credentials" or "urn:ietf:params:oauth:grant-type:jwt-bearer" (RFC 7523).
+# Default value is "client_credentials"
+[ grant_type: ]
+
+# Client secret to provide to authorization server. Only used if
+# GrantType is set empty or set to "client_credentials".
[ client_secret: ]
# Read the client secret from a file.
# It is mutually exclusive with `client_secret`.
[ client_secret_file: ]
+# Secret key to sign JWT with. Only used if
+# GrantType is set to "urn:ietf:params:oauth:grant-type:jwt-bearer".
+[ client_certificate_key: ]
+
+# Read the secret key from a file.
+# It is mutually exclusive with `client_certificate_key`.
+[ client_certificate_key_file: ]
+
+# JWT kid value to include in the JWT header. Only used if
+# GrantType is set to "urn:ietf:params:oauth:grant-type:jwt-bearer".
+[ client_certificate_key_id: ]
+
+# Signature algorithm used to sign JWT token. Only used if
+# GrantType is set to "urn:ietf:params:oauth:grant-type:jwt-bearer".
+# Default value is RS256 and valid values RS256, RS384, RS512
+[ signature_algorithm: ]
+
+# OAuth client identifier used when communicating with
+# the configured OAuth provider. Default value is client_id. Only used if
+# GrantType is set to "urn:ietf:params:oauth:grant-type:jwt-bearer".
+[ iss: ]
+
+# Intended audience of the request. If empty, the value
+# of TokenURL is used as the intended audience. Only used if
+# GrantType is set to "urn:ietf:params:oauth:grant-type:jwt-bearer".
+[ audience: ]
+
+# Map of claims to be added to the JWT token. Only used if
+# GrantType is set to "urn:ietf:params:oauth:grant-type:jwt-bearer".
+claims:
+ [ : ... ]
+
# Scopes for the token request.
scopes:
[ - ... ]
@@ -812,6 +868,323 @@ http_headers:
[ files: [, ...] ] ]
```
+### ``
+
+AWS SD configurations allow retrieving scrape targets from AWS services.
+This is a unified service discovery that supports multiple AWS service types through the `role` parameter.
+
+One of the following `role` types can be configured to discover targets:
+
+#### `ec2`
+
+The `ec2` role discovers targets from AWS EC2 instances. The private IP address is used by default, but may be changed to
+the public IP address with relabeling.
+
+The IAM credentials used must have the `ec2:DescribeInstances` permission to
+discover scrape targets, and may optionally have the
+`ec2:DescribeAvailabilityZones` permission if you want the availability zone ID
+available as a label (see below).
+
+The following meta labels are available on targets during [relabeling](#relabel_config):
+
+* `__meta_ec2_ami`: the EC2 Amazon Machine Image
+* `__meta_ec2_architecture`: the architecture of the instance
+* `__meta_ec2_availability_zone`: the availability zone in which the instance is running
+* `__meta_ec2_availability_zone_id`: the [availability zone ID](https://docs.aws.amazon.com/ram/latest/userguide/working-with-az-ids.html) in which the instance is running (requires `ec2:DescribeAvailabilityZones`)
+* `__meta_ec2_instance_id`: the EC2 instance ID
+* `__meta_ec2_instance_lifecycle`: the lifecycle of the EC2 instance, set only for 'spot' or 'scheduled' instances, absent otherwise
+* `__meta_ec2_instance_state`: the state of the EC2 instance
+* `__meta_ec2_instance_type`: the type of the EC2 instance
+* `__meta_ec2_ipv6_addresses`: comma separated list of IPv6 addresses assigned to the instance's network interfaces, if present
+* `__meta_ec2_owner_id`: the ID of the AWS account that owns the EC2 instance
+* `__meta_ec2_platform`: the Operating System platform, set to 'windows' on Windows servers, absent otherwise
+* `__meta_ec2_primary_ipv6_addresses`: comma separated list of the Primary IPv6 addresses of the instance, if present. The list is ordered based on the position of each corresponding network interface in the attachment order.
+* `__meta_ec2_primary_subnet_id`: the subnet ID of the primary network interface, if available
+* `__meta_ec2_private_dns_name`: the private DNS name of the instance, if available
+* `__meta_ec2_private_ip`: the private IP address of the instance, if present
+* `__meta_ec2_public_dns_name`: the public DNS name of the instance, if available
+* `__meta_ec2_public_ip`: the public IP address of the instance, if available
+* `__meta_ec2_region`: the region of the instance
+* `__meta_ec2_subnet_id`: comma separated list of subnets IDs in which the instance is running, if available
+* `__meta_ec2_tag_`: each tag value of the instance
+* `__meta_ec2_vpc_id`: the ID of the VPC in which the instance is running, if available
+
+#### `lightsail`
+
+The `lightsail` role discovers targets from [AWS Lightsail](https://aws.amazon.com/lightsail/)
+instances. The private IP address is used by default, but may be changed to
+the public IP address with relabeling.
+
+The following meta labels are available on targets during [relabeling](#relabel_config):
+
+* `__meta_lightsail_availability_zone`: the availability zone in which the instance is running
+* `__meta_lightsail_blueprint_id`: the Lightsail blueprint ID
+* `__meta_lightsail_bundle_id`: the Lightsail bundle ID
+* `__meta_lightsail_instance_name`: the name of the Lightsail instance
+* `__meta_lightsail_instance_state`: the state of the Lightsail instance
+* `__meta_lightsail_instance_support_code`: the support code of the Lightsail instance
+* `__meta_lightsail_ipv6_addresses`: comma separated list of IPv6 addresses assigned to the instance's network interfaces, if present
+* `__meta_lightsail_private_ip`: the private IP address of the instance
+* `__meta_lightsail_public_ip`: the public IP address of the instance, if available
+* `__meta_lightsail_region`: the region of the instance
+* `__meta_lightsail_tag_`: each tag value of the instance
+
+#### `ecs`
+
+The `ecs` role discovers targets from AWS ECS containers.
+
+ECS service discovery supports all ECS networking modes:
+- **awsvpc mode** (Fargate and EC2 with ENI): Uses the task's private IP address from its elastic network interface
+- **bridge mode** (EC2): Uses the EC2 host instance's private IP address
+- **host mode** (EC2): Uses the EC2 host instance's private IP address
+
+The private IP address is used by default, but may be changed to the public IP address with relabeling.
+
+The IAM credentials used must have the following permissions to discover scrape targets:
+
+- `ecs:ListClusters`
+- `ecs:DescribeClusters`
+- `ecs:ListServices`
+- `ecs:DescribeServices`
+- `ecs:ListTasks`
+- `ecs:DescribeTasks`
+- `ecs:DescribeContainerInstances` (required for EC2 launch type tasks)
+- `ec2:DescribeInstances` (required for EC2 launch type tasks)
+- `ec2:DescribeNetworkInterfaces` (required to get public IP for awsvpc mode tasks)
+
+The following meta labels are available on targets during [relabeling](#relabel_config):
+
+* `__meta_ecs_cluster`: the name of the ECS cluster
+* `__meta_ecs_cluster_arn`: the ARN of the ECS cluster
+* `__meta_ecs_service`: the name of the ECS service
+* `__meta_ecs_service_arn`: the ARN of the ECS service
+* `__meta_ecs_service_status`: the status of the ECS service
+* `__meta_ecs_task_group`: the ECS task group (typically service:service-name)
+* `__meta_ecs_task_arn`: the ARN of the ECS task
+* `__meta_ecs_task_definition`: the ARN of the ECS task definition
+* `__meta_ecs_ip_address`: the private IP address of the task
+* `__meta_ecs_launch_type`: the launch type of the task (EC2 or Fargate)
+* `__meta_ecs_desired_status`: the desired status of the task
+* `__meta_ecs_last_status`: the last known status of the task
+* `__meta_ecs_health_status`: the health status of the task
+* `__meta_ecs_platform_family`: the platform family (e.g., Linux, Windows)
+* `__meta_ecs_platform_version`: the platform version
+* `__meta_ecs_subnet_id`: the subnet ID where the task is running
+* `__meta_ecs_availability_zone`: the availability zone where the task is running
+* `__meta_ecs_region`: the AWS region
+* `__meta_ecs_public_ip`: the public IP address (from ENI for awsvpc mode, from EC2 instance for bridge/host mode), if available
+* `__meta_ecs_network_mode`: the network mode of the task (awsvpc or bridge)
+* `__meta_ecs_container_instance_arn`: the ARN of the container instance (EC2 launch type only)
+* `__meta_ecs_ec2_instance_id`: the EC2 instance ID (EC2 launch type only)
+* `__meta_ecs_ec2_instance_type`: the EC2 instance type (EC2 launch type only)
+* `__meta_ecs_ec2_instance_private_ip`: the private IP address of the EC2 instance (EC2 launch type only)
+* `__meta_ecs_ec2_instance_public_ip`: the public IP address of the EC2 instance, if available (EC2 launch type only)
+* `__meta_ecs_tag_cluster_`: each cluster tag value, keyed by tag name
+* `__meta_ecs_tag_service_`: each service tag value, keyed by tag name
+* `__meta_ecs_tag_task_`: each task tag value, keyed by tag name
+* `__meta_ecs_tag_ec2_`: each EC2 instance tag value, keyed by tag name (EC2 launch type only)
+
+#### `msk`
+
+The `msk` role discovers targets from AWS MSK (Managed Streaming for Apache Kafka) provisioned clusters.
+
+**Important**: This service discovery only works with **provisioned clusters**. Serverless clusters are not supported as they do not expose individual broker nodes.
+
+Discovery includes:
+- **Broker nodes**: Kafka broker instances (supports both ZooKeeper-based and KRaft-based clusters)
+- **KRaft Controller nodes**: Controller instances (KRaft-based clusters only)
+
+Note: ZooKeeper nodes are not discoverable via the MSK API. For monitoring, MSK provides:
+- **JMX Exporter**: Available on both broker and KRaft controller nodes (when enabled)
+- **Node Exporter**: Available on broker nodes only (when enabled)
+
+The IAM credentials used must have the following permissions to discover
+scrape targets:
+
+- `kafka:DescribeClusterV2`
+- `kafka:ListClustersV2`
+- `kafka:ListNodes`
+
+The following meta labels are available on targets during [relabeling](#relabel_config):
+
+* `__meta_msk_cluster_name`: the name of the MSK cluster
+* `__meta_msk_cluster_arn`: the ARN of the MSK cluster
+* `__meta_msk_cluster_state`: the state of the MSK cluster (e.g., ACTIVE, CREATING, DELETING)
+* `__meta_msk_cluster_type`: the type of the MSK cluster (e.g., PROVISIONED, SERVERLESS)
+* `__meta_msk_cluster_version`: the current version of the MSK cluster
+* `__meta_msk_cluster_kafka_version`: the Kafka version running on the cluster
+* `__meta_msk_cluster_jmx_exporter_enabled`: whether JMX exporter is enabled on the cluster
+* `__meta_msk_cluster_configuration_arn`: the ARN of the MSK configuration
+* `__meta_msk_cluster_configuration_revision`: the revision of the MSK configuration
+* `__meta_msk_cluster_tag_`: each cluster tag value, keyed by tag name
+* `__meta_msk_node_type`: the type of the node (BROKER or CONTROLLER)
+* `__meta_msk_node_arn`: the ARN of the node
+* `__meta_msk_node_added_time`: the time the node was added to the cluster
+* `__meta_msk_node_instance_type`: the instance type of the node
+* `__meta_msk_node_attached_eni`: the ID of the attached ENI
+* `__meta_msk_broker_id`: the broker ID (broker nodes only)
+* `__meta_msk_broker_endpoint_index`: the index of the broker endpoint (broker nodes only)
+* `__meta_msk_broker_client_subnet`: the client subnet of the broker (broker nodes only)
+* `__meta_msk_broker_client_vpc_ip`: the VPC IP address of the broker (broker nodes only)
+* `__meta_msk_broker_node_exporter_enabled`: whether node exporter is enabled on brokers (broker nodes only)
+* `__meta_msk_controller_endpoint_index`: the index of the controller endpoint (controller nodes only)
+
+#### `elasticache`
+
+The `elasticache` role discovers targets from AWS ElastiCache for both serverless caches and cache clusters.
+
+**Important**: For cache clusters, one target is created per cache node. Each target includes the cluster-level labels (ARN, status, tags, etc.) and node-specific labels (node ID, endpoint, availability zone, etc.). The `__address__` label is set to the individual node's endpoint address and port.
+
+For serverless caches, one target is created per serverless cache, with the `__address__` label set to the serverless cache endpoint.
+
+The IAM credentials used must have the following permissions to discover scrape targets:
+
+- `elasticache:DescribeServerlessCaches`
+- `elasticache:DescribeCacheClusters`
+- `elasticache:ListTagsForResource`
+
+The following meta labels are available on targets during [relabeling](#relabel_config):
+
+**Common labels (available on all targets):**
+
+* `__meta_elasticache_deployment_option`: the deployment option - either `serverless` for serverless caches or `node` for cache cluster nodes
+
+**Serverless Cache labels:**
+
+* `__meta_elasticache_serverless_cache_arn`: the ARN of the serverless cache
+* `__meta_elasticache_serverless_cache_name`: the name of the serverless cache
+* `__meta_elasticache_serverless_cache_status`: the status of the serverless cache
+* `__meta_elasticache_serverless_cache_engine`: the cache engine (redis or valkey)
+* `__meta_elasticache_serverless_cache_full_engine_version`: the full engine version
+* `__meta_elasticache_serverless_cache_major_engine_version`: the major engine version
+* `__meta_elasticache_serverless_cache_description`: the description of the serverless cache
+* `__meta_elasticache_serverless_cache_create_time`: the creation time in RFC3339 format
+* `__meta_elasticache_serverless_cache_snapshot_retention_limit`: the snapshot retention limit in days
+* `__meta_elasticache_serverless_cache_daily_snapshot_time`: the daily snapshot time
+* `__meta_elasticache_serverless_cache_user_group_id`: the user group ID
+* `__meta_elasticache_serverless_cache_kms_key_id`: the KMS key ID for encryption at rest
+* `__meta_elasticache_serverless_cache_endpoint_address`: the endpoint address
+* `__meta_elasticache_serverless_cache_endpoint_port`: the endpoint port
+* `__meta_elasticache_serverless_cache_reader_endpoint_address`: the reader endpoint address
+* `__meta_elasticache_serverless_cache_reader_endpoint_port`: the reader endpoint port
+* `__meta_elasticache_serverless_cache_security_group_id_`: security group IDs (indexed)
+* `__meta_elasticache_serverless_cache_subnet_id_`: subnet IDs (indexed)
+* `__meta_elasticache_serverless_cache_cache_usage_limit_data_storage_maximum`: maximum data storage in the specified unit
+* `__meta_elasticache_serverless_cache_cache_usage_limit_data_storage_minimum`: minimum data storage in the specified unit
+* `__meta_elasticache_serverless_cache_cache_usage_limit_data_storage_unit`: unit for data storage (e.g., GB)
+* `__meta_elasticache_serverless_cache_cache_usage_limit_ecpu_per_second_maximum`: maximum ECPU per second
+* `__meta_elasticache_serverless_cache_cache_usage_limit_ecpu_per_second_minimum`: minimum ECPU per second
+* `__meta_elasticache_serverless_cache_tag_`: each serverless cache tag value, keyed by tag name
+
+**Cache Cluster labels:**
+
+* `__meta_elasticache_cache_cluster_arn`: the ARN of the cache cluster
+* `__meta_elasticache_cache_cluster_cache_cluster_id`: the cache cluster ID
+* `__meta_elasticache_cache_cluster_cache_cluster_status`: the status of the cache cluster
+* `__meta_elasticache_cache_cluster_engine`: the cache engine (redis or memcached)
+* `__meta_elasticache_cache_cluster_engine_version`: the engine version
+* `__meta_elasticache_cache_cluster_cache_node_type`: the cache node type (e.g., cache.t3.micro)
+* `__meta_elasticache_cache_cluster_num_cache_nodes`: the number of cache nodes
+* `__meta_elasticache_cache_cluster_cache_cluster_create_time`: the creation time in RFC3339 format
+* `__meta_elasticache_cache_cluster_at_rest_encryption_enabled`: whether encryption at rest is enabled
+* `__meta_elasticache_cache_cluster_transit_encryption_enabled`: whether encryption in transit is enabled
+* `__meta_elasticache_cache_cluster_transit_encryption_mode`: the transit encryption mode
+* `__meta_elasticache_cache_cluster_auth_token_enabled`: whether auth token is enabled
+* `__meta_elasticache_cache_cluster_auth_token_last_modified`: the last modification time of auth token
+* `__meta_elasticache_cache_cluster_auto_minor_version_upgrade`: whether auto minor version upgrade is enabled
+* `__meta_elasticache_cache_cluster_cache_parameter_group`: the cache parameter group name
+* `__meta_elasticache_cache_cluster_cache_subnet_group_name`: the cache subnet group name
+* `__meta_elasticache_cache_cluster_client_download_landing_page`: the client download landing page URL
+* `__meta_elasticache_cache_cluster_ip_discovery`: the IP discovery mode (ipv4 or ipv6)
+* `__meta_elasticache_cache_cluster_network_type`: the network type (ipv4, ipv6, or dual_stack)
+* `__meta_elasticache_cache_cluster_preferred_availability_zone`: the preferred availability zone
+* `__meta_elasticache_cache_cluster_preferred_maintenance_window`: the preferred maintenance window
+* `__meta_elasticache_cache_cluster_preferred_outpost_arn`: the preferred outpost ARN
+* `__meta_elasticache_cache_cluster_replication_group_id`: the replication group ID (for Redis clusters that are part of a replication group)
+* `__meta_elasticache_cache_cluster_replication_group_log_delivery_enabled`: whether log delivery is enabled for the replication group
+* `__meta_elasticache_cache_cluster_snapshot_retention_limit`: the snapshot retention limit in days
+* `__meta_elasticache_cache_cluster_snapshot_window`: the daily snapshot window
+* `__meta_elasticache_cache_cluster_configuration_endpoint_address`: the configuration endpoint address (cluster mode enabled only)
+* `__meta_elasticache_cache_cluster_configuration_endpoint_port`: the configuration endpoint port (cluster mode enabled only)
+* `__meta_elasticache_cache_cluster_notification_topic_arn`: the SNS topic ARN for notifications
+* `__meta_elasticache_cache_cluster_notification_topic_status`: the SNS topic status
+* `__meta_elasticache_cache_cluster_log_delivery_configuration_destination_type_`: log delivery destination type (cloudwatch-logs or kinesis-firehose)
+* `__meta_elasticache_cache_cluster_log_delivery_configuration_log_format_`: log format (text or json)
+* `__meta_elasticache_cache_cluster_log_delivery_configuration_log_type_`: log type (slow-log or engine-log)
+* `__meta_elasticache_cache_cluster_log_delivery_configuration_status_`: log delivery status
+* `__meta_elasticache_cache_cluster_log_delivery_configuration_message_`: log delivery message
+* `__meta_elasticache_cache_cluster_log_delivery_configuration_log_group_`: CloudWatch log group name (cloudwatch-logs destination only)
+* `__meta_elasticache_cache_cluster_log_delivery_configuration_delivery_stream_`: Kinesis Firehose delivery stream name (kinesis-firehose destination only)
+* `__meta_elasticache_cache_cluster_pending_modified_values_auth_token_status`: pending auth token status
+* `__meta_elasticache_cache_cluster_pending_modified_values_cache_node_type`: pending cache node type change
+* `__meta_elasticache_cache_cluster_pending_modified_values_engine_version`: pending engine version upgrade
+* `__meta_elasticache_cache_cluster_pending_modified_values_num_cache_nodes`: pending number of cache nodes
+* `__meta_elasticache_cache_cluster_pending_modified_values_transit_encryption_enabled`: pending transit encryption status
+* `__meta_elasticache_cache_cluster_pending_modified_values_transit_encryption_mode`: pending transit encryption mode
+* `__meta_elasticache_cache_cluster_pending_modified_values_cache_node_ids_to_remove`: comma-separated list of cache node IDs to be removed
+* `__meta_elasticache_cache_cluster_security_group_membership_id_`: security group ID (indexed)
+* `__meta_elasticache_cache_cluster_security_group_membership_status_`: security group status (indexed)
+* `__meta_elasticache_cache_cluster_node_id`: cache node ID
+* `__meta_elasticache_cache_cluster_node_status`: cache node status
+* `__meta_elasticache_cache_cluster_node_create_time`: cache node creation time in RFC3339 format
+* `__meta_elasticache_cache_cluster_node_availability_zone`: cache node availability zone
+* `__meta_elasticache_cache_cluster_node_customer_outpost_arn`: cache node outpost ARN
+* `__meta_elasticache_cache_cluster_node_source_cache_node_id`: source cache node ID for replication
+* `__meta_elasticache_cache_cluster_node_parameter_group_status`: parameter group status
+* `__meta_elasticache_cache_cluster_node_endpoint_address`: cache node endpoint address
+* `__meta_elasticache_cache_cluster_node_endpoint_port`: cache node endpoint port
+* `__meta_elasticache_cache_cluster_tag_`: each cache cluster tag value, keyed by tag name
+
+See below for the configuration options for AWS discovery:
+
+```yaml
+# The AWS role to use for service discovery.
+# Must be one of: ec2, lightsail, ecs, msk, or elasticache.
+role:
+
+# The AWS region. If blank, the region from the instance metadata is used.
+[ region: ]
+
+# Custom endpoint to be used.
+[ endpoint: ]
+
+# AWS access key ID. If blank, the environment variable AWS_ACCESS_KEY_ID is used.
+[ access_key: ]
+
+# AWS secret access key. If blank, the environment variable AWS_SECRET_ACCESS_KEY is used.
+[ secret_key: ]
+
+# Named AWS profile used to authenticate.
+[ profile: ]
+
+# AWS Role ARN, an alternative to using AWS API keys.
+[ role_arn: ]
+
+# Refresh interval to re-read the targets list.
+[ refresh_interval: | default = 60s ]
+
+# The port to scrape metrics from. If using the public IP address, this must
+# instead be specified in the relabeling rule.
+[ port: | default = 80 ]
+
+# Filters can be used optionally to filter the instance list by other criteria (ec2 role only).
+# Available filter criteria can be found here:
+# https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeInstances.html
+# Filter API documentation: https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_Filter.html
+filters:
+ [ - name:
+ values: , [...] ]
+
+# List of ECS, ElastiCache, or MSK cluster identifiers (ecs, elasticache, and msk roles only) to discover.
+# A List of ARNs of clusters to discover. If empty, all clusters in the region are discovered.
+# This can significantly improve performance when you only need to monitor specific clusters/caches.
+[ clusters: [, ...] ]
+
+# HTTP client settings, including authentication methods (such as basic auth and
+# authorization), proxy configurations, TLS options, custom HTTP headers, etc.
+[ ]
+```
+
### ``
Azure SD configurations allow retrieving scrape targets from Azure VMs.
@@ -2262,8 +2635,7 @@ in the configuration file), which can also be changed using relabeling.
### ``
-Nerve SD configurations allow retrieving scrape targets from [AirBnB's Nerve]
-(https://github.com/airbnb/nerve) which are stored in
+Nerve SD configurations allow retrieving scrape targets from [AirBnB's Nerve](https://github.com/airbnb/nerve) which are stored in
[Zookeeper](https://zookeeper.apache.org/).
The following meta labels are available on targets during [relabeling](#relabel_config):
@@ -2317,8 +2689,7 @@ The following meta labels are available on targets during [relabeling](#relabel_
### ``
-Serverset SD configurations allow retrieving scrape targets from [Serversets]
-(https://github.com/twitter/finagle/tree/develop/finagle-serversets) which are
+Serverset SD configurations allow retrieving scrape targets from [Serversets](https://github.com/twitter/finagle/tree/develop/finagle-serversets) which are
stored in [Zookeeper](https://zookeeper.apache.org/). Serversets are commonly
used by [Finagle](https://twitter.github.io/finagle/) and
[Aurora](https://aurora.apache.org/).
@@ -2401,12 +2772,35 @@ project:
[ ]
```
-A Service Account Token can be set through `http_config`.
+A [Service Account Key](https://docs.stackit.cloud/platform/access-and-identity/service-accounts/how-tos/manage-service-account-keys/) can be set through `http_config`. This can be done mapping values from STACKIT Service Account json into oauth2 configuration.
+
+From a given Service Account json
+```json
+{
+ //....
+ "credentials": {
+ "kid": "6a7c3b36-xxxxxxxx",
+ "iss": "xxxx@sa.stackit.cloud",
+ "sub": "af2c2336-xxxxxxxx",
+ "aud": "https://stackit-service-account-prod.apps.01.cf.eu01.stackit.cloud",
+ "privateKey": "-----BEGIN PRIVATE KEY-----xxxx"
+ }
+}
+```
+
+properties can be mapped as:
```yaml
stackit_sd_config:
-- authorization:
- credentials:
+- oauth2:
+ client_id:
+ client_certificate_key:
+ client_certificate_key_id:
+ iss:
+ audience:
+ grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer"
+ token_url: "https://service-account.api.stackit.cloud/token"
+ signature_algorithm: RS512
```
### ``
@@ -2729,6 +3123,11 @@ labels:
[ : ... ]
```
+The special labels mentioned in the [relabeling](#relabel_config) section can also be
+used here to override the respective settings in the scrape configuration. This is
+especially useful when combined with any of the service discovery mechanisms that do not
+support these settings directly.
+
### ``
Relabeling is a powerful tool to dynamically rewrite the label set of a target before
@@ -2738,6 +3137,11 @@ in the configuration file.
Initially, aside from the configured per-target labels, a target's `job`
label is set to the `job_name` value of the respective scrape configuration.
+
+You can also use special labels like `__address__`, `__scheme__`, `__metrics_path__`,
+`__scrape_interval__`, `__scrape_timeout__` to customize the defined targets. These will
+override the respective settings in the scrape configuration.
+
The `__address__` label is set to the `:` address of the target.
After relabeling, the `instance` label is set to the value of `__address__` by default if
it was not set during relabeling.
@@ -2881,10 +3285,23 @@ sigv4:
# AWS Role ARN, an alternative to using AWS API keys.
[ role_arn: ]
+ # AWS External ID used when assuming a role.
+ # Can only be used with role_arn.
+ [ external_id: ]
+
+ # Defines the FIPS mode for the AWS STS endpoint.
+ # Requires Prometheus >= 2.54.0
+ # Note: FIPS STS selection should be configured via use_fips_sts_endpoint rather than environment variables. (The problem report that motivated this: AWS_USE_FIPS_ENDPOINT no longer works.)
+ [ use_fips_sts_endpoint: | default = false ]
+
# HTTP client settings, including authentication methods (such as basic auth and
# authorization), proxy configurations, TLS options, custom HTTP headers, etc.
[ ]
+# List of AWS service discovery configurations.
+aws_sd_configs:
+ [ - ... ]
+
# List of Azure service discovery configurations.
azure_sd_configs:
[ - ... ]
@@ -3083,6 +3500,15 @@ sigv4:
# AWS Role ARN, an alternative to using AWS API keys.
[ role_arn: ]
+ # AWS External ID used when assuming a role.
+ # Can only be used with role_arn.
+ [ external_id: ]
+
+ # Defines the FIPS mode for the AWS STS endpoint.
+ # Requires Prometheus >= 2.54.0
+ # Note: FIPS STS selection should be configured via use_fips_sts_endpoint rather than environment variables. (The problem report that motivated this: AWS_USE_FIPS_ENDPOINT no longer works.)
+ [ use_fips_sts_endpoint: | default = false ]
+
# Optional AzureAD configuration.
# Cannot be used at the same time as basic_auth, authorization, oauth2, sigv4 or google_iam.
azuread:
@@ -3110,6 +3536,14 @@ azuread:
[ sdk:
[ tenant_id: ] ]
+ # Optional custom OAuth 2.0 scope to request when acquiring tokens.
+ # If not specified, defaults to the appropriate monitoring scope for the cloud:
+ # - AzurePublic: https://monitor.azure.com//.default
+ # - AzureGovernment: https://monitor.azure.us//.default
+ # - AzureChina: https://monitor.azure.cn//.default
+ # Use this to authenticate against custom Azure applications or non-standard endpoints.
+ [ scope: ]
+
# WARNING: Remote write is NOT SUPPORTED by Google Cloud. This configuration is reserved for future use.
# Optional Google Cloud Monitoring configuration.
# Cannot be used at the same time as basic_auth, authorization, oauth2, sigv4 or azuread.
@@ -3230,6 +3664,19 @@ with this feature.
# to the timestamp of the last appended sample for the same series.
[ out_of_order_time_window: | default = 0s ]
+# Configures the trigger point for compacting the stale series from the memory into persistent blocks
+# and remove those stale series from the memory.
+#
+# The threshold is a number between 0.0 and 1.0. It represents the ratio of stale series in the memory
+# to the total series in the memory. The stale series compaction is triggered when this ratio crosses
+# the configured threshold. It may not trigger the stale series compaction if the usual head compaction
+# is about to happen soon.
+#
+# If set to 0, stale series compaction is disabled.
+#
+# This is an experimental feature, this behaviour could change or be removed in the future.
+[ stale_series_compaction_threshold: | default = 0 ]
+
# Configures data retention settings for TSDB.
#
diff --git a/docs/configuration/unit_testing_rules.md b/docs/configuration/unit_testing_rules.md
index d237c8cf88..af94c414f0 100644
--- a/docs/configuration/unit_testing_rules.md
+++ b/docs/configuration/unit_testing_rules.md
@@ -48,6 +48,18 @@ input_series:
# Name of the test group
[ name: ]
+# Start timestamp for the test group. This sets the base time for all samples
+# and evaluations in this test group.
+# Accepts either a Unix timestamp (e.g., 1609459200) or an RFC3339 formatted
+# timestamp (e.g., "2021-01-01T00:00:00Z").
+# Default: 0 (Unix epoch: 1970-01-01 00:00:00 UTC)
+#
+# When set:
+# - All input_series samples are timestamped starting from start_timestamp
+# - The eval_time in test cases is relative to start_timestamp
+# - The time() function returns start_timestamp + eval_time
+[ start_timestamp: | | default = 0 ]
+
# Unit tests for the above data.
# Unit tests for alerting rules. We consider the alerting rules from the input file.
@@ -137,7 +149,8 @@ values:
Prometheus allows you to have same alertname for different alerting rules. Hence in this unit testing, you have to list the union of all the firing alerts for the alertname under a single ``.
``` yaml
-# The time elapsed from time=0s when the alerts have to be checked.
+# The time elapsed from start_timestamp when the alerts have to be checked.
+# This is a duration relative to start_timestamp (which defaults to 0).
eval_time:
# Name of the alert to be tested.
@@ -168,7 +181,8 @@ exp_annotations:
# Expression to evaluate
expr:
-# The time elapsed from time=0s when the expression has to be evaluated.
+# The time elapsed from start_timestamp when the expression has to be evaluated.
+# This is a duration relative to start_timestamp (which defaults to 0).
eval_time:
# Expected samples at the given evaluation time.
@@ -275,3 +289,24 @@ groups:
summary: "Instance {{ $labels.instance }} down"
description: "{{ $labels.instance }} of job {{ $labels.job }} has been down for more than 5 minutes."
```
+
+### Time within tests
+
+It should be noted that in all tests, either in `alert_test_case` or
+`promql_test_case`, the output from all functions related to the current time,
+for example the `time()` and `day_of_*()` functions, will output a consistent value
+for tests.
+
+By default, at the start of the test evaluation, `time()` returns 0 (Unix epoch:
+January 1, 1970 00:00:00 UTC). The `eval_time` field specifies a duration relative
+to `start_timestamp`, so by default `time()` will return a value of `0 + eval_time`.
+
+You can configure a custom start timestamp for your tests by setting the `start_timestamp`
+field in your test group. This field accepts either:
+- A Unix timestamp (e.g., `1609459200` for January 1, 2021 00:00:00 UTC)
+- An RFC3339 formatted timestamp (e.g., `"2021-01-01T00:00:00Z"`)
+
+When you set `start_timestamp`:
+- All `input_series` samples will be timestamped starting from `start_timestamp`
+- The `eval_time` field in test cases is interpreted as a duration relative to `start_timestamp`
+- The `time()` function will return `start_timestamp + eval_time`
diff --git a/docs/feature_flags.md b/docs/feature_flags.md
index 1b3c21aae8..247941c5ce 100644
--- a/docs/feature_flags.md
+++ b/docs/feature_flags.md
@@ -28,6 +28,8 @@ and m-mapped chunks, while a WAL replay from disk is only needed for the parts o
`--enable-feature=extra-scrape-metrics`
+> **Note:** This feature flag is deprecated. Please use the `extra_scrape_metrics` configuration option instead (available at both global and scrape-config level). The feature flag will be removed in a future major version. See the [configuration documentation](configuration/configuration.md) for more details.
+
When enabled, for each instance scrape, Prometheus stores a sample in the following additional time series:
- `scrape_timeout_seconds`. The configured `scrape_timeout` for a target. This allows you to measure each target to find out how close they are to timing out with `scrape_duration_seconds / scrape_timeout_seconds`.
@@ -45,20 +47,6 @@ statistics. Currently this is limited to totalQueryableSamples.
When disabled in either the engine or the query, per-step statistics are not
computed at all.
-## Native Histograms
-
-`--enable-feature=native-histograms`
-
-_This feature flag is being phased out. You should not use it anymore._
-
-Native histograms are a stable feature by now. However, to scrape native
-histograms, a scrape config setting `scrape_native_histograms` is required. To
-ease the transition, this feature flag sets the default value of
-`scrape_native_histograms` to `true`. From v3.9 on, this feature flag will be a
-true no-op, and the default value of `scrape_native_histograms` will be always
-`false`. If you are still using this feature flag while running v3.8, update
-your scrape configs and stop using the feature flag before upgrading to v3.9.
-
## Experimental PromQL functions
`--enable-feature=promql-experimental-functions`
@@ -67,20 +55,27 @@ Enables PromQL functions that are considered experimental. These functions
might change their name, syntax, or semantics. They might also get removed
entirely.
-## Created Timestamps Zero Injection
+## Start (Created) Timestamps Zero Injection
`--enable-feature=created-timestamp-zero-ingestion`
-Enables ingestion of created timestamp. Created timestamps are injected as 0 valued samples when appropriate. See [PromCon talk](https://youtu.be/nWf0BfQ5EEA) for details.
+> NOTE: CreatedTimestamp feature was renamed to StartTimestamp for consistency. The above flag uses old name for stability.
-Currently Prometheus supports created timestamps only on the traditional
-Prometheus Protobuf protocol (WIP for other protocols). Therefore, enabling
-this feature pre-sets the global `scrape_protocols` configuration option to
-`[ PrometheusProto, OpenMetricsText1.0.0, OpenMetricsText0.0.1, PrometheusText0.0.4 ]`,
-resulting in negotiating the Prometheus Protobuf protocol with first priority
-(unless the `scrape_protocols` option is set to a different value explicitly).
+Enables ingestion of start timestamp. Start timestamps are injected as 0 valued samples when appropriate. See [PromCon talk](https://youtu.be/nWf0BfQ5EEA) for details.
-Besides enabling this feature in Prometheus, created timestamps need to be exposed by the application being scraped.
+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
+`[ 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.
## Concurrent evaluation of independent rules
@@ -143,6 +138,8 @@ These queries are rare to occur and easy to fix. (In the above example,
removing `by (__name__)` doesn't change anything without the feature flag and
fixes the possible problem with the feature flag.)
+It is possible to craft a query that aggregates by `__name__` and puts samples with and without delayed name removal into the same group. In that case, the name is removed from the affected group. Note that this case hardly occurs in queries that fulfill a practical purpose.
+
## Auto Reload Config
`--enable-feature=auto-reload-config`
@@ -202,7 +199,12 @@ the offset calculation.
`step()` can be used in duration expressions.
For a **range query**, it resolves to the step width of the range query.
-For an **instant query**, it resolves to `0s`.
+For an **instant query**, it resolves to `0s`.
+
+`range()` can be used in duration expressions.
+For a **range query**, it resolves to the full range of the query (end time - start time).
+For an **instant query**, it resolves to `0s`.
+This is particularly useful in combination with `@end()` to look back over the entire query range, e.g., `max_over_time(metric[range()] @ end())`.
`min(, )` and `max(, )` can be used to find the minimum or maximum of two duration expressions.
@@ -286,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
@@ -336,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/http_sd.md b/docs/http_sd.md
index 3bd6bada39..d329ce07af 100644
--- a/docs/http_sd.md
+++ b/docs/http_sd.md
@@ -39,8 +39,9 @@ an empty list `[]`. Target lists are unordered.
Prometheus caches target lists. If an error occurs while fetching an updated
targets list, Prometheus keeps using the current targets list. The targets list
-is not saved across restart. The `prometheus_sd_http_failures_total` counter
-metric tracks the number of refresh failures.
+is not saved across restart. The `prometheus_sd_refresh_failures_total` counter
+metric tracks the number of refresh failures and the `prometheus_sd_refresh_duration_seconds`
+bucket can be used to track HTTP SD refresh attempts or performance.
The whole list of targets must be returned on every scrape. There is no support
for incremental updates. A Prometheus instance does not send its hostname and it
diff --git a/docs/images/prometheus_agent.png b/docs/images/prometheus_agent.png
new file mode 100644
index 0000000000..01a9f00f39
Binary files /dev/null and b/docs/images/prometheus_agent.png differ
diff --git a/docs/index.md b/docs/index.md
index d9d4d2b152..fff28fa54a 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -13,6 +13,7 @@ The documentation is available alongside all the project documentation at
- [Getting started](getting_started.md)
- [Installation](installation.md)
+- [Agent Mode](prometheus_agent.md)
- [Configuration](configuration/configuration.md)
- [Querying](querying/basics.md)
- [Storage](storage.md)
diff --git a/docs/migration.md b/docs/migration.md
index 78db5b7d0f..6a08373f5c 100644
--- a/docs/migration.md
+++ b/docs/migration.md
@@ -43,18 +43,19 @@ This document offers guidance on migrating from Prometheus 2.x to Prometheus 3.0
Prometheus v3 will log a warning if you continue to pass these to
`--enable-feature`.
-- Starting from Prometheus version v3.8, the feature flag `native-histograms` is
- deprecated. Use the new `scrape_native_histograms` global and per-scrape
- configuration option instead.
+- Starting from v3.9, the feature flag `native-histograms` is a no-op. Native
+ histograms are a stable feature now, but scraping them has to be enabled via
+ the `scrape_native_histograms` global or per-scrape configuration option
+ (added in v3.8).
## Configuration
-- The scrape job level configuration option `scrape_classic_histograms` has been
- renamed to `always_scrape_classic_histograms`. If you use the
- `--enable-feature=native-histograms` feature flag to ingest native histograms
- and you also want to ingest classic histograms that an endpoint might expose
- along with native histograms, be sure to add this configuration or change your
- configuration from the old name.
+- The scrape job level configuration option `scrape_classic_histograms` has
+ been renamed to `always_scrape_classic_histograms`. If you use the
+ `scrape_native_histograms` scrape configuration option to ingest native
+ histograms and you also want to ingest classic histograms that an endpoint
+ might expose along with native histograms, be sure to add this configuration
+ or change your configuration from the old name.
- The `http_config.enable_http2` in `remote_write` items default has been
changed to `false`. In Prometheus v2 the remote write http client would
default to use http2. In order to parallelize multiple remote write queues
diff --git a/docs/prometheus_agent.md b/docs/prometheus_agent.md
new file mode 100644
index 0000000000..0d8c3fa94a
--- /dev/null
+++ b/docs/prometheus_agent.md
@@ -0,0 +1,54 @@
+---
+title: Prometheus Agent Mode
+nav_title: Agent Mode
+sort_rank: 4
+---
+
+## Prometheus Agent Mode
+
+The Prometheus Agent is an operational mode built into the Prometheus binary with the same scraping APIs, semantics, configuration, and discovery mechanism; this agent mode disables some of Prometheus' usual features(TSDB, alerting, and rule evaluations) and optimizes the binary for scraping and remote writing to remote locations.
+
+The Prometheus Remote Write protocol forwards(streams) all or a subset of metrics collected by Prometheus to a remote location; you can configure Prometheus to forward some metrics (if you want, with all metadata and exemplars!) to one or more locations that support the Remote Write API.
+
+With Remote Write, Prometheus still uses a pull model to gather metrics from applications, which gives us an understanding of those different failure modes. After that, we batch samples and series and export, replicate (push) data to the Remote Write endpoints, limiting the number of monitoring unknowns that the central point has.
+Streaming data from such a scraper enables Global View use cases by allowing you to store metrics data in a centralized location. This enables the separation of concerns, which is useful when different teams manage applications than the observability or monitoring pipelines.
+
+The Agent mode optimizes Prometheus for the remote write use case. It disables querying, alerting, and local storage and replaces it with a customized TSDB WAL. Everything else stays the same: scraping logic, service discovery, and related configuration. It can be used as a drop-in replacement for Prometheus if you want to just forward your data to a remote Prometheus server or any other Remote-Write-compliant project.
+
+In essence, it looks like this:
+
+
+### Benefits of agent mode
+
+- Improved efficiency. The customized Agent TSDB WAL removes the data immediately after successful writes. If it cannot reach the remote endpoint, it persists the data temporarily on the disk until the remote endpoint is back online. This is currently limited to a two-hour buffer only, similar to non-agent Prometheus. This means that there is no need to build chunks of data in memory or maintain a full index for querying purposes. Essentially the Agent mode uses a fraction of the resources that a normal Prometheus server would use in a similar situation.
+- Agent mode enables easier [horizontal scalability for ingestion](https://prometheus.io/blog/2021/11/16/agent/#the-dream-auto-scalable-metric-ingestion).
+
+### Downsides of agent mode
+
+- No local queries. You can not query the local Prometheus instance.
+- Recording rules are not possible. You can not pre-summarize data for sending to remote write. Rules must be done remotely.
+- No alerting. All alerting must be done by the remote system.
+
+### How to Use Agent Mode in Detail
+
+If you show the help output of Prometheus (--help flag), you should see more or less the following:
+
+```
+usage: prometheus []
+
+The Prometheus monitoring server
+
+Flags:
+ -h, --help Show context-sensitive help (also try --help-long and --help-man).
+ (... other flags)
+ --storage.tsdb.path="data/"
+ Base path for metrics storage. Use with server mode only.
+ --storage.agent.path="data-agent/"
+ Base path for metrics storage. Use with agent mode only.
+ (... other flags)
+ --[no-]agent Run Prometheus in 'Agent mode'.
+```
+
+Use the `--agent` flag to run Prometheus in the Agent mode. The rest of the flags are either for both server and Agent or only for a specific mode. You can see which flag is for which mode by checking the last sentence of a flag's help string. "Use with server mode only" means it's only for server mode. If you don't see any mention like this, it means the flag is shared.
+
+The Agent mode accepts the same scrape configuration with the same discovery options and remote write options. It also exposes a web UI on port 9095 with disabled query capabilities but shows build info, configuration, targets, and service discovery information as in a normal Prometheus server.
diff --git a/docs/querying/api.md b/docs/querying/api.md
index b377c6174e..78574ec103 100644
--- a/docs/querying/api.md
+++ b/docs/querying/api.md
@@ -6,6 +6,22 @@ sort_rank: 7
The current stable HTTP API is reachable under `/api/v1` on a Prometheus
server. Any non-breaking additions will be added under that endpoint.
+## OpenAPI Specification
+
+An OpenAPI specification for the HTTP API is available at `/api/v1/openapi.yaml`.
+By default, it returns OpenAPI 3.1 for broader compatibility. Use `?openapi_version=3.2`
+for OpenAPI 3.2, which includes advanced features and endpoints like `/api/v1/notifications/live`.
+
+This machine-readable specification describes all available endpoints, request parameters,
+response formats, and schemas.
+
+The OpenAPI specification can be used to:
+
+- Generate client libraries in various programming languages.
+- Validate API requests and responses.
+- Generate interactive API documentation.
+- Test API endpoints.
+
## Format overview
The API response format is JSON. Every successful API request returns a `2xx`
@@ -84,8 +100,9 @@ URL query parameters:
- `time=`: Evaluation timestamp. Optional.
- `timeout=`: Evaluation timeout. Optional. Defaults to and
is capped by the value of the `-query.timeout` flag.
-- `limit=`: Maximum number of returned series. Doesn’t affect scalars or strings but truncates the number of series for matrices and vectors. Optional. 0 means disabled.
+- `limit=`: Maximum number of returned series. Doesn't affect scalars or strings but truncates the number of series for matrices and vectors. Optional. 0 means disabled.
- `lookback_delta=`: Override the the [lookback period](#staleness) just for this query. Optional.
+- `stats=`: Include query statistics in the response. If set to `all`, includes detailed statistics. Optional.
The current server time is used if the `time` parameter is omitted.
@@ -159,6 +176,7 @@ URL query parameters:
is capped by the value of the `-query.timeout` flag.
- `limit=`: Maximum number of returned series. Optional. 0 means disabled.
- `lookback_delta=`: Override the the [lookback period](#staleness) just for this query. Optional.
+- `stats=`: Include query statistics in the response. If set to `all`, includes detailed statistics. Optional.
You can URL-encode these parameters directly in the request body by using the `POST` method and
`Content-Type: application/x-www-form-urlencoded` header. This is useful when specifying a large
@@ -670,6 +688,35 @@ Note that with the currently implemented bucket schemas, positive buckets are
“open left”, negative buckets are “open right”, and the zero bucket (with a
negative left boundary and a positive right boundary) is “closed both”.
+## Scrape pools
+
+The following endpoint returns a list of all configured scrape pools:
+
+```
+GET /api/v1/scrape_pools
+```
+
+The `data` section of the JSON response is a list of string scrape pool names.
+
+```bash
+curl http://localhost:9090/api/v1/scrape_pools
+```
+
+```json
+{
+ "status": "success",
+ "data": {
+ "scrapePools": [
+ "prometheus",
+ "node_exporter",
+ "blackbox"
+ ]
+ }
+}
+```
+
+*New in v2.42*
+
## Targets
The following endpoint returns an overview of the current state of the
@@ -982,6 +1029,7 @@ curl http://localhost:9090/api/v1/alerts
## Querying target metadata
The following endpoint returns metadata about metrics currently scraped from targets.
+The endpoint has the limitation that only metadata scraped from targets directly is returned, metadata sent over Remote-Write or OTLP to Prometheus is not included in this endpoint and will not show up on the UI in "Explore Metrics".
This is **experimental** and might change in the future.
```
@@ -1346,7 +1394,7 @@ GET /api/v1/status/tsdb
```
URL query parameters:
-- `limit=`: Limit the number of returned items to a given number for each set of statistics. By default, 10 items are returned.
+- `limit=`: Limit the number of returned items to a given number for each set of statistics. By default, 10 items are returned. The maximum allowed limit is 10000.
The `data` section of the query result consists of:
@@ -1700,3 +1748,80 @@ GET /api/v1/notifications/live
```
*New in v3.0*
+
+### Features
+
+The following endpoint returns a list of enabled features in the Prometheus server:
+
+```
+GET /api/v1/features
+```
+
+This endpoint provides information about which features are currently enabled or disabled in the Prometheus instance. Features are organized into categories such as `api`, `promql`, `promql_functions`, etc.
+
+The `data` section contains a map where each key is a feature category, and each value is a map of feature names to their enabled status (boolean).
+
+```bash
+curl http://localhost:9090/api/v1/features
+```
+
+```json
+{
+ "status": "success",
+ "data": {
+ "api": {
+ "admin": false,
+ "exclude_alerts": true
+ },
+ "otlp_receiver": {
+ "delta_conversion": false,
+ "native_delta_ingestion": false
+ },
+ "prometheus": {
+ "agent_mode": false,
+ "auto_reload_config": false
+ },
+ "promql": {
+ "anchored": false,
+ "at_modifier": true
+ },
+ "promql_functions": {
+ "abs": true,
+ "absent": true
+ },
+ "promql_operators": {
+ "!=": true,
+ "!~": true
+ },
+ "rules": {
+ "concurrent_rule_eval": false,
+ "keep_firing_for": true
+ },
+ "scrape": {
+ "start_timestamp_zero_ingestion": false,
+ "extra_metrics": false
+ },
+ "service_discovery": {
+ "azure": true,
+ "consul": true
+ },
+ "templating": {
+ "args": true,
+ "externalURL": true
+ },
+ "tsdb": {
+ "delayed_compaction": false,
+ "exemplar_storage": false
+ }
+ }
+}
+```
+
+**Notes:**
+
+- All feature names use `snake_case` naming convention
+- Features set to `false` may be omitted from the response
+- Clients should treat absent features as equivalent to `false`
+- Clients must ignore unknown feature names and categories for forward compatibility
+
+*New in v3.8*
diff --git a/docs/querying/functions.md b/docs/querying/functions.md
index 0cae149dd7..68a003359d 100644
--- a/docs/querying/functions.md
+++ b/docs/querying/functions.md
@@ -433,6 +433,23 @@ and is therefore flagged by an info-level annotation reading `input to
histogram_quantile needed to be fixed for monotonicity`. If you encounter this
annotation, you should find and remove the source of the invalid data.
+## `histogram_quantiles()`
+
+**This function has to be enabled via the [feature
+flag](../feature_flags.md#experimental-promql-functions)
+`--enable-feature=promql-experimental-functions`.**
+
+`histogram_quantiles(v instant-vector, quantile_label string, φ_1 scalar, φ_2 scalar, ...)` calculates multiple (between 1 and 10) φ-quantiles (0 ≤
+φ ≤ 1) from a [classic
+histogram](https://prometheus.io/docs/concepts/metric_types/#histogram) or from
+a native histogram. Quantile calculation works the same way as in `histogram_quantile()`.
+The second argument (a string) specifies the label name that is used to identify different quantiles in the query result.
+```
+histogram_quantiles(sum(rate(foo[1m])), "quantile", 0.9, 0.99)
+# => {quantile="0.9"} 123
+ {quantile="0.99"} 128
+```
+
## `histogram_stddev()` and `histogram_stdvar()`
`histogram_stddev(v instant-vector)` returns the estimated standard deviation
@@ -568,6 +585,8 @@ While `info` normally automatically finds all matching info series, it's possibl
restrict them by providing a `__name__` label matcher, e.g.
`{__name__="target_info"}`.
+Note that if there are any time series in `v` that match the `data-label-selector` (or the default `target_info` if that argument is not specified), they will be treated as info series and will be returned unchanged.
+
### Limitations
In its current iteration, `info` defaults to considering only info series with
diff --git a/docs/querying/operators.md b/docs/querying/operators.md
index 9aaae3fbcf..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.
@@ -425,14 +475,14 @@ To get the 5 instances with the highest memory consumption across all instances
topk(5, memory_consumption_bytes)
-#### `limitk` and `limit_ratio`
+#### `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/docs/storage.md b/docs/storage.md
index 7b6e3bffe8..e9c81a5036 100644
--- a/docs/storage.md
+++ b/docs/storage.md
@@ -11,13 +11,13 @@ Prometheus's local time series database stores data in a custom, highly efficien
### On-disk layout
-Ingested samples are grouped into blocks of two hours. Each two-hour block consists
-of a directory containing a chunks subdirectory containing all the time series samples
-for that window of time, a metadata file, and an index file (which indexes metric names
-and labels to time series in the chunks directory). The samples in the chunks directory
-are grouped together into one or more segment files of up to 512MB each by default. When
-series are deleted via the API, deletion records are stored in separate tombstone files
-(instead of deleting the data immediately from the chunk segments).
+Ingested samples are grouped into two-hour blocks. Each block consists of a directory that
+contains a chunks subdirectory with all the time series samples for that time window,
+a metadata file, and an index file (which maps metric names and labels to the time series
+in the chunks directory). The samples in the chunks directory are organized into one
+or more segment files, each up to 512 MB by default. When series are deleted via the API,
+the deletion records are stored in separate tombstone files rather than being immediately
+removed from the chunk segments.
The current block for incoming samples is kept in memory and is not fully
persisted. It is secured against crashes by a write-ahead log (WAL) that can be
@@ -59,12 +59,16 @@ A Prometheus server's data directory looks something like this:
Note that a limitation of local storage is that it is not clustered or
replicated. Thus, it is not arbitrarily scalable or durable in the face of
drive or node outages and should be managed like any other single node
-database.
+database. With proper architecture, it is possible to retain years of
+data in local storage.
[Snapshots](querying/api.md#snapshot) are recommended for backups. Backups
made without snapshots run the risk of losing data that was recorded since
-the last WAL sync, which typically happens every two hours. With proper
-architecture, it is possible to retain years of data in local storage.
+the last TSDB block was created, which typically happens every two hours,
+covering the last three hours of samples. Excluding the WAL files (the
+`chunks_head/`, `wal/`, and `wbl/` directories in `storage.tsdb.path`)
+on backup or restore will ensure a coherent backup, in any case, at the
+cost of losing the time range covered by the WAL files.
Alternatively, external storage may be used via the
[remote read/write APIs](https://prometheus.io/docs/operating/integrations/#remote-endpoints-and-storage).
diff --git a/documentation/examples/Makefile b/documentation/examples/Makefile
index 4085155f80..8ed308899b 100644
--- a/documentation/examples/Makefile
+++ b/documentation/examples/Makefile
@@ -1,4 +1,4 @@
-# Copyright 2022 The Prometheus Authors
+# 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
diff --git a/documentation/examples/custom-sd/adapter-usage/main.go b/documentation/examples/custom-sd/adapter-usage/main.go
index e7f7a69b5d..c0ce03cd0f 100644
--- a/documentation/examples/custom-sd/adapter-usage/main.go
+++ b/documentation/examples/custom-sd/adapter-usage/main.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Prometheus Authors
+// 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
diff --git a/documentation/examples/custom-sd/adapter/adapter.go b/documentation/examples/custom-sd/adapter/adapter.go
index b242c4eaa0..83f0e80c49 100644
--- a/documentation/examples/custom-sd/adapter/adapter.go
+++ b/documentation/examples/custom-sd/adapter/adapter.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Prometheus Authors
+// 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
diff --git a/documentation/examples/custom-sd/adapter/adapter_test.go b/documentation/examples/custom-sd/adapter/adapter_test.go
index 329ca8c29a..0ec69348d8 100644
--- a/documentation/examples/custom-sd/adapter/adapter_test.go
+++ b/documentation/examples/custom-sd/adapter/adapter_test.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Prometheus Authors
+// 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
diff --git a/documentation/examples/prometheus-stackit.yml b/documentation/examples/prometheus-stackit.yml
index 623cb231ff..9be3f9c53a 100644
--- a/documentation/examples/prometheus-stackit.yml
+++ b/documentation/examples/prometheus-stackit.yml
@@ -12,8 +12,15 @@ scrape_configs:
stackit_sd_configs:
- project: 11111111-1111-1111-1111-111111111111
- authorization:
- credentials: ""
+ oauth2:
+ client_id:
+ client_certificate_key:
+ client_certificate_key_id:
+ iss:
+ audience:
+ grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer"
+ token_url: "https://service-account.api.stackit.cloud/token"
+ signature_algorithm: RS512
relabel_configs:
# Use the public IPv4 and port 9100 to scrape the target.
- source_labels: [__meta_stackit_public_ipv4]
@@ -25,8 +32,15 @@ scrape_configs:
stackit_sd_configs:
- project: 11111111-1111-1111-1111-111111111111
- authorization:
- credentials: ""
+ oauth2:
+ client_id:
+ client_certificate_key:
+ client_certificate_key_id:
+ iss:
+ audience:
+ grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer"
+ token_url: "https://service-account.api.stackit.cloud/token"
+ signature_algorithm: RS512
relabel_configs:
# Use the private IPv4 within the STACKIT Subnet and port 9100 to scrape the target.
- source_labels: [__meta_stackit_private_ipv4_mynet]
diff --git a/documentation/examples/remote_storage/Makefile b/documentation/examples/remote_storage/Makefile
index e0dfd4d647..a6c8e48c45 100644
--- a/documentation/examples/remote_storage/Makefile
+++ b/documentation/examples/remote_storage/Makefile
@@ -1,4 +1,4 @@
-# Copyright 2022 The Prometheus Authors
+# 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
diff --git a/documentation/examples/remote_storage/example_write_adapter/server.go b/documentation/examples/remote_storage/example_write_adapter/server.go
index 727a3056d3..c2ec7184e3 100644
--- a/documentation/examples/remote_storage/example_write_adapter/server.go
+++ b/documentation/examples/remote_storage/example_write_adapter/server.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The Prometheus Authors
+// 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
@@ -59,7 +59,11 @@ func main() {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
- printV2(req)
+ err = printV2(req)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
default:
msg := fmt.Sprintf("Unknown remote write content type: %s", contentType)
fmt.Println(msg)
@@ -93,10 +97,13 @@ func printV1(req *prompb.WriteRequest) {
}
}
-func printV2(req *writev2.Request) {
+func printV2(req *writev2.Request) error {
b := labels.NewScratchBuilder(0)
for _, ts := range req.Timeseries {
- l := ts.ToLabels(&b, req.Symbols)
+ l, err := ts.ToLabels(&b, req.Symbols)
+ if err != nil {
+ return err
+ }
m := ts.ToMetadata(req.Symbols)
fmt.Println(l, m)
@@ -104,7 +111,10 @@ func printV2(req *writev2.Request) {
fmt.Printf("\tSample: %f %d\n", s.Value, s.Timestamp)
}
for _, ep := range ts.Exemplars {
- e := ep.ToExemplar(&b, req.Symbols)
+ e, err := ep.ToExemplar(&b, req.Symbols)
+ if err != nil {
+ return err
+ }
fmt.Printf("\tExemplar: %+v %f %d\n", e.Labels, e.Value, ep.Timestamp)
}
for _, hp := range ts.Histograms {
@@ -117,4 +127,5 @@ func printV2(req *writev2.Request) {
fmt.Printf("\tHistogram: %s\n", h.String())
}
}
+ return nil
}
diff --git a/documentation/examples/remote_storage/go.mod b/documentation/examples/remote_storage/go.mod
index a97ad32a6a..0c80c6e7c6 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.0
+go 1.25.5
require (
github.com/alecthomas/kingpin/v2 v2.4.0
@@ -8,58 +8,78 @@ require (
github.com/golang/snappy v1.0.0
github.com/influxdata/influxdb-client-go/v2 v2.14.0
github.com/prometheus/client_golang v1.23.2
- github.com/prometheus/common v0.66.1
- github.com/prometheus/prometheus v0.305.1-0.20250905124657-5c2e43f09c03
+ github.com/prometheus/common v0.67.5
+ github.com/prometheus/prometheus v0.308.1
github.com/stretchr/testify v1.11.1
)
require (
- cloud.google.com/go/auth v0.16.2 // indirect
+ cloud.google.com/go/auth v0.18.1 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
- cloud.google.com/go/compute/metadata v0.7.0 // indirect
- github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 // indirect
- github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 // indirect
- github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect
- github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect
+ cloud.google.com/go/compute/metadata v0.9.0 // indirect
+ github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 // indirect
+ github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 // indirect
+ github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
+ github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect
+ github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
- github.com/aws/aws-sdk-go-v2 v1.37.0 // indirect
- github.com/aws/aws-sdk-go-v2/config v1.29.14 // indirect
- github.com/aws/aws-sdk-go-v2/credentials v1.17.67 // indirect
- github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect
- github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.0 // indirect
- github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.0 // indirect
- github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
- github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0 // indirect
- github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.0 // indirect
- github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 // indirect
- github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 // indirect
- github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 // indirect
- github.com/aws/smithy-go v1.22.5 // indirect
+ github.com/aws/aws-sdk-go-v2 v1.41.1 // indirect
+ github.com/aws/aws-sdk-go-v2/config v1.32.7 // indirect
+ github.com/aws/aws-sdk-go-v2/credentials v1.19.7 // indirect
+ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
+ github.com/aws/aws-sdk-go-v2/service/ec2 v1.286.0 // indirect
+ github.com/aws/aws-sdk-go-v2/service/ecs v1.71.0 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 // indirect
+ github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.11 // indirect
+ github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 // indirect
+ github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 // indirect
+ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 // indirect
+ github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 // indirect
+ github.com/aws/smithy-go v1.24.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dennwc/varint v1.0.0 // indirect
+ github.com/digitalocean/godo v1.174.0 // indirect
+ github.com/distribution/reference v0.6.0 // indirect
+ github.com/docker/go-connections v0.6.0 // indirect
+ github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect
+ github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
- github.com/go-viper/mapstructure/v2 v2.3.0 // indirect
+ github.com/go-openapi/jsonreference v0.21.4 // indirect
+ github.com/go-openapi/swag v0.25.4 // indirect
+ github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/gobwas/glob v0.2.3 // indirect
- github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
+ github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/google/uuid v1.6.0 // indirect
- github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
- github.com/googleapis/gax-go/v2 v2.14.2 // indirect
- github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect
- github.com/hashicorp/go-version v1.7.0 // indirect
+ github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect
+ github.com/googleapis/gax-go/v2 v2.17.0 // indirect
+ github.com/gophercloud/gophercloud/v2 v2.10.0 // indirect
+ github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 // indirect
+ github.com/hashicorp/consul/api v1.33.2 // indirect
+ github.com/hashicorp/go-version v1.8.0 // indirect
+ github.com/hashicorp/nomad/api v0.0.0-20260209224925-94b77491c895 // indirect
+ github.com/hetznercloud/hcloud-go/v2 v2.36.0 // indirect
github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 // indirect
+ github.com/ionos-cloud/sdk-go/v6 v6.3.6 // indirect
github.com/jpillora/backoff v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
- github.com/klauspost/compress v1.18.0 // indirect
+ github.com/klauspost/compress v1.18.4 // indirect
github.com/knadh/koanf/maps v0.1.2 // indirect
github.com/knadh/koanf/providers/confmap v1.0.0 // indirect
- github.com/knadh/koanf/v2 v2.2.1 // indirect
+ github.com/knadh/koanf/v2 v2.3.2 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
+ github.com/linode/linodego v1.65.0 // indirect
+ github.com/mattn/go-colorable v0.1.14 // indirect
+ github.com/miekg/dns v1.1.72 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
@@ -67,53 +87,60 @@ require (
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect
github.com/oapi-codegen/runtime v1.0.0 // indirect
- github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics v0.129.0 // indirect
- github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.129.0 // indirect
- github.com/open-telemetry/opentelemetry-collector-contrib/processor/deltatocumulativeprocessor v0.129.0 // indirect
+ github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics v0.145.0 // indirect
+ github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.145.0 // indirect
+ github.com/open-telemetry/opentelemetry-collector-contrib/processor/deltatocumulativeprocessor v0.145.0 // indirect
+ github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
+ github.com/prometheus/client_golang/exp v0.0.0-20260108101519-fb0838f53562 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
- github.com/prometheus/otlptranslator v0.0.2 // indirect
+ github.com/prometheus/otlptranslator v1.0.0 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
- github.com/prometheus/sigv4 v0.2.0 // indirect
+ github.com/prometheus/sigv4 v0.4.1 // indirect
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
+ github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 // indirect
+ github.com/spf13/pflag v1.0.10 // indirect
+ github.com/stackitcloud/stackit-sdk-go/core v0.21.1 // indirect
github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
- go.opentelemetry.io/auto/sdk v1.1.0 // indirect
- go.opentelemetry.io/collector/component v1.45.0 // indirect
- go.opentelemetry.io/collector/confmap v1.35.0 // indirect
- go.opentelemetry.io/collector/confmap/xconfmap v0.129.0 // indirect
- go.opentelemetry.io/collector/consumer v1.45.0 // indirect
- go.opentelemetry.io/collector/featuregate v1.45.0 // indirect
- go.opentelemetry.io/collector/pdata v1.45.0 // indirect
- go.opentelemetry.io/collector/pipeline v1.45.0 // indirect
- go.opentelemetry.io/collector/processor v1.45.0 // indirect
+ go.opentelemetry.io/auto/sdk v1.2.1 // indirect
+ go.opentelemetry.io/collector/component v1.51.0 // indirect
+ go.opentelemetry.io/collector/confmap v1.51.0 // indirect
+ go.opentelemetry.io/collector/confmap/xconfmap v0.145.0 // indirect
+ go.opentelemetry.io/collector/consumer v1.51.0 // indirect
+ go.opentelemetry.io/collector/featuregate v1.51.0 // indirect
+ go.opentelemetry.io/collector/internal/componentalias v0.145.0 // indirect
+ go.opentelemetry.io/collector/pdata v1.51.0 // indirect
+ go.opentelemetry.io/collector/pipeline v1.51.0 // indirect
+ go.opentelemetry.io/collector/processor v1.51.0 // indirect
go.opentelemetry.io/collector/semconv v0.128.0 // indirect
- go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0 // indirect
- go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
- go.opentelemetry.io/otel v1.38.0 // indirect
- go.opentelemetry.io/otel/metric v1.38.0 // indirect
- go.opentelemetry.io/otel/trace v1.38.0 // indirect
+ go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.65.0 // indirect
+ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect
+ go.opentelemetry.io/otel v1.40.0 // indirect
+ go.opentelemetry.io/otel/metric v1.40.0 // indirect
+ go.opentelemetry.io/otel/trace v1.40.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
- go.uber.org/zap v1.27.0 // indirect
- go.yaml.in/yaml/v2 v2.4.2 // indirect
- golang.org/x/crypto v0.41.0 // indirect
- golang.org/x/net v0.43.0 // indirect
- golang.org/x/oauth2 v0.30.0 // indirect
- golang.org/x/sys v0.35.0 // indirect
- golang.org/x/text v0.28.0 // indirect
- golang.org/x/time v0.12.0 // indirect
- google.golang.org/api v0.239.0 // indirect
- google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect
- google.golang.org/grpc v1.76.0 // indirect
- google.golang.org/protobuf v1.36.10 // indirect
- gopkg.in/yaml.v2 v2.4.0 // indirect
+ go.uber.org/zap v1.27.1 // indirect
+ go.yaml.in/yaml/v2 v2.4.3 // indirect
+ go.yaml.in/yaml/v3 v3.0.4 // indirect
+ golang.org/x/crypto v0.47.0 // indirect
+ golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect
+ golang.org/x/net v0.49.0 // indirect
+ golang.org/x/oauth2 v0.35.0 // indirect
+ golang.org/x/sys v0.41.0 // indirect
+ golang.org/x/text v0.34.0 // indirect
+ golang.org/x/time v0.14.0 // indirect
+ google.golang.org/api v0.266.0 // indirect
+ google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
+ google.golang.org/grpc v1.78.0 // indirect
+ google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
- k8s.io/apimachinery v0.33.5 // indirect
- k8s.io/client-go v0.33.5 // indirect
+ k8s.io/apimachinery v0.35.0 // indirect
+ k8s.io/client-go v0.35.0 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
- k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
- sigs.k8s.io/yaml v1.4.0 // indirect
+ k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
)
exclude (
diff --git a/documentation/examples/remote_storage/go.sum b/documentation/examples/remote_storage/go.sum
index b7c633982b..6ebede1adf 100644
--- a/documentation/examples/remote_storage/go.sum
+++ b/documentation/examples/remote_storage/go.sum
@@ -1,29 +1,29 @@
-cloud.google.com/go/auth v0.16.2 h1:QvBAGFPLrDeoiNjyfVunhQ10HKNYuOwZ5noee0M5df4=
-cloud.google.com/go/auth v0.16.2/go.mod h1:sRBas2Y1fB1vZTdurouM0AzuYQBMZinrUYL8EufhtEA=
+cloud.google.com/go/auth v0.18.1 h1:IwTEx92GFUo2pJ6Qea0EU3zYvKnTAeRCODxfA/G5UWs=
+cloud.google.com/go/auth v0.18.1/go.mod h1:GfTYoS9G3CWpRA3Va9doKN9mjPGRS+v41jmZAhBzbrA=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
-cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU=
-cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo=
-github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U=
-github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM=
-github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4=
-github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4=
+cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
+cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0=
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8=
-github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4=
-github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5 v5.7.0 h1:LkHbJbgF3YyvC53aqYGR+wWQDn2Rdp9AQdGndf9QvY4=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5 v5.7.0/go.mod h1:QyiQdW4f4/BIfB8ZutZ2s+28RAgfa/pT+zS++ZHyM1I=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v4 v4.3.0 h1:bXwSugBiSbgtz7rOtbfGf+woewp4f06orW9OP5BjHLA=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v4 v4.3.0/go.mod h1:Y/HgrePTmGy9HjdSGTqZNa+apUpTVIEVKXJyARP2lrk=
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
-github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs=
-github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
+github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs=
+github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
github.com/Code-Hex/go-generics-cache v1.5.1 h1:6vhZGc5M7Y/YD8cIUcY8kcuQLB4cHR7U+0KMqAA0KcU=
github.com/Code-Hex/go-generics-cache v1.5.1/go.mod h1:qxcC9kRVrct9rHeiYpFWSoW1vxyillCVzX13KZG8dl4=
-github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
-github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
+github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
+github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY=
github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE=
@@ -33,36 +33,40 @@ github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7D
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA=
github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4=
-github.com/aws/aws-sdk-go-v2 v1.37.0 h1:YtCOESR/pN4j5oA7cVHSfOwIcuh/KwHC4DOSXFbv5F0=
-github.com/aws/aws-sdk-go-v2 v1.37.0/go.mod h1:9Q0OoGQoboYIAJyslFyF1f5K1Ryddop8gqMhWx/n4Wg=
-github.com/aws/aws-sdk-go-v2/config v1.29.14 h1:f+eEi/2cKCg9pqKBoAIwRGzVb70MRKqWX4dg1BDcSJM=
-github.com/aws/aws-sdk-go-v2/config v1.29.14/go.mod h1:wVPHWcIFv3WO89w0rE10gzf17ZYy+UVS1Geq8Iei34g=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.67 h1:9KxtdcIA/5xPNQyZRgUSpYOE6j9Bc4+D7nZua0KGYOM=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.67/go.mod h1:p3C44m+cfnbv763s52gCqrjaqyPikj9Sg47kUVaNZQQ=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.0 h1:H2iZoqW/v2Jnrh1FnU725Bq6KJ0k2uP63yH+DcY+HUI=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.0/go.mod h1:L0FqLbwMXHvNC/7crWV1iIxUlOKYZUE8KuTIA+TozAI=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.0 h1:EDped/rNzAhFPhVY0sDGbtD16OKqksfA8OjF/kLEgw8=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.0/go.mod h1:uUI335jvzpZRPpjYx6ODc/wg1qH+NnoSTK/FwVeK0C0=
-github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
-github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
-github.com/aws/aws-sdk-go-v2/service/ec2 v1.237.0 h1:XHE2G+yaDQql32FZt19QmQt4WuisqQJIkMUSCxeCUl8=
-github.com/aws/aws-sdk-go-v2/service/ec2 v1.237.0/go.mod h1:t11/j/nH9i6bbsPH9xc04BJOsV2nVPUqrB67/TLDsyM=
-github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0 h1:6+lZi2JeGKtCraAj1rpoZfKqnQ9SptseRZioejfUOLM=
-github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0/go.mod h1:eb3gfbVIxIoGgJsi9pGne19dhCBpK6opTYpQqAmdy44=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.0 h1:eRhU3Sh8dGbaniI6B+I48XJMrTPRkK4DKo+vqIxziOU=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.0/go.mod h1:paNLV18DZ6FnWE/bd06RIKPDIFpjuvCkGKWTG/GDBeM=
-github.com/aws/aws-sdk-go-v2/service/lightsail v1.44.0 h1:QiiCqpKy0prxq+92uWfESzcb7/8Y9JAamcMOzVYLEoM=
-github.com/aws/aws-sdk-go-v2/service/lightsail v1.44.0/go.mod h1:ESppxYqXQCpCY+KWl3BdkQjmsQX6zxKP39SnDtRDoU0=
-github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 h1:1Gw+9ajCV1jogloEv1RRnvfRFia2cL6c9cuKV2Ps+G8=
-github.com/aws/aws-sdk-go-v2/service/sso v1.25.3/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 h1:hXmVKytPfTy5axZ+fYbR5d0cFmC3JvwLm5kM83luako=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs=
-github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 h1:1XuUZ8mYJw9B6lzAkXhqHlJd/XvaX32evhproijJEZY=
-github.com/aws/aws-sdk-go-v2/service/sts v1.33.19/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4=
-github.com/aws/smithy-go v1.22.5 h1:P9ATCXPMb2mPjYBgueqJNCA5S9UfktsW0tTxi+a7eqw=
-github.com/aws/smithy-go v1.22.5/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
+github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU=
+github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
+github.com/aws/aws-sdk-go-v2/config v1.32.7 h1:vxUyWGUwmkQ2g19n7JY/9YL8MfAIl7bTesIUykECXmY=
+github.com/aws/aws-sdk-go-v2/config v1.32.7/go.mod h1:2/Qm5vKUU/r7Y+zUk/Ptt2MDAEKAfUtKc1+3U1Mo3oY=
+github.com/aws/aws-sdk-go-v2/credentials v1.19.7 h1:tHK47VqqtJxOymRrNtUXN5SP/zUTvZKeLx4tH6PGQc8=
+github.com/aws/aws-sdk-go-v2/credentials v1.19.7/go.mod h1:qOZk8sPDrxhf+4Wf4oT2urYJrYt3RejHSzgAquYeppw=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 h1:I0GyV8wiYrP8XpA70g1HBcQO1JlQxCMTW9npl5UbDHY=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17/go.mod h1:tyw7BOl5bBe/oqvoIeECFJjMdzXoa/dfVz3QQ5lgHGA=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17/go.mod h1:5M5CI3D12dNOtH3/mk6minaRwI2/37ifCURZISxA/IQ=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 h1:WWLqlh79iO48yLkj1v3ISRNiv+3KdQoZ6JWyfcsyQik=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17/go.mod h1:EhG22vHRrvF8oXSTYStZhJc1aUgKtnJe+aOiFEV90cM=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
+github.com/aws/aws-sdk-go-v2/service/ec2 v1.286.0 h1:GgLc+o2oD2sXxlEwGUCCWz/1v3Wa8dN9RRebcIFXeOo=
+github.com/aws/aws-sdk-go-v2/service/ec2 v1.286.0/go.mod h1:Uy+C+Sc58jozdoL1McQr8bDsEvNFx+/nBY+vpO1HVUY=
+github.com/aws/aws-sdk-go-v2/service/ecs v1.71.0 h1:MzP/ElwTpINq+hS80ZQz4epKVnUTlz8Sz+P/AFORCKM=
+github.com/aws/aws-sdk-go-v2/service/ecs v1.71.0/go.mod h1:pMlGFDpHoLTJOIZHGdJOAWmi+xeIlQXuFTuQxs1epYE=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 h1:RuNSMoozM8oXlgLG/n6WLaFGoea7/CddrCfIiSA+xdY=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17/go.mod h1:F2xxQ9TZz5gDWsclCtPQscGpP0VUOc8RqgFM3vDENmU=
+github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.11 h1:VM5e5M39zRSs+aT0O9SoxHjUXqXxhbw3Yi0FdMQWPIc=
+github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.11/go.mod h1:0jvzYPIQGCpnY/dmdaotTk2JH4QuBlnW0oeyrcGLWJ4=
+github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 h1:VrhDvQib/i0lxvr3zqlUwLwJP4fpmpyD9wYG1vfSu+Y=
+github.com/aws/aws-sdk-go-v2/service/signin v1.0.5/go.mod h1:k029+U8SY30/3/ras4G/Fnv/b88N4mAfliNn08Dem4M=
+github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 h1:v6EiMvhEYBoHABfbGB4alOYmCIrcgyPPiBE1wZAEbqk=
+github.com/aws/aws-sdk-go-v2/service/sso v1.30.9/go.mod h1:yifAsgBxgJWn3ggx70A3urX2AN49Y5sJTD1UQFlfqBw=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 h1:gd84Omyu9JLriJVCbGApcLzVR3XtmC4ZDPcAI6Ftvds=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13/go.mod h1:sTGThjphYE4Ohw8vJiRStAcu3rbjtXRsdNB0TvZ5wwo=
+github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/pJ1jOWYlFDJTjRQ=
+github.com/aws/aws-sdk-go-v2/service/sts v1.41.6/go.mod h1:qgFDZQSD/Kys7nJnVqYlWKnh0SSdMjAi0uSwON4wgYQ=
+github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
+github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 h1:6df1vn4bBlDDo4tARvBm7l6KA9iVMnE3NWizDeWSrps=
github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3/go.mod h1:CIWtjkly68+yqLPbvwwR/fjNJA/idrtULjZWh2v1ys0=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -70,8 +74,8 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
-github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls=
-github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
+github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f h1:Y8xYupdHxryycyPlc9Y+bSQAYZnetRJ70VMVKm5CKI0=
+github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f/go.mod h1:HlzOvOjVBOfTGSRXRyY0OiCS/3J1akRGQQpRO/7zyF4=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
@@ -82,90 +86,108 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dennwc/varint v1.0.0 h1:kGNFFSSw8ToIy3obO/kKr8U9GZYUAxQEVuix4zfDWzE=
github.com/dennwc/varint v1.0.0/go.mod h1:hnItb35rvZvJrbTALZtY/iQfDs48JKRG1RPpgziApxA=
-github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
-github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
-github.com/digitalocean/godo v1.157.0 h1:ReELaS6FxXNf8gryUiVH0wmyUmZN8/NCmBX4gXd3F0o=
-github.com/digitalocean/godo v1.157.0/go.mod h1:tYeiWY5ZXVpU48YaFv0M5irUFHXGorZpDNm7zzdWMzM=
-github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0=
-github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
-github.com/docker/docker v28.3.0+incompatible h1:ffS62aKWupCWdvcee7nBU9fhnmknOqDPaJAMtfK0ImQ=
-github.com/docker/docker v28.3.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
-github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
-github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
+github.com/digitalocean/godo v1.174.0 h1:9nVX8WqAPd7ZN9Yn63HeLRAI8m2vi9QeotcDvYmB+ns=
+github.com/digitalocean/godo v1.174.0/go.mod h1:xQsWpVCCbkDrWisHA72hPzPlnC+4W5w/McZY5ij9uvU=
+github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
+github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
+github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
+github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
+github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/edsrzf/mmap-go v1.2.0 h1:hXLYlkbaPzt1SaQk+anYwKSRNhufIDCchSPkUD6dD84=
github.com/edsrzf/mmap-go v1.2.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q=
-github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
-github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
-github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M=
-github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A=
-github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw=
-github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
-github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
+github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
+github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
+github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g=
+github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98=
+github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4=
+github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA=
github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb h1:IT4JYU7k4ikYg1SCxNI1/Tieq/NFvh6dzLdgi7eu0tM=
github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb/go.mod h1:bH6Xx7IW64qjjJq8M2u4dxNaBiDfKK+z/3eGDpXEQhc=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
-github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
-github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
-github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
-github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
+github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
+github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
+github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
+github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
-github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
-github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
-github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
-github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
-github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
-github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
-github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
-github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
-github.com/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+dX7970Q7jk=
-github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
+github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4=
+github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80=
+github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8=
+github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4=
+github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU=
+github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ=
+github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4=
+github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0=
+github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4=
+github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU=
+github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y=
+github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk=
+github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI=
+github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag=
+github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA=
+github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY=
+github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s=
+github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE=
+github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48=
+github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg=
+github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0=
+github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg=
+github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8=
+github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0=
+github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw=
+github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE=
+github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw=
+github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc=
+github.com/go-resty/resty/v2 v2.17.1 h1:x3aMpHK1YM9e4va/TMDRlusDDoZiQ+ViDu/WpA6xTM4=
+github.com/go-resty/resty/v2 v2.17.1/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA=
+github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
+github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/go-zookeeper/zk v1.0.4 h1:DPzxraQx7OrPyXq2phlGlNSIyWEsAox0RJmjTseMV6I=
github.com/go-zookeeper/zk v1.0.4/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
-github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
-github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
+github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
+github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
-github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw=
-github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw=
-github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
+github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
-github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
-github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
+github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
+github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
-github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
-github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3GqO0k0=
-github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w=
-github.com/gophercloud/gophercloud/v2 v2.7.0 h1:o0m4kgVcPgHlcXiWAjoVxGd8QCmvM5VU+YM71pFbn0E=
-github.com/gophercloud/gophercloud/v2 v2.7.0/go.mod h1:Ki/ILhYZr/5EPebrPL9Ej+tUg4lqx71/YH2JWVeU+Qk=
+github.com/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao=
+github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8=
+github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc=
+github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY=
+github.com/gophercloud/gophercloud/v2 v2.10.0 h1:NRadC0aHNvy4iMoFXj5AFiPmut/Sj3hAPAo9B59VMGc=
+github.com/gophercloud/gophercloud/v2 v2.10.0/go.mod h1:Ki/ILhYZr/5EPebrPL9Ej+tUg4lqx71/YH2JWVeU+Qk=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
-github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248=
-github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk=
-github.com/hashicorp/consul/api v1.32.0 h1:5wp5u780Gri7c4OedGEPzmlUEzi0g2KyiPphSr6zjVg=
-github.com/hashicorp/consul/api v1.32.0/go.mod h1:Z8YgY0eVPukT/17ejW+l+C7zJmKwgPHtjU1q16v/Y40=
-github.com/hashicorp/cronexpr v1.1.2 h1:wG/ZYIKT+RT3QkOdgYc+xsKWVRgnxJ1OJtjjy84fJ9A=
-github.com/hashicorp/cronexpr v1.1.2/go.mod h1:P4wA0KBl9C5q2hABiMO7cp6jcIg96CDh1Efb3g1PWA4=
+github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 h1:cLN4IBkmkYZNnk7EAJ0BHIethd+J6LqxFNw5mSiI2bM=
+github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk=
+github.com/hashicorp/consul/api v1.33.2 h1:Q6mE0WZsUTJerlnl9TuXzqrtZ0cKdOCsxcZhj5mKbMs=
+github.com/hashicorp/consul/api v1.33.2/go.mod h1:K3yoL/vnIBcQV/25NeMZVokRvPPERiqp2Udtr4xAfhs=
+github.com/hashicorp/cronexpr v1.1.3 h1:rl5IkxXN2m681EfivTlccqIryzYJSXRGRNa0xeG7NA4=
+github.com/hashicorp/cronexpr v1.1.3/go.mod h1:P4wA0KBl9C5q2hABiMO7cp6jcIg96CDh1Efb3g1PWA4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
@@ -180,24 +202,22 @@ github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISH
github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
-github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
-github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
+github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4=
+github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/golang-lru v0.6.0 h1:uL2shRDx7RTrOrTCUZEGP/wJUFiUI8QT6E7z5o8jga4=
github.com/hashicorp/golang-lru v0.6.0/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
-github.com/hashicorp/nomad/api v0.0.0-20241218080744-e3ac00f30eec h1:+YBzb977VrmffaCX/OBm17dEVJUcWn5dW+eqs3aIJ/A=
-github.com/hashicorp/nomad/api v0.0.0-20241218080744-e3ac00f30eec/go.mod h1:svtxn6QnrQ69P23VvIWMR34tg3vmwLz4UdUzm1dSCgE=
+github.com/hashicorp/nomad/api v0.0.0-20260209224925-94b77491c895 h1:JAnsaAOxJDDHvd9E9DtbXCheE9nVUbS4gchQeV4Lt98=
+github.com/hashicorp/nomad/api v0.0.0-20260209224925-94b77491c895/go.mod h1:JAmS1nGJ1KcTM+MHAkgyrL0GDbsnKiJsp75KyqO2wWc=
github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY=
github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4=
-github.com/hetznercloud/hcloud-go/v2 v2.21.1 h1:IH3liW8/cCRjfJ4cyqYvw3s1ek+KWP8dl1roa0lD8JM=
-github.com/hetznercloud/hcloud-go/v2 v2.21.1/go.mod h1:XOaYycZJ3XKMVWzmqQ24/+1V7ormJHmPdck/kxrNnQA=
+github.com/hetznercloud/hcloud-go/v2 v2.36.0 h1:HlLL/aaVXUulqe+rsjoJmrxKhPi1MflL5O9iq5QEtvo=
+github.com/hetznercloud/hcloud-go/v2 v2.36.0/go.mod h1:MnN/QJEa/RYNQiiVoJjNHPntM7Z1wlYPgJ2HA40/cDE=
github.com/influxdata/influxdb-client-go/v2 v2.14.0 h1:AjbBfJuq+QoaXNcrova8smSjwJdUHnwvfjMF71M1iI4=
github.com/influxdata/influxdb-client-go/v2 v2.14.0/go.mod h1:Ahpm3QXKMJslpXl3IftVLVezreAUtBOTZssDrjZEFHI=
github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 h1:W9WBk7wlPfJLvMCdtV4zPulc4uCPrlywQOmbFOhgQNU=
github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo=
-github.com/ionos-cloud/sdk-go/v6 v6.3.4 h1:jTvGl4LOF8v8OYoEIBNVwbFoqSGAFqn6vGE7sp7/BqQ=
-github.com/ionos-cloud/sdk-go/v6 v6.3.4/go.mod h1:wCVwNJ/21W29FWFUv+fNawOTMlFoP1dS3L+ZuztFW48=
-github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
-github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
+github.com/ionos-cloud/sdk-go/v6 v6.3.6 h1:l/TtKgdQ1wUH3DDe2SfFD78AW+TJWdEbDpQhHkWd6CM=
+github.com/ionos-cloud/sdk-go/v6 v6.3.6/go.mod h1:nUGHP4kZHAZngCVr4v6C8nuargFrtvt7GrzH/hqn7c4=
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
@@ -207,14 +227,14 @@ github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRt
github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
-github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
-github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
+github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
+github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo=
github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
github.com/knadh/koanf/providers/confmap v1.0.0 h1:mHKLJTE7iXEys6deO5p6olAiZdG5zwp8Aebir+/EaRE=
github.com/knadh/koanf/providers/confmap v1.0.0/go.mod h1:txHYHiI2hAtF0/0sCmcuol4IDcuQbKTybiB1nOcUo1A=
-github.com/knadh/koanf/v2 v2.2.1 h1:jaleChtw85y3UdBnI0wCqcg1sj1gPoz6D3caGNHtrNE=
-github.com/knadh/koanf/v2 v2.2.1/go.mod h1:PSFru3ufQgTsI7IF+95rf9s8XA1+aHxKuO/W+dPoHEY=
+github.com/knadh/koanf/v2 v2.3.2 h1:Ee6tuzQYFwcZXQpc2MiVeC6qHMandf5SMUJJNoFp/c4=
+github.com/knadh/koanf/v2 v2.3.2/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28=
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b h1:udzkj9S/zlT5X367kqJis0QP7YMxobob6zhzq6Yre00=
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b/go.mod h1:pcaDhQK0/NJZEvtCO0qQPPropqV0sJOJ6YW7X+9kRwM=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -223,22 +243,18 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
-github.com/linode/linodego v1.52.2 h1:N9ozU27To1LMSrDd8WvJZ5STSz1eGYdyLnxhAR/dIZg=
-github.com/linode/linodego v1.52.2/go.mod h1:bI949fZaVchjWyKIA08hNyvAcV6BAS+PM2op3p7PAWA=
-github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
-github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
-github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
-github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
+github.com/linode/linodego v1.65.0 h1:SdsuGD8VSsPWeShXpE7ihl5vec+fD3MgwhnfYC/rj7k=
+github.com/linode/linodego v1.65.0/go.mod h1:tOFiTErdjkbVnV+4S0+NmIE9dqqZUEM2HsJaGu8wMh8=
+github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
+github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
-github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE=
-github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE=
+github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
+github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
-github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
-github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
@@ -258,18 +274,16 @@ github.com/oapi-codegen/runtime v1.0.0/go.mod h1:LmCUMQuPB4M/nLXilQXhHw+BLZdDb18
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s=
github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
-github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics v0.129.0 h1:2pzb6bC/AAfciC9DN+8d7Y8Rsk8ZPCfp/ACTfZu87FQ=
-github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics v0.129.0/go.mod h1:tIE4dzdxuM7HnFeYA6sj5zfLuUA/JxzQ+UDl1YrHvQw=
-github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.129.0 h1:ydkfqpZ5BWZfEJEs7OUhTHW59og5aZspbUYxoGcAEok=
-github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.129.0/go.mod h1:oA+49dkzmhUx0YFC9JXGuPPSBL0TOTp6jkv7qSr2n0Q=
-github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.129.0 h1:AOVxBvCZfTPj0GLGqBVHpAnlC9t9pl1JXUQXymHliiY=
-github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.129.0/go.mod h1:0CAJ32V/bCUBhNTEvnN9wlOG5IsyZ+Bmhe9e3Eri7CU=
-github.com/open-telemetry/opentelemetry-collector-contrib/processor/deltatocumulativeprocessor v0.129.0 h1:yDLSAoIi3jNt4R/5xN4IJ9YAg1rhOShgchlO/ESv8EY=
-github.com/open-telemetry/opentelemetry-collector-contrib/processor/deltatocumulativeprocessor v0.129.0/go.mod h1:IXQHbTPxqNcuu44FvkyvpYJ6Qy4wh4YsCVkKsp0Flzo=
+github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics v0.145.0 h1:0dYiJ7krIwaHFX6YLNDo/yawTZIu8X16tT/nwW1UTG8=
+github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics v0.145.0/go.mod h1:mhoa9lipcEH0heeKf6+xHzGUrCuAgImQv4/Qpmu0+Fk=
+github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.145.0 h1:sB4yuYx45zig1ceQ+kmrEYy0xMZ+mGagwYIFtJkkU1w=
+github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.145.0/go.mod h1:uLhceuH7ZtiVxk+B0MHI0vhJG2Y4aOzT/hrV6c5KjVU=
+github.com/open-telemetry/opentelemetry-collector-contrib/processor/deltatocumulativeprocessor v0.145.0 h1:en86L47oOTsAkbDc5VEMF5cziXPBK2D4hqGRqLaJtCw=
+github.com/open-telemetry/opentelemetry-collector-contrib/processor/deltatocumulativeprocessor v0.145.0/go.mod h1:osDRUOIfd7IiKkDvcE/VrPp9FFOPJmFp73RuvgOn5gE=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
-github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM=
-github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
+github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
+github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/ovh/go-ovh v1.9.0 h1:6K8VoL3BYjVV3In9tPJUdT7qMx9h0GExN9EXx1r2kKE=
github.com/ovh/go-ovh v1.9.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
@@ -283,31 +297,31 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
+github.com/prometheus/client_golang/exp v0.0.0-20260108101519-fb0838f53562 h1:vwqZvuobg82U0gcG2eVrFH27806bUbNr32SvfRbvdsg=
+github.com/prometheus/client_golang/exp v0.0.0-20260108101519-fb0838f53562/go.mod h1:PmAYDB13uBFBG9qE1qxZZgZWhg7Rg6SfKM5DMK7hjyI=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
-github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
-github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
-github.com/prometheus/otlptranslator v0.0.2 h1:+1CdeLVrRQ6Psmhnobldo0kTp96Rj80DRXRd5OSnMEQ=
-github.com/prometheus/otlptranslator v0.0.2/go.mod h1:P8AwMgdD7XEr6QRUJ2QWLpiAZTgTE2UYgjlu3svompI=
+github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
+github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
+github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos=
+github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
-github.com/prometheus/prometheus v0.305.1-0.20250905124657-5c2e43f09c03 h1:NIVtqQm7NTsUcxfjdHuVE7pw3GVjEgwL6a9ADLSj+Wg=
-github.com/prometheus/prometheus v0.305.1-0.20250905124657-5c2e43f09c03/go.mod h1:9D9CfSEbKg087QXXz2ev+G1SoB6MqQE0ll4jCmrgCe0=
-github.com/prometheus/sigv4 v0.2.0 h1:qDFKnHYFswJxdzGeRP63c4HlH3Vbn1Yf/Ao2zabtVXk=
-github.com/prometheus/sigv4 v0.2.0/go.mod h1:D04rqmAaPPEUkjRQxGqjoxdyJuyCh6E0M18fZr0zBiE=
+github.com/prometheus/prometheus v0.308.1 h1:ApMNI/3/es3Ze90Z7CMb+wwU2BsSYur0m5VKeqHj7h4=
+github.com/prometheus/prometheus v0.308.1/go.mod h1:aHjYCDz9zKRyoUXvMWvu13K9XHOkBB12XrEqibs3e0A=
+github.com/prometheus/sigv4 v0.4.1 h1:EIc3j+8NBea9u1iV6O5ZAN8uvPq2xOIUPcqCTivHuXs=
+github.com/prometheus/sigv4 v0.4.1/go.mod h1:eu+ZbRvsc5TPiHwqh77OWuCnWK73IdkETYY46P4dXOU=
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
-github.com/redis/go-redis/v9 v9.8.0 h1:q3nRvjrlge/6UD7eTu/DSg2uYiU2mCL0G/uzBWqhicI=
-github.com/redis/go-redis/v9 v9.8.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
-github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
-github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
-github.com/scaleway/scaleway-sdk-go v1.0.0-beta.33 h1:KhF0WejiUTDbL5X55nXowP7zNopwpowa6qaMAWyIE+0=
-github.com/scaleway/scaleway-sdk-go v1.0.0-beta.33/go.mod h1:792k1RTU+5JeMXm35/e2Wgp71qPH/DmDoZrRc+EFZDk=
-github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
-github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
+github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
+github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 h1:ObX9hZmK+VmijreZO/8x9pQ8/P/ToHD/bdSb4Eg4tUo=
+github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36/go.mod h1:LEsDu4BubxK7/cWhtlQWfuxwL4rf/2UEpxXz1o1EMtM=
+github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
+github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
-github.com/stackitcloud/stackit-sdk-go/core v0.17.2 h1:jPyn+i8rkp2hM80+hOg0B/1EVRbMt778Tr5RWyK1m2E=
-github.com/stackitcloud/stackit-sdk-go/core v0.17.2/go.mod h1:8KIw3czdNJ9sdil9QQimxjR6vHjeINFrRv0iZ67wfn0=
+github.com/stackitcloud/stackit-sdk-go/core v0.21.1 h1:Y/PcAgM7DPYMNqum0MLv4n1mF9ieuevzcCIZYQfm3Ts=
+github.com/stackitcloud/stackit-sdk-go/core v0.21.1/go.mod h1:osMglDby4csGZ5sIfhNyYq1bS1TxIdPY88+skE/kkmI=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -328,164 +342,169 @@ github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
-go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
-go.opentelemetry.io/collector/component v1.45.0 h1:gGFfVdbQ+1YuyUkJjWo85I7euu3H/CiupuzCHv8OgHA=
-go.opentelemetry.io/collector/component v1.45.0/go.mod h1:xoNFnRKE8Iv6gmlqAKgjayWraRnDcYLLgrPt9VgyO2g=
-go.opentelemetry.io/collector/component/componentstatus v0.129.0 h1:ejpBAt7hXAAZiQKcSxLvcy8sj8SjY4HOLdoXIlW6ybw=
-go.opentelemetry.io/collector/component/componentstatus v0.129.0/go.mod h1:/dLPIxn/tRMWmGi+DPtuFoBsffOLqPpSZ2IpEQzYtwI=
-go.opentelemetry.io/collector/component/componenttest v0.129.0 h1:gpKkZGCRPu3Yn0U2co09bMvhs17yLFb59oV8Gl9mmRI=
-go.opentelemetry.io/collector/component/componenttest v0.129.0/go.mod h1:JR9k34Qvd/pap6sYkPr5QqdHpTn66A5lYeYwhenKBAM=
-go.opentelemetry.io/collector/confmap v1.35.0 h1:U4JDATAl4PrKWe9bGHbZkoQXmJXefWgR2DIkFvw8ULQ=
-go.opentelemetry.io/collector/confmap v1.35.0/go.mod h1:qX37ExVBa+WU4jWWJCZc7IJ+uBjb58/9oL+/ctF1Bt0=
-go.opentelemetry.io/collector/confmap/xconfmap v0.129.0 h1:Q/+pJKrkCaMPSoSAH2BpC3UZCh+5hTiFkh/bdy5yChk=
-go.opentelemetry.io/collector/confmap/xconfmap v0.129.0/go.mod h1:RNMnlay2meJDXcKjxiLbST9/YAhKLJlj0kZCrJrLGgw=
-go.opentelemetry.io/collector/consumer v1.45.0 h1:TtqXxgW+1GSCwdoohq0fzqnfqrZBKbfo++1XRj8mrEA=
-go.opentelemetry.io/collector/consumer v1.45.0/go.mod h1:pJzqTWBubwLt8mVou+G4/Hs23b3m425rVmld3LqOYpY=
-go.opentelemetry.io/collector/consumer/consumertest v0.139.0 h1:06mu43mMO7l49ASJ/GEbKgTWcV3py5zE/pKhNBZ1b3k=
-go.opentelemetry.io/collector/consumer/consumertest v0.139.0/go.mod h1:gaeCpRQGbCFYTeLzi+Z2cTDt40GiIa3hgIEgLEmiC78=
-go.opentelemetry.io/collector/consumer/xconsumer v0.139.0 h1:FhzDv+idglnrfjqPvnUw3YAEOkXSNv/FuNsuMiXQwcY=
-go.opentelemetry.io/collector/consumer/xconsumer v0.139.0/go.mod h1:yWrg/6FE/A4Q7eo/Mg++CzkBoSILHdeMnTlxV3serI0=
-go.opentelemetry.io/collector/featuregate v1.45.0 h1:D06hpf1F2KzKC+qXLmVv5e8IZpgCyZVeVVC8iOQxVmw=
-go.opentelemetry.io/collector/featuregate v1.45.0/go.mod h1:d0tiRzVYrytB6LkcYgz2ESFTv7OktRPQe0QEQcPt1L4=
-go.opentelemetry.io/collector/pdata v1.45.0 h1:q4XaISpeX640BcwXwb2mKOVw/gb67r22HjGWl8sbWsk=
-go.opentelemetry.io/collector/pdata v1.45.0/go.mod h1:5q2f001YhwMQO8QvpFhCOa4Cq/vtwX9W4HRMsXkU/nE=
-go.opentelemetry.io/collector/pdata/pprofile v0.139.0 h1:UA5TgFzYmRuJN3Wz0GR1efLUfjbs5rH0HTaxfASpTR8=
-go.opentelemetry.io/collector/pdata/pprofile v0.139.0/go.mod h1:sI5qHt+zzE2fhOWFdJIaiDBR0yGGjD4A4ZvDFU0tiHk=
-go.opentelemetry.io/collector/pdata/testdata v0.129.0 h1:n1QLnLOtrcAR57oMSVzmtPsQEpCc/nE5Avk1xfuAkjY=
-go.opentelemetry.io/collector/pdata/testdata v0.129.0/go.mod h1:RfY5IKpmcvkS2IGVjl9jG9fcT7xpQEBWpg9sQOn/7mY=
-go.opentelemetry.io/collector/pipeline v1.45.0 h1:sn9JJAEBe3XABTkWechMk0eH60QMBjjNe5V+ccBl+Uo=
-go.opentelemetry.io/collector/pipeline v1.45.0/go.mod h1:xUrAqiebzYbrgxyoXSkk6/Y3oi5Sy3im2iCA51LwUAI=
-go.opentelemetry.io/collector/processor v1.45.0 h1:GH5km9BkDQOoz7MR0jzTnzB1Kb5vtKzPwa/wDmRg2dQ=
-go.opentelemetry.io/collector/processor v1.45.0/go.mod h1:wdlaTTC3wqlZIJP9R9/SLc2q7h+MFGARsxfjgPtwbes=
-go.opentelemetry.io/collector/processor/processortest v0.129.0 h1:r5iJHdS7Ffdb2zmMVYx4ahe92PLrce5cas/AJEXivkY=
-go.opentelemetry.io/collector/processor/processortest v0.129.0/go.mod h1:gdf8GzyzjGoDTA11+CPwC4jfXphtC+B7MWbWn+LIWXc=
-go.opentelemetry.io/collector/processor/xprocessor v0.129.0 h1:V3Zgd+YIeu3Ij3DPlGtzdcTwpqOQIqQVcL5jdHHS7sc=
-go.opentelemetry.io/collector/processor/xprocessor v0.129.0/go.mod h1:78T+AP5NO137W/E+SibQhaqOyS67fR+IN697b4JFh00=
+go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
+go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
+go.opentelemetry.io/collector/component v1.51.0 h1:btNW76MCRmpsk0ARRT5wspDXF9tvdaLd3uBtYXIiQn0=
+go.opentelemetry.io/collector/component v1.51.0/go.mod h1:Zlgwh4yTLDhJglOXqiyXZ7paepTvvoijfFjLqOr/Qww=
+go.opentelemetry.io/collector/component/componentstatus v0.145.0 h1:EwUZfSaagdpRXnlrb0TqReJXXW2p9HWBU5YiIeXPCAE=
+go.opentelemetry.io/collector/component/componentstatus v0.145.0/go.mod h1:OiYb8rT4FtSJPFSGCKYvOaajdueDUTJZncixGrmy5aM=
+go.opentelemetry.io/collector/component/componenttest v0.145.0 h1:ryhRrXqQybGMhz7A7t32NC8BXAFcX2o1RetgPM7vw88=
+go.opentelemetry.io/collector/component/componenttest v0.145.0/go.mod h1:5uStrhUdZ0Fw3se00CPmVaRtW8o9N8kKiY76OSCWFjQ=
+go.opentelemetry.io/collector/confmap v1.51.0 h1:C9YlMNkIgzuauLpUz2F7DLlWwqAmkQKNcKj1XATVWuE=
+go.opentelemetry.io/collector/confmap v1.51.0/go.mod h1:uWi4b9lHfvEC2poJ2I2vXwGUREVEQTcdUguOpfqdcHM=
+go.opentelemetry.io/collector/confmap/xconfmap v0.145.0 h1:ngbyfh4+SKlA+osgsak3AxUNPxVxaJTmA0Sl7VfJzwY=
+go.opentelemetry.io/collector/confmap/xconfmap v0.145.0/go.mod h1:zTSK+c76NAy/tI1R3xfZjdoI04D9EYDnzAHQQwl6AmA=
+go.opentelemetry.io/collector/consumer v1.51.0 h1:Ex1x/k9VEEA2DOgt/eSc2Z9KTp0I6xBSruLmrYFfIFY=
+go.opentelemetry.io/collector/consumer v1.51.0/go.mod h1:Erk6qdfVj+24QTrGCpurcrF+qdUlHkb4dgMy5wJxLvY=
+go.opentelemetry.io/collector/consumer/consumertest v0.145.0 h1:3+uMwuMHoXMAU+Z6mwCRA3AxWeL7SujcAQwqqHJ1gCc=
+go.opentelemetry.io/collector/consumer/consumertest v0.145.0/go.mod h1:IFc/FeaIHQClb8KK0aVn0tFDNMc+/MmfQ+aBT1cJNeo=
+go.opentelemetry.io/collector/consumer/xconsumer v0.145.0 h1:9w7KKv9lVJoHvMLC6SUJHenU/KySdEgFJXbB4JQOEsk=
+go.opentelemetry.io/collector/consumer/xconsumer v0.145.0/go.mod h1:SryDCLP2ZaFeZJtA2CSksJ0XvjH8k3LmlfXvy/kC7Wc=
+go.opentelemetry.io/collector/featuregate v1.51.0 h1:dxJuv/3T84dhNKp7fz5+8srHz1dhquGzDpLW4OZTFBw=
+go.opentelemetry.io/collector/featuregate v1.51.0/go.mod h1:/1bclXgP91pISaEeNulRxzzmzMTm4I5Xih2SnI4HRSo=
+go.opentelemetry.io/collector/internal/componentalias v0.145.0 h1:A9V5IiETzz8FCtjxjRM5gf7RE3sOtA1h8phmpQjXTZ4=
+go.opentelemetry.io/collector/internal/componentalias v0.145.0/go.mod h1:sEKEAwAn45ZiXRk3T/vbkvetw14tIRd0CJIxcEx9SsQ=
+go.opentelemetry.io/collector/internal/testutil v0.145.0 h1:H/KL0GH3kGqSMKxZvnQ0B0CulfO9xdTg4DZf28uV7fY=
+go.opentelemetry.io/collector/internal/testutil v0.145.0/go.mod h1:YAD9EAkwh/l5asZNbEBEUCqEjoL1OKMjAMoPjPqH76c=
+go.opentelemetry.io/collector/pdata v1.51.0 h1:DnDhSEuDXNdzGRB7f6oOfXpbDApwBX3tY+3K69oUrDA=
+go.opentelemetry.io/collector/pdata v1.51.0/go.mod h1:GoX1bjKDR++mgFKdT7Hynv9+mdgQ1DDXbjs7/Ww209Q=
+go.opentelemetry.io/collector/pdata/pprofile v0.145.0 h1:ASMKpoqokf8HhzjoeMKZf0K6UXLhufVwNXH0sSuUn5w=
+go.opentelemetry.io/collector/pdata/pprofile v0.145.0/go.mod h1:a60GC7wQPhLAixWzKbbP51QLwwc+J0Cmp4SurOlhGUk=
+go.opentelemetry.io/collector/pdata/testdata v0.145.0 h1:iFsxsCMtE3lnAc/5kZbhZHpRv1OMmM+O5ry46xdQHbg=
+go.opentelemetry.io/collector/pdata/testdata v0.145.0/go.mod h1:0y2ERArdzqmYdJHdKLKue+AUubSEGlwK49F+23+Mbic=
+go.opentelemetry.io/collector/pipeline v1.51.0 h1:GZBNW+aaOE+zufGzAkXy0OI7n1cqepEa5J+beaOpS2k=
+go.opentelemetry.io/collector/pipeline v1.51.0/go.mod h1:xUrAqiebzYbrgxyoXSkk6/Y3oi5Sy3im2iCA51LwUAI=
+go.opentelemetry.io/collector/processor v1.51.0 h1:PKpCzkLQmqaW08TOVh/zM0qx07Ihq+DR5J/OBkPiL9o=
+go.opentelemetry.io/collector/processor v1.51.0/go.mod h1:rtIPFS+EFRAkG+CSwtjxs2IsIkuZStObvALeueD02XI=
+go.opentelemetry.io/collector/processor/processortest v0.145.0 h1:RDGBmyZnHk7XVK/EdLt/8iPWj+QLStbbVi1nFTNR01s=
+go.opentelemetry.io/collector/processor/processortest v0.145.0/go.mod h1:WAvxAzSojkdoZB915Z1lsVHCPDJBb2fepjJBjenrzjg=
+go.opentelemetry.io/collector/processor/xprocessor v0.145.0 h1:DaIE7MxRlg0OL1o2P0GQZtmZeExAmVso3qWv8S0RLps=
+go.opentelemetry.io/collector/processor/xprocessor v0.145.0/go.mod h1:kUwRyKBU/kjCmXodd+0z7CpvcP0A9G9/QL+MaJt4U2o=
go.opentelemetry.io/collector/semconv v0.128.0 h1:MzYOz7Vgb3Kf5D7b49pqqgeUhEmOCuT10bIXb/Cc+k4=
go.opentelemetry.io/collector/semconv v0.128.0/go.mod h1:OPXer4l43X23cnjLXIZnRj/qQOjSuq4TgBLI76P9hns=
-go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0 h1:2pn7OzMewmYRiNtv1doZnLo3gONcnMHlFnmOR8Vgt+8=
-go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0/go.mod h1:rjbQTDEPQymPE0YnRQp9/NuPwwtL0sesz/fnqRW/v84=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
-go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
-go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
-go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
-go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
-go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
-go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
-go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
-go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
-go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
-go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
-go.opentelemetry.io/proto/slim/otlp v1.8.0 h1:afcLwp2XOeCbGrjufT1qWyruFt+6C9g5SOuymrSPUXQ=
-go.opentelemetry.io/proto/slim/otlp v1.8.0/go.mod h1:Yaa5fjYm1SMCq0hG0x/87wV1MP9H5xDuG/1+AhvBcsI=
-go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.1.0 h1:Uc+elixz922LHx5colXGi1ORbsW8DTIGM+gg+D9V7HE=
-go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.1.0/go.mod h1:VyU6dTWBWv6h9w/+DYgSZAPMabWbPTFTuxp25sM8+s0=
-go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.1.0 h1:i8YpvWGm/Uq1koL//bnbJ/26eV3OrKWm09+rDYo7keU=
-go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.1.0/go.mod h1:pQ70xHY/ZVxNUBPn+qUWPl8nwai87eWdqL3M37lNi9A=
+go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.65.0 h1:ab5U7DpTjjN8pNgwqlA/s0Csb+N2Raqo9eTSDhfg4Z8=
+go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.65.0/go.mod h1:nwFJC46Dxhqz5R9k7IV8To/Z46JPvW+GNKhTxQQlUzg=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
+go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
+go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
+go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
+go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
+go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
+go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
+go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
+go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
+go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
+go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
+go.opentelemetry.io/proto/slim/otlp v1.9.0 h1:fPVMv8tP3TrsqlkH1HWYUpbCY9cAIemx184VGkS6vlE=
+go.opentelemetry.io/proto/slim/otlp v1.9.0/go.mod h1:xXdeJJ90Gqyll+orzUkY4bOd2HECo5JofeoLpymVqdI=
+go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.2.0 h1:o13nadWDNkH/quoDomDUClnQBpdQQ2Qqv0lQBjIXjE8=
+go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.2.0/go.mod h1:Gyb6Xe7FTi/6xBHwMmngGoHqL0w29Y4eW8TGFzpefGA=
+go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.2.0 h1:EiUYvtwu6PMrMHVjcPfnsG3v+ajPkbUeH+IL93+QYyk=
+go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.2.0/go.mod h1:mUUHKFiN2SST3AhJ8XhJxEoeVW12oqfXog0Bo8W3Ec4=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
-go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
-go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
-go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
-go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
+go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
+go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
+go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
+go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
+go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
+go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
-golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
-golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
-golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
+golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
+golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
+golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
+golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
-golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
+golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
+golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
-golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
-golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
-golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
+golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
+golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
+golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
+golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
-golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
+golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
-golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
-golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
-golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
+golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
+golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
+golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
-golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
-golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
-golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
+golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
+golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
+golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
+golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
-golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
+golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
+golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
-google.golang.org/api v0.239.0 h1:2hZKUnFZEy81eugPs4e2XzIJ5SOwQg0G82bpXD65Puo=
-google.golang.org/api v0.239.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50=
-google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 h1:1tXaIXCracvtsRxSBsYDiSBN0cuJvM7QYW+MrpIRY78=
-google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b h1:ULiyYQ0FdsJhwwZUwbaXpZF5yUE3h+RA+gxvBu37ucc=
-google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:oDOGiMSXHL4sDTJvFvIB9nRQCGdLP1o/iVaqQK8zB+M=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b h1:zPKJod4w6F1+nRGDI9ubnXYhU9NSWoFAijkHkUXeTK8=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
-google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
-google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
-google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
-google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
+google.golang.org/api v0.266.0 h1:hco+oNCf9y7DmLeAtHJi/uBAY7n/7XC9mZPxu1ROiyk=
+google.golang.org/api v0.266.0/go.mod h1:Jzc0+ZfLnyvXma3UtaTl023TdhZu6OMBP9tJ+0EmFD0=
+google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0=
+google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
+google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
+google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
+google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
+google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
-gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
-gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
+gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
+gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
-gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
-gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/ini.v1 v1.67.1 h1:tVBILHy0R6e4wkYOn3XmiITt/hEVH4TFMYvAX2Ytz6k=
+gopkg.in/ini.v1 v1.67.1/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-k8s.io/api v0.33.5 h1:YR+uhYj05jdRpcksv8kjSliW+v9hwXxn6Cv10aR8Juw=
-k8s.io/api v0.33.5/go.mod h1:2gzShdwXKT5yPGiqrTrn/U/nLZ7ZyT4WuAj3XGDVgVs=
-k8s.io/apimachinery v0.33.5 h1:NiT64hln4TQXeYR18/ES39OrNsjGz8NguxsBgp+6QIo=
-k8s.io/apimachinery v0.33.5/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
-k8s.io/client-go v0.33.5 h1:I8BdmQGxInpkMEnJvV6iG7dqzP3JRlpZZlib3OMFc3o=
-k8s.io/client-go v0.33.5/go.mod h1:W8PQP4MxbM4ypgagVE65mUUqK1/ByQkSALF9tzuQ6u0=
+k8s.io/api v0.35.0 h1:iBAU5LTyBI9vw3L5glmat1njFK34srdLmktWwLTprlY=
+k8s.io/api v0.35.0/go.mod h1:AQ0SNTzm4ZAczM03QH42c7l3bih1TbAXYo0DkF8ktnA=
+k8s.io/apimachinery v0.35.0 h1:Z2L3IHvPVv/MJ7xRxHEtk6GoJElaAqDCCU0S6ncYok8=
+k8s.io/apimachinery v0.35.0/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
+k8s.io/client-go v0.35.0 h1:IAW0ifFbfQQwQmga0UdoH0yvdqrbwMdq9vIFEhRpxBE=
+k8s.io/client-go v0.35.0/go.mod h1:q2E5AAyqcbeLGPdoRB+Nxe3KYTfPce1Dnu1myQdqz9o=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
-k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4=
-k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8=
-k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=
-k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
-sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
-sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
+k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=
+k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
+k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
+k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
+sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
+sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
-sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc=
-sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps=
-sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
-sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
+sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
+sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
+sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
+sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
diff --git a/documentation/examples/remote_storage/remote_storage_adapter/graphite/client.go b/documentation/examples/remote_storage/remote_storage_adapter/graphite/client.go
index d04355a712..61488127f6 100644
--- a/documentation/examples/remote_storage/remote_storage_adapter/graphite/client.go
+++ b/documentation/examples/remote_storage/remote_storage_adapter/graphite/client.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
@@ -73,8 +73,7 @@ func pathFromMetric(m model.Metric, prefix string) string {
// Since we use '.' instead of '=' to separate label and values
// it means that we can't have an '.' in the metric name. Fortunately
// this is prohibited in prometheus metrics.
- buffer.WriteString(fmt.Sprintf(
- ".%s.%s", string(l), escape(v)))
+ fmt.Fprintf(&buffer, ".%s.%s", string(l), escape(v))
}
return buffer.String()
}
diff --git a/documentation/examples/remote_storage/remote_storage_adapter/graphite/client_test.go b/documentation/examples/remote_storage/remote_storage_adapter/graphite/client_test.go
index 535027e076..8a96413443 100644
--- a/documentation/examples/remote_storage/remote_storage_adapter/graphite/client_test.go
+++ b/documentation/examples/remote_storage/remote_storage_adapter/graphite/client_test.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
diff --git a/documentation/examples/remote_storage/remote_storage_adapter/graphite/escape.go b/documentation/examples/remote_storage/remote_storage_adapter/graphite/escape.go
index 1386f46761..e7357c001a 100644
--- a/documentation/examples/remote_storage/remote_storage_adapter/graphite/escape.go
+++ b/documentation/examples/remote_storage/remote_storage_adapter/graphite/escape.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
@@ -82,7 +82,7 @@ const (
func escape(tv model.LabelValue) string {
length := len(tv)
result := bytes.NewBuffer(make([]byte, 0, length))
- for i := 0; i < length; i++ {
+ for i := range length {
b := tv[i]
switch {
// . is reserved by graphite, % is used to escape other bytes.
diff --git a/documentation/examples/remote_storage/remote_storage_adapter/influxdb/client.go b/documentation/examples/remote_storage/remote_storage_adapter/influxdb/client.go
index 005f8d534d..9ef5b03e72 100644
--- a/documentation/examples/remote_storage/remote_storage_adapter/influxdb/client.go
+++ b/documentation/examples/remote_storage/remote_storage_adapter/influxdb/client.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
@@ -96,7 +96,7 @@ func (c *Client) Write(samples model.Samples) error {
p := influx.NewPoint(
string(s.Metric[model.MetricNameLabel]),
tagsFromMetric(s.Metric),
- map[string]interface{}{"value": v},
+ map[string]any{"value": v},
s.Timestamp.Time(),
)
points = append(points, p)
@@ -158,16 +158,17 @@ func (c *Client) buildCommand(q *prompb.Query) (string, error) {
// If we don't find a metric name matcher, query all metrics
// (InfluxDB measurements) by default.
- measurement := `r._measurement`
+ var measurement strings.Builder
+ measurement.WriteString(`r._measurement`)
matchers := make([]string, 0, len(q.Matchers))
var joinedMatchers string
for _, m := range q.Matchers {
if m.Name == model.MetricNameLabel {
switch m.Type {
case prompb.LabelMatcher_EQ:
- measurement += fmt.Sprintf(" == \"%s\"", m.Value)
+ fmt.Fprintf(&measurement, " == \"%s\"", m.Value)
case prompb.LabelMatcher_RE:
- measurement += fmt.Sprintf(" =~ /%s/", escapeSlashes(m.Value))
+ fmt.Fprintf(&measurement, " =~ /%s/", escapeSlashes(m.Value))
default:
// TODO: Figure out how to support these efficiently.
return "", errors.New("non-equal or regex-non-equal matchers are not supported on the metric name yet")
@@ -195,7 +196,7 @@ func (c *Client) buildCommand(q *prompb.Query) (string, error) {
// _measurement must be retained, otherwise "invalid metric name" shall be thrown
command := fmt.Sprintf(
"from(bucket: \"%s\") |> range(%s) |> filter(fn: (r) => %s%s)",
- c.bucket, rangeInNs, measurement, joinedMatchers,
+ c.bucket, rangeInNs, measurement.String(), joinedMatchers,
)
return command, nil
@@ -237,7 +238,7 @@ func mergeResult(labelsToSeries map[string]*prompb.TimeSeries, record *query.Flu
return nil
}
-func filterOutBuiltInLabels(labels map[string]interface{}) {
+func filterOutBuiltInLabels(labels map[string]any) {
delete(labels, "table")
delete(labels, "_start")
delete(labels, "_stop")
@@ -248,7 +249,7 @@ func filterOutBuiltInLabels(labels map[string]interface{}) {
delete(labels, "_measurement")
}
-func concatLabels(labels map[string]interface{}) string {
+func concatLabels(labels map[string]any) string {
// 0xff cannot occur in valid UTF-8 sequences, so use it
// as a separator here.
separator := "\xff"
@@ -259,7 +260,7 @@ func concatLabels(labels map[string]interface{}) string {
return strings.Join(pairs, separator)
}
-func tagsToLabelPairs(name string, tags map[string]interface{}) []prompb.Label {
+func tagsToLabelPairs(name string, tags map[string]any) []prompb.Label {
pairs := make([]prompb.Label, 0, len(tags))
for k, v := range tags {
if v == nil {
@@ -283,7 +284,7 @@ func tagsToLabelPairs(name string, tags map[string]interface{}) []prompb.Label {
return pairs
}
-func valuesToSamples(timestamp time.Time, value interface{}) (prompb.Sample, error) {
+func valuesToSamples(timestamp time.Time, value any) (prompb.Sample, error) {
var valueFloat64 float64
var valueInt64 int64
var ok bool
diff --git a/documentation/examples/remote_storage/remote_storage_adapter/influxdb/client_test.go b/documentation/examples/remote_storage/remote_storage_adapter/influxdb/client_test.go
index f78d4db794..faf48045cb 100644
--- a/documentation/examples/remote_storage/remote_storage_adapter/influxdb/client_test.go
+++ b/documentation/examples/remote_storage/remote_storage_adapter/influxdb/client_test.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
diff --git a/documentation/examples/remote_storage/remote_storage_adapter/main.go b/documentation/examples/remote_storage/remote_storage_adapter/main.go
index ffcbb5385a..ac891cca50 100644
--- a/documentation/examples/remote_storage/remote_storage_adapter/main.go
+++ b/documentation/examples/remote_storage/remote_storage_adapter/main.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
diff --git a/documentation/examples/remote_storage/remote_storage_adapter/opentsdb/client.go b/documentation/examples/remote_storage/remote_storage_adapter/opentsdb/client.go
index ffc6c58b88..e2f64be5d8 100644
--- a/documentation/examples/remote_storage/remote_storage_adapter/opentsdb/client.go
+++ b/documentation/examples/remote_storage/remote_storage_adapter/opentsdb/client.go
@@ -1,4 +1,4 @@
-// Copyright 2013 The Prometheus Authors
+// 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
diff --git a/documentation/examples/remote_storage/remote_storage_adapter/opentsdb/client_test.go b/documentation/examples/remote_storage/remote_storage_adapter/opentsdb/client_test.go
index bc9703c88c..fa76cc334d 100644
--- a/documentation/examples/remote_storage/remote_storage_adapter/opentsdb/client_test.go
+++ b/documentation/examples/remote_storage/remote_storage_adapter/opentsdb/client_test.go
@@ -1,4 +1,4 @@
-// Copyright 2013 The Prometheus Authors
+// 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
diff --git a/documentation/examples/remote_storage/remote_storage_adapter/opentsdb/tagvalue.go b/documentation/examples/remote_storage/remote_storage_adapter/opentsdb/tagvalue.go
index 6a691778af..f822e37808 100644
--- a/documentation/examples/remote_storage/remote_storage_adapter/opentsdb/tagvalue.go
+++ b/documentation/examples/remote_storage/remote_storage_adapter/opentsdb/tagvalue.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The Prometheus Authors
+// 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
@@ -66,7 +66,7 @@ func (tv TagValue) MarshalJSON() ([]byte, error) {
// Need at least two more bytes than in tv.
result := bytes.NewBuffer(make([]byte, 0, length+2))
result.WriteByte('"')
- for i := 0; i < length; i++ {
+ for i := range length {
b := tv[i]
switch {
case (b >= '-' && b <= '9') || // '-', '.', '/', 0-9
diff --git a/documentation/examples/remote_storage/remote_storage_adapter/opentsdb/tagvalue_test.go b/documentation/examples/remote_storage/remote_storage_adapter/opentsdb/tagvalue_test.go
index 5adedb3248..071fd5a85a 100644
--- a/documentation/examples/remote_storage/remote_storage_adapter/opentsdb/tagvalue_test.go
+++ b/documentation/examples/remote_storage/remote_storage_adapter/opentsdb/tagvalue_test.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The Prometheus Authors
+// 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
diff --git a/go.mod b/go.mod
index 2f1b4e5039..803cf2b926 100644
--- a/go.mod
+++ b/go.mod
@@ -1,139 +1,164 @@
module github.com/prometheus/prometheus
-go 1.24.0
+go 1.25.5
require (
- github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1
- github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.12.0
+ github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0
+ github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5 v5.7.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v4 v4.3.0
github.com/Code-Hex/go-generics-cache v1.5.1
- github.com/KimMachineGun/automemlimit v0.7.4
+ github.com/KimMachineGun/automemlimit v0.7.5
github.com/alecthomas/kingpin/v2 v2.4.0
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b
- github.com/aws/aws-sdk-go-v2 v1.39.6
- github.com/aws/aws-sdk-go-v2/config v1.31.17
- github.com/aws/aws-sdk-go-v2/credentials v1.18.21
- github.com/aws/aws-sdk-go-v2/service/ec2 v1.262.0
- github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.4
- github.com/aws/aws-sdk-go-v2/service/sts v1.39.1
- github.com/aws/smithy-go v1.23.2
+ github.com/aws/aws-sdk-go-v2 v1.41.1
+ github.com/aws/aws-sdk-go-v2/config v1.32.9
+ github.com/aws/aws-sdk-go-v2/credentials v1.19.9
+ github.com/aws/aws-sdk-go-v2/service/ec2 v1.290.0
+ github.com/aws/aws-sdk-go-v2/service/ecs v1.72.0
+ github.com/aws/aws-sdk-go-v2/service/elasticache v1.51.9
+ github.com/aws/aws-sdk-go-v2/service/kafka v1.48.0
+ github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.11
+ github.com/aws/aws-sdk-go-v2/service/sts v1.41.6
+ github.com/aws/smithy-go v1.24.1
github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3
github.com/cespare/xxhash/v2 v2.3.0
github.com/dennwc/varint v1.0.0
- github.com/digitalocean/godo v1.168.0
+ github.com/digitalocean/godo v1.175.0
github.com/docker/docker v28.5.2+incompatible
github.com/edsrzf/mmap-go v1.2.0
- github.com/envoyproxy/go-control-plane/envoy v1.35.0
- github.com/envoyproxy/protoc-gen-validate v1.2.1
+ github.com/envoyproxy/go-control-plane/envoy v1.37.0
+ github.com/envoyproxy/protoc-gen-validate v1.3.3
github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb
+ github.com/felixge/fgprof v0.9.5
github.com/fsnotify/fsnotify v1.9.0
- github.com/go-openapi/strfmt v0.24.0
+ github.com/go-openapi/strfmt v0.25.0
github.com/go-zookeeper/zk v1.0.4
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-20250923004556-9e5a51aed1e8
+ github.com/google/pprof v0.0.0-20260202012954-cb029daf43ef
github.com/google/uuid v1.6.0
- github.com/gophercloud/gophercloud/v2 v2.8.0
+ github.com/gophercloud/gophercloud/v2 v2.10.0
github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853
- github.com/hashicorp/consul/api v1.32.0
- github.com/hashicorp/nomad/api v0.0.0-20250930071859-eaa0fe0e27af
- github.com/hetznercloud/hcloud-go/v2 v2.29.0
- github.com/ionos-cloud/sdk-go/v6 v6.3.4
+ github.com/hashicorp/consul/api v1.33.2
+ github.com/hashicorp/nomad/api v0.0.0-20260220212019-daca79db0bd6
+ github.com/hetznercloud/hcloud-go/v2 v2.36.0
+ github.com/ionos-cloud/sdk-go/v6 v6.3.6
github.com/json-iterator/go v1.1.12
- github.com/klauspost/compress v1.18.1
+ github.com/klauspost/compress v1.18.4
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b
- github.com/linode/linodego v1.60.0
- github.com/miekg/dns v1.1.68
+ github.com/linode/linodego v1.65.0
+ github.com/miekg/dns v1.1.72
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f
- github.com/nsf/jsondiff v0.0.0-20230430225905-43f6cf3098c1
+ github.com/nsf/jsondiff v0.0.0-20260207060731-8e8d90c4c0ac
github.com/oklog/run v1.2.0
github.com/oklog/ulid/v2 v2.1.1
- github.com/open-telemetry/opentelemetry-collector-contrib/processor/deltatocumulativeprocessor v0.139.0
+ github.com/open-telemetry/opentelemetry-collector-contrib/processor/deltatocumulativeprocessor v0.145.0
github.com/ovh/go-ovh v1.9.0
- github.com/prometheus/alertmanager v0.28.1
+ github.com/pb33f/libopenapi v0.33.4
+ github.com/pb33f/libopenapi-validator v0.11.1
+ github.com/prometheus/alertmanager v0.31.1
github.com/prometheus/client_golang v1.23.2
- github.com/prometheus/client_golang/exp v0.0.0-20250914183048-a974e0d45e0a
+ github.com/prometheus/client_golang/exp v0.0.0-20260108101519-fb0838f53562
github.com/prometheus/client_model v0.6.2
- github.com/prometheus/common v0.67.2
+ github.com/prometheus/common v0.67.5
github.com/prometheus/common/assets v0.2.0
- github.com/prometheus/exporter-toolkit v0.15.0
- github.com/prometheus/sigv4 v0.2.1
- github.com/scaleway/scaleway-sdk-go v1.0.0-beta.35
+ github.com/prometheus/exporter-toolkit v0.15.1
+ github.com/prometheus/sigv4 v0.4.1
+ github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36
github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c
- github.com/stackitcloud/stackit-sdk-go/core v0.17.3
+ github.com/stackitcloud/stackit-sdk-go/core v0.21.1
github.com/stretchr/testify v1.11.1
github.com/vultr/govultr/v2 v2.17.2
- go.opentelemetry.io/collector/component v1.45.0
- go.opentelemetry.io/collector/consumer v1.45.0
- go.opentelemetry.io/collector/pdata v1.45.0
- go.opentelemetry.io/collector/processor v1.45.0
- go.opentelemetry.io/collector/semconv v0.128.0
- go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0
- go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0
- go.opentelemetry.io/otel v1.38.0
- go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0
- go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0
- go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0
- go.opentelemetry.io/otel/metric v1.38.0
- go.opentelemetry.io/otel/sdk v1.38.0
- go.opentelemetry.io/otel/trace v1.38.0
+ go.opentelemetry.io/collector/component v1.51.0
+ go.opentelemetry.io/collector/consumer v1.51.0
+ go.opentelemetry.io/collector/pdata v1.51.0
+ go.opentelemetry.io/collector/processor v1.51.0
+ go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.65.0
+ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0
+ go.opentelemetry.io/otel v1.40.0
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0
+ go.opentelemetry.io/otel/metric v1.40.0
+ go.opentelemetry.io/otel/sdk v1.40.0
+ go.opentelemetry.io/otel/trace v1.40.0
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
- golang.org/x/oauth2 v0.32.0
- golang.org/x/sync v0.17.0
- golang.org/x/sys v0.37.0
- golang.org/x/text v0.30.0
- google.golang.org/api v0.252.0
- google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4
- google.golang.org/grpc v1.76.0
- google.golang.org/protobuf v1.36.10
- gopkg.in/yaml.v3 v3.0.1
- k8s.io/api v0.34.1
- k8s.io/apimachinery v0.34.1
- k8s.io/client-go v0.34.1
+ go.yaml.in/yaml/v3 v3.0.4
+ go.yaml.in/yaml/v4 v4.0.0-rc.4
+ golang.org/x/oauth2 v0.35.0
+ golang.org/x/sync v0.19.0
+ golang.org/x/sys v0.41.0
+ golang.org/x/text v0.34.0
+ google.golang.org/api v0.267.0
+ google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d
+ google.golang.org/grpc v1.79.1
+ google.golang.org/protobuf v1.36.11
+ k8s.io/api v0.35.1
+ k8s.io/apimachinery v0.35.1
+ k8s.io/client-go v0.35.1
k8s.io/klog v1.0.0
k8s.io/klog/v2 v2.130.1
)
require (
- go.yaml.in/yaml/v3 v3.0.4 // indirect
- golang.org/x/tools/godoc v0.1.0-deprecated // indirect
+ github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 // indirect
+ github.com/bahlo/generic-list-go v0.2.0 // indirect
+ github.com/basgys/goxml2json v1.1.1-0.20231018121955-e66ee54ceaad // indirect
+ github.com/buger/jsonparser v1.1.1 // indirect
+ github.com/go-openapi/swag/cmdutils v0.25.4 // indirect
+ github.com/go-openapi/swag/conv v0.25.4 // indirect
+ github.com/go-openapi/swag/fileutils v0.25.4 // indirect
+ github.com/go-openapi/swag/jsonname v0.25.4 // indirect
+ github.com/go-openapi/swag/jsonutils v0.25.4 // indirect
+ github.com/go-openapi/swag/loading v0.25.4 // indirect
+ github.com/go-openapi/swag/mangling v0.25.4 // indirect
+ github.com/go-openapi/swag/netutils v0.25.4 // indirect
+ github.com/go-openapi/swag/stringutils v0.25.4 // indirect
+ github.com/go-openapi/swag/typeutils v0.25.4 // indirect
+ github.com/go-openapi/swag/yamlutils v0.25.4 // indirect
+ github.com/pb33f/jsonpath v0.7.1 // indirect
+ github.com/pb33f/ordered-map/v2 v2.3.0 // indirect
+ github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
+ github.com/sirupsen/logrus v1.9.4 // indirect
+ go.opentelemetry.io/collector/internal/componentalias v0.145.0 // indirect
+ go.uber.org/multierr v1.11.0 // indirect
+ gopkg.in/yaml.v2 v2.4.0 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
)
require (
- cloud.google.com/go/auth v0.17.0 // indirect
+ cloud.google.com/go/auth v0.18.1 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
- github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0 // indirect
- github.com/Microsoft/go-winio v0.6.1 // indirect
+ github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect
+ github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/armon/go-metrics v0.4.1 // indirect
- github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
- github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13
- github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 // indirect
- github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13 // indirect
+ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17
+ github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
- github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 // indirect
- github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 // indirect
- github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 // indirect
- github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 // indirect
+ github.com/aws/aws-sdk-go-v2/service/sso v1.30.10 // indirect
+ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
- github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect
+ github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/coreos/go-systemd/v22 v22.6.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
- github.com/distribution/reference v0.5.0 // indirect
- github.com/docker/go-connections v0.4.0 // indirect
+ github.com/distribution/reference v0.6.0 // indirect
+ github.com/docker/go-connections v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
github.com/fatih/color v1.16.0 // indirect
@@ -141,25 +166,25 @@ require (
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
- github.com/go-openapi/analysis v0.23.0 // indirect
- github.com/go-openapi/errors v0.22.3 // indirect
- github.com/go-openapi/jsonpointer v0.21.0 // indirect
- github.com/go-openapi/jsonreference v0.21.0 // indirect
- github.com/go-openapi/loads v0.22.0 // indirect
- github.com/go-openapi/spec v0.21.0 // indirect
- github.com/go-openapi/swag v0.23.0 // indirect
- github.com/go-openapi/validate v0.24.0 // indirect
- github.com/go-resty/resty/v2 v2.16.5 // indirect
- github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
+ github.com/go-openapi/analysis v0.24.2 // indirect
+ github.com/go-openapi/errors v0.22.6 // indirect
+ github.com/go-openapi/jsonpointer v0.22.4 // indirect
+ github.com/go-openapi/jsonreference v0.21.4 // indirect
+ github.com/go-openapi/loads v0.23.2 // indirect
+ github.com/go-openapi/spec v0.22.3 // indirect
+ github.com/go-openapi/swag v0.25.4 // indirect
+ github.com/go-openapi/validate v0.25.1 // indirect
+ github.com/go-resty/resty/v2 v2.17.1 // indirect
+ github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/gobwas/glob v0.2.3 // indirect
- github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
+ github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
github.com/google/gnostic-models v0.7.0 // indirect
- github.com/google/go-querystring v1.1.0 // indirect
+ github.com/google/go-querystring v1.2.0 // indirect
github.com/google/s2a-go v0.1.9 // indirect
- github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
- github.com/googleapis/gax-go/v2 v2.15.0 // indirect
+ github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect
+ github.com/googleapis/gax-go/v2 v2.17.0 // indirect
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
- github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
+ github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect
github.com/hashicorp/cronexpr v1.1.3 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
@@ -168,36 +193,33 @@ require (
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
- github.com/hashicorp/go-version v1.7.0 // indirect
+ github.com/hashicorp/go-version v1.8.0 // indirect
github.com/hashicorp/golang-lru v0.6.0 // indirect
github.com/hashicorp/serf v0.10.1 // indirect
- github.com/josharian/intern v1.0.0 // indirect
github.com/jpillora/backoff v1.0.0 // indirect
github.com/julienschmidt/httprouter v1.3.0 // indirect
github.com/knadh/koanf/maps v0.1.2 // indirect
github.com/knadh/koanf/providers/confmap v1.0.0 // indirect
- github.com/knadh/koanf/v2 v2.3.0 // indirect
+ github.com/knadh/koanf/v2 v2.3.2 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
- github.com/mailru/easyjson v0.7.7 // indirect
- github.com/mattn/go-colorable v0.1.13 // indirect
+ github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mdlayher/socket v0.4.1 // indirect
github.com/mdlayher/vsock v1.2.1 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
- github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/sys/atomicwriter v0.1.0 // indirect
- github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
+ github.com/moby/term v0.5.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
- github.com/morikuni/aec v1.0.0 // indirect
+ github.com/morikuni/aec v1.1.0 // indirect
github.com/oklog/ulid v1.3.1 // indirect
- github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics v0.139.0 // indirect
- github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.139.0 // indirect
+ github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics v0.145.0 // indirect
+ github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.145.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
- github.com/opencontainers/image-spec v1.0.2 // indirect
+ github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect
@@ -206,34 +228,33 @@ require (
github.com/prometheus/otlptranslator v1.0.0
github.com/prometheus/procfs v0.16.1 // indirect
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
- github.com/spf13/pflag v1.0.6 // indirect
+ github.com/spf13/pflag v1.0.10 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
- go.mongodb.org/mongo-driver v1.17.4 // indirect
- go.opentelemetry.io/auto/sdk v1.1.0 // indirect
- go.opentelemetry.io/collector/confmap v1.45.0 // indirect
- go.opentelemetry.io/collector/confmap/xconfmap v0.139.0 // indirect
- go.opentelemetry.io/collector/featuregate v1.45.0 // indirect
- go.opentelemetry.io/collector/pipeline v1.45.0 // indirect
- go.opentelemetry.io/proto/otlp v1.7.1 // indirect
- go.uber.org/zap v1.27.0 // indirect
- golang.org/x/crypto v0.43.0 // indirect
- golang.org/x/exp v0.0.0-20250808145144-a408d31f581a // indirect
- golang.org/x/mod v0.28.0 // indirect
- golang.org/x/net v0.46.0 // indirect
- golang.org/x/term v0.36.0 // indirect
- golang.org/x/time v0.13.0 // indirect
- golang.org/x/tools v0.37.0 // indirect
- google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect
- gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
+ go.mongodb.org/mongo-driver v1.17.9 // indirect
+ go.opentelemetry.io/auto/sdk v1.2.1 // indirect
+ go.opentelemetry.io/collector/confmap v1.51.0 // indirect
+ go.opentelemetry.io/collector/confmap/xconfmap v0.145.0 // indirect
+ go.opentelemetry.io/collector/featuregate v1.51.0 // indirect
+ go.opentelemetry.io/collector/pipeline v1.51.0 // indirect
+ go.opentelemetry.io/proto/otlp v1.9.0 // indirect
+ go.uber.org/zap v1.27.1 // indirect
+ golang.org/x/crypto v0.47.0 // indirect
+ golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect
+ golang.org/x/mod v0.32.0 // indirect
+ golang.org/x/net v0.49.0 // indirect
+ golang.org/x/term v0.39.0 // indirect
+ golang.org/x/time v0.14.0 // indirect
+ golang.org/x/tools v0.41.0 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
+ gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
- gopkg.in/ini.v1 v1.67.0 // indirect
- gopkg.in/yaml.v2 v2.4.0 // indirect
- gotest.tools/v3 v3.0.3 // indirect
- k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect
- k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect
- sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
+ gopkg.in/ini.v1 v1.67.1 // indirect
+ gotest.tools/v3 v3.5.1 // indirect
+ k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect
+ k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
+ sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
)
@@ -246,3 +267,5 @@ exclude (
github.com/grpc-ecosystem/grpc-gateway v1.14.7
google.golang.org/api v0.30.0
)
+
+replace cloud.google.com/go => cloud.google.com/go v0.123.0
diff --git a/go.sum b/go.sum
index 64bc639857..e288929bea 100644
--- a/go.sum
+++ b/go.sum
@@ -1,13 +1,13 @@
-cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4=
-cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ=
+cloud.google.com/go/auth v0.18.1 h1:IwTEx92GFUo2pJ6Qea0EU3zYvKnTAeRCODxfA/G5UWs=
+cloud.google.com/go/auth v0.18.1/go.mod h1:GfTYoS9G3CWpRA3Va9doKN9mjPGRS+v41jmZAhBzbrA=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
-github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 h1:5YTBM8QDVIBN3sxBil89WfdAAqDZbyJTgh688DSxX5w=
-github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw=
-github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.12.0 h1:wL5IEG5zb7BVv1Kv0Xm92orq+5hB5Nipn3B5tn4Rqfk=
-github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.12.0/go.mod h1:J7MUC/wtRpfGVbQ5sIItY5/FuVWmvzlY21WAOfQnq/I=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0=
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=
@@ -20,19 +20,21 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v4 v4.3
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v4 v4.3.0/go.mod h1:Y/HgrePTmGy9HjdSGTqZNa+apUpTVIEVKXJyARP2lrk=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.1.1 h1:7CBQ+Ei8SP2c6ydQTGCCrS35bDxgTMfoP2miAwK++OU=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.1.1/go.mod h1:c/wcGeGx5FUPbM/JltUYHZcKmigwyVLJlDq+4HdtXaw=
-github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
-github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
+github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
+github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
-github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0 h1:XkkQbfMyuH2jTSjQjSoihryI8GINRcs4xp8lNawg0FI=
-github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
+github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs=
+github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
github.com/Code-Hex/go-generics-cache v1.5.1 h1:6vhZGc5M7Y/YD8cIUcY8kcuQLB4cHR7U+0KMqAA0KcU=
github.com/Code-Hex/go-generics-cache v1.5.1/go.mod h1:qxcC9kRVrct9rHeiYpFWSoW1vxyillCVzX13KZG8dl4=
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
-github.com/KimMachineGun/automemlimit v0.7.4 h1:UY7QYOIfrr3wjjOAqahFmC3IaQCLWvur9nmfIn6LnWk=
-github.com/KimMachineGun/automemlimit v0.7.4/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM=
-github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
-github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
+github.com/KimMachineGun/automemlimit v0.7.5 h1:RkbaC0MwhjL1ZuBKunGDjE/ggwAX43DwZrJqVwyveTk=
+github.com/KimMachineGun/automemlimit v0.7.5/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM=
+github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
+github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
+github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
+github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY=
github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
@@ -47,38 +49,48 @@ github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJ
github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
-github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
-github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
-github.com/aws/aws-sdk-go-v2 v1.39.6 h1:2JrPCVgWJm7bm83BDwY5z8ietmeJUbh3O2ACnn+Xsqk=
-github.com/aws/aws-sdk-go-v2 v1.39.6/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE=
-github.com/aws/aws-sdk-go-v2/config v1.31.17 h1:QFl8lL6RgakNK86vusim14P2k8BFSxjvUkcWLDjgz9Y=
-github.com/aws/aws-sdk-go-v2/config v1.31.17/go.mod h1:V8P7ILjp/Uef/aX8TjGk6OHZN6IKPM5YW6S78QnRD5c=
-github.com/aws/aws-sdk-go-v2/credentials v1.18.21 h1:56HGpsgnmD+2/KpG0ikvvR8+3v3COCwaF4r+oWwOeNA=
-github.com/aws/aws-sdk-go-v2/credentials v1.18.21/go.mod h1:3YELwedmQbw7cXNaII2Wywd+YY58AmLPwX4LzARgmmA=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 h1:T1brd5dR3/fzNFAQch/iBKeX07/ffu/cLu+q+RuzEWk=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13/go.mod h1:Peg/GBAQ6JDt+RoBf4meB1wylmAipb7Kg2ZFakZTlwk=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 h1:a+8/MLcWlIxo1lF9xaGt3J/u3yOZx+CdSveSNwjhD40=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13/go.mod h1:oGnKwIYZ4XttyU2JWxFrwvhF6YKiK/9/wmE3v3Iu9K8=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13 h1:HBSI2kDkMdWz4ZM7FjwE7e/pWDEZ+nR95x8Ztet1ooY=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13/go.mod h1:YE94ZoDArI7awZqJzBAZ3PDD2zSfuP7w6P2knOzIn8M=
+github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU=
+github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
+github.com/aws/aws-sdk-go-v2/config v1.32.9 h1:ktda/mtAydeObvJXlHzyGpK1xcsLaP16zfUPDGoW90A=
+github.com/aws/aws-sdk-go-v2/config v1.32.9/go.mod h1:U+fCQ+9QKsLW786BCfEjYRj34VVTbPdsLP3CHSYXMOI=
+github.com/aws/aws-sdk-go-v2/credentials v1.19.9 h1:sWvTKsyrMlJGEuj/WgrwilpoJ6Xa1+KhIpGdzw7mMU8=
+github.com/aws/aws-sdk-go-v2/credentials v1.19.9/go.mod h1:+J44MBhmfVY/lETFiKI+klz0Vym2aCmIjqgClMmW82w=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 h1:I0GyV8wiYrP8XpA70g1HBcQO1JlQxCMTW9npl5UbDHY=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17/go.mod h1:tyw7BOl5bBe/oqvoIeECFJjMdzXoa/dfVz3QQ5lgHGA=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17/go.mod h1:5M5CI3D12dNOtH3/mk6minaRwI2/37ifCURZISxA/IQ=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 h1:WWLqlh79iO48yLkj1v3ISRNiv+3KdQoZ6JWyfcsyQik=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17/go.mod h1:EhG22vHRrvF8oXSTYStZhJc1aUgKtnJe+aOiFEV90cM=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
-github.com/aws/aws-sdk-go-v2/service/ec2 v1.262.0 h1:5qBb1XV/D18qtCHd3bmmxoVglI+fZ4QWuS/EB8kIXYQ=
-github.com/aws/aws-sdk-go-v2/service/ec2 v1.262.0/go.mod h1:NDdDLLW5PtLLXN661gKcvJvqAH5OBXsfhMlmKVu1/pY=
-github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 h1:x2Ibm/Af8Fi+BH+Hsn9TXGdT+hKbDd5XOTZxTMxDk7o=
-github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3/go.mod h1:IW1jwyrQgMdhisceG8fQLmQIydcT/jWY21rFhzgaKwo=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 h1:kDqdFvMY4AtKoACfzIGD8A0+hbT41KTKF//gq7jITfM=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13/go.mod h1:lmKuogqSU3HzQCwZ9ZtcqOc5XGMqtDK7OIc2+DxiUEg=
-github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.4 h1:/1o2AYwHJojUDeMvQNyJiKZwcWCc3e4kQuTXqRLuThc=
-github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.4/go.mod h1:Nn2xx6HojGuNMtUFxxz/nyNLSS+tHMRsMhe3+W3wB5k=
-github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 h1:0JPwLz1J+5lEOfy/g0SURC9cxhbQ1lIMHMa+AHZSzz0=
-github.com/aws/aws-sdk-go-v2/service/sso v1.30.1/go.mod h1:fKvyjJcz63iL/ftA6RaM8sRCtN4r4zl4tjL3qw5ec7k=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 h1:OWs0/j2UYR5LOGi88sD5/lhN6TDLG6SfA7CqsQO9zF0=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5/go.mod h1:klO+ejMvYsB4QATfEOIXk8WAEwN4N0aBfJpvC+5SZBo=
-github.com/aws/aws-sdk-go-v2/service/sts v1.39.1 h1:mLlUgHn02ue8whiR4BmxxGJLR2gwU6s6ZzJ5wDamBUs=
-github.com/aws/aws-sdk-go-v2/service/sts v1.39.1/go.mod h1:E19xDjpzPZC7LS2knI9E6BaRFDK43Eul7vd6rSq2HWk=
-github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM=
-github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
+github.com/aws/aws-sdk-go-v2/service/ec2 v1.290.0 h1:Ub4CvLWf8wEQ7/pEiqXM9tTsHXf2BokPLwbqEvrmAq0=
+github.com/aws/aws-sdk-go-v2/service/ec2 v1.290.0/go.mod h1:Uy+C+Sc58jozdoL1McQr8bDsEvNFx+/nBY+vpO1HVUY=
+github.com/aws/aws-sdk-go-v2/service/ecs v1.72.0 h1:hggRKpv26DpYMOik3wWo1Ty5MkANoXhNobjfWpC3G4M=
+github.com/aws/aws-sdk-go-v2/service/ecs v1.72.0/go.mod h1:pMlGFDpHoLTJOIZHGdJOAWmi+xeIlQXuFTuQxs1epYE=
+github.com/aws/aws-sdk-go-v2/service/elasticache v1.51.9 h1:hTgZLyNoDWphZUtTtcvQh0LP6TZO0mtdSfZK/GObDLk=
+github.com/aws/aws-sdk-go-v2/service/elasticache v1.51.9/go.mod h1:91RkIYy9ubykxB50XGYDsbljLZnrZ6rp/Urt4rZrbwQ=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 h1:RuNSMoozM8oXlgLG/n6WLaFGoea7/CddrCfIiSA+xdY=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17/go.mod h1:F2xxQ9TZz5gDWsclCtPQscGpP0VUOc8RqgFM3vDENmU=
+github.com/aws/aws-sdk-go-v2/service/kafka v1.48.0 h1:CKRWqysU9INeoi0nTI9gDzDAJk+GatzFduVYxT/wkrw=
+github.com/aws/aws-sdk-go-v2/service/kafka v1.48.0/go.mod h1:tWnHS64fg5ydLHivFlCAtEh/1iMNzr56QsH3F+UTwD4=
+github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.11 h1:VM5e5M39zRSs+aT0O9SoxHjUXqXxhbw3Yi0FdMQWPIc=
+github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.11/go.mod h1:0jvzYPIQGCpnY/dmdaotTk2JH4QuBlnW0oeyrcGLWJ4=
+github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 h1:VrhDvQib/i0lxvr3zqlUwLwJP4fpmpyD9wYG1vfSu+Y=
+github.com/aws/aws-sdk-go-v2/service/signin v1.0.5/go.mod h1:k029+U8SY30/3/ras4G/Fnv/b88N4mAfliNn08Dem4M=
+github.com/aws/aws-sdk-go-v2/service/sso v1.30.10 h1:+VTRawC4iVY58pS/lzpo0lnoa/SYNGF4/B/3/U5ro8Y=
+github.com/aws/aws-sdk-go-v2/service/sso v1.30.10/go.mod h1:yifAsgBxgJWn3ggx70A3urX2AN49Y5sJTD1UQFlfqBw=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14 h1:0jbJeuEHlwKJ9PfXtpSFc4MF+WIWORdhN1n30ITZGFM=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14/go.mod h1:sTGThjphYE4Ohw8vJiRStAcu3rbjtXRsdNB0TvZ5wwo=
+github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/pJ1jOWYlFDJTjRQ=
+github.com/aws/aws-sdk-go-v2/service/sts v1.41.6/go.mod h1:qgFDZQSD/Kys7nJnVqYlWKnh0SSdMjAi0uSwON4wgYQ=
+github.com/aws/smithy-go v1.24.1 h1:VbyeNfmYkWoxMVpGUAbQumkODcYmfMRfZ8yQiH30SK0=
+github.com/aws/smithy-go v1.24.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
+github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
+github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
+github.com/basgys/goxml2json v1.1.1-0.20231018121955-e66ee54ceaad h1:3swAvbzgfaI6nKuDDU7BiKfZRdF+h2ZwKgMHd8Ha4t8=
+github.com/basgys/goxml2json v1.1.1-0.20231018121955-e66ee54ceaad/go.mod h1:9+nBLYNWkvPcq9ep0owWUsPTLgL9ZXTsZWcCSVGGLJ0=
github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 h1:6df1vn4bBlDDo4tARvBm7l6KA9iVMnE3NWizDeWSrps=
github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3/go.mod h1:CIWtjkly68+yqLPbvwwR/fjNJA/idrtULjZWh2v1ys0=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
@@ -86,15 +98,25 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
+github.com/bitly/go-simplejson v0.5.1 h1:xgwPbetQScXt1gh9BmoJ6j9JMr3TElvuIyjR8pgdoow=
+github.com/bitly/go-simplejson v0.5.1/go.mod h1:YOPVLzCfwK14b4Sff3oP1AmGhI9T9Vsg84etUnlyp+Q=
+github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
+github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
+github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs=
+github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
+github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
+github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
+github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
-github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls=
-github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
+github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w=
+github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
@@ -103,33 +125,32 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo=
github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU=
-github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dennwc/varint v1.0.0 h1:kGNFFSSw8ToIy3obO/kKr8U9GZYUAxQEVuix4zfDWzE=
github.com/dennwc/varint v1.0.0/go.mod h1:hnItb35rvZvJrbTALZtY/iQfDs48JKRG1RPpgziApxA=
-github.com/digitalocean/godo v1.168.0 h1:mlORtUcPD91LQeJoznrH3XvfvgK3t8Wvrpph9giUT/Q=
-github.com/digitalocean/godo v1.168.0/go.mod h1:xQsWpVCCbkDrWisHA72hPzPlnC+4W5w/McZY5ij9uvU=
-github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0=
-github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
-github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
-github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
+github.com/digitalocean/godo v1.175.0 h1:tpfwJFkBzpePxvvFazOn69TXctdxuFlOs7DMVXsI7oU=
+github.com/digitalocean/godo v1.175.0/go.mod h1:xQsWpVCCbkDrWisHA72hPzPlnC+4W5w/McZY5ij9uvU=
+github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
+github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
+github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
+github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
-github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
-github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
+github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
+github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/edsrzf/mmap-go v1.2.0 h1:hXLYlkbaPzt1SaQk+anYwKSRNhufIDCchSPkUD6dD84=
github.com/edsrzf/mmap-go v1.2.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q=
github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
-github.com/envoyproxy/go-control-plane/envoy v1.35.0 h1:ixjkELDE+ru6idPxcHLj8LBVc2bFP7iBytj353BoHUo=
-github.com/envoyproxy/go-control-plane/envoy v1.35.0/go.mod h1:09qwbGVuSWWAyN5t/b3iyVfz5+z8QWGrzkoqm/8SbEs=
-github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
-github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
+github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ=
+github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A=
+github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds=
+github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0=
github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb h1:IT4JYU7k4ikYg1SCxNI1/Tieq/NFvh6dzLdgi7eu0tM=
github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb/go.mod h1:bH6Xx7IW64qjjJq8M2u4dxNaBiDfKK+z/3eGDpXEQhc=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
@@ -137,6 +158,8 @@ github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
+github.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY=
+github.com/felixge/fgprof v0.9.5/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
@@ -153,40 +176,71 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
-github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU=
-github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo=
-github.com/go-openapi/errors v0.22.3 h1:k6Hxa5Jg1TUyZnOwV2Lh81j8ayNw5VVYLvKrp4zFKFs=
-github.com/go-openapi/errors v0.22.3/go.mod h1:+WvbaBBULWCOna//9B9TbLNGSFOfF8lY9dw4hGiEiKQ=
-github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
-github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
-github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
-github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
-github.com/go-openapi/loads v0.22.0 h1:ECPGd4jX1U6NApCGG1We+uEozOAvXvJSF4nnwHZ8Aco=
-github.com/go-openapi/loads v0.22.0/go.mod h1:yLsaTCS92mnSAZX5WWoxszLj0u+Ojl+Zs5Stn1oF+rs=
-github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
-github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
-github.com/go-openapi/strfmt v0.24.0 h1:dDsopqbI3wrrlIzeXRbqMihRNnjzGC+ez4NQaAAJLuc=
-github.com/go-openapi/strfmt v0.24.0/go.mod h1:Lnn1Bk9rZjXxU9VMADbEEOo7D7CDyKGLsSKekhFr7s4=
-github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
-github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
-github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58=
-github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ=
-github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
-github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
+github.com/go-openapi/analysis v0.24.2 h1:6p7WXEuKy1llDgOH8FooVeO+Uq2za9qoAOq4ZN08B50=
+github.com/go-openapi/analysis v0.24.2/go.mod h1:x27OOHKANE0lutg2ml4kzYLoHGMKgRm1Cj2ijVOjJuE=
+github.com/go-openapi/errors v0.22.6 h1:eDxcf89O8odEnohIXwEjY1IB4ph5vmbUsBMsFNwXWPo=
+github.com/go-openapi/errors v0.22.6/go.mod h1:z9S8ASTUqx7+CP1Q8dD8ewGH/1JWFFLX/2PmAYNQLgk=
+github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4=
+github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80=
+github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8=
+github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4=
+github.com/go-openapi/loads v0.23.2 h1:rJXAcP7g1+lWyBHC7iTY+WAF0rprtM+pm8Jxv1uQJp4=
+github.com/go-openapi/loads v0.23.2/go.mod h1:IEVw1GfRt/P2Pplkelxzj9BYFajiWOtY2nHZNj4UnWY=
+github.com/go-openapi/spec v0.22.3 h1:qRSmj6Smz2rEBxMnLRBMeBWxbbOvuOoElvSvObIgwQc=
+github.com/go-openapi/spec v0.22.3/go.mod h1:iIImLODL2loCh3Vnox8TY2YWYJZjMAKYyLH2Mu8lOZs=
+github.com/go-openapi/strfmt v0.25.0 h1:7R0RX7mbKLa9EYCTHRcCuIPcaqlyQiWNPTXwClK0saQ=
+github.com/go-openapi/strfmt v0.25.0/go.mod h1:nNXct7OzbwrMY9+5tLX4I21pzcmE6ccMGXl3jFdPfn8=
+github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU=
+github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ=
+github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4=
+github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0=
+github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4=
+github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU=
+github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y=
+github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk=
+github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI=
+github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag=
+github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA=
+github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY=
+github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo=
+github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM=
+github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s=
+github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE=
+github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48=
+github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg=
+github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0=
+github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg=
+github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8=
+github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0=
+github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw=
+github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE=
+github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw=
+github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc=
+github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4=
+github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg=
+github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
+github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
+github.com/go-openapi/validate v0.25.1 h1:sSACUI6Jcnbo5IWqbYHgjibrhhmt3vR6lCzKZnmAgBw=
+github.com/go-openapi/validate v0.25.1/go.mod h1:RMVyVFYte0gbSTaZ0N4KmTn6u/kClvAFp+mAVfS/DQc=
+github.com/go-resty/resty/v2 v2.17.1 h1:x3aMpHK1YM9e4va/TMDRlusDDoZiQ+ViDu/WpA6xTM4=
+github.com/go-resty/resty/v2 v2.17.1/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
-github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
-github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
+github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
+github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/go-zookeeper/zk v1.0.4 h1:DPzxraQx7OrPyXq2phlGlNSIyWEsAox0RJmjTseMV6I=
github.com/go-zookeeper/zk v1.0.4/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
+github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
+github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
+github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
-github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
-github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
+github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
+github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -199,37 +253,37 @@ github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
-github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
-github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
-github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
+github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
+github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
-github.com/google/pprof v0.0.0-20250923004556-9e5a51aed1e8 h1:ZI8gCoCjGzPsum4L21jHdQs8shFBIQih1TM9Rd/c+EQ=
-github.com/google/pprof v0.0.0-20250923004556-9e5a51aed1e8/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U=
+github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
+github.com/google/pprof v0.0.0-20260202012954-cb029daf43ef h1:xpF9fUHpoIrrjX24DURVKiwHcFpw19ndIs+FwTSMbno=
+github.com/google/pprof v0.0.0-20260202012954-cb029daf43ef/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=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
-github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
-github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
-github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
-github.com/gophercloud/gophercloud/v2 v2.8.0 h1:of2+8tT6+FbEYHfYC8GBu8TXJNsXYSNm9KuvpX7Neqo=
-github.com/gophercloud/gophercloud/v2 v2.8.0/go.mod h1:Ki/ILhYZr/5EPebrPL9Ej+tUg4lqx71/YH2JWVeU+Qk=
+github.com/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao=
+github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8=
+github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc=
+github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY=
+github.com/gophercloud/gophercloud/v2 v2.10.0 h1:NRadC0aHNvy4iMoFXj5AFiPmut/Sj3hAPAo9B59VMGc=
+github.com/gophercloud/gophercloud/v2 v2.10.0/go.mod h1:Ki/ILhYZr/5EPebrPL9Ej+tUg4lqx71/YH2JWVeU+Qk=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 h1:cLN4IBkmkYZNnk7EAJ0BHIethd+J6LqxFNw5mSiI2bM=
github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
-github.com/hashicorp/consul/api v1.32.0 h1:5wp5u780Gri7c4OedGEPzmlUEzi0g2KyiPphSr6zjVg=
-github.com/hashicorp/consul/api v1.32.0/go.mod h1:Z8YgY0eVPukT/17ejW+l+C7zJmKwgPHtjU1q16v/Y40=
-github.com/hashicorp/consul/sdk v0.16.1 h1:V8TxTnImoPD5cj0U9Spl0TUxcytjcbbJeADFF07KdHg=
-github.com/hashicorp/consul/sdk v0.16.1/go.mod h1:fSXvwxB2hmh1FMZCNl6PwX0Q/1wdWtHJcZ7Ea5tns0s=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII=
+github.com/hashicorp/consul/api v1.33.2 h1:Q6mE0WZsUTJerlnl9TuXzqrtZ0cKdOCsxcZhj5mKbMs=
+github.com/hashicorp/consul/api v1.33.2/go.mod h1:K3yoL/vnIBcQV/25NeMZVokRvPPERiqp2Udtr4xAfhs=
+github.com/hashicorp/consul/sdk v0.17.1 h1:LumAh8larSXmXw2wvw/lK5ZALkJ2wK8VRwWMLVV5M5c=
+github.com/hashicorp/consul/sdk v0.17.1/go.mod h1:EngiixMhmw9T7wApycq6rDRFXXVUwjjf7HuLiGMH/Sw=
github.com/hashicorp/cronexpr v1.1.3 h1:rl5IkxXN2m681EfivTlccqIryzYJSXRGRNa0xeG7NA4=
github.com/hashicorp/cronexpr v1.1.3/go.mod h1:P4wA0KBl9C5q2hABiMO7cp6jcIg96CDh1Efb3g1PWA4=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -243,11 +297,13 @@ github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVH
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc=
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
+github.com/hashicorp/go-metrics v0.5.4 h1:8mmPiIJkTPPEbAiV97IxdAGNdRdaWwVap1BU6elejKY=
+github.com/hashicorp/go-metrics v0.5.4/go.mod h1:CG5yz4NZ/AI/aQt9Ucm/vdBnbh7fvmv4lxZ350i+QQI=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-msgpack v0.5.5 h1:i9R9JSrqIz0QVLz3sz+i3YJdT7TTSLcfLLzJi9aZTuI=
github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
-github.com/hashicorp/go-msgpack/v2 v2.1.1 h1:xQEY9yB2wnHitoSzk/B9UjXWRQ67QKu5AOm8aFp8N3I=
-github.com/hashicorp/go-msgpack/v2 v2.1.1/go.mod h1:upybraOAblm4S7rx0+jeNy+CWWhzywQsSRV5033mMu4=
+github.com/hashicorp/go-msgpack/v2 v2.1.5 h1:Ue879bPnutj/hXfmUk6s/jtIK90XxgiUIcXRl656T44=
+github.com/hashicorp/go-msgpack/v2 v2.1.5/go.mod h1:bjCsRXpZ7NsJdk45PoCQnzRGDaK8TKm5ZnDI/9y3J4M=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
@@ -265,27 +321,27 @@ github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/b
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
-github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
-github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
+github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4=
+github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.6.0 h1:uL2shRDx7RTrOrTCUZEGP/wJUFiUI8QT6E7z5o8jga4=
github.com/hashicorp/golang-lru v0.6.0/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
github.com/hashicorp/memberlist v0.5.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0=
-github.com/hashicorp/memberlist v0.5.1 h1:mk5dRuzeDNis2bi6LLoQIXfMH7JQvAzt3mQD0vNZZUo=
-github.com/hashicorp/memberlist v0.5.1/go.mod h1:zGDXV6AqbDTKTM6yxW0I4+JtFzZAJVoIPvss4hV8F24=
-github.com/hashicorp/nomad/api v0.0.0-20250930071859-eaa0fe0e27af h1:ScAYf8O+9xTqTJPZH8MIlUfO+ak8cb31rW1aYJgS+jE=
-github.com/hashicorp/nomad/api v0.0.0-20250930071859-eaa0fe0e27af/go.mod h1:sldFTIgs+FsUeKU3LwVjviAIuksxD8TzDOn02MYwslE=
+github.com/hashicorp/memberlist v0.5.4 h1:40YY+3qq2tAUhZIMEK8kqusKZBBjdwJ3NUjvYkcxh74=
+github.com/hashicorp/memberlist v0.5.4/go.mod h1:OgN6xiIo6RlHUWk+ALjP9e32xWCoQrsOCmHrWCm2MWA=
+github.com/hashicorp/nomad/api v0.0.0-20260220212019-daca79db0bd6 h1:QN/GwpGyiW8RdNcHGMA1xVnM8tJkAGNDR/BZ47XR+OU=
+github.com/hashicorp/nomad/api v0.0.0-20260220212019-daca79db0bd6/go.mod h1:KkLNLU0Nyfh5jWsFoF/PsmMbKpRIAoIV4lmQoJWgKCk=
github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY=
github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4=
-github.com/hetznercloud/hcloud-go/v2 v2.29.0 h1:LzNFw5XLBfftyu3WM1sdSLjOZBlWORtz2hgGydHaYV8=
-github.com/hetznercloud/hcloud-go/v2 v2.29.0/go.mod h1:XBU4+EDH2KVqu2KU7Ws0+ciZcX4ygukQl/J0L5GS8P8=
-github.com/ionos-cloud/sdk-go/v6 v6.3.4 h1:jTvGl4LOF8v8OYoEIBNVwbFoqSGAFqn6vGE7sp7/BqQ=
-github.com/ionos-cloud/sdk-go/v6 v6.3.4/go.mod h1:wCVwNJ/21W29FWFUv+fNawOTMlFoP1dS3L+ZuztFW48=
+github.com/hetznercloud/hcloud-go/v2 v2.36.0 h1:HlLL/aaVXUulqe+rsjoJmrxKhPi1MflL5O9iq5QEtvo=
+github.com/hetznercloud/hcloud-go/v2 v2.36.0/go.mod h1:MnN/QJEa/RYNQiiVoJjNHPntM7Z1wlYPgJ2HA40/cDE=
+github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
+github.com/ionos-cloud/sdk-go/v6 v6.3.6 h1:l/TtKgdQ1wUH3DDe2SfFD78AW+TJWdEbDpQhHkWd6CM=
+github.com/ionos-cloud/sdk-go/v6 v6.3.6/go.mod h1:nUGHP4kZHAZngCVr4v6C8nuargFrtvt7GrzH/hqn7c4=
github.com/jarcoal/httpmock v1.4.1 h1:0Ju+VCFuARfFlhVXFc2HxlcQkfB+Xq12/EotHko+x2A=
github.com/jarcoal/httpmock v1.4.1/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0=
-github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
@@ -300,14 +356,14 @@ github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRt
github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
-github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
-github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
+github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
+github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo=
github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
github.com/knadh/koanf/providers/confmap v1.0.0 h1:mHKLJTE7iXEys6deO5p6olAiZdG5zwp8Aebir+/EaRE=
github.com/knadh/koanf/providers/confmap v1.0.0/go.mod h1:txHYHiI2hAtF0/0sCmcuol4IDcuQbKTybiB1nOcUo1A=
-github.com/knadh/koanf/v2 v2.3.0 h1:Qg076dDRFHvqnKG97ZEsi9TAg2/nFTa9hCdcSa1lvlM=
-github.com/knadh/koanf/v2 v2.3.0/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28=
+github.com/knadh/koanf/v2 v2.3.2 h1:Ee6tuzQYFwcZXQpc2MiVeC6qHMandf5SMUJJNoFp/c4=
+github.com/knadh/koanf/v2 v2.3.2/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28=
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b h1:udzkj9S/zlT5X367kqJis0QP7YMxobob6zhzq6Yre00=
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b/go.mod h1:pcaDhQK0/NJZEvtCO0qQPPropqV0sJOJ6YW7X+9kRwM=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -321,23 +377,22 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
-github.com/linode/linodego v1.60.0 h1:SgsebJFRCi+lSmYy+C40wmKZeJllGGm+W12Qw4+yVdI=
-github.com/linode/linodego v1.60.0/go.mod h1:1+Bt0oTz5rBnDOJbGhccxn7LYVytXTIIfAy7QYmijDs=
-github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
+github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
+github.com/linode/linodego v1.65.0 h1:SdsuGD8VSsPWeShXpE7ihl5vec+fD3MgwhnfYC/rj7k=
+github.com/linode/linodego v1.65.0/go.mod h1:tOFiTErdjkbVnV+4S0+NmIE9dqqZUEM2HsJaGu8wMh8=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
-github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
-github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
+github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
+github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
-github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
@@ -349,16 +404,14 @@ github.com/mdlayher/vsock v1.2.1 h1:pC1mTJTvjo1r9n9fbm7S1j04rCgCzhCOS5DY0zqHlnQ=
github.com/mdlayher/vsock v1.2.1/go.mod h1:NRfCibel++DgeMD8z/hP+PPTjlNJsdPOmxcnENvE+SE=
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
-github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
-github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
+github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
+github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
-github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
-github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
@@ -367,8 +420,8 @@ github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
-github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc=
-github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw=
+github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
+github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -377,40 +430,49 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
-github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
-github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
+github.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ=
+github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
-github.com/nsf/jsondiff v0.0.0-20230430225905-43f6cf3098c1 h1:dOYG7LS/WK00RWZc8XGgcUTlTxpp3mKhdR2Q9z9HbXM=
-github.com/nsf/jsondiff v0.0.0-20230430225905-43f6cf3098c1/go.mod h1:mpRZBD8SJ55OIICQ3iWH0Yz3cjzA61JdqMLoWXeB2+8=
+github.com/nsf/jsondiff v0.0.0-20260207060731-8e8d90c4c0ac h1:4YV96Dzy2csSnhzl14/Qk5YsSrKAQusGsIADDn/4/g8=
+github.com/nsf/jsondiff v0.0.0-20260207060731-8e8d90c4c0ac/go.mod h1:mpRZBD8SJ55OIICQ3iWH0Yz3cjzA61JdqMLoWXeB2+8=
github.com/oklog/run v1.2.0 h1:O8x3yXwah4A73hJdlrwo/2X6J62gE5qTMusH0dvz60E=
github.com/oklog/run v1.2.0/go.mod h1:mgDbKRSwPhJfesJ4PntqFUbKQRZ50NgmZTSPlFA0YFk=
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s=
github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
-github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM=
-github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
-github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
-github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
-github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics v0.139.0 h1:D5aGQCErSCb4sKIHoZhgR4El6AzgviTRYlHUpbSFqDo=
-github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics v0.139.0/go.mod h1:ZjeRsA5oaVk89fg5D+iXStx2QncmhAvtGbdSumT07H4=
-github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.139.0 h1:6/j0Ta8ZJnmAFVEoC3aZ1Hs19RB4fHzlN6kOZhsBJqM=
-github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.139.0/go.mod h1:VfA8xHz4xg7Fyj5bBsCDbOO3iVYzDn9wP/QFsjcAE5c=
-github.com/open-telemetry/opentelemetry-collector-contrib/processor/deltatocumulativeprocessor v0.139.0 h1:iRNX/ueuad1psOVgnNkxuQmXxvF3ze5ZZCP66xKFk/w=
-github.com/open-telemetry/opentelemetry-collector-contrib/processor/deltatocumulativeprocessor v0.139.0/go.mod h1:bW09lo3WgHsPsZ1mgsJvby9wCefT5o13patM5phdfIU=
+github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns=
+github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
+github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
+github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
+github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics v0.145.0 h1:0dYiJ7krIwaHFX6YLNDo/yawTZIu8X16tT/nwW1UTG8=
+github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics v0.145.0/go.mod h1:mhoa9lipcEH0heeKf6+xHzGUrCuAgImQv4/Qpmu0+Fk=
+github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.145.0 h1:sB4yuYx45zig1ceQ+kmrEYy0xMZ+mGagwYIFtJkkU1w=
+github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.145.0/go.mod h1:uLhceuH7ZtiVxk+B0MHI0vhJG2Y4aOzT/hrV6c5KjVU=
+github.com/open-telemetry/opentelemetry-collector-contrib/processor/deltatocumulativeprocessor v0.145.0 h1:en86L47oOTsAkbDc5VEMF5cziXPBK2D4hqGRqLaJtCw=
+github.com/open-telemetry/opentelemetry-collector-contrib/processor/deltatocumulativeprocessor v0.145.0/go.mod h1:osDRUOIfd7IiKkDvcE/VrPp9FFOPJmFp73RuvgOn5gE=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
-github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM=
-github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
+github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
+github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
+github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
github.com/ovh/go-ovh v1.9.0 h1:6K8VoL3BYjVV3In9tPJUdT7qMx9h0GExN9EXx1r2kKE=
github.com/ovh/go-ovh v1.9.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
+github.com/pb33f/jsonpath v0.7.1 h1:dEp6oIZuJbpDSyuHAl9m7GonoDW4M20BcD5vT0tPYRE=
+github.com/pb33f/jsonpath v0.7.1/go.mod h1:zBV5LJW4OQOPatmQE2QdKpGQJvhDTlE5IEj6ASaRNTo=
+github.com/pb33f/libopenapi v0.33.4 h1:Rgczgrg4VQKXW/NtSj/nApmtYKS+TVpLgTsG692JxmE=
+github.com/pb33f/libopenapi v0.33.4/go.mod h1:e/dmd2Pf1nkjqkI0r7guFSyt9T5V0IIQKgs0L6B/3b0=
+github.com/pb33f/libopenapi-validator v0.11.1 h1:lTW738oB3lwpS9poDzmI3jpTPZSb5W46vklZqtyf7+Q=
+github.com/pb33f/libopenapi-validator v0.11.1/go.mod h1:7CfboslU/utKhiuQRuenriGYZ+HQLDOvARxjqRwd57w=
+github.com/pb33f/ordered-map/v2 v2.3.0 h1:k2OhVEQkhTCQMhAicQ3Z6iInzoZNQ7L9MVomwKBZ5WQ=
+github.com/pb33f/ordered-map/v2 v2.3.0/go.mod h1:oe5ue+6ZNhy7QN9cPZvPA23Hx0vMHnNVeMg4fGdCANw=
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
@@ -429,15 +491,15 @@ github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndr
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
-github.com/prometheus/alertmanager v0.28.1 h1:BK5pCoAtaKg01BYRUJhEDV1tqJMEtYBGzPw8QdvnnvA=
-github.com/prometheus/alertmanager v0.28.1/go.mod h1:0StpPUDDHi1VXeM7p2yYfeZgLVi/PPlt39vo9LQUHxM=
+github.com/prometheus/alertmanager v0.31.1 h1:eAmIC42lzbWslHkMt693T36qdxfyZULswiHr681YS3Q=
+github.com/prometheus/alertmanager v0.31.1/go.mod h1:zWPQwhbLt2ybee8rL921UONeQ59Oncash+m/hGP17tU=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
-github.com/prometheus/client_golang/exp v0.0.0-20250914183048-a974e0d45e0a h1:RF1vfKM34/3DbGNis22BGd6sDDY3XBi0eM7pYqmOEO0=
-github.com/prometheus/client_golang/exp v0.0.0-20250914183048-a974e0d45e0a/go.mod h1:FGJuwvfcPY0V5enm+w8zF1RNS062yugQtPPQp1c4Io4=
+github.com/prometheus/client_golang/exp v0.0.0-20260108101519-fb0838f53562 h1:vwqZvuobg82U0gcG2eVrFH27806bUbNr32SvfRbvdsg=
+github.com/prometheus/client_golang/exp v0.0.0-20260108101519-fb0838f53562/go.mod h1:PmAYDB13uBFBG9qE1qxZZgZWhg7Rg6SfKM5DMK7hjyI=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
@@ -445,12 +507,12 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
-github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8=
-github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko=
+github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
+github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/common/assets v0.2.0 h1:0P5OrzoHrYBOSM1OigWL3mY8ZvV2N4zIE/5AahrSrfM=
github.com/prometheus/common/assets v0.2.0/go.mod h1:D17UVUE12bHbim7HzwUvtqm6gwBEaDQ0F+hIGbFbccI=
-github.com/prometheus/exporter-toolkit v0.15.0 h1:Pcle5sSViwR1x0gdPd0wtYrPQENBieQAM7TmT0qtb2U=
-github.com/prometheus/exporter-toolkit v0.15.0/go.mod h1:OyRWd2iTo6Xge9Kedvv0IhCrJSBu36JCfJ2yVniRIYk=
+github.com/prometheus/exporter-toolkit v0.15.1 h1:XrGGr/qWl8Gd+pqJqTkNLww9eG8vR/CoRk0FubOKfLE=
+github.com/prometheus/exporter-toolkit v0.15.1/go.mod h1:P/NR9qFRGbCFgpklyhix9F6v6fFr/VQB/CVsrMDGKo4=
github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos=
github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
@@ -458,15 +520,17 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
-github.com/prometheus/sigv4 v0.2.1 h1:hl8D3+QEzU9rRmbKIRwMKRwaFGyLkbPdH5ZerglRHY0=
-github.com/prometheus/sigv4 v0.2.1/go.mod h1:ySk6TahIlsR2sxADuHy4IBFhwEjRGGsfbbLGhFYFj6Q=
+github.com/prometheus/sigv4 v0.4.1 h1:EIc3j+8NBea9u1iV6O5ZAN8uvPq2xOIUPcqCTivHuXs=
+github.com/prometheus/sigv4 v0.4.1/go.mod h1:eu+ZbRvsc5TPiHwqh77OWuCnWK73IdkETYY46P4dXOU=
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
-github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
-github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
+github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
+github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
-github.com/scaleway/scaleway-sdk-go v1.0.0-beta.35 h1:8xfn1RzeI9yoCUuEwDy08F+No6PcKZGEDOQ6hrRyLts=
-github.com/scaleway/scaleway-sdk-go v1.0.0-beta.35/go.mod h1:47B1d/YXmSAxlJxUJxClzHR6b3T4M1WyCvwENPQNBWc=
+github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
+github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
+github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 h1:ObX9hZmK+VmijreZO/8x9pQ8/P/ToHD/bdSb4Eg4tUo=
+github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36/go.mod h1:LEsDu4BubxK7/cWhtlQWfuxwL4rf/2UEpxXz1o1EMtM=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shoenig/test v1.12.2 h1:ZVT8NeIUwGWpZcKaepPmFMoNQ3sVpxvqUh/MAqwFiJI=
@@ -475,13 +539,12 @@ github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c h1:aqg5Vm5dwtvL+Yg
github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c/go.mod h1:owqhoLW1qZoYLZzLnBw+QkPP9WZnjlSWihhxAJC1+/M=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
-github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
-github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
-github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
-github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
-github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
-github.com/stackitcloud/stackit-sdk-go/core v0.17.3 h1:GsZGmRRc/3GJLmCUnsZswirr5wfLRrwavbnL/renOqg=
-github.com/stackitcloud/stackit-sdk-go/core v0.17.3/go.mod h1:HBCXJGPgdRulplDzhrmwC+Dak9B/x0nzNtmOpu+1Ahg=
+github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
+github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
+github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
+github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/stackitcloud/stackit-sdk-go/core v0.21.1 h1:Y/PcAgM7DPYMNqum0MLv4n1mF9ieuevzcCIZYQfm3Ts=
+github.com/stackitcloud/stackit-sdk-go/core v0.21.1/go.mod h1:osMglDby4csGZ5sIfhNyYq1bS1TxIdPY88+skE/kkmI=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
@@ -491,6 +554,7 @@ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
@@ -507,72 +571,75 @@ github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw=
-go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
-go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
-go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
-go.opentelemetry.io/collector/component v1.45.0 h1:gGFfVdbQ+1YuyUkJjWo85I7euu3H/CiupuzCHv8OgHA=
-go.opentelemetry.io/collector/component v1.45.0/go.mod h1:xoNFnRKE8Iv6gmlqAKgjayWraRnDcYLLgrPt9VgyO2g=
-go.opentelemetry.io/collector/component/componentstatus v0.139.0 h1:bQmkv1t7xW7uIDireE0a2Am4IMOprXm6zQr/qDtGCIA=
-go.opentelemetry.io/collector/component/componentstatus v0.139.0/go.mod h1:ibZOohpG0u081/NaT/jMCTsKwRbbwwxWrjZml+owpyM=
-go.opentelemetry.io/collector/component/componenttest v0.139.0 h1:x9Yu2eYhrHxdZ7sFXWtAWVjQ3UIraje557LgNurDC2I=
-go.opentelemetry.io/collector/component/componenttest v0.139.0/go.mod h1:S9cj+qkf9FgHMzjvlYsLwQKd9BiS7B7oLZvxvlENM/c=
-go.opentelemetry.io/collector/confmap v1.45.0 h1:7M7TTlpzX4r+mIzP/ARdxZBAvI4N+1V96phDane+akU=
-go.opentelemetry.io/collector/confmap v1.45.0/go.mod h1:AE1dnkjv0T9gptsh5+mTX0XFGdXx0n7JS4b7CcPfJ6Q=
-go.opentelemetry.io/collector/confmap/xconfmap v0.139.0 h1:uQGpFuWnTCXqdMbI3gDSvkwU66/kF/aoC0kVMrit1EM=
-go.opentelemetry.io/collector/confmap/xconfmap v0.139.0/go.mod h1:d0ucaeNq2rojFRSQsCHF/gkT3cgBx5H2bVkPQMj57ck=
-go.opentelemetry.io/collector/consumer v1.45.0 h1:TtqXxgW+1GSCwdoohq0fzqnfqrZBKbfo++1XRj8mrEA=
-go.opentelemetry.io/collector/consumer v1.45.0/go.mod h1:pJzqTWBubwLt8mVou+G4/Hs23b3m425rVmld3LqOYpY=
-go.opentelemetry.io/collector/consumer/consumertest v0.139.0 h1:06mu43mMO7l49ASJ/GEbKgTWcV3py5zE/pKhNBZ1b3k=
-go.opentelemetry.io/collector/consumer/consumertest v0.139.0/go.mod h1:gaeCpRQGbCFYTeLzi+Z2cTDt40GiIa3hgIEgLEmiC78=
-go.opentelemetry.io/collector/consumer/xconsumer v0.139.0 h1:FhzDv+idglnrfjqPvnUw3YAEOkXSNv/FuNsuMiXQwcY=
-go.opentelemetry.io/collector/consumer/xconsumer v0.139.0/go.mod h1:yWrg/6FE/A4Q7eo/Mg++CzkBoSILHdeMnTlxV3serI0=
-go.opentelemetry.io/collector/featuregate v1.45.0 h1:D06hpf1F2KzKC+qXLmVv5e8IZpgCyZVeVVC8iOQxVmw=
-go.opentelemetry.io/collector/featuregate v1.45.0/go.mod h1:d0tiRzVYrytB6LkcYgz2ESFTv7OktRPQe0QEQcPt1L4=
-go.opentelemetry.io/collector/pdata v1.45.0 h1:q4XaISpeX640BcwXwb2mKOVw/gb67r22HjGWl8sbWsk=
-go.opentelemetry.io/collector/pdata v1.45.0/go.mod h1:5q2f001YhwMQO8QvpFhCOa4Cq/vtwX9W4HRMsXkU/nE=
-go.opentelemetry.io/collector/pdata/pprofile v0.139.0 h1:UA5TgFzYmRuJN3Wz0GR1efLUfjbs5rH0HTaxfASpTR8=
-go.opentelemetry.io/collector/pdata/pprofile v0.139.0/go.mod h1:sI5qHt+zzE2fhOWFdJIaiDBR0yGGjD4A4ZvDFU0tiHk=
-go.opentelemetry.io/collector/pdata/testdata v0.139.0 h1:n7O5bmLLhc3T6PePV4447fFcI/6QWcMhBsLtfCaD0do=
-go.opentelemetry.io/collector/pdata/testdata v0.139.0/go.mod h1:fxZ2VrhYLYBLHYBHC1XQRKZ6IJXwy0I2rPaaRlebYaY=
-go.opentelemetry.io/collector/pipeline v1.45.0 h1:sn9JJAEBe3XABTkWechMk0eH60QMBjjNe5V+ccBl+Uo=
-go.opentelemetry.io/collector/pipeline v1.45.0/go.mod h1:xUrAqiebzYbrgxyoXSkk6/Y3oi5Sy3im2iCA51LwUAI=
-go.opentelemetry.io/collector/processor v1.45.0 h1:GH5km9BkDQOoz7MR0jzTnzB1Kb5vtKzPwa/wDmRg2dQ=
-go.opentelemetry.io/collector/processor v1.45.0/go.mod h1:wdlaTTC3wqlZIJP9R9/SLc2q7h+MFGARsxfjgPtwbes=
-go.opentelemetry.io/collector/processor/processortest v0.139.0 h1:30akUdruFNG7EDpayuBhXoX2lV+hcfxW9Gl3Z6MYHb0=
-go.opentelemetry.io/collector/processor/processortest v0.139.0/go.mod h1:RTll3UKHrqj/VS6RGjTHtuGIJzyLEwFhbw8KuCL3pjo=
-go.opentelemetry.io/collector/processor/xprocessor v0.139.0 h1:O9x9RF/OG8gZ+HrOcB4f6F1fjniby484xf2D8GBxgqU=
-go.opentelemetry.io/collector/processor/xprocessor v0.139.0/go.mod h1:hqGhEZ1/PftD/QHaYna0o1xAqZUsb7GhqpOiaTTDJnQ=
-go.opentelemetry.io/collector/semconv v0.128.0 h1:MzYOz7Vgb3Kf5D7b49pqqgeUhEmOCuT10bIXb/Cc+k4=
-go.opentelemetry.io/collector/semconv v0.128.0/go.mod h1:OPXer4l43X23cnjLXIZnRj/qQOjSuq4TgBLI76P9hns=
-go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0 h1:2pn7OzMewmYRiNtv1doZnLo3gONcnMHlFnmOR8Vgt+8=
-go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0/go.mod h1:rjbQTDEPQymPE0YnRQp9/NuPwwtL0sesz/fnqRW/v84=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
-go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
-go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
-go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
-go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
-go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
-go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
-go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
-go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
-go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
-go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
-go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
-go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
-go.opentelemetry.io/proto/slim/otlp v1.8.0 h1:afcLwp2XOeCbGrjufT1qWyruFt+6C9g5SOuymrSPUXQ=
-go.opentelemetry.io/proto/slim/otlp v1.8.0/go.mod h1:Yaa5fjYm1SMCq0hG0x/87wV1MP9H5xDuG/1+AhvBcsI=
-go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.1.0 h1:Uc+elixz922LHx5colXGi1ORbsW8DTIGM+gg+D9V7HE=
-go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.1.0/go.mod h1:VyU6dTWBWv6h9w/+DYgSZAPMabWbPTFTuxp25sM8+s0=
-go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.1.0 h1:i8YpvWGm/Uq1koL//bnbJ/26eV3OrKWm09+rDYo7keU=
-go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.1.0/go.mod h1:pQ70xHY/ZVxNUBPn+qUWPl8nwai87eWdqL3M37lNi9A=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU=
+go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ=
+go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
+go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
+go.opentelemetry.io/collector/component v1.51.0 h1:btNW76MCRmpsk0ARRT5wspDXF9tvdaLd3uBtYXIiQn0=
+go.opentelemetry.io/collector/component v1.51.0/go.mod h1:Zlgwh4yTLDhJglOXqiyXZ7paepTvvoijfFjLqOr/Qww=
+go.opentelemetry.io/collector/component/componentstatus v0.145.0 h1:EwUZfSaagdpRXnlrb0TqReJXXW2p9HWBU5YiIeXPCAE=
+go.opentelemetry.io/collector/component/componentstatus v0.145.0/go.mod h1:OiYb8rT4FtSJPFSGCKYvOaajdueDUTJZncixGrmy5aM=
+go.opentelemetry.io/collector/component/componenttest v0.145.0 h1:ryhRrXqQybGMhz7A7t32NC8BXAFcX2o1RetgPM7vw88=
+go.opentelemetry.io/collector/component/componenttest v0.145.0/go.mod h1:5uStrhUdZ0Fw3se00CPmVaRtW8o9N8kKiY76OSCWFjQ=
+go.opentelemetry.io/collector/confmap v1.51.0 h1:C9YlMNkIgzuauLpUz2F7DLlWwqAmkQKNcKj1XATVWuE=
+go.opentelemetry.io/collector/confmap v1.51.0/go.mod h1:uWi4b9lHfvEC2poJ2I2vXwGUREVEQTcdUguOpfqdcHM=
+go.opentelemetry.io/collector/confmap/xconfmap v0.145.0 h1:ngbyfh4+SKlA+osgsak3AxUNPxVxaJTmA0Sl7VfJzwY=
+go.opentelemetry.io/collector/confmap/xconfmap v0.145.0/go.mod h1:zTSK+c76NAy/tI1R3xfZjdoI04D9EYDnzAHQQwl6AmA=
+go.opentelemetry.io/collector/consumer v1.51.0 h1:Ex1x/k9VEEA2DOgt/eSc2Z9KTp0I6xBSruLmrYFfIFY=
+go.opentelemetry.io/collector/consumer v1.51.0/go.mod h1:Erk6qdfVj+24QTrGCpurcrF+qdUlHkb4dgMy5wJxLvY=
+go.opentelemetry.io/collector/consumer/consumertest v0.145.0 h1:3+uMwuMHoXMAU+Z6mwCRA3AxWeL7SujcAQwqqHJ1gCc=
+go.opentelemetry.io/collector/consumer/consumertest v0.145.0/go.mod h1:IFc/FeaIHQClb8KK0aVn0tFDNMc+/MmfQ+aBT1cJNeo=
+go.opentelemetry.io/collector/consumer/xconsumer v0.145.0 h1:9w7KKv9lVJoHvMLC6SUJHenU/KySdEgFJXbB4JQOEsk=
+go.opentelemetry.io/collector/consumer/xconsumer v0.145.0/go.mod h1:SryDCLP2ZaFeZJtA2CSksJ0XvjH8k3LmlfXvy/kC7Wc=
+go.opentelemetry.io/collector/featuregate v1.51.0 h1:dxJuv/3T84dhNKp7fz5+8srHz1dhquGzDpLW4OZTFBw=
+go.opentelemetry.io/collector/featuregate v1.51.0/go.mod h1:/1bclXgP91pISaEeNulRxzzmzMTm4I5Xih2SnI4HRSo=
+go.opentelemetry.io/collector/internal/componentalias v0.145.0 h1:A9V5IiETzz8FCtjxjRM5gf7RE3sOtA1h8phmpQjXTZ4=
+go.opentelemetry.io/collector/internal/componentalias v0.145.0/go.mod h1:sEKEAwAn45ZiXRk3T/vbkvetw14tIRd0CJIxcEx9SsQ=
+go.opentelemetry.io/collector/internal/testutil v0.145.0 h1:H/KL0GH3kGqSMKxZvnQ0B0CulfO9xdTg4DZf28uV7fY=
+go.opentelemetry.io/collector/internal/testutil v0.145.0/go.mod h1:YAD9EAkwh/l5asZNbEBEUCqEjoL1OKMjAMoPjPqH76c=
+go.opentelemetry.io/collector/pdata v1.51.0 h1:DnDhSEuDXNdzGRB7f6oOfXpbDApwBX3tY+3K69oUrDA=
+go.opentelemetry.io/collector/pdata v1.51.0/go.mod h1:GoX1bjKDR++mgFKdT7Hynv9+mdgQ1DDXbjs7/Ww209Q=
+go.opentelemetry.io/collector/pdata/pprofile v0.145.0 h1:ASMKpoqokf8HhzjoeMKZf0K6UXLhufVwNXH0sSuUn5w=
+go.opentelemetry.io/collector/pdata/pprofile v0.145.0/go.mod h1:a60GC7wQPhLAixWzKbbP51QLwwc+J0Cmp4SurOlhGUk=
+go.opentelemetry.io/collector/pdata/testdata v0.145.0 h1:iFsxsCMtE3lnAc/5kZbhZHpRv1OMmM+O5ry46xdQHbg=
+go.opentelemetry.io/collector/pdata/testdata v0.145.0/go.mod h1:0y2ERArdzqmYdJHdKLKue+AUubSEGlwK49F+23+Mbic=
+go.opentelemetry.io/collector/pipeline v1.51.0 h1:GZBNW+aaOE+zufGzAkXy0OI7n1cqepEa5J+beaOpS2k=
+go.opentelemetry.io/collector/pipeline v1.51.0/go.mod h1:xUrAqiebzYbrgxyoXSkk6/Y3oi5Sy3im2iCA51LwUAI=
+go.opentelemetry.io/collector/processor v1.51.0 h1:PKpCzkLQmqaW08TOVh/zM0qx07Ihq+DR5J/OBkPiL9o=
+go.opentelemetry.io/collector/processor v1.51.0/go.mod h1:rtIPFS+EFRAkG+CSwtjxs2IsIkuZStObvALeueD02XI=
+go.opentelemetry.io/collector/processor/processortest v0.145.0 h1:RDGBmyZnHk7XVK/EdLt/8iPWj+QLStbbVi1nFTNR01s=
+go.opentelemetry.io/collector/processor/processortest v0.145.0/go.mod h1:WAvxAzSojkdoZB915Z1lsVHCPDJBb2fepjJBjenrzjg=
+go.opentelemetry.io/collector/processor/xprocessor v0.145.0 h1:DaIE7MxRlg0OL1o2P0GQZtmZeExAmVso3qWv8S0RLps=
+go.opentelemetry.io/collector/processor/xprocessor v0.145.0/go.mod h1:kUwRyKBU/kjCmXodd+0z7CpvcP0A9G9/QL+MaJt4U2o=
+go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.65.0 h1:ab5U7DpTjjN8pNgwqlA/s0Csb+N2Raqo9eTSDhfg4Z8=
+go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.65.0/go.mod h1:nwFJC46Dxhqz5R9k7IV8To/Z46JPvW+GNKhTxQQlUzg=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
+go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
+go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 h1:DvJDOPmSWQHWywQS6lKL+pb8s3gBLOZUtw4N+mavW1I=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0/go.mod h1:EtekO9DEJb4/jRyN4v4Qjc2yA7AtfCBuz2FynRUWTXs=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 h1:wVZXIWjQSeSmMoxF74LzAnpVQOAFDo3pPji9Y4SOFKc=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0/go.mod h1:khvBS2IggMFNwZK/6lEeHg/W57h/IX6J4URh57fuI40=
+go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
+go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
+go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
+go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
+go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
+go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
+go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
+go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
+go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
+go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
+go.opentelemetry.io/proto/slim/otlp v1.9.0 h1:fPVMv8tP3TrsqlkH1HWYUpbCY9cAIemx184VGkS6vlE=
+go.opentelemetry.io/proto/slim/otlp v1.9.0/go.mod h1:xXdeJJ90Gqyll+orzUkY4bOd2HECo5JofeoLpymVqdI=
+go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.2.0 h1:o13nadWDNkH/quoDomDUClnQBpdQQ2Qqv0lQBjIXjE8=
+go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.2.0/go.mod h1:Gyb6Xe7FTi/6xBHwMmngGoHqL0w29Y4eW8TGFzpefGA=
+go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.2.0 h1:EiUYvtwu6PMrMHVjcPfnsG3v+ajPkbUeH+IL93+QYyk=
+go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.2.0/go.mod h1:mUUHKFiN2SST3AhJ8XhJxEoeVW12oqfXog0Bo8W3Ec4=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
@@ -581,27 +648,32 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
-go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
-go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
+go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
+go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
+go.yaml.in/yaml/v4 v4.0.0-rc.4 h1:UP4+v6fFrBIb1l934bDl//mmnoIZEDK0idg1+AIvX5U=
+go.yaml.in/yaml/v4 v4.0.0-rc.4/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
-golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
-golang.org/x/exp v0.0.0-20250808145144-a408d31f581a h1:Y+7uR/b1Mw2iSXZ3G//1haIiSElDQZ8KWh0h+sZPG90=
-golang.org/x/exp v0.0.0-20250808145144-a408d31f581a/go.mod h1:rT6SFzZ7oxADUDx58pcaKFTcZ+inxAa9fTrYx/uVYwg=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
+golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
+golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
+golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
+golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
-golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
+golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -610,18 +682,24 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
-golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
-golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
-golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
-golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
+golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
+golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
+golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
+golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
+golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
-golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
+golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -639,35 +717,49 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
-golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
+golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
-golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
+golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
+golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
+golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
-golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
-golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI=
-golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
+golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
+golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
+golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
-golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
+golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
+golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
golang.org/x/tools/godoc v0.1.0-deprecated h1:o+aZ1BOj6Hsx/GBdJO/s815sqftjSnrZZwyYTHODvtk=
golang.org/x/tools/godoc v0.1.0-deprecated/go.mod h1:qM63CriJ961IHWmnWa9CjZnBndniPt4a3CK0PVB9bIg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -676,29 +768,31 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
-google.golang.org/api v0.252.0 h1:xfKJeAJaMwb8OC9fesr369rjciQ704AjU/psjkKURSI=
-google.golang.org/api v0.252.0/go.mod h1:dnHOv81x5RAmumZ7BWLShB/u7JZNeyalImxHmtTHxqw=
-google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4=
-google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s=
-google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 h1:8XJ4pajGwOlasW+L13MnEGA8W4115jJySQtVfS2/IBU=
-google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4/go.mod h1:NnuHhy+bxcg30o7FnVAZbXsPHUDQ9qKWAQKCD7VxFtk=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ=
-google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
-google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
-google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
-google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
+google.golang.org/api v0.267.0 h1:w+vfWPMPYeRs8qH1aYYsFX68jMls5acWl/jocfLomwE=
+google.golang.org/api v0.267.0/go.mod h1:Jzc0+ZfLnyvXma3UtaTl023TdhZu6OMBP9tJ+0EmFD0=
+google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM=
+google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM=
+google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d h1:EocjzKLywydp5uZ5tJ79iP6Q0UjDnyiHkGRWxuPBP8s=
+google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:48U2I+QQUYhsFrg2SY6r+nJzeOtjey7j//WBESw+qyQ=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
+google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
+google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
+google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
+google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
-gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
-gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
+gopkg.in/dnaeon/go-vcr.v4 v4.0.6 h1:PiJkrakkmzc5s7EfBnZOnyiLwi7o7A9fwPzN0X2uwe0=
+gopkg.in/dnaeon/go-vcr.v4 v4.0.6/go.mod h1:sbq5oMEcM4PXngbcNbHhzfCP9OdZodLhrbRYoyg09HY=
+gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
+gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
-gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
-gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/ini.v1 v1.67.1 h1:tVBILHy0R6e4wkYOn3XmiITt/hEVH4TFMYvAX2Ytz6k=
+gopkg.in/ini.v1 v1.67.1/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@@ -708,25 +802,24 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
-gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
-gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
-k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM=
-k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk=
-k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4=
-k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
-k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY=
-k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8=
+gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
+gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
+k8s.io/api v0.35.1 h1:0PO/1FhlK/EQNVK5+txc4FuhQibV25VLSdLMmGpDE/Q=
+k8s.io/api v0.35.1/go.mod h1:28uR9xlXWml9eT0uaGo6y71xK86JBELShLy4wR1XtxM=
+k8s.io/apimachinery v0.35.1 h1:yxO6gV555P1YV0SANtnTjXYfiivaTPvCTKX6w6qdDsU=
+k8s.io/apimachinery v0.35.1/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
+k8s.io/client-go v0.35.1 h1:+eSfZHwuo/I19PaSxqumjqZ9l5XiTEKbIaJ+j1wLcLM=
+k8s.io/client-go v0.35.1/go.mod h1:1p1KxDt3a0ruRfc/pG4qT/3oHmUj1AhSHEcxNSGg+OA=
k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8=
k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
-k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA=
-k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts=
-k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y=
-k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
-sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
-sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
+k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=
+k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
+k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
+k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
+sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
+sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
diff --git a/go.work b/go.work
new file mode 100644
index 0000000000..aea341baab
--- /dev/null
+++ b/go.work
@@ -0,0 +1,9 @@
+go 1.25.5
+
+use (
+ .
+ ./documentation/examples/remote_storage
+ ./internal/tools
+ ./web/ui/mantine-ui/src/promql/tools
+ ./compliance
+)
diff --git a/internal/tools/go.mod b/internal/tools/go.mod
index a343a56834..041724c22d 100644
--- a/internal/tools/go.mod
+++ b/internal/tools/go.mod
@@ -1,107 +1,117 @@
module github.com/prometheus/prometheus/internal/tools
-go 1.24.0
+go 1.25.5
require (
- github.com/bufbuild/buf v1.57.2
+ github.com/bufbuild/buf v1.65.0
github.com/daixiang0/gci v0.13.7
github.com/gogo/protobuf v1.3.2
- github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3
+ github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0
)
require (
- buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.9-20250718181942-e35f9b667443.1 // indirect
- buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.9-20250912141014-52f32327d4b0.1 // indirect
- buf.build/gen/go/bufbuild/registry/connectrpc/go v1.18.1-20250903170917-c4be0f57e197.1 // indirect
- buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.9-20250903170917-c4be0f57e197.1 // indirect
- buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.8-20241007202033-cf42259fcbfc.1 // indirect
- buf.build/go/app v0.1.0 // indirect
+ buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.11-20250718181942-e35f9b667443.1 // indirect
+ buf.build/gen/go/bufbuild/protodescriptor/protocolbuffers/go v1.36.11-20250109164928-1da0de137947.1 // indirect
+ buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1 // indirect
+ buf.build/gen/go/bufbuild/registry/connectrpc/go v1.19.1-20260126144947-819582968857.2 // indirect
+ buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260126144947-819582968857.1 // indirect
+ buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.11-20241007202033-cf42259fcbfc.1 // indirect
+ buf.build/go/app v0.2.0 // indirect
buf.build/go/bufplugin v0.9.0 // indirect
+ buf.build/go/bufprivateusage v0.1.0 // indirect
buf.build/go/interrupt v1.1.0 // indirect
- buf.build/go/protovalidate v1.0.0 // indirect
+ buf.build/go/protovalidate v1.1.0 // indirect
buf.build/go/protoyaml v0.6.0 // indirect
buf.build/go/spdx v0.2.0 // indirect
buf.build/go/standard v0.1.0 // indirect
- cel.dev/expr v0.24.0 // indirect
- connectrpc.com/connect v1.18.1 // indirect
- connectrpc.com/otelconnect v0.8.0 // indirect
+ cel.dev/expr v0.25.1 // indirect
+ connectrpc.com/connect v1.19.1 // indirect
+ connectrpc.com/otelconnect v0.9.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
- github.com/bufbuild/protocompile v0.14.1 // indirect
+ github.com/bufbuild/protocompile v0.14.2-0.20260130195850-5c64bed4577e // indirect
github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1 // indirect
+ github.com/cespare/xxhash/v2 v2.3.0 // indirect
+ github.com/cli/browser v1.3.0 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
- github.com/containerd/stargz-snapshotter/estargz v0.17.0 // indirect
+ github.com/containerd/stargz-snapshotter/estargz v0.18.2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
+ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/distribution/reference v0.6.0 // indirect
- github.com/docker/cli v28.4.0+incompatible // indirect
+ github.com/docker/cli v29.2.0+incompatible // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect
- github.com/docker/docker v28.4.0+incompatible // indirect
- github.com/docker/docker-credential-helpers v0.9.3 // indirect
+ github.com/docker/docker v28.5.2+incompatible // indirect
+ github.com/docker/docker-credential-helpers v0.9.5 // indirect
github.com/docker/go-connections v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
- github.com/go-chi/chi/v5 v5.2.3 // indirect
+ github.com/go-chi/chi/v5 v5.2.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
- github.com/gofrs/flock v0.12.1 // indirect
- github.com/google/cel-go v0.26.1 // indirect
- github.com/google/go-containerregistry v0.20.6 // indirect
+ github.com/gofrs/flock v0.13.0 // indirect
+ github.com/google/cel-go v0.27.0 // indirect
+ github.com/google/go-containerregistry v0.20.7 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hexops/gotextdiff v1.0.3 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jdx/go-netrc v1.0.0 // indirect
- github.com/klauspost/compress v1.18.0 // indirect
+ github.com/klauspost/compress v1.18.4 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/term v0.5.2 // indirect
- github.com/morikuni/aec v1.0.0 // indirect
+ github.com/morikuni/aec v1.1.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
- github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
+ github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 // indirect
github.com/pkg/errors v0.9.1 // indirect
- github.com/quic-go/qpack v0.5.1 // indirect
- github.com/quic-go/quic-go v0.54.0 // indirect
+ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
+ github.com/quic-go/qpack v0.6.0 // indirect
+ github.com/quic-go/quic-go v0.59.0 // indirect
+ github.com/rivo/uniseg v0.4.7 // indirect
github.com/rs/cors v1.11.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
- github.com/segmentio/asm v1.2.0 // indirect
+ github.com/segmentio/asm v1.2.1 // indirect
github.com/segmentio/encoding v0.5.3 // indirect
- github.com/sirupsen/logrus v1.9.3 // indirect
- github.com/spf13/cobra v1.10.1 // indirect
+ github.com/sirupsen/logrus v1.9.4 // indirect
+ github.com/spf13/cobra v1.10.2 // indirect
github.com/spf13/pflag v1.0.10 // indirect
- github.com/stoewer/go-strcase v1.3.1 // indirect
- github.com/tetratelabs/wazero v1.9.0 // indirect
- github.com/vbatts/tar-split v0.12.1 // indirect
+ github.com/tetratelabs/wazero v1.11.0 // indirect
+ github.com/tidwall/btree v1.8.1 // indirect
+ github.com/vbatts/tar-split v0.12.2 // indirect
go.lsp.dev/jsonrpc2 v0.10.0 // indirect
go.lsp.dev/pkg v0.0.0-20210717090340-384b27a52fb2 // indirect
go.lsp.dev/protocol v0.12.0 // indirect
go.lsp.dev/uri v0.3.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
- go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
- go.opentelemetry.io/otel v1.38.0 // indirect
- go.opentelemetry.io/otel/metric v1.38.0 // indirect
- go.opentelemetry.io/otel/trace v1.38.0 // indirect
+ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect
+ go.opentelemetry.io/otel v1.40.0 // indirect
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 // indirect
+ go.opentelemetry.io/otel/metric v1.40.0 // indirect
+ go.opentelemetry.io/otel/trace v1.40.0 // indirect
go.uber.org/mock v0.6.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
- go.uber.org/zap v1.27.0 // indirect
+ go.uber.org/zap v1.27.1 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
- golang.org/x/crypto v0.42.0 // indirect
- golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b // indirect
- golang.org/x/mod v0.28.0 // indirect
- golang.org/x/net v0.44.0 // indirect
- golang.org/x/sync v0.17.0 // indirect
- golang.org/x/sys v0.36.0 // indirect
- golang.org/x/term v0.35.0 // indirect
- golang.org/x/text v0.29.0 // indirect
- golang.org/x/tools v0.37.0 // indirect
- google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 // indirect
- google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 // indirect
- google.golang.org/grpc v1.75.1 // indirect
- google.golang.org/protobuf v1.36.10 // indirect
+ golang.org/x/crypto v0.47.0 // indirect
+ golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect
+ golang.org/x/mod v0.32.0 // indirect
+ golang.org/x/net v0.49.0 // indirect
+ golang.org/x/sync v0.19.0 // indirect
+ golang.org/x/sys v0.41.0 // indirect
+ golang.org/x/term v0.39.0 // indirect
+ golang.org/x/text v0.34.0 // indirect
+ golang.org/x/time v0.14.0 // indirect
+ google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
+ google.golang.org/grpc v1.79.1 // indirect
+ google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
+ gotest.tools/v3 v3.5.1 // indirect
+ mvdan.cc/xurls/v2 v2.6.0 // indirect
pluginrpc.com/pluginrpc v0.5.0 // indirect
)
diff --git a/internal/tools/go.sum b/internal/tools/go.sum
index 3a2788f200..62602903c7 100644
--- a/internal/tools/go.sum
+++ b/internal/tools/go.sum
@@ -1,55 +1,67 @@
-buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.9-20250718181942-e35f9b667443.1 h1:HiLfreYRsqycF5QDlsnvSQOnl4tvhBoROl8+DkbaphI=
-buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.9-20250718181942-e35f9b667443.1/go.mod h1:WSxC6zKCpqVRcGZCpOgVwkATp9XBIleoAdSAnkq7dhw=
-buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.9-20250912141014-52f32327d4b0.1 h1:DQLS/rRxLHuugVzjJU5AvOwD57pdFl9he/0O7e5P294=
-buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.9-20250912141014-52f32327d4b0.1/go.mod h1:aY3zbkNan5F+cGm9lITDP6oxJIwu0dn9KjJuJjWaHkg=
-buf.build/gen/go/bufbuild/registry/connectrpc/go v1.18.1-20250903170917-c4be0f57e197.1 h1:isqFuFhL6JRd7+KF/vivWqZGJMCaTuAccZIWwneCcqE=
-buf.build/gen/go/bufbuild/registry/connectrpc/go v1.18.1-20250903170917-c4be0f57e197.1/go.mod h1:eGjb9P6sl1irS46NKyXnxkyozT2aWs3BF4tbYWQuCsw=
-buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.9-20250903170917-c4be0f57e197.1 h1:q+tABqEH2Cpcp8fO9TBZlvKok7zorHGy+/UyywXaAKo=
-buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.9-20250903170917-c4be0f57e197.1/go.mod h1:Y3m+VD8IH6JTgnFYggPHvFul/ry6dL3QDliy8xH7610=
-buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.8-20241007202033-cf42259fcbfc.1 h1:KuP+b+in6LGh2ukof5KgDCD8hPXotEq6EVOo13Wg1pE=
-buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.8-20241007202033-cf42259fcbfc.1/go.mod h1:dV1Kz6zdmyXt7QWm5OXby44OFpyLemllUDBUG5HMLio=
-buf.build/go/app v0.1.0 h1:nlqD/h0rhIN73ZoiDElprrPiO2N6JV+RmNK34K29Ihg=
-buf.build/go/app v0.1.0/go.mod h1:0XVOYemubVbxNXVY0DnsVgWeGkcbbAvjDa1fmhBC+Wo=
+buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.11-20250718181942-e35f9b667443.1 h1:zQ9C3e6FtwSZUFuKAQfpIKGFk5ZuRoGt5g35Bix55sI=
+buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.11-20250718181942-e35f9b667443.1/go.mod h1:1Znr6gmYBhbxWUPRrrVnSLXQsz8bvFVw1HHJq2bI3VQ=
+buf.build/gen/go/bufbuild/protodescriptor/protocolbuffers/go v1.36.11-20250109164928-1da0de137947.1 h1:HwzzCRS4ZrEm1++rzSDxHnO0DOjiT1b8I/24e8a4exY=
+buf.build/gen/go/bufbuild/protodescriptor/protocolbuffers/go v1.36.11-20250109164928-1da0de137947.1/go.mod h1:8PRKXhgNes29Tjrnv8KdZzg3I1QceOkzibW1QK7EXv0=
+buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1 h1:j9yeqTWEFrtimt8Nng2MIeRrpoCvQzM9/g25XTvqUGg=
+buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM=
+buf.build/gen/go/bufbuild/registry/connectrpc/go v1.19.1-20260126144947-819582968857.2 h1:XPrWCd9ydEo5Ofv1aNJVJaxndMXLQjRO9vVzsJG3jL8=
+buf.build/gen/go/bufbuild/registry/connectrpc/go v1.19.1-20260126144947-819582968857.2/go.mod h1:mpsjeEaxOYPIJV2cz4IagLghZufRvx+NPVtInjEeoQ8=
+buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260126144947-819582968857.1 h1:Yreby6Ypa58wdQUEm9Fnc5g8n/jP487Dq3aK5yBYwfk=
+buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260126144947-819582968857.1/go.mod h1:1JJi9jvOqRxSMa+JxiZSm57doB+db/1WYCIa2lHfc40=
+buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.11-20241007202033-cf42259fcbfc.1 h1:iGPvEJltOXUMANWf0zajcRcbiOXLD90ZwPUFvbcuv6Q=
+buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.11-20241007202033-cf42259fcbfc.1/go.mod h1:nWVKKRA29zdt4uvkjka3i/y4mkrswyWwiu0TbdX0zts=
+buf.build/go/app v0.2.0 h1:NYaH13A+RzPb7M5vO8uZYZ2maBZI5+MS9A9tQm66fy8=
+buf.build/go/app v0.2.0/go.mod h1:0XVOYemubVbxNXVY0DnsVgWeGkcbbAvjDa1fmhBC+Wo=
buf.build/go/bufplugin v0.9.0 h1:ktZJNP3If7ldcWVqh46XKeiYJVPxHQxCfjzVQDzZ/lo=
buf.build/go/bufplugin v0.9.0/go.mod h1:Z0CxA3sKQ6EPz/Os4kJJneeRO6CjPeidtP1ABh5jPPY=
+buf.build/go/bufprivateusage v0.1.0 h1:SzCoCcmzS3zyXHEXHeSQhGI7OTkgtljoknLzsUz9Gg4=
+buf.build/go/bufprivateusage v0.1.0/go.mod h1:GlCCJ3VVF7EqqU0CoRmo1FzAwwaKymEWSr+ty69xU5w=
buf.build/go/interrupt v1.1.0 h1:olBuhgv9Sav4/9pkSLoxgiOsZDgM5VhRhvRpn3DL0lE=
buf.build/go/interrupt v1.1.0/go.mod h1:ql56nXPG1oHlvZa6efNC7SKAQ/tUjS6z0mhJl0gyeRM=
-buf.build/go/protovalidate v1.0.0 h1:IAG1etULddAy93fiBsFVhpj7es5zL53AfB/79CVGtyY=
-buf.build/go/protovalidate v1.0.0/go.mod h1:KQmEUrcQuC99hAw+juzOEAmILScQiKBP1Oc36vvCLW8=
+buf.build/go/protovalidate v1.1.0 h1:pQqEQRpOo4SqS60qkvmhLTTQU9JwzEvdyiqAtXa5SeY=
+buf.build/go/protovalidate v1.1.0/go.mod h1:bGZcPiAQDC3ErCHK3t74jSoJDFOs2JH3d7LWuTEIdss=
buf.build/go/protoyaml v0.6.0 h1:Nzz1lvcXF8YgNZXk+voPPwdU8FjDPTUV4ndNTXN0n2w=
buf.build/go/protoyaml v0.6.0/go.mod h1:RgUOsBu/GYKLDSIRgQXniXbNgFlGEZnQpRAUdLAFV2Q=
buf.build/go/spdx v0.2.0 h1:IItqM0/cMxvFJJumcBuP8NrsIzMs/UYjp/6WSpq8LTw=
buf.build/go/spdx v0.2.0/go.mod h1:bXdwQFem9Si3nsbNy8aJKGPoaPi5DKwdeEp5/ArZ6w8=
buf.build/go/standard v0.1.0 h1:g98T9IyvAl0vS3Pq8iVk6Cvj2ZiFvoUJRtfyGa0120U=
buf.build/go/standard v0.1.0/go.mod h1:PiqpHz/7ZFq+kqvYhc/SK3lxFIB9N/aiH2CFC2JHIQg=
-cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
-cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
-connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw=
-connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8=
-connectrpc.com/otelconnect v0.8.0 h1:a4qrN4H8aEE2jAoCxheZYYfEjXMgVPyL9OzPQLBEFXU=
-connectrpc.com/otelconnect v0.8.0/go.mod h1:AEkVLjCPXra+ObGFCOClcJkNjS7zPaQSqvO0lCyjfZc=
+cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
+cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
+connectrpc.com/connect v1.19.1 h1:R5M57z05+90EfEvCY1b7hBxDVOUl45PrtXtAV2fOC14=
+connectrpc.com/connect v1.19.1/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w=
+connectrpc.com/otelconnect v0.9.0 h1:NggB3pzRC3pukQWaYbRHJulxuXvmCKCKkQ9hbrHAWoA=
+connectrpc.com/otelconnect v0.9.0/go.mod h1:AEkVLjCPXra+ObGFCOClcJkNjS7zPaQSqvO0lCyjfZc=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
-github.com/bufbuild/buf v1.57.2 h1:2vxP0giB8DVo0Lkem9T8WDUYIEC3zqY98+NHqAlP4ig=
-github.com/bufbuild/buf v1.57.2/go.mod h1:8cygE3L/J84dtgQAaquZKpXLo9MjAn+dSdFuXvbUNYg=
-github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=
-github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=
+github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs=
+github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
+github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4=
+github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs=
+github.com/bufbuild/buf v1.65.0 h1:f2BzeCY9rRh9P5KD340ZoPAaFLTkssoUTHx7lpqozgg=
+github.com/bufbuild/buf v1.65.0/go.mod h1:7SAs2YqGpPXHqBBXBeYQbCzY0OQq4Jbg6XCqirEiYvQ=
+github.com/bufbuild/protocompile v0.14.2-0.20260130195850-5c64bed4577e h1:emH16Bf1w4C0cJ3ge4QtBAl4sIYJe23EfpWH0SpA9co=
+github.com/bufbuild/protocompile v0.14.2-0.20260130195850-5c64bed4577e/go.mod h1:cxhE8h+14t0Yxq2H9MV/UggzQ1L0gh0t2tJobITWsBE=
github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1 h1:V1xulAoqLqVg44rY97xOR+mQpD2N+GzhMHVwJ030WEU=
github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1/go.mod h1:c5D8gWRIZ2HLWO3gXYTtUfw/hbJyD8xikv2ooPxnklQ=
-github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
-github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
+github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
+github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo=
+github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
-github.com/containerd/stargz-snapshotter/estargz v0.17.0 h1:+TyQIsR/zSFI1Rm31EQBwpAA1ovYgIKHy7kctL3sLcE=
-github.com/containerd/stargz-snapshotter/estargz v0.17.0/go.mod h1:s06tWAiJcXQo9/8AReBCIo/QxcXFZ2n4qfsRnpl71SM=
+github.com/containerd/stargz-snapshotter/estargz v0.18.2 h1:yXkZFYIzz3eoLwlTUZKz2iQ4MrckBxJjkmD16ynUTrw=
+github.com/containerd/stargz-snapshotter/estargz v0.18.2/go.mod h1:XyVU5tcJ3PRpkA9XS2T5us6Eg35yM0214Y+wvrZTBrY=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
@@ -57,47 +69,46 @@ github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/daixiang0/gci v0.13.7 h1:+0bG5eK9vlI08J+J/NWGbWPTNiXPG4WhNLJOkSxWITQ=
github.com/daixiang0/gci v0.13.7/go.mod h1:812WVN6JLFY9S6Tv76twqmNqevN0pa3SX3nih0brVzQ=
-github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
-github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
-github.com/docker/cli v28.4.0+incompatible h1:RBcf3Kjw2pMtwui5V0DIMdyeab8glEw5QY0UUU4C9kY=
-github.com/docker/cli v28.4.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
+github.com/docker/cli v29.2.0+incompatible h1:9oBd9+YM7rxjZLfyMGxjraKBKE4/nVyvVfN4qNl9XRM=
+github.com/docker/cli v29.2.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
-github.com/docker/docker v28.4.0+incompatible h1:KVC7bz5zJY/4AZe/78BIvCnPsLaC9T/zh72xnlrTTOk=
-github.com/docker/docker v28.4.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
-github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8=
-github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo=
+github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
+github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY=
+github.com/docker/docker-credential-helpers v0.9.5/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
-github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
-github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
+github.com/go-chi/chi/v5 v5.2.4 h1:WtFKPHwlywe8Srng8j2BhOD9312j9cGUxG1SP4V2cR4=
+github.com/go-chi/chi/v5 v5.2.4/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
-github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
-github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
+github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
+github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
-github.com/google/cel-go v0.26.1 h1:iPbVVEdkhTX++hpe3lzSk7D3G3QSYqLGoHOcEio+UXQ=
-github.com/google/cel-go v0.26.1/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM=
+github.com/google/cel-go v0.27.0 h1:e7ih85+4qVrBuqQWTW4FKSqZYokVuc3HnhH5keboFTo=
+github.com/google/cel-go v0.27.0/go.mod h1:tTJ11FWqnhw5KKpnWpvW9CJC3Y9GK4EIS0WXnBbebzw=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
-github.com/google/go-containerregistry v0.20.6 h1:cvWX87UxxLgaH76b4hIvya6Dzz9qHB31qAwjAohdSTU=
-github.com/google/go-containerregistry v0.20.6/go.mod h1:T0x8MuoAoKX/873bkeSfLD2FAkwCDf9/HZgsFJ02E2Y=
+github.com/google/go-containerregistry v0.20.7 h1:24VGNpS0IwrOZ2ms2P1QE3Xa5X9p4phx0aUgzYzHW6I=
+github.com/google/go-containerregistry v0.20.7/go.mod h1:Lx5LCZQjLH1QBaMPeGwsME9biPeo1lPx6lbGj/UmzgM=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
@@ -108,8 +119,8 @@ github.com/jhump/protoreflect/v2 v2.0.0-beta.2 h1:qZU+rEZUOYTz1Bnhi3xbwn+VxdXkLV
github.com/jhump/protoreflect/v2 v2.0.0-beta.2/go.mod h1:4tnOYkB/mq7QTyS3YKtVtNrJv4Psqout8HA1U+hZtgM=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
-github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
-github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
+github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
+github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -130,54 +141,53 @@ github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7z
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
-github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
-github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
+github.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ=
+github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
-github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
-github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
+github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 h1:KPpdlQLZcHfTMQRi6bFQ7ogNO0ltFT4PmtwTLW4W+14=
+github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
-github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
-github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
-github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
-github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/protocolbuffers/protoscope v0.0.0-20221109213918-8e7a6aafa2c9 h1:arwj11zP0yJIxIRiDn22E0H8PxfF7TsTrc2wIPFIsf4=
+github.com/protocolbuffers/protoscope v0.0.0-20221109213918-8e7a6aafa2c9/go.mod h1:SKZx6stCn03JN3BOWTwvVIO2ajMkb/zQdTceXYhKw/4=
+github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
+github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
+github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
+github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
+github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
+github.com/rodaine/protogofakeit v0.1.1 h1:ZKouljuRM3A+TArppfBqnH8tGZHOwM/pjvtXe9DaXH8=
+github.com/rodaine/protogofakeit v0.1.1/go.mod h1:pXn/AstBYMaSfc1/RqH3N82pBuxtWgejz1AlYpY1mI0=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
-github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
+github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
+github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/segmentio/encoding v0.5.3 h1:OjMgICtcSFuNvQCdwqMCv9Tg7lEOXGwm1J5RPQccx6w=
github.com/segmentio/encoding v0.5.3/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
-github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
-github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
-github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
-github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
+github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
+github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
+github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
+github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
-github.com/stoewer/go-strcase v1.3.1 h1:iS0MdW+kVTxgMoE1LAZyMiYJFKlOzLooE4MxjirtkAs=
-github.com/stoewer/go-strcase v1.3.1/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
-github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
-github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
-github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
-github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
-github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
-github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
-github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo=
-github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA=
+github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA=
+github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU=
+github.com/tidwall/btree v1.8.1 h1:27ehoXvm5AG/g+1VxLS1SD3vRhp/H7LuEfwNvddEdmA=
+github.com/tidwall/btree v1.8.1/go.mod h1:jBbTdUWhSZClZWoDg54VnvV7/54modSOzDN7VXftj1A=
+github.com/vbatts/tar-split v0.12.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CPa4=
+github.com/vbatts/tar-split v0.12.2/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.lsp.dev/jsonrpc2 v0.10.0 h1:Pr/YcXJoEOTMc/b6OTmcR1DPJ3mSWl/SWiU1Cct6VmI=
@@ -190,98 +200,97 @@ go.lsp.dev/uri v0.3.0 h1:KcZJmh6nFIBeJzTugn5JTU6OOyG0lDOo3R9KwTxTYbo=
go.lsp.dev/uri v0.3.0/go.mod h1:P5sbO1IQR+qySTWOCnhnK7phBx+W3zbLqSMDJNTw88I=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
-go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
-go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 h1:wpMfgF8E1rkrT1Z6meFh1NDtownE9Ii3n3X2GJYjsaU=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0=
-go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
-go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
-go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
-go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
-go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
-go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
-go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
-go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
-go.opentelemetry.io/proto/otlp v1.8.0 h1:fRAZQDcAFHySxpJ1TwlA1cJ4tvcrw7nXl9xWWC8N5CE=
-go.opentelemetry.io/proto/otlp v1.8.0/go.mod h1:tIeYOeNBU4cvmPqpaji1P+KbB4Oloai8wN4rWzRrFF0=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
+go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
+go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 h1:wVZXIWjQSeSmMoxF74LzAnpVQOAFDo3pPji9Y4SOFKc=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0/go.mod h1:khvBS2IggMFNwZK/6lEeHg/W57h/IX6J4URh57fuI40=
+go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
+go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
+go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
+go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
+go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
+go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
+go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
+go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
+go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
+go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
-go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
-go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
+go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
+go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
-golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
-golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0=
-golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
+golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
+golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
+golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
+golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
-golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
+golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
+golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
-golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
+golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
+golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
-golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
+golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
+golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
-golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
-golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
-golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
+golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
+golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
+golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
-golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
-golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs=
-golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
+golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
+golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
+golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
-golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
+golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
+golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 h1:8XJ4pajGwOlasW+L13MnEGA8W4115jJySQtVfS2/IBU=
-google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4/go.mod h1:NnuHhy+bxcg30o7FnVAZbXsPHUDQ9qKWAQKCD7VxFtk=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 h1:i8QOKZfYg6AbGVZzUAY3LrNWCKF8O6zFisU9Wl9RER4=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ=
-google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
-google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
-google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
-google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
+google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0=
+google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
+google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
+google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
+google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
+google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
-gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
+gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
+gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
+mvdan.cc/xurls/v2 v2.6.0 h1:3NTZpeTxYVWNSokW3MKeyVkz/j7uYXYiMtXRUfmjbgI=
+mvdan.cc/xurls/v2 v2.6.0/go.mod h1:bCvEZ1XvdA6wDnxY7jPPjEmigDtvtvPXAD/Exa9IMSk=
pluginrpc.com/pluginrpc v0.5.0 h1:tOQj2D35hOmvHyPu8e7ohW2/QvAnEtKscy2IJYWQ2yo=
pluginrpc.com/pluginrpc v0.5.0/go.mod h1:UNWZ941hcVAoOZUn8YZsMmOZBzbUjQa3XMns8RQLp9o=
diff --git a/internal/tools/tools.go b/internal/tools/tools.go
index e57e37186f..22e79a56f7 100644
--- a/internal/tools/tools.go
+++ b/internal/tools/tools.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Prometheus Authors
+// 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
diff --git a/model/exemplar/exemplar.go b/model/exemplar/exemplar.go
index d03940f1b2..5db7c46a68 100644
--- a/model/exemplar/exemplar.go
+++ b/model/exemplar/exemplar.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The Prometheus Authors
+// 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
diff --git a/model/histogram/float_histogram.go b/model/histogram/float_histogram.go
index c607448f38..d457d8ab25 100644
--- a/model/histogram/float_histogram.go
+++ b/model/histogram/float_histogram.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
@@ -18,6 +18,8 @@ import (
"fmt"
"math"
"strings"
+
+ "github.com/prometheus/prometheus/util/kahansum"
)
// FloatHistogram is similar to Histogram but uses float64 for all
@@ -164,8 +166,8 @@ func (h *FloatHistogram) CopyToSchema(targetSchema int32) *FloatHistogram {
Sum: h.Sum,
}
- c.PositiveSpans, c.PositiveBuckets = reduceResolution(h.PositiveSpans, h.PositiveBuckets, h.Schema, targetSchema, false, false)
- c.NegativeSpans, c.NegativeBuckets = reduceResolution(h.NegativeSpans, h.NegativeBuckets, h.Schema, targetSchema, false, false)
+ c.PositiveSpans, c.PositiveBuckets = mustReduceResolution(h.PositiveSpans, h.PositiveBuckets, h.Schema, targetSchema, false, false)
+ c.NegativeSpans, c.NegativeBuckets = mustReduceResolution(h.NegativeSpans, h.NegativeBuckets, h.Schema, targetSchema, false, false)
return &c
}
@@ -353,7 +355,7 @@ func (h *FloatHistogram) Add(other *FloatHistogram) (res *FloatHistogram, counte
}
counterResetCollision = h.adjustCounterReset(other)
if !h.UsesCustomBuckets() {
- otherZeroCount := h.reconcileZeroBuckets(other)
+ otherZeroCount, _ := h.reconcileZeroBuckets(other, nil)
h.ZeroCount += otherZeroCount
}
h.Count += other.Count
@@ -374,11 +376,11 @@ func (h *FloatHistogram) Add(other *FloatHistogram) (res *FloatHistogram, counte
intersectedBounds := intersectCustomBucketBounds(h.CustomValues, other.CustomValues)
// Add with mapping - maps both histograms to intersected layout.
- h.PositiveSpans, h.PositiveBuckets = addCustomBucketsWithMismatches(
+ h.PositiveSpans, h.PositiveBuckets, _ = addCustomBucketsWithMismatches(
false,
hPositiveSpans, hPositiveBuckets, h.CustomValues,
otherPositiveSpans, otherPositiveBuckets, other.CustomValues,
- intersectedBounds)
+ nil, intersectedBounds)
h.CustomValues = intersectedBounds
}
return h, counterResetCollision, nhcbBoundsReconciled, nil
@@ -393,13 +395,13 @@ func (h *FloatHistogram) Add(other *FloatHistogram) (res *FloatHistogram, counte
switch {
case other.Schema < h.Schema:
- hPositiveSpans, hPositiveBuckets = reduceResolution(hPositiveSpans, hPositiveBuckets, h.Schema, other.Schema, false, true)
- hNegativeSpans, hNegativeBuckets = reduceResolution(hNegativeSpans, hNegativeBuckets, h.Schema, other.Schema, false, true)
+ hPositiveSpans, hPositiveBuckets = mustReduceResolution(hPositiveSpans, hPositiveBuckets, h.Schema, other.Schema, false, true)
+ hNegativeSpans, hNegativeBuckets = mustReduceResolution(hNegativeSpans, hNegativeBuckets, h.Schema, other.Schema, false, true)
h.Schema = other.Schema
case other.Schema > h.Schema:
- otherPositiveSpans, otherPositiveBuckets = reduceResolution(otherPositiveSpans, otherPositiveBuckets, other.Schema, h.Schema, false, false)
- otherNegativeSpans, otherNegativeBuckets = reduceResolution(otherNegativeSpans, otherNegativeBuckets, other.Schema, h.Schema, false, false)
+ otherPositiveSpans, otherPositiveBuckets = mustReduceResolution(otherPositiveSpans, otherPositiveBuckets, other.Schema, h.Schema, false, false)
+ otherNegativeSpans, otherNegativeBuckets = mustReduceResolution(otherNegativeSpans, otherNegativeBuckets, other.Schema, h.Schema, false, false)
}
h.PositiveSpans, h.PositiveBuckets = addBuckets(h.Schema, h.ZeroThreshold, false, hPositiveSpans, hPositiveBuckets, otherPositiveSpans, otherPositiveBuckets)
@@ -408,6 +410,121 @@ func (h *FloatHistogram) Add(other *FloatHistogram) (res *FloatHistogram, counte
return h, counterResetCollision, nhcbBoundsReconciled, nil
}
+// KahanAdd works like Add but using the Kahan summation algorithm to minimize numerical errors.
+// c is a histogram holding the Kahan compensation term. It is modified in-place if non-nil.
+// If c is nil, a new compensation histogram is created inside the function. In this case,
+// the caller must use the returned updatedC, because the original c variable is not modified.
+func (h *FloatHistogram) KahanAdd(other, c *FloatHistogram) (updatedC *FloatHistogram, counterResetCollision, nhcbBoundsReconciled bool, err error) {
+ if err := h.checkSchemaAndBounds(other); err != nil {
+ return nil, false, false, err
+ }
+
+ counterResetCollision = h.adjustCounterReset(other)
+
+ if c == nil {
+ c = h.newCompensationHistogram()
+ }
+ if !h.UsesCustomBuckets() {
+ otherZeroCount, otherCZeroCount := h.reconcileZeroBuckets(other, c)
+ h.ZeroCount, c.ZeroCount = kahansum.Inc(otherZeroCount, h.ZeroCount, c.ZeroCount)
+ h.ZeroCount, c.ZeroCount = kahansum.Inc(otherCZeroCount, h.ZeroCount, c.ZeroCount)
+ }
+ h.Count, c.Count = kahansum.Inc(other.Count, h.Count, c.Count)
+ h.Sum, c.Sum = kahansum.Inc(other.Sum, h.Sum, c.Sum)
+
+ var (
+ hPositiveSpans = h.PositiveSpans
+ hPositiveBuckets = h.PositiveBuckets
+ otherPositiveSpans = other.PositiveSpans
+ otherPositiveBuckets = other.PositiveBuckets
+ cPositiveBuckets = c.PositiveBuckets
+ )
+
+ if h.UsesCustomBuckets() {
+ if CustomBucketBoundsMatch(h.CustomValues, other.CustomValues) {
+ h.PositiveSpans, h.PositiveBuckets, c.PositiveBuckets = kahanAddBuckets(
+ h.Schema, h.ZeroThreshold, false,
+ hPositiveSpans, hPositiveBuckets,
+ otherPositiveSpans, otherPositiveBuckets,
+ cPositiveBuckets, nil,
+ )
+ } else {
+ nhcbBoundsReconciled = true
+ intersectedBounds := intersectCustomBucketBounds(h.CustomValues, other.CustomValues)
+
+ // Add with mapping - maps both histograms to intersected layout.
+ h.PositiveSpans, h.PositiveBuckets, c.PositiveBuckets = addCustomBucketsWithMismatches(
+ false,
+ hPositiveSpans, hPositiveBuckets, h.CustomValues,
+ otherPositiveSpans, otherPositiveBuckets, other.CustomValues,
+ cPositiveBuckets, intersectedBounds)
+ h.CustomValues = intersectedBounds
+ c.CustomValues = intersectedBounds
+ }
+ c.PositiveSpans = h.PositiveSpans
+ return c, counterResetCollision, nhcbBoundsReconciled, nil
+ }
+
+ otherC := other.newCompensationHistogram()
+
+ var (
+ hNegativeSpans = h.NegativeSpans
+ hNegativeBuckets = h.NegativeBuckets
+ otherNegativeSpans = other.NegativeSpans
+ otherNegativeBuckets = other.NegativeBuckets
+ cNegativeBuckets = c.NegativeBuckets
+ otherCPositiveBuckets = otherC.PositiveBuckets
+ otherCNegativeBuckets = otherC.NegativeBuckets
+ )
+
+ switch {
+ case other.Schema < h.Schema:
+ hPositiveSpans, hPositiveBuckets, cPositiveBuckets = kahanReduceResolution(
+ hPositiveSpans, hPositiveBuckets, cPositiveBuckets,
+ h.Schema, other.Schema,
+ true,
+ )
+ hNegativeSpans, hNegativeBuckets, cNegativeBuckets = kahanReduceResolution(
+ hNegativeSpans, hNegativeBuckets, cNegativeBuckets,
+ h.Schema, other.Schema,
+ true,
+ )
+ h.Schema = other.Schema
+
+ case other.Schema > h.Schema:
+ otherPositiveSpans, otherPositiveBuckets, otherCPositiveBuckets = kahanReduceResolution(
+ otherPositiveSpans, otherPositiveBuckets, otherCPositiveBuckets,
+ other.Schema, h.Schema,
+ false,
+ )
+ otherNegativeSpans, otherNegativeBuckets, otherCNegativeBuckets = kahanReduceResolution(
+ otherNegativeSpans, otherNegativeBuckets, otherCNegativeBuckets,
+ other.Schema, h.Schema,
+ false,
+ )
+ }
+
+ h.PositiveSpans, h.PositiveBuckets, c.PositiveBuckets = kahanAddBuckets(
+ h.Schema, h.ZeroThreshold, false,
+ hPositiveSpans, hPositiveBuckets,
+ otherPositiveSpans, otherPositiveBuckets,
+ cPositiveBuckets, otherCPositiveBuckets,
+ )
+ h.NegativeSpans, h.NegativeBuckets, c.NegativeBuckets = kahanAddBuckets(
+ h.Schema, h.ZeroThreshold, false,
+ hNegativeSpans, hNegativeBuckets,
+ otherNegativeSpans, otherNegativeBuckets,
+ cNegativeBuckets, otherCNegativeBuckets,
+ )
+
+ c.Schema = h.Schema
+ c.ZeroThreshold = h.ZeroThreshold
+ c.PositiveSpans = h.PositiveSpans
+ c.NegativeSpans = h.NegativeSpans
+
+ return c, counterResetCollision, nhcbBoundsReconciled, nil
+}
+
// Sub works like Add but subtracts the other histogram. It uses the same logic
// to adjust the counter reset hint. This is useful where this method is used
// for incremental mean calculation. However, if it is used for the actual "-"
@@ -419,7 +536,7 @@ func (h *FloatHistogram) Sub(other *FloatHistogram) (res *FloatHistogram, counte
}
counterResetCollision = h.adjustCounterReset(other)
if !h.UsesCustomBuckets() {
- otherZeroCount := h.reconcileZeroBuckets(other)
+ otherZeroCount, _ := h.reconcileZeroBuckets(other, nil)
h.ZeroCount -= otherZeroCount
}
h.Count -= other.Count
@@ -440,11 +557,11 @@ func (h *FloatHistogram) Sub(other *FloatHistogram) (res *FloatHistogram, counte
intersectedBounds := intersectCustomBucketBounds(h.CustomValues, other.CustomValues)
// Subtract with mapping - maps both histograms to intersected layout.
- h.PositiveSpans, h.PositiveBuckets = addCustomBucketsWithMismatches(
+ h.PositiveSpans, h.PositiveBuckets, _ = addCustomBucketsWithMismatches(
true,
hPositiveSpans, hPositiveBuckets, h.CustomValues,
otherPositiveSpans, otherPositiveBuckets, other.CustomValues,
- intersectedBounds)
+ nil, intersectedBounds)
h.CustomValues = intersectedBounds
}
return h, counterResetCollision, nhcbBoundsReconciled, nil
@@ -459,12 +576,12 @@ func (h *FloatHistogram) Sub(other *FloatHistogram) (res *FloatHistogram, counte
switch {
case other.Schema < h.Schema:
- hPositiveSpans, hPositiveBuckets = reduceResolution(hPositiveSpans, hPositiveBuckets, h.Schema, other.Schema, false, true)
- hNegativeSpans, hNegativeBuckets = reduceResolution(hNegativeSpans, hNegativeBuckets, h.Schema, other.Schema, false, true)
+ hPositiveSpans, hPositiveBuckets = mustReduceResolution(hPositiveSpans, hPositiveBuckets, h.Schema, other.Schema, false, true)
+ hNegativeSpans, hNegativeBuckets = mustReduceResolution(hNegativeSpans, hNegativeBuckets, h.Schema, other.Schema, false, true)
h.Schema = other.Schema
case other.Schema > h.Schema:
- otherPositiveSpans, otherPositiveBuckets = reduceResolution(otherPositiveSpans, otherPositiveBuckets, other.Schema, h.Schema, false, false)
- otherNegativeSpans, otherNegativeBuckets = reduceResolution(otherNegativeSpans, otherNegativeBuckets, other.Schema, h.Schema, false, false)
+ otherPositiveSpans, otherPositiveBuckets = mustReduceResolution(otherPositiveSpans, otherPositiveBuckets, other.Schema, h.Schema, false, false)
+ otherNegativeSpans, otherNegativeBuckets = mustReduceResolution(otherNegativeSpans, otherNegativeBuckets, other.Schema, h.Schema, false, false)
}
h.PositiveSpans, h.PositiveBuckets = addBuckets(h.Schema, h.ZeroThreshold, true, hPositiveSpans, hPositiveBuckets, otherPositiveSpans, otherPositiveBuckets)
@@ -484,7 +601,7 @@ func (h *FloatHistogram) Sub(other *FloatHistogram) (res *FloatHistogram, counte
// supposed to be used according to the schema.
func (h *FloatHistogram) Equals(h2 *FloatHistogram) bool {
if h2 == nil {
- return false
+ return h == nil
}
if h.Schema != h2.Schema ||
@@ -576,15 +693,28 @@ func (h *FloatHistogram) Size() int {
// easier to iterate through. Still, the safest bet is to use maxEmptyBuckets==0
// and only use a larger number if you know what you are doing.
func (h *FloatHistogram) Compact(maxEmptyBuckets int) *FloatHistogram {
- h.PositiveBuckets, h.PositiveSpans = compactBuckets(
- h.PositiveBuckets, h.PositiveSpans, maxEmptyBuckets, false,
+ h.PositiveBuckets, _, h.PositiveSpans = compactBuckets(
+ h.PositiveBuckets, nil, h.PositiveSpans, maxEmptyBuckets, false,
)
- h.NegativeBuckets, h.NegativeSpans = compactBuckets(
- h.NegativeBuckets, h.NegativeSpans, maxEmptyBuckets, false,
+ h.NegativeBuckets, _, h.NegativeSpans = compactBuckets(
+ h.NegativeBuckets, nil, h.NegativeSpans, maxEmptyBuckets, false,
)
return h
}
+// kahanCompact works like Compact, but it is specialized for FloatHistogram's KahanAdd method.
+// c is a histogram holding the Kahan compensation term.
+func (h *FloatHistogram) kahanCompact(maxEmptyBuckets int, c *FloatHistogram,
+) (updatedH, updatedC *FloatHistogram) {
+ h.PositiveBuckets, c.PositiveBuckets, h.PositiveSpans = compactBuckets(
+ h.PositiveBuckets, c.PositiveBuckets, h.PositiveSpans, maxEmptyBuckets, false,
+ )
+ h.NegativeBuckets, c.NegativeBuckets, h.NegativeSpans = compactBuckets(
+ h.NegativeBuckets, c.NegativeBuckets, h.NegativeSpans, maxEmptyBuckets, false,
+ )
+ return h, c
+}
+
// DetectReset returns true if the receiving histogram is missing any buckets
// that have a non-zero population in the provided previous histogram. It also
// returns true if any count (in any bucket, in the zero count, or in the count
@@ -652,7 +782,7 @@ func (h *FloatHistogram) DetectReset(previous *FloatHistogram) bool {
// ZeroThreshold decreased.
return true
}
- previousZeroCount, newThreshold := previous.zeroCountForLargerThreshold(h.ZeroThreshold)
+ previousZeroCount, newThreshold, _ := previous.zeroCountForLargerThreshold(h.ZeroThreshold, nil)
if newThreshold != h.ZeroThreshold {
// ZeroThreshold is within a populated bucket in previous
// histogram.
@@ -847,30 +977,42 @@ func (h *FloatHistogram) Validate() error {
}
// zeroCountForLargerThreshold returns what the histogram's zero count would be
-// if the ZeroThreshold had the provided larger (or equal) value. If the
-// provided value is less than the histogram's ZeroThreshold, the method panics.
+// if the ZeroThreshold had the provided larger (or equal) value. It also returns the
+// zero count of the compensation histogram `c` if provided (used for Kahan summation).
+//
+// If the provided ZeroThreshold is less than the histogram's ZeroThreshold, the method panics.
// If the largerThreshold ends up within a populated bucket of the histogram, it
// is adjusted upwards to the lower limit of that bucket (all in terms of
// absolute values) and that bucket's count is included in the returned
// count. The adjusted threshold is returned, too.
-func (h *FloatHistogram) zeroCountForLargerThreshold(largerThreshold float64) (count, threshold float64) {
+func (h *FloatHistogram) zeroCountForLargerThreshold(
+ largerThreshold float64, c *FloatHistogram) (hZeroCount, threshold, cZeroCount float64,
+) {
+ if c != nil {
+ cZeroCount = c.ZeroCount
+ }
// Fast path.
if largerThreshold == h.ZeroThreshold {
- return h.ZeroCount, largerThreshold
+ return h.ZeroCount, largerThreshold, cZeroCount
}
if largerThreshold < h.ZeroThreshold {
panic(fmt.Errorf("new threshold %f is less than old threshold %f", largerThreshold, h.ZeroThreshold))
}
outer:
for {
- count = h.ZeroCount
+ hZeroCount = h.ZeroCount
i := h.PositiveBucketIterator()
+ bucketsIdx := 0
for i.Next() {
b := i.At()
if b.Lower >= largerThreshold {
break
}
- count += b.Count // Bucket to be merged into zero bucket.
+ // Bucket to be merged into zero bucket.
+ hZeroCount, cZeroCount = kahansum.Inc(b.Count, hZeroCount, cZeroCount)
+ if c != nil {
+ hZeroCount, cZeroCount = kahansum.Inc(c.PositiveBuckets[bucketsIdx], hZeroCount, cZeroCount)
+ }
if b.Upper > largerThreshold {
// New threshold ended up within a bucket. if it's
// populated, we need to adjust largerThreshold before
@@ -880,14 +1022,20 @@ outer:
}
break
}
+ bucketsIdx++
}
i = h.NegativeBucketIterator()
+ bucketsIdx = 0
for i.Next() {
b := i.At()
if b.Upper <= -largerThreshold {
break
}
- count += b.Count // Bucket to be merged into zero bucket.
+ // Bucket to be merged into zero bucket.
+ hZeroCount, cZeroCount = kahansum.Inc(b.Count, hZeroCount, cZeroCount)
+ if c != nil {
+ hZeroCount, cZeroCount = kahansum.Inc(c.NegativeBuckets[bucketsIdx], hZeroCount, cZeroCount)
+ }
if b.Lower < -largerThreshold {
// New threshold ended up within a bucket. If
// it's populated, we need to adjust
@@ -900,15 +1048,17 @@ outer:
}
break
}
+ bucketsIdx++
}
- return count, largerThreshold
+ return hZeroCount, largerThreshold, cZeroCount
}
}
// trimBucketsInZeroBucket removes all buckets that are within the zero
// bucket. It assumes that the zero threshold is at a bucket boundary and that
// the counts in the buckets to remove are already part of the zero count.
-func (h *FloatHistogram) trimBucketsInZeroBucket() {
+// c is a histogram holding the Kahan compensation term.
+func (h *FloatHistogram) trimBucketsInZeroBucket(c *FloatHistogram) {
i := h.PositiveBucketIterator()
bucketsIdx := 0
for i.Next() {
@@ -917,6 +1067,9 @@ func (h *FloatHistogram) trimBucketsInZeroBucket() {
break
}
h.PositiveBuckets[bucketsIdx] = 0
+ if c != nil {
+ c.PositiveBuckets[bucketsIdx] = 0
+ }
bucketsIdx++
}
i = h.NegativeBucketIterator()
@@ -927,34 +1080,46 @@ func (h *FloatHistogram) trimBucketsInZeroBucket() {
break
}
h.NegativeBuckets[bucketsIdx] = 0
+ if c != nil {
+ c.NegativeBuckets[bucketsIdx] = 0
+ }
bucketsIdx++
}
// We are abusing Compact to trim the buckets set to zero
// above. Premature compacting could cause additional cost, but this
// code path is probably rarely used anyway.
- h.Compact(0)
+ if c != nil {
+ h.kahanCompact(0, c)
+ } else {
+ h.Compact(0)
+ }
}
// reconcileZeroBuckets finds a zero bucket large enough to include the zero
// buckets of both histograms (the receiving histogram and the other histogram)
// with a zero threshold that is not within a populated bucket in either
-// histogram. This method modifies the receiving histogram accordingly, but
-// leaves the other histogram as is. Instead, it returns the zero count the
-// other histogram would have if it were modified.
-func (h *FloatHistogram) reconcileZeroBuckets(other *FloatHistogram) float64 {
- otherZeroCount := other.ZeroCount
+// histogram. This method modifies the receiving histogram accordingly, and
+// also modifies the compensation histogram `c` (used for Kahan summation) if provided,
+// but leaves the other histogram as is. Instead, it returns the zero count the
+// other histogram would have if it were modified, as well as its Kahan compensation term.
+func (h *FloatHistogram) reconcileZeroBuckets(other, c *FloatHistogram) (otherZeroCount, otherCZeroCount float64) {
+ otherZeroCount = other.ZeroCount
otherZeroThreshold := other.ZeroThreshold
for otherZeroThreshold != h.ZeroThreshold {
if h.ZeroThreshold > otherZeroThreshold {
- otherZeroCount, otherZeroThreshold = other.zeroCountForLargerThreshold(h.ZeroThreshold)
+ otherZeroCount, otherZeroThreshold, otherCZeroCount = other.zeroCountForLargerThreshold(h.ZeroThreshold, nil)
}
if otherZeroThreshold > h.ZeroThreshold {
- h.ZeroCount, h.ZeroThreshold = h.zeroCountForLargerThreshold(otherZeroThreshold)
- h.trimBucketsInZeroBucket()
+ var cZeroCount float64
+ h.ZeroCount, h.ZeroThreshold, cZeroCount = h.zeroCountForLargerThreshold(otherZeroThreshold, c)
+ if c != nil {
+ c.ZeroCount = cZeroCount
+ }
+ h.trimBucketsInZeroBucket(c)
}
}
- return otherZeroCount
+ return otherZeroCount, otherCZeroCount
}
// floatBucketIterator is a low-level constructor for bucket iterators.
@@ -1050,16 +1215,21 @@ func (i *floatBucketIterator) Next() bool {
if i.spansIdx >= len(i.spans) {
return false
}
+ span := i.spans[i.spansIdx]
if i.schema == i.targetSchema {
// Fast path for the common case.
- span := i.spans[i.spansIdx]
if i.bucketsIdx == 0 {
// Seed origIdx for the first bucket.
i.currIdx = span.Offset
} else {
i.currIdx++
}
+ if i.bucketsIdx >= len(i.buckets) {
+ // This protects against index out of range panic, which
+ // can only happen with an invalid histogram.
+ return false
+ }
for i.idxInSpan >= span.Length {
// We have exhausted the current span and have to find a new
@@ -1080,7 +1250,6 @@ func (i *floatBucketIterator) Next() bool {
// Copy all of these into local variables so that we can forward to the
// next bucket and then roll back if needed.
origIdx, spansIdx, idxInSpan := i.origIdx, i.spansIdx, i.idxInSpan
- span := i.spans[spansIdx]
firstPass := true
i.currCount = 0
@@ -1092,6 +1261,14 @@ func (i *floatBucketIterator) Next() bool {
} else {
origIdx++
}
+ if i.bucketsIdx >= len(i.buckets) {
+ // This protects against index out of range panic, which
+ // can only happen with an invalid histogram.
+ if firstPass {
+ return false
+ }
+ break mergeLoop
+ }
for idxInSpan >= span.Length {
// We have exhausted the current span and have to find a new
// one. We even handle pathologic spans of length 0 here.
@@ -1152,6 +1329,11 @@ func (i *reverseFloatBucketIterator) Next() bool {
// We have exhausted the current span and have to find a new
// one. We'll even handle pathologic spans of length 0.
i.spansIdx--
+ if i.spansIdx < 0 {
+ // This protects against index out of range panic, which
+ // can only happen with an invalid histogram.
+ return false
+ }
i.idxInSpan = int32(i.spans[i.spansIdx].Length) - 1
i.currIdx -= i.spans[i.spansIdx+1].Offset
}
@@ -1352,6 +1534,145 @@ func addBuckets(
return spansA, bucketsA
}
+// kahanAddBuckets works like addBuckets but it is used in FloatHistogram's KahanAdd method
+// and takes additional arguments, compensationBucketsA and compensationBucketsB,
+// which hold the Kahan compensation values associated with histograms A and B.
+// It returns the resulting spans/buckets and compensation buckets.
+func kahanAddBuckets(
+ schema int32, threshold float64, negative bool,
+ spansA []Span, bucketsA []float64,
+ spansB []Span, bucketsB []float64,
+ compensationBucketsA, compensationBucketsB []float64,
+) (newSpans []Span, newBucketsA, newBucketsC []float64) {
+ var (
+ iSpan = -1
+ iBucket = -1
+ iInSpan int32
+ indexA int32
+ indexB int32
+ bIdxB int
+ bucketB float64
+ compensationBucketB float64
+ deltaIndex int32
+ lowerThanThreshold = true
+ )
+
+ for _, spanB := range spansB {
+ indexB += spanB.Offset
+ for j := 0; j < int(spanB.Length); j++ {
+ if lowerThanThreshold && IsExponentialSchema(schema) && getBoundExponential(indexB, schema) <= threshold {
+ goto nextLoop
+ }
+ lowerThanThreshold = false
+
+ bucketB = bucketsB[bIdxB]
+ if compensationBucketsB != nil {
+ compensationBucketB = compensationBucketsB[bIdxB]
+ }
+ if negative {
+ bucketB *= -1
+ compensationBucketB *= -1
+ }
+
+ if iSpan == -1 {
+ if len(spansA) == 0 || spansA[0].Offset > indexB {
+ // Add bucket before all others.
+ bucketsA = append(bucketsA, 0)
+ copy(bucketsA[1:], bucketsA)
+ bucketsA[0] = bucketB
+ compensationBucketsA = append(compensationBucketsA, 0)
+ copy(compensationBucketsA[1:], compensationBucketsA)
+ compensationBucketsA[0] = compensationBucketB
+ if len(spansA) > 0 && spansA[0].Offset == indexB+1 {
+ spansA[0].Length++
+ spansA[0].Offset--
+ goto nextLoop
+ }
+ spansA = append(spansA, Span{})
+ copy(spansA[1:], spansA)
+ spansA[0] = Span{Offset: indexB, Length: 1}
+ if len(spansA) > 1 {
+ // Convert the absolute offset in the formerly
+ // first span to a relative offset.
+ spansA[1].Offset -= indexB + 1
+ }
+ goto nextLoop
+ } else if spansA[0].Offset == indexB {
+ // Just add to first bucket.
+ bucketsA[0], compensationBucketsA[0] = kahansum.Inc(bucketB, bucketsA[0], compensationBucketsA[0])
+ bucketsA[0], compensationBucketsA[0] = kahansum.Inc(compensationBucketB, bucketsA[0], compensationBucketsA[0])
+ goto nextLoop
+ }
+ iSpan, iBucket, iInSpan = 0, 0, 0
+ indexA = spansA[0].Offset
+ }
+ deltaIndex = indexB - indexA
+ for {
+ remainingInSpan := int32(spansA[iSpan].Length) - iInSpan
+ if deltaIndex < remainingInSpan {
+ // Bucket is in current span.
+ iBucket += int(deltaIndex)
+ iInSpan += deltaIndex
+ bucketsA[iBucket], compensationBucketsA[iBucket] = kahansum.Inc(bucketB, bucketsA[iBucket], compensationBucketsA[iBucket])
+ bucketsA[iBucket], compensationBucketsA[iBucket] = kahansum.Inc(compensationBucketB, bucketsA[iBucket], compensationBucketsA[iBucket])
+ break
+ }
+ deltaIndex -= remainingInSpan
+ iBucket += int(remainingInSpan)
+ iSpan++
+ if iSpan == len(spansA) || deltaIndex < spansA[iSpan].Offset {
+ // Bucket is in gap behind previous span (or there are no further spans).
+ bucketsA = append(bucketsA, 0)
+ copy(bucketsA[iBucket+1:], bucketsA[iBucket:])
+ bucketsA[iBucket] = bucketB
+ compensationBucketsA = append(compensationBucketsA, 0)
+ copy(compensationBucketsA[iBucket+1:], compensationBucketsA[iBucket:])
+ compensationBucketsA[iBucket] = compensationBucketB
+ switch {
+ case deltaIndex == 0:
+ // Directly after previous span, extend previous span.
+ if iSpan < len(spansA) {
+ spansA[iSpan].Offset--
+ }
+ iSpan--
+ iInSpan = int32(spansA[iSpan].Length)
+ spansA[iSpan].Length++
+ goto nextLoop
+ case iSpan < len(spansA) && deltaIndex == spansA[iSpan].Offset-1:
+ // Directly before next span, extend next span.
+ iInSpan = 0
+ spansA[iSpan].Offset--
+ spansA[iSpan].Length++
+ goto nextLoop
+ default:
+ // No next span, or next span is not directly adjacent to new bucket.
+ // Add new span.
+ iInSpan = 0
+ if iSpan < len(spansA) {
+ spansA[iSpan].Offset -= deltaIndex + 1
+ }
+ spansA = append(spansA, Span{})
+ copy(spansA[iSpan+1:], spansA[iSpan:])
+ spansA[iSpan] = Span{Length: 1, Offset: deltaIndex}
+ goto nextLoop
+ }
+ } else {
+ // Try start of next span.
+ deltaIndex -= spansA[iSpan].Offset
+ iInSpan = 0
+ }
+ }
+
+ nextLoop:
+ indexA = indexB
+ indexB++
+ bIdxB++
+ }
+ }
+
+ return spansA, bucketsA, compensationBucketsA
+}
+
// floatBucketsMatch compares bucket values of two float histograms using binary float comparison
// and returns true if all values match.
func floatBucketsMatch(b1, b2 []float64) bool {
@@ -1479,15 +1800,18 @@ func intersectCustomBucketBounds(boundsA, boundsB []float64) []float64 {
// addCustomBucketsWithMismatches handles adding/subtracting custom bucket histograms
// with mismatched bucket layouts by mapping both to an intersected layout.
+// It also processes the Kahan compensation term if provided.
func addCustomBucketsWithMismatches(
negative bool,
spansA []Span, bucketsA, boundsA []float64,
spansB []Span, bucketsB, boundsB []float64,
+ bucketsC []float64,
intersectedBounds []float64,
-) ([]Span, []float64) {
+) ([]Span, []float64, []float64) {
targetBuckets := make([]float64, len(intersectedBounds)+1)
+ cTargetBuckets := make([]float64, len(intersectedBounds)+1)
- mapBuckets := func(spans []Span, buckets, bounds []float64, negative bool) {
+ mapBuckets := func(spans []Span, buckets, bounds []float64, negative, withCompensation bool) {
srcIdx := 0
bucketIdx := 0
intersectIdx := 0
@@ -1513,9 +1837,12 @@ func addCustomBucketsWithMismatches(
}
if negative {
- targetBuckets[targetIdx] -= value
+ targetBuckets[targetIdx], cTargetBuckets[targetIdx] = kahansum.Dec(value, targetBuckets[targetIdx], cTargetBuckets[targetIdx])
} else {
- targetBuckets[targetIdx] += value
+ targetBuckets[targetIdx], cTargetBuckets[targetIdx] = kahansum.Inc(value, targetBuckets[targetIdx], cTargetBuckets[targetIdx])
+ if withCompensation && bucketsC != nil {
+ targetBuckets[targetIdx], cTargetBuckets[targetIdx] = kahansum.Inc(bucketsC[bucketIdx], targetBuckets[targetIdx], cTargetBuckets[targetIdx])
+ }
}
}
srcIdx++
@@ -1524,21 +1851,23 @@ func addCustomBucketsWithMismatches(
}
}
- // Map both histograms to the intersected layout.
- mapBuckets(spansA, bucketsA, boundsA, false)
- mapBuckets(spansB, bucketsB, boundsB, negative)
+ // Map histograms to the intersected layout.
+ mapBuckets(spansA, bucketsA, boundsA, false, true)
+ mapBuckets(spansB, bucketsB, boundsB, negative, false)
// Build spans and buckets, excluding zero-valued buckets from the final result.
- destSpans := spansA[:0] // Reuse spansA capacity for destSpans since we don't need it anymore.
- destBuckets := targetBuckets[:0] // Reuse targetBuckets capacity for destBuckets since it's guaranteed to be large enough.
+ destSpans := spansA[:0] // Reuse spansA capacity for destSpans since we don't need it anymore.
+ destBuckets := targetBuckets[:0] // Reuse targetBuckets capacity for destBuckets since it's guaranteed to be large enough.
+ cDestBuckets := cTargetBuckets[:0] // Reuse cTargetBuckets capacity for cDestBuckets since it's guaranteed to be large enough.
lastIdx := int32(-1)
- for i, count := range targetBuckets {
- if count == 0 {
+ for i := range targetBuckets {
+ if targetBuckets[i] == 0 && cTargetBuckets[i] == 0 {
continue
}
- destBuckets = append(destBuckets, count)
+ destBuckets = append(destBuckets, targetBuckets[i])
+ cDestBuckets = append(cDestBuckets, cTargetBuckets[i])
idx := int32(i)
if len(destSpans) > 0 && idx == lastIdx+1 {
@@ -1561,29 +1890,159 @@ func addCustomBucketsWithMismatches(
lastIdx = idx
}
- return destSpans, destBuckets
+ return destSpans, destBuckets, cDestBuckets
}
// ReduceResolution reduces the float histogram's spans, buckets into target schema.
-// The target schema must be smaller than the current float histogram's schema.
-// This will panic if the histogram has custom buckets or if the target schema is
-// a custom buckets schema.
-func (h *FloatHistogram) ReduceResolution(targetSchema int32) *FloatHistogram {
+// An error is returned in the following cases:
+// - The target schema is not smaller than the current histogram's schema.
+// - The histogram has custom buckets.
+// - The target schema is a custom buckets schema.
+// - Any spans have an invalid offset.
+// - The spans are inconsistent with the number of buckets.
+func (h *FloatHistogram) ReduceResolution(targetSchema int32) error {
+ // Note that the follow three returns are not returning a
+ // histogram.Error because they are programming errors.
if h.UsesCustomBuckets() {
- panic("cannot reduce resolution when there are custom buckets")
+ return errors.New("cannot reduce resolution when there are custom buckets")
}
if IsCustomBucketsSchema(targetSchema) {
- panic("cannot reduce resolution to custom buckets schema")
+ return errors.New("cannot reduce resolution to custom buckets schema")
}
if targetSchema >= h.Schema {
- panic(fmt.Errorf("cannot reduce resolution from schema %d to %d", h.Schema, targetSchema))
+ return fmt.Errorf("cannot reduce resolution from schema %d to %d", h.Schema, targetSchema)
}
- h.PositiveSpans, h.PositiveBuckets = reduceResolution(h.PositiveSpans, h.PositiveBuckets, h.Schema, targetSchema, false, true)
- h.NegativeSpans, h.NegativeBuckets = reduceResolution(h.NegativeSpans, h.NegativeBuckets, h.Schema, targetSchema, false, true)
+ var err error
+
+ if h.PositiveSpans, h.PositiveBuckets, err = reduceResolution(
+ h.PositiveSpans, h.PositiveBuckets, h.Schema, targetSchema, false, true,
+ ); err != nil {
+ return err
+ }
+ if h.NegativeSpans, h.NegativeBuckets, err = reduceResolution(
+ h.NegativeSpans, h.NegativeBuckets, h.Schema, targetSchema, false, true,
+ ); err != nil {
+ return err
+ }
h.Schema = targetSchema
- return h
+ return nil
+}
+
+// kahanReduceResolution works like reduceResolution, but it is specialized for FloatHistogram's KahanAdd method.
+// Unlike reduceResolution, which supports both float and integer buckets, this function only operates on float buckets.
+// It also takes an additional argument, originCompensationBuckets, representing the compensation buckets for the origin histogram.
+// Modifies both the origin histogram buckets and their associated compensation buckets.
+func kahanReduceResolution(
+ originSpans []Span,
+ originReceivingBuckets []float64,
+ originCompensationBuckets []float64,
+ originSchema,
+ targetSchema int32,
+ inplace bool,
+) (newSpans []Span, newReceivingBuckets, newCompensationBuckets []float64) {
+ var (
+ targetSpans []Span // The spans in the target schema.
+ targetReceivingBuckets []float64 // The receiving bucket counts in the target schema.
+ targetCompensationBuckets []float64 // The compensation bucket counts in the target schema.
+ bucketIdx int32 // The index of bucket in the origin schema.
+ bucketCountIdx int // The position of a bucket in origin bucket count slice `originBuckets`.
+ targetBucketIdx int32 // The index of bucket in the target schema.
+ lastTargetBucketIdx int32 // The index of the last added target bucket.
+ )
+
+ if inplace {
+ // Slice reuse is safe because when reducing the resolution,
+ // target slices don't grow faster than origin slices are being read.
+ targetSpans = originSpans[:0]
+ targetReceivingBuckets = originReceivingBuckets[:0]
+ targetCompensationBuckets = originCompensationBuckets[:0]
+ }
+
+ for _, span := range originSpans {
+ // Determine the index of the first bucket in this span.
+ bucketIdx += span.Offset
+ for j := 0; j < int(span.Length); j++ {
+ // Determine the index of the bucket in the target schema from the index in the original schema.
+ targetBucketIdx = targetIdx(bucketIdx, originSchema, targetSchema)
+
+ switch {
+ case len(targetSpans) == 0:
+ // This is the first span in the targetSpans.
+ span := Span{
+ Offset: targetBucketIdx,
+ Length: 1,
+ }
+ targetSpans = append(targetSpans, span)
+ targetReceivingBuckets = append(targetReceivingBuckets, originReceivingBuckets[bucketCountIdx])
+ lastTargetBucketIdx = targetBucketIdx
+ targetCompensationBuckets = append(targetCompensationBuckets, originCompensationBuckets[bucketCountIdx])
+
+ case lastTargetBucketIdx == targetBucketIdx:
+ // The current bucket has to be merged into the same target bucket as the previous bucket.
+ lastBucketIdx := len(targetReceivingBuckets) - 1
+ targetReceivingBuckets[lastBucketIdx], targetCompensationBuckets[lastBucketIdx] = kahansum.Inc(
+ originReceivingBuckets[bucketCountIdx],
+ targetReceivingBuckets[lastBucketIdx],
+ targetCompensationBuckets[lastBucketIdx],
+ )
+ targetReceivingBuckets[lastBucketIdx], targetCompensationBuckets[lastBucketIdx] = kahansum.Inc(
+ originCompensationBuckets[bucketCountIdx],
+ targetReceivingBuckets[lastBucketIdx],
+ targetCompensationBuckets[lastBucketIdx],
+ )
+
+ case (lastTargetBucketIdx + 1) == targetBucketIdx:
+ // The current bucket has to go into a new target bucket,
+ // and that bucket is next to the previous target bucket,
+ // so we add it to the current target span.
+ targetSpans[len(targetSpans)-1].Length++
+ lastTargetBucketIdx++
+ targetReceivingBuckets = append(targetReceivingBuckets, originReceivingBuckets[bucketCountIdx])
+ targetCompensationBuckets = append(targetCompensationBuckets, originCompensationBuckets[bucketCountIdx])
+
+ case (lastTargetBucketIdx + 1) < targetBucketIdx:
+ // The current bucket has to go into a new target bucket,
+ // and that bucket is separated by a gap from the previous target bucket,
+ // so we need to add a new target span.
+ span := Span{
+ Offset: targetBucketIdx - lastTargetBucketIdx - 1,
+ Length: 1,
+ }
+ targetSpans = append(targetSpans, span)
+ lastTargetBucketIdx = targetBucketIdx
+ targetReceivingBuckets = append(targetReceivingBuckets, originReceivingBuckets[bucketCountIdx])
+ targetCompensationBuckets = append(targetCompensationBuckets, originCompensationBuckets[bucketCountIdx])
+ }
+
+ bucketIdx++
+ bucketCountIdx++
+ }
+ }
+
+ return targetSpans, targetReceivingBuckets, targetCompensationBuckets
+}
+
+// newCompensationHistogram initializes a new compensation histogram that can be used
+// alongside the current FloatHistogram in Kahan summation.
+// The compensation histogram is structured to match the receiving histogram's bucket layout
+// including its schema, zero threshold and custom values, and it shares spans with the receiving
+// histogram. However, the bucket values in the compensation histogram are initialized to zero.
+func (h *FloatHistogram) newCompensationHistogram() *FloatHistogram {
+ c := &FloatHistogram{
+ CounterResetHint: h.CounterResetHint,
+ Schema: h.Schema,
+ ZeroThreshold: h.ZeroThreshold,
+ CustomValues: h.CustomValues,
+ PositiveBuckets: make([]float64, len(h.PositiveBuckets)),
+ PositiveSpans: h.PositiveSpans,
+ NegativeSpans: h.NegativeSpans,
+ }
+ if !h.UsesCustomBuckets() {
+ c.NegativeBuckets = make([]float64, len(h.NegativeBuckets))
+ }
+ return c
}
// checkSchemaAndBounds checks if two histograms are compatible because they
@@ -1627,3 +2086,27 @@ func (h *FloatHistogram) adjustCounterReset(other *FloatHistogram) (counterReset
}
return false
}
+
+// HasOverflow reports whether any of the FloatHistogram's fields contain an infinite value.
+// This can happen when aggregating multiple histograms and exceeding float64 capacity.
+func (h *FloatHistogram) HasOverflow() bool {
+ if math.IsInf(h.ZeroCount, 0) || math.IsInf(h.Count, 0) || math.IsInf(h.Sum, 0) {
+ return true
+ }
+ for _, v := range h.PositiveBuckets {
+ if math.IsInf(v, 0) {
+ return true
+ }
+ }
+ for _, v := range h.NegativeBuckets {
+ if math.IsInf(v, 0) {
+ return true
+ }
+ }
+ for _, v := range h.CustomValues {
+ if math.IsInf(v, 0) {
+ return true
+ }
+ }
+ return false
+}
diff --git a/model/histogram/float_histogram_test.go b/model/histogram/float_histogram_test.go
index 7454e9c77c..caf77b6256 100644
--- a/model/histogram/float_histogram_test.go
+++ b/model/histogram/float_histogram_test.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
@@ -2514,6 +2514,243 @@ func TestFloatHistogramAdd(t *testing.T) {
t.Run(c.name, func(t *testing.T) {
testHistogramAdd(t, c.in1, c.in2, c.expected, c.expErrMsg, c.expCounterResetCollision, c.expNHCBBoundsReconciled)
testHistogramAdd(t, c.in2, c.in1, c.expected, c.expErrMsg, c.expCounterResetCollision, c.expNHCBBoundsReconciled)
+ testHistogramKahanAdd(t, c.in1, nil, c.in2, c.expected, c.expErrMsg, c.expCounterResetCollision, c.expNHCBBoundsReconciled)
+ testHistogramKahanAdd(t, c.in2, nil, c.in1, c.expected, c.expErrMsg, c.expCounterResetCollision, c.expNHCBBoundsReconciled)
+ })
+ }
+}
+
+// TestKahanAddWithCompHistogram tests KahanAdd.
+// Test cases provide two float histograms and a compensation histogram with predefined values.
+func TestKahanAddWithCompHistogram(t *testing.T) {
+ cases := []struct {
+ name string
+ in1, comp, in2, expectedSum *FloatHistogram
+ expErrMsg string
+ expCounterResetCollision bool
+ expNHCBBoundsReconciled bool
+ }{
+ {
+ name: "larger zero bucket in first histogram",
+ in1: &FloatHistogram{
+ ZeroThreshold: 1,
+ ZeroCount: 17,
+ Count: 21,
+ Sum: 1.234,
+ PositiveSpans: []Span{{1, 2}, {0, 3}},
+ PositiveBuckets: []float64{2, 3, 6, 2, 5},
+ NegativeSpans: []Span{{4, 2}, {1, 2}},
+ NegativeBuckets: []float64{1, 1, 4, 4},
+ },
+ comp: &FloatHistogram{
+ ZeroThreshold: 1,
+ PositiveSpans: []Span{{1, 2}, {0, 3}},
+ PositiveBuckets: []float64{0.02, 0.03, 0.06, 0.02, 0.05},
+ NegativeSpans: []Span{{4, 2}, {1, 2}},
+ NegativeBuckets: []float64{0.01, 0.01, 0.04, 0.04},
+ },
+ in2: &FloatHistogram{
+ ZeroThreshold: 0.01,
+ ZeroCount: 11,
+ Count: 30,
+ Sum: 2.345,
+ PositiveSpans: []Span{{-2, 2}, {2, 3}},
+ PositiveBuckets: []float64{1, 0, 3, 4, 7},
+ NegativeSpans: []Span{{3, 2}, {3, 2}},
+ NegativeBuckets: []float64{3, 1, 5, 6},
+ },
+ expectedSum: &FloatHistogram{
+ ZeroThreshold: 1,
+ ZeroCount: 29,
+ Count: 51,
+ Sum: 3.579,
+ PositiveSpans: []Span{{1, 2}, {0, 3}},
+ PositiveBuckets: []float64{2.02, 6.03, 10.06, 9.02, 5.05},
+ NegativeSpans: []Span{{3, 3}, {1, 3}},
+ NegativeBuckets: []float64{3, 2.01, 1.01, 4.04, 9.04, 6},
+ },
+ expErrMsg: "",
+ expCounterResetCollision: false,
+ expNHCBBoundsReconciled: false,
+ },
+ {
+ name: "smaller zero bucket in first histogram",
+ in1: &FloatHistogram{
+ ZeroThreshold: 0.01,
+ ZeroCount: 11,
+ Count: 40,
+ Sum: 2.345,
+ PositiveSpans: []Span{{-2, 2}, {2, 3}},
+ PositiveBuckets: []float64{1, 2, 3, 4, 7},
+ NegativeSpans: []Span{{3, 2}, {3, 2}},
+ NegativeBuckets: []float64{3, 1, 5, 6},
+ },
+ comp: &FloatHistogram{
+ ZeroThreshold: 0.01,
+ ZeroCount: 0,
+ PositiveSpans: []Span{{-2, 2}, {2, 3}},
+ PositiveBuckets: []float64{0.02, 0.03, 0.06, 0.07, 0.05},
+ NegativeSpans: []Span{{3, 2}, {3, 2}},
+ NegativeBuckets: []float64{0.01, 0.01, 0.04, 0.04},
+ },
+ in2: &FloatHistogram{
+ ZeroThreshold: 1,
+ ZeroCount: 17,
+ Count: 11,
+ Sum: 1.234,
+ PositiveSpans: []Span{{1, 2}, {0, 3}},
+ PositiveBuckets: []float64{2, 3, 6, 2, 5},
+ NegativeSpans: []Span{{4, 2}, {1, 2}},
+ NegativeBuckets: []float64{1, 1, 4, 4},
+ },
+ expectedSum: &FloatHistogram{
+ ZeroThreshold: 1,
+ ZeroCount: 31.05,
+ Count: 51,
+ Sum: 3.579,
+ PositiveSpans: []Span{{1, 5}},
+ PositiveBuckets: []float64{2, 6.06, 10.07, 9.05, 5},
+ NegativeSpans: []Span{{3, 3}, {1, 3}},
+ NegativeBuckets: []float64{3.01, 2.01, 1, 4, 9.04, 6.04},
+ },
+ expErrMsg: "",
+ expCounterResetCollision: false,
+ expNHCBBoundsReconciled: false,
+ },
+ {
+ name: "first histogram contains zero buckets and Compact is called",
+ in1: &FloatHistogram{
+ ZeroThreshold: 0.01,
+ ZeroCount: 11,
+ Count: 30,
+ Sum: 2.345,
+ PositiveSpans: []Span{{-2, 2}, {1, 1}, {1, 3}},
+ PositiveBuckets: []float64{1, 3, 3, 0, 7, -6},
+ },
+ comp: &FloatHistogram{
+ ZeroThreshold: 0.01,
+ PositiveSpans: []Span{{-2, 2}, {1, 1}, {1, 3}},
+ PositiveBuckets: []float64{7, 2, 0.03, 0, 0.05, 0.06},
+ },
+ in2: &FloatHistogram{
+ ZeroThreshold: 1,
+ ZeroCount: 17,
+ Count: 21,
+ Sum: 1.234,
+ PositiveSpans: []Span{{1, 2}, {1, 2}},
+ PositiveBuckets: []float64{2, 3, 2, 5},
+ },
+ expectedSum: &FloatHistogram{
+ ZeroThreshold: 1,
+ ZeroCount: 41,
+ Count: 51,
+ Sum: 3.579,
+ PositiveSpans: []Span{{1, 2}, {1, 2}},
+ PositiveBuckets: []float64{5.03, 3, 9.05, -0.94},
+ },
+ expErrMsg: "",
+ expCounterResetCollision: false,
+ expNHCBBoundsReconciled: false,
+ },
+ {
+ name: "reduce resolution",
+ in1: &FloatHistogram{
+ Schema: 2,
+ ZeroThreshold: 0.01,
+ ZeroCount: 11,
+ Count: 30,
+ Sum: 2.345,
+ PositiveSpans: []Span{{-2, 2}, {1, 1}, {1, 3}},
+ PositiveBuckets: []float64{1, 3, 1e100, 0, 7, -6},
+ },
+ comp: &FloatHistogram{
+ Schema: 2,
+ ZeroThreshold: 0.01,
+ ZeroCount: 1,
+ PositiveSpans: []Span{{-2, 2}, {1, 1}, {1, 3}},
+ PositiveBuckets: []float64{7, 2, 0.03, 0, 0.05, 0.06},
+ },
+ in2: &FloatHistogram{
+ Schema: 1,
+ ZeroThreshold: 1,
+ ZeroCount: 17,
+ Count: 21,
+ Sum: 1.234,
+ PositiveSpans: []Span{{1, 2}, {1, 2}},
+ PositiveBuckets: []float64{-1e100, 3, 2, 5},
+ },
+ expectedSum: &FloatHistogram{
+ Schema: 1,
+ ZeroThreshold: 1,
+ ZeroCount: 42,
+ Count: 51,
+ Sum: 3.579,
+ PositiveSpans: []Span{{1, 5}},
+ PositiveBuckets: []float64{0.03, 10.05, -5.94, 2, 5},
+ },
+ expErrMsg: "",
+ expCounterResetCollision: false,
+ expNHCBBoundsReconciled: false,
+ },
+ {
+ name: "reduce resolution of 'other' histogram",
+ in1: &FloatHistogram{
+ Schema: 0,
+ ZeroThreshold: 1,
+ ZeroCount: 17,
+ Count: 21,
+ Sum: 1.234,
+ PositiveSpans: []Span{{1, 2}, {1, 2}},
+ PositiveBuckets: []float64{2, 3, 2, 5},
+ },
+ comp: &FloatHistogram{
+ Schema: 0,
+ ZeroThreshold: 1,
+ ZeroCount: 1,
+ PositiveSpans: []Span{{1, 2}, {1, 2}},
+ PositiveBuckets: []float64{17, 2, 0.03, 0},
+ },
+ in2: &FloatHistogram{
+ Schema: 2,
+ ZeroThreshold: 0.01,
+ ZeroCount: 11,
+ Count: 30,
+ Sum: 2.345,
+ PositiveSpans: []Span{{-2, 3}, {1, 1}, {1, 3}},
+ PositiveBuckets: []float64{1e100, 4.1, -1e100, 2.1, 0, 7, -6},
+ },
+ expectedSum: &FloatHistogram{
+ Schema: 0,
+ ZeroThreshold: 1,
+ ZeroCount: 33.1,
+ Count: 51,
+ Sum: 3.579,
+ PositiveSpans: []Span{{1, 2}, {1, 2}},
+ PositiveBuckets: []float64{21.1, 6, 2.03, 5},
+ },
+ expErrMsg: "",
+ expCounterResetCollision: false,
+ expNHCBBoundsReconciled: false,
+ },
+ {
+ name: "warn on counter reset hint collision",
+ in1: &FloatHistogram{
+ Schema: CustomBucketsSchema,
+ CounterResetHint: CounterReset,
+ },
+ in2: &FloatHistogram{
+ Schema: CustomBucketsSchema,
+ CounterResetHint: NotCounterReset,
+ },
+ expErrMsg: "",
+ expCounterResetCollision: true,
+ expNHCBBoundsReconciled: false,
+ },
+ }
+
+ for _, c := range cases {
+ t.Run(c.name, func(t *testing.T) {
+ testHistogramKahanAdd(t, c.in1, c.comp, c.in2, c.expectedSum, c.expErrMsg, c.expCounterResetCollision, c.expNHCBBoundsReconciled)
})
}
}
@@ -2557,6 +2794,68 @@ func testHistogramAdd(t *testing.T, a, b, expected *FloatHistogram, expErrMsg st
}
}
+func testHistogramKahanAdd(
+ t *testing.T, a, c, b, expectedSum *FloatHistogram, expErrMsg string, expCounterResetCollision, expNHCBBoundsReconciled bool,
+) {
+ var (
+ aCopy = a.Copy()
+ bCopy = b.Copy()
+ cCopy *FloatHistogram
+ expectedSumCopy *FloatHistogram
+ )
+
+ if c != nil {
+ cCopy = c.Copy()
+ }
+
+ if expectedSum != nil {
+ expectedSumCopy = expectedSum.Copy()
+ }
+
+ comp, counterResetCollision, nhcbBoundsReconciled, err := aCopy.KahanAdd(bCopy, cCopy)
+ if expErrMsg != "" {
+ require.EqualError(t, err, expErrMsg)
+ } else {
+ require.NoError(t, err)
+ }
+
+ var res *FloatHistogram
+ if comp != nil {
+ // Check that aCopy and its compensation histogram layouts match after addition.
+ require.Equal(t, aCopy.Schema, comp.Schema)
+ require.Equal(t, aCopy.ZeroThreshold, comp.ZeroThreshold)
+ require.Equal(t, aCopy.PositiveSpans, comp.PositiveSpans)
+ require.Equal(t, aCopy.NegativeSpans, comp.NegativeSpans)
+ require.Len(t, aCopy.CustomValues, len(comp.CustomValues))
+ require.Len(t, aCopy.PositiveBuckets, len(comp.PositiveBuckets))
+ require.Len(t, aCopy.NegativeBuckets, len(comp.NegativeBuckets))
+
+ res, _, _, err = aCopy.Add(comp)
+ if expErrMsg != "" {
+ require.EqualError(t, err, expErrMsg)
+ } else {
+ require.NoError(t, err)
+ }
+ }
+
+ // Check that the warnings are correct.
+ require.Equal(t, expCounterResetCollision, counterResetCollision)
+ require.Equal(t, expNHCBBoundsReconciled, nhcbBoundsReconciled)
+
+ if expectedSum != nil {
+ res.Compact(0)
+ expectedSumCopy.Compact(0)
+
+ require.Equal(t, expectedSumCopy, res)
+
+ // Has it also happened in-place?
+ require.Equal(t, expectedSumCopy, aCopy)
+
+ // Check that the argument was not mutated.
+ require.Equal(t, b, bCopy)
+ }
+}
+
func TestFloatHistogramSub(t *testing.T) {
// This has fewer test cases than TestFloatHistogramAdd because Add and
// Sub share most of the trickier code.
@@ -3344,6 +3643,84 @@ func TestAllReverseFloatBucketIterator(t *testing.T) {
includeZero: true,
includePos: true,
},
+ {
+ h: FloatHistogram{
+ Count: 405,
+ ZeroCount: 102,
+ ZeroThreshold: 0.001,
+ Sum: 1008.4,
+ Schema: 1,
+ PositiveSpans: []Span{
+ {Offset: 0, Length: 4},
+ {Offset: 1, Length: 0},
+ {Offset: 3, Length: 3},
+ {Offset: 3, Length: 0},
+ {Offset: 2, Length: 0},
+ {Offset: 5, Length: 3},
+ },
+ // Spans expect one more bucket than listed
+ // here. We mostly want to make sure here that
+ // no panic happens. Data is invalid anyway, so
+ // there is no real "correct" data anymore.
+ PositiveBuckets: []float64{100, 344, 123, 55, 3, 63, 2, 54, 235},
+ NegativeSpans: []Span{
+ {Offset: 0, Length: 3},
+ {Offset: 1, Length: 0},
+ {Offset: 3, Length: 0},
+ {Offset: 3, Length: 4},
+ {Offset: 2, Length: 0},
+ {Offset: 5, Length: 3},
+ },
+ // Spans expect one more bucket than listed
+ // here. We mostly want to make sure here that
+ // no panic happens. Data is invalid anyway, so
+ // there is no real "correct" data anymore.
+ NegativeBuckets: []float64{10, 34, 1230, 54, 67, 63, 2, 554, 235},
+ },
+ includeNeg: true,
+ includeZero: true,
+ includePos: true,
+ },
+ {
+ h: FloatHistogram{
+ Count: 447,
+ ZeroCount: 42,
+ ZeroThreshold: 0.6, // Within the bucket closest to zero.
+ Sum: 1008.4,
+ Schema: 0,
+ PositiveSpans: []Span{
+ {Offset: 0, Length: 4},
+ {Offset: 1, Length: 0},
+ {Offset: 3, Length: 3},
+ {Offset: 3, Length: 0},
+ {Offset: 2, Length: 0},
+ {Offset: 5, Length: 3},
+ },
+ // One more bucket listed here than expected by
+ // the spans. We mostly want to make sure here
+ // that no panic happens. Data is invalid
+ // anyway, so there is no real "correct" data
+ // anymore.
+ PositiveBuckets: []float64{100, 344, 123, 55, 3, 63, 2, 54, 235, 33, 42},
+ NegativeSpans: []Span{
+ {Offset: 0, Length: 3},
+ {Offset: 1, Length: 0},
+ {Offset: 3, Length: 0},
+ {Offset: 3, Length: 4},
+ {Offset: 2, Length: 0},
+ {Offset: 5, Length: 3},
+ },
+ // One more bucket listed here than expected by
+ // the spans. We mostly want to make sure here
+ // that no panic happens. Data is invalid
+ // anyway, so there is no real "correct" data
+ // anymore.
+ NegativeBuckets: []float64{10, 34, 1230, 54, 67, 63, 2, 554, 235, 33, 42},
+ },
+ includeNeg: true,
+ includeZero: true,
+ includePos: true,
+ },
}
for i, c := range cases {
@@ -3391,50 +3768,217 @@ func TestAllReverseFloatBucketIterator(t *testing.T) {
}
func TestFloatBucketIteratorTargetSchema(t *testing.T) {
- h := FloatHistogram{
- Count: 405,
- Sum: 1008.4,
- Schema: 1,
- PositiveSpans: []Span{
- {Offset: 0, Length: 4},
- {Offset: 1, Length: 3},
- {Offset: 2, Length: 3},
+ cases := map[string]struct {
+ h FloatHistogram
+ expPositiveBuckets []Bucket[float64]
+ expNegativeBuckets []Bucket[float64]
+ }{
+ "regular": {
+ h: FloatHistogram{
+ Count: 405,
+ Sum: 1008.4,
+ Schema: 1,
+ PositiveSpans: []Span{
+ {Offset: 0, Length: 4},
+ {Offset: 1, Length: 3},
+ {Offset: 2, Length: 3},
+ },
+ PositiveBuckets: []float64{100, 344, 123, 55, 3, 63, 2, 54, 235, 33},
+ NegativeSpans: []Span{
+ {Offset: 0, Length: 3},
+ {Offset: 7, Length: 4},
+ {Offset: 1, Length: 3},
+ },
+ NegativeBuckets: []float64{10, 34, 1230, 54, 67, 63, 2, 554, 235, 33},
+ },
+ expPositiveBuckets: []Bucket[float64]{
+ {Lower: 0.25, Upper: 1, LowerInclusive: false, UpperInclusive: true, Count: 100, Index: 0},
+ {Lower: 1, Upper: 4, LowerInclusive: false, UpperInclusive: true, Count: 522, Index: 1},
+ {Lower: 4, Upper: 16, LowerInclusive: false, UpperInclusive: true, Count: 68, Index: 2},
+ {Lower: 16, Upper: 64, LowerInclusive: false, UpperInclusive: true, Count: 322, Index: 3},
+ },
+ expNegativeBuckets: []Bucket[float64]{
+ {Lower: -1, Upper: -0.25, LowerInclusive: true, UpperInclusive: false, Count: 10, Index: 0},
+ {Lower: -4, Upper: -1, LowerInclusive: true, UpperInclusive: false, Count: 1264, Index: 1},
+ {Lower: -64, Upper: -16, LowerInclusive: true, UpperInclusive: false, Count: 184, Index: 3},
+ {Lower: -256, Upper: -64, LowerInclusive: true, UpperInclusive: false, Count: 791, Index: 4},
+ {Lower: -1024, Upper: -256, LowerInclusive: true, UpperInclusive: false, Count: 33, Index: 5},
+ },
},
- PositiveBuckets: []float64{100, 344, 123, 55, 3, 63, 2, 54, 235, 33},
- NegativeSpans: []Span{
- {Offset: 0, Length: 3},
- {Offset: 7, Length: 4},
- {Offset: 1, Length: 3},
+ "missing buckets": {
+ // One fewer bucket than expected based on spans. This
+ // can only happen with invalid histograms. We still
+ // want to handle it gracefully, essentially by
+ // considering the missing bucket as empty.
+ h: FloatHistogram{
+ Count: 405,
+ Sum: 1008.4,
+ Schema: 1,
+ PositiveSpans: []Span{
+ {Offset: 0, Length: 4},
+ {Offset: 1, Length: 3},
+ {Offset: 2, Length: 3},
+ },
+ PositiveBuckets: []float64{100, 344, 123, 55, 3, 63, 2, 54, 235},
+ NegativeSpans: []Span{
+ {Offset: 0, Length: 3},
+ {Offset: 7, Length: 4},
+ {Offset: 1, Length: 3},
+ },
+ NegativeBuckets: []float64{10, 34, 1230, 54, 67, 63, 2, 554, 235},
+ },
+ expPositiveBuckets: []Bucket[float64]{
+ {Lower: 0.25, Upper: 1, LowerInclusive: false, UpperInclusive: true, Count: 100, Index: 0},
+ {Lower: 1, Upper: 4, LowerInclusive: false, UpperInclusive: true, Count: 522, Index: 1},
+ {Lower: 4, Upper: 16, LowerInclusive: false, UpperInclusive: true, Count: 68, Index: 2},
+ {Lower: 16, Upper: 64, LowerInclusive: false, UpperInclusive: true, Count: 289, Index: 3},
+ },
+ expNegativeBuckets: []Bucket[float64]{
+ {Lower: -1, Upper: -0.25, LowerInclusive: true, UpperInclusive: false, Count: 10, Index: 0},
+ {Lower: -4, Upper: -1, LowerInclusive: true, UpperInclusive: false, Count: 1264, Index: 1},
+ {Lower: -64, Upper: -16, LowerInclusive: true, UpperInclusive: false, Count: 184, Index: 3},
+ {Lower: -256, Upper: -64, LowerInclusive: true, UpperInclusive: false, Count: 791, Index: 4},
+ },
+ },
+ "spurious bucket": {
+ // One more bucket than expected based on spans. This
+ // can only happen with invalid histograms. We still
+ // want to handle it gracefully, essentially by ignoring
+ // the spurious bucket.
+ h: FloatHistogram{
+ Count: 405,
+ Sum: 1008.4,
+ Schema: 1,
+ PositiveSpans: []Span{
+ {Offset: 0, Length: 4},
+ {Offset: 1, Length: 3},
+ {Offset: 2, Length: 3},
+ },
+ PositiveBuckets: []float64{100, 344, 123, 55, 3, 63, 2, 54, 235, 33, 42},
+ NegativeSpans: []Span{
+ {Offset: 0, Length: 3},
+ {Offset: 7, Length: 4},
+ {Offset: 1, Length: 3},
+ },
+ NegativeBuckets: []float64{10, 34, 1230, 54, 67, 63, 2, 554, 235, 33, 42},
+ },
+ expPositiveBuckets: []Bucket[float64]{
+ {Lower: 0.25, Upper: 1, LowerInclusive: false, UpperInclusive: true, Count: 100, Index: 0},
+ {Lower: 1, Upper: 4, LowerInclusive: false, UpperInclusive: true, Count: 522, Index: 1},
+ {Lower: 4, Upper: 16, LowerInclusive: false, UpperInclusive: true, Count: 68, Index: 2},
+ {Lower: 16, Upper: 64, LowerInclusive: false, UpperInclusive: true, Count: 322, Index: 3},
+ },
+ expNegativeBuckets: []Bucket[float64]{
+ {Lower: -1, Upper: -0.25, LowerInclusive: true, UpperInclusive: false, Count: 10, Index: 0},
+ {Lower: -4, Upper: -1, LowerInclusive: true, UpperInclusive: false, Count: 1264, Index: 1},
+ {Lower: -64, Upper: -16, LowerInclusive: true, UpperInclusive: false, Count: 184, Index: 3},
+ {Lower: -256, Upper: -64, LowerInclusive: true, UpperInclusive: false, Count: 791, Index: 4},
+ {Lower: -1024, Upper: -256, LowerInclusive: true, UpperInclusive: false, Count: 33, Index: 5},
+ },
+ },
+ "no schema change": {
+ h: FloatHistogram{
+ Count: 405,
+ Sum: 1008.4,
+ Schema: -1,
+ PositiveSpans: []Span{
+ {Offset: 0, Length: 2},
+ {Offset: 1, Length: 2},
+ },
+ PositiveBuckets: []float64{100, 522, 68, 322},
+ NegativeSpans: []Span{
+ {Offset: 0, Length: 2},
+ {Offset: 1, Length: 2},
+ },
+ NegativeBuckets: []float64{100, 522, 68, 322},
+ },
+ expPositiveBuckets: []Bucket[float64]{
+ {Lower: 0.25, Upper: 1, LowerInclusive: false, UpperInclusive: true, Count: 100, Index: 0},
+ {Lower: 1, Upper: 4, LowerInclusive: false, UpperInclusive: true, Count: 522, Index: 1},
+ {Lower: 16, Upper: 64, LowerInclusive: false, UpperInclusive: true, Count: 68, Index: 3},
+ {Lower: 64, Upper: 256, LowerInclusive: false, UpperInclusive: true, Count: 322, Index: 4},
+ },
+ expNegativeBuckets: []Bucket[float64]{
+ {Lower: -1, Upper: -0.25, LowerInclusive: true, UpperInclusive: false, Count: 100, Index: 0},
+ {Lower: -4, Upper: -1, LowerInclusive: true, UpperInclusive: false, Count: 522, Index: 1},
+ {Lower: -64, Upper: -16, LowerInclusive: true, UpperInclusive: false, Count: 68, Index: 3},
+ {Lower: -256, Upper: -64, LowerInclusive: true, UpperInclusive: false, Count: 322, Index: 4},
+ },
+ },
+ "no schema change, missing bucket": {
+ h: FloatHistogram{
+ Count: 405,
+ Sum: 1008.4,
+ Schema: -1,
+ PositiveSpans: []Span{
+ {Offset: 0, Length: 2},
+ {Offset: 1, Length: 2},
+ },
+ PositiveBuckets: []float64{100, 522, 68},
+ NegativeSpans: []Span{
+ {Offset: 0, Length: 2},
+ {Offset: 1, Length: 2},
+ },
+ NegativeBuckets: []float64{100, 522, 68},
+ },
+ expPositiveBuckets: []Bucket[float64]{
+ {Lower: 0.25, Upper: 1, LowerInclusive: false, UpperInclusive: true, Count: 100, Index: 0},
+ {Lower: 1, Upper: 4, LowerInclusive: false, UpperInclusive: true, Count: 522, Index: 1},
+ {Lower: 16, Upper: 64, LowerInclusive: false, UpperInclusive: true, Count: 68, Index: 3},
+ },
+ expNegativeBuckets: []Bucket[float64]{
+ {Lower: -1, Upper: -0.25, LowerInclusive: true, UpperInclusive: false, Count: 100, Index: 0},
+ {Lower: -4, Upper: -1, LowerInclusive: true, UpperInclusive: false, Count: 522, Index: 1},
+ {Lower: -64, Upper: -16, LowerInclusive: true, UpperInclusive: false, Count: 68, Index: 3},
+ },
+ },
+ "no schema change, spurious bucket": {
+ h: FloatHistogram{
+ Count: 405,
+ Sum: 1008.4,
+ Schema: -1,
+ PositiveSpans: []Span{
+ {Offset: 0, Length: 2},
+ {Offset: 1, Length: 2},
+ },
+ PositiveBuckets: []float64{100, 522, 68, 322, 42},
+ NegativeSpans: []Span{
+ {Offset: 0, Length: 2},
+ {Offset: 1, Length: 2},
+ },
+ NegativeBuckets: []float64{100, 522, 68, 322, 42},
+ },
+ expPositiveBuckets: []Bucket[float64]{
+ {Lower: 0.25, Upper: 1, LowerInclusive: false, UpperInclusive: true, Count: 100, Index: 0},
+ {Lower: 1, Upper: 4, LowerInclusive: false, UpperInclusive: true, Count: 522, Index: 1},
+ {Lower: 16, Upper: 64, LowerInclusive: false, UpperInclusive: true, Count: 68, Index: 3},
+ {Lower: 64, Upper: 256, LowerInclusive: false, UpperInclusive: true, Count: 322, Index: 4},
+ },
+ expNegativeBuckets: []Bucket[float64]{
+ {Lower: -1, Upper: -0.25, LowerInclusive: true, UpperInclusive: false, Count: 100, Index: 0},
+ {Lower: -4, Upper: -1, LowerInclusive: true, UpperInclusive: false, Count: 522, Index: 1},
+ {Lower: -64, Upper: -16, LowerInclusive: true, UpperInclusive: false, Count: 68, Index: 3},
+ {Lower: -256, Upper: -64, LowerInclusive: true, UpperInclusive: false, Count: 322, Index: 4},
+ },
},
- NegativeBuckets: []float64{10, 34, 1230, 54, 67, 63, 2, 554, 235, 33},
- }
- expPositiveBuckets := []Bucket[float64]{
- {Lower: 0.25, Upper: 1, LowerInclusive: false, UpperInclusive: true, Count: 100, Index: 0},
- {Lower: 1, Upper: 4, LowerInclusive: false, UpperInclusive: true, Count: 522, Index: 1},
- {Lower: 4, Upper: 16, LowerInclusive: false, UpperInclusive: true, Count: 68, Index: 2},
- {Lower: 16, Upper: 64, LowerInclusive: false, UpperInclusive: true, Count: 322, Index: 3},
- }
- expNegativeBuckets := []Bucket[float64]{
- {Lower: -1, Upper: -0.25, LowerInclusive: true, UpperInclusive: false, Count: 10, Index: 0},
- {Lower: -4, Upper: -1, LowerInclusive: true, UpperInclusive: false, Count: 1264, Index: 1},
- {Lower: -64, Upper: -16, LowerInclusive: true, UpperInclusive: false, Count: 184, Index: 3},
- {Lower: -256, Upper: -64, LowerInclusive: true, UpperInclusive: false, Count: 791, Index: 4},
- {Lower: -1024, Upper: -256, LowerInclusive: true, UpperInclusive: false, Count: 33, Index: 5},
}
+ for tn, tc := range cases {
+ t.Run(tn, func(t *testing.T) {
+ it := tc.h.floatBucketIterator(true, 0, -1)
+ for i, b := range tc.expPositiveBuckets {
+ require.True(t, it.Next(), "positive iterator exhausted too early")
+ require.Equal(t, b, it.At(), "bucket %d", i)
+ }
+ require.False(t, it.Next(), "positive iterator not exhausted")
- it := h.floatBucketIterator(true, 0, -1)
- for i, b := range expPositiveBuckets {
- require.True(t, it.Next(), "positive iterator exhausted too early")
- require.Equal(t, b, it.At(), "bucket %d", i)
+ it = tc.h.floatBucketIterator(false, 0, -1)
+ for i, b := range tc.expNegativeBuckets {
+ require.True(t, it.Next(), "negative iterator exhausted too early")
+ require.Equal(t, b, it.At(), "bucket %d", i)
+ }
+ require.False(t, it.Next(), "negative iterator not exhausted")
+ })
}
- require.False(t, it.Next(), "positive iterator not exhausted")
-
- it = h.floatBucketIterator(false, 0, -1)
- for i, b := range expNegativeBuckets {
- require.True(t, it.Next(), "negative iterator exhausted too early")
- require.Equal(t, b, it.At(), "bucket %d", i)
- }
- require.False(t, it.Next(), "negative iterator not exhausted")
}
func TestFloatCustomBucketsIterators(t *testing.T) {
@@ -3896,14 +4440,16 @@ func createRandomSpans(rng *rand.Rand, spanNum int32) ([]Span, []float64) {
func TestFloatHistogramReduceResolution(t *testing.T) {
tcs := map[string]struct {
- origin *FloatHistogram
- target *FloatHistogram
+ origin *FloatHistogram
+ targetSchema int32
+ target *FloatHistogram
+ errorMsg string
}{
"valid float histogram": {
origin: &FloatHistogram{
Schema: 0,
PositiveSpans: []Span{
- {Offset: 0, Length: 4},
+ {Offset: -2, Length: 4},
{Offset: 0, Length: 0},
{Offset: 3, Length: 2},
},
@@ -3915,10 +4461,11 @@ func TestFloatHistogramReduceResolution(t *testing.T) {
},
NegativeBuckets: []float64{1, 3, 1, 2, 1, 1},
},
+ targetSchema: -1,
target: &FloatHistogram{
Schema: -1,
PositiveSpans: []Span{
- {Offset: 0, Length: 3},
+ {Offset: -1, Length: 3},
{Offset: 1, Length: 1},
},
PositiveBuckets: []float64{1, 4, 2, 2},
@@ -3929,12 +4476,58 @@ func TestFloatHistogramReduceResolution(t *testing.T) {
NegativeBuckets: []float64{1, 4, 2, 2},
},
},
+ "not enough buckets": {
+ origin: &FloatHistogram{
+ Schema: 0,
+ PositiveSpans: []Span{
+ {Offset: -2, Length: 4},
+ {Offset: 0, Length: 0},
+ {Offset: 3, Length: 2},
+ },
+ PositiveBuckets: []float64{1, 3, 1, 2, 1},
+ },
+ targetSchema: -1,
+ errorMsg: "have 5 buckets but spans need more: histogram spans specify different number of buckets than provided",
+ },
+ "too many buckets": {
+ origin: &FloatHistogram{
+ Schema: 0,
+ PositiveSpans: []Span{
+ {Offset: -2, Length: 4},
+ {Offset: 0, Length: 0},
+ {Offset: 3, Length: 2},
+ },
+ PositiveBuckets: []float64{1, 3, 1, 2, 1, 1, 5},
+ },
+ targetSchema: -1,
+ errorMsg: "spans need 6 buckets, have 7 buckets: histogram spans specify different number of buckets than provided",
+ },
+ "negative offset": {
+ origin: &FloatHistogram{
+ Schema: 0,
+ PositiveSpans: []Span{
+ {Offset: -2, Length: 4},
+ {Offset: -1, Length: 0},
+ {Offset: 3, Length: 2},
+ },
+ PositiveBuckets: []float64{1, 3, 1, 2, 1, 1},
+ },
+ targetSchema: -1,
+ errorMsg: "span number 2 with offset -1: histogram has a span whose offset is negative",
+ },
}
- for _, tc := range tcs {
- target := tc.origin.ReduceResolution(tc.target.Schema)
- require.Equal(t, tc.target, target)
- // Check that receiver histogram was mutated:
- require.Equal(t, tc.target, tc.origin)
+ for tn, tc := range tcs {
+ t.Run(tn, func(t *testing.T) {
+ err := tc.origin.ReduceResolution(tc.targetSchema)
+ if tc.errorMsg != "" {
+ require.Equal(t, tc.errorMsg, err.Error())
+ // The returned error should be a histogram.Error.
+ require.ErrorAs(t, err, &Error{})
+ return
+ }
+ require.NoError(t, err)
+ require.Equal(t, tc.target, tc.origin)
+ })
}
}
diff --git a/model/histogram/generic.go b/model/histogram/generic.go
index cd385407d5..9ec9e9cd4b 100644
--- a/model/histogram/generic.go
+++ b/model/histogram/generic.go
@@ -1,4 +1,4 @@
-// Copyright 2022 The Prometheus Authors
+// 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
@@ -230,14 +230,29 @@ func (b *baseBucketIterator[BC, IBC]) strippedAt() strippedBucket[BC] {
// compactBuckets is a generic function used by both Histogram.Compact and
// FloatHistogram.Compact. Set deltaBuckets to true if the provided buckets are
// deltas. Set it to false if the buckets contain absolute counts.
-func compactBuckets[IBC InternalBucketCount](buckets []IBC, spans []Span, maxEmptyBuckets int, deltaBuckets bool) ([]IBC, []Span) {
+// For float histograms, deltaBuckets is always false.
+// primaryBuckets hold the main histogram values, while compensationBuckets (if provided) store
+// Kahan compensation values. compensationBuckets can only be provided for float histograms
+// and are processed in parallel with primaryBuckets to maintain synchronization.
+func compactBuckets[IBC InternalBucketCount](
+ primaryBuckets []IBC, compensationBuckets []float64,
+ spans []Span, maxEmptyBuckets int, deltaBuckets bool,
+) (updatedPrimaryBuckets []IBC, updatedCompensationBuckets []float64, updatedSpans []Span) {
+ if deltaBuckets && compensationBuckets != nil {
+ panic("histogram type mismatch: deltaBuckets cannot be true when compensationBuckets is provided")
+ } else if compensationBuckets != nil && len(primaryBuckets) != len(compensationBuckets) {
+ panic(fmt.Errorf(
+ "primary buckets layout (%v) mismatch against associated compensation buckets layout (%v)",
+ primaryBuckets, compensationBuckets),
+ )
+ }
// Fast path: If there are no empty buckets AND no offset in any span is
// <= maxEmptyBuckets AND no span has length 0, there is nothing to do and we can return
// immediately. We check that first because it's cheap and presumably
// common.
nothingToDo := true
var currentBucketAbsolute IBC
- for _, bucket := range buckets {
+ for _, bucket := range primaryBuckets {
if deltaBuckets {
currentBucketAbsolute += bucket
} else {
@@ -256,7 +271,7 @@ func compactBuckets[IBC InternalBucketCount](buckets []IBC, spans []Span, maxEmp
}
}
if nothingToDo {
- return buckets, spans
+ return primaryBuckets, compensationBuckets, spans
}
}
@@ -268,12 +283,19 @@ func compactBuckets[IBC InternalBucketCount](buckets []IBC, spans []Span, maxEmp
emptyBucketsHere := func() int {
i := 0
abs := currentBucketAbsolute
- for uint32(i)+posInSpan < spans[iSpan].Length && abs == 0 {
+ comp := float64(0)
+ if compensationBuckets != nil {
+ comp = compensationBuckets[iBucket]
+ }
+ for uint32(i)+posInSpan < spans[iSpan].Length && abs == 0 && comp == 0 {
i++
- if i+iBucket >= len(buckets) {
+ if i+iBucket >= len(primaryBuckets) {
break
}
- abs = buckets[i+iBucket]
+ abs = primaryBuckets[i+iBucket]
+ if compensationBuckets != nil {
+ comp = compensationBuckets[i+iBucket]
+ }
}
return i
}
@@ -313,11 +335,11 @@ func compactBuckets[IBC InternalBucketCount](buckets []IBC, spans []Span, maxEmp
// Cut out empty buckets from start and end of spans, no matter
// what. Also cut out empty buckets from the middle of a span but only
// if there are more than maxEmptyBuckets consecutive empty buckets.
- for iBucket < len(buckets) {
+ for iBucket < len(primaryBuckets) {
if deltaBuckets {
- currentBucketAbsolute += buckets[iBucket]
+ currentBucketAbsolute += primaryBuckets[iBucket]
} else {
- currentBucketAbsolute = buckets[iBucket]
+ currentBucketAbsolute = primaryBuckets[iBucket]
}
if nEmpty := emptyBucketsHere(); nEmpty > 0 {
if posInSpan > 0 &&
@@ -334,11 +356,14 @@ func compactBuckets[IBC InternalBucketCount](buckets []IBC, spans []Span, maxEmp
continue
}
// In all other cases, we cut out the empty buckets.
- if deltaBuckets && iBucket+nEmpty < len(buckets) {
- currentBucketAbsolute = -buckets[iBucket]
- buckets[iBucket+nEmpty] += buckets[iBucket]
+ if deltaBuckets && iBucket+nEmpty < len(primaryBuckets) {
+ currentBucketAbsolute = -primaryBuckets[iBucket]
+ primaryBuckets[iBucket+nEmpty] += primaryBuckets[iBucket]
+ }
+ primaryBuckets = append(primaryBuckets[:iBucket], primaryBuckets[iBucket+nEmpty:]...)
+ if compensationBuckets != nil {
+ compensationBuckets = append(compensationBuckets[:iBucket], compensationBuckets[iBucket+nEmpty:]...)
}
- buckets = append(buckets[:iBucket], buckets[iBucket+nEmpty:]...)
if posInSpan == 0 {
// Start of span.
if nEmpty == int(spans[iSpan].Length) {
@@ -388,8 +413,8 @@ func compactBuckets[IBC InternalBucketCount](buckets []IBC, spans []Span, maxEmp
iSpan++
}
}
- if maxEmptyBuckets == 0 || len(buckets) == 0 {
- return buckets, spans
+ if maxEmptyBuckets == 0 || len(primaryBuckets) == 0 {
+ return primaryBuckets, compensationBuckets, spans
}
// Finally, check if any offsets between spans are small enough to merge
@@ -397,7 +422,7 @@ func compactBuckets[IBC InternalBucketCount](buckets []IBC, spans []Span, maxEmp
iBucket = int(spans[0].Length)
if deltaBuckets {
currentBucketAbsolute = 0
- for _, bucket := range buckets[:iBucket] {
+ for _, bucket := range primaryBuckets[:iBucket] {
currentBucketAbsolute += bucket
}
}
@@ -406,7 +431,7 @@ func compactBuckets[IBC InternalBucketCount](buckets []IBC, spans []Span, maxEmp
if int(spans[iSpan].Offset) > maxEmptyBuckets {
l := int(spans[iSpan].Length)
if deltaBuckets {
- for _, bucket := range buckets[iBucket : iBucket+l] {
+ for _, bucket := range primaryBuckets[iBucket : iBucket+l] {
currentBucketAbsolute += bucket
}
}
@@ -418,22 +443,28 @@ func compactBuckets[IBC InternalBucketCount](buckets []IBC, spans []Span, maxEmp
offset := int(spans[iSpan].Offset)
spans[iSpan-1].Length += uint32(offset) + spans[iSpan].Length
spans = append(spans[:iSpan], spans[iSpan+1:]...)
- newBuckets := make([]IBC, len(buckets)+offset)
- copy(newBuckets, buckets[:iBucket])
- copy(newBuckets[iBucket+offset:], buckets[iBucket:])
+ newPrimaryBuckets := make([]IBC, len(primaryBuckets)+offset)
+ copy(newPrimaryBuckets, primaryBuckets[:iBucket])
+ copy(newPrimaryBuckets[iBucket+offset:], primaryBuckets[iBucket:])
if deltaBuckets {
- newBuckets[iBucket] = -currentBucketAbsolute
- newBuckets[iBucket+offset] += currentBucketAbsolute
+ newPrimaryBuckets[iBucket] = -currentBucketAbsolute
+ newPrimaryBuckets[iBucket+offset] += currentBucketAbsolute
+ }
+ primaryBuckets = newPrimaryBuckets
+ if compensationBuckets != nil {
+ newCompensationBuckets := make([]float64, len(compensationBuckets)+offset)
+ copy(newCompensationBuckets, compensationBuckets[:iBucket])
+ copy(newCompensationBuckets[iBucket+offset:], compensationBuckets[iBucket:])
+ compensationBuckets = newCompensationBuckets
}
iBucket += offset
- buckets = newBuckets
- currentBucketAbsolute = buckets[iBucket]
+ currentBucketAbsolute = primaryBuckets[iBucket]
// Note that with many merges, it would be more efficient to
// first record all the chunks of empty buckets to insert and
// then do it in one go through all the buckets.
}
- return buckets, spans
+ return primaryBuckets, compensationBuckets, spans
}
func checkHistogramSpans(spans []Span, numBuckets int) error {
@@ -738,6 +769,8 @@ var exponentialBounds = [][]float64{
// deltas. Set it to false if the buckets contain absolute counts.
// Set inplace to true to reuse input slices and avoid allocations (otherwise
// new slices will be allocated for result).
+// The functions returns an error if there are too many or too few buckets for the spans
+// or if any span except the first has a negative offset.
func reduceResolution[IBC InternalBucketCount](
originSpans []Span,
originBuckets []IBC,
@@ -745,7 +778,7 @@ func reduceResolution[IBC InternalBucketCount](
targetSchema int32,
deltaBuckets bool,
inplace bool,
-) ([]Span, []IBC) {
+) ([]Span, []IBC, error) {
var (
targetSpans []Span // The spans in the target schema.
targetBuckets []IBC // The bucket counts in the target schema.
@@ -764,10 +797,18 @@ func reduceResolution[IBC InternalBucketCount](
targetBuckets = originBuckets[:0]
}
- for _, span := range originSpans {
+ for n, span := range originSpans {
+ if n > 0 && span.Offset < 0 {
+ return nil, nil, fmt.Errorf("span number %d with offset %d: %w", n+1, span.Offset, ErrHistogramSpanNegativeOffset)
+ }
// Determine the index of the first bucket in this span.
bucketIdx += span.Offset
for j := 0; j < int(span.Length); j++ {
+ // Protect against too few buckets in the origin.
+ if bucketCountIdx >= len(originBuckets) {
+ return nil, nil, fmt.Errorf("have %d buckets but spans need more: %w", len(originBuckets), ErrHistogramSpansBucketsMismatch)
+ }
+
// Determine the index of the bucket in the target schema from the index in the original schema.
targetBucketIdx = targetIdx(bucketIdx, originSchema, targetSchema)
@@ -826,12 +867,33 @@ func reduceResolution[IBC InternalBucketCount](
targetBuckets = append(targetBuckets, originBuckets[bucketCountIdx])
}
}
-
bucketIdx++
bucketCountIdx++
}
}
+ if bucketCountIdx != len(originBuckets) {
+ return nil, nil, fmt.Errorf("spans need %d buckets, have %d buckets: %w", bucketCountIdx, len(originBuckets), ErrHistogramSpansBucketsMismatch)
+ }
+ return targetSpans, targetBuckets, nil
+}
+// mustReduceResolution works like reduceResolution, but panics instead of
+// returning an error. Use mustReduceResolution if you are sure that the spans
+// and buckets are valid.
+func mustReduceResolution[IBC InternalBucketCount](
+ originSpans []Span,
+ originBuckets []IBC,
+ originSchema,
+ targetSchema int32,
+ deltaBuckets bool,
+ inplace bool,
+) ([]Span, []IBC) {
+ targetSpans, targetBuckets, err := reduceResolution(
+ originSpans, originBuckets, originSchema, targetSchema, deltaBuckets, inplace,
+ )
+ if err != nil {
+ panic(err)
+ }
return targetSpans, targetBuckets
}
diff --git a/model/histogram/generic_test.go b/model/histogram/generic_test.go
index 1651830e9d..525c731571 100644
--- a/model/histogram/generic_test.go
+++ b/model/histogram/generic_test.go
@@ -1,4 +1,4 @@
-// Copyright 2022 The Prometheus Authors
+// 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
@@ -142,7 +142,7 @@ func TestReduceResolutionHistogram(t *testing.T) {
for _, tc := range cases {
spansCopy, bucketsCopy := slices.Clone(tc.spans), slices.Clone(tc.buckets)
- spans, buckets := reduceResolution(tc.spans, tc.buckets, tc.schema, tc.targetSchema, true, false)
+ spans, buckets := mustReduceResolution(tc.spans, tc.buckets, tc.schema, tc.targetSchema, true, false)
require.Equal(t, tc.expectedSpans, spans)
require.Equal(t, tc.expectedBuckets, buckets)
// Verify inputs were not mutated:
@@ -151,7 +151,7 @@ func TestReduceResolutionHistogram(t *testing.T) {
// Output slices reuse input slices:
const inplace = true
- spans, buckets = reduceResolution(tc.spans, tc.buckets, tc.schema, tc.targetSchema, true, inplace)
+ spans, buckets = mustReduceResolution(tc.spans, tc.buckets, tc.schema, tc.targetSchema, true, inplace)
require.Equal(t, tc.expectedSpans, spans)
require.Equal(t, tc.expectedBuckets, buckets)
// Verify inputs were mutated which is now expected:
@@ -190,7 +190,7 @@ func TestReduceResolutionFloatHistogram(t *testing.T) {
for _, tc := range cases {
spansCopy, bucketsCopy := slices.Clone(tc.spans), slices.Clone(tc.buckets)
- spans, buckets := reduceResolution(tc.spans, tc.buckets, tc.schema, tc.targetSchema, false, false)
+ spans, buckets := mustReduceResolution(tc.spans, tc.buckets, tc.schema, tc.targetSchema, false, false)
require.Equal(t, tc.expectedSpans, spans)
require.Equal(t, tc.expectedBuckets, buckets)
// Verify inputs were not mutated:
@@ -199,7 +199,7 @@ func TestReduceResolutionFloatHistogram(t *testing.T) {
// Output slices reuse input slices:
const inplace = true
- spans, buckets = reduceResolution(tc.spans, tc.buckets, tc.schema, tc.targetSchema, false, inplace)
+ spans, buckets = mustReduceResolution(tc.spans, tc.buckets, tc.schema, tc.targetSchema, false, inplace)
require.Equal(t, tc.expectedSpans, spans)
require.Equal(t, tc.expectedBuckets, buckets)
// Verify inputs were mutated which is now expected:
diff --git a/model/histogram/histogram.go b/model/histogram/histogram.go
index a7d9ce80f0..6ed02aed57 100644
--- a/model/histogram/histogram.go
+++ b/model/histogram/histogram.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
@@ -14,6 +14,7 @@
package histogram
import (
+ "errors"
"fmt"
"math"
"slices"
@@ -246,7 +247,7 @@ func (h *Histogram) CumulativeBucketIterator() BucketIterator[uint64] {
// supposed to be used according to the schema.
func (h *Histogram) Equals(h2 *Histogram) bool {
if h2 == nil {
- return false
+ return h == nil
}
if h.Schema != h2.Schema || h.Count != h2.Count ||
@@ -348,11 +349,11 @@ func allEmptySpans(s []Span) bool {
// Compact works like FloatHistogram.Compact. See there for detailed
// explanations.
func (h *Histogram) Compact(maxEmptyBuckets int) *Histogram {
- h.PositiveBuckets, h.PositiveSpans = compactBuckets(
- h.PositiveBuckets, h.PositiveSpans, maxEmptyBuckets, true,
+ h.PositiveBuckets, _, h.PositiveSpans = compactBuckets(
+ h.PositiveBuckets, nil, h.PositiveSpans, maxEmptyBuckets, true,
)
- h.NegativeBuckets, h.NegativeSpans = compactBuckets(
- h.NegativeBuckets, h.NegativeSpans, maxEmptyBuckets, true,
+ h.NegativeBuckets, _, h.NegativeSpans = compactBuckets(
+ h.NegativeBuckets, nil, h.NegativeSpans, maxEmptyBuckets, true,
)
return h
}
@@ -515,6 +516,11 @@ func (r *regularBucketIterator) Next() bool {
r.currIdx += span.Offset
}
+ // This protects against index out of range panic, which
+ // can only happen with an invalid histogram.
+ if r.bucketsIdx >= len(r.buckets) {
+ return false
+ }
r.currCount += r.buckets[r.bucketsIdx]
r.idxInSpan++
r.bucketsIdx++
@@ -576,6 +582,11 @@ func (c *cumulativeBucketIterator) Next() bool {
c.initialized = true
}
+ // This protects against index out of range panic, which
+ // can only happen with an invalid histogram.
+ if c.posBucketsIdx >= len(c.h.PositiveBuckets) {
+ return false
+ }
c.currCount += c.h.PositiveBuckets[c.posBucketsIdx]
c.currCumulativeCount += uint64(c.currCount)
c.currUpper = getBound(c.currIdx, c.h.Schema, c.h.CustomValues)
@@ -607,26 +618,37 @@ func (c *cumulativeBucketIterator) At() Bucket[uint64] {
}
// ReduceResolution reduces the histogram's spans, buckets into target schema.
-// The target schema must be smaller than the current histogram's schema.
-// This will panic if the histogram has custom buckets or if the target schema is
-// a custom buckets schema.
-func (h *Histogram) ReduceResolution(targetSchema int32) *Histogram {
+// An error is returned in the following cases:
+// - The target schema is not smaller than the current histogram's schema.
+// - The histogram has custom buckets.
+// - The target schema is a custom buckets schema.
+// - Any spans have an invalid offset.
+// - The spans are inconsistent with the number of buckets.
+func (h *Histogram) ReduceResolution(targetSchema int32) error {
+ // Note that the follow three returns are not returning a
+ // histogram.Error because they are programming errors.
if h.UsesCustomBuckets() {
- panic("cannot reduce resolution when there are custom buckets")
+ return errors.New("cannot reduce resolution when there are custom buckets")
}
if IsCustomBucketsSchema(targetSchema) {
- panic("cannot reduce resolution to custom buckets schema")
+ return errors.New("cannot reduce resolution to custom buckets schema")
}
if targetSchema >= h.Schema {
- panic(fmt.Errorf("cannot reduce resolution from schema %d to %d", h.Schema, targetSchema))
+ return fmt.Errorf("cannot reduce resolution from schema %d to %d", h.Schema, targetSchema)
}
- h.PositiveSpans, h.PositiveBuckets = reduceResolution(
+ var err error
+
+ if h.PositiveSpans, h.PositiveBuckets, err = reduceResolution(
h.PositiveSpans, h.PositiveBuckets, h.Schema, targetSchema, true, true,
- )
- h.NegativeSpans, h.NegativeBuckets = reduceResolution(
+ ); err != nil {
+ return err
+ }
+ if h.NegativeSpans, h.NegativeBuckets, err = reduceResolution(
h.NegativeSpans, h.NegativeBuckets, h.Schema, targetSchema, true, true,
- )
+ ); err != nil {
+ return err
+ }
h.Schema = targetSchema
- return h
+ return nil
}
diff --git a/model/histogram/histogram_test.go b/model/histogram/histogram_test.go
index d65049c68c..a2b4c7c0a8 100644
--- a/model/histogram/histogram_test.go
+++ b/model/histogram/histogram_test.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
@@ -243,6 +243,46 @@ func TestCumulativeBucketIterator(t *testing.T) {
{Lower: math.Inf(-1), Upper: math.Inf(1), Count: 5, LowerInclusive: true, UpperInclusive: true, Index: 4},
},
},
+ {
+ histogram: Histogram{
+ Schema: 0,
+ PositiveSpans: []Span{
+ {Offset: 0, Length: 2},
+ {Offset: 1, Length: 2},
+ },
+ // One spurious bucket, which we expect to be ignored.
+ PositiveBuckets: []int64{1, 1, -1, 0, 2},
+ },
+ expectedBuckets: []Bucket[uint64]{
+ {Lower: math.Inf(-1), Upper: 1, Count: 1, LowerInclusive: true, UpperInclusive: true, Index: 0},
+ {Lower: math.Inf(-1), Upper: 2, Count: 3, LowerInclusive: true, UpperInclusive: true, Index: 1},
+
+ {Lower: math.Inf(-1), Upper: 4, Count: 3, LowerInclusive: true, UpperInclusive: true, Index: 2},
+
+ {Lower: math.Inf(-1), Upper: 8, Count: 4, LowerInclusive: true, UpperInclusive: true, Index: 3},
+ {Lower: math.Inf(-1), Upper: 16, Count: 5, LowerInclusive: true, UpperInclusive: true, Index: 4},
+ },
+ },
+ {
+ histogram: Histogram{
+ Schema: 0,
+ PositiveSpans: []Span{
+ {Offset: 0, Length: 2},
+ {Offset: 1, Length: 3},
+ },
+ // One bucket is missing. We expect the iteration to end with the last bucket.
+ PositiveBuckets: []int64{1, 1, -1, 0},
+ },
+ expectedBuckets: []Bucket[uint64]{
+ {Lower: math.Inf(-1), Upper: 1, Count: 1, LowerInclusive: true, UpperInclusive: true, Index: 0},
+ {Lower: math.Inf(-1), Upper: 2, Count: 3, LowerInclusive: true, UpperInclusive: true, Index: 1},
+
+ {Lower: math.Inf(-1), Upper: 4, Count: 3, LowerInclusive: true, UpperInclusive: true, Index: 2},
+
+ {Lower: math.Inf(-1), Upper: 8, Count: 4, LowerInclusive: true, UpperInclusive: true, Index: 3},
+ {Lower: math.Inf(-1), Upper: 16, Count: 5, LowerInclusive: true, UpperInclusive: true, Index: 4},
+ },
+ },
}
for i, c := range cases {
@@ -459,6 +499,46 @@ func TestRegularBucketIterator(t *testing.T) {
},
expectedNegativeBuckets: []Bucket[uint64]{},
},
+ {
+ histogram: Histogram{
+ Schema: 0,
+ NegativeSpans: []Span{
+ {Offset: 0, Length: 5},
+ {Offset: 1, Length: 1},
+ },
+ // One spurious bucket, which we expect to be ignored.
+ NegativeBuckets: []int64{1, 2, -2, 1, -1, 0, 3},
+ },
+ expectedPositiveBuckets: []Bucket[uint64]{},
+ expectedNegativeBuckets: []Bucket[uint64]{
+ {Lower: -1, Upper: -0.5, Count: 1, LowerInclusive: true, UpperInclusive: false, Index: 0},
+ {Lower: -2, Upper: -1, Count: 3, LowerInclusive: true, UpperInclusive: false, Index: 1},
+ {Lower: -4, Upper: -2, Count: 1, LowerInclusive: true, UpperInclusive: false, Index: 2},
+ {Lower: -8, Upper: -4, Count: 2, LowerInclusive: true, UpperInclusive: false, Index: 3},
+ {Lower: -16, Upper: -8, Count: 1, LowerInclusive: true, UpperInclusive: false, Index: 4},
+
+ {Lower: -64, Upper: -32, Count: 1, LowerInclusive: true, UpperInclusive: false, Index: 6},
+ },
+ },
+ {
+ histogram: Histogram{
+ Schema: 0,
+ NegativeSpans: []Span{
+ {Offset: 0, Length: 5},
+ {Offset: 1, Length: 1},
+ },
+ // One bucket is missing. We expect the iteration to end with the last bucket.
+ NegativeBuckets: []int64{1, 2, -2, 1, -1},
+ },
+ expectedPositiveBuckets: []Bucket[uint64]{},
+ expectedNegativeBuckets: []Bucket[uint64]{
+ {Lower: -1, Upper: -0.5, Count: 1, LowerInclusive: true, UpperInclusive: false, Index: 0},
+ {Lower: -2, Upper: -1, Count: 3, LowerInclusive: true, UpperInclusive: false, Index: 1},
+ {Lower: -4, Upper: -2, Count: 1, LowerInclusive: true, UpperInclusive: false, Index: 2},
+ {Lower: -8, Upper: -4, Count: 2, LowerInclusive: true, UpperInclusive: false, Index: 3},
+ {Lower: -16, Upper: -8, Count: 1, LowerInclusive: true, UpperInclusive: false, Index: 4},
+ },
+ },
}
for i, c := range cases {
@@ -1639,14 +1719,16 @@ func BenchmarkHistogramValidation(b *testing.B) {
func TestHistogramReduceResolution(t *testing.T) {
tcs := map[string]struct {
- origin *Histogram
- target *Histogram
+ origin *Histogram
+ targetSchema int32
+ target *Histogram
+ errorMsg string
}{
"valid histogram": {
origin: &Histogram{
Schema: 0,
PositiveSpans: []Span{
- {Offset: 0, Length: 4},
+ {Offset: -2, Length: 4},
{Offset: 0, Length: 0},
{Offset: 3, Length: 2},
},
@@ -1658,10 +1740,11 @@ func TestHistogramReduceResolution(t *testing.T) {
},
NegativeBuckets: []int64{1, 2, -2, 1, -1, 0},
},
+ targetSchema: -1,
target: &Histogram{
Schema: -1,
PositiveSpans: []Span{
- {Offset: 0, Length: 3},
+ {Offset: -1, Length: 3},
{Offset: 1, Length: 1},
},
PositiveBuckets: []int64{1, 3, -2, 0},
@@ -1672,12 +1755,58 @@ func TestHistogramReduceResolution(t *testing.T) {
NegativeBuckets: []int64{1, 3, -2, 0},
},
},
+ "not enough buckets": {
+ origin: &Histogram{
+ Schema: 0,
+ PositiveSpans: []Span{
+ {Offset: -2, Length: 4},
+ {Offset: 0, Length: 0},
+ {Offset: 3, Length: 2},
+ },
+ PositiveBuckets: []int64{1, 2, -2, 1, -1},
+ },
+ targetSchema: -1,
+ errorMsg: "have 5 buckets but spans need more: histogram spans specify different number of buckets than provided",
+ },
+ "too many buckets": {
+ origin: &Histogram{
+ Schema: 0,
+ PositiveSpans: []Span{
+ {Offset: -2, Length: 4},
+ {Offset: 0, Length: 0},
+ {Offset: 3, Length: 2},
+ },
+ PositiveBuckets: []int64{1, 2, -2, 1, -1, 0, 3},
+ },
+ targetSchema: -1,
+ errorMsg: "spans need 6 buckets, have 7 buckets: histogram spans specify different number of buckets than provided",
+ },
+ "negative offset": {
+ origin: &Histogram{
+ Schema: 0,
+ PositiveSpans: []Span{
+ {Offset: -2, Length: 4},
+ {Offset: -1, Length: 0},
+ {Offset: 3, Length: 2},
+ },
+ PositiveBuckets: []int64{1, 2, -2, 1, -1, 0},
+ },
+ targetSchema: -1,
+ errorMsg: "span number 2 with offset -1: histogram has a span whose offset is negative",
+ },
}
- for _, tc := range tcs {
- target := tc.origin.ReduceResolution(tc.target.Schema)
- require.Equal(t, tc.target, target)
- // Check that receiver histogram was mutated:
- require.Equal(t, tc.target, tc.origin)
+ for tn, tc := range tcs {
+ t.Run(tn, func(t *testing.T) {
+ err := tc.origin.ReduceResolution(tc.targetSchema)
+ if tc.errorMsg != "" {
+ require.Equal(t, tc.errorMsg, err.Error())
+ // The returned error should be a histogram.Error.
+ require.ErrorAs(t, err, &Error{})
+ return
+ }
+ require.NoError(t, err)
+ require.Equal(t, tc.target, tc.origin)
+ })
}
}
diff --git a/model/histogram/test_utils.go b/model/histogram/test_utils.go
index a4871ada31..c86becdcf9 100644
--- a/model/histogram/test_utils.go
+++ b/model/histogram/test_utils.go
@@ -1,4 +1,4 @@
-// Copyright 2023 The Prometheus Authors
+// 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
diff --git a/model/labels/labels_common.go b/model/labels/labels_common.go
index 5a3979784c..571064d6c4 100644
--- a/model/labels/labels_common.go
+++ b/model/labels/labels_common.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
@@ -26,7 +26,9 @@ import (
const (
// MetricName is a special label name that represent a metric name.
//
- // Deprecated: Use schema.Metadata structure and its methods.
+ // Deprecated: Instead, consider using schema.Metadata structure and its methods for consistent metadata behaviour with the newly added __type__ and __unit__ labels. Alternatively use github.com/prometheus/common/model.MetricNameLabel for the direct replacement.
+ //
+ // labels package is providing label transport, agnostic to semantic meaning of each label.
MetricName = "__name__"
AlertName = "alertname"
diff --git a/model/labels/labels_dedupelabels.go b/model/labels/labels_dedupelabels.go
index 1e736c832e..ae751fe34a 100644
--- a/model/labels/labels_dedupelabels.go
+++ b/model/labels/labels_dedupelabels.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Prometheus Authors
+// 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
@@ -24,6 +24,9 @@ import (
"github.com/cespare/xxhash/v2"
)
+// ImplementationName is the name of the labels implementation.
+const ImplementationName = "dedupelabels"
+
// Labels is implemented by a SymbolTable and string holding name/value
// pairs encoded as indexes into the table in varint encoding.
// Names are in alphabetical order.
diff --git a/model/labels/labels_dedupelabels_test.go b/model/labels/labels_dedupelabels_test.go
index 229bb45a8e..b05d18e4cc 100644
--- a/model/labels/labels_dedupelabels_test.go
+++ b/model/labels/labels_dedupelabels_test.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Prometheus Authors
+// 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
diff --git a/model/labels/labels_slicelabels.go b/model/labels/labels_slicelabels.go
index 21ad145c1c..2a9056e68f 100644
--- a/model/labels/labels_slicelabels.go
+++ b/model/labels/labels_slicelabels.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
@@ -25,6 +25,9 @@ import (
"github.com/cespare/xxhash/v2"
)
+// ImplementationName is the name of the labels implementation.
+const ImplementationName = "slicelabels"
+
// Labels is a sorted set of labels. Order has to be guaranteed upon
// instantiation.
type Labels []Label
@@ -297,12 +300,9 @@ func FromStrings(ss ...string) Labels {
// Compare compares the two label sets.
// The result will be 0 if a==b, <0 if a < b, and >0 if a > b.
func Compare(a, b Labels) int {
- l := len(a)
- if len(b) < l {
- l = len(b)
- }
+ l := min(len(b), len(a))
- for i := 0; i < l; i++ {
+ for i := range l {
if a[i].Name != b[i].Name {
if a[i].Name < b[i].Name {
return -1
@@ -419,10 +419,7 @@ func (b *Builder) Labels() Labels {
return b.base
}
- expectedSize := len(b.base) + len(b.add) - len(b.del)
- if expectedSize < 1 {
- expectedSize = 1
- }
+ expectedSize := max(len(b.base)+len(b.add)-len(b.del), 1)
res := make(Labels, 0, expectedSize)
for _, l := range b.base {
if slices.Contains(b.del, l.Name) || contains(b.add, l.Name) {
diff --git a/model/labels/labels_slicelabels_test.go b/model/labels/labels_slicelabels_test.go
index 7961828378..700e88fd13 100644
--- a/model/labels/labels_slicelabels_test.go
+++ b/model/labels/labels_slicelabels_test.go
@@ -1,4 +1,4 @@
-// Copyright 2025 The Prometheus Authors
+// 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
@@ -77,8 +77,8 @@ func BenchmarkScratchBuilderUnsafeAdd(b *testing.B) {
l.SetUnsafeAdd(true)
b.ReportAllocs()
- b.ResetTimer()
- for i := 0; i < b.N; i++ {
+
+ for b.Loop() {
l.Add("__name__", "metric1")
l.add = l.add[:0] // Reset slice so add can be repeated without side effects.
}
diff --git a/model/labels/labels_stringlabels.go b/model/labels/labels_stringlabels.go
index f087223802..c9be42bf74 100644
--- a/model/labels/labels_stringlabels.go
+++ b/model/labels/labels_stringlabels.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
@@ -23,6 +23,9 @@ import (
"github.com/cespare/xxhash/v2"
)
+// ImplementationName is the name of the labels implementation.
+const ImplementationName = "stringlabels"
+
// Labels is implemented by a single flat string holding name/value pairs.
// Each name and value is preceded by its length, encoded as a single byte
// for size 0-254, or the following 3 bytes little-endian, if the first byte is 255.
diff --git a/model/labels/labels_stringlabels_test.go b/model/labels/labels_stringlabels_test.go
index 0704a2ff36..45b5a19f40 100644
--- a/model/labels/labels_stringlabels_test.go
+++ b/model/labels/labels_stringlabels_test.go
@@ -1,4 +1,4 @@
-// Copyright 2025 The Prometheus Authors
+// 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
diff --git a/model/labels/labels_test.go b/model/labels/labels_test.go
index 554efc6c5a..67614daf92 100644
--- a/model/labels/labels_test.go
+++ b/model/labels/labels_test.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The Prometheus Authors
+// 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
@@ -67,17 +67,19 @@ func TestSizeOfLabels(t *testing.T) {
require.Len(t, expectedSizeOfLabels, len(testCaseLabels))
for i, c := range expectedSizeOfLabels { // Declared in build-tag-specific files, e.g. labels_slicelabels_test.go.
var total uint64
- testCaseLabels[i].Range(func(l Label) {
+ labels := testCaseLabels[i]
+ labels.Range(func(l Label) {
total += SizeOfLabels(l.Name, l.Value, 1)
})
- require.Equal(t, c, total)
+ require.Equalf(t, c, total, "unexpected size for test case %d: %v", i, labels)
}
}
func TestByteSize(t *testing.T) {
require.Len(t, expectedByteSize, len(testCaseLabels))
for i, c := range expectedByteSize { // Declared in build-tag-specific files, e.g. labels_slicelabels_test.go.
- require.Equal(t, c, testCaseLabels[i].ByteSize())
+ labels := testCaseLabels[i]
+ require.Equalf(t, c, labels.ByteSize(), "unexpected size for test case %d: %v", i, labels)
}
}
diff --git a/model/labels/matcher.go b/model/labels/matcher.go
index a09c838e3f..6d22b1bf64 100644
--- a/model/labels/matcher.go
+++ b/model/labels/matcher.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
diff --git a/model/labels/matcher_test.go b/model/labels/matcher_test.go
index 214bb37eff..11ed6dd29c 100644
--- a/model/labels/matcher_test.go
+++ b/model/labels/matcher_test.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
diff --git a/model/labels/regexp.go b/model/labels/regexp.go
index 47b50e703a..f446b5358a 100644
--- a/model/labels/regexp.go
+++ b/model/labels/regexp.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
@@ -67,17 +67,35 @@ func NewFastRegexMatcher(v string) (*FastRegexMatcher, error) {
if err != nil {
return nil, err
}
+
+ parsed = optimizeAlternatingSimpleContains(parsed)
+
m.re, err = regexp.Compile("^(?s:" + parsed.String() + ")$")
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)
}
if matches, caseSensitive := findSetMatches(parsed); caseSensitive {
m.setMatches = matches
}
- m.stringMatcher = stringMatcherFromRegexp(parsed)
+
+ // Check if we have a pattern like .*-.*-.*.
+ // If so, then we can rely on the containsInOrder check in compileMatchStringFunction,
+ // so no further inspection of the string is required.
+ // We can't do this in stringMatcherFromRegexpInternal as we only want to apply this
+ // if the top-level pattern satisfies this requirement.
+ if isSimpleConcatenationPattern(parsed) {
+ m.stringMatcher = trueMatcher{}
+ } else {
+ m.stringMatcher = stringMatcherFromRegexp(parsed)
+ }
+
m.matchString = m.compileMatchStringFunction()
}
@@ -354,6 +372,43 @@ func optimizeAlternatingLiterals(s string) (StringMatcher, []string) {
return multiMatcher, multiMatcher.setMatches()
}
+// optimizeAlternatingSimpleContains checks to see if a regex is a series of alternations that take the form .*literal.*
+// In these cases, the regex itself can be rewritten as .*(foo|bar).*,
+// which can result in a significant performance improvement at execution.
+func optimizeAlternatingSimpleContains(r *syntax.Regexp) *syntax.Regexp {
+ if r.Op != syntax.OpAlternate {
+ return r
+ }
+ containsLiterals := make([]*syntax.Regexp, 0, len(r.Sub))
+ for _, sub := range r.Sub {
+ // If any subexpression does not take the form .*literal.*, we should not try to optimize this
+ if sub.Op != syntax.OpConcat || len(sub.Sub) != 3 {
+ return r
+ }
+ concatSubs := sub.Sub
+ if !isCaseSensitiveLiteral(concatSubs[1]) || !isMatchAny(concatSubs[0]) || !isMatchAny(concatSubs[2]) {
+ return r
+ }
+ containsLiterals = append(containsLiterals, concatSubs[1])
+ }
+
+ // Only rewrite the regex if there's more than one literal
+ if len(containsLiterals) > 1 {
+ returnRegex := &syntax.Regexp{Op: syntax.OpConcat}
+ prefixAnyMatcher := &syntax.Regexp{Op: syntax.OpStar, Sub: []*syntax.Regexp{{Op: syntax.OpAnyChar}}, Flags: syntax.Perl | syntax.DotNL}
+ suffixAnyMatcher := &syntax.Regexp{Op: syntax.OpStar, Sub: []*syntax.Regexp{{Op: syntax.OpAnyChar}}, Flags: syntax.Perl | syntax.DotNL}
+ alts := &syntax.Regexp{Op: syntax.OpAlternate}
+ alts.Sub = containsLiterals
+ returnRegex.Sub = []*syntax.Regexp{
+ prefixAnyMatcher,
+ alts,
+ suffixAnyMatcher,
+ }
+ return returnRegex
+ }
+ return r
+}
+
// optimizeConcatRegex returns literal prefix/suffix text that can be safely
// checked against the label value before running the regexp matcher.
func optimizeConcatRegex(r *syntax.Regexp) (prefix, suffix string, contains []string) {
@@ -566,6 +621,40 @@ func stringMatcherFromRegexpInternal(re *syntax.Regexp) StringMatcher {
return nil
}
+// isSimpleConcatenationPattern returns true if re contains only literals or wildcard matchers,
+// and starts and ends with a wildcard matcher (eg. .*-.*-.*).
+func isSimpleConcatenationPattern(re *syntax.Regexp) bool {
+ if re.Op != syntax.OpConcat {
+ return false
+ }
+
+ if len(re.Sub) < 2 {
+ return false
+ }
+
+ first := re.Sub[0]
+ last := re.Sub[len(re.Sub)-1]
+ if !isMatchAny(first) || !isMatchAny(last) {
+ return false
+ }
+
+ for _, re := range re.Sub[1 : len(re.Sub)-1] {
+ if !isMatchAny(re) && !isCaseSensitiveLiteral(re) {
+ return false
+ }
+ }
+
+ return true
+}
+
+func isMatchAny(re *syntax.Regexp) bool {
+ return re.Op == syntax.OpStar && re.Sub[0].Op == syntax.OpAnyChar
+}
+
+func isCaseSensitiveLiteral(re *syntax.Regexp) bool {
+ return re.Op == syntax.OpLiteral && isCaseSensitive(re)
+}
+
// containsStringMatcher matches a string if it contains any of the substrings.
// If left and right are not nil, it's a contains operation where left and right must match.
// If left is nil, it's a hasPrefix operation and right must match.
diff --git a/model/labels/regexp_test.go b/model/labels/regexp_test.go
index 94ef14028b..be3417a8c0 100644
--- a/model/labels/regexp_test.go
+++ b/model/labels/regexp_test.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
@@ -46,6 +46,13 @@ var (
"foo\n.*",
".*foo.*",
".+foo.+",
+ ".*foo.*|",
+ ".*foo.*|bar.*",
+ "foo.*|.*bar.*",
+ ".*foo.*|.*bar.*",
+ ".*foo.*bar.*|.*hello.*",
+ ".*foo.*|.*bar.*|.*hello.*",
+ ".+.*foo.*|.*bar.*",
"(?s:.*)",
"(?s:.+)",
"(?s:^.*foo$)",
@@ -68,6 +75,8 @@ var (
// values of a label like kubernetes pod will often include the
// deployment name as a prefix.
"jyyfj00j0061|jyyfj00j0062|jyyfj94j0093|jyyfj99j0093|jyyfm01j0021|jyyfm02j0021|jyefj00j0192|jyefj00j0193|jyefj00j0194|jyefj00j0195|jyefj00j0196|jyefj00j0197|jyefj00j0290|jyefj00j0291|jyefj00j0292|jyefj00j0293|jyefj00j0294|jyefj00j0295|jyefj00j0296|jyefj00j0297|jyefj89j0394|jyefj90j0394|jyefj91j0394|jyefj95j0347|jyefj96j0322|jyefj96j0347|jyefj97j0322|jyefj97j0347|jyefj98j0322|jyefj98j0347|jyefj99j0320|jyefj99j0322|jyefj99j0323|jyefj99j0335|jyefj99j0336|jyefj99j0344|jyefj99j0347|jyefj99j0349|jyefj99j0351|jyeff00j0117|lyyfm01j0025|lyyfm01j0028|lyyfm01j0041|lyyfm01j0133|lyyfm01j0701|lyyfm02j0025|lyyfm02j0028|lyyfm02j0041|lyyfm02j0133|lyyfm02j0701|lyyfm03j0701|lyefj00j0775|lyefj00j0776|lyefj00j0777|lyefj00j0778|lyefj00j0779|lyefj00j0780|lyefj00j0781|lyefj00j0782|lyefj50j3807|lyefj50j3852|lyefj51j3807|lyefj51j3852|lyefj52j3807|lyefj52j3852|lyefj53j3807|lyefj53j3852|lyefj54j3807|lyefj54j3852|lyefj54j3886|lyefj55j3807|lyefj55j3852|lyefj55j3886|lyefj56j3807|lyefj56j3852|lyefj56j3886|lyefj57j3807|lyefj57j3852|lyefj57j3886|lyefj58j3807|lyefj58j3852|lyefj58j3886|lyefj59j3807|lyefj59j3852|lyefj59j3886|lyefj60j3807|lyefj60j3852|lyefj60j3886|lyefj61j3807|lyefj61j3852|lyefj61j3886|lyefj62j3807|lyefj62j3852|lyefj62j3886|lyefj63j3807|lyefj63j3852|lyefj63j3886|lyefj64j3807|lyefj64j3852|lyefj64j3886|lyefj65j3807|lyefj65j3852|lyefj65j3886|lyefj66j3807|lyefj66j3852|lyefj66j3886|lyefj67j3807|lyefj67j3852|lyefj67j3886|lyefj68j3807|lyefj68j3852|lyefj68j3886|lyefj69j3807|lyefj69j3846|lyefj69j3852|lyefj69j3886|lyefj70j3807|lyefj70j3846|lyefj70j3852|lyefj70j3886|lyefj71j3807|lyefj71j3846|lyefj71j3852|lyefj71j3886|lyefj72j3807|lyefj72j3846|lyefj72j3852|lyefj72j3886|lyefj73j3807|lyefj73j3846|lyefj73j3852|lyefj73j3886|lyefj74j3807|lyefj74j3846|lyefj74j3852|lyefj74j3886|lyefj75j3807|lyefj75j3808|lyefj75j3846|lyefj75j3852|lyefj75j3886|lyefj76j3732|lyefj76j3807|lyefj76j3808|lyefj76j3846|lyefj76j3852|lyefj76j3886|lyefj77j3732|lyefj77j3807|lyefj77j3808|lyefj77j3846|lyefj77j3852|lyefj77j3886|lyefj78j3278|lyefj78j3732|lyefj78j3807|lyefj78j3808|lyefj78j3846|lyefj78j3852|lyefj78j3886|lyefj79j3732|lyefj79j3807|lyefj79j3808|lyefj79j3846|lyefj79j3852|lyefj79j3886|lyefj80j3732|lyefj80j3807|lyefj80j3808|lyefj80j3846|lyefj80j3852|lyefj80j3886|lyefj81j3732|lyefj81j3807|lyefj81j3808|lyefj81j3846|lyefj81j3852|lyefj81j3886|lyefj82j3732|lyefj82j3807|lyefj82j3808|lyefj82j3846|lyefj82j3852|lyefj82j3886|lyefj83j3732|lyefj83j3807|lyefj83j3808|lyefj83j3846|lyefj83j3852|lyefj83j3886|lyefj84j3732|lyefj84j3807|lyefj84j3808|lyefj84j3846|lyefj84j3852|lyefj84j3886|lyefj85j3732|lyefj85j3807|lyefj85j3808|lyefj85j3846|lyefj85j3852|lyefj85j3886|lyefj86j3278|lyefj86j3732|lyefj86j3807|lyefj86j3808|lyefj86j3846|lyefj86j3852|lyefj86j3886|lyefj87j3278|lyefj87j3732|lyefj87j3807|lyefj87j3808|lyefj87j3846|lyefj87j3852|lyefj87j3886|lyefj88j3732|lyefj88j3807|lyefj88j3808|lyefj88j3846|lyefj88j3852|lyefj88j3886|lyefj89j3732|lyefj89j3807|lyefj89j3808|lyefj89j3846|lyefj89j3852|lyefj89j3886|lyefj90j3732|lyefj90j3807|lyefj90j3808|lyefj90j3846|lyefj90j3852|lyefj90j3886|lyefj91j3732|lyefj91j3807|lyefj91j3808|lyefj91j3846|lyefj91j3852|lyefj91j3886|lyefj92j3732|lyefj92j3807|lyefj92j3808|lyefj92j3846|lyefj92j3852|lyefj92j3886|lyefj93j3732|lyefj93j3807|lyefj93j3808|lyefj93j3846|lyefj93j3852|lyefj93j3885|lyefj93j3886|lyefj94j3525|lyefj94j3732|lyefj94j3807|lyefj94j3808|lyefj94j3846|lyefj94j3852|lyefj94j3885|lyefj94j3886|lyefj95j3525|lyefj95j3732|lyefj95j3807|lyefj95j3808|lyefj95j3846|lyefj95j3852|lyefj95j3886|lyefj96j3732|lyefj96j3803|lyefj96j3807|lyefj96j3808|lyefj96j3846|lyefj96j3852|lyefj96j3886|lyefj97j3333|lyefj97j3732|lyefj97j3792|lyefj97j3803|lyefj97j3807|lyefj97j3808|lyefj97j3838|lyefj97j3843|lyefj97j3846|lyefj97j3852|lyefj97j3886|lyefj98j3083|lyefj98j3333|lyefj98j3732|lyefj98j3807|lyefj98j3808|lyefj98j3838|lyefj98j3843|lyefj98j3846|lyefj98j3852|lyefj98j3873|lyefj98j3877|lyefj98j3882|lyefj98j3886|lyefj99j2984|lyefj99j3083|lyefj99j3333|lyefj99j3732|lyefj99j3807|lyefj99j3808|lyefj99j3846|lyefj99j3849|lyefj99j3852|lyefj99j3873|lyefj99j3877|lyefj99j3882|lyefj99j3884|lyefj99j3886|lyeff00j0106|lyeff00j0107|lyeff00j0108|lyeff00j0129|lyeff00j0130|lyeff00j0131|lyeff00j0132|lyeff00j0133|lyeff00j0134|lyeff00j0444|lyeff00j0445|lyeff91j0473|lyeff92j0473|lyeff92j3877|lyeff93j3877|lyeff94j0501|lyeff94j3525|lyeff94j3877|lyeff95j0501|lyeff95j3525|lyeff95j3877|lyeff96j0503|lyeff96j3877|lyeff97j3877|lyeff98j3333|lyeff98j3877|lyeff99j2984|lyeff99j3333|lyeff99j3877|mfyr9149ej|mfyr9149ek|mfyr9156ej|mfyr9156ek|mfyr9157ej|mfyr9157ek|mfyr9159ej|mfyr9159ek|mfyr9203ej|mfyr9204ej|mfyr9205ej|mfyr9206ej|mfyr9207ej|mfyr9207ek|mfyr9217ej|mfyr9217ek|mfyr9222ej|mfyr9222ek|mfyu0185ej|mfye9187ej|mfye9187ek|mfye9188ej|mfye9188ek|mfye9189ej|mfye9189ek|mfyf0185ej|oyefj87j0007|oyefj88j0007|oyefj89j0007|oyefj90j0007|oyefj91j0007|oyefj95j0001|oyefj96j0001|oyefj98j0004|oyefj99j0004|oyeff91j0004|oyeff92j0004|oyeff93j0004|oyeff94j0004|oyeff95j0004|oyeff96j0004|rklvyaxmany|ryefj93j0001|ryefj94j0001|tyyfj00a0001|tyyfj84j0005|tyyfj85j0005|tyyfj86j0005|tyyfj87j0005|tyyfj88j0005|tyyfj89j0005|tyyfj90j0005|tyyfj91j0005|tyyfj92j0005|tyyfj93j0005|tyyfj94j0005|tyyfj95j0005|tyyfj96j0005|tyyfj97j0005|tyyfj98j0005|tyyfj99j0005|tyefj50j0015|tyefj50j0017|tyefj50j0019|tyefj50j0020|tyefj50j0021|tyefj51j0015|tyefj51j0017|tyefj51j0019|tyefj51j0020|tyefj51j0021|tyefj52j0015|tyefj52j0017|tyefj52j0019|tyefj52j0020|tyefj52j0021|tyefj53j0015|tyefj53j0017|tyefj53j0019|tyefj53j0020|tyefj53j0021|tyefj54j0015|tyefj54j0017|tyefj54j0019|tyefj54j0020|tyefj54j0021|tyefj55j0015|tyefj55j0017|tyefj55j0019|tyefj55j0020|tyefj55j0021|tyefj56j0015|tyefj56j0017|tyefj56j0019|tyefj56j0020|tyefj56j0021|tyefj57j0015|tyefj57j0017|tyefj57j0019|tyefj57j0020|tyefj57j0021|tyefj58j0015|tyefj58j0017|tyefj58j0019|tyefj58j0020|tyefj58j0021|tyefj59j0015|tyefj59j0017|tyefj59j0019|tyefj59j0020|tyefj59j0021|tyefj60j0015|tyefj60j0017|tyefj60j0019|tyefj60j0020|tyefj60j0021|tyefj61j0015|tyefj61j0017|tyefj61j0019|tyefj61j0020|tyefj61j0021|tyefj62j0015|tyefj62j0017|tyefj62j0019|tyefj62j0020|tyefj62j0021|tyefj63j0015|tyefj63j0017|tyefj63j0019|tyefj63j0020|tyefj63j0021|tyefj64j0015|tyefj64j0017|tyefj64j0019|tyefj64j0020|tyefj64j0021|tyefj65j0015|tyefj65j0017|tyefj65j0019|tyefj65j0020|tyefj65j0021|tyefj66j0015|tyefj66j0017|tyefj66j0019|tyefj66j0020|tyefj66j0021|tyefj67j0015|tyefj67j0017|tyefj67j0019|tyefj67j0020|tyefj67j0021|tyefj68j0015|tyefj68j0017|tyefj68j0019|tyefj68j0020|tyefj68j0021|tyefj69j0015|tyefj69j0017|tyefj69j0019|tyefj69j0020|tyefj69j0021|tyefj70j0015|tyefj70j0017|tyefj70j0019|tyefj70j0020|tyefj70j0021|tyefj71j0015|tyefj71j0017|tyefj71j0019|tyefj71j0020|tyefj71j0021|tyefj72j0015|tyefj72j0017|tyefj72j0019|tyefj72j0020|tyefj72j0021|tyefj72j0022|tyefj73j0015|tyefj73j0017|tyefj73j0019|tyefj73j0020|tyefj73j0021|tyefj73j0022|tyefj74j0015|tyefj74j0017|tyefj74j0019|tyefj74j0020|tyefj74j0021|tyefj74j0022|tyefj75j0015|tyefj75j0017|tyefj75j0019|tyefj75j0020|tyefj75j0021|tyefj75j0022|tyefj76j0015|tyefj76j0017|tyefj76j0019|tyefj76j0020|tyefj76j0021|tyefj76j0022|tyefj76j0119|tyefj77j0015|tyefj77j0017|tyefj77j0019|tyefj77j0020|tyefj77j0021|tyefj77j0022|tyefj77j0119|tyefj78j0015|tyefj78j0017|tyefj78j0019|tyefj78j0020|tyefj78j0021|tyefj78j0022|tyefj78j0119|tyefj79j0015|tyefj79j0017|tyefj79j0019|tyefj79j0020|tyefj79j0021|tyefj79j0022|tyefj79j0119|tyefj80j0015|tyefj80j0017|tyefj80j0019|tyefj80j0020|tyefj80j0021|tyefj80j0022|tyefj80j0114|tyefj80j0119|tyefj81j0015|tyefj81j0017|tyefj81j0019|tyefj81j0020|tyefj81j0021|tyefj81j0022|tyefj81j0114|tyefj81j0119|tyefj82j0015|tyefj82j0017|tyefj82j0019|tyefj82j0020|tyefj82j0021|tyefj82j0022|tyefj82j0119|tyefj83j0015|tyefj83j0017|tyefj83j0019|tyefj83j0020|tyefj83j0021|tyefj83j0022|tyefj83j0119|tyefj84j0014|tyefj84j0015|tyefj84j0017|tyefj84j0019|tyefj84j0020|tyefj84j0021|tyefj84j0022|tyefj84j0119|tyefj85j0014|tyefj85j0015|tyefj85j0017|tyefj85j0019|tyefj85j0020|tyefj85j0021|tyefj85j0022|tyefj85j0119|tyefj86j0014|tyefj86j0015|tyefj86j0017|tyefj86j0019|tyefj86j0020|tyefj86j0021|tyefj86j0022|tyefj87j0014|tyefj87j0015|tyefj87j0017|tyefj87j0019|tyefj87j0020|tyefj87j0021|tyefj87j0022|tyefj88j0014|tyefj88j0015|tyefj88j0017|tyefj88j0019|tyefj88j0020|tyefj88j0021|tyefj88j0022|tyefj88j0100|tyefj88j0115|tyefj89j0003|tyefj89j0014|tyefj89j0015|tyefj89j0017|tyefj89j0019|tyefj89j0020|tyefj89j0021|tyefj89j0022|tyefj89j0100|tyefj89j0115|tyefj90j0014|tyefj90j0015|tyefj90j0016|tyefj90j0017|tyefj90j0018|tyefj90j0019|tyefj90j0020|tyefj90j0021|tyefj90j0022|tyefj90j0100|tyefj90j0111|tyefj90j0115|tyefj91j0014|tyefj91j0015|tyefj91j0016|tyefj91j0017|tyefj91j0018|tyefj91j0019|tyefj91j0020|tyefj91j0021|tyefj91j0022|tyefj91j0100|tyefj91j0111|tyefj91j0115|tyefj92j0014|tyefj92j0015|tyefj92j0016|tyefj92j0017|tyefj92j0018|tyefj92j0019|tyefj92j0020|tyefj92j0021|tyefj92j0022|tyefj92j0100|tyefj92j0105|tyefj92j0115|tyefj92j0121|tyefj93j0004|tyefj93j0014|tyefj93j0015|tyefj93j0017|tyefj93j0018|tyefj93j0019|tyefj93j0020|tyefj93j0021|tyefj93j0022|tyefj93j0100|tyefj93j0105|tyefj93j0115|tyefj93j0121|tyefj94j0002|tyefj94j0004|tyefj94j0008|tyefj94j0014|tyefj94j0015|tyefj94j0017|tyefj94j0019|tyefj94j0020|tyefj94j0021|tyefj94j0022|tyefj94j0084|tyefj94j0088|tyefj94j0100|tyefj94j0106|tyefj94j0116|tyefj94j0121|tyefj94j0123|tyefj95j0002|tyefj95j0004|tyefj95j0008|tyefj95j0014|tyefj95j0015|tyefj95j0017|tyefj95j0019|tyefj95j0020|tyefj95j0021|tyefj95j0022|tyefj95j0084|tyefj95j0088|tyefj95j0100|tyefj95j0101|tyefj95j0106|tyefj95j0112|tyefj95j0116|tyefj95j0121|tyefj95j0123|tyefj96j0014|tyefj96j0015|tyefj96j0017|tyefj96j0019|tyefj96j0020|tyefj96j0021|tyefj96j0022|tyefj96j0082|tyefj96j0084|tyefj96j0100|tyefj96j0101|tyefj96j0112|tyefj96j0117|tyefj96j0121|tyefj96j0124|tyefj97j0014|tyefj97j0015|tyefj97j0017|tyefj97j0019|tyefj97j0020|tyefj97j0021|tyefj97j0022|tyefj97j0081|tyefj97j0087|tyefj97j0098|tyefj97j0100|tyefj97j0107|tyefj97j0109|tyefj97j0113|tyefj97j0117|tyefj97j0118|tyefj97j0121|tyefj98j0003|tyefj98j0006|tyefj98j0014|tyefj98j0015|tyefj98j0017|tyefj98j0019|tyefj98j0020|tyefj98j0021|tyefj98j0022|tyefj98j0083|tyefj98j0085|tyefj98j0086|tyefj98j0100|tyefj98j0104|tyefj98j0118|tyefj98j0121|tyefj99j0003|tyefj99j0006|tyefj99j0007|tyefj99j0014|tyefj99j0015|tyefj99j0017|tyefj99j0019|tyefj99j0020|tyefj99j0021|tyefj99j0022|tyefj99j0023|tyefj99j0100|tyefj99j0108|tyefj99j0110|tyefj99j0121|tyefj99j0125|tyeff94j0002|tyeff94j0008|tyeff94j0010|tyeff94j0011|tyeff94j0035|tyeff95j0002|tyeff95j0006|tyeff95j0008|tyeff95j0010|tyeff95j0011|tyeff95j0035|tyeff96j0003|tyeff96j0006|tyeff96j0009|tyeff96j0010|tyeff97j0004|tyeff97j0009|tyeff97j0116|tyeff98j0007|tyeff99j0007|tyeff99j0125|uyyfj00j0484|uyyfj00j0485|uyyfj00j0486|uyyfj00j0487|uyyfj00j0488|uyyfj00j0489|uyyfj00j0490|uyyfj00j0491|uyyfj00j0492|uyyfj00j0493|uyyfj00j0494|uyyfj00j0495|uyyfj00j0496|uyyfj00j0497|uyyfj00j0498|uyyfj00j0499|uyyfj00j0500|uyyfj00j0501|uyyfj00j0502|uyyfj00j0503|uyyfj00j0504|uyyfj00j0505|uyyfj00j0506|uyyfj00j0507|uyyfj00j0508|uyyfj00j0509|uyyfj00j0510|uyyfj00j0511|uyyfj00j0512|uyyfj00j0513|uyyfj00j0514|uyyfj00j0515|uyyfj00j0516|uyyfj00j0517|uyyfj00j0518|uyyfj00j0519|uyyfj00j0520|uyyfj00j0521|uyyfj00j0522|uyyfj00j0523|uyyfj00j0524|uyyfj00j0525|uyyfj00j0526|uyyfj00j0527|uyyfj00j0528|uyyfj00j0529|uyyfj00j0530|uyyfj00j0531|uyyfj00j0532|uyyfj00j0533|uyyfj00j0534|uyyfj00j0535|uyyfj00j0536|uyyfj00j0537|uyyfj00j0538|uyyfj00j0539|uyyfj00j0540|uyyfj00j0541|uyyfj00j0542|uyyfj00j0543|uyyfj00j0544|uyyfj00j0545|uyyfj00j0546|uyyfj00j0547|uyyfj00j0548|uyyfj00j0549|uyyfj00j0550|uyyfj00j0551|uyyfj00j0553|uyyfj00j0554|uyyfj00j0555|uyyfj00j0556|uyyfj00j0557|uyyfj00j0558|uyyfj00j0559|uyyfj00j0560|uyyfj00j0561|uyyfj00j0562|uyyfj00j0563|uyyfj00j0564|uyyfj00j0565|uyyfj00j0566|uyyfj00j0614|uyyfj00j0615|uyyfj00j0616|uyyfj00j0617|uyyfj00j0618|uyyfj00j0619|uyyfj00j0620|uyyfj00j0621|uyyfj00j0622|uyyfj00j0623|uyyfj00j0624|uyyfj00j0625|uyyfj00j0626|uyyfj00j0627|uyyfj00j0628|uyyfj00j0629|uyyfj00j0630|uyyfj00j0631|uyyfj00j0632|uyyfj00j0633|uyyfj00j0634|uyyfj00j0635|uyyfj00j0636|uyyfj00j0637|uyyfj00j0638|uyyfj00j0639|uyyfj00j0640|uyyfj00j0641|uyyfj00j0642|uyyfj00j0643|uyyfj00j0644|uyyfj00j0645|uyyfj00j0646|uyyfj00j0647|uyyfj00j0648|uyyfj00j0649|uyyfj00j0650|uyyfj00j0651|uyyfj00j0652|uyyfj00j0653|uyyfj00j0654|uyyfj00j0655|uyyfj00j0656|uyyfj00j0657|uyyfj00j0658|uyyfj00j0659|uyyfj00j0660|uyyfj00j0661|uyyfj00j0662|uyyfj00j0663|uyyfj00j0664|uyyfj00j0665|uyyfj00j0666|uyyfj00j0667|uyyfj00j0668|uyyfj00j0669|uyyfj00j0670|uyyfj00j0671|uyyfj00j0672|uyyfj00j0673|uyyfj00j0674|uyyfj00j0675|uyyfj00j0676|uyyfj00j0677|uyyfj00j0678|uyyfj00j0679|uyyfj00j0680|uyyfj00j0681|uyyfj00j0682|uyyfj00j0683|uyyfj00j0684|uyyfj00j0685|uyyfj00j0686|uyyfj00j0687|uyyfj00j0688|uyyfj00j0689|uyyfj00j0690|uyyfj00j0691|uyyfj00j0692|uyyfj00j0693|uyyfj00j0694|uyyfj00j0695|uyyfj00j0696|uyyfj00j0697|uyyfj00j0698|uyyfj00j0699|uyyfj00j0700|uyyfj00j0701|uyyfj00j0702|uyyfj00j0703|uyyfj00j0704|uyyfj00j0705|uyyfj00j0706|uyyfj00j0707|uyyfj00j0708|uyyfj00j0709|uyyfj00j0710|uyyfj00j0711|uyyfj00j0712|uyyfj00j0713|uyyfj00j0714|uyyfj00j0715|uyyfj00j0716|uyyfj00j0717|uyyfj00j0718|uyyfj00j0719|uyyfj00j0720|uyyfj00j0721|uyyfj00j0722|uyyfj00j0723|uyyfj00j0724|uyyfj00j0725|uyyfj00j0726|uyyfj00j0727|uyyfj00j0728|uyyfj00j0729|uyyfj00j0730|uyyfj00j0731|uyyfj00j0732|uyyfj00j0733|uyyfj00j0734|uyyfj00j0735|uyyfj00j0736|uyyfj00j0737|uyyfj00j0738|uyyfj00j0739|uyyfj00j0740|uyyfj00j0741|uyyfj00j0742|uyyfj00j0743|uyyfj00j0744|uyyfj00j0745|uyyfj00j0746|uyyfj00j0747|uyyfj00j0748|uyyfj00j0749|uyyfj00j0750|uyyfj00j0751|uyyfj00j0752|uyyfj00j0753|uyyfj00j0754|uyyfj00j0755|uyyfj00j0756|uyyfj00j0757|uyyfj00j0758|uyyfj00j0759|uyyfj00j0760|uyyfj00j0761|uyyfj00j0762|uyyfj00j0763|uyyfj00j0764|uyyfj00j0765|uyyfj00j0766|uyyfj00j0767|uyyfj00j0768|uyyfj00j0769|uyyfj00j0770|uyyfj00j0771|uyyfj00j0772|uyyfj00j0773|uyyfj00j0774|uyyfj00j0775|uyyfj00j0776|uyyfj00j0777|uyyfj00j0778|uyyfj00j0779|uyyfj00j0780|uyyfj00j0781|uyyfj00j0782|uyyff00j0011|uyyff00j0031|uyyff00j0032|uyyff00j0033|uyyff00j0034|uyyff99j0012|uyefj00j0071|uyefj00j0455|uyefj00j0456|uyefj00j0582|uyefj00j0583|uyefj00j0584|uyefj00j0585|uyefj00j0586|uyefj00j0590|uyeff00j0188|xyrly-f-jyy-y01|xyrly-f-jyy-y02|xyrly-f-jyy-y03|xyrly-f-jyy-y04|xyrly-f-jyy-y05|xyrly-f-jyy-y06|xyrly-f-jyy-y07|xyrly-f-jyy-y08|xyrly-f-jyy-y09|xyrly-f-jyy-y10|xyrly-f-jyy-y11|xyrly-f-jyy-y12|xyrly-f-jyy-y13|xyrly-f-jyy-y14|xyrly-f-jyy-y15|xyrly-f-jyy-y16|xyrly-f-url-y01|xyrly-f-url-y02|yyefj97j0005|ybyfcy4000|ybyfcy4001|ayefj99j0035|by-b-y-bzu-l01|by-b-y-bzu-l02|by-b-e-079|by-b-e-080|by-b-e-082|by-b-e-083|byefj72j0002|byefj73j0002|byefj74j0002|byefj75j0002|byefj76j0002|byefj77j0002|byefj78j0002|byefj79j0002|byefj91j0007|byefj92j0007|byefj98j0003|byefj99j0003|byefj99j0005|byefj99j0006|byeff88j0002|byeff89j0002|byeff90j0002|byeff91j0002|byeff92j0002|byeff93j0002|byeff96j0003|byeff97j0003|byeff98j0003|byeff99j0003|fymfj98j0001|fymfj99j0001|fyyaj98k0297|fyyaj99k0297|fyyfj00j0109|fyyfj00j0110|fyyfj00j0122|fyyfj00j0123|fyyfj00j0201|fyyfj00j0202|fyyfj00j0207|fyyfj00j0208|fyyfj00j0227|fyyfj00j0228|fyyfj00j0229|fyyfj00j0230|fyyfj00j0231|fyyfj00j0232|fyyfj00j0233|fyyfj00j0234|fyyfj00j0235|fyyfj00j0236|fyyfj00j0237|fyyfj00j0238|fyyfj00j0239|fyyfj00j0240|fyyfj00j0241|fyyfj00j0242|fyyfj00j0243|fyyfj00j0244|fyyfj00j0245|fyyfj00j0246|fyyfj00j0247|fyyfj00j0248|fyyfj00j0249|fyyfj00j0250|fyyfj00j0251|fyyfj00j0252|fyyfj00j0253|fyyfj00j0254|fyyfj00j0255|fyyfj00j0256|fyyfj00j0257|fyyfj00j0258|fyyfj00j0259|fyyfj00j0260|fyyfj00j0261|fyyfj00j0262|fyyfj00j0263|fyyfj00j0264|fyyfj00j0265|fyyfj00j0266|fyyfj00j0267|fyyfj00j0268|fyyfj00j0290|fyyfj00j0291|fyyfj00j0292|fyyfj00j0293|fyyfj00j0294|fyyfj00j0295|fyyfj00j0296|fyyfj00j0297|fyyfj00j0298|fyyfj00j0299|fyyfj00j0300|fyyfj00j0301|fyyfj00j0302|fyyfj00j0303|fyyfj00j0304|fyyfj00j0305|fyyfj00j0306|fyyfj00j0307|fyyfj00j0308|fyyfj00j0309|fyyfj00j0310|fyyfj00j0311|fyyfj00j0312|fyyfj00j0313|fyyfj00j0314|fyyfj00j0315|fyyfj00j0316|fyyfj00j0317|fyyfj00j0318|fyyfj00j0319|fyyfj00j0320|fyyfj00j0321|fyyfj00j0322|fyyfj00j0323|fyyfj00j0324|fyyfj00j0325|fyyfj00j0326|fyyfj00j0327|fyyfj00j0328|fyyfj00j0329|fyyfj00j0330|fyyfj00j0331|fyyfj00j0332|fyyfj00j0333|fyyfj00j0334|fyyfj00j0335|fyyfj00j0340|fyyfj00j0341|fyyfj00j0342|fyyfj00j0343|fyyfj00j0344|fyyfj00j0345|fyyfj00j0346|fyyfj00j0347|fyyfj00j0348|fyyfj00j0349|fyyfj00j0367|fyyfj00j0368|fyyfj00j0369|fyyfj00j0370|fyyfj00j0371|fyyfj00j0372|fyyfj00j0373|fyyfj00j0374|fyyfj00j0375|fyyfj00j0376|fyyfj00j0377|fyyfj00j0378|fyyfj00j0379|fyyfj00j0380|fyyfj00j0381|fyyfj00j0382|fyyfj00j0383|fyyfj00j0384|fyyfj00j0385|fyyfj00j0386|fyyfj00j0387|fyyfj00j0388|fyyfj00j0415|fyyfj00j0416|fyyfj00j0417|fyyfj00j0418|fyyfj00j0419|fyyfj00j0420|fyyfj00j0421|fyyfj00j0422|fyyfj00j0423|fyyfj00j0424|fyyfj00j0425|fyyfj00j0426|fyyfj00j0427|fyyfj00j0428|fyyfj00j0429|fyyfj00j0430|fyyfj00j0431|fyyfj00j0432|fyyfj00j0433|fyyfj00j0434|fyyfj00j0435|fyyfj00j0436|fyyfj00j0437|fyyfj00j0438|fyyfj00j0439|fyyfj00j0440|fyyfj00j0441|fyyfj00j0446|fyyfj00j0447|fyyfj00j0448|fyyfj00j0449|fyyfj00j0451|fyyfj00j0452|fyyfj00j0453|fyyfj00j0454|fyyfj00j0455|fyyfj00j0456|fyyfj00j0457|fyyfj00j0459|fyyfj00j0460|fyyfj00j0461|fyyfj00j0462|fyyfj00j0463|fyyfj00j0464|fyyfj00j0465|fyyfj00j0466|fyyfj00j0467|fyyfj00j0468|fyyfj00j0469|fyyfj00j0470|fyyfj00j0471|fyyfj00j0474|fyyfj00j0475|fyyfj00j0476|fyyfj00j0477|fyyfj00j0478|fyyfj00j0479|fyyfj00j0480|fyyfj00j0481|fyyfj00j0482|fyyfj00j0483|fyyfj00j0484|fyyfj00j0485|fyyfj00j0486|fyyfj00j0487|fyyfj00j0488|fyyfj00j0489|fyyfj00j0490|fyyfj00j0491|fyyfj00j0492|fyyfj00j0493|fyyfj00j0494|fyyfj00j0495|fyyfj00j0496|fyyfj00j0497|fyyfj00j0498|fyyfj00j0499|fyyfj00j0500|fyyfj00j0501|fyyfj00j0502|fyyfj00j0503|fyyfj00j0504|fyyfj00j0505|fyyfj00j0506|fyyfj00j0507|fyyfj00j0508|fyyfj00j0509|fyyfj00j0510|fyyfj00j0511|fyyfj00j0512|fyyfj00j0513|fyyfj00j0514|fyyfj00j0515|fyyfj00j0516|fyyfj00j0517|fyyfj00j0518|fyyfj00j0521|fyyfj00j0522|fyyfj00j0523|fyyfj00j0524|fyyfj00j0526|fyyfj00j0527|fyyfj00j0528|fyyfj00j0529|fyyfj00j0530|fyyfj00j0531|fyyfj00j0532|fyyfj00j0533|fyyfj00j0534|fyyfj00j0535|fyyfj00j0536|fyyfj00j0537|fyyfj00j0538|fyyfj00j0539|fyyfj00j0540|fyyfj00j0541|fyyfj00j0542|fyyfj00j0543|fyyfj00j0544|fyyfj00j0545|fyyfj00j0546|fyyfj00j0564|fyyfj00j0565|fyyfj00j0566|fyyfj00j0567|fyyfj00j0568|fyyfj00j0569|fyyfj00j0570|fyyfj00j0571|fyyfj00j0572|fyyfj00j0574|fyyfj00j0575|fyyfj00j0576|fyyfj00j0577|fyyfj00j0578|fyyfj00j0579|fyyfj00j0580|fyyfj01j0473|fyyfj02j0473|fyyfj36j0289|fyyfj37j0209|fyyfj37j0289|fyyfj38j0209|fyyfj38j0289|fyyfj39j0209|fyyfj39j0289|fyyfj40j0209|fyyfj40j0289|fyyfj41j0209|fyyfj41j0289|fyyfj42j0209|fyyfj42j0289|fyyfj43j0209|fyyfj43j0289|fyyfj44j0209|fyyfj44j0289|fyyfj45j0104|fyyfj45j0209|fyyfj45j0289|fyyfj46j0104|fyyfj46j0209|fyyfj46j0289|fyyfj47j0104|fyyfj47j0209|fyyfj47j0289|fyyfj48j0104|fyyfj48j0209|fyyfj48j0289|fyyfj49j0104|fyyfj49j0209|fyyfj49j0289|fyyfj50j0104|fyyfj50j0209|fyyfj50j0289|fyyfj50j0500|fyyfj51j0104|fyyfj51j0209|fyyfj51j0289|fyyfj51j0500|fyyfj52j0104|fyyfj52j0209|fyyfj52j0289|fyyfj52j0500|fyyfj53j0104|fyyfj53j0209|fyyfj53j0289|fyyfj53j0500|fyyfj54j0104|fyyfj54j0209|fyyfj54j0289|fyyfj54j0500|fyyfj55j0104|fyyfj55j0209|fyyfj55j0289|fyyfj55j0500|fyyfj56j0104|fyyfj56j0209|fyyfj56j0289|fyyfj56j0500|fyyfj57j0104|fyyfj57j0209|fyyfj57j0289|fyyfj57j0500|fyyfj58j0104|fyyfj58j0209|fyyfj58j0289|fyyfj58j0500|fyyfj59j0104|fyyfj59j0209|fyyfj59j0289|fyyfj59j0500|fyyfj60j0104|fyyfj60j0209|fyyfj60j0289|fyyfj60j0500|fyyfj61j0104|fyyfj61j0209|fyyfj61j0289|fyyfj61j0500|fyyfj62j0104|fyyfj62j0209|fyyfj62j0289|fyyfj62j0500|fyyfj63j0104|fyyfj63j0209|fyyfj63j0289|fyyfj63j0500|fyyfj64j0104|fyyfj64j0107|fyyfj64j0209|fyyfj64j0289|fyyfj64j0500|fyyfj64j0573|fyyfj65j0104|fyyfj65j0107|fyyfj65j0209|fyyfj65j0289|fyyfj65j0500|fyyfj65j0573|fyyfj66j0104|fyyfj66j0107|fyyfj66j0209|fyyfj66j0289|fyyfj66j0500|fyyfj66j0573|fyyfj67j0104|fyyfj67j0107|fyyfj67j0209|fyyfj67j0289|fyyfj67j0500|fyyfj67j0573|fyyfj68j0104|fyyfj68j0107|fyyfj68j0209|fyyfj68j0289|fyyfj68j0500|fyyfj68j0573|fyyfj69j0104|fyyfj69j0107|fyyfj69j0209|fyyfj69j0289|fyyfj69j0500|fyyfj69j0573|fyyfj70j0104|fyyfj70j0107|fyyfj70j0209|fyyfj70j0289|fyyfj70j0472|fyyfj70j0500|fyyfj70j0573|fyyfj71j0104|fyyfj71j0107|fyyfj71j0209|fyyfj71j0289|fyyfj71j0472|fyyfj71j0500|fyyfj71j0573|fyyfj72j0104|fyyfj72j0107|fyyfj72j0209|fyyfj72j0289|fyyfj72j0472|fyyfj72j0500|fyyfj72j0573|fyyfj73j0104|fyyfj73j0107|fyyfj73j0209|fyyfj73j0289|fyyfj73j0472|fyyfj73j0500|fyyfj73j0573|fyyfj74j0104|fyyfj74j0107|fyyfj74j0209|fyyfj74j0289|fyyfj74j0472|fyyfj74j0500|fyyfj74j0573|fyyfj75j0104|fyyfj75j0107|fyyfj75j0108|fyyfj75j0209|fyyfj75j0289|fyyfj75j0472|fyyfj75j0500|fyyfj75j0573|fyyfj76j0104|fyyfj76j0107|fyyfj76j0108|fyyfj76j0209|fyyfj76j0289|fyyfj76j0472|fyyfj76j0500|fyyfj76j0573|fyyfj77j0104|fyyfj77j0107|fyyfj77j0108|fyyfj77j0209|fyyfj77j0289|fyyfj77j0472|fyyfj77j0500|fyyfj77j0573|fyyfj78j0104|fyyfj78j0107|fyyfj78j0108|fyyfj78j0209|fyyfj78j0289|fyyfj78j0472|fyyfj78j0500|fyyfj78j0573|fyyfj79j0104|fyyfj79j0107|fyyfj79j0108|fyyfj79j0209|fyyfj79j0289|fyyfj79j0339|fyyfj79j0472|fyyfj79j0500|fyyfj79j0573|fyyfj80j0104|fyyfj80j0107|fyyfj80j0108|fyyfj80j0209|fyyfj80j0289|fyyfj80j0339|fyyfj80j0352|fyyfj80j0472|fyyfj80j0500|fyyfj80j0573|fyyfj81j0104|fyyfj81j0107|fyyfj81j0108|fyyfj81j0209|fyyfj81j0289|fyyfj81j0339|fyyfj81j0352|fyyfj81j0472|fyyfj81j0500|fyyfj81j0573|fyyfj82j0104|fyyfj82j0107|fyyfj82j0108|fyyfj82j0209|fyyfj82j0289|fyyfj82j0339|fyyfj82j0352|fyyfj82j0472|fyyfj82j0500|fyyfj82j0573|fyyfj83j0104|fyyfj83j0107|fyyfj83j0108|fyyfj83j0209|fyyfj83j0289|fyyfj83j0339|fyyfj83j0352|fyyfj83j0472|fyyfj83j0500|fyyfj83j0573|fyyfj84j0104|fyyfj84j0107|fyyfj84j0108|fyyfj84j0209|fyyfj84j0289|fyyfj84j0339|fyyfj84j0352|fyyfj84j0472|fyyfj84j0500|fyyfj84j0573|fyyfj85j0104|fyyfj85j0107|fyyfj85j0108|fyyfj85j0209|fyyfj85j0289|fyyfj85j0301|fyyfj85j0339|fyyfj85j0352|fyyfj85j0472|fyyfj85j0500|fyyfj85j0573|fyyfj86j0104|fyyfj86j0107|fyyfj86j0108|fyyfj86j0209|fyyfj86j0289|fyyfj86j0301|fyyfj86j0339|fyyfj86j0352|fyyfj86j0472|fyyfj86j0500|fyyfj86j0573|fyyfj87j0067|fyyfj87j0104|fyyfj87j0107|fyyfj87j0108|fyyfj87j0209|fyyfj87j0289|fyyfj87j0301|fyyfj87j0339|fyyfj87j0352|fyyfj87j0472|fyyfj87j0500|fyyfj87j0573|fyyfj88j0067|fyyfj88j0104|fyyfj88j0107|fyyfj88j0108|fyyfj88j0209|fyyfj88j0289|fyyfj88j0301|fyyfj88j0339|fyyfj88j0352|fyyfj88j0472|fyyfj88j0500|fyyfj88j0573|fyyfj89j0067|fyyfj89j0104|fyyfj89j0107|fyyfj89j0108|fyyfj89j0209|fyyfj89j0289|fyyfj89j0301|fyyfj89j0339|fyyfj89j0352|fyyfj89j0358|fyyfj89j0472|fyyfj89j0500|fyyfj89j0573|fyyfj90j0067|fyyfj90j0104|fyyfj90j0107|fyyfj90j0108|fyyfj90j0209|fyyfj90j0289|fyyfj90j0301|fyyfj90j0321|fyyfj90j0339|fyyfj90j0352|fyyfj90j0358|fyyfj90j0452|fyyfj90j0472|fyyfj90j0500|fyyfj90j0573|fyyfj91j0067|fyyfj91j0104|fyyfj91j0107|fyyfj91j0108|fyyfj91j0209|fyyfj91j0289|fyyfj91j0301|fyyfj91j0321|fyyfj91j0339|fyyfj91j0352|fyyfj91j0358|fyyfj91j0452|fyyfj91j0472|fyyfj91j0500|fyyfj91j0573|fyyfj92j0067|fyyfj92j0104|fyyfj92j0107|fyyfj92j0108|fyyfj92j0209|fyyfj92j0289|fyyfj92j0301|fyyfj92j0321|fyyfj92j0339|fyyfj92j0352|fyyfj92j0358|fyyfj92j0452|fyyfj92j0472|fyyfj92j0500|fyyfj92j0573|fyyfj93j0067|fyyfj93j0099|fyyfj93j0104|fyyfj93j0107|fyyfj93j0108|fyyfj93j0209|fyyfj93j0289|fyyfj93j0301|fyyfj93j0321|fyyfj93j0352|fyyfj93j0358|fyyfj93j0452|fyyfj93j0472|fyyfj93j0500|fyyfj93j0573|fyyfj94j0067|fyyfj94j0099|fyyfj94j0104|fyyfj94j0107|fyyfj94j0108|fyyfj94j0209|fyyfj94j0211|fyyfj94j0289|fyyfj94j0301|fyyfj94j0321|fyyfj94j0352|fyyfj94j0358|fyyfj94j0359|fyyfj94j0452|fyyfj94j0472|fyyfj94j0500|fyyfj94j0573|fyyfj95j0067|fyyfj95j0099|fyyfj95j0104|fyyfj95j0107|fyyfj95j0108|fyyfj95j0209|fyyfj95j0211|fyyfj95j0289|fyyfj95j0298|fyyfj95j0301|fyyfj95j0321|fyyfj95j0339|fyyfj95j0352|fyyfj95j0358|fyyfj95j0359|fyyfj95j0414|fyyfj95j0452|fyyfj95j0472|fyyfj95j0500|fyyfj95j0573|fyyfj96j0067|fyyfj96j0099|fyyfj96j0104|fyyfj96j0107|fyyfj96j0108|fyyfj96j0209|fyyfj96j0211|fyyfj96j0289|fyyfj96j0298|fyyfj96j0301|fyyfj96j0321|fyyfj96j0339|fyyfj96j0352|fyyfj96j0358|fyyfj96j0359|fyyfj96j0414|fyyfj96j0452|fyyfj96j0472|fyyfj96j0500|fyyfj96j0573|fyyfj97j0067|fyyfj97j0099|fyyfj97j0100|fyyfj97j0104|fyyfj97j0107|fyyfj97j0108|fyyfj97j0209|fyyfj97j0211|fyyfj97j0289|fyyfj97j0298|fyyfj97j0301|fyyfj97j0321|fyyfj97j0339|fyyfj97j0352|fyyfj97j0358|fyyfj97j0359|fyyfj97j0414|fyyfj97j0445|fyyfj97j0452|fyyfj97j0472|fyyfj97j0500|fyyfj97j0573|fyyfj98j0067|fyyfj98j0099|fyyfj98j0100|fyyfj98j0104|fyyfj98j0107|fyyfj98j0108|fyyfj98j0178|fyyfj98j0209|fyyfj98j0211|fyyfj98j0289|fyyfj98j0298|fyyfj98j0301|fyyfj98j0303|fyyfj98j0321|fyyfj98j0339|fyyfj98j0352|fyyfj98j0358|fyyfj98j0359|fyyfj98j0413|fyyfj98j0414|fyyfj98j0445|fyyfj98j0452|fyyfj98j0472|fyyfj98j0500|fyyfj98j0573|fyyfj99j0067|fyyfj99j0099|fyyfj99j0100|fyyfj99j0104|fyyfj99j0107|fyyfj99j0108|fyyfj99j0131|fyyfj99j0209|fyyfj99j0211|fyyfj99j0285|fyyfj99j0289|fyyfj99j0298|fyyfj99j0301|fyyfj99j0303|fyyfj99j0321|fyyfj99j0339|fyyfj99j0352|fyyfj99j0358|fyyfj99j0359|fyyfj99j0413|fyyfj99j0414|fyyfj99j0445|fyyfj99j0452|fyyfj99j0472|fyyfj99j0500|fyyfj99j0573|fyyfm01j0064|fyyfm01j0070|fyyfm01j0071|fyyfm01j0088|fyyfm01j0091|fyyfm01j0108|fyyfm01j0111|fyyfm01j0112|fyyfm01j0114|fyyfm01j0115|fyyfm01j0133|fyyfm01j0140|fyyfm01j0141|fyyfm01j0142|fyyfm01j0143|fyyfm01j0148|fyyfm01j0149|fyyfm01j0152|fyyfm01j0153|fyyfm01j0155|fyyfm01j0159|fyyfm01j0160|fyyfm01j0163|fyyfm01j0165|fyyfm01j0168|fyyfm01j0169|fyyfm01j0221|fyyfm01j0223|fyyfm01j0268|fyyfm01j0271|fyyfm01j0285|fyyfm01j0299|fyyfm01j0320|fyyfm01j0321|fyyfm01j0360|fyyfm01j0369|fyyfm01j0400|fyyfm01j0401|fyyfm01j0411|fyyfm01j0572|fyyfm01j0765|fyyfm02j0064|fyyfm02j0069|fyyfm02j0070|fyyfm02j0071|fyyfm02j0088|fyyfm02j0091|fyyfm02j0108|fyyfm02j0111|fyyfm02j0112|fyyfm02j0114|fyyfm02j0115|fyyfm02j0133|fyyfm02j0140|fyyfm02j0141|fyyfm02j0142|fyyfm02j0143|fyyfm02j0148|fyyfm02j0149|fyyfm02j0152|fyyfm02j0153|fyyfm02j0155|fyyfm02j0159|fyyfm02j0160|fyyfm02j0163|fyyfm02j0165|fyyfm02j0168|fyyfm02j0169|fyyfm02j0221|fyyfm02j0223|fyyfm02j0268|fyyfm02j0271|fyyfm02j0285|fyyfm02j0299|fyyfm02j0320|fyyfm02j0321|fyyfm02j0360|fyyfm02j0369|fyyfm02j0400|fyyfm02j0572|fyyfm02j0765|fyyfm03j0064|fyyfm03j0070|fyyfm03j0091|fyyfm03j0108|fyyfm03j0111|fyyfm03j0115|fyyfm03j0160|fyyfm03j0165|fyyfm03j0299|fyyfm03j0400|fyyfm03j0572|fyyfm04j0111|fyyfm51j0064|fyyfm51j0369|fyyfm52j0064|fyyfm52j0369|fyyfr88j0003|fyyfr89j0003|fyyff98j0071|fyyff98j0303|fyyff99j0029|fyyff99j0303|fyefj00j0112|fyefj00j0545|fyefj00j0546|fyefj00j0633|fyefj00j0634|fyefj00j0635|fyefj00j0636|fyefj00j0637|fyefj00j0649|fyefj00j0651|fyefj00j0652|fyefj00j0656|fyefj00j0657|fyefj00j0658|fyefj00j0659|fyefj00j0660|fyefj00j0685|fyefj00j0686|fyefj00j0688|fyefj00j0701|fyefj00j0702|fyefj00j0703|fyefj00j0715|fyefj00j0720|fyefj00j0721|fyefj00j0722|fyefj00j0724|fyefj00j0725|fyefj00j0726|fyefj00j0731|fyefj00j0751|fyefj00j0752|fyefj00j0756|fyefj00j0757|fyefj00j0758|fyefj00j0759|fyefj00j0761|fyefj00j0762|fyefj00j0763|fyefj00j0764|fyefj00j0768|fyefj00j0769|fyefj00j0785|fyefj00j0786|fyefj00j0789|fyefj00j0790|fyefj00j0793|fyefj00j0794|fyefj00j0803|fyefj00j0811|fyefj00j0821|fyefj00j0822|fyefj00j0823|fyefj00j0824|fyefj00j0825|fyefj00j0826|fyefj00j0827|fyefj00j0828|fyefj00j0829|fyefj00j0831|fyefj00j0832|fyefj00j0833|fyefj00j0838|fyefj00j0839|fyefj00j0840|fyefj00j0854|fyefj00j0855|fyefj00j0856|fyefj00j0859|fyefj00j0860|fyefj00j0861|fyefj00j0869|fyefj00j0870|fyefj00j0879|fyefj00j0887|fyefj00j0888|fyefj00j0889|fyefj00j0900|fyefj00j0901|fyefj00j0903|fyefj00j0904|fyefj00j0905|fyefj00j0959|fyefj00j0960|fyefj00j0961|fyefj00j1004|fyefj00j1005|fyefj00j1012|fyefj00j1013|fyefj00j1014|fyefj00j1015|fyefj00j1016|fyefj00j1017|fyefj00j1018|fyefj00j1019|fyefj00j1020|fyefj00j1021|fyefj00j1218|fyefj00j1219|fyefj00j1220|fyefj00j1221|fyefj00j1222|fyefj00j1811|fyefj00j1854|fyefj00j1855|fyefj00j1856|fyefj01j0707|fyefj02j0707|fyefj03j0707|fyefj66j0001|fyefj67j0001|fyefj68j0001|fyefj68j1064|fyefj69j0001|fyefj69j1064|fyefj70j0001|fyefj70j0859|fyefj70j1064|fyefj71j0001|fyefj71j1064|fyefj72j0001|fyefj72j1064|fyefj73j0001|fyefj73j1064|fyefj74j0001|fyefj74j1064|fyefj75j0001|fyefj75j1064|fyefj75j1092|fyefj76j0001|fyefj76j1064|fyefj76j1092|fyefj77j0001|fyefj77j1064|fyefj77j1092|fyefj78j0001|fyefj78j1064|fyefj78j1092|fyefj79j0001|fyefj79j1064|fyefj79j1092|fyefj80j0001|fyefj80j0859|fyefj80j1064|fyefj80j1077|fyefj80j1092|fyefj81j0001|fyefj81j1064|fyefj81j1077|fyefj81j1092|fyefj82j0001|fyefj82j1064|fyefj82j1092|fyefj83j0001|fyefj83j1064|fyefj83j1092|fyefj84j0001|fyefj84j1064|fyefj84j1092|fyefj85j0001|fyefj85j0356|fyefj85j1064|fyefj85j1092|fyefj86j0001|fyefj86j0356|fyefj86j1064|fyefj87j0001|fyefj87j0356|fyefj87j1064|fyefj88j0001|fyefj88j0356|fyefj88j1064|fyefj89j0001|fyefj89j0356|fyefj89j1064|fyefj89j1067|fyefj90j0001|fyefj90j0758|fyefj90j1021|fyefj90j1064|fyefj90j1067|fyefj91j0001|fyefj91j0758|fyefj91j0791|fyefj91j1021|fyefj91j1064|fyefj91j1067|fyefj91j1077|fyefj92j0001|fyefj92j0359|fyefj92j0678|fyefj92j0758|fyefj92j0791|fyefj92j0867|fyefj92j1021|fyefj92j1064|fyefj92j1077|fyefj93j0001|fyefj93j0359|fyefj93j0678|fyefj93j0758|fyefj93j0791|fyefj93j0867|fyefj93j1010|fyefj93j1021|fyefj93j1049|fyefj93j1064|fyefj93j1077|fyefj94j0001|fyefj94j0678|fyefj94j0758|fyefj94j0791|fyefj94j0867|fyefj94j1010|fyefj94j1021|fyefj94j1049|fyefj94j1064|fyefj94j1070|fyefj94j1077|fyefj94j1085|fyefj95j0001|fyefj95j0678|fyefj95j0758|fyefj95j0791|fyefj95j0867|fyefj95j0965|fyefj95j0966|fyefj95j1010|fyefj95j1011|fyefj95j1021|fyefj95j1055|fyefj95j1064|fyefj95j1069|fyefj95j1077|fyefj95j1085|fyefj95j1089|fyefj96j0001|fyefj96j0106|fyefj96j0671|fyefj96j0678|fyefj96j0758|fyefj96j0791|fyefj96j0814|fyefj96j0836|fyefj96j0867|fyefj96j0931|fyefj96j0965|fyefj96j0966|fyefj96j0976|fyefj96j1010|fyefj96j1021|fyefj96j1051|fyefj96j1055|fyefj96j1064|fyefj96j1068|fyefj96j1070|fyefj96j1077|fyefj96j1079|fyefj96j1081|fyefj96j1086|fyefj96j1088|fyefj96j1091|fyefj96j1093|fyefj96j1094|fyefj97j0001|fyefj97j0106|fyefj97j0584|fyefj97j0586|fyefj97j0671|fyefj97j0678|fyefj97j0758|fyefj97j0791|fyefj97j0814|fyefj97j0825|fyefj97j0836|fyefj97j0863|fyefj97j0865|fyefj97j0867|fyefj97j0914|fyefj97j0931|fyefj97j0952|fyefj97j0965|fyefj97j0966|fyefj97j0969|fyefj97j0971|fyefj97j0972|fyefj97j0976|fyefj97j0985|fyefj97j1010|fyefj97j1021|fyefj97j1051|fyefj97j1052|fyefj97j1055|fyefj97j1058|fyefj97j1059|fyefj97j1064|fyefj97j1068|fyefj97j1077|fyefj97j1079|fyefj97j1081|fyefj97j1086|fyefj97j1088|fyefj97j1095|fyefj98j0001|fyefj98j0243|fyefj98j0326|fyefj98j0329|fyefj98j0343|fyefj98j0344|fyefj98j0380|fyefj98j0472|fyefj98j0584|fyefj98j0586|fyefj98j0604|fyefj98j0671|fyefj98j0673|fyefj98j0676|fyefj98j0677|fyefj98j0678|fyefj98j0694|fyefj98j0758|fyefj98j0814|fyefj98j0825|fyefj98j0836|fyefj98j0863|fyefj98j0865|fyefj98j0867|fyefj98j0896|fyefj98j0898|fyefj98j0901|fyefj98j0906|fyefj98j0910|fyefj98j0913|fyefj98j0914|fyefj98j0922|fyefj98j0931|fyefj98j0934|fyefj98j0936|fyefj98j0951|fyefj98j0952|fyefj98j0963|fyefj98j0965|fyefj98j0966|fyefj98j0969|fyefj98j0971|fyefj98j0972|fyefj98j0974|fyefj98j0975|fyefj98j0976|fyefj98j0977|fyefj98j0978|fyefj98j0985|fyefj98j0992|fyefj98j1008|fyefj98j1009|fyefj98j1010|fyefj98j1011|fyefj98j1012|fyefj98j1019|fyefj98j1021|fyefj98j1028|fyefj98j1034|fyefj98j1039|fyefj98j1046|fyefj98j1047|fyefj98j1048|fyefj98j1054|fyefj98j1055|fyefj98j1064|fyefj98j1068|fyefj98j1077|fyefj98j1079|fyefj98j1080|fyefj98j1081|fyefj98j1082|fyefj98j1084|fyefj98j1087|fyefj98j1088|fyefj98j1090|fyefj99j0010|fyefj99j0188|fyefj99j0243|fyefj99j0268|fyefj99j0280|fyefj99j0301|fyefj99j0329|fyefj99j0343|fyefj99j0344|fyefj99j0380|fyefj99j0552|fyefj99j0573|fyefj99j0584|fyefj99j0586|fyefj99j0604|fyefj99j0671|fyefj99j0673|fyefj99j0676|fyefj99j0677|fyefj99j0678|fyefj99j0694|fyefj99j0722|fyefj99j0757|fyefj99j0758|fyefj99j0771|fyefj99j0772|fyefj99j0804|fyefj99j0806|fyefj99j0809|fyefj99j0814|fyefj99j0825|fyefj99j0836|fyefj99j0862|fyefj99j0863|fyefj99j0865|fyefj99j0866|fyefj99j0867|fyefj99j0875|fyefj99j0896|fyefj99j0898|fyefj99j0901|fyefj99j0906|fyefj99j0907|fyefj99j0908|fyefj99j0910|fyefj99j0912|fyefj99j0913|fyefj99j0914|fyefj99j0921|fyefj99j0922|fyefj99j0923|fyefj99j0931|fyefj99j0934|fyefj99j0936|fyefj99j0937|fyefj99j0949|fyefj99j0951|fyefj99j0952|fyefj99j0962|fyefj99j0963|fyefj99j0965|fyefj99j0966|fyefj99j0969|fyefj99j0971|fyefj99j0972|fyefj99j0974|fyefj99j0975|fyefj99j0976|fyefj99j0977|fyefj99j0978|fyefj99j0982|fyefj99j0985|fyefj99j0986|fyefj99j0988|fyefj99j0991|fyefj99j0992|fyefj99j0995|fyefj99j0997|fyefj99j0999|fyefj99j1003|fyefj99j1006|fyefj99j1008|fyefj99j1009|fyefj99j1010|fyefj99j1011|fyefj99j1016|fyefj99j1019|fyefj99j1020|fyefj99j1021|fyefj99j1024|fyefj99j1026|fyefj99j1028|fyefj99j1031|fyefj99j1033|fyefj99j1034|fyefj99j1036|fyefj99j1039|fyefj99j1042|fyefj99j1045|fyefj99j1046|fyefj99j1048|fyefj99j1053|fyefj99j1054|fyefj99j1055|fyefj99j1061|fyefj99j1062|fyefj99j1063|fyefj99j1064|fyefj99j1068|fyefj99j1072|fyefj99j1076|fyefj99j1077|fyefj99j1079|fyefj99j1080|fyefj99j1081|fyefj99j1083|fyefj99j1084|fyefj99j1087|fyefj99j1088|fyefm00j0113|fyefm01j0057|fyefm01j0088|fyefm01j0091|fyefm01j0101|fyefm01j0104|fyefm01j0107|fyefm01j0112|fyefm01j0379|fyefm02j0057|fyefm02j0101|fyefm02j0104|fyefm02j0107|fyefm02j0112|fyefm02j0379|fyefm98j0066|fyefm99j0066|fyefm99j0090|fyefm99j0093|fyefm99j0110|fyefm99j0165|fyefm99j0208|fyefm99j0209|fyefm99j0295|fyefm99j0401|fyefm99j0402|fyefm99j0907|fyefm99j1054|fyefn98j0015|fyefn98j0024|fyefn98j0030|fyefn99j0015|fyefn99j0024|fyefn99j0030|fyefr94j0559|fyefr95j0559|fyefr96j0559|fyefr97j0559|fyefr98j0559|fyefr99j0012|fyefr99j0559|fyefb01305|fyeff00j0170|fyeff00j0224|fyeff00j0227|fyeff00j0228|fyeff00j0229|fyeff00j0280|fyeff00j0281|fyeff00j0282|fyeff00j0283|fyeff00j0288|fyeff00j0289|fyeff00j0331|fyeff00j0332|fyeff00j0333|fyeff00j0334|fyeff00j0335|fyeff00j0336|fyeff00j0337|fyeff00j0338|fyeff00j0346|fyeff00j0347|fyeff00j0348|fyeff00j0349|fyeff00j0350|fyeff00j0351|fyeff00j0357|fyeff00j0358|fyeff00j0371|fyeff00j0372|fyeff00j0396|fyeff00j0397|fyeff00j0424|fyeff00j0425|fyeff01j0416|fyeff02j0416|fyeff78j0418|fyeff79j0418|fyeff79j1051|fyeff80j1051|fyeff81j1051|fyeff82j1051|fyeff83j1051|fyeff84j1051|fyeff85j1051|fyeff86j1051|fyeff87j1051|fyeff88j0422|fyeff89j0422|fyeff90j0422|fyeff90j0434|fyeff90j0440|fyeff91j0422|fyeff91j0434|fyeff91j0440|fyeff92j0440|fyeff93j0440|fyeff93j1045|fyeff93j1067|fyeff94j0392|fyeff94j0440|fyeff94j0443|fyeff94j1045|fyeff94j1067|fyeff95j0219|fyeff95j0392|fyeff95j0439|fyeff95j0440|fyeff95j0443|fyeff96j0053|fyeff96j0219|fyeff96j0392|fyeff96j0429|fyeff96j0434|fyeff96j0950|fyeff96j1019|fyeff96j1028|fyeff97j0053|fyeff97j0178|fyeff97j0191|fyeff97j0219|fyeff97j0221|fyeff97j0258|fyeff97j0324|fyeff97j0355|fyeff97j0370|fyeff97j0377|fyeff97j0392|fyeff97j0429|fyeff97j0434|fyeff97j0950|fyeff97j1019|fyeff98j0053|fyeff98j0065|fyeff98j0101|fyeff98j0144|fyeff98j0156|fyeff98j0178|fyeff98j0191|fyeff98j0193|fyeff98j0196|fyeff98j0197|fyeff98j0209|fyeff98j0210|fyeff98j0211|fyeff98j0214|fyeff98j0215|fyeff98j0218|fyeff98j0219|fyeff98j0221|fyeff98j0258|fyeff98j0260|fyeff98j0279|fyeff98j0284|fyeff98j0295|fyeff98j0296|fyeff98j0298|fyeff98j0324|fyeff98j0355|fyeff98j0370|fyeff98j0376|fyeff98j0379|fyeff98j0381|fyeff98j0392|fyeff98j0401|fyeff98j0404|fyeff98j0405|fyeff98j0407|fyeff98j0411|fyeff98j0418|fyeff98j0421|fyeff98j0423|fyeff98j0433|fyeff98j0436|fyeff98j0673|fyeff98j0896|fyeff98j0950|fyeff98j0985|fyeff98j1012|fyeff99j0053|fyeff99j0065|fyeff99j0152|fyeff99j0156|fyeff99j0159|fyeff99j0178|fyeff99j0191|fyeff99j0193|fyeff99j0196|fyeff99j0197|fyeff99j0209|fyeff99j0210|fyeff99j0211|fyeff99j0214|fyeff99j0215|fyeff99j0218|fyeff99j0219|fyeff99j0220|fyeff99j0221|fyeff99j0260|fyeff99j0279|fyeff99j0284|fyeff99j0291|fyeff99j0295|fyeff99j0296|fyeff99j0297|fyeff99j0298|fyeff99j0324|fyeff99j0339|fyeff99j0355|fyeff99j0370|fyeff99j0376|fyeff99j0379|fyeff99j0381|fyeff99j0392|fyeff99j0401|fyeff99j0404|fyeff99j0405|fyeff99j0407|fyeff99j0410|fyeff99j0411|fyeff99j0413|fyeff99j0414|fyeff99j0415|fyeff99j0418|fyeff99j0421|fyeff99j0423|fyeff99j0436|fyeff99j0673|fyeff99j0896|fyeff99j0950|fyeff99j0962|fyeff99j0985|fyeff99j1010|fyeff99j1012|fyeff99j1028|fyeff99j1090|fyeff99j1370|fayfm01j0148|fayfm01j0149|fayfm01j0155|fayfm02j0148|fayfm02j0149|fayfm02j0155|faefj00j0594|faefj00j0595|faefj00j0596|faefj00j0597|faefj01j0707|faefj02j0707|faefj03j0707|faefj90j1023|faefj91j1023|faefj92j1023|faefj94j1056|faefj95j1023|faefj95j1056|faefj96j1056|faefj98j1038|faefj99j1078|fdeff99j9001|fdeff99j9002|gyefj99j0005",
+ // A long case sensitive alternation where each entry is sandwiched by .*
+ ".*zQPbMkNO.*|.*NNSPdvMi.*|.*iWuuSoAl.*|.*qbvKMimS.*|.*IecrXtPa.*|.*seTckYqt.*|.*NxnyHkgB.*|.*fIDlOgKb.*|.*UhlWIygH.*|.*OtNoJxHG.*|.*cUTkFVIV.*|.*mTgFIHjr.*|.*jQkoIDtE.*|.*PPMKxRXl.*|.*AwMfwVkQ.*|.*CQyMrTQJ.*|.*BzrqxVSi.*|.*nTpcWuhF.*|.*PertdywG.*|.*ZZDgCtXN.*|.*WWdDPyyE.*|.*uVtNQsKk.*|.*BdeCHvPZ.*|.*wshRnFlH.*|.*aOUIitIp.*|.*RxZeCdXT.*|.*CFZMslCj.*|.*AVBZRDxl.*|.*IzIGCnhw.*|.*ythYuWiz.*|.*oztXVXhl.*|.*VbLkwqQx.*|.*qvaUgyVC.*|.*VawUjPWC.*|.*ecloYJuj.*|.*boCLTdSU.*|.*uPrKeAZx.*|.*hrMWLWBq.*|.*JOnUNHRM.*|.*rYnujkPq.*|.*dDEdZhIj.*|.*DRrfvugG.*|.*yEGfDxVV.*|.*YMYdJWuP.*|.*PHUQZNWM.*|.*AmKNrLis.*|.*zTxndVfn.*|.*FPsHoJnc.*|.*EIulZTua.*|.*KlAPhdzg.*|.*ScHJJCLt.*|.*NtTfMzME.*|.*eMCwuFdo.*|.*SEpJVJbR.*|.*cdhXZeCx.*|.*sAVtBwRh.*|.*kVFEVcMI.*|.*jzJrxraA.*|.*tGLHTell.*|.*NNWoeSaw.*|.*DcOKSetX.*|.*UXZAJyka.*|.*THpMphDP.*|.*rizheevl.*|.*kDCBRidd.*|.*pCZZRqyu.*|.*pSygkitl.*|.*SwZGkAaW.*|.*wILOrfNX.*|.*QkwVOerj.*|.*kHOMxPDr.*|.*EwOVycJv.*|.*AJvtzQFS.*|.*yEOjKYYB.*|.*LizIINLL.*|.*JBRSsfcG.*|.*YPiUqqNl.*|.*IsdEbvee.*|.*MjEpGcBm.*|.*OxXZVgEQ.*|.*xClXGuxa.*|.*UzRCGFEb.*|.*buJbvfvA.*|.*IPZQxRet.*|.*oFYShsMc.*|.*oBHffuHO.*|.*bzzKrcBR.*|.*KAjzrGCl.*|.*IPUsAVls.*|.*OGMUMbIU.*|.*gyDccHuR.*|.*bjlalnDd.*|.*ZLWjeMna.*|.*fdsuIlxQ.*|.*dVXtiomV.*|.*XxedTjNg.*|.*XWMHlNoA.*|.*nnyqArQX.*|.*opfkWGhb.*|.*wYtnhdYb.*",
// A long case insensitive alternation.
"(?i:(zQPbMkNO|NNSPdvMi|iWuuSoAl|qbvKMimS|IecrXtPa|seTckYqt|NxnyHkgB|fIDlOgKb|UhlWIygH|OtNoJxHG|cUTkFVIV|mTgFIHjr|jQkoIDtE|PPMKxRXl|AwMfwVkQ|CQyMrTQJ|BzrqxVSi|nTpcWuhF|PertdywG|ZZDgCtXN|WWdDPyyE|uVtNQsKk|BdeCHvPZ|wshRnFlH|aOUIitIp|RxZeCdXT|CFZMslCj|AVBZRDxl|IzIGCnhw|ythYuWiz|oztXVXhl|VbLkwqQx|qvaUgyVC|VawUjPWC|ecloYJuj|boCLTdSU|uPrKeAZx|hrMWLWBq|JOnUNHRM|rYnujkPq|dDEdZhIj|DRrfvugG|yEGfDxVV|YMYdJWuP|PHUQZNWM|AmKNrLis|zTxndVfn|FPsHoJnc|EIulZTua|KlAPhdzg|ScHJJCLt|NtTfMzME|eMCwuFdo|SEpJVJbR|cdhXZeCx|sAVtBwRh|kVFEVcMI|jzJrxraA|tGLHTell|NNWoeSaw|DcOKSetX|UXZAJyka|THpMphDP|rizheevl|kDCBRidd|pCZZRqyu|pSygkitl|SwZGkAaW|wILOrfNX|QkwVOerj|kHOMxPDr|EwOVycJv|AJvtzQFS|yEOjKYYB|LizIINLL|JBRSsfcG|YPiUqqNl|IsdEbvee|MjEpGcBm|OxXZVgEQ|xClXGuxa|UzRCGFEb|buJbvfvA|IPZQxRet|oFYShsMc|oBHffuHO|bzzKrcBR|KAjzrGCl|IPUsAVls|OGMUMbIU|gyDccHuR|bjlalnDd|ZLWjeMna|fdsuIlxQ|dVXtiomV|XxedTjNg|XWMHlNoA|nnyqArQX|opfkWGhb|wYtnhdYb))",
"(?i:(AAAAAAAAAAAAAAAAAAAAAAAA|BBBBBBBBBBBBBBBBBBBBBBBB|cccccccccccccccccccccccC|ſſſſſſſſſſſſſſſſſſſſſſſſS|SSSSSSSSSSSSSSSSSSSSSSSSſ))",
@@ -87,15 +96,24 @@ var (
"ſſs",
// Concat of literals and wildcards.
".*-.*-.*-.*-.*",
+ ".+-.*-.*-.*-.+",
+ "-.*-.*-.*-.*",
+ ".*-.*-.*-.*-",
"(.+)-(.+)-(.+)-(.+)-(.+)",
"((.*))(?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", "",
"FOO", "Foo", "fOo", "foO", "OO", "Oo", "\nfoo\n", strings.Repeat("f", 20), "prometheus", "prometheus_api_v1", "prometheus_api_v1_foo",
"10.0.1.20", "10.0.2.10", "10.0.3.30", "10.0.4.40",
"foofoo0", "foofoo", "😀foo0", "ſſs", "ſſS", "AAAAAAAAAAAAAAAAAAAAAAAA", "BBBBBBBBBBBBBBBBBBBBBBBB", "cccccccccccccccccccccccC", "ſſſſſſſſſſſſſſſſſſſſſſſſS", "SSSSSSSSSSSSSSSSSSSSSSSSſ",
+ "a-b-c-d-e",
+ "aaaaaa-bbbbbb-cccccc-dddddd-eeeeee",
+ "aaaaaa----eeeeee",
+ "----",
+ "-a-a-a-",
// Values matching / not matching the test regexps on long alternations.
"zQPbMkNO", "zQPbMkNo", "jyyfj00j0061", "jyyfj00j006", "jyyfj00j00612", "NNSPdvMi", "NNSPdvMiXXX", "NNSPdvMixxx", "nnSPdvMi", "nnSPdvMiXXX",
@@ -162,6 +180,7 @@ func TestOptimizeConcatRegex(t *testing.T) {
{regex: "^5..$", prefix: "5", suffix: "", contains: nil},
{regex: "^release.*", prefix: "release", suffix: "", contains: nil},
{regex: "^env-[0-9]+laio[1]?[^0-9].*", prefix: "env-", suffix: "", contains: []string{"laio"}},
+ {regex: ".*-.*-.*-.*-.*", prefix: "", suffix: "", contains: []string{"-", "-", "-", "-"}},
}
for _, c := range cases {
@@ -341,7 +360,7 @@ func BenchmarkToNormalizedLower(b *testing.B) {
}
}
-func TestStringMatcherFromRegexp(t *testing.T) {
+func TestNewFastRegexMatcher(t *testing.T) {
for _, c := range []struct {
pattern string
exp StringMatcher
@@ -364,12 +383,12 @@ func TestStringMatcherFromRegexp(t *testing.T) {
{`(?i:((foo1|foo2|bar)))`, orStringMatcher([]StringMatcher{orStringMatcher([]StringMatcher{&equalStringMatcher{s: "FOO1", caseSensitive: false}, &equalStringMatcher{s: "FOO2", caseSensitive: false}}), &equalStringMatcher{s: "BAR", caseSensitive: false}})},
{"^((?i:foo|oo)|(bar))$", orStringMatcher([]StringMatcher{&equalStringMatcher{s: "FOO", caseSensitive: false}, &equalStringMatcher{s: "OO", caseSensitive: false}, &equalStringMatcher{s: "bar", caseSensitive: true}})},
{"(?i:(foo1|foo2|bar))", orStringMatcher([]StringMatcher{orStringMatcher([]StringMatcher{&equalStringMatcher{s: "FOO1", caseSensitive: false}, &equalStringMatcher{s: "FOO2", caseSensitive: false}}), &equalStringMatcher{s: "BAR", caseSensitive: false}})},
- {".*foo.*", &containsStringMatcher{substrings: []string{"foo"}, left: trueMatcher{}, right: trueMatcher{}}},
- {"(.*)foo.*", &containsStringMatcher{substrings: []string{"foo"}, left: trueMatcher{}, right: trueMatcher{}}},
- {"(.*)foo(.*)", &containsStringMatcher{substrings: []string{"foo"}, left: trueMatcher{}, right: trueMatcher{}}},
+ {".*foo.*", trueMatcher{}}, // The containsInOrder check done in the function returned by compileMatchStringFunction is sufficient.
+ {"(.*)foo.*", trueMatcher{}}, // The containsInOrder check done in the function returned by compileMatchStringFunction is sufficient.
+ {"(.*)foo(.*)", trueMatcher{}}, // The containsInOrder check done in the function returned by compileMatchStringFunction is sufficient.
{"(.+)foo(.*)", &containsStringMatcher{substrings: []string{"foo"}, left: &anyNonEmptyStringMatcher{matchNL: true}, right: trueMatcher{}}},
{"^.+foo.+", &containsStringMatcher{substrings: []string{"foo"}, left: &anyNonEmptyStringMatcher{matchNL: true}, right: &anyNonEmptyStringMatcher{matchNL: true}}},
- {"^(.*)(foo)(.*)$", &containsStringMatcher{substrings: []string{"foo"}, left: trueMatcher{}, right: trueMatcher{}}},
+ {"^(.*)(foo)(.*)$", trueMatcher{}}, // The containsInOrder check done in the function returned by compileMatchStringFunction is sufficient.
{"^(.*)(foo|foobar)(.*)$", &containsStringMatcher{substrings: []string{"foo", "foobar"}, left: trueMatcher{}, right: trueMatcher{}}},
{"^(.*)(foo|foobar)(.+)$", &containsStringMatcher{substrings: []string{"foo", "foobar"}, left: trueMatcher{}, right: &anyNonEmptyStringMatcher{matchNL: true}}},
{"^(.*)(bar|b|buzz)(.+)$", &containsStringMatcher{substrings: []string{"bar", "b", "buzz"}, left: trueMatcher{}, right: &anyNonEmptyStringMatcher{matchNL: true}}},
@@ -388,7 +407,7 @@ func TestStringMatcherFromRegexp(t *testing.T) {
{"(api|rpc)_(v1|prom)_((?i)push|query)", nil},
{"[a-z][a-z]", nil},
{"[1^3]", nil},
- {".*foo.*bar.*", nil},
+ {".*foo.*bar.*", trueMatcher{}}, // The containsInOrder check done in the function returned by compileMatchStringFunction is sufficient.
{`\d*`, nil},
{".", nil},
{"/|/bar.*", &literalPrefixSensitiveStringMatcher{prefix: "/", right: orStringMatcher{emptyStringMatcher{}, &literalPrefixSensitiveStringMatcher{prefix: "bar", right: trueMatcher{}}}}},
@@ -412,13 +431,13 @@ func TestStringMatcherFromRegexp(t *testing.T) {
{"(?s)(ext.?|xfs)", orStringMatcher{&literalPrefixSensitiveStringMatcher{prefix: "ext", right: &zeroOrOneCharacterStringMatcher{matchNL: true}}, &equalStringMatcher{s: "xfs", caseSensitive: true}}},
{"foo.?", &literalPrefixSensitiveStringMatcher{prefix: "foo", right: &zeroOrOneCharacterStringMatcher{matchNL: true}}},
{"f.?o", nil},
+ {".*foo.*|.*bar.*|.*baz.*", &containsStringMatcher{left: trueMatcher{}, substrings: []string{"foo", "bar", "baz"}, right: trueMatcher{}}},
} {
t.Run(c.pattern, func(t *testing.T) {
t.Parallel()
- parsed, err := syntax.Parse(c.pattern, syntax.Perl|syntax.DotNL)
+ matcher, err := NewFastRegexMatcher(c.pattern)
require.NoError(t, err)
- matches := stringMatcherFromRegexp(parsed)
- require.Equal(t, c.exp, matches)
+ require.Equal(t, c.exp, matcher.stringMatcher)
})
}
}
@@ -1389,3 +1408,42 @@ func TestToNormalisedLower(t *testing.T) {
require.Equal(t, expectedOutput, toNormalisedLower(input, nil))
}
}
+
+func TestIsSimpleConcatenationPattern(t *testing.T) {
+ testCases := map[string]bool{
+ ".*-.*-.*-.*-.*": true,
+ ".+-.*-.*-.*-.+": false,
+ "-.*-.*-.*-.*": false,
+ ".*-.*-.*-.*-": false,
+ "-": false,
+ ".*": false,
+ }
+
+ for testCase, expected := range testCases {
+ t.Run(testCase, func(t *testing.T) {
+ re, err := syntax.Parse(testCase, syntax.Perl|syntax.DotNL)
+ require.NoError(t, err)
+ require.Equal(t, expected, isSimpleConcatenationPattern(re))
+ })
+ }
+}
+
+func BenchmarkFastRegexMatcher_ConcatenatedPattern(b *testing.B) {
+ pattern, err := NewFastRegexMatcher(".*-.*-.*-.*-.*")
+ require.NoError(b, err)
+
+ testCases := []string{
+ "a-b-c-d-e",
+ "aaaaaa-bbbbbb-cccccc-dddddd-eeeeee",
+ "aaaaaa----eeeeee",
+ "----",
+ "-a-a-a-",
+ "abcd",
+ }
+
+ for b.Loop() {
+ for _, s := range testCases {
+ pattern.MatchString(s)
+ }
+ }
+}
diff --git a/model/labels/sharding.go b/model/labels/sharding.go
index ed05da675f..6394d0a01e 100644
--- a/model/labels/sharding.go
+++ b/model/labels/sharding.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
diff --git a/model/labels/sharding_dedupelabels.go b/model/labels/sharding_dedupelabels.go
index 5bf41b05d6..11342146a8 100644
--- a/model/labels/sharding_dedupelabels.go
+++ b/model/labels/sharding_dedupelabels.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Prometheus Authors
+// 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
diff --git a/model/labels/sharding_stringlabels.go b/model/labels/sharding_stringlabels.go
index 4dcbaa21d1..776a58bb5e 100644
--- a/model/labels/sharding_stringlabels.go
+++ b/model/labels/sharding_stringlabels.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
diff --git a/model/labels/sharding_test.go b/model/labels/sharding_test.go
index 78e3047509..8d094d780e 100644
--- a/model/labels/sharding_test.go
+++ b/model/labels/sharding_test.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
diff --git a/model/labels/test_utils.go b/model/labels/test_utils.go
index 66020799e9..21d1d71296 100644
--- a/model/labels/test_utils.go
+++ b/model/labels/test_utils.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
diff --git a/model/metadata/metadata.go b/model/metadata/metadata.go
index 1b7e63e0f3..d2a91bb560 100644
--- a/model/metadata/metadata.go
+++ b/model/metadata/metadata.go
@@ -1,4 +1,4 @@
-// Copyright 2022 The Prometheus Authors
+// 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
@@ -13,7 +13,11 @@
package metadata
-import "github.com/prometheus/common/model"
+import (
+ "strings"
+
+ "github.com/prometheus/common/model"
+)
// Metadata stores a series' metadata information.
type Metadata struct {
@@ -21,3 +25,21 @@ type Metadata struct {
Unit string `json:"unit"`
Help string `json:"help"`
}
+
+// IsEmpty returns true if metadata structure is empty, including unknown type case.
+func (m Metadata) IsEmpty() bool {
+ return (m.Type == "" || m.Type == model.MetricTypeUnknown) && m.Unit == "" && m.Help == ""
+}
+
+// Equals returns true if m is semantically the same as other metadata.
+func (m Metadata) Equals(other Metadata) bool {
+ if strings.Compare(m.Unit, other.Unit) != 0 || strings.Compare(m.Help, other.Help) != 0 {
+ return false
+ }
+
+ // Unknown means the same as empty string.
+ if m.Type == "" || m.Type == model.MetricTypeUnknown {
+ return other.Type == "" || other.Type == model.MetricTypeUnknown
+ }
+ return m.Type == other.Type
+}
diff --git a/model/metadata/metadata_test.go b/model/metadata/metadata_test.go
new file mode 100644
index 0000000000..169cd60c2e
--- /dev/null
+++ b/model/metadata/metadata_test.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.
+
+package metadata
+
+import (
+ "testing"
+
+ "github.com/prometheus/common/model"
+ "github.com/stretchr/testify/require"
+)
+
+func TestMetadata_IsEmpty(t *testing.T) {
+ for _, tt := range []struct {
+ name string
+ m Metadata
+ expected bool
+ }{
+ {
+ name: "empty struct", expected: true,
+ },
+ {
+ name: "unknown type with empty fields", expected: true,
+ m: Metadata{Type: model.MetricTypeUnknown},
+ },
+ {
+ name: "type", expected: false,
+ m: Metadata{Type: model.MetricTypeCounter},
+ },
+ {
+ name: "unit", expected: false,
+ m: Metadata{Unit: "seconds"},
+ },
+ {
+ name: "help", expected: false,
+ m: Metadata{Help: "help text"},
+ },
+ {
+ name: "unknown type with help", expected: false,
+ m: Metadata{Type: model.MetricTypeUnknown, Help: "help text"},
+ },
+ } {
+ t.Run(tt.name, func(t *testing.T) {
+ require.Equal(t, tt.expected, tt.m.IsEmpty())
+ })
+ }
+}
+
+func TestMetadata_Equals(t *testing.T) {
+ for _, tt := range []struct {
+ name string
+ m Metadata
+ other Metadata
+ expected bool
+ }{
+ {
+ name: "same empty", expected: true,
+ },
+ {
+ name: "same", expected: true,
+ m: Metadata{Type: model.MetricTypeCounter, Unit: "s", Help: "doc"},
+ other: Metadata{Type: model.MetricTypeCounter, Unit: "s", Help: "doc"},
+ },
+ {
+ name: "same unknown type", expected: true,
+ m: Metadata{Type: model.MetricTypeUnknown, Unit: "s", Help: "doc"},
+ other: Metadata{Type: model.MetricTypeUnknown, Unit: "s", Help: "doc"},
+ },
+ {
+ name: "same mixed unknown type", expected: true,
+ m: Metadata{Type: "", Unit: "s", Help: "doc"},
+ other: Metadata{Type: model.MetricTypeUnknown, Unit: "s", Help: "doc"},
+ },
+ {
+ name: "different unit", expected: false,
+ m: Metadata{Type: model.MetricTypeCounter, Unit: "s", Help: "doc"},
+ other: Metadata{Type: model.MetricTypeCounter, Unit: "bytes", Help: "doc"},
+ },
+ {
+ name: "different help", expected: false,
+ m: Metadata{Type: model.MetricTypeCounter, Unit: "s", Help: "doc"},
+ other: Metadata{Type: model.MetricTypeCounter, Unit: "s", Help: "other doc"},
+ },
+ {
+ name: "different type", expected: false,
+ m: Metadata{Type: model.MetricTypeCounter, Unit: "s", Help: "doc"},
+ other: Metadata{Type: model.MetricTypeGauge, Unit: "s", Help: "doc"},
+ },
+ {
+ name: "different type with unknown", expected: false,
+ m: Metadata{Type: model.MetricTypeUnknown, Unit: "s", Help: "doc"},
+ other: Metadata{Type: model.MetricTypeCounter, Unit: "s", Help: "doc"},
+ },
+ {
+ name: "different type with empty", expected: false,
+ m: Metadata{Type: "", Unit: "s", Help: "doc"},
+ other: Metadata{Type: model.MetricTypeCounter, Unit: "s", Help: "doc"},
+ },
+ } {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := tt.m.Equals(tt.other); got != tt.expected {
+ t.Errorf("Metadata.Equals() = %v, expected %v", got, tt.expected)
+ }
+ })
+ }
+}
diff --git a/model/relabel/relabel.go b/model/relabel/relabel.go
index f7085037fd..4045cc65db 100644
--- a/model/relabel/relabel.go
+++ b/model/relabel/relabel.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
@@ -269,22 +269,8 @@ func (re Regexp) String() string {
return str[5 : len(str)-2]
}
-// Process returns a relabeled version of the given label set. The relabel configurations
-// are applied in order of input.
-// There are circumstances where Process will modify the input label.
-// If you want to avoid issues with the input label set being modified, at the cost of
-// higher memory usage, you can use lbls.Copy().
-// If a label set is dropped, EmptyLabels and false is returned.
-func Process(lbls labels.Labels, cfgs ...*Config) (ret labels.Labels, keep bool) {
- lb := labels.NewBuilder(lbls)
- if !ProcessBuilder(lb, cfgs...) {
- return labels.EmptyLabels(), false
- }
- return lb.Labels(), true
-}
-
-// ProcessBuilder is like Process, but the caller passes a labels.Builder
-// containing the initial set of labels, which is mutated by the rules.
+// ProcessBuilder applies relabeling configurations (rules) to the labels in lb.
+// The rules are applied in order of input. Returns false if the rule says to drop.
func ProcessBuilder(lb *labels.Builder, cfgs ...*Config) (keep bool) {
for _, cfg := range cfgs {
keep = relabel(cfg, lb)
diff --git a/model/relabel/relabel_test.go b/model/relabel/relabel_test.go
index 7ce3c86549..8c2ba55ea7 100644
--- a/model/relabel/relabel_test.go
+++ b/model/relabel/relabel_test.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
@@ -751,10 +751,11 @@ func TestRelabel(t *testing.T) {
require.NoError(t, cfg.Validate(model.UTF8Validation))
}
- res, keep := Process(test.input, test.relabel...)
+ lb := labels.NewBuilder(test.input)
+ keep := ProcessBuilder(lb, test.relabel...)
require.Equal(t, !test.drop, keep)
if keep {
- testutil.RequireEqual(t, test.output, res)
+ testutil.RequireEqual(t, test.output, lb.Labels())
}
}
}
@@ -1064,9 +1065,11 @@ func BenchmarkRelabel(b *testing.B) {
require.NoError(b, err)
}
for _, tt := range tests {
+ lb := labels.NewBuilder(labels.EmptyLabels())
b.Run(tt.name, func(b *testing.B) {
for b.Loop() {
- _, _ = Process(tt.lbls, tt.cfgs...)
+ lb.Reset(tt.lbls)
+ _ = ProcessBuilder(lb, tt.cfgs...)
}
})
}
diff --git a/model/rulefmt/rulefmt.go b/model/rulefmt/rulefmt.go
index 83203ba769..d284a14c40 100644
--- a/model/rulefmt/rulefmt.go
+++ b/model/rulefmt/rulefmt.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
@@ -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"
@@ -97,7 +97,7 @@ type ruleGroups struct {
}
// Validate validates all rules in the rule groups.
-func (g *RuleGroups) Validate(node ruleGroups, nameValidationScheme model.ValidationScheme) (errs []error) {
+func (g *RuleGroups) Validate(node ruleGroups, nameValidationScheme model.ValidationScheme, p parser.Parser) (errs []error) {
if err := namevalidationutil.CheckNameValidationScheme(nameValidationScheme); err != nil {
errs = append(errs, err)
return errs
@@ -134,7 +134,7 @@ func (g *RuleGroups) Validate(node ruleGroups, nameValidationScheme model.Valida
set[g.Name] = struct{}{}
for i, r := range g.Rules {
- for _, node := range r.Validate(node.Groups[j].Rules[i], nameValidationScheme) {
+ for _, node := range r.Validate(node.Groups[j].Rules[i], nameValidationScheme, p) {
var ruleName string
if r.Alert != "" {
ruleName = r.Alert
@@ -198,7 +198,7 @@ type RuleNode struct {
}
// Validate the rule and return a list of encountered errors.
-func (r *Rule) Validate(node RuleNode, nameValidationScheme model.ValidationScheme) (nodes []WrappedError) {
+func (r *Rule) Validate(node RuleNode, nameValidationScheme model.ValidationScheme, p parser.Parser) (nodes []WrappedError) {
if r.Record != "" && r.Alert != "" {
nodes = append(nodes, WrappedError{
err: errors.New("only one of 'record' and 'alert' must be set"),
@@ -219,7 +219,7 @@ func (r *Rule) Validate(node RuleNode, nameValidationScheme model.ValidationSche
err: errors.New("field 'expr' must be set in rule"),
node: &node.Expr,
})
- } else if _, err := parser.ParseExpr(r.Expr); err != nil {
+ } else if _, err := p.ParseExpr(r.Expr); err != nil {
nodes = append(nodes, WrappedError{
err: fmt.Errorf("could not parse expression: %w", err),
node: &node.Expr,
@@ -339,7 +339,7 @@ func testTemplateParsing(rl *Rule) (errs []error) {
}
// Parse parses and validates a set of rules.
-func Parse(content []byte, ignoreUnknownFields bool, nameValidationScheme model.ValidationScheme) (*RuleGroups, []error) {
+func Parse(content []byte, ignoreUnknownFields bool, nameValidationScheme model.ValidationScheme, p parser.Parser) (*RuleGroups, []error) {
var (
groups RuleGroups
node ruleGroups
@@ -364,16 +364,16 @@ func Parse(content []byte, ignoreUnknownFields bool, nameValidationScheme model.
return nil, errs
}
- return &groups, groups.Validate(node, nameValidationScheme)
+ return &groups, groups.Validate(node, nameValidationScheme, p)
}
// ParseFile reads and parses rules from a file.
-func ParseFile(file string, ignoreUnknownFields bool, nameValidationScheme model.ValidationScheme) (*RuleGroups, []error) {
+func ParseFile(file string, ignoreUnknownFields bool, nameValidationScheme model.ValidationScheme, p parser.Parser) (*RuleGroups, []error) {
b, err := os.ReadFile(file)
if err != nil {
return nil, []error{fmt.Errorf("%s: %w", file, err)}
}
- rgs, errs := Parse(b, ignoreUnknownFields, nameValidationScheme)
+ rgs, errs := Parse(b, ignoreUnknownFields, nameValidationScheme, p)
for i := range errs {
errs[i] = fmt.Errorf("%s: %w", file, errs[i])
}
diff --git a/model/rulefmt/rulefmt_test.go b/model/rulefmt/rulefmt_test.go
index 45fc0f8227..071711319c 100644
--- a/model/rulefmt/rulefmt_test.go
+++ b/model/rulefmt/rulefmt_test.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
@@ -21,18 +21,22 @@ import (
"github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
- "gopkg.in/yaml.v3"
+ "go.yaml.in/yaml/v3"
+
+ "github.com/prometheus/prometheus/promql/parser"
)
+var testParser = parser.NewParser(parser.Options{})
+
func TestParseFileSuccess(t *testing.T) {
- _, errs := ParseFile("testdata/test.yaml", false, model.UTF8Validation)
+ _, errs := ParseFile("testdata/test.yaml", false, model.UTF8Validation, testParser)
require.Empty(t, errs, "unexpected errors parsing file")
- _, errs = ParseFile("testdata/utf-8_lname.good.yaml", false, model.UTF8Validation)
+ _, errs = ParseFile("testdata/utf-8_lname.good.yaml", false, model.UTF8Validation, testParser)
require.Empty(t, errs, "unexpected errors parsing file")
- _, errs = ParseFile("testdata/utf-8_annotation.good.yaml", false, model.UTF8Validation)
+ _, errs = ParseFile("testdata/utf-8_annotation.good.yaml", false, model.UTF8Validation, testParser)
require.Empty(t, errs, "unexpected errors parsing file")
- _, errs = ParseFile("testdata/legacy_validation_annotation.good.yaml", false, model.LegacyValidation)
+ _, errs = ParseFile("testdata/legacy_validation_annotation.good.yaml", false, model.LegacyValidation, testParser)
require.Empty(t, errs, "unexpected errors parsing file")
}
@@ -41,7 +45,7 @@ func TestParseFileSuccessWithAliases(t *testing.T) {
/
sum without(instance) (rate(requests_total[5m]))
`
- rgs, errs := ParseFile("testdata/test_aliases.yaml", false, model.UTF8Validation)
+ rgs, errs := ParseFile("testdata/test_aliases.yaml", false, model.UTF8Validation, testParser)
require.Empty(t, errs, "unexpected errors parsing file")
for _, rg := range rgs.Groups {
require.Equal(t, "HighAlert", rg.Rules[0].Alert)
@@ -119,7 +123,7 @@ func TestParseFileFailure(t *testing.T) {
if c.nameValidationScheme == model.UnsetValidation {
c.nameValidationScheme = model.UTF8Validation
}
- _, errs := ParseFile(filepath.Join("testdata", c.filename), false, c.nameValidationScheme)
+ _, errs := ParseFile(filepath.Join("testdata", c.filename), false, c.nameValidationScheme, testParser)
require.NotEmpty(t, errs, "Expected error parsing %s but got none", c.filename)
require.ErrorContainsf(t, errs[0], c.errMsg, "Expected error for %s.", c.filename)
})
@@ -215,7 +219,7 @@ groups:
}
for _, tst := range tests {
- rgs, errs := Parse([]byte(tst.ruleString), false, model.UTF8Validation)
+ rgs, errs := Parse([]byte(tst.ruleString), false, model.UTF8Validation, testParser)
require.NotNil(t, rgs, "Rule parsing, rule=\n"+tst.ruleString)
passed := (tst.shouldPass && len(errs) == 0) || (!tst.shouldPass && len(errs) > 0)
require.True(t, passed, "Rule validation failed, rule=\n"+tst.ruleString)
@@ -242,7 +246,7 @@ groups:
annotations:
summary: "Instance {{ $labels.instance }} up"
`
- _, errs := Parse([]byte(group), false, model.UTF8Validation)
+ _, errs := Parse([]byte(group), false, model.UTF8Validation, testParser)
require.Len(t, errs, 2, "Expected two errors")
var err00 *Error
require.ErrorAs(t, errs[0], &err00)
diff --git a/model/textparse/benchmark_test.go b/model/textparse/benchmark_test.go
index 8445017ddf..cf63dad260 100644
--- a/model/textparse/benchmark_test.go
+++ b/model/textparse/benchmark_test.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Prometheus Authors
+// 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
@@ -36,7 +36,7 @@ import (
// and allows comparison with expfmt decoders if applicable.
//
// NOTE(bwplotka): Previous iterations of this benchmark had different cases for isolated
-// Series, Series+Metrics with and without reuse, Series+CT. Those cases are sometimes
+// Series, Series+Metrics with and without reuse, Series+ST. Those cases are sometimes
// good to know if you are working on a certain optimization, but it does not
// make sense to persist such cases for everybody (e.g. for CI one day).
// For local iteration, feel free to adjust cases/comment out code etc.
@@ -153,7 +153,7 @@ func benchParse(b *testing.B, data []byte, parser string) {
}
case "omtext":
newParserFn = func(b []byte, st *labels.SymbolTable) Parser {
- return NewOpenMetricsParser(b, st, WithOMParserCTSeriesSkipped())
+ return NewOpenMetricsParser(b, st, WithOMParserSTSeriesSkipped())
}
case "omtext_with_nhcb":
newParserFn = func(buf []byte, st *labels.SymbolTable) Parser {
@@ -206,7 +206,7 @@ func benchParse(b *testing.B, data []byte, parser string) {
}
p.Labels(&res)
- _ = p.CreatedTimestamp()
+ _ = p.StartTimestamp()
for hasExemplar := p.Exemplar(&e); hasExemplar; hasExemplar = p.Exemplar(&e) {
}
}
@@ -266,11 +266,11 @@ func readTestdataFile(tb testing.TB, file string) []byte {
/*
export bench=v1 && go test ./model/textparse/... \
- -run '^$' -bench '^BenchmarkCreatedTimestampPromProto' \
+ -run '^$' -bench '^BenchmarkStartTimestampPromProto' \
-benchtime 2s -count 6 -cpu 2 -benchmem -timeout 999m \
| tee ${bench}.txt
*/
-func BenchmarkCreatedTimestampPromProto(b *testing.B) {
+func BenchmarkStartTimestampPromProto(b *testing.B) {
data := createTestProtoBuf(b).Bytes()
st := labels.NewSymbolTable()
@@ -301,7 +301,7 @@ Inner:
b.ReportAllocs()
b.ResetTimer()
for b.Loop() {
- if p.CreatedTimestamp() != 0 {
+ if p.StartTimestamp() != 0 {
b.Fatal("should be nil")
}
}
@@ -331,7 +331,7 @@ Inner2:
b.ReportAllocs()
b.ResetTimer()
for b.Loop() {
- if p.CreatedTimestamp() == 0 {
+ if p.StartTimestamp() == 0 {
b.Fatal("should be not nil")
}
}
diff --git a/model/textparse/interface.go b/model/textparse/interface.go
index 37b1b761a0..08d9a080a7 100644
--- a/model/textparse/interface.go
+++ b/model/textparse/interface.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Prometheus Authors
+// 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
@@ -29,7 +29,7 @@ import (
type Parser interface {
// Series returns the bytes of a series with a simple float64 as a
// value, the timestamp if set, and the value of the current sample.
- // TODO(bwplotka): Similar to CreatedTimestamp, have ts == 0 meaning no timestamp provided.
+ // TODO(bwplotka): Similar to StartTimestamp, have ts == 0 meaning no timestamp provided.
// We already accepted in many places (PRW, proto parsing histograms) that 0 timestamp is not a
// a valid timestamp. If needed it can be represented as 0+1ms.
Series() ([]byte, *int64, float64)
@@ -38,7 +38,7 @@ type Parser interface {
// value, the timestamp if set, and the histogram in the current sample.
// Depending on the parsed input, the function returns an (integer) Histogram
// or a FloatHistogram, with the respective other return value being nil.
- // TODO(bwplotka): Similar to CreatedTimestamp, have ts == 0 meaning no timestamp provided.
+ // TODO(bwplotka): Similar to StartTimestamp, have ts == 0 meaning no timestamp provided.
// We already accepted in many places (PRW, proto parsing histograms) that 0 timestamp is not a
// a valid timestamp. If needed it can be represented as 0+1ms.
Histogram() ([]byte, *int64, *histogram.Histogram, *histogram.FloatHistogram)
@@ -76,10 +76,10 @@ type Parser interface {
// retrieved (including the case where no exemplars exist at all).
Exemplar(l *exemplar.Exemplar) bool
- // CreatedTimestamp returns the created timestamp (in milliseconds) for the
+ // StartTimestamp returns the created timestamp (in milliseconds) for the
// current sample. It returns 0 if it is unknown e.g. if it wasn't set or
// if the scrape protocol or metric type does not support created timestamps.
- CreatedTimestamp() int64
+ StartTimestamp() int64
// Next advances the parser to the next sample.
// It returns (EntryInvalid, io.EOF) if no samples were read.
@@ -146,9 +146,9 @@ type ParserOptions struct {
// that is also present as a native histogram. (Proto parsing only).
KeepClassicOnClassicAndNativeHistograms bool
- // OpenMetricsSkipCTSeries determines whether to skip `_created` timestamp series
+ // OpenMetricsSkipSTSeries determines whether to skip `_created` timestamp series
// during (OpenMetrics parsing only).
- OpenMetricsSkipCTSeries bool
+ OpenMetricsSkipSTSeries bool
// FallbackContentType specifies the fallback content type to use when the provided
// Content-Type header cannot be parsed or is not supported.
@@ -175,7 +175,7 @@ func New(b []byte, contentType string, st *labels.SymbolTable, opts ParserOption
switch mediaType {
case "application/openmetrics-text":
baseParser = NewOpenMetricsParser(b, st, func(o *openMetricsParserOptions) {
- o.skipCTSeries = opts.OpenMetricsSkipCTSeries
+ o.skipSTSeries = opts.OpenMetricsSkipSTSeries
o.enableTypeAndUnitLabels = opts.EnableTypeAndUnitLabels
})
case "application/vnd.google.protobuf":
diff --git a/model/textparse/interface_test.go b/model/textparse/interface_test.go
index 532c474845..d0b6b293a9 100644
--- a/model/textparse/interface_test.go
+++ b/model/textparse/interface_test.go
@@ -1,4 +1,4 @@
-// Copyright 2022 The Prometheus Authors
+// 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
@@ -195,7 +195,7 @@ type parsedEntry struct {
lset labels.Labels
t *int64
es []exemplar.Exemplar
- ct int64
+ st int64
// In EntryType.
typ model.MetricType
@@ -255,7 +255,7 @@ func testParse(t *testing.T, p Parser) (ret []parsedEntry) {
}
got.m = string(m)
p.Labels(&got.lset)
- got.ct = p.CreatedTimestamp()
+ got.st = p.StartTimestamp()
for e := (exemplar.Exemplar{}); p.Exemplar(&e); {
got.es = append(got.es, e)
diff --git a/model/textparse/nhcbparse.go b/model/textparse/nhcbparse.go
index 8ec541de8a..13ce3ca988 100644
--- a/model/textparse/nhcbparse.go
+++ b/model/textparse/nhcbparse.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Prometheus Authors
+// 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
@@ -83,7 +83,7 @@ type NHCBParser struct {
fhNHCB *histogram.FloatHistogram
lsetNHCB labels.Labels
exemplars []exemplar.Exemplar
- ctNHCB int64
+ stNHCB int64
metricStringNHCB string
// Collates values from the classic histogram series to build
@@ -92,7 +92,7 @@ type NHCBParser struct {
tempNHCB convertnhcb.TempHistogram
tempExemplars []exemplar.Exemplar
tempExemplarCount int
- tempCT int64
+ tempST int64
// Remembers the last base histogram metric name (assuming it's
// a classic histogram) so we can tell if the next float series
@@ -159,16 +159,16 @@ func (p *NHCBParser) Exemplar(ex *exemplar.Exemplar) bool {
return p.parser.Exemplar(ex)
}
-func (p *NHCBParser) CreatedTimestamp() int64 {
+func (p *NHCBParser) StartTimestamp() int64 {
switch p.state {
case stateStart, stateInhibiting:
if p.entry == EntrySeries || p.entry == EntryHistogram {
- return p.parser.CreatedTimestamp()
+ return p.parser.StartTimestamp()
}
case stateCollecting:
- return p.tempCT
+ return p.tempST
case stateEmitting:
- return p.ctNHCB
+ return p.stNHCB
}
return 0
}
@@ -318,7 +318,7 @@ func (p *NHCBParser) handleClassicHistogramSeries(lset labels.Labels) bool {
func (p *NHCBParser) processClassicHistogramSeries(lset labels.Labels, name string, updateHist func(*convertnhcb.TempHistogram)) {
if p.state != stateCollecting {
p.storeClassicLabels(name)
- p.tempCT = p.parser.CreatedTimestamp()
+ p.tempST = p.parser.StartTimestamp()
p.state = stateCollecting
p.tempLsetNHCB = convertnhcb.GetHistogramMetricBase(lset, name)
}
@@ -352,7 +352,7 @@ func (p *NHCBParser) swapExemplars() {
}
// processNHCB converts the collated classic histogram series to NHCB and caches the info
-// to be returned to callers. Retruns true if the conversion was successful.
+// to be returned to callers. Returns true if the conversion was successful.
func (p *NHCBParser) processNHCB() bool {
if p.state != stateCollecting {
return false
@@ -385,13 +385,13 @@ func (p *NHCBParser) processNHCB() bool {
p.bytesNHCB = []byte(p.metricStringNHCB)
p.lsetNHCB = p.tempLsetNHCB
p.swapExemplars()
- p.ctNHCB = p.tempCT
+ p.stNHCB = p.tempST
p.state = stateEmitting
} else {
p.state = stateStart
}
p.tempNHCB.Reset()
p.tempExemplarCount = 0
- p.tempCT = 0
+ p.tempST = 0
return err == nil
}
diff --git a/model/textparse/nhcbparse_test.go b/model/textparse/nhcbparse_test.go
index f5836b5f7f..9a27c16ea8 100644
--- a/model/textparse/nhcbparse_test.go
+++ b/model/textparse/nhcbparse_test.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Prometheus Authors
+// 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
@@ -67,13 +67,13 @@ ss{A="a"} 0
_metric_starting_with_underscore 1
testmetric{_label_starting_with_underscore="foo"} 1
testmetric{label="\"bar\""} 1
-# HELP foo Counter with and without labels to certify CT is parsed for both cases
+# HELP foo Counter with and without labels to certify ST is parsed for both cases
# TYPE foo counter
foo_total 17.0 1520879607.789 # {id="counter-test"} 5
foo_created 1520872607.123
foo_total{a="b"} 17.0 1520879607.789 # {id="counter-test"} 5
foo_created{a="b"} 1520872607.123
-# HELP bar Summary with CT at the end, making sure we find CT even if it's multiple lines a far
+# HELP bar Summary with ST at the end, making sure we find ST even if it's multiple lines a far
# TYPE bar summary
bar_count 17.0
bar_sum 324789.3
@@ -87,7 +87,7 @@ baz_bucket{le="+Inf"} 17
baz_count 17
baz_sum 324789.3
baz_created 1520872609.125
-# HELP fizz_created Gauge which shouldn't be parsed as CT
+# HELP fizz_created Gauge which shouldn't be parsed as ST
# TYPE fizz_created gauge
fizz_created 17.0
# HELP something Histogram with _created between buckets and summary
@@ -279,7 +279,7 @@ foobar{quantile="0.99"} 150.1`
lset: labels.FromStrings("__name__", "testmetric", "label", `"bar"`),
}, {
m: "foo",
- help: "Counter with and without labels to certify CT is parsed for both cases",
+ help: "Counter with and without labels to certify ST is parsed for both cases",
}, {
m: "foo",
typ: model.MetricTypeCounter,
@@ -289,17 +289,17 @@ foobar{quantile="0.99"} 150.1`
lset: labels.FromStrings("__name__", "foo_total"),
t: int64p(1520879607789),
es: []exemplar.Exemplar{{Labels: labels.FromStrings("id", "counter-test"), Value: 5}},
- ct: 1520872607123,
+ st: 1520872607123,
}, {
m: `foo_total{a="b"}`,
v: 17.0,
lset: labels.FromStrings("__name__", "foo_total", "a", "b"),
t: int64p(1520879607789),
es: []exemplar.Exemplar{{Labels: labels.FromStrings("id", "counter-test"), Value: 5}},
- ct: 1520872607123,
+ st: 1520872607123,
}, {
m: "bar",
- help: "Summary with CT at the end, making sure we find CT even if it's multiple lines a far",
+ help: "Summary with ST at the end, making sure we find ST even if it's multiple lines a far",
}, {
m: "bar",
typ: model.MetricTypeSummary,
@@ -307,22 +307,22 @@ foobar{quantile="0.99"} 150.1`
m: "bar_count",
v: 17.0,
lset: labels.FromStrings("__name__", "bar_count"),
- ct: 1520872608124,
+ st: 1520872608124,
}, {
m: "bar_sum",
v: 324789.3,
lset: labels.FromStrings("__name__", "bar_sum"),
- ct: 1520872608124,
+ st: 1520872608124,
}, {
m: `bar{quantile="0.95"}`,
v: 123.7,
lset: labels.FromStrings("__name__", "bar", "quantile", "0.95"),
- ct: 1520872608124,
+ st: 1520872608124,
}, {
m: `bar{quantile="0.99"}`,
v: 150.0,
lset: labels.FromStrings("__name__", "bar", "quantile", "0.99"),
- ct: 1520872608124,
+ st: 1520872608124,
}, {
m: "baz",
help: "Histogram with the same objective as above's summary",
@@ -340,10 +340,10 @@ foobar{quantile="0.99"} 150.1`
CustomValues: []float64{0.0}, // We do not store the +Inf boundary.
},
lset: labels.FromStrings("__name__", "baz"),
- ct: 1520872609125,
+ st: 1520872609125,
}, {
m: "fizz_created",
- help: "Gauge which shouldn't be parsed as CT",
+ help: "Gauge which shouldn't be parsed as ST",
}, {
m: "fizz_created",
typ: model.MetricTypeGauge,
@@ -368,7 +368,7 @@ foobar{quantile="0.99"} 150.1`
CustomValues: []float64{0.0}, // We do not store the +Inf boundary.
},
lset: labels.FromStrings("__name__", "something"),
- ct: 1520430001000,
+ st: 1520430001000,
}, {
m: `something{a="b"}`,
shs: &histogram.Histogram{
@@ -380,7 +380,7 @@ foobar{quantile="0.99"} 150.1`
CustomValues: []float64{0.0}, // We do not store the +Inf boundary.
},
lset: labels.FromStrings("__name__", "something", "a", "b"),
- ct: 1520430002000,
+ st: 1520430002000,
}, {
m: "yum",
help: "Summary with _created between sum and quantiles",
@@ -391,22 +391,22 @@ foobar{quantile="0.99"} 150.1`
m: `yum_count`,
v: 20,
lset: labels.FromStrings("__name__", "yum_count"),
- ct: 1520430003000,
+ st: 1520430003000,
}, {
m: `yum_sum`,
v: 324789.5,
lset: labels.FromStrings("__name__", "yum_sum"),
- ct: 1520430003000,
+ st: 1520430003000,
}, {
m: `yum{quantile="0.95"}`,
v: 123.7,
lset: labels.FromStrings("__name__", "yum", "quantile", "0.95"),
- ct: 1520430003000,
+ st: 1520430003000,
}, {
m: `yum{quantile="0.99"}`,
v: 150.0,
lset: labels.FromStrings("__name__", "yum", "quantile", "0.99"),
- ct: 1520430003000,
+ st: 1520430003000,
}, {
m: "foobar",
help: "Summary with _created as the first line",
@@ -417,22 +417,22 @@ foobar{quantile="0.99"} 150.1`
m: `foobar_count`,
v: 21,
lset: labels.FromStrings("__name__", "foobar_count"),
- ct: 1520430004000,
+ st: 1520430004000,
}, {
m: `foobar_sum`,
v: 324789.6,
lset: labels.FromStrings("__name__", "foobar_sum"),
- ct: 1520430004000,
+ st: 1520430004000,
}, {
m: `foobar{quantile="0.95"}`,
v: 123.8,
lset: labels.FromStrings("__name__", "foobar", "quantile", "0.95"),
- ct: 1520430004000,
+ st: 1520430004000,
}, {
m: `foobar{quantile="0.99"}`,
v: 150.1,
lset: labels.FromStrings("__name__", "foobar", "quantile", "0.99"),
- ct: 1520430004000,
+ st: 1520430004000,
}, {
m: "metric",
help: "foo\x00bar",
@@ -587,8 +587,8 @@ func TestNHCBParser_NoNHCBWhenExponential(t *testing.T) {
}
type parserOptions struct {
- useUTF8sep bool
- hasCreatedTimeStamp bool
+ useUTF8sep bool
+ hasStartTimestamp bool
}
// Defines the parser name, the Parser factory and the test cases
// supported by the parser and parser options.
@@ -598,14 +598,14 @@ func TestNHCBParser_NoNHCBWhenExponential(t *testing.T) {
inputBuf := createTestProtoBufHistogram(t)
return New(inputBuf.Bytes(), "application/vnd.google.protobuf", labels.NewSymbolTable(), ParserOptions{KeepClassicOnClassicAndNativeHistograms: keepClassic, ConvertClassicHistogramsToNHCB: nhcb})
}
- return "ProtoBuf", factory, []int{1, 2, 3}, parserOptions{useUTF8sep: true, hasCreatedTimeStamp: true}
+ return "ProtoBuf", factory, []int{1, 2, 3}, parserOptions{useUTF8sep: true, hasStartTimestamp: true}
},
func() (string, parserFactory, []int, parserOptions) {
factory := func(keepClassic, nhcb bool) (Parser, error) {
input := createTestOpenMetricsHistogram()
return New([]byte(input), "application/openmetrics-text", labels.NewSymbolTable(), ParserOptions{KeepClassicOnClassicAndNativeHistograms: keepClassic, ConvertClassicHistogramsToNHCB: nhcb})
}
- return "OpenMetrics", factory, []int{1}, parserOptions{hasCreatedTimeStamp: true}
+ return "OpenMetrics", factory, []int{1}, parserOptions{hasStartTimestamp: true}
},
func() (string, parserFactory, []int, parserOptions) {
factory := func(keepClassic, nhcb bool) (Parser, error) {
@@ -643,9 +643,9 @@ func TestNHCBParser_NoNHCBWhenExponential(t *testing.T) {
typ: model.MetricTypeHistogram,
})
- var ct int64
- if options.hasCreatedTimeStamp {
- ct = 1000
+ var st int64
+ if options.hasStartTimestamp {
+ st = 1000
}
var bucketForMetric func(string) string
@@ -677,7 +677,7 @@ func TestNHCBParser_NoNHCBWhenExponential(t *testing.T) {
},
lset: labels.FromStrings("__name__", metric),
t: int64p(1234568),
- ct: ct,
+ st: st,
},
}
tc.exp = append(tc.exp, exponentialSeries...)
@@ -690,42 +690,42 @@ func TestNHCBParser_NoNHCBWhenExponential(t *testing.T) {
v: 175,
lset: labels.FromStrings("__name__", metric+"_count"),
t: int64p(1234568),
- ct: ct,
+ st: st,
},
{
m: metric + "_sum",
v: 0.0008280461746287094,
lset: labels.FromStrings("__name__", metric+"_sum"),
t: int64p(1234568),
- ct: ct,
+ st: st,
},
{
m: metric + bucketForMetric("-0.0004899999999999998"),
v: 2,
lset: labels.FromStrings("__name__", metric+"_bucket", "le", "-0.0004899999999999998"),
t: int64p(1234568),
- ct: ct,
+ st: st,
},
{
m: metric + bucketForMetric("-0.0003899999999999998"),
v: 4,
lset: labels.FromStrings("__name__", metric+"_bucket", "le", "-0.0003899999999999998"),
t: int64p(1234568),
- ct: ct,
+ st: st,
},
{
m: metric + bucketForMetric("-0.0002899999999999998"),
v: 16,
lset: labels.FromStrings("__name__", metric+"_bucket", "le", "-0.0002899999999999998"),
t: int64p(1234568),
- ct: ct,
+ st: st,
},
{
m: metric + bucketForMetric("+Inf"),
v: 175,
lset: labels.FromStrings("__name__", metric+"_bucket", "le", "+Inf"),
t: int64p(1234568),
- ct: ct,
+ st: st,
},
}
tc.exp = append(tc.exp, classicSeries...)
@@ -745,7 +745,7 @@ func TestNHCBParser_NoNHCBWhenExponential(t *testing.T) {
},
lset: labels.FromStrings("__name__", metric),
t: int64p(1234568),
- ct: ct,
+ st: st,
},
}
tc.exp = append(tc.exp, nhcbSeries...)
@@ -952,7 +952,7 @@ something_bucket{a="b",le="+Inf"} 9
CustomValues: []float64{0.0}, // We do not store the +Inf boundary.
},
lset: labels.FromStrings("__name__", "something", "a", "b"),
- ct: 1520430002000,
+ st: 1520430002000,
},
}
@@ -1061,7 +1061,7 @@ metric: <
},
lset: labels.FromStrings("__name__", "test_histogram1"),
t: int64p(1234568),
- ct: 1000,
+ st: 1000,
},
{
m: "test_histogram2",
@@ -1083,7 +1083,7 @@ metric: <
},
lset: labels.FromStrings("__name__", "test_histogram2"),
t: int64p(1234568),
- ct: 1000,
+ st: 1000,
},
}
diff --git a/model/textparse/openmetricsparse.go b/model/textparse/openmetricsparse.go
index 505e45fc40..724c340546 100644
--- a/model/textparse/openmetricsparse.go
+++ b/model/textparse/openmetricsparse.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Prometheus Authors
+// 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
@@ -103,34 +103,34 @@ type OpenMetricsParser struct {
hasExemplarTs bool
// Created timestamp parsing state.
- ct int64
- ctHashSet uint64
+ st int64
+ stHashSet uint64
// ignoreExemplar instructs the parser to not overwrite exemplars (to keep them while peeking ahead).
ignoreExemplar bool
// visitedMFName is the metric family name of the last visited metric when peeking ahead
- // for _created series during the execution of the CreatedTimestamp method.
+ // for _created series during the execution of the StartTimestamp method.
visitedMFName []byte
- skipCTSeries bool
+ skipSTSeries bool
enableTypeAndUnitLabels bool
}
type openMetricsParserOptions struct {
- skipCTSeries bool
+ skipSTSeries bool
enableTypeAndUnitLabels bool
}
type OpenMetricsOption func(*openMetricsParserOptions)
-// WithOMParserCTSeriesSkipped turns off exposing _created lines
+// WithOMParserSTSeriesSkipped turns off exposing _created lines
// as series, which makes those only used for parsing created timestamp
-// for `CreatedTimestamp` method purposes.
+// for `StartTimestamp` method purposes.
//
// It's recommended to use this option to avoid using _created lines for other
// purposes than created timestamp, but leave false by default for the
// best-effort compatibility.
-func WithOMParserCTSeriesSkipped() OpenMetricsOption {
+func WithOMParserSTSeriesSkipped() OpenMetricsOption {
return func(o *openMetricsParserOptions) {
- o.skipCTSeries = true
+ o.skipSTSeries = true
}
}
@@ -142,7 +142,7 @@ func WithOMParserTypeAndUnitLabels() OpenMetricsOption {
}
}
-// NewOpenMetricsParser returns a new parser for the byte slice with option to skip CT series parsing.
+// NewOpenMetricsParser returns a new parser for the byte slice with option to skip ST series parsing.
func NewOpenMetricsParser(b []byte, st *labels.SymbolTable, opts ...OpenMetricsOption) Parser {
options := &openMetricsParserOptions{}
@@ -153,7 +153,7 @@ func NewOpenMetricsParser(b []byte, st *labels.SymbolTable, opts ...OpenMetricsO
parser := &OpenMetricsParser{
l: &openMetricsLexer{b: b},
builder: labels.NewScratchBuilderWithSymbolTable(st, 16),
- skipCTSeries: options.skipCTSeries,
+ skipSTSeries: options.skipSTSeries,
enableTypeAndUnitLabels: options.enableTypeAndUnitLabels,
}
@@ -285,12 +285,12 @@ func (p *OpenMetricsParser) Exemplar(e *exemplar.Exemplar) bool {
return true
}
-// CreatedTimestamp returns the created timestamp for a current Metric if exists or nil.
+// StartTimestamp returns the created timestamp for a current Metric if exists or nil.
// NOTE(Maniktherana): Might use additional CPU/mem resources due to deep copy of parser required for peeking given 1.0 OM specification on _created series.
-func (p *OpenMetricsParser) CreatedTimestamp() int64 {
- if !typeRequiresCT(p.mtype) {
- // Not a CT supported metric type, fast path.
- p.ctHashSet = 0 // Use ctHashSet as a single way of telling "empty cache"
+func (p *OpenMetricsParser) StartTimestamp() int64 {
+ if !typeRequiresST(p.mtype) {
+ // Not a ST supported metric type, fast path.
+ p.stHashSet = 0 // Use stHashSet as a single way of telling "empty cache"
return 0
}
@@ -307,8 +307,8 @@ func (p *OpenMetricsParser) CreatedTimestamp() int64 {
currHash := p.seriesHash(&buf, currName)
// Check cache, perhaps we fetched something already.
- if currHash == p.ctHashSet && p.ct > 0 {
- return p.ct
+ if currHash == p.stHashSet && p.st > 0 {
+ return p.st
}
// Create a new lexer and other core state details to reset the parser once this function is done executing.
@@ -322,7 +322,7 @@ func (p *OpenMetricsParser) CreatedTimestamp() int64 {
resetStart := p.start
resetMType := p.mtype
- p.skipCTSeries = false
+ p.skipSTSeries = false
p.ignoreExemplar = true
defer func() {
p.l = resetLexer
@@ -334,38 +334,38 @@ func (p *OpenMetricsParser) CreatedTimestamp() int64 {
for {
eType, err := p.Next()
if err != nil {
- // This means p.Next() will give error too later on, so def no CT line found.
- // This might result in partial scrape with wrong/missing CT, but only
+ // This means p.Next() will give error too later on, so def no ST line found.
+ // This might result in partial scrape with wrong/missing ST, but only
// spec improvement would help.
- // TODO: Make sure OM 1.1/2.0 pass CT via metadata or exemplar-like to avoid this.
- p.resetCTParseValues()
+ // TODO: Make sure OM 1.1/2.0 pass ST via metadata or exemplar-like to avoid this.
+ p.resetSTParseValues()
return 0
}
if eType != EntrySeries {
- // Assume we hit different family, no CT line found.
- p.resetCTParseValues()
+ // Assume we hit different family, no ST line found.
+ p.resetSTParseValues()
return 0
}
peekedName := p.series[p.offsets[0]-p.start : p.offsets[1]-p.start]
if len(peekedName) < 8 || string(peekedName[len(peekedName)-8:]) != "_created" {
- // Not a CT line, search more.
+ // Not a ST line, search more.
continue
}
// Remove _created suffix.
peekedHash := p.seriesHash(&buf, peekedName[:len(peekedName)-8])
if peekedHash != currHash {
- // Found CT line for a different series, for our series no CT.
- p.resetCTParseValues()
+ // Found ST line for a different series, for our series no ST.
+ p.resetSTParseValues()
return 0
}
// All timestamps in OpenMetrics are Unix Epoch in seconds. Convert to milliseconds.
// https://github.com/prometheus/OpenMetrics/blob/v1.0.0/specification/OpenMetrics.md#timestamps
- ct := int64(p.val * 1000.0)
- p.setCTParseValues(ct, currHash, currName, true)
- return ct
+ st := int64(p.val * 1000.0)
+ p.setSTParseValues(st, currHash, currName, true)
+ return st
}
}
@@ -404,23 +404,23 @@ func (p *OpenMetricsParser) seriesHash(offsetsArr *[]byte, metricFamilyName []by
return hashedOffsets
}
-// setCTParseValues sets the parser to the state after CreatedTimestamp method was called and CT was found.
-// This is useful to prevent re-parsing the same series again and early return the CT value.
-func (p *OpenMetricsParser) setCTParseValues(ct int64, ctHashSet uint64, mfName []byte, skipCTSeries bool) {
- p.ct = ct
- p.ctHashSet = ctHashSet
+// setSTParseValues sets the parser to the state after StartTimestamp method was called and ST was found.
+// This is useful to prevent re-parsing the same series again and early return the ST value.
+func (p *OpenMetricsParser) setSTParseValues(st int64, stHashSet uint64, mfName []byte, skipSTSeries bool) {
+ p.st = st
+ p.stHashSet = stHashSet
p.visitedMFName = mfName
- p.skipCTSeries = skipCTSeries // Do we need to set it?
+ p.skipSTSeries = skipSTSeries // Do we need to set it?
}
-// resetCTParseValues resets the parser to the state before CreatedTimestamp method was called.
-func (p *OpenMetricsParser) resetCTParseValues() {
- p.ctHashSet = 0
- p.skipCTSeries = true
+// resetSTParseValues resets the parser to the state before StartTimestamp method was called.
+func (p *OpenMetricsParser) resetSTParseValues() {
+ p.stHashSet = 0
+ p.skipSTSeries = true
}
-// typeRequiresCT returns true if the metric type requires a _created timestamp.
-func typeRequiresCT(t model.MetricType) bool {
+// typeRequiresST returns true if the metric type requires a _created timestamp.
+func typeRequiresST(t model.MetricType) bool {
switch t {
case model.MetricTypeCounter, model.MetricTypeSummary, model.MetricTypeHistogram:
return true
@@ -544,7 +544,7 @@ func (p *OpenMetricsParser) Next() (Entry, error) {
if err := p.parseSeriesEndOfLine(p.nextToken()); err != nil {
return EntryInvalid, err
}
- if p.skipCTSeries && p.isCreatedSeries() {
+ if p.skipSTSeries && p.isCreatedSeries() {
return p.Next()
}
return EntrySeries, nil
@@ -565,7 +565,7 @@ func (p *OpenMetricsParser) Next() (Entry, error) {
if err := p.parseSeriesEndOfLine(t2); err != nil {
return EntryInvalid, err
}
- if p.skipCTSeries && p.isCreatedSeries() {
+ if p.skipSTSeries && p.isCreatedSeries() {
return p.Next()
}
return EntrySeries, nil
@@ -697,7 +697,7 @@ func (p *OpenMetricsParser) parseLVals(offsets []int, isExemplar bool) ([]int, e
func (p *OpenMetricsParser) isCreatedSeries() bool {
metricName := p.series[p.offsets[0]-p.start : p.offsets[1]-p.start]
// check length so the metric is longer than len("_created")
- if typeRequiresCT(p.mtype) && len(metricName) >= 8 && string(metricName[len(metricName)-8:]) == "_created" {
+ if typeRequiresST(p.mtype) && len(metricName) >= 8 && string(metricName[len(metricName)-8:]) == "_created" {
return true
}
return false
diff --git a/model/textparse/openmetricsparse_test.go b/model/textparse/openmetricsparse_test.go
index 35536e7861..8f6393cd53 100644
--- a/model/textparse/openmetricsparse_test.go
+++ b/model/textparse/openmetricsparse_test.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
@@ -66,7 +66,7 @@ ss{A="a"} 0
_metric_starting_with_underscore 1
testmetric{_label_starting_with_underscore="foo"} 1
testmetric{label="\"bar\""} 1
-# HELP foo Counter with and without labels to certify CT is parsed for both cases
+# HELP foo Counter with and without labels to certify ST is parsed for both cases
# TYPE foo counter
foo_total 17.0 1520879607.789 # {id="counter-test"} 5
foo_created 1520872607.123
@@ -75,7 +75,7 @@ foo_created{a="b"} 1520872607.123
foo_total{le="c"} 21.0
foo_created{le="c"} 1520872621.123
foo_total{le="1"} 10.0
-# HELP bar Summary with CT at the end, making sure we find CT even if it's multiple lines a far
+# HELP bar Summary with ST at the end, making sure we find ST even if it's multiple lines a far
# TYPE bar summary
bar_count 17.0
bar_sum 324789.3
@@ -89,7 +89,7 @@ baz_bucket{le="+Inf"} 17
baz_count 17
baz_sum 324789.3
baz_created 1520872609.125
-# HELP fizz_created Gauge which shouldn't be parsed as CT
+# HELP fizz_created Gauge which shouldn't be parsed as ST
# TYPE fizz_created gauge
fizz_created 17.0
# HELP something Histogram with _created between buckets and summary
@@ -351,7 +351,7 @@ foobar{quantile="0.99"} 150.1`
lset: labels.FromStrings("__name__", "testmetric", "label", `"bar"`),
}, {
m: "foo",
- help: "Counter with and without labels to certify CT is parsed for both cases",
+ help: "Counter with and without labels to certify ST is parsed for both cases",
}, {
m: "foo",
typ: model.MetricTypeCounter,
@@ -367,7 +367,7 @@ foobar{quantile="0.99"} 150.1`
es: []exemplar.Exemplar{
{Labels: labels.FromStrings("id", "counter-test"), Value: 5},
},
- ct: 1520872607123,
+ st: 1520872607123,
}, {
m: `foo_total{a="b"}`,
v: 17.0,
@@ -380,7 +380,7 @@ foobar{quantile="0.99"} 150.1`
es: []exemplar.Exemplar{
{Labels: labels.FromStrings("id", "counter-test"), Value: 5},
},
- ct: 1520872607123,
+ st: 1520872607123,
}, {
m: `foo_total{le="c"}`,
v: 21.0,
@@ -389,7 +389,7 @@ foobar{quantile="0.99"} 150.1`
labels.FromStrings("__name__", "foo_total", "__type__", string(model.MetricTypeCounter), "le", "c"),
labels.FromStrings("__name__", "foo_total", "le", "c"),
),
- ct: 1520872621123,
+ st: 1520872621123,
}, {
m: `foo_total{le="1"}`,
v: 10.0,
@@ -400,7 +400,7 @@ foobar{quantile="0.99"} 150.1`
),
}, {
m: "bar",
- help: "Summary with CT at the end, making sure we find CT even if it's multiple lines a far",
+ help: "Summary with ST at the end, making sure we find ST even if it's multiple lines a far",
}, {
m: "bar",
typ: model.MetricTypeSummary,
@@ -412,7 +412,7 @@ foobar{quantile="0.99"} 150.1`
labels.FromStrings("__name__", "bar_count", "__type__", string(model.MetricTypeSummary)),
labels.FromStrings("__name__", "bar_count"),
),
- ct: 1520872608124,
+ st: 1520872608124,
}, {
m: "bar_sum",
v: 324789.3,
@@ -421,7 +421,7 @@ foobar{quantile="0.99"} 150.1`
labels.FromStrings("__name__", "bar_sum", "__type__", string(model.MetricTypeSummary)),
labels.FromStrings("__name__", "bar_sum"),
),
- ct: 1520872608124,
+ st: 1520872608124,
}, {
m: `bar{quantile="0.95"}`,
v: 123.7,
@@ -430,7 +430,7 @@ foobar{quantile="0.99"} 150.1`
labels.FromStrings("__name__", "bar", "__type__", string(model.MetricTypeSummary), "quantile", "0.95"),
labels.FromStrings("__name__", "bar", "quantile", "0.95"),
),
- ct: 1520872608124,
+ st: 1520872608124,
}, {
m: `bar{quantile="0.99"}`,
v: 150.0,
@@ -439,7 +439,7 @@ foobar{quantile="0.99"} 150.1`
labels.FromStrings("__name__", "bar", "__type__", string(model.MetricTypeSummary), "quantile", "0.99"),
labels.FromStrings("__name__", "bar", "quantile", "0.99"),
),
- ct: 1520872608124,
+ st: 1520872608124,
}, {
m: "baz",
help: "Histogram with the same objective as above's summary",
@@ -454,7 +454,7 @@ foobar{quantile="0.99"} 150.1`
labels.FromStrings("__name__", "baz_bucket", "__type__", string(model.MetricTypeHistogram), "le", "0.0"),
labels.FromStrings("__name__", "baz_bucket", "le", "0.0"),
),
- ct: 1520872609125,
+ st: 1520872609125,
}, {
m: `baz_bucket{le="+Inf"}`,
v: 17,
@@ -463,7 +463,7 @@ foobar{quantile="0.99"} 150.1`
labels.FromStrings("__name__", "baz_bucket", "__type__", string(model.MetricTypeHistogram), "le", "+Inf"),
labels.FromStrings("__name__", "baz_bucket", "le", "+Inf"),
),
- ct: 1520872609125,
+ st: 1520872609125,
}, {
m: `baz_count`,
v: 17,
@@ -472,7 +472,7 @@ foobar{quantile="0.99"} 150.1`
labels.FromStrings("__name__", "baz_count", "__type__", string(model.MetricTypeHistogram)),
labels.FromStrings("__name__", "baz_count"),
),
- ct: 1520872609125,
+ st: 1520872609125,
}, {
m: `baz_sum`,
v: 324789.3,
@@ -481,10 +481,10 @@ foobar{quantile="0.99"} 150.1`
labels.FromStrings("__name__", "baz_sum", "__type__", string(model.MetricTypeHistogram)),
labels.FromStrings("__name__", "baz_sum"),
),
- ct: 1520872609125,
+ st: 1520872609125,
}, {
m: "fizz_created",
- help: "Gauge which shouldn't be parsed as CT",
+ help: "Gauge which shouldn't be parsed as ST",
}, {
m: "fizz_created",
typ: model.MetricTypeGauge,
@@ -510,7 +510,7 @@ foobar{quantile="0.99"} 150.1`
labels.FromStrings("__name__", "something_count", "__type__", string(model.MetricTypeHistogram)),
labels.FromStrings("__name__", "something_count"),
),
- ct: 1520430001000,
+ st: 1520430001000,
}, {
m: `something_sum`,
v: 324789.4,
@@ -519,7 +519,7 @@ foobar{quantile="0.99"} 150.1`
labels.FromStrings("__name__", "something_sum", "__type__", string(model.MetricTypeHistogram)),
labels.FromStrings("__name__", "something_sum"),
),
- ct: 1520430001000,
+ st: 1520430001000,
}, {
m: `something_bucket{le="0.0"}`,
v: 1,
@@ -528,7 +528,7 @@ foobar{quantile="0.99"} 150.1`
labels.FromStrings("__name__", "something_bucket", "__type__", string(model.MetricTypeHistogram), "le", "0.0"),
labels.FromStrings("__name__", "something_bucket", "le", "0.0"),
),
- ct: 1520430001000,
+ st: 1520430001000,
}, {
m: `something_bucket{le="1"}`,
v: 2,
@@ -537,7 +537,7 @@ foobar{quantile="0.99"} 150.1`
labels.FromStrings("__name__", "something_bucket", "__type__", string(model.MetricTypeHistogram), "le", "1.0"),
labels.FromStrings("__name__", "something_bucket", "le", "1.0"),
),
- ct: 1520430001000,
+ st: 1520430001000,
}, {
m: `something_bucket{le="+Inf"}`,
v: 18,
@@ -546,7 +546,7 @@ foobar{quantile="0.99"} 150.1`
labels.FromStrings("__name__", "something_bucket", "__type__", string(model.MetricTypeHistogram), "le", "+Inf"),
labels.FromStrings("__name__", "something_bucket", "le", "+Inf"),
),
- ct: 1520430001000,
+ st: 1520430001000,
}, {
m: "yum",
help: "Summary with _created between sum and quantiles",
@@ -561,7 +561,7 @@ foobar{quantile="0.99"} 150.1`
labels.FromStrings("__name__", "yum_count", "__type__", string(model.MetricTypeSummary)),
labels.FromStrings("__name__", "yum_count"),
),
- ct: 1520430003000,
+ st: 1520430003000,
}, {
m: `yum_sum`,
v: 324789.5,
@@ -570,7 +570,7 @@ foobar{quantile="0.99"} 150.1`
labels.FromStrings("__name__", "yum_sum", "__type__", string(model.MetricTypeSummary)),
labels.FromStrings("__name__", "yum_sum"),
),
- ct: 1520430003000,
+ st: 1520430003000,
}, {
m: `yum{quantile="0.95"}`,
v: 123.7,
@@ -579,7 +579,7 @@ foobar{quantile="0.99"} 150.1`
labels.FromStrings("__name__", "yum", "__type__", string(model.MetricTypeSummary), "quantile", "0.95"),
labels.FromStrings("__name__", "yum", "quantile", "0.95"),
),
- ct: 1520430003000,
+ st: 1520430003000,
}, {
m: `yum{quantile="0.99"}`,
v: 150.0,
@@ -588,7 +588,7 @@ foobar{quantile="0.99"} 150.1`
labels.FromStrings("__name__", "yum", "__type__", string(model.MetricTypeSummary), "quantile", "0.99"),
labels.FromStrings("__name__", "yum", "quantile", "0.99"),
),
- ct: 1520430003000,
+ st: 1520430003000,
}, {
m: "foobar",
help: "Summary with _created as the first line",
@@ -603,7 +603,7 @@ foobar{quantile="0.99"} 150.1`
labels.FromStrings("__name__", "foobar_count", "__type__", string(model.MetricTypeSummary)),
labels.FromStrings("__name__", "foobar_count"),
),
- ct: 1520430004000,
+ st: 1520430004000,
}, {
m: `foobar_sum`,
v: 324789.6,
@@ -612,7 +612,7 @@ foobar{quantile="0.99"} 150.1`
labels.FromStrings("__name__", "foobar_sum", "__type__", string(model.MetricTypeSummary)),
labels.FromStrings("__name__", "foobar_sum"),
),
- ct: 1520430004000,
+ st: 1520430004000,
}, {
m: `foobar{quantile="0.95"}`,
v: 123.8,
@@ -621,7 +621,7 @@ foobar{quantile="0.99"} 150.1`
labels.FromStrings("__name__", "foobar", "__type__", string(model.MetricTypeSummary), "quantile", "0.95"),
labels.FromStrings("__name__", "foobar", "quantile", "0.95"),
),
- ct: 1520430004000,
+ st: 1520430004000,
}, {
m: `foobar{quantile="0.99"}`,
v: 150.1,
@@ -630,7 +630,7 @@ foobar{quantile="0.99"} 150.1`
labels.FromStrings("__name__", "foobar", "__type__", string(model.MetricTypeSummary), "quantile", "0.99"),
labels.FromStrings("__name__", "foobar", "quantile", "0.99"),
),
- ct: 1520430004000,
+ st: 1520430004000,
}, {
m: "metric",
help: "foo\x00bar",
@@ -640,7 +640,7 @@ foobar{quantile="0.99"} 150.1`
lset: todoDetectFamilySwitch(typeAndUnitEnabled, labels.FromStrings("__name__", "null_byte_metric", "a", "abc\x00"), model.MetricTypeSummary),
},
}
- opts := []OpenMetricsOption{WithOMParserCTSeriesSkipped()}
+ opts := []OpenMetricsOption{WithOMParserSTSeriesSkipped()}
if typeAndUnitEnabled {
opts = append(opts, WithOMParserTypeAndUnitLabels())
}
@@ -684,12 +684,12 @@ quotedexemplar2_count 1 # {"id.thing"="histogram-count-test",other="hello"} 4
m: `{"go.gc_duration_seconds",quantile="0"}`,
v: 4.9351e-05,
lset: labels.FromStrings("__name__", "go.gc_duration_seconds", "quantile", "0.0"),
- ct: 1520872607123,
+ st: 1520872607123,
}, {
m: `{"go.gc_duration_seconds",quantile="0.25"}`,
v: 7.424100000000001e-05,
lset: labels.FromStrings("__name__", "go.gc_duration_seconds", "quantile", "0.25"),
- ct: 1520872607123,
+ st: 1520872607123,
}, {
m: `{"go.gc_duration_seconds",quantile="0.5",a="b"}`,
v: 8.3835e-05,
@@ -732,7 +732,7 @@ choices}`, "strange©™\n'quoted' \"name\"", "6"),
},
}
- p := NewOpenMetricsParser([]byte(input), labels.NewSymbolTable(), WithOMParserCTSeriesSkipped())
+ p := NewOpenMetricsParser([]byte(input), labels.NewSymbolTable(), WithOMParserSTSeriesSkipped())
got := testParse(t, p)
requireEntries(t, exp, got)
}
@@ -1028,7 +1028,7 @@ func TestOpenMetricsParseErrors(t *testing.T) {
}
for i, c := range cases {
- p := NewOpenMetricsParser([]byte(c.input), labels.NewSymbolTable(), WithOMParserCTSeriesSkipped())
+ p := NewOpenMetricsParser([]byte(c.input), labels.NewSymbolTable(), WithOMParserSTSeriesSkipped())
var err error
for err == nil {
_, err = p.Next()
@@ -1093,7 +1093,7 @@ func TestOMNullByteHandling(t *testing.T) {
}
for i, c := range cases {
- p := NewOpenMetricsParser([]byte(c.input), labels.NewSymbolTable(), WithOMParserCTSeriesSkipped())
+ p := NewOpenMetricsParser([]byte(c.input), labels.NewSymbolTable(), WithOMParserSTSeriesSkipped())
var err error
for err == nil {
_, err = p.Next()
@@ -1108,10 +1108,10 @@ func TestOMNullByteHandling(t *testing.T) {
}
}
-// TestCTParseFailures tests known failure edge cases, we know does not work due
+// TestSTParseFailures tests known failure edge cases, we know does not work due
// current OM spec limitations or clients with broken OM format.
-// TODO(maniktherana): Make sure OM 1.1/2.0 pass CT via metadata or exemplar-like to avoid this.
-func TestCTParseFailures(t *testing.T) {
+// TODO(maniktherana): Make sure OM 1.1/2.0 pass ST via metadata or exemplar-like to avoid this.
+func TestSTParseFailures(t *testing.T) {
for _, tcase := range []struct {
name string
input string
@@ -1143,19 +1143,19 @@ thing_c_total 14123.232
},
{
m: `thing_count`,
- ct: 0, // Should be int64p(1520872607123).
+ st: 0, // Should be int64p(1520872607123).
},
{
m: `thing_sum`,
- ct: 0, // Should be int64p(1520872607123).
+ st: 0, // Should be int64p(1520872607123).
},
{
m: `thing_bucket{le="0.0"}`,
- ct: 0, // Should be int64p(1520872607123).
+ st: 0, // Should be int64p(1520872607123).
},
{
m: `thing_bucket{le="+Inf"}`,
- ct: 0, // Should be int64p(1520872607123),
+ st: 0, // Should be int64p(1520872607123),
},
{
m: "thing_c",
@@ -1167,7 +1167,7 @@ thing_c_total 14123.232
},
{
m: `thing_c_total`,
- ct: 0, // Should be int64p(1520872607123).
+ st: 0, // Should be int64p(1520872607123).
},
},
},
@@ -1197,9 +1197,9 @@ foo_created{a="b"} 1520872608.123
},
} {
t.Run(fmt.Sprintf("case=%v", tcase.name), func(t *testing.T) {
- p := NewOpenMetricsParser([]byte(tcase.input), labels.NewSymbolTable(), WithOMParserCTSeriesSkipped())
+ p := NewOpenMetricsParser([]byte(tcase.input), labels.NewSymbolTable(), WithOMParserSTSeriesSkipped())
got := testParse(t, p)
- resetValAndLset(got) // Keep this test focused on metric, basic entries and CT only.
+ resetValAndLset(got) // Keep this test focused on metric, basic entries and ST only.
requireEntries(t, tcase.expected, got)
})
}
diff --git a/model/textparse/promparse.go b/model/textparse/promparse.go
index 2b4b750b4d..ada1b29013 100644
--- a/model/textparse/promparse.go
+++ b/model/textparse/promparse.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
@@ -274,9 +274,9 @@ func (*PromParser) Exemplar(*exemplar.Exemplar) bool {
return false
}
-// CreatedTimestamp returns 0 as it's not implemented yet.
+// StartTimestamp returns 0 as it's not implemented yet.
// TODO(bwplotka): https://github.com/prometheus/prometheus/issues/12980
-func (*PromParser) CreatedTimestamp() int64 {
+func (*PromParser) StartTimestamp() int64 {
return 0
}
diff --git a/model/textparse/promparse_test.go b/model/textparse/promparse_test.go
index 4e9406808f..a398067efe 100644
--- a/model/textparse/promparse_test.go
+++ b/model/textparse/promparse_test.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
diff --git a/model/textparse/protobufparse.go b/model/textparse/protobufparse.go
index 800f02085e..637ae7b747 100644
--- a/model/textparse/protobufparse.go
+++ b/model/textparse/protobufparse.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
@@ -19,7 +19,6 @@ import (
"fmt"
"io"
"math"
- "strings"
"unicode/utf8"
"github.com/gogo/protobuf/types"
@@ -400,24 +399,24 @@ func (p *ProtobufParser) Exemplar(ex *exemplar.Exemplar) bool {
return true
}
-// CreatedTimestamp returns CT or 0 if CT is not present on counters, summaries or histograms.
-func (p *ProtobufParser) CreatedTimestamp() int64 {
- var ct *types.Timestamp
+// StartTimestamp returns ST or 0 if ST is not present on counters, summaries or histograms.
+func (p *ProtobufParser) StartTimestamp() int64 {
+ var st *types.Timestamp
switch p.dec.GetType() {
case dto.MetricType_COUNTER:
- ct = p.dec.GetCounter().GetCreatedTimestamp()
+ st = p.dec.GetCounter().GetCreatedTimestamp()
case dto.MetricType_SUMMARY:
- ct = p.dec.GetSummary().GetCreatedTimestamp()
+ st = p.dec.GetSummary().GetCreatedTimestamp()
case dto.MetricType_HISTOGRAM, dto.MetricType_GAUGE_HISTOGRAM:
- ct = p.dec.GetHistogram().GetCreatedTimestamp()
+ st = p.dec.GetHistogram().GetCreatedTimestamp()
default:
}
- if ct == nil {
+ if st == nil {
return 0
}
// Same as the gogo proto types.TimestampFromProto but straight to integer.
// and without validation.
- return ct.GetSeconds()*1e3 + int64(ct.GetNanos())/1e6
+ return st.GetSeconds()*1e3 + int64(st.GetNanos())/1e6
}
// Next advances the parser to the next "sample" (emulating the behavior of a
@@ -466,16 +465,6 @@ func (p *ProtobufParser) Next() (Entry, error) {
default:
return EntryInvalid, fmt.Errorf("unknown metric type for metric %q: %s", name, p.dec.GetType())
}
- unit := p.dec.GetUnit()
- if len(unit) > 0 {
- if p.dec.GetType() == dto.MetricType_COUNTER && strings.HasSuffix(name, "_total") {
- if !strings.HasSuffix(name[:len(name)-6], unit) || len(name)-6 < len(unit)+1 || name[len(name)-6-len(unit)-1] != '_' {
- return EntryInvalid, fmt.Errorf("unit %q not a suffix of counter %q", unit, name)
- }
- } else if !strings.HasSuffix(name, unit) || len(name) < len(unit)+1 || name[len(name)-len(unit)-1] != '_' {
- return EntryInvalid, fmt.Errorf("unit %q not a suffix of metric %q", unit, name)
- }
- }
p.entryBytes.Reset()
p.entryBytes.WriteString(name)
p.state = EntryHelp
diff --git a/model/textparse/protobufparse_test.go b/model/textparse/protobufparse_test.go
index 9e8ca3a6f2..3a4f4abdda 100644
--- a/model/textparse/protobufparse_test.go
+++ b/model/textparse/protobufparse_test.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
@@ -1334,7 +1334,7 @@ func TestProtobufParse(t *testing.T) {
{
m: "test_counter_with_createdtimestamp",
v: 42,
- ct: 1625851153146,
+ st: 1625851153146,
lset: labels.FromStrings(
"__name__", "test_counter_with_createdtimestamp",
),
@@ -1350,7 +1350,7 @@ func TestProtobufParse(t *testing.T) {
{
m: "test_summary_with_createdtimestamp_count",
v: 42,
- ct: 1625851153146,
+ st: 1625851153146,
lset: labels.FromStrings(
"__name__", "test_summary_with_createdtimestamp_count",
),
@@ -1358,7 +1358,7 @@ func TestProtobufParse(t *testing.T) {
{
m: "test_summary_with_createdtimestamp_sum",
v: 1.234,
- ct: 1625851153146,
+ st: 1625851153146,
lset: labels.FromStrings(
"__name__", "test_summary_with_createdtimestamp_sum",
),
@@ -1373,7 +1373,7 @@ func TestProtobufParse(t *testing.T) {
},
{
m: "test_histogram_with_createdtimestamp",
- ct: 1625851153146,
+ st: 1625851153146,
shs: &histogram.Histogram{
CounterResetHint: histogram.UnknownCounterReset,
PositiveSpans: []histogram.Span{},
@@ -1393,7 +1393,7 @@ func TestProtobufParse(t *testing.T) {
},
{
m: "test_gaugehistogram_with_createdtimestamp",
- ct: 1625851153146,
+ st: 1625851153146,
shs: &histogram.Histogram{
CounterResetHint: histogram.GaugeType,
PositiveSpans: []histogram.Span{},
@@ -1999,7 +1999,7 @@ func TestProtobufParse(t *testing.T) {
{
m: "test_counter_with_createdtimestamp\xff__type__\xffcounter",
v: 42,
- ct: 1625851153146,
+ st: 1625851153146,
lset: labels.FromStrings(
"__name__", "test_counter_with_createdtimestamp",
"__type__", string(model.MetricTypeCounter),
@@ -2016,7 +2016,7 @@ func TestProtobufParse(t *testing.T) {
{
m: "test_summary_with_createdtimestamp_count\xff__type__\xffsummary",
v: 42,
- ct: 1625851153146,
+ st: 1625851153146,
lset: labels.FromStrings(
"__name__", "test_summary_with_createdtimestamp_count",
"__type__", string(model.MetricTypeSummary),
@@ -2025,7 +2025,7 @@ func TestProtobufParse(t *testing.T) {
{
m: "test_summary_with_createdtimestamp_sum\xff__type__\xffsummary",
v: 1.234,
- ct: 1625851153146,
+ st: 1625851153146,
lset: labels.FromStrings(
"__name__", "test_summary_with_createdtimestamp_sum",
"__type__", string(model.MetricTypeSummary),
@@ -2041,7 +2041,7 @@ func TestProtobufParse(t *testing.T) {
},
{
m: "test_histogram_with_createdtimestamp\xff__type__\xffhistogram",
- ct: 1625851153146,
+ st: 1625851153146,
shs: &histogram.Histogram{
CounterResetHint: histogram.UnknownCounterReset,
PositiveSpans: []histogram.Span{},
@@ -2062,7 +2062,7 @@ func TestProtobufParse(t *testing.T) {
},
{
m: "test_gaugehistogram_with_createdtimestamp\xff__type__\xffgaugehistogram",
- ct: 1625851153146,
+ st: 1625851153146,
shs: &histogram.Histogram{
CounterResetHint: histogram.GaugeType,
PositiveSpans: []histogram.Span{},
@@ -2959,7 +2959,7 @@ func TestProtobufParse(t *testing.T) {
{
m: "test_counter_with_createdtimestamp",
v: 42,
- ct: 1625851153146,
+ st: 1625851153146,
lset: labels.FromStrings(
"__name__", "test_counter_with_createdtimestamp",
),
@@ -2975,7 +2975,7 @@ func TestProtobufParse(t *testing.T) {
{
m: "test_summary_with_createdtimestamp_count",
v: 42,
- ct: 1625851153146,
+ st: 1625851153146,
lset: labels.FromStrings(
"__name__", "test_summary_with_createdtimestamp_count",
),
@@ -2983,7 +2983,7 @@ func TestProtobufParse(t *testing.T) {
{
m: "test_summary_with_createdtimestamp_sum",
v: 1.234,
- ct: 1625851153146,
+ st: 1625851153146,
lset: labels.FromStrings(
"__name__", "test_summary_with_createdtimestamp_sum",
),
@@ -2998,7 +2998,7 @@ func TestProtobufParse(t *testing.T) {
},
{
m: "test_histogram_with_createdtimestamp",
- ct: 1625851153146,
+ st: 1625851153146,
shs: &histogram.Histogram{
CounterResetHint: histogram.UnknownCounterReset,
PositiveSpans: []histogram.Span{},
@@ -3018,7 +3018,7 @@ func TestProtobufParse(t *testing.T) {
},
{
m: "test_gaugehistogram_with_createdtimestamp",
- ct: 1625851153146,
+ st: 1625851153146,
shs: &histogram.Histogram{
CounterResetHint: histogram.GaugeType,
PositiveSpans: []histogram.Span{},
@@ -3893,7 +3893,7 @@ func TestProtobufParse(t *testing.T) {
{
m: "test_counter_with_createdtimestamp",
v: 42,
- ct: 1625851153146,
+ st: 1625851153146,
lset: labels.FromStrings(
"__name__", "test_counter_with_createdtimestamp",
),
@@ -3909,7 +3909,7 @@ func TestProtobufParse(t *testing.T) {
{
m: "test_summary_with_createdtimestamp_count",
v: 42,
- ct: 1625851153146,
+ st: 1625851153146,
lset: labels.FromStrings(
"__name__", "test_summary_with_createdtimestamp_count",
),
@@ -3917,7 +3917,7 @@ func TestProtobufParse(t *testing.T) {
{
m: "test_summary_with_createdtimestamp_sum",
v: 1.234,
- ct: 1625851153146,
+ st: 1625851153146,
lset: labels.FromStrings(
"__name__", "test_summary_with_createdtimestamp_sum",
),
@@ -3933,7 +3933,7 @@ func TestProtobufParse(t *testing.T) {
{
m: "test_histogram_with_createdtimestamp_count",
v: 0,
- ct: 1625851153146,
+ st: 1625851153146,
lset: labels.FromStrings(
"__name__", "test_histogram_with_createdtimestamp_count",
),
@@ -3941,7 +3941,7 @@ func TestProtobufParse(t *testing.T) {
{
m: "test_histogram_with_createdtimestamp_sum",
v: 0,
- ct: 1625851153146,
+ st: 1625851153146,
lset: labels.FromStrings(
"__name__", "test_histogram_with_createdtimestamp_sum",
),
@@ -3949,7 +3949,7 @@ func TestProtobufParse(t *testing.T) {
{
m: "test_histogram_with_createdtimestamp_bucket\xffle\xff+Inf",
v: 0,
- ct: 1625851153146,
+ st: 1625851153146,
lset: labels.FromStrings(
"__name__", "test_histogram_with_createdtimestamp_bucket",
"le", "+Inf",
@@ -3966,7 +3966,7 @@ func TestProtobufParse(t *testing.T) {
{
m: "test_gaugehistogram_with_createdtimestamp_count",
v: 0,
- ct: 1625851153146,
+ st: 1625851153146,
lset: labels.FromStrings(
"__name__", "test_gaugehistogram_with_createdtimestamp_count",
),
@@ -3974,7 +3974,7 @@ func TestProtobufParse(t *testing.T) {
{
m: "test_gaugehistogram_with_createdtimestamp_sum",
v: 0,
- ct: 1625851153146,
+ st: 1625851153146,
lset: labels.FromStrings(
"__name__", "test_gaugehistogram_with_createdtimestamp_sum",
),
@@ -3982,7 +3982,7 @@ func TestProtobufParse(t *testing.T) {
{
m: "test_gaugehistogram_with_createdtimestamp_bucket\xffle\xff+Inf",
v: 0,
- ct: 1625851153146,
+ st: 1625851153146,
lset: labels.FromStrings(
"__name__", "test_gaugehistogram_with_createdtimestamp_bucket",
"le", "+Inf",
diff --git a/model/timestamp/timestamp.go b/model/timestamp/timestamp.go
index 93458f644d..0f27314e57 100644
--- a/model/timestamp/timestamp.go
+++ b/model/timestamp/timestamp.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
diff --git a/model/value/value.go b/model/value/value.go
index 655ce852d5..fe8f50e002 100644
--- a/model/value/value.go
+++ b/model/value/value.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The Prometheus Authors
+// 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
diff --git a/notifier/alert.go b/notifier/alert.go
index 83e7a97fe0..5e6df2097b 100644
--- a/notifier/alert.go
+++ b/notifier/alert.go
@@ -1,4 +1,4 @@
-// Copyright 2013 The Prometheus Authors
+// 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
diff --git a/notifier/alertmanager.go b/notifier/alertmanager.go
index 8bcf7954ec..a9c1e8669f 100644
--- a/notifier/alertmanager.go
+++ b/notifier/alertmanager.go
@@ -1,4 +1,4 @@
-// Copyright 2013 The Prometheus Authors
+// 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
diff --git a/notifier/alertmanager_test.go b/notifier/alertmanager_test.go
index ea27f37be7..ca2bd2f771 100644
--- a/notifier/alertmanager_test.go
+++ b/notifier/alertmanager_test.go
@@ -1,4 +1,4 @@
-// Copyright 2013 The Prometheus Authors
+// 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
@@ -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 c47c9ea23a..81565b5cf8 100644
--- a/notifier/alertmanagerset.go
+++ b/notifier/alertmanagerset.go
@@ -1,4 +1,4 @@
-// Copyright 2013 The Prometheus Authors
+// 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
@@ -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,35 +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.latency.DeleteLabelValues(us)
- s.metrics.sent.DeleteLabelValues(us)
- s.metrics.errors.DeleteLabelValues(us)
seen[us] = struct{}{}
+ s.cleanSendLoops(am)
}
}
@@ -126,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 65adfd5c3e..7eeed79b79 100644
--- a/notifier/manager.go
+++ b/notifier/manager.go
@@ -1,4 +1,4 @@
-// Copyright 2013 The Prometheus Authors
+// 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
@@ -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,156 +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)
- }
-
- n.metrics.latency.WithLabelValues(url).Observe(time.Since(begin).Seconds())
- 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 64de020338..d7108c1628 100644
--- a/notifier/manager_test.go
+++ b/notifier/manager_test.go
@@ -1,4 +1,4 @@
-// Copyright 2013 The Prometheus Authors
+// 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
@@ -19,14 +19,17 @@ import (
"encoding/json"
"fmt"
"io"
+ "log/slog"
"net/http"
"net/http/httptest"
"net/url"
"strconv"
+ "strings"
"testing"
"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"
@@ -40,27 +43,9 @@ import (
"github.com/prometheus/prometheus/discovery/targetgroup"
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/model/relabel"
+ "github.com/prometheus/prometheus/util/testutil/synctest"
)
-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 +93,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 +137,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 +159,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 +184,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 +256,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 +285,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 +327,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 +380,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 +404,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 +428,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 +450,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 +495,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 +510,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 +542,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 +567,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])
}
}
@@ -713,319 +701,321 @@ func makeInputTargetGroup() *targetgroup.Group {
// queued alerts. This test reproduces the issue described in https://github.com/prometheus/prometheus/issues/13676.
// and https://github.com/prometheus/prometheus/issues/8768.
func TestHangingNotifier(t *testing.T) {
- const (
- batches = 100
- alertsCount = maxBatchSize * batches
- )
+ synctest.Test(t, func(t *testing.T) {
+ const (
+ batches = 100
+ alertsCount = DefaultMaxBatchSize * batches
- var (
- sendTimeout = 100 * time.Millisecond
- sdUpdatert = sendTimeout / 2
+ faultyURL = "http://faulty:9093/api/v2/alerts"
+ functionalURL = "http://functional:9093/api/v2/alerts"
+ )
- done = make(chan struct{})
- )
+ var (
+ sendTimeout = 100 * time.Millisecond
+ sdUpdatert = sendTimeout / 2
+ )
- defer func() {
- close(done)
- }()
+ // Track which alertmanagers have been called.
+ var faultyCalled, functionalCalled atomic.Bool
- // Set up a faulty Alertmanager.
- var faultyCalled atomic.Bool
- faultyServer := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
- faultyCalled.Store(true)
- select {
- case <-done:
- case <-time.After(time.Hour):
- }
- }))
- faultyURL, err := url.Parse(faultyServer.URL)
- require.NoError(t, err)
-
- // Set up a functional Alertmanager.
- var functionalCalled atomic.Bool
- functionalServer := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
- functionalCalled.Store(true)
- }))
- functionalURL, err := url.Parse(functionalServer.URL)
- require.NoError(t, err)
-
- // 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()
- reg := prometheus.NewRegistry()
- sdMetrics, err := discovery.RegisterSDMetrics(reg, discovery.NewRefreshMetrics(reg))
- require.NoError(t, err)
- sdManager := discovery.NewManager(
- ctx,
- promslog.NewNopLogger(),
- reg,
- sdMetrics,
- discovery.Name("sd-manager"),
- discovery.Updatert(sdUpdatert),
- )
- go sdManager.Run()
-
- // Set up the notifier with both faulty and functional Alertmanagers.
- notifier := NewManager(
- &Options{
- QueueCapacity: alertsCount,
- },
- model.UTF8Validation,
- nil,
- )
- 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,
- }
- go notifier.Run(sdManager.SyncCh())
- defer notifier.Stop()
-
- require.Len(t, notifier.Alertmanagers(), 2)
-
- // Enqueue the alerts.
- var alerts []*Alert
- for i := range make([]struct{}, alertsCount) {
- alerts = append(alerts, &Alert{
- Labels: labels.FromStrings("alertname", strconv.Itoa(i)),
- })
- }
- notifier.Send(alerts...)
-
- // Wait for the Alertmanagers to start receiving alerts.
- // 10*sdUpdatert is used as an arbitrary timeout here.
- timeout := time.After(10 * sdUpdatert)
-loop1:
- for {
- select {
- case <-timeout:
- t.Fatalf("Timeout waiting for the alertmanagers to be reached for the first time.")
- default:
- if faultyCalled.Load() && functionalCalled.Load() {
- break loop1
+ // Fake Do function that simulates alertmanager behavior in-process.
+ // This runs within the synctest bubble, so time.Sleep uses fake time.
+ fakeDo := func(ctx context.Context, _ *http.Client, req *http.Request) (*http.Response, error) {
+ url := req.URL.String()
+ if strings.Contains(url, "faulty") {
+ faultyCalled.Store(true)
+ // Faulty alertmanager hangs until context is canceled (by timeout).
+ <-ctx.Done()
+ return nil, ctx.Err()
}
+ // Functional alertmanager responds successfully.
+ // Sleep simulates network latency that real HTTP would have—without it,
+ // the queue drains instantly and the final queueLen() assertion fails.
+ functionalCalled.Store(true)
+ time.Sleep(sendTimeout / 2)
+ return &http.Response{
+ StatusCode: http.StatusOK,
+ Body: io.NopCloser(bytes.NewBuffer(nil)),
+ }, nil
}
- }
- // Request to remove the faulty Alertmanager.
- c := map[string]discovery.Configs{
- "config-0": {
- discovery.StaticConfig{
- &targetgroup.Group{
- Targets: []model.LabelSet{
- {
- model.AddressLabel: model.LabelValue(functionalURL.Host),
+ // 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, cancelSdManager := context.WithCancel(t.Context())
+ defer cancelSdManager()
+ reg := prometheus.NewRegistry()
+ sdMetrics, err := discovery.RegisterSDMetrics(reg, discovery.NewRefreshMetrics(reg))
+ require.NoError(t, err)
+ sdManager := discovery.NewManager(
+ ctx,
+ promslog.NewNopLogger(),
+ reg,
+ sdMetrics,
+ discovery.Name("sd-manager"),
+ discovery.Updatert(sdUpdatert),
+ )
+ go sdManager.Run()
+
+ // Set up the notifier with both faulty and functional Alertmanagers.
+ notifier := NewManager(
+ &Options{
+ QueueCapacity: alertsCount,
+ Registerer: reg,
+ Do: fakeDo,
+ },
+ model.UTF8Validation,
+ nil,
+ )
+
+ notifier.alertmanagers = make(map[string]*alertmanagerSet)
+ amCfg := config.DefaultAlertmanagerConfig
+ amCfg.Timeout = model.Duration(sendTimeout)
+ notifier.alertmanagers["config-0"] = newTestAlertmanagerSet(&amCfg, nil, notifier.opts, notifier.metrics, faultyURL, functionalURL)
+
+ for _, ams := range notifier.alertmanagers {
+ ams.startSendLoops(ams.ams)
+ }
+
+ go notifier.Run(sdManager.SyncCh())
+ t.Cleanup(func() {
+ notifier.Stop()
+ // Advance time so in-flight request timeouts fire.
+ time.Sleep(sendTimeout * 2)
+ })
+
+ require.Len(t, notifier.Alertmanagers(), 2)
+
+ // Enqueue the alerts.
+ var alerts []*Alert
+ for i := range make([]struct{}, alertsCount) {
+ alerts = append(alerts, &Alert{
+ Labels: labels.FromStrings("alertname", strconv.Itoa(i)),
+ })
+ }
+ notifier.Send(alerts...)
+
+ // Wait for the Alertmanagers to start receiving alerts.
+ // Use a polling loop since we need to wait for goroutines to process.
+ for !faultyCalled.Load() || !functionalCalled.Load() {
+ time.Sleep(sdUpdatert)
+ synctest.Wait()
+ }
+
+ // Request to remove the faulty Alertmanager.
+ c := map[string]discovery.Configs{
+ "config-0": {
+ discovery.StaticConfig{
+ &targetgroup.Group{
+ Targets: []model.LabelSet{
+ {
+ model.AddressLabel: "functional:9093",
+ },
},
},
},
},
- },
- }
- 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 {
- select {
- case <-timeout:
- t.Fatalf("Timeout, the faulty alertmanager not removed on time.")
- default:
- // The faulty alertmanager was dropped.
- if len(notifier.Alertmanagers()) == 1 {
- // Prevent from TOCTOU.
- require.Positive(t, notifier.queueLen())
- break loop2
- }
- require.Positive(t, notifier.queueLen(), "The faulty alertmanager wasn't dropped before the alerts queue was emptied.")
}
- }
+ require.NoError(t, sdManager.ApplyConfig(c))
+
+ // Wait for the discovery update to be processed.
+ // Advance time to trigger the discovery manager's update interval.
+ // The faulty alertmanager should be dropped without waiting for its queue to drain.
+ for len(notifier.Alertmanagers()) != 1 {
+ time.Sleep(sdUpdatert)
+ synctest.Wait()
+ }
+ // 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].queueLen())
+ })
}
func TestStop_DrainingDisabled(t *testing.T) {
- releaseReceiver := make(chan struct{})
- receiverReceivedRequest := make(chan struct{}, 2)
- alertsReceived := atomic.NewInt64(0)
+ synctest.Test(t, func(t *testing.T) {
+ const alertmanagerURL = "http://alertmanager:9093/api/v2/alerts"
- server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- // Let the test know we've received a request.
- receiverReceivedRequest <- struct{}{}
+ handlerStarted := make(chan struct{})
+ alertsReceived := atomic.NewInt64(0)
- var alerts []*Alert
+ // Fake Do function that simulates a hanging alertmanager that times out.
+ fakeDo := func(ctx context.Context, _ *http.Client, req *http.Request) (*http.Response, error) {
+ var alerts []*Alert
+ b, err := io.ReadAll(req.Body)
+ if err != nil {
+ return nil, fmt.Errorf("read request body: %w", err)
+ }
+ if err := json.Unmarshal(b, &alerts); err != nil {
+ return nil, fmt.Errorf("unmarshal request body: %w", err)
+ }
+ alertsReceived.Add(int64(len(alerts)))
- b, err := io.ReadAll(r.Body)
- require.NoError(t, err)
+ // Signal arrival, then block until context times out.
+ handlerStarted <- struct{}{}
+ <-ctx.Done()
- err = json.Unmarshal(b, &alerts)
- require.NoError(t, err)
+ return nil, ctx.Err()
+ }
- alertsReceived.Add(int64(len(alerts)))
-
- // Wait for the test to release us.
- <-releaseReceiver
-
- w.WriteHeader(http.StatusOK)
- }))
- defer func() {
- server.Close()
- }()
-
- m := NewManager(
- &Options{
- QueueCapacity: 10,
- DrainOnShutdown: false,
- },
- model.UTF8Validation,
- nil,
- )
-
- m.alertmanagers = make(map[string]*alertmanagerSet)
-
- am1Cfg := config.DefaultAlertmanagerConfig
- am1Cfg.Timeout = model.Duration(time.Second)
-
- m.alertmanagers["1"] = &alertmanagerSet{
- ams: []alertmanager{
- alertmanagerMock{
- urlf: func() string { return server.URL },
+ reg := prometheus.NewRegistry()
+ m := NewManager(
+ &Options{
+ QueueCapacity: 10,
+ DrainOnShutdown: false,
+ Registerer: reg,
+ Do: fakeDo,
},
- },
- cfg: &am1Cfg,
- }
+ model.UTF8Validation,
+ nil,
+ )
- notificationManagerStopped := make(chan struct{})
+ m.alertmanagers = make(map[string]*alertmanagerSet)
- go func() {
- defer close(notificationManagerStopped)
- m.Run(nil)
- }()
+ am1Cfg := config.DefaultAlertmanagerConfig
+ am1Cfg.Timeout = model.Duration(time.Second)
+ m.alertmanagers["1"] = newTestAlertmanagerSet(&am1Cfg, nil, m.opts, m.metrics, alertmanagerURL)
- // Queue two alerts. The first should be immediately sent to the receiver, which should block until we release it later.
- m.Send(&Alert{Labels: labels.FromStrings(labels.AlertName, "alert-1")})
+ for _, ams := range m.alertmanagers {
+ ams.startSendLoops(ams.ams)
+ }
- select {
- case <-receiverReceivedRequest:
- // Nothing more to do.
- case <-time.After(time.Second):
- require.FailNow(t, "gave up waiting for receiver to receive notification of first alert")
- }
+ // This will be waited on automatically when synctest.Test exits.
+ go m.Run(nil)
- m.Send(&Alert{Labels: labels.FromStrings(labels.AlertName, "alert-2")})
+ // Queue two alerts. The first should be immediately sent to the receiver, which should block until we release it later.
+ m.Send(&Alert{Labels: labels.FromStrings(labels.AlertName, "alert-1")})
- // Stop the notification manager, pause to allow the shutdown to be observed, and then allow the receiver to proceed.
- m.Stop()
- time.Sleep(time.Second)
- close(releaseReceiver)
+ // Wait for receiver to get the request.
+ <-handlerStarted
- // Wait for the notification manager to stop and confirm only the first notification was sent.
- // The second notification should be dropped.
- select {
- case <-notificationManagerStopped:
- // Nothing more to do.
- case <-time.After(time.Second):
- require.FailNow(t, "gave up waiting for notification manager to stop")
- }
+ m.Send(&Alert{Labels: labels.FromStrings(labels.AlertName, "alert-2")})
- require.Equal(t, int64(1), alertsReceived.Load())
+ // Stop the notification manager, then advance time to trigger the request timeout.
+ m.Stop()
+ time.Sleep(time.Second)
+
+ // Allow goroutines to finish.
+ synctest.Wait()
+
+ // Confirm only the first notification was sent. The second notification should be dropped.
+ require.Equal(t, int64(1), alertsReceived.Load())
+ })
}
func TestStop_DrainingEnabled(t *testing.T) {
- releaseReceiver := make(chan struct{})
- receiverReceivedRequest := make(chan struct{}, 2)
- alertsReceived := atomic.NewInt64(0)
+ synctest.Test(t, func(t *testing.T) {
+ const alertmanagerURL = "http://alertmanager:9093/api/v2/alerts"
- server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- // Let the test know we've received a request.
- receiverReceivedRequest <- struct{}{}
+ handlerStarted := make(chan struct{}, 1)
+ alertsReceived := atomic.NewInt64(0)
- var alerts []*Alert
+ // Fake Do function that simulates alertmanager responding slowly but successfully.
+ fakeDo := func(_ context.Context, _ *http.Client, req *http.Request) (*http.Response, error) {
+ var alerts []*Alert
+ b, err := io.ReadAll(req.Body)
+ if err != nil {
+ return nil, fmt.Errorf("read request body: %w", err)
+ }
+ if err := json.Unmarshal(b, &alerts); err != nil {
+ return nil, fmt.Errorf("unmarshal request body: %w", err)
+ }
+ alertsReceived.Add(int64(len(alerts)))
- b, err := io.ReadAll(r.Body)
- require.NoError(t, err)
+ // Signal arrival.
+ handlerStarted <- struct{}{}
- err = json.Unmarshal(b, &alerts)
- require.NoError(t, err)
+ // Block to allow for alert-2 to be queued while this request is in-flight.
+ time.Sleep(100 * time.Millisecond)
- alertsReceived.Add(int64(len(alerts)))
+ return &http.Response{
+ StatusCode: http.StatusOK,
+ Body: io.NopCloser(bytes.NewBuffer(nil)),
+ }, nil
+ }
- // Wait for the test to release us.
- <-releaseReceiver
-
- w.WriteHeader(http.StatusOK)
- }))
- defer func() {
- server.Close()
- }()
-
- m := NewManager(
- &Options{
- QueueCapacity: 10,
- DrainOnShutdown: true,
- },
- model.UTF8Validation,
- nil,
- )
-
- m.alertmanagers = make(map[string]*alertmanagerSet)
-
- am1Cfg := config.DefaultAlertmanagerConfig
- am1Cfg.Timeout = model.Duration(time.Second)
-
- m.alertmanagers["1"] = &alertmanagerSet{
- ams: []alertmanager{
- alertmanagerMock{
- urlf: func() string { return server.URL },
+ reg := prometheus.NewRegistry()
+ m := NewManager(
+ &Options{
+ QueueCapacity: 10,
+ DrainOnShutdown: true,
+ Registerer: reg,
+ Do: fakeDo,
},
- },
- cfg: &am1Cfg,
+ model.UTF8Validation,
+ nil,
+ )
+
+ m.alertmanagers = make(map[string]*alertmanagerSet)
+
+ am1Cfg := config.DefaultAlertmanagerConfig
+ am1Cfg.Timeout = model.Duration(time.Second)
+ m.alertmanagers["1"] = newTestAlertmanagerSet(&am1Cfg, nil, m.opts, m.metrics, alertmanagerURL)
+
+ for _, ams := range m.alertmanagers {
+ ams.startSendLoops(ams.ams)
+ }
+
+ go m.Run(nil)
+
+ // Queue two alerts. The first should be immediately sent to the receiver.
+ m.Send(&Alert{Labels: labels.FromStrings(labels.AlertName, "alert-1")})
+
+ // Wait for receiver to get the first request.
+ <-handlerStarted
+
+ // Send second alert while first is still being processed (fakeDo has 100ms delay).
+ m.Send(&Alert{Labels: labels.FromStrings(labels.AlertName, "alert-2")})
+
+ // Stop the notification manager. With DrainOnShutdown=true, this should wait
+ // for the queue to drain, ensuring both alerts are sent.
+ m.Stop()
+
+ // Advance time so in-flight requests complete.
+ time.Sleep(time.Second)
+
+ // Allow goroutines to finish.
+ synctest.Wait()
+
+ // Confirm both notifications were sent.
+ 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")
+ }
+ }
+ })
}
-
- notificationManagerStopped := make(chan struct{})
-
- go func() {
- defer close(notificationManagerStopped)
- m.Run(nil)
- }()
-
- // Queue two alerts. The first should be immediately sent to the receiver, which should block until we release it later.
- m.Send(&Alert{Labels: labels.FromStrings(labels.AlertName, "alert-1")})
-
- select {
- case <-receiverReceivedRequest:
- // Nothing more to do.
- case <-time.After(time.Second):
- require.FailNow(t, "gave up waiting for receiver to receive notification of first alert")
- }
-
- m.Send(&Alert{Labels: labels.FromStrings(labels.AlertName, "alert-2")})
-
- // Stop the notification manager and allow the receiver to proceed.
- m.Stop()
- close(releaseReceiver)
-
- // Wait for the notification manager to stop and confirm both notifications were sent.
- select {
- case <-notificationManagerStopped:
- // Nothing more to do.
- case <-time.After(200 * time.Millisecond):
- require.FailNow(t, "gave up waiting for notification manager to stop")
- }
-
- require.Equal(t, int64(2), alertsReceived.Load())
}
func TestApplyConfig(t *testing.T) {
@@ -1152,7 +1142,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 +1162,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 +1198,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 b9a55b3ec7..a150331ab1 100644
--- a/notifier/metric.go
+++ b/notifier/metric.go
@@ -1,4 +1,4 @@
-// Copyright 2013 The Prometheus Authors
+// 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
@@ -13,21 +13,26 @@
package notifier
-import "github.com/prometheus/client_golang/prometheus"
+import (
+ "time"
+
+ "github.com/prometheus/client_golang/prometheus"
+)
type alertMetrics struct {
- latency *prometheus.SummaryVec
+ latencySummary *prometheus.SummaryVec
+ 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{
- latency: prometheus.NewSummaryVec(prometheus.SummaryOpts{
+ latencySummary: prometheus.NewSummaryVec(prometheus.SummaryOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "latency_seconds",
@@ -36,6 +41,19 @@ func newAlertMetrics(r prometheus.Registerer, queueCap int, queueLen, alertmanag
},
[]string{alertmanagerLabel},
),
+ latencyHistogram: prometheus.NewHistogramVec(prometheus.HistogramOpts{
+ Namespace: namespace,
+ Subsystem: subsystem,
+ Name: "latency_histogram_seconds",
+ Help: "Latency histogram for sending alert notifications.",
+
+ Buckets: []float64{.01, .1, 1, 10},
+ NativeHistogramBucketFactor: 1.1,
+ NativeHistogramMaxBucketNumber: 100,
+ NativeHistogramMinResetDuration: 1 * time.Hour,
+ },
+ []string{alertmanagerLabel},
+ ),
errors: prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: namespace,
Subsystem: subsystem,
@@ -52,18 +70,18 @@ func newAlertMetrics(r prometheus.Registerer, queueCap int, queueLen, alertmanag
},
[]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,
@@ -76,11 +94,10 @@ func newAlertMetrics(r prometheus.Registerer, queueCap int, queueLen, alertmanag
}, alertmanagersDiscovered),
}
- m.queueCapacity.Set(float64(queueCap))
-
if r != nil {
r.MustRegister(
- m.latency,
+ m.latencySummary,
+ m.latencyHistogram,
m.errors,
m.sent,
m.dropped,
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.go b/notifier/util.go
index c21c33a57b..cf9a53eda0 100644
--- a/notifier/util.go
+++ b/notifier/util.go
@@ -1,4 +1,4 @@
-// Copyright 2013 The Prometheus Authors
+// 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
diff --git a/notifier/util_test.go b/notifier/util_test.go
index 2c1c7d241b..78f45ba85c 100644
--- a/notifier/util_test.go
+++ b/notifier/util_test.go
@@ -1,4 +1,4 @@
-// Copyright 2013 The Prometheus Authors
+// 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
@@ -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/plugins.yml b/plugins.yml
deleted file mode 100644
index 0541fe4852..0000000000
--- a/plugins.yml
+++ /dev/null
@@ -1,24 +0,0 @@
-- github.com/prometheus/prometheus/discovery/aws
-- github.com/prometheus/prometheus/discovery/azure
-- github.com/prometheus/prometheus/discovery/consul
-- github.com/prometheus/prometheus/discovery/digitalocean
-- github.com/prometheus/prometheus/discovery/dns
-- github.com/prometheus/prometheus/discovery/eureka
-- github.com/prometheus/prometheus/discovery/gce
-- github.com/prometheus/prometheus/discovery/hetzner
-- github.com/prometheus/prometheus/discovery/ionos
-- github.com/prometheus/prometheus/discovery/kubernetes
-- github.com/prometheus/prometheus/discovery/linode
-- github.com/prometheus/prometheus/discovery/marathon
-- github.com/prometheus/prometheus/discovery/moby
-- github.com/prometheus/prometheus/discovery/nomad
-- github.com/prometheus/prometheus/discovery/openstack
-- github.com/prometheus/prometheus/discovery/ovhcloud
-- github.com/prometheus/prometheus/discovery/puppetdb
-- github.com/prometheus/prometheus/discovery/scaleway
-- github.com/prometheus/prometheus/discovery/stackit
-- github.com/prometheus/prometheus/discovery/triton
-- github.com/prometheus/prometheus/discovery/uyuni
-- github.com/prometheus/prometheus/discovery/vultr
-- github.com/prometheus/prometheus/discovery/xds
-- github.com/prometheus/prometheus/discovery/zookeeper
diff --git a/plugins/generate.go b/plugins/generate.go
deleted file mode 100644
index 2c4ba410f2..0000000000
--- a/plugins/generate.go
+++ /dev/null
@@ -1,93 +0,0 @@
-// Copyright 2022 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 plugins
-
-package main
-
-import (
- "fmt"
- "log"
- "os"
- "path"
- "path/filepath"
-
- "go.yaml.in/yaml/v2"
-)
-
-//go:generate go run generate.go
-
-func main() {
- data, err := os.ReadFile(filepath.Join("..", "plugins.yml"))
- if err != nil {
- log.Fatal(err)
- }
-
- var plugins []string
- err = yaml.Unmarshal(data, &plugins)
- if err != nil {
- log.Fatal(err)
- }
-
- f, err := os.Create("plugins.go")
- if err != nil {
- log.Fatal(err)
- }
- defer f.Close()
- _, err = f.WriteString(`// Copyright 2022 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.
-
-// Code generated by "make plugins". DO NOT EDIT.
-
-package plugins
-
-`)
- if err != nil {
- log.Fatal(err)
- }
-
- if len(plugins) == 0 {
- return
- }
-
- _, err = f.WriteString("import (\n")
- if err != nil {
- log.Fatal(err)
- }
-
- for _, plugin := range plugins {
- _, err = f.WriteString(fmt.Sprintf("\t// Register %s plugin.\n", path.Base(plugin)))
- if err != nil {
- log.Fatal(err)
- }
- _, err = f.WriteString(fmt.Sprintf("\t_ \"%s\"\n", plugin))
- if err != nil {
- log.Fatal(err)
- }
- }
-
- _, err = f.WriteString(")\n")
- if err != nil {
- log.Fatal(err)
- }
-}
diff --git a/plugins/minimum.go b/plugins/minimum.go
index 8541de922f..9797c2dbe2 100644
--- a/plugins/minimum.go
+++ b/plugins/minimum.go
@@ -1,4 +1,4 @@
-// Copyright 2022 The Prometheus Authors
+// 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
diff --git a/plugins/plugin_aws.go b/plugins/plugin_aws.go
new file mode 100644
index 0000000000..711ef38c3e
--- /dev/null
+++ b/plugins/plugin_aws.go
@@ -0,0 +1,20 @@
+// 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 !remove_all_sd || enable_aws_sd
+
+package plugins
+
+import (
+ _ "github.com/prometheus/prometheus/discovery/aws" // Register aws plugin.
+)
diff --git a/plugins/plugin_azure.go b/plugins/plugin_azure.go
new file mode 100644
index 0000000000..1f72812b8a
--- /dev/null
+++ b/plugins/plugin_azure.go
@@ -0,0 +1,20 @@
+// 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 !remove_all_sd || enable_azure_sd
+
+package plugins
+
+import (
+ _ "github.com/prometheus/prometheus/discovery/azure" // Register azure plugin.
+)
diff --git a/plugins/plugin_consul.go b/plugins/plugin_consul.go
new file mode 100644
index 0000000000..6ff5003041
--- /dev/null
+++ b/plugins/plugin_consul.go
@@ -0,0 +1,20 @@
+// 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 !remove_all_sd || enable_consul_sd
+
+package plugins
+
+import (
+ _ "github.com/prometheus/prometheus/discovery/consul" // Register consul plugin.
+)
diff --git a/plugins/plugin_digitalocean.go b/plugins/plugin_digitalocean.go
new file mode 100644
index 0000000000..927180e90b
--- /dev/null
+++ b/plugins/plugin_digitalocean.go
@@ -0,0 +1,20 @@
+// 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 !remove_all_sd || enable_digitalocean_sd
+
+package plugins
+
+import (
+ _ "github.com/prometheus/prometheus/discovery/digitalocean" // Register digitalocean plugin.
+)
diff --git a/plugins/plugin_dns.go b/plugins/plugin_dns.go
new file mode 100644
index 0000000000..7bec66371e
--- /dev/null
+++ b/plugins/plugin_dns.go
@@ -0,0 +1,20 @@
+// 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 !remove_all_sd || enable_dns_sd
+
+package plugins
+
+import (
+ _ "github.com/prometheus/prometheus/discovery/dns" // Register dns plugin.
+)
diff --git a/plugins/plugin_eureka.go b/plugins/plugin_eureka.go
new file mode 100644
index 0000000000..e4011da02a
--- /dev/null
+++ b/plugins/plugin_eureka.go
@@ -0,0 +1,20 @@
+// 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 !remove_all_sd || enable_eureka_sd
+
+package plugins
+
+import (
+ _ "github.com/prometheus/prometheus/discovery/eureka" // Register eureka plugin.
+)
diff --git a/plugins/plugin_gce.go b/plugins/plugin_gce.go
new file mode 100644
index 0000000000..1c67657260
--- /dev/null
+++ b/plugins/plugin_gce.go
@@ -0,0 +1,20 @@
+// 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 !remove_all_sd || enable_gce_sd
+
+package plugins
+
+import (
+ _ "github.com/prometheus/prometheus/discovery/gce" // Register gce plugin.
+)
diff --git a/plugins/plugin_hetzner.go b/plugins/plugin_hetzner.go
new file mode 100644
index 0000000000..f6b7db4563
--- /dev/null
+++ b/plugins/plugin_hetzner.go
@@ -0,0 +1,20 @@
+// 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 !remove_all_sd || enable_hetzner_sd
+
+package plugins
+
+import (
+ _ "github.com/prometheus/prometheus/discovery/hetzner" // Register hetzner plugin.
+)
diff --git a/plugins/plugin_ionos.go b/plugins/plugin_ionos.go
new file mode 100644
index 0000000000..bf53b73053
--- /dev/null
+++ b/plugins/plugin_ionos.go
@@ -0,0 +1,20 @@
+// 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 !remove_all_sd || enable_ionos_sd
+
+package plugins
+
+import (
+ _ "github.com/prometheus/prometheus/discovery/ionos" // Register ionos plugin.
+)
diff --git a/plugins/plugin_kubernetes.go b/plugins/plugin_kubernetes.go
new file mode 100644
index 0000000000..7145cedb2e
--- /dev/null
+++ b/plugins/plugin_kubernetes.go
@@ -0,0 +1,20 @@
+// 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 !remove_all_sd || enable_kubernetes_sd
+
+package plugins
+
+import (
+ _ "github.com/prometheus/prometheus/discovery/kubernetes" // Register kubernetes plugin.
+)
diff --git a/plugins/plugin_linode.go b/plugins/plugin_linode.go
new file mode 100644
index 0000000000..4eb24b409c
--- /dev/null
+++ b/plugins/plugin_linode.go
@@ -0,0 +1,20 @@
+// 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 !remove_all_sd || enable_linode_sd
+
+package plugins
+
+import (
+ _ "github.com/prometheus/prometheus/discovery/linode" // Register linode plugin.
+)
diff --git a/plugins/plugin_marathon.go b/plugins/plugin_marathon.go
new file mode 100644
index 0000000000..c26219a37a
--- /dev/null
+++ b/plugins/plugin_marathon.go
@@ -0,0 +1,20 @@
+// 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 !remove_all_sd || enable_marathon_sd
+
+package plugins
+
+import (
+ _ "github.com/prometheus/prometheus/discovery/marathon" // Register marathon plugin.
+)
diff --git a/plugins/plugin_moby.go b/plugins/plugin_moby.go
new file mode 100644
index 0000000000..2c7c8e158b
--- /dev/null
+++ b/plugins/plugin_moby.go
@@ -0,0 +1,20 @@
+// 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 !remove_all_sd || enable_moby_sd
+
+package plugins
+
+import (
+ _ "github.com/prometheus/prometheus/discovery/moby" // Register moby plugin.
+)
diff --git a/plugins/plugin_nomad.go b/plugins/plugin_nomad.go
new file mode 100644
index 0000000000..7251e507a2
--- /dev/null
+++ b/plugins/plugin_nomad.go
@@ -0,0 +1,20 @@
+// 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 !remove_all_sd || enable_nomad_sd
+
+package plugins
+
+import (
+ _ "github.com/prometheus/prometheus/discovery/nomad" // Register nomad plugin.
+)
diff --git a/plugins/plugin_openstack.go b/plugins/plugin_openstack.go
new file mode 100644
index 0000000000..0dd227e8ac
--- /dev/null
+++ b/plugins/plugin_openstack.go
@@ -0,0 +1,20 @@
+// 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 !remove_all_sd || enable_openstack_sd
+
+package plugins
+
+import (
+ _ "github.com/prometheus/prometheus/discovery/openstack" // Register openstack plugin.
+)
diff --git a/plugins/plugin_ovhcloud.go b/plugins/plugin_ovhcloud.go
new file mode 100644
index 0000000000..e3c372db8c
--- /dev/null
+++ b/plugins/plugin_ovhcloud.go
@@ -0,0 +1,20 @@
+// 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 !remove_all_sd || enable_ovhcloud_sd
+
+package plugins
+
+import (
+ _ "github.com/prometheus/prometheus/discovery/ovhcloud" // Register ovhcloud plugin.
+)
diff --git a/plugins/plugin_puppetdb.go b/plugins/plugin_puppetdb.go
new file mode 100644
index 0000000000..33e82b6eac
--- /dev/null
+++ b/plugins/plugin_puppetdb.go
@@ -0,0 +1,20 @@
+// 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 !remove_all_sd || enable_puppetdb_sd
+
+package plugins
+
+import (
+ _ "github.com/prometheus/prometheus/discovery/puppetdb" // Register puppetdb plugin.
+)
diff --git a/plugins/plugin_scaleway.go b/plugins/plugin_scaleway.go
new file mode 100644
index 0000000000..88e58ac646
--- /dev/null
+++ b/plugins/plugin_scaleway.go
@@ -0,0 +1,20 @@
+// 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 !remove_all_sd || enable_scaleway_sd
+
+package plugins
+
+import (
+ _ "github.com/prometheus/prometheus/discovery/scaleway" // Register scaleway plugin.
+)
diff --git a/plugins/plugin_stackit.go b/plugins/plugin_stackit.go
new file mode 100644
index 0000000000..ac19419c27
--- /dev/null
+++ b/plugins/plugin_stackit.go
@@ -0,0 +1,20 @@
+// 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 !remove_all_sd || enable_stackit_sd
+
+package plugins
+
+import (
+ _ "github.com/prometheus/prometheus/discovery/stackit" // Register stackit plugin.
+)
diff --git a/plugins/plugin_triton.go b/plugins/plugin_triton.go
new file mode 100644
index 0000000000..48989df8dd
--- /dev/null
+++ b/plugins/plugin_triton.go
@@ -0,0 +1,20 @@
+// 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 !remove_all_sd || enable_triton_sd
+
+package plugins
+
+import (
+ _ "github.com/prometheus/prometheus/discovery/triton" // Register triton plugin.
+)
diff --git a/plugins/plugin_uyuni.go b/plugins/plugin_uyuni.go
new file mode 100644
index 0000000000..09f9ff033d
--- /dev/null
+++ b/plugins/plugin_uyuni.go
@@ -0,0 +1,20 @@
+// 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 !remove_all_sd || enable_uyuni_sd
+
+package plugins
+
+import (
+ _ "github.com/prometheus/prometheus/discovery/uyuni" // Register uyuni plugin.
+)
diff --git a/plugins/plugin_vultr.go b/plugins/plugin_vultr.go
new file mode 100644
index 0000000000..5de4747cc7
--- /dev/null
+++ b/plugins/plugin_vultr.go
@@ -0,0 +1,20 @@
+// 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 !remove_all_sd || enable_vultr_sd
+
+package plugins
+
+import (
+ _ "github.com/prometheus/prometheus/discovery/vultr" // Register vultr plugin.
+)
diff --git a/plugins/plugin_xds.go b/plugins/plugin_xds.go
new file mode 100644
index 0000000000..e0b0f048d2
--- /dev/null
+++ b/plugins/plugin_xds.go
@@ -0,0 +1,20 @@
+// 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 !remove_all_sd || enable_xds_sd
+
+package plugins
+
+import (
+ _ "github.com/prometheus/prometheus/discovery/xds" // Register xds plugin.
+)
diff --git a/plugins/plugin_zookeeper.go b/plugins/plugin_zookeeper.go
new file mode 100644
index 0000000000..0852432920
--- /dev/null
+++ b/plugins/plugin_zookeeper.go
@@ -0,0 +1,20 @@
+// 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 !remove_all_sd || enable_zookeeper_sd
+
+package plugins
+
+import (
+ _ "github.com/prometheus/prometheus/discovery/zookeeper" // Register zookeeper plugin.
+)
diff --git a/plugins/plugins.go b/plugins/plugins.go
deleted file mode 100644
index 90b1407281..0000000000
--- a/plugins/plugins.go
+++ /dev/null
@@ -1,67 +0,0 @@
-// Copyright 2022 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.
-
-// Code generated by "make plugins". DO NOT EDIT.
-
-package plugins
-
-import (
- // Register aws plugin.
- _ "github.com/prometheus/prometheus/discovery/aws"
- // Register azure plugin.
- _ "github.com/prometheus/prometheus/discovery/azure"
- // Register consul plugin.
- _ "github.com/prometheus/prometheus/discovery/consul"
- // Register digitalocean plugin.
- _ "github.com/prometheus/prometheus/discovery/digitalocean"
- // Register dns plugin.
- _ "github.com/prometheus/prometheus/discovery/dns"
- // Register eureka plugin.
- _ "github.com/prometheus/prometheus/discovery/eureka"
- // Register gce plugin.
- _ "github.com/prometheus/prometheus/discovery/gce"
- // Register hetzner plugin.
- _ "github.com/prometheus/prometheus/discovery/hetzner"
- // Register ionos plugin.
- _ "github.com/prometheus/prometheus/discovery/ionos"
- // Register kubernetes plugin.
- _ "github.com/prometheus/prometheus/discovery/kubernetes"
- // Register linode plugin.
- _ "github.com/prometheus/prometheus/discovery/linode"
- // Register marathon plugin.
- _ "github.com/prometheus/prometheus/discovery/marathon"
- // Register moby plugin.
- _ "github.com/prometheus/prometheus/discovery/moby"
- // Register nomad plugin.
- _ "github.com/prometheus/prometheus/discovery/nomad"
- // Register openstack plugin.
- _ "github.com/prometheus/prometheus/discovery/openstack"
- // Register ovhcloud plugin.
- _ "github.com/prometheus/prometheus/discovery/ovhcloud"
- // Register puppetdb plugin.
- _ "github.com/prometheus/prometheus/discovery/puppetdb"
- // Register scaleway plugin.
- _ "github.com/prometheus/prometheus/discovery/scaleway"
- // Register stackit plugin.
- _ "github.com/prometheus/prometheus/discovery/stackit"
- // Register triton plugin.
- _ "github.com/prometheus/prometheus/discovery/triton"
- // Register uyuni plugin.
- _ "github.com/prometheus/prometheus/discovery/uyuni"
- // Register vultr plugin.
- _ "github.com/prometheus/prometheus/discovery/vultr"
- // Register xds plugin.
- _ "github.com/prometheus/prometheus/discovery/xds"
- // Register zookeeper plugin.
- _ "github.com/prometheus/prometheus/discovery/zookeeper"
-)
diff --git a/prompb/codec.go b/prompb/codec.go
index 6cc0cdc861..36490984a0 100644
--- a/prompb/codec.go
+++ b/prompb/codec.go
@@ -1,4 +1,4 @@
-// Copyright 2024 Prometheus Team
+// 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
@@ -110,7 +110,7 @@ func (h Histogram) ToFloatHistogram() *histogram.FloatHistogram {
PositiveBuckets: h.GetPositiveCounts(),
NegativeSpans: spansProtoToSpans(h.GetNegativeSpans()),
NegativeBuckets: h.GetNegativeCounts(),
- CustomValues: h.CustomValues,
+ CustomValues: h.CustomValues, // CustomValues are immutable.
}
}
// Conversion from integer histogram.
@@ -125,6 +125,7 @@ func (h Histogram) ToFloatHistogram() *histogram.FloatHistogram {
PositiveBuckets: deltasToCounts(h.GetPositiveDeltas()),
NegativeSpans: spansProtoToSpans(h.GetNegativeSpans()),
NegativeBuckets: deltasToCounts(h.GetNegativeDeltas()),
+ CustomValues: h.CustomValues, // CustomValues are immutable.
}
}
@@ -161,6 +162,7 @@ func FromIntHistogram(timestamp int64, h *histogram.Histogram) Histogram {
PositiveDeltas: h.PositiveBuckets,
ResetHint: Histogram_ResetHint(h.CounterResetHint),
Timestamp: timestamp,
+ CustomValues: h.CustomValues, // CustomValues are immutable.
}
}
@@ -178,6 +180,7 @@ func FromFloatHistogram(timestamp int64, fh *histogram.FloatHistogram) Histogram
PositiveCounts: fh.PositiveBuckets,
ResetHint: Histogram_ResetHint(fh.CounterResetHint),
Timestamp: timestamp,
+ CustomValues: fh.CustomValues, // CustomValues are immutable.
}
}
diff --git a/prompb/custom.go b/prompb/custom.go
index f73ddd446b..65f856a755 100644
--- a/prompb/custom.go
+++ b/prompb/custom.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
diff --git a/prompb/io/prometheus/client/decoder.go b/prompb/io/prometheus/client/decoder.go
index 6bc9600ab6..de7184c4b5 100644
--- a/prompb/io/prometheus/client/decoder.go
+++ b/prompb/io/prometheus/client/decoder.go
@@ -1,4 +1,4 @@
-// Copyright 2025 The Prometheus Authors
+// 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
diff --git a/prompb/io/prometheus/client/decoder_test.go b/prompb/io/prometheus/client/decoder_test.go
index b28fe43db9..0b210c7c0f 100644
--- a/prompb/io/prometheus/client/decoder_test.go
+++ b/prompb/io/prometheus/client/decoder_test.go
@@ -1,4 +1,4 @@
-// Copyright 2025 The Prometheus Authors
+// 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
diff --git a/prompb/io/prometheus/write/v2/codec.go b/prompb/io/prometheus/write/v2/codec.go
index 71196edb88..ae4d0f635a 100644
--- a/prompb/io/prometheus/write/v2/codec.go
+++ b/prompb/io/prometheus/write/v2/codec.go
@@ -1,4 +1,4 @@
-// Copyright 2024 Prometheus Team
+// 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
diff --git a/prompb/io/prometheus/write/v2/custom.go b/prompb/io/prometheus/write/v2/custom.go
index 3aa778eb60..4063cf32ed 100644
--- a/prompb/io/prometheus/write/v2/custom.go
+++ b/prompb/io/prometheus/write/v2/custom.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Prometheus Authors
+// 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
@@ -80,11 +80,6 @@ func (m *TimeSeries) OptimizedMarshalToSizedBuffer(dAtA []byte) (int, error) {
i -= len(m.XXX_unrecognized)
copy(dAtA[i:], m.XXX_unrecognized)
}
- if m.CreatedTimestamp != 0 {
- i = encodeVarintTypes(dAtA, i, uint64(m.CreatedTimestamp))
- i--
- dAtA[i] = 0x30
- }
{
size, err := m.Metadata.MarshalToSizedBuffer(dAtA[:i])
if err != nil {
diff --git a/prompb/io/prometheus/write/v2/custom_test.go b/prompb/io/prometheus/write/v2/custom_test.go
index 139cbfb225..30715477cb 100644
--- a/prompb/io/prometheus/write/v2/custom_test.go
+++ b/prompb/io/prometheus/write/v2/custom_test.go
@@ -1,4 +1,4 @@
-// Copyright 2023 The Prometheus Authors
+// 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
diff --git a/prompb/io/prometheus/write/v2/symbols.go b/prompb/io/prometheus/write/v2/symbols.go
index 7c7feca239..292801a185 100644
--- a/prompb/io/prometheus/write/v2/symbols.go
+++ b/prompb/io/prometheus/write/v2/symbols.go
@@ -1,4 +1,4 @@
-// Copyright 2024 Prometheus Team
+// 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
diff --git a/prompb/io/prometheus/write/v2/symbols_test.go b/prompb/io/prometheus/write/v2/symbols_test.go
index 7e7c7cb0bd..d0f335665a 100644
--- a/prompb/io/prometheus/write/v2/symbols_test.go
+++ b/prompb/io/prometheus/write/v2/symbols_test.go
@@ -1,4 +1,4 @@
-// Copyright 2024 Prometheus Team
+// 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
diff --git a/prompb/io/prometheus/write/v2/types.pb.go b/prompb/io/prometheus/write/v2/types.pb.go
index 1419de217e..a726efb5b5 100644
--- a/prompb/io/prometheus/write/v2/types.pb.go
+++ b/prompb/io/prometheus/write/v2/types.pb.go
@@ -106,6 +106,8 @@ func (Histogram_ResetHint) EnumDescriptor() ([]byte, []int) {
// The canonical Content-Type request header value for this message is
// "application/x-protobuf;proto=io.prometheus.write.v2.Request"
//
+// Version: v2.0-rc.4
+//
// NOTE: gogoproto options might change in future for this file, they
// are not part of the spec proto (they only modify the generated Go code, not
// the serialized message). See: https://github.com/prometheus/prometheus/issues/11908
@@ -181,7 +183,7 @@ type TimeSeries struct {
//
// Note that there might be multiple TimeSeries objects in the same
// Requests with the same labels e.g. for different exemplars, metadata
- // or created timestamp.
+ // or start timestamp.
LabelsRefs []uint32 `protobuf:"varint,1,rep,packed,name=labels_refs,json=labelsRefs,proto3" json:"labels_refs,omitempty"`
// Timeseries messages can either specify samples or (native) histogram samples
// (histogram field), but not both. For a typical sender (real-time metric
@@ -193,24 +195,7 @@ type TimeSeries struct {
// exemplars represents an optional set of exemplars attached to this series' samples.
Exemplars []Exemplar `protobuf:"bytes,4,rep,name=exemplars,proto3" json:"exemplars"`
// metadata represents the metadata associated with the given series' samples.
- Metadata Metadata `protobuf:"bytes,5,opt,name=metadata,proto3" json:"metadata"`
- // created_timestamp represents an optional created timestamp associated with
- // this series' samples in ms format, typically for counter or histogram type
- // metrics. Created timestamp represents the time when the counter started
- // counting (sometimes referred to as start timestamp), which can increase
- // the accuracy of query results.
- //
- // Note that some receivers might require this and in return fail to
- // ingest such samples within the Request.
- //
- // For Go, see github.com/prometheus/prometheus/model/timestamp/timestamp.go
- // for conversion from/to time.Time to Prometheus timestamp.
- //
- // Note that the "optional" keyword is omitted due to
- // https://cloud.google.com/apis/design/design_patterns.md#optional_primitive_fields
- // Zero value means value not set. If you need to use exactly zero value for
- // the timestamp, use 1 millisecond before or after.
- CreatedTimestamp int64 `protobuf:"varint,6,opt,name=created_timestamp,json=createdTimestamp,proto3" json:"created_timestamp,omitempty"`
+ Metadata Metadata `protobuf:"bytes,5,opt,name=metadata,proto3" json:"metadata"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
@@ -284,13 +269,6 @@ func (m *TimeSeries) GetMetadata() Metadata {
return Metadata{}
}
-func (m *TimeSeries) GetCreatedTimestamp() int64 {
- if m != nil {
- return m.CreatedTimestamp
- }
- return 0
-}
-
// Exemplar is an additional information attached to some series' samples.
// It is typically used to attach an example trace or request ID associated with
// the metric changes.
@@ -375,7 +353,27 @@ type Sample struct {
//
// For Go, see github.com/prometheus/prometheus/model/timestamp/timestamp.go
// for conversion from/to time.Time to Prometheus timestamp.
- Timestamp int64 `protobuf:"varint,2,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
+ Timestamp int64 `protobuf:"varint,2,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
+ // start_timestamp represents an optional start timestamp for the sample,
+ // in ms format. This information is typically used for counter, histogram (cumulative)
+ // or delta type metrics.
+ //
+ // For cumulative metrics, the start timestamp represents the time when the
+ // counter started counting (sometimes referred to as start timestamp), which
+ // can increase the accuracy of certain processing and query semantics (e.g. rates).
+ //
+ // Note:
+ // * That some receivers might require start timestamps for certain metric
+ // types; rejecting such samples within the Request as a result.
+ // * start timestamp is the same as "created timestamp" name Prometheus used in the past.
+ //
+ // For Go, see github.com/prometheus/prometheus/model/timestamp/timestamp.go
+ // for conversion from/to time.Time to Prometheus timestamp.
+ //
+ // Note that the "optional" keyword is omitted due to efficiency and consistency.
+ // Zero value means value not set. If you need to use exactly zero value for
+ // the timestamp, use 1 millisecond before or after.
+ StartTimestamp int64 `protobuf:"varint,3,opt,name=start_timestamp,json=startTimestamp,proto3" json:"start_timestamp,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
@@ -428,6 +426,13 @@ func (m *Sample) GetTimestamp() int64 {
return 0
}
+func (m *Sample) GetStartTimestamp() int64 {
+ if m != nil {
+ return m.StartTimestamp
+ }
+ return 0
+}
+
// Metadata represents the metadata associated with the given series' samples.
type Metadata struct {
Type Metadata_MetricType `protobuf:"varint,1,opt,name=type,proto3,enum=io.prometheus.write.v2.Metadata_MetricType" json:"type,omitempty"`
@@ -498,12 +503,11 @@ func (m *Metadata) GetUnitRef() uint32 {
return 0
}
-// A native histogram, also known as a sparse histogram.
-// Original design doc:
-// https://docs.google.com/document/d/1cLNv3aufPZb3fNfaJgdaRBZsInZKKIHo9E6HinJVbpM/edit
-// The appendix of this design doc also explains the concept of float
-// histograms. This Histogram message can represent both, the usual
-// integer histogram as well as a float histogram.
+// A native histogram message, supporting
+// * sparse exponential bucketing, custom bucketing.
+// * float or integer histograms.
+//
+// See the full spec: https://prometheus.io/docs/specs/native_histograms/
type Histogram struct {
// Types that are valid to be assigned to Count:
//
@@ -581,10 +585,27 @@ type Histogram struct {
//
// The last element is not only the upper inclusive bound of the last regular
// bucket, but implicitly the lower exclusive bound of the +Inf bucket.
- CustomValues []float64 `protobuf:"fixed64,16,rep,packed,name=custom_values,json=customValues,proto3" json:"custom_values,omitempty"`
- XXX_NoUnkeyedLiteral struct{} `json:"-"`
- XXX_unrecognized []byte `json:"-"`
- XXX_sizecache int32 `json:"-"`
+ CustomValues []float64 `protobuf:"fixed64,16,rep,packed,name=custom_values,json=customValues,proto3" json:"custom_values,omitempty"`
+ // start_timestamp represents an optional start timestamp for the histogram sample,
+ // in ms format. The start timestamp represents the time when the histogram
+ // started counting, which can increase the accuracy of certain processing and
+ // query semantics (e.g. rates).
+ //
+ // Note:
+ // * That some receivers might require start timestamps for certain metric
+ // types; rejecting such samples within the Request as a result.
+ // * start timestamp is the same as "created timestamp" name Prometheus used in the past.
+ //
+ // For Go, see github.com/prometheus/prometheus/model/timestamp/timestamp.go
+ // for conversion from/to time.Time to Prometheus timestamp.
+ //
+ // Note that the "optional" keyword is omitted due to efficiency and consistency.
+ // Zero value means value not set. If you need to use exactly zero value for
+ // the timestamp, use 1 millisecond before or after.
+ StartTimestamp int64 `protobuf:"varint,17,opt,name=start_timestamp,json=startTimestamp,proto3" json:"start_timestamp,omitempty"`
+ XXX_NoUnkeyedLiteral struct{} `json:"-"`
+ XXX_unrecognized []byte `json:"-"`
+ XXX_sizecache int32 `json:"-"`
}
func (m *Histogram) Reset() { *m = Histogram{} }
@@ -774,6 +795,13 @@ func (m *Histogram) GetCustomValues() []float64 {
return nil
}
+func (m *Histogram) GetStartTimestamp() int64 {
+ if m != nil {
+ return m.StartTimestamp
+ }
+ return 0
+}
+
// XXX_OneofWrappers is for the internal use of the proto package.
func (*Histogram) XXX_OneofWrappers() []interface{} {
return []interface{}{
@@ -861,65 +889,66 @@ func init() {
}
var fileDescriptor_f139519efd9fa8d7 = []byte{
- // 926 bytes of a gzipped FileDescriptorProto
+ // 931 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x55, 0x5d, 0x6f, 0xe3, 0x44,
- 0x14, 0xed, 0xc4, 0x69, 0x3e, 0x6e, 0x9a, 0xac, 0x33, 0xb4, 0x5d, 0x6f, 0x81, 0x6c, 0xd6, 0x08,
- 0x88, 0x58, 0x29, 0x91, 0xc2, 0xeb, 0x0a, 0xd4, 0xb4, 0x6e, 0x93, 0x95, 0x92, 0xac, 0x26, 0x2e,
- 0x52, 0x79, 0xb1, 0xdc, 0x64, 0x92, 0x58, 0xd8, 0xb1, 0xf1, 0x4c, 0x02, 0xe5, 0xf7, 0xf1, 0xb0,
- 0x8f, 0xfc, 0x01, 0x10, 0xf4, 0x9d, 0xff, 0x80, 0x66, 0xfc, 0xd9, 0x42, 0xbb, 0xe2, 0x6d, 0xe6,
- 0xdc, 0x73, 0xee, 0x3d, 0xb9, 0xbe, 0x77, 0x02, 0xba, 0xe3, 0xf7, 0x82, 0xd0, 0xf7, 0x28, 0x5f,
- 0xd3, 0x2d, 0xeb, 0xfd, 0x14, 0x3a, 0x9c, 0xf6, 0x76, 0xfd, 0x1e, 0xbf, 0x0d, 0x28, 0xeb, 0x06,
- 0xa1, 0xcf, 0x7d, 0x7c, 0xec, 0xf8, 0xdd, 0x8c, 0xd3, 0x95, 0x9c, 0xee, 0xae, 0x7f, 0x72, 0xb8,
- 0xf2, 0x57, 0xbe, 0xa4, 0xf4, 0xc4, 0x29, 0x62, 0xeb, 0x0c, 0xca, 0x84, 0xfe, 0xb8, 0xa5, 0x8c,
- 0x63, 0x0d, 0xca, 0xec, 0xd6, 0xbb, 0xf1, 0x5d, 0xa6, 0x15, 0xdb, 0x4a, 0xa7, 0x4a, 0x92, 0x2b,
- 0x1e, 0x02, 0x70, 0xc7, 0xa3, 0x8c, 0x86, 0x0e, 0x65, 0xda, 0x7e, 0x5b, 0xe9, 0xd4, 0xfa, 0x7a,
- 0xf7, 0xbf, 0xeb, 0x74, 0x4d, 0xc7, 0xa3, 0x33, 0xc9, 0x1c, 0x14, 0xdf, 0xff, 0xf1, 0x72, 0x8f,
- 0xe4, 0xb4, 0x6f, 0x8b, 0x15, 0xa4, 0x16, 0xf5, 0xbf, 0x0b, 0x00, 0x19, 0x0d, 0xbf, 0x84, 0x9a,
- 0x6b, 0xdf, 0x50, 0x97, 0x59, 0x21, 0x5d, 0x32, 0x0d, 0xb5, 0x95, 0x4e, 0x9d, 0x40, 0x04, 0x11,
- 0xba, 0x64, 0xf8, 0x1b, 0x28, 0x33, 0xdb, 0x0b, 0x5c, 0xca, 0xb4, 0x82, 0x2c, 0xde, 0x7a, 0xac,
- 0xf8, 0x4c, 0xd2, 0xe2, 0xc2, 0x89, 0x08, 0x5f, 0x02, 0xac, 0x1d, 0xc6, 0xfd, 0x55, 0x68, 0x7b,
- 0x4c, 0x53, 0x64, 0x8a, 0x57, 0x8f, 0xa5, 0x18, 0x26, 0xcc, 0xc4, 0x7e, 0x26, 0xc5, 0xe7, 0x50,
- 0xa5, 0x3f, 0x53, 0x2f, 0x70, 0xed, 0x30, 0x6a, 0x52, 0xad, 0xdf, 0x7e, 0x2c, 0x8f, 0x11, 0x13,
- 0xe3, 0x34, 0x99, 0x10, 0x0f, 0xa0, 0xe2, 0x51, 0x6e, 0x2f, 0x6c, 0x6e, 0x6b, 0xfb, 0x6d, 0xf4,
- 0x54, 0x92, 0x71, 0xcc, 0x8b, 0x93, 0xa4, 0x3a, 0xfc, 0x1a, 0x9a, 0xf3, 0x90, 0xda, 0x9c, 0x2e,
- 0x2c, 0xd9, 0x5e, 0x6e, 0x7b, 0x81, 0x56, 0x6a, 0xa3, 0x8e, 0x42, 0xd4, 0x38, 0x60, 0x26, 0xb8,
- 0x6e, 0x41, 0x25, 0x71, 0xf3, 0xe1, 0x66, 0x1f, 0xc2, 0xfe, 0xce, 0x76, 0xb7, 0x54, 0x2b, 0xb4,
- 0x51, 0x07, 0x91, 0xe8, 0x82, 0x3f, 0x81, 0x6a, 0x56, 0x47, 0x91, 0x75, 0x32, 0x40, 0x7f, 0x03,
- 0xa5, 0xa8, 0xf3, 0x99, 0x1a, 0x3d, 0xaa, 0x2e, 0x3c, 0x54, 0xff, 0x55, 0x80, 0x4a, 0xf2, 0x43,
- 0xf1, 0xb7, 0x50, 0x14, 0xd3, 0x2c, 0xf5, 0x8d, 0xfe, 0xeb, 0x0f, 0x35, 0x46, 0x1c, 0x42, 0x67,
- 0x6e, 0xde, 0x06, 0x94, 0x48, 0x21, 0x7e, 0x01, 0x95, 0x35, 0x75, 0x03, 0xf1, 0xf3, 0xa4, 0xd1,
- 0x3a, 0x29, 0x8b, 0x3b, 0xa1, 0x4b, 0x11, 0xda, 0x6e, 0x1c, 0x2e, 0x43, 0xc5, 0x28, 0x24, 0xee,
- 0x84, 0x2e, 0xf5, 0xdf, 0x11, 0x40, 0x96, 0x0a, 0x7f, 0x0c, 0xcf, 0xc7, 0x86, 0x49, 0x46, 0x67,
- 0x96, 0x79, 0xfd, 0xce, 0xb0, 0xae, 0x26, 0xb3, 0x77, 0xc6, 0xd9, 0xe8, 0x62, 0x64, 0x9c, 0xab,
- 0x7b, 0xf8, 0x39, 0x7c, 0x94, 0x0f, 0x9e, 0x4d, 0xaf, 0x26, 0xa6, 0x41, 0x54, 0x84, 0x8f, 0xa0,
- 0x99, 0x0f, 0x5c, 0x9e, 0x5e, 0x5d, 0x1a, 0x6a, 0x01, 0xbf, 0x80, 0xa3, 0x3c, 0x3c, 0x1c, 0xcd,
- 0xcc, 0xe9, 0x25, 0x39, 0x1d, 0xab, 0x0a, 0x6e, 0xc1, 0xc9, 0xbf, 0x14, 0x59, 0xbc, 0xf8, 0xb0,
- 0xd4, 0xec, 0x6a, 0x3c, 0x3e, 0x25, 0xd7, 0xea, 0x3e, 0x3e, 0x04, 0x35, 0x1f, 0x18, 0x4d, 0x2e,
- 0xa6, 0x6a, 0x09, 0x6b, 0x70, 0x78, 0x8f, 0x6e, 0x9e, 0x9a, 0xc6, 0xcc, 0x30, 0xd5, 0xb2, 0xfe,
- 0x6b, 0x09, 0xaa, 0xe9, 0x64, 0xe3, 0x4f, 0xa1, 0x3a, 0xf7, 0xb7, 0x1b, 0x6e, 0x39, 0x1b, 0x2e,
- 0x3b, 0x5d, 0x1c, 0xee, 0x91, 0x8a, 0x84, 0x46, 0x1b, 0x8e, 0x5f, 0x41, 0x2d, 0x0a, 0x2f, 0x5d,
- 0xdf, 0xe6, 0xd1, 0x20, 0x0c, 0xf7, 0x08, 0x48, 0xf0, 0x42, 0x60, 0x58, 0x05, 0x85, 0x6d, 0x3d,
- 0xd9, 0x60, 0x44, 0xc4, 0x11, 0x1f, 0x43, 0x89, 0xcd, 0xd7, 0xd4, 0xb3, 0x65, 0x6b, 0x9b, 0x24,
- 0xbe, 0xe1, 0xcf, 0xa1, 0xf1, 0x0b, 0x0d, 0x7d, 0x8b, 0xaf, 0x43, 0xca, 0xd6, 0xbe, 0xbb, 0x90,
- 0x33, 0x8f, 0x48, 0x5d, 0xa0, 0x66, 0x02, 0xe2, 0x2f, 0x62, 0x5a, 0xe6, 0xab, 0x24, 0x7d, 0x21,
- 0x72, 0x20, 0xf0, 0xb3, 0xc4, 0xdb, 0x57, 0xa0, 0xe6, 0x78, 0x91, 0xc1, 0xb2, 0x34, 0x88, 0x48,
- 0x23, 0x65, 0x46, 0x26, 0xa7, 0xd0, 0xd8, 0xd0, 0x95, 0xcd, 0x9d, 0x1d, 0xb5, 0x58, 0x60, 0x6f,
- 0x98, 0x56, 0x79, 0xfa, 0xed, 0x1a, 0x6c, 0xe7, 0x3f, 0x50, 0x3e, 0x0b, 0xec, 0x4d, 0xbc, 0x70,
- 0xf5, 0x44, 0x2f, 0x30, 0x86, 0xbf, 0x84, 0x67, 0x69, 0xc2, 0x05, 0x75, 0xb9, 0xcd, 0xb4, 0x6a,
- 0x5b, 0xe9, 0x60, 0x92, 0xd6, 0x39, 0x97, 0xe8, 0x3d, 0xa2, 0x74, 0xca, 0x34, 0x68, 0x2b, 0x1d,
- 0x94, 0x11, 0xa5, 0x4d, 0x26, 0x2c, 0x06, 0x3e, 0x73, 0x72, 0x16, 0x6b, 0xff, 0xd7, 0x62, 0xa2,
- 0x4f, 0x2d, 0xa6, 0x09, 0x63, 0x8b, 0x07, 0x91, 0xc5, 0x04, 0xce, 0x2c, 0xa6, 0xc4, 0xd8, 0x62,
- 0x3d, 0xb2, 0x98, 0xc0, 0xb1, 0xc5, 0xb7, 0x00, 0x21, 0x65, 0x94, 0x5b, 0x6b, 0xf1, 0x55, 0x1a,
- 0x4f, 0xef, 0x65, 0x3a, 0x63, 0x5d, 0x22, 0x34, 0x43, 0x67, 0xc3, 0x49, 0x35, 0x4c, 0x8e, 0xf7,
- 0x1f, 0x82, 0x67, 0x0f, 0x1e, 0x02, 0xfc, 0x19, 0xd4, 0xe7, 0x5b, 0xc6, 0x7d, 0xcf, 0x92, 0xcf,
- 0x06, 0xd3, 0x54, 0x69, 0xe8, 0x20, 0x02, 0xbf, 0x93, 0x98, 0xbe, 0x80, 0x6a, 0x9a, 0x1a, 0x9f,
- 0xc0, 0x31, 0x11, 0x13, 0x6e, 0x0d, 0x47, 0x13, 0xf3, 0xc1, 0x9a, 0x62, 0x68, 0xe4, 0x62, 0xd7,
- 0xc6, 0x4c, 0x45, 0xb8, 0x09, 0xf5, 0x1c, 0x36, 0x99, 0xaa, 0x05, 0xb1, 0x49, 0x39, 0x28, 0xda,
- 0x59, 0x65, 0x50, 0x86, 0x7d, 0xd9, 0x94, 0xc1, 0x01, 0x40, 0x36, 0x6f, 0xfa, 0x1b, 0x80, 0xec,
- 0x03, 0x88, 0x91, 0xf7, 0x97, 0x4b, 0x46, 0xa3, 0x1d, 0x6a, 0x92, 0xf8, 0x26, 0x70, 0x97, 0x6e,
- 0x56, 0x7c, 0x2d, 0x57, 0xa7, 0x4e, 0xe2, 0xdb, 0xe0, 0xe8, 0xfd, 0x5d, 0x0b, 0xfd, 0x76, 0xd7,
- 0x42, 0x7f, 0xde, 0xb5, 0xd0, 0xf7, 0x65, 0xd9, 0xb4, 0x5d, 0xff, 0xa6, 0x24, 0xff, 0x8a, 0xbf,
- 0xfe, 0x27, 0x00, 0x00, 0xff, 0xff, 0x3e, 0xfc, 0x93, 0x1c, 0xde, 0x07, 0x00, 0x00,
+ 0x14, 0xed, 0xc4, 0xf9, 0xbc, 0x69, 0xb2, 0xce, 0xd0, 0x76, 0xbd, 0x05, 0xb2, 0xd9, 0x20, 0x20,
+ 0x02, 0x29, 0x91, 0xc2, 0x2b, 0x02, 0x35, 0xad, 0xdb, 0xa4, 0x52, 0x92, 0xd5, 0xc4, 0x45, 0x2a,
+ 0x2f, 0x96, 0x9b, 0x4e, 0x12, 0x0b, 0x3b, 0x36, 0x9e, 0x49, 0xa0, 0xfc, 0x40, 0xb4, 0x8f, 0xfc,
+ 0x01, 0x10, 0xf4, 0x9d, 0xff, 0x80, 0x66, 0xfc, 0xd9, 0xd0, 0x76, 0xb5, 0x6f, 0x33, 0xe7, 0x9e,
+ 0x73, 0xef, 0xc9, 0xf5, 0xbd, 0x13, 0x68, 0xdb, 0x5e, 0xcf, 0x0f, 0x3c, 0x97, 0xf2, 0x15, 0xdd,
+ 0xb0, 0xde, 0x2f, 0x81, 0xcd, 0x69, 0x6f, 0xdb, 0xef, 0xf1, 0x3b, 0x9f, 0xb2, 0xae, 0x1f, 0x78,
+ 0xdc, 0xc3, 0x47, 0xb6, 0xd7, 0x4d, 0x39, 0x5d, 0xc9, 0xe9, 0x6e, 0xfb, 0xc7, 0x07, 0x4b, 0x6f,
+ 0xe9, 0x49, 0x4a, 0x4f, 0x9c, 0x42, 0x76, 0x9b, 0x41, 0x89, 0xd0, 0x9f, 0x37, 0x94, 0x71, 0xac,
+ 0x41, 0x89, 0xdd, 0xb9, 0x37, 0x9e, 0xc3, 0xb4, 0x7c, 0x4b, 0xe9, 0x54, 0x48, 0x7c, 0xc5, 0x43,
+ 0x00, 0x6e, 0xbb, 0x94, 0xd1, 0xc0, 0xa6, 0x4c, 0x2b, 0xb4, 0x94, 0x4e, 0xb5, 0xdf, 0xee, 0x3e,
+ 0x5e, 0xa7, 0x6b, 0xd8, 0x2e, 0x9d, 0x49, 0xe6, 0x20, 0xff, 0xee, 0xaf, 0xd7, 0x7b, 0x24, 0xa3,
+ 0xbd, 0xcc, 0x97, 0x91, 0x9a, 0x6f, 0xff, 0x9e, 0x03, 0x48, 0x69, 0xf8, 0x35, 0x54, 0x1d, 0xeb,
+ 0x86, 0x3a, 0xcc, 0x0c, 0xe8, 0x82, 0x69, 0xa8, 0xa5, 0x74, 0x6a, 0x04, 0x42, 0x88, 0xd0, 0x05,
+ 0xc3, 0xdf, 0x41, 0x89, 0x59, 0xae, 0xef, 0x50, 0xa6, 0xe5, 0x64, 0xf1, 0xe6, 0x53, 0xc5, 0x67,
+ 0x92, 0x16, 0x15, 0x8e, 0x45, 0xf8, 0x02, 0x60, 0x65, 0x33, 0xee, 0x2d, 0x03, 0xcb, 0x65, 0x9a,
+ 0x22, 0x53, 0xbc, 0x79, 0x2a, 0xc5, 0x30, 0x66, 0xc6, 0xf6, 0x53, 0x29, 0x3e, 0x83, 0x0a, 0xfd,
+ 0x95, 0xba, 0xbe, 0x63, 0x05, 0x61, 0x93, 0xaa, 0xfd, 0xd6, 0x53, 0x79, 0xf4, 0x88, 0x18, 0xa5,
+ 0x49, 0x85, 0x78, 0x00, 0x65, 0x97, 0x72, 0xeb, 0xd6, 0xe2, 0x96, 0x56, 0x68, 0xa1, 0xe7, 0x92,
+ 0x8c, 0x23, 0x5e, 0x94, 0x24, 0xd1, 0x5d, 0xe6, 0xcb, 0x45, 0xb5, 0xd4, 0x36, 0xa1, 0x1c, 0x97,
+ 0x79, 0x7f, 0x17, 0x0f, 0xa0, 0xb0, 0xb5, 0x9c, 0x0d, 0xd5, 0x72, 0x2d, 0xd4, 0x41, 0x24, 0xbc,
+ 0xe0, 0x4f, 0xa0, 0x22, 0xbf, 0x0f, 0xb7, 0x5c, 0x5f, 0x53, 0x5a, 0xa8, 0xa3, 0x90, 0x14, 0x68,
+ 0x53, 0x28, 0x86, 0x2d, 0x4d, 0xd5, 0xe8, 0x49, 0x75, 0x6e, 0x47, 0x8d, 0xbf, 0x84, 0x17, 0x8c,
+ 0x5b, 0x01, 0x37, 0x77, 0x2b, 0xd4, 0x25, 0x6c, 0x24, 0x65, 0xfe, 0xc9, 0x41, 0x39, 0xfe, 0xa9,
+ 0xf8, 0x7b, 0xc8, 0x8b, 0x79, 0x96, 0x85, 0xea, 0xfd, 0xaf, 0xdf, 0xd7, 0x1a, 0x71, 0x08, 0xec,
+ 0xb9, 0x71, 0xe7, 0x53, 0x22, 0x85, 0xf8, 0x15, 0x94, 0x57, 0xd4, 0xf1, 0x45, 0x1f, 0x64, 0xbd,
+ 0x1a, 0x29, 0x89, 0x3b, 0xa1, 0x0b, 0x11, 0xda, 0xac, 0x6d, 0x2e, 0x43, 0xf9, 0x30, 0x24, 0xee,
+ 0x84, 0x2e, 0xda, 0x7f, 0x22, 0x80, 0x34, 0x15, 0xfe, 0x18, 0x5e, 0x8e, 0x75, 0x83, 0x8c, 0x4e,
+ 0x4d, 0xe3, 0xfa, 0xad, 0x6e, 0x5e, 0x4d, 0x66, 0x6f, 0xf5, 0xd3, 0xd1, 0xf9, 0x48, 0x3f, 0x53,
+ 0xf7, 0xf0, 0x4b, 0xf8, 0x28, 0x1b, 0x3c, 0x9d, 0x5e, 0x4d, 0x0c, 0x9d, 0xa8, 0x08, 0x1f, 0x42,
+ 0x23, 0x1b, 0xb8, 0x38, 0xb9, 0xba, 0xd0, 0xd5, 0x1c, 0x7e, 0x05, 0x87, 0x59, 0x78, 0x38, 0x9a,
+ 0x19, 0xd3, 0x0b, 0x72, 0x32, 0x56, 0x15, 0xdc, 0x84, 0xe3, 0xff, 0x29, 0xd2, 0x78, 0x7e, 0xb7,
+ 0xd4, 0xec, 0x6a, 0x3c, 0x3e, 0x21, 0xd7, 0x6a, 0x01, 0x1f, 0x80, 0x9a, 0x0d, 0x8c, 0x26, 0xe7,
+ 0x53, 0xb5, 0x88, 0x35, 0x38, 0x78, 0x40, 0x37, 0x4e, 0x0c, 0x7d, 0xa6, 0x1b, 0x6a, 0xa9, 0xfd,
+ 0x6f, 0x11, 0x2a, 0xc9, 0x6c, 0xe3, 0x4f, 0xa1, 0x32, 0xf7, 0x36, 0x6b, 0x6e, 0xda, 0x6b, 0x2e,
+ 0x3b, 0x9d, 0x1f, 0xee, 0x91, 0xb2, 0x84, 0x46, 0x6b, 0x8e, 0xdf, 0x40, 0x35, 0x0c, 0x2f, 0x1c,
+ 0xcf, 0xe2, 0xe1, 0xc4, 0x0c, 0xf7, 0x08, 0x48, 0xf0, 0x5c, 0x60, 0x58, 0x05, 0x85, 0x6d, 0x5c,
+ 0xd9, 0x60, 0x44, 0xc4, 0x11, 0x1f, 0x41, 0x91, 0xcd, 0x57, 0xd4, 0xb5, 0x64, 0x6b, 0x1b, 0x24,
+ 0xba, 0xe1, 0xcf, 0xa1, 0xfe, 0x1b, 0x0d, 0x3c, 0x93, 0xaf, 0x02, 0xca, 0x56, 0x9e, 0x73, 0x2b,
+ 0xa7, 0x1e, 0x91, 0x9a, 0x40, 0x8d, 0x18, 0xc4, 0x5f, 0x44, 0xb4, 0xd4, 0x57, 0x51, 0xfa, 0x42,
+ 0x64, 0x5f, 0xe0, 0xa7, 0xb1, 0xb7, 0xaf, 0x40, 0xcd, 0xf0, 0x42, 0x83, 0x25, 0x69, 0x10, 0x91,
+ 0x7a, 0xc2, 0x0c, 0x4d, 0x4e, 0xa1, 0xbe, 0xa6, 0x4b, 0x8b, 0xdb, 0x5b, 0x6a, 0x32, 0xdf, 0x5a,
+ 0x33, 0xad, 0xfc, 0xfc, 0xeb, 0x35, 0xd8, 0xcc, 0x7f, 0xa2, 0x7c, 0xe6, 0x5b, 0xeb, 0x68, 0xe5,
+ 0x6a, 0xb1, 0x5e, 0x60, 0x4c, 0x8c, 0x74, 0x92, 0xf0, 0x96, 0x3a, 0xdc, 0x62, 0x5a, 0xa5, 0xa5,
+ 0x74, 0x30, 0x49, 0xea, 0x9c, 0x49, 0xf4, 0x01, 0x51, 0x3a, 0x65, 0x1a, 0xb4, 0x94, 0x0e, 0x4a,
+ 0x89, 0xd2, 0x26, 0x13, 0x16, 0x7d, 0x8f, 0xd9, 0x19, 0x8b, 0xd5, 0x0f, 0xb5, 0x18, 0xeb, 0x13,
+ 0x8b, 0x49, 0xc2, 0xc8, 0xe2, 0x7e, 0x68, 0x31, 0x86, 0x53, 0x8b, 0x09, 0x31, 0xb2, 0x58, 0x0b,
+ 0x2d, 0xc6, 0x70, 0x64, 0xf1, 0x12, 0x20, 0xa0, 0x8c, 0x72, 0x73, 0x25, 0xbe, 0x4a, 0xfd, 0xf9,
+ 0xbd, 0x4c, 0x66, 0xac, 0x4b, 0x84, 0x66, 0x68, 0xaf, 0x39, 0xa9, 0x04, 0xf1, 0xf1, 0xe1, 0x8b,
+ 0xf1, 0x62, 0xf7, 0xc5, 0xf8, 0x0c, 0x6a, 0xf3, 0x0d, 0xe3, 0x9e, 0x6b, 0xca, 0xf7, 0x85, 0x69,
+ 0xaa, 0x34, 0xb4, 0x1f, 0x82, 0x3f, 0x48, 0xec, 0xb1, 0x67, 0xa5, 0xf1, 0xe8, 0xb3, 0x72, 0x0b,
+ 0x95, 0xc4, 0x03, 0x3e, 0x86, 0x23, 0x22, 0x56, 0xc1, 0x1c, 0x8e, 0x26, 0xc6, 0xce, 0x3e, 0x63,
+ 0xa8, 0x67, 0x62, 0xd7, 0xfa, 0x4c, 0x45, 0xb8, 0x01, 0xb5, 0x0c, 0x36, 0x99, 0xaa, 0x39, 0xb1,
+ 0x72, 0x19, 0x28, 0x5c, 0x6e, 0x65, 0x50, 0x82, 0x82, 0xec, 0xde, 0x60, 0x1f, 0x20, 0x1d, 0xcc,
+ 0xf6, 0xb7, 0x00, 0xe9, 0x97, 0x12, 0xbb, 0xe1, 0x2d, 0x16, 0x8c, 0x86, 0xcb, 0xd6, 0x20, 0xd1,
+ 0x4d, 0xe0, 0x0e, 0x5d, 0x2f, 0xf9, 0x4a, 0xee, 0x58, 0x8d, 0x44, 0xb7, 0xc1, 0xe1, 0xbb, 0xfb,
+ 0x26, 0xfa, 0xe3, 0xbe, 0x89, 0xfe, 0xbe, 0x6f, 0xa2, 0x1f, 0x4b, 0xb2, 0xbb, 0xdb, 0xfe, 0x4d,
+ 0x51, 0xfe, 0x6b, 0x7f, 0xf3, 0x5f, 0x00, 0x00, 0x00, 0xff, 0xff, 0x62, 0x8f, 0x36, 0x4b, 0x09,
+ 0x08, 0x00, 0x00,
}
func (m *Request) Marshal() (dAtA []byte, err error) {
@@ -996,11 +1025,6 @@ func (m *TimeSeries) MarshalToSizedBuffer(dAtA []byte) (int, error) {
i -= len(m.XXX_unrecognized)
copy(dAtA[i:], m.XXX_unrecognized)
}
- if m.CreatedTimestamp != 0 {
- i = encodeVarintTypes(dAtA, i, uint64(m.CreatedTimestamp))
- i--
- dAtA[i] = 0x30
- }
{
size, err := m.Metadata.MarshalToSizedBuffer(dAtA[:i])
if err != nil {
@@ -1154,6 +1178,11 @@ func (m *Sample) MarshalToSizedBuffer(dAtA []byte) (int, error) {
i -= len(m.XXX_unrecognized)
copy(dAtA[i:], m.XXX_unrecognized)
}
+ if m.StartTimestamp != 0 {
+ i = encodeVarintTypes(dAtA, i, uint64(m.StartTimestamp))
+ i--
+ dAtA[i] = 0x18
+ }
if m.Timestamp != 0 {
i = encodeVarintTypes(dAtA, i, uint64(m.Timestamp))
i--
@@ -1234,6 +1263,13 @@ func (m *Histogram) MarshalToSizedBuffer(dAtA []byte) (int, error) {
i -= len(m.XXX_unrecognized)
copy(dAtA[i:], m.XXX_unrecognized)
}
+ if m.StartTimestamp != 0 {
+ i = encodeVarintTypes(dAtA, i, uint64(m.StartTimestamp))
+ i--
+ dAtA[i] = 0x1
+ i--
+ dAtA[i] = 0x88
+ }
if len(m.CustomValues) > 0 {
for iNdEx := len(m.CustomValues) - 1; iNdEx >= 0; iNdEx-- {
f6 := math.Float64bits(float64(m.CustomValues[iNdEx]))
@@ -1535,9 +1571,6 @@ func (m *TimeSeries) Size() (n int) {
}
l = m.Metadata.Size()
n += 1 + l + sovTypes(uint64(l))
- if m.CreatedTimestamp != 0 {
- n += 1 + sovTypes(uint64(m.CreatedTimestamp))
- }
if m.XXX_unrecognized != nil {
n += len(m.XXX_unrecognized)
}
@@ -1581,6 +1614,9 @@ func (m *Sample) Size() (n int) {
if m.Timestamp != 0 {
n += 1 + sovTypes(uint64(m.Timestamp))
}
+ if m.StartTimestamp != 0 {
+ n += 1 + sovTypes(uint64(m.StartTimestamp))
+ }
if m.XXX_unrecognized != nil {
n += len(m.XXX_unrecognized)
}
@@ -1670,6 +1706,9 @@ func (m *Histogram) Size() (n int) {
if len(m.CustomValues) > 0 {
n += 2 + sovTypes(uint64(len(m.CustomValues)*8)) + len(m.CustomValues)*8
}
+ if m.StartTimestamp != 0 {
+ n += 2 + sovTypes(uint64(m.StartTimestamp))
+ }
if m.XXX_unrecognized != nil {
n += len(m.XXX_unrecognized)
}
@@ -2093,25 +2132,6 @@ func (m *TimeSeries) Unmarshal(dAtA []byte) error {
return err
}
iNdEx = postIndex
- case 6:
- if wireType != 0 {
- return fmt.Errorf("proto: wrong wireType = %d for field CreatedTimestamp", wireType)
- }
- m.CreatedTimestamp = 0
- for shift := uint(0); ; shift += 7 {
- if shift >= 64 {
- return ErrIntOverflowTypes
- }
- if iNdEx >= l {
- return io.ErrUnexpectedEOF
- }
- b := dAtA[iNdEx]
- iNdEx++
- m.CreatedTimestamp |= int64(b&0x7F) << shift
- if b < 0x80 {
- break
- }
- }
default:
iNdEx = preIndex
skippy, err := skipTypes(dAtA[iNdEx:])
@@ -2350,6 +2370,25 @@ func (m *Sample) Unmarshal(dAtA []byte) error {
break
}
}
+ case 3:
+ if wireType != 0 {
+ return fmt.Errorf("proto: wrong wireType = %d for field StartTimestamp", wireType)
+ }
+ m.StartTimestamp = 0
+ for shift := uint(0); ; shift += 7 {
+ if shift >= 64 {
+ return ErrIntOverflowTypes
+ }
+ if iNdEx >= l {
+ return io.ErrUnexpectedEOF
+ }
+ b := dAtA[iNdEx]
+ iNdEx++
+ m.StartTimestamp |= int64(b&0x7F) << shift
+ if b < 0x80 {
+ break
+ }
+ }
default:
iNdEx = preIndex
skippy, err := skipTypes(dAtA[iNdEx:])
@@ -3038,6 +3077,25 @@ func (m *Histogram) Unmarshal(dAtA []byte) error {
} else {
return fmt.Errorf("proto: wrong wireType = %d for field CustomValues", wireType)
}
+ case 17:
+ if wireType != 0 {
+ return fmt.Errorf("proto: wrong wireType = %d for field StartTimestamp", wireType)
+ }
+ m.StartTimestamp = 0
+ for shift := uint(0); ; shift += 7 {
+ if shift >= 64 {
+ return ErrIntOverflowTypes
+ }
+ if iNdEx >= l {
+ return io.ErrUnexpectedEOF
+ }
+ b := dAtA[iNdEx]
+ iNdEx++
+ m.StartTimestamp |= int64(b&0x7F) << shift
+ if b < 0x80 {
+ break
+ }
+ }
default:
iNdEx = preIndex
skippy, err := skipTypes(dAtA[iNdEx:])
diff --git a/prompb/io/prometheus/write/v2/types.proto b/prompb/io/prometheus/write/v2/types.proto
index ff6c4936bb..c1ae04d206 100644
--- a/prompb/io/prometheus/write/v2/types.proto
+++ b/prompb/io/prometheus/write/v2/types.proto
@@ -14,6 +14,7 @@
// NOTE: This file is also available on https://buf.build/prometheus/prometheus/docs/main:io.prometheus.write.v2
syntax = "proto3";
+
package io.prometheus.write.v2;
option go_package = "writev2";
@@ -27,6 +28,8 @@ import "gogoproto/gogo.proto";
// The canonical Content-Type request header value for this message is
// "application/x-protobuf;proto=io.prometheus.write.v2.Request"
//
+// Version: v2.0-rc.4
+//
// NOTE: gogoproto options might change in future for this file, they
// are not part of the spec proto (they only modify the generated Go code, not
// the serialized message). See: https://github.com/prometheus/prometheus/issues/11908
@@ -59,7 +62,7 @@ message TimeSeries {
//
// Note that there might be multiple TimeSeries objects in the same
// Requests with the same labels e.g. for different exemplars, metadata
- // or created timestamp.
+ // or start timestamp.
repeated uint32 labels_refs = 1;
// Timeseries messages can either specify samples or (native) histogram samples
@@ -76,23 +79,9 @@ message TimeSeries {
// metadata represents the metadata associated with the given series' samples.
Metadata metadata = 5 [(gogoproto.nullable) = false];
- // created_timestamp represents an optional created timestamp associated with
- // this series' samples in ms format, typically for counter or histogram type
- // metrics. Created timestamp represents the time when the counter started
- // counting (sometimes referred to as start timestamp), which can increase
- // the accuracy of query results.
- //
- // Note that some receivers might require this and in return fail to
- // ingest such samples within the Request.
- //
- // For Go, see github.com/prometheus/prometheus/model/timestamp/timestamp.go
- // for conversion from/to time.Time to Prometheus timestamp.
- //
- // Note that the "optional" keyword is omitted due to
- // https://cloud.google.com/apis/design/design_patterns.md#optional_primitive_fields
- // Zero value means value not set. If you need to use exactly zero value for
- // the timestamp, use 1 millisecond before or after.
- int64 created_timestamp = 6;
+ // This field is reserved for backward compatibility with the deprecated fields;
+ // previously present in the experimental remote write period.
+ reserved 6;
}
// Exemplar is an additional information attached to some series' samples.
@@ -123,6 +112,26 @@ message Sample {
// For Go, see github.com/prometheus/prometheus/model/timestamp/timestamp.go
// for conversion from/to time.Time to Prometheus timestamp.
int64 timestamp = 2;
+ // start_timestamp represents an optional start timestamp for the sample,
+ // in ms format. This information is typically used for counter, histogram (cumulative)
+ // or delta type metrics.
+ //
+ // For cumulative metrics, the start timestamp represents the time when the
+ // counter started counting (sometimes referred to as start timestamp), which
+ // can increase the accuracy of certain processing and query semantics (e.g. rates).
+ //
+ // Note:
+ // * That some receivers might require start timestamps for certain metric
+ // types; rejecting such samples within the Request as a result.
+ // * start timestamp is the same as "created timestamp" name Prometheus used in the past.
+ //
+ // For Go, see github.com/prometheus/prometheus/model/timestamp/timestamp.go
+ // for conversion from/to time.Time to Prometheus timestamp.
+ //
+ // Note that the "optional" keyword is omitted due to efficiency and consistency.
+ // Zero value means value not set. If you need to use exactly zero value for
+ // the timestamp, use 1 millisecond before or after.
+ int64 start_timestamp = 3;
}
// Metadata represents the metadata associated with the given series' samples.
@@ -148,12 +157,11 @@ message Metadata {
uint32 unit_ref = 4;
}
-// A native histogram, also known as a sparse histogram.
-// Original design doc:
-// https://docs.google.com/document/d/1cLNv3aufPZb3fNfaJgdaRBZsInZKKIHo9E6HinJVbpM/edit
-// The appendix of this design doc also explains the concept of float
-// histograms. This Histogram message can represent both, the usual
-// integer histogram as well as a float histogram.
+// A native histogram message, supporting
+// * sparse exponential bucketing, custom bucketing.
+// * float or integer histograms.
+//
+// See the full spec: https://prometheus.io/docs/specs/native_histograms/
message Histogram {
enum ResetHint {
RESET_HINT_UNSPECIFIED = 0; // Need to test for a counter reset explicitly.
@@ -242,6 +250,24 @@ message Histogram {
// The last element is not only the upper inclusive bound of the last regular
// bucket, but implicitly the lower exclusive bound of the +Inf bucket.
repeated double custom_values = 16;
+
+ // start_timestamp represents an optional start timestamp for the histogram sample,
+ // in ms format. The start timestamp represents the time when the histogram
+ // started counting, which can increase the accuracy of certain processing and
+ // query semantics (e.g. rates).
+ //
+ // Note:
+ // * That some receivers might require start timestamps for certain metric
+ // types; rejecting such samples within the Request as a result.
+ // * start timestamp is the same as "created timestamp" name Prometheus used in the past.
+ //
+ // For Go, see github.com/prometheus/prometheus/model/timestamp/timestamp.go
+ // for conversion from/to time.Time to Prometheus timestamp.
+ //
+ // Note that the "optional" keyword is omitted due to efficiency and consistency.
+ // Zero value means value not set. If you need to use exactly zero value for
+ // the timestamp, use 1 millisecond before or after.
+ int64 start_timestamp = 17;
}
// A BucketSpan defines a number of consecutive buckets with their
diff --git a/prompb/io/prometheus/write/v2/types_test.go b/prompb/io/prometheus/write/v2/types_test.go
index 5b7622fc2f..12528943a1 100644
--- a/prompb/io/prometheus/write/v2/types_test.go
+++ b/prompb/io/prometheus/write/v2/types_test.go
@@ -1,4 +1,4 @@
-// Copyright 2024 Prometheus Team
+// 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
diff --git a/prompb/rwcommon/codec_test.go b/prompb/rwcommon/codec_test.go
index 73a8196fa8..ee92581f59 100644
--- a/prompb/rwcommon/codec_test.go
+++ b/prompb/rwcommon/codec_test.go
@@ -1,4 +1,4 @@
-// Copyright 2024 Prometheus Team
+// 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
@@ -198,17 +198,14 @@ func testFloatHistogram() histogram.FloatHistogram {
func TestFromIntToFloatOrIntHistogram(t *testing.T) {
t.Run("v1", func(t *testing.T) {
- // v1 does not support nhcb.
- testIntHistWithoutNHCB := testIntHistogram()
- testIntHistWithoutNHCB.CustomValues = nil
- testFloatHistWithoutNHCB := testFloatHistogram()
- testFloatHistWithoutNHCB.CustomValues = nil
+ testIntHist := testIntHistogram()
+ testFloatHist := testFloatHistogram()
- h := prompb.FromIntHistogram(123, &testIntHistWithoutNHCB)
+ h := prompb.FromIntHistogram(123, &testIntHist)
require.False(t, h.IsFloatHistogram())
require.Equal(t, int64(123), h.Timestamp)
- require.Equal(t, testIntHistWithoutNHCB, *h.ToIntHistogram())
- require.Equal(t, testFloatHistWithoutNHCB, *h.ToFloatHistogram())
+ require.Equal(t, testIntHist, *h.ToIntHistogram())
+ require.Equal(t, testFloatHist, *h.ToFloatHistogram())
})
t.Run("v2", func(t *testing.T) {
testIntHist := testIntHistogram()
@@ -224,15 +221,13 @@ func TestFromIntToFloatOrIntHistogram(t *testing.T) {
func TestFromFloatToFloatHistogram(t *testing.T) {
t.Run("v1", func(t *testing.T) {
- // v1 does not support nhcb.
- testFloatHistWithoutNHCB := testFloatHistogram()
- testFloatHistWithoutNHCB.CustomValues = nil
+ testFloatHist := testFloatHistogram()
- h := prompb.FromFloatHistogram(123, &testFloatHistWithoutNHCB)
+ h := prompb.FromFloatHistogram(123, &testFloatHist)
require.True(t, h.IsFloatHistogram())
require.Equal(t, int64(123), h.Timestamp)
require.Nil(t, h.ToIntHistogram())
- require.Equal(t, testFloatHistWithoutNHCB, *h.ToFloatHistogram())
+ require.Equal(t, testFloatHist, *h.ToFloatHistogram())
})
t.Run("v2", func(t *testing.T) {
testFloatHist := testFloatHistogram()
diff --git a/promql/bench_test.go b/promql/bench_test.go
index 37c8311305..9f0de52ec8 100644
--- a/promql/bench_test.go
+++ b/promql/bench_test.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
@@ -36,6 +36,8 @@ import (
"github.com/prometheus/prometheus/util/teststorage"
)
+var testParser = parser.NewParser(parser.Options{})
+
func setupRangeQueryTestData(stor *teststorage.TestStorage, _ *promql.Engine, interval, numIntervals int) error {
ctx := context.Background()
@@ -332,18 +334,15 @@ func rangeQueryCases() []benchCase {
}
func BenchmarkRangeQuery(b *testing.B) {
- parser.EnableExtendedRangeSelectors = true
- b.Cleanup(func() {
- parser.EnableExtendedRangeSelectors = false
- })
stor := teststorage.New(b)
stor.DisableCompactions() // Don't want auto-compaction disrupting timings.
- defer stor.Close()
+
opts := promql.EngineOpts{
Logger: nil,
Reg: nil,
MaxSamples: 50000000,
Timeout: 100 * time.Second,
+ Parser: parser.NewParser(parser.Options{EnableExtendedRangeSelectors: true, EnableExperimentalFunctions: true}),
}
engine := promqltest.NewTestEngineWithOpts(b, opts)
@@ -383,7 +382,6 @@ func BenchmarkRangeQuery(b *testing.B) {
func BenchmarkJoinQuery(b *testing.B) {
stor := teststorage.New(b)
stor.DisableCompactions() // Don't want auto-compaction disrupting timings.
- defer stor.Close()
opts := promql.EngineOpts{
Logger: nil,
@@ -393,40 +391,44 @@ func BenchmarkJoinQuery(b *testing.B) {
}
engine := promqltest.NewTestEngineWithOpts(b, opts)
- const interval = 10000 // 10s interval.
+ const (
+ interval = 10000 // 10s interval.
+ steps = 5000
+ numInstances = 1000
+ )
- // A day of data plus 10k steps.
- numIntervals := 8640 + 10000
+ // A day of data plus steps.
+ numIntervals := 8640 + steps
- require.NoError(b, setupJoinQueryTestData(stor, engine, interval, numIntervals, 1000))
+ require.NoError(b, setupJoinQueryTestData(stor, engine, interval, numIntervals, numInstances))
for _, c := range []benchCase{
{
expr: `rpc_request_success_total + rpc_request_error_total`,
- steps: 10000,
+ steps: steps,
},
{
expr: `rpc_request_success_total + ON (job, instance) GROUP_LEFT rpc_request_error_total`,
- steps: 10000,
+ steps: steps,
},
{
expr: `rpc_request_success_total AND rpc_request_error_total{instance=~"0.*"}`, // 0.* keeps 1/16 of UUID values
- steps: 10000,
+ steps: steps,
},
{
expr: `rpc_request_success_total OR rpc_request_error_total{instance=~"0.*"}`, // 0.* keeps 1/16 of UUID values
- steps: 10000,
+ steps: steps,
},
{
expr: `rpc_request_success_total UNLESS rpc_request_error_total{instance=~"0.*"}`, // 0.* keeps 1/16 of UUID values
- steps: 10000,
+ steps: steps,
},
} {
name := fmt.Sprintf("expr=%s/steps=%d", c.expr, c.steps)
b.Run(name, func(b *testing.B) {
ctx := context.Background()
- b.ReportAllocs()
- for b.Loop() {
+
+ queryFn := func() {
qry, err := engine.NewRangeQuery(
ctx, stor, nil, c.expr,
timestamp.Time(int64((numIntervals-c.steps)*10_000)),
@@ -439,13 +441,20 @@ func BenchmarkJoinQuery(b *testing.B) {
qry.Close()
}
+
+ queryFn() // Warm up run.
+
+ b.ResetTimer()
+ b.ReportAllocs()
+ for b.Loop() {
+ queryFn()
+ }
})
}
}
func BenchmarkNativeHistograms(b *testing.B) {
testStorage := teststorage.New(b)
- defer testStorage.Close()
app := testStorage.Appender(context.TODO())
if err := generateNativeHistogramSeries(app, 3000); err != nil {
@@ -523,7 +532,6 @@ func BenchmarkNativeHistograms(b *testing.B) {
func BenchmarkNativeHistogramsCustomBuckets(b *testing.B) {
testStorage := teststorage.New(b)
- defer testStorage.Close()
app := testStorage.Appender(context.TODO())
if err := generateNativeHistogramCustomBucketsSeries(app, 3000); err != nil {
@@ -594,7 +602,6 @@ func BenchmarkNativeHistogramsCustomBuckets(b *testing.B) {
func BenchmarkInfoFunction(b *testing.B) {
// Initialize test storage and generate test series data.
testStorage := teststorage.New(b)
- defer testStorage.Close()
start := time.Unix(0, 0)
end := start.Add(2 * time.Hour)
@@ -636,6 +643,7 @@ func BenchmarkInfoFunction(b *testing.B) {
Timeout: 100 * time.Second,
EnableAtModifier: true,
EnableNegativeOffset: true,
+ Parser: parser.NewParser(parser.Options{EnableExperimentalFunctions: true}),
}
engine := promql.NewEngine(opts)
b.Run(tc.name, func(b *testing.B) {
@@ -796,13 +804,13 @@ func BenchmarkParser(b *testing.B) {
b.Run(c, func(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
- parser.ParseExpr(c)
+ testParser.ParseExpr(c)
}
})
}
for _, c := range cases {
b.Run("preprocess "+c, func(b *testing.B) {
- expr, _ := parser.ParseExpr(c)
+ expr, _ := testParser.ParseExpr(c)
start, end := time.Now().Add(-time.Hour), time.Now()
for b.Loop() {
promql.PreprocessExpr(expr, start, end, 0)
@@ -814,7 +822,7 @@ func BenchmarkParser(b *testing.B) {
b.Run(name, func(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
- parser.ParseExpr(c)
+ testParser.ParseExpr(c)
}
})
}
diff --git a/promql/durations.go b/promql/durations.go
index c882adfbb6..c660dbf464 100644
--- a/promql/durations.go
+++ b/promql/durations.go
@@ -1,4 +1,4 @@
-// Copyright 2025 The Prometheus Authors
+// 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
@@ -28,7 +28,8 @@ import (
// in OriginalOffsetExpr representing (1h / 2). This visitor evaluates
// such duration expression, setting OriginalOffset to 30m.
type durationVisitor struct {
- step time.Duration
+ step time.Duration
+ queryRange time.Duration
}
// Visit finds any duration expressions in AST Nodes and modifies the Node to
@@ -121,6 +122,8 @@ func (v *durationVisitor) evaluateDurationExpr(expr parser.Expr) (float64, error
switch n.Op {
case parser.STEP:
return float64(v.step.Seconds()), nil
+ case parser.RANGE:
+ return float64(v.queryRange.Seconds()), nil
case parser.MIN:
return math.Min(lhs, rhs), nil
case parser.MAX:
diff --git a/promql/durations_test.go b/promql/durations_test.go
index 18592a0d0a..103c068dc1 100644
--- a/promql/durations_test.go
+++ b/promql/durations_test.go
@@ -1,4 +1,4 @@
-// Copyright 2025 The Prometheus Authors
+// 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
@@ -23,11 +23,7 @@ import (
)
func TestDurationVisitor(t *testing.T) {
- // Enable experimental duration expression parsing.
- parser.ExperimentalDurationExpr = true
- t.Cleanup(func() {
- parser.ExperimentalDurationExpr = false
- })
+ p := parser.NewParser(parser.Options{ExperimentalDurationExpr: true})
complexExpr := `sum_over_time(
rate(metric[5m] offset 1h)[10m:30s] offset 2h
) +
@@ -38,7 +34,7 @@ func TestDurationVisitor(t *testing.T) {
metric[2h * 0.5]
)`
- expr, err := parser.ParseExpr(complexExpr)
+ expr, err := p.ParseExpr(complexExpr)
require.NoError(t, err)
err = parser.Walk(&durationVisitor{}, expr, nil)
@@ -213,6 +209,37 @@ func TestCalculateDuration(t *testing.T) {
},
expected: 3 * time.Second,
},
+ {
+ name: "range",
+ expr: &parser.DurationExpr{
+ Op: parser.RANGE,
+ },
+ expected: 5 * time.Minute,
+ },
+ {
+ name: "range division",
+ expr: &parser.DurationExpr{
+ LHS: &parser.DurationExpr{
+ Op: parser.RANGE,
+ },
+ RHS: &parser.NumberLiteral{Val: 2},
+ Op: parser.DIV,
+ },
+ expected: 150 * time.Second,
+ },
+ {
+ name: "max of step and range",
+ expr: &parser.DurationExpr{
+ LHS: &parser.DurationExpr{
+ Op: parser.STEP,
+ },
+ RHS: &parser.DurationExpr{
+ Op: parser.RANGE,
+ },
+ Op: parser.MAX,
+ },
+ expected: 5 * time.Minute,
+ },
{
name: "division by zero",
expr: &parser.DurationExpr{
@@ -243,7 +270,7 @@ func TestCalculateDuration(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- v := &durationVisitor{step: 1 * time.Second}
+ v := &durationVisitor{step: 1 * time.Second, queryRange: 5 * time.Minute}
result, err := v.calculateDuration(tt.expr, tt.allowedNegative)
if tt.errorMessage != "" {
require.Error(t, err)
diff --git a/promql/engine.go b/promql/engine.go
index 75fc9b05d3..bd7b868d86 100644
--- a/promql/engine.go
+++ b/promql/engine.go
@@ -1,4 +1,4 @@
-// Copyright 2013 The Prometheus Authors
+// 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
@@ -49,6 +49,8 @@ import (
"github.com/prometheus/prometheus/storage"
"github.com/prometheus/prometheus/tsdb/chunkenc"
"github.com/prometheus/prometheus/util/annotations"
+ "github.com/prometheus/prometheus/util/features"
+ "github.com/prometheus/prometheus/util/kahansum"
"github.com/prometheus/prometheus/util/logging"
"github.com/prometheus/prometheus/util/stats"
"github.com/prometheus/prometheus/util/zeropool"
@@ -76,15 +78,19 @@ const (
)
type engineMetrics struct {
- currentQueries prometheus.Gauge
- maxConcurrentQueries prometheus.Gauge
- queryLogEnabled prometheus.Gauge
- queryLogFailures prometheus.Counter
- queryQueueTime prometheus.Observer
- queryPrepareTime prometheus.Observer
- queryInnerEval prometheus.Observer
- queryResultSort prometheus.Observer
- querySamples prometheus.Counter
+ currentQueries prometheus.Gauge
+ maxConcurrentQueries prometheus.Gauge
+ queryLogEnabled prometheus.Gauge
+ queryLogFailures prometheus.Counter
+ queryQueueTime prometheus.Observer
+ queryQueueTimeHistogram prometheus.Observer
+ queryPrepareTime prometheus.Observer
+ queryPrepareTimeHistogram prometheus.Observer
+ queryInnerEval prometheus.Observer
+ queryInnerEvalHistogram prometheus.Observer
+ queryResultSort prometheus.Observer
+ queryResultSortHistogram prometheus.Observer
+ querySamples prometheus.Counter
}
type (
@@ -326,6 +332,12 @@ type EngineOpts struct {
EnableDelayedNameRemoval bool
// EnableTypeAndUnitLabels will allow PromQL Engine to make decisions based on the type and unit labels.
EnableTypeAndUnitLabels bool
+
+ // FeatureRegistry is the registry for tracking enabled/disabled features.
+ FeatureRegistry features.Collector
+
+ // Parser is the PromQL parser instance used for parsing expressions.
+ Parser parser.Parser
}
// Engine handles the lifetime of queries from beginning to end.
@@ -345,6 +357,7 @@ type Engine struct {
enablePerStepStats bool
enableDelayedNameRemoval bool
enableTypeAndUnitLabels bool
+ parser parser.Parser
}
// NewEngine returns a new engine.
@@ -363,6 +376,19 @@ func NewEngine(opts EngineOpts) *Engine {
[]string{"slice"},
)
+ queryResultHistogram := prometheus.NewHistogramVec(prometheus.HistogramOpts{
+ Namespace: namespace,
+ Subsystem: subsystem,
+ Name: "query_duration_histogram_seconds",
+ Help: "The duration of various parts of PromQL query execution.",
+ Buckets: []float64{.01, .1, 1, 10},
+ NativeHistogramBucketFactor: 1.1,
+ NativeHistogramMaxBucketNumber: 100,
+ NativeHistogramMinResetDuration: 1 * time.Hour,
+ },
+ []string{"slice"},
+ )
+
metrics := &engineMetrics{
currentQueries: prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: namespace,
@@ -394,10 +420,14 @@ func NewEngine(opts EngineOpts) *Engine {
Name: "query_samples_total",
Help: "The total number of samples loaded by all queries.",
}),
- queryQueueTime: queryResultSummary.WithLabelValues("queue_time"),
- queryPrepareTime: queryResultSummary.WithLabelValues("prepare_time"),
- queryInnerEval: queryResultSummary.WithLabelValues("inner_eval"),
- queryResultSort: queryResultSummary.WithLabelValues("result_sort"),
+ queryQueueTime: queryResultSummary.WithLabelValues("queue_time"),
+ queryQueueTimeHistogram: queryResultHistogram.WithLabelValues("queue_time"),
+ queryPrepareTime: queryResultSummary.WithLabelValues("prepare_time"),
+ queryPrepareTimeHistogram: queryResultHistogram.WithLabelValues("prepare_time"),
+ queryInnerEval: queryResultSummary.WithLabelValues("inner_eval"),
+ queryInnerEvalHistogram: queryResultHistogram.WithLabelValues("inner_eval"),
+ queryResultSort: queryResultSummary.WithLabelValues("result_sort"),
+ queryResultSortHistogram: queryResultHistogram.WithLabelValues("result_sort"),
}
if t := opts.ActiveQueryTracker; t != nil {
@@ -406,6 +436,10 @@ func NewEngine(opts EngineOpts) *Engine {
metrics.maxConcurrentQueries.Set(-1)
}
+ if opts.Parser == nil {
+ opts.Parser = parser.NewParser(parser.Options{})
+ }
+
if opts.LookbackDelta == 0 {
opts.LookbackDelta = defaultLookbackDelta
if l := opts.Logger; l != nil {
@@ -421,9 +455,24 @@ func NewEngine(opts EngineOpts) *Engine {
metrics.queryLogFailures,
metrics.querySamples,
queryResultSummary,
+ queryResultHistogram,
)
}
+ if r := opts.FeatureRegistry; r != nil {
+ r.Set(features.PromQL, "at_modifier", opts.EnableAtModifier)
+ r.Set(features.PromQL, "negative_offset", opts.EnableNegativeOffset)
+ r.Set(features.PromQL, "per_step_stats", opts.EnablePerStepStats)
+ r.Set(features.PromQL, "delayed_name_removal", opts.EnableDelayedNameRemoval)
+ r.Set(features.PromQL, "type_and_unit_labels", opts.EnableTypeAndUnitLabels)
+ r.Enable(features.PromQL, "per_query_lookback_delta")
+ r.Enable(features.PromQL, "subqueries")
+
+ if opts.Parser != nil {
+ opts.Parser.RegisterFeatures(r)
+ }
+ }
+
return &Engine{
timeout: opts.Timeout,
logger: opts.Logger,
@@ -437,6 +486,7 @@ func NewEngine(opts EngineOpts) *Engine {
enablePerStepStats: opts.EnablePerStepStats,
enableDelayedNameRemoval: opts.EnableDelayedNameRemoval,
enableTypeAndUnitLabels: opts.EnableTypeAndUnitLabels,
+ parser: opts.Parser,
}
}
@@ -485,7 +535,7 @@ func (ng *Engine) NewInstantQuery(ctx context.Context, q storage.Queryable, opts
return nil, err
}
defer finishQueue()
- expr, err := parser.ParseExpr(qs)
+ expr, err := ng.parser.ParseExpr(qs)
if err != nil {
return nil, err
}
@@ -506,7 +556,7 @@ func (ng *Engine) NewRangeQuery(ctx context.Context, q storage.Queryable, opts Q
return nil, err
}
defer finishQueue()
- expr, err := parser.ParseExpr(qs)
+ expr, err := ng.parser.ParseExpr(qs)
if err != nil {
return nil, err
}
@@ -701,7 +751,7 @@ func (ng *Engine) queueActive(ctx context.Context, q *query) (func(), error) {
if ng.activeQueryTracker == nil {
return func() {}, nil
}
- queueSpanTimer, _ := q.stats.GetSpanTimer(ctx, stats.ExecQueueTime, ng.metrics.queryQueueTime)
+ queueSpanTimer, _ := q.stats.GetSpanTimer(ctx, stats.ExecQueueTime, ng.metrics.queryQueueTime, ng.metrics.queryQueueTimeHistogram)
queryIndex, err := ng.activeQueryTracker.Insert(ctx, q.q)
queueSpanTimer.Finish()
return func() { ng.activeQueryTracker.Delete(queryIndex) }, err
@@ -717,7 +767,7 @@ func durationMilliseconds(d time.Duration) int64 {
// execEvalStmt evaluates the expression of an evaluation statement for the given time range.
func (ng *Engine) execEvalStmt(ctx context.Context, query *query, s *parser.EvalStmt) (parser.Value, annotations.Annotations, error) {
- prepareSpanTimer, ctxPrepare := query.stats.GetSpanTimer(ctx, stats.QueryPreparationTime, ng.metrics.queryPrepareTime)
+ prepareSpanTimer, ctxPrepare := query.stats.GetSpanTimer(ctx, stats.QueryPreparationTime, ng.metrics.queryPrepareTime, ng.metrics.queryPrepareTimeHistogram)
mint, maxt := FindMinMaxTime(s)
querier, err := query.queryable.Querier(mint, maxt)
if err != nil {
@@ -732,7 +782,7 @@ func (ng *Engine) execEvalStmt(ctx context.Context, query *query, s *parser.Eval
// Modify the offset of vector and matrix selectors for the @ modifier
// w.r.t. the start time since only 1 evaluation will be done on them.
setOffsetForAtModifier(timeMilliseconds(s.Start), s.Expr)
- evalSpanTimer, ctxInnerEval := query.stats.GetSpanTimer(ctx, stats.InnerEvalTime, ng.metrics.queryInnerEval)
+ evalSpanTimer, ctxInnerEval := query.stats.GetSpanTimer(ctx, stats.InnerEvalTime, ng.metrics.queryInnerEval, ng.metrics.queryInnerEvalHistogram)
// Instant evaluation. This is executed as a range evaluation with one step.
if s.Start.Equal(s.End) && s.Interval == 0 {
start := timeMilliseconds(s.Start)
@@ -835,7 +885,7 @@ func (ng *Engine) execEvalStmt(ctx context.Context, query *query, s *parser.Eval
}
func (ng *Engine) sortMatrixResult(ctx context.Context, query *query, mat Matrix) {
- sortSpanTimer, _ := query.stats.GetSpanTimer(ctx, stats.ResultSortTime, ng.metrics.queryResultSort)
+ sortSpanTimer, _ := query.stats.GetSpanTimer(ctx, stats.ResultSortTime, ng.metrics.queryResultSort, ng.metrics.queryResultSortHistogram)
sort.Sort(mat)
sortSpanTimer.Finish()
}
@@ -1137,15 +1187,20 @@ func (ev *evaluator) Eval(ctx context.Context, expr parser.Expr) (v parser.Value
v, ws = ev.eval(ctx, expr)
if ev.enableDelayedNameRemoval {
- ev.cleanupMetricLabels(v)
+ v = ev.cleanupMetricLabels(v)
}
return v, ws, nil
}
// EvalSeriesHelper stores extra information about a series.
type EvalSeriesHelper struct {
- // Used to map left-hand to right-hand in binary operations.
- signature string
+ // Ordinal number of join signature, used to map left-hand to right-hand in
+ // binary operations. For example given the following series, if
+ // the join signature is job, instance then:
+ // metric{job="a", instance="1", other="x"} -> sigOrdinal 0
+ // metric{job="a", instance="1", other="y"} -> sigOrdinal 0
+ // metric{job="a", instance="2", other="x"} -> sigOrdinal 1
+ sigOrdinal int
}
// EvalNodeHelper stores extra information and caches for evaluating a single node across steps.
@@ -1159,15 +1214,22 @@ type EvalNodeHelper struct {
// funcHistogramQuantile and funcHistogramFraction for classic histograms.
signatureToMetricWithBuckets map[string]*metricWithBuckets
nativeHistogramSamples []Sample
+ // funcHistogramQuantiles for histograms.
+ quantileStrs map[float64]string
+ signatureToLabelsWithQuantile map[string]map[float64]labels.Labels
lb *labels.Builder
lblBuf []byte
lblResultBuf []byte
// For binary vector matching.
- rightSigs map[string]Sample
- matchedSigs map[string]map[uint64]struct{}
+ rightSigs map[int]Sample
+ matchedSigs map[int]map[uint64]struct{}
resultMetric map[string]labels.Labels
+ numSigs int
+
+ // For info series matching.
+ rightStrSigs map[string]Sample
// Additional options for the evaluation.
enableDelayedNameRemoval bool
@@ -1246,17 +1308,47 @@ func (enh *EvalNodeHelper) resetHistograms(inVec Vector, arg parser.Expr) annota
return annos
}
+func (enh *EvalNodeHelper) getOrCreateLblsWithQuantile(lbls labels.Labels, quantileLabel string, q float64) labels.Labels {
+ if enh.signatureToLabelsWithQuantile == nil {
+ enh.signatureToLabelsWithQuantile = make(map[string]map[float64]labels.Labels)
+ }
+
+ enh.lblBuf = lbls.Bytes(enh.lblBuf)
+ cachedLbls, ok := enh.signatureToLabelsWithQuantile[string(enh.lblBuf)]
+ if !ok {
+ cachedLbls = make(map[float64]labels.Labels, len(enh.quantileStrs))
+ enh.signatureToLabelsWithQuantile[string(enh.lblBuf)] = cachedLbls
+ }
+
+ cachedLblsWithQuantile, ok := cachedLbls[q]
+ if !ok {
+ quantileStr := "NaN"
+ if !math.IsNaN(q) {
+ // Cannot do map lookup by NaN key.
+ quantileStr = enh.quantileStrs[q]
+ }
+ cachedLblsWithQuantile = labels.NewBuilder(lbls).
+ Set(quantileLabel, quantileStr).
+ Labels()
+
+ cachedLbls[q] = cachedLblsWithQuantile
+ }
+
+ return cachedLblsWithQuantile
+}
+
// rangeEval evaluates the given expressions, and then for each step calls
// the given funcCall with the values computed for each expression at that
// step. The return value is the combination into time series of all the
// function call results.
-// The prepSeries function (if provided) can be used to prepare the helper
+// The matching (if provided) can be used to prepare the helper
// for each series, then passed to each call funcCall.
-func (ev *evaluator) rangeEval(ctx context.Context, prepSeries func(labels.Labels, *EvalSeriesHelper), funcCall func([]Vector, Matrix, [][]EvalSeriesHelper, *EvalNodeHelper) (Vector, annotations.Annotations), exprs ...parser.Expr) (Matrix, annotations.Annotations) {
+func (ev *evaluator) rangeEval(ctx context.Context, matching *parser.VectorMatching, funcCall func([]Vector, Matrix, [][]EvalSeriesHelper, *EvalNodeHelper) (Vector, annotations.Annotations), exprs ...parser.Expr) (Matrix, annotations.Annotations) {
numSteps := int((ev.endTimestamp-ev.startTimestamp)/ev.interval) + 1
matrixes := make([]Matrix, len(exprs))
origMatrixes := make([]Matrix, len(exprs))
originalNumSamples := ev.currentSamples
+ useSignatures := matching != nil
var warnings annotations.Annotations
for i, e := range exprs {
@@ -1297,18 +1389,46 @@ func (ev *evaluator) rangeEval(ctx context.Context, prepSeries func(labels.Label
bufHelpers [][]EvalSeriesHelper // Buffer updated on each step
)
- // If the series preparation function is provided, we should run it for
- // every single series in the matrix.
- if prepSeries != nil {
+ if useSignatures {
+ var (
+ // Function to compute the join signature for each series.
+ sigf func(labels.Labels) string
+ buf = make([]byte, 0, 1024)
+ names = slices.Clone(matching.MatchingLabels)
+ )
+ if matching.On {
+ slices.Sort(names)
+ sigf = func(lset labels.Labels) string {
+ return string(lset.BytesWithLabels(buf, names...))
+ }
+ } else { // "without"
+ names = append([]string{labels.MetricName}, names...)
+ slices.Sort(names)
+ sigf = func(lset labels.Labels) string {
+ return string(lset.BytesWithoutLabels(buf, names...))
+ }
+ }
+
seriesHelpers = make([][]EvalSeriesHelper, len(exprs))
bufHelpers = make([][]EvalSeriesHelper, len(exprs))
+ signatureToOrdinal := make(map[string]int)
+
for i := range exprs {
seriesHelpers[i] = make([]EvalSeriesHelper, len(matrixes[i]))
bufHelpers[i] = make([]EvalSeriesHelper, len(matrixes[i]))
for si, series := range matrixes[i] {
- prepSeries(series.Metric, &seriesHelpers[i][si])
+ strSig := sigf(series.Metric)
+
+ if sigOrd, ok := signatureToOrdinal[strSig]; ok {
+ seriesHelpers[i][si] = EvalSeriesHelper{sigOrdinal: sigOrd}
+ continue
+ }
+
+ signatureToOrdinal[strSig] = enh.numSigs
+ seriesHelpers[i][si] = EvalSeriesHelper{sigOrdinal: enh.numSigs}
+ enh.numSigs++
}
}
}
@@ -1323,12 +1443,12 @@ func (ev *evaluator) rangeEval(ctx context.Context, prepSeries func(labels.Label
for i := range exprs {
var bh []EvalSeriesHelper
var sh []EvalSeriesHelper
- if prepSeries != nil {
+ if useSignatures {
bh = bufHelpers[i][:0]
sh = seriesHelpers[i]
}
vectors[i], bh = ev.gatherVector(ts, matrixes[i], vectors[i], bh, sh)
- if prepSeries != nil {
+ if useSignatures {
bufHelpers[i] = bh
}
}
@@ -1591,7 +1711,7 @@ func (ev *evaluator) smoothSeries(series []storage.Series, offset time.Duration)
// Interpolate between prev and next.
// TODO: detect if the sample is a counter, based on __type__ or metadata.
prev, next := floats[i-1], floats[i]
- val := interpolate(prev, next, ts, false, false)
+ val := interpolate(prev, next, ts, false)
ss.Floats = append(ss.Floats, FPoint{F: val, T: ts})
case i > 0:
@@ -2115,8 +2235,8 @@ func (ev *evaluator) eval(ctx context.Context, expr parser.Expr) (parser.Value,
mat[i].Histograms[j].H = mat[i].Histograms[j].H.Copy().Mul(-1)
}
}
- if !ev.enableDelayedNameRemoval && mat.ContainsSameLabelset() {
- ev.errorf("vector cannot contain metrics with the same labelset")
+ if !ev.enableDelayedNameRemoval {
+ mat = ev.mergeSeriesWithSameLabelset(mat)
}
}
return mat, ws
@@ -2129,27 +2249,21 @@ func (ev *evaluator) eval(ctx context.Context, expr parser.Expr) (parser.Value,
return append(enh.Out, Sample{F: val}), nil
}, e.LHS, e.RHS)
case lt == parser.ValueTypeVector && rt == parser.ValueTypeVector:
- // Function to compute the join signature for each series.
- buf := make([]byte, 0, 1024)
- sigf := signatureFunc(e.VectorMatching.On, buf, e.VectorMatching.MatchingLabels...)
- initSignatures := func(series labels.Labels, h *EvalSeriesHelper) {
- h.signature = sigf(series)
- }
switch e.Op {
case parser.LAND:
- return ev.rangeEval(ctx, initSignatures, func(v []Vector, _ Matrix, sh [][]EvalSeriesHelper, enh *EvalNodeHelper) (Vector, annotations.Annotations) {
+ return ev.rangeEval(ctx, e.VectorMatching, func(v []Vector, _ Matrix, sh [][]EvalSeriesHelper, enh *EvalNodeHelper) (Vector, annotations.Annotations) {
return ev.VectorAnd(v[0], v[1], e.VectorMatching, sh[0], sh[1], enh), nil
}, e.LHS, e.RHS)
case parser.LOR:
- return ev.rangeEval(ctx, initSignatures, func(v []Vector, _ Matrix, sh [][]EvalSeriesHelper, enh *EvalNodeHelper) (Vector, annotations.Annotations) {
+ return ev.rangeEval(ctx, e.VectorMatching, func(v []Vector, _ Matrix, sh [][]EvalSeriesHelper, enh *EvalNodeHelper) (Vector, annotations.Annotations) {
return ev.VectorOr(v[0], v[1], e.VectorMatching, sh[0], sh[1], enh), nil
}, e.LHS, e.RHS)
case parser.LUNLESS:
- return ev.rangeEval(ctx, initSignatures, func(v []Vector, _ Matrix, sh [][]EvalSeriesHelper, enh *EvalNodeHelper) (Vector, annotations.Annotations) {
+ return ev.rangeEval(ctx, e.VectorMatching, func(v []Vector, _ Matrix, sh [][]EvalSeriesHelper, enh *EvalNodeHelper) (Vector, annotations.Annotations) {
return ev.VectorUnless(v[0], v[1], e.VectorMatching, sh[0], sh[1], enh), nil
}, e.LHS, e.RHS)
default:
- return ev.rangeEval(ctx, initSignatures, func(v []Vector, _ Matrix, sh [][]EvalSeriesHelper, enh *EvalNodeHelper) (Vector, annotations.Annotations) {
+ return ev.rangeEval(ctx, e.VectorMatching, func(v []Vector, _ Matrix, sh [][]EvalSeriesHelper, enh *EvalNodeHelper) (Vector, annotations.Annotations) {
vec, err := ev.VectorBinop(e.Op, v[0], v[1], e.VectorMatching, e.ReturnBool, sh[0], sh[1], enh, e.PositionRange())
return vec, handleVectorBinopError(err, e)
}, e.LHS, e.RHS)
@@ -2720,16 +2834,15 @@ func (*evaluator) VectorAnd(lhs, rhs Vector, matching *parser.VectorMatching, lh
return nil // Short-circuit: AND with nothing is nothing.
}
- // The set of signatures for the right-hand side Vector.
- rightSigs := map[string]struct{}{}
- // Add all rhs samples to a map so we can easily find matches later.
+ // Ordinals of signatures present on the right-hand side.
+ rightSigOrdinalsPresent := make([]bool, enh.numSigs)
for _, sh := range rhsh {
- rightSigs[sh.signature] = struct{}{}
+ rightSigOrdinalsPresent[sh.sigOrdinal] = true
}
for i, ls := range lhs {
// If there's a matching entry in the right-hand side Vector, add the sample.
- if _, ok := rightSigs[lhsh[i].signature]; ok {
+ if rightSigOrdinalsPresent[lhsh[i].sigOrdinal] {
enh.Out = append(enh.Out, ls)
}
}
@@ -2748,15 +2861,15 @@ func (*evaluator) VectorOr(lhs, rhs Vector, matching *parser.VectorMatching, lhs
return enh.Out
}
- leftSigs := map[string]struct{}{}
+ leftSigOrdinalsPresent := make([]bool, enh.numSigs)
// Add everything from the left-hand-side Vector.
for i, ls := range lhs {
- leftSigs[lhsh[i].signature] = struct{}{}
+ leftSigOrdinalsPresent[lhsh[i].sigOrdinal] = true
enh.Out = append(enh.Out, ls)
}
// Add all right-hand side elements which have not been added from the left-hand side.
for j, rs := range rhs {
- if _, ok := leftSigs[rhsh[j].signature]; !ok {
+ if !leftSigOrdinalsPresent[rhsh[j].sigOrdinal] {
enh.Out = append(enh.Out, rs)
}
}
@@ -2774,13 +2887,14 @@ func (*evaluator) VectorUnless(lhs, rhs Vector, matching *parser.VectorMatching,
return enh.Out
}
- rightSigs := map[string]struct{}{}
+ // Ordinals of signatures present on the right-hand side.
+ rightSigOrdinalsPresent := make([]bool, enh.numSigs)
for _, sh := range rhsh {
- rightSigs[sh.signature] = struct{}{}
+ rightSigOrdinalsPresent[sh.sigOrdinal] = true
}
for i, ls := range lhs {
- if _, ok := rightSigs[lhsh[i].signature]; !ok {
+ if !rightSigOrdinalsPresent[lhsh[i].sigOrdinal] {
enh.Out = append(enh.Out, ls)
}
}
@@ -2792,7 +2906,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.
}
@@ -2804,22 +2919,20 @@ func (ev *evaluator) VectorBinop(op parser.ItemType, lhs, rhs Vector, matching *
lhsh, rhsh = rhsh, lhsh
}
- // All samples from the rhs hashed by the matching label/values.
+ // All samples from the rhs by their join signature ordinal.
if enh.rightSigs == nil {
- enh.rightSigs = make(map[string]Sample, len(enh.Out))
+ enh.rightSigs = make(map[int]Sample, len(enh.Out))
} else {
- for k := range enh.rightSigs {
- delete(enh.rightSigs, k)
- }
+ clear(enh.rightSigs)
}
rightSigs := enh.rightSigs
// Add all rhs samples to a map so we can easily find matches later.
for i, rs := range rhs {
- sig := rhsh[i].signature
+ sigOrd := rhsh[i].sigOrdinal
// The rhs is guaranteed to be the 'one' side. Having multiple samples
// with the same signature means that the matching is many-to-many.
- if duplSample, found := rightSigs[sig]; found {
+ if duplSample, found := rightSigs[sigOrd]; found {
// oneSide represents which side of the vector represents the 'one' in the many-to-one relationship.
oneSide := "right"
if matching.Card == parser.CardOneToMany {
@@ -2830,31 +2943,21 @@ func (ev *evaluator) VectorBinop(op parser.ItemType, lhs, rhs Vector, matching *
ev.errorf("found duplicate series for the match group %s on the %s hand-side of the operation: [%s, %s]"+
";many-to-many matching not allowed: matching labels must be unique on one side", matchedLabels.String(), oneSide, rs.Metric.String(), duplSample.Metric.String())
}
- rightSigs[sig] = rs
+ rightSigs[sigOrd] = rs
}
- // Tracks the match-signature. For one-to-one operations the value is nil. For many-to-one
- // the value is a set of signatures to detect duplicated result elements.
+ // Tracks the matching by signature ordinals. For one-to-one operations the value is nil.
+ // For many-to-one the value is a set of hashes to detect duplicated result elements.
if enh.matchedSigs == nil {
- enh.matchedSigs = make(map[string]map[uint64]struct{}, len(rightSigs))
+ enh.matchedSigs = make(map[int]map[uint64]struct{}, len(rightSigs))
} else {
- for k := range enh.matchedSigs {
- delete(enh.matchedSigs, k)
- }
+ clear(enh.matchedSigs)
}
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 {
- sig := lhsh[i].signature
-
- rs, found := rightSigs[sig] // 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
@@ -2865,32 +2968,30 @@ 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
}
- switch {
- case returnBool:
+ if returnBool {
histogramValue = nil
if keep {
floatValue = 1.0
} else {
floatValue = 0.0
}
- case !keep:
- continue
}
+
metric := resultMetric(ls.Metric, rs.Metric, op, matching, enh)
if !ev.enableDelayedNameRemoval && returnBool {
metric = metric.DropReserved(schema.IsMetadataLabel)
}
- insertedSigs, exists := matchedSigs[sig]
+ insertedSigs, exists := matchedSigs[sigOrd]
if matching.Card == parser.CardOneToOne {
if exists {
ev.errorf("multiple matches for labels: many-to-one matching must be explicit (group_left/group_right)")
}
- matchedSigs[sig] = nil // Set existence to true.
+ matchedSigs[sigOrd] = nil // Set existence to true.
} else {
// In many-to-one matching the grouping labels have to ensure a unique metric
// for the result Vector. Check whether those labels have already been added for
@@ -2899,13 +3000,17 @@ func (ev *evaluator) VectorBinop(op parser.ItemType, lhs, rhs Vector, matching *
if !exists {
insertedSigs = map[uint64]struct{}{}
- matchedSigs[sig] = insertedSigs
+ matchedSigs[sigOrd] = insertedSigs
} else if _, duplicate := insertedSigs[insertSig]; duplicate {
ev.errorf("multiple matches for labels: grouping labels must ensure unique matches")
}
insertedSigs[insertSig] = struct{}{}
}
+ if !keep && !returnBool {
+ return
+ }
+
enh.Out = append(enh.Out, Sample{
Metric: metric,
F: floatValue,
@@ -2913,21 +3018,44 @@ func (ev *evaluator) VectorBinop(op parser.ItemType, lhs, rhs Vector, matching *
DropName: returnBool,
})
}
- return enh.Out, lastErr
-}
-func signatureFunc(on bool, b []byte, names ...string) func(labels.Labels) string {
- if on {
- slices.Sort(names)
- return func(lset labels.Labels) string {
- return string(lset.BytesWithLabels(b, names...))
+ // 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)
}
}
- names = append([]string{labels.MetricName}, names...)
- slices.Sort(names)
- return func(lset labels.Labels) string {
- return string(lset.BytesWithoutLabels(b, names...))
- }
+
+ return enh.Out, lastErr
}
// resultMetric returns the metric for the given sample(s) based on the Vector
@@ -2937,7 +3065,6 @@ func resultMetric(lhs, rhs labels.Labels, op parser.ItemType, matching *parser.V
enh.resultMetric = make(map[string]labels.Labels, len(enh.Out))
}
- enh.resetBuilder(lhs)
buf := bytes.NewBuffer(enh.lblResultBuf[:0])
enh.lblBuf = lhs.Bytes(enh.lblBuf)
buf.Write(enh.lblBuf)
@@ -2950,6 +3077,7 @@ func resultMetric(lhs, rhs labels.Labels, op parser.ItemType, matching *parser.V
}
str := string(enh.lblResultBuf)
+ enh.resetBuilder(lhs)
if changesMetricSchema(op) {
// Setting empty Metadata causes the deletion of those if they exists.
schema.Metadata{}.SetToLabels(enh.lb)
@@ -3058,7 +3186,6 @@ func scalarBinop(op parser.ItemType, lhs, rhs float64) float64 {
// vectorElemBinop evaluates a binary operation between two Vector elements.
func vectorElemBinop(op parser.ItemType, lhs, rhs float64, hlhs, hrhs *histogram.FloatHistogram, pos posrange.PositionRange) (res float64, resH *histogram.FloatHistogram, keep bool, info, err error) {
- opName := parser.ItemTypeStr[op]
switch {
case hlhs == nil && hrhs == nil:
{
@@ -3097,7 +3224,7 @@ func vectorElemBinop(op parser.ItemType, lhs, rhs float64, hlhs, hrhs *histogram
case parser.MUL:
return 0, hrhs.Copy().Mul(lhs).Compact(0), true, nil, nil
case parser.ADD, parser.SUB, parser.DIV, parser.POW, parser.MOD, parser.EQLC, parser.NEQ, parser.GTR, parser.LSS, parser.GTE, parser.LTE, parser.ATAN2:
- return 0, nil, false, nil, annotations.NewIncompatibleTypesInBinOpInfo("float", opName, "histogram", pos)
+ return 0, nil, false, nil, annotations.NewIncompatibleTypesInBinOpInfo("float", parser.ItemTypeStr[op], "histogram", pos)
}
}
case hlhs != nil && hrhs == nil:
@@ -3108,7 +3235,7 @@ func vectorElemBinop(op parser.ItemType, lhs, rhs float64, hlhs, hrhs *histogram
case parser.DIV:
return 0, hlhs.Copy().Div(rhs).Compact(0), true, nil, nil
case parser.ADD, parser.SUB, parser.POW, parser.MOD, parser.EQLC, parser.NEQ, parser.GTR, parser.LSS, parser.GTE, parser.LTE, parser.ATAN2:
- return 0, nil, false, nil, annotations.NewIncompatibleTypesInBinOpInfo("histogram", opName, "float", pos)
+ return 0, nil, false, nil, annotations.NewIncompatibleTypesInBinOpInfo("histogram", parser.ItemTypeStr[op], "float", pos)
}
}
case hlhs != nil && hrhs != nil:
@@ -3148,7 +3275,7 @@ func vectorElemBinop(op parser.ItemType, lhs, rhs float64, hlhs, hrhs *histogram
// This operation expects that both histograms are compacted.
return 0, hlhs, !hlhs.Equals(hrhs), nil, nil
case parser.MUL, parser.DIV, parser.POW, parser.MOD, parser.GTR, parser.LSS, parser.GTE, parser.LTE, parser.ATAN2:
- return 0, nil, false, nil, annotations.NewIncompatibleTypesInBinOpInfo("histogram", opName, "histogram", pos)
+ return 0, nil, false, nil, annotations.NewIncompatibleTypesInBinOpInfo("histogram", parser.ItemTypeStr[op], "histogram", pos)
}
}
}
@@ -3156,22 +3283,26 @@ func vectorElemBinop(op parser.ItemType, lhs, rhs float64, hlhs, hrhs *histogram
}
type groupedAggregation struct {
- floatValue float64
- histogramValue *histogram.FloatHistogram
- floatMean float64
- floatKahanC float64 // "Compensating value" for Kahan summation.
- groupCount float64
- heap vectorByValueHeap
+ floatValue float64
+ floatMean float64
+ floatKahanC float64 // Compensation float for Kahan summation.
+ histogramValue *histogram.FloatHistogram
+ histogramMean *histogram.FloatHistogram
+ histogramKahanC *histogram.FloatHistogram // Compensation histogram for Kahan summation.
+ groupCount float64
+ heap vectorByValueHeap
// All bools together for better packing within the struct.
- seen bool // Was this output groups seen in the input at this timestamp.
- hasFloat bool // Has at least 1 float64 sample aggregated.
- hasHistogram bool // Has at least 1 histogram sample aggregated.
- incompatibleHistograms bool // If true, group has seen mixed exponential and custom buckets.
- groupAggrComplete bool // Used by LIMITK to short-cut series loop when we've reached K elem on every group.
- incrementalMean bool // True after reverting to incremental calculation of the mean value.
- counterResetSeen bool // Counter reset hint CounterReset seen. Currently only used for histogram samples.
- notCounterResetSeen bool // Counter reset hint NotCounterReset seen. Currently only used for histogram samples.
+ seen bool // Was this output groups seen in the input at this timestamp.
+ hasFloat bool // Has at least 1 float64 sample aggregated.
+ hasHistogram bool // Has at least 1 histogram sample aggregated.
+ incompatibleHistograms bool // If true, group has seen mixed exponential and custom buckets.
+ groupAggrComplete bool // Used by LIMITK to short-cut series loop when we've reached K elem on every group.
+ floatIncrementalMean bool // True after reverting to incremental calculation for float-based mean value.
+ histogramIncrementalMean bool // True after reverting to incremental calculation for histogram-based mean value.
+ counterResetSeen bool // Counter reset hint CounterReset seen. Currently only used for histogram samples.
+ notCounterResetSeen bool // Counter reset hint NotCounterReset seen. Currently only used for histogram samples.
+ dropName bool // True if any sample in this group has DropName set.
}
// aggregation evaluates sum, avg, count, stdvar, stddev or quantile at one timestep on inputMatrix.
@@ -3200,6 +3331,7 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, q float64, inputMatrix
floatMean: f,
incompatibleHistograms: false,
groupCount: 1,
+ dropName: inputMatrix[si].DropName,
}
switch op {
case parser.AVG, parser.SUM:
@@ -3256,6 +3388,15 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, q float64, inputMatrix
continue
}
+ if inputMatrix[si].DropName {
+ group.dropName = true
+ }
+
+ var (
+ nhcbBoundsReconciled bool
+ err error
+ )
+
switch op {
case parser.SUM:
if h != nil {
@@ -3267,7 +3408,7 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, q float64, inputMatrix
case histogram.NotCounterReset:
group.notCounterResetSeen = true
}
- _, _, nhcbBoundsReconciled, err := group.histogramValue.Add(h)
+ group.histogramKahanC, _, nhcbBoundsReconciled, err = group.histogramValue.KahanAdd(h, group.histogramKahanC)
if err != nil {
handleAggregationError(err, e, inputMatrix[si].Metric.Get(model.MetricNameLabel), &annos)
group.incompatibleHistograms = true
@@ -3281,18 +3422,13 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, q float64, inputMatrix
// point in copying the histogram in that case.
} else {
group.hasFloat = true
- group.floatValue, group.floatKahanC = kahanSumInc(f, group.floatValue, group.floatKahanC)
+ group.floatValue, group.floatKahanC = kahansum.Inc(f, group.floatValue, group.floatKahanC)
}
case parser.AVG:
- // For the average calculation of histograms, we use
- // incremental mean calculation without the help of
- // Kahan summation (but this should change, see
- // https://github.com/prometheus/prometheus/issues/14105
- // ). For floats, we improve the accuracy with the help
- // of Kahan summation. For a while, we assumed that
- // incremental mean calculation combined with Kahan
- // summation (see
+ // We improve the accuracy with the help of Kahan summation.
+ // For a while, we assumed that incremental mean calculation
+ // combined with Kahan summation (see
// https://stackoverflow.com/questions/61665473/is-it-beneficial-for-precision-to-calculate-the-incremental-mean-average
// for inspiration) is generally the preferred solution.
// However, it then turned out that direct mean
@@ -3327,20 +3463,37 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, q float64, inputMatrix
case histogram.NotCounterReset:
group.notCounterResetSeen = true
}
- left := h.Copy().Div(group.groupCount)
- right := group.histogramValue.Copy().Div(group.groupCount)
-
- toAdd, _, nhcbBoundsReconciled, err := left.Sub(right)
- if err != nil {
- handleAggregationError(err, e, inputMatrix[si].Metric.Get(model.MetricNameLabel), &annos)
- group.incompatibleHistograms = true
- continue
+ if !group.histogramIncrementalMean {
+ v := group.histogramValue.Copy()
+ var c *histogram.FloatHistogram
+ if group.histogramKahanC != nil {
+ c = group.histogramKahanC.Copy()
+ }
+ c, _, nhcbBoundsReconciled, err = v.KahanAdd(h, c)
+ if err != nil {
+ handleAggregationError(err, e, inputMatrix[si].Metric.Get(model.MetricNameLabel), &annos)
+ group.incompatibleHistograms = true
+ continue
+ }
+ if nhcbBoundsReconciled {
+ annos.Add(annotations.NewMismatchedCustomBucketsHistogramsInfo(e.Expr.PositionRange(), annotations.HistogramAgg))
+ }
+ if !v.HasOverflow() {
+ group.histogramValue, group.histogramKahanC = v, c
+ break
+ }
+ group.histogramIncrementalMean = true
+ group.histogramMean = group.histogramValue.Copy().Div(group.groupCount - 1)
+ if group.histogramKahanC != nil {
+ group.histogramKahanC.Div(group.groupCount - 1)
+ }
}
- if nhcbBoundsReconciled {
- annos.Add(annotations.NewMismatchedCustomBucketsHistogramsInfo(e.Expr.PositionRange(), annotations.HistogramAgg))
+ q := (group.groupCount - 1) / group.groupCount
+ if group.histogramKahanC != nil {
+ group.histogramKahanC.Mul(q)
}
-
- _, _, nhcbBoundsReconciled, err = group.histogramValue.Add(toAdd)
+ toAdd := h.Copy().Div(group.groupCount)
+ group.histogramKahanC, _, nhcbBoundsReconciled, err = group.histogramMean.Mul(q).KahanAdd(toAdd, group.histogramKahanC)
if err != nil {
handleAggregationError(err, e, inputMatrix[si].Metric.Get(model.MetricNameLabel), &annos)
group.incompatibleHistograms = true
@@ -3355,8 +3508,8 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, q float64, inputMatrix
// point in copying the histogram in that case.
} else {
group.hasFloat = true
- if !group.incrementalMean {
- newV, newC := kahanSumInc(f, group.floatValue, group.floatKahanC)
+ if !group.floatIncrementalMean {
+ newV, newC := kahansum.Inc(f, group.floatValue, group.floatKahanC)
if !math.IsInf(newV, 0) {
// The sum doesn't overflow, so we propagate it to the
// group struct and continue with the regular
@@ -3367,12 +3520,12 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, q float64, inputMatrix
// If we are here, we know that the sum _would_ overflow. So
// instead of continue to sum up, we revert to incremental
// calculation of the mean value from here on.
- group.incrementalMean = true
+ group.floatIncrementalMean = true
group.floatMean = group.floatValue / (group.groupCount - 1)
group.floatKahanC /= group.groupCount - 1
}
q := (group.groupCount - 1) / group.groupCount
- group.floatMean, group.floatKahanC = kahanSumInc(
+ group.floatMean, group.floatKahanC = kahansum.Inc(
f/group.groupCount,
q*group.floatMean,
q*group.floatKahanC,
@@ -3447,8 +3600,24 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, q float64, inputMatrix
case aggr.incompatibleHistograms:
continue
case aggr.hasHistogram:
+ if aggr.histogramIncrementalMean {
+ if aggr.histogramKahanC != nil {
+ aggr.histogramValue, _, _, _ = aggr.histogramMean.Add(aggr.histogramKahanC)
+ // Add can theoretically return ErrHistogramsIncompatibleSchema, but at
+ // this stage errors should not occur if earlier KahanAdd calls succeeded.
+ } else {
+ aggr.histogramValue = aggr.histogramMean
+ }
+ } else {
+ aggr.histogramValue.Div(aggr.groupCount)
+ if aggr.histogramKahanC != nil {
+ aggr.histogramValue, _, _, _ = aggr.histogramValue.Add(aggr.histogramKahanC.Div(aggr.groupCount))
+ // Add can theoretically return ErrHistogramsIncompatibleSchema, but at
+ // this stage errors should not occur if earlier KahanAdd calls succeeded.
+ }
+ }
aggr.histogramValue = aggr.histogramValue.Compact(0)
- case aggr.incrementalMean:
+ case aggr.floatIncrementalMean:
aggr.floatValue = aggr.floatMean + aggr.floatKahanC
default:
aggr.floatValue = aggr.floatValue/aggr.groupCount + aggr.floatKahanC/aggr.groupCount
@@ -3476,6 +3645,11 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, q float64, inputMatrix
case aggr.incompatibleHistograms:
continue
case aggr.hasHistogram:
+ if aggr.histogramKahanC != nil {
+ aggr.histogramValue, _, _, _ = aggr.histogramValue.Add(aggr.histogramKahanC)
+ // Add can theoretically return ErrHistogramsIncompatibleSchema, but at
+ // this stage errors should not occur if earlier KahanAdd calls succeeded.
+ }
aggr.histogramValue.Compact(0)
default:
aggr.floatValue += aggr.floatKahanC
@@ -3494,7 +3668,7 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, q float64, inputMatrix
ss := &outputMatrix[ri]
addToSeries(ss, enh.Ts, aggr.floatValue, aggr.histogramValue, numSteps)
- ss.DropName = inputMatrix[ri].DropName
+ ss.DropName = aggr.dropName
}
return annos
@@ -3773,7 +3947,7 @@ func (*evaluator) aggregationCountValues(e *parser.AggregateExpr, grouping []str
return enh.Out, nil
}
-func (ev *evaluator) cleanupMetricLabels(v parser.Value) {
+func (ev *evaluator) cleanupMetricLabels(v parser.Value) parser.Value {
if v.Type() == parser.ValueTypeMatrix {
mat := v.(Matrix)
for i := range mat {
@@ -3781,9 +3955,7 @@ func (ev *evaluator) cleanupMetricLabels(v parser.Value) {
mat[i].Metric = mat[i].Metric.DropReserved(schema.IsMetadataLabel)
}
}
- if mat.ContainsSameLabelset() {
- ev.errorf("vector cannot contain metrics with the same labelset")
- }
+ return ev.mergeSeriesWithSameLabelset(mat)
} else if v.Type() == parser.ValueTypeVector {
vec := v.(Vector)
for i := range vec {
@@ -3794,7 +3966,75 @@ func (ev *evaluator) cleanupMetricLabels(v parser.Value) {
if vec.ContainsSameLabelset() {
ev.errorf("vector cannot contain metrics with the same labelset")
}
+ return vec
}
+ return v
+}
+
+// mergeSeriesWithSameLabelset merges series in a matrix that have the same labelset
+// after __name__ label removal. This happens when delayed name removal is enabled and
+// operations like OR combine series that originally had different names but end up
+// with the same labelset after dropping the name. If series with the same labelset
+// have overlapping timestamps, an error is returned.
+func (ev *evaluator) mergeSeriesWithSameLabelset(mat Matrix) Matrix {
+ if len(mat) <= 1 {
+ return mat
+ }
+
+ // Fast path: check if there are any duplicate labelsets without allocating.
+ // This is the common case and we want to avoid allocations.
+ if !mat.ContainsSameLabelset() {
+ return mat
+ }
+
+ // Slow path: there are duplicates, so we need to merge series with non-overlapping timestamps.
+ // Group series by their labelset hash.
+ seriesByHash := make(map[uint64][]int)
+ for i := range mat {
+ hash := mat[i].Metric.Hash()
+ seriesByHash[hash] = append(seriesByHash[hash], i)
+ }
+
+ // Merge series with the same labelset.
+ merged := make(Matrix, 0, len(seriesByHash))
+ for _, indices := range seriesByHash {
+ if len(indices) == 1 {
+ // No collision, add as-is.
+ merged = append(merged, mat[indices[0]])
+ continue
+ }
+
+ // Multiple series with the same labelset - merge all samples.
+ base := mat[indices[0]]
+ for _, idx := range indices[1:] {
+ base.Floats = append(base.Floats, mat[idx].Floats...)
+ base.Histograms = append(base.Histograms, mat[idx].Histograms...)
+ }
+
+ // Sort merged samples by timestamp.
+ sort.Slice(base.Floats, func(i, j int) bool {
+ return base.Floats[i].T < base.Floats[j].T
+ })
+ sort.Slice(base.Histograms, func(i, j int) bool {
+ return base.Histograms[i].T < base.Histograms[j].T
+ })
+
+ // Check for duplicate timestamps in sorted samples.
+ for i := 1; i < len(base.Floats); i++ {
+ if base.Floats[i].T == base.Floats[i-1].T {
+ ev.errorf("vector cannot contain metrics with the same labelset")
+ }
+ }
+ for i := 1; i < len(base.Histograms); i++ {
+ if base.Histograms[i].T == base.Histograms[i-1].T {
+ ev.errorf("vector cannot contain metrics with the same labelset")
+ }
+ }
+
+ merged = append(merged, base)
+ }
+
+ return merged
}
func addToSeries(ss *Series, ts int64, f float64, h *histogram.FloatHistogram, numSteps int) {
@@ -3838,13 +4078,13 @@ func handleVectorBinopError(err error, e *parser.BinaryExpr) annotations.Annotat
if err == nil {
return nil
}
- op := parser.ItemTypeStr[e.Op]
- pos := e.PositionRange()
if errors.Is(err, annotations.PromQLInfo) || errors.Is(err, annotations.PromQLWarning) {
return annotations.New().Add(err)
}
// TODO(NeerajGartia21): Test the exact annotation output once the testing framework can do so.
if errors.Is(err, histogram.ErrHistogramsIncompatibleSchema) {
+ op := parser.ItemTypeStr[e.Op]
+ pos := e.PositionRange()
return annotations.New().Add(annotations.NewIncompatibleBucketLayoutInBinOpWarning(op, pos))
}
return nil
@@ -3932,7 +4172,7 @@ func unwrapStepInvariantExpr(e parser.Expr) parser.Expr {
func PreprocessExpr(expr parser.Expr, start, end time.Time, step time.Duration) (parser.Expr, error) {
detectHistogramStatsDecoding(expr)
- if err := parser.Walk(&durationVisitor{step: step}, expr, nil); err != nil {
+ if err := parser.Walk(&durationVisitor{step: step, queryRange: end.Sub(start)}, expr, nil); err != nil {
return nil, err
}
@@ -4112,7 +4352,7 @@ func detectHistogramStatsDecoding(expr parser.Expr) {
// further up (the latter wouldn't make sense,
// but no harm in detecting it).
n.SkipHistogramBuckets = true
- case "histogram_quantile", "histogram_fraction":
+ case "histogram_quantile", "histogram_quantiles", "histogram_fraction":
// If we ever see a function that needs the
// whole histogram, we will not skip the
// buckets.
@@ -4132,13 +4372,17 @@ func makeInt64Pointer(val int64) *int64 {
// RatioSampler allows unit-testing (previously: Randomizer).
type RatioSampler interface {
- // Return this sample "offset" between [0.0, 1.0]
- sampleOffset(ts int64, sample *Sample) float64
- AddRatioSample(r float64, sample *Sample) bool
+ // SampleOffset returns this sample "offset" between [0.0, 1.0].
+ SampleOffset(metric *labels.Labels) float64
+ // AddRatioSample reports whether the sampling offset for the given sample falls within the specified ratio limit.
+ AddRatioSample(ratioLimit float64, sample *Sample) bool
+ // AddRatioSampleWithOffset reports whether the given sampling offset falls within the specified ratio limit.
+ AddRatioSampleWithOffset(ratioLimit, sampleOffset float64) bool
}
// HashRatioSampler uses Hash(labels.String()) / maxUint64 as a "deterministic"
// value in [0.0, 1.0].
+// It is a utility used for limit_ratio aggregations.
type HashRatioSampler struct{}
var ratiosampler RatioSampler = NewHashRatioSampler()
@@ -4147,14 +4391,42 @@ func NewHashRatioSampler() *HashRatioSampler {
return &HashRatioSampler{}
}
-func (*HashRatioSampler) sampleOffset(_ int64, sample *Sample) float64 {
+// SampleOffset returns a deterministic sampling offset in the range [0, 1)
+// derived from the hash of the provided metric labels.
+//
+// The offset is computed by normalizing the 64-bit hash value of the label set
+// to a float64 fraction of math.MaxUint64. This ensures that metrics with the
+// same label set always produce the same offset, while different label sets
+// produce uniformly distributed offsets suitable for sampling decisions.
+func (*HashRatioSampler) SampleOffset(metric *labels.Labels) float64 {
const (
float64MaxUint64 = float64(math.MaxUint64)
)
- return float64(sample.Metric.Hash()) / float64MaxUint64
+ return float64(metric.Hash()) / float64MaxUint64
}
+// AddRatioSample returns a bool indicating if the sampling offset for the given sample is
+// within the given ratio limit.
+//
+// See SampleOffset() for further details on the sample offset.
+// See AddRatioSampleWithOffset() for further details on the ratioLimit and sampling offset comparison.
func (s *HashRatioSampler) AddRatioSample(ratioLimit float64, sample *Sample) bool {
+ sampleOffset := s.SampleOffset(&sample.Metric)
+ return s.AddRatioSampleWithOffset(ratioLimit, sampleOffset)
+}
+
+// AddRatioSampleWithOffset reports whether the given sampling offset falls within
+// the specified ratio limit.
+//
+// The ratioLimit must be in the range [-1, 1]. The sampleOffset should be derived
+// using SampleOffset().
+//
+// When ratioLimit >= 0, the function returns true if sampleOffset < ratioLimit.
+// When ratioLimit < 0, the function returns true if sampleOffset >= 1 + ratioLimit.
+//
+// Note that this method could be moved into AddRatioSample and removed from the Prometheus codebase,
+// but it is useful for downstream projects using this code as a library.
+func (*HashRatioSampler) AddRatioSampleWithOffset(ratioLimit, sampleOffset float64) bool {
// If ratioLimit >= 0: add sample if sampleOffset is lesser than ratioLimit
//
// 0.0 ratioLimit 1.0
@@ -4175,7 +4447,6 @@ func (s *HashRatioSampler) AddRatioSample(ratioLimit float64, sample *Sample) bo
// e.g.:
// sampleOffset==0.3 && ratioLimit==-0.6
// 0.3 >= 0.4 ? --> don't add sample
- sampleOffset := s.sampleOffset(sample.T, sample)
return (ratioLimit >= 0 && sampleOffset < ratioLimit) ||
(ratioLimit < 0 && sampleOffset >= (1.0+ratioLimit))
}
@@ -4262,9 +4533,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_internal_test.go b/promql/engine_internal_test.go
index 4c5d532cbc..27bf5503f4 100644
--- a/promql/engine_internal_test.go
+++ b/promql/engine_internal_test.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Prometheus Authors
+// 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
@@ -27,12 +27,14 @@ import (
"github.com/prometheus/prometheus/util/annotations"
)
+var testParser = parser.NewParser(parser.Options{})
+
func TestRecoverEvaluatorRuntime(t *testing.T) {
var output bytes.Buffer
logger := promslog.New(&promslog.Config{Writer: &output})
ev := &evaluator{logger: logger}
- expr, _ := parser.ParseExpr("sum(up)")
+ expr, _ := testParser.ParseExpr("sum(up)")
var err error
diff --git a/promql/engine_test.go b/promql/engine_test.go
index 80bb75c945..5dfffd7cc7 100644
--- a/promql/engine_test.go
+++ b/promql/engine_test.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The Prometheus Authors
+// 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
@@ -52,8 +52,6 @@ const (
)
func TestMain(m *testing.M) {
- // Enable experimental functions testing
- parser.EnableExperimentalFunctions = true
testutil.TolerantVerifyLeak(m)
}
@@ -96,11 +94,9 @@ func TestQueryConcurrency(t *testing.T) {
var wg sync.WaitGroup
for range maxConcurrency {
q := engine.NewTestQuery(f)
- wg.Add(1)
- go func() {
+ wg.Go(func() {
q.Exec(ctx)
- wg.Done()
- }()
+ })
select {
case <-processing:
// Expected.
@@ -110,11 +106,9 @@ func TestQueryConcurrency(t *testing.T) {
}
q := engine.NewTestQuery(f)
- wg.Add(1)
- go func() {
+ wg.Go(func() {
q.Exec(ctx)
- wg.Done()
- }()
+ })
select {
case <-processing:
@@ -676,7 +670,6 @@ func TestEngineEvalStmtTimestamps(t *testing.T) {
load 10s
metric 1 2
`)
- t.Cleanup(func() { storage.Close() })
cases := []struct {
Query string
@@ -789,7 +782,6 @@ load 10s
metricWith3SampleEvery10Seconds{a="3",b="2"} 1+1x100
metricWith1HistogramEvery10Seconds {{schema:1 count:5 sum:20 buckets:[1 2 1 1]}}+{{schema:1 count:10 sum:5 buckets:[1 2 3 4]}}x100
`)
- t.Cleanup(func() { storage.Close() })
cases := []struct {
Query string
@@ -1339,7 +1331,6 @@ load 10s
bigmetric{a="1"} 1+1x100
bigmetric{a="2"} 1+1x100
`)
- t.Cleanup(func() { storage.Close() })
// These test cases should be touching the limit exactly (hence no exceeding).
// Exceeding the limit will be tested by doing -1 to the MaxSamples.
@@ -1511,11 +1502,6 @@ load 10s
}
func TestExtendedRangeSelectors(t *testing.T) {
- parser.EnableExtendedRangeSelectors = true
- t.Cleanup(func() {
- parser.EnableExtendedRangeSelectors = false
- })
-
engine := newTestEngine(t)
storage := promqltest.LoadedStorage(t, `
load 10s
@@ -1523,7 +1509,6 @@ func TestExtendedRangeSelectors(t *testing.T) {
withreset 1+1x4 1+1x5
notregular 0 5 100 2 8
`)
- t.Cleanup(func() { storage.Close() })
tc := []struct {
query string
@@ -1664,6 +1649,40 @@ func TestExtendedRangeSelectors(t *testing.T) {
}
}
+// TestParserConfigIsolation ensures the engine's parser configuration is respected.
+func TestParserConfigIsolation(t *testing.T) {
+ ctx := context.Background()
+ storage := promqltest.LoadedStorage(t, `
+ load 10s
+ metric 1+1x10
+ `)
+ t.Cleanup(func() { storage.Close() })
+
+ query := "metric[10s] smoothed"
+ t.Run("engine_with_feature_disabled_rejects", func(t *testing.T) {
+ engine := promql.NewEngine(promql.EngineOpts{
+ MaxSamples: 1000, Timeout: 10 * time.Second,
+ Parser: parser.NewParser(parser.Options{EnableExtendedRangeSelectors: false}),
+ })
+ t.Cleanup(func() { _ = engine.Close() })
+ _, err := engine.NewInstantQuery(ctx, storage, nil, query, time.Unix(10, 0))
+ require.Error(t, err)
+ require.Contains(t, err.Error(), "parse")
+ })
+ t.Run("engine_with_feature_enabled_accepts", func(t *testing.T) {
+ engine := promql.NewEngine(promql.EngineOpts{
+ MaxSamples: 1000, Timeout: 10 * time.Second,
+ Parser: parser.NewParser(parser.Options{EnableExtendedRangeSelectors: true}),
+ })
+ t.Cleanup(func() { _ = engine.Close() })
+ q, err := engine.NewInstantQuery(ctx, storage, nil, query, time.Unix(10, 0))
+ require.NoError(t, err)
+ defer q.Close()
+ res := q.Exec(ctx)
+ require.NoError(t, res.Err)
+ })
+}
+
func TestAtModifier(t *testing.T) {
engine := newTestEngine(t)
storage := promqltest.LoadedStorage(t, `
@@ -1677,7 +1696,6 @@ load 10s
load 1ms
metric_ms 0+1x10000
`)
- t.Cleanup(func() { storage.Close() })
lbls1 := labels.FromStrings("__name__", "metric", "job", "1")
lbls2 := labels.FromStrings("__name__", "metric", "job", "2")
@@ -2283,7 +2301,6 @@ func TestSubquerySelector(t *testing.T) {
t.Run("", func(t *testing.T) {
engine := newTestEngine(t)
storage := promqltest.LoadedStorage(t, tst.loadString)
- t.Cleanup(func() { storage.Close() })
for _, c := range tst.cases {
t.Run(c.Query, func(t *testing.T) {
@@ -3239,7 +3256,7 @@ func TestPreprocessAndWrapWithStepInvariantExpr(t *testing.T) {
for _, test := range testCases {
t.Run(test.input, func(t *testing.T) {
- expr, err := parser.ParseExpr(test.input)
+ expr, err := testParser.ParseExpr(test.input)
require.NoError(t, err)
expr, err = promql.PreprocessExpr(expr, startTime, endTime, 0)
require.NoError(t, err)
@@ -3410,7 +3427,6 @@ metric 0 1 2
t.Run(c.name, func(t *testing.T) {
engine := promqltest.NewTestEngine(t, false, c.engineLookback, promqltest.DefaultMaxSamplesPerQuery)
storage := promqltest.LoadedStorage(t, load)
- t.Cleanup(func() { storage.Close() })
opts := promql.NewPrometheusQueryOpts(false, c.queryLookback)
qry, err := engine.NewInstantQuery(context.Background(), storage, opts, query, c.ts)
@@ -3444,7 +3460,7 @@ func TestHistogramCopyFromIteratorRegression(t *testing.T) {
histogram {{sum:4 count:4 buckets:[2 2]}} {{sum:6 count:6 buckets:[3 3]}} {{sum:1 count:1 buckets:[1]}}
`
storage := promqltest.LoadedStorage(t, load)
- t.Cleanup(func() { storage.Close() })
+
engine := promqltest.NewTestEngine(t, false, 0, promqltest.DefaultMaxSamplesPerQuery)
verify := func(t *testing.T, qry promql.Query, expected []histogram.FloatHistogram) {
@@ -3747,12 +3763,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 +3778,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 +3789,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)
@@ -3849,6 +3865,7 @@ func TestEvaluationWithDelayedNameRemovalDisabled(t *testing.T) {
MaxSamples: 10000,
Timeout: 10 * time.Second,
EnableDelayedNameRemoval: false,
+ Parser: parser.NewParser(promqltest.TestParserOpts),
}
engine := promqltest.NewTestEngineWithOpts(t, opts)
@@ -3946,6 +3963,41 @@ eval instant at 1m histogram_fraction(-Inf, 0.7071067811865475, histogram_nan)
{case="100% NaNs"} 0.0
{case="20% NaNs"} 0.4
+# Test unary negation with non-overlapping series that have different metric names.
+# After negation, the __name__ label is dropped, so series with different names
+# but same other labels should merge if they don't overlap in time.
+clear
+load 20m
+ http_requests{job="api"} 2 _
+ http_errors{job="api"} _ 4
+
+eval instant at 0 -{job="api"}
+ {job="api"} -2
+
+eval instant at 20m -{job="api"}
+ {job="api"} -4
+
+eval range from 0 to 20m step 20m -{job="api"}
+ {job="api"} -2 -4
+
+# Test unary negation failure with overlapping timestamps (same labelset at same time).
+clear
+load 1m
+ http_requests{job="api"} 1
+ http_errors{job="api"} 2
+
+eval_fail instant at 0 -{job="api"}
+
+# Test unary negation with "or" operator combining metrics with removed names.
+clear
+load 10m
+ metric_a 1 _
+ metric_b 3 4
+
+# Use "-" unary operator as a simple way to remove the metric name.
+eval range from 0 to 20m step 10m -metric_a or -metric_b
+ {} -1 -4
+
`, engine)
}
diff --git a/promql/functions.go b/promql/functions.go
index ca8cfdce15..546f94df12 100644
--- a/promql/functions.go
+++ b/promql/functions.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
@@ -33,6 +33,7 @@ import (
"github.com/prometheus/prometheus/promql/parser/posrange"
"github.com/prometheus/prometheus/schema"
"github.com/prometheus/prometheus/util/annotations"
+ "github.com/prometheus/prometheus/util/kahansum"
)
// FunctionCall is the type of a PromQL function implementation
@@ -70,7 +71,7 @@ func funcTime(_ []Vector, _ Matrix, _ parser.Expressions, enh *EvalNodeHelper) (
// it returns the interpolated value at the left boundary; otherwise, it returns the first sample's value.
func pickOrInterpolateLeft(floats []FPoint, first int, rangeStart int64, smoothed, isCounter bool) float64 {
if smoothed && floats[first].T < rangeStart {
- return interpolate(floats[first], floats[first+1], rangeStart, isCounter, true)
+ return interpolate(floats[first], floats[first+1], rangeStart, isCounter)
}
return floats[first].F
}
@@ -80,25 +81,20 @@ func pickOrInterpolateLeft(floats []FPoint, first int, rangeStart int64, smoothe
// it returns the interpolated value at the right boundary; otherwise, it returns the last sample's value.
func pickOrInterpolateRight(floats []FPoint, last int, rangeEnd int64, smoothed, isCounter bool) float64 {
if smoothed && last > 0 && floats[last].T > rangeEnd {
- return interpolate(floats[last-1], floats[last], rangeEnd, isCounter, false)
+ return interpolate(floats[last-1], floats[last], rangeEnd, isCounter)
}
return floats[last].F
}
// interpolate performs linear interpolation between two points.
-// If isCounter is true and there is a counter reset:
-// - on the left edge, it sets the value to 0.
-// - on the right edge, it adds the left value to the right value.
+// If isCounter is true and there is a counter reset, it models the counter
+// as starting from 0 (post-reset) by setting y1 to 0.
// It then calculates the interpolated value at the given timestamp.
-func interpolate(p1, p2 FPoint, t int64, isCounter, leftEdge bool) float64 {
+func interpolate(p1, p2 FPoint, t int64, isCounter bool) float64 {
y1 := p1.F
y2 := p2.F
if isCounter && y2 < y1 {
- if leftEdge {
- y1 = 0
- } else {
- y2 += y1
- }
+ y1 = 0
}
return y1 + (y2-y1)*float64(t-p1.T)/float64(p2.T-p1.T)
@@ -200,9 +196,8 @@ func extrapolatedRate(vals Matrix, args parser.Expressions, enh *EvalNodeHelper,
// We need either at least two Histograms and no Floats, or at least two
// Floats and no Histograms to calculate a rate. Otherwise, drop this
// Vector element.
- metricName := samples.Metric.Get(labels.MetricName)
if len(samples.Histograms) > 0 && len(samples.Floats) > 0 {
- return enh.Out, annos.Add(annotations.NewMixedFloatsHistogramsWarning(metricName, args[0].PositionRange()))
+ return enh.Out, annos.Add(annotations.NewMixedFloatsHistogramsWarning(getMetricName(samples.Metric), args[0].PositionRange()))
}
switch {
@@ -211,7 +206,7 @@ func extrapolatedRate(vals Matrix, args parser.Expressions, enh *EvalNodeHelper,
firstT = samples.Histograms[0].T
lastT = samples.Histograms[numSamplesMinusOne].T
var newAnnos annotations.Annotations
- resultHistogram, newAnnos = histogramRate(samples.Histograms, isCounter, metricName, args[0].PositionRange())
+ resultHistogram, newAnnos = histogramRate(samples.Histograms, isCounter, samples.Metric, args[0].PositionRange())
annos.Merge(newAnnos)
if resultHistogram == nil {
// The histograms are not compatible with each other.
@@ -305,7 +300,7 @@ func extrapolatedRate(vals Matrix, args parser.Expressions, enh *EvalNodeHelper,
// points[0] to be a histogram. It returns nil if any other Point in points is
// not a histogram, and a warning wrapped in an annotation in that case.
// Otherwise, it returns the calculated histogram and an empty annotation.
-func histogramRate(points []HPoint, isCounter bool, metricName string, pos posrange.PositionRange) (*histogram.FloatHistogram, annotations.Annotations) {
+func histogramRate(points []HPoint, isCounter bool, labels labels.Labels, pos posrange.PositionRange) (*histogram.FloatHistogram, annotations.Annotations) {
var (
prev = points[0].H
usingCustomBuckets = prev.UsesCustomBuckets()
@@ -314,14 +309,14 @@ func histogramRate(points []HPoint, isCounter bool, metricName string, pos posra
)
if last == nil {
- return nil, annos.Add(annotations.NewMixedFloatsHistogramsWarning(metricName, pos))
+ return nil, annos.Add(annotations.NewMixedFloatsHistogramsWarning(getMetricName(labels), pos))
}
// We check for gauge type histograms in the loop below, but the loop
// below does not run on the first and last point, so check the first
// and last point now.
if isCounter && (prev.CounterResetHint == histogram.GaugeType || last.CounterResetHint == histogram.GaugeType) {
- annos.Add(annotations.NewNativeHistogramNotCounterWarning(metricName, pos))
+ annos.Add(annotations.NewNativeHistogramNotCounterWarning(getMetricName(labels), pos))
}
// Null out the 1st sample if there is a counter reset between the 1st
@@ -338,7 +333,7 @@ func histogramRate(points []HPoint, isCounter bool, metricName string, pos posra
}
if last.UsesCustomBuckets() != usingCustomBuckets {
- return nil, annos.Add(annotations.NewMixedExponentialCustomHistogramsWarning(metricName, pos))
+ return nil, annos.Add(annotations.NewMixedExponentialCustomHistogramsWarning(getMetricName(labels), pos))
}
// First iteration to find out two things:
@@ -348,19 +343,19 @@ func histogramRate(points []HPoint, isCounter bool, metricName string, pos posra
for _, currPoint := range points[1 : len(points)-1] {
curr := currPoint.H
if curr == nil {
- return nil, annotations.New().Add(annotations.NewMixedFloatsHistogramsWarning(metricName, pos))
+ return nil, annotations.New().Add(annotations.NewMixedFloatsHistogramsWarning(getMetricName(labels), pos))
}
if !isCounter {
continue
}
if curr.CounterResetHint == histogram.GaugeType {
- annos.Add(annotations.NewNativeHistogramNotCounterWarning(metricName, pos))
+ annos.Add(annotations.NewNativeHistogramNotCounterWarning(getMetricName(labels), pos))
}
if curr.Schema < minSchema {
minSchema = curr.Schema
}
if curr.UsesCustomBuckets() != usingCustomBuckets {
- return nil, annotations.New().Add(annotations.NewMixedExponentialCustomHistogramsWarning(metricName, pos))
+ return nil, annotations.New().Add(annotations.NewMixedExponentialCustomHistogramsWarning(getMetricName(labels), pos))
}
}
@@ -371,7 +366,7 @@ func histogramRate(points []HPoint, isCounter bool, metricName string, pos posra
_, _, nhcbBoundsReconciled, err := h.Sub(prev)
if err != nil {
if errors.Is(err, histogram.ErrHistogramsIncompatibleSchema) {
- return nil, annotations.New().Add(annotations.NewMixedExponentialCustomHistogramsWarning(metricName, pos))
+ return nil, annotations.New().Add(annotations.NewMixedExponentialCustomHistogramsWarning(getMetricName(labels), pos))
}
}
if nhcbBoundsReconciled {
@@ -387,7 +382,7 @@ func histogramRate(points []HPoint, isCounter bool, metricName string, pos posra
_, _, nhcbBoundsReconciled, err := h.Add(prev)
if err != nil {
if errors.Is(err, histogram.ErrHistogramsIncompatibleSchema) {
- return nil, annotations.New().Add(annotations.NewMixedExponentialCustomHistogramsWarning(metricName, pos))
+ return nil, annotations.New().Add(annotations.NewMixedExponentialCustomHistogramsWarning(getMetricName(labels), pos))
}
}
if nhcbBoundsReconciled {
@@ -397,9 +392,10 @@ func histogramRate(points []HPoint, isCounter bool, metricName string, pos posra
prev = curr
}
} else if points[0].H.CounterResetHint != histogram.GaugeType || points[len(points)-1].H.CounterResetHint != histogram.GaugeType {
- annos.Add(annotations.NewNativeHistogramNotGaugeWarning(metricName, pos))
+ annos.Add(annotations.NewNativeHistogramNotGaugeWarning(getMetricName(labels), pos))
}
+ h.CounterResetHint = histogram.GaugeType
return h.Compact(0), annos
}
@@ -430,10 +426,9 @@ func funcIdelta(_ []Vector, matrixVals Matrix, args parser.Expressions, enh *Eva
func instantValue(vals Matrix, args parser.Expressions, out Vector, isRate bool) (Vector, annotations.Annotations) {
var (
- samples = vals[0]
- metricName = samples.Metric.Get(labels.MetricName)
- ss = make([]Sample, 0, 2)
- annos annotations.Annotations
+ samples = vals[0]
+ ss = make([]Sample, 0, 2)
+ annos annotations.Annotations
)
// No sense in trying to compute a rate without at least two points. Drop
@@ -499,11 +494,11 @@ func instantValue(vals Matrix, args parser.Expressions, out Vector, isRate bool)
resultSample.H = ss[1].H.Copy()
// irate should only be applied to counters.
if isRate && (ss[1].H.CounterResetHint == histogram.GaugeType || ss[0].H.CounterResetHint == histogram.GaugeType) {
- annos.Add(annotations.NewNativeHistogramNotCounterWarning(metricName, args.PositionRange()))
+ annos.Add(annotations.NewNativeHistogramNotCounterWarning(getMetricName(samples.Metric), args.PositionRange()))
}
// idelta should only be applied to gauges.
if !isRate && (ss[1].H.CounterResetHint != histogram.GaugeType || ss[0].H.CounterResetHint != histogram.GaugeType) {
- annos.Add(annotations.NewNativeHistogramNotGaugeWarning(metricName, args.PositionRange()))
+ annos.Add(annotations.NewNativeHistogramNotGaugeWarning(getMetricName(samples.Metric), args.PositionRange()))
}
if !isRate || !ss[1].H.DetectReset(ss[0].H) {
// This subtraction may deliberately include conflicting
@@ -512,7 +507,7 @@ func instantValue(vals Matrix, args parser.Expressions, out Vector, isRate bool)
// conflicting counter resets is ignored here.
_, _, nhcbBoundsReconciled, err := resultSample.H.Sub(ss[0].H)
if errors.Is(err, histogram.ErrHistogramsIncompatibleSchema) {
- return out, annos.Add(annotations.NewMixedExponentialCustomHistogramsWarning(metricName, args.PositionRange()))
+ return out, annos.Add(annotations.NewMixedExponentialCustomHistogramsWarning(getMetricName(samples.Metric), args.PositionRange()))
}
if nhcbBoundsReconciled {
annos.Add(annotations.NewMismatchedCustomBucketsHistogramsInfo(args.PositionRange(), annotations.HistogramSub))
@@ -522,7 +517,7 @@ func instantValue(vals Matrix, args parser.Expressions, out Vector, isRate bool)
resultSample.H.Compact(0)
default:
// Mix of a float and a histogram.
- return out, annos.Add(annotations.NewMixedFloatsHistogramsWarning(metricName, args.PositionRange()))
+ return out, annos.Add(annotations.NewMixedFloatsHistogramsWarning(getMetricName(samples.Metric), args.PositionRange()))
}
if isRate {
@@ -563,8 +558,10 @@ func calcTrendValue(i int, tf, s0, s1, b float64) float64 {
// trend factor increases the influence. of trends. Algorithm taken from
// https://en.wikipedia.org/wiki/Exponential_smoothing .
func funcDoubleExponentialSmoothing(vectorVals []Vector, matrixVal Matrix, args parser.Expressions, enh *EvalNodeHelper) (Vector, annotations.Annotations) {
+ if len(vectorVals) < 2 || len(vectorVals[0]) == 0 || len(vectorVals[1]) == 0 || len(matrixVal) == 0 {
+ return enh.Out, nil
+ }
samples := matrixVal[0]
- metricName := samples.Metric.Get(labels.MetricName)
// The smoothing factor argument.
sf := vectorVals[0][0].F
@@ -585,7 +582,7 @@ func funcDoubleExponentialSmoothing(vectorVals []Vector, matrixVal Matrix, args
if l < 2 {
// Annotate mix of float and histogram.
if l == 1 && len(samples.Histograms) > 0 {
- return enh.Out, annotations.New().Add(annotations.NewHistogramIgnoredInMixedRangeInfo(metricName, args[0].PositionRange()))
+ return enh.Out, annotations.New().Add(annotations.NewHistogramIgnoredInMixedRangeInfo(getMetricName(samples.Metric), args[0].PositionRange()))
}
return enh.Out, nil
}
@@ -608,7 +605,7 @@ func funcDoubleExponentialSmoothing(vectorVals []Vector, matrixVal Matrix, args
s0, s1 = s1, x+y
}
if len(samples.Histograms) > 0 {
- return append(enh.Out, Sample{F: s1}), annotations.New().Add(annotations.NewHistogramIgnoredInMixedRangeInfo(metricName, args[0].PositionRange()))
+ return append(enh.Out, Sample{F: s1}), annotations.New().Add(annotations.NewHistogramIgnoredInMixedRangeInfo(getMetricName(samples.Metric), args[0].PositionRange()))
}
return append(enh.Out, Sample{F: s1}), nil
}
@@ -778,12 +775,18 @@ func funcScalar(vectorVals []Vector, _ Matrix, _ parser.Expressions, enh *EvalNo
}
func aggrOverTime(matrixVal Matrix, enh *EvalNodeHelper, aggrFn func(Series) float64) Vector {
+ if len(matrixVal) == 0 {
+ return enh.Out
+ }
el := matrixVal[0]
return append(enh.Out, Sample{F: aggrFn(el)})
}
func aggrHistOverTime(matrixVal Matrix, enh *EvalNodeHelper, aggrFn func(Series) (*histogram.FloatHistogram, error)) (Vector, error) {
+ if len(matrixVal) == 0 {
+ return enh.Out, nil
+ }
el := matrixVal[0]
res, err := aggrFn(el)
@@ -792,15 +795,14 @@ func aggrHistOverTime(matrixVal Matrix, enh *EvalNodeHelper, aggrFn func(Series)
// === avg_over_time(Matrix parser.ValueTypeMatrix) (Vector, Annotations) ===
func funcAvgOverTime(_ []Vector, matrixVal Matrix, args parser.Expressions, enh *EvalNodeHelper) (Vector, annotations.Annotations) {
+ if len(matrixVal) == 0 {
+ return enh.Out, nil
+ }
firstSeries := matrixVal[0]
if len(firstSeries.Floats) > 0 && len(firstSeries.Histograms) > 0 {
- metricName := firstSeries.Metric.Get(labels.MetricName)
- return enh.Out, annotations.New().Add(annotations.NewMixedFloatsHistogramsWarning(metricName, args[0].PositionRange()))
+ return enh.Out, annotations.New().Add(annotations.NewMixedFloatsHistogramsWarning(getMetricName(firstSeries.Metric), args[0].PositionRange()))
}
- // For the average calculation of histograms, we use incremental mean
- // calculation without the help of Kahan summation (but this should
- // change, see https://github.com/prometheus/prometheus/issues/14105 ).
- // For floats, we improve the accuracy with the help of Kahan summation.
+ // We improve the accuracy with the help of Kahan summation.
// For a while, we assumed that incremental mean calculation combined
// with Kahan summation (see
// https://stackoverflow.com/questions/61665473/is-it-beneficial-for-precision-to-calculate-the-incremental-mean-average
@@ -843,23 +845,47 @@ func funcAvgOverTime(_ []Vector, matrixVal Matrix, args parser.Expressions, enh
}
}()
- mean := s.Histograms[0].H.Copy()
- trackCounterReset(mean)
+ var (
+ sum = s.Histograms[0].H.Copy()
+ mean, kahanC *histogram.FloatHistogram
+ count = 1.
+ incrementalMean bool
+ nhcbBoundsReconciled bool
+ err error
+ )
+ trackCounterReset(sum)
for i, h := range s.Histograms[1:] {
trackCounterReset(h.H)
- count := float64(i + 2)
- left := h.H.Copy().Div(count)
- right := mean.Copy().Div(count)
-
- toAdd, _, nhcbBoundsReconciled, err := left.Sub(right)
- if err != nil {
- return mean, err
+ count = float64(i + 2)
+ if !incrementalMean {
+ sumCopy := sum.Copy()
+ var cCopy *histogram.FloatHistogram
+ if kahanC != nil {
+ cCopy = kahanC.Copy()
+ }
+ cCopy, _, nhcbBoundsReconciled, err = sumCopy.KahanAdd(h.H, cCopy)
+ if err != nil {
+ return sumCopy.Div(count), err
+ }
+ if nhcbBoundsReconciled {
+ nhcbBoundsReconciledSeen = true
+ }
+ if !sumCopy.HasOverflow() {
+ sum, kahanC = sumCopy, cCopy
+ continue
+ }
+ incrementalMean = true
+ mean = sum.Copy().Div(count - 1)
+ if kahanC != nil {
+ kahanC.Div(count - 1)
+ }
}
- if nhcbBoundsReconciled {
- nhcbBoundsReconciledSeen = true
+ q := (count - 1) / count
+ if kahanC != nil {
+ kahanC.Mul(q)
}
-
- _, _, nhcbBoundsReconciled, err = mean.Add(toAdd)
+ toAdd := h.H.Copy().Div(count)
+ kahanC, _, nhcbBoundsReconciled, err = mean.Mul(q).KahanAdd(toAdd, kahanC)
if err != nil {
return mean, err
}
@@ -867,12 +893,22 @@ func funcAvgOverTime(_ []Vector, matrixVal Matrix, args parser.Expressions, enh
nhcbBoundsReconciledSeen = true
}
}
- return mean, nil
+ if incrementalMean {
+ if kahanC != nil {
+ _, _, _, err := mean.Add(kahanC)
+ return mean, err
+ }
+ return mean, nil
+ }
+ if kahanC != nil {
+ _, _, _, err := sum.Div(count).Add(kahanC.Div(count))
+ return sum, err
+ }
+ return sum.Div(count), nil
})
if err != nil {
- metricName := firstSeries.Metric.Get(labels.MetricName)
if errors.Is(err, histogram.ErrHistogramsIncompatibleSchema) {
- return enh.Out, annotations.New().Add(annotations.NewMixedExponentialCustomHistogramsWarning(metricName, args[0].PositionRange()))
+ return enh.Out, annotations.New().Add(annotations.NewMixedExponentialCustomHistogramsWarning(getMetricName(firstSeries.Metric), args[0].PositionRange()))
}
}
return vec, annos
@@ -887,7 +923,7 @@ func funcAvgOverTime(_ []Vector, matrixVal Matrix, args parser.Expressions, enh
for i, f := range s.Floats[1:] {
count = float64(i + 2)
if !incrementalMean {
- newSum, newC := kahanSumInc(f.F, sum, kahanC)
+ newSum, newC := kahansum.Inc(f.F, sum, kahanC)
// Perform regular mean calculation as long as
// the sum doesn't overflow.
if !math.IsInf(newSum, 0) {
@@ -901,7 +937,7 @@ func funcAvgOverTime(_ []Vector, matrixVal Matrix, args parser.Expressions, enh
kahanC /= (count - 1)
}
q := (count - 1) / count
- mean, kahanC = kahanSumInc(f.F/count, q*mean, q*kahanC)
+ mean, kahanC = kahansum.Inc(f.F/count, q*mean, q*kahanC)
}
if incrementalMean {
return mean + kahanC
@@ -919,6 +955,9 @@ func funcCountOverTime(_ []Vector, matrixVals Matrix, _ parser.Expressions, enh
// === first_over_time(Matrix parser.ValueTypeMatrix) (Vector, Notes) ===
func funcFirstOverTime(_ []Vector, matrixVal Matrix, _ parser.Expressions, enh *EvalNodeHelper) (Vector, annotations.Annotations) {
+ if len(matrixVal) == 0 {
+ return enh.Out, nil
+ }
el := matrixVal[0]
var f FPoint
@@ -947,6 +986,9 @@ func funcFirstOverTime(_ []Vector, matrixVal Matrix, _ parser.Expressions, enh *
// === last_over_time(Matrix parser.ValueTypeMatrix) (Vector, Notes) ===
func funcLastOverTime(_ []Vector, matrixVal Matrix, _ parser.Expressions, enh *EvalNodeHelper) (Vector, annotations.Annotations) {
+ if len(matrixVal) == 0 {
+ return enh.Out, nil
+ }
el := matrixVal[0]
var f FPoint
@@ -973,14 +1015,16 @@ func funcLastOverTime(_ []Vector, matrixVal Matrix, _ parser.Expressions, enh *E
// === mad_over_time(Matrix parser.ValueTypeMatrix) (Vector, Annotations) ===
func funcMadOverTime(_ []Vector, matrixVal Matrix, args parser.Expressions, enh *EvalNodeHelper) (Vector, annotations.Annotations) {
+ if len(matrixVal) == 0 {
+ return enh.Out, nil
+ }
samples := matrixVal[0]
var annos annotations.Annotations
if len(samples.Floats) == 0 {
return enh.Out, nil
}
if len(samples.Histograms) > 0 {
- metricName := samples.Metric.Get(labels.MetricName)
- annos.Add(annotations.NewHistogramIgnoredInMixedRangeInfo(metricName, args[0].PositionRange()))
+ annos.Add(annotations.NewHistogramIgnoredInMixedRangeInfo(getMetricName(samples.Metric), args[0].PositionRange()))
}
return aggrOverTime(matrixVal, enh, func(s Series) float64 {
values := make(vectorByValueHeap, 0, len(s.Floats))
@@ -998,6 +1042,9 @@ func funcMadOverTime(_ []Vector, matrixVal Matrix, args parser.Expressions, enh
// === ts_of_first_over_time(Matrix parser.ValueTypeMatrix) (Vector, Notes) ===
func funcTsOfFirstOverTime(_ []Vector, matrixVal Matrix, _ parser.Expressions, enh *EvalNodeHelper) (Vector, annotations.Annotations) {
+ if len(matrixVal) == 0 {
+ return enh.Out, nil
+ }
el := matrixVal[0]
var tf int64 = math.MaxInt64
@@ -1018,6 +1065,9 @@ func funcTsOfFirstOverTime(_ []Vector, matrixVal Matrix, _ parser.Expressions, e
// === ts_of_last_over_time(Matrix parser.ValueTypeMatrix) (Vector, Notes) ===
func funcTsOfLastOverTime(_ []Vector, matrixVal Matrix, _ parser.Expressions, enh *EvalNodeHelper) (Vector, annotations.Annotations) {
+ if len(matrixVal) == 0 {
+ return enh.Out, nil
+ }
el := matrixVal[0]
var tf int64
@@ -1052,14 +1102,16 @@ func funcTsOfMinOverTime(_ []Vector, matrixVals Matrix, args parser.Expressions,
// compareOverTime is a helper used by funcMaxOverTime and funcMinOverTime.
func compareOverTime(matrixVal Matrix, args parser.Expressions, enh *EvalNodeHelper, compareFn func(float64, float64) bool, returnTimestamp bool) (Vector, annotations.Annotations) {
+ if len(matrixVal) == 0 {
+ return enh.Out, nil
+ }
samples := matrixVal[0]
var annos annotations.Annotations
if len(samples.Floats) == 0 {
return enh.Out, nil
}
if len(samples.Histograms) > 0 {
- metricName := samples.Metric.Get(labels.MetricName)
- annos.Add(annotations.NewHistogramIgnoredInMixedRangeInfo(metricName, args[0].PositionRange()))
+ annos.Add(annotations.NewHistogramIgnoredInMixedRangeInfo(getMetricName(samples.Metric), args[0].PositionRange()))
}
return aggrOverTime(matrixVal, enh, func(s Series) float64 {
maxVal := s.Floats[0].F
@@ -1093,10 +1145,12 @@ func funcMinOverTime(_ []Vector, matrixVals Matrix, args parser.Expressions, enh
// === sum_over_time(Matrix parser.ValueTypeMatrix) (Vector, Annotations) ===
func funcSumOverTime(_ []Vector, matrixVal Matrix, args parser.Expressions, enh *EvalNodeHelper) (Vector, annotations.Annotations) {
+ if len(matrixVal) == 0 {
+ return enh.Out, nil
+ }
firstSeries := matrixVal[0]
if len(firstSeries.Floats) > 0 && len(firstSeries.Histograms) > 0 {
- metricName := firstSeries.Metric.Get(labels.MetricName)
- return enh.Out, annotations.New().Add(annotations.NewMixedFloatsHistogramsWarning(metricName, args[0].PositionRange()))
+ return enh.Out, annotations.New().Add(annotations.NewMixedFloatsHistogramsWarning(getMetricName(firstSeries.Metric), args[0].PositionRange()))
}
if len(firstSeries.Floats) == 0 {
// The passed values only contain histograms.
@@ -1124,9 +1178,14 @@ func funcSumOverTime(_ []Vector, matrixVal Matrix, args parser.Expressions, enh
sum := s.Histograms[0].H.Copy()
trackCounterReset(sum)
+ var (
+ comp *histogram.FloatHistogram
+ nhcbBoundsReconciled bool
+ err error
+ )
for _, h := range s.Histograms[1:] {
trackCounterReset(h.H)
- _, _, nhcbBoundsReconciled, err := sum.Add(h.H)
+ comp, _, nhcbBoundsReconciled, err = sum.KahanAdd(h.H, comp)
if err != nil {
return sum, err
}
@@ -1134,12 +1193,20 @@ func funcSumOverTime(_ []Vector, matrixVal Matrix, args parser.Expressions, enh
nhcbBoundsReconciledSeen = true
}
}
- return sum, nil
+ if comp != nil {
+ sum, _, nhcbBoundsReconciled, err = sum.Add(comp)
+ if err != nil {
+ return sum, err
+ }
+ if nhcbBoundsReconciled {
+ nhcbBoundsReconciledSeen = true
+ }
+ }
+ return sum, err
})
if err != nil {
- metricName := firstSeries.Metric.Get(labels.MetricName)
if errors.Is(err, histogram.ErrHistogramsIncompatibleSchema) {
- return enh.Out, annotations.New().Add(annotations.NewMixedExponentialCustomHistogramsWarning(metricName, args[0].PositionRange()))
+ return enh.Out, annotations.New().Add(annotations.NewMixedExponentialCustomHistogramsWarning(getMetricName(firstSeries.Metric), args[0].PositionRange()))
}
}
return vec, annos
@@ -1147,7 +1214,7 @@ func funcSumOverTime(_ []Vector, matrixVal Matrix, args parser.Expressions, enh
return aggrOverTime(matrixVal, enh, func(s Series) float64 {
var sum, c float64
for _, f := range s.Floats {
- sum, c = kahanSumInc(f.F, sum, c)
+ sum, c = kahansum.Inc(f.F, sum, c)
}
if math.IsInf(sum, 0) {
return sum
@@ -1158,6 +1225,9 @@ func funcSumOverTime(_ []Vector, matrixVal Matrix, args parser.Expressions, enh
// === quantile_over_time(Matrix parser.ValueTypeMatrix) (Vector, Annotations) ===
func funcQuantileOverTime(vectorVals []Vector, matrixVal Matrix, args parser.Expressions, enh *EvalNodeHelper) (Vector, annotations.Annotations) {
+ if len(vectorVals) == 0 || len(vectorVals[0]) == 0 || len(matrixVal) == 0 {
+ return enh.Out, nil
+ }
q := vectorVals[0][0].F
el := matrixVal[0]
if len(el.Floats) == 0 {
@@ -1169,8 +1239,7 @@ func funcQuantileOverTime(vectorVals []Vector, matrixVal Matrix, args parser.Exp
annos.Add(annotations.NewInvalidQuantileWarning(q, args[0].PositionRange()))
}
if len(el.Histograms) > 0 {
- metricName := el.Metric.Get(labels.MetricName)
- annos.Add(annotations.NewHistogramIgnoredInMixedRangeInfo(metricName, args[0].PositionRange()))
+ annos.Add(annotations.NewHistogramIgnoredInMixedRangeInfo(getMetricName(el.Metric), args[0].PositionRange()))
}
values := make(vectorByValueHeap, 0, len(el.Floats))
for _, f := range el.Floats {
@@ -1180,14 +1249,16 @@ func funcQuantileOverTime(vectorVals []Vector, matrixVal Matrix, args parser.Exp
}
func varianceOverTime(matrixVal Matrix, args parser.Expressions, enh *EvalNodeHelper, varianceToResult func(float64) float64) (Vector, annotations.Annotations) {
+ if len(matrixVal) == 0 {
+ return enh.Out, nil
+ }
samples := matrixVal[0]
var annos annotations.Annotations
if len(samples.Floats) == 0 {
return enh.Out, nil
}
if len(samples.Histograms) > 0 {
- metricName := samples.Metric.Get(labels.MetricName)
- annos.Add(annotations.NewHistogramIgnoredInMixedRangeInfo(metricName, args[0].PositionRange()))
+ annos.Add(annotations.NewHistogramIgnoredInMixedRangeInfo(getMetricName(samples.Metric), args[0].PositionRange()))
}
return aggrOverTime(matrixVal, enh, func(s Series) float64 {
var count float64
@@ -1196,8 +1267,8 @@ func varianceOverTime(matrixVal Matrix, args parser.Expressions, enh *EvalNodeHe
for _, f := range s.Floats {
count++
delta := f.F - (mean + cMean)
- mean, cMean = kahanSumInc(delta/count, mean, cMean)
- aux, cAux = kahanSumInc(delta*(f.F-(mean+cMean)), aux, cAux)
+ mean, cMean = kahansum.Inc(delta/count, mean, cMean)
+ aux, cAux = kahansum.Inc(delta*(f.F-(mean+cMean)), aux, cAux)
}
variance := (aux + cAux) / count
if varianceToResult == nil {
@@ -1410,24 +1481,6 @@ func funcTimestamp(vectorVals []Vector, _ Matrix, _ parser.Expressions, enh *Eva
return enh.Out, nil
}
-// We get incorrect results if this function is inlined; see https://github.com/prometheus/prometheus/issues/16714.
-//
-//go:noinline
-func kahanSumInc(inc, sum, c float64) (newSum, newC float64) {
- t := sum + inc
- switch {
- case math.IsInf(t, 0):
- c = 0
-
- // Using Neumaier improvement, swap if next term larger than sum.
- case math.Abs(sum) >= math.Abs(inc):
- c += (sum - t) + inc
- default:
- c += (inc - t) + sum
- }
- return t, c
-}
-
// linearRegression performs a least-square linear regression analysis on the
// provided SamplePairs. It returns the slope, and the intercept value at the
// provided time.
@@ -1450,10 +1503,10 @@ func linearRegression(samples []FPoint, interceptTime int64) (slope, intercept f
}
n += 1.0
x := float64(sample.T-interceptTime) / 1e3
- sumX, cX = kahanSumInc(x, sumX, cX)
- sumY, cY = kahanSumInc(sample.F, sumY, cY)
- sumXY, cXY = kahanSumInc(x*sample.F, sumXY, cXY)
- sumX2, cX2 = kahanSumInc(x*x, sumX2, cX2)
+ sumX, cX = kahansum.Inc(x, sumX, cX)
+ sumY, cY = kahansum.Inc(sample.F, sumY, cY)
+ sumXY, cXY = kahansum.Inc(x*sample.F, sumXY, cXY)
+ sumX2, cX2 = kahansum.Inc(x*x, sumX2, cX2)
}
if constY {
if math.IsInf(initY, 0) {
@@ -1476,15 +1529,17 @@ func linearRegression(samples []FPoint, interceptTime int64) (slope, intercept f
// === deriv(node parser.ValueTypeMatrix) (Vector, Annotations) ===
func funcDeriv(_ []Vector, matrixVal Matrix, args parser.Expressions, enh *EvalNodeHelper) (Vector, annotations.Annotations) {
+ if len(matrixVal) == 0 {
+ return enh.Out, nil
+ }
samples := matrixVal[0]
- metricName := samples.Metric.Get(labels.MetricName)
// No sense in trying to compute a derivative without at least two float points.
// Drop this Vector element.
if len(samples.Floats) < 2 {
// Annotate mix of float and histogram.
if len(samples.Floats) == 1 && len(samples.Histograms) > 0 {
- return enh.Out, annotations.New().Add(annotations.NewHistogramIgnoredInMixedRangeInfo(metricName, args[0].PositionRange()))
+ return enh.Out, annotations.New().Add(annotations.NewHistogramIgnoredInMixedRangeInfo(getMetricName(samples.Metric), args[0].PositionRange()))
}
return enh.Out, nil
}
@@ -1494,30 +1549,32 @@ func funcDeriv(_ []Vector, matrixVal Matrix, args parser.Expressions, enh *EvalN
// https://github.com/prometheus/prometheus/issues/2674
slope, _ := linearRegression(samples.Floats, samples.Floats[0].T)
if len(samples.Histograms) > 0 {
- return append(enh.Out, Sample{F: slope}), annotations.New().Add(annotations.NewHistogramIgnoredInMixedRangeInfo(metricName, args[0].PositionRange()))
+ return append(enh.Out, Sample{F: slope}), annotations.New().Add(annotations.NewHistogramIgnoredInMixedRangeInfo(getMetricName(samples.Metric), args[0].PositionRange()))
}
return append(enh.Out, Sample{F: slope}), nil
}
// === predict_linear(node parser.ValueTypeMatrix, k parser.ValueTypeScalar) (Vector, Annotations) ===
func funcPredictLinear(vectorVals []Vector, matrixVal Matrix, args parser.Expressions, enh *EvalNodeHelper) (Vector, annotations.Annotations) {
+ if len(vectorVals) == 0 || len(vectorVals[0]) == 0 || len(matrixVal) == 0 {
+ return enh.Out, nil
+ }
samples := matrixVal[0]
duration := vectorVals[0][0].F
- metricName := samples.Metric.Get(labels.MetricName)
// No sense in trying to predict anything without at least two float points.
// Drop this Vector element.
if len(samples.Floats) < 2 {
// Annotate mix of float and histogram.
if len(samples.Floats) == 1 && len(samples.Histograms) > 0 {
- return enh.Out, annotations.New().Add(annotations.NewHistogramIgnoredInMixedRangeInfo(metricName, args[0].PositionRange()))
+ return enh.Out, annotations.New().Add(annotations.NewHistogramIgnoredInMixedRangeInfo(getMetricName(samples.Metric), args[0].PositionRange()))
}
return enh.Out, nil
}
slope, intercept := linearRegression(samples.Floats, enh.Ts)
if len(samples.Histograms) > 0 {
- return append(enh.Out, Sample{F: slope*duration + intercept}), annotations.New().Add(annotations.NewHistogramIgnoredInMixedRangeInfo(metricName, args[0].PositionRange()))
+ return append(enh.Out, Sample{F: slope*duration + intercept}), annotations.New().Add(annotations.NewHistogramIgnoredInMixedRangeInfo(getMetricName(samples.Metric), args[0].PositionRange()))
}
return append(enh.Out, Sample{F: slope*duration + intercept}), nil
}
@@ -1585,7 +1642,7 @@ func histogramVariance(vectorVals []Vector, enh *EvalNodeHelper, varianceToResul
}
}
delta := val - mean
- variance, cVariance = kahanSumInc(bucket.Count*delta*delta, variance, cVariance)
+ variance, cVariance = kahansum.Inc(bucket.Count*delta*delta, variance, cVariance)
}
variance += cVariance
variance /= h.Count
@@ -1608,6 +1665,9 @@ func funcHistogramStdVar(vectorVals []Vector, _ Matrix, _ parser.Expressions, en
// === histogram_fraction(lower, upper parser.ValueTypeScalar, Vector parser.ValueTypeVector) (Vector, Annotations) ===
func funcHistogramFraction(vectorVals []Vector, _ Matrix, args parser.Expressions, enh *EvalNodeHelper) (Vector, annotations.Annotations) {
+ if len(vectorVals) < 3 || len(vectorVals[0]) == 0 || len(vectorVals[1]) == 0 {
+ return enh.Out, nil
+ }
lower := vectorVals[0][0].F
upper := vectorVals[1][0].F
inVec := vectorVals[2]
@@ -1623,7 +1683,7 @@ func funcHistogramFraction(vectorVals []Vector, _ Matrix, args parser.Expression
if !enh.enableDelayedNameRemoval {
sample.Metric = sample.Metric.DropReserved(schema.IsMetadataLabel)
}
- hf, hfAnnos := HistogramFraction(lower, upper, sample.H, sample.Metric.Get(model.MetricNameLabel), args[0].PositionRange())
+ hf, hfAnnos := HistogramFraction(lower, upper, sample.H, getMetricName(sample.Metric), args[0].PositionRange())
annos.Merge(hfAnnos)
enh.Out = append(enh.Out, Sample{
Metric: sample.Metric,
@@ -1653,12 +1713,15 @@ func funcHistogramFraction(vectorVals []Vector, _ Matrix, args parser.Expression
// === histogram_quantile(k parser.ValueTypeScalar, Vector parser.ValueTypeVector) (Vector, Annotations) ===
func funcHistogramQuantile(vectorVals []Vector, _ Matrix, args parser.Expressions, enh *EvalNodeHelper) (Vector, annotations.Annotations) {
+ if len(vectorVals) < 2 || len(vectorVals[0]) == 0 {
+ return enh.Out, nil
+ }
q := vectorVals[0][0].F
inVec := vectorVals[1]
var annos annotations.Annotations
- if math.IsNaN(q) || q < 0 || q > 1 {
- annos.Add(annotations.NewInvalidQuantileWarning(q, args[0].PositionRange()))
+ if err := validateQuantile(q, args[0]); err != nil {
+ annos.Add(err)
}
annos.Merge(enh.resetHistograms(inVec, args[1]))
@@ -1671,7 +1734,7 @@ func funcHistogramQuantile(vectorVals []Vector, _ Matrix, args parser.Expression
if !enh.enableDelayedNameRemoval {
sample.Metric = sample.Metric.DropReserved(schema.IsMetadataLabel)
}
- hq, hqAnnos := HistogramQuantile(q, sample.H, sample.Metric.Get(model.MetricNameLabel), args[0].PositionRange())
+ hq, hqAnnos := HistogramQuantile(q, sample.H, getMetricName(sample.Metric), args[0].PositionRange())
annos.Merge(hqAnnos)
enh.Out = append(enh.Out, Sample{
Metric: sample.Metric,
@@ -1683,13 +1746,13 @@ func funcHistogramQuantile(vectorVals []Vector, _ Matrix, args parser.Expression
// Deal with classic histograms that have already been filtered for conflicting native histograms.
for _, mb := range enh.signatureToMetricWithBuckets {
if len(mb.buckets) > 0 {
- res, forcedMonotonicity, _ := BucketQuantile(q, mb.buckets)
+ quantile, forcedMonotonicity, _, minBucket, maxBucket, maxDiff := BucketQuantile(q, mb.buckets)
if forcedMonotonicity {
+ metricName := ""
if enh.enableDelayedNameRemoval {
- annos.Add(annotations.NewHistogramQuantileForcedMonotonicityInfo(mb.metric.Get(labels.MetricName), args[1].PositionRange()))
- } else {
- annos.Add(annotations.NewHistogramQuantileForcedMonotonicityInfo("", args[1].PositionRange()))
+ metricName = getMetricName(mb.metric)
}
+ annos.Add(annotations.NewHistogramQuantileForcedMonotonicityInfo(metricName, args[1].PositionRange(), enh.Ts, minBucket, maxBucket, maxDiff))
}
if !enh.enableDelayedNameRemoval {
@@ -1698,7 +1761,7 @@ func funcHistogramQuantile(vectorVals []Vector, _ Matrix, args parser.Expression
enh.Out = append(enh.Out, Sample{
Metric: mb.metric,
- F: res,
+ F: quantile,
DropName: true,
})
}
@@ -1707,21 +1770,111 @@ func funcHistogramQuantile(vectorVals []Vector, _ Matrix, args parser.Expression
return enh.Out, annos
}
+func validateQuantile(q float64, arg parser.Expr) error {
+ if math.IsNaN(q) || q < 0 || q > 1 {
+ return annotations.NewInvalidQuantileWarning(q, arg.PositionRange())
+ }
+ return nil
+}
+
+// === histogram_quantiles(Vector parser.ValueTypeVector, label parser.ValueTypeString, q0 parser.ValueTypeScalar, qs parser.ValueTypeScalar...) (Vector, Annotations) ===
+func funcHistogramQuantiles(vectorVals []Vector, _ Matrix, args parser.Expressions, enh *EvalNodeHelper) (Vector, annotations.Annotations) {
+ var (
+ inVec = vectorVals[0]
+ quantileLabel = args[1].(*parser.StringLiteral).Val
+ numQuantiles = len(vectorVals[2:])
+ qs = make([]float64, 0, numQuantiles)
+
+ annos annotations.Annotations
+ )
+
+ if enh.quantileStrs == nil {
+ enh.quantileStrs = make(map[float64]string, numQuantiles)
+ }
+ for i := 2; i < len(vectorVals); i++ {
+ q := vectorVals[i][0].F
+
+ if err := validateQuantile(q, args[i]); err != nil {
+ annos.Add(err)
+ }
+
+ if _, ok := enh.quantileStrs[q]; !ok {
+ enh.quantileStrs[q] = labels.FormatOpenMetricsFloat(q)
+ }
+ qs = append(qs, q)
+ }
+
+ annos.Merge(enh.resetHistograms(inVec, args[0]))
+
+ for _, q := range qs {
+ // Deal with the native histograms.
+ for _, sample := range enh.nativeHistogramSamples {
+ if sample.H == nil {
+ // Native histogram conflicts with classic histogram at the same timestamp, ignore.
+ continue
+ }
+ if !enh.enableDelayedNameRemoval {
+ sample.Metric = sample.Metric.DropReserved(schema.IsMetadataLabel)
+ }
+ hq, hqAnnos := HistogramQuantile(q, sample.H, sample.Metric.Get(model.MetricNameLabel), args[0].PositionRange())
+ annos.Merge(hqAnnos)
+ enh.Out = append(enh.Out, Sample{
+ Metric: enh.getOrCreateLblsWithQuantile(sample.Metric, quantileLabel, q),
+ F: hq,
+ DropName: true,
+ })
+ }
+
+ // Deal with classic histograms that have already been filtered for conflicting native histograms.
+ for _, mb := range enh.signatureToMetricWithBuckets {
+ if len(mb.buckets) > 0 {
+ hq, forcedMonotonicity, _, minBucket, maxBucket, maxDiff := BucketQuantile(q, mb.buckets)
+ if forcedMonotonicity {
+ metricName := ""
+ if enh.enableDelayedNameRemoval {
+ metricName = getMetricName(mb.metric)
+ }
+ annos.Add(annotations.NewHistogramQuantileForcedMonotonicityInfo(metricName, args[1].PositionRange(), enh.Ts, minBucket, maxBucket, maxDiff))
+ }
+
+ if !enh.enableDelayedNameRemoval {
+ mb.metric = mb.metric.DropReserved(schema.IsMetadataLabel)
+ }
+
+ enh.Out = append(enh.Out, Sample{
+ Metric: enh.getOrCreateLblsWithQuantile(mb.metric, quantileLabel, q),
+ F: hq,
+ DropName: true,
+ })
+ }
+ }
+ }
+
+ return enh.Out, annos
+}
+
// pickFirstSampleIndex returns the index of the last sample before
// or at the range start, or 0 if none exist before the range start.
-// If the vector selector is not anchored, it always returns 0.
-func pickFirstSampleIndex(floats []FPoint, args parser.Expressions, enh *EvalNodeHelper) int {
+// If the vector selector is not anchored, it always returns 0, true.
+// The second return value is false if there are no samples in range (for anchored selectors).
+func pickFirstSampleIndex(floats []FPoint, args parser.Expressions, enh *EvalNodeHelper) (int, bool) {
ms := args[0].(*parser.MatrixSelector)
vs := ms.VectorSelector.(*parser.VectorSelector)
if !vs.Anchored {
- return 0
+ return 0, true
}
rangeStart := enh.Ts - durationMilliseconds(ms.Range+vs.Offset)
- return max(0, sort.Search(len(floats)-1, func(i int) bool { return floats[i].T > rangeStart })-1)
+ if len(floats) == 0 || floats[len(floats)-1].T <= rangeStart {
+ return 0, false
+ }
+ return max(0, sort.Search(len(floats)-1, func(i int) bool { return floats[i].T > rangeStart })-1), true
}
// === resets(Matrix parser.ValueTypeMatrix) (Vector, Annotations) ===
func funcResets(_ []Vector, matrixVal Matrix, args parser.Expressions, enh *EvalNodeHelper) (Vector, annotations.Annotations) {
+ if len(matrixVal) == 0 {
+ return enh.Out, nil
+ }
floats := matrixVal[0].Floats
histograms := matrixVal[0].Histograms
resets := 0
@@ -1730,7 +1883,10 @@ func funcResets(_ []Vector, matrixVal Matrix, args parser.Expressions, enh *Eval
}
var prevSample, curSample Sample
- firstSampleIndex := pickFirstSampleIndex(floats, args, enh)
+ firstSampleIndex, found := pickFirstSampleIndex(floats, args, enh)
+ if !found {
+ return enh.Out, nil
+ }
for iFloat, iHistogram := firstSampleIndex, 0; iFloat < len(floats) || iHistogram < len(histograms); {
switch {
// Process a float sample if no histogram sample remains or its timestamp is earlier.
@@ -1768,6 +1924,9 @@ func funcResets(_ []Vector, matrixVal Matrix, args parser.Expressions, enh *Eval
// === changes(Matrix parser.ValueTypeMatrix) (Vector, Annotations) ===
func funcChanges(_ []Vector, matrixVal Matrix, args parser.Expressions, enh *EvalNodeHelper) (Vector, annotations.Annotations) {
+ if len(matrixVal) == 0 {
+ return enh.Out, nil
+ }
floats := matrixVal[0].Floats
histograms := matrixVal[0].Histograms
changes := 0
@@ -1776,7 +1935,10 @@ func funcChanges(_ []Vector, matrixVal Matrix, args parser.Expressions, enh *Eva
}
var prevSample, curSample Sample
- firstSampleIndex := pickFirstSampleIndex(floats, args, enh)
+ firstSampleIndex, found := pickFirstSampleIndex(floats, args, enh)
+ if !found {
+ return enh.Out, nil
+ }
for iFloat, iHistogram := firstSampleIndex, 0; iFloat < len(floats) || iHistogram < len(histograms); {
switch {
// Process a float sample if no histogram sample remains or its timestamp is earlier.
@@ -1848,11 +2010,8 @@ func (ev *evaluator) evalLabelReplace(ctx context.Context, args parser.Expressio
}
}
}
- if matrix.ContainsSameLabelset() {
- ev.errorf("vector cannot contain metrics with the same labelset")
- }
- return matrix, ws
+ return ev.mergeSeriesWithSameLabelset(matrix), ws
}
// === Vector(s Scalar) (Vector, Annotations) ===
@@ -1902,11 +2061,8 @@ func (ev *evaluator) evalLabelJoin(ctx context.Context, args parser.Expressions)
matrix[i].DropName = el.DropName
}
}
- if matrix.ContainsSameLabelset() {
- ev.errorf("vector cannot contain metrics with the same labelset")
- }
- return matrix, ws
+ return ev.mergeSeriesWithSameLabelset(matrix), ws
}
// Common code for date related functions.
@@ -2027,6 +2183,7 @@ var FunctionCalls = map[string]FunctionCall{
"histogram_count": funcHistogramCount,
"histogram_fraction": funcHistogramFraction,
"histogram_quantile": funcHistogramQuantile,
+ "histogram_quantiles": funcHistogramQuantiles,
"histogram_sum": funcHistogramSum,
"histogram_stddev": funcHistogramStdDev,
"histogram_stdvar": funcHistogramStdVar,
@@ -2219,3 +2376,7 @@ func stringSliceFromArgs(args parser.Expressions) []string {
}
return tmp
}
+
+func getMetricName(metric labels.Labels) string {
+ return metric.Get(model.MetricNameLabel)
+}
diff --git a/promql/functions_internal_test.go b/promql/functions_internal_test.go
index 658eb7550d..cd170823a8 100644
--- a/promql/functions_internal_test.go
+++ b/promql/functions_internal_test.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
@@ -18,9 +18,28 @@ import (
"math"
"testing"
+ "github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
+
+ "github.com/prometheus/prometheus/model/histogram"
+ "github.com/prometheus/prometheus/model/labels"
+ "github.com/prometheus/prometheus/promql/parser/posrange"
+ "github.com/prometheus/prometheus/util/kahansum"
)
+func TestHistogramRateCounterResetHint(t *testing.T) {
+ points := []HPoint{
+ {T: 0, H: &histogram.FloatHistogram{CounterResetHint: histogram.CounterReset, Count: 5, Sum: 5}},
+ {T: 1, H: &histogram.FloatHistogram{CounterResetHint: histogram.UnknownCounterReset, Count: 10, Sum: 10}},
+ }
+ labels := labels.FromMap(map[string]string{model.MetricNameLabel: "foo"})
+ fh, _ := histogramRate(points, false, labels, posrange.PositionRange{})
+ require.Equal(t, histogram.GaugeType, fh.CounterResetHint)
+
+ fh, _ = histogramRate(points, true, labels, posrange.PositionRange{})
+ require.Equal(t, histogram.GaugeType, fh.CounterResetHint)
+}
+
func TestKahanSumInc(t *testing.T) {
testCases := map[string]struct {
first float64
@@ -61,7 +80,7 @@ func TestKahanSumInc(t *testing.T) {
runTest := func(t *testing.T, a, b, expected float64) {
t.Run(fmt.Sprintf("%v + %v = %v", a, b, expected), func(t *testing.T) {
- sum, c := kahanSumInc(b, a, 0)
+ sum, c := kahansum.Inc(b, a, 0)
result := sum + c
if math.IsNaN(expected) {
@@ -90,13 +109,13 @@ func TestInterpolate(t *testing.T) {
{FPoint{T: 1, F: 100}, FPoint{T: 2, F: 200}, 1, false, 100},
{FPoint{T: 0, F: 100}, FPoint{T: 2, F: 200}, 1, false, 150},
{FPoint{T: 0, F: 200}, FPoint{T: 2, F: 100}, 1, false, 150},
- {FPoint{T: 0, F: 200}, FPoint{T: 2, F: 0}, 1, true, 200},
- {FPoint{T: 0, F: 200}, FPoint{T: 2, F: 100}, 1, true, 250},
- {FPoint{T: 0, F: 500}, FPoint{T: 2, F: 100}, 1, true, 550},
- {FPoint{T: 0, F: 500}, FPoint{T: 10, F: 0}, 1, true, 500},
+ {FPoint{T: 0, F: 200}, FPoint{T: 2, F: 0}, 1, true, 0},
+ {FPoint{T: 0, F: 200}, FPoint{T: 2, F: 100}, 1, true, 50},
+ {FPoint{T: 0, F: 500}, FPoint{T: 2, F: 100}, 1, true, 50},
+ {FPoint{T: 0, F: 500}, FPoint{T: 10, F: 0}, 1, true, 0},
}
for _, test := range tests {
- result := interpolate(test.p1, test.p2, test.t, test.isCounter, false)
+ result := interpolate(test.p1, test.p2, test.t, test.isCounter)
require.Equal(t, test.expected, result)
}
}
diff --git a/promql/functions_test.go b/promql/functions_test.go
index 8dd91e7537..023417bfc2 100644
--- a/promql/functions_test.go
+++ b/promql/functions_test.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
@@ -33,7 +33,7 @@ func TestDeriv(t *testing.T) {
// This requires more precision than the usual test system offers,
// so we test it by hand.
storage := teststorage.New(t)
- defer storage.Close()
+
opts := promql.EngineOpts{
Logger: nil,
Reg: nil,
diff --git a/promql/fuzz.go b/promql/fuzz.go
index a71a63f8eb..3fa28abe48 100644
--- a/promql/fuzz.go
+++ b/promql/fuzz.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
@@ -60,6 +60,8 @@ const (
// Use package-scope symbol table to avoid memory allocation on every fuzzing operation.
var symbolTable = labels.NewSymbolTable()
+var fuzzParser = parser.NewParser(parser.Options{})
+
func fuzzParseMetricWithContentType(in []byte, contentType string) int {
p, warning := textparse.New(in, contentType, symbolTable, textparse.ParserOptions{})
if p == nil || warning != nil {
@@ -103,7 +105,7 @@ func FuzzParseMetricSelector(in []byte) int {
if len(in) > maxInputSize {
return fuzzMeh
}
- _, err := parser.ParseMetricSelector(string(in))
+ _, err := fuzzParser.ParseMetricSelector(string(in))
if err == nil {
return fuzzInteresting
}
@@ -116,7 +118,7 @@ func FuzzParseExpr(in []byte) int {
if len(in) > maxInputSize {
return fuzzMeh
}
- _, err := parser.ParseExpr(string(in))
+ _, err := fuzzParser.ParseExpr(string(in))
if err == nil {
return fuzzInteresting
}
diff --git a/promql/fuzz_test.go b/promql/fuzz_test.go
index 4a26798ded..a24da48e63 100644
--- a/promql/fuzz_test.go
+++ b/promql/fuzz_test.go
@@ -1,4 +1,4 @@
-// Copyright 2022 The Prometheus Authors
+// 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
diff --git a/promql/histogram_stats_iterator.go b/promql/histogram_stats_iterator.go
index e58cc7d848..87cc5acfbd 100644
--- a/promql/histogram_stats_iterator.go
+++ b/promql/histogram_stats_iterator.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Prometheus Authors
+// 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
diff --git a/promql/histogram_stats_iterator_test.go b/promql/histogram_stats_iterator_test.go
index 80bfee519d..d3a76820da 100644
--- a/promql/histogram_stats_iterator_test.go
+++ b/promql/histogram_stats_iterator_test.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
@@ -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 0067fce845..97a79cd0f1 100644
--- a/promql/info.go
+++ b/promql/info.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Prometheus Authors
+// 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
@@ -21,6 +21,7 @@ import (
"strings"
"github.com/grafana/regexp"
+ "github.com/prometheus/common/model"
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/promql/parser"
@@ -46,24 +47,20 @@ func (ev *evaluator) evalInfo(ctx context.Context, args parser.Expressions) (par
labelSelector := args[1].(*parser.VectorSelector)
for _, m := range labelSelector.LabelMatchers {
dataLabelMatchers[m.Name] = append(dataLabelMatchers[m.Name], m)
- if m.Name == labels.MetricName {
+ if m.Name == model.MetricNameLabel {
infoNameMatchers = append(infoNameMatchers, m)
}
}
} else {
- infoNameMatchers = []*labels.Matcher{labels.MustNewMatcher(labels.MatchEqual, labels.MetricName, targetInfo)}
+ infoNameMatchers = []*labels.Matcher{labels.MustNewMatcher(labels.MatchEqual, model.MetricNameLabel, targetInfo)}
}
// Don't try to enrich info series.
ignoreSeries := map[uint64]struct{}{}
-loop:
for _, s := range mat {
- name := s.Metric.Get(labels.MetricName)
- for _, m := range infoNameMatchers {
- if m.Matches(name) {
- ignoreSeries[s.Metric.Hash()] = struct{}{}
- continue loop
- }
+ name := s.Metric.Get(model.MetricNameLabel)
+ if len(infoNameMatchers) > 0 && matchersMatch(infoNameMatchers, name) {
+ ignoreSeries[s.Metric.Hash()] = struct{}{}
}
}
@@ -79,6 +76,15 @@ loop:
return res, annots
}
+func matchersMatch(matchers []*labels.Matcher, value string) bool {
+ for _, m := range matchers {
+ if !m.Matches(value) {
+ return false
+ }
+ }
+ return true
+}
+
// infoSelectHints calculates the storage.SelectHints for selecting info series, given expr (first argument to info call).
func (ev *evaluator) infoSelectHints(expr parser.Expr) storage.SelectHints {
var nodeTimestamp *int64
@@ -122,6 +128,19 @@ func (ev *evaluator) infoSelectHints(expr parser.Expr) storage.SelectHints {
// Series in ignoreSeries are not fetched.
// dataLabelMatchers may be mutated.
func (ev *evaluator) fetchInfoSeries(ctx context.Context, mat Matrix, ignoreSeries map[uint64]struct{}, dataLabelMatchers map[string][]*labels.Matcher, selectHints storage.SelectHints) (Matrix, annotations.Annotations, error) {
+ removeNameFromDataLabelMatchers := func() {
+ for name, ms := range dataLabelMatchers {
+ ms = slices.DeleteFunc(ms, func(m *labels.Matcher) bool {
+ return m.Name == model.MetricNameLabel
+ })
+ if len(ms) > 0 {
+ dataLabelMatchers[name] = ms
+ } else {
+ delete(dataLabelMatchers, name)
+ }
+ }
+ }
+
// A map of values for all identifying labels we are interested in.
idLblValues := map[string]map[string]struct{}{}
for _, s := range mat {
@@ -143,6 +162,11 @@ 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.
+ removeNameFromDataLabelMatchers()
return nil, nil, nil
}
@@ -166,24 +190,19 @@ func (ev *evaluator) fetchInfoSeries(ctx context.Context, mat Matrix, ignoreSeri
for name, re := range idLblRegexps {
infoLabelMatchers = append(infoLabelMatchers, labels.MustNewMatcher(labels.MatchRegexp, name, re))
}
- var nameMatcher *labels.Matcher
- for name, ms := range dataLabelMatchers {
- for i, m := range ms {
- if m.Name == labels.MetricName {
- nameMatcher = m
- ms = slices.Delete(ms, i, i+1)
+ hasNameMatcher := false
+ for _, ms := range dataLabelMatchers {
+ for _, m := range ms {
+ if m.Name == model.MetricNameLabel {
+ hasNameMatcher = true
}
infoLabelMatchers = append(infoLabelMatchers, m)
}
- if len(ms) > 0 {
- dataLabelMatchers[name] = ms
- } else {
- delete(dataLabelMatchers, name)
- }
}
- if nameMatcher == nil {
+ removeNameFromDataLabelMatchers()
+ if !hasNameMatcher {
// Default to using the target_info metric.
- infoLabelMatchers = append([]*labels.Matcher{labels.MustNewMatcher(labels.MatchEqual, labels.MetricName, targetInfo)}, infoLabelMatchers...)
+ infoLabelMatchers = append([]*labels.Matcher{labels.MustNewMatcher(labels.MatchEqual, model.MetricNameLabel, targetInfo)}, infoLabelMatchers...)
}
infoIt := ev.querier.Select(ctx, false, &selectHints, infoLabelMatchers...)
@@ -203,7 +222,7 @@ func (ev *evaluator) combineWithInfoSeries(ctx context.Context, mat, infoMat Mat
sigFunction := func(name string) func(labels.Labels) string {
return func(lset labels.Labels) string {
lb.Reset()
- lb.Add(labels.MetricName, name)
+ lb.Add(model.MetricNameLabel, name)
lset.MatchLabels(true, identifyingLabels...).Range(func(l labels.Label) {
lb.Add(l.Name, l.Value)
})
@@ -215,7 +234,7 @@ func (ev *evaluator) combineWithInfoSeries(ctx context.Context, mat, infoMat Mat
infoMetrics := map[string]struct{}{}
for _, is := range infoMat {
lblMap := is.Metric.Map()
- infoMetrics[lblMap[labels.MetricName]] = struct{}{}
+ infoMetrics[lblMap[model.MetricNameLabel]] = struct{}{}
}
sigfs := make(map[string]func(labels.Labels) string, len(infoMetrics))
for name := range infoMetrics {
@@ -260,7 +279,7 @@ func (ev *evaluator) combineWithInfoSeries(ctx context.Context, mat, infoMat Mat
infoSigs := make(map[uint64]string, len(infoMat))
for _, s := range infoMat {
- name := s.Metric.Map()[labels.MetricName]
+ name := s.Metric.Map()[model.MetricNameLabel]
infoSigs[s.Metric.Hash()] = sigfs[name](s.Metric)
}
@@ -337,10 +356,10 @@ func (ev *evaluator) combineWithInfoVector(base, info Vector, ignoreSeries map[u
}
// All samples from the info Vector hashed by the matching label/values.
- if enh.rightSigs == nil {
- enh.rightSigs = make(map[string]Sample, len(enh.Out))
+ if enh.rightStrSigs == nil {
+ enh.rightStrSigs = make(map[string]Sample, len(enh.Out))
} else {
- clear(enh.rightSigs)
+ clear(enh.rightStrSigs)
}
for _, s := range info {
@@ -351,7 +370,7 @@ func (ev *evaluator) combineWithInfoVector(base, info Vector, ignoreSeries map[u
origT := int64(s.F)
sig := infoSigs[s.Metric.Hash()]
- if existing, exists := enh.rightSigs[sig]; exists {
+ if existing, exists := enh.rightStrSigs[sig]; exists {
// We encode original info sample timestamps via the float value.
existingOrigT := int64(existing.F)
switch {
@@ -359,14 +378,14 @@ func (ev *evaluator) combineWithInfoVector(base, info Vector, ignoreSeries map[u
// Keep the other info sample, since it's newer.
case existingOrigT < origT:
// Keep this info sample, since it's newer.
- enh.rightSigs[sig] = s
+ enh.rightStrSigs[sig] = s
default:
// The two info samples have the same timestamp - conflict.
ev.errorf("found duplicate series for info metric: existing %s @ %d, new %s @ %d",
existing.Metric.String(), existingOrigT, s.Metric.String(), origT)
}
} else {
- enh.rightSigs[sig] = s
+ enh.rightStrSigs[sig] = s
}
}
@@ -389,7 +408,7 @@ func (ev *evaluator) combineWithInfoVector(base, info Vector, ignoreSeries map[u
// For every info metric name, try to find an info series with the same signature.
seenInfoMetrics := map[string]struct{}{}
for infoName, sig := range baseSigs[hash] {
- is, exists := enh.rightSigs[sig]
+ is, exists := enh.rightStrSigs[sig]
if !exists {
continue
}
@@ -398,7 +417,7 @@ func (ev *evaluator) combineWithInfoVector(base, info Vector, ignoreSeries map[u
}
err := is.Metric.Validate(func(l labels.Label) error {
- if l.Name == labels.MetricName {
+ if l.Name == model.MetricNameLabel {
return nil
}
if _, exists := dataLabelMatchers[l.Name]; len(dataLabelMatchers) > 0 && !exists {
@@ -424,9 +443,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 67ecb190fe..6496095287 100644
--- a/promql/parser/ast.go
+++ b/promql/parser/ast.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
@@ -116,8 +116,8 @@ type DurationExpr struct {
LHS, RHS Expr // The operands on the respective sides of the operator.
Wrapped bool // Set when the duration is wrapped in parentheses.
- StartPos posrange.Pos // For unary operations and step(), the start position of the operator.
- EndPos posrange.Pos // For step(), the end position of the operator.
+ StartPos posrange.Pos // For unary operations, step(), and range(), the start position of the operator.
+ EndPos posrange.Pos // For step() and range(), the end position of the operator.
}
// Call represents a function call.
@@ -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
@@ -474,7 +487,7 @@ func (e *BinaryExpr) PositionRange() posrange.PositionRange {
}
func (e *DurationExpr) PositionRange() posrange.PositionRange {
- if e.Op == STEP {
+ if e.Op == STEP || e.Op == RANGE {
return posrange.PositionRange{
Start: e.StartPos,
End: e.EndPos,
diff --git a/promql/parser/features.go b/promql/parser/features.go
new file mode 100644
index 0000000000..3bd3c493f5
--- /dev/null
+++ b/promql/parser/features.go
@@ -0,0 +1,58 @@
+// 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 parser
+
+import "github.com/prometheus/prometheus/util/features"
+
+// RegisterFeatures registers all PromQL features with the feature registry.
+// This includes operators (arithmetic and comparison/set), aggregators (standard
+// and experimental), and functions.
+func (pql *promQLParser) RegisterFeatures(r features.Collector) {
+ // Register core PromQL language keywords.
+ for keyword, itemType := range key {
+ if itemType.IsKeyword() {
+ switch keyword {
+ case "anchored", "smoothed":
+ r.Set(features.PromQL, keyword, pql.options.EnableExtendedRangeSelectors)
+ case "fill", "fill_left", "fill_right":
+ r.Set(features.PromQL, keyword, pql.options.EnableBinopFillModifiers)
+ default:
+ r.Enable(features.PromQL, keyword)
+ }
+ }
+ }
+
+ // Register operators.
+ for o := ItemType(operatorsStart + 1); o < operatorsEnd; o++ {
+ if o.IsOperator() {
+ r.Set(features.PromQLOperators, o.String(), true)
+ }
+ }
+
+ // Register aggregators.
+ for a := ItemType(aggregatorsStart + 1); a < aggregatorsEnd; a++ {
+ if a.IsAggregator() {
+ experimental := a.IsExperimentalAggregator() && !pql.options.EnableExperimentalFunctions
+ r.Set(features.PromQLOperators, a.String(), !experimental)
+ }
+ }
+
+ // Register functions.
+ for f, fc := range Functions {
+ r.Set(features.PromQLFunctions, f, !fc.Experimental || pql.options.EnableExperimentalFunctions)
+ }
+
+ // Register experimental parser features.
+ r.Set(features.PromQL, "duration_expr", pql.options.ExperimentalDurationExpr)
+}
diff --git a/promql/parser/functions.go b/promql/parser/functions.go
index a471cb3a6d..180a255ab0 100644
--- a/promql/parser/functions.go
+++ b/promql/parser/functions.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
@@ -23,9 +23,6 @@ type Function struct {
Experimental bool
}
-// EnableExperimentalFunctions controls whether experimentalFunctions are enabled.
-var EnableExperimentalFunctions bool
-
// Functions is a list of all functions supported by PromQL, including their types.
var Functions = map[string]*Function{
"abs": {
@@ -208,6 +205,13 @@ var Functions = map[string]*Function{
ArgTypes: []ValueType{ValueTypeScalar, ValueTypeVector},
ReturnType: ValueTypeVector,
},
+ "histogram_quantiles": {
+ Name: "histogram_quantiles",
+ ArgTypes: []ValueType{ValueTypeVector, ValueTypeString, ValueTypeScalar, ValueTypeScalar},
+ Variadic: 9,
+ ReturnType: ValueTypeVector,
+ Experimental: true,
+ },
"double_exponential_smoothing": {
Name: "double_exponential_smoothing",
ArgTypes: []ValueType{ValueTypeMatrix, ValueTypeScalar, ValueTypeScalar},
diff --git a/promql/parser/generated_parser.y b/promql/parser/generated_parser.y
index d9bbb10b28..1196002b76 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
@@ -153,6 +156,7 @@ WITHOUT
START
END
STEP
+RANGE
%token preprocessorEnd
// Counter reset hints.
@@ -189,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
@@ -301,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{
@@ -345,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 }
@@ -386,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.
*/
@@ -396,7 +456,7 @@ function_call : IDENTIFIER function_call_body
if !exist{
yylex.(*parser).addParseErrf($1.PositionRange(),"unknown function with name %q", $1.Val)
}
- if fn != nil && fn.Experimental && !EnableExperimentalFunctions {
+ if fn != nil && fn.Experimental && !yylex.(*parser).options.EnableExperimentalFunctions {
yylex.(*parser).addParseErrf($1.PositionRange(),"function %q is not enabled", $1.Val)
}
$$ = &Call{
@@ -465,7 +525,7 @@ offset_expr: expr OFFSET offset_duration_expr
$$ = $1
}
| expr OFFSET error
- { yylex.(*parser).unexpected("offset", "number, duration, or step()"); $$ = $1 }
+ { yylex.(*parser).unexpected("offset", "number, duration, step(), or range()"); $$ = $1 }
;
/*
@@ -575,11 +635,11 @@ subquery_expr : expr LEFT_BRACKET positive_duration_expr COLON positive_durati
| expr LEFT_BRACKET positive_duration_expr COLON positive_duration_expr error
{ yylex.(*parser).unexpected("subquery selector", "\"]\""); $$ = $1 }
| expr LEFT_BRACKET positive_duration_expr COLON error
- { yylex.(*parser).unexpected("subquery selector", "number, duration, or step() or \"]\""); $$ = $1 }
+ { yylex.(*parser).unexpected("subquery selector", "number, duration, step(), range(), or \"]\""); $$ = $1 }
| expr LEFT_BRACKET positive_duration_expr error
{ yylex.(*parser).unexpected("subquery or range", "\":\" or \"]\""); $$ = $1 }
| expr LEFT_BRACKET error
- { yylex.(*parser).unexpected("subquery or range selector", "number, duration, or step()"); $$ = $1 }
+ { yylex.(*parser).unexpected("subquery or range selector", "number, duration, step(), or range()"); $$ = $1 }
;
/*
@@ -696,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 | 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...) }
@@ -790,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
}
}
@@ -953,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 | 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;
@@ -1088,6 +1149,14 @@ offset_duration_expr : number_duration_literal
EndPos: $3.PositionRange().End,
}
}
+ | RANGE LEFT_PAREN RIGHT_PAREN
+ {
+ $$ = &DurationExpr{
+ Op: RANGE,
+ StartPos: $1.PositionRange().Start,
+ EndPos: $3.PositionRange().End,
+ }
+ }
| unary_op STEP LEFT_PAREN RIGHT_PAREN
{
$$ = &DurationExpr{
@@ -1100,6 +1169,18 @@ offset_duration_expr : number_duration_literal
StartPos: $1.Pos,
}
}
+ | unary_op RANGE LEFT_PAREN RIGHT_PAREN
+ {
+ $$ = &DurationExpr{
+ Op: $1.Typ,
+ RHS: &DurationExpr{
+ Op: RANGE,
+ StartPos: $2.PositionRange().Start,
+ EndPos: $4.PositionRange().End,
+ },
+ StartPos: $1.Pos,
+ }
+ }
| min_max LEFT_PAREN duration_expr COMMA duration_expr RIGHT_PAREN
{
$$ = &DurationExpr{
@@ -1141,7 +1222,7 @@ offset_duration_expr : number_duration_literal
}
| duration_expr
;
-
+
min_max: MIN | MAX ;
duration_expr : number_duration_literal
@@ -1234,6 +1315,14 @@ duration_expr : number_duration_literal
EndPos: $3.PositionRange().End,
}
}
+ | RANGE LEFT_PAREN RIGHT_PAREN
+ {
+ $$ = &DurationExpr{
+ Op: RANGE,
+ StartPos: $1.PositionRange().Start,
+ EndPos: $3.PositionRange().End,
+ }
+ }
| min_max LEFT_PAREN duration_expr COMMA duration_expr RIGHT_PAREN
{
$$ = &DurationExpr{
@@ -1248,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 eb4b32129a..3a69f55516 100644
--- a/promql/parser/generated_parser.y.go
+++ b/promql/parser/generated_parser.y.go
@@ -113,30 +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 preprocessorEnd = 57435
-const counterResetHintsStart = 57436
-const UNKNOWN_COUNTER_RESET = 57437
-const COUNTER_RESET = 57438
-const NOT_COUNTER_RESET = 57439
-const GAUGE_TYPE = 57440
-const counterResetHintsEnd = 57441
-const startSymbolsStart = 57442
-const START_METRIC = 57443
-const START_SERIES_DESCRIPTION = 57444
-const START_EXPRESSION = 57445
-const START_METRIC_SELECTOR = 57446
-const startSymbolsEnd = 57447
+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",
@@ -220,6 +224,9 @@ var yyToknames = [...]string{
"BY",
"GROUP_LEFT",
"GROUP_RIGHT",
+ "FILL",
+ "FILL_LEFT",
+ "FILL_RIGHT",
"IGNORING",
"OFFSET",
"SMOOTHED",
@@ -231,6 +238,7 @@ var yyToknames = [...]string{
"START",
"END",
"STEP",
+ "RANGE",
"preprocessorEnd",
"counterResetHintsStart",
"UNKNOWN_COUNTER_RESET",
@@ -256,376 +264,403 @@ var yyExca = [...]int16{
-1, 1,
1, -1,
-2, 0,
- -1, 40,
- 1, 149,
- 10, 149,
- 24, 149,
+ -1, 44,
+ 1, 161,
+ 10, 161,
+ 24, 161,
-2, 0,
- -1, 70,
- 2, 192,
- 15, 192,
- 79, 192,
- 87, 192,
- -2, 107,
- -1, 71,
- 2, 193,
- 15, 193,
- 79, 193,
- 87, 193,
- -2, 108,
- -1, 72,
- 2, 194,
- 15, 194,
- 79, 194,
- 87, 194,
- -2, 110,
- -1, 73,
- 2, 195,
- 15, 195,
- 79, 195,
- 87, 195,
- -2, 111,
- -1, 74,
- 2, 196,
- 15, 196,
- 79, 196,
- 87, 196,
- -2, 112,
-1, 75,
- 2, 197,
- 15, 197,
- 79, 197,
- 87, 197,
- -2, 117,
- -1, 76,
- 2, 198,
- 15, 198,
- 79, 198,
- 87, 198,
- -2, 119,
- -1, 77,
- 2, 199,
- 15, 199,
- 79, 199,
- 87, 199,
- -2, 121,
- -1, 78,
- 2, 200,
- 15, 200,
- 79, 200,
- 87, 200,
- -2, 122,
- -1, 79,
- 2, 201,
- 15, 201,
- 79, 201,
- 87, 201,
- -2, 123,
- -1, 80,
- 2, 202,
- 15, 202,
- 79, 202,
- 87, 202,
- -2, 124,
- -1, 81,
- 2, 203,
- 15, 203,
- 79, 203,
- 87, 203,
- -2, 125,
- -1, 82,
2, 204,
15, 204,
79, 204,
- 87, 204,
- -2, 129,
- -1, 83,
+ 90, 204,
+ -2, 115,
+ -1, 76,
2, 205,
15, 205,
79, 205,
- 87, 205,
+ 90, 205,
+ -2, 116,
+ -1, 77,
+ 2, 206,
+ 15, 206,
+ 79, 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, 135,
- 41, 270,
- 42, 270,
- 52, 270,
- 53, 270,
- 57, 270,
+ -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, 245,
- 9, 257,
- 12, 257,
- 13, 257,
- 18, 257,
- 19, 257,
- 25, 257,
- 41, 257,
- 47, 257,
- 48, 257,
- 51, 257,
- 57, 257,
- 62, 257,
- 63, 257,
- 64, 257,
- 65, 257,
- 66, 257,
- 67, 257,
- 68, 257,
- 69, 257,
- 70, 257,
- 71, 257,
- 72, 257,
- 73, 257,
- 74, 257,
- 75, 257,
- 79, 257,
- 83, 257,
- 84, 257,
- 85, 257,
- 87, 257,
- 90, 257,
- 91, 257,
- 92, 257,
+ -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, 246,
- 9, 257,
- 12, 257,
- 13, 257,
- 18, 257,
- 19, 257,
- 25, 257,
- 41, 257,
- 47, 257,
- 48, 257,
- 51, 257,
- 57, 257,
- 62, 257,
- 63, 257,
- 64, 257,
- 65, 257,
- 66, 257,
- 67, 257,
- 68, 257,
- 69, 257,
- 70, 257,
- 71, 257,
- 72, 257,
- 73, 257,
- 74, 257,
- 75, 257,
- 79, 257,
- 83, 257,
- 84, 257,
- 85, 257,
- 87, 257,
- 90, 257,
- 91, 257,
- 92, 257,
+ -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 = 1071
+const yyLast = 1224
var yyAct = [...]int16{
- 57, 182, 401, 399, 185, 406, 278, 237, 193, 332,
- 93, 47, 346, 141, 68, 221, 91, 413, 414, 415,
- 416, 127, 128, 64, 156, 186, 66, 126, 347, 326,
- 129, 243, 122, 125, 130, 244, 245, 246, 119, 122,
- 118, 124, 123, 121, 327, 151, 124, 118, 214, 123,
- 121, 396, 373, 124, 120, 364, 395, 366, 323, 385,
- 328, 354, 352, 133, 216, 135, 6, 98, 100, 101,
- 364, 102, 103, 104, 105, 106, 107, 108, 109, 110,
- 111, 324, 112, 113, 117, 99, 42, 131, 315, 112,
- 144, 117, 136, 400, 241, 350, 191, 143, 128, 349,
- 142, 137, 270, 314, 322, 320, 129, 268, 317, 114,
- 116, 115, 192, 95, 233, 178, 114, 116, 115, 195,
- 199, 200, 201, 202, 203, 204, 174, 321, 319, 177,
- 196, 196, 196, 196, 196, 196, 196, 232, 175, 217,
- 267, 130, 197, 197, 197, 197, 197, 197, 197, 132,
- 196, 134, 138, 205, 390, 407, 239, 207, 210, 227,
- 206, 223, 197, 229, 428, 2, 3, 4, 5, 360,
- 190, 194, 429, 389, 359, 7, 266, 240, 61, 86,
- 189, 231, 269, 427, 181, 150, 426, 262, 60, 358,
- 264, 119, 122, 196, 425, 209, 271, 272, 266, 197,
- 152, 225, 123, 121, 230, 197, 124, 120, 208, 196,
- 84, 224, 226, 119, 122, 38, 384, 213, 222, 383,
- 223, 197, 10, 382, 123, 121, 85, 235, 124, 120,
- 143, 190, 88, 318, 238, 381, 180, 179, 241, 242,
- 380, 189, 379, 378, 247, 248, 249, 250, 251, 252,
- 253, 254, 255, 256, 257, 258, 259, 260, 261, 348,
- 225, 198, 325, 191, 94, 377, 351, 376, 97, 353,
- 224, 226, 344, 345, 92, 195, 375, 196, 374, 192,
- 196, 39, 228, 355, 61, 55, 196, 95, 1, 197,
- 181, 87, 197, 149, 60, 148, 172, 69, 197, 54,
- 157, 158, 159, 160, 161, 162, 163, 164, 165, 166,
- 167, 168, 169, 170, 171, 417, 84, 362, 65, 53,
- 190, 9, 9, 144, 52, 51, 363, 365, 196, 367,
- 189, 155, 85, 142, 275, 368, 369, 184, 274, 50,
- 197, 140, 180, 179, 190, 49, 95, 48, 372, 119,
- 122, 386, 191, 273, 189, 8, 46, 153, 211, 40,
- 123, 121, 196, 371, 124, 120, 392, 198, 192, 394,
- 370, 388, 94, 45, 197, 154, 191, 402, 403, 404,
- 398, 44, 92, 405, 43, 409, 408, 411, 410, 418,
- 90, 281, 192, 56, 236, 95, 422, 316, 419, 420,
- 196, 291, 361, 421, 393, 119, 122, 297, 329, 423,
- 96, 391, 197, 234, 280, 276, 123, 121, 424, 89,
- 124, 120, 412, 119, 122, 187, 188, 183, 431, 196,
- 279, 119, 122, 58, 123, 121, 293, 294, 124, 120,
- 295, 197, 123, 121, 139, 0, 124, 120, 308, 0,
- 0, 282, 284, 286, 287, 288, 296, 298, 301, 302,
- 303, 304, 305, 309, 310, 0, 281, 283, 285, 289,
- 290, 292, 299, 313, 312, 300, 291, 0, 220, 306,
- 307, 311, 297, 219, 0, 0, 277, 387, 0, 280,
- 147, 0, 190, 61, 0, 146, 218, 0, 0, 265,
- 0, 0, 189, 60, 430, 0, 119, 122, 145, 0,
- 0, 293, 294, 0, 0, 295, 0, 123, 121, 0,
- 0, 124, 120, 308, 191, 84, 282, 284, 286, 287,
- 288, 296, 298, 301, 302, 303, 304, 305, 309, 310,
- 192, 85, 283, 285, 289, 290, 292, 299, 313, 312,
- 300, 180, 179, 0, 306, 307, 311, 61, 0, 118,
- 59, 86, 0, 62, 0, 0, 22, 60, 0, 0,
- 212, 0, 0, 63, 0, 0, 263, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 98, 100, 0, 84,
- 0, 0, 0, 0, 0, 18, 19, 109, 110, 20,
- 0, 112, 113, 117, 99, 85, 0, 0, 0, 0,
- 70, 71, 72, 73, 74, 75, 76, 77, 78, 79,
- 80, 81, 82, 83, 0, 0, 0, 13, 114, 116,
- 115, 24, 37, 36, 215, 30, 0, 0, 31, 32,
- 67, 61, 41, 0, 59, 86, 0, 62, 0, 0,
- 22, 60, 0, 119, 122, 0, 0, 63, 0, 0,
- 0, 0, 0, 0, 123, 121, 0, 0, 124, 120,
- 0, 357, 0, 84, 0, 0, 0, 0, 61, 18,
- 19, 0, 0, 20, 181, 0, 0, 0, 60, 85,
- 356, 0, 0, 0, 70, 71, 72, 73, 74, 75,
- 76, 77, 78, 79, 80, 81, 82, 83, 0, 0,
- 84, 13, 0, 0, 0, 24, 37, 36, 0, 30,
- 0, 0, 31, 32, 67, 61, 85, 0, 59, 86,
- 0, 62, 331, 0, 22, 60, 180, 179, 0, 330,
- 0, 63, 0, 334, 335, 333, 340, 342, 339, 341,
- 336, 337, 338, 343, 0, 0, 0, 84, 0, 0,
- 0, 198, 0, 18, 19, 0, 0, 20, 0, 0,
- 0, 0, 0, 85, 0, 0, 0, 0, 70, 71,
- 72, 73, 74, 75, 76, 77, 78, 79, 80, 81,
- 82, 83, 17, 86, 0, 13, 0, 0, 22, 24,
- 37, 36, 397, 30, 0, 0, 31, 32, 67, 0,
- 0, 0, 0, 334, 335, 333, 340, 342, 339, 341,
- 336, 337, 338, 343, 0, 0, 0, 18, 19, 0,
- 0, 20, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 11, 12, 14, 15, 16, 21, 23, 25,
- 26, 27, 28, 29, 33, 34, 17, 38, 0, 13,
- 0, 0, 22, 24, 37, 36, 0, 30, 0, 0,
- 31, 32, 35, 0, 0, 0, 0, 0, 0, 0,
+ 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, 18, 19, 0, 0, 20, 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,
- 16, 21, 23, 25, 26, 27, 28, 29, 33, 34,
- 118, 0, 0, 13, 0, 0, 0, 24, 37, 36,
- 0, 30, 0, 0, 31, 32, 35, 0, 0, 118,
- 0, 0, 0, 0, 0, 0, 0, 98, 100, 101,
- 0, 102, 103, 104, 105, 106, 107, 108, 109, 110,
- 111, 0, 112, 113, 117, 99, 98, 100, 101, 0,
- 102, 103, 104, 0, 106, 107, 108, 109, 110, 111,
- 173, 112, 113, 117, 99, 118, 0, 61, 0, 114,
- 116, 115, 0, 181, 118, 0, 0, 60, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 114, 116,
- 115, 0, 98, 100, 101, 0, 102, 103, 0, 84,
- 106, 107, 100, 109, 110, 111, 0, 112, 113, 117,
- 99, 0, 109, 110, 0, 85, 112, 0, 117, 99,
- 0, 0, 0, 0, 0, 180, 179, 0, 0, 0,
- 0, 0, 0, 0, 114, 116, 115, 0, 0, 0,
- 0, 0, 0, 114, 116, 115, 0, 0, 0, 0,
- 176,
+ 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{
- 64, 165, 844, 844, 632, 780, -1000, -1000, -1000, 202,
- -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, 370, -1000,
- 266, -1000, 906, -1000, -1000, -1000, -1000, -1000, -1000, -1000,
- -1000, -1000, -1000, -1000, -1000, -1000, -1000, -3, 19, 126,
- -1000, -1000, 716, -1000, 716, 166, -1000, 86, 137, -1000,
- -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000,
- -1000, -1000, -1000, -1000, -1000, -1000, 321, -1000, -1000, 488,
- -1000, -1000, 291, 181, -1000, -1000, 21, -1000, -54, -54,
- -54, -54, -54, -54, -54, -54, -54, -54, -54, -54,
- -54, -54, -54, -54, 978, -1000, -1000, 335, 169, 275,
- 275, 275, 275, 275, 275, 126, -57, -1000, 193, 193,
- 548, -1000, 26, 612, 33, -15, -1000, 42, 275, 476,
- -1000, -1000, 216, 157, -1000, -1000, 262, -1000, 179, -1000,
- 112, 222, 716, -1000, -51, -44, -1000, 716, 716, 716,
- 716, 716, 716, 716, 716, 716, 716, 716, 716, 716,
- 716, 716, -1000, -1000, -1000, 484, 125, 92, -3, -1000,
- -1000, 275, -1000, 87, -1000, -1000, -1000, -1000, -1000, -1000,
- -1000, 161, 161, 332, -1000, -3, -1000, 275, 86, -10,
- -10, -15, -15, -15, -15, -1000, -1000, -1000, 464, -1000,
- -1000, 81, -1000, 906, -1000, -1000, -1000, 390, -1000, 88,
- -1000, 103, -1000, -1000, -1000, -1000, -1000, 102, -1000, -1000,
- -1000, -1000, -1000, -1000, -1000, 32, 55, 3, -1000, -1000,
- -1000, 715, 980, 193, 193, 193, 193, 33, 33, 545,
- 545, 545, 971, 925, 545, 545, 971, 33, 33, 545,
- 33, 980, -1000, 84, 80, 275, -15, 40, 275, 612,
- 39, -1000, -1000, -1000, 669, -1000, 167, -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, -1000, 716, 275, -1000, -1000, -1000,
- -1000, -1000, -1000, 51, 51, 31, 51, 78, 78, 346,
- 35, -1000, -1000, 272, 270, 261, 259, 237, 236, 234,
- 229, 217, 213, 210, -1000, -1000, -1000, -1000, -1000, 37,
- 275, 465, -1000, 364, -1000, 152, -1000, -1000, -1000, 389,
- -1000, 906, 382, -1000, -1000, -1000, 51, -1000, 30, 25,
- 785, -1000, -1000, -1000, 36, 311, 311, 311, 161, 141,
- 141, 36, 141, 36, -78, -1000, 308, -1000, 275, -1000,
- -1000, -1000, -1000, -1000, -1000, 51, 51, -1000, -1000, -1000,
- 51, -1000, -1000, -1000, -1000, -1000, -1000, 311, -1000, -1000,
- -1000, -1000, -1000, -1000, -1000, -1000, -1000, 275, 172, -1000,
- -1000, -1000, 162, -1000, 150, -1000, 483, -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, 444, 13, 433, 6, 15, 430, 318, 23, 427,
- 10, 422, 14, 222, 355, 419, 16, 415, 28, 12,
- 413, 410, 7, 408, 9, 5, 396, 3, 2, 4,
- 394, 25, 1, 393, 384, 33, 200, 381, 375, 86,
- 373, 358, 27, 357, 26, 356, 11, 347, 345, 339,
- 331, 325, 324, 319, 299, 285, 0, 297, 8, 296,
- 288, 281,
+ 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, 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, 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, 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, 12, 12, 56,
- 56, 56, 56, 56, 56, 56, 56, 56, 56, 56,
- 57,
+ 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
+ 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{
@@ -634,124 +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, 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, 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, 2, 2,
- 1, 1, 1, 2, 1, 1, 1, 0, 1, 1,
- 2, 3, 4, 6, 7, 4, 1, 1, 1, 1,
- 2, 3, 3, 3, 3, 3, 3, 3, 6, 1,
- 3,
+ 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,
}
var yyChk = [...]int16{
- -1000, -60, 101, 102, 103, 104, 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, 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, -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, -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, -12, -56, 68,
- 67, 15, -32, -9, 2, -29, -31, 90, 91, 19,
- 9, 41, 57, -58, 2, -56, -46, -8, 92, -56,
- -56, -56, -56, -56, -56, -42, -35, -18, 15, 2,
- -18, -41, 22, -39, 22, 22, 22, -56, 20, 7,
+ -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, -39, 82, 86, 80, 81, -39, -39, -39,
- -39, -39, -39, -39, -39, -39, -39, -39, -39, -39,
- -39, -39, -46, 92, -12, 15, -56, 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, 85, 84, 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, -56, 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, -56, 22, 7, 21,
+ -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, -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, 95, 96, 97, 98, 7, -56, -28,
- -28, -28, -26, -32, -56, 22, 24, 21, 2, 22,
- 21, -32,
+ -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, 137, 137, 0, 0, 7, 6, 1, 137,
- 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, 0, 2,
- -2, 3, 4, 8, 9, 10, 11, 12, 13, 14,
- 15, 16, 17, 18, 19, 20, 21, 22, 0, 113,
- 244, 245, 0, 255, 0, 90, 91, 131, 0, 279,
- -2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
- -2, -2, -2, -2, 238, 239, 0, 5, 105, 0,
- 136, 139, 0, 143, 147, 256, 148, 152, 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,
- 94, 96, 0, 100, 104, 134, 0, 140, 0, 146,
- 0, 151, 0, 45, 50, 51, 47, 0, 0, 0,
+ 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, 72, 73, 259, 0, 0, 0, 266, 267,
- 268, 0, 76, 0, 78, 250, 251, 79, 80, 246,
- 247, 0, 0, 0, 87, 71, 269, 0, 0, 271,
- 272, 273, 274, 275, 276, 23, 24, 27, 0, 57,
- 28, 0, 66, 68, 70, 280, 277, 0, 92, 0,
- 97, 0, 103, 240, 241, 242, 243, 0, 135, 138,
- 141, 144, 142, 145, 150, 153, 155, 158, 162, 163,
- 164, 0, 29, 0, 0, -2, -2, 30, 31, 32,
- 33, 34, 35, 36, 37, 38, 39, 40, 41, 42,
- 43, 44, 260, 0, 0, 0, 270, 0, 0, 0,
- 0, 248, 249, 81, 0, 86, 0, 56, 59, 61,
- 62, 63, 206, 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, 65, 69, 0, 93, 95, 98,
- 102, 99, 101, 0, 0, 0, 0, 0, 0, 0,
- 0, 168, 170, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 48, 49, 52, 258, 53, 0,
- 0, 0, 261, 0, 77, 0, 83, 85, 54, 0,
- 60, 67, 0, 154, 252, 156, 0, 159, 0, 0,
- 0, 166, 171, 167, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 262, 0, 265, 0, 82,
- 84, 55, 58, 278, 157, 0, 0, 165, 169, 172,
- 0, 254, 173, 174, 175, 176, 177, 0, 178, 179,
- 180, 181, 182, 188, 189, 190, 191, 0, 0, 160,
- 161, 253, 0, 186, 0, 263, 0, 184, 187, 264,
- 183, 185,
+ 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{
@@ -769,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,
+ 102, 103, 104, 105, 106, 107, 108, 109,
}
var yyTok3 = [...]int8{
@@ -1294,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) {
@@ -1339,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)
@@ -1350,20 +1431,35 @@ 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)
if !exist {
yylex.(*parser).addParseErrf(yyDollar[1].item.PositionRange(), "unknown function with name %q", yyDollar[1].item.Val)
}
- if fn != nil && fn.Experimental && !EnableExperimentalFunctions {
+ if fn != nil && fn.Experimental && !yylex.(*parser).options.EnableExperimentalFunctions {
yylex.(*parser).addParseErrf(yyDollar[1].item.PositionRange(), "function %q is not enabled", yyDollar[1].item.Val)
}
yyVAL.node = &Call{
@@ -1375,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 {
@@ -1420,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 {
@@ -1431,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, or step()")
+ 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
@@ -1495,7 +1591,7 @@ yydefault:
EndPos: yylex.(*parser).lastClosing,
}
}
- case 82:
+ case 90:
yyDollar = yyS[yypt-6 : yypt+1]
{
var rangeNl time.Duration
@@ -1517,7 +1613,7 @@ yydefault:
EndPos: yyDollar[6].item.Pos + 1,
}
}
- case 83:
+ case 91:
yyDollar = yyS[yypt-5 : yypt+1]
{
var rangeNl time.Duration
@@ -1532,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, or step() or \"]\"")
+ 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, or step()")
+ 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 {
@@ -1569,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)
@@ -1578,7 +1674,7 @@ yydefault:
yylex.(*parser).assembleVectorSelector(vs)
yyVAL.node = vs
}
- case 90:
+ case 98:
yyDollar = yyS[yypt-1 : yypt+1]
{
vs := &VectorSelector{
@@ -1589,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{
@@ -1604,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{
@@ -1612,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{
@@ -1620,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 {
@@ -1629,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 134:
+ case 146:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.labels = labels.New(yyDollar[2].lblList...)
}
- case 135:
+ case 147:
yyDollar = yyS[yypt-4 : yypt+1]
{
yyVAL.labels = labels.New(yyDollar[2].lblList...)
}
- case 136:
+ case 148:
yyDollar = yyS[yypt-2 : yypt+1]
{
yyVAL.labels = labels.New()
}
- case 137:
+ case 149:
yyDollar = yyS[yypt-0 : yypt+1]
{
yyVAL.labels = labels.New()
}
- case 138:
+ case 150:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.lblList = append(yyDollar[1].lblList, yyDollar[3].label)
}
- case 139:
+ case 151:
yyDollar = yyS[yypt-1 : yypt+1]
{
yyVAL.lblList = []labels.Label{yyDollar[1].label}
}
- case 140:
+ case 152:
yyDollar = yyS[yypt-2 : yypt+1]
{
yylex.(*parser).unexpected("label set", "\",\" or \"}\"")
yyVAL.lblList = yyDollar[1].lblList
}
- case 141:
+ 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 142:
+ 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 143:
+ case 155:
yyDollar = yyS[yypt-1 : yypt+1]
{
yyVAL.label = labels.Label{Name: labels.MetricName, Value: yyDollar[1].item.Val}
}
- case 144:
+ case 156:
yyDollar = yyS[yypt-3 : yypt+1]
{
yylex.(*parser).unexpected("label set", "string")
yyVAL.label = labels.Label{}
}
- case 145:
+ case 157:
yyDollar = yyS[yypt-3 : yypt+1]
{
yylex.(*parser).unexpected("label set", "string")
yyVAL.label = labels.Label{}
}
- case 146:
+ case 158:
yyDollar = yyS[yypt-2 : yypt+1]
{
yylex.(*parser).unexpected("label set", "\"=\"")
yyVAL.label = labels.Label{}
}
- case 147:
+ case 159:
yyDollar = yyS[yypt-1 : yypt+1]
{
yylex.(*parser).unexpected("label set", "identifier or \"}\"")
yyVAL.label = labels.Label{}
}
- case 148:
+ case 160:
yyDollar = yyS[yypt-2 : yypt+1]
{
yylex.(*parser).generatedParserResult = &seriesDescription{
@@ -1774,33 +1870,33 @@ yydefault:
values: yyDollar[2].series,
}
}
- case 149:
+ case 161:
yyDollar = yyS[yypt-0 : yypt+1]
{
yyVAL.series = []SequenceValue{}
}
- case 150:
+ case 162:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.series = append(yyDollar[1].series, yyDollar[3].series...)
}
- case 151:
+ case 163:
yyDollar = yyS[yypt-2 : yypt+1]
{
yyVAL.series = yyDollar[1].series
}
- case 152:
+ case 164:
yyDollar = yyS[yypt-1 : yypt+1]
{
yylex.(*parser).unexpected("series values", "")
yyVAL.series = nil
}
- case 153:
+ case 165:
yyDollar = yyS[yypt-1 : yypt+1]
{
yyVAL.series = []SequenceValue{{Omitted: true}}
}
- case 154:
+ case 166:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.series = []SequenceValue{}
@@ -1808,12 +1904,12 @@ yydefault:
yyVAL.series = append(yyVAL.series, SequenceValue{Omitted: true})
}
}
- case 155:
+ case 167:
yyDollar = yyS[yypt-1 : yypt+1]
{
yyVAL.series = []SequenceValue{{Value: yyDollar[1].float}}
}
- case 156:
+ case 168:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.series = []SequenceValue{}
@@ -1822,7 +1918,7 @@ yydefault:
yyVAL.series = append(yyVAL.series, SequenceValue{Value: yyDollar[1].float})
}
}
- case 157:
+ case 169:
yyDollar = yyS[yypt-4 : yypt+1]
{
yyVAL.series = []SequenceValue{}
@@ -1832,22 +1928,23 @@ yydefault:
yyDollar[1].float += yyDollar[2].float
}
}
- case 158:
+ 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 159:
+ 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 160:
+ case 172:
yyDollar = yyS[yypt-5 : yypt+1]
{
val, err := yylex.(*parser).histogramsIncreaseSeries(yyDollar[1].histogram, yyDollar[3].histogram, yyDollar[5].uint)
@@ -1856,7 +1953,7 @@ yydefault:
}
yyVAL.series = val
}
- case 161:
+ case 173:
yyDollar = yyS[yypt-5 : yypt+1]
{
val, err := yylex.(*parser).histogramsDecreaseSeries(yyDollar[1].histogram, yyDollar[3].histogram, yyDollar[5].uint)
@@ -1865,7 +1962,7 @@ yydefault:
}
yyVAL.series = val
}
- case 162:
+ case 174:
yyDollar = yyS[yypt-1 : yypt+1]
{
if yyDollar[1].item.Val != "stale" {
@@ -1873,130 +1970,130 @@ yydefault:
}
yyVAL.float = math.Float64frombits(value.StaleNaN)
}
- case 165:
+ case 177:
yyDollar = yyS[yypt-4 : yypt+1]
{
yyVAL.histogram = yylex.(*parser).buildHistogramFromMap(&yyDollar[2].descriptors)
}
- case 166:
+ case 178:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.histogram = yylex.(*parser).buildHistogramFromMap(&yyDollar[2].descriptors)
}
- case 167:
+ case 179:
yyDollar = yyS[yypt-3 : yypt+1]
{
m := yylex.(*parser).newMap()
yyVAL.histogram = yylex.(*parser).buildHistogramFromMap(&m)
}
- case 168:
+ case 180:
yyDollar = yyS[yypt-2 : yypt+1]
{
m := yylex.(*parser).newMap()
yyVAL.histogram = yylex.(*parser).buildHistogramFromMap(&m)
}
- case 169:
+ case 181:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.descriptors = *(yylex.(*parser).mergeMaps(&yyDollar[1].descriptors, &yyDollar[3].descriptors))
}
- case 170:
+ case 182:
yyDollar = yyS[yypt-1 : yypt+1]
{
yyVAL.descriptors = yyDollar[1].descriptors
}
- case 171:
+ case 183:
yyDollar = yyS[yypt-2 : yypt+1]
{
yylex.(*parser).unexpected("histogram description", "histogram description key, e.g. buckets:[5 10 7]")
}
- case 172:
+ case 184:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.descriptors = yylex.(*parser).newMap()
yyVAL.descriptors["schema"] = yyDollar[3].int
}
- case 173:
+ case 185:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.descriptors = yylex.(*parser).newMap()
yyVAL.descriptors["sum"] = yyDollar[3].float
}
- case 174:
+ case 186:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.descriptors = yylex.(*parser).newMap()
yyVAL.descriptors["count"] = yyDollar[3].float
}
- case 175:
+ case 187:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.descriptors = yylex.(*parser).newMap()
yyVAL.descriptors["z_bucket"] = yyDollar[3].float
}
- case 176:
+ case 188:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.descriptors = yylex.(*parser).newMap()
yyVAL.descriptors["z_bucket_w"] = yyDollar[3].float
}
- case 177:
+ case 189:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.descriptors = yylex.(*parser).newMap()
yyVAL.descriptors["custom_values"] = yyDollar[3].bucket_set
}
- case 178:
+ case 190:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.descriptors = yylex.(*parser).newMap()
yyVAL.descriptors["buckets"] = yyDollar[3].bucket_set
}
- case 179:
+ case 191:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.descriptors = yylex.(*parser).newMap()
yyVAL.descriptors["offset"] = yyDollar[3].int
}
- case 180:
+ case 192:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.descriptors = yylex.(*parser).newMap()
yyVAL.descriptors["n_buckets"] = yyDollar[3].bucket_set
}
- case 181:
+ case 193:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.descriptors = yylex.(*parser).newMap()
yyVAL.descriptors["n_offset"] = yyDollar[3].int
}
- case 182:
+ case 194:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.descriptors = yylex.(*parser).newMap()
yyVAL.descriptors["counter_reset_hint"] = yyDollar[3].item
}
- case 183:
+ case 195:
yyDollar = yyS[yypt-4 : yypt+1]
{
yyVAL.bucket_set = yyDollar[2].bucket_set
}
- case 184:
+ case 196:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.bucket_set = yyDollar[2].bucket_set
}
- case 185:
+ case 197:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.bucket_set = append(yyDollar[1].bucket_set, yyDollar[3].float)
}
- case 186:
+ case 198:
yyDollar = yyS[yypt-1 : yypt+1]
{
yyVAL.bucket_set = []float64{yyDollar[1].float}
}
- case 244:
+ case 260:
yyDollar = yyS[yypt-1 : yypt+1]
{
yyVAL.node = &NumberLiteral{
@@ -2004,7 +2101,7 @@ yydefault:
PosRange: yyDollar[1].item.PositionRange(),
}
}
- case 245:
+ case 261:
yyDollar = yyS[yypt-1 : yypt+1]
{
var err error
@@ -2019,12 +2116,12 @@ yydefault:
Duration: true,
}
}
- case 246:
+ case 262:
yyDollar = yyS[yypt-1 : yypt+1]
{
yyVAL.float = yylex.(*parser).number(yyDollar[1].item.Val)
}
- case 247:
+ case 263:
yyDollar = yyS[yypt-1 : yypt+1]
{
var err error
@@ -2035,17 +2132,17 @@ yydefault:
}
yyVAL.float = dur.Seconds()
}
- case 248:
+ case 264:
yyDollar = yyS[yypt-2 : yypt+1]
{
yyVAL.float = yyDollar[2].float
}
- case 249:
+ case 265:
yyDollar = yyS[yypt-2 : yypt+1]
{
yyVAL.float = -yyDollar[2].float
}
- case 252:
+ case 268:
yyDollar = yyS[yypt-1 : yypt+1]
{
var err error
@@ -2054,17 +2151,17 @@ yydefault:
yylex.(*parser).addParseErrf(yyDollar[1].item.PositionRange(), "invalid repetition in series values: %s", err)
}
}
- case 253:
+ case 269:
yyDollar = yyS[yypt-2 : yypt+1]
{
yyVAL.int = -int64(yyDollar[2].uint)
}
- case 254:
+ case 270:
yyDollar = yyS[yypt-1 : yypt+1]
{
yyVAL.int = int64(yyDollar[1].uint)
}
- case 255:
+ case 271:
yyDollar = yyS[yypt-1 : yypt+1]
{
yyVAL.node = &StringLiteral{
@@ -2072,7 +2169,7 @@ yydefault:
PosRange: yyDollar[1].item.PositionRange(),
}
}
- case 256:
+ case 272:
yyDollar = yyS[yypt-1 : yypt+1]
{
yyVAL.item = Item{
@@ -2081,12 +2178,12 @@ yydefault:
Val: yylex.(*parser).unquoteString(yyDollar[1].item.Val),
}
}
- case 257:
+ case 273:
yyDollar = yyS[yypt-0 : yypt+1]
{
yyVAL.strings = nil
}
- case 259:
+ case 275:
yyDollar = yyS[yypt-1 : yypt+1]
{
nl := yyDollar[1].node.(*NumberLiteral)
@@ -2097,7 +2194,7 @@ yydefault:
}
yyVAL.node = nl
}
- case 260:
+ case 276:
yyDollar = yyS[yypt-2 : yypt+1]
{
nl := yyDollar[2].node.(*NumberLiteral)
@@ -2112,7 +2209,7 @@ yydefault:
nl.PosRange.Start = yyDollar[1].item.Pos
yyVAL.node = nl
}
- case 261:
+ case 277:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.node = &DurationExpr{
@@ -2121,7 +2218,16 @@ yydefault:
EndPos: yyDollar[3].item.PositionRange().End,
}
}
- case 262:
+ case 278:
+ yyDollar = yyS[yypt-3 : yypt+1]
+ {
+ yyVAL.node = &DurationExpr{
+ Op: RANGE,
+ StartPos: yyDollar[1].item.PositionRange().Start,
+ EndPos: yyDollar[3].item.PositionRange().End,
+ }
+ }
+ case 279:
yyDollar = yyS[yypt-4 : yypt+1]
{
yyVAL.node = &DurationExpr{
@@ -2134,7 +2240,20 @@ yydefault:
StartPos: yyDollar[1].item.Pos,
}
}
- case 263:
+ case 280:
+ yyDollar = yyS[yypt-4 : yypt+1]
+ {
+ yyVAL.node = &DurationExpr{
+ Op: yyDollar[1].item.Typ,
+ RHS: &DurationExpr{
+ Op: RANGE,
+ StartPos: yyDollar[2].item.PositionRange().Start,
+ EndPos: yyDollar[4].item.PositionRange().End,
+ },
+ StartPos: yyDollar[1].item.Pos,
+ }
+ }
+ case 281:
yyDollar = yyS[yypt-6 : yypt+1]
{
yyVAL.node = &DurationExpr{
@@ -2145,7 +2264,7 @@ yydefault:
RHS: yyDollar[5].node.(Expr),
}
}
- case 264:
+ case 282:
yyDollar = yyS[yypt-7 : yypt+1]
{
yyVAL.node = &DurationExpr{
@@ -2161,7 +2280,7 @@ yydefault:
},
}
}
- case 265:
+ case 283:
yyDollar = yyS[yypt-4 : yypt+1]
{
de := yyDollar[3].node.(*DurationExpr)
@@ -2176,7 +2295,7 @@ yydefault:
}
yyVAL.node = yyDollar[3].node
}
- case 269:
+ case 287:
yyDollar = yyS[yypt-1 : yypt+1]
{
nl := yyDollar[1].node.(*NumberLiteral)
@@ -2187,7 +2306,7 @@ yydefault:
}
yyVAL.node = nl
}
- case 270:
+ case 288:
yyDollar = yyS[yypt-2 : yypt+1]
{
switch expr := yyDollar[2].node.(type) {
@@ -2220,25 +2339,25 @@ yydefault:
break
}
}
- case 271:
+ 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 272:
+ 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 273:
+ 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 274:
+ case 292:
yyDollar = yyS[yypt-3 : yypt+1]
{
yylex.(*parser).experimentalDurationExpr(yyDollar[1].node.(Expr))
@@ -2249,7 +2368,7 @@ yydefault:
}
yyVAL.node = &DurationExpr{Op: DIV, LHS: yyDollar[1].node.(Expr), RHS: yyDollar[3].node.(Expr)}
}
- case 275:
+ case 293:
yyDollar = yyS[yypt-3 : yypt+1]
{
yylex.(*parser).experimentalDurationExpr(yyDollar[1].node.(Expr))
@@ -2260,13 +2379,13 @@ yydefault:
}
yyVAL.node = &DurationExpr{Op: MOD, LHS: yyDollar[1].node.(Expr), RHS: yyDollar[3].node.(Expr)}
}
- case 276:
+ 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 277:
+ case 295:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.node = &DurationExpr{
@@ -2275,7 +2394,16 @@ yydefault:
EndPos: yyDollar[3].item.PositionRange().End,
}
}
- case 278:
+ case 296:
+ yyDollar = yyS[yypt-3 : yypt+1]
+ {
+ yyVAL.node = &DurationExpr{
+ Op: RANGE,
+ StartPos: yyDollar[1].item.PositionRange().Start,
+ EndPos: yyDollar[3].item.PositionRange().End,
+ }
+ }
+ case 297:
yyDollar = yyS[yypt-6 : yypt+1]
{
yyVAL.node = &DurationExpr{
@@ -2286,7 +2414,7 @@ yydefault:
RHS: yyDollar[5].node.(Expr),
}
}
- case 280:
+ 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 296b91d1ae..7149985767 100644
--- a/promql/parser/lex.go
+++ b/promql/parser/lex.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
@@ -137,12 +137,16 @@ 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.
"start": START,
"end": END,
"step": STEP,
+ "range": RANGE,
}
var histogramDesc = map[string]ItemType{
@@ -915,6 +919,9 @@ func (l *Lexer) scanDurationKeyword() bool {
case "step":
l.emit(STEP)
return true
+ case "range":
+ l.emit(RANGE)
+ return true
case "min":
l.emit(MIN)
return true
@@ -1079,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)
@@ -1094,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'
}
@@ -1175,7 +1210,7 @@ func lexDurationExpr(l *Lexer) stateFn {
case r == ',':
l.emit(COMMA)
return lexDurationExpr
- case r == 's' || r == 'S' || r == 'm' || r == 'M':
+ case r == 's' || r == 'S' || r == 'm' || r == 'M' || r == 'r' || r == 'R':
if l.scanDurationKeyword() {
return lexDurationExpr
}
diff --git a/promql/parser/lex_test.go b/promql/parser/lex_test.go
index f86f282089..5c915ec74f 100644
--- a/promql/parser/lex_test.go
+++ b/promql/parser/lex_test.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
diff --git a/promql/parser/parse.go b/promql/parser/parse.go
index bcd511f467..ec3e1001d9 100644
--- a/promql/parser/parse.go
+++ b/promql/parser/parse.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
@@ -30,6 +30,7 @@ import (
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/model/timestamp"
"github.com/prometheus/prometheus/promql/parser/posrange"
+ "github.com/prometheus/prometheus/util/features"
"github.com/prometheus/prometheus/util/strutil"
)
@@ -39,15 +40,104 @@ var parserPool = sync.Pool{
},
}
-// ExperimentalDurationExpr is a flag to enable experimental duration expression parsing.
-var ExperimentalDurationExpr bool
-
-// EnableExtendedRangeSelectors is a flag to enable experimental extended range selectors.
-var EnableExtendedRangeSelectors bool
+// Options holds the configuration for the PromQL parser.
+type Options struct {
+ EnableExperimentalFunctions bool
+ ExperimentalDurationExpr bool
+ EnableExtendedRangeSelectors bool
+ EnableBinopFillModifiers bool
+}
+// Parser provides PromQL parsing methods. Create one with NewParser.
type Parser interface {
- ParseExpr() (Expr, error)
- Close()
+ ParseExpr(input string) (Expr, error)
+ ParseMetric(input string) (labels.Labels, error)
+ ParseMetricSelector(input string) ([]*labels.Matcher, error)
+ ParseMetricSelectors(matchers []string) ([][]*labels.Matcher, error)
+ ParseSeriesDesc(input string) (labels.Labels, []SequenceValue, error)
+ RegisterFeatures(r features.Collector)
+}
+
+type promQLParser struct {
+ options Options
+}
+
+// NewParser returns a new PromQL Parser configured with the given options.
+func NewParser(opts Options) Parser {
+ return &promQLParser{options: opts}
+}
+
+func (pql *promQLParser) ParseExpr(input string) (Expr, error) {
+ p := newParser(input, pql.options)
+ defer p.Close()
+ return p.parseExpr()
+}
+
+func (pql *promQLParser) ParseMetric(input string) (m labels.Labels, err error) {
+ p := newParser(input, pql.options)
+ defer p.Close()
+ defer p.recover(&err)
+
+ parseResult := p.parseGenerated(START_METRIC)
+ if parseResult != nil {
+ m = parseResult.(labels.Labels)
+ }
+
+ if len(p.parseErrors) != 0 {
+ err = p.parseErrors
+ }
+
+ return m, err
+}
+
+func (pql *promQLParser) ParseMetricSelector(input string) (m []*labels.Matcher, err error) {
+ p := newParser(input, pql.options)
+ defer p.Close()
+ defer p.recover(&err)
+
+ parseResult := p.parseGenerated(START_METRIC_SELECTOR)
+ if parseResult != nil {
+ m = parseResult.(*VectorSelector).LabelMatchers
+ }
+
+ if len(p.parseErrors) != 0 {
+ err = p.parseErrors
+ }
+
+ return m, err
+}
+
+func (pql *promQLParser) ParseMetricSelectors(matchers []string) ([][]*labels.Matcher, error) {
+ var matcherSets [][]*labels.Matcher
+ for _, s := range matchers {
+ ms, err := pql.ParseMetricSelector(s)
+ if err != nil {
+ return nil, err
+ }
+ matcherSets = append(matcherSets, ms)
+ }
+ return matcherSets, nil
+}
+
+func (pql *promQLParser) ParseSeriesDesc(input string) (lbls labels.Labels, values []SequenceValue, err error) {
+ p := newParser(input, pql.options)
+ p.lex.seriesDesc = true
+
+ defer p.Close()
+ defer p.recover(&err)
+
+ parseResult := p.parseGenerated(START_SERIES_DESCRIPTION)
+ if parseResult != nil {
+ result := parseResult.(*seriesDescription)
+ lbls = result.labels
+ values = result.values
+ }
+
+ if len(p.parseErrors) != 0 {
+ err = p.parseErrors
+ }
+
+ return lbls, values, err
}
type parser struct {
@@ -67,18 +157,17 @@ 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
+
+ options Options
}
-type Opt func(p *parser)
-
-func WithFunctions(functions map[string]*Function) Opt {
- return func(p *parser) {
- p.functions = functions
- }
-}
-
-// NewParser returns a new parser.
-func NewParser(input string, opts ...Opt) *parser { //nolint:revive // unexported-return
+// newParser returns a new low-level parser instance from the pool.
+func newParser(input string, opts Options) *parser {
p := parserPool.Get().(*parser)
p.functions = Functions
@@ -86,6 +175,7 @@ func NewParser(input string, opts ...Opt) *parser { //nolint:revive // unexporte
p.parseErrors = nil
p.generatedParserResult = nil
p.lastClosing = posrange.Pos(0)
+ p.options = opts
// Clear lexer struct before reusing.
p.lex = Lexer{
@@ -93,15 +183,17 @@ func NewParser(input string, opts ...Opt) *parser { //nolint:revive // unexporte
state: lexStatements,
}
- // Apply user define options.
- for _, opt := range opts {
- opt(p)
- }
-
return p
}
-func (p *parser) ParseExpr() (expr Expr, err error) {
+// newParserWithFunctions returns a new low-level parser instance with custom functions.
+func newParserWithFunctions(input string, opts Options, functions map[string]*Function) *parser {
+ p := newParser(input, opts)
+ p.functions = functions
+ return p
+}
+
+func (p *parser) parseExpr() (expr Expr, err error) {
defer p.recover(&err)
parseResult := p.parseGenerated(START_EXPRESSION)
@@ -171,69 +263,16 @@ func EnrichParseError(err error, enrich func(parseErr *ParseErr)) {
}
}
-// ParseExpr returns the expression parsed from the input.
-func ParseExpr(input string) (expr Expr, err error) {
- p := NewParser(input)
- defer p.Close()
- return p.ParseExpr()
-}
-
-// ParseMetric parses the input into a metric.
-func ParseMetric(input string) (m labels.Labels, err error) {
- p := NewParser(input)
- defer p.Close()
- defer p.recover(&err)
-
- parseResult := p.parseGenerated(START_METRIC)
- if parseResult != nil {
- m = parseResult.(labels.Labels)
- }
-
- if len(p.parseErrors) != 0 {
- err = p.parseErrors
- }
-
- return m, err
-}
-
-// ParseMetricSelector parses the provided textual metric selector into a list of
-// label matchers.
-func ParseMetricSelector(input string) (m []*labels.Matcher, err error) {
- p := NewParser(input)
- defer p.Close()
- defer p.recover(&err)
-
- parseResult := p.parseGenerated(START_METRIC_SELECTOR)
- if parseResult != nil {
- m = parseResult.(*VectorSelector).LabelMatchers
- }
-
- if len(p.parseErrors) != 0 {
- err = p.parseErrors
- }
-
- return m, err
-}
-
-// ParseMetricSelectors parses a list of provided textual metric selectors into lists of
-// label matchers.
-func ParseMetricSelectors(matchers []string) (m [][]*labels.Matcher, err error) {
- var matcherSets [][]*labels.Matcher
- for _, s := range matchers {
- matchers, err := ParseMetricSelector(s)
- if err != nil {
- return nil, err
- }
- matcherSets = append(matcherSets, matchers)
- }
- return matcherSets, nil
-}
-
// SequenceValue is an omittable value in a sequence of time series values.
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 {
@@ -251,30 +290,6 @@ type seriesDescription struct {
values []SequenceValue
}
-// ParseSeriesDesc parses the description of a time series. It is only used in
-// the PromQL testing framework code.
-func ParseSeriesDesc(input string) (labels labels.Labels, values []SequenceValue, err error) {
- p := NewParser(input)
- p.lex.seriesDesc = true
-
- defer p.Close()
- defer p.recover(&err)
-
- parseResult := p.parseGenerated(START_SERIES_DESCRIPTION)
- if parseResult != nil {
- result := parseResult.(*seriesDescription)
-
- labels = result.labels
- values = result.values
- }
-
- if len(p.parseErrors) != 0 {
- err = p.parseErrors
- }
-
- return labels, values, err
-}
-
// addParseErrf formats the error and appends it to the list of parsing errors.
func (p *parser) addParseErrf(positionRange posrange.PositionRange, format string, args ...any) {
p.addParseErr(positionRange, fmt.Errorf(format, args...))
@@ -413,13 +428,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 !p.options.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
}
@@ -458,7 +478,7 @@ func (p *parser) newAggregateExpr(op Item, modifier, args Node, overread bool) (
desiredArgs := 1
if ret.Op.IsAggregatorWithParam() {
- if !EnableExperimentalFunctions && ret.Op.IsExperimentalAggregator() {
+ if !p.options.EnableExperimentalFunctions && ret.Op.IsExperimentalAggregator() {
p.addParseErrf(ret.PositionRange(), "%s() is experimental and must be enabled with --enable-feature=promql-experimental-functions", ret.Op)
return ret
}
@@ -496,25 +516,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 +551,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 +560,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 +622,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 +655,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 +807,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 +818,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() {
@@ -1030,7 +1075,7 @@ func (p *parser) addOffsetExpr(e Node, expr *DurationExpr) {
}
func (p *parser) setAnchored(e Node) {
- if !EnableExtendedRangeSelectors {
+ if !p.options.EnableExtendedRangeSelectors {
p.addParseErrf(e.PositionRange(), "anchored modifier is experimental and not enabled")
return
}
@@ -1053,7 +1098,7 @@ func (p *parser) setAnchored(e Node) {
}
func (p *parser) setSmoothed(e Node) {
- if !EnableExtendedRangeSelectors {
+ if !p.options.EnableExtendedRangeSelectors {
p.addParseErrf(e.PositionRange(), "smoothed modifier is experimental and not enabled")
return
}
@@ -1149,7 +1194,7 @@ func (p *parser) getAtModifierVars(e Node) (**int64, *ItemType, *posrange.Pos, b
}
func (p *parser) experimentalDurationExpr(e Expr) {
- if !ExperimentalDurationExpr {
+ if !p.options.ExperimentalDurationExpr {
p.addParseErrf(e.PositionRange(), "experimental duration expression is not enabled")
}
}
diff --git a/promql/parser/parse_test.go b/promql/parser/parse_test.go
index b5d7c288d1..f5b2e2dff0 100644
--- a/promql/parser/parse_test.go
+++ b/promql/parser/parse_test.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
@@ -31,6 +31,8 @@ import (
"github.com/prometheus/prometheus/util/testutil"
)
+var testParser = NewParser(Options{})
+
func repeatError(query string, err error, start, startStep, end, endStep, count int) (errs ParseErrors) {
for i := range count {
errs = append(errs, ParseErr{
@@ -2708,7 +2710,7 @@ var testExpr = []struct {
errors: ParseErrors{
ParseErr{
PositionRange: posrange.PositionRange{Start: 4, End: 5},
- Err: errors.New("unexpected \"]\" in subquery or range selector, expected number, duration, or step()"),
+ Err: errors.New("unexpected \"]\" in subquery or range selector, expected number, duration, step(), or range()"),
Query: `foo[]`,
},
},
@@ -2741,7 +2743,7 @@ var testExpr = []struct {
errors: ParseErrors{
ParseErr{
PositionRange: posrange.PositionRange{Start: 22, End: 22},
- Err: errors.New("unexpected end of input in offset, expected number, duration, or step()"),
+ Err: errors.New("unexpected end of input in offset, expected number, duration, step(), or range()"),
Query: `some_metric[5m] OFFSET`,
},
},
@@ -4698,6 +4700,100 @@ var testExpr = []struct {
},
},
},
+ {
+ input: `foo[range()]`,
+ expected: &MatrixSelector{
+ VectorSelector: &VectorSelector{
+ Name: "foo",
+ LabelMatchers: []*labels.Matcher{
+ MustLabelMatcher(labels.MatchEqual, model.MetricNameLabel, "foo"),
+ },
+ PosRange: posrange.PositionRange{Start: 0, End: 3},
+ },
+ RangeExpr: &DurationExpr{
+ Op: RANGE,
+ StartPos: 4,
+ EndPos: 11,
+ },
+ EndPos: 12,
+ },
+ },
+ {
+ input: `foo[-range()]`,
+ expected: &MatrixSelector{
+ VectorSelector: &VectorSelector{
+ Name: "foo",
+ LabelMatchers: []*labels.Matcher{
+ MustLabelMatcher(labels.MatchEqual, model.MetricNameLabel, "foo"),
+ },
+ PosRange: posrange.PositionRange{Start: 0, End: 3},
+ },
+ RangeExpr: &DurationExpr{
+ Op: SUB,
+ StartPos: 4,
+ RHS: &DurationExpr{Op: RANGE, StartPos: 5, EndPos: 12},
+ },
+ EndPos: 13,
+ },
+ },
+ {
+ input: `foo offset range()`,
+ expected: &VectorSelector{
+ Name: "foo",
+ LabelMatchers: []*labels.Matcher{
+ MustLabelMatcher(labels.MatchEqual, model.MetricNameLabel, "foo"),
+ },
+ PosRange: posrange.PositionRange{Start: 0, End: 18},
+ OriginalOffsetExpr: &DurationExpr{
+ Op: RANGE,
+ StartPos: 11,
+ EndPos: 18,
+ },
+ },
+ },
+ {
+ input: `foo offset -range()`,
+ expected: &VectorSelector{
+ Name: "foo",
+ LabelMatchers: []*labels.Matcher{
+ MustLabelMatcher(labels.MatchEqual, model.MetricNameLabel, "foo"),
+ },
+ PosRange: posrange.PositionRange{Start: 0, End: 19},
+ OriginalOffsetExpr: &DurationExpr{
+ Op: SUB,
+ RHS: &DurationExpr{Op: RANGE, StartPos: 12, EndPos: 19},
+ StartPos: 11,
+ },
+ },
+ },
+ {
+ input: `foo[max(range(),5s)]`,
+ expected: &MatrixSelector{
+ VectorSelector: &VectorSelector{
+ Name: "foo",
+ LabelMatchers: []*labels.Matcher{
+ MustLabelMatcher(labels.MatchEqual, model.MetricNameLabel, "foo"),
+ },
+ PosRange: posrange.PositionRange{Start: 0, End: 3},
+ },
+ RangeExpr: &DurationExpr{
+ Op: MAX,
+ LHS: &DurationExpr{
+ Op: RANGE,
+ StartPos: 8,
+ EndPos: 15,
+ },
+ RHS: &NumberLiteral{
+ Val: 5,
+ Duration: true,
+ PosRange: posrange.PositionRange{Start: 16, End: 18},
+ },
+ StartPos: 4,
+ EndPos: 19,
+ },
+ EndPos: 20,
+ },
+ },
{
input: `foo[4s+4s:1s*2] offset (5s-8)`,
expected: &SubqueryExpr{
@@ -4942,7 +5038,7 @@ var testExpr = []struct {
errors: ParseErrors{
ParseErr{
PositionRange: posrange.PositionRange{Start: 8, End: 9},
- Err: errors.New(`unexpected "]" in subquery or range selector, expected number, duration, or step()`),
+ Err: errors.New(`unexpected "]" in subquery or range selector, expected number, duration, step(), or range()`),
Query: `foo[step]`,
},
},
@@ -5203,18 +5299,14 @@ func readable(s string) string {
}
func TestParseExpressions(t *testing.T) {
- // Enable experimental functions testing.
- EnableExperimentalFunctions = true
- // Enable experimental duration expression parsing.
- ExperimentalDurationExpr = true
- t.Cleanup(func() {
- EnableExperimentalFunctions = false
- ExperimentalDurationExpr = false
+ optsParser := NewParser(Options{
+ EnableExperimentalFunctions: true,
+ ExperimentalDurationExpr: true,
})
for _, test := range testExpr {
t.Run(readable(test.input), func(t *testing.T) {
- expr, err := ParseExpr(test.input)
+ expr, err := optsParser.ParseExpr(test.input)
// Unexpected errors are always caused by a bug.
require.NotEqual(t, err, errUnexpected, "unexpected error occurred")
@@ -5342,7 +5434,7 @@ func TestParseSeriesDesc(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
- l, v, err := ParseSeriesDesc(tc.input)
+ l, v, err := testParser.ParseSeriesDesc(tc.input)
if tc.expectError != "" {
require.Contains(t, err.Error(), tc.expectError)
} else {
@@ -5356,7 +5448,7 @@ func TestParseSeriesDesc(t *testing.T) {
// NaN has no equality. Thus, we need a separate test for it.
func TestNaNExpression(t *testing.T) {
- expr, err := ParseExpr("NaN")
+ expr, err := testParser.ParseExpr("NaN")
require.NoError(t, err)
nl, ok := expr.(*NumberLiteral)
@@ -5784,7 +5876,7 @@ func TestParseHistogramSeries(t *testing.T) {
},
} {
t.Run(test.name, func(t *testing.T) {
- _, vals, err := ParseSeriesDesc(test.input)
+ _, vals, err := testParser.ParseSeriesDesc(test.input)
if test.expectedError != "" {
require.EqualError(t, err, test.expectedError)
return
@@ -5856,7 +5948,7 @@ func TestHistogramTestExpression(t *testing.T) {
t.Run(test.name, func(t *testing.T) {
expression := test.input.TestExpression()
require.Equal(t, test.expected, expression)
- _, vals, err := ParseSeriesDesc("{} " + expression)
+ _, vals, err := testParser.ParseSeriesDesc("{} " + expression)
require.NoError(t, err)
require.Len(t, vals, 1)
canonical := vals[0].Histogram
@@ -5868,7 +5960,7 @@ func TestHistogramTestExpression(t *testing.T) {
func TestParseSeries(t *testing.T) {
for _, test := range testSeries {
- metric, vals, err := ParseSeriesDesc(test.input)
+ metric, vals, err := testParser.ParseSeriesDesc(test.input)
// Unexpected errors are always caused by a bug.
require.NotEqual(t, err, errUnexpected, "unexpected error occurred")
@@ -5884,7 +5976,7 @@ func TestParseSeries(t *testing.T) {
}
func TestRecoverParserRuntime(t *testing.T) {
- p := NewParser("foo bar")
+ p := newParser("foo bar", Options{})
var err error
defer func() {
@@ -5897,7 +5989,7 @@ func TestRecoverParserRuntime(t *testing.T) {
}
func TestRecoverParserError(t *testing.T) {
- p := NewParser("foo bar")
+ p := newParser("foo bar", Options{})
var err error
e := errors.New("custom error")
@@ -5932,12 +6024,12 @@ func TestExtractSelectors(t *testing.T) {
[]string{},
},
} {
- expr, err := ParseExpr(tc.input)
+ expr, err := testParser.ParseExpr(tc.input)
require.NoError(t, err)
var expected [][]*labels.Matcher
for _, s := range tc.expected {
- selector, err := ParseMetricSelector(s)
+ selector, err := testParser.ParseMetricSelector(s)
require.NoError(t, err)
expected = append(expected, selector)
}
@@ -5954,11 +6046,37 @@ func TestParseCustomFunctions(t *testing.T) {
ReturnType: ValueTypeVector,
}
input := "custom_func(metric[1m])"
- p := NewParser(input, WithFunctions(funcs))
- expr, err := p.ParseExpr()
+ p := newParserWithFunctions(input, Options{}, funcs)
+ expr, err := p.parseExpr()
require.NoError(t, err)
call, ok := expr.(*Call)
require.True(t, ok)
require.Equal(t, "custom_func", call.Func.Name)
}
+
+func TestNewParser(t *testing.T) {
+ p := NewParser(Options{
+ EnableExperimentalFunctions: true,
+ ExperimentalDurationExpr: true,
+ })
+
+ // ParseExpr should work.
+ expr, err := p.ParseExpr("up")
+ require.NoError(t, err)
+ require.NotNil(t, expr)
+
+ // ParseMetricSelector should work.
+ matchers, err := p.ParseMetricSelector(`{job="prometheus"}`)
+ require.NoError(t, err)
+ require.Len(t, matchers, 1)
+
+ // ParseMetricSelectors should work.
+ matcherSets, err := p.ParseMetricSelectors([]string{`{job="prometheus"}`, `{job="grafana"}`})
+ require.NoError(t, err)
+ require.Len(t, matcherSets, 2)
+
+ // Invalid input should return errors.
+ _, err = p.ParseExpr("===")
+ require.Error(t, err)
+}
diff --git a/promql/parser/posrange/posrange.go b/promql/parser/posrange/posrange.go
index f883a91bbb..c5cdc4b91b 100644
--- a/promql/parser/posrange/posrange.go
+++ b/promql/parser/posrange/posrange.go
@@ -1,4 +1,4 @@
-// Copyright 2023 The Prometheus Authors
+// 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
diff --git a/promql/parser/prettier.go b/promql/parser/prettier.go
index 90fb7a0cf9..a0ab9e1219 100644
--- a/promql/parser/prettier.go
+++ b/promql/parser/prettier.go
@@ -1,4 +1,4 @@
-// Copyright 2022 The Prometheus Authors
+// 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
diff --git a/promql/parser/prettier_test.go b/promql/parser/prettier_test.go
index ea9a7a1a26..d00bc283ec 100644
--- a/promql/parser/prettier_test.go
+++ b/promql/parser/prettier_test.go
@@ -1,4 +1,4 @@
-// Copyright 2022 The Prometheus Authors
+// 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
@@ -114,7 +114,7 @@ task:errors:rate10s{job="s"}))`,
},
}
for _, test := range inputs {
- expr, err := ParseExpr(test.in)
+ expr, err := testParser.ParseExpr(test.in)
require.NoError(t, err)
require.Equal(t, test.out, Prettify(expr))
@@ -185,7 +185,7 @@ func TestBinaryExprPretty(t *testing.T) {
}
for _, test := range inputs {
t.Run(test.in, func(t *testing.T) {
- expr, err := ParseExpr(test.in)
+ expr, err := testParser.ParseExpr(test.in)
require.NoError(t, err)
require.Equal(t, test.out, Prettify(expr))
@@ -261,7 +261,7 @@ func TestCallExprPretty(t *testing.T) {
},
}
for _, test := range inputs {
- expr, err := ParseExpr(test.in)
+ expr, err := testParser.ParseExpr(test.in)
require.NoError(t, err)
fmt.Println("=>", expr.String())
@@ -308,7 +308,7 @@ func TestParenExprPretty(t *testing.T) {
},
}
for _, test := range inputs {
- expr, err := ParseExpr(test.in)
+ expr, err := testParser.ParseExpr(test.in)
require.NoError(t, err)
require.Equal(t, test.out, Prettify(expr))
@@ -334,7 +334,7 @@ func TestStepInvariantExpr(t *testing.T) {
},
}
for _, test := range inputs {
- expr, err := ParseExpr(test.in)
+ expr, err := testParser.ParseExpr(test.in)
require.NoError(t, err)
require.Equal(t, test.out, Prettify(expr))
@@ -594,7 +594,7 @@ or
},
}
for _, test := range inputs {
- expr, err := ParseExpr(test.in)
+ expr, err := testParser.ParseExpr(test.in)
require.NoError(t, err)
require.Equal(t, test.out, Prettify(expr))
}
@@ -662,7 +662,7 @@ func TestUnaryPretty(t *testing.T) {
}
for _, test := range inputs {
t.Run(test.in, func(t *testing.T) {
- expr, err := ParseExpr(test.in)
+ expr, err := testParser.ParseExpr(test.in)
require.NoError(t, err)
require.Equal(t, test.out, Prettify(expr))
})
@@ -670,11 +670,7 @@ func TestUnaryPretty(t *testing.T) {
}
func TestDurationExprPretty(t *testing.T) {
- // Enable experimental duration expression parsing.
- ExperimentalDurationExpr = true
- t.Cleanup(func() {
- ExperimentalDurationExpr = false
- })
+ optsParser := NewParser(Options{ExperimentalDurationExpr: true})
maxCharactersPerLine = 10
inputs := []struct {
in, out string
@@ -700,7 +696,7 @@ func TestDurationExprPretty(t *testing.T) {
}
for _, test := range inputs {
t.Run(test.in, func(t *testing.T) {
- expr, err := ParseExpr(test.in)
+ expr, err := optsParser.ParseExpr(test.in)
require.NoError(t, err)
require.Equal(t, test.out, Prettify(expr))
})
diff --git a/promql/parser/printer.go b/promql/parser/printer.go
index a562b88044..cc5c931975 100644
--- a/promql/parser/printer.go
+++ b/promql/parser/printer.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
@@ -37,15 +37,16 @@ func tree(node Node, level string) string {
}
typs := strings.Split(fmt.Sprintf("%T", node), ".")[1]
- t := fmt.Sprintf("%s |---- %s :: %s\n", level, typs, node)
+ var t strings.Builder
+ fmt.Fprintf(&t, "%s |---- %s :: %s\n", level, typs, node)
level += " · · ·"
for e := range ChildrenIter(node) {
- t += tree(e, level)
+ t.WriteString(tree(e, level))
}
- return t
+ return t.String()
}
func (node *EvalStmt) String() string {
@@ -108,7 +109,7 @@ func writeLabels(b *bytes.Buffer, ss []string) {
if i > 0 {
b.WriteString(", ")
}
- if !model.LegacyValidation.IsValidMetricName(s) {
+ if !model.LegacyValidation.IsValidLabelName(s) {
b.Write(strconv.AppendQuote(b.AvailableBuffer(), s))
} else {
b.WriteString(s)
@@ -146,20 +147,43 @@ func (node *BinaryExpr) ShortString() string {
func (node *BinaryExpr) getMatchingStr() string {
matching := ""
+ var b bytes.Buffer
vm := node.VectorMatching
- if vm != nil && (len(vm.MatchingLabels) > 0 || vm.On) {
- vmTag := "ignoring"
- if vm.On {
- vmTag = "on"
+ if vm != nil {
+ if len(vm.MatchingLabels) > 0 || vm.On || vm.Card == CardManyToOne || vm.Card == CardOneToMany {
+ vmTag := "ignoring"
+ if vm.On {
+ vmTag = "on"
+ }
+ b.WriteString(" " + vmTag + " (")
+ writeLabels(&b, vm.MatchingLabels)
+ b.WriteString(")")
+ matching = b.String()
}
- matching = fmt.Sprintf(" %s (%s)", vmTag, strings.Join(vm.MatchingLabels, ", "))
if vm.Card == CardManyToOne || vm.Card == CardOneToMany {
vmCard := "right"
if vm.Card == CardManyToOne {
vmCard = "left"
}
- matching += fmt.Sprintf(" group_%s (%s)", vmCard, strings.Join(vm.Include, ", "))
+ b.Reset()
+ b.WriteString(" group_" + vmCard + " (")
+ writeLabels(&b, vm.Include)
+ 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
@@ -179,6 +203,8 @@ func (node *DurationExpr) writeTo(b *bytes.Buffer) {
switch {
case node.Op == STEP:
b.WriteString("step()")
+ case node.Op == RANGE:
+ b.WriteString("range()")
case node.Op == MIN:
b.WriteString("min(")
b.WriteString(node.LHS.String())
diff --git a/promql/parser/printer_test.go b/promql/parser/printer_test.go
index aadfd5688a..eae91d4f88 100644
--- a/promql/parser/printer_test.go
+++ b/promql/parser/printer_test.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
@@ -22,9 +22,10 @@ import (
)
func TestExprString(t *testing.T) {
- ExperimentalDurationExpr = true
- t.Cleanup(func() {
- ExperimentalDurationExpr = false
+ optsParser := NewParser(Options{
+ ExperimentalDurationExpr: true,
+ EnableExtendedRangeSelectors: true,
+ EnableBinopFillModifiers: true,
})
// A list of valid expressions that are expected to be
// returned as out when calling String(). If out is empty the output
@@ -94,6 +95,45 @@ func TestExprString(t *testing.T) {
in: `a - ignoring() c`,
out: `a - c`,
},
+ {
+ // This is a bit of an odd case, but valid. If the user specifies ignoring() with
+ // no labels, it means that both label sets have to be exactly the same on both
+ // sides (except for the metric name). This is the same behavior as specifying
+ // no matching modifier at all, but if the user wants to include the metric name
+ // from either side in the output via group_x(__name__), they have to specify
+ // ignoring() explicitly to be able to do so, since the grammar does not allow
+ // grouping modifiers without either ignoring(...) or on(...). So we need to
+ // preserve the empty ignoring() clause in this case.
+ //
+ // a - group_left(__name__) c <--- Parse error
+ // a - ignoring() group_left(__name__) c <--- Valid
+ in: `a - ignoring() group_left(__metric__) c`,
+ out: `a - ignoring () group_left (__metric__) c`,
+ },
+ {
+ 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`,
},
@@ -247,19 +287,41 @@ func TestExprString(t *testing.T) {
{
in: "foo[200 - min(step() + 10s, -max(step() ^ 2, 3))]",
},
+ {
+ in: "foo[range()]",
+ },
+ {
+ in: "foo[-range()]",
+ },
+ {
+ in: "foo offset range()",
+ },
+ {
+ in: "foo offset -range()",
+ },
+ {
+ in: "foo[max(range(), 5s)]",
+ },
{
in: `predict_linear(foo[1h], 3000)`,
},
+ {
+ in: `sum by("üüü") (foo)`,
+ out: `sum by ("üüü") (foo)`,
+ },
+ {
+ in: `sum without("äää") (foo)`,
+ out: `sum without ("äää") (foo)`,
+ },
+ {
+ in: `count by("ööö", job) (foo)`,
+ out: `count by ("ööö", job) (foo)`,
+ },
}
- EnableExtendedRangeSelectors = true
- t.Cleanup(func() {
- EnableExtendedRangeSelectors = false
- })
-
for _, test := range inputs {
t.Run(test.in, func(t *testing.T) {
- expr, err := ParseExpr(test.in)
+ expr, err := optsParser.ParseExpr(test.in)
require.NoError(t, err)
exp := test.in
@@ -284,7 +346,7 @@ func BenchmarkExprString(b *testing.B) {
for _, test := range inputs {
b.Run(readable(test), func(b *testing.B) {
- expr, err := ParseExpr(test)
+ expr, err := testParser.ParseExpr(test)
require.NoError(b, err)
for b.Loop() {
_ = expr.String()
@@ -375,3 +437,55 @@ func TestVectorSelector_String(t *testing.T) {
})
}
}
+
+func TestBinaryExprUTF8Labels(t *testing.T) {
+ testCases := []struct {
+ name string
+ input string
+ expected string
+ }{
+ {
+ name: "UTF-8 labels in on clause",
+ input: `foo / on("äää") bar`,
+ expected: `foo / on ("äää") bar`,
+ },
+ {
+ name: "UTF-8 labels in ignoring clause",
+ input: `foo / ignoring("üüü") bar`,
+ expected: `foo / ignoring ("üüü") bar`,
+ },
+ {
+ name: "UTF-8 labels in group_left clause",
+ input: `foo / on("äää") group_left("ööö") bar`,
+ expected: `foo / on ("äää") group_left ("ööö") bar`,
+ },
+ {
+ name: "UTF-8 labels in group_right clause",
+ input: `foo / on("äää") group_right("ööö") bar`,
+ expected: `foo / on ("äää") group_right ("ööö") bar`,
+ },
+ {
+ name: "Mixed legacy and UTF-8 labels",
+ input: `foo / on(legacy, "üüü") bar`,
+ expected: `foo / on (legacy, "üüü") bar`,
+ },
+ {
+ name: "Legacy labels only (should not quote)",
+ input: `foo / on(job, instance) bar`,
+ expected: `foo / on (job, instance) bar`,
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ expr, err := testParser.ParseExpr(tc.input)
+ if err != nil {
+ t.Fatalf("Failed to parse: %v", err)
+ }
+ result := expr.String()
+ if result != tc.expected {
+ t.Errorf("Expected: %s\nGot: %s", tc.expected, result)
+ }
+ })
+ }
+}
diff --git a/promql/parser/value.go b/promql/parser/value.go
index f882f9f0be..3c1c8571dc 100644
--- a/promql/parser/value.go
+++ b/promql/parser/value.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
diff --git a/promql/promql_test.go b/promql/promql_test.go
index 92d933f1ee..01189f6e57 100644
--- a/promql/promql_test.go
+++ b/promql/promql_test.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
@@ -39,20 +39,17 @@ func TestEvaluations(t *testing.T) {
// Run a lot of queries at the same time, to check for race conditions.
func TestConcurrentRangeQueries(t *testing.T) {
stor := teststorage.New(t)
- defer stor.Close()
+
opts := promql.EngineOpts{
Logger: nil,
Reg: nil,
MaxSamples: 50000000,
Timeout: 100 * time.Second,
+ Parser: parser.NewParser(parser.Options{
+ EnableExperimentalFunctions: true,
+ EnableExtendedRangeSelectors: true,
+ }),
}
- // Enable experimental functions testing
- parser.EnableExperimentalFunctions = true
- parser.EnableExtendedRangeSelectors = true
- t.Cleanup(func() {
- parser.EnableExperimentalFunctions = false
- parser.EnableExtendedRangeSelectors = false
- })
engine := promqltest.NewTestEngineWithOpts(t, opts)
const interval = 10000 // 10s interval.
diff --git a/promql/promqltest/README.md b/promql/promqltest/README.md
index d26c01c6f1..b4efd9c128 100644
--- a/promql/promqltest/README.md
+++ b/promql/promqltest/README.md
@@ -110,6 +110,15 @@ eval range from to step
* ` ""` (optional) for matching a string literal
* `` and `` specify the expected values, and follow the same syntax as for `load` above
+### Special handling of counter reset hints in native histograms
+
+Native histograms as part of `` may or may not contain an explicit
+`counter_reset_hint` property. If a `counter_reset_hint` is provided
+explicitly, the counter reset hint of the histogram is tested to have the
+provided value (`unknown`, `reset`, `not_reset`, or `gauge`). However, if no
+`counter_reset_hint` is specified, the `counter_reset_hint` is not tested at
+all (rather than testing for the usual default value `unknown`).
+
### `expect string`
This can be used to specify that a string literal is the expected result.
diff --git a/promql/promqltest/cmd/migrate/main.go b/promql/promqltest/cmd/migrate/main.go
index a506f084c5..b570b1dfaa 100644
--- a/promql/promqltest/cmd/migrate/main.go
+++ b/promql/promqltest/cmd/migrate/main.go
@@ -1,4 +1,4 @@
-// Copyright 2025 The Prometheus Authors
+// 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
diff --git a/promql/promqltest/test.go b/promql/promqltest/test.go
index 41d8cdde20..a634a194fb 100644
--- a/promql/promqltest/test.go
+++ b/promql/promqltest/test.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
@@ -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)
@@ -87,6 +86,14 @@ func LoadedStorage(t testutil.T, input string) *teststorage.TestStorage {
return test.storage.(*teststorage.TestStorage)
}
+// TestParserOpts are the parser options used for all built-in test engines.
+var TestParserOpts = parser.Options{
+ EnableExperimentalFunctions: true,
+ ExperimentalDurationExpr: true,
+ EnableExtendedRangeSelectors: true,
+ EnableBinopFillModifiers: true,
+}
+
// NewTestEngine creates a promql.Engine with enablePerStepStats, lookbackDelta and maxSamples, and returns it.
func NewTestEngine(tb testing.TB, enablePerStepStats bool, lookbackDelta time.Duration, maxSamples int) *promql.Engine {
return NewTestEngineWithOpts(tb, promql.EngineOpts{
@@ -100,6 +107,7 @@ func NewTestEngine(tb testing.TB, enablePerStepStats bool, lookbackDelta time.Du
EnablePerStepStats: enablePerStepStats,
LookbackDelta: lookbackDelta,
EnableDelayedNameRemoval: true,
+ Parser: parser.NewParser(TestParserOpts),
})
}
@@ -113,22 +121,47 @@ 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) {
- t.Cleanup(func() {
- parser.EnableExperimentalFunctions = false
- parser.ExperimentalDurationExpr = false
- parser.EnableExtendedRangeSelectors = false
- })
- parser.EnableExperimentalFunctions = true
- parser.ExperimentalDurationExpr = true
- parser.EnableExtendedRangeSelectors = true
-
+// The engine must be created with ParserOptions that enable all experimental features used in the test files.
+func RunBuiltinTestsWithStorage(t TBRun, engine promql.QueryEngine, newStorage func(testing.TB) storage.Storage) {
files, err := fs.Glob(testsFs, "*/*.test")
require.NoError(t, err)
@@ -142,22 +175,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 +225,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 +240,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 +253,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
@@ -231,7 +265,7 @@ func raise(line int, format string, v ...any) error {
}
}
-func parseLoad(lines []string, i int) (int, *loadCmd, error) {
+func parseLoad(lines []string, i int, startTime time.Time) (int, *loadCmd, error) {
if !patLoad.MatchString(lines[i]) {
return i, nil, raise(i, "invalid load command. (load[_with_nhcb] )")
}
@@ -245,6 +279,7 @@ func parseLoad(lines []string, i int) (int, *loadCmd, error) {
return i, nil, raise(i, "invalid step definition %q: %s", step, err)
}
cmd := newLoadCmd(time.Duration(gap), withNHCB)
+ cmd.startTime = startTime
for i+1 < len(lines) {
i++
defLine := lines[i]
@@ -262,7 +297,8 @@ func parseLoad(lines []string, i int) (int, *loadCmd, error) {
}
func parseSeries(defLine string, line int) (labels.Labels, []parser.SequenceValue, error) {
- metric, vals, err := parser.ParseSeriesDesc(defLine)
+ testParser := parser.NewParser(TestParserOpts)
+ metric, vals, err := testParser.ParseSeriesDesc(defLine)
if err != nil {
parser.EnrichParseError(err, func(parseErr *parser.ParseErr) {
parseErr.LineOffset = line
@@ -391,7 +427,7 @@ func (t *test) parseEval(lines []string, i int) (int, *evalCmd, error) {
expr = rangeParts[5]
}
- _, err := parser.ParseExpr(expr)
+ _, err := parserForBuiltinTests.ParseExpr(expr)
if err != nil {
parser.EnrichParseError(err, func(parseErr *parser.ParseErr) {
parseErr.LineOffset = i
@@ -579,7 +615,7 @@ func (t *test) parse(input string) error {
case c == "clear":
cmd = &clearCmd{}
case strings.HasPrefix(c, "load"):
- i, cmd, err = parseLoad(lines, i)
+ i, cmd, err = parseLoad(lines, i, testStartTime)
case strings.HasPrefix(c, "eval"):
i, cmd, err = t.parseEval(lines, i)
default:
@@ -611,6 +647,7 @@ type loadCmd struct {
defs map[uint64][]promql.Sample
exemplars map[uint64][]exemplar.Exemplar
withNHCB bool
+ startTime time.Time
}
func newLoadCmd(gap time.Duration, withNHCB bool) *loadCmd {
@@ -620,6 +657,7 @@ func newLoadCmd(gap time.Duration, withNHCB bool) *loadCmd {
defs: map[uint64][]promql.Sample{},
exemplars: map[uint64][]exemplar.Exemplar{},
withNHCB: withNHCB,
+ startTime: testStartTime,
}
}
@@ -632,7 +670,7 @@ func (cmd *loadCmd) set(m labels.Labels, vals ...parser.SequenceValue) {
h := m.Hash()
samples := make([]promql.Sample, 0, len(vals))
- ts := testStartTime
+ ts := cmd.startTime
for _, v := range vals {
if !v.Omitted {
samples = append(samples, promql.Sample{
@@ -1009,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)
@@ -1021,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})
}
@@ -1050,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))
}
}
@@ -1089,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) {
@@ -1127,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
}
@@ -1163,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
}
@@ -1306,8 +1363,13 @@ type atModifierTestCase struct {
evalTime time.Time
}
+// parserForBuiltinTests is the parser used when parsing expressions in the
+// built-in test framework (e.g. atModifierTestCases). It must match the Parser
+// used by NewTestEngine so that expressions parse consistently.
+var parserForBuiltinTests = parser.NewParser(TestParserOpts)
+
func atModifierTestCases(exprStr string, evalTime time.Time) ([]atModifierTestCase, error) {
- expr, err := parser.ParseExpr(exprStr)
+ expr, err := parserForBuiltinTests.ParseExpr(exprStr)
if err != nil {
return nil, err
}
@@ -1418,7 +1480,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())
})
@@ -1430,6 +1492,11 @@ func (t *test) execEval(cmd *evalCmd, engine promql.QueryEngine) error {
func (t *test) execRangeEval(cmd *evalCmd, engine promql.QueryEngine) error {
q, err := engine.NewRangeQuery(t.context, t.storage, nil, cmd.expr, cmd.start, cmd.end, cmd.step)
if err != nil {
+ if cmd.isFail() {
+ if err := cmd.checkExpectedFailure(err); err == nil {
+ return nil
+ }
+ }
return fmt.Errorf("error creating range query for %q (line %d): %w", cmd.expr, cmd.line, err)
}
defer q.Close()
@@ -1473,6 +1540,11 @@ func (t *test) execInstantEval(cmd *evalCmd, engine promql.QueryEngine) error {
func (t *test) runInstantQuery(iq atModifierTestCase, cmd *evalCmd, engine promql.QueryEngine) error {
q, err := engine.NewInstantQuery(t.context, t.storage, nil, iq.expr, iq.evalTime)
if err != nil {
+ if cmd.isFail() {
+ if err := cmd.checkExpectedFailure(err); err == nil {
+ return nil
+ }
+ }
return fmt.Errorf("error creating instant query for %q (line %d): %w", cmd.expr, cmd.line, err)
}
defer q.Close()
@@ -1506,6 +1578,10 @@ func (t *test) runInstantQuery(iq atModifierTestCase, cmd *evalCmd, engine promq
// Check query returns same result in range mode,
// by checking against the middle step.
+ // Skip this check for queries containing range() since it would resolve differently.
+ if strings.Contains(iq.expr, "range()") {
+ return nil
+ }
q, err = engine.NewRangeQuery(t.context, t.storage, nil, iq.expr, iq.evalTime.Add(-time.Minute), iq.evalTime.Add(time.Minute), time.Minute)
if err != nil {
return fmt.Errorf("error creating range query for %q (line %d): %w", cmd.expr, cmd.line, err)
@@ -1572,12 +1648,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())
}
@@ -1617,6 +1693,8 @@ type LazyLoaderOpts struct {
// Currently defaults to false, matches the "promql-delayed-name-removal"
// feature flag.
EnableDelayedNameRemoval bool
+ // StartTime is the start time for the test. If zero, defaults to Unix epoch.
+ StartTime time.Time
}
// NewLazyLoader returns an initialized empty LazyLoader.
@@ -1642,7 +1720,12 @@ func (ll *LazyLoader) parse(input string) error {
continue
}
if strings.HasPrefix(strings.ToLower(patSpace.Split(l, 2)[0]), "load") {
- _, cmd, err := parseLoad(lines, i)
+ // Determine the start time to use for loading samples.
+ startTime := testStartTime
+ if !ll.opts.StartTime.IsZero() {
+ startTime = ll.opts.StartTime
+ }
+ _, cmd, err := parseLoad(lines, i, startTime)
if err != nil {
return err
}
diff --git a/promql/promqltest/test_migrate.go b/promql/promqltest/test_migrate.go
index 0b233e7592..693b773b7d 100644
--- a/promql/promqltest/test_migrate.go
+++ b/promql/promqltest/test_migrate.go
@@ -1,4 +1,4 @@
-// Copyright 2025 The Prometheus Authors
+// 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
diff --git a/promql/promqltest/test_migrate_test.go b/promql/promqltest/test_migrate_test.go
index fcf7e9db03..6c9784b56f 100644
--- a/promql/promqltest/test_migrate_test.go
+++ b/promql/promqltest/test_migrate_test.go
@@ -1,4 +1,4 @@
-// Copyright 2025 The Prometheus Authors
+// 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
diff --git a/promql/promqltest/test_test.go b/promql/promqltest/test_test.go
index f441d148d6..cbb73a5651 100644
--- a/promql/promqltest/test_test.go
+++ b/promql/promqltest/test_test.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The Prometheus Authors
+// 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
diff --git a/promql/promqltest/testdata/aggregators.test b/promql/promqltest/testdata/aggregators.test
index 576b36868f..a3dc61dcff 100644
--- a/promql/promqltest/testdata/aggregators.test
+++ b/promql/promqltest/testdata/aggregators.test
@@ -687,6 +687,11 @@ load 10s
eval instant at 1m sum(data{test="ten"})
{} 10
+# Plain addition doesn't use Kahan summation, so operations involving very large magnitudes
+# (±1e+100) lose precision. The smaller values are absorbed, leading to an incorrect result.
+# eval instant at 1m sum(data{test="ten",point="a"}) + sum(data{test="ten",point="b"}) + sum(data{test="ten",point="c"}) + sum(data{test="ten",point="d"})
+# {} 10
+
eval instant at 1m avg(data{test="ten"})
{} 2.5
diff --git a/promql/promqltest/testdata/at_modifier.test b/promql/promqltest/testdata/at_modifier.test
index 4091f7eabf..194c877803 100644
--- a/promql/promqltest/testdata/at_modifier.test
+++ b/promql/promqltest/testdata/at_modifier.test
@@ -215,3 +215,43 @@ eval instant at 0s sum_over_time(timestamp(timestamp(metric{job="1"} @ 999))[10s
clear
+
+# Tests for @ modifier with empty data.
+# Data only at 0s, 10s, 20s. Eval at timestamp with no data.
+load 10s
+ up 1 2 3
+
+# Functions that should return empty results when @ modifier points to timestamp with no data.
+# These were panicking before the fix.
+
+eval instant at 1111111s quantile_over_time(scalar(up) + 1, {__name__="up"}[1h:1m] @ 1111111)
+
+eval instant at 1111111s predict_linear({__name__="up"}[1h:1m] @ 1111111, 0.1)
+
+eval instant at 1111111s deriv({__name__="up"}[1h:1m] @ 1111111)
+
+eval instant at 1111111s changes({__name__="up"}[1h:1m] @ 1111111)
+
+eval instant at 1111111s resets({__name__="up"}[1h:1m] @ 1111111)
+
+eval instant at 1111111s first_over_time({__name__="up"}[1h:1m] @ 1111111)
+
+eval instant at 1111111s last_over_time({__name__="up"}[1h:1m] @ 1111111)
+
+eval instant at 1111111s sum_over_time({__name__="up"}[1h:1m] @ 1111111)
+
+eval instant at 1111111s avg_over_time({__name__="up"}[1h:1m] @ 1111111)
+
+eval instant at 1111111s min_over_time({__name__="up"}[1h:1m] @ 1111111)
+
+eval instant at 1111111s max_over_time({__name__="up"}[1h:1m] @ 1111111)
+
+eval instant at 1111111s count_over_time({__name__="up"}[1h:1m] @ 1111111)
+
+eval instant at 1111111s stddev_over_time({__name__="up"}[1h:1m] @ 1111111)
+
+eval instant at 1111111s stdvar_over_time({__name__="up"}[1h:1m] @ 1111111)
+
+eval instant at 1111111s mad_over_time({__name__="up"}[1h:1m] @ 1111111)
+
+clear
diff --git a/promql/promqltest/testdata/duration_expression.test b/promql/promqltest/testdata/duration_expression.test
index db8253777b..e58b34131b 100644
--- a/promql/promqltest/testdata/duration_expression.test
+++ b/promql/promqltest/testdata/duration_expression.test
@@ -225,4 +225,27 @@ eval range from 50s to 60s step 5s metric1_total offset max(3s,min(step(), 1s))+
{} 8047 8052 8057
eval range from 50s to 60s step 5s metric1_total offset -(min(step(), 2s)-5)+8000
- {} 8047 8052 8057
\ No newline at end of file
+ {} 8047 8052 8057
+
+# Test range() function - resolves to query range (end - start).
+# For a range query from 50s to 60s, range() = 10s.
+eval range from 50s to 60s step 10s count_over_time(metric1_total[range()])
+ {} 10 10
+
+eval range from 50s to 60s step 5s count_over_time(metric1_total[range()])
+ {} 10 10 10
+
+eval range from 50s to 60s step 5s metric1_total offset range()
+ metric1_total{} 40 45 50
+
+eval range from 50s to 60s step 5s metric1_total offset min(range(), 8s)
+ metric1_total{} 42 47 52
+
+clear
+
+load 1s
+ metric1_total 0+1x100
+
+# For an instant query (start == end), range() = 0s, offset 0s.
+eval instant at 50s metric1_total offset range()
+ metric1_total{} 50
diff --git a/promql/promqltest/testdata/extended_vectors.test b/promql/promqltest/testdata/extended_vectors.test
index 8e116b1ac5..0bc1140522 100644
--- a/promql/promqltest/testdata/extended_vectors.test
+++ b/promql/promqltest/testdata/extended_vectors.test
@@ -319,7 +319,6 @@ eval instant at 2m changes(metric[1m] anchored)
{id="2"} 1
eval instant at 3m changes(metric[1m] anchored)
- {id="1"} 1
{id="2"} 1
eval instant at 8m changes(metric[1m] anchored)
@@ -342,7 +341,6 @@ eval instant at 2m resets(metric[1m] anchored)
{id="2"} 1
eval instant at 3m resets(metric[1m] anchored)
- {id="1"} 1
{id="2"} 1
eval instant at 8m resets(metric[1m] anchored)
@@ -360,6 +358,14 @@ load 1m
eval instant at 2m15s increase(metric[2m] smoothed)
{} 12
+# Smoothed rate interpolation across a counter reset.
+clear
+load 15s
+ metric 100 10
+
+eval instant at 12s rate(metric[10s] smoothed)
+ {} 0.666666666666667
+
clear
eval instant at 1m deriv(foo[3m] smoothed)
expect fail msg: smoothed modifier can only be used with: delta, increase, rate - not with deriv
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/functions.test b/promql/promqltest/testdata/functions.test
index ba3df76ff6..7bc4bcb624 100644
--- a/promql/promqltest/testdata/functions.test
+++ b/promql/promqltest/testdata/functions.test
@@ -2014,3 +2014,38 @@ eval instant at 0m scalar({type="histogram"})
# One float in the vector.
eval instant at 0m scalar({l="x"})
1
+
+clear
+load 20m
+ series{label="a", idx="1"} 2 _
+ series{label="a", idx="2"} _ 4
+
+eval instant at 0 label_replace(series, "idx", "replaced", "idx", ".*")
+ series{label="a", idx="replaced"} 2
+
+eval instant at 20m label_replace(series, "idx", "replaced", "idx", ".*")
+ series{label="a", idx="replaced"} 4
+
+eval range from 0 to 20m step 20m label_replace(series, "idx", "replaced", "idx", ".*")
+ series{label="a", idx="replaced"} 2 4
+
+# Test label_join with non-overlapping series.
+eval instant at 0 label_join(series, "idx", ",", "label", "label")
+ series{label="a", idx="a,a"} 2
+
+eval instant at 20m label_join(series, "idx", ",", "label", "label")
+ series{label="a", idx="a,a"} 4
+
+eval range from 0 to 20m step 20m label_join(series, "idx", ",", "label", "label")
+ series{label="a", idx="a,a"} 2 4
+
+# Test label_replace failure with overlapping timestamps (same labelset at same time).
+clear
+load 1m
+ overlap{label="a", idx="1"} 1
+ overlap{label="a", idx="2"} 2
+
+eval_fail instant at 0 label_replace(overlap, "idx", "same", "idx", ".*")
+
+# Test label_join failure with overlapping timestamps (same labelset at same time).
+eval_fail instant at 0 label_join(overlap, "idx", ",", "label", "label")
diff --git a/promql/promqltest/testdata/histograms.test b/promql/promqltest/testdata/histograms.test
index 84a467a314..db7d5de230 100644
--- a/promql/promqltest/testdata/histograms.test
+++ b/promql/promqltest/testdata/histograms.test
@@ -158,6 +158,383 @@ eval instant at 50m histogram_fraction(0, 0.2, rate(testhistogram3_bucket[10m]))
{start="positive"} 0.6363636363636364
{start="negative"} 0
+# Positive buckets, lower falls in the first bucket.
+load_with_nhcb 5m
+ positive_buckets_lower_falls_in_the_first_bucket_bucket{le="1"} 1+0x10
+ positive_buckets_lower_falls_in_the_first_bucket_bucket{le="2"} 3+0x10
+ positive_buckets_lower_falls_in_the_first_bucket_bucket{le="3"} 6+0x10
+ positive_buckets_lower_falls_in_the_first_bucket_bucket{le="+Inf"} 100+0x10
+
+# - Bucket [0, 1]: contributes 1.0 observation (full bucket).
+# - Bucket [1, 2]: contributes (1.5-1)/(2-1) * (3-1) = 0.5 * 2 = 1.0 observations.
+# Total: (1.0 + 1.0) / 100.0 = 0.02
+
+eval instant at 50m histogram_fraction(0, 1.5, positive_buckets_lower_falls_in_the_first_bucket_bucket)
+ expect no_warn
+ {} 0.02
+
+eval instant at 50m histogram_fraction(0, 1.5, positive_buckets_lower_falls_in_the_first_bucket)
+ expect no_warn
+ {} 0.02
+
+# Negative buckets, lower falls in the first bucket.
+load_with_nhcb 5m
+ negative_buckets_lower_falls_in_the_first_bucket_bucket{le="-3"} 10+0x10
+ negative_buckets_lower_falls_in_the_first_bucket_bucket{le="-2"} 12+0x10
+ negative_buckets_lower_falls_in_the_first_bucket_bucket{le="-1"} 15+0x10
+ negative_buckets_lower_falls_in_the_first_bucket_bucket{le="+Inf"} 100+0x10
+
+# - Bucket [-Inf, -3]: contributes zero observations (no interpolation with infinite width bucket).
+# - Bucket [-3, -2]: contributes 12-10 = 2.0 observations (full bucket).
+# Total: 2.0 / 100.0 = 0.02
+
+eval instant at 50m histogram_fraction(-4, -2, negative_buckets_lower_falls_in_the_first_bucket_bucket)
+ expect no_warn
+ {} 0.02
+
+eval instant at 50m histogram_fraction(-4, -2, negative_buckets_lower_falls_in_the_first_bucket)
+ expect no_warn
+ {} 0.02
+
+# Lower is -Inf.
+load_with_nhcb 5m
+ lower_is_negative_Inf_bucket{le="-3"} 10+0x10
+ lower_is_negative_Inf_bucket{le="-2"} 12+0x10
+ lower_is_negative_Inf_bucket{le="-1"} 15+0x10
+ lower_is_negative_Inf_bucket{le="+Inf"} 100+0x10
+
+# - Bucket [-Inf, -3]: contributes 10.0 observations (full bucket).
+# - Bucket [-3, -2]: contributes 12-10 = 2.0 observations (full bucket).
+# - Bucket [-2, -1]: contributes (-1.5-(-2))/(-1-(-2)) * (15-12) = 0.5 * 3 = 1.5 observations.
+# Total: (10.0 + 2.0 + 1.5) / 100.0 = 0.135
+
+eval instant at 50m histogram_fraction(-Inf, -1.5, lower_is_negative_Inf_bucket)
+ expect no_warn
+ {} 0.135
+
+eval instant at 50m histogram_fraction(-Inf, -1.5, lower_is_negative_Inf)
+ expect no_warn
+ {} 0.135
+
+# Lower is -Inf and upper is +Inf (positive buckets).
+load_with_nhcb 5m
+ lower_is_negative_Inf_and_upper_is_positive_Inf__positive_buckets__bucket{le="1"} 1+0x10
+ lower_is_negative_Inf_and_upper_is_positive_Inf__positive_buckets__bucket{le="2"} 3+0x10
+ lower_is_negative_Inf_and_upper_is_positive_Inf__positive_buckets__bucket{le="3"} 6+0x10
+ lower_is_negative_Inf_and_upper_is_positive_Inf__positive_buckets__bucket{le="+Inf"} 100+0x10
+
+# Range [-Inf, +Inf] captures all observations.
+
+eval instant at 50m histogram_fraction(-Inf, +Inf, lower_is_negative_Inf_and_upper_is_positive_Inf__positive_buckets__bucket)
+ expect no_warn
+ {} 1.0
+
+eval instant at 50m histogram_fraction(-Inf, +Inf, lower_is_negative_Inf_and_upper_is_positive_Inf__positive_buckets_)
+ expect no_warn
+ {} 1.0
+
+# Lower is -Inf and upper is +Inf (negative buckets).
+load_with_nhcb 5m
+ lower_is_negative_Inf_and_upper_is_positive_Inf__negative_buckets__bucket{le="-3"} 10+0x10
+ lower_is_negative_Inf_and_upper_is_positive_Inf__negative_buckets__bucket{le="-2"} 12+0x10
+ lower_is_negative_Inf_and_upper_is_positive_Inf__negative_buckets__bucket{le="-1"} 15+0x10
+ lower_is_negative_Inf_and_upper_is_positive_Inf__negative_buckets__bucket{le="+Inf"} 100+0x10
+
+# Range [-Inf, +Inf] captures all observations.
+
+eval instant at 50m histogram_fraction(-Inf, +Inf, lower_is_negative_Inf_and_upper_is_positive_Inf__negative_buckets__bucket)
+ expect no_warn
+ {} 1.0
+
+eval instant at 50m histogram_fraction(-Inf, +Inf, lower_is_negative_Inf_and_upper_is_positive_Inf__negative_buckets_)
+ expect no_warn
+ {} 1.0
+
+# Lower and upper fall in last bucket (positive buckets).
+load_with_nhcb 5m
+ lower_and_upper_fall_in_last_bucket__positive_buckets__bucket{le="1"} 1+0x10
+ lower_and_upper_fall_in_last_bucket__positive_buckets__bucket{le="2"} 3+0x10
+ lower_and_upper_fall_in_last_bucket__positive_buckets__bucket{le="3"} 6+0x10
+ lower_and_upper_fall_in_last_bucket__positive_buckets__bucket{le="+Inf"} 100+0x10
+
+# - Bucket [3, +Inf]: contributes zero observations (no interpolation with infinite width bucket).
+# Total: 0.0 / 100.0 = 0.0
+
+eval instant at 50m histogram_fraction(4, 5, lower_and_upper_fall_in_last_bucket__positive_buckets__bucket)
+ expect no_warn
+ {} 0.0
+
+eval instant at 50m histogram_fraction(4, 5, lower_and_upper_fall_in_last_bucket__positive_buckets_)
+ expect no_warn
+ {} 0.0
+
+# Lower and upper fall in last bucket (negative buckets).
+load_with_nhcb 5m
+ lower_and_upper_fall_in_last_bucket__negative_buckets__bucket{le="-3"} 10+0x10
+ lower_and_upper_fall_in_last_bucket__negative_buckets__bucket{le="-2"} 12+0x10
+ lower_and_upper_fall_in_last_bucket__negative_buckets__bucket{le="-1"} 15+0x10
+ lower_and_upper_fall_in_last_bucket__negative_buckets__bucket{le="+Inf"} 100+0x10
+
+# - Bucket [-1, +Inf]: contributes zero observations (no interpolation with infinite width bucket).
+# Total: 0.0 / 100.0 = 0.0
+
+eval instant at 50m histogram_fraction(0, 1, lower_and_upper_fall_in_last_bucket__negative_buckets__bucket)
+ expect no_warn
+ {} 0.0
+
+eval instant at 50m histogram_fraction(0, 1, lower_and_upper_fall_in_last_bucket__negative_buckets_)
+ expect no_warn
+ {} 0.0
+
+# Upper falls in last bucket.
+load_with_nhcb 5m
+ upper_falls_in_last_bucket_bucket{le="1"} 1+0x10
+ upper_falls_in_last_bucket_bucket{le="2"} 3+0x10
+ upper_falls_in_last_bucket_bucket{le="3"} 6+0x10
+ upper_falls_in_last_bucket_bucket{le="+Inf"} 100+0x10
+
+# - Bucket [2, 3]: 6-3 = 3.0 observations (full bucket).
+# - Bucket [3, +Inf]: contributes zero observations (no interpolation with infinite width bucket).
+# Total: 3.0 / 100.0 = 0.03
+
+eval instant at 50m histogram_fraction(2, 5, upper_falls_in_last_bucket_bucket)
+ expect no_warn
+ {} 0.03
+
+eval instant at 50m histogram_fraction(2, 5, upper_falls_in_last_bucket)
+ expect no_warn
+ {} 0.03
+
+# Upper is +Inf.
+load_with_nhcb 5m
+ upper_is_positive_Inf_bucket{le="1"} 1+0x10
+ upper_is_positive_Inf_bucket{le="2"} 3+0x10
+ upper_is_positive_Inf_bucket{le="3"} 6+0x10
+ upper_is_positive_Inf_bucket{le="+Inf"} 100+0x10
+
+# All observations in +Inf bucket: 100-6 = 94.0 observations.
+# Total: 94.0 / 100.0 = 0.94
+
+eval instant at 50m histogram_fraction(400, +Inf, upper_is_positive_Inf_bucket)
+ expect no_warn
+ {} 0.94
+
+eval instant at 50m histogram_fraction(400, +Inf, upper_is_positive_Inf)
+ expect no_warn
+ {} 0.94
+
+# Lower equals upper.
+load_with_nhcb 5m
+ lower_equals_upper_bucket{le="1"} 1+0x10
+ lower_equals_upper_bucket{le="2"} 3+0x10
+ lower_equals_upper_bucket{le="3"} 6+0x10
+ lower_equals_upper_bucket{le="+Inf"} 100+0x10
+
+# No observations can be captured in a zero-width range.
+
+eval instant at 50m histogram_fraction(2, 2, lower_equals_upper_bucket)
+ expect no_warn
+ {} 0.0
+
+eval instant at 50m histogram_fraction(2, 2, lower_equals_upper)
+ expect no_warn
+ {} 0.0
+
+# Lower greater than upper.
+load_with_nhcb 5m
+ lower_greater_than_upper_bucket{le="1"} 1+0x10
+ lower_greater_than_upper_bucket{le="2"} 3+0x10
+ lower_greater_than_upper_bucket{le="3"} 6+0x10
+ lower_greater_than_upper_bucket{le="+Inf"} 100+0x10
+
+eval instant at 50m histogram_fraction(3, 2, lower_greater_than_upper_bucket)
+ expect no_warn
+ {} 0.0
+
+eval instant at 50m histogram_fraction(3, 2, lower_greater_than_upper)
+ expect no_warn
+ {} 0.0
+
+# Single bucket.
+load_with_nhcb 5m
+ single_bucket_bucket{le="+Inf"} 100+0x10
+
+# - Bucket [0, +Inf]: contributes zero observations (no interpolation with infinite width bucket).
+# Total: 0.0 / 100.0 = 0.0
+
+eval instant at 50m histogram_fraction(0, 1, single_bucket_bucket)
+ expect no_warn
+ {} 0.0
+
+eval instant at 50m histogram_fraction(0, 1, single_bucket)
+ expect no_warn
+ {} 0.0
+
+# All zero counts.
+load_with_nhcb 5m
+ all_zero_counts_bucket{le="1"} 0+0x10
+ all_zero_counts_bucket{le="2"} 0+0x10
+ all_zero_counts_bucket{le="3"} 0+0x10
+ all_zero_counts_bucket{le="+Inf"} 0+0x10
+
+eval instant at 50m histogram_fraction(0, 5, all_zero_counts_bucket)
+ expect no_warn
+ {} NaN
+
+eval instant at 50m histogram_fraction(0, 5, all_zero_counts)
+ expect no_warn
+ {} NaN
+
+# Lower exactly on bucket boundary.
+load_with_nhcb 5m
+ lower_exactly_on_bucket_boundary_bucket{le="1"} 1+0x10
+ lower_exactly_on_bucket_boundary_bucket{le="2"} 3+0x10
+ lower_exactly_on_bucket_boundary_bucket{le="3"} 6+0x10
+ lower_exactly_on_bucket_boundary_bucket{le="+Inf"} 100+0x10
+
+# - Bucket [2, 3]: 6-3 = 3.0 observations (full bucket).
+# - Bucket [3, +Inf]: contributes zero observations (no interpolation with infinite width bucket).
+# Total: 3.0 / 100.0 = 0.03
+
+eval instant at 50m histogram_fraction(2, 3.5, lower_exactly_on_bucket_boundary_bucket)
+ expect no_warn
+ {} 0.03
+
+eval instant at 50m histogram_fraction(2, 3.5, lower_exactly_on_bucket_boundary)
+ expect no_warn
+ {} 0.03
+
+# Upper exactly on bucket boundary.
+load_with_nhcb 5m
+ upper_exactly_on_bucket_boundary_bucket{le="1"} 1+0x10
+ upper_exactly_on_bucket_boundary_bucket{le="2"} 3+0x10
+ upper_exactly_on_bucket_boundary_bucket{le="3"} 6+0x10
+ upper_exactly_on_bucket_boundary_bucket{le="+Inf"} 100+0x10
+
+# - Bucket [0, 1]: (1.0-0.5)/(1.0-0.0) * 1.0 = 0.5 * 1.0 = 0.5 observations.
+# - Bucket [1, 2]: 3-1 = 2.0 observations (full bucket).
+# Total: (0.5 + 2.0) / 100.0 = 0.025
+
+eval instant at 50m histogram_fraction(0.5, 2, upper_exactly_on_bucket_boundary_bucket)
+ expect no_warn
+ {} 0.025
+
+eval instant at 50m histogram_fraction(0.5, 2, upper_exactly_on_bucket_boundary)
+ expect no_warn
+ {} 0.025
+
+# Both bounds exactly on bucket boundaries.
+load_with_nhcb 5m
+ both_bounds_exactly_on_bucket_boundaries_bucket{le="1"} 1+0x10
+ both_bounds_exactly_on_bucket_boundaries_bucket{le="2"} 3+0x10
+ both_bounds_exactly_on_bucket_boundaries_bucket{le="3"} 6+0x10
+ both_bounds_exactly_on_bucket_boundaries_bucket{le="+Inf"} 100+0x10
+
+# - Bucket [1, 2]: 3-1 = 2.0 observations (full bucket).
+# - Bucket [2, 3]: 6-3 = 3.0 observations (full bucket).
+# Total: (2.0 + 3.0) / 100.0 = 0.05
+
+eval instant at 50m histogram_fraction(1, 3, both_bounds_exactly_on_bucket_boundaries_bucket)
+ expect no_warn
+ {} 0.05
+
+eval instant at 50m histogram_fraction(1, 3, both_bounds_exactly_on_bucket_boundaries)
+ expect no_warn
+ {} 0.05
+
+# Fractional bucket bounds.
+load_with_nhcb 5m
+ fractional_bucket_bounds_bucket{le="0.5"} 2.5+0x10
+ fractional_bucket_bounds_bucket{le="1"} 7.5+0x10
+ fractional_bucket_bounds_bucket{le="+Inf"} 100+0x10
+
+# - Bucket [0, 0.5]: (0.5-0.1)/(0.5-0.0) * 2.5 = 0.8 * 2.5 = 2.0 observations.
+# - Bucket [0.5, 1.0]: (0.75-0.5)/(1.0-0.5) * (7.5-2.5) = 0.5 * 5.0 = 2.5 observations.
+# Total: (2.0 + 2.5) / 100.0 = 0.045
+
+eval instant at 50m histogram_fraction(0.1, 0.75, fractional_bucket_bounds_bucket)
+ expect no_warn
+ {} 0.045
+
+eval instant at 50m histogram_fraction(0.1, 0.75, fractional_bucket_bounds)
+ expect no_warn
+ {} 0.045
+
+# Range crosses zero.
+load_with_nhcb 5m
+ range_crosses_zero_bucket{le="-2"} 5+0x10
+ range_crosses_zero_bucket{le="-1"} 10+0x10
+ range_crosses_zero_bucket{le="0"} 15+0x10
+ range_crosses_zero_bucket{le="1"} 20+0x10
+ range_crosses_zero_bucket{le="+Inf"} 100+0x10
+
+# - Bucket [-1, 0]: 15-10 = 5.0 observations (full bucket).
+# - Bucket [0, 1]: 20-15 = 5.0 observations (full bucket).
+# Total: (5.0 + 5.0) / 100.0 = 0.1
+
+eval instant at 50m histogram_fraction(-1, 1, range_crosses_zero_bucket)
+ expect no_warn
+ {} 0.1
+
+eval instant at 50m histogram_fraction(-1, 1, range_crosses_zero)
+ expect no_warn
+ {} 0.1
+
+# Lower is NaN.
+load_with_nhcb 5m
+ lower_is_NaN_bucket{le="1"} 1+0x10
+ lower_is_NaN_bucket{le="+Inf"} 100+0x10
+
+eval instant at 50m histogram_fraction(NaN, 1, lower_is_NaN_bucket)
+ expect no_warn
+ {} NaN
+
+eval instant at 50m histogram_fraction(NaN, 1, lower_is_NaN)
+ expect no_warn
+ {} NaN
+
+# Upper is NaN.
+load_with_nhcb 5m
+ upper_is_NaN_bucket{le="1"} 1+0x10
+ upper_is_NaN_bucket{le="+Inf"} 100+0x10
+
+eval instant at 50m histogram_fraction(0, NaN, upper_is_NaN_bucket)
+ expect no_warn
+ {} NaN
+
+eval instant at 50m histogram_fraction(0, NaN, upper_is_NaN)
+ expect no_warn
+ {} NaN
+
+# Range entirely below all buckets.
+load_with_nhcb 5m
+ range_entirely_below_all_buckets_bucket{le="1"} 1+0x10
+ range_entirely_below_all_buckets_bucket{le="2"} 3+0x10
+ range_entirely_below_all_buckets_bucket{le="+Inf"} 10+0x10
+
+eval instant at 50m histogram_fraction(-10, -5, range_entirely_below_all_buckets_bucket)
+ expect no_warn
+ {} 0.0
+
+eval instant at 50m histogram_fraction(-10, -5, range_entirely_below_all_buckets)
+ expect no_warn
+ {} 0.0
+
+# Range entirely above all buckets.
+load_with_nhcb 5m
+ range_entirely_above_all_buckets_bucket{le="1"} 1+0x10
+ range_entirely_above_all_buckets_bucket{le="2"} 3+0x10
+ range_entirely_above_all_buckets_bucket{le="+Inf"} 10+0x10
+
+eval instant at 50m histogram_fraction(5, 10, range_entirely_above_all_buckets_bucket)
+ expect no_warn
+ {} 0.0
+
+eval instant at 50m histogram_fraction(5, 10, range_entirely_above_all_buckets)
+ expect no_warn
+ {} 0.0
+
+
# In the classic histogram, we can access the corresponding bucket (if
# it exists) and divide by the count to get the same result.
@@ -221,6 +598,40 @@ eval instant at 50m histogram_quantile(1, testhistogram3_bucket)
{start="positive"} 1
{start="negative"} -0.1
+eval instant at 50m histogram_quantiles(testhistogram3, "q", 0, 0.25, 0.5, 0.75, 1)
+ expect no_warn
+ {q="0.0", start="positive"} 0
+ {q="0.0", start="negative"} -0.25
+ {q="0.25", start="positive"} 0.055
+ {q="0.25", start="negative"} -0.225
+ {q="0.5", start="positive"} 0.125
+ {q="0.5", start="negative"} -0.2
+ {q="0.75", start="positive"} 0.45
+ {q="0.75", start="negative"} -0.15
+ {q="1.0", start="positive"} 1
+ {q="1.0", start="negative"} -0.1
+
+eval instant at 50m histogram_quantiles(testhistogram3_bucket, "q", 0, 0.25, 0.5, 0.75, 1)
+ expect no_warn
+ {q="0.0", start="positive"} 0
+ {q="0.0", start="negative"} -0.25
+ {q="0.25", start="positive"} 0.055
+ {q="0.25", start="negative"} -0.225
+ {q="0.5", start="positive"} 0.125
+ {q="0.5", start="negative"} -0.2
+ {q="0.75", start="positive"} 0.45
+ {q="0.75", start="negative"} -0.15
+ {q="1.0", start="positive"} 1
+ {q="1.0", start="negative"} -0.1
+
+# Break label set uniqueness.
+
+eval instant at 50m histogram_quantiles(testhistogram3, "start", 0, 0.25, 0.5, 0.75, 1)
+ expect fail
+
+eval instant at 50m histogram_quantiles(testhistogram3_bucket, "start", 0, 0.25, 0.5, 0.75, 1)
+ expect fail
+
# Quantile too low.
eval instant at 50m histogram_quantile(-0.1, testhistogram)
@@ -233,6 +644,16 @@ eval instant at 50m histogram_quantile(-0.1, testhistogram_bucket)
{start="positive"} -Inf
{start="negative"} -Inf
+eval instant at 50m histogram_quantiles(testhistogram, "q", -0.1)
+ expect warn
+ {q="-0.1", start="positive"} -Inf
+ {q="-0.1", start="negative"} -Inf
+
+eval instant at 50m histogram_quantiles(testhistogram_bucket, "q", -0.1)
+ expect warn
+ {q="-0.1", start="positive"} -Inf
+ {q="-0.1", start="negative"} -Inf
+
# Quantile too high.
eval instant at 50m histogram_quantile(1.01, testhistogram)
@@ -245,6 +666,16 @@ eval instant at 50m histogram_quantile(1.01, testhistogram_bucket)
{start="positive"} +Inf
{start="negative"} +Inf
+eval instant at 50m histogram_quantiles(testhistogram, "q", 1.01)
+ expect warn
+ {q="1.01", start="positive"} +Inf
+ {q="1.01", start="negative"} +Inf
+
+eval instant at 50m histogram_quantiles(testhistogram_bucket, "q", 1.01)
+ expect warn
+ {q="1.01", start="positive"} +Inf
+ {q="1.01", start="negative"} +Inf
+
# Quantile invalid.
eval instant at 50m histogram_quantile(NaN, testhistogram)
@@ -257,9 +688,22 @@ eval instant at 50m histogram_quantile(NaN, testhistogram_bucket)
{start="positive"} NaN
{start="negative"} NaN
+eval instant at 50m histogram_quantiles(testhistogram, "q", NaN)
+ expect warn
+ {q="NaN", start="positive"} NaN
+ {q="NaN", start="negative"} NaN
+
+eval instant at 50m histogram_quantiles(testhistogram_bucket, "q", NaN)
+ expect warn
+ {q="NaN", start="positive"} NaN
+ {q="NaN", start="negative"} NaN
+
eval instant at 50m histogram_quantile(NaN, non_existent)
expect warn msg: PromQL warning: quantile value should be between 0 and 1, got NaN
+eval instant at 50m histogram_quantiles(non_existent, "q", NaN)
+ expect warn msg: PromQL warning: quantile value should be between 0 and 1, got NaN
+
# Quantile value in lowest bucket.
eval instant at 50m histogram_quantile(0, testhistogram)
@@ -590,6 +1034,12 @@ eval instant at 50m histogram_quantile(0.99, nonmonotonic_bucket)
expect info
{} 979.75
+eval instant at 50m histogram_quantiles(nonmonotonic_bucket, "q", 0.01, 0.5, 0.99)
+ expect info
+ {q="0.01"} 0.0045
+ {q="0.5"} 8.5
+ {q="0.99"} 979.75
+
# Buckets with different representations of the same upper bound.
eval instant at 50m histogram_quantile(0.5, rate(mixed_bucket[10m]))
{instance="ins1", job="job1"} 0.15
@@ -625,9 +1075,15 @@ load_with_nhcb 5m
eval instant at 50m histogram_quantile(0.99, {__name__=~"request_duration_seconds\\d*_bucket"})
expect fail
+eval instant at 50m histogram_quantiles({__name__=~"request_duration_seconds\\d*_bucket"}, "q", 0.99)
+ expect fail
+
eval instant at 50m histogram_quantile(0.99, {__name__=~"request_duration_seconds\\d*"})
expect fail
+eval instant at 50m histogram_quantiles({__name__=~"request_duration_seconds\\d*"}, "q", 0.99)
+ expect fail
+
# Histogram with constant buckets.
load_with_nhcb 1m
const_histogram_bucket{le="0.0"} 1 1 1 1 1
@@ -689,7 +1145,7 @@ eval instant at 10m histogram_sum(increase(histogram_with_reset[15m]))
clear
-# Test histogram_quantile and histogram_fraction with conflicting classic and native histograms.
+# Test histogram_quantile(s) and histogram_fraction with conflicting classic and native histograms.
load 1m
series{host="a"} {{schema:0 sum:5 count:4 buckets:[9 2 1]}}
series{host="a", le="0.1"} 2
@@ -704,6 +1160,11 @@ eval instant at 0 histogram_quantile(0.8, series)
expect warn msg: PromQL warning: vector contains a mix of classic and native histograms for metric name "series"
# Should return no results.
+eval instant at 0 histogram_quantiles(series, "q", 0.1, 0.2)
+ expect no_info
+ expect warn msg: PromQL warning: vector contains a mix of classic and native histograms for metric name "series"
+ # Should return no results.
+
eval instant at 0 histogram_fraction(-Inf, 1, series)
expect no_info
expect warn msg: PromQL warning: vector contains a mix of classic and native histograms for metric name "series"
diff --git a/promql/promqltest/testdata/info.test b/promql/promqltest/testdata/info.test
index 891e0eaa53..a3988abc64 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,9 +70,29 @@ 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
+
+load 5m
+ metric{instance="a", job="1", label="value"} 0 1 2
+ target_info{instance="a", job="1", data="info", another_data="another info"} 1 1 1
+ build_info{instance="a", job="1", build_data="build"} 1 1 1
+ target_build{instance="a", job="1", build_data="build"} 1 1 1
+
+# Multiple positive __name__ matchers.
+eval range from 0m to 10m step 5m info(metric, {__name__=~"target_.+", __name__=~".+_info"})
+ metric{instance="a", job="1", label="value", data="info", another_data="another info"} 0 1 2
+
+# A positive and a negative __name__ matcher.
+eval range from 0m to 10m step 5m info(metric, {__name__=~".+_info", __name__!~".*build.*"})
+ metric{instance="a", job="1", label="value", data="info", another_data="another info"} 0 1 2
+
clear
# Overlapping target_info series.
@@ -150,3 +186,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/name_label_dropping.test b/promql/promqltest/testdata/name_label_dropping.test
index 3682021ba9..e0180c7ffe 100644
--- a/promql/promqltest/testdata/name_label_dropping.test
+++ b/promql/promqltest/testdata/name_label_dropping.test
@@ -91,3 +91,48 @@ eval instant at 10m topk(10, sum by (__name__, env) (metric_total{env="1"}))
eval instant at 10m topk(10, sum by (__name__, env) (rate(metric_total{env="1"}[10m])))
{env="1"} 0.2
+
+clear
+
+# More testing for __name__ label drop with different input series.
+load 1m
+ metric_total{env="1"} 0+1x10
+ metric_total{env="2"} 0+3x10
+
+# Metric name is preserved as there is no function that drops it.
+eval instant at 10m sum by (__name__) (metric_total{env="1"})
+ metric_total 10
+
+# Metric name is dropped at the end because of rate and because there is no label function to preserve it.
+eval instant at 10m sum by (__name__) (rate(metric_total{env="2"}[5m]))
+ {} 0.05
+
+# Metric name is preserved with label_replace even though it would have been dropped with rate.
+eval instant at 10m label_replace(sum by (__name__) (rate(metric_total{env="2"}[5m])), "__name__", "$1", "__name__", "(.+)")
+ metric_total 0.05
+
+# Combining the above cases in an OR expression, we drop the name if any of the series drops it.
+eval instant at 10m sum by (__name__) (metric_total{env="1"} or rate(metric_total{env="2"}[5m]))
+ {} 10.05
+
+# Changing the order of the OR expression should not change the result.
+eval instant at 10m sum by (__name__) (rate(metric_total{env="2"}[5m]) or metric_total{env="1"})
+ {} 10.05
+
+# With non-matching first selector, we use the second to determine if __name__ is dropped.
+eval instant at 10m sum by (__name__) (metric_total{env="3"} or rate(metric_total{env="2"}[5m]))
+ {} 0.05
+
+# Same as above, but with reversed order.
+eval instant at 10m sum by (__name__) (rate(metric_total{env="3"}[5m]) or metric_total{env="1"})
+ metric_total 10
+
+clear
+
+# Test delayed name removal with range queries and OR operator.
+load 10m
+ metric_a 1 _
+ metric_b 3 4
+
+eval range from 0 to 20m step 10m -metric_a or -metric_b
+ {} -1 -4 _
diff --git a/promql/promqltest/testdata/native_histograms.test b/promql/promqltest/testdata/native_histograms.test
index fd4b1f4178..40789b295a 100644
--- a/promql/promqltest/testdata/native_histograms.test
+++ b/promql/promqltest/testdata/native_histograms.test
@@ -55,6 +55,10 @@ eval instant at 1m histogram_quantile(0.5, single_histogram)
expect no_info
{} 1.414213562373095
+eval instant at 1m histogram_quantiles(single_histogram, "q", 0.5)
+ expect no_info
+ {q="0.5"} 1.414213562373095
+
clear
# Repeat the same histogram 10 times.
@@ -1283,7 +1287,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
@@ -1388,22 +1392,28 @@ clear
# Test native histograms with sum, count, avg.
load 10m
- histogram_sum{idx="0"} {{schema:0 count:25 sum:1234.5 z_bucket:4 z_bucket_w:0.001 buckets:[1 2 0 1 1] n_buckets:[2 4 0 0 1 9]}}x1
- histogram_sum{idx="1"} {{schema:0 count:41 sum:2345.6 z_bucket:5 z_bucket_w:0.001 buckets:[1 3 1 2 1 1 1] n_buckets:[0 1 4 2 7 0 0 0 0 5 5 2]}}x1
- histogram_sum{idx="2"} {{schema:0 count:41 sum:1111.1 z_bucket:5 z_bucket_w:0.001 buckets:[1 3 1 2 1 1 1] n_buckets:[0 1 4 2 7 0 0 0 0 5 5 2]}}x1
- histogram_sum{idx="3"} {{schema:1 count:0}}x1
+ histogram_sum{idx="0"} {{schema:0 count:25 sum:3.1 z_bucket:4 z_bucket_w:0.001 buckets:[1 2 0 1 1] n_buckets:[2 4 0 0 1 9]}}x1
+ histogram_sum{idx="1"} {{schema:0 count:41 sum:1e100 z_bucket:5 z_bucket_w:0.001 buckets:[1 3 1 2 1 1 1] n_buckets:[0 1 4 2 7 0 0 0 0 5 5 2]}}x1
+ histogram_sum{idx="2"} {{schema:0 count:41 sum:-1e100 z_bucket:5 z_bucket_w:0.001 buckets:[1 3 1 2 1 1 1] n_buckets:[0 1 4 2 7 0 0 0 0 5 5 2]}}x1
+ histogram_sum{idx="3"} {{schema:1 count:0 sum:1.3 z_bucket:3 z_bucket_w:0.001 buckets:[2 4 2 3 2 2] n_buckets:[1 2 5 3 8 1 1 1 1 6 3]}}x1
histogram_sum_float{idx="0"} 42.0x1
eval instant at 10m sum(histogram_sum)
expect no_warn
- {} {{schema:0 count:107 sum:4691.2 z_bucket:14 z_bucket_w:0.001 buckets:[3 8 2 5 3 2 2] n_buckets:[2 6 8 4 15 9 0 0 0 10 10 4]}}
+ {} {{schema:0 count:107 sum:4.4 z_bucket:17 z_bucket_w:0.001 buckets:[5 14 7 7 3 2 2] n_buckets:[3 13 19 6 17 18 0 0 0 10 10 4]}}
eval instant at 10m sum({idx="0"})
expect warn
-eval instant at 10m sum(histogram_sum{idx="0"} + ignoring(idx) histogram_sum{idx="1"} + ignoring(idx) histogram_sum{idx="2"} + ignoring(idx) histogram_sum{idx="3"})
+eval instant at 10m sum(histogram_sum{idx="0"} + ignoring(idx) histogram_sum{idx="3"})
expect no_warn
- {} {{schema:0 count:107 sum:4691.2 z_bucket:14 z_bucket_w:0.001 buckets:[3 8 2 5 3 2 2] n_buckets:[2 6 8 4 15 9 0 0 0 10 10 4]}}
+ {} {{schema:0 count:25 sum:4.4 z_bucket:7 z_bucket_w:0.001 buckets:[3 8 5 3 1] n_buckets:[3 11 11 2 3 18]}}
+
+# Plain addition doesn't use Kahan summation, so operations involving very large magnitudes
+# (±1e+100) lose precision. The smaller values are absorbed, leading to an incorrect result.
+# eval instant at 10m sum(histogram_sum{idx="0"} + ignoring(idx) histogram_sum{idx="1"} + ignoring(idx) histogram_sum{idx="2"} + ignoring(idx) histogram_sum{idx="3"})
+# expect no_warn
+# {} {{schema:0 count:107 sum:4.4 z_bucket:14 z_bucket_w:0.001 buckets:[3 8 2 5 3 2 2] n_buckets:[2 6 8 4 15 9 0 0 0 10 10 4]}}
eval instant at 10m count(histogram_sum)
expect no_warn
@@ -1411,13 +1421,63 @@ eval instant at 10m count(histogram_sum)
eval instant at 10m avg(histogram_sum)
expect no_warn
- {} {{schema:0 count:26.75 sum:1172.8 z_bucket:3.5 z_bucket_w:0.001 buckets:[0.75 2 0.5 1.25 0.75 0.5 0.5] n_buckets:[0.5 1.5 2 1 3.75 2.25 0 0 0 2.5 2.5 1]}}
+ {} {{schema:0 count:26.75 sum:1.1 z_bucket:4.25 z_bucket_w:0.001 buckets:[1.25 3.5 1.75 1.75 0.75 0.5 0.5] n_buckets:[0.75 3.25 4.75 1.5 4.25 4.5 0 0 0 2.5 2.5 1]}}
+
+clear
+
+# Test native histograms with incremental avg calulation.
+# Very large floats involved trigger incremental avg calculation, as direct avg calculation would overflow float64.
+load 10m
+ histogram_avg_incremental{idx="0"} {{schema:0 count:1.7976931348623157e+308 sum:5.30921651659898 z_bucket:1.78264e50 z_bucket_w:0.001 buckets:[1.78264E+50 1.78264E+215 1.78264E+219 3363.5121756487] n_buckets:[1178.20696291113 731.697776280323 715.201503759399 1386.11378876781 855.572553278132]}}x1
+ histogram_avg_incremental{idx="1"} {{schema:0 count:1e308 sum:0.961118537914768 z_bucket:0.76342771 z_bucket_w:0.001 buckets:[0.76342771 0.76342771 0.76342771 195.70084087969] n_buckets:[421.30382970055 0 450441.779]}}x1
+ histogram_avg_incremental{idx="2"} {{schema:0 count:1e-6 sum:1.62091361305318 z_bucket:1.9592258 z_bucket_w:0.001 buckets:[1.9592258 1.9592258 1.9592258 1135.74692279] n_buckets:[0 4504.41779 588.599358265103 40.3760942760943]}}x1
+ histogram_avg_incremental{idx="3"} {{schema:0 count:1e-6 sum:0.865089463758091 z_bucket:7.69805412 z_bucket_w:0.001 buckets:[2.258E+220 2.258E+220 2.3757689E+217 1078.68071312804] n_buckets:[349.905284031261 0 0 0.161173466838949 588.599358]}}x1
+ histogram_avg_incremental{idx="4"} {{schema:0 count:1e-6 sum:0.323055185914577 z_bucket:458.90154 z_bucket_w:0.001 buckets:[7.69805412 7.69805412 2.258E+220 3173.28218135701]}}x1
+ histogram_avg_incremental{idx="5"} {{schema:0 count:1e-6 sum:0.951811357687154 z_bucket:1.78264e50 z_bucket_w:0.001 buckets:[458.90154 458.90154 7.69805412 2178.35] n_buckets:[2054.92644438789 844.560108898123]}}x1
+ histogram_avg_incremental{idx="6"} {{schema:0 count:1e-6 sum:0 z_bucket:5 z_bucket_w:0.001 buckets:[0 0 1.78264E+219 376.770478890989]}}x1
+ histogram_avg_incremental{idx="7"} {{schema:0 count:1e-6 sum:0 z_bucket:0 z_bucket_w:0.001 buckets:[0 0 458.90154 250325.5] n_buckets:[0 0.0000000011353 0 608.697257]}}x1
+# This test fails due to float64 rounding in the incremental average calculation.
+# For large intermediate means (e.g. ~1e99), multiplying by a fractional weight like (n-1)/n
+# produces values such as 2.0000000000000002e99 instead of the mathematically exact 2e99.
+# While the relative error is tiny, subtracting nearly equal high-magnitude values later
+# result in a large absolute error. The outcome also depends on the (effectively random) order
+# in which input series are processed which makes the test flaky.
+# histogram_avg_incremental_2{idx="0"} {{schema:0 count:1.7976931348623157e+308 sum:5.3 z_bucket:1.78264e50 z_bucket_w:0.001 buckets:[1.78264E+50 1.78264E+215 1.78264E+219 3363.5121756487] n_buckets:[1178.20696291113 731.697776280323 715.201503759399 1386.11378876781 855.572553278132]}}x1
+# histogram_avg_incremental_2{idx="1"} {{schema:0 count:1e308 sum:1e100 z_bucket:0.76342771 z_bucket_w:0.001 buckets:[0.76342771 0.76342771 0.76342771 195.70084087969] n_buckets:[421.30382970055 0 450441.779]}}x1
+# histogram_avg_incremental_2{idx="2"} {{schema:0 count:1e-6 sum:1 z_bucket:1.9592258 z_bucket_w:0.001 buckets:[1.9592258 1.9592258 1.9592258 1135.74692279] n_buckets:[0 4504.41779 588.599358265103 40.3760942760943]}}x1
+# histogram_avg_incremental_2{idx="3"} {{schema:0 count:1e-6 sum:-1e100 z_bucket:7.69805412 z_bucket_w:0.001 buckets:[2.258E+220 2.258E+220 2.3757689E+217 1078.68071312804] n_buckets:[349.905284031261 0 0 0.161173466838949 588.599358]}}x1
+# histogram_avg_incremental_2{idx="4"} {{schema:0 count:1e-6 sum:1 z_bucket:458.90154 z_bucket_w:0.001 buckets:[7.69805412 7.69805412 2.258E+220 3173.28218135701]}}x1
+# histogram_avg_incremental_2{idx="5"} {{schema:0 count:1e-6 sum:1 z_bucket:1.78264e50 z_bucket_w:0.001 buckets:[458.90154 458.90154 7.69805412 2178.35] n_buckets:[2054.92644438789 844.560108898123]}}x1
+# histogram_avg_incremental_2{idx="6"} {{schema:0 count:1e-6 sum:0 z_bucket:5 z_bucket_w:0.001 buckets:[0 0 1.78264E+219 376.770478890989]}}x1
+# histogram_avg_incremental_2{idx="7"} {{schema:0 count:1e-6 sum:0 z_bucket:0 z_bucket_w:0.001 buckets:[0 0 458.90154 250325.5] n_buckets:[0 0.0000000011353 0 608.697257]}}x1
+
+eval instant at 10m avg(histogram_avg_incremental)
+ {} {{schema:0 count:3.497116418577895e+307 sum:1.2539005843658437 z_bucket:4.4566e49 z_bucket_w:0.001 buckets:[2.8225e+219 2.822522283e+219 3.271129711125e+219 32728.442914086805] n_buckets:[500.5428151288539 760.0844593974477 56468.19748275306 254.4185391888429 180.5214889097665]}}
+
+# This test doesn't work, see the load section above for reasoning.
+# eval instant at 10m avg(histogram_avg_incremental_2)
+# {} {{schema:0 count:3.497116418577895e+307 sum:1.0375 z_bucket:4.4566e49 z_bucket_w:0.001 buckets:[2.8225e+219 2.822522283e+219 3.271129711125e+219 32728.442914086805] n_buckets:[500.5428151288539 760.0844593974477 56468.19748275306 254.4185391888429 180.5214889097665]}}
clear
# Test native histograms with sum_over_time, avg_over_time.
load 1m
histogram_sum_over_time {{schema:0 count:25 sum:1234.5 z_bucket:4 z_bucket_w:0.001 buckets:[1 2 0 1 1] n_buckets:[2 4 0 0 1 9]}} {{schema:0 count:41 sum:2345.6 z_bucket:5 z_bucket_w:0.001 buckets:[1 3 1 2 1 1 1] n_buckets:[0 1 4 2 7 0 0 0 0 5 5 2]}} {{schema:0 count:41 sum:1111.1 z_bucket:5 z_bucket_w:0.001 buckets:[1 3 1 2 1 1 1] n_buckets:[0 1 4 2 7 0 0 0 0 5 5 2]}} {{schema:1 count:0}}
+ histogram_sum_over_time_2 {{schema:0 count:1e10 sum:5.30921651659898 z_bucket:1.78264e50 z_bucket_w:0.001 buckets:[1.78264E+50 1.78264E+215 1.78264E+219 3363.5121756487] n_buckets:[1178.20696291113 731.697776280323 715.201503759399 1386.11378876781 855.572553278132]}} {{schema:0 count:1e-6 sum:0.961118537914768 z_bucket:0.76342771 z_bucket_w:0.001 buckets:[0.76342771 0.76342771 0.76342771 195.70084087969] n_buckets:[421.30382970055 0 450441.779]}} {{schema:0 count:1e-6 sum:1.62091361305318 z_bucket:1.9592258 z_bucket_w:0.001 buckets:[1.9592258 1.9592258 1.9592258 1135.74692279] n_buckets:[0 4504.41779 588.599358265103 40.3760942760943]}} {{schema:0 count:1e-6 sum:0.865089463758091 z_bucket:7.69805412 z_bucket_w:0.001 buckets:[2.258E+220 2.258E+220 2.3757689E+217 1078.68071312804] n_buckets:[349.905284031261 0 0 0.161173466838949 588.599358]}} {{schema:0 count:1e-6 sum:0.323055185914577 z_bucket:458.90154 z_bucket_w:0.001 buckets:[7.69805412 7.69805412 2.258E+220 3173.28218135701]}} {{schema:0 count:1e-6 sum:0.951811357687154 z_bucket:1.78264e50 z_bucket_w:0.001 buckets:[458.90154 458.90154 7.69805412 2178.35] n_buckets:[2054.92644438789 844.560108898123]}} {{schema:0 count:1e-6 sum:0 z_bucket:5 z_bucket_w:0.001 buckets:[0 0 1.78264E+219 376.770478890989]}} {{schema:0 count:1e-6 sum:0 z_bucket:0 z_bucket_w:0.001 buckets:[0 0 458.90154 250325.5] n_buckets:[0 0.0000000011353 0 608.697257]}}
+ histogram_sum_over_time_3 {{schema:0 count:1 sum:1}} {{schema:0 count:2 sum:1e100}} {{schema:0 count:3 sum:1}} {{schema:0 count:4 sum:-1e100}}
+ histogram_sum_over_time_4 {{schema:0 count:1 sum:5.3}} {{schema:0 count:2 sum:1e100}} {{schema:0 count:3 sum:1}} {{schema:0 count:4 sum:-1e100}} {{schema:0 count:5 sum:2}} {{schema:0 count:6 sum:1e50}} {{schema:0 count:7 sum:-1e50}}
+ histogram_sum_over_time_incremental {{schema:0 count:1.7976931348623157e+308 sum:5.30921651659898 z_bucket:1.78264e50 z_bucket_w:0.001 buckets:[1.78264E+50 1.78264E+215 1.78264E+219 3363.5121756487] n_buckets:[1178.20696291113 731.697776280323 715.201503759399 1386.11378876781 855.572553278132]}} {{schema:0 count:1e308 sum:0.961118537914768 z_bucket:0.76342771 z_bucket_w:0.001 buckets:[0.76342771 0.76342771 0.76342771 195.70084087969] n_buckets:[421.30382970055 0 450441.779]}} {{schema:0 count:1e-6 sum:1.62091361305318 z_bucket:1.9592258 z_bucket_w:0.001 buckets:[1.9592258 1.9592258 1.9592258 1135.74692279] n_buckets:[0 4504.41779 588.599358265103 40.3760942760943]}} {{schema:0 count:1e-6 sum:0.865089463758091 z_bucket:7.69805412 z_bucket_w:0.001 buckets:[2.258E+220 2.258E+220 2.3757689E+217 1078.68071312804] n_buckets:[349.905284031261 0 0 0.161173466838949 588.599358]}} {{schema:0 count:1e-6 sum:0.323055185914577 z_bucket:458.90154 z_bucket_w:0.001 buckets:[7.69805412 7.69805412 2.258E+220 3173.28218135701]}} {{schema:0 count:1e-6 sum:0.951811357687154 z_bucket:1.78264e50 z_bucket_w:0.001 buckets:[458.90154 458.90154 7.69805412 2178.35] n_buckets:[2054.92644438789 844.560108898123]}} {{schema:0 count:1e-6 sum:0 z_bucket:5 z_bucket_w:0.001 buckets:[0 0 1.78264E+219 376.770478890989]}} {{schema:0 count:1e-6 sum:0 z_bucket:0 z_bucket_w:0.001 buckets:[0 0 458.90154 250325.5] n_buckets:[0 0.0000000011353 0 608.697257]}}
+ histogram_sum_over_time_incremental_2 {{schema:0 count:1.7976931348623157e+308 sum:5.3}} {{schema:0 count:1e308 sum:1e100}} {{schema:0 count:1e-6 sum:1}} {{schema:0 count:1e-6 sum:-1e100}} {{schema:0 count:1e-6 sum:2}} {{schema:0 count:1e-6 sum:0}} {{schema:0 count:1e-6 sum:0}}
+ histogram_sum_over_time_incremental_3 {{schema:0 count:1.7976931348623157e+308 sum:5.3}} {{schema:0 count:1e308 sum:1e100}} {{schema:0 count:1e-6 sum:-1e100}} {{schema:0 count:1e-6 sum:1}} {{schema:0 count:1e-6 sum:1e100}} {{schema:0 count:1e-6 sum:-1e100}} {{schema:0 count:1e-6 sum:0}}
+ histogram_sum_over_time_incremental_4 {{schema:0 count:1.7976931348623157e+308 sum:5.3}} {{schema:0 count:1e308 sum:1e100}} {{schema:0 count:1e-6 sum:-1e100}} {{schema:0 count:1e-6 sum:1}} {{schema:0 count:1e-6 sum:1e50}} {{schema:0 count:1e-6 sum:-1e50}} {{schema:0 count:1e-6 sum:0}}
+ histogram_sum_over_time_incremental_6 {{schema:0 count:1.7976931348623157e+308 sum:1}} {{schema:0 count:1e308 sum:1e100}} {{schema:0 count:1e-6 sum:1}} {{schema:0 count:1e-6 sum:-1e100}}
+# Kahan summation only compensates reliably across two magnitude scales. In following inputs, the
+# series contains three distinct magnitude groups (≈1, ≈1e50, and ≈1e100). When these magnitudes
+# are interleaved, rounding error can't be fully compensated, causing smaller values to be lost.
+# However, when values are ordered so that cancellation within one magnitude group
+# occurs first, followed by cancellation of the next group, the outcome remains accurate.
+# histogram_sum_over_time_5 {{schema:0 count:1 sum:5.3}} {{schema:0 count:2 sum:1e100}} {{schema:0 count:3 sum:1}} {{schema:0 count:4 sum:1e50}} {{schema:0 count:5 sum:2}} {{schema:0 count:6 sum:-1e100}} {{schema:0 count:7 sum:-1e50}}
+# histogram_sum_over_time_incremental_5 {{schema:0 count:1.7976931348623157e+308 sum:5.3}} {{schema:0 count:1e308 sum:1e100}} {{schema:0 count:1e-6 sum:1e50}} {{schema:0 count:1e-6 sum:1}} {{schema:0 count:1e-6 sum:-1e100}} {{schema:0 count:1e-6 sum:-1e50}} {{schema:0 count:1e-6 sum:0}}
eval instant at 3m sum_over_time(histogram_sum_over_time[4m:1m])
{} {{schema:0 count:107 sum:4691.2 z_bucket:14 z_bucket_w:0.001 buckets:[3 8 2 5 3 2 2] n_buckets:[2 6 8 4 15 9 0 0 0 10 10 4]}}
@@ -1425,6 +1485,83 @@ eval instant at 3m sum_over_time(histogram_sum_over_time[4m:1m])
eval instant at 3m avg_over_time(histogram_sum_over_time[4m:1m])
{} {{schema:0 count:26.75 sum:1172.8 z_bucket:3.5 z_bucket_w:0.001 buckets:[0.75 2 0.5 1.25 0.75 0.5 0.5] n_buckets:[0.5 1.5 2 1 3.75 2.25 0 0 0 2.5 2.5 1]}}
+eval instant at 7m sum_over_time(histogram_sum_over_time_2[8m:1m])
+ {} {{schema:0 count:10000000000.000008 sum:10.03120467492675 z_bucket:3.56528e+50 z_bucket_w:0.001 buckets:[2.258e+220 2.2580178264e+220 2.6169037689e+220 261827.54331269444] n_buckets:[4004.342521030831 6080.675675179582 451745.57986202446 2035.3483135107433 1444.171911278132]}}
+
+eval instant at 7m avg_over_time(histogram_sum_over_time_2[8m:1m])
+ {} {{schema:0 count:1250000000.000001 sum:1.2539005843658437 z_bucket:4.4566e49 z_bucket_w:0.001 buckets:[2.8225e+219 2.822522283e+219 3.271129711125e+219 32728.442914086805] n_buckets:[500.5428151288539 760.0844593974477 56468.19748275306 254.4185391888429 180.5214889097665]}}
+
+eval instant at 3m sum_over_time(histogram_sum_over_time_3[4m:1m])
+ {} {{schema:0 count:10 sum:2}}
+
+eval instant at 3m avg_over_time(histogram_sum_over_time_3[4m:1m])
+ {} {{schema:0 count:2.5 sum:0.5}}
+
+eval instant at 6m sum_over_time(histogram_sum_over_time_4[7m:1m])
+ {} {{schema:0 count:28 sum:8.3}}
+
+eval instant at 6m avg_over_time(histogram_sum_over_time_4[7m:1m])
+ {} {{schema:0 count:4 sum:1.1857142857142857}}
+
+# These tests don't work, see the load section above for reasoning.
+# eval instant at 6m sum_over_time(histogram_sum_over_time_5[7m:1m])
+# {} {{schema:0 count:28 sum:8.3}}
+#
+# eval instant at 6m avg_over_time(histogram_sum_over_time_5[7m:1m])
+# {} {{schema:0 count:4 sum:1.1857142857142857}}
+
+eval instant at 7m sum_over_time(histogram_sum_over_time_incremental[8m:1m])
+ {} {{schema:0 count:Inf sum:10.03120467492675 z_bucket:3.56528e+50 z_bucket_w:0.001 buckets:[2.258e+220 2.2580178264e+220 2.6169037689e+220 261827.54331269444] n_buckets:[4004.342521030831 6080.675675179582 451745.57986202446 2035.3483135107433 1444.171911278132]}}
+
+eval instant at 7m avg_over_time(histogram_sum_over_time_incremental[8m:1m])
+ {} {{schema:0 count:3.497116418577895e+307 sum:1.2539005843658437 z_bucket:4.4566e49 z_bucket_w:0.001 buckets:[2.8225e+219 2.822522283e+219 3.271129711125e+219 32728.442914086805] n_buckets:[500.5428151288539 760.0844593974477 56468.19748275306 254.4185391888429 180.5214889097665]}}
+
+eval instant at 6m sum_over_time(histogram_sum_over_time_incremental_2[7m:1m])
+ {} {{schema:0 count:Inf sum:8.3}}
+
+eval instant at 6m avg_over_time(histogram_sum_over_time_incremental_2[7m:1m])
+ {} {{schema:0 count:3.9967044783747367e+307 sum:1.1857142857142857}}
+
+eval instant at 6m sum_over_time(histogram_sum_over_time_incremental_3[7m:1m])
+ {} {{schema:0 count:Inf sum:6.3}}
+
+eval instant at 6m avg_over_time(histogram_sum_over_time_incremental_3[7m:1m])
+ {} {{schema:0 count:3.9967044783747367e+307 sum:0.9}}
+
+eval instant at 6m sum_over_time(histogram_sum_over_time_incremental_4[7m:1m])
+ {} {{schema:0 count:Inf sum:6.3}}
+
+eval instant at 6m avg_over_time(histogram_sum_over_time_incremental_4[7m:1m])
+ {} {{schema:0 count:3.9967044783747367e+307 sum:0.9}}
+
+# These tests don't work, see the load section above for reasoning.
+# eval instant at 6m sum_over_time(histogram_sum_over_time_incremental_5[7m:1m])
+# {} {{schema:0 count:Inf sum:6.3}}
+#
+# eval instant at 6m avg_over_time(histogram_sum_over_time_incremental_5[7m:1m])
+# {} {{schema:0 count:3.9967044783747367e+307 sum:0.9}}
+
+eval instant at 3m sum_over_time(histogram_sum_over_time_incremental_6[4m:1m])
+ {} {{schema:0 count:Inf sum:2}}
+
+eval instant at 3m avg_over_time(histogram_sum_over_time_incremental_6[4m:1m])
+ {} {{schema:0 count:6.99423283715579e+307 sum:0.5}}
+
+clear
+
+# Test avg_over_time with a single histogram sample (regression test for division by zero bug).
+load 1m
+ single_histogram_sample {{schema:3 sum:5 count:4 buckets:[1 2 1]}}
+ single_nhcb_sample {{schema:-53 sum:1 count:5 custom_values:[5 10] buckets:[1 4]}}
+
+# avg_over_time should return the histogram unchanged when there's only one sample, not Inf/NaN.
+eval instant at 0m avg_over_time(single_histogram_sample[1m])
+ {} {{schema:3 sum:5 count:4 buckets:[1 2 1]}}
+
+# Test with native histogram with custom buckets (NHCB).
+eval instant at 0m avg_over_time(single_nhcb_sample[1m])
+ {} {{schema:-53 sum:1 count:5 custom_values:[5 10] buckets:[1 4]}}
+
clear
# Test native histograms with sub operator.
@@ -1472,6 +1609,11 @@ eval instant at 1m histogram_quantile(0.81, histogram_nan)
{case="100% NaNs"} NaN
{case="20% NaNs"} NaN
+eval instant at 1m histogram_quantiles(histogram_nan, "q", 0.81)
+ expect info msg: PromQL info: input to histogram_quantile has NaN observations, result is NaN for metric name "histogram_nan"
+ {case="100% NaNs", q="0.81"} NaN
+ {case="20% NaNs", q="0.81"} NaN
+
eval instant at 1m histogram_quantile(0.8, histogram_nan{case="100% NaNs"})
expect info msg: PromQL info: input to histogram_quantile has NaN observations, result is NaN for metric name "histogram_nan"
{case="100% NaNs"} NaN
@@ -1758,6 +1900,9 @@ eval instant at 1m histogram_quantile(0.5, myHistogram2)
eval instant at 1m histogram_quantile(0.5, mixedHistogram)
expect warn msg: PromQL warning: vector contains a mix of classic and native histograms for metric name "mixedHistogram"
+eval instant at 1m histogram_quantiles(mixedHistogram, "q", 0.5)
+ expect warn msg: PromQL warning: vector contains a mix of classic and native histograms for metric name "mixedHistogram"
+
clear
# A counter reset only in a bucket. Sub-queries still need to detect
@@ -1827,6 +1972,9 @@ eval instant at 1m histogram_count(histogram unless histogram_quantile(0.5, hist
eval instant at 1m histogram_quantile(0.5, histogram unless histogram_count(histogram) == 0)
{} 3.1748021039363987
+eval instant at 1m histogram_quantiles(histogram unless histogram_count(histogram) == 0, "q", 0.5)
+ {q="0.5"} 3.1748021039363987
+
clear
# Regression test for:
diff --git a/promql/promqltest/testdata/operators.test b/promql/promqltest/testdata/operators.test
index 0e779f192c..cd608b3c36 100644
--- a/promql/promqltest/testdata/operators.test
+++ b/promql/promqltest/testdata/operators.test
@@ -316,6 +316,27 @@ eval instant at 5m http_requests_histogram == http_requests_histogram
eval instant at 5m http_requests_histogram != http_requests_histogram
expect no_info
+clear
+
+# Check that we track many-to-one vector matching errors even when all but 0 or 1
+# series on the "many" side are filtered away.
+load 5m
+ many_side{label="foo",job="test"} 0
+ many_side{label="bar",job="test"} 1
+ one_side{job="test"} 1
+
+# Check 0 series surviving the filtering producing an error.
+eval instant at 0m many_side > on(job) one_side
+ expect fail
+
+# Check 1 series surviving the filtering producing an error.
+eval instant at 0m many_side >= on(job) one_side
+ expect fail
+
+# Check 2 series surviving the filtering producing an error.
+eval instant at 0m many_side <= on(job) one_side
+ expect fail
+
# group_left/group_right.
clear
@@ -959,3 +980,40 @@ eval instant at 10m (testhistogram) and on() (vector(-1) == 1)
eval range from 0 to 10m step 5m (testhistogram) and on() (vector(-1) == 1)
clear
+
+# Test unary negation with non-overlapping series that have different metric names.
+# After negation, the __name__ label is dropped, so series with different names
+# but same other labels should merge if they don't overlap in time.
+load 20m
+ http_requests{job="api"} 2 _
+ http_errors{job="api"} _ 4
+
+eval instant at 0 -{job="api"}
+ {job="api"} -2
+
+eval instant at 20m -{job="api"}
+ {job="api"} -4
+
+eval range from 0 to 20m step 20m -{job="api"}
+ {job="api"} -2 -4
+
+# Test unary negation failure with overlapping timestamps (same labelset at same time).
+clear
+load 1m
+ http_requests{job="api"} 1
+ http_errors{job="api"} 2
+
+eval_fail instant at 0 -{job="api"}
+
+clear
+
+# Test unary negation with "or" operator combining metrics with removed names.
+load 10m
+ metric_a 1 _
+ metric_b 3 4
+
+# Use "-" unary operator as a simple way to remove the metric name.
+eval range from 0 to 20m step 10m -metric_a or -metric_b
+ {} -1 -4
+
+clear
diff --git a/promql/quantile.go b/promql/quantile.go
index 1454974107..f3657e1621 100644
--- a/promql/quantile.go
+++ b/promql/quantile.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
@@ -94,10 +94,7 @@ type metricWithBuckets struct {
//
// If q>1, +Inf is returned.
//
-// We also return a bool to indicate if monotonicity needed to be forced,
-// and another bool to indicate if small differences between buckets (that
-// are likely artifacts of floating point precision issues) have been
-// ignored.
+// We also return extra info, see doc for ensureMonotonicAndIgnoreSmallDeltas.
//
// Generically speaking, BucketQuantile is for calculating the
// histogram_quantile() of classic histograms. See also: HistogramQuantile
@@ -105,15 +102,21 @@ type metricWithBuckets struct {
//
// BucketQuantile is exported as a useful quantile function over a set of
// given buckets. It may be used by other PromQL engine implementations.
-func BucketQuantile(q float64, buckets Buckets) (float64, bool, bool) {
- if math.IsNaN(q) {
- return math.NaN(), false, false
- }
- if q < 0 {
- return math.Inf(-1), false, false
- }
- if q > 1 {
- return math.Inf(+1), false, false
+func BucketQuantile(q float64, buckets Buckets) (
+ quantile float64,
+ forcedMonotonic, fixedPrecision bool,
+ minBucket, maxBucket, maxDiff float64,
+) {
+ switch {
+ case math.IsNaN(q):
+ quantile = math.NaN()
+ return quantile, forcedMonotonic, fixedPrecision, minBucket, maxBucket, maxDiff
+ case q < 0:
+ quantile = math.Inf(-1)
+ return quantile, forcedMonotonic, fixedPrecision, minBucket, maxBucket, maxDiff
+ case q > 1:
+ quantile = math.Inf(+1)
+ return quantile, forcedMonotonic, fixedPrecision, minBucket, maxBucket, maxDiff
}
slices.SortFunc(buckets, func(a, b Bucket) int {
// We don't expect the bucket boundary to be a NaN.
@@ -126,39 +129,44 @@ func BucketQuantile(q float64, buckets Buckets) (float64, bool, bool) {
return 0
})
if !math.IsInf(buckets[len(buckets)-1].UpperBound, +1) {
- return math.NaN(), false, false
+ quantile = math.NaN()
+ return quantile, forcedMonotonic, fixedPrecision, minBucket, maxBucket, maxDiff
}
buckets = coalesceBuckets(buckets)
- forcedMonotonic, fixedPrecision := ensureMonotonicAndIgnoreSmallDeltas(buckets, smallDeltaTolerance)
+ forcedMonotonic, fixedPrecision, minBucket, maxBucket, maxDiff = ensureMonotonicAndIgnoreSmallDeltas(buckets, smallDeltaTolerance)
if len(buckets) < 2 {
- return math.NaN(), false, false
+ quantile = math.NaN()
+ return quantile, forcedMonotonic, fixedPrecision, minBucket, maxBucket, maxDiff
}
observations := buckets[len(buckets)-1].Count
if observations == 0 {
- return math.NaN(), false, false
+ quantile = math.NaN()
+ return quantile, forcedMonotonic, fixedPrecision, minBucket, maxBucket, maxDiff
}
rank := q * observations
b := sort.Search(len(buckets)-1, func(i int) bool { return buckets[i].Count >= rank })
- if b == len(buckets)-1 {
- return buckets[len(buckets)-2].UpperBound, forcedMonotonic, fixedPrecision
+ switch {
+ case b == len(buckets)-1:
+ quantile = buckets[len(buckets)-2].UpperBound
+ case b == 0 && buckets[0].UpperBound <= 0:
+ quantile = buckets[0].UpperBound
+ default:
+ var (
+ bucketStart float64
+ bucketEnd = buckets[b].UpperBound
+ count = buckets[b].Count
+ )
+ if b > 0 {
+ bucketStart = buckets[b-1].UpperBound
+ count -= buckets[b-1].Count
+ rank -= buckets[b-1].Count
+ }
+ quantile = bucketStart + (bucketEnd-bucketStart)*(rank/count)
}
- if b == 0 && buckets[0].UpperBound <= 0 {
- return buckets[0].UpperBound, forcedMonotonic, fixedPrecision
- }
- var (
- bucketStart float64
- bucketEnd = buckets[b].UpperBound
- count = buckets[b].Count
- )
- if b > 0 {
- bucketStart = buckets[b-1].UpperBound
- count -= buckets[b-1].Count
- rank -= buckets[b-1].Count
- }
- return bucketStart + (bucketEnd-bucketStart)*(rank/count), forcedMonotonic, fixedPrecision
+ return quantile, forcedMonotonic, fixedPrecision, minBucket, maxBucket, maxDiff
}
// HistogramQuantile calculates the quantile 'q' based on the given histogram.
@@ -406,6 +414,18 @@ func HistogramFraction(lower, upper float64, h *histogram.FloatHistogram, metric
// consistent with the linear interpolation known from classic
// histograms. It is also used for the zero bucket.
interpolateLinearly := func(v float64) float64 {
+ // Note: `v` is a finite value.
+ // For buckets with infinite bounds, we cannot interpolate meaningfully.
+ // For +Inf upper bound, interpolation returns the cumulative count of the previous bucket
+ // as the second term in the interpolation formula yields 0 (finite/Inf).
+ // In other words, no observations from the last bucket are considered in the fraction calculation.
+ // For -Inf lower bound, however, the second term would be (v-(-Inf))/(upperBound-(-Inf)) = Inf/Inf = NaN.
+ // To achieve the same effect of no contribution as the +Inf bucket, handle the -Inf case by returning
+ // the cumulative count at the first bucket (which equals the bucket's count).
+ // In both cases, we effectively skip interpolation within the infinite-width bucket.
+ if b.Lower == math.Inf(-1) {
+ return b.Count
+ }
return rank + b.Count*(v-b.Lower)/(b.Upper-b.Lower)
}
@@ -531,14 +551,34 @@ func BucketFraction(lower, upper float64, buckets Buckets) float64 {
rank, lowerRank, upperRank float64
lowerSet, upperSet bool
)
+
+ // If the upper bound of the first bucket is greater than 0, we assume
+ // we are dealing with positive buckets only and lowerBound for the
+ // first bucket is set to 0; otherwise it is set to -Inf.
+ lowerBound := 0.0
+ if buckets[0].UpperBound <= 0 {
+ lowerBound = math.Inf(-1)
+ }
+
for i, b := range buckets {
- lowerBound := math.Inf(-1)
if i > 0 {
lowerBound = buckets[i-1].UpperBound
}
upperBound := b.UpperBound
interpolateLinearly := func(v float64) float64 {
+ // Note: `v` is a finite value.
+ // For buckets with infinite bounds, we cannot interpolate meaningfully.
+ // For +Inf upper bound, interpolation returns the cumulative count of the previous bucket
+ // as the second term in the interpolation formula yields 0 (finite/Inf).
+ // In other words, no observations from the last bucket are considered in the fraction calculation.
+ // For -Inf lower bound, however, the second term would be (v-(-Inf))/(upperBound-(-Inf)) = Inf/Inf = NaN.
+ // To achieve the same effect of no contribution as the +Inf bucket, handle the -Inf case by returning
+ // the cumulative count at the first bucket.
+ // In both cases, we effectively skip interpolation within the infinite-width bucket.
+ if lowerBound == math.Inf(-1) {
+ return b.Count
+ }
return rank + (b.Count-rank)*(v-lowerBound)/(upperBound-lowerBound)
}
@@ -623,10 +663,20 @@ func coalesceBuckets(buckets Buckets) Buckets {
// the histogram buckets, essentially removing any decreases in the count
// between successive buckets.
//
-// We return a bool to indicate if this monotonicity was forced or not, and
-// another bool to indicate if small deltas were ignored or not.
-func ensureMonotonicAndIgnoreSmallDeltas(buckets Buckets, tolerance float64) (bool, bool) {
- var forcedMonotonic, fixedPrecision bool
+// We return:
+// - a bool to indicate if monotonicity needed to be forced
+// - a bool to indicate if small differences between buckets (that are likely
+// artifacts of floating point precision issues) have been ignored.
+// - a float to indicate the minimum bucket upper bound where monotonicity was forced, if applicable
+// - a float to indicate the maximum bucket upper bound where monotonicity was forced, if applicable
+// - a float to indicate the maximum difference between the count of two consecutive buckets
+// where monotonicity was forced, if applicable
+func ensureMonotonicAndIgnoreSmallDeltas(buckets Buckets, tolerance float64) (
+ forcedMonotonic, fixedPrecision bool,
+ minBucket, maxBucket, maxDiff float64,
+) {
+ minBucket = math.Inf(+1)
+ maxBucket = math.Inf(-1)
prev := buckets[0].Count
for i := 1; i < len(buckets); i++ {
curr := buckets[i].Count // Assumed always positive.
@@ -647,11 +697,20 @@ func ensureMonotonicAndIgnoreSmallDeltas(buckets Buckets, tolerance float64) (bo
// Do not update the 'prev' value as we are ignoring the decrease.
buckets[i].Count = prev
forcedMonotonic = true
+ if buckets[i].UpperBound < minBucket {
+ minBucket = buckets[i].UpperBound
+ }
+ if buckets[i].UpperBound > maxBucket {
+ maxBucket = buckets[i].UpperBound
+ }
+ if diff := prev - curr; diff > maxDiff {
+ maxDiff = diff
+ }
continue
}
prev = curr
}
- return forcedMonotonic, fixedPrecision
+ return forcedMonotonic, fixedPrecision, minBucket, maxBucket, maxDiff
}
// quantile calculates the given quantile of a vector of samples.
diff --git a/promql/quantile_test.go b/promql/quantile_test.go
index a1047d73f4..e2042dc3c4 100644
--- a/promql/quantile_test.go
+++ b/promql/quantile_test.go
@@ -1,4 +1,4 @@
-// Copyright 2023 The Prometheus Authors
+// 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
@@ -308,10 +308,10 @@ func TestBucketQuantile_ForcedMonotonicity(t *testing.T) {
} {
t.Run(name, func(t *testing.T) {
for q, v := range tc.expectedValues {
- res, forced, fixed := BucketQuantile(q, tc.getInput())
+ quantile, forced, fixed, _, _, _ := BucketQuantile(q, tc.getInput())
require.Equal(t, tc.expectedForced, forced)
require.Equal(t, tc.expectedFixed, fixed)
- require.InEpsilon(t, v, res, eps)
+ require.InEpsilon(t, v, quantile, eps)
}
})
}
diff --git a/promql/query_logger.go b/promql/query_logger.go
index 5923223aa0..0c4b218828 100644
--- a/promql/query_logger.go
+++ b/promql/query_logger.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The Prometheus Authors
+// 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
@@ -164,7 +164,7 @@ func trimStringByBytes(str string, size int) string {
trimIndex := len(bytesStr)
if size < len(bytesStr) {
- for !utf8.RuneStart(bytesStr[size]) {
+ for size > 0 && !utf8.RuneStart(bytesStr[size]) {
size--
}
trimIndex = size
diff --git a/promql/query_logger_test.go b/promql/query_logger_test.go
index 47a6d1a25d..edd3baad12 100644
--- a/promql/query_logger_test.go
+++ b/promql/query_logger_test.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The Prometheus Authors
+// 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
@@ -127,6 +127,47 @@ func TestMMapFile(t *testing.T) {
require.Equal(t, []byte(data), bytes[:2], "Mmap failed")
}
+func TestTrimStringByBytes(t *testing.T) {
+ for _, tc := range []struct {
+ name string
+ input string
+ size int
+ expected string
+ }{
+ {
+ name: "normal ASCII string",
+ input: "hello",
+ size: 3,
+ expected: "hel",
+ },
+ {
+ name: "no trimming needed",
+ input: "hi",
+ size: 10,
+ expected: "hi",
+ },
+ {
+ name: "UTF-8 multibyte character boundary",
+ input: "日本", // 6 bytes (3 bytes per character)
+ size: 4,
+ expected: "日", // trims back to complete character boundary
+ },
+ {
+ name: "invalid UTF-8 continuation-only bytes",
+ input: string([]byte{0x80, 0x81, 0x82, 0x83, 0x84}), // only continuation bytes
+ size: 4,
+ expected: "",
+ },
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ require.NotPanics(t, func() {
+ result := trimStringByBytes(tc.input, tc.size)
+ require.Equal(t, tc.expected, result)
+ })
+ })
+ }
+}
+
func TestParseBrokenJSON(t *testing.T) {
for _, tc := range []struct {
b []byte
diff --git a/promql/value.go b/promql/value.go
index b909085b17..17afdfc410 100644
--- a/promql/value.go
+++ b/promql/value.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
@@ -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/promql/value_test.go b/promql/value_test.go
index 0017b41e2c..c7454284ff 100644
--- a/promql/value_test.go
+++ b/promql/value_test.go
@@ -1,4 +1,4 @@
-// Copyright 2022 The Prometheus Authors
+// 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
diff --git a/renovate.json b/renovate.json
index 175e1d6464..c0490c5610 100644
--- a/renovate.json
+++ b/renovate.json
@@ -1,62 +1,92 @@
{
- "$schema": "https://docs.renovatebot.com/renovate-schema.json",
- "extends": [
- "config:recommended"
- ],
- "separateMultipleMajor": true,
- "baseBranches": ["main"],
- "postUpdateOptions": [
- "gomodTidy",
- "gomodUpdateImportPaths"
- ],
- "schedule": ["* 0-8 * * 1"],
- "timezone": "UTC",
- "packageRules": [
- {
- "description": "Don't update replace directives",
- "matchPackageNames": [
- "github.com/fsnotify/fsnotify"
- ],
- "enabled": false
- },
- {
- "description": "Don't update prometheus-io namespace packages",
- "matchPackageNames": ["@prometheus-io/**"],
- "enabled": false
- },
- {
- "description": "Group Mantine UI dependencies",
- "matchFileNames": [
- "web/ui/mantine-ui/package.json"
- ],
- "groupName": "Mantine UI",
- "matchUpdateTypes": ["minor", "patch"],
- "enabled": true
- },
- {
- "description": "Group React App dependencies",
- "matchFileNames": [
- "web/ui/react-app/package.json"
- ],
- "groupName": "React App",
- "matchUpdateTypes": ["minor", "patch"],
- "enabled": true
- },
- {
- "description": "Group module dependencies",
- "matchFileNames": [
- "web/ui/module/**/package.json"
- ],
- "groupName": "Modules",
- "matchUpdateTypes": ["minor", "patch"],
- "enabled": true
- }
- ],
- "branchPrefix": "deps-update/",
- "vulnerabilityAlerts": {
- "enabled": true,
- "labels": ["security-update"]
+ "$schema": "https://docs.renovatebot.com/renovate-schema.json",
+ "extends": [
+ "config:recommended"
+ ],
+ "separateMultipleMajor": true,
+ "baseBranches": ["main"],
+ "postUpdateOptions": [
+ "gomodTidy",
+ "gomodUpdateImportPaths"
+ ],
+ "schedule": ["* * 21 * *"],
+ "timezone": "UTC",
+ "github-actions": {
+ "managerFilePatterns": ["scripts/**"]
+ },
+ "prBodyNotes": ["```release-notes","NONE","```"],
+ "prConcurrentLimit": 20,
+ "prHourlyLimit": 5,
+ "packageRules": [
+ {
+ "description": "Don't update replace directives",
+ "matchPackageNames": [
+ "github.com/fsnotify/fsnotify"
+ ],
+ "enabled": false
},
- "osvVulnerabilityAlerts": true,
- "dependencyDashboardApproval": false
+ {
+ "description": "Don't update prometheus-io namespace packages",
+ "matchPackageNames": ["@prometheus-io/**"],
+ "enabled": false
+ },
+ {
+ "description": "Group AWS Go dependencies",
+ "matchManagers": ["gomod"],
+ "matchPackageNames": ["github.com/aws/**"],
+ "groupName": "AWS Go dependencies"
+ },
+ {
+ "description": "Group Azure Go dependencies",
+ "matchManagers": ["gomod"],
+ "matchPackageNames": ["github.com/Azure/**"],
+ "groupName": "Azure Go dependencies"
+ },
+ {
+ "description": "Group Kubernetes Go dependencies",
+ "matchManagers": ["gomod"],
+ "matchPackageNames": ["k8s.io/**"],
+ "groupName": "Kubernetes Go dependencies"
+ },
+ {
+ "description": "Group OpenTelemetry Go dependencies",
+ "matchManagers": ["gomod"],
+ "matchPackageNames": ["go.opentelemetry.io/**"],
+ "groupName": "OpenTelemetry Go dependencies"
+ },
+ {
+ "description": "Group Mantine UI dependencies",
+ "matchFileNames": [
+ "web/ui/mantine-ui/package.json"
+ ],
+ "groupName": "Mantine UI",
+ "matchUpdateTypes": ["minor", "patch"],
+ "enabled": true
+ },
+ {
+ "description": "Group React App dependencies",
+ "matchFileNames": [
+ "web/ui/react-app/package.json"
+ ],
+ "groupName": "React App",
+ "matchUpdateTypes": ["minor", "patch"],
+ "enabled": true
+ },
+ {
+ "description": "Group module dependencies",
+ "matchFileNames": [
+ "web/ui/module/**/package.json"
+ ],
+ "groupName": "Modules",
+ "matchUpdateTypes": ["minor", "patch"],
+ "enabled": true
+ }
+ ],
+ "branchPrefix": "deps-update/",
+ "vulnerabilityAlerts": {
+ "enabled": true,
+ "labels": ["security-update"]
+ },
+ "osvVulnerabilityAlerts": true,
+ "dependencyDashboardApproval": false
}
diff --git a/rules/alerting.go b/rules/alerting.go
index b0151d7cb3..d94113b46b 100644
--- a/rules/alerting.go
+++ b/rules/alerting.go
@@ -1,4 +1,4 @@
-// Copyright 2013 The Prometheus Authors
+// 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
@@ -46,6 +46,10 @@ const (
alertStateLabel = "alertstate"
)
+// ErrDuplicateAlertLabelSet is returned when an alerting rule evaluation produces
+// metrics with identical labelsets after applying alert labels.
+var ErrDuplicateAlertLabelSet = errors.New("vector contains metrics with the same labelset after applying alert labels")
+
// AlertState denotes the state of an active alert.
type AlertState int
@@ -441,7 +445,7 @@ func (r *AlertingRule) Eval(ctx context.Context, queryOffset time.Duration, ts t
resultFPs[h] = struct{}{}
if _, ok := alerts[h]; ok {
- return nil, errors.New("vector contains metrics with the same labelset after applying alert labels")
+ return nil, ErrDuplicateAlertLabelSet
}
alerts[h] = &Alert{
diff --git a/rules/alerting_test.go b/rules/alerting_test.go
index dc5a6d1c43..91ea09e5fc 100644
--- a/rules/alerting_test.go
+++ b/rules/alerting_test.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The Prometheus Authors
+// 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
@@ -115,7 +115,7 @@ func TestAlertingRuleTemplateWithHistogram(t *testing.T) {
return []promql.Sample{{H: &h}}, nil
}
- expr, err := parser.ParseExpr("foo")
+ expr, err := testParser.ParseExpr("foo")
require.NoError(t, err)
rule := NewAlertingRule(
@@ -158,9 +158,8 @@ func TestAlertingRuleLabelsUpdate(t *testing.T) {
load 1m
http_requests{job="app-server", instance="0"} 75 85 70 70 stale
`)
- t.Cleanup(func() { storage.Close() })
- expr, err := parser.ParseExpr(`http_requests < 100`)
+ expr, err := testParser.ParseExpr(`http_requests < 100`)
require.NoError(t, err)
rule := NewAlertingRule(
@@ -264,9 +263,8 @@ func TestAlertingRuleExternalLabelsInTemplate(t *testing.T) {
load 1m
http_requests{job="app-server", instance="0"} 75 85 70 70
`)
- t.Cleanup(func() { storage.Close() })
- expr, err := parser.ParseExpr(`http_requests < 100`)
+ expr, err := testParser.ParseExpr(`http_requests < 100`)
require.NoError(t, err)
ruleWithoutExternalLabels := NewAlertingRule(
@@ -359,9 +357,8 @@ func TestAlertingRuleExternalURLInTemplate(t *testing.T) {
load 1m
http_requests{job="app-server", instance="0"} 75 85 70 70
`)
- t.Cleanup(func() { storage.Close() })
- expr, err := parser.ParseExpr(`http_requests < 100`)
+ expr, err := testParser.ParseExpr(`http_requests < 100`)
require.NoError(t, err)
ruleWithoutExternalURL := NewAlertingRule(
@@ -454,9 +451,8 @@ func TestAlertingRuleEmptyLabelFromTemplate(t *testing.T) {
load 1m
http_requests{job="app-server", instance="0"} 75 85 70 70
`)
- t.Cleanup(func() { storage.Close() })
- expr, err := parser.ParseExpr(`http_requests < 100`)
+ expr, err := testParser.ParseExpr(`http_requests < 100`)
require.NoError(t, err)
rule := NewAlertingRule(
@@ -510,9 +506,8 @@ func TestAlertingRuleQueryInTemplate(t *testing.T) {
load 1m
http_requests{job="app-server", instance="0"} 70 85 70 70
`)
- t.Cleanup(func() { storage.Close() })
- expr, err := parser.ParseExpr(`sum(http_requests) < 100`)
+ expr, err := testParser.ParseExpr(`sum(http_requests) < 100`)
require.NoError(t, err)
ruleWithQueryInTemplate := NewAlertingRule(
@@ -584,7 +579,6 @@ func BenchmarkAlertingRuleAtomicField(b *testing.B) {
func TestAlertingRuleDuplicate(t *testing.T) {
storage := teststorage.New(t)
- defer storage.Close()
opts := promql.EngineOpts{
Logger: nil,
@@ -598,7 +592,7 @@ func TestAlertingRuleDuplicate(t *testing.T) {
now := time.Now()
- expr, _ := parser.ParseExpr(`vector(0) or label_replace(vector(0),"test","x","","")`)
+ expr, _ := testParser.ParseExpr(`vector(0) or label_replace(vector(0),"test","x","","")`)
rule := NewAlertingRule(
"foo",
expr,
@@ -612,7 +606,7 @@ func TestAlertingRuleDuplicate(t *testing.T) {
)
_, err := rule.Eval(ctx, 0, now, EngineQueryFunc(engine, storage), nil, 0)
require.Error(t, err)
- require.EqualError(t, err, "vector contains metrics with the same labelset after applying alert labels")
+ require.ErrorIs(t, err, ErrDuplicateAlertLabelSet)
}
func TestAlertingRuleLimit(t *testing.T) {
@@ -621,7 +615,6 @@ func TestAlertingRuleLimit(t *testing.T) {
metric{label="1"} 1
metric{label="2"} 1
`)
- t.Cleanup(func() { storage.Close() })
tests := []struct {
limit int
@@ -642,7 +635,7 @@ func TestAlertingRuleLimit(t *testing.T) {
},
}
- expr, _ := parser.ParseExpr(`metric > 0`)
+ expr, _ := testParser.ParseExpr(`metric > 0`)
rule := NewAlertingRule(
"foo",
expr,
@@ -697,12 +690,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"},
@@ -763,7 +758,7 @@ func TestSendAlertsDontAffectActiveAlerts(t *testing.T) {
al := &Alert{State: StateFiring, Labels: lbls, ActiveAt: time.Now()}
rule.active[h] = al
- expr, err := parser.ParseExpr("foo")
+ expr, err := testParser.ParseExpr("foo")
require.NoError(t, err)
rule.vector = expr
@@ -803,9 +798,8 @@ func TestKeepFiringFor(t *testing.T) {
load 1m
http_requests{job="app-server", instance="0"} 75 85 70 70 10x5
`)
- t.Cleanup(func() { storage.Close() })
- expr, err := parser.ParseExpr(`http_requests > 50`)
+ expr, err := testParser.ParseExpr(`http_requests > 50`)
require.NoError(t, err)
rule := NewAlertingRule(
@@ -914,9 +908,8 @@ func TestPendingAndKeepFiringFor(t *testing.T) {
load 1m
http_requests{job="app-server", instance="0"} 75 10x10
`)
- t.Cleanup(func() { storage.Close() })
- expr, err := parser.ParseExpr(`http_requests > 50`)
+ expr, err := testParser.ParseExpr(`http_requests > 50`)
require.NoError(t, err)
rule := NewAlertingRule(
@@ -976,7 +969,7 @@ func TestAlertingEvalWithOrigin(t *testing.T) {
lbs = labels.FromStrings("test", "test")
)
- expr, err := parser.ParseExpr(query)
+ expr, err := testParser.ParseExpr(query)
require.NoError(t, err)
rule := NewAlertingRule(
diff --git a/rules/group.go b/rules/group.go
index 8cedcd40d1..704fd13d85 100644
--- a/rules/group.go
+++ b/rules/group.go
@@ -1,4 +1,4 @@
-// Copyright 2013 The Prometheus Authors
+// 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
@@ -519,6 +519,7 @@ func (g *Group) Eval(ctx context.Context, ts time.Time) {
since := time.Since(t)
g.metrics.EvalDuration.Observe(since.Seconds())
+ g.metrics.EvalDurationHistogram.Observe(since.Seconds())
rule.SetEvaluationDuration(since)
rule.SetEvaluationTimestamp(t)
}(time.Now())
@@ -910,19 +911,21 @@ const namespace = "prometheus"
// Metrics for rule evaluation.
type Metrics struct {
- EvalDuration prometheus.Summary
- IterationDuration prometheus.Summary
- IterationsMissed *prometheus.CounterVec
- IterationsScheduled *prometheus.CounterVec
- EvalTotal *prometheus.CounterVec
- EvalFailures *prometheus.CounterVec
- GroupInterval *prometheus.GaugeVec
- GroupLastEvalTime *prometheus.GaugeVec
- GroupLastDuration *prometheus.GaugeVec
- GroupLastRuleDurationSum *prometheus.GaugeVec
- GroupLastRestoreDuration *prometheus.GaugeVec
- GroupRules *prometheus.GaugeVec
- GroupSamples *prometheus.GaugeVec
+ EvalDuration prometheus.Summary
+ EvalDurationHistogram prometheus.Histogram
+ IterationDuration prometheus.Summary
+ IterationDurationHistogram prometheus.Histogram
+ IterationsMissed *prometheus.CounterVec
+ IterationsScheduled *prometheus.CounterVec
+ EvalTotal *prometheus.CounterVec
+ EvalFailures *prometheus.CounterVec
+ GroupInterval *prometheus.GaugeVec
+ GroupLastEvalTime *prometheus.GaugeVec
+ GroupLastDuration *prometheus.GaugeVec
+ GroupLastRuleDurationSum *prometheus.GaugeVec
+ GroupLastRestoreDuration *prometheus.GaugeVec
+ GroupRules *prometheus.GaugeVec
+ GroupSamples *prometheus.GaugeVec
}
// NewGroupMetrics creates a new instance of Metrics and registers it with the provided registerer,
@@ -936,12 +939,30 @@ func NewGroupMetrics(reg prometheus.Registerer) *Metrics {
Help: "The duration for a rule to execute.",
Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
}),
+ EvalDurationHistogram: prometheus.NewHistogram(prometheus.HistogramOpts{
+ Namespace: namespace,
+ Name: "rule_evaluation_duration_histogram_seconds",
+ Help: "The duration for a rule to execute.",
+ Buckets: []float64{.01, .1, 1, 10},
+ NativeHistogramBucketFactor: 1.1,
+ NativeHistogramMaxBucketNumber: 100,
+ NativeHistogramMinResetDuration: 1 * time.Hour,
+ }),
IterationDuration: prometheus.NewSummary(prometheus.SummaryOpts{
Namespace: namespace,
Name: "rule_group_duration_seconds",
Help: "The duration of rule group evaluations.",
Objectives: map[float64]float64{0.01: 0.001, 0.05: 0.005, 0.5: 0.05, 0.90: 0.01, 0.99: 0.001},
}),
+ IterationDurationHistogram: prometheus.NewHistogram(prometheus.HistogramOpts{
+ Namespace: namespace,
+ Name: "rule_group_duration_histogram_seconds",
+ Help: "The duration of rule group evaluations.",
+ Buckets: []float64{.01, .1, 1, 10},
+ NativeHistogramBucketFactor: 1.1,
+ NativeHistogramMaxBucketNumber: 100,
+ NativeHistogramMinResetDuration: 1 * time.Hour,
+ }),
IterationsMissed: prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: namespace,
@@ -1035,7 +1056,9 @@ func NewGroupMetrics(reg prometheus.Registerer) *Metrics {
if reg != nil {
reg.MustRegister(
m.EvalDuration,
+ m.EvalDurationHistogram,
m.IterationDuration,
+ m.IterationDurationHistogram,
m.IterationsMissed,
m.IterationsScheduled,
m.EvalTotal,
diff --git a/rules/group_test.go b/rules/group_test.go
index ff1ef3d6c1..a110c78510 100644
--- a/rules/group_test.go
+++ b/rules/group_test.go
@@ -1,4 +1,4 @@
-// Copyright 2013 The Prometheus Authors
+// 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
diff --git a/rules/manager.go b/rules/manager.go
index d2fb0a7797..5548359ce6 100644
--- a/rules/manager.go
+++ b/rules/manager.go
@@ -1,4 +1,4 @@
-// Copyright 2013 The Prometheus Authors
+// 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
@@ -37,6 +37,7 @@ import (
"github.com/prometheus/prometheus/promql"
"github.com/prometheus/prometheus/promql/parser"
"github.com/prometheus/prometheus/storage"
+ "github.com/prometheus/prometheus/util/features"
"github.com/prometheus/prometheus/util/strutil"
)
@@ -85,6 +86,7 @@ func DefaultEvalIterationFunc(ctx context.Context, g *Group, evalTimestamp time.
timeSinceStart := time.Since(start)
g.metrics.IterationDuration.Observe(timeSinceStart.Seconds())
+ g.metrics.IterationDurationHistogram.Observe(timeSinceStart.Seconds())
g.updateRuleEvaluationTimeSum()
g.setEvaluationTime(timeSinceStart)
g.setLastEvaluation(start)
@@ -133,6 +135,12 @@ type ManagerOptions struct {
RestoreNewRuleGroups bool
Metrics *Metrics
+
+ // FeatureRegistry is used to register rule manager features.
+ FeatureRegistry features.Collector
+
+ // Parser is the PromQL parser used for parsing rule expressions.
+ Parser parser.Parser
}
// NewManager returns an implementation of Manager, ready to be started
@@ -153,8 +161,12 @@ func NewManager(o *ManagerOptions) *Manager {
o.Metrics = NewGroupMetrics(o.Registerer)
}
+ if o.Parser == nil {
+ o.Parser = parser.NewParser(parser.Options{})
+ }
+
if o.GroupLoader == nil {
- o.GroupLoader = FileLoader{}
+ o.GroupLoader = FileLoader{parser: o.Parser}
}
if o.RuleConcurrencyController == nil {
@@ -173,6 +185,13 @@ func NewManager(o *ManagerOptions) *Manager {
o.Logger = promslog.NewNopLogger()
}
+ // Register rule manager features if a registry is provided.
+ if o.FeatureRegistry != nil {
+ o.FeatureRegistry.Set(features.Rules, "concurrent_rule_eval", o.ConcurrentEvalsEnabled)
+ o.FeatureRegistry.Enable(features.Rules, "query_offset")
+ o.FeatureRegistry.Enable(features.Rules, "keep_firing_for")
+ }
+
m := &Manager{
groups: map[string]*Group{},
opts: o,
@@ -308,14 +327,18 @@ type GroupLoader interface {
}
// FileLoader is the default GroupLoader implementation. It defers to rulefmt.ParseFile
-// and parser.ParseExpr.
-type FileLoader struct{}
-
-func (FileLoader) Load(identifier string, ignoreUnknownFields bool, nameValidationScheme model.ValidationScheme) (*rulefmt.RuleGroups, []error) {
- return rulefmt.ParseFile(identifier, ignoreUnknownFields, nameValidationScheme)
+// for loading and uses the configured Parser for expression parsing.
+type FileLoader struct {
+ parser parser.Parser
}
-func (FileLoader) Parse(query string) (parser.Expr, error) { return parser.ParseExpr(query) }
+func (fl FileLoader) Load(identifier string, ignoreUnknownFields bool, nameValidationScheme model.ValidationScheme) (*rulefmt.RuleGroups, []error) {
+ return rulefmt.ParseFile(identifier, ignoreUnknownFields, nameValidationScheme, fl.parser)
+}
+
+func (fl FileLoader) Parse(query string) (parser.Expr, error) {
+ return fl.parser.ParseExpr(query)
+}
// LoadGroups reads groups from a list of files.
func (m *Manager) LoadGroups(
@@ -594,7 +617,7 @@ func FromMaps(maps ...map[string]string) labels.Labels {
}
// ParseFiles parses the rule files corresponding to glob patterns.
-func ParseFiles(patterns []string, nameValidationScheme model.ValidationScheme) error {
+func ParseFiles(patterns []string, nameValidationScheme model.ValidationScheme, p parser.Parser) error {
files := map[string]string{}
for _, pat := range patterns {
fns, err := filepath.Glob(pat)
@@ -614,7 +637,7 @@ func ParseFiles(patterns []string, nameValidationScheme model.ValidationScheme)
}
}
for fn, pat := range files {
- _, errs := rulefmt.ParseFile(fn, false, nameValidationScheme)
+ _, errs := rulefmt.ParseFile(fn, false, nameValidationScheme, p)
if len(errs) > 0 {
return fmt.Errorf("parse rules from file %q (pattern: %q): %w", fn, pat, errors.Join(errs...))
}
diff --git a/rules/manager_test.go b/rules/manager_test.go
index a88be1e5d1..19c815e50c 100644
--- a/rules/manager_test.go
+++ b/rules/manager_test.go
@@ -1,4 +1,4 @@
-// Copyright 2013 The Prometheus Authors
+// 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
@@ -42,13 +42,14 @@ import (
"github.com/prometheus/prometheus/model/timestamp"
"github.com/prometheus/prometheus/model/value"
"github.com/prometheus/prometheus/promql"
- "github.com/prometheus/prometheus/promql/parser"
"github.com/prometheus/prometheus/promql/promqltest"
"github.com/prometheus/prometheus/storage"
+ "github.com/prometheus/prometheus/tsdb"
"github.com/prometheus/prometheus/tsdb/chunkenc"
"github.com/prometheus/prometheus/tsdb/tsdbutil"
"github.com/prometheus/prometheus/util/teststorage"
prom_testutil "github.com/prometheus/prometheus/util/testutil"
+ "github.com/prometheus/prometheus/util/testutil/synctest"
)
func TestMain(m *testing.M) {
@@ -61,9 +62,8 @@ func TestAlertingRule(t *testing.T) {
http_requests{job="app-server", instance="0", group="canary", severity="overwrite-me"} 75 85 95 105 105 95 85
http_requests{job="app-server", instance="1", group="canary", severity="overwrite-me"} 80 90 100 110 120 130 140
`)
- t.Cleanup(func() { storage.Close() })
- expr, err := parser.ParseExpr(`http_requests{group="canary", job="app-server"} < 100`)
+ expr, err := testParser.ParseExpr(`http_requests{group="canary", job="app-server"} < 100`)
require.NoError(t, err)
rule := NewAlertingRule(
@@ -204,9 +204,8 @@ func TestForStateAddSamples(t *testing.T) {
http_requests{job="app-server", instance="0", group="canary", severity="overwrite-me"} 75 85 95 105 105 95 85
http_requests{job="app-server", instance="1", group="canary", severity="overwrite-me"} 80 90 100 110 120 130 140
`)
- t.Cleanup(func() { storage.Close() })
- expr, err := parser.ParseExpr(`http_requests{group="canary", job="app-server"} < 100`)
+ expr, err := testParser.ParseExpr(`http_requests{group="canary", job="app-server"} < 100`)
require.NoError(t, err)
rule := NewAlertingRule(
@@ -366,9 +365,8 @@ func TestForStateRestore(t *testing.T) {
http_requests{job="app-server", instance="0", group="canary", severity="overwrite-me"} 75 85 50 0 0 25 0 0 40 0 120
http_requests{job="app-server", instance="1", group="canary", severity="overwrite-me"} 125 90 60 0 0 25 0 0 40 0 130
`)
- t.Cleanup(func() { storage.Close() })
- expr, err := parser.ParseExpr(`http_requests{group="canary", job="app-server"} < 100`)
+ expr, err := testParser.ParseExpr(`http_requests{group="canary", job="app-server"} < 100`)
require.NoError(t, err)
ng := testEngine(t)
@@ -537,7 +535,7 @@ func TestForStateRestore(t *testing.T) {
func TestStaleness(t *testing.T) {
for _, queryOffset := range []time.Duration{0, time.Minute} {
st := teststorage.New(t)
- defer st.Close()
+
engineOpts := promql.EngineOpts{
Logger: nil,
Reg: nil,
@@ -553,7 +551,7 @@ func TestStaleness(t *testing.T) {
Logger: promslog.NewNopLogger(),
}
- expr, err := parser.ParseExpr("a + 1")
+ expr, err := testParser.ParseExpr("a + 1")
require.NoError(t, err)
rule := NewRecordingRule("a_plus_one", expr, labels.Labels{})
group := NewGroup(GroupOptions{
@@ -725,7 +723,7 @@ func TestCopyState(t *testing.T) {
func TestDeletedRuleMarkedStale(t *testing.T) {
st := teststorage.New(t)
- defer st.Close()
+
oldGroup := &Group{
rules: []Rule{
NewRecordingRule("rule1", nil, labels.FromStrings("l1", "v1")),
@@ -771,7 +769,7 @@ func TestUpdate(t *testing.T) {
"test": labels.FromStrings("name", "value"),
}
st := teststorage.New(t)
- defer st.Close()
+
opts := promql.EngineOpts{
Logger: nil,
Reg: nil,
@@ -811,7 +809,7 @@ func TestUpdate(t *testing.T) {
}
// Groups will be recreated if updated.
- rgs, errs := rulefmt.ParseFile("fixtures/rules.yaml", false, model.UTF8Validation)
+ rgs, errs := rulefmt.ParseFile("fixtures/rules.yaml", false, model.UTF8Validation, testParser)
require.Empty(t, errs, "file parsing failures")
tmpFile, err := os.CreateTemp("", "rules.test.*.yaml")
@@ -909,7 +907,7 @@ func reloadAndValidate(rgs *rulefmt.RuleGroups, t *testing.T, tmpFile *os.File,
func TestNotify(t *testing.T) {
storage := teststorage.New(t)
- defer storage.Close()
+
engineOpts := promql.EngineOpts{
Logger: nil,
Reg: nil,
@@ -931,7 +929,7 @@ func TestNotify(t *testing.T) {
ResendDelay: 2 * time.Second,
}
- expr, err := parser.ParseExpr("a > 1")
+ expr, err := testParser.ParseExpr("a > 1")
require.NoError(t, err)
rule := NewAlertingRule("aTooHigh", expr, 0, 0, labels.Labels{}, labels.Labels{}, labels.EmptyLabels(), "", true, promslog.NewNopLogger())
group := NewGroup(GroupOptions{
@@ -983,7 +981,7 @@ func TestMetricsUpdate(t *testing.T) {
}
storage := teststorage.New(t)
- defer storage.Close()
+
registry := prometheus.NewRegistry()
opts := promql.EngineOpts{
Logger: nil,
@@ -1056,7 +1054,7 @@ func TestGroupStalenessOnRemoval(t *testing.T) {
sameFiles := []string{"fixtures/rules2_copy.yaml"}
storage := teststorage.New(t)
- defer storage.Close()
+
opts := promql.EngineOpts{
Logger: nil,
Reg: nil,
@@ -1134,7 +1132,7 @@ func TestMetricsStalenessOnManagerShutdown(t *testing.T) {
files := []string{"fixtures/rules2.yaml"}
storage := teststorage.New(t)
- defer storage.Close()
+
opts := promql.EngineOpts{
Logger: nil,
Reg: nil,
@@ -1201,8 +1199,10 @@ func TestRuleMovedBetweenGroups(t *testing.T) {
t.Skip("skipping test in short mode.")
}
- storage := teststorage.New(t, 600000)
- defer storage.Close()
+ storage := teststorage.New(t, func(opt *tsdb.Options) {
+ opt.OutOfOrderTimeWindow = 600000
+ })
+
opts := promql.EngineOpts{
Logger: nil,
Reg: nil,
@@ -1284,7 +1284,7 @@ func TestGroupHasAlertingRules(t *testing.T) {
func TestRuleHealthUpdates(t *testing.T) {
st := teststorage.New(t)
- defer st.Close()
+
engineOpts := promql.EngineOpts{
Logger: nil,
Reg: nil,
@@ -1300,7 +1300,7 @@ func TestRuleHealthUpdates(t *testing.T) {
Logger: promslog.NewNopLogger(),
}
- expr, err := parser.ParseExpr("a + 1")
+ expr, err := testParser.ParseExpr("a + 1")
require.NoError(t, err)
rule := NewRecordingRule("a_plus_one", expr, labels.Labels{})
group := NewGroup(GroupOptions{
@@ -1345,9 +1345,8 @@ func TestRuleGroupEvalIterationFunc(t *testing.T) {
load 5m
http_requests{instance="0"} 75 85 50 0 0 25 0 0 40 0 120
`)
- t.Cleanup(func() { storage.Close() })
- expr, err := parser.ParseExpr(`http_requests{group="canary", job="app-server"} < 100`)
+ expr, err := testParser.ParseExpr(`http_requests{group="canary", job="app-server"} < 100`)
require.NoError(t, err)
testValue := 1
@@ -1460,7 +1459,6 @@ func TestRuleGroupEvalIterationFunc(t *testing.T) {
func TestNativeHistogramsInRecordingRules(t *testing.T) {
storage := teststorage.New(t)
- t.Cleanup(func() { storage.Close() })
// Add some histograms.
db := storage.DB
@@ -1483,7 +1481,7 @@ func TestNativeHistogramsInRecordingRules(t *testing.T) {
Logger: promslog.NewNopLogger(),
}
- expr, err := parser.ParseExpr("sum(histogram_metric)")
+ expr, err := testParser.ParseExpr("sum(histogram_metric)")
require.NoError(t, err)
rule := NewRecordingRule("sum:histogram_metric", expr, labels.Labels{})
@@ -1522,9 +1520,6 @@ func TestNativeHistogramsInRecordingRules(t *testing.T) {
func TestManager_LoadGroups_ShouldCheckWhetherEachRuleHasDependentsAndDependencies(t *testing.T) {
storage := teststorage.New(t)
- t.Cleanup(func() {
- require.NoError(t, storage.Close())
- })
ruleManager := NewManager(&ManagerOptions{
Context: context.Background(),
@@ -1587,23 +1582,23 @@ func TestDependencyMap(t *testing.T) {
Logger: promslog.NewNopLogger(),
}
- expr, err := parser.ParseExpr("sum by (user) (rate(requests[1m]))")
+ expr, err := testParser.ParseExpr("sum by (user) (rate(requests[1m]))")
require.NoError(t, err)
rule := NewRecordingRule("user:requests:rate1m", expr, labels.Labels{})
- expr, err = parser.ParseExpr("user:requests:rate1m <= 0")
+ expr, err = testParser.ParseExpr("user:requests:rate1m <= 0")
require.NoError(t, err)
rule2 := NewAlertingRule("ZeroRequests", expr, 0, 0, labels.Labels{}, labels.Labels{}, labels.EmptyLabels(), "", true, promslog.NewNopLogger())
- expr, err = parser.ParseExpr("sum by (user) (rate(requests[5m]))")
+ expr, err = testParser.ParseExpr("sum by (user) (rate(requests[5m]))")
require.NoError(t, err)
rule3 := NewRecordingRule("user:requests:rate5m", expr, labels.Labels{})
- expr, err = parser.ParseExpr("increase(user:requests:rate1m[1h])")
+ expr, err = testParser.ParseExpr("increase(user:requests:rate1m[1h])")
require.NoError(t, err)
rule4 := NewRecordingRule("user:requests:increase1h", expr, labels.Labels{})
- expr, err = parser.ParseExpr(`sum by (user) ({__name__=~"user:requests.+5m"})`)
+ expr, err = testParser.ParseExpr(`sum by (user) ({__name__=~"user:requests.+5m"})`)
require.NoError(t, err)
rule5 := NewRecordingRule("user:requests:sum5m", expr, labels.Labels{})
@@ -1645,7 +1640,7 @@ func TestNoDependency(t *testing.T) {
Logger: promslog.NewNopLogger(),
}
- expr, err := parser.ParseExpr("sum by (user) (rate(requests[1m]))")
+ expr, err := testParser.ParseExpr("sum by (user) (rate(requests[1m]))")
require.NoError(t, err)
rule := NewRecordingRule("user:requests:rate1m", expr, labels.Labels{})
@@ -1676,7 +1671,7 @@ func TestDependenciesEdgeCases(t *testing.T) {
Opts: opts,
})
- expr, err := parser.ParseExpr("sum by (user) (rate(requests[1m]))")
+ expr, err := testParser.ParseExpr("sum by (user) (rate(requests[1m]))")
require.NoError(t, err)
rule := NewRecordingRule("user:requests:rate1m", expr, labels.Labels{})
@@ -1687,11 +1682,11 @@ func TestDependenciesEdgeCases(t *testing.T) {
})
t.Run("rules which reference no series", func(t *testing.T) {
- expr, err := parser.ParseExpr("one")
+ expr, err := testParser.ParseExpr("one")
require.NoError(t, err)
rule1 := NewRecordingRule("1", expr, labels.Labels{})
- expr, err = parser.ParseExpr("two")
+ expr, err = testParser.ParseExpr("two")
require.NoError(t, err)
rule2 := NewRecordingRule("2", expr, labels.Labels{})
@@ -1709,11 +1704,11 @@ func TestDependenciesEdgeCases(t *testing.T) {
})
t.Run("rule with regexp matcher on metric name", func(t *testing.T) {
- expr, err := parser.ParseExpr("sum(requests)")
+ expr, err := testParser.ParseExpr("sum(requests)")
require.NoError(t, err)
rule1 := NewRecordingRule("first", expr, labels.Labels{})
- expr, err = parser.ParseExpr(`sum({__name__=~".+"})`)
+ expr, err = testParser.ParseExpr(`sum({__name__=~".+"})`)
require.NoError(t, err)
rule2 := NewRecordingRule("second", expr, labels.Labels{})
@@ -1731,11 +1726,11 @@ func TestDependenciesEdgeCases(t *testing.T) {
})
t.Run("rule with not equal matcher on metric name", func(t *testing.T) {
- expr, err := parser.ParseExpr("sum(requests)")
+ expr, err := testParser.ParseExpr("sum(requests)")
require.NoError(t, err)
rule1 := NewRecordingRule("first", expr, labels.Labels{})
- expr, err = parser.ParseExpr(`sum({__name__!="requests", service="app"})`)
+ expr, err = testParser.ParseExpr(`sum({__name__!="requests", service="app"})`)
require.NoError(t, err)
rule2 := NewRecordingRule("second", expr, labels.Labels{})
@@ -1753,11 +1748,11 @@ func TestDependenciesEdgeCases(t *testing.T) {
})
t.Run("rule with not regexp matcher on metric name", func(t *testing.T) {
- expr, err := parser.ParseExpr("sum(requests)")
+ expr, err := testParser.ParseExpr("sum(requests)")
require.NoError(t, err)
rule1 := NewRecordingRule("first", expr, labels.Labels{})
- expr, err = parser.ParseExpr(`sum({__name__!~"requests.+", service="app"})`)
+ expr, err = testParser.ParseExpr(`sum({__name__!~"requests.+", service="app"})`)
require.NoError(t, err)
rule2 := NewRecordingRule("second", expr, labels.Labels{})
@@ -1777,27 +1772,27 @@ func TestDependenciesEdgeCases(t *testing.T) {
for _, metaMetric := range []string{alertMetricName, alertForStateMetricName} {
t.Run(metaMetric, func(t *testing.T) {
t.Run("rule querying alerts meta-metric with alertname", func(t *testing.T) {
- expr, err := parser.ParseExpr("sum(requests) > 0")
+ expr, err := testParser.ParseExpr("sum(requests) > 0")
require.NoError(t, err)
rule1 := NewAlertingRule("first", expr, 0, 0, labels.Labels{}, labels.Labels{}, labels.EmptyLabels(), "", true, promslog.NewNopLogger())
- expr, err = parser.ParseExpr(fmt.Sprintf(`sum(%s{alertname="test"}) > 0`, metaMetric))
+ expr, err = testParser.ParseExpr(fmt.Sprintf(`sum(%s{alertname="test"}) > 0`, metaMetric))
require.NoError(t, err)
rule2 := NewAlertingRule("second", expr, 0, 0, labels.Labels{}, labels.Labels{}, labels.EmptyLabels(), "", true, promslog.NewNopLogger())
- expr, err = parser.ParseExpr(fmt.Sprintf(`sum(%s{alertname=~"first.*"}) > 0`, metaMetric))
+ expr, err = testParser.ParseExpr(fmt.Sprintf(`sum(%s{alertname=~"first.*"}) > 0`, metaMetric))
require.NoError(t, err)
rule3 := NewAlertingRule("third", expr, 0, 0, labels.Labels{}, labels.Labels{}, labels.EmptyLabels(), "", true, promslog.NewNopLogger())
- expr, err = parser.ParseExpr(fmt.Sprintf(`sum(%s{alertname!="first"}) > 0`, metaMetric))
+ expr, err = testParser.ParseExpr(fmt.Sprintf(`sum(%s{alertname!="first"}) > 0`, metaMetric))
require.NoError(t, err)
rule4 := NewAlertingRule("fourth", expr, 0, 0, labels.Labels{}, labels.Labels{}, labels.EmptyLabels(), "", true, promslog.NewNopLogger())
- expr, err = parser.ParseExpr("sum(failures)")
+ expr, err = testParser.ParseExpr("sum(failures)")
require.NoError(t, err)
rule5 := NewRecordingRule("fifth", expr, labels.Labels{})
- expr, err = parser.ParseExpr(fmt.Sprintf(`fifth > 0 and sum(%s{alertname="fourth"}) > 0`, metaMetric))
+ expr, err = testParser.ParseExpr(fmt.Sprintf(`fifth > 0 and sum(%s{alertname="fourth"}) > 0`, metaMetric))
require.NoError(t, err)
rule6 := NewAlertingRule("sixth", expr, 0, 0, labels.Labels{}, labels.Labels{}, labels.EmptyLabels(), "", true, promslog.NewNopLogger())
@@ -1836,23 +1831,23 @@ func TestDependenciesEdgeCases(t *testing.T) {
})
t.Run("rule querying alerts meta-metric without alertname", func(t *testing.T) {
- expr, err := parser.ParseExpr("sum(requests)")
+ expr, err := testParser.ParseExpr("sum(requests)")
require.NoError(t, err)
rule1 := NewRecordingRule("first", expr, labels.Labels{})
- expr, err = parser.ParseExpr(`sum(requests) > 0`)
+ expr, err = testParser.ParseExpr(`sum(requests) > 0`)
require.NoError(t, err)
rule2 := NewAlertingRule("second", expr, 0, 0, labels.Labels{}, labels.Labels{}, labels.EmptyLabels(), "", true, promslog.NewNopLogger())
- expr, err = parser.ParseExpr(fmt.Sprintf(`sum(%s) > 0`, metaMetric))
+ expr, err = testParser.ParseExpr(fmt.Sprintf(`sum(%s) > 0`, metaMetric))
require.NoError(t, err)
rule3 := NewAlertingRule("third", expr, 0, 0, labels.Labels{}, labels.Labels{}, labels.EmptyLabels(), "", true, promslog.NewNopLogger())
- expr, err = parser.ParseExpr("sum(failures)")
+ expr, err = testParser.ParseExpr("sum(failures)")
require.NoError(t, err)
rule4 := NewRecordingRule("fourth", expr, labels.Labels{})
- expr, err = parser.ParseExpr(fmt.Sprintf(`fourth > 0 and sum(%s) > 0`, metaMetric))
+ expr, err = testParser.ParseExpr(fmt.Sprintf(`fourth > 0 and sum(%s) > 0`, metaMetric))
require.NoError(t, err)
rule5 := NewAlertingRule("fifth", expr, 0, 0, labels.Labels{}, labels.Labels{}, labels.EmptyLabels(), "", true, promslog.NewNopLogger())
@@ -1896,11 +1891,11 @@ func TestNoMetricSelector(t *testing.T) {
Logger: promslog.NewNopLogger(),
}
- expr, err := parser.ParseExpr("sum by (user) (rate(requests[1m]))")
+ expr, err := testParser.ParseExpr("sum by (user) (rate(requests[1m]))")
require.NoError(t, err)
rule := NewRecordingRule("user:requests:rate1m", expr, labels.Labels{})
- expr, err = parser.ParseExpr(`count({user="bob"})`)
+ expr, err = testParser.ParseExpr(`count({user="bob"})`)
require.NoError(t, err)
rule2 := NewRecordingRule("user:requests:rate1m", expr, labels.Labels{})
@@ -1925,15 +1920,15 @@ func TestDependentRulesWithNonMetricExpression(t *testing.T) {
Logger: promslog.NewNopLogger(),
}
- expr, err := parser.ParseExpr("sum by (user) (rate(requests[1m]))")
+ expr, err := testParser.ParseExpr("sum by (user) (rate(requests[1m]))")
require.NoError(t, err)
rule := NewRecordingRule("user:requests:rate1m", expr, labels.Labels{})
- expr, err = parser.ParseExpr("user:requests:rate1m <= 0")
+ expr, err = testParser.ParseExpr("user:requests:rate1m <= 0")
require.NoError(t, err)
rule2 := NewAlertingRule("ZeroRequests", expr, 0, 0, labels.Labels{}, labels.Labels{}, labels.EmptyLabels(), "", true, promslog.NewNopLogger())
- expr, err = parser.ParseExpr("3")
+ expr, err = testParser.ParseExpr("3")
require.NoError(t, err)
rule3 := NewRecordingRule("three", expr, labels.Labels{})
@@ -2016,313 +2011,313 @@ func TestDependencyMapUpdatesOnGroupUpdate(t *testing.T) {
func TestAsyncRuleEvaluation(t *testing.T) {
t.Run("synchronous evaluation with independent rules", func(t *testing.T) {
- t.Parallel()
- storage := teststorage.New(t)
- t.Cleanup(func() { storage.Close() })
- inflightQueries := atomic.Int32{}
- maxInflight := atomic.Int32{}
+ synctest.Test(t, func(t *testing.T) {
+ storage := teststorage.New(t)
- ctx, cancel := context.WithCancel(context.Background())
- t.Cleanup(cancel)
+ inflightQueries := atomic.Int32{}
+ maxInflight := atomic.Int32{}
- ruleManager := NewManager(optsFactory(storage, &maxInflight, &inflightQueries, 0))
- groups, errs := ruleManager.LoadGroups(time.Second, labels.EmptyLabels(), "", nil, false, []string{"fixtures/rules_multiple.yaml"}...)
- require.Empty(t, errs)
- require.Len(t, groups, 1)
+ ctx := t.Context()
- expectedRuleCount := 6
- expectedSampleCount := 4
+ ruleManager := NewManager(optsFactory(storage, &maxInflight, &inflightQueries, 0))
+ groups, errs := ruleManager.LoadGroups(time.Second, labels.EmptyLabels(), "", nil, false, []string{"fixtures/rules_multiple.yaml"}...)
+ require.Empty(t, errs)
+ require.Len(t, groups, 1)
- for _, group := range groups {
- require.Len(t, group.rules, expectedRuleCount)
+ expectedRuleCount := 6
+ expectedSampleCount := 4
- start := time.Now()
- DefaultEvalIterationFunc(ctx, group, start)
+ for _, group := range groups {
+ require.Len(t, group.rules, expectedRuleCount)
- // Expected evaluation order
- order := group.opts.RuleConcurrencyController.SplitGroupIntoBatches(ctx, group)
- require.Nil(t, order)
+ start := time.Now()
+ DefaultEvalIterationFunc(ctx, group, start)
- // Never expect more than 1 inflight query at a time.
- require.EqualValues(t, 1, maxInflight.Load())
- // Each rule should take at least 1 second to execute sequentially.
- require.GreaterOrEqual(t, time.Since(start).Seconds(), (time.Duration(expectedRuleCount) * artificialDelay).Seconds())
- // Each recording rule produces one vector.
- require.EqualValues(t, expectedSampleCount, testutil.ToFloat64(group.metrics.GroupSamples))
- // Group duration is higher than the sum of rule durations (group overhead).
- require.GreaterOrEqual(t, group.GetEvaluationTime(), group.GetRuleEvaluationTimeSum())
- }
+ // Expected evaluation order
+ order := group.opts.RuleConcurrencyController.SplitGroupIntoBatches(ctx, group)
+ require.Nil(t, order)
+
+ // Never expect more than 1 inflight query at a time.
+ require.EqualValues(t, 1, maxInflight.Load())
+ // Each rule should take at least 1 second to execute sequentially.
+ require.GreaterOrEqual(t, time.Since(start).Seconds(), (time.Duration(expectedRuleCount) * artificialDelay).Seconds())
+ // Each recording rule produces one vector.
+ require.EqualValues(t, expectedSampleCount, testutil.ToFloat64(group.metrics.GroupSamples))
+ // Group duration is higher than the sum of rule durations (group overhead).
+ require.GreaterOrEqual(t, group.GetEvaluationTime(), group.GetRuleEvaluationTimeSum())
+ }
+ })
})
t.Run("asynchronous evaluation with independent and dependent rules", func(t *testing.T) {
- t.Parallel()
- storage := teststorage.New(t)
- t.Cleanup(func() { storage.Close() })
- inflightQueries := atomic.Int32{}
- maxInflight := atomic.Int32{}
+ synctest.Test(t, func(t *testing.T) {
+ storage := teststorage.New(t)
- ctx, cancel := context.WithCancel(context.Background())
- t.Cleanup(cancel)
+ inflightQueries := atomic.Int32{}
+ maxInflight := atomic.Int32{}
- expectedRuleCount := 6
- expectedSampleCount := 4
- opts := optsFactory(storage, &maxInflight, &inflightQueries, 0)
+ ctx := t.Context()
- // Configure concurrency settings.
- opts.ConcurrentEvalsEnabled = true
- opts.MaxConcurrentEvals = 2
- opts.RuleConcurrencyController = nil
- ruleManager := NewManager(opts)
+ expectedRuleCount := 6
+ expectedSampleCount := 4
+ opts := optsFactory(storage, &maxInflight, &inflightQueries, 0)
- groups, errs := ruleManager.LoadGroups(time.Second, labels.EmptyLabels(), "", nil, false, []string{"fixtures/rules_multiple.yaml"}...)
- require.Empty(t, errs)
- require.Len(t, groups, 1)
+ // Configure concurrency settings.
+ opts.ConcurrentEvalsEnabled = true
+ opts.MaxConcurrentEvals = 2
+ opts.RuleConcurrencyController = nil
+ ruleManager := NewManager(opts)
- for _, group := range groups {
- require.Len(t, group.rules, expectedRuleCount)
+ groups, errs := ruleManager.LoadGroups(time.Second, labels.EmptyLabels(), "", nil, false, []string{"fixtures/rules_multiple.yaml"}...)
+ require.Empty(t, errs)
+ require.Len(t, groups, 1)
- start := time.Now()
- DefaultEvalIterationFunc(ctx, group, start)
+ for _, group := range groups {
+ require.Len(t, group.rules, expectedRuleCount)
- // Max inflight can be 1 synchronous eval and up to MaxConcurrentEvals concurrent evals.
- require.EqualValues(t, opts.MaxConcurrentEvals+1, maxInflight.Load())
- // Some rules should execute concurrently so should complete quicker.
- require.Less(t, time.Since(start).Seconds(), (time.Duration(expectedRuleCount) * artificialDelay).Seconds())
- // Each recording rule produces one vector.
- require.EqualValues(t, expectedSampleCount, testutil.ToFloat64(group.metrics.GroupSamples))
- }
+ start := time.Now()
+ DefaultEvalIterationFunc(ctx, group, start)
+
+ // Max inflight can be 1 synchronous eval and up to MaxConcurrentEvals concurrent evals.
+ require.EqualValues(t, opts.MaxConcurrentEvals+1, maxInflight.Load())
+ // Some rules should execute concurrently so should complete quicker.
+ require.Less(t, time.Since(start).Seconds(), (time.Duration(expectedRuleCount) * artificialDelay).Seconds())
+ // Each recording rule produces one vector.
+ require.EqualValues(t, expectedSampleCount, testutil.ToFloat64(group.metrics.GroupSamples))
+ }
+ })
})
t.Run("asynchronous evaluation of all independent rules, insufficient concurrency", func(t *testing.T) {
- t.Parallel()
- storage := teststorage.New(t)
- t.Cleanup(func() { storage.Close() })
- inflightQueries := atomic.Int32{}
- maxInflight := atomic.Int32{}
+ synctest.Test(t, func(t *testing.T) {
+ storage := teststorage.New(t)
- ctx, cancel := context.WithCancel(context.Background())
- t.Cleanup(cancel)
+ inflightQueries := atomic.Int32{}
+ maxInflight := atomic.Int32{}
- expectedRuleCount := 8
- expectedSampleCount := expectedRuleCount
- opts := optsFactory(storage, &maxInflight, &inflightQueries, 0)
+ ctx := t.Context()
- // Configure concurrency settings.
- opts.ConcurrentEvalsEnabled = true
- opts.MaxConcurrentEvals = 2
- opts.RuleConcurrencyController = nil
- ruleManager := NewManager(opts)
+ expectedRuleCount := 8
+ expectedSampleCount := expectedRuleCount
+ opts := optsFactory(storage, &maxInflight, &inflightQueries, 0)
- groups, errs := ruleManager.LoadGroups(time.Second, labels.EmptyLabels(), "", nil, false, []string{"fixtures/rules_multiple_independent.yaml"}...)
- require.Empty(t, errs)
- require.Len(t, groups, 1)
+ // Configure concurrency settings.
+ opts.ConcurrentEvalsEnabled = true
+ opts.MaxConcurrentEvals = 2
+ opts.RuleConcurrencyController = nil
+ ruleManager := NewManager(opts)
- for _, group := range groups {
- require.Len(t, group.rules, expectedRuleCount)
+ groups, errs := ruleManager.LoadGroups(time.Second, labels.EmptyLabels(), "", nil, false, []string{"fixtures/rules_multiple_independent.yaml"}...)
+ require.Empty(t, errs)
+ require.Len(t, groups, 1)
- start := time.Now()
- DefaultEvalIterationFunc(ctx, group, start)
+ for _, group := range groups {
+ require.Len(t, group.rules, expectedRuleCount)
- // Expected evaluation order (isn't affected by concurrency settings)
- order := group.opts.RuleConcurrencyController.SplitGroupIntoBatches(ctx, group)
- require.Equal(t, []ConcurrentRules{
- {0, 1, 2, 3, 4, 5, 6, 7},
- }, order)
+ start := time.Now()
+ DefaultEvalIterationFunc(ctx, group, start)
- // Max inflight can be 1 synchronous eval and up to MaxConcurrentEvals concurrent evals.
- require.EqualValues(t, opts.MaxConcurrentEvals+1, maxInflight.Load())
- // Some rules should execute concurrently so should complete quicker.
- require.Less(t, time.Since(start).Seconds(), (time.Duration(expectedRuleCount) * artificialDelay).Seconds())
- // Each recording rule produces one vector.
- require.EqualValues(t, expectedSampleCount, testutil.ToFloat64(group.metrics.GroupSamples))
- }
+ // Expected evaluation order (isn't affected by concurrency settings)
+ order := group.opts.RuleConcurrencyController.SplitGroupIntoBatches(ctx, group)
+ require.Equal(t, []ConcurrentRules{
+ {0, 1, 2, 3, 4, 5, 6, 7},
+ }, order)
+
+ // Max inflight can be 1 synchronous eval and up to MaxConcurrentEvals concurrent evals.
+ require.EqualValues(t, opts.MaxConcurrentEvals+1, maxInflight.Load())
+ // Some rules should execute concurrently so should complete quicker.
+ require.Less(t, time.Since(start).Seconds(), (time.Duration(expectedRuleCount) * artificialDelay).Seconds())
+ // Each recording rule produces one vector.
+ require.EqualValues(t, expectedSampleCount, testutil.ToFloat64(group.metrics.GroupSamples))
+ }
+ })
})
t.Run("asynchronous evaluation of all independent rules, sufficient concurrency", func(t *testing.T) {
- t.Parallel()
- storage := teststorage.New(t)
- t.Cleanup(func() { storage.Close() })
- inflightQueries := atomic.Int32{}
- maxInflight := atomic.Int32{}
+ synctest.Test(t, func(t *testing.T) {
+ storage := teststorage.New(t)
- ctx, cancel := context.WithCancel(context.Background())
- t.Cleanup(cancel)
+ inflightQueries := atomic.Int32{}
+ maxInflight := atomic.Int32{}
- expectedRuleCount := 8
- expectedSampleCount := expectedRuleCount
- opts := optsFactory(storage, &maxInflight, &inflightQueries, 0)
+ ctx := t.Context()
- // Configure concurrency settings.
- opts.ConcurrentEvalsEnabled = true
- opts.MaxConcurrentEvals = int64(expectedRuleCount) * 2
- opts.RuleConcurrencyController = nil
- ruleManager := NewManager(opts)
+ expectedRuleCount := 8
+ expectedSampleCount := expectedRuleCount
+ opts := optsFactory(storage, &maxInflight, &inflightQueries, 0)
- groups, errs := ruleManager.LoadGroups(time.Second, labels.EmptyLabels(), "", nil, false, []string{"fixtures/rules_multiple_independent.yaml"}...)
- require.Empty(t, errs)
- require.Len(t, groups, 1)
+ // Configure concurrency settings.
+ opts.ConcurrentEvalsEnabled = true
+ opts.MaxConcurrentEvals = int64(expectedRuleCount) * 2
+ opts.RuleConcurrencyController = nil
+ ruleManager := NewManager(opts)
- for _, group := range groups {
- require.Len(t, group.rules, expectedRuleCount)
+ groups, errs := ruleManager.LoadGroups(time.Second, labels.EmptyLabels(), "", nil, false, []string{"fixtures/rules_multiple_independent.yaml"}...)
+ require.Empty(t, errs)
+ require.Len(t, groups, 1)
+
+ for _, group := range groups {
+ require.Len(t, group.rules, expectedRuleCount)
+
+ start := time.Now()
+
+ DefaultEvalIterationFunc(ctx, group, start)
+
+ // Expected evaluation order
+ order := group.opts.RuleConcurrencyController.SplitGroupIntoBatches(ctx, group)
+ require.Equal(t, []ConcurrentRules{
+ {0, 1, 2, 3, 4, 5, 6, 7},
+ }, order)
+
+ // Max inflight can be up to MaxConcurrentEvals concurrent evals, since there is sufficient concurrency to run all rules at once.
+ require.LessOrEqual(t, int64(maxInflight.Load()), opts.MaxConcurrentEvals)
+ // Some rules should execute concurrently so should complete quicker.
+ require.Less(t, time.Since(start).Seconds(), (time.Duration(expectedRuleCount) * artificialDelay).Seconds())
+ // Each recording rule produces one vector.
+ require.EqualValues(t, expectedSampleCount, testutil.ToFloat64(group.metrics.GroupSamples))
+ // Group duration is less than the sum of rule durations
+ require.Less(t, group.GetEvaluationTime(), group.GetRuleEvaluationTimeSum())
+ }
+ })
+ })
+
+ t.Run("asynchronous evaluation of independent rules, with indeterminate. Should be synchronous", func(t *testing.T) {
+ synctest.Test(t, func(t *testing.T) {
+ storage := teststorage.New(t)
+
+ inflightQueries := atomic.Int32{}
+ maxInflight := atomic.Int32{}
+
+ ctx := t.Context()
+
+ ruleCount := 7
+ opts := optsFactory(storage, &maxInflight, &inflightQueries, 0)
+
+ // Configure concurrency settings.
+ opts.ConcurrentEvalsEnabled = true
+ opts.MaxConcurrentEvals = int64(ruleCount) * 2
+ opts.RuleConcurrencyController = nil
+ ruleManager := NewManager(opts)
+
+ groups, errs := ruleManager.LoadGroups(time.Second, labels.EmptyLabels(), "", nil, false, []string{"fixtures/rules_indeterminates.yaml"}...)
+ require.Empty(t, errs)
+ require.Len(t, groups, 1)
+
+ for _, group := range groups {
+ require.Len(t, group.rules, ruleCount)
+
+ start := time.Now()
+
+ group.Eval(ctx, start)
+
+ // Never expect more than 1 inflight query at a time.
+ require.EqualValues(t, 1, maxInflight.Load())
+ // Each rule should take at least 1 second to execute sequentially.
+ require.GreaterOrEqual(t, time.Since(start).Seconds(), (time.Duration(ruleCount) * artificialDelay).Seconds())
+ // Each rule produces one vector.
+ require.EqualValues(t, ruleCount, testutil.ToFloat64(group.metrics.GroupSamples))
+ }
+ })
+ })
+
+ t.Run("asynchronous evaluation of rules that benefit from reordering", func(t *testing.T) {
+ synctest.Test(t, func(t *testing.T) {
+ storage := teststorage.New(t)
+
+ inflightQueries := atomic.Int32{}
+ maxInflight := atomic.Int32{}
+
+ ctx := t.Context()
+
+ ruleCount := 8
+ opts := optsFactory(storage, &maxInflight, &inflightQueries, 0)
+
+ // Configure concurrency settings.
+ opts.ConcurrentEvalsEnabled = true
+ opts.MaxConcurrentEvals = int64(ruleCount) * 2
+ opts.RuleConcurrencyController = nil
+ ruleManager := NewManager(opts)
+
+ groups, errs := ruleManager.LoadGroups(time.Second, labels.EmptyLabels(), "", nil, false, []string{"fixtures/rules_multiple_dependents_on_base.yaml"}...)
+ require.Empty(t, errs)
+ require.Len(t, groups, 1)
+ var group *Group
+ for _, g := range groups {
+ group = g
+ }
start := time.Now()
- DefaultEvalIterationFunc(ctx, group, start)
-
// Expected evaluation order
order := group.opts.RuleConcurrencyController.SplitGroupIntoBatches(ctx, group)
require.Equal(t, []ConcurrentRules{
- {0, 1, 2, 3, 4, 5, 6, 7},
+ {0, 4},
+ {1, 2, 3, 5, 6, 7},
}, order)
- // Max inflight can be up to MaxConcurrentEvals concurrent evals, since there is sufficient concurrency to run all rules at once.
- require.LessOrEqual(t, int64(maxInflight.Load()), opts.MaxConcurrentEvals)
- // Some rules should execute concurrently so should complete quicker.
- require.Less(t, time.Since(start).Seconds(), (time.Duration(expectedRuleCount) * artificialDelay).Seconds())
- // Each recording rule produces one vector.
- require.EqualValues(t, expectedSampleCount, testutil.ToFloat64(group.metrics.GroupSamples))
- // Group duration is less than the sum of rule durations
- require.Less(t, group.GetEvaluationTime(), group.GetRuleEvaluationTimeSum())
- }
- })
-
- t.Run("asynchronous evaluation of independent rules, with indeterminate. Should be synchronous", func(t *testing.T) {
- t.Parallel()
- storage := teststorage.New(t)
- t.Cleanup(func() { storage.Close() })
- inflightQueries := atomic.Int32{}
- maxInflight := atomic.Int32{}
-
- ctx, cancel := context.WithCancel(context.Background())
- t.Cleanup(cancel)
-
- ruleCount := 7
- opts := optsFactory(storage, &maxInflight, &inflightQueries, 0)
-
- // Configure concurrency settings.
- opts.ConcurrentEvalsEnabled = true
- opts.MaxConcurrentEvals = int64(ruleCount) * 2
- opts.RuleConcurrencyController = nil
- ruleManager := NewManager(opts)
-
- groups, errs := ruleManager.LoadGroups(time.Second, labels.EmptyLabels(), "", nil, false, []string{"fixtures/rules_indeterminates.yaml"}...)
- require.Empty(t, errs)
- require.Len(t, groups, 1)
-
- for _, group := range groups {
- require.Len(t, group.rules, ruleCount)
-
- start := time.Now()
-
group.Eval(ctx, start)
- // Never expect more than 1 inflight query at a time.
- require.EqualValues(t, 1, maxInflight.Load())
- // Each rule should take at least 1 second to execute sequentially.
- require.GreaterOrEqual(t, time.Since(start).Seconds(), (time.Duration(ruleCount) * artificialDelay).Seconds())
+ // Inflight queries should be equal to 6. This is the size of the second batch of rules that can be executed concurrently.
+ require.EqualValues(t, 6, maxInflight.Load())
+ // Some rules should execute concurrently so should complete quicker.
+ require.Less(t, time.Since(start).Seconds(), (time.Duration(ruleCount) * artificialDelay).Seconds())
// Each rule produces one vector.
require.EqualValues(t, ruleCount, testutil.ToFloat64(group.metrics.GroupSamples))
- }
- })
-
- t.Run("asynchronous evaluation of rules that benefit from reordering", func(t *testing.T) {
- t.Parallel()
- storage := teststorage.New(t)
- t.Cleanup(func() { storage.Close() })
- inflightQueries := atomic.Int32{}
- maxInflight := atomic.Int32{}
-
- ctx, cancel := context.WithCancel(context.Background())
- t.Cleanup(cancel)
-
- ruleCount := 8
- opts := optsFactory(storage, &maxInflight, &inflightQueries, 0)
-
- // Configure concurrency settings.
- opts.ConcurrentEvalsEnabled = true
- opts.MaxConcurrentEvals = int64(ruleCount) * 2
- opts.RuleConcurrencyController = nil
- ruleManager := NewManager(opts)
-
- groups, errs := ruleManager.LoadGroups(time.Second, labels.EmptyLabels(), "", nil, false, []string{"fixtures/rules_multiple_dependents_on_base.yaml"}...)
- require.Empty(t, errs)
- require.Len(t, groups, 1)
- var group *Group
- for _, g := range groups {
- group = g
- }
-
- start := time.Now()
-
- // Expected evaluation order
- order := group.opts.RuleConcurrencyController.SplitGroupIntoBatches(ctx, group)
- require.Equal(t, []ConcurrentRules{
- {0, 4},
- {1, 2, 3, 5, 6, 7},
- }, order)
-
- group.Eval(ctx, start)
-
- // Inflight queries should be equal to 6. This is the size of the second batch of rules that can be executed concurrently.
- require.EqualValues(t, 6, maxInflight.Load())
- // Some rules should execute concurrently so should complete quicker.
- require.Less(t, time.Since(start).Seconds(), (time.Duration(ruleCount) * artificialDelay).Seconds())
- // Each rule produces one vector.
- require.EqualValues(t, ruleCount, testutil.ToFloat64(group.metrics.GroupSamples))
+ })
})
t.Run("attempted asynchronous evaluation of chained rules", func(t *testing.T) {
- t.Parallel()
- storage := teststorage.New(t)
- t.Cleanup(func() { storage.Close() })
- inflightQueries := atomic.Int32{}
- maxInflight := atomic.Int32{}
+ synctest.Test(t, func(t *testing.T) {
+ storage := teststorage.New(t)
- ctx, cancel := context.WithCancel(context.Background())
- t.Cleanup(cancel)
+ inflightQueries := atomic.Int32{}
+ maxInflight := atomic.Int32{}
- ruleCount := 7
- opts := optsFactory(storage, &maxInflight, &inflightQueries, 0)
+ ctx := t.Context()
- // Configure concurrency settings.
- opts.ConcurrentEvalsEnabled = true
- opts.MaxConcurrentEvals = int64(ruleCount) * 2
- opts.RuleConcurrencyController = nil
- ruleManager := NewManager(opts)
+ ruleCount := 7
+ opts := optsFactory(storage, &maxInflight, &inflightQueries, 0)
- groups, errs := ruleManager.LoadGroups(time.Second, labels.EmptyLabels(), "", nil, false, []string{"fixtures/rules_chain.yaml"}...)
- require.Empty(t, errs)
- require.Len(t, groups, 1)
- var group *Group
- for _, g := range groups {
- group = g
- }
+ // Configure concurrency settings.
+ opts.ConcurrentEvalsEnabled = true
+ opts.MaxConcurrentEvals = int64(ruleCount) * 2
+ opts.RuleConcurrencyController = nil
+ ruleManager := NewManager(opts)
- start := time.Now()
+ groups, errs := ruleManager.LoadGroups(time.Second, labels.EmptyLabels(), "", nil, false, []string{"fixtures/rules_chain.yaml"}...)
+ require.Empty(t, errs)
+ require.Len(t, groups, 1)
+ var group *Group
+ for _, g := range groups {
+ group = g
+ }
- // Expected evaluation order
- order := group.opts.RuleConcurrencyController.SplitGroupIntoBatches(ctx, group)
- require.Equal(t, []ConcurrentRules{
- {0, 1},
- {2},
- {3},
- {4, 5, 6},
- }, order)
+ start := time.Now()
- group.Eval(ctx, start)
+ // Expected evaluation order
+ order := group.opts.RuleConcurrencyController.SplitGroupIntoBatches(ctx, group)
+ require.Equal(t, []ConcurrentRules{
+ {0, 1},
+ {2},
+ {3},
+ {4, 5, 6},
+ }, order)
- require.EqualValues(t, 3, maxInflight.Load())
- // Some rules should execute concurrently so should complete quicker.
- require.Less(t, time.Since(start).Seconds(), (time.Duration(ruleCount) * artificialDelay).Seconds())
- // Each rule produces one vector.
- require.EqualValues(t, ruleCount, testutil.ToFloat64(group.metrics.GroupSamples))
+ group.Eval(ctx, start)
+
+ require.EqualValues(t, 3, maxInflight.Load())
+ // Some rules should execute concurrently so should complete quicker.
+ require.Less(t, time.Since(start).Seconds(), (time.Duration(ruleCount) * artificialDelay).Seconds())
+ // Each rule produces one vector.
+ require.EqualValues(t, ruleCount, testutil.ToFloat64(group.metrics.GroupSamples))
+ })
})
}
func TestNewRuleGroupRestoration(t *testing.T) {
t.Parallel()
store := teststorage.New(t)
- t.Cleanup(func() { store.Close() })
+
var (
inflightQueries atomic.Int32
maxInflight atomic.Int32
@@ -2386,7 +2381,7 @@ func TestNewRuleGroupRestoration(t *testing.T) {
func TestNewRuleGroupRestorationWithRestoreNewGroupOption(t *testing.T) {
t.Parallel()
store := teststorage.New(t)
- t.Cleanup(func() { store.Close() })
+
var (
inflightQueries atomic.Int32
maxInflight atomic.Int32
@@ -2456,7 +2451,6 @@ func TestNewRuleGroupRestorationWithRestoreNewGroupOption(t *testing.T) {
func TestBoundedRuleEvalConcurrency(t *testing.T) {
storage := teststorage.New(t)
- t.Cleanup(func() { storage.Close() })
var (
inflightQueries atomic.Int32
@@ -2479,11 +2473,9 @@ func TestBoundedRuleEvalConcurrency(t *testing.T) {
// Evaluate groups concurrently (like they normally do).
var wg sync.WaitGroup
for _, group := range groups {
- wg.Add(1)
- go func() {
+ wg.Go(func() {
group.Eval(ctx, time.Now())
- wg.Done()
- }()
+ })
}
wg.Wait()
@@ -2511,7 +2503,6 @@ func TestUpdateWhenStopped(t *testing.T) {
func TestGroup_Eval_RaceConditionOnStoppingGroupEvaluationWhileRulesAreEvaluatedConcurrently(t *testing.T) {
storage := teststorage.New(t)
- t.Cleanup(func() { storage.Close() })
var (
inflightQueries atomic.Int32
@@ -2603,11 +2594,11 @@ func TestLabels_FromMaps(t *testing.T) {
func TestParseFiles(t *testing.T) {
t.Run("good files", func(t *testing.T) {
- err := ParseFiles([]string{filepath.Join("fixtures", "rules.y*ml")}, model.UTF8Validation)
+ err := ParseFiles([]string{filepath.Join("fixtures", "rules.y*ml")}, model.UTF8Validation, testParser)
require.NoError(t, err)
})
t.Run("bad files", func(t *testing.T) {
- err := ParseFiles([]string{filepath.Join("fixtures", "invalid_rules.y*ml")}, model.UTF8Validation)
+ err := ParseFiles([]string{filepath.Join("fixtures", "invalid_rules.y*ml")}, model.UTF8Validation, testParser)
require.ErrorContains(t, err, "field unexpected_field not found in type rulefmt.Rule")
})
}
@@ -2730,7 +2721,6 @@ func TestRuleDependencyController_AnalyseRules(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
storage := teststorage.New(t)
- t.Cleanup(func() { storage.Close() })
ruleManager := NewManager(&ManagerOptions{
Context: context.Background(),
@@ -2759,7 +2749,6 @@ func TestRuleDependencyController_AnalyseRules(t *testing.T) {
func BenchmarkRuleDependencyController_AnalyseRules(b *testing.B) {
storage := teststorage.New(b)
- b.Cleanup(func() { storage.Close() })
ruleManager := NewManager(&ManagerOptions{
Context: context.Background(),
diff --git a/rules/origin.go b/rules/origin.go
index 695fc5f838..683568c71f 100644
--- a/rules/origin.go
+++ b/rules/origin.go
@@ -1,4 +1,4 @@
-// Copyright 2023 The Prometheus Authors
+// 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
diff --git a/rules/origin_test.go b/rules/origin_test.go
index 16f87de716..55ad927fd9 100644
--- a/rules/origin_test.go
+++ b/rules/origin_test.go
@@ -1,4 +1,4 @@
-// Copyright 2023 The Prometheus Authors
+// 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
diff --git a/rules/recording.go b/rules/recording.go
index 2da6885f5b..61a27aceb6 100644
--- a/rules/recording.go
+++ b/rules/recording.go
@@ -1,4 +1,4 @@
-// Copyright 2013 The Prometheus Authors
+// 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
@@ -30,6 +30,10 @@ import (
"github.com/prometheus/prometheus/promql/parser"
)
+// ErrDuplicateRecordingLabelSet is returned when a recording rule evaluation produces
+// metrics with identical labelsets after applying rule labels.
+var ErrDuplicateRecordingLabelSet = errors.New("vector contains metrics with the same labelset after applying rule labels")
+
// A RecordingRule records its vector expression into new timeseries.
type RecordingRule struct {
name string
@@ -104,7 +108,7 @@ func (rule *RecordingRule) Eval(ctx context.Context, queryOffset time.Duration,
// Check that the rule does not produce identical metrics after applying
// labels.
if vector.ContainsSameLabelset() {
- return nil, errors.New("vector contains metrics with the same labelset after applying rule labels")
+ return nil, ErrDuplicateRecordingLabelSet
}
numSeries := len(vector)
diff --git a/rules/recording_test.go b/rules/recording_test.go
index 014aa85ceb..e59c079d91 100644
--- a/rules/recording_test.go
+++ b/rules/recording_test.go
@@ -1,4 +1,4 @@
-// Copyright 2013 The Prometheus Authors
+// 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
@@ -29,10 +29,12 @@ import (
"github.com/prometheus/prometheus/util/testutil"
)
+var testParser = parser.NewParser(parser.Options{})
+
var (
ruleEvaluationTime = time.Unix(0, 0).UTC()
- exprWithMetricName, _ = parser.ParseExpr(`sort(metric)`)
- exprWithoutMetricName, _ = parser.ParseExpr(`sort(metric + metric)`)
+ exprWithMetricName, _ = testParser.ParseExpr(`sort(metric)`)
+ exprWithoutMetricName, _ = testParser.ParseExpr(`sort(metric + metric)`)
)
var ruleEvalTestScenarios = []struct {
@@ -111,7 +113,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
@@ -121,7 +123,6 @@ func setUpRuleEvalTest(t require.TestingT) *teststorage.TestStorage {
func TestRuleEval(t *testing.T) {
storage := setUpRuleEvalTest(t)
- t.Cleanup(func() { storage.Close() })
ng := testEngine(t)
for _, scenario := range ruleEvalTestScenarios {
@@ -158,7 +159,6 @@ func BenchmarkRuleEval(b *testing.B) {
// TestRuleEvalDuplicate tests for duplicate labels in recorded metrics, see #5529.
func TestRuleEvalDuplicate(t *testing.T) {
storage := teststorage.New(t)
- defer storage.Close()
opts := promql.EngineOpts{
Logger: nil,
@@ -172,11 +172,11 @@ func TestRuleEvalDuplicate(t *testing.T) {
now := time.Now()
- expr, _ := parser.ParseExpr(`vector(0) or label_replace(vector(0),"test","x","","")`)
+ expr, _ := testParser.ParseExpr(`vector(0) or label_replace(vector(0),"test","x","","")`)
rule := NewRecordingRule("foo", expr, labels.FromStrings("test", "test"))
_, err := rule.Eval(ctx, 0, now, EngineQueryFunc(engine, storage), nil, 0)
require.Error(t, err)
- require.EqualError(t, err, "vector contains metrics with the same labelset after applying rule labels")
+ require.ErrorIs(t, err, ErrDuplicateRecordingLabelSet)
}
func TestRecordingRuleLimit(t *testing.T) {
@@ -185,7 +185,6 @@ func TestRecordingRuleLimit(t *testing.T) {
metric{label="1"} 1
metric{label="2"} 1
`)
- t.Cleanup(func() { storage.Close() })
tests := []struct {
limit int
@@ -206,7 +205,7 @@ func TestRecordingRuleLimit(t *testing.T) {
},
}
- expr, _ := parser.ParseExpr(`metric > 0`)
+ expr, _ := testParser.ParseExpr(`metric > 0`)
rule := NewRecordingRule(
"foo",
expr,
@@ -241,7 +240,7 @@ func TestRecordingEvalWithOrigin(t *testing.T) {
lbs = labels.FromStrings("foo", "bar")
)
- expr, err := parser.ParseExpr(query)
+ expr, err := testParser.ParseExpr(query)
require.NoError(t, err)
rule := NewRecordingRule(name, expr, lbs)
diff --git a/rules/rule.go b/rules/rule.go
index 33f1755ac5..fc88e22840 100644
--- a/rules/rule.go
+++ b/rules/rule.go
@@ -1,4 +1,4 @@
-// Copyright 2013 The Prometheus Authors
+// 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
diff --git a/schema/labels.go b/schema/labels.go
index 6df7445171..c71e352640 100644
--- a/schema/labels.go
+++ b/schema/labels.go
@@ -1,4 +1,4 @@
-// Copyright 2025 The Prometheus Authors
+// 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
@@ -19,20 +19,10 @@ import (
"github.com/prometheus/prometheus/model/labels"
)
-const (
- // Special label names and selectors for schema.Metadata fields.
- // They are currently private to ensure __name__, __type__ and __unit__ are used
- // together and remain extensible in Prometheus. See NewMetadataFromLabels and Metadata
- // methods for the interactions with the labels package structs.
- metricName = "__name__"
- metricType = "__type__"
- metricUnit = "__unit__"
-)
-
// IsMetadataLabel returns true if the given label name is a special
// schema Metadata label.
func IsMetadataLabel(name string) bool {
- return name == metricName || name == metricType || name == metricUnit
+ return name == model.MetricNameLabel || name == model.MetricTypeLabel || name == model.MetricUnitLabel
}
// Metadata represents the core metric schema/metadata elements that:
@@ -79,13 +69,13 @@ type Metadata struct {
// NewMetadataFromLabels returns the schema metadata from the labels.
func NewMetadataFromLabels(ls labels.Labels) Metadata {
typ := model.MetricTypeUnknown
- if got := ls.Get(metricType); got != "" {
+ if got := ls.Get(model.MetricTypeLabel); got != "" {
typ = model.MetricType(got)
}
return Metadata{
- Name: ls.Get(metricName),
+ Name: ls.Get(model.MetricNameLabel),
Type: typ,
- Unit: ls.Get(metricUnit),
+ Unit: ls.Get(model.MetricUnitLabel),
}
}
@@ -99,11 +89,11 @@ func (m Metadata) IsTypeEmpty() bool {
// IsEmptyFor returns true.
func (m Metadata) IsEmptyFor(labelName string) bool {
switch labelName {
- case metricName:
+ case model.MetricNameLabel:
return m.Name == ""
- case metricType:
+ case model.MetricTypeLabel:
return m.IsTypeEmpty()
- case metricUnit:
+ case model.MetricUnitLabel:
return m.Unit == ""
default:
return true
@@ -114,13 +104,13 @@ func (m Metadata) IsEmptyFor(labelName string) bool {
// Empty Metadata fields will be ignored (not added).
func (m Metadata) AddToLabels(b *labels.ScratchBuilder) {
if m.Name != "" {
- b.Add(metricName, m.Name)
+ b.Add(model.MetricNameLabel, m.Name)
}
if !m.IsTypeEmpty() {
- b.Add(metricType, string(m.Type))
+ b.Add(model.MetricTypeLabel, string(m.Type))
}
if m.Unit != "" {
- b.Add(metricUnit, m.Unit)
+ b.Add(model.MetricUnitLabel, m.Unit)
}
}
@@ -128,15 +118,15 @@ func (m Metadata) AddToLabels(b *labels.ScratchBuilder) {
// It follows the labels.Builder.Set semantics, so empty Metadata fields will
// remove the corresponding existing labels if they were previously set.
func (m Metadata) SetToLabels(b *labels.Builder) {
- b.Set(metricName, m.Name)
+ b.Set(model.MetricNameLabel, m.Name)
if m.Type == model.MetricTypeUnknown {
// Unknown equals empty semantically, so remove the label on unknown too as per
// method signature comment.
- b.Set(metricType, "")
+ b.Set(model.MetricTypeLabel, "")
} else {
- b.Set(metricType, string(m.Type))
+ b.Set(model.MetricTypeLabel, string(m.Type))
}
- b.Set(metricUnit, m.Unit)
+ b.Set(model.MetricUnitLabel, m.Unit)
}
// NewIgnoreOverriddenMetadataLabelScratchBuilder creates IgnoreOverriddenMetadataLabelScratchBuilder.
diff --git a/schema/labels_test.go b/schema/labels_test.go
index 57b0401157..c2ba576c4a 100644
--- a/schema/labels_test.go
+++ b/schema/labels_test.go
@@ -1,4 +1,4 @@
-// Copyright 2025 The Prometheus Authors
+// 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
@@ -50,17 +50,17 @@ func TestMetadata(t *testing.T) {
lb.Add("foo", "bar")
if !tcase.emptyName {
- lb.Add(metricName, testMeta.Name)
+ lb.Add(model.MetricNameLabel, testMeta.Name)
expectedMeta.Name = testMeta.Name
}
if !tcase.emptyType {
- lb.Add(metricType, string(testMeta.Type))
+ lb.Add(model.MetricTypeLabel, string(testMeta.Type))
expectedMeta.Type = testMeta.Type
} else {
expectedMeta.Type = model.MetricTypeUnknown
}
if !tcase.emptyUnit {
- lb.Add(metricUnit, testMeta.Unit)
+ lb.Add(model.MetricUnitLabel, testMeta.Unit)
expectedMeta.Unit = testMeta.Unit
}
lb.Sort()
@@ -75,10 +75,10 @@ func TestMetadata(t *testing.T) {
}
{
// Empty methods.
- require.Equal(t, tcase.emptyName, expectedMeta.IsEmptyFor(metricName))
- require.Equal(t, tcase.emptyType, expectedMeta.IsEmptyFor(metricType))
+ require.Equal(t, tcase.emptyName, expectedMeta.IsEmptyFor(model.MetricNameLabel))
+ require.Equal(t, tcase.emptyType, expectedMeta.IsEmptyFor(model.MetricTypeLabel))
require.Equal(t, tcase.emptyType, expectedMeta.IsTypeEmpty())
- require.Equal(t, tcase.emptyUnit, expectedMeta.IsEmptyFor(metricUnit))
+ require.Equal(t, tcase.emptyUnit, expectedMeta.IsEmptyFor(model.MetricUnitLabel))
}
{
// From Metadata to labels for various builders.
@@ -100,7 +100,7 @@ func TestIgnoreOverriddenMetadataLabelsScratchBuilder(t *testing.T) {
// PROM-39 specifies that metadata labels should be sourced primarily from the metadata structures.
// However, the original labels should be preserved IF the metadata structure does not set or support certain information.
// Test those cases with common label interactions.
- incomingLabels := labels.FromStrings(metricName, "different_name", metricType, string(model.MetricTypeSummary), metricUnit, "MB", "foo", "bar")
+ incomingLabels := labels.FromStrings(model.MetricNameLabel, "different_name", model.MetricTypeLabel, string(model.MetricTypeSummary), model.MetricUnitLabel, "MB", "foo", "bar")
for _, tcase := range []struct {
highPrioMeta Metadata
expectedLabels labels.Labels
@@ -114,21 +114,21 @@ func TestIgnoreOverriddenMetadataLabelsScratchBuilder(t *testing.T) {
Type: model.MetricTypeCounter,
Unit: "seconds",
},
- expectedLabels: labels.FromStrings(metricName, "metric_total", metricType, string(model.MetricTypeCounter), metricUnit, "seconds", "foo", "bar"),
+ expectedLabels: labels.FromStrings(model.MetricNameLabel, "metric_total", model.MetricTypeLabel, string(model.MetricTypeCounter), model.MetricUnitLabel, "seconds", "foo", "bar"),
},
{
highPrioMeta: Metadata{
Name: "metric_total",
Type: model.MetricTypeCounter,
},
- expectedLabels: labels.FromStrings(metricName, "metric_total", metricType, string(model.MetricTypeCounter), metricUnit, "MB", "foo", "bar"),
+ expectedLabels: labels.FromStrings(model.MetricNameLabel, "metric_total", model.MetricTypeLabel, string(model.MetricTypeCounter), model.MetricUnitLabel, "MB", "foo", "bar"),
},
{
highPrioMeta: Metadata{
Type: model.MetricTypeCounter,
Unit: "seconds",
},
- expectedLabels: labels.FromStrings(metricName, "different_name", metricType, string(model.MetricTypeCounter), metricUnit, "seconds", "foo", "bar"),
+ expectedLabels: labels.FromStrings(model.MetricNameLabel, "different_name", model.MetricTypeLabel, string(model.MetricTypeCounter), model.MetricUnitLabel, "seconds", "foo", "bar"),
},
{
highPrioMeta: Metadata{
@@ -136,7 +136,7 @@ func TestIgnoreOverriddenMetadataLabelsScratchBuilder(t *testing.T) {
Type: model.MetricTypeUnknown,
Unit: "seconds",
},
- expectedLabels: labels.FromStrings(metricName, "metric_total", metricType, string(model.MetricTypeSummary), metricUnit, "seconds", "foo", "bar"),
+ expectedLabels: labels.FromStrings(model.MetricNameLabel, "metric_total", model.MetricTypeLabel, string(model.MetricTypeSummary), model.MetricUnitLabel, "seconds", "foo", "bar"),
},
} {
t.Run(fmt.Sprintf("meta=%#v", tcase.highPrioMeta), func(t *testing.T) {
diff --git a/scrape/clientprotobuf.go b/scrape/clientprotobuf.go
index 6dc22c959f..d84d4bebfc 100644
--- a/scrape/clientprotobuf.go
+++ b/scrape/clientprotobuf.go
@@ -1,4 +1,4 @@
-// Copyright 2023 The Prometheus Authors
+// 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
diff --git a/scrape/helpers_test.go b/scrape/helpers_test.go
index abc2011bef..1db229561d 100644
--- a/scrape/helpers_test.go
+++ b/scrape/helpers_test.go
@@ -1,4 +1,4 @@
-// Copyright 2013 The Prometheus Authors
+// 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
@@ -18,239 +18,138 @@ import (
"context"
"encoding/binary"
"fmt"
- "math"
- "math/rand"
- "strings"
- "sync"
+ "net/http"
"testing"
+ "time"
"github.com/gogo/protobuf/proto"
dto "github.com/prometheus/client_model/go"
+ "github.com/prometheus/common/model"
+ "github.com/prometheus/common/promslog"
"github.com/stretchr/testify/require"
- "github.com/prometheus/prometheus/model/exemplar"
+ "github.com/prometheus/prometheus/config"
"github.com/prometheus/prometheus/model/histogram"
"github.com/prometheus/prometheus/model/labels"
- "github.com/prometheus/prometheus/model/metadata"
"github.com/prometheus/prometheus/storage"
+ "github.com/prometheus/prometheus/util/pool"
+ "github.com/prometheus/prometheus/util/teststorage"
)
-type nopAppendable struct{}
+// For readability.
+type sample = teststorage.Sample
-func (nopAppendable) Appender(context.Context) storage.Appender {
- return nopAppender{}
+type compatAppendable interface {
+ storage.Appendable
+ storage.AppendableV2
}
-type nopAppender struct{}
-
-func (nopAppender) SetOptions(*storage.AppendOptions) {}
-
-func (nopAppender) Append(storage.SeriesRef, labels.Labels, int64, float64) (storage.SeriesRef, error) {
- return 1, nil
-}
-
-func (nopAppender) AppendExemplar(storage.SeriesRef, labels.Labels, exemplar.Exemplar) (storage.SeriesRef, error) {
- return 2, nil
-}
-
-func (nopAppender) AppendHistogram(storage.SeriesRef, labels.Labels, int64, *histogram.Histogram, *histogram.FloatHistogram) (storage.SeriesRef, error) {
- return 3, nil
-}
-
-func (nopAppender) AppendHistogramCTZeroSample(storage.SeriesRef, labels.Labels, int64, int64, *histogram.Histogram, *histogram.FloatHistogram) (storage.SeriesRef, error) {
- return 0, nil
-}
-
-func (nopAppender) UpdateMetadata(storage.SeriesRef, labels.Labels, metadata.Metadata) (storage.SeriesRef, error) {
- return 4, nil
-}
-
-func (nopAppender) AppendCTZeroSample(storage.SeriesRef, labels.Labels, int64, int64) (storage.SeriesRef, error) {
- return 5, nil
-}
-
-func (nopAppender) Commit() error { return nil }
-func (nopAppender) Rollback() error { return nil }
-
-type floatSample struct {
- metric labels.Labels
- t int64
- f float64
-}
-
-func equalFloatSamples(a, b floatSample) bool {
- // Compare Float64bits so NaN values which are exactly the same will compare equal.
- return labels.Equal(a.metric, b.metric) && a.t == b.t && math.Float64bits(a.f) == math.Float64bits(b.f)
-}
-
-type histogramSample struct {
- metric labels.Labels
- t int64
- h *histogram.Histogram
- fh *histogram.FloatHistogram
-}
-
-type metadataEntry struct {
- m metadata.Metadata
- metric labels.Labels
-}
-
-func metadataEntryEqual(a, b metadataEntry) bool {
- if !labels.Equal(a.metric, b.metric) {
- return false
+func withCtx(ctx context.Context) func(sl *scrapeLoop) {
+ return func(sl *scrapeLoop) {
+ sl.ctx = ctx
}
- if a.m.Type != b.m.Type {
- return false
- }
- if a.m.Unit != b.m.Unit {
- return false
- }
- if a.m.Help != b.m.Help {
- return false
- }
- return true
}
-type collectResultAppendable struct {
- *collectResultAppender
-}
-
-func (a *collectResultAppendable) Appender(context.Context) storage.Appender {
- return a
-}
-
-// collectResultAppender records all samples that were added through the appender.
-// It can be used as its zero value or be backed by another appender it writes samples through.
-type collectResultAppender struct {
- mtx sync.Mutex
-
- next storage.Appender
- resultFloats []floatSample
- pendingFloats []floatSample
- rolledbackFloats []floatSample
- resultHistograms []histogramSample
- pendingHistograms []histogramSample
- rolledbackHistograms []histogramSample
- resultExemplars []exemplar.Exemplar
- pendingExemplars []exemplar.Exemplar
- resultMetadata []metadataEntry
- pendingMetadata []metadataEntry
-}
-
-func (*collectResultAppender) SetOptions(*storage.AppendOptions) {}
-
-func (a *collectResultAppender) Append(ref storage.SeriesRef, lset labels.Labels, t int64, v float64) (storage.SeriesRef, error) {
- a.mtx.Lock()
- defer a.mtx.Unlock()
- a.pendingFloats = append(a.pendingFloats, floatSample{
- metric: lset,
- t: t,
- f: v,
- })
-
- if ref == 0 {
- ref = storage.SeriesRef(rand.Uint64())
+func withAppendable(app compatAppendable, appV2 bool) func(sl *scrapeLoop) {
+ return func(sl *scrapeLoop) {
+ sa := selectAppendable(app, appV2)
+ sl.appendable = sa.V1()
+ sl.appendableV2 = sa.V2()
}
- if a.next == nil {
- return ref, nil
+}
+
+// newTestScrapeLoop is the initial scrape loop for all tests.
+// It returns scrapeLoop and mock scraper you can customize.
+//
+// It's recommended to use withXYZ functions for simple option customizations, e.g:
+//
+// sl, _ := newTestScrapeLoop(t, withCtx(customCtx))
+//
+// However, when changing more than one scrapeLoop options it's more readable to have one explicit opt function:
+//
+// ctx, cancel := context.WithCancel(t.Context())
+// appTest := teststorage.NewAppendable()
+// sl, scraper := newTestScrapeLoop(t, func(sl *scrapeLoop) {
+// sl.ctx = ctx
+// sl.appendableV2 = appTest
+// // Since we're writing samples directly below we need to provide a protocol fallback.
+// sl.fallbackScrapeProtocol = "text/plain"
+// })
+//
+// NOTE: Try to NOT add more parameter to this function. Try to NOT add more
+// newTestScrapeLoop-like constructors. It should be flexible enough with scrapeLoop
+// used for initial options.
+func newTestScrapeLoop(t testing.TB, opts ...func(sl *scrapeLoop)) (_ *scrapeLoop, scraper *testScraper) {
+ metrics := newTestScrapeMetrics(t)
+ sl := &scrapeLoop{
+ stopped: make(chan struct{}),
+
+ l: promslog.NewNopLogger(),
+ cache: newScrapeCache(metrics),
+
+ interval: 10 * time.Millisecond,
+ timeout: 1 * time.Hour,
+ sampleMutator: nopMutator,
+ reportSampleMutator: nopMutator,
+ buffers: pool.New(1e3, 1e6, 3, func(sz int) any { return make([]byte, 0, sz) }),
+ metrics: metrics,
+ maxSchema: histogram.ExponentialSchemaMax,
+ honorTimestamps: true,
+ enableCompression: true,
+ validationScheme: model.UTF8Validation,
+ symbolTable: labels.NewSymbolTable(),
+ appendMetadataToWAL: true, // Tests assumes it's enabled, unless explicitly turned off.
+ }
+ for _, o := range opts {
+ o(sl)
}
- ref, err := a.next.Append(ref, lset, t, v)
- if err != nil {
- return 0, err
+ if sl.appendable != nil && sl.appendableV2 != nil {
+ t.Fatal("select the appendable to use, both were passed, likely a bug")
}
- return ref, nil
+
+ // 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")
+ require.Nil(t, sl.cancel, "newTestScrapeLoop does not support injecting custom cancel function")
+ require.Nil(t, sl.scraper, "newTestScrapeLoop does not support injecting scraper, it's mocked, use the returned scraper")
+
+ rootCtx := t.Context()
+ // Use sl.ctx for context injection.
+ // True contexts (sl.appenderCtx, sl.parentCtx, sl.ctx) are populated from it
+ if sl.ctx != nil {
+ rootCtx = sl.ctx
+ }
+ ctx, cancel := context.WithCancel(rootCtx)
+ sl.ctx = ctx
+ sl.cancel = cancel
+ sl.appenderCtx = rootCtx
+ sl.parentCtx = rootCtx
+
+ scraper = &testScraper{}
+ sl.scraper = scraper
+ return sl, scraper
}
-func (a *collectResultAppender) AppendExemplar(ref storage.SeriesRef, l labels.Labels, e exemplar.Exemplar) (storage.SeriesRef, error) {
- a.mtx.Lock()
- defer a.mtx.Unlock()
- a.pendingExemplars = append(a.pendingExemplars, e)
- if a.next == nil {
- return 0, nil
- }
+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() {},
+ logger: promslog.NewNopLogger(),
+ config: &config.ScrapeConfig{},
+ options: &Options{},
+ client: http.DefaultClient,
- return a.next.AppendExemplar(ref, l, e)
-}
+ activeTargets: map[uint64]*Target{},
+ loops: map[uint64]loop{},
+ injectTestNewLoop: injectNewLoop,
-func (a *collectResultAppender) AppendHistogram(ref storage.SeriesRef, l labels.Labels, t int64, h *histogram.Histogram, fh *histogram.FloatHistogram) (storage.SeriesRef, error) {
- a.mtx.Lock()
- defer a.mtx.Unlock()
- a.pendingHistograms = append(a.pendingHistograms, histogramSample{h: h, fh: fh, t: t, metric: l})
- if a.next == nil {
- return 0, nil
- }
+ appendable: sa.V1(), appendableV2: sa.V2(),
- return a.next.AppendHistogram(ref, l, t, h, fh)
-}
-
-func (a *collectResultAppender) AppendHistogramCTZeroSample(ref storage.SeriesRef, l labels.Labels, _, ct int64, h *histogram.Histogram, _ *histogram.FloatHistogram) (storage.SeriesRef, error) {
- if h != nil {
- return a.AppendHistogram(ref, l, ct, &histogram.Histogram{}, nil)
+ symbolTable: labels.NewSymbolTable(),
+ metrics: newTestScrapeMetrics(t),
}
- return a.AppendHistogram(ref, l, ct, nil, &histogram.FloatHistogram{})
-}
-
-func (a *collectResultAppender) UpdateMetadata(ref storage.SeriesRef, l labels.Labels, m metadata.Metadata) (storage.SeriesRef, error) {
- a.mtx.Lock()
- defer a.mtx.Unlock()
- a.pendingMetadata = append(a.pendingMetadata, metadataEntry{metric: l, m: m})
- if ref == 0 {
- ref = storage.SeriesRef(rand.Uint64())
- }
- if a.next == nil {
- return ref, nil
- }
-
- return a.next.UpdateMetadata(ref, l, m)
-}
-
-func (a *collectResultAppender) AppendCTZeroSample(ref storage.SeriesRef, l labels.Labels, _, ct int64) (storage.SeriesRef, error) {
- return a.Append(ref, l, ct, 0.0)
-}
-
-func (a *collectResultAppender) Commit() error {
- a.mtx.Lock()
- defer a.mtx.Unlock()
- a.resultFloats = append(a.resultFloats, a.pendingFloats...)
- a.resultExemplars = append(a.resultExemplars, a.pendingExemplars...)
- a.resultHistograms = append(a.resultHistograms, a.pendingHistograms...)
- a.resultMetadata = append(a.resultMetadata, a.pendingMetadata...)
- a.pendingFloats = nil
- a.pendingExemplars = nil
- a.pendingHistograms = nil
- a.pendingMetadata = nil
- if a.next == nil {
- return nil
- }
- return a.next.Commit()
-}
-
-func (a *collectResultAppender) Rollback() error {
- a.mtx.Lock()
- defer a.mtx.Unlock()
- a.rolledbackFloats = a.pendingFloats
- a.rolledbackHistograms = a.pendingHistograms
- a.pendingFloats = nil
- a.pendingHistograms = nil
- if a.next == nil {
- return nil
- }
- return a.next.Rollback()
-}
-
-func (a *collectResultAppender) String() string {
- var sb strings.Builder
- for _, s := range a.resultFloats {
- sb.WriteString(fmt.Sprintf("committed: %s %f %d\n", s.metric, s.f, s.t))
- }
- for _, s := range a.pendingFloats {
- sb.WriteString(fmt.Sprintf("pending: %s %f %d\n", s.metric, s.f, s.t))
- }
- for _, s := range a.rolledbackFloats {
- sb.WriteString(fmt.Sprintf("rolledback: %s %f %d\n", s.metric, s.f, s.t))
- }
- return sb.String()
}
// protoMarshalDelimited marshals a MetricFamily into a delimited
@@ -271,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 7389f24b52..24a63b056b 100644
--- a/scrape/manager.go
+++ b/scrape/manager.go
@@ -1,4 +1,4 @@
-// Copyright 2013 The Prometheus Authors
+// 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
@@ -33,19 +33,41 @@ import (
"github.com/prometheus/prometheus/discovery/targetgroup"
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/storage"
+ "github.com/prometheus/prometheus/util/features"
"github.com/prometheus/prometheus/util/logging"
"github.com/prometheus/prometheus/util/osutil"
"github.com/prometheus/prometheus/util/pool"
)
-// NewManager is the Manager constructor.
-func NewManager(o *Options, logger *slog.Logger, newScrapeFailureLogger func(string) (*logging.JSONFileLogger, error), app storage.Appendable, registerer prometheus.Registerer) (*Manager, error) {
+// NewManager is the Manager constructor using storage.Appendable or storage.AppendableV2.
+//
+// If unsure which one to use/implement, implement AppendableV2 as it significantly simplifies implementation and allows more
+// (passing ST, always-on metadata, exemplars per sample).
+//
+// NewManager returns error if both appendable and appendableV2 are specified.
+//
+// Switch to AppendableV2 is in progress (https://github.com/prometheus/prometheus/issues/17632).
+// storage.Appendable will be removed soon (ETA: Q2 2026).
+func NewManager(
+ o *Options,
+ logger *slog.Logger,
+ newScrapeFailureLogger func(string) (*logging.JSONFileLogger, error),
+ appendable storage.Appendable,
+ appendableV2 storage.AppendableV2,
+ registerer prometheus.Registerer,
+) (*Manager, error) {
if o == nil {
o = &Options{}
}
if logger == nil {
logger = promslog.NewNopLogger()
}
+ if appendable != nil && appendableV2 != nil {
+ return nil, errors.New("scrape.NewManager: appendable and appendableV2 cannot be provided at the same time")
+ }
+ if appendable == nil && appendableV2 == nil {
+ return nil, errors.New("scrape.NewManager: provide either appendable or appendableV2")
+ }
sm, err := newScrapeMetrics(registerer)
if err != nil {
@@ -53,7 +75,8 @@ func NewManager(o *Options, logger *slog.Logger, newScrapeFailureLogger func(str
}
m := &Manager{
- append: app,
+ appendable: appendable,
+ appendableV2: appendableV2,
opts: o,
logger: logger,
newScrapeFailureLogger: newScrapeFailureLogger,
@@ -67,32 +90,42 @@ func NewManager(o *Options, logger *slog.Logger, newScrapeFailureLogger func(str
m.metrics.setTargetMetadataCacheGatherer(m)
+ // Register scrape features.
+ if r := o.FeatureRegistry; r != nil {
+ // "Extra scrape metrics" is always enabled because it moved from feature flag to config file.
+ r.Enable(features.Scrape, "extra_scrape_metrics")
+ r.Set(features.Scrape, "start_timestamp_zero_ingestion", o.EnableStartTimestampZeroIngestion)
+ r.Set(features.Scrape, "type_and_unit_labels", o.EnableTypeAndUnitLabels)
+ }
+
return m, nil
}
// Options are the configuration parameters to the scrape manager.
type Options struct {
- ExtraMetrics bool
// Option used by downstream scraper users like OpenTelemetry Collector
// to help lookup metric metadata. Should be false for Prometheus.
PassMetadataInContext bool
// Option to enable appending of scraped Metadata to the TSDB/other appenders. Individual appenders
// can decide what to do with metadata, but for practical purposes this flag exists so that metadata
// can be written to the WAL and thus read for remote write.
- // TODO: implement some form of metadata storage
AppendMetadata bool
// Option to increase the interval used by scrape manager to throttle target groups updates.
DiscoveryReloadInterval model.Duration
+
// Option to enable the ingestion of the created timestamp as a synthetic zero sample.
// See: https://github.com/prometheus/proposals/blob/main/proposals/2023-06-13_created-timestamp.md
- EnableCreatedTimestampZeroIngestion bool
+ EnableStartTimestampZeroIngestion bool
- // EnableTypeAndUnitLabels
+ // EnableTypeAndUnitLabels represents type-and-unit-labels feature flag.
EnableTypeAndUnitLabels bool
// Optional HTTP client options to use when scraping.
HTTPClientOptions []config_util.HTTPClientOption
+ // FeatureRegistry is the registry for tracking enabled/disabled features.
+ FeatureRegistry features.Collector
+
// private option for testability.
skipOffsetting bool
}
@@ -100,9 +133,12 @@ type Options struct {
// Manager maintains a set of scrape pools and manages start/stop cycles
// when receiving new target groups from the discovery manager.
type Manager struct {
- opts *Options
- logger *slog.Logger
- append storage.Appendable
+ opts *Options
+ logger *slog.Logger
+
+ appendable storage.Appendable
+ appendableV2 storage.AppendableV2
+
graceShut chan struct{}
offsetSeed uint64 // Global offsetSeed seed is used to spread scrape workload across HA setup.
@@ -183,7 +219,7 @@ func (m *Manager) reload() {
continue
}
m.metrics.targetScrapePools.Inc()
- sp, err := newScrapePool(scrapeConfig, m.append, 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 a4f3552f82..395cc98a82 100644
--- a/scrape/manager_test.go
+++ b/scrape/manager_test.go
@@ -1,4 +1,4 @@
-// Copyright 2013 The Prometheus Authors
+// 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
@@ -51,6 +51,7 @@ import (
"github.com/prometheus/prometheus/storage"
"github.com/prometheus/prometheus/tsdb/tsdbutil"
"github.com/prometheus/prometheus/util/runutil"
+ "github.com/prometheus/prometheus/util/teststorage"
"github.com/prometheus/prometheus/util/testutil"
)
@@ -521,27 +522,18 @@ scrape_configs:
)
opts := Options{}
- scrapeManager, err := NewManager(&opts, nil, nil, nil, testRegistry)
+ scrapeManager, err := NewManager(&opts, nil, nil, nil, teststorage.NewAppendable(), testRegistry)
require.NoError(t, err)
newLoop := func(scrapeLoopOptions) loop {
ch <- struct{}{}
return noopLoop()
}
- sp := &scrapePool{
- appendable: &nopAppendable{},
- activeTargets: map[uint64]*Target{
- 1: {},
- },
- loops: map[uint64]loop{
- 1: noopLoop(),
- },
- newLoop: newLoop,
- logger: nil,
- config: cfg1.ScrapeConfigs[0],
- client: http.DefaultClient,
- metrics: scrapeManager.metrics,
- symbolTable: labels.NewSymbolTable(),
- }
+ sp := newTestScrapePool(t, nil, false, newLoop)
+ sp.activeTargets[1] = &Target{}
+ sp.loops[1] = noopLoop()
+ sp.config = cfg1.ScrapeConfigs[0]
+ sp.metrics = scrapeManager.metrics
+
scrapeManager.scrapePools = map[string]*scrapePool{
"job1": sp,
}
@@ -586,11 +578,11 @@ scrape_configs:
func TestManagerTargetsUpdates(t *testing.T) {
opts := Options{}
testRegistry := prometheus.NewRegistry()
- m, err := NewManager(&opts, nil, nil, nil, testRegistry)
+ m, err := NewManager(&opts, nil, nil, nil, teststorage.NewAppendable(), testRegistry)
require.NoError(t, err)
- ts := make(chan map[string][]*targetgroup.Group)
- go m.Run(ts)
+ targetSetsCh := make(chan map[string][]*targetgroup.Group)
+ go m.Run(targetSetsCh)
defer m.Stop()
tgSent := make(map[string][]*targetgroup.Group)
@@ -602,7 +594,7 @@ func TestManagerTargetsUpdates(t *testing.T) {
}
select {
- case ts <- tgSent:
+ case targetSetsCh <- tgSent:
case <-time.After(10 * time.Millisecond):
require.Fail(t, "Scrape manager's channel remained blocked after the set threshold.")
}
@@ -639,7 +631,7 @@ global:
opts := Options{}
testRegistry := prometheus.NewRegistry()
- scrapeManager, err := NewManager(&opts, nil, nil, nil, testRegistry)
+ scrapeManager, err := NewManager(&opts, nil, nil, nil, teststorage.NewAppendable(), testRegistry)
require.NoError(t, err)
// Load the first config.
@@ -691,18 +683,11 @@ scrape_configs:
for _, sc := range cfg.ScrapeConfigs {
_, cancel := context.WithCancel(context.Background())
defer cancel()
- sp := &scrapePool{
- appendable: &nopAppendable{},
- activeTargets: map[uint64]*Target{},
- loops: map[uint64]loop{
- 1: noopLoop(),
- },
- newLoop: newLoop,
- logger: nil,
- config: sc,
- client: http.DefaultClient,
- cancel: cancel,
- }
+
+ sp := newTestScrapePool(t, nil, false, newLoop)
+ sp.loops[1] = noopLoop()
+ sp.config = cfg1.ScrapeConfigs[0]
+ sp.metrics = scrapeManager.metrics
for _, c := range sc.ServiceDiscoveryConfigs {
staticConfig := c.(discovery.StaticConfig)
for _, group := range staticConfig {
@@ -716,7 +701,7 @@ scrape_configs:
}
opts := Options{}
- scrapeManager, err := NewManager(&opts, nil, nil, nil, testRegistry)
+ scrapeManager, err := NewManager(&opts, nil, nil, nil, teststorage.NewAppendable(), testRegistry)
require.NoError(t, err)
reload(scrapeManager, cfg1)
@@ -749,8 +734,10 @@ func setupTestServer(t *testing.T, typ string, toWrite []byte) *httptest.Server
return server
}
-// TestManagerCTZeroIngestion tests scrape manager for various CT cases.
-func TestManagerCTZeroIngestion(t *testing.T) {
+// TestManagerSTZeroIngestion tests scrape manager for various ST cases.
+// NOTE(bwplotka): There is no AppenderV2 test for this STZeroIngestion feature as in V2 flow it's
+// moved to AppenderV2 implementation (e.g. storage) and it's tested there, e.g. tsdb.TestHeadAppenderV2_Append_EnableSTAsZeroSample.
+func TestManagerSTZeroIngestion(t *testing.T) {
t.Parallel()
const (
// _total suffix is required, otherwise expfmt with OMText will mark metric as "unknown"
@@ -761,27 +748,27 @@ func TestManagerCTZeroIngestion(t *testing.T) {
for _, testFormat := range []config.ScrapeProtocol{config.PrometheusProto, config.OpenMetricsText1_0_0} {
t.Run(fmt.Sprintf("format=%s", testFormat), func(t *testing.T) {
- for _, testWithCT := range []bool{false, true} {
- t.Run(fmt.Sprintf("withCT=%v", testWithCT), func(t *testing.T) {
- for _, testCTZeroIngest := range []bool{false, true} {
- t.Run(fmt.Sprintf("ctZeroIngest=%v", testCTZeroIngest), func(t *testing.T) {
+ for _, testWithST := range []bool{false, true} {
+ t.Run(fmt.Sprintf("withST=%v", testWithST), func(t *testing.T) {
+ for _, testSTZeroIngest := range []bool{false, true} {
+ t.Run(fmt.Sprintf("stZeroIngest=%v", testSTZeroIngest), func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
sampleTs := time.Now()
- ctTs := time.Time{}
- if testWithCT {
- ctTs = sampleTs.Add(-2 * time.Minute)
+ stTs := time.Time{}
+ if testWithST {
+ stTs = sampleTs.Add(-2 * time.Minute)
}
// TODO(bwplotka): Add more types than just counter?
- encoded := prepareTestEncodedCounter(t, testFormat, expectedMetricName, expectedSampleValue, sampleTs, ctTs)
+ encoded := prepareTestEncodedCounter(t, testFormat, expectedMetricName, expectedSampleValue, sampleTs, stTs)
- app := &collectResultAppender{}
+ app := teststorage.NewAppendable()
discoveryManager, scrapeManager := runManagers(t, ctx, &Options{
- EnableCreatedTimestampZeroIngestion: testCTZeroIngest,
- skipOffsetting: true,
- }, &collectResultAppendable{app})
+ EnableStartTimestampZeroIngestion: testSTZeroIngest,
+ skipOffsetting: true,
+ }, app, nil)
defer scrapeManager.Stop()
server := setupTestServer(t, config.ScrapeProtocolsHeaders[testFormat], encoded)
@@ -806,44 +793,41 @@ scrape_configs:
ctx, cancel = context.WithTimeout(ctx, 1*time.Minute)
defer cancel()
require.NoError(t, runutil.Retry(100*time.Millisecond, ctx.Done(), func() error {
- app.mtx.Lock()
- defer app.mtx.Unlock()
-
// Check if scrape happened and grab the relevant samples.
- if len(app.resultFloats) > 0 {
+ if len(app.ResultSamples()) > 0 {
return nil
}
return errors.New("expected some float samples, got none")
}), "after 1 minute")
// Verify results.
- // Verify what we got vs expectations around CT injection.
- samples := findSamplesForMetric(app.resultFloats, expectedMetricName)
- if testWithCT && testCTZeroIngest {
- require.Len(t, samples, 2)
- require.Equal(t, 0.0, samples[0].f)
- require.Equal(t, timestamp.FromTime(ctTs), samples[0].t)
- require.Equal(t, expectedSampleValue, samples[1].f)
- require.Equal(t, timestamp.FromTime(sampleTs), samples[1].t)
+ // Verify what we got vs expectations around ST injection.
+ got := findSamplesForMetric(app.ResultSamples(), expectedMetricName)
+ if testWithST && testSTZeroIngest {
+ require.Len(t, got, 2)
+ require.Equal(t, 0.0, got[0].V)
+ require.Equal(t, timestamp.FromTime(stTs), got[0].T)
+ require.Equal(t, expectedSampleValue, got[1].V)
+ require.Equal(t, timestamp.FromTime(sampleTs), got[1].T)
} else {
- require.Len(t, samples, 1)
- require.Equal(t, expectedSampleValue, samples[0].f)
- require.Equal(t, timestamp.FromTime(sampleTs), samples[0].t)
+ require.Len(t, got, 1)
+ require.Equal(t, expectedSampleValue, got[0].V)
+ require.Equal(t, timestamp.FromTime(sampleTs), got[0].T)
}
// Verify what we got vs expectations around additional _created series for OM text.
- // enableCTZeroInjection also kills that _created line.
- createdSeriesSamples := findSamplesForMetric(app.resultFloats, expectedCreatedMetricName)
- if testFormat == config.OpenMetricsText1_0_0 && testWithCT && !testCTZeroIngest {
- // For OM Text, when counter has CT, and feature flag disabled we should see _created lines.
- require.Len(t, createdSeriesSamples, 1)
+ // enableSTZeroInjection also kills that _created line.
+ gotSTSeries := findSamplesForMetric(app.ResultSamples(), expectedCreatedMetricName)
+ if testFormat == config.OpenMetricsText1_0_0 && testWithST && !testSTZeroIngest {
+ // For OM Text, when counter has ST, and feature flag disabled we should see _created lines.
+ require.Len(t, gotSTSeries, 1)
// Conversion taken from common/expfmt.writeOpenMetricsFloat.
- // We don't check the ct timestamp as explicit ts was not implemented in expfmt.Encoder,
+ // We don't check the st timestamp as explicit ts was not implemented in expfmt.Encoder,
// but exists in OM https://github.com/prometheus/OpenMetrics/blob/v1.0.0/specification/OpenMetrics.md#:~:text=An%20example%20with%20a%20Metric%20with%20no%20labels%2C%20and%20a%20MetricPoint%20with%20a%20timestamp%20and%20a%20created
- // We can implement this, but we want to potentially get rid of OM 1.0 CT lines
- require.Equal(t, float64(timestamppb.New(ctTs).AsTime().UnixNano())/1e9, createdSeriesSamples[0].f)
+ // We can implement this, but we want to potentially get rid of OM 1.0 ST lines
+ require.Equal(t, float64(timestamppb.New(stTs).AsTime().UnixNano())/1e9, gotSTSeries[0].V)
} else {
- require.Empty(t, createdSeriesSamples)
+ require.Empty(t, gotSTSeries)
}
})
}
@@ -853,12 +837,12 @@ scrape_configs:
}
}
-func prepareTestEncodedCounter(t *testing.T, format config.ScrapeProtocol, mName string, v float64, ts, ct time.Time) (encoded []byte) {
+func prepareTestEncodedCounter(t *testing.T, format config.ScrapeProtocol, mName string, v float64, ts, st time.Time) (encoded []byte) {
t.Helper()
counter := &dto.Counter{Value: proto.Float64(v)}
- if !ct.IsZero() {
- counter.CreatedTimestamp = timestamppb.New(ct)
+ if !st.IsZero() {
+ counter.CreatedTimestamp = timestamppb.New(st)
}
ctrType := dto.MetricType_COUNTER
inputMetric := &dto.MetricFamily{
@@ -885,9 +869,9 @@ func prepareTestEncodedCounter(t *testing.T, format config.ScrapeProtocol, mName
}
}
-func findSamplesForMetric(floats []floatSample, metricName string) (ret []floatSample) {
+func findSamplesForMetric(floats []sample, metricName string) (ret []sample) {
for _, f := range floats {
- if f.metric.Get(model.MetricNameLabel) == metricName {
+ if f.L.Get(model.MetricNameLabel) == metricName {
ret = append(ret, f)
}
}
@@ -923,40 +907,42 @@ func generateTestHistogram(i int) *dto.Histogram {
return h
}
-func TestManagerCTZeroIngestionHistogram(t *testing.T) {
+// NOTE(bwplotka): There is no AppenderV2 test for this STZeroIngestion feature as in V2 flow it's
+// moved to AppenderV2 implementation (e.g. storage) and it's tested there, e.g. tsdb.TestHeadAppenderV2_Append_EnableSTAsZeroSample.
+func TestManagerSTZeroIngestionHistogram(t *testing.T) {
t.Parallel()
const mName = "expected_histogram"
for _, tc := range []struct {
name string
inputHistSample *dto.Histogram
- enableCTZeroIngestion bool
+ enableSTZeroIngestion bool
}{
{
- name: "disabled with CT on histogram",
+ name: "disabled with ST on histogram",
inputHistSample: func() *dto.Histogram {
h := generateTestHistogram(0)
h.CreatedTimestamp = timestamppb.Now()
return h
}(),
- enableCTZeroIngestion: false,
+ enableSTZeroIngestion: false,
},
{
- name: "enabled with CT on histogram",
+ name: "enabled with ST on histogram",
inputHistSample: func() *dto.Histogram {
h := generateTestHistogram(0)
h.CreatedTimestamp = timestamppb.Now()
return h
}(),
- enableCTZeroIngestion: true,
+ enableSTZeroIngestion: true,
},
{
- name: "enabled without CT on histogram",
+ name: "enabled without ST on histogram",
inputHistSample: func() *dto.Histogram {
h := generateTestHistogram(0)
return h
}(),
- enableCTZeroIngestion: true,
+ enableSTZeroIngestion: true,
},
} {
t.Run(tc.name, func(t *testing.T) {
@@ -964,11 +950,11 @@ func TestManagerCTZeroIngestionHistogram(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
- app := &collectResultAppender{}
+ app := teststorage.NewAppendable()
discoveryManager, scrapeManager := runManagers(t, ctx, &Options{
- EnableCreatedTimestampZeroIngestion: tc.enableCTZeroIngestion,
- skipOffsetting: true,
- }, &collectResultAppendable{app})
+ EnableStartTimestampZeroIngestion: tc.enableSTZeroIngestion,
+ skipOffsetting: true,
+ }, app, nil)
defer scrapeManager.Stop()
once := sync.Once{}
@@ -1012,43 +998,33 @@ scrape_configs:
`, serverURL.Host)
applyConfig(t, testConfig, scrapeManager, discoveryManager)
- var got []histogramSample
-
// Wait for one scrape.
ctx, cancel = context.WithTimeout(ctx, 1*time.Minute)
defer cancel()
require.NoError(t, runutil.Retry(100*time.Millisecond, ctx.Done(), func() error {
- app.mtx.Lock()
- defer app.mtx.Unlock()
-
- // Check if scrape happened and grab the relevant histograms, they have to be there - or it's a bug
- // and it's not worth waiting.
- for _, h := range app.resultHistograms {
- if h.metric.Get(model.MetricNameLabel) == mName {
- got = append(got, h)
- }
- }
- if len(app.resultHistograms) > 0 {
+ if len(app.ResultSamples()) > 0 {
return nil
}
return errors.New("expected some histogram samples, got none")
}), "after 1 minute")
+ got := findSamplesForMetric(app.ResultSamples(), mName)
+
// Check for zero samples, assuming we only injected always one histogram sample.
- // Did it contain CT to inject? If yes, was CT zero enabled?
- if tc.inputHistSample.CreatedTimestamp.IsValid() && tc.enableCTZeroIngestion {
+ // Did it contain ST to inject? If yes, was ST zero enabled?
+ if tc.inputHistSample.CreatedTimestamp.IsValid() && tc.enableSTZeroIngestion {
require.Len(t, got, 2)
// Zero sample.
- require.Equal(t, histogram.Histogram{}, *got[0].h)
+ require.Equal(t, histogram.Histogram{}, *got[0].H)
// Quick soft check to make sure it's the same sample or at least not zero.
- require.Equal(t, tc.inputHistSample.GetSampleSum(), got[1].h.Sum)
+ require.Equal(t, tc.inputHistSample.GetSampleSum(), got[1].H.Sum)
return
}
// Expect only one, valid sample.
require.Len(t, got, 1)
// Quick soft check to make sure it's the same sample or at least not zero.
- require.Equal(t, tc.inputHistSample.GetSampleSum(), got[0].h.Sum)
+ require.Equal(t, tc.inputHistSample.GetSampleSum(), got[0].H.Sum)
})
}
}
@@ -1058,7 +1034,7 @@ func TestUnregisterMetrics(t *testing.T) {
// Check that all metrics can be unregistered, allowing a second manager to be created.
for range 2 {
opts := Options{}
- manager, err := NewManager(&opts, nil, nil, nil, reg)
+ manager, err := NewManager(&opts, nil, nil, nil, teststorage.NewAppendable(), reg)
require.NotNil(t, manager)
require.NoError(t, err)
// Unregister all metrics.
@@ -1066,12 +1042,15 @@ func TestUnregisterMetrics(t *testing.T) {
}
}
-// TestNHCBAndCTZeroIngestion verifies that both ConvertClassicHistogramsToNHCBEnabled
-// and EnableCreatedTimestampZeroIngestion can be used simultaneously without errors.
+// TestNHCBAndSTZeroIngestion verifies that both ConvertClassicHistogramsToNHCBEnabled
+// and EnableStartTimestampZeroIngestion can be used simultaneously without errors.
// This test addresses issue #17216 by ensuring the previously blocking check has been removed.
// The test verifies that the presence of exemplars in the input does not cause errors,
// although exemplars are not preserved during NHCB conversion (as documented below).
-func TestNHCBAndCTZeroIngestion(t *testing.T) {
+//
+// NOTE(bwplotka): There is no AppenderV2 test for this STZeroIngestion feature as in V2 flow it's
+// moved to AppenderV2 implementation (e.g. storage) and it's tested there, e.g. tsdb.TestHeadAppenderV2_Append_EnableSTAsZeroSample.
+func TestNHCBAndSTZeroIngestion(t *testing.T) {
t.Parallel()
const (
@@ -1083,11 +1062,11 @@ func TestNHCBAndCTZeroIngestion(t *testing.T) {
ctx := t.Context()
- app := &collectResultAppender{}
+ app := teststorage.NewAppendable()
discoveryManager, scrapeManager := runManagers(t, ctx, &Options{
- EnableCreatedTimestampZeroIngestion: true,
- skipOffsetting: true,
- }, &collectResultAppendable{app})
+ EnableStartTimestampZeroIngestion: true,
+ skipOffsetting: true,
+ }, app, nil)
defer scrapeManager.Stop()
once := sync.Once{}
@@ -1122,7 +1101,7 @@ test_histogram_created 1520430001
serverURL, err := url.Parse(server.URL)
require.NoError(t, err)
- // Configuration with both convert_classic_histograms_to_nhcb enabled and CT zero ingestion enabled.
+ // Configuration with both convert_classic_histograms_to_nhcb enabled and ST zero ingestion enabled.
testConfig := fmt.Sprintf(`
global:
# Use a very long scrape_interval to prevent automatic scraping during the test.
@@ -1146,33 +1125,19 @@ scrape_configs:
return exists
}, 5*time.Second, 100*time.Millisecond, "scrape pool should be created for job 'test'")
- // Helper function to get matching histograms to avoid race conditions.
- getMatchingHistograms := func() []histogramSample {
- app.mtx.Lock()
- defer app.mtx.Unlock()
-
- var got []histogramSample
- for _, h := range app.resultHistograms {
- if h.metric.Get(model.MetricNameLabel) == mName {
- got = append(got, h)
- }
- }
- return got
- }
-
require.Eventually(t, func() bool {
- return len(getMatchingHistograms()) > 0
+ return len(app.ResultSamples()) > 0
}, 1*time.Minute, 100*time.Millisecond, "expected histogram samples, got none")
// Verify that samples were ingested (proving both features work together).
- got := getMatchingHistograms()
+ got := findSamplesForMetric(app.ResultSamples(), mName)
- // With CT zero ingestion enabled and a created timestamp present, we expect 2 samples:
+ // With ST zero ingestion enabled and a created timestamp present, we expect 2 samples:
// one zero sample and one actual sample.
require.Len(t, got, 2, "expected 2 histogram samples (zero sample + actual sample)")
- require.Equal(t, histogram.Histogram{}, *got[0].h, "first sample should be zero sample")
- require.InDelta(t, expectedHistogramSum, got[1].h.Sum, 1e-9, "second sample should retain the expected sum")
- require.Len(t, app.resultExemplars, 2, "expected 2 exemplars from histogram buckets")
+ require.Equal(t, histogram.Histogram{}, *got[0].H, "first sample should be zero sample")
+ require.InDelta(t, expectedHistogramSum, got[1].H.Sum, 1e-9, "second sample should retain the expected sum")
+ require.Len(t, got[1].ES, 2, "expected 2 exemplars on second histogram")
}
func applyConfig(
@@ -1195,16 +1160,13 @@ func applyConfig(
require.NoError(t, discoveryManager.ApplyConfig(c))
}
-func runManagers(t *testing.T, ctx context.Context, opts *Options, app storage.Appendable) (*discovery.Manager, *Manager) {
+func runManagers(t *testing.T, ctx context.Context, opts *Options, app storage.Appendable, appV2 storage.AppendableV2) (*discovery.Manager, *Manager) {
t.Helper()
if opts == nil {
opts = &Options{}
}
opts.DiscoveryReloadInterval = model.Duration(100 * time.Millisecond)
- if app == nil {
- app = nopAppendable{}
- }
reg := prometheus.NewRegistry()
sdMetrics, err := discovery.RegisterSDMetrics(reg, discovery.NewRefreshMetrics(reg))
@@ -1220,7 +1182,7 @@ func runManagers(t *testing.T, ctx context.Context, opts *Options, app storage.A
opts,
nil,
nil,
- app,
+ app, appV2,
prometheus.NewRegistry(),
)
require.NoError(t, err)
@@ -1293,7 +1255,7 @@ scrape_configs:
- files: ['%s']
`
- discoveryManager, scrapeManager := runManagers(t, ctx, nil, nil)
+ discoveryManager, scrapeManager := runManagers(t, ctx, nil, nil, teststorage.NewAppendable())
defer scrapeManager.Stop()
applyConfig(
@@ -1392,7 +1354,7 @@ scrape_configs:
file_sd_configs:
- files: ['%s', '%s']
`
- discoveryManager, scrapeManager := runManagers(t, ctx, nil, nil)
+ discoveryManager, scrapeManager := runManagers(t, ctx, nil, nil, teststorage.NewAppendable())
defer scrapeManager.Stop()
applyConfig(
@@ -1451,7 +1413,7 @@ scrape_configs:
file_sd_configs:
- files: ['%s']
`
- discoveryManager, scrapeManager := runManagers(t, ctx, nil, nil)
+ discoveryManager, scrapeManager := runManagers(t, ctx, nil, nil, teststorage.NewAppendable())
defer scrapeManager.Stop()
applyConfig(
@@ -1517,7 +1479,7 @@ scrape_configs:
- targets: ['%s']
`
- discoveryManager, scrapeManager := runManagers(t, ctx, nil, nil)
+ discoveryManager, scrapeManager := runManagers(t, ctx, nil, nil, teststorage.NewAppendable())
defer scrapeManager.Stop()
// Apply the initial config with an existing file
@@ -1601,7 +1563,7 @@ scrape_configs:
cfg := loadConfiguration(t, cfgText)
- m, err := NewManager(&Options{}, nil, nil, &nopAppendable{}, prometheus.NewRegistry())
+ m, err := NewManager(&Options{}, nil, nil, nil, teststorage.NewAppendable(), prometheus.NewRegistry())
require.NoError(t, err)
defer m.Stop()
require.NoError(t, m.ApplyConfig(cfg))
diff --git a/scrape/metrics.go b/scrape/metrics.go
index e7395c6191..34f1e28dba 100644
--- a/scrape/metrics.go
+++ b/scrape/metrics.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The Prometheus Authors
+// 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
@@ -15,6 +15,7 @@ package scrape
import (
"fmt"
+ "time"
"github.com/prometheus/client_golang/prometheus"
)
@@ -36,6 +37,7 @@ type scrapeMetrics struct {
targetScrapePoolTargetsAdded *prometheus.GaugeVec
targetScrapePoolSymbolTableItems *prometheus.GaugeVec
targetSyncIntervalLength *prometheus.SummaryVec
+ targetSyncIntervalLengthHistogram *prometheus.HistogramVec
targetSyncFailed *prometheus.CounterVec
// Used by targetScraper.
@@ -46,6 +48,7 @@ type scrapeMetrics struct {
// Used by scrapeLoop.
targetIntervalLength *prometheus.SummaryVec
+ targetIntervalLengthHistogram *prometheus.HistogramVec
targetScrapeSampleLimit prometheus.Counter
targetScrapeSampleDuplicate prometheus.Counter
targetScrapeSampleOutOfOrder prometheus.Counter
@@ -53,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) {
@@ -152,6 +156,17 @@ func newScrapeMetrics(reg prometheus.Registerer) (*scrapeMetrics, error) {
},
[]string{"scrape_job"},
)
+ sm.targetSyncIntervalLengthHistogram = prometheus.NewHistogramVec(
+ prometheus.HistogramOpts{
+ Name: "prometheus_target_sync_length_histogram_seconds",
+ Help: "Actual interval to sync the scrape pool.",
+ Buckets: []float64{.01, .1, 1, 10},
+ NativeHistogramBucketFactor: 1.1,
+ NativeHistogramMaxBucketNumber: 100,
+ NativeHistogramMinResetDuration: 1 * time.Hour,
+ },
+ []string{"scrape_job"},
+ )
sm.targetSyncFailed = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "prometheus_target_sync_failed_total",
@@ -185,6 +200,17 @@ func newScrapeMetrics(reg prometheus.Registerer) (*scrapeMetrics, error) {
},
[]string{"interval"},
)
+ sm.targetIntervalLengthHistogram = prometheus.NewHistogramVec(
+ prometheus.HistogramOpts{
+ Name: "prometheus_target_interval_length_histogram_seconds",
+ Help: "Actual intervals between scrapes.",
+ Buckets: []float64{.01, .1, 1, 10},
+ NativeHistogramBucketFactor: 1.1,
+ NativeHistogramMaxBucketNumber: 100,
+ NativeHistogramMinResetDuration: 1 * time.Hour,
+ },
+ []string{"interval"},
+ )
sm.targetScrapeSampleLimit = prometheus.NewCounter(
prometheus.CounterOpts{
Name: "prometheus_target_scrapes_exceeded_sample_limit_total",
@@ -227,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.
@@ -238,6 +273,7 @@ func newScrapeMetrics(reg prometheus.Registerer) (*scrapeMetrics, error) {
sm.targetScrapePoolReloads,
sm.targetScrapePoolReloadsFailed,
sm.targetSyncIntervalLength,
+ sm.targetSyncIntervalLengthHistogram,
sm.targetScrapePoolSyncsCounter,
sm.targetScrapePoolExceededTargetLimit,
sm.targetScrapePoolTargetLimit,
@@ -250,6 +286,7 @@ func newScrapeMetrics(reg prometheus.Registerer) (*scrapeMetrics, error) {
sm.targetScrapeCacheFlushForced,
// Used by scrapeLoop.
sm.targetIntervalLength,
+ sm.targetIntervalLengthHistogram,
sm.targetScrapeSampleLimit,
sm.targetScrapeSampleDuplicate,
sm.targetScrapeSampleOutOfOrder,
@@ -257,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 {
@@ -279,6 +317,7 @@ func (sm *scrapeMetrics) Unregister() {
sm.reg.Unregister(sm.targetScrapePoolReloads)
sm.reg.Unregister(sm.targetScrapePoolReloadsFailed)
sm.reg.Unregister(sm.targetSyncIntervalLength)
+ sm.reg.Unregister(sm.targetSyncIntervalLengthHistogram)
sm.reg.Unregister(sm.targetScrapePoolSyncsCounter)
sm.reg.Unregister(sm.targetScrapePoolExceededTargetLimit)
sm.reg.Unregister(sm.targetScrapePoolTargetLimit)
@@ -288,6 +327,7 @@ func (sm *scrapeMetrics) Unregister() {
sm.reg.Unregister(sm.targetScrapeExceededBodySizeLimit)
sm.reg.Unregister(sm.targetScrapeCacheFlushForced)
sm.reg.Unregister(sm.targetIntervalLength)
+ sm.reg.Unregister(sm.targetIntervalLengthHistogram)
sm.reg.Unregister(sm.targetScrapeSampleLimit)
sm.reg.Unregister(sm.targetScrapeSampleDuplicate)
sm.reg.Unregister(sm.targetScrapeSampleOutOfOrder)
@@ -295,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 09652d0484..d5a9ba72b4 100644
--- a/scrape/scrape.go
+++ b/scrape/scrape.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The Prometheus Authors
+// 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
@@ -59,6 +59,8 @@ import (
"github.com/prometheus/prometheus/util/pool"
)
+var aOptionRejectEarlyOOO = storage.AppendOptions{DiscardOutOfOrder: true}
+
// ScrapeTimestampTolerance is the tolerance for scrape appends timestamps
// alignment, to enable better compression at the TSDB level.
// See https://github.com/prometheus/prometheus/issues/7846
@@ -67,7 +69,7 @@ var ScrapeTimestampTolerance = 2 * time.Millisecond
// AlignScrapeTimestamps enables the tolerance for scrape appends timestamps described above.
var AlignScrapeTimestamps = true
-var errNameLabelMandatory = fmt.Errorf("missing metric name (%s label)", labels.MetricName)
+var errNameLabelMandatory = fmt.Errorf("missing metric name (%s label)", model.MetricNameLabel)
var _ FailureLogger = (*logging.JSONFileLogger)(nil)
@@ -80,10 +82,12 @@ type FailureLogger interface {
// scrapePool manages scrapes for sets of targets.
type scrapePool struct {
- appendable storage.Appendable
- logger *slog.Logger
- cancel context.CancelFunc
- httpOpts []config_util.HTTPClientOption
+ 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
@@ -102,16 +106,15 @@ type scrapePool struct {
droppedTargets []*Target // Subject to KeepDroppedTargets limit.
droppedTargetsCount int // Count of all dropped targets.
- // Constructor for new scrape loops. This is settable for testing convenience.
- newLoop func(scrapeLoopOptions) loop
+ // newLoop injection for testing purposes.
+ injectTestNewLoop func(scrapeLoopOptions) loop
- metrics *scrapeMetrics
+ metrics *scrapeMetrics
+ buffers *pool.Pool
+ offsetSeed uint64
scrapeFailureLogger FailureLogger
scrapeFailureLoggerMtx sync.RWMutex
-
- validationScheme model.ValidationScheme
- escapingScheme model.EscapingScheme
}
type labelLimits struct {
@@ -120,118 +123,82 @@ type labelLimits struct {
labelValueLengthLimit int
}
-type scrapeLoopOptions struct {
- target *Target
- scraper scraper
- sampleLimit int
- bucketLimit int
- maxSchema int32
- labelLimits *labelLimits
- honorLabels bool
- honorTimestamps bool
- trackTimestampsStaleness bool
- interval time.Duration
- timeout time.Duration
- scrapeNativeHist bool
- alwaysScrapeClassicHist bool
- convertClassicHistToNHCB bool
- fallbackScrapeProtocol string
-
- mrc []*relabel.Config
- cache *scrapeCache
- enableCompression bool
-}
-
const maxAheadTime = 10 * time.Minute
// returning an empty label set is interpreted as "drop".
type labelsMutator func(labels.Labels) labels.Labels
-func newScrapePool(cfg *config.ScrapeConfig, app storage.Appendable, offsetSeed uint64, logger *slog.Logger, buffers *pool.Pool, options *Options, metrics *scrapeMetrics) (*scrapePool, error) {
+// scrapeLoopAppendAdapter allows support for multiple storage.Appender versions.
+type scrapeLoopAppendAdapter interface {
+ Commit() error
+ Rollback() error
+
+ addReportSample(s reportSample, t int64, v float64, b *labels.Builder, rejectOOO bool) error
+ append(b []byte, contentType string, ts time.Time) (total, added, seriesAdded int, err error)
+}
+
+func newScrapePool(
+ cfg *config.ScrapeConfig,
+ appendable storage.Appendable,
+ appendableV2 storage.AppendableV2,
+ offsetSeed uint64,
+ logger *slog.Logger,
+ buffers *pool.Pool,
+ options *Options,
+ metrics *scrapeMetrics,
+) (*scrapePool, error) {
if logger == nil {
logger = promslog.NewNopLogger()
}
+ if buffers == nil {
+ buffers = pool.New(1e3, 1e6, 3, func(sz int) any { return make([]byte, 0, sz) })
+ }
client, err := newScrapeClient(cfg.HTTPClientConfig, cfg.JobName, options.HTTPClientOptions...)
if err != nil {
return nil, err
}
+ // Validate scheme so we don't need to do it later.
+ // We also do it on scrapePool.reload(...)
+ // TODO(bwplotka): Can we move it to scrape config validation?
if err := namevalidationutil.CheckNameValidationScheme(cfg.MetricNameValidationScheme); err != nil {
return nil, errors.New("newScrapePool: MetricNameValidationScheme must be set in scrape configuration")
}
- var escapingScheme model.EscapingScheme
- escapingScheme, err = config.ToEscapingScheme(cfg.MetricNameEscapingScheme, cfg.MetricNameValidationScheme)
- if err != nil {
+ if _, err = config.ToEscapingScheme(cfg.MetricNameEscapingScheme, cfg.MetricNameValidationScheme); err != nil {
return nil, fmt.Errorf("invalid metric name escaping scheme, %w", err)
}
+ symbols := labels.NewSymbolTable()
ctx, cancel := context.WithCancel(context.Background())
sp := &scrapePool{
+ appendable: appendable,
+ appendableV2: appendableV2,
+ logger: logger,
+ ctx: ctx,
cancel: cancel,
- appendable: app,
+ options: options,
config: cfg,
client: client,
- activeTargets: map[uint64]*Target{},
loops: map[uint64]loop{},
- symbolTable: labels.NewSymbolTable(),
+ symbolTable: symbols,
lastSymbolTableCheck: time.Now(),
- logger: logger,
+ activeTargets: map[uint64]*Target{},
metrics: metrics,
- httpOpts: options.HTTPClientOptions,
- validationScheme: cfg.MetricNameValidationScheme,
- escapingScheme: escapingScheme,
- }
- sp.newLoop = func(opts scrapeLoopOptions) loop {
- // Update the targets retrieval function for metadata to a new scrape cache.
- cache := opts.cache
- if cache == nil {
- cache = newScrapeCache(metrics)
- }
- opts.target.SetMetadataStore(cache)
-
- return newScrapeLoop(
- ctx,
- opts.scraper,
- logger.With("target", opts.target),
- buffers,
- func(l labels.Labels) labels.Labels {
- return mutateSampleLabels(l, opts.target, opts.honorLabels, opts.mrc)
- },
- func(l labels.Labels) labels.Labels { return mutateReportSampleLabels(l, opts.target) },
- func(ctx context.Context) storage.Appender { return app.Appender(ctx) },
- cache,
- sp.symbolTable,
- offsetSeed,
- opts.honorTimestamps,
- opts.trackTimestampsStaleness,
- opts.enableCompression,
- opts.sampleLimit,
- opts.bucketLimit,
- opts.maxSchema,
- opts.labelLimits,
- opts.interval,
- opts.timeout,
- opts.alwaysScrapeClassicHist,
- opts.convertClassicHistToNHCB,
- cfg.ScrapeNativeHistogramsEnabled(),
- options.EnableCreatedTimestampZeroIngestion,
- options.EnableTypeAndUnitLabels,
- options.ExtraMetrics,
- options.AppendMetadata,
- opts.target,
- options.PassMetadataInContext,
- metrics,
- options.skipOffsetting,
- sp.validationScheme,
- sp.escapingScheme,
- opts.fallbackScrapeProtocol,
- )
+ buffers: buffers,
+ offsetSeed: offsetSeed,
}
sp.metrics.targetScrapePoolTargetLimit.WithLabelValues(sp.config.JobName).Set(float64(sp.config.TargetLimit))
return sp, nil
}
+func (sp *scrapePool) newLoop(opts scrapeLoopOptions) loop {
+ if sp.injectTestNewLoop != nil {
+ return sp.injectTestNewLoop(opts)
+ }
+ return newScrapeLoop(opts)
+}
+
func (sp *scrapePool) ActiveTargets() []*Target {
sp.targetMtx.Lock()
defer sp.targetMtx.Unlock()
@@ -309,6 +276,7 @@ func (sp *scrapePool) stop() {
sp.metrics.targetScrapePoolTargetsAdded.DeleteLabelValues(sp.config.JobName)
sp.metrics.targetScrapePoolSymbolTableItems.DeleteLabelValues(sp.config.JobName)
sp.metrics.targetSyncIntervalLength.DeleteLabelValues(sp.config.JobName)
+ sp.metrics.targetSyncIntervalLengthHistogram.DeleteLabelValues(sp.config.JobName)
sp.metrics.targetSyncFailed.DeleteLabelValues(sp.config.JobName)
}
}
@@ -322,7 +290,7 @@ func (sp *scrapePool) reload(cfg *config.ScrapeConfig) error {
sp.metrics.targetScrapePoolReloads.Inc()
start := time.Now()
- client, err := newScrapeClient(cfg.HTTPClientConfig, cfg.JobName, sp.httpOpts...)
+ client, err := newScrapeClient(cfg.HTTPClientConfig, cfg.JobName, sp.options.HTTPClientOptions...)
if err != nil {
sp.metrics.targetScrapePoolReloadsFailed.Inc()
return err
@@ -332,17 +300,14 @@ func (sp *scrapePool) reload(cfg *config.ScrapeConfig) error {
sp.config = cfg
oldClient := sp.client
sp.client = client
+
+ // Validate scheme so we don't need to do it later.
if err := namevalidationutil.CheckNameValidationScheme(cfg.MetricNameValidationScheme); err != nil {
return errors.New("scrapePool.reload: MetricNameValidationScheme must be set in scrape configuration")
}
- sp.validationScheme = cfg.MetricNameValidationScheme
- var escapingScheme model.EscapingScheme
- escapingScheme, err = model.ToEscapingScheme(cfg.MetricNameEscapingScheme)
- if err != nil {
- return fmt.Errorf("invalid metric name escaping scheme, %w", err)
+ if _, err = config.ToEscapingScheme(cfg.MetricNameEscapingScheme, cfg.MetricNameValidationScheme); err != nil {
+ return fmt.Errorf("scrapePool.reload: invalid metric name escaping scheme, %w", err)
}
- sp.escapingScheme = escapingScheme
-
sp.metrics.targetScrapePoolTargetLimit.WithLabelValues(sp.config.JobName).Set(float64(sp.config.TargetLimit))
sp.restartLoops(reuseCache)
@@ -354,30 +319,7 @@ func (sp *scrapePool) reload(cfg *config.ScrapeConfig) error {
}
func (sp *scrapePool) restartLoops(reuseCache bool) {
- var (
- wg sync.WaitGroup
- interval = time.Duration(sp.config.ScrapeInterval)
- timeout = time.Duration(sp.config.ScrapeTimeout)
- bodySizeLimit = int64(sp.config.BodySizeLimit)
- sampleLimit = int(sp.config.SampleLimit)
- bucketLimit = int(sp.config.NativeHistogramBucketLimit)
- maxSchema = pickSchema(sp.config.NativeHistogramMinBucketFactor)
- labelLimits = &labelLimits{
- labelLimit: int(sp.config.LabelLimit),
- labelNameLengthLimit: int(sp.config.LabelNameLengthLimit),
- labelValueLengthLimit: int(sp.config.LabelValueLengthLimit),
- }
- honorLabels = sp.config.HonorLabels
- honorTimestamps = sp.config.HonorTimestamps
- enableCompression = sp.config.EnableCompression
- trackTimestampsStaleness = sp.config.TrackTimestampsStaleness
- mrc = sp.config.MetricRelabelConfigs
- fallbackScrapeProtocol = sp.config.ScrapeFallbackProtocol.HeaderMediaType()
- scrapeNativeHist = sp.config.ScrapeNativeHistogramsEnabled()
- alwaysScrapeClassicHist = sp.config.AlwaysScrapeClassicHistogramsEnabled()
- convertClassicHistToNHCB = sp.config.ConvertClassicHistogramsToNHCBEnabled()
- )
-
+ var wg sync.WaitGroup
sp.targetMtx.Lock()
forcedErr := sp.refreshTargetLimitErr()
@@ -391,38 +333,27 @@ func (sp *scrapePool) restartLoops(reuseCache bool) {
}
t := sp.activeTargets[fp]
- targetInterval, targetTimeout, err := t.intervalAndTimeout(interval, timeout)
- var (
- s = &targetScraper{
+ targetInterval, targetTimeout, err := t.intervalAndTimeout(
+ time.Duration(sp.config.ScrapeInterval),
+ time.Duration(sp.config.ScrapeTimeout),
+ )
+ escapingScheme, _ := config.ToEscapingScheme(sp.config.MetricNameEscapingScheme, sp.config.MetricNameValidationScheme)
+ newLoop := sp.newLoop(scrapeLoopOptions{
+ target: t,
+ scraper: &targetScraper{
Target: t,
client: sp.client,
timeout: targetTimeout,
- bodySizeLimit: bodySizeLimit,
- acceptHeader: acceptHeader(sp.config.ScrapeProtocols, sp.escapingScheme),
- acceptEncodingHeader: acceptEncodingHeader(enableCompression),
+ bodySizeLimit: int64(sp.config.BodySizeLimit),
+ acceptHeader: acceptHeader(sp.config.ScrapeProtocols, escapingScheme),
+ acceptEncodingHeader: acceptEncodingHeader(sp.config.EnableCompression),
metrics: sp.metrics,
- }
- newLoop = sp.newLoop(scrapeLoopOptions{
- target: t,
- scraper: s,
- sampleLimit: sampleLimit,
- bucketLimit: bucketLimit,
- maxSchema: maxSchema,
- labelLimits: labelLimits,
- honorLabels: honorLabels,
- honorTimestamps: honorTimestamps,
- enableCompression: enableCompression,
- trackTimestampsStaleness: trackTimestampsStaleness,
- mrc: mrc,
- cache: cache,
- interval: targetInterval,
- timeout: targetTimeout,
- fallbackScrapeProtocol: fallbackScrapeProtocol,
- scrapeNativeHist: scrapeNativeHist,
- alwaysScrapeClassicHist: alwaysScrapeClassicHist,
- convertClassicHistToNHCB: convertClassicHistToNHCB,
- })
- )
+ },
+ cache: cache,
+ interval: targetInterval,
+ timeout: targetTimeout,
+ sp: sp,
+ })
if err != nil {
newLoop.setForcedError(err)
}
@@ -505,6 +436,9 @@ func (sp *scrapePool) Sync(tgs []*targetgroup.Group) {
sp.metrics.targetSyncIntervalLength.WithLabelValues(sp.config.JobName).Observe(
time.Since(start).Seconds(),
)
+ sp.metrics.targetSyncIntervalLengthHistogram.WithLabelValues(sp.config.JobName).Observe(
+ time.Since(start).Seconds(),
+ )
sp.metrics.targetScrapePoolSyncsCounter.WithLabelValues(sp.config.JobName).Inc()
}
@@ -512,31 +446,10 @@ func (sp *scrapePool) Sync(tgs []*targetgroup.Group) {
// scrape loops for new targets, and stops scrape loops for disappeared targets.
// It returns after all stopped scrape loops terminated.
func (sp *scrapePool) sync(targets []*Target) {
- var (
- uniqueLoops = make(map[uint64]loop)
- interval = time.Duration(sp.config.ScrapeInterval)
- timeout = time.Duration(sp.config.ScrapeTimeout)
- bodySizeLimit = int64(sp.config.BodySizeLimit)
- sampleLimit = int(sp.config.SampleLimit)
- bucketLimit = int(sp.config.NativeHistogramBucketLimit)
- maxSchema = pickSchema(sp.config.NativeHistogramMinBucketFactor)
- labelLimits = &labelLimits{
- labelLimit: int(sp.config.LabelLimit),
- labelNameLengthLimit: int(sp.config.LabelNameLengthLimit),
- labelValueLengthLimit: int(sp.config.LabelValueLengthLimit),
- }
- honorLabels = sp.config.HonorLabels
- honorTimestamps = sp.config.HonorTimestamps
- enableCompression = sp.config.EnableCompression
- trackTimestampsStaleness = sp.config.TrackTimestampsStaleness
- mrc = sp.config.MetricRelabelConfigs
- fallbackScrapeProtocol = sp.config.ScrapeFallbackProtocol.HeaderMediaType()
- scrapeNativeHist = sp.config.ScrapeNativeHistogramsEnabled()
- alwaysScrapeClassicHist = sp.config.AlwaysScrapeClassicHistogramsEnabled()
- convertClassicHistToNHCB = sp.config.ConvertClassicHistogramsToNHCBEnabled()
- )
+ uniqueLoops := make(map[uint64]loop)
sp.targetMtx.Lock()
+ escapingScheme, _ := config.ToEscapingScheme(sp.config.MetricNameEscapingScheme, sp.config.MetricNameValidationScheme)
for _, t := range targets {
hash := t.hash()
@@ -545,34 +458,25 @@ func (sp *scrapePool) sync(targets []*Target) {
// so whether changed via relabeling or not, they'll exist and hold the correct values
// for every target.
var err error
- interval, timeout, err = t.intervalAndTimeout(interval, timeout)
- s := &targetScraper{
- Target: t,
- client: sp.client,
- timeout: timeout,
- bodySizeLimit: bodySizeLimit,
- acceptHeader: acceptHeader(sp.config.ScrapeProtocols, sp.escapingScheme),
- acceptEncodingHeader: acceptEncodingHeader(enableCompression),
- metrics: sp.metrics,
- }
+ targetInterval, targetTimeout, err := t.intervalAndTimeout(
+ time.Duration(sp.config.ScrapeInterval),
+ time.Duration(sp.config.ScrapeTimeout),
+ )
l := sp.newLoop(scrapeLoopOptions{
- target: t,
- scraper: s,
- sampleLimit: sampleLimit,
- bucketLimit: bucketLimit,
- maxSchema: maxSchema,
- labelLimits: labelLimits,
- honorLabels: honorLabels,
- honorTimestamps: honorTimestamps,
- enableCompression: enableCompression,
- trackTimestampsStaleness: trackTimestampsStaleness,
- mrc: mrc,
- interval: interval,
- timeout: timeout,
- scrapeNativeHist: scrapeNativeHist,
- alwaysScrapeClassicHist: alwaysScrapeClassicHist,
- convertClassicHistToNHCB: convertClassicHistToNHCB,
- fallbackScrapeProtocol: fallbackScrapeProtocol,
+ target: t,
+ scraper: &targetScraper{
+ Target: t,
+ client: sp.client,
+ timeout: targetTimeout,
+ bodySizeLimit: int64(sp.config.BodySizeLimit),
+ acceptHeader: acceptHeader(sp.config.ScrapeProtocols, escapingScheme),
+ acceptEncodingHeader: acceptEncodingHeader(sp.config.EnableCompression),
+ metrics: sp.metrics,
+ },
+ cache: newScrapeCache(sp.metrics),
+ interval: targetInterval,
+ timeout: targetTimeout,
+ sp: sp,
})
if err != nil {
l.setForcedError(err)
@@ -657,7 +561,7 @@ func verifyLabelLimits(lset labels.Labels, limits *labelLimits) error {
return nil
}
- met := lset.Get(labels.MetricName)
+ met := lset.Get(model.MetricNameLabel)
if limits.labelLimit > 0 {
nbLabels := lset.Len()
if nbLabels > limits.labelLimit {
@@ -712,13 +616,11 @@ func mutateSampleLabels(lset labels.Labels, target *Target, honor bool, rc []*re
}
}
- res := lb.Labels()
-
- if len(rc) > 0 {
- res, _ = relabel.Process(res, rc...)
+ if keep := relabel.ProcessBuilder(lb, rc...); !keep {
+ return labels.EmptyLabels()
}
- return res
+ return lb.Labels()
}
func resolveConflictingExposedLabels(lb *labels.Builder, conflictingExposedLabels []labels.Label) {
@@ -749,8 +651,8 @@ func mutateReportSampleLabels(lset labels.Labels, target *Target) labels.Labels
return lb.Labels()
}
-// appender returns an appender for ingested samples from the target.
-func appender(app storage.Appender, sampleLimit, bucketLimit int, maxSchema int32) storage.Appender {
+// appenderWithLimits returns an appender with additional validation.
+func appenderWithLimits(app storage.Appender, sampleLimit, bucketLimit int, maxSchema int32) storage.Appender {
app = &timeLimitAppender{
Appender: app,
maxTime: timestamp.FromTime(time.Now().Add(maxAheadTime)),
@@ -927,55 +829,64 @@ type cacheEntry struct {
}
type scrapeLoop struct {
- scraper scraper
- l *slog.Logger
- scrapeFailureLogger FailureLogger
- scrapeFailureLoggerMtx sync.RWMutex
- cache *scrapeCache
- lastScrapeSize int
- buffers *pool.Pool
- offsetSeed uint64
- honorTimestamps bool
- trackTimestampsStaleness bool
- enableCompression bool
- forcedErr error
- forcedErrMtx sync.Mutex
- sampleLimit int
- bucketLimit int
- maxSchema int32
- labelLimits *labelLimits
- interval time.Duration
- timeout time.Duration
- validationScheme model.ValidationScheme
- escapingScheme model.EscapingScheme
-
- alwaysScrapeClassicHist bool
- convertClassicHistToNHCB bool
- enableCTZeroIngestion bool
- enableTypeAndUnitLabels bool
- fallbackScrapeProtocol string
-
- enableNativeHistogramScraping bool
-
- appender func(ctx context.Context) storage.Appender
- symbolTable *labels.SymbolTable
- sampleMutator labelsMutator
- reportSampleMutator labelsMutator
-
- parentCtx context.Context
- appenderCtx context.Context
+ // Parameters.
ctx context.Context
cancel func()
stopped chan struct{}
+ parentCtx context.Context
+ appenderCtx context.Context
+ l *slog.Logger
+ cache *scrapeCache
+ interval time.Duration
+ timeout time.Duration
+ sampleMutator labelsMutator
+ reportSampleMutator labelsMutator
+ scraper scraper
+
+ // Static params per scrapePool.
+ appendable storage.Appendable
+ appendableV2 storage.AppendableV2
+ buffers *pool.Pool
+ offsetSeed uint64
+ symbolTable *labels.SymbolTable
+ metrics *scrapeMetrics
+
+ // Options from config.ScrapeConfig.
+ sampleLimit int
+ bucketLimit int
+ maxSchema int32
+ labelLimits *labelLimits
+ honorLabels bool
+ honorTimestamps bool
+ trackTimestampsStaleness bool
+ enableNativeHistogramScraping bool
+ alwaysScrapeClassicHist bool
+ convertClassicHistToNHCB bool
+ fallbackScrapeProtocol string
+ enableCompression bool
+ mrc []*relabel.Config
+ validationScheme model.ValidationScheme
+
+ // Options from scrape.Options.
+ enableSTZeroIngestion bool
+ enableTypeAndUnitLabels bool
+ reportExtraMetrics bool
+ appendMetadataToWAL bool
+ passMetadataInContext bool
+ skipOffsetting bool // For testability.
+
+ // error injection through setForcedError.
+ forcedErr error
+ forcedErrMtx sync.Mutex
+
+ // Special logger set on setScrapeFailureLogger
+ scrapeFailureLoggerMtx sync.RWMutex
+ scrapeFailureLogger FailureLogger
+
+ // Locally cached data.
+ lastScrapeSize int
disabledEndOfRunStalenessMarkers atomic.Bool
-
- reportExtraMetrics bool
- appendMetadataToWAL bool
-
- metrics *scrapeMetrics
-
- skipOffsetting bool // For testability.
}
// scrapeCache tracks mappings of exposed metric strings to label sets and
@@ -996,14 +907,12 @@ type scrapeCache struct {
// be a pointer so we can update it.
droppedSeries map[string]*uint64
- // seriesCur and seriesPrev store the labels of series that were seen
- // in the current and previous scrape.
- // We hold two maps and swap them out to save allocations.
- seriesCur map[uint64]*cacheEntry
- seriesPrev map[uint64]*cacheEntry
+ // Series that were seen in the current and previous scrape, for staleness detection.
+ seriesCur map[storage.SeriesRef]*cacheEntry
+ seriesPrev map[storage.SeriesRef]*cacheEntry
- // TODO(bwplotka): Consider moving Metadata API to use WAL instead of scrape loop to
- // avoid locking (using metadata API can block scraping).
+ // TODO(bwplotka): Consider moving metadata caching to head. See
+ // https://github.com/prometheus/prometheus/issues/17619.
metaMtx sync.Mutex // Mutex is needed due to api touching it when metadata is queried.
metadata map[string]*metaEntry // metadata by metric family name.
@@ -1027,8 +936,8 @@ func newScrapeCache(metrics *scrapeMetrics) *scrapeCache {
return &scrapeCache{
series: map[string]*cacheEntry{},
droppedSeries: map[string]*uint64{},
- seriesCur: map[uint64]*cacheEntry{},
- seriesPrev: map[uint64]*cacheEntry{},
+ seriesCur: map[storage.SeriesRef]*cacheEntry{},
+ seriesPrev: map[storage.SeriesRef]*cacheEntry{},
metadata: map[string]*metaEntry{},
metrics: metrics,
}
@@ -1076,13 +985,9 @@ func (c *scrapeCache) iterDone(flushCache bool) {
c.metaMtx.Unlock()
}
- // Swap current and previous series.
+ // Swap current and previous series then clear the new current, to save allocations.
c.seriesPrev, c.seriesCur = c.seriesCur, c.seriesPrev
-
- // We have to delete every single key in the map.
- for k := range c.seriesCur {
- delete(c.seriesCur, k)
- }
+ clear(c.seriesCur)
c.iter++
}
@@ -1119,13 +1024,13 @@ func (c *scrapeCache) getDropped(met []byte) bool {
return ok
}
-func (c *scrapeCache) trackStaleness(hash uint64, ce *cacheEntry) {
- c.seriesCur[hash] = ce
+func (c *scrapeCache) trackStaleness(ref storage.SeriesRef, ce *cacheEntry) {
+ c.seriesCur[ref] = ce
}
func (c *scrapeCache) forEachStale(f func(storage.SeriesRef, labels.Labels) bool) {
- for h, ce := range c.seriesPrev {
- if _, ok := c.seriesCur[h]; !ok {
+ for ref, ce := range c.seriesPrev {
+ if _, ok := c.seriesCur[ref]; !ok {
if !f(ce.ref, ce.lset) {
break
}
@@ -1242,99 +1147,88 @@ func (c *scrapeCache) LengthMetadata() int {
return len(c.metadata)
}
-func newScrapeLoop(ctx context.Context,
- sc scraper,
- l *slog.Logger,
- buffers *pool.Pool,
- sampleMutator labelsMutator,
- reportSampleMutator labelsMutator,
- appender func(ctx context.Context) storage.Appender,
- cache *scrapeCache,
- symbolTable *labels.SymbolTable,
- offsetSeed uint64,
- honorTimestamps bool,
- trackTimestampsStaleness bool,
- enableCompression bool,
- sampleLimit int,
- bucketLimit int,
- maxSchema int32,
- labelLimits *labelLimits,
- interval time.Duration,
- timeout time.Duration,
- alwaysScrapeClassicHist bool,
- convertClassicHistToNHCB bool,
- enableNativeHistogramScraping bool,
- enableCTZeroIngestion bool,
- enableTypeAndUnitLabels bool,
- reportExtraMetrics bool,
- appendMetadataToWAL bool,
- target *Target,
- passMetadataInContext bool,
- metrics *scrapeMetrics,
- skipOffsetting bool,
- validationScheme model.ValidationScheme,
- escapingScheme model.EscapingScheme,
- fallbackScrapeProtocol string,
-) *scrapeLoop {
- if l == nil {
- l = promslog.NewNopLogger()
- }
- if buffers == nil {
- buffers = pool.New(1e3, 1e6, 3, func(sz int) any { return make([]byte, 0, sz) })
- }
- if cache == nil {
- cache = newScrapeCache(metrics)
- }
+// scrapeLoopOptions contains static options that do not change per scrapePool lifecycle.
+type scrapeLoopOptions struct {
+ target *Target
+ scraper scraper
+ cache *scrapeCache
+ interval, timeout time.Duration
- appenderCtx := ctx
+ sp *scrapePool
+}
- if passMetadataInContext {
+// newScrapeLoop constructs new scrapeLoop.
+// NOTE: Technically this could be a scrapePool method, but it's a standalone function to make it clear scrapeLoop
+// can be used outside scrapePool lifecycle (e.g. in tests).
+func newScrapeLoop(opts scrapeLoopOptions) *scrapeLoop {
+ // Update the targets retrieval function for metadata to a new target.
+ opts.target.SetMetadataStore(opts.cache)
+
+ appenderCtx := opts.sp.ctx
+ if opts.sp.options.PassMetadataInContext {
// Store the cache and target in the context. This is then used by downstream OTel Collector
// to lookup the metadata required to process the samples. Not used by Prometheus itself.
// TODO(gouthamve) We're using a dedicated context because using the parentCtx caused a memory
// leak. We should ideally fix the main leak. See: https://github.com/prometheus/prometheus/pull/10590
- appenderCtx = ContextWithMetricMetadataStore(appenderCtx, cache)
- appenderCtx = ContextWithTarget(appenderCtx, target)
+ // TODO(bwplotka): Remove once OpenTelemetry collector uses AppenderV2 (add issue)
+ appenderCtx = ContextWithMetricMetadataStore(appenderCtx, opts.cache)
+ appenderCtx = ContextWithTarget(appenderCtx, opts.target)
}
- sl := &scrapeLoop{
- scraper: sc,
- buffers: buffers,
- cache: cache,
- appender: appender,
- symbolTable: symbolTable,
- sampleMutator: sampleMutator,
- reportSampleMutator: reportSampleMutator,
- stopped: make(chan struct{}),
- offsetSeed: offsetSeed,
- l: l,
- parentCtx: ctx,
- appenderCtx: appenderCtx,
- honorTimestamps: honorTimestamps,
- trackTimestampsStaleness: trackTimestampsStaleness,
- enableCompression: enableCompression,
- sampleLimit: sampleLimit,
- bucketLimit: bucketLimit,
- maxSchema: maxSchema,
- labelLimits: labelLimits,
- interval: interval,
- timeout: timeout,
- alwaysScrapeClassicHist: alwaysScrapeClassicHist,
- convertClassicHistToNHCB: convertClassicHistToNHCB,
- enableCTZeroIngestion: enableCTZeroIngestion,
- enableTypeAndUnitLabels: enableTypeAndUnitLabels,
- fallbackScrapeProtocol: fallbackScrapeProtocol,
- enableNativeHistogramScraping: enableNativeHistogramScraping,
- reportExtraMetrics: reportExtraMetrics,
- appendMetadataToWAL: appendMetadataToWAL,
- metrics: metrics,
- skipOffsetting: skipOffsetting,
- validationScheme: validationScheme,
- escapingScheme: escapingScheme,
- }
- sl.ctx, sl.cancel = context.WithCancel(ctx)
+ ctx, cancel := context.WithCancel(opts.sp.ctx)
+ return &scrapeLoop{
+ ctx: ctx,
+ cancel: cancel,
+ stopped: make(chan struct{}),
+ parentCtx: opts.sp.ctx,
+ appenderCtx: appenderCtx,
+ l: opts.sp.logger.With("target", opts.target),
+ cache: opts.cache,
- return sl
+ interval: opts.interval,
+ timeout: opts.timeout,
+ sampleMutator: func(l labels.Labels) labels.Labels {
+ return mutateSampleLabels(l, opts.target, opts.sp.config.HonorLabels, opts.sp.config.MetricRelabelConfigs)
+ },
+ reportSampleMutator: func(l labels.Labels) labels.Labels { return mutateReportSampleLabels(l, opts.target) },
+ scraper: opts.scraper,
+
+ // Static params per scrapePool.
+ 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),
+ bucketLimit: int(opts.sp.config.NativeHistogramBucketLimit),
+ maxSchema: pickSchema(opts.sp.config.NativeHistogramMinBucketFactor),
+ labelLimits: &labelLimits{
+ labelLimit: int(opts.sp.config.LabelLimit),
+ labelNameLengthLimit: int(opts.sp.config.LabelNameLengthLimit),
+ labelValueLengthLimit: int(opts.sp.config.LabelValueLengthLimit),
+ },
+ honorLabels: opts.sp.config.HonorLabels,
+ honorTimestamps: opts.sp.config.HonorTimestamps,
+ trackTimestampsStaleness: opts.sp.config.TrackTimestampsStaleness,
+ enableNativeHistogramScraping: opts.sp.config.ScrapeNativeHistogramsEnabled(),
+ alwaysScrapeClassicHist: opts.sp.config.AlwaysScrapeClassicHistogramsEnabled(),
+ convertClassicHistToNHCB: opts.sp.config.ConvertClassicHistogramsToNHCBEnabled(),
+ fallbackScrapeProtocol: opts.sp.config.ScrapeFallbackProtocol.HeaderMediaType(),
+ enableCompression: opts.sp.config.EnableCompression,
+ mrc: opts.sp.config.MetricRelabelConfigs,
+ reportExtraMetrics: opts.sp.config.ExtraScrapeMetricsEnabled(),
+ validationScheme: opts.sp.config.MetricNameValidationScheme,
+
+ // scrape.Options.
+ enableSTZeroIngestion: opts.sp.options.EnableStartTimestampZeroIngestion,
+ enableTypeAndUnitLabels: opts.sp.options.EnableTypeAndUnitLabels,
+ appendMetadataToWAL: opts.sp.options.AppendMetadata,
+ passMetadataInContext: opts.sp.options.PassMetadataInContext,
+ skipOffsetting: opts.sp.options.skipOffsetting,
+ }
}
func (sl *scrapeLoop) setScrapeFailureLogger(l FailureLogger) {
@@ -1413,6 +1307,13 @@ mainLoop:
}
}
+func (sl *scrapeLoop) appender() scrapeLoopAppendAdapter {
+ if sl.appendableV2 != nil {
+ return &scrapeLoopAppenderV2{scrapeLoop: sl, AppenderV2: sl.appendableV2.AppenderV2(sl.appenderCtx)}
+ }
+ return &scrapeLoopAppender{scrapeLoop: sl, Appender: sl.appendable.Appender(sl.appenderCtx)}
+}
+
// scrapeAndReport performs a scrape and then appends the result to the storage
// together with reporting metrics, by using as few appenders as possible.
// In the happy scenario, a single appender is used.
@@ -1426,18 +1327,26 @@ func (sl *scrapeLoop) scrapeAndReport(last, appendTime time.Time, errc chan<- er
sl.metrics.targetIntervalLength.WithLabelValues(sl.interval.String()).Observe(
time.Since(last).Seconds(),
)
+ sl.metrics.targetIntervalLengthHistogram.WithLabelValues(sl.interval.String()).Observe(
+ time.Since(last).Seconds(),
+ )
}
var total, added, seriesAdded, bytesRead int
var err, appErr, scrapeErr error
- app := sl.appender(sl.appenderCtx)
+ app := sl.appender()
defer func() {
if err != nil {
- app.Rollback()
+ _ = app.Rollback()
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)
}
@@ -1452,13 +1361,16 @@ func (sl *scrapeLoop) scrapeAndReport(last, appendTime time.Time, errc chan<- er
if forcedErr := sl.getForcedError(); forcedErr != nil {
scrapeErr = forcedErr
// Add stale markers.
- if _, _, _, err := sl.append(app, []byte{}, "", appendTime); err != nil {
- app.Rollback()
- app = sl.appender(sl.appenderCtx)
+ if _, _, _, err := app.append([]byte{}, "", appendTime); err != nil {
+ _ = app.Rollback()
+ app = sl.appender()
sl.l.Warn("Append failed", "err", err)
}
if errc != nil {
- errc <- forcedErr
+ select {
+ case errc <- forcedErr:
+ case <-sl.ctx.Done():
+ }
}
return start
@@ -1495,7 +1407,10 @@ func (sl *scrapeLoop) scrapeAndReport(last, appendTime time.Time, errc chan<- er
}
sl.scrapeFailureLoggerMtx.RUnlock()
if errc != nil {
- errc <- scrapeErr
+ select {
+ case errc <- scrapeErr:
+ case <-sl.ctx.Done():
+ }
}
if errors.Is(scrapeErr, errBodySizeLimit) {
bytesRead = -1
@@ -1504,16 +1419,16 @@ func (sl *scrapeLoop) scrapeAndReport(last, appendTime time.Time, errc chan<- er
// A failed scrape is the same as an empty scrape,
// we still call sl.append to trigger stale markers.
- total, added, seriesAdded, appErr = sl.append(app, b, contentType, appendTime)
+ total, added, seriesAdded, appErr = app.append(b, contentType, appendTime)
if appErr != nil {
- app.Rollback()
- app = sl.appender(sl.appenderCtx)
+ _ = app.Rollback()
+ app = sl.appender()
sl.l.Debug("Append failed", "err", appErr)
// The append failed, probably due to a parse error or sample limit.
// Call sl.append again with an empty scrape to trigger stale markers.
- if _, _, _, err := sl.append(app, []byte{}, "", appendTime); err != nil {
- app.Rollback()
- app = sl.appender(sl.appenderCtx)
+ if _, _, _, err := app.append([]byte{}, "", appendTime); err != nil {
+ _ = app.Rollback()
+ app = sl.appender()
sl.l.Warn("Append failed", "err", err)
}
}
@@ -1583,11 +1498,11 @@ func (sl *scrapeLoop) endOfRunStaleness(last time.Time, ticker *time.Ticker, int
// If the target has since been recreated and scraped, the
// stale markers will be out of order and ignored.
// sl.context would have been cancelled, hence using sl.appenderCtx.
- app := sl.appender(sl.appenderCtx)
+ app := sl.appender()
var err error
defer func() {
if err != nil {
- app.Rollback()
+ _ = app.Rollback()
return
}
err = app.Commit()
@@ -1595,9 +1510,9 @@ func (sl *scrapeLoop) endOfRunStaleness(last time.Time, ticker *time.Ticker, int
sl.l.Warn("Stale commit failed", "err", err)
}
}()
- if _, _, _, err = sl.append(app, []byte{}, "", staleTime); err != nil {
- app.Rollback()
- app = sl.appender(sl.appenderCtx)
+ if _, _, _, err = app.append([]byte{}, "", staleTime); err != nil {
+ _ = app.Rollback()
+ app = sl.appender()
sl.l.Warn("Stale append failed", "err", err)
}
if err = sl.reportStale(app, staleTime); err != nil {
@@ -1631,7 +1546,7 @@ type appendErrors struct {
func (sl *scrapeLoop) updateStaleMarkers(app storage.Appender, defTime int64) (err error) {
sl.cache.forEachStale(func(ref storage.SeriesRef, lset labels.Labels) bool {
// Series no longer exposed, mark it stale.
- app.SetOptions(&storage.AppendOptions{DiscardOutOfOrder: true})
+ app.SetOptions(&aOptionRejectEarlyOOO)
_, err = app.Append(ref, lset, defTime, math.Float64frombits(value.StaleNaN))
app.SetOptions(nil)
switch {
@@ -1645,12 +1560,20 @@ func (sl *scrapeLoop) updateStaleMarkers(app storage.Appender, defTime int64) (e
return err
}
-func (sl *scrapeLoop) append(app storage.Appender, b []byte, contentType string, ts time.Time) (total, added, seriesAdded int, err error) {
+type scrapeLoopAppender struct {
+ *scrapeLoop
+
+ storage.Appender
+}
+
+var _ scrapeLoopAppendAdapter = &scrapeLoopAppender{}
+
+func (sl *scrapeLoopAppender) 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.updateStaleMarkers(app, defTime)
+ err = sl.updateStaleMarkers(sl.Appender, defTime)
sl.cache.iterDone(false)
return total, added, seriesAdded, err
}
@@ -1660,7 +1583,7 @@ func (sl *scrapeLoop) append(app storage.Appender, b []byte, contentType string,
IgnoreNativeHistograms: !sl.enableNativeHistogramScraping,
ConvertClassicHistogramsToNHCB: sl.convertClassicHistToNHCB,
KeepClassicOnClassicAndNativeHistograms: sl.alwaysScrapeClassicHist,
- OpenMetricsSkipCTSeries: sl.enableCTZeroIngestion,
+ OpenMetricsSkipSTSeries: sl.enableSTZeroIngestion,
FallbackContentType: sl.fallbackScrapeProtocol,
})
if p == nil {
@@ -1693,7 +1616,7 @@ func (sl *scrapeLoop) append(app storage.Appender, b []byte, contentType string,
exemplars := make([]exemplar.Exemplar, 0, 1)
// Take an appender with limits.
- app = appender(app, sl.sampleLimit, sl.bucketLimit, sl.maxSchema)
+ app := appenderWithLimits(sl.Appender, sl.sampleLimit, sl.bucketLimit, sl.maxSchema)
defer func() {
if err != nil {
@@ -1721,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
@@ -1782,7 +1705,7 @@ loop:
continue
}
- if !lset.Has(labels.MetricName) {
+ if !lset.Has(model.MetricNameLabel) {
err = errNameLabelMandatory
break loop
}
@@ -1801,21 +1724,21 @@ loop:
if seriesAlreadyScraped && parsedTimestamp == nil {
err = storage.ErrDuplicateSampleForTimestamp
} else {
- if sl.enableCTZeroIngestion {
- if ctMs := p.CreatedTimestamp(); ctMs != 0 {
+ if sl.enableSTZeroIngestion {
+ if stMs := p.StartTimestamp(); stMs != 0 {
if isHistogram {
if h != nil {
- ref, err = app.AppendHistogramCTZeroSample(ref, lset, t, ctMs, h, nil)
+ ref, err = app.AppendHistogramSTZeroSample(ref, lset, t, stMs, h, nil)
} else {
- ref, err = app.AppendHistogramCTZeroSample(ref, lset, t, ctMs, nil, fh)
+ ref, err = app.AppendHistogramSTZeroSample(ref, lset, t, stMs, nil, fh)
}
} else {
- ref, err = app.AppendCTZeroSample(ref, lset, t, ctMs)
+ ref, err = app.AppendSTZeroSample(ref, lset, t, stMs)
}
- if err != nil && !errors.Is(err, storage.ErrOutOfOrderCT) { // OOO is a common case, ignoring completely for now.
- // CT is an experimental feature. For now, we don't need to fail the
+ if err != nil && !errors.Is(err, storage.ErrOutOfOrderST) { // OOO is a common case, ignoring completely for now.
+ // ST is an experimental feature. For now, we don't need to fail the
// scrape on errors updating the created timestamp, log debug.
- sl.l.Debug("Error when appending CT in scrape loop", "series", string(met), "ct", ctMs, "t", t, "err", err)
+ sl.l.Debug("Error when appending ST in scrape loop", "series", string(met), "ct", stMs, "t", t, "err", err)
}
}
}
@@ -1833,11 +1756,11 @@ loop:
if err == nil {
if (parsedTimestamp == nil || sl.trackTimestampsStaleness) && ce != nil {
- sl.cache.trackStaleness(ce.hash, ce)
+ sl.cache.trackStaleness(ce.ref, ce)
}
}
- 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)
@@ -1854,9 +1777,9 @@ loop:
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(hash, ce)
+ sl.cache.trackStaleness(ref, ce)
}
- if sampleAdded && sampleLimitErr == nil && bucketLimitErr == nil {
+ if sampleLimitErr == nil && bucketLimitErr == nil {
seriesAdded++
}
}
@@ -1913,8 +1836,8 @@ 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. CT and NHCB parsing).
- if isSeriesPartOfFamily(lset.Get(labels.MetricName), lastMFName, lastMeta.Type) {
+ // 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.
sl.l.Debug("Error when appending metadata in scrape loop", "ref", fmt.Sprintf("%d", ref), "metadata", fmt.Sprintf("%+v", lastMeta.Metadata), "err", merr)
@@ -1955,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.
@@ -2026,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) (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
@@ -2058,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
}
}
@@ -2138,7 +2082,7 @@ var (
}
)
-func (sl *scrapeLoop) report(app storage.Appender, start time.Time, duration time.Duration, scraped, added, seriesAdded, bytes int, scrapeErr error) (err error) {
+func (sl *scrapeLoop) report(app scrapeLoopAppendAdapter, start time.Time, duration time.Duration, scraped, added, seriesAdded, bytes int, scrapeErr error) (err error) {
sl.scraper.Report(start, duration, scrapeErr)
ts := timestamp.FromTime(start)
@@ -2149,71 +2093,70 @@ func (sl *scrapeLoop) report(app storage.Appender, start time.Time, duration tim
}
b := labels.NewBuilderWithSymbolTable(sl.symbolTable)
- if err = sl.addReportSample(app, scrapeHealthMetric, ts, health, b); err != nil {
+ if err = app.addReportSample(scrapeHealthMetric, ts, health, b, false); err != nil {
return err
}
- if err = sl.addReportSample(app, scrapeDurationMetric, ts, duration.Seconds(), b); err != nil {
+ if err = app.addReportSample(scrapeDurationMetric, ts, duration.Seconds(), b, false); err != nil {
return err
}
- if err = sl.addReportSample(app, scrapeSamplesMetric, ts, float64(scraped), b); err != nil {
+ if err = app.addReportSample(scrapeSamplesMetric, ts, float64(scraped), b, false); err != nil {
return err
}
- if err = sl.addReportSample(app, samplesPostRelabelMetric, ts, float64(added), b); err != nil {
+ if err = app.addReportSample(samplesPostRelabelMetric, ts, float64(added), b, false); err != nil {
return err
}
- if err = sl.addReportSample(app, scrapeSeriesAddedMetric, ts, float64(seriesAdded), b); err != nil {
+ if err = app.addReportSample(scrapeSeriesAddedMetric, ts, float64(seriesAdded), b, false); err != nil {
return err
}
if sl.reportExtraMetrics {
- if err = sl.addReportSample(app, scrapeTimeoutMetric, ts, sl.timeout.Seconds(), b); err != nil {
+ if err = app.addReportSample(scrapeTimeoutMetric, ts, sl.timeout.Seconds(), b, false); err != nil {
return err
}
- if err = sl.addReportSample(app, scrapeSampleLimitMetric, ts, float64(sl.sampleLimit), b); err != nil {
+ if err = app.addReportSample(scrapeSampleLimitMetric, ts, float64(sl.sampleLimit), b, false); err != nil {
return err
}
- if err = sl.addReportSample(app, scrapeBodySizeBytesMetric, ts, float64(bytes), b); err != nil {
+ if err = app.addReportSample(scrapeBodySizeBytesMetric, ts, float64(bytes), b, false); err != nil {
return err
}
}
return err
}
-func (sl *scrapeLoop) reportStale(app storage.Appender, start time.Time) (err error) {
+func (sl *scrapeLoop) reportStale(app scrapeLoopAppendAdapter, start time.Time) (err error) {
ts := timestamp.FromTime(start)
- app.SetOptions(&storage.AppendOptions{DiscardOutOfOrder: true})
stale := math.Float64frombits(value.StaleNaN)
b := labels.NewBuilder(labels.EmptyLabels())
- if err = sl.addReportSample(app, scrapeHealthMetric, ts, stale, b); err != nil {
+ if err = app.addReportSample(scrapeHealthMetric, ts, stale, b, true); err != nil {
return err
}
- if err = sl.addReportSample(app, scrapeDurationMetric, ts, stale, b); err != nil {
+ if err = app.addReportSample(scrapeDurationMetric, ts, stale, b, true); err != nil {
return err
}
- if err = sl.addReportSample(app, scrapeSamplesMetric, ts, stale, b); err != nil {
+ if err = app.addReportSample(scrapeSamplesMetric, ts, stale, b, true); err != nil {
return err
}
- if err = sl.addReportSample(app, samplesPostRelabelMetric, ts, stale, b); err != nil {
+ if err = app.addReportSample(samplesPostRelabelMetric, ts, stale, b, true); err != nil {
return err
}
- if err = sl.addReportSample(app, scrapeSeriesAddedMetric, ts, stale, b); err != nil {
+ if err = app.addReportSample(scrapeSeriesAddedMetric, ts, stale, b, true); err != nil {
return err
}
if sl.reportExtraMetrics {
- if err = sl.addReportSample(app, scrapeTimeoutMetric, ts, stale, b); err != nil {
+ if err = app.addReportSample(scrapeTimeoutMetric, ts, stale, b, true); err != nil {
return err
}
- if err = sl.addReportSample(app, scrapeSampleLimitMetric, ts, stale, b); err != nil {
+ if err = app.addReportSample(scrapeSampleLimitMetric, ts, stale, b, true); err != nil {
return err
}
- if err = sl.addReportSample(app, scrapeBodySizeBytesMetric, ts, stale, b); err != nil {
+ if err = app.addReportSample(scrapeBodySizeBytesMetric, ts, stale, b, true); err != nil {
return err
}
}
return err
}
-func (sl *scrapeLoop) addReportSample(app storage.Appender, s reportSample, t int64, v float64, b *labels.Builder) error {
+func (sl *scrapeLoopAppender) 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
@@ -2225,18 +2168,26 @@ func (sl *scrapeLoop) addReportSample(app storage.Appender, s reportSample, t in
// with scraped metrics in the cache.
// We have to drop it when building the actual metric.
b.Reset(labels.EmptyLabels())
- b.Set(labels.MetricName, string(s.name[:len(s.name)-1]))
+ b.Set(model.MetricNameLabel, string(s.name[:len(s.name)-1]))
lset = sl.reportSampleMutator(b.Labels())
}
- ref, err := app.Append(ref, lset, t, v)
+ // This will be improved in AppenderV2.
+ if rejectOOO {
+ sl.SetOptions(&aOptionRejectEarlyOOO)
+ ref, err = sl.Append(ref, lset, t, v)
+ sl.SetOptions(nil)
+ } else {
+ ref, err = sl.Append(ref, lset, t, v)
+ }
+
switch {
case err == nil:
if !ok {
sl.cache.addRef(s.name, ref, lset, lset.Hash())
// We only need to add metadata once a scrape target appears.
if sl.appendMetadataToWAL {
- if _, merr := app.UpdateMetadata(ref, lset, s.Metadata); merr != nil {
+ if _, merr := sl.UpdateMetadata(ref, lset, s.Metadata); merr != nil {
sl.l.Debug("Error when appending metadata in addReportSample", "ref", fmt.Sprintf("%d", ref), "metadata", fmt.Sprintf("%+v", s.Metadata), "err", merr)
}
}
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 c7412365d0..cab2b2918a 100644
--- a/scrape/scrape_test.go
+++ b/scrape/scrape_test.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The Prometheus Authors
+// 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
@@ -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"
@@ -51,6 +49,7 @@ import (
"go.opentelemetry.io/otel/propagation"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"go.uber.org/atomic"
+ "go.uber.org/goleak"
"github.com/prometheus/prometheus/config"
"github.com/prometheus/prometheus/discovery"
@@ -64,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"
@@ -87,50 +87,65 @@ 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 = &nopAppendable{}
+ 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)
- a, ok := sp.appendable.(*nopAppendable)
+ 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.NotNil(t, sp.newLoop, "newLoop function not initialized.")
+
+ 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)
- t.Cleanup(func() {
- _ = s.Close()
+ s := teststorage.New(t, func(opt *tsdb.Options) {
+ opt.OutOfOrderTimeWindow = 600000
})
- runScrapeLoopTest(t, s, true)
+ runScrapeLoopTest(t, appV2, s, true)
})
}
-func runScrapeLoopTest(t *testing.T, s *teststorage.TestStorage, expectOutOfOrder bool) {
- // Create an appender for adding samples to the storage.
- app := s.Appender(context.Background())
- capp := &collectResultAppender{next: app}
- sl := newBasicScrapeLoop(t, context.Background(), nil, func(context.Context) storage.Appender { return capp }, 0)
+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()
@@ -141,37 +156,35 @@ func runScrapeLoopTest(t *testing.T, s *teststorage.TestStorage, expectOutOfOrde
timestampOutOfOrder := now.Add(-5 * time.Minute)
timestampInorder2 := now.Add(5 * time.Minute)
- slApp := sl.appender(context.Background())
- _, _, _, err := sl.append(slApp, []byte(`metric_total{a="1",b="1"} 1`), "text/plain", timestampInorder1)
+ app := sl.appender()
+ _, _, _, err := app.append([]byte(`metric_total{a="1",b="1"} 1`), "text/plain", timestampInorder1)
require.NoError(t, err)
- _, _, _, err = sl.append(slApp, []byte(`metric_total{a="1",b="1"} 2`), "text/plain", timestampOutOfOrder)
+ _, _, _, err = app.append([]byte(`metric_total{a="1",b="1"} 2`), "text/plain", timestampOutOfOrder)
require.NoError(t, err)
- _, _, _, err = sl.append(slApp, []byte(`metric_total{a="1",b="1"} 3`), "text/plain", timestampInorder2)
+ _, _, _, err = app.append([]byte(`metric_total{a="1",b="1"} 3`), "text/plain", timestampInorder2)
require.NoError(t, err)
- require.NoError(t, slApp.Commit())
+ require.NoError(t, app.Commit())
// Query the samples back from the storage.
- ctx, cancel := context.WithCancel(context.Background())
- defer cancel()
q, err := s.Querier(time.Time{}.UnixNano(), time.Now().UnixNano())
require.NoError(t, err)
- defer q.Close()
+ t.Cleanup(func() { _ = q.Close() })
// Use a matcher to filter the metric name.
- series := q.Select(ctx, false, nil, labels.MustNewMatcher(labels.MatchRegexp, "__name__", "metric_total"))
+ series := q.Select(t.Context(), false, nil, labels.MustNewMatcher(labels.MatchRegexp, "__name__", "metric_total"))
- var results []floatSample
+ var results []sample
for series.Next() {
it := series.At().Iterator(nil)
for it.Next() == chunkenc.ValFloat {
t, v := it.At()
- results = append(results, floatSample{
- metric: series.At().Labels(),
- t: t,
- f: v,
+ results = append(results, sample{
+ L: series.At().Labels(),
+ T: t,
+ V: v,
})
}
require.NoError(t, it.Err())
@@ -179,28 +192,34 @@ func runScrapeLoopTest(t *testing.T, s *teststorage.TestStorage, expectOutOfOrde
require.NoError(t, series.Err())
// Define the expected results
- want := []floatSample{
+ want := []sample{
{
- metric: labels.FromStrings("__name__", "metric_total", "a", "1", "b", "1"),
- t: timestamp.FromTime(timestampInorder1),
- f: 1,
+ L: labels.FromStrings("__name__", "metric_total", "a", "1", "b", "1"),
+ T: timestamp.FromTime(timestampInorder1),
+ V: 1,
},
{
- metric: labels.FromStrings("__name__", "metric_total", "a", "1", "b", "1"),
- t: timestamp.FromTime(timestampInorder2),
- f: 3,
+ L: labels.FromStrings("__name__", "metric_total", "a", "1", "b", "1"),
+ T: timestamp.FromTime(timestampInorder2),
+ V: 3,
},
}
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 TestScrapeAppendMetadataUpdate(t *testing.T) {
+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
@@ -223,60 +242,68 @@ test_metric2{foo="bar"} 22
# EOF`
)
- // Create an appender for adding samples to the storage.
- capp := &collectResultAppender{next: nopAppender{}}
- sl := newBasicScrapeLoop(t, context.Background(), nil, func(context.Context) storage.Appender { return capp }, 0)
+ appTest := teststorage.NewAppendable()
+ sl, _ := newTestScrapeLoop(t, withAppendable(appTest, appV2))
now := time.Now()
- slApp := sl.appender(context.Background())
- _, _, _, err := sl.append(slApp, []byte(scrape1), "application/openmetrics-text", now)
+ app := sl.appender()
+ _, _, _, err := app.append([]byte(scrape1), "application/openmetrics-text", now)
require.NoError(t, err)
- require.NoError(t, slApp.Commit())
- testutil.RequireEqualWithOptions(t, []metadataEntry{
- {metric: labels.FromStrings("__name__", "test_metric_total"), m: metadata.Metadata{Type: "counter", Unit: "metric", Help: "some help text"}},
- {metric: labels.FromStrings("__name__", "test_metric2", "foo", "bar"), m: metadata.Metadata{Type: "gauge", Unit: "", Help: "other help text"}},
- }, capp.resultMetadata, []cmp.Option{cmp.Comparer(metadataEntryEqual)})
- capp.resultMetadata = nil
+ require.NoError(t, app.Commit())
+ 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 add new metadata entries.
- slApp = sl.appender(context.Background())
- _, _, _, err = sl.append(slApp, []byte(scrape1), "application/openmetrics-text", now.Add(15*time.Second))
+ app = sl.appender()
+ _, _, _, err = app.append([]byte(scrape1), "application/openmetrics-text", now.Add(15*time.Second))
require.NoError(t, err)
- require.NoError(t, slApp.Commit())
- testutil.RequireEqualWithOptions(t, []metadataEntry(nil), capp.resultMetadata, []cmp.Option{cmp.Comparer(metadataEntryEqual)})
+ require.NoError(t, app.Commit())
+ 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()
- slApp = sl.appender(context.Background())
- _, _, _, err = sl.append(slApp, []byte(scrape2), "application/openmetrics-text", now.Add(15*time.Second))
+ app = sl.appender()
+ _, _, _, err = app.append([]byte(scrape2), "application/openmetrics-text", now.Add(15*time.Second))
require.NoError(t, err)
- require.NoError(t, slApp.Commit())
- testutil.RequireEqualWithOptions(t, []metadataEntry{
- {metric: 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.
- {metric: labels.FromStrings("__name__", "test_metric2", "foo", "bar"), m: metadata.Metadata{Type: "gauge", Unit: "metric2", Help: "other help text"}},
- }, capp.resultMetadata, []cmp.Option{cmp.Comparer(metadataEntryEqual)})
+ require.NoError(t, app.Commit())
+ 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())
+ appTest.ResultReset()
}
-type nopScraper struct {
- scraper
+func TestScrapeReportMetadata(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeReportMetadata(t, appV2)
+ })
}
-func (nopScraper) Report(time.Time, time.Duration, error) {}
+func testScrapeReportMetadata(t *testing.T, appV2 bool) {
+ appTest := teststorage.NewAppendable()
+ sl, _ := newTestScrapeLoop(t, withAppendable(appTest, appV2))
+ app := sl.appender()
-func TestScrapeReportMetadataUpdate(t *testing.T) {
- // Create an appender for adding samples to the storage.
- capp := &collectResultAppender{next: nopAppender{}}
- sl := newBasicScrapeLoop(t, context.Background(), nopScraper{}, func(context.Context) storage.Appender { return capp }, 0)
now := time.Now()
- slApp := sl.appender(context.Background())
-
- require.NoError(t, sl.report(slApp, now, 2*time.Second, 1, 1, 1, 512, nil))
- require.NoError(t, slApp.Commit())
- testutil.RequireEqualWithOptions(t, []metadataEntry{
- {metric: labels.FromStrings("__name__", "up"), m: scrapeHealthMetric.Metadata},
- {metric: labels.FromStrings("__name__", "scrape_duration_seconds"), m: scrapeDurationMetric.Metadata},
- {metric: labels.FromStrings("__name__", "scrape_samples_scraped"), m: scrapeSamplesMetric.Metadata},
- {metric: labels.FromStrings("__name__", "scrape_samples_post_metric_relabeling"), m: samplesPostRelabelMetric.Metadata},
- {metric: labels.FromStrings("__name__", "scrape_series_added"), m: scrapeSeriesAddedMetric.Metadata},
- }, capp.resultMetadata, []cmp.Option{cmp.Comparer(metadataEntryEqual)})
+ require.NoError(t, sl.report(app, now, 2*time.Second, 1, 1, 1, 512, nil))
+ require.NoError(t, app.Commit())
+ 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},
+ {L: labels.FromStrings("__name__", "scrape_samples_post_metric_relabeling"), M: samplesPostRelabelMetric.Metadata},
+ {L: labels.FromStrings("__name__", "scrape_series_added"), M: scrapeSeriesAddedMetric.Metadata},
+ }, appTest.ResultMetadata())
}
func TestIsSeriesPartOfFamily(t *testing.T) {
@@ -328,8 +355,14 @@ 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 = &nopAppendable{}
+ app = teststorage.NewAppendable()
cfg = &config.ScrapeConfig{
JobName: "dropMe",
ScrapeInterval: model.Duration(1),
@@ -352,7 +385,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
)
@@ -373,9 +407,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 := &scrapePool{
- metrics: newTestScrapeMetrics(t),
- }
+ sp := newTestScrapePool(t, nil, false, nil)
// These are used when syncing so need this to avoid a panic.
sp.config = &config.ScrapeConfig{
@@ -447,13 +479,8 @@ func (*testLoop) getCache() *scrapeCache {
func TestScrapePoolStop(t *testing.T) {
t.Parallel()
- sp := &scrapePool{
- activeTargets: map[uint64]*Target{},
- loops: map[uint64]loop{},
- cancel: func() {},
- client: http.DefaultClient,
- metrics: newTestScrapeMetrics(t),
- }
+ sp := newTestScrapePool(t, nil, false, nil)
+
var mtx sync.Mutex
stopped := map[uint64]bool{}
numTargets := 20
@@ -505,26 +532,42 @@ func TestScrapePoolStop(t *testing.T) {
require.Empty(t, sp.loops, "Loops were not cleared on stopping: %d left", len(sp.loops))
}
+// TestScrapePoolReload tests reloading logic, so:
+// * all loops are reloaded, reusing cache if scrape config changed.
+// * reloaded loops are stopped before new ones are started.
+// * new scrapeLoops are configured with the updated scrape config.
func TestScrapePoolReload(t *testing.T) {
t.Parallel()
- var mtx sync.Mutex
- numTargets := 20
- stopped := map[uint64]bool{}
+ var (
+ mtx sync.Mutex
+ numTargets = 20
+ stopped = map[uint64]bool{}
+ )
- reloadCfg := &config.ScrapeConfig{
+ cfg0 := &config.ScrapeConfig{}
+ cfg1 := &config.ScrapeConfig{
ScrapeInterval: model.Duration(3 * time.Second),
ScrapeTimeout: model.Duration(2 * time.Second),
MetricNameValidationScheme: model.UTF8Validation,
MetricNameEscapingScheme: model.AllowUTF8,
+
+ // Test a few example options.
+ SampleLimit: 123,
+ ScrapeFallbackProtocol: "application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=delimited",
}
- // On starting to run, new loops created on reload check whether their preceding
- // equivalents have been stopped.
- newLoop := func(opts scrapeLoopOptions) loop {
- l := &testLoop{interval: time.Duration(reloadCfg.ScrapeInterval), timeout: time.Duration(reloadCfg.ScrapeTimeout)}
+ newLoopCfg1 := func(opts scrapeLoopOptions) loop {
+ // Test cfg1 is being used.
+ require.Equal(t, cfg1, opts.sp.config)
+
+ // Inject out testLoop that allows mocking start and stop.
+ l := &testLoop{interval: opts.interval, timeout: opts.timeout}
+
+ // On start, expect previous loop instances for the same target to be stopped.
l.startFunc = func(interval, timeout time.Duration, _ chan<- error) {
- require.Equal(t, 3*time.Second, interval, "Unexpected scrape interval")
- require.Equal(t, 2*time.Second, timeout, "Unexpected scrape timeout")
+ // Ensure cfg1 interval and timeout are correctly configured.
+ require.Equal(t, time.Duration(cfg1.ScrapeInterval), interval, "Unexpected scrape interval")
+ require.Equal(t, time.Duration(cfg1.ScrapeTimeout), timeout, "Unexpected scrape timeout")
mtx.Lock()
targetScraper := opts.scraper.(*targetScraper)
@@ -534,32 +577,21 @@ func TestScrapePoolReload(t *testing.T) {
return l
}
+ // Create test pool.
reg, metrics := newTestRegistryAndScrapeMetrics(t)
- sp := &scrapePool{
- appendable: &nopAppendable{},
- activeTargets: map[uint64]*Target{},
- loops: map[uint64]loop{},
- newLoop: newLoop,
- logger: nil,
- client: http.DefaultClient,
- metrics: metrics,
- symbolTable: labels.NewSymbolTable(),
- }
-
- // Reloading a scrape pool with a new scrape configuration must stop all scrape
- // loops and start new ones. A new loop must not be started before the preceding
- // one terminated.
+ sp := newTestScrapePool(t, nil, false, newLoopCfg1)
+ sp.metrics = metrics
+ // Prefill pool with 20 loops, simulating 20 scrape targets.
for i := range numTargets {
- labels := labels.FromStrings(model.AddressLabel, fmt.Sprintf("example.com:%d", i))
t := &Target{
- labels: labels,
- scrapeConfig: &config.ScrapeConfig{},
+ labels: labels.FromStrings(model.AddressLabel, fmt.Sprintf("example.com:%d", i)),
+ scrapeConfig: cfg0,
}
l := &testLoop{}
d := time.Duration((i+1)*20) * time.Millisecond
l.stopFunc = func() {
- time.Sleep(d)
+ time.Sleep(d) // Sleep uneven time on stop.
mtx.Lock()
stopped[t.hash()] = true
@@ -569,36 +601,26 @@ func TestScrapePoolReload(t *testing.T) {
sp.activeTargets[t.hash()] = t
sp.loops[t.hash()] = l
}
- done := make(chan struct{})
beforeTargets := map[uint64]*Target{}
maps.Copy(beforeTargets, sp.activeTargets)
- reloadTime := time.Now()
-
- go func() {
- sp.reload(reloadCfg)
- close(done)
- }()
-
- select {
- case <-time.After(5 * time.Second):
- require.FailNow(t, "scrapeLoop.reload() did not return as expected")
- case <-done:
- // This should have taken at least as long as the last target slept.
- require.GreaterOrEqual(t, time.Since(reloadTime), time.Duration(numTargets*20)*time.Millisecond, "scrapeLoop.stop() exited before all targets stopped")
- }
-
+ // Reloading a scrape pool with a new scrape configuration must stop all scrape
+ // loops and start new ones. A new loop must not be started before the preceding
+ // one terminated.
+ require.NoError(t, sp.reload(cfg1))
+ var stoppedCount int
mtx.Lock()
- require.Len(t, stopped, numTargets, "Unexpected number of stopped loops")
+ stoppedCount = len(stopped)
mtx.Unlock()
-
+ require.Equal(t, numTargets, stoppedCount, "Unexpected number of stopped loops")
require.Equal(t, sp.activeTargets, beforeTargets, "Reloading affected target states unexpectedly")
- require.Len(t, sp.loops, numTargets, "Unexpected number of stopped loops after reload")
+ require.Len(t, sp.loops, numTargets, "Unexpected number of loops after reload")
+ // Check if prometheus_target_reload_length_seconds points to cfg1.ScrapeInterval.
got, err := gatherLabels(reg, "prometheus_target_reload_length_seconds")
require.NoError(t, err)
- expectedName, expectedValue := "interval", "3s"
+ expectedName, expectedValue := "interval", cfg1.ScrapeInterval.String()
require.Equal(t, [][]*dto.LabelPair{{{Name: &expectedName, Value: &expectedValue}}}, got)
require.Equal(t, 1.0, prom_testutil.ToFloat64(sp.metrics.targetScrapePoolReloads))
}
@@ -619,22 +641,12 @@ func TestScrapePoolReloadPreserveRelabeledIntervalTimeout(t *testing.T) {
return l
}
reg, metrics := newTestRegistryAndScrapeMetrics(t)
- sp := &scrapePool{
- appendable: &nopAppendable{},
- activeTargets: map[uint64]*Target{
- 1: {
- labels: labels.FromStrings(model.ScrapeIntervalLabel, "5s", model.ScrapeTimeoutLabel, "3s"),
- },
- },
- loops: map[uint64]loop{
- 1: noopLoop(),
- },
- newLoop: newLoop,
- logger: nil,
- client: http.DefaultClient,
- metrics: metrics,
- symbolTable: labels.NewSymbolTable(),
+ sp := newTestScrapePool(t, nil, false, newLoop)
+ sp.activeTargets[1] = &Target{
+ labels: labels.FromStrings(model.ScrapeIntervalLabel, "5s", model.ScrapeTimeoutLabel, "3s"),
}
+ sp.metrics = metrics
+ sp.loops[1] = noopLoop()
err := sp.reload(reloadCfg)
if err != nil {
@@ -680,18 +692,10 @@ func TestScrapePoolTargetLimit(t *testing.T) {
}
return l
}
- sp := &scrapePool{
- appendable: &nopAppendable{},
- activeTargets: map[uint64]*Target{},
- loops: map[uint64]loop{},
- newLoop: newLoop,
- logger: promslog.NewNopLogger(),
- client: http.DefaultClient,
- metrics: newTestScrapeMetrics(t),
- symbolTable: labels.NewSymbolTable(),
- }
- tgs := []*targetgroup.Group{}
+ sp := newTestScrapePool(t, nil, false, newLoop)
+
+ var tgs []*targetgroup.Group
for i := range 50 {
tgs = append(tgs,
&targetgroup.Group{
@@ -781,12 +785,12 @@ func TestScrapePoolTargetLimit(t *testing.T) {
tgs = append(tgs,
&targetgroup.Group{
Targets: []model.LabelSet{
- {model.AddressLabel: model.LabelValue("127.0.0.1:1090")},
+ {model.AddressLabel: "127.0.0.1:1090"},
},
},
&targetgroup.Group{
Targets: []model.LabelSet{
- {model.AddressLabel: model.LabelValue("127.0.0.1:1090")},
+ {model.AddressLabel: "127.0.0.1:1090"},
},
},
)
@@ -796,62 +800,50 @@ func TestScrapePoolTargetLimit(t *testing.T) {
validateErrorMessage(false)
}
-func TestScrapePoolAppender(t *testing.T) {
- cfg := &config.ScrapeConfig{
- MetricNameValidationScheme: model.UTF8Validation,
- MetricNameEscapingScheme: model.AllowUTF8,
- }
- app := &nopAppendable{}
- sp, _ := newScrapePool(cfg, app, 0, nil, nil, &Options{}, newTestScrapeMetrics(t))
+func TestScrapePoolAppenderWithLimits(t *testing.T) {
+ // Create a unique value, to validate the correct chain of appenders.
+ baseAppender := struct{ storage.Appender }{}
+ appendable := appendableFunc(func(context.Context) storage.Appender { return baseAppender })
- loop := sp.newLoop(scrapeLoopOptions{
- target: &Target{},
+ sl, _ := newTestScrapeLoop(t, func(sl *scrapeLoop) {
+ sl.appendable = appendable
})
- appl, ok := loop.(*scrapeLoop)
- require.True(t, ok, "Expected scrapeLoop but got %T", loop)
-
- wrapped := appender(appl.appender(context.Background()), 0, 0, histogram.ExponentialSchemaMax)
+ wrapped := appenderWithLimits(sl.appendable.Appender(context.Background()), 0, 0, histogram.ExponentialSchemaMax)
tl, ok := wrapped.(*timeLimitAppender)
require.True(t, ok, "Expected timeLimitAppender but got %T", wrapped)
- _, ok = tl.Appender.(nopAppender)
- require.True(t, ok, "Expected base appender but got %T", tl.Appender)
+ require.Equal(t, baseAppender, tl.Appender, "Expected base appender but got %T", tl.Appender)
sampleLimit := 100
- loop = sp.newLoop(scrapeLoopOptions{
- target: &Target{},
- sampleLimit: sampleLimit,
+ sl, _ = newTestScrapeLoop(t, func(sl *scrapeLoop) {
+ sl.appendable = appendable
+ sl.sampleLimit = sampleLimit
})
- appl, ok = loop.(*scrapeLoop)
- require.True(t, ok, "Expected scrapeLoop but got %T", loop)
+ wrapped = appenderWithLimits(sl.appendable.Appender(context.Background()), sampleLimit, 0, histogram.ExponentialSchemaMax)
- wrapped = appender(appl.appender(context.Background()), sampleLimit, 0, histogram.ExponentialSchemaMax)
-
- sl, ok := wrapped.(*limitAppender)
+ la, ok := wrapped.(*limitAppender)
require.True(t, ok, "Expected limitAppender but got %T", wrapped)
- tl, ok = sl.Appender.(*timeLimitAppender)
- require.True(t, ok, "Expected timeLimitAppender but got %T", sl.Appender)
+ tl, ok = la.Appender.(*timeLimitAppender)
+ require.True(t, ok, "Expected timeLimitAppender but got %T", la.Appender)
- _, ok = tl.Appender.(nopAppender)
- require.True(t, ok, "Expected base appender but got %T", tl.Appender)
+ require.Equal(t, baseAppender, tl.Appender, "Expected base appender but got %T", tl.Appender)
- wrapped = appender(appl.appender(context.Background()), sampleLimit, 100, histogram.ExponentialSchemaMax)
+ wrapped = appenderWithLimits(sl.appendable.Appender(context.Background()), sampleLimit, 100, histogram.ExponentialSchemaMax)
bl, ok := wrapped.(*bucketLimitAppender)
require.True(t, ok, "Expected bucketLimitAppender but got %T", wrapped)
- sl, ok = bl.Appender.(*limitAppender)
+ la, ok = bl.Appender.(*limitAppender)
require.True(t, ok, "Expected limitAppender but got %T", bl)
- tl, ok = sl.Appender.(*timeLimitAppender)
- require.True(t, ok, "Expected timeLimitAppender but got %T", sl.Appender)
+ tl, ok = la.Appender.(*timeLimitAppender)
+ require.True(t, ok, "Expected timeLimitAppender but got %T", la.Appender)
- _, ok = tl.Appender.(nopAppender)
- require.True(t, ok, "Expected base appender but got %T", tl.Appender)
+ require.Equal(t, baseAppender, tl.Appender, "Expected base appender but got %T", tl.Appender)
- wrapped = appender(appl.appender(context.Background()), sampleLimit, 100, 0)
+ wrapped = appenderWithLimits(sl.appendable.Appender(context.Background()), sampleLimit, 100, 0)
ml, ok := wrapped.(*maxSchemaAppender)
require.True(t, ok, "Expected maxSchemaAppender but got %T", wrapped)
@@ -859,17 +851,86 @@ func TestScrapePoolAppender(t *testing.T) {
bl, ok = ml.Appender.(*bucketLimitAppender)
require.True(t, ok, "Expected bucketLimitAppender but got %T", wrapped)
- sl, ok = bl.Appender.(*limitAppender)
+ la, ok = bl.Appender.(*limitAppender)
require.True(t, ok, "Expected limitAppender but got %T", bl)
- tl, ok = sl.Appender.(*timeLimitAppender)
- require.True(t, ok, "Expected timeLimitAppender but got %T", sl.Appender)
+ tl, ok = la.Appender.(*timeLimitAppender)
+ require.True(t, ok, "Expected timeLimitAppender but got %T", la.Appender)
- _, ok = tl.Appender.(nopAppender)
- require.True(t, ok, "Expected base appender but got %T", tl.Appender)
+ 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")
@@ -881,7 +942,8 @@ func TestScrapePoolRaces(t *testing.T) {
MetricNameEscapingScheme: model.AllowUTF8,
}
}
- sp, _ := newScrapePool(newConfig(), &nopAppendable{}, 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{
@@ -907,12 +969,18 @@ func TestScrapePoolRaces(t *testing.T) {
for range 20 {
time.Sleep(10 * time.Millisecond)
- sp.reload(newConfig())
+ _ = sp.reload(newConfig())
}
sp.stop()
}
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)
@@ -924,16 +992,7 @@ func TestScrapePoolScrapeLoopsStarted(t *testing.T) {
}
return l
}
- sp := &scrapePool{
- appendable: &nopAppendable{},
- activeTargets: map[uint64]*Target{},
- loops: map[uint64]loop{},
- newLoop: newLoop,
- logger: nil,
- client: http.DefaultClient,
- metrics: newTestScrapeMetrics(t),
- symbolTable: labels.NewSymbolTable(),
- }
+ sp := newTestScrapePool(t, teststorage.NewAppendable(), appV2, newLoop)
tgs := []*targetgroup.Group{
{
@@ -964,51 +1023,13 @@ func TestScrapePoolScrapeLoopsStarted(t *testing.T) {
}
}
-func newBasicScrapeLoop(t testing.TB, ctx context.Context, scraper scraper, app func(ctx context.Context) storage.Appender, interval time.Duration) *scrapeLoop {
- return newBasicScrapeLoopWithFallback(t, ctx, scraper, app, interval, "")
-}
-
-func newBasicScrapeLoopWithFallback(t testing.TB, ctx context.Context, scraper scraper, app func(ctx context.Context) storage.Appender, interval time.Duration, fallback string) *scrapeLoop {
- return newScrapeLoop(ctx,
- scraper,
- nil, nil,
- nopMutator,
- nopMutator,
- app,
- nil,
- labels.NewSymbolTable(),
- 0,
- true,
- false,
- true,
- 0, 0, histogram.ExponentialSchemaMax,
- nil,
- interval,
- time.Hour,
- false,
- false,
- false,
- false,
- false,
- false,
- true,
- nil,
- false,
- newTestScrapeMetrics(t),
- false,
- model.UTF8Validation,
- model.NoEscaping,
- fallback,
- )
-}
-
func TestScrapeLoopStopBeforeRun(t *testing.T) {
t.Parallel()
- scraper := &testScraper{}
- sl := newBasicScrapeLoop(t, context.Background(), scraper, nil, 1)
+
+ sl, scraper := newTestScrapeLoop(t)
// The scrape pool synchronizes on stopping scrape loops. However, new scrape
- // loops are started asynchronously. Thus it's possible, that a loop is stopped
+ // loops are started asynchronously. Thus, it's possible, that a loop is stopped
// again before having started properly.
// Stopping not-yet-started loops must block until the run method was called and exited.
// The run method must exit immediately.
@@ -1053,26 +1074,29 @@ func TestScrapeLoopStopBeforeRun(t *testing.T) {
func nopMutator(l labels.Labels) labels.Labels { return l }
func TestScrapeLoopStop(t *testing.T) {
- var (
- signal = make(chan struct{}, 1)
- appender = &collectResultAppender{}
- scraper = &testScraper{}
- app = func(context.Context) storage.Appender { return appender }
- )
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopStop(t, appV2)
+ })
+}
- // Since we're writing samples directly below we need to provide a protocol fallback.
- sl := newBasicScrapeLoopWithFallback(t, context.Background(), scraper, app, 10*time.Millisecond, "text/plain")
+func testScrapeLoopStop(t *testing.T, appV2 bool) {
+ signal := make(chan struct{}, 1)
+
+ appTest := teststorage.NewAppendable()
+ 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"
+ })
// Terminate loop after 2 scrapes.
numScrapes := 0
-
scraper.scrapeFunc = func(ctx context.Context, w io.Writer) error {
numScrapes++
if numScrapes == 2 {
go sl.stop()
<-sl.ctx.Done()
}
- w.Write([]byte("metric_a 42\n"))
+ _, _ = w.Write([]byte("metric_a 42\n"))
return ctx.Err()
}
@@ -1087,70 +1111,42 @@ func TestScrapeLoopStop(t *testing.T) {
require.FailNow(t, "Scrape wasn't stopped.")
}
+ got := appTest.ResultSamples()
// We expected 1 actual sample for each scrape plus 5 for report samples.
// At least 2 scrapes were made, plus the final stale markers.
- require.GreaterOrEqual(t, len(appender.resultFloats), 6*3, "Expected at least 3 scrapes with 6 samples each.")
- require.Zero(t, len(appender.resultFloats)%6, "There is a scrape with missing samples.")
+ require.GreaterOrEqual(t, len(got), 6*3, "Expected at least 3 scrapes with 6 samples each.")
+ require.Zero(t, len(got)%6, "There is a scrape with missing samples.")
// All samples in a scrape must have the same timestamp.
var ts int64
- for i, s := range appender.resultFloats {
+ for i, s := range got {
switch {
case i%6 == 0:
- ts = s.t
- case s.t != ts:
+ ts = s.T
+ case s.T != ts:
t.Fatalf("Unexpected multiple timestamps within single scrape")
}
}
// All samples from the last scrape must be stale markers.
- for _, s := range appender.resultFloats[len(appender.resultFloats)-5:] {
- require.True(t, value.IsStaleNaN(s.f), "Appended last sample not as expected. Wanted: stale NaN Got: %x", math.Float64bits(s.f))
+ for _, s := range got[len(got)-5:] {
+ require.True(t, value.IsStaleNaN(s.V), "Appended last sample not as expected. Wanted: stale NaN Got: %x", math.Float64bits(s.V))
}
}
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)
errc = make(chan error)
-
- scraper = &testScraper{}
- app = func(context.Context) storage.Appender { return &nopAppender{} }
- scrapeMetrics = newTestScrapeMetrics(t)
- )
-
- ctx, cancel := context.WithCancel(context.Background())
- sl := newScrapeLoop(ctx,
- scraper,
- nil, nil,
- nopMutator,
- nopMutator,
- app,
- nil,
- nil,
- 0,
- true,
- false,
- true,
- 0, 0, histogram.ExponentialSchemaMax,
- nil,
- time.Second,
- time.Hour,
- false,
- false,
- false,
- false,
- false,
- false,
- false,
- nil,
- false,
- scrapeMetrics,
- false,
- model.UTF8Validation,
- model.NoEscaping,
- "",
)
+ ctx, cancel := context.WithCancel(t.Context())
+ 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
@@ -1172,24 +1168,26 @@ func TestScrapeLoopRun(t *testing.T) {
require.FailNow(t, "Unexpected error", "err: %s", err)
}
+ ctx, cancel = context.WithCancel(t.Context())
+ sl, scraper = newTestScrapeLoop(t, withAppendable(teststorage.NewAppendable(), appV2), func(sl *scrapeLoop) {
+ sl.ctx = ctx
+ sl.timeout = 100 * time.Millisecond
+ })
// The provided timeout must cause cancellation of the context passed down to the
// scraper. The scraper has to respect the context.
scraper.offsetDur = 0
- block := make(chan struct{})
+ blockCtx, blockCancel := context.WithCancel(t.Context())
scraper.scrapeFunc = func(ctx context.Context, _ io.Writer) error {
select {
- case <-block:
+ case <-blockCtx.Done():
+ cancel()
case <-ctx.Done():
return ctx.Err()
}
return nil
}
- ctx, cancel = context.WithCancel(context.Background())
- sl = newBasicScrapeLoop(t, ctx, scraper, app, time.Second)
- sl.timeout = 100 * time.Millisecond
-
go func() {
sl.run(errc)
signal <- struct{}{}
@@ -1205,9 +1203,7 @@ func TestScrapeLoopRun(t *testing.T) {
// We already caught the timeout error and are certainly in the loop.
// Let the scrapes returns immediately to cause no further timeout errors
// and check whether canceling the parent context terminates the loop.
- close(block)
- cancel()
-
+ blockCancel()
select {
case <-signal:
// Loop terminated as expected.
@@ -1219,16 +1215,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)
-
- scraper = &testScraper{}
- app = func(context.Context) storage.Appender { return &nopAppender{} }
)
- ctx, cancel := context.WithCancel(context.Background())
- sl := newBasicScrapeLoop(t, ctx, scraper, app, time.Second)
+ ctx, cancel := context.WithCancel(t.Context())
+ sl, scraper := newTestScrapeLoop(t, withCtx(ctx), withAppendable(teststorage.NewAppendable(), appV2))
forcedErr := errors.New("forced err")
sl.setForcedError(forcedErr)
@@ -1258,51 +1257,59 @@ func TestScrapeLoopForcedErr(t *testing.T) {
}
}
-func TestScrapeLoopMetadata(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)
+
var (
- signal = make(chan struct{})
- scraper = &testScraper{}
- scrapeMetrics = newTestScrapeMetrics(t)
- cache = newScrapeCache(scrapeMetrics)
+ signal = make(chan struct{})
+ errc = make(chan error)
)
- defer close(signal)
- ctx, cancel := context.WithCancel(context.Background())
- sl := newScrapeLoop(ctx,
- scraper,
- nil, nil,
- nopMutator,
- nopMutator,
- func(context.Context) storage.Appender { return nopAppender{} },
- cache,
- labels.NewSymbolTable(),
- 0,
- true,
- false,
- true,
- 0, 0, histogram.ExponentialSchemaMax,
- nil,
- 0,
- 0,
- false,
- false,
- false,
- false,
- false,
- false,
- false,
- nil,
- false,
- scrapeMetrics,
- false,
- model.UTF8Validation,
- model.NoEscaping,
- "",
- )
- defer cancel()
+ ctx, cancel := context.WithCancel(t.Context())
+ sl, scraper := newTestScrapeLoop(t, withCtx(ctx), withAppendable(teststorage.NewAppendable(), appV2))
- slApp := sl.appender(ctx)
- total, _, _, err := sl.append(slApp, []byte(`# TYPE test_metric counter
+ forcedErr := errors.New("forced err")
+ sl.setForcedError(forcedErr)
+
+ scraper.scrapeFunc = func(context.Context, io.Writer) error {
+ return nil
+ }
+
+ go func() {
+ sl.run(errc)
+ close(signal)
+ }()
+
+ time.Sleep(50 * time.Millisecond)
+
+ cancel()
+
+ select {
+ case <-signal:
+ // success case
+ case <-time.After(3 * time.Second):
+ require.FailNow(t, "Scrape loop failed to exit on context cancellation (goroutine leak detected)")
+ }
+}
+
+func TestScrapeLoopMetadata(t *testing.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
# HELP test_metric some help text
# UNIT test_metric metric
test_metric_total 1
@@ -1310,54 +1317,48 @@ test_metric_total 1
# HELP test_metric_no_type other help text
# EOF`), "application/openmetrics-text", time.Now())
require.NoError(t, err)
- require.NoError(t, slApp.Commit())
+ require.NoError(t, app.Commit())
require.Equal(t, 1, total)
- md, ok := cache.GetMetadata("test_metric")
+ md, ok := sl.cache.GetMetadata("test_metric")
require.True(t, ok, "expected metadata to be present")
require.Equal(t, model.MetricTypeCounter, md.Type, "unexpected metric type")
require.Equal(t, "some help text", md.Help)
require.Equal(t, "metric", md.Unit)
- md, ok = cache.GetMetadata("test_metric_no_help")
+ md, ok = sl.cache.GetMetadata("test_metric_no_help")
require.True(t, ok, "expected metadata to be present")
require.Equal(t, model.MetricTypeGauge, md.Type, "unexpected metric type")
require.Empty(t, md.Help)
require.Empty(t, md.Unit)
- md, ok = cache.GetMetadata("test_metric_no_type")
+ md, ok = sl.cache.GetMetadata("test_metric_no_type")
require.True(t, ok, "expected metadata to be present")
require.Equal(t, model.MetricTypeUnknown, md.Type, "unexpected metric type")
require.Equal(t, "other help text", md.Help)
require.Empty(t, md.Unit)
}
-func simpleTestScrapeLoop(t testing.TB) (context.Context, *scrapeLoop) {
- // Need a full storage for correct Add/AddFast semantics.
- s := teststorage.New(t)
- t.Cleanup(func() { s.Close() })
-
- ctx, cancel := context.WithCancel(context.Background())
- sl := newBasicScrapeLoop(t, ctx, &testScraper{}, s.Appender, 0)
- t.Cleanup(func() { cancel() })
-
- return ctx, sl
+func TestScrapeLoopSeriesAdded(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopSeriesAdded(t, appV2)
+ })
}
-func TestScrapeLoopSeriesAdded(t *testing.T) {
- ctx, sl := simpleTestScrapeLoop(t)
+func testScrapeLoopSeriesAdded(t *testing.T, appV2 bool) {
+ sl, _ := newTestScrapeLoop(t, withAppendable(teststorage.NewAppendable(), appV2))
- slApp := sl.appender(ctx)
- total, added, seriesAdded, err := sl.append(slApp, []byte("test_metric 1\n"), "text/plain", time.Time{})
+ app := sl.appender()
+ total, added, seriesAdded, err := app.append([]byte("test_metric 1\n"), "text/plain", time.Time{})
require.NoError(t, err)
- require.NoError(t, slApp.Commit())
+ require.NoError(t, app.Commit())
require.Equal(t, 1, total)
require.Equal(t, 1, added)
require.Equal(t, 1, seriesAdded)
- slApp = sl.appender(ctx)
- total, added, seriesAdded, err = sl.append(slApp, []byte("test_metric 1\n"), "text/plain", time.Time{})
- require.NoError(t, slApp.Commit())
+ app = sl.appender()
+ total, added, seriesAdded, err = app.append([]byte("test_metric 1\n"), "text/plain", time.Time{})
+ require.NoError(t, app.Commit())
require.NoError(t, err)
require.Equal(t, 1, total)
require.Equal(t, 1, added)
@@ -1365,10 +1366,12 @@ func TestScrapeLoopSeriesAdded(t *testing.T) {
}
func TestScrapeLoopFailWithInvalidLabelsAfterRelabel(t *testing.T) {
- s := teststorage.New(t)
- defer s.Close()
- ctx := t.Context()
+ 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"),
}
@@ -1379,43 +1382,47 @@ func TestScrapeLoopFailWithInvalidLabelsAfterRelabel(t *testing.T) {
Replacement: "$1",
NameValidationScheme: model.UTF8Validation,
}}
- sl := newBasicScrapeLoop(t, ctx, &testScraper{}, s.Appender, 0)
- sl.sampleMutator = func(l labels.Labels) labels.Labels {
- return mutateSampleLabels(l, target, true, relabelConfig)
- }
+ sl, _ := newTestScrapeLoop(t, withAppendable(teststorage.NewAppendable(), appV2), func(sl *scrapeLoop) {
+ sl.sampleMutator = func(l labels.Labels) labels.Labels {
+ return mutateSampleLabels(l, target, true, relabelConfig)
+ }
+ })
- slApp := sl.appender(ctx)
- total, added, seriesAdded, err := sl.append(slApp, []byte("test_metric 1\n"), "text/plain", time.Time{})
+ app := sl.appender()
+ total, added, seriesAdded, err := app.append([]byte("test_metric 1\n"), "text/plain", time.Time{})
require.ErrorContains(t, err, "invalid metric name or label names")
- require.NoError(t, slApp.Rollback())
+ require.NoError(t, app.Rollback())
require.Equal(t, 1, total)
require.Equal(t, 0, added)
require.Equal(t, 0, seriesAdded)
}
func TestScrapeLoopFailLegacyUnderUTF8(t *testing.T) {
- // Test that scrapes fail when default validation is utf8 but scrape config is
- // legacy.
- s := teststorage.New(t)
- defer s.Close()
- ctx := t.Context()
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopFailLegacyUnderUTF8(t, appV2)
+ })
+}
- sl := newBasicScrapeLoop(t, ctx, &testScraper{}, s.Appender, 0)
- sl.validationScheme = model.LegacyValidation
+func testScrapeLoopFailLegacyUnderUTF8(t *testing.T, appV2 bool) {
+ sl, _ := newTestScrapeLoop(t, withAppendable(teststorage.NewAppendable(), appV2), func(sl *scrapeLoop) {
+ sl.validationScheme = model.LegacyValidation
+ })
- slApp := sl.appender(ctx)
- total, added, seriesAdded, err := sl.append(slApp, []byte("{\"test.metric\"} 1\n"), "text/plain", time.Time{})
+ app := sl.appender()
+ total, added, seriesAdded, err := app.append([]byte("{\"test.metric\"} 1\n"), "text/plain", time.Time{})
require.ErrorContains(t, err, "invalid metric name or label names")
- require.NoError(t, slApp.Rollback())
+ require.NoError(t, app.Rollback())
require.Equal(t, 1, total)
require.Equal(t, 0, added)
require.Equal(t, 0, seriesAdded)
// When scrapeloop has validation set to UTF-8, the metric is allowed.
- sl.validationScheme = model.UTF8Validation
+ sl, _ = newTestScrapeLoop(t, withAppendable(teststorage.NewAppendable(), appV2), func(sl *scrapeLoop) {
+ sl.validationScheme = model.UTF8Validation
+ })
- slApp = sl.appender(ctx)
- total, added, seriesAdded, err = sl.append(slApp, []byte("{\"test.metric\"} 1\n"), "text/plain", time.Time{})
+ app = sl.appender()
+ total, added, seriesAdded, err = app.append([]byte("{\"test.metric\"} 1\n"), "text/plain", time.Time{})
require.NoError(t, err)
require.Equal(t, 1, total)
require.Equal(t, 1, added)
@@ -1429,27 +1436,66 @@ func readTextParseTestMetrics(t testing.TB) []byte {
if err != nil {
t.Fatal(err)
}
- return b
+
+ // Replace all Carriage Return chars that appear when testing on windows.
+ return bytes.ReplaceAll(b, []byte{'\r'}, nil)
}
func makeTestGauges(n int) []byte {
sb := bytes.Buffer{}
- fmt.Fprintf(&sb, "# TYPE metric_a gauge\n")
- fmt.Fprintf(&sb, "# HELP metric_a help text\n")
+ sb.WriteString("# TYPE metric_a gauge\n")
+ sb.WriteString("# HELP metric_a help text\n")
for i := range n {
- fmt.Fprintf(&sb, "metric_a{foo=\"%d\",bar=\"%d\"} 1\n", i, i*100)
+ _, _ = fmt.Fprintf(&sb, "metric_a{foo=\"%d\",bar=\"%d\"} 1\n", i, i*100)
}
- fmt.Fprintf(&sb, "# EOF\n")
+ sb.WriteString("# EOF\n")
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
@@ -1475,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)
@@ -1500,86 +1545,405 @@ 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:
-// *`data`: different sizes of metrics scraped e.g. one big gauge metric family
+// TestScrapeLoopAppend_WithStorage tests appends and storage integration for the
+// large input files that are also used in benchmarks.
+func TestScrapeLoopAppend_WithStorage(t *testing.T) {
+ ts := time.Now()
+
+ for _, appV2 := range []bool{false, true} {
+ for _, tc := range []struct {
+ name string
+ parsableText []byte
+
+ expectedSamplesLen int
+ testAppendedSamples func(t *testing.T, committed []sample)
+ testExemplars func(t *testing.T, er []exemplar.QueryResult)
+ }{
+ {
+ name: "1Fam2000Gauges",
+ parsableText: makeTestGauges(2000),
+
+ expectedSamplesLen: 2000,
+ testAppendedSamples: func(t *testing.T, committed []sample) {
+ var expectedMF string
+ if appV2 {
+ expectedMF = "metric_a" // Only AppenderV2 supports metric family passing.
+ }
+ // Verify a few samples.
+ testutil.RequireEqual(t, sample{
+ MF: expectedMF,
+ M: metadata.Metadata{Type: model.MetricTypeGauge, Help: "help text"},
+ L: labels.FromStrings(model.MetricNameLabel, "metric_a", "foo", "0", "bar", "0"), V: 1, T: timestamp.FromTime(ts),
+ }, committed[0])
+ testutil.RequireEqual(t, sample{
+ MF: expectedMF,
+ M: metadata.Metadata{Type: model.MetricTypeGauge, Help: "help text"},
+ L: labels.FromStrings(model.MetricNameLabel, "metric_a", "foo", "1245", "bar", "124500"), V: 1, T: timestamp.FromTime(ts),
+ }, committed[1245])
+ testutil.RequireEqual(t, sample{
+ MF: expectedMF,
+ M: metadata.Metadata{Type: model.MetricTypeGauge, Help: "help text"},
+ L: labels.FromStrings(model.MetricNameLabel, "metric_a", "foo", "1999", "bar", "199900"), V: 1, T: timestamp.FromTime(ts),
+ }, committed[len(committed)-1])
+ },
+ },
+ {
+ name: "237FamsAllTypes",
+ parsableText: readTextParseTestMetrics(t),
+
+ expectedSamplesLen: 1857,
+ testAppendedSamples: func(t *testing.T, committed []sample) {
+ // Verify a few samples.
+ testutil.RequireEqual(t, sample{
+ MF: func() string {
+ if !appV2 {
+ return ""
+ }
+ return "go_gc_gomemlimit_bytes"
+ }(),
+ M: metadata.Metadata{Type: model.MetricTypeGauge, Help: "Go runtime memory limit configured by the user, otherwise math.MaxInt64. This value is set by the GOMEMLIMIT environment variable, and the runtime/debug.SetMemoryLimit function. Sourced from /gc/gomemlimit:bytes"},
+ L: labels.FromStrings(model.MetricNameLabel, "go_gc_gomemlimit_bytes"), V: 9.03676723e+08, T: timestamp.FromTime(ts),
+ }, committed[11])
+ testutil.RequireEqual(t, sample{
+ MF: func() string {
+ if !appV2 {
+ return "" // Only AppenderV2 supports metric family passing.
+ }
+ return "prometheus_http_request_duration_seconds"
+ }(),
+ M: metadata.Metadata{Type: model.MetricTypeHistogram, Help: "Histogram of latencies for HTTP requests."},
+ L: labels.FromStrings(model.MetricNameLabel, "prometheus_http_request_duration_seconds_bucket", "handler", "/api/v1/query_range", "le", "120.0"), V: 118157, T: timestamp.FromTime(ts),
+ }, committed[448])
+ testutil.RequireEqual(t, sample{
+ MF: func() string {
+ if !appV2 {
+ return "" // Only AppenderV2 supports metric family passing.
+ }
+ return "promhttp_metric_handler_requests_total"
+ }(),
+ M: metadata.Metadata{Type: model.MetricTypeCounter, Help: "Total number of scrapes by HTTP status code."},
+ L: labels.FromStrings(model.MetricNameLabel, "promhttp_metric_handler_requests_total", "code", "503"), V: 0, T: timestamp.FromTime(ts),
+ }, committed[len(committed)-1])
+ },
+ },
+ {
+ name: "100HistsWithExemplars",
+ parsableText: makeTestHistogramsWithExemplars(100),
+
+ expectedSamplesLen: 24 * 100,
+ testAppendedSamples: func(t *testing.T, committed []sample) {
+ // Verify a few samples.
+ m := metadata.Metadata{Type: model.MetricTypeHistogram, Help: "RPC latency distributions."}
+ testutil.RequireEqual(t, sample{
+ MF: func() string {
+ if !appV2 {
+ return "" // Only AppenderV2 supports metric family passing.
+ }
+ return "rpc_durations_histogram0_seconds"
+ }(),
+ M: m, L: labels.FromStrings(model.MetricNameLabel, "rpc_durations_histogram0_seconds_bucket", "le", "0.0003100000000000002"), V: 15, T: timestamp.FromTime(ts),
+ ES: []exemplar.Exemplar{
+ {Labels: labels.FromStrings("dummyID", "9818"), Value: 0.0002791130914009552, Ts: 1726839814982, HasTs: true},
+ },
+ }, committed[13])
+ testutil.RequireEqual(t, sample{
+ MF: func() string {
+ if !appV2 {
+ return "" // Only AppenderV2 supports metric family passing.
+ }
+ return "rpc_durations_histogram49_seconds"
+ }(),
+ M: m, L: labels.FromStrings(model.MetricNameLabel, "rpc_durations_histogram49_seconds_sum"), V: -8.452185437166741e-05, T: timestamp.FromTime(ts),
+ }, committed[24*50-3])
+
+ // This series does not have metadata, nor metric family, because of isSeriesPartOfFamily bug and OpenMetric 1.0 limitations around _created series.
+ // TODO(bwplotka): Fix with https://github.com/prometheus/prometheus/issues/17900
+ testutil.RequireEqual(t, sample{
+ L: labels.FromStrings(model.MetricNameLabel, "rpc_durations_histogram99_seconds_created"), V: 1.726839813016302e+09, T: timestamp.FromTime(ts),
+ }, committed[len(committed)-1])
+ },
+ testExemplars: func(t *testing.T, er []exemplar.QueryResult) {
+ // 12 out of 24 histogram series have exemplars.
+ require.Len(t, er, 12*100)
+ testutil.RequireEqual(t, exemplar.QueryResult{
+ SeriesLabels: labels.FromStrings(model.MetricNameLabel, "rpc_durations_histogram0_seconds_bucket", "le", "0.0003100000000000002"),
+ Exemplars: []exemplar.Exemplar{
+ {Labels: labels.FromStrings("dummyID", "9818"), Value: 0.0002791130914009552, Ts: 1726839814982, HasTs: true},
+ },
+ }, er[10])
+ testutil.RequireEqual(t, exemplar.QueryResult{
+ SeriesLabels: labels.FromStrings(model.MetricNameLabel, "rpc_durations_histogram9_seconds_bucket", "le", "1.0000000000000216e-05"),
+ Exemplars: []exemplar.Exemplar{
+ {Labels: labels.FromStrings("dummyID", "19206"), Value: -4.6156147425468016e-05, Ts: 1726839815133, HasTs: true},
+ },
+ }, er[len(er)-1])
+ },
+ },
+ } {
+ t.Run(fmt.Sprintf("appV2=%v/data=%v", appV2, tc.name), func(t *testing.T) {
+ s := teststorage.New(t, func(opt *tsdb.Options) {
+ opt.EnableMetadataWALRecords = true
+ })
+
+ appTest := teststorage.NewAppendable().Then(s)
+ sl, _ := newTestScrapeLoop(t, withAppendable(appTest, appV2))
+ app := sl.appender()
+
+ _, _, _, err := app.append(tc.parsableText, "application/openmetrics-text", ts)
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+
+ // Check the recorded samples on the Appender layer.
+ require.Nil(t, appTest.PendingSamples())
+ require.Nil(t, appTest.RolledbackSamples())
+
+ got := appTest.ResultSamples()
+ require.Len(t, got, tc.expectedSamplesLen)
+ tc.testAppendedSamples(t, got)
+
+ // Check basic storage stats.
+ stats := s.Head().Stats(model.MetricNameLabel, 2000)
+ require.Equal(t, tc.expectedSamplesLen, int(stats.NumSeries))
+
+ // Check exemplars.
+ eq, err := s.ExemplarQuerier(t.Context())
+ require.NoError(t, err)
+
+ er, err := eq.Select(math.MinInt64, math.MaxInt64, nil)
+ require.NoError(t, err)
+
+ if tc.testExemplars != nil {
+ tc.testExemplars(t, er)
+ } else {
+ // Expect no exemplars.
+ require.Empty(t, er, "%v is not empty", er)
+ }
+ })
+ }
+ }
+}
+
+// BenchmarkScrapeLoopAppend benchmarks scrape appends for typical cases.
+//
+// Benchmark compares append function run across 5 dimensions:
+// * `withStorage`: without storage isolates the benchmark to the scrape loop append code. With storage is an
+// integration benchmark with the TSDB head appender code. For acceptance criteria run with storage, without for debugging.
+// * `appV2`: appender V1 or V2.
+// * `appendMetadataToWAL`: metadata-wal-records feature enabled or not (problematic feature we might need to change
+// soon, see https://github.com/prometheus/prometheus/issues/15911.
+// * `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.
+// * `fmt`: different scrape formats which will benchmark different parsers e.g.
// promtext, omtext and promproto.
//
-// Recommended CLI invocation:
+// NOTE: withStorage=true uses sync.Pool buffers which is heavily non-deterministic and shared across go routines.
+// As a result, it's recommended to run dimensions you want to compare with in e.g. separate go tool invocations.
+// Recommended CLI invocation(s):
/*
- export bench=append-v1 && go test ./scrape/... \
- -run '^$' -bench '^BenchmarkScrapeLoopAppend' \
- -benchtime 5s -count 6 -cpu 2 -timeout 999m \
+ # Acceptance: With storage with V1 and V2 in separate process:
+ export bench=appendV1 && go test ./scrape/... \
+ -run '^$' -bench '^BenchmarkScrapeLoopAppend/withStorage=true/appV2=false/$' \
+ -benchtime 2s -count 6 -cpu 2 -timeout 999m \
+ | tee ${bench}.txt
+
+ export bench=appendV2 && go test ./scrape/... \
+ -run '^$' -bench '^BenchmarkScrapeLoopAppend/withStorage=true/appV2=true/$' \
+ -benchtime 2s -count 6 -cpu 2 -timeout 999m \
+ | tee ${bench}.txt
+
+ # For debugging scrape overheads:
+ export bench=appendNoStorage && go test ./scrape/... \
+ -run '^$' -bench '^BenchmarkScrapeLoopAppend/withStorage=false/$' \
+ -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 _, withStorage := range []bool{false, true} {
+ for _, appV2 := range []bool{false, true} {
+ for _, appendMetadataToWAL := range []bool{false, true} {
+ for _, data := range []struct {
+ name string
+ parsableText []byte
+ }{
+ {name: "1Fam2000Gauges", 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("withStorage=%v/appV2=%v/appendMetadataToWAL=%v/data=%v", withStorage, appV2, appendMetadataToWAL, data.name), func(b *testing.B) {
+ metricsProto := promTextToProto(b, data.parsableText)
- 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) {
- ctx, sl := simpleTestScrapeLoop(b)
-
- slApp := sl.appender(ctx)
- ts := time.Time{}
-
- b.ReportAllocs()
- b.ResetTimer()
- for b.Loop() {
- ts = ts.Add(time.Second)
- _, _, _, err := sl.append(slApp, 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, withStorage, appV2, bcase.parsable, bcase.contentType, appendMetadataToWAL, false)
+ })
}
- }
- })
+ })
+ }
+ }
+ }
+ }
+}
+
+func benchScrapeLoopAppend(
+ b *testing.B,
+ withStorage bool,
+ appV2 bool,
+ parsable []byte,
+ contentType string,
+ appendMetadataToWAL bool,
+ enableExemplarStorage bool,
+) {
+ var a compatAppendable = teststorage.NewAppendable().SkipRecording(true) // Make it noop for benchmark purposes.
+ if withStorage {
+ a = teststorage.New(b, func(opt *tsdb.Options) {
+ opt.EnableMetadataWALRecords = appendMetadataToWAL
+ if enableExemplarStorage {
+ opt.EnableExemplarStorage = true
+ opt.MaxExemplars = 1e5
+ }
+ })
+ }
+ sl, _ := newTestScrapeLoop(b, withAppendable(a, appV2), func(sl *scrapeLoop) {
+ sl.appendMetadataToWAL = appendMetadataToWAL
+ })
+ ts := time.Time{}
+
+ b.ReportAllocs()
+ b.ResetTimer()
+ for b.Loop() {
+ app := sl.appender()
+ 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)
+ }
+ }
+}
+
+// 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, true, 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, withAppendable(appTest, 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{}
+
+ sl.scrapeAndReport(time.Time{}, ts, nil)
+ require.NoError(t, scraper.lastError)
+
+ require.Len(t, appTest.ResultSamples(), 1862)
+ require.Len(t, appTest.ResultMetadata(), 1862)
+}
+
+// Recommended CLI invocation:
+/*
+ export bench=scrapeAndReport && go test ./scrape/... \
+ -run '^$' -bench '^BenchmarkScrapeLoopScrapeAndReport$' \
+ -benchtime 5s -count 6 -cpu 2 -timeout 999m \
+ | tee ${bench}.txt
+*/
+func BenchmarkScrapeLoopScrapeAndReport(b *testing.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)
+
+ 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{}
+
+ 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)
- defer s.Close()
+ 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
+ })
signal := make(chan struct{}, 1)
- ctx, cancel := context.WithCancel(context.Background())
+ ctx, cancel := context.WithCancel(t.Context())
defer cancel()
// Function to run the scrape loop
runScrapeLoop := func(ctx context.Context, t *testing.T, cue int, action func(*scrapeLoop)) {
- var (
- scraper = &testScraper{}
- app = func(ctx context.Context) storage.Appender {
- return s.Appender(ctx)
- }
- )
- sl := newBasicScrapeLoop(t, ctx, scraper, app, 10*time.Millisecond)
+ sl, scraper := newTestScrapeLoop(t, withAppendable(s, appV2), func(sl *scrapeLoop) {
+ sl.ctx = ctx
+ })
+
numScrapes := 0
scraper.scrapeFunc = func(_ context.Context, w io.Writer) error {
numScrapes++
if numScrapes == cue {
action(sl)
}
- fmt.Fprintf(w, "metric_a{a=\"1\",b=\"1\"} %d\n", 42+numScrapes)
+ _, _ = fmt.Fprintf(w, "metric_a{a=\"1\",b=\"1\"} %d\n", 42+numScrapes)
return nil
}
sl.run(nil)
@@ -1604,25 +1968,25 @@ func TestSetOptionsHandlingStaleness(t *testing.T) {
t.Fatalf("Scrape wasn't stopped.")
}
- ctx1, cancel := context.WithCancel(context.Background())
+ ctx1, cancel := context.WithCancel(t.Context())
defer cancel()
q, err := s.Querier(0, time.Now().UnixNano())
require.NoError(t, err)
- defer q.Close()
+ t.Cleanup(func() { _ = q.Close() })
series := q.Select(ctx1, false, nil, labels.MustNewMatcher(labels.MatchRegexp, "__name__", "metric_a"))
- var results []floatSample
+ var results []sample
for series.Next() {
it := series.At().Iterator(nil)
for it.Next() == chunkenc.ValFloat {
t, v := it.At()
- results = append(results, floatSample{
- metric: series.At().Labels(),
- t: t,
- f: v,
+ results = append(results, sample{
+ L: series.At().Labels(),
+ T: t,
+ V: v,
})
}
require.NoError(t, it.Err())
@@ -1630,33 +1994,38 @@ func TestSetOptionsHandlingStaleness(t *testing.T) {
require.NoError(t, series.Err())
var c int
for _, s := range results {
- if value.IsStaleNaN(s.f) {
+ if value.IsStaleNaN(s.V) {
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) {
- appender := &collectResultAppender{}
- var (
- signal = make(chan struct{}, 1)
- scraper = &testScraper{}
- app = func(context.Context) storage.Appender { return appender }
- )
+ 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, 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"
+ })
- ctx, cancel := context.WithCancel(context.Background())
- // Since we're writing samples directly below we need to provide a protocol fallback.
- sl := newBasicScrapeLoopWithFallback(t, ctx, scraper, app, 10*time.Millisecond, "text/plain")
// Succeed once, several failures, then stop.
numScrapes := 0
-
scraper.scrapeFunc = func(_ context.Context, w io.Writer) error {
numScrapes++
switch numScrapes {
case 1:
- w.Write([]byte("metric_a 42\n"))
+ _, _ = w.Write([]byte("metric_a 42\n"))
return nil
case 5:
cancel()
@@ -1675,36 +2044,44 @@ func TestScrapeLoopRunCreatesStaleMarkersOnFailedScrape(t *testing.T) {
require.FailNow(t, "Scrape wasn't stopped.")
}
- // 1 successfully scraped sample, 1 stale marker after first fail, 5 report samples for
- // each scrape successful or not.
- require.Len(t, appender.resultFloats, 27, "Appended samples not as expected:\n%s", appender)
- require.Equal(t, 42.0, appender.resultFloats[0].f, "Appended first sample not as expected")
- require.True(t, value.IsStaleNaN(appender.resultFloats[6].f),
- "Appended second sample not as expected. Wanted: stale NaN Got: %x", math.Float64bits(appender.resultFloats[6].f))
+ got := appTest.ResultSamples()
+ // 1 successfully scraped sample
+ // 1 stale marker after first fail
+ // 5x 5 report samples for each scrape successful or not.
+ require.Len(t, got, 27, "Appended samples not as expected:\n%s", appTest)
+ require.Equal(t, 42.0, got[0].V, "Appended first sample not as expected")
+ require.True(t, value.IsStaleNaN(got[6].V),
+ "Appended second sample not as expected. Wanted: stale NaN Got: %x", math.Float64bits(got[6].V))
}
func TestScrapeLoopRunCreatesStaleMarkersOnParseFailure(t *testing.T) {
- appender := &collectResultAppender{}
- var (
- signal = make(chan struct{}, 1)
- scraper = &testScraper{}
- app = func(context.Context) storage.Appender { return appender }
- numScrapes = 0
- )
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopRunCreatesStaleMarkersOnParseFailure(t, appV2)
+ })
+}
- ctx, cancel := context.WithCancel(context.Background())
- // Since we're writing samples directly below we need to provide a protocol fallback.
- sl := newBasicScrapeLoopWithFallback(t, ctx, scraper, app, 10*time.Millisecond, "text/plain")
+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, 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"
+ })
// Succeed once, several failures, then stop.
+ numScrapes := 0
scraper.scrapeFunc = func(_ context.Context, w io.Writer) error {
numScrapes++
+
switch numScrapes {
case 1:
- w.Write([]byte("metric_a 42\n"))
+ _, _ = w.Write([]byte("metric_a 42\n"))
return nil
case 2:
- w.Write([]byte("7&-\n"))
+ _, _ = w.Write([]byte("7&-\n"))
return nil
case 3:
cancel()
@@ -1719,46 +2096,54 @@ func TestScrapeLoopRunCreatesStaleMarkersOnParseFailure(t *testing.T) {
select {
case <-signal:
+ // TODO(bwplotka): Prone to flakiness, depend on atomic numScrapes.
case <-time.After(5 * time.Second):
require.FailNow(t, "Scrape wasn't stopped.")
}
- // 1 successfully scraped sample, 1 stale marker after first fail, 5 report samples for
- // each scrape successful or not.
- require.Len(t, appender.resultFloats, 17, "Appended samples not as expected:\n%s", appender)
- require.Equal(t, 42.0, appender.resultFloats[0].f, "Appended first sample not as expected")
- require.True(t, value.IsStaleNaN(appender.resultFloats[6].f),
- "Appended second sample not as expected. Wanted: stale NaN Got: %x", math.Float64bits(appender.resultFloats[6].f))
+ got := appTest.ResultSamples()
+ // 1 successfully scraped sample
+ // 1 stale marker after first fail
+ // 3x 5 report samples for each scrape successful or not.
+ require.Len(t, got, 17, "Appended samples not as expected:\n%s", appTest)
+ require.Equal(t, 42.0, got[0].V, "Appended first sample not as expected")
+ require.True(t, value.IsStaleNaN(got[6].V),
+ "Appended second sample not as expected. Wanted: stale NaN Got: %x", math.Float64bits(got[6].V))
}
-// If we have a target with sample_limit set and scrape initially works but then we hit the sample_limit error,
+// 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) {
- appender := &collectResultAppender{}
- var (
- signal = make(chan struct{}, 1)
- scraper = &testScraper{}
- app = func(_ context.Context) storage.Appender { return appender }
- numScrapes = 0
- )
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopRunCreatesStaleMarkersOnSampleLimit(t, appV2)
+ })
+}
- ctx, cancel := context.WithCancel(context.Background())
- // Since we're writing samples directly below we need to provide a protocol fallback.
- sl := newBasicScrapeLoopWithFallback(t, ctx, scraper, app, 10*time.Millisecond, "text/plain")
- sl.sampleLimit = 4
+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, 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.sampleLimit = 4
+ })
// Succeed once, several failures, then stop.
+ numScrapes := 0
scraper.scrapeFunc = func(_ context.Context, w io.Writer) error {
numScrapes++
switch numScrapes {
case 1:
- w.Write([]byte("metric_a 10\nmetric_b 10\nmetric_c 10\nmetric_d 10\n"))
+ _, _ = w.Write([]byte("metric_a 10\nmetric_b 10\nmetric_c 10\nmetric_d 10\n"))
return nil
case 2:
- w.Write([]byte("metric_a 20\nmetric_b 20\nmetric_c 20\nmetric_d 20\nmetric_e 999\n"))
+ _, _ = w.Write([]byte("metric_a 20\nmetric_b 20\nmetric_c 20\nmetric_d 20\nmetric_e 999\n"))
return nil
case 3:
- w.Write([]byte("metric_a 30\nmetric_b 30\nmetric_c 30\nmetric_d 30\n"))
+ _, _ = w.Write([]byte("metric_a 30\nmetric_b 30\nmetric_c 30\nmetric_d 30\n"))
return nil
case 4:
cancel()
@@ -1777,49 +2162,56 @@ func TestScrapeLoopRunCreatesStaleMarkersOnSampleLimit(t *testing.T) {
require.FailNow(t, "Scrape wasn't stopped.")
}
+ got := appTest.ResultSamples()
+
// 4 scrapes in total:
// #1 - success - 4 samples appended + 5 report series
// #2 - sample_limit exceeded - no samples appended, only 5 report series
// #3 - success - 4 samples appended + 5 report series
// #4 - scrape canceled - 4 StaleNaNs appended because of scrape error + 5 report series
- require.Len(t, appender.resultFloats, (4+5)+5+(4+5)+(4+5), "Appended samples not as expected:\n%s", appender)
+ require.Len(t, got, (4+5)+5+(4+5)+(4+5), "Appended samples not as expected:\n%s", appTest)
// Expect first 4 samples to be metric_X [0-3].
for i := range 4 {
- require.Equal(t, 10.0, appender.resultFloats[i].f, "Appended %d sample not as expected", i)
+ require.Equal(t, 10.0, got[i].V, "Appended %d sample not as expected", i)
}
// Next 5 samples are report series [4-8].
// Next 5 samples are report series for the second scrape [9-13].
// Expect first 4 samples to be metric_X from the third scrape [14-17].
for i := 14; i <= 17; i++ {
- require.Equal(t, 30.0, appender.resultFloats[i].f, "Appended %d sample not as expected", i)
+ require.Equal(t, 30.0, got[i].V, "Appended %d sample not as expected", i)
}
// Next 5 samples are report series [18-22].
// Next 5 samples are report series [23-26].
for i := 23; i <= 26; i++ {
- require.True(t, value.IsStaleNaN(appender.resultFloats[i].f),
- "Appended second sample not as expected. Wanted: stale NaN Got: %x", math.Float64bits(appender.resultFloats[i].f))
+ require.True(t, value.IsStaleNaN(got[i].V),
+ "Appended second sample not as expected. Wanted: stale NaN Got: %x", math.Float64bits(got[i].V))
}
}
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)
- defer s.Close()
- appender := &collectResultAppender{}
- var (
- signal = make(chan struct{}, 1)
- scraper = &testScraper{}
- app = func(ctx context.Context) storage.Appender { appender.next = s.Appender(ctx); return appender }
- )
+ signal := make(chan struct{}, 1)
- ctx, cancel := context.WithCancel(context.Background())
- // Decreasing the scrape interval could make the test fail, as multiple scrapes might be initiated at identical millisecond timestamps.
- // See https://github.com/prometheus/prometheus/issues/12727.
- // Since we're writing samples directly below we need to provide a protocol fallback.
- sl := newBasicScrapeLoopWithFallback(t, ctx, scraper, app, 100*time.Millisecond, "text/plain")
+ ctx, cancel := context.WithCancel(t.Context())
+ appTest := teststorage.NewAppendable().Then(s)
+ sl, scraper := newTestScrapeLoop(t, withAppendable(appTest, appV2), func(sl *scrapeLoop) {
+ sl.ctx = ctx
+ sl.l = promslog.New(&promslog.Config{})
+ // 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.
+ // See https://github.com/prometheus/prometheus/issues/12727.
+ sl.interval = 100 * time.Millisecond
+ })
numScrapes := 0
-
scraper.scrapeFunc = func(_ context.Context, w io.Writer) error {
switch numScrapes {
case 1, 2:
@@ -1837,10 +2229,10 @@ func TestScrapeLoopCache(t *testing.T) {
numScrapes++
switch numScrapes {
case 1:
- w.Write([]byte("metric_a 42\nmetric_b 43\n"))
+ _, _ = w.Write([]byte("metric_a 42\nmetric_b 43\n"))
return nil
case 3:
- w.Write([]byte("metric_a 44\n"))
+ _, _ = w.Write([]byte("metric_a 44\n"))
return nil
case 4:
cancel()
@@ -1859,29 +2251,28 @@ func TestScrapeLoopCache(t *testing.T) {
require.FailNow(t, "Scrape wasn't stopped.")
}
- // 1 successfully scraped sample, 1 stale marker after first fail, 5 report samples for
- // each scrape successful or not.
- require.Len(t, appender.resultFloats, 26, "Appended samples not as expected:\n%s", appender)
+ // 3 successfully scraped samples
+ // 3 stale marker after samples were missing.
+ // 4x 5 report samples for each scrape successful or not.
+ require.Len(t, appTest.ResultSamples(), 26, "Appended samples not as expected:\n%s", appTest)
}
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)
- defer s.Close()
- sapp := s.Appender(context.Background())
-
- appender := &collectResultAppender{next: sapp}
- var (
- signal = make(chan struct{}, 1)
- scraper = &testScraper{}
- app = func(context.Context) storage.Appender { return appender }
- )
-
- ctx, cancel := context.WithCancel(context.Background())
- sl := newBasicScrapeLoop(t, ctx, scraper, app, 10*time.Millisecond)
+ signal := make(chan struct{}, 1)
+ ctx, cancel := context.WithCancel(t.Context())
+ sl, scraper := newTestScrapeLoop(t, withAppendable(teststorage.NewAppendable().Then(s), appV2), func(sl *scrapeLoop) {
+ sl.ctx = ctx
+ })
numScrapes := 0
-
scraper.scrapeFunc = func(_ context.Context, w io.Writer) error {
numScrapes++
if numScrapes < 5 {
@@ -1889,7 +2280,7 @@ func TestScrapeLoopCacheMemoryExhaustionProtection(t *testing.T) {
for i := range 500 {
s = fmt.Sprintf("%smetric_%d_%d 42\n", s, i, numScrapes)
}
- w.Write([]byte(s + "&"))
+ _, _ = w.Write([]byte(s + "&"))
} else {
cancel()
}
@@ -1910,138 +2301,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 {
- app := &collectResultAppender{}
-
- discoveryLabels := &Target{
- labels: labels.FromStrings(test.discoveryLabels...),
- }
-
- sl := newBasicScrapeLoop(t, context.Background(), nil, func(context.Context) storage.Appender { return app }, 0)
- 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()
-
- slApp := sl.appender(context.Background())
- _, _, _, err := sl.append(slApp, []byte(test.scrapeLabels), "text/plain", now)
- require.NoError(t, err)
- require.NoError(t, slApp.Commit())
-
- expected := []floatSample{
- {
- metric: test.expLset,
- t: timestamp.FromTime(now),
- f: test.expValue,
- },
- }
-
- t.Logf("Test:%s", test.title)
- requireEqual(t, expected, app.resultFloats)
- }
+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(equalFloatSamples),
- cmp.AllowUnexported(histogramSample{}),
- // 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 floatSample) int {
- return labels.Compare(a.metric, b.metric)
- }),
+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{
@@ -2049,7 +2426,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{
@@ -2062,36 +2440,41 @@ func TestScrapeLoopAppendForConflictingPrefixedLabels(t *testing.T) {
"foo", "0",
},
},
- }
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ appTest := teststorage.NewAppendable()
+ 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)
+ }
+ })
- for name, tc := range testcases {
- t.Run(name, func(t *testing.T) {
- app := &collectResultAppender{}
- sl := newBasicScrapeLoop(t, context.Background(), nil, func(context.Context) storage.Appender { return app }, 0)
- sl.sampleMutator = func(l labels.Labels) labels.Labels {
- return mutateSampleLabels(l, &Target{labels: labels.FromStrings(tc.targetLabels...)}, false, nil)
- }
- slApp := sl.appender(context.Background())
- _, _, _, err := sl.append(slApp, []byte(tc.exposedLabels), "text/plain", time.Date(2000, 1, 1, 1, 0, 0, 0, time.UTC))
+ app := sl.appender()
+ _, _, _, err := app.append([]byte(tc.exposedLabels), "text/plain", time.Date(2000, 1, 1, 1, 0, 0, 0, time.UTC))
require.NoError(t, err)
- require.NoError(t, slApp.Commit())
+ require.NoError(t, app.Commit())
- requireEqual(t, []floatSample{
+ teststorage.RequireEqual(t, []sample{
{
- metric: labels.FromStrings(tc.expected...),
- t: timestamp.FromTime(time.Date(2000, 1, 1, 1, 0, 0, 0, time.UTC)),
- f: 0,
+ L: labels.FromStrings(tc.expected...),
+ T: timestamp.FromTime(time.Date(2000, 1, 1, 1, 0, 0, 0, time.UTC)),
+ V: 0,
},
- }, app.resultFloats)
+ }, appTest.ResultSamples())
})
}
}
func TestScrapeLoopAppendCacheEntryButErrNotFound(t *testing.T) {
- // collectResultAppender's AddFast always returns ErrNotFound if we don't give it a next.
- app := &collectResultAppender{}
- sl := newBasicScrapeLoop(t, context.Background(), nil, func(context.Context) storage.Appender { return app }, 0)
+ 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, appV2))
fakeRef := storage.SeriesRef(1)
expValue := float64(1)
@@ -2101,7 +2484,8 @@ func TestScrapeLoopAppendCacheEntryButErrNotFound(t *testing.T) {
require.NoError(t, warning)
var lset labels.Labels
- p.Next()
+ _, err := p.Next()
+ require.NoError(t, err)
p.Labels(&lset)
hash := lset.Hash()
@@ -2109,36 +2493,56 @@ func TestScrapeLoopAppendCacheEntryButErrNotFound(t *testing.T) {
sl.cache.addRef(metric, fakeRef, lset, hash)
now := time.Now()
- slApp := sl.appender(context.Background())
- _, _, _, err := sl.append(slApp, metric, "text/plain", now)
+ app := sl.appender()
+ _, _, _, err = app.append(metric, "text/plain", now)
require.NoError(t, err)
- require.NoError(t, slApp.Commit())
+ require.NoError(t, app.Commit())
- expected := []floatSample{
+ expected := []sample{
{
- metric: lset,
- t: timestamp.FromTime(now),
- f: expValue,
+ L: lset,
+ T: timestamp.FromTime(now),
+ V: expValue,
},
}
-
- require.Equal(t, expected, app.resultFloats)
+ teststorage.RequireEqual(t, expected, appTest.ResultSamples())
}
+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) {
- resApp := &collectResultAppender{}
- app := &limitAppender{Appender: resApp, limit: 1}
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopAppendSampleLimit(t, appV2)
+ })
+}
- sl := newBasicScrapeLoop(t, context.Background(), nil, func(context.Context) storage.Appender { return app }, 0)
- sl.sampleMutator = func(l labels.Labels) labels.Labels {
- if l.Has("deleteme") {
- return labels.EmptyLabels()
+func testScrapeLoopAppendSampleLimit(t *testing.T, appV2 bool) {
+ appTest := teststorage.NewAppendable()
+ sl, _ := newTestScrapeLoop(t, func(sl *scrapeLoop) {
+ 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}
+ })
}
- return l
- }
- sl.sampleLimit = app.limit
- // Get the value of the Counter before performing the append.
+ sl.sampleMutator = func(l labels.Labels) labels.Labels {
+ if l.Has("deleteme") {
+ return labels.EmptyLabels()
+ }
+ return l
+ }
+ sl.sampleLimit = 1 // Same as limitAppender.limit
+ })
+
+ // Get the value of the Counter before performing append.
beforeMetric := dto.Metric{}
err := sl.metrics.targetScrapeSampleLimit.Write(&beforeMetric)
require.NoError(t, err)
@@ -2146,10 +2550,10 @@ func TestScrapeLoopAppendSampleLimit(t *testing.T) {
beforeMetricValue := beforeMetric.GetCounter().GetValue()
now := time.Now()
- slApp := sl.appender(context.Background())
- total, added, seriesAdded, err := sl.append(app, []byte("metric_a 1\nmetric_b 1\nmetric_c 1\n"), "text/plain", now)
+ app := sl.appender()
+ total, added, seriesAdded, err := app.append([]byte("metric_a 1\nmetric_b 1\nmetric_c 1\n"), "text/plain", now)
require.ErrorIs(t, err, errSampleLimit)
- require.NoError(t, slApp.Rollback())
+ require.NoError(t, app.Rollback())
require.Equal(t, 3, total)
require.Equal(t, 3, added)
require.Equal(t, 1, seriesAdded)
@@ -2160,42 +2564,57 @@ func TestScrapeLoopAppendSampleLimit(t *testing.T) {
err = sl.metrics.targetScrapeSampleLimit.Write(&metric)
require.NoError(t, err)
- value := metric.GetCounter().GetValue()
- change := value - beforeMetricValue
+ v := metric.GetCounter().GetValue()
+ change := v - beforeMetricValue
require.Equal(t, 1.0, change, "Unexpected change of sample limit metric: %f", change)
// And verify that we got the samples that fit under the limit.
- want := []floatSample{
+ want := []sample{
{
- metric: labels.FromStrings(model.MetricNameLabel, "metric_a"),
- t: timestamp.FromTime(now),
- f: 1,
+ L: labels.FromStrings(model.MetricNameLabel, "metric_a"),
+ T: timestamp.FromTime(now),
+ V: 1,
},
}
- requireEqual(t, want, resApp.rolledbackFloats, "Appended samples not as expected:\n%s", appender)
+ teststorage.RequireEqual(t, want, appTest.RolledbackSamples(), "Appended samples not as expected:\n%s", appTest)
now = time.Now()
- slApp = sl.appender(context.Background())
- total, added, seriesAdded, err = sl.append(slApp, []byte("metric_a 1\nmetric_b 1\nmetric_c{deleteme=\"yes\"} 1\nmetric_d 1\nmetric_e 1\nmetric_f 1\nmetric_g 1\nmetric_h{deleteme=\"yes\"} 1\nmetric_i{deleteme=\"yes\"} 1\n"), "text/plain", now)
+ app = sl.appender()
+ total, added, seriesAdded, err = app.append([]byte("metric_a 1\nmetric_b 1\nmetric_c{deleteme=\"yes\"} 1\nmetric_d 1\nmetric_e 1\nmetric_f 1\nmetric_g 1\nmetric_h{deleteme=\"yes\"} 1\nmetric_i{deleteme=\"yes\"} 1\n"), "text/plain", now)
require.ErrorIs(t, err, errSampleLimit)
- require.NoError(t, slApp.Rollback())
+ require.NoError(t, app.Rollback())
require.Equal(t, 9, total)
require.Equal(t, 6, added)
- require.Equal(t, 0, seriesAdded)
+ require.Equal(t, 1, seriesAdded)
}
func TestScrapeLoop_HistogramBucketLimit(t *testing.T) {
- resApp := &collectResultAppender{}
- app := &bucketLimitAppender{Appender: resApp, limit: 2}
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopHistogramBucketLimit(t, appV2)
+ })
+}
- sl := newBasicScrapeLoop(t, context.Background(), nil, func(context.Context) storage.Appender { return app }, 0)
- sl.enableNativeHistogramScraping = true
- sl.sampleMutator = func(l labels.Labels) labels.Labels {
- if l.Has("deleteme") {
- return labels.EmptyLabels()
+func testScrapeLoopHistogramBucketLimit(t *testing.T, appV2 bool) {
+ sl, _ := newTestScrapeLoop(t, func(sl *scrapeLoop) {
+ 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}
+ })
}
- return l
- }
+
+ sl.enableNativeHistogramScraping = true
+ sl.sampleMutator = func(l labels.Labels) labels.Labels {
+ if l.Has("deleteme") {
+ return labels.EmptyLabels()
+ }
+ return l
+ }
+ })
+ app := sl.appender()
metric := dto.Metric{}
err := sl.metrics.targetScrapeNativeHistogramBucketLimit.Write(&metric)
@@ -2214,7 +2633,7 @@ func TestScrapeLoop_HistogramBucketLimit(t *testing.T) {
[]string{"size"},
)
registry := prometheus.NewRegistry()
- registry.Register(nativeHistogram)
+ require.NoError(t, registry.Register(nativeHistogram))
nativeHistogram.WithLabelValues("S").Observe(1.0)
nativeHistogram.WithLabelValues("M").Observe(1.0)
nativeHistogram.WithLabelValues("L").Observe(1.0)
@@ -2230,7 +2649,7 @@ func TestScrapeLoop_HistogramBucketLimit(t *testing.T) {
require.NoError(t, err)
now := time.Now()
- total, added, seriesAdded, err := sl.append(app, msg, "application/vnd.google.protobuf", now)
+ total, added, seriesAdded, err := app.append(msg, "application/vnd.google.protobuf", now)
require.NoError(t, err)
require.Equal(t, 3, total)
require.Equal(t, 3, added)
@@ -2253,11 +2672,11 @@ func TestScrapeLoop_HistogramBucketLimit(t *testing.T) {
require.NoError(t, err)
now = time.Now()
- total, added, seriesAdded, err = sl.append(app, msg, "application/vnd.google.protobuf", now)
+ total, added, seriesAdded, err = app.append(msg, "application/vnd.google.protobuf", now)
require.NoError(t, err)
require.Equal(t, 3, total)
require.Equal(t, 3, added)
- require.Equal(t, 3, seriesAdded)
+ require.Equal(t, 0, seriesAdded) // Series are cached.
err = sl.metrics.targetScrapeNativeHistogramBucketLimit.Write(&metric)
require.NoError(t, err)
@@ -2276,14 +2695,14 @@ func TestScrapeLoop_HistogramBucketLimit(t *testing.T) {
require.NoError(t, err)
now = time.Now()
- total, added, seriesAdded, err = sl.append(app, msg, "application/vnd.google.protobuf", now)
+ total, added, seriesAdded, err = app.append(msg, "application/vnd.google.protobuf", now)
if !errors.Is(err, errBucketLimit) {
t.Fatalf("Did not see expected histogram bucket limit error: %s", err)
}
require.NoError(t, app.Rollback())
require.Equal(t, 3, total)
require.Equal(t, 3, added)
- require.Equal(t, 0, seriesAdded)
+ require.Equal(t, 0, seriesAdded) // Series are cached.
err = sl.metrics.targetScrapeNativeHistogramBucketLimit.Write(&metric)
require.NoError(t, err)
@@ -2292,174 +2711,221 @@ func TestScrapeLoop_HistogramBucketLimit(t *testing.T) {
}
func TestScrapeLoop_ChangingMetricString(t *testing.T) {
- // 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.
- s := teststorage.New(t)
- defer s.Close()
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopChangingMetricString(t, appV2)
+ })
+}
- capp := &collectResultAppender{}
- sl := newBasicScrapeLoop(t, context.Background(), nil, func(context.Context) storage.Appender { return capp }, 0)
+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, appV2))
now := time.Now()
- slApp := sl.appender(context.Background())
- _, _, _, err := sl.append(slApp, []byte(`metric_a{a="1",b="1"} 1`), "text/plain", now)
+ app := sl.appender()
+ _, _, _, err := app.append([]byte(`metric_a{a="1",b="1"} 1`), "text/plain", now)
require.NoError(t, err)
- require.NoError(t, slApp.Commit())
+ require.NoError(t, app.Commit())
- slApp = sl.appender(context.Background())
- _, _, _, err = sl.append(slApp, []byte(`metric_a{b="1",a="1"} 2`), "text/plain", now.Add(time.Minute))
+ app = sl.appender()
+ _, _, _, err = app.append([]byte(`metric_a{b="1",a="1"} 2`), "text/plain", now.Add(time.Minute))
require.NoError(t, err)
- require.NoError(t, slApp.Commit())
+ require.NoError(t, app.Commit())
- want := []floatSample{
+ want := []sample{
{
- metric: labels.FromStrings("__name__", "metric_a", "a", "1", "b", "1"),
- t: timestamp.FromTime(now),
- f: 1,
+ L: labels.FromStrings("__name__", "metric_a", "a", "1", "b", "1"),
+ T: timestamp.FromTime(now),
+ V: 1,
},
{
- metric: labels.FromStrings("__name__", "metric_a", "a", "1", "b", "1"),
- t: timestamp.FromTime(now.Add(time.Minute)),
- f: 2,
+ L: labels.FromStrings("__name__", "metric_a", "a", "1", "b", "1"),
+ T: timestamp.FromTime(now.Add(time.Minute)),
+ V: 2,
},
}
- require.Equal(t, want, capp.resultFloats, "Appended samples not as expected:\n%s", appender)
+ teststorage.RequireEqual(t, want, appTest.ResultSamples(), "Appended samples not as expected:\n%s", appTest)
}
func TestScrapeLoopAppendFailsWithNoContentType(t *testing.T) {
- app := &collectResultAppender{}
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopAppendFailsWithNoContentType(t, appV2)
+ })
+}
- // Explicitly setting the lack of fallback protocol here to make it obvious.
- sl := newBasicScrapeLoopWithFallback(t, context.Background(), nil, func(context.Context) storage.Appender { return app }, 0, "")
+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 = ""
+ })
now := time.Now()
- slApp := sl.appender(context.Background())
- _, _, _, err := sl.append(slApp, []byte("metric_a 1\n"), "", now)
- // We expect the appropriate error.
+ app := sl.appender()
+ _, _, _, err := app.append([]byte("metric_a 1\n"), "", now)
+ // We expected the appropriate error.
require.ErrorContains(t, err, "non-compliant scrape target sending blank Content-Type and no fallback_scrape_protocol specified for target", "Expected \"non-compliant scrape\" error but got: %s", err)
}
+// 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) {
- // This test ensures we there are no errors when we get a blank scrape or just want to append a stale marker.
- app := &collectResultAppender{}
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopAppendEmptyWithNoContentType(t, appV2)
+ })
+}
- // Explicitly setting the lack of fallback protocol here to make it obvious.
- sl := newBasicScrapeLoopWithFallback(t, context.Background(), nil, func(context.Context) storage.Appender { return app }, 0, "")
+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 = ""
+ })
now := time.Now()
- slApp := sl.appender(context.Background())
- _, _, _, err := sl.append(slApp, []byte(""), "", now)
+ app := sl.appender()
+ _, _, _, err := app.append([]byte(""), "", now)
require.NoError(t, err)
- require.NoError(t, slApp.Commit())
+ require.NoError(t, app.Commit())
}
func TestScrapeLoopAppendStaleness(t *testing.T) {
- app := &collectResultAppender{}
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopAppendStaleness(t, appV2)
+ })
+}
- sl := newBasicScrapeLoop(t, context.Background(), nil, func(context.Context) storage.Appender { return app }, 0)
+func testScrapeLoopAppendStaleness(t *testing.T, appV2 bool) {
+ appTest := teststorage.NewAppendable()
+ sl, _ := newTestScrapeLoop(t, withAppendable(appTest, appV2))
now := time.Now()
- slApp := sl.appender(context.Background())
- _, _, _, err := sl.append(slApp, []byte("metric_a 1\n"), "text/plain", now)
+ app := sl.appender()
+ _, _, _, err := app.append([]byte("metric_a 1\n"), "text/plain", now)
require.NoError(t, err)
- require.NoError(t, slApp.Commit())
+ require.NoError(t, app.Commit())
- slApp = sl.appender(context.Background())
- _, _, _, err = sl.append(slApp, []byte(""), "", now.Add(time.Second))
+ app = sl.appender()
+ _, _, _, err = app.append([]byte(""), "", now.Add(time.Second))
require.NoError(t, err)
- require.NoError(t, slApp.Commit())
+ require.NoError(t, app.Commit())
- want := []floatSample{
+ want := []sample{
{
- metric: labels.FromStrings(model.MetricNameLabel, "metric_a"),
- t: timestamp.FromTime(now),
- f: 1,
+ L: labels.FromStrings(model.MetricNameLabel, "metric_a"),
+ T: timestamp.FromTime(now),
+ V: 1,
},
{
- metric: labels.FromStrings(model.MetricNameLabel, "metric_a"),
- t: timestamp.FromTime(now.Add(time.Second)),
- f: math.Float64frombits(value.StaleNaN),
+ L: labels.FromStrings(model.MetricNameLabel, "metric_a"),
+ T: timestamp.FromTime(now.Add(time.Second)),
+ V: math.Float64frombits(value.StaleNaN),
},
}
- requireEqual(t, want, app.resultFloats, "Appended samples not as expected:\n%s", appender)
+ teststorage.RequireEqual(t, want, appTest.ResultSamples(), "Appended samples not as expected:\n%s", appTest)
}
func TestScrapeLoopAppendNoStalenessIfTimestamp(t *testing.T) {
- app := &collectResultAppender{}
- sl := newBasicScrapeLoop(t, context.Background(), nil, func(context.Context) storage.Appender { return app }, 0)
+ 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, appV2))
now := time.Now()
- slApp := sl.appender(context.Background())
- _, _, _, err := sl.append(slApp, []byte("metric_a 1 1000\n"), "text/plain", now)
+ app := sl.appender()
+ _, _, _, err := app.append([]byte("metric_a 1 1000\n"), "text/plain", now)
require.NoError(t, err)
- require.NoError(t, slApp.Commit())
+ require.NoError(t, app.Commit())
- slApp = sl.appender(context.Background())
- _, _, _, err = sl.append(slApp, []byte(""), "", now.Add(time.Second))
+ app = sl.appender()
+ _, _, _, err = app.append([]byte(""), "", now.Add(time.Second))
require.NoError(t, err)
- require.NoError(t, slApp.Commit())
+ require.NoError(t, app.Commit())
- want := []floatSample{
+ want := []sample{
{
- metric: labels.FromStrings(model.MetricNameLabel, "metric_a"),
- t: 1000,
- f: 1,
+ L: labels.FromStrings(model.MetricNameLabel, "metric_a"),
+ T: 1000,
+ V: 1,
},
}
- require.Equal(t, want, app.resultFloats, "Appended samples not as expected:\n%s", appender)
+ teststorage.RequireEqual(t, want, appTest.ResultSamples(), "Appended samples not as expected:\n%s", appTest)
}
func TestScrapeLoopAppendStalenessIfTrackTimestampStaleness(t *testing.T) {
- app := &collectResultAppender{}
- sl := newBasicScrapeLoop(t, context.Background(), nil, func(context.Context) storage.Appender { return app }, 0)
- sl.trackTimestampsStaleness = true
-
- now := time.Now()
- slApp := sl.appender(context.Background())
- _, _, _, err := sl.append(slApp, []byte("metric_a 1 1000\n"), "text/plain", now)
- require.NoError(t, err)
- require.NoError(t, slApp.Commit())
-
- slApp = sl.appender(context.Background())
- _, _, _, err = sl.append(slApp, []byte(""), "", now.Add(time.Second))
- require.NoError(t, err)
- require.NoError(t, slApp.Commit())
-
- want := []floatSample{
- {
- metric: labels.FromStrings(model.MetricNameLabel, "metric_a"),
- t: 1000,
- f: 1,
- },
- {
- metric: labels.FromStrings(model.MetricNameLabel, "metric_a"),
- t: timestamp.FromTime(now.Add(time.Second)),
- f: math.Float64frombits(value.StaleNaN),
- },
- }
- requireEqual(t, want, app.resultFloats, "Appended samples not as expected:\n%s", appender)
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopAppendStalenessIfTrackTimestampStaleness(t, appV2)
+ })
}
-func TestScrapeLoopAppendExemplar(t *testing.T) {
- tests := []struct {
+func testScrapeLoopAppendStalenessIfTrackTimestampStaleness(t *testing.T, appV2 bool) {
+ appTest := teststorage.NewAppendable()
+ sl, _ := newTestScrapeLoop(t, withAppendable(appTest, appV2), func(sl *scrapeLoop) {
+ sl.trackTimestampsStaleness = true
+ })
+
+ now := time.Now()
+ app := sl.appender()
+ _, _, _, err := app.append([]byte("metric_a 1 1000\n"), "text/plain", now)
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+
+ app = sl.appender()
+ _, _, _, err = app.append([]byte(""), "", now.Add(time.Second))
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+
+ want := []sample{
+ {
+ L: labels.FromStrings(model.MetricNameLabel, "metric_a"),
+ T: 1000,
+ V: 1,
+ },
+ {
+ L: labels.FromStrings(model.MetricNameLabel, "metric_a"),
+ T: timestamp.FromTime(now.Add(time.Second)),
+ V: math.Float64frombits(value.StaleNaN),
+ },
+ }
+ teststorage.RequireEqual(t, want, appTest.ResultSamples(), "Appended samples not as expected:\n%s", appTest)
+}
+
+// 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
scrapeText string
contentType string
discoveryLabels []string
- floats []floatSample
- histograms []histogramSample
- exemplars []exemplar.Exemplar
+ 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",
contentType: "application/openmetrics-text",
discoveryLabels: []string{"n", "2"},
- floats: []floatSample{{
- metric: labels.FromStrings("__name__", "metric_total", "exported_n", "1", "n", "2"),
- f: 0,
+ samples: []sample{{
+ L: labels.FromStrings("__name__", "metric_total", "exported_n", "1", "n", "2"),
+ V: 0,
}},
},
{
@@ -2467,26 +2933,24 @@ func TestScrapeLoopAppendExemplar(t *testing.T) {
scrapeText: "metric_total{n=\"1\"} 0 # {a=\"abc\"} 1.0\n# EOF",
contentType: "application/openmetrics-text",
discoveryLabels: []string{"n", "2"},
- floats: []floatSample{{
- metric: labels.FromStrings("__name__", "metric_total", "exported_n", "1", "n", "2"),
- f: 0,
+ samples: []sample{{
+ L: labels.FromStrings("__name__", "metric_total", "exported_n", "1", "n", "2"),
+ V: 0,
+ ES: []exemplar.Exemplar{
+ {Labels: labels.FromStrings("a", "abc"), Value: 1},
+ },
}},
- exemplars: []exemplar.Exemplar{
- {Labels: labels.FromStrings("a", "abc"), Value: 1},
- },
},
{
title: "Metric with exemplars and TS",
scrapeText: "metric_total{n=\"1\"} 0 # {a=\"abc\"} 1.0 10000\n# EOF",
contentType: "application/openmetrics-text",
discoveryLabels: []string{"n", "2"},
- floats: []floatSample{{
- metric: labels.FromStrings("__name__", "metric_total", "exported_n", "1", "n", "2"),
- f: 0,
+ samples: []sample{{
+ L: labels.FromStrings("__name__", "metric_total", "exported_n", "1", "n", "2"),
+ V: 0,
+ ES: []exemplar.Exemplar{{Labels: labels.FromStrings("a", "abc"), Value: 1, Ts: 10000000, HasTs: true}},
}},
- exemplars: []exemplar.Exemplar{
- {Labels: labels.FromStrings("a", "abc"), Value: 1, Ts: 10000000, HasTs: true},
- },
},
{
title: "Two metrics and exemplars",
@@ -2494,17 +2958,15 @@ func TestScrapeLoopAppendExemplar(t *testing.T) {
metric_total{n="2"} 2 # {t="2"} 2.0 20000
# EOF`,
contentType: "application/openmetrics-text",
- floats: []floatSample{{
- metric: labels.FromStrings("__name__", "metric_total", "n", "1"),
- f: 1,
+ samples: []sample{{
+ L: labels.FromStrings("__name__", "metric_total", "n", "1"),
+ V: 1,
+ ES: []exemplar.Exemplar{{Labels: labels.FromStrings("t", "1"), Value: 1, Ts: 10000000, HasTs: true}},
}, {
- metric: labels.FromStrings("__name__", "metric_total", "n", "2"),
- f: 2,
+ L: labels.FromStrings("__name__", "metric_total", "n", "2"),
+ V: 2,
+ ES: []exemplar.Exemplar{{Labels: labels.FromStrings("t", "2"), Value: 2, Ts: 20000000, HasTs: true}},
}},
- exemplars: []exemplar.Exemplar{
- {Labels: labels.FromStrings("t", "1"), Value: 1, Ts: 10000000, HasTs: true},
- {Labels: labels.FromStrings("t", "2"), Value: 2, Ts: 20000000, HasTs: true},
- },
},
{
title: "Native histogram with three exemplars from classic buckets",
@@ -2596,10 +3058,10 @@ metric: <
`,
contentType: "application/vnd.google.protobuf",
- histograms: []histogramSample{{
- t: 1234568,
- metric: labels.FromStrings("__name__", "test_histogram"),
- h: &histogram.Histogram{
+ samples: []sample{{
+ T: 1234568,
+ L: labels.FromStrings("__name__", "test_histogram"),
+ H: &histogram.Histogram{
Count: 175,
ZeroCount: 2,
Sum: 0.0008280461746287094,
@@ -2616,12 +3078,12 @@ metric: <
PositiveBuckets: []int64{1, 2, -1, -1},
NegativeBuckets: []int64{1, 3, -2, -1, 1},
},
+ ES: []exemplar.Exemplar{
+ // Native histogram exemplars are arranged by timestamp, and those with missing timestamps are dropped.
+ {Labels: labels.FromStrings("dummyID", "58215"), Value: -0.00019, Ts: 1625851055146, HasTs: true},
+ {Labels: labels.FromStrings("dummyID", "59727"), Value: -0.00039, Ts: 1625851155146, HasTs: true},
+ },
}},
- exemplars: []exemplar.Exemplar{
- // Native histogram exemplars are arranged by timestamp, and those with missing timestamps are dropped.
- {Labels: labels.FromStrings("dummyID", "58215"), Value: -0.00019, Ts: 1625851055146, HasTs: true},
- {Labels: labels.FromStrings("dummyID", "59727"), Value: -0.00039, Ts: 1625851155146, HasTs: true},
- },
},
{
title: "Native histogram with three exemplars scraped as classic histogram",
@@ -2714,46 +3176,50 @@ metric: <
`,
alwaysScrapeClassicHist: true,
contentType: "application/vnd.google.protobuf",
- floats: []floatSample{
- {metric: labels.FromStrings("__name__", "test_histogram_count"), t: 1234568, f: 175},
- {metric: labels.FromStrings("__name__", "test_histogram_sum"), t: 1234568, f: 0.0008280461746287094},
- {metric: labels.FromStrings("__name__", "test_histogram_bucket", "le", "-0.0004899999999999998"), t: 1234568, f: 2},
- {metric: labels.FromStrings("__name__", "test_histogram_bucket", "le", "-0.0003899999999999998"), t: 1234568, f: 4},
- {metric: labels.FromStrings("__name__", "test_histogram_bucket", "le", "-0.0002899999999999998"), t: 1234568, f: 16},
- {metric: labels.FromStrings("__name__", "test_histogram_bucket", "le", "-0.0001899999999999998"), t: 1234568, f: 32},
- {metric: labels.FromStrings("__name__", "test_histogram_bucket", "le", "+Inf"), t: 1234568, f: 175},
- },
- histograms: []histogramSample{{
- t: 1234568,
- metric: labels.FromStrings("__name__", "test_histogram"),
- h: &histogram.Histogram{
- Count: 175,
- ZeroCount: 2,
- Sum: 0.0008280461746287094,
- ZeroThreshold: 2.938735877055719e-39,
- Schema: 3,
- PositiveSpans: []histogram.Span{
- {Offset: -161, Length: 1},
- {Offset: 8, Length: 3},
+ samples: []sample{
+ {
+ T: 1234568,
+ L: labels.FromStrings("__name__", "test_histogram"),
+ H: &histogram.Histogram{
+ Count: 175,
+ ZeroCount: 2,
+ Sum: 0.0008280461746287094,
+ ZeroThreshold: 2.938735877055719e-39,
+ Schema: 3,
+ PositiveSpans: []histogram.Span{
+ {Offset: -161, Length: 1},
+ {Offset: 8, Length: 3},
+ },
+ NegativeSpans: []histogram.Span{
+ {Offset: -162, Length: 1},
+ {Offset: 23, Length: 4},
+ },
+ PositiveBuckets: []int64{1, 2, -1, -1},
+ NegativeBuckets: []int64{1, 3, -2, -1, 1},
},
- NegativeSpans: []histogram.Span{
- {Offset: -162, Length: 1},
- {Offset: 23, Length: 4},
+ ES: []exemplar.Exemplar{
+ // Native histogram one is arranged by timestamp.
+ // Exemplars with missing timestamps are dropped for native histograms.
+ {Labels: labels.FromStrings("dummyID", "58215"), Value: -0.00019, Ts: 1625851055146, HasTs: true},
+ {Labels: labels.FromStrings("dummyID", "59727"), Value: -0.00039, Ts: 1625851155146, HasTs: true},
},
- PositiveBuckets: []int64{1, 2, -1, -1},
- NegativeBuckets: []int64{1, 3, -2, -1, 1},
},
- }},
- exemplars: []exemplar.Exemplar{
- // Native histogram one is arranged by timestamp.
- // Exemplars with missing timestamps are dropped for native histograms.
- {Labels: labels.FromStrings("dummyID", "58215"), Value: -0.00019, Ts: 1625851055146, HasTs: true},
- {Labels: labels.FromStrings("dummyID", "59727"), Value: -0.00039, Ts: 1625851155146, HasTs: true},
- // Classic histogram one is in order of appearance.
- // Exemplars with missing timestamps are supported for classic histograms.
- {Labels: labels.FromStrings("dummyID", "59727"), Value: -0.00039, Ts: 1625851155146, HasTs: true},
- {Labels: labels.FromStrings("dummyID", "5617"), Value: -0.00029, Ts: 1234568, HasTs: false},
- {Labels: labels.FromStrings("dummyID", "58215"), Value: -0.00019, Ts: 1625851055146, 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},
},
},
{
@@ -2829,10 +3295,10 @@ metric: <
>
`,
- histograms: []histogramSample{{
- t: 1234568,
- metric: labels.FromStrings("__name__", "test_histogram"),
- h: &histogram.Histogram{
+ samples: []sample{{
+ T: 1234568,
+ L: labels.FromStrings("__name__", "test_histogram"),
+ H: &histogram.Histogram{
Count: 175,
ZeroCount: 2,
Sum: 0.0008280461746287094,
@@ -2849,12 +3315,12 @@ metric: <
PositiveBuckets: []int64{1, 2, -1, -1},
NegativeBuckets: []int64{1, 3, -2, -1, 1},
},
+ ES: []exemplar.Exemplar{
+ // Exemplars with missing timestamps are dropped for native histograms.
+ {Labels: labels.FromStrings("dummyID", "58242"), Value: -0.00019, Ts: 1625851055146, HasTs: true},
+ {Labels: labels.FromStrings("dummyID", "59732"), Value: -0.00039, Ts: 1625851155146, HasTs: true},
+ },
}},
- exemplars: []exemplar.Exemplar{
- // Exemplars with missing timestamps are dropped for native histograms.
- {Labels: labels.FromStrings("dummyID", "58242"), Value: -0.00019, Ts: 1625851055146, HasTs: true},
- {Labels: labels.FromStrings("dummyID", "59732"), Value: -0.00039, Ts: 1625851155146, HasTs: true},
- },
},
{
title: "Native histogram with exemplars but ingestion disabled",
@@ -2929,45 +3395,55 @@ metric: <
>
`,
- floats: []floatSample{
- {metric: labels.FromStrings("__name__", "test_histogram_count"), t: 1234568, f: 175},
- {metric: labels.FromStrings("__name__", "test_histogram_sum"), t: 1234568, f: 0.0008280461746287094},
- {metric: labels.FromStrings("__name__", "test_histogram_bucket", "le", "+Inf"), t: 1234568, f: 175},
+ 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", "+Inf"), T: 1234568, V: 175},
},
},
- }
-
- for _, test := range tests {
+ } {
t.Run(test.title, func(t *testing.T) {
- app := &collectResultAppender{}
-
discoveryLabels := &Target{
labels: labels.FromStrings(test.discoveryLabels...),
}
- sl := newBasicScrapeLoop(t, context.Background(), nil, func(context.Context) storage.Appender { return app }, 0)
- sl.enableNativeHistogramScraping = test.enableNativeHistogramsIngestion
- sl.sampleMutator = func(l labels.Labels) labels.Labels {
- return mutateSampleLabels(l, discoveryLabels, false, nil)
- }
- sl.reportSampleMutator = func(l labels.Labels) labels.Labels {
- return mutateReportSampleLabels(l, discoveryLabels)
- }
- sl.alwaysScrapeClassicHist = test.alwaysScrapeClassicHist
+ appTest := teststorage.NewAppendable()
+ 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)
+ }
+ sl.reportSampleMutator = func(l labels.Labels) labels.Labels {
+ 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
+ // expectations.
+ // TODO(bwplotka): Add cases for append metadata to WAL and pass metadata
+ sl.appendMetadataToWAL = false
+ })
+ app := sl.appender()
now := time.Now()
- for i := range test.floats {
- if test.floats[i].t != 0 {
+ // 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
}
- test.floats[i].t = timestamp.FromTime(now)
- }
+ test.samples[i].T = timestamp.FromTime(now)
- // We need to set the timestamp for expected exemplars that does not have a timestamp.
- for i := range test.exemplars {
- if test.exemplars[i].Ts == 0 {
- test.exemplars[i].Ts = timestamp.FromTime(now)
+ // We need to set the timestamp for expected exemplars that does not have a timestamp.
+ for j := range test.samples[i].ES {
+ if test.samples[i].ES[j].Ts == 0 {
+ test.samples[i].ES[j].Ts = timestamp.FromTime(now)
+ }
}
}
@@ -2978,12 +3454,10 @@ metric: <
buf.WriteString(test.scrapeText)
}
- _, _, _, err := sl.append(app, buf.Bytes(), test.contentType, now)
+ _, _, _, err := app.append(buf.Bytes(), test.contentType, now)
require.NoError(t, err)
require.NoError(t, app.Commit())
- requireEqual(t, test.floats, app.resultFloats)
- requireEqual(t, test.histograms, app.resultHistograms)
- requireEqual(t, test.exemplars, app.resultExemplars)
+ teststorage.RequireEqual(t, test.samples, appTest.ResultSamples())
})
}
}
@@ -3009,155 +3483,175 @@ 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`}
- samples := []floatSample{{
- metric: labels.FromStrings("__name__", "metric_total", "n", "1"),
- f: 1,
+ samples := []sample{{
+ L: labels.FromStrings("__name__", "metric_total", "n", "1"),
+ V: 1,
+ ES: []exemplar.Exemplar{
+ {Labels: labels.FromStrings("t", "1"), Value: 1, Ts: 10000000, HasTs: true},
+ },
}, {
- metric: labels.FromStrings("__name__", "metric_total", "n", "1"),
- f: 2,
+ L: labels.FromStrings("__name__", "metric_total", "n", "1"),
+ V: 2,
+ ES: []exemplar.Exemplar{
+ {Labels: labels.FromStrings("t", "2"), Value: 2, Ts: 20000000, HasTs: true},
+ },
}}
- exemplars := []exemplar.Exemplar{
- {Labels: labels.FromStrings("t", "1"), Value: 1, Ts: 10000000, HasTs: true},
- {Labels: labels.FromStrings("t", "2"), Value: 2, Ts: 20000000, HasTs: true},
- }
discoveryLabels := &Target{
labels: labels.FromStrings(),
}
- app := &collectResultAppender{}
-
- sl := newBasicScrapeLoop(t, context.Background(), nil, func(context.Context) storage.Appender { return app }, 0)
- sl.sampleMutator = func(l labels.Labels) labels.Labels {
- return mutateSampleLabels(l, discoveryLabels, false, nil)
- }
- sl.reportSampleMutator = func(l labels.Labels) labels.Labels {
- return mutateReportSampleLabels(l, 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, false, nil)
+ }
+ sl.reportSampleMutator = func(l labels.Labels) labels.Labels {
+ return mutateReportSampleLabels(l, discoveryLabels)
+ }
+ // This test does not care about metadata. Having this true would mean we need to add metadata to sample
+ // expectations.
+ sl.appendMetadataToWAL = false
+ })
now := time.Now()
-
for i := range samples {
ts := now.Add(time.Second * time.Duration(i))
- samples[i].t = timestamp.FromTime(ts)
- }
-
- // We need to set the timestamp for expected exemplars that does not have a timestamp.
- for i := range exemplars {
- if exemplars[i].Ts == 0 {
- ts := now.Add(time.Second * time.Duration(i))
- exemplars[i].Ts = timestamp.FromTime(ts)
- }
+ samples[i].T = timestamp.FromTime(ts)
}
for i, st := range scrapeText {
- _, _, _, err := sl.append(app, []byte(st), "application/openmetrics-text", timestamp.Time(samples[i].t))
+ app := sl.appender()
+ _, _, _, err := app.append([]byte(st), "application/openmetrics-text", timestamp.Time(samples[i].T))
require.NoError(t, err)
require.NoError(t, app.Commit())
}
- requireEqual(t, samples, app.resultFloats)
- requireEqual(t, exemplars, app.resultExemplars)
+ teststorage.RequireEqual(t, samples, appTest.ResultSamples())
}
func TestScrapeLoopRunReportsTargetDownOnScrapeError(t *testing.T) {
- var (
- scraper = &testScraper{}
- appender = &collectResultAppender{}
- app = func(context.Context) storage.Appender { return appender }
- )
-
- ctx, cancel := context.WithCancel(context.Background())
- sl := newBasicScrapeLoop(t, ctx, scraper, app, 10*time.Millisecond)
+ 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, withAppendable(appTest, appV2), func(sl *scrapeLoop) {
+ sl.ctx = ctx
+ })
scraper.scrapeFunc = func(context.Context, io.Writer) error {
cancel()
return errors.New("scrape failed")
}
sl.run(nil)
- require.Equal(t, 0.0, appender.resultFloats[0].f, "bad 'up' value")
+ require.Equal(t, 0.0, appTest.ResultSamples()[0].V, "bad 'up' value")
}
func TestScrapeLoopRunReportsTargetDownOnInvalidUTF8(t *testing.T) {
- var (
- scraper = &testScraper{}
- appender = &collectResultAppender{}
- app = func(context.Context) storage.Appender { return appender }
- )
-
- ctx, cancel := context.WithCancel(context.Background())
- sl := newBasicScrapeLoop(t, ctx, scraper, app, 10*time.Millisecond)
+ 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, withAppendable(appTest, appV2), func(sl *scrapeLoop) {
+ sl.ctx = ctx
+ })
scraper.scrapeFunc = func(_ context.Context, w io.Writer) error {
cancel()
- w.Write([]byte("a{l=\"\xff\"} 1\n"))
+ _, _ = w.Write([]byte("a{l=\"\xff\"} 1\n"))
return nil
}
sl.run(nil)
- require.Equal(t, 0.0, appender.resultFloats[0].f, "bad 'up' value")
-}
-
-type errorAppender struct {
- collectResultAppender
-}
-
-func (app *errorAppender) Append(ref storage.SeriesRef, lset labels.Labels, t int64, v float64) (storage.SeriesRef, error) {
- switch lset.Get(model.MetricNameLabel) {
- case "out_of_order":
- return 0, storage.ErrOutOfOrderSample
- case "amend":
- return 0, storage.ErrDuplicateSampleForTimestamp
- case "out_of_bounds":
- return 0, storage.ErrOutOfBounds
- default:
- return app.collectResultAppender.Append(ref, lset, t, v)
- }
+ require.Equal(t, 0.0, appTest.ResultSamples()[0].V, "bad 'up' value")
}
func TestScrapeLoopAppendGracefullyIfAmendOrOutOfOrderOrOutOfBounds(t *testing.T) {
- app := &errorAppender{}
- sl := newBasicScrapeLoop(t, context.Background(), nil, func(context.Context) storage.Appender { return app }, 0)
+ 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) {
+ case "out_of_order":
+ return storage.ErrOutOfOrderSample
+ case "amend":
+ return storage.ErrDuplicateSampleForTimestamp
+ case "out_of_bounds":
+ return storage.ErrOutOfBounds
+ default:
+ return nil
+ }
+ }, nil, nil)
+ sl, _ := newTestScrapeLoop(t, withAppendable(appTest, appV2))
now := time.Unix(1, 0)
- slApp := sl.appender(context.Background())
- total, added, seriesAdded, err := sl.append(slApp, []byte("out_of_order 1\namend 1\nnormal 1\nout_of_bounds 1\n"), "text/plain", now)
+ app := sl.appender()
+ total, added, seriesAdded, err := app.append([]byte("out_of_order 1\namend 1\nnormal 1\nout_of_bounds 1\n"), "text/plain", now)
require.NoError(t, err)
- require.NoError(t, slApp.Commit())
+ require.NoError(t, app.Commit())
- want := []floatSample{
+ want := []sample{
{
- metric: labels.FromStrings(model.MetricNameLabel, "normal"),
- t: timestamp.FromTime(now),
- f: 1,
+ L: labels.FromStrings(model.MetricNameLabel, "normal"),
+ T: timestamp.FromTime(now),
+ V: 1,
},
}
- requireEqual(t, want, app.resultFloats, "Appended samples not as expected:\n%s", appender)
+ 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) {
- app := &collectResultAppender{}
- sl := newBasicScrapeLoop(t, context.Background(), nil,
- func(context.Context) storage.Appender {
- return &timeLimitAppender{
- Appender: app,
- maxTime: timestamp.FromTime(time.Now().Add(10 * time.Minute)),
- }
- },
- 0,
- )
+ 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)
- slApp := sl.appender(context.Background())
- total, added, seriesAdded, err := sl.append(slApp, []byte("normal 1\n"), "text/plain", now)
+ app := sl.appender()
+ total, added, seriesAdded, err := app.append([]byte("normal 1\n"), "text/plain", now)
require.NoError(t, err)
- require.NoError(t, slApp.Commit())
+ require.NoError(t, app.Commit())
require.Equal(t, 1, total)
require.Equal(t, 1, added)
require.Equal(t, 0, seriesAdded)
@@ -3252,7 +3746,7 @@ func TestRequestTraceparentHeader(t *testing.T) {
resp, err := ts.scrape(context.Background())
require.NoError(t, err)
require.NotNil(t, resp)
- defer resp.Body.Close()
+ t.Cleanup(func() { _ = resp.Body.Close() })
}
func TestTargetScraperScrapeOK(t *testing.T) {
@@ -3281,8 +3775,8 @@ func TestTargetScraperScrapeOK(t *testing.T) {
}
contentTypes := strings.SplitSeq(accept, ",")
- for ct := range contentTypes {
- match := qValuePattern.FindStringSubmatch(ct)
+ for st := range contentTypes {
+ match := qValuePattern.FindStringSubmatch(st)
require.Len(t, match, 3)
qValue, err := strconv.ParseFloat(match[1], 64)
require.NoError(t, err, "Error parsing q value")
@@ -3299,7 +3793,7 @@ func TestTargetScraperScrapeOK(t *testing.T) {
} else {
w.Header().Set("Content-Type", `text/plain; version=0.0.4`)
}
- w.Write([]byte("metric_a 1\nmetric_b 2\n"))
+ _, _ = w.Write([]byte("metric_a 1\nmetric_b 2\n"))
}),
)
defer server.Close()
@@ -3414,9 +3908,9 @@ func TestTargetScrapeScrapeCancel(t *testing.T) {
_, err := ts.scrape(ctx)
switch {
case err == nil:
- errc <- errors.New("Expected error but got nil")
+ errc <- errors.New("expected error but got nil")
case !errors.Is(ctx.Err(), context.Canceled):
- errc <- fmt.Errorf("Expected context cancellation error but got: %w", ctx.Err())
+ errc <- fmt.Errorf("expected context cancellation error but got: %w", ctx.Err())
default:
close(errc)
}
@@ -3476,11 +3970,11 @@ func TestTargetScraperBodySizeLimit(t *testing.T) {
if gzipResponse {
w.Header().Set("Content-Encoding", "gzip")
gw := gzip.NewWriter(w)
- defer gw.Close()
- gw.Write([]byte(responseBody))
+ defer func() { _ = gw.Close() }()
+ _, _ = gw.Write([]byte(responseBody))
return
}
- w.Write([]byte(responseBody))
+ _, _ = w.Write([]byte(responseBody))
}),
)
defer server.Close()
@@ -3573,118 +4067,132 @@ func (ts *testScraper) readResponse(ctx context.Context, _ *http.Response, w io.
}
func TestScrapeLoop_RespectTimestamps(t *testing.T) {
- s := teststorage.New(t)
- defer s.Close()
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopRespectTimestamps(t, appV2)
+ })
+}
- app := s.Appender(context.Background())
- capp := &collectResultAppender{next: app}
- sl := newBasicScrapeLoop(t, context.Background(), nil, func(context.Context) storage.Appender { return capp }, 0)
+func testScrapeLoopRespectTimestamps(t *testing.T, appV2 bool) {
+ s := teststorage.New(t)
+
+ appTest := teststorage.NewAppendable().Then(s)
+ sl, _ := newTestScrapeLoop(t, withAppendable(appTest, appV2))
now := time.Now()
- slApp := sl.appender(context.Background())
- _, _, _, err := sl.append(slApp, []byte(`metric_a{a="1",b="1"} 1 0`), "text/plain", now)
+ app := sl.appender()
+ _, _, _, err := app.append([]byte(`metric_a{a="1",b="1"} 1 0`), "text/plain", now)
require.NoError(t, err)
- require.NoError(t, slApp.Commit())
+ require.NoError(t, app.Commit())
- want := []floatSample{
+ want := []sample{
{
- metric: labels.FromStrings("__name__", "metric_a", "a", "1", "b", "1"),
- t: 0,
- f: 1,
+ L: labels.FromStrings("__name__", "metric_a", "a", "1", "b", "1"),
+ T: 0,
+ V: 1,
},
}
- require.Equal(t, want, capp.resultFloats, "Appended samples not as expected:\n%s", appender)
+ 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)
- defer s.Close()
- app := s.Appender(context.Background())
-
- capp := &collectResultAppender{next: app}
-
- sl := newBasicScrapeLoop(t, context.Background(), nil, func(context.Context) storage.Appender { return capp }, 0)
- sl.honorTimestamps = false
+ appTest := teststorage.NewAppendable().Then(s)
+ sl, _ := newTestScrapeLoop(t, withAppendable(appTest, appV2), func(sl *scrapeLoop) {
+ sl.honorTimestamps = false
+ })
now := time.Now()
- slApp := sl.appender(context.Background())
- _, _, _, err := sl.append(slApp, []byte(`metric_a{a="1",b="1"} 1 0`), "text/plain", now)
+ app := sl.appender()
+ _, _, _, err := app.append([]byte(`metric_a{a="1",b="1"} 1 0`), "text/plain", now)
require.NoError(t, err)
- require.NoError(t, slApp.Commit())
+ require.NoError(t, app.Commit())
- want := []floatSample{
+ want := []sample{
{
- metric: labels.FromStrings("__name__", "metric_a", "a", "1", "b", "1"),
- t: timestamp.FromTime(now),
- f: 1,
+ L: labels.FromStrings("__name__", "metric_a", "a", "1", "b", "1"),
+ T: timestamp.FromTime(now),
+ V: 1,
},
}
- require.Equal(t, want, capp.resultFloats, "Appended samples not as expected:\n%s", appender)
+ teststorage.RequireEqual(t, want, appTest.ResultSamples(), "Appended samples not as expected:\n%s", appTest)
}
func TestScrapeLoopDiscardDuplicateLabels(t *testing.T) {
- s := teststorage.New(t)
- defer s.Close()
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopDiscardDuplicateLabels(t, appV2)
+ })
+}
- ctx, cancel := context.WithCancel(context.Background())
- sl := newBasicScrapeLoop(t, ctx, &testScraper{}, s.Appender, 0)
- defer cancel()
+func testScrapeLoopDiscardDuplicateLabels(t *testing.T, appV2 bool) {
+ s := teststorage.New(t)
+
+ appTest := teststorage.NewAppendable().Then(s)
+ sl, _ := newTestScrapeLoop(t, withAppendable(appTest, appV2))
// We add a good and a bad metric to check that both are discarded.
- slApp := sl.appender(ctx)
- _, _, _, err := sl.append(slApp, []byte("test_metric{le=\"500\"} 1\ntest_metric{le=\"600\",le=\"700\"} 1\n"), "text/plain", time.Time{})
+ app := sl.appender()
+ _, _, _, err := app.append([]byte("test_metric{le=\"500\"} 1\ntest_metric{le=\"600\",le=\"700\"} 1\n"), "text/plain", time.Time{})
require.Error(t, err)
- require.NoError(t, slApp.Rollback())
- // We need to cycle staleness cache maps after a manual rollback. Otherwise they will have old entries in them,
+ require.NoError(t, app.Rollback())
+ // We need to cycle staleness cache maps after a manual rollback. Otherwise, they will have old entries in them,
// which would cause ErrDuplicateSampleForTimestamp errors on the next append.
sl.cache.iterDone(true)
q, err := s.Querier(time.Time{}.UnixNano(), 0)
require.NoError(t, err)
- series := q.Select(ctx, false, nil, labels.MustNewMatcher(labels.MatchRegexp, "__name__", ".*"))
+ series := q.Select(sl.ctx, false, nil, labels.MustNewMatcher(labels.MatchRegexp, "__name__", ".*"))
require.False(t, series.Next(), "series found in tsdb")
require.NoError(t, series.Err())
// We add a good metric to check that it is recorded.
- slApp = sl.appender(ctx)
- _, _, _, err = sl.append(slApp, []byte("test_metric{le=\"500\"} 1\n"), "text/plain", time.Time{})
+ app = sl.appender()
+ _, _, _, err = app.append([]byte("test_metric{le=\"500\"} 1\n"), "text/plain", time.Time{})
require.NoError(t, err)
- require.NoError(t, slApp.Commit())
+ require.NoError(t, app.Commit())
q, err = s.Querier(time.Time{}.UnixNano(), 0)
require.NoError(t, err)
- series = q.Select(ctx, false, nil, labels.MustNewMatcher(labels.MatchEqual, "le", "500"))
+ series = q.Select(sl.ctx, false, nil, labels.MustNewMatcher(labels.MatchEqual, "le", "500"))
require.True(t, series.Next(), "series not found in tsdb")
require.NoError(t, series.Err())
require.False(t, series.Next(), "more than one series found in tsdb")
}
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)
- defer s.Close()
- app := s.Appender(context.Background())
-
- ctx, cancel := context.WithCancel(context.Background())
- sl := newBasicScrapeLoop(t, context.Background(), &testScraper{}, func(context.Context) storage.Appender { return app }, 0)
- sl.sampleMutator = func(l labels.Labels) labels.Labels {
- if l.Has("drop") {
- return labels.FromStrings("no", "name") // This label set will trigger an error.
+ appTest := teststorage.NewAppendable().Then(s)
+ 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.
+ }
+ return l
}
- return l
- }
- defer cancel()
+ })
- slApp := sl.appender(context.Background())
- _, _, _, err := sl.append(slApp, []byte("nok 1\nnok2{drop=\"drop\"} 1\n"), "text/plain", time.Time{})
+ app := sl.appender()
+ _, _, _, err := app.append([]byte("nok 1\nnok2{drop=\"drop\"} 1\n"), "text/plain", time.Time{})
require.Error(t, err)
- require.NoError(t, slApp.Rollback())
+ require.NoError(t, app.Rollback())
require.Equal(t, errNameLabelMandatory, err)
q, err := s.Querier(time.Time{}.UnixNano(), 0)
require.NoError(t, err)
- series := q.Select(ctx, false, nil, labels.MustNewMatcher(labels.MatchRegexp, "__name__", ".*"))
+ series := q.Select(sl.ctx, false, nil, labels.MustNewMatcher(labels.MatchRegexp, "__name__", ".*"))
require.False(t, series.Next(), "series found in tsdb")
require.NoError(t, series.Err())
}
@@ -3757,8 +4265,14 @@ 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 = &nopAppendable{}
+ app = teststorage.NewAppendable()
cfg = &config.ScrapeConfig{
JobName: "Prometheus",
ScrapeTimeout: model.Duration(5 * time.Second),
@@ -3767,7 +4281,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{
@@ -3924,7 +4439,7 @@ func TestReuseScrapeCache(t *testing.T) {
for i, s := range steps {
initCacheAddr := cacheAddr(sp)
- sp.reload(s.newConfig)
+ require.NoError(t, sp.reload(s.newConfig))
for fp, newCacheAddr := range cacheAddr(sp) {
if s.keep {
require.Equal(t, initCacheAddr[fp], newCacheAddr, "step %d: old cache and new cache are not the same", i)
@@ -3933,7 +4448,7 @@ func TestReuseScrapeCache(t *testing.T) {
}
}
initCacheAddr = cacheAddr(sp)
- sp.reload(s.newConfig)
+ require.NoError(t, sp.reload(s.newConfig))
for fp, newCacheAddr := range cacheAddr(sp) {
require.Equal(t, initCacheAddr[fp], newCacheAddr, "step %d: reloading the exact config invalidates the cache", i)
}
@@ -3941,17 +4456,20 @@ 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)
- defer s.Close()
- ctx, cancel := context.WithCancel(context.Background())
- sl := newBasicScrapeLoop(t, ctx, &testScraper{}, s.Appender, 0)
- defer cancel()
+ sl, _ := newTestScrapeLoop(t, withAppendable(s, appV2))
- slApp := sl.appender(ctx)
- _, _, _, err := sl.append(slApp, []byte("up 1\n"), "text/plain", time.Time{})
+ app := sl.appender()
+ _, _, _, err := app.append([]byte("up 1\n"), "text/plain", time.Time{})
require.NoError(t, err)
- require.NoError(t, slApp.Commit())
+ require.NoError(t, app.Commit())
// Poison the cache. There is just one entry, and one series in the
// storage. Changing the ref will create a 'not found' error.
@@ -3959,15 +4477,20 @@ func TestScrapeAddFast(t *testing.T) {
v.ref++
}
- slApp = sl.appender(ctx)
- _, _, _, err = sl.append(slApp, []byte("up 1\n"), "text/plain", time.Time{}.Add(time.Second))
+ app = sl.appender()
+ _, _, _, err = app.append([]byte("up 1\n"), "text/plain", time.Time{}.Add(time.Second))
require.NoError(t, err)
- require.NoError(t, slApp.Commit())
+ require.NoError(t, app.Commit())
}
func TestReuseCacheRace(t *testing.T) {
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testReuseCacheRace(t, appV2)
+ })
+}
+
+func testReuseCacheRace(t *testing.T, appV2 bool) {
var (
- app = &nopAppendable{}
cfg = &config.ScrapeConfig{
JobName: "Prometheus",
ScrapeTimeout: model.Duration(5 * time.Second),
@@ -3977,7 +4500,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, app, 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{},
@@ -3991,7 +4515,7 @@ func TestReuseCacheRace(t *testing.T) {
if time.Since(start) > 5*time.Second {
break
}
- sp.reload(&config.ScrapeConfig{
+ require.NoError(t, sp.reload(&config.ScrapeConfig{
JobName: "Prometheus",
ScrapeTimeout: model.Duration(1 * time.Millisecond),
ScrapeInterval: model.Duration(1 * time.Millisecond),
@@ -3999,39 +4523,45 @@ func TestReuseCacheRace(t *testing.T) {
SampleLimit: i,
MetricNameValidationScheme: model.UTF8Validation,
MetricNameEscapingScheme: model.AllowUTF8,
- })
+ }))
}
}
func TestCheckAddError(t *testing.T) {
var appErrs appendErrors
- sl := scrapeLoop{l: promslog.NewNopLogger(), metrics: newTestScrapeMetrics(t)}
- sl.checkAddError(nil, storage.ErrOutOfOrderSample, nil, nil, &appErrs)
+ sl, _ := newTestScrapeLoop(t)
+ // TODO: Check err etc
+ _, _ = 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)
- defer s.Close()
- var (
- signal = make(chan struct{}, 1)
- scraper = &testScraper{}
- )
+ signal := make(chan struct{}, 1)
- ctx, cancel := context.WithCancel(context.Background())
- // Since we're writing samples directly below we need to provide a protocol fallback.
- sl := newBasicScrapeLoopWithFallback(t, ctx, scraper, s.Appender, 10*time.Millisecond, "text/plain")
+ ctx, cancel := context.WithCancel(t.Context())
+ sl, scraper := newTestScrapeLoop(t, withAppendable(s, 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"
+ })
numScrapes := 0
-
scraper.scrapeFunc = func(_ context.Context, w io.Writer) error {
numScrapes++
if numScrapes%4 == 0 {
return errors.New("scrape failed")
}
- w.Write([]byte("metric_a 44\nmetric_b 44\nmetric_c 44\nmetric_d 44\n"))
+ _, _ = w.Write([]byte("metric_a 44\nmetric_b 44\nmetric_c 44\nmetric_d 44\n"))
return nil
}
@@ -4055,7 +4585,7 @@ func TestScrapeReportSingleAppender(t *testing.T) {
}
require.Equal(t, 0, c%9, "Appended samples not as expected: %d", c)
- q.Close()
+ require.NoError(t, q.Close())
}
cancel()
@@ -4067,8 +4597,13 @@ 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)
- defer s.Close()
cfg := &config.ScrapeConfig{
JobName: "test",
@@ -4083,7 +4618,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()
@@ -4106,7 +4642,7 @@ func TestScrapeReportLimit(t *testing.T) {
ctx := t.Context()
q, err := s.Querier(time.Time{}.UnixNano(), time.Now().UnixNano())
require.NoError(t, err)
- defer q.Close()
+ t.Cleanup(func() { _ = q.Close() })
series := q.Select(ctx, false, nil, labels.MustNewMatcher(labels.MatchRegexp, "__name__", "up"))
var found bool
@@ -4123,8 +4659,13 @@ 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)
- defer s.Close()
cfg := &config.ScrapeConfig{
JobName: "test",
@@ -4137,7 +4678,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()
@@ -4160,14 +4702,20 @@ func TestScrapeUTF8(t *testing.T) {
ctx := t.Context()
q, err := s.Querier(time.Time{}.UnixNano(), time.Now().UnixNano())
require.NoError(t, err)
- defer q.Close()
+ t.Cleanup(func() { _ = q.Close() })
series := q.Select(ctx, false, nil, labels.MustNewMatcher(labels.MatchRegexp, "__name__", "with.dots"))
require.True(t, series.Next(), "series not found in tsdb")
}
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
@@ -4229,41 +4777,44 @@ func TestScrapeLoopLabelLimit(t *testing.T) {
labelLimits: labelLimits{labelValueLengthLimit: 10},
expectErr: true,
},
- }
-
- for _, test := range tests {
- app := &collectResultAppender{}
-
+ } {
discoveryLabels := &Target{
labels: labels.FromStrings(test.discoveryLabels...),
}
- sl := newBasicScrapeLoop(t, context.Background(), nil, func(context.Context) storage.Appender { return app }, 0)
- sl.sampleMutator = func(l labels.Labels) labels.Labels {
- return mutateSampleLabels(l, discoveryLabels, false, nil)
- }
- sl.reportSampleMutator = func(l labels.Labels) labels.Labels {
- return mutateReportSampleLabels(l, discoveryLabels)
- }
- sl.labelLimits = &test.labelLimits
+ sl, _ := newTestScrapeLoop(t, withAppendable(teststorage.NewAppendable(), appV2), func(sl *scrapeLoop) {
+ sl.sampleMutator = func(l labels.Labels) labels.Labels {
+ return mutateSampleLabels(l, discoveryLabels, false, nil)
+ }
+ sl.reportSampleMutator = func(l labels.Labels) labels.Labels {
+ return mutateReportSampleLabels(l, discoveryLabels)
+ }
+ sl.labelLimits = &test.labelLimits
+ })
- slApp := sl.appender(context.Background())
- _, _, _, err := sl.append(slApp, []byte(test.scrapeLabels), "text/plain", time.Now())
+ app := sl.appender()
+ _, _, _, err := app.append([]byte(test.scrapeLabels), "text/plain", time.Now())
t.Logf("Test:%s", test.title)
if test.expectErr {
require.Error(t, err)
} else {
require.NoError(t, err)
- require.NoError(t, slApp.Commit())
+ require.NoError(t, app.Commit())
}
}
}
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")
- config := &config.ScrapeConfig{
+ cfg := &config.ScrapeConfig{
ScrapeInterval: interval,
ScrapeTimeout: timeout,
MetricNameValidationScheme: model.UTF8Validation,
@@ -4287,7 +4838,9 @@ func TestTargetScrapeIntervalAndTimeoutRelabel(t *testing.T) {
},
},
}
- sp, _ := newScrapePool(config, &nopAppendable{}, 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"}},
@@ -4303,10 +4856,15 @@ func TestTargetScrapeIntervalAndTimeoutRelabel(t *testing.T) {
// Testing whether we can remove trailing .0 from histogram 'le' and summary 'quantile' labels.
func TestLeQuantileReLabel(t *testing.T) {
- simpleStorage := teststorage.New(t)
- defer simpleStorage.Close()
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testLeQuantileReLabel(t, appV2)
+ })
+}
- config := &config.ScrapeConfig{
+func testLeQuantileReLabel(t *testing.T, appV2 bool) {
+ s := teststorage.New(t)
+
+ cfg := &config.ScrapeConfig{
JobName: "test",
MetricRelabelConfigs: []*relabel.Config{
{
@@ -4373,7 +4931,8 @@ test_summary_count 199
ts, scrapedTwice := newScrapableServer(metricsText)
defer ts.Close()
- sp, err := newScrapePool(config, simpleStorage, 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()
@@ -4393,9 +4952,9 @@ test_summary_count 199
}
ctx := t.Context()
- q, err := simpleStorage.Querier(time.Time{}.UnixNano(), time.Now().UnixNano())
+ q, err := s.Querier(time.Time{}.UnixNano(), time.Now().UnixNano())
require.NoError(t, err)
- defer q.Close()
+ t.Cleanup(func() { _ = q.Close() })
checkValues := func(labelName string, expectedValues []string, series storage.SeriesSet) {
foundLeValues := map[string]bool{}
@@ -4422,31 +4981,29 @@ 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, value int, withMetadata bool) string {
- if withMetadata {
- return fmt.Sprintf(`
+
+ genTestCounterText := func(name string) string {
+ return fmt.Sprintf(`
# HELP %s some help text
# TYPE %s counter
-%s{address="0.0.0.0",port="5001"} %d
-`, name, name, name, value)
- }
- return fmt.Sprintf(`
-%s %d
-`, name, value)
+%s{address="0.0.0.0",port="5001"} 1
+`, name, name, name)
}
- genTestHistText := func(name string, withMetadata bool) string {
+ genTestHistText := func(name string) string {
data := map[string]any{
"name": name,
}
b := &bytes.Buffer{}
- if withMetadata {
- template.Must(template.New("").Parse(`
+ require.NoError(t, template.Must(template.New("").Parse(`
# HELP {{.name}} This is a histogram with default buckets
# TYPE {{.name}} histogram
-`)).Execute(b, data)
- }
- template.Must(template.New("").Parse(`
{{.name}}_bucket{address="0.0.0.0",port="5001",le="0.005"} 0
{{.name}}_bucket{address="0.0.0.0",port="5001",le="0.01"} 0
{{.name}}_bucket{address="0.0.0.0",port="5001",le="0.025"} 0
@@ -4461,10 +5018,10 @@ func TestConvertClassicHistogramsToNHCB(t *testing.T) {
{{.name}}_bucket{address="0.0.0.0",port="5001",le="+Inf"} 1
{{.name}}_sum{address="0.0.0.0",port="5001"} 10
{{.name}}_count{address="0.0.0.0",port="5001"} 1
-`)).Execute(b, data)
+`)).Execute(b, data))
return b.String()
}
- genTestCounterProto := func(name string, value int) string {
+ genTestCounterProto := func(name string) string {
return fmt.Sprintf(`
name: "%s"
help: "some help text"
@@ -4482,7 +5039,7 @@ metric: <
value: %d
>
>
-`, name, value)
+`, name, 1)
}
genTestHistProto := func(name string, hasClassic, hasExponential bool) string {
var classic string
@@ -4576,60 +5133,60 @@ metric: <
}{
"text": {
text: []string{
- genTestCounterText("test_metric_1", 1, true),
- genTestCounterText("test_metric_1_count", 1, true),
- genTestCounterText("test_metric_1_sum", 1, true),
- genTestCounterText("test_metric_1_bucket", 1, true),
- genTestHistText("test_histogram_1", true),
- genTestCounterText("test_metric_2", 1, true),
- genTestCounterText("test_metric_2_count", 1, true),
- genTestCounterText("test_metric_2_sum", 1, true),
- genTestCounterText("test_metric_2_bucket", 1, true),
- genTestHistText("test_histogram_2", true),
- genTestCounterText("test_metric_3", 1, true),
- genTestCounterText("test_metric_3_count", 1, true),
- genTestCounterText("test_metric_3_sum", 1, true),
- genTestCounterText("test_metric_3_bucket", 1, true),
- genTestHistText("test_histogram_3", true),
+ genTestCounterText("test_metric_1"),
+ genTestCounterText("test_metric_1_count"),
+ genTestCounterText("test_metric_1_sum"),
+ genTestCounterText("test_metric_1_bucket"),
+ genTestHistText("test_histogram_1"),
+ genTestCounterText("test_metric_2"),
+ genTestCounterText("test_metric_2_count"),
+ genTestCounterText("test_metric_2_sum"),
+ genTestCounterText("test_metric_2_bucket"),
+ genTestHistText("test_histogram_2"),
+ genTestCounterText("test_metric_3"),
+ genTestCounterText("test_metric_3_count"),
+ genTestCounterText("test_metric_3_sum"),
+ genTestCounterText("test_metric_3_bucket"),
+ genTestHistText("test_histogram_3"),
},
hasClassic: true,
},
"text, in different order": {
text: []string{
- genTestCounterText("test_metric_1", 1, true),
- genTestCounterText("test_metric_1_count", 1, true),
- genTestCounterText("test_metric_1_sum", 1, true),
- genTestCounterText("test_metric_1_bucket", 1, true),
- genTestHistText("test_histogram_1", true),
- genTestCounterText("test_metric_2", 1, true),
- genTestCounterText("test_metric_2_count", 1, true),
- genTestCounterText("test_metric_2_sum", 1, true),
- genTestCounterText("test_metric_2_bucket", 1, true),
- genTestHistText("test_histogram_2", true),
- genTestHistText("test_histogram_3", true),
- genTestCounterText("test_metric_3", 1, true),
- genTestCounterText("test_metric_3_count", 1, true),
- genTestCounterText("test_metric_3_sum", 1, true),
- genTestCounterText("test_metric_3_bucket", 1, true),
+ genTestCounterText("test_metric_1"),
+ genTestCounterText("test_metric_1_count"),
+ genTestCounterText("test_metric_1_sum"),
+ genTestCounterText("test_metric_1_bucket"),
+ genTestHistText("test_histogram_1"),
+ genTestCounterText("test_metric_2"),
+ genTestCounterText("test_metric_2_count"),
+ genTestCounterText("test_metric_2_sum"),
+ genTestCounterText("test_metric_2_bucket"),
+ genTestHistText("test_histogram_2"),
+ genTestHistText("test_histogram_3"),
+ genTestCounterText("test_metric_3"),
+ genTestCounterText("test_metric_3_count"),
+ genTestCounterText("test_metric_3_sum"),
+ genTestCounterText("test_metric_3_bucket"),
},
hasClassic: true,
},
"protobuf": {
text: []string{
- genTestCounterProto("test_metric_1", 1),
- genTestCounterProto("test_metric_1_count", 1),
- genTestCounterProto("test_metric_1_sum", 1),
- genTestCounterProto("test_metric_1_bucket", 1),
+ genTestCounterProto("test_metric_1"),
+ genTestCounterProto("test_metric_1_count"),
+ genTestCounterProto("test_metric_1_sum"),
+ genTestCounterProto("test_metric_1_bucket"),
genTestHistProto("test_histogram_1", true, false),
- genTestCounterProto("test_metric_2", 1),
- genTestCounterProto("test_metric_2_count", 1),
- genTestCounterProto("test_metric_2_sum", 1),
- genTestCounterProto("test_metric_2_bucket", 1),
+ genTestCounterProto("test_metric_2"),
+ genTestCounterProto("test_metric_2_count"),
+ genTestCounterProto("test_metric_2_sum"),
+ genTestCounterProto("test_metric_2_bucket"),
genTestHistProto("test_histogram_2", true, false),
- genTestCounterProto("test_metric_3", 1),
- genTestCounterProto("test_metric_3_count", 1),
- genTestCounterProto("test_metric_3_sum", 1),
- genTestCounterProto("test_metric_3_bucket", 1),
+ genTestCounterProto("test_metric_3"),
+ genTestCounterProto("test_metric_3_count"),
+ genTestCounterProto("test_metric_3_sum"),
+ genTestCounterProto("test_metric_3_bucket"),
genTestHistProto("test_histogram_3", true, false),
},
contentType: "application/vnd.google.protobuf",
@@ -4638,40 +5195,40 @@ metric: <
"protobuf, in different order": {
text: []string{
genTestHistProto("test_histogram_1", true, false),
- genTestCounterProto("test_metric_1", 1),
- genTestCounterProto("test_metric_1_count", 1),
- genTestCounterProto("test_metric_1_sum", 1),
- genTestCounterProto("test_metric_1_bucket", 1),
+ genTestCounterProto("test_metric_1"),
+ genTestCounterProto("test_metric_1_count"),
+ genTestCounterProto("test_metric_1_sum"),
+ genTestCounterProto("test_metric_1_bucket"),
genTestHistProto("test_histogram_2", true, false),
- genTestCounterProto("test_metric_2", 1),
- genTestCounterProto("test_metric_2_count", 1),
- genTestCounterProto("test_metric_2_sum", 1),
- genTestCounterProto("test_metric_2_bucket", 1),
+ genTestCounterProto("test_metric_2"),
+ genTestCounterProto("test_metric_2_count"),
+ genTestCounterProto("test_metric_2_sum"),
+ genTestCounterProto("test_metric_2_bucket"),
genTestHistProto("test_histogram_3", true, false),
- genTestCounterProto("test_metric_3", 1),
- genTestCounterProto("test_metric_3_count", 1),
- genTestCounterProto("test_metric_3_sum", 1),
- genTestCounterProto("test_metric_3_bucket", 1),
+ genTestCounterProto("test_metric_3"),
+ genTestCounterProto("test_metric_3_count"),
+ genTestCounterProto("test_metric_3_sum"),
+ genTestCounterProto("test_metric_3_bucket"),
},
contentType: "application/vnd.google.protobuf",
hasClassic: true,
},
"protobuf, with additional native exponential histogram": {
text: []string{
- genTestCounterProto("test_metric_1", 1),
- genTestCounterProto("test_metric_1_count", 1),
- genTestCounterProto("test_metric_1_sum", 1),
- genTestCounterProto("test_metric_1_bucket", 1),
+ genTestCounterProto("test_metric_1"),
+ genTestCounterProto("test_metric_1_count"),
+ genTestCounterProto("test_metric_1_sum"),
+ genTestCounterProto("test_metric_1_bucket"),
genTestHistProto("test_histogram_1", true, true),
- genTestCounterProto("test_metric_2", 1),
- genTestCounterProto("test_metric_2_count", 1),
- genTestCounterProto("test_metric_2_sum", 1),
- genTestCounterProto("test_metric_2_bucket", 1),
+ genTestCounterProto("test_metric_2"),
+ genTestCounterProto("test_metric_2_count"),
+ genTestCounterProto("test_metric_2_sum"),
+ genTestCounterProto("test_metric_2_bucket"),
genTestHistProto("test_histogram_2", true, true),
- genTestCounterProto("test_metric_3", 1),
- genTestCounterProto("test_metric_3_count", 1),
- genTestCounterProto("test_metric_3_sum", 1),
- genTestCounterProto("test_metric_3_bucket", 1),
+ genTestCounterProto("test_metric_3"),
+ genTestCounterProto("test_metric_3_count"),
+ genTestCounterProto("test_metric_3_sum"),
+ genTestCounterProto("test_metric_3_bucket"),
genTestHistProto("test_histogram_3", true, true),
},
contentType: "application/vnd.google.protobuf",
@@ -4680,20 +5237,20 @@ metric: <
},
"protobuf, with only native exponential histogram": {
text: []string{
- genTestCounterProto("test_metric_1", 1),
- genTestCounterProto("test_metric_1_count", 1),
- genTestCounterProto("test_metric_1_sum", 1),
- genTestCounterProto("test_metric_1_bucket", 1),
+ genTestCounterProto("test_metric_1"),
+ genTestCounterProto("test_metric_1_count"),
+ genTestCounterProto("test_metric_1_sum"),
+ genTestCounterProto("test_metric_1_bucket"),
genTestHistProto("test_histogram_1", false, true),
- genTestCounterProto("test_metric_2", 1),
- genTestCounterProto("test_metric_2_count", 1),
- genTestCounterProto("test_metric_2_sum", 1),
- genTestCounterProto("test_metric_2_bucket", 1),
+ genTestCounterProto("test_metric_2"),
+ genTestCounterProto("test_metric_2_count"),
+ genTestCounterProto("test_metric_2_sum"),
+ genTestCounterProto("test_metric_2_bucket"),
genTestHistProto("test_histogram_2", false, true),
- genTestCounterProto("test_metric_3", 1),
- genTestCounterProto("test_metric_3_count", 1),
- genTestCounterProto("test_metric_3_sum", 1),
- genTestCounterProto("test_metric_3_bucket", 1),
+ genTestCounterProto("test_metric_3"),
+ genTestCounterProto("test_metric_3_count"),
+ genTestCounterProto("test_metric_3_sum"),
+ genTestCounterProto("test_metric_3_bucket"),
genTestHistProto("test_histogram_3", false, true),
},
contentType: "application/vnd.google.protobuf",
@@ -4701,7 +5258,7 @@ metric: <
},
}
- checkBucketValues := func(expectedCount int, series storage.SeriesSet) {
+ checkBucketValues := func(t testing.TB, expectedCount int, series storage.SeriesSet) {
labelName := "le"
var expectedValues []string
if expectedCount > 0 {
@@ -4723,7 +5280,7 @@ metric: <
}
// Checks that the expected series is present and runs a basic sanity check of the float values.
- checkFloatSeries := func(series storage.SeriesSet, expectedCount int, expectedFloat float64) {
+ checkFloatSeries := func(t testing.TB, series storage.SeriesSet, expectedCount int, expectedFloat float64) {
count := 0
for series.Next() {
i := series.At().Iterator(nil)
@@ -4749,7 +5306,7 @@ metric: <
}
// Checks that the expected series is present and runs a basic sanity check of the histogram values.
- checkHistSeries := func(series storage.SeriesSet, expectedCount int, expectedSchema int32) {
+ checkHistSeries := func(t testing.TB, series storage.SeriesSet, expectedCount int, expectedSchema int32) {
count := 0
for series.Next() {
i := series.At().Iterator(nil)
@@ -4831,14 +5388,13 @@ metric: <
t.Run(fmt.Sprintf("%s with %s", name, metricsTextName), func(t *testing.T) {
t.Parallel()
- simpleStorage := teststorage.New(t)
- defer simpleStorage.Close()
+ s := teststorage.New(t)
- sl := newBasicScrapeLoop(t, context.Background(), nil, func(ctx context.Context) storage.Appender { return simpleStorage.Appender(ctx) }, 0)
- sl.alwaysScrapeClassicHist = tc.alwaysScrapeClassicHistograms
- sl.convertClassicHistToNHCB = tc.convertClassicHistToNHCB
- sl.enableNativeHistogramScraping = true
- app := simpleStorage.Appender(context.Background())
+ sl, _ := newTestScrapeLoop(t, withAppendable(s, appV2), func(sl *scrapeLoop) {
+ sl.alwaysScrapeClassicHist = tc.alwaysScrapeClassicHistograms
+ sl.convertClassicHistToNHCB = tc.convertClassicHistToNHCB
+ sl.enableNativeHistogramScraping = true
+ })
var content []byte
contentType := metricsText.contentType
@@ -4862,47 +5418,50 @@ metric: <
default:
t.Error("unexpected content type")
}
- sl.append(app, content, contentType, time.Now())
+ now := time.Now()
+ app := sl.appender()
+ _, _, _, err := app.append(content, contentType, now)
+ require.NoError(t, err)
require.NoError(t, app.Commit())
+ var expectedSchema int32
+ if expectCustomBuckets {
+ expectedSchema = histogram.CustomBucketsSchema
+ } else {
+ expectedSchema = 3
+ }
+
+ // Validated what was appended can be queried.
ctx := t.Context()
- q, err := simpleStorage.Querier(time.Time{}.UnixNano(), time.Now().UnixNano())
+ q, err := s.Querier(time.Time{}.UnixNano(), time.Now().UnixNano())
require.NoError(t, err)
- defer q.Close()
+ t.Cleanup(func() { _ = q.Close() })
var series storage.SeriesSet
-
for i := 1; i <= 3; i++ {
series = q.Select(ctx, false, nil, labels.MustNewMatcher(labels.MatchRegexp, "__name__", fmt.Sprintf("test_metric_%d", i)))
- checkFloatSeries(series, 1, 1.)
+ checkFloatSeries(t, series, 1, 1.)
series = q.Select(ctx, false, nil, labels.MustNewMatcher(labels.MatchRegexp, "__name__", fmt.Sprintf("test_metric_%d_count", i)))
- checkFloatSeries(series, 1, 1.)
+ checkFloatSeries(t, series, 1, 1.)
series = q.Select(ctx, false, nil, labels.MustNewMatcher(labels.MatchRegexp, "__name__", fmt.Sprintf("test_metric_%d_sum", i)))
- checkFloatSeries(series, 1, 1.)
+ checkFloatSeries(t, series, 1, 1.)
series = q.Select(ctx, false, nil, labels.MustNewMatcher(labels.MatchRegexp, "__name__", fmt.Sprintf("test_metric_%d_bucket", i)))
- checkFloatSeries(series, 1, 1.)
+ checkFloatSeries(t, series, 1, 1.)
series = q.Select(ctx, false, nil, labels.MustNewMatcher(labels.MatchRegexp, "__name__", fmt.Sprintf("test_histogram_%d_count", i)))
- checkFloatSeries(series, expectedClassicHistCount, 1.)
+ checkFloatSeries(t, series, expectedClassicHistCount, 1.)
series = q.Select(ctx, false, nil, labels.MustNewMatcher(labels.MatchRegexp, "__name__", fmt.Sprintf("test_histogram_%d_sum", i)))
- checkFloatSeries(series, expectedClassicHistCount, 10.)
+ checkFloatSeries(t, series, expectedClassicHistCount, 10.)
series = q.Select(ctx, false, nil, labels.MustNewMatcher(labels.MatchRegexp, "__name__", fmt.Sprintf("test_histogram_%d_bucket", i)))
- checkBucketValues(expectedClassicHistCount, series)
+ checkBucketValues(t, expectedClassicHistCount, series)
series = q.Select(ctx, false, nil, labels.MustNewMatcher(labels.MatchRegexp, "__name__", fmt.Sprintf("test_histogram_%d", i)))
-
- var expectedSchema int32
- if expectCustomBuckets {
- expectedSchema = histogram.CustomBucketsSchema
- } else {
- expectedSchema = 3
- }
- checkHistSeries(series, expectedNativeHistCount, expectedSchema)
+ checkHistSeries(t, series, expectedNativeHistCount, expectedSchema)
}
})
}
@@ -4910,10 +5469,15 @@ metric: <
}
func TestTypeUnitReLabel(t *testing.T) {
- simpleStorage := teststorage.New(t)
- defer simpleStorage.Close()
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testTypeUnitReLabel(t, appV2)
+ })
+}
- config := &config.ScrapeConfig{
+func testTypeUnitReLabel(t *testing.T, appV2 bool) {
+ s := teststorage.New(t)
+
+ cfg := &config.ScrapeConfig{
JobName: "test",
MetricRelabelConfigs: []*relabel.Config{
{
@@ -4958,7 +5522,8 @@ disk_usage_bytes 456
ts, scrapedTwice := newScrapableServer(metricsText)
defer ts.Close()
- sp, err := newScrapePool(config, simpleStorage, 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()
@@ -4978,9 +5543,9 @@ disk_usage_bytes 456
}
ctx := t.Context()
- q, err := simpleStorage.Querier(time.Time{}.UnixNano(), time.Now().UnixNano())
+ q, err := s.Querier(time.Time{}.UnixNano(), time.Now().UnixNano())
require.NoError(t, err)
- defer q.Close()
+ t.Cleanup(func() { _ = q.Close() })
series := q.Select(ctx, false, nil, labels.MustNewMatcher(labels.MatchRegexp, "__name__", ".*_total$"))
for series.Next() {
@@ -4996,26 +5561,30 @@ disk_usage_bytes 456
}
func TestScrapeLoopRunCreatesStaleMarkersOnFailedScrapeForTimestampedMetrics(t *testing.T) {
- appender := &collectResultAppender{}
- var (
- signal = make(chan struct{}, 1)
- scraper = &testScraper{}
- app = func(context.Context) storage.Appender { return appender }
- )
+ 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, 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
+ })
- ctx, cancel := context.WithCancel(context.Background())
- // Since we're writing samples directly below we need to provide a protocol fallback.
- sl := newBasicScrapeLoopWithFallback(t, ctx, scraper, app, 10*time.Millisecond, "text/plain")
- sl.trackTimestampsStaleness = true
// Succeed once, several failures, then stop.
numScrapes := 0
-
scraper.scrapeFunc = func(_ context.Context, w io.Writer) error {
numScrapes++
switch numScrapes {
case 1:
- fmt.Fprintf(w, "metric_a 42 %d\n", time.Now().UnixNano()/int64(time.Millisecond))
+ _, _ = fmt.Fprintf(w, "metric_a 42 %d\n", time.Now().UnixNano()/int64(time.Millisecond))
return nil
case 5:
cancel()
@@ -5033,17 +5602,24 @@ func TestScrapeLoopRunCreatesStaleMarkersOnFailedScrapeForTimestampedMetrics(t *
case <-time.After(5 * time.Second):
t.Fatalf("Scrape wasn't stopped.")
}
+
+ got := appTest.ResultSamples()
// 1 successfully scraped sample, 1 stale marker after first fail, 5 report samples for
// each scrape successful or not.
- require.Len(t, appender.resultFloats, 27, "Appended samples not as expected:\n%s", appender)
- require.Equal(t, 42.0, appender.resultFloats[0].f, "Appended first sample not as expected")
- require.True(t, value.IsStaleNaN(appender.resultFloats[6].f),
- "Appended second sample not as expected. Wanted: stale NaN Got: %x", math.Float64bits(appender.resultFloats[6].f))
+ require.Len(t, got, 27, "Appended samples not as expected:\n%s", appTest)
+ require.Equal(t, 42.0, got[0].V, "Appended first sample not as expected")
+ require.True(t, value.IsStaleNaN(got[6].V),
+ "Appended second sample not as expected. Wanted: stale NaN Got: %x", math.Float64bits(got[6].V))
}
func TestScrapeLoopCompression(t *testing.T) {
- simpleStorage := teststorage.New(t)
- defer simpleStorage.Close()
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopCompression(t, appV2)
+ })
+}
+
+func testScrapeLoopCompression(t *testing.T, appV2 bool) {
+ s := teststorage.New(t)
metricsText := makeTestGauges(10)
@@ -5065,12 +5641,12 @@ func TestScrapeLoopCompression(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, tc.acceptEncoding, r.Header.Get("Accept-Encoding"), "invalid value of the Accept-Encoding header")
- fmt.Fprint(w, string(metricsText))
+ _, _ = fmt.Fprint(w, string(metricsText))
close(scraped)
}))
defer ts.Close()
- config := &config.ScrapeConfig{
+ cfg := &config.ScrapeConfig{
JobName: "test",
SampleLimit: 100,
Scheme: "http",
@@ -5081,7 +5657,8 @@ func TestScrapeLoopCompression(t *testing.T) {
MetricNameEscapingScheme: model.AllowUTF8,
}
- sp, err := newScrapePool(config, simpleStorage, 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()
@@ -5191,11 +5768,11 @@ func BenchmarkTargetScraperGzip(b *testing.B) {
gw := gzip.NewWriter(&buf)
for j := 0; j < scenarios[i].metricsCount; j++ {
name = fmt.Sprintf("go_memstats_alloc_bytes_total_%d", j)
- fmt.Fprintf(gw, "# HELP %s Total number of bytes allocated, even if freed.\n", name)
- fmt.Fprintf(gw, "# TYPE %s counter\n", name)
- fmt.Fprintf(gw, "%s %d\n", name, i*j)
+ _, _ = fmt.Fprintf(gw, "# HELP %s Total number of bytes allocated, even if freed.\n", name)
+ _, _ = fmt.Fprintf(gw, "# TYPE %s counter\n", name)
+ _, _ = fmt.Fprintf(gw, "%s %d\n", name, i*j)
}
- gw.Close()
+ require.NoError(b, gw.Close())
scenarios[i].body = buf.Bytes()
}
@@ -5204,7 +5781,7 @@ func BenchmarkTargetScraperGzip(b *testing.B) {
w.Header().Set("Content-Encoding", "gzip")
for _, scenario := range scenarios {
if strconv.Itoa(scenario.metricsCount) == r.URL.Query()["count"][0] {
- w.Write(scenario.body)
+ _, _ = w.Write(scenario.body)
return
}
}
@@ -5253,31 +5830,37 @@ 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) {
- ctx, sl := simpleTestScrapeLoop(t)
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopSeriesAddedDuplicates(t, appV2)
+ })
+}
- slApp := sl.appender(ctx)
- total, added, seriesAdded, err := sl.append(slApp, []byte("test_metric 1\ntest_metric 2\ntest_metric 3\n"), "text/plain", time.Time{})
+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{})
require.NoError(t, err)
- require.NoError(t, slApp.Commit())
+ require.NoError(t, app.Commit())
require.Equal(t, 3, total)
require.Equal(t, 3, added)
require.Equal(t, 1, seriesAdded)
require.Equal(t, 2.0, prom_testutil.ToFloat64(sl.metrics.targetScrapeSampleDuplicate))
- slApp = sl.appender(ctx)
- total, added, seriesAdded, err = sl.append(slApp, []byte("test_metric 1\ntest_metric 1\ntest_metric 1\n"), "text/plain", time.Time{})
+ app = sl.appender()
+ total, added, seriesAdded, err = app.append([]byte("test_metric 1\ntest_metric 1\ntest_metric 1\n"), "text/plain", time.Time{})
require.NoError(t, err)
- require.NoError(t, slApp.Commit())
+ require.NoError(t, app.Commit())
require.Equal(t, 3, total)
require.Equal(t, 3, added)
require.Equal(t, 0, seriesAdded)
require.Equal(t, 4.0, prom_testutil.ToFloat64(sl.metrics.targetScrapeSampleDuplicate))
// When different timestamps are supplied, multiple samples are accepted.
- slApp = sl.appender(ctx)
- total, added, seriesAdded, err = sl.append(slApp, []byte("test_metric 1 1001\ntest_metric 1 1002\ntest_metric 1 1003\n"), "text/plain", time.Time{})
+ app = sl.appender()
+ total, added, seriesAdded, err = app.append([]byte("test_metric 1 1001\ntest_metric 1 1002\ntest_metric 1 1003\n"), "text/plain", time.Time{})
require.NoError(t, err)
- require.NoError(t, slApp.Commit())
+ require.NoError(t, app.Commit())
require.Equal(t, 3, total)
require.Equal(t, 3, added)
require.Equal(t, 0, seriesAdded)
@@ -5288,32 +5871,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{
@@ -5325,7 +5913,7 @@ func testNativeHistogramMaxSchemaSet(t *testing.T, minBucketFactor string, expec
},
)
registry := prometheus.NewRegistry()
- registry.Register(nativeHistogram)
+ require.NoError(t, registry.Register(nativeHistogram))
nativeHistogram.Observe(1.0)
nativeHistogram.Observe(1.0)
nativeHistogram.Observe(1.0)
@@ -5339,10 +5927,10 @@ func testNativeHistogramMaxSchemaSet(t *testing.T, minBucketFactor string, expec
histogramMetricFamily := gathered[0]
buffer := protoMarshalDelimited(t, histogramMetricFamily)
- // Create a HTTP server to serve /metrics via ProtoBuf
+ // Create an HTTP server to serve /metrics via ProtoBuf
metricsServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", `application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited`)
- w.Write(buffer)
+ _, _ = w.Write(buffer)
}))
defer metricsServer.Close()
@@ -5361,18 +5949,18 @@ scrape_configs:
`, minBucketFactor, strings.ReplaceAll(metricsServer.URL, "http://", ""))
s := teststorage.New(t)
- defer s.Close()
reg := prometheus.NewRegistry()
- mng, err := NewManager(&Options{DiscoveryReloadInterval: model.Duration(10 * time.Millisecond)}, nil, nil, s, reg)
+ sa := selectAppendable(s, appV2)
+ mng, err := NewManager(&Options{DiscoveryReloadInterval: model.Duration(10 * time.Millisecond)}, nil, nil, sa.V1(), sa.V2(), reg)
require.NoError(t, err)
+
cfg, err := config.Load(configStr, promslog.NewNopLogger())
require.NoError(t, err)
- mng.ApplyConfig(cfg)
+ require.NoError(t, mng.ApplyConfig(cfg))
tsets := make(chan map[string][]*targetgroup.Group)
go func() {
- err = mng.Run(tsets)
- require.NoError(t, err)
+ require.NoError(t, mng.Run(tsets))
}()
defer mng.Stop()
@@ -5401,7 +5989,7 @@ scrape_configs:
q, err := s.Querier(0, math.MaxInt64)
require.NoError(t, err)
seriesS := q.Select(context.Background(), false, nil, labels.MustNewMatcher(labels.MatchEqual, "__name__", "testing_example_native_histogram"))
- histogramSamples := []*histogram.Histogram{}
+ var histogramSamples []*histogram.Histogram
for seriesS.Next() {
series := seriesS.At()
it := series.Iterator(nil)
@@ -5422,6 +6010,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
@@ -5447,7 +6041,7 @@ func TestTargetScrapeConfigWithLabels(t *testing.T) {
require.Equal(t, expectedPath, r.URL.Path)
w.Header().Set("Content-Type", `text/plain; version=0.0.4`)
- w.Write([]byte("metric_a 1\nmetric_b 2\n"))
+ _, _ = w.Write([]byte("metric_a 1\nmetric_b 2\n"))
}),
)
t.Cleanup(server.Close)
@@ -5467,7 +6061,8 @@ func TestTargetScrapeConfigWithLabels(t *testing.T) {
}
}
- sp, err := newScrapePool(cfg, &nopAppendable{}, 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)
@@ -5595,7 +6190,7 @@ func newScrapableServer(scrapeText string) (s *httptest.Server, scrapedTwice cha
scrapedTwice = make(chan bool)
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
- fmt.Fprint(w, scrapeText)
+ _, _ = fmt.Fprint(w, scrapeText)
scrapes++
if scrapes == 2 {
close(scrapedTwice)
@@ -5605,9 +6200,15 @@ 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})
+ _, _ = w.Write([]byte{0x42, 0x42})
},
))
t.Cleanup(h.Close)
@@ -5630,7 +6231,8 @@ func TestScrapePoolScrapeAfterReload(t *testing.T) {
},
}
- p, err := newScrapePool(cfg, &nopAppendable{}, 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)
@@ -5650,6 +6252,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
`
@@ -5657,103 +6265,110 @@ func TestScrapeAppendWithParseError(t *testing.T) {
# EOF`
)
- sl := newBasicScrapeLoop(t, context.Background(), nil, nil, 0)
- sl.cache = newScrapeCache(sl.metrics)
-
+ appTest := teststorage.NewAppendable()
+ sl, _ := newTestScrapeLoop(t, withAppendable(appTest, appV2))
now := time.Now()
- capp := &collectResultAppender{next: nopAppender{}}
- _, _, _, err := sl.append(capp, []byte(scrape1), "application/openmetrics-text", now)
+
+ app := sl.appender()
+ _, _, _, err := app.append([]byte(scrape1), "application/openmetrics-text", now)
require.Error(t, err)
- _, _, _, err = sl.append(capp, nil, "application/openmetrics-text", now)
- require.NoError(t, err)
- require.Empty(t, capp.resultFloats)
+ require.NoError(t, app.Rollback())
- capp = &collectResultAppender{next: nopAppender{}}
- _, _, _, err = sl.append(capp, []byte(scrape2), "application/openmetrics-text", now.Add(15*time.Second))
+ app = sl.appender()
+ _, _, _, err = app.append(nil, "application/openmetrics-text", now)
require.NoError(t, err)
- require.NoError(t, capp.Commit())
+ require.NoError(t, app.Commit())
+ require.Empty(t, appTest.ResultSamples())
- want := []floatSample{
+ 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())
+
+ want := []sample{
{
- metric: labels.FromStrings(model.MetricNameLabel, "metric_a"),
- t: timestamp.FromTime(now.Add(15 * time.Second)),
- f: 11,
+ L: labels.FromStrings(model.MetricNameLabel, "metric_a"),
+ T: timestamp.FromTime(now.Add(15 * time.Second)),
+ V: 11,
},
}
- requireEqual(t, want, capp.resultFloats, "Appended samples not as expected:\n%s", capp)
+ 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 the some of exporter samples
+// 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
- resApp := &collectResultAppender{}
- sl := newBasicScrapeLoop(t, context.Background(), nil, func(_ context.Context) storage.Appender {
- return resApp
- }, 0)
- sl.sampleLimit = sampleLimit
+
+ appTest := teststorage.NewAppendable()
+ sl, _ := newTestScrapeLoop(t, withAppendable(appTest, appV2), func(sl *scrapeLoop) {
+ sl.sampleLimit = sampleLimit
+ })
now := time.Now()
- slApp := sl.appender(context.Background())
- samplesScraped, samplesAfterRelabel, createdSeries, err := sl.append(
- slApp,
+ app := sl.appender()
+ samplesScraped, samplesAfterRelabel, createdSeries, err := app.append(
// Start with 3 samples, all accepted.
[]byte("metric_a 1\nmetric_b 1\nmetric_c 1\n"),
"text/plain",
now,
)
require.NoError(t, err)
- require.NoError(t, slApp.Commit())
+ require.NoError(t, app.Commit())
require.Equal(t, 3, samplesScraped) // All on scrape.
require.Equal(t, 3, samplesAfterRelabel) // This is series after relabeling.
require.Equal(t, 3, createdSeries) // Newly added to TSDB.
- want := []floatSample{
+ want := []sample{
{
- metric: labels.FromStrings(model.MetricNameLabel, "metric_a"),
- t: timestamp.FromTime(now),
- f: 1,
+ L: labels.FromStrings(model.MetricNameLabel, "metric_a"),
+ T: timestamp.FromTime(now),
+ V: 1,
},
{
- metric: labels.FromStrings(model.MetricNameLabel, "metric_b"),
- t: timestamp.FromTime(now),
- f: 1,
+ L: labels.FromStrings(model.MetricNameLabel, "metric_b"),
+ T: timestamp.FromTime(now),
+ V: 1,
},
{
- metric: labels.FromStrings(model.MetricNameLabel, "metric_c"),
- t: timestamp.FromTime(now),
- f: 1,
+ L: labels.FromStrings(model.MetricNameLabel, "metric_c"),
+ T: timestamp.FromTime(now),
+ V: 1,
},
}
- requireEqual(t, want, resApp.resultFloats, "Appended samples not as expected:\n%s", slApp)
+ teststorage.RequireEqual(t, want, appTest.ResultSamples(), "Appended samples not as expected:\n%s", app)
now = now.Add(time.Minute)
- slApp = sl.appender(context.Background())
- samplesScraped, samplesAfterRelabel, createdSeries, err = sl.append(
- slApp,
+ app = sl.appender()
+ samplesScraped, samplesAfterRelabel, createdSeries, err = app.append(
// Start exporting 3 more samples, so we're over the limit now.
[]byte("metric_a 1\nmetric_b 1\nmetric_c 1\nmetric_d 1\nmetric_e 1\nmetric_f 1\n"),
"text/plain",
now,
)
require.ErrorIs(t, err, errSampleLimit)
- require.NoError(t, slApp.Rollback())
+ require.NoError(t, app.Rollback())
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, resApp.resultFloats, "Appended samples not as expected:\n%s", slApp)
+ testutil.RequireEqual(t, want, appTest.ResultSamples(), "Appended samples not as expected:\n%s", app)
sl.cache.iterDone(false)
now = now.Add(time.Minute)
- slApp = sl.appender(context.Background())
- samplesScraped, samplesAfterRelabel, createdSeries, err = sl.append(
- slApp,
+ app = sl.appender()
+ samplesScraped, samplesAfterRelabel, createdSeries, err = app.append(
// Remove all samples except first 2.
[]byte("metric_a 1\nmetric_b 1\n"),
"text/plain",
now,
)
require.NoError(t, err)
- require.NoError(t, slApp.Commit())
+ require.NoError(t, app.Commit())
require.Equal(t, 2, samplesScraped)
require.Equal(t, 2, samplesAfterRelabel)
require.Equal(t, 0, createdSeries)
@@ -5762,152 +6377,158 @@ func TestScrapeLoopAppendSampleLimitWithDisappearingSeries(t *testing.T) {
// - Append with stale markers for metric_c - this series was added during first scrape but disappeared during last scrape.
// - Append with stale marker for metric_d - this series was added during second scrape before we hit the sample_limit.
// We should NOT see:
- // - Appends with stale markers for metric_e & metric_f - both over the limit during second scrape and so they never made it into TSDB.
- want = append(want, []floatSample{
+ // - Appends with stale markers for metric_e & metric_f - both over the limit during second scrape, and so they never made it into TSDB.
+ want = append(want, []sample{
{
- metric: labels.FromStrings(model.MetricNameLabel, "metric_a"),
- t: timestamp.FromTime(now),
- f: 1,
+ L: labels.FromStrings(model.MetricNameLabel, "metric_a"),
+ T: timestamp.FromTime(now),
+ V: 1,
},
{
- metric: labels.FromStrings(model.MetricNameLabel, "metric_b"),
- t: timestamp.FromTime(now),
- f: 1,
+ L: labels.FromStrings(model.MetricNameLabel, "metric_b"),
+ T: timestamp.FromTime(now),
+ V: 1,
},
{
- metric: labels.FromStrings(model.MetricNameLabel, "metric_c"),
- t: timestamp.FromTime(now),
- f: math.Float64frombits(value.StaleNaN),
+ L: labels.FromStrings(model.MetricNameLabel, "metric_c"),
+ T: timestamp.FromTime(now),
+ V: math.Float64frombits(value.StaleNaN),
},
{
- metric: labels.FromStrings(model.MetricNameLabel, "metric_d"),
- t: timestamp.FromTime(now),
- f: math.Float64frombits(value.StaleNaN),
+ L: labels.FromStrings(model.MetricNameLabel, "metric_d"),
+ T: timestamp.FromTime(now),
+ V: math.Float64frombits(value.StaleNaN),
},
}...)
- requireEqual(t, want, resApp.resultFloats, "Appended samples not as expected:\n%s", slApp)
+ 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
- resApp := &collectResultAppender{}
- sl := newBasicScrapeLoop(t, context.Background(), nil, func(_ context.Context) storage.Appender {
- return resApp
- }, 0)
- sl.sampleLimit = sampleLimit
+
+ appTest := teststorage.NewAppendable()
+ sl, _ := newTestScrapeLoop(t, withAppendable(appTest, appV2), func(sl *scrapeLoop) {
+ sl.sampleLimit = sampleLimit
+ })
now := time.Now()
- slApp := sl.appender(context.Background())
- samplesScraped, samplesAfterRelabel, createdSeries, err := sl.append(
- slApp,
+ app := sl.appender()
+ samplesScraped, samplesAfterRelabel, createdSeries, err := app.append(
// Start with 4 samples, all accepted.
[]byte("metric_a 1\nmetric_b 1\nmetric_c 1\nmetric_d 1\n"),
"text/plain",
now,
)
require.NoError(t, err)
- require.NoError(t, slApp.Commit())
+ require.NoError(t, app.Commit())
require.Equal(t, 4, samplesScraped) // All on scrape.
require.Equal(t, 4, samplesAfterRelabel) // This is series after relabeling.
require.Equal(t, 4, createdSeries) // Newly added to TSDB.
- want := []floatSample{
+ want := []sample{
{
- metric: labels.FromStrings(model.MetricNameLabel, "metric_a"),
- t: timestamp.FromTime(now),
- f: 1,
+ L: labels.FromStrings(model.MetricNameLabel, "metric_a"),
+ T: timestamp.FromTime(now),
+ V: 1,
},
{
- metric: labels.FromStrings(model.MetricNameLabel, "metric_b"),
- t: timestamp.FromTime(now),
- f: 1,
+ L: labels.FromStrings(model.MetricNameLabel, "metric_b"),
+ T: timestamp.FromTime(now),
+ V: 1,
},
{
- metric: labels.FromStrings(model.MetricNameLabel, "metric_c"),
- t: timestamp.FromTime(now),
- f: 1,
+ L: labels.FromStrings(model.MetricNameLabel, "metric_c"),
+ T: timestamp.FromTime(now),
+ V: 1,
},
{
- metric: labels.FromStrings(model.MetricNameLabel, "metric_d"),
- t: timestamp.FromTime(now),
- f: 1,
+ L: labels.FromStrings(model.MetricNameLabel, "metric_d"),
+ T: timestamp.FromTime(now),
+ V: 1,
},
}
- requireEqual(t, want, resApp.resultFloats, "Appended samples not as expected:\n%s", slApp)
+ teststorage.RequireEqual(t, want, appTest.ResultSamples(), "Appended samples not as expected:\n%s", app)
now = now.Add(time.Minute)
- slApp = sl.appender(context.Background())
- samplesScraped, samplesAfterRelabel, createdSeries, err = sl.append(
- slApp,
+ app = sl.appender()
+ samplesScraped, samplesAfterRelabel, createdSeries, err = app.append(
// Replace all samples with new time series.
[]byte("metric_e 1\nmetric_f 1\nmetric_g 1\nmetric_h 1\n"),
"text/plain",
now,
)
require.NoError(t, err)
- require.NoError(t, slApp.Commit())
+ require.NoError(t, app.Commit())
require.Equal(t, 4, samplesScraped)
require.Equal(t, 4, samplesAfterRelabel)
require.Equal(t, 4, createdSeries)
// We replaced all samples from first scrape with new set of samples.
- // We expect to see:
+ // We expected to see:
// - 4 appends for new samples.
// - 4 appends with staleness markers for old samples.
- want = append(want, []floatSample{
+ want = append(want, []sample{
{
- metric: labels.FromStrings(model.MetricNameLabel, "metric_e"),
- t: timestamp.FromTime(now),
- f: 1,
+ L: labels.FromStrings(model.MetricNameLabel, "metric_e"),
+ T: timestamp.FromTime(now),
+ V: 1,
},
{
- metric: labels.FromStrings(model.MetricNameLabel, "metric_f"),
- t: timestamp.FromTime(now),
- f: 1,
+ L: labels.FromStrings(model.MetricNameLabel, "metric_f"),
+ T: timestamp.FromTime(now),
+ V: 1,
},
{
- metric: labels.FromStrings(model.MetricNameLabel, "metric_g"),
- t: timestamp.FromTime(now),
- f: 1,
+ L: labels.FromStrings(model.MetricNameLabel, "metric_g"),
+ T: timestamp.FromTime(now),
+ V: 1,
},
{
- metric: labels.FromStrings(model.MetricNameLabel, "metric_h"),
- t: timestamp.FromTime(now),
- f: 1,
+ L: labels.FromStrings(model.MetricNameLabel, "metric_h"),
+ T: timestamp.FromTime(now),
+ V: 1,
},
{
- metric: labels.FromStrings(model.MetricNameLabel, "metric_a"),
- t: timestamp.FromTime(now),
- f: math.Float64frombits(value.StaleNaN),
+ L: labels.FromStrings(model.MetricNameLabel, "metric_a"),
+ T: timestamp.FromTime(now),
+ V: math.Float64frombits(value.StaleNaN),
},
{
- metric: labels.FromStrings(model.MetricNameLabel, "metric_b"),
- t: timestamp.FromTime(now),
- f: math.Float64frombits(value.StaleNaN),
+ L: labels.FromStrings(model.MetricNameLabel, "metric_b"),
+ T: timestamp.FromTime(now),
+ V: math.Float64frombits(value.StaleNaN),
},
{
- metric: labels.FromStrings(model.MetricNameLabel, "metric_c"),
- t: timestamp.FromTime(now),
- f: math.Float64frombits(value.StaleNaN),
+ L: labels.FromStrings(model.MetricNameLabel, "metric_c"),
+ T: timestamp.FromTime(now),
+ V: math.Float64frombits(value.StaleNaN),
},
{
- metric: labels.FromStrings(model.MetricNameLabel, "metric_d"),
- t: timestamp.FromTime(now),
- f: math.Float64frombits(value.StaleNaN),
+ L: labels.FromStrings(model.MetricNameLabel, "metric_d"),
+ T: timestamp.FromTime(now),
+ V: math.Float64frombits(value.StaleNaN),
},
}...)
- requireEqual(t, want, resApp.resultFloats, "Appended samples not as expected:\n%s", slApp)
+ teststorage.RequireEqual(t, want, appTest.ResultSamples(), "Appended samples not as expected:\n%s", app)
}
func TestScrapeLoopDisableStalenessMarkerInjection(t *testing.T) {
- var (
- loopDone = atomic.NewBool(false)
- appender = &collectResultAppender{}
- scraper = &testScraper{}
- app = func(_ context.Context) storage.Appender { return appender }
- )
+ foreachAppendable(t, func(t *testing.T, appV2 bool) {
+ testScrapeLoopDisableStalenessMarkerInjection(t, appV2)
+ })
+}
- sl := newBasicScrapeLoop(t, context.Background(), scraper, app, 10*time.Millisecond)
+func testScrapeLoopDisableStalenessMarkerInjection(t *testing.T, appV2 bool) {
+ loopDone := atomic.NewBool(false)
+
+ appTest := teststorage.NewAppendable()
+ 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
@@ -5923,9 +6544,7 @@ func TestScrapeLoopDisableStalenessMarkerInjection(t *testing.T) {
// Wait for some samples to be appended.
require.Eventually(t, func() bool {
- appender.mtx.Lock()
- defer appender.mtx.Unlock()
- return len(appender.resultFloats) > 2
+ return len(appTest.ResultSamples()) > 2
}, 5*time.Second, 100*time.Millisecond, "Scrape loop didn't append any samples.")
// Disable end of run staleness markers and stop the loop.
@@ -5936,9 +6555,182 @@ func TestScrapeLoopDisableStalenessMarkerInjection(t *testing.T) {
}, 5*time.Second, 100*time.Millisecond, "Scrape loop didn't stop.")
// No stale markers should be appended, since they were disabled.
- for _, s := range appender.resultFloats {
- if value.IsStaleNaN(s.f) {
- t.Fatalf("Got stale NaN samples while end of run staleness is disabled: %x", math.Float64bits(s.f))
+ for _, s := range appTest.ResultSamples() {
+ if value.IsStaleNaN(s.V) {
+ t.Fatalf("Got stale NaN samples while end of run staleness is disabled: %x", math.Float64bits(s.V))
}
}
}
+
+// Recommended CLI invocation:
+/*
+ export bench=restartLoops && go test ./scrape/... \
+ -run '^$' -bench '^BenchmarkScrapePoolRestartLoops' \
+ -benchtime 5s -count 6 -cpu 2 -timeout 999m \
+ | tee ${bench}.txt
+*/
+func BenchmarkScrapePoolRestartLoops(b *testing.B) {
+ sp, err := newScrapePool(
+ &config.ScrapeConfig{
+ MetricNameValidationScheme: model.UTF8Validation,
+ ScrapeInterval: model.Duration(1 * time.Hour),
+ ScrapeTimeout: model.Duration(1 * time.Hour),
+ },
+ nil,
+ nil,
+ 0,
+ nil,
+ nil,
+ &Options{},
+ newTestScrapeMetrics(b),
+ )
+ require.NoError(b, err)
+ b.Cleanup(sp.stop)
+
+ for i := range 1000 {
+ sp.activeTargets[uint64(i)] = &Target{scrapeConfig: &config.ScrapeConfig{}}
+ sp.loops[uint64(i)] = noopLoop() // First restart will supplement those with proper scrapeLoops.
+ }
+ sp.restartLoops(true)
+
+ b.ReportAllocs()
+ b.ResetTimer()
+ for b.Loop() {
+ sp.restartLoops(true)
+ }
+}
+
+// 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.
+ // With honor_labels=false, the discovery value should win and scraped moves to exported_lbl.
+ testCases := []struct {
+ name string
+ honorLabels bool
+ expectedLbl string
+ expectedExpLbl string // exported_lbl value, empty if not expected
+ }{
+ {
+ name: "honor_labels=true",
+ honorLabels: true,
+ expectedLbl: "scraped",
+ },
+ {
+ name: "honor_labels=false",
+ honorLabels: false,
+ expectedLbl: "discovery",
+ expectedExpLbl: "scraped",
+ },
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ ts, scrapedTwice := newScrapableServer(`metric{lbl="scraped"} 1`)
+ defer ts.Close()
+
+ testURL, err := url.Parse(ts.URL)
+ require.NoError(t, err)
+
+ s := teststorage.New(t)
+
+ cfg := &config.ScrapeConfig{
+ JobName: "test",
+ Scheme: "http",
+ HonorLabels: tc.honorLabels,
+ HonorTimestamps: !tc.honorLabels, // Opposite of HonorLabels to catch wiring bugs
+ ScrapeInterval: model.Duration(1 * time.Second),
+ ScrapeTimeout: model.Duration(100 * time.Millisecond),
+ MetricNameValidationScheme: model.UTF8Validation,
+ }
+
+ 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()
+
+ // Sync with a target that has a conflicting label.
+ sp.Sync([]*targetgroup.Group{{
+ Targets: []model.LabelSet{{
+ model.AddressLabel: model.LabelValue(testURL.Host),
+ "lbl": "discovery",
+ }},
+ }})
+ require.Len(t, sp.ActiveTargets(), 1)
+
+ // Wait for scrape to complete.
+ select {
+ case <-time.After(5 * time.Second):
+ t.Fatal("scrape did not complete in time")
+ case <-scrapedTwice:
+ }
+
+ // Query the storage to verify label values.
+ q, err := s.Querier(time.Time{}.UnixNano(), time.Now().UnixNano())
+ require.NoError(t, err)
+ defer q.Close()
+
+ series := q.Select(t.Context(), false, nil, labels.MustNewMatcher(labels.MatchEqual, "__name__", "metric"))
+ require.True(t, series.Next(), "metric series not found")
+ require.Equal(t, tc.expectedLbl, series.At().Labels().Get("lbl"))
+ require.Equal(t, tc.expectedExpLbl, series.At().Labels().Get("exported_lbl"))
+ })
+ }
+}
+
+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{
+ {
+ SourceLabels: model.LabelNames{"__name__"},
+ Regex: relabel.MustNewRegexp("test_metric.*$"),
+ Action: relabel.Keep,
+ NameValidationScheme: model.UTF8Validation,
+ },
+ {
+ SourceLabels: model.LabelNames{"__name__"},
+ Regex: relabel.MustNewRegexp("test_metric_2$"),
+ Action: relabel.Drop,
+ NameValidationScheme: model.UTF8Validation,
+ },
+ }
+ sl, _ := newTestScrapeLoop(t, withAppendable(teststorage.NewAppendable(), appV2), func(sl *scrapeLoop) {
+ sl.sampleMutator = func(l labels.Labels) labels.Labels {
+ return mutateSampleLabels(l, target, true, relabelConfig)
+ }
+ })
+
+ app := sl.appender()
+ total, added, seriesAdded, err := app.append([]byte("test_metric_1 1\n"), "text/plain", time.Time{})
+ require.NoError(t, err)
+ require.Equal(t, 1, total)
+ require.Equal(t, 1, added)
+ require.Equal(t, 1, seriesAdded)
+
+ total, added, seriesAdded, err = app.append([]byte("test_metric_2 1\n"), "text/plain", time.Time{})
+ require.NoError(t, err)
+ require.Equal(t, 1, total)
+ require.Equal(t, 0, added)
+ require.Equal(t, 0, seriesAdded)
+
+ total, added, seriesAdded, err = app.append([]byte("unwanted_metric 1\n"), "text/plain", time.Time{})
+ require.NoError(t, err)
+ require.Equal(t, 1, total)
+ require.Equal(t, 0, added)
+ require.Equal(t, 0, seriesAdded)
+
+ require.NoError(t, app.Commit())
+}
diff --git a/scrape/target.go b/scrape/target.go
index 563fe33f82..1040241bd3 100644
--- a/scrape/target.go
+++ b/scrape/target.go
@@ -1,4 +1,4 @@
-// Copyright 2013 The Prometheus Authors
+// 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
@@ -389,6 +389,7 @@ type bucketLimitAppender struct {
}
func (app *bucketLimitAppender) AppendHistogram(ref storage.SeriesRef, lset labels.Labels, t int64, h *histogram.Histogram, fh *histogram.FloatHistogram) (storage.SeriesRef, error) {
+ var 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.
@@ -399,7 +400,9 @@ func (app *bucketLimitAppender) AppendHistogram(ref storage.SeriesRef, lset labe
if h.Schema <= histogram.ExponentialSchemaMin {
return 0, errBucketLimit
}
- h = h.ReduceResolution(h.Schema - 1)
+ if err = h.ReduceResolution(h.Schema - 1); err != nil {
+ return 0, err
+ }
}
}
if fh != nil {
@@ -412,11 +415,12 @@ func (app *bucketLimitAppender) AppendHistogram(ref storage.SeriesRef, lset labe
if fh.Schema <= histogram.ExponentialSchemaMin {
return 0, errBucketLimit
}
- fh = fh.ReduceResolution(fh.Schema - 1)
+ if err = fh.ReduceResolution(fh.Schema - 1); err != nil {
+ return 0, err
+ }
}
}
- ref, err := app.Appender.AppendHistogram(ref, lset, t, h, fh)
- if err != nil {
+ if ref, err = app.Appender.AppendHistogram(ref, lset, t, h, fh); err != nil {
return 0, err
}
return ref, nil
@@ -429,23 +433,126 @@ type maxSchemaAppender struct {
}
func (app *maxSchemaAppender) AppendHistogram(ref storage.SeriesRef, lset labels.Labels, t int64, h *histogram.Histogram, fh *histogram.FloatHistogram) (storage.SeriesRef, error) {
+ var err error
if h != nil {
if histogram.IsExponentialSchemaReserved(h.Schema) && h.Schema > app.maxSchema {
- h = h.ReduceResolution(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 {
- fh = fh.ReduceResolution(app.maxSchema)
+ if err = fh.ReduceResolution(app.maxSchema); err != nil {
+ return 0, err
+ }
}
}
- ref, err := app.Appender.AppendHistogram(ref, lset, t, h, fh)
- if err != nil {
+ if ref, err = app.Appender.AppendHistogram(ref, lset, t, h, fh); err != nil {
return 0, err
}
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 582b198c79..ea0aa2009f 100644
--- a/scrape/target_test.go
+++ b/scrape/target_test.go
@@ -1,4 +1,4 @@
-// Copyright 2013 The Prometheus Authors
+// 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
@@ -14,7 +14,6 @@
package scrape
import (
- "context"
"crypto/tls"
"crypto/x509"
"fmt"
@@ -37,6 +36,7 @@ import (
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/model/timestamp"
"github.com/prometheus/prometheus/storage"
+ "github.com/prometheus/prometheus/util/teststorage"
)
const (
@@ -611,37 +611,65 @@ func TestBucketLimitAppender(t *testing.T) {
},
}
- resApp := &collectResultAppender{}
-
for _, c := range cases {
for _, floatHisto := range []bool{true, false} {
t.Run(fmt.Sprintf("floatHistogram=%t", floatHisto), func(t *testing.T) {
- app := &bucketLimitAppender{Appender: resApp, 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())
+ })
})
}
}
@@ -697,65 +725,111 @@ func TestMaxSchemaAppender(t *testing.T) {
},
}
- resApp := &collectResultAppender{}
-
for _, c := range cases {
for _, floatHisto := range []bool{true, false} {
t.Run(fmt.Sprintf("floatHistogram=%t", floatHisto), func(t *testing.T) {
- app := &maxSchemaAppender{Appender: resApp, 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())
+ })
})
}
}
}
-// Test sample_limit when a scrape containst Native Histograms.
+// Test sample_limit when a scrape contains Native Histograms.
func TestAppendWithSampleLimitAndNativeHistogram(t *testing.T) {
- const sampleLimit = 2
- resApp := &collectResultAppender{}
- sl := newBasicScrapeLoop(t, context.Background(), nil, func(_ context.Context) storage.Appender {
- return resApp
- }, 0)
- sl.sampleLimit = sampleLimit
-
now := time.Now()
- app := appender(sl.appender(context.Background()), sl.sampleLimit, sl.bucketLimit, sl.maxSchema)
+ 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..4fd60b86b9 100755
--- a/scripts/check-go-mod-version.sh
+++ b/scripts/check-go-mod-version.sh
@@ -1,12 +1,71 @@
#!/usr/bin/env bash
+#
+# Description: Validate `go` directive in various Go mod files.
-readarray -t mod_files < <(find . -type f -name go.mod)
+set -u -o pipefail
+
+echo "Checking version support"
+
+version_url='https://go.dev/dl/?mode=json'
+get_supported_version() {
+ curl -s -f "${version_url}" \
+ | jq -r '.[].version' \
+ | sed 's/^go//' \
+ | cut -f2 -d'.' \
+ | sort -V \
+ | head -n1
+}
+
+get_current_version() {
+ awk '$1 == "go" {print $2}' go.mod \
+ | cut -f2 -d'.'
+}
+
+supported_version="$(get_supported_version)"
+if [[ "${supported_version}" -le 0 ]]; then
+ echo "Error getting supported version from '${version_url}'"
+ exit 1
+fi
+current_version="$(get_current_version)"
+if [[ "${current_version}" -le 0 ]]; then
+ echo "Error getting current version from go.mod"
+ exit 1
+fi
+
+if [[ "${current_version}" -gt "${supported_version}" ]] ; then
+ echo "Go mod version (1.${current_version}) is newer than upstream supported version (1.${supported_version})"
+ exit 1
+fi
+
+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
+
+ci_workflow=".github/workflows/ci.yml"
+if [[ -f "${ci_workflow}" ]] && yq -e '.jobs.test_go_oldest' "${ci_workflow}" > /dev/null 2>&1; then
+ echo "Checking CI workflow test_go_oldest uses N-1 Go version"
+
+ # Extract Go version from test_go_oldest job.
+ get_test_go_oldest_version() {
+ yq '.jobs.test_go_oldest.container.image' "${ci_workflow}" \
+ | grep -oP 'golang-builder:1\.\K[0-9]+'
+ }
+
+ test_go_oldest_version="$(get_test_go_oldest_version)"
+ if [[ -z "${test_go_oldest_version}" || "${test_go_oldest_version}" -le 0 ]]; then
+ echo "Error: Could not extract Go version from test_go_oldest job in ${ci_workflow}"
+ exit 1
+ fi
+
+ if [[ "${test_go_oldest_version}" -ne "${supported_version}" ]]; then
+ echo "Error: test_go_oldest uses Go 1.${test_go_oldest_version}, but should use Go 1.${supported_version} (oldest supported version)"
+ exit 1
+ fi
+fi
diff --git a/scripts/golangci-lint.yml b/scripts/golangci-lint.yml
index 75f886d546..dc8ffd02d9 100644
--- a/scripts/golangci-lint.yml
+++ b/scripts/golangci-lint.yml
@@ -3,6 +3,7 @@
name: golangci-lint
on:
push:
+ branches: [main, master, 'release-*']
paths:
- "go.sum"
- "go.mod"
@@ -10,6 +11,7 @@ on:
- "scripts/errcheck_excludes.txt"
- ".github/workflows/golangci-lint.yml"
- ".golangci.yml"
+ tags: ['v*']
pull_request:
permissions: # added using https://github.com/step-security/secure-repo
@@ -24,13 +26,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Install Go
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
+ uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
with:
- go-version: 1.25.x
+ go-version: 1.26.x
- name: Install snmp_exporter/generator dependencies
run: sudo apt-get update && sudo apt-get -y install libsnmp-dev
if: github.repository == 'prometheus/snmp_exporter'
@@ -38,7 +40,7 @@ jobs:
id: golangci-lint-version
run: echo "version=$(make print-golangci-lint-version)" >> $GITHUB_OUTPUT
- name: Lint
- uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0
+ uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0
with:
args: --verbose
version: ${{ steps.golangci-lint-version.outputs.version }}
diff --git a/scripts/sync_repo_files.sh b/scripts/sync_repo_files.sh
index 09b0e4d93a..04735475da 100755
--- a/scripts/sync_repo_files.sh
+++ b/scripts/sync_repo_files.sh
@@ -30,6 +30,22 @@ echo_yellow() {
echo -e "${color_yellow}$@${color_none}" 1>&2
}
+repo_log_red() {
+ echo_red "${org_repo}: $@"
+}
+
+repo_log_green() {
+ echo_green "${org_repo}: $@"
+}
+
+repo_log_yellow() {
+ echo_yellow "${org_repo}: $@"
+}
+
+repo_log() {
+ echo "${org_repo}: $@" 1>&2
+}
+
GITHUB_TOKEN="${GITHUB_TOKEN:-}"
if [ -z "${GITHUB_TOKEN}" ]; then
echo_red 'GitHub token (GITHUB_TOKEN) not set. Terminating.'
@@ -112,28 +128,28 @@ process_repo() {
local org_repo
local default_branch
org_repo="$1"
- echo_green "Analyzing '${org_repo}'"
+ repo_log_green "Analyzing '${org_repo}'"
default_branch="$(get_default_branch "${org_repo}")"
if [[ -z "${default_branch}" ]]; then
- echo "Can't get the default branch."
+ repo_log_red "Can't get the default branch."
return
fi
- echo "Default branch: ${default_branch}"
+ repo_log "Default branch: ${default_branch}"
local needs_update=()
for source_file in ${SYNC_FILES}; do
source_checksum="$(sha256sum "${source_dir}/${source_file}" | cut -d' ' -f1)"
if [[ "${source_file}" == 'scripts/golangci-lint.yml' ]] && ! check_go "${org_repo}" "${default_branch}" ; then
- echo "${org_repo} is not Go, skipping golangci-lint.yml."
+ repo_log "${org_repo} is not Go, skipping golangci-lint.yml."
continue
fi
if [[ "${source_file}" == '.github/workflows/container_description.yml' ]] && ! check_docker "${org_repo}" "${default_branch}" ; then
- echo "${org_repo} has no Dockerfile, skipping container_description.yml."
+ repo_log "${org_repo} has no Dockerfile, skipping container_description.yml."
continue
fi
if [[ "${source_file}" == 'LICENSE' ]] && ! check_license "${target_file}" ; then
- echo "LICENSE in ${org_repo} is not apache, skipping."
+ repo_log "LICENSE in ${org_repo} is not apache, skipping."
continue
fi
target_filename="${source_file}"
@@ -142,10 +158,10 @@ process_repo() {
fi
target_file="$(curl -sL --fail "https://raw.githubusercontent.com/${org_repo}/${default_branch}/${target_filename}")"
if [[ -z "${target_file}" ]]; then
- echo "${target_filename} doesn't exist in ${org_repo}"
+ repo_log "${target_filename} doesn't exist in ${org_repo}"
case "${source_file}" in
CODE_OF_CONDUCT.md | SECURITY.md | .github/workflows/container_description.yml)
- echo "${source_file} missing in ${org_repo}, force updating."
+ repo_log_yellow "${source_file} missing in ${org_repo}, force updating."
needs_update+=("${source_file}")
;;
esac
@@ -153,15 +169,15 @@ process_repo() {
fi
target_checksum="$(echo "${target_file}" | sha256sum | cut -d' ' -f1)"
if [ "${source_checksum}" == "${target_checksum}" ]; then
- echo "${source_file} is already in sync."
+ repo_log_green "${source_file} is already in sync."
continue
fi
- echo "${source_file} needs updating."
+ repo_log_yellow "${source_file} needs updating."
needs_update+=("${source_file}")
done
if [[ "${#needs_update[@]}" -eq 0 ]] ; then
- echo "No files need sync."
+ repo_log_green "No files need sync."
return
fi
@@ -184,17 +200,21 @@ process_repo() {
esac
done
+ repo_log "File sync complete"
+
if [[ -n "$(git status --porcelain)" ]]; then
git config user.email "${git_mail}"
git config user.name "${git_user}"
git add .
git commit -s -m "${commit_msg}"
+ repo_log "Commit created"
if push_branch "${org_repo}"; then
if ! post_pull_request "${org_repo}" "${default_branch}"; then
+ repo_log_red "Posting PR failed"
return 1
fi
else
- echo "Pushing ${branch} to ${org_repo} failed"
+ repo_log_red "Pushing ${branch} to ${org_repo} failed"
return 1
fi
fi
diff --git a/storage/buffer.go b/storage/buffer.go
index bc27948fd0..cdf8879f21 100644
--- a/storage/buffer.go
+++ b/storage/buffer.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
@@ -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 259e54d6f7..61d1601bc0 100644
--- a/storage/buffer_test.go
+++ b/storage/buffer_test.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
@@ -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/errors.go b/storage/errors.go
index dd48066db6..4dd61e2523 100644
--- a/storage/errors.go
+++ b/storage/errors.go
@@ -1,4 +1,4 @@
-// Copyright 2014 The Prometheus Authors
+// 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
diff --git a/storage/errors_test.go b/storage/errors_test.go
index b3e202b49b..706719d137 100644
--- a/storage/errors_test.go
+++ b/storage/errors_test.go
@@ -1,4 +1,4 @@
-// Copyright 2014 The Prometheus Authors
+// 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
@@ -20,6 +20,24 @@ import (
"github.com/stretchr/testify/require"
)
+func TestAppendPartialErrorToError(t *testing.T) {
+ // nil receiver returns nil.
+ var nilErr *AppendPartialError
+ require.NoError(t, nilErr.ToError())
+
+ // Empty ExemplarErrors returns nil.
+ emptyErr := &AppendPartialError{}
+ require.NoError(t, emptyErr.ToError())
+
+ // Also test explicitly empty slice.
+ emptySliceErr := &AppendPartialError{ExemplarErrors: []error{}}
+ require.NoError(t, emptySliceErr.ToError())
+
+ // Non-empty ExemplarErrors returns the error.
+ nonEmptyErr := &AppendPartialError{ExemplarErrors: []error{ErrOutOfOrderExemplar}}
+ require.ErrorIs(t, nonEmptyErr.ToError(), nonEmptyErr)
+}
+
func TestErrDuplicateSampleForTimestamp(t *testing.T) {
// All errDuplicateSampleForTimestamp are ErrDuplicateSampleForTimestamp
require.ErrorIs(t, ErrDuplicateSampleForTimestamp, errDuplicateSampleForTimestamp{})
diff --git a/storage/fanout.go b/storage/fanout.go
index f99edb473a..21f5f715e4 100644
--- a/storage/fanout.go
+++ b/storage/fanout.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
@@ -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.
@@ -199,14 +220,14 @@ func (f *fanoutAppender) AppendHistogram(ref SeriesRef, l labels.Labels, t int64
return ref, nil
}
-func (f *fanoutAppender) AppendHistogramCTZeroSample(ref SeriesRef, l labels.Labels, t, ct int64, h *histogram.Histogram, fh *histogram.FloatHistogram) (SeriesRef, error) {
- ref, err := f.primary.AppendHistogramCTZeroSample(ref, l, t, ct, h, fh)
+func (f *fanoutAppender) AppendHistogramSTZeroSample(ref SeriesRef, l labels.Labels, t, st int64, h *histogram.Histogram, fh *histogram.FloatHistogram) (SeriesRef, error) {
+ ref, err := f.primary.AppendHistogramSTZeroSample(ref, l, t, st, h, fh)
if err != nil {
return ref, err
}
for _, appender := range f.secondaries {
- if _, err := appender.AppendHistogramCTZeroSample(ref, l, t, ct, h, fh); err != nil {
+ if _, err := appender.AppendHistogramSTZeroSample(ref, l, t, st, h, fh); err != nil {
return 0, err
}
}
@@ -227,14 +248,14 @@ func (f *fanoutAppender) UpdateMetadata(ref SeriesRef, l labels.Labels, m metada
return ref, nil
}
-func (f *fanoutAppender) AppendCTZeroSample(ref SeriesRef, l labels.Labels, t, ct int64) (SeriesRef, error) {
- ref, err := f.primary.AppendCTZeroSample(ref, l, t, ct)
+func (f *fanoutAppender) AppendSTZeroSample(ref SeriesRef, l labels.Labels, t, st int64) (SeriesRef, error) {
+ ref, err := f.primary.AppendSTZeroSample(ref, l, t, st)
if err != nil {
return ref, err
}
for _, appender := range f.secondaries {
- if _, err := appender.AppendCTZeroSample(ref, l, t, ct); err != nil {
+ if _, err := appender.AppendSTZeroSample(ref, l, t, st); err != nil {
return 0, err
}
}
@@ -268,5 +289,61 @@ 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) {
+ var partialErr *AppendPartialError
+
+ ref, err := f.primary.Append(ref, l, st, t, v, h, fh, opts)
+ partialErr, err = partialErr.Handle(err)
+ if err != nil {
+ return ref, err
+ }
+
+ for _, appender := range f.secondaries {
+ _, serr := appender.Append(ref, l, st, t, v, h, fh, opts)
+ partialErr, serr = partialErr.Handle(serr)
+ if serr != nil {
+ return ref, serr
+ }
+ }
+ return ref, partialErr.ToError()
+}
+
+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 b1762ec555..027511aa3a 100644
--- a/storage/fanout_test.go
+++ b/storage/fanout_test.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
@@ -16,16 +16,20 @@ package storage_test
import (
"context"
"errors"
+ "strconv"
"testing"
"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) {
@@ -36,7 +40,6 @@ func TestFanout_SelectSorted(t *testing.T) {
ctx := context.Background()
priStorage := teststorage.New(t)
- defer priStorage.Close()
app1 := priStorage.Appender(ctx)
app1.Append(0, inputLabel, 0, 0)
inputTotalSize++
@@ -48,7 +51,6 @@ func TestFanout_SelectSorted(t *testing.T) {
require.NoError(t, err)
remoteStorage1 := teststorage.New(t)
- defer remoteStorage1.Close()
app2 := remoteStorage1.Appender(ctx)
app2.Append(0, inputLabel, 3000, 3)
inputTotalSize++
@@ -60,7 +62,6 @@ func TestFanout_SelectSorted(t *testing.T) {
require.NoError(t, err)
remoteStorage2 := teststorage.New(t)
- defer remoteStorage2.Close()
app3 := remoteStorage2.Appender(ctx)
app3.Append(0, inputLabel, 6000, 6)
@@ -132,9 +133,113 @@ 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)
+ 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)
+ 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)
+ 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()
cases := []struct {
primary storage.Storage
@@ -224,9 +329,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 +351,254 @@ 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())
+ })
+ }
+}
+
+// Recommended CLI invocation:
+/*
+ export bench=fanoutAppender && go test ./storage/... \
+ -run '^$' -bench '^BenchmarkFanoutAppenderV2' \
+ -benchtime 2s -count 6 -cpu 2 -timeout 999m \
+ | tee ${bench}.txt
+*/
+func BenchmarkFanoutAppenderV2(b *testing.B) {
+ ex := []exemplar.Exemplar{{Value: 1}}
+
+ var series []labels.Labels
+ for i := range 1000 {
+ series = append(series, labels.FromStrings(model.MetricNameLabel, "metric1", "i", strconv.Itoa(i)))
+ }
+ for _, tt := range fanoutAppenderTestCases(nil) {
+ // Turn our mock appender into ~noop for no allocs.
+ tt.primary.SkipRecording(true)
+ tt.secondary.SkipRecording(true)
+
+ b.Run(tt.name, func(b *testing.B) {
+ f := storage.NewFanout(nil, mockStorage{appV2: tt.primary}, mockStorage{appV2: tt.secondary})
+
+ b.ReportAllocs()
+ b.ResetTimer()
+ for b.Loop() {
+ app := f.AppenderV2(b.Context())
+ for _, s := range series {
+ // Purposefully skip errors as we want to benchmark error cases too (majority of the fanout logic).
+ _, _ = app.Append(0, s, 0, 0, 1, nil, nil, storage.AOptions{
+ Exemplars: ex,
+ })
+ }
+ require.NoError(b, app.Rollback())
+ }
+ })
+ }
+}
diff --git a/storage/generic.go b/storage/generic.go
index e5f4b4d03a..e85ac77b9c 100644
--- a/storage/generic.go
+++ b/storage/generic.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
diff --git a/storage/interface.go b/storage/interface.go
index 9d7e5d93a6..d15ba547c8 100644
--- a/storage/interface.go
+++ b/storage/interface.go
@@ -1,4 +1,4 @@
-// Copyright 2014 The Prometheus Authors
+// 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
@@ -44,13 +44,14 @@ var (
ErrExemplarsDisabled = errors.New("exemplar storage is disabled or max exemplars is less than or equal to 0")
ErrNativeHistogramsDisabled = errors.New("native histograms are disabled")
- // ErrOutOfOrderCT indicates failed append of CT to the storage
- // due to CT being older the then newer sample.
+ // ErrOutOfOrderST indicates failed append of ST to the storage
+ // due to ST being older the then newer sample.
// NOTE(bwplotka): This can be both an instrumentation failure or commonly expected
// behaviour, and we currently don't have a way to determine this. As a result
// it's recommended to ignore this error for now.
- ErrOutOfOrderCT = errors.New("created timestamp out of order, ignoring")
- ErrCTNewerThanSample = errors.New("CT is newer or the same as sample's timestamp, ignoring")
+ // TODO(bwplotka): Remove with appender v1 flow; not used in v2.
+ ErrOutOfOrderST = errors.New("start timestamp out of order, ignoring")
+ ErrSTNewerThanSample = errors.New("ST is newer or the same as sample's timestamp, ignoring")
)
// SeriesRef is a generic series reference. In prometheus it is either a
@@ -58,11 +59,15 @@ var (
// their own reference types.
type SeriesRef uint64
-// Appendable allows creating appenders.
+// Appendable allows creating Appender.
+//
+// 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. The implementation
- // can choose whether or not to use the context, for deadlines or to check
- // for errors.
+ // Appender returns a new appender for the storage.
+ //
+ // Implementations CAN choose whether to use the context e.g. for deadlines,
+ // but it's not mandatory.
Appender(ctx context.Context) Appender
}
@@ -73,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)
@@ -255,7 +266,14 @@ func (f QueryableFunc) Querier(mint, maxt int64) (Querier, error) {
return f(mint, maxt)
}
+// AppendOptions provides options for implementations of the Appender interface.
+//
+// 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
+ // error.
DiscardOutOfOrder bool
}
@@ -264,9 +282,15 @@ type AppendOptions struct {
//
// Operations on the Appender interface are not goroutine-safe.
//
-// The type of samples (float64, histogram, etc) appended for a given series must remain same within an Appender.
-// The behaviour is undefined if samples of different types are appended to the same series in a single Commit().
+// The order of samples appended via the Appender is preserved within each series.
+// I.e. timestamp order within batch is not validated, samples are not reordered per timestamp or by float/histogram
+// type.
+//
+// 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
+
// Append adds a sample pair for the given series.
// An optional series reference can be provided to accelerate calls.
// A series reference number is returned which can be used to add further
@@ -277,16 +301,6 @@ type Appender interface {
// If the reference is 0 it must not be used for caching.
Append(ref SeriesRef, l labels.Labels, t int64, v float64) (SeriesRef, error)
- // Commit submits the collected samples and purges the batch. If Commit
- // returns a non-nil error, it also rolls back all modifications made in
- // the appender so far, as Rollback would do. In any case, an Appender
- // must not be used anymore after Commit has been called.
- Commit() error
-
- // Rollback rolls back all modifications made in the appender so far.
- // Appender has to be discarded after rollback.
- Rollback() error
-
// SetOptions configures the appender with specific append options such as
// discarding out-of-order samples even if out-of-order is enabled in the TSDB.
SetOptions(opts *AppendOptions)
@@ -294,14 +308,14 @@ type Appender interface {
ExemplarAppender
HistogramAppender
MetadataUpdater
- CreatedTimestampAppender
+ StartTimestampAppender
}
// GetRef is an extra interface on Appenders used by downstream projects
// (e.g. Cortex) to avoid maintaining a parallel set of references.
type GetRef interface {
- // Returns reference number that can be used to pass to Appender.Append(),
- // and a set of labels that will not cause another copy when passed to Appender.Append().
+ // GetRef returns a reference number that can be used to pass to AppenderV2.Append(),
+ // and a set of labels that will not cause another copy when passed to AppenderV2.Append().
// 0 means the appender does not have a reference to this series.
// hash should be a hash of lset.
GetRef(lset labels.Labels, hash uint64) (SeriesRef, labels.Labels)
@@ -309,6 +323,9 @@ type GetRef interface {
// ExemplarAppender provides an interface for adding samples to exemplar storage, which
// within Prometheus is in-memory only.
+//
+// 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.
@@ -325,6 +342,9 @@ type ExemplarAppender interface {
}
// HistogramAppender provides an interface for appending histograms to the storage.
+//
+// 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
@@ -338,23 +358,26 @@ type HistogramAppender interface {
// pointer. AppendHistogram won't mutate the histogram, but in turn
// depends on the caller to not mutate it either.
AppendHistogram(ref SeriesRef, l labels.Labels, t int64, h *histogram.Histogram, fh *histogram.FloatHistogram) (SeriesRef, error)
- // AppendHistogramCTZeroSample adds synthetic zero sample for the given ct timestamp,
+ // AppendHistogramSTZeroSample adds synthetic zero sample for the given st timestamp,
// which will be associated with given series, labels and the incoming
- // sample's t (timestamp). AppendHistogramCTZeroSample returns error if zero sample can't be
- // appended, for example when ct is too old, or when it would collide with
+ // sample's t (timestamp). AppendHistogramSTZeroSample returns error if zero sample can't be
+ // appended, for example when st is too old, or when it would collide with
// incoming sample (sample has priority).
//
- // AppendHistogramCTZeroSample has to be called before the corresponding histogram AppendHistogram.
+ // AppendHistogramSTZeroSample has to be called before the corresponding histogram AppendHistogram.
// A series reference number is returned which can be used to modify the
- // CT for the given series in the same or later transactions.
+ // ST for the given series in the same or later transactions.
// Returned reference numbers are ephemeral and may be rejected in calls
- // to AppendHistogramCTZeroSample() at any point.
+ // to AppendHistogramSTZeroSample() at any point.
//
// If the reference is 0 it must not be used for caching.
- AppendHistogramCTZeroSample(ref SeriesRef, l labels.Labels, t, ct int64, h *histogram.Histogram, fh *histogram.FloatHistogram) (SeriesRef, error)
+ AppendHistogramSTZeroSample(ref SeriesRef, l labels.Labels, t, st int64, h *histogram.Histogram, fh *histogram.FloatHistogram) (SeriesRef, error)
}
// MetadataUpdater provides an interface for associating metadata to stored series.
+//
+// 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
@@ -366,22 +389,25 @@ type MetadataUpdater interface {
UpdateMetadata(ref SeriesRef, l labels.Labels, m metadata.Metadata) (SeriesRef, error)
}
-// CreatedTimestampAppender provides an interface for appending CT to storage.
-type CreatedTimestampAppender interface {
- // AppendCTZeroSample adds synthetic zero sample for the given ct timestamp,
+// StartTimestampAppender provides an interface for appending ST to storage.
+//
+// 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
- // sample's t (timestamp). AppendCTZeroSample returns error if zero sample can't be
- // appended, for example when ct is too old, or when it would collide with
+ // sample's t (timestamp). AppendSTZeroSample returns error if zero sample can't be
+ // appended, for example when st is too old, or when it would collide with
// incoming sample (sample has priority).
//
- // AppendCTZeroSample has to be called before the corresponding sample Append.
+ // AppendSTZeroSample has to be called before the corresponding sample Append.
// A series reference number is returned which can be used to modify the
- // CT for the given series in the same or later transactions.
+ // ST for the given series in the same or later transactions.
// Returned reference numbers are ephemeral and may be rejected in calls
- // to AppendCTZeroSample() at any point.
+ // to AppendSTZeroSample() at any point.
//
// If the reference is 0 it must not be used for caching.
- AppendCTZeroSample(ref SeriesRef, l labels.Labels, t, ct int64) (SeriesRef, error)
+ AppendSTZeroSample(ref SeriesRef, l labels.Labels, t, st int64) (SeriesRef, error)
}
// SeriesSet contains a set of series.
@@ -389,10 +415,10 @@ type SeriesSet interface {
Next() bool
// At returns full series. Returned series should be iterable even after Next is called.
At() Series
- // The error that iteration has failed with.
+ // Err returns the error that iteration has failed with.
// When an error occurs, set cannot continue to iterate.
Err() error
- // A collection of warnings for the whole set.
+ // Warnings returns a collection of warnings for the whole set.
// Warnings could be return even iteration has not failed with error.
Warnings() annotations.Annotations
}
@@ -460,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 {
@@ -470,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
new file mode 100644
index 0000000000..3753544eb0
--- /dev/null
+++ b/storage/interface_append.go
@@ -0,0 +1,231 @@
+// 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 storage
+
+import (
+ "context"
+ "errors"
+
+ "github.com/prometheus/prometheus/model/exemplar"
+ "github.com/prometheus/prometheus/model/histogram"
+ "github.com/prometheus/prometheus/model/labels"
+ "github.com/prometheus/prometheus/model/metadata"
+)
+
+// AppendableV2 allows creating AppenderV2.
+type AppendableV2 interface {
+ // AppenderV2 returns a new appender for the storage.
+ //
+ // Implementations CAN choose whether to use the context e.g. for deadlines,
+ // but it's not mandatory.
+ AppenderV2(ctx context.Context) AppenderV2
+}
+
+// AOptions is a shorthand for AppendV2Options.
+// NOTE: AppendOption is used already.
+type AOptions = AppendV2Options
+
+// AppendV2Options provides optional, auxiliary data and configuration for AppenderV2.Append.
+type AppendV2Options struct {
+ // MetricFamilyName (optional) provides metric family name for the appended sample's
+ // series. If the client of the AppenderV2 has this information
+ // (e.g. from scrape) it's recommended to pass it to the appender.
+ //
+ // Provided string bytes are unsafe to reuse, it only lives for the duration of the Append call.
+ //
+ // Some implementations use this to avoid slow and prone to error metric family detection for:
+ // * Metadata per metric family storages (e.g. Prometheus metadata WAL/API/RW1)
+ // * Strictly complex types storages (e.g. OpenTelemetry Collector).
+ //
+ // NOTE(krajorama): Example purpose is highlighted in OTLP ingestion: OTLP calculates the
+ // metric family name for all metrics and uses it for generating summary,
+ // histogram series by adding the magic suffixes. The metric family name is
+ // passed down to the appender in case the storage needs it for metadata updates.
+ // Known user of this is Mimir that implements /api/v1/metadata and uses
+ // Remote-Write 1.0 for this. Might be removed later if no longer
+ // needed by any downstream project.
+ // NOTE(bwplotka): Long term, once Prometheus uses complex types on storage level
+ // the MetricFamilyName can be removed as MetricFamilyName will equal to __name__ always.
+ MetricFamilyName string
+
+ // Metadata (optional) attached to the appended sample.
+ // Metadata strings are safe for reuse.
+ // IMPORTANT: Appender v1 was only providing update. This field MUST be
+ // set (if known) even if it didn't change since the last iteration.
+ // This moves the responsibility for metadata storage options to TSDB.
+ Metadata metadata.Metadata
+
+ // 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
+ // of order. An OOO append MUST be rejected with storage.ErrOutOfOrderSample
+ // error.
+ RejectOutOfOrder bool
+}
+
+// AppendPartialError represents an AppenderV2.Append error that tells
+// callers sample was written but some auxiliary optional data (e.g. exemplars)
+// was not (or partially written)
+//
+// It's up to the caller to decide if it's an ignorable error or not, plus
+// it allows extra reporting (e.g. for Remote Write 2.0 X-Remote-Write-Written headers).
+type AppendPartialError struct {
+ ExemplarErrors []error
+}
+
+// Error returns combined error string.
+func (e *AppendPartialError) Error() string {
+ if e == nil {
+ return ""
+ }
+
+ errs := errors.Join(e.ExemplarErrors...)
+ if errs == nil {
+ return ""
+ }
+ return errs.Error()
+}
+
+// ToError returns AppendPartialError as error, returning nil
+// if there are no errors.
+func (e *AppendPartialError) ToError() error {
+ if e == nil || len(e.ExemplarErrors) == 0 {
+ return nil
+ }
+ return e
+}
+
+// Is implements method that's expected by errors.Is.
+func (*AppendPartialError) Is(target error) bool {
+ // This does not need to handle wrapped errors as AppendPartialError.Is should be used
+ // via errors.Is.
+ _, ok := target.(*AppendPartialError)
+ return ok
+}
+
+// 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) (*AppendPartialError, error) {
+ if err == nil {
+ return e, nil
+ }
+
+ // Fast, alloc-free path first for non-partial error cases.
+ if !errors.Is(err, e) {
+ return e, err
+ }
+ var pErr *AppendPartialError
+ if !errors.As(err, &pErr) {
+ return e, err
+ }
+
+ if e == nil {
+ // Lazy allocation.
+ e = &AppendPartialError{}
+ }
+ e.ExemplarErrors = append(e.ExemplarErrors, pErr.ExemplarErrors...)
+ return e, nil
+}
+
+var _ error = &AppendPartialError{}
+
+// AppenderV2 provides appends against a storage for all types of samples.
+// It must be completed with a call to Commit or Rollback and must not be reused afterwards.
+//
+// Operations on the AppenderV2 interface are not goroutine-safe.
+//
+// The order of samples appended via the AppenderV2 is preserved within each series.
+// I.e. timestamp order within batch is not validated, samples are not reordered per timestamp or by float/histogram
+// type.
+type AppenderV2 interface {
+ AppenderTransaction
+
+ // Append appends a sample and related exemplars, metadata, and start timestamp (st) to the storage.
+ //
+ // ref (optional) represents the stable ID for the given series identified by ls (excluding metadata).
+ // Callers MAY provide the ref to help implementation avoid ls -> ref computation, otherwise ref MUST be 0 (unknown).
+ //
+ // ls represents labels for the sample's series.
+ //
+ // st (optional) represents sample start timestamp. 0 means unknown. Implementations
+ // are responsible for any potential ST storage logic (e.g. ST zero injections).
+ //
+ // t represents sample timestamp.
+ //
+ // v, h, fh represents sample value for each sample type.
+ // Callers MUST only provide one of the sample types (either v, h or fh).
+ // Implementations can detect the type of the sample with the following switch:
+ //
+ // switch {
+ // case fh != nil: It's a float histogram append.
+ // case h != nil: It's a histogram append.
+ // default: It's a float append.
+ // }
+ // TODO(bwplotka): We plan to experiment on using generics for complex sampleType, but do it after we unify interface (derisk) and before we add native summaries.
+ //
+ // Implementations MUST attempt to append sample even if metadata, exemplar or (st) start timestamp appends fail.
+ // Implementations MAY return AppendPartialError as an error. Use errors.As to detect.
+ // For the successful Append, Implementations MUST return valid SeriesRef that represents ls.
+ // NOTE(bwplotka): Given OTLP and native histograms and the relaxation of the requirement for
+ // type and unit suffixes in metric names we start to hit cases of ls being not enough for id
+ // of the series (metadata matters). Current solution is to enable 'type-and-unit-label' features for those cases, but we may
+ // start to extend the id with metadata one day.
+ Append(ref SeriesRef, ls labels.Labels, st, t int64, v float64, h *histogram.Histogram, fh *histogram.FloatHistogram, opts AppendV2Options) (SeriesRef, error)
+}
+
+// AppenderTransaction allows transactional appends.
+type AppenderTransaction interface {
+ // Commit submits the collected samples and purges the batch. If Commit
+ // returns a non-nil error, it also rolls back all modifications made in
+ // the appender so far, as Rollback would do. In any case, an Appender
+ // must not be used anymore after Commit has been called.
+ Commit() error
+
+ // Rollback rolls back all modifications made in the appender so far.
+ // Appender has to be discarded after rollback.
+ Rollback() error
+}
+
+// LimitedAppenderV1 is an Appender that only supports appending float and histogram samples.
+// This is to support migration to AppenderV2.
+// TODO(bwplotka): Remove once migration to AppenderV2 is fully complete.
+type LimitedAppenderV1 interface {
+ AppenderTransaction
+
+ Append(ref SeriesRef, l labels.Labels, t int64, v float64) (SeriesRef, error)
+ AppendHistogram(ref SeriesRef, l labels.Labels, t int64, h *histogram.Histogram, fh *histogram.FloatHistogram) (SeriesRef, error)
+}
+
+// AppenderV2AsLimitedV1 returns appender that exposes AppenderV2 as LimitedAppenderV1
+// TODO(bwplotka): Remove once migration to AppenderV2 is fully complete.
+func AppenderV2AsLimitedV1(app AppenderV2) LimitedAppenderV1 {
+ return &limitedAppenderV1{AppenderV2: app}
+}
+
+type limitedAppenderV1 struct {
+ AppenderV2
+}
+
+func (a *limitedAppenderV1) Append(ref SeriesRef, l labels.Labels, t int64, v float64) (SeriesRef, error) {
+ return a.AppenderV2.Append(ref, l, 0, t, v, nil, nil, AppendV2Options{})
+}
+
+func (a *limitedAppenderV1) AppendHistogram(ref SeriesRef, l labels.Labels, t int64, h *histogram.Histogram, fh *histogram.FloatHistogram) (SeriesRef, error) {
+ return a.AppenderV2.Append(ref, l, 0, t, 0, h, fh, AppendV2Options{})
+}
diff --git a/storage/interface_test.go b/storage/interface_test.go
index ba60721736..3ea4b757e7 100644
--- a/storage/interface_test.go
+++ b/storage/interface_test.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Prometheus Authors
+// 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
@@ -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/lazy.go b/storage/lazy.go
index fab974c286..2851ba7135 100644
--- a/storage/lazy.go
+++ b/storage/lazy.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
diff --git a/storage/memoized_iterator.go b/storage/memoized_iterator.go
index 273b3caa1d..b248bca641 100644
--- a/storage/memoized_iterator.go
+++ b/storage/memoized_iterator.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
diff --git a/storage/memoized_iterator_test.go b/storage/memoized_iterator_test.go
index 81e517f96e..1a1a5f7680 100644
--- a/storage/memoized_iterator_test.go
+++ b/storage/memoized_iterator_test.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
diff --git a/storage/merge.go b/storage/merge.go
index f8ba1ab76a..76bf0994e0 100644
--- a/storage/merge.go
+++ b/storage/merge.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
@@ -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 90f2097054..e42a6a4ce1 100644
--- a/storage/merge_test.go
+++ b/storage/merge_test.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
@@ -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/noop.go b/storage/noop.go
index f5092da7c7..751e6304db 100644
--- a/storage/noop.go
+++ b/storage/noop.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
diff --git a/storage/remote/azuread/azuread.go b/storage/remote/azuread/azuread.go
index ea2a816d94..fe0c4f9e21 100644
--- a/storage/remote/azuread/azuread.go
+++ b/storage/remote/azuread/azuread.go
@@ -1,4 +1,4 @@
-// Copyright 2023 The Prometheus Authors
+// 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
@@ -103,6 +103,9 @@ type AzureADConfig struct { //nolint:revive // exported.
// Cloud is the Azure cloud in which the service is running. Example: AzurePublic/AzureGovernment/AzureChina.
Cloud string `yaml:"cloud,omitempty"`
+
+ // Scope is the custom OAuth 2.0 scope to request when acquiring tokens.
+ Scope string `yaml:"scope,omitempty"`
}
// azureADRoundTripper is used to store the roundtripper and the tokenprovider.
@@ -211,6 +214,12 @@ func (c *AzureADConfig) Validate() error {
}
}
+ if c.Scope != "" {
+ if matched, err := regexp.MatchString("^[\\w\\s:/.\\-]+$", c.Scope); err != nil || !matched {
+ return errors.New("the provided scope contains invalid characters")
+ }
+ }
+
return nil
}
@@ -360,14 +369,22 @@ func newSDKTokenCredential(clientOpts *azcore.ClientOptions, sdkConfig *SDKConfi
// newTokenProvider helps to fetch accessToken for different types of credential. This also takes care of
// refreshing the accessToken before expiry. This accessToken is attached to the Authorization header while making requests.
func newTokenProvider(cfg *AzureADConfig, cred azcore.TokenCredential) (*tokenProvider, error) {
- audience, err := getAudience(cfg.Cloud)
- if err != nil {
- return nil, err
+ var scopes []string
+
+ // Use custom scope if provided, otherwise fallback to cloud-specific audience
+ if cfg.Scope != "" {
+ scopes = []string{cfg.Scope}
+ } else {
+ audience, err := getAudience(cfg.Cloud)
+ if err != nil {
+ return nil, err
+ }
+ scopes = []string{audience}
}
tokenProvider := &tokenProvider{
credentialClient: cred,
- options: &policy.TokenRequestOptions{Scopes: []string{audience}},
+ options: &policy.TokenRequestOptions{Scopes: scopes},
}
return tokenProvider, nil
diff --git a/storage/remote/azuread/azuread_test.go b/storage/remote/azuread/azuread_test.go
index d581f0218a..857ecdba8a 100644
--- a/storage/remote/azuread/azuread_test.go
+++ b/storage/remote/azuread/azuread_test.go
@@ -1,4 +1,4 @@
-// Copyright 2023 The Prometheus Authors
+// 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
@@ -198,6 +198,11 @@ func TestAzureAdConfig(t *testing.T) {
filename: "testdata/azuread_bad_workloadidentity_missingtenantid.yaml",
err: "must provide an Azure Workload Identity tenant_id in the Azure AD config",
},
+ // Invalid scope validation.
+ {
+ filename: "testdata/azuread_bad_scope_invalid.yaml",
+ err: "the provided scope contains invalid characters",
+ },
// Valid config with missing optionally cloud field.
{
filename: "testdata/azuread_good_cloudmissing.yaml",
@@ -222,6 +227,10 @@ func TestAzureAdConfig(t *testing.T) {
{
filename: "testdata/azuread_good_workloadidentity.yaml",
},
+ // Valid OAuth config with custom scope.
+ {
+ filename: "testdata/azuread_good_oauth_customscope.yaml",
+ },
}
for _, c := range cases {
_, err := loadAzureAdConfig(c.filename)
@@ -387,3 +396,87 @@ func getToken() azcore.AccessToken {
ExpiresOn: time.Now().Add(10 * time.Second),
}
}
+
+func TestCustomScopeSupport(t *testing.T) {
+ mockCredential := new(mockCredential)
+ testToken := &azcore.AccessToken{
+ Token: testTokenString,
+ ExpiresOn: testTokenExpiry(),
+ }
+
+ cases := []struct {
+ name string
+ cfg *AzureADConfig
+ expectedScope string
+ }{
+ {
+ name: "Custom scope with OAuth",
+ cfg: &AzureADConfig{
+ Cloud: "AzurePublic",
+ OAuth: &OAuthConfig{
+ ClientID: dummyClientID,
+ ClientSecret: dummyClientSecret,
+ TenantID: dummyTenantID,
+ },
+ Scope: "https://custom-app.com/.default",
+ },
+ expectedScope: "https://custom-app.com/.default",
+ },
+ {
+ name: "Custom scope with Managed Identity",
+ cfg: &AzureADConfig{
+ Cloud: "AzurePublic",
+ ManagedIdentity: &ManagedIdentityConfig{
+ ClientID: dummyClientID,
+ },
+ Scope: "https://monitor.azure.com//.default",
+ },
+ expectedScope: "https://monitor.azure.com//.default",
+ },
+ {
+ name: "Default scope fallback with OAuth",
+ cfg: &AzureADConfig{
+ Cloud: "AzurePublic",
+ OAuth: &OAuthConfig{
+ ClientID: dummyClientID,
+ ClientSecret: dummyClientSecret,
+ TenantID: dummyTenantID,
+ },
+ },
+ expectedScope: IngestionPublicAudience,
+ },
+ {
+ name: "Default scope fallback with China cloud",
+ cfg: &AzureADConfig{
+ Cloud: "AzureChina",
+ OAuth: &OAuthConfig{
+ ClientID: dummyClientID,
+ ClientSecret: dummyClientSecret,
+ TenantID: dummyTenantID,
+ },
+ },
+ expectedScope: IngestionChinaAudience,
+ },
+ }
+
+ for _, c := range cases {
+ t.Run(c.name, func(t *testing.T) {
+ // Set up mock to capture the actual scopes used
+ mockCredential.On("GetToken", mock.Anything, mock.MatchedBy(func(options policy.TokenRequestOptions) bool {
+ return len(options.Scopes) == 1 && options.Scopes[0] == c.expectedScope
+ })).Return(*testToken, nil).Once()
+
+ tokenProvider, err := newTokenProvider(c.cfg, mockCredential)
+ require.NoError(t, err)
+ require.NotNil(t, tokenProvider)
+
+ // Verify that the token provider uses the expected scope
+ token, err := tokenProvider.getAccessToken(context.Background())
+ require.NoError(t, err)
+ require.Equal(t, testTokenString, token)
+
+ // Reset mock for next test
+ mockCredential.ExpectedCalls = nil
+ })
+ }
+}
diff --git a/storage/remote/azuread/testdata/azuread_bad_scope_invalid.yaml b/storage/remote/azuread/testdata/azuread_bad_scope_invalid.yaml
new file mode 100644
index 0000000000..2e5678d783
--- /dev/null
+++ b/storage/remote/azuread/testdata/azuread_bad_scope_invalid.yaml
@@ -0,0 +1,6 @@
+cloud: AzurePublic
+oauth:
+ client_id: 00000000-0000-0000-0000-000000000000
+ client_secret: Cl1ent$ecret!
+ tenant_id: 00000000-a12b-3cd4-e56f-000000000000
+scope: "invalid<>scope*chars"
diff --git a/storage/remote/azuread/testdata/azuread_good_oauth_customscope.yaml b/storage/remote/azuread/testdata/azuread_good_oauth_customscope.yaml
new file mode 100644
index 0000000000..f7adf8b0af
--- /dev/null
+++ b/storage/remote/azuread/testdata/azuread_good_oauth_customscope.yaml
@@ -0,0 +1,6 @@
+cloud: AzurePublic
+oauth:
+ client_id: 00000000-0000-0000-0000-000000000000
+ client_secret: Cl1ent$ecret!
+ tenant_id: 00000000-a12b-3cd4-e56f-000000000000
+scope: "https://custom-app.com/.default"
diff --git a/storage/remote/chunked.go b/storage/remote/chunked.go
index aa5addd6aa..b6cadf8691 100644
--- a/storage/remote/chunked.go
+++ b/storage/remote/chunked.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The Prometheus Authors
+// 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
diff --git a/storage/remote/chunked_test.go b/storage/remote/chunked_test.go
index 82ed866345..7493d734a3 100644
--- a/storage/remote/chunked_test.go
+++ b/storage/remote/chunked_test.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The Prometheus Authors
+// 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
diff --git a/storage/remote/client.go b/storage/remote/client.go
index c535ea3425..78405b378e 100644
--- a/storage/remote/client.go
+++ b/storage/remote/client.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The Prometheus Authors
+// 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
@@ -301,6 +301,9 @@ func (c *Client) Store(ctx context.Context, req []byte, attempt int) (WriteRespo
_ = httpResp.Body.Close()
}()
+ // NOTE(bwplotka): Only PRW2 spec defines response HTTP headers. However, spec does not block
+ // PRW1 from sending them too for reliability. Support this case.
+ //
// TODO(bwplotka): Pass logger and emit debug on error?
// Parsing error means there were some response header values we can't parse,
// we can continue handling.
diff --git a/storage/remote/client_test.go b/storage/remote/client_test.go
index 7fb670a24d..d5f126342a 100644
--- a/storage/remote/client_test.go
+++ b/storage/remote/client_test.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
diff --git a/storage/remote/codec.go b/storage/remote/codec.go
index 7e21909354..c689a51164 100644
--- a/storage/remote/codec.go
+++ b/storage/remote/codec.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
@@ -389,6 +389,11 @@ type concreteSeriesIterator struct {
curValType chunkenc.ValueType
series *concreteSeries
err error
+
+ // These are pre-filled with the current model histogram if curValType
+ // is ValHistogram or ValFloatHistogram, respectively.
+ curH *histogram.Histogram
+ curFH *histogram.FloatHistogram
}
func newConcreteSeriesIterator(series *concreteSeries) chunkenc.Iterator {
@@ -461,9 +466,7 @@ func (c *concreteSeriesIterator) Seek(t int64) chunkenc.ValueType {
c.curValType = chunkenc.ValHistogram
}
if c.curValType == chunkenc.ValHistogram {
- h := &c.series.histograms[c.histogramsCur]
- c.curValType = getHistogramValType(h)
- c.err = validateHistogramSchema(h)
+ c.setCurrentHistogram()
}
if c.err != nil {
c.curValType = chunkenc.ValNone
@@ -471,18 +474,57 @@ func (c *concreteSeriesIterator) Seek(t int64) chunkenc.ValueType {
return c.curValType
}
-func validateHistogramSchema(h *prompb.Histogram) error {
- if histogram.IsKnownSchema(h.Schema) {
- return nil
- }
- return histogram.UnknownSchemaError(h.Schema)
-}
+// setCurrentHistogram pre-fills either the curH or the curFH field with a
+// converted model histogram and sets c.curValType accordingly. It validates the
+// histogram and sets c.err accordingly. This all has to be done in Seek() and
+// Next() already so that we know if the histogram we got from the remote-read
+// source is valid or not before we allow the AtHistogram()/AtFloatHistogram()
+// call.
+func (c *concreteSeriesIterator) setCurrentHistogram() {
+ pbH := c.series.histograms[c.histogramsCur]
-func getHistogramValType(h *prompb.Histogram) chunkenc.ValueType {
- if h.IsFloatHistogram() {
- return chunkenc.ValFloatHistogram
+ // Basic schema check first.
+ schema := pbH.Schema
+ if !histogram.IsKnownSchema(schema) {
+ c.err = histogram.UnknownSchemaError(schema)
+ return
}
- return chunkenc.ValHistogram
+
+ if pbH.IsFloatHistogram() {
+ c.curValType = chunkenc.ValFloatHistogram
+ mFH := pbH.ToFloatHistogram()
+ if mFH.Schema > histogram.ExponentialSchemaMax && mFH.Schema <= histogram.ExponentialSchemaMaxReserved {
+ // This is a very slow path, but it should only happen if the
+ // sample is from a newer Prometheus version that supports higher
+ // resolution.
+ if err := mFH.ReduceResolution(histogram.ExponentialSchemaMax); err != nil {
+ c.err = err
+ return
+ }
+ }
+ if err := mFH.Validate(); err != nil {
+ c.err = err
+ return
+ }
+ c.curFH = mFH
+ return
+ }
+ c.curValType = chunkenc.ValHistogram
+ mH := pbH.ToIntHistogram()
+ if mH.Schema > histogram.ExponentialSchemaMax && mH.Schema <= histogram.ExponentialSchemaMaxReserved {
+ // This is a very slow path, but it should only happen if the
+ // sample is from a newer Prometheus version that supports higher
+ // resolution.
+ if err := mH.ReduceResolution(histogram.ExponentialSchemaMax); err != nil {
+ c.err = err
+ return
+ }
+ }
+ if err := mH.Validate(); err != nil {
+ c.err = err
+ return
+ }
+ c.curH = mH
}
// At implements chunkenc.Iterator.
@@ -499,31 +541,19 @@ func (c *concreteSeriesIterator) AtHistogram(*histogram.Histogram) (int64, *hist
if c.curValType != chunkenc.ValHistogram {
panic("iterator is not on an integer histogram sample")
}
- h := c.series.histograms[c.histogramsCur]
- mh := h.ToIntHistogram()
- if mh.Schema > histogram.ExponentialSchemaMax && mh.Schema <= histogram.ExponentialSchemaMaxReserved {
- // This is a very slow path, but it should only happen if the
- // sample is from a newer Prometheus version that supports higher
- // resolution.
- mh.ReduceResolution(histogram.ExponentialSchemaMax)
- }
- return h.Timestamp, mh
+ return c.series.histograms[c.histogramsCur].Timestamp, c.curH
}
// AtFloatHistogram implements chunkenc.Iterator.
func (c *concreteSeriesIterator) AtFloatHistogram(*histogram.FloatHistogram) (int64, *histogram.FloatHistogram) {
- if c.curValType == chunkenc.ValHistogram || c.curValType == chunkenc.ValFloatHistogram {
- fh := c.series.histograms[c.histogramsCur]
- mfh := fh.ToFloatHistogram() // integer will be auto-converted.
- if mfh.Schema > histogram.ExponentialSchemaMax && mfh.Schema <= histogram.ExponentialSchemaMaxReserved {
- // This is a very slow path, but it should only happen if the
- // sample is from a newer Prometheus version that supports higher
- // resolution.
- mfh.ReduceResolution(histogram.ExponentialSchemaMax)
- }
- return fh.Timestamp, mfh
+ switch c.curValType {
+ case chunkenc.ValFloatHistogram:
+ return c.series.histograms[c.histogramsCur].Timestamp, c.curFH
+ case chunkenc.ValHistogram:
+ return c.series.histograms[c.histogramsCur].Timestamp, c.curH.ToFloat(nil)
+ default:
+ panic("iterator is not on a histogram sample")
}
- panic("iterator is not on a histogram sample")
}
// AtT implements chunkenc.Iterator.
@@ -534,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.
@@ -571,9 +607,7 @@ func (c *concreteSeriesIterator) Next() chunkenc.ValueType {
}
if c.curValType == chunkenc.ValHistogram {
- h := &c.series.histograms[c.histogramsCur]
- c.curValType = getHistogramValType(h)
- c.err = validateHistogramSchema(h)
+ c.setCurrentHistogram()
}
if c.err != nil {
c.curValType = chunkenc.ValNone
@@ -804,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 ddf8f76cf6..5da8c8176c 100644
--- a/storage/remote/codec_test.go
+++ b/storage/remote/codec_test.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
@@ -114,7 +114,7 @@ var (
HelpRef: 15, // Symbolized writeV2RequestSeries1Metadata.Help.
UnitRef: 16, // Symbolized writeV2RequestSeries1Metadata.Unit.
},
- Samples: []writev2.Sample{{Value: 1, Timestamp: 10}},
+ Samples: []writev2.Sample{{Value: 1, Timestamp: 10, StartTimestamp: 1}}, // ST needs to be lower than the sample's timestamp.
Exemplars: []writev2.Exemplar{{LabelsRefs: []uint32{11, 12}, Value: 1, Timestamp: 10}},
Histograms: []writev2.Histogram{
writev2.FromIntHistogram(10, &testHistogram),
@@ -122,7 +122,6 @@ var (
writev2.FromIntHistogram(30, &testHistogramCustomBuckets),
writev2.FromFloatHistogram(40, testHistogramCustomBuckets.ToFloat(nil)),
},
- CreatedTimestamp: 1, // CT needs to be lower than the sample's timestamp.
},
{
LabelsRefs: []uint32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, // Same series as first.
@@ -182,7 +181,7 @@ func TestWriteV2RequestFixture(t *testing.T) {
HelpRef: st.Symbolize(writeV2RequestSeries1Metadata.Help),
UnitRef: st.Symbolize(writeV2RequestSeries1Metadata.Unit),
},
- Samples: []writev2.Sample{{Value: 1, Timestamp: 10}},
+ Samples: []writev2.Sample{{Value: 1, Timestamp: 10, StartTimestamp: 1}},
Exemplars: []writev2.Exemplar{{LabelsRefs: exemplar1LabelRefs, Value: 1, Timestamp: 10}},
Histograms: []writev2.Histogram{
writev2.FromIntHistogram(10, &testHistogram),
@@ -190,7 +189,6 @@ func TestWriteV2RequestFixture(t *testing.T) {
writev2.FromIntHistogram(30, &testHistogramCustomBuckets),
writev2.FromFloatHistogram(40, testHistogramCustomBuckets.ToFloat(nil)),
},
- CreatedTimestamp: 1,
},
{
LabelsRefs: labelRefs,
@@ -548,7 +546,7 @@ func TestConcreteSeriesIterator_FloatAndHistogramSamples(t *testing.T) {
require.Equal(t, chunkenc.ValNone, it.Seek(1))
}
-func TestConcreteSeriesIterator_InvalidHistogramSamples(t *testing.T) {
+func TestConcreteSeriesIterator_HistogramSamplesWithInvalidSchema(t *testing.T) {
for _, schema := range []int32{-100, 100} {
t.Run(fmt.Sprintf("schema=%d", schema), func(t *testing.T) {
h := prompb.FromIntHistogram(2, &testHistogram)
@@ -593,6 +591,47 @@ func TestConcreteSeriesIterator_InvalidHistogramSamples(t *testing.T) {
}
}
+func TestConcreteSeriesIterator_HistogramSamplesWithMissingBucket(t *testing.T) {
+ mh := testHistogram.Copy()
+ mh.PositiveSpans = []histogram.Span{{Offset: 0, Length: 2}}
+ h := prompb.FromIntHistogram(2, mh)
+ fh := prompb.FromFloatHistogram(4, mh.ToFloat(nil))
+ series := &concreteSeries{
+ labels: labels.FromStrings("foo", "bar"),
+ floats: []prompb.Sample{
+ {Value: 1, Timestamp: 0},
+ {Value: 2, Timestamp: 3},
+ },
+ histograms: []prompb.Histogram{
+ h,
+ fh,
+ },
+ }
+ it := series.Iterator(nil)
+ require.Equal(t, chunkenc.ValFloat, it.Next())
+ require.Equal(t, chunkenc.ValNone, it.Next())
+ require.Error(t, it.Err())
+ require.ErrorIs(t, it.Err(), histogram.ErrHistogramSpansBucketsMismatch)
+
+ it = series.Iterator(it)
+ require.Equal(t, chunkenc.ValFloat, it.Next())
+ require.Equal(t, chunkenc.ValNone, it.Next())
+ require.ErrorIs(t, it.Err(), histogram.ErrHistogramSpansBucketsMismatch)
+
+ it = series.Iterator(it)
+ require.Equal(t, chunkenc.ValNone, it.Seek(1))
+ require.ErrorIs(t, it.Err(), histogram.ErrHistogramSpansBucketsMismatch)
+
+ it = series.Iterator(it)
+ require.Equal(t, chunkenc.ValFloat, it.Seek(3))
+ require.Equal(t, chunkenc.ValNone, it.Next())
+ require.ErrorIs(t, it.Err(), histogram.ErrHistogramSpansBucketsMismatch)
+
+ it = series.Iterator(it)
+ require.Equal(t, chunkenc.ValNone, it.Seek(4))
+ require.ErrorIs(t, it.Err(), histogram.ErrHistogramSpansBucketsMismatch)
+}
+
func TestConcreteSeriesIterator_ReducesHighResolutionHistograms(t *testing.T) {
for _, schema := range []int32{9, 52} {
t.Run(fmt.Sprintf("schema=%d", schema), func(t *testing.T) {
@@ -1107,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/dial_context.go b/storage/remote/dial_context.go
index b842728e4c..f7a52442ed 100644
--- a/storage/remote/dial_context.go
+++ b/storage/remote/dial_context.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Prometheus Authors
+// 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
diff --git a/storage/remote/dial_context_test.go b/storage/remote/dial_context_test.go
index 5a0cd7c88c..61b929401f 100644
--- a/storage/remote/dial_context_test.go
+++ b/storage/remote/dial_context_test.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Prometheus Authors
+// 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
diff --git a/storage/remote/ewma.go b/storage/remote/ewma.go
index ea4472c494..27ba39c35d 100644
--- a/storage/remote/ewma.go
+++ b/storage/remote/ewma.go
@@ -1,4 +1,4 @@
-// Copyright 2013 The Prometheus Authors
+// 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
diff --git a/storage/remote/googleiam/googleiam.go b/storage/remote/googleiam/googleiam.go
index acf3bd5a68..2095ee9747 100644
--- a/storage/remote/googleiam/googleiam.go
+++ b/storage/remote/googleiam/googleiam.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Prometheus Authors
+// 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
@@ -41,7 +41,7 @@ func NewRoundTripper(cfg *Config, next http.RoundTripper) (http.RoundTripper, er
option.WithScopes(scopes),
}
if cfg.CredentialsFile != "" {
- opts = append(opts, option.WithCredentialsFile(cfg.CredentialsFile))
+ opts = append(opts, option.WithAuthCredentialsFile(option.ServiceAccount, cfg.CredentialsFile))
} else {
creds, err := google.FindDefaultCredentials(ctx, scopes)
if err != nil {
diff --git a/storage/remote/intern.go b/storage/remote/intern.go
index 34edeb370e..193cdf96db 100644
--- a/storage/remote/intern.go
+++ b/storage/remote/intern.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The Prometheus Authors
+// 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
diff --git a/storage/remote/intern_test.go b/storage/remote/intern_test.go
index f992b2ada6..fd0ebed16f 100644
--- a/storage/remote/intern_test.go
+++ b/storage/remote/intern_test.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The Prometheus Authors
+// 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
diff --git a/storage/remote/max_timestamp.go b/storage/remote/max_timestamp.go
index bb67d9bb98..61dbda6bc6 100644
--- a/storage/remote/max_timestamp.go
+++ b/storage/remote/max_timestamp.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The Prometheus Authors
+// 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
diff --git a/storage/remote/metadata_watcher.go b/storage/remote/metadata_watcher.go
index b1f98038fc..f231691e30 100644
--- a/storage/remote/metadata_watcher.go
+++ b/storage/remote/metadata_watcher.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
diff --git a/storage/remote/metadata_watcher_test.go b/storage/remote/metadata_watcher_test.go
index 6c4608b3dd..f911a145bc 100644
--- a/storage/remote/metadata_watcher_test.go
+++ b/storage/remote/metadata_watcher_test.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
diff --git a/storage/remote/otlptranslator/prometheusremotewrite/combined_appender.go b/storage/remote/otlptranslator/prometheusremotewrite/combined_appender.go
deleted file mode 100644
index 1441aecb6d..0000000000
--- a/storage/remote/otlptranslator/prometheusremotewrite/combined_appender.go
+++ /dev/null
@@ -1,238 +0,0 @@
-// 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.
-
-// TODO(krajorama): rename this package to otlpappender or similar, as it is
-// not specific to Prometheus remote write anymore.
-// Note otlptranslator is already used by prometheus/otlptranslator repo.
-package prometheusremotewrite
-
-import (
- "errors"
- "fmt"
- "log/slog"
-
- "github.com/prometheus/client_golang/prometheus"
- "github.com/prometheus/client_golang/prometheus/promauto"
-
- "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/storage"
-)
-
-// Metadata extends metadata.Metadata with the metric family name.
-// OTLP calculates the metric family name for all metrics and uses
-// it for generating summary, histogram series by adding the magic
-// suffixes. The metric family name is passed down to the appender
-// in case the storage needs it for metadata updates.
-// Known user is Mimir that implements /api/v1/metadata and uses
-// Remote-Write 1.0 for this. Might be removed later if no longer
-// needed by any downstream project.
-type Metadata struct {
- metadata.Metadata
- MetricFamilyName string
-}
-
-// CombinedAppender is similar to storage.Appender, but combines updates to
-// metadata, created timestamps, exemplars and samples into a single call.
-type CombinedAppender interface {
- // AppendSample appends a sample and related exemplars, metadata, and
- // created timestamp to the storage.
- AppendSample(ls labels.Labels, meta Metadata, ct, t int64, v float64, es []exemplar.Exemplar) error
- // AppendHistogram appends a histogram and related exemplars, metadata, and
- // created timestamp to the storage.
- AppendHistogram(ls labels.Labels, meta Metadata, ct, t int64, h *histogram.Histogram, es []exemplar.Exemplar) error
-}
-
-// CombinedAppenderMetrics is for the metrics observed by the
-// combinedAppender implementation.
-type CombinedAppenderMetrics struct {
- samplesAppendedWithoutMetadata prometheus.Counter
- outOfOrderExemplars prometheus.Counter
-}
-
-func NewCombinedAppenderMetrics(reg prometheus.Registerer) CombinedAppenderMetrics {
- return CombinedAppenderMetrics{
- samplesAppendedWithoutMetadata: promauto.With(reg).NewCounter(prometheus.CounterOpts{
- Namespace: "prometheus",
- Subsystem: "api",
- Name: "otlp_appended_samples_without_metadata_total",
- Help: "The total number of samples ingested from OTLP without corresponding metadata.",
- }),
- outOfOrderExemplars: promauto.With(reg).NewCounter(prometheus.CounterOpts{
- Namespace: "prometheus",
- Subsystem: "api",
- Name: "otlp_out_of_order_exemplars_total",
- Help: "The total number of received OTLP exemplars which were rejected because they were out of order.",
- }),
- }
-}
-
-// NewCombinedAppender creates a combined appender that sets start times and
-// updates metadata for each series only once, and appends samples and
-// exemplars for each call.
-func NewCombinedAppender(app storage.Appender, logger *slog.Logger, ingestCTZeroSample bool, metrics CombinedAppenderMetrics) CombinedAppender {
- return &combinedAppender{
- app: app,
- logger: logger,
- ingestCTZeroSample: ingestCTZeroSample,
- refs: make(map[uint64]seriesRef),
- samplesAppendedWithoutMetadata: metrics.samplesAppendedWithoutMetadata,
- outOfOrderExemplars: metrics.outOfOrderExemplars,
- }
-}
-
-type seriesRef struct {
- ref storage.SeriesRef
- ct int64
- ls labels.Labels
- meta metadata.Metadata
-}
-
-type combinedAppender struct {
- app storage.Appender
- logger *slog.Logger
- samplesAppendedWithoutMetadata prometheus.Counter
- outOfOrderExemplars prometheus.Counter
- ingestCTZeroSample bool
- // Used to ensure we only update metadata and created timestamps once, and to share storage.SeriesRefs.
- // To detect hash collision it also stores the labels.
- // There is no overflow/conflict list, the TSDB will handle that part.
- refs map[uint64]seriesRef
-}
-
-func (b *combinedAppender) AppendSample(ls labels.Labels, meta Metadata, ct, t int64, v float64, es []exemplar.Exemplar) (err error) {
- return b.appendFloatOrHistogram(ls, meta.Metadata, ct, t, v, nil, es)
-}
-
-func (b *combinedAppender) AppendHistogram(ls labels.Labels, meta Metadata, ct, t int64, h *histogram.Histogram, es []exemplar.Exemplar) (err error) {
- if h == nil {
- // Sanity check, we should never get here with a nil histogram.
- b.logger.Error("Received nil histogram in CombinedAppender.AppendHistogram", "series", ls.String())
- return errors.New("internal error, attempted to append nil histogram")
- }
- return b.appendFloatOrHistogram(ls, meta.Metadata, ct, t, 0, h, es)
-}
-
-func (b *combinedAppender) appendFloatOrHistogram(ls labels.Labels, meta metadata.Metadata, ct, t int64, v float64, h *histogram.Histogram, es []exemplar.Exemplar) (err error) {
- hash := ls.Hash()
- series, exists := b.refs[hash]
- ref := series.ref
- if exists && !labels.Equal(series.ls, ls) {
- // Hash collision. The series reference we stored is pointing to a
- // different series so we cannot use it, we need to reset the
- // reference and cache.
- // Note: we don't need to keep track of conflicts here,
- // the TSDB will handle that part when we pass 0 reference.
- exists = false
- ref = 0
- }
- updateRefs := !exists || series.ct != ct
- if updateRefs && ct != 0 && ct < t && b.ingestCTZeroSample {
- var newRef storage.SeriesRef
- if h != nil {
- newRef, err = b.app.AppendHistogramCTZeroSample(ref, ls, t, ct, h, nil)
- } else {
- newRef, err = b.app.AppendCTZeroSample(ref, ls, t, ct)
- }
- if err != nil {
- if !errors.Is(err, storage.ErrOutOfOrderCT) && !errors.Is(err, storage.ErrDuplicateSampleForTimestamp) {
- // Even for the first sample OOO is a common scenario because
- // we can't tell if a CT was already ingested in a previous request.
- // We ignore the error.
- // ErrDuplicateSampleForTimestamp is also a common scenario because
- // unknown start times in Opentelemetry are indicated by setting
- // the start time to the same as the first sample time.
- // https://opentelemetry.io/docs/specs/otel/metrics/data-model/#cumulative-streams-handling-unknown-start-time
- b.logger.Warn("Error when appending CT from OTLP", "err", err, "series", ls.String(), "created_timestamp", ct, "timestamp", t, "sample_type", sampleType(h))
- }
- } else {
- // We only use the returned reference on success as otherwise an
- // error of CT append could invalidate the series reference.
- ref = newRef
- }
- }
- {
- var newRef storage.SeriesRef
- if h != nil {
- newRef, err = b.app.AppendHistogram(ref, ls, t, h, nil)
- } else {
- newRef, err = b.app.Append(ref, ls, t, v)
- }
- if err != nil {
- // Although Append does not currently return ErrDuplicateSampleForTimestamp there is
- // a note indicating its inclusion in the future.
- if errors.Is(err, storage.ErrOutOfOrderSample) ||
- errors.Is(err, storage.ErrOutOfBounds) ||
- errors.Is(err, storage.ErrDuplicateSampleForTimestamp) {
- b.logger.Error("Error when appending sample from OTLP", "err", err.Error(), "series", ls.String(), "timestamp", t, "sample_type", sampleType(h))
- }
- } else {
- // If the append was successful, we can use the returned reference.
- ref = newRef
- }
- }
-
- if ref == 0 {
- // We cannot update metadata or add exemplars on non existent series.
- return err
- }
-
- if !exists || series.meta.Help != meta.Help || series.meta.Type != meta.Type || series.meta.Unit != meta.Unit {
- updateRefs = true
- // If this is the first time we see this series, set the metadata.
- _, err := b.app.UpdateMetadata(ref, ls, meta)
- if err != nil {
- b.samplesAppendedWithoutMetadata.Add(1)
- b.logger.Warn("Error while updating metadata from OTLP", "err", err)
- }
- }
-
- if updateRefs {
- b.refs[hash] = seriesRef{
- ref: ref,
- ct: ct,
- ls: ls,
- meta: meta,
- }
- }
-
- b.appendExemplars(ref, ls, es)
-
- return err
-}
-
-func sampleType(h *histogram.Histogram) string {
- if h == nil {
- return "float"
- }
- return "histogram"
-}
-
-func (b *combinedAppender) appendExemplars(ref storage.SeriesRef, ls labels.Labels, es []exemplar.Exemplar) storage.SeriesRef {
- var err error
- for _, e := range es {
- if ref, err = b.app.AppendExemplar(ref, ls, e); err != nil {
- switch {
- case errors.Is(err, storage.ErrOutOfOrderExemplar):
- b.outOfOrderExemplars.Add(1)
- b.logger.Debug("Out of order exemplar from OTLP", "series", ls.String(), "exemplar", fmt.Sprintf("%+v", e))
- default:
- // Since exemplar storage is still experimental, we don't fail the request on ingestion errors
- b.logger.Debug("Error while adding exemplar from OTLP", "series", ls.String(), "exemplar", fmt.Sprintf("%+v", e), "err", err)
- }
- }
- }
- return ref
-}
diff --git a/storage/remote/otlptranslator/prometheusremotewrite/combined_appender_test.go b/storage/remote/otlptranslator/prometheusremotewrite/combined_appender_test.go
deleted file mode 100644
index a914277f92..0000000000
--- a/storage/remote/otlptranslator/prometheusremotewrite/combined_appender_test.go
+++ /dev/null
@@ -1,835 +0,0 @@
-// Copyright 2025 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 prometheusremotewrite
-
-import (
- "bytes"
- "context"
- "errors"
- "math"
- "testing"
- "time"
-
- "github.com/google/go-cmp/cmp"
- "github.com/prometheus/client_golang/prometheus"
- "github.com/prometheus/common/model"
- "github.com/prometheus/common/promslog"
- "github.com/stretchr/testify/require"
-
- "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/storage"
- "github.com/prometheus/prometheus/tsdb"
- "github.com/prometheus/prometheus/tsdb/chunkenc"
- "github.com/prometheus/prometheus/tsdb/tsdbutil"
- "github.com/prometheus/prometheus/util/testutil"
-)
-
-type mockCombinedAppender struct {
- pendingSamples []combinedSample
- pendingHistograms []combinedHistogram
-
- samples []combinedSample
- histograms []combinedHistogram
-}
-
-type combinedSample struct {
- metricFamilyName string
- ls labels.Labels
- meta metadata.Metadata
- t int64
- ct int64
- v float64
- es []exemplar.Exemplar
-}
-
-type combinedHistogram struct {
- metricFamilyName string
- ls labels.Labels
- meta metadata.Metadata
- t int64
- ct int64
- h *histogram.Histogram
- es []exemplar.Exemplar
-}
-
-func (m *mockCombinedAppender) AppendSample(ls labels.Labels, meta Metadata, ct, t int64, v float64, es []exemplar.Exemplar) error {
- m.pendingSamples = append(m.pendingSamples, combinedSample{
- metricFamilyName: meta.MetricFamilyName,
- ls: ls,
- meta: meta.Metadata,
- t: t,
- ct: ct,
- v: v,
- es: es,
- })
- return nil
-}
-
-func (m *mockCombinedAppender) AppendHistogram(ls labels.Labels, meta Metadata, ct, t int64, h *histogram.Histogram, es []exemplar.Exemplar) error {
- m.pendingHistograms = append(m.pendingHistograms, combinedHistogram{
- metricFamilyName: meta.MetricFamilyName,
- ls: ls,
- meta: meta.Metadata,
- t: t,
- ct: ct,
- h: h,
- es: es,
- })
- return nil
-}
-
-func (m *mockCombinedAppender) Commit() error {
- m.samples = append(m.samples, m.pendingSamples...)
- m.pendingSamples = m.pendingSamples[:0]
- m.histograms = append(m.histograms, m.pendingHistograms...)
- m.pendingHistograms = m.pendingHistograms[:0]
- return nil
-}
-
-func requireEqual(t testing.TB, expected, actual any, msgAndArgs ...any) {
- testutil.RequireEqualWithOptions(t, expected, actual, []cmp.Option{cmp.AllowUnexported(combinedSample{}, combinedHistogram{})}, msgAndArgs...)
-}
-
-// TestCombinedAppenderOnTSDB runs some basic tests on a real TSDB to check
-// that the combinedAppender works on a real TSDB.
-func TestCombinedAppenderOnTSDB(t *testing.T) {
- t.Run("ingestCTZeroSample=false", func(t *testing.T) { testCombinedAppenderOnTSDB(t, false) })
-
- t.Run("ingestCTZeroSample=true", func(t *testing.T) { testCombinedAppenderOnTSDB(t, true) })
-}
-
-func testCombinedAppenderOnTSDB(t *testing.T, ingestCTZeroSample bool) {
- t.Helper()
-
- now := time.Now()
-
- testExemplars := []exemplar.Exemplar{
- {
- Labels: labels.FromStrings("tracid", "122"),
- Value: 1337,
- },
- {
- Labels: labels.FromStrings("tracid", "132"),
- Value: 7777,
- },
- }
- expectedExemplars := []exemplar.QueryResult{
- {
- SeriesLabels: labels.FromStrings(
- model.MetricNameLabel, "test_bytes_total",
- "foo", "bar",
- ),
- Exemplars: testExemplars,
- },
- }
-
- seriesLabels := labels.FromStrings(
- model.MetricNameLabel, "test_bytes_total",
- "foo", "bar",
- )
- floatMetadata := Metadata{
- Metadata: metadata.Metadata{
- Type: model.MetricTypeCounter,
- Unit: "bytes",
- Help: "some help",
- },
- MetricFamilyName: "test_bytes_total",
- }
-
- histogramMetadata := Metadata{
- Metadata: metadata.Metadata{
- Type: model.MetricTypeHistogram,
- Unit: "bytes",
- Help: "some help",
- },
- MetricFamilyName: "test_bytes",
- }
-
- testCases := map[string]struct {
- appendFunc func(*testing.T, CombinedAppender)
- extraAppendFunc func(*testing.T, CombinedAppender)
- expectedSamples []sample
- expectedExemplars []exemplar.QueryResult
- expectedLogsForCT []string
- }{
- "single float sample, zero CT": {
- appendFunc: func(t *testing.T, app CombinedAppender) {
- require.NoError(t, app.AppendSample(seriesLabels.Copy(), floatMetadata, 0, now.UnixMilli(), 42.0, testExemplars))
- },
- expectedSamples: []sample{
- {
- t: now.UnixMilli(),
- f: 42.0,
- },
- },
- expectedExemplars: expectedExemplars,
- },
- "single float sample, very old CT": {
- appendFunc: func(t *testing.T, app CombinedAppender) {
- require.NoError(t, app.AppendSample(seriesLabels.Copy(), floatMetadata, 1, now.UnixMilli(), 42.0, nil))
- },
- expectedSamples: []sample{
- {
- t: now.UnixMilli(),
- f: 42.0,
- },
- },
- expectedLogsForCT: []string{
- "Error when appending CT from OTLP",
- "out of bound",
- },
- },
- "single float sample, normal CT": {
- appendFunc: func(t *testing.T, app CombinedAppender) {
- require.NoError(t, app.AppendSample(seriesLabels.Copy(), floatMetadata, now.Add(-2*time.Minute).UnixMilli(), now.UnixMilli(), 42.0, nil))
- },
- expectedSamples: []sample{
- {
- ctZero: true,
- t: now.Add(-2 * time.Minute).UnixMilli(),
- },
- {
- t: now.UnixMilli(),
- f: 42.0,
- },
- },
- },
- "single float sample, CT same time as sample": {
- appendFunc: func(t *testing.T, app CombinedAppender) {
- require.NoError(t, app.AppendSample(seriesLabels.Copy(), floatMetadata, now.UnixMilli(), now.UnixMilli(), 42.0, nil))
- },
- expectedSamples: []sample{
- {
- t: now.UnixMilli(),
- f: 42.0,
- },
- },
- },
- "two float samples in different messages, CT same time as first sample": {
- appendFunc: func(t *testing.T, app CombinedAppender) {
- require.NoError(t, app.AppendSample(seriesLabels.Copy(), floatMetadata, now.UnixMilli(), now.UnixMilli(), 42.0, nil))
- },
- extraAppendFunc: func(t *testing.T, app CombinedAppender) {
- require.NoError(t, app.AppendSample(seriesLabels.Copy(), floatMetadata, now.UnixMilli(), now.Add(time.Second).UnixMilli(), 43.0, nil))
- },
- expectedSamples: []sample{
- {
- t: now.UnixMilli(),
- f: 42.0,
- },
- {
- t: now.Add(time.Second).UnixMilli(),
- f: 43.0,
- },
- },
- },
- "single float sample, CT in the future of the sample": {
- appendFunc: func(t *testing.T, app CombinedAppender) {
- require.NoError(t, app.AppendSample(seriesLabels.Copy(), floatMetadata, now.Add(time.Minute).UnixMilli(), now.UnixMilli(), 42.0, nil))
- },
- expectedSamples: []sample{
- {
- t: now.UnixMilli(),
- f: 42.0,
- },
- },
- },
- "single histogram sample, zero CT": {
- appendFunc: func(t *testing.T, app CombinedAppender) {
- require.NoError(t, app.AppendHistogram(seriesLabels.Copy(), histogramMetadata, 0, now.UnixMilli(), tsdbutil.GenerateTestHistogram(42), testExemplars))
- },
- expectedSamples: []sample{
- {
- t: now.UnixMilli(),
- h: tsdbutil.GenerateTestHistogram(42),
- },
- },
- expectedExemplars: expectedExemplars,
- },
- "single histogram sample, very old CT": {
- appendFunc: func(t *testing.T, app CombinedAppender) {
- require.NoError(t, app.AppendHistogram(seriesLabels.Copy(), histogramMetadata, 1, now.UnixMilli(), tsdbutil.GenerateTestHistogram(42), nil))
- },
- expectedSamples: []sample{
- {
- t: now.UnixMilli(),
- h: tsdbutil.GenerateTestHistogram(42),
- },
- },
- expectedLogsForCT: []string{
- "Error when appending CT from OTLP",
- "out of bound",
- },
- },
- "single histogram sample, normal CT": {
- appendFunc: func(t *testing.T, app CombinedAppender) {
- require.NoError(t, app.AppendHistogram(seriesLabels.Copy(), histogramMetadata, now.Add(-2*time.Minute).UnixMilli(), now.UnixMilli(), tsdbutil.GenerateTestHistogram(42), nil))
- },
- expectedSamples: []sample{
- {
- ctZero: true,
- t: now.Add(-2 * time.Minute).UnixMilli(),
- h: &histogram.Histogram{},
- },
- {
- t: now.UnixMilli(),
- h: tsdbutil.GenerateTestHistogram(42),
- },
- },
- },
- "single histogram sample, CT same time as sample": {
- appendFunc: func(t *testing.T, app CombinedAppender) {
- require.NoError(t, app.AppendHistogram(seriesLabels.Copy(), histogramMetadata, now.UnixMilli(), now.UnixMilli(), tsdbutil.GenerateTestHistogram(42), nil))
- },
- expectedSamples: []sample{
- {
- t: now.UnixMilli(),
- h: tsdbutil.GenerateTestHistogram(42),
- },
- },
- },
- "two histogram samples in different messages, CT same time as first sample": {
- appendFunc: func(t *testing.T, app CombinedAppender) {
- require.NoError(t, app.AppendHistogram(seriesLabels.Copy(), floatMetadata, now.UnixMilli(), now.UnixMilli(), tsdbutil.GenerateTestHistogram(42), nil))
- },
- extraAppendFunc: func(t *testing.T, app CombinedAppender) {
- require.NoError(t, app.AppendHistogram(seriesLabels.Copy(), floatMetadata, now.UnixMilli(), now.Add(time.Second).UnixMilli(), tsdbutil.GenerateTestHistogram(43), nil))
- },
- expectedSamples: []sample{
- {
- t: now.UnixMilli(),
- h: tsdbutil.GenerateTestHistogram(42),
- },
- {
- t: now.Add(time.Second).UnixMilli(),
- h: tsdbutil.GenerateTestHistogram(43),
- },
- },
- },
- "single histogram sample, CT in the future of the sample": {
- appendFunc: func(t *testing.T, app CombinedAppender) {
- require.NoError(t, app.AppendHistogram(seriesLabels.Copy(), histogramMetadata, now.Add(time.Minute).UnixMilli(), now.UnixMilli(), tsdbutil.GenerateTestHistogram(42), nil))
- },
- expectedSamples: []sample{
- {
- t: now.UnixMilli(),
- h: tsdbutil.GenerateTestHistogram(42),
- },
- },
- },
- "multiple float samples": {
- appendFunc: func(t *testing.T, app CombinedAppender) {
- require.NoError(t, app.AppendSample(seriesLabels.Copy(), floatMetadata, 0, now.UnixMilli(), 42.0, nil))
- require.NoError(t, app.AppendSample(seriesLabels.Copy(), floatMetadata, 0, now.Add(15*time.Second).UnixMilli(), 62.0, nil))
- },
- expectedSamples: []sample{
- {
- t: now.UnixMilli(),
- f: 42.0,
- },
- {
- t: now.Add(15 * time.Second).UnixMilli(),
- f: 62.0,
- },
- },
- },
- "multiple histogram samples": {
- appendFunc: func(t *testing.T, app CombinedAppender) {
- require.NoError(t, app.AppendHistogram(seriesLabels.Copy(), histogramMetadata, 0, now.UnixMilli(), tsdbutil.GenerateTestHistogram(42), nil))
- require.NoError(t, app.AppendHistogram(seriesLabels.Copy(), histogramMetadata, 0, now.Add(15*time.Second).UnixMilli(), tsdbutil.GenerateTestHistogram(62), nil))
- },
- expectedSamples: []sample{
- {
- t: now.UnixMilli(),
- h: tsdbutil.GenerateTestHistogram(42),
- },
- {
- t: now.Add(15 * time.Second).UnixMilli(),
- h: tsdbutil.GenerateTestHistogram(62),
- },
- },
- },
- "float samples with CT changing": {
- appendFunc: func(t *testing.T, app CombinedAppender) {
- require.NoError(t, app.AppendSample(seriesLabels.Copy(), floatMetadata, now.Add(-4*time.Second).UnixMilli(), now.Add(-3*time.Second).UnixMilli(), 42.0, nil))
- require.NoError(t, app.AppendSample(seriesLabels.Copy(), floatMetadata, now.Add(-1*time.Second).UnixMilli(), now.UnixMilli(), 62.0, nil))
- },
- expectedSamples: []sample{
- {
- ctZero: true,
- t: now.Add(-4 * time.Second).UnixMilli(),
- },
- {
- t: now.Add(-3 * time.Second).UnixMilli(),
- f: 42.0,
- },
- {
- ctZero: true,
- t: now.Add(-1 * time.Second).UnixMilli(),
- },
- {
- t: now.UnixMilli(),
- f: 62.0,
- },
- },
- },
- }
-
- for name, tc := range testCases {
- t.Run(name, func(t *testing.T) {
- var expectedLogs []string
- if ingestCTZeroSample {
- expectedLogs = append(expectedLogs, tc.expectedLogsForCT...)
- }
-
- dir := t.TempDir()
- opts := tsdb.DefaultOptions()
- opts.EnableExemplarStorage = true
- opts.MaxExemplars = 100
- db, err := tsdb.Open(dir, promslog.NewNopLogger(), prometheus.NewRegistry(), opts, nil)
- require.NoError(t, err)
-
- t.Cleanup(func() { db.Close() })
-
- var output bytes.Buffer
- logger := promslog.New(&promslog.Config{Writer: &output})
-
- ctx := context.Background()
- reg := prometheus.NewRegistry()
- cappMetrics := NewCombinedAppenderMetrics(reg)
- app := db.Appender(ctx)
- capp := NewCombinedAppender(app, logger, ingestCTZeroSample, cappMetrics)
- tc.appendFunc(t, capp)
- require.NoError(t, app.Commit())
-
- if tc.extraAppendFunc != nil {
- app = db.Appender(ctx)
- capp = NewCombinedAppender(app, logger, ingestCTZeroSample, cappMetrics)
- tc.extraAppendFunc(t, capp)
- require.NoError(t, app.Commit())
- }
-
- if len(expectedLogs) > 0 {
- for _, expectedLog := range expectedLogs {
- require.Contains(t, output.String(), expectedLog)
- }
- } else {
- require.Empty(t, output.String(), "unexpected log output")
- }
-
- q, err := db.Querier(int64(math.MinInt64), int64(math.MaxInt64))
- require.NoError(t, err)
-
- ss := q.Select(ctx, false, &storage.SelectHints{
- Start: int64(math.MinInt64),
- End: int64(math.MaxInt64),
- }, labels.MustNewMatcher(labels.MatchEqual, model.MetricNameLabel, "test_bytes_total"))
-
- require.NoError(t, ss.Err())
-
- require.True(t, ss.Next())
- series := ss.At()
- it := series.Iterator(nil)
- for i, sample := range tc.expectedSamples {
- if !ingestCTZeroSample && sample.ctZero {
- continue
- }
- if sample.h == nil {
- require.Equal(t, chunkenc.ValFloat, it.Next())
- ts, v := it.At()
- require.Equal(t, sample.t, ts, "sample ts %d", i)
- require.Equal(t, sample.f, v, "sample v %d", i)
- } else {
- require.Equal(t, chunkenc.ValHistogram, it.Next())
- ts, h := it.AtHistogram(nil)
- require.Equal(t, sample.t, ts, "sample ts %d", i)
- require.Equal(t, sample.h.Count, h.Count, "sample v %d", i)
- }
- }
- require.False(t, ss.Next())
-
- eq, err := db.ExemplarQuerier(ctx)
- require.NoError(t, err)
- exResult, err := eq.Select(int64(math.MinInt64), int64(math.MaxInt64), []*labels.Matcher{labels.MustNewMatcher(labels.MatchEqual, model.MetricNameLabel, "test_bytes_total")})
- require.NoError(t, err)
- if tc.expectedExemplars == nil {
- tc.expectedExemplars = []exemplar.QueryResult{}
- }
- require.Equal(t, tc.expectedExemplars, exResult)
- })
- }
-}
-
-type sample struct {
- ctZero bool
-
- t int64
- f float64
- h *histogram.Histogram
-}
-
-// TestCombinedAppenderSeriesRefs checks that the combined appender
-// correctly uses and updates the series references in the internal map.
-func TestCombinedAppenderSeriesRefs(t *testing.T) {
- seriesLabels := labels.FromStrings(
- model.MetricNameLabel, "test_bytes_total",
- "foo", "bar",
- )
-
- floatMetadata := Metadata{
- Metadata: metadata.Metadata{
- Type: model.MetricTypeCounter,
- Unit: "bytes",
- Help: "some help",
- },
- MetricFamilyName: "test_bytes_total",
- }
-
- t.Run("happy case with CT zero, reference is passed and reused", func(t *testing.T) {
- app := &appenderRecorder{}
- capp := NewCombinedAppender(app, promslog.NewNopLogger(), true, NewCombinedAppenderMetrics(prometheus.NewRegistry()))
-
- require.NoError(t, capp.AppendSample(seriesLabels.Copy(), floatMetadata, 1, 2, 42.0, nil))
-
- require.NoError(t, capp.AppendSample(seriesLabels.Copy(), floatMetadata, 3, 4, 62.0, []exemplar.Exemplar{
- {
- Labels: labels.FromStrings("tracid", "122"),
- Value: 1337,
- },
- }))
-
- require.Len(t, app.records, 6)
- requireEqualOpAndRef(t, "AppendCTZeroSample", 0, app.records[0])
- ref := app.records[0].outRef
- require.NotZero(t, ref)
- requireEqualOpAndRef(t, "Append", ref, app.records[1])
- requireEqualOpAndRef(t, "UpdateMetadata", ref, app.records[2])
- requireEqualOpAndRef(t, "AppendCTZeroSample", ref, app.records[3])
- requireEqualOpAndRef(t, "Append", ref, app.records[4])
- requireEqualOpAndRef(t, "AppendExemplar", ref, app.records[5])
- })
-
- t.Run("error on second CT ingest doesn't update the reference", func(t *testing.T) {
- app := &appenderRecorder{}
- capp := NewCombinedAppender(app, promslog.NewNopLogger(), true, NewCombinedAppenderMetrics(prometheus.NewRegistry()))
-
- require.NoError(t, capp.AppendSample(seriesLabels.Copy(), floatMetadata, 1, 2, 42.0, nil))
-
- app.appendCTZeroSampleError = errors.New("test error")
- require.NoError(t, capp.AppendSample(seriesLabels.Copy(), floatMetadata, 3, 4, 62.0, nil))
-
- require.Len(t, app.records, 5)
- requireEqualOpAndRef(t, "AppendCTZeroSample", 0, app.records[0])
- ref := app.records[0].outRef
- require.NotZero(t, ref)
- requireEqualOpAndRef(t, "Append", ref, app.records[1])
- requireEqualOpAndRef(t, "UpdateMetadata", ref, app.records[2])
- requireEqualOpAndRef(t, "AppendCTZeroSample", ref, app.records[3])
- require.Zero(t, app.records[3].outRef, "the second AppendCTZeroSample returned 0")
- requireEqualOpAndRef(t, "Append", ref, app.records[4])
- })
-
- t.Run("updateMetadata called when meta help changes", func(t *testing.T) {
- app := &appenderRecorder{}
- capp := NewCombinedAppender(app, promslog.NewNopLogger(), true, NewCombinedAppenderMetrics(prometheus.NewRegistry()))
-
- newMetadata := floatMetadata
- newMetadata.Help = "some other help"
-
- require.NoError(t, capp.AppendSample(seriesLabels.Copy(), floatMetadata, 1, 2, 42.0, nil))
- require.NoError(t, capp.AppendSample(seriesLabels.Copy(), newMetadata, 3, 4, 62.0, nil))
- require.NoError(t, capp.AppendSample(seriesLabels.Copy(), newMetadata, 3, 5, 162.0, nil))
-
- require.Len(t, app.records, 7)
- requireEqualOpAndRef(t, "AppendCTZeroSample", 0, app.records[0])
- ref := app.records[0].outRef
- require.NotZero(t, ref)
- requireEqualOpAndRef(t, "Append", ref, app.records[1])
- requireEqualOpAndRef(t, "UpdateMetadata", ref, app.records[2])
- requireEqualOpAndRef(t, "AppendCTZeroSample", ref, app.records[3])
- requireEqualOpAndRef(t, "Append", ref, app.records[4])
- requireEqualOpAndRef(t, "UpdateMetadata", ref, app.records[5])
- requireEqualOpAndRef(t, "Append", ref, app.records[6])
- })
-
- t.Run("updateMetadata called when meta unit changes", func(t *testing.T) {
- app := &appenderRecorder{}
- capp := NewCombinedAppender(app, promslog.NewNopLogger(), true, NewCombinedAppenderMetrics(prometheus.NewRegistry()))
-
- newMetadata := floatMetadata
- newMetadata.Unit = "seconds"
-
- require.NoError(t, capp.AppendSample(seriesLabels.Copy(), floatMetadata, 1, 2, 42.0, nil))
- require.NoError(t, capp.AppendSample(seriesLabels.Copy(), newMetadata, 3, 4, 62.0, nil))
- require.NoError(t, capp.AppendSample(seriesLabels.Copy(), newMetadata, 3, 5, 162.0, nil))
-
- require.Len(t, app.records, 7)
- requireEqualOpAndRef(t, "AppendCTZeroSample", 0, app.records[0])
- ref := app.records[0].outRef
- require.NotZero(t, ref)
- requireEqualOpAndRef(t, "Append", ref, app.records[1])
- requireEqualOpAndRef(t, "UpdateMetadata", ref, app.records[2])
- requireEqualOpAndRef(t, "AppendCTZeroSample", ref, app.records[3])
- requireEqualOpAndRef(t, "Append", ref, app.records[4])
- requireEqualOpAndRef(t, "UpdateMetadata", ref, app.records[5])
- requireEqualOpAndRef(t, "Append", ref, app.records[6])
- })
-
- t.Run("updateMetadata called when meta type changes", func(t *testing.T) {
- app := &appenderRecorder{}
- capp := NewCombinedAppender(app, promslog.NewNopLogger(), true, NewCombinedAppenderMetrics(prometheus.NewRegistry()))
-
- newMetadata := floatMetadata
- newMetadata.Type = model.MetricTypeGauge
-
- require.NoError(t, capp.AppendSample(seriesLabels.Copy(), floatMetadata, 1, 2, 42.0, nil))
- require.NoError(t, capp.AppendSample(seriesLabels.Copy(), newMetadata, 3, 4, 62.0, nil))
- require.NoError(t, capp.AppendSample(seriesLabels.Copy(), newMetadata, 3, 5, 162.0, nil))
-
- require.Len(t, app.records, 7)
- requireEqualOpAndRef(t, "AppendCTZeroSample", 0, app.records[0])
- ref := app.records[0].outRef
- require.NotZero(t, ref)
- requireEqualOpAndRef(t, "Append", ref, app.records[1])
- requireEqualOpAndRef(t, "UpdateMetadata", ref, app.records[2])
- requireEqualOpAndRef(t, "AppendCTZeroSample", ref, app.records[3])
- requireEqualOpAndRef(t, "Append", ref, app.records[4])
- requireEqualOpAndRef(t, "UpdateMetadata", ref, app.records[5])
- requireEqualOpAndRef(t, "Append", ref, app.records[6])
- })
-
- t.Run("metadata, exemplars are not updated if append failed", func(t *testing.T) {
- app := &appenderRecorder{}
- capp := NewCombinedAppender(app, promslog.NewNopLogger(), true, NewCombinedAppenderMetrics(prometheus.NewRegistry()))
- app.appendError = errors.New("test error")
- require.Error(t, capp.AppendSample(seriesLabels.Copy(), floatMetadata, 0, 1, 42.0, []exemplar.Exemplar{
- {
- Labels: labels.FromStrings("tracid", "122"),
- Value: 1337,
- },
- }))
-
- require.Len(t, app.records, 1)
- require.Equal(t, appenderRecord{
- op: "Append",
- ls: labels.FromStrings(model.MetricNameLabel, "test_bytes_total", "foo", "bar"),
- }, app.records[0])
- })
-
- t.Run("metadata, exemplars are updated if append failed but reference is valid", func(t *testing.T) {
- app := &appenderRecorder{}
- capp := NewCombinedAppender(app, promslog.NewNopLogger(), true, NewCombinedAppenderMetrics(prometheus.NewRegistry()))
-
- newMetadata := floatMetadata
- newMetadata.Help = "some other help"
-
- require.NoError(t, capp.AppendSample(seriesLabels.Copy(), floatMetadata, 1, 2, 42.0, nil))
- app.appendError = errors.New("test error")
- require.Error(t, capp.AppendSample(seriesLabels.Copy(), newMetadata, 3, 4, 62.0, []exemplar.Exemplar{
- {
- Labels: labels.FromStrings("tracid", "122"),
- Value: 1337,
- },
- }))
-
- require.Len(t, app.records, 7)
- requireEqualOpAndRef(t, "AppendCTZeroSample", 0, app.records[0])
- ref := app.records[0].outRef
- require.NotZero(t, ref)
- requireEqualOpAndRef(t, "Append", ref, app.records[1])
- requireEqualOpAndRef(t, "UpdateMetadata", ref, app.records[2])
- requireEqualOpAndRef(t, "AppendCTZeroSample", ref, app.records[3])
- requireEqualOpAndRef(t, "Append", ref, app.records[4])
- require.Zero(t, app.records[4].outRef, "the second Append returned 0")
- requireEqualOpAndRef(t, "UpdateMetadata", ref, app.records[5])
- requireEqualOpAndRef(t, "AppendExemplar", ref, app.records[6])
- })
-
- t.Run("simulate conflict with existing series", func(t *testing.T) {
- app := &appenderRecorder{}
- capp := NewCombinedAppender(app, promslog.NewNopLogger(), true, NewCombinedAppenderMetrics(prometheus.NewRegistry()))
-
- ls := labels.FromStrings(
- model.MetricNameLabel, "test_bytes_total",
- "foo", "bar",
- )
-
- require.NoError(t, capp.AppendSample(ls, floatMetadata, 1, 2, 42.0, nil))
-
- hash := ls.Hash()
- cappImpl := capp.(*combinedAppender)
- series := cappImpl.refs[hash]
- series.ls = labels.FromStrings(
- model.MetricNameLabel, "test_bytes_total",
- "foo", "club",
- )
- // The hash and ref remain the same, but we altered the labels.
- // This simulates a conflict with an existing series.
- cappImpl.refs[hash] = series
-
- require.NoError(t, capp.AppendSample(ls, floatMetadata, 3, 4, 62.0, []exemplar.Exemplar{
- {
- Labels: labels.FromStrings("tracid", "122"),
- Value: 1337,
- },
- }))
-
- require.Len(t, app.records, 7)
- requireEqualOpAndRef(t, "AppendCTZeroSample", 0, app.records[0])
- ref := app.records[0].outRef
- require.NotZero(t, ref)
- requireEqualOpAndRef(t, "Append", ref, app.records[1])
- requireEqualOpAndRef(t, "UpdateMetadata", ref, app.records[2])
- requireEqualOpAndRef(t, "AppendCTZeroSample", 0, app.records[3])
- newRef := app.records[3].outRef
- require.NotEqual(t, ref, newRef, "the second AppendCTZeroSample returned a different reference")
- requireEqualOpAndRef(t, "Append", newRef, app.records[4])
- requireEqualOpAndRef(t, "UpdateMetadata", newRef, app.records[5])
- requireEqualOpAndRef(t, "AppendExemplar", newRef, app.records[6])
- })
-
- t.Run("check that invoking AppendHistogram returns an error for nil histogram", func(t *testing.T) {
- app := &appenderRecorder{}
- capp := NewCombinedAppender(app, promslog.NewNopLogger(), true, NewCombinedAppenderMetrics(prometheus.NewRegistry()))
-
- ls := labels.FromStrings(
- model.MetricNameLabel, "test_bytes_total",
- "foo", "bar",
- )
- err := capp.AppendHistogram(ls, Metadata{}, 4, 2, nil, nil)
- require.Error(t, err)
- })
-}
-
-func requireEqualOpAndRef(t *testing.T, expectedOp string, expectedRef storage.SeriesRef, actual appenderRecord) {
- t.Helper()
- require.Equal(t, expectedOp, actual.op)
- require.Equal(t, expectedRef, actual.ref)
-}
-
-type appenderRecord struct {
- op string
- ref storage.SeriesRef
- outRef storage.SeriesRef
- ls labels.Labels
-}
-
-type appenderRecorder struct {
- refcount uint64
- records []appenderRecord
-
- appendError error
- appendCTZeroSampleError error
- appendHistogramError error
- appendHistogramCTZeroSampleError error
- updateMetadataError error
- appendExemplarError error
-}
-
-var _ storage.Appender = &appenderRecorder{}
-
-func (a *appenderRecorder) setOutRef(ref storage.SeriesRef) {
- if len(a.records) == 0 {
- return
- }
- a.records[len(a.records)-1].outRef = ref
-}
-
-func (a *appenderRecorder) newRef() storage.SeriesRef {
- a.refcount++
- return storage.SeriesRef(a.refcount)
-}
-
-func (a *appenderRecorder) Append(ref storage.SeriesRef, ls labels.Labels, _ int64, _ float64) (storage.SeriesRef, error) {
- a.records = append(a.records, appenderRecord{op: "Append", ref: ref, ls: ls})
- if a.appendError != nil {
- return 0, a.appendError
- }
- if ref == 0 {
- ref = a.newRef()
- }
- a.setOutRef(ref)
- return ref, nil
-}
-
-func (a *appenderRecorder) AppendCTZeroSample(ref storage.SeriesRef, ls labels.Labels, _, _ int64) (storage.SeriesRef, error) {
- a.records = append(a.records, appenderRecord{op: "AppendCTZeroSample", ref: ref, ls: ls})
- if a.appendCTZeroSampleError != nil {
- return 0, a.appendCTZeroSampleError
- }
- if ref == 0 {
- ref = a.newRef()
- }
- a.setOutRef(ref)
- return ref, nil
-}
-
-func (a *appenderRecorder) AppendHistogram(ref storage.SeriesRef, ls labels.Labels, _ int64, _ *histogram.Histogram, _ *histogram.FloatHistogram) (storage.SeriesRef, error) {
- a.records = append(a.records, appenderRecord{op: "AppendHistogram", ref: ref, ls: ls})
- if a.appendHistogramError != nil {
- return 0, a.appendHistogramError
- }
- if ref == 0 {
- ref = a.newRef()
- }
- a.setOutRef(ref)
- return ref, nil
-}
-
-func (a *appenderRecorder) AppendHistogramCTZeroSample(ref storage.SeriesRef, ls labels.Labels, _, _ int64, _ *histogram.Histogram, _ *histogram.FloatHistogram) (storage.SeriesRef, error) {
- a.records = append(a.records, appenderRecord{op: "AppendHistogramCTZeroSample", ref: ref, ls: ls})
- if a.appendHistogramCTZeroSampleError != nil {
- return 0, a.appendHistogramCTZeroSampleError
- }
- if ref == 0 {
- ref = a.newRef()
- }
- a.setOutRef(ref)
- return ref, nil
-}
-
-func (a *appenderRecorder) UpdateMetadata(ref storage.SeriesRef, ls labels.Labels, _ metadata.Metadata) (storage.SeriesRef, error) {
- a.records = append(a.records, appenderRecord{op: "UpdateMetadata", ref: ref, ls: ls})
- if a.updateMetadataError != nil {
- return 0, a.updateMetadataError
- }
- a.setOutRef(ref)
- return ref, nil
-}
-
-func (a *appenderRecorder) AppendExemplar(ref storage.SeriesRef, ls labels.Labels, _ exemplar.Exemplar) (storage.SeriesRef, error) {
- a.records = append(a.records, appenderRecord{op: "AppendExemplar", ref: ref, ls: ls})
- if a.appendExemplarError != nil {
- return 0, a.appendExemplarError
- }
- a.setOutRef(ref)
- return ref, nil
-}
-
-func (a *appenderRecorder) Commit() error {
- a.records = append(a.records, appenderRecord{op: "Commit"})
- return nil
-}
-
-func (a *appenderRecorder) Rollback() error {
- a.records = append(a.records, appenderRecord{op: "Rollback"})
- return nil
-}
-
-func (*appenderRecorder) SetOptions(_ *storage.AppendOptions) {
- panic("not implemented")
-}
diff --git a/storage/remote/otlptranslator/prometheusremotewrite/context.go b/storage/remote/otlptranslator/prometheusremotewrite/context.go
index 5c6dd20f18..db3c180036 100644
--- a/storage/remote/otlptranslator/prometheusremotewrite/context.go
+++ b/storage/remote/otlptranslator/prometheusremotewrite/context.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Prometheus Authors
+// 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
diff --git a/storage/remote/otlptranslator/prometheusremotewrite/context_test.go b/storage/remote/otlptranslator/prometheusremotewrite/context_test.go
index 4b47964313..8aa24a8110 100644
--- a/storage/remote/otlptranslator/prometheusremotewrite/context_test.go
+++ b/storage/remote/otlptranslator/prometheusremotewrite/context_test.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Prometheus Authors
+// 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
diff --git a/storage/remote/otlptranslator/prometheusremotewrite/helper.go b/storage/remote/otlptranslator/prometheusremotewrite/helper.go
index aa54433836..1d321218e7 100644
--- a/storage/remote/otlptranslator/prometheusremotewrite/helper.go
+++ b/storage/remote/otlptranslator/prometheusremotewrite/helper.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Prometheus Authors
+// 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
@@ -19,6 +19,7 @@ package prometheusremotewrite
import (
"context"
"encoding/hex"
+ "errors"
"fmt"
"log"
"math"
@@ -32,13 +33,14 @@ 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"
"github.com/prometheus/prometheus/model/metadata"
"github.com/prometheus/prometheus/model/timestamp"
"github.com/prometheus/prometheus/model/value"
+ "github.com/prometheus/prometheus/storage"
)
const (
@@ -60,18 +62,29 @@ const (
defaultLookbackDelta = 5 * time.Minute
)
+// reservedLabelNames contains label names that should be filtered from
+// OTLP attributes because they are set separately (via extras parameter).
+// Allowing these through could create duplicate labels.
+var reservedLabelNames = []string{
+ model.MetricNameLabel, // "__name__" - set from metric name
+}
+
// 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,
- ignoreAttrs []string, logOnOverwrite bool, meta Metadata, extras ...string,
+//
+// 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.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 +101,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 +113,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 +129,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 +171,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 +183,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
}
@@ -222,8 +216,11 @@ func aggregationTemporality(metric pmetric.Metric) (pmetric.AggregationTemporali
// with the user defined bucket boundaries of non-exponential OTel histograms.
// 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,
+func (c *PrometheusConverter) addHistogramDataPoints(
+ ctx context.Context,
+ dataPoints pmetric.HistogramDataPointSlice,
+ settings Settings,
+ appOpts storage.AOptions,
) error {
for x := 0; x < dataPoints.Len(); x++ {
if err := c.everyN.checkContext(ctx); err != nil {
@@ -231,38 +228,37 @@ func (c *PrometheusConverter) addHistogramDataPoints(ctx context.Context, dataPo
}
pt := dataPoints.At(x)
+ // Clear stale exemplars from the previous data point to prevent
+ // them from leaking into _sum and _count of this data point.
+ appOpts.Exemplars = nil
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, reservedLabelNames, false, appOpts.Metadata)
if err != nil {
return err
}
- baseName := meta.MetricFamilyName
-
// If the sum is unset, it indicates the _sum metric point should be
// omitted
if pt.HasSum() {
- // treat sum as a sample in an individual TimeSeries
+ // Treat sum as a sample in an individual TimeSeries.
val := pt.Sum()
if pt.Flags().NoRecordedValue() {
val = math.Float64frombits(value.StaleNaN)
}
-
- sumlabels := c.addLabels(baseName+sumStr, baseLabels)
- if err := c.appender.AppendSample(sumlabels, meta, startTimestamp, timestamp, val, nil); err != nil {
+ sumLabels := c.addLabels(appOpts.MetricFamilyName+sumStr, baseLabels)
+ if _, err := c.appender.Append(0, sumLabels, startTimestamp, timestamp, val, nil, nil, appOpts); err != nil {
return err
}
}
- // treat count as a sample in an individual TimeSeries
+ // Treat count as a sample in an individual TimeSeries.
val := float64(pt.Count())
if pt.Flags().NoRecordedValue() {
val = math.Float64frombits(value.StaleNaN)
}
-
- countlabels := c.addLabels(baseName+countStr, baseLabels)
- if err := c.appender.AppendSample(countlabels, meta, startTimestamp, timestamp, val, nil); err != nil {
+ countLabels := c.addLabels(appOpts.MetricFamilyName+countStr, baseLabels)
+ if _, err := c.appender.Append(0, countLabels, startTimestamp, timestamp, val, nil, nil, appOpts); err != nil {
return err
}
exemplars, err := c.getPromExemplars(ctx, pt.Exemplars())
@@ -271,10 +267,10 @@ func (c *PrometheusConverter) addHistogramDataPoints(ctx context.Context, dataPo
}
nextExemplarIdx := 0
- // cumulative count for conversion to cumulative histogram
+ // Cumulative count for conversion to cumulative histogram.
var cumulativeCount uint64
- // process each bound, based on histograms proto definition, # of buckets = # of explicit bounds + 1
+ // Process each bound, based on histograms proto definition, # of buckets = # of explicit bounds + 1.
for i := 0; i < pt.ExplicitBounds().Len() && i < pt.BucketCounts().Len(); i++ {
if err := c.everyN.checkContext(ctx); err != nil {
return err
@@ -285,32 +281,34 @@ func (c *PrometheusConverter) addHistogramDataPoints(ctx context.Context, dataPo
// Find exemplars that belong to this bucket. Both exemplars and
// buckets are sorted in ascending order.
- var currentBucketExemplars []exemplar.Exemplar
+ appOpts.Exemplars = appOpts.Exemplars[:0]
for ; nextExemplarIdx < len(exemplars); nextExemplarIdx++ {
ex := exemplars[nextExemplarIdx]
if ex.Value > bound {
// This exemplar belongs in a higher bucket.
break
}
- currentBucketExemplars = append(currentBucketExemplars, ex)
+ appOpts.Exemplars = append(appOpts.Exemplars, ex)
}
val := float64(cumulativeCount)
if pt.Flags().NoRecordedValue() {
val = math.Float64frombits(value.StaleNaN)
}
boundStr := strconv.FormatFloat(bound, 'f', -1, 64)
- labels := c.addLabels(baseName+bucketStr, baseLabels, leStr, boundStr)
- if err := c.appender.AppendSample(labels, meta, startTimestamp, timestamp, val, currentBucketExemplars); err != nil {
+ bucketLabels := c.addLabels(appOpts.MetricFamilyName+bucketStr, baseLabels, leStr, boundStr)
+ if _, err := c.appender.Append(0, bucketLabels, startTimestamp, timestamp, val, nil, nil, appOpts); err != nil {
return err
}
}
- // add le=+Inf bucket
+
+ appOpts.Exemplars = exemplars[nextExemplarIdx:]
+ // Add le=+Inf bucket.
val = float64(pt.Count())
if pt.Flags().NoRecordedValue() {
val = math.Float64frombits(value.StaleNaN)
}
- infLabels := c.addLabels(baseName+bucketStr, baseLabels, leStr, pInfStr)
- if err := c.appender.AppendSample(infLabels, meta, startTimestamp, timestamp, val, exemplars[nextExemplarIdx:]); err != nil {
+ infLabels := c.addLabels(appOpts.MetricFamilyName+bucketStr, baseLabels, leStr, pInfStr)
+ if _, err := c.appender.Append(0, infLabels, startTimestamp, timestamp, val, nil, nil, appOpts); err != nil {
return err
}
}
@@ -424,8 +422,11 @@ 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,
+ appOpts storage.AOptions,
) error {
for x := 0; x < dataPoints.Len(); x++ {
if err := c.everyN.checkContext(ctx); err != nil {
@@ -435,21 +436,18 @@ 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, reservedLabelNames, false, appOpts.Metadata)
if err != nil {
return err
}
- baseName := meta.MetricFamilyName
-
// treat sum as a sample in an individual TimeSeries
val := pt.Sum()
if pt.Flags().NoRecordedValue() {
val = math.Float64frombits(value.StaleNaN)
}
- // sum and count of the summary should append suffix to baseName
- sumlabels := c.addLabels(baseName+sumStr, baseLabels)
- if err := c.appender.AppendSample(sumlabels, meta, startTimestamp, timestamp, val, nil); err != nil {
+ sumLabels := c.addLabels(appOpts.MetricFamilyName+sumStr, baseLabels)
+ if _, err := c.appender.Append(0, sumLabels, startTimestamp, timestamp, val, nil, nil, appOpts); err != nil {
return err
}
@@ -458,8 +456,8 @@ func (c *PrometheusConverter) addSummaryDataPoints(ctx context.Context, dataPoin
if pt.Flags().NoRecordedValue() {
val = math.Float64frombits(value.StaleNaN)
}
- countlabels := c.addLabels(baseName+countStr, baseLabels)
- if err := c.appender.AppendSample(countlabels, meta, startTimestamp, timestamp, val, nil); err != nil {
+ countLabels := c.addLabels(appOpts.MetricFamilyName+countStr, baseLabels)
+ if _, err := c.appender.Append(0, countLabels, startTimestamp, timestamp, val, nil, nil, appOpts); err != nil {
return err
}
@@ -471,8 +469,8 @@ func (c *PrometheusConverter) addSummaryDataPoints(ctx context.Context, dataPoin
val = math.Float64frombits(value.StaleNaN)
}
percentileStr := strconv.FormatFloat(qt.Quantile(), 'f', -1, 64)
- qtlabels := c.addLabels(baseName, baseLabels, quantileStr, percentileStr)
- if err := c.appender.AppendSample(qtlabels, meta, startTimestamp, timestamp, val, nil); err != nil {
+ qtlabels := c.addLabels(appOpts.MetricFamilyName, baseLabels, quantileStr, percentileStr)
+ if _, err := c.appender.Append(0, qtlabels, startTimestamp, timestamp, val, nil, nil, appOpts); err != nil {
return err
}
}
@@ -504,9 +502,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 {
@@ -530,7 +528,7 @@ func (c *PrometheusConverter) addResourceTargetInfo(resource pcommon.Resource, s
// Do not pass identifying attributes as ignoreAttrs below.
identifyingAttrs = nil
}
- meta := Metadata{
+ appOpts := storage.AOptions{
Metadata: metadata.Metadata{
Type: model.MetricTypeGauge,
Help: "Target metadata",
@@ -538,7 +536,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.Metadata{}, model.MetricNameLabel, name)
+ c.scopeLabels = savedScopeLabels
if err != nil {
return err
}
@@ -580,7 +583,8 @@ func (c *PrometheusConverter) addResourceTargetInfo(resource pcommon.Resource, s
}
c.seenTargetInfo[key] = struct{}{}
- if err := c.appender.AppendSample(lbls, meta, 0, timestampMs, float64(1), nil); err != nil {
+ _, err = c.appender.Append(0, lbls, 0, timestampMs, 1.0, nil, nil, appOpts)
+ if err != nil {
return err
}
}
@@ -596,7 +600,8 @@ func (c *PrometheusConverter) addResourceTargetInfo(resource pcommon.Resource, s
}
c.seenTargetInfo[key] = struct{}{}
- return c.appender.AppendSample(lbls, meta, 0, finalTimestampMs, float64(1), nil)
+ _, err = c.appender.Append(0, lbls, 0, finalTimestampMs, 1.0, nil, nil, appOpts)
+ return err
}
// convertTimeStamp converts OTLP timestamp in ns to timestamp in ms.
diff --git a/storage/remote/otlptranslator/prometheusremotewrite/helper_test.go b/storage/remote/otlptranslator/prometheusremotewrite/helper_test.go
index fc120c0b6a..f4f5283164 100644
--- a/storage/remote/otlptranslator/prometheusremotewrite/helper_test.go
+++ b/storage/remote/otlptranslator/prometheusremotewrite/helper_test.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Prometheus Authors
+// 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
@@ -30,12 +30,18 @@ import (
"go.opentelemetry.io/collector/pdata/pmetric"
"github.com/prometheus/prometheus/config"
+ "github.com/prometheus/prometheus/model/exemplar"
"github.com/prometheus/prometheus/model/labels"
+ "github.com/prometheus/prometheus/model/metadata"
"github.com/prometheus/prometheus/prompb"
+ "github.com/prometheus/prometheus/storage"
+ "github.com/prometheus/prometheus/util/teststorage"
"github.com/prometheus/prometheus/util/testutil"
)
-func TestCreateAttributes(t *testing.T) {
+type sample = teststorage.Sample
+
+func TestPrometheusConverter_createAttributes(t *testing.T) {
resourceAttrs := map[string]string{
"service.name": "service name",
"service.instance.id": "service ID",
@@ -386,10 +392,22 @@ func TestCreateAttributes(t *testing.T) {
"metric_multi", "multi metric",
),
},
+ {
+ name: "__name__ attribute is filtered when passed in ignoreAttrs",
+ promoteResourceAttributes: nil,
+ ignoreAttrs: []string{model.MetricNameLabel},
+ expectedLabels: labels.FromStrings(
+ "__name__", "test_metric",
+ "instance", "service ID",
+ "job", "service name",
+ "metric_attr", "metric value",
+ "metric_attr_other", "metric value other",
+ ),
+ },
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
- c := NewPrometheusConverter(&mockCombinedAppender{})
+ c := NewPrometheusConverter(teststorage.NewAppendable().AppenderV2(t.Context()))
settings := Settings{
PromoteResourceAttributes: NewPromoteResourceAttributes(config.OTLPConfig{
PromoteAllResourceAttributes: tc.promoteAllResourceAttributes,
@@ -413,12 +431,116 @@ 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.Metadata{}, model.MetricNameLabel, "test_metric")
require.NoError(t, err)
testutil.RequireEqual(t, tc.expectedLabels, lbls)
})
}
+
+ // Test that __name__ attributes in OTLP data are filtered out to prevent
+ // duplicate labels.
+ t.Run("__name__ attribute in OTLP data is filtered", func(t *testing.T) {
+ resource := pcommon.NewResource()
+ resource.Attributes().PutStr("service.name", "test-service")
+ resource.Attributes().PutStr("service.instance.id", "test-instance")
+
+ // Create attributes with __name__ to simulate problematic OTLP data.
+ attrsWithNameLabel := pcommon.NewMap()
+ attrsWithNameLabel.PutStr("__name__", "wrong_metric_name")
+ attrsWithNameLabel.PutStr("other_attr", "value")
+
+ c := NewPrometheusConverter(teststorage.NewAppendable().AppenderV2(t.Context()))
+ settings := Settings{}
+
+ require.NoError(t, c.setResourceContext(resource, settings))
+ require.NoError(t, c.setScopeContext(scope{}, settings))
+
+ // Call createAttributes with reservedLabelNames to filter __name__.
+ lbls, err := c.createAttributes(
+ attrsWithNameLabel,
+ settings,
+ reservedLabelNames,
+ true,
+ metadata.Metadata{},
+ model.MetricNameLabel, "correct_metric_name",
+ )
+ require.NoError(t, err)
+
+ // Verify there's exactly one __name__ label with the correct value.
+ nameCount := 0
+ var nameValue string
+ lbls.Range(func(l labels.Label) {
+ if l.Name == model.MetricNameLabel {
+ nameCount++
+ nameValue = l.Value
+ }
+ })
+
+ require.Equal(t, 1, nameCount)
+ require.Equal(t, "correct_metric_name", nameValue)
+ require.Equal(t, "value", lbls.Get("other_attr"))
+ })
+
+ // Test that __type__ and __unit__ attributes in OTLP data are overwritten
+ // by auto-generated labels from metadata when EnableTypeAndUnitLabels is true.
+ t.Run("__type__ and __unit__ attributes are overwritten by metadata", func(t *testing.T) {
+ resource := pcommon.NewResource()
+ resource.Attributes().PutStr("service.name", "test-service")
+ resource.Attributes().PutStr("service.instance.id", "test-instance")
+
+ // Create attributes with __type__ and __unit__ to simulate problematic OTLP data.
+ attrsWithTypeAndUnit := pcommon.NewMap()
+ attrsWithTypeAndUnit.PutStr(model.MetricTypeLabel, "wrong_type")
+ attrsWithTypeAndUnit.PutStr(model.MetricUnitLabel, "wrong_unit")
+ attrsWithTypeAndUnit.PutStr("other_attr", "value")
+
+ c := NewPrometheusConverter(teststorage.NewAppendable().AppenderV2(t.Context()))
+ settings := Settings{EnableTypeAndUnitLabels: true}
+
+ require.NoError(t, c.setResourceContext(resource, settings))
+ require.NoError(t, c.setScopeContext(scope{}, settings))
+
+ // Call createAttributes with Metadata containing correct Type and Unit.
+ lbls, err := c.createAttributes(
+ attrsWithTypeAndUnit,
+ settings,
+ reservedLabelNames,
+ true,
+ metadata.Metadata{Type: model.MetricTypeGauge, Unit: "seconds"},
+ model.MetricNameLabel, "test_metric",
+ )
+ require.NoError(t, err)
+
+ // Verify there's exactly one __type__ label with the correct value (from metadata).
+ typeCount := 0
+ var typeValue string
+ lbls.Range(func(l labels.Label) {
+ if l.Name == model.MetricTypeLabel {
+ typeCount++
+ typeValue = l.Value
+ }
+ })
+ require.Equal(t, 1, typeCount)
+ require.Equal(t, "gauge", typeValue)
+
+ // Verify there's exactly one __unit__ label with the correct value (from metadata).
+ unitCount := 0
+ var unitValue string
+ lbls.Range(func(l labels.Label) {
+ if l.Name == model.MetricUnitLabel {
+ unitCount++
+ unitValue = l.Value
+ }
+ })
+ require.Equal(t, 1, unitCount)
+ require.Equal(t, "seconds", unitValue)
+ require.Equal(t, "value", lbls.Get("other_attr"))
+ })
}
func Test_convertTimeStamp(t *testing.T) {
@@ -457,7 +579,7 @@ func TestPrometheusConverter_AddSummaryDataPoints(t *testing.T) {
metric func() pmetric.Metric
scope scope
promoteScope bool
- want func() []combinedSample
+ want func() []sample
}{
{
name: "summary with start time and without scope promotion",
@@ -474,25 +596,25 @@ func TestPrometheusConverter_AddSummaryDataPoints(t *testing.T) {
},
scope: defaultScope,
promoteScope: false,
- want: func() []combinedSample {
- return []combinedSample{
+ want: func() []sample {
+ return []sample{
{
- metricFamilyName: "test_summary",
- ls: labels.FromStrings(
+ MF: "test_summary",
+ L: labels.FromStrings(
model.MetricNameLabel, "test_summary"+sumStr,
),
- t: convertTimeStamp(ts),
- ct: convertTimeStamp(ts),
- v: 0,
+ T: convertTimeStamp(ts),
+ ST: convertTimeStamp(ts),
+ V: 0,
},
{
- metricFamilyName: "test_summary",
- ls: labels.FromStrings(
+ MF: "test_summary",
+ L: labels.FromStrings(
model.MetricNameLabel, "test_summary"+countStr,
),
- t: convertTimeStamp(ts),
- ct: convertTimeStamp(ts),
- v: 0,
+ T: convertTimeStamp(ts),
+ ST: convertTimeStamp(ts),
+ V: 0,
},
}
},
@@ -512,7 +634,7 @@ func TestPrometheusConverter_AddSummaryDataPoints(t *testing.T) {
},
scope: defaultScope,
promoteScope: true,
- want: func() []combinedSample {
+ want: func() []sample {
scopeLabels := []string{
"otel_scope_attr1", "value1",
"otel_scope_attr2", "value2",
@@ -520,22 +642,22 @@ func TestPrometheusConverter_AddSummaryDataPoints(t *testing.T) {
"otel_scope_schema_url", defaultScope.schemaURL,
"otel_scope_version", defaultScope.version,
}
- return []combinedSample{
+ return []sample{
{
- metricFamilyName: "test_summary",
- ls: labels.FromStrings(append(scopeLabels,
+ MF: "test_summary",
+ L: labels.FromStrings(append(scopeLabels,
model.MetricNameLabel, "test_summary"+sumStr)...),
- t: convertTimeStamp(ts),
- ct: convertTimeStamp(ts),
- v: 0,
+ T: convertTimeStamp(ts),
+ ST: convertTimeStamp(ts),
+ V: 0,
},
{
- metricFamilyName: "test_summary",
- ls: labels.FromStrings(append(scopeLabels,
+ MF: "test_summary",
+ L: labels.FromStrings(append(scopeLabels,
model.MetricNameLabel, "test_summary"+countStr)...),
- t: convertTimeStamp(ts),
- ct: convertTimeStamp(ts),
- v: 0,
+ T: convertTimeStamp(ts),
+ ST: convertTimeStamp(ts),
+ V: 0,
},
}
},
@@ -554,23 +676,23 @@ func TestPrometheusConverter_AddSummaryDataPoints(t *testing.T) {
},
scope: defaultScope,
promoteScope: false,
- want: func() []combinedSample {
- return []combinedSample{
+ want: func() []sample {
+ return []sample{
{
- metricFamilyName: "test_summary",
- ls: labels.FromStrings(
+ MF: "test_summary",
+ L: labels.FromStrings(
model.MetricNameLabel, "test_summary"+sumStr,
),
- t: convertTimeStamp(ts),
- v: 0,
+ T: convertTimeStamp(ts),
+ V: 0,
},
{
- metricFamilyName: "test_summary",
- ls: labels.FromStrings(
+ MF: "test_summary",
+ L: labels.FromStrings(
model.MetricNameLabel, "test_summary"+countStr,
),
- t: convertTimeStamp(ts),
- v: 0,
+ T: convertTimeStamp(ts),
+ V: 0,
},
}
},
@@ -598,41 +720,41 @@ func TestPrometheusConverter_AddSummaryDataPoints(t *testing.T) {
},
scope: defaultScope,
promoteScope: false,
- want: func() []combinedSample {
- return []combinedSample{
+ want: func() []sample {
+ return []sample{
{
- metricFamilyName: "test_summary",
- ls: labels.FromStrings(
+ MF: "test_summary",
+ L: labels.FromStrings(
model.MetricNameLabel, "test_summary"+sumStr,
),
- t: convertTimeStamp(ts),
- v: 100,
+ T: convertTimeStamp(ts),
+ V: 100,
},
{
- metricFamilyName: "test_summary",
- ls: labels.FromStrings(
+ MF: "test_summary",
+ L: labels.FromStrings(
model.MetricNameLabel, "test_summary"+countStr,
),
- t: convertTimeStamp(ts),
- v: 50,
+ T: convertTimeStamp(ts),
+ V: 50,
},
{
- metricFamilyName: "test_summary",
- ls: labels.FromStrings(
+ MF: "test_summary",
+ L: labels.FromStrings(
model.MetricNameLabel, "test_summary",
quantileStr, "0.5",
),
- t: convertTimeStamp(ts),
- v: 30,
+ T: convertTimeStamp(ts),
+ V: 30,
},
{
- metricFamilyName: "test_summary",
- ls: labels.FromStrings(
+ MF: "test_summary",
+ L: labels.FromStrings(
model.MetricNameLabel, "test_summary",
quantileStr, "0.9",
),
- t: convertTimeStamp(ts),
- v: 40,
+ T: convertTimeStamp(ts),
+ V: 40,
},
}
},
@@ -641,24 +763,28 @@ func TestPrometheusConverter_AddSummaryDataPoints(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
metric := tt.metric()
- mockAppender := &mockCombinedAppender{}
- converter := NewPrometheusConverter(mockAppender)
+ appTest := teststorage.NewAppendable()
+ app := appTest.AppenderV2(t.Context())
+ converter := NewPrometheusConverter(app)
+ settings := Settings{
+ PromoteScopeMetadata: tt.promoteScope,
+ }
+ resource := pcommon.NewResource()
- converter.addSummaryDataPoints(
+ // Initialize resource and scope context as FromMetrics would.
+ require.NoError(t, converter.setResourceContext(resource, settings))
+ require.NoError(t, converter.setScopeContext(tt.scope, settings))
+
+ require.NoError(t, converter.addSummaryDataPoints(
context.Background(),
metric.Summary().DataPoints(),
- pcommon.NewResource(),
- Settings{
- PromoteScopeMetadata: tt.promoteScope,
- },
- tt.scope,
- Metadata{
+ settings,
+ storage.AOptions{
MetricFamilyName: metric.Name(),
},
- )
- require.NoError(t, mockAppender.Commit())
-
- requireEqual(t, tt.want(), mockAppender.samples)
+ ))
+ require.NoError(t, app.Commit())
+ teststorage.RequireEqual(t, tt.want(), appTest.ResultSamples())
})
}
}
@@ -681,7 +807,7 @@ func TestPrometheusConverter_AddHistogramDataPoints(t *testing.T) {
metric func() pmetric.Metric
scope scope
promoteScope bool
- want func() []combinedSample
+ want func() []sample
}{
{
name: "histogram with start time and without scope promotion",
@@ -698,26 +824,26 @@ func TestPrometheusConverter_AddHistogramDataPoints(t *testing.T) {
},
scope: defaultScope,
promoteScope: false,
- want: func() []combinedSample {
- return []combinedSample{
+ want: func() []sample {
+ return []sample{
{
- metricFamilyName: "test_hist",
- ls: labels.FromStrings(
+ MF: "test_hist",
+ L: labels.FromStrings(
model.MetricNameLabel, "test_hist"+countStr,
),
- t: convertTimeStamp(ts),
- ct: convertTimeStamp(ts),
- v: 0,
+ T: convertTimeStamp(ts),
+ ST: convertTimeStamp(ts),
+ V: 0,
},
{
- metricFamilyName: "test_hist",
- ls: labels.FromStrings(
+ MF: "test_hist",
+ L: labels.FromStrings(
model.MetricNameLabel, "test_hist_bucket",
model.BucketLabel, "+Inf",
),
- t: convertTimeStamp(ts),
- ct: convertTimeStamp(ts),
- v: 0,
+ T: convertTimeStamp(ts),
+ ST: convertTimeStamp(ts),
+ V: 0,
},
}
},
@@ -737,7 +863,7 @@ func TestPrometheusConverter_AddHistogramDataPoints(t *testing.T) {
},
scope: defaultScope,
promoteScope: true,
- want: func() []combinedSample {
+ want: func() []sample {
scopeLabels := []string{
"otel_scope_attr1", "value1",
"otel_scope_attr2", "value2",
@@ -745,23 +871,23 @@ func TestPrometheusConverter_AddHistogramDataPoints(t *testing.T) {
"otel_scope_schema_url", defaultScope.schemaURL,
"otel_scope_version", defaultScope.version,
}
- return []combinedSample{
+ return []sample{
{
- metricFamilyName: "test_hist",
- ls: labels.FromStrings(append(scopeLabels,
+ MF: "test_hist",
+ L: labels.FromStrings(append(scopeLabels,
model.MetricNameLabel, "test_hist"+countStr)...),
- t: convertTimeStamp(ts),
- ct: convertTimeStamp(ts),
- v: 0,
+ T: convertTimeStamp(ts),
+ ST: convertTimeStamp(ts),
+ V: 0,
},
{
- metricFamilyName: "test_hist",
- ls: labels.FromStrings(append(scopeLabels,
+ MF: "test_hist",
+ L: labels.FromStrings(append(scopeLabels,
model.MetricNameLabel, "test_hist_bucket",
model.BucketLabel, "+Inf")...),
- t: convertTimeStamp(ts),
- ct: convertTimeStamp(ts),
- v: 0,
+ T: convertTimeStamp(ts),
+ ST: convertTimeStamp(ts),
+ V: 0,
},
}
},
@@ -778,24 +904,24 @@ func TestPrometheusConverter_AddHistogramDataPoints(t *testing.T) {
return metric
},
- want: func() []combinedSample {
- return []combinedSample{
+ want: func() []sample {
+ return []sample{
{
- metricFamilyName: "test_hist",
- ls: labels.FromStrings(
+ MF: "test_hist",
+ L: labels.FromStrings(
model.MetricNameLabel, "test_hist"+countStr,
),
- t: convertTimeStamp(ts),
- v: 0,
+ T: convertTimeStamp(ts),
+ V: 0,
},
{
- metricFamilyName: "test_hist",
- ls: labels.FromStrings(
+ MF: "test_hist",
+ L: labels.FromStrings(
model.MetricNameLabel, "test_hist_bucket",
model.BucketLabel, "+Inf",
),
- t: convertTimeStamp(ts),
- v: 0,
+ T: convertTimeStamp(ts),
+ V: 0,
},
}
},
@@ -804,31 +930,150 @@ func TestPrometheusConverter_AddHistogramDataPoints(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
metric := tt.metric()
- mockAppender := &mockCombinedAppender{}
- converter := NewPrometheusConverter(mockAppender)
+ appTest := teststorage.NewAppendable()
+ app := appTest.AppenderV2(t.Context())
+ converter := NewPrometheusConverter(app)
+ settings := Settings{
+ PromoteScopeMetadata: tt.promoteScope,
+ }
+ resource := pcommon.NewResource()
- converter.addHistogramDataPoints(
+ // Initialize resource and scope context as FromMetrics would.
+ require.NoError(t, converter.setResourceContext(resource, settings))
+ require.NoError(t, converter.setScopeContext(tt.scope, settings))
+
+ require.NoError(t, converter.addHistogramDataPoints(
context.Background(),
metric.Histogram().DataPoints(),
- pcommon.NewResource(),
- Settings{
- PromoteScopeMetadata: tt.promoteScope,
- },
- tt.scope,
- Metadata{
+ settings,
+ storage.AOptions{
MetricFamilyName: metric.Name(),
},
- )
- require.NoError(t, mockAppender.Commit())
-
- requireEqual(t, tt.want(), mockAppender.samples)
+ ))
+ require.NoError(t, app.Commit())
+ teststorage.RequireEqual(t, tt.want(), appTest.ResultSamples())
})
}
}
+// TestAddHistogramDataPoints_ExemplarLeakAcrossDataPoints verifies that
+// exemplars from a previous data point don't leak into _sum/_count of the
+// next data point. Regression test for stale exemplar leak.
+func TestAddHistogramDataPoints_ExemplarLeakAcrossDataPoints(t *testing.T) {
+ ts := pcommon.Timestamp(time.Now().UnixNano())
+ exTs := pcommon.Timestamp(time.Now().Add(time.Second).UnixNano())
+
+ metric := pmetric.NewMetric()
+ metric.SetName("test_hist")
+ metric.SetEmptyHistogram().SetAggregationTemporality(pmetric.AggregationTemporalityCumulative)
+
+ // First data point: has buckets and an exemplar with value 200 (> bound 100, so falls into +Inf).
+ pt1 := metric.Histogram().DataPoints().AppendEmpty()
+ pt1.SetTimestamp(ts)
+ pt1.SetStartTimestamp(ts)
+ pt1.SetSum(42)
+ pt1.SetCount(10)
+ pt1.ExplicitBounds().FromRaw([]float64{100})
+ pt1.BucketCounts().FromRaw([]uint64{7, 3})
+
+ ex := pt1.Exemplars().AppendEmpty()
+ ex.SetTimestamp(exTs)
+ ex.SetDoubleValue(200) // > 100, so falls into the +Inf bucket.
+
+ // Second data point: no exemplars.
+ pt2 := metric.Histogram().DataPoints().AppendEmpty()
+ pt2.SetTimestamp(ts)
+ pt2.SetStartTimestamp(ts)
+ pt2.SetSum(84)
+ pt2.SetCount(20)
+ pt2.ExplicitBounds().FromRaw([]float64{100})
+ pt2.BucketCounts().FromRaw([]uint64{14, 6})
+
+ appTest := teststorage.NewAppendable()
+ app := appTest.AppenderV2(t.Context())
+ converter := NewPrometheusConverter(app)
+ settings := Settings{}
+ resource := pcommon.NewResource()
+
+ require.NoError(t, converter.setResourceContext(resource, settings))
+ require.NoError(t, converter.setScopeContext(scope{}, settings))
+ require.NoError(t, converter.addHistogramDataPoints(
+ context.Background(),
+ metric.Histogram().DataPoints(),
+ settings,
+ storage.AOptions{
+ MetricFamilyName: metric.Name(),
+ },
+ ))
+ require.NoError(t, app.Commit())
+
+ exConverted := exemplar.Exemplar{
+ Value: 200,
+ Ts: convertTimeStamp(exTs),
+ HasTs: true,
+ }
+ tsMs := convertTimeStamp(ts)
+
+ want := []sample{
+ // -- First data point --
+ // _sum: no exemplars.
+ {
+ MF: "test_hist",
+ L: labels.FromStrings(model.MetricNameLabel, "test_hist_sum"),
+ T: tsMs, ST: tsMs, V: 42,
+ },
+ // _count: no exemplars.
+ {
+ MF: "test_hist",
+ L: labels.FromStrings(model.MetricNameLabel, "test_hist_count"),
+ T: tsMs, ST: tsMs, V: 10,
+ },
+ // le=100 bucket: no exemplars (exemplar value 200 > 100).
+ {
+ MF: "test_hist",
+ L: labels.FromStrings(model.MetricNameLabel, "test_hist_bucket", model.BucketLabel, "100"),
+ T: tsMs, ST: tsMs, V: 7,
+ },
+ // le=+Inf bucket: gets the exemplar.
+ {
+ MF: "test_hist",
+ L: labels.FromStrings(model.MetricNameLabel, "test_hist_bucket", model.BucketLabel, "+Inf"),
+ T: tsMs, ST: tsMs, V: 10,
+ ES: []exemplar.Exemplar{exConverted},
+ },
+ // -- Second data point --
+ // _sum: NO exemplars (this is the regression check).
+ {
+ MF: "test_hist",
+ L: labels.FromStrings(model.MetricNameLabel, "test_hist_sum"),
+ T: tsMs, ST: tsMs, V: 84,
+ },
+ // _count: NO exemplars (this is the regression check).
+ {
+ MF: "test_hist",
+ L: labels.FromStrings(model.MetricNameLabel, "test_hist_count"),
+ T: tsMs, ST: tsMs, V: 20,
+ },
+ // le=100 bucket: no exemplars.
+ {
+ MF: "test_hist",
+ L: labels.FromStrings(model.MetricNameLabel, "test_hist_bucket", model.BucketLabel, "100"),
+ T: tsMs, ST: tsMs, V: 14,
+ },
+ // le=+Inf bucket: no exemplars.
+ {
+ MF: "test_hist",
+ L: labels.FromStrings(model.MetricNameLabel, "test_hist_bucket", model.BucketLabel, "+Inf"),
+ T: tsMs, ST: tsMs, V: 20,
+ },
+ }
+
+ teststorage.RequireEqual(t, want, appTest.ResultSamples())
+}
+
func TestGetPromExemplars(t *testing.T) {
ctx := context.Background()
- c := NewPrometheusConverter(&mockCombinedAppender{})
+ c := NewPrometheusConverter(teststorage.NewAppendable().AppenderV2(t.Context()))
t.Run("Exemplars with int value", func(t *testing.T) {
es := pmetric.NewExemplarSlice()
diff --git a/storage/remote/otlptranslator/prometheusremotewrite/histograms.go b/storage/remote/otlptranslator/prometheusremotewrite/histograms.go
index 0bc8a876e4..31c16b1c10 100644
--- a/storage/remote/otlptranslator/prometheusremotewrite/histograms.go
+++ b/storage/remote/otlptranslator/prometheusremotewrite/histograms.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Prometheus Authors
+// 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
@@ -22,11 +22,11 @@ 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"
"github.com/prometheus/prometheus/model/value"
+ "github.com/prometheus/prometheus/storage"
"github.com/prometheus/prometheus/util/annotations"
)
@@ -34,9 +34,12 @@ 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,
+func (c *PrometheusConverter) addExponentialHistogramDataPoints(
+ ctx context.Context,
+ dataPoints pmetric.ExponentialHistogramDataPointSlice,
+ settings Settings,
+ temporality pmetric.AggregationTemporality,
+ appOpts storage.AOptions,
) (annotations.Annotations, error) {
var annots annotations.Annotations
for x := 0; x < dataPoints.Len(); x++ {
@@ -53,27 +56,27 @@ func (c *PrometheusConverter) addExponentialHistogramDataPoints(ctx context.Cont
}
lbls, err := c.createAttributes(
- resource,
pt.Attributes(),
- scope,
settings,
- nil,
+ reservedLabelNames,
true,
- meta,
+ appOpts.Metadata,
model.MetricNameLabel,
- meta.MetricFamilyName,
+ appOpts.MetricFamilyName,
)
if err != nil {
return annots, err
}
ts := convertTimeStamp(pt.Timestamp())
- ct := convertTimeStamp(pt.StartTimestamp())
+ st := convertTimeStamp(pt.StartTimestamp())
exemplars, err := c.getPromExemplars(ctx, pt.Exemplars())
if err != nil {
return annots, err
}
- // OTel exponential histograms are always Int Histograms.
- if err = c.appender.AppendHistogram(lbls, meta, ct, ts, hp, exemplars); err != nil {
+
+ appOpts.Exemplars = exemplars
+ // OTel exponential histograms are always integer histograms.
+ if _, err = c.appender.Append(0, lbls, st, ts, 0, hp, nil, appOpts); err != nil {
return annots, err
}
}
@@ -106,7 +109,7 @@ func exponentialToNativeHistogram(p pmetric.ExponentialHistogramDataPoint, tempo
// Sending a sample that triggers counter reset but with ResetHint==NO
// would lead to Prometheus panic as it does not double check the hint.
// Thus we're explicitly saying UNKNOWN here, which is always safe.
- // TODO: using created time stamp should be accurate, but we
+ // TODO: using start timestamp should be accurate, but we
// need to know here if it was used for the detection.
// Ref: https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/28663#issuecomment-1810577303
// Counter reset detection in Prometheus: https://github.com/prometheus/prometheus/blob/f997c72f294c0f18ca13fa06d51889af04135195/tsdb/chunkenc/histogram.go#L232
@@ -252,9 +255,12 @@ func convertBucketsLayout(bucketCounts []uint64, offset, scaleDown int32, adjust
return spans, deltas
}
-func (c *PrometheusConverter) addCustomBucketsHistogramDataPoints(ctx context.Context, dataPoints pmetric.HistogramDataPointSlice,
- resource pcommon.Resource, settings Settings, temporality pmetric.AggregationTemporality,
- scope scope, meta Metadata,
+func (c *PrometheusConverter) addCustomBucketsHistogramDataPoints(
+ ctx context.Context,
+ dataPoints pmetric.HistogramDataPointSlice,
+ settings Settings,
+ temporality pmetric.AggregationTemporality,
+ appOpts storage.AOptions,
) (annotations.Annotations, error) {
var annots annotations.Annotations
@@ -272,26 +278,26 @@ func (c *PrometheusConverter) addCustomBucketsHistogramDataPoints(ctx context.Co
}
lbls, err := c.createAttributes(
- resource,
pt.Attributes(),
- scope,
settings,
- nil,
+ reservedLabelNames,
true,
- meta,
+ appOpts.Metadata,
model.MetricNameLabel,
- meta.MetricFamilyName,
+ appOpts.MetricFamilyName,
)
if err != nil {
return annots, err
}
ts := convertTimeStamp(pt.Timestamp())
- ct := convertTimeStamp(pt.StartTimestamp())
+ st := convertTimeStamp(pt.StartTimestamp())
exemplars, err := c.getPromExemplars(ctx, pt.Exemplars())
if err != nil {
return annots, err
}
- if err = c.appender.AppendHistogram(lbls, meta, ct, ts, hp, exemplars); err != nil {
+
+ appOpts.Exemplars = exemplars
+ if _, err = c.appender.Append(0, lbls, st, ts, 0, hp, nil, appOpts); err != nil {
return annots, err
}
}
@@ -312,7 +318,7 @@ func explicitHistogramToCustomBucketsHistogram(p pmetric.HistogramDataPoint, tem
// Sending a sample that triggers counter reset but with ResetHint==NO
// would lead to Prometheus panic as it does not double check the hint.
// Thus we're explicitly saying UNKNOWN here, which is always safe.
- // TODO: using created time stamp should be accurate, but we
+ // TODO: using start timestamp should be accurate, but we
// need to know here if it was used for the detection.
// Ref: https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/28663#issuecomment-1810577303
// Counter reset detection in Prometheus: https://github.com/prometheus/prometheus/blob/f997c72f294c0f18ca13fa06d51889af04135195/tsdb/chunkenc/histogram.go#L232
diff --git a/storage/remote/otlptranslator/prometheusremotewrite/histograms_test.go b/storage/remote/otlptranslator/prometheusremotewrite/histograms_test.go
index adb0cf8eee..5422796002 100644
--- a/storage/remote/otlptranslator/prometheusremotewrite/histograms_test.go
+++ b/storage/remote/otlptranslator/prometheusremotewrite/histograms_test.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Prometheus Authors
+// 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
@@ -32,6 +32,8 @@ import (
"github.com/prometheus/prometheus/model/histogram"
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/model/metadata"
+ "github.com/prometheus/prometheus/storage"
+ "github.com/prometheus/prometheus/util/teststorage"
)
type expectedBucketLayout struct {
@@ -382,8 +384,8 @@ func TestConvertBucketsLayout(t *testing.T) {
for scaleDown, wantLayout := range tt.wantLayout {
t.Run(fmt.Sprintf("%s-scaleby-%d", tt.name, scaleDown), func(t *testing.T) {
gotSpans, gotDeltas := convertBucketsLayout(tt.buckets().BucketCounts().AsRaw(), tt.buckets().Offset(), scaleDown, true)
- requireEqual(t, wantLayout.wantSpans, gotSpans)
- requireEqual(t, wantLayout.wantDeltas, gotDeltas)
+ require.Equal(t, wantLayout.wantSpans, gotSpans)
+ require.Equal(t, wantLayout.wantDeltas, gotDeltas)
})
}
}
@@ -633,7 +635,7 @@ func TestPrometheusConverter_addExponentialHistogramDataPoints(t *testing.T) {
metric func() pmetric.Metric
scope scope
promoteScope bool
- wantSeries func() []combinedHistogram
+ wantSeries func() []sample
}{
{
name: "histogram data points with same labels and without scope promotion",
@@ -662,19 +664,19 @@ func TestPrometheusConverter_addExponentialHistogramDataPoints(t *testing.T) {
},
scope: defaultScope,
promoteScope: false,
- wantSeries: func() []combinedHistogram {
+ wantSeries: func() []sample {
lbls := labels.FromStrings(
model.MetricNameLabel, "test_hist",
"attr", "test_attr",
)
- return []combinedHistogram{
+ return []sample{
{
- metricFamilyName: "test_hist",
- ls: lbls,
- meta: metadata.Metadata{},
- t: 0,
- ct: 0,
- h: &histogram.Histogram{
+ MF: "test_hist",
+ L: lbls,
+ M: metadata.Metadata{},
+ T: 0,
+ ST: 0,
+ H: &histogram.Histogram{
Count: 7,
Schema: 1,
ZeroThreshold: defaultZeroThreshold,
@@ -682,15 +684,15 @@ func TestPrometheusConverter_addExponentialHistogramDataPoints(t *testing.T) {
PositiveSpans: []histogram.Span{{Offset: 0, Length: 2}},
PositiveBuckets: []int64{4, -2},
},
- es: []exemplar.Exemplar{{Value: 1}},
+ ES: []exemplar.Exemplar{{Value: 1}},
},
{
- metricFamilyName: "test_hist",
- ls: lbls,
- meta: metadata.Metadata{},
- t: 0,
- ct: 0,
- h: &histogram.Histogram{
+ MF: "test_hist",
+ L: lbls,
+ M: metadata.Metadata{},
+ T: 0,
+ ST: 0,
+ H: &histogram.Histogram{
Count: 4,
Schema: 1,
ZeroThreshold: defaultZeroThreshold,
@@ -698,7 +700,7 @@ func TestPrometheusConverter_addExponentialHistogramDataPoints(t *testing.T) {
PositiveSpans: []histogram.Span{{Offset: 0, Length: 3}},
PositiveBuckets: []int64{4, -2, -1},
},
- es: []exemplar.Exemplar{{Value: 2}},
+ ES: []exemplar.Exemplar{{Value: 2}},
},
}
},
@@ -730,7 +732,7 @@ func TestPrometheusConverter_addExponentialHistogramDataPoints(t *testing.T) {
},
scope: defaultScope,
promoteScope: true,
- wantSeries: func() []combinedHistogram {
+ wantSeries: func() []sample {
lbls := labels.FromStrings(
model.MetricNameLabel, "test_hist",
"attr", "test_attr",
@@ -740,14 +742,14 @@ func TestPrometheusConverter_addExponentialHistogramDataPoints(t *testing.T) {
"otel_scope_attr1", "value1",
"otel_scope_attr2", "value2",
)
- return []combinedHistogram{
+ return []sample{
{
- metricFamilyName: "test_hist",
- ls: lbls,
- meta: metadata.Metadata{},
- t: 0,
- ct: 0,
- h: &histogram.Histogram{
+ MF: "test_hist",
+ L: lbls,
+ M: metadata.Metadata{},
+ T: 0,
+ ST: 0,
+ H: &histogram.Histogram{
Count: 7,
Schema: 1,
ZeroThreshold: defaultZeroThreshold,
@@ -755,15 +757,15 @@ func TestPrometheusConverter_addExponentialHistogramDataPoints(t *testing.T) {
PositiveSpans: []histogram.Span{{Offset: 0, Length: 2}},
PositiveBuckets: []int64{4, -2},
},
- es: []exemplar.Exemplar{{Value: 1}},
+ ES: []exemplar.Exemplar{{Value: 1}},
},
{
- metricFamilyName: "test_hist",
- ls: lbls,
- meta: metadata.Metadata{},
- t: 0,
- ct: 0,
- h: &histogram.Histogram{
+ MF: "test_hist",
+ L: lbls,
+ M: metadata.Metadata{},
+ T: 0,
+ ST: 0,
+ H: &histogram.Histogram{
Count: 4,
Schema: 1,
ZeroThreshold: defaultZeroThreshold,
@@ -771,7 +773,7 @@ func TestPrometheusConverter_addExponentialHistogramDataPoints(t *testing.T) {
PositiveSpans: []histogram.Span{{Offset: 0, Length: 3}},
PositiveBuckets: []int64{4, -2, -1},
},
- es: []exemplar.Exemplar{{Value: 2}},
+ ES: []exemplar.Exemplar{{Value: 2}},
},
}
},
@@ -803,7 +805,7 @@ func TestPrometheusConverter_addExponentialHistogramDataPoints(t *testing.T) {
},
scope: defaultScope,
promoteScope: false,
- wantSeries: func() []combinedHistogram {
+ wantSeries: func() []sample {
lbls := labels.FromStrings(
model.MetricNameLabel, "test_hist",
"attr", "test_attr",
@@ -813,14 +815,14 @@ func TestPrometheusConverter_addExponentialHistogramDataPoints(t *testing.T) {
"attr", "test_attr_two",
)
- return []combinedHistogram{
+ return []sample{
{
- metricFamilyName: "test_hist",
- ls: lbls,
- meta: metadata.Metadata{},
- t: 0,
- ct: 0,
- h: &histogram.Histogram{
+ MF: "test_hist",
+ L: lbls,
+ M: metadata.Metadata{},
+ T: 0,
+ ST: 0,
+ H: &histogram.Histogram{
Count: 7,
Schema: 1,
ZeroThreshold: defaultZeroThreshold,
@@ -828,15 +830,15 @@ func TestPrometheusConverter_addExponentialHistogramDataPoints(t *testing.T) {
PositiveSpans: []histogram.Span{{Offset: 0, Length: 2}},
PositiveBuckets: []int64{4, -2},
},
- es: []exemplar.Exemplar{{Value: 1}},
+ ES: []exemplar.Exemplar{{Value: 1}},
},
{
- metricFamilyName: "test_hist",
- ls: labelsAnother,
- meta: metadata.Metadata{},
- t: 0,
- ct: 0,
- h: &histogram.Histogram{
+ MF: "test_hist",
+ L: labelsAnother,
+ M: metadata.Metadata{},
+ T: 0,
+ ST: 0,
+ H: &histogram.Histogram{
Count: 4,
Schema: 1,
ZeroThreshold: defaultZeroThreshold,
@@ -844,7 +846,7 @@ func TestPrometheusConverter_addExponentialHistogramDataPoints(t *testing.T) {
NegativeSpans: []histogram.Span{{Offset: 0, Length: 3}},
NegativeBuckets: []int64{4, -2, -1},
},
- es: []exemplar.Exemplar{{Value: 2}},
+ ES: []exemplar.Exemplar{{Value: 2}},
},
}
},
@@ -854,32 +856,37 @@ func TestPrometheusConverter_addExponentialHistogramDataPoints(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
metric := tt.metric()
- mockAppender := &mockCombinedAppender{}
- converter := NewPrometheusConverter(mockAppender)
+ appTest := teststorage.NewAppendable()
+ app := appTest.AppenderV2(t.Context())
+ converter := NewPrometheusConverter(app)
namer := otlptranslator.MetricNamer{
WithMetricSuffixes: true,
}
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{
+ storage.AOptions{
MetricFamilyName: name,
},
)
require.NoError(t, err)
require.Empty(t, annots)
- require.NoError(t, mockAppender.Commit())
-
- requireEqual(t, tt.wantSeries(), mockAppender.histograms)
+ require.NoError(t, app.Commit())
+ teststorage.RequireEqual(t, tt.wantSeries(), appTest.ResultSamples())
})
}
}
@@ -1106,7 +1113,7 @@ func TestPrometheusConverter_addCustomBucketsHistogramDataPoints(t *testing.T) {
metric func() pmetric.Metric
scope scope
promoteScope bool
- wantSeries func() []combinedHistogram
+ wantSeries func() []sample
}{
{
name: "histogram data points with same labels and without scope promotion",
@@ -1135,19 +1142,19 @@ func TestPrometheusConverter_addCustomBucketsHistogramDataPoints(t *testing.T) {
},
scope: defaultScope,
promoteScope: false,
- wantSeries: func() []combinedHistogram {
+ wantSeries: func() []sample {
lbls := labels.FromStrings(
model.MetricNameLabel, "test_hist_to_nhcb",
"attr", "test_attr",
)
- return []combinedHistogram{
+ return []sample{
{
- metricFamilyName: "test_hist_to_nhcb",
- ls: lbls,
- meta: metadata.Metadata{},
- t: 0,
- ct: 0,
- h: &histogram.Histogram{
+ MF: "test_hist_to_nhcb",
+ L: lbls,
+ M: metadata.Metadata{},
+ T: 0,
+ ST: 0,
+ H: &histogram.Histogram{
Count: 3,
Sum: 3,
Schema: -53,
@@ -1155,15 +1162,15 @@ func TestPrometheusConverter_addCustomBucketsHistogramDataPoints(t *testing.T) {
PositiveBuckets: []int64{2, -2, 1},
CustomValues: []float64{5, 10},
},
- es: []exemplar.Exemplar{{Value: 1}},
+ ES: []exemplar.Exemplar{{Value: 1}},
},
{
- metricFamilyName: "test_hist_to_nhcb",
- ls: lbls,
- meta: metadata.Metadata{},
- t: 0,
- ct: 0,
- h: &histogram.Histogram{
+ MF: "test_hist_to_nhcb",
+ L: lbls,
+ M: metadata.Metadata{},
+ T: 0,
+ ST: 0,
+ H: &histogram.Histogram{
Count: 11,
Sum: 5,
Schema: -53,
@@ -1171,7 +1178,7 @@ func TestPrometheusConverter_addCustomBucketsHistogramDataPoints(t *testing.T) {
PositiveBuckets: []int64{3, 5, -8},
CustomValues: []float64{0, 1},
},
- es: []exemplar.Exemplar{{Value: 2}},
+ ES: []exemplar.Exemplar{{Value: 2}},
},
}
},
@@ -1203,7 +1210,7 @@ func TestPrometheusConverter_addCustomBucketsHistogramDataPoints(t *testing.T) {
},
scope: defaultScope,
promoteScope: true,
- wantSeries: func() []combinedHistogram {
+ wantSeries: func() []sample {
lbls := labels.FromStrings(
model.MetricNameLabel, "test_hist_to_nhcb",
"attr", "test_attr",
@@ -1213,14 +1220,14 @@ func TestPrometheusConverter_addCustomBucketsHistogramDataPoints(t *testing.T) {
"otel_scope_attr1", "value1",
"otel_scope_attr2", "value2",
)
- return []combinedHistogram{
+ return []sample{
{
- metricFamilyName: "test_hist_to_nhcb",
- ls: lbls,
- meta: metadata.Metadata{},
- t: 0,
- ct: 0,
- h: &histogram.Histogram{
+ MF: "test_hist_to_nhcb",
+ L: lbls,
+ M: metadata.Metadata{},
+ T: 0,
+ ST: 0,
+ H: &histogram.Histogram{
Count: 3,
Sum: 3,
Schema: -53,
@@ -1228,15 +1235,15 @@ func TestPrometheusConverter_addCustomBucketsHistogramDataPoints(t *testing.T) {
PositiveBuckets: []int64{2, -2, 1},
CustomValues: []float64{5, 10},
},
- es: []exemplar.Exemplar{{Value: 1}},
+ ES: []exemplar.Exemplar{{Value: 1}},
},
{
- metricFamilyName: "test_hist_to_nhcb",
- ls: lbls,
- meta: metadata.Metadata{},
- t: 0,
- ct: 0,
- h: &histogram.Histogram{
+ MF: "test_hist_to_nhcb",
+ L: lbls,
+ M: metadata.Metadata{},
+ T: 0,
+ ST: 0,
+ H: &histogram.Histogram{
Count: 11,
Sum: 5,
Schema: -53,
@@ -1244,7 +1251,7 @@ func TestPrometheusConverter_addCustomBucketsHistogramDataPoints(t *testing.T) {
PositiveBuckets: []int64{3, 5, -8},
CustomValues: []float64{0, 1},
},
- es: []exemplar.Exemplar{{Value: 2}},
+ ES: []exemplar.Exemplar{{Value: 2}},
},
}
},
@@ -1276,7 +1283,7 @@ func TestPrometheusConverter_addCustomBucketsHistogramDataPoints(t *testing.T) {
},
scope: defaultScope,
promoteScope: false,
- wantSeries: func() []combinedHistogram {
+ wantSeries: func() []sample {
lbls := labels.FromStrings(
model.MetricNameLabel, "test_hist_to_nhcb",
"attr", "test_attr",
@@ -1286,14 +1293,14 @@ func TestPrometheusConverter_addCustomBucketsHistogramDataPoints(t *testing.T) {
"attr", "test_attr_two",
)
- return []combinedHistogram{
+ return []sample{
{
- metricFamilyName: "test_hist_to_nhcb",
- ls: lbls,
- meta: metadata.Metadata{},
- t: 0,
- ct: 0,
- h: &histogram.Histogram{
+ MF: "test_hist_to_nhcb",
+ L: lbls,
+ M: metadata.Metadata{},
+ T: 0,
+ ST: 0,
+ H: &histogram.Histogram{
Count: 6,
Sum: 3,
Schema: -53,
@@ -1301,15 +1308,15 @@ func TestPrometheusConverter_addCustomBucketsHistogramDataPoints(t *testing.T) {
PositiveBuckets: []int64{4, -2},
CustomValues: []float64{0, 1},
},
- es: []exemplar.Exemplar{{Value: 1}},
+ ES: []exemplar.Exemplar{{Value: 1}},
},
{
- metricFamilyName: "test_hist_to_nhcb",
- ls: labelsAnother,
- meta: metadata.Metadata{},
- t: 0,
- ct: 0,
- h: &histogram.Histogram{
+ MF: "test_hist_to_nhcb",
+ L: labelsAnother,
+ M: metadata.Metadata{},
+ T: 0,
+ ST: 0,
+ H: &histogram.Histogram{
Count: 11,
Sum: 5,
Schema: -53,
@@ -1317,7 +1324,7 @@ func TestPrometheusConverter_addCustomBucketsHistogramDataPoints(t *testing.T) {
PositiveBuckets: []int64{3, 5},
CustomValues: []float64{0, 1},
},
- es: []exemplar.Exemplar{{Value: 2}},
+ ES: []exemplar.Exemplar{{Value: 2}},
},
}
},
@@ -1327,24 +1334,30 @@ func TestPrometheusConverter_addCustomBucketsHistogramDataPoints(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
metric := tt.metric()
- mockAppender := &mockCombinedAppender{}
- converter := NewPrometheusConverter(mockAppender)
+ appTest := teststorage.NewAppendable()
+ app := appTest.AppenderV2(t.Context())
+ converter := NewPrometheusConverter(app)
namer := otlptranslator.MetricNamer{
WithMetricSuffixes: true,
}
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{
+ storage.AOptions{
MetricFamilyName: name,
},
)
@@ -1352,9 +1365,8 @@ func TestPrometheusConverter_addCustomBucketsHistogramDataPoints(t *testing.T) {
require.NoError(t, err)
require.Empty(t, annots)
- require.NoError(t, mockAppender.Commit())
-
- requireEqual(t, tt.wantSeries(), mockAppender.histograms)
+ require.NoError(t, app.Commit())
+ teststorage.RequireEqual(t, tt.wantSeries(), appTest.ResultSamples())
})
}
}
diff --git a/storage/remote/otlptranslator/prometheusremotewrite/metrics_to_prw.go b/storage/remote/otlptranslator/prometheusremotewrite/metrics_to_prw.go
index 5e575e6174..600282af6f 100644
--- a/storage/remote/otlptranslator/prometheusremotewrite/metrics_to_prw.go
+++ b/storage/remote/otlptranslator/prometheusremotewrite/metrics_to_prw.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Prometheus Authors
+// 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
@@ -26,11 +26,12 @@ 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"
"github.com/prometheus/prometheus/model/metadata"
+ "github.com/prometheus/prometheus/storage"
"github.com/prometheus/prometheus/util/annotations"
)
@@ -62,14 +63,41 @@ 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
scratchBuilder labels.ScratchBuilder
builder *labels.Builder
- appender CombinedAppender
+ appender storage.AppenderV2
// 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.
@@ -78,14 +106,29 @@ type targetInfoKey struct {
timestamp int64
}
-func NewPrometheusConverter(appender CombinedAppender) *PrometheusConverter {
+func NewPrometheusConverter(appender storage.AppenderV2) *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(),
@@ -128,7 +171,7 @@ func newScopeFromScopeMetrics(scopeMetrics pmetric.ScopeMetrics) scope {
}
}
-// FromMetrics converts pmetric.Metrics to Prometheus remote write format.
+// FromMetrics appends pmetric.Metrics to storage.AppenderV2.
func (c *PrometheusConverter) FromMetrics(ctx context.Context, md pmetric.Metrics, settings Settings) (annots annotations.Annotations, errs error) {
namer := otlptranslator.MetricNamer{
Namespace: settings.Namespace,
@@ -140,31 +183,33 @@ func (c *PrometheusConverter) FromMetrics(ctx context.Context, md pmetric.Metric
c.seenTargetInfo = make(map[targetInfoKey]struct{})
resourceMetricsSlice := md.ResourceMetrics()
- numMetrics := 0
- for i := 0; i < resourceMetricsSlice.Len(); i++ {
- scopeMetricsSlice := resourceMetricsSlice.At(i).ScopeMetrics()
- for j := 0; j < scopeMetricsSlice.Len(); j++ {
- numMetrics += scopeMetricsSlice.At(j).Metrics().Len()
- }
- }
-
- 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
}
@@ -172,7 +217,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
}
@@ -183,16 +228,17 @@ 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{
+
+ appOpts := storage.AOptions{
Metadata: metadata.Metadata{
Type: otelMetricTypeToPromMetricType(metric),
Unit: unitNamer.Build(metric.Unit()),
@@ -207,11 +253,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, appOpts); err != nil {
+ errs = errors.Join(errs, err)
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return annots, errs
}
@@ -219,11 +265,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, appOpts); err != nil {
+ errs = errors.Join(errs, err)
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return annots, errs
}
@@ -231,23 +277,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, appOpts,
)
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, appOpts); err != nil {
+ errs = errors.Join(errs, err)
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return annots, errs
}
@@ -256,21 +302,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,
+ appOpts,
)
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
}
@@ -278,17 +322,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, appOpts); 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"))
}
}
}
@@ -296,7 +340,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)
}
}
}
@@ -319,8 +363,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
}
@@ -330,13 +377,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
})
@@ -346,15 +391,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 341ee797cf..647105e640 100644
--- a/storage/remote/otlptranslator/prometheusremotewrite/metrics_to_prw_test.go
+++ b/storage/remote/otlptranslator/prometheusremotewrite/metrics_to_prw_test.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Prometheus Authors
+// 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
@@ -22,20 +22,19 @@ import (
"testing"
"time"
- "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/model"
- "github.com/prometheus/common/promslog"
"github.com/prometheus/otlptranslator"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/collector/pdata/pcommon"
"go.opentelemetry.io/collector/pdata/pmetric"
"go.opentelemetry.io/collector/pdata/pmetric/pmetricotlp"
- "github.com/prometheus/prometheus/model/exemplar"
+ "github.com/prometheus/prometheus/config"
"github.com/prometheus/prometheus/model/histogram"
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/model/metadata"
"github.com/prometheus/prometheus/storage"
+ "github.com/prometheus/prometheus/util/teststorage"
)
func TestFromMetrics(t *testing.T) {
@@ -81,8 +80,9 @@ func TestFromMetrics(t *testing.T) {
},
} {
t.Run(tc.name, func(t *testing.T) {
- mockAppender := &mockCombinedAppender{}
- converter := NewPrometheusConverter(mockAppender)
+ appTest := teststorage.NewAppendable()
+ app := appTest.AppenderV2(t.Context())
+ converter := NewPrometheusConverter(app)
payload, wantPromMetrics := createExportRequest(5, 128, 128, 2, 0, tc.settings, tc.temporality)
seenFamilyNames := map[string]struct{}{}
for _, wantMetric := range wantPromMetrics {
@@ -104,14 +104,14 @@ func TestFromMetrics(t *testing.T) {
require.NoError(t, err)
require.Empty(t, annots)
- require.NoError(t, mockAppender.Commit())
+ require.NoError(t, app.Commit())
- ts := mockAppender.samples
- require.Len(t, ts, 1536+1) // +1 for the target_info.
+ got := appTest.ResultSamples()
+ require.Len(t, got, 1536+1) // +1 for the target_info.
tgtInfoCount := 0
- for _, s := range ts {
- lbls := s.ls
+ for _, s := range got {
+ lbls := s.L
if lbls.Get(labels.MetricName) == "target_info" {
tgtInfoCount++
require.Equal(t, "test-namespace/test-service", lbls.Get("job"))
@@ -150,11 +150,14 @@ func TestFromMetrics(t *testing.T) {
h.SetCount(15)
h.SetSum(155)
+ h.BucketCounts().FromRaw([]uint64{3, 11, 0})
+ h.ExplicitBounds().FromRaw([]float64{0.124, 1.123})
generateAttributes(h.Attributes(), "series", 1)
- mockAppender := &mockCombinedAppender{}
- converter := NewPrometheusConverter(mockAppender)
+ appTest := teststorage.NewAppendable()
+ app := appTest.AppenderV2(t.Context())
+ converter := NewPrometheusConverter(app)
annots, err := converter.FromMetrics(
context.Background(),
request.Metrics(),
@@ -162,21 +165,56 @@ func TestFromMetrics(t *testing.T) {
)
require.NoError(t, err)
require.Empty(t, annots)
- require.NoError(t, mockAppender.Commit())
+ require.NoError(t, app.Commit())
- if convertHistogramsToNHCB {
- require.Len(t, mockAppender.histograms, 1)
- require.Empty(t, mockAppender.samples)
- } else {
- require.Empty(t, mockAppender.histograms)
- require.Len(t, mockAppender.samples, 3)
+ expectedSamples := []sample{
+ {
+ MF: "histogram_1", M: metadata.Metadata{Type: model.MetricTypeHistogram},
+ L: labels.FromStrings("__name__", "histogram_1_sum", "series_name_1", "value-1"),
+ T: ts.AsTime().UnixMilli(), V: 155,
+ },
+ {
+ MF: "histogram_1", M: metadata.Metadata{Type: model.MetricTypeHistogram},
+ L: labels.FromStrings("__name__", "histogram_1_count", "series_name_1", "value-1"),
+ T: ts.AsTime().UnixMilli(), V: 15,
+ },
+ {
+ MF: "histogram_1", M: metadata.Metadata{Type: model.MetricTypeHistogram},
+ L: labels.FromStrings("__name__", "histogram_1_bucket", "le", "0.124", "series_name_1", "value-1"),
+ T: ts.AsTime().UnixMilli(), V: 3,
+ },
+ {
+ MF: "histogram_1", M: metadata.Metadata{Type: model.MetricTypeHistogram},
+ L: labels.FromStrings("__name__", "histogram_1_bucket", "le", "1.123", "series_name_1", "value-1"),
+ T: ts.AsTime().UnixMilli(), V: 14,
+ },
+ {
+ MF: "histogram_1", M: metadata.Metadata{Type: model.MetricTypeHistogram},
+ L: labels.FromStrings("__name__", "histogram_1_bucket", "le", "+Inf", "series_name_1", "value-1"),
+ T: ts.AsTime().UnixMilli(), V: 15,
+ },
}
+ if convertHistogramsToNHCB {
+ expectedSamples = []sample{
+ {
+ MF: "histogram_1", M: metadata.Metadata{Type: model.MetricTypeHistogram},
+ L: labels.FromStrings("__name__", "histogram_1", "series_name_1", "value-1"),
+ T: ts.AsTime().UnixMilli(), H: &histogram.Histogram{
+ Schema: -53, Count: 15, Sum: 155,
+ PositiveSpans: []histogram.Span{{Offset: 0, Length: 3}},
+ PositiveBuckets: []int64{3, 8, -11},
+ CustomValues: []float64{0.124, 1.123},
+ },
+ },
+ }
+ }
+ teststorage.RequireEqual(t, expectedSamples, appTest.ResultSamples())
})
}
t.Run("context cancellation", func(t *testing.T) {
settings := Settings{}
- converter := NewPrometheusConverter(&mockCombinedAppender{})
+ converter := NewPrometheusConverter(teststorage.NewAppendable().AppenderV2(t.Context()))
ctx, cancel := context.WithCancel(context.Background())
// Verify that converter.FromMetrics respects cancellation.
cancel()
@@ -189,7 +227,7 @@ func TestFromMetrics(t *testing.T) {
t.Run("context timeout", func(t *testing.T) {
settings := Settings{}
- converter := NewPrometheusConverter(&mockCombinedAppender{})
+ converter := NewPrometheusConverter(teststorage.NewAppendable().AppenderV2(t.Context()))
// Verify that converter.FromMetrics respects timeout.
ctx, cancel := context.WithTimeout(context.Background(), 0)
t.Cleanup(cancel)
@@ -222,7 +260,7 @@ func TestFromMetrics(t *testing.T) {
generateAttributes(h.Attributes(), "series", 10)
}
- converter := NewPrometheusConverter(&mockCombinedAppender{})
+ converter := NewPrometheusConverter(teststorage.NewAppendable().AppenderV2(t.Context()))
annots, err := converter.FromMetrics(context.Background(), request.Metrics(), Settings{})
require.NoError(t, err)
require.NotEmpty(t, annots)
@@ -255,7 +293,7 @@ func TestFromMetrics(t *testing.T) {
generateAttributes(h.Attributes(), "series", 10)
}
- converter := NewPrometheusConverter(&mockCombinedAppender{})
+ converter := NewPrometheusConverter(teststorage.NewAppendable().AppenderV2(t.Context()))
annots, err := converter.FromMetrics(
context.Background(),
request.Metrics(),
@@ -303,8 +341,9 @@ func TestFromMetrics(t *testing.T) {
}
}
- mockAppender := &mockCombinedAppender{}
- converter := NewPrometheusConverter(mockAppender)
+ appTest := teststorage.NewAppendable()
+ app := appTest.AppenderV2(t.Context())
+ converter := NewPrometheusConverter(app)
annots, err := converter.FromMetrics(
context.Background(),
request.Metrics(),
@@ -314,8 +353,11 @@ func TestFromMetrics(t *testing.T) {
)
require.NoError(t, err)
require.Empty(t, annots)
- require.NoError(t, mockAppender.Commit())
- require.Len(t, mockAppender.samples, 22)
+ require.NoError(t, app.Commit())
+
+ got := appTest.ResultSamples()
+ require.Len(t, got, 22)
+
// There should be a target_info sample at the earliest metric timestamp, then two spaced lookback delta/2 apart,
// then one at the latest metric timestamp.
targetInfoLabels := labels.FromStrings(
@@ -332,36 +374,36 @@ func TestFromMetrics(t *testing.T) {
Type: model.MetricTypeGauge,
Help: "Target metadata",
}
- requireEqual(t, []combinedSample{
+ teststorage.RequireEqual(t, []sample{
{
- metricFamilyName: "target_info",
- v: 1,
- t: ts.AsTime().UnixMilli(),
- ls: targetInfoLabels,
- meta: targetInfoMeta,
+ MF: "target_info",
+ V: 1,
+ T: ts.AsTime().UnixMilli(),
+ L: targetInfoLabels,
+ M: targetInfoMeta,
},
{
- metricFamilyName: "target_info",
- v: 1,
- t: ts.AsTime().Add(defaultLookbackDelta / 2).UnixMilli(),
- ls: targetInfoLabels,
- meta: targetInfoMeta,
+ MF: "target_info",
+ V: 1,
+ T: ts.AsTime().Add(defaultLookbackDelta / 2).UnixMilli(),
+ L: targetInfoLabels,
+ M: targetInfoMeta,
},
{
- metricFamilyName: "target_info",
- v: 1,
- t: ts.AsTime().Add(defaultLookbackDelta).UnixMilli(),
- ls: targetInfoLabels,
- meta: targetInfoMeta,
+ MF: "target_info",
+ V: 1,
+ T: ts.AsTime().Add(defaultLookbackDelta).UnixMilli(),
+ L: targetInfoLabels,
+ M: targetInfoMeta,
},
{
- metricFamilyName: "target_info",
- v: 1,
- t: ts.AsTime().Add(defaultLookbackDelta + defaultLookbackDelta/4).UnixMilli(),
- ls: targetInfoLabels,
- meta: targetInfoMeta,
+ MF: "target_info",
+ V: 1,
+ T: ts.AsTime().Add(defaultLookbackDelta + defaultLookbackDelta/4).UnixMilli(),
+ L: targetInfoLabels,
+ M: targetInfoMeta,
},
- }, mockAppender.samples[len(mockAppender.samples)-4:])
+ }, got[len(got)-4:])
})
t.Run("target_info deduplication across multiple resources with same labels", func(t *testing.T) {
@@ -403,8 +445,9 @@ func TestFromMetrics(t *testing.T) {
generateAttributes(point2.Attributes(), "series", 1)
}
- mockAppender := &mockCombinedAppender{}
- converter := NewPrometheusConverter(mockAppender)
+ appTest := teststorage.NewAppendable()
+ app := appTest.AppenderV2(t.Context())
+ converter := NewPrometheusConverter(app)
annots, err := converter.FromMetrics(
context.Background(),
request.Metrics(),
@@ -414,11 +457,11 @@ func TestFromMetrics(t *testing.T) {
)
require.NoError(t, err)
require.Empty(t, annots)
- require.NoError(t, mockAppender.Commit())
+ require.NoError(t, app.Commit())
- var targetInfoSamples []combinedSample
- for _, s := range mockAppender.samples {
- if s.ls.Get(labels.MetricName) == "target_info" {
+ var targetInfoSamples []sample
+ for _, s := range appTest.ResultSamples() {
+ if s.L.Get(labels.MetricName) == "target_info" {
targetInfoSamples = append(targetInfoSamples, s)
}
}
@@ -439,36 +482,244 @@ func TestFromMetrics(t *testing.T) {
Type: model.MetricTypeGauge,
Help: "Target metadata",
}
- requireEqual(t, []combinedSample{
+ teststorage.RequireEqual(t, []sample{
{
- metricFamilyName: "target_info",
- v: 1,
- t: ts.AsTime().UnixMilli(),
- ls: targetInfoLabels,
- meta: targetInfoMeta,
+ MF: "target_info",
+ V: 1,
+ T: ts.AsTime().UnixMilli(),
+ L: targetInfoLabels,
+ M: targetInfoMeta,
},
{
- metricFamilyName: "target_info",
- v: 1,
- t: ts.AsTime().Add(defaultLookbackDelta / 2).UnixMilli(),
- ls: targetInfoLabels,
- meta: targetInfoMeta,
+ MF: "target_info",
+ V: 1,
+ T: ts.AsTime().Add(defaultLookbackDelta / 2).UnixMilli(),
+ L: targetInfoLabels,
+ M: targetInfoMeta,
},
}, 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)
+
+ appTest := teststorage.NewAppendable()
+ app := appTest.AppenderV2(t.Context())
+ converter := NewPrometheusConverter(app)
+ annots, err := converter.FromMetrics(
+ context.Background(),
+ request.Metrics(),
+ Settings{
+ PromoteScopeMetadata: true,
+ LookbackDelta: defaultLookbackDelta,
+ },
+ )
+ require.NoError(t, err)
+ require.Empty(t, annots)
+ require.NoError(t, app.Commit())
+
+ // Find target_info samples.
+ var targetInfoSamples []sample
+ for _, s := range appTest.ResultSamples() {
+ if s.L.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.L.Get("otel_scope_name"), "target_info should not have otel_scope_name")
+ require.Empty(t, s.L.Get("otel_scope_version"), "target_info should not have otel_scope_version")
+ require.Empty(t, s.L.Get("otel_scope_schema_url"), "target_info should not have otel_scope_schema_url")
+ require.Empty(t, s.L.Get("otel_scope_scope_attr"), "target_info should not have scope attributes")
+ }
+
+ // Verify the metric itself DOES have scope labels.
+ var metricSamples []sample
+ for _, s := range appTest.ResultSamples() {
+ if s.L.Get(labels.MetricName) == "test_gauge" {
+ metricSamples = append(metricSamples, s)
+ }
+ }
+
+ require.NotEmpty(t, metricSamples, "expected metric samples")
+ require.Equal(t, "my-scope", metricSamples[0].L.Get("otel_scope_name"), "metric should have otel_scope_name")
+ require.Equal(t, "1.0.0", metricSamples[0].L.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)
+
+ appTest := teststorage.NewAppendable()
+ app := appTest.AppenderV2(t.Context())
+ converter := NewPrometheusConverter(app)
+ 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, app.Commit())
+
+ // Find target_info samples.
+ var targetInfoSamples []sample
+ for _, s := range appTest.ResultSamples() {
+ if s.L.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.L.Get("custom_promoted_attr"), "target_info should have promoted resource attributes")
+ require.Equal(t, "another-value", s.L.Get("another_resource_attr"), "target_info should have non-promoted resource attributes")
+ }
+
+ // Verify the metric also has the promoted resource attribute.
+ var metricSamples []sample
+ for _, s := range appTest.ResultSamples() {
+ if s.L.Get(labels.MetricName) == "test_gauge" {
+ metricSamples = append(metricSamples, s)
+ }
+ }
+ require.NotEmpty(t, metricSamples, "expected metric samples")
+ require.Equal(t, "promoted-value", metricSamples[0].L.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)
+
+ appTest := teststorage.NewAppendable()
+ app := appTest.AppenderV2(t.Context())
+ converter := NewPrometheusConverter(app)
+ 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, app.Commit())
+
+ var targetInfoSamples []sample
+ for _, s := range appTest.ResultSamples() {
+ if s.L.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.L.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.L.Get("service_name"), "target_info should have service.name when KeepIdentifyingResourceAttributes is true")
+ require.Equal(t, "test-namespace", s.L.Get("service_namespace"), "target_info should have service.namespace when KeepIdentifyingResourceAttributes is true")
+ require.Equal(t, "instance-1", s.L.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.L.Get("another_resource_attr"), "target_info should have non-promoted resource attributes")
+ }
+
+ // Verify the metric also has the promoted resource attribute.
+ var metricSamples []sample
+ for _, s := range appTest.ResultSamples() {
+ if s.L.Get(labels.MetricName) == "test_gauge" {
+ metricSamples = append(metricSamples, s)
+ }
+ }
+ require.NotEmpty(t, metricSamples, "expected metric samples")
+ require.Equal(t, "promoted-value", metricSamples[0].L.Get("custom_promoted_attr"), "metric should have promoted resource attribute")
+ })
}
func TestTemporality(t *testing.T) {
ts := time.Unix(100, 0)
tests := []struct {
- name string
- allowDelta bool
- convertToNHCB bool
- inputSeries []pmetric.Metric
- expectedSamples []combinedSample
- expectedHistograms []combinedHistogram
- expectedError string
+ name string
+ allowDelta bool
+ convertToNHCB bool
+ inputSeries []pmetric.Metric
+ expectedSamples []sample
+ expectedError string
}{
{
name: "all cumulative when delta not allowed",
@@ -477,7 +728,7 @@ func TestTemporality(t *testing.T) {
createOtelSum("test_metric_1", pmetric.AggregationTemporalityCumulative, ts),
createOtelSum("test_metric_2", pmetric.AggregationTemporalityCumulative, ts),
},
- expectedSamples: []combinedSample{
+ expectedSamples: []sample{
createPromFloatSeries("test_metric_1", ts, model.MetricTypeCounter),
createPromFloatSeries("test_metric_2", ts, model.MetricTypeCounter),
},
@@ -489,7 +740,7 @@ func TestTemporality(t *testing.T) {
createOtelSum("test_metric_1", pmetric.AggregationTemporalityDelta, ts),
createOtelSum("test_metric_2", pmetric.AggregationTemporalityDelta, ts),
},
- expectedSamples: []combinedSample{
+ expectedSamples: []sample{
createPromFloatSeries("test_metric_1", ts, model.MetricTypeUnknown),
createPromFloatSeries("test_metric_2", ts, model.MetricTypeUnknown),
},
@@ -501,7 +752,7 @@ func TestTemporality(t *testing.T) {
createOtelSum("test_metric_1", pmetric.AggregationTemporalityDelta, ts),
createOtelSum("test_metric_2", pmetric.AggregationTemporalityCumulative, ts),
},
- expectedSamples: []combinedSample{
+ expectedSamples: []sample{
createPromFloatSeries("test_metric_1", ts, model.MetricTypeUnknown),
createPromFloatSeries("test_metric_2", ts, model.MetricTypeCounter),
},
@@ -513,7 +764,7 @@ func TestTemporality(t *testing.T) {
createOtelSum("test_metric_1", pmetric.AggregationTemporalityCumulative, ts),
createOtelSum("test_metric_2", pmetric.AggregationTemporalityDelta, ts),
},
- expectedSamples: []combinedSample{
+ expectedSamples: []sample{
createPromFloatSeries("test_metric_1", ts, model.MetricTypeCounter),
},
expectedError: `invalid temporality and type combination for metric "test_metric_2"`,
@@ -525,7 +776,7 @@ func TestTemporality(t *testing.T) {
createOtelSum("test_metric_1", pmetric.AggregationTemporalityCumulative, ts),
createOtelSum("test_metric_2", pmetric.AggregationTemporalityUnspecified, ts),
},
- expectedSamples: []combinedSample{
+ expectedSamples: []sample{
createPromFloatSeries("test_metric_1", ts, model.MetricTypeCounter),
},
expectedError: `invalid temporality and type combination for metric "test_metric_2"`,
@@ -536,7 +787,7 @@ func TestTemporality(t *testing.T) {
inputSeries: []pmetric.Metric{
createOtelExponentialHistogram("test_histogram", pmetric.AggregationTemporalityCumulative, ts),
},
- expectedHistograms: []combinedHistogram{
+ expectedSamples: []sample{
createPromNativeHistogramSeries("test_histogram", histogram.UnknownCounterReset, ts, model.MetricTypeHistogram),
},
},
@@ -547,7 +798,7 @@ func TestTemporality(t *testing.T) {
createOtelExponentialHistogram("test_histogram_1", pmetric.AggregationTemporalityDelta, ts),
createOtelExponentialHistogram("test_histogram_2", pmetric.AggregationTemporalityCumulative, ts),
},
- expectedHistograms: []combinedHistogram{
+ expectedSamples: []sample{
createPromNativeHistogramSeries("test_histogram_1", histogram.GaugeType, ts, model.MetricTypeUnknown),
createPromNativeHistogramSeries("test_histogram_2", histogram.UnknownCounterReset, ts, model.MetricTypeHistogram),
},
@@ -559,7 +810,7 @@ func TestTemporality(t *testing.T) {
createOtelExponentialHistogram("test_histogram_1", pmetric.AggregationTemporalityDelta, ts),
createOtelExponentialHistogram("test_histogram_2", pmetric.AggregationTemporalityCumulative, ts),
},
- expectedHistograms: []combinedHistogram{
+ expectedSamples: []sample{
createPromNativeHistogramSeries("test_histogram_2", histogram.UnknownCounterReset, ts, model.MetricTypeHistogram),
},
expectedError: `invalid temporality and type combination for metric "test_histogram_1"`,
@@ -571,7 +822,7 @@ func TestTemporality(t *testing.T) {
inputSeries: []pmetric.Metric{
createOtelExplicitHistogram("test_histogram", pmetric.AggregationTemporalityCumulative, ts),
},
- expectedHistograms: []combinedHistogram{
+ expectedSamples: []sample{
createPromNHCBSeries("test_histogram", histogram.UnknownCounterReset, ts, model.MetricTypeHistogram),
},
},
@@ -583,7 +834,7 @@ func TestTemporality(t *testing.T) {
createOtelExplicitHistogram("test_histogram_1", pmetric.AggregationTemporalityDelta, ts),
createOtelExplicitHistogram("test_histogram_2", pmetric.AggregationTemporalityCumulative, ts),
},
- expectedHistograms: []combinedHistogram{
+ expectedSamples: []sample{
createPromNHCBSeries("test_histogram_1", histogram.GaugeType, ts, model.MetricTypeUnknown),
createPromNHCBSeries("test_histogram_2", histogram.UnknownCounterReset, ts, model.MetricTypeHistogram),
},
@@ -596,7 +847,7 @@ func TestTemporality(t *testing.T) {
createOtelExplicitHistogram("test_histogram_1", pmetric.AggregationTemporalityDelta, ts),
createOtelExplicitHistogram("test_histogram_2", pmetric.AggregationTemporalityCumulative, ts),
},
- expectedHistograms: []combinedHistogram{
+ expectedSamples: []sample{
createPromNHCBSeries("test_histogram_2", histogram.UnknownCounterReset, ts, model.MetricTypeHistogram),
},
expectedError: `invalid temporality and type combination for metric "test_histogram_1"`,
@@ -637,7 +888,7 @@ func TestTemporality(t *testing.T) {
inputSeries: []pmetric.Metric{
createOtelGauge("test_gauge_1", ts),
},
- expectedSamples: []combinedSample{
+ expectedSamples: []sample{
createPromFloatSeries("test_gauge_1", ts, model.MetricTypeGauge),
},
},
@@ -660,25 +911,22 @@ func TestTemporality(t *testing.T) {
s.CopyTo(sm.Metrics().AppendEmpty())
}
- mockAppender := &mockCombinedAppender{}
- c := NewPrometheusConverter(mockAppender)
+ appTest := teststorage.NewAppendable()
+ app := appTest.AppenderV2(t.Context())
+ c := NewPrometheusConverter(app)
settings := Settings{
AllowDeltaTemporality: tc.allowDelta,
ConvertHistogramsToNHCB: tc.convertToNHCB,
}
_, err := c.FromMetrics(context.Background(), metrics, settings)
-
if tc.expectedError != "" {
require.EqualError(t, err, tc.expectedError)
} else {
require.NoError(t, err)
}
- require.NoError(t, mockAppender.Commit())
-
- // Sort series to make the test deterministic.
- requireEqual(t, tc.expectedSamples, mockAppender.samples)
- requireEqual(t, tc.expectedHistograms, mockAppender.histograms)
+ require.NoError(t, app.Commit())
+ teststorage.RequireEqual(t, tc.expectedSamples, appTest.ResultSamples())
})
}
}
@@ -697,13 +945,13 @@ func createOtelSum(name string, temporality pmetric.AggregationTemporality, ts t
return m
}
-func createPromFloatSeries(name string, ts time.Time, typ model.MetricType) combinedSample {
- return combinedSample{
- metricFamilyName: name,
- ls: labels.FromStrings("__name__", name, "test_label", "test_value"),
- t: ts.UnixMilli(),
- v: 5,
- meta: metadata.Metadata{
+func createPromFloatSeries(name string, ts time.Time, typ model.MetricType) sample {
+ return sample{
+ MF: name,
+ L: labels.FromStrings("__name__", name, "test_label", "test_value"),
+ T: ts.UnixMilli(),
+ V: 5,
+ M: metadata.Metadata{
Type: typ,
},
}
@@ -735,15 +983,15 @@ func createOtelExponentialHistogram(name string, temporality pmetric.Aggregation
return m
}
-func createPromNativeHistogramSeries(name string, hint histogram.CounterResetHint, ts time.Time, typ model.MetricType) combinedHistogram {
- return combinedHistogram{
- metricFamilyName: name,
- ls: labels.FromStrings("__name__", name, "test_label", "test_value"),
- t: ts.UnixMilli(),
- meta: metadata.Metadata{
+func createPromNativeHistogramSeries(name string, hint histogram.CounterResetHint, ts time.Time, typ model.MetricType) sample {
+ return sample{
+ MF: name,
+ L: labels.FromStrings("__name__", name, "test_label", "test_value"),
+ T: ts.UnixMilli(),
+ M: metadata.Metadata{
Type: typ,
},
- h: &histogram.Histogram{
+ H: &histogram.Histogram{
Count: 1,
Sum: 5,
Schema: 0,
@@ -770,15 +1018,15 @@ func createOtelExplicitHistogram(name string, temporality pmetric.AggregationTem
return m
}
-func createPromNHCBSeries(name string, hint histogram.CounterResetHint, ts time.Time, typ model.MetricType) combinedHistogram {
- return combinedHistogram{
- metricFamilyName: name,
- ls: labels.FromStrings("__name__", name, "test_label", "test_value"),
- meta: metadata.Metadata{
+func createPromNHCBSeries(name string, hint histogram.CounterResetHint, ts time.Time, typ model.MetricType) sample {
+ return sample{
+ MF: name,
+ L: labels.FromStrings("__name__", name, "test_label", "test_value"),
+ M: metadata.Metadata{
Type: typ,
},
- t: ts.UnixMilli(),
- h: &histogram.Histogram{
+ T: ts.UnixMilli(),
+ H: &histogram.Histogram{
Count: 20,
Sum: 30,
Schema: -53,
@@ -795,50 +1043,50 @@ func createPromNHCBSeries(name string, hint histogram.CounterResetHint, ts time.
}
}
-func createPromClassicHistogramSeries(name string, ts time.Time, typ model.MetricType) []combinedSample {
- return []combinedSample{
+func createPromClassicHistogramSeries(name string, ts time.Time, typ model.MetricType) []sample {
+ return []sample{
{
- metricFamilyName: name,
- ls: labels.FromStrings("__name__", name+"_sum", "test_label", "test_value"),
- t: ts.UnixMilli(),
- v: 30,
- meta: metadata.Metadata{
+ MF: name,
+ L: labels.FromStrings("__name__", name+"_sum", "test_label", "test_value"),
+ T: ts.UnixMilli(),
+ V: 30,
+ M: metadata.Metadata{
Type: typ,
},
},
{
- metricFamilyName: name,
- ls: labels.FromStrings("__name__", name+"_count", "test_label", "test_value"),
- t: ts.UnixMilli(),
- v: 20,
- meta: metadata.Metadata{
+ MF: name,
+ L: labels.FromStrings("__name__", name+"_count", "test_label", "test_value"),
+ T: ts.UnixMilli(),
+ V: 20,
+ M: metadata.Metadata{
Type: typ,
},
},
{
- metricFamilyName: name,
- ls: labels.FromStrings("__name__", name+"_bucket", "le", "1", "test_label", "test_value"),
- t: ts.UnixMilli(),
- v: 10,
- meta: metadata.Metadata{
+ MF: name,
+ L: labels.FromStrings("__name__", name+"_bucket", "le", "1", "test_label", "test_value"),
+ T: ts.UnixMilli(),
+ V: 10,
+ M: metadata.Metadata{
Type: typ,
},
},
{
- metricFamilyName: name,
- ls: labels.FromStrings("__name__", name+"_bucket", "le", "2", "test_label", "test_value"),
- t: ts.UnixMilli(),
- v: 20,
- meta: metadata.Metadata{
+ MF: name,
+ L: labels.FromStrings("__name__", name+"_bucket", "le", "2", "test_label", "test_value"),
+ T: ts.UnixMilli(),
+ V: 20,
+ M: metadata.Metadata{
Type: typ,
},
},
{
- metricFamilyName: name,
- ls: labels.FromStrings("__name__", name+"_bucket", "le", "+Inf", "test_label", "test_value"),
- t: ts.UnixMilli(),
- v: 20,
- meta: metadata.Metadata{
+ MF: name,
+ L: labels.FromStrings("__name__", name+"_bucket", "le", "+Inf", "test_label", "test_value"),
+ T: ts.UnixMilli(),
+ V: 20,
+ M: metadata.Metadata{
Type: typ,
},
},
@@ -861,32 +1109,32 @@ func createOtelSummary(name string, ts time.Time) pmetric.Metric {
return m
}
-func createPromSummarySeries(name string, ts time.Time) []combinedSample {
- return []combinedSample{
+func createPromSummarySeries(name string, ts time.Time) []sample {
+ return []sample{
{
- metricFamilyName: name,
- ls: labels.FromStrings("__name__", name+"_sum", "test_label", "test_value"),
- t: ts.UnixMilli(),
- v: 18,
- meta: metadata.Metadata{
+ MF: name,
+ L: labels.FromStrings("__name__", name+"_sum", "test_label", "test_value"),
+ T: ts.UnixMilli(),
+ V: 18,
+ M: metadata.Metadata{
Type: model.MetricTypeSummary,
},
},
{
- metricFamilyName: name,
- ls: labels.FromStrings("__name__", name+"_count", "test_label", "test_value"),
- t: ts.UnixMilli(),
- v: 9,
- meta: metadata.Metadata{
+ MF: name,
+ L: labels.FromStrings("__name__", name+"_count", "test_label", "test_value"),
+ T: ts.UnixMilli(),
+ V: 9,
+ M: metadata.Metadata{
Type: model.MetricTypeSummary,
},
},
{
- metricFamilyName: name,
- ls: labels.FromStrings("__name__", name, "quantile", "0.5", "test_label", "test_value"),
- t: ts.UnixMilli(),
- v: 2,
- meta: metadata.Metadata{
+ MF: name,
+ L: labels.FromStrings("__name__", name, "quantile", "0.5", "test_label", "test_value"),
+ T: ts.UnixMilli(),
+ V: 2,
+ M: metadata.Metadata{
Type: model.MetricTypeSummary,
},
},
@@ -1033,54 +1281,57 @@ func createOTelEmptyMetricForTranslator(name string) pmetric.Metric {
return m
}
+// Recommended CLI invocation(s):
+/*
+ export bench=fromMetrics && go test ./storage/remote/otlptranslator/prometheusremotewrite/... \
+ -run '^$' -bench '^BenchmarkPrometheusConverter_FromMetrics' \
+ -benchtime 1s -count 6 -cpu 2 -timeout 999m -benchmem \
+ | tee ${bench}.txt
+*/
func BenchmarkPrometheusConverter_FromMetrics(b *testing.B) {
for _, resourceAttributeCount := range []int{0, 5, 50} {
b.Run(fmt.Sprintf("resource attribute count: %v", resourceAttributeCount), func(b *testing.B) {
- for _, histogramCount := range []int{0, 1000} {
- b.Run(fmt.Sprintf("histogram count: %v", histogramCount), func(b *testing.B) {
- nonHistogramCounts := []int{0, 1000}
+ for _, metricCount := range []struct {
+ histogramCount int
+ nonHistogramCount int
+ }{
+ {histogramCount: 0, nonHistogramCount: 1000},
+ {histogramCount: 1000, nonHistogramCount: 0},
+ {histogramCount: 1000, nonHistogramCount: 1000},
+ } {
+ b.Run(fmt.Sprintf("histogram count: %v/non-histogram count: %v", metricCount.histogramCount, metricCount.nonHistogramCount), func(b *testing.B) {
+ for _, labelsPerMetric := range []int{2, 20} {
+ b.Run(fmt.Sprintf("labels per metric: %v", labelsPerMetric), func(b *testing.B) {
+ for _, exemplarsPerSeries := range []int{0, 5, 10} {
+ b.Run(fmt.Sprintf("exemplars per series: %v", exemplarsPerSeries), func(b *testing.B) {
+ settings := Settings{}
+ payload, _ := createExportRequest(
+ resourceAttributeCount,
+ metricCount.histogramCount,
+ metricCount.nonHistogramCount,
+ labelsPerMetric,
+ exemplarsPerSeries,
+ settings,
+ pmetric.AggregationTemporalityCumulative,
+ )
- if resourceAttributeCount == 0 && histogramCount == 0 {
- // Don't bother running a scenario where we'll generate no series.
- nonHistogramCounts = []int{1000}
- }
+ b.ResetTimer()
+ for b.Loop() {
+ app := &noOpAppender{}
+ converter := NewPrometheusConverter(app)
+ annots, err := converter.FromMetrics(context.Background(), payload.Metrics(), settings)
+ require.NoError(b, err)
+ require.Empty(b, annots)
- for _, nonHistogramCount := range nonHistogramCounts {
- b.Run(fmt.Sprintf("non-histogram count: %v", nonHistogramCount), func(b *testing.B) {
- for _, labelsPerMetric := range []int{2, 20} {
- b.Run(fmt.Sprintf("labels per metric: %v", labelsPerMetric), func(b *testing.B) {
- for _, exemplarsPerSeries := range []int{0, 5, 10} {
- b.Run(fmt.Sprintf("exemplars per series: %v", exemplarsPerSeries), func(b *testing.B) {
- settings := Settings{}
- payload, _ := createExportRequest(
- resourceAttributeCount,
- histogramCount,
- nonHistogramCount,
- labelsPerMetric,
- exemplarsPerSeries,
- settings,
- pmetric.AggregationTemporalityCumulative,
- )
- appMetrics := NewCombinedAppenderMetrics(prometheus.NewRegistry())
- noOpLogger := promslog.NewNopLogger()
- b.ResetTimer()
-
- for b.Loop() {
- app := &noOpAppender{}
- mockAppender := NewCombinedAppender(app, noOpLogger, false, appMetrics)
- converter := NewPrometheusConverter(mockAppender)
- annots, err := converter.FromMetrics(context.Background(), payload.Metrics(), settings)
- require.NoError(b, err)
- require.Empty(b, annots)
- if histogramCount+nonHistogramCount > 0 {
- require.Positive(b, app.samples+app.histograms)
- require.Positive(b, app.metadata)
- } else {
- require.Zero(b, app.samples+app.histograms)
- require.Zero(b, app.metadata)
- }
- }
- })
+ // TODO(bwplotka): This should be tested somewhere else, otherwise we benchmark
+ // mock too.
+ if metricCount.histogramCount+metricCount.nonHistogramCount > 0 {
+ require.Positive(b, app.samples+app.histograms)
+ require.Positive(b, app.metadata)
+ } else {
+ require.Zero(b, app.samples+app.histograms)
+ require.Zero(b, app.metadata)
+ }
}
})
}
@@ -1098,35 +1349,20 @@ type noOpAppender struct {
metadata int
}
-var _ storage.Appender = &noOpAppender{}
+var _ storage.AppenderV2 = &noOpAppender{}
-func (a *noOpAppender) Append(_ storage.SeriesRef, _ labels.Labels, _ int64, _ float64) (storage.SeriesRef, error) {
+func (a *noOpAppender) Append(_ storage.SeriesRef, _ labels.Labels, _, _ int64, _ float64, h *histogram.Histogram, _ *histogram.FloatHistogram, opts storage.AOptions) (_ storage.SeriesRef, err error) {
+ if !opts.Metadata.IsEmpty() {
+ a.metadata++
+ }
+ if h != nil {
+ a.histograms++
+ return 1, nil
+ }
a.samples++
return 1, nil
}
-func (*noOpAppender) AppendCTZeroSample(_ storage.SeriesRef, _ labels.Labels, _, _ int64) (storage.SeriesRef, error) {
- return 1, nil
-}
-
-func (a *noOpAppender) AppendHistogram(_ storage.SeriesRef, _ labels.Labels, _ int64, _ *histogram.Histogram, _ *histogram.FloatHistogram) (storage.SeriesRef, error) {
- a.histograms++
- return 1, nil
-}
-
-func (*noOpAppender) AppendHistogramCTZeroSample(_ storage.SeriesRef, _ labels.Labels, _, _ int64, _ *histogram.Histogram, _ *histogram.FloatHistogram) (storage.SeriesRef, error) {
- return 1, nil
-}
-
-func (a *noOpAppender) UpdateMetadata(_ storage.SeriesRef, _ labels.Labels, _ metadata.Metadata) (storage.SeriesRef, error) {
- a.metadata++
- return 1, nil
-}
-
-func (*noOpAppender) AppendExemplar(_ storage.SeriesRef, _ labels.Labels, _ exemplar.Exemplar) (storage.SeriesRef, error) {
- return 1, nil
-}
-
func (*noOpAppender) Commit() error {
return nil
}
@@ -1135,10 +1371,6 @@ func (*noOpAppender) Rollback() error {
return nil
}
-func (*noOpAppender) SetOptions(_ *storage.AppendOptions) {
- panic("not implemented")
-}
-
type wantPrometheusMetric struct {
name string
familyName string
@@ -1323,3 +1555,264 @@ 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,
+ )
+ b.ReportAllocs()
+ b.ResetTimer()
+
+ for b.Loop() {
+ app := &noOpAppender{}
+ converter := NewPrometheusConverter(app)
+ _, 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,
+ )
+ b.ReportAllocs()
+ b.ResetTimer()
+
+ for b.Loop() {
+ app := &noOpAppender{}
+ converter := NewPrometheusConverter(app)
+ _, 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,
+ )
+ b.ReportAllocs()
+ b.ResetTimer()
+
+ for b.Loop() {
+ app := &noOpAppender{}
+ converter := NewPrometheusConverter(app)
+ _, 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,
+ )
+ b.ReportAllocs()
+ b.ResetTimer()
+
+ for b.Loop() {
+ app := &noOpAppender{}
+ converter := NewPrometheusConverter(app)
+ _, 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 cdae978736..3c74ec9382 100644
--- a/storage/remote/otlptranslator/prometheusremotewrite/number_data_points.go
+++ b/storage/remote/otlptranslator/prometheusremotewrite/number_data_points.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Prometheus Authors
+// 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
@@ -21,14 +21,17 @@ 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"
+ "github.com/prometheus/prometheus/storage"
)
-func (c *PrometheusConverter) addGaugeNumberDataPoints(ctx context.Context, dataPoints pmetric.NumberDataPointSlice,
- resource pcommon.Resource, settings Settings, scope scope, meta Metadata,
+func (c *PrometheusConverter) addGaugeNumberDataPoints(
+ ctx context.Context,
+ dataPoints pmetric.NumberDataPointSlice,
+ settings Settings,
+ appOpts storage.AOptions,
) error {
for x := 0; x < dataPoints.Len(); x++ {
if err := c.everyN.checkContext(ctx); err != nil {
@@ -37,15 +40,13 @@ func (c *PrometheusConverter) addGaugeNumberDataPoints(ctx context.Context, data
pt := dataPoints.At(x)
labels, err := c.createAttributes(
- resource,
pt.Attributes(),
- scope,
settings,
- nil,
+ reservedLabelNames,
true,
- meta,
+ appOpts.Metadata,
model.MetricNameLabel,
- meta.MetricFamilyName,
+ appOpts.MetricFamilyName,
)
if err != nil {
return err
@@ -61,8 +62,8 @@ func (c *PrometheusConverter) addGaugeNumberDataPoints(ctx context.Context, data
val = math.Float64frombits(value.StaleNaN)
}
ts := convertTimeStamp(pt.Timestamp())
- ct := convertTimeStamp(pt.StartTimestamp())
- if err := c.appender.AppendSample(labels, meta, ct, ts, val, nil); err != nil {
+ st := convertTimeStamp(pt.StartTimestamp())
+ if _, err = c.appender.Append(0, labels, st, ts, val, nil, nil, appOpts); err != nil {
return err
}
}
@@ -70,8 +71,11 @@ func (c *PrometheusConverter) addGaugeNumberDataPoints(ctx context.Context, data
return nil
}
-func (c *PrometheusConverter) addSumNumberDataPoints(ctx context.Context, dataPoints pmetric.NumberDataPointSlice,
- resource pcommon.Resource, settings Settings, scope scope, meta Metadata,
+func (c *PrometheusConverter) addSumNumberDataPoints(
+ ctx context.Context,
+ dataPoints pmetric.NumberDataPointSlice,
+ settings Settings,
+ appOpts storage.AOptions,
) error {
for x := 0; x < dataPoints.Len(); x++ {
if err := c.everyN.checkContext(ctx); err != nil {
@@ -80,18 +84,16 @@ func (c *PrometheusConverter) addSumNumberDataPoints(ctx context.Context, dataPo
pt := dataPoints.At(x)
lbls, err := c.createAttributes(
- resource,
pt.Attributes(),
- scope,
settings,
- nil,
+ reservedLabelNames,
true,
- meta,
+ appOpts.Metadata,
model.MetricNameLabel,
- meta.MetricFamilyName,
+ appOpts.MetricFamilyName,
)
if err != nil {
- return nil
+ return err
}
var val float64
switch pt.ValueType() {
@@ -104,12 +106,14 @@ func (c *PrometheusConverter) addSumNumberDataPoints(ctx context.Context, dataPo
val = math.Float64frombits(value.StaleNaN)
}
ts := convertTimeStamp(pt.Timestamp())
- ct := convertTimeStamp(pt.StartTimestamp())
+ st := convertTimeStamp(pt.StartTimestamp())
exemplars, err := c.getPromExemplars(ctx, pt.Exemplars())
if err != nil {
return err
}
- if err := c.appender.AppendSample(lbls, meta, ct, ts, val, exemplars); err != nil {
+
+ appOpts.Exemplars = exemplars
+ if _, err = c.appender.Append(0, lbls, st, ts, val, nil, nil, appOpts); err != nil {
return err
}
}
diff --git a/storage/remote/otlptranslator/prometheusremotewrite/number_data_points_test.go b/storage/remote/otlptranslator/prometheusremotewrite/number_data_points_test.go
index 3e918eecbd..66e7e4c3bb 100644
--- a/storage/remote/otlptranslator/prometheusremotewrite/number_data_points_test.go
+++ b/storage/remote/otlptranslator/prometheusremotewrite/number_data_points_test.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Prometheus Authors
+// 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
@@ -29,6 +29,8 @@ import (
"github.com/prometheus/prometheus/model/exemplar"
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/model/metadata"
+ "github.com/prometheus/prometheus/storage"
+ "github.com/prometheus/prometheus/util/teststorage"
)
func TestPrometheusConverter_addGaugeNumberDataPoints(t *testing.T) {
@@ -49,7 +51,7 @@ func TestPrometheusConverter_addGaugeNumberDataPoints(t *testing.T) {
metric func() pmetric.Metric
scope scope
promoteScope bool
- want func() []combinedSample
+ want func() []sample
}{
{
name: "gauge without scope promotion",
@@ -62,17 +64,17 @@ func TestPrometheusConverter_addGaugeNumberDataPoints(t *testing.T) {
},
scope: defaultScope,
promoteScope: false,
- want: func() []combinedSample {
+ want: func() []sample {
lbls := labels.FromStrings(
model.MetricNameLabel, "test",
)
- return []combinedSample{
+ return []sample{
{
- metricFamilyName: "test",
- ls: lbls,
- meta: metadata.Metadata{},
- t: convertTimeStamp(pcommon.Timestamp(ts)),
- v: 1,
+ MF: "test",
+ L: lbls,
+ M: metadata.Metadata{},
+ T: convertTimeStamp(pcommon.Timestamp(ts)),
+ V: 1,
},
}
},
@@ -88,7 +90,7 @@ func TestPrometheusConverter_addGaugeNumberDataPoints(t *testing.T) {
},
scope: defaultScope,
promoteScope: true,
- want: func() []combinedSample {
+ want: func() []sample {
lbls := labels.FromStrings(
model.MetricNameLabel, "test",
"otel_scope_name", defaultScope.name,
@@ -97,13 +99,13 @@ func TestPrometheusConverter_addGaugeNumberDataPoints(t *testing.T) {
"otel_scope_attr1", "value1",
"otel_scope_attr2", "value2",
)
- return []combinedSample{
+ return []sample{
{
- metricFamilyName: "test",
- ls: lbls,
- meta: metadata.Metadata{},
- t: convertTimeStamp(pcommon.Timestamp(ts)),
- v: 1,
+ MF: "test",
+ L: lbls,
+ M: metadata.Metadata{},
+ T: convertTimeStamp(pcommon.Timestamp(ts)),
+ V: 1,
},
}
},
@@ -112,24 +114,28 @@ func TestPrometheusConverter_addGaugeNumberDataPoints(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
metric := tt.metric()
- mockAppender := &mockCombinedAppender{}
- converter := NewPrometheusConverter(mockAppender)
+ appTest := teststorage.NewAppendable()
+ app := appTest.AppenderV2(t.Context())
+ converter := NewPrometheusConverter(app)
+ 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,
- Metadata{
+ settings,
+ storage.AOptions{
MetricFamilyName: metric.Name(),
},
)
- require.NoError(t, mockAppender.Commit())
-
- requireEqual(t, tt.want(), mockAppender.samples)
+ require.NoError(t, app.Commit())
+ teststorage.RequireEqual(t, tt.want(), appTest.ResultSamples())
})
}
}
@@ -152,7 +158,7 @@ func TestPrometheusConverter_addSumNumberDataPoints(t *testing.T) {
metric func() pmetric.Metric
scope scope
promoteScope bool
- want func() []combinedSample
+ want func() []sample
}{
{
name: "sum without scope promotion",
@@ -166,17 +172,17 @@ func TestPrometheusConverter_addSumNumberDataPoints(t *testing.T) {
},
scope: defaultScope,
promoteScope: false,
- want: func() []combinedSample {
+ want: func() []sample {
lbls := labels.FromStrings(
model.MetricNameLabel, "test",
)
- return []combinedSample{
+ return []sample{
{
- metricFamilyName: "test",
- ls: lbls,
- meta: metadata.Metadata{},
- t: convertTimeStamp(ts),
- v: 1,
+ MF: "test",
+ L: lbls,
+ M: metadata.Metadata{},
+ T: convertTimeStamp(ts),
+ V: 1,
},
}
},
@@ -193,7 +199,7 @@ func TestPrometheusConverter_addSumNumberDataPoints(t *testing.T) {
},
scope: defaultScope,
promoteScope: true,
- want: func() []combinedSample {
+ want: func() []sample {
lbls := labels.FromStrings(
model.MetricNameLabel, "test",
"otel_scope_name", defaultScope.name,
@@ -202,13 +208,13 @@ func TestPrometheusConverter_addSumNumberDataPoints(t *testing.T) {
"otel_scope_attr1", "value1",
"otel_scope_attr2", "value2",
)
- return []combinedSample{
+ return []sample{
{
- metricFamilyName: "test",
- ls: lbls,
- meta: metadata.Metadata{},
- t: convertTimeStamp(ts),
- v: 1,
+ MF: "test",
+ L: lbls,
+ M: metadata.Metadata{},
+ T: convertTimeStamp(ts),
+ V: 1,
},
}
},
@@ -227,18 +233,18 @@ func TestPrometheusConverter_addSumNumberDataPoints(t *testing.T) {
},
scope: defaultScope,
promoteScope: false,
- want: func() []combinedSample {
+ want: func() []sample {
lbls := labels.FromStrings(
model.MetricNameLabel, "test",
)
- return []combinedSample{
+ return []sample{
{
- metricFamilyName: "test",
- ls: lbls,
- meta: metadata.Metadata{},
- t: convertTimeStamp(ts),
- v: 1,
- es: []exemplar.Exemplar{
+ MF: "test",
+ L: lbls,
+ M: metadata.Metadata{},
+ T: convertTimeStamp(ts),
+ V: 1,
+ ES: []exemplar.Exemplar{
{Value: 2},
},
},
@@ -262,18 +268,18 @@ func TestPrometheusConverter_addSumNumberDataPoints(t *testing.T) {
},
scope: defaultScope,
promoteScope: false,
- want: func() []combinedSample {
+ want: func() []sample {
lbls := labels.FromStrings(
model.MetricNameLabel, "test_sum",
)
- return []combinedSample{
+ return []sample{
{
- metricFamilyName: "test_sum",
- ls: lbls,
- meta: metadata.Metadata{},
- t: convertTimeStamp(ts),
- ct: convertTimeStamp(ts),
- v: 1,
+ MF: "test_sum",
+ L: lbls,
+ M: metadata.Metadata{},
+ T: convertTimeStamp(ts),
+ ST: convertTimeStamp(ts),
+ V: 1,
},
}
},
@@ -293,17 +299,17 @@ func TestPrometheusConverter_addSumNumberDataPoints(t *testing.T) {
},
scope: defaultScope,
promoteScope: false,
- want: func() []combinedSample {
+ want: func() []sample {
lbls := labels.FromStrings(
model.MetricNameLabel, "test_sum",
)
- return []combinedSample{
+ return []sample{
{
- metricFamilyName: "test_sum",
- ls: lbls,
- meta: metadata.Metadata{},
- t: convertTimeStamp(ts),
- v: 0,
+ MF: "test_sum",
+ L: lbls,
+ M: metadata.Metadata{},
+ T: convertTimeStamp(ts),
+ V: 0,
},
}
},
@@ -323,17 +329,17 @@ func TestPrometheusConverter_addSumNumberDataPoints(t *testing.T) {
},
scope: defaultScope,
promoteScope: false,
- want: func() []combinedSample {
+ want: func() []sample {
lbls := labels.FromStrings(
model.MetricNameLabel, "test_sum",
)
- return []combinedSample{
+ return []sample{
{
- metricFamilyName: "test_sum",
- ls: lbls,
- meta: metadata.Metadata{},
- t: convertTimeStamp(ts),
- v: 0,
+ MF: "test_sum",
+ L: lbls,
+ M: metadata.Metadata{},
+ T: convertTimeStamp(ts),
+ V: 0,
},
}
},
@@ -342,24 +348,28 @@ func TestPrometheusConverter_addSumNumberDataPoints(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
metric := tt.metric()
- mockAppender := &mockCombinedAppender{}
- converter := NewPrometheusConverter(mockAppender)
+ appTest := teststorage.NewAppendable()
+ app := appTest.AppenderV2(t.Context())
+ converter := NewPrometheusConverter(app)
+ 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,
- Metadata{
+ settings,
+ storage.AOptions{
MetricFamilyName: metric.Name(),
},
)
- require.NoError(t, mockAppender.Commit())
-
- requireEqual(t, tt.want(), mockAppender.samples)
+ require.NoError(t, app.Commit())
+ teststorage.RequireEqual(t, tt.want(), appTest.ResultSamples())
})
}
}
diff --git a/storage/remote/otlptranslator/prometheusremotewrite/otlp_to_openmetrics_metadata.go b/storage/remote/otlptranslator/prometheusremotewrite/otlp_to_openmetrics_metadata.go
index 49f96e0019..0292790156 100644
--- a/storage/remote/otlptranslator/prometheusremotewrite/otlp_to_openmetrics_metadata.go
+++ b/storage/remote/otlptranslator/prometheusremotewrite/otlp_to_openmetrics_metadata.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Prometheus Authors
+// 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
diff --git a/storage/remote/otlptranslator/prometheusremotewrite/testutil_test.go b/storage/remote/otlptranslator/prometheusremotewrite/testutil_test.go
index 187127fcb2..5194925cfe 100644
--- a/storage/remote/otlptranslator/prometheusremotewrite/testutil_test.go
+++ b/storage/remote/otlptranslator/prometheusremotewrite/testutil_test.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Prometheus Authors
+// 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
diff --git a/storage/remote/queue_manager.go b/storage/remote/queue_manager.go
index 25d3a94b6a..63cdfb36f4 100644
--- a/storage/remote/queue_manager.go
+++ b/storage/remote/queue_manager.go
@@ -1,4 +1,4 @@
-// Copyright 2013 The Prometheus Authors
+// 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
@@ -19,6 +19,7 @@ import (
"fmt"
"log/slog"
"math"
+ "slices"
"strconv"
"sync"
"time"
@@ -31,6 +32,7 @@ import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
semconv "go.opentelemetry.io/otel/semconv/v1.21.0"
+ "go.opentelemetry.io/otel/trace"
"go.uber.org/atomic"
"github.com/prometheus/prometheus/config"
@@ -644,63 +646,78 @@ func isSampleOld(baseTime time.Time, sampleAgeLimit time.Duration, ts int64) boo
return sampleTs.Before(limitTs)
}
+// timeSeriesAgeChecker encapsulates the logic for checking if time series data is too old.
+type timeSeriesAgeChecker struct {
+ metrics *queueManagerMetrics
+ baseTime time.Time
+ sampleAgeLimit time.Duration
+}
+
+// checkAndRecordIfOld checks if a timestamp is too old and records the appropriate metric.
+// Returns true if the data should be dropped.
+func (c *timeSeriesAgeChecker) checkAndRecordIfOld(timestamp int64, dataType string) bool {
+ if c.sampleAgeLimit == 0 {
+ // If sampleAgeLimit is unset, then we never skip samples due to their age.
+ return false
+ }
+
+ if !isSampleOld(c.baseTime, c.sampleAgeLimit, timestamp) {
+ return false
+ }
+
+ // Record the drop in metrics.
+ switch dataType {
+ case "sample":
+ c.metrics.droppedSamplesTotal.WithLabelValues(reasonTooOld).Inc()
+ case "histogram":
+ c.metrics.droppedHistogramsTotal.WithLabelValues(reasonTooOld).Inc()
+ case "exemplar":
+ c.metrics.droppedExemplarsTotal.WithLabelValues(reasonTooOld).Inc()
+ }
+ return true
+}
+
func isTimeSeriesOldFilter(metrics *queueManagerMetrics, baseTime time.Time, sampleAgeLimit time.Duration) func(ts prompb.TimeSeries) bool {
+ checker := &timeSeriesAgeChecker{
+ metrics: metrics,
+ baseTime: baseTime,
+ sampleAgeLimit: sampleAgeLimit,
+ }
+
return func(ts prompb.TimeSeries) bool {
- if sampleAgeLimit == 0 {
- // If sampleAgeLimit is unset, then we never skip samples due to their age.
- return false
- }
- switch {
// Only the first element should be set in the series, therefore we only check the first element.
+ switch {
case len(ts.Samples) > 0:
- if isSampleOld(baseTime, sampleAgeLimit, ts.Samples[0].Timestamp) {
- metrics.droppedSamplesTotal.WithLabelValues(reasonTooOld).Inc()
- return true
- }
+ return checker.checkAndRecordIfOld(ts.Samples[0].Timestamp, "sample")
case len(ts.Histograms) > 0:
- if isSampleOld(baseTime, sampleAgeLimit, ts.Histograms[0].Timestamp) {
- metrics.droppedHistogramsTotal.WithLabelValues(reasonTooOld).Inc()
- return true
- }
+ return checker.checkAndRecordIfOld(ts.Histograms[0].Timestamp, "histogram")
case len(ts.Exemplars) > 0:
- if isSampleOld(baseTime, sampleAgeLimit, ts.Exemplars[0].Timestamp) {
- metrics.droppedExemplarsTotal.WithLabelValues(reasonTooOld).Inc()
- return true
- }
+ return checker.checkAndRecordIfOld(ts.Exemplars[0].Timestamp, "exemplar")
default:
return false
}
- return false
}
}
func isV2TimeSeriesOldFilter(metrics *queueManagerMetrics, baseTime time.Time, sampleAgeLimit time.Duration) func(ts writev2.TimeSeries) bool {
+ checker := &timeSeriesAgeChecker{
+ metrics: metrics,
+ baseTime: baseTime,
+ sampleAgeLimit: sampleAgeLimit,
+ }
+
return func(ts writev2.TimeSeries) bool {
- if sampleAgeLimit == 0 {
- // If sampleAgeLimit is unset, then we never skip samples due to their age.
- return false
- }
- switch {
// Only the first element should be set in the series, therefore we only check the first element.
+ switch {
case len(ts.Samples) > 0:
- if isSampleOld(baseTime, sampleAgeLimit, ts.Samples[0].Timestamp) {
- metrics.droppedSamplesTotal.WithLabelValues(reasonTooOld).Inc()
- return true
- }
+ return checker.checkAndRecordIfOld(ts.Samples[0].Timestamp, "sample")
case len(ts.Histograms) > 0:
- if isSampleOld(baseTime, sampleAgeLimit, ts.Histograms[0].Timestamp) {
- metrics.droppedHistogramsTotal.WithLabelValues(reasonTooOld).Inc()
- return true
- }
+ return checker.checkAndRecordIfOld(ts.Histograms[0].Timestamp, "histogram")
case len(ts.Exemplars) > 0:
- if isSampleOld(baseTime, sampleAgeLimit, ts.Exemplars[0].Timestamp) {
- metrics.droppedExemplarsTotal.WithLabelValues(reasonTooOld).Inc()
- return true
- }
+ return checker.checkAndRecordIfOld(ts.Exemplars[0].Timestamp, "exemplar")
default:
return false
}
- return false
}
}
@@ -1737,6 +1754,20 @@ func (s *shards) sendSamplesWithBackoff(ctx context.Context, samples []prompb.Ti
}
reqSize := len(req)
+ sc := sendBatchContext{
+ ctx: ctx,
+ sampleCount: sampleCount,
+ exemplarCount: exemplarCount,
+ histogramCount: histogramCount,
+ metadataCount: metadataCount,
+ reqSize: reqSize,
+ }
+
+ metricsUpdater := batchMetricsUpdater{
+ metrics: s.qm.metrics,
+ storeClient: s.qm.storeClient,
+ sentDuration: s.qm.metrics.sentBatchDuration,
+ }
// Since we retry writes via attemptStore and sendWriteRequestWithBackoff we need
// to track the total amount of accepted data across the various attempts.
@@ -1772,33 +1803,14 @@ func (s *shards) sendSamplesWithBackoff(ctx context.Context, samples []prompb.Ti
req = req2
}
- ctx, span := otel.Tracer("").Start(ctx, "Remote Send Batch")
+ ctx, span := createBatchSpan(sc.ctx, sc, s.qm.storeClient.Name(), s.qm.storeClient.Endpoint(), try)
defer span.End()
- span.SetAttributes(
- attribute.Int("request_size", reqSize),
- attribute.Int("samples", sampleCount),
- attribute.Int("try", try),
- attribute.String("remote_name", s.qm.storeClient.Name()),
- attribute.String("remote_url", s.qm.storeClient.Endpoint()),
- )
-
- if exemplarCount > 0 {
- span.SetAttributes(attribute.Int("exemplars", exemplarCount))
- }
- if histogramCount > 0 {
- span.SetAttributes(attribute.Int("histograms", histogramCount))
- }
-
begin := time.Now()
- s.qm.metrics.samplesTotal.Add(float64(sampleCount))
- s.qm.metrics.exemplarsTotal.Add(float64(exemplarCount))
- s.qm.metrics.histogramsTotal.Add(float64(histogramCount))
- s.qm.metrics.metadataTotal.Add(float64(metadataCount))
+ metricsUpdater.recordBatchAttempt(sc, begin)
// Technically for v1, we will likely have empty response stats, but for
// newer Receivers this might be not, so used it in a best effort.
rs, err := s.qm.client().Store(ctx, req, try)
- s.qm.metrics.sentBatchDuration.Observe(time.Since(begin).Seconds())
// TODO(bwplotka): Revisit this once we have Receivers doing retriable partial error
// so far we don't have those, so it's ok to potentially skew statistics.
addStats(rs)
@@ -1811,9 +1823,7 @@ func (s *shards) sendSamplesWithBackoff(ctx context.Context, samples []prompb.Ti
}
onRetry := func() {
- s.qm.metrics.retriedSamplesTotal.Add(float64(sampleCount))
- s.qm.metrics.retriedExemplarsTotal.Add(float64(exemplarCount))
- s.qm.metrics.retriedHistogramsTotal.Add(float64(histogramCount))
+ metricsUpdater.recordRetry(sc)
}
err = s.qm.sendWriteRequestWithBackoff(ctx, attemptStore, onRetry)
@@ -1850,6 +1860,20 @@ func (s *shards) sendV2SamplesWithBackoff(ctx context.Context, samples []writev2
}
reqSize := len(req)
+ sc := sendBatchContext{
+ ctx: ctx,
+ sampleCount: sampleCount,
+ exemplarCount: exemplarCount,
+ histogramCount: histogramCount,
+ metadataCount: metadataCount,
+ reqSize: reqSize,
+ }
+
+ metricsUpdater := batchMetricsUpdater{
+ metrics: s.qm.metrics,
+ storeClient: s.qm.storeClient,
+ sentDuration: s.qm.metrics.sentBatchDuration,
+ }
// Since we retry writes via attemptStore and sendWriteRequestWithBackoff we need
// to track the total amount of accepted data across the various attempts.
@@ -1885,31 +1909,12 @@ func (s *shards) sendV2SamplesWithBackoff(ctx context.Context, samples []writev2
req = req2
}
- ctx, span := otel.Tracer("").Start(ctx, "Remote Send Batch")
+ ctx, span := createBatchSpan(sc.ctx, sc, s.qm.storeClient.Name(), s.qm.storeClient.Endpoint(), try)
defer span.End()
- span.SetAttributes(
- attribute.Int("request_size", reqSize),
- attribute.Int("samples", sampleCount),
- attribute.Int("try", try),
- attribute.String("remote_name", s.qm.storeClient.Name()),
- attribute.String("remote_url", s.qm.storeClient.Endpoint()),
- )
-
- if exemplarCount > 0 {
- span.SetAttributes(attribute.Int("exemplars", exemplarCount))
- }
- if histogramCount > 0 {
- span.SetAttributes(attribute.Int("histograms", histogramCount))
- }
-
begin := time.Now()
- s.qm.metrics.samplesTotal.Add(float64(sampleCount))
- s.qm.metrics.exemplarsTotal.Add(float64(exemplarCount))
- s.qm.metrics.histogramsTotal.Add(float64(histogramCount))
- s.qm.metrics.metadataTotal.Add(float64(metadataCount))
+ metricsUpdater.recordBatchAttempt(sc, begin)
rs, err := s.qm.client().Store(ctx, req, try)
- s.qm.metrics.sentBatchDuration.Observe(time.Since(begin).Seconds())
// TODO(bwplotka): Revisit this once we have Receivers doing retriable partial error
// so far we don't have those, so it's ok to potentially skew statistics.
addStats(rs)
@@ -1933,9 +1938,7 @@ func (s *shards) sendV2SamplesWithBackoff(ctx context.Context, samples []writev2
}
onRetry := func() {
- s.qm.metrics.retriedSamplesTotal.Add(float64(sampleCount))
- s.qm.metrics.retriedExemplarsTotal.Add(float64(exemplarCount))
- s.qm.metrics.retriedHistogramsTotal.Add(float64(histogramCount))
+ metricsUpdater.recordRetry(sc)
}
err = s.qm.sendWriteRequestWithBackoff(ctx, attemptStore, onRetry)
@@ -2101,66 +2104,36 @@ func setAtomicToNewer(value *atomic.Int64, newValue int64) (previous int64, upda
}
}
-func buildTimeSeries(timeSeries []prompb.TimeSeries, filter func(prompb.TimeSeries) bool) (int64, int64, []prompb.TimeSeries, int, int, int) {
- var highest int64
- var lowest int64
- var droppedSamples, droppedExemplars, droppedHistograms int
+func buildTimeSeries(timeSeries []prompb.TimeSeries, filter func(prompb.TimeSeries) bool) ([]prompb.TimeSeries, *timeSeriesStats) {
+ stats := newTimeSeriesStats()
- keepIdx := 0
- lowest = math.MaxInt64
- for i, ts := range timeSeries {
+ timeSeries = slices.DeleteFunc(timeSeries, func(ts prompb.TimeSeries) bool {
if filter != nil && filter(ts) {
- if len(ts.Samples) > 0 {
- droppedSamples++
- }
- if len(ts.Exemplars) > 0 {
- droppedExemplars++
- }
- if len(ts.Histograms) > 0 {
- droppedHistograms++
- }
- continue
+ stats.recordDropped(len(ts.Samples) > 0, len(ts.Exemplars) > 0, len(ts.Histograms) > 0)
+ return true
}
// At the moment we only ever append a TimeSeries with a single sample or exemplar in it.
- if len(ts.Samples) > 0 && ts.Samples[0].Timestamp > highest {
- highest = ts.Samples[0].Timestamp
+ if len(ts.Samples) > 0 {
+ stats.updateTimestamp(ts.Samples[0].Timestamp)
}
- if len(ts.Exemplars) > 0 && ts.Exemplars[0].Timestamp > highest {
- highest = ts.Exemplars[0].Timestamp
+ if len(ts.Exemplars) > 0 {
+ stats.updateTimestamp(ts.Exemplars[0].Timestamp)
}
- if len(ts.Histograms) > 0 && ts.Histograms[0].Timestamp > highest {
- highest = ts.Histograms[0].Timestamp
+ if len(ts.Histograms) > 0 {
+ stats.updateTimestamp(ts.Histograms[0].Timestamp)
}
+ return false
+ })
- // Get lowest timestamp
- if len(ts.Samples) > 0 && ts.Samples[0].Timestamp < lowest {
- lowest = ts.Samples[0].Timestamp
- }
- if len(ts.Exemplars) > 0 && ts.Exemplars[0].Timestamp < lowest {
- lowest = ts.Exemplars[0].Timestamp
- }
- if len(ts.Histograms) > 0 && ts.Histograms[0].Timestamp < lowest {
- lowest = ts.Histograms[0].Timestamp
- }
- if i != keepIdx {
- // We have to swap the kept timeseries with the one which should be dropped.
- // Copying any elements within timeSeries could cause data corruptions when reusing the slice in a next batch (shards.populateTimeSeries).
- timeSeries[keepIdx], timeSeries[i] = timeSeries[i], timeSeries[keepIdx]
- }
- keepIdx++
- }
-
- timeSeries = timeSeries[:keepIdx]
- return highest, lowest, timeSeries, droppedSamples, droppedExemplars, droppedHistograms
+ return timeSeries, stats
}
func buildWriteRequest(logger *slog.Logger, timeSeries []prompb.TimeSeries, metadata []prompb.MetricMetadata, pBuf *proto.Buffer, filter func(prompb.TimeSeries) bool, buf compression.EncodeBuffer, compr compression.Type) (_ []byte, highest, lowest int64, _ error) {
- highest, lowest, timeSeries,
- droppedSamples, droppedExemplars, droppedHistograms := buildTimeSeries(timeSeries, filter)
+ timeSeries, stats := buildTimeSeries(timeSeries, filter)
- if droppedSamples > 0 || droppedExemplars > 0 || droppedHistograms > 0 {
- logger.Debug("dropped data due to their age", "droppedSamples", droppedSamples, "droppedExemplars", droppedExemplars, "droppedHistograms", droppedHistograms)
+ if stats.droppedSamples > 0 || stats.droppedExemplars > 0 || stats.droppedHistograms > 0 {
+ logger.Debug("dropped data due to their age", "droppedSamples", stats.droppedSamples, "droppedExemplars", stats.droppedExemplars, "droppedHistograms", stats.droppedHistograms)
}
req := &prompb.WriteRequest{
@@ -2174,21 +2147,21 @@ func buildWriteRequest(logger *slog.Logger, timeSeries []prompb.TimeSeries, meta
pBuf.Reset()
}
if err := pBuf.Marshal(req); err != nil {
- return nil, highest, lowest, err
+ return nil, stats.highest, stats.lowest, err
}
compressed, err := compression.Encode(compr, pBuf.Bytes(), buf)
if err != nil {
- return nil, highest, lowest, err
+ return nil, stats.highest, stats.lowest, err
}
- return compressed, highest, lowest, nil
+ return compressed, stats.highest, stats.lowest, nil
}
func buildV2WriteRequest(logger *slog.Logger, samples []writev2.TimeSeries, labels []string, pBuf *[]byte, filter func(writev2.TimeSeries) bool, buf compression.EncodeBuffer, compr compression.Type) (compressed []byte, highest, lowest int64, _ error) {
- highest, lowest, timeSeries, droppedSamples, droppedExemplars, droppedHistograms := buildV2TimeSeries(samples, filter)
+ timeSeries, stats := buildV2TimeSeries(samples, filter)
- if droppedSamples > 0 || droppedExemplars > 0 || droppedHistograms > 0 {
- logger.Debug("dropped data due to their age", "droppedSamples", droppedSamples, "droppedExemplars", droppedExemplars, "droppedHistograms", droppedHistograms)
+ if stats.droppedSamples > 0 || stats.droppedExemplars > 0 || stats.droppedHistograms > 0 {
+ logger.Debug("dropped data due to their age", "droppedSamples", stats.droppedSamples, "droppedExemplars", stats.droppedExemplars, "droppedHistograms", stats.droppedHistograms)
}
req := &writev2.Request{
@@ -2202,59 +2175,38 @@ func buildV2WriteRequest(logger *slog.Logger, samples []writev2.TimeSeries, labe
data, err := req.OptimizedMarshal(*pBuf)
if err != nil {
- return nil, highest, lowest, err
+ return nil, stats.highest, stats.lowest, err
}
*pBuf = data
compressed, err = compression.Encode(compr, *pBuf, buf)
if err != nil {
- return nil, highest, lowest, err
+ return nil, stats.highest, stats.lowest, err
}
- return compressed, highest, lowest, nil
+ return compressed, stats.highest, stats.lowest, nil
}
-func buildV2TimeSeries(timeSeries []writev2.TimeSeries, filter func(writev2.TimeSeries) bool) (int64, int64, []writev2.TimeSeries, int, int, int) {
- var highest int64
- var lowest int64
- var droppedSamples, droppedExemplars, droppedHistograms int
-
+func buildV2TimeSeries(timeSeries []writev2.TimeSeries, filter func(writev2.TimeSeries) bool) ([]writev2.TimeSeries, *timeSeriesStats) {
+ stats := newTimeSeriesStats()
keepIdx := 0
- lowest = math.MaxInt64
+
for i, ts := range timeSeries {
if filter != nil && filter(ts) {
- if len(ts.Samples) > 0 {
- droppedSamples++
- }
- if len(ts.Exemplars) > 0 {
- droppedExemplars++
- }
- if len(ts.Histograms) > 0 {
- droppedHistograms++
- }
+ stats.recordDropped(len(ts.Samples) > 0, len(ts.Exemplars) > 0, len(ts.Histograms) > 0)
continue
}
// At the moment we only ever append a TimeSeries with a single sample or exemplar in it.
- if len(ts.Samples) > 0 && ts.Samples[0].Timestamp > highest {
- highest = ts.Samples[0].Timestamp
+ if len(ts.Samples) > 0 {
+ stats.updateTimestamp(ts.Samples[0].Timestamp)
}
- if len(ts.Exemplars) > 0 && ts.Exemplars[0].Timestamp > highest {
- highest = ts.Exemplars[0].Timestamp
+ if len(ts.Exemplars) > 0 {
+ stats.updateTimestamp(ts.Exemplars[0].Timestamp)
}
- if len(ts.Histograms) > 0 && ts.Histograms[0].Timestamp > highest {
- highest = ts.Histograms[0].Timestamp
+ if len(ts.Histograms) > 0 {
+ stats.updateTimestamp(ts.Histograms[0].Timestamp)
}
- // Get the lowest timestamp.
- if len(ts.Samples) > 0 && ts.Samples[0].Timestamp < lowest {
- lowest = ts.Samples[0].Timestamp
- }
- if len(ts.Exemplars) > 0 && ts.Exemplars[0].Timestamp < lowest {
- lowest = ts.Exemplars[0].Timestamp
- }
- if len(ts.Histograms) > 0 && ts.Histograms[0].Timestamp < lowest {
- lowest = ts.Histograms[0].Timestamp
- }
if i != keepIdx {
// We have to swap the kept timeseries with the one which should be dropped.
// Copying any elements within timeSeries could cause data corruptions when reusing the slice in a next batch (shards.populateTimeSeries).
@@ -2263,6 +2215,99 @@ func buildV2TimeSeries(timeSeries []writev2.TimeSeries, filter func(writev2.Time
keepIdx++
}
- timeSeries = timeSeries[:keepIdx]
- return highest, lowest, timeSeries, droppedSamples, droppedExemplars, droppedHistograms
+ return timeSeries[:keepIdx], stats
+}
+
+// timeSeriesStats tracks statistics during time series processing.
+type timeSeriesStats struct {
+ highest int64
+ lowest int64
+ droppedSamples int
+ droppedExemplars int
+ droppedHistograms int
+}
+
+// newTimeSeriesStats creates a new timeSeriesStats with lowest initialized to MaxInt64.
+func newTimeSeriesStats() *timeSeriesStats {
+ return &timeSeriesStats{
+ lowest: math.MaxInt64,
+ }
+}
+
+// updateTimestamp updates highest and lowest timestamps if the given timestamp is valid.
+func (s *timeSeriesStats) updateTimestamp(timestamp int64) {
+ if timestamp > 0 {
+ if timestamp > s.highest {
+ s.highest = timestamp
+ }
+ if timestamp < s.lowest {
+ s.lowest = timestamp
+ }
+ }
+}
+
+// recordDropped increments the dropped counters based on what data exists.
+func (s *timeSeriesStats) recordDropped(hasSamples, hasExemplars, hasHistograms bool) {
+ if hasSamples {
+ s.droppedSamples++
+ }
+ if hasExemplars {
+ s.droppedExemplars++
+ }
+ if hasHistograms {
+ s.droppedHistograms++
+ }
+}
+
+// sendBatchContext encapsulates the common parameters for sending batches.
+// This reduces the number of function arguments (addresses "too many arguments" issue).
+type sendBatchContext struct {
+ ctx context.Context
+ sampleCount int
+ exemplarCount int
+ histogramCount int
+ metadataCount int
+ reqSize int
+}
+
+// batchMetricsUpdater encapsulates metrics update operations for batch sending.
+type batchMetricsUpdater struct {
+ metrics *queueManagerMetrics
+ storeClient WriteClient
+ sentDuration prometheus.Observer
+}
+
+// recordBatchAttempt records metrics for a batch send attempt.
+func (b *batchMetricsUpdater) recordBatchAttempt(sc sendBatchContext, begin time.Time) {
+ b.metrics.samplesTotal.Add(float64(sc.sampleCount))
+ b.metrics.exemplarsTotal.Add(float64(sc.exemplarCount))
+ b.metrics.histogramsTotal.Add(float64(sc.histogramCount))
+ b.metrics.metadataTotal.Add(float64(sc.metadataCount))
+ b.sentDuration.Observe(time.Since(begin).Seconds())
+}
+
+// recordRetry records retry metrics for a batch.
+func (b *batchMetricsUpdater) recordRetry(sc sendBatchContext) {
+ b.metrics.retriedSamplesTotal.Add(float64(sc.sampleCount))
+ b.metrics.retriedExemplarsTotal.Add(float64(sc.exemplarCount))
+ b.metrics.retriedHistogramsTotal.Add(float64(sc.histogramCount))
+}
+
+// createBatchSpan creates and configures an OpenTelemetry span for batch sending.
+func createBatchSpan(ctx context.Context, sc sendBatchContext, remoteName, remoteURL string, try int) (context.Context, trace.Span) {
+ ctx, span := otel.Tracer("").Start(ctx, "Remote Send Batch")
+ span.SetAttributes(
+ attribute.Int("request_size", sc.reqSize),
+ attribute.Int("samples", sc.sampleCount),
+ attribute.Int("try", try),
+ attribute.String("remote_name", remoteName),
+ attribute.String("remote_url", remoteURL),
+ )
+ if sc.exemplarCount > 0 {
+ span.SetAttributes(attribute.Int("exemplars", sc.exemplarCount))
+ }
+ if sc.histogramCount > 0 {
+ span.SetAttributes(attribute.Int("histograms", sc.histogramCount))
+ }
+ return ctx, span
}
diff --git a/storage/remote/queue_manager_test.go b/storage/remote/queue_manager_test.go
index ce9cc6f1b6..a4b05d387a 100644
--- a/storage/remote/queue_manager_test.go
+++ b/storage/remote/queue_manager_test.go
@@ -1,4 +1,4 @@
-// Copyright 2013 The Prometheus Authors
+// 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
@@ -871,7 +871,7 @@ func createTimeseries(numSamples, numSeries int, extraLabels ...labels.Label) ([
return samples, series
}
-func createProtoTimeseriesWithOld(numSamples, baseTs int64, _ ...labels.Label) []prompb.TimeSeries {
+func createProtoTimeseriesWithOld(numSamples, baseTs int64) []prompb.TimeSeries {
samples := make([]prompb.TimeSeries, numSamples)
// use a fixed rand source so tests are consistent
r := rand.New(rand.NewSource(99))
@@ -2351,12 +2351,12 @@ func TestBuildTimeSeries(t *testing.T) {
// Run the test cases
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
- highest, lowest, result, droppedSamples, _, _ := buildTimeSeries(tc.ts, tc.filter)
+ result, stats := buildTimeSeries(tc.ts, tc.filter)
require.NotNil(t, result)
require.Len(t, result, tc.responseLen)
- require.Equal(t, tc.highestTs, highest)
- require.Equal(t, tc.lowestTs, lowest)
- require.Equal(t, tc.droppedSamples, droppedSamples)
+ require.Equal(t, tc.highestTs, stats.highest)
+ require.Equal(t, tc.lowestTs, stats.lowest)
+ require.Equal(t, tc.droppedSamples, stats.droppedSamples)
})
}
}
@@ -2365,9 +2365,15 @@ func BenchmarkBuildTimeSeries(b *testing.B) {
// Send one sample per series, which is the typical remote_write case
const numSamples = 10000
filter := func(ts prompb.TimeSeries) bool { return filterTsLimit(99, ts) }
+ originalSamples := createProtoTimeseriesWithOld(numSamples, 100)
+
+ b.ReportAllocs()
for b.Loop() {
- samples := createProtoTimeseriesWithOld(numSamples, 100, extraLabels...)
- _, _, result, _, _, _ := buildTimeSeries(samples, filter)
+ b.StopTimer()
+ samples := make([]prompb.TimeSeries, len(originalSamples))
+ copy(samples, originalSamples)
+ b.StartTimer()
+ result, _ := buildTimeSeries(samples, filter)
require.NotNil(b, result)
}
}
diff --git a/storage/remote/read.go b/storage/remote/read.go
index e21d1538f5..70b55980b8 100644
--- a/storage/remote/read.go
+++ b/storage/remote/read.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
diff --git a/storage/remote/read_handler.go b/storage/remote/read_handler.go
index 3e315a6157..a628dd34ff 100644
--- a/storage/remote/read_handler.go
+++ b/storage/remote/read_handler.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
diff --git a/storage/remote/read_handler_test.go b/storage/remote/read_handler_test.go
index 355973e4be..a59c940f30 100644
--- a/storage/remote/read_handler_test.go
+++ b/storage/remote/read_handler_test.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
@@ -15,7 +15,6 @@ package remote
import (
"bytes"
- "context"
"errors"
"io"
"net/http"
@@ -28,6 +27,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/prometheus/prometheus/config"
+ "github.com/prometheus/prometheus/model/histogram"
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/prompb"
"github.com/prometheus/prometheus/promql/promqltest"
@@ -64,13 +64,19 @@ func TestSampledReadEndpoint(t *testing.T) {
matcher3, err := labels.NewMatcher(labels.MatchEqual, "__name__", "test_histogram_metric1")
require.NoError(t, err)
+ matcher4, err := labels.NewMatcher(labels.MatchEqual, "__name__", "test_nhcb_metric1")
+ require.NoError(t, err)
+
query1, err := ToQuery(0, 1, []*labels.Matcher{matcher1, matcher2}, &storage.SelectHints{Step: 0, Func: "avg"})
require.NoError(t, err)
query2, err := ToQuery(0, 1, []*labels.Matcher{matcher3, matcher2}, &storage.SelectHints{Step: 0, Func: "avg"})
require.NoError(t, err)
- req := &prompb.ReadRequest{Queries: []*prompb.Query{query1, query2}}
+ query3, err := ToQuery(0, 1, []*labels.Matcher{matcher4, matcher2}, &storage.SelectHints{Step: 0, Func: "avg"})
+ require.NoError(t, err)
+
+ req := &prompb.ReadRequest{Queries: []*prompb.Query{query1, query2, query3}}
data, err := proto.Marshal(req)
require.NoError(t, err)
@@ -97,7 +103,7 @@ func TestSampledReadEndpoint(t *testing.T) {
err = proto.Unmarshal(uncompressed, &resp)
require.NoError(t, err)
- require.Len(t, resp.Results, 2, "Expected 2 results.")
+ require.Len(t, resp.Results, 3, "Expected 3 results.")
require.Equal(t, &prompb.QueryResult{
Timeseries: []*prompb.TimeSeries{
@@ -129,6 +135,33 @@ func TestSampledReadEndpoint(t *testing.T) {
},
},
}, resp.Results[1])
+
+ require.Equal(t, &prompb.QueryResult{
+ Timeseries: []*prompb.TimeSeries{
+ {
+ Labels: []prompb.Label{
+ {Name: "__name__", Value: "test_nhcb_metric1"},
+ {Name: "b", Value: "c"},
+ {Name: "baz", Value: "qux"},
+ {Name: "d", Value: "e"},
+ },
+ Histograms: []prompb.Histogram{{
+ // We cannot use prompb.FromFloatHistogram as that's one
+ // of the things we are testing here.
+ Schema: histogram.CustomBucketsSchema,
+ Count: &prompb.Histogram_CountFloat{CountFloat: 5},
+ Sum: 18.4,
+ ZeroCount: &prompb.Histogram_ZeroCountFloat{},
+ PositiveSpans: []prompb.BucketSpan{
+ {Offset: 0, Length: 2},
+ {Offset: 1, Length: 2},
+ },
+ PositiveCounts: []float64{1, 2, 1, 1},
+ CustomValues: []float64{0, 1, 2, 3, 4},
+ }},
+ },
+ },
+ }, resp.Results[2])
}
func BenchmarkStreamReadEndpoint(b *testing.B) {
@@ -433,10 +466,17 @@ func TestStreamReadEndpoint(t *testing.T) {
func addNativeHistogramsToTestSuite(t *testing.T, storage *teststorage.TestStorage, n int) {
lbls := labels.FromStrings("__name__", "test_histogram_metric1", "baz", "qux")
- app := storage.Appender(context.TODO())
+ app := storage.Appender(t.Context())
for i, fh := range tsdbutil.GenerateTestFloatHistograms(n) {
_, err := app.AppendHistogram(0, lbls, int64(i)*int64(60*time.Second/time.Millisecond), nil, fh)
require.NoError(t, err)
}
+
+ lbls = labels.FromStrings("__name__", "test_nhcb_metric1", "baz", "qux")
+ for i, fh := range tsdbutil.GenerateTestCustomBucketsFloatHistograms(n) {
+ _, err := app.AppendHistogram(0, lbls, int64(i)*int64(60*time.Second/time.Millisecond), nil, fh)
+ require.NoError(t, err)
+ }
+
require.NoError(t, app.Commit())
}
diff --git a/storage/remote/read_test.go b/storage/remote/read_test.go
index da0b7f81d4..49f29d9001 100644
--- a/storage/remote/read_test.go
+++ b/storage/remote/read_test.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
diff --git a/storage/remote/stats.go b/storage/remote/stats.go
index 89d00ffc31..3a1bfed805 100644
--- a/storage/remote/stats.go
+++ b/storage/remote/stats.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Prometheus Authors
+// 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
diff --git a/storage/remote/storage.go b/storage/remote/storage.go
index 648c91c955..be75d23383 100644
--- a/storage/remote/storage.go
+++ b/storage/remote/storage.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
@@ -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/storage_test.go b/storage/remote/storage_test.go
index f567c7a80b..416468cf79 100644
--- a/storage/remote/storage_test.go
+++ b/storage/remote/storage_test.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The Prometheus Authors
+// 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
diff --git a/storage/remote/write.go b/storage/remote/write.go
index 6bc02bd6fe..6a336dc06b 100644
--- a/storage/remote/write.go
+++ b/storage/remote/write.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
@@ -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
}
@@ -318,22 +336,22 @@ func (t *timestampTracker) AppendHistogram(_ storage.SeriesRef, _ labels.Labels,
return 0, nil
}
-func (t *timestampTracker) AppendCTZeroSample(_ storage.SeriesRef, _ labels.Labels, _, ct int64) (storage.SeriesRef, error) {
+func (t *timestampTracker) AppendSTZeroSample(_ storage.SeriesRef, _ labels.Labels, _, st int64) (storage.SeriesRef, error) {
t.samples++
- if ct > t.highestTimestamp {
- // Theoretically, we should never see a CT zero sample with a timestamp higher than the highest timestamp we've seen so far.
+ if st > t.highestTimestamp {
+ // Theoretically, we should never see a ST zero sample with a timestamp higher than the highest timestamp we've seen so far.
// However, we're not going to enforce that here, as it is not the responsibility of the tracker to enforce this.
- t.highestTimestamp = ct
+ t.highestTimestamp = st
}
return 0, nil
}
-func (t *timestampTracker) AppendHistogramCTZeroSample(_ storage.SeriesRef, _ labels.Labels, _, ct int64, _ *histogram.Histogram, _ *histogram.FloatHistogram) (storage.SeriesRef, error) {
+func (t *timestampTracker) AppendHistogramSTZeroSample(_ storage.SeriesRef, _ labels.Labels, _, st int64, _ *histogram.Histogram, _ *histogram.FloatHistogram) (storage.SeriesRef, error) {
t.histograms++
- if ct > t.highestTimestamp {
- // Theoretically, we should never see a CT zero sample with a timestamp higher than the highest timestamp we've seen so far.
+ if st > t.highestTimestamp {
+ // Theoretically, we should never see a ST zero sample with a timestamp higher than the highest timestamp we've seen so far.
// However, we're not going to enforce that here, as it is not the responsibility of the tracker to enforce this.
- t.highestTimestamp = ct
+ t.highestTimestamp = st
}
return 0, nil
}
@@ -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/remote/write_handler.go b/storage/remote/write_handler.go
index e8559dd00e..9fdd750692 100644
--- a/storage/remote/write_handler.go
+++ b/storage/remote/write_handler.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
@@ -23,18 +23,11 @@ import (
"time"
"github.com/gogo/protobuf/proto"
- deltatocumulative "github.com/open-telemetry/opentelemetry-collector-contrib/processor/deltatocumulativeprocessor"
remoteapi "github.com/prometheus/client_golang/exp/api/remote"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/prometheus/common/model"
- "go.opentelemetry.io/collector/component"
- "go.opentelemetry.io/collector/consumer"
- "go.opentelemetry.io/collector/pdata/pmetric"
- "go.opentelemetry.io/collector/processor"
- "go.opentelemetry.io/otel/metric/noop"
- "github.com/prometheus/prometheus/config"
"github.com/prometheus/prometheus/model/exemplar"
"github.com/prometheus/prometheus/model/histogram"
"github.com/prometheus/prometheus/model/labels"
@@ -43,7 +36,6 @@ import (
writev2 "github.com/prometheus/prometheus/prompb/io/prometheus/write/v2"
"github.com/prometheus/prometheus/schema"
"github.com/prometheus/prometheus/storage"
- otlptranslator "github.com/prometheus/prometheus/storage/remote/otlptranslator/prometheusremotewrite"
)
type writeHandler struct {
@@ -53,7 +45,7 @@ type writeHandler struct {
samplesWithInvalidLabelsTotal prometheus.Counter
samplesAppendedWithoutMetadata prometheus.Counter
- ingestCTZeroSample bool
+ ingestSTZeroSample bool
enableTypeAndUnitLabels bool
appendMetadata bool
}
@@ -65,7 +57,7 @@ const maxAheadTime = 10 * time.Minute
//
// NOTE(bwplotka): When accepting v2 proto and spec, partial writes are possible
// as per https://prometheus.io/docs/specs/remote_write_spec_2_0/#partial-write.
-func NewWriteHandler(logger *slog.Logger, reg prometheus.Registerer, appendable storage.Appendable, acceptedMsgs remoteapi.MessageTypes, ingestCTZeroSample, enableTypeAndUnitLabels, appendMetadata bool) http.Handler {
+func NewWriteHandler(logger *slog.Logger, reg prometheus.Registerer, appendable storage.Appendable, acceptedMsgs remoteapi.MessageTypes, ingestSTZeroSample, enableTypeAndUnitLabels, appendMetadata bool) http.Handler {
h := &writeHandler{
logger: logger,
appendable: appendable,
@@ -82,7 +74,7 @@ func NewWriteHandler(logger *slog.Logger, reg prometheus.Registerer, appendable
Help: "The total number of received remote write samples (and histogram samples) which were ingested without corresponding metadata.",
}),
- ingestCTZeroSample: ingestCTZeroSample,
+ ingestSTZeroSample: ingestSTZeroSample,
enableTypeAndUnitLabels: enableTypeAndUnitLabels,
appendMetadata: appendMetadata,
}
@@ -96,6 +88,10 @@ func isHistogramValidationError(err error) bool {
}
// Store implements remoteapi.writeStorage interface.
+// TODO(bwplotka): Improve remoteapi.Store API. Right now it's confusing if PRWv1 flows should use WriteResponse or not.
+// If it's not filled, it will be "confirmed zero" which caused partial error reporting on client side in the past.
+// Temporary fix was done to only care about WriteResponse stats for PRW2 (see https://github.com/prometheus/client_golang/pull/1927
+// but better approach would be to only confirm if explicit stats were injected.
func (h *writeHandler) Store(r *http.Request, msgType remoteapi.WriteMessageType) (*remoteapi.WriteResponse, error) {
// Store receives request with decompressed content in body.
body, err := io.ReadAll(r.Body)
@@ -229,7 +225,8 @@ func (h *writeHandler) appendV1Samples(app storage.Appender, ss []prompb.Sample,
if err != nil {
if errors.Is(err, storage.ErrOutOfOrderSample) ||
errors.Is(err, storage.ErrOutOfBounds) ||
- errors.Is(err, storage.ErrDuplicateSampleForTimestamp) {
+ errors.Is(err, storage.ErrDuplicateSampleForTimestamp) ||
+ errors.Is(err, storage.ErrTooOldSample) {
h.logger.Error("Out of order sample from remote write", "err", err.Error(), "series", labels.String(), "timestamp", s.Timestamp)
}
return err
@@ -251,7 +248,8 @@ func (h *writeHandler) appendV1Histograms(app storage.Appender, hh []prompb.Hist
// a note indicating its inclusion in the future.
if errors.Is(err, storage.ErrOutOfOrderSample) ||
errors.Is(err, storage.ErrOutOfBounds) ||
- errors.Is(err, storage.ErrDuplicateSampleForTimestamp) {
+ errors.Is(err, storage.ErrDuplicateSampleForTimestamp) ||
+ errors.Is(err, storage.ErrTooOldSample) {
h.logger.Error("Out of order histogram from remote write", "err", err.Error(), "series", labels.String(), "timestamp", hp.Timestamp)
}
return err
@@ -325,7 +323,11 @@ func (h *writeHandler) appendV2(app storage.Appender, req *writev2.Request, rs *
if h.enableTypeAndUnitLabels && (m.Type != model.MetricTypeUnknown || m.Unit != "") {
slb := labels.NewScratchBuilder(ls.Len() + 2) // +2 for __type__ and __unit__
ls.Range(func(l labels.Label) {
- slb.Add(l.Name, l.Value)
+ // Skip __type__ and __unit__ labels if they exist in the incoming labels.
+ // They will be added from metadata to avoid duplicates.
+ if l.Name != model.MetricTypeLabel && l.Name != model.MetricUnitLabel {
+ slb.Add(l.Name, l.Value)
+ }
})
schema.Metadata{Type: m.Type, Unit: m.Unit}.AddToLabels(&slb)
slb.Sort()
@@ -353,20 +355,18 @@ func (h *writeHandler) appendV2(app storage.Appender, req *writev2.Request, rs *
allSamplesSoFar := rs.AllSamples()
var ref storage.SeriesRef
-
- // Samples.
- if h.ingestCTZeroSample && len(ts.Samples) > 0 && ts.Samples[0].Timestamp != 0 && ts.CreatedTimestamp != 0 {
- // CT only needs to be ingested for the first sample, it will be considered
- // out of order for the rest.
- ref, err = app.AppendCTZeroSample(ref, ls, ts.Samples[0].Timestamp, ts.CreatedTimestamp)
- if err != nil && !errors.Is(err, storage.ErrOutOfOrderCT) {
- // Even for the first sample OOO is a common scenario because
- // we can't tell if a CT was already ingested in a previous request.
- // We ignore the error.
- h.logger.Debug("Error when appending CT in remote write request", "err", err, "series", ls.String(), "created_timestamp", ts.CreatedTimestamp, "timestamp", ts.Samples[0].Timestamp)
- }
- }
for _, s := range ts.Samples {
+ if h.ingestSTZeroSample && s.StartTimestamp != 0 && s.Timestamp != 0 {
+ ref, err = app.AppendSTZeroSample(ref, ls, s.Timestamp, s.StartTimestamp)
+ // We treat OOO errors specially as it's a common scenario given:
+ // * We can't tell if ST was already ingested in a previous request.
+ // * We don't check if ST changed for stream of samples (we typically have one though),
+ // as it's checked in the AppendSTZeroSample reliably.
+ if err != nil && !errors.Is(err, storage.ErrOutOfOrderST) {
+ h.logger.Debug("Error when appending ST from remote write request", "err", err, "series", ls.String(), "start_timestamp", s.StartTimestamp, "timestamp", s.Timestamp)
+ }
+ }
+
ref, err = app.Append(ref, ls, s.GetTimestamp(), s.GetValue())
if err == nil {
rs.Samples++
@@ -387,15 +387,14 @@ func (h *writeHandler) appendV2(app storage.Appender, req *writev2.Request, rs *
// Native Histograms.
for _, hp := range ts.Histograms {
- if h.ingestCTZeroSample && hp.Timestamp != 0 && ts.CreatedTimestamp != 0 {
- // Differently from samples, we need to handle CT for each histogram instead of just the first one.
- // This is because histograms and float histograms are stored separately, even if they have the same labels.
- ref, err = h.handleHistogramZeroSample(app, ref, ls, hp, ts.CreatedTimestamp)
- if err != nil && !errors.Is(err, storage.ErrOutOfOrderCT) {
- // Even for the first sample OOO is a common scenario because
- // we can't tell if a CT was already ingested in a previous request.
- // We ignore the error.
- h.logger.Debug("Error when appending CT in remote write request", "err", err, "series", ls.String(), "created_timestamp", ts.CreatedTimestamp, "timestamp", hp.Timestamp)
+ if h.ingestSTZeroSample && hp.StartTimestamp != 0 && hp.Timestamp != 0 {
+ ref, err = h.handleHistogramZeroSample(app, ref, ls, hp, hp.StartTimestamp)
+ // We treat OOO errors specially as it's a common scenario given:
+ // * We can't tell if ST was already ingested in a previous request.
+ // * We don't check if ST changed for stream of samples (we typically have one though),
+ // as it's checked in the ingestSTZeroSample reliably.
+ if err != nil && !errors.Is(err, storage.ErrOutOfOrderST) {
+ h.logger.Debug("Error when appending ST from remote write request", "err", err, "series", ls.String(), "start_timestamp", hp.StartTimestamp, "timestamp", hp.Timestamp)
}
}
if hp.IsFloatHistogram() {
@@ -412,7 +411,8 @@ func (h *writeHandler) appendV2(app storage.Appender, req *writev2.Request, rs *
// a note indicating its inclusion in the future.
if errors.Is(err, storage.ErrOutOfOrderSample) ||
errors.Is(err, storage.ErrOutOfBounds) ||
- errors.Is(err, storage.ErrDuplicateSampleForTimestamp) {
+ errors.Is(err, storage.ErrDuplicateSampleForTimestamp) ||
+ errors.Is(err, storage.ErrTooOldSample) {
// TODO(bwplotka): Not too spammy log?
h.logger.Error("Out of order histogram from remote write", "err", err.Error(), "series", ls.String(), "timestamp", hp.Timestamp)
badRequestErrs = append(badRequestErrs, fmt.Errorf("%w for series %v", err, ls.String()))
@@ -474,205 +474,20 @@ func (h *writeHandler) appendV2(app storage.Appender, req *writev2.Request, rs *
return samplesWithoutMetadata, http.StatusBadRequest, errors.Join(badRequestErrs...)
}
-// handleHistogramZeroSample appends CT as a zero-value sample with CT value as the sample timestamp.
-// It doesn't return errors in case of out of order CT.
-func (*writeHandler) handleHistogramZeroSample(app storage.Appender, ref storage.SeriesRef, l labels.Labels, hist writev2.Histogram, ct int64) (storage.SeriesRef, error) {
+// handleHistogramZeroSample appends ST as a zero-value sample with st value as the sample timestamp.
+// It doesn't return errors in case of out of order ST.
+func (*writeHandler) handleHistogramZeroSample(app storage.Appender, ref storage.SeriesRef, l labels.Labels, hist writev2.Histogram, st int64) (storage.SeriesRef, error) {
var err error
if hist.IsFloatHistogram() {
- ref, err = app.AppendHistogramCTZeroSample(ref, l, hist.Timestamp, ct, nil, hist.ToFloatHistogram())
+ ref, err = app.AppendHistogramSTZeroSample(ref, l, hist.Timestamp, st, nil, hist.ToFloatHistogram())
} else {
- ref, err = app.AppendHistogramCTZeroSample(ref, l, hist.Timestamp, ct, hist.ToIntHistogram(), nil)
+ ref, err = app.AppendHistogramSTZeroSample(ref, l, hist.Timestamp, st, hist.ToIntHistogram(), nil)
}
return ref, err
}
-type OTLPOptions struct {
- // Convert delta samples to their cumulative equivalent by aggregating in-memory
- ConvertDelta bool
- // Store the raw delta samples as metrics with unknown type (we don't have a proper type for delta yet, therefore
- // marking the metric type as unknown for now).
- // We're in an early phase of implementing delta support (proposal: https://github.com/prometheus/proposals/pull/48/)
- NativeDelta bool
- // LookbackDelta is the query lookback delta.
- // Used to calculate the target_info sample timestamp interval.
- LookbackDelta time.Duration
- // Add type and unit labels to the metrics.
- EnableTypeAndUnitLabels bool
- // IngestCTZeroSample enables writing zero samples based on the start time
- // of metrics.
- IngestCTZeroSample bool
-}
-
-// NewOTLPWriteHandler creates a http.Handler that accepts OTLP write requests and
-// writes them to the provided appendable.
-func NewOTLPWriteHandler(logger *slog.Logger, reg prometheus.Registerer, appendable storage.Appendable, configFunc func() config.Config, opts OTLPOptions) http.Handler {
- if opts.NativeDelta && opts.ConvertDelta {
- // This should be validated when iterating through feature flags, so not expected to fail here.
- panic("cannot enable native delta ingestion and delta2cumulative conversion at the same time")
- }
-
- ex := &rwExporter{
- logger: logger,
- appendable: appendable,
- config: configFunc,
- allowDeltaTemporality: opts.NativeDelta,
- lookbackDelta: opts.LookbackDelta,
- ingestCTZeroSample: opts.IngestCTZeroSample,
- enableTypeAndUnitLabels: opts.EnableTypeAndUnitLabels,
- // Register metrics.
- metrics: otlptranslator.NewCombinedAppenderMetrics(reg),
- }
-
- wh := &otlpWriteHandler{logger: logger, defaultConsumer: ex}
-
- if opts.ConvertDelta {
- fac := deltatocumulative.NewFactory()
- set := processor.Settings{
- ID: component.NewID(fac.Type()),
- TelemetrySettings: component.TelemetrySettings{MeterProvider: noop.NewMeterProvider()},
- }
- d2c, err := fac.CreateMetrics(context.Background(), set, fac.CreateDefaultConfig(), wh.defaultConsumer)
- if err != nil {
- // fac.CreateMetrics directly calls [deltatocumulativeprocessor.createMetricsProcessor],
- // which only errors if:
- // - cfg.(type) != *Config
- // - telemetry.New fails due to bad set.TelemetrySettings
- //
- // both cannot be the case, as we pass a valid *Config and valid TelemetrySettings.
- // as such, we assume this error to never occur.
- // if it is, our assumptions are broken in which case a panic seems acceptable.
- panic(fmt.Errorf("failed to create metrics processor: %w", err))
- }
- if err := d2c.Start(context.Background(), nil); err != nil {
- // deltatocumulative does not error on start. see above for panic reasoning
- panic(err)
- }
- wh.d2cConsumer = d2c
- }
-
- return wh
-}
-
-type rwExporter struct {
- logger *slog.Logger
- appendable storage.Appendable
- config func() config.Config
- allowDeltaTemporality bool
- lookbackDelta time.Duration
- ingestCTZeroSample bool
- enableTypeAndUnitLabels bool
-
- // Metrics.
- metrics otlptranslator.CombinedAppenderMetrics
-}
-
-func (rw *rwExporter) ConsumeMetrics(ctx context.Context, md pmetric.Metrics) error {
- otlpCfg := rw.config().OTLPConfig
- app := &remoteWriteAppender{
- Appender: rw.appendable.Appender(ctx),
- maxTime: timestamp.FromTime(time.Now().Add(maxAheadTime)),
- }
- combinedAppender := otlptranslator.NewCombinedAppender(app, rw.logger, rw.ingestCTZeroSample, rw.metrics)
- converter := otlptranslator.NewPrometheusConverter(combinedAppender)
- annots, err := converter.FromMetrics(ctx, md, otlptranslator.Settings{
- AddMetricSuffixes: otlpCfg.TranslationStrategy.ShouldAddSuffixes(),
- AllowUTF8: !otlpCfg.TranslationStrategy.ShouldEscape(),
- PromoteResourceAttributes: otlptranslator.NewPromoteResourceAttributes(otlpCfg),
- KeepIdentifyingResourceAttributes: otlpCfg.KeepIdentifyingResourceAttributes,
- ConvertHistogramsToNHCB: otlpCfg.ConvertHistogramsToNHCB,
- PromoteScopeMetadata: otlpCfg.PromoteScopeMetadata,
- AllowDeltaTemporality: rw.allowDeltaTemporality,
- LookbackDelta: rw.lookbackDelta,
- EnableTypeAndUnitLabels: rw.enableTypeAndUnitLabels,
- LabelNameUnderscoreSanitization: otlpCfg.LabelNameUnderscoreSanitization,
- LabelNamePreserveMultipleUnderscores: otlpCfg.LabelNamePreserveMultipleUnderscores,
- })
-
- defer func() {
- if err != nil {
- _ = app.Rollback()
- return
- }
- err = app.Commit()
- }()
- ws, _ := annots.AsStrings("", 0, 0)
- if len(ws) > 0 {
- rw.logger.Warn("Warnings translating OTLP metrics to Prometheus write request", "warnings", ws)
- }
- return err
-}
-
-func (*rwExporter) Capabilities() consumer.Capabilities {
- return consumer.Capabilities{MutatesData: false}
-}
-
-type otlpWriteHandler struct {
- logger *slog.Logger
-
- defaultConsumer consumer.Metrics // stores deltas as-is
- d2cConsumer consumer.Metrics // converts deltas to cumulative
-}
-
-func (h *otlpWriteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- req, err := DecodeOTLPWriteRequest(r)
- if err != nil {
- h.logger.Error("Error decoding OTLP write request", "err", err.Error())
- http.Error(w, err.Error(), http.StatusBadRequest)
- return
- }
-
- md := req.Metrics()
- // If deltatocumulative conversion enabled AND delta samples exist, use slower conversion path.
- // While deltatocumulative can also accept cumulative metrics (and then just forwards them as-is), it currently
- // holds a sync.Mutex when entering ConsumeMetrics. This is slow and not necessary when ingesting cumulative metrics.
- if h.d2cConsumer != nil && hasDelta(md) {
- err = h.d2cConsumer.ConsumeMetrics(r.Context(), md)
- } else {
- // Otherwise use default consumer (alongside cumulative samples, this will accept delta samples and write as-is
- // if native-delta-support is enabled).
- err = h.defaultConsumer.ConsumeMetrics(r.Context(), md)
- }
-
- switch {
- case err == nil:
- case errors.Is(err, storage.ErrOutOfOrderSample), errors.Is(err, storage.ErrOutOfBounds), errors.Is(err, storage.ErrDuplicateSampleForTimestamp):
- // Indicated an out of order sample is a bad request to prevent retries.
- http.Error(w, err.Error(), http.StatusBadRequest)
- return
- default:
- h.logger.Error("Error appending remote write", "err", err.Error())
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
-
- w.WriteHeader(http.StatusOK)
-}
-
-func hasDelta(md pmetric.Metrics) bool {
- for i := range md.ResourceMetrics().Len() {
- sms := md.ResourceMetrics().At(i).ScopeMetrics()
- for i := range sms.Len() {
- ms := sms.At(i).Metrics()
- for i := range ms.Len() {
- temporality := pmetric.AggregationTemporalityUnspecified
- m := ms.At(i)
- switch ms.At(i).Type() {
- case pmetric.MetricTypeSum:
- temporality = m.Sum().AggregationTemporality()
- case pmetric.MetricTypeExponentialHistogram:
- temporality = m.ExponentialHistogram().AggregationTemporality()
- case pmetric.MetricTypeHistogram:
- temporality = m.Histogram().AggregationTemporality()
- }
- if temporality == pmetric.AggregationTemporalityDelta {
- return true
- }
- }
- }
- }
- return false
-}
-
+// TODO(bwplotka): Consider exposing timeLimitAppender and bucketLimitAppender appenders from scrape/target.go
+// to DRY, they do the same.
type remoteWriteAppender struct {
storage.Appender
@@ -692,19 +507,23 @@ func (app *remoteWriteAppender) Append(ref storage.SeriesRef, lset labels.Labels
}
func (app *remoteWriteAppender) AppendHistogram(ref storage.SeriesRef, l labels.Labels, t int64, h *histogram.Histogram, fh *histogram.FloatHistogram) (storage.SeriesRef, error) {
+ var err error
if t > app.maxTime {
return 0, fmt.Errorf("%w: timestamp is too far in the future", storage.ErrOutOfBounds)
}
if h != nil && histogram.IsExponentialSchemaReserved(h.Schema) && h.Schema > histogram.ExponentialSchemaMax {
- h = h.ReduceResolution(histogram.ExponentialSchemaMax)
+ if err = h.ReduceResolution(histogram.ExponentialSchemaMax); err != nil {
+ return 0, err
+ }
}
if fh != nil && histogram.IsExponentialSchemaReserved(fh.Schema) && fh.Schema > histogram.ExponentialSchemaMax {
- fh = fh.ReduceResolution(histogram.ExponentialSchemaMax)
+ if err = fh.ReduceResolution(histogram.ExponentialSchemaMax); err != nil {
+ return 0, err
+ }
}
- ref, err := app.Appender.AppendHistogram(ref, l, t, h, fh)
- if err != nil {
+ if ref, err = app.Appender.AppendHistogram(ref, l, t, h, fh); err != nil {
return 0, err
}
return ref, nil
@@ -721,3 +540,27 @@ func (app *remoteWriteAppender) AppendExemplar(ref storage.SeriesRef, l labels.L
}
return ref, nil
}
+
+type remoteWriteAppenderV2 struct {
+ storage.AppenderV2
+
+ maxTime int64
+}
+
+func (app *remoteWriteAppenderV2) 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, fmt.Errorf("%w: timestamp is too far in the future", storage.ErrOutOfBounds)
+ }
+
+ if h != nil && histogram.IsExponentialSchemaReserved(h.Schema) && h.Schema > histogram.ExponentialSchemaMax {
+ if err := h.ReduceResolution(histogram.ExponentialSchemaMax); err != nil {
+ return 0, err
+ }
+ }
+ if fh != nil && histogram.IsExponentialSchemaReserved(fh.Schema) && fh.Schema > histogram.ExponentialSchemaMax {
+ if err := fh.ReduceResolution(histogram.ExponentialSchemaMax); err != nil {
+ return 0, err
+ }
+ }
+ return app.AppenderV2.Append(ref, ls, st, t, v, h, fh, opts)
+}
diff --git a/storage/remote/write_handler_test.go b/storage/remote/write_handler_test.go
index 536fba63cd..2cf1217933 100644
--- a/storage/remote/write_handler_test.go
+++ b/storage/remote/write_handler_test.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
@@ -358,12 +358,12 @@ func TestRemoteWriteHandler_V2Message(t *testing.T) {
commitErr error
appendSampleErr error
- appendCTZeroSampleErr error
+ appendSTZeroSampleErr error
appendHistogramErr error
appendExemplarErr error
updateMetadataErr error
- ingestCTZeroSample bool
+ ingestSTZeroSample bool
enableTypeAndUnitLabels bool
appendMetadata bool
expectedLabels labels.Labels // For verifying type/unit labels
@@ -372,7 +372,7 @@ func TestRemoteWriteHandler_V2Message(t *testing.T) {
desc: "All timeseries accepted/ct_enabled",
input: writeV2RequestFixture.Timeseries,
expectedCode: http.StatusNoContent,
- ingestCTZeroSample: true,
+ ingestSTZeroSample: true,
},
{
desc: "All timeseries accepted/ct_disabled",
@@ -701,12 +701,12 @@ func TestRemoteWriteHandler_V2Message(t *testing.T) {
appendable := &mockAppendable{
commitErr: tc.commitErr,
appendSampleErr: tc.appendSampleErr,
- appendCTZeroSampleErr: tc.appendCTZeroSampleErr,
+ appendSTZeroSampleErr: tc.appendSTZeroSampleErr,
appendHistogramErr: tc.appendHistogramErr,
appendExemplarErr: tc.appendExemplarErr,
updateMetadataErr: tc.updateMetadataErr,
}
- handler := NewWriteHandler(promslog.NewNopLogger(), nil, appendable, []remoteapi.WriteMessageType{remoteapi.WriteV2MessageType}, tc.ingestCTZeroSample, tc.enableTypeAndUnitLabels, tc.appendMetadata)
+ handler := NewWriteHandler(promslog.NewNopLogger(), nil, appendable, []remoteapi.WriteMessageType{remoteapi.WriteV2MessageType}, tc.ingestSTZeroSample, tc.enableTypeAndUnitLabels, tc.appendMetadata)
recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, req)
@@ -752,14 +752,12 @@ func TestRemoteWriteHandler_V2Message(t *testing.T) {
i, j, k, m int
)
for _, ts := range writeV2RequestFixture.Timeseries {
- zeroHistogramIngested := false
- zeroFloatHistogramIngested := false
ls, err := ts.ToLabels(&b, writeV2RequestFixture.Symbols)
require.NoError(t, err)
for _, s := range ts.Samples {
- if ts.CreatedTimestamp != 0 && tc.ingestCTZeroSample {
- requireEqual(t, mockSample{ls, ts.CreatedTimestamp, 0}, appendable.samples[i])
+ if s.StartTimestamp != 0 && tc.ingestSTZeroSample {
+ requireEqual(t, mockSample{ls, s.StartTimestamp, 0}, appendable.samples[i])
i++
}
requireEqual(t, mockSample{ls, s.Timestamp, s.Value}, appendable.samples[i])
@@ -768,27 +766,21 @@ func TestRemoteWriteHandler_V2Message(t *testing.T) {
for _, hp := range ts.Histograms {
if hp.IsFloatHistogram() {
fh := hp.ToFloatHistogram()
- if !zeroFloatHistogramIngested && ts.CreatedTimestamp != 0 && tc.ingestCTZeroSample {
- requireEqual(t, mockHistogram{ls, ts.CreatedTimestamp, nil, &histogram.FloatHistogram{}}, appendable.histograms[k])
+ if hp.StartTimestamp != 0 && tc.ingestSTZeroSample {
+ requireEqual(t, mockHistogram{ls, hp.StartTimestamp, nil, &histogram.FloatHistogram{}}, appendable.histograms[k])
k++
- zeroFloatHistogramIngested = true
}
requireEqual(t, mockHistogram{ls, hp.Timestamp, nil, fh}, appendable.histograms[k])
} else {
h := hp.ToIntHistogram()
- if !zeroHistogramIngested && ts.CreatedTimestamp != 0 && tc.ingestCTZeroSample {
- requireEqual(t, mockHistogram{ls, ts.CreatedTimestamp, &histogram.Histogram{}, nil}, appendable.histograms[k])
+ if hp.StartTimestamp != 0 && tc.ingestSTZeroSample {
+ requireEqual(t, mockHistogram{ls, hp.StartTimestamp, &histogram.Histogram{}, nil}, appendable.histograms[k])
k++
- zeroHistogramIngested = true
}
requireEqual(t, mockHistogram{ls, hp.Timestamp, h, nil}, appendable.histograms[k])
}
k++
}
- if ts.CreatedTimestamp != 0 && tc.ingestCTZeroSample {
- require.True(t, zeroHistogramIngested)
- require.True(t, zeroFloatHistogramIngested)
- }
if tc.appendExemplarErr == nil {
for _, e := range ts.Exemplars {
ex, err := e.ToExemplar(&b, writeV2RequestFixture.Symbols)
@@ -813,6 +805,104 @@ func TestRemoteWriteHandler_V2Message(t *testing.T) {
}
}
+// TestRemoteWriteHandler_V2Message_NoDuplicateTypeAndUnitLabels verifies that when
+// type-and-unit-labels feature is enabled, the receiver correctly handles cases where
+// __type__ and __unit__ labels are already present in the incoming labels.
+// Regression test for https://github.com/prometheus/prometheus/issues/17480.
+func TestRemoteWriteHandler_V2Message_NoDuplicateTypeAndUnitLabels(t *testing.T) {
+ for _, tc := range []struct {
+ desc string
+ labelsToSend labels.Labels
+ metadataToSend writev2.Metadata
+ expectedLabels labels.Labels
+ }{
+ {
+ desc: "Labels with __type__ and __unit__ should not be duplicated",
+ labelsToSend: labels.FromStrings("__name__", "node_cpu_seconds_total", "__type__", "counter", "__unit__", "seconds", "cpu", "0", "mode", "idle"),
+ metadataToSend: writev2.Metadata{
+ Type: writev2.Metadata_METRIC_TYPE_COUNTER,
+ },
+ expectedLabels: labels.FromStrings("__name__", "node_cpu_seconds_total", "__type__", "counter", "__unit__", "seconds", "cpu", "0", "mode", "idle"),
+ },
+ {
+ desc: "Labels with __type__ only should not be duplicated",
+ labelsToSend: labels.FromStrings("__name__", "test_gauge", "__type__", "gauge", "instance", "localhost"),
+ metadataToSend: writev2.Metadata{
+ Type: writev2.Metadata_METRIC_TYPE_GAUGE,
+ },
+ expectedLabels: labels.FromStrings("__name__", "test_gauge", "__type__", "gauge", "instance", "localhost"),
+ },
+ {
+ desc: "Labels with __unit__ only should not be duplicated when metadata has unit",
+ labelsToSend: labels.FromStrings("__name__", "test_metric", "__unit__", "bytes", "job", "test"),
+ metadataToSend: writev2.Metadata{
+ Type: writev2.Metadata_METRIC_TYPE_GAUGE,
+ },
+ expectedLabels: labels.FromStrings("__name__", "test_metric", "__type__", "gauge", "__unit__", "bytes", "job", "test"),
+ },
+ {
+ desc: "Metadata type and unit override labels",
+ labelsToSend: labels.FromStrings("__name__", "test_metric", "__type__", "counter", "__unit__", "seconds", "job", "test"),
+ metadataToSend: writev2.Metadata{
+ Type: writev2.Metadata_METRIC_TYPE_GAUGE,
+ },
+ expectedLabels: labels.FromStrings("__name__", "test_metric", "__type__", "gauge", "__unit__", "seconds", "job", "test"),
+ },
+ } {
+ t.Run(tc.desc, func(t *testing.T) {
+ symbolTable := writev2.NewSymbolTable()
+ labelRefs := symbolTable.SymbolizeLabels(tc.labelsToSend, nil)
+
+ var unitRef uint32
+ if unit := tc.labelsToSend.Get("__unit__"); unit != "" {
+ unitRef = symbolTable.Symbolize(unit)
+ }
+
+ ts := []writev2.TimeSeries{
+ {
+ LabelsRefs: labelRefs,
+ Metadata: writev2.Metadata{
+ Type: tc.metadataToSend.Type,
+ UnitRef: unitRef,
+ },
+ Samples: []writev2.Sample{{Value: 42.0, Timestamp: 1000}},
+ },
+ }
+
+ payload, _, _, err := buildV2WriteRequest(promslog.NewNopLogger(), ts, symbolTable.Symbols(), nil, nil, nil, "snappy")
+ require.NoError(t, err)
+
+ req, err := http.NewRequest(http.MethodPost, "", bytes.NewReader(payload))
+ require.NoError(t, err)
+
+ req.Header.Set("Content-Type", remoteWriteContentTypeHeaders[remoteapi.WriteV2MessageType])
+ req.Header.Set("Content-Encoding", compression.Snappy)
+ req.Header.Set(RemoteWriteVersionHeader, RemoteWriteVersion20HeaderValue)
+
+ appendable := &mockAppendable{}
+ handler := NewWriteHandler(promslog.NewNopLogger(), nil, appendable, []remoteapi.WriteMessageType{remoteapi.WriteV2MessageType}, false, true, false)
+
+ recorder := httptest.NewRecorder()
+ handler.ServeHTTP(recorder, req)
+
+ resp := recorder.Result()
+ require.Equal(t, http.StatusNoContent, resp.StatusCode)
+
+ require.Len(t, appendable.samples, 1)
+ receivedLabels := appendable.samples[0].l
+
+ duplicateLabel, hasDuplicate := receivedLabels.HasDuplicateLabelNames()
+ require.False(t, hasDuplicate, "Labels should NOT contain duplicates, but found duplicate label: %s\nReceived labels: %s", duplicateLabel, receivedLabels.String())
+
+ require.Equal(t, tc.expectedLabels.String(), receivedLabels.String(), "Labels should match expected")
+
+ if tc.expectedLabels.Get("__type__") != "" {
+ require.NotEmpty(t, receivedLabels.Get("__type__"), "__type__ should be present in labels")
+ }
+ })
+ }
+}
+
// NOTE: V2 Message is tested in TestRemoteWriteHandler_V2Message.
func TestOutOfOrderSample_V1Message(t *testing.T) {
for _, tc := range []struct {
@@ -1177,6 +1267,7 @@ func genSeriesWithSample(numSeries int, ts int64) []prompb.TimeSeries {
return series
}
+// TODO(bwplotka): Delete and switch all to teststorage.Appendable.
type mockAppendable struct {
latestSample map[uint64]int64
samples []mockSample
@@ -1190,7 +1281,7 @@ type mockAppendable struct {
// optional errors to inject.
commitErr error
appendSampleErr error
- appendCTZeroSampleErr error
+ appendSTZeroSampleErr error
appendHistogramErr error
appendExemplarErr error
updateMetadataErr error
@@ -1342,13 +1433,13 @@ func (m *mockAppendable) AppendHistogram(_ storage.SeriesRef, l labels.Labels, t
return storage.SeriesRef(hash), nil
}
-func (m *mockAppendable) AppendHistogramCTZeroSample(_ storage.SeriesRef, l labels.Labels, t, ct int64, h *histogram.Histogram, _ *histogram.FloatHistogram) (storage.SeriesRef, error) {
- if m.appendCTZeroSampleErr != nil {
- return 0, m.appendCTZeroSampleErr
+func (m *mockAppendable) AppendHistogramSTZeroSample(_ storage.SeriesRef, l labels.Labels, t, st int64, h *histogram.Histogram, _ *histogram.FloatHistogram) (storage.SeriesRef, error) {
+ if m.appendSTZeroSampleErr != nil {
+ return 0, m.appendSTZeroSampleErr
}
// Created Timestamp can't be higher than the original sample's timestamp.
- if ct > t {
+ if st > t {
return 0, storage.ErrOutOfOrderSample
}
hash := l.Hash()
@@ -1358,10 +1449,10 @@ func (m *mockAppendable) AppendHistogramCTZeroSample(_ storage.SeriesRef, l labe
} else {
latestTs = m.latestFloatHist[hash]
}
- if ct < latestTs {
+ if st < latestTs {
return 0, storage.ErrOutOfOrderSample
}
- if ct == latestTs {
+ if st == latestTs {
return 0, storage.ErrDuplicateSampleForTimestamp
}
@@ -1374,11 +1465,11 @@ func (m *mockAppendable) AppendHistogramCTZeroSample(_ storage.SeriesRef, l labe
}
if h != nil {
- m.latestHistogram[hash] = ct
- m.histograms = append(m.histograms, mockHistogram{l, ct, &histogram.Histogram{}, nil})
+ m.latestHistogram[hash] = st
+ m.histograms = append(m.histograms, mockHistogram{l, st, &histogram.Histogram{}, nil})
} else {
- m.latestFloatHist[hash] = ct
- m.histograms = append(m.histograms, mockHistogram{l, ct, nil, &histogram.FloatHistogram{}})
+ m.latestFloatHist[hash] = st
+ m.histograms = append(m.histograms, mockHistogram{l, st, nil, &histogram.FloatHistogram{}})
}
return storage.SeriesRef(hash), nil
}
@@ -1392,21 +1483,21 @@ func (m *mockAppendable) UpdateMetadata(ref storage.SeriesRef, l labels.Labels,
return ref, nil
}
-func (m *mockAppendable) AppendCTZeroSample(_ storage.SeriesRef, l labels.Labels, t, ct int64) (storage.SeriesRef, error) {
- if m.appendCTZeroSampleErr != nil {
- return 0, m.appendCTZeroSampleErr
+func (m *mockAppendable) AppendSTZeroSample(_ storage.SeriesRef, l labels.Labels, t, st int64) (storage.SeriesRef, error) {
+ if m.appendSTZeroSampleErr != nil {
+ return 0, m.appendSTZeroSampleErr
}
// Created Timestamp can't be higher than the original sample's timestamp.
- if ct > t {
+ if st > t {
return 0, storage.ErrOutOfOrderSample
}
hash := l.Hash()
latestTs := m.latestSample[hash]
- if ct < latestTs {
+ if st < latestTs {
return 0, storage.ErrOutOfOrderSample
}
- if ct == latestTs {
+ if st == latestTs {
return 0, storage.ErrDuplicateSampleForTimestamp
}
@@ -1417,8 +1508,8 @@ func (m *mockAppendable) AppendCTZeroSample(_ storage.SeriesRef, l labels.Labels
return 0, tsdb.ErrInvalidSample
}
- m.latestSample[hash] = ct
- m.samples = append(m.samples, mockSample{l, ct, 0})
+ m.latestSample[hash] = st
+ m.samples = append(m.samples, mockSample{l, st, 0})
return storage.SeriesRef(hash), nil
}
@@ -1518,3 +1609,74 @@ func TestHistogramsReduction(t *testing.T) {
})
}
}
+
+// Regression test for https://github.com/prometheus/prometheus/issues/17659
+func TestRemoteWriteHandler_ResponseStats(t *testing.T) {
+ payloadV1, _, _, err := buildWriteRequest(nil, writeRequestFixture.Timeseries, nil, nil, nil, nil, "snappy")
+ require.NoError(t, err)
+ payloadV2, _, _, err := buildV2WriteRequest(nil, writeV2RequestFixture.Timeseries, writeV2RequestFixture.Symbols, nil, nil, nil, "snappy")
+ require.NoError(t, err)
+
+ for _, tt := range []struct {
+ msgType remoteapi.WriteMessageType
+ payload []byte
+ forceInjectHeaders bool
+ expectHeaders bool
+ }{
+ {
+ msgType: remoteapi.WriteV1MessageType,
+ payload: payloadV1,
+ },
+ {
+ msgType: remoteapi.WriteV1MessageType,
+ payload: payloadV1,
+ forceInjectHeaders: true,
+ expectHeaders: true,
+ },
+ {
+ msgType: remoteapi.WriteV2MessageType,
+ payload: payloadV2,
+ expectHeaders: true,
+ },
+ } {
+ t.Run(fmt.Sprintf("msg=%v/force-inject-headers=%v", tt.msgType, tt.forceInjectHeaders), func(t *testing.T) {
+ // Setup server side.
+ appendable := &mockAppendable{}
+ handler := NewWriteHandler(
+ promslog.NewNopLogger(),
+ nil,
+ appendable,
+ []remoteapi.WriteMessageType{remoteapi.WriteV1MessageType, remoteapi.WriteV2MessageType},
+ false,
+ false,
+ false,
+ )
+
+ if tt.forceInjectHeaders {
+ base := handler
+ handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ // Inject response header. This simulates PRWv1 server that uses PRWv2 response headers
+ // for confirmation of samples. This is not against spec and we support it.
+ w.Header().Set(rw20WrittenSamplesHeader, "2")
+
+ base.ServeHTTP(w, r)
+ })
+ }
+
+ srv := httptest.NewServer(handler)
+
+ // Send message and do the parse response flow.
+ c := &Client{Client: srv.Client(), urlString: srv.URL, timeout: 5 * time.Minute, writeProtoMsg: tt.msgType}
+
+ stats, err := c.Store(t.Context(), tt.payload, 0)
+ require.NoError(t, err)
+
+ if tt.expectHeaders {
+ require.True(t, stats.Confirmed)
+ require.Equal(t, len(appendable.samples), stats.Samples)
+ } else {
+ require.False(t, stats.Confirmed)
+ }
+ })
+ }
+}
diff --git a/storage/remote/write_otlp_handler.go b/storage/remote/write_otlp_handler.go
new file mode 100644
index 0000000000..6cb4a0fff0
--- /dev/null
+++ b/storage/remote/write_otlp_handler.go
@@ -0,0 +1,276 @@
+// 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 remote
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "log/slog"
+ "net/http"
+ "time"
+
+ deltatocumulative "github.com/open-telemetry/opentelemetry-collector-contrib/processor/deltatocumulativeprocessor"
+ "github.com/prometheus/client_golang/prometheus"
+ "github.com/prometheus/client_golang/prometheus/promauto"
+ "go.opentelemetry.io/collector/component"
+ "go.opentelemetry.io/collector/consumer"
+ "go.opentelemetry.io/collector/pdata/pmetric"
+ "go.opentelemetry.io/collector/processor"
+ "go.opentelemetry.io/otel/metric/noop"
+
+ "github.com/prometheus/prometheus/config"
+ "github.com/prometheus/prometheus/model/histogram"
+ "github.com/prometheus/prometheus/model/labels"
+ "github.com/prometheus/prometheus/model/timestamp"
+ "github.com/prometheus/prometheus/storage"
+ otlptranslator "github.com/prometheus/prometheus/storage/remote/otlptranslator/prometheusremotewrite"
+)
+
+type OTLPOptions struct {
+ // Convert delta samples to their cumulative equivalent by aggregating in-memory
+ ConvertDelta bool
+ // Store the raw delta samples as metrics with unknown type (we don't have a proper type for delta yet, therefore
+ // marking the metric type as unknown for now).
+ // We're in an early phase of implementing delta support (proposal: https://github.com/prometheus/proposals/pull/48/)
+ NativeDelta bool
+ // LookbackDelta is the query lookback delta.
+ // Used to calculate the target_info sample timestamp interval.
+ LookbackDelta time.Duration
+ // Add type and unit labels to the metrics.
+ EnableTypeAndUnitLabels bool
+}
+
+// NewOTLPWriteHandler creates a http.Handler that accepts OTLP write requests and
+// writes them to the provided appendable.
+func NewOTLPWriteHandler(logger *slog.Logger, reg prometheus.Registerer, appendable storage.AppendableV2, configFunc func() config.Config, opts OTLPOptions) http.Handler {
+ if opts.NativeDelta && opts.ConvertDelta {
+ // This should be validated when iterating through feature flags, so not expected to fail here.
+ panic("cannot enable native delta ingestion and delta2cumulative conversion at the same time")
+ }
+
+ ex := &rwExporter{
+ logger: logger,
+ appendable: newOTLPInstrumentedAppendable(reg, appendable),
+ config: configFunc,
+ allowDeltaTemporality: opts.NativeDelta,
+ lookbackDelta: opts.LookbackDelta,
+ enableTypeAndUnitLabels: opts.EnableTypeAndUnitLabels,
+ }
+
+ wh := &otlpWriteHandler{logger: logger, defaultConsumer: ex}
+
+ if opts.ConvertDelta {
+ fac := deltatocumulative.NewFactory()
+ set := processor.Settings{
+ ID: component.NewID(fac.Type()),
+ TelemetrySettings: component.TelemetrySettings{MeterProvider: noop.NewMeterProvider()},
+ }
+ d2c, err := fac.CreateMetrics(context.Background(), set, fac.CreateDefaultConfig(), wh.defaultConsumer)
+ if err != nil {
+ // fac.CreateMetrics directly calls [deltatocumulativeprocessor.createMetricsProcessor],
+ // which only errors if:
+ // - cfg.(type) != *Config
+ // - telemetry.New fails due to bad set.TelemetrySettings
+ //
+ // both cannot be the case, as we pass a valid *Config and valid TelemetrySettings.
+ // as such, we assume this error to never occur.
+ // if it is, our assumptions are broken in which case a panic seems acceptable.
+ panic(fmt.Errorf("failed to create metrics processor: %w", err))
+ }
+ if err := d2c.Start(context.Background(), nil); err != nil {
+ // deltatocumulative does not error on start. see above for panic reasoning
+ panic(err)
+ }
+ wh.d2cConsumer = d2c
+ }
+
+ return wh
+}
+
+type rwExporter struct {
+ logger *slog.Logger
+ appendable storage.AppendableV2
+ config func() config.Config
+ allowDeltaTemporality bool
+ lookbackDelta time.Duration
+ enableTypeAndUnitLabels bool
+}
+
+func (rw *rwExporter) ConsumeMetrics(ctx context.Context, md pmetric.Metrics) error {
+ otlpCfg := rw.config().OTLPConfig
+ app := &remoteWriteAppenderV2{
+ AppenderV2: rw.appendable.AppenderV2(ctx),
+ maxTime: timestamp.FromTime(time.Now().Add(maxAheadTime)),
+ }
+ converter := otlptranslator.NewPrometheusConverter(app)
+ annots, err := converter.FromMetrics(ctx, md, otlptranslator.Settings{
+ AddMetricSuffixes: otlpCfg.TranslationStrategy.ShouldAddSuffixes(),
+ AllowUTF8: !otlpCfg.TranslationStrategy.ShouldEscape(),
+ PromoteResourceAttributes: otlptranslator.NewPromoteResourceAttributes(otlpCfg),
+ KeepIdentifyingResourceAttributes: otlpCfg.KeepIdentifyingResourceAttributes,
+ ConvertHistogramsToNHCB: otlpCfg.ConvertHistogramsToNHCB,
+ PromoteScopeMetadata: otlpCfg.PromoteScopeMetadata,
+ AllowDeltaTemporality: rw.allowDeltaTemporality,
+ LookbackDelta: rw.lookbackDelta,
+ EnableTypeAndUnitLabels: rw.enableTypeAndUnitLabels,
+ LabelNameUnderscoreSanitization: otlpCfg.LabelNameUnderscoreSanitization,
+ LabelNamePreserveMultipleUnderscores: otlpCfg.LabelNamePreserveMultipleUnderscores,
+ })
+
+ defer func() {
+ if err != nil {
+ _ = app.Rollback()
+ return
+ }
+ err = app.Commit()
+ }()
+ ws, _ := annots.AsStrings("", 0, 0)
+ if len(ws) > 0 {
+ rw.logger.Warn("Warnings translating OTLP metrics to Prometheus write request", "warnings", ws)
+ }
+ return err
+}
+
+func (*rwExporter) Capabilities() consumer.Capabilities {
+ return consumer.Capabilities{MutatesData: false}
+}
+
+type otlpWriteHandler struct {
+ logger *slog.Logger
+
+ defaultConsumer consumer.Metrics // stores deltas as-is
+ d2cConsumer consumer.Metrics // converts deltas to cumulative
+}
+
+func (h *otlpWriteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ req, err := DecodeOTLPWriteRequest(r)
+ if err != nil {
+ h.logger.Error("Error decoding OTLP write request", "err", err.Error())
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+
+ md := req.Metrics()
+ // If deltatocumulative conversion enabled AND delta samples exist, use slower conversion path.
+ // While deltatocumulative can also accept cumulative metrics (and then just forwards them as-is), it currently
+ // holds a sync.Mutex when entering ConsumeMetrics. This is slow and not necessary when ingesting cumulative metrics.
+ if h.d2cConsumer != nil && hasDelta(md) {
+ err = h.d2cConsumer.ConsumeMetrics(r.Context(), md)
+ } else {
+ // Otherwise use default consumer (alongside cumulative samples, this will accept delta samples and write as-is
+ // if native-delta-support is enabled).
+ err = h.defaultConsumer.ConsumeMetrics(r.Context(), md)
+ }
+
+ switch {
+ case err == nil:
+ case errors.Is(err, storage.ErrOutOfOrderSample), errors.Is(err, storage.ErrOutOfBounds), errors.Is(err, storage.ErrDuplicateSampleForTimestamp), errors.Is(err, storage.ErrTooOldSample):
+ // Indicated an out of order sample is a bad request to prevent retries.
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ default:
+ h.logger.Error("Error appending remote write", "err", err.Error())
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ w.WriteHeader(http.StatusOK)
+}
+
+func hasDelta(md pmetric.Metrics) bool {
+ for i := range md.ResourceMetrics().Len() {
+ sms := md.ResourceMetrics().At(i).ScopeMetrics()
+ for i := range sms.Len() {
+ ms := sms.At(i).Metrics()
+ for i := range ms.Len() {
+ temporality := pmetric.AggregationTemporalityUnspecified
+ m := ms.At(i)
+ switch ms.At(i).Type() {
+ case pmetric.MetricTypeSum:
+ temporality = m.Sum().AggregationTemporality()
+ case pmetric.MetricTypeExponentialHistogram:
+ temporality = m.ExponentialHistogram().AggregationTemporality()
+ case pmetric.MetricTypeHistogram:
+ temporality = m.Histogram().AggregationTemporality()
+ }
+ if temporality == pmetric.AggregationTemporalityDelta {
+ return true
+ }
+ }
+ }
+ }
+ return false
+}
+
+type otlpInstrumentedAppendable struct {
+ storage.AppendableV2
+
+ samplesAppendedWithoutMetadata prometheus.Counter
+ outOfOrderExemplars prometheus.Counter
+}
+
+// newOTLPInstrumentedAppendable instruments some OTLP metrics per append and
+// handles partial errors, so the caller does not need to.
+func newOTLPInstrumentedAppendable(reg prometheus.Registerer, app storage.AppendableV2) *otlpInstrumentedAppendable {
+ return &otlpInstrumentedAppendable{
+ AppendableV2: app,
+ samplesAppendedWithoutMetadata: promauto.With(reg).NewCounter(prometheus.CounterOpts{
+ Namespace: "prometheus",
+ Subsystem: "api",
+ Name: "otlp_appended_samples_without_metadata_total",
+ Help: "The total number of samples ingested from OTLP without corresponding metadata.",
+ }),
+ outOfOrderExemplars: promauto.With(reg).NewCounter(prometheus.CounterOpts{
+ Namespace: "prometheus",
+ Subsystem: "api",
+ Name: "otlp_out_of_order_exemplars_total",
+ Help: "The total number of received OTLP exemplars which were rejected because they were out of order.",
+ }),
+ }
+}
+
+func (a *otlpInstrumentedAppendable) AppenderV2(ctx context.Context) storage.AppenderV2 {
+ return &otlpInstrumentedAppender{
+ AppenderV2: a.AppendableV2.AppenderV2(ctx),
+
+ samplesAppendedWithoutMetadata: a.samplesAppendedWithoutMetadata,
+ outOfOrderExemplars: a.outOfOrderExemplars,
+ }
+}
+
+type otlpInstrumentedAppender struct {
+ storage.AppenderV2
+
+ samplesAppendedWithoutMetadata prometheus.Counter
+ outOfOrderExemplars prometheus.Counter
+}
+
+func (app *otlpInstrumentedAppender) Append(ref storage.SeriesRef, ls labels.Labels, st, t int64, v float64, h *histogram.Histogram, fh *histogram.FloatHistogram, opts storage.AOptions) (storage.SeriesRef, error) {
+ ref, err := app.AppenderV2.Append(ref, ls, st, t, v, h, fh, opts)
+ if err != nil {
+ var partialErr *storage.AppendPartialError
+ partialErr, hErr := partialErr.Handle(err)
+ if hErr != nil {
+ // Not a partial error, return err.
+ return 0, err
+ }
+ app.outOfOrderExemplars.Add(float64(len(partialErr.ExemplarErrors)))
+ // Hide the partial error as otlp converter does not handle it.
+ }
+ if opts.Metadata.IsEmpty() {
+ app.samplesAppendedWithoutMetadata.Inc()
+ }
+ return ref, nil
+}
diff --git a/storage/remote/write_otlp_handler_test.go b/storage/remote/write_otlp_handler_test.go
new file mode 100644
index 0000000000..be3482f440
--- /dev/null
+++ b/storage/remote/write_otlp_handler_test.go
@@ -0,0 +1,759 @@
+// 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 remote
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "log/slog"
+ "math/rand/v2"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "reflect"
+ "runtime"
+ "strconv"
+ "testing"
+ "time"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/prometheus/client_golang/prometheus"
+ "github.com/prometheus/client_golang/prometheus/testutil"
+ "github.com/prometheus/common/model"
+ "github.com/prometheus/otlptranslator"
+ "github.com/stretchr/testify/require"
+ "go.opentelemetry.io/collector/pdata/pcommon"
+ "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"
+ "github.com/prometheus/prometheus/model/metadata"
+ "github.com/prometheus/prometheus/model/timestamp"
+ "github.com/prometheus/prometheus/storage"
+ "github.com/prometheus/prometheus/util/teststorage"
+)
+
+type sample = teststorage.Sample
+
+func TestOTLPWriteHandler(t *testing.T) {
+ ts := time.Now()
+ st := ts.Add(-1 * time.Millisecond)
+
+ // Expected samples passed via OTLP request without details (labels for now) that
+ // depend on translation or type and unit labels options.
+ expectedSamplesWithoutLabelsFn := func() []sample {
+ return []sample{
+ {
+ M: metadata.Metadata{Type: model.MetricTypeCounter, Unit: "bytes", Help: "test-counter-description"},
+ V: 10.0, ST: timestamp.FromTime(st), T: timestamp.FromTime(ts), ES: []exemplar.Exemplar{
+ {
+ Labels: labels.FromStrings("span_id", "0001020304050607", "trace_id", "000102030405060708090a0b0c0d0e0f"),
+ Value: 10, Ts: timestamp.FromTime(ts), HasTs: true,
+ },
+ },
+ },
+ {
+ M: metadata.Metadata{Type: model.MetricTypeGauge, Unit: "bytes", Help: "test-gauge-description"},
+ V: 10.0, ST: timestamp.FromTime(st), T: timestamp.FromTime(ts),
+ },
+ {
+ M: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
+ V: 30.0, ST: timestamp.FromTime(st), T: timestamp.FromTime(ts),
+ },
+ {
+ M: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
+ V: 12.0, ST: timestamp.FromTime(st), T: timestamp.FromTime(ts),
+ },
+ {
+ M: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
+ V: 2.0, ST: timestamp.FromTime(st), T: timestamp.FromTime(ts),
+ },
+ {
+ M: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
+ V: 4.0, ST: timestamp.FromTime(st), T: timestamp.FromTime(ts),
+ },
+ {
+ M: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
+ V: 6.0, ST: timestamp.FromTime(st), T: timestamp.FromTime(ts),
+ },
+ {
+ M: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
+ V: 8.0, ST: timestamp.FromTime(st), T: timestamp.FromTime(ts),
+ },
+ {
+ M: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
+ V: 10.0, ST: timestamp.FromTime(st), T: timestamp.FromTime(ts),
+ },
+ {
+ M: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
+ V: 12.0, ST: timestamp.FromTime(st), T: timestamp.FromTime(ts),
+ },
+ {
+ M: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
+ V: 12.0, ST: timestamp.FromTime(st), T: timestamp.FromTime(ts),
+ },
+ {
+ M: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-exponential-histogram-description"},
+ H: &histogram.Histogram{
+ Count: 10,
+ Sum: 30.0,
+ Schema: 2,
+ ZeroThreshold: 1e-128,
+ ZeroCount: 2,
+ PositiveSpans: []histogram.Span{{Offset: 1, Length: 5}},
+ PositiveBuckets: []int64{2, 0, 0, 0, 0},
+ }, ST: timestamp.FromTime(st), T: timestamp.FromTime(ts),
+ },
+ {
+ M: metadata.Metadata{Type: model.MetricTypeGauge, Unit: "", Help: "Target metadata"}, V: 1, T: timestamp.FromTime(ts),
+ },
+ }
+ }
+
+ exportRequest := generateOTLPWriteRequest(ts, st)
+ for _, testCase := range []struct {
+ name string
+ otlpCfg config.OTLPConfig
+ typeAndUnitLabels bool
+ expectedLabelsAndMFs []sample
+ }{
+ {
+ name: "NoTranslation/NoTypeAndUnitLabels",
+ otlpCfg: config.OTLPConfig{
+ TranslationStrategy: otlptranslator.NoTranslation,
+ },
+ expectedLabelsAndMFs: []sample{
+ {MF: "test.counter", L: labels.FromStrings(model.MetricNameLabel, "test.counter", "foo.bar", "baz", "instance", "test-instance", "job", "test-service")},
+ {MF: "test.gauge", L: labels.FromStrings(model.MetricNameLabel, "test.gauge", "foo.bar", "baz", "instance", "test-instance", "job", "test-service")},
+ {MF: "test.histogram", L: labels.FromStrings(model.MetricNameLabel, "test.histogram_sum", "foo.bar", "baz", "instance", "test-instance", "job", "test-service")},
+ {MF: "test.histogram", L: labels.FromStrings(model.MetricNameLabel, "test.histogram_count", "foo.bar", "baz", "instance", "test-instance", "job", "test-service")},
+ {MF: "test.histogram", L: labels.FromStrings(model.MetricNameLabel, "test.histogram_bucket", "foo.bar", "baz", "instance", "test-instance", "job", "test-service", "le", "0")},
+ {MF: "test.histogram", L: labels.FromStrings(model.MetricNameLabel, "test.histogram_bucket", "foo.bar", "baz", "instance", "test-instance", "job", "test-service", "le", "1")},
+ {MF: "test.histogram", L: labels.FromStrings(model.MetricNameLabel, "test.histogram_bucket", "foo.bar", "baz", "instance", "test-instance", "job", "test-service", "le", "2")},
+ {MF: "test.histogram", L: labels.FromStrings(model.MetricNameLabel, "test.histogram_bucket", "foo.bar", "baz", "instance", "test-instance", "job", "test-service", "le", "3")},
+ {MF: "test.histogram", L: labels.FromStrings(model.MetricNameLabel, "test.histogram_bucket", "foo.bar", "baz", "instance", "test-instance", "job", "test-service", "le", "4")},
+ {MF: "test.histogram", L: labels.FromStrings(model.MetricNameLabel, "test.histogram_bucket", "foo.bar", "baz", "instance", "test-instance", "job", "test-service", "le", "5")},
+ {MF: "test.histogram", L: labels.FromStrings(model.MetricNameLabel, "test.histogram_bucket", "foo.bar", "baz", "instance", "test-instance", "job", "test-service", "le", "+Inf")},
+ {MF: "test.exponential.histogram", L: labels.FromStrings(model.MetricNameLabel, "test.exponential.histogram", "foo.bar", "baz", "instance", "test-instance", "job", "test-service")},
+ {MF: "target_info", L: labels.FromStrings(model.MetricNameLabel, "target_info", "host.name", "test-host", "instance", "test-instance", "job", "test-service")},
+ },
+ },
+ {
+ name: "NoTranslation/WithTypeAndUnitLabels",
+ otlpCfg: config.OTLPConfig{
+ TranslationStrategy: otlptranslator.NoTranslation,
+ },
+ typeAndUnitLabels: true,
+ expectedLabelsAndMFs: []sample{
+ {MF: "test.counter", L: labels.FromStrings(model.MetricNameLabel, "test.counter", "__type__", "counter", "__unit__", "bytes", "foo.bar", "baz", "instance", "test-instance", "job", "test-service")},
+ {MF: "test.gauge", L: labels.FromStrings(model.MetricNameLabel, "test.gauge", "__type__", "gauge", "__unit__", "bytes", "foo.bar", "baz", "instance", "test-instance", "job", "test-service")},
+ {MF: "test.histogram", L: labels.FromStrings(model.MetricNameLabel, "test.histogram_sum", "__type__", "histogram", "__unit__", "bytes", "foo.bar", "baz", "instance", "test-instance", "job", "test-service")},
+ {MF: "test.histogram", L: labels.FromStrings(model.MetricNameLabel, "test.histogram_count", "__type__", "histogram", "__unit__", "bytes", "foo.bar", "baz", "instance", "test-instance", "job", "test-service")},
+ {MF: "test.histogram", L: labels.FromStrings(model.MetricNameLabel, "test.histogram_bucket", "__type__", "histogram", "__unit__", "bytes", "foo.bar", "baz", "instance", "test-instance", "job", "test-service", "le", "0")},
+ {MF: "test.histogram", L: labels.FromStrings(model.MetricNameLabel, "test.histogram_bucket", "__type__", "histogram", "__unit__", "bytes", "foo.bar", "baz", "instance", "test-instance", "job", "test-service", "le", "1")},
+ {MF: "test.histogram", L: labels.FromStrings(model.MetricNameLabel, "test.histogram_bucket", "__type__", "histogram", "__unit__", "bytes", "foo.bar", "baz", "instance", "test-instance", "job", "test-service", "le", "2")},
+ {MF: "test.histogram", L: labels.FromStrings(model.MetricNameLabel, "test.histogram_bucket", "__type__", "histogram", "__unit__", "bytes", "foo.bar", "baz", "instance", "test-instance", "job", "test-service", "le", "3")},
+ {MF: "test.histogram", L: labels.FromStrings(model.MetricNameLabel, "test.histogram_bucket", "__type__", "histogram", "__unit__", "bytes", "foo.bar", "baz", "instance", "test-instance", "job", "test-service", "le", "4")},
+ {MF: "test.histogram", L: labels.FromStrings(model.MetricNameLabel, "test.histogram_bucket", "__type__", "histogram", "__unit__", "bytes", "foo.bar", "baz", "instance", "test-instance", "job", "test-service", "le", "5")},
+ {MF: "test.histogram", L: labels.FromStrings(model.MetricNameLabel, "test.histogram_bucket", "__type__", "histogram", "__unit__", "bytes", "foo.bar", "baz", "instance", "test-instance", "job", "test-service", "le", "+Inf")},
+ {MF: "test.exponential.histogram", L: labels.FromStrings(model.MetricNameLabel, "test.exponential.histogram", "__type__", "histogram", "__unit__", "bytes", "foo.bar", "baz", "instance", "test-instance", "job", "test-service")},
+ {MF: "target_info", L: labels.FromStrings(model.MetricNameLabel, "target_info", "host.name", "test-host", "instance", "test-instance", "job", "test-service")},
+ },
+ },
+ // For the following cases, skip type and unit cases, it has nothing todo with translation.
+ {
+ name: "UnderscoreEscapingWithSuffixes",
+ otlpCfg: config.OTLPConfig{
+ TranslationStrategy: otlptranslator.UnderscoreEscapingWithSuffixes,
+ },
+ expectedLabelsAndMFs: []sample{
+ {MF: "test_counter_bytes_total", L: labels.FromStrings(model.MetricNameLabel, "test_counter_bytes_total", "foo_bar", "baz", "instance", "test-instance", "job", "test-service")},
+ {MF: "test_gauge_bytes", L: labels.FromStrings(model.MetricNameLabel, "test_gauge_bytes", "foo_bar", "baz", "instance", "test-instance", "job", "test-service")},
+ {MF: "test_histogram_bytes", L: labels.FromStrings(model.MetricNameLabel, "test_histogram_bytes_sum", "foo_bar", "baz", "instance", "test-instance", "job", "test-service")},
+ {MF: "test_histogram_bytes", L: labels.FromStrings(model.MetricNameLabel, "test_histogram_bytes_count", "foo_bar", "baz", "instance", "test-instance", "job", "test-service")},
+ {MF: "test_histogram_bytes", L: labels.FromStrings(model.MetricNameLabel, "test_histogram_bytes_bucket", "foo_bar", "baz", "instance", "test-instance", "job", "test-service", "le", "0")},
+ {MF: "test_histogram_bytes", L: labels.FromStrings(model.MetricNameLabel, "test_histogram_bytes_bucket", "foo_bar", "baz", "instance", "test-instance", "job", "test-service", "le", "1")},
+ {MF: "test_histogram_bytes", L: labels.FromStrings(model.MetricNameLabel, "test_histogram_bytes_bucket", "foo_bar", "baz", "instance", "test-instance", "job", "test-service", "le", "2")},
+ {MF: "test_histogram_bytes", L: labels.FromStrings(model.MetricNameLabel, "test_histogram_bytes_bucket", "foo_bar", "baz", "instance", "test-instance", "job", "test-service", "le", "3")},
+ {MF: "test_histogram_bytes", L: labels.FromStrings(model.MetricNameLabel, "test_histogram_bytes_bucket", "foo_bar", "baz", "instance", "test-instance", "job", "test-service", "le", "4")},
+ {MF: "test_histogram_bytes", L: labels.FromStrings(model.MetricNameLabel, "test_histogram_bytes_bucket", "foo_bar", "baz", "instance", "test-instance", "job", "test-service", "le", "5")},
+ {MF: "test_histogram_bytes", L: labels.FromStrings(model.MetricNameLabel, "test_histogram_bytes_bucket", "foo_bar", "baz", "instance", "test-instance", "job", "test-service", "le", "+Inf")},
+ {MF: "test_exponential_histogram_bytes", L: labels.FromStrings(model.MetricNameLabel, "test_exponential_histogram_bytes", "foo_bar", "baz", "instance", "test-instance", "job", "test-service")},
+ {MF: "target_info", L: labels.FromStrings(model.MetricNameLabel, "target_info", "host_name", "test-host", "instance", "test-instance", "job", "test-service")},
+ },
+ },
+ {
+ name: "UnderscoreEscapingWithoutSuffixes",
+ otlpCfg: config.OTLPConfig{
+ TranslationStrategy: otlptranslator.UnderscoreEscapingWithoutSuffixes,
+ },
+ expectedLabelsAndMFs: []sample{
+ {MF: "test_counter", L: labels.FromStrings(model.MetricNameLabel, "test_counter", "foo_bar", "baz", "instance", "test-instance", "job", "test-service")},
+ {MF: "test_gauge", L: labels.FromStrings(model.MetricNameLabel, "test_gauge", "foo_bar", "baz", "instance", "test-instance", "job", "test-service")},
+ {MF: "test_histogram", L: labels.FromStrings(model.MetricNameLabel, "test_histogram_sum", "foo_bar", "baz", "instance", "test-instance", "job", "test-service")},
+ {MF: "test_histogram", L: labels.FromStrings(model.MetricNameLabel, "test_histogram_count", "foo_bar", "baz", "instance", "test-instance", "job", "test-service")},
+ {MF: "test_histogram", L: labels.FromStrings(model.MetricNameLabel, "test_histogram_bucket", "foo_bar", "baz", "instance", "test-instance", "job", "test-service", "le", "0")},
+ {MF: "test_histogram", L: labels.FromStrings(model.MetricNameLabel, "test_histogram_bucket", "foo_bar", "baz", "instance", "test-instance", "job", "test-service", "le", "1")},
+ {MF: "test_histogram", L: labels.FromStrings(model.MetricNameLabel, "test_histogram_bucket", "foo_bar", "baz", "instance", "test-instance", "job", "test-service", "le", "2")},
+ {MF: "test_histogram", L: labels.FromStrings(model.MetricNameLabel, "test_histogram_bucket", "foo_bar", "baz", "instance", "test-instance", "job", "test-service", "le", "3")},
+ {MF: "test_histogram", L: labels.FromStrings(model.MetricNameLabel, "test_histogram_bucket", "foo_bar", "baz", "instance", "test-instance", "job", "test-service", "le", "4")},
+ {MF: "test_histogram", L: labels.FromStrings(model.MetricNameLabel, "test_histogram_bucket", "foo_bar", "baz", "instance", "test-instance", "job", "test-service", "le", "5")},
+ {MF: "test_histogram", L: labels.FromStrings(model.MetricNameLabel, "test_histogram_bucket", "foo_bar", "baz", "instance", "test-instance", "job", "test-service", "le", "+Inf")},
+ {MF: "test_exponential_histogram", L: labels.FromStrings(model.MetricNameLabel, "test_exponential_histogram", "foo_bar", "baz", "instance", "test-instance", "job", "test-service")},
+ {MF: "target_info", L: labels.FromStrings(model.MetricNameLabel, "target_info", "host_name", "test-host", "instance", "test-instance", "job", "test-service")},
+ },
+ },
+ {
+ name: "NoUTF8EscapingWithSuffixes",
+ otlpCfg: config.OTLPConfig{
+ TranslationStrategy: otlptranslator.NoUTF8EscapingWithSuffixes,
+ },
+ expectedLabelsAndMFs: []sample{
+ // TODO: Counter MF name looks likea bug. Uncovered in unrelated refactor. fix it.
+ {MF: "test.counter_bytes_total", L: labels.FromStrings(model.MetricNameLabel, "test.counter_bytes_total", "foo.bar", "baz", "instance", "test-instance", "job", "test-service")},
+ {MF: "test.gauge_bytes", L: labels.FromStrings(model.MetricNameLabel, "test.gauge_bytes", "foo.bar", "baz", "instance", "test-instance", "job", "test-service")},
+ {MF: "test.histogram_bytes", L: labels.FromStrings(model.MetricNameLabel, "test.histogram_bytes_sum", "foo.bar", "baz", "instance", "test-instance", "job", "test-service")},
+ {MF: "test.histogram_bytes", L: labels.FromStrings(model.MetricNameLabel, "test.histogram_bytes_count", "foo.bar", "baz", "instance", "test-instance", "job", "test-service")},
+ {MF: "test.histogram_bytes", L: labels.FromStrings(model.MetricNameLabel, "test.histogram_bytes_bucket", "foo.bar", "baz", "instance", "test-instance", "job", "test-service", "le", "0")},
+ {MF: "test.histogram_bytes", L: labels.FromStrings(model.MetricNameLabel, "test.histogram_bytes_bucket", "foo.bar", "baz", "instance", "test-instance", "job", "test-service", "le", "1")},
+ {MF: "test.histogram_bytes", L: labels.FromStrings(model.MetricNameLabel, "test.histogram_bytes_bucket", "foo.bar", "baz", "instance", "test-instance", "job", "test-service", "le", "2")},
+ {MF: "test.histogram_bytes", L: labels.FromStrings(model.MetricNameLabel, "test.histogram_bytes_bucket", "foo.bar", "baz", "instance", "test-instance", "job", "test-service", "le", "3")},
+ {MF: "test.histogram_bytes", L: labels.FromStrings(model.MetricNameLabel, "test.histogram_bytes_bucket", "foo.bar", "baz", "instance", "test-instance", "job", "test-service", "le", "4")},
+ {MF: "test.histogram_bytes", L: labels.FromStrings(model.MetricNameLabel, "test.histogram_bytes_bucket", "foo.bar", "baz", "instance", "test-instance", "job", "test-service", "le", "5")},
+ {MF: "test.histogram_bytes", L: labels.FromStrings(model.MetricNameLabel, "test.histogram_bytes_bucket", "foo.bar", "baz", "instance", "test-instance", "job", "test-service", "le", "+Inf")},
+ {MF: "test.exponential.histogram_bytes", L: labels.FromStrings(model.MetricNameLabel, "test.exponential.histogram_bytes", "foo.bar", "baz", "instance", "test-instance", "job", "test-service")},
+ {MF: "target_info", L: labels.FromStrings(model.MetricNameLabel, "target_info", "host.name", "test-host", "instance", "test-instance", "job", "test-service")},
+ },
+ },
+ } {
+ t.Run(testCase.name, func(t *testing.T) {
+ otlpOpts := OTLPOptions{
+ EnableTypeAndUnitLabels: testCase.typeAndUnitLabels,
+ }
+ appendable := handleOTLP(t, exportRequest, testCase.otlpCfg, otlpOpts)
+
+ // Compile final expected samples.
+ expectedSamples := expectedSamplesWithoutLabelsFn()
+ for i, s := range testCase.expectedLabelsAndMFs {
+ expectedSamples[i].L = s.L
+ expectedSamples[i].MF = s.MF
+ }
+ teststorage.RequireEqual(t, expectedSamples, appendable.ResultSamples())
+ })
+ }
+}
+
+func handleOTLP(t *testing.T, exportRequest pmetricotlp.ExportRequest, otlpCfg config.OTLPConfig, otlpOpts OTLPOptions) *teststorage.Appendable {
+ t.Helper()
+
+ buf, err := exportRequest.MarshalProto()
+ require.NoError(t, err)
+
+ req, err := http.NewRequest("", "", bytes.NewReader(buf))
+ require.NoError(t, err)
+ req.Header.Set("Content-Type", "application/x-protobuf")
+
+ log := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelWarn}))
+ appendable := teststorage.NewAppendable()
+ handler := NewOTLPWriteHandler(log, nil, appendable, func() config.Config {
+ return config.Config{
+ OTLPConfig: otlpCfg,
+ }
+ }, otlpOpts)
+ recorder := httptest.NewRecorder()
+ handler.ServeHTTP(recorder, req)
+
+ resp := recorder.Result()
+ require.Equal(t, http.StatusOK, resp.StatusCode)
+
+ return appendable
+}
+
+func generateOTLPWriteRequest(timestamp, startTime time.Time) pmetricotlp.ExportRequest {
+ d := pmetric.NewMetrics()
+
+ // Generate One Counter, One Gauge, One Histogram, One Exponential-Histogram
+ // with resource attributes: service.name="test-service", service.instance.id="test-instance", host.name="test-host"
+ // with metric attribute: foo.bar="baz"
+
+ resourceMetric := d.ResourceMetrics().AppendEmpty()
+ resourceMetric.Resource().Attributes().PutStr("service.name", "test-service")
+ resourceMetric.Resource().Attributes().PutStr("service.instance.id", "test-instance")
+ resourceMetric.Resource().Attributes().PutStr("host.name", "test-host")
+
+ scopeMetric := resourceMetric.ScopeMetrics().AppendEmpty()
+
+ // Generate One Counter
+ counterMetric := scopeMetric.Metrics().AppendEmpty()
+ counterMetric.SetName("test.counter")
+ counterMetric.SetDescription("test-counter-description")
+ counterMetric.SetUnit("By")
+ counterMetric.SetEmptySum()
+ counterMetric.Sum().SetAggregationTemporality(pmetric.AggregationTemporalityCumulative)
+ counterMetric.Sum().SetIsMonotonic(true)
+
+ counterDataPoint := counterMetric.Sum().DataPoints().AppendEmpty()
+ counterDataPoint.SetTimestamp(pcommon.NewTimestampFromTime(timestamp))
+ counterDataPoint.SetStartTimestamp(pcommon.NewTimestampFromTime(startTime))
+ counterDataPoint.SetDoubleValue(10.0)
+ counterDataPoint.Attributes().PutStr("foo.bar", "baz")
+
+ counterExemplar := counterDataPoint.Exemplars().AppendEmpty()
+
+ counterExemplar.SetTimestamp(pcommon.NewTimestampFromTime(timestamp))
+ counterExemplar.SetDoubleValue(10.0)
+ counterExemplar.SetSpanID(pcommon.SpanID{0, 1, 2, 3, 4, 5, 6, 7})
+ counterExemplar.SetTraceID(pcommon.TraceID{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15})
+
+ // Generate One Gauge
+ gaugeMetric := scopeMetric.Metrics().AppendEmpty()
+ gaugeMetric.SetName("test.gauge")
+ gaugeMetric.SetDescription("test-gauge-description")
+ gaugeMetric.SetUnit("By")
+ gaugeMetric.SetEmptyGauge()
+
+ gaugeDataPoint := gaugeMetric.Gauge().DataPoints().AppendEmpty()
+ gaugeDataPoint.SetTimestamp(pcommon.NewTimestampFromTime(timestamp))
+ gaugeDataPoint.SetStartTimestamp(pcommon.NewTimestampFromTime(startTime))
+ gaugeDataPoint.SetDoubleValue(10.0)
+ gaugeDataPoint.Attributes().PutStr("foo.bar", "baz")
+
+ // Generate One Histogram
+ histogramMetric := scopeMetric.Metrics().AppendEmpty()
+ histogramMetric.SetName("test.histogram")
+ histogramMetric.SetDescription("test-histogram-description")
+ histogramMetric.SetUnit("By")
+ histogramMetric.SetEmptyHistogram()
+ histogramMetric.Histogram().SetAggregationTemporality(pmetric.AggregationTemporalityCumulative)
+
+ histogramDataPoint := histogramMetric.Histogram().DataPoints().AppendEmpty()
+ histogramDataPoint.SetTimestamp(pcommon.NewTimestampFromTime(timestamp))
+ histogramDataPoint.SetStartTimestamp(pcommon.NewTimestampFromTime(startTime))
+ histogramDataPoint.ExplicitBounds().FromRaw([]float64{0.0, 1.0, 2.0, 3.0, 4.0, 5.0})
+ histogramDataPoint.BucketCounts().FromRaw([]uint64{2, 2, 2, 2, 2, 2})
+ histogramDataPoint.SetCount(12)
+ histogramDataPoint.SetSum(30.0)
+ histogramDataPoint.Attributes().PutStr("foo.bar", "baz")
+
+ // Generate One Exponential-Histogram
+ exponentialHistogramMetric := scopeMetric.Metrics().AppendEmpty()
+ exponentialHistogramMetric.SetName("test.exponential.histogram")
+ exponentialHistogramMetric.SetDescription("test-exponential-histogram-description")
+ exponentialHistogramMetric.SetUnit("By")
+ exponentialHistogramMetric.SetEmptyExponentialHistogram()
+ exponentialHistogramMetric.ExponentialHistogram().SetAggregationTemporality(pmetric.AggregationTemporalityCumulative)
+
+ exponentialHistogramDataPoint := exponentialHistogramMetric.ExponentialHistogram().DataPoints().AppendEmpty()
+ exponentialHistogramDataPoint.SetTimestamp(pcommon.NewTimestampFromTime(timestamp))
+ exponentialHistogramDataPoint.SetStartTimestamp(pcommon.NewTimestampFromTime(startTime))
+ exponentialHistogramDataPoint.SetScale(2.0)
+ exponentialHistogramDataPoint.Positive().BucketCounts().FromRaw([]uint64{2, 2, 2, 2, 2})
+ exponentialHistogramDataPoint.SetZeroCount(2)
+ exponentialHistogramDataPoint.SetCount(10)
+ exponentialHistogramDataPoint.SetSum(30.0)
+ exponentialHistogramDataPoint.Attributes().PutStr("foo.bar", "baz")
+
+ return pmetricotlp.NewExportRequestFromMetrics(d)
+}
+
+func TestOTLPDelta(t *testing.T) {
+ log := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelWarn}))
+ appendable := teststorage.NewAppendable()
+ cfg := func() config.Config {
+ return config.Config{OTLPConfig: config.DefaultOTLPConfig}
+ }
+ handler := NewOTLPWriteHandler(log, nil, appendable, cfg, OTLPOptions{ConvertDelta: true})
+
+ md := pmetric.NewMetrics()
+ ms := md.ResourceMetrics().AppendEmpty().ScopeMetrics().AppendEmpty().Metrics()
+
+ m := ms.AppendEmpty()
+ m.SetName("some.delta.total")
+
+ sum := m.SetEmptySum()
+ sum.SetAggregationTemporality(pmetric.AggregationTemporalityDelta)
+
+ ts := time.Date(2000, 1, 2, 3, 4, 0, 0, time.UTC)
+ for i := range 3 {
+ dp := sum.DataPoints().AppendEmpty()
+ dp.SetIntValue(int64(i))
+ dp.SetTimestamp(pcommon.NewTimestampFromTime(ts.Add(time.Duration(i) * time.Second)))
+ }
+
+ proto, err := pmetricotlp.NewExportRequestFromMetrics(md).MarshalProto()
+ require.NoError(t, err)
+
+ req, err := http.NewRequest("", "", bytes.NewReader(proto))
+ require.NoError(t, err)
+ req.Header.Set("Content-Type", "application/x-protobuf")
+
+ rec := httptest.NewRecorder()
+ handler.ServeHTTP(rec, req)
+ require.Equal(t, http.StatusOK, rec.Result().StatusCode)
+
+ ls := labels.FromStrings("__name__", "some_delta_total")
+ milli := func(sec int) int64 {
+ return time.Date(2000, 1, 2, 3, 4, sec, 0, time.UTC).UnixMilli()
+ }
+
+ want := []sample{
+ {MF: "some_delta_total", M: metadata.Metadata{Type: model.MetricTypeGauge}, T: milli(0), L: ls, V: 0}, // +0
+ {MF: "some_delta_total", M: metadata.Metadata{Type: model.MetricTypeGauge}, T: milli(1), L: ls, V: 1}, // +1
+ {MF: "some_delta_total", M: metadata.Metadata{Type: model.MetricTypeGauge}, T: milli(2), L: ls, V: 3}, // +2
+ }
+ if diff := cmp.Diff(want, appendable.ResultSamples(), cmp.Exporter(func(reflect.Type) bool { return true })); diff != "" {
+ t.Fatal(diff)
+ }
+}
+
+func BenchmarkOTLP(b *testing.B) {
+ start := time.Date(2000, 1, 2, 3, 4, 5, 0, time.UTC)
+
+ type Type struct {
+ name string
+ data func(mode pmetric.AggregationTemporality, dpc, epoch int) []pmetric.Metric
+ }
+ types := []Type{{
+ name: "sum",
+ data: func() func(mode pmetric.AggregationTemporality, dpc, epoch int) []pmetric.Metric {
+ cumul := make(map[int]float64)
+ return func(mode pmetric.AggregationTemporality, dpc, epoch int) []pmetric.Metric {
+ m := pmetric.NewMetric()
+ sum := m.SetEmptySum()
+ sum.SetAggregationTemporality(mode)
+ dps := sum.DataPoints()
+ for id := range dpc {
+ dp := dps.AppendEmpty()
+ dp.SetStartTimestamp(pcommon.NewTimestampFromTime(start))
+ dp.SetTimestamp(pcommon.NewTimestampFromTime(start.Add(time.Duration(epoch) * time.Minute)))
+ dp.Attributes().PutStr("id", strconv.Itoa(id))
+ v := float64(rand.IntN(100)) / 10
+ switch mode {
+ case pmetric.AggregationTemporalityDelta:
+ dp.SetDoubleValue(v)
+ case pmetric.AggregationTemporalityCumulative:
+ cumul[id] += v
+ dp.SetDoubleValue(cumul[id])
+ }
+ }
+ return []pmetric.Metric{m}
+ }
+ }(),
+ }, {
+ name: "histogram",
+ data: func() func(mode pmetric.AggregationTemporality, dpc, epoch int) []pmetric.Metric {
+ bounds := [4]float64{1, 10, 100, 1000}
+ type state struct {
+ counts [4]uint64
+ count uint64
+ sum float64
+ }
+ var cumul []state
+ return func(mode pmetric.AggregationTemporality, dpc, epoch int) []pmetric.Metric {
+ if cumul == nil {
+ cumul = make([]state, dpc)
+ }
+ m := pmetric.NewMetric()
+ hist := m.SetEmptyHistogram()
+ hist.SetAggregationTemporality(mode)
+ dps := hist.DataPoints()
+ for id := range dpc {
+ dp := dps.AppendEmpty()
+ dp.SetStartTimestamp(pcommon.NewTimestampFromTime(start))
+ dp.SetTimestamp(pcommon.NewTimestampFromTime(start.Add(time.Duration(epoch) * time.Minute)))
+ dp.Attributes().PutStr("id", strconv.Itoa(id))
+ dp.ExplicitBounds().FromRaw(bounds[:])
+
+ var obs *state
+ switch mode {
+ case pmetric.AggregationTemporalityDelta:
+ obs = new(state)
+ case pmetric.AggregationTemporalityCumulative:
+ obs = &cumul[id]
+ }
+
+ for i := range obs.counts {
+ v := uint64(rand.IntN(10))
+ obs.counts[i] += v
+ obs.count++
+ obs.sum += float64(v)
+ }
+
+ dp.SetCount(obs.count)
+ dp.SetSum(obs.sum)
+ dp.BucketCounts().FromRaw(obs.counts[:])
+ }
+ return []pmetric.Metric{m}
+ }
+ }(),
+ }, {
+ name: "exponential",
+ data: func() func(mode pmetric.AggregationTemporality, dpc, epoch int) []pmetric.Metric {
+ type state struct {
+ counts [4]uint64
+ count uint64
+ sum float64
+ }
+ var cumul []state
+ return func(mode pmetric.AggregationTemporality, dpc, epoch int) []pmetric.Metric {
+ if cumul == nil {
+ cumul = make([]state, dpc)
+ }
+ m := pmetric.NewMetric()
+ ex := m.SetEmptyExponentialHistogram()
+ ex.SetAggregationTemporality(mode)
+ dps := ex.DataPoints()
+ for id := range dpc {
+ dp := dps.AppendEmpty()
+ dp.SetStartTimestamp(pcommon.NewTimestampFromTime(start))
+ dp.SetTimestamp(pcommon.NewTimestampFromTime(start.Add(time.Duration(epoch) * time.Minute)))
+ dp.Attributes().PutStr("id", strconv.Itoa(id))
+ dp.SetScale(2)
+
+ var obs *state
+ switch mode {
+ case pmetric.AggregationTemporalityDelta:
+ obs = new(state)
+ case pmetric.AggregationTemporalityCumulative:
+ obs = &cumul[id]
+ }
+
+ for i := range obs.counts {
+ v := uint64(rand.IntN(10))
+ obs.counts[i] += v
+ obs.count++
+ obs.sum += float64(v)
+ }
+
+ dp.Positive().BucketCounts().FromRaw(obs.counts[:])
+ dp.SetCount(obs.count)
+ dp.SetSum(obs.sum)
+ }
+
+ return []pmetric.Metric{m}
+ }
+ }(),
+ }}
+
+ modes := []struct {
+ name string
+ data func(func(pmetric.AggregationTemporality, int, int) []pmetric.Metric, int) []pmetric.Metric
+ }{{
+ name: "cumulative",
+ data: func(data func(pmetric.AggregationTemporality, int, int) []pmetric.Metric, epoch int) []pmetric.Metric {
+ return data(pmetric.AggregationTemporalityCumulative, 10, epoch)
+ },
+ }, {
+ name: "delta",
+ data: func(data func(pmetric.AggregationTemporality, int, int) []pmetric.Metric, epoch int) []pmetric.Metric {
+ return data(pmetric.AggregationTemporalityDelta, 10, epoch)
+ },
+ }, {
+ name: "mixed",
+ data: func(data func(pmetric.AggregationTemporality, int, int) []pmetric.Metric, epoch int) []pmetric.Metric {
+ cumul := data(pmetric.AggregationTemporalityCumulative, 5, epoch)
+ delta := data(pmetric.AggregationTemporalityDelta, 5, epoch)
+ out := append(cumul, delta...)
+ rand.Shuffle(len(out), func(i, j int) { out[i], out[j] = out[j], out[i] })
+ return out
+ },
+ }}
+
+ configs := []struct {
+ name string
+ opts OTLPOptions
+ }{
+ {name: "default"},
+ {name: "convert", opts: OTLPOptions{ConvertDelta: true}},
+ }
+
+ Workers := runtime.GOMAXPROCS(0)
+ for _, cs := range types {
+ for _, mode := range modes {
+ for _, cfg := range configs {
+ b.Run(fmt.Sprintf("type=%s/temporality=%s/cfg=%s", cs.name, mode.name, cfg.name), func(b *testing.B) {
+ if !cfg.opts.ConvertDelta && (mode.name == "delta" || mode.name == "mixed") {
+ b.Skip("not possible")
+ }
+
+ var total int
+
+ // reqs is a [b.N]*http.Request, divided across the workers.
+ // deltatocumulative requires timestamps to be strictly in
+ // order on a per-series basis. to ensure this, each reqs[k]
+ // contains samples of differently named series, sorted
+ // strictly in time order
+ reqs := make([][]*http.Request, Workers)
+ for n := range b.N {
+ k := n % Workers
+
+ md := pmetric.NewMetrics()
+ ms := md.ResourceMetrics().AppendEmpty().
+ ScopeMetrics().AppendEmpty().
+ Metrics()
+
+ for i, m := range mode.data(cs.data, n) {
+ m.SetName(fmt.Sprintf("benchmark_%d_%d", k, i))
+ m.MoveTo(ms.AppendEmpty())
+ }
+
+ total += sampleCount(md)
+
+ ex := pmetricotlp.NewExportRequestFromMetrics(md)
+ data, err := ex.MarshalProto()
+ require.NoError(b, err)
+
+ req, err := http.NewRequest("", "", bytes.NewReader(data))
+ require.NoError(b, err)
+ req.Header.Set("Content-Type", "application/x-protobuf")
+
+ reqs[k] = append(reqs[k], req)
+ }
+
+ log := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelWarn}))
+
+ appendable := teststorage.NewAppendable()
+ cfgfn := func() config.Config {
+ return config.Config{OTLPConfig: config.DefaultOTLPConfig}
+ }
+ handler := NewOTLPWriteHandler(log, nil, appendable, cfgfn, cfg.opts)
+
+ fail := make(chan struct{})
+ done := make(chan struct{})
+
+ b.ResetTimer()
+ b.ReportAllocs()
+
+ // we use multiple workers to mimic a real-world scenario
+ // where multiple OTel collectors are sending their
+ // time-series in parallel.
+ // this is necessary to exercise potential lock-contention
+ // in this benchmark
+ for k := range Workers {
+ go func() {
+ rec := httptest.NewRecorder()
+ for _, req := range reqs[k] {
+ handler.ServeHTTP(rec, req)
+ if rec.Result().StatusCode != http.StatusOK {
+ fail <- struct{}{}
+ return
+ }
+ }
+ done <- struct{}{}
+ }()
+ }
+
+ for range Workers {
+ select {
+ case <-fail:
+ b.FailNow()
+ case <-done:
+ }
+ }
+
+ require.Len(b, appendable.ResultSamples(), total)
+ })
+ }
+ }
+ }
+}
+
+func sampleCount(md pmetric.Metrics) int {
+ var total int
+ rms := md.ResourceMetrics()
+ for i := range rms.Len() {
+ sms := rms.At(i).ScopeMetrics()
+ for i := range sms.Len() {
+ ms := sms.At(i).Metrics()
+ for i := range ms.Len() {
+ m := ms.At(i)
+ switch m.Type() {
+ case pmetric.MetricTypeSum:
+ total += m.Sum().DataPoints().Len()
+ case pmetric.MetricTypeGauge:
+ total += m.Gauge().DataPoints().Len()
+ case pmetric.MetricTypeHistogram:
+ dps := m.Histogram().DataPoints()
+ for i := range dps.Len() {
+ total += dps.At(i).BucketCounts().Len()
+ total++ // le=+Inf series
+ total++ // _sum series
+ total++ // _count series
+ }
+ case pmetric.MetricTypeExponentialHistogram:
+ total += m.ExponentialHistogram().DataPoints().Len()
+ case pmetric.MetricTypeSummary:
+ total += m.Summary().DataPoints().Len()
+ }
+ }
+ }
+ }
+ return total
+}
+
+func TestOTLPInstrumentedAppendable(t *testing.T) {
+ t.Run("no problems", func(t *testing.T) {
+ appTest := teststorage.NewAppendable()
+ oa := newOTLPInstrumentedAppendable(prometheus.NewRegistry(), appTest)
+
+ require.Equal(t, 0.0, testutil.ToFloat64(oa.outOfOrderExemplars))
+ require.Equal(t, 0.0, testutil.ToFloat64(oa.samplesAppendedWithoutMetadata))
+
+ app := oa.AppenderV2(t.Context())
+ _, err := app.Append(0, labels.EmptyLabels(), -1, 1, 2, nil, nil, storage.AOptions{Metadata: metadata.Metadata{Help: "yo"}})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ require.Len(t, appTest.ResultSamples(), 1)
+
+ require.Equal(t, 0.0, testutil.ToFloat64(oa.outOfOrderExemplars))
+ require.Equal(t, 0.0, testutil.ToFloat64(oa.samplesAppendedWithoutMetadata))
+ })
+ t.Run("without metadata", func(t *testing.T) {
+ appTest := teststorage.NewAppendable()
+ oa := newOTLPInstrumentedAppendable(prometheus.NewRegistry(), appTest)
+
+ require.Equal(t, 0.0, testutil.ToFloat64(oa.outOfOrderExemplars))
+ require.Equal(t, 0.0, testutil.ToFloat64(oa.samplesAppendedWithoutMetadata))
+
+ app := oa.AppenderV2(t.Context())
+ _, err := app.Append(0, labels.EmptyLabels(), -1, 1, 2, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ require.Len(t, appTest.ResultSamples(), 1)
+
+ require.Equal(t, 0.0, testutil.ToFloat64(oa.outOfOrderExemplars))
+ require.Equal(t, 1.0, testutil.ToFloat64(oa.samplesAppendedWithoutMetadata))
+ })
+ t.Run("without metadata; 2 exemplar OOO errors", func(t *testing.T) {
+ appTest := teststorage.NewAppendable().WithErrs(nil, errors.New("exemplar error"), nil)
+ oa := newOTLPInstrumentedAppendable(prometheus.NewRegistry(), appTest)
+
+ require.Equal(t, 0.0, testutil.ToFloat64(oa.outOfOrderExemplars))
+ require.Equal(t, 0.0, testutil.ToFloat64(oa.samplesAppendedWithoutMetadata))
+
+ app := oa.AppenderV2(t.Context())
+ _, err := app.Append(0, labels.EmptyLabels(), -1, 1, 2, nil, nil, storage.AOptions{Exemplars: []exemplar.Exemplar{{}, {}}})
+ // Partial errors should be handled in the middleware, OTLP converter does not handle it.
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ require.Len(t, appTest.ResultSamples(), 1)
+
+ require.Equal(t, 2.0, testutil.ToFloat64(oa.outOfOrderExemplars))
+ require.Equal(t, 1.0, testutil.ToFloat64(oa.samplesAppendedWithoutMetadata))
+ })
+}
diff --git a/storage/remote/write_test.go b/storage/remote/write_test.go
index 6103a7f262..1b1b86ff1e 100644
--- a/storage/remote/write_test.go
+++ b/storage/remote/write_test.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
@@ -14,40 +14,20 @@
package remote
import (
- "bytes"
- "context"
"errors"
- "fmt"
- "log/slog"
- "math/rand/v2"
- "net/http"
- "net/http/httptest"
"net/url"
- "os"
- "reflect"
- "runtime"
- "strconv"
- "sync"
"testing"
"time"
- "github.com/google/go-cmp/cmp"
remoteapi "github.com/prometheus/client_golang/exp/api/remote"
"github.com/prometheus/client_golang/prometheus"
common_config "github.com/prometheus/common/config"
"github.com/prometheus/common/model"
- "github.com/prometheus/otlptranslator"
"github.com/stretchr/testify/require"
- "go.opentelemetry.io/collector/pdata/pcommon"
- "go.opentelemetry.io/collector/pdata/pmetric"
- "go.opentelemetry.io/collector/pdata/pmetric/pmetricotlp"
"github.com/prometheus/prometheus/config"
- "github.com/prometheus/prometheus/model/histogram"
"github.com/prometheus/prometheus/model/labels"
- "github.com/prometheus/prometheus/model/metadata"
"github.com/prometheus/prometheus/model/relabel"
- "github.com/prometheus/prometheus/storage"
)
func testRemoteWriteConfig() *config.RemoteWriteConfig {
@@ -385,1232 +365,6 @@ func TestWriteStorageApplyConfig_PartialUpdate(t *testing.T) {
require.NoError(t, s.Close())
}
-func TestOTLPWriteHandler(t *testing.T) {
- timestamp := time.Now()
- var zeroTime time.Time
- exportRequest := generateOTLPWriteRequest(timestamp, zeroTime)
- for _, testCase := range []struct {
- name string
- otlpCfg config.OTLPConfig
- typeAndUnitLabels bool
- expectedSamples []mockSample
- expectedMetadata []mockMetadata
- }{
- {
- name: "NoTranslation/NoTypeAndUnitLabels",
- otlpCfg: config.OTLPConfig{
- TranslationStrategy: otlptranslator.NoTranslation,
- },
- expectedSamples: []mockSample{
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.counter", "foo.bar", "baz", "instance", "test-instance", "job", "test-service"),
- t: timestamp.UnixMilli(),
- v: 10.0,
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "target_info", "host.name", "test-host", "instance", "test-instance", "job", "test-service"),
- t: timestamp.UnixMilli(),
- v: 1,
- },
- },
- expectedMetadata: []mockMetadata{
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.counter", "foo.bar", "baz", "instance", "test-instance", "job", "test-service"),
- m: metadata.Metadata{Type: model.MetricTypeCounter, Unit: "bytes", Help: "test-counter-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.gauge", "foo.bar", "baz", "instance", "test-instance", "job", "test-service"),
- m: metadata.Metadata{Type: model.MetricTypeGauge, Unit: "bytes", Help: "test-gauge-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.histogram_sum", "foo.bar", "baz", "instance", "test-instance", "job", "test-service"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.histogram_count", "foo.bar", "baz", "instance", "test-instance", "job", "test-service"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.histogram_bucket", "foo.bar", "baz", "instance", "test-instance", "job", "test-service", "le", "0"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.histogram_bucket", "foo.bar", "baz", "instance", "test-instance", "job", "test-service", "le", "1"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.histogram_bucket", "foo.bar", "baz", "instance", "test-instance", "job", "test-service", "le", "2"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.histogram_bucket", "foo.bar", "baz", "instance", "test-instance", "job", "test-service", "le", "3"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.histogram_bucket", "foo.bar", "baz", "instance", "test-instance", "job", "test-service", "le", "4"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.histogram_bucket", "foo.bar", "baz", "instance", "test-instance", "job", "test-service", "le", "5"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.histogram_bucket", "foo.bar", "baz", "instance", "test-instance", "job", "test-service", "le", "+Inf"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.exponential.histogram", "foo.bar", "baz", "instance", "test-instance", "job", "test-service"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-exponential-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "target_info", "host.name", "test-host", "instance", "test-instance", "job", "test-service"),
- m: metadata.Metadata{Type: model.MetricTypeGauge, Unit: "", Help: "Target metadata"},
- },
- },
- },
- {
- name: "NoTranslation/WithTypeAndUnitLabels",
- otlpCfg: config.OTLPConfig{
- TranslationStrategy: otlptranslator.NoTranslation,
- },
- typeAndUnitLabels: true,
- expectedSamples: []mockSample{
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.counter", "__type__", "counter", "__unit__", "bytes", "foo.bar", "baz", "instance", "test-instance", "job", "test-service"),
- t: timestamp.UnixMilli(),
- v: 10.0,
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "target_info", "host.name", "test-host", "instance", "test-instance", "job", "test-service"),
- t: timestamp.UnixMilli(),
- v: 1,
- },
- },
- expectedMetadata: []mockMetadata{
- {
- // Metadata labels follow series labels.
- l: labels.FromStrings(model.MetricNameLabel, "test.counter", "__type__", "counter", "__unit__", "bytes", "foo.bar", "baz", "instance", "test-instance", "job", "test-service"),
- m: metadata.Metadata{Type: model.MetricTypeCounter, Unit: "bytes", Help: "test-counter-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.gauge", "__type__", "gauge", "__unit__", "bytes", "foo.bar", "baz", "instance", "test-instance", "job", "test-service"),
- m: metadata.Metadata{Type: model.MetricTypeGauge, Unit: "bytes", Help: "test-gauge-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.histogram_sum", "__type__", "histogram", "__unit__", "bytes", "foo.bar", "baz", "instance", "test-instance", "job", "test-service"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.histogram_count", "__type__", "histogram", "__unit__", "bytes", "foo.bar", "baz", "instance", "test-instance", "job", "test-service"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.histogram_bucket", "__type__", "histogram", "__unit__", "bytes", "foo.bar", "baz", "instance", "test-instance", "job", "test-service", "le", "0"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.histogram_bucket", "__type__", "histogram", "__unit__", "bytes", "foo.bar", "baz", "instance", "test-instance", "job", "test-service", "le", "1"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.histogram_bucket", "__type__", "histogram", "__unit__", "bytes", "foo.bar", "baz", "instance", "test-instance", "job", "test-service", "le", "2"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.histogram_bucket", "__type__", "histogram", "__unit__", "bytes", "foo.bar", "baz", "instance", "test-instance", "job", "test-service", "le", "3"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.histogram_bucket", "__type__", "histogram", "__unit__", "bytes", "foo.bar", "baz", "instance", "test-instance", "job", "test-service", "le", "4"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.histogram_bucket", "__type__", "histogram", "__unit__", "bytes", "foo.bar", "baz", "instance", "test-instance", "job", "test-service", "le", "5"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.histogram_bucket", "__type__", "histogram", "__unit__", "bytes", "foo.bar", "baz", "instance", "test-instance", "job", "test-service", "le", "+Inf"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.exponential.histogram", "__type__", "histogram", "__unit__", "bytes", "foo.bar", "baz", "instance", "test-instance", "job", "test-service"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-exponential-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "target_info", "host.name", "test-host", "instance", "test-instance", "job", "test-service"),
- m: metadata.Metadata{Type: model.MetricTypeGauge, Unit: "", Help: "Target metadata"},
- },
- },
- },
- {
- name: "UnderscoreEscapingWithSuffixes/NoTypeAndUnitLabels",
- otlpCfg: config.OTLPConfig{
- TranslationStrategy: otlptranslator.UnderscoreEscapingWithSuffixes,
- },
- expectedSamples: []mockSample{
- {
- l: labels.FromStrings(model.MetricNameLabel, "test_counter_bytes_total", "foo_bar", "baz", "instance", "test-instance", "job", "test-service"),
- t: timestamp.UnixMilli(),
- v: 10.0,
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "target_info", "host_name", "test-host", "instance", "test-instance", "job", "test-service"),
- t: timestamp.UnixMilli(),
- v: 1,
- },
- },
- expectedMetadata: []mockMetadata{
- // All get _bytes unit suffix and counter also gets _total.
- {
- l: labels.FromStrings(model.MetricNameLabel, "test_counter_bytes_total", "foo_bar", "baz", "instance", "test-instance", "job", "test-service"),
- m: metadata.Metadata{Type: model.MetricTypeCounter, Unit: "bytes", Help: "test-counter-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test_gauge_bytes", "foo_bar", "baz", "instance", "test-instance", "job", "test-service"),
- m: metadata.Metadata{Type: model.MetricTypeGauge, Unit: "bytes", Help: "test-gauge-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test_histogram_bytes_sum", "foo_bar", "baz", "instance", "test-instance", "job", "test-service"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test_histogram_bytes_count", "foo_bar", "baz", "instance", "test-instance", "job", "test-service"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test_histogram_bytes_bucket", "foo_bar", "baz", "instance", "test-instance", "job", "test-service", "le", "0"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test_histogram_bytes_bucket", "foo_bar", "baz", "instance", "test-instance", "job", "test-service", "le", "1"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test_histogram_bytes_bucket", "foo_bar", "baz", "instance", "test-instance", "job", "test-service", "le", "2"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test_histogram_bytes_bucket", "foo_bar", "baz", "instance", "test-instance", "job", "test-service", "le", "3"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test_histogram_bytes_bucket", "foo_bar", "baz", "instance", "test-instance", "job", "test-service", "le", "4"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test_histogram_bytes_bucket", "foo_bar", "baz", "instance", "test-instance", "job", "test-service", "le", "5"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test_histogram_bytes_bucket", "foo_bar", "baz", "instance", "test-instance", "job", "test-service", "le", "+Inf"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test_exponential_histogram_bytes", "foo_bar", "baz", "instance", "test-instance", "job", "test-service"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-exponential-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "target_info", "host_name", "test-host", "instance", "test-instance", "job", "test-service"),
- m: metadata.Metadata{Type: model.MetricTypeGauge, Unit: "", Help: "Target metadata"},
- },
- },
- },
- {
- name: "UnderscoreEscapingWithoutSuffixes",
- otlpCfg: config.OTLPConfig{
- TranslationStrategy: otlptranslator.UnderscoreEscapingWithoutSuffixes,
- },
- expectedSamples: []mockSample{
- {
- l: labels.FromStrings(model.MetricNameLabel, "test_counter", "foo_bar", "baz", "instance", "test-instance", "job", "test-service"),
- t: timestamp.UnixMilli(),
- v: 10.0,
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "target_info", "host_name", "test-host", "instance", "test-instance", "job", "test-service"),
- t: timestamp.UnixMilli(),
- v: 1,
- },
- },
- expectedMetadata: []mockMetadata{
- {
- l: labels.FromStrings(model.MetricNameLabel, "test_counter", "foo_bar", "baz", "instance", "test-instance", "job", "test-service"),
- m: metadata.Metadata{Type: model.MetricTypeCounter, Unit: "bytes", Help: "test-counter-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test_gauge", "foo_bar", "baz", "instance", "test-instance", "job", "test-service"),
- m: metadata.Metadata{Type: model.MetricTypeGauge, Unit: "bytes", Help: "test-gauge-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test_histogram_sum", "foo_bar", "baz", "instance", "test-instance", "job", "test-service"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test_histogram_count", "foo_bar", "baz", "instance", "test-instance", "job", "test-service"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test_histogram_bucket", "foo_bar", "baz", "instance", "test-instance", "job", "test-service", "le", "0"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test_histogram_bucket", "foo_bar", "baz", "instance", "test-instance", "job", "test-service", "le", "1"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test_histogram_bucket", "foo_bar", "baz", "instance", "test-instance", "job", "test-service", "le", "2"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test_histogram_bucket", "foo_bar", "baz", "instance", "test-instance", "job", "test-service", "le", "3"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test_histogram_bucket", "foo_bar", "baz", "instance", "test-instance", "job", "test-service", "le", "4"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test_histogram_bucket", "foo_bar", "baz", "instance", "test-instance", "job", "test-service", "le", "5"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test_histogram_bucket", "foo_bar", "baz", "instance", "test-instance", "job", "test-service", "le", "+Inf"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test_exponential_histogram", "foo_bar", "baz", "instance", "test-instance", "job", "test-service"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-exponential-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "target_info", "host_name", "test-host", "instance", "test-instance", "job", "test-service"),
- m: metadata.Metadata{Type: model.MetricTypeGauge, Unit: "", Help: "Target metadata"},
- },
- },
- },
- {
- name: "UnderscoreEscapingWithSuffixes/WithTypeAndUnitLabels",
- otlpCfg: config.OTLPConfig{
- TranslationStrategy: otlptranslator.UnderscoreEscapingWithSuffixes,
- },
- typeAndUnitLabels: true,
- expectedSamples: []mockSample{
- {
- l: labels.New(labels.Label{Name: "__name__", Value: "test_counter_bytes_total"},
- labels.Label{Name: "__type__", Value: "counter"},
- labels.Label{Name: "__unit__", Value: "bytes"},
- labels.Label{Name: "foo_bar", Value: "baz"},
- labels.Label{Name: "instance", Value: "test-instance"},
- labels.Label{Name: "job", Value: "test-service"}),
- t: timestamp.UnixMilli(),
- v: 10.0,
- },
- {
- l: labels.New(
- labels.Label{Name: "__name__", Value: "target_info"},
- labels.Label{Name: "host_name", Value: "test-host"},
- labels.Label{Name: "instance", Value: "test-instance"},
- labels.Label{Name: "job", Value: "test-service"},
- ),
- t: timestamp.UnixMilli(),
- v: 1,
- },
- },
- expectedMetadata: []mockMetadata{
- {
- l: labels.FromStrings(model.MetricNameLabel, "test_counter_bytes_total", "__type__", "counter", "__unit__", "bytes", "foo_bar", "baz", "instance", "test-instance", "job", "test-service"),
- m: metadata.Metadata{Type: model.MetricTypeCounter, Unit: "bytes", Help: "test-counter-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test_gauge_bytes", "__type__", "gauge", "__unit__", "bytes", "foo_bar", "baz", "instance", "test-instance", "job", "test-service"),
- m: metadata.Metadata{Type: model.MetricTypeGauge, Unit: "bytes", Help: "test-gauge-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test_histogram_bytes_sum", "__type__", "histogram", "__unit__", "bytes", "foo_bar", "baz", "instance", "test-instance", "job", "test-service"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test_histogram_bytes_count", "__type__", "histogram", "__unit__", "bytes", "foo_bar", "baz", "instance", "test-instance", "job", "test-service"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test_histogram_bytes_bucket", "__type__", "histogram", "__unit__", "bytes", "foo_bar", "baz", "instance", "test-instance", "job", "test-service", "le", "0"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test_histogram_bytes_bucket", "__type__", "histogram", "__unit__", "bytes", "foo_bar", "baz", "instance", "test-instance", "job", "test-service", "le", "1"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test_histogram_bytes_bucket", "__type__", "histogram", "__unit__", "bytes", "foo_bar", "baz", "instance", "test-instance", "job", "test-service", "le", "2"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test_histogram_bytes_bucket", "__type__", "histogram", "__unit__", "bytes", "foo_bar", "baz", "instance", "test-instance", "job", "test-service", "le", "3"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test_histogram_bytes_bucket", "__type__", "histogram", "__unit__", "bytes", "foo_bar", "baz", "instance", "test-instance", "job", "test-service", "le", "4"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test_histogram_bytes_bucket", "__type__", "histogram", "__unit__", "bytes", "foo_bar", "baz", "instance", "test-instance", "job", "test-service", "le", "5"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test_histogram_bytes_bucket", "__type__", "histogram", "__unit__", "bytes", "foo_bar", "baz", "instance", "test-instance", "job", "test-service", "le", "+Inf"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test_exponential_histogram_bytes", "__type__", "histogram", "__unit__", "bytes", "foo_bar", "baz", "instance", "test-instance", "job", "test-service"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-exponential-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "target_info", "host_name", "test-host", "instance", "test-instance", "job", "test-service"),
- m: metadata.Metadata{Type: model.MetricTypeGauge, Unit: "", Help: "Target metadata"},
- },
- },
- },
- {
- name: "NoUTF8EscapingWithSuffixes/NoTypeAndUnitLabels",
- otlpCfg: config.OTLPConfig{
- TranslationStrategy: otlptranslator.NoUTF8EscapingWithSuffixes,
- },
- expectedSamples: []mockSample{
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.counter_bytes_total", "foo.bar", "baz", "instance", "test-instance", "job", "test-service"),
- t: timestamp.UnixMilli(),
- v: 10.0,
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "target_info", "host.name", "test-host", "instance", "test-instance", "job", "test-service"),
- t: timestamp.UnixMilli(),
- v: 1,
- },
- },
- expectedMetadata: []mockMetadata{
- // All get _bytes unit suffix and counter also gets _total.
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.counter_bytes_total", "foo.bar", "baz", "instance", "test-instance", "job", "test-service"),
- m: metadata.Metadata{Type: model.MetricTypeCounter, Unit: "bytes", Help: "test-counter-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.gauge_bytes", "foo.bar", "baz", "instance", "test-instance", "job", "test-service"),
- m: metadata.Metadata{Type: model.MetricTypeGauge, Unit: "bytes", Help: "test-gauge-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.histogram_bytes_sum", "foo.bar", "baz", "instance", "test-instance", "job", "test-service"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.histogram_bytes_count", "foo.bar", "baz", "instance", "test-instance", "job", "test-service"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.histogram_bytes_bucket", "foo.bar", "baz", "instance", "test-instance", "job", "test-service", "le", "0"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.histogram_bytes_bucket", "foo.bar", "baz", "instance", "test-instance", "job", "test-service", "le", "1"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.histogram_bytes_bucket", "foo.bar", "baz", "instance", "test-instance", "job", "test-service", "le", "2"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.histogram_bytes_bucket", "foo.bar", "baz", "instance", "test-instance", "job", "test-service", "le", "3"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.histogram_bytes_bucket", "foo.bar", "baz", "instance", "test-instance", "job", "test-service", "le", "4"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.histogram_bytes_bucket", "foo.bar", "baz", "instance", "test-instance", "job", "test-service", "le", "5"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.histogram_bytes_bucket", "foo.bar", "baz", "instance", "test-instance", "job", "test-service", "le", "+Inf"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.exponential.histogram_bytes", "foo.bar", "baz", "instance", "test-instance", "job", "test-service"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-exponential-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "target_info", "host.name", "test-host", "instance", "test-instance", "job", "test-service"),
- m: metadata.Metadata{Type: model.MetricTypeGauge, Unit: "", Help: "Target metadata"},
- },
- },
- },
- {
- name: "NoUTF8EscapingWithSuffixes/WithTypeAndUnitLabels",
- otlpCfg: config.OTLPConfig{
- TranslationStrategy: otlptranslator.NoUTF8EscapingWithSuffixes,
- },
- typeAndUnitLabels: true,
- expectedSamples: []mockSample{
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.counter_bytes_total", "__type__", "counter", "__unit__", "bytes", "foo.bar", "baz", "instance", "test-instance", "job", "test-service"),
- t: timestamp.UnixMilli(),
- v: 10.0,
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "target_info", "host.name", "test-host", "instance", "test-instance", "job", "test-service"),
- t: timestamp.UnixMilli(),
- v: 1,
- },
- },
- expectedMetadata: []mockMetadata{
- // All get _bytes unit suffix and counter also gets _total.
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.counter_bytes_total", "__type__", "counter", "__unit__", "bytes", "foo.bar", "baz", "instance", "test-instance", "job", "test-service"),
- m: metadata.Metadata{Type: model.MetricTypeCounter, Unit: "bytes", Help: "test-counter-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.gauge_bytes", "__type__", "gauge", "__unit__", "bytes", "foo.bar", "baz", "instance", "test-instance", "job", "test-service"),
- m: metadata.Metadata{Type: model.MetricTypeGauge, Unit: "bytes", Help: "test-gauge-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.histogram_bytes_sum", "__type__", "histogram", "__unit__", "bytes", "foo.bar", "baz", "instance", "test-instance", "job", "test-service"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.histogram_bytes_count", "__type__", "histogram", "__unit__", "bytes", "foo.bar", "baz", "instance", "test-instance", "job", "test-service"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.histogram_bytes_bucket", "__type__", "histogram", "__unit__", "bytes", "foo.bar", "baz", "instance", "test-instance", "job", "test-service", "le", "0"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.histogram_bytes_bucket", "__type__", "histogram", "__unit__", "bytes", "foo.bar", "baz", "instance", "test-instance", "job", "test-service", "le", "1"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.histogram_bytes_bucket", "__type__", "histogram", "__unit__", "bytes", "foo.bar", "baz", "instance", "test-instance", "job", "test-service", "le", "2"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.histogram_bytes_bucket", "__type__", "histogram", "__unit__", "bytes", "foo.bar", "baz", "instance", "test-instance", "job", "test-service", "le", "3"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.histogram_bytes_bucket", "__type__", "histogram", "__unit__", "bytes", "foo.bar", "baz", "instance", "test-instance", "job", "test-service", "le", "4"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.histogram_bytes_bucket", "__type__", "histogram", "__unit__", "bytes", "foo.bar", "baz", "instance", "test-instance", "job", "test-service", "le", "5"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.histogram_bytes_bucket", "__type__", "histogram", "__unit__", "bytes", "foo.bar", "baz", "instance", "test-instance", "job", "test-service", "le", "+Inf"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.exponential.histogram_bytes", "__type__", "histogram", "__unit__", "bytes", "foo.bar", "baz", "instance", "test-instance", "job", "test-service"),
- m: metadata.Metadata{Type: model.MetricTypeHistogram, Unit: "bytes", Help: "test-exponential-histogram-description"},
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "target_info", "host.name", "test-host", "instance", "test-instance", "job", "test-service"),
- m: metadata.Metadata{Type: model.MetricTypeGauge, Unit: "", Help: "Target metadata"},
- },
- },
- },
- } {
- t.Run(testCase.name, func(t *testing.T) {
- otlpOpts := OTLPOptions{
- EnableTypeAndUnitLabels: testCase.typeAndUnitLabels,
- }
- appendable := handleOTLP(t, exportRequest, testCase.otlpCfg, otlpOpts)
- for _, sample := range testCase.expectedSamples {
- requireContainsSample(t, appendable.samples, sample)
- }
- for _, meta := range testCase.expectedMetadata {
- requireContainsMetadata(t, appendable.metadata, meta)
- }
- require.Len(t, appendable.samples, 12) // 1 (counter) + 1 (gauge) + 1 (target_info) + 7 (hist_bucket) + 2 (hist_sum, hist_count)
- require.Len(t, appendable.histograms, 1) // 1 (exponential histogram)
- require.Len(t, appendable.metadata, 13) // for each float and histogram sample
- require.Len(t, appendable.exemplars, 1) // 1 (exemplar)
- })
- }
-}
-
-// Check that start time is ingested if ingestCTZeroSample is enabled
-// and the start time is actually set (non-zero).
-func TestOTLPWriteHandler_StartTime(t *testing.T) {
- timestamp := time.Now()
- startTime := timestamp.Add(-1 * time.Millisecond)
- var zeroTime time.Time
-
- expectedSamples := []mockSample{
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.counter", "foo.bar", "baz", "instance", "test-instance", "job", "test-service"),
- t: timestamp.UnixMilli(),
- v: 10.0,
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.gauge", "foo.bar", "baz", "instance", "test-instance", "job", "test-service"),
- t: timestamp.UnixMilli(),
- v: 10.0,
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.histogram_sum", "foo.bar", "baz", "instance", "test-instance", "job", "test-service"),
- t: timestamp.UnixMilli(),
- v: 30.0,
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.histogram_count", "foo.bar", "baz", "instance", "test-instance", "job", "test-service"),
- t: timestamp.UnixMilli(),
- v: 12.0,
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.histogram_bucket", "foo.bar", "baz", "instance", "test-instance", "job", "test-service", "le", "0"),
- t: timestamp.UnixMilli(),
- v: 2.0,
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.histogram_bucket", "foo.bar", "baz", "instance", "test-instance", "job", "test-service", "le", "1"),
- t: timestamp.UnixMilli(),
- v: 4.0,
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.histogram_bucket", "foo.bar", "baz", "instance", "test-instance", "job", "test-service", "le", "2"),
- t: timestamp.UnixMilli(),
- v: 6.0,
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.histogram_bucket", "foo.bar", "baz", "instance", "test-instance", "job", "test-service", "le", "3"),
- t: timestamp.UnixMilli(),
- v: 8.0,
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.histogram_bucket", "foo.bar", "baz", "instance", "test-instance", "job", "test-service", "le", "4"),
- t: timestamp.UnixMilli(),
- v: 10.0,
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.histogram_bucket", "foo.bar", "baz", "instance", "test-instance", "job", "test-service", "le", "5"),
- t: timestamp.UnixMilli(),
- v: 12.0,
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.histogram_bucket", "foo.bar", "baz", "instance", "test-instance", "job", "test-service", "le", "+Inf"),
- t: timestamp.UnixMilli(),
- v: 12.0,
- },
- {
- l: labels.FromStrings(model.MetricNameLabel, "target_info", "host.name", "test-host", "instance", "test-instance", "job", "test-service"),
- t: timestamp.UnixMilli(),
- v: 1.0,
- },
- }
- expectedHistograms := []mockHistogram{
- {
- l: labels.FromStrings(model.MetricNameLabel, "test.exponential.histogram", "foo.bar", "baz", "instance", "test-instance", "job", "test-service"),
- t: timestamp.UnixMilli(),
- h: &histogram.Histogram{
- Schema: 2,
- ZeroThreshold: 1e-128,
- ZeroCount: 2,
- Count: 10,
- Sum: 30,
- PositiveSpans: []histogram.Span{{Offset: 1, Length: 5}},
- PositiveBuckets: []int64{2, 0, 0, 0, 0},
- },
- },
- }
-
- expectedSamplesWithCTZero := make([]mockSample, 0, len(expectedSamples)*2-1) // All samples will get CT zero, except target_info.
- for _, s := range expectedSamples {
- if s.l.Get(model.MetricNameLabel) != "target_info" {
- expectedSamplesWithCTZero = append(expectedSamplesWithCTZero, mockSample{
- l: s.l.Copy(),
- t: startTime.UnixMilli(),
- v: 0,
- })
- }
- expectedSamplesWithCTZero = append(expectedSamplesWithCTZero, s)
- }
- expectedHistogramsWithCTZero := make([]mockHistogram, 0, len(expectedHistograms)*2)
- for _, s := range expectedHistograms {
- if s.l.Get(model.MetricNameLabel) != "target_info" {
- expectedHistogramsWithCTZero = append(expectedHistogramsWithCTZero, mockHistogram{
- l: s.l.Copy(),
- t: startTime.UnixMilli(),
- h: &histogram.Histogram{},
- })
- }
- expectedHistogramsWithCTZero = append(expectedHistogramsWithCTZero, s)
- }
-
- for _, testCase := range []struct {
- name string
- otlpOpts OTLPOptions
- startTime time.Time
- expectCTZero bool
- expectedSamples []mockSample
- expectedHistograms []mockHistogram
- }{
- {
- name: "IngestCTZero=false/startTime=0",
- otlpOpts: OTLPOptions{
- IngestCTZeroSample: false,
- },
- startTime: zeroTime,
- expectedSamples: expectedSamples,
- expectedHistograms: expectedHistograms,
- },
- {
- name: "IngestCTZero=true/startTime=0",
- otlpOpts: OTLPOptions{
- IngestCTZeroSample: true,
- },
- startTime: zeroTime,
- expectedSamples: expectedSamples,
- expectedHistograms: expectedHistograms,
- },
- {
- name: "IngestCTZero=false/startTime=ts-1ms",
- otlpOpts: OTLPOptions{
- IngestCTZeroSample: false,
- },
- startTime: startTime,
- expectedSamples: expectedSamples,
- expectedHistograms: expectedHistograms,
- },
- {
- name: "IngestCTZero=true/startTime=ts-1ms",
- otlpOpts: OTLPOptions{
- IngestCTZeroSample: true,
- },
- startTime: startTime,
- expectedSamples: expectedSamplesWithCTZero,
- expectedHistograms: expectedHistogramsWithCTZero,
- },
- } {
- t.Run(testCase.name, func(t *testing.T) {
- exportRequest := generateOTLPWriteRequest(timestamp, testCase.startTime)
- appendable := handleOTLP(t, exportRequest, config.OTLPConfig{
- TranslationStrategy: otlptranslator.NoTranslation,
- }, testCase.otlpOpts)
- for i, expect := range testCase.expectedSamples {
- actual := appendable.samples[i]
- require.True(t, labels.Equal(expect.l, actual.l), "sample labels,pos=%v", i)
- require.Equal(t, expect.t, actual.t, "sample timestamp,pos=%v", i)
- require.Equal(t, expect.v, actual.v, "sample value,pos=%v", i)
- }
- for i, expect := range testCase.expectedHistograms {
- actual := appendable.histograms[i]
- require.True(t, labels.Equal(expect.l, actual.l), "histogram labels,pos=%v", i)
- require.Equal(t, expect.t, actual.t, "histogram timestamp,pos=%v", i)
- require.True(t, expect.h.Equals(actual.h), "histogram value,pos=%v", i)
- }
- require.Len(t, appendable.samples, len(testCase.expectedSamples))
- require.Len(t, appendable.histograms, len(testCase.expectedHistograms))
- })
- }
-}
-
-func requireContainsSample(t *testing.T, actual []mockSample, expected mockSample) {
- t.Helper()
-
- for _, got := range actual {
- if labels.Equal(expected.l, got.l) && expected.t == got.t && expected.v == got.v {
- return
- }
- }
- require.Fail(t, fmt.Sprintf("Sample not found: \n"+
- "expected: %v\n"+
- "actual : %v", expected, actual))
-}
-
-func requireContainsMetadata(t *testing.T, actual []mockMetadata, expected mockMetadata) {
- t.Helper()
-
- for _, got := range actual {
- if labels.Equal(expected.l, got.l) && expected.m.Type == got.m.Type && expected.m.Unit == got.m.Unit && expected.m.Help == got.m.Help {
- return
- }
- }
- require.Fail(t, fmt.Sprintf("Metadata not found: \n"+
- "expected: %v\n"+
- "actual : %v", expected, actual))
-}
-
-func handleOTLP(t *testing.T, exportRequest pmetricotlp.ExportRequest, otlpCfg config.OTLPConfig, otlpOpts OTLPOptions) *mockAppendable {
- buf, err := exportRequest.MarshalProto()
- require.NoError(t, err)
-
- req, err := http.NewRequest("", "", bytes.NewReader(buf))
- require.NoError(t, err)
- req.Header.Set("Content-Type", "application/x-protobuf")
-
- log := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelWarn}))
- appendable := &mockAppendable{}
- handler := NewOTLPWriteHandler(log, nil, appendable, func() config.Config {
- return config.Config{
- OTLPConfig: otlpCfg,
- }
- }, otlpOpts)
- recorder := httptest.NewRecorder()
- handler.ServeHTTP(recorder, req)
-
- resp := recorder.Result()
- require.Equal(t, http.StatusOK, resp.StatusCode)
-
- return appendable
-}
-
-func generateOTLPWriteRequest(timestamp, startTime time.Time) pmetricotlp.ExportRequest {
- d := pmetric.NewMetrics()
-
- // Generate One Counter, One Gauge, One Histogram, One Exponential-Histogram
- // with resource attributes: service.name="test-service", service.instance.id="test-instance", host.name="test-host"
- // with metric attribute: foo.bar="baz"
-
- resourceMetric := d.ResourceMetrics().AppendEmpty()
- resourceMetric.Resource().Attributes().PutStr("service.name", "test-service")
- resourceMetric.Resource().Attributes().PutStr("service.instance.id", "test-instance")
- resourceMetric.Resource().Attributes().PutStr("host.name", "test-host")
-
- scopeMetric := resourceMetric.ScopeMetrics().AppendEmpty()
-
- // Generate One Counter
- counterMetric := scopeMetric.Metrics().AppendEmpty()
- counterMetric.SetName("test.counter")
- counterMetric.SetDescription("test-counter-description")
- counterMetric.SetUnit("By")
- counterMetric.SetEmptySum()
- counterMetric.Sum().SetAggregationTemporality(pmetric.AggregationTemporalityCumulative)
- counterMetric.Sum().SetIsMonotonic(true)
-
- counterDataPoint := counterMetric.Sum().DataPoints().AppendEmpty()
- counterDataPoint.SetTimestamp(pcommon.NewTimestampFromTime(timestamp))
- counterDataPoint.SetStartTimestamp(pcommon.NewTimestampFromTime(startTime))
- counterDataPoint.SetDoubleValue(10.0)
- counterDataPoint.Attributes().PutStr("foo.bar", "baz")
-
- counterExemplar := counterDataPoint.Exemplars().AppendEmpty()
-
- counterExemplar.SetTimestamp(pcommon.NewTimestampFromTime(timestamp))
- counterExemplar.SetDoubleValue(10.0)
- counterExemplar.SetSpanID(pcommon.SpanID{0, 1, 2, 3, 4, 5, 6, 7})
- counterExemplar.SetTraceID(pcommon.TraceID{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15})
-
- // Generate One Gauge
- gaugeMetric := scopeMetric.Metrics().AppendEmpty()
- gaugeMetric.SetName("test.gauge")
- gaugeMetric.SetDescription("test-gauge-description")
- gaugeMetric.SetUnit("By")
- gaugeMetric.SetEmptyGauge()
-
- gaugeDataPoint := gaugeMetric.Gauge().DataPoints().AppendEmpty()
- gaugeDataPoint.SetTimestamp(pcommon.NewTimestampFromTime(timestamp))
- gaugeDataPoint.SetStartTimestamp(pcommon.NewTimestampFromTime(startTime))
- gaugeDataPoint.SetDoubleValue(10.0)
- gaugeDataPoint.Attributes().PutStr("foo.bar", "baz")
-
- // Generate One Histogram
- histogramMetric := scopeMetric.Metrics().AppendEmpty()
- histogramMetric.SetName("test.histogram")
- histogramMetric.SetDescription("test-histogram-description")
- histogramMetric.SetUnit("By")
- histogramMetric.SetEmptyHistogram()
- histogramMetric.Histogram().SetAggregationTemporality(pmetric.AggregationTemporalityCumulative)
-
- histogramDataPoint := histogramMetric.Histogram().DataPoints().AppendEmpty()
- histogramDataPoint.SetTimestamp(pcommon.NewTimestampFromTime(timestamp))
- histogramDataPoint.SetStartTimestamp(pcommon.NewTimestampFromTime(startTime))
- histogramDataPoint.ExplicitBounds().FromRaw([]float64{0.0, 1.0, 2.0, 3.0, 4.0, 5.0})
- histogramDataPoint.BucketCounts().FromRaw([]uint64{2, 2, 2, 2, 2, 2})
- histogramDataPoint.SetCount(12)
- histogramDataPoint.SetSum(30.0)
- histogramDataPoint.Attributes().PutStr("foo.bar", "baz")
-
- // Generate One Exponential-Histogram
- exponentialHistogramMetric := scopeMetric.Metrics().AppendEmpty()
- exponentialHistogramMetric.SetName("test.exponential.histogram")
- exponentialHistogramMetric.SetDescription("test-exponential-histogram-description")
- exponentialHistogramMetric.SetUnit("By")
- exponentialHistogramMetric.SetEmptyExponentialHistogram()
- exponentialHistogramMetric.ExponentialHistogram().SetAggregationTemporality(pmetric.AggregationTemporalityCumulative)
-
- exponentialHistogramDataPoint := exponentialHistogramMetric.ExponentialHistogram().DataPoints().AppendEmpty()
- exponentialHistogramDataPoint.SetTimestamp(pcommon.NewTimestampFromTime(timestamp))
- exponentialHistogramDataPoint.SetStartTimestamp(pcommon.NewTimestampFromTime(startTime))
- exponentialHistogramDataPoint.SetScale(2.0)
- exponentialHistogramDataPoint.Positive().BucketCounts().FromRaw([]uint64{2, 2, 2, 2, 2})
- exponentialHistogramDataPoint.SetZeroCount(2)
- exponentialHistogramDataPoint.SetCount(10)
- exponentialHistogramDataPoint.SetSum(30.0)
- exponentialHistogramDataPoint.Attributes().PutStr("foo.bar", "baz")
-
- return pmetricotlp.NewExportRequestFromMetrics(d)
-}
-
-func TestOTLPDelta(t *testing.T) {
- log := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelWarn}))
- appendable := &mockAppendable{}
- cfg := func() config.Config {
- return config.Config{OTLPConfig: config.DefaultOTLPConfig}
- }
- handler := NewOTLPWriteHandler(log, nil, appendable, cfg, OTLPOptions{ConvertDelta: true})
-
- md := pmetric.NewMetrics()
- ms := md.ResourceMetrics().AppendEmpty().ScopeMetrics().AppendEmpty().Metrics()
-
- m := ms.AppendEmpty()
- m.SetName("some.delta.total")
-
- sum := m.SetEmptySum()
- sum.SetAggregationTemporality(pmetric.AggregationTemporalityDelta)
-
- ts := time.Date(2000, 1, 2, 3, 4, 0, 0, time.UTC)
- for i := range 3 {
- dp := sum.DataPoints().AppendEmpty()
- dp.SetIntValue(int64(i))
- dp.SetTimestamp(pcommon.NewTimestampFromTime(ts.Add(time.Duration(i) * time.Second)))
- }
-
- proto, err := pmetricotlp.NewExportRequestFromMetrics(md).MarshalProto()
- require.NoError(t, err)
-
- req, err := http.NewRequest("", "", bytes.NewReader(proto))
- require.NoError(t, err)
- req.Header.Set("Content-Type", "application/x-protobuf")
-
- rec := httptest.NewRecorder()
- handler.ServeHTTP(rec, req)
- require.Equal(t, http.StatusOK, rec.Result().StatusCode)
-
- ls := labels.FromStrings("__name__", "some_delta_total")
- milli := func(sec int) int64 {
- return time.Date(2000, 1, 2, 3, 4, sec, 0, time.UTC).UnixMilli()
- }
-
- want := []mockSample{
- {t: milli(0), l: ls, v: 0}, // +0
- {t: milli(1), l: ls, v: 1}, // +1
- {t: milli(2), l: ls, v: 3}, // +2
- }
- if diff := cmp.Diff(want, appendable.samples, cmp.Exporter(func(reflect.Type) bool { return true })); diff != "" {
- t.Fatal(diff)
- }
-}
-
-func BenchmarkOTLP(b *testing.B) {
- start := time.Date(2000, 1, 2, 3, 4, 5, 0, time.UTC)
-
- type Type struct {
- name string
- data func(mode pmetric.AggregationTemporality, dpc, epoch int) []pmetric.Metric
- }
- types := []Type{{
- name: "sum",
- data: func() func(mode pmetric.AggregationTemporality, dpc, epoch int) []pmetric.Metric {
- cumul := make(map[int]float64)
- return func(mode pmetric.AggregationTemporality, dpc, epoch int) []pmetric.Metric {
- m := pmetric.NewMetric()
- sum := m.SetEmptySum()
- sum.SetAggregationTemporality(mode)
- dps := sum.DataPoints()
- for id := range dpc {
- dp := dps.AppendEmpty()
- dp.SetStartTimestamp(pcommon.NewTimestampFromTime(start))
- dp.SetTimestamp(pcommon.NewTimestampFromTime(start.Add(time.Duration(epoch) * time.Minute)))
- dp.Attributes().PutStr("id", strconv.Itoa(id))
- v := float64(rand.IntN(100)) / 10
- switch mode {
- case pmetric.AggregationTemporalityDelta:
- dp.SetDoubleValue(v)
- case pmetric.AggregationTemporalityCumulative:
- cumul[id] += v
- dp.SetDoubleValue(cumul[id])
- }
- }
- return []pmetric.Metric{m}
- }
- }(),
- }, {
- name: "histogram",
- data: func() func(mode pmetric.AggregationTemporality, dpc, epoch int) []pmetric.Metric {
- bounds := [4]float64{1, 10, 100, 1000}
- type state struct {
- counts [4]uint64
- count uint64
- sum float64
- }
- var cumul []state
- return func(mode pmetric.AggregationTemporality, dpc, epoch int) []pmetric.Metric {
- if cumul == nil {
- cumul = make([]state, dpc)
- }
- m := pmetric.NewMetric()
- hist := m.SetEmptyHistogram()
- hist.SetAggregationTemporality(mode)
- dps := hist.DataPoints()
- for id := range dpc {
- dp := dps.AppendEmpty()
- dp.SetStartTimestamp(pcommon.NewTimestampFromTime(start))
- dp.SetTimestamp(pcommon.NewTimestampFromTime(start.Add(time.Duration(epoch) * time.Minute)))
- dp.Attributes().PutStr("id", strconv.Itoa(id))
- dp.ExplicitBounds().FromRaw(bounds[:])
-
- var obs *state
- switch mode {
- case pmetric.AggregationTemporalityDelta:
- obs = new(state)
- case pmetric.AggregationTemporalityCumulative:
- obs = &cumul[id]
- }
-
- for i := range obs.counts {
- v := uint64(rand.IntN(10))
- obs.counts[i] += v
- obs.count++
- obs.sum += float64(v)
- }
-
- dp.SetCount(obs.count)
- dp.SetSum(obs.sum)
- dp.BucketCounts().FromRaw(obs.counts[:])
- }
- return []pmetric.Metric{m}
- }
- }(),
- }, {
- name: "exponential",
- data: func() func(mode pmetric.AggregationTemporality, dpc, epoch int) []pmetric.Metric {
- type state struct {
- counts [4]uint64
- count uint64
- sum float64
- }
- var cumul []state
- return func(mode pmetric.AggregationTemporality, dpc, epoch int) []pmetric.Metric {
- if cumul == nil {
- cumul = make([]state, dpc)
- }
- m := pmetric.NewMetric()
- ex := m.SetEmptyExponentialHistogram()
- ex.SetAggregationTemporality(mode)
- dps := ex.DataPoints()
- for id := range dpc {
- dp := dps.AppendEmpty()
- dp.SetStartTimestamp(pcommon.NewTimestampFromTime(start))
- dp.SetTimestamp(pcommon.NewTimestampFromTime(start.Add(time.Duration(epoch) * time.Minute)))
- dp.Attributes().PutStr("id", strconv.Itoa(id))
- dp.SetScale(2)
-
- var obs *state
- switch mode {
- case pmetric.AggregationTemporalityDelta:
- obs = new(state)
- case pmetric.AggregationTemporalityCumulative:
- obs = &cumul[id]
- }
-
- for i := range obs.counts {
- v := uint64(rand.IntN(10))
- obs.counts[i] += v
- obs.count++
- obs.sum += float64(v)
- }
-
- dp.Positive().BucketCounts().FromRaw(obs.counts[:])
- dp.SetCount(obs.count)
- dp.SetSum(obs.sum)
- }
-
- return []pmetric.Metric{m}
- }
- }(),
- }}
-
- modes := []struct {
- name string
- data func(func(pmetric.AggregationTemporality, int, int) []pmetric.Metric, int) []pmetric.Metric
- }{{
- name: "cumulative",
- data: func(data func(pmetric.AggregationTemporality, int, int) []pmetric.Metric, epoch int) []pmetric.Metric {
- return data(pmetric.AggregationTemporalityCumulative, 10, epoch)
- },
- }, {
- name: "delta",
- data: func(data func(pmetric.AggregationTemporality, int, int) []pmetric.Metric, epoch int) []pmetric.Metric {
- return data(pmetric.AggregationTemporalityDelta, 10, epoch)
- },
- }, {
- name: "mixed",
- data: func(data func(pmetric.AggregationTemporality, int, int) []pmetric.Metric, epoch int) []pmetric.Metric {
- cumul := data(pmetric.AggregationTemporalityCumulative, 5, epoch)
- delta := data(pmetric.AggregationTemporalityDelta, 5, epoch)
- out := append(cumul, delta...)
- rand.Shuffle(len(out), func(i, j int) { out[i], out[j] = out[j], out[i] })
- return out
- },
- }}
-
- configs := []struct {
- name string
- opts OTLPOptions
- }{
- {name: "default"},
- {name: "convert", opts: OTLPOptions{ConvertDelta: true}},
- }
-
- Workers := runtime.GOMAXPROCS(0)
- for _, cs := range types {
- for _, mode := range modes {
- for _, cfg := range configs {
- b.Run(fmt.Sprintf("type=%s/temporality=%s/cfg=%s", cs.name, mode.name, cfg.name), func(b *testing.B) {
- if !cfg.opts.ConvertDelta && (mode.name == "delta" || mode.name == "mixed") {
- b.Skip("not possible")
- }
-
- var total int
-
- // reqs is a [b.N]*http.Request, divided across the workers.
- // deltatocumulative requires timestamps to be strictly in
- // order on a per-series basis. to ensure this, each reqs[k]
- // contains samples of differently named series, sorted
- // strictly in time order
- reqs := make([][]*http.Request, Workers)
- for n := range b.N {
- k := n % Workers
-
- md := pmetric.NewMetrics()
- ms := md.ResourceMetrics().AppendEmpty().
- ScopeMetrics().AppendEmpty().
- Metrics()
-
- for i, m := range mode.data(cs.data, n) {
- m.SetName(fmt.Sprintf("benchmark_%d_%d", k, i))
- m.MoveTo(ms.AppendEmpty())
- }
-
- total += sampleCount(md)
-
- ex := pmetricotlp.NewExportRequestFromMetrics(md)
- data, err := ex.MarshalProto()
- require.NoError(b, err)
-
- req, err := http.NewRequest("", "", bytes.NewReader(data))
- require.NoError(b, err)
- req.Header.Set("Content-Type", "application/x-protobuf")
-
- reqs[k] = append(reqs[k], req)
- }
-
- log := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelWarn}))
- mock := new(mockAppendable)
- appendable := syncAppendable{Appendable: mock, lock: new(sync.Mutex)}
- cfgfn := func() config.Config {
- return config.Config{OTLPConfig: config.DefaultOTLPConfig}
- }
- handler := NewOTLPWriteHandler(log, nil, appendable, cfgfn, cfg.opts)
-
- fail := make(chan struct{})
- done := make(chan struct{})
-
- b.ResetTimer()
- b.ReportAllocs()
-
- // we use multiple workers to mimic a real-world scenario
- // where multiple OTel collectors are sending their
- // time-series in parallel.
- // this is necessary to exercise potential lock-contention
- // in this benchmark
- for k := range Workers {
- go func() {
- rec := httptest.NewRecorder()
- for _, req := range reqs[k] {
- handler.ServeHTTP(rec, req)
- if rec.Result().StatusCode != http.StatusOK {
- fail <- struct{}{}
- return
- }
- }
- done <- struct{}{}
- }()
- }
-
- for range Workers {
- select {
- case <-fail:
- b.FailNow()
- case <-done:
- }
- }
-
- require.Equal(b, total, len(mock.samples)+len(mock.histograms))
- })
- }
- }
- }
-}
-
-func sampleCount(md pmetric.Metrics) int {
- var total int
- rms := md.ResourceMetrics()
- for i := range rms.Len() {
- sms := rms.At(i).ScopeMetrics()
- for i := range sms.Len() {
- ms := sms.At(i).Metrics()
- for i := range ms.Len() {
- m := ms.At(i)
- switch m.Type() {
- case pmetric.MetricTypeSum:
- total += m.Sum().DataPoints().Len()
- case pmetric.MetricTypeGauge:
- total += m.Gauge().DataPoints().Len()
- case pmetric.MetricTypeHistogram:
- dps := m.Histogram().DataPoints()
- for i := range dps.Len() {
- total += dps.At(i).BucketCounts().Len()
- total++ // le=+Inf series
- total++ // _sum series
- total++ // _count series
- }
- case pmetric.MetricTypeExponentialHistogram:
- total += m.ExponentialHistogram().DataPoints().Len()
- case pmetric.MetricTypeSummary:
- total += m.Summary().DataPoints().Len()
- }
- }
- }
- }
- return total
-}
-
-type syncAppendable struct {
- lock sync.Locker
- storage.Appendable
-}
-
-type syncAppender struct {
- lock sync.Locker
- storage.Appender
-}
-
-func (s syncAppendable) Appender(ctx context.Context) storage.Appender {
- return syncAppender{Appender: s.Appendable.Appender(ctx), lock: s.lock}
-}
-
-func (s syncAppender) Append(ref storage.SeriesRef, l labels.Labels, t int64, v float64) (storage.SeriesRef, error) {
- s.lock.Lock()
- defer s.lock.Unlock()
- return s.Appender.Append(ref, l, t, v)
-}
-
-func (s syncAppender) AppendHistogram(ref storage.SeriesRef, l labels.Labels, t int64, h *histogram.Histogram, f *histogram.FloatHistogram) (storage.SeriesRef, error) {
- s.lock.Lock()
- defer s.lock.Unlock()
- return s.Appender.AppendHistogram(ref, l, t, h, f)
-}
-
func TestWriteStorage_CanRegisterMetricsAfterClosing(t *testing.T) {
dir := t.TempDir()
reg := prometheus.NewPedanticRegistry()
diff --git a/storage/secondary.go b/storage/secondary.go
index 1cf8024b65..a071ddcfa3 100644
--- a/storage/secondary.go
+++ b/storage/secondary.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
diff --git a/storage/series.go b/storage/series.go
index 2fff56785a..bf6df7db3e 100644
--- a/storage/series.go
+++ b/storage/series.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
@@ -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}
}
@@ -439,16 +447,26 @@ func (e errChunksIterator) Err() error { return e.err }
// ExpandSamples iterates over all samples in the iterator, buffering all in slice.
// 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.
-func ExpandSamples(iter chunkenc.Iterator, newSampleFn func(t int64, f float64, h *histogram.Histogram, fh *histogram.FloatHistogram) chunks.Sample) ([]chunks.Sample, error) {
+// For float sample, NaN values are replaced with -42.
+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(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(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}
}
}
}
@@ -460,17 +478,20 @@ func ExpandSamples(iter chunkenc.Iterator, newSampleFn func(t int64, f float64,
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 math.IsNaN(f) {
+ 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 1ade558648..b33d6cb1b3 100644
--- a/storage/series_test.go
+++ b/storage/series_test.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
@@ -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/template/template.go b/template/template.go
index ea7e93b18c..0ea7382ed3 100644
--- a/template/template.go
+++ b/template/template.go
@@ -1,4 +1,4 @@
-// Copyright 2013 The Prometheus Authors
+// 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
@@ -36,6 +36,7 @@ import (
"golang.org/x/text/language"
"github.com/prometheus/prometheus/promql"
+ "github.com/prometheus/prometheus/util/features"
"github.com/prometheus/prometheus/util/strutil"
)
@@ -413,3 +414,29 @@ func floatToTime(v float64) (*time.Time, error) {
t := model.TimeFromUnixNano(int64(timestamp)).Time().UTC()
return &t, nil
}
+
+// templateFunctions returns a representative funcMap with all available template functions.
+// This is used to discover which functions are available for feature registration.
+func templateFunctions() text_template.FuncMap {
+ // Create a dummy expander to get the function map.
+ expander := NewTemplateExpander(
+ context.Background(),
+ "",
+ "",
+ nil,
+ 0,
+ nil,
+ &url.URL{},
+ nil,
+ )
+ return expander.funcMap
+}
+
+// RegisterFeatures registers all template functions with the feature registry.
+func RegisterFeatures(r features.Collector) {
+ // Get all function names from the template function map.
+ funcMap := templateFunctions()
+ for name := range funcMap {
+ r.Enable(features.TemplatingFunctions, name)
+ }
+}
diff --git a/template/template_amd64_test.go b/template/template_amd64_test.go
index 913a7e2b81..15db39b646 100644
--- a/template/template_amd64_test.go
+++ b/template/template_amd64_test.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
diff --git a/template/template_test.go b/template/template_test.go
index f3348caae6..073300a39b 100644
--- a/template/template_test.go
+++ b/template/template_test.go
@@ -1,4 +1,4 @@
-// Copyright 2014 The Prometheus Authors
+// 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
diff --git a/tracing/tracing.go b/tracing/tracing.go
index 91ac48007b..36a8d0fe10 100644
--- a/tracing/tracing.go
+++ b/tracing/tracing.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
@@ -29,7 +29,7 @@ import (
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/resource"
tracesdk "go.opentelemetry.io/otel/sdk/trace"
- semconv "go.opentelemetry.io/otel/semconv/v1.37.0"
+ semconv "go.opentelemetry.io/otel/semconv/v1.39.0"
"go.opentelemetry.io/otel/trace"
"go.opentelemetry.io/otel/trace/noop"
"google.golang.org/grpc/credentials"
diff --git a/tracing/tracing_test.go b/tracing/tracing_test.go
index e735e1a18a..0840abafdf 100644
--- a/tracing/tracing_test.go
+++ b/tracing/tracing_test.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
diff --git a/tsdb/agent/db.go b/tsdb/agent/db.go
index 7884366ebe..460ceb7c04 100644
--- a/tsdb/agent/db.go
+++ b/tsdb/agent/db.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
@@ -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"
@@ -84,6 +83,20 @@ type Options struct {
// OutOfOrderTimeWindow specifies how much out of order is allowed, if any.
OutOfOrderTimeWindow int64
+
+ // EnableSTAsZeroSample represents 'created-timestamp-zero-ingestion' feature flag.
+ // If true, ST, if non-empty and earlier than sample timestamp, will be stored
+ // as a zero sample before the actual sample.
+ //
+ // The zero sample is best-effort, only debug log on failure is emitted.
+ // NOTE(bwplotka): This feature might be deprecated and removed once PROM-60
+ // is implemented.
+ EnableSTAsZeroSample bool
+
+ // EnableSTStorage determines whether agent DB should write a Start Timestamp (ST)
+ // per sample to WAL.
+ // TODO(bwplotka): Implement this option as per PROM-60, currently it's noop.
+ EnableSTStorage bool
}
// DefaultOptions used for the WAL storage. They are reasonable for setups using
@@ -233,8 +246,9 @@ type DB struct {
wal *wlog.WL
locker *tsdbutil.DirLocker
- appenderPool sync.Pool
- bufPool sync.Pool
+ appenderPool sync.Pool
+ appenderV2Pool sync.Pool
+ bufPool sync.Pool
// These pools are only used during WAL replay and are reset at the end.
// NOTE: Adjust resetWALReplayResources() upon changes to the pools.
@@ -303,12 +317,26 @@ func Open(l *slog.Logger, reg prometheus.Registerer, rs *remote.Storage, dir str
db.appenderPool.New = func() any {
return &appender{
- DB: db,
- pendingSeries: make([]record.RefSeries, 0, 100),
- pendingSamples: make([]record.RefSample, 0, 100),
- pendingHistograms: make([]record.RefHistogramSample, 0, 100),
- pendingFloatHistograms: make([]record.RefFloatHistogramSample, 0, 100),
- pendingExamplars: make([]record.RefExemplar, 0, 10),
+ appenderBase: appenderBase{
+ DB: db,
+ pendingSeries: make([]record.RefSeries, 0, 100),
+ pendingSamples: make([]record.RefSample, 0, 100),
+ pendingHistograms: make([]record.RefHistogramSample, 0, 100),
+ pendingFloatHistograms: make([]record.RefFloatHistogramSample, 0, 100),
+ pendingExamplars: make([]record.RefExemplar, 0, 10),
+ },
+ }
+ }
+ db.appenderV2Pool.New = func() any {
+ return &appenderV2{
+ appenderBase: appenderBase{
+ DB: db,
+ pendingSeries: make([]record.RefSeries, 0, 100),
+ pendingSamples: make([]record.RefSample, 0, 100),
+ pendingHistograms: make([]record.RefHistogramSample, 0, 100),
+ pendingFloatHistograms: make([]record.RefFloatHistogramSample, 0, 100),
+ pendingExamplars: make([]record.RefExemplar, 0, 10),
+ },
}
}
@@ -774,12 +802,11 @@ 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 appender struct {
+type appenderBase struct {
*DB
- hints *storage.AppendOptions
pendingSeries []record.RefSeries
pendingSamples []record.RefSample
@@ -800,6 +827,12 @@ type appender struct {
floatHistogramSeries []*memSeries
}
+type appender struct {
+ appenderBase
+
+ hints *storage.AppendOptions
+}
+
func (a *appender) SetOptions(opts *storage.AppendOptions) {
a.hints = opts
}
@@ -810,26 +843,10 @@ func (a *appender) Append(ref storage.SeriesRef, l labels.Labels, t int64, v flo
series := a.series.GetByID(headRef)
if series == nil {
- // Ensure no empty or duplicate labels have gotten through. This mirrors the
- // equivalent validation code in the TSDB's headAppender.
- l = l.WithoutEmpty()
- if l.IsEmpty() {
- return 0, fmt.Errorf("empty labelset: %w", tsdb.ErrInvalidSample)
- }
-
- if lbl, dup := l.HasDuplicateLabelNames(); dup {
- return 0, fmt.Errorf(`label name "%s" is not unique: %w`, lbl, tsdb.ErrInvalidSample)
- }
-
- var created bool
- series, created = a.getOrCreate(l)
- if created {
- a.pendingSeries = append(a.pendingSeries, record.RefSeries{
- Ref: series.ref,
- Labels: l,
- })
-
- a.metrics.numActiveSeries.Inc()
+ var err error
+ series, err = a.getOrCreate(l)
+ if err != nil {
+ return 0, err
}
}
@@ -853,18 +870,35 @@ func (a *appender) Append(ref storage.SeriesRef, l labels.Labels, t int64, v flo
return storage.SeriesRef(series.ref), nil
}
-func (a *appender) getOrCreate(l labels.Labels) (series *memSeries, created bool) {
+func (a *appenderBase) getOrCreate(l labels.Labels) (series *memSeries, err error) {
+ // Ensure no empty or duplicate labels have gotten through. This mirrors the
+ // equivalent validation code in the TSDB's headAppender.
+ l = l.WithoutEmpty()
+ if l.IsEmpty() {
+ return nil, fmt.Errorf("empty labelset: %w", tsdb.ErrInvalidSample)
+ }
+
+ if lbl, dup := l.HasDuplicateLabelNames(); dup {
+ return nil, fmt.Errorf(`label name "%s" is not unique: %w`, lbl, tsdb.ErrInvalidSample)
+ }
+
hash := l.Hash()
series = a.series.GetByHash(hash, l)
if series != nil {
- return series, false
+ return series, nil
}
ref := chunks.HeadSeriesRef(a.nextRef.Inc())
series = &memSeries{ref: ref, lset: l, lastTs: math.MinInt64}
a.series.Set(hash, series)
- return series, true
+
+ a.pendingSeries = append(a.pendingSeries, record.RefSeries{
+ Ref: series.ref,
+ Labels: l,
+ })
+ a.metrics.numActiveSeries.Inc()
+ return series, nil
}
func (a *appender) AppendExemplar(ref storage.SeriesRef, _ labels.Labels, e exemplar.Exemplar) (storage.SeriesRef, error) {
@@ -879,47 +913,53 @@ func (a *appender) AppendExemplar(ref storage.SeriesRef, _ labels.Labels, e exem
// Ensure no empty labels have gotten through.
e.Labels = e.Labels.WithoutEmpty()
- if lbl, dup := e.Labels.HasDuplicateLabelNames(); dup {
- return 0, fmt.Errorf(`label name "%s" is not unique: %w`, lbl, tsdb.ErrInvalidExemplar)
- }
-
- // Exemplar label length does not include chars involved in text rendering such as quotes
- // equals sign, or commas. See definition of const ExemplarMaxLabelLength.
- labelSetLen := 0
- err := e.Labels.Validate(func(l labels.Label) error {
- labelSetLen += utf8.RuneCountInString(l.Name)
- labelSetLen += utf8.RuneCountInString(l.Value)
-
- if labelSetLen > exemplar.ExemplarMaxLabelSetLength {
- return storage.ErrExemplarLabelLength
+ if err := a.validateExemplar(s.ref, e); err != nil {
+ if errors.Is(err, storage.ErrDuplicateExemplar) {
+ // Duplicate, don't return an error but don't accept the exemplar.
+ return 0, nil
}
- return nil
- })
- if err != nil {
return 0, err
}
- // Check for duplicate vs last stored exemplar for this series, and discard those.
- // Otherwise, record the current exemplar as the latest.
- // Prometheus' TSDB returns 0 when encountering duplicates, so we do the same here.
- prevExemplar := a.series.GetLatestExemplar(s.ref)
- if prevExemplar != nil && prevExemplar.Equals(e) {
- // Duplicate, don't return an error but don't accept the exemplar.
- return 0, nil
- }
a.series.SetLatestExemplar(s.ref, &e)
-
a.pendingExamplars = append(a.pendingExamplars, record.RefExemplar{
Ref: s.ref,
T: e.Ts,
V: e.Value,
Labels: e.Labels,
})
-
a.metrics.totalAppendedExemplars.Inc()
return storage.SeriesRef(s.ref), nil
}
+func (a *appenderBase) validateExemplar(ref chunks.HeadSeriesRef, e exemplar.Exemplar) error {
+ if lbl, dup := e.Labels.HasDuplicateLabelNames(); dup {
+ return fmt.Errorf(`label name "%s" is not unique: %w`, lbl, tsdb.ErrInvalidExemplar)
+ }
+
+ // Exemplar label length does not include chars involved in text rendering such as quotes
+ // equals sign, or commas. See definition of const ExemplarMaxLabelLength.
+ labelSetLen := 0
+ if err := e.Labels.Validate(func(l labels.Label) error {
+ labelSetLen += utf8.RuneCountInString(l.Name)
+ labelSetLen += utf8.RuneCountInString(l.Value)
+ if labelSetLen > exemplar.ExemplarMaxLabelSetLength {
+ return storage.ErrExemplarLabelLength
+ }
+ return nil
+ }); err != nil {
+ return err
+ }
+ // Check for duplicate vs last stored exemplar for this series, and discard those.
+ // Otherwise, record the current exemplar as the latest.
+ // Prometheus' TSDB returns 0 when encountering duplicates, so we do the same here.
+ prevExemplar := a.series.GetLatestExemplar(ref)
+ if prevExemplar != nil && prevExemplar.Equals(e) {
+ return storage.ErrDuplicateExemplar
+ }
+ return nil
+}
+
func (a *appender) AppendHistogram(ref storage.SeriesRef, l labels.Labels, t int64, h *histogram.Histogram, fh *histogram.FloatHistogram) (storage.SeriesRef, error) {
if h != nil {
if err := h.Validate(); err != nil {
@@ -938,26 +978,10 @@ func (a *appender) AppendHistogram(ref storage.SeriesRef, l labels.Labels, t int
series := a.series.GetByID(headRef)
if series == nil {
- // Ensure no empty or duplicate labels have gotten through. This mirrors the
- // equivalent validation code in the TSDB's headAppender.
- l = l.WithoutEmpty()
- if l.IsEmpty() {
- return 0, fmt.Errorf("empty labelset: %w", tsdb.ErrInvalidSample)
- }
-
- if lbl, dup := l.HasDuplicateLabelNames(); dup {
- return 0, fmt.Errorf(`label name "%s" is not unique: %w`, lbl, tsdb.ErrInvalidSample)
- }
-
- var created bool
- series, created = a.getOrCreate(l)
- if created {
- a.pendingSeries = append(a.pendingSeries, record.RefSeries{
- Ref: series.ref,
- Labels: l,
- })
-
- a.metrics.numActiveSeries.Inc()
+ var err error
+ series, err = a.getOrCreate(l)
+ if err != nil {
+ return 0, err
}
}
@@ -997,7 +1021,7 @@ func (*appender) UpdateMetadata(storage.SeriesRef, labels.Labels, metadata.Metad
return 0, nil
}
-func (a *appender) AppendHistogramCTZeroSample(ref storage.SeriesRef, l labels.Labels, t, ct int64, h *histogram.Histogram, fh *histogram.FloatHistogram) (storage.SeriesRef, error) {
+func (a *appender) AppendHistogramSTZeroSample(ref storage.SeriesRef, l labels.Labels, t, st int64, h *histogram.Histogram, fh *histogram.FloatHistogram) (storage.SeriesRef, error) {
if h != nil {
if err := h.Validate(); err != nil {
return 0, err
@@ -1008,59 +1032,48 @@ func (a *appender) AppendHistogramCTZeroSample(ref storage.SeriesRef, l labels.L
return 0, err
}
}
- if ct >= t {
- return 0, storage.ErrCTNewerThanSample
+ if st >= t {
+ return 0, storage.ErrSTNewerThanSample
}
series := a.series.GetByID(chunks.HeadSeriesRef(ref))
if series == nil {
- // Ensure no empty labels have gotten through.
- l = l.WithoutEmpty()
- if l.IsEmpty() {
- return 0, fmt.Errorf("empty labelset: %w", tsdb.ErrInvalidSample)
- }
-
- if lbl, dup := l.HasDuplicateLabelNames(); dup {
- return 0, fmt.Errorf(`label name "%s" is not unique: %w`, lbl, tsdb.ErrInvalidSample)
- }
-
- var created bool
- series, created = a.getOrCreate(l)
- if created {
- a.pendingSeries = append(a.pendingSeries, record.RefSeries{
- Ref: series.ref,
- Labels: l,
- })
- a.metrics.numActiveSeries.Inc()
+ var err error
+ series, err = a.getOrCreate(l)
+ if err != nil {
+ return 0, err
}
}
series.Lock()
defer series.Unlock()
- if ct <= a.minValidTime(series.lastTs) {
- return 0, storage.ErrOutOfOrderCT
+ if st <= a.minValidTime(series.lastTs) {
+ return 0, storage.ErrOutOfOrderST
}
- if ct <= series.lastTs {
+ if st <= series.lastTs {
// discard the sample if it's out of order.
- return 0, storage.ErrOutOfOrderCT
+ return 0, storage.ErrOutOfOrderST
}
- series.lastTs = ct
+ // NOTE(bwplotka): This is a bug, as we "commit" pending sample TS as the WAL last TS. It was likely done
+ // to satisfy incorrect TestDBStartTimestampSamplesIngestion test. We are leaving it as-is given the planned removal
+ // of AppenderV1 as per https://github.com/prometheus/prometheus/issues/17632.
+ series.lastTs = st
switch {
case h != nil:
zeroHistogram := &histogram.Histogram{}
a.pendingHistograms = append(a.pendingHistograms, record.RefHistogramSample{
Ref: series.ref,
- T: ct,
+ T: st,
H: zeroHistogram,
})
a.histogramSeries = append(a.histogramSeries, series)
case fh != nil:
a.pendingFloatHistograms = append(a.pendingFloatHistograms, record.RefFloatHistogramSample{
Ref: series.ref,
- T: ct,
+ T: st,
FH: &histogram.FloatHistogram{},
})
a.floatHistogramSeries = append(a.floatHistogramSeries, series)
@@ -1070,32 +1083,18 @@ func (a *appender) AppendHistogramCTZeroSample(ref storage.SeriesRef, l labels.L
return storage.SeriesRef(series.ref), nil
}
-func (a *appender) AppendCTZeroSample(ref storage.SeriesRef, l labels.Labels, t, ct int64) (storage.SeriesRef, error) {
- if ct >= t {
- return 0, storage.ErrCTNewerThanSample
+func (a *appender) AppendSTZeroSample(ref storage.SeriesRef, l labels.Labels, t, st int64) (storage.SeriesRef, error) {
+ if st >= t {
+ return 0, storage.ErrSTNewerThanSample
}
series := a.series.GetByID(chunks.HeadSeriesRef(ref))
if series == nil {
- l = l.WithoutEmpty()
- if l.IsEmpty() {
- return 0, fmt.Errorf("empty labelset: %w", tsdb.ErrInvalidSample)
+ var err error
+ series, err = a.getOrCreate(l)
+ if err != nil {
+ return 0, err
}
-
- if lbl, dup := l.HasDuplicateLabelNames(); dup {
- return 0, fmt.Errorf(`label name "%s" is not unique: %w`, lbl, tsdb.ErrInvalidSample)
- }
-
- newSeries, created := a.getOrCreate(l)
- if created {
- a.pendingSeries = append(a.pendingSeries, record.RefSeries{
- Ref: newSeries.ref,
- Labels: l,
- })
- a.metrics.numActiveSeries.Inc()
- }
-
- series = newSeries
}
series.Lock()
@@ -1106,16 +1105,19 @@ func (a *appender) AppendCTZeroSample(ref storage.SeriesRef, l labels.Labels, t,
return 0, storage.ErrOutOfOrderSample
}
- if ct <= series.lastTs {
+ if st <= series.lastTs {
// discard the sample if it's out of order.
- return 0, storage.ErrOutOfOrderCT
+ return 0, storage.ErrOutOfOrderST
}
- series.lastTs = ct
+ // NOTE(bwplotka): This is a bug, as we "commit" pending sample TS as the WAL last TS. It was likely done
+ // to satisfy incorrect TestDBStartTimestampSamplesIngestion test. We are leaving it as-is given the planned removal
+ // of AppenderV1 as per https://github.com/prometheus/prometheus/issues/17632.
+ series.lastTs = st
// NOTE: always modify pendingSamples and sampleSeries together.
a.pendingSamples = append(a.pendingSamples, record.RefSample{
Ref: series.ref,
- T: ct,
+ T: st,
V: 0,
})
a.sampleSeries = append(a.sampleSeries, series)
@@ -1127,12 +1129,21 @@ func (a *appender) AppendCTZeroSample(ref storage.SeriesRef, l labels.Labels, t,
// Commit submits the collected samples and purges the batch.
func (a *appender) Commit() error {
+ defer a.appenderPool.Put(a)
+ return a.commit()
+}
+
+func (a *appender) Rollback() error {
+ defer a.appenderPool.Put(a)
+ return a.rollback()
+}
+
+func (a *appenderBase) commit() error {
if err := a.log(); err != nil {
return err
}
a.clearData()
- a.appenderPool.Put(a)
if a.writeNotified != nil {
a.writeNotified.Notify()
@@ -1141,7 +1152,7 @@ func (a *appender) Commit() error {
}
// log logs all pending data to the WAL.
-func (a *appender) log() error {
+func (a *appenderBase) log() error {
a.mtx.RLock()
defer a.mtx.RUnlock()
@@ -1235,7 +1246,7 @@ func (a *appender) log() error {
}
// clearData clears all pending data.
-func (a *appender) clearData() {
+func (a *appenderBase) clearData() {
a.pendingSeries = a.pendingSeries[:0]
a.pendingSamples = a.pendingSamples[:0]
a.pendingHistograms = a.pendingHistograms[:0]
@@ -1246,7 +1257,7 @@ func (a *appender) clearData() {
a.floatHistogramSeries = a.floatHistogramSeries[:0]
}
-func (a *appender) Rollback() error {
+func (a *appenderBase) rollback() error {
// Series are created in-memory regardless of rollback. This means we must
// log them to the WAL, otherwise subsequent commits may reference a series
// which was never written to the WAL.
@@ -1255,12 +1266,11 @@ func (a *appender) Rollback() error {
}
a.clearData()
- a.appenderPool.Put(a)
return nil
}
// logSeries logs only pending series records to the WAL.
-func (a *appender) logSeries() error {
+func (a *appenderBase) logSeries() error {
a.mtx.RLock()
defer a.mtx.RUnlock()
@@ -1283,7 +1293,7 @@ func (a *appender) logSeries() error {
// minValidTime returns the minimum timestamp that a sample can have
// and is needed for preventing underflow.
-func (a *appender) minValidTime(lastTs int64) int64 {
+func (a *appenderBase) minValidTime(lastTs int64) int64 {
if lastTs < math.MinInt64+a.opts.OutOfOrderTimeWindow {
return math.MinInt64
}
diff --git a/tsdb/agent/db_append_v2.go b/tsdb/agent/db_append_v2.go
new file mode 100644
index 0000000000..bb2601e1e3
--- /dev/null
+++ b/tsdb/agent/db_append_v2.go
@@ -0,0 +1,218 @@
+// 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 agent
+
+import (
+ "context"
+ "errors"
+ "fmt"
+
+ "github.com/prometheus/prometheus/model/exemplar"
+ "github.com/prometheus/prometheus/model/histogram"
+ "github.com/prometheus/prometheus/model/labels"
+ "github.com/prometheus/prometheus/model/value"
+ "github.com/prometheus/prometheus/storage"
+ "github.com/prometheus/prometheus/tsdb/chunks"
+ "github.com/prometheus/prometheus/tsdb/record"
+)
+
+// AppenderV2 implements storage.AppenderV2.
+func (db *DB) AppenderV2(context.Context) storage.AppenderV2 {
+ return db.appenderV2Pool.Get().(storage.AppenderV2)
+}
+
+type appenderV2 struct {
+ appenderBase
+}
+
+// Append appends pending sample to agent's DB.
+// TODO: Wire metadata in the Agent's appender.
+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, error) {
+ var (
+ // Avoid shadowing err variables for reliability.
+ valErr, partialErr error
+ sampleMetricType = sampleMetricTypeFloat
+ isStale bool
+ )
+ // Fail fast on incorrect histograms.
+ switch {
+ case fh != nil:
+ sampleMetricType = sampleMetricTypeHistogram
+ valErr = fh.Validate()
+ case h != nil:
+ sampleMetricType = sampleMetricTypeHistogram
+ valErr = h.Validate()
+ }
+ if valErr != nil {
+ return 0, valErr
+ }
+
+ // series references and chunk references are identical for agent mode.
+ s := a.series.GetByID(chunks.HeadSeriesRef(ref))
+ if s == nil {
+ var err error
+ s, err = a.getOrCreate(ls)
+ if err != nil {
+ return 0, err
+ }
+ }
+
+ s.Lock()
+ lastTS := s.lastTs
+ s.Unlock()
+
+ // TODO(bwplotka): Handle ST natively (as per PROM-60).
+ if a.opts.EnableSTAsZeroSample && st != 0 {
+ a.bestEffortAppendSTZeroSample(s, ls, lastTS, st, t, h, fh)
+ }
+
+ if t <= a.minValidTime(lastTS) {
+ a.metrics.totalOutOfOrderSamples.Inc()
+ return 0, storage.ErrOutOfOrderSample
+ }
+
+ switch {
+ case fh != nil:
+ isStale = value.IsStaleNaN(fh.Sum)
+ // NOTE: always modify pendingFloatHistograms and floatHistogramSeries together
+ a.pendingFloatHistograms = append(a.pendingFloatHistograms, record.RefFloatHistogramSample{
+ Ref: s.ref,
+ T: t,
+ FH: fh,
+ })
+ a.floatHistogramSeries = append(a.floatHistogramSeries, s)
+ case h != nil:
+ isStale = value.IsStaleNaN(h.Sum)
+ // NOTE: always modify pendingHistograms and histogramSeries together
+ a.pendingHistograms = append(a.pendingHistograms, record.RefHistogramSample{
+ Ref: s.ref,
+ T: t,
+ H: h,
+ })
+ a.histogramSeries = append(a.histogramSeries, s)
+ default:
+ isStale = value.IsStaleNaN(v)
+
+ // NOTE: always modify pendingSamples and sampleSeries together.
+ a.pendingSamples = append(a.pendingSamples, record.RefSample{
+ Ref: s.ref,
+ T: t,
+ V: v,
+ })
+ a.sampleSeries = append(a.sampleSeries, s)
+ }
+ a.metrics.totalAppendedSamples.WithLabelValues(sampleMetricType).Inc()
+ if isStale {
+ // For stale values we never attempt to process metadata/exemplars, claim the success.
+ return storage.SeriesRef(s.ref), nil
+ }
+
+ // Append exemplars if any and if storage was configured for it.
+ // TODO(bwplotka): Agent does not have equivalent of a.head.opts.EnableExemplarStorage && a.head.opts.MaxExemplars.Load() > 0 ?
+ if len(opts.Exemplars) > 0 {
+ // Currently only exemplars can return partial errors.
+ partialErr = a.appendExemplars(s, opts.Exemplars)
+ }
+ return storage.SeriesRef(s.ref), partialErr
+}
+
+func (a *appenderV2) Commit() error {
+ defer a.appenderV2Pool.Put(a)
+ return a.commit()
+}
+
+func (a *appenderV2) Rollback() error {
+ defer a.appenderV2Pool.Put(a)
+ return a.rollback()
+}
+
+func (a *appenderV2) appendExemplars(s *memSeries, exemplar []exemplar.Exemplar) error {
+ var errs []error
+ for _, e := range exemplar {
+ // Ensure no empty labels have gotten through.
+ e.Labels = e.Labels.WithoutEmpty()
+
+ if err := a.validateExemplar(s.ref, e); err != nil {
+ if !errors.Is(err, storage.ErrDuplicateExemplar) {
+ // Except duplicates, return partial errors.
+ errs = append(errs, err)
+ continue
+ }
+ if !errors.Is(err, storage.ErrOutOfOrderExemplar) {
+ a.logger.Debug("Error while adding an exemplar on AppendSample", "exemplars", fmt.Sprintf("%+v", e), "err", e)
+ }
+ continue
+ }
+
+ a.series.SetLatestExemplar(s.ref, &e)
+ a.pendingExamplars = append(a.pendingExamplars, record.RefExemplar{
+ Ref: s.ref,
+ T: e.Ts,
+ V: e.Value,
+ Labels: e.Labels,
+ })
+ a.metrics.totalAppendedExemplars.Inc()
+ }
+ if len(errs) > 0 {
+ return &storage.AppendPartialError{ExemplarErrors: errs}
+ }
+ return nil
+}
+
+// NOTE(bwplotka): This feature might be deprecated and removed once PROM-60
+// is implemented.
+//
+// ST is an experimental feature, we don't fail the append on errors, just debug log.
+func (a *appenderV2) bestEffortAppendSTZeroSample(s *memSeries, ls labels.Labels, lastTS, st, t int64, h *histogram.Histogram, fh *histogram.FloatHistogram) {
+ // NOTE: Use lset instead of s.lset to avoid locking memSeries. Using s.ref is acceptable without locking.
+ if st >= t {
+ a.logger.Debug("Error when appending ST", "series", ls.String(), "st", st, "t", t, "err", storage.ErrSTNewerThanSample)
+ return
+ }
+ if st <= lastTS {
+ a.logger.Debug("Error when appending ST", "series", ls.String(), "st", st, "t", t, "err", storage.ErrOutOfOrderST)
+ return
+ }
+
+ switch {
+ case fh != nil:
+ zeroFloatHistogram := &histogram.FloatHistogram{
+ // The STZeroSample represents a counter reset by definition.
+ CounterResetHint: histogram.CounterReset,
+ // Replicate other fields to avoid needless chunk creation.
+ Schema: fh.Schema,
+ ZeroThreshold: fh.ZeroThreshold,
+ CustomValues: fh.CustomValues,
+ }
+ a.pendingFloatHistograms = append(a.pendingFloatHistograms, record.RefFloatHistogramSample{Ref: s.ref, T: st, FH: zeroFloatHistogram})
+ a.floatHistogramSeries = append(a.floatHistogramSeries, s)
+ a.metrics.totalAppendedSamples.WithLabelValues(sampleMetricTypeHistogram).Inc()
+ case h != nil:
+ zeroHistogram := &histogram.Histogram{
+ // The STZeroSample represents a counter reset by definition.
+ CounterResetHint: histogram.CounterReset,
+ // Replicate other fields to avoid needless chunk creation.
+ Schema: h.Schema,
+ ZeroThreshold: h.ZeroThreshold,
+ CustomValues: h.CustomValues,
+ }
+ a.pendingHistograms = append(a.pendingHistograms, record.RefHistogramSample{Ref: s.ref, T: st, H: zeroHistogram})
+ a.histogramSeries = append(a.histogramSeries, s)
+ a.metrics.totalAppendedSamples.WithLabelValues(sampleMetricTypeHistogram).Inc()
+ default:
+ a.pendingSamples = append(a.pendingSamples, record.RefSample{Ref: s.ref, T: st, V: 0})
+ a.sampleSeries = append(a.sampleSeries, s)
+ a.metrics.totalAppendedSamples.WithLabelValues(sampleMetricTypeFloat).Inc()
+ }
+}
diff --git a/tsdb/agent/db_append_v2_test.go b/tsdb/agent/db_append_v2_test.go
new file mode 100644
index 0000000000..3e10a1163b
--- /dev/null
+++ b/tsdb/agent/db_append_v2_test.go
@@ -0,0 +1,1169 @@
+// 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 agent
+
+import (
+ "context"
+ "fmt"
+ "math"
+ "path/filepath"
+ "testing"
+ "time"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/prometheus/client_golang/prometheus"
+ "github.com/prometheus/common/model"
+ "github.com/prometheus/common/promslog"
+ "github.com/stretchr/testify/require"
+
+ "github.com/prometheus/prometheus/model/exemplar"
+ "github.com/prometheus/prometheus/model/histogram"
+ "github.com/prometheus/prometheus/model/labels"
+ "github.com/prometheus/prometheus/storage"
+ "github.com/prometheus/prometheus/storage/remote"
+ "github.com/prometheus/prometheus/tsdb"
+ "github.com/prometheus/prometheus/tsdb/chunks"
+ "github.com/prometheus/prometheus/tsdb/record"
+ "github.com/prometheus/prometheus/tsdb/tsdbutil"
+ "github.com/prometheus/prometheus/tsdb/wlog"
+ "github.com/prometheus/prometheus/util/testutil"
+)
+
+func TestDB_InvalidSeries_AppendV2(t *testing.T) {
+ s := createTestAgentDB(t, nil, DefaultOptions())
+ defer s.Close()
+
+ app := s.AppenderV2(context.Background())
+ t.Run("Samples", func(t *testing.T) {
+ _, err := app.Append(0, labels.Labels{}, 0, 0, 0, nil, nil, storage.AOptions{})
+ require.ErrorIs(t, err, tsdb.ErrInvalidSample, "should reject empty labels")
+
+ _, err = app.Append(0, labels.FromStrings("a", "1", "a", "2"), 0, 0, 0, nil, nil, storage.AOptions{})
+ require.ErrorIs(t, err, tsdb.ErrInvalidSample, "should reject duplicate labels")
+ })
+
+ t.Run("Histograms", func(t *testing.T) {
+ _, err := app.Append(0, labels.Labels{}, 0, 0, 0, tsdbutil.GenerateTestHistograms(1)[0], nil, storage.AOptions{})
+ require.ErrorIs(t, err, tsdb.ErrInvalidSample, "should reject empty labels")
+
+ _, err = app.Append(0, labels.FromStrings("a", "1", "a", "2"), 0, 0, 0, tsdbutil.GenerateTestHistograms(1)[0], nil, storage.AOptions{})
+ require.ErrorIs(t, err, tsdb.ErrInvalidSample, "should reject duplicate labels")
+ })
+
+ t.Run("Exemplars", func(t *testing.T) {
+ e := exemplar.Exemplar{Labels: labels.FromStrings("a", "1", "a", "2")}
+ _, err := app.Append(0, labels.FromStrings("a", "1"), 0, 0, 0, nil, nil, storage.AOptions{
+ Exemplars: []exemplar.Exemplar{e},
+ })
+ partErr := &storage.AppendPartialError{}
+ require.ErrorAs(t, err, &partErr)
+ require.Len(t, partErr.ExemplarErrors, 1)
+ require.ErrorIs(t, partErr.ExemplarErrors[0], tsdb.ErrInvalidExemplar, "should reject duplicate labels")
+
+ e = exemplar.Exemplar{Labels: labels.FromStrings("a_somewhat_long_trace_id", "nYJSNtFrFTY37VR7mHzEE/LIDt7cdAQcuOzFajgmLDAdBSRHYPDzrxhMA4zz7el8naI/AoXFv9/e/G0vcETcIoNUi3OieeLfaIRQci2oa")}
+ _, err = app.Append(0, labels.FromStrings("a", "2"), 0, 0, 0, nil, nil, storage.AOptions{
+ Exemplars: []exemplar.Exemplar{e},
+ })
+ partErr = &storage.AppendPartialError{}
+ require.ErrorAs(t, err, &partErr)
+ require.Len(t, partErr.ExemplarErrors, 1)
+ require.ErrorIs(t, partErr.ExemplarErrors[0], storage.ErrExemplarLabelLength, "should reject too long label length")
+
+ // Inverse check.
+ e = exemplar.Exemplar{Labels: labels.FromStrings("a", "1"), Value: 20, Ts: 10, HasTs: true}
+ _, err = app.Append(0, labels.FromStrings("a", "1"), 0, 0, 0, nil, nil, storage.AOptions{
+ Exemplars: []exemplar.Exemplar{e},
+ })
+ require.NoError(t, err, "should not reject valid exemplars")
+ })
+}
+
+func TestCommit_AppendV2(t *testing.T) {
+ const (
+ numDatapoints = 1000
+ numHistograms = 100
+ numSeries = 8
+ )
+
+ s := createTestAgentDB(t, nil, DefaultOptions())
+ app := s.AppenderV2(context.TODO())
+
+ lbls := labelsForTest(t.Name(), numSeries)
+ for _, l := range lbls {
+ lset := labels.New(l...)
+
+ for i := range numDatapoints {
+ sample := chunks.GenerateSamples(0, 1)
+ _, err := app.Append(0, lset, 0, sample[0].T(), sample[0].F(), nil, nil, storage.AOptions{
+ Exemplars: []exemplar.Exemplar{{
+ Labels: lset,
+ Ts: sample[0].T() + int64(i),
+ Value: sample[0].F(),
+ HasTs: true,
+ }},
+ })
+ require.NoError(t, err)
+ }
+ }
+
+ lbls = labelsForTest(t.Name()+"_histogram", numSeries)
+ for _, l := range lbls {
+ lset := labels.New(l...)
+
+ histograms := tsdbutil.GenerateTestHistograms(numHistograms)
+
+ for i := range numHistograms {
+ _, err := app.Append(0, lset, 0, int64(i), 0, histograms[i], nil, storage.AOptions{})
+ require.NoError(t, err)
+ }
+ }
+
+ lbls = labelsForTest(t.Name()+"_custom_buckets_histogram", numSeries)
+ for _, l := range lbls {
+ lset := labels.New(l...)
+
+ customBucketHistograms := tsdbutil.GenerateTestCustomBucketsHistograms(numHistograms)
+
+ for i := range numHistograms {
+ _, err := app.Append(0, lset, 0, int64(i), 0, customBucketHistograms[i], nil, storage.AOptions{})
+ require.NoError(t, err)
+ }
+ }
+
+ lbls = labelsForTest(t.Name()+"_float_histogram", numSeries)
+ for _, l := range lbls {
+ lset := labels.New(l...)
+
+ floatHistograms := tsdbutil.GenerateTestFloatHistograms(numHistograms)
+
+ for i := range numHistograms {
+ _, err := app.Append(0, lset, 0, int64(i), 0, nil, floatHistograms[i], storage.AOptions{})
+ require.NoError(t, err)
+ }
+ }
+
+ lbls = labelsForTest(t.Name()+"_custom_buckets_float_histogram", numSeries)
+ for _, l := range lbls {
+ lset := labels.New(l...)
+
+ customBucketFloatHistograms := tsdbutil.GenerateTestCustomBucketsFloatHistograms(numHistograms)
+
+ for i := range numHistograms {
+ _, err := app.Append(0, lset, 0, int64(i), 0, nil, customBucketFloatHistograms[i], storage.AOptions{})
+ require.NoError(t, err)
+ }
+ }
+
+ require.NoError(t, app.Commit())
+ require.NoError(t, s.Close())
+
+ sr, err := wlog.NewSegmentsReader(s.wal.Dir())
+ require.NoError(t, err)
+ defer func() {
+ require.NoError(t, sr.Close())
+ }()
+
+ // Read records from WAL and check for expected count of series, samples, and exemplars.
+ var (
+ r = wlog.NewReader(sr)
+ dec = record.NewDecoder(labels.NewSymbolTable(), promslog.NewNopLogger())
+
+ walSeriesCount, walSamplesCount, walExemplarsCount, walHistogramCount, walFloatHistogramCount int
+ )
+ for r.Next() {
+ rec := r.Record()
+ switch dec.Type(rec) {
+ case record.Series:
+ var series []record.RefSeries
+ series, err = dec.Series(rec, series)
+ require.NoError(t, err)
+ walSeriesCount += len(series)
+
+ case record.Samples:
+ var samples []record.RefSample
+ samples, err = dec.Samples(rec, samples)
+ require.NoError(t, err)
+ walSamplesCount += len(samples)
+
+ case record.HistogramSamples, record.CustomBucketsHistogramSamples:
+ var histograms []record.RefHistogramSample
+ histograms, err = dec.HistogramSamples(rec, histograms)
+ require.NoError(t, err)
+ walHistogramCount += len(histograms)
+
+ case record.FloatHistogramSamples, record.CustomBucketsFloatHistogramSamples:
+ var floatHistograms []record.RefFloatHistogramSample
+ floatHistograms, err = dec.FloatHistogramSamples(rec, floatHistograms)
+ require.NoError(t, err)
+ walFloatHistogramCount += len(floatHistograms)
+
+ case record.Exemplars:
+ var exemplars []record.RefExemplar
+ exemplars, err = dec.Exemplars(rec, exemplars)
+ require.NoError(t, err)
+ walExemplarsCount += len(exemplars)
+
+ default:
+ }
+ }
+
+ // Check that the WAL contained the same number of committed series/samples/exemplars.
+ require.Equal(t, numSeries*5, walSeriesCount, "unexpected number of series")
+ require.Equal(t, numSeries*numDatapoints, walSamplesCount, "unexpected number of samples")
+ require.Equal(t, numSeries*numDatapoints, walExemplarsCount, "unexpected number of exemplars")
+ require.Equal(t, numSeries*numHistograms*2, walHistogramCount, "unexpected number of histograms")
+ require.Equal(t, numSeries*numHistograms*2, walFloatHistogramCount, "unexpected number of float histograms")
+
+ // Check that we can still create both kinds of Appender - see https://github.com/prometheus/prometheus/issues/17800.
+ _ = s.Appender(context.TODO())
+ _ = s.AppenderV2(context.TODO())
+}
+
+func TestRollback_AppendV2(t *testing.T) {
+ const (
+ numDatapoints = 1000
+ numHistograms = 100
+ numSeries = 8
+ )
+
+ s := createTestAgentDB(t, nil, DefaultOptions())
+ app := s.AppenderV2(context.TODO())
+
+ lbls := labelsForTest(t.Name(), numSeries)
+ for _, l := range lbls {
+ lset := labels.New(l...)
+
+ for range numDatapoints {
+ sample := chunks.GenerateSamples(0, 1)
+ _, err := app.Append(0, lset, 0, sample[0].T(), sample[0].F(), nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ }
+ }
+
+ lbls = labelsForTest(t.Name()+"_histogram", numSeries)
+ for _, l := range lbls {
+ lset := labels.New(l...)
+
+ histograms := tsdbutil.GenerateTestHistograms(numHistograms)
+
+ for i := range numHistograms {
+ _, err := app.Append(0, lset, 0, int64(i), 0, histograms[i], nil, storage.AOptions{})
+ require.NoError(t, err)
+ }
+ }
+
+ lbls = labelsForTest(t.Name()+"_custom_buckets_histogram", numSeries)
+ for _, l := range lbls {
+ lset := labels.New(l...)
+
+ histograms := tsdbutil.GenerateTestCustomBucketsHistograms(numHistograms)
+
+ for i := range numHistograms {
+ _, err := app.Append(0, lset, 0, int64(i), 0, histograms[i], nil, storage.AOptions{})
+ require.NoError(t, err)
+ }
+ }
+
+ lbls = labelsForTest(t.Name()+"_float_histogram", numSeries)
+ for _, l := range lbls {
+ lset := labels.New(l...)
+
+ floatHistograms := tsdbutil.GenerateTestFloatHistograms(numHistograms)
+
+ for i := range numHistograms {
+ _, err := app.Append(0, lset, 0, int64(i), 0, nil, floatHistograms[i], storage.AOptions{})
+ require.NoError(t, err)
+ }
+ }
+
+ lbls = labelsForTest(t.Name()+"_custom_buckets_float_histogram", numSeries)
+ for _, l := range lbls {
+ lset := labels.New(l...)
+
+ floatHistograms := tsdbutil.GenerateTestCustomBucketsFloatHistograms(numHistograms)
+
+ for i := range numHistograms {
+ _, err := app.Append(0, lset, 0, int64(i), 0, nil, floatHistograms[i], storage.AOptions{})
+ require.NoError(t, err)
+ }
+ }
+
+ // Do a rollback, which should clear uncommitted data. A followup call to
+ // commit should persist nothing to the WAL.
+ require.NoError(t, app.Rollback())
+ require.NoError(t, app.Commit())
+ require.NoError(t, s.Close())
+
+ sr, err := wlog.NewSegmentsReader(s.wal.Dir())
+ require.NoError(t, err)
+ defer func() {
+ require.NoError(t, sr.Close())
+ }()
+
+ // Read records from WAL and check for expected count of series and samples.
+ var (
+ r = wlog.NewReader(sr)
+ dec = record.NewDecoder(labels.NewSymbolTable(), promslog.NewNopLogger())
+
+ walSeriesCount, walSamplesCount, walHistogramCount, walFloatHistogramCount, walExemplarsCount int
+ )
+ for r.Next() {
+ rec := r.Record()
+ switch dec.Type(rec) {
+ case record.Series:
+ var series []record.RefSeries
+ series, err = dec.Series(rec, series)
+ require.NoError(t, err)
+ walSeriesCount += len(series)
+
+ case record.Samples:
+ var samples []record.RefSample
+ samples, err = dec.Samples(rec, samples)
+ require.NoError(t, err)
+ walSamplesCount += len(samples)
+
+ case record.Exemplars:
+ var exemplars []record.RefExemplar
+ exemplars, err = dec.Exemplars(rec, exemplars)
+ require.NoError(t, err)
+ walExemplarsCount += len(exemplars)
+
+ case record.HistogramSamples, record.CustomBucketsHistogramSamples:
+ var histograms []record.RefHistogramSample
+ histograms, err = dec.HistogramSamples(rec, histograms)
+ require.NoError(t, err)
+ walHistogramCount += len(histograms)
+
+ case record.FloatHistogramSamples, record.CustomBucketsFloatHistogramSamples:
+ var floatHistograms []record.RefFloatHistogramSample
+ floatHistograms, err = dec.FloatHistogramSamples(rec, floatHistograms)
+ require.NoError(t, err)
+ walFloatHistogramCount += len(floatHistograms)
+
+ default:
+ }
+ }
+
+ // Check that only series get stored after calling Rollback.
+ require.Equal(t, numSeries*5, walSeriesCount, "series should have been written to WAL")
+ require.Equal(t, 0, walSamplesCount, "samples should not have been written to WAL")
+ require.Equal(t, 0, walExemplarsCount, "exemplars should not have been written to WAL")
+ require.Equal(t, 0, walHistogramCount, "histograms should not have been written to WAL")
+ require.Equal(t, 0, walFloatHistogramCount, "float histograms should not have been written to WAL")
+}
+
+func TestFullTruncateWAL_AppendV2(t *testing.T) {
+ const (
+ numDatapoints = 1000
+ numHistograms = 100
+ numSeries = 800
+ lastTs = 500
+ )
+
+ reg := prometheus.NewRegistry()
+ opts := DefaultOptions()
+ opts.TruncateFrequency = time.Minute * 2
+
+ s := createTestAgentDB(t, reg, opts)
+ defer func() {
+ require.NoError(t, s.Close())
+ }()
+ app := s.AppenderV2(context.TODO())
+
+ lbls := labelsForTest(t.Name(), numSeries)
+ for _, l := range lbls {
+ lset := labels.New(l...)
+
+ for range numDatapoints {
+ _, err := app.Append(0, lset, 0, int64(lastTs), 0, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ }
+ require.NoError(t, app.Commit())
+ }
+
+ lbls = labelsForTest(t.Name()+"_histogram", numSeries)
+ for _, l := range lbls {
+ lset := labels.New(l...)
+
+ histograms := tsdbutil.GenerateTestHistograms(numHistograms)
+
+ for i := range numHistograms {
+ _, err := app.Append(0, lset, 0, int64(lastTs), 0, histograms[i], nil, storage.AOptions{})
+ require.NoError(t, err)
+ }
+ require.NoError(t, app.Commit())
+ }
+
+ lbls = labelsForTest(t.Name()+"_custom_buckets_histogram", numSeries)
+ for _, l := range lbls {
+ lset := labels.New(l...)
+
+ histograms := tsdbutil.GenerateTestCustomBucketsHistograms(numHistograms)
+
+ for i := range numHistograms {
+ _, err := app.Append(0, lset, 0, int64(lastTs), 0, histograms[i], nil, storage.AOptions{})
+ require.NoError(t, err)
+ }
+ require.NoError(t, app.Commit())
+ }
+
+ lbls = labelsForTest(t.Name()+"_float_histogram", numSeries)
+ for _, l := range lbls {
+ lset := labels.New(l...)
+
+ floatHistograms := tsdbutil.GenerateTestFloatHistograms(numHistograms)
+
+ for i := range numHistograms {
+ _, err := app.Append(0, lset, 0, int64(lastTs), 0, nil, floatHistograms[i], storage.AOptions{})
+ require.NoError(t, err)
+ }
+ require.NoError(t, app.Commit())
+ }
+
+ lbls = labelsForTest(t.Name()+"_custom_buckets_float_histogram", numSeries)
+ for _, l := range lbls {
+ lset := labels.New(l...)
+
+ floatHistograms := tsdbutil.GenerateTestCustomBucketsFloatHistograms(numHistograms)
+
+ for i := range numHistograms {
+ _, err := app.Append(0, lset, 0, int64(lastTs), 0, nil, floatHistograms[i], storage.AOptions{})
+ require.NoError(t, err)
+ }
+ require.NoError(t, app.Commit())
+ }
+
+ // Truncate WAL with mint to GC all the samples.
+ s.truncate(lastTs + 1)
+
+ m := gatherFamily(t, reg, "prometheus_agent_deleted_series")
+ require.Equal(t, float64(numSeries*5), m.Metric[0].Gauge.GetValue(), "agent wal truncate mismatch of deleted series count")
+}
+
+func TestPartialTruncateWAL_AppendV2(t *testing.T) {
+ const (
+ numDatapoints = 1000
+ numSeries = 800
+ )
+
+ opts := DefaultOptions()
+
+ reg := prometheus.NewRegistry()
+ s := createTestAgentDB(t, reg, opts)
+ defer func() {
+ require.NoError(t, s.Close())
+ }()
+ app := s.AppenderV2(context.TODO())
+
+ // Create first batch of 800 series with 1000 data-points with a fixed lastTs as 500.
+ var lastTs int64 = 500
+ lbls := labelsForTest(t.Name()+"batch-1", numSeries)
+ for _, l := range lbls {
+ lset := labels.New(l...)
+
+ for range numDatapoints {
+ _, err := app.Append(0, lset, 0, lastTs, 0, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ }
+ require.NoError(t, app.Commit())
+ }
+
+ lbls = labelsForTest(t.Name()+"_histogram_batch-1", numSeries)
+ for _, l := range lbls {
+ lset := labels.New(l...)
+
+ histograms := tsdbutil.GenerateTestHistograms(numDatapoints)
+
+ for i := range numDatapoints {
+ _, err := app.Append(0, lset, 0, lastTs, 0, histograms[i], nil, storage.AOptions{})
+ require.NoError(t, err)
+ }
+ require.NoError(t, app.Commit())
+ }
+
+ lbls = labelsForTest(t.Name()+"_custom_buckets_histogram_batch-1", numSeries)
+ for _, l := range lbls {
+ lset := labels.New(l...)
+
+ histograms := tsdbutil.GenerateTestCustomBucketsHistograms(numDatapoints)
+
+ for i := range numDatapoints {
+ _, err := app.Append(0, lset, 0, lastTs, 0, histograms[i], nil, storage.AOptions{})
+ require.NoError(t, err)
+ }
+ require.NoError(t, app.Commit())
+ }
+
+ lbls = labelsForTest(t.Name()+"_float_histogram_batch-1", numSeries)
+ for _, l := range lbls {
+ lset := labels.New(l...)
+
+ floatHistograms := tsdbutil.GenerateTestFloatHistograms(numDatapoints)
+
+ for i := range numDatapoints {
+ _, err := app.Append(0, lset, 0, lastTs, 0, nil, floatHistograms[i], storage.AOptions{})
+ require.NoError(t, err)
+ }
+ require.NoError(t, app.Commit())
+ }
+
+ lbls = labelsForTest(t.Name()+"_custom_buckets_float_histogram_batch-1", numSeries)
+ for _, l := range lbls {
+ lset := labels.New(l...)
+
+ floatHistograms := tsdbutil.GenerateTestCustomBucketsFloatHistograms(numDatapoints)
+
+ for i := range numDatapoints {
+ _, err := app.Append(0, lset, 0, lastTs, 0, nil, floatHistograms[i], storage.AOptions{})
+ require.NoError(t, err)
+ }
+ require.NoError(t, app.Commit())
+ }
+
+ // Create second batch of 800 series with 1000 data-points with a fixed lastTs as 600.
+ lastTs = 600
+ lbls = labelsForTest(t.Name()+"batch-2", numSeries)
+ for _, l := range lbls {
+ lset := labels.New(l...)
+
+ for range numDatapoints {
+ _, err := app.Append(0, lset, 0, lastTs, 0, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ }
+ require.NoError(t, app.Commit())
+ }
+
+ lbls = labelsForTest(t.Name()+"_histogram_batch-2", numSeries)
+ for _, l := range lbls {
+ lset := labels.New(l...)
+
+ histograms := tsdbutil.GenerateTestHistograms(numDatapoints)
+
+ for i := range numDatapoints {
+ _, err := app.Append(0, lset, 0, lastTs, 0, histograms[i], nil, storage.AOptions{})
+ require.NoError(t, err)
+ }
+ require.NoError(t, app.Commit())
+ }
+
+ lbls = labelsForTest(t.Name()+"_custom_buckets_histogram_batch-2", numSeries)
+ for _, l := range lbls {
+ lset := labels.New(l...)
+
+ histograms := tsdbutil.GenerateTestCustomBucketsHistograms(numDatapoints)
+
+ for i := range numDatapoints {
+ _, err := app.Append(0, lset, 0, lastTs, 0, histograms[i], nil, storage.AOptions{})
+ require.NoError(t, err)
+ }
+ require.NoError(t, app.Commit())
+ }
+
+ lbls = labelsForTest(t.Name()+"_float_histogram_batch-2", numSeries)
+ for _, l := range lbls {
+ lset := labels.New(l...)
+
+ floatHistograms := tsdbutil.GenerateTestFloatHistograms(numDatapoints)
+
+ for i := range numDatapoints {
+ _, err := app.Append(0, lset, 0, lastTs, 0, nil, floatHistograms[i], storage.AOptions{})
+ require.NoError(t, err)
+ }
+ require.NoError(t, app.Commit())
+ }
+
+ lbls = labelsForTest(t.Name()+"_custom_buckets_float_histogram_batch-2", numSeries)
+ for _, l := range lbls {
+ lset := labels.New(l...)
+
+ floatHistograms := tsdbutil.GenerateTestCustomBucketsFloatHistograms(numDatapoints)
+
+ for i := range numDatapoints {
+ _, err := app.Append(0, lset, 0, lastTs, 0, nil, floatHistograms[i], storage.AOptions{})
+ require.NoError(t, err)
+ }
+ require.NoError(t, app.Commit())
+ }
+
+ // Truncate WAL with mint to GC only the first batch of 800 series and retaining 2nd batch of 800 series.
+ s.truncate(lastTs - 1)
+
+ m := gatherFamily(t, reg, "prometheus_agent_deleted_series")
+ require.Len(t, m.Metric, 1)
+ require.Equal(t, float64(numSeries*5), m.Metric[0].Gauge.GetValue(), "agent wal truncate mismatch of deleted series count")
+}
+
+func TestWALReplay_AppendV2(t *testing.T) {
+ const (
+ numDatapoints = 1000
+ numHistograms = 100
+ numSeries = 8
+ lastTs = 500
+ )
+
+ s := createTestAgentDB(t, nil, DefaultOptions())
+ app := s.AppenderV2(context.TODO())
+
+ lbls := labelsForTest(t.Name(), numSeries)
+ for _, l := range lbls {
+ lset := labels.New(l...)
+
+ for range numDatapoints {
+ _, err := app.Append(0, lset, 0, lastTs, 0, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ }
+ }
+
+ lbls = labelsForTest(t.Name()+"_histogram", numSeries)
+ for _, l := range lbls {
+ lset := labels.New(l...)
+
+ histograms := tsdbutil.GenerateTestHistograms(numHistograms)
+
+ for i := range numHistograms {
+ _, err := app.Append(0, lset, 0, lastTs, 0, histograms[i], nil, storage.AOptions{})
+ require.NoError(t, err)
+ }
+ }
+
+ lbls = labelsForTest(t.Name()+"_custom_buckets_histogram", numSeries)
+ for _, l := range lbls {
+ lset := labels.New(l...)
+
+ histograms := tsdbutil.GenerateTestCustomBucketsHistograms(numHistograms)
+
+ for i := range numHistograms {
+ _, err := app.Append(0, lset, 0, lastTs, 0, histograms[i], nil, storage.AOptions{})
+ require.NoError(t, err)
+ }
+ }
+
+ lbls = labelsForTest(t.Name()+"_float_histogram", numSeries)
+ for _, l := range lbls {
+ lset := labels.New(l...)
+
+ floatHistograms := tsdbutil.GenerateTestFloatHistograms(numHistograms)
+
+ for i := range numHistograms {
+ _, err := app.Append(0, lset, 0, lastTs, 0, nil, floatHistograms[i], storage.AOptions{})
+ require.NoError(t, err)
+ }
+ }
+
+ lbls = labelsForTest(t.Name()+"_custom_buckets_float_histogram", numSeries)
+ for _, l := range lbls {
+ lset := labels.New(l...)
+
+ floatHistograms := tsdbutil.GenerateTestCustomBucketsFloatHistograms(numHistograms)
+
+ for i := range numHistograms {
+ _, err := app.Append(0, lset, 0, lastTs, 0, nil, floatHistograms[i], storage.AOptions{})
+ require.NoError(t, err)
+ }
+ }
+
+ require.NoError(t, app.Commit())
+ require.NoError(t, s.Close())
+
+ // Hack: s.wal.Dir() is the /wal subdirectory of the original storage path.
+ // We need the original directory so we can recreate the storage for replay.
+ storageDir := filepath.Dir(s.wal.Dir())
+
+ reg := prometheus.NewRegistry()
+ replayStorage, err := Open(s.logger, reg, nil, storageDir, s.opts)
+ if err != nil {
+ t.Fatalf("unable to create storage for the agent: %v", err)
+ }
+ defer func() {
+ require.NoError(t, replayStorage.Close())
+ }()
+
+ // Check if all the series are retrieved back from the WAL.
+ m := gatherFamily(t, reg, "prometheus_agent_active_series")
+ require.Equal(t, float64(numSeries*5), m.Metric[0].Gauge.GetValue(), "agent wal replay mismatch of active series count")
+
+ // Check if lastTs of the samples retrieved from the WAL is retained.
+ metrics := replayStorage.series.series
+ for i := range metrics {
+ mp := metrics[i]
+ for _, v := range mp {
+ require.Equal(t, v.lastTs, int64(lastTs))
+ }
+ }
+}
+
+func Test_ExistingWAL_NextRef_AppendV2(t *testing.T) {
+ dbDir := t.TempDir()
+ rs := remote.NewStorage(promslog.NewNopLogger(), nil, startTime, dbDir, time.Second*30, nil, false)
+ defer func() {
+ require.NoError(t, rs.Close())
+ }()
+
+ db, err := Open(promslog.NewNopLogger(), nil, rs, dbDir, DefaultOptions())
+ require.NoError(t, err)
+
+ seriesCount := 10
+
+ // Append series
+ app := db.AppenderV2(context.Background())
+ for i := range seriesCount {
+ lset := labels.FromStrings(model.MetricNameLabel, fmt.Sprintf("series_%d", i))
+ _, err := app.Append(0, lset, 0, 0, 100, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ }
+
+ histogramCount := 10
+ histograms := tsdbutil.GenerateTestHistograms(histogramCount)
+ // Append series
+ for i := range histogramCount {
+ lset := labels.FromStrings(model.MetricNameLabel, fmt.Sprintf("histogram_%d", i))
+ _, err := app.Append(0, lset, 0, 0, 0, histograms[i], nil, storage.AOptions{})
+ require.NoError(t, err)
+ }
+ require.NoError(t, app.Commit())
+
+ // Truncate the WAL to force creation of a new segment.
+ require.NoError(t, db.truncate(0))
+ require.NoError(t, db.Close())
+
+ // Create a new storage and see what nextRef is initialized to.
+ db, err = Open(promslog.NewNopLogger(), nil, rs, dbDir, DefaultOptions())
+ require.NoError(t, err)
+ defer func() {
+ require.NoError(t, db.Close())
+ }()
+
+ require.Equal(t, uint64(seriesCount+histogramCount), db.nextRef.Load(), "nextRef should be equal to the number of series written across the entire WAL")
+}
+
+func TestStorage_DuplicateExemplarsIgnored_AppendV2(t *testing.T) {
+ s := createTestAgentDB(t, nil, DefaultOptions())
+ app := s.AppenderV2(context.Background())
+ defer s.Close()
+
+ // Write a few exemplars to our appender and call Commit().
+ // If the Labels, Value or Timestamp are different than the last exemplar,
+ // then a new one should be appended; Otherwise, it should be skipped.
+ e1 := exemplar.Exemplar{Labels: labels.FromStrings("a", "1"), Value: 20, Ts: 10, HasTs: true}
+ e2 := exemplar.Exemplar{Labels: labels.FromStrings("b", "2"), Value: 20, Ts: 10, HasTs: true}
+ e3 := exemplar.Exemplar{Labels: labels.FromStrings("b", "2"), Value: 42, Ts: 10, HasTs: true}
+ e4 := exemplar.Exemplar{Labels: labels.FromStrings("b", "2"), Value: 42, Ts: 25, HasTs: true}
+
+ _, err := app.Append(0, labels.FromStrings("a", "1"), 0, 0, 0, nil, nil, storage.AOptions{
+ Exemplars: []exemplar.Exemplar{e1, e1, e2, e2, e2, e3, e3, e4, e4},
+ })
+ require.NoError(t, err, "should not reject valid series")
+ require.NoError(t, app.Commit())
+
+ // Read back what was written to the WAL.
+ var walExemplarsCount int
+ sr, err := wlog.NewSegmentsReader(s.wal.Dir())
+ require.NoError(t, err)
+ defer sr.Close()
+ r := wlog.NewReader(sr)
+
+ dec := record.NewDecoder(labels.NewSymbolTable(), promslog.NewNopLogger())
+ for r.Next() {
+ rec := r.Record()
+ if dec.Type(rec) == record.Exemplars {
+ var exemplars []record.RefExemplar
+ exemplars, err = dec.Exemplars(rec, exemplars)
+ require.NoError(t, err)
+ walExemplarsCount += len(exemplars)
+ }
+ }
+
+ // We had 9 calls to AppendExemplar but only 4 of those should have gotten through.
+ require.Equal(t, 4, walExemplarsCount)
+}
+
+func TestDBAllowOOOSamples_AppendV2(t *testing.T) {
+ const (
+ numDatapoints = 5
+ numHistograms = 5
+ numSeries = 4
+ offset = 100
+ )
+
+ reg := prometheus.NewRegistry()
+ opts := DefaultOptions()
+ opts.OutOfOrderTimeWindow = math.MaxInt64
+ s := createTestAgentDB(t, reg, opts)
+ app := s.AppenderV2(context.TODO())
+
+ // Let's add some samples in the [offset, offset+numDatapoints) range.
+ lbls := labelsForTest(t.Name(), numSeries)
+ for _, l := range lbls {
+ lset := labels.New(l...)
+
+ for i := offset; i < numDatapoints+offset; i++ {
+ _, err := app.Append(0, lset, 0, int64(i), float64(i), nil, nil, storage.AOptions{
+ Exemplars: []exemplar.Exemplar{{
+ Labels: lset,
+ Ts: int64(i) * 2,
+ Value: float64(i),
+ HasTs: true,
+ }},
+ })
+ require.NoError(t, err)
+ }
+ }
+
+ lbls = labelsForTest(t.Name()+"_histogram", numSeries)
+ for _, l := range lbls {
+ lset := labels.New(l...)
+
+ histograms := tsdbutil.GenerateTestHistograms(numHistograms)
+
+ for i := offset; i < numDatapoints+offset; i++ {
+ _, err := app.Append(0, lset, 0, int64(i), 0, histograms[i-offset], nil, storage.AOptions{})
+ require.NoError(t, err)
+ }
+ }
+
+ lbls = labelsForTest(t.Name()+"_custom_buckets_histogram", numSeries)
+ for _, l := range lbls {
+ lset := labels.New(l...)
+
+ histograms := tsdbutil.GenerateTestCustomBucketsHistograms(numHistograms)
+
+ for i := offset; i < numDatapoints+offset; i++ {
+ _, err := app.Append(0, lset, 0, int64(i), 0, histograms[i-offset], nil, storage.AOptions{})
+ require.NoError(t, err)
+ }
+ }
+
+ lbls = labelsForTest(t.Name()+"_float_histogram", numSeries)
+ for _, l := range lbls {
+ lset := labels.New(l...)
+
+ floatHistograms := tsdbutil.GenerateTestFloatHistograms(numHistograms)
+
+ for i := offset; i < numDatapoints+offset; i++ {
+ _, err := app.Append(0, lset, 0, int64(i), 0, nil, floatHistograms[i-offset], storage.AOptions{})
+ require.NoError(t, err)
+ }
+ }
+
+ lbls = labelsForTest(t.Name()+"_custom_buckets_float_histogram", numSeries)
+ for _, l := range lbls {
+ lset := labels.New(l...)
+
+ floatHistograms := tsdbutil.GenerateTestCustomBucketsFloatHistograms(numHistograms)
+
+ for i := offset; i < numDatapoints+offset; i++ {
+ _, err := app.Append(0, lset, 0, int64(i), 0, nil, floatHistograms[i-offset], storage.AOptions{})
+ require.NoError(t, err)
+ }
+ }
+
+ require.NoError(t, app.Commit())
+ m := gatherFamily(t, reg, "prometheus_agent_samples_appended_total")
+ require.Equal(t, float64(20), m.Metric[0].Counter.GetValue(), "agent wal mismatch of total appended samples")
+ require.Equal(t, float64(80), m.Metric[1].Counter.GetValue(), "agent wal mismatch of total appended histograms")
+ require.NoError(t, s.Close())
+
+ // Hack: s.wal.Dir() is the /wal subdirectory of the original storage path.
+ // We need the original directory so we can recreate the storage for replay.
+ storageDir := filepath.Dir(s.wal.Dir())
+
+ // Replay the storage so that the lastTs for each series is recorded.
+ reg2 := prometheus.NewRegistry()
+ db, err := Open(s.logger, reg2, nil, storageDir, s.opts)
+ if err != nil {
+ t.Fatalf("unable to create storage for the agent: %v", err)
+ }
+
+ app = db.AppenderV2(context.Background())
+
+ // Now the lastTs will have been recorded successfully.
+ // Let's try appending twice as many OOO samples in the [0, numDatapoints) range.
+ lbls = labelsForTest(t.Name()+"_histogram", numSeries*2)
+ for _, l := range lbls {
+ lset := labels.New(l...)
+
+ for i := range numDatapoints {
+ _, err := app.Append(0, lset, 0, int64(i), float64(i), nil, nil, storage.AOptions{
+ Exemplars: []exemplar.Exemplar{{
+ Labels: lset,
+ Ts: int64(i) * 2,
+ Value: float64(i),
+ HasTs: true,
+ }},
+ })
+ require.NoError(t, err)
+ }
+ }
+
+ lbls = labelsForTest(t.Name()+"_histogram", numSeries*2)
+ for _, l := range lbls {
+ lset := labels.New(l...)
+
+ histograms := tsdbutil.GenerateTestHistograms(numHistograms)
+
+ for i := range numDatapoints {
+ _, err := app.Append(0, lset, 0, int64(i), 0, histograms[i], nil, storage.AOptions{})
+ require.NoError(t, err)
+ }
+ }
+
+ lbls = labelsForTest(t.Name()+"_custom_buckets_histogram", numSeries*2)
+ for _, l := range lbls {
+ lset := labels.New(l...)
+
+ histograms := tsdbutil.GenerateTestCustomBucketsHistograms(numHistograms)
+
+ for i := range numDatapoints {
+ _, err := app.Append(0, lset, 0, int64(i), 0, histograms[i], nil, storage.AOptions{})
+ require.NoError(t, err)
+ }
+ }
+
+ lbls = labelsForTest(t.Name()+"_float_histogram", numSeries*2)
+ for _, l := range lbls {
+ lset := labels.New(l...)
+
+ floatHistograms := tsdbutil.GenerateTestFloatHistograms(numHistograms)
+
+ for i := range numDatapoints {
+ _, err := app.Append(0, lset, 0, int64(i), 0, nil, floatHistograms[i], storage.AOptions{})
+ require.NoError(t, err)
+ }
+ }
+
+ lbls = labelsForTest(t.Name()+"_custom_buckets_float_histogram", numSeries*2)
+ for _, l := range lbls {
+ lset := labels.New(l...)
+
+ floatHistograms := tsdbutil.GenerateTestCustomBucketsFloatHistograms(numHistograms)
+
+ for i := range numDatapoints {
+ _, err := app.Append(0, lset, 0, int64(i), 0, nil, floatHistograms[i], storage.AOptions{})
+ require.NoError(t, err)
+ }
+ }
+
+ require.NoError(t, app.Commit())
+ m = gatherFamily(t, reg2, "prometheus_agent_samples_appended_total")
+ require.Equal(t, float64(40), m.Metric[0].Counter.GetValue(), "agent wal mismatch of total appended samples")
+ require.Equal(t, float64(160), m.Metric[1].Counter.GetValue(), "agent wal mismatch of total appended histograms")
+ require.NoError(t, db.Close())
+}
+
+func TestDBOutOfOrderTimeWindow_AppendV2(t *testing.T) {
+ tc := []struct {
+ outOfOrderTimeWindow, firstTs, secondTs int64
+ expectedError error
+ }{
+ {0, 100, 101, nil},
+ {0, 100, 100, storage.ErrOutOfOrderSample},
+ {0, 100, 99, storage.ErrOutOfOrderSample},
+ {100, 100, 1, nil},
+ {100, 100, 0, storage.ErrOutOfOrderSample},
+ }
+
+ for _, c := range tc {
+ t.Run(fmt.Sprintf("outOfOrderTimeWindow=%d, firstTs=%d, secondTs=%d, expectedError=%s", c.outOfOrderTimeWindow, c.firstTs, c.secondTs, c.expectedError), func(t *testing.T) {
+ reg := prometheus.NewRegistry()
+ opts := DefaultOptions()
+ opts.OutOfOrderTimeWindow = c.outOfOrderTimeWindow
+ s := createTestAgentDB(t, reg, opts)
+ app := s.AppenderV2(context.TODO())
+
+ lbls := labelsForTest(t.Name()+"_histogram", 1)
+ lset := labels.New(lbls[0]...)
+ _, err := app.Append(0, lset, 0, c.firstTs, 0, tsdbutil.GenerateTestHistograms(1)[0], nil, storage.AOptions{})
+ require.NoError(t, err)
+ err = app.Commit()
+ require.NoError(t, err)
+ _, err = app.Append(0, lset, 0, c.secondTs, 0, tsdbutil.GenerateTestHistograms(1)[0], nil, storage.AOptions{})
+ require.ErrorIs(t, err, c.expectedError)
+
+ lbls = labelsForTest(t.Name(), 1)
+ lset = labels.New(lbls[0]...)
+ _, err = app.Append(0, lset, 0, c.firstTs, 0, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ err = app.Commit()
+ require.NoError(t, err)
+ _, err = app.Append(0, lset, 0, c.secondTs, 0, nil, nil, storage.AOptions{})
+ require.ErrorIs(t, err, c.expectedError)
+
+ expectedAppendedSamples := float64(2)
+ if c.expectedError != nil {
+ expectedAppendedSamples = 1
+ }
+ m := gatherFamily(t, reg, "prometheus_agent_samples_appended_total")
+ require.Equal(t, expectedAppendedSamples, m.Metric[0].Counter.GetValue(), "agent wal mismatch of total appended samples")
+ require.Equal(t, expectedAppendedSamples, m.Metric[1].Counter.GetValue(), "agent wal mismatch of total appended histograms")
+ require.NoError(t, s.Close())
+ })
+ }
+}
+
+// TestDB_EnableSTZeroInjection_AppendV2 replaces TestDBStartTimestampSamplesIngestion.
+func TestDB_EnableSTZeroInjection_AppendV2(t *testing.T) {
+ t.Parallel()
+
+ // NOTE: Eventually wal sample and appendable sample should be the same.
+ type appendableSample struct {
+ st, t int64
+ v float64
+ lbls labels.Labels
+ h *histogram.Histogram
+ }
+
+ testHistograms := tsdbutil.GenerateTestHistograms(2)
+ zeroHistogram := &histogram.Histogram{
+ // The STZeroSample represents a counter reset by definition.
+ CounterResetHint: histogram.CounterReset,
+ // Replicate other fields to avoid needless chunk creation.
+ Schema: testHistograms[0].Schema,
+ ZeroThreshold: testHistograms[0].ZeroThreshold,
+ CustomValues: testHistograms[0].CustomValues,
+ }
+
+ lbls := labelsForTest(t.Name(), 1)
+ defLbls := labels.New(lbls[0]...)
+
+ testCases := []struct {
+ name string
+ inputSamples []appendableSample
+ expectedSamples []walSample
+ expectedSeriesCount int
+ }{
+ {
+ name: "in order ct+normal sample/floatSamples",
+ inputSamples: []appendableSample{
+ {t: 100, st: 1, v: 10, lbls: defLbls},
+ {t: 101, st: 1, v: 10, lbls: defLbls},
+ },
+ expectedSamples: []walSample{
+ {t: 1, f: 0, lbls: defLbls, ref: 1},
+ {t: 100, f: 10, lbls: defLbls, ref: 1},
+ {t: 101, f: 10, lbls: defLbls, ref: 1},
+ },
+ },
+ {
+ name: "ST+float && ST+histogram samples",
+ inputSamples: []appendableSample{
+ {
+ t: 100,
+ st: 30,
+ v: 20,
+ lbls: defLbls,
+ },
+ {
+ t: 300,
+ st: 230,
+ h: testHistograms[0],
+ lbls: defLbls,
+ },
+ },
+ expectedSamples: []walSample{
+ {t: 30, f: 0, lbls: defLbls, ref: 1},
+ {t: 100, f: 20, lbls: defLbls, ref: 1},
+ {t: 230, h: zeroHistogram, lbls: defLbls, ref: 1},
+ {t: 300, h: testHistograms[0], lbls: defLbls, ref: 1},
+ },
+ expectedSeriesCount: 1,
+ },
+ {
+ name: "ST+float && ST+histogram samples with error; should be ignored",
+ inputSamples: []appendableSample{
+ {
+ // invalid ST
+ t: 100,
+ st: 100,
+ v: 10,
+ lbls: defLbls,
+ },
+ {
+ // invalid ST histogram
+ t: 300,
+ st: 300,
+ h: testHistograms[0],
+ lbls: defLbls,
+ },
+ },
+ expectedSamples: []walSample{
+ {t: 100, f: 10, lbls: defLbls, ref: 1},
+ {t: 300, h: testHistograms[0], lbls: defLbls, ref: 1},
+ },
+ expectedSeriesCount: 0,
+ },
+ {
+ name: "In order ct+normal sample/histogram",
+ inputSamples: []appendableSample{
+ {t: 100, h: testHistograms[0], st: 1, lbls: defLbls},
+ {t: 101, h: testHistograms[1], st: 1, lbls: defLbls},
+ },
+ expectedSamples: []walSample{
+ {t: 1, h: zeroHistogram, lbls: defLbls, ref: 1},
+ {t: 100, h: testHistograms[0], lbls: defLbls, ref: 1},
+ {t: 101, h: testHistograms[1], lbls: defLbls, ref: 1},
+ },
+ },
+ {
+ name: "ct+normal then OOO sample/float",
+ inputSamples: []appendableSample{
+ {t: 60_000, st: 40_000, v: 10, lbls: defLbls},
+ {t: 120_000, st: 40_000, v: 10, lbls: defLbls},
+ {t: 180_000, st: 40_000, v: 10, lbls: defLbls},
+ {t: 50_000, st: 40_000, v: 10, lbls: defLbls},
+ },
+ expectedSamples: []walSample{
+ {t: 40_000, f: 0, lbls: defLbls, ref: 1},
+ {t: 60_000, f: 10, lbls: defLbls, ref: 1},
+ {t: 120_000, f: 10, lbls: defLbls, ref: 1},
+ {t: 180_000, f: 10, lbls: defLbls, ref: 1},
+ {t: 50_000, f: 10, lbls: defLbls, ref: 1}, // OOO sample.
+ },
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+
+ reg := prometheus.NewRegistry()
+ opts := DefaultOptions()
+ opts.OutOfOrderTimeWindow = 360_000
+ opts.EnableSTAsZeroSample = true
+ s := createTestAgentDB(t, reg, opts)
+
+ for _, sample := range tc.inputSamples {
+ // Simulate one sample per series logic we have in all our ingestion paths in Prometheus.
+ app := s.AppenderV2(t.Context())
+ _, err := app.Append(0, sample.lbls, sample.st, sample.t, sample.v, sample.h, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ }
+
+ // Close the DB to ensure all data is flushed to the WAL
+ require.NoError(t, s.Close())
+
+ // Check that we don't have any OOO samples in the WAL by checking metrics
+ families, err := reg.Gather()
+ require.NoError(t, err, "failed to gather metrics")
+ for _, f := range families {
+ if f.GetName() == "prometheus_agent_out_of_order_samples_total" {
+ t.Fatalf("unexpected metric %s", f.GetName())
+ }
+ }
+
+ got := readWALSamples(t, s.wal.Dir())
+ testutil.RequireEqualWithOptions(t, tc.expectedSamples, got, cmp.Options{cmp.AllowUnexported(walSample{})})
+ })
+ }
+}
diff --git a/tsdb/agent/db_test.go b/tsdb/agent/db_test.go
index c2674c8871..31e309d3fd 100644
--- a/tsdb/agent/db_test.go
+++ b/tsdb/agent/db_test.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
@@ -24,6 +24,7 @@ import (
"testing"
"time"
+ "github.com/google/go-cmp/cmp"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
"github.com/prometheus/common/model"
@@ -258,6 +259,9 @@ func TestCommit(t *testing.T) {
require.Equal(t, numSeries*numDatapoints, walExemplarsCount, "unexpected number of exemplars")
require.Equal(t, numSeries*numHistograms*2, walHistogramCount, "unexpected number of histograms")
require.Equal(t, numSeries*numHistograms*2, walFloatHistogramCount, "unexpected number of float histograms")
+
+ // Check that we can get another appender after this - see https://github.com/prometheus/prometheus/issues/17800.
+ _ = s.Appender(context.TODO())
}
func TestRollback(t *testing.T) {
@@ -1142,19 +1146,23 @@ type walSample struct {
ref storage.SeriesRef
}
-func TestDBCreatedTimestampSamplesIngestion(t *testing.T) {
+// NOTE(bwplotka): This test is testing behaviour of storage.Appender interface against its invariants (see
+// storage.Appender comment) around validation of the order of samples within a single Appender. This results
+// in a slight bug in AppendSTZero* methods. We are leaving it as-is given the planned removal of AppenderV1 as
+// per https://github.com/prometheus/prometheus/issues/17632.
+func TestDBStartTimestampSamplesIngestion(t *testing.T) {
t.Parallel()
type appendableSample struct {
t int64
- ct int64
+ st int64
v float64
lbls labels.Labels
h *histogram.Histogram
expectsError bool
}
- testHistogram := tsdbutil.GenerateTestHistograms(1)[0]
+ testHistograms := tsdbutil.GenerateTestHistograms(2)
zeroHistogram := &histogram.Histogram{}
lbls := labelsForTest(t.Name(), 1)
@@ -1163,97 +1171,97 @@ func TestDBCreatedTimestampSamplesIngestion(t *testing.T) {
testCases := []struct {
name string
inputSamples []appendableSample
- expectedSamples []*walSample
+ expectedSamples []walSample
expectedSeriesCount int
}{
{
name: "in order ct+normal sample/floatSamples",
inputSamples: []appendableSample{
- {t: 100, ct: 1, v: 10, lbls: defLbls},
- {t: 101, ct: 1, v: 10, lbls: defLbls},
+ {t: 100, st: 1, v: 10, lbls: defLbls},
+ {t: 101, st: 1, v: 10, lbls: defLbls},
},
- expectedSamples: []*walSample{
- {t: 1, f: 0, lbls: defLbls},
- {t: 100, f: 10, lbls: defLbls},
- {t: 101, f: 10, lbls: defLbls},
+ expectedSamples: []walSample{
+ {t: 1, f: 0, lbls: defLbls, ref: 1},
+ {t: 100, f: 10, lbls: defLbls, ref: 1},
+ {t: 101, f: 10, lbls: defLbls, ref: 1},
},
},
{
- name: "CT+float && CT+histogram samples",
+ name: "ST+float && ST+histogram samples",
inputSamples: []appendableSample{
{
t: 100,
- ct: 30,
+ st: 30,
v: 20,
lbls: defLbls,
},
{
t: 300,
- ct: 230,
- h: testHistogram,
+ st: 230,
+ h: testHistograms[0],
lbls: defLbls,
},
},
- expectedSamples: []*walSample{
- {t: 30, f: 0, lbls: defLbls},
- {t: 100, f: 20, lbls: defLbls},
- {t: 230, h: zeroHistogram, lbls: defLbls},
- {t: 300, h: testHistogram, lbls: defLbls},
+ expectedSamples: []walSample{
+ {t: 30, f: 0, lbls: defLbls, ref: 1},
+ {t: 100, f: 20, lbls: defLbls, ref: 1},
+ {t: 230, h: zeroHistogram, lbls: defLbls, ref: 1},
+ {t: 300, h: testHistograms[0], lbls: defLbls, ref: 1},
},
expectedSeriesCount: 1,
},
{
- name: "CT+float && CT+histogram samples with error",
+ name: "ST+float && ST+histogram samples with error",
inputSamples: []appendableSample{
{
- // invalid CT
+ // invalid ST
t: 100,
- ct: 100,
+ st: 100,
v: 10,
lbls: defLbls,
expectsError: true,
},
{
- // invalid CT histogram
+ // invalid ST histogram
t: 300,
- ct: 300,
- h: testHistogram,
+ st: 300,
+ h: testHistograms[0],
lbls: defLbls,
expectsError: true,
},
},
- expectedSamples: []*walSample{
- {t: 100, f: 10, lbls: defLbls},
- {t: 300, h: testHistogram, lbls: defLbls},
+ expectedSamples: []walSample{
+ {t: 100, f: 10, lbls: defLbls, ref: 1},
+ {t: 300, h: testHistograms[0], lbls: defLbls, ref: 1},
},
expectedSeriesCount: 0,
},
{
name: "In order ct+normal sample/histogram",
inputSamples: []appendableSample{
- {t: 100, h: testHistogram, ct: 1, lbls: defLbls},
- {t: 101, h: testHistogram, ct: 1, lbls: defLbls},
+ {t: 100, h: testHistograms[0], st: 1, lbls: defLbls},
+ {t: 101, h: testHistograms[1], st: 1, lbls: defLbls},
},
- expectedSamples: []*walSample{
- {t: 1, h: &histogram.Histogram{}},
- {t: 100, h: testHistogram},
- {t: 101, h: &histogram.Histogram{CounterResetHint: histogram.NotCounterReset}},
+ expectedSamples: []walSample{
+ {t: 1, h: &histogram.Histogram{}, lbls: defLbls, ref: 1},
+ {t: 100, h: testHistograms[0], lbls: defLbls, ref: 1},
+ {t: 101, h: testHistograms[1], lbls: defLbls, ref: 1},
},
},
{
name: "ct+normal then OOO sample/float",
inputSamples: []appendableSample{
- {t: 60_000, ct: 40_000, v: 10, lbls: defLbls},
- {t: 120_000, ct: 40_000, v: 10, lbls: defLbls},
- {t: 180_000, ct: 40_000, v: 10, lbls: defLbls},
- {t: 50_000, ct: 40_000, v: 10, lbls: defLbls},
+ {t: 60_000, st: 40_000, v: 10, lbls: defLbls},
+ {t: 120_000, st: 40_000, v: 10, lbls: defLbls},
+ {t: 180_000, st: 40_000, v: 10, lbls: defLbls},
+ {t: 50_000, st: 40_000, v: 10, lbls: defLbls},
},
- expectedSamples: []*walSample{
- {t: 40_000, f: 0, lbls: defLbls},
- {t: 50_000, f: 10, lbls: defLbls},
- {t: 60_000, f: 10, lbls: defLbls},
- {t: 120_000, f: 10, lbls: defLbls},
- {t: 180_000, f: 10, lbls: defLbls},
+ expectedSamples: []walSample{
+ {t: 40_000, f: 0, lbls: defLbls, ref: 1},
+ {t: 60_000, f: 10, lbls: defLbls, ref: 1},
+ {t: 120_000, f: 10, lbls: defLbls, ref: 1},
+ {t: 180_000, f: 10, lbls: defLbls, ref: 1},
+ {t: 50_000, f: 10, lbls: defLbls, ref: 1}, // OOO sample.
},
},
}
@@ -1271,8 +1279,8 @@ func TestDBCreatedTimestampSamplesIngestion(t *testing.T) {
for _, sample := range tc.inputSamples {
// We supposed to write a Histogram to the WAL
if sample.h != nil {
- _, err := app.AppendHistogramCTZeroSample(0, sample.lbls, sample.t, sample.ct, zeroHistogram, nil)
- if !errors.Is(err, storage.ErrOutOfOrderCT) {
+ _, err := app.AppendHistogramSTZeroSample(0, sample.lbls, sample.t, sample.st, zeroHistogram, nil)
+ if !errors.Is(err, storage.ErrOutOfOrderST) {
require.Equal(t, sample.expectsError, err != nil, "expected error: %v, got: %v", sample.expectsError, err)
}
@@ -1280,8 +1288,8 @@ func TestDBCreatedTimestampSamplesIngestion(t *testing.T) {
require.NoError(t, err)
} else {
// We supposed to write a float sample to the WAL
- _, err := app.AppendCTZeroSample(0, sample.lbls, sample.t, sample.ct)
- if !errors.Is(err, storage.ErrOutOfOrderCT) {
+ _, err := app.AppendSTZeroSample(0, sample.lbls, sample.t, sample.st)
+ if !errors.Is(err, storage.ErrOutOfOrderST) {
require.Equal(t, sample.expectsError, err != nil, "expected error: %v, got: %v", sample.expectsError, err)
}
@@ -1294,7 +1302,7 @@ func TestDBCreatedTimestampSamplesIngestion(t *testing.T) {
// Close the DB to ensure all data is flushed to the WAL
require.NoError(t, s.Close())
- // Check that we dont have any OOO samples in the WAL by checking metrics
+ // Check that we don't have any OOO samples in the WAL by checking metrics
families, err := reg.Gather()
require.NoError(t, err, "failed to gather metrics")
for _, f := range families {
@@ -1303,26 +1311,13 @@ func TestDBCreatedTimestampSamplesIngestion(t *testing.T) {
}
}
- outputSamples := readWALSamples(t, s.wal.Dir())
-
- require.Len(t, outputSamples, len(tc.expectedSamples), "Expected %d samples", len(tc.expectedSamples))
-
- for i, expectedSample := range tc.expectedSamples {
- for _, sample := range outputSamples {
- if sample.t == expectedSample.t && sample.lbls.String() == expectedSample.lbls.String() {
- if expectedSample.h != nil {
- require.Equal(t, expectedSample.h, sample.h, "histogram value mismatch (sample index %d)", i)
- } else {
- require.Equal(t, expectedSample.f, sample.f, "value mismatch (sample index %d)", i)
- }
- }
- }
- }
+ got := readWALSamples(t, s.wal.Dir())
+ testutil.RequireEqualWithOptions(t, tc.expectedSamples, got, cmp.Options{cmp.AllowUnexported(walSample{})})
})
}
}
-func readWALSamples(t *testing.T, walDir string) []*walSample {
+func readWALSamples(t *testing.T, walDir string) []walSample {
t.Helper()
sr, err := wlog.NewSegmentsReader(walDir)
require.NoError(t, err)
@@ -1339,7 +1334,7 @@ func readWALSamples(t *testing.T, walDir string) []*walSample {
histograms []record.RefHistogramSample
lastSeries record.RefSeries
- outputSamples = make([]*walSample, 0)
+ outputSamples = make([]walSample, 0)
)
for r.Next() {
@@ -1353,7 +1348,7 @@ func readWALSamples(t *testing.T, walDir string) []*walSample {
samples, err = dec.Samples(rec, samples[:0])
require.NoError(t, err)
for _, s := range samples {
- outputSamples = append(outputSamples, &walSample{
+ outputSamples = append(outputSamples, walSample{
t: s.T,
f: s.V,
lbls: lastSeries.Labels.Copy(),
@@ -1364,7 +1359,7 @@ func readWALSamples(t *testing.T, walDir string) []*walSample {
histograms, err = dec.HistogramSamples(rec, histograms[:0])
require.NoError(t, err)
for _, h := range histograms {
- outputSamples = append(outputSamples, &walSample{
+ outputSamples = append(outputSamples, walSample{
t: h.T,
h: h.H,
lbls: lastSeries.Labels.Copy(),
@@ -1373,14 +1368,14 @@ func readWALSamples(t *testing.T, walDir string) []*walSample {
}
}
}
-
return outputSamples
}
-func BenchmarkCreateSeries(b *testing.B) {
+func BenchmarkGetOrCreate(b *testing.B) {
s := createTestAgentDB(b, nil, DefaultOptions())
defer s.Close()
+ // NOTE: This benchmarks appenderBase, so it does not matter if it's V1 or V2.
app := s.Appender(context.Background()).(*appender)
lbls := make([]labels.Labels, b.N)
diff --git a/tsdb/agent/series.go b/tsdb/agent/series.go
index 76e7342171..4eb691bfd5 100644
--- a/tsdb/agent/series.go
+++ b/tsdb/agent/series.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
diff --git a/tsdb/agent/series_test.go b/tsdb/agent/series_test.go
index 036a80de4c..4b277b36b7 100644
--- a/tsdb/agent/series_test.go
+++ b/tsdb/agent/series_test.go
@@ -1,4 +1,4 @@
-// Copyright 2022 The Prometheus Authors
+// 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
diff --git a/tsdb/block.go b/tsdb/block.go
index 44c6ef5053..118dd672ef 100644
--- a/tsdb/block.go
+++ b/tsdb/block.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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.
@@ -33,7 +33,6 @@ import (
"github.com/prometheus/prometheus/storage"
"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"
"github.com/prometheus/prometheus/tsdb/tombstones"
@@ -102,11 +101,6 @@ type IndexReader interface {
// LabelNames returns all the unique label names present in the index in sorted order.
LabelNames(ctx context.Context, matchers ...*labels.Matcher) ([]string, error)
- // LabelValueFor returns label value for the given label name in the series referred to by ID.
- // If the series couldn't be found or the series doesn't have the requested label a
- // storage.ErrNotFound is returned as error.
- LabelValueFor(ctx context.Context, id storage.SeriesRef, label string) (string, error)
-
// LabelNamesFor returns all the label names for the series referred to by the postings.
// The names returned are sorted.
LabelNamesFor(ctx context.Context, postings index.Postings) ([]string, error)
@@ -233,6 +227,18 @@ func (bm *BlockMetaCompaction) FromOutOfOrder() bool {
return slices.Contains(bm.Hints, CompactionHintFromOutOfOrder)
}
+func (bm *BlockMetaCompaction) SetStaleSeries() {
+ if bm.FromStaleSeries() {
+ return
+ }
+ bm.Hints = append(bm.Hints, CompactionHintFromStaleSeries)
+ slices.Sort(bm.Hints)
+}
+
+func (bm *BlockMetaCompaction) FromStaleSeries() bool {
+ return slices.Contains(bm.Hints, CompactionHintFromStaleSeries)
+}
+
const (
indexFilename = "index"
metaFilename = "meta.json"
@@ -241,6 +247,10 @@ const (
// CompactionHintFromOutOfOrder is a hint noting that the block
// was created from out-of-order chunks.
CompactionHintFromOutOfOrder = "from-out-of-order"
+
+ // CompactionHintFromStaleSeries is a hint noting that the block
+ // was created from stale series.
+ CompactionHintFromStaleSeries = "from-stale-series"
)
func chunkDir(dir string) string { return filepath.Join(dir, "chunks") }
@@ -286,12 +296,12 @@ func writeMetaFile(logger *slog.Logger, dir string, meta *BlockMeta) (int64, err
n, err := f.Write(jsonMeta)
if err != nil {
- return 0, tsdb_errors.NewMulti(err, f.Close()).Err()
+ return 0, errors.Join(err, f.Close())
}
// Force the kernel to persist the file on disk to avoid data loss if the host crashes.
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 {
return 0, err
@@ -333,7 +343,7 @@ func OpenBlock(logger *slog.Logger, dir string, pool chunkenc.Pool, postingsDeco
var closers []io.Closer
defer func() {
if err != nil {
- err = tsdb_errors.NewMulti(err, tsdb_errors.CloseAll(closers)).Err()
+ err = errors.Join(err, closeAll(closers))
}
}()
meta, sizeMeta, err := readMetaFile(dir)
@@ -387,11 +397,11 @@ func (pb *Block) Close() error {
pb.pendingReaders.Wait()
- return tsdb_errors.NewMulti(
+ return errors.Join(
pb.chunkr.Close(),
pb.indexr.Close(),
pb.tombstones.Close(),
- ).Err()
+ )
}
func (pb *Block) String() string {
@@ -551,11 +561,6 @@ func (r blockIndexReader) Close() error {
return nil
}
-// LabelValueFor returns label value for the given label name in the series referred to by ID.
-func (r blockIndexReader) LabelValueFor(ctx context.Context, id storage.SeriesRef, label string) (string, error) {
- return r.ir.LabelValueFor(ctx, id, label)
-}
-
// LabelNamesFor returns all the label names for the series referred to by the postings.
// The names returned are sorted.
func (r blockIndexReader) LabelNamesFor(ctx context.Context, postings index.Postings) ([]string, error) {
diff --git a/tsdb/block_test.go b/tsdb/block_test.go
index d02f83a9e9..edd2df7415 100644
--- a/tsdb/block_test.go
+++ b/tsdb/block_test.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
@@ -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/blockwriter.go b/tsdb/blockwriter.go
index 14137f12cc..af83a98083 100644
--- a/tsdb/blockwriter.go
+++ b/tsdb/blockwriter.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
@@ -86,6 +86,12 @@ func (w *BlockWriter) Appender(ctx context.Context) storage.Appender {
return w.head.Appender(ctx)
}
+// AppenderV2 returns a new appender on the database.
+// AppenderV2 can't be called concurrently. However, the returned AppenderV2 can safely be used concurrently.
+func (w *BlockWriter) AppenderV2(ctx context.Context) storage.AppenderV2 {
+ return w.head.AppenderV2(ctx)
+}
+
// Flush implements the Writer interface. This is where actual block writing
// happens. After flush completes, no writes can be done.
func (w *BlockWriter) Flush(ctx context.Context) (ulid.ULID, error) {
diff --git a/tsdb/blockwriter_test.go b/tsdb/blockwriter_test.go
index e7c3146247..33f0e5a0f3 100644
--- a/tsdb/blockwriter_test.go
+++ b/tsdb/blockwriter_test.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
@@ -23,6 +23,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/prometheus/prometheus/model/labels"
+ "github.com/prometheus/prometheus/storage"
"github.com/prometheus/prometheus/tsdb/chunks"
)
@@ -59,3 +60,37 @@ func TestBlockWriter(t *testing.T) {
require.NoError(t, w.Close())
}
+
+func TestBlockWriter_AppenderV2(t *testing.T) {
+ ctx := context.Background()
+ outputDir := t.TempDir()
+ w, err := NewBlockWriter(promslog.NewNopLogger(), outputDir, DefaultBlockDuration)
+ require.NoError(t, err)
+
+ // Add some series.
+ app := w.AppenderV2(ctx)
+ ts1, v1 := int64(44), float64(7)
+ _, err = app.Append(0, labels.FromStrings("a", "b"), 0, ts1, v1, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ ts2, v2 := int64(55), float64(12)
+ _, err = app.Append(0, labels.FromStrings("c", "d"), 0, ts2, v2, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ id, err := w.Flush(ctx)
+ require.NoError(t, err)
+
+ // Confirm the block has the correct data.
+ blockpath := filepath.Join(outputDir, id.String())
+ b, err := OpenBlock(nil, blockpath, nil, nil)
+ require.NoError(t, err)
+ defer func() { require.NoError(t, b.Close()) }()
+ q, err := NewBlockQuerier(b, math.MinInt64, math.MaxInt64)
+ require.NoError(t, err)
+ series := query(t, q, labels.MustNewMatcher(labels.MatchRegexp, "", ".*"))
+ sample1 := []chunks.Sample{sample{t: ts1, f: v1}}
+ sample2 := []chunks.Sample{sample{t: ts2, f: v2}}
+ expectedSeries := map[string][]chunks.Sample{"{a=\"b\"}": sample1, "{c=\"d\"}": sample2}
+ require.Equal(t, expectedSeries, series)
+
+ require.NoError(t, w.Close())
+}
diff --git a/tsdb/chunkenc/bstream.go b/tsdb/chunkenc/bstream.go
index 6e01798f72..abf6e4dbef 100644
--- a/tsdb/chunkenc/bstream.go
+++ b/tsdb/chunkenc/bstream.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
diff --git a/tsdb/chunkenc/bstream_test.go b/tsdb/chunkenc/bstream_test.go
index 8ac45ef0b6..3098be5945 100644
--- a/tsdb/chunkenc/bstream_test.go
+++ b/tsdb/chunkenc/bstream_test.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
diff --git a/tsdb/chunkenc/chunk.go b/tsdb/chunkenc/chunk.go
index 8cccb189fa..711966ec39 100644
--- a/tsdb/chunkenc/chunk.go
+++ b/tsdb/chunkenc/chunk.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
@@ -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 eac9e12b29..41bb23ddd1 100644
--- a/tsdb/chunkenc/chunk_test.go
+++ b/tsdb/chunkenc/chunk_test.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
@@ -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 8002dd0d4e..6af2fa68e2 100644
--- a/tsdb/chunkenc/float_histogram.go
+++ b/tsdb/chunkenc/float_histogram.go
@@ -1,4 +1,4 @@
-// Copyright 2022 The Prometheus Authors
+// 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
@@ -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 {
@@ -884,7 +884,14 @@ func (it *floatHistogramIterator) AtFloatHistogram(fh *histogram.FloatHistogram)
// chunk is from a newer Prometheus version that supports higher
// resolution.
fh = fh.Copy()
- fh.ReduceResolution(histogram.ExponentialSchemaMax)
+ if err := fh.ReduceResolution(histogram.ExponentialSchemaMax); err != nil {
+ // With the checks above, this can only happen
+ // with invalid data in a chunk. As this is a
+ // rare edge case of a rare edge case, we'd
+ // rather not create all the plumbing to handle
+ // this error gracefully.
+ panic(err)
+ }
}
return it.t, fh
}
@@ -915,7 +922,13 @@ func (it *floatHistogramIterator) AtFloatHistogram(fh *histogram.FloatHistogram)
// This is a very slow path, but it should only happen if the
// chunk is from a newer Prometheus version that supports higher
// resolution.
- fh.ReduceResolution(histogram.ExponentialSchemaMax)
+ if err := fh.ReduceResolution(histogram.ExponentialSchemaMax); err != nil {
+ // With the checks above, this can only happen with
+ // invalid data in a chunk. As this is a rare edge case
+ // of a rare edge case, we'd rather not create all the
+ // plumbing to handle this error gracefully.
+ panic(err)
+ }
}
return it.t, fh
@@ -925,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 d112c81f1c..cbeb3171ce 100644
--- a/tsdb/chunkenc/float_histogram_test.go
+++ b/tsdb/chunkenc/float_histogram_test.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
@@ -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 cc1d771235..4e77f387d3 100644
--- a/tsdb/chunkenc/histogram.go
+++ b/tsdb/chunkenc/histogram.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
@@ -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 {
@@ -939,7 +939,14 @@ func (it *histogramIterator) AtHistogram(h *histogram.Histogram) (int64, *histog
// chunk is from a newer Prometheus version that supports higher
// resolution.
h = h.Copy()
- h.ReduceResolution(histogram.ExponentialSchemaMax)
+ if err := h.ReduceResolution(histogram.ExponentialSchemaMax); err != nil {
+ // With the checks above, this can only happen
+ // with invalid data in a chunk. As this is a
+ // rare edge case of a rare edge case, we'd
+ // rather not create all the plumbing to handle
+ // this error gracefully.
+ panic(err)
+ }
}
return it.t, h
}
@@ -970,7 +977,13 @@ func (it *histogramIterator) AtHistogram(h *histogram.Histogram) (int64, *histog
// This is a very slow path, but it should only happen if the
// chunk is from a newer Prometheus version that supports higher
// resolution.
- h.ReduceResolution(histogram.ExponentialSchemaMax)
+ if err := h.ReduceResolution(histogram.ExponentialSchemaMax); err != nil {
+ // With the checks above, this can only happen with
+ // invalid data in a chunk. As this is a rare edge case
+ // of a rare edge case, we'd rather not create all the
+ // plumbing to handle this error gracefully.
+ panic(err)
+ }
}
return it.t, h
@@ -1000,7 +1013,14 @@ func (it *histogramIterator) AtFloatHistogram(fh *histogram.FloatHistogram) (int
// chunk is from a newer Prometheus version that supports higher
// resolution.
fh = fh.Copy()
- fh.ReduceResolution(histogram.ExponentialSchemaMax)
+ if err := fh.ReduceResolution(histogram.ExponentialSchemaMax); err != nil {
+ // With the checks above, this can only happen
+ // with invalid data in a chunk. As this is a
+ // rare edge case of a rare edge case, we'd
+ // rather not create all the plumbing to handle
+ // this error gracefully.
+ panic(err)
+ }
}
return it.t, fh
}
@@ -1039,7 +1059,13 @@ func (it *histogramIterator) AtFloatHistogram(fh *histogram.FloatHistogram) (int
// This is a very slow path, but it should only happen if the
// chunk is from a newer Prometheus version that supports higher
// resolution.
- fh.ReduceResolution(histogram.ExponentialSchemaMax)
+ if err := fh.ReduceResolution(histogram.ExponentialSchemaMax); err != nil {
+ // With the checks above, this can only happen with
+ // invalid data in a chunk. As this is a rare edge case
+ // of a rare edge case, we'd rather not create all the
+ // plumbing to handle this error gracefully.
+ panic(err)
+ }
}
return it.t, fh
@@ -1049,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_meta.go b/tsdb/chunkenc/histogram_meta.go
index 22bc4a6d3d..874e086812 100644
--- a/tsdb/chunkenc/histogram_meta.go
+++ b/tsdb/chunkenc/histogram_meta.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
diff --git a/tsdb/chunkenc/histogram_meta_test.go b/tsdb/chunkenc/histogram_meta_test.go
index d3aa979b5e..3eb2a13962 100644
--- a/tsdb/chunkenc/histogram_meta_test.go
+++ b/tsdb/chunkenc/histogram_meta_test.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
diff --git a/tsdb/chunkenc/histogram_test.go b/tsdb/chunkenc/histogram_test.go
index c11102b470..6ac8500e64 100644
--- a/tsdb/chunkenc/histogram_test.go
+++ b/tsdb/chunkenc/histogram_test.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
@@ -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/varbit.go b/tsdb/chunkenc/varbit.go
index 00ba027dda..4338555328 100644
--- a/tsdb/chunkenc/varbit.go
+++ b/tsdb/chunkenc/varbit.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
diff --git a/tsdb/chunkenc/varbit_test.go b/tsdb/chunkenc/varbit_test.go
index 8042b98dc1..dcb43f08df 100644
--- a/tsdb/chunkenc/varbit_test.go
+++ b/tsdb/chunkenc/varbit_test.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
diff --git a/tsdb/chunkenc/xor.go b/tsdb/chunkenc/xor.go
index 29e2110705..5a9a59dc22 100644
--- a/tsdb/chunkenc/xor.go
+++ b/tsdb/chunkenc/xor.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
@@ -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 609a3ac5ea..b30c65283d 100644
--- a/tsdb/chunkenc/xor_test.go
+++ b/tsdb/chunkenc/xor_test.go
@@ -1,4 +1,4 @@
-// Copyright 2022 The Prometheus Authors
+// 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
@@ -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/chunk_write_queue.go b/tsdb/chunks/chunk_write_queue.go
index bb9f239707..a87c2602cd 100644
--- a/tsdb/chunks/chunk_write_queue.go
+++ b/tsdb/chunks/chunk_write_queue.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
@@ -111,10 +111,7 @@ func newChunkWriteQueue(reg prometheus.Registerer, size int, writeChunk writeChu
}
func (c *chunkWriteQueue) start() {
- c.workerWg.Add(1)
- go func() {
- defer c.workerWg.Done()
-
+ c.workerWg.Go(func() {
for {
job, ok := c.jobs.pop()
if !ok {
@@ -123,7 +120,7 @@ func (c *chunkWriteQueue) start() {
c.processJob(job)
}
- }()
+ })
c.isRunningMtx.Lock()
c.isRunning = true
diff --git a/tsdb/chunks/chunk_write_queue_test.go b/tsdb/chunks/chunk_write_queue_test.go
index fd81011091..489ff74210 100644
--- a/tsdb/chunks/chunk_write_queue_test.go
+++ b/tsdb/chunks/chunk_write_queue_test.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
diff --git a/tsdb/chunks/chunks.go b/tsdb/chunks/chunks.go
index 8b8f5d0f81..9b4e011562 100644
--- a/tsdb/chunks/chunks.go
+++ b/tsdb/chunks/chunks.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
@@ -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 all errors.
+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/chunks_test.go b/tsdb/chunks/chunks_test.go
index 6eb00f12ad..f40f996fde 100644
--- a/tsdb/chunks/chunks_test.go
+++ b/tsdb/chunks/chunks_test.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
diff --git a/tsdb/chunks/head_chunks.go b/tsdb/chunks/head_chunks.go
index 41fce69c72..809cd6b889 100644
--- a/tsdb/chunks/head_chunks.go
+++ b/tsdb/chunks/head_chunks.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
@@ -20,6 +20,7 @@ import (
"fmt"
"hash"
"io"
+ "math"
"os"
"path/filepath"
"slices"
@@ -31,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"
)
@@ -303,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
@@ -613,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())
}
}()
@@ -768,8 +768,25 @@ func (cdm *ChunkDiskMapper) Chunk(ref ChunkDiskMapperRef) (chunkenc.Chunk, error
}
}
+ if chkDataLen > uint64(math.MaxInt) {
+ return nil, &CorruptionErr{
+ Dir: cdm.dir.Name(),
+ FileIndex: sgmIndex,
+ Err: fmt.Errorf("chunk length %d exceeds supported size", chkDataLen),
+ }
+ }
+
+ chkDataLenInt := int(chkDataLen)
+ if chkDataLenStart > math.MaxInt-n-chkDataLenInt {
+ return nil, &CorruptionErr{
+ Dir: cdm.dir.Name(),
+ FileIndex: sgmIndex,
+ Err: fmt.Errorf("chunk data end overflows supported size (start=%d, len=%d, n=%d)", chkDataLenStart, chkDataLenInt, n),
+ }
+ }
+
// Verify the chunk data end.
- chkDataEnd := chkDataLenStart + n + int(chkDataLen)
+ chkDataEnd := chkDataLenStart + n + chkDataLenInt
if chkDataEnd > mmapFile.byteSlice.Len() {
return nil, &CorruptionErr{
Dir: cdm.dir.Name(),
@@ -952,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()
@@ -961,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.
@@ -985,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.
@@ -1080,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_other.go b/tsdb/chunks/head_chunks_other.go
index f30c5e55e9..42e94fc54d 100644
--- a/tsdb/chunks/head_chunks_other.go
+++ b/tsdb/chunks/head_chunks_other.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
diff --git a/tsdb/chunks/head_chunks_test.go b/tsdb/chunks/head_chunks_test.go
index 2d7744193d..c3cbc5a618 100644
--- a/tsdb/chunks/head_chunks_test.go
+++ b/tsdb/chunks/head_chunks_test.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
@@ -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/head_chunks_windows.go b/tsdb/chunks/head_chunks_windows.go
index 214ee42f59..a16d0ff38e 100644
--- a/tsdb/chunks/head_chunks_windows.go
+++ b/tsdb/chunks/head_chunks_windows.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
diff --git a/tsdb/chunks/queue.go b/tsdb/chunks/queue.go
index 860381a5fe..454d939ce6 100644
--- a/tsdb/chunks/queue.go
+++ b/tsdb/chunks/queue.go
@@ -1,4 +1,4 @@
-// Copyright 2022 The Prometheus Authors
+// 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
diff --git a/tsdb/chunks/queue_test.go b/tsdb/chunks/queue_test.go
index ab4dd14838..2e3fff59a8 100644
--- a/tsdb/chunks/queue_test.go
+++ b/tsdb/chunks/queue_test.go
@@ -1,4 +1,4 @@
-// Copyright 2022 The Prometheus Authors
+// 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
@@ -269,34 +269,26 @@ func TestQueuePushPopManyGoroutines(t *testing.T) {
readersWG := sync.WaitGroup{}
for range readGoroutines {
- readersWG.Add(1)
-
- go func() {
- defer readersWG.Done()
-
+ readersWG.Go(func() {
for j, ok := queue.pop(); ok; j, ok = queue.pop() {
refsMx.Lock()
refs[j.seriesRef] = true
refsMx.Unlock()
}
- }()
+ })
}
id := atomic.Uint64{}
writersWG := sync.WaitGroup{}
for range writeGoroutines {
- writersWG.Add(1)
-
- go func() {
- defer writersWG.Done()
-
+ writersWG.Go(func() {
for range writes {
ref := id.Inc()
require.True(t, queue.push(chunkWriteJob{seriesRef: HeadSeriesRef(ref)}))
}
- }()
+ })
}
// Wait until all writes are done.
diff --git a/tsdb/chunks/samples.go b/tsdb/chunks/samples.go
index a5b16094df..280f2dd606 100644
--- a/tsdb/chunks/samples.go
+++ b/tsdb/chunks/samples.go
@@ -1,4 +1,4 @@
-// Copyright 2023 The Prometheus Authors
+// 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
@@ -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.go b/tsdb/compact.go
index 49e88d6320..7091d34d50 100644
--- a/tsdb/compact.go
+++ b/tsdb/compact.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
@@ -32,7 +32,6 @@ import (
"github.com/prometheus/prometheus/storage"
"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"
"github.com/prometheus/prometheus/tsdb/tombstones"
@@ -87,6 +86,7 @@ type LeveledCompactor struct {
maxBlockChunkSegmentSize int64
useUncachedIO bool
mergeFunc storage.VerticalChunkSeriesMergeFunc
+ blockExcludeFunc BlockExcludeFilterFunc
postingsEncoder index.PostingsEncoder
postingsDecoderFactory PostingsDecoderFactory
enableOverlappingCompaction bool
@@ -160,16 +160,24 @@ type LeveledCompactorOptions struct {
// PE specifies the postings encoder. It is called when compactor is writing out the postings for a label name/value pair during compaction.
// If it is nil then the default encoder is used. At the moment that is the "raw" encoder. See index.EncodePostingsRaw for more.
PE index.PostingsEncoder
+
// PD specifies the postings decoder factory to return different postings decoder based on BlockMeta. It is called when opening a block or opening the index file.
// If it is nil then a default decoder is used, compatible with Prometheus v2.
PD PostingsDecoderFactory
+
// MaxBlockChunkSegmentSize is the max block chunk segment size. If it is 0 then the default chunks.DefaultChunkSegmentSize is used.
MaxBlockChunkSegmentSize int64
+
// MergeFunc is used for merging series together in vertical compaction. By default storage.NewCompactingChunkSeriesMerger(storage.ChainedSeriesMerge) is used.
MergeFunc storage.VerticalChunkSeriesMergeFunc
+
+ // BlockExcludeFilter is used to decide which blocks are exluded from compactions.
+ BlockExcludeFilter BlockExcludeFilterFunc
+
// EnableOverlappingCompaction enables compaction of overlapping blocks. In Prometheus it is always enabled.
// It is useful for downstream projects like Mimir, Cortex, Thanos where they have a separate component that does compaction.
EnableOverlappingCompaction bool
+
// Metrics is set of metrics for Compactor. By default, NewCompactorMetrics would be called to initialize metrics unless it is provided.
Metrics *CompactorMetrics
// UseUncachedIO allows bypassing the page cache when appropriate.
@@ -178,7 +186,9 @@ type LeveledCompactorOptions struct {
type PostingsDecoderFactory func(meta *BlockMeta) index.PostingsDecoder
-func DefaultPostingsDecoderFactory(*BlockMeta) index.PostingsDecoder {
+type BlockExcludeFilterFunc func(meta *BlockMeta) bool
+
+func DefaultPostingsDecoderFactory(_ *BlockMeta) index.PostingsDecoder {
return index.DecodePostingsRaw
}
@@ -226,6 +236,7 @@ func NewLeveledCompactorWithOptions(ctx context.Context, r prometheus.Registerer
postingsEncoder: pe,
postingsDecoderFactory: opts.PD,
enableOverlappingCompaction: opts.EnableOverlappingCompaction,
+ blockExcludeFunc: opts.BlockExcludeFilter,
}, nil
}
@@ -250,12 +261,26 @@ func (c *LeveledCompactor) Plan(dir string) ([]string, error) {
if err != nil {
return nil, err
}
+ if c.blockExcludeFunc != nil && c.blockExcludeFunc(meta) {
+ // Compactions work from oldest to newest, uploads do the same (usually).
+ // If you continue here you'll skip compactions on this one block, but:
+ // * all further blocks are NOT yet uploaded
+ // * some or all further blocks are uploaded
+ //
+ // If we continue and there are newer blocks to pick from,
+ // then you will compact in a non-continuous way, leaving gaps of individual un-compacted blocks.
+ break
+ }
dms = append(dms, dirMeta{dir, meta})
}
return c.plan(dms)
}
func (c *LeveledCompactor) plan(dms []dirMeta) ([]string, error) {
+ if len(dms) == 0 {
+ return nil, nil
+ }
+
slices.SortFunc(dms, func(a, b dirMeta) int {
switch {
case a.meta.MinTime < b.meta.MinTime:
@@ -546,16 +571,16 @@ func (c *LeveledCompactor) CompactWithBlockPopulator(dest string, dirs []string,
return []ulid.ULID{uid}, nil
}
- errs := tsdb_errors.NewMulti(err)
+ errs := []error{err}
if !errors.Is(err, context.Canceled) {
for _, b := range bs {
if err := b.setCompactionFailed(); err != nil {
- errs.Add(fmt.Errorf("setting compaction failed for block: %s: %w", b.Dir(), err))
+ errs = append(errs, fmt.Errorf("setting compaction failed for block: %s: %w", b.Dir(), err))
}
}
}
- return nil, errs.Err()
+ return nil, errors.Join(errs...)
}
func (c *LeveledCompactor) Write(dest string, b BlockReader, mint, maxt int64, base *BlockMeta) ([]ulid.ULID, error) {
@@ -579,6 +604,9 @@ func (c *LeveledCompactor) Write(dest string, b BlockReader, mint, maxt int64, b
if base.Compaction.FromOutOfOrder() {
meta.Compaction.SetOutOfOrder()
}
+ if base.Compaction.FromStaleSeries() {
+ meta.Compaction.SetStaleSeries()
+ }
}
err := c.write(dest, meta, DefaultBlockPopulator{}, b)
@@ -632,7 +660,7 @@ func (c *LeveledCompactor) write(dest string, meta *BlockMeta, blockPopulator Bl
tmp := dir + tmpForCreationBlockDirSuffix
var closers []io.Closer
defer func(t time.Time) {
- err = tsdb_errors.NewMulti(err, tsdb_errors.CloseAll(closers)).Err()
+ err = errors.Join(err, closeAll(closers))
// RemoveAll returns no error when tmp doesn't exist so it is safe to always run it.
if err := os.RemoveAll(tmp); err != nil {
@@ -689,13 +717,13 @@ func (c *LeveledCompactor) write(dest string, meta *BlockMeta, blockPopulator Bl
// though these are covered under defer. This is because in Windows,
// you cannot delete these unless they are closed and the defer is to
// make sure they are closed if the function exits due to an error above.
- errs := tsdb_errors.NewMulti()
+ var errs []error
for _, w := range closers {
- errs.Add(w.Close())
+ errs = append(errs, w.Close())
}
closers = closers[:0] // Avoid closing the writers twice in the defer.
- if errs.Err() != nil {
- return errs.Err()
+ if err := errors.Join(errs...); err != nil {
+ return err
}
// Populated block is empty, so exit early.
@@ -774,11 +802,9 @@ func (DefaultBlockPopulator) PopulateBlock(ctx context.Context, metrics *Compact
overlapping bool
)
defer func() {
- errs := tsdb_errors.NewMulti(err)
- if cerr := tsdb_errors.CloseAll(closers); cerr != nil {
- errs.Add(fmt.Errorf("close: %w", cerr))
+ if cerr := closeAll(closers); cerr != nil {
+ err = errors.Join(err, fmt.Errorf("close: %w", cerr))
}
- err = errs.Err()
metrics.PopulatingBlocks.Set(0)
}()
metrics.PopulatingBlocks.Set(1)
diff --git a/tsdb/compact_test.go b/tsdb/compact_test.go
index 203a04dec8..44a0921eec 100644
--- a/tsdb/compact_test.go
+++ b/tsdb/compact_test.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
@@ -173,214 +173,274 @@ func TestNoPanicFor0Tombstones(t *testing.T) {
c.plan(metas)
}
-func TestLeveledCompactor_plan(t *testing.T) {
- // This mimics our default ExponentialBlockRanges with min block size equals to 20.
- compactor, err := NewLeveledCompactor(context.Background(), nil, nil, []int64{
- 20,
- 60,
- 180,
- 540,
- 1620,
- }, nil, nil)
- require.NoError(t, err)
+func TestLeveledCompactor(t *testing.T) {
+ // Tests for the private plan() method.
+ t.Run("plan", func(t *testing.T) {
+ // This mimics our default ExponentialBlockRanges with min block size equals to 20.
+ compactor, err := NewLeveledCompactor(context.Background(), nil, nil, []int64{
+ 20,
+ 60,
+ 180,
+ 540,
+ 1620,
+ }, nil, nil)
+ require.NoError(t, err)
- cases := map[string]struct {
- metas []dirMeta
- expected []string
- }{
- "Outside Range": {
- metas: []dirMeta{
- metaRange("1", 0, 20, nil),
+ cases := map[string]struct {
+ metas []dirMeta
+ expected []string
+ }{
+ "Outside Range": {
+ metas: []dirMeta{
+ metaRange("1", 0, 20, nil),
+ },
+ expected: nil,
},
- expected: nil,
- },
- "We should wait for four blocks of size 20 to appear before compacting.": {
- metas: []dirMeta{
- metaRange("1", 0, 20, nil),
- metaRange("2", 20, 40, nil),
+ "We should wait for four blocks of size 20 to appear before compacting.": {
+ metas: []dirMeta{
+ metaRange("1", 0, 20, nil),
+ metaRange("2", 20, 40, nil),
+ },
+ expected: nil,
},
- expected: nil,
- },
- `We should wait for a next block of size 20 to appear before compacting
- the existing ones. We have three, but we ignore the fresh one from WAl`: {
- metas: []dirMeta{
- metaRange("1", 0, 20, nil),
- metaRange("2", 20, 40, nil),
- metaRange("3", 40, 60, nil),
+ `We should wait for a next block of size 20 to appear before compacting
+ the existing ones. We have three, but we ignore the fresh one from WAl`: {
+ metas: []dirMeta{
+ metaRange("1", 0, 20, nil),
+ metaRange("2", 20, 40, nil),
+ metaRange("3", 40, 60, nil),
+ },
+ expected: nil,
},
- expected: nil,
- },
- "Block to fill the entire parent range appeared – should be compacted": {
- metas: []dirMeta{
- metaRange("1", 0, 20, nil),
- metaRange("2", 20, 40, nil),
- metaRange("3", 40, 60, nil),
- metaRange("4", 60, 80, nil),
+ "Block to fill the entire parent range appeared – should be compacted": {
+ metas: []dirMeta{
+ metaRange("1", 0, 20, nil),
+ metaRange("2", 20, 40, nil),
+ metaRange("3", 40, 60, nil),
+ metaRange("4", 60, 80, nil),
+ },
+ expected: []string{"1", "2", "3"},
},
- expected: []string{"1", "2", "3"},
- },
- `Block for the next parent range appeared with gap with size 20. Nothing will happen in the first one
- anymore but we ignore fresh one still, so no compaction`: {
- metas: []dirMeta{
- metaRange("1", 0, 20, nil),
- metaRange("2", 20, 40, nil),
- metaRange("3", 60, 80, nil),
+ `Block for the next parent range appeared with gap with size 20. Nothing will happen in the first one
+ anymore but we ignore fresh one still, so no compaction`: {
+ metas: []dirMeta{
+ metaRange("1", 0, 20, nil),
+ metaRange("2", 20, 40, nil),
+ metaRange("3", 60, 80, nil),
+ },
+ expected: nil,
},
- expected: nil,
- },
- `Block for the next parent range appeared, and we have a gap with size 20 between second and third block.
- We will not get this missed gap anymore and we should compact just these two.`: {
- metas: []dirMeta{
- metaRange("1", 0, 20, nil),
- metaRange("2", 20, 40, nil),
- metaRange("3", 60, 80, nil),
- metaRange("4", 80, 100, nil),
+ `Block for the next parent range appeared, and we have a gap with size 20 between second and third block.
+ We will not get this missed gap anymore and we should compact just these two.`: {
+ metas: []dirMeta{
+ metaRange("1", 0, 20, nil),
+ metaRange("2", 20, 40, nil),
+ metaRange("3", 60, 80, nil),
+ metaRange("4", 80, 100, nil),
+ },
+ expected: []string{"1", "2"},
},
- expected: []string{"1", "2"},
- },
- "We have 20, 20, 20, 60, 60 range blocks. '5' is marked as fresh one": {
- metas: []dirMeta{
- metaRange("1", 0, 20, nil),
- metaRange("2", 20, 40, nil),
- metaRange("3", 40, 60, nil),
- metaRange("4", 60, 120, nil),
- metaRange("5", 120, 180, nil),
+ "We have 20, 20, 20, 60, 60 range blocks. '5' is marked as fresh one": {
+ metas: []dirMeta{
+ metaRange("1", 0, 20, nil),
+ metaRange("2", 20, 40, nil),
+ metaRange("3", 40, 60, nil),
+ metaRange("4", 60, 120, nil),
+ metaRange("5", 120, 180, nil),
+ },
+ expected: []string{"1", "2", "3"},
},
- expected: []string{"1", "2", "3"},
- },
- "We have 20, 60, 20, 60, 240 range blocks. We can compact 20 + 60 + 60": {
- metas: []dirMeta{
- metaRange("2", 20, 40, nil),
- metaRange("4", 60, 120, nil),
- metaRange("5", 960, 980, nil), // Fresh one.
- metaRange("6", 120, 180, nil),
- metaRange("7", 720, 960, nil),
+ "We have 20, 60, 20, 60, 240 range blocks. We can compact 20 + 60 + 60": {
+ metas: []dirMeta{
+ metaRange("2", 20, 40, nil),
+ metaRange("4", 60, 120, nil),
+ metaRange("5", 960, 980, nil), // Fresh one.
+ metaRange("6", 120, 180, nil),
+ metaRange("7", 720, 960, nil),
+ },
+ expected: []string{"2", "4", "6"},
},
- expected: []string{"2", "4", "6"},
- },
- "Do not select large blocks that have many tombstones when there is no fresh block": {
- metas: []dirMeta{
- metaRange("1", 0, 540, &BlockStats{
- NumSeries: 10,
- NumTombstones: 3,
- }),
+ "Do not select large blocks that have many tombstones when there is no fresh block": {
+ metas: []dirMeta{
+ metaRange("1", 0, 540, &BlockStats{
+ NumSeries: 10,
+ NumTombstones: 3,
+ }),
+ },
+ expected: nil,
},
- expected: nil,
- },
- "Select large blocks that have many tombstones when fresh appears": {
- metas: []dirMeta{
- metaRange("1", 0, 540, &BlockStats{
- NumSeries: 10,
- NumTombstones: 3,
- }),
- metaRange("2", 540, 560, nil),
+ "Select large blocks that have many tombstones when fresh appears": {
+ metas: []dirMeta{
+ metaRange("1", 0, 540, &BlockStats{
+ NumSeries: 10,
+ NumTombstones: 3,
+ }),
+ metaRange("2", 540, 560, nil),
+ },
+ expected: []string{"1"},
},
- expected: []string{"1"},
- },
- "For small blocks, do not compact tombstones, even when fresh appears.": {
- metas: []dirMeta{
- metaRange("1", 0, 60, &BlockStats{
- NumSeries: 10,
- NumTombstones: 3,
- }),
- metaRange("2", 60, 80, nil),
+ "For small blocks, do not compact tombstones, even when fresh appears.": {
+ metas: []dirMeta{
+ metaRange("1", 0, 60, &BlockStats{
+ NumSeries: 10,
+ NumTombstones: 3,
+ }),
+ metaRange("2", 60, 80, nil),
+ },
+ expected: nil,
},
- expected: nil,
- },
- `Regression test: we were stuck in a compact loop where we always recompacted
- the same block when tombstones and series counts were zero`: {
- metas: []dirMeta{
- metaRange("1", 0, 540, &BlockStats{
- NumSeries: 0,
- NumTombstones: 0,
- }),
- metaRange("2", 540, 560, nil),
+ `Regression test: we were stuck in a compact loop where we always recompacted
+ the same block when tombstones and series counts were zero`: {
+ metas: []dirMeta{
+ metaRange("1", 0, 540, &BlockStats{
+ NumSeries: 0,
+ NumTombstones: 0,
+ }),
+ metaRange("2", 540, 560, nil),
+ },
+ expected: nil,
},
- expected: nil,
- },
- `Regression test: we were wrongly assuming that new block is fresh from WAL when its ULID is newest.
- We need to actually look on max time instead.
+ `Regression test: we were wrongly assuming that new block is fresh from WAL when its ULID is newest.
+ We need to actually look on max time instead.
- With previous, wrong approach "8" block was ignored, so we were wrongly compacting 5 and 7 and introducing
- block overlaps`: {
- metas: []dirMeta{
- metaRange("5", 0, 360, nil),
- metaRange("6", 540, 560, nil), // Fresh one.
- metaRange("7", 360, 420, nil),
- metaRange("8", 420, 540, nil),
+ With previous, wrong approach "8" block was ignored, so we were wrongly compacting 5 and 7 and introducing
+ block overlaps`: {
+ metas: []dirMeta{
+ metaRange("5", 0, 360, nil),
+ metaRange("6", 540, 560, nil), // Fresh one.
+ metaRange("7", 360, 420, nil),
+ metaRange("8", 420, 540, nil),
+ },
+ expected: []string{"7", "8"},
},
- expected: []string{"7", "8"},
- },
- // |--------------|
- // |----------------|
- // |--------------|
- "Overlapping blocks 1": {
- metas: []dirMeta{
- metaRange("1", 0, 20, nil),
- metaRange("2", 19, 40, nil),
- metaRange("3", 40, 60, nil),
+ // |--------------|
+ // |----------------|
+ // |--------------|
+ "Overlapping blocks 1": {
+ metas: []dirMeta{
+ metaRange("1", 0, 20, nil),
+ metaRange("2", 19, 40, nil),
+ metaRange("3", 40, 60, nil),
+ },
+ expected: []string{"1", "2"},
},
- expected: []string{"1", "2"},
- },
- // |--------------|
- // |--------------|
- // |--------------|
- "Overlapping blocks 2": {
- metas: []dirMeta{
- metaRange("1", 0, 20, nil),
- metaRange("2", 20, 40, nil),
- metaRange("3", 30, 50, nil),
+ // |--------------|
+ // |--------------|
+ // |--------------|
+ "Overlapping blocks 2": {
+ metas: []dirMeta{
+ metaRange("1", 0, 20, nil),
+ metaRange("2", 20, 40, nil),
+ metaRange("3", 30, 50, nil),
+ },
+ expected: []string{"2", "3"},
},
- expected: []string{"2", "3"},
- },
- // |--------------|
- // |---------------------|
- // |--------------|
- "Overlapping blocks 3": {
- metas: []dirMeta{
- metaRange("1", 0, 20, nil),
- metaRange("2", 10, 40, nil),
- metaRange("3", 30, 50, nil),
+ // |--------------|
+ // |---------------------|
+ // |--------------|
+ "Overlapping blocks 3": {
+ metas: []dirMeta{
+ metaRange("1", 0, 20, nil),
+ metaRange("2", 10, 40, nil),
+ metaRange("3", 30, 50, nil),
+ },
+ expected: []string{"1", "2", "3"},
},
- expected: []string{"1", "2", "3"},
- },
- // |--------------|
- // |--------------------------------|
- // |--------------|
- // |--------------|
- "Overlapping blocks 4": {
- metas: []dirMeta{
- metaRange("5", 0, 360, nil),
- metaRange("6", 340, 560, nil),
- metaRange("7", 360, 420, nil),
- metaRange("8", 420, 540, nil),
+ // |--------------|
+ // |--------------------------------|
+ // |--------------|
+ // |--------------|
+ "Overlapping blocks 4": {
+ metas: []dirMeta{
+ metaRange("5", 0, 360, nil),
+ metaRange("6", 340, 560, nil),
+ metaRange("7", 360, 420, nil),
+ metaRange("8", 420, 540, nil),
+ },
+ expected: []string{"5", "6", "7", "8"},
},
- expected: []string{"5", "6", "7", "8"},
- },
- // |--------------|
- // |--------------|
- // |--------------|
- // |--------------|
- "Overlapping blocks 5": {
- metas: []dirMeta{
- metaRange("1", 0, 10, nil),
- metaRange("2", 9, 20, nil),
- metaRange("3", 30, 40, nil),
- metaRange("4", 39, 50, nil),
+ // |--------------|
+ // |--------------|
+ // |--------------|
+ // |--------------|
+ "Overlapping blocks 5": {
+ metas: []dirMeta{
+ metaRange("1", 0, 10, nil),
+ metaRange("2", 9, 20, nil),
+ metaRange("3", 30, 40, nil),
+ metaRange("4", 39, 50, nil),
+ },
+ expected: []string{"1", "2"},
},
- expected: []string{"1", "2"},
- },
- }
-
- for title, c := range cases {
- if !t.Run(title, func(t *testing.T) {
- res, err := compactor.plan(c.metas)
- require.NoError(t, err)
- require.Equal(t, c.expected, res)
- }) {
- return
}
- }
+
+ for title, c := range cases {
+ if !t.Run(title, func(t *testing.T) {
+ res, err := compactor.plan(c.metas)
+ require.NoError(t, err)
+ require.Equal(t, c.expected, res)
+ }) {
+ return
+ }
+ }
+ })
+
+ // Tests for the public Plan() method.
+ t.Run("Plan", func(t *testing.T) {
+ // Verify that when a BlockExcludeFilter excludes a block in the middle of
+ // the list, subsequent blocks are not processed.
+ t.Run("BlockExcludeFilter stops iteration", func(t *testing.T) {
+ dir := t.TempDir()
+
+ // Create 4 blocks with sequential ULIDs.
+ block1ULID := ulid.MustNew(1, nil)
+ block2ULID := ulid.MustNew(2, nil)
+ block3ULID := ulid.MustNew(3, nil)
+ block4ULID := ulid.MustNew(4, nil)
+
+ for i, uid := range []ulid.ULID{block1ULID, block2ULID, block3ULID, block4ULID} {
+ blockDir := filepath.Join(dir, uid.String())
+ require.NoError(t, os.MkdirAll(blockDir, 0o777))
+
+ meta := &BlockMeta{
+ ULID: uid,
+ MinTime: int64(i * 10),
+ MaxTime: int64((i + 1) * 10),
+ }
+ meta.Compaction.Level = 1
+ _, err := writeMetaFile(promslog.NewNopLogger(), blockDir, meta)
+ require.NoError(t, err)
+ }
+
+ // Track which blocks were evaluated by the exclude function.
+ var evaluatedBlocks []ulid.ULID
+ excludeFunc := func(meta *BlockMeta) bool {
+ evaluatedBlocks = append(evaluatedBlocks, meta.ULID)
+ return meta.ULID == block2ULID
+ }
+
+ c, err := NewLeveledCompactorWithOptions(
+ context.Background(),
+ nil,
+ promslog.NewNopLogger(),
+ []int64{20},
+ chunkenc.NewPool(),
+ LeveledCompactorOptions{
+ BlockExcludeFilter: excludeFunc,
+ EnableOverlappingCompaction: true,
+ },
+ )
+ require.NoError(t, err)
+
+ // Plan should evaluate all blocks.
+ _, err = c.Plan(dir)
+ require.NoError(t, err)
+
+ require.Len(t, evaluatedBlocks, 2, "Expected only 2 blocks to be evaluated")
+ require.Contains(t, evaluatedBlocks, block1ULID)
+ require.Contains(t, evaluatedBlocks, block2ULID)
+ })
+ })
}
func TestRangeWithFailedCompactionWontGetSelected(t *testing.T) {
@@ -1257,10 +1317,7 @@ func BenchmarkCompactionFromOOOHead(b *testing.B) {
// This is needed for unit tests that rely on
// checking state before and after a compaction.
func TestDisableAutoCompactions(t *testing.T) {
- db := openTestDB(t, nil, nil)
- defer func() {
- require.NoError(t, db.Close())
- }()
+ db := newTestDB(t)
blockRange := db.compactor.(*LeveledCompactor).ranges[0]
label := labels.FromStrings("foo", "bar")
@@ -1364,7 +1421,6 @@ func TestCancelCompactions(t *testing.T) {
// Make sure that no blocks were marked as compaction failed.
// This checks that the `context.Canceled` error is properly checked at all levels:
- // - tsdb_errors.NewMulti() should have the Is() method implemented for correct checks.
// - callers should check with errors.Is() instead of ==.
readOnlyDB, err := OpenDBReadOnly(tmpdirCopy, "", promslog.NewNopLogger())
require.NoError(t, err)
@@ -1418,10 +1474,7 @@ func TestDeleteCompactionBlockAfterFailedReload(t *testing.T) {
t.Run(title, func(t *testing.T) {
ctx := context.Background()
- db := openTestDB(t, nil, []int64{1, 100})
- defer func() {
- require.NoError(t, db.Close())
- }()
+ db := newTestDB(t, withRngs(1, 100))
db.DisableCompactions()
expBlocks := bootStrap(db)
@@ -1458,9 +1511,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()
@@ -1637,13 +1687,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
@@ -1673,10 +1717,7 @@ func TestSparseHistogramSpaceSavings(t *testing.T) {
var wg sync.WaitGroup
- wg.Add(1)
- go func() {
- defer wg.Done()
-
+ wg.Go(func() {
// Ingest sparse histograms.
for _, ah := range allSparseSeries {
var (
@@ -1699,7 +1740,7 @@ func TestSparseHistogramSpaceSavings(t *testing.T) {
sparseULIDs, err = compactor.Write(sparseHead.opts.ChunkDirRoot, sparseHead, mint, maxt, nil)
require.NoError(t, err)
require.Len(t, sparseULIDs, 1)
- }()
+ })
wg.Add(1)
go func(c testcase) {
@@ -1993,14 +2034,11 @@ func TestDelayedCompaction(t *testing.T) {
}
t.Parallel()
- var options *Options
+ var opts *Options
if c.compactionDelay > 0 {
- options = &Options{CompactionDelay: c.compactionDelay}
+ opts = &Options{CompactionDelay: c.compactionDelay}
}
- db := openTestDB(t, options, []int64{10})
- defer func() {
- require.NoError(t, db.Close())
- }()
+ db := newTestDB(t, withOpts(opts), withRngs(10))
label := labels.FromStrings("foo", "bar")
diff --git a/tsdb/db.go b/tsdb/db.go
index c57ae84c9c..a4a4a77f3c 100644
--- a/tsdb/db.go
+++ b/tsdb/db.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
@@ -41,12 +41,12 @@ import (
"github.com/prometheus/prometheus/storage"
"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/goversion" // Load the package into main to make sure minimum Go version is met.
"github.com/prometheus/prometheus/tsdb/tsdbutil"
"github.com/prometheus/prometheus/tsdb/wlog"
"github.com/prometheus/prometheus/util/compression"
+ "github.com/prometheus/prometheus/util/features"
)
const (
@@ -93,11 +93,16 @@ func DefaultOptions() *Options {
CompactionDelayMaxPercent: DefaultCompactionDelayMaxPercent,
CompactionDelay: time.Duration(0),
PostingsDecoderFactory: DefaultPostingsDecoderFactory,
+ BlockReloadInterval: 1 * time.Minute,
}
}
// Options of the DB storage.
type Options struct {
+ // staleSeriesCompactionThreshold is same as below option with same name, but is atomic so that we can do live updates without locks.
+ // This is the one that must be used by the code.
+ staleSeriesCompactionThreshold atomic.Float64
+
// Segments (wal files) max size.
// WALSegmentSize = 0, segment size is default size.
// WALSegmentSize > 0, segment size is WALSegmentSize.
@@ -219,6 +224,39 @@ type Options struct {
// UseUncachedIO allows bypassing the page cache when appropriate.
UseUncachedIO bool
+
+ // EnableSTAsZeroSample represents 'created-timestamp-zero-ingestion' feature flag.
+ // If true, ST, if non-zero and earlier than sample timestamp, will be stored
+ // as a zero sample before the actual sample.
+ //
+ // The zero sample is best-effort, only debug log on failure is emitted.
+ // NOTE(bwplotka): This feature might be deprecated and removed once PROM-60
+ // is implemented.
+ EnableSTAsZeroSample bool
+
+ // EnableSTStorage determines whether TSDB should write a Start Timestamp (ST)
+ // per sample to WAL.
+ // TODO(bwplotka): Implement this option as per PROM-60, currently it's noop.
+ EnableSTStorage bool
+
+ // EnableMetadataWALRecords represents 'metadata-wal-records' feature flag.
+ // NOTE(bwplotka): This feature might be deprecated and removed once PROM-60
+ // is implemented.
+ EnableMetadataWALRecords bool
+
+ // BlockCompactionExcludeFunc is a function which returns true for blocks that should NOT be compacted.
+ // It's passed down to the TSDB compactor.
+ BlockCompactionExcludeFunc BlockExcludeFilterFunc
+
+ // BlockReloadInterval is the interval at which blocks are reloaded.
+ BlockReloadInterval time.Duration
+
+ // FeatureRegistry is used to register TSDB features.
+ FeatureRegistry features.Collector
+
+ // StaleSeriesCompactionThreshold is a number between 0.0-1.0 indicating the % of stale series in
+ // the in-memory Head block. If the % of stale series crosses this threshold, stale series compaction is run immediately.
+ StaleSeriesCompactionThreshold float64
}
type NewCompactorFunc func(ctx context.Context, r prometheus.Registerer, l *slog.Logger, ranges []int64, pool chunkenc.Pool, opts *Options) (Compactor, error)
@@ -279,6 +317,10 @@ type DB struct {
// out-of-order compaction and vertical queries.
oooWasEnabled atomic.Bool
+ // lastHeadCompactionTime is the last wall clock time when the head block compaction was started,
+ // irrespective of success or failure. This does not include out-of-order compaction and stale series compaction.
+ lastHeadCompactionTime time.Time
+
writeNotified wlog.WriteNotified
registerer prometheus.Registerer
@@ -289,20 +331,23 @@ type DB struct {
}
type dbMetrics struct {
- loadedBlocks prometheus.GaugeFunc
- symbolTableSize prometheus.GaugeFunc
- reloads prometheus.Counter
- reloadsFailed prometheus.Counter
- compactionsFailed prometheus.Counter
- compactionsTriggered prometheus.Counter
- compactionsSkipped prometheus.Counter
- sizeRetentionCount prometheus.Counter
- timeRetentionCount prometheus.Counter
- startTime prometheus.GaugeFunc
- tombCleanTimer prometheus.Histogram
- blocksBytes prometheus.Gauge
- maxBytes prometheus.Gauge
- retentionDuration prometheus.Gauge
+ loadedBlocks prometheus.GaugeFunc
+ symbolTableSize prometheus.GaugeFunc
+ reloads prometheus.Counter
+ reloadsFailed prometheus.Counter
+ compactionsFailed prometheus.Counter
+ compactionsTriggered prometheus.Counter
+ compactionsSkipped prometheus.Counter
+ sizeRetentionCount prometheus.Counter
+ timeRetentionCount prometheus.Counter
+ startTime prometheus.GaugeFunc
+ tombCleanTimer prometheus.Histogram
+ blocksBytes prometheus.Gauge
+ maxBytes prometheus.Gauge
+ retentionDuration prometheus.Gauge
+ staleSeriesCompactionsTriggered prometheus.Counter
+ staleSeriesCompactionsFailed prometheus.Counter
+ staleSeriesCompactionDuration prometheus.Histogram
}
func newDBMetrics(db *DB, r prometheus.Registerer) *dbMetrics {
@@ -387,6 +432,22 @@ func newDBMetrics(db *DB, r prometheus.Registerer) *dbMetrics {
Name: "prometheus_tsdb_size_retentions_total",
Help: "The number of times that blocks were deleted because the maximum number of bytes was exceeded.",
})
+ m.staleSeriesCompactionsTriggered = prometheus.NewCounter(prometheus.CounterOpts{
+ Name: "prometheus_tsdb_stale_series_compactions_triggered_total",
+ Help: "Total number of triggered stale series compactions.",
+ })
+ m.staleSeriesCompactionsFailed = prometheus.NewCounter(prometheus.CounterOpts{
+ Name: "prometheus_tsdb_stale_series_compactions_failed_total",
+ Help: "Total number of stale series compactions that failed.",
+ })
+ m.staleSeriesCompactionDuration = prometheus.NewHistogram(prometheus.HistogramOpts{
+ Name: "prometheus_tsdb_stale_series_compaction_duration_seconds",
+ Help: "Duration of stale series compaction runs.",
+ Buckets: prometheus.ExponentialBuckets(1, 2, 14),
+ NativeHistogramBucketFactor: 1.1,
+ NativeHistogramMaxBucketNumber: 100,
+ NativeHistogramMinResetDuration: 1 * time.Hour,
+ })
if r != nil {
r.MustRegister(
@@ -404,6 +465,9 @@ func newDBMetrics(db *DB, r prometheus.Registerer) *dbMetrics {
m.blocksBytes,
m.maxBytes,
m.retentionDuration,
+ m.staleSeriesCompactionsTriggered,
+ m.staleSeriesCompactionsFailed,
+ m.staleSeriesCompactionDuration,
)
}
return m
@@ -495,11 +559,9 @@ func (db *DBReadOnly) FlushWAL(dir string) (returnErr error) {
return err
}
defer func() {
- errs := tsdb_errors.NewMulti(returnErr)
if err := head.Close(); err != nil {
- errs.Add(fmt.Errorf("closing Head: %w", err))
+ returnErr = errors.Join(returnErr, fmt.Errorf("closing Head: %w", err))
}
- returnErr = errs.Err()
}()
// Set the min valid time for the ingested wal samples
// to be no lower than the maxt of the last block.
@@ -654,13 +716,13 @@ func (db *DBReadOnly) Blocks() ([]BlockReader, error) {
db.logger.Warn("Closing block failed", "err", err, "block", b)
}
}
- errs := tsdb_errors.NewMulti()
+ var errs []error
for ulid, err := range corrupted {
if err != nil {
- errs.Add(fmt.Errorf("corrupted block %s: %w", ulid.String(), err))
+ errs = append(errs, fmt.Errorf("corrupted block %s: %w", ulid.String(), err))
}
}
- return nil, errs.Err()
+ return nil, errors.Join(errs...)
}
if len(loadable) == 0 {
@@ -771,7 +833,7 @@ func (db *DBReadOnly) Close() error {
}
close(db.closed)
- return tsdb_errors.CloseAll(db.closers)
+ return closeAll(db.closers)
}
// Open returns a new DB in the given directory. If options are empty, DefaultOptions will be used.
@@ -779,6 +841,15 @@ func Open(dir string, l *slog.Logger, r prometheus.Registerer, opts *Options, st
var rngs []int64
opts, rngs = validateOpts(opts, nil)
+ // Register TSDB features if a registry is provided.
+ if opts.FeatureRegistry != nil {
+ opts.FeatureRegistry.Set(features.TSDB, "exemplar_storage", opts.EnableExemplarStorage)
+ opts.FeatureRegistry.Set(features.TSDB, "delayed_compaction", opts.EnableDelayedCompaction)
+ opts.FeatureRegistry.Set(features.TSDB, "isolation", !opts.IsolationDisabled)
+ opts.FeatureRegistry.Set(features.TSDB, "use_uncached_io", opts.UseUncachedIO)
+ opts.FeatureRegistry.Enable(features.TSDB, "native_histograms")
+ }
+
return open(dir, l, r, opts, rngs, stats)
}
@@ -813,12 +884,17 @@ func validateOpts(opts *Options, rngs []int64) (*Options, []int64) {
if opts.OutOfOrderTimeWindow < 0 {
opts.OutOfOrderTimeWindow = 0
}
+ if opts.BlockReloadInterval < 1*time.Second {
+ opts.BlockReloadInterval = 1 * time.Second
+ }
if len(rngs) == 0 {
// Start with smallest block duration and create exponential buckets until the exceed the
// configured maximum block duration.
rngs = ExponentialBlockRanges(opts.MinBlockDuration, 10, 3)
}
+
+ opts.staleSeriesCompactionThreshold.Store(opts.StaleSeriesCompactionThreshold)
return opts, rngs
}
@@ -853,9 +929,13 @@ func open(dir string, l *slog.Logger, r prometheus.Registerer, opts *Options, rn
for _, tmpDir := range []string{walDir, dir} {
// Remove tmp dirs.
- if err := removeBestEffortTmpDirs(l, tmpDir); err != nil {
+ if err := tsdbutil.RemoveTmpDirs(l, tmpDir, isTmpDir); err != nil {
return nil, fmt.Errorf("remove tmp dirs: %w", err)
}
+ // Remove any temporary checkpoints that might have been interrupted during creation.
+ if err := wlog.DeleteTempCheckpoints(l, tmpDir); err != nil {
+ return nil, fmt.Errorf("delete temp checkpoints: %w", err)
+ }
}
db := &DB{
@@ -877,11 +957,9 @@ func open(dir string, l *slog.Logger, r prometheus.Registerer, opts *Options, rn
}
close(db.donec) // DB is never run if it was an error, so close this channel here.
- errs := tsdb_errors.NewMulti(returnedErr)
if err := db.Close(); err != nil {
- errs.Add(fmt.Errorf("close DB after failed startup: %w", err))
+ returnedErr = errors.Join(returnedErr, fmt.Errorf("close DB after failed startup: %w", err))
}
- returnedErr = errs.Err()
}()
if db.blocksToDelete == nil {
@@ -908,6 +986,7 @@ func open(dir string, l *slog.Logger, r prometheus.Registerer, opts *Options, rn
EnableOverlappingCompaction: opts.EnableOverlappingCompaction,
PD: opts.PostingsDecoderFactory,
UseUncachedIO: opts.UseUncachedIO,
+ BlockExcludeFilter: opts.BlockCompactionExcludeFunc,
})
}
if err != nil {
@@ -968,6 +1047,8 @@ func open(dir string, l *slog.Logger, r prometheus.Registerer, opts *Options, rn
headOpts.OutOfOrderTimeWindow.Store(opts.OutOfOrderTimeWindow)
headOpts.OutOfOrderCapMax.Store(opts.OutOfOrderCapMax)
headOpts.EnableSharding = opts.EnableSharding
+ headOpts.EnableSTAsZeroSample = opts.EnableSTAsZeroSample
+ headOpts.EnableMetadataWALRecords = opts.EnableMetadataWALRecords
if opts.WALReplayConcurrency > 0 {
headOpts.WALReplayConcurrency = opts.WALReplayConcurrency
}
@@ -1038,26 +1119,6 @@ func open(dir string, l *slog.Logger, r prometheus.Registerer, opts *Options, rn
return db, nil
}
-func removeBestEffortTmpDirs(l *slog.Logger, dir string) error {
- files, err := os.ReadDir(dir)
- if os.IsNotExist(err) {
- return nil
- }
- if err != nil {
- return err
- }
- for _, f := range files {
- if isTmpDir(f) {
- if err := os.RemoveAll(filepath.Join(dir, f.Name())); err != nil {
- l.Error("failed to delete tmp block dir", "dir", filepath.Join(dir, f.Name()), "err", err)
- continue
- }
- l.Info("Found and deleted tmp block dir", "dir", filepath.Join(dir, f.Name()))
- }
- }
- return nil
-}
-
// StartTime implements the Storage interface.
func (db *DB) StartTime() (int64, error) {
db.mtx.RLock()
@@ -1097,7 +1158,7 @@ func (db *DB) run(ctx context.Context) {
}
select {
- case <-time.After(1 * time.Minute):
+ case <-time.After(db.opts.BlockReloadInterval):
db.cmtx.Lock()
if err := db.reloadBlocks(); err != nil {
db.logger.Error("reloadBlocks", "err", err)
@@ -1110,6 +1171,29 @@ func (db *DB) run(ctx context.Context) {
}
// We attempt mmapping of head chunks regularly.
db.head.mmapHeadChunks()
+
+ numStaleSeries, numSeries := db.Head().NumStaleSeries(), db.Head().NumSeries()
+ if db.autoCompact && numSeries > 0 && db.opts.staleSeriesCompactionThreshold.Load() > 0 {
+ staleSeriesRatio := float64(numStaleSeries) / float64(numSeries)
+ if staleSeriesRatio >= db.opts.staleSeriesCompactionThreshold.Load() {
+ nextCompactionIsSoon := false
+ if !db.lastHeadCompactionTime.IsZero() {
+ compactionInterval := time.Duration(db.head.chunkRange.Load()) * time.Millisecond
+ nextEstimatedCompactionTime := db.lastHeadCompactionTime.Add(compactionInterval)
+ if time.Now().Add(10 * time.Minute).After(nextEstimatedCompactionTime) {
+ // Next compaction is starting within next 10 mins.
+ nextCompactionIsSoon = true
+ }
+ }
+
+ if !nextCompactionIsSoon {
+ if err := db.CompactStaleHead(); err != nil {
+ db.logger.Error("immediate stale series compaction failed", "err", err)
+ }
+ }
+ }
+ }
+
case <-db.compactc:
db.metrics.compactionsTriggered.Inc()
@@ -1131,11 +1215,16 @@ func (db *DB) run(ctx context.Context) {
}
}
-// Appender opens a new appender against the database.
+// Appender opens a new Appender against the database.
func (db *DB) Appender(ctx context.Context) storage.Appender {
return dbAppender{db: db, Appender: db.head.Appender(ctx)}
}
+// AppenderV2 opens a new AppenderV2 against the database.
+func (db *DB) AppenderV2(ctx context.Context) storage.AppenderV2 {
+ return dbAppenderV2{db: db, AppenderV2: db.head.AppenderV2(ctx)}
+}
+
// ApplyConfig applies a new config to the DB.
// Behaviour of 'OutOfOrderTimeWindow' is as follows:
// OOO enabled = oooTimeWindow > 0. OOO disabled = oooTimeWindow is 0.
@@ -1157,7 +1246,7 @@ func (db *DB) ApplyConfig(conf *config.Config) error {
oooTimeWindow := int64(0)
if conf.StorageConfig.TSDBConfig != nil {
oooTimeWindow = conf.StorageConfig.TSDBConfig.OutOfOrderTimeWindow
-
+ db.opts.staleSeriesCompactionThreshold.Store(conf.StorageConfig.TSDBConfig.StaleSeriesCompactionThreshold)
// Update retention configuration if provided.
if conf.StorageConfig.TSDBConfig.Retention != nil {
db.retentionMtx.Lock()
@@ -1171,6 +1260,8 @@ func (db *DB) ApplyConfig(conf *config.Config) error {
}
db.retentionMtx.Unlock()
}
+ } else {
+ db.opts.staleSeriesCompactionThreshold.Store(0)
}
if oooTimeWindow < 0 {
oooTimeWindow = 0
@@ -1249,6 +1340,36 @@ func (a dbAppender) Commit() error {
return err
}
+// dbAppenderV2 wraps the DB's head appender and triggers compactions on commit
+// if necessary.
+type dbAppenderV2 struct {
+ storage.AppenderV2
+ db *DB
+}
+
+var _ storage.GetRef = dbAppenderV2{}
+
+func (a dbAppenderV2) GetRef(lset labels.Labels, hash uint64) (storage.SeriesRef, labels.Labels) {
+ if g, ok := a.AppenderV2.(storage.GetRef); ok {
+ return g.GetRef(lset, hash)
+ }
+ return 0, labels.EmptyLabels()
+}
+
+func (a dbAppenderV2) Commit() error {
+ err := a.AppenderV2.Commit()
+
+ // We could just run this check every few minutes practically. But for benchmarks
+ // and high frequency use cases this is the safer way.
+ if a.db.head.compactable() {
+ select {
+ case a.db.compactc <- struct{}{}:
+ default:
+ }
+ }
+ return err
+}
+
// waitingForCompactionDelay returns true if the DB is waiting for the Head compaction delay.
// This doesn't guarantee that the Head is really compactable.
func (db *DB) waitingForCompactionDelay() bool {
@@ -1272,11 +1393,9 @@ func (db *DB) Compact(ctx context.Context) (returnErr error) {
lastBlockMaxt := int64(math.MinInt64)
defer func() {
- errs := tsdb_errors.NewMulti(returnErr)
if err := db.head.truncateWAL(lastBlockMaxt); err != nil {
- errs.Add(fmt.Errorf("WAL truncation in Compact defer: %w", err))
+ returnErr = errors.Join(returnErr, fmt.Errorf("WAL truncation in Compact defer: %w", err))
}
- returnErr = errs.Err()
}()
start := time.Now()
@@ -1401,13 +1520,13 @@ func (db *DB) compactOOOHead(ctx context.Context) error {
return fmt.Errorf("compact ooo head: %w", err)
}
if err := db.reloadBlocks(); err != nil {
- errs := tsdb_errors.NewMulti(err)
+ errs := []error{err}
for _, uid := range ulids {
if errRemoveAll := os.RemoveAll(filepath.Join(db.dir, uid.String())); errRemoveAll != nil {
- errs.Add(errRemoveAll)
+ errs = append(errs, errRemoveAll)
}
}
- return fmt.Errorf("reloadBlocks blocks after failed compact ooo head: %w", errs.Err())
+ return fmt.Errorf("reloadBlocks blocks after failed compact ooo head: %w", errors.Join(errs...))
}
lastWBLFile, minOOOMmapRef := oooHead.LastWBLFile(), oooHead.LastMmapRef()
@@ -1484,19 +1603,23 @@ func (db *DB) compactOOO(dest string, oooHead *OOOCompactionHead) (_ []ulid.ULID
// compactHead compacts the given RangeHead.
// The db.cmtx should be held before calling this method.
func (db *DB) compactHead(head *RangeHead) error {
+ db.lastHeadCompactionTime = time.Now()
+
uids, err := db.compactor.Write(db.dir, head, head.MinTime(), head.BlockMaxTime(), nil)
if err != nil {
return fmt.Errorf("persist head block: %w", err)
}
if err := db.reloadBlocks(); err != nil {
- multiErr := tsdb_errors.NewMulti(fmt.Errorf("reloadBlocks blocks: %w", err))
+ errs := []error{
+ fmt.Errorf("reloadBlocks blocks: %w", err),
+ }
for _, uid := range uids {
if errRemoveAll := os.RemoveAll(filepath.Join(db.dir, uid.String())); errRemoveAll != nil {
- multiErr.Add(fmt.Errorf("delete persisted head block after failed db reloadBlocks:%s: %w", uid, errRemoveAll))
+ errs = append(errs, fmt.Errorf("delete persisted head block after failed db reloadBlocks:%s: %w", uid, errRemoveAll))
}
}
- return multiErr.Err()
+ return errors.Join(errs...)
}
if err = db.head.truncateMemory(head.BlockMaxTime()); err != nil {
return fmt.Errorf("head memory truncate: %w", err)
@@ -1507,6 +1630,61 @@ func (db *DB) compactHead(head *RangeHead) error {
return nil
}
+func (db *DB) CompactStaleHead() (err error) {
+ db.cmtx.Lock()
+ defer func() {
+ db.cmtx.Unlock()
+ if err != nil {
+ db.metrics.staleSeriesCompactionsFailed.Inc()
+ }
+ }()
+
+ db.metrics.staleSeriesCompactionsTriggered.Inc()
+
+ db.logger.Info("Starting stale series compaction")
+ start := time.Now()
+
+ // We get the stale series reference first because this list can change during the compaction below.
+ // It is more efficient and easier to provide an index interface for the stale series when we have a static list.
+ staleSeriesRefs, err := db.head.SortedStaleSeriesRefsNoOOOData(context.Background())
+ if err != nil {
+ return err
+ }
+ meta := &BlockMeta{}
+ meta.Compaction.SetStaleSeries()
+ mint, maxt := db.head.opts.ChunkRange*(db.head.MinTime()/db.head.opts.ChunkRange), db.head.MaxTime()
+ for ; mint < maxt; mint += db.head.chunkRange.Load() {
+ staleHead := NewStaleHead(db.Head(), mint, mint+db.head.chunkRange.Load()-1, staleSeriesRefs)
+
+ uids, err := db.compactor.Write(db.dir, staleHead, staleHead.MinTime(), staleHead.BlockMaxTime(), meta)
+ if err != nil {
+ return fmt.Errorf("persist stale head: %w", err)
+ }
+
+ db.logger.Info("Stale series block created", "ulids", fmt.Sprintf("%v", uids), "min_time", mint, "max_time", maxt)
+
+ if err := db.reloadBlocks(); err != nil {
+ errs := []error{fmt.Errorf("reloadBlocks blocks: %w", err)}
+ for _, uid := range uids {
+ if errRemoveAll := os.RemoveAll(filepath.Join(db.dir, uid.String())); errRemoveAll != nil {
+ errs = append(errs, fmt.Errorf("delete persisted stale head block after failed db reloadBlocks:%s: %w", uid, errRemoveAll))
+ }
+ }
+ return errors.Join(errs...)
+ }
+ }
+
+ if err := db.head.truncateStaleSeries(staleSeriesRefs, maxt); err != nil {
+ return fmt.Errorf("head truncate: %w", err)
+ }
+ db.head.RebuildSymbolTable(db.logger)
+
+ elapsed := time.Since(start)
+ db.metrics.staleSeriesCompactionDuration.Observe(elapsed.Seconds())
+ db.logger.Info("Ending stale series compaction", "num_series", len(staleSeriesRefs), "duration", elapsed)
+ return nil
+}
+
// compactBlocks compacts all the eligible on-disk blocks.
// The db.cmtx should be held before calling this method.
func (db *DB) compactBlocks() (err error) {
@@ -1540,13 +1718,13 @@ func (db *DB) compactBlocks() (err error) {
}
if err := db.reloadBlocks(); err != nil {
- errs := tsdb_errors.NewMulti(fmt.Errorf("reloadBlocks blocks: %w", err))
+ errs := []error{fmt.Errorf("reloadBlocks blocks: %w", err)}
for _, uid := range uids {
if errRemoveAll := os.RemoveAll(filepath.Join(db.dir, uid.String())); errRemoveAll != nil {
- errs.Add(fmt.Errorf("delete persisted block after failed db reloadBlocks:%s: %w", uid, errRemoveAll))
+ errs = append(errs, fmt.Errorf("delete persisted block after failed db reloadBlocks:%s: %w", uid, errRemoveAll))
}
}
- return errs.Err()
+ return errors.Join(errs...)
}
}
@@ -1626,13 +1804,13 @@ func (db *DB) reloadBlocks() (err error) {
}
}
db.mtx.RUnlock()
- errs := tsdb_errors.NewMulti()
+ var errs []error
for ulid, err := range corrupted {
if err != nil {
- errs.Add(fmt.Errorf("corrupted block %s: %w", ulid.String(), err))
+ errs = append(errs, fmt.Errorf("corrupted block %s: %w", ulid.String(), err))
}
}
- return errs.Err()
+ return errors.Join(errs...)
}
var (
@@ -1966,7 +2144,7 @@ func (db *DB) inOrderBlocksMaxTime() (maxt int64, ok bool) {
maxt, ok = int64(math.MinInt64), false
// If blocks are overlapping, last block might not have the max time. So check all blocks.
for _, b := range db.Blocks() {
- if !b.meta.Compaction.FromOutOfOrder() && b.meta.MaxTime > maxt {
+ if !b.meta.Compaction.FromOutOfOrder() && !b.meta.Compaction.FromStaleSeries() && b.meta.MaxTime > maxt {
ok = true
maxt = b.meta.MaxTime
}
@@ -1981,6 +2159,13 @@ func (db *DB) Head() *Head {
// Close the partition.
func (db *DB) Close() error {
+ // Allow close-after-close operation for simpler use (e.g. tests).
+ select {
+ case <-db.donec:
+ return nil
+ default:
+ }
+
close(db.stopc)
if db.compactCancel != nil {
db.compactCancel()
@@ -1997,11 +2182,14 @@ func (db *DB) Close() error {
g.Go(pb.Close)
}
- errs := tsdb_errors.NewMulti(g.Wait(), db.locker.Release())
- if db.head != nil {
- errs.Add(db.head.Close())
+ errs := []error{
+ g.Wait(),
+ db.locker.Release(),
}
- return errs.Err()
+ if db.head != nil {
+ errs = append(errs, db.head.Close())
+ }
+ return errors.Join(errs...)
}
// DisableCompactions disables auto compactions.
@@ -2334,8 +2522,7 @@ func isBlockDir(fi fs.DirEntry) bool {
return err == nil
}
-// isTmpDir returns true if the given file-info contains a block ULID, a checkpoint prefix,
-// or a chunk snapshot prefix and a tmp extension.
+// isTmpDir returns true if the given file-info contains a block ULID, or a chunk snapshot prefix and a tmp extension.
func isTmpDir(fi fs.DirEntry) bool {
if !fi.IsDir() {
return false
@@ -2344,9 +2531,6 @@ func isTmpDir(fi fs.DirEntry) bool {
fn := fi.Name()
ext := filepath.Ext(fn)
if ext == tmpForDeletionBlockDirSuffix || ext == tmpForCreationBlockDirSuffix || ext == tmpLegacy {
- if strings.HasPrefix(fn, wlog.CheckpointPrefix) {
- return true
- }
if strings.HasPrefix(fn, chunkSnapshotPrefix) {
return true
}
@@ -2382,3 +2566,12 @@ func exponential(d, minD, maxD time.Duration) time.Duration {
}
return d
}
+
+// closeAll closes all given closers while recording all errors.
+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/db_append_v2_test.go b/tsdb/db_append_v2_test.go
new file mode 100644
index 0000000000..8083829537
--- /dev/null
+++ b/tsdb/db_append_v2_test.go
@@ -0,0 +1,7522 @@
+// 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 tsdb
+
+import (
+ "bufio"
+ "context"
+ "fmt"
+ "log/slog"
+ "math"
+ "math/rand"
+ "os"
+ "path"
+ "path/filepath"
+ "runtime"
+ "sort"
+ "strconv"
+ "testing"
+ "time"
+
+ "github.com/oklog/ulid/v2"
+ "github.com/prometheus/client_golang/prometheus"
+ prom_testutil "github.com/prometheus/client_golang/prometheus/testutil"
+ "github.com/prometheus/common/promslog"
+ "github.com/stretchr/testify/require"
+ "go.uber.org/atomic"
+
+ "github.com/prometheus/prometheus/config"
+ "github.com/prometheus/prometheus/model/histogram"
+ "github.com/prometheus/prometheus/model/labels"
+ "github.com/prometheus/prometheus/model/metadata"
+ "github.com/prometheus/prometheus/storage"
+ "github.com/prometheus/prometheus/tsdb/chunkenc"
+ "github.com/prometheus/prometheus/tsdb/chunks"
+ "github.com/prometheus/prometheus/tsdb/fileutil"
+ "github.com/prometheus/prometheus/tsdb/index"
+ "github.com/prometheus/prometheus/tsdb/record"
+ "github.com/prometheus/prometheus/tsdb/tombstones"
+ "github.com/prometheus/prometheus/tsdb/tsdbutil"
+ "github.com/prometheus/prometheus/tsdb/wlog"
+ "github.com/prometheus/prometheus/util/annotations"
+ "github.com/prometheus/prometheus/util/compression"
+ "github.com/prometheus/prometheus/util/testutil"
+)
+
+// TODO(bwplotka): Ensure non-ported tests are not deleted from db_test.go when removing AppenderV1 flow (#17632):
+// * TestQuerier_ShouldNotPanicIfHeadChunkIsTruncatedWhileReadingQueriedChunks
+// * TestChunkQuerier_ShouldNotPanicIfHeadChunkIsTruncatedWhileReadingQueriedChunks
+// * TestEmptyLabelsetCausesError
+// * TestQueryHistogramFromBlocksWithCompaction
+
+// TODO(krajorama): Add histograms test cases.
+func TestDataAvailableOnlyAfterCommit_AppendV2(t *testing.T) {
+ db := newTestDB(t)
+
+ ctx := context.Background()
+ app := db.AppenderV2(ctx)
+
+ _, err := app.Append(0, labels.FromStrings("foo", "bar"), 0, 0, 0, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+
+ querier, err := db.Querier(0, 1)
+ require.NoError(t, err)
+ seriesSet := query(t, querier, labels.MustNewMatcher(labels.MatchEqual, "foo", "bar"))
+ require.Equal(t, map[string][]chunks.Sample{}, seriesSet)
+
+ err = app.Commit()
+ require.NoError(t, err)
+
+ querier, err = db.Querier(0, 1)
+ require.NoError(t, err)
+ defer querier.Close()
+
+ seriesSet = query(t, querier, labels.MustNewMatcher(labels.MatchEqual, "foo", "bar"))
+
+ require.Equal(t, map[string][]chunks.Sample{`{foo="bar"}`: {sample{t: 0, f: 0}}}, seriesSet)
+}
+
+// TestNoPanicAfterWALCorruption ensures that querying the db after a WAL corruption doesn't cause a panic.
+// https://github.com/prometheus/prometheus/issues/7548
+func TestNoPanicAfterWALCorruption_AppendV2(t *testing.T) {
+ db := newTestDB(t, withOpts(&Options{WALSegmentSize: 32 * 1024}))
+
+ // Append until the first mmapped head chunk.
+ // This is to ensure that all samples can be read from the mmapped chunks when the WAL is corrupted.
+ var expSamples []chunks.Sample
+ var maxt int64
+ ctx := context.Background()
+ {
+ // Appending 121 samples because on the 121st a new chunk will be created.
+ for range 121 {
+ app := db.AppenderV2(ctx)
+ _, err := app.Append(0, labels.FromStrings("foo", "bar"), 0, maxt, 0, nil, nil, storage.AOptions{})
+ expSamples = append(expSamples, sample{t: maxt, f: 0})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ maxt++
+ }
+ require.NoError(t, db.Close())
+ }
+
+ // Corrupt the WAL after the first sample of the series so that it has at least one sample and
+ // it is not garbage collected.
+ // The repair deletes all WAL records after the corrupted record and these are read from the mmapped chunk.
+ {
+ walFiles, err := os.ReadDir(path.Join(db.Dir(), "wal"))
+ require.NoError(t, err)
+ f, err := os.OpenFile(path.Join(db.Dir(), "wal", walFiles[0].Name()), os.O_RDWR, 0o666)
+ require.NoError(t, err)
+ r := wlog.NewReader(bufio.NewReader(f))
+ require.True(t, r.Next(), "reading the series record")
+ require.True(t, r.Next(), "reading the first sample record")
+ // Write an invalid record header to corrupt everything after the first wal sample.
+ _, err = f.WriteAt([]byte{99}, r.Offset())
+ require.NoError(t, err)
+ f.Close()
+ }
+
+ // Query the data.
+ {
+ db := newTestDB(t, withDir(db.Dir()))
+ require.Equal(t, 1.0, prom_testutil.ToFloat64(db.head.metrics.walCorruptionsTotal), "WAL corruption count mismatch")
+
+ querier, err := db.Querier(0, maxt)
+ require.NoError(t, err)
+ seriesSet := query(t, querier, labels.MustNewMatcher(labels.MatchEqual, "", ""))
+ // The last sample should be missing as it was after the WAL segment corruption.
+ require.Equal(t, map[string][]chunks.Sample{`{foo="bar"}`: expSamples[0 : len(expSamples)-1]}, seriesSet)
+ }
+}
+
+func TestDataNotAvailableAfterRollback_AppendV2(t *testing.T) {
+ db := newTestDB(t)
+
+ app := db.AppenderV2(context.Background())
+ _, err := app.Append(0, labels.FromStrings("type", "float"), 0, 0, 0, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+
+ _, err = app.Append(
+ 0, labels.FromStrings("type", "histogram"), 0, 0, 0,
+ &histogram.Histogram{Count: 42, Sum: math.NaN()}, nil,
+ storage.AOptions{},
+ )
+ require.NoError(t, err)
+
+ _, err = app.Append(
+ 0, labels.FromStrings("type", "floathistogram"), 0, 0, 0,
+ nil, &histogram.FloatHistogram{Count: 42, Sum: math.NaN()},
+ storage.AOptions{},
+ )
+ require.NoError(t, err)
+
+ err = app.Rollback()
+ require.NoError(t, err)
+
+ for _, typ := range []string{"float", "histogram", "floathistogram"} {
+ querier, err := db.Querier(0, 1)
+ require.NoError(t, err)
+ seriesSet := query(t, querier, labels.MustNewMatcher(labels.MatchEqual, "type", typ))
+ require.Equal(t, map[string][]chunks.Sample{}, seriesSet)
+ }
+
+ sr, err := wlog.NewSegmentsReader(db.head.wal.Dir())
+ require.NoError(t, err)
+ defer func() {
+ require.NoError(t, sr.Close())
+ }()
+
+ // Read records from WAL and check for expected count of series and samples.
+ var (
+ r = wlog.NewReader(sr)
+ dec = record.NewDecoder(labels.NewSymbolTable(), promslog.NewNopLogger())
+
+ walSeriesCount, walSamplesCount, walHistogramCount, walFloatHistogramCount, walExemplarsCount int
+ )
+ for r.Next() {
+ rec := r.Record()
+ switch dec.Type(rec) {
+ case record.Series:
+ var series []record.RefSeries
+ series, err = dec.Series(rec, series)
+ require.NoError(t, err)
+ walSeriesCount += len(series)
+
+ case record.Samples:
+ var samples []record.RefSample
+ samples, err = dec.Samples(rec, samples)
+ require.NoError(t, err)
+ walSamplesCount += len(samples)
+
+ case record.Exemplars:
+ var exemplars []record.RefExemplar
+ exemplars, err = dec.Exemplars(rec, exemplars)
+ require.NoError(t, err)
+ walExemplarsCount += len(exemplars)
+
+ case record.HistogramSamples, record.CustomBucketsHistogramSamples:
+ var histograms []record.RefHistogramSample
+ histograms, err = dec.HistogramSamples(rec, histograms)
+ require.NoError(t, err)
+ walHistogramCount += len(histograms)
+
+ case record.FloatHistogramSamples, record.CustomBucketsFloatHistogramSamples:
+ var floatHistograms []record.RefFloatHistogramSample
+ floatHistograms, err = dec.FloatHistogramSamples(rec, floatHistograms)
+ require.NoError(t, err)
+ walFloatHistogramCount += len(floatHistograms)
+
+ default:
+ }
+ }
+
+ // Check that only series get stored after calling Rollback.
+ require.Equal(t, 3, walSeriesCount, "series should have been written to WAL")
+ require.Equal(t, 0, walSamplesCount, "samples should not have been written to WAL")
+ require.Equal(t, 0, walExemplarsCount, "exemplars should not have been written to WAL")
+ require.Equal(t, 0, walHistogramCount, "histograms should not have been written to WAL")
+ require.Equal(t, 0, walFloatHistogramCount, "float histograms should not have been written to WAL")
+}
+
+func TestDBAppenderV2_AddRef(t *testing.T) {
+ db := newTestDB(t)
+
+ ctx := context.Background()
+ app1 := db.AppenderV2(ctx)
+
+ ref1, err := app1.Append(0, labels.FromStrings("a", "b"), 0, 123, 0, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+
+ // Reference should already work before commit.
+ ref2, err := app1.Append(ref1, labels.EmptyLabels(), 0, 124, 1, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.Equal(t, ref1, ref2)
+
+ err = app1.Commit()
+ require.NoError(t, err)
+
+ app2 := db.AppenderV2(ctx)
+
+ // first ref should already work in next transaction.
+ ref3, err := app2.Append(ref1, labels.EmptyLabels(), 0, 125, 0, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.Equal(t, ref1, ref3)
+
+ ref4, err := app2.Append(ref1, labels.FromStrings("a", "b"), 0, 133, 1, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.Equal(t, ref1, ref4)
+
+ // Reference must be valid to add another sample.
+ ref5, err := app2.Append(ref2, labels.EmptyLabels(), 0, 143, 2, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.Equal(t, ref1, ref5)
+
+ // Missing labels & invalid refs should fail.
+ _, err = app2.Append(9999999, labels.EmptyLabels(), 0, 1, 1, nil, nil, storage.AOptions{})
+ require.ErrorIs(t, err, ErrInvalidSample)
+
+ require.NoError(t, app2.Commit())
+
+ q, err := db.Querier(0, 200)
+ require.NoError(t, err)
+
+ res := query(t, q, labels.MustNewMatcher(labels.MatchEqual, "a", "b"))
+
+ require.Equal(t, map[string][]chunks.Sample{
+ labels.FromStrings("a", "b").String(): {
+ sample{t: 123, f: 0},
+ sample{t: 124, f: 1},
+ sample{t: 125, f: 0},
+ sample{t: 133, f: 1},
+ sample{t: 143, f: 2},
+ },
+ }, res)
+}
+
+func TestDBAppenderV2_EmptyLabelsIgnored(t *testing.T) {
+ db := newTestDB(t)
+
+ ctx := context.Background()
+ app1 := db.AppenderV2(ctx)
+
+ ref1, err := app1.Append(0, labels.FromStrings("a", "b"), 0, 123, 0, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+
+ // Add with empty label.
+ ref2, err := app1.Append(0, labels.FromStrings("a", "b", "c", ""), 0, 124, 0, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+
+ // Should be the same series.
+ require.Equal(t, ref1, ref2)
+
+ err = app1.Commit()
+ require.NoError(t, err)
+}
+
+func TestDBAppenderV2_EmptyLabelsetCausesError(t *testing.T) {
+ db := newTestDB(t)
+
+ ctx := context.Background()
+ app := db.AppenderV2(ctx)
+ _, err := app.Append(0, labels.Labels{}, 0, 0, 0, nil, nil, storage.AOptions{})
+ require.Error(t, err)
+ require.Equal(t, "empty labelset: invalid sample", err.Error())
+}
+
+func TestDeleteSimple_AppendV2(t *testing.T) {
+ const numSamples int64 = 10
+
+ cases := []struct {
+ Intervals tombstones.Intervals
+ remaint []int64
+ }{
+ {
+ Intervals: tombstones.Intervals{{Mint: 0, Maxt: 3}},
+ remaint: []int64{4, 5, 6, 7, 8, 9},
+ },
+ {
+ Intervals: tombstones.Intervals{{Mint: 1, Maxt: 3}},
+ remaint: []int64{0, 4, 5, 6, 7, 8, 9},
+ },
+ {
+ Intervals: tombstones.Intervals{{Mint: 1, Maxt: 3}, {Mint: 4, Maxt: 7}},
+ remaint: []int64{0, 8, 9},
+ },
+ {
+ Intervals: tombstones.Intervals{{Mint: 1, Maxt: 3}, {Mint: 4, Maxt: 700}},
+ remaint: []int64{0},
+ },
+ { // This case is to ensure that labels and symbols are deleted.
+ Intervals: tombstones.Intervals{{Mint: 0, Maxt: 9}},
+ remaint: []int64{},
+ },
+ }
+
+ for _, c := range cases {
+ t.Run("", func(t *testing.T) {
+ db := newTestDB(t)
+
+ ctx := context.Background()
+ app := db.AppenderV2(ctx)
+
+ smpls := make([]float64, numSamples)
+ for i := range numSamples {
+ smpls[i] = rand.Float64()
+ app.Append(0, labels.FromStrings("a", "b"), 0, i, smpls[i], nil, nil, storage.AOptions{})
+ }
+
+ require.NoError(t, app.Commit())
+
+ // TODO(gouthamve): Reset the tombstones somehow.
+ // Delete the ranges.
+ for _, r := range c.Intervals {
+ require.NoError(t, db.Delete(ctx, r.Mint, r.Maxt, labels.MustNewMatcher(labels.MatchEqual, "a", "b")))
+ }
+
+ // Compare the result.
+ q, err := db.Querier(0, numSamples)
+ require.NoError(t, err)
+
+ res := q.Select(ctx, false, nil, labels.MustNewMatcher(labels.MatchEqual, "a", "b"))
+
+ expSamples := make([]chunks.Sample, 0, len(c.remaint))
+ for _, ts := range c.remaint {
+ expSamples = append(expSamples, sample{0, ts, smpls[ts], nil, nil})
+ }
+
+ expss := newMockSeriesSet([]storage.Series{
+ storage.NewListSeries(labels.FromStrings("a", "b"), expSamples),
+ })
+
+ for {
+ eok, rok := expss.Next(), res.Next()
+ require.Equal(t, eok, rok)
+
+ if !eok {
+ require.Empty(t, res.Warnings())
+ break
+ }
+ sexp := expss.At()
+ sres := res.At()
+
+ require.Equal(t, sexp.Labels(), sres.Labels())
+
+ smplExp, errExp := storage.ExpandSamples(sexp.Iterator(nil), nil)
+ smplRes, errRes := storage.ExpandSamples(sres.Iterator(nil), nil)
+
+ require.Equal(t, errExp, errRes)
+ require.Equal(t, smplExp, smplRes)
+ }
+ })
+ }
+}
+
+func TestAmendHistogramDatapointCausesError_AppendV2(t *testing.T) {
+ db := newTestDB(t)
+
+ ctx := context.Background()
+ app := db.AppenderV2(ctx)
+ _, err := app.Append(0, labels.FromStrings("a", "b"), 0, 0, 0, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+
+ app = db.AppenderV2(ctx)
+ _, err = app.Append(0, labels.FromStrings("a", "b"), 0, 0, 0, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ _, err = app.Append(0, labels.FromStrings("a", "b"), 0, 0, 1, nil, nil, storage.AOptions{})
+ require.ErrorIs(t, err, storage.ErrDuplicateSampleForTimestamp)
+ require.NoError(t, app.Rollback())
+
+ h := histogram.Histogram{
+ Schema: 3,
+ Count: 52,
+ Sum: 2.7,
+ ZeroThreshold: 0.1,
+ ZeroCount: 42,
+ PositiveSpans: []histogram.Span{
+ {Offset: 0, Length: 4},
+ {Offset: 10, Length: 3},
+ },
+ PositiveBuckets: []int64{1, 2, -2, 1, -1, 0, 0},
+ }
+ fh := h.ToFloat(nil)
+
+ app = db.AppenderV2(ctx)
+ _, err = app.Append(0, labels.FromStrings("a", "c"), 0, 0, 0, h.Copy(), nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+
+ app = db.AppenderV2(ctx)
+ _, err = app.Append(0, labels.FromStrings("a", "c"), 0, 0, 0, h.Copy(), nil, storage.AOptions{})
+ require.NoError(t, err)
+ h.Schema = 2
+ _, err = app.Append(0, labels.FromStrings("a", "c"), 0, 0, 0, h.Copy(), nil, storage.AOptions{})
+ require.Equal(t, storage.ErrDuplicateSampleForTimestamp, err)
+ require.NoError(t, app.Rollback())
+
+ // Float histogram.
+ app = db.AppenderV2(ctx)
+ _, err = app.Append(0, labels.FromStrings("a", "d"), 0, 0, 0, nil, fh.Copy(), storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+
+ app = db.AppenderV2(ctx)
+ _, err = app.Append(0, labels.FromStrings("a", "d"), 0, 0, 0, nil, fh.Copy(), storage.AOptions{})
+ require.NoError(t, err)
+ fh.Schema = 2
+ _, err = app.Append(0, labels.FromStrings("a", "d"), 0, 0, 0, nil, fh.Copy(), storage.AOptions{})
+ require.Equal(t, storage.ErrDuplicateSampleForTimestamp, err)
+ require.NoError(t, app.Rollback())
+}
+
+func TestDuplicateNaNDatapointNoAmendError_AppendV2(t *testing.T) {
+ db := newTestDB(t)
+
+ ctx := context.Background()
+ app := db.AppenderV2(ctx)
+ _, err := app.Append(0, labels.FromStrings("a", "b"), 0, 0, math.NaN(), nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+
+ app = db.AppenderV2(ctx)
+ _, err = app.Append(0, labels.FromStrings("a", "b"), 0, 0, math.NaN(), nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+}
+
+func TestNonDuplicateNaNDatapointsCausesAmendError_AppendV2(t *testing.T) {
+ db := newTestDB(t)
+
+ ctx := context.Background()
+ app := db.AppenderV2(ctx)
+ _, err := app.Append(0, labels.FromStrings("a", "b"), 0, 0, math.Float64frombits(0x7ff0000000000001), nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+
+ app = db.AppenderV2(ctx)
+ _, err = app.Append(0, labels.FromStrings("a", "b"), 0, 0, math.Float64frombits(0x7ff0000000000002), nil, nil, storage.AOptions{})
+ require.ErrorIs(t, err, storage.ErrDuplicateSampleForTimestamp)
+}
+
+func TestSkippingInvalidValuesInSameTxn_AppendV2(t *testing.T) {
+ db := newTestDB(t)
+
+ // Append AmendedValue.
+ ctx := context.Background()
+ app := db.AppenderV2(ctx)
+ _, err := app.Append(0, labels.FromStrings("a", "b"), 0, 0, 1, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ _, err = app.Append(0, labels.FromStrings("a", "b"), 0, 0, 2, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+
+ // Make sure the right value is stored.
+ q, err := db.Querier(0, 10)
+ require.NoError(t, err)
+
+ 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, 0, 1, nil, nil}},
+ }, ssMap)
+
+ // Append Out of Order Value.
+ app = db.AppenderV2(ctx)
+ _, err = app.Append(0, labels.FromStrings("a", "b"), 0, 10, 3, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ _, err = app.Append(0, labels.FromStrings("a", "b"), 0, 7, 5, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+
+ q, err = db.Querier(0, 10)
+ require.NoError(t, err)
+
+ 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, 0, 1, nil, nil}, sample{0, 10, 3, nil, nil}},
+ }, ssMap)
+}
+
+func TestDB_Snapshot_AppendV2(t *testing.T) {
+ db := newTestDB(t)
+
+ // append data
+ ctx := context.Background()
+ app := db.AppenderV2(ctx)
+ mint := int64(1414141414000)
+ for i := range 1000 {
+ _, err := app.Append(0, labels.FromStrings("foo", "bar"), 0, mint+int64(i), 1.0, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ }
+ require.NoError(t, app.Commit())
+
+ // create snapshot
+ snap := t.TempDir()
+ require.NoError(t, db.Snapshot(snap, true))
+ require.NoError(t, db.Close())
+
+ // reopen DB from snapshot
+ db = newTestDB(t, withDir(snap))
+
+ querier, err := db.Querier(mint, mint+1000)
+ require.NoError(t, err)
+ defer func() { require.NoError(t, querier.Close()) }()
+
+ // sum values
+ seriesSet := querier.Select(context.Background(), false, nil, labels.MustNewMatcher(labels.MatchEqual, "foo", "bar"))
+ var series chunkenc.Iterator
+ sum := 0.0
+ for seriesSet.Next() {
+ series = seriesSet.At().Iterator(series)
+ for series.Next() == chunkenc.ValFloat {
+ _, v := series.At()
+ sum += v
+ }
+ require.NoError(t, series.Err())
+ }
+ require.NoError(t, seriesSet.Err())
+ require.Empty(t, seriesSet.Warnings())
+ require.Equal(t, 1000.0, sum)
+}
+
+// TestDB_Snapshot_ChunksOutsideOfCompactedRange ensures that a snapshot removes chunks samples
+// that are outside the set block time range.
+// See https://github.com/prometheus/prometheus/issues/5105
+func TestDB_Snapshot_ChunksOutsideOfCompactedRange_AppendV2(t *testing.T) {
+ db := newTestDB(t)
+
+ ctx := context.Background()
+ app := db.AppenderV2(ctx)
+ mint := int64(1414141414000)
+ for i := range 1000 {
+ _, err := app.Append(0, labels.FromStrings("foo", "bar"), 0, mint+int64(i), 1.0, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ }
+ require.NoError(t, app.Commit())
+
+ snap := t.TempDir()
+
+ // Hackingly introduce "race", by having lower max time then maxTime in last chunk.
+ db.head.maxTime.Sub(10)
+
+ require.NoError(t, db.Snapshot(snap, true))
+ require.NoError(t, db.Close())
+
+ // reopen DB from snapshot
+ db = newTestDB(t, withDir(snap))
+
+ querier, err := db.Querier(mint, mint+1000)
+ require.NoError(t, err)
+ defer func() { require.NoError(t, querier.Close()) }()
+
+ // Sum values.
+ seriesSet := querier.Select(ctx, false, nil, labels.MustNewMatcher(labels.MatchEqual, "foo", "bar"))
+ var series chunkenc.Iterator
+ sum := 0.0
+ for seriesSet.Next() {
+ series = seriesSet.At().Iterator(series)
+ for series.Next() == chunkenc.ValFloat {
+ _, v := series.At()
+ sum += v
+ }
+ require.NoError(t, series.Err())
+ }
+ require.NoError(t, seriesSet.Err())
+ require.Empty(t, seriesSet.Warnings())
+
+ // Since we snapshotted with MaxTime - 10, so expect 10 less samples.
+ require.Equal(t, 1000.0-10, sum)
+}
+
+func TestDB_SnapshotWithDelete_AppendV2(t *testing.T) {
+ const numSamples int64 = 10
+
+ db := newTestDB(t)
+
+ ctx := context.Background()
+ app := db.AppenderV2(ctx)
+
+ smpls := make([]float64, numSamples)
+ for i := range numSamples {
+ smpls[i] = rand.Float64()
+ app.Append(0, labels.FromStrings("a", "b"), 0, i, smpls[i], nil, nil, storage.AOptions{})
+ }
+
+ require.NoError(t, app.Commit())
+ cases := []struct {
+ intervals tombstones.Intervals
+ remaint []int64
+ }{
+ {
+ intervals: tombstones.Intervals{{Mint: 1, Maxt: 3}, {Mint: 4, Maxt: 7}},
+ remaint: []int64{0, 8, 9},
+ },
+ }
+
+ for _, c := range cases {
+ t.Run("", func(t *testing.T) {
+ // TODO(gouthamve): Reset the tombstones somehow.
+ // Delete the ranges.
+ for _, r := range c.intervals {
+ require.NoError(t, db.Delete(ctx, r.Mint, r.Maxt, labels.MustNewMatcher(labels.MatchEqual, "a", "b")))
+ }
+
+ // create snapshot
+ snap := t.TempDir()
+
+ require.NoError(t, db.Snapshot(snap, true))
+
+ // reopen DB from snapshot
+ db := newTestDB(t, withDir(snap))
+
+ // Compare the result.
+ q, err := db.Querier(0, numSamples)
+ require.NoError(t, err)
+ defer func() { require.NoError(t, q.Close()) }()
+
+ res := q.Select(ctx, false, nil, labels.MustNewMatcher(labels.MatchEqual, "a", "b"))
+
+ expSamples := make([]chunks.Sample, 0, len(c.remaint))
+ for _, ts := range c.remaint {
+ expSamples = append(expSamples, sample{0, ts, smpls[ts], nil, nil})
+ }
+
+ expss := newMockSeriesSet([]storage.Series{
+ storage.NewListSeries(labels.FromStrings("a", "b"), expSamples),
+ })
+
+ if len(expSamples) == 0 {
+ require.False(t, res.Next())
+ return
+ }
+
+ for {
+ eok, rok := expss.Next(), res.Next()
+ require.Equal(t, eok, rok)
+
+ if !eok {
+ require.Empty(t, res.Warnings())
+ break
+ }
+ sexp := expss.At()
+ sres := res.At()
+
+ require.Equal(t, sexp.Labels(), sres.Labels())
+
+ smplExp, errExp := storage.ExpandSamples(sexp.Iterator(nil), nil)
+ smplRes, errRes := storage.ExpandSamples(sres.Iterator(nil), nil)
+
+ require.Equal(t, errExp, errRes)
+ require.Equal(t, smplExp, smplRes)
+ }
+ })
+ }
+}
+
+func TestDB_e2e_AppendV2(t *testing.T) {
+ const (
+ numDatapoints = 1000
+ numRanges = 1000
+ timeInterval = int64(3)
+ )
+ // Create 8 series with 1000 data-points of different ranges and run queries.
+ lbls := [][]labels.Label{
+ {
+ {Name: "a", Value: "b"},
+ {Name: "instance", Value: "localhost:9090"},
+ {Name: "job", Value: "prometheus"},
+ },
+ {
+ {Name: "a", Value: "b"},
+ {Name: "instance", Value: "127.0.0.1:9090"},
+ {Name: "job", Value: "prometheus"},
+ },
+ {
+ {Name: "a", Value: "b"},
+ {Name: "instance", Value: "127.0.0.1:9090"},
+ {Name: "job", Value: "prom-k8s"},
+ },
+ {
+ {Name: "a", Value: "b"},
+ {Name: "instance", Value: "localhost:9090"},
+ {Name: "job", Value: "prom-k8s"},
+ },
+ {
+ {Name: "a", Value: "c"},
+ {Name: "instance", Value: "localhost:9090"},
+ {Name: "job", Value: "prometheus"},
+ },
+ {
+ {Name: "a", Value: "c"},
+ {Name: "instance", Value: "127.0.0.1:9090"},
+ {Name: "job", Value: "prometheus"},
+ },
+ {
+ {Name: "a", Value: "c"},
+ {Name: "instance", Value: "127.0.0.1:9090"},
+ {Name: "job", Value: "prom-k8s"},
+ },
+ {
+ {Name: "a", Value: "c"},
+ {Name: "instance", Value: "localhost:9090"},
+ {Name: "job", Value: "prom-k8s"},
+ },
+ }
+
+ seriesMap := map[string][]chunks.Sample{}
+ for _, l := range lbls {
+ seriesMap[labels.New(l...).String()] = []chunks.Sample{}
+ }
+
+ db := newTestDB(t)
+
+ ctx := context.Background()
+ app := db.AppenderV2(ctx)
+
+ for _, l := range lbls {
+ lset := labels.New(l...)
+ series := []chunks.Sample{}
+
+ ts := rand.Int63n(300)
+ for range numDatapoints {
+ v := rand.Float64()
+
+ 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)
+
+ ts += rand.Int63n(timeInterval) + 1
+ }
+
+ seriesMap[lset.String()] = series
+ }
+
+ require.NoError(t, app.Commit())
+
+ // Query each selector on 1000 random time-ranges.
+ queries := []struct {
+ ms []*labels.Matcher
+ }{
+ {
+ ms: []*labels.Matcher{labels.MustNewMatcher(labels.MatchEqual, "a", "b")},
+ },
+ {
+ ms: []*labels.Matcher{
+ labels.MustNewMatcher(labels.MatchEqual, "a", "b"),
+ labels.MustNewMatcher(labels.MatchEqual, "job", "prom-k8s"),
+ },
+ },
+ {
+ ms: []*labels.Matcher{
+ labels.MustNewMatcher(labels.MatchEqual, "a", "c"),
+ labels.MustNewMatcher(labels.MatchEqual, "instance", "localhost:9090"),
+ labels.MustNewMatcher(labels.MatchEqual, "job", "prometheus"),
+ },
+ },
+ // TODO: Add Regexp Matchers.
+ }
+
+ for _, qry := range queries {
+ matched := labels.Slice{}
+ for _, l := range lbls {
+ s := labels.Selector(qry.ms)
+ ls := labels.New(l...)
+ if s.Matches(ls) {
+ matched = append(matched, ls)
+ }
+ }
+
+ sort.Sort(matched)
+
+ for range numRanges {
+ mint := rand.Int63n(300)
+ maxt := mint + rand.Int63n(timeInterval*int64(numDatapoints))
+
+ expected := map[string][]chunks.Sample{}
+
+ // Build the mockSeriesSet.
+ for _, m := range matched {
+ smpls := boundedSamples(seriesMap[m.String()], mint, maxt)
+ if len(smpls) > 0 {
+ expected[m.String()] = smpls
+ }
+ }
+
+ q, err := db.Querier(mint, maxt)
+ require.NoError(t, err)
+
+ ss := q.Select(ctx, false, nil, qry.ms...)
+ result := map[string][]chunks.Sample{}
+
+ for ss.Next() {
+ x := ss.At()
+
+ smpls, err := storage.ExpandSamples(x.Iterator(nil), newSample)
+ require.NoError(t, err)
+
+ if len(smpls) > 0 {
+ result[x.Labels().String()] = smpls
+ }
+ }
+
+ require.NoError(t, ss.Err())
+ require.Empty(t, ss.Warnings())
+ require.Equal(t, expected, result)
+
+ q.Close()
+ }
+ }
+}
+
+func TestWALFlushedOnDBClose_AppendV2(t *testing.T) {
+ db := newTestDB(t)
+
+ lbls := labels.FromStrings("labelname", "labelvalue")
+
+ ctx := context.Background()
+ app := db.AppenderV2(ctx)
+ _, err := app.Append(0, lbls, 0, 0, 1, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+
+ require.NoError(t, db.Close())
+
+ db = newTestDB(t, withDir(db.Dir()))
+
+ q, err := db.Querier(0, 1)
+ require.NoError(t, err)
+
+ values, ws, err := q.LabelValues(ctx, "labelname", nil)
+ require.NoError(t, err)
+ require.Empty(t, ws)
+ require.Equal(t, []string{"labelvalue"}, values)
+}
+
+func TestWALSegmentSizeOptions_AppendV2(t *testing.T) {
+ tests := map[int]func(dbdir string, segmentSize int){
+ // Default Wal Size.
+ 0: func(dbDir string, _ int) {
+ filesAndDir, err := os.ReadDir(filepath.Join(dbDir, "wal"))
+ require.NoError(t, err)
+ files := []os.FileInfo{}
+ for _, f := range filesAndDir {
+ if !f.IsDir() {
+ fi, err := f.Info()
+ require.NoError(t, err)
+ files = append(files, fi)
+ }
+ }
+ // All the full segment files (all but the last) should match the segment size option.
+ for _, f := range files[:len(files)-1] {
+ require.Equal(t, int64(DefaultOptions().WALSegmentSize), f.Size(), "WAL file size doesn't match WALSegmentSize option, filename: %v", f.Name())
+ }
+ lastFile := files[len(files)-1]
+ require.Greater(t, int64(DefaultOptions().WALSegmentSize), lastFile.Size(), "last WAL file size is not smaller than the WALSegmentSize option, filename: %v", lastFile.Name())
+ },
+ // Custom Wal Size.
+ 2 * 32 * 1024: func(dbDir string, segmentSize int) {
+ filesAndDir, err := os.ReadDir(filepath.Join(dbDir, "wal"))
+ require.NoError(t, err)
+ files := []os.FileInfo{}
+ for _, f := range filesAndDir {
+ if !f.IsDir() {
+ fi, err := f.Info()
+ require.NoError(t, err)
+ files = append(files, fi)
+ }
+ }
+ require.NotEmpty(t, files, "current WALSegmentSize should result in more than a single WAL file.")
+ // All the full segment files (all but the last) should match the segment size option.
+ for _, f := range files[:len(files)-1] {
+ require.Equal(t, int64(segmentSize), f.Size(), "WAL file size doesn't match WALSegmentSize option, filename: %v", f.Name())
+ }
+ lastFile := files[len(files)-1]
+ require.Greater(t, int64(segmentSize), lastFile.Size(), "last WAL file size is not smaller than the WALSegmentSize option, filename: %v", lastFile.Name())
+ },
+ // Wal disabled.
+ -1: func(dbDir string, _ int) {
+ // Check that WAL dir is not there.
+ _, err := os.Stat(filepath.Join(dbDir, "wal"))
+ require.Error(t, err)
+ // Check that there is chunks dir.
+ _, err = os.Stat(mmappedChunksDir(dbDir))
+ require.NoError(t, err)
+ },
+ }
+ for segmentSize, testFunc := range tests {
+ t.Run(fmt.Sprintf("WALSegmentSize %d test", segmentSize), func(t *testing.T) {
+ opts := DefaultOptions()
+ opts.WALSegmentSize = segmentSize
+ db := newTestDB(t, withOpts(opts))
+
+ for i := range int64(155) {
+ app := db.AppenderV2(context.Background())
+ ref, err := app.Append(0, labels.FromStrings("wal"+strconv.Itoa(int(i)), "size"), 0, i, rand.Float64(), nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ for j := int64(1); j <= 78; j++ {
+ _, err := app.Append(ref, labels.EmptyLabels(), 0, i+j, rand.Float64(), nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ }
+ require.NoError(t, app.Commit())
+ }
+
+ require.NoError(t, db.Close())
+ testFunc(db.Dir(), opts.WALSegmentSize)
+ })
+ }
+}
+
+// https://github.com/prometheus/prometheus/issues/9846
+// https://github.com/prometheus/prometheus/issues/9859
+func TestWALReplayRaceOnSamplesLoggedBeforeSeries_AppendV2(t *testing.T) {
+ const (
+ numRuns = 1
+ numSamplesBeforeSeriesCreation = 1000
+ )
+
+ // We test both with few and many samples appended after series creation. If samples are < 120 then there's no
+ // mmap-ed chunk, otherwise there's at least 1 mmap-ed chunk when replaying the WAL.
+ for _, numSamplesAfterSeriesCreation := range []int{1, 1000} {
+ for run := 1; run <= numRuns; run++ {
+ t.Run(fmt.Sprintf("samples after series creation = %d, run = %d", numSamplesAfterSeriesCreation, run), func(t *testing.T) {
+ testWALReplayRaceOnSamplesLoggedBeforeSeriesAppendV2(t, numSamplesBeforeSeriesCreation, numSamplesAfterSeriesCreation)
+ })
+ }
+ }
+}
+
+func testWALReplayRaceOnSamplesLoggedBeforeSeriesAppendV2(t *testing.T, numSamplesBeforeSeriesCreation, numSamplesAfterSeriesCreation int) {
+ const numSeries = 1000
+
+ db := newTestDB(t)
+ db.DisableCompactions()
+
+ for seriesRef := 1; seriesRef <= numSeries; seriesRef++ {
+ // Log samples before the series is logged to the WAL.
+ var enc record.Encoder
+ var samples []record.RefSample
+
+ for ts := range numSamplesBeforeSeriesCreation {
+ samples = append(samples, record.RefSample{
+ Ref: chunks.HeadSeriesRef(uint64(seriesRef)),
+ T: int64(ts),
+ V: float64(ts),
+ })
+ }
+
+ err := db.Head().wal.Log(enc.Samples(samples, nil))
+ require.NoError(t, err)
+
+ // Add samples via appender so that they're logged after the series in the WAL.
+ app := db.AppenderV2(context.Background())
+ lbls := labels.FromStrings("series_id", strconv.Itoa(seriesRef))
+
+ for ts := numSamplesBeforeSeriesCreation; ts < numSamplesBeforeSeriesCreation+numSamplesAfterSeriesCreation; ts++ {
+ _, err := app.Append(0, lbls, 0, int64(ts), float64(ts), nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ }
+ require.NoError(t, app.Commit())
+ }
+
+ require.NoError(t, db.Close())
+
+ // Reopen the DB, replaying the WAL.
+ db = newTestDB(t, withDir(db.Dir()))
+
+ // Query back chunks for all series.
+ q, err := db.ChunkQuerier(math.MinInt64, math.MaxInt64)
+ require.NoError(t, err)
+
+ set := q.Select(context.Background(), false, nil, labels.MustNewMatcher(labels.MatchRegexp, "series_id", ".+"))
+ actualSeries := 0
+ var chunksIt chunks.Iterator
+
+ for set.Next() {
+ actualSeries++
+ actualChunks := 0
+
+ chunksIt = set.At().Iterator(chunksIt)
+ for chunksIt.Next() {
+ actualChunks++
+ }
+ require.NoError(t, chunksIt.Err())
+
+ // We expect 1 chunk every 120 samples after series creation.
+ require.Equalf(t, (numSamplesAfterSeriesCreation/120)+1, actualChunks, "series: %s", set.At().Labels().String())
+ }
+
+ require.NoError(t, set.Err())
+ require.Equal(t, numSeries, actualSeries)
+}
+
+func TestTombstoneClean_AppendV2(t *testing.T) {
+ t.Parallel()
+ const numSamples int64 = 10
+
+ db := newTestDB(t)
+
+ ctx := context.Background()
+ app := db.AppenderV2(ctx)
+
+ smpls := make([]float64, numSamples)
+ for i := range numSamples {
+ smpls[i] = rand.Float64()
+ app.Append(0, labels.FromStrings("a", "b"), 0, i, smpls[i], nil, nil, storage.AOptions{})
+ }
+
+ require.NoError(t, app.Commit())
+ cases := []struct {
+ intervals tombstones.Intervals
+ remaint []int64
+ }{
+ {
+ intervals: tombstones.Intervals{{Mint: 1, Maxt: 3}, {Mint: 4, Maxt: 7}},
+ remaint: []int64{0, 8, 9},
+ },
+ }
+
+ for _, c := range cases {
+ // Delete the ranges.
+
+ // Create snapshot.
+ snap := t.TempDir()
+ require.NoError(t, db.Snapshot(snap, true))
+ require.NoError(t, db.Close())
+
+ // Reopen DB from snapshot.
+ db := newTestDB(t, withDir(snap))
+
+ for _, r := range c.intervals {
+ require.NoError(t, db.Delete(ctx, r.Mint, r.Maxt, labels.MustNewMatcher(labels.MatchEqual, "a", "b")))
+ }
+
+ // All of the setup for THIS line.
+ require.NoError(t, db.CleanTombstones())
+
+ // Compare the result.
+ q, err := db.Querier(0, numSamples)
+ require.NoError(t, err)
+ defer q.Close()
+
+ res := q.Select(ctx, false, nil, labels.MustNewMatcher(labels.MatchEqual, "a", "b"))
+
+ expSamples := make([]chunks.Sample, 0, len(c.remaint))
+ for _, ts := range c.remaint {
+ expSamples = append(expSamples, sample{0, ts, smpls[ts], nil, nil})
+ }
+
+ expss := newMockSeriesSet([]storage.Series{
+ storage.NewListSeries(labels.FromStrings("a", "b"), expSamples),
+ })
+
+ if len(expSamples) == 0 {
+ require.False(t, res.Next())
+ continue
+ }
+
+ for {
+ eok, rok := expss.Next(), res.Next()
+ require.Equal(t, eok, rok)
+
+ if !eok {
+ break
+ }
+ sexp := expss.At()
+ sres := res.At()
+
+ require.Equal(t, sexp.Labels(), sres.Labels())
+
+ smplExp, errExp := storage.ExpandSamples(sexp.Iterator(nil), nil)
+ smplRes, errRes := storage.ExpandSamples(sres.Iterator(nil), nil)
+
+ require.Equal(t, errExp, errRes)
+ require.Equal(t, smplExp, smplRes)
+ }
+ require.Empty(t, res.Warnings())
+
+ for _, b := range db.Blocks() {
+ require.Equal(t, tombstones.NewMemTombstones(), b.tombstones)
+ }
+ }
+}
+
+// TestTombstoneCleanResultEmptyBlock tests that a TombstoneClean that results in empty blocks (no timeseries)
+// will also delete the resultant block.
+func TestTombstoneCleanResultEmptyBlock_AppendV2(t *testing.T) {
+ t.Parallel()
+ numSamples := int64(10)
+
+ db := newTestDB(t)
+
+ ctx := context.Background()
+ app := db.AppenderV2(ctx)
+
+ smpls := make([]float64, numSamples)
+ for i := range numSamples {
+ smpls[i] = rand.Float64()
+ app.Append(0, labels.FromStrings("a", "b"), 0, i, smpls[i], nil, nil, storage.AOptions{})
+ }
+
+ require.NoError(t, app.Commit())
+ // Interval should cover the whole block.
+ intervals := tombstones.Intervals{{Mint: 0, Maxt: numSamples}}
+
+ // Create snapshot.
+ snap := t.TempDir()
+ require.NoError(t, db.Snapshot(snap, true))
+ require.NoError(t, db.Close())
+
+ // Reopen DB from snapshot.
+ db = newTestDB(t, withDir(snap))
+
+ // Create tombstones by deleting all samples.
+ for _, r := range intervals {
+ require.NoError(t, db.Delete(ctx, r.Mint, r.Maxt, labels.MustNewMatcher(labels.MatchEqual, "a", "b")))
+ }
+
+ require.NoError(t, db.CleanTombstones())
+
+ // After cleaning tombstones that covers the entire block, no blocks should be left behind.
+ actualBlockDirs, err := blockDirs(db.Dir())
+ require.NoError(t, err)
+ require.Empty(t, actualBlockDirs)
+}
+
+func TestSizeRetention_AppendV2(t *testing.T) {
+ t.Parallel()
+ opts := DefaultOptions()
+ opts.OutOfOrderTimeWindow = 100
+ db := newTestDB(t, withOpts(opts), withRngs(100))
+
+ blocks := []*BlockMeta{
+ {MinTime: 100, MaxTime: 200}, // Oldest block
+ {MinTime: 200, MaxTime: 300},
+ {MinTime: 300, MaxTime: 400},
+ {MinTime: 400, MaxTime: 500},
+ {MinTime: 500, MaxTime: 600}, // Newest Block
+ }
+
+ for _, m := range blocks {
+ createBlock(t, db.Dir(), genSeries(100, 10, m.MinTime, m.MaxTime))
+ }
+
+ headBlocks := []*BlockMeta{
+ {MinTime: 700, MaxTime: 800},
+ }
+
+ // Add some data to the WAL.
+ headApp := db.Head().AppenderV2(context.Background())
+ var aSeries labels.Labels
+ var it chunkenc.Iterator
+ for _, m := range headBlocks {
+ series := genSeries(100, 10, m.MinTime, m.MaxTime+1)
+ for _, s := range series {
+ aSeries = s.Labels()
+ it = s.Iterator(it)
+ for it.Next() == chunkenc.ValFloat {
+ tim, v := it.At()
+ _, err := headApp.Append(0, s.Labels(), 0, tim, v, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ }
+ require.NoError(t, it.Err())
+ }
+ }
+ require.NoError(t, headApp.Commit())
+ db.Head().mmapHeadChunks()
+
+ require.Eventually(t, func() bool {
+ return db.Head().chunkDiskMapper.IsQueueEmpty()
+ }, 2*time.Second, 100*time.Millisecond)
+
+ // Test that registered size matches the actual disk size.
+ require.NoError(t, db.reloadBlocks()) // Reload the db to register the new db size.
+ require.Len(t, db.Blocks(), len(blocks)) // Ensure all blocks are registered.
+ blockSize := int64(prom_testutil.ToFloat64(db.metrics.blocksBytes)) // Use the actual internal metrics.
+ walSize, err := db.Head().wal.Size()
+ require.NoError(t, err)
+ cdmSize, err := db.Head().chunkDiskMapper.Size()
+ require.NoError(t, err)
+ require.NotZero(t, cdmSize)
+ // Expected size should take into account block size + WAL size + Head
+ // chunks size
+ expSize := blockSize + walSize + cdmSize
+ actSize, err := fileutil.DirSize(db.Dir())
+ require.NoError(t, err)
+ require.Equal(t, expSize, actSize, "registered size doesn't match actual disk size")
+
+ // Create a WAL checkpoint, and compare sizes.
+ first, last, err := wlog.Segments(db.Head().wal.Dir())
+ require.NoError(t, err)
+ _, err = wlog.Checkpoint(promslog.NewNopLogger(), db.Head().wal, first, last-1, func(chunks.HeadSeriesRef) bool { return false }, 0)
+ require.NoError(t, err)
+ blockSize = int64(prom_testutil.ToFloat64(db.metrics.blocksBytes)) // Use the actual internal metrics.
+ walSize, err = db.Head().wal.Size()
+ require.NoError(t, err)
+ cdmSize, err = db.Head().chunkDiskMapper.Size()
+ require.NoError(t, err)
+ require.NotZero(t, cdmSize)
+ expSize = blockSize + walSize + cdmSize
+ actSize, err = fileutil.DirSize(db.Dir())
+ require.NoError(t, err)
+ require.Equal(t, expSize, actSize, "registered size doesn't match actual disk size")
+
+ // Truncate Chunk Disk Mapper and compare sizes.
+ require.NoError(t, db.Head().chunkDiskMapper.Truncate(900))
+ cdmSize, err = db.Head().chunkDiskMapper.Size()
+ require.NoError(t, err)
+ require.NotZero(t, cdmSize)
+ expSize = blockSize + walSize + cdmSize
+ actSize, err = fileutil.DirSize(db.Dir())
+ require.NoError(t, err)
+ require.Equal(t, expSize, actSize, "registered size doesn't match actual disk size")
+
+ // Add some out of order samples to check the size of WBL.
+ headApp = db.Head().AppenderV2(context.Background())
+ for ts := int64(750); ts < 800; ts++ {
+ _, err := headApp.Append(0, aSeries, 0, ts, float64(ts), nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ }
+ require.NoError(t, headApp.Commit())
+
+ walSize, err = db.Head().wal.Size()
+ require.NoError(t, err)
+ wblSize, err := db.Head().wbl.Size()
+ require.NoError(t, err)
+ require.NotZero(t, wblSize)
+ cdmSize, err = db.Head().chunkDiskMapper.Size()
+ require.NoError(t, err)
+ expSize = blockSize + walSize + wblSize + cdmSize
+ actSize, err = fileutil.DirSize(db.Dir())
+ require.NoError(t, err)
+ require.Equal(t, expSize, actSize, "registered size doesn't match actual disk size")
+
+ // Decrease the max bytes limit so that a delete is triggered.
+ // Check total size, total count and check that the oldest block was deleted.
+ firstBlockSize := db.Blocks()[0].Size()
+ sizeLimit := actSize - firstBlockSize
+ db.opts.MaxBytes = sizeLimit // Set the new db size limit one block smaller that the actual size.
+ require.NoError(t, db.reloadBlocks()) // Reload the db to register the new db size.
+
+ expBlocks := blocks[1:]
+ actBlocks := db.Blocks()
+ blockSize = int64(prom_testutil.ToFloat64(db.metrics.blocksBytes))
+ walSize, err = db.Head().wal.Size()
+ require.NoError(t, err)
+ cdmSize, err = db.Head().chunkDiskMapper.Size()
+ require.NoError(t, err)
+ require.NotZero(t, cdmSize)
+ // Expected size should take into account block size + WAL size + WBL size
+ expSize = blockSize + walSize + wblSize + cdmSize
+ actRetentionCount := int(prom_testutil.ToFloat64(db.metrics.sizeRetentionCount))
+ actSize, err = fileutil.DirSize(db.Dir())
+ require.NoError(t, err)
+
+ require.Equal(t, 1, actRetentionCount, "metric retention count mismatch")
+ require.Equal(t, expSize, actSize, "metric db size doesn't match actual disk size")
+ require.LessOrEqual(t, expSize, sizeLimit, "actual size (%v) is expected to be less than or equal to limit (%v)", expSize, sizeLimit)
+ require.Len(t, actBlocks, len(blocks)-1, "new block count should be decreased from:%v to:%v", len(blocks), len(blocks)-1)
+ require.Equal(t, expBlocks[0].MaxTime, actBlocks[0].meta.MaxTime, "maxT mismatch of the first block")
+ require.Equal(t, expBlocks[len(expBlocks)-1].MaxTime, actBlocks[len(actBlocks)-1].meta.MaxTime, "maxT mismatch of the last block")
+}
+
+func TestNotMatcherSelectsLabelsUnsetSeries_AppendV2(t *testing.T) {
+ db := newTestDB(t)
+
+ labelpairs := []labels.Labels{
+ labels.FromStrings("a", "abcd", "b", "abcde"),
+ labels.FromStrings("labelname", "labelvalue"),
+ }
+
+ ctx := context.Background()
+ app := db.AppenderV2(ctx)
+ for _, lbls := range labelpairs {
+ _, err := app.Append(0, lbls, 0, 0, 1, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ }
+ require.NoError(t, app.Commit())
+
+ cases := []struct {
+ selector labels.Selector
+ series []labels.Labels
+ }{{
+ selector: labels.Selector{
+ labels.MustNewMatcher(labels.MatchNotEqual, "lname", "lvalue"),
+ },
+ series: labelpairs,
+ }, {
+ selector: labels.Selector{
+ labels.MustNewMatcher(labels.MatchEqual, "a", "abcd"),
+ labels.MustNewMatcher(labels.MatchNotEqual, "b", "abcde"),
+ },
+ series: []labels.Labels{},
+ }, {
+ selector: labels.Selector{
+ labels.MustNewMatcher(labels.MatchEqual, "a", "abcd"),
+ labels.MustNewMatcher(labels.MatchNotEqual, "b", "abc"),
+ },
+ series: []labels.Labels{labelpairs[0]},
+ }, {
+ selector: labels.Selector{
+ labels.MustNewMatcher(labels.MatchNotRegexp, "a", "abd.*"),
+ },
+ series: labelpairs,
+ }, {
+ selector: labels.Selector{
+ labels.MustNewMatcher(labels.MatchNotRegexp, "a", "abc.*"),
+ },
+ series: labelpairs[1:],
+ }, {
+ selector: labels.Selector{
+ labels.MustNewMatcher(labels.MatchNotRegexp, "c", "abd.*"),
+ },
+ series: labelpairs,
+ }, {
+ selector: labels.Selector{
+ labels.MustNewMatcher(labels.MatchNotRegexp, "labelname", "labelvalue"),
+ },
+ series: labelpairs[:1],
+ }}
+
+ q, err := db.Querier(0, 10)
+ require.NoError(t, err)
+ defer func() { require.NoError(t, q.Close()) }()
+
+ for _, c := range cases {
+ ss := q.Select(ctx, false, nil, c.selector...)
+ lres, _, ws, err := expandSeriesSet(ss)
+ require.NoError(t, err)
+ require.Empty(t, ws)
+ require.Equal(t, c.series, lres)
+ }
+}
+
+// Regression test for https://github.com/prometheus/tsdb/issues/347
+func TestChunkAtBlockBoundary_AppendV2(t *testing.T) {
+ t.Parallel()
+ db := newTestDB(t)
+
+ ctx := context.Background()
+ app := db.AppenderV2(ctx)
+
+ blockRange := db.compactor.(*LeveledCompactor).ranges[0]
+ label := labels.FromStrings("foo", "bar")
+
+ for i := range int64(3) {
+ _, err := app.Append(0, label, 0, i*blockRange, 0, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ _, err = app.Append(0, label, 0, i*blockRange+1000, 0, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ }
+
+ err := app.Commit()
+ require.NoError(t, err)
+
+ err = db.Compact(ctx)
+ require.NoError(t, err)
+
+ var builder labels.ScratchBuilder
+
+ for _, block := range db.Blocks() {
+ r, err := block.Index()
+ require.NoError(t, err)
+ defer r.Close()
+
+ meta := block.Meta()
+
+ k, v := index.AllPostingsKey()
+ p, err := r.Postings(ctx, k, v)
+ require.NoError(t, err)
+
+ var chks []chunks.Meta
+
+ chunkCount := 0
+
+ for p.Next() {
+ err = r.Series(p.At(), &builder, &chks)
+ require.NoError(t, err)
+ for _, c := range chks {
+ require.True(t, meta.MinTime <= c.MinTime && c.MaxTime <= meta.MaxTime,
+ "chunk spans beyond block boundaries: [block.MinTime=%d, block.MaxTime=%d]; [chunk.MinTime=%d, chunk.MaxTime=%d]",
+ meta.MinTime, meta.MaxTime, c.MinTime, c.MaxTime)
+ chunkCount++
+ }
+ }
+ require.Equal(t, 1, chunkCount, "expected 1 chunk in block %s, got %d", meta.ULID, chunkCount)
+ }
+}
+
+func TestQuerierWithBoundaryChunks_AppendV2(t *testing.T) {
+ t.Parallel()
+ db := newTestDB(t)
+
+ ctx := context.Background()
+ app := db.AppenderV2(ctx)
+
+ blockRange := db.compactor.(*LeveledCompactor).ranges[0]
+ label := labels.FromStrings("foo", "bar")
+
+ for i := range int64(5) {
+ _, err := app.Append(0, label, 0, i*blockRange, 0, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ _, err = app.Append(0, labels.FromStrings("blockID", strconv.FormatInt(i, 10)), 0, i*blockRange, 0, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ }
+
+ err := app.Commit()
+ require.NoError(t, err)
+
+ err = db.Compact(ctx)
+ require.NoError(t, err)
+
+ require.GreaterOrEqual(t, len(db.blocks), 3, "invalid test, less than three blocks in DB")
+
+ q, err := db.Querier(blockRange, 2*blockRange)
+ require.NoError(t, err)
+ defer q.Close()
+
+ // The requested interval covers 2 blocks, so the querier's label values for blockID should give us 2 values, one from each block.
+ b, ws, err := q.LabelValues(ctx, "blockID", nil)
+ require.NoError(t, err)
+ var nilAnnotations annotations.Annotations
+ require.Equal(t, nilAnnotations, ws)
+ require.Equal(t, []string{"1", "2"}, b)
+}
+
+// TestInitializeHeadTimestamp ensures that the h.minTime is set properly.
+// - no blocks no WAL: set to the time of the first appended sample
+// - no blocks with WAL: set to the smallest sample from the WAL
+// - with blocks no WAL: set to the last block maxT
+// - with blocks with WAL: same as above
+func TestInitializeHeadTimestamp_AppendV2(t *testing.T) {
+ t.Parallel()
+ t.Run("clean", func(t *testing.T) {
+ db := newTestDB(t)
+
+ // Should be set to init values if no WAL or blocks exist so far.
+ require.Equal(t, int64(math.MaxInt64), db.head.MinTime())
+ require.Equal(t, int64(math.MinInt64), db.head.MaxTime())
+ require.False(t, db.head.initialized())
+
+ // First added sample initializes the writable range.
+ ctx := context.Background()
+ app := db.AppenderV2(ctx)
+ _, err := app.Append(0, labels.FromStrings("a", "b"), 0, 1000, 1, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+
+ require.Equal(t, int64(1000), db.head.MinTime())
+ require.Equal(t, int64(1000), db.head.MaxTime())
+ require.True(t, db.head.initialized())
+ })
+ t.Run("wal-only", func(t *testing.T) {
+ dir := t.TempDir()
+
+ require.NoError(t, os.MkdirAll(path.Join(dir, "wal"), 0o777))
+ w, err := wlog.New(nil, nil, path.Join(dir, "wal"), compression.None)
+ require.NoError(t, err)
+
+ var enc record.Encoder
+ err = w.Log(
+ enc.Series([]record.RefSeries{
+ {Ref: 123, Labels: labels.FromStrings("a", "1")},
+ {Ref: 124, Labels: labels.FromStrings("a", "2")},
+ }, nil),
+ enc.Samples([]record.RefSample{
+ {Ref: 123, T: 5000, V: 1},
+ {Ref: 124, T: 15000, V: 1},
+ }, nil),
+ )
+ require.NoError(t, err)
+ require.NoError(t, w.Close())
+
+ db := newTestDB(t, withDir(dir))
+
+ require.Equal(t, int64(5000), db.head.MinTime())
+ require.Equal(t, int64(15000), db.head.MaxTime())
+ require.True(t, db.head.initialized())
+ })
+ t.Run("existing-block", func(t *testing.T) {
+ dir := t.TempDir()
+
+ createBlock(t, dir, genSeries(1, 1, 1000, 2000))
+
+ db := newTestDB(t, withDir(dir))
+
+ require.Equal(t, int64(2000), db.head.MinTime())
+ require.Equal(t, int64(2000), db.head.MaxTime())
+ require.True(t, db.head.initialized())
+ })
+ t.Run("existing-block-and-wal", func(t *testing.T) {
+ dir := t.TempDir()
+
+ createBlock(t, dir, genSeries(1, 1, 1000, 6000))
+
+ require.NoError(t, os.MkdirAll(path.Join(dir, "wal"), 0o777))
+ w, err := wlog.New(nil, nil, path.Join(dir, "wal"), compression.None)
+ require.NoError(t, err)
+
+ var enc record.Encoder
+ err = w.Log(
+ enc.Series([]record.RefSeries{
+ {Ref: 123, Labels: labels.FromStrings("a", "1")},
+ {Ref: 124, Labels: labels.FromStrings("a", "2")},
+ }, nil),
+ enc.Samples([]record.RefSample{
+ {Ref: 123, T: 5000, V: 1},
+ {Ref: 124, T: 15000, V: 1},
+ }, nil),
+ )
+ require.NoError(t, err)
+ require.NoError(t, w.Close())
+
+ db := newTestDB(t, withDir(dir))
+
+ require.Equal(t, int64(6000), db.head.MinTime())
+ require.Equal(t, int64(15000), db.head.MaxTime())
+ require.True(t, db.head.initialized())
+ // Check that old series has been GCed.
+ require.Equal(t, 1.0, prom_testutil.ToFloat64(db.head.metrics.series))
+ })
+}
+
+func TestNoEmptyBlocks_AppendV2(t *testing.T) {
+ t.Parallel()
+ db := newTestDB(t, withRngs(100))
+ ctx := context.Background()
+
+ db.DisableCompactions()
+
+ rangeToTriggerCompaction := db.compactor.(*LeveledCompactor).ranges[0]/2*3 - 1
+ defaultLabel := labels.FromStrings("foo", "bar")
+ defaultMatcher := labels.MustNewMatcher(labels.MatchRegexp, "", ".*")
+
+ t.Run("Test no blocks after compact with empty head.", func(t *testing.T) {
+ require.NoError(t, db.Compact(ctx))
+ actBlocks, err := blockDirs(db.Dir())
+ require.NoError(t, err)
+ require.Len(t, actBlocks, len(db.Blocks()))
+ require.Empty(t, actBlocks)
+ require.Equal(t, 0, int(prom_testutil.ToFloat64(db.compactor.(*LeveledCompactor).metrics.Ran)), "no compaction should be triggered here")
+ })
+
+ t.Run("Test no blocks after deleting all samples from head.", func(t *testing.T) {
+ app := db.AppenderV2(ctx)
+ _, err := app.Append(0, defaultLabel, 0, 1, 0, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ _, err = app.Append(0, defaultLabel, 0, 2, 0, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ _, err = app.Append(0, defaultLabel, 0, 3+rangeToTriggerCompaction, 0, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ require.NoError(t, db.Delete(ctx, math.MinInt64, math.MaxInt64, defaultMatcher))
+ require.NoError(t, db.Compact(ctx))
+ require.Equal(t, 1, int(prom_testutil.ToFloat64(db.compactor.(*LeveledCompactor).metrics.Ran)), "compaction should have been triggered here")
+
+ actBlocks, err := blockDirs(db.Dir())
+ require.NoError(t, err)
+ require.Len(t, actBlocks, len(db.Blocks()))
+ require.Empty(t, actBlocks)
+
+ app = db.AppenderV2(ctx)
+ _, err = app.Append(0, defaultLabel, 0, 1, 0, nil, nil, storage.AOptions{})
+ require.Equal(t, storage.ErrOutOfBounds, err, "the head should be truncated so no samples in the past should be allowed")
+
+ // Adding new blocks.
+ currentTime := db.Head().MaxTime()
+ _, err = app.Append(0, defaultLabel, 0, currentTime, 0, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ _, err = app.Append(0, defaultLabel, 0, currentTime+1, 0, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ _, err = app.Append(0, defaultLabel, 0, currentTime+rangeToTriggerCompaction, 0, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+
+ require.NoError(t, db.Compact(ctx))
+ require.Equal(t, 2, int(prom_testutil.ToFloat64(db.compactor.(*LeveledCompactor).metrics.Ran)), "compaction should have been triggered here")
+ actBlocks, err = blockDirs(db.Dir())
+ require.NoError(t, err)
+ require.Len(t, actBlocks, len(db.Blocks()))
+ require.Len(t, actBlocks, 1, "No blocks created when compacting with >0 samples")
+ })
+
+ t.Run(`When no new block is created from head, and there are some blocks on disk
+ compaction should not run into infinite loop (was seen during development).`, func(t *testing.T) {
+ oldBlocks := db.Blocks()
+ app := db.AppenderV2(ctx)
+ currentTime := db.Head().MaxTime()
+ _, err := app.Append(0, defaultLabel, 0, currentTime, 0, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ _, err = app.Append(0, defaultLabel, 0, currentTime+1, 0, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ _, err = app.Append(0, defaultLabel, 0, currentTime+rangeToTriggerCompaction, 0, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ require.NoError(t, db.head.Delete(ctx, math.MinInt64, math.MaxInt64, defaultMatcher))
+ require.NoError(t, db.Compact(ctx))
+ require.Equal(t, 3, int(prom_testutil.ToFloat64(db.compactor.(*LeveledCompactor).metrics.Ran)), "compaction should have been triggered here")
+ require.Equal(t, oldBlocks, db.Blocks())
+ })
+
+ t.Run("Test no blocks remaining after deleting all samples from disk.", func(t *testing.T) {
+ currentTime := db.Head().MaxTime()
+ blocks := []*BlockMeta{
+ {MinTime: currentTime, MaxTime: currentTime + db.compactor.(*LeveledCompactor).ranges[0]},
+ {MinTime: currentTime + 100, MaxTime: currentTime + 100 + db.compactor.(*LeveledCompactor).ranges[0]},
+ }
+ for _, m := range blocks {
+ createBlock(t, db.Dir(), genSeries(2, 2, m.MinTime, m.MaxTime))
+ }
+
+ oldBlocks := db.Blocks()
+ require.NoError(t, db.reloadBlocks()) // Reload the db to register the new blocks.
+ require.Len(t, db.Blocks(), len(blocks)+len(oldBlocks)) // Ensure all blocks are registered.
+ require.NoError(t, db.Delete(ctx, math.MinInt64, math.MaxInt64, defaultMatcher))
+ require.NoError(t, db.Compact(ctx))
+ require.Equal(t, 5, int(prom_testutil.ToFloat64(db.compactor.(*LeveledCompactor).metrics.Ran)), "compaction should have been triggered here once for each block that have tombstones")
+
+ actBlocks, err := blockDirs(db.Dir())
+ require.NoError(t, err)
+ require.Len(t, actBlocks, len(db.Blocks()))
+ require.Len(t, actBlocks, 1, "All samples are deleted. Only the most recent block should remain after compaction.")
+ })
+}
+
+func TestDB_LabelNames_AppendV2(t *testing.T) {
+ ctx := context.Background()
+ tests := []struct {
+ // Add 'sampleLabels1' -> Test Head -> Compact -> Test Disk ->
+ // -> Add 'sampleLabels2' -> Test Head+Disk
+
+ sampleLabels1 [][2]string // For checking head and disk separately.
+ // To test Head+Disk, sampleLabels2 should have
+ // at least 1 unique label name which is not in sampleLabels1.
+ sampleLabels2 [][2]string // For checking head and disk together.
+ exp1 []string // after adding sampleLabels1.
+ exp2 []string // after adding sampleLabels1 and sampleLabels2.
+ }{
+ {
+ sampleLabels1: [][2]string{
+ {"name1", "1"},
+ {"name3", "3"},
+ {"name2", "2"},
+ },
+ sampleLabels2: [][2]string{
+ {"name4", "4"},
+ {"name1", "1"},
+ },
+ exp1: []string{"name1", "name2", "name3"},
+ exp2: []string{"name1", "name2", "name3", "name4"},
+ },
+ {
+ sampleLabels1: [][2]string{
+ {"name2", "2"},
+ {"name1", "1"},
+ {"name2", "2"},
+ },
+ sampleLabels2: [][2]string{
+ {"name6", "6"},
+ {"name0", "0"},
+ },
+ exp1: []string{"name1", "name2"},
+ exp2: []string{"name0", "name1", "name2", "name6"},
+ },
+ }
+
+ blockRange := int64(1000)
+ // Appends samples into the database.
+ appendSamples := func(db *DB, mint, maxt int64, sampleLabels [][2]string) {
+ t.Helper()
+ app := db.AppenderV2(ctx)
+ for i := mint; i <= maxt; i++ {
+ for _, tuple := range sampleLabels {
+ label := labels.FromStrings(tuple[0], tuple[1])
+ _, err := app.Append(0, label, 0, i*blockRange, 0, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ }
+ }
+ err := app.Commit()
+ require.NoError(t, err)
+ }
+ for _, tst := range tests {
+ t.Run("", func(t *testing.T) {
+ ctx := context.Background()
+ db := newTestDB(t)
+
+ appendSamples(db, 0, 4, tst.sampleLabels1)
+
+ // Testing head.
+ headIndexr, err := db.head.Index()
+ require.NoError(t, err)
+ labelNames, err := headIndexr.LabelNames(ctx)
+ require.NoError(t, err)
+ require.Equal(t, tst.exp1, labelNames)
+ require.NoError(t, headIndexr.Close())
+
+ // Testing disk.
+ err = db.Compact(ctx)
+ require.NoError(t, err)
+ // All blocks have same label names, hence check them individually.
+ // No need to aggregate and check.
+ for _, b := range db.Blocks() {
+ blockIndexr, err := b.Index()
+ require.NoError(t, err)
+ labelNames, err = blockIndexr.LabelNames(ctx)
+ require.NoError(t, err)
+ require.Equal(t, tst.exp1, labelNames)
+ require.NoError(t, blockIndexr.Close())
+ }
+
+ // Adding more samples to head with new label names
+ // so that we can test (head+disk).LabelNames(ctx) (the union).
+ appendSamples(db, 5, 9, tst.sampleLabels2)
+
+ // Testing DB (union).
+ q, err := db.Querier(math.MinInt64, math.MaxInt64)
+ require.NoError(t, err)
+ var ws annotations.Annotations
+ labelNames, ws, err = q.LabelNames(ctx, nil)
+ require.NoError(t, err)
+ require.Empty(t, ws)
+ require.NoError(t, q.Close())
+ require.Equal(t, tst.exp2, labelNames)
+ })
+ }
+}
+
+func TestCorrectNumTombstones_AppendV2(t *testing.T) {
+ t.Parallel()
+ db := newTestDB(t)
+
+ blockRange := db.compactor.(*LeveledCompactor).ranges[0]
+ name, value := "foo", "bar"
+ defaultLabel := labels.FromStrings(name, value)
+ defaultMatcher := labels.MustNewMatcher(labels.MatchEqual, name, value)
+
+ ctx := context.Background()
+ app := db.AppenderV2(ctx)
+ for i := range int64(3) {
+ for j := range int64(15) {
+ _, err := app.Append(0, defaultLabel, 0, i*blockRange+j, 0, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ }
+ }
+ require.NoError(t, app.Commit())
+
+ err := db.Compact(ctx)
+ require.NoError(t, err)
+ require.Len(t, db.blocks, 1)
+
+ require.NoError(t, db.Delete(ctx, 0, 1, defaultMatcher))
+ require.Equal(t, uint64(1), db.blocks[0].meta.Stats.NumTombstones)
+
+ // {0, 1} and {2, 3} are merged to form 1 tombstone.
+ require.NoError(t, db.Delete(ctx, 2, 3, defaultMatcher))
+ require.Equal(t, uint64(1), db.blocks[0].meta.Stats.NumTombstones)
+
+ require.NoError(t, db.Delete(ctx, 5, 6, defaultMatcher))
+ require.Equal(t, uint64(2), db.blocks[0].meta.Stats.NumTombstones)
+
+ require.NoError(t, db.Delete(ctx, 9, 11, defaultMatcher))
+ require.Equal(t, uint64(3), db.blocks[0].meta.Stats.NumTombstones)
+}
+
+// TestBlockRanges checks the following use cases:
+// - No samples can be added with timestamps lower than the last block maxt.
+// - The compactor doesn't create overlapping blocks
+//
+// even when the last blocks is not within the default boundaries.
+// - Lower boundary is based on the smallest sample in the head and
+//
+// upper boundary is rounded to the configured block range.
+//
+// This ensures that a snapshot that includes the head and creates a block with a custom time range
+// will not overlap with the first block created by the next compaction.
+func TestBlockRanges_AppendV2(t *testing.T) {
+ t.Parallel()
+ logger := promslog.New(&promslog.Config{})
+ ctx := context.Background()
+
+ dir := t.TempDir()
+
+ // Test that the compactor doesn't create overlapping blocks
+ // when a non standard block already exists.
+ firstBlockMaxT := int64(3)
+ createBlock(t, dir, genSeries(1, 1, 0, firstBlockMaxT))
+ db, err := open(dir, logger, nil, DefaultOptions(), []int64{10000}, nil)
+ require.NoError(t, err)
+ db.DisableCompactions()
+
+ rangeToTriggerCompaction := db.compactor.(*LeveledCompactor).ranges[0]/2*3 + 1
+
+ app := db.AppenderV2(ctx)
+ lbl := labels.FromStrings("a", "b")
+ _, err = app.Append(0, lbl, 0, firstBlockMaxT-1, rand.Float64(), nil, nil, storage.AOptions{})
+ require.Error(t, err, "appending a sample with a timestamp covered by a previous block shouldn't be possible")
+ _, err = app.Append(0, lbl, 0, firstBlockMaxT+1, rand.Float64(), nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ _, err = app.Append(0, lbl, 0, firstBlockMaxT+2, rand.Float64(), nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ secondBlockMaxt := firstBlockMaxT + rangeToTriggerCompaction
+ _, err = app.Append(0, lbl, 0, secondBlockMaxt, rand.Float64(), nil, nil, storage.AOptions{}) // Add samples to trigger a new compaction
+
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ require.NoError(t, db.Compact(ctx))
+ blocks := db.Blocks()
+ require.Len(t, blocks, 2, "no new block after compaction")
+
+ require.GreaterOrEqual(t, blocks[1].Meta().MinTime, blocks[0].Meta().MaxTime,
+ "new block overlaps old:%v,new:%v", blocks[0].Meta(), blocks[1].Meta())
+
+ // Test that wal records are skipped when an existing block covers the same time ranges
+ // and compaction doesn't create an overlapping block.
+ app = db.AppenderV2(ctx)
+ _, err = app.Append(0, lbl, 0, secondBlockMaxt+1, rand.Float64(), nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ _, err = app.Append(0, lbl, 0, secondBlockMaxt+2, rand.Float64(), nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ _, err = app.Append(0, lbl, 0, secondBlockMaxt+3, rand.Float64(), nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ _, err = app.Append(0, lbl, 0, secondBlockMaxt+4, rand.Float64(), nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ require.NoError(t, db.Close())
+
+ thirdBlockMaxt := secondBlockMaxt + 2
+ createBlock(t, dir, genSeries(1, 1, secondBlockMaxt+1, thirdBlockMaxt))
+
+ db, err = open(dir, logger, nil, DefaultOptions(), []int64{10000}, nil)
+ require.NoError(t, err)
+ db.DisableCompactions()
+
+ defer db.Close()
+ require.Len(t, db.Blocks(), 3, "db doesn't include expected number of blocks")
+ require.Equal(t, db.Blocks()[2].Meta().MaxTime, thirdBlockMaxt, "unexpected maxt of the last block")
+
+ app = db.AppenderV2(ctx)
+ _, err = app.Append(0, lbl, 0, thirdBlockMaxt+rangeToTriggerCompaction, rand.Float64(), nil, nil, storage.AOptions{}) // Trigger a compaction
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ require.NoError(t, db.Compact(ctx))
+ blocks = db.Blocks()
+ require.Len(t, blocks, 4, "no new block after compaction")
+
+ require.GreaterOrEqual(t, blocks[3].Meta().MinTime, blocks[2].Meta().MaxTime,
+ "new block overlaps old:%v,new:%v", blocks[2].Meta(), blocks[3].Meta())
+}
+
+// TestDBReadOnly ensures that opening a DB in readonly mode doesn't modify any files on the disk.
+// It also checks that the API calls return equivalent results as a normal db.Open() mode.
+func TestDBReadOnly_AppendV2(t *testing.T) {
+ t.Parallel()
+ var (
+ dbDir = t.TempDir()
+ logger = promslog.New(&promslog.Config{})
+ expBlocks []*Block
+ expBlock *Block
+ expSeries map[string][]chunks.Sample
+ expChunks map[string][][]chunks.Sample
+ expDBHash []byte
+ matchAll = labels.MustNewMatcher(labels.MatchEqual, "", "")
+ err error
+ )
+
+ // Bootstrap the db.
+ {
+ dbBlocks := []*BlockMeta{
+ // Create three 2-sample blocks.
+ {MinTime: 10, MaxTime: 12},
+ {MinTime: 12, MaxTime: 14},
+ {MinTime: 14, MaxTime: 16},
+ }
+
+ for _, m := range dbBlocks {
+ _ = createBlock(t, dbDir, genSeries(1, 1, m.MinTime, m.MaxTime))
+ }
+
+ // Add head to test DBReadOnly WAL reading capabilities.
+ w, err := wlog.New(logger, nil, filepath.Join(dbDir, "wal"), compression.Snappy)
+ require.NoError(t, err)
+ h := createHead(t, w, genSeries(1, 1, 16, 18), dbDir)
+ require.NoError(t, h.Close())
+ }
+
+ // Open a normal db to use for a comparison.
+ {
+ dbWritable := newTestDB(t, withDir(dbDir))
+ dbWritable.DisableCompactions()
+
+ dbSizeBeforeAppend, err := fileutil.DirSize(dbWritable.Dir())
+ require.NoError(t, err)
+ app := dbWritable.AppenderV2(context.Background())
+ _, err = app.Append(0, labels.FromStrings("foo", "bar"), 0, dbWritable.Head().MaxTime()+1, 0, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+
+ expBlocks = dbWritable.Blocks()
+ expBlock = expBlocks[0]
+ expDbSize, err := fileutil.DirSize(dbWritable.Dir())
+ require.NoError(t, err)
+ require.Greater(t, expDbSize, dbSizeBeforeAppend, "db size didn't increase after an append")
+
+ q, err := dbWritable.Querier(math.MinInt64, math.MaxInt64)
+ require.NoError(t, err)
+ expSeries = query(t, q, matchAll)
+ cq, err := dbWritable.ChunkQuerier(math.MinInt64, math.MaxInt64)
+ require.NoError(t, err)
+ expChunks = queryAndExpandChunks(t, cq, matchAll)
+
+ require.NoError(t, dbWritable.Close()) // Close here to allow getting the dir hash for windows.
+ expDBHash = testutil.DirHash(t, dbWritable.Dir())
+ }
+
+ // Open a read only db and ensure that the API returns the same result as the normal DB.
+ dbReadOnly, err := OpenDBReadOnly(dbDir, "", logger)
+ require.NoError(t, err)
+ defer func() { require.NoError(t, dbReadOnly.Close()) }()
+
+ t.Run("blocks", func(t *testing.T) {
+ blocks, err := dbReadOnly.Blocks()
+ require.NoError(t, err)
+ require.Len(t, blocks, len(expBlocks))
+ for i, expBlock := range expBlocks {
+ require.Equal(t, expBlock.Meta(), blocks[i].Meta(), "block meta mismatch")
+ }
+ })
+ t.Run("block", func(t *testing.T) {
+ blockID := expBlock.meta.ULID.String()
+ block, err := dbReadOnly.Block(blockID, nil)
+ require.NoError(t, err)
+ require.Equal(t, expBlock.Meta(), block.Meta(), "block meta mismatch")
+ })
+ t.Run("invalid block ID", func(t *testing.T) {
+ blockID := "01GTDVZZF52NSWB5SXQF0P2PGF"
+ _, err := dbReadOnly.Block(blockID, nil)
+ require.Error(t, err)
+ })
+ t.Run("last block ID", func(t *testing.T) {
+ blockID, err := dbReadOnly.LastBlockID()
+ require.NoError(t, err)
+ require.Equal(t, expBlocks[2].Meta().ULID.String(), blockID)
+ })
+ t.Run("querier", func(t *testing.T) {
+ // Open a read only db and ensure that the API returns the same result as the normal DB.
+ q, err := dbReadOnly.Querier(math.MinInt64, math.MaxInt64)
+ require.NoError(t, err)
+ readOnlySeries := query(t, q, matchAll)
+ readOnlyDBHash := testutil.DirHash(t, dbDir)
+
+ require.Len(t, readOnlySeries, len(expSeries), "total series mismatch")
+ require.Equal(t, expSeries, readOnlySeries, "series mismatch")
+ require.Equal(t, expDBHash, readOnlyDBHash, "after all read operations the db hash should remain the same")
+ })
+ t.Run("chunk querier", func(t *testing.T) {
+ cq, err := dbReadOnly.ChunkQuerier(math.MinInt64, math.MaxInt64)
+ require.NoError(t, err)
+ readOnlySeries := queryAndExpandChunks(t, cq, matchAll)
+ readOnlyDBHash := testutil.DirHash(t, dbDir)
+
+ require.Len(t, readOnlySeries, len(expChunks), "total series mismatch")
+ require.Equal(t, expChunks, readOnlySeries, "series chunks mismatch")
+ require.Equal(t, expDBHash, readOnlyDBHash, "after all read operations the db hash should remain the same")
+ })
+}
+
+func TestDBReadOnly_FlushWAL_AppendV2(t *testing.T) {
+ t.Parallel()
+ var (
+ dbDir = t.TempDir()
+ logger = promslog.New(&promslog.Config{})
+ err error
+ maxt int
+ ctx = context.Background()
+ )
+
+ // Bootstrap the db.
+ {
+ // Append data to the WAL.
+ db := newTestDB(t, withDir(dbDir))
+ db.DisableCompactions()
+ app := db.AppenderV2(ctx)
+ maxt = 1000
+ for i := 0; i < maxt; i++ {
+ _, err := app.Append(0, labels.FromStrings(defaultLabelName, "flush"), 0, int64(i), 1.0, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ }
+ require.NoError(t, app.Commit())
+ require.NoError(t, db.Close())
+ }
+
+ // Flush WAL.
+ db, err := OpenDBReadOnly(dbDir, "", logger)
+ require.NoError(t, err)
+
+ flush := t.TempDir()
+ require.NoError(t, db.FlushWAL(flush))
+ require.NoError(t, db.Close())
+
+ // Reopen the DB from the flushed WAL block.
+ db, err = OpenDBReadOnly(flush, "", logger)
+ require.NoError(t, err)
+ defer func() { require.NoError(t, db.Close()) }()
+ blocks, err := db.Blocks()
+ require.NoError(t, err)
+ require.Len(t, blocks, 1)
+
+ querier, err := db.Querier(0, int64(maxt)-1)
+ require.NoError(t, err)
+ defer func() { require.NoError(t, querier.Close()) }()
+
+ // Sum the values.
+ seriesSet := querier.Select(ctx, false, nil, labels.MustNewMatcher(labels.MatchEqual, defaultLabelName, "flush"))
+ var series chunkenc.Iterator
+
+ sum := 0.0
+ for seriesSet.Next() {
+ series = seriesSet.At().Iterator(series)
+ for series.Next() == chunkenc.ValFloat {
+ _, v := series.At()
+ sum += v
+ }
+ require.NoError(t, series.Err())
+ }
+ require.NoError(t, seriesSet.Err())
+ require.Empty(t, seriesSet.Warnings())
+ require.Equal(t, 1000.0, sum)
+}
+
+func TestDBReadOnly_Querier_NoAlteration_AppendV2(t *testing.T) {
+ countChunks := func(dir string) int {
+ files, err := os.ReadDir(mmappedChunksDir(dir))
+ require.NoError(t, err)
+ return len(files)
+ }
+
+ dirHash := func(dir string) (hash []byte) {
+ // Windows requires the DB to be closed: "xxx\lock: The process cannot access the file because it is being used by another process."
+ // But closing the DB alters the directory in this case (it'll cut a new chunk).
+ if runtime.GOOS != "windows" {
+ hash = testutil.DirHash(t, dir)
+ }
+ return hash
+ }
+
+ spinUpQuerierAndCheck := func(dir, sandboxDir string, chunksCount int) {
+ dBDirHash := dirHash(dir)
+ // Bootstrap a RO db from the same dir and set up a querier.
+ dbReadOnly, err := OpenDBReadOnly(dir, sandboxDir, nil)
+ require.NoError(t, err)
+ require.Equal(t, chunksCount, countChunks(dir))
+ q, err := dbReadOnly.Querier(math.MinInt, math.MaxInt)
+ require.NoError(t, err)
+ require.NoError(t, q.Close())
+ require.NoError(t, dbReadOnly.Close())
+ // The RO Head doesn't alter RW db chunks_head/.
+ require.Equal(t, chunksCount, countChunks(dir))
+ require.Equal(t, dirHash(dir), dBDirHash)
+ }
+
+ t.Run("doesn't cut chunks while replaying WAL", func(t *testing.T) {
+ db := newTestDB(t)
+
+ // Append until the first mmapped head chunk.
+ for i := range 121 {
+ app := db.AppenderV2(context.Background())
+ _, err := app.Append(0, labels.FromStrings("foo", "bar"), 0, int64(i), 0, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ }
+
+ spinUpQuerierAndCheck(db.Dir(), t.TempDir(), 0)
+
+ // The RW Head should have no problem cutting its own chunk,
+ // this also proves that a chunk needed to be cut.
+ require.NotPanics(t, func() { db.ForceHeadMMap() })
+ require.Equal(t, 1, countChunks(db.Dir()))
+ })
+
+ t.Run("doesn't truncate corrupted chunks", func(t *testing.T) {
+ db := newTestDB(t)
+ require.NoError(t, db.Close())
+
+ // Simulate a corrupted chunk: without a header.
+ chunk, err := os.Create(path.Join(mmappedChunksDir(db.Dir()), "000001"))
+ require.NoError(t, err)
+ require.NoError(t, chunk.Close())
+
+ spinUpQuerierAndCheck(db.Dir(), t.TempDir(), 1)
+
+ // The RW Head should have no problem truncating its corrupted file:
+ // this proves that the chunk needed to be truncated.
+ db = newTestDB(t, withDir(db.Dir()))
+
+ require.NoError(t, err)
+ require.Equal(t, 0, countChunks(db.Dir()))
+ })
+}
+
+func TestDBCannotSeePartialCommits_AppendV2(t *testing.T) {
+ if defaultIsolationDisabled {
+ t.Skip("skipping test since tsdb isolation is disabled")
+ }
+
+ db := newTestDB(t)
+
+ stop := make(chan struct{})
+ firstInsert := make(chan struct{})
+ ctx := context.Background()
+
+ // Insert data in batches.
+ go func() {
+ iter := 0
+ for {
+ app := db.AppenderV2(ctx)
+
+ for j := range 100 {
+ _, err := app.Append(0, labels.FromStrings("foo", "bar", "a", strconv.Itoa(j)), 0, int64(iter), float64(iter), nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ }
+ require.NoError(t, app.Commit())
+
+ if iter == 0 {
+ close(firstInsert)
+ }
+ iter++
+
+ select {
+ case <-stop:
+ return
+ default:
+ }
+ }
+ }()
+
+ <-firstInsert
+
+ // This is a race condition, so do a few tests to tickle it.
+ // Usually most will fail.
+ inconsistencies := 0
+ for range 10 {
+ func() {
+ querier, err := db.Querier(0, 1000000)
+ require.NoError(t, err)
+ defer querier.Close()
+
+ ss := querier.Select(ctx, false, nil, labels.MustNewMatcher(labels.MatchEqual, "foo", "bar"))
+ _, seriesSet, ws, err := expandSeriesSet(ss)
+ require.NoError(t, err)
+ require.Empty(t, ws)
+
+ values := map[float64]struct{}{}
+ for _, series := range seriesSet {
+ values[series[len(series)-1].f] = struct{}{}
+ }
+ if len(values) != 1 {
+ inconsistencies++
+ }
+ }()
+ }
+ stop <- struct{}{}
+
+ require.Equal(t, 0, inconsistencies, "Some queries saw inconsistent results.")
+}
+
+func TestDBQueryDoesntSeeAppendsAfterCreation_AppendV2(t *testing.T) {
+ if defaultIsolationDisabled {
+ t.Skip("skipping test since tsdb isolation is disabled")
+ }
+
+ db := newTestDB(t)
+ querierBeforeAdd, err := db.Querier(0, 1000000)
+ require.NoError(t, err)
+ defer querierBeforeAdd.Close()
+
+ ctx := context.Background()
+ app := db.AppenderV2(ctx)
+ _, err = app.Append(0, labels.FromStrings("foo", "bar"), 0, 0, 0, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+
+ querierAfterAddButBeforeCommit, err := db.Querier(0, 1000000)
+ require.NoError(t, err)
+ defer querierAfterAddButBeforeCommit.Close()
+
+ // None of the queriers should return anything after the Add but before the commit.
+ ss := querierBeforeAdd.Select(ctx, false, nil, labels.MustNewMatcher(labels.MatchEqual, "foo", "bar"))
+ _, seriesSet, ws, err := expandSeriesSet(ss)
+ require.NoError(t, err)
+ require.Empty(t, ws)
+ require.Equal(t, map[string][]sample{}, seriesSet)
+
+ ss = querierAfterAddButBeforeCommit.Select(ctx, false, nil, labels.MustNewMatcher(labels.MatchEqual, "foo", "bar"))
+ _, seriesSet, ws, err = expandSeriesSet(ss)
+ require.NoError(t, err)
+ require.Empty(t, ws)
+ require.Equal(t, map[string][]sample{}, seriesSet)
+
+ // This commit is after the queriers are created, so should not be returned.
+ err = app.Commit()
+ require.NoError(t, err)
+
+ // Nothing returned for querier created before the Add.
+ ss = querierBeforeAdd.Select(ctx, false, nil, labels.MustNewMatcher(labels.MatchEqual, "foo", "bar"))
+ _, seriesSet, ws, err = expandSeriesSet(ss)
+ require.NoError(t, err)
+ require.Empty(t, ws)
+ require.Equal(t, map[string][]sample{}, seriesSet)
+
+ // Series exists but has no samples for querier created after Add.
+ ss = querierAfterAddButBeforeCommit.Select(ctx, false, nil, labels.MustNewMatcher(labels.MatchEqual, "foo", "bar"))
+ _, seriesSet, ws, err = expandSeriesSet(ss)
+ require.NoError(t, err)
+ require.Empty(t, ws)
+ require.Equal(t, map[string][]sample{`{foo="bar"}`: {}}, seriesSet)
+
+ querierAfterCommit, err := db.Querier(0, 1000000)
+ require.NoError(t, err)
+ defer querierAfterCommit.Close()
+
+ // Samples are returned for querier created after Commit.
+ ss = querierAfterCommit.Select(ctx, false, nil, labels.MustNewMatcher(labels.MatchEqual, "foo", "bar"))
+ _, seriesSet, ws, err = expandSeriesSet(ss)
+ require.NoError(t, err)
+ require.Empty(t, ws)
+ require.Equal(t, map[string][]sample{`{foo="bar"}`: {{t: 0, f: 0}}}, seriesSet)
+}
+
+// TestCompactHead ensures that the head compaction
+// creates a block that is ready for loading and
+// does not cause data loss.
+// This test:
+// * opens a storage;
+// * appends values;
+// * compacts the head; and
+// * queries the db to ensure the samples are present from the compacted head.
+func TestCompactHead_AppendV2(t *testing.T) {
+ t.Parallel()
+
+ // Open a DB and append data to the WAL.
+ opts := &Options{
+ RetentionDuration: int64(time.Hour * 24 * 15 / time.Millisecond),
+ NoLockfile: true,
+ MinBlockDuration: int64(time.Hour * 2 / time.Millisecond),
+ MaxBlockDuration: int64(time.Hour * 2 / time.Millisecond),
+ WALCompression: compression.Snappy,
+ }
+ db := newTestDB(t, withOpts(opts))
+ ctx := context.Background()
+ app := db.AppenderV2(ctx)
+ var expSamples []sample
+ maxt := 100
+ for i := range maxt {
+ 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{0, int64(i), val, nil, nil})
+ }
+ require.NoError(t, app.Commit())
+
+ // Compact the Head to create a new block.
+ require.NoError(t, db.CompactHead(NewRangeHead(db.Head(), 0, int64(maxt)-1)))
+ require.NoError(t, db.Close())
+
+ // Delete everything but the new block and
+ // reopen the db to query it to ensure it includes the head data.
+ require.NoError(t, deleteNonBlocks(db.Dir()))
+ db = newTestDB(t, withDir(db.Dir()), withOpts(opts))
+ require.Len(t, db.Blocks(), 1)
+ require.Equal(t, int64(maxt), db.Head().MinTime())
+ defer func() { require.NoError(t, db.Close()) }()
+ querier, err := db.Querier(0, int64(maxt)-1)
+ require.NoError(t, err)
+ defer func() { require.NoError(t, querier.Close()) }()
+
+ seriesSet := querier.Select(ctx, false, nil, &labels.Matcher{Type: labels.MatchEqual, Name: "a", Value: "b"})
+ var series chunkenc.Iterator
+ var actSamples []sample
+
+ for seriesSet.Next() {
+ series = seriesSet.At().Iterator(series)
+ for series.Next() == chunkenc.ValFloat {
+ time, val := series.At()
+ actSamples = append(actSamples, sample{0, time, val, nil, nil})
+ }
+ require.NoError(t, series.Err())
+ }
+ require.Equal(t, expSamples, actSamples)
+ require.NoError(t, seriesSet.Err())
+}
+
+// TestCompactHeadWithDeletion tests https://github.com/prometheus/prometheus/issues/11585.
+func TestCompactHeadWithDeletion_AppendV2(t *testing.T) {
+ db := newTestDB(t)
+
+ ctx := context.Background()
+
+ app := db.AppenderV2(ctx)
+ _, err := app.Append(0, labels.FromStrings("a", "b"), 0, 10, rand.Float64(), nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+
+ err = db.Delete(ctx, 0, 100, labels.MustNewMatcher(labels.MatchEqual, "a", "b"))
+ require.NoError(t, err)
+
+ // This recreates the bug.
+ require.NoError(t, db.CompactHead(NewRangeHead(db.Head(), 0, 100)))
+ require.NoError(t, db.Close())
+}
+
+func TestOneCheckpointPerCompactCall_AppendV2(t *testing.T) {
+ t.Parallel()
+ blockRange := int64(1000)
+ opts := &Options{
+ RetentionDuration: blockRange * 1000,
+ NoLockfile: true,
+ MinBlockDuration: blockRange,
+ MaxBlockDuration: blockRange,
+ }
+
+ ctx := context.Background()
+
+ db := newTestDB(t, withOpts(opts))
+ db.DisableCompactions()
+
+ // Case 1: Lot's of uncompacted data in Head.
+
+ lbls := labels.FromStrings("foo_d", "choco_bar")
+ // Append samples spanning 59 block ranges.
+ app := db.AppenderV2(context.Background())
+ for i := range int64(60) {
+ _, err := app.Append(0, lbls, 0, blockRange*i, rand.Float64(), nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ _, err = app.Append(0, lbls, 0, (blockRange*i)+blockRange/2, rand.Float64(), nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ // Rotate the WAL file so that there is >3 files for checkpoint to happen.
+ _, err = db.head.wal.NextSegment()
+ require.NoError(t, err)
+ }
+ require.NoError(t, app.Commit())
+
+ // Check the existing WAL files.
+ first, last, err := wlog.Segments(db.head.wal.Dir())
+ require.NoError(t, err)
+ require.Equal(t, 0, first)
+ require.Equal(t, 60, last)
+
+ require.Equal(t, 0.0, prom_testutil.ToFloat64(db.head.metrics.checkpointCreationTotal))
+ require.NoError(t, db.Compact(ctx))
+ require.Equal(t, 1.0, prom_testutil.ToFloat64(db.head.metrics.checkpointCreationTotal))
+
+ // As the data spans for 59 blocks, 58 go to disk and 1 remains in Head.
+ require.Len(t, db.Blocks(), 58)
+ // Though WAL was truncated only once, head should be truncated after each compaction.
+ require.Equal(t, 58.0, prom_testutil.ToFloat64(db.head.metrics.headTruncateTotal))
+
+ // The compaction should have only truncated first 2/3 of WAL (while also rotating the files).
+ first, last, err = wlog.Segments(db.head.wal.Dir())
+ require.NoError(t, err)
+ require.Equal(t, 40, first)
+ require.Equal(t, 61, last)
+
+ // The first checkpoint would be for first 2/3rd of WAL, hence till 39.
+ // That should be the last checkpoint.
+ _, cno, err := wlog.LastCheckpoint(db.head.wal.Dir())
+ require.NoError(t, err)
+ require.Equal(t, 39, cno)
+
+ // Case 2: Old blocks on disk.
+ // The above blocks will act as old blocks.
+
+ // Creating a block to cover the data in the Head so that
+ // Head will skip the data during replay and start fresh.
+ blocks := db.Blocks()
+ newBlockMint := blocks[len(blocks)-1].Meta().MaxTime
+ newBlockMaxt := db.Head().MaxTime() + 1
+ require.NoError(t, db.Close())
+
+ createBlock(t, db.Dir(), genSeries(1, 1, newBlockMint, newBlockMaxt))
+
+ db = newTestDB(t, withDir(db.Dir()), withOpts(opts))
+ db.DisableCompactions()
+
+ // 1 block more.
+ require.Len(t, db.Blocks(), 59)
+ // No series in Head because of this new block.
+ require.Equal(t, 0, int(db.head.NumSeries()))
+
+ // Adding sample way into the future.
+ app = db.AppenderV2(context.Background())
+ _, err = app.Append(0, lbls, 0, blockRange*120, rand.Float64(), nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+
+ // The mint of head is the last block maxt, that means the gap between mint and maxt
+ // of Head is too large. This will trigger many compactions.
+ require.Equal(t, newBlockMaxt, db.head.MinTime())
+
+ // Another WAL file was rotated.
+ first, last, err = wlog.Segments(db.head.wal.Dir())
+ require.NoError(t, err)
+ require.Equal(t, 40, first)
+ require.Equal(t, 62, last)
+
+ require.Equal(t, 0.0, prom_testutil.ToFloat64(db.head.metrics.checkpointCreationTotal))
+ require.NoError(t, db.Compact(ctx))
+ require.Equal(t, 1.0, prom_testutil.ToFloat64(db.head.metrics.checkpointCreationTotal))
+
+ // No new blocks should be created as there was not data in between the new samples and the blocks.
+ require.Len(t, db.Blocks(), 59)
+
+ // The compaction should have only truncated first 2/3 of WAL (while also rotating the files).
+ first, last, err = wlog.Segments(db.head.wal.Dir())
+ require.NoError(t, err)
+ require.Equal(t, 55, first)
+ require.Equal(t, 63, last)
+
+ // The first checkpoint would be for first 2/3rd of WAL, hence till 54.
+ // That should be the last checkpoint.
+ _, cno, err = wlog.LastCheckpoint(db.head.wal.Dir())
+ require.NoError(t, err)
+ require.Equal(t, 54, cno)
+}
+
+func TestQuerierShouldNotFailIfOOOCompactionOccursAfterRetrievingQuerier_AppendV2(t *testing.T) {
+ opts := DefaultOptions()
+ opts.OutOfOrderTimeWindow = 3 * DefaultBlockDuration
+ db := newTestDB(t, withOpts(opts))
+
+ // Disable compactions so we can control it.
+ db.DisableCompactions()
+
+ metric := labels.FromStrings(labels.MetricName, "test_metric")
+ ctx := context.Background()
+ interval := int64(15 * time.Second / time.Millisecond)
+ ts := int64(0)
+ samplesWritten := 0
+
+ // Capture the first timestamp - this will be the timestamp of the OOO sample we'll append below.
+ oooTS := ts
+ ts += interval
+
+ // Push samples after the OOO sample we'll write below.
+ for ; ts < 10*interval; ts += interval {
+ app := db.AppenderV2(ctx)
+ _, err := app.Append(0, metric, 0, ts, float64(ts), nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ samplesWritten++
+ }
+
+ // Push a single OOO sample.
+ app := db.AppenderV2(ctx)
+ _, err := app.Append(0, metric, 0, oooTS, float64(ts), nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ samplesWritten++
+
+ // Get a querier.
+ querierCreatedBeforeCompaction, err := db.ChunkQuerier(0, math.MaxInt64)
+ require.NoError(t, err)
+
+ // Start OOO head compaction.
+ compactionComplete := atomic.NewBool(false)
+ go func() {
+ defer compactionComplete.Store(true)
+
+ require.NoError(t, db.CompactOOOHead(ctx))
+ require.Equal(t, float64(1), prom_testutil.ToFloat64(db.Head().metrics.chunksRemoved))
+ }()
+
+ // Give CompactOOOHead time to start work.
+ // If it does not wait for querierCreatedBeforeCompaction to be closed, then the query will return incorrect results or fail.
+ time.Sleep(time.Second)
+ require.False(t, compactionComplete.Load(), "compaction completed before reading chunks or closing querier created before compaction")
+
+ // Get another querier. This one should only use the compacted blocks from disk and ignore the chunks that will be garbage collected.
+ querierCreatedAfterCompaction, err := db.ChunkQuerier(0, math.MaxInt64)
+ require.NoError(t, err)
+
+ testQuerier := func(q storage.ChunkQuerier) {
+ // Query back the series.
+ hints := &storage.SelectHints{Start: 0, End: math.MaxInt64, Step: interval}
+ seriesSet := q.Select(ctx, true, hints, labels.MustNewMatcher(labels.MatchEqual, labels.MetricName, "test_metric"))
+
+ // Collect the iterator for the series.
+ var iterators []chunks.Iterator
+ for seriesSet.Next() {
+ iterators = append(iterators, seriesSet.At().Iterator(nil))
+ }
+ require.NoError(t, seriesSet.Err())
+ require.Len(t, iterators, 1)
+ iterator := iterators[0]
+
+ // Check that we can still successfully read all samples.
+ samplesRead := 0
+ for iterator.Next() {
+ samplesRead += iterator.At().Chunk.NumSamples()
+ }
+
+ require.NoError(t, iterator.Err())
+ require.Equal(t, samplesWritten, samplesRead)
+ }
+
+ testQuerier(querierCreatedBeforeCompaction)
+
+ require.False(t, compactionComplete.Load(), "compaction completed before closing querier created before compaction")
+ require.NoError(t, querierCreatedBeforeCompaction.Close())
+ require.Eventually(t, compactionComplete.Load, time.Second, 10*time.Millisecond, "compaction should complete after querier created before compaction was closed, and not wait for querier created after compaction")
+
+ // Use the querier created after compaction and confirm it returns the expected results (ie. from the disk block created from OOO head and in-order head) without error.
+ testQuerier(querierCreatedAfterCompaction)
+ require.NoError(t, querierCreatedAfterCompaction.Close())
+}
+
+func TestQuerierShouldNotFailIfOOOCompactionOccursAfterSelecting_AppendV2(t *testing.T) {
+ opts := DefaultOptions()
+ opts.OutOfOrderTimeWindow = 3 * DefaultBlockDuration
+ db := newTestDB(t, withOpts(opts))
+
+ // Disable compactions so we can control it.
+ db.DisableCompactions()
+
+ metric := labels.FromStrings(labels.MetricName, "test_metric")
+ ctx := context.Background()
+ interval := int64(15 * time.Second / time.Millisecond)
+ ts := int64(0)
+ samplesWritten := 0
+
+ // Capture the first timestamp - this will be the timestamp of the OOO sample we'll append below.
+ oooTS := ts
+ ts += interval
+
+ // Push samples after the OOO sample we'll write below.
+ for ; ts < 10*interval; ts += interval {
+ app := db.AppenderV2(ctx)
+ _, err := app.Append(0, metric, 0, ts, float64(ts), nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ samplesWritten++
+ }
+
+ // Push a single OOO sample.
+ app := db.AppenderV2(ctx)
+ _, err := app.Append(0, metric, 0, oooTS, float64(ts), nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ samplesWritten++
+
+ // Get a querier.
+ querier, err := db.ChunkQuerier(0, math.MaxInt64)
+ require.NoError(t, err)
+
+ // Query back the series.
+ hints := &storage.SelectHints{Start: 0, End: math.MaxInt64, Step: interval}
+ seriesSet := querier.Select(ctx, true, hints, labels.MustNewMatcher(labels.MatchEqual, labels.MetricName, "test_metric"))
+
+ // Start OOO head compaction.
+ compactionComplete := atomic.NewBool(false)
+ go func() {
+ defer compactionComplete.Store(true)
+
+ require.NoError(t, db.CompactOOOHead(ctx))
+ require.Equal(t, float64(1), prom_testutil.ToFloat64(db.Head().metrics.chunksRemoved))
+ }()
+
+ // Give CompactOOOHead time to start work.
+ // If it does not wait for the querier to be closed, then the query will return incorrect results or fail.
+ time.Sleep(time.Second)
+ require.False(t, compactionComplete.Load(), "compaction completed before reading chunks or closing querier")
+
+ // Collect the iterator for the series.
+ var iterators []chunks.Iterator
+ for seriesSet.Next() {
+ iterators = append(iterators, seriesSet.At().Iterator(nil))
+ }
+ require.NoError(t, seriesSet.Err())
+ require.Len(t, iterators, 1)
+ iterator := iterators[0]
+
+ // Check that we can still successfully read all samples.
+ samplesRead := 0
+ for iterator.Next() {
+ samplesRead += iterator.At().Chunk.NumSamples()
+ }
+
+ require.NoError(t, iterator.Err())
+ require.Equal(t, samplesWritten, samplesRead)
+
+ require.False(t, compactionComplete.Load(), "compaction completed before closing querier")
+ require.NoError(t, querier.Close())
+ require.Eventually(t, compactionComplete.Load, time.Second, 10*time.Millisecond, "compaction should complete after querier was closed")
+}
+
+func TestQuerierShouldNotFailIfOOOCompactionOccursAfterRetrievingIterators_AppendV2(t *testing.T) {
+ opts := DefaultOptions()
+ opts.OutOfOrderTimeWindow = 3 * DefaultBlockDuration
+ db := newTestDB(t, withOpts(opts))
+
+ // Disable compactions so we can control it.
+ db.DisableCompactions()
+
+ metric := labels.FromStrings(labels.MetricName, "test_metric")
+ ctx := context.Background()
+ interval := int64(15 * time.Second / time.Millisecond)
+ ts := int64(0)
+ samplesWritten := 0
+
+ // Capture the first timestamp - this will be the timestamp of the OOO sample we'll append below.
+ oooTS := ts
+ ts += interval
+
+ // Push samples after the OOO sample we'll write below.
+ for ; ts < 10*interval; ts += interval {
+ app := db.AppenderV2(ctx)
+ _, err := app.Append(0, metric, 0, ts, float64(ts), nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ samplesWritten++
+ }
+
+ // Push a single OOO sample.
+ app := db.AppenderV2(ctx)
+ _, err := app.Append(0, metric, 0, oooTS, float64(ts), nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ samplesWritten++
+
+ // Get a querier.
+ querier, err := db.ChunkQuerier(0, math.MaxInt64)
+ require.NoError(t, err)
+
+ // Query back the series.
+ hints := &storage.SelectHints{Start: 0, End: math.MaxInt64, Step: interval}
+ seriesSet := querier.Select(ctx, true, hints, labels.MustNewMatcher(labels.MatchEqual, labels.MetricName, "test_metric"))
+
+ // Collect the iterator for the series.
+ var iterators []chunks.Iterator
+ for seriesSet.Next() {
+ iterators = append(iterators, seriesSet.At().Iterator(nil))
+ }
+ require.NoError(t, seriesSet.Err())
+ require.Len(t, iterators, 1)
+ iterator := iterators[0]
+
+ // Start OOO head compaction.
+ compactionComplete := atomic.NewBool(false)
+ go func() {
+ defer compactionComplete.Store(true)
+
+ require.NoError(t, db.CompactOOOHead(ctx))
+ require.Equal(t, float64(1), prom_testutil.ToFloat64(db.Head().metrics.chunksRemoved))
+ }()
+
+ // Give CompactOOOHead time to start work.
+ // If it does not wait for the querier to be closed, then the query will return incorrect results or fail.
+ time.Sleep(time.Second)
+ require.False(t, compactionComplete.Load(), "compaction completed before reading chunks or closing querier")
+
+ // Check that we can still successfully read all samples.
+ samplesRead := 0
+ for iterator.Next() {
+ samplesRead += iterator.At().Chunk.NumSamples()
+ }
+
+ require.NoError(t, iterator.Err())
+ require.Equal(t, samplesWritten, samplesRead)
+
+ require.False(t, compactionComplete.Load(), "compaction completed before closing querier")
+ require.NoError(t, querier.Close())
+ require.Eventually(t, compactionComplete.Load, time.Second, 10*time.Millisecond, "compaction should complete after querier was closed")
+}
+
+func TestOOOWALWrite_AppendV2(t *testing.T) {
+ minutes := func(m int64) int64 { return m * time.Minute.Milliseconds() }
+
+ s := labels.NewSymbolTable()
+ scratchBuilder1 := labels.NewScratchBuilderWithSymbolTable(s, 1)
+ scratchBuilder1.Add("l", "v1")
+ s1 := scratchBuilder1.Labels()
+ scratchBuilder2 := labels.NewScratchBuilderWithSymbolTable(s, 1)
+ scratchBuilder2.Add("l", "v2")
+ s2 := scratchBuilder2.Labels()
+
+ scenarios := map[string]struct {
+ appendSample func(app storage.AppenderV2, l labels.Labels, mins int64) (storage.SeriesRef, error)
+ expectedOOORecords []any
+ expectedInORecords []any
+ }{
+ "float": {
+ appendSample: func(app storage.AppenderV2, l labels.Labels, mins int64) (storage.SeriesRef, error) {
+ seriesRef, err := app.Append(0, l, 0, minutes(mins), float64(mins), nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ return seriesRef, nil
+ },
+ expectedOOORecords: []any{
+ // The MmapRef in this are not hand calculated, and instead taken from the test run.
+ // What is important here is the order of records, and that MmapRef increases for each record.
+ []record.RefMmapMarker{
+ {Ref: 1},
+ },
+ []record.RefSample{
+ {Ref: 1, T: minutes(40), V: 40},
+ },
+
+ []record.RefMmapMarker{
+ {Ref: 2},
+ },
+ []record.RefSample{
+ {Ref: 2, T: minutes(42), V: 42},
+ },
+
+ []record.RefSample{
+ {Ref: 2, T: minutes(45), V: 45},
+ {Ref: 1, T: minutes(35), V: 35},
+ },
+ []record.RefMmapMarker{ // 3rd sample, hence m-mapped.
+ {Ref: 1, MmapRef: 0x100000000 + 8},
+ },
+ []record.RefSample{
+ {Ref: 1, T: minutes(36), V: 36},
+ {Ref: 1, T: minutes(37), V: 37},
+ },
+
+ []record.RefMmapMarker{ // 3rd sample, hence m-mapped.
+ {Ref: 1, MmapRef: 0x100000000 + 58},
+ },
+ []record.RefSample{ // Does not contain the in-order sample here.
+ {Ref: 1, T: minutes(50), V: 50},
+ },
+
+ // Single commit but multiple OOO records.
+ []record.RefMmapMarker{
+ {Ref: 2, MmapRef: 0x100000000 + 107},
+ },
+ []record.RefSample{
+ {Ref: 2, T: minutes(50), V: 50},
+ {Ref: 2, T: minutes(51), V: 51},
+ },
+ []record.RefMmapMarker{
+ {Ref: 2, MmapRef: 0x100000000 + 156},
+ },
+ []record.RefSample{
+ {Ref: 2, T: minutes(52), V: 52},
+ {Ref: 2, T: minutes(53), V: 53},
+ },
+ },
+ expectedInORecords: []any{
+ []record.RefSeries{
+ {Ref: 1, Labels: s1},
+ {Ref: 2, Labels: s2},
+ },
+ []record.RefSample{
+ {Ref: 1, T: minutes(60), V: 60},
+ {Ref: 2, T: minutes(60), V: 60},
+ },
+ []record.RefSample{
+ {Ref: 1, T: minutes(40), V: 40},
+ },
+ []record.RefSample{
+ {Ref: 2, T: minutes(42), V: 42},
+ },
+ []record.RefSample{
+ {Ref: 2, T: minutes(45), V: 45},
+ {Ref: 1, T: minutes(35), V: 35},
+ {Ref: 1, T: minutes(36), V: 36},
+ {Ref: 1, T: minutes(37), V: 37},
+ },
+ []record.RefSample{ // Contains both in-order and ooo sample.
+ {Ref: 1, T: minutes(50), V: 50},
+ {Ref: 2, T: minutes(65), V: 65},
+ },
+ []record.RefSample{
+ {Ref: 2, T: minutes(50), V: 50},
+ {Ref: 2, T: minutes(51), V: 51},
+ {Ref: 2, T: minutes(52), V: 52},
+ {Ref: 2, T: minutes(53), V: 53},
+ },
+ },
+ },
+ "integer histogram": {
+ appendSample: func(app storage.AppenderV2, l labels.Labels, mins int64) (storage.SeriesRef, error) {
+ seriesRef, err := app.Append(0, l, 0, minutes(mins), 0, tsdbutil.GenerateTestHistogram(mins), nil, storage.AOptions{})
+ require.NoError(t, err)
+ return seriesRef, nil
+ },
+ expectedOOORecords: []any{
+ // The MmapRef in this are not hand calculated, and instead taken from the test run.
+ // What is important here is the order of records, and that MmapRef increases for each record.
+ []record.RefMmapMarker{
+ {Ref: 1},
+ },
+ []record.RefHistogramSample{
+ {Ref: 1, T: minutes(40), H: tsdbutil.GenerateTestHistogram(40)},
+ },
+
+ []record.RefMmapMarker{
+ {Ref: 2},
+ },
+ []record.RefHistogramSample{
+ {Ref: 2, T: minutes(42), H: tsdbutil.GenerateTestHistogram(42)},
+ },
+
+ []record.RefHistogramSample{
+ {Ref: 2, T: minutes(45), H: tsdbutil.GenerateTestHistogram(45)},
+ {Ref: 1, T: minutes(35), H: tsdbutil.GenerateTestHistogram(35)},
+ },
+ []record.RefMmapMarker{ // 3rd sample, hence m-mapped.
+ {Ref: 1, MmapRef: 0x100000000 + 8},
+ },
+ []record.RefHistogramSample{
+ {Ref: 1, T: minutes(36), H: tsdbutil.GenerateTestHistogram(36)},
+ {Ref: 1, T: minutes(37), H: tsdbutil.GenerateTestHistogram(37)},
+ },
+
+ []record.RefMmapMarker{ // 3rd sample, hence m-mapped.
+ {Ref: 1, MmapRef: 0x100000000 + 89},
+ },
+ []record.RefHistogramSample{ // Does not contain the in-order sample here.
+ {Ref: 1, T: minutes(50), H: tsdbutil.GenerateTestHistogram(50)},
+ },
+
+ // Single commit but multiple OOO records.
+ []record.RefMmapMarker{
+ {Ref: 2, MmapRef: 0x100000000 + 172},
+ },
+ []record.RefHistogramSample{
+ {Ref: 2, T: minutes(50), H: tsdbutil.GenerateTestHistogram(50)},
+ {Ref: 2, T: minutes(51), H: tsdbutil.GenerateTestHistogram(51)},
+ },
+ []record.RefMmapMarker{
+ {Ref: 2, MmapRef: 0x100000000 + 257},
+ },
+ []record.RefHistogramSample{
+ {Ref: 2, T: minutes(52), H: tsdbutil.GenerateTestHistogram(52)},
+ {Ref: 2, T: minutes(53), H: tsdbutil.GenerateTestHistogram(53)},
+ },
+ },
+ expectedInORecords: []any{
+ []record.RefSeries{
+ {Ref: 1, Labels: s1},
+ {Ref: 2, Labels: s2},
+ },
+ []record.RefHistogramSample{
+ {Ref: 1, T: minutes(60), H: tsdbutil.GenerateTestHistogram(60)},
+ {Ref: 2, T: minutes(60), H: tsdbutil.GenerateTestHistogram(60)},
+ },
+ []record.RefHistogramSample{
+ {Ref: 1, T: minutes(40), H: tsdbutil.GenerateTestHistogram(40)},
+ },
+ []record.RefHistogramSample{
+ {Ref: 2, T: minutes(42), H: tsdbutil.GenerateTestHistogram(42)},
+ },
+ []record.RefHistogramSample{
+ {Ref: 2, T: minutes(45), H: tsdbutil.GenerateTestHistogram(45)},
+ {Ref: 1, T: minutes(35), H: tsdbutil.GenerateTestHistogram(35)},
+ {Ref: 1, T: minutes(36), H: tsdbutil.GenerateTestHistogram(36)},
+ {Ref: 1, T: minutes(37), H: tsdbutil.GenerateTestHistogram(37)},
+ },
+ []record.RefHistogramSample{ // Contains both in-order and ooo sample.
+ {Ref: 1, T: minutes(50), H: tsdbutil.GenerateTestHistogram(50)},
+ {Ref: 2, T: minutes(65), H: tsdbutil.GenerateTestHistogram(65)},
+ },
+ []record.RefHistogramSample{
+ {Ref: 2, T: minutes(50), H: tsdbutil.GenerateTestHistogram(50)},
+ {Ref: 2, T: minutes(51), H: tsdbutil.GenerateTestHistogram(51)},
+ {Ref: 2, T: minutes(52), H: tsdbutil.GenerateTestHistogram(52)},
+ {Ref: 2, T: minutes(53), H: tsdbutil.GenerateTestHistogram(53)},
+ },
+ },
+ },
+ "float histogram": {
+ appendSample: func(app storage.AppenderV2, l labels.Labels, mins int64) (storage.SeriesRef, error) {
+ seriesRef, err := app.Append(0, l, 0, minutes(mins), 0, nil, tsdbutil.GenerateTestFloatHistogram(mins), storage.AOptions{})
+ require.NoError(t, err)
+ return seriesRef, nil
+ },
+ expectedOOORecords: []any{
+ // The MmapRef in this are not hand calculated, and instead taken from the test run.
+ // What is important here is the order of records, and that MmapRef increases for each record.
+ []record.RefMmapMarker{
+ {Ref: 1},
+ },
+ []record.RefFloatHistogramSample{
+ {Ref: 1, T: minutes(40), FH: tsdbutil.GenerateTestFloatHistogram(40)},
+ },
+
+ []record.RefMmapMarker{
+ {Ref: 2},
+ },
+ []record.RefFloatHistogramSample{
+ {Ref: 2, T: minutes(42), FH: tsdbutil.GenerateTestFloatHistogram(42)},
+ },
+
+ []record.RefFloatHistogramSample{
+ {Ref: 2, T: minutes(45), FH: tsdbutil.GenerateTestFloatHistogram(45)},
+ {Ref: 1, T: minutes(35), FH: tsdbutil.GenerateTestFloatHistogram(35)},
+ },
+ []record.RefMmapMarker{ // 3rd sample, hence m-mapped.
+ {Ref: 1, MmapRef: 0x100000000 + 8},
+ },
+ []record.RefFloatHistogramSample{
+ {Ref: 1, T: minutes(36), FH: tsdbutil.GenerateTestFloatHistogram(36)},
+ {Ref: 1, T: minutes(37), FH: tsdbutil.GenerateTestFloatHistogram(37)},
+ },
+
+ []record.RefMmapMarker{ // 3rd sample, hence m-mapped.
+ {Ref: 1, MmapRef: 0x100000000 + 177},
+ },
+ []record.RefFloatHistogramSample{ // Does not contain the in-order sample here.
+ {Ref: 1, T: minutes(50), FH: tsdbutil.GenerateTestFloatHistogram(50)},
+ },
+
+ // Single commit but multiple OOO records.
+ []record.RefMmapMarker{
+ {Ref: 2, MmapRef: 0x100000000 + 348},
+ },
+ []record.RefFloatHistogramSample{
+ {Ref: 2, T: minutes(50), FH: tsdbutil.GenerateTestFloatHistogram(50)},
+ {Ref: 2, T: minutes(51), FH: tsdbutil.GenerateTestFloatHistogram(51)},
+ },
+ []record.RefMmapMarker{
+ {Ref: 2, MmapRef: 0x100000000 + 521},
+ },
+ []record.RefFloatHistogramSample{
+ {Ref: 2, T: minutes(52), FH: tsdbutil.GenerateTestFloatHistogram(52)},
+ {Ref: 2, T: minutes(53), FH: tsdbutil.GenerateTestFloatHistogram(53)},
+ },
+ },
+ expectedInORecords: []any{
+ []record.RefSeries{
+ {Ref: 1, Labels: s1},
+ {Ref: 2, Labels: s2},
+ },
+ []record.RefFloatHistogramSample{
+ {Ref: 1, T: minutes(60), FH: tsdbutil.GenerateTestFloatHistogram(60)},
+ {Ref: 2, T: minutes(60), FH: tsdbutil.GenerateTestFloatHistogram(60)},
+ },
+ []record.RefFloatHistogramSample{
+ {Ref: 1, T: minutes(40), FH: tsdbutil.GenerateTestFloatHistogram(40)},
+ },
+ []record.RefFloatHistogramSample{
+ {Ref: 2, T: minutes(42), FH: tsdbutil.GenerateTestFloatHistogram(42)},
+ },
+ []record.RefFloatHistogramSample{
+ {Ref: 2, T: minutes(45), FH: tsdbutil.GenerateTestFloatHistogram(45)},
+ {Ref: 1, T: minutes(35), FH: tsdbutil.GenerateTestFloatHistogram(35)},
+ {Ref: 1, T: minutes(36), FH: tsdbutil.GenerateTestFloatHistogram(36)},
+ {Ref: 1, T: minutes(37), FH: tsdbutil.GenerateTestFloatHistogram(37)},
+ },
+ []record.RefFloatHistogramSample{ // Contains both in-order and ooo sample.
+ {Ref: 1, T: minutes(50), FH: tsdbutil.GenerateTestFloatHistogram(50)},
+ {Ref: 2, T: minutes(65), FH: tsdbutil.GenerateTestFloatHistogram(65)},
+ },
+ []record.RefFloatHistogramSample{
+ {Ref: 2, T: minutes(50), FH: tsdbutil.GenerateTestFloatHistogram(50)},
+ {Ref: 2, T: minutes(51), FH: tsdbutil.GenerateTestFloatHistogram(51)},
+ {Ref: 2, T: minutes(52), FH: tsdbutil.GenerateTestFloatHistogram(52)},
+ {Ref: 2, T: minutes(53), FH: tsdbutil.GenerateTestFloatHistogram(53)},
+ },
+ },
+ },
+ "custom buckets histogram": {
+ appendSample: func(app storage.AppenderV2, l labels.Labels, mins int64) (storage.SeriesRef, error) {
+ seriesRef, err := app.Append(0, l, 0, minutes(mins), 0, tsdbutil.GenerateTestCustomBucketsHistogram(mins), nil, storage.AOptions{})
+ require.NoError(t, err)
+ return seriesRef, nil
+ },
+ expectedOOORecords: []any{
+ // The MmapRef in this are not hand calculated, and instead taken from the test run.
+ // What is important here is the order of records, and that MmapRef increases for each record.
+ []record.RefMmapMarker{
+ {Ref: 1},
+ },
+ []record.RefHistogramSample{
+ {Ref: 1, T: minutes(40), H: tsdbutil.GenerateTestCustomBucketsHistogram(40)},
+ },
+
+ []record.RefMmapMarker{
+ {Ref: 2},
+ },
+ []record.RefHistogramSample{
+ {Ref: 2, T: minutes(42), H: tsdbutil.GenerateTestCustomBucketsHistogram(42)},
+ },
+
+ []record.RefHistogramSample{
+ {Ref: 2, T: minutes(45), H: tsdbutil.GenerateTestCustomBucketsHistogram(45)},
+ {Ref: 1, T: minutes(35), H: tsdbutil.GenerateTestCustomBucketsHistogram(35)},
+ },
+ []record.RefMmapMarker{ // 3rd sample, hence m-mapped.
+ {Ref: 1, MmapRef: 0x100000000 + 8},
+ },
+ []record.RefHistogramSample{
+ {Ref: 1, T: minutes(36), H: tsdbutil.GenerateTestCustomBucketsHistogram(36)},
+ {Ref: 1, T: minutes(37), H: tsdbutil.GenerateTestCustomBucketsHistogram(37)},
+ },
+
+ []record.RefMmapMarker{ // 3rd sample, hence m-mapped.
+ {Ref: 1, MmapRef: 0x100000000 + 82},
+ },
+ []record.RefHistogramSample{ // Does not contain the in-order sample here.
+ {Ref: 1, T: minutes(50), H: tsdbutil.GenerateTestCustomBucketsHistogram(50)},
+ },
+
+ // Single commit but multiple OOO records.
+ []record.RefMmapMarker{
+ {Ref: 2, MmapRef: 0x100000000 + 160},
+ },
+ []record.RefHistogramSample{
+ {Ref: 2, T: minutes(50), H: tsdbutil.GenerateTestCustomBucketsHistogram(50)},
+ {Ref: 2, T: minutes(51), H: tsdbutil.GenerateTestCustomBucketsHistogram(51)},
+ },
+ []record.RefMmapMarker{
+ {Ref: 2, MmapRef: 0x100000000 + 239},
+ },
+ []record.RefHistogramSample{
+ {Ref: 2, T: minutes(52), H: tsdbutil.GenerateTestCustomBucketsHistogram(52)},
+ {Ref: 2, T: minutes(53), H: tsdbutil.GenerateTestCustomBucketsHistogram(53)},
+ },
+ },
+ expectedInORecords: []any{
+ []record.RefSeries{
+ {Ref: 1, Labels: s1},
+ {Ref: 2, Labels: s2},
+ },
+ []record.RefHistogramSample{
+ {Ref: 1, T: minutes(60), H: tsdbutil.GenerateTestCustomBucketsHistogram(60)},
+ {Ref: 2, T: minutes(60), H: tsdbutil.GenerateTestCustomBucketsHistogram(60)},
+ },
+ []record.RefHistogramSample{
+ {Ref: 1, T: minutes(40), H: tsdbutil.GenerateTestCustomBucketsHistogram(40)},
+ },
+ []record.RefHistogramSample{
+ {Ref: 2, T: minutes(42), H: tsdbutil.GenerateTestCustomBucketsHistogram(42)},
+ },
+ []record.RefHistogramSample{
+ {Ref: 2, T: minutes(45), H: tsdbutil.GenerateTestCustomBucketsHistogram(45)},
+ {Ref: 1, T: minutes(35), H: tsdbutil.GenerateTestCustomBucketsHistogram(35)},
+ {Ref: 1, T: minutes(36), H: tsdbutil.GenerateTestCustomBucketsHistogram(36)},
+ {Ref: 1, T: minutes(37), H: tsdbutil.GenerateTestCustomBucketsHistogram(37)},
+ },
+ []record.RefHistogramSample{ // Contains both in-order and ooo sample.
+ {Ref: 1, T: minutes(50), H: tsdbutil.GenerateTestCustomBucketsHistogram(50)},
+ {Ref: 2, T: minutes(65), H: tsdbutil.GenerateTestCustomBucketsHistogram(65)},
+ },
+ []record.RefHistogramSample{
+ {Ref: 2, T: minutes(50), H: tsdbutil.GenerateTestCustomBucketsHistogram(50)},
+ {Ref: 2, T: minutes(51), H: tsdbutil.GenerateTestCustomBucketsHistogram(51)},
+ {Ref: 2, T: minutes(52), H: tsdbutil.GenerateTestCustomBucketsHistogram(52)},
+ {Ref: 2, T: minutes(53), H: tsdbutil.GenerateTestCustomBucketsHistogram(53)},
+ },
+ },
+ },
+ "custom buckets float histogram": {
+ appendSample: func(app storage.AppenderV2, l labels.Labels, mins int64) (storage.SeriesRef, error) {
+ seriesRef, err := app.Append(0, l, 0, minutes(mins), 0, nil, tsdbutil.GenerateTestCustomBucketsFloatHistogram(mins), storage.AOptions{})
+ require.NoError(t, err)
+ return seriesRef, nil
+ },
+ expectedOOORecords: []any{
+ // The MmapRef in this are not hand calculated, and instead taken from the test run.
+ // What is important here is the order of records, and that MmapRef increases for each record.
+ []record.RefMmapMarker{
+ {Ref: 1},
+ },
+ []record.RefFloatHistogramSample{
+ {Ref: 1, T: minutes(40), FH: tsdbutil.GenerateTestCustomBucketsFloatHistogram(40)},
+ },
+
+ []record.RefMmapMarker{
+ {Ref: 2},
+ },
+ []record.RefFloatHistogramSample{
+ {Ref: 2, T: minutes(42), FH: tsdbutil.GenerateTestCustomBucketsFloatHistogram(42)},
+ },
+
+ []record.RefFloatHistogramSample{
+ {Ref: 2, T: minutes(45), FH: tsdbutil.GenerateTestCustomBucketsFloatHistogram(45)},
+ {Ref: 1, T: minutes(35), FH: tsdbutil.GenerateTestCustomBucketsFloatHistogram(35)},
+ },
+ []record.RefMmapMarker{ // 3rd sample, hence m-mapped.
+ {Ref: 1, MmapRef: 0x100000000 + 8},
+ },
+ []record.RefFloatHistogramSample{
+ {Ref: 1, T: minutes(36), FH: tsdbutil.GenerateTestCustomBucketsFloatHistogram(36)},
+ {Ref: 1, T: minutes(37), FH: tsdbutil.GenerateTestCustomBucketsFloatHistogram(37)},
+ },
+
+ []record.RefMmapMarker{ // 3rd sample, hence m-mapped.
+ {Ref: 1, MmapRef: 0x100000000 + 134},
+ },
+ []record.RefFloatHistogramSample{ // Does not contain the in-order sample here.
+ {Ref: 1, T: minutes(50), FH: tsdbutil.GenerateTestCustomBucketsFloatHistogram(50)},
+ },
+
+ // Single commit but multiple OOO records.
+ []record.RefMmapMarker{
+ {Ref: 2, MmapRef: 0x100000000 + 263},
+ },
+ []record.RefFloatHistogramSample{
+ {Ref: 2, T: minutes(50), FH: tsdbutil.GenerateTestCustomBucketsFloatHistogram(50)},
+ {Ref: 2, T: minutes(51), FH: tsdbutil.GenerateTestCustomBucketsFloatHistogram(51)},
+ },
+ []record.RefMmapMarker{
+ {Ref: 2, MmapRef: 0x100000000 + 393},
+ },
+ []record.RefFloatHistogramSample{
+ {Ref: 2, T: minutes(52), FH: tsdbutil.GenerateTestCustomBucketsFloatHistogram(52)},
+ {Ref: 2, T: minutes(53), FH: tsdbutil.GenerateTestCustomBucketsFloatHistogram(53)},
+ },
+ },
+ expectedInORecords: []any{
+ []record.RefSeries{
+ {Ref: 1, Labels: s1},
+ {Ref: 2, Labels: s2},
+ },
+ []record.RefFloatHistogramSample{
+ {Ref: 1, T: minutes(60), FH: tsdbutil.GenerateTestCustomBucketsFloatHistogram(60)},
+ {Ref: 2, T: minutes(60), FH: tsdbutil.GenerateTestCustomBucketsFloatHistogram(60)},
+ },
+ []record.RefFloatHistogramSample{
+ {Ref: 1, T: minutes(40), FH: tsdbutil.GenerateTestCustomBucketsFloatHistogram(40)},
+ },
+ []record.RefFloatHistogramSample{
+ {Ref: 2, T: minutes(42), FH: tsdbutil.GenerateTestCustomBucketsFloatHistogram(42)},
+ },
+ []record.RefFloatHistogramSample{
+ {Ref: 2, T: minutes(45), FH: tsdbutil.GenerateTestCustomBucketsFloatHistogram(45)},
+ {Ref: 1, T: minutes(35), FH: tsdbutil.GenerateTestCustomBucketsFloatHistogram(35)},
+ {Ref: 1, T: minutes(36), FH: tsdbutil.GenerateTestCustomBucketsFloatHistogram(36)},
+ {Ref: 1, T: minutes(37), FH: tsdbutil.GenerateTestCustomBucketsFloatHistogram(37)},
+ },
+ []record.RefFloatHistogramSample{ // Contains both in-order and ooo sample.
+ {Ref: 1, T: minutes(50), FH: tsdbutil.GenerateTestCustomBucketsFloatHistogram(50)},
+ {Ref: 2, T: minutes(65), FH: tsdbutil.GenerateTestCustomBucketsFloatHistogram(65)},
+ },
+ []record.RefFloatHistogramSample{
+ {Ref: 2, T: minutes(50), FH: tsdbutil.GenerateTestCustomBucketsFloatHistogram(50)},
+ {Ref: 2, T: minutes(51), FH: tsdbutil.GenerateTestCustomBucketsFloatHistogram(51)},
+ {Ref: 2, T: minutes(52), FH: tsdbutil.GenerateTestCustomBucketsFloatHistogram(52)},
+ {Ref: 2, T: minutes(53), FH: tsdbutil.GenerateTestCustomBucketsFloatHistogram(53)},
+ },
+ },
+ },
+ }
+ for name, scenario := range scenarios {
+ t.Run(name, func(t *testing.T) {
+ testOOOWALWriteAppendV2(t, scenario.appendSample, scenario.expectedOOORecords, scenario.expectedInORecords)
+ })
+ }
+}
+
+func testOOOWALWriteAppendV2(t *testing.T,
+ appendSample func(app storage.AppenderV2, l labels.Labels, mins int64) (storage.SeriesRef, error),
+ expectedOOORecords []any,
+ expectedInORecords []any,
+) {
+ opts := DefaultOptions()
+ opts.OutOfOrderCapMax = 2
+ opts.OutOfOrderTimeWindow = 30 * time.Minute.Milliseconds()
+ db := newTestDB(t, withOpts(opts))
+
+ s1, s2 := labels.FromStrings("l", "v1"), labels.FromStrings("l", "v2")
+
+ // Ingest sample at 1h.
+ app := db.AppenderV2(context.Background())
+ appendSample(app, s1, 60)
+ appendSample(app, s2, 60)
+ require.NoError(t, app.Commit())
+
+ // OOO for s1.
+ app = db.AppenderV2(context.Background())
+ appendSample(app, s1, 40)
+ require.NoError(t, app.Commit())
+
+ // OOO for s2.
+ app = db.AppenderV2(context.Background())
+ appendSample(app, s2, 42)
+ require.NoError(t, app.Commit())
+
+ // OOO for both s1 and s2 in the same commit.
+ app = db.AppenderV2(context.Background())
+ appendSample(app, s2, 45)
+ appendSample(app, s1, 35)
+ appendSample(app, s1, 36) // m-maps.
+ appendSample(app, s1, 37)
+ require.NoError(t, app.Commit())
+
+ // OOO for s1 but not for s2 in the same commit.
+ app = db.AppenderV2(context.Background())
+ appendSample(app, s1, 50) // m-maps.
+ appendSample(app, s2, 65)
+ require.NoError(t, app.Commit())
+
+ // Single commit has 2 times m-mapping and more samples after m-map.
+ app = db.AppenderV2(context.Background())
+ appendSample(app, s2, 50) // m-maps.
+ appendSample(app, s2, 51)
+ appendSample(app, s2, 52) // m-maps.
+ appendSample(app, s2, 53)
+ require.NoError(t, app.Commit())
+
+ getRecords := func(walDir string) []any {
+ sr, err := wlog.NewSegmentsReader(walDir)
+ require.NoError(t, err)
+ r := wlog.NewReader(sr)
+ defer func() {
+ require.NoError(t, sr.Close())
+ }()
+
+ var records []any
+ dec := record.NewDecoder(nil, promslog.NewNopLogger())
+ for r.Next() {
+ rec := r.Record()
+ switch typ := dec.Type(rec); typ {
+ case record.Series:
+ series, err := dec.Series(rec, nil)
+ require.NoError(t, err)
+ records = append(records, series)
+ case record.Samples:
+ samples, err := dec.Samples(rec, nil)
+ require.NoError(t, err)
+ records = append(records, samples)
+ case record.MmapMarkers:
+ markers, err := dec.MmapMarkers(rec, nil)
+ require.NoError(t, err)
+ records = append(records, markers)
+ case record.HistogramSamples, record.CustomBucketsHistogramSamples:
+ histogramSamples, err := dec.HistogramSamples(rec, nil)
+ require.NoError(t, err)
+ records = append(records, histogramSamples)
+ case record.FloatHistogramSamples, record.CustomBucketsFloatHistogramSamples:
+ floatHistogramSamples, err := dec.FloatHistogramSamples(rec, nil)
+ require.NoError(t, err)
+ records = append(records, floatHistogramSamples)
+ default:
+ t.Fatalf("got a WAL record that is not series or samples: %v", typ)
+ }
+ }
+
+ return records
+ }
+
+ // The normal WAL.
+ actRecs := getRecords(path.Join(db.Dir(), "wal"))
+ require.Equal(t, expectedInORecords, actRecs)
+
+ // The WBL.
+ actRecs = getRecords(path.Join(db.Dir(), wlog.WblDirName))
+ require.Equal(t, expectedOOORecords, actRecs)
+}
+
+// Tests https://github.com/prometheus/prometheus/issues/10291#issuecomment-1044373110.
+func TestDBPanicOnMmappingHeadChunk_AppendV2(t *testing.T) {
+ var err error
+ ctx := context.Background()
+
+ db := newTestDB(t)
+ db.DisableCompactions()
+
+ // Choosing scrape interval of 45s to have chunk larger than 1h.
+ itvl := int64(45 * time.Second / time.Millisecond)
+
+ lastTs := int64(0)
+ addSamples := func(numSamples int) {
+ app := db.AppenderV2(context.Background())
+ var ref storage.SeriesRef
+ lbls := labels.FromStrings("__name__", "testing", "foo", "bar")
+ for i := range numSamples {
+ ref, err = app.Append(ref, lbls, 0, lastTs, float64(lastTs), nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ lastTs += itvl
+ if i%10 == 0 {
+ require.NoError(t, app.Commit())
+ app = db.AppenderV2(context.Background())
+ }
+ }
+ require.NoError(t, app.Commit())
+ }
+
+ // Ingest samples upto 2h50m to make the head "about to compact".
+ numSamples := int(170*time.Minute/time.Millisecond) / int(itvl)
+ addSamples(numSamples)
+
+ require.Empty(t, db.Blocks())
+ require.NoError(t, db.Compact(ctx))
+ require.Empty(t, db.Blocks())
+
+ // Restarting.
+ require.NoError(t, db.Close())
+
+ db = newTestDB(t, withDir(db.Dir()))
+ db.DisableCompactions()
+
+ // Ingest samples upto 20m more to make the head compact.
+ numSamples = int(20*time.Minute/time.Millisecond) / int(itvl)
+ addSamples(numSamples)
+
+ require.Empty(t, db.Blocks())
+ require.NoError(t, db.Compact(ctx))
+ require.Len(t, db.Blocks(), 1)
+
+ // More samples to m-map and panic.
+ numSamples = int(120*time.Minute/time.Millisecond) / int(itvl)
+ addSamples(numSamples)
+
+ require.NoError(t, db.Close())
+}
+
+// TODO(bwplotka): Add cases ensuring stale sample appends will skipp metadata persisting.
+func TestMetadataInWAL_AppenderV2(t *testing.T) {
+ opts := DefaultOptions()
+ opts.EnableMetadataWALRecords = true
+ db := newTestDB(t, withOpts(opts))
+ ctx := context.Background()
+
+ // Add some series so we can attach metadata to them.
+ s1 := labels.FromStrings("a", "b")
+ s2 := labels.FromStrings("c", "d")
+ s3 := labels.FromStrings("e", "f")
+ s4 := labels.FromStrings("g", "h")
+
+ // Add a first round of metadata to the first three series.
+ m1 := metadata.Metadata{Type: "gauge", Unit: "unit_1", Help: "help_1"}
+ m2 := metadata.Metadata{Type: "gauge", Unit: "unit_2", Help: "help_2"}
+ m3 := metadata.Metadata{Type: "gauge", Unit: "unit_3", Help: "help_3"}
+
+ app := db.AppenderV2(ctx)
+ ts := int64(0)
+ _, err := app.Append(0, s1, 0, ts, 0, nil, nil, storage.AOptions{Metadata: m1})
+ require.NoError(t, err)
+ _, err = app.Append(0, s2, 0, ts, 0, nil, nil, storage.AOptions{Metadata: m2})
+ require.NoError(t, err)
+ _, err = app.Append(0, s3, 0, ts, 0, nil, nil, storage.AOptions{Metadata: m3})
+ require.NoError(t, err)
+ _, err = app.Append(0, s4, 0, ts, 0, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+
+ // Add a replicated metadata entry to the first series,
+ // a completely new metadata entry for the fourth series,
+ // and a changed metadata entry to the second series.
+ m4 := metadata.Metadata{Type: "counter", Unit: "unit_4", Help: "help_4"}
+ m5 := metadata.Metadata{Type: "counter", Unit: "unit_5", Help: "help_5"}
+ app = db.AppenderV2(ctx)
+ ts++
+ _, err = app.Append(0, s1, 0, ts, 0, nil, nil, storage.AOptions{Metadata: m1})
+ require.NoError(t, err)
+ _, err = app.Append(0, s4, 0, ts, 0, nil, nil, storage.AOptions{Metadata: m4})
+ require.NoError(t, err)
+ _, err = app.Append(0, s2, 0, ts, 0, nil, nil, storage.AOptions{Metadata: m5})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+
+ // Read the WAL to see if the disk storage format is correct.
+ recs := readTestWAL(t, path.Join(db.Dir(), "wal"))
+ var gotMetadataBlocks [][]record.RefMetadata
+ for _, rec := range recs {
+ if mr, ok := rec.([]record.RefMetadata); ok {
+ gotMetadataBlocks = append(gotMetadataBlocks, mr)
+ }
+ }
+
+ expectedMetadata := []record.RefMetadata{
+ {Ref: 1, Type: record.GetMetricType(m1.Type), Unit: m1.Unit, Help: m1.Help},
+ {Ref: 2, Type: record.GetMetricType(m2.Type), Unit: m2.Unit, Help: m2.Help},
+ {Ref: 3, Type: record.GetMetricType(m3.Type), Unit: m3.Unit, Help: m3.Help},
+ {Ref: 4, Type: record.GetMetricType(m4.Type), Unit: m4.Unit, Help: m4.Help},
+ {Ref: 2, Type: record.GetMetricType(m5.Type), Unit: m5.Unit, Help: m5.Help},
+ }
+ require.Len(t, gotMetadataBlocks, 2)
+ require.Equal(t, expectedMetadata[:3], gotMetadataBlocks[0])
+ require.Equal(t, expectedMetadata[3:], gotMetadataBlocks[1])
+}
+
+func TestMetadataCheckpointingOnlyKeepsLatestEntry_AppendV2(t *testing.T) {
+ ctx := context.Background()
+ numSamples := 10000
+ hb, w := newTestHead(t, int64(numSamples)*10, compression.None, false)
+ hb.opts.EnableMetadataWALRecords = true
+
+ // Add some series so we can append metadata to them.
+ s1 := labels.FromStrings("a", "b")
+ s2 := labels.FromStrings("c", "d")
+ s3 := labels.FromStrings("e", "f")
+ s4 := labels.FromStrings("g", "h")
+
+ m1 := metadata.Metadata{Type: "gauge", Unit: "unit_1", Help: "help_1"}
+ m2 := metadata.Metadata{Type: "gauge", Unit: "unit_2", Help: "help_2"}
+ m3 := metadata.Metadata{Type: "gauge", Unit: "unit_3", Help: "help_3"}
+ m4 := metadata.Metadata{Type: "gauge", Unit: "unit_4", Help: "help_4"}
+
+ app := hb.AppenderV2(ctx)
+ ts := int64(0)
+ _, err := app.Append(0, s1, 0, ts, 0, nil, nil, storage.AOptions{Metadata: m1})
+ require.NoError(t, err)
+ _, err = app.Append(0, s2, 0, ts, 0, nil, nil, storage.AOptions{Metadata: m2})
+ require.NoError(t, err)
+ _, err = app.Append(0, s3, 0, ts, 0, nil, nil, storage.AOptions{Metadata: m3})
+ require.NoError(t, err)
+ _, err = app.Append(0, s4, 0, ts, 0, nil, nil, storage.AOptions{Metadata: m4})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+
+ // Update metadata for first series.
+ m5 := metadata.Metadata{Type: "counter", Unit: "unit_5", Help: "help_5"}
+ app = hb.AppenderV2(ctx)
+ ts++
+ _, err = app.Append(0, s1, 0, ts, 0, nil, nil, storage.AOptions{Metadata: m5})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+
+ // Switch back-and-forth metadata for second series.
+ // Since it ended on a new metadata record, we expect a single new entry.
+ m6 := metadata.Metadata{Type: "counter", Unit: "unit_6", Help: "help_6"}
+
+ app = hb.AppenderV2(ctx)
+ ts++
+ _, err = app.Append(0, s2, 0, ts, 0, nil, nil, storage.AOptions{Metadata: m6})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+
+ app = hb.AppenderV2(ctx)
+ ts++
+ _, err = app.Append(0, s2, 0, ts, 0, nil, nil, storage.AOptions{Metadata: m2})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+
+ app = hb.AppenderV2(ctx)
+ ts++
+ _, err = app.Append(0, s2, 0, ts, 0, nil, nil, storage.AOptions{Metadata: m6})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+
+ app = hb.AppenderV2(ctx)
+ ts++
+ _, err = app.Append(0, s2, 0, ts, 0, nil, nil, storage.AOptions{Metadata: m2})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+
+ app = hb.AppenderV2(ctx)
+ ts++
+ _, err = app.Append(0, s2, 0, ts, 0, nil, nil, storage.AOptions{Metadata: m6})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+
+ // Let's create a checkpoint.
+ first, last, err := wlog.Segments(w.Dir())
+ require.NoError(t, err)
+ keep := func(id chunks.HeadSeriesRef) bool {
+ return id != 3
+ }
+ _, err = wlog.Checkpoint(promslog.NewNopLogger(), w, first, last-1, keep, 0)
+ require.NoError(t, err)
+
+ // Confirm there's been a checkpoint.
+ cdir, _, err := wlog.LastCheckpoint(w.Dir())
+ require.NoError(t, err)
+
+ // Read in checkpoint and WAL.
+ recs := readTestWAL(t, cdir)
+ var gotMetadataBlocks [][]record.RefMetadata
+ for _, rec := range recs {
+ if mr, ok := rec.([]record.RefMetadata); ok {
+ gotMetadataBlocks = append(gotMetadataBlocks, mr)
+ }
+ }
+
+ // There should only be 1 metadata block present, with only the latest
+ // metadata kept around.
+ wantMetadata := []record.RefMetadata{
+ {Ref: 1, Type: record.GetMetricType(m5.Type), Unit: m5.Unit, Help: m5.Help},
+ {Ref: 2, Type: record.GetMetricType(m6.Type), Unit: m6.Unit, Help: m6.Help},
+ {Ref: 4, Type: record.GetMetricType(m4.Type), Unit: m4.Unit, Help: m4.Help},
+ }
+ require.Len(t, gotMetadataBlocks, 1)
+ require.Len(t, gotMetadataBlocks[0], 3)
+ gotMetadataBlock := gotMetadataBlocks[0]
+
+ sort.Slice(gotMetadataBlock, func(i, j int) bool { return gotMetadataBlock[i].Ref < gotMetadataBlock[j].Ref })
+ require.Equal(t, wantMetadata, gotMetadataBlock)
+ require.NoError(t, hb.Close())
+}
+
+func TestMetadataAssertInMemoryData_AppendV2(t *testing.T) {
+ opts := DefaultOptions()
+ opts.EnableMetadataWALRecords = true
+ db := newTestDB(t, withOpts(opts))
+ ctx := context.Background()
+
+ // Add some series so we can append metadata to them.
+ s1 := labels.FromStrings("a", "b")
+ s2 := labels.FromStrings("c", "d")
+ s3 := labels.FromStrings("e", "f")
+ s4 := labels.FromStrings("g", "h")
+
+ // Add a first round of metadata to the first three series.
+ // The in-memory data held in the db Head should hold the metadata.
+ m1 := metadata.Metadata{Type: "gauge", Unit: "unit_1", Help: "help_1"}
+ m2 := metadata.Metadata{Type: "gauge", Unit: "unit_2", Help: "help_2"}
+ m3 := metadata.Metadata{Type: "gauge", Unit: "unit_3", Help: "help_3"}
+
+ app := db.AppenderV2(ctx)
+ ts := int64(0)
+ _, err := app.Append(0, s1, 0, ts, 0, nil, nil, storage.AOptions{Metadata: m1})
+ require.NoError(t, err)
+ _, err = app.Append(0, s2, 0, ts, 0, nil, nil, storage.AOptions{Metadata: m2})
+ require.NoError(t, err)
+ _, err = app.Append(0, s3, 0, ts, 0, nil, nil, storage.AOptions{Metadata: m3})
+ require.NoError(t, err)
+ _, err = app.Append(0, s4, 0, ts, 0, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+
+ series1 := db.head.series.getByHash(s1.Hash(), s1)
+ series2 := db.head.series.getByHash(s2.Hash(), s2)
+ series3 := db.head.series.getByHash(s3.Hash(), s3)
+ series4 := db.head.series.getByHash(s4.Hash(), s4)
+ require.Equal(t, *series1.meta, m1)
+ require.Equal(t, *series2.meta, m2)
+ require.Equal(t, *series3.meta, m3)
+ require.Nil(t, series4.meta)
+
+ // Add a replicated metadata entry to the first series,
+ // a changed metadata entry to the second series,
+ // and a completely new metadata entry for the fourth series.
+ // The in-memory data held in the db Head should be correctly updated.
+ m4 := metadata.Metadata{Type: "counter", Unit: "unit_4", Help: "help_4"}
+ m5 := metadata.Metadata{Type: "counter", Unit: "unit_5", Help: "help_5"}
+ app = db.AppenderV2(ctx)
+ ts++
+ _, err = app.Append(0, s1, 0, ts, 0, nil, nil, storage.AOptions{Metadata: m1})
+ require.NoError(t, err)
+ _, err = app.Append(0, s4, 0, ts, 0, nil, nil, storage.AOptions{Metadata: m4})
+ require.NoError(t, err)
+ _, err = app.Append(0, s2, 0, ts, 0, nil, nil, storage.AOptions{Metadata: m5})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+
+ series1 = db.head.series.getByHash(s1.Hash(), s1)
+ series2 = db.head.series.getByHash(s2.Hash(), s2)
+ series3 = db.head.series.getByHash(s3.Hash(), s3)
+ series4 = db.head.series.getByHash(s4.Hash(), s4)
+ require.Equal(t, *series1.meta, m1)
+ require.Equal(t, *series2.meta, m5)
+ require.Equal(t, *series3.meta, m3)
+ require.Equal(t, *series4.meta, m4)
+
+ require.NoError(t, db.Close())
+
+ // Reopen the DB, replaying the WAL. The Head must have been replayed
+ // correctly in memory.
+ reopenDB, err := Open(db.Dir(), nil, nil, nil, nil)
+ require.NoError(t, err)
+ t.Cleanup(func() {
+ require.NoError(t, reopenDB.Close())
+ })
+
+ _, err = reopenDB.head.wal.Size()
+ require.NoError(t, err)
+
+ require.Equal(t, *reopenDB.head.series.getByHash(s1.Hash(), s1).meta, m1)
+ require.Equal(t, *reopenDB.head.series.getByHash(s2.Hash(), s2).meta, m5)
+ require.Equal(t, *reopenDB.head.series.getByHash(s3.Hash(), s3).meta, m3)
+ require.Equal(t, *reopenDB.head.series.getByHash(s4.Hash(), s4).meta, m4)
+}
+
+// TestMultipleEncodingsCommitOrder mainly serves to demonstrate when happens when committing a batch of samples for the
+// same series when there are multiple encodings. With issue #15177 fixed, this now all works as expected.
+func TestMultipleEncodingsCommitOrder_AppendV2(t *testing.T) {
+ opts := DefaultOptions()
+ opts.OutOfOrderCapMax = 30
+ opts.OutOfOrderTimeWindow = 24 * time.Hour.Milliseconds()
+
+ db := newTestDB(t, withOpts(opts))
+ db.DisableCompactions()
+
+ series1 := labels.FromStrings("foo", "bar1")
+ addSample := func(app storage.AppenderV2, ts int64, valType chunkenc.ValueType) chunks.Sample {
+ if valType == chunkenc.ValFloat {
+ _, err := app.Append(0, labels.FromStrings("foo", "bar1"), 0, ts, float64(ts), nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ return sample{t: ts, f: float64(ts)}
+ }
+ if valType == chunkenc.ValHistogram {
+ h := tsdbutil.GenerateTestHistogram(ts)
+ _, err := app.Append(0, labels.FromStrings("foo", "bar1"), 0, ts, 0, h, nil, storage.AOptions{})
+ require.NoError(t, err)
+ return sample{t: ts, h: h}
+ }
+ fh := tsdbutil.GenerateTestFloatHistogram(ts)
+ _, err := app.Append(0, labels.FromStrings("foo", "bar1"), 0, ts, 0, nil, fh, storage.AOptions{})
+ require.NoError(t, err)
+ return sample{t: ts, fh: fh}
+ }
+
+ verifySamples := func(minT, maxT int64, expSamples []chunks.Sample, oooCount int) {
+ requireEqualOOOSamples(t, oooCount, db)
+
+ // Verify samples querier.
+ querier, err := db.Querier(minT, maxT)
+ require.NoError(t, err)
+ defer querier.Close()
+
+ seriesSet := query(t, querier, labels.MustNewMatcher(labels.MatchEqual, "foo", "bar1"))
+ require.Len(t, seriesSet, 1)
+ gotSamples := seriesSet[series1.String()]
+ requireEqualSamples(t, series1.String(), expSamples, gotSamples, requireEqualSamplesIgnoreCounterResets)
+
+ // Verify chunks querier.
+ chunkQuerier, err := db.ChunkQuerier(minT, maxT)
+ require.NoError(t, err)
+ defer chunkQuerier.Close()
+
+ chks := queryChunks(t, chunkQuerier, labels.MustNewMatcher(labels.MatchEqual, "foo", "bar1"))
+ require.NotNil(t, chks[series1.String()])
+ require.Len(t, chks, 1)
+ var gotChunkSamples []chunks.Sample
+ for _, chunk := range chks[series1.String()] {
+ it := chunk.Chunk.Iterator(nil)
+ smpls, err := storage.ExpandSamples(it, newSample)
+ require.NoError(t, err)
+ gotChunkSamples = append(gotChunkSamples, smpls...)
+ require.NoError(t, it.Err())
+ }
+ requireEqualSamples(t, series1.String(), expSamples, gotChunkSamples, requireEqualSamplesIgnoreCounterResets)
+ }
+
+ var expSamples []chunks.Sample
+
+ // Append samples with different encoding types and then commit them at once.
+ app := db.AppenderV2(context.Background())
+
+ for i := 100; i < 105; i++ {
+ s := addSample(app, int64(i), chunkenc.ValFloat)
+ expSamples = append(expSamples, s)
+ }
+ for i := 110; i < 120; i++ {
+ s := addSample(app, int64(i), chunkenc.ValHistogram)
+ expSamples = append(expSamples, s)
+ }
+ for i := 120; i < 130; i++ {
+ s := addSample(app, int64(i), chunkenc.ValFloatHistogram)
+ expSamples = append(expSamples, s)
+ }
+ for i := 140; i < 150; i++ {
+ s := addSample(app, int64(i), chunkenc.ValFloatHistogram)
+ expSamples = append(expSamples, s)
+ }
+ // These samples will be marked as out-of-order.
+ for i := 130; i < 135; i++ {
+ s := addSample(app, int64(i), chunkenc.ValFloat)
+ expSamples = append(expSamples, s)
+ }
+
+ require.NoError(t, app.Commit())
+
+ sort.Slice(expSamples, func(i, j int) bool {
+ return expSamples[i].T() < expSamples[j].T()
+ })
+
+ // oooCount = 5 for the samples 130 to 134.
+ verifySamples(100, 150, expSamples, 5)
+
+ // Append and commit some in-order histograms by themselves.
+ app = db.AppenderV2(context.Background())
+ for i := 150; i < 160; i++ {
+ s := addSample(app, int64(i), chunkenc.ValHistogram)
+ expSamples = append(expSamples, s)
+ }
+ require.NoError(t, app.Commit())
+
+ // oooCount remains at 5.
+ verifySamples(100, 160, expSamples, 5)
+
+ // Append and commit samples for all encoding types. This time all samples will be treated as OOO because samples
+ // with newer timestamps have already been committed.
+ app = db.AppenderV2(context.Background())
+ for i := 50; i < 55; i++ {
+ s := addSample(app, int64(i), chunkenc.ValFloat)
+ expSamples = append(expSamples, s)
+ }
+ for i := 60; i < 70; i++ {
+ s := addSample(app, int64(i), chunkenc.ValHistogram)
+ expSamples = append(expSamples, s)
+ }
+ for i := 70; i < 75; i++ {
+ s := addSample(app, int64(i), chunkenc.ValFloat)
+ expSamples = append(expSamples, s)
+ }
+ for i := 80; i < 90; i++ {
+ s := addSample(app, int64(i), chunkenc.ValFloatHistogram)
+ expSamples = append(expSamples, s)
+ }
+ require.NoError(t, app.Commit())
+
+ // Sort samples again because OOO samples have been added.
+ sort.Slice(expSamples, func(i, j int) bool {
+ return expSamples[i].T() < expSamples[j].T()
+ })
+
+ // oooCount = 35 as we've added 30 more OOO samples.
+ verifySamples(50, 160, expSamples, 35)
+}
+
+// TODO(codesome): test more samples incoming once compaction has started. To verify new samples after the start
+//
+// are not included in this compaction.
+func TestOOOCompaction_AppendV2(t *testing.T) {
+ for name, scenario := range sampleTypeScenarios {
+ t.Run(name, func(t *testing.T) {
+ testOOOCompactionAppenderV2(t, scenario, false)
+ })
+ t.Run(name+"+extra", func(t *testing.T) {
+ testOOOCompactionAppenderV2(t, scenario, true)
+ })
+ }
+}
+
+func testOOOCompactionAppenderV2(t *testing.T, scenario sampleTypeScenario, addExtraSamples bool) {
+ ctx := context.Background()
+
+ opts := DefaultOptions()
+ opts.OutOfOrderCapMax = 30
+ opts.OutOfOrderTimeWindow = 300 * time.Minute.Milliseconds()
+ db := newTestDB(t, withOpts(opts))
+ db.DisableCompactions() // We want to manually call it.
+
+ series1 := labels.FromStrings("foo", "bar1")
+ series2 := labels.FromStrings("foo", "bar2")
+
+ addSample := func(fromMins, toMins int64) {
+ app := db.AppenderV2(context.Background())
+ for m := fromMins; m <= toMins; m++ {
+ ts := m * time.Minute.Milliseconds()
+ _, _, err := scenario.appendFunc(storage.AppenderV2AsLimitedV1(app), series1, ts, ts)
+ require.NoError(t, err)
+ _, _, err = scenario.appendFunc(storage.AppenderV2AsLimitedV1(app), series2, ts, 2*ts)
+ require.NoError(t, err)
+ }
+ require.NoError(t, app.Commit())
+ }
+
+ // Add an in-order samples.
+ addSample(250, 300)
+
+ // Verify that the in-memory ooo chunk is empty.
+ checkEmptyOOOChunk := func(lbls labels.Labels) {
+ ms, created, err := db.head.getOrCreate(lbls.Hash(), lbls, false)
+ require.NoError(t, err)
+ require.False(t, created)
+ require.Nil(t, ms.ooo)
+ }
+ checkEmptyOOOChunk(series1)
+ checkEmptyOOOChunk(series2)
+
+ // Add ooo samples that creates multiple chunks.
+ // 90 to 300 spans across 3 block ranges: [0, 120), [120, 240), [240, 360)
+ addSample(90, 300)
+ // Adding same samples to create overlapping chunks.
+ // Since the active chunk won't start at 90 again, all the new
+ // chunks will have different time ranges than the previous chunks.
+ addSample(90, 300)
+
+ var highest int64 = 300
+
+ verifyDBSamples := func() {
+ var series1Samples, series2Samples []chunks.Sample
+ for _, r := range [][2]int64{{90, 119}, {120, 239}, {240, highest}} {
+ fromMins, toMins := r[0], r[1]
+ for m := fromMins; m <= toMins; m++ {
+ ts := m * time.Minute.Milliseconds()
+ series1Samples = append(series1Samples, scenario.sampleFunc(ts, ts))
+ series2Samples = append(series2Samples, scenario.sampleFunc(ts, 2*ts))
+ }
+ }
+ expRes := map[string][]chunks.Sample{
+ series1.String(): series1Samples,
+ series2.String(): series2Samples,
+ }
+
+ q, err := db.Querier(math.MinInt64, math.MaxInt64)
+ require.NoError(t, err)
+
+ actRes := query(t, q, labels.MustNewMatcher(labels.MatchRegexp, "foo", "bar.*"))
+ requireEqualSeries(t, expRes, actRes, true)
+ }
+
+ verifyDBSamples() // Before any compaction.
+
+ // Verify that the in-memory ooo chunk is not empty.
+ checkNonEmptyOOOChunk := func(lbls labels.Labels) {
+ ms, created, err := db.head.getOrCreate(lbls.Hash(), lbls, false)
+ require.NoError(t, err)
+ require.False(t, created)
+ require.Positive(t, ms.ooo.oooHeadChunk.chunk.NumSamples())
+ require.Len(t, ms.ooo.oooMmappedChunks, 13) // 7 original, 6 duplicate.
+ }
+ checkNonEmptyOOOChunk(series1)
+ checkNonEmptyOOOChunk(series2)
+
+ // No blocks before compaction.
+ require.Empty(t, db.Blocks())
+
+ // There is a 0th WBL file.
+ require.NoError(t, db.head.wbl.Sync()) // syncing to make sure wbl is flushed in windows
+ files, err := os.ReadDir(db.head.wbl.Dir())
+ require.NoError(t, err)
+ require.Len(t, files, 1)
+ require.Equal(t, "00000000", files[0].Name())
+ f, err := files[0].Info()
+ require.NoError(t, err)
+ require.Greater(t, f.Size(), int64(100))
+
+ if addExtraSamples {
+ compactOOOHeadTestingCallback = func() {
+ addSample(90, 120) // Back in time, to generate a new OOO chunk.
+ addSample(300, 330) // Now some samples after the previous highest timestamp.
+ addSample(300, 330) // Repeat to generate an OOO chunk at these timestamps.
+ }
+ highest = 330
+ }
+
+ // OOO compaction happens here.
+ require.NoError(t, db.CompactOOOHead(ctx))
+
+ // 3 blocks exist now. [0, 120), [120, 240), [240, 360)
+ require.Len(t, db.Blocks(), 3)
+
+ verifyDBSamples() // Blocks created out of OOO head now.
+
+ // 0th WBL file will be deleted and 1st will be the only present.
+ files, err = os.ReadDir(db.head.wbl.Dir())
+ require.NoError(t, err)
+ require.Len(t, files, 1)
+ require.Equal(t, "00000001", files[0].Name())
+ f, err = files[0].Info()
+ require.NoError(t, err)
+
+ if !addExtraSamples {
+ require.Equal(t, int64(0), f.Size())
+ // OOO stuff should not be present in the Head now.
+ checkEmptyOOOChunk(series1)
+ checkEmptyOOOChunk(series2)
+ }
+
+ verifySamples := func(block *Block, fromMins, toMins int64) {
+ series1Samples := make([]chunks.Sample, 0, toMins-fromMins+1)
+ series2Samples := make([]chunks.Sample, 0, toMins-fromMins+1)
+ for m := fromMins; m <= toMins; m++ {
+ ts := m * time.Minute.Milliseconds()
+ series1Samples = append(series1Samples, scenario.sampleFunc(ts, ts))
+ series2Samples = append(series2Samples, scenario.sampleFunc(ts, 2*ts))
+ }
+ expRes := map[string][]chunks.Sample{
+ series1.String(): series1Samples,
+ series2.String(): series2Samples,
+ }
+
+ q, err := NewBlockQuerier(block, math.MinInt64, math.MaxInt64)
+ require.NoError(t, err)
+
+ actRes := query(t, q, labels.MustNewMatcher(labels.MatchRegexp, "foo", "bar.*"))
+ requireEqualSeries(t, expRes, actRes, true)
+ }
+
+ // Checking for expected data in the blocks.
+ verifySamples(db.Blocks()[0], 90, 119)
+ verifySamples(db.Blocks()[1], 120, 239)
+ verifySamples(db.Blocks()[2], 240, 299)
+
+ // There should be a single m-map file.
+ mmapDir := mmappedChunksDir(db.head.opts.ChunkDirRoot)
+ files, err = os.ReadDir(mmapDir)
+ require.NoError(t, err)
+ require.Len(t, files, 1)
+
+ // Compact the in-order head and expect another block.
+ // Since this is a forced compaction, this block is not aligned with 2h.
+ err = db.CompactHead(NewRangeHead(db.head, 250*time.Minute.Milliseconds(), 350*time.Minute.Milliseconds()))
+ require.NoError(t, err)
+ require.Len(t, db.Blocks(), 4) // [0, 120), [120, 240), [240, 360), [250, 351)
+ verifySamples(db.Blocks()[3], 250, highest)
+
+ verifyDBSamples() // Blocks created out of normal and OOO head now. But not merged.
+
+ // The compaction also clears out the old m-map files. Including
+ // the file that has ooo chunks.
+ files, err = os.ReadDir(mmapDir)
+ require.NoError(t, err)
+ require.Len(t, files, 1)
+ require.Equal(t, "000001", files[0].Name())
+
+ // This will merge overlapping block.
+ require.NoError(t, db.Compact(ctx))
+
+ require.Len(t, db.Blocks(), 3) // [0, 120), [120, 240), [240, 360)
+ verifySamples(db.Blocks()[0], 90, 119)
+ verifySamples(db.Blocks()[1], 120, 239)
+ verifySamples(db.Blocks()[2], 240, highest) // Merged block.
+
+ verifyDBSamples() // Final state. Blocks from normal and OOO head are merged.
+}
+
+// TestOOOCompactionWithNormalCompaction tests if OOO compaction is performed
+// when the normal head's compaction is done.
+func TestOOOCompactionWithNormalCompaction_AppendV2(t *testing.T) {
+ t.Parallel()
+ for name, scenario := range sampleTypeScenarios {
+ t.Run(name, func(t *testing.T) {
+ testOOOCompactionWithNormalCompactionAppendV2(t, scenario)
+ })
+ }
+}
+
+func testOOOCompactionWithNormalCompactionAppendV2(t *testing.T, scenario sampleTypeScenario) {
+ t.Parallel()
+ ctx := context.Background()
+
+ opts := DefaultOptions()
+ opts.OutOfOrderCapMax = 30
+ opts.OutOfOrderTimeWindow = 300 * time.Minute.Milliseconds()
+
+ db := newTestDB(t, withOpts(opts))
+ db.DisableCompactions() // We want to manually call it.
+
+ series1 := labels.FromStrings("foo", "bar1")
+ series2 := labels.FromStrings("foo", "bar2")
+
+ addSamples := func(fromMins, toMins int64) {
+ app := db.AppenderV2(context.Background())
+ for m := fromMins; m <= toMins; m++ {
+ ts := m * time.Minute.Milliseconds()
+ _, _, err := scenario.appendFunc(storage.AppenderV2AsLimitedV1(app), series1, ts, ts)
+ require.NoError(t, err)
+ _, _, err = scenario.appendFunc(storage.AppenderV2AsLimitedV1(app), series2, ts, 2*ts)
+ require.NoError(t, err)
+ }
+ require.NoError(t, app.Commit())
+ }
+
+ // Add an in-order samples.
+ addSamples(250, 350)
+
+ // Add ooo samples that will result into a single block.
+ addSamples(90, 110)
+
+ // Checking that ooo chunk is not empty.
+ for _, lbls := range []labels.Labels{series1, series2} {
+ ms, created, err := db.head.getOrCreate(lbls.Hash(), lbls, false)
+ require.NoError(t, err)
+ require.False(t, created)
+ require.Positive(t, ms.ooo.oooHeadChunk.chunk.NumSamples())
+ }
+
+ // If the normal Head is not compacted, the OOO head compaction does not take place.
+ require.NoError(t, db.Compact(ctx))
+ require.Empty(t, db.Blocks())
+
+ // Add more in-order samples in future that would trigger the compaction.
+ addSamples(400, 450)
+
+ // No blocks before compaction.
+ require.Empty(t, db.Blocks())
+
+ // Compacts normal and OOO head.
+ require.NoError(t, db.Compact(ctx))
+
+ // 2 blocks exist now. [0, 120), [250, 360)
+ require.Len(t, db.Blocks(), 2)
+ require.Equal(t, int64(0), db.Blocks()[0].MinTime())
+ require.Equal(t, 120*time.Minute.Milliseconds(), db.Blocks()[0].MaxTime())
+ require.Equal(t, 250*time.Minute.Milliseconds(), db.Blocks()[1].MinTime())
+ require.Equal(t, 360*time.Minute.Milliseconds(), db.Blocks()[1].MaxTime())
+
+ // Checking that ooo chunk is empty.
+ for _, lbls := range []labels.Labels{series1, series2} {
+ ms, created, err := db.head.getOrCreate(lbls.Hash(), lbls, false)
+ require.NoError(t, err)
+ require.False(t, created)
+ require.Nil(t, ms.ooo)
+ }
+
+ verifySamples := func(block *Block, fromMins, toMins int64) {
+ series1Samples := make([]chunks.Sample, 0, toMins-fromMins+1)
+ series2Samples := make([]chunks.Sample, 0, toMins-fromMins+1)
+ for m := fromMins; m <= toMins; m++ {
+ ts := m * time.Minute.Milliseconds()
+ series1Samples = append(series1Samples, scenario.sampleFunc(ts, ts))
+ series2Samples = append(series2Samples, scenario.sampleFunc(ts, 2*ts))
+ }
+ expRes := map[string][]chunks.Sample{
+ series1.String(): series1Samples,
+ series2.String(): series2Samples,
+ }
+
+ q, err := NewBlockQuerier(block, math.MinInt64, math.MaxInt64)
+ require.NoError(t, err)
+
+ actRes := query(t, q, labels.MustNewMatcher(labels.MatchRegexp, "foo", "bar.*"))
+ requireEqualSeries(t, expRes, actRes, true)
+ }
+
+ // Checking for expected data in the blocks.
+ verifySamples(db.Blocks()[0], 90, 110)
+ verifySamples(db.Blocks()[1], 250, 350)
+}
+
+// TestOOOCompactionWithDisabledWriteLog tests the scenario where the TSDB is
+// configured to not have wal and wbl but its able to compact both the in-order
+// and out-of-order head.
+func TestOOOCompactionWithDisabledWriteLog_AppendV2(t *testing.T) {
+ t.Parallel()
+ for name, scenario := range sampleTypeScenarios {
+ t.Run(name, func(t *testing.T) {
+ testOOOCompactionWithDisabledWriteLogAppend2(t, scenario)
+ })
+ }
+}
+
+func testOOOCompactionWithDisabledWriteLogAppend2(t *testing.T, scenario sampleTypeScenario) {
+ t.Parallel()
+ ctx := context.Background()
+
+ opts := DefaultOptions()
+ opts.OutOfOrderCapMax = 30
+ opts.OutOfOrderTimeWindow = 300 * time.Minute.Milliseconds()
+ opts.WALSegmentSize = -1 // disabled WAL and WBL
+
+ db := newTestDB(t, withOpts(opts))
+ db.DisableCompactions() // We want to manually call it.
+
+ series1 := labels.FromStrings("foo", "bar1")
+ series2 := labels.FromStrings("foo", "bar2")
+
+ addSamples := func(fromMins, toMins int64) {
+ app := db.AppenderV2(context.Background())
+ for m := fromMins; m <= toMins; m++ {
+ ts := m * time.Minute.Milliseconds()
+ _, _, err := scenario.appendFunc(storage.AppenderV2AsLimitedV1(app), series1, ts, ts)
+ require.NoError(t, err)
+ _, _, err = scenario.appendFunc(storage.AppenderV2AsLimitedV1(app), series2, ts, 2*ts)
+ require.NoError(t, err)
+ }
+ require.NoError(t, app.Commit())
+ }
+
+ // Add an in-order samples.
+ addSamples(250, 350)
+
+ // Add ooo samples that will result into a single block.
+ addSamples(90, 110)
+
+ // Checking that ooo chunk is not empty.
+ for _, lbls := range []labels.Labels{series1, series2} {
+ ms, created, err := db.head.getOrCreate(lbls.Hash(), lbls, false)
+ require.NoError(t, err)
+ require.False(t, created)
+ require.Positive(t, ms.ooo.oooHeadChunk.chunk.NumSamples())
+ }
+
+ // If the normal Head is not compacted, the OOO head compaction does not take place.
+ require.NoError(t, db.Compact(ctx))
+ require.Empty(t, db.Blocks())
+
+ // Add more in-order samples in future that would trigger the compaction.
+ addSamples(400, 450)
+
+ // No blocks before compaction.
+ require.Empty(t, db.Blocks())
+
+ // Compacts normal and OOO head.
+ require.NoError(t, db.Compact(ctx))
+
+ // 2 blocks exist now. [0, 120), [250, 360)
+ require.Len(t, db.Blocks(), 2)
+ require.Equal(t, int64(0), db.Blocks()[0].MinTime())
+ require.Equal(t, 120*time.Minute.Milliseconds(), db.Blocks()[0].MaxTime())
+ require.Equal(t, 250*time.Minute.Milliseconds(), db.Blocks()[1].MinTime())
+ require.Equal(t, 360*time.Minute.Milliseconds(), db.Blocks()[1].MaxTime())
+
+ // Checking that ooo chunk is empty.
+ for _, lbls := range []labels.Labels{series1, series2} {
+ ms, created, err := db.head.getOrCreate(lbls.Hash(), lbls, false)
+ require.NoError(t, err)
+ require.False(t, created)
+ require.Nil(t, ms.ooo)
+ }
+
+ verifySamples := func(block *Block, fromMins, toMins int64) {
+ series1Samples := make([]chunks.Sample, 0, toMins-fromMins+1)
+ series2Samples := make([]chunks.Sample, 0, toMins-fromMins+1)
+ for m := fromMins; m <= toMins; m++ {
+ ts := m * time.Minute.Milliseconds()
+ series1Samples = append(series1Samples, scenario.sampleFunc(ts, ts))
+ series2Samples = append(series2Samples, scenario.sampleFunc(ts, 2*ts))
+ }
+ expRes := map[string][]chunks.Sample{
+ series1.String(): series1Samples,
+ series2.String(): series2Samples,
+ }
+
+ q, err := NewBlockQuerier(block, math.MinInt64, math.MaxInt64)
+ require.NoError(t, err)
+
+ actRes := query(t, q, labels.MustNewMatcher(labels.MatchRegexp, "foo", "bar.*"))
+ requireEqualSeries(t, expRes, actRes, true)
+ }
+
+ // Checking for expected data in the blocks.
+ verifySamples(db.Blocks()[0], 90, 110)
+ verifySamples(db.Blocks()[1], 250, 350)
+}
+
+// TestOOOQueryAfterRestartWithSnapshotAndRemovedWBL_AppendV2 tests the scenario where the WBL goes
+// missing after a restart while snapshot was enabled, but the query still returns the right
+// data from the mmap chunks.
+func TestOOOQueryAfterRestartWithSnapshotAndRemovedWBL_AppendV2(t *testing.T) {
+ t.Parallel()
+ for name, scenario := range sampleTypeScenarios {
+ t.Run(name, func(t *testing.T) {
+ testOOOQueryAfterRestartWithSnapshotAndRemovedWBLAppendV2(t, scenario)
+ })
+ }
+}
+
+func testOOOQueryAfterRestartWithSnapshotAndRemovedWBLAppendV2(t *testing.T, scenario sampleTypeScenario) {
+ ctx := context.Background()
+
+ opts := DefaultOptions()
+ opts.OutOfOrderCapMax = 10
+ opts.OutOfOrderTimeWindow = 300 * time.Minute.Milliseconds()
+ opts.EnableMemorySnapshotOnShutdown = true
+
+ db := newTestDB(t, withOpts(opts))
+ db.DisableCompactions() // We want to manually call it.
+
+ series1 := labels.FromStrings("foo", "bar1")
+ series2 := labels.FromStrings("foo", "bar2")
+
+ addSamples := func(fromMins, toMins int64) {
+ app := db.AppenderV2(context.Background())
+ for m := fromMins; m <= toMins; m++ {
+ ts := m * time.Minute.Milliseconds()
+ _, _, err := scenario.appendFunc(storage.AppenderV2AsLimitedV1(app), series1, ts, ts)
+ require.NoError(t, err)
+ _, _, err = scenario.appendFunc(storage.AppenderV2AsLimitedV1(app), series2, ts, 2*ts)
+ require.NoError(t, err)
+ }
+ require.NoError(t, app.Commit())
+ }
+
+ // Add an in-order samples.
+ addSamples(250, 350)
+
+ // Add ooo samples that will result into a single block.
+ addSamples(90, 110) // The sample 110 will not be in m-map chunks.
+
+ // Checking that there are some ooo m-map chunks.
+ for _, lbls := range []labels.Labels{series1, series2} {
+ ms, created, err := db.head.getOrCreate(lbls.Hash(), lbls, false)
+ require.NoError(t, err)
+ require.False(t, created)
+ require.Len(t, ms.ooo.oooMmappedChunks, 2)
+ require.NotNil(t, ms.ooo.oooHeadChunk)
+ }
+
+ // Restart DB.
+ require.NoError(t, db.Close())
+
+ // For some reason wbl goes missing.
+ require.NoError(t, os.RemoveAll(path.Join(db.Dir(), "wbl")))
+
+ db = newTestDB(t, withDir(db.Dir()))
+ db.DisableCompactions() // We want to manually call it.
+
+ // Check ooo m-map chunks again.
+ for _, lbls := range []labels.Labels{series1, series2} {
+ ms, created, err := db.head.getOrCreate(lbls.Hash(), lbls, false)
+ require.NoError(t, err)
+ require.False(t, created)
+ require.Len(t, ms.ooo.oooMmappedChunks, 2)
+ require.Equal(t, 109*time.Minute.Milliseconds(), ms.ooo.oooMmappedChunks[1].maxTime)
+ require.Nil(t, ms.ooo.oooHeadChunk) // Because of missing wbl.
+ }
+
+ verifySamples := func(fromMins, toMins int64) {
+ series1Samples := make([]chunks.Sample, 0, toMins-fromMins+1)
+ series2Samples := make([]chunks.Sample, 0, toMins-fromMins+1)
+ for m := fromMins; m <= toMins; m++ {
+ ts := m * time.Minute.Milliseconds()
+ series1Samples = append(series1Samples, scenario.sampleFunc(ts, ts))
+ series2Samples = append(series2Samples, scenario.sampleFunc(ts, ts*2))
+ }
+ expRes := map[string][]chunks.Sample{
+ series1.String(): series1Samples,
+ series2.String(): series2Samples,
+ }
+
+ q, err := db.Querier(fromMins*time.Minute.Milliseconds(), toMins*time.Minute.Milliseconds())
+ require.NoError(t, err)
+
+ actRes := query(t, q, labels.MustNewMatcher(labels.MatchRegexp, "foo", "bar.*"))
+ requireEqualSeries(t, expRes, actRes, true)
+ }
+
+ // Checking for expected ooo data from mmap chunks.
+ verifySamples(90, 109)
+
+ // Compaction should also work fine.
+ require.Empty(t, db.Blocks())
+ require.NoError(t, db.CompactOOOHead(ctx))
+ require.Len(t, db.Blocks(), 1) // One block from OOO data.
+ require.Equal(t, int64(0), db.Blocks()[0].MinTime())
+ require.Equal(t, 120*time.Minute.Milliseconds(), db.Blocks()[0].MaxTime())
+
+ // Checking that ooo chunk is empty in Head.
+ for _, lbls := range []labels.Labels{series1, series2} {
+ ms, created, err := db.head.getOrCreate(lbls.Hash(), lbls, false)
+ require.NoError(t, err)
+ require.False(t, created)
+ require.Nil(t, ms.ooo)
+ }
+
+ verifySamples(90, 109)
+}
+
+func TestQuerierOOOQuery_AppendV2(t *testing.T) {
+ scenarios := map[string]struct {
+ appendFunc func(app storage.AppenderV2, ts int64, counterReset bool) (storage.SeriesRef, error)
+ sampleFunc func(ts int64) chunks.Sample
+ }{
+ "float": {
+ appendFunc: func(app storage.AppenderV2, ts int64, _ bool) (storage.SeriesRef, error) {
+ return app.Append(0, labels.FromStrings("foo", "bar1"), 0, ts, float64(ts), nil, nil, storage.AOptions{})
+ },
+ sampleFunc: func(ts int64) chunks.Sample {
+ return sample{t: ts, f: float64(ts)}
+ },
+ },
+ "integer histogram": {
+ appendFunc: func(app storage.AppenderV2, ts int64, counterReset bool) (storage.SeriesRef, error) {
+ h := tsdbutil.GenerateTestHistogram(ts)
+ if counterReset {
+ h.CounterResetHint = histogram.CounterReset
+ }
+ return app.Append(0, labels.FromStrings("foo", "bar1"), 0, ts, 0, h, nil, storage.AOptions{})
+ },
+ sampleFunc: func(ts int64) chunks.Sample {
+ return sample{t: ts, h: tsdbutil.GenerateTestHistogram(ts)}
+ },
+ },
+ "float histogram": {
+ appendFunc: func(app storage.AppenderV2, ts int64, counterReset bool) (storage.SeriesRef, error) {
+ fh := tsdbutil.GenerateTestFloatHistogram(ts)
+ if counterReset {
+ fh.CounterResetHint = histogram.CounterReset
+ }
+ return app.Append(0, labels.FromStrings("foo", "bar1"), 0, ts, 0, nil, fh, storage.AOptions{})
+ },
+ sampleFunc: func(ts int64) chunks.Sample {
+ return sample{t: ts, fh: tsdbutil.GenerateTestFloatHistogram(ts)}
+ },
+ },
+ "integer histogram counter resets": {
+ // Adding counter reset to all histograms means each histogram will have its own chunk.
+ appendFunc: func(app storage.AppenderV2, ts int64, _ bool) (storage.SeriesRef, error) {
+ h := tsdbutil.GenerateTestHistogram(ts)
+ h.CounterResetHint = histogram.CounterReset // For this scenario, ignore the counterReset argument.
+ return app.Append(0, labels.FromStrings("foo", "bar1"), 0, ts, 0, h, nil, storage.AOptions{})
+ },
+ sampleFunc: func(ts int64) chunks.Sample {
+ return sample{t: ts, h: tsdbutil.GenerateTestHistogram(ts)}
+ },
+ },
+ }
+
+ for name, scenario := range scenarios {
+ t.Run(name, func(t *testing.T) {
+ testQuerierOOOQueryAppendV2(t, scenario.appendFunc, scenario.sampleFunc)
+ })
+ }
+}
+
+func testQuerierOOOQueryAppendV2(t *testing.T,
+ appendFunc func(app storage.AppenderV2, ts int64, counterReset bool) (storage.SeriesRef, error),
+ sampleFunc func(ts int64) chunks.Sample,
+) {
+ opts := DefaultOptions()
+ opts.OutOfOrderTimeWindow = 24 * time.Hour.Milliseconds()
+
+ series1 := labels.FromStrings("foo", "bar1")
+
+ type filterFunc func(t int64) bool
+ defaultFilterFunc := func(int64) bool { return true }
+
+ minutes := func(m int64) int64 { return m * time.Minute.Milliseconds() }
+ addSample := func(db *DB, fromMins, toMins, queryMinT, queryMaxT int64, expSamples []chunks.Sample, filter filterFunc, counterReset bool) ([]chunks.Sample, int) {
+ app := db.AppenderV2(context.Background())
+ totalAppended := 0
+ for m := fromMins; m <= toMins; m += time.Minute.Milliseconds() {
+ if !filter(m / time.Minute.Milliseconds()) {
+ continue
+ }
+ _, err := appendFunc(app, m, counterReset)
+ if m >= queryMinT && m <= queryMaxT {
+ expSamples = append(expSamples, sampleFunc(m))
+ }
+ require.NoError(t, err)
+ totalAppended++
+ }
+ require.NoError(t, app.Commit())
+ require.Positive(t, totalAppended, 0) // Sanity check that filter is not too zealous.
+ return expSamples, totalAppended
+ }
+
+ type sampleBatch struct {
+ minT int64
+ maxT int64
+ filter filterFunc
+ counterReset bool
+ isOOO bool
+ }
+
+ tests := []struct {
+ name string
+ oooCap int64
+ queryMinT int64
+ queryMaxT int64
+ batches []sampleBatch
+ }{
+ {
+ name: "query interval covering ooomint and inordermaxt returns all ingested samples",
+ oooCap: 30,
+ queryMinT: minutes(0),
+ queryMaxT: minutes(200),
+ batches: []sampleBatch{
+ {
+ minT: minutes(100),
+ maxT: minutes(200),
+ filter: defaultFilterFunc,
+ },
+ {
+ minT: minutes(0),
+ maxT: minutes(99),
+ filter: defaultFilterFunc,
+ isOOO: true,
+ },
+ },
+ },
+ {
+ name: "partial query interval returns only samples within interval",
+ oooCap: 30,
+ queryMinT: minutes(20),
+ queryMaxT: minutes(180),
+ batches: []sampleBatch{
+ {
+ minT: minutes(100),
+ maxT: minutes(200),
+ filter: defaultFilterFunc,
+ },
+ {
+ minT: minutes(0),
+ maxT: minutes(99),
+ filter: defaultFilterFunc,
+ isOOO: true,
+ },
+ },
+ },
+ {
+ name: "alternating OOO batches", // In order: 100-200 normal. out of order first path: 0, 2, 4, ... 98 (no counter reset), second pass: 1, 3, 5, ... 99 (with counter reset).
+ queryMinT: minutes(0),
+ queryMaxT: minutes(200),
+ batches: []sampleBatch{
+ {
+ minT: minutes(100),
+ maxT: minutes(200),
+ filter: defaultFilterFunc,
+ },
+ {
+ minT: minutes(0),
+ maxT: minutes(99),
+ filter: func(t int64) bool { return t%2 == 0 },
+ isOOO: true,
+ },
+ {
+ minT: minutes(0),
+ maxT: minutes(99),
+ filter: func(t int64) bool { return t%2 == 1 },
+ counterReset: true,
+ isOOO: true,
+ },
+ },
+ },
+ {
+ name: "query overlapping inorder and ooo samples returns all ingested samples at the end of the interval",
+ oooCap: 30,
+ queryMinT: minutes(0),
+ queryMaxT: minutes(200),
+ batches: []sampleBatch{
+ {
+ minT: minutes(100),
+ maxT: minutes(200),
+ filter: func(t int64) bool { return t%2 == 0 },
+ isOOO: false,
+ },
+ {
+ minT: minutes(170),
+ maxT: minutes(180),
+ filter: func(t int64) bool { return t%2 == 1 },
+ isOOO: true,
+ },
+ },
+ },
+ {
+ name: "query overlapping inorder and ooo in-memory samples returns all ingested samples at the beginning of the interval",
+ oooCap: 30,
+ queryMinT: minutes(0),
+ queryMaxT: minutes(200),
+ batches: []sampleBatch{
+ {
+ minT: minutes(100),
+ maxT: minutes(200),
+ filter: func(t int64) bool { return t%2 == 0 },
+ isOOO: false,
+ },
+ {
+ minT: minutes(100),
+ maxT: minutes(110),
+ filter: func(t int64) bool { return t%2 == 1 },
+ isOOO: true,
+ },
+ },
+ },
+ {
+ name: "query inorder contain ooo mmapped samples returns all ingested samples at the beginning of the interval",
+ oooCap: 5,
+ queryMinT: minutes(0),
+ queryMaxT: minutes(200),
+ batches: []sampleBatch{
+ {
+ minT: minutes(100),
+ maxT: minutes(200),
+ filter: func(t int64) bool { return t%2 == 0 },
+ isOOO: false,
+ },
+ {
+ minT: minutes(101),
+ maxT: minutes(101 + (5-1)*2), // Append samples to fit in a single mmapped OOO chunk and fit inside the first in-order mmapped chunk.
+ filter: func(t int64) bool { return t%2 == 1 },
+ isOOO: true,
+ },
+ {
+ minT: minutes(191),
+ maxT: minutes(193), // Append some more OOO samples to trigger mapping the OOO chunk, but use time 151 to not overlap with in-order head chunk.
+ filter: func(t int64) bool { return t%2 == 1 },
+ isOOO: true,
+ },
+ },
+ },
+ {
+ name: "query overlapping inorder and ooo mmapped samples returns all ingested samples at the beginning of the interval",
+ oooCap: 30,
+ queryMinT: minutes(0),
+ queryMaxT: minutes(200),
+ batches: []sampleBatch{
+ {
+ minT: minutes(100),
+ maxT: minutes(200),
+ filter: func(t int64) bool { return t%2 == 0 },
+ isOOO: false,
+ },
+ {
+ minT: minutes(101),
+ maxT: minutes(101 + (30-1)*2), // Append samples to fit in a single mmapped OOO chunk and overlap the first in-order mmapped chunk.
+ filter: func(t int64) bool { return t%2 == 1 },
+ isOOO: true,
+ },
+ {
+ minT: minutes(191),
+ maxT: minutes(193), // Append some more OOO samples to trigger mapping the OOO chunk, but use time 151 to not overlap with in-order head chunk.
+ filter: func(t int64) bool { return t%2 == 1 },
+ isOOO: true,
+ },
+ },
+ },
+ }
+ for _, tc := range tests {
+ t.Run(fmt.Sprintf("name=%s", tc.name), func(t *testing.T) {
+ opts.OutOfOrderCapMax = tc.oooCap
+ db := newTestDB(t, withOpts(opts))
+ db.DisableCompactions()
+
+ var expSamples []chunks.Sample
+ var oooSamples, appendedCount int
+
+ for _, batch := range tc.batches {
+ expSamples, appendedCount = addSample(db, batch.minT, batch.maxT, tc.queryMinT, tc.queryMaxT, expSamples, batch.filter, batch.counterReset)
+ if batch.isOOO {
+ oooSamples += appendedCount
+ }
+ }
+
+ sort.Slice(expSamples, func(i, j int) bool {
+ return expSamples[i].T() < expSamples[j].T()
+ })
+
+ querier, err := db.Querier(tc.queryMinT, tc.queryMaxT)
+ require.NoError(t, err)
+ defer querier.Close()
+
+ seriesSet := query(t, querier, labels.MustNewMatcher(labels.MatchEqual, "foo", "bar1"))
+ gotSamples := seriesSet[series1.String()]
+ require.NotNil(t, gotSamples)
+ require.Len(t, seriesSet, 1)
+ requireEqualSamples(t, series1.String(), expSamples, gotSamples, requireEqualSamplesIgnoreCounterResets)
+ requireEqualOOOSamples(t, oooSamples, db)
+ })
+ }
+}
+
+func TestChunkQuerierOOOQuery_AppendV2(t *testing.T) {
+ nBucketHistogram := func(n int64) *histogram.Histogram {
+ h := &histogram.Histogram{
+ Count: uint64(n),
+ Sum: float64(n),
+ }
+ if n == 0 {
+ h.PositiveSpans = []histogram.Span{}
+ h.PositiveBuckets = []int64{}
+ return h
+ }
+ h.PositiveSpans = []histogram.Span{{Offset: 0, Length: uint32(n)}}
+ h.PositiveBuckets = make([]int64, n)
+ h.PositiveBuckets[0] = 1
+ return h
+ }
+
+ scenarios := map[string]struct {
+ appendFunc func(app storage.AppenderV2, ts int64, counterReset bool) (storage.SeriesRef, error)
+ sampleFunc func(ts int64) chunks.Sample
+ checkInUseBucket bool
+ }{
+ "float": {
+ appendFunc: func(app storage.AppenderV2, ts int64, _ bool) (storage.SeriesRef, error) {
+ return app.Append(0, labels.FromStrings("foo", "bar1"), 0, ts, float64(ts), nil, nil, storage.AOptions{})
+ },
+ sampleFunc: func(ts int64) chunks.Sample {
+ return sample{t: ts, f: float64(ts)}
+ },
+ },
+ "integer histogram": {
+ appendFunc: func(app storage.AppenderV2, ts int64, counterReset bool) (storage.SeriesRef, error) {
+ h := tsdbutil.GenerateTestHistogram(ts)
+ if counterReset {
+ h.CounterResetHint = histogram.CounterReset
+ }
+ return app.Append(0, labels.FromStrings("foo", "bar1"), 0, ts, 0, h, nil, storage.AOptions{})
+ },
+ sampleFunc: func(ts int64) chunks.Sample {
+ return sample{t: ts, h: tsdbutil.GenerateTestHistogram(ts)}
+ },
+ },
+ "float histogram": {
+ appendFunc: func(app storage.AppenderV2, ts int64, counterReset bool) (storage.SeriesRef, error) {
+ fh := tsdbutil.GenerateTestFloatHistogram(ts)
+ if counterReset {
+ fh.CounterResetHint = histogram.CounterReset
+ }
+ return app.Append(0, labels.FromStrings("foo", "bar1"), 0, ts, 0, nil, fh, storage.AOptions{})
+ },
+ sampleFunc: func(ts int64) chunks.Sample {
+ return sample{t: ts, fh: tsdbutil.GenerateTestFloatHistogram(ts)}
+ },
+ },
+ "integer histogram counter resets": {
+ // Adding counter reset to all histograms means each histogram will have its own chunk.
+ appendFunc: func(app storage.AppenderV2, ts int64, _ bool) (storage.SeriesRef, error) {
+ h := tsdbutil.GenerateTestHistogram(ts)
+ h.CounterResetHint = histogram.CounterReset // For this scenario, ignore the counterReset argument.
+ return app.Append(0, labels.FromStrings("foo", "bar1"), 0, ts, 0, h, nil, storage.AOptions{})
+ },
+ sampleFunc: func(ts int64) chunks.Sample {
+ return sample{t: ts, h: tsdbutil.GenerateTestHistogram(ts)}
+ },
+ },
+ "integer histogram with recode": {
+ // Histograms have increasing number of buckets so their chunks are recoded.
+ appendFunc: func(app storage.AppenderV2, ts int64, _ bool) (storage.SeriesRef, error) {
+ n := ts / time.Minute.Milliseconds()
+ return app.Append(0, labels.FromStrings("foo", "bar1"), 0, ts, 0, nBucketHistogram(n), nil, storage.AOptions{})
+ },
+ sampleFunc: func(ts int64) chunks.Sample {
+ n := ts / time.Minute.Milliseconds()
+ return sample{t: ts, h: nBucketHistogram(n)}
+ },
+ // Only check in-use buckets for this scenario.
+ // Recoding adds empty buckets.
+ checkInUseBucket: true,
+ },
+ }
+ for name, scenario := range scenarios {
+ t.Run(name, func(t *testing.T) {
+ testChunkQuerierOOOQueryAppendV2(t, scenario.appendFunc, scenario.sampleFunc, scenario.checkInUseBucket)
+ })
+ }
+}
+
+func testChunkQuerierOOOQueryAppendV2(t *testing.T,
+ appendFunc func(app storage.AppenderV2, ts int64, counterReset bool) (storage.SeriesRef, error),
+ sampleFunc func(ts int64) chunks.Sample,
+ checkInUseBuckets bool,
+) {
+ opts := DefaultOptions()
+ opts.OutOfOrderCapMax = 30
+ opts.OutOfOrderTimeWindow = 24 * time.Hour.Milliseconds()
+
+ series1 := labels.FromStrings("foo", "bar1")
+
+ type filterFunc func(t int64) bool
+ defaultFilterFunc := func(int64) bool { return true }
+
+ minutes := func(m int64) int64 { return m * time.Minute.Milliseconds() }
+ addSample := func(db *DB, fromMins, toMins, queryMinT, queryMaxT int64, expSamples []chunks.Sample, filter filterFunc, counterReset bool) ([]chunks.Sample, int) {
+ app := db.AppenderV2(context.Background())
+ totalAppended := 0
+ for m := fromMins; m <= toMins; m += time.Minute.Milliseconds() {
+ if !filter(m / time.Minute.Milliseconds()) {
+ continue
+ }
+ _, err := appendFunc(app, m, counterReset)
+ if m >= queryMinT && m <= queryMaxT {
+ expSamples = append(expSamples, sampleFunc(m))
+ }
+ require.NoError(t, err)
+ totalAppended++
+ }
+ require.NoError(t, app.Commit())
+ require.Positive(t, totalAppended) // Sanity check that filter is not too zealous.
+ return expSamples, totalAppended
+ }
+
+ type sampleBatch struct {
+ minT int64
+ maxT int64
+ filter filterFunc
+ counterReset bool
+ isOOO bool
+ }
+
+ tests := []struct {
+ name string
+ oooCap int64
+ queryMinT int64
+ queryMaxT int64
+ batches []sampleBatch
+ }{
+ {
+ name: "query interval covering ooomint and inordermaxt returns all ingested samples",
+ oooCap: 30,
+ queryMinT: minutes(0),
+ queryMaxT: minutes(200),
+ batches: []sampleBatch{
+ {
+ minT: minutes(100),
+ maxT: minutes(200),
+ filter: defaultFilterFunc,
+ },
+ {
+ minT: minutes(0),
+ maxT: minutes(99),
+ filter: defaultFilterFunc,
+ isOOO: true,
+ },
+ },
+ },
+ {
+ name: "partial query interval returns only samples within interval",
+ oooCap: 30,
+ queryMinT: minutes(20),
+ queryMaxT: minutes(180),
+ batches: []sampleBatch{
+ {
+ minT: minutes(100),
+ maxT: minutes(200),
+ filter: defaultFilterFunc,
+ },
+ {
+ minT: minutes(0),
+ maxT: minutes(99),
+ filter: defaultFilterFunc,
+ isOOO: true,
+ },
+ },
+ },
+ {
+ name: "alternating OOO batches", // In order: 100-200 normal. out of order first path: 0, 2, 4, ... 98 (no counter reset), second pass: 1, 3, 5, ... 99 (with counter reset).
+ queryMinT: minutes(0),
+ queryMaxT: minutes(200),
+ batches: []sampleBatch{
+ {
+ minT: minutes(100),
+ maxT: minutes(200),
+ filter: defaultFilterFunc,
+ },
+ {
+ minT: minutes(0),
+ maxT: minutes(99),
+ filter: func(t int64) bool { return t%2 == 0 },
+ isOOO: true,
+ },
+ {
+ minT: minutes(0),
+ maxT: minutes(99),
+ filter: func(t int64) bool { return t%2 == 1 },
+ counterReset: true,
+ isOOO: true,
+ },
+ },
+ },
+ {
+ name: "query overlapping inorder and ooo samples returns all ingested samples at the end of the interval",
+ oooCap: 30,
+ queryMinT: minutes(0),
+ queryMaxT: minutes(200),
+ batches: []sampleBatch{
+ {
+ minT: minutes(100),
+ maxT: minutes(200),
+ filter: func(t int64) bool { return t%2 == 0 },
+ isOOO: false,
+ },
+ {
+ minT: minutes(170),
+ maxT: minutes(180),
+ filter: func(t int64) bool { return t%2 == 1 },
+ isOOO: true,
+ },
+ },
+ },
+ {
+ name: "query overlapping inorder and ooo in-memory samples returns all ingested samples at the beginning of the interval",
+ oooCap: 30,
+ queryMinT: minutes(0),
+ queryMaxT: minutes(200),
+ batches: []sampleBatch{
+ {
+ minT: minutes(100),
+ maxT: minutes(200),
+ filter: func(t int64) bool { return t%2 == 0 },
+ isOOO: false,
+ },
+ {
+ minT: minutes(100),
+ maxT: minutes(110),
+ filter: func(t int64) bool { return t%2 == 1 },
+ isOOO: true,
+ },
+ },
+ },
+ {
+ name: "query inorder contain ooo mmapped samples returns all ingested samples at the beginning of the interval",
+ oooCap: 5,
+ queryMinT: minutes(0),
+ queryMaxT: minutes(200),
+ batches: []sampleBatch{
+ {
+ minT: minutes(100),
+ maxT: minutes(200),
+ filter: func(t int64) bool { return t%2 == 0 },
+ isOOO: false,
+ },
+ {
+ minT: minutes(101),
+ maxT: minutes(101 + (5-1)*2), // Append samples to fit in a single mmapped OOO chunk and fit inside the first in-order mmapped chunk.
+ filter: func(t int64) bool { return t%2 == 1 },
+ isOOO: true,
+ },
+ {
+ minT: minutes(191),
+ maxT: minutes(193), // Append some more OOO samples to trigger mapping the OOO chunk, but use time 151 to not overlap with in-order head chunk.
+ filter: func(t int64) bool { return t%2 == 1 },
+ isOOO: true,
+ },
+ },
+ },
+ {
+ name: "query overlapping inorder and ooo mmapped samples returns all ingested samples at the beginning of the interval",
+ oooCap: 30,
+ queryMinT: minutes(0),
+ queryMaxT: minutes(200),
+ batches: []sampleBatch{
+ {
+ minT: minutes(100),
+ maxT: minutes(200),
+ filter: func(t int64) bool { return t%2 == 0 },
+ isOOO: false,
+ },
+ {
+ minT: minutes(101),
+ maxT: minutes(101 + (30-1)*2), // Append samples to fit in a single mmapped OOO chunk and overlap the first in-order mmapped chunk.
+ filter: func(t int64) bool { return t%2 == 1 },
+ isOOO: true,
+ },
+ {
+ minT: minutes(191),
+ maxT: minutes(193), // Append some more OOO samples to trigger mapping the OOO chunk, but use time 151 to not overlap with in-order head chunk.
+ filter: func(t int64) bool { return t%2 == 1 },
+ isOOO: true,
+ },
+ },
+ },
+ }
+ for _, tc := range tests {
+ t.Run(fmt.Sprintf("name=%s", tc.name), func(t *testing.T) {
+ opts.OutOfOrderCapMax = tc.oooCap
+ db := newTestDB(t, withOpts(opts))
+ db.DisableCompactions()
+
+ var expSamples []chunks.Sample
+ var oooSamples, appendedCount int
+
+ for _, batch := range tc.batches {
+ expSamples, appendedCount = addSample(db, batch.minT, batch.maxT, tc.queryMinT, tc.queryMaxT, expSamples, batch.filter, batch.counterReset)
+ if batch.isOOO {
+ oooSamples += appendedCount
+ }
+ }
+
+ sort.Slice(expSamples, func(i, j int) bool {
+ return expSamples[i].T() < expSamples[j].T()
+ })
+
+ querier, err := db.ChunkQuerier(tc.queryMinT, tc.queryMaxT)
+ require.NoError(t, err)
+ defer querier.Close()
+
+ chks := queryChunks(t, querier, labels.MustNewMatcher(labels.MatchEqual, "foo", "bar1"))
+ require.NotNil(t, chks[series1.String()])
+ require.Len(t, chks, 1)
+ requireEqualOOOSamples(t, oooSamples, db)
+ var gotSamples []chunks.Sample
+ for _, chunk := range chks[series1.String()] {
+ it := chunk.Chunk.Iterator(nil)
+ smpls, err := storage.ExpandSamples(it, newSample)
+ require.NoError(t, err)
+
+ // Verify that no sample is outside the chunk's time range.
+ for i, s := range smpls {
+ switch i {
+ case 0:
+ require.Equal(t, chunk.MinTime, s.T(), "first sample %v not at chunk min time %v", s, chunk.MinTime)
+ case len(smpls) - 1:
+ require.Equal(t, chunk.MaxTime, s.T(), "last sample %v not at chunk max time %v", s, chunk.MaxTime)
+ default:
+ require.GreaterOrEqual(t, s.T(), chunk.MinTime, "sample %v before chunk min time %v", s, chunk.MinTime)
+ require.LessOrEqual(t, s.T(), chunk.MaxTime, "sample %v after chunk max time %v", s, chunk.MaxTime)
+ }
+ }
+
+ gotSamples = append(gotSamples, smpls...)
+ require.NoError(t, it.Err())
+ }
+ if checkInUseBuckets {
+ requireEqualSamples(t, series1.String(), expSamples, gotSamples, requireEqualSamplesIgnoreCounterResets, requireEqualSamplesInUseBucketCompare)
+ } else {
+ requireEqualSamples(t, series1.String(), expSamples, gotSamples, requireEqualSamplesIgnoreCounterResets)
+ }
+ })
+ }
+}
+
+// TestOOONativeHistogramsWithCounterResets verifies the counter reset headers for in-order and out-of-order samples
+// upon ingestion. Note that when the counter reset(s) occur in OOO samples, the header is set to UnknownCounterReset
+// rather than CounterReset. This is because with OOO native histogram samples, it cannot be definitely
+// determined if a counter reset occurred because the samples are not consecutive, and another sample
+// could potentially come in that would change the status of the header. In this case, the UnknownCounterReset
+// headers would be re-checked at query time and updated as needed. However, this test is checking the counter
+// reset headers at the time of storage.
+func TestOOONativeHistogramsWithCounterResets_AppendV2(t *testing.T) {
+ for name, scenario := range sampleTypeScenarios {
+ t.Run(name, func(t *testing.T) {
+ if name == intHistogram || name == floatHistogram {
+ testOOONativeHistogramsWithCounterResetsAppendV2(t, scenario)
+ }
+ })
+ }
+}
+
+func testOOONativeHistogramsWithCounterResetsAppendV2(t *testing.T, scenario sampleTypeScenario) {
+ opts := DefaultOptions()
+ opts.OutOfOrderCapMax = 30
+ opts.OutOfOrderTimeWindow = 24 * time.Hour.Milliseconds()
+
+ type resetFunc func(v int64) bool
+ defaultResetFunc := func(int64) bool { return false }
+
+ lbls := labels.FromStrings("foo", "bar1")
+ minutes := func(m int64) int64 { return m * time.Minute.Milliseconds() }
+
+ type sampleBatch struct {
+ from int64
+ until int64
+ shouldReset resetFunc
+ expCounterResetHints []histogram.CounterResetHint
+ }
+
+ tests := []struct {
+ name string
+ queryMin int64
+ queryMax int64
+ batches []sampleBatch
+ expectedSamples []chunks.Sample
+ }{
+ {
+ name: "Counter reset within in-order samples",
+ queryMin: minutes(40),
+ queryMax: minutes(55),
+ batches: []sampleBatch{
+ // In-order samples
+ {
+ from: 40,
+ until: 50,
+ shouldReset: func(v int64) bool {
+ return v == 45
+ },
+ expCounterResetHints: []histogram.CounterResetHint{histogram.UnknownCounterReset, histogram.NotCounterReset, histogram.NotCounterReset, histogram.NotCounterReset, histogram.NotCounterReset, histogram.UnknownCounterReset, histogram.NotCounterReset, histogram.NotCounterReset, histogram.NotCounterReset, histogram.NotCounterReset},
+ },
+ },
+ },
+ {
+ name: "Counter reset right at beginning of OOO samples",
+ queryMin: minutes(40),
+ queryMax: minutes(55),
+ batches: []sampleBatch{
+ // In-order samples
+ {
+ from: 40,
+ until: 45,
+ shouldReset: defaultResetFunc,
+ expCounterResetHints: []histogram.CounterResetHint{histogram.UnknownCounterReset, histogram.NotCounterReset, histogram.NotCounterReset, histogram.NotCounterReset, histogram.NotCounterReset},
+ },
+ {
+ from: 50,
+ until: 55,
+ shouldReset: defaultResetFunc,
+ expCounterResetHints: []histogram.CounterResetHint{histogram.UnknownCounterReset, histogram.NotCounterReset, histogram.NotCounterReset, histogram.NotCounterReset, histogram.NotCounterReset},
+ },
+ // OOO samples
+ {
+ from: 45,
+ until: 50,
+ shouldReset: func(v int64) bool {
+ return v == 45
+ },
+ expCounterResetHints: []histogram.CounterResetHint{histogram.UnknownCounterReset, histogram.NotCounterReset, histogram.NotCounterReset, histogram.NotCounterReset, histogram.NotCounterReset},
+ },
+ },
+ },
+ {
+ name: "Counter resets in both in-order and OOO samples",
+ queryMin: minutes(40),
+ queryMax: minutes(55),
+ batches: []sampleBatch{
+ // In-order samples
+ {
+ from: 40,
+ until: 45,
+ shouldReset: func(v int64) bool {
+ return v == 44
+ },
+ expCounterResetHints: []histogram.CounterResetHint{histogram.UnknownCounterReset, histogram.NotCounterReset, histogram.NotCounterReset, histogram.NotCounterReset, histogram.UnknownCounterReset},
+ },
+ {
+ from: 50,
+ until: 55,
+ shouldReset: defaultResetFunc,
+ expCounterResetHints: []histogram.CounterResetHint{histogram.UnknownCounterReset, histogram.NotCounterReset, histogram.NotCounterReset, histogram.NotCounterReset, histogram.NotCounterReset},
+ },
+ // OOO samples
+ {
+ from: 45,
+ until: 50,
+ shouldReset: func(v int64) bool {
+ return v == 49
+ },
+ expCounterResetHints: []histogram.CounterResetHint{histogram.UnknownCounterReset, histogram.NotCounterReset, histogram.NotCounterReset, histogram.NotCounterReset, histogram.UnknownCounterReset},
+ },
+ },
+ },
+ }
+ for _, tc := range tests {
+ t.Run(fmt.Sprintf("name=%s", tc.name), func(t *testing.T) {
+ db := newTestDB(t, withOpts(opts))
+ db.DisableCompactions()
+
+ app := db.AppenderV2(context.Background())
+
+ expSamples := make(map[string][]chunks.Sample)
+
+ for _, batch := range tc.batches {
+ j := batch.from
+ smplIdx := 0
+ for i := batch.from; i < batch.until; i++ {
+ resetCount := batch.shouldReset(i)
+ if resetCount {
+ j = 0
+ }
+ _, s, err := scenario.appendFunc(storage.AppenderV2AsLimitedV1(app), lbls, minutes(i), j)
+ require.NoError(t, err)
+ if s.Type() == chunkenc.ValHistogram {
+ s.H().CounterResetHint = batch.expCounterResetHints[smplIdx]
+ } else if s.Type() == chunkenc.ValFloatHistogram {
+ s.FH().CounterResetHint = batch.expCounterResetHints[smplIdx]
+ }
+ expSamples[lbls.String()] = append(expSamples[lbls.String()], s)
+ j++
+ smplIdx++
+ }
+ }
+
+ require.NoError(t, app.Commit())
+
+ for k, v := range expSamples {
+ sort.Slice(v, func(i, j int) bool {
+ return v[i].T() < v[j].T()
+ })
+ expSamples[k] = v
+ }
+
+ querier, err := db.Querier(tc.queryMin, tc.queryMax)
+ require.NoError(t, err)
+ defer querier.Close()
+
+ seriesSet := query(t, querier, labels.MustNewMatcher(labels.MatchEqual, "foo", "bar1"))
+ require.NotNil(t, seriesSet[lbls.String()])
+ require.Len(t, seriesSet, 1)
+ requireEqualSeries(t, expSamples, seriesSet, false)
+ })
+ }
+}
+
+func TestOOOInterleavedImplicitCounterResets_AppendV2(t *testing.T) {
+ for name, scenario := range sampleTypeScenarios {
+ t.Run(name, func(t *testing.T) {
+ testOOOInterleavedImplicitCounterResetsV2(t, name, scenario)
+ })
+ }
+}
+
+func testOOOInterleavedImplicitCounterResetsV2(t *testing.T, name string, scenario sampleTypeScenario) {
+ var appendFunc func(app storage.AppenderV2, ts, v int64) error
+
+ if scenario.sampleType != sampleMetricTypeHistogram {
+ return
+ }
+
+ switch name {
+ case intHistogram:
+ appendFunc = func(app storage.AppenderV2, ts, v int64) error {
+ h := &histogram.Histogram{
+ Count: uint64(v),
+ Sum: float64(v),
+ PositiveSpans: []histogram.Span{{Offset: 0, Length: 1}},
+ PositiveBuckets: []int64{v},
+ }
+ _, err := app.Append(0, labels.FromStrings("foo", "bar1"), 0, ts, 0, h, nil, storage.AOptions{})
+ return err
+ }
+ case floatHistogram:
+ appendFunc = func(app storage.AppenderV2, ts, v int64) error {
+ fh := &histogram.FloatHistogram{
+ Count: float64(v),
+ Sum: float64(v),
+ PositiveSpans: []histogram.Span{{Offset: 0, Length: 1}},
+ PositiveBuckets: []float64{float64(v)},
+ }
+ _, err := app.Append(0, labels.FromStrings("foo", "bar1"), 0, ts, 0, nil, fh, storage.AOptions{})
+ return err
+ }
+ case customBucketsIntHistogram:
+ appendFunc = func(app storage.AppenderV2, ts, v int64) error {
+ h := &histogram.Histogram{
+ Schema: -53,
+ Count: uint64(v),
+ Sum: float64(v),
+ PositiveSpans: []histogram.Span{{Offset: 0, Length: 1}},
+ PositiveBuckets: []int64{v},
+ CustomValues: []float64{float64(1), float64(2), float64(3)},
+ }
+ _, err := app.Append(0, labels.FromStrings("foo", "bar1"), 0, ts, 0, h, nil, storage.AOptions{})
+ return err
+ }
+ case customBucketsFloatHistogram:
+ appendFunc = func(app storage.AppenderV2, ts, v int64) error {
+ fh := &histogram.FloatHistogram{
+ Schema: -53,
+ Count: float64(v),
+ Sum: float64(v),
+ PositiveSpans: []histogram.Span{{Offset: 0, Length: 1}},
+ PositiveBuckets: []float64{float64(v)},
+ CustomValues: []float64{float64(1), float64(2), float64(3)},
+ }
+ _, err := app.Append(0, labels.FromStrings("foo", "bar1"), 0, ts, 0, nil, fh, storage.AOptions{})
+ return err
+ }
+ case gaugeIntHistogram, gaugeFloatHistogram:
+ return
+ }
+
+ // Not a sample, we're encoding an integer counter that we convert to a
+ // histogram with a single bucket.
+ type tsValue struct {
+ ts int64
+ v int64
+ }
+
+ type expectedTsValue struct {
+ ts int64
+ v int64
+ hint histogram.CounterResetHint
+ }
+
+ type expectedChunk struct {
+ hint histogram.CounterResetHint
+ size int
+ }
+
+ cases := map[string]struct {
+ samples []tsValue
+ oooCap int64
+ // The expected samples with counter reset.
+ expectedSamples []expectedTsValue
+ // The expected counter reset hint for each chunk.
+ expectedChunks []expectedChunk
+ }{
+ "counter reset in-order cleared by in-memory OOO chunk": {
+ samples: []tsValue{
+ {1, 40}, // New in In-order. I1.
+ {4, 30}, // In-order counter reset. I2.
+ {2, 40}, // New in OOO. O1.
+ {3, 10}, // OOO counter reset. O2.
+ },
+ oooCap: 30,
+ // Expect all to be set to UnknownCounterReset because we switch between
+ // in-order and out-of-order samples.
+ expectedSamples: []expectedTsValue{
+ {1, 40, histogram.UnknownCounterReset}, // I1.
+ {2, 40, histogram.UnknownCounterReset}, // O1.
+ {3, 10, histogram.UnknownCounterReset}, // O2.
+ {4, 30, histogram.UnknownCounterReset}, // I2. Counter reset cleared by iterator change.
+ },
+ expectedChunks: []expectedChunk{
+ {histogram.UnknownCounterReset, 1}, // I1.
+ {histogram.UnknownCounterReset, 1}, // O1.
+ {histogram.UnknownCounterReset, 1}, // O2.
+ {histogram.UnknownCounterReset, 1}, // I2.
+ },
+ },
+ "counter reset in OOO mmapped chunk cleared by in-memory ooo chunk": {
+ samples: []tsValue{
+ {8, 30}, // In-order, new chunk. I1.
+ {1, 10}, // OOO, new chunk (will be mmapped). MO1.
+ {2, 20}, // OOO, no reset (will be mmapped). MO1.
+ {3, 30}, // OOO, no reset (will be mmapped). MO1.
+ {5, 20}, // OOO, reset (will be mmapped). MO2.
+ {6, 10}, // OOO, reset (will be mmapped). MO3.
+ {7, 20}, // OOO, no reset (will be mmapped). MO3.
+ {4, 10}, // OOO, inserted into memory, triggers mmap. O1.
+ },
+ oooCap: 6,
+ expectedSamples: []expectedTsValue{
+ {1, 10, histogram.UnknownCounterReset}, // MO1.
+ {2, 20, histogram.NotCounterReset}, // MO1.
+ {3, 30, histogram.NotCounterReset}, // MO1.
+ {4, 10, histogram.UnknownCounterReset}, // O1. Counter reset cleared by iterator change.
+ {5, 20, histogram.UnknownCounterReset}, // MO2.
+ {6, 10, histogram.UnknownCounterReset}, // MO3.
+ {7, 20, histogram.NotCounterReset}, // MO3.
+ {8, 30, histogram.UnknownCounterReset}, // I1.
+ },
+ expectedChunks: []expectedChunk{
+ {histogram.UnknownCounterReset, 3}, // MO1.
+ {histogram.UnknownCounterReset, 1}, // O1.
+ {histogram.UnknownCounterReset, 1}, // MO2.
+ {histogram.UnknownCounterReset, 2}, // MO3.
+ {histogram.UnknownCounterReset, 1}, // I1.
+ },
+ },
+ "counter reset in OOO mmapped chunk cleared by another OOO mmapped chunk": {
+ samples: []tsValue{
+ {8, 100}, // In-order, new chunk. I1.
+ {1, 50}, // OOO, new chunk (will be mmapped). MO1.
+ {5, 40}, // OOO, reset (will be mmapped). MO2.
+ {6, 50}, // OOO, no reset (will be mmapped). MO2.
+ {2, 10}, // OOO, new chunk no reset (will be mmapped). MO3.
+ {3, 20}, // OOO, no reset (will be mmapped). MO3.
+ {4, 30}, // OOO, no reset (will be mmapped). MO3.
+ {7, 60}, // OOO, no reset in memory. O1.
+ },
+ oooCap: 3,
+ expectedSamples: []expectedTsValue{
+ {1, 50, histogram.UnknownCounterReset}, // MO1.
+ {2, 10, histogram.UnknownCounterReset}, // MO3.
+ {3, 20, histogram.NotCounterReset}, // MO3.
+ {4, 30, histogram.NotCounterReset}, // MO3.
+ {5, 40, histogram.UnknownCounterReset}, // MO2.
+ {6, 50, histogram.NotCounterReset}, // MO2.
+ {7, 60, histogram.UnknownCounterReset}, // O1.
+ {8, 100, histogram.UnknownCounterReset}, // I1.
+ },
+ expectedChunks: []expectedChunk{
+ {histogram.UnknownCounterReset, 1}, // MO1.
+ {histogram.UnknownCounterReset, 3}, // MO3.
+ {histogram.UnknownCounterReset, 2}, // MO2.
+ {histogram.UnknownCounterReset, 1}, // O1.
+ {histogram.UnknownCounterReset, 1}, // I1.
+ },
+ },
+ }
+
+ for tcName, tc := range cases {
+ t.Run(tcName, func(t *testing.T) {
+ opts := DefaultOptions()
+ opts.OutOfOrderCapMax = tc.oooCap
+ opts.OutOfOrderTimeWindow = 24 * time.Hour.Milliseconds()
+
+ db := newTestDB(t, withOpts(opts))
+ db.DisableCompactions()
+
+ app := db.AppenderV2(context.Background())
+ for _, s := range tc.samples {
+ require.NoError(t, appendFunc(app, s.ts, s.v))
+ }
+ require.NoError(t, app.Commit())
+
+ t.Run("querier", func(t *testing.T) {
+ querier, err := db.Querier(0, 10)
+ require.NoError(t, err)
+ defer querier.Close()
+
+ seriesSet := query(t, querier, labels.MustNewMatcher(labels.MatchEqual, "foo", "bar1"))
+ require.Len(t, seriesSet, 1)
+ samples, ok := seriesSet["{foo=\"bar1\"}"]
+ require.True(t, ok)
+ require.Len(t, samples, len(tc.samples))
+ require.Len(t, samples, len(tc.expectedSamples))
+
+ // We expect all unknown counter resets because we clear the counter reset
+ // hint when we switch between in-order and out-of-order samples.
+ for i, s := range samples {
+ switch name {
+ case intHistogram:
+ require.Equal(t, tc.expectedSamples[i].hint, s.H().CounterResetHint, "sample %d", i)
+ require.Equal(t, tc.expectedSamples[i].v, int64(s.H().Count), "sample %d", i)
+ case floatHistogram:
+ require.Equal(t, tc.expectedSamples[i].hint, s.FH().CounterResetHint, "sample %d", i)
+ require.Equal(t, tc.expectedSamples[i].v, int64(s.FH().Count), "sample %d", i)
+ case customBucketsIntHistogram:
+ require.Equal(t, tc.expectedSamples[i].hint, s.H().CounterResetHint, "sample %d", i)
+ require.Equal(t, tc.expectedSamples[i].v, int64(s.H().Count), "sample %d", i)
+ case customBucketsFloatHistogram:
+ require.Equal(t, tc.expectedSamples[i].hint, s.FH().CounterResetHint, "sample %d", i)
+ require.Equal(t, tc.expectedSamples[i].v, int64(s.FH().Count), "sample %d", i)
+ default:
+ t.Fatalf("unexpected sample type %s", name)
+ }
+ }
+ })
+
+ t.Run("chunk-querier", func(t *testing.T) {
+ querier, err := db.ChunkQuerier(0, 10)
+ require.NoError(t, err)
+ defer querier.Close()
+
+ chunkSet := queryAndExpandChunks(t, querier, labels.MustNewMatcher(labels.MatchEqual, "foo", "bar1"))
+ require.Len(t, chunkSet, 1)
+ chunks, ok := chunkSet["{foo=\"bar1\"}"]
+ require.True(t, ok)
+ require.Len(t, chunks, len(tc.expectedChunks))
+ idx := 0
+ for i, samples := range chunks {
+ require.Len(t, samples, tc.expectedChunks[i].size)
+ for j, s := range samples {
+ expectHint := tc.expectedChunks[i].hint
+ if j > 0 {
+ expectHint = histogram.NotCounterReset
+ }
+ switch name {
+ case intHistogram:
+ require.Equal(t, expectHint, s.H().CounterResetHint, "sample %d", idx)
+ require.Equal(t, tc.expectedSamples[idx].v, int64(s.H().Count), "sample %d", idx)
+ case floatHistogram:
+ require.Equal(t, expectHint, s.FH().CounterResetHint, "sample %d", idx)
+ require.Equal(t, tc.expectedSamples[idx].v, int64(s.FH().Count), "sample %d", idx)
+ case customBucketsIntHistogram:
+ require.Equal(t, expectHint, s.H().CounterResetHint, "sample %d", idx)
+ require.Equal(t, tc.expectedSamples[idx].v, int64(s.H().Count), "sample %d", idx)
+ case customBucketsFloatHistogram:
+ require.Equal(t, expectHint, s.FH().CounterResetHint, "sample %d", idx)
+ require.Equal(t, tc.expectedSamples[idx].v, int64(s.FH().Count), "sample %d", idx)
+ default:
+ t.Fatalf("unexpected sample type %s", name)
+ }
+ idx++
+ }
+ }
+ })
+ })
+ }
+}
+
+func TestOOOAppendAndQuery_AppendV2(t *testing.T) {
+ for name, scenario := range sampleTypeScenarios {
+ t.Run(name, func(t *testing.T) {
+ testOOOAppendAndQueryAppendV2(t, scenario)
+ })
+ }
+}
+
+func testOOOAppendAndQueryAppendV2(t *testing.T, scenario sampleTypeScenario) {
+ opts := DefaultOptions()
+ opts.OutOfOrderCapMax = 30
+ opts.OutOfOrderTimeWindow = 4 * time.Hour.Milliseconds()
+
+ db := newTestDB(t, withOpts(opts))
+ db.DisableCompactions()
+
+ s1 := labels.FromStrings("foo", "bar1")
+ s2 := labels.FromStrings("foo", "bar2")
+
+ minutes := func(m int64) int64 { return m * time.Minute.Milliseconds() }
+ appendedSamples := make(map[string][]chunks.Sample)
+ totalSamples := 0
+ addSample := func(lbls labels.Labels, fromMins, toMins int64, faceError bool) {
+ app := db.AppenderV2(context.Background())
+ key := lbls.String()
+ from, to := minutes(fromMins), minutes(toMins)
+ for m := from; m <= to; m += time.Minute.Milliseconds() {
+ val := rand.Intn(1000)
+ _, s, err := scenario.appendFunc(storage.AppenderV2AsLimitedV1(app), lbls, m, int64(val))
+ if faceError {
+ require.Error(t, err)
+ } else {
+ require.NoError(t, err)
+ appendedSamples[key] = append(appendedSamples[key], s)
+ totalSamples++
+ }
+ }
+ if faceError {
+ require.NoError(t, app.Rollback())
+ } else {
+ require.NoError(t, app.Commit())
+ }
+ }
+
+ testQuery := func(from, to int64) {
+ querier, err := db.Querier(from, to)
+ require.NoError(t, err)
+
+ seriesSet := query(t, querier, labels.MustNewMatcher(labels.MatchRegexp, "foo", "bar."))
+
+ for k, v := range appendedSamples {
+ sort.Slice(v, func(i, j int) bool {
+ return v[i].T() < v[j].T()
+ })
+ appendedSamples[k] = v
+ }
+
+ expSamples := make(map[string][]chunks.Sample)
+ for k, samples := range appendedSamples {
+ for _, s := range samples {
+ if s.T() < from {
+ continue
+ }
+ if s.T() > to {
+ continue
+ }
+ expSamples[k] = append(expSamples[k], s)
+ }
+ }
+ requireEqualSeries(t, expSamples, seriesSet, true)
+ requireEqualOOOSamples(t, totalSamples-2, db)
+ }
+
+ verifyOOOMinMaxTimes := func(expMin, expMax int64) {
+ require.Equal(t, minutes(expMin), db.head.MinOOOTime())
+ require.Equal(t, minutes(expMax), db.head.MaxOOOTime())
+ }
+
+ // In-order samples.
+ addSample(s1, 300, 300, false)
+ addSample(s2, 290, 290, false)
+ require.Equal(t, float64(2), prom_testutil.ToFloat64(db.head.metrics.chunksCreated))
+ testQuery(math.MinInt64, math.MaxInt64)
+
+ // Some ooo samples.
+ addSample(s1, 250, 260, false)
+ addSample(s2, 255, 265, false)
+ verifyOOOMinMaxTimes(250, 265)
+ testQuery(math.MinInt64, math.MaxInt64)
+ testQuery(minutes(250), minutes(265)) // Test querying ooo data time range.
+ testQuery(minutes(290), minutes(300)) // Test querying in-order data time range.
+ testQuery(minutes(250), minutes(300)) // Test querying the entire range.
+
+ // Out of time window.
+ addSample(s1, 59, 59, true)
+ addSample(s2, 49, 49, true)
+ verifyOOOMinMaxTimes(250, 265)
+ testQuery(math.MinInt64, math.MaxInt64)
+
+ // At the edge of time window, also it would be "out of bound" without the ooo support.
+ addSample(s1, 60, 65, false)
+ verifyOOOMinMaxTimes(60, 265)
+ testQuery(math.MinInt64, math.MaxInt64)
+
+ // This sample is not within the time window w.r.t. the head's maxt, but it is within the window
+ // w.r.t. the series' maxt. But we consider only head's maxt.
+ addSample(s2, 59, 59, true)
+ verifyOOOMinMaxTimes(60, 265)
+ testQuery(math.MinInt64, math.MaxInt64)
+
+ // Now the sample is within time window w.r.t. the head's maxt.
+ addSample(s2, 60, 65, false)
+ verifyOOOMinMaxTimes(60, 265)
+ testQuery(math.MinInt64, math.MaxInt64)
+
+ // Out of time window again.
+ addSample(s1, 59, 59, true)
+ addSample(s2, 49, 49, true)
+ testQuery(math.MinInt64, math.MaxInt64)
+
+ // Generating some m-map chunks. The m-map chunks here are in such a way
+ // that when sorted w.r.t. mint, the last chunk's maxt is not the overall maxt
+ // of the merged chunk. This tests a bug fixed in https://github.com/grafana/mimir-prometheus/pull/238/.
+ require.Equal(t, float64(4), prom_testutil.ToFloat64(db.head.metrics.chunksCreated))
+ addSample(s1, 180, 249, false)
+ require.Equal(t, float64(6), prom_testutil.ToFloat64(db.head.metrics.chunksCreated))
+ verifyOOOMinMaxTimes(60, 265)
+ testQuery(math.MinInt64, math.MaxInt64)
+}
+
+func TestOOODisabled_AppendV2(t *testing.T) {
+ for name, scenario := range sampleTypeScenarios {
+ t.Run(name, func(t *testing.T) {
+ testOOODisabledAppendV2(t, scenario)
+ })
+ }
+}
+
+func testOOODisabledAppendV2(t *testing.T, scenario sampleTypeScenario) {
+ opts := DefaultOptions()
+ opts.OutOfOrderTimeWindow = 0
+ db := newTestDB(t, withOpts(opts))
+ db.DisableCompactions()
+
+ s1 := labels.FromStrings("foo", "bar1")
+ minutes := func(m int64) int64 { return m * time.Minute.Milliseconds() }
+ expSamples := make(map[string][]chunks.Sample)
+ totalSamples := 0
+ failedSamples := 0
+
+ addSample := func(db *DB, lbls labels.Labels, fromMins, toMins int64, faceError bool) {
+ app := db.AppenderV2(context.Background())
+ key := lbls.String()
+ from, to := minutes(fromMins), minutes(toMins)
+ for m := from; m <= to; m += time.Minute.Milliseconds() {
+ _, _, err := scenario.appendFunc(storage.AppenderV2AsLimitedV1(app), lbls, m, m)
+ if faceError {
+ require.Error(t, err)
+ failedSamples++
+ } else {
+ require.NoError(t, err)
+ expSamples[key] = append(expSamples[key], scenario.sampleFunc(m, m))
+ totalSamples++
+ }
+ }
+ if faceError {
+ require.NoError(t, app.Rollback())
+ } else {
+ require.NoError(t, app.Commit())
+ }
+ }
+
+ addSample(db, s1, 300, 300, false) // In-order samples.
+ addSample(db, s1, 250, 260, true) // Some ooo samples.
+ addSample(db, s1, 59, 59, true) // Out of time window.
+ addSample(db, s1, 60, 65, true) // At the edge of time window, also it would be "out of bound" without the ooo support.
+ addSample(db, s1, 59, 59, true) // Out of time window again.
+ addSample(db, s1, 301, 310, false) // More in-order samples.
+
+ querier, err := db.Querier(math.MinInt64, math.MaxInt64)
+ require.NoError(t, err)
+
+ seriesSet := query(t, querier, labels.MustNewMatcher(labels.MatchRegexp, "foo", "bar."))
+ requireEqualSeries(t, expSamples, seriesSet, true)
+ requireEqualOOOSamples(t, 0, db)
+ require.Equal(t, float64(failedSamples),
+ prom_testutil.ToFloat64(db.head.metrics.outOfOrderSamples.WithLabelValues(scenario.sampleType))+prom_testutil.ToFloat64(db.head.metrics.outOfBoundSamples.WithLabelValues(scenario.sampleType)),
+ "number of ooo/oob samples mismatch")
+
+ // Verifying that no OOO artifacts were generated.
+ _, err = os.ReadDir(path.Join(db.Dir(), wlog.WblDirName))
+ require.True(t, os.IsNotExist(err))
+
+ ms, created, err := db.head.getOrCreate(s1.Hash(), s1, false)
+ require.NoError(t, err)
+ require.False(t, created)
+ require.NotNil(t, ms)
+ require.Nil(t, ms.ooo)
+}
+
+func TestWBLAndMmapReplay_AppendV2(t *testing.T) {
+ for name, scenario := range sampleTypeScenarios {
+ t.Run(name, func(t *testing.T) {
+ testWBLAndMmapReplayAppendV2(t, scenario)
+ })
+ }
+}
+
+func testWBLAndMmapReplayAppendV2(t *testing.T, scenario sampleTypeScenario) {
+ opts := DefaultOptions()
+ opts.OutOfOrderCapMax = 30
+ opts.OutOfOrderTimeWindow = 4 * time.Hour.Milliseconds()
+
+ db := newTestDB(t, withOpts(opts))
+ db.DisableCompactions()
+
+ s1 := labels.FromStrings("foo", "bar1")
+
+ minutes := func(m int64) int64 { return m * time.Minute.Milliseconds() }
+ expSamples := make(map[string][]chunks.Sample)
+ totalSamples := 0
+ addSample := func(lbls labels.Labels, fromMins, toMins int64) {
+ app := db.AppenderV2(context.Background())
+ key := lbls.String()
+ from, to := minutes(fromMins), minutes(toMins)
+ for m := from; m <= to; m += time.Minute.Milliseconds() {
+ val := rand.Intn(1000)
+ _, s, err := scenario.appendFunc(storage.AppenderV2AsLimitedV1(app), lbls, m, int64(val))
+ require.NoError(t, err)
+ expSamples[key] = append(expSamples[key], s)
+ totalSamples++
+ }
+ require.NoError(t, app.Commit())
+ }
+
+ testQuery := func(exp map[string][]chunks.Sample) {
+ querier, err := db.Querier(math.MinInt64, math.MaxInt64)
+ require.NoError(t, err)
+
+ seriesSet := query(t, querier, labels.MustNewMatcher(labels.MatchRegexp, "foo", "bar."))
+
+ for k, v := range exp {
+ sort.Slice(v, func(i, j int) bool {
+ return v[i].T() < v[j].T()
+ })
+ exp[k] = v
+ }
+ requireEqualSeries(t, exp, seriesSet, true)
+ }
+
+ // In-order samples.
+ addSample(s1, 300, 300)
+ require.Equal(t, float64(1), prom_testutil.ToFloat64(db.head.metrics.chunksCreated))
+
+ // Some ooo samples.
+ addSample(s1, 250, 260)
+ addSample(s1, 195, 249) // This creates some m-map chunks.
+ require.Equal(t, float64(4), prom_testutil.ToFloat64(db.head.metrics.chunksCreated))
+ testQuery(expSamples)
+ oooMint, oooMaxt := minutes(195), minutes(260)
+
+ // Collect the samples only present in the ooo m-map chunks.
+ ms, created, err := db.head.getOrCreate(s1.Hash(), s1, false)
+ require.False(t, created)
+ require.NoError(t, err)
+ var s1MmapSamples []chunks.Sample
+ for _, mc := range ms.ooo.oooMmappedChunks {
+ chk, err := db.head.chunkDiskMapper.Chunk(mc.ref)
+ require.NoError(t, err)
+ it := chk.Iterator(nil)
+ smpls, err := storage.ExpandSamples(it, newSample)
+ require.NoError(t, err)
+ s1MmapSamples = append(s1MmapSamples, smpls...)
+ }
+ require.NotEmpty(t, s1MmapSamples)
+
+ require.NoError(t, db.Close())
+
+ // Making a copy of original state of WBL and Mmap files to use it later.
+ mmapDir := mmappedChunksDir(db.head.opts.ChunkDirRoot)
+ wblDir := db.head.wbl.Dir()
+ originalWblDir := filepath.Join(t.TempDir(), "original_wbl")
+ originalMmapDir := filepath.Join(t.TempDir(), "original_mmap")
+ require.NoError(t, fileutil.CopyDirs(wblDir, originalWblDir))
+ require.NoError(t, fileutil.CopyDirs(mmapDir, originalMmapDir))
+ resetWBLToOriginal := func() {
+ require.NoError(t, os.RemoveAll(wblDir))
+ require.NoError(t, fileutil.CopyDirs(originalWblDir, wblDir))
+ }
+ resetMmapToOriginal := func() {
+ require.NoError(t, os.RemoveAll(mmapDir))
+ require.NoError(t, fileutil.CopyDirs(originalMmapDir, mmapDir))
+ }
+
+ t.Run("Restart DB with both WBL and M-map files for ooo data", func(t *testing.T) {
+ db = newTestDB(t, withDir(db.Dir()), withOpts(opts))
+ require.Equal(t, oooMint, db.head.MinOOOTime())
+ require.Equal(t, oooMaxt, db.head.MaxOOOTime())
+ testQuery(expSamples)
+ })
+
+ t.Run("Restart DB with only WBL for ooo data", func(t *testing.T) {
+ require.NoError(t, os.RemoveAll(mmapDir))
+
+ db = newTestDB(t, withDir(db.Dir()), withOpts(opts))
+ require.Equal(t, oooMint, db.head.MinOOOTime())
+ require.Equal(t, oooMaxt, db.head.MaxOOOTime())
+ testQuery(expSamples)
+ })
+
+ t.Run("Restart DB with only M-map files for ooo data", func(t *testing.T) {
+ require.NoError(t, os.RemoveAll(wblDir))
+ resetMmapToOriginal()
+
+ db = newTestDB(t, withDir(db.Dir()), withOpts(opts))
+ require.Equal(t, oooMint, db.head.MinOOOTime())
+ require.Equal(t, oooMaxt, db.head.MaxOOOTime())
+ inOrderSample := expSamples[s1.String()][len(expSamples[s1.String()])-1]
+ testQuery(map[string][]chunks.Sample{
+ s1.String(): append(s1MmapSamples, inOrderSample),
+ })
+ })
+
+ t.Run("Restart DB with WBL+Mmap while increasing the OOOCapMax", func(t *testing.T) {
+ resetWBLToOriginal()
+ resetMmapToOriginal()
+
+ opts.OutOfOrderCapMax = 60
+ db = newTestDB(t, withDir(db.Dir()), withOpts(opts))
+ require.NoError(t, err)
+ require.Equal(t, oooMint, db.head.MinOOOTime())
+ require.Equal(t, oooMaxt, db.head.MaxOOOTime())
+ testQuery(expSamples)
+ })
+
+ t.Run("Restart DB with WBL+Mmap while decreasing the OOOCapMax", func(t *testing.T) {
+ resetMmapToOriginal() // We need to reset because new duplicate chunks can be written above.
+
+ opts.OutOfOrderCapMax = 10
+ db = newTestDB(t, withDir(db.Dir()), withOpts(opts))
+ require.NoError(t, err)
+ require.Equal(t, oooMint, db.head.MinOOOTime())
+ require.Equal(t, oooMaxt, db.head.MaxOOOTime())
+ testQuery(expSamples)
+ })
+
+ t.Run("Restart DB with WBL+Mmap while having no m-map markers in WBL", func(t *testing.T) {
+ resetMmapToOriginal() // We neet to reset because new duplicate chunks can be written above.
+
+ // Removing m-map markers in WBL by rewriting it.
+ newWbl, err := wlog.New(promslog.NewNopLogger(), nil, filepath.Join(t.TempDir(), "new_wbl"), compression.None)
+ require.NoError(t, err)
+ sr, err := wlog.NewSegmentsReader(originalWblDir)
+ require.NoError(t, err)
+ dec := record.NewDecoder(labels.NewSymbolTable(), promslog.NewNopLogger())
+ r, markers, addedRecs := wlog.NewReader(sr), 0, 0
+ for r.Next() {
+ rec := r.Record()
+ if dec.Type(rec) == record.MmapMarkers {
+ markers++
+ continue
+ }
+ addedRecs++
+ require.NoError(t, newWbl.Log(rec))
+ }
+ require.Positive(t, markers)
+ require.Positive(t, addedRecs)
+ require.NoError(t, newWbl.Close())
+ require.NoError(t, sr.Close())
+ require.NoError(t, os.RemoveAll(wblDir))
+ require.NoError(t, os.Rename(newWbl.Dir(), wblDir))
+
+ opts.OutOfOrderCapMax = 30
+ db = newTestDB(t, withDir(db.Dir()), withOpts(opts))
+ require.NoError(t, err)
+ require.Equal(t, oooMint, db.head.MinOOOTime())
+ require.Equal(t, oooMaxt, db.head.MaxOOOTime())
+ testQuery(expSamples)
+ })
+}
+
+func TestOOOHistogramCompactionWithCounterResets_AppendV2(t *testing.T) {
+ for _, floatHistogram := range []bool{false, true} {
+ ctx := context.Background()
+
+ opts := DefaultOptions()
+ opts.OutOfOrderCapMax = 30
+ opts.OutOfOrderTimeWindow = 500 * time.Minute.Milliseconds()
+
+ db := newTestDB(t, withOpts(opts))
+ db.DisableCompactions() // We want to manually call it.
+
+ series1 := labels.FromStrings("foo", "bar1")
+ series2 := labels.FromStrings("foo", "bar2")
+
+ var series1ExpSamplesPreCompact, series2ExpSamplesPreCompact, series1ExpSamplesPostCompact, series2ExpSamplesPostCompact []chunks.Sample
+
+ addSample := func(ts int64, l labels.Labels, val int, hint histogram.CounterResetHint) sample {
+ app := db.AppenderV2(context.Background())
+ tsMs := ts * time.Minute.Milliseconds()
+ if floatHistogram {
+ h := tsdbutil.GenerateTestFloatHistogram(int64(val))
+ h.CounterResetHint = hint
+ _, err := app.Append(0, l, 0, tsMs, 0, nil, h, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ return sample{t: tsMs, fh: h.Copy()}
+ }
+
+ h := tsdbutil.GenerateTestHistogram(int64(val))
+ h.CounterResetHint = hint
+ _, err := app.Append(0, l, 0, tsMs, 0, h, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ return sample{t: tsMs, h: h.Copy()}
+ }
+
+ // Add an in-order sample to each series.
+ s := addSample(520, series1, 1000000, histogram.UnknownCounterReset)
+ series1ExpSamplesPreCompact = append(series1ExpSamplesPreCompact, s)
+ series1ExpSamplesPostCompact = append(series1ExpSamplesPostCompact, s)
+
+ s = addSample(520, series2, 1000000, histogram.UnknownCounterReset)
+ series2ExpSamplesPreCompact = append(series2ExpSamplesPreCompact, s)
+ series2ExpSamplesPostCompact = append(series2ExpSamplesPostCompact, s)
+
+ // Verify that the in-memory ooo chunk is empty.
+ checkEmptyOOOChunk := func(lbls labels.Labels) {
+ ms, created, err := db.head.getOrCreate(lbls.Hash(), lbls, false)
+ require.NoError(t, err)
+ require.False(t, created)
+ require.Nil(t, ms.ooo)
+ }
+
+ checkEmptyOOOChunk(series1)
+ checkEmptyOOOChunk(series2)
+
+ // Add samples for series1. There are three head chunks that will be created:
+ // Chunk 1 - Samples between 100 - 440. One explicit counter reset at ts 250.
+ // Chunk 2 - Samples between 105 - 395. Overlaps with Chunk 1. One detected counter reset at ts 165.
+ // Chunk 3 - Samples between 480 - 509. All within one block boundary. One detected counter reset at 490.
+
+ // Chunk 1.
+ // First add 10 samples.
+ for i := 100; i < 200; i += 10 {
+ s = addSample(int64(i), series1, 100000+i, histogram.UnknownCounterReset)
+ // Before compaction, all the samples have UnknownCounterReset even though they've been added to the same
+ // chunk. This is because they overlap with the samples from chunk two and when merging two chunks on read,
+ // the header is set as unknown when the next sample is not in the same chunk as the previous one.
+ series1ExpSamplesPreCompact = append(series1ExpSamplesPreCompact, s)
+ // After compaction, samples from multiple mmapped chunks will be merged, so there won't be any overlapping
+ // chunks. Therefore, most samples will have the NotCounterReset header.
+ // 100 is the first sample in the first chunk in the blocks, so is still set to UnknownCounterReset.
+ // 120 is a block boundary - after compaction, 120 will be the first sample in a chunk, so is still set to
+ // UnknownCounterReset.
+ if i > 100 && i != 120 {
+ s = copyWithCounterReset(s, histogram.NotCounterReset)
+ }
+ series1ExpSamplesPostCompact = append(series1ExpSamplesPostCompact, s)
+ }
+ // Explicit counter reset - the counter reset header is set to CounterReset but the value is higher
+ // than for the previous timestamp. Explicit counter reset headers are actually ignored though, so when reading
+ // the sample back you actually get unknown/not counter reset. This is as the chainSampleIterator ignores
+ // existing headers and sets the header as UnknownCounterReset if the next sample is not in the same chunk as
+ // the previous one, and counter resets always create a new chunk.
+ // This case has been added to document what's happening, though it might not be the ideal behavior.
+ s = addSample(250, series1, 100000+250, histogram.CounterReset)
+ series1ExpSamplesPreCompact = append(series1ExpSamplesPreCompact, copyWithCounterReset(s, histogram.UnknownCounterReset))
+ series1ExpSamplesPostCompact = append(series1ExpSamplesPostCompact, copyWithCounterReset(s, histogram.NotCounterReset))
+
+ // Add 19 more samples to complete a chunk.
+ for i := 260; i < 450; i += 10 {
+ s = addSample(int64(i), series1, 100000+i, histogram.UnknownCounterReset)
+ // The samples with timestamp less than 410 overlap with the samples from chunk 2, so before compaction,
+ // they're all UnknownCounterReset. Samples greater than or equal to 410 don't overlap with other chunks
+ // so they're always detected as NotCounterReset pre and post compaction.
+ if i >= 410 {
+ s = copyWithCounterReset(s, histogram.NotCounterReset)
+ }
+ series1ExpSamplesPreCompact = append(series1ExpSamplesPreCompact, s)
+ //
+ // 360 is a block boundary, so after compaction its header is still UnknownCounterReset.
+ if i != 360 {
+ s = copyWithCounterReset(s, histogram.NotCounterReset)
+ }
+ series1ExpSamplesPostCompact = append(series1ExpSamplesPostCompact, s)
+ }
+
+ // Chunk 2.
+ // Add six OOO samples.
+ for i := 105; i < 165; i += 10 {
+ s = addSample(int64(i), series1, 100000+i, histogram.UnknownCounterReset)
+ // Samples overlap with chunk 1 so before compaction all headers are UnknownCounterReset.
+ series1ExpSamplesPreCompact = append(series1ExpSamplesPreCompact, s)
+ series1ExpSamplesPostCompact = append(series1ExpSamplesPostCompact, copyWithCounterReset(s, histogram.NotCounterReset))
+ }
+
+ // Add sample that will be detected as a counter reset.
+ s = addSample(165, series1, 100000, histogram.UnknownCounterReset)
+ // Before compaction, sample has an UnknownCounterReset header due to the chainSampleIterator.
+ series1ExpSamplesPreCompact = append(series1ExpSamplesPreCompact, s)
+ // After compaction, the sample's counter reset is still UnknownCounterReset as we cannot trust CounterReset
+ // headers in chunks at the moment, so when reading the first sample in a chunk, its hint is set to
+ // UnknownCounterReset.
+ series1ExpSamplesPostCompact = append(series1ExpSamplesPostCompact, s)
+
+ // Add 23 more samples to complete a chunk.
+ for i := 175; i < 405; i += 10 {
+ s = addSample(int64(i), series1, 100000+i, histogram.UnknownCounterReset)
+ // Samples between 205-255 overlap with chunk 1 so before compaction those samples will have the
+ // UnknownCounterReset header.
+ if i >= 205 && i < 255 {
+ s = copyWithCounterReset(s, histogram.NotCounterReset)
+ }
+ series1ExpSamplesPreCompact = append(series1ExpSamplesPreCompact, s)
+ // 245 is the first sample >= the block boundary at 240, so it's still UnknownCounterReset after compaction.
+ if i != 245 {
+ s = copyWithCounterReset(s, histogram.NotCounterReset)
+ } else {
+ s = copyWithCounterReset(s, histogram.UnknownCounterReset)
+ }
+ series1ExpSamplesPostCompact = append(series1ExpSamplesPostCompact, s)
+ }
+
+ // Chunk 3.
+ for i := 480; i < 490; i++ {
+ s = addSample(int64(i), series1, 100000+i, histogram.UnknownCounterReset)
+ // No overlapping samples in other chunks, so all other samples will already be detected as NotCounterReset
+ // before compaction.
+ if i > 480 {
+ s = copyWithCounterReset(s, histogram.NotCounterReset)
+ }
+ series1ExpSamplesPreCompact = append(series1ExpSamplesPreCompact, s)
+ // 480 is block boundary.
+ if i == 480 {
+ s = copyWithCounterReset(s, histogram.UnknownCounterReset)
+ }
+ series1ExpSamplesPostCompact = append(series1ExpSamplesPostCompact, s)
+ }
+ // Counter reset.
+ s = addSample(int64(490), series1, 100000, histogram.UnknownCounterReset)
+ series1ExpSamplesPreCompact = append(series1ExpSamplesPreCompact, s)
+ series1ExpSamplesPostCompact = append(series1ExpSamplesPostCompact, s)
+ // Add some more samples after the counter reset.
+ for i := 491; i < 510; i++ {
+ s = addSample(int64(i), series1, 100000+i, histogram.UnknownCounterReset)
+ s = copyWithCounterReset(s, histogram.NotCounterReset)
+ series1ExpSamplesPreCompact = append(series1ExpSamplesPreCompact, s)
+ series1ExpSamplesPostCompact = append(series1ExpSamplesPostCompact, s)
+ }
+
+ // Add samples for series2 - one chunk with one detected counter reset at 300.
+ for i := 200; i < 300; i += 10 {
+ s = addSample(int64(i), series2, 100000+i, histogram.UnknownCounterReset)
+ if i > 200 {
+ s = copyWithCounterReset(s, histogram.NotCounterReset)
+ }
+ series2ExpSamplesPreCompact = append(series2ExpSamplesPreCompact, s)
+ if i == 240 {
+ s = copyWithCounterReset(s, histogram.UnknownCounterReset)
+ }
+ series2ExpSamplesPostCompact = append(series2ExpSamplesPostCompact, s)
+ }
+ // Counter reset.
+ s = addSample(int64(300), series2, 100000, histogram.UnknownCounterReset)
+ series2ExpSamplesPreCompact = append(series2ExpSamplesPreCompact, s)
+ series2ExpSamplesPostCompact = append(series2ExpSamplesPostCompact, s)
+ // Add some more samples after the counter reset.
+ for i := 310; i < 500; i += 10 {
+ s := addSample(int64(i), series2, 100000+i, histogram.UnknownCounterReset)
+ s = copyWithCounterReset(s, histogram.NotCounterReset)
+ series2ExpSamplesPreCompact = append(series2ExpSamplesPreCompact, s)
+ // 360 and 480 are block boundaries.
+ if i == 360 || i == 480 {
+ s = copyWithCounterReset(s, histogram.UnknownCounterReset)
+ }
+ series2ExpSamplesPostCompact = append(series2ExpSamplesPostCompact, s)
+ }
+
+ // Sort samples (as OOO samples not added in time-order).
+ sort.Slice(series1ExpSamplesPreCompact, func(i, j int) bool {
+ return series1ExpSamplesPreCompact[i].T() < series1ExpSamplesPreCompact[j].T()
+ })
+ sort.Slice(series1ExpSamplesPostCompact, func(i, j int) bool {
+ return series1ExpSamplesPostCompact[i].T() < series1ExpSamplesPostCompact[j].T()
+ })
+ sort.Slice(series2ExpSamplesPreCompact, func(i, j int) bool {
+ return series2ExpSamplesPreCompact[i].T() < series2ExpSamplesPreCompact[j].T()
+ })
+ sort.Slice(series2ExpSamplesPostCompact, func(i, j int) bool {
+ return series2ExpSamplesPostCompact[i].T() < series2ExpSamplesPostCompact[j].T()
+ })
+
+ verifyDBSamples := func(s1Samples, s2Samples []chunks.Sample) {
+ expRes := map[string][]chunks.Sample{
+ series1.String(): s1Samples,
+ series2.String(): s2Samples,
+ }
+
+ q, err := db.Querier(math.MinInt64, math.MaxInt64)
+ require.NoError(t, err)
+ actRes := query(t, q, labels.MustNewMatcher(labels.MatchRegexp, "foo", "bar.*"))
+ requireEqualSeries(t, expRes, actRes, false)
+ }
+
+ // Verify DB samples before compaction.
+ verifyDBSamples(series1ExpSamplesPreCompact, series2ExpSamplesPreCompact)
+
+ // Verify that the in-memory ooo chunk is not empty.
+ checkNonEmptyOOOChunk := func(lbls labels.Labels) {
+ ms, created, err := db.head.getOrCreate(lbls.Hash(), lbls, false)
+ require.NoError(t, err)
+ require.False(t, created)
+ require.Positive(t, ms.ooo.oooHeadChunk.chunk.NumSamples())
+ }
+
+ checkNonEmptyOOOChunk(series1)
+ checkNonEmptyOOOChunk(series2)
+
+ // No blocks before compaction.
+ require.Empty(t, db.Blocks())
+
+ // There is a 0th WBL file.
+ require.NoError(t, db.head.wbl.Sync()) // syncing to make sure wbl is flushed in windows
+ files, err := os.ReadDir(db.head.wbl.Dir())
+ require.NoError(t, err)
+ require.Len(t, files, 1)
+ require.Equal(t, "00000000", files[0].Name())
+ f, err := files[0].Info()
+ require.NoError(t, err)
+ require.Greater(t, f.Size(), int64(100))
+
+ // OOO compaction happens here.
+ require.NoError(t, db.CompactOOOHead(ctx))
+
+ // Check that blocks are created after compaction.
+ require.Len(t, db.Blocks(), 5)
+
+ // Check samples after compaction.
+ verifyDBSamples(series1ExpSamplesPostCompact, series2ExpSamplesPostCompact)
+
+ // 0th WBL file will be deleted and 1st will be the only present.
+ files, err = os.ReadDir(db.head.wbl.Dir())
+ require.NoError(t, err)
+ require.Len(t, files, 1)
+ require.Equal(t, "00000001", files[0].Name())
+ f, err = files[0].Info()
+ require.NoError(t, err)
+ require.Equal(t, int64(0), f.Size())
+
+ // OOO stuff should not be present in the Head now.
+ checkEmptyOOOChunk(series1)
+ checkEmptyOOOChunk(series2)
+
+ verifyBlockSamples := func(block *Block, fromMins, toMins int64) {
+ var series1Samples, series2Samples []chunks.Sample
+
+ for _, s := range series1ExpSamplesPostCompact {
+ if s.T() >= fromMins*time.Minute.Milliseconds() {
+ // Samples should be sorted, so break out of loop when we reach a timestamp that's too big.
+ if s.T() > toMins*time.Minute.Milliseconds() {
+ break
+ }
+ series1Samples = append(series1Samples, s)
+ }
+ }
+ for _, s := range series2ExpSamplesPostCompact {
+ if s.T() >= fromMins*time.Minute.Milliseconds() {
+ // Samples should be sorted, so break out of loop when we reach a timestamp that's too big.
+ if s.T() > toMins*time.Minute.Milliseconds() {
+ break
+ }
+ series2Samples = append(series2Samples, s)
+ }
+ }
+
+ expRes := map[string][]chunks.Sample{}
+ if len(series1Samples) != 0 {
+ expRes[series1.String()] = series1Samples
+ }
+ if len(series2Samples) != 0 {
+ expRes[series2.String()] = series2Samples
+ }
+
+ q, err := NewBlockQuerier(block, math.MinInt64, math.MaxInt64)
+ require.NoError(t, err)
+
+ actRes := query(t, q, labels.MustNewMatcher(labels.MatchRegexp, "foo", "bar.*"))
+ requireEqualSeries(t, expRes, actRes, false)
+ }
+
+ // Checking for expected data in the blocks.
+ verifyBlockSamples(db.Blocks()[0], 100, 119)
+ verifyBlockSamples(db.Blocks()[1], 120, 239)
+ verifyBlockSamples(db.Blocks()[2], 240, 359)
+ verifyBlockSamples(db.Blocks()[3], 360, 479)
+ verifyBlockSamples(db.Blocks()[4], 480, 509)
+
+ // There should be a single m-map file.
+ mmapDir := mmappedChunksDir(db.head.opts.ChunkDirRoot)
+ files, err = os.ReadDir(mmapDir)
+ require.NoError(t, err)
+ require.Len(t, files, 1)
+
+ // Compact the in-order head and expect another block.
+ // Since this is a forced compaction, this block is not aligned with 2h.
+ err = db.CompactHead(NewRangeHead(db.head, 500*time.Minute.Milliseconds(), 550*time.Minute.Milliseconds()))
+ require.NoError(t, err)
+ require.Len(t, db.Blocks(), 6)
+ verifyBlockSamples(db.Blocks()[5], 520, 520)
+
+ // Blocks created out of normal and OOO head now. But not merged.
+ verifyDBSamples(series1ExpSamplesPostCompact, series2ExpSamplesPostCompact)
+
+ // The compaction also clears out the old m-map files. Including
+ // the file that has ooo chunks.
+ files, err = os.ReadDir(mmapDir)
+ require.NoError(t, err)
+ require.Len(t, files, 1)
+ require.Equal(t, "000001", files[0].Name())
+
+ // This will merge overlapping block.
+ require.NoError(t, db.Compact(ctx))
+
+ require.Len(t, db.Blocks(), 5)
+ verifyBlockSamples(db.Blocks()[0], 100, 119)
+ verifyBlockSamples(db.Blocks()[1], 120, 239)
+ verifyBlockSamples(db.Blocks()[2], 240, 359)
+ verifyBlockSamples(db.Blocks()[3], 360, 479)
+ verifyBlockSamples(db.Blocks()[4], 480, 520) // Merged block.
+
+ // Final state. Blocks from normal and OOO head are merged.
+ verifyDBSamples(series1ExpSamplesPostCompact, series2ExpSamplesPostCompact)
+ }
+}
+
+func TestInterleavedInOrderAndOOOHistogramCompactionWithCounterResets_AppendV2(t *testing.T) {
+ for _, floatHistogram := range []bool{false, true} {
+ ctx := context.Background()
+
+ opts := DefaultOptions()
+ opts.OutOfOrderCapMax = 30
+ opts.OutOfOrderTimeWindow = 500 * time.Minute.Milliseconds()
+
+ db := newTestDB(t, withOpts(opts))
+ db.DisableCompactions() // We want to manually call it.
+
+ series1 := labels.FromStrings("foo", "bar1")
+
+ addSample := func(ts int64, l labels.Labels, val int) sample {
+ app := db.AppenderV2(context.Background())
+ tsMs := ts
+ if floatHistogram {
+ h := tsdbutil.GenerateTestFloatHistogram(int64(val))
+ _, err := app.Append(0, l, 0, tsMs, 0, nil, h, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ return sample{t: tsMs, fh: h.Copy()}
+ }
+
+ h := tsdbutil.GenerateTestHistogram(int64(val))
+ _, err := app.Append(0, l, 0, tsMs, 0, h, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ return sample{t: tsMs, h: h.Copy()}
+ }
+
+ var expSamples []chunks.Sample
+
+ s := addSample(0, series1, 0)
+ expSamples = append(expSamples, s)
+ s = addSample(1, series1, 10)
+ expSamples = append(expSamples, copyWithCounterReset(s, histogram.NotCounterReset))
+ s = addSample(3, series1, 3)
+ expSamples = append(expSamples, copyWithCounterReset(s, histogram.UnknownCounterReset))
+ s = addSample(2, series1, 0)
+ expSamples = append(expSamples, copyWithCounterReset(s, histogram.UnknownCounterReset))
+
+ // Sort samples (as OOO samples not added in time-order).
+ sort.Slice(expSamples, func(i, j int) bool {
+ return expSamples[i].T() < expSamples[j].T()
+ })
+
+ verifyDBSamples := func(s1Samples []chunks.Sample) {
+ t.Helper()
+ expRes := map[string][]chunks.Sample{
+ series1.String(): s1Samples,
+ }
+
+ q, err := db.Querier(math.MinInt64, math.MaxInt64)
+ require.NoError(t, err)
+ actRes := query(t, q, labels.MustNewMatcher(labels.MatchRegexp, "foo", "bar.*"))
+ requireEqualSeries(t, expRes, actRes, false)
+ }
+
+ // Verify DB samples before compaction.
+ verifyDBSamples(expSamples)
+
+ require.NoError(t, db.CompactOOOHead(ctx))
+
+ // Check samples after OOO compaction.
+ verifyDBSamples(expSamples)
+
+ // Checking for expected data in the blocks.
+ // Check that blocks are created after compaction.
+ require.Len(t, db.Blocks(), 1)
+
+ // Compact the in-order head and expect another block.
+ // Since this is a forced compaction, this block is not aligned with 2h.
+ err := db.CompactHead(NewRangeHead(db.head, 0, 3))
+ require.NoError(t, err)
+ require.Len(t, db.Blocks(), 2)
+
+ // Blocks created out of normal and OOO head now. But not merged.
+ verifyDBSamples(expSamples)
+
+ // This will merge overlapping block.
+ require.NoError(t, db.Compact(ctx))
+
+ require.Len(t, db.Blocks(), 1)
+
+ // Final state. Blocks from normal and OOO head are merged.
+ verifyDBSamples(expSamples)
+ }
+}
+
+func TestOOOCompactionFailure_AppendV2(t *testing.T) {
+ t.Parallel()
+ for name, scenario := range sampleTypeScenarios {
+ t.Run(name, func(t *testing.T) {
+ testOOOCompactionFailureAppendV2(t, scenario)
+ })
+ }
+}
+
+func testOOOCompactionFailureAppendV2(t *testing.T, scenario sampleTypeScenario) {
+ ctx := context.Background()
+
+ opts := DefaultOptions()
+ opts.OutOfOrderCapMax = 30
+ opts.OutOfOrderTimeWindow = 300 * time.Minute.Milliseconds()
+ db := newTestDB(t, withOpts(opts))
+ db.DisableCompactions() // We want to manually call it.
+ t.Cleanup(func() {
+ require.NoError(t, db.Close())
+ })
+
+ series1 := labels.FromStrings("foo", "bar1")
+
+ addSample := func(fromMins, toMins int64) {
+ app := db.AppenderV2(context.Background())
+ for m := fromMins; m <= toMins; m++ {
+ ts := m * time.Minute.Milliseconds()
+ _, _, err := scenario.appendFunc(storage.AppenderV2AsLimitedV1(app), series1, ts, ts)
+ require.NoError(t, err)
+ }
+ require.NoError(t, app.Commit())
+ }
+
+ // Add an in-order samples.
+ addSample(250, 350)
+
+ // Add ooo samples that creates multiple chunks.
+ addSample(90, 310)
+
+ // No blocks before compaction.
+ require.Empty(t, db.Blocks())
+
+ // There is a 0th WBL file.
+ verifyFirstWBLFileIs0 := func(count int) {
+ require.NoError(t, db.head.wbl.Sync()) // Syncing to make sure wbl is flushed in windows.
+ files, err := os.ReadDir(db.head.wbl.Dir())
+ require.NoError(t, err)
+ require.Len(t, files, count)
+ require.Equal(t, "00000000", files[0].Name())
+ f, err := files[0].Info()
+ require.NoError(t, err)
+ require.Greater(t, f.Size(), int64(100))
+ }
+ verifyFirstWBLFileIs0(1)
+
+ verifyMmapFiles := func(exp ...string) {
+ mmapDir := mmappedChunksDir(db.head.opts.ChunkDirRoot)
+ files, err := os.ReadDir(mmapDir)
+ require.NoError(t, err)
+ require.Len(t, files, len(exp))
+ for i, f := range files {
+ require.Equal(t, exp[i], f.Name())
+ }
+ }
+
+ verifyMmapFiles("000001")
+
+ // OOO compaction fails 5 times.
+ originalCompactor := db.compactor
+ db.compactor = &mockCompactorFailing{t: t}
+ for range 5 {
+ require.Error(t, db.CompactOOOHead(ctx))
+ }
+ require.Empty(t, db.Blocks())
+
+ // M-map files don't change after failed compaction.
+ verifyMmapFiles("000001")
+
+ // Because of 5 compaction attempts, there are 6 files now.
+ verifyFirstWBLFileIs0(6)
+
+ db.compactor = originalCompactor
+ require.NoError(t, db.CompactOOOHead(ctx))
+ oldBlocks := db.Blocks()
+ require.Len(t, db.Blocks(), 3)
+
+ // Check that the ooo chunks were removed.
+ ms, created, err := db.head.getOrCreate(series1.Hash(), series1, false)
+ require.NoError(t, err)
+ require.False(t, created)
+ require.Nil(t, ms.ooo)
+
+ // The failed compaction should not have left the ooo Head corrupted.
+ // Hence, expect no new blocks with another OOO compaction call.
+ require.NoError(t, db.CompactOOOHead(ctx))
+ require.Len(t, db.Blocks(), 3)
+ require.Equal(t, oldBlocks, db.Blocks())
+
+ // There should be a single m-map file.
+ verifyMmapFiles("000001")
+
+ // All but last WBL file will be deleted.
+ // 8 files in total (starting at 0) because of 7 compaction calls.
+ files, err := os.ReadDir(db.head.wbl.Dir())
+ require.NoError(t, err)
+ require.Len(t, files, 1)
+ require.Equal(t, "00000007", files[0].Name())
+ f, err := files[0].Info()
+ require.NoError(t, err)
+ require.Equal(t, int64(0), f.Size())
+
+ verifySamples := func(block *Block, fromMins, toMins int64) {
+ series1Samples := make([]chunks.Sample, 0, toMins-fromMins+1)
+ for m := fromMins; m <= toMins; m++ {
+ ts := m * time.Minute.Milliseconds()
+ series1Samples = append(series1Samples, scenario.sampleFunc(ts, ts))
+ }
+ expRes := map[string][]chunks.Sample{
+ series1.String(): series1Samples,
+ }
+
+ q, err := NewBlockQuerier(block, math.MinInt64, math.MaxInt64)
+ require.NoError(t, err)
+ actRes := query(t, q, labels.MustNewMatcher(labels.MatchRegexp, "foo", "bar.*"))
+ requireEqualSeries(t, expRes, actRes, true)
+ }
+
+ // Checking for expected data in the blocks.
+ verifySamples(db.Blocks()[0], 90, 119)
+ verifySamples(db.Blocks()[1], 120, 239)
+ verifySamples(db.Blocks()[2], 240, 310)
+
+ // Compact the in-order head and expect another block.
+ // Since this is a forced compaction, this block is not aligned with 2h.
+ err = db.CompactHead(NewRangeHead(db.head, 250*time.Minute.Milliseconds(), 350*time.Minute.Milliseconds()))
+ require.NoError(t, err)
+ require.Len(t, db.Blocks(), 4) // [0, 120), [120, 240), [240, 360), [250, 351)
+ verifySamples(db.Blocks()[3], 250, 350)
+
+ // The compaction also clears out the old m-map files. Including
+ // the file that has ooo chunks.
+ verifyMmapFiles("000001")
+}
+
+func TestWBLCorruption_AppendV2(t *testing.T) {
+ opts := DefaultOptions()
+ opts.OutOfOrderCapMax = 30
+ opts.OutOfOrderTimeWindow = 300 * time.Minute.Milliseconds()
+
+ db := newTestDB(t, withOpts(opts))
+
+ series1 := labels.FromStrings("foo", "bar1")
+ var allSamples, expAfterRestart []chunks.Sample
+ addSamples := func(fromMins, toMins int64, afterRestart bool) {
+ app := db.AppenderV2(context.Background())
+ for m := fromMins; m <= toMins; m++ {
+ ts := m * time.Minute.Milliseconds()
+ _, err := app.Append(0, series1, 0, ts, float64(ts), nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ allSamples = append(allSamples, sample{t: ts, f: float64(ts)})
+ if afterRestart {
+ expAfterRestart = append(expAfterRestart, sample{t: ts, f: float64(ts)})
+ }
+ }
+ require.NoError(t, app.Commit())
+ }
+
+ // Add an in-order samples.
+ addSamples(340, 350, true)
+
+ // OOO samples.
+ addSamples(90, 99, true)
+ addSamples(100, 119, true)
+ addSamples(120, 130, true)
+
+ // Moving onto the second file.
+ _, err := db.head.wbl.NextSegment()
+ require.NoError(t, err)
+
+ // More OOO samples.
+ addSamples(200, 230, true)
+ addSamples(240, 255, true)
+
+ // We corrupt WBL after the sample at 255. So everything added later
+ // should be deleted after replay.
+
+ // Checking where we corrupt it.
+ require.NoError(t, db.head.wbl.Sync()) // Syncing to make sure wbl is flushed in windows.
+ files, err := os.ReadDir(db.head.wbl.Dir())
+ require.NoError(t, err)
+ require.Len(t, files, 2)
+ f1, err := files[1].Info()
+ require.NoError(t, err)
+ corruptIndex := f1.Size()
+ corruptFilePath := path.Join(db.head.wbl.Dir(), files[1].Name())
+
+ // Corrupt the WBL by adding a malformed record.
+ require.NoError(t, db.head.wbl.Log([]byte{byte(record.Samples), 99, 9, 99, 9, 99, 9, 99}))
+
+ // More samples after the corruption point.
+ addSamples(260, 280, false)
+ addSamples(290, 300, false)
+
+ // Another file.
+ _, err = db.head.wbl.NextSegment()
+ require.NoError(t, err)
+
+ addSamples(310, 320, false)
+
+ // Verifying that we have data after corruption point.
+ require.NoError(t, db.head.wbl.Sync()) // Syncing to make sure wbl is flushed in windows.
+ files, err = os.ReadDir(db.head.wbl.Dir())
+ require.NoError(t, err)
+ require.Len(t, files, 3)
+ f1, err = files[1].Info()
+ require.NoError(t, err)
+ require.Greater(t, f1.Size(), corruptIndex)
+ f0, err := files[0].Info()
+ require.NoError(t, err)
+ require.Greater(t, f0.Size(), int64(100))
+ f2, err := files[2].Info()
+ require.NoError(t, err)
+ require.Greater(t, f2.Size(), int64(100))
+
+ verifySamples := func(expSamples []chunks.Sample) {
+ sort.Slice(expSamples, func(i, j int) bool {
+ return expSamples[i].T() < expSamples[j].T()
+ })
+
+ expRes := map[string][]chunks.Sample{
+ series1.String(): expSamples,
+ }
+
+ q, err := db.Querier(math.MinInt64, math.MaxInt64)
+ require.NoError(t, err)
+
+ actRes := query(t, q, labels.MustNewMatcher(labels.MatchRegexp, "foo", "bar.*"))
+ require.Equal(t, expRes, actRes)
+ }
+
+ verifySamples(allSamples)
+
+ require.NoError(t, db.Close())
+
+ // We want everything to be replayed from the WBL. So we delete the m-map files.
+ require.NoError(t, os.RemoveAll(mmappedChunksDir(db.head.opts.ChunkDirRoot)))
+
+ // Restart does the replay and repair.
+ db = newTestDB(t, withDir(db.Dir()), withOpts(opts))
+ require.NoError(t, err)
+ require.Equal(t, 1.0, prom_testutil.ToFloat64(db.head.metrics.walCorruptionsTotal))
+ require.Less(t, len(expAfterRestart), len(allSamples))
+ verifySamples(expAfterRestart)
+
+ // Verify that it did the repair on disk.
+ files, err = os.ReadDir(db.head.wbl.Dir())
+ require.NoError(t, err)
+ require.Len(t, files, 3)
+ f0, err = files[0].Info()
+ require.NoError(t, err)
+ require.Greater(t, f0.Size(), int64(100))
+ f2, err = files[2].Info()
+ require.NoError(t, err)
+ require.Equal(t, int64(0), f2.Size())
+ require.Equal(t, corruptFilePath, path.Join(db.head.wbl.Dir(), files[1].Name()))
+
+ // Verifying that everything after the corruption point is set to 0.
+ b, err := os.ReadFile(corruptFilePath)
+ require.NoError(t, err)
+ sum := 0
+ for _, val := range b[corruptIndex:] {
+ sum += int(val)
+ }
+ require.Equal(t, 0, sum)
+
+ // Another restart, everything normal with no repair.
+ require.NoError(t, db.Close())
+ db = newTestDB(t, withDir(db.Dir()), withOpts(opts))
+ require.NoError(t, err)
+ require.Equal(t, 0.0, prom_testutil.ToFloat64(db.head.metrics.walCorruptionsTotal))
+ verifySamples(expAfterRestart)
+}
+
+func TestOOOMmapCorruption_AppendV2(t *testing.T) {
+ for name, scenario := range sampleTypeScenarios {
+ t.Run(name, func(t *testing.T) {
+ testOOOMmapCorruptionAppendV2(t, scenario)
+ })
+ }
+}
+
+func testOOOMmapCorruptionAppendV2(t *testing.T, scenario sampleTypeScenario) {
+ opts := DefaultOptions()
+ opts.OutOfOrderCapMax = 10
+ opts.OutOfOrderTimeWindow = 300 * time.Minute.Milliseconds()
+
+ db := newTestDB(t, withOpts(opts))
+
+ series1 := labels.FromStrings("foo", "bar1")
+ var allSamples, expInMmapChunks []chunks.Sample
+ addSamples := func(fromMins, toMins int64, inMmapAfterCorruption bool) {
+ app := db.AppenderV2(context.Background())
+ for m := fromMins; m <= toMins; m++ {
+ ts := m * time.Minute.Milliseconds()
+ _, s, err := scenario.appendFunc(storage.AppenderV2AsLimitedV1(app), series1, ts, ts)
+ require.NoError(t, err)
+ allSamples = append(allSamples, s)
+ if inMmapAfterCorruption {
+ expInMmapChunks = append(expInMmapChunks, s)
+ }
+ }
+ require.NoError(t, app.Commit())
+ }
+
+ // Add an in-order samples.
+ addSamples(340, 350, true)
+
+ // OOO samples.
+ addSamples(90, 99, true)
+ addSamples(100, 109, true)
+ // This sample m-maps a chunk. But 120 goes into a new chunk.
+ addSamples(120, 120, false)
+
+ // Second m-map file. We will corrupt this file. Sample 120 goes into this new file.
+ db.head.chunkDiskMapper.CutNewFile()
+
+ // More OOO samples.
+ addSamples(200, 230, false)
+ addSamples(240, 255, false)
+
+ db.head.chunkDiskMapper.CutNewFile()
+ addSamples(260, 290, false)
+
+ verifySamples := func(expSamples []chunks.Sample) {
+ sort.Slice(expSamples, func(i, j int) bool {
+ return expSamples[i].T() < expSamples[j].T()
+ })
+
+ expRes := map[string][]chunks.Sample{
+ series1.String(): expSamples,
+ }
+
+ q, err := db.Querier(math.MinInt64, math.MaxInt64)
+ require.NoError(t, err)
+
+ actRes := query(t, q, labels.MustNewMatcher(labels.MatchRegexp, "foo", "bar.*"))
+ requireEqualSeries(t, expRes, actRes, true)
+ }
+
+ verifySamples(allSamples)
+
+ // Verifying existing files.
+ mmapDir := mmappedChunksDir(db.head.opts.ChunkDirRoot)
+ files, err := os.ReadDir(mmapDir)
+ require.NoError(t, err)
+ require.Len(t, files, 3)
+
+ // Corrupting the 2nd file.
+ f, err := os.OpenFile(path.Join(mmapDir, files[1].Name()), os.O_RDWR, 0o666)
+ require.NoError(t, err)
+ _, err = f.WriteAt([]byte{99, 9, 99, 9, 99}, 20)
+ require.NoError(t, err)
+ require.NoError(t, f.Close())
+ firstFileName := files[0].Name()
+
+ require.NoError(t, db.Close())
+
+ // Moving OOO WBL to use it later.
+ wblDir := db.head.wbl.Dir()
+ wblDirTmp := path.Join(t.TempDir(), "wbl_tmp")
+ require.NoError(t, os.Rename(wblDir, wblDirTmp))
+
+ // Restart does the replay and repair of m-map files.
+ db = newTestDB(t, withDir(db.Dir()), withOpts(opts))
+ require.NoError(t, err)
+ require.Equal(t, 1.0, prom_testutil.ToFloat64(db.head.metrics.mmapChunkCorruptionTotal))
+ require.Less(t, len(expInMmapChunks), len(allSamples))
+
+ // Since there is no WBL, only samples from m-map chunks comes in the query.
+ verifySamples(expInMmapChunks)
+
+ // Verify that it did the repair on disk. All files from the point of corruption
+ // should be deleted.
+ files, err = os.ReadDir(mmapDir)
+ require.NoError(t, err)
+ require.Len(t, files, 1)
+ f0, err := files[0].Info()
+ require.NoError(t, err)
+ require.Greater(t, f0.Size(), int64(100))
+ require.Equal(t, firstFileName, files[0].Name())
+
+ // Another restart, everything normal with no repair.
+ require.NoError(t, db.Close())
+ db = newTestDB(t, withDir(db.Dir()), withOpts(opts))
+ require.NoError(t, err)
+ require.Equal(t, 0.0, prom_testutil.ToFloat64(db.head.metrics.mmapChunkCorruptionTotal))
+ verifySamples(expInMmapChunks)
+
+ // Restart again with the WBL, all samples should be present now.
+ require.NoError(t, db.Close())
+ require.NoError(t, os.RemoveAll(wblDir))
+ require.NoError(t, os.Rename(wblDirTmp, wblDir))
+ db = newTestDB(t, withDir(db.Dir()), withOpts(opts))
+ require.NoError(t, err)
+ verifySamples(allSamples)
+}
+
+func TestOutOfOrderRuntimeConfig_AppendV2(t *testing.T) {
+ t.Parallel()
+ for name, scenario := range sampleTypeScenarios {
+ t.Run(name, func(t *testing.T) {
+ testOutOfOrderRuntimeConfigAppendV2(t, scenario)
+ })
+ }
+}
+
+func testOutOfOrderRuntimeConfigAppendV2(t *testing.T, scenario sampleTypeScenario) {
+ ctx := context.Background()
+
+ getDB := func(oooTimeWindow int64) *DB {
+ opts := DefaultOptions()
+ opts.OutOfOrderTimeWindow = oooTimeWindow
+ db := newTestDB(t, withOpts(opts))
+ db.DisableCompactions()
+ return db
+ }
+
+ makeConfig := func(oooTimeWindow int) *config.Config {
+ return &config.Config{
+ StorageConfig: config.StorageConfig{
+ TSDBConfig: &config.TSDBConfig{
+ OutOfOrderTimeWindow: int64(oooTimeWindow) * time.Minute.Milliseconds(),
+ },
+ },
+ }
+ }
+
+ series1 := labels.FromStrings("foo", "bar1")
+ addSamples := func(t *testing.T, db *DB, fromMins, toMins int64, success bool, allSamples []chunks.Sample) []chunks.Sample {
+ app := db.AppenderV2(context.Background())
+ for m := fromMins; m <= toMins; m++ {
+ ts := m * time.Minute.Milliseconds()
+ _, s, err := scenario.appendFunc(storage.AppenderV2AsLimitedV1(app), series1, ts, ts)
+ if success {
+ require.NoError(t, err)
+ allSamples = append(allSamples, s)
+ } else {
+ require.Error(t, err)
+ }
+ }
+ require.NoError(t, app.Commit())
+ return allSamples
+ }
+
+ verifySamples := func(t *testing.T, db *DB, expSamples []chunks.Sample) {
+ sort.Slice(expSamples, func(i, j int) bool {
+ return expSamples[i].T() < expSamples[j].T()
+ })
+
+ expRes := map[string][]chunks.Sample{
+ series1.String(): expSamples,
+ }
+
+ q, err := db.Querier(math.MinInt64, math.MaxInt64)
+ require.NoError(t, err)
+
+ actRes := query(t, q, labels.MustNewMatcher(labels.MatchRegexp, "foo", "bar.*"))
+ requireEqualSeries(t, expRes, actRes, true)
+ }
+
+ doOOOCompaction := func(t *testing.T, db *DB) {
+ // WBL is not empty.
+ size, err := db.head.wbl.Size()
+ require.NoError(t, err)
+ require.Positive(t, size)
+
+ require.Empty(t, db.Blocks())
+ require.NoError(t, db.compactOOOHead(ctx))
+ require.NotEmpty(t, db.Blocks())
+
+ // WBL is empty.
+ size, err = db.head.wbl.Size()
+ require.NoError(t, err)
+ require.Equal(t, int64(0), size)
+ }
+
+ t.Run("increase time window", func(t *testing.T) {
+ var allSamples []chunks.Sample
+ db := getDB(30 * time.Minute.Milliseconds())
+
+ // In-order.
+ allSamples = addSamples(t, db, 300, 310, true, allSamples)
+
+ // OOO upto 30m old is success.
+ allSamples = addSamples(t, db, 281, 290, true, allSamples)
+
+ // OOO of 59m old fails.
+ s := addSamples(t, db, 251, 260, false, nil)
+ require.Empty(t, s)
+ verifySamples(t, db, allSamples)
+
+ oldWblPtr := fmt.Sprintf("%p", db.head.wbl)
+
+ // Increase time window and try adding again.
+ err := db.ApplyConfig(makeConfig(60))
+ require.NoError(t, err)
+ allSamples = addSamples(t, db, 251, 260, true, allSamples)
+
+ // WBL does not change.
+ newWblPtr := fmt.Sprintf("%p", db.head.wbl)
+ require.Equal(t, oldWblPtr, newWblPtr)
+
+ doOOOCompaction(t, db)
+ verifySamples(t, db, allSamples)
+ })
+
+ t.Run("decrease time window and increase again", func(t *testing.T) {
+ var allSamples []chunks.Sample
+ db := getDB(60 * time.Minute.Milliseconds())
+
+ // In-order.
+ allSamples = addSamples(t, db, 300, 310, true, allSamples)
+
+ // OOO upto 59m old is success.
+ allSamples = addSamples(t, db, 251, 260, true, allSamples)
+
+ oldWblPtr := fmt.Sprintf("%p", db.head.wbl)
+ // Decrease time window.
+ err := db.ApplyConfig(makeConfig(30))
+ require.NoError(t, err)
+
+ // OOO of 49m old fails.
+ s := addSamples(t, db, 261, 270, false, nil)
+ require.Empty(t, s)
+
+ // WBL does not change.
+ newWblPtr := fmt.Sprintf("%p", db.head.wbl)
+ require.Equal(t, oldWblPtr, newWblPtr)
+
+ verifySamples(t, db, allSamples)
+
+ // Increase time window again and check
+ err = db.ApplyConfig(makeConfig(60))
+ require.NoError(t, err)
+ allSamples = addSamples(t, db, 261, 270, true, allSamples)
+ verifySamples(t, db, allSamples)
+
+ // WBL does not change.
+ newWblPtr = fmt.Sprintf("%p", db.head.wbl)
+ require.Equal(t, oldWblPtr, newWblPtr)
+
+ doOOOCompaction(t, db)
+ verifySamples(t, db, allSamples)
+ })
+
+ t.Run("disabled to enabled", func(t *testing.T) {
+ var allSamples []chunks.Sample
+ db := getDB(0)
+
+ // In-order.
+ allSamples = addSamples(t, db, 300, 310, true, allSamples)
+
+ // OOO fails.
+ s := addSamples(t, db, 251, 260, false, nil)
+ require.Empty(t, s)
+ verifySamples(t, db, allSamples)
+
+ require.Nil(t, db.head.wbl)
+
+ // Increase time window and try adding again.
+ err := db.ApplyConfig(makeConfig(60))
+ require.NoError(t, err)
+ allSamples = addSamples(t, db, 251, 260, true, allSamples)
+
+ // WBL gets created.
+ require.NotNil(t, db.head.wbl)
+
+ verifySamples(t, db, allSamples)
+
+ // OOO compaction works now.
+ doOOOCompaction(t, db)
+ verifySamples(t, db, allSamples)
+ })
+
+ t.Run("enabled to disabled", func(t *testing.T) {
+ var allSamples []chunks.Sample
+ db := getDB(60 * time.Minute.Milliseconds())
+
+ // In-order.
+ allSamples = addSamples(t, db, 300, 310, true, allSamples)
+
+ // OOO upto 59m old is success.
+ allSamples = addSamples(t, db, 251, 260, true, allSamples)
+
+ oldWblPtr := fmt.Sprintf("%p", db.head.wbl)
+ // Time Window to 0, hence disabled.
+ err := db.ApplyConfig(makeConfig(0))
+ require.NoError(t, err)
+
+ // OOO within old time window fails.
+ s := addSamples(t, db, 290, 309, false, nil)
+ require.Empty(t, s)
+
+ // WBL does not change and is not removed.
+ newWblPtr := fmt.Sprintf("%p", db.head.wbl)
+ require.Equal(t, oldWblPtr, newWblPtr)
+
+ verifySamples(t, db, allSamples)
+
+ // Compaction still works after disabling with WBL cleanup.
+ doOOOCompaction(t, db)
+ verifySamples(t, db, allSamples)
+ })
+
+ t.Run("disabled to disabled", func(t *testing.T) {
+ var allSamples []chunks.Sample
+ db := getDB(0)
+
+ // In-order.
+ allSamples = addSamples(t, db, 300, 310, true, allSamples)
+
+ // OOO fails.
+ s := addSamples(t, db, 290, 309, false, nil)
+ require.Empty(t, s)
+ verifySamples(t, db, allSamples)
+ require.Nil(t, db.head.wbl)
+
+ // Time window to 0.
+ err := db.ApplyConfig(makeConfig(0))
+ require.NoError(t, err)
+
+ // OOO still fails.
+ s = addSamples(t, db, 290, 309, false, nil)
+ require.Empty(t, s)
+ verifySamples(t, db, allSamples)
+ require.Nil(t, db.head.wbl)
+ })
+}
+
+func TestNoGapAfterRestartWithOOO_AppendV2(t *testing.T) {
+ t.Parallel()
+ for name, scenario := range sampleTypeScenarios {
+ t.Run(name, func(t *testing.T) {
+ testNoGapAfterRestartWithOOOAppendV2(t, scenario)
+ })
+ }
+}
+
+func testNoGapAfterRestartWithOOOAppendV2(t *testing.T, scenario sampleTypeScenario) {
+ series1 := labels.FromStrings("foo", "bar1")
+ addSamples := func(t *testing.T, db *DB, fromMins, toMins int64, success bool) {
+ app := db.AppenderV2(context.Background())
+ for m := fromMins; m <= toMins; m++ {
+ ts := m * time.Minute.Milliseconds()
+ _, _, err := scenario.appendFunc(storage.AppenderV2AsLimitedV1(app), series1, ts, ts)
+ if success {
+ require.NoError(t, err)
+ } else {
+ require.Error(t, err)
+ }
+ }
+ require.NoError(t, app.Commit())
+ }
+
+ verifySamples := func(t *testing.T, db *DB, fromMins, toMins int64) {
+ var expSamples []chunks.Sample
+ for m := fromMins; m <= toMins; m++ {
+ ts := m * time.Minute.Milliseconds()
+ expSamples = append(expSamples, scenario.sampleFunc(ts, ts))
+ }
+
+ expRes := map[string][]chunks.Sample{
+ series1.String(): expSamples,
+ }
+
+ q, err := db.Querier(math.MinInt64, math.MaxInt64)
+ require.NoError(t, err)
+
+ actRes := query(t, q, labels.MustNewMatcher(labels.MatchRegexp, "foo", "bar.*"))
+ requireEqualSeries(t, expRes, actRes, true)
+ }
+
+ cases := []struct {
+ inOrderMint, inOrderMaxt int64
+ oooMint, oooMaxt int64
+ // After compaction.
+ blockRanges [][2]int64
+ headMint, headMaxt int64
+ }{
+ {
+ 300, 490,
+ 489, 489,
+ [][2]int64{{300, 360}, {480, 600}},
+ 360, 490,
+ },
+ {
+ 300, 490,
+ 479, 479,
+ [][2]int64{{300, 360}, {360, 480}},
+ 360, 490,
+ },
+ }
+
+ for i, c := range cases {
+ t.Run(fmt.Sprintf("case=%d", i), func(t *testing.T) {
+ ctx := context.Background()
+
+ opts := DefaultOptions()
+ opts.OutOfOrderTimeWindow = 30 * time.Minute.Milliseconds()
+ db := newTestDB(t, withOpts(opts))
+ db.DisableCompactions()
+
+ // 3h10m=190m worth in-order data.
+ addSamples(t, db, c.inOrderMint, c.inOrderMaxt, true)
+ verifySamples(t, db, c.inOrderMint, c.inOrderMaxt)
+
+ // One ooo samples.
+ addSamples(t, db, c.oooMint, c.oooMaxt, true)
+ verifySamples(t, db, c.inOrderMint, c.inOrderMaxt)
+
+ // We get 2 blocks. 1 from OOO, 1 from in-order.
+ require.NoError(t, db.Compact(ctx))
+ verifyBlockRanges := func() {
+ blocks := db.Blocks()
+ require.Len(t, blocks, len(c.blockRanges))
+ for j, br := range c.blockRanges {
+ require.Equal(t, br[0]*time.Minute.Milliseconds(), blocks[j].MinTime())
+ require.Equal(t, br[1]*time.Minute.Milliseconds(), blocks[j].MaxTime())
+ }
+ }
+ verifyBlockRanges()
+ require.Equal(t, c.headMint*time.Minute.Milliseconds(), db.head.MinTime())
+ require.Equal(t, c.headMaxt*time.Minute.Milliseconds(), db.head.MaxTime())
+
+ // Restart and expect all samples to be present.
+ require.NoError(t, db.Close())
+
+ db = newTestDB(t, withDir(db.Dir()), withOpts(opts))
+ db.DisableCompactions()
+
+ verifyBlockRanges()
+ require.Equal(t, c.headMint*time.Minute.Milliseconds(), db.head.MinTime())
+ require.Equal(t, c.headMaxt*time.Minute.Milliseconds(), db.head.MaxTime())
+ verifySamples(t, db, c.inOrderMint, c.inOrderMaxt)
+ })
+ }
+}
+
+func TestWblReplayAfterOOODisableAndRestart_AppendV2(t *testing.T) {
+ for name, scenario := range sampleTypeScenarios {
+ t.Run(name, func(t *testing.T) {
+ testWblReplayAfterOOODisableAndRestartAppendV2(t, scenario)
+ })
+ }
+}
+
+func testWblReplayAfterOOODisableAndRestartAppendV2(t *testing.T, scenario sampleTypeScenario) {
+ opts := DefaultOptions()
+ opts.OutOfOrderTimeWindow = 60 * time.Minute.Milliseconds()
+
+ db := newTestDB(t, withOpts(opts))
+
+ series1 := labels.FromStrings("foo", "bar1")
+ var allSamples []chunks.Sample
+ addSamples := func(fromMins, toMins int64) {
+ app := db.AppenderV2(context.Background())
+ for m := fromMins; m <= toMins; m++ {
+ ts := m * time.Minute.Milliseconds()
+ _, s, err := scenario.appendFunc(storage.AppenderV2AsLimitedV1(app), series1, ts, ts)
+ require.NoError(t, err)
+ allSamples = append(allSamples, s)
+ }
+ require.NoError(t, app.Commit())
+ }
+
+ // In-order samples.
+ addSamples(290, 300)
+ // OOO samples.
+ addSamples(250, 260)
+
+ verifySamples := func(expSamples []chunks.Sample) {
+ sort.Slice(expSamples, func(i, j int) bool {
+ return expSamples[i].T() < expSamples[j].T()
+ })
+
+ expRes := map[string][]chunks.Sample{
+ series1.String(): expSamples,
+ }
+
+ q, err := db.Querier(math.MinInt64, math.MaxInt64)
+ require.NoError(t, err)
+
+ actRes := query(t, q, labels.MustNewMatcher(labels.MatchRegexp, "foo", "bar.*"))
+ requireEqualSeries(t, expRes, actRes, true)
+ }
+
+ verifySamples(allSamples)
+
+ // Restart DB with OOO disabled.
+ require.NoError(t, db.Close())
+
+ opts.OutOfOrderTimeWindow = 0
+ db = newTestDB(t, withDir(db.Dir()), withOpts(opts))
+
+ // We can still query OOO samples when OOO is disabled.
+ verifySamples(allSamples)
+}
+
+func TestPanicOnApplyConfig_AppendV2(t *testing.T) {
+ for name, scenario := range sampleTypeScenarios {
+ t.Run(name, func(t *testing.T) {
+ testPanicOnApplyConfigAppendV2(t, scenario)
+ })
+ }
+}
+
+func testPanicOnApplyConfigAppendV2(t *testing.T, scenario sampleTypeScenario) {
+ opts := DefaultOptions()
+ opts.OutOfOrderTimeWindow = 60 * time.Minute.Milliseconds()
+
+ db := newTestDB(t, withOpts(opts))
+
+ series1 := labels.FromStrings("foo", "bar1")
+ var allSamples []chunks.Sample
+ addSamples := func(fromMins, toMins int64) {
+ app := db.AppenderV2(context.Background())
+ for m := fromMins; m <= toMins; m++ {
+ ts := m * time.Minute.Milliseconds()
+ _, s, err := scenario.appendFunc(storage.AppenderV2AsLimitedV1(app), series1, ts, ts)
+ require.NoError(t, err)
+ allSamples = append(allSamples, s)
+ }
+ require.NoError(t, app.Commit())
+ }
+
+ // In-order samples.
+ addSamples(290, 300)
+ // OOO samples.
+ addSamples(250, 260)
+
+ // Restart DB with OOO disabled.
+ require.NoError(t, db.Close())
+
+ opts.OutOfOrderTimeWindow = 0
+ db = newTestDB(t, withDir(db.Dir()), withOpts(opts))
+
+ // ApplyConfig with OOO enabled and expect no panic.
+ err := db.ApplyConfig(&config.Config{
+ StorageConfig: config.StorageConfig{
+ TSDBConfig: &config.TSDBConfig{
+ OutOfOrderTimeWindow: 60 * time.Minute.Milliseconds(),
+ },
+ },
+ })
+ require.NoError(t, err)
+}
+
+func TestHistogramAppendAndQuery_AppendV2(t *testing.T) {
+ t.Run("integer histograms", func(t *testing.T) {
+ testHistogramAppendAndQueryHelperAppendV2(t, false)
+ })
+ t.Run("float histograms", func(t *testing.T) {
+ testHistogramAppendAndQueryHelperAppendV2(t, true)
+ })
+}
+
+func testHistogramAppendAndQueryHelperAppendV2(t *testing.T, floatHistogram bool) {
+ t.Helper()
+ db := newTestDB(t)
+ minute := func(m int) int64 { return int64(m) * time.Minute.Milliseconds() }
+ t.Cleanup(func() {
+ require.NoError(t, db.Close())
+ })
+
+ ctx := context.Background()
+ appendHistogram := func(t *testing.T,
+ lbls labels.Labels, tsMinute int, h *histogram.Histogram,
+ exp *[]chunks.Sample, expCRH histogram.CounterResetHint,
+ ) {
+ t.Helper()
+ var err error
+ app := db.AppenderV2(ctx)
+ if floatHistogram {
+ _, err = app.Append(0, lbls, 0, minute(tsMinute), 0, nil, h.ToFloat(nil), storage.AOptions{})
+ efh := h.ToFloat(nil)
+ efh.CounterResetHint = expCRH
+ *exp = append(*exp, sample{t: minute(tsMinute), fh: efh})
+ } else {
+ _, err = app.Append(0, lbls, 0, minute(tsMinute), 0, h.Copy(), nil, storage.AOptions{})
+ eh := h.Copy()
+ eh.CounterResetHint = expCRH
+ *exp = append(*exp, sample{t: minute(tsMinute), h: eh})
+ }
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ }
+ appendFloat := func(t *testing.T, lbls labels.Labels, tsMinute int, val float64, exp *[]chunks.Sample) {
+ t.Helper()
+ app := db.AppenderV2(ctx)
+ _, err := app.Append(0, lbls, 0, minute(tsMinute), val, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ *exp = append(*exp, sample{t: minute(tsMinute), f: val})
+ }
+
+ testQuery := func(t *testing.T, name, value string, exp map[string][]chunks.Sample) {
+ t.Helper()
+ q, err := db.Querier(math.MinInt64, math.MaxInt64)
+ require.NoError(t, err)
+ act := query(t, q, labels.MustNewMatcher(labels.MatchRegexp, name, value))
+ require.Equal(t, exp, act)
+ }
+
+ baseH := &histogram.Histogram{
+ Count: 15,
+ ZeroCount: 4,
+ ZeroThreshold: 0.001,
+ Sum: 35.5,
+ Schema: 1,
+ PositiveSpans: []histogram.Span{
+ {Offset: 0, Length: 2},
+ {Offset: 2, Length: 2},
+ },
+ PositiveBuckets: []int64{1, 1, -1, 0},
+ NegativeSpans: []histogram.Span{
+ {Offset: 0, Length: 1},
+ {Offset: 1, Length: 2},
+ },
+ NegativeBuckets: []int64{1, 2, -1},
+ }
+
+ var (
+ series1 = labels.FromStrings("foo", "bar1")
+ series2 = labels.FromStrings("foo", "bar2")
+ series3 = labels.FromStrings("foo", "bar3")
+ series4 = labels.FromStrings("foo", "bar4")
+ exp1, exp2, exp3, exp4 []chunks.Sample
+ )
+
+ // TODO(codesome): test everything for negative buckets as well.
+ t.Run("series with only histograms", func(t *testing.T) {
+ h := baseH.Copy() // This is shared across all sub tests.
+
+ appendHistogram(t, series1, 100, h, &exp1, histogram.UnknownCounterReset)
+ testQuery(t, "foo", "bar1", map[string][]chunks.Sample{series1.String(): exp1})
+
+ h.PositiveBuckets[0]++
+ h.NegativeBuckets[0] += 2
+ h.Count += 10
+ appendHistogram(t, series1, 101, h, &exp1, histogram.NotCounterReset)
+ testQuery(t, "foo", "bar1", map[string][]chunks.Sample{series1.String(): exp1})
+
+ t.Run("changing schema", func(t *testing.T) {
+ h.Schema = 2
+ appendHistogram(t, series1, 102, h, &exp1, histogram.UnknownCounterReset)
+ testQuery(t, "foo", "bar1", map[string][]chunks.Sample{series1.String(): exp1})
+
+ // Schema back to old.
+ h.Schema = 1
+ appendHistogram(t, series1, 103, h, &exp1, histogram.UnknownCounterReset)
+ testQuery(t, "foo", "bar1", map[string][]chunks.Sample{series1.String(): exp1})
+ })
+
+ t.Run("new buckets incoming", func(t *testing.T) {
+ // In the previous unit test, during the last histogram append, we
+ // changed the schema and that caused a new chunk creation. Because
+ // of the next append the layout of the last histogram will change
+ // because the chunk will be re-encoded. So this forces us to modify
+ // the last histogram in exp1 so when we query we get the expected
+ // results.
+ if floatHistogram {
+ lh := exp1[len(exp1)-1].FH().Copy()
+ lh.PositiveSpans[1].Length++
+ lh.PositiveBuckets = append(lh.PositiveBuckets, 0)
+ exp1[len(exp1)-1] = sample{t: exp1[len(exp1)-1].T(), fh: lh}
+ } else {
+ lh := exp1[len(exp1)-1].H().Copy()
+ lh.PositiveSpans[1].Length++
+ lh.PositiveBuckets = append(lh.PositiveBuckets, -2) // -2 makes the last bucket 0.
+ exp1[len(exp1)-1] = sample{t: exp1[len(exp1)-1].T(), h: lh}
+ }
+
+ // This histogram with new bucket at the end causes the re-encoding of the previous histogram.
+ // Hence the previous histogram is recoded into this new layout.
+ // But the query returns the histogram from the in-memory buffer, hence we don't see the recode here yet.
+ h.PositiveSpans[1].Length++
+ h.PositiveBuckets = append(h.PositiveBuckets, 1)
+ h.Count += 3
+ appendHistogram(t, series1, 104, h, &exp1, histogram.NotCounterReset)
+ testQuery(t, "foo", "bar1", map[string][]chunks.Sample{series1.String(): exp1})
+
+ // Because of the previous two histograms being on the active chunk,
+ // and the next append is only adding a new bucket, the active chunk
+ // will be re-encoded to the new layout.
+ if floatHistogram {
+ lh := exp1[len(exp1)-2].FH().Copy()
+ lh.PositiveSpans[0].Length++
+ lh.PositiveSpans[1].Offset--
+ lh.PositiveBuckets = []float64{2, 3, 0, 2, 2, 0}
+ exp1[len(exp1)-2] = sample{t: exp1[len(exp1)-2].T(), fh: lh}
+
+ lh = exp1[len(exp1)-1].FH().Copy()
+ lh.PositiveSpans[0].Length++
+ lh.PositiveSpans[1].Offset--
+ lh.PositiveBuckets = []float64{2, 3, 0, 2, 2, 3}
+ exp1[len(exp1)-1] = sample{t: exp1[len(exp1)-1].T(), fh: lh}
+ } else {
+ lh := exp1[len(exp1)-2].H().Copy()
+ lh.PositiveSpans[0].Length++
+ lh.PositiveSpans[1].Offset--
+ lh.PositiveBuckets = []int64{2, 1, -3, 2, 0, -2}
+ exp1[len(exp1)-2] = sample{t: exp1[len(exp1)-2].T(), h: lh}
+
+ lh = exp1[len(exp1)-1].H().Copy()
+ lh.PositiveSpans[0].Length++
+ lh.PositiveSpans[1].Offset--
+ lh.PositiveBuckets = []int64{2, 1, -3, 2, 0, 1}
+ exp1[len(exp1)-1] = sample{t: exp1[len(exp1)-1].T(), h: lh}
+ }
+
+ // Now we add the new buckets in between. Empty bucket is again not present for the old histogram.
+ h.PositiveSpans[0].Length++
+ h.PositiveSpans[1].Offset--
+ h.Count += 3
+ // {2, 1, -1, 0, 1} -> {2, 1, 0, -1, 0, 1}
+ h.PositiveBuckets = append(h.PositiveBuckets[:2], append([]int64{0}, h.PositiveBuckets[2:]...)...)
+ appendHistogram(t, series1, 105, h, &exp1, histogram.NotCounterReset)
+ testQuery(t, "foo", "bar1", map[string][]chunks.Sample{series1.String(): exp1})
+
+ // We add 4 more histograms to clear out the buffer and see the re-encoded histograms.
+ appendHistogram(t, series1, 106, h, &exp1, histogram.NotCounterReset)
+ appendHistogram(t, series1, 107, h, &exp1, histogram.NotCounterReset)
+ appendHistogram(t, series1, 108, h, &exp1, histogram.NotCounterReset)
+ appendHistogram(t, series1, 109, h, &exp1, histogram.NotCounterReset)
+
+ // Update the expected histograms to reflect the re-encoding.
+ if floatHistogram {
+ l := len(exp1)
+ h7 := exp1[l-7].FH()
+ h7.PositiveSpans = exp1[l-1].FH().PositiveSpans
+ h7.PositiveBuckets = []float64{2, 3, 0, 2, 2, 0}
+ exp1[l-7] = sample{t: exp1[l-7].T(), fh: h7}
+
+ h6 := exp1[l-6].FH()
+ h6.PositiveSpans = exp1[l-1].FH().PositiveSpans
+ h6.PositiveBuckets = []float64{2, 3, 0, 2, 2, 3}
+ exp1[l-6] = sample{t: exp1[l-6].T(), fh: h6}
+ } else {
+ l := len(exp1)
+ h7 := exp1[l-7].H()
+ h7.PositiveSpans = exp1[l-1].H().PositiveSpans
+ h7.PositiveBuckets = []int64{2, 1, -3, 2, 0, -2} // -3 and -2 are the empty buckets.
+ exp1[l-7] = sample{t: exp1[l-7].T(), h: h7}
+
+ h6 := exp1[l-6].H()
+ h6.PositiveSpans = exp1[l-1].H().PositiveSpans
+ h6.PositiveBuckets = []int64{2, 1, -3, 2, 0, 1} // -3 is the empty bucket.
+ exp1[l-6] = sample{t: exp1[l-6].T(), h: h6}
+ }
+
+ testQuery(t, "foo", "bar1", map[string][]chunks.Sample{series1.String(): exp1})
+ })
+
+ t.Run("buckets disappearing", func(t *testing.T) {
+ h.PositiveSpans[1].Length--
+ h.PositiveBuckets = h.PositiveBuckets[:len(h.PositiveBuckets)-1]
+ h.Count -= 3
+ appendHistogram(t, series1, 110, h, &exp1, histogram.UnknownCounterReset)
+ testQuery(t, "foo", "bar1", map[string][]chunks.Sample{series1.String(): exp1})
+ })
+ })
+
+ t.Run("series starting with float and then getting histograms", func(t *testing.T) {
+ appendFloat(t, series2, 100, 100, &exp2)
+ appendFloat(t, series2, 101, 101, &exp2)
+ appendFloat(t, series2, 102, 102, &exp2)
+ testQuery(t, "foo", "bar2", map[string][]chunks.Sample{series2.String(): exp2})
+
+ h := baseH.Copy()
+ appendHistogram(t, series2, 103, h, &exp2, histogram.UnknownCounterReset)
+ appendHistogram(t, series2, 104, h, &exp2, histogram.NotCounterReset)
+ appendHistogram(t, series2, 105, h, &exp2, histogram.NotCounterReset)
+ testQuery(t, "foo", "bar2", map[string][]chunks.Sample{series2.String(): exp2})
+
+ // Switching between float and histograms again.
+ appendFloat(t, series2, 106, 106, &exp2)
+ appendFloat(t, series2, 107, 107, &exp2)
+ testQuery(t, "foo", "bar2", map[string][]chunks.Sample{series2.String(): exp2})
+
+ appendHistogram(t, series2, 108, h, &exp2, histogram.UnknownCounterReset)
+ appendHistogram(t, series2, 109, h, &exp2, histogram.NotCounterReset)
+ testQuery(t, "foo", "bar2", map[string][]chunks.Sample{series2.String(): exp2})
+ })
+
+ t.Run("series starting with histogram and then getting float", func(t *testing.T) {
+ h := baseH.Copy()
+ appendHistogram(t, series3, 101, h, &exp3, histogram.UnknownCounterReset)
+ appendHistogram(t, series3, 102, h, &exp3, histogram.NotCounterReset)
+ appendHistogram(t, series3, 103, h, &exp3, histogram.NotCounterReset)
+ testQuery(t, "foo", "bar3", map[string][]chunks.Sample{series3.String(): exp3})
+
+ appendFloat(t, series3, 104, 100, &exp3)
+ appendFloat(t, series3, 105, 101, &exp3)
+ appendFloat(t, series3, 106, 102, &exp3)
+ testQuery(t, "foo", "bar3", map[string][]chunks.Sample{series3.String(): exp3})
+
+ // Switching between histogram and float again.
+ appendHistogram(t, series3, 107, h, &exp3, histogram.UnknownCounterReset)
+ appendHistogram(t, series3, 108, h, &exp3, histogram.NotCounterReset)
+ testQuery(t, "foo", "bar3", map[string][]chunks.Sample{series3.String(): exp3})
+
+ appendFloat(t, series3, 109, 106, &exp3)
+ appendFloat(t, series3, 110, 107, &exp3)
+ testQuery(t, "foo", "bar3", map[string][]chunks.Sample{series3.String(): exp3})
+ })
+
+ t.Run("query mix of histogram and float series", func(t *testing.T) {
+ // A float only series.
+ appendFloat(t, series4, 100, 100, &exp4)
+ appendFloat(t, series4, 101, 101, &exp4)
+ appendFloat(t, series4, 102, 102, &exp4)
+
+ testQuery(t, "foo", "bar.*", map[string][]chunks.Sample{
+ series1.String(): exp1,
+ series2.String(): exp2,
+ series3.String(): exp3,
+ series4.String(): exp4,
+ })
+ })
+}
+
+func TestOOONativeHistogramsSettings_AppendV2(t *testing.T) {
+ h := &histogram.Histogram{
+ Count: 9,
+ ZeroCount: 4,
+ ZeroThreshold: 0.001,
+ Sum: 35.5,
+ Schema: 1,
+ PositiveSpans: []histogram.Span{
+ {Offset: 0, Length: 2},
+ {Offset: 2, Length: 2},
+ },
+ PositiveBuckets: []int64{1, 1, -1, 0},
+ }
+
+ l := labels.FromStrings("foo", "bar")
+
+ t.Run("Test OOO native histograms if OOO is disabled and Native Histograms is enabled", func(t *testing.T) {
+ opts := DefaultOptions()
+ opts.OutOfOrderTimeWindow = 0
+ db := newTestDB(t, withOpts(opts), withRngs(100))
+
+ app := db.AppenderV2(context.Background())
+ _, err := app.Append(0, l, 0, 100, 0, h, nil, storage.AOptions{})
+ require.NoError(t, err)
+
+ _, err = app.Append(0, l, 0, 50, 0, h, nil, storage.AOptions{})
+ require.NoError(t, err) // The OOO sample is not detected until it is committed, so no error is returned
+
+ require.NoError(t, app.Commit())
+
+ q, err := db.Querier(math.MinInt, math.MaxInt64)
+ require.NoError(t, err)
+ act := query(t, q, labels.MustNewMatcher(labels.MatchEqual, "foo", "bar"))
+ require.Equal(t, map[string][]chunks.Sample{
+ l.String(): {sample{t: 100, h: h}},
+ }, act)
+ })
+ t.Run("Test OOO native histograms when both OOO and Native Histograms are enabled", func(t *testing.T) {
+ opts := DefaultOptions()
+ opts.OutOfOrderTimeWindow = 100
+ db := newTestDB(t, withOpts(opts), withRngs(100))
+
+ // Add in-order samples
+ app := db.AppenderV2(context.Background())
+ _, err := app.Append(0, l, 0, 200, 0, h, nil, storage.AOptions{})
+ require.NoError(t, err)
+
+ // Add OOO samples
+ _, err = app.Append(0, l, 0, 100, 0, h, nil, storage.AOptions{})
+ require.NoError(t, err)
+ _, err = app.Append(0, l, 0, 150, 0, h, nil, storage.AOptions{})
+ require.NoError(t, err)
+
+ require.NoError(t, app.Commit())
+
+ q, err := db.Querier(math.MinInt, math.MaxInt64)
+ require.NoError(t, err)
+ act := query(t, q, labels.MustNewMatcher(labels.MatchEqual, "foo", "bar"))
+ requireEqualSeries(t, map[string][]chunks.Sample{
+ l.String(): {sample{t: 100, h: h}, sample{t: 150, h: h}, sample{t: 200, h: h}},
+ }, act, true)
+ })
+}
+
+// TestChunkQuerierReadWriteRace looks for any possible race between appending
+// samples and reading chunks because the head chunk that is being appended to
+// can be read in parallel and we should be able to make a copy of the chunk without
+// worrying about the parallel write.
+func TestChunkQuerierReadWriteRace_AppendV2(t *testing.T) {
+ t.Parallel()
+ db := newTestDB(t)
+
+ lbls := labels.FromStrings("foo", "bar")
+
+ writer := func() error {
+ <-time.After(5 * time.Millisecond) // Initial pause while readers start.
+ ts := 0
+ for range 500 {
+ app := db.AppenderV2(context.Background())
+ for range 10 {
+ ts++
+ _, err := app.Append(0, lbls, 0, int64(ts), float64(ts*100), nil, nil, storage.AOptions{})
+ if err != nil {
+ return err
+ }
+ }
+ err := app.Commit()
+ if err != nil {
+ return err
+ }
+ <-time.After(time.Millisecond)
+ }
+ return nil
+ }
+
+ reader := func() {
+ querier, err := db.ChunkQuerier(math.MinInt64, math.MaxInt64)
+ require.NoError(t, err)
+ defer func(q storage.ChunkQuerier) {
+ require.NoError(t, q.Close())
+ }(querier)
+ ss := querier.Select(context.Background(), false, nil, labels.MustNewMatcher(labels.MatchEqual, "foo", "bar"))
+ for ss.Next() {
+ cs := ss.At()
+ it := cs.Iterator(nil)
+ for it.Next() {
+ m := it.At()
+ b := m.Chunk.Bytes()
+ bb := make([]byte, len(b))
+ copy(bb, b) // This copying of chunk bytes detects any race.
+ }
+ }
+ require.NoError(t, ss.Err())
+ }
+
+ ch := make(chan struct{})
+ var writerErr error
+ go func() {
+ defer close(ch)
+ writerErr = writer()
+ }()
+
+Outer:
+ for {
+ reader()
+ select {
+ case <-ch:
+ break Outer
+ default:
+ }
+ }
+
+ require.NoError(t, writerErr)
+}
+
+// Regression test for https://github.com/prometheus/prometheus/pull/13754
+func TestAbortBlockCompactions_AppendV2(t *testing.T) {
+ // Create a test DB
+ db := newTestDB(t)
+ // It should NOT be compactable at the beginning of the test
+ require.False(t, db.head.compactable(), "head should NOT be compactable")
+
+ // Track the number of compactions run inside db.compactBlocks()
+ var compactions int
+
+ // Use a mock compactor with custom Plan() implementation
+ db.compactor = &mockCompactorFn{
+ planFn: func() ([]string, error) {
+ // On every Plan() run increment compactions. After 4 compactions
+ // update HEAD to make it compactable to force an exit from db.compactBlocks() loop.
+ compactions++
+ if compactions > 3 {
+ chunkRange := db.head.chunkRange.Load()
+ db.head.minTime.Store(0)
+ db.head.maxTime.Store(chunkRange * 2)
+ require.True(t, db.head.compactable(), "head should be compactable")
+ }
+ // Our custom Plan() will always return something to compact.
+ return []string{"1", "2", "3"}, nil
+ },
+ compactFn: func() ([]ulid.ULID, error) {
+ return []ulid.ULID{}, nil
+ },
+ writeFn: func() ([]ulid.ULID, error) {
+ return []ulid.ULID{}, nil
+ },
+ }
+
+ err := db.Compact(context.Background())
+ require.NoError(t, err)
+ require.True(t, db.head.compactable(), "head should be compactable")
+ require.Equal(t, 4, compactions, "expected 4 compactions to be completed")
+}
+
+func TestNewCompactorFunc_AppendV2(t *testing.T) {
+ opts := DefaultOptions()
+ block1 := ulid.MustNew(1, nil)
+ block2 := ulid.MustNew(2, nil)
+ opts.NewCompactorFunc = func(context.Context, prometheus.Registerer, *slog.Logger, []int64, chunkenc.Pool, *Options) (Compactor, error) {
+ return &mockCompactorFn{
+ planFn: func() ([]string, error) {
+ return []string{block1.String(), block2.String()}, nil
+ },
+ compactFn: func() ([]ulid.ULID, error) {
+ return []ulid.ULID{block1}, nil
+ },
+ writeFn: func() ([]ulid.ULID, error) {
+ return []ulid.ULID{block2}, nil
+ },
+ }, nil
+ }
+ db := newTestDB(t, withOpts(opts))
+
+ plans, err := db.compactor.Plan("")
+ require.NoError(t, err)
+ require.Equal(t, []string{block1.String(), block2.String()}, plans)
+ ulids, err := db.compactor.Compact("", nil, nil)
+ require.NoError(t, err)
+ require.Len(t, ulids, 1)
+ require.Equal(t, block1, ulids[0])
+ ulids, err = db.compactor.Write("", nil, 0, 1, nil)
+ require.NoError(t, err)
+ require.Len(t, ulids, 1)
+ require.Equal(t, block2, ulids[0])
+}
diff --git a/tsdb/db_test.go b/tsdb/db_test.go
index 100318c474..3f2861d633 100644
--- a/tsdb/db_test.go
+++ b/tsdb/db_test.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
@@ -52,6 +52,7 @@ import (
"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/prompb"
"github.com/prometheus/prometheus/storage"
"github.com/prometheus/prometheus/storage/remote"
@@ -80,28 +81,82 @@ func TestMain(m *testing.M) {
goleak.IgnoreTopFunction("go.opencensus.io/stats/view.(*worker).start"))
}
-func openTestDB(t testing.TB, opts *Options, rngs []int64) (db *DB) {
- tmpdir := t.TempDir()
- var err error
+type testDBOptions struct {
+ dir string
+ opts *Options
+ rngs []int64
+}
+type testDBOpt func(o *testDBOptions)
- if opts == nil {
- opts = DefaultOptions()
+func withDir(dir string) testDBOpt {
+ return func(o *testDBOptions) {
+ o.dir = dir
+ }
+}
+
+func withOpts(opts *Options) testDBOpt {
+ return func(o *testDBOptions) {
+ o.opts = opts
+ }
+}
+
+func withRngs(rngs ...int64) testDBOpt {
+ return func(o *testDBOptions) {
+ o.rngs = rngs
+ }
+}
+
+func newTestDB(t testing.TB, opts ...testDBOpt) (db *DB) {
+ var o testDBOptions
+ for _, opt := range opts {
+ opt(&o)
+ }
+ if o.opts == nil {
+ o.opts = DefaultOptions()
+ }
+ if o.dir == "" {
+ o.dir = t.TempDir()
}
- if len(rngs) == 0 {
- db, err = Open(tmpdir, nil, nil, opts, nil)
+ var err error
+ if len(o.rngs) == 0 {
+ db, err = Open(o.dir, nil, nil, o.opts, nil)
} else {
- opts, rngs = validateOpts(opts, rngs)
- db, err = open(tmpdir, nil, nil, opts, rngs, nil)
+ o.opts, o.rngs = validateOpts(o.opts, o.rngs)
+ db, err = open(o.dir, nil, nil, o.opts, o.rngs, nil)
}
require.NoError(t, err)
- // Do not Close() the test database by default as it will deadlock on test failures.
+ t.Cleanup(func() {
+ // Always close. DB is safe for close-after-close.
+ require.NoError(t, db.Close())
+ })
return db
}
+func TestDBClose_AfterClose(t *testing.T) {
+ db := newTestDB(t)
+ require.NoError(t, db.Close())
+ require.NoError(t, db.Close())
+
+ // Double check if we are closing correct DB after reuse.
+ db = newTestDB(t)
+ require.NoError(t, db.Close())
+ require.NoError(t, db.Close())
+}
+
// query runs a matcher query against the querier and fully expands its data.
func query(t testing.TB, q storage.Querier, matchers ...*labels.Matcher) map[string][]chunks.Sample {
+ return queryHelper(t, q, true, matchers...)
+}
+
+// queryWithoutReplacingNaNs runs a matcher query against the querier and fully expands its data.
+func queryWithoutReplacingNaNs(t testing.TB, q storage.Querier, matchers ...*labels.Matcher) map[string][]chunks.Sample {
+ return queryHelper(t, q, false, matchers...)
+}
+
+// queryHelper runs a matcher query against the querier and fully expands its data.
+func queryHelper(t testing.TB, q storage.Querier, withNaNReplacement bool, matchers ...*labels.Matcher) map[string][]chunks.Sample {
ss := q.Select(context.Background(), false, nil, matchers...)
defer func() {
require.NoError(t, q.Close())
@@ -113,7 +168,13 @@ func query(t testing.TB, q storage.Querier, matchers ...*labels.Matcher) map[str
series := ss.At()
it = series.Iterator(it)
- samples, err := storage.ExpandSamples(it, newSample)
+ var samples []chunks.Sample
+ var err error
+ if withNaNReplacement {
+ samples, err = storage.ExpandSamples(it, newSample)
+ } else {
+ samples, err = storage.ExpandSamplesWithoutReplacingNaNs(it, newSample)
+ }
require.NoError(t, err)
require.NoError(t, it.Err())
@@ -182,10 +243,7 @@ func queryChunks(t testing.TB, q storage.ChunkQuerier, matchers ...*labels.Match
// Ensure that blocks are held in memory in their time order
// and not in ULID order as they are read from the directory.
func TestDB_reloadOrder(t *testing.T) {
- db := openTestDB(t, nil, nil)
- defer func() {
- require.NoError(t, db.Close())
- }()
+ db := newTestDB(t)
metas := []BlockMeta{
{MinTime: 90, MaxTime: 100},
@@ -208,10 +266,7 @@ func TestDB_reloadOrder(t *testing.T) {
}
func TestDataAvailableOnlyAfterCommit(t *testing.T) {
- db := openTestDB(t, nil, nil)
- defer func() {
- require.NoError(t, db.Close())
- }()
+ db := newTestDB(t)
ctx := context.Background()
app := db.Appender(ctx)
@@ -239,7 +294,7 @@ func TestDataAvailableOnlyAfterCommit(t *testing.T) {
// TestNoPanicAfterWALCorruption ensures that querying the db after a WAL corruption doesn't cause a panic.
// https://github.com/prometheus/prometheus/issues/7548
func TestNoPanicAfterWALCorruption(t *testing.T) {
- db := openTestDB(t, &Options{WALSegmentSize: 32 * 1024}, nil)
+ db := newTestDB(t, withOpts(&Options{WALSegmentSize: 32 * 1024}))
// Append until the first mmapped head chunk.
// This is to ensure that all samples can be read from the mmapped chunks when the WAL is corrupted.
@@ -278,11 +333,7 @@ func TestNoPanicAfterWALCorruption(t *testing.T) {
// Query the data.
{
- db, err := Open(db.Dir(), nil, nil, nil, nil)
- require.NoError(t, err)
- defer func() {
- require.NoError(t, db.Close())
- }()
+ db := newTestDB(t, withDir(db.Dir()))
require.Equal(t, 1.0, prom_testutil.ToFloat64(db.head.metrics.walCorruptionsTotal), "WAL corruption count mismatch")
querier, err := db.Querier(0, maxt)
@@ -294,10 +345,7 @@ func TestNoPanicAfterWALCorruption(t *testing.T) {
}
func TestDataNotAvailableAfterRollback(t *testing.T) {
- db := openTestDB(t, nil, nil)
- defer func() {
- require.NoError(t, db.Close())
- }()
+ db := newTestDB(t)
app := db.Appender(context.Background())
_, err := app.Append(0, labels.FromStrings("type", "float"), 0, 0)
@@ -384,10 +432,7 @@ func TestDataNotAvailableAfterRollback(t *testing.T) {
}
func TestDBAppenderAddRef(t *testing.T) {
- db := openTestDB(t, nil, nil)
- defer func() {
- require.NoError(t, db.Close())
- }()
+ db := newTestDB(t)
ctx := context.Background()
app1 := db.Appender(ctx)
@@ -442,10 +487,7 @@ func TestDBAppenderAddRef(t *testing.T) {
}
func TestAppendEmptyLabelsIgnored(t *testing.T) {
- db := openTestDB(t, nil, nil)
- defer func() {
- require.NoError(t, db.Close())
- }()
+ db := newTestDB(t)
ctx := context.Background()
app1 := db.Appender(ctx)
@@ -495,10 +537,7 @@ func TestDeleteSimple(t *testing.T) {
for _, c := range cases {
t.Run("", func(t *testing.T) {
- db := openTestDB(t, nil, nil)
- defer func() {
- require.NoError(t, db.Close())
- }()
+ db := newTestDB(t)
ctx := context.Background()
app := db.Appender(ctx)
@@ -525,7 +564,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{
@@ -556,10 +595,7 @@ func TestDeleteSimple(t *testing.T) {
}
func TestAmendHistogramDatapointCausesError(t *testing.T) {
- db := openTestDB(t, nil, nil)
- defer func() {
- require.NoError(t, db.Close())
- }()
+ db := newTestDB(t)
ctx := context.Background()
app := db.Appender(ctx)
@@ -617,10 +653,7 @@ func TestAmendHistogramDatapointCausesError(t *testing.T) {
}
func TestDuplicateNaNDatapointNoAmendError(t *testing.T) {
- db := openTestDB(t, nil, nil)
- defer func() {
- require.NoError(t, db.Close())
- }()
+ db := newTestDB(t)
ctx := context.Background()
app := db.Appender(ctx)
@@ -634,10 +667,7 @@ func TestDuplicateNaNDatapointNoAmendError(t *testing.T) {
}
func TestNonDuplicateNaNDatapointsCausesAmendError(t *testing.T) {
- db := openTestDB(t, nil, nil)
- defer func() {
- require.NoError(t, db.Close())
- }()
+ db := newTestDB(t)
ctx := context.Background()
app := db.Appender(ctx)
@@ -651,10 +681,7 @@ func TestNonDuplicateNaNDatapointsCausesAmendError(t *testing.T) {
}
func TestEmptyLabelsetCausesError(t *testing.T) {
- db := openTestDB(t, nil, nil)
- defer func() {
- require.NoError(t, db.Close())
- }()
+ db := newTestDB(t)
ctx := context.Background()
app := db.Appender(ctx)
@@ -664,10 +691,7 @@ func TestEmptyLabelsetCausesError(t *testing.T) {
}
func TestSkippingInvalidValuesInSameTxn(t *testing.T) {
- db := openTestDB(t, nil, nil)
- defer func() {
- require.NoError(t, db.Close())
- }()
+ db := newTestDB(t)
// Append AmendedValue.
ctx := context.Background()
@@ -685,7 +709,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.
@@ -702,12 +726,12 @@ 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)
}
func TestDB_Snapshot(t *testing.T) {
- db := openTestDB(t, nil, nil)
+ db := newTestDB(t)
// append data
ctx := context.Background()
@@ -725,9 +749,7 @@ func TestDB_Snapshot(t *testing.T) {
require.NoError(t, db.Close())
// reopen DB from snapshot
- db, err := Open(snap, nil, nil, nil, nil)
- require.NoError(t, err)
- defer func() { require.NoError(t, db.Close()) }()
+ db = newTestDB(t, withDir(snap))
querier, err := db.Querier(mint, mint+1000)
require.NoError(t, err)
@@ -754,7 +776,7 @@ func TestDB_Snapshot(t *testing.T) {
// that are outside the set block time range.
// See https://github.com/prometheus/prometheus/issues/5105
func TestDB_Snapshot_ChunksOutsideOfCompactedRange(t *testing.T) {
- db := openTestDB(t, nil, nil)
+ db := newTestDB(t)
ctx := context.Background()
app := db.Appender(ctx)
@@ -773,10 +795,8 @@ func TestDB_Snapshot_ChunksOutsideOfCompactedRange(t *testing.T) {
require.NoError(t, db.Snapshot(snap, true))
require.NoError(t, db.Close())
- // Reopen DB from snapshot.
- db, err := Open(snap, nil, nil, nil, nil)
- require.NoError(t, err)
- defer func() { require.NoError(t, db.Close()) }()
+ // reopen DB from snapshot
+ db = newTestDB(t, withDir(snap))
querier, err := db.Querier(mint, mint+1000)
require.NoError(t, err)
@@ -804,8 +824,7 @@ func TestDB_Snapshot_ChunksOutsideOfCompactedRange(t *testing.T) {
func TestDB_SnapshotWithDelete(t *testing.T) {
const numSamples int64 = 10
- db := openTestDB(t, nil, nil)
- defer func() { require.NoError(t, db.Close()) }()
+ db := newTestDB(t)
ctx := context.Background()
app := db.Appender(ctx)
@@ -841,12 +860,10 @@ func TestDB_SnapshotWithDelete(t *testing.T) {
require.NoError(t, db.Snapshot(snap, true))
// reopen DB from snapshot
- newDB, err := Open(snap, nil, nil, nil, nil)
- require.NoError(t, err)
- defer func() { require.NoError(t, newDB.Close()) }()
+ db := newTestDB(t, withDir(snap))
// Compare the result.
- q, err := newDB.Querier(0, numSamples)
+ q, err := db.Querier(0, numSamples)
require.NoError(t, err)
defer func() { require.NoError(t, q.Close()) }()
@@ -854,7 +871,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{
@@ -944,10 +961,7 @@ func TestDB_e2e(t *testing.T) {
seriesMap[labels.New(l...).String()] = []chunks.Sample{}
}
- db := openTestDB(t, nil, nil)
- defer func() {
- require.NoError(t, db.Close())
- }()
+ db := newTestDB(t)
ctx := context.Background()
app := db.Appender(ctx)
@@ -960,7 +974,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)
@@ -1049,9 +1063,7 @@ func TestDB_e2e(t *testing.T) {
}
func TestWALFlushedOnDBClose(t *testing.T) {
- db := openTestDB(t, nil, nil)
-
- dirDb := db.Dir()
+ db := newTestDB(t)
lbls := labels.FromStrings("labelname", "labelvalue")
@@ -1063,9 +1075,7 @@ func TestWALFlushedOnDBClose(t *testing.T) {
require.NoError(t, db.Close())
- db, err = Open(dirDb, nil, nil, nil, nil)
- require.NoError(t, err)
- defer func() { require.NoError(t, db.Close()) }()
+ db = newTestDB(t, withDir(db.Dir()))
q, err := db.Querier(0, 1)
require.NoError(t, err)
@@ -1131,7 +1141,7 @@ func TestWALSegmentSizeOptions(t *testing.T) {
t.Run(fmt.Sprintf("WALSegmentSize %d test", segmentSize), func(t *testing.T) {
opts := DefaultOptions()
opts.WALSegmentSize = segmentSize
- db := openTestDB(t, opts, nil)
+ db := newTestDB(t, withOpts(opts))
for i := range int64(155) {
app := db.Appender(context.Background())
@@ -1144,9 +1154,8 @@ func TestWALSegmentSizeOptions(t *testing.T) {
require.NoError(t, app.Commit())
}
- dbDir := db.Dir()
require.NoError(t, db.Close())
- testFunc(dbDir, opts.WALSegmentSize)
+ testFunc(db.Dir(), opts.WALSegmentSize)
})
}
}
@@ -1173,7 +1182,7 @@ func TestWALReplayRaceOnSamplesLoggedBeforeSeries(t *testing.T) {
func testWALReplayRaceOnSamplesLoggedBeforeSeries(t *testing.T, numSamplesBeforeSeriesCreation, numSamplesAfterSeriesCreation int) {
const numSeries = 1000
- db := openTestDB(t, nil, nil)
+ db := newTestDB(t)
db.DisableCompactions()
for seriesRef := 1; seriesRef <= numSeries; seriesRef++ {
@@ -1206,14 +1215,10 @@ func testWALReplayRaceOnSamplesLoggedBeforeSeries(t *testing.T, numSamplesBefore
require.NoError(t, db.Close())
// Reopen the DB, replaying the WAL.
- reopenDB, err := Open(db.Dir(), promslog.New(&promslog.Config{}), nil, nil, nil)
- require.NoError(t, err)
- t.Cleanup(func() {
- require.NoError(t, reopenDB.Close())
- })
+ db = newTestDB(t, withDir(db.Dir()))
// Query back chunks for all series.
- q, err := reopenDB.ChunkQuerier(math.MinInt64, math.MaxInt64)
+ q, err := db.ChunkQuerier(math.MinInt64, math.MaxInt64)
require.NoError(t, err)
set := q.Select(context.Background(), false, nil, labels.MustNewMatcher(labels.MatchRegexp, "series_id", ".+"))
@@ -1242,7 +1247,7 @@ func TestTombstoneClean(t *testing.T) {
t.Parallel()
const numSamples int64 = 10
- db := openTestDB(t, nil, nil)
+ db := newTestDB(t)
ctx := context.Background()
app := db.Appender(ctx)
@@ -1273,9 +1278,7 @@ func TestTombstoneClean(t *testing.T) {
require.NoError(t, db.Close())
// Reopen DB from snapshot.
- db, err := Open(snap, nil, nil, nil, nil)
- require.NoError(t, err)
- defer db.Close()
+ db := newTestDB(t, withDir(snap))
for _, r := range c.intervals {
require.NoError(t, db.Delete(ctx, r.Mint, r.Maxt, labels.MustNewMatcher(labels.MatchEqual, "a", "b")))
@@ -1293,7 +1296,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{
@@ -1337,7 +1340,7 @@ func TestTombstoneCleanResultEmptyBlock(t *testing.T) {
t.Parallel()
numSamples := int64(10)
- db := openTestDB(t, nil, nil)
+ db := newTestDB(t)
ctx := context.Background()
app := db.Appender(ctx)
@@ -1358,9 +1361,7 @@ func TestTombstoneCleanResultEmptyBlock(t *testing.T) {
require.NoError(t, db.Close())
// Reopen DB from snapshot.
- db, err := Open(snap, nil, nil, nil, nil)
- require.NoError(t, err)
- defer db.Close()
+ db = newTestDB(t, withDir(snap))
// Create tombstones by deleting all samples.
for _, r := range intervals {
@@ -1370,7 +1371,7 @@ func TestTombstoneCleanResultEmptyBlock(t *testing.T) {
require.NoError(t, db.CleanTombstones())
// After cleaning tombstones that covers the entire block, no blocks should be left behind.
- actualBlockDirs, err := blockDirs(db.dir)
+ actualBlockDirs, err := blockDirs(db.Dir())
require.NoError(t, err)
require.Empty(t, actualBlockDirs)
}
@@ -1380,10 +1381,7 @@ func TestTombstoneCleanResultEmptyBlock(t *testing.T) {
// if TombstoneClean leaves any blocks behind these will overlap.
func TestTombstoneCleanFail(t *testing.T) {
t.Parallel()
- db := openTestDB(t, nil, nil)
- defer func() {
- require.NoError(t, db.Close())
- }()
+ db := newTestDB(t)
var oldBlockDirs []string
@@ -1415,7 +1413,7 @@ func TestTombstoneCleanFail(t *testing.T) {
require.Error(t, db.CleanTombstones())
// Now check that the CleanTombstones replaced the old block even after a failure.
- actualBlockDirs, err := blockDirs(db.dir)
+ actualBlockDirs, err := blockDirs(db.Dir())
require.NoError(t, err)
// Only one block should have been replaced by a new block.
require.Len(t, actualBlockDirs, len(oldBlockDirs))
@@ -1516,10 +1514,7 @@ func TestTimeRetention(t *testing.T) {
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
- db := openTestDB(t, nil, []int64{1000})
- defer func() {
- require.NoError(t, db.Close())
- }()
+ db := newTestDB(t, withRngs(1000))
for _, m := range tc.blocks {
createBlock(t, db.Dir(), genSeries(10, 10, m.MinTime, m.MaxTime))
@@ -1545,12 +1540,9 @@ func TestTimeRetention(t *testing.T) {
}
func TestRetentionDurationMetric(t *testing.T) {
- db := openTestDB(t, &Options{
+ db := newTestDB(t, withOpts(&Options{
RetentionDuration: 1000,
- }, []int64{100})
- defer func() {
- require.NoError(t, db.Close())
- }()
+ }), withRngs(100))
expRetentionDuration := 1.0
actRetentionDuration := prom_testutil.ToFloat64(db.metrics.retentionDuration)
@@ -1561,10 +1553,7 @@ func TestSizeRetention(t *testing.T) {
t.Parallel()
opts := DefaultOptions()
opts.OutOfOrderTimeWindow = 100
- db := openTestDB(t, opts, []int64{100})
- defer func() {
- require.NoError(t, db.Close())
- }()
+ db := newTestDB(t, withOpts(opts), withRngs(100))
blocks := []*BlockMeta{
{MinTime: 100, MaxTime: 200}, // Oldest block
@@ -1708,12 +1697,9 @@ func TestSizeRetentionMetric(t *testing.T) {
}
for _, c := range cases {
- db := openTestDB(t, &Options{
+ db := newTestDB(t, withOpts(&Options{
MaxBytes: c.maxBytes,
- }, []int64{100})
- defer func() {
- require.NoError(t, db.Close())
- }()
+ }), withRngs(100))
actMaxBytes := int64(prom_testutil.ToFloat64(db.metrics.maxBytes))
require.Equal(t, c.expMaxBytes, actMaxBytes, "metric retention limit bytes mismatch")
@@ -1730,12 +1716,9 @@ func TestRuntimeRetentionConfigChange(t *testing.T) {
shorterRetentionDuration = int64(1 * time.Hour / time.Millisecond) // 1 hour
)
- db := openTestDB(t, &Options{
+ db := newTestDB(t, withOpts(&Options{
RetentionDuration: initialRetentionDuration,
- }, []int64{100})
- defer func() {
- require.NoError(t, db.Close())
- }()
+ }), withRngs(100))
nineHoursMs := int64(9 * time.Hour / time.Millisecond)
nineAndHalfHoursMs := int64((9*time.Hour + 30*time.Minute) / time.Millisecond)
@@ -1790,10 +1773,7 @@ func TestRuntimeRetentionConfigChange(t *testing.T) {
}
func TestNotMatcherSelectsLabelsUnsetSeries(t *testing.T) {
- db := openTestDB(t, nil, nil)
- defer func() {
- require.NoError(t, db.Close())
- }()
+ db := newTestDB(t)
labelpairs := []labels.Labels{
labels.FromStrings("a", "abcd", "b", "abcde"),
@@ -1978,10 +1958,7 @@ func TestOverlappingBlocksDetectsAllOverlaps(t *testing.T) {
// Regression test for https://github.com/prometheus/tsdb/issues/347
func TestChunkAtBlockBoundary(t *testing.T) {
t.Parallel()
- db := openTestDB(t, nil, nil)
- defer func() {
- require.NoError(t, db.Close())
- }()
+ db := newTestDB(t)
ctx := context.Background()
app := db.Appender(ctx)
@@ -2035,10 +2012,7 @@ func TestChunkAtBlockBoundary(t *testing.T) {
func TestQuerierWithBoundaryChunks(t *testing.T) {
t.Parallel()
- db := openTestDB(t, nil, nil)
- defer func() {
- require.NoError(t, db.Close())
- }()
+ db := newTestDB(t)
ctx := context.Background()
app := db.Appender(ctx)
@@ -2081,11 +2055,7 @@ func TestQuerierWithBoundaryChunks(t *testing.T) {
func TestInitializeHeadTimestamp(t *testing.T) {
t.Parallel()
t.Run("clean", func(t *testing.T) {
- dir := t.TempDir()
-
- db, err := Open(dir, nil, nil, nil, nil)
- require.NoError(t, err)
- defer db.Close()
+ db := newTestDB(t)
// Should be set to init values if no WAL or blocks exist so far.
require.Equal(t, int64(math.MaxInt64), db.head.MinTime())
@@ -2095,7 +2065,7 @@ func TestInitializeHeadTimestamp(t *testing.T) {
// First added sample initializes the writable range.
ctx := context.Background()
app := db.Appender(ctx)
- _, err = app.Append(0, labels.FromStrings("a", "b"), 1000, 1)
+ _, err := app.Append(0, labels.FromStrings("a", "b"), 1000, 1)
require.NoError(t, err)
require.Equal(t, int64(1000), db.head.MinTime())
@@ -2123,9 +2093,7 @@ func TestInitializeHeadTimestamp(t *testing.T) {
require.NoError(t, err)
require.NoError(t, w.Close())
- db, err := Open(dir, nil, nil, nil, nil)
- require.NoError(t, err)
- defer db.Close()
+ db := newTestDB(t, withDir(dir))
require.Equal(t, int64(5000), db.head.MinTime())
require.Equal(t, int64(15000), db.head.MaxTime())
@@ -2136,9 +2104,7 @@ func TestInitializeHeadTimestamp(t *testing.T) {
createBlock(t, dir, genSeries(1, 1, 1000, 2000))
- db, err := Open(dir, nil, nil, nil, nil)
- require.NoError(t, err)
- defer db.Close()
+ db := newTestDB(t, withDir(dir))
require.Equal(t, int64(2000), db.head.MinTime())
require.Equal(t, int64(2000), db.head.MaxTime())
@@ -2167,11 +2133,7 @@ func TestInitializeHeadTimestamp(t *testing.T) {
require.NoError(t, err)
require.NoError(t, w.Close())
- r := prometheus.NewRegistry()
-
- db, err := Open(dir, nil, r, nil, nil)
- require.NoError(t, err)
- defer db.Close()
+ db := newTestDB(t, withDir(dir))
require.Equal(t, int64(6000), db.head.MinTime())
require.Equal(t, int64(15000), db.head.MaxTime())
@@ -2183,11 +2145,9 @@ func TestInitializeHeadTimestamp(t *testing.T) {
func TestNoEmptyBlocks(t *testing.T) {
t.Parallel()
- db := openTestDB(t, nil, []int64{100})
+ db := newTestDB(t, withRngs(100))
ctx := context.Background()
- defer func() {
- require.NoError(t, db.Close())
- }()
+
db.DisableCompactions()
rangeToTriggerCompaction := db.compactor.(*LeveledCompactor).ranges[0]/2*3 - 1
@@ -2344,10 +2304,7 @@ func TestDB_LabelNames(t *testing.T) {
for _, tst := range tests {
t.Run("", func(t *testing.T) {
ctx := context.Background()
- db := openTestDB(t, nil, nil)
- defer func() {
- require.NoError(t, db.Close())
- }()
+ db := newTestDB(t)
appendSamples(db, 0, 4, tst.sampleLabels1)
@@ -2392,10 +2349,7 @@ func TestDB_LabelNames(t *testing.T) {
func TestCorrectNumTombstones(t *testing.T) {
t.Parallel()
- db := openTestDB(t, nil, nil)
- defer func() {
- require.NoError(t, db.Close())
- }()
+ db := newTestDB(t)
blockRange := db.compactor.(*LeveledCompactor).ranges[0]
name, value := "foo", "bar"
@@ -2454,6 +2408,7 @@ func TestBlockRanges(t *testing.T) {
createBlock(t, dir, genSeries(1, 1, 0, firstBlockMaxT))
db, err := open(dir, logger, nil, DefaultOptions(), []int64{10000}, nil)
require.NoError(t, err)
+ db.DisableCompactions()
rangeToTriggerCompaction := db.compactor.(*LeveledCompactor).ranges[0]/2*3 + 1
@@ -2470,21 +2425,16 @@ func TestBlockRanges(t *testing.T) {
require.NoError(t, err)
require.NoError(t, app.Commit())
- for range 100 {
- if len(db.Blocks()) == 2 {
- break
- }
- time.Sleep(100 * time.Millisecond)
- }
- require.Len(t, db.Blocks(), 2, "no new block created after the set timeout")
+ require.NoError(t, db.Compact(ctx))
+ blocks := db.Blocks()
+ require.Len(t, blocks, 2, "no new block after compaction")
- require.LessOrEqual(t, db.Blocks()[1].Meta().MinTime, db.Blocks()[0].Meta().MaxTime,
- "new block overlaps old:%v,new:%v", db.Blocks()[0].Meta(), db.Blocks()[1].Meta())
+ require.GreaterOrEqual(t, blocks[1].Meta().MinTime, blocks[0].Meta().MaxTime,
+ "new block overlaps old:%v,new:%v", blocks[0].Meta(), blocks[1].Meta())
// Test that wal records are skipped when an existing block covers the same time ranges
// and compaction doesn't create an overlapping block.
app = db.Appender(ctx)
- db.DisableCompactions()
_, err = app.Append(0, lbl, secondBlockMaxt+1, rand.Float64())
require.NoError(t, err)
_, err = app.Append(0, lbl, secondBlockMaxt+2, rand.Float64())
@@ -2501,6 +2451,7 @@ func TestBlockRanges(t *testing.T) {
db, err = open(dir, logger, nil, DefaultOptions(), []int64{10000}, nil)
require.NoError(t, err)
+ db.DisableCompactions()
defer db.Close()
require.Len(t, db.Blocks(), 3, "db doesn't include expected number of blocks")
@@ -2510,17 +2461,12 @@ func TestBlockRanges(t *testing.T) {
_, err = app.Append(0, lbl, thirdBlockMaxt+rangeToTriggerCompaction, rand.Float64()) // Trigger a compaction
require.NoError(t, err)
require.NoError(t, app.Commit())
- for range 100 {
- if len(db.Blocks()) == 4 {
- break
- }
- time.Sleep(100 * time.Millisecond)
- }
+ require.NoError(t, db.Compact(ctx))
+ blocks = db.Blocks()
+ require.Len(t, blocks, 4, "no new block after compaction")
- require.Len(t, db.Blocks(), 4, "no new block created after the set timeout")
-
- require.LessOrEqual(t, db.Blocks()[3].Meta().MinTime, db.Blocks()[2].Meta().MaxTime,
- "new block overlaps old:%v,new:%v", db.Blocks()[2].Meta(), db.Blocks()[3].Meta())
+ require.GreaterOrEqual(t, blocks[3].Meta().MinTime, blocks[2].Meta().MaxTime,
+ "new block overlaps old:%v,new:%v", blocks[2].Meta(), blocks[3].Meta())
}
// TestDBReadOnly ensures that opening a DB in readonly mode doesn't modify any files on the disk.
@@ -2528,8 +2474,7 @@ func TestBlockRanges(t *testing.T) {
func TestDBReadOnly(t *testing.T) {
t.Parallel()
var (
- dbDir string
- logger = promslog.New(&promslog.Config{})
+ dbDir = t.TempDir()
expBlocks []*Block
expBlock *Block
expSeries map[string][]chunks.Sample
@@ -2541,8 +2486,6 @@ func TestDBReadOnly(t *testing.T) {
// Bootstrap the db.
{
- dbDir = t.TempDir()
-
dbBlocks := []*BlockMeta{
// Create three 2-sample blocks.
{MinTime: 10, MaxTime: 12},
@@ -2555,7 +2498,7 @@ func TestDBReadOnly(t *testing.T) {
}
// Add head to test DBReadOnly WAL reading capabilities.
- w, err := wlog.New(logger, nil, filepath.Join(dbDir, "wal"), compression.Snappy)
+ w, err := wlog.New(nil, nil, filepath.Join(dbDir, "wal"), compression.Snappy)
require.NoError(t, err)
h := createHead(t, w, genSeries(1, 1, 16, 18), dbDir)
require.NoError(t, h.Close())
@@ -2563,8 +2506,7 @@ func TestDBReadOnly(t *testing.T) {
// Open a normal db to use for a comparison.
{
- dbWritable, err := Open(dbDir, logger, nil, nil, nil)
- require.NoError(t, err)
+ dbWritable := newTestDB(t, withDir(dbDir))
dbWritable.DisableCompactions()
dbSizeBeforeAppend, err := fileutil.DirSize(dbWritable.Dir())
@@ -2592,7 +2534,7 @@ func TestDBReadOnly(t *testing.T) {
}
// Open a read only db and ensure that the API returns the same result as the normal DB.
- dbReadOnly, err := OpenDBReadOnly(dbDir, "", logger)
+ dbReadOnly, err := OpenDBReadOnly(dbDir, "", nil)
require.NoError(t, err)
defer func() { require.NoError(t, dbReadOnly.Close()) }()
@@ -2665,24 +2607,20 @@ func TestDBReadOnlyClosing(t *testing.T) {
func TestDBReadOnly_FlushWAL(t *testing.T) {
t.Parallel()
var (
- dbDir string
- logger = promslog.New(&promslog.Config{})
- err error
- maxt int
- ctx = context.Background()
+ dbDir = t.TempDir()
+ err error
+ maxt int
+ ctx = context.Background()
)
// Bootstrap the db.
{
- dbDir = t.TempDir()
-
// Append data to the WAL.
- db, err := Open(dbDir, logger, nil, nil, nil)
- require.NoError(t, err)
+ db := newTestDB(t, withDir(dbDir))
db.DisableCompactions()
app := db.Appender(ctx)
maxt = 1000
- for i := 0; i < maxt; i++ {
+ for i := range maxt {
_, err := app.Append(0, labels.FromStrings(defaultLabelName, "flush"), int64(i), 1.0)
require.NoError(t, err)
}
@@ -2691,7 +2629,7 @@ func TestDBReadOnly_FlushWAL(t *testing.T) {
}
// Flush WAL.
- db, err := OpenDBReadOnly(dbDir, "", logger)
+ db, err := OpenDBReadOnly(dbDir, "", nil)
require.NoError(t, err)
flush := t.TempDir()
@@ -2699,7 +2637,7 @@ func TestDBReadOnly_FlushWAL(t *testing.T) {
require.NoError(t, db.Close())
// Reopen the DB from the flushed WAL block.
- db, err = OpenDBReadOnly(flush, "", logger)
+ db, err = OpenDBReadOnly(flush, "", nil)
require.NoError(t, err)
defer func() { require.NoError(t, db.Close()) }()
blocks, err := db.Blocks()
@@ -2760,10 +2698,7 @@ func TestDBReadOnly_Querier_NoAlteration(t *testing.T) {
}
t.Run("doesn't cut chunks while replaying WAL", func(t *testing.T) {
- db := openTestDB(t, nil, nil)
- defer func() {
- require.NoError(t, db.Close())
- }()
+ db := newTestDB(t)
// Append until the first mmapped head chunk.
for i := range 121 {
@@ -2773,33 +2708,31 @@ func TestDBReadOnly_Querier_NoAlteration(t *testing.T) {
require.NoError(t, app.Commit())
}
- spinUpQuerierAndCheck(db.dir, t.TempDir(), 0)
+ spinUpQuerierAndCheck(db.Dir(), t.TempDir(), 0)
// The RW Head should have no problem cutting its own chunk,
// this also proves that a chunk needed to be cut.
require.NotPanics(t, func() { db.ForceHeadMMap() })
- require.Equal(t, 1, countChunks(db.dir))
+ require.Equal(t, 1, countChunks(db.Dir()))
})
t.Run("doesn't truncate corrupted chunks", func(t *testing.T) {
- db := openTestDB(t, nil, nil)
+ db := newTestDB(t)
require.NoError(t, db.Close())
// Simulate a corrupted chunk: without a header.
- chunk, err := os.Create(path.Join(mmappedChunksDir(db.dir), "000001"))
+ chunk, err := os.Create(path.Join(mmappedChunksDir(db.Dir()), "000001"))
require.NoError(t, err)
require.NoError(t, chunk.Close())
- spinUpQuerierAndCheck(db.dir, t.TempDir(), 1)
+ spinUpQuerierAndCheck(db.Dir(), t.TempDir(), 1)
// The RW Head should have no problem truncating its corrupted file:
// this proves that the chunk needed to be truncated.
- db, err = Open(db.dir, nil, nil, nil, nil)
- defer func() {
- require.NoError(t, db.Close())
- }()
+ db = newTestDB(t, withDir(db.Dir()))
+
require.NoError(t, err)
- require.Equal(t, 0, countChunks(db.dir))
+ require.Equal(t, 0, countChunks(db.Dir()))
})
}
@@ -2808,11 +2741,7 @@ func TestDBCannotSeePartialCommits(t *testing.T) {
t.Skip("skipping test since tsdb isolation is disabled")
}
- tmpdir := t.TempDir()
-
- db, err := Open(tmpdir, nil, nil, nil, nil)
- require.NoError(t, err)
- defer db.Close()
+ db := newTestDB(t)
stop := make(chan struct{})
firstInsert := make(chan struct{})
@@ -2828,8 +2757,7 @@ func TestDBCannotSeePartialCommits(t *testing.T) {
_, err := app.Append(0, labels.FromStrings("foo", "bar", "a", strconv.Itoa(j)), int64(iter), float64(iter))
require.NoError(t, err)
}
- err = app.Commit()
- require.NoError(t, err)
+ require.NoError(t, app.Commit())
if iter == 0 {
close(firstInsert)
@@ -2879,12 +2807,7 @@ func TestDBQueryDoesntSeeAppendsAfterCreation(t *testing.T) {
t.Skip("skipping test since tsdb isolation is disabled")
}
- tmpdir := t.TempDir()
-
- db, err := Open(tmpdir, nil, nil, nil, nil)
- require.NoError(t, err)
- defer db.Close()
-
+ db := newTestDB(t)
querierBeforeAdd, err := db.Querier(0, 1000000)
require.NoError(t, err)
defer querierBeforeAdd.Close()
@@ -2950,11 +2873,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 {
@@ -3156,11 +3079,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()
@@ -3202,19 +3125,16 @@ func TestChunkReader_ConcurrentReads(t *testing.T) {
// * queries the db to ensure the samples are present from the compacted head.
func TestCompactHead(t *testing.T) {
t.Parallel()
- dbDir := t.TempDir()
// Open a DB and append data to the WAL.
- tsdbCfg := &Options{
+ opts := &Options{
RetentionDuration: int64(time.Hour * 24 * 15 / time.Millisecond),
NoLockfile: true,
MinBlockDuration: int64(time.Hour * 2 / time.Millisecond),
MaxBlockDuration: int64(time.Hour * 2 / time.Millisecond),
WALCompression: compression.Snappy,
}
-
- db, err := Open(dbDir, promslog.NewNopLogger(), prometheus.NewRegistry(), tsdbCfg, nil)
- require.NoError(t, err)
+ db := newTestDB(t, withOpts(opts))
ctx := context.Background()
app := db.Appender(ctx)
var expSamples []sample
@@ -3223,7 +3143,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())
@@ -3234,8 +3154,7 @@ func TestCompactHead(t *testing.T) {
// Delete everything but the new block and
// reopen the db to query it to ensure it includes the head data.
require.NoError(t, deleteNonBlocks(db.Dir()))
- db, err = Open(dbDir, promslog.NewNopLogger(), prometheus.NewRegistry(), tsdbCfg, nil)
- require.NoError(t, err)
+ db = newTestDB(t, withDir(db.Dir()), withOpts(opts))
require.Len(t, db.Blocks(), 1)
require.Equal(t, int64(maxt), db.Head().MinTime())
defer func() { require.NoError(t, db.Close()) }()
@@ -3251,7 +3170,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())
}
@@ -3261,13 +3180,12 @@ func TestCompactHead(t *testing.T) {
// TestCompactHeadWithDeletion tests https://github.com/prometheus/prometheus/issues/11585.
func TestCompactHeadWithDeletion(t *testing.T) {
- db, err := Open(t.TempDir(), promslog.NewNopLogger(), prometheus.NewRegistry(), nil, nil)
- require.NoError(t, err)
+ db := newTestDB(t)
ctx := context.Background()
app := db.Appender(ctx)
- _, err = app.Append(0, labels.FromStrings("a", "b"), 10, rand.Float64())
+ _, err := app.Append(0, labels.FromStrings("a", "b"), 10, rand.Float64())
require.NoError(t, err)
require.NoError(t, app.Commit())
@@ -3276,7 +3194,6 @@ func TestCompactHeadWithDeletion(t *testing.T) {
// This recreates the bug.
require.NoError(t, db.CompactHead(NewRangeHead(db.Head(), 0, 100)))
- require.NoError(t, db.Close())
}
func deleteNonBlocks(dbDir string) error {
@@ -3386,9 +3303,7 @@ func TestOpen_VariousBlockStates(t *testing.T) {
opts := DefaultOptions()
opts.RetentionDuration = 0
- db, err := Open(tmpDir, promslog.New(&promslog.Config{}), nil, opts, nil)
- require.NoError(t, err)
-
+ db := newTestDB(t, withDir(tmpDir), withOpts(opts))
loadedBlocks := db.Blocks()
var loaded int
@@ -3421,21 +3336,16 @@ func TestOpen_VariousBlockStates(t *testing.T) {
func TestOneCheckpointPerCompactCall(t *testing.T) {
t.Parallel()
blockRange := int64(1000)
- tsdbCfg := &Options{
+ opts := &Options{
RetentionDuration: blockRange * 1000,
NoLockfile: true,
MinBlockDuration: blockRange,
MaxBlockDuration: blockRange,
}
- tmpDir := t.TempDir()
ctx := context.Background()
- db, err := Open(tmpDir, promslog.NewNopLogger(), prometheus.NewRegistry(), tsdbCfg, nil)
- require.NoError(t, err)
- t.Cleanup(func() {
- require.NoError(t, db.Close())
- })
+ db := newTestDB(t, withOpts(opts))
db.DisableCompactions()
// Case 1: Lot's of uncompacted data in Head.
@@ -3491,10 +3401,9 @@ func TestOneCheckpointPerCompactCall(t *testing.T) {
newBlockMaxt := db.Head().MaxTime() + 1
require.NoError(t, db.Close())
- createBlock(t, db.dir, genSeries(1, 1, newBlockMint, newBlockMaxt))
+ createBlock(t, db.Dir(), genSeries(1, 1, newBlockMint, newBlockMaxt))
- db, err = Open(db.dir, promslog.NewNopLogger(), prometheus.NewRegistry(), tsdbCfg, nil)
- require.NoError(t, err)
+ db = newTestDB(t, withDir(db.Dir()), withOpts(opts))
db.DisableCompactions()
// 1 block more.
@@ -3587,10 +3496,7 @@ func testQuerierShouldNotPanicIfHeadChunkIsTruncatedWhileReadingQueriedChunks(t
maxStressAllocationBytes = 512 * 1024
)
- db := openTestDB(t, nil, nil)
- defer func() {
- require.NoError(t, db.Close())
- }()
+ db := newTestDB(t)
// Disable compactions so we can control it.
db.DisableCompactions()
@@ -3723,10 +3629,7 @@ func testChunkQuerierShouldNotPanicIfHeadChunkIsTruncatedWhileReadingQueriedChun
maxStressAllocationBytes = 512 * 1024
)
- db := openTestDB(t, nil, nil)
- defer func() {
- require.NoError(t, db.Close())
- }()
+ db := newTestDB(t)
// Disable compactions so we can control it.
db.DisableCompactions()
@@ -3828,10 +3731,7 @@ func testChunkQuerierShouldNotPanicIfHeadChunkIsTruncatedWhileReadingQueriedChun
func TestQuerierShouldNotFailIfOOOCompactionOccursAfterRetrievingQuerier(t *testing.T) {
opts := DefaultOptions()
opts.OutOfOrderTimeWindow = 3 * DefaultBlockDuration
- db := openTestDB(t, opts, nil)
- defer func() {
- require.NoError(t, db.Close())
- }()
+ db := newTestDB(t, withOpts(opts))
// Disable compactions so we can control it.
db.DisableCompactions()
@@ -3922,10 +3822,7 @@ func TestQuerierShouldNotFailIfOOOCompactionOccursAfterRetrievingQuerier(t *test
func TestQuerierShouldNotFailIfOOOCompactionOccursAfterSelecting(t *testing.T) {
opts := DefaultOptions()
opts.OutOfOrderTimeWindow = 3 * DefaultBlockDuration
- db := openTestDB(t, opts, nil)
- defer func() {
- require.NoError(t, db.Close())
- }()
+ db := newTestDB(t, withOpts(opts))
// Disable compactions so we can control it.
db.DisableCompactions()
@@ -4004,10 +3901,7 @@ func TestQuerierShouldNotFailIfOOOCompactionOccursAfterSelecting(t *testing.T) {
func TestQuerierShouldNotFailIfOOOCompactionOccursAfterRetrievingIterators(t *testing.T) {
opts := DefaultOptions()
opts.OutOfOrderTimeWindow = 3 * DefaultBlockDuration
- db := openTestDB(t, opts, nil)
- defer func() {
- require.NoError(t, db.Close())
- }()
+ db := newTestDB(t, withOpts(opts))
// Disable compactions so we can control it.
db.DisableCompactions()
@@ -4083,17 +3977,6 @@ func TestQuerierShouldNotFailIfOOOCompactionOccursAfterRetrievingIterators(t *te
require.Eventually(t, compactionComplete.Load, time.Second, 10*time.Millisecond, "compaction should complete after querier was closed")
}
-func newTestDB(t *testing.T) *DB {
- dir := t.TempDir()
-
- db, err := Open(dir, nil, nil, DefaultOptions(), nil)
- require.NoError(t, err)
- t.Cleanup(func() {
- require.NoError(t, db.Close())
- })
- return db
-}
-
func TestOOOWALWrite(t *testing.T) {
minutes := func(m int64) int64 { return m * time.Minute.Milliseconds() }
@@ -4578,18 +4461,10 @@ func testOOOWALWrite(t *testing.T,
expectedOOORecords []any,
expectedInORecords []any,
) {
- dir := t.TempDir()
-
opts := DefaultOptions()
opts.OutOfOrderCapMax = 2
opts.OutOfOrderTimeWindow = 30 * time.Minute.Milliseconds()
-
- db, err := Open(dir, nil, nil, opts, nil)
- require.NoError(t, err)
-
- t.Cleanup(func() {
- require.NoError(t, db.Close())
- })
+ db := newTestDB(t, withOpts(opts))
s1, s2 := labels.FromStrings("l", "v1"), labels.FromStrings("l", "v2")
@@ -4673,21 +4548,20 @@ func testOOOWALWrite(t *testing.T,
}
// The normal WAL.
- actRecs := getRecords(path.Join(dir, "wal"))
+ actRecs := getRecords(path.Join(db.Dir(), "wal"))
require.Equal(t, expectedInORecords, actRecs)
// The WBL.
- actRecs = getRecords(path.Join(dir, wlog.WblDirName))
+ actRecs = getRecords(path.Join(db.Dir(), wlog.WblDirName))
require.Equal(t, expectedOOORecords, actRecs)
}
// Tests https://github.com/prometheus/prometheus/issues/10291#issuecomment-1044373110.
func TestDBPanicOnMmappingHeadChunk(t *testing.T) {
- dir := t.TempDir()
+ var err error
ctx := context.Background()
- db, err := Open(dir, nil, nil, DefaultOptions(), nil)
- require.NoError(t, err)
+ db := newTestDB(t)
db.DisableCompactions()
// Choosing scrape interval of 45s to have chunk larger than 1h.
@@ -4721,8 +4595,7 @@ func TestDBPanicOnMmappingHeadChunk(t *testing.T) {
// Restarting.
require.NoError(t, db.Close())
- db, err = Open(dir, nil, nil, DefaultOptions(), nil)
- require.NoError(t, err)
+ db = newTestDB(t, withDir(db.Dir()))
db.DisableCompactions()
// Ingest samples upto 20m more to make the head compact.
@@ -4915,7 +4788,7 @@ func TestMetadataAssertInMemoryData(t *testing.T) {
require.NoError(t, err)
}
- db := openTestDB(t, nil, nil)
+ db := newTestDB(t)
ctx := context.Background()
// Add some series so we can append metadata to them.
@@ -4976,19 +4849,14 @@ func TestMetadataAssertInMemoryData(t *testing.T) {
// Reopen the DB, replaying the WAL. The Head must have been replayed
// correctly in memory.
- reopenDB, err := Open(db.Dir(), nil, nil, nil, nil)
- require.NoError(t, err)
- t.Cleanup(func() {
- require.NoError(t, reopenDB.Close())
- })
-
- _, err = reopenDB.head.wal.Size()
+ db = newTestDB(t, withDir(db.Dir()))
+ _, err := db.head.wal.Size()
require.NoError(t, err)
- require.Equal(t, *reopenDB.head.series.getByHash(s1.Hash(), s1).meta, m1)
- require.Equal(t, *reopenDB.head.series.getByHash(s2.Hash(), s2).meta, m5)
- require.Equal(t, *reopenDB.head.series.getByHash(s3.Hash(), s3).meta, m3)
- require.Equal(t, *reopenDB.head.series.getByHash(s4.Hash(), s4).meta, m4)
+ require.Equal(t, *db.head.series.getByHash(s1.Hash(), s1).meta, m1)
+ require.Equal(t, *db.head.series.getByHash(s2.Hash(), s2).meta, m5)
+ require.Equal(t, *db.head.series.getByHash(s3.Hash(), s3).meta, m3)
+ require.Equal(t, *db.head.series.getByHash(s4.Hash(), s4).meta, m4)
}
// TestMultipleEncodingsCommitOrder mainly serves to demonstrate when happens when committing a batch of samples for the
@@ -4998,14 +4866,10 @@ func TestMultipleEncodingsCommitOrder(t *testing.T) {
opts.OutOfOrderCapMax = 30
opts.OutOfOrderTimeWindow = 24 * time.Hour.Milliseconds()
- series1 := labels.FromStrings("foo", "bar1")
-
- db := openTestDB(t, opts, nil)
+ db := newTestDB(t, withOpts(opts))
db.DisableCompactions()
- defer func() {
- require.NoError(t, db.Close())
- }()
+ series1 := labels.FromStrings("foo", "bar1")
addSample := func(app storage.Appender, ts int64, valType chunkenc.ValueType) chunks.Sample {
if valType == chunkenc.ValFloat {
_, err := app.Append(0, labels.FromStrings("foo", "bar1"), ts, float64(ts))
@@ -5148,19 +5012,13 @@ func TestOOOCompaction(t *testing.T) {
}
func testOOOCompaction(t *testing.T, scenario sampleTypeScenario, addExtraSamples bool) {
- dir := t.TempDir()
ctx := context.Background()
opts := DefaultOptions()
opts.OutOfOrderCapMax = 30
opts.OutOfOrderTimeWindow = 300 * time.Minute.Milliseconds()
-
- db, err := Open(dir, nil, nil, opts, nil)
- require.NoError(t, err)
+ db := newTestDB(t, withOpts(opts))
db.DisableCompactions() // We want to manually call it.
- t.Cleanup(func() {
- require.NoError(t, db.Close())
- })
series1 := labels.FromStrings("foo", "bar1")
series2 := labels.FromStrings("foo", "bar2")
@@ -5351,19 +5209,14 @@ func TestOOOCompactionWithNormalCompaction(t *testing.T) {
func testOOOCompactionWithNormalCompaction(t *testing.T, scenario sampleTypeScenario) {
t.Parallel()
- dir := t.TempDir()
ctx := context.Background()
opts := DefaultOptions()
opts.OutOfOrderCapMax = 30
opts.OutOfOrderTimeWindow = 300 * time.Minute.Milliseconds()
- db, err := Open(dir, nil, nil, opts, nil)
- require.NoError(t, err)
+ db := newTestDB(t, withOpts(opts))
db.DisableCompactions() // We want to manually call it.
- t.Cleanup(func() {
- require.NoError(t, db.Close())
- })
series1 := labels.FromStrings("foo", "bar1")
series2 := labels.FromStrings("foo", "bar2")
@@ -5461,7 +5314,6 @@ func TestOOOCompactionWithDisabledWriteLog(t *testing.T) {
func testOOOCompactionWithDisabledWriteLog(t *testing.T, scenario sampleTypeScenario) {
t.Parallel()
- dir := t.TempDir()
ctx := context.Background()
opts := DefaultOptions()
@@ -5469,12 +5321,8 @@ func testOOOCompactionWithDisabledWriteLog(t *testing.T, scenario sampleTypeScen
opts.OutOfOrderTimeWindow = 300 * time.Minute.Milliseconds()
opts.WALSegmentSize = -1 // disabled WAL and WBL
- db, err := Open(dir, nil, nil, opts, nil)
- require.NoError(t, err)
+ db := newTestDB(t, withOpts(opts))
db.DisableCompactions() // We want to manually call it.
- t.Cleanup(func() {
- require.NoError(t, db.Close())
- })
series1 := labels.FromStrings("foo", "bar1")
series2 := labels.FromStrings("foo", "bar2")
@@ -5571,7 +5419,6 @@ func TestOOOQueryAfterRestartWithSnapshotAndRemovedWBL(t *testing.T) {
}
func testOOOQueryAfterRestartWithSnapshotAndRemovedWBL(t *testing.T, scenario sampleTypeScenario) {
- dir := t.TempDir()
ctx := context.Background()
opts := DefaultOptions()
@@ -5579,12 +5426,8 @@ func testOOOQueryAfterRestartWithSnapshotAndRemovedWBL(t *testing.T, scenario sa
opts.OutOfOrderTimeWindow = 300 * time.Minute.Milliseconds()
opts.EnableMemorySnapshotOnShutdown = true
- db, err := Open(dir, nil, nil, opts, nil)
- require.NoError(t, err)
+ db := newTestDB(t, withOpts(opts))
db.DisableCompactions() // We want to manually call it.
- t.Cleanup(func() {
- require.NoError(t, db.Close())
- })
series1 := labels.FromStrings("foo", "bar1")
series2 := labels.FromStrings("foo", "bar2")
@@ -5620,10 +5463,9 @@ func testOOOQueryAfterRestartWithSnapshotAndRemovedWBL(t *testing.T, scenario sa
require.NoError(t, db.Close())
// For some reason wbl goes missing.
- require.NoError(t, os.RemoveAll(path.Join(dir, "wbl")))
+ require.NoError(t, os.RemoveAll(path.Join(db.Dir(), "wbl")))
- db, err = Open(dir, nil, nil, opts, nil)
- require.NoError(t, err)
+ db = newTestDB(t, withDir(db.Dir()))
db.DisableCompactions() // We want to manually call it.
// Check ooo m-map chunks again.
@@ -5940,11 +5782,8 @@ func testQuerierOOOQuery(t *testing.T,
for _, tc := range tests {
t.Run(fmt.Sprintf("name=%s", tc.name), func(t *testing.T) {
opts.OutOfOrderCapMax = tc.oooCap
- db := openTestDB(t, opts, nil)
+ db := newTestDB(t, withOpts(opts))
db.DisableCompactions()
- defer func() {
- require.NoError(t, db.Close())
- }()
var expSamples []chunks.Sample
var oooSamples, appendedCount int
@@ -6269,11 +6108,8 @@ func testChunkQuerierOOOQuery(t *testing.T,
for _, tc := range tests {
t.Run(fmt.Sprintf("name=%s", tc.name), func(t *testing.T) {
opts.OutOfOrderCapMax = tc.oooCap
- db := openTestDB(t, opts, nil)
+ db := newTestDB(t, withOpts(opts))
db.DisableCompactions()
- defer func() {
- require.NoError(t, db.Close())
- }()
var expSamples []chunks.Sample
var oooSamples, appendedCount int
@@ -6449,11 +6285,8 @@ func testOOONativeHistogramsWithCounterResets(t *testing.T, scenario sampleTypeS
}
for _, tc := range tests {
t.Run(fmt.Sprintf("name=%s", tc.name), func(t *testing.T) {
- db := openTestDB(t, opts, nil)
+ db := newTestDB(t, withOpts(opts))
db.DisableCompactions()
- defer func() {
- require.NoError(t, db.Close())
- }()
app := db.Appender(context.Background())
@@ -6686,11 +6519,8 @@ func testOOOInterleavedImplicitCounterResets(t *testing.T, name string, scenario
opts.OutOfOrderCapMax = tc.oooCap
opts.OutOfOrderTimeWindow = 24 * time.Hour.Milliseconds()
- db := openTestDB(t, opts, nil)
+ db := newTestDB(t, withOpts(opts))
db.DisableCompactions()
- defer func() {
- require.NoError(t, db.Close())
- }()
app := db.Appender(context.Background())
for _, s := range tc.samples {
@@ -6787,11 +6617,8 @@ func testOOOAppendAndQuery(t *testing.T, scenario sampleTypeScenario) {
opts.OutOfOrderCapMax = 30
opts.OutOfOrderTimeWindow = 4 * time.Hour.Milliseconds()
- db := openTestDB(t, opts, nil)
+ db := newTestDB(t, withOpts(opts))
db.DisableCompactions()
- t.Cleanup(func() {
- require.NoError(t, db.Close())
- })
s1 := labels.FromStrings("foo", "bar1")
s2 := labels.FromStrings("foo", "bar2")
@@ -6918,11 +6745,8 @@ func TestOOODisabled(t *testing.T) {
func testOOODisabled(t *testing.T, scenario sampleTypeScenario) {
opts := DefaultOptions()
opts.OutOfOrderTimeWindow = 0
- db := openTestDB(t, opts, nil)
+ db := newTestDB(t, withOpts(opts))
db.DisableCompactions()
- t.Cleanup(func() {
- require.NoError(t, db.Close())
- })
s1 := labels.FromStrings("foo", "bar1")
minutes := func(m int64) int64 { return m * time.Minute.Milliseconds() }
@@ -6993,11 +6817,8 @@ func testWBLAndMmapReplay(t *testing.T, scenario sampleTypeScenario) {
opts.OutOfOrderCapMax = 30
opts.OutOfOrderTimeWindow = 4 * time.Hour.Milliseconds()
- db := openTestDB(t, opts, nil)
+ db := newTestDB(t, withOpts(opts))
db.DisableCompactions()
- t.Cleanup(func() {
- require.NoError(t, db.Close())
- })
s1 := labels.FromStrings("foo", "bar1")
@@ -7078,38 +6899,32 @@ func testWBLAndMmapReplay(t *testing.T, scenario sampleTypeScenario) {
}
t.Run("Restart DB with both WBL and M-map files for ooo data", func(t *testing.T) {
- db, err = Open(db.dir, nil, nil, opts, nil)
- require.NoError(t, err)
+ db = newTestDB(t, withDir(db.Dir()), withOpts(opts))
require.Equal(t, oooMint, db.head.MinOOOTime())
require.Equal(t, oooMaxt, db.head.MaxOOOTime())
testQuery(expSamples)
- require.NoError(t, db.Close())
})
t.Run("Restart DB with only WBL for ooo data", func(t *testing.T) {
require.NoError(t, os.RemoveAll(mmapDir))
- db, err = Open(db.dir, nil, nil, opts, nil)
- require.NoError(t, err)
+ db = newTestDB(t, withDir(db.Dir()), withOpts(opts))
require.Equal(t, oooMint, db.head.MinOOOTime())
require.Equal(t, oooMaxt, db.head.MaxOOOTime())
testQuery(expSamples)
- require.NoError(t, db.Close())
})
t.Run("Restart DB with only M-map files for ooo data", func(t *testing.T) {
require.NoError(t, os.RemoveAll(wblDir))
resetMmapToOriginal()
- db, err = Open(db.dir, nil, nil, opts, nil)
- require.NoError(t, err)
+ db = newTestDB(t, withDir(db.Dir()), withOpts(opts))
require.Equal(t, oooMint, db.head.MinOOOTime())
require.Equal(t, oooMaxt, db.head.MaxOOOTime())
inOrderSample := expSamples[s1.String()][len(expSamples[s1.String()])-1]
testQuery(map[string][]chunks.Sample{
s1.String(): append(s1MmapSamples, inOrderSample),
})
- require.NoError(t, db.Close())
})
t.Run("Restart DB with WBL+Mmap while increasing the OOOCapMax", func(t *testing.T) {
@@ -7117,24 +6932,22 @@ func testWBLAndMmapReplay(t *testing.T, scenario sampleTypeScenario) {
resetMmapToOriginal()
opts.OutOfOrderCapMax = 60
- db, err = Open(db.dir, nil, nil, opts, nil)
+ db = newTestDB(t, withDir(db.Dir()), withOpts(opts))
require.NoError(t, err)
require.Equal(t, oooMint, db.head.MinOOOTime())
require.Equal(t, oooMaxt, db.head.MaxOOOTime())
testQuery(expSamples)
- require.NoError(t, db.Close())
})
t.Run("Restart DB with WBL+Mmap while decreasing the OOOCapMax", func(t *testing.T) {
resetMmapToOriginal() // We need to reset because new duplicate chunks can be written above.
opts.OutOfOrderCapMax = 10
- db, err = Open(db.dir, nil, nil, opts, nil)
+ db = newTestDB(t, withDir(db.Dir()), withOpts(opts))
require.NoError(t, err)
require.Equal(t, oooMint, db.head.MinOOOTime())
require.Equal(t, oooMaxt, db.head.MaxOOOTime())
testQuery(expSamples)
- require.NoError(t, db.Close())
})
t.Run("Restart DB with WBL+Mmap while having no m-map markers in WBL", func(t *testing.T) {
@@ -7164,7 +6977,7 @@ func testWBLAndMmapReplay(t *testing.T, scenario sampleTypeScenario) {
require.NoError(t, os.Rename(newWbl.Dir(), wblDir))
opts.OutOfOrderCapMax = 30
- db, err = Open(db.dir, nil, nil, opts, nil)
+ db = newTestDB(t, withDir(db.Dir()), withOpts(opts))
require.NoError(t, err)
require.Equal(t, oooMint, db.head.MinOOOTime())
require.Equal(t, oooMaxt, db.head.MaxOOOTime())
@@ -7174,19 +6987,14 @@ func testWBLAndMmapReplay(t *testing.T, scenario sampleTypeScenario) {
func TestOOOHistogramCompactionWithCounterResets(t *testing.T) {
for _, floatHistogram := range []bool{false, true} {
- dir := t.TempDir()
ctx := context.Background()
opts := DefaultOptions()
opts.OutOfOrderCapMax = 30
opts.OutOfOrderTimeWindow = 500 * time.Minute.Milliseconds()
- db, err := Open(dir, nil, nil, opts, nil)
- require.NoError(t, err)
+ db := newTestDB(t, withOpts(opts))
db.DisableCompactions() // We want to manually call it.
- t.Cleanup(func() {
- require.NoError(t, db.Close())
- })
series1 := labels.FromStrings("foo", "bar1")
series2 := labels.FromStrings("foo", "bar2")
@@ -7199,7 +7007,7 @@ func TestOOOHistogramCompactionWithCounterResets(t *testing.T) {
if floatHistogram {
h := tsdbutil.GenerateTestFloatHistogram(int64(val))
h.CounterResetHint = hint
- _, err = app.AppendHistogram(0, l, tsMs, nil, h)
+ _, err := app.AppendHistogram(0, l, tsMs, nil, h)
require.NoError(t, err)
require.NoError(t, app.Commit())
return sample{t: tsMs, fh: h.Copy()}
@@ -7207,7 +7015,7 @@ func TestOOOHistogramCompactionWithCounterResets(t *testing.T) {
h := tsdbutil.GenerateTestHistogram(int64(val))
h.CounterResetHint = hint
- _, err = app.AppendHistogram(0, l, tsMs, h, nil)
+ _, err := app.AppendHistogram(0, l, tsMs, h, nil)
require.NoError(t, err)
require.NoError(t, app.Commit())
return sample{t: tsMs, h: h.Copy()}
@@ -7534,19 +7342,14 @@ func TestOOOHistogramCompactionWithCounterResets(t *testing.T) {
func TestInterleavedInOrderAndOOOHistogramCompactionWithCounterResets(t *testing.T) {
for _, floatHistogram := range []bool{false, true} {
- dir := t.TempDir()
ctx := context.Background()
opts := DefaultOptions()
opts.OutOfOrderCapMax = 30
opts.OutOfOrderTimeWindow = 500 * time.Minute.Milliseconds()
- db, err := Open(dir, nil, nil, opts, nil)
- require.NoError(t, err)
+ db := newTestDB(t, withOpts(opts))
db.DisableCompactions() // We want to manually call it.
- t.Cleanup(func() {
- require.NoError(t, db.Close())
- })
series1 := labels.FromStrings("foo", "bar1")
@@ -7555,14 +7358,14 @@ func TestInterleavedInOrderAndOOOHistogramCompactionWithCounterResets(t *testing
tsMs := ts
if floatHistogram {
h := tsdbutil.GenerateTestFloatHistogram(int64(val))
- _, err = app.AppendHistogram(0, l, tsMs, nil, h)
+ _, err := app.AppendHistogram(0, l, tsMs, nil, h)
require.NoError(t, err)
require.NoError(t, app.Commit())
return sample{t: tsMs, fh: h.Copy()}
}
h := tsdbutil.GenerateTestHistogram(int64(val))
- _, err = app.AppendHistogram(0, l, tsMs, h, nil)
+ _, err := app.AppendHistogram(0, l, tsMs, h, nil)
require.NoError(t, err)
require.NoError(t, app.Commit())
return sample{t: tsMs, h: h.Copy()}
@@ -7610,8 +7413,7 @@ func TestInterleavedInOrderAndOOOHistogramCompactionWithCounterResets(t *testing
// Compact the in-order head and expect another block.
// Since this is a forced compaction, this block is not aligned with 2h.
- err = db.CompactHead(NewRangeHead(db.head, 0, 3))
- require.NoError(t, err)
+ require.NoError(t, db.CompactHead(NewRangeHead(db.head, 0, 3)))
require.Len(t, db.Blocks(), 2)
// Blocks created out of normal and OOO head now. But not merged.
@@ -7649,19 +7451,13 @@ func TestOOOCompactionFailure(t *testing.T) {
}
func testOOOCompactionFailure(t *testing.T, scenario sampleTypeScenario) {
- dir := t.TempDir()
ctx := context.Background()
opts := DefaultOptions()
opts.OutOfOrderCapMax = 30
opts.OutOfOrderTimeWindow = 300 * time.Minute.Milliseconds()
-
- db, err := Open(dir, nil, nil, opts, nil)
- require.NoError(t, err)
+ db := newTestDB(t, withOpts(opts))
db.DisableCompactions() // We want to manually call it.
- t.Cleanup(func() {
- require.NoError(t, db.Close())
- })
series1 := labels.FromStrings("foo", "bar1")
@@ -7787,18 +7583,11 @@ func testOOOCompactionFailure(t *testing.T, scenario sampleTypeScenario) {
}
func TestWBLCorruption(t *testing.T) {
- dir := t.TempDir()
-
opts := DefaultOptions()
opts.OutOfOrderCapMax = 30
opts.OutOfOrderTimeWindow = 300 * time.Minute.Milliseconds()
- db, err := Open(dir, nil, nil, opts, nil)
- require.NoError(t, err)
- db.DisableCompactions()
- t.Cleanup(func() {
- require.NoError(t, db.Close())
- })
+ db := newTestDB(t, withOpts(opts))
series1 := labels.FromStrings("foo", "bar1")
var allSamples, expAfterRestart []chunks.Sample
@@ -7825,7 +7614,7 @@ func TestWBLCorruption(t *testing.T) {
addSamples(120, 130, true)
// Moving onto the second file.
- _, err = db.head.wbl.NextSegment()
+ _, err := db.head.wbl.NextSegment()
require.NoError(t, err)
// More OOO samples.
@@ -7897,7 +7686,7 @@ func TestWBLCorruption(t *testing.T) {
require.NoError(t, os.RemoveAll(mmappedChunksDir(db.head.opts.ChunkDirRoot)))
// Restart does the replay and repair.
- db, err = Open(db.dir, nil, nil, opts, nil)
+ db = newTestDB(t, withDir(db.Dir()), withOpts(opts))
require.NoError(t, err)
require.Equal(t, 1.0, prom_testutil.ToFloat64(db.head.metrics.walCorruptionsTotal))
require.Less(t, len(expAfterRestart), len(allSamples))
@@ -7926,7 +7715,7 @@ func TestWBLCorruption(t *testing.T) {
// Another restart, everything normal with no repair.
require.NoError(t, db.Close())
- db, err = Open(db.dir, nil, nil, opts, nil)
+ db = newTestDB(t, withDir(db.Dir()), withOpts(opts))
require.NoError(t, err)
require.Equal(t, 0.0, prom_testutil.ToFloat64(db.head.metrics.walCorruptionsTotal))
verifySamples(expAfterRestart)
@@ -7941,18 +7730,11 @@ func TestOOOMmapCorruption(t *testing.T) {
}
func testOOOMmapCorruption(t *testing.T, scenario sampleTypeScenario) {
- dir := t.TempDir()
-
opts := DefaultOptions()
opts.OutOfOrderCapMax = 10
opts.OutOfOrderTimeWindow = 300 * time.Minute.Milliseconds()
- db, err := Open(dir, nil, nil, opts, nil)
- require.NoError(t, err)
- db.DisableCompactions()
- t.Cleanup(func() {
- require.NoError(t, db.Close())
- })
+ db := newTestDB(t, withOpts(opts))
series1 := labels.FromStrings("foo", "bar1")
var allSamples, expInMmapChunks []chunks.Sample
@@ -8029,7 +7811,7 @@ func testOOOMmapCorruption(t *testing.T, scenario sampleTypeScenario) {
require.NoError(t, os.Rename(wblDir, wblDirTmp))
// Restart does the replay and repair of m-map files.
- db, err = Open(db.dir, nil, nil, opts, nil)
+ db = newTestDB(t, withDir(db.Dir()), withOpts(opts))
require.NoError(t, err)
require.Equal(t, 1.0, prom_testutil.ToFloat64(db.head.metrics.mmapChunkCorruptionTotal))
require.Less(t, len(expInMmapChunks), len(allSamples))
@@ -8049,7 +7831,7 @@ func testOOOMmapCorruption(t *testing.T, scenario sampleTypeScenario) {
// Another restart, everything normal with no repair.
require.NoError(t, db.Close())
- db, err = Open(db.dir, nil, nil, opts, nil)
+ db = newTestDB(t, withDir(db.Dir()), withOpts(opts))
require.NoError(t, err)
require.Equal(t, 0.0, prom_testutil.ToFloat64(db.head.metrics.mmapChunkCorruptionTotal))
verifySamples(expInMmapChunks)
@@ -8058,7 +7840,7 @@ func testOOOMmapCorruption(t *testing.T, scenario sampleTypeScenario) {
require.NoError(t, db.Close())
require.NoError(t, os.RemoveAll(wblDir))
require.NoError(t, os.Rename(wblDirTmp, wblDir))
- db, err = Open(db.dir, nil, nil, opts, nil)
+ db = newTestDB(t, withDir(db.Dir()), withOpts(opts))
require.NoError(t, err)
verifySamples(allSamples)
}
@@ -8076,18 +7858,10 @@ func testOutOfOrderRuntimeConfig(t *testing.T, scenario sampleTypeScenario) {
ctx := context.Background()
getDB := func(oooTimeWindow int64) *DB {
- dir := t.TempDir()
-
opts := DefaultOptions()
opts.OutOfOrderTimeWindow = oooTimeWindow
-
- db, err := Open(dir, nil, nil, opts, nil)
- require.NoError(t, err)
+ db := newTestDB(t, withOpts(opts))
db.DisableCompactions()
- t.Cleanup(func() {
- require.NoError(t, db.Close())
- })
-
return db
}
@@ -8369,18 +8143,12 @@ func testNoGapAfterRestartWithOOO(t *testing.T, scenario sampleTypeScenario) {
for i, c := range cases {
t.Run(fmt.Sprintf("case=%d", i), func(t *testing.T) {
- dir := t.TempDir()
ctx := context.Background()
opts := DefaultOptions()
opts.OutOfOrderTimeWindow = 30 * time.Minute.Milliseconds()
-
- db, err := Open(dir, nil, nil, opts, nil)
- require.NoError(t, err)
+ db := newTestDB(t, withOpts(opts))
db.DisableCompactions()
- t.Cleanup(func() {
- require.NoError(t, db.Close())
- })
// 3h10m=190m worth in-order data.
addSamples(t, db, c.inOrderMint, c.inOrderMaxt, true)
@@ -8407,8 +8175,7 @@ func testNoGapAfterRestartWithOOO(t *testing.T, scenario sampleTypeScenario) {
// Restart and expect all samples to be present.
require.NoError(t, db.Close())
- db, err = Open(dir, nil, nil, opts, nil)
- require.NoError(t, err)
+ db = newTestDB(t, withDir(db.Dir()), withOpts(opts))
db.DisableCompactions()
verifyBlockRanges()
@@ -8428,17 +8195,10 @@ func TestWblReplayAfterOOODisableAndRestart(t *testing.T) {
}
func testWblReplayAfterOOODisableAndRestart(t *testing.T, scenario sampleTypeScenario) {
- dir := t.TempDir()
-
opts := DefaultOptions()
opts.OutOfOrderTimeWindow = 60 * time.Minute.Milliseconds()
- db, err := Open(dir, nil, nil, opts, nil)
- require.NoError(t, err)
- db.DisableCompactions()
- t.Cleanup(func() {
- require.NoError(t, db.Close())
- })
+ db := newTestDB(t, withOpts(opts))
series1 := labels.FromStrings("foo", "bar1")
var allSamples []chunks.Sample
@@ -8478,9 +8238,9 @@ func testWblReplayAfterOOODisableAndRestart(t *testing.T, scenario sampleTypeSce
// Restart DB with OOO disabled.
require.NoError(t, db.Close())
+
opts.OutOfOrderTimeWindow = 0
- db, err = Open(db.dir, nil, nil, opts, nil)
- require.NoError(t, err)
+ db = newTestDB(t, withDir(db.Dir()), withOpts(opts))
// We can still query OOO samples when OOO is disabled.
verifySamples(allSamples)
@@ -8495,17 +8255,10 @@ func TestPanicOnApplyConfig(t *testing.T) {
}
func testPanicOnApplyConfig(t *testing.T, scenario sampleTypeScenario) {
- dir := t.TempDir()
-
opts := DefaultOptions()
opts.OutOfOrderTimeWindow = 60 * time.Minute.Milliseconds()
- db, err := Open(dir, nil, nil, opts, nil)
- require.NoError(t, err)
- db.DisableCompactions()
- t.Cleanup(func() {
- require.NoError(t, db.Close())
- })
+ db := newTestDB(t, withOpts(opts))
series1 := labels.FromStrings("foo", "bar1")
var allSamples []chunks.Sample
@@ -8527,12 +8280,12 @@ func testPanicOnApplyConfig(t *testing.T, scenario sampleTypeScenario) {
// Restart DB with OOO disabled.
require.NoError(t, db.Close())
+
opts.OutOfOrderTimeWindow = 0
- db, err = Open(db.dir, nil, prometheus.NewRegistry(), opts, nil)
- require.NoError(t, err)
+ db = newTestDB(t, withDir(db.Dir()), withOpts(opts))
// ApplyConfig with OOO enabled and expect no panic.
- err = db.ApplyConfig(&config.Config{
+ err := db.ApplyConfig(&config.Config{
StorageConfig: config.StorageConfig{
TSDBConfig: &config.TSDBConfig{
OutOfOrderTimeWindow: 60 * time.Minute.Milliseconds(),
@@ -8544,32 +8297,37 @@ func testPanicOnApplyConfig(t *testing.T, scenario sampleTypeScenario) {
func TestDiskFillingUpAfterDisablingOOO(t *testing.T) {
t.Parallel()
- for name, scenario := range sampleTypeScenarios {
- t.Run(name, func(t *testing.T) {
- testDiskFillingUpAfterDisablingOOO(t, scenario)
- })
+ for _, appV2 := range []bool{true, false} {
+ for name, scenario := range sampleTypeScenarios {
+ t.Run(fmt.Sprintf("sample=%v/appV2=%v", name, appV2), func(t *testing.T) {
+ testDiskFillingUpAfterDisablingOOO(t, scenario, func(db *DB, ctx context.Context) storage.LimitedAppenderV1 {
+ if appV2 {
+ return storage.AppenderV2AsLimitedV1(db.AppenderV2(ctx))
+ }
+ return db.Appender(ctx)
+ })
+ })
+ }
}
}
-func testDiskFillingUpAfterDisablingOOO(t *testing.T, scenario sampleTypeScenario) {
+func testDiskFillingUpAfterDisablingOOO(t *testing.T, scenario sampleTypeScenario, appenderFn func(db *DB, ctx context.Context) storage.LimitedAppenderV1) {
t.Parallel()
- dir := t.TempDir()
- ctx := context.Background()
opts := DefaultOptions()
opts.OutOfOrderTimeWindow = 60 * time.Minute.Milliseconds()
- db, err := Open(dir, nil, nil, opts, nil)
- require.NoError(t, err)
+ db := newTestDB(t, withOpts(opts))
db.DisableCompactions()
- t.Cleanup(func() {
- require.NoError(t, db.Close())
- })
- series1 := labels.FromStrings("foo", "bar1")
- var allSamples []chunks.Sample
+ var (
+ ctx = t.Context()
+ series1 = labels.FromStrings("foo", "bar1")
+ allSamples []chunks.Sample
+ )
+
addSamples := func(fromMins, toMins int64) {
- app := db.Appender(context.Background())
+ app := appenderFn(db, ctx)
for m := fromMins; m <= toMins; m++ {
ts := m * time.Minute.Milliseconds()
_, s, err := scenario.appendFunc(app, series1, ts, ts)
@@ -8586,9 +8344,9 @@ func testDiskFillingUpAfterDisablingOOO(t *testing.T, scenario sampleTypeScenari
// Restart DB with OOO disabled.
require.NoError(t, db.Close())
+
opts.OutOfOrderTimeWindow = 0
- db, err = Open(db.dir, nil, prometheus.NewRegistry(), opts, nil)
- require.NoError(t, err)
+ db = newTestDB(t, withDir(db.Dir()), withOpts(opts))
db.DisableCompactions()
ms := db.head.series.getByHash(series1.Hash(), series1)
@@ -8612,21 +8370,36 @@ func testDiskFillingUpAfterDisablingOOO(t *testing.T, scenario sampleTypeScenari
}
}
- // Add in-order samples until ready for compaction..
+ // Add in-order samples until ready for compaction.
addSamples(301, 500)
// Check that m-map files gets deleted properly after compactions.
db.head.mmapHeadChunks()
checkMmapFileContents([]string{"000001", "000002"}, nil)
- require.NoError(t, db.Compact(ctx))
+
+ // NOTE: We are investigating flaky errors from this compaction on i386 architecture. Compaction panics due to chunk
+ // mapper fatal error. Recover here to understand the error cause. Leaving panic recovery to test causes deadlock
+ // as t.Cleanup tries to close DB with open locks.
+ // See https://github.com/prometheus/prometheus/issues/17941#issuecomment-3846381263
+ require.NotPanics(t, func() {
+ require.NoError(t, db.Compact(ctx))
+ })
+
checkMmapFileContents([]string{"000002"}, []string{"000001"})
require.Nil(t, ms.ooo, "OOO mmap chunk was not compacted")
addSamples(501, 650)
db.head.mmapHeadChunks()
checkMmapFileContents([]string{"000002", "000003"}, []string{"000001"})
- require.NoError(t, db.Compact(ctx))
+
+ // NOTE: We are investigating flaky errors from this compaction on i386 architecture. Compaction panics due to chunk
+ // mapper fatal error. Recover here to understand the error cause. Leaving panic recovery to test causes deadlock
+ // as t.Cleanup tries to close DB with open locks.
+ // See https://github.com/prometheus/prometheus/issues/17941#issuecomment-3846381263
+ require.NotPanics(t, func() {
+ require.NoError(t, db.Compact(ctx))
+ })
checkMmapFileContents(nil, []string{"000001", "000002", "000003"})
// Verify that WBL is empty.
@@ -8649,11 +8422,8 @@ func TestHistogramAppendAndQuery(t *testing.T) {
func testHistogramAppendAndQueryHelper(t *testing.T, floatHistogram bool) {
t.Helper()
- db := openTestDB(t, nil, nil)
+ db := newTestDB(t)
minute := func(m int) int64 { return int64(m) * time.Minute.Milliseconds() }
- t.Cleanup(func() {
- require.NoError(t, db.Close())
- })
ctx := context.Background()
appendHistogram := func(t *testing.T,
@@ -8920,10 +8690,7 @@ func TestQueryHistogramFromBlocksWithCompaction(t *testing.T) {
t.Helper()
opts := DefaultOptions()
- db := openTestDB(t, opts, nil)
- t.Cleanup(func() {
- require.NoError(t, db.Close())
- })
+ db := newTestDB(t, withOpts(opts))
var it chunkenc.Iterator
exp := make(map[string][]chunks.Sample)
@@ -9066,10 +8833,7 @@ func TestOOONativeHistogramsSettings(t *testing.T) {
t.Run("Test OOO native histograms if OOO is disabled and Native Histograms is enabled", func(t *testing.T) {
opts := DefaultOptions()
opts.OutOfOrderTimeWindow = 0
- db := openTestDB(t, opts, []int64{100})
- defer func() {
- require.NoError(t, db.Close())
- }()
+ db := newTestDB(t, withOpts(opts), withRngs(100))
app := db.Appender(context.Background())
_, err := app.AppendHistogram(0, l, 100, h, nil)
@@ -9090,10 +8854,7 @@ func TestOOONativeHistogramsSettings(t *testing.T) {
t.Run("Test OOO native histograms when both OOO and Native Histograms are enabled", func(t *testing.T) {
opts := DefaultOptions()
opts.OutOfOrderTimeWindow = 100
- db := openTestDB(t, opts, []int64{100})
- defer func() {
- require.NoError(t, db.Close())
- }()
+ db := newTestDB(t, withOpts(opts), withRngs(100))
// Add in-order samples
app := db.Appender(context.Background())
@@ -9183,10 +8944,7 @@ func compareSeries(t require.TestingT, expected, actual map[string][]chunks.Samp
// worrying about the parallel write.
func TestChunkQuerierReadWriteRace(t *testing.T) {
t.Parallel()
- db := openTestDB(t, nil, nil)
- defer func() {
- require.NoError(t, db.Close())
- }()
+ db := newTestDB(t)
lbls := labels.FromStrings("foo", "bar")
@@ -9272,10 +9030,7 @@ func (c *mockCompactorFn) Write(string, BlockReader, int64, int64, *BlockMeta) (
// Regression test for https://github.com/prometheus/prometheus/pull/13754
func TestAbortBlockCompactions(t *testing.T) {
// Create a test DB
- db := openTestDB(t, nil, nil)
- defer func() {
- require.NoError(t, db.Close())
- }()
+ db := newTestDB(t)
// It should NOT be compactable at the beginning of the test
require.False(t, db.head.compactable(), "head should NOT be compactable")
@@ -9328,10 +9083,8 @@ func TestNewCompactorFunc(t *testing.T) {
},
}, nil
}
- db := openTestDB(t, opts, nil)
- defer func() {
- require.NoError(t, db.Close())
- }()
+ db := newTestDB(t, withOpts(opts))
+
plans, err := db.compactor.Plan("")
require.NoError(t, err)
require.Equal(t, []string{block1.String(), block2.String()}, plans)
@@ -9362,10 +9115,7 @@ func TestBlockQuerierAndBlockChunkQuerier(t *testing.T) {
return storage.NoopChunkedQuerier(), nil
}
- db := openTestDB(t, opts, nil)
- defer func() {
- require.NoError(t, db.Close())
- }()
+ db := newTestDB(t, withOpts(opts))
metas := []BlockMeta{
{Compaction: BlockMetaCompaction{Hints: []string{"test-hint"}}},
@@ -9450,10 +9200,8 @@ func TestGenerateCompactionDelay(t *testing.T) {
for _, c := range cases {
opts.CompactionDelayMaxPercent = c.compactionDelayPercent
- db := openTestDB(t, opts, []int64{60000})
- defer func() {
- require.NoError(t, db.Close())
- }()
+ db := newTestDB(t, withOpts(opts), withRngs(60000))
+
// The offset is generated and changed while opening.
assertDelay(db.opts.CompactionDelay, c.compactionDelayPercent)
@@ -9496,15 +9244,17 @@ func TestBlockClosingBlockedDuringRemoteRead(t *testing.T) {
dir := t.TempDir()
createBlock(t, dir, genSeries(2, 1, 0, 10))
+
+ // Not using newTestDB as db.Close is expected to return error.
db, err := Open(dir, nil, nil, nil, nil)
require.NoError(t, err)
- // No error checking as manually closing the block is supposed to make this fail.
defer db.Close()
- readAPI := remote.NewReadHandler(nil, nil, db, func() config.Config {
- return config.Config{}
- },
- 0, 1, 0,
+ readAPI := remote.NewReadHandler(
+ nil, nil, db,
+ func() config.Config {
+ return config.Config{}
+ }, 0, 1, 0,
)
matcher, err := labels.NewMatcher(labels.MatchRegexp, "__name__", ".*")
@@ -9569,3 +9319,287 @@ func TestBlockClosingBlockedDuringRemoteRead(t *testing.T) {
case <-blockClosed:
}
}
+
+func TestBlockReloadInterval(t *testing.T) {
+ t.Parallel()
+
+ cases := []struct {
+ name string
+ reloadInterval time.Duration
+ expectedReloads float64
+ }{
+ {
+ name: "extremely small interval",
+ reloadInterval: 1 * time.Millisecond,
+ expectedReloads: 5,
+ },
+ {
+ name: "one second interval",
+ reloadInterval: 1 * time.Second,
+ expectedReloads: 5,
+ },
+ }
+ for _, c := range cases {
+ t.Run(c.name, func(t *testing.T) {
+ t.Parallel()
+ db := newTestDB(t, withOpts(&Options{
+ BlockReloadInterval: c.reloadInterval,
+ }))
+ if c.reloadInterval < 1*time.Second {
+ require.Equal(t, 1*time.Second, db.opts.BlockReloadInterval, "interval should be clamped to minimum of 1 second")
+ }
+ require.Equal(t, float64(1), prom_testutil.ToFloat64(db.metrics.reloads), "there should be one initial reload")
+ require.Eventually(t, func() bool {
+ return prom_testutil.ToFloat64(db.metrics.reloads) == c.expectedReloads
+ },
+ 5*time.Second,
+ 100*time.Millisecond,
+ )
+ })
+ }
+}
+
+func TestStaleSeriesCompaction(t *testing.T) {
+ opts := DefaultOptions()
+ opts.MinBlockDuration = 1000
+ opts.MaxBlockDuration = 1000
+ db := newTestDB(t, withOpts(opts))
+ db.DisableCompactions()
+ t.Cleanup(func() {
+ require.NoError(t, db.Close())
+ })
+
+ var (
+ nonStaleSeries, staleSeries,
+ nonStaleHist, staleHist,
+ nonStaleFHist, staleFHist,
+ staleSeriesCrossingBoundary, staleHistCrossingBoundary, staleFHistCrossingBoundary []labels.Labels
+ numSeriesPerCategory = 1
+ )
+ for i := range numSeriesPerCategory {
+ nonStaleSeries = append(nonStaleSeries, labels.FromStrings("name", fmt.Sprintf("series%d", 1000+i)))
+ nonStaleHist = append(nonStaleHist, labels.FromStrings("name", fmt.Sprintf("series%d", 2000+i)))
+ nonStaleFHist = append(nonStaleFHist, labels.FromStrings("name", fmt.Sprintf("series%d", 3000+i)))
+
+ staleSeries = append(staleSeries, labels.FromStrings("name", fmt.Sprintf("series%d", 4000+i)))
+ staleHist = append(staleHist, labels.FromStrings("name", fmt.Sprintf("series%d", 5000+i)))
+ staleFHist = append(staleFHist, labels.FromStrings("name", fmt.Sprintf("series%d", 6000+i)))
+
+ staleSeriesCrossingBoundary = append(staleSeriesCrossingBoundary, labels.FromStrings("name", fmt.Sprintf("series%d", 7000+i)))
+ staleHistCrossingBoundary = append(staleHistCrossingBoundary, labels.FromStrings("name", fmt.Sprintf("series%d", 8000+i)))
+ staleFHistCrossingBoundary = append(staleFHistCrossingBoundary, labels.FromStrings("name", fmt.Sprintf("series%d", 9000+i)))
+ }
+
+ var (
+ v = 10.0
+ staleV = math.Float64frombits(value.StaleNaN)
+ h = tsdbutil.GenerateTestHistograms(1)[0]
+ fh = tsdbutil.GenerateTestFloatHistograms(1)[0]
+ staleH = &histogram.Histogram{Sum: staleV}
+ staleFH = &histogram.FloatHistogram{Sum: staleV}
+ )
+
+ addNormalSamples := func(ts int64, floatSeries, histSeries, floatHistSeries []labels.Labels) {
+ app := db.Appender(context.Background())
+ for i := range len(floatSeries) {
+ _, err := app.Append(0, floatSeries[i], ts, v)
+ require.NoError(t, err)
+ _, err = app.AppendHistogram(0, histSeries[i], ts, h, nil)
+ require.NoError(t, err)
+ _, err = app.AppendHistogram(0, floatHistSeries[i], ts, nil, fh)
+ require.NoError(t, err)
+ }
+ require.NoError(t, app.Commit())
+ }
+ addStaleSamples := func(ts int64, floatSeries, histSeries, floatHistSeries []labels.Labels) {
+ app := db.Appender(context.Background())
+ for i := range len(floatSeries) {
+ _, err := app.Append(0, floatSeries[i], ts, staleV)
+ require.NoError(t, err)
+ _, err = app.AppendHistogram(0, histSeries[i], ts, staleH, nil)
+ require.NoError(t, err)
+ _, err = app.AppendHistogram(0, floatHistSeries[i], ts, nil, staleFH)
+ require.NoError(t, err)
+ }
+ require.NoError(t, app.Commit())
+ }
+
+ // Normal sample for all.
+ addNormalSamples(100, nonStaleSeries, nonStaleHist, nonStaleFHist)
+ addNormalSamples(100, staleSeries, staleHist, staleFHist)
+
+ // Stale sample for the stale series. Normal sample for the non-stale series.
+ addNormalSamples(200, nonStaleSeries, nonStaleHist, nonStaleFHist)
+ addStaleSamples(200, staleSeries, staleHist, staleFHist)
+
+ // Normal samples for the non-stale series later
+ addNormalSamples(300, nonStaleSeries, nonStaleHist, nonStaleFHist)
+
+ require.Equal(t, uint64(6*numSeriesPerCategory), db.Head().NumSeries())
+ require.Equal(t, uint64(3*numSeriesPerCategory), db.Head().NumStaleSeries())
+
+ // Series crossing block boundary and gets stale.
+ addNormalSamples(300, staleSeriesCrossingBoundary, staleHistCrossingBoundary, staleFHistCrossingBoundary)
+ addNormalSamples(700, staleSeriesCrossingBoundary, staleHistCrossingBoundary, staleFHistCrossingBoundary)
+ addNormalSamples(1100, staleSeriesCrossingBoundary, staleHistCrossingBoundary, staleFHistCrossingBoundary)
+ addStaleSamples(1200, staleSeriesCrossingBoundary, staleHistCrossingBoundary, staleFHistCrossingBoundary)
+
+ require.NoError(t, db.CompactStaleHead())
+
+ require.Equal(t, uint64(3*numSeriesPerCategory), db.Head().NumSeries())
+ require.Equal(t, uint64(0), db.Head().NumStaleSeries())
+
+ require.Len(t, db.Blocks(), 2)
+ m := db.Blocks()[0].Meta()
+ require.Equal(t, int64(0), m.MinTime)
+ require.Equal(t, int64(1000), m.MaxTime)
+ require.Truef(t, m.Compaction.FromStaleSeries(), "stale series info not found in block meta")
+ m = db.Blocks()[1].Meta()
+ require.Equal(t, int64(1000), m.MinTime)
+ require.Equal(t, int64(2000), m.MaxTime)
+ require.Truef(t, m.Compaction.FromStaleSeries(), "stale series info not found in block meta")
+
+ // To make sure that Head is not truncated based on stale series block.
+ require.NoError(t, db.reload())
+
+ nonFirstH := h.Copy()
+ nonFirstH.CounterResetHint = histogram.NotCounterReset
+ nonFirstFH := fh.Copy()
+ nonFirstFH.CounterResetHint = histogram.NotCounterReset
+
+ // Verify head block.
+ verifyHeadBlock := func() {
+ require.Equal(t, uint64(3), db.head.NumSeries())
+ require.Equal(t, uint64(0), db.head.NumStaleSeries())
+
+ expHeadQuery := make(map[string][]chunks.Sample)
+ for i := range numSeriesPerCategory {
+ expHeadQuery[fmt.Sprintf(`{name="%s"}`, nonStaleSeries[i].Get("name"))] = []chunks.Sample{
+ sample{t: 100, f: v}, sample{t: 200, f: v}, sample{t: 300, f: v},
+ }
+ expHeadQuery[fmt.Sprintf(`{name="%s"}`, nonStaleHist[i].Get("name"))] = []chunks.Sample{
+ sample{t: 100, h: h}, sample{t: 200, h: nonFirstH}, sample{t: 300, h: nonFirstH},
+ }
+ expHeadQuery[fmt.Sprintf(`{name="%s"}`, nonStaleFHist[i].Get("name"))] = []chunks.Sample{
+ sample{t: 100, fh: fh}, sample{t: 200, fh: nonFirstFH}, sample{t: 300, fh: nonFirstFH},
+ }
+ }
+
+ querier, err := NewBlockQuerier(NewRangeHead(db.head, 0, 300), 0, 300)
+ require.NoError(t, err)
+ t.Cleanup(func() {
+ querier.Close()
+ })
+ seriesSet := query(t, querier, labels.MustNewMatcher(labels.MatchRegexp, "name", "series.*"))
+ require.Equal(t, expHeadQuery, seriesSet)
+ }
+
+ verifyHeadBlock()
+
+ // Verify blocks from stale series.
+ {
+ expBlockQuery := make(map[string][]chunks.Sample)
+ for i := range numSeriesPerCategory {
+ expBlockQuery[fmt.Sprintf(`{name="%s"}`, staleSeries[i].Get("name"))] = []chunks.Sample{
+ sample{t: 100, f: v}, sample{t: 200, f: staleV},
+ }
+ expBlockQuery[fmt.Sprintf(`{name="%s"}`, staleHist[i].Get("name"))] = []chunks.Sample{
+ sample{t: 100, h: h}, sample{t: 200, h: staleH},
+ }
+ expBlockQuery[fmt.Sprintf(`{name="%s"}`, staleFHist[i].Get("name"))] = []chunks.Sample{
+ sample{t: 100, fh: fh}, sample{t: 200, fh: staleFH},
+ }
+ expBlockQuery[fmt.Sprintf(`{name="%s"}`, staleSeriesCrossingBoundary[i].Get("name"))] = []chunks.Sample{
+ sample{t: 300, f: v}, sample{t: 700, f: v}, sample{t: 1100, f: v}, sample{t: 1200, f: staleV},
+ }
+ expBlockQuery[fmt.Sprintf(`{name="%s"}`, staleHistCrossingBoundary[i].Get("name"))] = []chunks.Sample{
+ sample{t: 300, h: h}, sample{t: 700, h: nonFirstH}, sample{t: 1100, h: h}, sample{t: 1200, h: staleH},
+ }
+ expBlockQuery[fmt.Sprintf(`{name="%s"}`, staleFHistCrossingBoundary[i].Get("name"))] = []chunks.Sample{
+ sample{t: 300, fh: fh}, sample{t: 700, fh: nonFirstFH}, sample{t: 1100, fh: fh}, sample{t: 1200, fh: staleFH},
+ }
+ }
+
+ querier, err := NewBlockQuerier(db.Blocks()[0], 0, 1000)
+ require.NoError(t, err)
+ t.Cleanup(func() {
+ querier.Close()
+ })
+ seriesSet := queryWithoutReplacingNaNs(t, querier, labels.MustNewMatcher(labels.MatchRegexp, "name", "series.*"))
+
+ querier, err = NewBlockQuerier(db.Blocks()[1], 1000, 2000)
+ require.NoError(t, err)
+ t.Cleanup(func() {
+ querier.Close()
+ })
+ seriesSet2 := queryWithoutReplacingNaNs(t, querier, labels.MustNewMatcher(labels.MatchRegexp, "name", "series.*"))
+ for k, v := range seriesSet2 {
+ seriesSet[k] = append(seriesSet[k], v...)
+ }
+
+ require.Len(t, seriesSet, len(expBlockQuery))
+
+ // Compare all the samples except the stale value that needs special handling.
+ for _, category := range [][]labels.Labels{
+ staleSeries, staleHist, staleFHist,
+ staleSeriesCrossingBoundary, staleHistCrossingBoundary, staleFHistCrossingBoundary,
+ } {
+ for i := range numSeriesPerCategory {
+ seriesKey := fmt.Sprintf(`{name="%s"}`, category[i].Get("name"))
+ samples := expBlockQuery[seriesKey]
+ actSamples, exists := seriesSet[seriesKey]
+ require.Truef(t, exists, "series not found in result %s", seriesKey)
+ require.Len(t, actSamples, len(samples))
+
+ for i := range len(samples) - 1 {
+ require.Equal(t, samples[i], actSamples[i])
+ }
+
+ l := len(samples) - 1
+ require.Equal(t, samples[l].T(), actSamples[l].T())
+ switch {
+ case value.IsStaleNaN(samples[l].F()):
+ require.True(t, value.IsStaleNaN(actSamples[l].F()))
+ case samples[l].H() != nil:
+ require.True(t, value.IsStaleNaN(actSamples[l].H().Sum))
+ default:
+ require.True(t, value.IsStaleNaN(actSamples[l].FH().Sum))
+ }
+ }
+ }
+ }
+
+ {
+ // Restart DB and verify that stale series were discarded from WAL replay.
+ require.NoError(t, db.Close())
+ var err error
+ db, err = Open(db.Dir(), db.logger, db.registerer, db.opts, nil)
+ require.NoError(t, err)
+
+ verifyHeadBlock()
+ }
+}
+
+// TestStaleSeriesCompactionWithZeroSeries verifies that CompactStaleHead handles
+// an empty head (0 series) gracefully without division by zero or incorrectly
+// triggering compaction. This is a regression test for issue #17949.
+func TestStaleSeriesCompactionWithZeroSeries(t *testing.T) {
+ opts := DefaultOptions()
+ opts.MinBlockDuration = 1000
+ opts.MaxBlockDuration = 1000
+ db := newTestDB(t, withOpts(opts))
+ db.DisableCompactions()
+ t.Cleanup(func() {
+ require.NoError(t, db.Close())
+ })
+
+ // Verify the head is empty.
+ require.Equal(t, uint64(0), db.Head().NumSeries())
+ require.Equal(t, uint64(0), db.Head().NumStaleSeries())
+
+ // CompactStaleHead should handle zero series gracefully (no panic, no error).
+ require.NoError(t, db.CompactStaleHead())
+
+ // Should still have no blocks since there was nothing to compact.
+ require.Empty(t, db.Blocks())
+}
diff --git a/tsdb/encoding/encoding.go b/tsdb/encoding/encoding.go
index cc7d0990f6..a6d6fe4d44 100644
--- a/tsdb/encoding/encoding.go
+++ b/tsdb/encoding/encoding.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Prometheus Authors
+// 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
diff --git a/tsdb/errors/errors.go b/tsdb/errors/errors.go
deleted file mode 100644
index ded4ae3a27..0000000000
--- a/tsdb/errors/errors.go
+++ /dev/null
@@ -1,109 +0,0 @@
-// Copyright 2016 The etcd 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 errors
-
-import (
- "bytes"
- "errors"
- "fmt"
- "io"
-)
-
-// multiError type allows combining multiple errors into one.
-type multiError []error
-
-// NewMulti returns multiError with provided errors added if not nil.
-func NewMulti(errs ...error) multiError { //nolint:revive // unexported-return
- m := multiError{}
- m.Add(errs...)
- return m
-}
-
-// Add adds single or many errors to the error list. Each error is added only if not nil.
-// If the error is a nonNilMultiError type, the errors inside nonNilMultiError are added to the main multiError.
-func (es *multiError) Add(errs ...error) {
- for _, err := range errs {
- if err == nil {
- continue
- }
- var merr nonNilMultiError
- if errors.As(err, &merr) {
- *es = append(*es, merr.errs...)
- continue
- }
- *es = append(*es, err)
- }
-}
-
-// Err returns the error list as an error or nil if it is empty.
-func (es multiError) Err() error {
- if len(es) == 0 {
- return nil
- }
- return nonNilMultiError{errs: es}
-}
-
-// nonNilMultiError implements the error interface, and it represents
-// multiError with at least one error inside it.
-// This type is needed to make sure that nil is returned when no error is combined in multiError for err != nil
-// check to work.
-type nonNilMultiError struct {
- errs multiError
-}
-
-// Error returns a concatenated string of the contained errors.
-func (es nonNilMultiError) Error() string {
- var buf bytes.Buffer
-
- if len(es.errs) > 1 {
- fmt.Fprintf(&buf, "%d errors: ", len(es.errs))
- }
-
- for i, err := range es.errs {
- if i != 0 {
- buf.WriteString("; ")
- }
- buf.WriteString(err.Error())
- }
-
- return buf.String()
-}
-
-// Is attempts to match the provided error against errors in the error list.
-//
-// This function allows errors.Is to traverse the values stored in the MultiError.
-// It returns true if any of the errors in the list match the target.
-func (es nonNilMultiError) Is(target error) bool {
- for _, err := range es.errs {
- if errors.Is(err, target) {
- return true
- }
- }
- return false
-}
-
-// Unwrap returns the list of errors contained in the multiError.
-func (es nonNilMultiError) Unwrap() []error {
- return es.errs
-}
-
-// CloseAll closes all given closers while recording error in MultiError.
-func CloseAll(cs []io.Closer) error {
- errs := NewMulti()
- for _, c := range cs {
- errs.Add(c.Close())
- }
- return errs.Err()
-}
diff --git a/tsdb/errors/errors_test.go b/tsdb/errors/errors_test.go
deleted file mode 100644
index 146c66bf00..0000000000
--- a/tsdb/errors/errors_test.go
+++ /dev/null
@@ -1,172 +0,0 @@
-// Copyright 2025 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 errors
-
-import (
- "context"
- "errors"
- "fmt"
- "testing"
-
- "github.com/stretchr/testify/require"
-)
-
-func TestMultiError_Is(t *testing.T) {
- customErr1 := errors.New("test error 1")
- customErr2 := errors.New("test error 2")
-
- testCases := map[string]struct {
- sourceErrors []error
- target error
- is bool
- }{
- "adding a context cancellation doesn't lose the information": {
- sourceErrors: []error{context.Canceled},
- target: context.Canceled,
- is: true,
- },
- "adding multiple context cancellations doesn't lose the information": {
- sourceErrors: []error{context.Canceled, context.Canceled},
- target: context.Canceled,
- is: true,
- },
- "adding wrapped context cancellations doesn't lose the information": {
- sourceErrors: []error{errors.New("some error"), fmt.Errorf("some message: %w", context.Canceled)},
- target: context.Canceled,
- is: true,
- },
- "adding a nil error doesn't lose the information": {
- sourceErrors: []error{errors.New("some error"), fmt.Errorf("some message: %w", context.Canceled), nil},
- target: context.Canceled,
- is: true,
- },
- "errors with no context cancellation error are not a context canceled error": {
- sourceErrors: []error{errors.New("first error"), errors.New("second error")},
- target: context.Canceled,
- is: false,
- },
- "no errors are not a context canceled error": {
- sourceErrors: nil,
- target: context.Canceled,
- is: false,
- },
- "no errors are a nil error": {
- sourceErrors: nil,
- target: nil,
- is: true,
- },
- "nested multi-error contains customErr1": {
- sourceErrors: []error{
- customErr1,
- NewMulti(
- customErr2,
- fmt.Errorf("wrapped %w", context.Canceled),
- ).Err(),
- },
- target: customErr1,
- is: true,
- },
- "nested multi-error contains customErr2": {
- sourceErrors: []error{
- customErr1,
- NewMulti(
- customErr2,
- fmt.Errorf("wrapped %w", context.Canceled),
- ).Err(),
- },
- target: customErr2,
- is: true,
- },
- "nested multi-error contains wrapped context.Canceled": {
- sourceErrors: []error{
- customErr1,
- NewMulti(
- customErr2,
- fmt.Errorf("wrapped %w", context.Canceled),
- ).Err(),
- },
- target: context.Canceled,
- is: true,
- },
- "nested multi-error does not contain context.DeadlineExceeded": {
- sourceErrors: []error{
- customErr1,
- NewMulti(
- customErr2,
- fmt.Errorf("wrapped %w", context.Canceled),
- ).Err(),
- },
- target: context.DeadlineExceeded,
- is: false, // make sure we still return false in valid cases
- },
- }
-
- for testName, testCase := range testCases {
- t.Run(testName, func(t *testing.T) {
- mErr := NewMulti(testCase.sourceErrors...)
- require.Equal(t, testCase.is, errors.Is(mErr.Err(), testCase.target))
- })
- }
-}
-
-func TestMultiError_As(t *testing.T) {
- tE1 := testError{"error cause 1"}
- tE2 := testError{"error cause 2"}
- var target testError
- testCases := map[string]struct {
- sourceErrors []error
- target error
- as bool
- }{
- "MultiError containing only a testError can be cast to that testError": {
- sourceErrors: []error{tE1},
- target: tE1,
- as: true,
- },
- "MultiError containing multiple testErrors can be cast to the first testError added": {
- sourceErrors: []error{tE1, tE2},
- target: tE1,
- as: true,
- },
- "MultiError containing multiple errors can be cast to the first testError added": {
- sourceErrors: []error{context.Canceled, tE1, context.DeadlineExceeded, tE2},
- target: tE1,
- as: true,
- },
- "MultiError not containing a testError cannot be cast to a testError": {
- sourceErrors: []error{context.Canceled, context.DeadlineExceeded},
- as: false,
- },
- }
-
- for testName, testCase := range testCases {
- t.Run(testName, func(t *testing.T) {
- mErr := NewMulti(testCase.sourceErrors...).Err()
- if testCase.as {
- require.ErrorAs(t, mErr, &target)
- require.Equal(t, testCase.target, target)
- } else {
- require.NotErrorAs(t, mErr, &target)
- }
- })
- }
-}
-
-type testError struct {
- cause string
-}
-
-func (e testError) Error() string {
- return fmt.Sprintf("testError[cause: %s]", e.cause)
-}
diff --git a/tsdb/example_test.go b/tsdb/example_test.go
index 46deae5198..88632b69f9 100644
--- a/tsdb/example_test.go
+++ b/tsdb/example_test.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
diff --git a/tsdb/exemplar.go b/tsdb/exemplar.go
index cdbcd5cde6..36b0a7e660 100644
--- a/tsdb/exemplar.go
+++ b/tsdb/exemplar.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
@@ -36,10 +36,11 @@ const (
)
type CircularExemplarStorage struct {
- lock sync.RWMutex
- exemplars []circularBufferEntry
- nextIndex int
- metrics *ExemplarMetrics
+ lock sync.RWMutex
+ exemplars []circularBufferEntry
+ nextIndex int
+ metrics *ExemplarMetrics
+ oooTimeWindowMillis int64
// Map of series labels as a string to index entry, which points to the first
// and last exemplar for the series in the exemplars circular buffer.
@@ -55,6 +56,7 @@ type indexEntry struct {
type circularBufferEntry struct {
exemplar exemplar.Exemplar
next int
+ prev int
ref *indexEntry
}
@@ -115,15 +117,19 @@ func NewExemplarMetrics(reg prometheus.Registerer) *ExemplarMetrics {
// If we assume the average case 95 bytes per exemplar we can fit 5651272 exemplars in
// 1GB of extra memory, accounting for the fact that this is heap allocated space.
// If len <= 0, then the exemplar storage is essentially a noop storage but can later be
-// resized to store exemplars.
-func NewCircularExemplarStorage(length int64, m *ExemplarMetrics) (ExemplarStorage, error) {
+// resized to store exemplars. If oooTimeWindowMillis <= 0, out-of-order exemplars are disabled.
+func NewCircularExemplarStorage(length int64, m *ExemplarMetrics, oooTimeWindowMillis int64) (ExemplarStorage, error) {
if length < 0 {
length = 0
}
+ if oooTimeWindowMillis < 0 {
+ oooTimeWindowMillis = 0
+ }
c := &CircularExemplarStorage{
- exemplars: make([]circularBufferEntry, length),
- index: make(map[string]*indexEntry, length/estimatedExemplarsPerSeries),
- metrics: m,
+ exemplars: make([]circularBufferEntry, length),
+ index: make(map[string]*indexEntry, length/estimatedExemplarsPerSeries),
+ metrics: m,
+ oooTimeWindowMillis: oooTimeWindowMillis,
}
c.metrics.maxExemplars.Set(float64(length))
@@ -171,6 +177,9 @@ func (ce *CircularExemplarStorage) Select(start, end int64, matchers ...[]*label
}
se.SeriesLabels = idx.seriesLabels
+ // TODO: Since we maintain a doubly-linked-list, we can also iterate from head to tail
+ // which might be more performant if the selected interval is skewed to the head.
+
// Loop through all exemplars in the circular buffer for the current series.
for e.exemplar.Ts <= end {
if e.exemplar.Ts >= start {
@@ -253,16 +262,12 @@ func (ce *CircularExemplarStorage) validateExemplar(idx *indexEntry, e exemplar.
return storage.ErrDuplicateExemplar
}
- // Since during the scrape the exemplars are sorted first by timestamp, then value, then labels,
- // if any of these conditions are true, we know that the exemplar is either a duplicate
- // of a previous one (but not the most recent one as that is checked above) or out of order.
- // We now allow exemplars with duplicate timestamps as long as they have different values and/or labels
- // since that can happen for different buckets of a native histogram.
- // We do not distinguish between duplicates and out of order as iterating through the exemplars
- // to check for that would be expensive (versus just comparing with the most recent one) especially
- // since this is run under a lock, and not worth it as we just need to return an error so we do not
- // append the exemplar.
- if e.Ts < newestExemplar.Ts ||
+ // Reject exemplars older than the OOO time window relative to the newest exemplar.
+ // Exemplars with the same timestamp are ordered by value then label hash to detect
+ // duplicates without iterating through all stored exemplars, which would be too
+ // expensive under lock. Exemplars with equal timestamps but different values or
+ // labels are allowed to support multiple buckets of native histograms.
+ if (e.Ts < newestExemplar.Ts && e.Ts <= newestExemplar.Ts-ce.oooTimeWindowMillis) ||
(e.Ts == newestExemplar.Ts && e.Value < newestExemplar.Value) ||
(e.Ts == newestExemplar.Ts && e.Value == newestExemplar.Value && e.Labels.Hash() < newestExemplar.Labels.Hash()) {
if appended {
@@ -273,8 +278,19 @@ func (ce *CircularExemplarStorage) validateExemplar(idx *indexEntry, e exemplar.
return nil
}
-// Resize changes the size of exemplar buffer by allocating a new buffer and migrating data to it.
-// Exemplars are kept when possible. Shrinking will discard oldest data (in order of ingest) as needed.
+// SetOutOfOrderTimeWindow sets the out-of-order time window for exemplars in
+// milliseconds. Exemplars older than it are not added to the circular exemplar
+// buffer.
+func (ce *CircularExemplarStorage) SetOutOfOrderTimeWindow(d int64) {
+ ce.lock.Lock()
+ defer ce.lock.Unlock()
+ ce.oooTimeWindowMillis = d
+}
+
+// Resize changes the size of exemplar buffer by allocating a new buffer and
+// migrating data to it. Exemplars are kept when possible. Shrinking will discard
+// old data (in order of ingestion) as needed. Returns the number of migrated
+// exemplars.
func (ce *CircularExemplarStorage) Resize(l int64) int {
// Accept negative values as just 0 size.
if l <= 0 {
@@ -284,65 +300,85 @@ func (ce *CircularExemplarStorage) Resize(l int64) int {
ce.lock.Lock()
defer ce.lock.Unlock()
- if l == int64(len(ce.exemplars)) {
- return 0
- }
-
- oldBuffer := ce.exemplars
- oldNextIndex := int64(ce.nextIndex)
-
- ce.exemplars = make([]circularBufferEntry, l)
- ce.index = make(map[string]*indexEntry, l/estimatedExemplarsPerSeries)
- ce.nextIndex = 0
-
- // Replay as many entries as needed, starting with oldest first.
- count := min(l, int64(len(oldBuffer)))
-
+ oldSize := int64(len(ce.exemplars))
migrated := 0
-
- if l > 0 && len(oldBuffer) > 0 {
- // Rewind previous next index by count with wrap-around.
- // This math is essentially looking at nextIndex, where we would write the next exemplar to,
- // and find the index in the old exemplar buffer that we should start migrating exemplars from.
- // This way we don't migrate exemplars that would just be overwritten when migrating later exemplars.
- startIndex := (oldNextIndex - count + int64(len(oldBuffer))) % int64(len(oldBuffer))
-
- var buf [1024]byte
- for i := range count {
- idx := (startIndex + i) % int64(len(oldBuffer))
- if oldBuffer[idx].ref != nil {
- ce.migrate(&oldBuffer[idx], buf[:])
- migrated++
- }
- }
+ switch {
+ case l == oldSize:
+ // NOOP.
+ return migrated
+ case l > oldSize:
+ migrated = ce.grow(l)
+ case l < oldSize:
+ migrated = ce.shrink(l)
}
ce.computeMetrics()
ce.metrics.maxExemplars.Set(float64(l))
-
return migrated
}
-// migrate is like AddExemplar but reuses existing structs. Expected to be called in batch and requires
-// external lock and does not compute metrics.
-func (ce *CircularExemplarStorage) migrate(entry *circularBufferEntry, buf []byte) {
- seriesLabels := entry.ref.seriesLabels.Bytes(buf[:0])
-
- idx, ok := ce.index[string(seriesLabels)]
- if !ok {
- idx = entry.ref
- idx.oldest = ce.nextIndex
- ce.index[string(seriesLabels)] = idx
- } else {
- entry.ref = idx
- ce.exemplars[idx.newest].next = ce.nextIndex
+// grow the circular buffer to have size l by allocating a new slice and copying
+// the old data to it. After growing, ce.nextIndex points to the next free entry
+// in the buffer. This function must be called with the lock acquired.
+func (ce *CircularExemplarStorage) grow(l int64) int {
+ oldSize := len(ce.exemplars)
+ newSlice := make([]circularBufferEntry, l)
+ ranges := []intRange{
+ {from: ce.nextIndex, to: oldSize},
+ {from: 0, to: ce.nextIndex},
}
- idx.newest = ce.nextIndex
+ totalCopied, migrated := copyExemplarRanges(ce.index, newSlice, ce.exemplars, ranges)
+ ce.nextIndex = totalCopied
+ ce.exemplars = newSlice
+ return migrated
+}
- entry.next = noExemplar
- ce.exemplars[ce.nextIndex] = *entry
+// shrink the circular buffer by either trimming from the right or deleting the
+// oldest samples to accommodate the new size l. This function must be called
+// with the lock acquired.
+func (ce *CircularExemplarStorage) shrink(l int64) (migrated int) {
+ oldSize := len(ce.exemplars)
+ diff := int(int64(oldSize) - l)
+ deleteStart := ce.nextIndex
+ deleteEnd := (deleteStart + diff) % oldSize
- ce.nextIndex = (ce.nextIndex + 1) % len(ce.exemplars)
+ // Remove items from the buffer starting from c.nextIndex. This drops older
+ // entries first in the order of ingestion.
+ for i := range diff {
+ idx := (deleteStart + i) % oldSize
+ ref := ce.exemplars[idx].ref
+ if ce.removeExemplar(&ce.exemplars[idx]) {
+ ce.removeIndex(ref)
+ }
+ }
+
+ 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
+ // delete the index since removeExemplar already did. Simply remove all elements
+ // and reset tracking pointers.
+ ce.exemplars = newSlice
+ ce.nextIndex = 0
+ return 0
+ case deleteStart < deleteEnd:
+ // We delete an "inner" section of the circular buffer.
+ 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.
+ totalCopied, migrated = copyExemplarRanges(ce.index, newSlice, ce.exemplars, []intRange{
+ {from: deleteEnd, to: deleteStart},
+ })
+ }
+
+ ce.nextIndex = totalCopied % int(l)
+ ce.exemplars = newSlice
+ return migrated
}
func (ce *CircularExemplarStorage) AddExemplar(l labels.Labels, e exemplar.Exemplar) error {
@@ -358,7 +394,7 @@ func (ce *CircularExemplarStorage) AddExemplar(l labels.Labels, e exemplar.Exemp
var buf [1024]byte
seriesLabels := l.Bytes(buf[:])
- idx, ok := ce.index[string(seriesLabels)]
+ idx, indexExists := ce.index[string(seriesLabels)]
err := ce.validateExemplar(idx, e, true)
if err != nil {
if errors.Is(err, storage.ErrDuplicateExemplar) {
@@ -368,32 +404,82 @@ func (ce *CircularExemplarStorage) AddExemplar(l labels.Labels, e exemplar.Exemp
return err
}
- if !ok {
- idx = &indexEntry{oldest: ce.nextIndex, seriesLabels: l}
- ce.index[string(seriesLabels)] = idx
- } else {
- ce.exemplars[idx.newest].next = ce.nextIndex
- }
-
- if prev := &ce.exemplars[ce.nextIndex]; prev.ref != nil {
- // There exists an exemplar already on this ce.nextIndex entry,
- // drop it, to make place for others.
- if prev.next == noExemplar {
- // Last item for this series, remove index entry.
- var buf [1024]byte
- prevLabels := prev.ref.seriesLabels.Bytes(buf[:])
- delete(ce.index, string(prevLabels))
- } else {
- prev.ref.oldest = prev.next
+ // 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
+ if outOfOrder {
+ insertionIndex = ce.findInsertionIndex(e, idx)
+ if ce.exemplars[insertionIndex].exemplar.Ts == e.Ts {
+ // Assume duplicate exemplar, noop.
+ // Native histograms will exercise this code path a lot due to
+ // having multiple exemplars per series so checking the
+ // value and labels would be too expensive.
+ return nil
+ }
}
}
- // Default the next value to -1 (which we use to detect that we've iterated through all exemplars for a series in Select)
- // since this is the first exemplar stored for this series.
- ce.exemplars[ce.nextIndex].next = noExemplar
+ // If the index didn't exist (new series), create one.
+ if !indexExists {
+ idx = &indexEntry{seriesLabels: l}
+ ce.index[string(seriesLabels)] = idx
+ }
+
+ // Remove entries if the buffer is full.
+ if prev := &ce.exemplars[ce.nextIndex]; prev.ref != nil {
+ prevRef := prev.ref
+ if ce.removeExemplar(prev) {
+ if prevRef == idx {
+ // Do not delete the indexEntry we're inserting to.
+ indexExists = false
+ } 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)
+ }
+ }
+
+ // We create a new entry in the linked list.
ce.exemplars[ce.nextIndex].exemplar = e
ce.exemplars[ce.nextIndex].ref = idx
- idx.newest = ce.nextIndex
+
+ switch {
+ case !indexExists:
+ // Add the first and only exemplar to the list.
+ idx.oldest = ce.nextIndex
+ idx.newest = ce.nextIndex
+ ce.exemplars[ce.nextIndex].prev = noExemplar
+ ce.exemplars[ce.nextIndex].next = noExemplar
+ case e.Ts >= ce.exemplars[idx.newest].exemplar.Ts:
+ // Add the exemplar at the tip (after newest).
+ ce.exemplars[idx.newest].next = ce.nextIndex
+ ce.exemplars[ce.nextIndex].prev = idx.newest
+ ce.exemplars[ce.nextIndex].next = noExemplar
+ idx.newest = ce.nextIndex
+ case e.Ts < ce.exemplars[idx.oldest].exemplar.Ts:
+ // Add the exemplar at the tail (before oldest).
+ ce.exemplars[idx.oldest].prev = ce.nextIndex
+ ce.exemplars[ce.nextIndex].prev = noExemplar
+ ce.exemplars[ce.nextIndex].next = idx.oldest
+ idx.oldest = ce.nextIndex
+ default:
+ // Insert the exemplar into the list by finding the most recent
+ // in-order exemplar that precedes it, and placing it after.
+ nextExemplar := ce.exemplars[insertionIndex].next
+ ce.exemplars[ce.nextIndex].prev = insertionIndex
+ ce.exemplars[ce.nextIndex].next = nextExemplar
+ ce.exemplars[insertionIndex].next = ce.nextIndex
+ if nextExemplar != noExemplar {
+ ce.exemplars[nextExemplar].prev = ce.nextIndex
+ }
+ }
ce.nextIndex = (ce.nextIndex + 1) % len(ce.exemplars)
@@ -402,6 +488,56 @@ func (ce *CircularExemplarStorage) AddExemplar(l labels.Labels, e exemplar.Exemp
return nil
}
+// removeExemplar removes the given entry from the circular buffer. Returns true
+// iff the deleted entry was the last entry (and the index is now empty).
+// This function must be called with the lock acquired.
+func (ce *CircularExemplarStorage) removeExemplar(entry *circularBufferEntry) bool {
+ ref := entry.ref
+ if ref == nil {
+ return false
+ }
+
+ if entry.prev != noExemplar {
+ ce.exemplars[entry.prev].next = entry.next
+ } else {
+ ref.oldest = entry.next
+ }
+
+ if entry.next != noExemplar {
+ ce.exemplars[entry.next].prev = entry.prev
+ } else {
+ ref.newest = entry.prev
+ }
+
+ // Mark this item as deleted.
+ entry.ref = nil
+
+ return ref.oldest == noExemplar && ref.newest == noExemplar
+}
+
+// removeIndex removes an indexEntry from the circular exemplar storage.
+// This function must be called with the lock acquired.
+func (ce *CircularExemplarStorage) removeIndex(ref *indexEntry) {
+ var buf [1024]byte
+ entryLabels := ref.seriesLabels.Bytes(buf[:])
+ delete(ce.index, string(entryLabels))
+}
+
+// findInsertionIndex finds the position at which e should be placed in the
+// doubly-linked list by traversing the linked list from idx.newest to idx.oldest
+// and following back links. Since out-of-order exemplars commonly lie close to
+// the newest entry, traversing from newest to oldest is usually faster.
+func (ce *CircularExemplarStorage) findInsertionIndex(e exemplar.Exemplar, idx *indexEntry) int {
+ for i := idx.newest; i != noExemplar; {
+ current := ce.exemplars[i]
+ if current.exemplar.Ts <= e.Ts {
+ return i
+ }
+ i = current.prev
+ }
+ return idx.oldest
+}
+
func (ce *CircularExemplarStorage) computeMetrics() {
ce.metrics.seriesWithExemplarsInStorage.Set(float64(len(ce.index)))
@@ -443,3 +579,65 @@ func (ce *CircularExemplarStorage) IterateExemplars(f func(seriesLabels labels.L
}
return nil
}
+
+type intRange struct {
+ from, to int
+}
+
+func (e intRange) contains(i int) bool {
+ return i >= e.from && i < e.to
+}
+
+// copyExemplarRanges copies non-overlapping ranges from src into dest and
+// 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,
+) (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
+ for di := range n {
+ e := &dest[di]
+ if e.ref == nil {
+ // We potentially copied empty entries. Subtract them now to correctly show the
+ // number of "migrated" items.
+ migratedEntries--
+ continue
+ }
+ for i, rng := range ranges {
+ if rng.contains(e.prev) {
+ e.prev += offsets[i]
+ break
+ }
+ }
+ for i, rng := range ranges {
+ if rng.contains(e.next) {
+ e.next += offsets[i]
+ break
+ }
+ }
+ }
+ for _, idx := range index {
+ for i, rng := range ranges {
+ if rng.contains(idx.oldest) {
+ idx.oldest += offsets[i]
+ break
+ }
+ }
+ for i, rng := range ranges {
+ if rng.contains(idx.newest) {
+ idx.newest += offsets[i]
+ break
+ }
+ }
+ }
+ return n, migratedEntries
+}
diff --git a/tsdb/exemplar_test.go b/tsdb/exemplar_test.go
index bf6ad2fabb..10a0745d87 100644
--- a/tsdb/exemplar_test.go
+++ b/tsdb/exemplar_test.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
@@ -18,6 +18,7 @@ import (
"fmt"
"math"
"reflect"
+ "sort"
"strconv"
"strings"
"sync"
@@ -35,7 +36,7 @@ var eMetrics = NewExemplarMetrics(prometheus.DefaultRegisterer)
// Tests the same exemplar cases as AddExemplar, but specifically the ValidateExemplar function so it can be relied on externally.
func TestValidateExemplar(t *testing.T) {
- exs, err := NewCircularExemplarStorage(2, eMetrics)
+ exs, err := NewCircularExemplarStorage(2, eMetrics, 0)
require.NoError(t, err)
es := exs.(*CircularExemplarStorage)
@@ -76,54 +77,681 @@ func TestValidateExemplar(t *testing.T) {
require.Equal(t, storage.ErrExemplarLabelLength, es.ValidateExemplar(l, e4))
}
-func TestAddExemplar(t *testing.T) {
- exs, err := NewCircularExemplarStorage(2, eMetrics)
- require.NoError(t, err)
- es := exs.(*CircularExemplarStorage)
+func TestCircularExemplarStorage_AddExemplar(t *testing.T) {
+ series1 := labels.FromStrings("trace_id", "foo")
+ series2 := labels.FromStrings("trace_id", "bar")
- l := labels.FromStrings("service", "asdf")
- e := exemplar.Exemplar{
- Labels: labels.FromStrings("trace_id", "qwerty"),
- Value: 0.1,
- Ts: 1,
+ series1Matcher := []*labels.Matcher{{
+ Type: labels.MatchEqual,
+ Name: "trace_id",
+ Value: series1.Get("trace_id"),
+ }}
+
+ series2Matcher := []*labels.Matcher{{
+ Type: labels.MatchEqual,
+ Name: "trace_id",
+ Value: series2.Get("trace_id"),
+ }}
+
+ testCases := []struct {
+ name string
+ size int64
+ exemplars []exemplar.Exemplar
+ wantExemplars []exemplar.Exemplar
+ matcher []*labels.Matcher
+ wantError error
+ }{
+ {
+ name: "insert after newest",
+ size: 3,
+ exemplars: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.1, Ts: 1},
+ {Labels: series1, Value: 0.2, Ts: 2},
+ },
+ matcher: series1Matcher,
+ wantExemplars: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.1, Ts: 1},
+ {Labels: series1, Value: 0.2, Ts: 2},
+ },
+ },
+ {
+ name: "insert before oldest",
+ size: 3,
+ exemplars: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.1, Ts: 2},
+ {Labels: series1, Value: 0.2, Ts: 1},
+ },
+ matcher: series1Matcher,
+ wantExemplars: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.2, Ts: 1},
+ {Labels: series1, Value: 0.1, Ts: 2},
+ },
+ },
+ {
+ name: "insert in between",
+ size: 3,
+ exemplars: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.1, Ts: 1},
+ {Labels: series1, Value: 0.2, Ts: 3},
+ {Labels: series1, Value: 0.3, Ts: 2},
+ },
+ matcher: series1Matcher,
+ wantExemplars: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.1, Ts: 1},
+ {Labels: series1, Value: 0.3, Ts: 2},
+ {Labels: series1, Value: 0.2, Ts: 3},
+ },
+ },
+ {
+ name: "insert after newest with overflow",
+ size: 3,
+ exemplars: []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},
+ },
+ matcher: series1Matcher,
+ wantExemplars: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.2, Ts: 2},
+ {Labels: series1, Value: 0.3, Ts: 3},
+ {Labels: series1, Value: 0.4, Ts: 4},
+ },
+ },
+ {
+ name: "insert before oldest with overflow",
+ size: 3,
+ exemplars: []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: 0},
+ },
+ matcher: series1Matcher,
+ wantExemplars: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.4, Ts: 0},
+ {Labels: series1, Value: 0.2, Ts: 2},
+ {Labels: series1, Value: 0.3, Ts: 3},
+ },
+ },
+ {
+ name: "insert between with overflow",
+ size: 3,
+ exemplars: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.1, Ts: 1},
+ {Labels: series1, Value: 0.2, Ts: 3},
+ {Labels: series1, Value: 0.3, Ts: 4},
+ {Labels: series1, Value: 0.4, Ts: 2},
+ },
+ matcher: series1Matcher,
+ wantExemplars: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.4, Ts: 2},
+ {Labels: series1, Value: 0.2, Ts: 3},
+ {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,
+ exemplars: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.1, Ts: 200},
+ {Labels: series1, Value: 0.2, Ts: 1},
+ },
+ wantError: storage.ErrOutOfOrderExemplar,
+ },
+ {
+ name: "insert multiple series",
+ size: 3,
+ exemplars: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.1, Ts: 1},
+ {Labels: series1, Value: 0.2, Ts: 3},
+ {Labels: series2, Value: 0.3, Ts: 4},
+ },
+ matcher: series1Matcher,
+ wantExemplars: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.1, Ts: 1},
+ {Labels: series1, Value: 0.2, Ts: 3},
+ },
+ },
+ {
+ name: "insert multiple series with overflow",
+ size: 3,
+ exemplars: []exemplar.Exemplar{
+ {Labels: series2, Value: 0.1, Ts: 1},
+ {Labels: series2, Value: 0.2, Ts: 2},
+ {Labels: series2, Value: 0.3, Ts: 3},
+ {Labels: series1, Value: 0.4, Ts: 4},
+ },
+ matcher: series2Matcher,
+ wantExemplars: []exemplar.Exemplar{
+ {Labels: series2, Value: 0.2, Ts: 2},
+ {Labels: series2, Value: 0.3, Ts: 3},
+ },
+ },
+ {
+ name: "series1 overflows series2 out-of-order",
+ size: 3,
+ exemplars: []exemplar.Exemplar{
+ {Labels: series2, Value: 0.1, Ts: 3},
+ {Labels: series2, Value: 0.2, Ts: 2},
+ {Labels: series2, Value: 0.3, Ts: 4},
+ {Labels: series1, Value: 0.4, Ts: 4},
+ {Labels: series1, Value: 0.5, Ts: 1},
+ },
+ matcher: series2Matcher,
+ wantExemplars: []exemplar.Exemplar{
+ {Labels: series2, Value: 0.3, Ts: 4},
+ },
+ },
+ {
+ name: "ignore duplicate exemplars",
+ size: 3,
+ exemplars: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.1, Ts: 3},
+ {Labels: series1, Value: 0.1, Ts: 3},
+ },
+ matcher: series1Matcher,
+ wantExemplars: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.1, Ts: 3},
+ },
+ },
+ {
+ name: "ignore duplicate exemplars when buffer is full",
+ size: 3,
+ exemplars: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.1, Ts: 3},
+ {Labels: series1, Value: 0.2, Ts: 4},
+ {Labels: series1, Value: 0.3, Ts: 5},
+ {Labels: series1, Value: 0.3, Ts: 5},
+ },
+ matcher: series1Matcher,
+ wantExemplars: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.1, Ts: 3},
+ {Labels: series1, Value: 0.2, Ts: 4},
+ {Labels: series1, Value: 0.3, Ts: 5},
+ },
+ },
+ {
+ name: "empty timestamps are valid",
+ size: 3,
+ exemplars: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.1, Ts: 0},
+ {Labels: series1, Value: 0.2, Ts: 0},
+ },
+ matcher: series1Matcher,
+ wantExemplars: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.1, Ts: 0},
+ {Labels: series1, Value: 0.2, Ts: 0},
+ },
+ },
+ {
+ name: "exemplar label length exceeds maximum",
+ size: 3,
+ exemplars: []exemplar.Exemplar{
+ {Labels: labels.FromStrings("a", strings.Repeat("b", exemplar.ExemplarMaxLabelSetLength)), Value: 0.1, Ts: 2},
+ },
+ wantError: storage.ErrExemplarLabelLength,
+ },
+ {
+ name: "native histograms",
+ size: 6,
+ exemplars: []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.1, Ts: 1},
+ {Labels: series1, Value: 0.2, Ts: 2},
+ {Labels: series1, Value: 0.3, Ts: 3},
+ },
+ matcher: series1Matcher,
+ wantExemplars: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.1, Ts: 1},
+ {Labels: series1, Value: 0.2, Ts: 2},
+ {Labels: series1, Value: 0.3, Ts: 3},
+ },
+ },
+ {
+ name: "evict only exemplar for series then re-add",
+ size: 2,
+ exemplars: []exemplar.Exemplar{
+ // series1 at index 0, series2 at index 1, then series1 evicts its own only exemplar
+ {Labels: series1, Value: 0.1, Ts: 1},
+ {Labels: series2, Value: 0.2, Ts: 2},
+ {Labels: series1, Value: 0.3, Ts: 3},
+ },
+ matcher: series1Matcher,
+ wantExemplars: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.3, Ts: 3},
+ },
+ },
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ exs, err := NewCircularExemplarStorage(tc.size, eMetrics, 100)
+ require.NoError(t, err)
+ es := exs.(*CircularExemplarStorage)
+
+ // Add exemplars and compare tc.wantErr against the first exemplar failing.
+ var addError error
+ for i, ex := range tc.exemplars {
+ addError = es.AddExemplar(ex.Labels, ex)
+ if addError != nil {
+ break
+ }
+ if testing.Verbose() {
+ t.Logf("Buffer[%d]:\n%s", i, debugCircularBuffer(es))
+ }
+ }
+ if tc.wantError == nil {
+ require.NoError(t, addError)
+ } else {
+ require.ErrorIs(t, addError, tc.wantError)
+ }
+ if addError != nil {
+ return
+ }
+
+ // Ensure exemplars are returned correctly and in-order.
+ gotExemplars, err := es.Select(0, 1000, tc.matcher)
+ require.NoError(t, err)
+ if len(tc.wantExemplars) == 0 {
+ require.Empty(t, gotExemplars)
+ } else {
+ require.Len(t, gotExemplars, 1)
+ require.Equal(t, tc.wantExemplars, gotExemplars[0].Exemplars)
+ }
+ })
+ }
+}
+
+func TestCircularExemplarStorage_Resize(t *testing.T) {
+ series1 := labels.FromStrings("trace_id", "foo")
+ series2 := labels.FromStrings("trace_id", "bar")
+ matcher1 := []*labels.Matcher{
+ labels.MustNewMatcher(labels.MatchRegexp, "trace_id", "(foo|bar)"),
}
- require.NoError(t, es.AddExemplar(l, e))
- require.Equal(t, 0, es.index[string(l.Bytes(nil))].newest, "exemplar was not stored correctly")
-
- e2 := exemplar.Exemplar{
- Labels: labels.FromStrings("trace_id", "zxcvb"),
- Value: 0.1,
- Ts: 2,
+ testCases := []struct {
+ name string
+ exemplars []exemplar.Exemplar
+ resize int64
+ wantExemplars []exemplar.Exemplar
+ wantNextIndex int
+ wantError error
+ }{
+ {
+ name: "in-order, grow",
+ exemplars: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.1, Ts: 1},
+ {Labels: series1, Value: 0.2, Ts: 2},
+ },
+ resize: 10,
+ wantExemplars: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.1, Ts: 1},
+ {Labels: series1, Value: 0.2, Ts: 2},
+ },
+ wantNextIndex: 3,
+ },
+ {
+ name: "in-order, shrink",
+ exemplars: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.1, Ts: 1},
+ {Labels: series1, Value: 0.2, Ts: 2},
+ {Labels: series1, Value: 0.3, Ts: 3},
+ },
+ resize: 2,
+ wantExemplars: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.2, Ts: 2},
+ {Labels: series1, Value: 0.3, Ts: 3},
+ },
+ wantNextIndex: 0,
+ },
+ {
+ name: "out-of-order, shrink",
+ exemplars: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.3, Ts: 3},
+ {Labels: series1, Value: 0.2, Ts: 2},
+ {Labels: series1, Value: 0.1, Ts: 1},
+ },
+ resize: 2,
+ wantExemplars: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.1, Ts: 1},
+ {Labels: series1, Value: 0.2, Ts: 2},
+ },
+ wantNextIndex: 0,
+ },
+ {
+ name: "out-of-order, grow",
+ exemplars: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.3, Ts: 3},
+ {Labels: series1, Value: 0.2, Ts: 2},
+ },
+ resize: 5,
+ wantExemplars: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.2, Ts: 2},
+ {Labels: series1, Value: 0.3, Ts: 3},
+ },
+ wantNextIndex: 3,
+ },
+ {
+ name: "duplicate timestamps",
+ exemplars: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.1, Ts: 1},
+ {Labels: series1, Value: 0.2, Ts: 1},
+ {Labels: series1, Value: 0.3, Ts: 2},
+ },
+ resize: 3,
+ wantExemplars: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.1, Ts: 1},
+ {Labels: series1, Value: 0.2, Ts: 1},
+ {Labels: series1, Value: 0.3, Ts: 2},
+ },
+ },
+ {
+ name: "empty input, grow",
+ exemplars: []exemplar.Exemplar{},
+ resize: 10,
+ wantExemplars: []exemplar.Exemplar{},
+ wantNextIndex: 3,
+ },
+ {
+ name: "empty input, shrink",
+ exemplars: []exemplar.Exemplar{},
+ resize: 1,
+ wantExemplars: []exemplar.Exemplar{},
+ wantNextIndex: 0,
+ },
+ {
+ name: "shrink to zero",
+ exemplars: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.1, Ts: 1},
+ {Labels: series1, Value: 0.2, Ts: 2},
+ },
+ resize: 0,
+ wantExemplars: []exemplar.Exemplar{},
+ wantNextIndex: 0,
+ },
+ {
+ name: "multiple series, shrink",
+ exemplars: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.1, Ts: 1},
+ {Labels: series2, Value: 1.1, Ts: 2},
+ {Labels: series1, Value: 0.2, Ts: 3},
+ {Labels: series2, Value: 1.2, Ts: 4},
+ },
+ resize: 2,
+ wantExemplars: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.2, Ts: 3},
+ {Labels: series2, Value: 1.2, Ts: 4},
+ },
+ wantNextIndex: 0,
+ },
+ {
+ name: "shrink to one",
+ exemplars: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.1, Ts: 1},
+ {Labels: series1, Value: 0.2, Ts: 2},
+ },
+ resize: 1,
+ wantExemplars: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.2, Ts: 2},
+ },
+ wantNextIndex: 0,
+ },
+ {
+ name: "shrink to two",
+ exemplars: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.1, Ts: 1},
+ },
+ resize: 2,
+ wantExemplars: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.1, Ts: 1},
+ },
+ wantNextIndex: 0,
+ },
}
- require.NoError(t, es.AddExemplar(l, e2))
- require.Equal(t, 1, es.index[string(l.Bytes(nil))].newest, "exemplar was not stored correctly, location of newest exemplar for series in index did not update")
- require.True(t, es.exemplars[es.index[string(l.Bytes(nil))].newest].exemplar.Equals(e2), "exemplar was not stored correctly, expected %+v got: %+v", e2, es.exemplars[es.index[string(l.Bytes(nil))].newest].exemplar)
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ exs, err := NewCircularExemplarStorage(3, eMetrics, 100)
+ require.NoError(t, err)
+ es := exs.(*CircularExemplarStorage)
- require.NoError(t, es.AddExemplar(l, e2), "no error is expected attempting to add duplicate exemplar")
+ for _, ex := range tc.exemplars {
+ require.NoError(t, es.AddExemplar(ex.Labels, ex))
+ }
- e3 := e2
- e3.Ts = 3
- require.NoError(t, es.AddExemplar(l, e3), "no error is expected when attempting to add duplicate exemplar, even with different timestamp")
+ // Resize the circular buffer.
+ if testing.Verbose() {
+ t.Logf("Buffer[before-resize]:\n%s", debugCircularBuffer(es))
+ }
+ es.Resize(tc.resize)
+ if testing.Verbose() {
+ t.Logf("Buffer[after-resize]:\n%s", debugCircularBuffer(es))
+ }
- e3.Ts = 1
- e3.Value = 0.3
- require.Equal(t, storage.ErrOutOfOrderExemplar, es.AddExemplar(l, e3))
-
- e4 := exemplar.Exemplar{
- Labels: labels.FromStrings("a", strings.Repeat("b", exemplar.ExemplarMaxLabelSetLength)),
- Value: 0.1,
- Ts: 2,
+ // Ensure exemplars are returned correctly and in-order.
+ gotExemplars, err := es.Select(0, 1000, matcher1)
+ require.NoError(t, err)
+ flat := make([]exemplar.Exemplar, 0)
+ for _, group := range gotExemplars {
+ flat = append(flat, group.Exemplars...)
+ }
+ sort.Slice(flat, func(i, j int) bool {
+ return flat[i].Ts < flat[j].Ts
+ })
+ require.Equal(t, tc.wantExemplars, flat, "exemplar mismatch")
+ require.Equal(t, tc.wantNextIndex, es.nextIndex, "next index mismatch")
+ })
+ }
+
+ resizeTwiceCases := []struct {
+ name string
+ addExemplars1 []exemplar.Exemplar
+ resize1 int64
+ wantExemplars1 []exemplar.Exemplar
+ resize2 int64
+ addExemplars2 []exemplar.Exemplar
+ wantExemplars2 []exemplar.Exemplar
+ }{
+ {
+ name: "shrink then grow ordered",
+ addExemplars1: []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},
+ },
+ resize1: 2,
+ wantExemplars1: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.3, Ts: 3},
+ {Labels: series1, Value: 0.4, Ts: 4},
+ },
+ resize2: 5,
+ addExemplars2: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.5, Ts: 5},
+ {Labels: series1, Value: 0.6, Ts: 6},
+ {Labels: series1, Value: 0.7, Ts: 7},
+ },
+ wantExemplars2: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.3, Ts: 3},
+ {Labels: series1, Value: 0.4, Ts: 4},
+ {Labels: series1, Value: 0.5, Ts: 5},
+ {Labels: series1, Value: 0.6, Ts: 6},
+ {Labels: series1, Value: 0.7, Ts: 7},
+ },
+ },
+ {
+ name: "shrink then grow out-of-order",
+ addExemplars1: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.1, Ts: 1},
+ {Labels: series1, Value: 0.4, Ts: 4},
+ {Labels: series1, Value: 0.2, Ts: 2},
+ {Labels: series1, Value: 0.3, Ts: 3},
+ },
+ resize1: 2,
+ wantExemplars1: []exemplar.Exemplar{
+ // We delete in the order of ingestion, not temporally.
+ {Labels: series1, Value: 0.2, Ts: 2},
+ {Labels: series1, Value: 0.3, Ts: 3},
+ },
+ resize2: 5,
+ addExemplars2: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.7, Ts: 7},
+ {Labels: series1, Value: 0.6, Ts: 6},
+ {Labels: series1, Value: 0.5, Ts: 5},
+ },
+ wantExemplars2: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.2, Ts: 2},
+ {Labels: series1, Value: 0.3, Ts: 3},
+ {Labels: series1, Value: 0.5, Ts: 5},
+ {Labels: series1, Value: 0.6, Ts: 6},
+ {Labels: series1, Value: 0.7, Ts: 7},
+ },
+ },
+ {
+ name: "grow then shrink ordered",
+ addExemplars1: []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},
+ },
+ resize1: 5,
+ wantExemplars1: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.2, Ts: 2},
+ {Labels: series1, Value: 0.3, Ts: 3},
+ {Labels: series1, Value: 0.4, Ts: 4},
+ },
+ resize2: 2,
+ addExemplars2: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.5, Ts: 5},
+ {Labels: series1, Value: 0.6, Ts: 6},
+ {Labels: series1, Value: 0.7, Ts: 7},
+ },
+ wantExemplars2: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.6, Ts: 6},
+ {Labels: series1, Value: 0.7, Ts: 7},
+ },
+ },
+ {
+ name: "grow then shrink out-of-order",
+ addExemplars1: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.1, Ts: 1},
+ {Labels: series1, Value: 0.4, Ts: 4},
+ {Labels: series1, Value: 0.2, Ts: 2},
+ {Labels: series1, Value: 0.3, Ts: 3},
+ },
+ resize1: 5,
+ wantExemplars1: []exemplar.Exemplar{
+ // We delete in the order of ingestion, not temporally.
+ {Labels: series1, Value: 0.2, Ts: 2},
+ {Labels: series1, Value: 0.3, Ts: 3},
+ {Labels: series1, Value: 0.4, Ts: 4},
+ },
+ resize2: 2,
+ addExemplars2: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.7, Ts: 7},
+ {Labels: series1, Value: 0.5, Ts: 5},
+ {Labels: series1, Value: 0.6, Ts: 6},
+ },
+ wantExemplars2: []exemplar.Exemplar{
+ {Labels: series1, Value: 0.5, Ts: 5},
+ {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 {
+ t.Run(tc.name, func(t *testing.T) {
+ exs, err := NewCircularExemplarStorage(3, eMetrics, 100)
+ require.NoError(t, err)
+ es := exs.(*CircularExemplarStorage)
+ for _, ex := range tc.addExemplars1 {
+ require.NoError(t, es.AddExemplar(ex.Labels, ex))
+ }
+ es.Resize(tc.resize1)
+ gotExemplars, err := es.Select(0, 1000, matcher1)
+ require.NoError(t, err)
+ require.Len(t, gotExemplars, 1)
+ require.Equal(t, tc.wantExemplars1, gotExemplars[0].Exemplars)
+ es.Resize(tc.resize2)
+ for _, ex := range tc.addExemplars2 {
+ require.NoError(t, es.AddExemplar(ex.Labels, ex))
+ }
+ if testing.Verbose() {
+ t.Logf("Buffer[after-resize2]:\n%s", debugCircularBuffer(es))
+ }
+ gotExemplars, err = es.Select(0, 1000, matcher1)
+ require.NoError(t, err)
+ require.Len(t, gotExemplars, 1)
+ require.Equal(t, tc.wantExemplars2, gotExemplars[0].Exemplars)
+ })
}
- require.Equal(t, storage.ErrExemplarLabelLength, es.AddExemplar(l, e4))
}
func TestStorageOverflow(t *testing.T) {
// Test that circular buffer index and assignment
// works properly, adding more exemplars than can
// be stored and then querying for them.
- exs, err := NewCircularExemplarStorage(5, eMetrics)
+ exs, err := NewCircularExemplarStorage(5, eMetrics, 0)
require.NoError(t, err)
es := exs.(*CircularExemplarStorage)
@@ -152,7 +780,7 @@ func TestStorageOverflow(t *testing.T) {
}
func TestSelectExemplar(t *testing.T) {
- exs, err := NewCircularExemplarStorage(5, eMetrics)
+ exs, err := NewCircularExemplarStorage(5, eMetrics, 0)
require.NoError(t, err)
es := exs.(*CircularExemplarStorage)
@@ -179,7 +807,7 @@ func TestSelectExemplar(t *testing.T) {
}
func TestSelectExemplar_MultiSeries(t *testing.T) {
- exs, err := NewCircularExemplarStorage(5, eMetrics)
+ exs, err := NewCircularExemplarStorage(5, eMetrics, 0)
require.NoError(t, err)
es := exs.(*CircularExemplarStorage)
@@ -223,7 +851,7 @@ func TestSelectExemplar_MultiSeries(t *testing.T) {
func TestSelectExemplar_TimeRange(t *testing.T) {
var lenEs int64 = 5
- exs, err := NewCircularExemplarStorage(lenEs, eMetrics)
+ exs, err := NewCircularExemplarStorage(lenEs, eMetrics, 0)
require.NoError(t, err)
es := exs.(*CircularExemplarStorage)
@@ -251,7 +879,7 @@ func TestSelectExemplar_TimeRange(t *testing.T) {
// Test to ensure that even though a series matches more than one matcher from the
// query that it's exemplars are only included in the result a single time.
func TestSelectExemplar_DuplicateSeries(t *testing.T) {
- exs, err := NewCircularExemplarStorage(4, eMetrics)
+ exs, err := NewCircularExemplarStorage(4, eMetrics, 0)
require.NoError(t, err)
es := exs.(*CircularExemplarStorage)
@@ -286,7 +914,7 @@ func TestSelectExemplar_DuplicateSeries(t *testing.T) {
}
func TestIndexOverwrite(t *testing.T) {
- exs, err := NewCircularExemplarStorage(2, eMetrics)
+ exs, err := NewCircularExemplarStorage(2, eMetrics, 0)
require.NoError(t, err)
es := exs.(*CircularExemplarStorage)
@@ -374,7 +1002,7 @@ func TestResize(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
- exs, err := NewCircularExemplarStorage(tc.startSize, eMetrics)
+ exs, err := NewCircularExemplarStorage(tc.startSize, eMetrics, 0)
require.NoError(t, err)
es := exs.(*CircularExemplarStorage)
@@ -386,7 +1014,14 @@ func TestResize(t *testing.T) {
require.NoError(t, err)
}
+ if testing.Verbose() {
+ t.Logf("Buffer[before-resize]:\n%s", debugCircularBuffer(es))
+ }
resized := es.Resize(tc.newCount)
+ if testing.Verbose() {
+ t.Logf("Buffer[after-resize]:\n%s", debugCircularBuffer(es))
+ }
+
require.Equal(t, tc.expectedMigrated, resized)
q, err := es.Querier(context.TODO())
@@ -421,7 +1056,7 @@ func BenchmarkAddExemplar(b *testing.B) {
b.Run(fmt.Sprintf("%d/%d", n, capacity), func(b *testing.B) {
for b.Loop() {
b.StopTimer()
- exs, err := NewCircularExemplarStorage(int64(capacity), eMetrics)
+ exs, err := NewCircularExemplarStorage(int64(capacity), eMetrics, 0)
require.NoError(b, err)
es := exs.(*CircularExemplarStorage)
var l labels.Labels
@@ -442,6 +1077,91 @@ func BenchmarkAddExemplar(b *testing.B) {
}
}
+func BenchmarkAddExemplar_OutOfOrder(b *testing.B) {
+ // We need to include these labels since we do length calculation
+ // before adding.
+ exLabels := labels.FromStrings("trace_id", "89620921")
+
+ const (
+ capacity = 5000
+ )
+
+ fillOneSeries := func(es *CircularExemplarStorage) {
+ for i := range capacity {
+ e := exemplar.Exemplar{Value: float64(i), Ts: int64(i), Labels: exLabels}
+ if err := es.AddExemplar(exLabels, e); err != nil {
+ panic(err)
+ }
+ }
+ }
+
+ fillMultipleSeries := func(es *CircularExemplarStorage) {
+ for i := range capacity {
+ l := labels.FromStrings("service", strconv.Itoa(i))
+ e := exemplar.Exemplar{Value: float64(i), Ts: int64(i), Labels: l}
+ if err := es.AddExemplar(l, e); err != nil {
+ panic(err)
+ }
+ }
+ }
+
+ outOfOrder := func(ts *int64, _ *labels.Labels) {
+ switch *ts % 3 {
+ case 0:
+ return
+ case 1:
+ *ts = capacity - *ts
+ case 2:
+ *ts = (capacity - *ts) + 100
+ }
+ }
+
+ reverseOrder := func(ts *int64, _ *labels.Labels) {
+ *ts = capacity - *ts
+ }
+
+ multipleSeries := func(f func(*int64, *labels.Labels)) func(*int64, *labels.Labels) {
+ return func(ts *int64, l *labels.Labels) {
+ f(ts, l)
+ *l = labels.FromStrings("service", strconv.Itoa(int(*ts)))
+ }
+ }
+
+ for fillName, setup := range map[string]func(es *CircularExemplarStorage){
+ "empty": func(*CircularExemplarStorage) {},
+ "full-one": fillOneSeries,
+ "full-multiple": fillMultipleSeries,
+ } {
+ for orderName, forEach := range map[string]func(ts *int64, l *labels.Labels){
+ "in-order": func(*int64, *labels.Labels) {},
+ "reverse": reverseOrder,
+ "out-of-order": outOfOrder,
+ "multi-in-order": multipleSeries(func(*int64, *labels.Labels) {}),
+ "multi-reverse": multipleSeries(reverseOrder),
+ "multi-out-of-order": multipleSeries(outOfOrder),
+ } {
+ b.Run(fmt.Sprintf("%s/%s", fillName, orderName), func(b *testing.B) {
+ exs, err := NewCircularExemplarStorage(int64(capacity), eMetrics, 100000)
+ require.NoError(b, err)
+ es := exs.(*CircularExemplarStorage)
+ l := labels.FromStrings("service", "0")
+ setup(es)
+ b.ResetTimer()
+ for b.Loop() {
+ for i := range capacity {
+ ts := int64(i)
+ forEach(&ts, &l)
+ err = es.AddExemplar(l, exemplar.Exemplar{Value: float64(i), Ts: ts, Labels: l})
+ if err != nil {
+ b.Fatalf("Failed to insert item %d %s: %v", i, l, err)
+ }
+ }
+ }
+ })
+ }
+ }
+}
+
func BenchmarkResizeExemplars(b *testing.B) {
testCases := []struct {
name string
@@ -479,7 +1199,7 @@ func BenchmarkResizeExemplars(b *testing.B) {
b.Run(fmt.Sprintf("%s-%d-to-%d", tc.name, tc.startSize, tc.endSize), func(b *testing.B) {
for b.Loop() {
b.StopTimer()
- exs, err := NewCircularExemplarStorage(tc.startSize, eMetrics)
+ exs, err := NewCircularExemplarStorage(tc.startSize, eMetrics, 0)
require.NoError(b, err)
es := exs.(*CircularExemplarStorage)
@@ -504,7 +1224,7 @@ func BenchmarkResizeExemplars(b *testing.B) {
// TestCircularExemplarStorage_Concurrent_AddExemplar_Resize tries to provoke a data race between AddExemplar and Resize.
// Run with race detection enabled.
func TestCircularExemplarStorage_Concurrent_AddExemplar_Resize(t *testing.T) {
- exs, err := NewCircularExemplarStorage(0, eMetrics)
+ exs, err := NewCircularExemplarStorage(0, eMetrics, 0)
require.NoError(t, err)
es := exs.(*CircularExemplarStorage)
@@ -537,3 +1257,28 @@ func TestCircularExemplarStorage_Concurrent_AddExemplar_Resize(t *testing.T) {
}
}
}
+
+// debugCircularBuffer iterates all exemplars in the circular exemplar storage
+// and returns them as a string. The textual representation contains index
+// pointers and helps debugging exemplar storage.
+func debugCircularBuffer(ce *CircularExemplarStorage) string {
+ var sb strings.Builder
+ for i, e := range ce.exemplars {
+ if e.ref == nil {
+ continue
+ }
+ fmt.Fprintf(&sb, "i: %d, ts: %d, next: %d, prev: %d",
+ i, e.exemplar.Ts, e.next, e.prev)
+ for _, idx := range ce.index {
+ if i == idx.newest {
+ sb.WriteString(" <- newest " + idx.seriesLabels.String())
+ }
+ if i == idx.oldest {
+ sb.WriteString(" <- oldest " + idx.seriesLabels.String())
+ }
+ }
+ sb.WriteString("\n")
+ }
+ fmt.Fprintf(&sb, "Next index: %d\n", ce.nextIndex)
+ return sb.String()
+}
diff --git a/tsdb/fileutil/dir.go b/tsdb/fileutil/dir.go
index ad039d2231..795c9f221b 100644
--- a/tsdb/fileutil/dir.go
+++ b/tsdb/fileutil/dir.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The Prometheus Authors
+// 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
diff --git a/tsdb/fileutil/dir_unix.go b/tsdb/fileutil/dir_unix.go
index 2afb2aeaba..05c24893cd 100644
--- a/tsdb/fileutil/dir_unix.go
+++ b/tsdb/fileutil/dir_unix.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The etcd Authors
+// 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.
diff --git a/tsdb/fileutil/dir_windows.go b/tsdb/fileutil/dir_windows.go
index 307077ebc3..cfd55291d5 100644
--- a/tsdb/fileutil/dir_windows.go
+++ b/tsdb/fileutil/dir_windows.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The etcd Authors
+// 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.
diff --git a/tsdb/fileutil/direct_io.go b/tsdb/fileutil/direct_io.go
index ad306776ca..76815de6b1 100644
--- a/tsdb/fileutil/direct_io.go
+++ b/tsdb/fileutil/direct_io.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Prometheus Authors
+// 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
diff --git a/tsdb/fileutil/direct_io_force.go b/tsdb/fileutil/direct_io_force.go
index bb65403911..8ae4ef4fd7 100644
--- a/tsdb/fileutil/direct_io_force.go
+++ b/tsdb/fileutil/direct_io_force.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Prometheus Authors
+// 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
diff --git a/tsdb/fileutil/direct_io_linux.go b/tsdb/fileutil/direct_io_linux.go
index a1d5f9577d..0640b503f6 100644
--- a/tsdb/fileutil/direct_io_linux.go
+++ b/tsdb/fileutil/direct_io_linux.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Prometheus Authors
+// 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
diff --git a/tsdb/fileutil/direct_io_unsupported.go b/tsdb/fileutil/direct_io_unsupported.go
index a03782fe42..f17c68705f 100644
--- a/tsdb/fileutil/direct_io_unsupported.go
+++ b/tsdb/fileutil/direct_io_unsupported.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Prometheus Authors
+// 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
diff --git a/tsdb/fileutil/direct_io_writer.go b/tsdb/fileutil/direct_io_writer.go
index 793d081481..3eeb2aa225 100644
--- a/tsdb/fileutil/direct_io_writer.go
+++ b/tsdb/fileutil/direct_io_writer.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Prometheus Authors
+// 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
diff --git a/tsdb/fileutil/direct_io_writer_test.go b/tsdb/fileutil/direct_io_writer_test.go
index e60df1f3bc..367b7fa6aa 100644
--- a/tsdb/fileutil/direct_io_writer_test.go
+++ b/tsdb/fileutil/direct_io_writer_test.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Prometheus Authors
+// 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
diff --git a/tsdb/fileutil/fileutil.go b/tsdb/fileutil/fileutil.go
index 523f99292c..0aa67e113a 100644
--- a/tsdb/fileutil/fileutil.go
+++ b/tsdb/fileutil/fileutil.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Prometheus Authors
+// 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
diff --git a/tsdb/fileutil/flock.go b/tsdb/fileutil/flock.go
index e0082e2f2c..345581cc92 100644
--- a/tsdb/fileutil/flock.go
+++ b/tsdb/fileutil/flock.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The Prometheus Authors
+// 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
diff --git a/tsdb/fileutil/flock_js.go b/tsdb/fileutil/flock_js.go
index 6029cdf4d8..025e678a1d 100644
--- a/tsdb/fileutil/flock_js.go
+++ b/tsdb/fileutil/flock_js.go
@@ -1,4 +1,4 @@
-// Copyright 2022 The Prometheus Authors
+// 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
diff --git a/tsdb/fileutil/flock_plan9.go b/tsdb/fileutil/flock_plan9.go
index 3b9550e7f2..543195e066 100644
--- a/tsdb/fileutil/flock_plan9.go
+++ b/tsdb/fileutil/flock_plan9.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The Prometheus Authors
+// 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
diff --git a/tsdb/fileutil/flock_solaris.go b/tsdb/fileutil/flock_solaris.go
index 8ca919f3b0..b7a69d9063 100644
--- a/tsdb/fileutil/flock_solaris.go
+++ b/tsdb/fileutil/flock_solaris.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The Prometheus Authors
+// 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
diff --git a/tsdb/fileutil/flock_test.go b/tsdb/fileutil/flock_test.go
index 7aff789a26..dec7d4e98d 100644
--- a/tsdb/fileutil/flock_test.go
+++ b/tsdb/fileutil/flock_test.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The Prometheus Authors
+// 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
diff --git a/tsdb/fileutil/flock_unix.go b/tsdb/fileutil/flock_unix.go
index 25de0ffb22..eddf427e7e 100644
--- a/tsdb/fileutil/flock_unix.go
+++ b/tsdb/fileutil/flock_unix.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The Prometheus Authors
+// 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
diff --git a/tsdb/fileutil/flock_windows.go b/tsdb/fileutil/flock_windows.go
index 1c17ff4ea3..64ce827324 100644
--- a/tsdb/fileutil/flock_windows.go
+++ b/tsdb/fileutil/flock_windows.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The Prometheus Authors
+// 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
diff --git a/tsdb/fileutil/mmap.go b/tsdb/fileutil/mmap.go
index 782ff27ec9..9893d1014b 100644
--- a/tsdb/fileutil/mmap.go
+++ b/tsdb/fileutil/mmap.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Prometheus Authors
+// 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
diff --git a/tsdb/fileutil/mmap_386.go b/tsdb/fileutil/mmap_386.go
index 85c0cce096..01e4333a42 100644
--- a/tsdb/fileutil/mmap_386.go
+++ b/tsdb/fileutil/mmap_386.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Prometheus Authors
+// 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
diff --git a/tsdb/fileutil/mmap_amd64.go b/tsdb/fileutil/mmap_amd64.go
index 71fc568bd5..6d426f1866 100644
--- a/tsdb/fileutil/mmap_amd64.go
+++ b/tsdb/fileutil/mmap_amd64.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Prometheus Authors
+// 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
diff --git a/tsdb/fileutil/mmap_arm64.go b/tsdb/fileutil/mmap_arm64.go
index 71fc568bd5..6d426f1866 100644
--- a/tsdb/fileutil/mmap_arm64.go
+++ b/tsdb/fileutil/mmap_arm64.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Prometheus Authors
+// 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
diff --git a/tsdb/fileutil/mmap_js.go b/tsdb/fileutil/mmap_js.go
index f29106fc1e..59e1fcf877 100644
--- a/tsdb/fileutil/mmap_js.go
+++ b/tsdb/fileutil/mmap_js.go
@@ -1,4 +1,4 @@
-// Copyright 2022 The Prometheus Authors
+// 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
diff --git a/tsdb/fileutil/mmap_unix.go b/tsdb/fileutil/mmap_unix.go
index 3d15e1a8c1..b35352fef9 100644
--- a/tsdb/fileutil/mmap_unix.go
+++ b/tsdb/fileutil/mmap_unix.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
diff --git a/tsdb/fileutil/mmap_windows.go b/tsdb/fileutil/mmap_windows.go
index b942264123..8322f68971 100644
--- a/tsdb/fileutil/mmap_windows.go
+++ b/tsdb/fileutil/mmap_windows.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
@@ -27,14 +27,15 @@ func mmap(f *os.File, size int) ([]byte, error) {
}
addr, errno := syscall.MapViewOfFile(h, syscall.FILE_MAP_READ, 0, 0, uintptr(size))
- if addr == 0 {
- return nil, os.NewSyscallError("MapViewOfFile", errno)
- }
if err := syscall.CloseHandle(syscall.Handle(h)); err != nil {
return nil, os.NewSyscallError("CloseHandle", err)
}
+ if addr == 0 {
+ return nil, os.NewSyscallError("MapViewOfFile", errno)
+ }
+
return (*[maxMapSize]byte)(unsafe.Pointer(addr))[:size], nil
}
diff --git a/tsdb/fileutil/preallocate.go b/tsdb/fileutil/preallocate.go
index c747b7cf81..e9a587b2bd 100644
--- a/tsdb/fileutil/preallocate.go
+++ b/tsdb/fileutil/preallocate.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The etcd Authors
+// 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.
diff --git a/tsdb/fileutil/preallocate_darwin.go b/tsdb/fileutil/preallocate_darwin.go
index 1d9eb806d1..58f83c5ba5 100644
--- a/tsdb/fileutil/preallocate_darwin.go
+++ b/tsdb/fileutil/preallocate_darwin.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The etcd Authors
+// 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.
diff --git a/tsdb/fileutil/preallocate_linux.go b/tsdb/fileutil/preallocate_linux.go
index 026c69b354..1271c48928 100644
--- a/tsdb/fileutil/preallocate_linux.go
+++ b/tsdb/fileutil/preallocate_linux.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The etcd Authors
+// 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.
diff --git a/tsdb/fileutil/preallocate_other.go b/tsdb/fileutil/preallocate_other.go
index e7fd937a43..55a44c7636 100644
--- a/tsdb/fileutil/preallocate_other.go
+++ b/tsdb/fileutil/preallocate_other.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The etcd Authors
+// 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.
diff --git a/tsdb/fileutil/sync.go b/tsdb/fileutil/sync.go
index e1a4a7fd3d..9390b044a5 100644
--- a/tsdb/fileutil/sync.go
+++ b/tsdb/fileutil/sync.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The etcd Authors
+// 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.
diff --git a/tsdb/fileutil/sync_darwin.go b/tsdb/fileutil/sync_darwin.go
index d698b896af..3dc42fc57a 100644
--- a/tsdb/fileutil/sync_darwin.go
+++ b/tsdb/fileutil/sync_darwin.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The etcd Authors
+// 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.
diff --git a/tsdb/fileutil/sync_linux.go b/tsdb/fileutil/sync_linux.go
index 2b4c620bb0..138bbee1e5 100644
--- a/tsdb/fileutil/sync_linux.go
+++ b/tsdb/fileutil/sync_linux.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The etcd Authors
+// 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.
diff --git a/tsdb/goversion/goversion.go b/tsdb/goversion/goversion.go
index ec23d25f2e..050ced875d 100644
--- a/tsdb/goversion/goversion.go
+++ b/tsdb/goversion/goversion.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
diff --git a/tsdb/goversion/goversion_test.go b/tsdb/goversion/goversion_test.go
index 853844fb93..1e52b9655c 100644
--- a/tsdb/goversion/goversion_test.go
+++ b/tsdb/goversion/goversion_test.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
diff --git a/tsdb/goversion/init.go b/tsdb/goversion/init.go
index dd15e1f7af..eb97bf7637 100644
--- a/tsdb/goversion/init.go
+++ b/tsdb/goversion/init.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
diff --git a/tsdb/head.go b/tsdb/head.go
index 4e77314b02..6fe42c8cf2 100644
--- a/tsdb/head.go
+++ b/tsdb/head.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
@@ -40,7 +40,6 @@ import (
"github.com/prometheus/prometheus/storage"
"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/index"
"github.com/prometheus/prometheus/tsdb/record"
"github.com/prometheus/prometheus/tsdb/tombstones"
@@ -187,6 +186,20 @@ type HeadOptions struct {
// EnableSharding enables ShardedPostings() support in the Head.
EnableSharding bool
+
+ // EnableSTAsZeroSample represents 'created-timestamp-zero-ingestion' feature flag.
+ // If true, ST, if non-empty and earlier than sample timestamp, will be stored
+ // as a zero sample before the actual sample.
+ //
+ // The zero sample is best-effort, only debug log on failure is emitted.
+ // NOTE(bwplotka): This feature might be deprecated and removed once PROM-60
+ // is implemented.
+ EnableSTAsZeroSample bool
+
+ // EnableMetadataWALRecords represents 'metadata-wal-records' feature flag.
+ // NOTE(bwplotka): This feature might be deprecated and removed once PROM-60
+ // is implemented.
+ EnableMetadataWALRecords bool
}
const (
@@ -313,7 +326,7 @@ func (h *Head) resetInMemoryState() error {
if em == nil {
em = NewExemplarMetrics(h.reg)
}
- es, err := NewCircularExemplarStorage(h.opts.MaxExemplars.Load(), em)
+ es, err := NewCircularExemplarStorage(h.opts.MaxExemplars.Load(), em, h.opts.OutOfOrderTimeWindow.Load())
if err != nil {
return err
}
@@ -562,6 +575,7 @@ func newHeadMetrics(h *Head, r prometheus.Registerer) *headMetrics {
m.checkpointDeleteTotal,
m.checkpointCreationFail,
m.checkpointCreationTotal,
+ m.oooHistogram,
m.mmapChunksTotal,
m.mmapChunkCorruptionTotal,
m.snapshotReplayErrorTotal,
@@ -970,7 +984,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
}
@@ -1022,6 +1036,8 @@ func (h *Head) ApplyConfig(cfg *config.Config, wbl *wlog.WL) {
return
}
+ h.exemplars.(*CircularExemplarStorage).SetOutOfOrderTimeWindow(oooTimeWindow)
+
// Head uses opts.MaxExemplars in combination with opts.EnableExemplarStorage
// to decide if it should pass exemplars along to its exemplar storage, so we
// need to update opts.MaxExemplars here.
@@ -1186,6 +1202,36 @@ func (h *Head) truncateMemory(mint int64) (err error) {
return h.truncateSeriesAndChunkDiskMapper("truncateMemory")
}
+// truncateStaleSeries removes the provided series as long as they are still stale.
+func (h *Head) truncateStaleSeries(seriesRefs []storage.SeriesRef, maxt int64) error {
+ h.chunkSnapshotMtx.Lock()
+ defer h.chunkSnapshotMtx.Unlock()
+
+ if h.MinTime() >= maxt {
+ return nil
+ }
+
+ h.WaitForPendingReadersInTimeRange(h.MinTime(), maxt)
+
+ deleted := h.gcStaleSeries(seriesRefs, maxt)
+
+ // Record these stale series refs in the WAL so that we can ignore them during replay.
+ if h.wal != nil {
+ stones := make([]tombstones.Stone, 0, len(seriesRefs))
+ for ref := range deleted {
+ stones = append(stones, tombstones.Stone{
+ Ref: ref,
+ Intervals: tombstones.Intervals{{Mint: math.MinInt64, Maxt: math.MaxInt64}},
+ })
+ }
+ var enc record.Encoder
+ if err := h.wal.Log(enc.Tombstones(stones, nil)); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
// WaitForPendingReadersInTimeRange waits for queries overlapping with given range to finish querying.
// The query timeout limits the max wait time of this function implicitly.
// The mint is inclusive and maxt is the truncation time hence exclusive.
@@ -1539,6 +1585,53 @@ func (h *RangeHead) String() string {
return fmt.Sprintf("range head (mint: %d, maxt: %d)", h.MinTime(), h.MaxTime())
}
+// StaleHead allows querying the stale series in the Head via an IndexReader, ChunkReader and tombstones.Reader.
+// Used only for compactions.
+type StaleHead struct {
+ RangeHead
+ staleSeriesRefs []storage.SeriesRef
+}
+
+// NewStaleHead returns a *StaleHead.
+func NewStaleHead(head *Head, mint, maxt int64, staleSeriesRefs []storage.SeriesRef) *StaleHead {
+ return &StaleHead{
+ RangeHead: RangeHead{
+ head: head,
+ mint: mint,
+ maxt: maxt,
+ },
+ staleSeriesRefs: staleSeriesRefs,
+ }
+}
+
+func (h *StaleHead) Index() (_ IndexReader, err error) {
+ return h.head.staleIndex(h.mint, h.maxt, h.staleSeriesRefs)
+}
+
+func (h *StaleHead) NumSeries() uint64 {
+ return h.head.NumStaleSeries()
+}
+
+var staleHeadULID = ulid.MustParse("0000000000XXXXXXXSTALEHEAD")
+
+func (h *StaleHead) Meta() BlockMeta {
+ return BlockMeta{
+ MinTime: h.MinTime(),
+ MaxTime: h.MaxTime(),
+ ULID: staleHeadULID,
+ Stats: BlockStats{
+ NumSeries: h.NumSeries(),
+ },
+ }
+}
+
+// String returns an human readable representation of the stake head. It's important to
+// keep this function in order to avoid the struct dump when the head is stringified in
+// errors or logs.
+func (h *StaleHead) String() string {
+ return fmt.Sprintf("stale head (mint: %d, maxt: %d)", h.MinTime(), h.MaxTime())
+}
+
// Delete all samples in the range of [mint, maxt] for series that satisfy the given
// label matchers.
func (h *Head) Delete(ctx context.Context, mint, maxt int64, ms ...*labels.Matcher) error {
@@ -1608,13 +1701,14 @@ func (h *Head) gc() (actualInOrderMint, minOOOTime int64, minMmapFile int) {
// Drop old chunks and remember series IDs and hashes if they can be
// deleted entirely.
- deleted, affected, chunksRemoved, actualInOrderMint, minOOOTime, minMmapFile := h.series.gc(mint, minOOOMmapRef, &h.numStaleSeries)
+ deleted, affected, chunksRemoved, staleSeriesDeleted, actualInOrderMint, minOOOTime, minMmapFile := h.series.gc(mint, minOOOMmapRef)
seriesRemoved := len(deleted)
h.metrics.seriesRemoved.Add(float64(seriesRemoved))
h.metrics.chunksRemoved.Add(float64(chunksRemoved))
h.metrics.chunks.Sub(float64(chunksRemoved))
h.numSeries.Sub(uint64(seriesRemoved))
+ h.numStaleSeries.Sub(uint64(staleSeriesDeleted))
// Remove deleted series IDs from the postings lists.
h.postings.Delete(deleted, affected)
@@ -1717,17 +1811,17 @@ func (h *Head) Close() error {
// takes samples from most recent head chunk.
h.mmapHeadChunks()
- errs := tsdb_errors.NewMulti(h.chunkDiskMapper.Close())
+ errs := h.chunkDiskMapper.Close()
if h.wal != nil {
- errs.Add(h.wal.Close())
+ errs = errors.Join(errs, h.wal.Close())
}
if h.wbl != nil {
- errs.Add(h.wbl.Close())
+ errs = errors.Join(errs, h.wbl.Close())
}
- if errs.Err() == nil && h.opts.EnableMemorySnapshotOnShutdown {
- errs.Add(h.performChunkSnapshot())
+ if errs == nil && h.opts.EnableMemorySnapshotOnShutdown {
+ errs = errors.Join(errs, h.performChunkSnapshot())
}
- return errs.Err()
+ return errs
}
// String returns an human readable representation of the TSDB head. It's important to
@@ -1738,32 +1832,31 @@ func (*Head) String() string {
}
func (h *Head) getOrCreate(hash uint64, lset labels.Labels, pendingCommit bool) (*memSeries, bool, error) {
- // Just using `getOrCreateWithID` below would be semantically sufficient, but we'd create
- // a new series on every sample inserted via Add(), which causes allocations
- // and makes our series IDs rather random and harder to compress in postings.
s := h.series.getByHash(hash, lset)
if s != nil {
return s, false, nil
}
- // Optimistically assume that we are the first one to create the series.
- id := chunks.HeadSeriesRef(h.lastSeriesID.Inc())
-
- return h.getOrCreateWithID(id, hash, lset, pendingCommit)
+ return h.getOrCreateWithOptionalID(0, hash, lset, pendingCommit)
}
-func (h *Head) getOrCreateWithID(id chunks.HeadSeriesRef, hash uint64, lset labels.Labels, pendingCommit bool) (*memSeries, bool, error) {
- s, created, err := h.series.getOrSet(hash, lset, func() *memSeries {
- shardHash := uint64(0)
- if h.opts.EnableSharding {
- shardHash = labels.StableHash(lset)
- }
-
- return newMemSeries(lset, id, shardHash, h.opts.IsolationDisabled, pendingCommit)
- })
- if err != nil {
- return nil, false, err
+// If id is zero, one will be allocated.
+func (h *Head) getOrCreateWithOptionalID(id chunks.HeadSeriesRef, hash uint64, lset labels.Labels, pendingCommit bool) (*memSeries, bool, error) {
+ if preCreationErr := h.series.seriesLifecycleCallback.PreCreation(lset); preCreationErr != nil {
+ return nil, false, preCreationErr
}
+ if id == 0 {
+ // Note this id is wasted in the case where a concurrent operation creates the same series first.
+ id = chunks.HeadSeriesRef(h.lastSeriesID.Inc())
+ }
+
+ shardHash := uint64(0)
+ if h.opts.EnableSharding {
+ shardHash = labels.StableHash(lset)
+ }
+ optimisticallyCreatedSeries := newMemSeries(lset, id, shardHash, h.opts.IsolationDisabled, pendingCommit)
+
+ s, created := h.series.setUnlessAlreadySet(hash, lset, optimisticallyCreatedSeries)
if !created {
return s, false, nil
}
@@ -1932,13 +2025,14 @@ func newStripeSeries(stripeSize int, seriesCallback SeriesLifecycleCallback) *st
// but the returned map goes into postings.Delete() which expects a map[storage.SeriesRef]struct
// and there's no easy way to cast maps.
// minMmapFile is the min mmap file number seen in the series (in-order and out-of-order) after gc'ing the series.
-func (s *stripeSeries) gc(mint int64, minOOOMmapRef chunks.ChunkDiskMapperRef, numStaleSeries *atomic.Uint64) (_ map[storage.SeriesRef]struct{}, _ map[labels.Label]struct{}, _ int, _, _ int64, minMmapFile int) {
+func (s *stripeSeries) gc(mint int64, minOOOMmapRef chunks.ChunkDiskMapperRef) (_ map[storage.SeriesRef]struct{}, _ map[labels.Label]struct{}, _, _ int, _, _ int64, minMmapFile int) {
var (
- deleted = map[storage.SeriesRef]struct{}{}
- affected = map[labels.Label]struct{}{}
- rmChunks = 0
- actualMint int64 = math.MaxInt64
- minOOOTime int64 = math.MaxInt64
+ deleted = map[storage.SeriesRef]struct{}{}
+ affected = map[labels.Label]struct{}{}
+ rmChunks = 0
+ staleSeriesDeleted = 0
+ actualMint int64 = math.MaxInt64
+ minOOOTime int64 = math.MaxInt64
)
minMmapFile = math.MaxInt32
@@ -1993,7 +2087,7 @@ func (s *stripeSeries) gc(mint int64, minOOOMmapRef chunks.ChunkDiskMapperRef, n
if value.IsStaleNaN(series.lastValue) ||
(series.lastHistogramValue != nil && value.IsStaleNaN(series.lastHistogramValue.Sum)) ||
(series.lastFloatHistogramValue != nil && value.IsStaleNaN(series.lastFloatHistogramValue.Sum)) {
- numStaleSeries.Dec()
+ staleSeriesDeleted++
}
deleted[storage.SeriesRef(series.ref)] = struct{}{}
@@ -2009,7 +2103,166 @@ func (s *stripeSeries) gc(mint int64, minOOOMmapRef chunks.ChunkDiskMapperRef, n
actualMint = mint
}
- return deleted, affected, rmChunks, actualMint, minOOOTime, minMmapFile
+ return deleted, affected, rmChunks, staleSeriesDeleted, actualMint, minOOOTime, minMmapFile
+}
+
+// gcStaleSeries removes all the provided series as long as they are still stale
+// and the series maxt is <= the given max.
+// The returned references are the series that got deleted.
+func (h *Head) gcStaleSeries(seriesRefs []storage.SeriesRef, maxt int64) map[storage.SeriesRef]struct{} {
+ // Drop old chunks and remember series IDs and hashes if they can be
+ // deleted entirely.
+ deleted, affected, chunksRemoved := h.series.gcStaleSeries(seriesRefs, maxt)
+ seriesRemoved := len(deleted)
+
+ h.metrics.seriesRemoved.Add(float64(seriesRemoved))
+ h.metrics.chunksRemoved.Add(float64(chunksRemoved))
+ h.metrics.chunks.Sub(float64(chunksRemoved))
+ h.numSeries.Sub(uint64(seriesRemoved))
+ h.numStaleSeries.Sub(uint64(seriesRemoved))
+
+ // Remove deleted series IDs from the postings lists.
+ h.postings.Delete(deleted, affected)
+
+ // Remove tombstones referring to the deleted series.
+ h.tombstones.DeleteTombstones(deleted)
+
+ if h.wal != nil {
+ _, last, _ := wlog.Segments(h.wal.Dir())
+ h.walExpiriesMtx.Lock()
+ // Keep series records until we're past segment 'last'
+ // because the WAL will still have samples records with
+ // this ref ID. If we didn't keep these series records then
+ // on start up when we replay the WAL, or any other code
+ // that reads the WAL, wouldn't be able to use those
+ // samples since we would have no labels for that ref ID.
+ for ref := range deleted {
+ h.walExpiries[chunks.HeadSeriesRef(ref)] = int64(last)
+ }
+ h.walExpiriesMtx.Unlock()
+ }
+
+ return deleted
+}
+
+// deleteSeriesByID deletes the series with the given reference.
+// Only used for WAL replay.
+func (h *Head) deleteSeriesByID(refs []chunks.HeadSeriesRef) {
+ var (
+ deleted = map[storage.SeriesRef]struct{}{}
+ affected = map[labels.Label]struct{}{}
+ staleSeriesDeleted = 0
+ chunksRemoved = 0
+ )
+
+ for _, ref := range refs {
+ refShard := int(ref) & (h.series.size - 1)
+ h.series.locks[refShard].Lock()
+
+ // Copying getByID here to avoid locking and unlocking twice.
+ series := h.series.series[refShard][ref]
+ if series == nil {
+ h.series.locks[refShard].Unlock()
+ continue
+ }
+
+ if value.IsStaleNaN(series.lastValue) ||
+ (series.lastHistogramValue != nil && value.IsStaleNaN(series.lastHistogramValue.Sum)) ||
+ (series.lastFloatHistogramValue != nil && value.IsStaleNaN(series.lastFloatHistogramValue.Sum)) {
+ staleSeriesDeleted++
+ }
+
+ hash := series.lset.Hash()
+ hashShard := int(hash) & (h.series.size - 1)
+
+ chunksRemoved += len(series.mmappedChunks)
+ if series.headChunks != nil {
+ chunksRemoved += series.headChunks.len()
+ }
+
+ deleted[storage.SeriesRef(series.ref)] = struct{}{}
+ series.lset.Range(func(l labels.Label) { affected[l] = struct{}{} })
+ h.series.hashes[hashShard].del(hash, series.ref)
+ delete(h.series.series[refShard], series.ref)
+
+ h.series.locks[refShard].Unlock()
+ }
+
+ h.metrics.seriesRemoved.Add(float64(len(deleted)))
+ h.metrics.chunksRemoved.Add(float64(chunksRemoved))
+ h.metrics.chunks.Sub(float64(chunksRemoved))
+ h.numSeries.Sub(uint64(len(deleted)))
+ h.numStaleSeries.Sub(uint64(staleSeriesDeleted))
+
+ // Remove deleted series IDs from the postings lists.
+ h.postings.Delete(deleted, affected)
+
+ // Remove tombstones referring to the deleted series.
+ h.tombstones.DeleteTombstones(deleted)
+}
+
+// gcStaleSeries removes all the stale series provided that they are still stale
+// and the series maxt is <= the given max.
+func (s *stripeSeries) gcStaleSeries(seriesRefs []storage.SeriesRef, maxt int64) (_ map[storage.SeriesRef]struct{}, _ map[labels.Label]struct{}, _ int) {
+ var (
+ deleted = map[storage.SeriesRef]struct{}{}
+ affected = map[labels.Label]struct{}{}
+ rmChunks = 0
+ )
+
+ staleSeriesMap := map[storage.SeriesRef]struct{}{}
+ for _, ref := range seriesRefs {
+ staleSeriesMap[ref] = struct{}{}
+ }
+
+ check := func(hashShard int, hash uint64, series *memSeries, deletedForCallback map[chunks.HeadSeriesRef]labels.Labels) {
+ if _, exists := staleSeriesMap[storage.SeriesRef(series.ref)]; !exists {
+ // This series was not compacted. Skip it.
+ return
+ }
+
+ series.Lock()
+ defer series.Unlock()
+
+ if series.maxTime() > maxt {
+ return
+ }
+
+ // Check if the series is still stale.
+ isStale := value.IsStaleNaN(series.lastValue) ||
+ (series.lastHistogramValue != nil && value.IsStaleNaN(series.lastHistogramValue.Sum)) ||
+ (series.lastFloatHistogramValue != nil && value.IsStaleNaN(series.lastFloatHistogramValue.Sum))
+
+ if !isStale {
+ return
+ }
+
+ if series.headChunks != nil {
+ rmChunks += series.headChunks.len()
+ }
+ rmChunks += len(series.mmappedChunks)
+
+ // The series is gone entirely. We need to keep the series lock
+ // and make sure we have acquired the stripe locks for hash and ID of the
+ // series alike.
+ // If we don't hold them all, there's a very small chance that a series receives
+ // samples again while we are half-way into deleting it.
+ refShard := int(series.ref) & (s.size - 1)
+ if hashShard != refShard {
+ s.locks[refShard].Lock()
+ defer s.locks[refShard].Unlock()
+ }
+
+ deleted[storage.SeriesRef(series.ref)] = struct{}{}
+ series.lset.Range(func(l labels.Label) { affected[l] = struct{}{} })
+ s.hashes[hashShard].del(hash, series.ref)
+ delete(s.series[refShard], series.ref)
+ deletedForCallback[series.ref] = series.lset // OK to access lset; series is locked at the top of this function.
+ }
+
+ s.iterForDeletion(check)
+
+ return deleted, affected, rmChunks
}
// The iterForDeletion function iterates through all series, invoking the checkDeletedFunc for each.
@@ -2061,43 +2314,23 @@ func (s *stripeSeries) getByHash(hash uint64, lset labels.Labels) *memSeries {
return series
}
-func (s *stripeSeries) getOrSet(hash uint64, lset labels.Labels, createSeries func() *memSeries) (*memSeries, bool, error) {
- // PreCreation is called here to avoid calling it inside the lock.
- // It is not necessary to call it just before creating a series,
- // rather it gives a 'hint' whether to create a series or not.
- preCreationErr := s.seriesLifecycleCallback.PreCreation(lset)
-
- // Create the series, unless the PreCreation() callback as failed.
- // If failed, we'll not allow to create a new series anyway.
- var series *memSeries
- if preCreationErr == nil {
- series = createSeries()
- }
-
+func (s *stripeSeries) setUnlessAlreadySet(hash uint64, lset labels.Labels, series *memSeries) (*memSeries, bool) {
i := hash & uint64(s.size-1)
s.locks[i].Lock()
-
if prev := s.hashes[i].get(hash, lset); prev != nil {
s.locks[i].Unlock()
- return prev, false, nil
- }
- if preCreationErr == nil {
- s.hashes[i].set(hash, series)
+ return prev, false
}
+ s.hashes[i].set(hash, series)
s.locks[i].Unlock()
- if preCreationErr != nil {
- // The callback prevented creation of series.
- return nil, false, preCreationErr
- }
-
i = uint64(series.ref) & uint64(s.size-1)
s.locks[i].Lock()
s.series[i][series.ref] = series
s.locks[i].Unlock()
- return series, true, nil
+ return series, true
}
func (s *stripeSeries) postCreation(lset labels.Labels) {
@@ -2105,17 +2338,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 8740d2f5ad..e6c9f2828a 100644
--- a/tsdb/head_append.go
+++ b/tsdb/head_append.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
@@ -19,6 +19,7 @@ import (
"fmt"
"log/slog"
"math"
+ "time"
"github.com/prometheus/prometheus/model/exemplar"
"github.com/prometheus/prometheus/model/histogram"
@@ -83,14 +84,14 @@ func (a *initAppender) AppendHistogram(ref storage.SeriesRef, l labels.Labels, t
return a.app.AppendHistogram(ref, l, t, h, fh)
}
-func (a *initAppender) AppendHistogramCTZeroSample(ref storage.SeriesRef, l labels.Labels, t, ct int64, h *histogram.Histogram, fh *histogram.FloatHistogram) (storage.SeriesRef, error) {
+func (a *initAppender) AppendHistogramSTZeroSample(ref storage.SeriesRef, l labels.Labels, t, st int64, h *histogram.Histogram, fh *histogram.FloatHistogram) (storage.SeriesRef, error) {
if a.app != nil {
- return a.app.AppendHistogramCTZeroSample(ref, l, t, ct, h, fh)
+ return a.app.AppendHistogramSTZeroSample(ref, l, t, st, h, fh)
}
a.head.initTime(t)
a.app = a.head.appender()
- return a.app.AppendHistogramCTZeroSample(ref, l, t, ct, h, fh)
+ return a.app.AppendHistogramSTZeroSample(ref, l, t, st, h, fh)
}
func (a *initAppender) UpdateMetadata(ref storage.SeriesRef, l labels.Labels, m metadata.Metadata) (storage.SeriesRef, error) {
@@ -102,25 +103,34 @@ func (a *initAppender) UpdateMetadata(ref storage.SeriesRef, l labels.Labels, m
return a.app.UpdateMetadata(ref, l, m)
}
-func (a *initAppender) AppendCTZeroSample(ref storage.SeriesRef, lset labels.Labels, t, ct int64) (storage.SeriesRef, error) {
+func (a *initAppender) AppendSTZeroSample(ref storage.SeriesRef, lset labels.Labels, t, st int64) (storage.SeriesRef, error) {
if a.app != nil {
- return a.app.AppendCTZeroSample(ref, lset, t, ct)
+ return a.app.AppendSTZeroSample(ref, lset, t, st)
}
a.head.initTime(t)
a.app = a.head.appender()
- return a.app.AppendCTZeroSample(ref, lset, t, ct)
+ return a.app.AppendSTZeroSample(ref, lset, t, st)
}
// initTime initializes a head with the first timestamp. This only needs to be called
// for a completely fresh head with an empty WAL.
func (h *Head) initTime(t int64) {
if !h.minTime.CompareAndSwap(math.MaxInt64, t) {
+ // Concurrent appends that are initializing.
+ // Wait until h.maxTime is swapped to avoid minTime/maxTime races.
+ antiDeadlockTimeout := time.After(500 * time.Millisecond)
+ for h.maxTime.Load() == math.MinInt64 {
+ select {
+ case <-antiDeadlockTimeout:
+ return
+ default:
+ }
+ }
return
}
// Ensure that max time is initialized to at least the min time we just set.
- // Concurrent appenders may already have set it to a higher value.
h.maxTime.CompareAndSwap(math.MinInt64, t)
}
@@ -165,17 +175,17 @@ func (h *Head) appender() *headAppender {
minValidTime := h.appendableMinValidTime()
appendID, cleanupAppendIDsBelow := h.iso.newAppendID(minValidTime) // Every appender gets an ID that is cleared upon commit/rollback.
return &headAppender{
- head: h,
- minValidTime: minValidTime,
- mint: math.MaxInt64,
- maxt: math.MinInt64,
- headMaxt: h.MaxTime(),
- oooTimeWindow: h.opts.OutOfOrderTimeWindow.Load(),
- seriesRefs: h.getRefSeriesBuffer(),
- series: h.getSeriesBuffer(),
- typesInBatch: h.getTypeMap(),
- appendID: appendID,
- cleanupAppendIDsBelow: cleanupAppendIDsBelow,
+ headAppenderBase: headAppenderBase{
+ head: h,
+ minValidTime: minValidTime,
+ headMaxt: h.MaxTime(),
+ oooTimeWindow: h.opts.OutOfOrderTimeWindow.Load(),
+ seriesRefs: h.getRefSeriesBuffer(),
+ series: h.getSeriesBuffer(),
+ typesInBatch: h.getTypeMap(),
+ appendID: appendID,
+ cleanupAppendIDsBelow: cleanupAppendIDsBelow,
+ },
}
}
@@ -212,6 +222,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])
}
@@ -255,6 +268,7 @@ func (h *Head) getHistogramBuffer() []record.RefHistogramSample {
}
func (h *Head) putHistogramBuffer(b []record.RefHistogramSample) {
+ clear(b)
h.histogramsPool.Put(b[:0])
}
@@ -267,6 +281,7 @@ func (h *Head) getFloatHistogramBuffer() []record.RefFloatHistogramSample {
}
func (h *Head) putFloatHistogramBuffer(b []record.RefFloatHistogramSample) {
+ clear(b)
h.floatHistogramsPool.Put(b[:0])
}
@@ -279,6 +294,7 @@ func (h *Head) getMetadataBuffer() []record.RefMetadata {
}
func (h *Head) putMetadataBuffer(b []record.RefMetadata) {
+ clear(b)
h.metadataPool.Put(b[:0])
}
@@ -382,10 +398,9 @@ func (b *appendBatch) close(h *Head) {
b.exemplars = nil
}
-type headAppender struct {
+type headAppenderBase struct {
head *Head
minValidTime int64 // No samples below this timestamp are allowed.
- mint, maxt int64
headMaxt int64 // We track it here to not take the lock for every sample appended.
oooTimeWindow int64 // Use the same for the entire append, and don't load the atomic for each sample.
@@ -397,7 +412,10 @@ type headAppender struct {
appendID, cleanupAppendIDsBelow uint64
closed bool
- hints *storage.AppendOptions
+}
+type headAppender struct {
+ headAppenderBase
+ hints *storage.AppendOptions
}
func (a *headAppender) SetOptions(opts *storage.AppendOptions) {
@@ -466,13 +484,6 @@ func (a *headAppender) Append(ref storage.SeriesRef, lset labels.Labels, t int64
return 0, err
}
- if t < a.mint {
- a.mint = t
- }
- if t > a.maxt {
- a.maxt = t
- }
-
b := a.getCurrentBatch(stFloat, s.ref)
b.floats = append(b.floats, record.RefSample{
Ref: s.ref,
@@ -483,12 +494,12 @@ func (a *headAppender) Append(ref storage.SeriesRef, lset labels.Labels, t int64
return storage.SeriesRef(s.ref), nil
}
-// AppendCTZeroSample appends synthetic zero sample for ct timestamp. It returns
+// AppendSTZeroSample appends synthetic zero sample for st timestamp. It returns
// error when sample can't be appended. See
-// storage.CreatedTimestampAppender.AppendCTZeroSample for further documentation.
-func (a *headAppender) AppendCTZeroSample(ref storage.SeriesRef, lset labels.Labels, t, ct int64) (storage.SeriesRef, error) {
- if ct >= t {
- return 0, storage.ErrCTNewerThanSample
+// storage.StartTimestampAppender.AppendSTZeroSample for further documentation.
+func (a *headAppender) AppendSTZeroSample(ref storage.SeriesRef, lset labels.Labels, t, st int64) (storage.SeriesRef, error) {
+ if st >= t {
+ return 0, storage.ErrSTNewerThanSample
}
s := a.head.series.getByID(chunks.HeadSeriesRef(ref))
@@ -500,11 +511,11 @@ func (a *headAppender) AppendCTZeroSample(ref storage.SeriesRef, lset labels.Lab
}
}
- // Check if CT wouldn't be OOO vs samples we already might have for this series.
+ // Check if ST wouldn't be OOO vs samples we already might have for this series.
// NOTE(bwplotka): This will be often hit as it's expected for long living
- // counters to share the same CT.
+ // counters to share the same ST.
s.Lock()
- isOOO, _, err := s.appendable(ct, 0, a.headMaxt, a.minValidTime, a.oooTimeWindow)
+ isOOO, _, err := s.appendable(st, 0, a.headMaxt, a.minValidTime, a.oooTimeWindow)
if err == nil {
s.pendingCommit = true
}
@@ -513,19 +524,16 @@ func (a *headAppender) AppendCTZeroSample(ref storage.SeriesRef, lset labels.Lab
return 0, err
}
if isOOO {
- return storage.SeriesRef(s.ref), storage.ErrOutOfOrderCT
+ return storage.SeriesRef(s.ref), storage.ErrOutOfOrderST
}
- if ct > a.maxt {
- a.maxt = ct
- }
b := a.getCurrentBatch(stFloat, s.ref)
- b.floats = append(b.floats, record.RefSample{Ref: s.ref, T: ct, V: 0.0})
+ b.floats = append(b.floats, record.RefSample{Ref: s.ref, T: st, V: 0.0})
b.floatSeries = append(b.floatSeries, s)
return storage.SeriesRef(s.ref), nil
}
-func (a *headAppender) getOrCreate(lset labels.Labels) (s *memSeries, created bool, err error) {
+func (a *headAppenderBase) getOrCreate(lset labels.Labels) (s *memSeries, created bool, err error) {
// Ensure no empty labels have gotten through.
lset = lset.WithoutEmpty()
if lset.IsEmpty() {
@@ -550,7 +558,7 @@ func (a *headAppender) getOrCreate(lset labels.Labels) (s *memSeries, created bo
// getCurrentBatch returns the current batch if it fits the provided sampleType
// for the provided series. Otherwise, it adds a new batch and returns it.
-func (a *headAppender) getCurrentBatch(st sampleType, s chunks.HeadSeriesRef) *appendBatch {
+func (a *headAppenderBase) getCurrentBatch(st sampleType, s chunks.HeadSeriesRef) *appendBatch {
h := a.head
newBatch := func() *appendBatch {
@@ -892,19 +900,12 @@ func (a *headAppender) AppendHistogram(ref storage.SeriesRef, lset labels.Labels
b.floatHistogramSeries = append(b.floatHistogramSeries, s)
}
- if t < a.mint {
- a.mint = t
- }
- if t > a.maxt {
- a.maxt = t
- }
-
return storage.SeriesRef(s.ref), nil
}
-func (a *headAppender) AppendHistogramCTZeroSample(ref storage.SeriesRef, lset labels.Labels, t, ct int64, h *histogram.Histogram, fh *histogram.FloatHistogram) (storage.SeriesRef, error) {
- if ct >= t {
- return 0, storage.ErrCTNewerThanSample
+func (a *headAppender) AppendHistogramSTZeroSample(ref storage.SeriesRef, lset labels.Labels, t, st int64, h *histogram.Histogram, fh *histogram.FloatHistogram) (storage.SeriesRef, error) {
+ if st >= t {
+ return 0, storage.ErrSTNewerThanSample
}
s := a.head.series.getByID(chunks.HeadSeriesRef(ref))
@@ -919,7 +920,7 @@ func (a *headAppender) AppendHistogramCTZeroSample(ref storage.SeriesRef, lset l
switch {
case h != nil:
zeroHistogram := &histogram.Histogram{
- // The CTZeroSample represents a counter reset by definition.
+ // The STZeroSample represents a counter reset by definition.
CounterResetHint: histogram.CounterReset,
// Replicate other fields to avoid needless chunk creation.
Schema: h.Schema,
@@ -927,41 +928,41 @@ func (a *headAppender) AppendHistogramCTZeroSample(ref storage.SeriesRef, lset l
CustomValues: h.CustomValues,
}
s.Lock()
- // For CTZeroSamples OOO is not allowed.
+ // For STZeroSamples OOO is not allowed.
// We set it to true to make this implementation as close as possible to the float implementation.
- isOOO, _, err := s.appendableHistogram(ct, zeroHistogram, a.headMaxt, a.minValidTime, a.oooTimeWindow)
+ isOOO, _, err := s.appendableHistogram(st, zeroHistogram, a.headMaxt, a.minValidTime, a.oooTimeWindow)
if err != nil {
s.Unlock()
if errors.Is(err, storage.ErrOutOfOrderSample) {
- return 0, storage.ErrOutOfOrderCT
+ return 0, storage.ErrOutOfOrderST
}
return 0, err
}
- // OOO is not allowed because after the first scrape, CT will be the same for most (if not all) future samples.
+ // OOO is not allowed because after the first scrape, ST will be the same for most (if not all) future samples.
// This is to prevent the injected zero from being marked as OOO forever.
if isOOO {
s.Unlock()
- return 0, storage.ErrOutOfOrderCT
+ return 0, storage.ErrOutOfOrderST
}
s.pendingCommit = true
s.Unlock()
- st := stHistogram
+ sTyp := stHistogram
if h.UsesCustomBuckets() {
- st = stCustomBucketHistogram
+ sTyp = stCustomBucketHistogram
}
- b := a.getCurrentBatch(st, s.ref)
+ b := a.getCurrentBatch(sTyp, s.ref)
b.histograms = append(b.histograms, record.RefHistogramSample{
Ref: s.ref,
- T: ct,
+ T: st,
H: zeroHistogram,
})
b.histogramSeries = append(b.histogramSeries, s)
case fh != nil:
zeroFloatHistogram := &histogram.FloatHistogram{
- // The CTZeroSample represents a counter reset by definition.
+ // The STZeroSample represents a counter reset by definition.
CounterResetHint: histogram.CounterReset,
// Replicate other fields to avoid needless chunk creation.
Schema: fh.Schema,
@@ -970,42 +971,38 @@ func (a *headAppender) AppendHistogramCTZeroSample(ref storage.SeriesRef, lset l
}
s.Lock()
// We set it to true to make this implementation as close as possible to the float implementation.
- isOOO, _, err := s.appendableFloatHistogram(ct, zeroFloatHistogram, a.headMaxt, a.minValidTime, a.oooTimeWindow) // OOO is not allowed for CTZeroSamples.
+ isOOO, _, err := s.appendableFloatHistogram(st, zeroFloatHistogram, a.headMaxt, a.minValidTime, a.oooTimeWindow) // OOO is not allowed for STZeroSamples.
if err != nil {
s.Unlock()
if errors.Is(err, storage.ErrOutOfOrderSample) {
- return 0, storage.ErrOutOfOrderCT
+ return 0, storage.ErrOutOfOrderST
}
return 0, err
}
- // OOO is not allowed because after the first scrape, CT will be the same for most (if not all) future samples.
+ // OOO is not allowed because after the first scrape, ST will be the same for most (if not all) future samples.
// This is to prevent the injected zero from being marked as OOO forever.
if isOOO {
s.Unlock()
- return 0, storage.ErrOutOfOrderCT
+ return 0, storage.ErrOutOfOrderST
}
s.pendingCommit = true
s.Unlock()
- st := stFloatHistogram
+ sTyp := stFloatHistogram
if fh.UsesCustomBuckets() {
- st = stCustomBucketFloatHistogram
+ sTyp = stCustomBucketFloatHistogram
}
- b := a.getCurrentBatch(st, s.ref)
+ b := a.getCurrentBatch(sTyp, s.ref)
b.floatHistograms = append(b.floatHistograms, record.RefFloatHistogramSample{
Ref: s.ref,
- T: ct,
+ T: st,
FH: zeroFloatHistogram,
})
b.floatHistogramSeries = append(b.floatHistogramSeries, s)
}
- if ct > a.maxt {
- a.maxt = ct
- }
-
return storage.SeriesRef(s.ref), nil
}
@@ -1043,7 +1040,7 @@ func (a *headAppender) UpdateMetadata(ref storage.SeriesRef, lset labels.Labels,
var _ storage.GetRef = &headAppender{}
-func (a *headAppender) GetRef(lset labels.Labels, hash uint64) (storage.SeriesRef, labels.Labels) {
+func (a *headAppenderBase) GetRef(lset labels.Labels, hash uint64) (storage.SeriesRef, labels.Labels) {
s := a.head.series.getByHash(hash, lset)
if s == nil {
return 0, labels.EmptyLabels()
@@ -1053,7 +1050,7 @@ func (a *headAppender) GetRef(lset labels.Labels, hash uint64) (storage.SeriesRe
}
// log writes all headAppender's data to the WAL.
-func (a *headAppender) log() error {
+func (a *headAppenderBase) log() error {
if a.head.wal == nil {
return nil
}
@@ -1185,7 +1182,7 @@ type appenderCommitContext struct {
}
// commitExemplars adds all exemplars from the provided batch to the head's exemplar storage.
-func (a *headAppender) commitExemplars(b *appendBatch) {
+func (a *headAppenderBase) commitExemplars(b *appendBatch) {
// No errors logging to WAL, so pass the exemplars along to the in memory storage.
for _, e := range b.exemplars {
s := a.head.series.getByID(chunks.HeadSeriesRef(e.ref))
@@ -1205,7 +1202,7 @@ func (a *headAppender) commitExemplars(b *appendBatch) {
}
}
-func (acc *appenderCommitContext) collectOOORecords(a *headAppender) {
+func (acc *appenderCommitContext) collectOOORecords(a *headAppenderBase) {
if a.head.wbl == nil {
// WBL is not enabled. So no need to collect.
acc.wblSamples = nil
@@ -1310,7 +1307,7 @@ func handleAppendableError(err error, appended, oooRejected, oobRejected, tooOld
// operations on the series after appending the samples.
//
// There are also specific functions to commit histograms and float histograms.
-func (a *headAppender) commitFloats(b *appendBatch, acc *appenderCommitContext) {
+func (a *headAppenderBase) commitFloats(b *appendBatch, acc *appenderCommitContext) {
var ok, chunkCreated bool
var series *memSeries
@@ -1466,7 +1463,7 @@ func (a *headAppender) commitFloats(b *appendBatch, acc *appenderCommitContext)
}
// For details on the commitHistograms function, see the commitFloats docs.
-func (a *headAppender) commitHistograms(b *appendBatch, acc *appenderCommitContext) {
+func (a *headAppenderBase) commitHistograms(b *appendBatch, acc *appenderCommitContext) {
var ok, chunkCreated bool
var series *memSeries
@@ -1575,7 +1572,7 @@ func (a *headAppender) commitHistograms(b *appendBatch, acc *appenderCommitConte
}
// For details on the commitFloatHistograms function, see the commitFloats docs.
-func (a *headAppender) commitFloatHistograms(b *appendBatch, acc *appenderCommitContext) {
+func (a *headAppenderBase) commitFloatHistograms(b *appendBatch, acc *appenderCommitContext) {
var ok, chunkCreated bool
var series *memSeries
@@ -1697,7 +1694,7 @@ func commitMetadata(b *appendBatch) {
}
}
-func (a *headAppender) unmarkCreatedSeriesAsPendingCommit() {
+func (a *headAppenderBase) unmarkCreatedSeriesAsPendingCommit() {
for _, s := range a.series {
s.Lock()
s.pendingCommit = false
@@ -1707,7 +1704,7 @@ func (a *headAppender) unmarkCreatedSeriesAsPendingCommit() {
// Commit writes to the WAL and adds the data to the Head.
// TODO(codesome): Refactor this method to reduce indentation and make it more readable.
-func (a *headAppender) Commit() (err error) {
+func (a *headAppenderBase) Commit() (err error) {
if a.closed {
return ErrAppenderClosed
}
@@ -1838,7 +1835,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
@@ -1880,7 +1878,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
@@ -1937,7 +1936,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
@@ -2231,6 +2231,9 @@ func (s *memSeries) mmapChunks(chunkDiskMapper *chunks.ChunkDiskMapper) (count i
return count
}
+// TODO(bwplotka): Propagate errors correctly, even when they are async. Panicking here do occurs from time to time
+// and cause flaky tests with hidden root cause (unlocked mutexes when deferred closing).
+// We didn't have evidences of prod impact though, yet.
func handleChunkWriteError(err error) {
if err != nil && !errors.Is(err, chunks.ErrChunkDiskMapperClosed) {
panic(err)
@@ -2238,7 +2241,7 @@ func handleChunkWriteError(err error) {
}
// Rollback removes the samples and exemplars from headAppender and writes any series to WAL.
-func (a *headAppender) Rollback() (err error) {
+func (a *headAppenderBase) Rollback() (err error) {
if a.closed {
return ErrAppenderClosed
}
diff --git a/tsdb/head_append_v2.go b/tsdb/head_append_v2.go
new file mode 100644
index 0000000000..87b62df536
--- /dev/null
+++ b/tsdb/head_append_v2.go
@@ -0,0 +1,382 @@
+// 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 tsdb
+
+import (
+ "context"
+ "errors"
+ "fmt"
+
+ "github.com/prometheus/prometheus/model/exemplar"
+ "github.com/prometheus/prometheus/model/histogram"
+ "github.com/prometheus/prometheus/model/labels"
+ "github.com/prometheus/prometheus/model/value"
+ "github.com/prometheus/prometheus/storage"
+ "github.com/prometheus/prometheus/tsdb/chunks"
+ "github.com/prometheus/prometheus/tsdb/record"
+)
+
+// initAppenderV2 is a helper to initialize the time bounds of the head
+// upon the first sample it receives.
+type initAppenderV2 struct {
+ app storage.AppenderV2
+ head *Head
+}
+
+var _ storage.GetRef = &initAppenderV2{}
+
+func (a *initAppenderV2) 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 a.app == nil {
+ a.head.initTime(t)
+ a.app = a.head.appenderV2()
+ }
+ return a.app.Append(ref, ls, st, t, v, h, fh, opts)
+}
+
+func (a *initAppenderV2) GetRef(lset labels.Labels, hash uint64) (storage.SeriesRef, labels.Labels) {
+ if g, ok := a.app.(storage.GetRef); ok {
+ return g.GetRef(lset, hash)
+ }
+ return 0, labels.EmptyLabels()
+}
+
+func (a *initAppenderV2) Commit() error {
+ if a.app == nil {
+ a.head.metrics.activeAppenders.Dec()
+ return nil
+ }
+ return a.app.Commit()
+}
+
+func (a *initAppenderV2) Rollback() error {
+ if a.app == nil {
+ a.head.metrics.activeAppenders.Dec()
+ return nil
+ }
+ return a.app.Rollback()
+}
+
+// AppenderV2 returns a new AppenderV2 on the database.
+func (h *Head) AppenderV2(context.Context) storage.AppenderV2 {
+ h.metrics.activeAppenders.Inc()
+
+ // The head cache might not have a starting point yet. The init appender
+ // picks up the first appended timestamp as the base.
+ if !h.initialized() {
+ return &initAppenderV2{
+ head: h,
+ }
+ }
+ return h.appenderV2()
+}
+
+func (h *Head) appenderV2() *headAppenderV2 {
+ minValidTime := h.appendableMinValidTime()
+ appendID, cleanupAppendIDsBelow := h.iso.newAppendID(minValidTime) // Every appender gets an ID that is cleared upon commit/rollback.
+ return &headAppenderV2{
+ headAppenderBase: headAppenderBase{
+ head: h,
+ minValidTime: minValidTime,
+ headMaxt: h.MaxTime(),
+ oooTimeWindow: h.opts.OutOfOrderTimeWindow.Load(),
+ seriesRefs: h.getRefSeriesBuffer(),
+ series: h.getSeriesBuffer(),
+ typesInBatch: h.getTypeMap(),
+ appendID: appendID,
+ cleanupAppendIDsBelow: cleanupAppendIDsBelow,
+ },
+ }
+}
+
+type headAppenderV2 struct {
+ headAppenderBase
+}
+
+func (a *headAppenderV2) Append(ref storage.SeriesRef, ls labels.Labels, st, t int64, v float64, h *histogram.Histogram, fh *histogram.FloatHistogram, opts storage.AOptions) (storage.SeriesRef, error) {
+ var (
+ // Avoid shadowing err variables for reliability.
+ valErr, appErr, partialErr error
+ sampleMetricType = sampleMetricTypeFloat
+ isStale bool
+ )
+ // Fail fast on incorrect histograms.
+
+ switch {
+ case fh != nil:
+ sampleMetricType = sampleMetricTypeHistogram
+ valErr = fh.Validate()
+ case h != nil:
+ sampleMetricType = sampleMetricTypeHistogram
+ valErr = h.Validate()
+ }
+ if valErr != nil {
+ return 0, valErr
+ }
+
+ // Fail fast if OOO is disabled and the sample is out of bounds.
+ // Otherwise, a full check will be done later to decide if the sample is in-order or out-of-order.
+ if a.oooTimeWindow == 0 && t < a.minValidTime {
+ a.head.metrics.outOfBoundSamples.WithLabelValues(sampleMetricType).Inc()
+ return 0, storage.ErrOutOfBounds
+ }
+
+ s := a.head.series.getByID(chunks.HeadSeriesRef(ref))
+ if s == nil {
+ var err error
+ s, _, err = a.getOrCreate(ls)
+ if err != nil {
+ return 0, err
+ }
+ }
+
+ // TODO(bwplotka): Handle ST natively (as per PROM-60).
+ if a.head.opts.EnableSTAsZeroSample && st != 0 {
+ a.bestEffortAppendSTZeroSample(s, ls, st, t, h, fh)
+ }
+
+ switch {
+ case fh != nil:
+ isStale = value.IsStaleNaN(fh.Sum)
+ appErr = a.appendFloatHistogram(s, t, fh, opts.RejectOutOfOrder)
+ case h != nil:
+ isStale = value.IsStaleNaN(h.Sum)
+ appErr = a.appendHistogram(s, t, h, opts.RejectOutOfOrder)
+ default:
+ isStale = value.IsStaleNaN(v)
+ if isStale {
+ // If we have added a sample before with this same appender, we
+ // can check the previously used type and turn a stale float
+ // sample into a stale histogram sample or stale float histogram
+ // sample as appropriate. This prevents an unnecessary creation
+ // of a new batch. However, since other appenders might append
+ // to the same series concurrently, this is not perfect but just
+ // an optimization for the more likely case.
+ switch a.typesInBatch[s.ref] {
+ case stHistogram, stCustomBucketHistogram:
+ return a.Append(storage.SeriesRef(s.ref), ls, st, t, 0, &histogram.Histogram{Sum: v}, nil, storage.AOptions{
+ RejectOutOfOrder: opts.RejectOutOfOrder,
+ })
+ case stFloatHistogram, stCustomBucketFloatHistogram:
+ return a.Append(storage.SeriesRef(s.ref), ls, st, t, 0, nil, &histogram.FloatHistogram{Sum: v}, storage.AOptions{
+ RejectOutOfOrder: opts.RejectOutOfOrder,
+ })
+ }
+ // Note that a series reference not yet in the map will come out
+ // as stNone, but since we do not handle that case separately,
+ // we do not need to check for the difference between "unknown
+ // series" and "known series with stNone".
+ }
+ appErr = a.appendFloat(s, t, v, opts.RejectOutOfOrder)
+ }
+ // Handle append error, if any.
+ if appErr != nil {
+ switch {
+ case errors.Is(appErr, storage.ErrOutOfOrderSample):
+ a.head.metrics.outOfOrderSamples.WithLabelValues(sampleMetricType).Inc()
+ case errors.Is(appErr, storage.ErrTooOldSample):
+ a.head.metrics.tooOldSamples.WithLabelValues(sampleMetricType).Inc()
+ }
+ return 0, appErr
+ }
+
+ if isStale {
+ // For stale values we never attempt to process metadata/exemplars, claim the success.
+ return storage.SeriesRef(s.ref), nil
+ }
+
+ // Append exemplars if any and if storage was configured for it.
+ if len(opts.Exemplars) > 0 && a.head.opts.EnableExemplarStorage && a.head.opts.MaxExemplars.Load() > 0 {
+ // Currently only exemplars can return partial errors.
+ partialErr = a.appendExemplars(s, opts.Exemplars)
+ }
+ if a.head.opts.EnableMetadataWALRecords && !opts.Metadata.IsEmpty() {
+ s.Lock()
+ metaChanged := s.meta == nil || !s.meta.Equals(opts.Metadata)
+ s.Unlock()
+ if metaChanged {
+ b := a.getCurrentBatch(stNone, s.ref)
+ b.metadata = append(b.metadata, record.RefMetadata{
+ Ref: s.ref,
+ Type: record.GetMetricType(opts.Metadata.Type),
+ Unit: opts.Metadata.Unit,
+ Help: opts.Metadata.Help,
+ })
+ b.metadataSeries = append(b.metadataSeries, s)
+ }
+ }
+ return storage.SeriesRef(s.ref), partialErr
+}
+
+func (a *headAppenderV2) appendFloat(s *memSeries, t int64, v float64, fastRejectOOO bool) error {
+ s.Lock()
+ // TODO(codesome): If we definitely know at this point that the sample is ooo, then optimise
+ // to skip that sample from the WAL and write only in the WBL.
+ isOOO, delta, err := s.appendable(t, v, a.headMaxt, a.minValidTime, a.oooTimeWindow)
+ if isOOO && fastRejectOOO {
+ s.Unlock()
+ return storage.ErrOutOfOrderSample
+ }
+ if err == nil {
+ s.pendingCommit = true
+ }
+ s.Unlock()
+ if delta > 0 {
+ a.head.metrics.oooHistogram.Observe(float64(delta) / 1000)
+ }
+ if err != nil {
+ return err
+ }
+
+ b := a.getCurrentBatch(stFloat, s.ref)
+ b.floats = append(b.floats, record.RefSample{Ref: s.ref, T: t, V: v})
+ b.floatSeries = append(b.floatSeries, s)
+ return nil
+}
+
+func (a *headAppenderV2) appendHistogram(s *memSeries, t int64, h *histogram.Histogram, fastRejectOOO bool) error {
+ s.Lock()
+ // TODO(codesome): If we definitely know at this point that the sample is ooo, then optimise
+ // to skip that sample from the WAL and write only in the WBL.
+ isOOO, delta, err := s.appendableHistogram(t, h, a.headMaxt, a.minValidTime, a.oooTimeWindow)
+ if isOOO && fastRejectOOO {
+ s.Unlock()
+ return storage.ErrOutOfOrderSample
+ }
+ if err == nil {
+ s.pendingCommit = true
+ }
+ s.Unlock()
+ if delta > 0 {
+ a.head.metrics.oooHistogram.Observe(float64(delta) / 1000)
+ }
+ if err != nil {
+ return err
+ }
+ st := stHistogram
+ if h.UsesCustomBuckets() {
+ st = stCustomBucketHistogram
+ }
+ b := a.getCurrentBatch(st, s.ref)
+ b.histograms = append(b.histograms, record.RefHistogramSample{Ref: s.ref, T: t, H: h})
+ b.histogramSeries = append(b.histogramSeries, s)
+ return nil
+}
+
+func (a *headAppenderV2) appendFloatHistogram(s *memSeries, t int64, fh *histogram.FloatHistogram, fastRejectOOO bool) error {
+ s.Lock()
+ // TODO(codesome): If we definitely know at this point that the sample is ooo, then optimise
+ // to skip that sample from the WAL and write only in the WBL.
+ isOOO, delta, err := s.appendableFloatHistogram(t, fh, a.headMaxt, a.minValidTime, a.oooTimeWindow)
+ if isOOO && fastRejectOOO {
+ s.Unlock()
+ return storage.ErrOutOfOrderSample
+ }
+ if err == nil {
+ s.pendingCommit = true
+ }
+ s.Unlock()
+ if delta > 0 {
+ a.head.metrics.oooHistogram.Observe(float64(delta) / 1000)
+ }
+ if err != nil {
+ return err
+ }
+ st := stFloatHistogram
+ if fh.UsesCustomBuckets() {
+ st = stCustomBucketFloatHistogram
+ }
+ b := a.getCurrentBatch(st, s.ref)
+ b.floatHistograms = append(b.floatHistograms, record.RefFloatHistogramSample{Ref: s.ref, T: t, FH: fh})
+ b.floatHistogramSeries = append(b.floatHistogramSeries, s)
+ return nil
+}
+
+func (a *headAppenderV2) appendExemplars(s *memSeries, exemplar []exemplar.Exemplar) error {
+ var errs []error
+ for _, e := range exemplar {
+ // Ensure no empty labels have gotten through.
+ e.Labels = e.Labels.WithoutEmpty()
+ 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
+ }
+ if !errors.Is(err, storage.ErrOutOfOrderExemplar) {
+ a.head.logger.Debug("Error while adding an exemplar on AppendSample", "exemplars", fmt.Sprintf("%+v", e), "err", err)
+ }
+ continue
+ }
+ b := a.getCurrentBatch(stNone, s.ref)
+ b.exemplars = append(b.exemplars, exemplarWithSeriesRef{storage.SeriesRef(s.ref), e})
+ }
+ if len(errs) > 0 {
+ return &storage.AppendPartialError{ExemplarErrors: errs}
+ }
+ return nil
+}
+
+// NOTE(bwplotka): This feature might be deprecated and removed once PROM-60
+// is implemented.
+//
+// ST is an experimental feature, we don't fail the append on errors, just debug log.
+func (a *headAppenderV2) bestEffortAppendSTZeroSample(s *memSeries, ls labels.Labels, st, t int64, h *histogram.Histogram, fh *histogram.FloatHistogram) {
+ // NOTE: Use lset instead of s.lset to avoid locking memSeries. Using s.ref is acceptable without locking.
+ if st >= t {
+ a.head.logger.Debug("Error when appending ST", "series", ls.String(), "st", st, "t", t, "err", storage.ErrSTNewerThanSample)
+ return
+ }
+ if st < a.minValidTime {
+ a.head.logger.Debug("Error when appending ST", "series", ls.String(), "st", st, "t", t, "err", storage.ErrOutOfBounds)
+ return
+ }
+
+ var err error
+ switch {
+ case fh != nil:
+ zeroFloatHistogram := &histogram.FloatHistogram{
+ // The STZeroSample represents a counter reset by definition.
+ CounterResetHint: histogram.CounterReset,
+ // Replicate other fields to avoid needless chunk creation.
+ Schema: fh.Schema,
+ ZeroThreshold: fh.ZeroThreshold,
+ CustomValues: fh.CustomValues,
+ }
+ err = a.appendFloatHistogram(s, st, zeroFloatHistogram, true)
+ case h != nil:
+ zeroHistogram := &histogram.Histogram{
+ // The STZeroSample represents a counter reset by definition.
+ CounterResetHint: histogram.CounterReset,
+ // Replicate other fields to avoid needless chunk creation.
+ Schema: h.Schema,
+ ZeroThreshold: h.ZeroThreshold,
+ CustomValues: h.CustomValues,
+ }
+ err = a.appendHistogram(s, st, zeroHistogram, true)
+ default:
+ err = a.appendFloat(s, st, 0, true)
+ }
+
+ if err != nil {
+ if errors.Is(err, storage.ErrOutOfOrderSample) {
+ // OOO errors are common and expected (cumulative). Explicitly ignored.
+ return
+ }
+ a.head.logger.Debug("Error when appending ST", "series", s.lset.String(), "st", st, "t", t, "err", err)
+ return
+ }
+}
+
+var _ storage.GetRef = &headAppenderV2{}
diff --git a/tsdb/head_append_v2_test.go b/tsdb/head_append_v2_test.go
new file mode 100644
index 0000000000..61b2eecf4e
--- /dev/null
+++ b/tsdb/head_append_v2_test.go
@@ -0,0 +1,4750 @@
+// 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 tsdb
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "math"
+ "math/rand"
+ "os"
+ "path"
+ "path/filepath"
+ "reflect"
+ "slices"
+ "sort"
+ "strconv"
+ "strings"
+ "sync"
+ "testing"
+ "time"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/prometheus/client_golang/prometheus"
+ prom_testutil "github.com/prometheus/client_golang/prometheus/testutil"
+ dto "github.com/prometheus/client_model/go"
+ "github.com/stretchr/testify/require"
+ "go.uber.org/atomic"
+
+ "github.com/prometheus/prometheus/config"
+ "github.com/prometheus/prometheus/model/exemplar"
+ "github.com/prometheus/prometheus/model/histogram"
+ "github.com/prometheus/prometheus/model/labels"
+ "github.com/prometheus/prometheus/model/value"
+ "github.com/prometheus/prometheus/storage"
+ "github.com/prometheus/prometheus/tsdb/chunkenc"
+ "github.com/prometheus/prometheus/tsdb/chunks"
+ "github.com/prometheus/prometheus/tsdb/record"
+ "github.com/prometheus/prometheus/tsdb/tombstones"
+ "github.com/prometheus/prometheus/tsdb/tsdbutil"
+ "github.com/prometheus/prometheus/tsdb/wlog"
+ "github.com/prometheus/prometheus/util/compression"
+ "github.com/prometheus/prometheus/util/testutil"
+ "github.com/prometheus/prometheus/util/testutil/synctest"
+)
+
+// TODO(bwplotka): Ensure non-ported tests are not deleted from head_test.go when removing AppenderV1 flow (#17632),
+// for example:
+// * TestChunkNotFoundHeadGCRace
+// * TestHeadSeriesChunkRace
+// * TestHeadLabelValuesWithMatchers
+// * TestHeadLabelNamesWithMatchers
+// * TestHeadShardedPostings
+// * TestHead_HighConcurrencyReadAndWrite
+
+func TestHeadAppenderV2_WALMultiRef(t *testing.T) {
+ head, w := newTestHead(t, 1000, compression.None, false)
+
+ require.NoError(t, head.Init(0))
+
+ app := head.AppenderV2(context.Background())
+ ref1, err := app.Append(0, labels.FromStrings("foo", "bar"), 0, 100, 1, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ require.Equal(t, 1.0, prom_testutil.ToFloat64(head.metrics.chunksCreated))
+
+ // Add another sample outside chunk range to mmap a chunk.
+ app = head.AppenderV2(context.Background())
+ _, err = app.Append(0, labels.FromStrings("foo", "bar"), 0, 1500, 2, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ require.Equal(t, 2.0, prom_testutil.ToFloat64(head.metrics.chunksCreated))
+
+ require.NoError(t, head.Truncate(1600))
+
+ app = head.AppenderV2(context.Background())
+ ref2, err := app.Append(0, labels.FromStrings("foo", "bar"), 0, 1700, 3, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ require.Equal(t, 3.0, prom_testutil.ToFloat64(head.metrics.chunksCreated))
+
+ // Add another sample outside chunk range to mmap a chunk.
+ app = head.AppenderV2(context.Background())
+ _, err = app.Append(0, labels.FromStrings("foo", "bar"), 0, 2000, 4, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ require.Equal(t, 4.0, prom_testutil.ToFloat64(head.metrics.chunksCreated))
+
+ require.NotEqual(t, ref1, ref2, "Refs are the same")
+ require.NoError(t, head.Close())
+
+ w, err = wlog.New(nil, nil, w.Dir(), compression.None)
+ require.NoError(t, err)
+
+ opts := DefaultHeadOptions()
+ opts.ChunkRange = 1000
+ opts.ChunkDirRoot = head.opts.ChunkDirRoot
+ head, err = NewHead(nil, nil, w, nil, opts, nil)
+ require.NoError(t, err)
+ require.NoError(t, head.Init(0))
+ defer func() {
+ require.NoError(t, head.Close())
+ }()
+
+ q, err := NewBlockQuerier(head, 0, 2100)
+ require.NoError(t, err)
+ series := query(t, q, labels.MustNewMatcher(labels.MatchEqual, "foo", "bar"))
+ // 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{0, 1700, 3, nil, nil},
+ sample{0, 2000, 4, nil, nil},
+ }}, series)
+}
+
+func TestHeadAppenderV2_ActiveAppenders(t *testing.T) {
+ head, _ := newTestHead(t, 1000, compression.None, false)
+ defer head.Close()
+
+ require.NoError(t, head.Init(0))
+
+ // First rollback with no samples.
+ app := head.AppenderV2(context.Background())
+ require.Equal(t, 1.0, prom_testutil.ToFloat64(head.metrics.activeAppenders))
+ require.NoError(t, app.Rollback())
+ require.Equal(t, 0.0, prom_testutil.ToFloat64(head.metrics.activeAppenders))
+
+ // Then commit with no samples.
+ app = head.AppenderV2(context.Background())
+ require.NoError(t, app.Commit())
+ require.Equal(t, 0.0, prom_testutil.ToFloat64(head.metrics.activeAppenders))
+
+ // Now rollback with one sample.
+ app = head.AppenderV2(context.Background())
+ _, err := app.Append(0, labels.FromStrings("foo", "bar"), 0, 100, 1, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.Equal(t, 1.0, prom_testutil.ToFloat64(head.metrics.activeAppenders))
+ require.NoError(t, app.Rollback())
+ require.Equal(t, 0.0, prom_testutil.ToFloat64(head.metrics.activeAppenders))
+
+ // Now commit with one sample.
+ app = head.AppenderV2(context.Background())
+ _, err = app.Append(0, labels.FromStrings("foo", "bar"), 0, 100, 1, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ require.Equal(t, 0.0, prom_testutil.ToFloat64(head.metrics.activeAppenders))
+}
+
+func TestHeadAppenderV2_RaceBetweenSeriesCreationAndGC(t *testing.T) {
+ head, _ := newTestHead(t, 1000, compression.None, false)
+ require.NoError(t, head.Init(0))
+
+ const totalSeries = 100_000
+ series := make([]labels.Labels, totalSeries)
+ for i := range totalSeries {
+ series[i] = labels.FromStrings("foo", strconv.Itoa(i))
+ }
+ done := atomic.NewBool(false)
+
+ go func() {
+ defer done.Store(true)
+ app := head.AppenderV2(context.Background())
+ defer func() {
+ if err := app.Commit(); err != nil {
+ t.Errorf("Failed to commit: %v", err)
+ }
+ }()
+ for i := range totalSeries {
+ _, err := app.Append(0, series[i], 0, 100, 1, nil, nil, storage.AOptions{})
+ if err != nil {
+ t.Errorf("Failed to append: %v", err)
+ return
+ }
+ }
+ }()
+
+ // Don't check the atomic.Bool on all iterations in order to perform more gc iterations and make the race condition more likely.
+ for i := 1; i%128 != 0 || !done.Load(); i++ {
+ head.gc()
+ }
+
+ require.Equal(t, totalSeries, int(head.NumSeries()))
+}
+
+func TestHeadAppenderV2_CanGCSeriesCreatedWithoutSamples(t *testing.T) {
+ for op, finishTxn := range map[string]func(app storage.AppenderTransaction) error{
+ "after commit": func(app storage.AppenderTransaction) error { return app.Commit() },
+ "after rollback": func(app storage.AppenderTransaction) error { return app.Rollback() },
+ } {
+ t.Run(op, func(t *testing.T) {
+ chunkRange := time.Hour.Milliseconds()
+ head, _ := newTestHead(t, chunkRange, compression.None, true)
+
+ require.NoError(t, head.Init(0))
+
+ firstSampleTime := 10 * chunkRange
+ {
+ // Append first sample, it should init head max time to firstSampleTime.
+ app := head.AppenderV2(context.Background())
+ _, err := app.Append(0, labels.FromStrings("lbl", "ok"), 0, firstSampleTime, 1, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ require.Equal(t, 1, int(head.NumSeries()))
+ }
+
+ // Append a sample in a time range that is not covered by the chunk range,
+ // We would create series first and then append no sample.
+ app := head.AppenderV2(context.Background())
+ invalidSampleTime := firstSampleTime - chunkRange
+ _, err := app.Append(0, labels.FromStrings("foo", "bar"), 0, invalidSampleTime, 2, nil, nil, storage.AOptions{})
+ require.Error(t, err)
+ // These are our assumptions: we're not testing them, we're just checking them to make debugging a failed
+ // test easier if someone refactors the code and breaks these assumptions.
+ // If these assumptions fail after a refactor, feel free to remove them but make sure that the test is still what we intended to test.
+ require.NotErrorIs(t, err, storage.ErrOutOfBounds, "Failed to append sample shouldn't take the shortcut that returns storage.ErrOutOfBounds")
+ require.ErrorIs(t, err, storage.ErrTooOldSample, "Failed to append sample should return storage.ErrTooOldSample, because OOO window was enabled but this sample doesn't fall into it.")
+ // Do commit or rollback, depending on what we're testing.
+ require.NoError(t, finishTxn(app))
+
+ // Garbage-collect, since we finished the transaction and series has no samples, it should be collectable.
+ head.gc()
+ require.Equal(t, 1, int(head.NumSeries()))
+ })
+ }
+}
+
+func TestHeadAppenderV2_DeleteSimple(t *testing.T) {
+ buildSmpls := func(s []int64) []sample {
+ ss := make([]sample, 0, len(s))
+ for _, t := range s {
+ ss = append(ss, sample{t: t, f: float64(t)})
+ }
+ return ss
+ }
+ smplsAll := buildSmpls([]int64{0, 1, 2, 3, 4, 5, 6, 7, 8, 9})
+ lblDefault := labels.Label{Name: "a", Value: "b"}
+ lblsDefault := labels.FromStrings("a", "b")
+
+ cases := []struct {
+ dranges tombstones.Intervals
+ addSamples []sample // Samples to add after delete.
+ smplsExp []sample
+ }{
+ {
+ dranges: tombstones.Intervals{{Mint: 0, Maxt: 3}},
+ smplsExp: buildSmpls([]int64{4, 5, 6, 7, 8, 9}),
+ },
+ {
+ dranges: tombstones.Intervals{{Mint: 1, Maxt: 3}},
+ smplsExp: buildSmpls([]int64{0, 4, 5, 6, 7, 8, 9}),
+ },
+ {
+ dranges: tombstones.Intervals{{Mint: 1, Maxt: 3}, {Mint: 4, Maxt: 7}},
+ smplsExp: buildSmpls([]int64{0, 8, 9}),
+ },
+ {
+ dranges: tombstones.Intervals{{Mint: 1, Maxt: 3}, {Mint: 4, Maxt: 700}},
+ smplsExp: buildSmpls([]int64{0}),
+ },
+ { // This case is to ensure that labels and symbols are deleted.
+ dranges: tombstones.Intervals{{Mint: 0, Maxt: 9}},
+ smplsExp: buildSmpls([]int64{}),
+ },
+ {
+ dranges: tombstones.Intervals{{Mint: 1, Maxt: 3}},
+ addSamples: buildSmpls([]int64{11, 13, 15}),
+ smplsExp: buildSmpls([]int64{0, 4, 5, 6, 7, 8, 9, 11, 13, 15}),
+ },
+ {
+ // After delete, the appended samples in the deleted range should be visible
+ // as the tombstones are clamped to head min/max time.
+ dranges: tombstones.Intervals{{Mint: 7, Maxt: 20}},
+ addSamples: buildSmpls([]int64{11, 13, 15}),
+ smplsExp: buildSmpls([]int64{0, 1, 2, 3, 4, 5, 6, 11, 13, 15}),
+ },
+ }
+
+ for _, compress := range []compression.Type{compression.None, compression.Snappy, compression.Zstd} {
+ t.Run(fmt.Sprintf("compress=%s", compress), func(t *testing.T) {
+ for _, c := range cases {
+ head, w := newTestHead(t, 1000, compress, false)
+ require.NoError(t, head.Init(0))
+
+ app := head.AppenderV2(context.Background())
+ for _, smpl := range smplsAll {
+ _, err := app.Append(0, lblsDefault, 0, smpl.t, smpl.f, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ }
+ require.NoError(t, app.Commit())
+
+ // Delete the ranges.
+ for _, r := range c.dranges {
+ require.NoError(t, head.Delete(context.Background(), r.Mint, r.Maxt, labels.MustNewMatcher(labels.MatchEqual, lblDefault.Name, lblDefault.Value)))
+ }
+
+ // Add more samples.
+ app = head.AppenderV2(context.Background())
+ for _, smpl := range c.addSamples {
+ _, err := app.Append(0, lblsDefault, 0, smpl.t, smpl.f, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ }
+ require.NoError(t, app.Commit())
+
+ // Compare the samples for both heads - before and after the reloadBlocks.
+ reloadedW, err := wlog.New(nil, nil, w.Dir(), compress) // Use a new wal to ensure deleted samples are gone even after a reloadBlocks.
+ require.NoError(t, err)
+ opts := DefaultHeadOptions()
+ opts.ChunkRange = 1000
+ opts.ChunkDirRoot = reloadedW.Dir()
+ reloadedHead, err := NewHead(nil, nil, reloadedW, nil, opts, nil)
+ require.NoError(t, err)
+ require.NoError(t, reloadedHead.Init(0))
+
+ // Compare the query results for both heads - before and after the reloadBlocks.
+ Outer:
+ for _, h := range []*Head{head, reloadedHead} {
+ q, err := NewBlockQuerier(h, h.MinTime(), h.MaxTime())
+ require.NoError(t, err)
+ actSeriesSet := q.Select(context.Background(), false, nil, labels.MustNewMatcher(labels.MatchEqual, lblDefault.Name, lblDefault.Value))
+ require.NoError(t, q.Close())
+ expSeriesSet := newMockSeriesSet([]storage.Series{
+ storage.NewListSeries(lblsDefault, func() []chunks.Sample {
+ ss := make([]chunks.Sample, 0, len(c.smplsExp))
+ for _, s := range c.smplsExp {
+ ss = append(ss, s)
+ }
+ return ss
+ }(),
+ ),
+ })
+
+ for {
+ eok, rok := expSeriesSet.Next(), actSeriesSet.Next()
+ require.Equal(t, eok, rok)
+
+ if !eok {
+ require.NoError(t, h.Close())
+ require.NoError(t, actSeriesSet.Err())
+ require.Empty(t, actSeriesSet.Warnings())
+ continue Outer
+ }
+ expSeries := expSeriesSet.At()
+ actSeries := actSeriesSet.At()
+
+ require.Equal(t, expSeries.Labels(), actSeries.Labels())
+
+ smplExp, errExp := storage.ExpandSamples(expSeries.Iterator(nil), nil)
+ smplRes, errRes := storage.ExpandSamples(actSeries.Iterator(nil), nil)
+
+ require.Equal(t, errExp, errRes)
+ require.Equal(t, smplExp, smplRes)
+ }
+ }
+ }
+ })
+ }
+}
+
+func TestHeadAppenderV2_DeleteUntilCurrMax(t *testing.T) {
+ hb, _ := newTestHead(t, 1000000, compression.None, false)
+ defer func() {
+ require.NoError(t, hb.Close())
+ }()
+
+ numSamples := int64(10)
+ app := hb.AppenderV2(context.Background())
+ smpls := make([]float64, numSamples)
+ for i := range numSamples {
+ smpls[i] = rand.Float64()
+ _, err := app.Append(0, labels.FromStrings("a", "b"), 0, i, smpls[i], nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ }
+ require.NoError(t, app.Commit())
+ require.NoError(t, hb.Delete(context.Background(), 0, 10000, labels.MustNewMatcher(labels.MatchEqual, "a", "b")))
+
+ // Test the series returns no samples. The series is cleared only after compaction.
+ q, err := NewBlockQuerier(hb, 0, 100000)
+ require.NoError(t, err)
+ res := q.Select(context.Background(), false, nil, labels.MustNewMatcher(labels.MatchEqual, "a", "b"))
+ require.True(t, res.Next(), "series is not present")
+ s := res.At()
+ it := s.Iterator(nil)
+ require.Equal(t, chunkenc.ValNone, it.Next(), "expected no samples")
+ for res.Next() {
+ }
+ require.NoError(t, res.Err())
+ require.Empty(t, res.Warnings())
+
+ // Add again and test for presence.
+ app = hb.AppenderV2(context.Background())
+ _, err = app.Append(0, labels.FromStrings("a", "b"), 0, 11, 1, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ q, err = NewBlockQuerier(hb, 0, 100000)
+ require.NoError(t, err)
+ res = q.Select(context.Background(), false, nil, labels.MustNewMatcher(labels.MatchEqual, "a", "b"))
+ require.True(t, res.Next(), "series don't exist")
+ exps := res.At()
+ it = exps.Iterator(nil)
+ resSamples, err := storage.ExpandSamples(it, newSample)
+ require.NoError(t, err)
+ require.Equal(t, []chunks.Sample{sample{0, 11, 1, nil, nil}}, resSamples)
+ for res.Next() {
+ }
+ require.NoError(t, res.Err())
+ require.Empty(t, res.Warnings())
+}
+
+func TestHeadAppenderV2_DeleteSamplesAndSeriesStillInWALAfterCheckpoint(t *testing.T) {
+ numSamples := 10000
+
+ // Enough samples to cause a checkpoint.
+ hb, w := newTestHead(t, int64(numSamples)*10, compression.None, false)
+
+ for i := range numSamples {
+ app := hb.AppenderV2(context.Background())
+ _, err := app.Append(0, labels.FromStrings("a", "b"), 0, int64(i), 0, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ }
+ require.NoError(t, hb.Delete(context.Background(), 0, int64(numSamples), labels.MustNewMatcher(labels.MatchEqual, "a", "b")))
+ require.NoError(t, hb.Truncate(1))
+ require.NoError(t, hb.Close())
+
+ // Confirm there's been a checkpoint.
+ cdir, _, err := wlog.LastCheckpoint(w.Dir())
+ require.NoError(t, err)
+ // Read in checkpoint and WAL.
+ recs := readTestWAL(t, cdir)
+ recs = append(recs, readTestWAL(t, w.Dir())...)
+
+ var series, samples, stones, metadata int
+ for _, rec := range recs {
+ switch rec.(type) {
+ case []record.RefSeries:
+ series++
+ case []record.RefSample:
+ samples++
+ case []tombstones.Stone:
+ stones++
+ case []record.RefMetadata:
+ metadata++
+ default:
+ require.Fail(t, "unknown record type")
+ }
+ }
+ require.Equal(t, 1, series)
+ require.Equal(t, 9999, samples)
+ require.Equal(t, 1, stones)
+ require.Equal(t, 0, metadata)
+}
+
+func TestHeadAppenderV2_Delete_e2e(t *testing.T) {
+ numDatapoints := 1000
+ numRanges := 1000
+ timeInterval := int64(2)
+ // Create 8 series with 1000 data-points of different ranges, delete and run queries.
+ lbls := [][]labels.Label{
+ {
+ {Name: "a", Value: "b"},
+ {Name: "instance", Value: "localhost:9090"},
+ {Name: "job", Value: "prometheus"},
+ },
+ {
+ {Name: "a", Value: "b"},
+ {Name: "instance", Value: "127.0.0.1:9090"},
+ {Name: "job", Value: "prometheus"},
+ },
+ {
+ {Name: "a", Value: "b"},
+ {Name: "instance", Value: "127.0.0.1:9090"},
+ {Name: "job", Value: "prom-k8s"},
+ },
+ {
+ {Name: "a", Value: "b"},
+ {Name: "instance", Value: "localhost:9090"},
+ {Name: "job", Value: "prom-k8s"},
+ },
+ {
+ {Name: "a", Value: "c"},
+ {Name: "instance", Value: "localhost:9090"},
+ {Name: "job", Value: "prometheus"},
+ },
+ {
+ {Name: "a", Value: "c"},
+ {Name: "instance", Value: "127.0.0.1:9090"},
+ {Name: "job", Value: "prometheus"},
+ },
+ {
+ {Name: "a", Value: "c"},
+ {Name: "instance", Value: "127.0.0.1:9090"},
+ {Name: "job", Value: "prom-k8s"},
+ },
+ {
+ {Name: "a", Value: "c"},
+ {Name: "instance", Value: "localhost:9090"},
+ {Name: "job", Value: "prom-k8s"},
+ },
+ }
+ seriesMap := map[string][]chunks.Sample{}
+ for _, l := range lbls {
+ seriesMap[labels.New(l...).String()] = []chunks.Sample{}
+ }
+
+ hb, _ := newTestHead(t, 100000, compression.None, false)
+ defer func() {
+ require.NoError(t, hb.Close())
+ }()
+
+ app := hb.AppenderV2(context.Background())
+ for _, l := range lbls {
+ ls := labels.New(l...)
+ series := []chunks.Sample{}
+ ts := rand.Int63n(300)
+ for range numDatapoints {
+ v := rand.Float64()
+ _, err := app.Append(0, ls, 0, ts, v, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ series = append(series, sample{0, ts, v, nil, nil})
+ ts += rand.Int63n(timeInterval) + 1
+ }
+ seriesMap[labels.New(l...).String()] = series
+ }
+ require.NoError(t, app.Commit())
+ // Delete a time-range from each-selector.
+ dels := []struct {
+ ms []*labels.Matcher
+ drange tombstones.Intervals
+ }{
+ {
+ ms: []*labels.Matcher{labels.MustNewMatcher(labels.MatchEqual, "a", "b")},
+ drange: tombstones.Intervals{{Mint: 300, Maxt: 500}, {Mint: 600, Maxt: 670}},
+ },
+ {
+ ms: []*labels.Matcher{
+ labels.MustNewMatcher(labels.MatchEqual, "a", "b"),
+ labels.MustNewMatcher(labels.MatchEqual, "job", "prom-k8s"),
+ },
+ drange: tombstones.Intervals{{Mint: 300, Maxt: 500}, {Mint: 100, Maxt: 670}},
+ },
+ {
+ ms: []*labels.Matcher{
+ labels.MustNewMatcher(labels.MatchEqual, "a", "c"),
+ labels.MustNewMatcher(labels.MatchEqual, "instance", "localhost:9090"),
+ labels.MustNewMatcher(labels.MatchEqual, "job", "prometheus"),
+ },
+ drange: tombstones.Intervals{{Mint: 300, Maxt: 400}, {Mint: 100, Maxt: 6700}},
+ },
+ // TODO: Add Regexp Matchers.
+ }
+ for _, del := range dels {
+ for _, r := range del.drange {
+ require.NoError(t, hb.Delete(context.Background(), r.Mint, r.Maxt, del.ms...))
+ }
+ matched := labels.Slice{}
+ for _, l := range lbls {
+ s := labels.Selector(del.ms)
+ ls := labels.New(l...)
+ if s.Matches(ls) {
+ matched = append(matched, ls)
+ }
+ }
+ sort.Sort(matched)
+ for range numRanges {
+ q, err := NewBlockQuerier(hb, 0, 100000)
+ require.NoError(t, err)
+ ss := q.Select(context.Background(), true, nil, del.ms...)
+ // Build the mockSeriesSet.
+ matchedSeries := make([]storage.Series, 0, len(matched))
+ for _, m := range matched {
+ smpls := seriesMap[m.String()]
+ smpls = deletedSamples(smpls, del.drange)
+ // Only append those series for which samples exist as mockSeriesSet
+ // doesn't skip series with no samples.
+ // TODO: But sometimes SeriesSet returns an empty chunkenc.Iterator
+ if len(smpls) > 0 {
+ matchedSeries = append(matchedSeries, storage.NewListSeries(m, smpls))
+ }
+ }
+ expSs := newMockSeriesSet(matchedSeries)
+ // Compare both SeriesSets.
+ for {
+ eok, rok := expSs.Next(), ss.Next()
+ // Skip a series if iterator is empty.
+ if rok {
+ for ss.At().Iterator(nil).Next() == chunkenc.ValNone {
+ rok = ss.Next()
+ if !rok {
+ break
+ }
+ }
+ }
+ require.Equal(t, eok, rok)
+ if !eok {
+ break
+ }
+ sexp := expSs.At()
+ sres := ss.At()
+ require.Equal(t, sexp.Labels(), sres.Labels())
+ smplExp, errExp := storage.ExpandSamples(sexp.Iterator(nil), nil)
+ smplRes, errRes := storage.ExpandSamples(sres.Iterator(nil), nil)
+ require.Equal(t, errExp, errRes)
+ require.Equal(t, smplExp, smplRes)
+ }
+ require.NoError(t, ss.Err())
+ require.Empty(t, ss.Warnings())
+ require.NoError(t, q.Close())
+ }
+ }
+}
+
+func TestHeadAppenderV2_UncommittedSamplesNotLostOnTruncate(t *testing.T) {
+ h, _ := newTestHead(t, 1000, compression.None, false)
+ defer func() {
+ require.NoError(t, h.Close())
+ }()
+
+ h.initTime(0)
+
+ app := h.appenderV2()
+ lset := labels.FromStrings("a", "1")
+ _, err := app.Append(0, lset, 0, 2100, 1, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+
+ require.NoError(t, h.Truncate(2000))
+ require.NotNil(t, h.series.getByHash(lset.Hash(), lset), "series should not have been garbage collected")
+
+ require.NoError(t, app.Commit())
+
+ q, err := NewBlockQuerier(h, 1500, 2500)
+ require.NoError(t, err)
+ defer q.Close()
+
+ ss := q.Select(context.Background(), false, nil, labels.MustNewMatcher(labels.MatchEqual, "a", "1"))
+ require.True(t, ss.Next())
+ for ss.Next() {
+ }
+ require.NoError(t, ss.Err())
+ require.Empty(t, ss.Warnings())
+}
+
+func TestHeadAppenderV2_TestRemoveSeriesAfterRollbackAndTruncate(t *testing.T) {
+ h, _ := newTestHead(t, 1000, compression.None, false)
+ defer func() {
+ require.NoError(t, h.Close())
+ }()
+
+ h.initTime(0)
+
+ app := h.appenderV2()
+ lset := labels.FromStrings("a", "1")
+ _, err := app.Append(0, lset, 0, 2100, 1, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+
+ require.NoError(t, h.Truncate(2000))
+ require.NotNil(t, h.series.getByHash(lset.Hash(), lset), "series should not have been garbage collected")
+
+ require.NoError(t, app.Rollback())
+
+ q, err := NewBlockQuerier(h, 1500, 2500)
+ require.NoError(t, err)
+
+ ss := q.Select(context.Background(), false, nil, labels.MustNewMatcher(labels.MatchEqual, "a", "1"))
+ require.False(t, ss.Next())
+ require.Empty(t, ss.Warnings())
+ require.NoError(t, q.Close())
+
+ // Truncate again, this time the series should be deleted
+ require.NoError(t, h.Truncate(2050))
+ require.Equal(t, (*memSeries)(nil), h.series.getByHash(lset.Hash(), lset))
+}
+
+func TestHeadAppenderV2_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.AppenderV2(context.Background())
+ _, err := app.Append(0, labels.FromStrings("a", "b"), 0, 1, 2, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+
+ require.NoError(t, app.Rollback())
+ recs := readTestWAL(t, w.Dir())
+
+ require.Len(t, recs, 1)
+
+ series, ok := recs[0].([]record.RefSeries)
+ require.True(t, ok, "expected series record but got %+v", recs[0])
+ require.Equal(t, []record.RefSeries{{Ref: 1, Labels: labels.FromStrings("a", "b")}}, series)
+ })
+ }
+}
+
+func TestHeadAppenderV2_ReturnsSortedLabelValues(t *testing.T) {
+ h, _ := newTestHead(t, 1000, compression.None, false)
+ defer func() {
+ require.NoError(t, h.Close())
+ }()
+
+ h.initTime(0)
+
+ app := h.appenderV2()
+ for i := 100; i > 0; i-- {
+ for j := range 10 {
+ lset := labels.FromStrings(
+ "__name__", fmt.Sprintf("metric_%d", i),
+ "label", fmt.Sprintf("value_%d", j),
+ )
+ _, err := app.Append(0, lset, 0, 2100, 1, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ }
+ }
+
+ q, err := NewBlockQuerier(h, 1500, 2500)
+ require.NoError(t, err)
+
+ res, _, err := q.LabelValues(context.Background(), "__name__", nil)
+ require.NoError(t, err)
+
+ require.True(t, slices.IsSorted(res))
+ require.NoError(t, q.Close())
+}
+
+func TestHeadAppenderV2_NewWalSegmentOnTruncate(t *testing.T) {
+ h, wal := newTestHead(t, 1000, compression.None, false)
+ defer func() {
+ require.NoError(t, h.Close())
+ }()
+ add := func(ts int64) {
+ app := h.AppenderV2(context.Background())
+ _, err := app.Append(0, labels.FromStrings("a", "b"), 0, ts, 0, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ }
+
+ add(0)
+ _, last, err := wlog.Segments(wal.Dir())
+ require.NoError(t, err)
+ require.Equal(t, 0, last)
+
+ add(1)
+ require.NoError(t, h.Truncate(1))
+ _, last, err = wlog.Segments(wal.Dir())
+ require.NoError(t, err)
+ require.Equal(t, 1, last)
+
+ add(2)
+ require.NoError(t, h.Truncate(2))
+ _, last, err = wlog.Segments(wal.Dir())
+ require.NoError(t, err)
+ require.Equal(t, 2, last)
+}
+
+func TestHeadAppenderV2_Append_DuplicateLabelName(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.AppenderV2(context.Background())
+ _, err := app.Append(0, labels, 0, 0, 0, nil, nil, storage.AOptions{})
+ require.EqualError(t, err, fmt.Sprintf(`label name "%s" is not unique: invalid sample`, labelName))
+ }
+
+ add(labels.FromStrings("a", "c", "a", "b"), "a")
+ add(labels.FromStrings("a", "c", "a", "c"), "a")
+ add(labels.FromStrings("__name__", "up", "job", "prometheus", "le", "500", "le", "400", "unit", "s"), "le")
+}
+
+func TestHeadAppenderV2_MemSeriesIsolation(t *testing.T) {
+ if defaultIsolationDisabled {
+ t.Skip("skipping test since tsdb isolation is disabled")
+ }
+
+ // Put a series, select it. GC it and then access it.
+ lastValue := func(h *Head, maxAppendID uint64) int {
+ idx, err := h.Index()
+
+ require.NoError(t, err)
+
+ iso := h.iso.State(math.MinInt64, math.MaxInt64)
+ iso.maxAppendID = maxAppendID
+
+ chunks, err := h.chunksRange(math.MinInt64, math.MaxInt64, iso)
+ require.NoError(t, err)
+ // Hm.. here direct block chunk querier might be required?
+ querier := blockQuerier{
+ blockBaseQuerier: &blockBaseQuerier{
+ index: idx,
+ chunks: chunks,
+ tombstones: tombstones.NewMemTombstones(),
+
+ mint: 0,
+ maxt: 10000,
+ },
+ }
+
+ require.NoError(t, err)
+ defer querier.Close()
+
+ ss := querier.Select(context.Background(), false, nil, labels.MustNewMatcher(labels.MatchEqual, "foo", "bar"))
+ _, seriesSet, ws, err := expandSeriesSet(ss)
+ require.NoError(t, err)
+ require.Empty(t, ws)
+
+ for _, series := range seriesSet {
+ return int(series[len(series)-1].f)
+ }
+ return -1
+ }
+
+ addSamples := func(h *Head) int {
+ i := 1
+ for ; i <= 1000; i++ {
+ var app storage.AppenderV2
+ // To initialize bounds.
+ if h.MinTime() == math.MaxInt64 {
+ app = &initAppenderV2{head: h}
+ } else {
+ a := h.appenderV2()
+ a.cleanupAppendIDsBelow = 0
+ app = a
+ }
+
+ _, err := app.Append(0, labels.FromStrings("foo", "bar"), 0, int64(i), float64(i), nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ h.mmapHeadChunks()
+ }
+ return i
+ }
+
+ testIsolation := func(*Head, int) {
+ }
+
+ // Test isolation without restart of Head.
+ hb, _ := newTestHead(t, 1000, compression.None, false)
+ i := addSamples(hb)
+ testIsolation(hb, i)
+
+ // Test simple cases in different chunks when no appendID cleanup has been performed.
+ require.Equal(t, 10, lastValue(hb, 10))
+ require.Equal(t, 130, lastValue(hb, 130))
+ require.Equal(t, 160, lastValue(hb, 160))
+ require.Equal(t, 240, lastValue(hb, 240))
+ require.Equal(t, 500, lastValue(hb, 500))
+ require.Equal(t, 750, lastValue(hb, 750))
+ require.Equal(t, 995, lastValue(hb, 995))
+ require.Equal(t, 999, lastValue(hb, 999))
+
+ // Cleanup appendIDs below 500.
+ app := hb.appenderV2()
+ app.cleanupAppendIDsBelow = 500
+ _, err := app.Append(0, labels.FromStrings("foo", "bar"), 0, int64(i), float64(i), nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ i++
+
+ // We should not get queries with a maxAppendID below 500 after the cleanup,
+ // but they only take the remaining appendIDs into account.
+ require.Equal(t, 499, lastValue(hb, 10))
+ require.Equal(t, 499, lastValue(hb, 130))
+ require.Equal(t, 499, lastValue(hb, 160))
+ require.Equal(t, 499, lastValue(hb, 240))
+ require.Equal(t, 500, lastValue(hb, 500))
+ require.Equal(t, 995, lastValue(hb, 995))
+ require.Equal(t, 999, lastValue(hb, 999))
+
+ // Cleanup appendIDs below 1000, which means the sample buffer is
+ // the only thing with appendIDs.
+ app = hb.appenderV2()
+ app.cleanupAppendIDsBelow = 1000
+ _, err = app.Append(0, labels.FromStrings("foo", "bar"), 0, int64(i), float64(i), nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ require.Equal(t, 999, lastValue(hb, 998))
+ require.Equal(t, 999, lastValue(hb, 999))
+ require.Equal(t, 1000, lastValue(hb, 1000))
+ require.Equal(t, 1001, lastValue(hb, 1001))
+ require.Equal(t, 1002, lastValue(hb, 1002))
+ require.Equal(t, 1002, lastValue(hb, 1003))
+
+ i++
+ // Cleanup appendIDs below 1001, but with a rollback.
+ app = hb.appenderV2()
+ app.cleanupAppendIDsBelow = 1001
+ _, err = app.Append(0, labels.FromStrings("foo", "bar"), 0, int64(i), float64(i), nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Rollback())
+ require.Equal(t, 1000, lastValue(hb, 999))
+ require.Equal(t, 1000, lastValue(hb, 1000))
+ require.Equal(t, 1001, lastValue(hb, 1001))
+ require.Equal(t, 1002, lastValue(hb, 1002))
+ require.Equal(t, 1002, lastValue(hb, 1003))
+
+ require.NoError(t, hb.Close())
+
+ // Test isolation with restart of Head. This is to verify the num samples of chunks after m-map chunk replay.
+ hb, w := newTestHead(t, 1000, compression.None, false)
+ i = addSamples(hb)
+ require.NoError(t, hb.Close())
+
+ wal, err := wlog.NewSize(nil, nil, w.Dir(), 32768, compression.None)
+ require.NoError(t, err)
+ opts := DefaultHeadOptions()
+ opts.ChunkRange = 1000
+ opts.ChunkDirRoot = wal.Dir()
+ hb, err = NewHead(nil, nil, wal, nil, opts, nil)
+ defer func() { require.NoError(t, hb.Close()) }()
+ require.NoError(t, err)
+ require.NoError(t, hb.Init(0))
+
+ // No appends after restarting. Hence all should return the last value.
+ require.Equal(t, 1000, lastValue(hb, 10))
+ require.Equal(t, 1000, lastValue(hb, 130))
+ require.Equal(t, 1000, lastValue(hb, 160))
+ require.Equal(t, 1000, lastValue(hb, 240))
+ require.Equal(t, 1000, lastValue(hb, 500))
+
+ // Cleanup appendIDs below 1000, which means the sample buffer is
+ // the only thing with appendIDs.
+ app = hb.appenderV2()
+ _, err = app.Append(0, labels.FromStrings("foo", "bar"), 0, int64(i), float64(i), nil, nil, storage.AOptions{})
+ i++
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ require.Equal(t, 1001, lastValue(hb, 998))
+ require.Equal(t, 1001, lastValue(hb, 999))
+ require.Equal(t, 1001, lastValue(hb, 1000))
+ require.Equal(t, 1001, lastValue(hb, 1001))
+ require.Equal(t, 1001, lastValue(hb, 1002))
+ require.Equal(t, 1001, lastValue(hb, 1003))
+
+ // Cleanup appendIDs below 1002, but with a rollback.
+ app = hb.appenderV2()
+ _, err = app.Append(0, labels.FromStrings("foo", "bar"), 0, int64(i), float64(i), nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Rollback())
+ require.Equal(t, 1001, lastValue(hb, 999))
+ require.Equal(t, 1001, lastValue(hb, 1000))
+ require.Equal(t, 1001, lastValue(hb, 1001))
+ require.Equal(t, 1001, lastValue(hb, 1002))
+ require.Equal(t, 1001, lastValue(hb, 1003))
+}
+
+func TestHeadAppenderV2_IsolationRollback(t *testing.T) {
+ if defaultIsolationDisabled {
+ t.Skip("skipping test since tsdb isolation is disabled")
+ }
+
+ // 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.AppenderV2(context.Background())
+ _, err := app.Append(0, labels.FromStrings("foo", "bar"), 0, 0, 0, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ require.Equal(t, uint64(1), hb.iso.lowWatermark())
+
+ app = hb.AppenderV2(context.Background())
+ _, err = app.Append(0, labels.FromStrings("foo", "bar"), 0, 1, 1, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ _, err = app.Append(0, labels.FromStrings("foo", "bar", "foo", "baz"), 0, 2, 2, nil, nil, storage.AOptions{})
+ require.Error(t, err)
+ require.NoError(t, app.Rollback())
+ require.Equal(t, uint64(2), hb.iso.lowWatermark())
+
+ app = hb.AppenderV2(context.Background())
+ _, err = app.Append(0, labels.FromStrings("foo", "bar"), 0, 3, 3, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ require.Equal(t, uint64(3), hb.iso.lowWatermark(), "Low watermark should proceed to 3 even if append #2 was rolled back.")
+}
+
+func TestHeadAppenderV2_IsolationLowWatermarkMonotonous(t *testing.T) {
+ if defaultIsolationDisabled {
+ t.Skip("skipping test since tsdb isolation is disabled")
+ }
+
+ hb, _ := newTestHead(t, 1000, compression.None, false)
+ defer func() {
+ require.NoError(t, hb.Close())
+ }()
+
+ app1 := hb.AppenderV2(context.Background())
+ _, err := app1.Append(0, labels.FromStrings("foo", "bar"), 0, 0, 0, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app1.Commit())
+ require.Equal(t, uint64(1), hb.iso.lowWatermark(), "Low watermark should by 1 after 1st append.")
+
+ app1 = hb.AppenderV2(context.Background())
+ _, err = app1.Append(0, labels.FromStrings("foo", "bar"), 0, 1, 1, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.Equal(t, uint64(2), hb.iso.lowWatermark(), "Low watermark should be two, even if append is not committed yet.")
+
+ app2 := hb.AppenderV2(context.Background())
+ _, err = app2.Append(0, labels.FromStrings("foo", "baz"), 0, 1, 1, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app2.Commit())
+ require.Equal(t, uint64(2), hb.iso.lowWatermark(), "Low watermark should stay two because app1 is not committed yet.")
+
+ is := hb.iso.State(math.MinInt64, math.MaxInt64)
+ require.Equal(t, uint64(2), hb.iso.lowWatermark(), "After simulated read (iso state retrieved), low watermark should stay at 2.")
+
+ require.NoError(t, app1.Commit())
+ require.Equal(t, uint64(2), hb.iso.lowWatermark(), "Even after app1 is committed, low watermark should stay at 2 because read is still ongoing.")
+
+ is.Close()
+ require.Equal(t, uint64(3), hb.iso.lowWatermark(), "After read has finished (iso state closed), low watermark should jump to three.")
+}
+
+func TestHeadAppenderV2_IsolationWithoutAdd(t *testing.T) {
+ if defaultIsolationDisabled {
+ t.Skip("skipping test since tsdb isolation is disabled")
+ }
+
+ hb, _ := newTestHead(t, 1000, compression.None, false)
+ defer func() {
+ require.NoError(t, hb.Close())
+ }()
+
+ app := hb.AppenderV2(context.Background())
+ require.NoError(t, app.Commit())
+
+ app = hb.AppenderV2(context.Background())
+ _, err := app.Append(0, labels.FromStrings("foo", "baz"), 0, 1, 1, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+
+ require.Equal(t, hb.iso.lastAppendID(), hb.iso.lowWatermark(), "High watermark should be equal to the low watermark")
+}
+
+func TestHeadAppenderV2_Append_OutOfOrderSamplesMetric(t *testing.T) {
+ t.Parallel()
+ for name, scenario := range sampleTypeScenarios {
+ t.Run(name, func(t *testing.T) {
+ options := DefaultOptions()
+ testHeadAppenderV2OutOfOrderSamplesMetric(t, scenario, options, storage.ErrOutOfOrderSample)
+ })
+ }
+}
+
+func TestHeadAppenderV2_Append_OutOfOrderSamplesMetricNativeHistogramOOODisabled(t *testing.T) {
+ for name, scenario := range sampleTypeScenarios {
+ if scenario.sampleType != "histogram" {
+ continue
+ }
+ t.Run(name, func(t *testing.T) {
+ options := DefaultOptions()
+ options.OutOfOrderTimeWindow = 0
+ testHeadAppenderV2OutOfOrderSamplesMetric(t, scenario, options, storage.ErrOutOfOrderSample)
+ })
+ }
+}
+
+func testHeadAppenderV2OutOfOrderSamplesMetric(t *testing.T, scenario sampleTypeScenario, options *Options, expectOutOfOrderError error) {
+ dir := t.TempDir()
+ db, err := Open(dir, nil, nil, options, nil)
+ require.NoError(t, err)
+ defer func() {
+ require.NoError(t, db.Close())
+ }()
+ db.DisableCompactions()
+
+ appendSample := func(app storage.AppenderV2, ts int64) (storage.SeriesRef, error) {
+ // TODO(bwplotka): Migrate to V2 natively.
+ ref, _, err := scenario.appendFunc(storage.AppenderV2AsLimitedV1(app), labels.FromStrings("a", "b"), ts, 99)
+ return ref, err
+ }
+
+ ctx := context.Background()
+ app := db.AppenderV2(ctx)
+ for i := 1; i <= 5; i++ {
+ _, err = appendSample(app, int64(i))
+ require.NoError(t, err)
+ }
+ require.NoError(t, app.Commit())
+
+ // Test out of order metric.
+ require.Equal(t, 0.0, prom_testutil.ToFloat64(db.head.metrics.outOfOrderSamples.WithLabelValues(scenario.sampleType)))
+ app = db.AppenderV2(ctx)
+ _, err = appendSample(app, 2)
+ require.Equal(t, expectOutOfOrderError, err)
+ require.Equal(t, 1.0, prom_testutil.ToFloat64(db.head.metrics.outOfOrderSamples.WithLabelValues(scenario.sampleType)))
+
+ _, err = appendSample(app, 3)
+ require.Equal(t, expectOutOfOrderError, err)
+ require.Equal(t, 2.0, prom_testutil.ToFloat64(db.head.metrics.outOfOrderSamples.WithLabelValues(scenario.sampleType)))
+
+ _, err = appendSample(app, 4)
+ require.Equal(t, expectOutOfOrderError, err)
+ require.Equal(t, 3.0, prom_testutil.ToFloat64(db.head.metrics.outOfOrderSamples.WithLabelValues(scenario.sampleType)))
+ require.NoError(t, app.Commit())
+
+ // Compact Head to test out of bound metric.
+ app = db.AppenderV2(ctx)
+ _, err = appendSample(app, DefaultBlockDuration*2)
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+
+ require.Equal(t, int64(math.MinInt64), db.head.minValidTime.Load())
+ require.NoError(t, db.Compact(ctx))
+ require.Positive(t, db.head.minValidTime.Load())
+ require.Equal(t, 0.0, prom_testutil.ToFloat64(db.head.metrics.outOfBoundSamples.WithLabelValues(scenario.sampleType)))
+
+ app = db.AppenderV2(ctx)
+ _, err = appendSample(app, db.head.minValidTime.Load()-2)
+ require.Equal(t, storage.ErrOutOfBounds, err)
+ require.Equal(t, 1.0, prom_testutil.ToFloat64(db.head.metrics.outOfBoundSamples.WithLabelValues(scenario.sampleType)))
+
+ _, err = appendSample(app, db.head.minValidTime.Load()-1)
+ require.Equal(t, storage.ErrOutOfBounds, err)
+ require.Equal(t, 2.0, prom_testutil.ToFloat64(db.head.metrics.outOfBoundSamples.WithLabelValues(scenario.sampleType)))
+ require.NoError(t, app.Commit())
+
+ // Some more valid samples for out of order.
+ app = db.AppenderV2(ctx)
+ for i := 1; i <= 5; i++ {
+ _, err = appendSample(app, db.head.minValidTime.Load()+DefaultBlockDuration+int64(i))
+ require.NoError(t, err)
+ }
+ require.NoError(t, app.Commit())
+
+ // Test out of order metric.
+ app = db.AppenderV2(ctx)
+ _, err = appendSample(app, db.head.minValidTime.Load()+DefaultBlockDuration+2)
+ require.Equal(t, expectOutOfOrderError, err)
+ require.Equal(t, 4.0, prom_testutil.ToFloat64(db.head.metrics.outOfOrderSamples.WithLabelValues(scenario.sampleType)))
+
+ _, err = appendSample(app, db.head.minValidTime.Load()+DefaultBlockDuration+3)
+ require.Equal(t, expectOutOfOrderError, err)
+ require.Equal(t, 5.0, prom_testutil.ToFloat64(db.head.metrics.outOfOrderSamples.WithLabelValues(scenario.sampleType)))
+
+ _, err = appendSample(app, db.head.minValidTime.Load()+DefaultBlockDuration+4)
+ require.Equal(t, expectOutOfOrderError, err)
+ require.Equal(t, 6.0, prom_testutil.ToFloat64(db.head.metrics.outOfOrderSamples.WithLabelValues(scenario.sampleType)))
+ require.NoError(t, app.Commit())
+}
+
+func TestHeadLabelNamesValuesWithMinMaxRange_AppenderV2(t *testing.T) {
+ head, _ := newTestHead(t, 1000, compression.None, false)
+ defer func() {
+ require.NoError(t, head.Close())
+ }()
+
+ const (
+ firstSeriesTimestamp int64 = 100
+ secondSeriesTimestamp int64 = 200
+ lastSeriesTimestamp int64 = 300
+ )
+ var (
+ seriesTimestamps = []int64{
+ firstSeriesTimestamp,
+ secondSeriesTimestamp,
+ lastSeriesTimestamp,
+ }
+ expectedLabelNames = []string{"a", "b", "c"}
+ expectedLabelValues = []string{"d", "e", "f"}
+ ctx = context.Background()
+ )
+
+ app := head.AppenderV2(ctx)
+ for i, name := range expectedLabelNames {
+ _, err := app.Append(0, labels.FromStrings(name, expectedLabelValues[i]), 0, seriesTimestamps[i], 0, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ }
+ require.NoError(t, app.Commit())
+ require.Equal(t, firstSeriesTimestamp, head.MinTime())
+ require.Equal(t, lastSeriesTimestamp, head.MaxTime())
+
+ testCases := []struct {
+ name string
+ mint int64
+ maxt int64
+ expectedNames []string
+ expectedValues []string
+ }{
+ {"maxt less than head min", head.MaxTime() - 10, head.MinTime() - 10, []string{}, []string{}},
+ {"mint less than head max", head.MaxTime() + 10, head.MinTime() + 10, []string{}, []string{}},
+ {"mint and maxt outside head", head.MaxTime() + 10, head.MinTime() - 10, []string{}, []string{}},
+ {"mint and maxt within head", head.MaxTime() - 10, head.MinTime() + 10, expectedLabelNames, expectedLabelValues},
+ }
+
+ for _, tt := range testCases {
+ t.Run(tt.name, func(t *testing.T) {
+ headIdxReader := head.indexRange(tt.mint, tt.maxt)
+ actualLabelNames, err := headIdxReader.LabelNames(ctx)
+ require.NoError(t, err)
+ require.Equal(t, tt.expectedNames, actualLabelNames)
+ if len(tt.expectedValues) > 0 {
+ for i, name := range expectedLabelNames {
+ actualLabelValue, err := headIdxReader.SortedLabelValues(ctx, name, nil)
+ require.NoError(t, err)
+ require.Equal(t, []string{tt.expectedValues[i]}, actualLabelValue)
+ }
+ }
+ })
+ }
+}
+
+func TestHeadAppenderV2_ErrReuse(t *testing.T) {
+ head, _ := newTestHead(t, 1000, compression.None, false)
+ defer func() {
+ require.NoError(t, head.Close())
+ }()
+
+ app := head.AppenderV2(context.Background())
+ _, err := app.Append(0, labels.FromStrings("test", "test"), 0, 0, 0, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ require.Error(t, app.Commit())
+ require.Error(t, app.Rollback())
+
+ app = head.AppenderV2(context.Background())
+ _, err = app.Append(0, labels.FromStrings("test", "test"), 0, 1, 0, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Rollback())
+ require.Error(t, app.Rollback())
+ require.Error(t, app.Commit())
+
+ app = head.AppenderV2(context.Background())
+ _, err = app.Append(0, labels.FromStrings("test", "test"), 0, 2, 0, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ require.Error(t, app.Rollback())
+ require.Error(t, app.Commit())
+
+ app = head.AppenderV2(context.Background())
+ _, err = app.Append(0, labels.FromStrings("test", "test"), 0, 3, 0, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Rollback())
+ require.Error(t, app.Commit())
+ require.Error(t, app.Rollback())
+}
+
+func TestHeadAppenderV2_MinTimeAfterTruncation(t *testing.T) {
+ chunkRange := int64(2000)
+ head, _ := newTestHead(t, chunkRange, compression.None, false)
+
+ app := head.AppenderV2(context.Background())
+ _, err := app.Append(0, labels.FromStrings("a", "b"), 0, 100, 100, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ _, err = app.Append(0, labels.FromStrings("a", "b"), 0, 4000, 200, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ _, err = app.Append(0, labels.FromStrings("a", "b"), 0, 8000, 300, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+
+ // Truncating outside the appendable window and actual mint being outside
+ // appendable window should leave mint at the actual mint.
+ require.NoError(t, head.Truncate(3500))
+ require.Equal(t, int64(4000), head.MinTime())
+ require.Equal(t, int64(4000), head.minValidTime.Load())
+
+ // After truncation outside the appendable window if the actual min time
+ // is in the appendable window then we should leave mint at the start of appendable window.
+ require.NoError(t, head.Truncate(5000))
+ require.Equal(t, head.appendableMinValidTime(), head.MinTime())
+ require.Equal(t, head.appendableMinValidTime(), head.minValidTime.Load())
+
+ // If the truncation time is inside the appendable window, then the min time
+ // should be the truncation time.
+ 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 TestHeadAppenderV2_AppendExemplars(t *testing.T) {
+ chunkRange := int64(2000)
+ head, _ := newTestHead(t, chunkRange, compression.None, false)
+ app := head.AppenderV2(context.Background())
+
+ l := labels.FromStrings("trace_id", "123")
+
+ // It is perfectly valid to add Exemplars before the current start time -
+ // histogram buckets that haven't been update in a while could still be
+ // exported exemplars from an hour ago.
+ _, err := app.Append(0, labels.FromStrings("a", "b"), 0, 100, 100, nil, nil, storage.AOptions{
+ Exemplars: []exemplar.Exemplar{{Labels: l, HasTs: true, Ts: -1000, Value: 1}},
+ })
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ require.NoError(t, head.Close())
+}
+
+// Tests https://github.com/prometheus/prometheus/issues/9079.
+func TestDataMissingOnQueryDuringCompaction_AppenderV2(t *testing.T) {
+ t.Parallel()
+ db := newTestDB(t)
+ db.DisableCompactions()
+ ctx := context.Background()
+
+ var (
+ app = db.AppenderV2(context.Background())
+ ref = storage.SeriesRef(0)
+ mint, maxt = int64(0), int64(0)
+ err error
+ )
+
+ // Appends samples to span over 1.5 block ranges.
+ expSamples := make([]chunks.Sample, 0)
+ // 7 chunks with 15s scrape interval.
+ for i := int64(0); i <= 120*7; i++ {
+ ts := i * DefaultBlockDuration / (4 * 120)
+ 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{0, ts, float64(i), nil, nil})
+ }
+ require.NoError(t, app.Commit())
+
+ // Get a querier before compaction (or when compaction is about to begin).
+ q, err := db.Querier(mint, maxt)
+ require.NoError(t, err)
+
+ var wg sync.WaitGroup
+ wg.Go(func() {
+ // Compacting head while the querier spans the compaction time.
+ require.NoError(t, db.Compact(ctx))
+ require.NotEmpty(t, db.Blocks())
+ })
+
+ // Give enough time for compaction to finish.
+ // We expect it to be blocked until querier is closed.
+ <-time.After(3 * time.Second)
+
+ // Querying the querier that was got before compaction.
+ series := query(t, q, labels.MustNewMatcher(labels.MatchEqual, "a", "b"))
+ require.Equal(t, map[string][]chunks.Sample{`{a="b"}`: expSamples}, series)
+
+ wg.Wait()
+}
+
+func TestIsQuerierCollidingWithTruncation_AppenderV2(t *testing.T) {
+ db := newTestDB(t)
+ db.DisableCompactions()
+
+ var (
+ app = db.AppenderV2(context.Background())
+ ref = storage.SeriesRef(0)
+ err error
+ )
+
+ for i := int64(0); i <= 3000; i++ {
+ ref, err = app.Append(ref, labels.FromStrings("a", "b"), 0, i, float64(i), nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ }
+ require.NoError(t, app.Commit())
+
+ // This mocks truncation.
+ db.head.memTruncationInProcess.Store(true)
+ db.head.lastMemoryTruncationTime.Store(2000)
+
+ // Test that IsQuerierValid suggests correct querier ranges.
+ cases := []struct {
+ mint, maxt int64 // For the querier.
+ expShouldClose, expGetNew bool
+ expNewMint int64
+ }{
+ {-200, -100, true, false, 0},
+ {-200, 300, true, false, 0},
+ {100, 1900, true, false, 0},
+ {1900, 2200, true, true, 2000},
+ {2000, 2500, false, false, 0},
+ }
+
+ for _, c := range cases {
+ t.Run(fmt.Sprintf("mint=%d,maxt=%d", c.mint, c.maxt), func(t *testing.T) {
+ shouldClose, getNew, newMint := db.head.IsQuerierCollidingWithTruncation(c.mint, c.maxt)
+ require.Equal(t, c.expShouldClose, shouldClose)
+ require.Equal(t, c.expGetNew, getNew)
+ if getNew {
+ require.Equal(t, c.expNewMint, newMint)
+ }
+ })
+ }
+}
+
+func TestWaitForPendingReadersInTimeRange_AppenderV2(t *testing.T) {
+ t.Parallel()
+ db := newTestDB(t)
+ db.DisableCompactions()
+
+ sampleTs := func(i int64) int64 { return i * DefaultBlockDuration / (4 * 120) }
+
+ var (
+ app = db.AppenderV2(context.Background())
+ ref = storage.SeriesRef(0)
+ err error
+ )
+
+ for i := int64(0); i <= 3000; i++ {
+ ts := sampleTs(i)
+ ref, err = app.Append(ref, labels.FromStrings("a", "b"), 0, ts, float64(i), nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ }
+ require.NoError(t, app.Commit())
+
+ truncMint, truncMaxt := int64(1000), int64(2000)
+ cases := []struct {
+ mint, maxt int64
+ shouldWait bool
+ }{
+ {0, 500, false}, // Before truncation range.
+ {500, 1500, true}, // Overlaps with truncation at the start.
+ {1200, 1700, true}, // Within truncation range.
+ {1800, 2500, true}, // Overlaps with truncation at the end.
+ {2000, 2500, false}, // After truncation range.
+ {2100, 2500, false}, // After truncation range.
+ }
+ for _, c := range cases {
+ t.Run(fmt.Sprintf("mint=%d,maxt=%d,shouldWait=%t", c.mint, c.maxt, c.shouldWait), func(t *testing.T) {
+ // checkWaiting verifies WaitForPendingReadersInTimeRange behavior using synctest
+ // for deterministic time control. The function should block while an overlapping
+ // querier is open and return immediately when there's no overlap.
+ checkWaiting := func(cl io.Closer) {
+ synctest.Test(t, func(t *testing.T) {
+ var waitOver atomic.Bool
+ go func() {
+ db.head.WaitForPendingReadersInTimeRange(truncMint, truncMaxt)
+ waitOver.Store(true)
+ }()
+
+ // Wait for goroutine to either complete (no overlap) or block on Sleep (overlap).
+ synctest.Wait()
+
+ if c.shouldWait {
+ require.False(t, waitOver.Load(),
+ "WaitForPendingReadersInTimeRange should block while overlapping querier is open")
+ require.NoError(t, cl.Close())
+ // Advance fake time past the 500ms poll interval, then let goroutine process.
+ time.Sleep(time.Second)
+ synctest.Wait()
+ require.True(t, waitOver.Load(),
+ "WaitForPendingReadersInTimeRange should complete after querier is closed")
+ } else {
+ require.True(t, waitOver.Load(),
+ "WaitForPendingReadersInTimeRange should return immediately when no overlap")
+ require.NoError(t, cl.Close())
+ }
+ })
+ }
+
+ q, err := db.Querier(c.mint, c.maxt)
+ require.NoError(t, err)
+ checkWaiting(q)
+
+ cq, err := db.ChunkQuerier(c.mint, c.maxt)
+ require.NoError(t, err)
+ checkWaiting(cq)
+ })
+ }
+}
+
+func TestChunkQueryOOOHeadDuringTruncate_AppenderV2(t *testing.T) {
+ testQueryOOOHeadDuringTruncateAppenderV2(t,
+ func(db *DB, minT, maxT int64) (storage.LabelQuerier, error) {
+ return db.ChunkQuerier(minT, maxT)
+ },
+ func(t *testing.T, lq storage.LabelQuerier, minT, _ int64) {
+ // Chunks
+ q, ok := lq.(storage.ChunkQuerier)
+ require.True(t, ok)
+ ss := q.Select(context.Background(), false, nil, labels.MustNewMatcher(labels.MatchEqual, "a", "b"))
+ require.True(t, ss.Next())
+ s := ss.At()
+ require.False(t, ss.Next()) // One series.
+ metaIt := s.Iterator(nil)
+ require.True(t, metaIt.Next())
+ meta := metaIt.At()
+ // Samples
+ it := meta.Chunk.Iterator(nil)
+ require.NotEqual(t, chunkenc.ValNone, it.Next()) // Has some data.
+ require.Equal(t, minT, it.AtT()) // It is an in-order sample.
+ require.NotEqual(t, chunkenc.ValNone, it.Next()) // Has some data.
+ require.Equal(t, minT+50, it.AtT()) // it is an out-of-order sample.
+ require.NoError(t, it.Err())
+ },
+ )
+}
+
+func testQueryOOOHeadDuringTruncateAppenderV2(t *testing.T, makeQuerier func(db *DB, minT, maxT int64) (storage.LabelQuerier, error), verify func(t *testing.T, q storage.LabelQuerier, minT, maxT int64)) {
+ const maxT int64 = 6000
+
+ dir := t.TempDir()
+ opts := DefaultOptions()
+ opts.OutOfOrderTimeWindow = maxT
+ opts.MinBlockDuration = maxT / 2 // So that head will compact up to 3000.
+
+ db, err := Open(dir, nil, nil, opts, nil)
+ require.NoError(t, err)
+ t.Cleanup(func() {
+ require.NoError(t, db.Close())
+ })
+ db.DisableCompactions()
+
+ var (
+ ref = storage.SeriesRef(0)
+ app = db.AppenderV2(context.Background())
+ )
+ // Add in-order samples at every 100ms starting at 0ms.
+ for i := int64(0); i < maxT; i += 100 {
+ _, err := app.Append(ref, labels.FromStrings("a", "b"), 0, i, 0, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ }
+ // Add out-of-order samples at every 100ms starting at 50ms.
+ for i := int64(50); i < maxT; i += 100 {
+ _, err := app.Append(ref, labels.FromStrings("a", "b"), 0, i, 0, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ }
+ require.NoError(t, app.Commit())
+
+ requireEqualOOOSamples(t, int(maxT/100-1), db)
+
+ // Synchronization points.
+ allowQueryToStart := make(chan struct{})
+ queryStarted := make(chan struct{})
+ compactionFinished := make(chan struct{})
+
+ db.head.memTruncationCallBack = func() {
+ // Compaction has started, let the query start and wait for it to actually start to simulate race condition.
+ allowQueryToStart <- struct{}{}
+ <-queryStarted
+ }
+
+ go func() {
+ db.Compact(context.Background()) // Compact and write blocks up to 3000 (maxtT/2).
+ compactionFinished <- struct{}{}
+ }()
+
+ // Wait for the compaction to start.
+ <-allowQueryToStart
+
+ q, err := makeQuerier(db, 1500, 2500)
+ require.NoError(t, err)
+ queryStarted <- struct{}{} // Unblock the compaction.
+ ctx := context.Background()
+
+ // Label names.
+ res, annots, err := q.LabelNames(ctx, nil, labels.MustNewMatcher(labels.MatchEqual, "a", "b"))
+ require.NoError(t, err)
+ require.Empty(t, annots)
+ require.Equal(t, []string{"a"}, res)
+
+ // Label values.
+ res, annots, err = q.LabelValues(ctx, "a", nil, labels.MustNewMatcher(labels.MatchEqual, "a", "b"))
+ require.NoError(t, err)
+ require.Empty(t, annots)
+ require.Equal(t, []string{"b"}, res)
+
+ verify(t, q, 1500, 2500)
+
+ require.NoError(t, q.Close()) // Cannot be deferred as the compaction waits for queries to close before finishing.
+
+ <-compactionFinished // Wait for compaction otherwise Go test finds stray goroutines.
+}
+
+func TestHeadAppenderV2_Append_Histogram(t *testing.T) {
+ l := labels.FromStrings("a", "b")
+ 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)
+ app := head.AppenderV2(context.Background())
+
+ expHistograms := make([]chunks.Sample, 0, 2*numHistograms)
+
+ // Counter integer histograms.
+ for _, h := range tsdbutil.GenerateTestHistograms(numHistograms) {
+ _, err := app.Append(0, l, 0, ingestTs, 0, h, nil, storage.AOptions{})
+ require.NoError(t, err)
+ expHistograms = append(expHistograms, sample{t: ingestTs, h: h})
+ ingestTs++
+ if ingestTs%50 == 0 {
+ require.NoError(t, app.Commit())
+ app = head.AppenderV2(context.Background())
+ }
+ }
+
+ // Gauge integer histograms.
+ for _, h := range tsdbutil.GenerateTestGaugeHistograms(numHistograms) {
+ _, err := app.Append(0, l, 0, ingestTs, 0, h, nil, storage.AOptions{})
+ require.NoError(t, err)
+ expHistograms = append(expHistograms, sample{t: ingestTs, h: h})
+ ingestTs++
+ if ingestTs%50 == 0 {
+ require.NoError(t, app.Commit())
+ app = head.AppenderV2(context.Background())
+ }
+ }
+
+ expFloatHistograms := make([]chunks.Sample, 0, 2*numHistograms)
+
+ // Counter float histograms.
+ for _, fh := range tsdbutil.GenerateTestFloatHistograms(numHistograms) {
+ _, err := app.Append(0, l, 0, ingestTs, 0, nil, fh, storage.AOptions{})
+ require.NoError(t, err)
+ expFloatHistograms = append(expFloatHistograms, sample{t: ingestTs, fh: fh})
+ ingestTs++
+ if ingestTs%50 == 0 {
+ require.NoError(t, app.Commit())
+ app = head.AppenderV2(context.Background())
+ }
+ }
+
+ // Gauge float histograms.
+ for _, fh := range tsdbutil.GenerateTestGaugeFloatHistograms(numHistograms) {
+ _, err := app.Append(0, l, 0, ingestTs, 0, nil, fh, storage.AOptions{})
+ require.NoError(t, err)
+ expFloatHistograms = append(expFloatHistograms, sample{t: ingestTs, fh: fh})
+ ingestTs++
+ if ingestTs%50 == 0 {
+ require.NoError(t, app.Commit())
+ app = head.AppenderV2(context.Background())
+ }
+ }
+
+ require.NoError(t, app.Commit())
+
+ q, err := NewBlockQuerier(head, head.MinTime(), head.MaxTime())
+ require.NoError(t, err)
+ t.Cleanup(func() {
+ require.NoError(t, q.Close())
+ })
+
+ ss := q.Select(context.Background(), false, nil, labels.MustNewMatcher(labels.MatchEqual, "a", "b"))
+
+ require.True(t, ss.Next())
+ s := ss.At()
+ require.False(t, ss.Next())
+
+ it := s.Iterator(nil)
+ actHistograms := make([]chunks.Sample, 0, len(expHistograms))
+ actFloatHistograms := make([]chunks.Sample, 0, len(expFloatHistograms))
+ for typ := it.Next(); typ != chunkenc.ValNone; typ = it.Next() {
+ switch typ {
+ case chunkenc.ValHistogram:
+ ts, h := it.AtHistogram(nil)
+ actHistograms = append(actHistograms, sample{t: ts, h: h})
+ case chunkenc.ValFloatHistogram:
+ ts, fh := it.AtFloatHistogram(nil)
+ actFloatHistograms = append(actFloatHistograms, sample{t: ts, fh: fh})
+ }
+ }
+
+ compareSeries(
+ t,
+ map[string][]chunks.Sample{"dummy": expHistograms},
+ map[string][]chunks.Sample{"dummy": actHistograms},
+ )
+ compareSeries(
+ t,
+ map[string][]chunks.Sample{"dummy": expFloatHistograms},
+ map[string][]chunks.Sample{"dummy": actFloatHistograms},
+ )
+ })
+ }
+}
+
+func TestHistogramInWALAndMmapChunk_AppenderV2(t *testing.T) {
+ head, _ := newTestHead(t, 3000, compression.None, false)
+ t.Cleanup(func() {
+ // Captures head by reference, so it closes the final head after restarts.
+ _ = head.Close()
+ })
+ require.NoError(t, head.Init(0))
+
+ // Series with only histograms.
+ s1 := labels.FromStrings("a", "b1")
+ k1 := s1.String()
+ numHistograms := 300
+ exp := map[string][]chunks.Sample{}
+ ts := int64(0)
+ var app storage.AppenderV2
+ for _, gauge := range []bool{true, false} {
+ app = head.AppenderV2(context.Background())
+ var hists []*histogram.Histogram
+ if gauge {
+ hists = tsdbutil.GenerateTestGaugeHistograms(numHistograms)
+ } else {
+ hists = tsdbutil.GenerateTestHistograms(numHistograms)
+ }
+ for _, h := range hists {
+ h.NegativeSpans = h.PositiveSpans
+ h.NegativeBuckets = h.PositiveBuckets
+ _, err := app.Append(0, s1, 0, ts, 0, h, nil, storage.AOptions{})
+ require.NoError(t, err)
+ exp[k1] = append(exp[k1], sample{t: ts, h: h.Copy()})
+ ts++
+ if ts%5 == 0 {
+ require.NoError(t, app.Commit())
+ app = head.AppenderV2(context.Background())
+ }
+ }
+ require.NoError(t, app.Commit())
+ }
+ for _, gauge := range []bool{true, false} {
+ app = head.AppenderV2(context.Background())
+ var hists []*histogram.FloatHistogram
+ if gauge {
+ hists = tsdbutil.GenerateTestGaugeFloatHistograms(numHistograms)
+ } else {
+ hists = tsdbutil.GenerateTestFloatHistograms(numHistograms)
+ }
+ for _, h := range hists {
+ h.NegativeSpans = h.PositiveSpans
+ h.NegativeBuckets = h.PositiveBuckets
+ _, err := app.Append(0, s1, 0, ts, 0, nil, h, storage.AOptions{})
+ require.NoError(t, err)
+ exp[k1] = append(exp[k1], sample{t: ts, fh: h.Copy()})
+ ts++
+ if ts%5 == 0 {
+ require.NoError(t, app.Commit())
+ app = head.AppenderV2(context.Background())
+ }
+ }
+ require.NoError(t, app.Commit())
+ head.mmapHeadChunks()
+ }
+
+ // There should be 20 mmap chunks in s1.
+ ms := head.series.getByHash(s1.Hash(), s1)
+ require.Len(t, ms.mmappedChunks, 25)
+ expMmapChunks := make([]*mmappedChunk, 0, 20)
+ for _, mmap := range ms.mmappedChunks {
+ require.Positive(t, mmap.numSamples)
+ cpy := *mmap
+ expMmapChunks = append(expMmapChunks, &cpy)
+ }
+ expHeadChunkSamples := ms.headChunks.chunk.NumSamples()
+ require.Positive(t, expHeadChunkSamples)
+
+ // Series with mix of histograms and float.
+ s2 := labels.FromStrings("a", "b2")
+ k2 := s2.String()
+ ts = 0
+ for _, gauge := range []bool{true, false} {
+ app = head.AppenderV2(context.Background())
+ var hists []*histogram.Histogram
+ if gauge {
+ hists = tsdbutil.GenerateTestGaugeHistograms(100)
+ } else {
+ hists = tsdbutil.GenerateTestHistograms(100)
+ }
+ for _, h := range hists {
+ ts++
+ h.NegativeSpans = h.PositiveSpans
+ h.NegativeBuckets = h.PositiveBuckets
+ _, err := app.Append(0, s2, 0, ts, 0, h, nil, storage.AOptions{})
+ require.NoError(t, err)
+ eh := h.Copy()
+ if !gauge && ts > 30 && (ts-10)%20 == 1 {
+ // Need "unknown" hint after float sample.
+ eh.CounterResetHint = histogram.UnknownCounterReset
+ }
+ exp[k2] = append(exp[k2], sample{t: ts, h: eh})
+ if ts%20 == 0 {
+ require.NoError(t, app.Commit())
+ app = head.AppenderV2(context.Background())
+ // Add some float.
+ for range 10 {
+ ts++
+ _, err := app.Append(0, s2, 0, ts, float64(ts), nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ exp[k2] = append(exp[k2], sample{t: ts, f: float64(ts)})
+ }
+ require.NoError(t, app.Commit())
+ app = head.AppenderV2(context.Background())
+ }
+ }
+ require.NoError(t, app.Commit())
+ }
+ for _, gauge := range []bool{true, false} {
+ app = head.AppenderV2(context.Background())
+ var hists []*histogram.FloatHistogram
+ if gauge {
+ hists = tsdbutil.GenerateTestGaugeFloatHistograms(100)
+ } else {
+ hists = tsdbutil.GenerateTestFloatHistograms(100)
+ }
+ for _, h := range hists {
+ ts++
+ h.NegativeSpans = h.PositiveSpans
+ h.NegativeBuckets = h.PositiveBuckets
+ _, err := app.Append(0, s2, 0, ts, 0, nil, h, storage.AOptions{})
+ require.NoError(t, err)
+ eh := h.Copy()
+ if !gauge && ts > 30 && (ts-10)%20 == 1 {
+ // Need "unknown" hint after float sample.
+ eh.CounterResetHint = histogram.UnknownCounterReset
+ }
+ exp[k2] = append(exp[k2], sample{t: ts, fh: eh})
+ if ts%20 == 0 {
+ require.NoError(t, app.Commit())
+ app = head.AppenderV2(context.Background())
+ // Add some float.
+ for range 10 {
+ ts++
+ _, err := app.Append(0, s2, 0, ts, float64(ts), nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ exp[k2] = append(exp[k2], sample{t: ts, f: float64(ts)})
+ }
+ require.NoError(t, app.Commit())
+ app = head.AppenderV2(context.Background())
+ }
+ }
+ require.NoError(t, app.Commit())
+ }
+
+ // Restart head.
+ walDir := head.wal.Dir()
+ require.NoError(t, head.Close())
+ startHead := func() {
+ 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)
+ require.NoError(t, head.Init(0))
+ }
+ startHead()
+
+ // Checking contents of s1.
+ ms = head.series.getByHash(s1.Hash(), s1)
+ require.Equal(t, expMmapChunks, ms.mmappedChunks)
+ require.Equal(t, expHeadChunkSamples, ms.headChunks.chunk.NumSamples())
+
+ testQuery := func() {
+ q, err := NewBlockQuerier(head, head.MinTime(), head.MaxTime())
+ require.NoError(t, err)
+ act := query(t, q, labels.MustNewMatcher(labels.MatchRegexp, "a", "b.*"))
+ compareSeries(t, exp, act)
+ }
+ testQuery()
+
+ // Restart with no mmap chunks to test WAL replay.
+ require.NoError(t, head.Close())
+ require.NoError(t, os.RemoveAll(mmappedChunksDir(head.opts.ChunkDirRoot)))
+ startHead()
+ testQuery()
+}
+
+func TestChunkSnapshot_AppenderV2(t *testing.T) {
+ head, _ := newTestHead(t, 120*4, compression.None, false)
+ defer func() {
+ head.opts.EnableMemorySnapshotOnShutdown = false
+ require.NoError(t, head.Close())
+ }()
+
+ type ex struct {
+ seriesLabels labels.Labels
+ e exemplar.Exemplar
+ }
+
+ numSeries := 10
+ expSeries := make(map[string][]chunks.Sample)
+ expHist := make(map[string][]chunks.Sample)
+ expFloatHist := make(map[string][]chunks.Sample)
+ expTombstones := make(map[storage.SeriesRef]tombstones.Intervals)
+ expExemplars := make([]ex, 0)
+ histograms := tsdbutil.GenerateTestGaugeHistograms(481)
+ floatHistogram := tsdbutil.GenerateTestGaugeFloatHistograms(481)
+
+ newExemplar := func(lbls labels.Labels, ts int64) exemplar.Exemplar {
+ e := ex{
+ seriesLabels: lbls,
+ e: exemplar.Exemplar{
+ Labels: labels.FromStrings("trace_id", strconv.Itoa(rand.Int())),
+ Value: rand.Float64(),
+ Ts: ts,
+ },
+ }
+ expExemplars = append(expExemplars, e)
+ return e.e
+ }
+
+ checkSamples := func() {
+ q, err := NewBlockQuerier(head, math.MinInt64, math.MaxInt64)
+ require.NoError(t, err)
+ series := query(t, q, labels.MustNewMatcher(labels.MatchRegexp, "foo", "bar.*"))
+ require.Equal(t, expSeries, series)
+ }
+ checkHistograms := func() {
+ q, err := NewBlockQuerier(head, math.MinInt64, math.MaxInt64)
+ require.NoError(t, err)
+ series := query(t, q, labels.MustNewMatcher(labels.MatchRegexp, "hist", "baz.*"))
+ require.Equal(t, expHist, series)
+ }
+ checkFloatHistograms := func() {
+ q, err := NewBlockQuerier(head, math.MinInt64, math.MaxInt64)
+ require.NoError(t, err)
+ series := query(t, q, labels.MustNewMatcher(labels.MatchRegexp, "floathist", "bat.*"))
+ require.Equal(t, expFloatHist, series)
+ }
+ checkTombstones := func() {
+ tr, err := head.Tombstones()
+ require.NoError(t, err)
+ actTombstones := make(map[storage.SeriesRef]tombstones.Intervals)
+ require.NoError(t, tr.Iter(func(ref storage.SeriesRef, itvs tombstones.Intervals) error {
+ for _, itv := range itvs {
+ actTombstones[ref].Add(itv)
+ }
+ return nil
+ }))
+ require.Equal(t, expTombstones, actTombstones)
+ }
+ checkExemplars := func() {
+ actExemplars := make([]ex, 0, len(expExemplars))
+ err := head.exemplars.IterateExemplars(func(seriesLabels labels.Labels, e exemplar.Exemplar) error {
+ actExemplars = append(actExemplars, ex{
+ seriesLabels: seriesLabels,
+ e: e,
+ })
+ return nil
+ })
+ require.NoError(t, err)
+ // Verifies both existence of right exemplars and order of exemplars in the buffer.
+ testutil.RequireEqualWithOptions(t, expExemplars, actExemplars, []cmp.Option{cmp.AllowUnexported(ex{})})
+ }
+
+ var (
+ wlast, woffset int
+ err error
+ )
+
+ closeHeadAndCheckSnapshot := func() {
+ require.NoError(t, head.Close())
+
+ _, sidx, soffset, err := LastChunkSnapshot(head.opts.ChunkDirRoot)
+ require.NoError(t, err)
+ require.Equal(t, wlast, sidx)
+ require.Equal(t, woffset, soffset)
+ }
+
+ openHeadAndCheckReplay := func() {
+ w, err := wlog.NewSize(nil, nil, head.wal.Dir(), 32768, compression.None)
+ require.NoError(t, err)
+ head, err = NewHead(nil, nil, w, nil, head.opts, nil)
+ require.NoError(t, err)
+ require.NoError(t, head.Init(math.MinInt64))
+
+ checkSamples()
+ checkHistograms()
+ checkFloatHistograms()
+ checkTombstones()
+ checkExemplars()
+ }
+
+ { // Initial data that goes into snapshot.
+ // Add some initial samples with >=1 m-map chunk.
+ app := head.AppenderV2(context.Background())
+ for i := 1; i <= numSeries; i++ {
+ lbls := labels.FromStrings("foo", fmt.Sprintf("bar%d", i))
+ lblStr := lbls.String()
+ lblsHist := labels.FromStrings("hist", fmt.Sprintf("baz%d", i))
+ lblsHistStr := lblsHist.String()
+ lblsFloatHist := labels.FromStrings("floathist", fmt.Sprintf("bat%d", i))
+ lblsFloatHistStr := lblsFloatHist.String()
+
+ // 240 samples should m-map at least 1 chunk.
+ for ts := int64(1); ts <= 240; ts++ {
+ // Add an exemplar, but only to float sample.
+ aOpts := storage.AOptions{}
+ if ts%10 == 0 {
+ aOpts.Exemplars = []exemplar.Exemplar{newExemplar(lbls, ts)}
+ }
+ val := rand.Float64()
+ 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{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{0, ts, 0, nil, floatHist})
+ _, err = app.Append(0, lblsFloatHist, 0, ts, 0, nil, floatHist, storage.AOptions{})
+ require.NoError(t, err)
+
+ // Create multiple WAL records (commit).
+ if ts%10 == 0 {
+ require.NoError(t, app.Commit())
+ app = head.AppenderV2(context.Background())
+ }
+ }
+ }
+ require.NoError(t, app.Commit())
+
+ // Add some tombstones.
+ var enc record.Encoder
+ for i := 1; i <= numSeries; i++ {
+ ref := storage.SeriesRef(i)
+ itvs := tombstones.Intervals{
+ {Mint: 1234, Maxt: 2345},
+ {Mint: 3456, Maxt: 4567},
+ }
+ for _, itv := range itvs {
+ expTombstones[ref].Add(itv)
+ }
+ head.tombstones.AddInterval(ref, itvs...)
+ err := head.wal.Log(enc.Tombstones([]tombstones.Stone{
+ {Ref: ref, Intervals: itvs},
+ }, nil))
+ require.NoError(t, err)
+ }
+ }
+
+ // These references should be the ones used for the snapshot.
+ wlast, woffset, err = head.wal.LastSegmentAndOffset()
+ require.NoError(t, err)
+ if woffset != 0 && woffset < 32*1024 {
+ // The page is always filled before taking the snapshot.
+ woffset = 32 * 1024
+ }
+
+ {
+ // Creating snapshot and verifying it.
+ head.opts.EnableMemorySnapshotOnShutdown = true
+ closeHeadAndCheckSnapshot() // This will create a snapshot.
+
+ // Test the replay of snapshot.
+ openHeadAndCheckReplay()
+ }
+
+ { // Additional data to only include in WAL and m-mapped chunks and not snapshot. This mimics having an old snapshot on disk.
+ // Add more samples.
+ app := head.AppenderV2(context.Background())
+ for i := 1; i <= numSeries; i++ {
+ lbls := labels.FromStrings("foo", fmt.Sprintf("bar%d", i))
+ lblStr := lbls.String()
+ lblsHist := labels.FromStrings("hist", fmt.Sprintf("baz%d", i))
+ lblsHistStr := lblsHist.String()
+ lblsFloatHist := labels.FromStrings("floathist", fmt.Sprintf("bat%d", i))
+ lblsFloatHistStr := lblsFloatHist.String()
+
+ // 240 samples should m-map at least 1 chunk.
+ for ts := int64(241); ts <= 480; ts++ {
+ // Add an exemplar, but only to float sample.
+ aOpts := storage.AOptions{}
+ if ts%10 == 0 {
+ aOpts.Exemplars = []exemplar.Exemplar{newExemplar(lbls, ts)}
+ }
+ val := rand.Float64()
+ 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{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{0, ts, 0, nil, floatHist})
+ _, err = app.Append(0, lblsFloatHist, 0, ts, 0, nil, floatHist, storage.AOptions{})
+ require.NoError(t, err)
+
+ // Create multiple WAL records (commit).
+ if ts%10 == 0 {
+ require.NoError(t, app.Commit())
+ app = head.AppenderV2(context.Background())
+ }
+ }
+ }
+ require.NoError(t, app.Commit())
+
+ // Add more tombstones.
+ var enc record.Encoder
+ for i := 1; i <= numSeries; i++ {
+ ref := storage.SeriesRef(i)
+ itvs := tombstones.Intervals{
+ {Mint: 12345, Maxt: 23456},
+ {Mint: 34567, Maxt: 45678},
+ }
+ for _, itv := range itvs {
+ expTombstones[ref].Add(itv)
+ }
+ head.tombstones.AddInterval(ref, itvs...)
+ err := head.wal.Log(enc.Tombstones([]tombstones.Stone{
+ {Ref: ref, Intervals: itvs},
+ }, nil))
+ require.NoError(t, err)
+ }
+ }
+ {
+ // Close Head and verify that new snapshot was not created.
+ head.opts.EnableMemorySnapshotOnShutdown = false
+ closeHeadAndCheckSnapshot() // This should not create a snapshot.
+
+ // Test the replay of snapshot, m-map chunks, and WAL.
+ head.opts.EnableMemorySnapshotOnShutdown = true // Enabled to read from snapshot.
+ openHeadAndCheckReplay()
+ }
+
+ // Creating another snapshot should delete the older snapshot and replay still works fine.
+ wlast, woffset, err = head.wal.LastSegmentAndOffset()
+ require.NoError(t, err)
+ if woffset != 0 && woffset < 32*1024 {
+ // The page is always filled before taking the snapshot.
+ woffset = 32 * 1024
+ }
+
+ {
+ // Close Head and verify that new snapshot was created.
+ closeHeadAndCheckSnapshot()
+
+ // Verify that there is only 1 snapshot.
+ files, err := os.ReadDir(head.opts.ChunkDirRoot)
+ require.NoError(t, err)
+ snapshots := 0
+ for i := len(files) - 1; i >= 0; i-- {
+ fi := files[i]
+ if strings.HasPrefix(fi.Name(), chunkSnapshotPrefix) {
+ snapshots++
+ require.Equal(t, chunkSnapshotDir(wlast, woffset), fi.Name())
+ }
+ }
+ require.Equal(t, 1, snapshots)
+
+ // Test the replay of snapshot.
+ head.opts.EnableMemorySnapshotOnShutdown = true // Enabled to read from snapshot.
+
+ // Disabling exemplars to check that it does not hard fail replay
+ // https://github.com/prometheus/prometheus/issues/9437#issuecomment-933285870.
+ head.opts.EnableExemplarStorage = false
+ head.opts.MaxExemplars.Store(0)
+ expExemplars = expExemplars[:0]
+
+ openHeadAndCheckReplay()
+
+ require.Equal(t, 0.0, prom_testutil.ToFloat64(head.metrics.snapshotReplayErrorTotal))
+ }
+}
+
+func TestSnapshotError_AppenderV2(t *testing.T) {
+ head, _ := newTestHead(t, 120*4, compression.None, false)
+ defer func() {
+ head.opts.EnableMemorySnapshotOnShutdown = false
+ require.NoError(t, head.Close())
+ }()
+
+ // Add a sample.
+ app := head.AppenderV2(context.Background())
+ lbls := labels.FromStrings("foo", "bar")
+ _, err := app.Append(0, lbls, 0, 99, 99, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+
+ // Add histograms
+ hist := tsdbutil.GenerateTestGaugeHistograms(1)[0]
+ floatHist := tsdbutil.GenerateTestGaugeFloatHistograms(1)[0]
+ lblsHist := labels.FromStrings("hist", "bar")
+ lblsFloatHist := labels.FromStrings("floathist", "bar")
+
+ _, err = app.Append(0, lblsHist, 0, 99, 0, hist, nil, storage.AOptions{})
+ require.NoError(t, err)
+
+ _, err = app.Append(0, lblsFloatHist, 0, 99, 0, nil, floatHist, storage.AOptions{})
+ require.NoError(t, err)
+
+ require.NoError(t, app.Commit())
+
+ // Add some tombstones.
+ itvs := tombstones.Intervals{
+ {Mint: 1234, Maxt: 2345},
+ {Mint: 3456, Maxt: 4567},
+ }
+ head.tombstones.AddInterval(1, itvs...)
+
+ // Check existence of data.
+ require.NotNil(t, head.series.getByHash(lbls.Hash(), lbls))
+ tm, err := head.tombstones.Get(1)
+ require.NoError(t, err)
+ require.NotEmpty(t, tm)
+
+ head.opts.EnableMemorySnapshotOnShutdown = true
+ require.NoError(t, head.Close()) // This will create a snapshot.
+
+ // Remove the WAL so that we don't load from it.
+ require.NoError(t, os.RemoveAll(head.wal.Dir()))
+
+ // Corrupt the snapshot.
+ snapDir, _, _, err := LastChunkSnapshot(head.opts.ChunkDirRoot)
+ require.NoError(t, err)
+ files, err := os.ReadDir(snapDir)
+ require.NoError(t, err)
+ f, err := os.OpenFile(path.Join(snapDir, files[0].Name()), os.O_RDWR, 0)
+ require.NoError(t, err)
+ // Create snapshot backup to be restored on future test cases.
+ snapshotBackup, err := io.ReadAll(f)
+ require.NoError(t, err)
+ _, err = f.WriteAt([]byte{0b11111111}, 18)
+ require.NoError(t, err)
+ require.NoError(t, f.Close())
+
+ // Create new Head which should replay this snapshot.
+ w, err := wlog.NewSize(nil, nil, head.wal.Dir(), 32768, compression.None)
+ require.NoError(t, err)
+ // Testing https://github.com/prometheus/prometheus/issues/9437 with the registry.
+ head, err = NewHead(prometheus.NewRegistry(), nil, w, nil, head.opts, nil)
+ require.NoError(t, err)
+ require.NoError(t, head.Init(math.MinInt64))
+
+ // There should be no series in the memory after snapshot error since WAL was removed.
+ require.Equal(t, 1.0, prom_testutil.ToFloat64(head.metrics.snapshotReplayErrorTotal))
+ require.Equal(t, uint64(0), head.NumSeries())
+ require.Nil(t, head.series.getByHash(lbls.Hash(), lbls))
+ tm, err = head.tombstones.Get(1)
+ require.NoError(t, err)
+ require.Empty(t, tm)
+ require.NoError(t, head.Close())
+
+ // Test corruption in the middle of the snapshot.
+ f, err = os.OpenFile(path.Join(snapDir, files[0].Name()), os.O_RDWR, 0)
+ require.NoError(t, err)
+ _, err = f.WriteAt(snapshotBackup, 0)
+ require.NoError(t, err)
+ _, err = f.WriteAt([]byte{0b11111111}, 300)
+ require.NoError(t, err)
+ require.NoError(t, f.Close())
+
+ c := &countSeriesLifecycleCallback{}
+ opts := head.opts
+ opts.SeriesCallback = c
+
+ w, err = wlog.NewSize(nil, nil, head.wal.Dir(), 32768, compression.None)
+ require.NoError(t, err)
+ head, err = NewHead(prometheus.NewRegistry(), nil, w, nil, head.opts, nil)
+ require.NoError(t, err)
+ require.NoError(t, head.Init(math.MinInt64))
+
+ // There should be no series in the memory after snapshot error since WAL was removed.
+ require.Equal(t, 1.0, prom_testutil.ToFloat64(head.metrics.snapshotReplayErrorTotal))
+ require.Nil(t, head.series.getByHash(lbls.Hash(), lbls))
+ require.Equal(t, uint64(0), head.NumSeries())
+
+ // Since the snapshot could replay certain series, we continue invoking the create hooks.
+ // In such instances, we need to ensure that we also trigger the delete hooks when resetting the memory.
+ require.Equal(t, int64(2), c.created.Load())
+ require.Equal(t, int64(2), c.deleted.Load())
+
+ require.Equal(t, 2.0, prom_testutil.ToFloat64(head.metrics.seriesRemoved))
+ require.Equal(t, 2.0, prom_testutil.ToFloat64(head.metrics.seriesCreated))
+}
+
+func TestHeadAppenderV2_Append_HistogramSamplesAppendedMetric(t *testing.T) {
+ numHistograms := 10
+ head, _ := newTestHead(t, 1000, compression.None, false)
+ t.Cleanup(func() {
+ require.NoError(t, head.Close())
+ })
+ require.NoError(t, head.Init(0))
+
+ expHSeries, expHSamples := 0, 0
+
+ for x := range 5 {
+ expHSeries++
+ l := labels.FromStrings("a", fmt.Sprintf("b%d", x))
+ for i, h := range tsdbutil.GenerateTestHistograms(numHistograms) {
+ app := head.AppenderV2(context.Background())
+ _, err := app.Append(0, l, 0, int64(i), 0, h, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ expHSamples++
+ }
+ for i, fh := range tsdbutil.GenerateTestFloatHistograms(numHistograms) {
+ app := head.AppenderV2(context.Background())
+ _, err := app.Append(0, l, 0, int64(numHistograms+i), 0, nil, fh, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ expHSamples++
+ }
+ }
+
+ require.Equal(t, float64(expHSamples), prom_testutil.ToFloat64(head.metrics.samplesAppended.WithLabelValues(sampleMetricTypeHistogram)))
+
+ require.NoError(t, head.Close())
+ w, err := wlog.NewSize(nil, nil, head.wal.Dir(), 32768, compression.None)
+ require.NoError(t, err)
+ head, err = NewHead(nil, nil, w, nil, head.opts, nil)
+ require.NoError(t, err)
+ require.NoError(t, head.Init(0))
+
+ require.Equal(t, float64(0), prom_testutil.ToFloat64(head.metrics.samplesAppended.WithLabelValues(sampleMetricTypeHistogram))) // Counter reset.
+}
+
+func TestHeadAppenderV2_Append_StaleHistogram(t *testing.T) {
+ t.Run("integer histogram", func(t *testing.T) {
+ testHeadAppenderV2AppendStaleHistogram(t, false)
+ })
+ t.Run("float histogram", func(t *testing.T) {
+ testHeadAppenderV2AppendStaleHistogram(t, true)
+ })
+}
+
+func testHeadAppenderV2AppendStaleHistogram(t *testing.T, floatHistogram bool) {
+ t.Helper()
+ l := labels.FromStrings("a", "b")
+ numHistograms := 20
+ head, _ := newTestHead(t, 100000, compression.None, false)
+ t.Cleanup(func() {
+ require.NoError(t, head.Close())
+ })
+ require.NoError(t, head.Init(0))
+
+ type timedHistogram struct {
+ t int64
+ h *histogram.Histogram
+ fh *histogram.FloatHistogram
+ }
+ expHistograms := make([]timedHistogram, 0, numHistograms)
+
+ testQuery := func(numStale int) {
+ q, err := NewBlockQuerier(head, head.MinTime(), head.MaxTime())
+ require.NoError(t, err)
+ t.Cleanup(func() {
+ require.NoError(t, q.Close())
+ })
+
+ ss := q.Select(context.Background(), false, nil, labels.MustNewMatcher(labels.MatchEqual, "a", "b"))
+
+ require.True(t, ss.Next())
+ s := ss.At()
+ require.False(t, ss.Next())
+
+ it := s.Iterator(nil)
+ actHistograms := make([]timedHistogram, 0, len(expHistograms))
+ for typ := it.Next(); typ != chunkenc.ValNone; typ = it.Next() {
+ switch typ {
+ case chunkenc.ValHistogram:
+ t, h := it.AtHistogram(nil)
+ actHistograms = append(actHistograms, timedHistogram{t: t, h: h})
+ case chunkenc.ValFloatHistogram:
+ t, h := it.AtFloatHistogram(nil)
+ actHistograms = append(actHistograms, timedHistogram{t: t, fh: h})
+ }
+ }
+
+ // We cannot compare StaleNAN with require.Equal, hence checking each histogram manually.
+ require.Len(t, actHistograms, len(expHistograms))
+ actNumStale := 0
+ for i, eh := range expHistograms {
+ ah := actHistograms[i]
+ if floatHistogram {
+ switch {
+ case value.IsStaleNaN(eh.fh.Sum):
+ actNumStale++
+ require.True(t, value.IsStaleNaN(ah.fh.Sum))
+ // To make require.Equal work.
+ ah.fh.Sum = 0
+ eh.fh = eh.fh.Copy()
+ eh.fh.Sum = 0
+ case i > 0:
+ prev := expHistograms[i-1]
+ if prev.fh == nil || value.IsStaleNaN(prev.fh.Sum) {
+ eh.fh.CounterResetHint = histogram.UnknownCounterReset
+ }
+ }
+ require.Equal(t, eh, ah)
+ } else {
+ switch {
+ case value.IsStaleNaN(eh.h.Sum):
+ actNumStale++
+ require.True(t, value.IsStaleNaN(ah.h.Sum))
+ // To make require.Equal work.
+ ah.h.Sum = 0
+ eh.h = eh.h.Copy()
+ eh.h.Sum = 0
+ case i > 0:
+ prev := expHistograms[i-1]
+ if prev.h == nil || value.IsStaleNaN(prev.h.Sum) {
+ eh.h.CounterResetHint = histogram.UnknownCounterReset
+ }
+ }
+ require.Equal(t, eh, ah)
+ }
+ }
+ require.Equal(t, numStale, actNumStale)
+ }
+
+ // Adding stale in the same appender.
+ app := head.AppenderV2(context.Background())
+ for _, h := range tsdbutil.GenerateTestHistograms(numHistograms) {
+ var err error
+ if floatHistogram {
+ _, err = app.Append(0, l, 0, 100*int64(len(expHistograms)), 0, nil, h.ToFloat(nil), storage.AOptions{})
+ expHistograms = append(expHistograms, timedHistogram{t: 100 * int64(len(expHistograms)), fh: h.ToFloat(nil)})
+ } else {
+ _, err = app.Append(0, l, 0, 100*int64(len(expHistograms)), 0, h, nil, storage.AOptions{})
+ expHistograms = append(expHistograms, timedHistogram{t: 100 * int64(len(expHistograms)), h: h})
+ }
+ require.NoError(t, err)
+ }
+ // +1 so that delta-of-delta is not 0.
+ _, err := app.Append(0, l, 0, 100*int64(len(expHistograms))+1, math.Float64frombits(value.StaleNaN), nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ if floatHistogram {
+ expHistograms = append(expHistograms, timedHistogram{t: 100*int64(len(expHistograms)) + 1, fh: &histogram.FloatHistogram{Sum: math.Float64frombits(value.StaleNaN)}})
+ } else {
+ expHistograms = append(expHistograms, timedHistogram{t: 100*int64(len(expHistograms)) + 1, h: &histogram.Histogram{Sum: math.Float64frombits(value.StaleNaN)}})
+ }
+ require.NoError(t, app.Commit())
+
+ // Only 1 chunk in the memory, no m-mapped chunk.
+ s := head.series.getByHash(l.Hash(), l)
+ require.NotNil(t, s)
+ require.NotNil(t, s.headChunks)
+ require.Equal(t, 1, s.headChunks.len())
+ require.Empty(t, s.mmappedChunks)
+ testQuery(1)
+
+ // Adding stale in different appender and continuing series after a stale sample.
+ app = head.AppenderV2(context.Background())
+ for _, h := range tsdbutil.GenerateTestHistograms(2 * numHistograms)[numHistograms:] {
+ var err error
+ if floatHistogram {
+ _, err = app.Append(0, l, 0, 100*int64(len(expHistograms)), 0, nil, h.ToFloat(nil), storage.AOptions{})
+ expHistograms = append(expHistograms, timedHistogram{t: 100 * int64(len(expHistograms)), fh: h.ToFloat(nil)})
+ } else {
+ _, err = app.Append(0, l, 0, 100*int64(len(expHistograms)), 0, h, nil, storage.AOptions{})
+ expHistograms = append(expHistograms, timedHistogram{t: 100 * int64(len(expHistograms)), h: h})
+ }
+ require.NoError(t, err)
+ }
+ require.NoError(t, app.Commit())
+
+ app = head.AppenderV2(context.Background())
+ // +1 so that delta-of-delta is not 0.
+ _, err = app.Append(0, l, 0, 100*int64(len(expHistograms))+1, math.Float64frombits(value.StaleNaN), nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ if floatHistogram {
+ expHistograms = append(expHistograms, timedHistogram{t: 100*int64(len(expHistograms)) + 1, fh: &histogram.FloatHistogram{Sum: math.Float64frombits(value.StaleNaN)}})
+ } else {
+ expHistograms = append(expHistograms, timedHistogram{t: 100*int64(len(expHistograms)) + 1, h: &histogram.Histogram{Sum: math.Float64frombits(value.StaleNaN)}})
+ }
+ require.NoError(t, app.Commit())
+ head.mmapHeadChunks()
+
+ // Total 2 chunks, 1 m-mapped.
+ s = head.series.getByHash(l.Hash(), l)
+ require.NotNil(t, s)
+ require.NotNil(t, s.headChunks)
+ require.Equal(t, 1, s.headChunks.len())
+ require.Len(t, s.mmappedChunks, 1)
+ testQuery(2)
+}
+
+func TestHeadAppenderV2_Append_CounterResetHeader(t *testing.T) {
+ for _, floatHisto := range []bool{true} { // FIXME
+ t.Run(fmt.Sprintf("floatHistogram=%t", floatHisto), func(t *testing.T) {
+ l := labels.FromStrings("a", "b")
+ head, _ := newTestHead(t, 1000, compression.None, false)
+ t.Cleanup(func() {
+ require.NoError(t, head.Close())
+ })
+ require.NoError(t, head.Init(0))
+
+ ts := int64(0)
+ appendHistogram := func(h *histogram.Histogram) {
+ ts++
+ app := head.AppenderV2(context.Background())
+ var err error
+ if floatHisto {
+ _, err = app.Append(0, l, 0, ts, 0, nil, h.ToFloat(nil), storage.AOptions{})
+ } else {
+ _, err = app.Append(0, l, 0, ts, 0, h.Copy(), nil, storage.AOptions{})
+ }
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ }
+
+ var expHeaders []chunkenc.CounterResetHeader
+ checkExpCounterResetHeader := func(newHeaders ...chunkenc.CounterResetHeader) {
+ expHeaders = append(expHeaders, newHeaders...)
+
+ ms, _, err := head.getOrCreate(l.Hash(), l, false)
+ require.NoError(t, err)
+ ms.mmapChunks(head.chunkDiskMapper)
+ require.Len(t, ms.mmappedChunks, len(expHeaders)-1) // One is the head chunk.
+
+ for i, mmapChunk := range ms.mmappedChunks {
+ chk, err := head.chunkDiskMapper.Chunk(mmapChunk.ref)
+ require.NoError(t, err)
+ if floatHisto {
+ require.Equal(t, expHeaders[i], chk.(*chunkenc.FloatHistogramChunk).GetCounterResetHeader())
+ } else {
+ require.Equal(t, expHeaders[i], chk.(*chunkenc.HistogramChunk).GetCounterResetHeader())
+ }
+ }
+ if floatHisto {
+ require.Equal(t, expHeaders[len(expHeaders)-1], ms.headChunks.chunk.(*chunkenc.FloatHistogramChunk).GetCounterResetHeader())
+ } else {
+ require.Equal(t, expHeaders[len(expHeaders)-1], ms.headChunks.chunk.(*chunkenc.HistogramChunk).GetCounterResetHeader())
+ }
+ }
+
+ h := tsdbutil.GenerateTestHistograms(1)[0]
+ h.PositiveBuckets = []int64{100, 1, 1, 1}
+ h.NegativeBuckets = []int64{100, 1, 1, 1}
+ h.Count = 1000
+
+ // First histogram is UnknownCounterReset.
+ appendHistogram(h)
+ checkExpCounterResetHeader(chunkenc.UnknownCounterReset)
+
+ // Another normal histogram.
+ h.Count++
+ appendHistogram(h)
+ checkExpCounterResetHeader()
+
+ // Counter reset via Count.
+ h.Count--
+ appendHistogram(h)
+ checkExpCounterResetHeader(chunkenc.CounterReset)
+
+ // Add 2 non-counter reset histogram chunks (each chunk targets 1024 bytes which contains ~500 int histogram
+ // samples or ~1000 float histogram samples).
+ numAppend := 2000
+ if floatHisto {
+ numAppend = 1000
+ }
+ for i := 0; i < numAppend; i++ {
+ appendHistogram(h)
+ }
+
+ checkExpCounterResetHeader(chunkenc.NotCounterReset, chunkenc.NotCounterReset)
+
+ // Changing schema will cut a new chunk with unknown counter reset.
+ h.Schema++
+ appendHistogram(h)
+ checkExpCounterResetHeader(chunkenc.UnknownCounterReset)
+
+ // Changing schema will zero threshold a new chunk with unknown counter reset.
+ h.ZeroThreshold += 0.01
+ appendHistogram(h)
+ checkExpCounterResetHeader(chunkenc.UnknownCounterReset)
+
+ // Counter reset by removing a positive bucket.
+ h.PositiveSpans[1].Length--
+ h.PositiveBuckets = h.PositiveBuckets[1:]
+ appendHistogram(h)
+ checkExpCounterResetHeader(chunkenc.CounterReset)
+
+ // Counter reset by removing a negative bucket.
+ h.NegativeSpans[1].Length--
+ h.NegativeBuckets = h.NegativeBuckets[1:]
+ appendHistogram(h)
+ checkExpCounterResetHeader(chunkenc.CounterReset)
+
+ // Add 2 non-counter reset histogram chunks. Just to have some non-counter reset chunks in between.
+ for range 2000 {
+ appendHistogram(h)
+ }
+ checkExpCounterResetHeader(chunkenc.NotCounterReset, chunkenc.NotCounterReset)
+
+ // Counter reset with counter reset in a positive bucket.
+ h.PositiveBuckets[len(h.PositiveBuckets)-1]--
+ appendHistogram(h)
+ checkExpCounterResetHeader(chunkenc.CounterReset)
+
+ // Counter reset with counter reset in a negative bucket.
+ h.NegativeBuckets[len(h.NegativeBuckets)-1]--
+ appendHistogram(h)
+ checkExpCounterResetHeader(chunkenc.CounterReset)
+ })
+ }
+}
+
+func TestHeadAppenderV2_Append_OOOHistogramCounterResetHeaders(t *testing.T) {
+ for _, floatHisto := range []bool{true, false} {
+ t.Run(fmt.Sprintf("floatHistogram=%t", floatHisto), func(t *testing.T) {
+ l := labels.FromStrings("a", "b")
+ head, _ := newTestHead(t, 1000, compression.None, true)
+ head.opts.OutOfOrderCapMax.Store(5)
+
+ t.Cleanup(func() {
+ require.NoError(t, head.Close())
+ })
+ require.NoError(t, head.Init(0))
+
+ appendHistogram := func(ts int64, h *histogram.Histogram) {
+ app := head.AppenderV2(context.Background())
+ var err error
+ if floatHisto {
+ _, err = app.Append(0, l, 0, ts, 0, nil, h.ToFloat(nil), storage.AOptions{})
+ } else {
+ _, err = app.Append(0, l, 0, ts, 0, h.Copy(), nil, storage.AOptions{})
+ }
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ }
+
+ type expOOOMmappedChunks struct {
+ header chunkenc.CounterResetHeader
+ mint, maxt int64
+ numSamples uint16
+ }
+
+ var expChunks []expOOOMmappedChunks
+ checkOOOExpCounterResetHeader := func(newChunks ...expOOOMmappedChunks) {
+ expChunks = append(expChunks, newChunks...)
+
+ ms, _, err := head.getOrCreate(l.Hash(), l, false)
+ require.NoError(t, err)
+
+ require.Len(t, ms.ooo.oooMmappedChunks, len(expChunks))
+
+ for i, mmapChunk := range ms.ooo.oooMmappedChunks {
+ chk, err := head.chunkDiskMapper.Chunk(mmapChunk.ref)
+ require.NoError(t, err)
+ if floatHisto {
+ require.Equal(t, expChunks[i].header, chk.(*chunkenc.FloatHistogramChunk).GetCounterResetHeader())
+ } else {
+ require.Equal(t, expChunks[i].header, chk.(*chunkenc.HistogramChunk).GetCounterResetHeader())
+ }
+ require.Equal(t, expChunks[i].mint, mmapChunk.minTime)
+ require.Equal(t, expChunks[i].maxt, mmapChunk.maxTime)
+ require.Equal(t, expChunks[i].numSamples, mmapChunk.numSamples)
+ }
+ }
+
+ // Append an in-order histogram, so the rest of the samples can be detected as OOO.
+ appendHistogram(1000, tsdbutil.GenerateTestHistogram(1000))
+
+ // OOO histogram
+ for i := 1; i <= 5; i++ {
+ appendHistogram(100+int64(i), tsdbutil.GenerateTestHistogram(1000+int64(i)))
+ }
+ // Nothing mmapped yet.
+ checkOOOExpCounterResetHeader()
+
+ // 6th observation (which triggers a head chunk mmapping).
+ appendHistogram(int64(112), tsdbutil.GenerateTestHistogram(1002))
+
+ // One mmapped chunk with (ts, val) [(101, 1001), (102, 1002), (103, 1003), (104, 1004), (105, 1005)].
+ checkOOOExpCounterResetHeader(expOOOMmappedChunks{
+ header: chunkenc.UnknownCounterReset,
+ mint: 101,
+ maxt: 105,
+ numSamples: 5,
+ })
+
+ // Add more samples, there's a counter reset at ts 122.
+ appendHistogram(int64(110), tsdbutil.GenerateTestHistogram(1001))
+ appendHistogram(int64(124), tsdbutil.GenerateTestHistogram(904))
+ appendHistogram(int64(123), tsdbutil.GenerateTestHistogram(903))
+ appendHistogram(int64(122), tsdbutil.GenerateTestHistogram(902))
+
+ // New samples not mmapped yet.
+ checkOOOExpCounterResetHeader()
+
+ // 11th observation (which triggers another head chunk mmapping).
+ appendHistogram(int64(200), tsdbutil.GenerateTestHistogram(2000))
+
+ // Two new mmapped chunks [(110, 1001), (112, 1002)], [(122, 902), (123, 903), (124, 904)].
+ checkOOOExpCounterResetHeader(
+ expOOOMmappedChunks{
+ header: chunkenc.UnknownCounterReset,
+ mint: 110,
+ maxt: 112,
+ numSamples: 2,
+ },
+ expOOOMmappedChunks{
+ header: chunkenc.CounterReset,
+ mint: 122,
+ maxt: 124,
+ numSamples: 3,
+ },
+ )
+
+ // Count is lower than previous sample at ts 200, and NotCounterReset is always ignored on append.
+ appendHistogram(int64(205), tsdbutil.SetHistogramNotCounterReset(tsdbutil.GenerateTestHistogram(1000)))
+
+ appendHistogram(int64(210), tsdbutil.SetHistogramCounterReset(tsdbutil.GenerateTestHistogram(2010)))
+
+ appendHistogram(int64(220), tsdbutil.GenerateTestHistogram(2020))
+
+ appendHistogram(int64(215), tsdbutil.GenerateTestHistogram(2005))
+
+ // 16th observation (which triggers another head chunk mmapping).
+ appendHistogram(int64(350), tsdbutil.GenerateTestHistogram(4000))
+
+ // Four new mmapped chunks: [(200, 2000)] [(205, 1000)], [(210, 2010)], [(215, 2015), (220, 2020)]
+ checkOOOExpCounterResetHeader(
+ expOOOMmappedChunks{
+ header: chunkenc.UnknownCounterReset,
+ mint: 200,
+ maxt: 200,
+ numSamples: 1,
+ },
+ expOOOMmappedChunks{
+ header: chunkenc.CounterReset,
+ mint: 205,
+ maxt: 205,
+ numSamples: 1,
+ },
+ expOOOMmappedChunks{
+ header: chunkenc.CounterReset,
+ mint: 210,
+ maxt: 210,
+ numSamples: 1,
+ },
+ expOOOMmappedChunks{
+ header: chunkenc.CounterReset,
+ mint: 215,
+ maxt: 220,
+ numSamples: 2,
+ },
+ )
+
+ // Adding five more samples (21 in total), so another mmapped chunk is created.
+ appendHistogram(300, tsdbutil.SetHistogramCounterReset(tsdbutil.GenerateTestHistogram(3000)))
+
+ for i := 1; i <= 4; i++ {
+ appendHistogram(300+int64(i), tsdbutil.GenerateTestHistogram(3000+int64(i)))
+ }
+
+ // One mmapped chunk with (ts, val) [(300, 3000), (301, 3001), (302, 3002), (303, 3003), (350, 4000)].
+ checkOOOExpCounterResetHeader(expOOOMmappedChunks{
+ header: chunkenc.CounterReset,
+ mint: 300,
+ maxt: 350,
+ numSamples: 5,
+ })
+ })
+ }
+}
+
+func TestHeadAppenderV2_Append_DifferentEncodingSameSeries(t *testing.T) {
+ dir := t.TempDir()
+ opts := DefaultOptions()
+ db, err := Open(dir, nil, nil, opts, nil)
+ require.NoError(t, err)
+ t.Cleanup(func() {
+ require.NoError(t, db.Close())
+ })
+ db.DisableCompactions()
+
+ hists := tsdbutil.GenerateTestHistograms(10)
+ floatHists := tsdbutil.GenerateTestFloatHistograms(10)
+ lbls := labels.FromStrings("a", "b")
+
+ var expResult []chunks.Sample
+ checkExpChunks := func(count int) {
+ ms, created, err := db.Head().getOrCreate(lbls.Hash(), lbls, false)
+ require.NoError(t, err)
+ require.False(t, created)
+ require.NotNil(t, ms)
+ require.Equal(t, count, ms.headChunks.len())
+ }
+
+ appends := []struct {
+ samples []chunks.Sample
+ expChunks int
+ err error
+ }{
+ // Histograms that end up in the expected samples are copied here so that we
+ // can independently set the CounterResetHint later.
+ {
+ samples: []chunks.Sample{sample{t: 100, h: hists[0].Copy()}},
+ expChunks: 1,
+ },
+ {
+ samples: []chunks.Sample{sample{t: 200, f: 2}},
+ expChunks: 2,
+ },
+ {
+ samples: []chunks.Sample{sample{t: 210, fh: floatHists[0].Copy()}},
+ expChunks: 3,
+ },
+ {
+ samples: []chunks.Sample{sample{t: 220, h: hists[1].Copy()}},
+ expChunks: 4,
+ },
+ {
+ samples: []chunks.Sample{sample{t: 230, fh: floatHists[3].Copy()}},
+ expChunks: 5,
+ },
+ {
+ samples: []chunks.Sample{sample{t: 100, h: hists[2].Copy()}},
+ err: storage.ErrOutOfOrderSample,
+ },
+ {
+ samples: []chunks.Sample{sample{t: 300, h: hists[3].Copy()}},
+ expChunks: 6,
+ },
+ {
+ samples: []chunks.Sample{sample{t: 100, f: 2}},
+ err: storage.ErrOutOfOrderSample,
+ },
+ {
+ samples: []chunks.Sample{sample{t: 100, fh: floatHists[4].Copy()}},
+ err: storage.ErrOutOfOrderSample,
+ },
+ // The three next tests all failed before #15177 was fixed.
+ {
+ samples: []chunks.Sample{
+ sample{t: 400, f: 4},
+ sample{t: 500, h: hists[5]},
+ sample{t: 600, f: 6},
+ },
+ expChunks: 9, // Each of the three samples above creates a new chunk because the type changes.
+ },
+ {
+ samples: []chunks.Sample{
+ sample{t: 700, h: hists[7]},
+ sample{t: 800, f: 8},
+ sample{t: 900, h: hists[9]},
+ },
+ expChunks: 12, // Again each sample creates a new chunk.
+ },
+ {
+ samples: []chunks.Sample{
+ sample{t: 1000, fh: floatHists[7]},
+ sample{t: 1100, h: hists[9]},
+ },
+ expChunks: 14, // Even changes between float and integer histogram create new chunks.
+ },
+ }
+
+ for _, a := range appends {
+ app := db.AppenderV2(context.Background())
+ for _, s := range a.samples {
+ var err error
+ if s.H() != nil || s.FH() != nil {
+ _, err = app.Append(0, lbls, 0, s.T(), 0, s.H(), s.FH(), storage.AOptions{})
+ } else {
+ _, err = app.Append(0, lbls, 0, s.T(), s.F(), nil, nil, storage.AOptions{})
+ }
+ require.Equal(t, a.err, err)
+ }
+
+ if a.err == nil {
+ require.NoError(t, app.Commit())
+ expResult = append(expResult, a.samples...)
+ checkExpChunks(a.expChunks)
+ } else {
+ require.NoError(t, app.Rollback())
+ }
+ }
+ for i, s := range expResult[1:] {
+ switch {
+ case s.H() != nil && expResult[i].H() == nil:
+ s.(sample).h.CounterResetHint = histogram.UnknownCounterReset
+ case s.FH() != nil && expResult[i].FH() == nil:
+ s.(sample).fh.CounterResetHint = histogram.UnknownCounterReset
+ }
+ }
+
+ // Query back and expect same order of samples.
+ q, err := db.Querier(math.MinInt64, math.MaxInt64)
+ require.NoError(t, err)
+
+ series := query(t, q, labels.MustNewMatcher(labels.MatchEqual, "a", "b"))
+ require.Equal(t, map[string][]chunks.Sample{lbls.String(): expResult}, series)
+}
+
+func TestChunkSnapshotTakenAfterIncompleteSnapshot_AppenderV2(t *testing.T) {
+ dir := t.TempDir()
+ wlTemp, err := wlog.NewSize(nil, nil, filepath.Join(dir, "wal"), 32768, compression.Snappy)
+ require.NoError(t, err)
+
+ // Write a snapshot with .tmp suffix. This used to fail taking any further snapshots or replay of snapshots.
+ snapshotName := chunkSnapshotDir(0, 100) + ".tmp"
+ cpdir := filepath.Join(dir, snapshotName)
+ require.NoError(t, os.MkdirAll(cpdir, 0o777))
+
+ opts := DefaultHeadOptions()
+ opts.ChunkDirRoot = dir
+ opts.EnableMemorySnapshotOnShutdown = true
+ head, err := NewHead(nil, nil, wlTemp, nil, opts, nil)
+ require.NoError(t, err)
+ require.NoError(t, head.Init(math.MinInt64))
+
+ require.Equal(t, 0.0, prom_testutil.ToFloat64(head.metrics.snapshotReplayErrorTotal))
+
+ // Add some samples for the snapshot.
+ app := head.AppenderV2(context.Background())
+ _, err = app.Append(0, labels.FromStrings("foo", "bar"), 0, 10, 10, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+
+ // Should not return any error for a successful snapshot.
+ require.NoError(t, head.Close())
+
+ // Verify the snapshot.
+ name, idx, offset, err := LastChunkSnapshot(dir)
+ require.NoError(t, err)
+ require.NotEmpty(t, name)
+ require.Equal(t, 0, idx)
+ require.Positive(t, offset)
+}
+
+// TestWBLReplay checks the replay at a low level.
+func TestWBLReplay_AppenderV2(t *testing.T) {
+ for name, scenario := range sampleTypeScenarios {
+ t.Run(name, func(t *testing.T) {
+ testWBLReplayAppenderV2(t, scenario)
+ })
+ }
+}
+
+func testWBLReplayAppenderV2(t *testing.T, scenario sampleTypeScenario) {
+ dir := t.TempDir()
+ wal, err := wlog.NewSize(nil, nil, filepath.Join(dir, "wal"), 32768, compression.Snappy)
+ require.NoError(t, err)
+ oooWlog, err := wlog.NewSize(nil, nil, filepath.Join(dir, wlog.WblDirName), 32768, compression.Snappy)
+ require.NoError(t, err)
+
+ opts := DefaultHeadOptions()
+ opts.ChunkRange = 1000
+ opts.ChunkDirRoot = dir
+ opts.OutOfOrderTimeWindow.Store(30 * time.Minute.Milliseconds())
+
+ h, err := NewHead(nil, nil, wal, oooWlog, opts, nil)
+ require.NoError(t, err)
+ require.NoError(t, h.Init(0))
+
+ var expOOOSamples []chunks.Sample
+ l := labels.FromStrings("foo", "bar")
+ appendSample := func(mins int64, _ float64, isOOO bool) {
+ app := h.AppenderV2(context.Background())
+ _, s, err := scenario.appendFunc(storage.AppenderV2AsLimitedV1(app), l, mins*time.Minute.Milliseconds(), mins)
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+
+ if isOOO {
+ expOOOSamples = append(expOOOSamples, s)
+ }
+ }
+
+ // In-order sample.
+ appendSample(60, 60, false)
+
+ // Out of order samples.
+ appendSample(40, 40, true)
+ appendSample(35, 35, true)
+ appendSample(50, 50, true)
+ appendSample(55, 55, true)
+ appendSample(59, 59, true)
+ appendSample(31, 31, true)
+
+ // Check that Head's time ranges are set properly.
+ require.Equal(t, 60*time.Minute.Milliseconds(), h.MinTime())
+ require.Equal(t, 60*time.Minute.Milliseconds(), h.MaxTime())
+ require.Equal(t, 31*time.Minute.Milliseconds(), h.MinOOOTime())
+ require.Equal(t, 59*time.Minute.Milliseconds(), h.MaxOOOTime())
+
+ // Restart head.
+ require.NoError(t, h.Close())
+ wal, err = wlog.NewSize(nil, nil, filepath.Join(dir, "wal"), 32768, compression.Snappy)
+ require.NoError(t, err)
+ oooWlog, err = wlog.NewSize(nil, nil, filepath.Join(dir, wlog.WblDirName), 32768, compression.Snappy)
+ require.NoError(t, err)
+ h, err = NewHead(nil, nil, wal, oooWlog, opts, nil)
+ require.NoError(t, err)
+ require.NoError(t, h.Init(0)) // Replay happens here.
+
+ // Get the ooo samples from the Head.
+ ms, ok, err := h.getOrCreate(l.Hash(), l, false)
+ require.NoError(t, err)
+ require.False(t, ok)
+ require.NotNil(t, ms)
+
+ chks, err := ms.ooo.oooHeadChunk.chunk.ToEncodedChunks(math.MinInt64, math.MaxInt64)
+ require.NoError(t, err)
+ require.Len(t, chks, 1)
+
+ it := chks[0].chunk.Iterator(nil)
+ actOOOSamples, err := storage.ExpandSamples(it, nil)
+ require.NoError(t, err)
+
+ // OOO chunk will be sorted. Hence sort the expected samples.
+ sort.Slice(expOOOSamples, func(i, j int) bool {
+ return expOOOSamples[i].T() < expOOOSamples[j].T()
+ })
+
+ // Passing in true for the 'ignoreCounterResets' parameter prevents differences in counter reset headers
+ // from being factored in to the sample comparison
+ // TODO(fionaliao): understand counter reset behaviour, might want to modify this later
+ requireEqualSamples(t, l.String(), expOOOSamples, actOOOSamples, requireEqualSamplesIgnoreCounterResets)
+
+ require.NoError(t, h.Close())
+}
+
+// TestOOOMmapReplay checks the replay at a low level.
+func TestOOOMmapReplay_AppenderV2(t *testing.T) {
+ for name, scenario := range sampleTypeScenarios {
+ t.Run(name, func(t *testing.T) {
+ testOOOMmapReplayAppenderV2(t, scenario)
+ })
+ }
+}
+
+func testOOOMmapReplayAppenderV2(t *testing.T, scenario sampleTypeScenario) {
+ dir := t.TempDir()
+ wal, err := wlog.NewSize(nil, nil, filepath.Join(dir, "wal"), 32768, compression.Snappy)
+ require.NoError(t, err)
+ oooWlog, err := wlog.NewSize(nil, nil, filepath.Join(dir, wlog.WblDirName), 32768, compression.Snappy)
+ require.NoError(t, err)
+
+ opts := DefaultHeadOptions()
+ opts.ChunkRange = 1000
+ opts.ChunkDirRoot = dir
+ opts.OutOfOrderCapMax.Store(30)
+ opts.OutOfOrderTimeWindow.Store(1000 * time.Minute.Milliseconds())
+
+ h, err := NewHead(nil, nil, wal, oooWlog, opts, nil)
+ require.NoError(t, err)
+ require.NoError(t, h.Init(0))
+
+ l := labels.FromStrings("foo", "bar")
+ appendSample := func(mins int64) {
+ app := h.AppenderV2(context.Background())
+ _, _, err := scenario.appendFunc(storage.AppenderV2AsLimitedV1(app), l, mins*time.Minute.Milliseconds(), mins)
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ }
+
+ // In-order sample.
+ appendSample(200)
+
+ // Out of order samples. 92 samples to create 3 m-map chunks.
+ for mins := int64(100); mins <= 191; mins++ {
+ appendSample(mins)
+ }
+
+ ms, ok, err := h.getOrCreate(l.Hash(), l, false)
+ require.NoError(t, err)
+ require.False(t, ok)
+ require.NotNil(t, ms)
+
+ require.Len(t, ms.ooo.oooMmappedChunks, 3)
+ // Verify that we can access the chunks without error.
+ for _, m := range ms.ooo.oooMmappedChunks {
+ chk, err := h.chunkDiskMapper.Chunk(m.ref)
+ require.NoError(t, err)
+ require.Equal(t, int(m.numSamples), chk.NumSamples())
+ }
+
+ expMmapChunks := make([]*mmappedChunk, 3)
+ copy(expMmapChunks, ms.ooo.oooMmappedChunks)
+
+ // Restart head.
+ require.NoError(t, h.Close())
+
+ wal, err = wlog.NewSize(nil, nil, filepath.Join(dir, "wal"), 32768, compression.Snappy)
+ require.NoError(t, err)
+ oooWlog, err = wlog.NewSize(nil, nil, filepath.Join(dir, wlog.WblDirName), 32768, compression.Snappy)
+ require.NoError(t, err)
+ h, err = NewHead(nil, nil, wal, oooWlog, opts, nil)
+ require.NoError(t, err)
+ require.NoError(t, h.Init(0)) // Replay happens here.
+
+ // Get the mmap chunks from the Head.
+ ms, ok, err = h.getOrCreate(l.Hash(), l, false)
+ require.NoError(t, err)
+ require.False(t, ok)
+ require.NotNil(t, ms)
+
+ require.Len(t, ms.ooo.oooMmappedChunks, len(expMmapChunks))
+ // Verify that we can access the chunks without error.
+ for _, m := range ms.ooo.oooMmappedChunks {
+ chk, err := h.chunkDiskMapper.Chunk(m.ref)
+ require.NoError(t, err)
+ require.Equal(t, int(m.numSamples), chk.NumSamples())
+ }
+
+ actMmapChunks := make([]*mmappedChunk, len(expMmapChunks))
+ copy(actMmapChunks, ms.ooo.oooMmappedChunks)
+
+ require.Equal(t, expMmapChunks, actMmapChunks)
+
+ require.NoError(t, h.Close())
+}
+
+func TestHead_Init_DiscardChunksWithUnsupportedEncoding(t *testing.T) {
+ h, _ := newTestHead(t, 1000, compression.None, false)
+ defer func() {
+ require.NoError(t, h.Close())
+ }()
+
+ require.NoError(t, h.Init(0))
+
+ ctx := context.Background()
+ app := h.AppenderV2(ctx)
+ seriesLabels := labels.FromStrings("a", "1")
+ var seriesRef storage.SeriesRef
+ var err error
+ for i := range 400 {
+ seriesRef, err = app.Append(0, seriesLabels, 0, int64(i), float64(i), nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ }
+
+ require.NoError(t, app.Commit())
+ require.Greater(t, prom_testutil.ToFloat64(h.metrics.chunksCreated), 1.0)
+
+ uc := newUnsupportedChunk()
+ // Make this chunk not overlap with the previous and the next
+ h.chunkDiskMapper.WriteChunk(chunks.HeadSeriesRef(seriesRef), 500, 600, uc, false, func(err error) { require.NoError(t, err) })
+
+ app = h.AppenderV2(ctx)
+ for i := 700; i < 1200; i++ {
+ _, err := app.Append(0, seriesLabels, 0, int64(i), float64(i), nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ }
+
+ require.NoError(t, app.Commit())
+ require.Greater(t, prom_testutil.ToFloat64(h.metrics.chunksCreated), 4.0)
+
+ series, created, err := h.getOrCreate(seriesLabels.Hash(), seriesLabels, false)
+ require.NoError(t, err)
+ require.False(t, created, "should already exist")
+ require.NotNil(t, series, "should return the series we created above")
+
+ series.mmapChunks(h.chunkDiskMapper)
+ expChunks := make([]*mmappedChunk, len(series.mmappedChunks))
+ copy(expChunks, series.mmappedChunks)
+
+ require.NoError(t, h.Close())
+
+ wal, err := wlog.NewSize(nil, nil, filepath.Join(h.opts.ChunkDirRoot, "wal"), 32768, compression.None)
+ require.NoError(t, err)
+ h, err = NewHead(nil, nil, wal, nil, h.opts, nil)
+ require.NoError(t, err)
+ require.NoError(t, h.Init(0))
+
+ series, created, err = h.getOrCreate(seriesLabels.Hash(), seriesLabels, false)
+ require.NoError(t, err)
+ require.False(t, created, "should already exist")
+ require.NotNil(t, series, "should return the series we created above")
+
+ require.Equal(t, expChunks, series.mmappedChunks)
+}
+
+// Tests https://github.com/prometheus/prometheus/issues/10277.
+func TestMmapPanicAfterMmapReplayCorruption_AppenderV2(t *testing.T) {
+ dir := t.TempDir()
+ wal, err := wlog.NewSize(nil, nil, filepath.Join(dir, "wal"), 32768, compression.None)
+ require.NoError(t, err)
+
+ opts := DefaultHeadOptions()
+ opts.ChunkRange = DefaultBlockDuration
+ opts.ChunkDirRoot = dir
+ opts.EnableExemplarStorage = true
+ opts.MaxExemplars.Store(config.DefaultExemplarsConfig.MaxExemplars)
+
+ h, err := NewHead(nil, nil, wal, nil, opts, nil)
+ require.NoError(t, err)
+ require.NoError(t, h.Init(0))
+
+ lastTs := int64(0)
+ var ref storage.SeriesRef
+ lbls := labels.FromStrings("__name__", "testing", "foo", "bar")
+ addChunks := func() {
+ interval := DefaultBlockDuration / (4 * 120)
+ app := h.AppenderV2(context.Background())
+ for i := range 250 {
+ ref, err = app.Append(ref, lbls, 0, lastTs, float64(lastTs), nil, nil, storage.AOptions{})
+ lastTs += interval
+ if i%10 == 0 {
+ require.NoError(t, app.Commit())
+ app = h.AppenderV2(context.Background())
+ }
+ }
+ require.NoError(t, app.Commit())
+ }
+
+ addChunks()
+
+ require.NoError(t, h.Close())
+ wal, err = wlog.NewSize(nil, nil, filepath.Join(dir, "wal"), 32768, compression.None)
+ require.NoError(t, err)
+
+ mmapFilePath := filepath.Join(dir, "chunks_head", "000001")
+ f, err := os.OpenFile(mmapFilePath, os.O_WRONLY, 0o666)
+ require.NoError(t, err)
+ _, err = f.WriteAt([]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}, 17)
+ require.NoError(t, err)
+ require.NoError(t, f.Close())
+
+ h, err = NewHead(nil, nil, wal, nil, opts, nil)
+ require.NoError(t, err)
+ require.NoError(t, h.Init(0))
+
+ addChunks()
+
+ require.NoError(t, h.Close())
+}
+
+// Tests https://github.com/prometheus/prometheus/issues/10277.
+func TestReplayAfterMmapReplayError_AppenderV2(t *testing.T) {
+ dir := t.TempDir()
+ var h *Head
+ var err error
+
+ openHead := func() {
+ wal, err := wlog.NewSize(nil, nil, filepath.Join(dir, "wal"), 32768, compression.None)
+ require.NoError(t, err)
+
+ opts := DefaultHeadOptions()
+ opts.ChunkRange = DefaultBlockDuration
+ opts.ChunkDirRoot = dir
+ opts.EnableMemorySnapshotOnShutdown = true
+ opts.MaxExemplars.Store(config.DefaultExemplarsConfig.MaxExemplars)
+
+ h, err = NewHead(nil, nil, wal, nil, opts, nil)
+ require.NoError(t, err)
+ require.NoError(t, h.Init(0))
+ }
+
+ openHead()
+
+ itvl := int64(15 * time.Second / time.Millisecond)
+ lastTs := int64(0)
+ lbls := labels.FromStrings("__name__", "testing", "foo", "bar")
+ var expSamples []chunks.Sample
+ addSamples := func(numSamples int) {
+ app := h.AppenderV2(context.Background())
+ var ref storage.SeriesRef
+ for i := range numSamples {
+ ref, err = app.Append(ref, lbls, 0, lastTs, float64(lastTs), nil, nil, storage.AOptions{})
+ expSamples = append(expSamples, sample{t: lastTs, f: float64(lastTs)})
+ require.NoError(t, err)
+ lastTs += itvl
+ if i%10 == 0 {
+ require.NoError(t, app.Commit())
+ app = h.AppenderV2(context.Background())
+ }
+ }
+ require.NoError(t, app.Commit())
+ }
+
+ // Creating multiple m-map files.
+ for i := range 5 {
+ addSamples(250)
+ require.NoError(t, h.Close())
+ if i != 4 {
+ // Don't open head for the last iteration.
+ openHead()
+ }
+ }
+
+ files, err := os.ReadDir(filepath.Join(dir, "chunks_head"))
+ require.Len(t, files, 5)
+
+ // Corrupt a m-map file.
+ mmapFilePath := filepath.Join(dir, "chunks_head", "000002")
+ f, err := os.OpenFile(mmapFilePath, os.O_WRONLY, 0o666)
+ require.NoError(t, err)
+ _, err = f.WriteAt([]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}, 17)
+ require.NoError(t, err)
+ require.NoError(t, f.Close())
+
+ openHead()
+ h.mmapHeadChunks()
+
+ // There should be less m-map files due to corruption.
+ files, err = os.ReadDir(filepath.Join(dir, "chunks_head"))
+ require.Len(t, files, 2)
+
+ // Querying should not panic.
+ q, err := NewBlockQuerier(h, 0, lastTs)
+ require.NoError(t, err)
+ res := query(t, q, labels.MustNewMatcher(labels.MatchEqual, "__name__", "testing"))
+ require.Equal(t, map[string][]chunks.Sample{lbls.String(): expSamples}, res)
+
+ require.NoError(t, h.Close())
+}
+
+func TestHeadAppenderV2_Append_OOOWithNoSeries(t *testing.T) {
+ for name, scenario := range sampleTypeScenarios {
+ t.Run(name, func(t *testing.T) {
+ testHeadAppenderV2AppendOOOWithNoSeries(t, scenario.appendFunc)
+ })
+ }
+}
+
+func testHeadAppenderV2AppendOOOWithNoSeries(t *testing.T, appendFunc func(appender storage.LimitedAppenderV1, lbls labels.Labels, ts, value int64) (storage.SeriesRef, sample, error)) {
+ dir := t.TempDir()
+ wal, err := wlog.NewSize(nil, nil, filepath.Join(dir, "wal"), 32768, compression.Snappy)
+ require.NoError(t, err)
+ oooWlog, err := wlog.NewSize(nil, nil, filepath.Join(dir, wlog.WblDirName), 32768, compression.Snappy)
+ require.NoError(t, err)
+
+ opts := DefaultHeadOptions()
+ opts.ChunkDirRoot = dir
+ opts.OutOfOrderCapMax.Store(30)
+ opts.OutOfOrderTimeWindow.Store(120 * time.Minute.Milliseconds())
+
+ h, err := NewHead(nil, nil, wal, oooWlog, opts, nil)
+ require.NoError(t, err)
+ t.Cleanup(func() {
+ require.NoError(t, h.Close())
+ })
+ require.NoError(t, h.Init(0))
+
+ appendSample := func(lbls labels.Labels, ts int64) {
+ app := h.AppenderV2(context.Background())
+ _, _, err := appendFunc(storage.AppenderV2AsLimitedV1(app), lbls, ts*time.Minute.Milliseconds(), ts)
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ }
+
+ verifyOOOSamples := func(lbls labels.Labels, expSamples int) {
+ ms, created, err := h.getOrCreate(lbls.Hash(), lbls, false)
+ require.NoError(t, err)
+ require.False(t, created)
+ require.NotNil(t, ms)
+
+ require.Nil(t, ms.headChunks)
+ require.NotNil(t, ms.ooo.oooHeadChunk)
+ require.Equal(t, expSamples, ms.ooo.oooHeadChunk.chunk.NumSamples())
+ }
+
+ verifyInOrderSamples := func(lbls labels.Labels, expSamples int) {
+ ms, created, err := h.getOrCreate(lbls.Hash(), lbls, false)
+ require.NoError(t, err)
+ require.False(t, created)
+ require.NotNil(t, ms)
+
+ require.Nil(t, ms.ooo)
+ require.NotNil(t, ms.headChunks)
+ require.Equal(t, expSamples, ms.headChunks.chunk.NumSamples())
+ }
+
+ newLabels := func(idx int) labels.Labels { return labels.FromStrings("foo", strconv.Itoa(idx)) }
+
+ s1 := newLabels(1)
+ appendSample(s1, 300) // At 300m.
+ verifyInOrderSamples(s1, 1)
+
+ // At 239m, the sample cannot be appended to in-order chunk since it is
+ // beyond the minValidTime. So it should go in OOO chunk.
+ // Series does not exist for s2 yet.
+ s2 := newLabels(2)
+ appendSample(s2, 239) // OOO sample.
+ verifyOOOSamples(s2, 1)
+
+ // Similar for 180m.
+ s3 := newLabels(3)
+ appendSample(s3, 180) // OOO sample.
+ verifyOOOSamples(s3, 1)
+
+ // Now 179m is too old.
+ s4 := newLabels(4)
+ app := h.AppenderV2(context.Background())
+ _, _, err = appendFunc(storage.AppenderV2AsLimitedV1(app), s4, 179*time.Minute.Milliseconds(), 179)
+ require.Equal(t, storage.ErrTooOldSample, err)
+ require.NoError(t, app.Rollback())
+ verifyOOOSamples(s3, 1)
+
+ // Samples still go into in-order chunk for samples within
+ // appendable minValidTime.
+ s5 := newLabels(5)
+ appendSample(s5, 240)
+ verifyInOrderSamples(s5, 1)
+}
+
+func TestHead_MinOOOTime_Update_AppenderV2(t *testing.T) {
+ for name, scenario := range sampleTypeScenarios {
+ t.Run(name, func(t *testing.T) {
+ if scenario.sampleType == sampleMetricTypeFloat {
+ testHeadMinOOOTimeUpdateAppenderV2(t, scenario)
+ }
+ })
+ }
+}
+
+func testHeadMinOOOTimeUpdateAppenderV2(t *testing.T, scenario sampleTypeScenario) {
+ dir := t.TempDir()
+ wal, err := wlog.NewSize(nil, nil, filepath.Join(dir, "wal"), 32768, compression.Snappy)
+ require.NoError(t, err)
+ oooWlog, err := wlog.NewSize(nil, nil, filepath.Join(dir, wlog.WblDirName), 32768, compression.Snappy)
+ require.NoError(t, err)
+
+ opts := DefaultHeadOptions()
+ opts.ChunkDirRoot = dir
+ opts.OutOfOrderTimeWindow.Store(10 * time.Minute.Milliseconds())
+
+ h, err := NewHead(nil, nil, wal, oooWlog, opts, nil)
+ require.NoError(t, err)
+ t.Cleanup(func() {
+ require.NoError(t, h.Close())
+ })
+ require.NoError(t, h.Init(0))
+
+ appendSample := func(ts int64) {
+ app := h.AppenderV2(context.Background())
+ _, _, err = scenario.appendFunc(storage.AppenderV2AsLimitedV1(app), labels.FromStrings("a", "b"), ts*time.Minute.Milliseconds(), ts)
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ }
+
+ appendSample(300) // In-order sample.
+ require.Equal(t, int64(math.MaxInt64), h.MinOOOTime())
+
+ appendSample(295) // OOO sample.
+ require.Equal(t, 295*time.Minute.Milliseconds(), h.MinOOOTime())
+
+ // Allowed window for OOO is >=290, which is before the earliest ooo sample 295, so it gets set to the lower value.
+ require.NoError(t, h.truncateOOO(0, 1))
+ require.Equal(t, 290*time.Minute.Milliseconds(), h.MinOOOTime())
+
+ appendSample(310) // In-order sample.
+ appendSample(305) // OOO sample.
+ require.Equal(t, 290*time.Minute.Milliseconds(), h.MinOOOTime())
+
+ // Now the OOO sample 295 was not gc'ed yet. And allowed window for OOO is now >=300.
+ // So the lowest among them, 295, is set as minOOOTime.
+ require.NoError(t, h.truncateOOO(0, 2))
+ require.Equal(t, 295*time.Minute.Milliseconds(), h.MinOOOTime())
+}
+
+func TestGaugeHistogramWALAndChunkHeader_AppenderV2(t *testing.T) {
+ l := labels.FromStrings("a", "b")
+ head, _ := newTestHead(t, 1000, compression.None, false)
+ t.Cleanup(func() {
+ require.NoError(t, head.Close())
+ })
+ require.NoError(t, head.Init(0))
+
+ ts := int64(0)
+ appendHistogram := func(h *histogram.Histogram) {
+ ts++
+ app := head.AppenderV2(context.Background())
+ _, err := app.Append(0, l, 0, ts, 0, h.Copy(), nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ }
+
+ hists := tsdbutil.GenerateTestGaugeHistograms(5)
+ hists[0].CounterResetHint = histogram.UnknownCounterReset
+ appendHistogram(hists[0])
+ appendHistogram(hists[1])
+ appendHistogram(hists[2])
+ hists[3].CounterResetHint = histogram.UnknownCounterReset
+ appendHistogram(hists[3])
+ appendHistogram(hists[3])
+ appendHistogram(hists[4])
+
+ checkHeaders := func() {
+ head.mmapHeadChunks()
+ ms, _, err := head.getOrCreate(l.Hash(), l, false)
+ require.NoError(t, err)
+ require.Len(t, ms.mmappedChunks, 3)
+ expHeaders := []chunkenc.CounterResetHeader{
+ chunkenc.UnknownCounterReset,
+ chunkenc.GaugeType,
+ chunkenc.NotCounterReset,
+ chunkenc.GaugeType,
+ }
+ for i, mmapChunk := range ms.mmappedChunks {
+ chk, err := head.chunkDiskMapper.Chunk(mmapChunk.ref)
+ require.NoError(t, err)
+ require.Equal(t, expHeaders[i], chk.(*chunkenc.HistogramChunk).GetCounterResetHeader())
+ }
+ require.Equal(t, expHeaders[len(expHeaders)-1], ms.headChunks.chunk.(*chunkenc.HistogramChunk).GetCounterResetHeader())
+ }
+ checkHeaders()
+
+ recs := readTestWAL(t, head.wal.Dir())
+ require.Equal(t, []any{
+ []record.RefSeries{
+ {
+ Ref: 1,
+ Labels: labels.FromStrings("a", "b"),
+ },
+ },
+ []record.RefHistogramSample{{Ref: 1, T: 1, H: hists[0]}},
+ []record.RefHistogramSample{{Ref: 1, T: 2, H: hists[1]}},
+ []record.RefHistogramSample{{Ref: 1, T: 3, H: hists[2]}},
+ []record.RefHistogramSample{{Ref: 1, T: 4, H: hists[3]}},
+ []record.RefHistogramSample{{Ref: 1, T: 5, H: hists[3]}},
+ []record.RefHistogramSample{{Ref: 1, T: 6, H: hists[4]}},
+ }, recs)
+
+ // Restart Head without mmap chunks to expect the WAL replay to recognize gauge histograms.
+ require.NoError(t, head.Close())
+ require.NoError(t, os.RemoveAll(mmappedChunksDir(head.opts.ChunkDirRoot)))
+
+ w, err := wlog.NewSize(nil, nil, head.wal.Dir(), 32768, compression.None)
+ require.NoError(t, err)
+ head, err = NewHead(nil, nil, w, nil, head.opts, nil)
+ require.NoError(t, err)
+ require.NoError(t, head.Init(0))
+
+ checkHeaders()
+}
+
+func TestGaugeFloatHistogramWALAndChunkHeader_AppenderV2(t *testing.T) {
+ l := labels.FromStrings("a", "b")
+ head, _ := newTestHead(t, 1000, compression.None, false)
+ t.Cleanup(func() {
+ require.NoError(t, head.Close())
+ })
+ require.NoError(t, head.Init(0))
+
+ ts := int64(0)
+ appendHistogram := func(h *histogram.FloatHistogram) {
+ ts++
+ app := head.AppenderV2(context.Background())
+ _, err := app.Append(0, l, 0, ts, 0, nil, h.Copy(), storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ }
+
+ hists := tsdbutil.GenerateTestGaugeFloatHistograms(5)
+ hists[0].CounterResetHint = histogram.UnknownCounterReset
+ appendHistogram(hists[0])
+ appendHistogram(hists[1])
+ appendHistogram(hists[2])
+ hists[3].CounterResetHint = histogram.UnknownCounterReset
+ appendHistogram(hists[3])
+ appendHistogram(hists[3])
+ appendHistogram(hists[4])
+
+ checkHeaders := func() {
+ ms, _, err := head.getOrCreate(l.Hash(), l, false)
+ require.NoError(t, err)
+ head.mmapHeadChunks()
+ require.Len(t, ms.mmappedChunks, 3)
+ expHeaders := []chunkenc.CounterResetHeader{
+ chunkenc.UnknownCounterReset,
+ chunkenc.GaugeType,
+ chunkenc.UnknownCounterReset,
+ chunkenc.GaugeType,
+ }
+ for i, mmapChunk := range ms.mmappedChunks {
+ chk, err := head.chunkDiskMapper.Chunk(mmapChunk.ref)
+ require.NoError(t, err)
+ require.Equal(t, expHeaders[i], chk.(*chunkenc.FloatHistogramChunk).GetCounterResetHeader())
+ }
+ require.Equal(t, expHeaders[len(expHeaders)-1], ms.headChunks.chunk.(*chunkenc.FloatHistogramChunk).GetCounterResetHeader())
+ }
+ checkHeaders()
+
+ recs := readTestWAL(t, head.wal.Dir())
+ require.Equal(t, []any{
+ []record.RefSeries{
+ {
+ Ref: 1,
+ Labels: labels.FromStrings("a", "b"),
+ },
+ },
+ []record.RefFloatHistogramSample{{Ref: 1, T: 1, FH: hists[0]}},
+ []record.RefFloatHistogramSample{{Ref: 1, T: 2, FH: hists[1]}},
+ []record.RefFloatHistogramSample{{Ref: 1, T: 3, FH: hists[2]}},
+ []record.RefFloatHistogramSample{{Ref: 1, T: 4, FH: hists[3]}},
+ []record.RefFloatHistogramSample{{Ref: 1, T: 5, FH: hists[3]}},
+ []record.RefFloatHistogramSample{{Ref: 1, T: 6, FH: hists[4]}},
+ }, recs)
+
+ // Restart Head without mmap chunks to expect the WAL replay to recognize gauge histograms.
+ require.NoError(t, head.Close())
+ require.NoError(t, os.RemoveAll(mmappedChunksDir(head.opts.ChunkDirRoot)))
+
+ w, err := wlog.NewSize(nil, nil, head.wal.Dir(), 32768, compression.None)
+ require.NoError(t, err)
+ head, err = NewHead(nil, nil, w, nil, head.opts, nil)
+ require.NoError(t, err)
+ require.NoError(t, head.Init(0))
+
+ checkHeaders()
+}
+
+func TestSnapshotAheadOfWALError_AppenderV2(t *testing.T) {
+ head, _ := newTestHead(t, 120*4, compression.None, false)
+ head.opts.EnableMemorySnapshotOnShutdown = true
+ // Add a sample to fill WAL.
+ app := head.AppenderV2(context.Background())
+ _, err := app.Append(0, labels.FromStrings("foo", "bar"), 0, 10, 10, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+
+ // Increment snapshot index to create sufficiently large difference.
+ for range 2 {
+ _, err = head.wal.NextSegment()
+ require.NoError(t, err)
+ }
+ require.NoError(t, head.Close()) // This will create a snapshot.
+
+ _, idx, _, err := LastChunkSnapshot(head.opts.ChunkDirRoot)
+ require.NoError(t, err)
+ require.Equal(t, 2, idx)
+
+ // Restart the WAL while keeping the old snapshot. The new head is created manually in this case in order
+ // to keep using the same snapshot directory instead of a random one.
+ require.NoError(t, os.RemoveAll(head.wal.Dir()))
+ head.opts.EnableMemorySnapshotOnShutdown = false
+ w, _ := wlog.NewSize(nil, nil, head.wal.Dir(), 32768, compression.None)
+ head, err = NewHead(nil, nil, w, nil, head.opts, nil)
+ require.NoError(t, err)
+ // Add a sample to fill WAL.
+ app = head.AppenderV2(context.Background())
+ _, err = app.Append(0, labels.FromStrings("foo", "bar"), 0, 10, 10, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ lastSegment, _, _ := w.LastSegmentAndOffset()
+ require.Equal(t, 0, lastSegment)
+ require.NoError(t, head.Close())
+
+ // New WAL is saved, but old snapshot still exists.
+ _, idx, _, err = LastChunkSnapshot(head.opts.ChunkDirRoot)
+ require.NoError(t, err)
+ require.Equal(t, 2, idx)
+
+ // Create new Head which should detect the incorrect index and delete the snapshot.
+ head.opts.EnableMemorySnapshotOnShutdown = true
+ w, _ = wlog.NewSize(nil, nil, head.wal.Dir(), 32768, compression.None)
+ head, err = NewHead(nil, nil, w, nil, head.opts, nil)
+ require.NoError(t, err)
+ require.NoError(t, head.Init(math.MinInt64))
+
+ // Verify that snapshot directory does not exist anymore.
+ _, _, _, err = LastChunkSnapshot(head.opts.ChunkDirRoot)
+ require.Equal(t, record.ErrNotFound, err)
+
+ require.NoError(t, head.Close())
+}
+
+func TestCuttingNewHeadChunks_AppenderV2(t *testing.T) {
+ ctx := context.Background()
+ testCases := map[string]struct {
+ numTotalSamples int
+ timestampJitter bool
+ floatValFunc func(i int) float64
+ histValFunc func(i int) *histogram.Histogram
+ expectedChks []struct {
+ numSamples int
+ numBytes int
+ }
+ }{
+ "float samples": {
+ numTotalSamples: 180,
+ floatValFunc: func(int) float64 {
+ return 1.
+ },
+ expectedChks: []struct {
+ numSamples int
+ numBytes int
+ }{
+ {numSamples: 120, numBytes: 46},
+ {numSamples: 60, numBytes: 32},
+ },
+ },
+ "large float samples": {
+ // Normally 120 samples would fit into a single chunk but these chunks violate the 1005 byte soft cap.
+ numTotalSamples: 120,
+ timestampJitter: true,
+ floatValFunc: func(i int) float64 {
+ // Flipping between these two make each sample val take at least 64 bits.
+ vals := []float64{math.MaxFloat64, 0x00}
+ return vals[i%len(vals)]
+ },
+ expectedChks: []struct {
+ numSamples int
+ numBytes int
+ }{
+ {99, 1008},
+ {21, 219},
+ },
+ },
+ "small histograms": {
+ numTotalSamples: 240,
+ histValFunc: func() func(i int) *histogram.Histogram {
+ hists := histogram.GenerateBigTestHistograms(240, 10)
+ return func(i int) *histogram.Histogram {
+ return hists[i]
+ }
+ }(),
+ expectedChks: []struct {
+ numSamples int
+ numBytes int
+ }{
+ {120, 1087},
+ {120, 1039},
+ },
+ },
+ "large histograms": {
+ numTotalSamples: 240,
+ histValFunc: func() func(i int) *histogram.Histogram {
+ hists := histogram.GenerateBigTestHistograms(240, 100)
+ return func(i int) *histogram.Histogram {
+ return hists[i]
+ }
+ }(),
+ expectedChks: []struct {
+ numSamples int
+ numBytes int
+ }{
+ {40, 896},
+ {40, 899},
+ {40, 896},
+ {30, 690},
+ {30, 691},
+ {30, 694},
+ {30, 693},
+ },
+ },
+ "really large histograms": {
+ // Really large histograms; each chunk can only contain a single histogram but we have a 10 sample minimum
+ // per chunk.
+ numTotalSamples: 11,
+ histValFunc: func() func(i int) *histogram.Histogram {
+ hists := histogram.GenerateBigTestHistograms(11, 100000)
+ return func(i int) *histogram.Histogram {
+ return hists[i]
+ }
+ }(),
+ expectedChks: []struct {
+ numSamples int
+ numBytes int
+ }{
+ {10, 200103},
+ {1, 87540},
+ },
+ },
+ }
+ 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.AppenderV2(context.Background())
+
+ ts := int64(10000)
+ lbls := labels.FromStrings("foo", "bar")
+ jitter := []int64{0, 1} // A bit of jitter to prevent dod=0.
+
+ for i := 0; i < tc.numTotalSamples; i++ {
+ if tc.floatValFunc != nil {
+ _, err := a.Append(0, lbls, 0, ts, tc.floatValFunc(i), nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ } else if tc.histValFunc != nil {
+ _, err := a.Append(0, lbls, 0, ts, 0, tc.histValFunc(i), nil, storage.AOptions{})
+ require.NoError(t, err)
+ }
+
+ ts += int64(60 * time.Second / time.Millisecond)
+ if tc.timestampJitter {
+ ts += jitter[i%len(jitter)]
+ }
+ }
+
+ require.NoError(t, a.Commit())
+
+ idxReader, err := h.Index()
+ require.NoError(t, err)
+
+ chkReader, err := h.Chunks()
+ require.NoError(t, err)
+
+ p, err := idxReader.Postings(ctx, "foo", "bar")
+ require.NoError(t, err)
+
+ var lblBuilder labels.ScratchBuilder
+
+ for p.Next() {
+ sRef := p.At()
+
+ chkMetas := make([]chunks.Meta, len(tc.expectedChks))
+ require.NoError(t, idxReader.Series(sRef, &lblBuilder, &chkMetas))
+
+ require.Len(t, chkMetas, len(tc.expectedChks))
+
+ for i, expected := range tc.expectedChks {
+ chk, iterable, err := chkReader.ChunkOrIterable(chkMetas[i])
+ require.NoError(t, err)
+ require.Nil(t, iterable)
+
+ require.Equal(t, expected.numSamples, chk.NumSamples())
+ require.Len(t, chk.Bytes(), expected.numBytes)
+ }
+ }
+ })
+ }
+}
+
+// TestHeadDetectsDuplicateSampleAtSizeLimit tests a regression where a duplicate sample
+// is appended to the head, right when the head chunk is at the size limit.
+// The test adds all samples as duplicate, thus expecting that the result has
+// exactly half of the samples.
+func TestHeadDetectsDuplicateSampleAtSizeLimit_AppenderV2(t *testing.T) {
+ numSamples := 1000
+ baseTS := int64(1695209650)
+
+ h, _ := newTestHead(t, DefaultBlockDuration, compression.None, false)
+ defer func() {
+ require.NoError(t, h.Close())
+ }()
+
+ a := h.AppenderV2(context.Background())
+ var err error
+ vals := []float64{math.MaxFloat64, 0x00} // Use the worst case scenario for the XOR encoding. Otherwise we hit the sample limit before the size limit.
+ for i := range numSamples {
+ ts := baseTS + int64(i/2)*10000
+ a.Append(0, labels.FromStrings("foo", "bar"), 0, ts, vals[(i/2)%len(vals)], nil, nil, storage.AOptions{})
+ err = a.Commit()
+ require.NoError(t, err)
+ a = h.AppenderV2(context.Background())
+ }
+
+ indexReader, err := h.Index()
+ require.NoError(t, err)
+
+ var (
+ chunks []chunks.Meta
+ builder labels.ScratchBuilder
+ )
+ require.NoError(t, indexReader.Series(1, &builder, &chunks))
+
+ chunkReader, err := h.Chunks()
+ require.NoError(t, err)
+
+ storedSampleCount := 0
+ for _, chunkMeta := range chunks {
+ chunk, iterable, err := chunkReader.ChunkOrIterable(chunkMeta)
+ require.NoError(t, err)
+ require.Nil(t, iterable)
+ storedSampleCount += chunk.NumSamples()
+ }
+
+ require.Equal(t, numSamples/2, storedSampleCount)
+}
+
+func TestWALSampleAndExemplarOrder_AppenderV2(t *testing.T) {
+ lbls := labels.FromStrings("foo", "bar")
+ testcases := map[string]struct {
+ appendF func(app storage.AppenderV2, ts int64) (storage.SeriesRef, error)
+ expectedType reflect.Type
+ }{
+ "float sample": {
+ appendF: func(app storage.AppenderV2, ts int64) (storage.SeriesRef, error) {
+ return app.Append(0, lbls, 0, ts, 1.0, nil, nil, storage.AOptions{Exemplars: []exemplar.Exemplar{{Value: 1.0, Ts: 5}}})
+ },
+ expectedType: reflect.TypeFor[[]record.RefSample](),
+ },
+ "histogram sample": {
+ appendF: func(app storage.AppenderV2, ts int64) (storage.SeriesRef, error) {
+ return app.Append(0, lbls, 0, ts, 0, tsdbutil.GenerateTestHistogram(1), nil, storage.AOptions{Exemplars: []exemplar.Exemplar{{Value: 1.0, Ts: 5}}})
+ },
+ expectedType: reflect.TypeFor[[]record.RefHistogramSample](),
+ },
+ "float histogram sample": {
+ appendF: func(app storage.AppenderV2, ts int64) (storage.SeriesRef, error) {
+ return app.Append(0, lbls, 0, ts, 0, nil, tsdbutil.GenerateTestFloatHistogram(1), storage.AOptions{Exemplars: []exemplar.Exemplar{{Value: 1.0, Ts: 5}}})
+ },
+ expectedType: reflect.TypeFor[[]record.RefFloatHistogramSample](),
+ },
+ }
+
+ 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.AppenderV2(context.Background())
+ _, err := tc.appendF(app, 10)
+ require.NoError(t, err)
+
+ require.NoError(t, app.Commit())
+
+ recs := readTestWAL(t, w.Dir())
+ require.Len(t, recs, 3)
+ _, ok := recs[0].([]record.RefSeries)
+ require.True(t, ok, "expected first record to be a RefSeries")
+ actualType := reflect.TypeOf(recs[1])
+ require.Equal(t, tc.expectedType, actualType, "expected second record to be a %s", tc.expectedType)
+ _, ok = recs[2].([]record.RefExemplar)
+ require.True(t, ok, "expected third record to be a RefExemplar")
+ })
+ }
+}
+
+func TestHeadAppenderV2_Append_FloatWithSameTimestampAsPreviousHistogram(t *testing.T) {
+ head, _ := newTestHead(t, DefaultBlockDuration, compression.None, false)
+
+ ls := labels.FromStrings(labels.MetricName, "test")
+
+ {
+ // Append a float 10.0 @ 1_000
+ app := head.AppenderV2(context.Background())
+ _, err := app.Append(0, ls, 0, 1_000, 10.0, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ }
+
+ {
+ // Append a float histogram @ 2_000
+ app := head.AppenderV2(context.Background())
+ h := tsdbutil.GenerateTestHistogram(1)
+ _, err := app.Append(0, ls, 0, 2_000, 0, h, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ }
+
+ app := head.AppenderV2(context.Background())
+ _, err := app.Append(0, ls, 0, 2_000, 10.0, nil, nil, storage.AOptions{})
+ require.Error(t, err)
+ require.ErrorIs(t, err, storage.NewDuplicateHistogramToFloatErr(2_000, 10.0))
+}
+
+func TestHeadAppenderV2_Append_EnableSTAsZeroSample(t *testing.T) {
+ // Make sure counter resets hints are non-zero, so we can detect ST histogram samples.
+ testHistogram := tsdbutil.GenerateTestHistogram(1)
+ testHistogram.CounterResetHint = histogram.NotCounterReset
+
+ testFloatHistogram := tsdbutil.GenerateTestFloatHistogram(1)
+ testFloatHistogram.CounterResetHint = histogram.NotCounterReset
+
+ testNHCB := tsdbutil.GenerateTestCustomBucketsHistogram(1)
+ testNHCB.CounterResetHint = histogram.NotCounterReset
+
+ testFloatNHCB := tsdbutil.GenerateTestCustomBucketsFloatHistogram(1)
+ testFloatNHCB.CounterResetHint = histogram.NotCounterReset
+
+ // TODO(beorn7): Once issue #15346 is fixed, the CounterResetHint of the
+ // following zero histograms should be histogram.CounterReset.
+ testZeroHistogram := &histogram.Histogram{
+ Schema: testHistogram.Schema,
+ ZeroThreshold: testHistogram.ZeroThreshold,
+ PositiveSpans: testHistogram.PositiveSpans,
+ NegativeSpans: testHistogram.NegativeSpans,
+ PositiveBuckets: []int64{0, 0, 0, 0},
+ NegativeBuckets: []int64{0, 0, 0, 0},
+ }
+ testZeroFloatHistogram := &histogram.FloatHistogram{
+ Schema: testFloatHistogram.Schema,
+ ZeroThreshold: testFloatHistogram.ZeroThreshold,
+ PositiveSpans: testFloatHistogram.PositiveSpans,
+ NegativeSpans: testFloatHistogram.NegativeSpans,
+ PositiveBuckets: []float64{0, 0, 0, 0},
+ NegativeBuckets: []float64{0, 0, 0, 0},
+ }
+ testZeroNHCB := &histogram.Histogram{
+ Schema: testNHCB.Schema,
+ PositiveSpans: testNHCB.PositiveSpans,
+ PositiveBuckets: []int64{0, 0, 0, 0},
+ CustomValues: testNHCB.CustomValues,
+ }
+ testZeroFloatNHCB := &histogram.FloatHistogram{
+ Schema: testFloatNHCB.Schema,
+ PositiveSpans: testFloatNHCB.PositiveSpans,
+ PositiveBuckets: []float64{0, 0, 0, 0},
+ CustomValues: testFloatNHCB.CustomValues,
+ }
+
+ type appendableSamples struct {
+ ts int64
+ fSample float64
+ h *histogram.Histogram
+ fh *histogram.FloatHistogram
+ st int64
+ }
+ for _, tc := range []struct {
+ name string
+ appendableSamples []appendableSamples
+ expectedSamples []chunks.Sample
+ }{
+ {
+ name: "In order ct+normal sample/floatSample",
+ appendableSamples: []appendableSamples{
+ {ts: 100, fSample: 10, st: 1},
+ {ts: 101, fSample: 10, st: 1},
+ },
+ expectedSamples: []chunks.Sample{
+ sample{t: 1, f: 0},
+ sample{t: 100, f: 10},
+ sample{t: 101, f: 10},
+ },
+ },
+ {
+ name: "In order ct+normal sample/histogram",
+ appendableSamples: []appendableSamples{
+ {ts: 100, h: testHistogram, st: 1},
+ {ts: 101, h: testHistogram, st: 1},
+ },
+ expectedSamples: func() []chunks.Sample {
+ return []chunks.Sample{
+ sample{t: 1, h: testZeroHistogram},
+ sample{t: 100, h: testHistogram},
+ sample{t: 101, h: testHistogram},
+ }
+ }(),
+ },
+ {
+ name: "In order ct+normal sample/floathistogram",
+ appendableSamples: []appendableSamples{
+ {ts: 100, fh: testFloatHistogram, st: 1},
+ {ts: 101, fh: testFloatHistogram, st: 1},
+ },
+ expectedSamples: func() []chunks.Sample {
+ return []chunks.Sample{
+ sample{t: 1, fh: testZeroFloatHistogram},
+ sample{t: 100, fh: testFloatHistogram},
+ sample{t: 101, fh: testFloatHistogram},
+ }
+ }(),
+ },
+ {
+ name: "In order ct+normal sample/NHCB",
+ appendableSamples: []appendableSamples{
+ {ts: 100, h: testNHCB, st: 1},
+ {ts: 101, h: testNHCB, st: 1},
+ },
+ expectedSamples: func() []chunks.Sample {
+ return []chunks.Sample{
+ sample{t: 1, h: testZeroNHCB},
+ sample{t: 100, h: testNHCB},
+ sample{t: 101, h: testNHCB},
+ }
+ }(),
+ },
+ {
+ name: "In order ct+normal sample/floatNHCB",
+ appendableSamples: []appendableSamples{
+ {ts: 100, fh: testFloatNHCB, st: 1},
+ {ts: 101, fh: testFloatNHCB, st: 1},
+ },
+ expectedSamples: func() []chunks.Sample {
+ return []chunks.Sample{
+ sample{t: 1, fh: testZeroFloatNHCB},
+ sample{t: 100, fh: testFloatNHCB},
+ sample{t: 101, fh: testFloatNHCB},
+ }
+ }(),
+ },
+ {
+ name: "Consecutive appends with same st ignore st/floatSample",
+ appendableSamples: []appendableSamples{
+ {ts: 100, fSample: 10, st: 1},
+ {ts: 101, fSample: 10, st: 1},
+ },
+ expectedSamples: []chunks.Sample{
+ sample{t: 1, f: 0},
+ sample{t: 100, f: 10},
+ sample{t: 101, f: 10},
+ },
+ },
+ {
+ name: "Consecutive appends with same st ignore st/histogram",
+ appendableSamples: []appendableSamples{
+ {ts: 100, h: testHistogram, st: 1},
+ {ts: 101, h: testHistogram, st: 1},
+ },
+ expectedSamples: func() []chunks.Sample {
+ return []chunks.Sample{
+ sample{t: 1, h: testZeroHistogram},
+ sample{t: 100, h: testHistogram},
+ sample{t: 101, h: testHistogram},
+ }
+ }(),
+ },
+ {
+ name: "Consecutive appends with same st ignore st/floathistogram",
+ appendableSamples: []appendableSamples{
+ {ts: 100, fh: testFloatHistogram, st: 1},
+ {ts: 101, fh: testFloatHistogram, st: 1},
+ },
+ expectedSamples: func() []chunks.Sample {
+ return []chunks.Sample{
+ sample{t: 1, fh: testZeroFloatHistogram},
+ sample{t: 100, fh: testFloatHistogram},
+ sample{t: 101, fh: testFloatHistogram},
+ }
+ }(),
+ },
+ {
+ name: "Consecutive appends with same st ignore st/NHCB",
+ appendableSamples: []appendableSamples{
+ {ts: 100, h: testNHCB, st: 1},
+ {ts: 101, h: testNHCB, st: 1},
+ },
+ expectedSamples: func() []chunks.Sample {
+ return []chunks.Sample{
+ sample{t: 1, h: testZeroNHCB},
+ sample{t: 100, h: testNHCB},
+ sample{t: 101, h: testNHCB},
+ }
+ }(),
+ },
+ {
+ name: "Consecutive appends with same st ignore st/floatNHCB",
+ appendableSamples: []appendableSamples{
+ {ts: 100, fh: testFloatNHCB, st: 1},
+ {ts: 101, fh: testFloatNHCB, st: 1},
+ },
+ expectedSamples: func() []chunks.Sample {
+ return []chunks.Sample{
+ sample{t: 1, fh: testZeroFloatNHCB},
+ sample{t: 100, fh: testFloatNHCB},
+ sample{t: 101, fh: testFloatNHCB},
+ }
+ }(),
+ },
+ {
+ name: "Consecutive appends with newer st do not ignore st/floatSample",
+ appendableSamples: []appendableSamples{
+ {ts: 100, fSample: 10, st: 1},
+ {ts: 102, fSample: 10, st: 101},
+ },
+ expectedSamples: []chunks.Sample{
+ sample{t: 1, f: 0},
+ sample{t: 100, f: 10},
+ sample{t: 101, f: 0},
+ sample{t: 102, f: 10},
+ },
+ },
+ {
+ name: "Consecutive appends with newer st do not ignore st/histogram",
+ appendableSamples: []appendableSamples{
+ {ts: 100, h: testHistogram, st: 1},
+ {ts: 102, h: testHistogram, st: 101},
+ },
+ expectedSamples: []chunks.Sample{
+ sample{t: 1, h: testZeroHistogram},
+ sample{t: 100, h: testHistogram},
+ sample{t: 101, h: testZeroHistogram},
+ sample{t: 102, h: testHistogram},
+ },
+ },
+ {
+ name: "Consecutive appends with newer st do not ignore st/floathistogram",
+ appendableSamples: []appendableSamples{
+ {ts: 100, fh: testFloatHistogram, st: 1},
+ {ts: 102, fh: testFloatHistogram, st: 101},
+ },
+ expectedSamples: []chunks.Sample{
+ sample{t: 1, fh: testZeroFloatHistogram},
+ sample{t: 100, fh: testFloatHistogram},
+ sample{t: 101, fh: testZeroFloatHistogram},
+ sample{t: 102, fh: testFloatHistogram},
+ },
+ },
+ {
+ name: "Consecutive appends with newer st do not ignore st/NHCB",
+ appendableSamples: []appendableSamples{
+ {ts: 100, h: testNHCB, st: 1},
+ {ts: 102, h: testNHCB, st: 101},
+ },
+ expectedSamples: []chunks.Sample{
+ sample{t: 1, h: testZeroNHCB},
+ sample{t: 100, h: testNHCB},
+ sample{t: 101, h: testZeroNHCB},
+ sample{t: 102, h: testNHCB},
+ },
+ },
+ {
+ name: "Consecutive appends with newer st do not ignore st/floatNHCB",
+ appendableSamples: []appendableSamples{
+ {ts: 100, fh: testFloatNHCB, st: 1},
+ {ts: 102, fh: testFloatNHCB, st: 101},
+ },
+ expectedSamples: []chunks.Sample{
+ sample{t: 1, fh: testZeroFloatNHCB},
+ sample{t: 100, fh: testFloatNHCB},
+ sample{t: 101, fh: testZeroFloatNHCB},
+ sample{t: 102, fh: testFloatNHCB},
+ },
+ },
+ {
+ name: "ST equals to previous sample timestamp is ignored/floatSample",
+ appendableSamples: []appendableSamples{
+ {ts: 100, fSample: 10, st: 1},
+ {ts: 101, fSample: 10, st: 100},
+ },
+ expectedSamples: []chunks.Sample{
+ sample{t: 1, f: 0},
+ sample{t: 100, f: 10},
+ sample{t: 101, f: 10},
+ },
+ },
+ {
+ name: "ST equals to previous sample timestamp is ignored/histogram",
+ appendableSamples: []appendableSamples{
+ {ts: 100, h: testHistogram, st: 1},
+ {ts: 101, h: testHistogram, st: 100},
+ },
+ expectedSamples: func() []chunks.Sample {
+ return []chunks.Sample{
+ sample{t: 1, h: testZeroHistogram},
+ sample{t: 100, h: testHistogram},
+ sample{t: 101, h: testHistogram},
+ }
+ }(),
+ },
+ {
+ name: "ST equals to previous sample timestamp is ignored/floathistogram",
+ appendableSamples: []appendableSamples{
+ {ts: 100, fh: testFloatHistogram, st: 1},
+ {ts: 101, fh: testFloatHistogram, st: 100},
+ },
+ expectedSamples: func() []chunks.Sample {
+ return []chunks.Sample{
+ sample{t: 1, fh: testZeroFloatHistogram},
+ sample{t: 100, fh: testFloatHistogram},
+ sample{t: 101, fh: testFloatHistogram},
+ }
+ }(),
+ },
+ {
+ name: "ST equals to previous sample timestamp is ignored/NHCB",
+ appendableSamples: []appendableSamples{
+ {ts: 100, h: testNHCB, st: 1},
+ {ts: 101, h: testNHCB, st: 100},
+ },
+ expectedSamples: func() []chunks.Sample {
+ return []chunks.Sample{
+ sample{t: 1, h: testZeroNHCB},
+ sample{t: 100, h: testNHCB},
+ sample{t: 101, h: testNHCB},
+ }
+ }(),
+ },
+ {
+ name: "ST equals to previous sample timestamp is ignored/floatNHCB",
+ appendableSamples: []appendableSamples{
+ {ts: 100, fh: testFloatNHCB, st: 1},
+ {ts: 101, fh: testFloatNHCB, st: 100},
+ },
+ expectedSamples: func() []chunks.Sample {
+ return []chunks.Sample{
+ sample{t: 1, fh: testZeroFloatNHCB},
+ sample{t: 100, fh: testFloatNHCB},
+ sample{t: 101, fh: testFloatNHCB},
+ }
+ }(),
+ },
+ {
+ name: "ST lower than minValidTime/float",
+ appendableSamples: []appendableSamples{
+ {ts: 100, fSample: 10, st: -1},
+ },
+ // ST results ErrOutOfBounds, but ST append is best effort, so
+ // ST should be ignored, but sample appended.
+ expectedSamples: func() []chunks.Sample {
+ return []chunks.Sample{
+ sample{t: 100, f: 10},
+ }
+ }(),
+ },
+ {
+ name: "ST lower than minValidTime/histogram",
+ appendableSamples: []appendableSamples{
+ {ts: 100, h: testHistogram, st: -1},
+ },
+ // ST results ErrOutOfBounds, but ST append is best effort, so
+ // ST should be ignored, but sample appended.
+ expectedSamples: func() []chunks.Sample {
+ // NOTE: Without ST, on query, first histogram sample will get
+ // CounterReset adjusted to 0.
+ firstSample := testHistogram.Copy()
+ firstSample.CounterResetHint = histogram.UnknownCounterReset
+ return []chunks.Sample{
+ sample{t: 100, h: firstSample},
+ }
+ }(),
+ },
+ {
+ name: "ST lower than minValidTime/floathistogram",
+ appendableSamples: []appendableSamples{
+ {ts: 100, fh: testFloatHistogram, st: -1},
+ },
+ // ST results ErrOutOfBounds, but ST append is best effort, so
+ // ST should be ignored, but sample appended.
+ expectedSamples: func() []chunks.Sample {
+ // NOTE: Without ST, on query, first histogram sample will get
+ // CounterReset adjusted to 0.
+ firstSample := testFloatHistogram.Copy()
+ firstSample.CounterResetHint = histogram.UnknownCounterReset
+ return []chunks.Sample{
+ sample{t: 100, fh: firstSample},
+ }
+ }(),
+ },
+ {
+ name: "ST lower than minValidTime/NHCB",
+ appendableSamples: []appendableSamples{
+ {ts: 100, h: testNHCB, st: -1},
+ },
+ // ST results ErrOutOfBounds, but ST append is best effort, so
+ // ST should be ignored, but sample appended.
+ expectedSamples: func() []chunks.Sample {
+ // NOTE: Without ST, on query, first histogram sample will get
+ // CounterReset adjusted to 0.
+ firstSample := testNHCB.Copy()
+ firstSample.CounterResetHint = histogram.UnknownCounterReset
+ return []chunks.Sample{
+ sample{t: 100, h: firstSample},
+ }
+ }(),
+ },
+ {
+ name: "ST lower than minValidTime/floatNHCB",
+ appendableSamples: []appendableSamples{
+ {ts: 100, fh: testFloatNHCB, st: -1},
+ },
+ // ST results ErrOutOfBounds, but ST append is best effort, so
+ // ST should be ignored, but sample appended.
+ expectedSamples: func() []chunks.Sample {
+ // NOTE: Without ST, on query, first histogram sample will get
+ // CounterReset adjusted to 0.
+ firstSample := testFloatNHCB.Copy()
+ firstSample.CounterResetHint = histogram.UnknownCounterReset
+ return []chunks.Sample{
+ sample{t: 100, fh: firstSample},
+ }
+ }(),
+ },
+ {
+ name: "ST duplicates an existing sample/float",
+ appendableSamples: []appendableSamples{
+ {ts: 100, fSample: 10},
+ {ts: 200, fSample: 10, st: 100},
+ },
+ // ST results ErrOutOfBounds, but ST append is best effort, so
+ // ST should be ignored, but sample appended.
+ expectedSamples: func() []chunks.Sample {
+ return []chunks.Sample{
+ sample{t: 100, f: 10},
+ sample{t: 200, f: 10},
+ }
+ }(),
+ },
+ {
+ name: "ST duplicates an existing sample/histogram",
+ appendableSamples: []appendableSamples{
+ {ts: 100, h: testHistogram},
+ {ts: 200, h: testHistogram, st: 100},
+ },
+ // ST results ErrDuplicateSampleForTimestamp, but ST append is best effort, so
+ // ST should be ignored, but sample appended.
+ expectedSamples: func() []chunks.Sample {
+ // NOTE: Without ST, on query, first histogram sample will get
+ // CounterReset adjusted to 0.
+ firstSample := testHistogram.Copy()
+ firstSample.CounterResetHint = histogram.UnknownCounterReset
+ return []chunks.Sample{
+ sample{t: 100, h: firstSample},
+ sample{t: 200, h: testHistogram},
+ }
+ }(),
+ },
+ {
+ name: "ST duplicates an existing sample/floathistogram",
+ appendableSamples: []appendableSamples{
+ {ts: 100, fh: testFloatHistogram},
+ {ts: 200, fh: testFloatHistogram, st: 100},
+ },
+ // ST results ErrDuplicateSampleForTimestamp, but ST append is best effort, so
+ // ST should ignored, but sample appended.
+ expectedSamples: func() []chunks.Sample {
+ // NOTE: Without ST, on query, first histogram sample will get
+ // CounterReset adjusted to 0.
+ firstSample := testFloatHistogram.Copy()
+ firstSample.CounterResetHint = histogram.UnknownCounterReset
+ return []chunks.Sample{
+ sample{t: 100, fh: firstSample},
+ sample{t: 200, fh: testFloatHistogram},
+ }
+ }(),
+ },
+ {
+ name: "ST duplicates an existing sample/NHCB",
+ appendableSamples: []appendableSamples{
+ {ts: 100, h: testNHCB},
+ {ts: 200, h: testNHCB, st: 100},
+ },
+ // ST results ErrDuplicateSampleForTimestamp, but ST append is best effort, so
+ // ST should be ignored, but sample appended.
+ expectedSamples: func() []chunks.Sample {
+ // NOTE: Without ST, on query, first histogram sample will get
+ // CounterReset adjusted to 0.
+ firstSample := testNHCB.Copy()
+ firstSample.CounterResetHint = histogram.UnknownCounterReset
+ return []chunks.Sample{
+ sample{t: 100, h: firstSample},
+ sample{t: 200, h: testNHCB},
+ }
+ }(),
+ },
+ {
+ name: "ST duplicates an existing sample/floatNHCB",
+ appendableSamples: []appendableSamples{
+ {ts: 100, fh: testFloatNHCB},
+ {ts: 200, fh: testFloatNHCB, st: 100},
+ },
+ // ST results ErrDuplicateSampleForTimestamp, but ST append is best effort, so
+ // ST should ignored, but sample appended.
+ expectedSamples: func() []chunks.Sample {
+ // NOTE: Without ST, on query, first histogram sample will get
+ // CounterReset adjusted to 0.
+ firstSample := testFloatNHCB.Copy()
+ firstSample.CounterResetHint = histogram.UnknownCounterReset
+ return []chunks.Sample{
+ sample{t: 100, fh: firstSample},
+ sample{t: 200, fh: testFloatNHCB},
+ }
+ }(),
+ },
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ opts := newTestHeadDefaultOptions(DefaultBlockDuration, false)
+ opts.EnableSTAsZeroSample = true
+ h, _ := newTestHeadWithOptions(t, compression.None, opts)
+ defer func() {
+ require.NoError(t, h.Close())
+ }()
+
+ a := h.AppenderV2(context.Background())
+ lbls := labels.FromStrings("foo", "bar")
+
+ for _, s := range tc.appendableSamples {
+ _, err := a.Append(0, lbls, s.st, s.ts, s.fSample, s.h, s.fh, storage.AOptions{})
+ require.NoError(t, err)
+ }
+ require.NoError(t, a.Commit())
+
+ q, err := NewBlockQuerier(h, math.MinInt64, math.MaxInt64)
+ require.NoError(t, err)
+ result := query(t, q, labels.MustNewMatcher(labels.MatchEqual, "foo", "bar"))
+ require.Equal(t, tc.expectedSamples, result[`{foo="bar"}`])
+ })
+ }
+}
+
+// Regression test for data race https://github.com/prometheus/prometheus/issues/15139.
+func TestHeadAppenderV2_Append_HistogramAndCommitConcurrency(t *testing.T) {
+ h := tsdbutil.GenerateTestHistogram(1)
+ fh := tsdbutil.GenerateTestFloatHistogram(1)
+
+ testCases := map[string]func(storage.AppenderV2, int) error{
+ "integer histogram": func(app storage.AppenderV2, i int) error {
+ _, err := app.Append(0, labels.FromStrings("foo", "bar", "serial", strconv.Itoa(i)), 0, 1, 0, h, nil, storage.AOptions{})
+ return err
+ },
+ "float histogram": func(app storage.AppenderV2, i int) error {
+ _, err := app.Append(0, labels.FromStrings("foo", "bar", "serial", strconv.Itoa(i)), 0, 1, 0, nil, fh, storage.AOptions{})
+ return err
+ },
+ }
+ for name, tc := range testCases {
+ t.Run(name, func(t *testing.T) {
+ testHeadAppenderV2AppendHistogramAndCommitConcurrency(t, tc)
+ })
+ }
+}
+
+func testHeadAppenderV2AppendHistogramAndCommitConcurrency(t *testing.T, appendFn func(storage.AppenderV2, int) error) {
+ head, _ := newTestHead(t, 1000, compression.None, false)
+ defer func() {
+ require.NoError(t, head.Close())
+ }()
+
+ wg := sync.WaitGroup{}
+ wg.Add(2)
+
+ // How this works: Commit() should be atomic, thus one of the commits will
+ // be first and the other second. The first commit will create a new series
+ // and write a sample. The second commit will see an exact duplicate sample
+ // which it should ignore. Unless there's a race that causes the
+ // memSeries.lastHistogram to be corrupt and fail the duplicate check.
+ go func() {
+ defer wg.Done()
+ for i := range 10000 {
+ app := head.AppenderV2(context.Background())
+ require.NoError(t, appendFn(app, i))
+ require.NoError(t, app.Commit())
+ }
+ }()
+
+ go func() {
+ defer wg.Done()
+ for i := range 10000 {
+ app := head.AppenderV2(context.Background())
+ require.NoError(t, appendFn(app, i))
+ require.NoError(t, app.Commit())
+ }
+ }()
+
+ wg.Wait()
+}
+
+func TestHeadAppenderV2_NumStaleSeries(t *testing.T) {
+ head, _ := newTestHead(t, 1000, compression.None, false)
+ t.Cleanup(func() {
+ // Captures head by reference, so it closes the final head after restarts.
+ _ = head.Close()
+ })
+ require.NoError(t, head.Init(0))
+
+ // Initially, no series should be stale.
+ require.Equal(t, uint64(0), head.NumStaleSeries())
+
+ appendSample := func(lbls labels.Labels, ts int64, val float64) {
+ app := head.AppenderV2(context.Background())
+ _, err := app.Append(0, lbls, 0, ts, val, nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ }
+ appendHistogram := func(lbls labels.Labels, ts int64, val *histogram.Histogram) {
+ app := head.AppenderV2(context.Background())
+ _, err := app.Append(0, lbls, 0, ts, 0, val, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ }
+ appendFloatHistogram := func(lbls labels.Labels, ts int64, val *histogram.FloatHistogram) {
+ app := head.AppenderV2(context.Background())
+ _, err := app.Append(0, lbls, 0, ts, 0, nil, val, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ }
+
+ verifySeriesCounts := func(numStaleSeries, numSeries int) {
+ require.Equal(t, uint64(numStaleSeries), head.NumStaleSeries())
+ require.Equal(t, uint64(numSeries), head.NumSeries())
+ }
+
+ restartHeadAndVerifySeriesCounts := func(numStaleSeries, numSeries int) {
+ verifySeriesCounts(numStaleSeries, numSeries)
+
+ require.NoError(t, head.Close())
+
+ wal, err := wlog.NewSize(nil, nil, filepath.Join(head.opts.ChunkDirRoot, "wal"), 32768, compression.None)
+ require.NoError(t, err)
+ head, err = NewHead(nil, nil, wal, nil, head.opts, nil)
+ require.NoError(t, err)
+ require.NoError(t, head.Init(0))
+
+ verifySeriesCounts(numStaleSeries, numSeries)
+ }
+
+ // Create some series with normal samples.
+ series1 := labels.FromStrings("name", "series1", "label", "value1")
+ series2 := labels.FromStrings("name", "series2", "label", "value2")
+ series3 := labels.FromStrings("name", "series3", "label", "value3")
+
+ // Add normal samples to all series.
+ appendSample(series1, 100, 1)
+ appendSample(series2, 100, 2)
+ appendSample(series3, 100, 3)
+ // Still no stale series.
+ verifySeriesCounts(0, 3)
+
+ // Make series1 stale by appending a stale sample. Now we should have 1 stale series.
+ appendSample(series1, 200, math.Float64frombits(value.StaleNaN))
+ verifySeriesCounts(1, 3)
+
+ // Make series2 stale as well.
+ appendSample(series2, 200, math.Float64frombits(value.StaleNaN))
+ verifySeriesCounts(2, 3)
+ restartHeadAndVerifySeriesCounts(2, 3)
+
+ // Add a non-stale sample to series1. It should not be counted as stale now.
+ appendSample(series1, 300, 10)
+ verifySeriesCounts(1, 3)
+ restartHeadAndVerifySeriesCounts(1, 3)
+
+ // Test that series3 doesn't become stale when we add another normal sample.
+ appendSample(series3, 200, 10)
+ verifySeriesCounts(1, 3)
+
+ // Test histogram stale samples as well.
+ series4 := labels.FromStrings("name", "series4", "type", "histogram")
+ h := tsdbutil.GenerateTestHistograms(1)[0]
+ appendHistogram(series4, 100, h)
+ verifySeriesCounts(1, 4)
+
+ // Make histogram series stale.
+ staleHist := h.Copy()
+ staleHist.Sum = math.Float64frombits(value.StaleNaN)
+ appendHistogram(series4, 200, staleHist)
+ verifySeriesCounts(2, 4)
+
+ // Test float histogram stale samples.
+ series5 := labels.FromStrings("name", "series5", "type", "float_histogram")
+ fh := tsdbutil.GenerateTestFloatHistograms(1)[0]
+ appendFloatHistogram(series5, 100, fh)
+ verifySeriesCounts(2, 5)
+ restartHeadAndVerifySeriesCounts(2, 5)
+
+ // Make float histogram series stale.
+ staleFH := fh.Copy()
+ staleFH.Sum = math.Float64frombits(value.StaleNaN)
+ appendFloatHistogram(series5, 200, staleFH)
+ verifySeriesCounts(3, 5)
+
+ // Make histogram sample non-stale and stale back again.
+ appendHistogram(series4, 210, h)
+ verifySeriesCounts(2, 5)
+ appendHistogram(series4, 220, staleHist)
+ verifySeriesCounts(3, 5)
+
+ // Make float histogram sample non-stale and stale back again.
+ appendFloatHistogram(series5, 210, fh)
+ verifySeriesCounts(2, 5)
+ appendFloatHistogram(series5, 220, staleFH)
+ verifySeriesCounts(3, 5)
+
+ // Series 1 and 3 are not stale at this point. Add a new sample to series 1 and series 5,
+ // so after the GC and removing series 2, 3, 4, we should be left with 1 stale and 1 non-stale series.
+ appendSample(series1, 400, 10)
+ appendFloatHistogram(series5, 400, staleFH)
+ restartHeadAndVerifySeriesCounts(3, 5)
+
+ // This will test restarting with snapshot.
+ head.opts.EnableMemorySnapshotOnShutdown = true
+ restartHeadAndVerifySeriesCounts(3, 5)
+
+ // Test garbage collection behavior - stale series should be decremented when GC'd.
+ // Force a garbage collection by truncating old data.
+ require.NoError(t, head.Truncate(300))
+
+ // After truncation, run GC to collect old chunks/series.
+ head.gc()
+
+ // series 1 and series 5 are left.
+ verifySeriesCounts(1, 2)
+
+ // Test creating a new series for each of float, histogram, float histogram that starts as stale.
+ // This should be counted as stale.
+ series6 := labels.FromStrings("name", "series6", "direct", "stale")
+ series7 := labels.FromStrings("name", "series7", "direct", "stale")
+ series8 := labels.FromStrings("name", "series8", "direct", "stale")
+ appendSample(series6, 400, math.Float64frombits(value.StaleNaN))
+ verifySeriesCounts(2, 3)
+ appendHistogram(series7, 400, staleHist)
+ verifySeriesCounts(3, 4)
+ appendFloatHistogram(series8, 400, staleFH)
+ verifySeriesCounts(4, 5)
+}
+
+// TestHistogramStalenessConversionMetrics verifies that staleness marker conversion correctly
+// increments the right appender metrics for both histogram and float histogram scenarios.
+func TestHeadAppenderV2_Append_HistogramStalenessConversionMetrics(t *testing.T) {
+ testCases := []struct {
+ name string
+ setupHistogram func(app storage.AppenderV2, lbls labels.Labels) error
+ }{
+ {
+ name: "float_staleness_to_histogram",
+ setupHistogram: func(app storage.AppenderV2, lbls labels.Labels) error {
+ _, err := app.Append(0, lbls, 0, 1000, 0, tsdbutil.GenerateTestHistograms(1)[0], nil, storage.AOptions{})
+ return err
+ },
+ },
+ {
+ name: "float_staleness_to_float_histogram",
+ setupHistogram: func(app storage.AppenderV2, lbls labels.Labels) error {
+ _, err := app.Append(0, lbls, 0, 1000, 0, nil, tsdbutil.GenerateTestFloatHistograms(1)[0], storage.AOptions{})
+ return err
+ },
+ },
+ }
+
+ 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)
+
+ // Helper to get counter values
+ getSampleCounter := func(sampleType string) float64 {
+ metric := &dto.Metric{}
+ err := head.metrics.samplesAppended.WithLabelValues(sampleType).Write(metric)
+ require.NoError(t, err)
+ return metric.GetCounter().GetValue()
+ }
+
+ // Step 1: Establish a series with histogram data
+ app := head.AppenderV2(context.Background())
+ err := tc.setupHistogram(app, lbls)
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+
+ // Step 2: Add a float staleness marker
+ app = head.AppenderV2(context.Background())
+ _, err = app.Append(0, lbls, 0, 2000, math.Float64frombits(value.StaleNaN), nil, nil, storage.AOptions{})
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+
+ // Count what was actually stored by querying the series
+ q, err := NewBlockQuerier(head, 0, 3000)
+ require.NoError(t, err)
+ defer q.Close()
+
+ ss := q.Select(context.Background(), false, nil, labels.MustNewMatcher(labels.MatchEqual, "name", tc.name))
+ require.True(t, ss.Next())
+ series := ss.At()
+
+ it := series.Iterator(nil)
+
+ actualFloatSamples := 0
+ actualHistogramSamples := 0
+
+ for valType := it.Next(); valType != chunkenc.ValNone; valType = it.Next() {
+ switch valType {
+ case chunkenc.ValFloat:
+ actualFloatSamples++
+ case chunkenc.ValHistogram, chunkenc.ValFloatHistogram:
+ actualHistogramSamples++
+ }
+ }
+ require.NoError(t, it.Err())
+
+ // Verify what was actually stored - should be 0 floats, 2 histograms (original + converted staleness marker)
+ require.Equal(t, 0, actualFloatSamples, "Should have 0 float samples stored")
+ require.Equal(t, 2, actualHistogramSamples, "Should have 2 histogram samples: original + converted staleness marker")
+
+ // The metrics should match what was actually stored
+ require.Equal(t, float64(actualFloatSamples), getSampleCounter(sampleMetricTypeFloat),
+ "Float counter should match actual float samples stored")
+ require.Equal(t, float64(actualHistogramSamples), getSampleCounter(sampleMetricTypeHistogram),
+ "Histogram counter should match actual histogram samples stored")
+ })
+ }
+}
diff --git a/tsdb/head_bench_test.go b/tsdb/head_bench_test.go
index c98fb6613d..d15f6cc310 100644
--- a/tsdb/head_bench_test.go
+++ b/tsdb/head_bench_test.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Prometheus Authors
+// 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
@@ -14,7 +14,6 @@
package tsdb
import (
- "context"
"errors"
"fmt"
"math/rand"
@@ -32,6 +31,227 @@ import (
"github.com/prometheus/prometheus/util/compression"
)
+type benchAppendFunc func(b *testing.B, h *Head, ts int64, series []storage.Series, samplesPerAppend int64) storage.AppenderTransaction
+
+func appendV1Float(b *testing.B, h *Head, ts int64, series []storage.Series, samplesPerAppend int64) storage.AppenderTransaction {
+ var err error
+ app := h.Appender(b.Context())
+ for _, s := range series {
+ var ref storage.SeriesRef
+ for sampleIndex := range samplesPerAppend {
+ ref, err = app.Append(ref, s.Labels(), ts+sampleIndex, float64(ts+sampleIndex))
+ require.NoError(b, err)
+ }
+ }
+ return app
+}
+
+func appendV2Float(b *testing.B, h *Head, ts int64, series []storage.Series, samplesPerAppend int64) storage.AppenderTransaction {
+ var err error
+ app := h.AppenderV2(b.Context())
+ for _, s := range series {
+ var ref storage.SeriesRef
+ for sampleIndex := range samplesPerAppend {
+ ref, err = app.Append(ref, s.Labels(), 0, ts+sampleIndex, float64(ts+sampleIndex), nil, nil, storage.AOptions{})
+ require.NoError(b, err)
+ }
+ }
+ return app
+}
+
+func appendV1FloatOrHistogramWithExemplars(b *testing.B, h *Head, ts int64, series []storage.Series, samplesPerAppend int64) storage.AppenderTransaction {
+ var err error
+ app := h.Appender(b.Context())
+ for i, s := range series {
+ var ref storage.SeriesRef
+ for sampleIndex := range samplesPerAppend {
+ // if i is even, append a sample, else append a histogram.
+ if i%2 == 0 {
+ ref, err = app.Append(ref, s.Labels(), ts+sampleIndex, float64(ts+sampleIndex))
+ require.NoError(b, err)
+ // Every sample also has an exemplar attached.
+ _, err = app.AppendExemplar(ref, s.Labels(), exemplar.Exemplar{
+ Labels: labels.FromStrings("trace_id", strconv.Itoa(rand.Int())),
+ Value: rand.Float64(),
+ Ts: ts + sampleIndex,
+ })
+ require.NoError(b, err)
+ continue
+ }
+
+ h := &histogram.Histogram{
+ Count: 7 + uint64(ts*5),
+ ZeroCount: 2 + uint64(ts),
+ ZeroThreshold: 0.001,
+ Sum: 18.4 * rand.Float64(),
+ Schema: 1,
+ PositiveSpans: []histogram.Span{
+ {Offset: 0, Length: 2},
+ {Offset: 1, Length: 2},
+ },
+ PositiveBuckets: []int64{ts + 1, 1, -1, 0},
+ }
+ ref, err = app.AppendHistogram(ref, s.Labels(), ts, h, nil)
+ require.NoError(b, err)
+ // Every histogram sample also has 3 exemplars attached.
+ _, err = app.AppendExemplar(ref, s.Labels(), exemplar.Exemplar{
+ Labels: labels.FromStrings("trace_id", strconv.Itoa(rand.Int())),
+ Value: rand.Float64(),
+ Ts: ts + sampleIndex,
+ })
+ require.NoError(b, err)
+ _, err = app.AppendExemplar(ref, s.Labels(), exemplar.Exemplar{
+ Labels: labels.FromStrings("trace_id", strconv.Itoa(rand.Int())),
+ Value: rand.Float64(),
+ Ts: ts + sampleIndex,
+ })
+ require.NoError(b, err)
+ _, err = app.AppendExemplar(ref, s.Labels(), exemplar.Exemplar{
+ Labels: labels.FromStrings("trace_id", strconv.Itoa(rand.Int())),
+ Value: rand.Float64(),
+ Ts: ts + sampleIndex,
+ })
+ require.NoError(b, err)
+ }
+ }
+ return app
+}
+
+func appendV2FloatOrHistogramWithExemplars(b *testing.B, h *Head, ts int64, series []storage.Series, samplesPerAppend int64) storage.AppenderTransaction {
+ var (
+ err error
+ ex = make([]exemplar.Exemplar, 3)
+ )
+
+ app := h.AppenderV2(b.Context())
+ for i, s := range series {
+ var ref storage.SeriesRef
+ for sampleIndex := range samplesPerAppend {
+ aOpts := storage.AOptions{Exemplars: ex[:0]}
+
+ // if i is even, append a sample, else append a histogram.
+ if i%2 == 0 {
+ // Every sample also has an exemplar attached.
+ aOpts.Exemplars = append(aOpts.Exemplars, exemplar.Exemplar{
+ Labels: labels.FromStrings("trace_id", strconv.Itoa(rand.Int())),
+ Value: rand.Float64(),
+ Ts: ts + sampleIndex,
+ })
+ ref, err = app.Append(ref, s.Labels(), 0, ts, float64(ts), nil, nil, aOpts)
+ require.NoError(b, err)
+ continue
+ }
+ h := &histogram.Histogram{
+ Count: 7 + uint64(ts*5),
+ ZeroCount: 2 + uint64(ts),
+ ZeroThreshold: 0.001,
+ Sum: 18.4 * rand.Float64(),
+ Schema: 1,
+ PositiveSpans: []histogram.Span{
+ {Offset: 0, Length: 2},
+ {Offset: 1, Length: 2},
+ },
+ PositiveBuckets: []int64{ts + 1, 1, -1, 0},
+ }
+
+ // Every histogram sample also has 3 exemplars attached.
+ aOpts.Exemplars = append(aOpts.Exemplars,
+ exemplar.Exemplar{
+ Labels: labels.FromStrings("trace_id", strconv.Itoa(rand.Int())),
+ Value: rand.Float64(),
+ Ts: ts + sampleIndex,
+ },
+ exemplar.Exemplar{
+ Labels: labels.FromStrings("trace_id", strconv.Itoa(rand.Int())),
+ Value: rand.Float64(),
+ Ts: ts + sampleIndex,
+ },
+ exemplar.Exemplar{
+ Labels: labels.FromStrings("trace_id", strconv.Itoa(rand.Int())),
+ Value: rand.Float64(),
+ Ts: ts + sampleIndex,
+ },
+ )
+ ref, err = app.Append(ref, s.Labels(), 0, ts, 0, h, nil, aOpts)
+ require.NoError(b, err)
+ }
+ }
+ return app
+}
+
+type appendCase struct {
+ name string
+ appendFunc benchAppendFunc
+}
+
+func appendCases() []appendCase {
+ return []appendCase{
+ {
+ name: "appender=v1/case=floats",
+ appendFunc: appendV1Float,
+ },
+ {
+ name: "appender=v2/case=floats",
+ appendFunc: appendV2Float,
+ },
+ {
+ name: "appender=v1/case=floatsHistogramsExemplars",
+ appendFunc: appendV1FloatOrHistogramWithExemplars,
+ },
+ {
+ name: "appender=v2/case=floatsHistogramsExemplars",
+ appendFunc: appendV2FloatOrHistogramWithExemplars,
+ },
+ }
+}
+
+/*
+ export bench=append && go test \
+ -run '^$' -bench '^BenchmarkHeadAppender_AppendCommit$' \
+ -benchtime 5s -count 6 -cpu 2 -timeout 999m \
+ | tee ${bench}.txt
+*/
+func BenchmarkHeadAppender_AppendCommit(b *testing.B) {
+ // NOTE(bwplotka): Previously we also had 1k and 10k series case. There is nothing
+ // special happening in 100 vs 1k vs 10k, so let's save considerable amount of benchmark time
+ // for quicker feedback. In return, we add more sample type cases.
+ // Similarly, we removed the 2 sample in append case.
+ //
+ // TODO(bwplotka): This still takes ~6500s (~2h) for -benchtime 5s -count 6 to complete.
+ // We might want to reduce the time bit more. 5s is really important as the slowest
+ // case (appender=v1/case=floatsHistogramsExemplars/series=100/samples_per_append=100-2)
+ // in 5s yields only 255 iters 23184892 ns/op. Perhaps -benchtime=300x would be better?
+ seriesCounts := []int{10, 100}
+ series := genSeries(100, 10, 0, 0) // Only using the generated labels.
+ for _, appendCase := range appendCases() {
+ for _, seriesCount := range seriesCounts {
+ for _, samplesPerAppend := range []int64{1, 5, 100} {
+ b.Run(fmt.Sprintf("%s/series=%d/samples_per_append=%d", appendCase.name, seriesCount, samplesPerAppend), func(b *testing.B) {
+ opts := newTestHeadDefaultOptions(10000, false)
+ opts.EnableExemplarStorage = true // We benchmark with exemplars, benchmark with them.
+ h, _ := newTestHeadWithOptions(b, compression.None, opts)
+
+ ts := int64(1000)
+
+ // Init series, that's not what we're benchmarking here.
+ app := appendCase.appendFunc(b, h, ts, series[:seriesCount], samplesPerAppend)
+ require.NoError(b, app.Commit())
+ ts += 1000 // should increment more than highest samplesPerAppend
+
+ b.ReportAllocs()
+ b.ResetTimer()
+
+ for b.Loop() {
+ app := appendCase.appendFunc(b, h, ts, series[:seriesCount], samplesPerAppend)
+ require.NoError(b, app.Commit())
+ ts += 1000 // should increment more than highest samplesPerAppend
+ }
+ })
+ }
+ }
+ }
+}
+
func BenchmarkHeadStripeSeriesCreate(b *testing.B) {
chunkDir := b.TempDir()
// Put a series, select it. GC it and then access it.
@@ -86,86 +306,6 @@ func BenchmarkHeadStripeSeriesCreate_PreCreationFailure(b *testing.B) {
}
}
-func BenchmarkHead_WalCommit(b *testing.B) {
- seriesCounts := []int{100, 1000, 10000}
- series := genSeries(10000, 10, 0, 0) // Only using the generated labels.
-
- appendSamples := func(b *testing.B, app storage.Appender, seriesCount int, ts int64) {
- var err error
- for i, s := range series[:seriesCount] {
- var ref storage.SeriesRef
- // if i is even, append a sample, else append a histogram.
- if i%2 == 0 {
- ref, err = app.Append(ref, s.Labels(), ts, float64(ts))
- } else {
- h := &histogram.Histogram{
- Count: 7 + uint64(ts*5),
- ZeroCount: 2 + uint64(ts),
- ZeroThreshold: 0.001,
- Sum: 18.4 * rand.Float64(),
- Schema: 1,
- PositiveSpans: []histogram.Span{
- {Offset: 0, Length: 2},
- {Offset: 1, Length: 2},
- },
- PositiveBuckets: []int64{ts + 1, 1, -1, 0},
- }
- ref, err = app.AppendHistogram(ref, s.Labels(), ts, h, nil)
- }
- require.NoError(b, err)
-
- _, err = app.AppendExemplar(ref, s.Labels(), exemplar.Exemplar{
- Labels: labels.FromStrings("trace_id", strconv.Itoa(rand.Int())),
- Value: rand.Float64(),
- Ts: ts,
- })
- require.NoError(b, err)
- }
- }
-
- for _, seriesCount := range seriesCounts {
- b.Run(fmt.Sprintf("%d series", seriesCount), func(b *testing.B) {
- for _, commits := range []int64{1, 2} { // To test commits that create new series and when the series already exists.
- b.Run(fmt.Sprintf("%d commits", commits), func(b *testing.B) {
- b.ReportAllocs()
- b.ResetTimer()
-
- for b.Loop() {
- b.StopTimer()
- h, w := newTestHead(b, 10000, compression.None, false)
- b.Cleanup(func() {
- if h != nil {
- h.Close()
- }
- if w != nil {
- w.Close()
- }
- })
- app := h.Appender(context.Background())
-
- appendSamples(b, app, seriesCount, 0)
-
- b.StartTimer()
- require.NoError(b, app.Commit())
- if commits == 2 {
- b.StopTimer()
- app = h.Appender(context.Background())
- appendSamples(b, app, seriesCount, 1)
- b.StartTimer()
- require.NoError(b, app.Commit())
- }
- b.StopTimer()
- h.Close()
- h = nil
- w.Close()
- w = nil
- }
- })
- }
- })
- }
-}
-
type failingSeriesLifecycleCallback struct{}
func (failingSeriesLifecycleCallback) PreCreation(labels.Labels) error { return errors.New("failed") }
diff --git a/tsdb/head_dedupelabels.go b/tsdb/head_dedupelabels.go
index a75f337224..f8bcec2e78 100644
--- a/tsdb/head_dedupelabels.go
+++ b/tsdb/head_dedupelabels.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Prometheus Authors
+// 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
diff --git a/tsdb/head_other.go b/tsdb/head_other.go
index 7e1eea8b05..d6d5795e20 100644
--- a/tsdb/head_other.go
+++ b/tsdb/head_other.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Prometheus Authors
+// 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
diff --git a/tsdb/head_read.go b/tsdb/head_read.go
index 8485d65435..f0a1331fbb 100644
--- a/tsdb/head_read.go
+++ b/tsdb/head_read.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
@@ -22,6 +22,7 @@ import (
"sync"
"github.com/prometheus/prometheus/model/labels"
+ "github.com/prometheus/prometheus/model/value"
"github.com/prometheus/prometheus/storage"
"github.com/prometheus/prometheus/tsdb/chunkenc"
"github.com/prometheus/prometheus/tsdb/chunks"
@@ -201,6 +202,112 @@ func (h *headIndexReader) Series(ref storage.SeriesRef, builder *labels.ScratchB
return nil
}
+func (h *Head) staleIndex(mint, maxt int64, staleSeriesRefs []storage.SeriesRef) (*headStaleIndexReader, error) {
+ return &headStaleIndexReader{
+ headIndexReader: h.indexRange(mint, maxt),
+ staleSeriesRefs: staleSeriesRefs,
+ }, nil
+}
+
+// headStaleIndexReader gives the stale series that have no out-of-order data.
+// This is only used for stale series compaction at the moment, that will only ask for all
+// the series during compaction. So to make that efficient, this index reader requires the
+// pre-calculated list of stale series refs that can be returned without re-reading the Head.
+type headStaleIndexReader struct {
+ *headIndexReader
+ staleSeriesRefs []storage.SeriesRef
+}
+
+func (h *headStaleIndexReader) Postings(ctx context.Context, name string, values ...string) (index.Postings, error) {
+ // If all postings are requested, return the precalculated list.
+ k, v := index.AllPostingsKey()
+ if len(h.staleSeriesRefs) > 0 && name == k && len(values) == 1 && values[0] == v {
+ return index.NewListPostings(h.staleSeriesRefs), nil
+ }
+ seriesRefs, err := h.head.filterStaleSeriesAndSortPostings(h.head.postings.Postings(ctx, name, values...))
+ if err != nil {
+ return index.ErrPostings(err), err
+ }
+ return index.NewListPostings(seriesRefs), nil
+}
+
+func (h *headStaleIndexReader) PostingsForLabelMatching(ctx context.Context, name string, match func(string) bool) index.Postings {
+ // Unused for compaction, so we don't need to optimise.
+ seriesRefs, err := h.head.filterStaleSeriesAndSortPostings(h.head.postings.PostingsForLabelMatching(ctx, name, match))
+ if err != nil {
+ return index.ErrPostings(err)
+ }
+ return index.NewListPostings(seriesRefs)
+}
+
+func (h *headStaleIndexReader) PostingsForAllLabelValues(ctx context.Context, name string) index.Postings {
+ // Unused for compaction, so we don't need to optimise.
+ seriesRefs, err := h.head.filterStaleSeriesAndSortPostings(h.head.postings.PostingsForAllLabelValues(ctx, name))
+ if err != nil {
+ return index.ErrPostings(err)
+ }
+ return index.NewListPostings(seriesRefs)
+}
+
+// filterStaleSeriesAndSortPostings returns the stale series references from the given postings
+// that also do not have any out-of-order data.
+func (h *Head) filterStaleSeriesAndSortPostings(p index.Postings) ([]storage.SeriesRef, error) {
+ series := make([]*memSeries, 0, 1024)
+
+ notFoundSeriesCount := 0
+ for p.Next() {
+ s := h.series.getByID(chunks.HeadSeriesRef(p.At()))
+ if s == nil {
+ notFoundSeriesCount++
+ continue
+ }
+
+ s.Lock()
+ if s.ooo != nil {
+ // Has out-of-order data; skip it because we cannot determine if a series
+ // is stale when it's getting out-of-order data.
+ s.Unlock()
+ continue
+ }
+
+ if value.IsStaleNaN(s.lastValue) ||
+ (s.lastHistogramValue != nil && value.IsStaleNaN(s.lastHistogramValue.Sum)) ||
+ (s.lastFloatHistogramValue != nil && value.IsStaleNaN(s.lastFloatHistogramValue.Sum)) {
+ series = append(series, s)
+ }
+ s.Unlock()
+ }
+ if notFoundSeriesCount > 0 {
+ h.logger.Debug("Looked up stale series not found", "count", notFoundSeriesCount)
+ }
+ if err := p.Err(); err != nil {
+ return nil, fmt.Errorf("expand postings: %w", err)
+ }
+
+ slices.SortFunc(series, func(a, b *memSeries) int {
+ return labels.Compare(a.labels(), b.labels())
+ })
+
+ refs := make([]storage.SeriesRef, 0, len(series))
+ for _, p := range series {
+ refs = append(refs, storage.SeriesRef(p.ref))
+ }
+ return refs, nil
+}
+
+// SortedPostings returns the postings as it is because we expect any postings obtained via
+// headStaleIndexReader to be already sorted.
+func (*headStaleIndexReader) SortedPostings(p index.Postings) index.Postings {
+ // All the postings function above already give the sorted list of postings.
+ return p
+}
+
+// SortedStaleSeriesRefsNoOOOData returns all the series refs of the stale series that do not have any out-of-order data.
+func (h *Head) SortedStaleSeriesRefsNoOOOData(ctx context.Context) ([]storage.SeriesRef, error) {
+ k, v := index.AllPostingsKey()
+ return h.filterStaleSeriesAndSortPostings(h.postings.Postings(ctx, k, v))
+}
+
func appendSeriesChunks(s *memSeries, mint, maxt int64, chks []chunks.Meta) []chunks.Meta {
for i, c := range s.mmappedChunks {
// Do not expose chunks that are outside of the specified range.
@@ -261,21 +368,6 @@ func unpackHeadChunkRef(ref chunks.ChunkRef) (seriesID chunks.HeadSeriesRef, chu
return sid, (cid & (oooChunkIDMask - 1)), (cid & oooChunkIDMask) != 0
}
-// LabelValueFor returns label value for the given label name in the series referred to by ID.
-func (h *headIndexReader) LabelValueFor(_ context.Context, id storage.SeriesRef, label string) (string, error) {
- memSeries := h.head.series.getByID(chunks.HeadSeriesRef(id))
- if memSeries == nil {
- return "", storage.ErrNotFound
- }
-
- value := memSeries.labels().Get(label)
- if value == "" {
- return "", storage.ErrNotFound
- }
-
- return value, nil
-}
-
// LabelNamesFor returns all the label names for the series referred to by the postings.
// The names returned are sorted.
func (h *headIndexReader) LabelNamesFor(ctx context.Context, series index.Postings) ([]string, error) {
diff --git a/tsdb/head_read_test.go b/tsdb/head_read_test.go
index b9f1700706..cf55973a01 100644
--- a/tsdb/head_read_test.go
+++ b/tsdb/head_read_test.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
diff --git a/tsdb/head_test.go b/tsdb/head_test.go
index 29d9ad8cbd..142fbc18e7 100644
--- a/tsdb/head_test.go
+++ b/tsdb/head_test.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
@@ -44,6 +44,7 @@ import (
"github.com/prometheus/prometheus/model/exemplar"
"github.com/prometheus/prometheus/model/histogram"
"github.com/prometheus/prometheus/model/labels"
+ "github.com/prometheus/prometheus/model/timestamp"
"github.com/prometheus/prometheus/model/value"
"github.com/prometheus/prometheus/storage"
"github.com/prometheus/prometheus/tsdb/chunkenc"
@@ -56,6 +57,7 @@ import (
"github.com/prometheus/prometheus/tsdb/wlog"
"github.com/prometheus/prometheus/util/compression"
"github.com/prometheus/prometheus/util/testutil"
+ "github.com/prometheus/prometheus/util/testutil/synctest"
)
// newTestHeadDefaultOptions returns the HeadOptions that should be used by default in unit tests.
@@ -84,6 +86,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 +103,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()
@@ -107,49 +112,6 @@ func BenchmarkCreateSeries(b *testing.B) {
}
}
-func BenchmarkHeadAppender_Append_Commit_ExistingSeries(b *testing.B) {
- seriesCounts := []int{100, 1000, 10000}
- series := genSeries(10000, 10, 0, 0)
-
- for _, seriesCount := range seriesCounts {
- b.Run(fmt.Sprintf("%d series", seriesCount), func(b *testing.B) {
- for _, samplesPerAppend := range []int64{1, 2, 5, 100} {
- b.Run(fmt.Sprintf("%d samples per append", samplesPerAppend), func(b *testing.B) {
- h, _ := newTestHead(b, 10000, compression.None, false)
- b.Cleanup(func() { require.NoError(b, h.Close()) })
-
- ts := int64(1000)
- appendSamples := func() error {
- var err error
- app := h.Appender(context.Background())
- for _, s := range series[:seriesCount] {
- var ref storage.SeriesRef
- for sampleIndex := range samplesPerAppend {
- ref, err = app.Append(ref, s.Labels(), ts+sampleIndex, float64(ts+sampleIndex))
- if err != nil {
- return err
- }
- }
- }
- ts += 1000 // should increment more than highest samplesPerAppend
- return app.Commit()
- }
-
- // Init series, that's not what we're benchmarking here.
- require.NoError(b, appendSamples())
-
- b.ReportAllocs()
- b.ResetTimer()
-
- for b.Loop() {
- require.NoError(b, appendSamples())
- }
- })
- }
- })
- }
-}
-
func populateTestWL(t testing.TB, w *wlog.WL, recs []any, buf []byte) []byte {
var enc record.Encoder
for _, r := range recs {
@@ -510,198 +472,242 @@ func BenchmarkLoadRealWLs(b *testing.B) {
}
}
+// TestHead_InitAppenderRace_ErrOutOfBounds tests against init races with maxTime vs minTime on empty head concurrent appends.
+// See: https://github.com/prometheus/prometheus/pull/17963
+func TestHead_InitAppenderRace_ErrOutOfBounds(t *testing.T) {
+ head, _ := newTestHead(t, DefaultBlockDuration, compression.None, false)
+ require.NoError(t, head.Init(0))
+
+ ts := timestamp.FromTime(time.Now())
+ appendCycles := 100
+
+ g, ctx := errgroup.WithContext(t.Context())
+ var wg sync.WaitGroup
+ wg.Add(1)
+
+ for i := range 100 {
+ g.Go(func() error {
+ appends := 0
+ wg.Wait()
+ for ctx.Err() == nil && appends < appendCycles {
+ appends++
+ app := head.Appender(t.Context())
+ if _, err := app.Append(0, labels.FromStrings("__name__", strconv.Itoa(i)), ts, float64(ts)); err != nil {
+ return fmt.Errorf("error when appending to head: %w", err)
+ }
+ if err := app.Rollback(); err != nil {
+ return err
+ }
+ }
+ return nil
+ })
+ }
+ wg.Done()
+ require.NoError(t, g.Wait())
+}
+
// TestHead_HighConcurrencyReadAndWrite generates 1000 series with a step of 15s and fills a whole block with samples,
// this means in total it generates 4000 chunks because with a step of 15s there are 4 chunks per block per series.
// While appending the samples to the head it concurrently queries them from multiple go routines and verifies that the
// returned results are correct.
func TestHead_HighConcurrencyReadAndWrite(t *testing.T) {
- head, _ := newTestHead(t, DefaultBlockDuration, compression.None, false)
- defer func() {
- require.NoError(t, head.Close())
- }()
+ for _, appV2 := range []bool{false, true} {
+ t.Run(fmt.Sprintf("appV2=%v", appV2), func(t *testing.T) {
+ head, _ := newTestHead(t, DefaultBlockDuration, compression.None, false)
- seriesCnt := 1000
- readConcurrency := 2
- writeConcurrency := 10
- startTs := uint64(DefaultBlockDuration) // start at the second block relative to the unix epoch.
- qryRange := uint64(5 * time.Minute.Milliseconds())
- step := uint64(15 * time.Second / time.Millisecond)
- endTs := startTs + uint64(DefaultBlockDuration)
+ seriesCnt := 1000
+ readConcurrency := 2
+ writeConcurrency := 10
+ startTs := uint64(DefaultBlockDuration) // Start at the second block relative to the unix epoch.
+ qryRange := uint64(5 * time.Minute.Milliseconds())
+ step := uint64(15 * time.Second / time.Millisecond)
+ endTs := startTs + uint64(DefaultBlockDuration)
- labelSets := make([]labels.Labels, seriesCnt)
- for i := range seriesCnt {
- labelSets[i] = labels.FromStrings("seriesId", strconv.Itoa(i))
- }
-
- head.Init(0)
-
- g, ctx := errgroup.WithContext(context.Background())
- whileNotCanceled := func(f func() (bool, error)) error {
- for ctx.Err() == nil {
- cont, err := f()
- if err != nil {
- return err
+ labelSets := make([]labels.Labels, seriesCnt)
+ for i := range seriesCnt {
+ labelSets[i] = labels.FromStrings("seriesId", strconv.Itoa(i))
}
- if !cont {
+ require.NoError(t, head.Init(0))
+
+ g, ctx := errgroup.WithContext(t.Context())
+ whileNotCanceled := func(f func() (bool, error)) error {
+ for ctx.Err() == nil {
+ cont, err := f()
+ if err != nil {
+ return err
+ }
+ if !cont {
+ return nil
+ }
+ }
return nil
}
- }
- return nil
- }
- // Create one channel for each write worker, the channels will be used by the coordinator
- // go routine to coordinate which timestamps each write worker has to write.
- writerTsCh := make([]chan uint64, writeConcurrency)
- for writerTsChIdx := range writerTsCh {
- writerTsCh[writerTsChIdx] = make(chan uint64)
- }
+ // Create one channel for each write worker, the channels will be used by the coordinator
+ // go routine to coordinate which timestamps each write worker has to write.
+ writerTsCh := make([]chan uint64, writeConcurrency)
+ for writerTsChIdx := range writerTsCh {
+ writerTsCh[writerTsChIdx] = make(chan uint64)
+ }
- // workerReadyWg is used to synchronize the start of the test,
- // we only start the test once all workers signal that they're ready.
- var workerReadyWg sync.WaitGroup
- workerReadyWg.Add(writeConcurrency + readConcurrency)
+ // workerReadyWg is used to synchronize the start of the test,
+ // we only start the test once all workers signal that they're ready.
+ var workerReadyWg sync.WaitGroup
+ workerReadyWg.Add(writeConcurrency + readConcurrency)
- // Start the write workers.
- for wid := range writeConcurrency {
- // Create copy of workerID to be used by worker routine.
- workerID := wid
+ // Start the write workers.
+ for wid := range writeConcurrency {
+ // Create copy of workerID to be used by worker routine.
+ workerID := wid
- g.Go(func() error {
- // The label sets which this worker will write.
- workerLabelSets := labelSets[(seriesCnt/writeConcurrency)*workerID : (seriesCnt/writeConcurrency)*(workerID+1)]
+ g.Go(func() error {
+ // The label sets which this worker will write.
+ workerLabelSets := labelSets[(seriesCnt/writeConcurrency)*workerID : (seriesCnt/writeConcurrency)*(workerID+1)]
- // Signal that this worker is ready.
- workerReadyWg.Done()
+ // Signal that this worker is ready.
+ workerReadyWg.Done()
- return whileNotCanceled(func() (bool, error) {
- ts, ok := <-writerTsCh[workerID]
- if !ok {
- return false, nil
- }
+ return whileNotCanceled(func() (bool, error) {
+ ts, ok := <-writerTsCh[workerID]
+ if !ok {
+ return false, nil
+ }
- app := head.Appender(ctx)
- for i := range workerLabelSets {
- // We also use the timestamp as the sample value.
- _, err := app.Append(0, workerLabelSets[i], int64(ts), float64(ts))
- if err != nil {
- return false, fmt.Errorf("Error when appending to head: %w", err)
- }
- }
+ if appV2 {
+ app := head.AppenderV2(ctx)
+ for i := range workerLabelSets {
+ // We also use the timestamp as the sample value.
+ if _, err := app.Append(0, workerLabelSets[i], 0, int64(ts), float64(ts), nil, nil, storage.AOptions{}); err != nil {
+ return false, fmt.Errorf("error when appending (V2) to head: %w", err)
+ }
+ }
+ return true, app.Commit()
+ }
- return true, app.Commit()
- })
- })
- }
-
- // queryHead is a helper to query the head for a given time range and labelset.
- queryHead := func(mint, maxt uint64, label labels.Label) (map[string][]chunks.Sample, error) {
- q, err := NewBlockQuerier(head, int64(mint), int64(maxt))
- if err != nil {
- return nil, err
- }
- return query(t, q, labels.MustNewMatcher(labels.MatchEqual, label.Name, label.Value)), nil
- }
-
- // readerTsCh will be used by the coordinator go routine to coordinate which timestamps the reader should read.
- readerTsCh := make(chan uint64)
-
- // Start the read workers.
- for wid := range readConcurrency {
- // Create copy of threadID to be used by worker routine.
- workerID := wid
-
- g.Go(func() error {
- querySeriesRef := (seriesCnt / readConcurrency) * workerID
-
- // Signal that this worker is ready.
- workerReadyWg.Done()
-
- return whileNotCanceled(func() (bool, error) {
- ts, ok := <-readerTsCh
- if !ok {
- return false, nil
- }
-
- querySeriesRef = (querySeriesRef + 1) % seriesCnt
- lbls := labelSets[querySeriesRef]
- // lbls has a single entry; extract it so we can run a query.
- var lbl labels.Label
- lbls.Range(func(l labels.Label) {
- lbl = l
+ app := head.Appender(ctx)
+ for i := range workerLabelSets {
+ // We also use the timestamp as the sample value.
+ if _, err := app.Append(0, workerLabelSets[i], int64(ts), float64(ts)); err != nil {
+ return false, fmt.Errorf("error when appending to head: %w", err)
+ }
+ }
+ return true, app.Commit()
+ })
})
- samples, err := queryHead(ts-qryRange, ts, lbl)
+ }
+
+ // queryHead is a helper to query the head for a given time range and labelset.
+ queryHead := func(mint, maxt uint64, label labels.Label) (map[string][]chunks.Sample, error) {
+ q, err := NewBlockQuerier(head, int64(mint), int64(maxt))
if err != nil {
- return false, err
+ return nil, err
}
+ return query(t, q, labels.MustNewMatcher(labels.MatchEqual, label.Name, label.Value)), nil
+ }
- if len(samples) != 1 {
- return false, fmt.Errorf("expected 1 series, got %d", len(samples))
- }
+ // readerTsCh will be used by the coordinator go routine to coordinate which timestamps the reader should read.
+ readerTsCh := make(chan uint64)
- series := lbls.String()
- expectSampleCnt := qryRange/step + 1
- if expectSampleCnt != uint64(len(samples[series])) {
- return false, fmt.Errorf("expected %d samples, got %d", expectSampleCnt, len(samples[series]))
- }
+ // Start the read workers.
+ for wid := range readConcurrency {
+ // Create copy of threadID to be used by worker routine.
+ workerID := wid
- for sampleIdx, sample := range samples[series] {
- expectedValue := ts - qryRange + (uint64(sampleIdx) * step)
- if sample.T() != int64(expectedValue) {
- return false, fmt.Errorf("expected sample %d to have ts %d, got %d", sampleIdx, expectedValue, sample.T())
+ g.Go(func() error {
+ querySeriesRef := (seriesCnt / readConcurrency) * workerID
+
+ // Signal that this worker is ready.
+ workerReadyWg.Done()
+
+ return whileNotCanceled(func() (bool, error) {
+ ts, ok := <-readerTsCh
+ if !ok {
+ return false, nil
+ }
+
+ querySeriesRef = (querySeriesRef + 1) % seriesCnt
+ lbls := labelSets[querySeriesRef]
+ // lbls has a single entry; extract it so we can run a query.
+ var lbl labels.Label
+ lbls.Range(func(l labels.Label) {
+ lbl = l
+ })
+ samples, err := queryHead(ts-qryRange, ts, lbl)
+ if err != nil {
+ return false, err
+ }
+
+ if len(samples) != 1 {
+ return false, fmt.Errorf("expected 1 series, got %d", len(samples))
+ }
+
+ series := lbls.String()
+ expectSampleCnt := qryRange/step + 1
+ if expectSampleCnt != uint64(len(samples[series])) {
+ return false, fmt.Errorf("expected %d samples, got %d", expectSampleCnt, len(samples[series]))
+ }
+
+ for sampleIdx, sample := range samples[series] {
+ expectedValue := ts - qryRange + (uint64(sampleIdx) * step)
+ if sample.T() != int64(expectedValue) {
+ return false, fmt.Errorf("expected sample %d to have ts %d, got %d", sampleIdx, expectedValue, sample.T())
+ }
+ if sample.F() != float64(expectedValue) {
+ return false, fmt.Errorf("expected sample %d to have value %d, got %f", sampleIdx, expectedValue, sample.F())
+ }
+ }
+
+ return true, nil
+ })
+ })
+ }
+
+ // Start the coordinator go routine.
+ g.Go(func() error {
+ currTs := startTs
+
+ defer func() {
+ // End of the test, close all channels to stop the workers.
+ for _, ch := range writerTsCh {
+ close(ch)
}
- if sample.F() != float64(expectedValue) {
- return false, fmt.Errorf("expected sample %d to have value %d, got %f", sampleIdx, expectedValue, sample.F())
- }
- }
+ close(readerTsCh)
+ }()
- return true, nil
+ // Wait until all workers are ready to start the test.
+ workerReadyWg.Wait()
+
+ return whileNotCanceled(func() (bool, error) {
+ // Send the current timestamp to each of the writers.
+ for _, ch := range writerTsCh {
+ select {
+ case ch <- currTs:
+ case <-ctx.Done():
+ return false, nil
+ }
+ }
+
+ // Once data for at least has been ingested, send the current timestamp to the readers.
+ if currTs > startTs+qryRange {
+ select {
+ case readerTsCh <- currTs - step:
+ case <-ctx.Done():
+ return false, nil
+ }
+ }
+
+ currTs += step
+ if currTs > endTs {
+ return false, nil
+ }
+
+ return true, nil
+ })
})
+
+ require.NoError(t, g.Wait())
})
}
-
- // Start the coordinator go routine.
- g.Go(func() error {
- currTs := startTs
-
- defer func() {
- // End of the test, close all channels to stop the workers.
- for _, ch := range writerTsCh {
- close(ch)
- }
- close(readerTsCh)
- }()
-
- // Wait until all workers are ready to start the test.
- workerReadyWg.Wait()
- return whileNotCanceled(func() (bool, error) {
- // Send the current timestamp to each of the writers.
- for _, ch := range writerTsCh {
- select {
- case ch <- currTs:
- case <-ctx.Done():
- return false, nil
- }
- }
-
- // Once data for at least has been ingested, send the current timestamp to the readers.
- if currTs > startTs+qryRange {
- select {
- case readerTsCh <- currTs - step:
- case <-ctx.Done():
- return false, nil
- }
- }
-
- currTs += step
- if currTs > endTs {
- return false, nil
- }
-
- return true, nil
- })
- })
-
- require.NoError(t, g.Wait())
}
func TestHead_ReadWAL(t *testing.T) {
@@ -746,9 +752,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)
@@ -788,7 +791,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)
@@ -801,14 +804,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)
@@ -884,8 +887,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)
}
@@ -1099,9 +1102,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())
@@ -1152,7 +1152,7 @@ func TestHead_KeepSeriesInWALCheckpoint(t *testing.T) {
{
name: "keep series still in the head",
prepare: func(t *testing.T, h *Head) {
- _, _, err := h.getOrCreateWithID(chunks.HeadSeriesRef(existingRef), existingLbls.Hash(), existingLbls, false)
+ _, _, err := h.getOrCreateWithOptionalID(chunks.HeadSeriesRef(existingRef), existingLbls.Hash(), existingLbls, false)
require.NoError(t, err)
},
expected: true,
@@ -1177,9 +1177,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)
@@ -1195,7 +1192,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))
@@ -1228,7 +1224,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
@@ -1271,7 +1266,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))
@@ -1310,7 +1304,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
@@ -1320,9 +1313,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)
@@ -1389,9 +1379,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)
@@ -1714,9 +1701,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)
@@ -1861,9 +1845,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())
@@ -1902,7 +1883,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())
@@ -2006,9 +1987,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 {
@@ -2019,7 +1997,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
@@ -2374,9 +2352,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,
@@ -2433,9 +2408,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,
@@ -2492,9 +2464,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)
@@ -2522,9 +2491,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)
@@ -2555,9 +2521,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)
@@ -2577,9 +2540,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)
@@ -2850,9 +2810,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)
@@ -2880,9 +2837,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())
@@ -3078,9 +3032,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)
@@ -3109,9 +3060,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)
@@ -3146,9 +3094,6 @@ func TestIsolationAppendIDZeroIsNoop(t *testing.T) {
}
h, _ := newTestHead(t, 1000, compression.None, false)
- defer func() {
- require.NoError(t, h.Close())
- }()
h.initTime(0)
@@ -3178,9 +3123,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())
@@ -3300,9 +3242,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())
@@ -3320,12 +3259,10 @@ func testHeadSeriesChunkRace(t *testing.T) {
defer q.Close()
var wg sync.WaitGroup
- wg.Add(1)
- go func() {
- defer wg.Done()
+ wg.Go(func() {
h.updateMinMaxTime(20, 25)
h.gc()
- }()
+ })
ss := q.Select(context.Background(), false, nil, matcher)
for ss.Next() {
}
@@ -3335,9 +3272,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
@@ -3396,7 +3330,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()
@@ -3472,9 +3405,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 {
@@ -3542,9 +3472,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()
@@ -3605,9 +3532,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)
@@ -3668,8 +3592,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) {
@@ -3691,13 +3613,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()
@@ -3826,13 +3746,11 @@ func TestChunkNotFoundHeadGCRace(t *testing.T) {
s := ss.At()
var wg sync.WaitGroup
- wg.Add(1)
- go func() {
- defer wg.Done()
+ wg.Go(func() {
// Compacting head while the querier spans the compaction time.
require.NoError(t, db.Compact(ctx))
require.NotEmpty(t, db.Blocks())
- }()
+ })
// Give enough time for compaction to finish.
// We expect it to be blocked until querier is closed.
@@ -3881,7 +3799,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())
@@ -3890,13 +3808,11 @@ func TestDataMissingOnQueryDuringCompaction(t *testing.T) {
require.NoError(t, err)
var wg sync.WaitGroup
- wg.Add(1)
- go func() {
- defer wg.Done()
+ wg.Go(func() {
// Compacting head while the querier spans the compaction time.
require.NoError(t, db.Compact(ctx))
require.NotEmpty(t, db.Blocks())
- }()
+ })
// Give enough time for compaction to finish.
// We expect it to be blocked until querier is closed.
@@ -3988,17 +3904,35 @@ func TestWaitForPendingReadersInTimeRange(t *testing.T) {
}
for _, c := range cases {
t.Run(fmt.Sprintf("mint=%d,maxt=%d,shouldWait=%t", c.mint, c.maxt, c.shouldWait), func(t *testing.T) {
+ // checkWaiting verifies WaitForPendingReadersInTimeRange behavior using synctest
+ // for deterministic time control. The function should block while an overlapping
+ // querier is open and return immediately when there's no overlap.
checkWaiting := func(cl io.Closer) {
- var waitOver atomic.Bool
- go func() {
- db.head.WaitForPendingReadersInTimeRange(truncMint, truncMaxt)
- waitOver.Store(true)
- }()
- <-time.After(550 * time.Millisecond)
- require.Equal(t, !c.shouldWait, waitOver.Load())
- require.NoError(t, cl.Close())
- <-time.After(550 * time.Millisecond)
- require.True(t, waitOver.Load())
+ synctest.Test(t, func(t *testing.T) {
+ var waitOver atomic.Bool
+ go func() {
+ db.head.WaitForPendingReadersInTimeRange(truncMint, truncMaxt)
+ waitOver.Store(true)
+ }()
+
+ // Wait for goroutine to either complete (no overlap) or block on Sleep (overlap).
+ synctest.Wait()
+
+ if c.shouldWait {
+ require.False(t, waitOver.Load(),
+ "WaitForPendingReadersInTimeRange should block while overlapping querier is open")
+ require.NoError(t, cl.Close())
+ // Advance fake time past the 500ms poll interval, then let goroutine process.
+ time.Sleep(time.Second)
+ synctest.Wait()
+ require.True(t, waitOver.Load(),
+ "WaitForPendingReadersInTimeRange should complete after querier is closed")
+ } else {
+ require.True(t, waitOver.Load(),
+ "WaitForPendingReadersInTimeRange should return immediately when no overlap")
+ require.NoError(t, cl.Close())
+ }
+ })
}
q, err := db.Querier(c.mint, c.maxt)
@@ -4143,9 +4077,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)
@@ -4248,7 +4179,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))
@@ -4395,9 +4327,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)
@@ -4546,17 +4479,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)
@@ -4620,17 +4553,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)
@@ -5723,9 +5656,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))
@@ -5770,6 +5700,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)
@@ -5941,7 +5874,7 @@ func TestOOOAppendWithNoSeries(t *testing.T) {
}
}
-func testOOOAppendWithNoSeries(t *testing.T, appendFunc func(appender storage.Appender, lbls labels.Labels, ts, value int64) (storage.SeriesRef, sample, error)) {
+func testOOOAppendWithNoSeries(t *testing.T, appendFunc func(appender storage.LimitedAppenderV1, lbls labels.Labels, ts, value int64) (storage.SeriesRef, sample, error)) {
dir := t.TempDir()
wal, err := wlog.NewSize(nil, nil, filepath.Join(dir, "wal"), 32768, compression.Snappy)
require.NoError(t, err)
@@ -6284,6 +6217,7 @@ func TestSnapshotAheadOfWALError(t *testing.T) {
require.NoError(t, head.Close())
}
+// TODO(bwplotka): Bad benchmark (no b.Loop/b.N), fix or remove.
func BenchmarkCuttingHeadHistogramChunks(b *testing.B) {
const (
numSamples = 50000
@@ -6409,9 +6343,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())
@@ -6477,9 +6408,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
@@ -6525,28 +6453,25 @@ func TestWALSampleAndExemplarOrder(t *testing.T) {
appendF: func(app storage.Appender, ts int64) (storage.SeriesRef, error) {
return app.Append(0, lbls, ts, 1.0)
},
- expectedType: reflect.TypeOf([]record.RefSample{}),
+ expectedType: reflect.TypeFor[[]record.RefSample](),
},
"histogram sample": {
appendF: func(app storage.Appender, ts int64) (storage.SeriesRef, error) {
return app.AppendHistogram(0, lbls, ts, tsdbutil.GenerateTestHistogram(1), nil)
},
- expectedType: reflect.TypeOf([]record.RefHistogramSample{}),
+ expectedType: reflect.TypeFor[[]record.RefHistogramSample](),
},
"float histogram sample": {
appendF: func(app storage.Appender, ts int64) (storage.SeriesRef, error) {
return app.AppendHistogram(0, lbls, ts, nil, tsdbutil.GenerateTestFloatHistogram(1))
},
- expectedType: reflect.TypeOf([]record.RefFloatHistogramSample{}),
+ expectedType: reflect.TypeFor[[]record.RefFloatHistogramSample](),
},
}
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)
@@ -6579,6 +6504,8 @@ func TestWALSampleAndExemplarOrder(t *testing.T) {
// would trigger the
// `signal SIGSEGV: segmentation violation code=0x1 addr=0x20 pc=0xbb03d1`
// panic, that we have seen in the wild once.
+//
+// TODO(bwplotka): This no longer can happen in AppenderV2, remove once AppenderV1 is removed, see #17632.
func TestHeadCompactionWhileAppendAndCommitExemplar(t *testing.T) {
h, _ := newTestHead(t, DefaultBlockDuration, compression.None, false)
app := h.Appender(context.Background())
@@ -6592,7 +6519,6 @@ func TestHeadCompactionWhileAppendAndCommitExemplar(t *testing.T) {
require.NoError(t, err)
h.Truncate(10)
app.Commit()
- h.Close()
}
func labelsWithHashCollision() (labels.Labels, labels.Labels) {
@@ -6627,18 +6553,12 @@ func stripeSeriesWithCollidingSeries(t *testing.T) (*stripeSeries, *memSeries, *
hash := lbls1.Hash()
s := newStripeSeries(1, noopSeriesLifecycleCallback{})
- got, created, err := s.getOrSet(hash, lbls1, func() *memSeries {
- return &ms1
- })
- require.NoError(t, err)
+ got, created := s.setUnlessAlreadySet(hash, lbls1, &ms1)
require.True(t, created)
require.Same(t, &ms1, got)
// Add a conflicting series
- got, created, err = s.getOrSet(hash, lbls2, func() *memSeries {
- return &ms2
- })
- require.NoError(t, err)
+ got, created = s.setUnlessAlreadySet(hash, lbls2, &ms2)
require.True(t, created)
require.Same(t, &ms2, got)
@@ -6660,7 +6580,7 @@ func TestStripeSeries_gc(t *testing.T) {
s, ms1, ms2 := stripeSeriesWithCollidingSeries(t)
hash := ms1.lset.Hash()
- s.gc(0, 0, nil)
+ s.gc(0, 0)
// Verify that we can get neither ms1 nor ms2 after gc-ing corresponding series
got := s.getByHash(hash, ms1.lset)
@@ -6694,7 +6614,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")
@@ -6721,7 +6640,7 @@ func TestHeadAppender_AppendFloatWithSameTimestampAsPreviousHistogram(t *testing
require.ErrorIs(t, err, storage.NewDuplicateHistogramToFloatErr(2_000, 10.0))
}
-func TestHeadAppender_AppendCT(t *testing.T) {
+func TestHeadAppender_AppendST(t *testing.T) {
testHistogram := tsdbutil.GenerateTestHistogram(1)
testHistogram.CounterResetHint = histogram.NotCounterReset
testFloatHistogram := tsdbutil.GenerateTestFloatHistogram(1)
@@ -6749,7 +6668,7 @@ func TestHeadAppender_AppendCT(t *testing.T) {
fSample float64
h *histogram.Histogram
fh *histogram.FloatHistogram
- ct int64
+ st int64
}
for _, tc := range []struct {
name string
@@ -6759,8 +6678,8 @@ func TestHeadAppender_AppendCT(t *testing.T) {
{
name: "In order ct+normal sample/floatSample",
appendableSamples: []appendableSamples{
- {ts: 100, fSample: 10, ct: 1},
- {ts: 101, fSample: 10, ct: 1},
+ {ts: 100, fSample: 10, st: 1},
+ {ts: 101, fSample: 10, st: 1},
},
expectedSamples: []chunks.Sample{
sample{t: 1, f: 0},
@@ -6771,8 +6690,8 @@ func TestHeadAppender_AppendCT(t *testing.T) {
{
name: "In order ct+normal sample/histogram",
appendableSamples: []appendableSamples{
- {ts: 100, h: testHistogram, ct: 1},
- {ts: 101, h: testHistogram, ct: 1},
+ {ts: 100, h: testHistogram, st: 1},
+ {ts: 101, h: testHistogram, st: 1},
},
expectedSamples: func() []chunks.Sample {
return []chunks.Sample{
@@ -6785,8 +6704,8 @@ func TestHeadAppender_AppendCT(t *testing.T) {
{
name: "In order ct+normal sample/floathistogram",
appendableSamples: []appendableSamples{
- {ts: 100, fh: testFloatHistogram, ct: 1},
- {ts: 101, fh: testFloatHistogram, ct: 1},
+ {ts: 100, fh: testFloatHistogram, st: 1},
+ {ts: 101, fh: testFloatHistogram, st: 1},
},
expectedSamples: func() []chunks.Sample {
return []chunks.Sample{
@@ -6797,10 +6716,10 @@ func TestHeadAppender_AppendCT(t *testing.T) {
}(),
},
{
- name: "Consecutive appends with same ct ignore ct/floatSample",
+ name: "Consecutive appends with same st ignore st/floatSample",
appendableSamples: []appendableSamples{
- {ts: 100, fSample: 10, ct: 1},
- {ts: 101, fSample: 10, ct: 1},
+ {ts: 100, fSample: 10, st: 1},
+ {ts: 101, fSample: 10, st: 1},
},
expectedSamples: []chunks.Sample{
sample{t: 1, f: 0},
@@ -6809,10 +6728,10 @@ func TestHeadAppender_AppendCT(t *testing.T) {
},
},
{
- name: "Consecutive appends with same ct ignore ct/histogram",
+ name: "Consecutive appends with same st ignore st/histogram",
appendableSamples: []appendableSamples{
- {ts: 100, h: testHistogram, ct: 1},
- {ts: 101, h: testHistogram, ct: 1},
+ {ts: 100, h: testHistogram, st: 1},
+ {ts: 101, h: testHistogram, st: 1},
},
expectedSamples: func() []chunks.Sample {
return []chunks.Sample{
@@ -6823,10 +6742,10 @@ func TestHeadAppender_AppendCT(t *testing.T) {
}(),
},
{
- name: "Consecutive appends with same ct ignore ct/floathistogram",
+ name: "Consecutive appends with same st ignore st/floathistogram",
appendableSamples: []appendableSamples{
- {ts: 100, fh: testFloatHistogram, ct: 1},
- {ts: 101, fh: testFloatHistogram, ct: 1},
+ {ts: 100, fh: testFloatHistogram, st: 1},
+ {ts: 101, fh: testFloatHistogram, st: 1},
},
expectedSamples: func() []chunks.Sample {
return []chunks.Sample{
@@ -6837,10 +6756,10 @@ func TestHeadAppender_AppendCT(t *testing.T) {
}(),
},
{
- name: "Consecutive appends with newer ct do not ignore ct/floatSample",
+ name: "Consecutive appends with newer st do not ignore st/floatSample",
appendableSamples: []appendableSamples{
- {ts: 100, fSample: 10, ct: 1},
- {ts: 102, fSample: 10, ct: 101},
+ {ts: 100, fSample: 10, st: 1},
+ {ts: 102, fSample: 10, st: 101},
},
expectedSamples: []chunks.Sample{
sample{t: 1, f: 0},
@@ -6850,10 +6769,10 @@ func TestHeadAppender_AppendCT(t *testing.T) {
},
},
{
- name: "Consecutive appends with newer ct do not ignore ct/histogram",
+ name: "Consecutive appends with newer st do not ignore st/histogram",
appendableSamples: []appendableSamples{
- {ts: 100, h: testHistogram, ct: 1},
- {ts: 102, h: testHistogram, ct: 101},
+ {ts: 100, h: testHistogram, st: 1},
+ {ts: 102, h: testHistogram, st: 101},
},
expectedSamples: []chunks.Sample{
sample{t: 1, h: testZeroHistogram},
@@ -6863,10 +6782,10 @@ func TestHeadAppender_AppendCT(t *testing.T) {
},
},
{
- name: "Consecutive appends with newer ct do not ignore ct/floathistogram",
+ name: "Consecutive appends with newer st do not ignore st/floathistogram",
appendableSamples: []appendableSamples{
- {ts: 100, fh: testFloatHistogram, ct: 1},
- {ts: 102, fh: testFloatHistogram, ct: 101},
+ {ts: 100, fh: testFloatHistogram, st: 1},
+ {ts: 102, fh: testFloatHistogram, st: 101},
},
expectedSamples: []chunks.Sample{
sample{t: 1, fh: testZeroFloatHistogram},
@@ -6876,10 +6795,10 @@ func TestHeadAppender_AppendCT(t *testing.T) {
},
},
{
- name: "CT equals to previous sample timestamp is ignored/floatSample",
+ name: "ST equals to previous sample timestamp is ignored/floatSample",
appendableSamples: []appendableSamples{
- {ts: 100, fSample: 10, ct: 1},
- {ts: 101, fSample: 10, ct: 100},
+ {ts: 100, fSample: 10, st: 1},
+ {ts: 101, fSample: 10, st: 100},
},
expectedSamples: []chunks.Sample{
sample{t: 1, f: 0},
@@ -6888,10 +6807,10 @@ func TestHeadAppender_AppendCT(t *testing.T) {
},
},
{
- name: "CT equals to previous sample timestamp is ignored/histogram",
+ name: "ST equals to previous sample timestamp is ignored/histogram",
appendableSamples: []appendableSamples{
- {ts: 100, h: testHistogram, ct: 1},
- {ts: 101, h: testHistogram, ct: 100},
+ {ts: 100, h: testHistogram, st: 1},
+ {ts: 101, h: testHistogram, st: 100},
},
expectedSamples: func() []chunks.Sample {
return []chunks.Sample{
@@ -6902,10 +6821,10 @@ func TestHeadAppender_AppendCT(t *testing.T) {
}(),
},
{
- name: "CT equals to previous sample timestamp is ignored/floathistogram",
+ name: "ST equals to previous sample timestamp is ignored/floathistogram",
appendableSamples: []appendableSamples{
- {ts: 100, fh: testFloatHistogram, ct: 1},
- {ts: 101, fh: testFloatHistogram, ct: 100},
+ {ts: 100, fh: testFloatHistogram, st: 1},
+ {ts: 101, fh: testFloatHistogram, st: 100},
},
expectedSamples: func() []chunks.Sample {
return []chunks.Sample{
@@ -6918,15 +6837,12 @@ func TestHeadAppender_AppendCT(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 {
// Append float if it's a float test case
if sample.fSample != 0 {
- _, err := a.AppendCTZeroSample(0, lbls, sample.ts, sample.ct)
+ _, err := a.AppendSTZeroSample(0, lbls, sample.ts, sample.st)
require.NoError(t, err)
_, err = a.Append(0, lbls, sample.ts, sample.fSample)
require.NoError(t, err)
@@ -6934,7 +6850,7 @@ func TestHeadAppender_AppendCT(t *testing.T) {
// Append histograms if it's a histogram test case
if sample.h != nil || sample.fh != nil {
- ref, err := a.AppendHistogramCTZeroSample(0, lbls, sample.ts, sample.ct, sample.h, sample.fh)
+ ref, err := a.AppendHistogramSTZeroSample(0, lbls, sample.ts, sample.st, sample.h, sample.fh)
require.NoError(t, err)
_, err = a.AppendHistogram(ref, lbls, sample.ts, sample.h, sample.fh)
require.NoError(t, err)
@@ -6950,12 +6866,12 @@ func TestHeadAppender_AppendCT(t *testing.T) {
}
}
-func TestHeadAppender_AppendHistogramCTZeroSample(t *testing.T) {
+func TestHeadAppender_AppendHistogramSTZeroSample(t *testing.T) {
type appendableSamples struct {
ts int64
h *histogram.Histogram
fh *histogram.FloatHistogram
- ct int64 // 0 if no created timestamp.
+ st int64 // 0 if no created timestamp.
}
for _, tc := range []struct {
name string
@@ -6963,32 +6879,32 @@ func TestHeadAppender_AppendHistogramCTZeroSample(t *testing.T) {
expectedError error
}{
{
- name: "integer histogram CT lower than minValidTime initiates ErrOutOfBounds",
+ name: "integer histogram ST lower than minValidTime initiates ErrOutOfBounds",
appendableSamples: []appendableSamples{
- {ts: 100, h: tsdbutil.GenerateTestHistogram(1), ct: -1},
+ {ts: 100, h: tsdbutil.GenerateTestHistogram(1), st: -1},
},
expectedError: storage.ErrOutOfBounds,
},
{
- name: "float histograms CT lower than minValidTime initiates ErrOutOfBounds",
+ name: "float histograms ST lower than minValidTime initiates ErrOutOfBounds",
appendableSamples: []appendableSamples{
- {ts: 100, fh: tsdbutil.GenerateTestFloatHistogram(1), ct: -1},
+ {ts: 100, fh: tsdbutil.GenerateTestFloatHistogram(1), st: -1},
},
expectedError: storage.ErrOutOfBounds,
},
{
- name: "integer histogram CT duplicates an existing sample",
+ name: "integer histogram ST duplicates an existing sample",
appendableSamples: []appendableSamples{
{ts: 100, h: tsdbutil.GenerateTestHistogram(1)},
- {ts: 200, h: tsdbutil.GenerateTestHistogram(1), ct: 100},
+ {ts: 200, h: tsdbutil.GenerateTestHistogram(1), st: 100},
},
expectedError: storage.ErrDuplicateSampleForTimestamp,
},
{
- name: "float histogram CT duplicates an existing sample",
+ name: "float histogram ST duplicates an existing sample",
appendableSamples: []appendableSamples{
{ts: 100, fh: tsdbutil.GenerateTestFloatHistogram(1)},
- {ts: 200, fh: tsdbutil.GenerateTestFloatHistogram(1), ct: 100},
+ {ts: 200, fh: tsdbutil.GenerateTestFloatHistogram(1), st: 100},
},
expectedError: storage.ErrDuplicateSampleForTimestamp,
},
@@ -6996,18 +6912,14 @@ func TestHeadAppender_AppendHistogramCTZeroSample(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
for _, sample := range tc.appendableSamples {
a := h.Appender(context.Background())
var err error
- if sample.ct != 0 {
- ref, err = a.AppendHistogramCTZeroSample(ref, lbls, sample.ts, sample.ct, sample.h, sample.fh)
+ if sample.st != 0 {
+ ref, err = a.AppendHistogramSTZeroSample(ref, lbls, sample.ts, sample.st, sample.h, sample.fh)
require.ErrorIs(t, err, tc.expectedError)
}
@@ -7025,9 +6937,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())
}
@@ -7067,9 +6976,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)
@@ -7103,7 +7009,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))
@@ -7274,9 +7181,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/head_wal.go b/tsdb/head_wal.go
index eed68125d4..0581b9306e 100644
--- a/tsdb/head_wal.go
+++ b/tsdb/head_wal.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
@@ -37,7 +37,6 @@ import (
"github.com/prometheus/prometheus/tsdb/chunkenc"
"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"
"github.com/prometheus/prometheus/tsdb/record"
"github.com/prometheus/prometheus/tsdb/tombstones"
@@ -255,7 +254,7 @@ Outer:
switch v := d.(type) {
case []record.RefSeries:
for _, walSeries := range v {
- mSeries, created, err := h.getOrCreateWithID(walSeries.Ref, walSeries.Labels.Hash(), walSeries.Labels, false)
+ mSeries, created, err := h.getOrCreateWithOptionalID(walSeries.Ref, walSeries.Labels.Hash(), walSeries.Labels, false)
if err != nil {
seriesCreationErr = err
break Outer
@@ -308,7 +307,21 @@ Outer:
}
h.wlReplaySamplesPool.Put(v)
case []tombstones.Stone:
+ // Tombstone records will be fairly rare, so not trying to optimise the allocations here.
+ deleteSeriesShards := make([][]chunks.HeadSeriesRef, concurrency)
for _, s := range v {
+ if len(s.Intervals) == 1 && s.Intervals[0].Mint == math.MinInt64 && s.Intervals[0].Maxt == math.MaxInt64 {
+ // This series was fully deleted at this point. This record is only done for stale series at the moment.
+ mod := uint64(s.Ref) % uint64(concurrency)
+ deleteSeriesShards[mod] = append(deleteSeriesShards[mod], chunks.HeadSeriesRef(s.Ref))
+
+ // If the series is with a different reference, try deleting that.
+ if r, ok := multiRef[chunks.HeadSeriesRef(s.Ref)]; ok {
+ mod := uint64(r) % uint64(concurrency)
+ deleteSeriesShards[mod] = append(deleteSeriesShards[mod], r)
+ }
+ continue
+ }
for _, itv := range s.Intervals {
if itv.Maxt < h.minValidTime.Load() {
continue
@@ -326,6 +339,14 @@ Outer:
h.tombstones.AddInterval(s.Ref, itv)
}
}
+
+ for i := range concurrency {
+ if len(deleteSeriesShards[i]) > 0 {
+ processors[i].input <- walSubsetProcessorInputItem{deletedSeriesRefs: deleteSeriesShards[i]}
+ deleteSeriesShards[i] = nil
+ }
+ }
+
h.wlReplaytStonesPool.Put(v)
case []record.RefExemplar:
for _, e := range v {
@@ -558,10 +579,11 @@ type walSubsetProcessor struct {
}
type walSubsetProcessorInputItem struct {
- samples []record.RefSample
- histogramSamples []histogramRecord
- existingSeries *memSeries
- walSeriesRef chunks.HeadSeriesRef
+ samples []record.RefSample
+ histogramSamples []histogramRecord
+ existingSeries *memSeries
+ walSeriesRef chunks.HeadSeriesRef
+ deletedSeriesRefs []chunks.HeadSeriesRef
}
func (wp *walSubsetProcessor) setup() {
@@ -712,6 +734,10 @@ func (wp *walSubsetProcessor) processWALSamples(h *Head, mmappedChunks, oooMmapp
case wp.histogramsOutput <- in.histogramSamples:
default:
}
+
+ if len(in.deletedSeriesRefs) > 0 {
+ h.deleteSeriesByID(in.deletedSeriesRefs)
+ }
}
h.updateMinMaxTime(mint, maxt)
@@ -1509,7 +1535,7 @@ func DeleteChunkSnapshots(dir string, maxIndex, maxOffset int) error {
return err
}
- errs := tsdb_errors.NewMulti()
+ var errs []error
for _, fi := range files {
if !strings.HasPrefix(fi.Name(), chunkSnapshotPrefix) {
continue
@@ -1532,11 +1558,11 @@ func DeleteChunkSnapshots(dir string, maxIndex, maxOffset int) error {
if idx < maxIndex || (idx == maxIndex && offset < maxOffset) {
if err := os.RemoveAll(filepath.Join(dir, fi.Name())); err != nil {
- errs.Add(err)
+ errs = append(errs, err)
}
}
}
- return errs.Err()
+ return errors.Join(errs...)
}
// loadChunkSnapshot replays the chunk snapshot and restores the Head state from it. If there was any error returned,
@@ -1590,7 +1616,7 @@ func (h *Head) loadChunkSnapshot() (int, int, map[chunks.HeadSeriesRef]*memSerie
localRefSeries := shardedRefSeries[idx]
for csr := range rc {
- series, _, err := h.getOrCreateWithID(csr.ref, csr.lset.Hash(), csr.lset, false)
+ series, _, err := h.getOrCreateWithOptionalID(csr.ref, csr.lset.Hash(), csr.lset, false)
if err != nil {
errChan <- err
return
@@ -1724,14 +1750,14 @@ Outer:
}
close(errChan)
- merr := tsdb_errors.NewMulti()
+ var errs []error
if loopErr != nil {
- merr.Add(fmt.Errorf("decode loop: %w", loopErr))
+ errs = append(errs, fmt.Errorf("decode loop: %w", loopErr))
}
for err := range errChan {
- merr.Add(fmt.Errorf("record processing: %w", err))
+ errs = append(errs, fmt.Errorf("record processing: %w", err))
}
- if err := merr.Err(); err != nil {
+ if err := errors.Join(errs...); err != nil {
return -1, -1, nil, err
}
diff --git a/tsdb/index/index.go b/tsdb/index/index.go
index 28eacd7c00..9b907bb7a7 100644
--- a/tsdb/index/index.go
+++ b/tsdb/index/index.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
@@ -17,6 +17,7 @@ import (
"bufio"
"context"
"encoding/binary"
+ "errors"
"fmt"
"hash"
"hash/crc32"
@@ -32,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"
)
@@ -94,6 +94,16 @@ func (s indexWriterStage) String() string {
return ""
}
+// ErrPostingsOffsetTableTooLarge is returned when the postings offset table length
+// would exceed 4 bytes (table would exceed the 4GB limit).
+var ErrPostingsOffsetTableTooLarge = errors.New("length size exceeds 4 bytes")
+
+// ErrIndexExceeds64GiB is returned when the index file would exceed the 64GiB limit.
+var ErrIndexExceeds64GiB = errors.New("exceeding max size of 64GiB")
+
+// ErrSymbolTableTooLarge is returned when the symbol table size exceeds 4 bytes (4GiB limit).
+var ErrSymbolTableTooLarge = fmt.Errorf("symbol table size exceeds %d bytes", uint32(math.MaxUint32))
+
// The table gets initialized with sync.Once but may still cause a race
// with any other use of the crc32 package anywhere. Thus we initialize it
// before.
@@ -303,7 +313,7 @@ func (fw *FileWriter) Write(bufs ...[]byte) error {
// Once we move to compressed/varint representations in those areas, this limitation
// can be lifted.
if fw.pos > 16*math.MaxUint32 {
- return fmt.Errorf("%q exceeding max size of 64GiB", fw.name)
+ return fmt.Errorf("%q %w", fw.name, ErrIndexExceeds64GiB)
}
}
return nil
@@ -543,7 +553,7 @@ func (w *Writer) finishSymbols() error {
symbolTableSize := w.f.pos - w.toc.Symbols - 4
// The symbol table's part is 4 bytes. So the total symbol table size must be less than or equal to 2^32-1
if symbolTableSize > math.MaxUint32 {
- return fmt.Errorf("symbol table size exceeds %d bytes: %d", uint32(math.MaxUint32), symbolTableSize)
+ return fmt.Errorf("%w: %d", ErrSymbolTableTooLarge, symbolTableSize)
}
// Write out the length and symbol count.
@@ -660,7 +670,7 @@ func (w *Writer) writeLengthAndHash(startPos uint64) error {
w.buf1.Reset()
l := w.f.pos - startPos - 4
if l > math.MaxUint32 {
- return fmt.Errorf("length size exceeds 4 bytes: %d", l)
+ return fmt.Errorf("%w: %d", ErrPostingsOffsetTableTooLarge, l)
}
w.buf1.PutBE32int(int(l))
if err := w.writeAt(w.buf1.Get(), startPos); err != nil {
@@ -999,10 +1009,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
@@ -1447,32 +1457,6 @@ func (r *Reader) LabelNamesFor(ctx context.Context, postings Postings) ([]string
return names, nil
}
-// LabelValueFor returns label value for the given label name in the series referred to by ID.
-func (r *Reader) LabelValueFor(ctx context.Context, id storage.SeriesRef, label string) (string, error) {
- offset := id
- // In version 2 series IDs are no longer exact references but series are 16-byte padded
- // and the ID is the multiple of 16 of the actual position.
- if r.version != FormatV1 {
- offset = id * seriesByteAlign
- }
- d := encoding.NewDecbufUvarintAt(r.b, int(offset), castagnoliTable)
- buf := d.Get()
- if d.Err() != nil {
- return "", fmt.Errorf("label values for: %w", d.Err())
- }
-
- value, err := r.dec.LabelValueFor(ctx, buf, label)
- if err != nil {
- return "", storage.ErrNotFound
- }
-
- if value == "" {
- return "", storage.ErrNotFound
- }
-
- return value, nil
-}
-
// Series reads the series with the given ID and writes its labels and chunks into builder and chks.
func (r *Reader) Series(id storage.SeriesRef, builder *labels.ScratchBuilder, chks *[]chunks.Meta) error {
offset := id
@@ -1809,37 +1793,6 @@ func (*Decoder) LabelNamesOffsetsFor(b []byte) ([]uint32, error) {
return offsets, d.Err()
}
-// LabelValueFor decodes a label for a given series.
-func (dec *Decoder) LabelValueFor(ctx context.Context, b []byte, label string) (string, error) {
- d := encoding.Decbuf{B: b}
- k := d.Uvarint()
-
- for range k {
- lno := uint32(d.Uvarint())
- lvo := uint32(d.Uvarint())
-
- if d.Err() != nil {
- return "", fmt.Errorf("read series label offsets: %w", d.Err())
- }
-
- ln, err := dec.LookupSymbol(ctx, lno)
- if err != nil {
- return "", fmt.Errorf("lookup label name: %w", err)
- }
-
- if ln == label {
- lv, err := dec.LookupSymbol(ctx, lvo)
- if err != nil {
- return "", fmt.Errorf("lookup label value: %w", err)
- }
-
- return lv, nil
- }
- }
-
- return "", d.Err()
-}
-
// Series decodes a series entry from the given byte slice into builder and chks.
// Previous contents of builder can be overwritten - make sure you copy before retaining.
// Skips reading chunks metadata if chks is nil.
diff --git a/tsdb/index/index_test.go b/tsdb/index/index_test.go
index 9013a1d5cd..20399dcdcf 100644
--- a/tsdb/index/index_test.go
+++ b/tsdb/index/index_test.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
diff --git a/tsdb/index/postings.go b/tsdb/index/postings.go
index d5a17c3daa..c0bf213c45 100644
--- a/tsdb/index/postings.go
+++ b/tsdb/index/postings.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
@@ -391,7 +391,7 @@ func (p *MemPostings) Iter(f func(labels.Label, Postings) error) error {
for n, e := range p.m {
for v, p := range e {
- if err := f(labels.Label{Name: n, Value: v}, newListPostings(p...)); err != nil {
+ if err := f(labels.Label{Name: n, Value: v}, NewListPostings(p)); err != nil {
return err
}
}
@@ -478,8 +478,8 @@ func (p *MemPostings) PostingsForLabelMatching(ctx context.Context, name string,
}
// Now `vals` only contains the values that matched, get their postings.
- its := make([]*ListPostings, 0, len(vals))
- lps := make([]ListPostings, len(vals))
+ its := make([]*listPostings, 0, len(vals))
+ lps := make([]listPostings, len(vals))
p.mtx.RLock()
e := p.m[name]
for i, v := range vals {
@@ -488,7 +488,7 @@ func (p *MemPostings) PostingsForLabelMatching(ctx context.Context, name string,
// If we didn't let the mutex go, we'd have these postings here, but they would be pointing nowhere
// because there would be a `MemPostings.Delete()` call waiting for the lock to delete these labels,
// because the series were deleted already.
- lps[i] = ListPostings{list: refs}
+ lps[i] = listPostings{list: refs}
its = append(its, &lps[i])
}
}
@@ -500,13 +500,13 @@ func (p *MemPostings) PostingsForLabelMatching(ctx context.Context, name string,
// Postings returns a postings iterator for the given label values.
func (p *MemPostings) Postings(ctx context.Context, name string, values ...string) Postings {
- res := make([]*ListPostings, 0, len(values))
- lps := make([]ListPostings, len(values))
+ res := make([]*listPostings, 0, len(values))
+ lps := make([]listPostings, len(values))
p.mtx.RLock()
postingsMapForName := p.m[name]
for i, value := range values {
if lp := postingsMapForName[value]; lp != nil {
- lps[i] = ListPostings{list: lp}
+ lps[i] = listPostings{list: lp}
res = append(res, &lps[i])
}
}
@@ -518,12 +518,12 @@ func (p *MemPostings) PostingsForAllLabelValues(ctx context.Context, name string
p.mtx.RLock()
e := p.m[name]
- its := make([]*ListPostings, 0, len(e))
- lps := make([]ListPostings, len(e))
+ its := make([]*listPostings, 0, len(e))
+ lps := make([]listPostings, len(e))
i := 0
for _, refs := range e {
if len(refs) > 0 {
- lps[i] = ListPostings{list: refs}
+ lps[i] = listPostings{list: refs}
its = append(its, &lps[i])
}
i++
@@ -542,7 +542,7 @@ func ExpandPostings(p Postings) (res []storage.SeriesRef, err error) {
return res, p.Err()
}
-// Postings provides iterative access over a postings list.
+// Postings provides iterative access over an ordered list of SeriesRef.
type Postings interface {
// Next advances the iterator and returns true if another value was found.
Next() bool
@@ -641,15 +641,28 @@ func (it *intersectPostings) Seek(target storage.SeriesRef) bool {
}
func (it *intersectPostings) Next() bool {
- target := it.current
- for _, p := range it.postings {
+ // Move forward the first Postings and take its value as the target to match.
+ if !it.postings[0].Next() {
+ return false
+ }
+ target := it.postings[0].At()
+ allEqual := true
+ for _, p := range it.postings[1:] { // Now move forward all the other ones and check if they match.
if !p.Next() {
return false
}
- if p.At() > target {
- target = p.At()
+ at := p.At()
+ if at > target { // This one is past the target, so pick up a new target to Seek at the end.
+ target = at
+ allEqual = false
+ } else if at < target { // This one needs to Seek to the target, but carry on with other postings in case they have an even higher target.
+ allEqual = false
}
}
+ if allEqual {
+ it.current = target
+ return true
+ }
return it.Seek(target)
}
@@ -814,25 +827,23 @@ func (rp *removedPostings) Err() error {
return rp.remove.Err()
}
-// ListPostings implements the Postings interface over a plain list.
-type ListPostings struct {
+// listPostings implements the Postings interface over a plain list.
+type listPostings struct {
list []storage.SeriesRef
cur storage.SeriesRef
}
+// NewListPostings creates a Postings from the supplied SeriesRefs, which must be in order.
+// The list slice passed in is retained.
func NewListPostings(list []storage.SeriesRef) Postings {
- return newListPostings(list...)
+ return &listPostings{list: list}
}
-func newListPostings(list ...storage.SeriesRef) *ListPostings {
- return &ListPostings{list: list}
-}
-
-func (it *ListPostings) At() storage.SeriesRef {
+func (it *listPostings) At() storage.SeriesRef {
return it.cur
}
-func (it *ListPostings) Next() bool {
+func (it *listPostings) Next() bool {
if len(it.list) > 0 {
it.cur = it.list[0]
it.list = it.list[1:]
@@ -842,7 +853,7 @@ func (it *ListPostings) Next() bool {
return false
}
-func (it *ListPostings) Seek(x storage.SeriesRef) bool {
+func (it *listPostings) Seek(x storage.SeriesRef) bool {
// If the current value satisfies, then return.
if it.cur >= x {
return true
@@ -851,23 +862,25 @@ func (it *ListPostings) Seek(x storage.SeriesRef) bool {
return false
}
- // Do binary search between current position and end.
- i, _ := slices.BinarySearch(it.list, x)
- if i < len(it.list) {
- it.cur = it.list[i]
- it.list = it.list[i+1:]
- return true
+ i := 0 // Check the next item in the list, otherwise binary search between current position and end.
+ if it.list[0] < x {
+ i, _ = slices.BinarySearch(it.list, x)
+ if i >= len(it.list) { // Off the end - terminate the iterator.
+ it.list = nil
+ return false
+ }
}
- it.list = nil
- return false
+ it.cur = it.list[i]
+ it.list = it.list[i+1:]
+ return true
}
-func (*ListPostings) Err() error {
+func (*listPostings) Err() error {
return nil
}
// Len returns the remaining number of postings in the list.
-func (it *ListPostings) Len() int {
+func (it *listPostings) Len() int {
return len(it.list)
}
@@ -943,7 +956,7 @@ func FindIntersectingPostings(p Postings, candidates []Postings) (indexes []int,
}
if p.At() == h.at() {
indexes = append(indexes, h.popIndex())
- } else if err := h.next(); err != nil {
+ } else if err := h.seekHead(p.At()); err != nil {
return nil, err
}
}
@@ -986,20 +999,18 @@ func (h *postingsWithIndexHeap) popIndex() int {
// at provides the storage.SeriesRef where root Postings is pointing at this moment.
func (h postingsWithIndexHeap) at() storage.SeriesRef { return h[0].p.At() }
-// next performs the Postings.Next() operation on the root of the heap, performing the related operation on the heap
-// and conveniently returning the result of calling Postings.Err() if the result of calling Next() was false.
-// If Next() succeeds, heap is fixed to move the root to its new position, according to its Postings.At() value.
-// If Next() returns fails and there's no error reported by Postings.Err(), then root is marked as removed and heap is fixed.
-func (h *postingsWithIndexHeap) next() error {
+// seekHead performs the Postings.Seek() operation on the root of the heap.
+// If the root is exhausted or fails, it is removed from the heap.
+func (h *postingsWithIndexHeap) seekHead(val storage.SeriesRef) error {
pi := (*h)[0]
- next := pi.p.Next()
+ next := pi.p.Seek(val)
if next {
heap.Fix(h, 0)
return nil
}
if err := pi.p.Err(); err != nil {
- return fmt.Errorf("postings %d: %w", pi.index, err)
+ return fmt.Errorf("seek postings %d: %w", pi.index, err)
}
h.popIndex()
return nil
diff --git a/tsdb/index/postings_test.go b/tsdb/index/postings_test.go
index 3ba523c22f..5c67a2da6d 100644
--- a/tsdb/index/postings_test.go
+++ b/tsdb/index/postings_test.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
@@ -20,6 +20,7 @@ import (
"errors"
"fmt"
"math/rand"
+ "slices"
"sort"
"strconv"
"strings"
@@ -62,9 +63,7 @@ func TestMemPostings_ensureOrder(t *testing.T) {
for _, e := range p.m {
for _, l := range e {
- ok := sort.SliceIsSorted(l, func(i, j int) bool {
- return l[i] < l[j]
- })
+ ok := slices.IsSorted(l)
require.True(t, ok, "postings list %v is not sorted", l)
}
}
@@ -285,92 +284,85 @@ func consumePostings(p Postings) error {
return p.Err()
}
+func newListPostings(list ...storage.SeriesRef) *listPostings {
+ if !slices.IsSorted(list) {
+ panic("newListPostings: list is not sorted")
+ }
+ return &listPostings{list: list}
+}
+
+// Create ListPostings for a benchmark, collecting the original sets of references
+// so they can be reset without additional memory allocations.
+func createPostings(lps *[]*listPostings, refs *[][]storage.SeriesRef, params ...storage.SeriesRef) {
+ var temp []storage.SeriesRef
+ for i := 0; i < len(params); i += 3 {
+ for j := params[i]; j < params[i+1]; j += params[i+2] {
+ temp = append(temp, j)
+ }
+ }
+ *lps = append(*lps, newListPostings(temp...))
+ *refs = append(*refs, temp)
+}
+
+// Reset the ListPostings to their original values each time round the benchmark loop.
+func resetPostings(its []Postings, lps []*listPostings, refs [][]storage.SeriesRef) {
+ for j := range refs {
+ lps[j].list = refs[j]
+ its[j] = lps[j]
+ }
+}
+
func BenchmarkIntersect(t *testing.B) {
t.Run("LongPostings1", func(bench *testing.B) {
- var a, b, c, d []storage.SeriesRef
-
- for i := 0; i < 10000000; i += 2 {
- a = append(a, storage.SeriesRef(i))
- }
- for i := 5000000; i < 5000100; i += 4 {
- b = append(b, storage.SeriesRef(i))
- }
- for i := 5090000; i < 5090600; i += 4 {
- b = append(b, storage.SeriesRef(i))
- }
- for i := 4990000; i < 5100000; i++ {
- c = append(c, storage.SeriesRef(i))
- }
- for i := 4000000; i < 6000000; i++ {
- d = append(d, storage.SeriesRef(i))
- }
+ var lps []*listPostings
+ var refs [][]storage.SeriesRef
+ createPostings(&lps, &refs, 0, 10000000, 2)
+ createPostings(&lps, &refs, 5000000, 5000100, 4, 5090000, 5090600, 4)
+ createPostings(&lps, &refs, 4990000, 5100000, 1)
+ createPostings(&lps, &refs, 4000000, 6000000, 1)
+ its := make([]Postings, len(refs))
bench.ResetTimer()
bench.ReportAllocs()
for bench.Loop() {
- i1 := newListPostings(a...)
- i2 := newListPostings(b...)
- i3 := newListPostings(c...)
- i4 := newListPostings(d...)
- if err := consumePostings(Intersect(i1, i2, i3, i4)); err != nil {
+ resetPostings(its, lps, refs)
+ if err := consumePostings(Intersect(its...)); err != nil {
bench.Fatal(err)
}
}
})
t.Run("LongPostings2", func(bench *testing.B) {
- var a, b, c, d []storage.SeriesRef
-
- for i := range 12500000 {
- a = append(a, storage.SeriesRef(i))
- }
- for i := 7500000; i < 12500000; i++ {
- b = append(b, storage.SeriesRef(i))
- }
- for i := 9000000; i < 20000000; i++ {
- c = append(c, storage.SeriesRef(i))
- }
- for i := 10000000; i < 12000000; i++ {
- d = append(d, storage.SeriesRef(i))
- }
+ var lps []*listPostings
+ var refs [][]storage.SeriesRef
+ createPostings(&lps, &refs, 0, 12500000, 1)
+ createPostings(&lps, &refs, 7500000, 12500000, 1)
+ createPostings(&lps, &refs, 9000000, 20000000, 1)
+ createPostings(&lps, &refs, 10000000, 12000000, 1)
+ its := make([]Postings, len(refs))
bench.ResetTimer()
bench.ReportAllocs()
for bench.Loop() {
- i1 := newListPostings(a...)
- i2 := newListPostings(b...)
- i3 := newListPostings(c...)
- i4 := newListPostings(d...)
- if err := consumePostings(Intersect(i1, i2, i3, i4)); err != nil {
+ resetPostings(its, lps, refs)
+ if err := consumePostings(Intersect(its...)); err != nil {
bench.Fatal(err)
}
}
})
- // Many matchers(k >> n).
t.Run("ManyPostings", func(bench *testing.B) {
- var lps []*ListPostings
+ var lps []*listPostings
var refs [][]storage.SeriesRef
-
- // Create 100000 matchers(k=100000), making sure all memory allocation is done before starting the loop.
- for range 100000 {
- var temp []storage.SeriesRef
- for j := storage.SeriesRef(1); j < 100; j++ {
- temp = append(temp, j)
- }
- lps = append(lps, newListPostings(temp...))
- refs = append(refs, temp)
+ for range 100 {
+ createPostings(&lps, &refs, 1, 100, 1)
}
its := make([]Postings, len(refs))
bench.ResetTimer()
bench.ReportAllocs()
for bench.Loop() {
- // Reset the ListPostings to their original values each time round the loop.
- for j := range refs {
- lps[j].list = refs[j]
- its[j] = lps[j]
- }
+ resetPostings(its, lps, refs)
if err := consumePostings(Intersect(its...)); err != nil {
bench.Fatal(err)
}
@@ -379,7 +371,7 @@ func BenchmarkIntersect(t *testing.B) {
}
func BenchmarkMerge(t *testing.B) {
- var lps []*ListPostings
+ var lps []*listPostings
var refs [][]storage.SeriesRef
// Create 100000 matchers(k=100000), making sure all memory allocation is done before starting the loop.
@@ -392,7 +384,7 @@ func BenchmarkMerge(t *testing.B) {
refs = append(refs, temp)
}
- its := make([]*ListPostings, len(refs))
+ its := make([]*listPostings, len(refs))
for _, nSeries := range []int{1, 10, 10000, 100000} {
t.Run(strconv.Itoa(nSeries), func(bench *testing.B) {
ctx := context.Background()
@@ -1200,7 +1192,7 @@ func (p *postingsFailingAfterNthCall) Err() error {
}
func TestPostingsWithIndexHeap(t *testing.T) {
- t.Run("iterate", func(t *testing.T) {
+ t.Run("seekHead", func(t *testing.T) {
h := postingsWithIndexHeap{
{index: 0, p: NewListPostings([]storage.SeriesRef{10, 20, 30})},
{index: 1, p: NewListPostings([]storage.SeriesRef{1, 5})},
@@ -1213,7 +1205,7 @@ func TestPostingsWithIndexHeap(t *testing.T) {
for _, expected := range []storage.SeriesRef{1, 5, 10, 20, 25, 30, 50} {
require.Equal(t, expected, h.at())
- require.NoError(t, h.next())
+ require.NoError(t, h.seekHead(h.at()+1))
}
require.True(t, h.empty())
})
@@ -1231,7 +1223,7 @@ func TestPostingsWithIndexHeap(t *testing.T) {
for _, expected := range []storage.SeriesRef{1, 5, 10, 20} {
require.Equal(t, expected, h.at())
- require.NoError(t, h.next())
+ require.NoError(t, h.seekHead(h.at()+1))
}
require.Equal(t, storage.SeriesRef(25), h.at())
node := heap.Pop(&h).(postingsWithIndex)
@@ -1243,78 +1235,78 @@ func TestPostingsWithIndexHeap(t *testing.T) {
func TestListPostings(t *testing.T) {
t.Run("empty list", func(t *testing.T) {
p := NewListPostings(nil)
- require.Equal(t, 0, p.(*ListPostings).Len())
+ require.Equal(t, 0, p.(*listPostings).Len())
require.False(t, p.Next())
require.False(t, p.Seek(10))
require.False(t, p.Next())
require.NoError(t, p.Err())
- require.Equal(t, 0, p.(*ListPostings).Len())
+ require.Equal(t, 0, p.(*listPostings).Len())
})
t.Run("one posting", func(t *testing.T) {
t.Run("next", func(t *testing.T) {
p := NewListPostings([]storage.SeriesRef{10})
- require.Equal(t, 1, p.(*ListPostings).Len())
+ require.Equal(t, 1, p.(*listPostings).Len())
require.True(t, p.Next())
require.Equal(t, storage.SeriesRef(10), p.At())
require.False(t, p.Next())
require.NoError(t, p.Err())
- require.Equal(t, 0, p.(*ListPostings).Len())
+ require.Equal(t, 0, p.(*listPostings).Len())
})
t.Run("seek less", func(t *testing.T) {
p := NewListPostings([]storage.SeriesRef{10})
- require.Equal(t, 1, p.(*ListPostings).Len())
+ require.Equal(t, 1, p.(*listPostings).Len())
require.True(t, p.Seek(5))
require.Equal(t, storage.SeriesRef(10), p.At())
require.True(t, p.Seek(5))
require.Equal(t, storage.SeriesRef(10), p.At())
require.False(t, p.Next())
require.NoError(t, p.Err())
- require.Equal(t, 0, p.(*ListPostings).Len())
+ require.Equal(t, 0, p.(*listPostings).Len())
})
t.Run("seek equal", func(t *testing.T) {
p := NewListPostings([]storage.SeriesRef{10})
- require.Equal(t, 1, p.(*ListPostings).Len())
+ require.Equal(t, 1, p.(*listPostings).Len())
require.True(t, p.Seek(10))
require.Equal(t, storage.SeriesRef(10), p.At())
require.False(t, p.Next())
require.NoError(t, p.Err())
- require.Equal(t, 0, p.(*ListPostings).Len())
+ require.Equal(t, 0, p.(*listPostings).Len())
})
t.Run("seek more", func(t *testing.T) {
p := NewListPostings([]storage.SeriesRef{10})
- require.Equal(t, 1, p.(*ListPostings).Len())
+ require.Equal(t, 1, p.(*listPostings).Len())
require.False(t, p.Seek(15))
require.False(t, p.Next())
require.NoError(t, p.Err())
- require.Equal(t, 0, p.(*ListPostings).Len())
+ require.Equal(t, 0, p.(*listPostings).Len())
})
t.Run("seek after next", func(t *testing.T) {
p := NewListPostings([]storage.SeriesRef{10})
- require.Equal(t, 1, p.(*ListPostings).Len())
+ require.Equal(t, 1, p.(*listPostings).Len())
require.True(t, p.Next())
require.False(t, p.Seek(15))
require.False(t, p.Next())
require.NoError(t, p.Err())
- require.Equal(t, 0, p.(*ListPostings).Len())
+ require.Equal(t, 0, p.(*listPostings).Len())
})
})
t.Run("multiple postings", func(t *testing.T) {
t.Run("next", func(t *testing.T) {
p := NewListPostings([]storage.SeriesRef{10, 20})
- require.Equal(t, 2, p.(*ListPostings).Len())
+ require.Equal(t, 2, p.(*listPostings).Len())
require.True(t, p.Next())
require.Equal(t, storage.SeriesRef(10), p.At())
require.True(t, p.Next())
require.Equal(t, storage.SeriesRef(20), p.At())
require.False(t, p.Next())
require.NoError(t, p.Err())
- require.Equal(t, 0, p.(*ListPostings).Len())
+ require.Equal(t, 0, p.(*listPostings).Len())
})
t.Run("seek", func(t *testing.T) {
p := NewListPostings([]storage.SeriesRef{10, 20})
- require.Equal(t, 2, p.(*ListPostings).Len())
+ require.Equal(t, 2, p.(*listPostings).Len())
require.True(t, p.Seek(5))
require.Equal(t, storage.SeriesRef(10), p.At())
require.True(t, p.Seek(5))
@@ -1329,30 +1321,30 @@ func TestListPostings(t *testing.T) {
require.Equal(t, storage.SeriesRef(20), p.At())
require.False(t, p.Next())
require.NoError(t, p.Err())
- require.Equal(t, 0, p.(*ListPostings).Len())
+ require.Equal(t, 0, p.(*listPostings).Len())
})
t.Run("seek lest than last", func(t *testing.T) {
p := NewListPostings([]storage.SeriesRef{10, 20, 30, 40, 50})
- require.Equal(t, 5, p.(*ListPostings).Len())
+ require.Equal(t, 5, p.(*listPostings).Len())
require.True(t, p.Seek(45))
require.Equal(t, storage.SeriesRef(50), p.At())
require.False(t, p.Next())
- require.Equal(t, 0, p.(*ListPostings).Len())
+ require.Equal(t, 0, p.(*listPostings).Len())
})
t.Run("seek exactly last", func(t *testing.T) {
p := NewListPostings([]storage.SeriesRef{10, 20, 30, 40, 50})
- require.Equal(t, 5, p.(*ListPostings).Len())
+ require.Equal(t, 5, p.(*listPostings).Len())
require.True(t, p.Seek(50))
require.Equal(t, storage.SeriesRef(50), p.At())
require.False(t, p.Next())
- require.Equal(t, 0, p.(*ListPostings).Len())
+ require.Equal(t, 0, p.(*listPostings).Len())
})
t.Run("seek more than last", func(t *testing.T) {
p := NewListPostings([]storage.SeriesRef{10, 20, 30, 40, 50})
- require.Equal(t, 5, p.(*ListPostings).Len())
+ require.Equal(t, 5, p.(*listPostings).Len())
require.False(t, p.Seek(60))
require.False(t, p.Next())
- require.Equal(t, 0, p.(*ListPostings).Len())
+ require.Equal(t, 0, p.(*listPostings).Len())
})
})
diff --git a/tsdb/index/postingsstats.go b/tsdb/index/postingsstats.go
index f9ee640ff5..ebbe835207 100644
--- a/tsdb/index/postingsstats.go
+++ b/tsdb/index/postingsstats.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The Prometheus Authors
+// 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
diff --git a/tsdb/index/postingsstats_test.go b/tsdb/index/postingsstats_test.go
index b218dd9fc7..766c5055c1 100644
--- a/tsdb/index/postingsstats_test.go
+++ b/tsdb/index/postingsstats_test.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The Prometheus Authors
+// 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
diff --git a/tsdb/isolation.go b/tsdb/isolation.go
index 95d3cfa5eb..029efaf181 100644
--- a/tsdb/isolation.go
+++ b/tsdb/isolation.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
diff --git a/tsdb/isolation_test.go b/tsdb/isolation_test.go
index 1e41b9c753..2b2e1a6487 100644
--- a/tsdb/isolation_test.go
+++ b/tsdb/isolation_test.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
@@ -88,10 +88,7 @@ func BenchmarkIsolation(b *testing.B) {
start := make(chan struct{})
for range goroutines {
- wg.Add(1)
-
- go func() {
- defer wg.Done()
+ wg.Go(func() {
<-start
for b.Loop() {
@@ -99,7 +96,7 @@ func BenchmarkIsolation(b *testing.B) {
iso.closeAppend(appendID)
}
- }()
+ })
}
b.ResetTimer()
@@ -118,10 +115,7 @@ func BenchmarkIsolationWithState(b *testing.B) {
start := make(chan struct{})
for range goroutines {
- wg.Add(1)
-
- go func() {
- defer wg.Done()
+ wg.Go(func() {
<-start
for b.Loop() {
@@ -129,7 +123,7 @@ func BenchmarkIsolationWithState(b *testing.B) {
iso.closeAppend(appendID)
}
- }()
+ })
}
readers := goroutines / 100
@@ -138,17 +132,14 @@ func BenchmarkIsolationWithState(b *testing.B) {
}
for g := 0; g < readers; g++ {
- wg.Add(1)
-
- go func() {
- defer wg.Done()
+ wg.Go(func() {
<-start
for b.Loop() {
s := iso.State(math.MinInt64, math.MaxInt64)
s.Close()
}
- }()
+ })
}
b.ResetTimer()
diff --git a/tsdb/label_values_bench_test.go b/tsdb/label_values_bench_test.go
new file mode 100644
index 0000000000..1e55cf80c0
--- /dev/null
+++ b/tsdb/label_values_bench_test.go
@@ -0,0 +1,86 @@
+// 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 tsdb
+
+import (
+ "context"
+ "strconv"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/prometheus/prometheus/model/labels"
+ "github.com/prometheus/prometheus/tsdb/wlog"
+)
+
+// BenchmarkLabelValues_SlowPath benchmarks the performance of LabelValues when the matcher
+// is far ahead of the candidate posting list. This reproduces the performance regression
+// described in #14551 where dense candidates caused O(N) iteration instead of O(log N) seeking.
+func BenchmarkLabelValues_SlowPath(b *testing.B) {
+ // Create a head with some data.
+ opts := DefaultHeadOptions()
+ opts.ChunkDirRoot = b.TempDir()
+ h, err := NewHead(nil, nil, nil, nil, opts, nil)
+ require.NoError(b, err)
+ defer h.Close()
+
+ app := h.Appender(context.Background())
+ // 1. Create a large number of series for a "candidate" label (e.g. "job").
+ // We want these to NOT match the target matcher, but be candidates for a different label.
+ // We use "job=api" and "instance=..."
+ // We want the interaction to be:
+ // LabelValues("instance", "job"="api")
+ // "job"="api" will have 1 series at the END.
+ // "instance" will have 100k series.
+
+ // Actually, let's stick to the reproduction case:
+ // distinct values for "val1".
+ // "b"="1" matcher.
+
+ // Create 100k series with the same label value ("common") but without the matcher label.
+ // This results in a single large posting list for that value, simulating a dense candidate.
+ for i := range 100000 {
+ _, err := app.Append(0, labels.FromStrings("val1", "common", "extra", strconv.Itoa(i)), time.Now().UnixMilli(), 1)
+ require.NoError(b, err)
+ }
+
+ // Create 1 series that matches the label "b=1", with a series ID greater than all previous ones.
+ // This forces the intersection to skip over all 100k previous candidates.
+ _, err = app.Append(0, labels.FromStrings("val1", "common", "b", "1"), time.Now().UnixMilli(), 1)
+ require.NoError(b, err)
+
+ require.NoError(b, app.Commit())
+
+ ctx := context.Background()
+ matcher := labels.MustNewMatcher(labels.MatchEqual, "b", "1")
+
+ // Use the correct method to access label values.
+ idx, err := h.Index()
+ require.NoError(b, err)
+
+ b.ResetTimer()
+ b.ReportAllocs()
+
+ for b.Loop() {
+ // "val1"="common" has 100k+1 postings.
+ // "b=1" has 1 posting (the last one).
+ vals, err := idx.LabelValues(ctx, "val1", nil, matcher)
+ require.NoError(b, err)
+ require.Equal(b, []string{"common"}, vals)
+ }
+}
+
+// Ensure wlog/wal needed for NewHead.
+var _ = wlog.WL{}
diff --git a/tsdb/mocks_test.go b/tsdb/mocks_test.go
index 986048d3d2..b3d2208bc1 100644
--- a/tsdb/mocks_test.go
+++ b/tsdb/mocks_test.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
diff --git a/tsdb/ooo_head.go b/tsdb/ooo_head.go
index b3f5e2b675..f9746c4c61 100644
--- a/tsdb/ooo_head.go
+++ b/tsdb/ooo_head.go
@@ -1,4 +1,4 @@
-// Copyright 2022 The Prometheus Authors
+// 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
@@ -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.go b/tsdb/ooo_head_read.go
index af8f9b1f83..5d2347c2d7 100644
--- a/tsdb/ooo_head_read.go
+++ b/tsdb/ooo_head_read.go
@@ -1,4 +1,4 @@
-// Copyright 2022 The Prometheus Authors
+// 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
@@ -500,10 +500,6 @@ func (*OOOCompactionHeadIndexReader) LabelNames(context.Context, ...*labels.Matc
return nil, errors.New("not implemented")
}
-func (*OOOCompactionHeadIndexReader) LabelValueFor(context.Context, storage.SeriesRef, string) (string, error) {
- return "", errors.New("not implemented")
-}
-
func (*OOOCompactionHeadIndexReader) LabelNamesFor(context.Context, index.Postings) ([]string, error) {
return nil, errors.New("not implemented")
}
diff --git a/tsdb/ooo_head_read_test.go b/tsdb/ooo_head_read_test.go
index d197eacb56..f58ee3aada 100644
--- a/tsdb/ooo_head_read_test.go
+++ b/tsdb/ooo_head_read_test.go
@@ -1,4 +1,4 @@
-// Copyright 2022 The Prometheus Authors
+// 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
@@ -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()
@@ -498,7 +494,7 @@ func testOOOHeadChunkReader_Chunk(t *testing.T, scenario sampleTypeScenario) {
minutes := func(m int64) int64 { return m * time.Minute.Milliseconds() }
t.Run("Getting a non existing chunk fails with not found error", func(t *testing.T) {
- db := newTestDBWithOpts(t, opts)
+ db := newTestDB(t, withOpts(opts))
cr := NewHeadAndOOOChunkReader(db.head, 0, 1000, nil, nil, 0)
defer cr.Close()
@@ -837,7 +833,7 @@ func testOOOHeadChunkReader_Chunk(t *testing.T, scenario sampleTypeScenario) {
for _, tc := range tests {
t.Run(fmt.Sprintf("name=%s", tc.name), func(t *testing.T) {
- db := newTestDBWithOpts(t, opts)
+ db := newTestDB(t, withOpts(opts))
app := db.Appender(context.Background())
s1Ref, _, err := scenario.appendFunc(app, s1, tc.firstInOrderSampleAt, tc.firstInOrderSampleAt/1*time.Minute.Milliseconds())
@@ -1006,7 +1002,7 @@ func testOOOHeadChunkReader_Chunk_ConsistentQueryResponseDespiteOfHeadExpanding(
for _, tc := range tests {
t.Run(fmt.Sprintf("name=%s", tc.name), func(t *testing.T) {
- db := newTestDBWithOpts(t, opts)
+ db := newTestDB(t, withOpts(opts))
app := db.Appender(context.Background())
s1Ref, _, err := scenario.appendFunc(app, s1, tc.firstInOrderSampleAt, tc.firstInOrderSampleAt/1*time.Minute.Milliseconds())
@@ -1118,16 +1114,3 @@ func TestSortMetaByMinTimeAndMinRef(t *testing.T) {
})
}
}
-
-func newTestDBWithOpts(t *testing.T, opts *Options) *DB {
- dir := t.TempDir()
-
- db, err := Open(dir, nil, nil, opts, nil)
- require.NoError(t, err)
-
- t.Cleanup(func() {
- require.NoError(t, db.Close())
- })
-
- return db
-}
diff --git a/tsdb/ooo_head_test.go b/tsdb/ooo_head_test.go
index 8f773b6ef9..99cd357a30 100644
--- a/tsdb/ooo_head_test.go
+++ b/tsdb/ooo_head_test.go
@@ -1,4 +1,4 @@
-// Copyright 2022 The Prometheus Authors
+// 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
diff --git a/tsdb/ooo_isolation.go b/tsdb/ooo_isolation.go
index 3e3e165a0a..3aeee693a9 100644
--- a/tsdb/ooo_isolation.go
+++ b/tsdb/ooo_isolation.go
@@ -1,4 +1,4 @@
-// Copyright 2023 The Prometheus Authors
+// 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
diff --git a/tsdb/ooo_isolation_test.go b/tsdb/ooo_isolation_test.go
index 4ff0488ab1..054823b30c 100644
--- a/tsdb/ooo_isolation_test.go
+++ b/tsdb/ooo_isolation_test.go
@@ -1,4 +1,4 @@
-// Copyright 2023 The Prometheus Authors
+// 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
diff --git a/tsdb/querier.go b/tsdb/querier.go
index 788991235f..ac7a14e1b3 100644
--- a/tsdb/querier.go
+++ b/tsdb/querier.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
@@ -27,7 +27,6 @@ import (
"github.com/prometheus/prometheus/storage"
"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/index"
"github.com/prometheus/prometheus/tsdb/tombstones"
"github.com/prometheus/prometheus/util/annotations"
@@ -92,13 +91,13 @@ func (q *blockBaseQuerier) Close() error {
return errors.New("block querier already closed")
}
- errs := tsdb_errors.NewMulti(
+ errs := []error{
q.index.Close(),
q.chunks.Close(),
q.tombstones.Close(),
- )
+ }
q.closed = true
- return errs.Err()
+ return errors.Join(errs...)
}
type blockQuerier struct {
@@ -788,6 +787,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 +866,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 +882,7 @@ func (p *populateWithDelChunkSeriesIterator) populateCurrForSingleChunk() bool {
var (
newChunk chunkenc.Chunk
app chunkenc.Appender
- t int64
+ st, t int64
err error
)
switch valueType {
@@ -893,7 +898,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 +916,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 +931,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 +958,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 +974,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 +1013,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 +1214,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_bench_test.go b/tsdb/querier_bench_test.go
index 514fa05a17..ca9ee119f7 100644
--- a/tsdb/querier_bench_test.go
+++ b/tsdb/querier_bench_test.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Prometheus Authors
+// 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
diff --git a/tsdb/querier_test.go b/tsdb/querier_test.go
index a5efa35ceb..4387635959 100644
--- a/tsdb/querier_test.go
+++ b/tsdb/querier_test.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
@@ -23,6 +23,7 @@ import (
"slices"
"sort"
"strconv"
+ "strings"
"sync"
"testing"
"time"
@@ -140,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
@@ -149,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
@@ -159,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
}
@@ -317,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}},
),
}),
},
@@ -344,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}},
),
}),
},
@@ -368,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}},
),
}),
},
@@ -394,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}},
),
}),
},
@@ -453,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}},
),
}),
},
@@ -480,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}},
),
}),
},
@@ -536,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}},
),
}),
}
@@ -573,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}},
},
},
}
@@ -635,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}},
),
}),
},
@@ -662,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}},
),
}),
},
@@ -789,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++
@@ -870,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}},
@@ -886,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}},
@@ -906,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}},
@@ -938,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,
@@ -948,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.
@@ -980,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}},
@@ -1001,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}},
@@ -1022,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}},
@@ -1043,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}},
@@ -1062,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}),
@@ -1088,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}},
@@ -1114,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}},
@@ -1137,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}},
@@ -1162,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}},
@@ -1188,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}},
@@ -1211,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}},
@@ -1236,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}},
@@ -1262,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}},
@@ -1285,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}},
@@ -1310,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}},
@@ -1336,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}},
@@ -1359,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}},
@@ -1383,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}},
@@ -1416,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}},
@@ -1448,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}},
@@ -1479,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}},
@@ -1511,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{
@@ -1580,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{
@@ -1649,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{
@@ -1844,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}},
},
},
{
@@ -1853,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}},
},
},
{
@@ -1862,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)}},
},
},
}
@@ -1897,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}},
{},
},
},
@@ -1906,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}},
{},
},
},
@@ -1915,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)}},
{},
},
},
@@ -1947,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)}},
},
},
}
@@ -1990,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)}},
},
},
}
@@ -2095,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 {
@@ -2155,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 {
@@ -2293,10 +2298,6 @@ func (m mockIndex) LabelValues(_ context.Context, name string, hints *storage.La
return values, nil
}
-func (m mockIndex) LabelValueFor(_ context.Context, id storage.SeriesRef, label string) (string, error) {
- return m.series[id].l.Get(label), nil
-}
-
func (m mockIndex) LabelNamesFor(_ context.Context, postings index.Postings) ([]string, error) {
namesMap := make(map[string]bool)
for postings.Next() {
@@ -3037,14 +3038,14 @@ func TestPostingsForMatchers(t *testing.T) {
require.NoError(t, err)
for _, c := range cases {
- name := ""
+ var name strings.Builder
for i, matcher := range c.matchers {
if i > 0 {
- name += ","
+ name.WriteString(",")
}
- name += matcher.String()
+ name.WriteString(matcher.String())
}
- t.Run(name, func(t *testing.T) {
+ t.Run(name.String(), func(t *testing.T) {
exp := map[string]struct{}{}
for _, l := range c.exp {
exp[l.String()] = struct{}{}
@@ -3094,11 +3095,8 @@ func TestQuerierIndexQueriesRace(t *testing.T) {
for _, c := range testCases {
t.Run(fmt.Sprintf("%v", c.matchers), func(t *testing.T) {
t.Parallel()
- db := openTestDB(t, DefaultOptions(), nil)
+ db := newTestDB(t)
h := db.Head()
- t.Cleanup(func() {
- require.NoError(t, db.Close())
- })
ctx, cancel := context.WithCancel(context.Background())
wg := &sync.WaitGroup{}
wg.Add(1)
@@ -3317,10 +3315,6 @@ func (mockMatcherIndex) LabelValues(context.Context, string, *storage.LabelHints
return []string{}, errors.New("label values called")
}
-func (mockMatcherIndex) LabelValueFor(context.Context, storage.SeriesRef, string) (string, error) {
- return "", errors.New("label value for called")
-}
-
func (mockMatcherIndex) LabelNamesFor(context.Context, index.Postings) ([]string, error) {
return nil, errors.New("label names for called")
}
@@ -3496,10 +3490,7 @@ func TestBlockBaseSeriesSet(t *testing.T) {
}
func BenchmarkHeadChunkQuerier(b *testing.B) {
- db := openTestDB(b, nil, nil)
- defer func() {
- require.NoError(b, db.Close())
- }()
+ db := newTestDB(b)
// 3h of data.
numTimeseries := 100
@@ -3541,10 +3532,7 @@ func BenchmarkHeadChunkQuerier(b *testing.B) {
}
func BenchmarkHeadQuerier(b *testing.B) {
- db := openTestDB(b, nil, nil)
- defer func() {
- require.NoError(b, db.Close())
- }()
+ db := newTestDB(b)
// 3h of data.
numTimeseries := 100
@@ -3606,12 +3594,8 @@ func TestQueryWithDeletedHistograms(t *testing.T) {
for name, tc := range testcases {
t.Run(name, func(t *testing.T) {
- db := openTestDB(t, nil, nil)
- defer func() {
- require.NoError(t, db.Close())
- }()
-
- appender := db.Appender(context.Background())
+ db := newTestDB(t)
+ app := db.Appender(context.Background())
var (
err error
@@ -3621,12 +3605,11 @@ func TestQueryWithDeletedHistograms(t *testing.T) {
for i := range 100 {
h, fh := tc(i)
- seriesRef, err = appender.AppendHistogram(seriesRef, lbs, int64(i), h, fh)
+ seriesRef, err = app.AppendHistogram(seriesRef, lbs, int64(i), h, fh)
require.NoError(t, err)
}
- err = appender.Commit()
- require.NoError(t, err)
+ require.NoError(t, app.Commit())
matcher, err := labels.NewMatcher(labels.MatchEqual, "__name__", "test")
require.NoError(t, err)
@@ -3664,12 +3647,8 @@ func TestQueryWithDeletedHistograms(t *testing.T) {
func TestQueryWithOneChunkCompletelyDeleted(t *testing.T) {
ctx := context.Background()
- db := openTestDB(t, nil, nil)
- defer func() {
- require.NoError(t, db.Close())
- }()
-
- appender := db.Appender(context.Background())
+ db := newTestDB(t)
+ app := db.Appender(context.Background())
var (
err error
@@ -3680,12 +3659,12 @@ func TestQueryWithOneChunkCompletelyDeleted(t *testing.T) {
// Create an int histogram chunk with samples between 0 - 20 and 30 - 40.
for i := range 20 {
h := tsdbutil.GenerateTestHistogram(1)
- seriesRef, err = appender.AppendHistogram(seriesRef, lbs, int64(i), h, nil)
+ seriesRef, err = app.AppendHistogram(seriesRef, lbs, int64(i), h, nil)
require.NoError(t, err)
}
for i := 30; i < 40; i++ {
h := tsdbutil.GenerateTestHistogram(1)
- seriesRef, err = appender.AppendHistogram(seriesRef, lbs, int64(i), h, nil)
+ seriesRef, err = app.AppendHistogram(seriesRef, lbs, int64(i), h, nil)
require.NoError(t, err)
}
@@ -3693,12 +3672,11 @@ func TestQueryWithOneChunkCompletelyDeleted(t *testing.T) {
// type from int histograms so a new chunk is created.
for i := 60; i < 100; i++ {
fh := tsdbutil.GenerateTestFloatHistogram(1)
- seriesRef, err = appender.AppendHistogram(seriesRef, lbs, int64(i), nil, fh)
+ seriesRef, err = app.AppendHistogram(seriesRef, lbs, int64(i), nil, fh)
require.NoError(t, err)
}
- err = appender.Commit()
- require.NoError(t, err)
+ require.NoError(t, app.Commit())
matcher, err := labels.NewMatcher(labels.MatchEqual, "__name__", "test")
require.NoError(t, err)
@@ -3757,10 +3735,6 @@ func (mockReaderOfLabels) LabelValues(context.Context, string, *storage.LabelHin
return make([]string, mockReaderOfLabelsSeriesCount), nil
}
-func (mockReaderOfLabels) LabelValueFor(context.Context, storage.SeriesRef, string) (string, error) {
- panic("LabelValueFor called")
-}
-
func (mockReaderOfLabels) SortedLabelValues(context.Context, string, *storage.LabelHints, ...*labels.Matcher) ([]string, error) {
panic("SortedLabelValues called")
}
diff --git a/tsdb/record/record.go b/tsdb/record/record.go
index 561810a3a5..106b8e51bc 100644
--- a/tsdb/record/record.go
+++ b/tsdb/record/record.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Prometheus Authors
+// 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.
@@ -475,7 +475,9 @@ func (d *Decoder) HistogramSamples(rec []byte, histograms []RefHistogramSample)
// This is a very slow path, but it should only happen if the
// record is from a newer Prometheus version that supports higher
// resolution.
- rh.H.ReduceResolution(histogram.ExponentialSchemaMax)
+ if err := rh.H.ReduceResolution(histogram.ExponentialSchemaMax); err != nil {
+ return nil, fmt.Errorf("error reducing resolution of histogram #%d: %w", len(histograms)+1, err)
+ }
}
histograms = append(histograms, rh)
@@ -579,7 +581,9 @@ func (d *Decoder) FloatHistogramSamples(rec []byte, histograms []RefFloatHistogr
// This is a very slow path, but it should only happen if the
// record is from a newer Prometheus version that supports higher
// resolution.
- rh.FH.ReduceResolution(histogram.ExponentialSchemaMax)
+ if err := rh.FH.ReduceResolution(histogram.ExponentialSchemaMax); err != nil {
+ return nil, fmt.Errorf("error reducing resolution of histogram #%d: %w", len(histograms)+1, err)
+ }
}
histograms = append(histograms, rh)
diff --git a/tsdb/record/record_test.go b/tsdb/record/record_test.go
index bbbea04940..8ebd805d4d 100644
--- a/tsdb/record/record_test.go
+++ b/tsdb/record/record_test.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Prometheus Authors
+// 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.
diff --git a/tsdb/repair.go b/tsdb/repair.go
index 8bdc645b5e..4ef69c80ed 100644
--- a/tsdb/repair.go
+++ b/tsdb/repair.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Prometheus Authors
+// 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
@@ -15,13 +15,13 @@ package tsdb
import (
"encoding/json"
+ "errors"
"fmt"
"io"
"log/slog"
"os"
"path/filepath"
- tsdb_errors "github.com/prometheus/prometheus/tsdb/errors"
"github.com/prometheus/prometheus/tsdb/fileutil"
)
@@ -82,20 +82,22 @@ func repairBadIndexVersion(logger *slog.Logger, dir string) error {
// Set the 5th byte to 2 to indicate the correct file format version.
if _, err := repl.WriteAt([]byte{2}, 4); err != nil {
- errs := tsdb_errors.NewMulti(
- fmt.Errorf("rewrite of index.repaired for block dir: %v: %w", d, err))
- if err := repl.Close(); err != nil {
- errs.Add(fmt.Errorf("close: %w", err))
+ errs := []error{
+ fmt.Errorf("rewrite of index.repaired for block dir: %v: %w", d, err),
}
- return errs.Err()
+ if err := repl.Close(); err != nil {
+ errs = append(errs, fmt.Errorf("close: %w", err))
+ }
+ return errors.Join(errs...)
}
if err := repl.Sync(); err != nil {
- errs := tsdb_errors.NewMulti(
- fmt.Errorf("sync of index.repaired for block dir: %v: %w", d, err))
- if err := repl.Close(); err != nil {
- errs.Add(fmt.Errorf("close: %w", err))
+ errs := []error{
+ fmt.Errorf("sync of index.repaired for block dir: %v: %w", d, err),
}
- return errs.Err()
+ if err := repl.Close(); err != nil {
+ errs = append(errs, fmt.Errorf("close: %w", err))
+ }
+ return errors.Join(errs...)
}
if err := repl.Close(); err != nil {
return fmt.Errorf("close repaired index for block dir: %v: %w", d, err)
diff --git a/tsdb/repair_test.go b/tsdb/repair_test.go
index 8a192c4f78..34fe85f422 100644
--- a/tsdb/repair_test.go
+++ b/tsdb/repair_test.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Prometheus Authors
+// 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
diff --git a/tsdb/testutil.go b/tsdb/testutil.go
index 4d413322c8..feb921447d 100644
--- a/tsdb/testutil.go
+++ b/tsdb/testutil.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
@@ -44,14 +44,14 @@ type testValue struct {
type sampleTypeScenario struct {
sampleType string
- appendFunc func(appender storage.Appender, lbls labels.Labels, ts, value int64) (storage.SeriesRef, sample, error)
+ appendFunc func(appender storage.LimitedAppenderV1, lbls labels.Labels, ts, value int64) (storage.SeriesRef, sample, error)
sampleFunc func(ts, value int64) sample
}
var sampleTypeScenarios = map[string]sampleTypeScenario{
float: {
sampleType: sampleMetricTypeFloat,
- appendFunc: func(appender storage.Appender, lbls labels.Labels, ts, value int64) (storage.SeriesRef, sample, error) {
+ appendFunc: func(appender storage.LimitedAppenderV1, lbls labels.Labels, ts, value int64) (storage.SeriesRef, sample, error) {
s := sample{t: ts, f: float64(value)}
ref, err := appender.Append(0, lbls, ts, s.f)
return ref, s, err
@@ -62,7 +62,7 @@ var sampleTypeScenarios = map[string]sampleTypeScenario{
},
intHistogram: {
sampleType: sampleMetricTypeHistogram,
- appendFunc: func(appender storage.Appender, lbls labels.Labels, ts, value int64) (storage.SeriesRef, sample, error) {
+ appendFunc: func(appender storage.LimitedAppenderV1, lbls labels.Labels, ts, value int64) (storage.SeriesRef, sample, error) {
s := sample{t: ts, h: tsdbutil.GenerateTestHistogram(value)}
ref, err := appender.AppendHistogram(0, lbls, ts, s.h, nil)
return ref, s, err
@@ -73,7 +73,7 @@ var sampleTypeScenarios = map[string]sampleTypeScenario{
},
floatHistogram: {
sampleType: sampleMetricTypeHistogram,
- appendFunc: func(appender storage.Appender, lbls labels.Labels, ts, value int64) (storage.SeriesRef, sample, error) {
+ appendFunc: func(appender storage.LimitedAppenderV1, lbls labels.Labels, ts, value int64) (storage.SeriesRef, sample, error) {
s := sample{t: ts, fh: tsdbutil.GenerateTestFloatHistogram(value)}
ref, err := appender.AppendHistogram(0, lbls, ts, nil, s.fh)
return ref, s, err
@@ -84,7 +84,7 @@ var sampleTypeScenarios = map[string]sampleTypeScenario{
},
customBucketsIntHistogram: {
sampleType: sampleMetricTypeHistogram,
- appendFunc: func(appender storage.Appender, lbls labels.Labels, ts, value int64) (storage.SeriesRef, sample, error) {
+ appendFunc: func(appender storage.LimitedAppenderV1, lbls labels.Labels, ts, value int64) (storage.SeriesRef, sample, error) {
s := sample{t: ts, h: tsdbutil.GenerateTestCustomBucketsHistogram(value)}
ref, err := appender.AppendHistogram(0, lbls, ts, s.h, nil)
return ref, s, err
@@ -95,7 +95,7 @@ var sampleTypeScenarios = map[string]sampleTypeScenario{
},
customBucketsFloatHistogram: {
sampleType: sampleMetricTypeHistogram,
- appendFunc: func(appender storage.Appender, lbls labels.Labels, ts, value int64) (storage.SeriesRef, sample, error) {
+ appendFunc: func(appender storage.LimitedAppenderV1, lbls labels.Labels, ts, value int64) (storage.SeriesRef, sample, error) {
s := sample{t: ts, fh: tsdbutil.GenerateTestCustomBucketsFloatHistogram(value)}
ref, err := appender.AppendHistogram(0, lbls, ts, nil, s.fh)
return ref, s, err
@@ -106,7 +106,7 @@ var sampleTypeScenarios = map[string]sampleTypeScenario{
},
gaugeIntHistogram: {
sampleType: sampleMetricTypeHistogram,
- appendFunc: func(appender storage.Appender, lbls labels.Labels, ts, value int64) (storage.SeriesRef, sample, error) {
+ appendFunc: func(appender storage.LimitedAppenderV1, lbls labels.Labels, ts, value int64) (storage.SeriesRef, sample, error) {
s := sample{t: ts, h: tsdbutil.GenerateTestGaugeHistogram(value)}
ref, err := appender.AppendHistogram(0, lbls, ts, s.h, nil)
return ref, s, err
@@ -117,7 +117,7 @@ var sampleTypeScenarios = map[string]sampleTypeScenario{
},
gaugeFloatHistogram: {
sampleType: sampleMetricTypeHistogram,
- appendFunc: func(appender storage.Appender, lbls labels.Labels, ts, value int64) (storage.SeriesRef, sample, error) {
+ appendFunc: func(appender storage.LimitedAppenderV1, lbls labels.Labels, ts, value int64) (storage.SeriesRef, sample, error) {
s := sample{t: ts, fh: tsdbutil.GenerateTestGaugeFloatHistogram(value)}
ref, err := appender.AppendHistogram(0, lbls, ts, nil, s.fh)
return ref, s, err
diff --git a/tsdb/tombstones/tombstones.go b/tsdb/tombstones/tombstones.go
index bda565eae4..b7bcd8801b 100644
--- a/tsdb/tombstones/tombstones.go
+++ b/tsdb/tombstones/tombstones.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
@@ -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/tombstones/tombstones_test.go b/tsdb/tombstones/tombstones_test.go
index de036e22d0..17802672c6 100644
--- a/tsdb/tombstones/tombstones_test.go
+++ b/tsdb/tombstones/tombstones_test.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
diff --git a/tsdb/tsdbblockutil.go b/tsdb/tsdbblockutil.go
index af2348019a..1c6882b085 100644
--- a/tsdb/tsdbblockutil.go
+++ b/tsdb/tsdbblockutil.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The Prometheus Authors
+// 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
diff --git a/tsdb/tsdbutil/dir_locker.go b/tsdb/tsdbutil/dir_locker.go
index 4b69e1f9d6..139e66859a 100644
--- a/tsdb/tsdbutil/dir_locker.go
+++ b/tsdb/tsdbutil/dir_locker.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
@@ -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/tsdbutil/dir_locker_test.go b/tsdb/tsdbutil/dir_locker_test.go
index 8c027415d3..e3f323932a 100644
--- a/tsdb/tsdbutil/dir_locker_test.go
+++ b/tsdb/tsdbutil/dir_locker_test.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
diff --git a/tsdb/tsdbutil/dir_locker_testutil.go b/tsdb/tsdbutil/dir_locker_testutil.go
index 5a335989c7..ffbf039339 100644
--- a/tsdb/tsdbutil/dir_locker_testutil.go
+++ b/tsdb/tsdbutil/dir_locker_testutil.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
diff --git a/tsdb/tsdbutil/histogram.go b/tsdb/tsdbutil/histogram.go
index 64311a8c3b..e6a67c8212 100644
--- a/tsdb/tsdbutil/histogram.go
+++ b/tsdb/tsdbutil/histogram.go
@@ -1,4 +1,4 @@
-// Copyright 2023 The Prometheus Authors
+// 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
diff --git a/tsdb/tsdbutil/remove_tmp_dirs.go b/tsdb/tsdbutil/remove_tmp_dirs.go
new file mode 100644
index 0000000000..a95db3159e
--- /dev/null
+++ b/tsdb/tsdbutil/remove_tmp_dirs.go
@@ -0,0 +1,45 @@
+// Copyright 2018 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 tsdbutil
+
+import (
+ "io/fs"
+ "log/slog"
+ "os"
+ "path/filepath"
+)
+
+// RemoveTmpDirs attempts to remove directories in the specified directory which match the isTmpDir predicate.
+// Errors encountered during reading the directory that other than non-existence are returned. All other errors
+// encountered during removal of tmp directories are logged but do not cause early termination.
+func RemoveTmpDirs(l *slog.Logger, dir string, isTmpDir func(fi fs.DirEntry) bool) error {
+ files, err := os.ReadDir(dir)
+ if os.IsNotExist(err) {
+ return nil
+ }
+ if err != nil {
+ return err
+ }
+ for _, f := range files {
+ if isTmpDir(f) {
+ if err := os.RemoveAll(filepath.Join(dir, f.Name())); err != nil {
+ l.Error("failed to delete tmp dir", "dir", filepath.Join(dir, f.Name()), "err", err)
+ continue
+ }
+ l.Info("Found and deleted tmp dir", "dir", filepath.Join(dir, f.Name()))
+ }
+ }
+ return nil
+}
diff --git a/tsdb/tsdbutil/remove_tmp_dirs_test.go b/tsdb/tsdbutil/remove_tmp_dirs_test.go
new file mode 100644
index 0000000000..4ab282d3b3
--- /dev/null
+++ b/tsdb/tsdbutil/remove_tmp_dirs_test.go
@@ -0,0 +1,124 @@
+// Copyright 2018 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 tsdbutil
+
+import (
+ "io/fs"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/prometheus/common/promslog"
+ "github.com/stretchr/testify/require"
+)
+
+func TestRemoveTmpDirs(t *testing.T) {
+ tests := []struct {
+ name string
+ isTmpDir func(fi fs.DirEntry) bool
+ setup func(t *testing.T, dir string)
+ expectedDirs []string // Directories that should remain after cleanup
+ }{
+ {
+ name: "remove directories with tmp prefix",
+ isTmpDir: func(fi fs.DirEntry) bool {
+ return fi.IsDir() && strings.HasPrefix(fi.Name(), "tmp")
+ },
+ setup: func(t *testing.T, dir string) {
+ require.NoError(t, os.Mkdir(filepath.Join(dir, "tmpdir1"), 0o755))
+ require.NoError(t, os.Mkdir(filepath.Join(dir, "tmpdir2"), 0o755))
+ require.NoError(t, os.Mkdir(filepath.Join(dir, "normaldir"), 0o755))
+ },
+ expectedDirs: []string{"normaldir"},
+ },
+ {
+ name: "remove directories with specific suffix",
+ isTmpDir: func(fi fs.DirEntry) bool {
+ return fi.IsDir() && strings.HasSuffix(fi.Name(), ".tmp")
+ },
+ setup: func(t *testing.T, dir string) {
+ require.NoError(t, os.Mkdir(filepath.Join(dir, "data.tmp"), 0o755))
+ require.NoError(t, os.Mkdir(filepath.Join(dir, "cache.tmp"), 0o755))
+ require.NoError(t, os.Mkdir(filepath.Join(dir, "permanent"), 0o755))
+ },
+ expectedDirs: []string{"permanent"},
+ },
+ {
+ name: "no temporary directories to remove",
+ isTmpDir: func(fi fs.DirEntry) bool {
+ return fi.IsDir() && strings.HasPrefix(fi.Name(), "tmp")
+ },
+ setup: func(t *testing.T, dir string) {
+ require.NoError(t, os.Mkdir(filepath.Join(dir, "normaldir1"), 0o755))
+ require.NoError(t, os.Mkdir(filepath.Join(dir, "normaldir2"), 0o755))
+ },
+ expectedDirs: []string{"normaldir1", "normaldir2"},
+ },
+ {
+ name: "empty directory",
+ isTmpDir: func(fi fs.DirEntry) bool {
+ return fi.IsDir() && strings.HasPrefix(fi.Name(), "tmp")
+ },
+ setup: func(_ *testing.T, _ string) {}, // No setup needed - directory is empty
+ expectedDirs: []string{},
+ },
+ {
+ name: "directory with files only (no directories)",
+ isTmpDir: func(fi fs.DirEntry) bool {
+ return fi.IsDir() && strings.HasPrefix(fi.Name(), "tmp")
+ },
+ setup: func(t *testing.T, dir string) {
+ require.NoError(t, os.WriteFile(filepath.Join(dir, "tmpfile1.txt"), []byte("test"), 0o644))
+ require.NoError(t, os.WriteFile(filepath.Join(dir, "tmpfile2.txt"), []byte("test"), 0o644))
+ },
+ expectedDirs: []string{},
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ testDir := t.TempDir()
+
+ if tt.setup != nil {
+ tt.setup(t, testDir)
+ }
+
+ require.NoError(t, RemoveTmpDirs(promslog.NewNopLogger(), testDir, tt.isTmpDir))
+
+ entries, err := os.ReadDir(testDir)
+ require.NoError(t, err)
+
+ // Get actual remaining directories
+ var actualDirs []string
+ for _, entry := range entries {
+ if entry.IsDir() {
+ actualDirs = append(actualDirs, entry.Name())
+ }
+ }
+
+ require.ElementsMatch(t, tt.expectedDirs, actualDirs, "Remaining directories don't match expected")
+ })
+ }
+}
+
+func TestRemoveTmpDirs_NonExistentDirectory(t *testing.T) {
+ testDir := t.TempDir()
+ nonExistent := filepath.Join(testDir, "does_not_exist")
+
+ require.NoError(t, RemoveTmpDirs(promslog.NewNopLogger(), nonExistent, func(_ fs.DirEntry) bool {
+ return true
+ }))
+}
diff --git a/tsdb/wlog/checkpoint.go b/tsdb/wlog/checkpoint.go
index c26f3f1052..3a4e194fec 100644
--- a/tsdb/wlog/checkpoint.go
+++ b/tsdb/wlog/checkpoint.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Prometheus Authors
+// 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.
@@ -18,6 +18,7 @@ import (
"errors"
"fmt"
"io"
+ "io/fs"
"log/slog"
"math"
"os"
@@ -28,10 +29,10 @@ 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"
+ "github.com/prometheus/prometheus/tsdb/tsdbutil"
)
// CheckpointStats returns stats about a created checkpoint.
@@ -71,18 +72,26 @@ 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.
-const CheckpointPrefix = "checkpoint."
+// checkpointTempFileSuffix is the suffix used when creating temporary checkpoint files.
+const checkpointTempFileSuffix = ".tmp"
+
+// DeleteTempCheckpoints deletes all temporary checkpoint directories in the given directory.
+func DeleteTempCheckpoints(logger *slog.Logger, dir string) error {
+ if err := tsdbutil.RemoveTmpDirs(logger, dir, isTempDir); err != nil {
+ return fmt.Errorf("remove previous temporary checkpoint dirs: %w", err)
+ }
+ return nil
+}
// Checkpoint creates a compacted checkpoint of segments in range [from, to] in the given WAL.
// It includes the most recent checkpoint if it exists.
@@ -124,13 +133,13 @@ func Checkpoint(logger *slog.Logger, w *WL, from, to int, keep func(id chunks.He
defer sgmReader.Close()
}
- cpdir := checkpointDir(w.Dir(), to)
- cpdirtmp := cpdir + ".tmp"
-
- if err := os.RemoveAll(cpdirtmp); err != nil {
- return nil, fmt.Errorf("remove previous temporary checkpoint dir: %w", err)
+ if err := DeleteTempCheckpoints(logger, w.Dir()); err != nil {
+ return nil, err
}
+ cpdir := checkpointDir(w.Dir(), to)
+ cpdirtmp := cpdir + checkpointTempFileSuffix
+
if err := os.MkdirAll(cpdirtmp, 0o777); err != nil {
return nil, fmt.Errorf("create checkpoint dir: %w", err)
}
@@ -395,8 +404,11 @@ func Checkpoint(logger *slog.Logger, w *WL, from, to int, keep func(id chunks.He
return stats, nil
}
+// checkpointPrefix is the prefix used for checkpoint files.
+const checkpointPrefix = "checkpoint."
+
func checkpointDir(dir string, i int) string {
- return filepath.Join(dir, fmt.Sprintf(CheckpointPrefix+"%08d", i))
+ return filepath.Join(dir, fmt.Sprintf(checkpointPrefix+"%08d", i))
}
type checkpointRef struct {
@@ -412,13 +424,13 @@ func listCheckpoints(dir string) (refs []checkpointRef, err error) {
for i := range files {
fi := files[i]
- if !strings.HasPrefix(fi.Name(), CheckpointPrefix) {
+ if !strings.HasPrefix(fi.Name(), checkpointPrefix) {
continue
}
if !fi.IsDir() {
return nil, fmt.Errorf("checkpoint %s is not a directory", fi.Name())
}
- idx, err := strconv.Atoi(fi.Name()[len(CheckpointPrefix):])
+ idx, err := strconv.Atoi(fi.Name()[len(checkpointPrefix):])
if err != nil {
continue
}
@@ -432,3 +444,7 @@ func listCheckpoints(dir string) (refs []checkpointRef, err error) {
return refs, nil
}
+
+func isTempDir(fi fs.DirEntry) bool {
+ return strings.HasPrefix(fi.Name(), checkpointPrefix) && strings.HasSuffix(fi.Name(), checkpointTempFileSuffix)
+}
diff --git a/tsdb/wlog/checkpoint_test.go b/tsdb/wlog/checkpoint_test.go
index b83724ea2e..a348239ec7 100644
--- a/tsdb/wlog/checkpoint_test.go
+++ b/tsdb/wlog/checkpoint_test.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Prometheus Authors
+// 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.
@@ -417,3 +417,81 @@ func TestCheckpointNoTmpFolderAfterError(t *testing.T) {
})
require.NoError(t, err)
}
+
+func TestCheckpointDeletesTemporaryCheckpoints(t *testing.T) {
+ dir := t.TempDir()
+
+ // Create one tmp checkpoint directory
+ require.NoError(t, os.MkdirAll(filepath.Join(dir, "checkpoint.00001000.tmp"), 0o777))
+
+ w, err := New(nil, nil, dir, compression.None)
+ require.NoError(t, err)
+ defer w.Close()
+
+ _, err = Checkpoint(promslog.NewNopLogger(), w, 0, 1000, func(_ chunks.HeadSeriesRef) bool { return true }, 1000)
+ require.NoError(t, err)
+
+ files, err := os.ReadDir(dir)
+ require.NoError(t, err)
+
+ var actualDirectories []string
+ for _, f := range files {
+ if !f.IsDir() {
+ continue
+ }
+ actualDirectories = append(actualDirectories, f.Name())
+ }
+ require.Equal(t, []string{"checkpoint.00001000"}, actualDirectories)
+}
+
+func TestDeleteTempCheckpoints(t *testing.T) {
+ testCases := []struct {
+ name string
+ checkpointDirectoriesToCreate []string
+ expectedDirectories []string
+ }{
+ {
+ name: "no tmp checkpoints",
+ checkpointDirectoriesToCreate: nil,
+ expectedDirectories: nil,
+ },
+ {
+ name: "one tmp checkpoint",
+ checkpointDirectoriesToCreate: []string{"checkpoint.00001000.tmp"},
+ expectedDirectories: nil,
+ },
+ {
+ name: "many tmp checkpoints",
+ checkpointDirectoriesToCreate: []string{"checkpoint.00000001.tmp", "checkpoint.00001000.tmp", "checkpoint.00002000.tmp"},
+ expectedDirectories: nil,
+ },
+ {
+ name: "mix of tmp and regular checkpoints",
+ checkpointDirectoriesToCreate: []string{"checkpoint.00000001", "checkpoint.00000001.tmp", "checkpoint.00001000.tmp", "checkpoint.00002000"},
+ expectedDirectories: []string{"checkpoint.00000001", "checkpoint.00002000"},
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ dir := t.TempDir()
+ for _, fn := range tc.checkpointDirectoriesToCreate {
+ require.NoError(t, os.MkdirAll(filepath.Join(dir, fn), 0o777))
+ }
+
+ require.NoError(t, DeleteTempCheckpoints(promslog.NewNopLogger(), dir))
+
+ files, err := os.ReadDir(dir)
+ require.NoError(t, err)
+
+ var actualDirectories []string
+ for _, f := range files {
+ if !f.IsDir() {
+ continue
+ }
+ actualDirectories = append(actualDirectories, f.Name())
+ }
+ require.Equal(t, tc.expectedDirectories, actualDirectories)
+ })
+ }
+}
diff --git a/tsdb/wlog/live_reader.go b/tsdb/wlog/live_reader.go
index 004c397270..359f29274b 100644
--- a/tsdb/wlog/live_reader.go
+++ b/tsdb/wlog/live_reader.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The Prometheus Authors
+// 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.
diff --git a/tsdb/wlog/reader.go b/tsdb/wlog/reader.go
index c559d85b89..54b1baf4c4 100644
--- a/tsdb/wlog/reader.go
+++ b/tsdb/wlog/reader.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The Prometheus Authors
+// 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.
diff --git a/tsdb/wlog/reader_test.go b/tsdb/wlog/reader_test.go
index 1ddc33e2c8..9381fe99b5 100644
--- a/tsdb/wlog/reader_test.go
+++ b/tsdb/wlog/reader_test.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The Prometheus Authors
+// 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.
@@ -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 all errors.
+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/wlog/watcher.go b/tsdb/wlog/watcher.go
index abb5ef9731..a841a44fc8 100644
--- a/tsdb/wlog/watcher.go
+++ b/tsdb/wlog/watcher.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Prometheus Authors
+// 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
diff --git a/tsdb/wlog/watcher_test.go b/tsdb/wlog/watcher_test.go
index 9e6ea65a7f..b9a6504298 100644
--- a/tsdb/wlog/watcher_test.go
+++ b/tsdb/wlog/watcher_test.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Prometheus Authors
+// 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
diff --git a/tsdb/wlog/wlog.go b/tsdb/wlog/wlog.go
index 176531c478..5a80d58abf 100644
--- a/tsdb/wlog/wlog.go
+++ b/tsdb/wlog/wlog.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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.
diff --git a/tsdb/wlog/wlog_test.go b/tsdb/wlog/wlog_test.go
index 1ade42d3ff..79955d499c 100644
--- a/tsdb/wlog/wlog_test.go
+++ b/tsdb/wlog/wlog_test.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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.
diff --git a/util/almost/almost.go b/util/almost/almost.go
index 5f866b89b3..b89f968db6 100644
--- a/util/almost/almost.go
+++ b/util/almost/almost.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Prometheus Authors
+// 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
diff --git a/util/almost/almost_test.go b/util/almost/almost_test.go
index fba37f13f6..4e225bf862 100644
--- a/util/almost/almost_test.go
+++ b/util/almost/almost_test.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Prometheus Authors
+// 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
diff --git a/util/annotations/annotations.go b/util/annotations/annotations.go
index 817f670b5e..550b9fcdc5 100644
--- a/util/annotations/annotations.go
+++ b/util/annotations/annotations.go
@@ -1,4 +1,4 @@
-// Copyright 2023 The Prometheus Authors
+// 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
@@ -16,7 +16,7 @@ package annotations
import (
"errors"
"fmt"
- "maps"
+ "time"
"github.com/prometheus/common/model"
@@ -43,12 +43,18 @@ func (a *Annotations) Add(err error) Annotations {
if *a == nil {
*a = Annotations{}
}
+ if prevErr, exists := (*a)[err.Error()]; exists {
+ var anErr annoError
+ if errors.As(err, &anErr) {
+ err = anErr.Merge(prevErr)
+ }
+ }
(*a)[err.Error()] = err
return *a
}
-// Merge adds the contents of the second annotation to the first, modifying
-// the first in-place, and returns the merged first Annotation for convenience.
+// Merge adds the contents of the second set of Annotations to the first, modifying
+// the first in-place, and returns the merged first Annotations for convenience.
func (a *Annotations) Merge(aa Annotations) Annotations {
if *a == nil {
if aa == nil {
@@ -56,7 +62,15 @@ func (a *Annotations) Merge(aa Annotations) Annotations {
}
*a = Annotations{}
}
- maps.Copy((*a), aa)
+ for key, val := range aa {
+ if prevVal, exists := (*a)[key]; exists {
+ var anErr annoError
+ if errors.As(val, &anErr) {
+ val = anErr.Merge(prevVal)
+ }
+ }
+ (*a)[key] = val
+ }
return *a
}
@@ -81,10 +95,9 @@ func (a Annotations) AsStrings(query string, maxWarnings, maxInfos int) (warning
warnSkipped := 0
infoSkipped := 0
for _, err := range a {
- var anErr annoErr
+ var anErr annoError
if errors.As(err, &anErr) {
- anErr.Query = query
- err = anErr
+ anErr.SetQuery(query)
}
switch {
case errors.Is(err, PromQLInfo):
@@ -157,23 +170,48 @@ var (
MismatchedCustomBucketsHistogramsInfo = fmt.Errorf("%w: mismatched custom buckets were reconciled during", PromQLInfo)
)
+// annoError extends the standard error interface to provide additional functionality
+// for PromQL annotations, allowing them to be merged with other similar errors.
+type annoError interface {
+ error
+ // Necessary so we can use errors.Is() to disambiguate between warning and info.
+ Unwrap() error
+ // Necessary when we want to show position info. Also, this is only called at the end when we call
+ // AsStrings(), so before that we deduplicate based on the raw error string when query is empty,
+ // and the full error string with details will only be shown in the end when query is set.
+ SetQuery(string)
+ // We can define custom merge functions to merge individual annotations of the same type if they have
+ // the same raw error string.
+ Merge(error) error
+}
+
type annoErr struct {
PositionRange posrange.PositionRange
Err error
Query string
}
-func (e annoErr) Error() string {
+func (e *annoErr) Error() string {
if e.Query == "" {
return e.Err.Error()
}
return fmt.Sprintf("%s (%s)", e.Err, e.PositionRange.StartPosInput(e.Query, 0))
}
-func (e annoErr) Unwrap() error {
+func (e *annoErr) Unwrap() error {
return e.Err
}
+func (e *annoErr) SetQuery(query string) {
+ e.Query = query
+}
+
+// We do not merge generic annotations, instead we just ignore the provided error
+// and return the original.
+func (e *annoErr) Merge(_ error) error {
+ return e
+}
+
func maybeAddMetricName(anno error, metricName string) error {
if metricName == "" {
return anno
@@ -184,7 +222,7 @@ func maybeAddMetricName(anno error, metricName string) error {
// NewInvalidQuantileWarning is used when the user specifies an invalid quantile
// value, i.e. a float that is outside the range [0, 1] or NaN.
func NewInvalidQuantileWarning(q float64, pos posrange.PositionRange) error {
- return annoErr{
+ return &annoErr{
PositionRange: pos,
Err: fmt.Errorf("%w, got %g", InvalidQuantileWarning, q),
}
@@ -193,7 +231,7 @@ func NewInvalidQuantileWarning(q float64, pos posrange.PositionRange) error {
// NewInvalidRatioWarning is used when the user specifies an invalid ratio
// value, i.e. a float that is outside the range [-1, 1] or NaN.
func NewInvalidRatioWarning(q, to float64, pos posrange.PositionRange) error {
- return annoErr{
+ return &annoErr{
PositionRange: pos,
Err: fmt.Errorf("%w, got %g, capping to %g", InvalidRatioWarning, q, to),
}
@@ -203,7 +241,7 @@ func NewInvalidRatioWarning(q, to float64, pos posrange.PositionRange) error {
// of a classic histogram.
func NewBadBucketLabelWarning(metricName, label string, pos posrange.PositionRange) error {
anno := maybeAddMetricName(fmt.Errorf("%w of %q", BadBucketLabelWarning, label), metricName)
- return annoErr{
+ return &annoErr{
PositionRange: pos,
Err: anno,
}
@@ -213,7 +251,7 @@ func NewBadBucketLabelWarning(metricName, label string, pos posrange.PositionRan
// float samples and histogram samples for functions that do not support mixed
// samples.
func NewMixedFloatsHistogramsWarning(metricName string, pos posrange.PositionRange) error {
- return annoErr{
+ return &annoErr{
PositionRange: pos,
Err: fmt.Errorf("%w metric name %q", MixedFloatsHistogramsWarning, metricName),
}
@@ -222,7 +260,7 @@ func NewMixedFloatsHistogramsWarning(metricName string, pos posrange.PositionRan
// NewMixedFloatsHistogramsAggWarning is used when the queried series includes both
// float samples and histogram samples in an aggregation.
func NewMixedFloatsHistogramsAggWarning(pos posrange.PositionRange) error {
- return annoErr{
+ return &annoErr{
PositionRange: pos,
Err: fmt.Errorf("%w aggregation", MixedFloatsHistogramsWarning),
}
@@ -231,7 +269,7 @@ func NewMixedFloatsHistogramsAggWarning(pos posrange.PositionRange) error {
// NewMixedClassicNativeHistogramsWarning is used when the queried series includes
// both classic and native histograms.
func NewMixedClassicNativeHistogramsWarning(metricName string, pos posrange.PositionRange) error {
- return annoErr{
+ return &annoErr{
PositionRange: pos,
Err: maybeAddMetricName(MixedClassicNativeHistogramsWarning, metricName),
}
@@ -240,7 +278,7 @@ func NewMixedClassicNativeHistogramsWarning(metricName string, pos posrange.Posi
// NewNativeHistogramNotCounterWarning is used when histogramRate is called
// with isCounter set to true on a gauge histogram.
func NewNativeHistogramNotCounterWarning(metricName string, pos posrange.PositionRange) error {
- return annoErr{
+ return &annoErr{
PositionRange: pos,
Err: fmt.Errorf("%w %q", NativeHistogramNotCounterWarning, metricName),
}
@@ -249,7 +287,7 @@ func NewNativeHistogramNotCounterWarning(metricName string, pos posrange.Positio
// NewNativeHistogramNotGaugeWarning is used when histogramRate is called
// with isCounter set to false on a counter histogram.
func NewNativeHistogramNotGaugeWarning(metricName string, pos posrange.PositionRange) error {
- return annoErr{
+ return &annoErr{
PositionRange: pos,
Err: fmt.Errorf("%w %q", NativeHistogramNotGaugeWarning, metricName),
}
@@ -258,7 +296,7 @@ func NewNativeHistogramNotGaugeWarning(metricName string, pos posrange.PositionR
// NewMixedExponentialCustomHistogramsWarning is used when the queried series includes
// histograms with both exponential and custom buckets schemas.
func NewMixedExponentialCustomHistogramsWarning(metricName string, pos posrange.PositionRange) error {
- return annoErr{
+ return &annoErr{
PositionRange: pos,
Err: fmt.Errorf("%w %q", MixedExponentialCustomHistogramsWarning, metricName),
}
@@ -267,7 +305,7 @@ func NewMixedExponentialCustomHistogramsWarning(metricName string, pos posrange.
// NewPossibleNonCounterInfo is used when a named counter metric with only float samples does not
// have the suffixes _total, _sum, _count, or _bucket.
func NewPossibleNonCounterInfo(metricName string, pos posrange.PositionRange) error {
- return annoErr{
+ return &annoErr{
PositionRange: pos,
Err: fmt.Errorf("%w %q", PossibleNonCounterInfo, metricName),
}
@@ -276,25 +314,84 @@ func NewPossibleNonCounterInfo(metricName string, pos posrange.PositionRange) er
// NewPossibleNonCounterLabelInfo is used when a named counter metric with only float samples does not
// have the __type__ label set to "counter".
func NewPossibleNonCounterLabelInfo(metricName, typeLabel string, pos posrange.PositionRange) error {
- return annoErr{
+ return &annoErr{
PositionRange: pos,
Err: fmt.Errorf("%w, got %q: %q", PossibleNonCounterLabelInfo, typeLabel, metricName),
}
}
+type histogramQuantileForcedMonotonicityErr struct {
+ PositionRange posrange.PositionRange
+ Err error
+ Query string
+ minTs, maxTs int64
+ minBucket, maxBucket, maxDiff float64
+ count int
+}
+
+func (e *histogramQuantileForcedMonotonicityErr) Error() string {
+ if e.Query == "" {
+ return e.Err.Error()
+ }
+ startTime := time.Unix(e.minTs/1000, 0).UTC().Format(time.RFC3339)
+ endTime := time.Unix(e.maxTs/1000, 0).UTC().Format(time.RFC3339)
+ return fmt.Sprintf("%s, from buckets %g to %g, with a max diff of %.2g, over %d samples from %s to %s (%s)", e.Err, e.minBucket, e.maxBucket, e.maxDiff, e.count+1, startTime, endTime, e.PositionRange.StartPosInput(e.Query, 0))
+}
+
+func (e *histogramQuantileForcedMonotonicityErr) Unwrap() error {
+ return e.Err
+}
+
+func (e *histogramQuantileForcedMonotonicityErr) SetQuery(query string) {
+ e.Query = query
+}
+
+func (e *histogramQuantileForcedMonotonicityErr) Merge(other error) error {
+ o := &histogramQuantileForcedMonotonicityErr{}
+ ok := errors.As(other, &o)
+ if !ok {
+ return e
+ }
+ if e.Err.Error() != o.Err.Error() {
+ return e
+ }
+ if e.minTs < o.minTs {
+ o.minTs = e.minTs
+ }
+ if e.maxTs > o.maxTs {
+ o.maxTs = e.maxTs
+ }
+ if e.minBucket < o.minBucket {
+ o.minBucket = e.minBucket
+ }
+ if e.maxBucket > o.maxBucket {
+ o.maxBucket = e.maxBucket
+ }
+ if e.maxDiff > o.maxDiff {
+ o.maxDiff = e.maxDiff
+ }
+ o.count += e.count + 1
+ return o
+}
+
// NewHistogramQuantileForcedMonotonicityInfo is used when the input (classic histograms) to
// histogram_quantile needs to be forced to be monotonic.
-func NewHistogramQuantileForcedMonotonicityInfo(metricName string, pos posrange.PositionRange) error {
- return annoErr{
+func NewHistogramQuantileForcedMonotonicityInfo(metricName string, pos posrange.PositionRange, ts int64, minBucket, maxBucket, maxDiff float64) error {
+ return &histogramQuantileForcedMonotonicityErr{
PositionRange: pos,
Err: maybeAddMetricName(HistogramQuantileForcedMonotonicityInfo, metricName),
+ minTs: ts,
+ maxTs: ts,
+ minBucket: minBucket,
+ maxBucket: maxBucket,
+ maxDiff: maxDiff,
}
}
// NewIncompatibleTypesInBinOpInfo is used if binary operators act on a
// combination of types that doesn't work and therefore returns no result.
func NewIncompatibleTypesInBinOpInfo(lhsType, operator, rhsType string, pos posrange.PositionRange) error {
- return annoErr{
+ return &annoErr{
PositionRange: pos,
Err: fmt.Errorf("%w %q: %s %s %s", IncompatibleTypesInBinOpInfo, operator, lhsType, operator, rhsType),
}
@@ -303,7 +400,7 @@ func NewIncompatibleTypesInBinOpInfo(lhsType, operator, rhsType string, pos posr
// NewHistogramIgnoredInAggregationInfo is used when a histogram is ignored by
// an aggregation operator that cannot handle histograms.
func NewHistogramIgnoredInAggregationInfo(aggregation string, pos posrange.PositionRange) error {
- return annoErr{
+ return &annoErr{
PositionRange: pos,
Err: fmt.Errorf("%w %s aggregation", HistogramIgnoredInAggregationInfo, aggregation),
}
@@ -312,7 +409,7 @@ func NewHistogramIgnoredInAggregationInfo(aggregation string, pos posrange.Posit
// NewHistogramIgnoredInMixedRangeInfo is used when a histogram is ignored
// in a range vector which contains mix of floats and histograms.
func NewHistogramIgnoredInMixedRangeInfo(metricName string, pos posrange.PositionRange) error {
- return annoErr{
+ return &annoErr{
PositionRange: pos,
Err: fmt.Errorf("%w %q", HistogramIgnoredInMixedRangeInfo, metricName),
}
@@ -321,28 +418,28 @@ func NewHistogramIgnoredInMixedRangeInfo(metricName string, pos posrange.Positio
// NewIncompatibleBucketLayoutInBinOpWarning is used if binary operators act on a
// combination of two incompatible histograms.
func NewIncompatibleBucketLayoutInBinOpWarning(operator string, pos posrange.PositionRange) error {
- return annoErr{
+ return &annoErr{
PositionRange: pos,
Err: fmt.Errorf("%w %s", IncompatibleBucketLayoutInBinOpWarning, operator),
}
}
func NewNativeHistogramQuantileNaNResultInfo(metricName string, pos posrange.PositionRange) error {
- return annoErr{
+ return &annoErr{
PositionRange: pos,
Err: maybeAddMetricName(NativeHistogramQuantileNaNResultInfo, metricName),
}
}
func NewNativeHistogramQuantileNaNSkewInfo(metricName string, pos posrange.PositionRange) error {
- return annoErr{
+ return &annoErr{
PositionRange: pos,
Err: maybeAddMetricName(NativeHistogramQuantileNaNSkewInfo, metricName),
}
}
func NewNativeHistogramFractionNaNsInfo(metricName string, pos posrange.PositionRange) error {
- return annoErr{
+ return &annoErr{
PositionRange: pos,
Err: maybeAddMetricName(NativeHistogramFractionNaNsInfo, metricName),
}
@@ -368,7 +465,7 @@ func (op HistogramOperation) String() string {
// NewHistogramCounterResetCollisionWarning is used when two counter histograms are added or subtracted where one has
// a CounterReset hint and the other has NotCounterReset.
func NewHistogramCounterResetCollisionWarning(pos posrange.PositionRange, operation HistogramOperation) error {
- return annoErr{
+ return &annoErr{
PositionRange: pos,
Err: fmt.Errorf("%w %s", HistogramCounterResetCollisionWarning, operation.String()),
}
@@ -377,7 +474,7 @@ func NewHistogramCounterResetCollisionWarning(pos posrange.PositionRange, operat
// NewMismatchedCustomBucketsHistogramsInfo is used when the queried series includes
// custom buckets histograms with mismatched custom bounds that cause reconciling.
func NewMismatchedCustomBucketsHistogramsInfo(pos posrange.PositionRange, operation HistogramOperation) error {
- return annoErr{
+ return &annoErr{
PositionRange: pos,
Err: fmt.Errorf("%w %s", MismatchedCustomBucketsHistogramsInfo, operation.String()),
}
diff --git a/util/annotations/annotations_test.go b/util/annotations/annotations_test.go
new file mode 100644
index 0000000000..39fb8e62f4
--- /dev/null
+++ b/util/annotations/annotations_test.go
@@ -0,0 +1,114 @@
+// Copyright 2024 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 annotations
+
+import (
+ "errors"
+ "fmt"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/prometheus/prometheus/promql/parser/posrange"
+)
+
+func TestAnnotations_AsStrings(t *testing.T) {
+ var annos Annotations
+ pos := posrange.PositionRange{Start: 3, End: 8}
+
+ annos.Add(errors.New("this is a non-annotation error"))
+
+ annos.Add(NewInvalidRatioWarning(1.1, 100, pos))
+ annos.Add(NewInvalidRatioWarning(1.2, 123, pos))
+
+ annos.Add(newTestCustomWarning(1.5, pos, 12, 14))
+ annos.Add(newTestCustomWarning(1.5, pos, 10, 20))
+ annos.Add(newTestCustomWarning(1.5, pos, 5, 15))
+ annos.Add(newTestCustomWarning(1.5, pos, 12, 14))
+
+ annos.Add(NewHistogramIgnoredInAggregationInfo("sum", pos))
+
+ annos.Add(NewHistogramQuantileForcedMonotonicityInfo("series_1", pos, 1735084800000, 5, 50, 5.5))
+ annos.Add(NewHistogramQuantileForcedMonotonicityInfo("series_1", pos, 1703462400000, 10, 100, 10))
+ annos.Add(NewHistogramQuantileForcedMonotonicityInfo("series_1", pos, 1733011200000, 2.5, 75, 7.5))
+
+ warnings, infos := annos.AsStrings("lorem ipsum dolor sit amet", 0, 0)
+ require.ElementsMatch(t, warnings, []string{
+ "this is a non-annotation error",
+ "PromQL warning: ratio value should be between -1 and 1, got 1.1, capping to 100 (1:4)",
+ "PromQL warning: ratio value should be between -1 and 1, got 1.2, capping to 123 (1:4)",
+ "PromQL warning: custom value set to 1.5, 4 instances with smallest 5 and biggest 20 (1:4)",
+ })
+ require.ElementsMatch(t, infos, []string{
+ "PromQL info: ignored histogram in sum aggregation (1:4)",
+ `PromQL info: input to histogram_quantile needed to be fixed for monotonicity (see https://prometheus.io/docs/prometheus/latest/querying/functions/#histogram_quantile) for metric name "series_1", from buckets 2.5 to 100, with a max diff of 10, over 3 samples from 2023-12-25T00:00:00Z to 2024-12-25T00:00:00Z (1:4)`,
+ })
+}
+
+type testCustomError struct {
+ PositionRange posrange.PositionRange
+ Err error
+ Query string
+ Min []float64
+ Max []float64
+ Count int
+}
+
+func (e *testCustomError) Error() string {
+ if e.Query == "" {
+ return e.Err.Error()
+ }
+ return fmt.Sprintf("%s, %d instances with smallest %g and biggest %g (%s)", e.Err, e.Count+1, e.Min[0], e.Max[0], e.PositionRange.StartPosInput(e.Query, 0))
+}
+
+func (e *testCustomError) Unwrap() error {
+ return e.Err
+}
+
+func (e *testCustomError) SetQuery(query string) {
+ e.Query = query
+}
+
+func (e *testCustomError) Merge(other error) error {
+ o := &testCustomError{}
+ ok := errors.As(other, &o)
+ if !ok {
+ return e
+ }
+ if e.Err.Error() != o.Err.Error() || len(e.Min) != len(o.Min) || len(e.Max) != len(o.Max) {
+ return e
+ }
+ for i, aMin := range e.Min {
+ if aMin < o.Min[i] {
+ o.Min[i] = aMin
+ }
+ }
+ for i, aMax := range e.Max {
+ if aMax > o.Max[i] {
+ o.Max[i] = aMax
+ }
+ }
+ o.Count += e.Count + 1
+ return o
+}
+
+func newTestCustomWarning(q float64, pos posrange.PositionRange, smallest, largest float64) error {
+ testCustomWarning := fmt.Errorf("%w: custom value set to", PromQLWarning)
+ return &testCustomError{
+ PositionRange: pos,
+ Err: fmt.Errorf("%w %g", testCustomWarning, q),
+ Min: []float64{smallest},
+ Max: []float64{largest},
+ }
+}
diff --git a/util/compression/buffers.go b/util/compression/buffers.go
index f510efc042..30f002970b 100644
--- a/util/compression/buffers.go
+++ b/util/compression/buffers.go
@@ -1,4 +1,4 @@
-// Copyright 2025 The Prometheus Authors
+// 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
diff --git a/util/compression/compression.go b/util/compression/compression.go
index a1e9b7e530..26cff6a22e 100644
--- a/util/compression/compression.go
+++ b/util/compression/compression.go
@@ -1,4 +1,4 @@
-// Copyright 2025 The Prometheus Authors
+// 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
diff --git a/util/compression/compression_test.go b/util/compression/compression_test.go
index 736bb934e3..4c52b8f42e 100644
--- a/util/compression/compression_test.go
+++ b/util/compression/compression_test.go
@@ -1,4 +1,4 @@
-// Copyright 2025 The Prometheus Authors
+// 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
diff --git a/util/convertnhcb/convertnhcb.go b/util/convertnhcb/convertnhcb.go
index 21ae62b3cb..64ec9054a3 100644
--- a/util/convertnhcb/convertnhcb.go
+++ b/util/convertnhcb/convertnhcb.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Prometheus Authors
+// 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
diff --git a/util/convertnhcb/convertnhcb_test.go b/util/convertnhcb/convertnhcb_test.go
index 7486ac18bb..710d47385a 100644
--- a/util/convertnhcb/convertnhcb_test.go
+++ b/util/convertnhcb/convertnhcb_test.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Prometheus Authors
+// 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
diff --git a/util/documentcli/documentcli.go b/util/documentcli/documentcli.go
index 14382663ee..ebd7d91a5d 100644
--- a/util/documentcli/documentcli.go
+++ b/util/documentcli/documentcli.go
@@ -1,4 +1,4 @@
-// Copyright 2023 The Prometheus Authors
+// 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
diff --git a/util/features/features.go b/util/features/features.go
new file mode 100644
index 0000000000..d52384dbd8
--- /dev/null
+++ b/util/features/features.go
@@ -0,0 +1,127 @@
+// 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 features
+
+import (
+ "maps"
+ "sync"
+)
+
+// Category constants define the standard feature flag categories used in Prometheus.
+const (
+ API = "api"
+ OTLPReceiver = "otlp_receiver"
+ Prometheus = "prometheus"
+ PromQL = "promql"
+ PromQLFunctions = "promql_functions"
+ PromQLOperators = "promql_operators"
+ Rules = "rules"
+ Scrape = "scrape"
+ ServiceDiscoveryProviders = "service_discovery_providers"
+ TemplatingFunctions = "templating_functions"
+ TSDB = "tsdb"
+ UI = "ui"
+)
+
+// Collector defines the interface for collecting and managing feature flags.
+// It provides methods to enable, disable, and retrieve feature states.
+type Collector interface {
+ // Enable marks a feature as enabled in the registry.
+ // The category and name should use snake_case naming convention.
+ Enable(category, name string)
+
+ // Disable marks a feature as disabled in the registry.
+ // The category and name should use snake_case naming convention.
+ Disable(category, name string)
+
+ // Set sets a feature to the specified enabled state.
+ // The category and name should use snake_case naming convention.
+ Set(category, name string, enabled bool)
+
+ // Get returns a copy of all registered features organized by category.
+ // Returns a map where the keys are category names and values are maps
+ // of feature names to their enabled status.
+ Get() map[string]map[string]bool
+}
+
+// registry is the private implementation of the Collector interface.
+// It stores feature information organized by category.
+type registry struct {
+ mu sync.RWMutex
+ features map[string]map[string]bool
+}
+
+// DefaultRegistry is the package-level registry used by Prometheus.
+var DefaultRegistry = NewRegistry()
+
+// NewRegistry creates a new feature registry.
+func NewRegistry() Collector {
+ return ®istry{
+ features: make(map[string]map[string]bool),
+ }
+}
+
+// Enable marks a feature as enabled in the registry.
+func (r *registry) Enable(category, name string) {
+ r.Set(category, name, true)
+}
+
+// Disable marks a feature as disabled in the registry.
+func (r *registry) Disable(category, name string) {
+ r.Set(category, name, false)
+}
+
+// Set sets a feature to the specified enabled state.
+func (r *registry) Set(category, name string, enabled bool) {
+ r.mu.Lock()
+ defer r.mu.Unlock()
+
+ if r.features[category] == nil {
+ r.features[category] = make(map[string]bool)
+ }
+ r.features[category][name] = enabled
+}
+
+// Get returns a copy of all registered features organized by category.
+func (r *registry) Get() map[string]map[string]bool {
+ r.mu.RLock()
+ defer r.mu.RUnlock()
+
+ result := make(map[string]map[string]bool, len(r.features))
+ for category, features := range r.features {
+ result[category] = make(map[string]bool, len(features))
+ maps.Copy(result[category], features)
+ }
+ return result
+}
+
+// Enable marks a feature as enabled in the default registry.
+func Enable(category, name string) {
+ DefaultRegistry.Enable(category, name)
+}
+
+// Disable marks a feature as disabled in the default registry.
+func Disable(category, name string) {
+ DefaultRegistry.Disable(category, name)
+}
+
+// Set sets a feature to the specified enabled state in the default registry.
+func Set(category, name string, enabled bool) {
+ DefaultRegistry.Set(category, name, enabled)
+}
+
+// Get returns all features from the default registry.
+func Get() map[string]map[string]bool {
+ return DefaultRegistry.Get()
+}
diff --git a/util/fmtutil/format.go b/util/fmtutil/format.go
index 7a78df849c..a4ac7d43ca 100644
--- a/util/fmtutil/format.go
+++ b/util/fmtutil/format.go
@@ -1,4 +1,4 @@
-// Copyright 2023 The Prometheus Authors
+// 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
@@ -18,6 +18,7 @@ import (
"fmt"
"io"
"maps"
+ "math"
"sort"
"time"
@@ -140,11 +141,23 @@ func makeTimeseries(wr *prompb.WriteRequest, labels map[string]string, m *dto.Me
// Add Histogram bucket timeseries
bucketLabels := make(map[string]string, len(labels)+1)
maps.Copy(bucketLabels, labels)
+ var hasInf bool
for _, b := range m.GetHistogram().Bucket {
+ if b.GetUpperBound() == math.Inf(1) {
+ hasInf = true
+ }
bucketLabels[model.MetricNameLabel] = metricName + bucketStr
bucketLabels[model.BucketLabel] = fmt.Sprint(b.GetUpperBound())
toTimeseries(wr, bucketLabels, timestamp, float64(b.GetCumulativeCount()))
}
+
+ // Add +Inf bucket if not present
+ if !hasInf {
+ bucketLabels[model.MetricNameLabel] = metricName + bucketStr
+ bucketLabels[model.BucketLabel] = "+Inf"
+ toTimeseries(wr, bucketLabels, timestamp, float64(m.GetHistogram().GetSampleCount()))
+ }
+
// Overwrite label model.MetricNameLabel for count and sum metrics
// Add Histogram sum timeseries
labels[model.MetricNameLabel] = metricName + sumStr
diff --git a/util/fmtutil/format_test.go b/util/fmtutil/format_test.go
index c592630fe8..73dbe39f45 100644
--- a/util/fmtutil/format_test.go
+++ b/util/fmtutil/format_test.go
@@ -1,4 +1,4 @@
-// Copyright 2023 The Prometheus Authors
+// 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
@@ -15,8 +15,11 @@ package fmtutil
import (
"bytes"
+ "math"
"testing"
+ "time"
+ dto "github.com/prometheus/client_model/go"
"github.com/stretchr/testify/require"
"github.com/prometheus/prometheus/prompb"
@@ -231,3 +234,50 @@ func TestMetricTextToWriteRequestErrorParsingMetricType(t *testing.T) {
_, err := MetricTextToWriteRequest(input, labels)
require.Equal(t, "text format parsing error in line 3: unknown metric type \"info\"", err.Error())
}
+
+func TestMakeTimeseries_HistogramInfBucket(t *testing.T) {
+ tests := map[string]*dto.Histogram{
+ "Histogram missing +Inf bucket": {
+ Bucket: []*dto.Bucket{
+ {CumulativeCount: p[uint64](5), UpperBound: p(1.0)},
+ {CumulativeCount: p[uint64](10), UpperBound: p(5.0)},
+ },
+ SampleCount: p[uint64](15),
+ },
+ "Histogram already has +Inf bucket": {
+ Bucket: []*dto.Bucket{
+ {CumulativeCount: p[uint64](5), UpperBound: p(1.0)},
+ {CumulativeCount: p[uint64](10), UpperBound: p(5.0)},
+ {CumulativeCount: p[uint64](15), UpperBound: p(math.Inf(1))},
+ },
+ SampleCount: p[uint64](15),
+ },
+ }
+
+ for name, histogram := range tests {
+ t.Run(name, func(t *testing.T) {
+ wr := &prompb.WriteRequest{}
+ labels := map[string]string{"__name__": "test_histogram"}
+ metric := &dto.Metric{
+ Histogram: histogram,
+ TimestampMs: p(time.Now().UnixMilli()),
+ }
+
+ require.NoError(t, makeTimeseries(wr, labels, metric))
+
+ var hasInf bool
+ for _, ts := range wr.Timeseries {
+ for _, lbl := range ts.Labels {
+ if lbl.Name == "le" && lbl.Value == "+Inf" {
+ hasInf = true
+ }
+ }
+ }
+ require.Truef(t, hasInf, "expected +Inf bucket in histogram")
+ })
+ }
+}
+
+func p[T any](v T) *T {
+ return &v
+}
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..025e4dfd7a
--- /dev/null
+++ b/util/fuzzing/corpus.go
@@ -0,0 +1,111 @@
+// 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/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) {
+ // 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..ec6d7c4e72
--- /dev/null
+++ b/util/fuzzing/fuzz_test.go
@@ -0,0 +1,149 @@
+// 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()
+
+var fuzzParser = parser.NewParser(parser.Options{})
+
+// 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 := fuzzParser.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) {
+ // 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)
+ }
+
+ p := parser.NewParser(parser.Options{
+ EnableExperimentalFunctions: true,
+ ExperimentalDurationExpr: true,
+ EnableExtendedRangeSelectors: true,
+ EnableBinopFillModifiers: true,
+ })
+ f.Fuzz(func(t *testing.T, in string) {
+ if len(in) > maxInputSize {
+ t.Skip()
+ }
+ _, err := p.ParseExpr(in)
+ // We don't care about errors, just that we don't panic.
+ _ = err
+ })
+}
diff --git a/util/gate/gate.go b/util/gate/gate.go
index 6cb9d583c6..a1066fd74f 100644
--- a/util/gate/gate.go
+++ b/util/gate/gate.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The Prometheus Authors
+// 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
diff --git a/util/httputil/compression.go b/util/httputil/compression.go
index d5bedb7fa9..ca9f3c17da 100644
--- a/util/httputil/compression.go
+++ b/util/httputil/compression.go
@@ -1,4 +1,4 @@
-// Copyright 2013 The Prometheus Authors
+// 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
@@ -56,6 +56,7 @@ func (c *compressedResponseWriter) Close() {
// Constructs a new compressedResponseWriter based on client request headers.
func newCompressedResponseWriter(writer http.ResponseWriter, req *http.Request) *compressedResponseWriter {
+ writer.Header().Add("Vary", acceptEncodingHeader)
raw := req.Header.Get(acceptEncodingHeader)
var (
encoding string
@@ -65,13 +66,17 @@ func newCompressedResponseWriter(writer http.ResponseWriter, req *http.Request)
encoding, raw, commaFound = strings.Cut(raw, ",")
switch strings.TrimSpace(encoding) {
case gzipEncoding:
- writer.Header().Set(contentEncodingHeader, gzipEncoding)
+ h := writer.Header()
+ h.Del("Content-Length") // avoid stale length after compression
+ h.Set(contentEncodingHeader, gzipEncoding)
return &compressedResponseWriter{
ResponseWriter: writer,
writer: gzip.NewWriter(writer),
}
case deflateEncoding:
- writer.Header().Set(contentEncodingHeader, deflateEncoding)
+ h := writer.Header()
+ h.Del("Content-Length")
+ h.Set(contentEncodingHeader, deflateEncoding)
return &compressedResponseWriter{
ResponseWriter: writer,
writer: zlib.NewWriter(writer),
diff --git a/util/httputil/compression_test.go b/util/httputil/compression_test.go
index 11df0a7c4c..6bdde914ce 100644
--- a/util/httputil/compression_test.go
+++ b/util/httputil/compression_test.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The Prometheus Authors
+// 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
diff --git a/util/httputil/context.go b/util/httputil/context.go
index 9b16428892..7aaeebdb3e 100644
--- a/util/httputil/context.go
+++ b/util/httputil/context.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
diff --git a/util/httputil/cors.go b/util/httputil/cors.go
index 2d4cc91ccb..e319762b5f 100644
--- a/util/httputil/cors.go
+++ b/util/httputil/cors.go
@@ -1,4 +1,4 @@
-// Copyright 2013 The Prometheus Authors
+// 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
diff --git a/util/httputil/cors_test.go b/util/httputil/cors_test.go
index 30567947a9..d637932267 100644
--- a/util/httputil/cors_test.go
+++ b/util/httputil/cors_test.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The Prometheus Authors
+// 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
diff --git a/util/jsonutil/marshal.go b/util/jsonutil/marshal.go
index d715eabe68..61ce4234eb 100644
--- a/util/jsonutil/marshal.go
+++ b/util/jsonutil/marshal.go
@@ -1,4 +1,4 @@
-// Copyright 2022 The Prometheus Authors
+// 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
diff --git a/util/junitxml/junitxml.go b/util/junitxml/junitxml.go
index 14e4b6dbae..8249290830 100644
--- a/util/junitxml/junitxml.go
+++ b/util/junitxml/junitxml.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Prometheus Authors
+// 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
diff --git a/util/junitxml/junitxml_test.go b/util/junitxml/junitxml_test.go
index ad4d0293d0..92a32f2ddf 100644
--- a/util/junitxml/junitxml_test.go
+++ b/util/junitxml/junitxml_test.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Prometheus Authors
+// 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
diff --git a/util/kahansum/kahansum.go b/util/kahansum/kahansum.go
new file mode 100644
index 0000000000..d55defcb29
--- /dev/null
+++ b/util/kahansum/kahansum.go
@@ -0,0 +1,39 @@
+// Copyright 2024 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 kahansum
+
+import "math"
+
+// Inc performs addition of two floating-point numbers using the Kahan summation algorithm.
+// We get incorrect results if this function is inlined; see https://github.com/prometheus/prometheus/issues/16714.
+//
+//go:noinline
+func Inc(inc, sum, c float64) (newSum, newC float64) {
+ t := sum + inc
+ switch {
+ case math.IsInf(t, 0):
+ c = 0
+
+ // Using Neumaier improvement, swap if next term larger than sum.
+ case math.Abs(sum) >= math.Abs(inc):
+ c += (sum - t) + inc
+ default:
+ c += (inc - t) + sum
+ }
+ return t, c
+}
+
+func Dec(dec, sum, c float64) (newSum, newC float64) {
+ return Inc(-dec, sum, c)
+}
diff --git a/util/logging/dedupe.go b/util/logging/dedupe.go
index 8137f4f22b..244cd6495c 100644
--- a/util/logging/dedupe.go
+++ b/util/logging/dedupe.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The Prometheus Authors
+// 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
diff --git a/util/logging/dedupe_test.go b/util/logging/dedupe_test.go
index 918c5d60bd..b584f12572 100644
--- a/util/logging/dedupe_test.go
+++ b/util/logging/dedupe_test.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The Prometheus Authors
+// 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
diff --git a/util/logging/file.go b/util/logging/file.go
index 5e379442a2..bce9be9ae6 100644
--- a/util/logging/file.go
+++ b/util/logging/file.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
diff --git a/util/logging/file_test.go b/util/logging/file_test.go
index bd34bc2a3a..58a55697d9 100644
--- a/util/logging/file_test.go
+++ b/util/logging/file_test.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
diff --git a/util/namevalidationutil/namevalidationutil.go b/util/namevalidationutil/namevalidationutil.go
index 2e656b6a19..14796b48f4 100644
--- a/util/namevalidationutil/namevalidationutil.go
+++ b/util/namevalidationutil/namevalidationutil.go
@@ -1,4 +1,4 @@
-// Copyright 2025 The Prometheus Authors
+// 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
diff --git a/util/namevalidationutil/namevalidationutil_test.go b/util/namevalidationutil/namevalidationutil_test.go
index 660b6100b0..692bc2692b 100644
--- a/util/namevalidationutil/namevalidationutil_test.go
+++ b/util/namevalidationutil/namevalidationutil_test.go
@@ -1,4 +1,4 @@
-// Copyright 2025 The Prometheus Authors
+// 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
diff --git a/util/netconnlimit/netconnlimit.go b/util/netconnlimit/netconnlimit.go
index 3bdd805b83..5f54d0616a 100644
--- a/util/netconnlimit/netconnlimit.go
+++ b/util/netconnlimit/netconnlimit.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Prometheus Authors
+// Copyright The Prometheus Authors
// Based on golang.org/x/net/netutil:
// Copyright 2013 The Go Authors
// Licensed under the Apache License, Version 2.0 (the "License");
diff --git a/util/netconnlimit/netconnlimit_test.go b/util/netconnlimit/netconnlimit_test.go
index e4d4904209..c33c7b342f 100644
--- a/util/netconnlimit/netconnlimit_test.go
+++ b/util/netconnlimit/netconnlimit_test.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Prometheus Authors
+// 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
diff --git a/util/notifications/notifications.go b/util/notifications/notifications.go
index 4888a0b664..0e3882ce36 100644
--- a/util/notifications/notifications.go
+++ b/util/notifications/notifications.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Prometheus Authors
+// 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
diff --git a/util/notifications/notifications_test.go b/util/notifications/notifications_test.go
index 3d9ba6bb12..84db90c6e3 100644
--- a/util/notifications/notifications_test.go
+++ b/util/notifications/notifications_test.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Prometheus Authors
+// 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
diff --git a/util/osutil/hostname.go b/util/osutil/hostname.go
index c44cb391b6..f0444114f7 100644
--- a/util/osutil/hostname.go
+++ b/util/osutil/hostname.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
diff --git a/util/pool/pool.go b/util/pool/pool.go
index 7d5a8e3abf..a7f1bbb54e 100644
--- a/util/pool/pool.go
+++ b/util/pool/pool.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
diff --git a/util/pool/pool_test.go b/util/pool/pool_test.go
index e1ac13fb90..a14da6be8b 100644
--- a/util/pool/pool_test.go
+++ b/util/pool/pool_test.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// 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
diff --git a/util/runtime/limits_default.go b/util/runtime/limits_default.go
index 156747d450..51a78423d3 100644
--- a/util/runtime/limits_default.go
+++ b/util/runtime/limits_default.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
diff --git a/util/runtime/limits_windows.go b/util/runtime/limits_windows.go
index ce82d31e6d..1cb7ea33a7 100644
--- a/util/runtime/limits_windows.go
+++ b/util/runtime/limits_windows.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
diff --git a/util/runtime/statfs.go b/util/runtime/statfs.go
index 66bedb5ea1..98dd822e4a 100644
--- a/util/runtime/statfs.go
+++ b/util/runtime/statfs.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
diff --git a/util/runtime/statfs_default.go b/util/runtime/statfs_default.go
index 78cfb1fe41..0cf5c2e616 100644
--- a/util/runtime/statfs_default.go
+++ b/util/runtime/statfs_default.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
diff --git a/util/runtime/statfs_linux_386.go b/util/runtime/statfs_linux_386.go
index a003b2effe..33dbc4c3e9 100644
--- a/util/runtime/statfs_linux_386.go
+++ b/util/runtime/statfs_linux_386.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
diff --git a/util/runtime/statfs_uint32.go b/util/runtime/statfs_uint32.go
index fbf994ea63..2fb4d70849 100644
--- a/util/runtime/statfs_uint32.go
+++ b/util/runtime/statfs_uint32.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
diff --git a/util/runtime/uname_default.go b/util/runtime/uname_default.go
index 0052dbab47..1bdc2e6696 100644
--- a/util/runtime/uname_default.go
+++ b/util/runtime/uname_default.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
diff --git a/util/runtime/uname_linux.go b/util/runtime/uname_linux.go
index ce3bc42a25..f2798cda4b 100644
--- a/util/runtime/uname_linux.go
+++ b/util/runtime/uname_linux.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
diff --git a/util/runtime/vmlimits_default.go b/util/runtime/vmlimits_default.go
index aef4341061..0e3bc0ead5 100644
--- a/util/runtime/vmlimits_default.go
+++ b/util/runtime/vmlimits_default.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Prometheus Authors
+// 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
diff --git a/util/runtime/vmlimits_openbsd.go b/util/runtime/vmlimits_openbsd.go
index b40f065883..ce9aa181e6 100644
--- a/util/runtime/vmlimits_openbsd.go
+++ b/util/runtime/vmlimits_openbsd.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Prometheus Authors
+// 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
diff --git a/util/runutil/runutil.go b/util/runutil/runutil.go
index 5a77c332ba..14752ed796 100644
--- a/util/runutil/runutil.go
+++ b/util/runutil/runutil.go
@@ -1,4 +1,4 @@
-// Copyright 2013 The Prometheus Authors
+// 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
diff --git a/util/stats/query_stats.go b/util/stats/query_stats.go
index d8ec186f4c..9801d658a7 100644
--- a/util/stats/query_stats.go
+++ b/util/stats/query_stats.go
@@ -1,4 +1,4 @@
-// Copyright 2013 The Prometheus Authors
+// 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
diff --git a/util/stats/stats_test.go b/util/stats/stats_test.go
index 28753b95fc..245f7cbc16 100644
--- a/util/stats/stats_test.go
+++ b/util/stats/stats_test.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
diff --git a/util/stats/timer.go b/util/stats/timer.go
index eca0fcccb0..1b9e430a09 100644
--- a/util/stats/timer.go
+++ b/util/stats/timer.go
@@ -1,4 +1,4 @@
-// Copyright 2013 The Prometheus Authors
+// 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
diff --git a/util/strutil/quote.go b/util/strutil/quote.go
index 0a78421fd4..d7e65395f4 100644
--- a/util/strutil/quote.go
+++ b/util/strutil/quote.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
diff --git a/util/strutil/quote_test.go b/util/strutil/quote_test.go
index de33230551..c077a5ed49 100644
--- a/util/strutil/quote_test.go
+++ b/util/strutil/quote_test.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
diff --git a/util/strutil/strconv.go b/util/strutil/strconv.go
index 88d2a3b610..77f1acc94d 100644
--- a/util/strutil/strconv.go
+++ b/util/strutil/strconv.go
@@ -1,4 +1,4 @@
-// Copyright 2013 The Prometheus Authors
+// 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
diff --git a/util/strutil/strconv_test.go b/util/strutil/strconv_test.go
index f09e7ffb3f..362fa79a6a 100644
--- a/util/strutil/strconv_test.go
+++ b/util/strutil/strconv_test.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The Prometheus Authors
+// 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
@@ -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
new file mode 100644
index 0000000000..6b1ba31f7d
--- /dev/null
+++ b/util/teststorage/appender.go
@@ -0,0 +1,616 @@
+// 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 teststorage
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "math"
+ "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.
+type Sample struct {
+ MF string
+ L labels.Labels
+ M metadata.Metadata
+ ST, T int64
+ V float64
+ H *histogram.Histogram
+ FH *histogram.FloatHistogram
+ ES []exemplar.Exemplar
+}
+
+func (s Sample) String() string {
+ // Attempting to format similar to ~ OpenMetrics 2.0 for readability.
+ b := strings.Builder{}
+ if s.M.Help != "" {
+ b.WriteString("HELP ")
+ b.WriteString(s.M.Help)
+ b.WriteString("\n")
+ }
+ if s.M.Type != model.MetricTypeUnknown && s.M.Type != "" {
+ b.WriteString("type@")
+ b.WriteString(string(s.M.Type))
+ b.WriteString(" ")
+ }
+ if s.M.Unit != "" {
+ b.WriteString("unit@")
+ b.WriteString(s.M.Unit)
+ b.WriteString(" ")
+ }
+ // 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()
+ }
+ fh := ""
+ if s.FH != nil {
+ fh = " " + s.FH.String()
+ }
+ fmt.Fprintf(&b, "%s %v%v%v st@%v t@%v", s.L.String(), s.V, h, fh, s.ST, s.T)
+ if len(s.ES) > 0 {
+ fmt.Fprintf(&b, " %v", s.ES)
+ }
+ b.WriteString("\n")
+ return b.String()
+}
+
+func (s Sample) Equals(other Sample) bool {
+ return strings.Compare(s.MF, other.MF) == 0 &&
+ labels.Equal(s.L, other.L) &&
+ s.M.Equals(other.M) &&
+ s.ST == other.ST &&
+ s.T == other.T &&
+ math.Float64bits(s.V) == math.Float64bits(other.V) && // Compare Float64bits so NaN values which are exactly the same will compare equal.
+ s.H.Equals(other.H) &&
+ s.FH.Equals(other.FH) &&
+ 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.
+type Appendable struct {
+ appendErrFn func(ls labels.Labels) error // If non-nil, inject appender error on every Append, AppendHistogram and ST zero calls.
+ appendExemplarsError error // If non-nil, inject exemplar error.
+ commitErr error // If non-nil, inject commit error.
+ skipRecording bool // If true, Appendable won't record samples, useful for benchmarks.
+
+ mtx sync.Mutex
+ openAppenders atomic.Int32 // Guard against multi-appender use.
+
+ // Recorded results.
+ pendingSamples []Sample
+ resultSamples []Sample
+ rolledbackSamples []Sample
+
+ // Optional chain (Appender will collect samples, then run next).
+ next compatAppendable
+}
+
+// NewAppendable returns mock Appendable.
+func NewAppendable() *Appendable {
+ return &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
+}
+
+// WithErrs allows injecting errors to the appender.
+func (a *Appendable) WithErrs(appendErrFn func(ls labels.Labels) error, appendExemplarsError, commitErr error) *Appendable {
+ a.appendErrFn = appendErrFn
+ a.appendExemplarsError = appendExemplarsError
+ a.commitErr = commitErr
+ return a
+}
+
+// SkipRecording enables or disables recording appended samples.
+// If skipped, Appendable allocs less, but Result*() methods will give always empty results. This is useful for benchmarking.
+func (a *Appendable) SkipRecording(skipRecording bool) *Appendable {
+ a.skipRecording = skipRecording
+ return a
+}
+
+// PendingSamples returns pending samples (samples appended without commit).
+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)
+ return ret
+}
+
+// ResultSamples returns committed samples.
+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)
+ return ret
+}
+
+// RolledbackSamples returns rolled back samples.
+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)
+ return ret
+}
+
+func (a *Appendable) ResultReset() {
+ a.mtx.Lock()
+ defer a.mtx.Unlock()
+
+ a.pendingSamples = a.pendingSamples[:0]
+ a.resultSamples = a.resultSamples[:0]
+ a.rolledbackSamples = a.rolledbackSamples[:0]
+}
+
+// ResultMetadata returns resultSamples with samples only containing L and M.
+// This is for compatibility with tests that only focus on metadata.
+//
+// TODO: Rewrite tests to test metadata on resultSamples instead.
+func (a *Appendable) ResultMetadata() []Sample {
+ a.mtx.Lock()
+ defer a.mtx.Unlock()
+
+ var ret []Sample
+ for _, s := range a.resultSamples {
+ if s.M.IsEmpty() {
+ continue
+ }
+ ret = append(ret, Sample{L: s.L, M: s.M})
+ }
+ return ret
+}
+
+func (a *Appendable) String() string {
+ var sb strings.Builder
+ sb.WriteString("committed:\n")
+ for _, s := range a.resultSamples {
+ sb.WriteString("\n")
+ sb.WriteString(s.String())
+ }
+ sb.WriteString("pending:\n")
+ for _, s := range a.pendingSamples {
+ sb.WriteString("\n")
+ sb.WriteString(s.String())
+ }
+ sb.WriteString("rolledback:\n")
+ for _, s := range a.rolledbackSamples {
+ sb.WriteString("\n")
+ sb.WriteString(s.String())
+ }
+ return sb.String()
+}
+
+var errClosedAppender = errors.New("appender was already committed/rolledback")
+
+type baseAppender struct {
+ err error
+
+ nextTr storage.AppenderTransaction
+ a *Appendable
+}
+
+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()
+ if !a.a.skipRecording {
+ 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()
+ if !a.a.skipRecording {
+ 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{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 {
+ app := a.next.Appender(ctx)
+ ret.next, ret.nextTr = app, app
+ }
+ return ret
+}
+
+func (*appender) SetOptions(*storage.AppendOptions) {}
+
+func (a *appender) Append(ref storage.SeriesRef, ls labels.Labels, t int64, v float64) (storage.SeriesRef, 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
+ }
+ }
+
+ if !a.a.skipRecording {
+ a.a.mtx.Lock()
+ a.a.pendingSamples = append(a.a.pendingSamples, Sample{L: ls, T: t, V: v})
+ a.a.mtx.Unlock()
+ }
+
+ if a.next != nil {
+ return a.next.Append(ref, ls, t, v)
+ }
+
+ return computeOrCheckRef(ref, ls)
+}
+
+func computeOrCheckRef(ref storage.SeriesRef, ls labels.Labels) (storage.SeriesRef, error) {
+ h := ls.Hash()
+ if ref == 0 {
+ // Use labels hash as a stand-in for unique series reference, to avoid having to track all series.
+ return storage.SeriesRef(h), nil
+ }
+
+ if storage.SeriesRef(h) != ref {
+ // 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
+}
+
+func (a *appender) AppendHistogram(ref storage.SeriesRef, ls labels.Labels, t int64, h *histogram.Histogram, fh *histogram.FloatHistogram) (storage.SeriesRef, 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
+ }
+ }
+
+ if !a.a.skipRecording {
+ a.a.mtx.Lock()
+ a.a.pendingSamples = append(a.a.pendingSamples, Sample{L: ls, T: t, H: h, FH: fh})
+ a.a.mtx.Unlock()
+ }
+
+ if a.next != nil {
+ return a.next.AppendHistogram(ref, ls, t, h, fh)
+ }
+
+ return computeOrCheckRef(ref, ls)
+}
+
+func (a *appender) AppendExemplar(ref storage.SeriesRef, l labels.Labels, e exemplar.Exemplar) (storage.SeriesRef, error) {
+ if err := a.checkErr(); err != nil {
+ return 0, err
+ }
+ if a.a.appendExemplarsError != nil {
+ return 0, a.a.appendExemplarsError
+ }
+
+ if !a.a.skipRecording {
+ var appended bool
+
+ a.a.mtx.Lock()
+ // NOTE(bwplotka): Eventually exemplar 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 exemplars to the last matching sample.
+ 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 !appended {
+ return 0, fmt.Errorf("teststorage.appender: exemplar appender without series; ref %v; l %v; exemplar: %v", ref, l, e)
+ }
+ }
+
+ if a.next != nil {
+ return a.next.AppendExemplar(ref, l, e)
+ }
+ return computeOrCheckRef(ref, l)
+}
+
+func (a *appender) AppendSTZeroSample(ref storage.SeriesRef, l labels.Labels, _, st int64) (storage.SeriesRef, error) {
+ return a.Append(ref, l, st, 0.0) // This will change soon with AppenderV2, but we already report ST as 0 samples.
+}
+
+func (a *appender) AppendHistogramSTZeroSample(ref storage.SeriesRef, l labels.Labels, _, st int64, h *histogram.Histogram, _ *histogram.FloatHistogram) (storage.SeriesRef, error) {
+ if h != nil {
+ return a.AppendHistogram(ref, l, st, &histogram.Histogram{}, nil)
+ }
+ return a.AppendHistogram(ref, l, st, nil, &histogram.FloatHistogram{}) // This will change soon with AppenderV2, but we already report ST as 0 histograms.
+}
+
+func (a *appender) UpdateMetadata(ref storage.SeriesRef, l labels.Labels, m metadata.Metadata) (storage.SeriesRef, error) {
+ if err := a.checkErr(); err != nil {
+ return 0, err
+ }
+
+ if !a.a.skipRecording {
+ 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 labels.Equal(l, a.a.pendingSamples[i].L) {
+ a.a.pendingSamples[i].M = m
+ updated = true
+ break
+ }
+ }
+ a.a.mtx.Unlock()
+ if !updated {
+ return 0, fmt.Errorf("teststorage.appender: metadata update without series; ref %v; l %v; m: %v", ref, l, m)
+ }
+ }
+
+ if a.next != nil {
+ return a.next.UpdateMetadata(ref, l, m)
+ }
+ return computeOrCheckRef(ref, l)
+}
+
+type appenderV2 struct {
+ baseAppender
+
+ next storage.AppenderV2
+}
+
+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 partialErr error
+ if !a.a.skipRecording {
+ var es []exemplar.Exemplar
+
+ 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)
+ }
+ }
+
+ a.a.mtx.Lock()
+ 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 {
+ 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 ref, partialErr
+}
diff --git a/util/teststorage/appender_test.go b/util/teststorage/appender_test.go
new file mode 100644
index 0000000000..41260ba43f
--- /dev/null
+++ b/util/teststorage/appender_test.go
@@ -0,0 +1,413 @@
+// 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 teststorage
+
+import (
+ "errors"
+ "math"
+ "testing"
+
+ "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"
+)
+
+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(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")}}},
+ }
+ RequireEqual(t, a, a)
+
+ b1 := []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_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)
+
+ b2 := []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: 123.123},
+ {ES: []exemplar.Exemplar{{Labels: labels.FromStrings(model.MetricNameLabel, "yolo2")}}}, // exemplar is different.
+ }
+ RequireNotEqual(t, a, b2)
+
+ b3 := []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: 123.123, T: 123}, // Timestamp is different.
+ {ES: []exemplar.Exemplar{{Labels: labels.FromStrings(model.MetricNameLabel, "yolo")}}},
+ }
+ RequireNotEqual(t, a, b3)
+
+ b4 := []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: 456.456}, // Value is different.
+ {ES: []exemplar.Exemplar{{Labels: labels.FromStrings(model.MetricNameLabel, "yolo")}}},
+ }
+ RequireNotEqual(t, a, b4)
+
+ b5 := []Sample{
+ {},
+ {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)
+
+ // 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")}}},
+ }
+ 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) {
+ a := NewAppendable()
+
+ // Non-concurrent multiple use if fine.
+ app := a.Appender(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.Appender(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.Appender(t.Context())
+ require.Error(t, app.Commit())
+
+ a.WithErrs(nil, nil, nil)
+ app = a.Appender(t.Context())
+ require.NoError(t, app.Commit())
+ require.Equal(t, int32(0), a.openAppenders.Load())
+
+ // Concurrent use should return appender that errors.
+ _ = a.Appender(t.Context())
+ app = a.Appender(t.Context())
+ _, err := app.Append(0, labels.EmptyLabels(), 0, 0)
+ require.Error(t, err)
+ _, err = app.AppendHistogram(0, labels.EmptyLabels(), 0, nil, nil)
+ require.Error(t, err)
+ 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 e0a6f39be2..65c2f87e21 100644
--- a/util/teststorage/storage.go
+++ b/util/teststorage/storage.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
@@ -16,66 +16,66 @@ package teststorage
import (
"fmt"
"os"
+ "testing"
"time"
- "github.com/prometheus/client_golang/prometheus"
"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"
- "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...)
+//
+// Caller does not need to close the TestStorage after use, it's deferred via t.Cleanup.
+func New(t testing.TB, o ...Option) *TestStorage {
+ s, err := NewWithError(o...)
require.NoError(t, err)
- return stor
+
+ t.Cleanup(func() {
+ _ = s.Close() // Ignore errors, as it could be a double close.
+ })
+ 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)
- }
-
+//
+// It's a caller responsibility to close the TestStorage after use.
+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
+ // Enable exemplars storage by default.
+ opts.EnableExemplarStorage = true
+ opts.MaxExemplars = 1e5
+
+ 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())
if err != nil {
return nil, fmt.Errorf("opening test storage: %w", err)
}
- reg := prometheus.NewRegistry()
- eMetrics := tsdb.NewExemplarMetrics(reg)
-
- es, err := tsdb.NewCircularExemplarStorage(10, eMetrics)
- if err != nil {
- return nil, fmt.Errorf("opening test exemplar storage: %w", err)
- }
- return &TestStorage{DB: db, exemplarStorage: es, dir: dir}, nil
+ return &TestStorage{DB: db, dir: dir}, nil
}
type TestStorage struct {
*tsdb.DB
- exemplarStorage tsdb.ExemplarStorage
- dir string
+ dir string
}
func (s TestStorage) Close() error {
@@ -84,15 +84,3 @@ func (s TestStorage) Close() error {
}
return os.RemoveAll(s.dir)
}
-
-func (s TestStorage) ExemplarAppender() storage.ExemplarAppender {
- return s
-}
-
-func (s TestStorage) ExemplarQueryable() storage.ExemplarQueryable {
- return s.exemplarStorage
-}
-
-func (s TestStorage) AppendExemplar(ref storage.SeriesRef, l labels.Labels, e exemplar.Exemplar) (storage.SeriesRef, error) {
- return ref, s.exemplarStorage.AddExemplar(l, e)
-}
diff --git a/util/testutil/cmp.go b/util/testutil/cmp.go
index 3ea1f40168..9be01a5b4b 100644
--- a/util/testutil/cmp.go
+++ b/util/testutil/cmp.go
@@ -1,4 +1,4 @@
-// Copyright 2023 The Prometheus Authors
+// 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
diff --git a/util/testutil/context.go b/util/testutil/context.go
index 3d2a09d637..15f50fbff5 100644
--- a/util/testutil/context.go
+++ b/util/testutil/context.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The Prometheus Authors
+// 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
diff --git a/util/testutil/directory.go b/util/testutil/directory.go
index 176acb5dc1..b65a3f4fa0 100644
--- a/util/testutil/directory.go
+++ b/util/testutil/directory.go
@@ -1,4 +1,4 @@
-// Copyright 2013 The Prometheus Authors
+// 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
@@ -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/util/testutil/port.go b/util/testutil/port.go
index 91c1291749..3a9be3f1a3 100644
--- a/util/testutil/port.go
+++ b/util/testutil/port.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
diff --git a/util/testutil/roundtrip.go b/util/testutil/roundtrip.go
index 364e0c2642..0bd003ca68 100644
--- a/util/testutil/roundtrip.go
+++ b/util/testutil/roundtrip.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// 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
diff --git a/util/testutil/synctest/disabled.go b/util/testutil/synctest/disabled.go
index e87454afcf..595b93c650 100644
--- a/util/testutil/synctest/disabled.go
+++ b/util/testutil/synctest/disabled.go
@@ -1,4 +1,4 @@
-// Copyright 2025 The Prometheus Authors
+// 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
diff --git a/util/testutil/synctest/enabled.go b/util/testutil/synctest/enabled.go
index 61aa85dcf7..d219903809 100644
--- a/util/testutil/synctest/enabled.go
+++ b/util/testutil/synctest/enabled.go
@@ -1,4 +1,4 @@
-// Copyright 2025 The Prometheus Authors
+// 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
diff --git a/util/testutil/synctest/synctest.go b/util/testutil/synctest/synctest.go
index 6780798a9b..41750f9892 100644
--- a/util/testutil/synctest/synctest.go
+++ b/util/testutil/synctest/synctest.go
@@ -1,4 +1,4 @@
-// Copyright 2025 The Prometheus Authors
+// 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
diff --git a/util/treecache/treecache.go b/util/treecache/treecache.go
index 86fd207074..deb950b55a 100644
--- a/util/treecache/treecache.go
+++ b/util/treecache/treecache.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The Prometheus Authors
+// 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
@@ -265,8 +265,7 @@ func (tc *ZookeeperTreeCache) recursiveNodeUpdate(path string, node *zookeeperTr
}
}
- tc.wg.Add(1)
- go func() {
+ tc.wg.Go(func() {
numWatchers.Inc()
// Pass up zookeeper events, until the node is deleted.
select {
@@ -277,8 +276,7 @@ func (tc *ZookeeperTreeCache) recursiveNodeUpdate(path string, node *zookeeperTr
case <-node.done:
}
numWatchers.Dec()
- tc.wg.Done()
- }()
+ })
return nil
}
diff --git a/util/zeropool/pool.go b/util/zeropool/pool.go
index 946ce02091..6eab9f3365 100644
--- a/util/zeropool/pool.go
+++ b/util/zeropool/pool.go
@@ -1,4 +1,4 @@
-// Copyright 2023 The Prometheus Authors
+// 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
diff --git a/util/zeropool/pool_test.go b/util/zeropool/pool_test.go
index 24598cbfa3..f93e75d539 100644
--- a/util/zeropool/pool_test.go
+++ b/util/zeropool/pool_test.go
@@ -1,4 +1,4 @@
-// Copyright 2023 The Prometheus Authors
+// 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
diff --git a/web/api/testhelpers/api.go b/web/api/testhelpers/api.go
new file mode 100644
index 0000000000..07d7003b5c
--- /dev/null
+++ b/web/api/testhelpers/api.go
@@ -0,0 +1,244 @@
+// 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 testhelpers provides utilities for testing the Prometheus HTTP API.
+// This file contains helper functions for creating test API instances and managing test lifecycles.
+package testhelpers
+
+import (
+ "context"
+ "log/slog"
+ "net/http"
+ "net/url"
+ "testing"
+ "time"
+
+ "github.com/prometheus/client_golang/prometheus"
+ "github.com/prometheus/common/promslog"
+
+ "github.com/prometheus/prometheus/config"
+ "github.com/prometheus/prometheus/model/labels"
+ "github.com/prometheus/prometheus/promql"
+ "github.com/prometheus/prometheus/promql/promqltest"
+ "github.com/prometheus/prometheus/rules"
+ "github.com/prometheus/prometheus/scrape"
+ "github.com/prometheus/prometheus/storage"
+ "github.com/prometheus/prometheus/tsdb"
+ "github.com/prometheus/prometheus/util/notifications"
+)
+
+// RulesRetriever provides a list of active rules and alerts.
+type RulesRetriever interface {
+ RuleGroups() []*rules.Group
+ AlertingRules() []*rules.AlertingRule
+}
+
+// TargetRetriever provides the list of active/dropped targets to scrape or not.
+type TargetRetriever interface {
+ TargetsActive() map[string][]*scrape.Target
+ TargetsDropped() map[string][]*scrape.Target
+ TargetsDroppedCounts() map[string]int
+ ScrapePoolConfig(string) (*config.ScrapeConfig, error)
+}
+
+// ScrapePoolsRetriever provide the list of all scrape pools.
+type ScrapePoolsRetriever interface {
+ ScrapePools() []string
+}
+
+// AlertmanagerRetriever provides a list of all/dropped AlertManager URLs.
+type AlertmanagerRetriever interface {
+ Alertmanagers() []*url.URL
+ DroppedAlertmanagers() []*url.URL
+}
+
+// TSDBAdminStats provides TSDB admin statistics.
+type TSDBAdminStats interface {
+ CleanTombstones() error
+ Delete(ctx context.Context, mint, maxt int64, ms ...*labels.Matcher) error
+ Snapshot(dir string, withHead bool) error
+ Stats(statsByLabelName string, limit int) (*tsdb.Stats, error)
+ WALReplayStatus() (tsdb.WALReplayStatus, error)
+ BlockMetas() ([]tsdb.BlockMeta, error)
+}
+
+// APIConfig holds configuration for creating a test API instance.
+type APIConfig struct {
+ // Core dependencies.
+ QueryEngine *LazyLoader[promql.QueryEngine]
+ Queryable *LazyLoader[storage.SampleAndChunkQueryable]
+ ExemplarQueryable *LazyLoader[storage.ExemplarQueryable]
+
+ // Retrievers.
+ RulesRetriever *LazyLoader[RulesRetriever]
+ TargetRetriever *LazyLoader[TargetRetriever]
+ ScrapePoolsRetriever *LazyLoader[ScrapePoolsRetriever]
+ AlertmanagerRetriever *LazyLoader[AlertmanagerRetriever]
+
+ // Admin.
+ TSDBAdmin *LazyLoader[TSDBAdminStats]
+ DBDir string
+
+ // Optional overrides.
+ Config func() config.Config
+ FlagsMap map[string]string
+ Now func() time.Time
+}
+
+// APIWrapper wraps the API and provides a handler for testing.
+type APIWrapper struct {
+ Handler http.Handler
+}
+
+// PrometheusVersion contains build information about Prometheus.
+type PrometheusVersion struct {
+ Version string `json:"version"`
+ Revision string `json:"revision"`
+ Branch string `json:"branch"`
+ BuildUser string `json:"buildUser"`
+ BuildDate string `json:"buildDate"`
+ GoVersion string `json:"goVersion"`
+}
+
+// RuntimeInfo contains runtime information about Prometheus.
+type RuntimeInfo struct {
+ StartTime time.Time `json:"startTime"`
+ CWD string `json:"CWD"`
+ Hostname string `json:"hostname"`
+ ServerTime time.Time `json:"serverTime"`
+ ReloadConfigSuccess bool `json:"reloadConfigSuccess"`
+ LastConfigTime time.Time `json:"lastConfigTime"`
+ CorruptionCount int64 `json:"corruptionCount"`
+ GoroutineCount int `json:"goroutineCount"`
+ GOMAXPROCS int `json:"GOMAXPROCS"`
+ GOMEMLIMIT int64 `json:"GOMEMLIMIT"`
+ GOGC string `json:"GOGC"`
+ GODEBUG string `json:"GODEBUG"`
+ StorageRetention string `json:"storageRetention"`
+}
+
+// NewAPIParams holds all the parameters needed to create a v1.API instance.
+type NewAPIParams struct {
+ QueryEngine promql.QueryEngine
+ Queryable storage.SampleAndChunkQueryable
+ ExemplarQueryable storage.ExemplarQueryable
+ ScrapePoolsRetriever func(context.Context) ScrapePoolsRetriever
+ TargetRetriever func(context.Context) TargetRetriever
+ AlertmanagerRetriever func(context.Context) AlertmanagerRetriever
+ ConfigFunc func() config.Config
+ FlagsMap map[string]string
+ ReadyFunc func(http.HandlerFunc) http.HandlerFunc
+ TSDBAdmin TSDBAdminStats
+ DBDir string
+ Logger *slog.Logger
+ RulesRetriever func(context.Context) RulesRetriever
+ RuntimeInfoFunc func() (RuntimeInfo, error)
+ BuildInfo *PrometheusVersion
+ NotificationsGetter func() []notifications.Notification
+ NotificationsSub func() (<-chan notifications.Notification, func(), bool)
+ Gatherer prometheus.Gatherer
+ Registerer prometheus.Registerer
+}
+
+// PrepareAPI creates a NewAPIParams with sensible defaults for testing.
+func PrepareAPI(t *testing.T, cfg APIConfig) NewAPIParams {
+ t.Helper()
+
+ // Create defaults for unset lazy loaders.
+ if cfg.QueryEngine == nil {
+ cfg.QueryEngine = NewLazyLoader(func() promql.QueryEngine {
+ return promqltest.NewTestEngineWithOpts(t, promql.EngineOpts{
+ Logger: nil,
+ Reg: nil,
+ MaxSamples: 10000,
+ Timeout: 100 * time.Second,
+ NoStepSubqueryIntervalFn: func(int64) int64 { return 60 * 1000 },
+ EnableAtModifier: true,
+ EnableNegativeOffset: true,
+ EnablePerStepStats: true,
+ })
+ })
+ }
+
+ if cfg.Queryable == nil {
+ cfg.Queryable = NewLazyLoader(NewEmptyQueryable)
+ }
+
+ if cfg.ExemplarQueryable == nil {
+ cfg.ExemplarQueryable = NewLazyLoader(NewEmptyExemplarQueryable)
+ }
+
+ if cfg.RulesRetriever == nil {
+ cfg.RulesRetriever = NewLazyLoader(func() RulesRetriever {
+ return NewEmptyRulesRetriever()
+ })
+ }
+
+ if cfg.TargetRetriever == nil {
+ cfg.TargetRetriever = NewLazyLoader(func() TargetRetriever {
+ return NewEmptyTargetRetriever()
+ })
+ }
+
+ if cfg.ScrapePoolsRetriever == nil {
+ cfg.ScrapePoolsRetriever = NewLazyLoader(func() ScrapePoolsRetriever {
+ return NewEmptyScrapePoolsRetriever()
+ })
+ }
+
+ if cfg.AlertmanagerRetriever == nil {
+ cfg.AlertmanagerRetriever = NewLazyLoader(func() AlertmanagerRetriever {
+ return NewEmptyAlertmanagerRetriever()
+ })
+ }
+
+ if cfg.TSDBAdmin == nil {
+ cfg.TSDBAdmin = NewLazyLoader(func() TSDBAdminStats {
+ return NewEmptyTSDBAdminStats()
+ })
+ }
+
+ if cfg.Config == nil {
+ cfg.Config = func() config.Config { return config.Config{} }
+ }
+
+ if cfg.FlagsMap == nil {
+ cfg.FlagsMap = map[string]string{}
+ }
+
+ if cfg.DBDir == "" {
+ cfg.DBDir = t.TempDir()
+ }
+
+ return NewAPIParams{
+ QueryEngine: cfg.QueryEngine.Get(),
+ Queryable: cfg.Queryable.Get(),
+ ExemplarQueryable: cfg.ExemplarQueryable.Get(),
+ ScrapePoolsRetriever: func(context.Context) ScrapePoolsRetriever { return cfg.ScrapePoolsRetriever.Get() },
+ TargetRetriever: func(context.Context) TargetRetriever { return cfg.TargetRetriever.Get() },
+ AlertmanagerRetriever: func(context.Context) AlertmanagerRetriever { return cfg.AlertmanagerRetriever.Get() },
+ ConfigFunc: cfg.Config,
+ FlagsMap: cfg.FlagsMap,
+ ReadyFunc: func(f http.HandlerFunc) http.HandlerFunc { return f },
+ TSDBAdmin: cfg.TSDBAdmin.Get(),
+ DBDir: cfg.DBDir,
+ Logger: promslog.NewNopLogger(),
+ RulesRetriever: func(context.Context) RulesRetriever { return cfg.RulesRetriever.Get() },
+ RuntimeInfoFunc: func() (RuntimeInfo, error) { return RuntimeInfo{}, nil },
+ BuildInfo: &PrometheusVersion{},
+ NotificationsGetter: func() []notifications.Notification { return nil },
+ NotificationsSub: func() (<-chan notifications.Notification, func(), bool) { return nil, func() {}, false },
+ Gatherer: prometheus.NewRegistry(),
+ Registerer: prometheus.NewRegistry(),
+ }
+}
diff --git a/web/api/testhelpers/assertions.go b/web/api/testhelpers/assertions.go
new file mode 100644
index 0000000000..8a0a0d4a97
--- /dev/null
+++ b/web/api/testhelpers/assertions.go
@@ -0,0 +1,262 @@
+// 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.
+
+// This file provides assertion helpers for validating API responses in tests.
+package testhelpers
+
+import (
+ "fmt"
+ "slices"
+ "strings"
+
+ "github.com/stretchr/testify/require"
+)
+
+// RequireSuccess asserts that the response has status "success" and returns the response for chaining.
+func (r *Response) RequireSuccess() *Response {
+ r.t.Helper()
+ require.NotNil(r.t, r.JSON, "response body is not JSON")
+ require.Equal(r.t, "success", r.JSON["status"], "expected status to be 'success'")
+ return r
+}
+
+// RequireError asserts that the response has status "error" and returns the response for chaining.
+func (r *Response) RequireError() *Response {
+ r.t.Helper()
+ require.NotNil(r.t, r.JSON, "response body is not JSON")
+ require.Equal(r.t, "error", r.JSON["status"], "expected status to be 'error'")
+ return r
+}
+
+// RequireStatusCode asserts that the response has the given HTTP status code and returns the response for chaining.
+func (r *Response) RequireStatusCode(expectedCode int) *Response {
+ r.t.Helper()
+ require.Equal(r.t, expectedCode, r.StatusCode, "unexpected HTTP status code")
+ return r
+}
+
+// RequireJSONPathExists asserts that a JSON path exists and returns the response for chaining.
+func (r *Response) RequireJSONPathExists(path string) *Response {
+ r.t.Helper()
+ require.NotNil(r.t, r.JSON, "response body is not JSON")
+
+ value := getJSONPath(r.JSON, path)
+ require.NotNil(r.t, value, "JSON path %q does not exist", path)
+ return r
+}
+
+// RequireJSONPathNotExists asserts that a JSON path does not exist and returns the response for chaining.
+func (r *Response) RequireJSONPathNotExists(path string) *Response {
+ r.t.Helper()
+ require.NotNil(r.t, r.JSON, "response body is not JSON")
+
+ value := getJSONPath(r.JSON, path)
+ require.Nil(r.t, value, "JSON path %q should not exist but was found", path)
+ return r
+}
+
+// RequireEquals asserts that a JSON path equals the expected value and returns the response for chaining.
+func (r *Response) RequireEquals(path string, expected any) *Response {
+ r.t.Helper()
+ require.NotNil(r.t, r.JSON, "response body is not JSON")
+
+ value := getJSONPath(r.JSON, path)
+ require.NotNil(r.t, value, "JSON path %q does not exist", path)
+ require.Equal(r.t, expected, value, "JSON path %q has unexpected value", path)
+ return r
+}
+
+// RequireJSONArray asserts that a JSON path contains an array and returns the response for chaining.
+func (r *Response) RequireJSONArray(path string) *Response {
+ r.t.Helper()
+ require.NotNil(r.t, r.JSON, "response body is not JSON")
+
+ value := getJSONPath(r.JSON, path)
+ require.NotNil(r.t, value, "JSON path %q does not exist", path)
+ _, ok := value.([]any)
+ require.True(r.t, ok, "JSON path %q is not an array", path)
+ return r
+}
+
+// RequireLenAtLeast asserts that a JSON path contains an array with at least minLen elements and returns the response for chaining.
+func (r *Response) RequireLenAtLeast(path string, minLen int) *Response {
+ r.t.Helper()
+ require.NotNil(r.t, r.JSON, "response body is not JSON")
+
+ value := getJSONPath(r.JSON, path)
+ require.NotNil(r.t, value, "JSON path %q does not exist", path)
+ arr, ok := value.([]any)
+ require.True(r.t, ok, "JSON path %q is not an array", path)
+ require.GreaterOrEqual(r.t, len(arr), minLen, "JSON path %q has fewer than %d elements", path, minLen)
+ return r
+}
+
+// RequireArrayContains asserts that a JSON path contains an array with the expected element and returns the response for chaining.
+func (r *Response) RequireArrayContains(path string, expected any) *Response {
+ r.t.Helper()
+ require.NotNil(r.t, r.JSON, "response body is not JSON")
+
+ value := getJSONPath(r.JSON, path)
+ require.NotNil(r.t, value, "JSON path %q does not exist", path)
+ arr, ok := value.([]any)
+ require.True(r.t, ok, "JSON path %q is not an array", path)
+
+ found := slices.Contains(arr, expected)
+ require.True(r.t, found, "JSON path %q does not contain expected value %v", path, expected)
+ return r
+}
+
+// RequireSome asserts that at least one element in an array satisfies the predicate and returns the response for chaining.
+func (r *Response) RequireSome(path string, predicate func(any) bool) *Response {
+ r.t.Helper()
+ require.NotNil(r.t, r.JSON, "response body is not JSON")
+
+ value := getJSONPath(r.JSON, path)
+ require.NotNil(r.t, value, "JSON path %q does not exist", path)
+ arr, ok := value.([]any)
+ require.True(r.t, ok, "JSON path %q is not an array", path)
+
+ found := slices.ContainsFunc(arr, predicate)
+ require.True(r.t, found, "no element in JSON path %q satisfies the predicate", path)
+ return r
+}
+
+// getJSONPath extracts a value from a JSON object using a simple path notation.
+// Supports paths like "$.data", "$.data.groups", "$.data.groups[0]".
+func getJSONPath(data map[string]any, path string) any {
+ // Remove leading "$." if present.
+ path = strings.TrimPrefix(path, "$.")
+
+ if path == "" {
+ return data
+ }
+
+ parts := strings.Split(path, ".")
+ current := any(data)
+
+ for _, part := range parts {
+ // Handle array indexing (e.g., "groups[0]").
+ if strings.Contains(part, "[") {
+ // Not implementing array indexing for simplicity.
+ // Tests should use direct field access or RequireSome.
+ return nil
+ }
+
+ // Navigate to the next level.
+ m, ok := current.(map[string]any)
+ if !ok {
+ return nil
+ }
+ current = m[part]
+ }
+
+ return current
+}
+
+// RequireVectorResult is a convenience helper for checking vector query results.
+func (r *Response) RequireVectorResult() *Response {
+ r.t.Helper()
+ return r.RequireSuccess().RequireEquals("$.data.resultType", "vector")
+}
+
+// RequireMatrixResult is a convenience helper for checking matrix query results.
+func (r *Response) RequireMatrixResult() *Response {
+ r.t.Helper()
+ return r.RequireSuccess().RequireEquals("$.data.resultType", "matrix")
+}
+
+// RequireScalarResult is a convenience helper for checking scalar query results.
+func (r *Response) RequireScalarResult() *Response {
+ r.t.Helper()
+ return r.RequireSuccess().RequireEquals("$.data.resultType", "scalar")
+}
+
+// RequireRulesGroupNamed asserts that a rules response contains a group with the given name.
+func (r *Response) RequireRulesGroupNamed(name string) *Response {
+ r.t.Helper()
+ return r.RequireSuccess().RequireSome("$.data.groups", func(group any) bool {
+ if g, ok := group.(map[string]any); ok {
+ return g["name"] == name
+ }
+ return false
+ })
+}
+
+// RequireTargetCount asserts that a targets response contains at least n targets.
+func (r *Response) RequireTargetCount(minCount int) *Response {
+ r.t.Helper()
+ r.RequireSuccess()
+
+ // The targets endpoint returns activeTargets as an array of targets.
+ value := getJSONPath(r.JSON, "$.data.activeTargets")
+ require.NotNil(r.t, value, "JSON path $.data.activeTargets does not exist")
+
+ arr, ok := value.([]any)
+ require.True(r.t, ok, "$.data.activeTargets is not an array")
+ require.GreaterOrEqual(r.t, len(arr), minCount, "expected at least %d targets, got %d", minCount, len(arr))
+ return r
+}
+
+// DebugJSON is a helper for debugging JSON responses in tests.
+func (r *Response) DebugJSON() *Response {
+ r.t.Helper()
+ r.t.Logf("Response status code: %d", r.StatusCode)
+ r.t.Logf("Response body: %s", r.Body)
+ if r.JSON != nil {
+ r.t.Logf("Response JSON: %+v", r.JSON)
+ }
+ return r
+}
+
+// RequireContainsSubstring asserts that the response body contains the given substring.
+func (r *Response) RequireContainsSubstring(substring string) *Response {
+ r.t.Helper()
+ require.Contains(r.t, r.Body, substring, "response body does not contain expected substring")
+ return r
+}
+
+// RequireField asserts that a field exists at the given path and returns its value.
+// Note: This method cannot be chained further since it returns the field value, not the Response.
+func (r *Response) RequireField(path string) any {
+ r.t.Helper()
+ require.NotNil(r.t, r.JSON, "response body is not JSON")
+
+ value := getJSONPath(r.JSON, path)
+ require.NotNil(r.t, value, "JSON path %q does not exist", path)
+ return value
+}
+
+// RequireFieldType asserts that a field exists and has the expected type.
+func (r *Response) RequireFieldType(path, expectedType string) *Response {
+ r.t.Helper()
+ value := r.RequireField(path)
+
+ var actualType string
+ switch value.(type) {
+ case string:
+ actualType = "string"
+ case float64:
+ actualType = "number"
+ case bool:
+ actualType = "bool"
+ case []any:
+ actualType = "array"
+ case map[string]any:
+ actualType = "object"
+ default:
+ actualType = fmt.Sprintf("%T", value)
+ }
+
+ require.Equal(r.t, expectedType, actualType, "JSON path %q has unexpected type", path)
+ return r
+}
diff --git a/web/api/testhelpers/fixtures.go b/web/api/testhelpers/fixtures.go
new file mode 100644
index 0000000000..7bb0151dca
--- /dev/null
+++ b/web/api/testhelpers/fixtures.go
@@ -0,0 +1,180 @@
+// 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.
+
+// This file provides test fixture data for API tests.
+package testhelpers
+
+import (
+ "time"
+
+ "github.com/prometheus/prometheus/model/histogram"
+ "github.com/prometheus/prometheus/model/labels"
+ "github.com/prometheus/prometheus/promql"
+ "github.com/prometheus/prometheus/promql/parser"
+ "github.com/prometheus/prometheus/rules"
+ "github.com/prometheus/prometheus/storage"
+)
+
+var testParser = parser.NewParser(parser.Options{})
+
+// FixtureSeries creates a simple series with the "up" metric.
+func FixtureSeries() []storage.Series {
+ // Use timestamps relative to "now" so queries work.
+ now := time.Now().UnixMilli()
+ return []storage.Series{
+ &FakeSeries{
+ labels: labels.FromStrings("__name__", "up", "job", "prometheus", "instance", "localhost:9090"),
+ samples: []promql.FPoint{
+ {T: now - 120000, F: 1},
+ {T: now - 60000, F: 1},
+ {T: now, F: 1},
+ },
+ },
+ }
+}
+
+// FixtureMultipleSeries creates multiple series for testing.
+func FixtureMultipleSeries() []storage.Series {
+ // Use timestamps relative to "now" so queries work.
+ now := time.Now().UnixMilli()
+ return []storage.Series{
+ &FakeSeries{
+ labels: labels.FromStrings("__name__", "up", "job", "prometheus", "instance", "localhost:9090"),
+ samples: []promql.FPoint{
+ {T: now - 60000, F: 1},
+ {T: now, F: 1},
+ },
+ },
+ &FakeSeries{
+ labels: labels.FromStrings("__name__", "up", "job", "node", "instance", "localhost:9100"),
+ samples: []promql.FPoint{
+ {T: now - 60000, F: 1},
+ {T: now, F: 0},
+ },
+ },
+ &FakeSeries{
+ labels: labels.FromStrings("__name__", "http_requests_total", "job", "api", "instance", "localhost:8080"),
+ samples: []promql.FPoint{
+ {T: now - 60000, F: 100},
+ {T: now, F: 150},
+ },
+ },
+ }
+}
+
+// FixtureRuleGroups creates a simple set of rule groups for testing.
+func FixtureRuleGroups() []*rules.Group {
+ // Create a simple recording rule.
+ expr, _ := testParser.ParseExpr("up == 1")
+ recordingRule := rules.NewRecordingRule(
+ "job:up:sum",
+ expr,
+ labels.EmptyLabels(),
+ )
+
+ // Create a simple alerting rule.
+ alertExpr, _ := testParser.ParseExpr("up == 0")
+ alertingRule := rules.NewAlertingRule(
+ "InstanceDown",
+ alertExpr,
+ time.Minute,
+ 0,
+ labels.FromStrings("severity", "critical"),
+ labels.EmptyLabels(),
+ labels.EmptyLabels(),
+ "Instance {{ $labels.instance }} is down",
+ true,
+ nil,
+ )
+
+ // Create a rule group.
+ group := rules.NewGroup(rules.GroupOptions{
+ Name: "example",
+ File: "example.rules",
+ Interval: time.Minute,
+ Rules: []rules.Rule{
+ recordingRule,
+ alertingRule,
+ },
+ })
+
+ return []*rules.Group{group}
+}
+
+// FixtureEmptyRuleGroups returns an empty set of rule groups.
+func FixtureEmptyRuleGroups() []*rules.Group {
+ return []*rules.Group{}
+}
+
+// FixtureSingleSeries creates a single series for simple tests.
+func FixtureSingleSeries(metricName string, value float64) []storage.Series {
+ return []storage.Series{
+ &FakeSeries{
+ labels: labels.FromStrings("__name__", metricName),
+ samples: []promql.FPoint{
+ {T: 0, F: value},
+ },
+ },
+ }
+}
+
+// FixtureHistogramSeries creates a series with native histogram data.
+func FixtureHistogramSeries() []storage.Series {
+ // Use timestamps relative to "now" so queries work.
+ now := time.Now().UnixMilli()
+ return []storage.Series{
+ &FakeHistogramSeries{
+ labels: labels.FromStrings("__name__", "test_histogram", "job", "prometheus", "instance", "localhost:9090"),
+ histograms: []promql.HPoint{
+ {
+ T: now - 60000,
+ H: &histogram.FloatHistogram{
+ Schema: 2,
+ ZeroThreshold: 0.001,
+ ZeroCount: 5,
+ Count: 50,
+ Sum: 100,
+ PositiveSpans: []histogram.Span{
+ {Offset: 0, Length: 2},
+ {Offset: 1, Length: 2},
+ },
+ NegativeSpans: []histogram.Span{
+ {Offset: 0, Length: 1},
+ },
+ PositiveBuckets: []float64{5, 10, 8, 7},
+ NegativeBuckets: []float64{3},
+ },
+ },
+ {
+ T: now,
+ H: &histogram.FloatHistogram{
+ Schema: 2,
+ ZeroThreshold: 0.001,
+ ZeroCount: 8,
+ Count: 60,
+ Sum: 120,
+ PositiveSpans: []histogram.Span{
+ {Offset: 0, Length: 2},
+ {Offset: 1, Length: 2},
+ },
+ NegativeSpans: []histogram.Span{
+ {Offset: 0, Length: 1},
+ },
+ PositiveBuckets: []float64{6, 12, 10, 9},
+ NegativeBuckets: []float64{4},
+ },
+ },
+ },
+ },
+ }
+}
diff --git a/web/api/testhelpers/mocks.go b/web/api/testhelpers/mocks.go
new file mode 100644
index 0000000000..527febb727
--- /dev/null
+++ b/web/api/testhelpers/mocks.go
@@ -0,0 +1,534 @@
+// 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.
+
+// This file contains mock implementations of API dependencies for testing.
+package testhelpers
+
+import (
+ "context"
+ "net/url"
+
+ "github.com/prometheus/prometheus/config"
+ "github.com/prometheus/prometheus/model/exemplar"
+ "github.com/prometheus/prometheus/model/histogram"
+ "github.com/prometheus/prometheus/model/labels"
+ "github.com/prometheus/prometheus/promql"
+ "github.com/prometheus/prometheus/rules"
+ "github.com/prometheus/prometheus/scrape"
+ "github.com/prometheus/prometheus/storage"
+ "github.com/prometheus/prometheus/tsdb"
+ "github.com/prometheus/prometheus/tsdb/chunkenc"
+ "github.com/prometheus/prometheus/tsdb/chunks"
+ "github.com/prometheus/prometheus/util/annotations"
+)
+
+// LazyLoader allows lazy initialization of mocks per test.
+type LazyLoader[T any] struct {
+ loader func() T
+ value *T
+}
+
+// NewLazyLoader creates a new LazyLoader with the given loader function.
+func NewLazyLoader[T any](loader func() T) *LazyLoader[T] {
+ return &LazyLoader[T]{loader: loader}
+}
+
+// Get returns the loaded value, initializing it if necessary.
+func (l *LazyLoader[T]) Get() T {
+ if l.value == nil {
+ v := l.loader()
+ l.value = &v
+ }
+ return *l.value
+}
+
+// FakeQueryable implements storage.SampleAndChunkQueryable with configurable behavior.
+type FakeQueryable struct {
+ series []storage.Series
+}
+
+func (f *FakeQueryable) Querier(_, _ int64) (storage.Querier, error) {
+ return &FakeQuerier{series: f.series}, nil
+}
+
+func (f *FakeQueryable) ChunkQuerier(_, _ int64) (storage.ChunkQuerier, error) {
+ return &FakeChunkQuerier{series: f.series}, nil
+}
+
+// FakeQuerier implements storage.Querier.
+type FakeQuerier struct {
+ series []storage.Series
+}
+
+func (f *FakeQuerier) Select(_ context.Context, _ bool, _ *storage.SelectHints, _ ...*labels.Matcher) storage.SeriesSet {
+ return &FakeSeriesSet{series: f.series, idx: -1}
+}
+
+func (f *FakeQuerier) LabelValues(_ context.Context, name string, _ *storage.LabelHints, _ ...*labels.Matcher) ([]string, annotations.Annotations, error) {
+ valuesMap := make(map[string]struct{})
+ for _, s := range f.series {
+ lbls := s.Labels()
+ if val := lbls.Get(name); val != "" {
+ valuesMap[val] = struct{}{}
+ }
+ }
+ values := make([]string, 0, len(valuesMap))
+ for v := range valuesMap {
+ values = append(values, v)
+ }
+ return values, nil, nil
+}
+
+func (f *FakeQuerier) LabelNames(_ context.Context, _ *storage.LabelHints, _ ...*labels.Matcher) ([]string, annotations.Annotations, error) {
+ namesMap := make(map[string]struct{})
+ for _, s := range f.series {
+ lbls := s.Labels()
+ lbls.Range(func(l labels.Label) {
+ namesMap[l.Name] = struct{}{}
+ })
+ }
+ names := make([]string, 0, len(namesMap))
+ for n := range namesMap {
+ names = append(names, n)
+ }
+ return names, nil, nil
+}
+
+func (*FakeQuerier) Close() error {
+ return nil
+}
+
+// FakeChunkQuerier implements storage.ChunkQuerier.
+type FakeChunkQuerier struct {
+ series []storage.Series
+}
+
+func (f *FakeChunkQuerier) Select(_ context.Context, _ bool, _ *storage.SelectHints, _ ...*labels.Matcher) storage.ChunkSeriesSet {
+ return &FakeChunkSeriesSet{series: f.series, idx: -1}
+}
+
+func (f *FakeChunkQuerier) LabelValues(_ context.Context, name string, _ *storage.LabelHints, _ ...*labels.Matcher) ([]string, annotations.Annotations, error) {
+ valuesMap := make(map[string]struct{})
+ for _, s := range f.series {
+ lbls := s.Labels()
+ if val := lbls.Get(name); val != "" {
+ valuesMap[val] = struct{}{}
+ }
+ }
+ values := make([]string, 0, len(valuesMap))
+ for v := range valuesMap {
+ values = append(values, v)
+ }
+ return values, nil, nil
+}
+
+func (f *FakeChunkQuerier) LabelNames(_ context.Context, _ *storage.LabelHints, _ ...*labels.Matcher) ([]string, annotations.Annotations, error) {
+ namesMap := make(map[string]struct{})
+ for _, s := range f.series {
+ lbls := s.Labels()
+ lbls.Range(func(l labels.Label) {
+ namesMap[l.Name] = struct{}{}
+ })
+ }
+ names := make([]string, 0, len(namesMap))
+ for n := range namesMap {
+ names = append(names, n)
+ }
+ return names, nil, nil
+}
+
+func (*FakeChunkQuerier) Close() error {
+ return nil
+}
+
+// FakeSeriesSet implements storage.SeriesSet.
+type FakeSeriesSet struct {
+ series []storage.Series
+ idx int
+}
+
+func (f *FakeSeriesSet) Next() bool {
+ f.idx++
+ return f.idx < len(f.series)
+}
+
+func (f *FakeSeriesSet) At() storage.Series {
+ return f.series[f.idx]
+}
+
+func (*FakeSeriesSet) Err() error {
+ return nil
+}
+
+func (*FakeSeriesSet) Warnings() annotations.Annotations {
+ return nil
+}
+
+// FakeChunkSeriesSet implements storage.ChunkSeriesSet.
+type FakeChunkSeriesSet struct {
+ series []storage.Series
+ idx int
+}
+
+func (f *FakeChunkSeriesSet) Next() bool {
+ f.idx++
+ return f.idx < len(f.series)
+}
+
+func (f *FakeChunkSeriesSet) At() storage.ChunkSeries {
+ return &FakeChunkSeries{series: f.series[f.idx]}
+}
+
+func (*FakeChunkSeriesSet) Err() error {
+ return nil
+}
+
+func (*FakeChunkSeriesSet) Warnings() annotations.Annotations {
+ return nil
+}
+
+// FakeChunkSeries implements storage.ChunkSeries.
+type FakeChunkSeries struct {
+ series storage.Series
+}
+
+func (f *FakeChunkSeries) Labels() labels.Labels {
+ return f.series.Labels()
+}
+
+func (*FakeChunkSeries) Iterator(_ chunks.Iterator) chunks.Iterator {
+ return &FakeChunkSeriesIterator{}
+}
+
+// FakeChunkSeriesIterator implements chunks.Iterator.
+type FakeChunkSeriesIterator struct{}
+
+func (*FakeChunkSeriesIterator) Next() bool {
+ return false
+}
+
+func (*FakeChunkSeriesIterator) At() chunks.Meta {
+ return chunks.Meta{}
+}
+
+func (*FakeChunkSeriesIterator) Err() error {
+ return nil
+}
+
+// FakeSeries implements storage.Series.
+type FakeSeries struct {
+ labels labels.Labels
+ samples []promql.FPoint
+}
+
+func (f *FakeSeries) Labels() labels.Labels {
+ return f.labels
+}
+
+func (f *FakeSeries) Iterator(chunkenc.Iterator) chunkenc.Iterator {
+ return &FakeSeriesIterator{samples: f.samples, idx: -1}
+}
+
+// FakeSeriesIterator implements chunkenc.Iterator.
+type FakeSeriesIterator struct {
+ samples []promql.FPoint
+ idx int
+}
+
+func (f *FakeSeriesIterator) Next() chunkenc.ValueType {
+ f.idx++
+ if f.idx < len(f.samples) {
+ return chunkenc.ValFloat
+ }
+ return chunkenc.ValNone
+}
+
+func (f *FakeSeriesIterator) Seek(t int64) chunkenc.ValueType {
+ for f.idx < len(f.samples)-1 {
+ f.idx++
+ if f.samples[f.idx].T >= t {
+ return chunkenc.ValFloat
+ }
+ }
+ return chunkenc.ValNone
+}
+
+func (f *FakeSeriesIterator) At() (int64, float64) {
+ s := f.samples[f.idx]
+ return s.T, s.F
+}
+
+func (*FakeSeriesIterator) AtHistogram(*histogram.Histogram) (int64, *histogram.Histogram) {
+ panic("not implemented")
+}
+
+func (*FakeSeriesIterator) AtFloatHistogram(*histogram.FloatHistogram) (int64, *histogram.FloatHistogram) {
+ panic("not implemented")
+}
+
+func (f *FakeSeriesIterator) AtT() int64 {
+ return f.samples[f.idx].T
+}
+
+func (*FakeSeriesIterator) AtST() int64 {
+ return 0
+}
+
+func (*FakeSeriesIterator) Err() error {
+ return nil
+}
+
+// FakeHistogramSeries implements storage.Series for histogram data.
+type FakeHistogramSeries struct {
+ labels labels.Labels
+ histograms []promql.HPoint
+}
+
+func (f *FakeHistogramSeries) Labels() labels.Labels {
+ return f.labels
+}
+
+func (f *FakeHistogramSeries) Iterator(chunkenc.Iterator) chunkenc.Iterator {
+ return &FakeHistogramSeriesIterator{histograms: f.histograms, idx: -1}
+}
+
+// FakeHistogramSeriesIterator implements chunkenc.Iterator for histogram data.
+type FakeHistogramSeriesIterator struct {
+ histograms []promql.HPoint
+ idx int
+}
+
+func (f *FakeHistogramSeriesIterator) Next() chunkenc.ValueType {
+ f.idx++
+ if f.idx < len(f.histograms) {
+ return chunkenc.ValFloatHistogram
+ }
+ return chunkenc.ValNone
+}
+
+func (f *FakeHistogramSeriesIterator) Seek(t int64) chunkenc.ValueType {
+ for f.idx < len(f.histograms)-1 {
+ f.idx++
+ if f.histograms[f.idx].T >= t {
+ return chunkenc.ValFloatHistogram
+ }
+ }
+ return chunkenc.ValNone
+}
+
+func (*FakeHistogramSeriesIterator) At() (int64, float64) {
+ panic("not a float value")
+}
+
+func (*FakeHistogramSeriesIterator) AtHistogram(*histogram.Histogram) (int64, *histogram.Histogram) {
+ panic("not implemented")
+}
+
+func (f *FakeHistogramSeriesIterator) AtFloatHistogram(*histogram.FloatHistogram) (int64, *histogram.FloatHistogram) {
+ h := f.histograms[f.idx]
+ return h.T, h.H
+}
+
+func (f *FakeHistogramSeriesIterator) AtT() int64 {
+ return f.histograms[f.idx].T
+}
+
+func (*FakeHistogramSeriesIterator) AtST() int64 {
+ return 0
+}
+
+func (*FakeHistogramSeriesIterator) Err() error {
+ return nil
+}
+
+// FakeExemplarQueryable implements storage.ExemplarQueryable.
+type FakeExemplarQueryable struct{}
+
+func (*FakeExemplarQueryable) ExemplarQuerier(_ context.Context) (storage.ExemplarQuerier, error) {
+ return &FakeExemplarQuerier{}, nil
+}
+
+// FakeExemplarQuerier implements storage.ExemplarQuerier.
+type FakeExemplarQuerier struct{}
+
+func (*FakeExemplarQuerier) Select(_, _ int64, _ ...[]*labels.Matcher) ([]exemplar.QueryResult, error) {
+ return nil, nil
+}
+
+// FakeRulesRetriever implements v1.RulesRetriever.
+type FakeRulesRetriever struct {
+ groups []*rules.Group
+}
+
+func (f *FakeRulesRetriever) RuleGroups() []*rules.Group {
+ return f.groups
+}
+
+func (f *FakeRulesRetriever) AlertingRules() []*rules.AlertingRule {
+ var alertingRules []*rules.AlertingRule
+ for _, g := range f.groups {
+ for _, r := range g.Rules() {
+ if ar, ok := r.(*rules.AlertingRule); ok {
+ alertingRules = append(alertingRules, ar)
+ }
+ }
+ }
+ return alertingRules
+}
+
+// FakeTargetRetriever implements v1.TargetRetriever.
+type FakeTargetRetriever struct {
+ active map[string][]*scrape.Target
+ dropped map[string][]*scrape.Target
+ droppedCounts map[string]int
+ scrapeConfig map[string]*config.ScrapeConfig
+}
+
+func (f *FakeTargetRetriever) TargetsActive() map[string][]*scrape.Target {
+ if f.active == nil {
+ return make(map[string][]*scrape.Target)
+ }
+ return f.active
+}
+
+func (f *FakeTargetRetriever) TargetsDropped() map[string][]*scrape.Target {
+ if f.dropped == nil {
+ return make(map[string][]*scrape.Target)
+ }
+ return f.dropped
+}
+
+func (f *FakeTargetRetriever) TargetsDroppedCounts() map[string]int {
+ if f.droppedCounts == nil {
+ return make(map[string]int)
+ }
+ return f.droppedCounts
+}
+
+func (f *FakeTargetRetriever) ScrapePoolConfig(name string) (*config.ScrapeConfig, error) {
+ if f.scrapeConfig == nil {
+ return nil, nil
+ }
+ return f.scrapeConfig[name], nil
+}
+
+// FakeScrapePoolsRetriever implements v1.ScrapePoolsRetriever.
+type FakeScrapePoolsRetriever struct {
+ pools []string
+}
+
+func (f *FakeScrapePoolsRetriever) ScrapePools() []string {
+ if f.pools == nil {
+ return []string{}
+ }
+ return f.pools
+}
+
+// FakeAlertmanagerRetriever implements v1.AlertmanagerRetriever.
+type FakeAlertmanagerRetriever struct{}
+
+func (*FakeAlertmanagerRetriever) Alertmanagers() []*url.URL {
+ return nil
+}
+
+func (*FakeAlertmanagerRetriever) DroppedAlertmanagers() []*url.URL {
+ return nil
+}
+
+// FakeTSDBAdminStats implements v1.TSDBAdminStats.
+type FakeTSDBAdminStats struct{}
+
+func (*FakeTSDBAdminStats) CleanTombstones() error {
+ return nil
+}
+
+func (*FakeTSDBAdminStats) Delete(_ context.Context, _, _ int64, _ ...*labels.Matcher) error {
+ return nil
+}
+
+func (*FakeTSDBAdminStats) Snapshot(_ string, _ bool) error {
+ return nil
+}
+
+func (*FakeTSDBAdminStats) Stats(_ string, _ int) (*tsdb.Stats, error) {
+ return &tsdb.Stats{}, nil
+}
+
+func (*FakeTSDBAdminStats) WALReplayStatus() (tsdb.WALReplayStatus, error) {
+ return tsdb.WALReplayStatus{}, nil
+}
+
+func (*FakeTSDBAdminStats) BlockMetas() ([]tsdb.BlockMeta, error) {
+ return []tsdb.BlockMeta{}, nil
+}
+
+// NewEmptyQueryable returns a queryable with no series.
+func NewEmptyQueryable() storage.SampleAndChunkQueryable {
+ return &FakeQueryable{series: []storage.Series{}}
+}
+
+// NewQueryableWithSeries returns a queryable with the given series.
+func NewQueryableWithSeries(series []storage.Series) storage.SampleAndChunkQueryable {
+ return &FakeQueryable{series: series}
+}
+
+// TSDBNotReadyQueryable implements storage.SampleAndChunkQueryable that returns tsdb.ErrNotReady.
+type TSDBNotReadyQueryable struct{}
+
+func (*TSDBNotReadyQueryable) Querier(_, _ int64) (storage.Querier, error) {
+ return nil, tsdb.ErrNotReady
+}
+
+func (*TSDBNotReadyQueryable) ChunkQuerier(_, _ int64) (storage.ChunkQuerier, error) {
+ return nil, tsdb.ErrNotReady
+}
+
+// NewTSDBNotReadyQueryable returns a queryable that always returns tsdb.ErrNotReady.
+func NewTSDBNotReadyQueryable() storage.SampleAndChunkQueryable {
+ return &TSDBNotReadyQueryable{}
+}
+
+// NewEmptyExemplarQueryable returns an exemplar queryable with no exemplars.
+func NewEmptyExemplarQueryable() storage.ExemplarQueryable {
+ return &FakeExemplarQueryable{}
+}
+
+// NewEmptyRulesRetriever returns a rules retriever with no rules.
+func NewEmptyRulesRetriever() *FakeRulesRetriever {
+ return &FakeRulesRetriever{groups: []*rules.Group{}}
+}
+
+// NewRulesRetrieverWithGroups returns a rules retriever with the given groups.
+func NewRulesRetrieverWithGroups(groups []*rules.Group) *FakeRulesRetriever {
+ return &FakeRulesRetriever{groups: groups}
+}
+
+// NewEmptyTargetRetriever returns a target retriever with no targets.
+func NewEmptyTargetRetriever() *FakeTargetRetriever {
+ return &FakeTargetRetriever{}
+}
+
+// NewEmptyScrapePoolsRetriever returns a scrape pools retriever with no pools.
+func NewEmptyScrapePoolsRetriever() *FakeScrapePoolsRetriever {
+ return &FakeScrapePoolsRetriever{pools: []string{}}
+}
+
+// NewEmptyAlertmanagerRetriever returns an alertmanager retriever with no alertmanagers.
+func NewEmptyAlertmanagerRetriever() *FakeAlertmanagerRetriever {
+ return &FakeAlertmanagerRetriever{}
+}
+
+// NewEmptyTSDBAdminStats returns a TSDB admin stats with no-op implementations.
+func NewEmptyTSDBAdminStats() *FakeTSDBAdminStats {
+ return &FakeTSDBAdminStats{}
+}
diff --git a/web/api/testhelpers/openapi.go b/web/api/testhelpers/openapi.go
new file mode 100644
index 0000000000..d2e88943d2
--- /dev/null
+++ b/web/api/testhelpers/openapi.go
@@ -0,0 +1,204 @@
+// 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.
+
+// This file provides OpenAPI-specific test utilities for validating spec compliance.
+package testhelpers
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "net/http"
+ "os"
+ "path/filepath"
+ "strings"
+ "sync"
+
+ "github.com/pb33f/libopenapi"
+ validator "github.com/pb33f/libopenapi-validator"
+ valerrors "github.com/pb33f/libopenapi-validator/errors"
+ "github.com/stretchr/testify/require"
+)
+
+var (
+ openAPIValidator31 validator.Validator
+ openAPIValidator32 validator.Validator
+ openAPIValidatorOnce sync.Once
+ openAPIValidatorErr error
+)
+
+// loadOpenAPIValidators loads and caches both OpenAPI 3.1 and 3.2 validators from golden files.
+func loadOpenAPIValidators() (v31, v32 validator.Validator, err error) {
+ openAPIValidatorOnce.Do(func() {
+ // Load OpenAPI 3.1 validator.
+ goldenPath31 := filepath.Join("testdata", "openapi_3.1_golden.yaml")
+ specBytes31, err := os.ReadFile(goldenPath31)
+ if err != nil {
+ openAPIValidatorErr = fmt.Errorf("failed to read OpenAPI 3.1 spec from %s: %w", goldenPath31, err)
+ return
+ }
+
+ doc31, err := libopenapi.NewDocument(specBytes31)
+ if err != nil {
+ openAPIValidatorErr = fmt.Errorf("failed to parse OpenAPI 3.1 document: %w", err)
+ return
+ }
+
+ v31, errs := validator.NewValidator(doc31)
+ if len(errs) > 0 {
+ openAPIValidatorErr = fmt.Errorf("failed to create OpenAPI 3.1 validator: %v", errs)
+ return
+ }
+
+ openAPIValidator31 = v31
+
+ // Load OpenAPI 3.2 validator.
+ goldenPath32 := filepath.Join("testdata", "openapi_3.2_golden.yaml")
+ specBytes32, err := os.ReadFile(goldenPath32)
+ if err != nil {
+ openAPIValidatorErr = fmt.Errorf("failed to read OpenAPI 3.2 spec from %s: %w", goldenPath32, err)
+ return
+ }
+
+ doc32, err := libopenapi.NewDocument(specBytes32)
+ if err != nil {
+ openAPIValidatorErr = fmt.Errorf("failed to parse OpenAPI 3.2 document: %w", err)
+ return
+ }
+
+ v32, errs := validator.NewValidator(doc32)
+ if len(errs) > 0 {
+ openAPIValidatorErr = fmt.Errorf("failed to create OpenAPI 3.2 validator: %v", errs)
+ return
+ }
+
+ openAPIValidator32 = v32
+ })
+
+ if openAPIValidatorErr != nil {
+ return nil, nil, openAPIValidatorErr
+ }
+
+ return openAPIValidator31, openAPIValidator32, nil
+}
+
+// ValidateOpenAPI validates the request and response against both OpenAPI 3.1 and 3.2 specifications.
+// This ensures API endpoints are compatible with both OpenAPI versions.
+// Returns the response for chaining.
+func (r *Response) ValidateOpenAPI() *Response {
+ r.t.Helper()
+
+ // Load both validators (cached after first call).
+ v31, v32, err := loadOpenAPIValidators()
+ require.NoError(r.t, err, "failed to load OpenAPI validators")
+
+ // Validate against OpenAPI 3.1 spec.
+ if r.request != nil {
+ r.validateRequestWithVersion(v31, "3.1")
+ }
+ r.validateResponseWithVersion(v31, "3.1")
+
+ // Validate against OpenAPI 3.2 spec.
+ if r.request != nil {
+ r.validateRequestWithVersion(v32, "3.2")
+ }
+ r.validateResponseWithVersion(v32, "3.2")
+
+ return r
+}
+
+// validateRequestWithVersion validates the HTTP request against a specific OpenAPI version's spec.
+func (r *Response) validateRequestWithVersion(v validator.Validator, version string) {
+ r.t.Helper()
+
+ // Create a validation request from the original request.
+ validationReq := &http.Request{
+ Method: r.request.Method,
+ URL: r.request.URL,
+ Header: r.request.Header,
+ Body: io.NopCloser(bytes.NewReader(r.requestBody)),
+ }
+
+ // Validate the request.
+ valid, errors := v.ValidateHttpRequest(validationReq)
+ if !valid {
+ // Check if the error is because the path doesn't exist in this version.
+ // Some endpoints (like /notifications/live) only exist in 3.2, not 3.1.
+ if isPathNotFoundError(errors) && version == "3.1" && strings.Contains(r.request.URL.Path, "/notifications/live") {
+ // Expected: /notifications/live is only in OpenAPI 3.2.
+ return
+ }
+
+ var errorMessages []string
+ for _, e := range errors {
+ errorMessages = append(errorMessages, e.Error())
+ }
+ require.Fail(r.t, fmt.Sprintf("OpenAPI %s request validation failed", version),
+ "Request to %s %s failed OpenAPI %s validation:\n%v",
+ r.request.Method, r.request.URL.Path, version, errorMessages)
+ }
+}
+
+// validateResponseWithVersion validates the HTTP response against a specific OpenAPI version's spec.
+func (r *Response) validateResponseWithVersion(v validator.Validator, version string) {
+ r.t.Helper()
+
+ // Create a validation request (needed for response validation context).
+ validationReq := &http.Request{
+ Method: r.request.Method,
+ URL: r.request.URL,
+ Header: r.request.Header,
+ }
+
+ // Create a response for validation.
+ validationResp := &http.Response{
+ StatusCode: r.StatusCode,
+ Header: r.responseHeader,
+ Body: io.NopCloser(bytes.NewReader([]byte(r.Body))),
+ Request: validationReq,
+ }
+
+ // Validate the response.
+ valid, errors := v.ValidateHttpResponse(validationReq, validationResp)
+ if !valid {
+ // Check if the error is because the path doesn't exist in this version.
+ // Some endpoints (like /notifications/live) only exist in 3.2, not 3.1.
+ if isPathNotFoundError(errors) && version == "3.1" && strings.Contains(r.request.URL.Path, "/notifications/live") {
+ // Expected: /notifications/live is only in OpenAPI 3.2.
+ return
+ }
+
+ var errorMessages []string
+ for _, e := range errors {
+ errorMessages = append(errorMessages, e.Error())
+ }
+ require.Fail(r.t, fmt.Sprintf("OpenAPI %s response validation failed", version),
+ "Response from %s %s (status %d) failed OpenAPI %s validation:\n%v",
+ r.request.Method, r.request.URL.Path, r.StatusCode, version, errorMessages)
+ }
+}
+
+// isPathNotFoundError checks if the validation errors indicate a path was not found in the spec.
+func isPathNotFoundError(errors []*valerrors.ValidationError) bool {
+ for _, err := range errors {
+ errStr := err.Error()
+ // Check for common "path not found" error messages from libopenapi-validator.
+ if strings.Contains(errStr, "path") && (strings.Contains(errStr, "not found") || strings.Contains(errStr, "does not exist")) {
+ return true
+ }
+ if strings.Contains(errStr, "GET /notifications/live") || strings.Contains(errStr, "/notifications/live not found") {
+ return true
+ }
+ }
+ return false
+}
diff --git a/web/api/testhelpers/request.go b/web/api/testhelpers/request.go
new file mode 100644
index 0000000000..81650e4c49
--- /dev/null
+++ b/web/api/testhelpers/request.go
@@ -0,0 +1,145 @@
+// 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.
+
+// This file provides HTTP request builders for testing API endpoints.
+package testhelpers
+
+import (
+ "encoding/json"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "strings"
+ "testing"
+)
+
+// Response wraps an HTTP response with parsed JSON data.
+// It supports method chaining for assertions.
+//
+// Example usage:
+//
+// testhelpers.GET(t, api, "/api/v1/query", "query", "up").
+// ValidateOpenAPI().
+// RequireSuccess().
+// RequireEquals("$.data.resultType", "vector").
+// RequireLenAtLeast("$.data.result", 1)
+//
+// testhelpers.POST(t, api, "/api/v1/query", "query", "up").
+// ValidateOpenAPI().
+// RequireSuccess().
+// RequireArrayContains("$.data.result", expectedValue)
+type Response struct {
+ StatusCode int
+ Body string
+ JSON map[string]any
+ t *testing.T
+ request *http.Request
+ requestBody []byte
+ responseHeader http.Header
+}
+
+// GET sends a GET request to the API and returns a Response with parsed JSON.
+// queryParams should be pairs of key-value strings.
+func GET(t *testing.T, api *APIWrapper, path string, queryParams ...string) *Response {
+ t.Helper()
+
+ if len(queryParams)%2 != 0 {
+ t.Fatal("queryParams must be key-value pairs")
+ }
+
+ // Build query string.
+ values := url.Values{}
+ for i := 0; i < len(queryParams); i += 2 {
+ values.Add(queryParams[i], queryParams[i+1])
+ }
+
+ fullPath := path
+ if len(values) > 0 {
+ fullPath = path + "?" + values.Encode()
+ }
+
+ req := httptest.NewRequest(http.MethodGet, fullPath, nil)
+ return executeRequest(t, api, req)
+}
+
+// POST sends a POST request to the API with the given body and returns a Response with parsed JSON.
+// bodyParams should be pairs of key-value strings for form data.
+func POST(t *testing.T, api *APIWrapper, path string, bodyParams ...string) *Response {
+ t.Helper()
+
+ if len(bodyParams)%2 != 0 {
+ t.Fatal("bodyParams must be key-value pairs")
+ }
+
+ // Build form data.
+ values := url.Values{}
+ for i := 0; i < len(bodyParams); i += 2 {
+ values.Add(bodyParams[i], bodyParams[i+1])
+ }
+
+ req := httptest.NewRequest(http.MethodPost, path, strings.NewReader(values.Encode()))
+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+ return executeRequest(t, api, req)
+}
+
+// executeRequest executes an HTTP request and parses the response as JSON.
+func executeRequest(t *testing.T, api *APIWrapper, req *http.Request) *Response {
+ t.Helper()
+
+ // Capture the request body for validation.
+ var requestBody []byte
+ if req.Body != nil {
+ var err error
+ requestBody, err = io.ReadAll(req.Body)
+ if err != nil {
+ t.Fatalf("failed to read request body: %v", err)
+ }
+ // Restore the body for the actual request.
+ req.Body = io.NopCloser(strings.NewReader(string(requestBody)))
+ }
+
+ recorder := httptest.NewRecorder()
+ api.Handler.ServeHTTP(recorder, req)
+
+ result := recorder.Result()
+ defer result.Body.Close()
+
+ bodyBytes, err := io.ReadAll(result.Body)
+ if err != nil {
+ t.Fatalf("failed to read response body: %v", err)
+ }
+
+ resp := &Response{
+ StatusCode: result.StatusCode,
+ Body: string(bodyBytes),
+ t: t,
+ request: req,
+ requestBody: requestBody,
+ responseHeader: result.Header,
+ }
+
+ // Try to parse as JSON.
+ if result.Header.Get("Content-Type") == "application/json" || strings.Contains(result.Header.Get("Content-Type"), "application/json") {
+ var jsonData map[string]any
+ if err := json.Unmarshal(bodyBytes, &jsonData); err != nil {
+ // If JSON parsing fails, leave JSON as nil.
+ // This allows tests to handle non-JSON responses.
+ resp.JSON = nil
+ } else {
+ resp.JSON = jsonData
+ }
+ }
+
+ return resp
+}
diff --git a/web/api/v1/api.go b/web/api/v1/api.go
index baddedd495..6e61fd19c6 100644
--- a/web/api/v1/api.go
+++ b/web/api/v1/api.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The Prometheus Authors
+// 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
@@ -56,6 +56,7 @@ import (
"github.com/prometheus/prometheus/tsdb"
"github.com/prometheus/prometheus/tsdb/index"
"github.com/prometheus/prometheus/util/annotations"
+ "github.com/prometheus/prometheus/util/features"
"github.com/prometheus/prometheus/util/httputil"
"github.com/prometheus/prometheus/util/notifications"
"github.com/prometheus/prometheus/util/stats"
@@ -255,13 +256,18 @@ type API struct {
otlpWriteHandler http.Handler
codecs []Codec
+
+ featureRegistry features.Collector
+ openAPIBuilder *OpenAPIBuilder
+
+ parser parser.Parser
}
// NewAPI returns an initialized API type.
func NewAPI(
qe promql.QueryEngine,
q storage.SampleAndChunkQueryable,
- ap storage.Appendable,
+ ap storage.Appendable, apV2 storage.AppendableV2,
eq storage.ExemplarQueryable,
spsr func(context.Context) ScrapePoolsRetriever,
tr func(context.Context) TargetRetriever,
@@ -290,11 +296,14 @@ func NewAPI(
rwEnabled bool,
acceptRemoteWriteProtoMsgs remoteapi.MessageTypes,
otlpEnabled, otlpDeltaToCumulative, otlpNativeDeltaIngestion bool,
- ctZeroIngestionEnabled bool,
+ stZeroIngestionEnabled bool,
lookbackDelta time.Duration,
enableTypeAndUnitLabels bool,
appendMetadata bool,
overrideErrorCode OverrideErrorCode,
+ featureRegistry features.Collector,
+ openAPIOptions OpenAPIOptions,
+ promqlParser parser.Parser,
) *API {
a := &API{
QueryEngine: qe,
@@ -324,29 +333,35 @@ func NewAPI(
notificationsGetter: notificationsGetter,
notificationsSub: notificationsSub,
overrideErrorCode: overrideErrorCode,
+ featureRegistry: featureRegistry,
+ openAPIBuilder: NewOpenAPIBuilder(openAPIOptions, logger),
+ parser: promqlParser,
remoteReadHandler: remote.NewReadHandler(logger, registerer, q, configFunc, remoteReadSampleLimit, remoteReadConcurrencyLimit, remoteReadMaxBytesInFrame),
}
+ if a.parser == nil {
+ a.parser = parser.NewParser(parser.Options{})
+ }
+
a.InstallCodec(JSONCodec{})
if statsRenderer != nil {
a.statsRenderer = statsRenderer
}
- if ap == nil && (rwEnabled || otlpEnabled) {
+ if (ap == nil || apV2 == nil) && (rwEnabled || otlpEnabled) {
panic("remote write or otlp write enabled, but no appender passed in.")
}
if rwEnabled {
- a.remoteWriteHandler = remote.NewWriteHandler(logger, registerer, ap, acceptRemoteWriteProtoMsgs, ctZeroIngestionEnabled, enableTypeAndUnitLabels, appendMetadata)
+ a.remoteWriteHandler = remote.NewWriteHandler(logger, registerer, ap, acceptRemoteWriteProtoMsgs, stZeroIngestionEnabled, enableTypeAndUnitLabels, appendMetadata)
}
if otlpEnabled {
- a.otlpWriteHandler = remote.NewOTLPWriteHandler(logger, registerer, ap, configFunc, remote.OTLPOptions{
+ a.otlpWriteHandler = remote.NewOTLPWriteHandler(logger, registerer, apV2, configFunc, remote.OTLPOptions{
ConvertDelta: otlpDeltaToCumulative,
NativeDelta: otlpNativeDeltaIngestion,
LookbackDelta: lookbackDelta,
- IngestCTZeroSample: ctZeroIngestionEnabled,
EnableTypeAndUnitLabels: enableTypeAndUnitLabels,
})
}
@@ -394,7 +409,7 @@ func (api *API) Register(r *route.Router) {
w.WriteHeader(http.StatusNoContent)
})
return api.ready(httputil.CompressionHandler{
- Handler: hf,
+ Handler: api.openAPIBuilder.WrapHandler(hf),
}.ServeHTTP)
}
@@ -444,6 +459,7 @@ func (api *API) Register(r *route.Router) {
r.Get("/status/flags", wrap(api.serveFlags))
r.Get("/status/tsdb", wrapAgent(api.serveTSDBStatus))
r.Get("/status/tsdb/blocks", wrapAgent(api.serveTSDBBlocks))
+ r.Get("/features", wrap(api.features))
r.Get("/status/walreplay", api.serveWALReplayStatus)
r.Get("/notifications", api.notifications)
r.Get("/notifications/live", api.notificationsSSE)
@@ -462,6 +478,9 @@ func (api *API) Register(r *route.Router) {
r.Put("/admin/tsdb/delete_series", wrapAgent(api.deleteSeries))
r.Put("/admin/tsdb/clean_tombstones", wrapAgent(api.cleanTombstones))
r.Put("/admin/tsdb/snapshot", wrapAgent(api.snapshot))
+
+ // OpenAPI endpoint.
+ r.Get("/openapi.yaml", api.ready(api.openAPIBuilder.ServeOpenAPI))
}
type QueryData struct {
@@ -549,8 +568,8 @@ func (api *API) query(r *http.Request) (result apiFuncResult) {
}, nil, warnings, qry.Close}
}
-func (*API) formatQuery(r *http.Request) (result apiFuncResult) {
- expr, err := parser.ParseExpr(r.FormValue("query"))
+func (api *API) formatQuery(r *http.Request) (result apiFuncResult) {
+ expr, err := api.parser.ParseExpr(r.FormValue("query"))
if err != nil {
return invalidParamError(err, "query")
}
@@ -558,8 +577,8 @@ func (*API) formatQuery(r *http.Request) (result apiFuncResult) {
return apiFuncResult{expr.Pretty(0), nil, nil, nil}
}
-func (*API) parseQuery(r *http.Request) apiFuncResult {
- expr, err := parser.ParseExpr(r.FormValue("query"))
+func (api *API) parseQuery(r *http.Request) apiFuncResult {
+ expr, err := api.parser.ParseExpr(r.FormValue("query"))
if err != nil {
return invalidParamError(err, "query")
}
@@ -688,7 +707,7 @@ func (api *API) queryExemplars(r *http.Request) apiFuncResult {
return apiFuncResult{nil, &apiError{errorBadData, err}, nil, nil}
}
- expr, err := parser.ParseExpr(r.FormValue("query"))
+ expr, err := api.parser.ParseExpr(r.FormValue("query"))
if err != nil {
return apiFuncResult{nil, &apiError{errorBadData, err}, nil, nil}
}
@@ -751,7 +770,7 @@ func (api *API) labelNames(r *http.Request) apiFuncResult {
return invalidParamError(err, "end")
}
- matcherSets, err := parseMatchersParam(r.Form["match[]"])
+ matcherSets, err := api.parseMatchersParam(r.Form["match[]"])
if err != nil {
return apiFuncResult{nil, &apiError{errorBadData, err}, nil, nil}
}
@@ -839,7 +858,7 @@ func (api *API) labelValues(r *http.Request) (result apiFuncResult) {
return invalidParamError(err, "end")
}
- matcherSets, err := parseMatchersParam(r.Form["match[]"])
+ matcherSets, err := api.parseMatchersParam(r.Form["match[]"])
if err != nil {
return apiFuncResult{nil, &apiError{errorBadData, err}, nil, nil}
}
@@ -958,7 +977,7 @@ func (api *API) series(r *http.Request) (result apiFuncResult) {
return invalidParamError(err, "end")
}
- matcherSets, err := parseMatchersParam(r.Form["match[]"])
+ matcherSets, err := api.parseMatchersParam(r.Form["match[]"])
if err != nil {
return invalidParamError(err, "match[]")
}
@@ -1253,7 +1272,7 @@ func (api *API) targetMetadata(r *http.Request) apiFuncResult {
var matchers []*labels.Matcher
var err error
if matchTarget != "" {
- matchers, err = parser.ParseMetricSelector(matchTarget)
+ matchers, err = api.parser.ParseMetricSelector(matchTarget)
if err != nil {
return invalidParamError(err, "match_target")
}
@@ -1339,13 +1358,19 @@ func (api *API) targetRelabelSteps(r *http.Request) apiFuncResult {
rules := scrapeConfig.RelabelConfigs
steps := make([]RelabelStep, len(rules))
+ lb := labels.NewBuilder(lbls)
+ keep := true
for i, rule := range rules {
- outLabels, keep := relabel.Process(lbls, rules[:i+1]...)
- steps[i] = RelabelStep{
- Rule: rule,
- Output: outLabels,
- Keep: keep,
+ if keep {
+ keep = relabel.ProcessBuilder(lb, rule)
}
+
+ outLabels := labels.EmptyLabels()
+ if keep {
+ outLabels = lb.Labels()
+ }
+
+ steps[i] = RelabelStep{Rule: rule, Output: outLabels, Keep: keep}
}
return apiFuncResult{&RelabelStepsResponse{Steps: steps}, nil, nil, nil}
@@ -1566,7 +1591,7 @@ func (api *API) rules(r *http.Request) apiFuncResult {
rgSet := queryFormToSet(r.Form["rule_group[]"])
fSet := queryFormToSet(r.Form["file[]"])
- matcherSets, err := parseMatchersParam(r.Form["match[]"])
+ matcherSets, err := api.parseMatchersParam(r.Form["match[]"])
if err != nil {
return apiFuncResult{nil, &apiError{errorBadData, err}, nil, nil}
}
@@ -1788,6 +1813,29 @@ func (api *API) serveFlags(*http.Request) apiFuncResult {
return apiFuncResult{api.flagsMap, nil, nil, nil}
}
+// featuresData wraps feature flags data to provide custom JSON marshaling without HTML escaping.
+// featuresData does not contain user-provided input, and it is more convenient to have unescaped
+// representation of PromQL operators like >=.
+type featuresData struct {
+ data map[string]map[string]bool
+}
+
+func (f featuresData) MarshalJSON() ([]byte, error) {
+ json := jsoniter.Config{
+ EscapeHTML: false,
+ SortMapKeys: true,
+ ValidateJsonRawMessage: true,
+ }.Froze()
+ return json.Marshal(f.data)
+}
+
+func (api *API) features(*http.Request) apiFuncResult {
+ if api.featureRegistry == nil {
+ return apiFuncResult{nil, &apiError{errorInternal, errors.New("feature registry not configured")}, nil, nil}
+ }
+ return apiFuncResult{featuresData{data: api.featureRegistry.Get()}, nil, nil, nil}
+}
+
// TSDBStat holds the information about individual cardinality.
type TSDBStat struct {
Name string `json:"name"`
@@ -1836,12 +1884,16 @@ func (api *API) serveTSDBBlocks(*http.Request) apiFuncResult {
}
func (api *API) serveTSDBStatus(r *http.Request) apiFuncResult {
+ const maxTSDBLimit = 10000
limit := 10
if s := r.FormValue("limit"); s != "" {
var err error
if limit, err = strconv.Atoi(s); err != nil || limit < 1 {
return apiFuncResult{nil, &apiError{errorBadData, errors.New("limit must be a positive number")}, nil, nil}
}
+ if limit > maxTSDBLimit {
+ return apiFuncResult{nil, &apiError{errorBadData, fmt.Errorf("limit must not exceed %d", maxTSDBLimit)}, nil, nil}
+ }
}
s, err := api.db.Stats(labels.MetricName, limit)
if err != nil {
@@ -1992,7 +2044,7 @@ func (api *API) deleteSeries(r *http.Request) apiFuncResult {
}
for _, s := range r.Form["match[]"] {
- matchers, err := parser.ParseMetricSelector(s)
+ matchers, err := api.parser.ParseMetricSelector(s)
if err != nil {
return invalidParamError(err, "match[]")
}
@@ -2201,8 +2253,8 @@ func parseDuration(s string) (time.Duration, error) {
return 0, fmt.Errorf("cannot parse %q to a valid duration", s)
}
-func parseMatchersParam(matchers []string) ([][]*labels.Matcher, error) {
- matcherSets, err := parser.ParseMetricSelectors(matchers)
+func (api *API) parseMatchersParam(matchers []string) ([][]*labels.Matcher, error) {
+ matcherSets, err := api.parser.ParseMetricSelectors(matchers)
if err != nil {
return nil, err
}
diff --git a/web/api/v1/api_scenarios_test.go b/web/api/v1/api_scenarios_test.go
new file mode 100644
index 0000000000..5bdccf08d5
--- /dev/null
+++ b/web/api/v1/api_scenarios_test.go
@@ -0,0 +1,510 @@
+// 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 v1
+
+import (
+ "strconv"
+ "testing"
+ "time"
+
+ "github.com/prometheus/prometheus/storage"
+ "github.com/prometheus/prometheus/web/api/testhelpers"
+)
+
+// TODO: Generate automated tests from OpenAPI spec to validate API responses.
+
+// TestAPIEmpty tests the API with no metrics and no rules.
+func TestAPIEmpty(t *testing.T) {
+ // Create an API with empty defaults (no series, no rules).
+ api := newTestAPI(t, testhelpers.APIConfig{})
+
+ t.Run("GET /api/v1/labels returns success with empty array", func(t *testing.T) {
+ testhelpers.GET(t, api, "/api/v1/labels").
+ RequireSuccess().
+ ValidateOpenAPI().
+ RequireJSONArray("$.data")
+ })
+
+ t.Run("GET /api/v1/query?query=up returns success (empty result ok)", func(t *testing.T) {
+ testhelpers.GET(t, api, "/api/v1/query", "query", "up").
+ ValidateOpenAPI().
+ RequireSuccess().
+ RequireEquals("$.data.resultType", "vector")
+ })
+
+ t.Run("GET /api/v1/query_range?query=up returns success", func(t *testing.T) {
+ testhelpers.GET(t, api, "/api/v1/query_range",
+ "query", "up",
+ "start", "0",
+ "end", "100",
+ "step", "10").
+ RequireSuccess().
+ ValidateOpenAPI().
+ RequireEquals("$.data.resultType", "matrix")
+ })
+
+ t.Run("GET /api/v1/series returns success with empty result", func(t *testing.T) {
+ testhelpers.GET(t, api, "/api/v1/series",
+ "match[]", "up",
+ "start", "0",
+ "end", "100").
+ RequireSuccess().
+ ValidateOpenAPI().
+ RequireJSONArray("$.data")
+ })
+
+ t.Run("GET /api/v1/label/__name__/values returns success with empty array", func(t *testing.T) {
+ testhelpers.GET(t, api, "/api/v1/label/__name__/values").
+ RequireSuccess().
+ ValidateOpenAPI().
+ RequireJSONArray("$.data")
+ })
+
+ t.Run("GET /api/v1/targets returns success", func(t *testing.T) {
+ testhelpers.GET(t, api, "/api/v1/targets").
+ RequireSuccess().
+ RequireJSONPathExists("$.data.activeTargets")
+ })
+
+ t.Run("GET /api/v1/rules returns success with empty groups", func(t *testing.T) {
+ testhelpers.GET(t, api, "/api/v1/rules").
+ RequireSuccess().
+ ValidateOpenAPI().
+ RequireJSONPathExists("$.data.groups")
+ })
+
+ t.Run("GET /api/v1/alerts returns success with empty alerts", func(t *testing.T) {
+ testhelpers.GET(t, api, "/api/v1/alerts").
+ RequireSuccess().
+ ValidateOpenAPI().
+ RequireJSONPathExists("$.data.alerts")
+ })
+
+ t.Run("GET /api/v1/alertmanagers returns success", func(t *testing.T) {
+ testhelpers.GET(t, api, "/api/v1/alertmanagers").
+ RequireSuccess().
+ ValidateOpenAPI().
+ RequireJSONPathExists("$.data.activeAlertmanagers")
+ })
+
+ t.Run("GET /api/v1/metadata returns success", func(t *testing.T) {
+ testhelpers.GET(t, api, "/api/v1/metadata").
+ RequireSuccess().
+ ValidateOpenAPI().
+ RequireJSONPathExists("$.data")
+ })
+
+ t.Run("GET /api/v1/status/config returns success", func(t *testing.T) {
+ testhelpers.GET(t, api, "/api/v1/status/config").
+ RequireSuccess().
+ ValidateOpenAPI().
+ RequireJSONPathExists("$.data.yaml")
+ })
+
+ t.Run("GET /api/v1/status/flags returns success", func(t *testing.T) {
+ testhelpers.GET(t, api, "/api/v1/status/flags").
+ RequireSuccess().
+ ValidateOpenAPI().
+ RequireJSONPathExists("$.data")
+ })
+
+ t.Run("GET /api/v1/status/runtimeinfo returns success", func(t *testing.T) {
+ testhelpers.GET(t, api, "/api/v1/status/runtimeinfo").
+ RequireSuccess().
+ ValidateOpenAPI().
+ RequireJSONPathExists("$.data")
+ })
+
+ t.Run("GET /api/v1/status/buildinfo returns success", func(t *testing.T) {
+ testhelpers.GET(t, api, "/api/v1/status/buildinfo").
+ RequireSuccess().
+ ValidateOpenAPI().
+ RequireJSONPathExists("$.data")
+ })
+
+ t.Run("POST /api/v1/query with form data returns success", func(t *testing.T) {
+ testhelpers.POST(t, api, "/api/v1/query", "query", "up").
+ RequireSuccess().
+ ValidateOpenAPI().
+ RequireEquals("$.data.resultType", "vector")
+ })
+}
+
+// TestAPIWithSeries tests the API with metrics/series data.
+func TestAPIWithSeries(t *testing.T) {
+ // Create an API with sample series data.
+ api := newTestAPI(t, testhelpers.APIConfig{
+ Queryable: testhelpers.NewLazyLoader(func() storage.SampleAndChunkQueryable {
+ return testhelpers.NewQueryableWithSeries(testhelpers.FixtureMultipleSeries())
+ }),
+ })
+
+ t.Run("GET /api/v1/query returns vector with >= 1 sample", func(t *testing.T) {
+ testhelpers.GET(t, api, "/api/v1/query", "query", "up").
+ RequireSuccess().
+ ValidateOpenAPI().
+ RequireEquals("$.data.resultType", "vector").
+ RequireLenAtLeast("$.data.result", 1)
+ })
+
+ t.Run("GET /api/v1/query_range returns matrix result type", func(t *testing.T) {
+ // Use relative timestamps to match our fixtures.
+ now := time.Now().Unix()
+ testhelpers.GET(t, api, "/api/v1/query_range",
+ "query", "up",
+ "start", strconv.FormatInt(now-120, 10),
+ "end", strconv.FormatInt(now, 10),
+ "step", "60").
+ RequireSuccess().
+ ValidateOpenAPI().
+ RequireEquals("$.data.resultType", "matrix")
+ // Note: Result may be empty if timestamps don't align perfectly with samples.
+ })
+
+ t.Run("GET /api/v1/labels returns non-empty array", func(t *testing.T) {
+ testhelpers.GET(t, api, "/api/v1/labels").
+ RequireSuccess().
+ ValidateOpenAPI().
+ RequireJSONArray("$.data").
+ RequireLenAtLeast("$.data", 1)
+ })
+
+ t.Run("GET /api/v1/label/__name__/values contains expected metric names", func(t *testing.T) {
+ testhelpers.GET(t, api, "/api/v1/label/__name__/values").
+ RequireSuccess().
+ ValidateOpenAPI().
+ RequireArrayContains("$.data", "up").
+ RequireArrayContains("$.data", "http_requests_total")
+ })
+
+ t.Run("GET /api/v1/label/job/values contains expected jobs", func(t *testing.T) {
+ testhelpers.GET(t, api, "/api/v1/label/job/values").
+ RequireSuccess().
+ ValidateOpenAPI().
+ RequireJSONArray("$.data").
+ RequireArrayContains("$.data", "prometheus").
+ RequireArrayContains("$.data", "node").
+ RequireArrayContains("$.data", "api")
+ })
+
+ t.Run("GET /api/v1/series with match returns results", func(t *testing.T) {
+ testhelpers.GET(t, api, "/api/v1/series",
+ "match[]", "up",
+ "start", "0",
+ "end", "120").
+ RequireSuccess().
+ ValidateOpenAPI().
+ RequireJSONArray("$.data").
+ RequireLenAtLeast("$.data", 1)
+ })
+
+ t.Run("GET /api/v1/query with specific job returns filtered results", func(t *testing.T) {
+ testhelpers.GET(t, api, "/api/v1/query", "query", `up{job="prometheus"}`).
+ RequireSuccess().
+ ValidateOpenAPI().
+ RequireEquals("$.data.resultType", "vector").
+ RequireLenAtLeast("$.data.result", 1)
+ })
+
+ t.Run("GET /api/v1/query with aggregation returns result", func(t *testing.T) {
+ testhelpers.GET(t, api, "/api/v1/query", "query", "sum(up)").
+ RequireSuccess().
+ ValidateOpenAPI().
+ RequireEquals("$.data.resultType", "vector")
+ })
+
+ t.Run("POST /api/v1/query returns vector with data", func(t *testing.T) {
+ testhelpers.POST(t, api, "/api/v1/query", "query", "up").
+ RequireSuccess().
+ ValidateOpenAPI().
+ RequireEquals("$.data.resultType", "vector").
+ RequireLenAtLeast("$.data.result", 1)
+ })
+}
+
+// TestAPIWithRules tests the API with rules configured.
+func TestAPIWithRules(t *testing.T) {
+ // Create an API with rule groups.
+ api := newTestAPI(t, testhelpers.APIConfig{
+ RulesRetriever: testhelpers.NewLazyLoader(func() testhelpers.RulesRetriever {
+ return testhelpers.NewRulesRetrieverWithGroups(testhelpers.FixtureRuleGroups())
+ }),
+ })
+
+ t.Run("GET /api/v1/rules returns groups with rules", func(t *testing.T) {
+ testhelpers.GET(t, api, "/api/v1/rules").
+ RequireSuccess().
+ ValidateOpenAPI().
+ RequireJSONPathExists("$.data.groups").
+ RequireLenAtLeast("$.data.groups", 1).
+ RequireSome("$.data.groups", func(group any) bool {
+ if g, ok := group.(map[string]any); ok {
+ return g["name"] == "example"
+ }
+ return false
+ }).
+ RequireSome("$.data.groups", func(group any) bool {
+ if g, ok := group.(map[string]any); ok {
+ if g["name"] == "example" {
+ // Check that the group has rules.
+ if rules, ok := g["rules"].([]any); ok {
+ return len(rules) > 0
+ }
+ }
+ }
+ return false
+ })
+ })
+
+ t.Run("GET /api/v1/alerts returns alerts array", func(t *testing.T) {
+ testhelpers.GET(t, api, "/api/v1/alerts").
+ RequireSuccess().
+ ValidateOpenAPI().
+ RequireJSONPathExists("$.data.alerts").
+ RequireJSONArray("$.data.alerts")
+ })
+
+ t.Run("GET /api/v1/rules with rule_name filter", func(t *testing.T) {
+ testhelpers.GET(t, api, "/api/v1/rules", "rule_name[]", "InstanceDown").
+ RequireSuccess().
+ ValidateOpenAPI().
+ RequireJSONPathExists("$.data.groups")
+ })
+}
+
+// TestAPITSDBNotReady tests the API when TSDB is not ready (e.g., during WAL replay).
+// TSDB not ready errors are converted to errorUnavailable by setUnavailStatusOnTSDBNotReady,
+// which returns HTTP 500 Internal Server Error (the default for errorUnavailable).
+func TestAPITSDBNotReady(t *testing.T) {
+ // Create an API with a queryable that returns tsdb.ErrNotReady.
+ api := newTestAPI(t, testhelpers.APIConfig{
+ Queryable: testhelpers.NewLazyLoader(testhelpers.NewTSDBNotReadyQueryable),
+ })
+
+ t.Run("GET /api/v1/query returns 500 when TSDB not ready", func(t *testing.T) {
+ testhelpers.GET(t, api, "/api/v1/query", "query", "up").
+ RequireStatusCode(500).
+ ValidateOpenAPI().
+ RequireError()
+ })
+
+ t.Run("POST /api/v1/query returns 500 when TSDB not ready", func(t *testing.T) {
+ testhelpers.POST(t, api, "/api/v1/query", "query", "up").
+ RequireStatusCode(500).
+ ValidateOpenAPI().
+ RequireError()
+ })
+
+ t.Run("GET /api/v1/query_range returns 500 when TSDB not ready", func(t *testing.T) {
+ testhelpers.GET(t, api, "/api/v1/query_range",
+ "query", "up",
+ "start", "0",
+ "end", "100",
+ "step", "10").
+ RequireStatusCode(500).
+ ValidateOpenAPI().
+ RequireError()
+ })
+
+ t.Run("GET /api/v1/series returns 500 when TSDB not ready", func(t *testing.T) {
+ testhelpers.GET(t, api, "/api/v1/series",
+ "match[]", "up",
+ "start", "0",
+ "end", "100").
+ RequireStatusCode(500).
+ ValidateOpenAPI().
+ RequireError()
+ })
+
+ t.Run("GET /api/v1/labels returns 500 when TSDB not ready", func(t *testing.T) {
+ testhelpers.GET(t, api, "/api/v1/labels").
+ RequireStatusCode(500).
+ ValidateOpenAPI().
+ RequireError()
+ })
+
+ t.Run("GET /api/v1/label/{name}/values returns 500 when TSDB not ready", func(t *testing.T) {
+ testhelpers.GET(t, api, "/api/v1/label/__name__/values").
+ RequireStatusCode(500).
+ ValidateOpenAPI().
+ RequireError()
+ })
+}
+
+// TestAPIWithNativeHistograms tests the API with native histogram data.
+func TestAPIWithNativeHistograms(t *testing.T) {
+ // Create an API with histogram series data.
+ api := newTestAPI(t, testhelpers.APIConfig{
+ Queryable: testhelpers.NewLazyLoader(func() storage.SampleAndChunkQueryable {
+ return testhelpers.NewQueryableWithSeries(testhelpers.FixtureHistogramSeries())
+ }),
+ })
+
+ t.Run("GET /api/v1/query returns vector with native histogram", func(t *testing.T) {
+ testhelpers.GET(t, api, "/api/v1/query", "query", "test_histogram").
+ RequireSuccess().
+ ValidateOpenAPI().
+ RequireEquals("$.data.resultType", "vector").
+ RequireLenAtLeast("$.data.result", 1).
+ RequireSome("$.data.result", func(item any) bool {
+ sample, ok := item.(map[string]any)
+ if !ok {
+ return false
+ }
+ // Check that the sample has a histogram field (not a value field).
+ _, hasHistogram := sample["histogram"]
+ return hasHistogram
+ })
+ })
+
+ t.Run("POST /api/v1/query returns vector with native histogram", func(t *testing.T) {
+ testhelpers.POST(t, api, "/api/v1/query", "query", "test_histogram").
+ RequireSuccess().
+ ValidateOpenAPI().
+ RequireEquals("$.data.resultType", "vector").
+ RequireLenAtLeast("$.data.result", 1).
+ RequireSome("$.data.result", func(item any) bool {
+ sample, ok := item.(map[string]any)
+ if !ok {
+ return false
+ }
+ // Check that the sample has a histogram field (not a value field).
+ _, hasHistogram := sample["histogram"]
+ return hasHistogram
+ })
+ })
+
+ t.Run("GET /api/v1/query_range returns matrix with native histogram", func(t *testing.T) {
+ // Use relative timestamps to match our fixtures.
+ now := time.Now().Unix()
+ testhelpers.GET(t, api, "/api/v1/query_range",
+ "query", "test_histogram",
+ "start", strconv.FormatInt(now-120, 10),
+ "end", strconv.FormatInt(now, 10),
+ "step", "60").
+ RequireSuccess().
+ ValidateOpenAPI().
+ RequireEquals("$.data.resultType", "matrix")
+ })
+
+ t.Run("GET /api/v1/query with histogram selector", func(t *testing.T) {
+ testhelpers.GET(t, api, "/api/v1/query", "query", `test_histogram{job="prometheus"}`).
+ RequireSuccess().
+ ValidateOpenAPI().
+ RequireEquals("$.data.resultType", "vector").
+ RequireLenAtLeast("$.data.result", 1)
+ })
+
+ t.Run("GET /api/v1/series returns histogram metric series", func(t *testing.T) {
+ testhelpers.GET(t, api, "/api/v1/series",
+ "match[]", "test_histogram",
+ "start", "0",
+ "end", strconv.FormatInt(time.Now().Unix(), 10)).
+ RequireSuccess().
+ ValidateOpenAPI().
+ RequireJSONArray("$.data").
+ RequireLenAtLeast("$.data", 1)
+ })
+}
+
+// TestAPIWithStats tests the API with the stats query parameter.
+func TestAPIWithStats(t *testing.T) {
+ // Create an API with sample series data.
+ api := newTestAPI(t, testhelpers.APIConfig{
+ Queryable: testhelpers.NewLazyLoader(func() storage.SampleAndChunkQueryable {
+ return testhelpers.NewQueryableWithSeries(testhelpers.FixtureMultipleSeries())
+ }),
+ })
+
+ now := time.Now().Unix()
+
+ // Test combinations of methods, endpoints, and stats values.
+ methods := []string{"GET", "POST"}
+ statsValues := []struct {
+ value string
+ expectStats bool
+ }{
+ {"true", true},
+ {"all", true},
+ {"1", true},
+ {"", false},
+ }
+
+ for _, method := range methods {
+ for _, stats := range statsValues {
+ t.Run(method+" /api/v1/query with stats="+stats.value, func(t *testing.T) {
+ var params []string
+ if stats.value != "" {
+ params = []string{"query", "up", "stats", stats.value}
+ } else {
+ params = []string{"query", "up"}
+ }
+
+ var resp *testhelpers.Response
+ if method == "GET" {
+ resp = testhelpers.GET(t, api, "/api/v1/query", params...)
+ } else {
+ resp = testhelpers.POST(t, api, "/api/v1/query", params...)
+ }
+
+ resp.RequireSuccess().ValidateOpenAPI()
+
+ if stats.expectStats {
+ resp.RequireJSONPathExists("$.data.stats").
+ RequireJSONPathExists("$.data.stats.timings").
+ RequireJSONPathExists("$.data.stats.samples")
+ } else {
+ resp.RequireJSONPathNotExists("$.data.stats")
+ }
+ })
+
+ t.Run(method+" /api/v1/query_range with stats="+stats.value, func(t *testing.T) {
+ var params []string
+ if stats.value != "" {
+ params = []string{
+ "query", "up",
+ "start", strconv.FormatInt(now-120, 10),
+ "end", strconv.FormatInt(now, 10),
+ "step", "60",
+ "stats", stats.value,
+ }
+ } else {
+ params = []string{
+ "query", "up",
+ "start", strconv.FormatInt(now-120, 10),
+ "end", strconv.FormatInt(now, 10),
+ "step", "60",
+ }
+ }
+
+ var resp *testhelpers.Response
+ if method == "GET" {
+ resp = testhelpers.GET(t, api, "/api/v1/query_range", params...)
+ } else {
+ resp = testhelpers.POST(t, api, "/api/v1/query_range", params...)
+ }
+
+ resp.RequireSuccess().ValidateOpenAPI()
+
+ if stats.expectStats {
+ resp.RequireJSONPathExists("$.data.stats").
+ RequireJSONPathExists("$.data.stats.timings").
+ RequireJSONPathExists("$.data.stats.samples")
+ } else {
+ resp.RequireJSONPathNotExists("$.data.stats")
+ }
+ })
+ }
+ }
+}
diff --git a/web/api/v1/api_test.go b/web/api/v1/api_test.go
index 8e0adc0802..1fdb7ab645 100644
--- a/web/api/v1/api_test.go
+++ b/web/api/v1/api_test.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The Prometheus Authors
+// 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
@@ -63,6 +63,8 @@ import (
"github.com/prometheus/prometheus/util/testutil"
)
+var testParser = parser.NewParser(parser.Options{})
+
func testEngine(t *testing.T) *promql.Engine {
t.Helper()
return promqltest.NewTestEngineWithOpts(t, promql.EngineOpts{
@@ -166,8 +168,8 @@ func (t testTargetRetriever) TargetsDroppedCounts() map[string]int {
return r
}
-func (testTargetRetriever) ScrapePoolConfig(_ string) (*config.ScrapeConfig, error) {
- return &config.ScrapeConfig{
+func (testTargetRetriever) ScrapePoolConfig(pool string) (*config.ScrapeConfig, error) {
+ cfg := &config.ScrapeConfig{
RelabelConfigs: []*relabel.Config{
{
Action: relabel.Replace,
@@ -182,20 +184,26 @@ func (testTargetRetriever) ScrapePoolConfig(_ string) (*config.ScrapeConfig, err
Regex: relabel.MustNewRegexp(`example\.com:.*`),
},
},
- }, nil
+ }
+ if pool == "testpool3" {
+ cfg.RelabelConfigs = append(cfg.RelabelConfigs, &relabel.Config{
+ Action: relabel.Replace,
+ TargetLabel: "job",
+ Regex: relabel.MustNewRegexp(".*"),
+ Replacement: "should_not_apply",
+ })
+ }
+ return cfg, nil
}
func (t *testTargetRetriever) SetMetadataStoreForTargets(identifier string, metadata scrape.MetricMetadataStore) error {
targets, ok := t.activeTargets[identifier]
-
if !ok {
- return errors.New("targets not found")
+ return fmt.Errorf("no active target for %v", identifier)
}
-
for _, at := range targets {
at.SetMetadataStore(metadata)
}
-
return nil
}
@@ -244,11 +252,11 @@ type rulesRetrieverMock struct {
}
func (m *rulesRetrieverMock) CreateAlertingRules() {
- expr1, err := parser.ParseExpr(`absent(test_metric3) != 1`)
+ expr1, err := testParser.ParseExpr(`absent(test_metric3) != 1`)
require.NoError(m.testing, err)
- expr2, err := parser.ParseExpr(`up == 1`)
+ expr2, err := testParser.ParseExpr(`up == 1`)
require.NoError(m.testing, err)
- expr3, err := parser.ParseExpr(`vector(1)`)
+ expr3, err := testParser.ParseExpr(`vector(1)`)
require.NoError(m.testing, err)
rule1 := rules.NewAlertingRule(
@@ -323,8 +331,8 @@ func (m *rulesRetrieverMock) CreateAlertingRules() {
func (m *rulesRetrieverMock) CreateRuleGroups() {
m.CreateAlertingRules()
arules := m.AlertingRules()
- storage := teststorage.New(m.testing)
- defer storage.Close()
+ // Create separate storage for recordings to not pollute the main one.
+ s := teststorage.New(m.testing)
engineOpts := promql.EngineOpts{
Logger: nil,
@@ -334,8 +342,8 @@ func (m *rulesRetrieverMock) CreateRuleGroups() {
}
engine := promqltest.NewTestEngineWithOpts(m.testing, engineOpts)
opts := &rules.ManagerOptions{
- QueryFunc: rules.EngineQueryFunc(engine, storage),
- Appendable: storage,
+ QueryFunc: rules.EngineQueryFunc(engine, s),
+ Appendable: s,
Context: context.Background(),
Logger: promslog.NewNopLogger(),
NotifyFunc: func(context.Context, string, ...*rules.Alert) {},
@@ -347,7 +355,7 @@ func (m *rulesRetrieverMock) CreateRuleGroups() {
r = append(r, alertrule)
}
- recordingExpr, err := parser.ParseExpr(`vector(1)`)
+ recordingExpr, err := testParser.ParseExpr(`vector(1)`)
require.NoError(m.testing, err, "unable to parse alert expression")
recordingRule := rules.NewRecordingRule("recording-rule-1", recordingExpr, labels.Labels{})
recordingRule2 := rules.NewRecordingRule("recording-rule-2", recordingExpr, labels.FromStrings("testlabel", "rule"))
@@ -400,8 +408,23 @@ var sampleFlagMap = map[string]string{
"flag2": "value2",
}
+func appendExemplars(t testing.TB, s storage.Storage, ex []exemplar.QueryResult) {
+ t.Helper()
+
+ // TODO(bwplotka): Use AppenderV2.AppendExemplar per series flow
+ // once its implemented: https://github.com/prometheus/prometheus/issues/17632#issuecomment-3759315095
+ app := s.Appender(t.Context())
+ for _, ed := range ex {
+ for _, e := range ed.Exemplars {
+ _, err := app.AppendExemplar(0, ed.SeriesLabels, e)
+ require.NoError(t, err)
+ }
+ }
+ require.NoError(t, app.Commit())
+}
+
func TestEndpoints(t *testing.T) {
- storage := promqltest.LoadedStorage(t, `
+ s := promqltest.LoadedStorage(t, `
load 1m
test_metric1{foo="bar"} 0+100x100
test_metric1{foo="boo"} 1+0x100
@@ -414,8 +437,8 @@ func TestEndpoints(t *testing.T) {
test_metric5{"host.name"="localhost"} 1+0x100
test_metric5{"junk\n{},=: chars"="bar"} 1+0x100
`)
- t.Cleanup(func() { storage.Close() })
+ // Add exemplar testdata here, given promqltest does not support exemplars.
start := time.Unix(0, 0)
exemplars := []exemplar.QueryResult{
{
@@ -459,15 +482,10 @@ func TestEndpoints(t *testing.T) {
},
},
}
- for _, ed := range exemplars {
- _, err := storage.AppendExemplar(0, ed.SeriesLabels, ed.Exemplars[0])
- require.NoError(t, err, "failed to add exemplar: %+v", ed.Exemplars[0])
- }
+ appendExemplars(t, s, exemplars)
now := time.Now()
-
ng := testEngine(t)
-
t.Run("local", func(t *testing.T) {
algr := rulesRetrieverMock{testing: t}
@@ -480,9 +498,9 @@ func TestEndpoints(t *testing.T) {
testTargetRetriever := setupTestTargetRetriever(t)
api := &API{
- Queryable: storage,
+ Queryable: s,
QueryEngine: ng,
- ExemplarQueryable: storage.ExemplarQueryable(),
+ ExemplarQueryable: s,
targetRetriever: testTargetRetriever.toFactory(),
alertmanagerRetriever: testAlertmanagerRetriever{}.toFactory(),
flagsMap: sampleFlagMap,
@@ -490,15 +508,16 @@ func TestEndpoints(t *testing.T) {
config: func() config.Config { return samplePrometheusCfg },
ready: func(f http.HandlerFunc) http.HandlerFunc { return f },
rulesRetriever: algr.toFactory(),
+ parser: testParser,
}
- testEndpoints(t, api, testTargetRetriever, storage, true)
+ testEndpoints(t, api, testTargetRetriever, true)
})
// Run all the API tests against an API that is wired to forward queries via
// the remote read client to a test server, which in turn sends them to the
// data from the test storage.
t.Run("remote", func(t *testing.T) {
- server := setupRemote(storage)
+ server := setupRemote(s)
defer server.Close()
u, err := url.Parse(server.URL)
@@ -520,6 +539,7 @@ func TestEndpoints(t *testing.T) {
remote := remote.NewStorage(promslog.New(&promslogConfig), prometheus.DefaultRegisterer, func() (int64, error) {
return 0, nil
}, dbDir, 1*time.Second, nil, false)
+ t.Cleanup(func() { _ = remote.Close() })
err = remote.ApplyConfig(&config.Config{
RemoteReadConfigs: []*config.RemoteReadConfig{
@@ -545,7 +565,7 @@ func TestEndpoints(t *testing.T) {
api := &API{
Queryable: remote,
QueryEngine: ng,
- ExemplarQueryable: storage.ExemplarQueryable(),
+ ExemplarQueryable: s,
targetRetriever: testTargetRetriever.toFactory(),
alertmanagerRetriever: testAlertmanagerRetriever{}.toFactory(),
flagsMap: sampleFlagMap,
@@ -553,8 +573,9 @@ func TestEndpoints(t *testing.T) {
config: func() config.Config { return samplePrometheusCfg },
ready: func(f http.HandlerFunc) http.HandlerFunc { return f },
rulesRetriever: algr.toFactory(),
+ parser: testParser,
}
- testEndpoints(t, api, testTargetRetriever, storage, false)
+ testEndpoints(t, api, testTargetRetriever, false)
})
}
@@ -567,7 +588,7 @@ func (b byLabels) Less(i, j int) bool { return labels.Compare(b[i], b[j]) < 0 }
func TestGetSeries(t *testing.T) {
// TestEndpoints doesn't have enough label names to test api.labelNames
// endpoint properly. Hence we test it separately.
- storage := promqltest.LoadedStorage(t, `
+ s := promqltest.LoadedStorage(t, `
load 1m
test_metric1{foo1="bar", baz="abc"} 0+100x100
test_metric1{foo2="boo"} 1+0x100
@@ -575,9 +596,10 @@ func TestGetSeries(t *testing.T) {
test_metric2{foo="boo", xyz="qwerty"} 1+0x100
test_metric2{foo="baz", abc="qwerty"} 1+0x100
`)
- t.Cleanup(func() { storage.Close() })
+
api := &API{
- Queryable: storage,
+ Queryable: s,
+ parser: testParser,
}
request := func(method string, matchers ...string) (*http.Request, error) {
u, err := url.Parse("http://example.com")
@@ -642,6 +664,7 @@ func TestGetSeries(t *testing.T) {
expectedErrorType: errorExec,
api: &API{
Queryable: errorTestQueryable{err: errors.New("generic")},
+ parser: testParser,
},
},
{
@@ -650,6 +673,7 @@ func TestGetSeries(t *testing.T) {
expectedErrorType: errorInternal,
api: &API{
Queryable: errorTestQueryable{err: promql.ErrStorage{Err: errors.New("generic")}},
+ parser: testParser,
},
},
} {
@@ -671,7 +695,7 @@ func TestGetSeries(t *testing.T) {
func TestQueryExemplars(t *testing.T) {
start := time.Unix(0, 0)
- storage := promqltest.LoadedStorage(t, `
+ s := promqltest.LoadedStorage(t, `
load 1m
test_metric1{foo="bar"} 0+100x100
test_metric1{foo="boo"} 1+0x100
@@ -682,12 +706,12 @@ func TestQueryExemplars(t *testing.T) {
test_metric4{foo="boo", dup="1"} 1+0x100
test_metric4{foo="boo"} 1+0x100
`)
- t.Cleanup(func() { storage.Close() })
api := &API{
- Queryable: storage,
+ Queryable: s,
QueryEngine: testEngine(t),
- ExemplarQueryable: storage.ExemplarQueryable(),
+ ExemplarQueryable: s,
+ parser: testParser,
}
request := func(method string, qs url.Values) (*http.Request, error) {
@@ -744,6 +768,7 @@ func TestQueryExemplars(t *testing.T) {
expectedErrorType: errorExec,
api: &API{
ExemplarQueryable: errorTestQueryable{err: errors.New("generic")},
+ parser: testParser,
},
query: url.Values{
"query": []string{`test_metric3{foo="boo"} - test_metric4{foo="bar"}`},
@@ -756,6 +781,7 @@ func TestQueryExemplars(t *testing.T) {
expectedErrorType: errorInternal,
api: &API{
ExemplarQueryable: errorTestQueryable{err: promql.ErrStorage{Err: errors.New("generic")}},
+ parser: testParser,
},
query: url.Values{
"query": []string{`test_metric3{foo="boo"} - test_metric4{foo="bar"}`},
@@ -765,15 +791,10 @@ func TestQueryExemplars(t *testing.T) {
},
} {
t.Run(tc.name, func(t *testing.T) {
- es := storage
+ es := s
ctx := context.Background()
- for _, te := range tc.exemplars {
- for _, e := range te.Exemplars {
- _, err := es.AppendExemplar(0, te.SeriesLabels, e)
- require.NoError(t, err)
- }
- }
+ appendExemplars(t, es, tc.exemplars)
req, err := request(http.MethodGet, tc.query)
require.NoError(t, err)
@@ -790,7 +811,7 @@ func TestQueryExemplars(t *testing.T) {
func TestLabelNames(t *testing.T) {
// TestEndpoints doesn't have enough label names to test api.labelNames
// endpoint properly. Hence we test it separately.
- storage := promqltest.LoadedStorage(t, `
+ s := promqltest.LoadedStorage(t, `
load 1m
test_metric1{foo1="bar", baz="abc"} 0+100x100
test_metric1{foo2="boo"} 1+0x100
@@ -798,9 +819,10 @@ func TestLabelNames(t *testing.T) {
test_metric2{foo="boo", xyz="qwerty"} 1+0x100
test_metric2{foo="baz", abc="qwerty"} 1+0x100
`)
- t.Cleanup(func() { storage.Close() })
+
api := &API{
- Queryable: storage,
+ Queryable: s,
+ parser: testParser,
}
request := func(method, limit string, matchers ...string) (*http.Request, error) {
u, err := url.Parse("http://example.com")
@@ -865,6 +887,7 @@ func TestLabelNames(t *testing.T) {
expectedErrorType: errorExec,
api: &API{
Queryable: errorTestQueryable{err: errors.New("generic")},
+ parser: testParser,
},
},
{
@@ -873,6 +896,7 @@ func TestLabelNames(t *testing.T) {
expectedErrorType: errorInternal,
api: &API{
Queryable: errorTestQueryable{err: promql.ErrStorage{Err: errors.New("generic")}},
+ parser: testParser,
},
},
} {
@@ -900,12 +924,12 @@ func (testStats) Builtin() (_ stats.BuiltinStats) {
}
func TestStats(t *testing.T) {
- storage := teststorage.New(t)
- t.Cleanup(func() { storage.Close() })
+ s := teststorage.New(t)
api := &API{
- Queryable: storage,
+ Queryable: s,
QueryEngine: testEngine(t),
+ parser: testParser,
now: func() time.Time {
return time.Unix(123, 0)
},
@@ -1119,7 +1143,7 @@ func setupRemote(s storage.Storage) *httptest.Server {
return httptest.NewServer(handler)
}
-func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, es storage.ExemplarStorage, testLabelAPI bool) {
+func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, testLabelAPI bool) {
start := time.Unix(0, 0)
type targetMetadata struct {
@@ -1139,7 +1163,6 @@ func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, es storage.E
errType errorType
sorter func(any)
metadata []targetMetadata
- exemplars []exemplar.QueryResult
zeroFunc func(any)
}
@@ -1937,6 +1960,47 @@ func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, es storage.E
},
},
},
+ {
+ endpoint: api.targetRelabelSteps,
+ query: url.Values{"scrapePool": []string{"testpool3"}, "labels": []string{`{"job":"test","__address__":"localhost:9090"}`}},
+ response: &RelabelStepsResponse{
+ Steps: []RelabelStep{
+ {
+ Rule: &relabel.Config{
+ Action: relabel.Replace,
+ Replacement: "example.com:443",
+ TargetLabel: "__address__",
+ Regex: relabel.MustNewRegexp(""),
+ NameValidationScheme: model.LegacyValidation,
+ },
+ Output: labels.FromMap(map[string]string{
+ "job": "test",
+ "__address__": "example.com:443",
+ }),
+ Keep: true,
+ },
+ {
+ Rule: &relabel.Config{
+ Action: relabel.Drop,
+ SourceLabels: []model.LabelName{"__address__"},
+ Regex: relabel.MustNewRegexp(`example\.com:.*`),
+ },
+ Output: labels.EmptyLabels(),
+ Keep: false,
+ },
+ {
+ Rule: &relabel.Config{
+ Action: relabel.Replace,
+ TargetLabel: "job",
+ Regex: relabel.MustNewRegexp(".*"),
+ Replacement: "should_not_apply",
+ },
+ Output: labels.EmptyLabels(),
+ Keep: false,
+ },
+ },
+ },
+ },
// With a matching metric.
{
endpoint: api.targetMetadata,
@@ -2047,8 +2111,8 @@ func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, es storage.E
},
sorter: func(m any) {
sort.Slice(m.([]metricMetadata), func(i, j int) bool {
- s := m.([]metricMetadata)
- return s[i].MetricFamily < s[j].MetricFamily
+ mm := m.([]metricMetadata)
+ return mm[i].MetricFamily < mm[j].MetricFamily
})
},
},
@@ -3762,17 +3826,16 @@ func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, es storage.E
tr.ResetMetadataStore()
for _, tm := range test.metadata {
- tr.SetMetadataStoreForTargets(tm.identifier, &testMetaStore{Metadata: tm.metadata})
- }
-
- for _, te := range test.exemplars {
- for _, e := range te.Exemplars {
- _, err := es.AppendExemplar(0, te.SeriesLabels, e)
- require.NoError(t, err)
- }
+ // TODO: Check error and fixed broken test/bug.
+ // TestEndpoints/local/run_60_metricMetadata_"limit=1&limit_per_metric=1"/GET fails if we check the error.
+ _ = tr.SetMetadataStoreForTargets(tm.identifier, &testMetaStore{Metadata: tm.metadata})
}
res := test.endpoint(req.WithContext(ctx))
+ if res.finalizer != nil {
+ // Finalizers were added to ensure closed readers on API panics, ensure they are closed here too.
+ res.finalizer()
+ }
assertAPIError(t, res.err, test.errType)
if test.sorter != nil {
@@ -4052,6 +4115,7 @@ func TestAdminEndpoints(t *testing.T) {
dbDir: dir,
ready: func(f http.HandlerFunc) http.HandlerFunc { return f },
enableAdmin: tc.enableAdmin,
+ parser: testParser,
}
endpoint := tc.endpoint(api)
@@ -4465,6 +4529,18 @@ func TestTSDBStatus(t *testing.T) {
values: map[string][]string{"limit": {"0"}},
errType: errorBadData,
},
+ {
+ db: tsdb,
+ endpoint: tsdbStatusAPI,
+ values: map[string][]string{"limit": {"10000"}},
+ errType: errorNone,
+ },
+ {
+ db: tsdb,
+ endpoint: tsdbStatusAPI,
+ values: map[string][]string{"limit": {"10001"}},
+ errType: errorBadData,
+ },
} {
t.Run(strconv.Itoa(i), func(t *testing.T) {
api := &API{db: tc.db, gatherer: prometheus.DefaultGatherer}
@@ -4758,13 +4834,10 @@ func TestExtractQueryOpts(t *testing.T) {
// Test query timeout parameter.
func TestQueryTimeout(t *testing.T) {
- storage := promqltest.LoadedStorage(t, `
+ s := promqltest.LoadedStorage(t, `
load 1m
test_metric1{foo="bar"} 0+100x100
`)
- t.Cleanup(func() {
- _ = storage.Close()
- })
now := time.Now()
@@ -4784,14 +4857,15 @@ func TestQueryTimeout(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
engine := &fakeEngine{}
api := &API{
- Queryable: storage,
+ Queryable: s,
QueryEngine: engine,
- ExemplarQueryable: storage.ExemplarQueryable(),
+ ExemplarQueryable: s,
alertmanagerRetriever: testAlertmanagerRetriever{}.toFactory(),
flagsMap: sampleFlagMap,
now: func() time.Time { return now },
config: func() config.Config { return samplePrometheusCfg },
ready: func(f http.HandlerFunc) http.HandlerFunc { return f },
+ parser: testParser,
}
query := url.Values{
diff --git a/web/api/v1/codec.go b/web/api/v1/codec.go
index 492e00a74a..e7e53b466c 100644
--- a/web/api/v1/codec.go
+++ b/web/api/v1/codec.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The Prometheus Authors
+// 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
diff --git a/web/api/v1/codec_test.go b/web/api/v1/codec_test.go
index 911bf206e3..10038b605a 100644
--- a/web/api/v1/codec_test.go
+++ b/web/api/v1/codec_test.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The Prometheus Authors
+// 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
diff --git a/web/api/v1/errors_test.go b/web/api/v1/errors_test.go
index c44444404b..b041024a48 100644
--- a/web/api/v1/errors_test.go
+++ b/web/api/v1/errors_test.go
@@ -1,4 +1,4 @@
-// Copyright 2022 The Prometheus Authors
+// 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
@@ -34,6 +34,7 @@ import (
"github.com/prometheus/prometheus/config"
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/promql"
+ "github.com/prometheus/prometheus/promql/parser"
"github.com/prometheus/prometheus/promql/promqltest"
"github.com/prometheus/prometheus/rules"
"github.com/prometheus/prometheus/scrape"
@@ -134,7 +135,7 @@ func createPrometheusAPI(t *testing.T, q storage.SampleAndChunkQueryable, overri
api := NewAPI(
engine,
q,
- nil,
+ nil, nil,
nil,
func(context.Context) ScrapePoolsRetriever { return &DummyScrapePoolsRetriever{} },
func(context.Context) TargetRetriever { return &DummyTargetRetriever{} },
@@ -168,6 +169,9 @@ func createPrometheusAPI(t *testing.T, q storage.SampleAndChunkQueryable, overri
false,
false,
overrideErrorCode,
+ nil,
+ OpenAPIOptions{},
+ parser.NewParser(parser.Options{}),
)
promRouter := route.New().WithPrefix("/api/v1")
diff --git a/web/api/v1/json_codec.go b/web/api/v1/json_codec.go
index 4f3a23e976..adcf0e34bc 100644
--- a/web/api/v1/json_codec.go
+++ b/web/api/v1/json_codec.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The Prometheus Authors
+// 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
diff --git a/web/api/v1/json_codec_test.go b/web/api/v1/json_codec_test.go
index f0a671d6d1..8d17a1759f 100644
--- a/web/api/v1/json_codec_test.go
+++ b/web/api/v1/json_codec_test.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The Prometheus Authors
+// 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
diff --git a/web/api/v1/openapi.go b/web/api/v1/openapi.go
new file mode 100644
index 0000000000..59fa8969ef
--- /dev/null
+++ b/web/api/v1/openapi.go
@@ -0,0 +1,320 @@
+// 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.
+
+// This file implements OpenAPI 3.2 specification generation for the Prometheus HTTP API.
+// It provides dynamic spec building with optional path filtering.
+package v1
+
+import (
+ "log/slog"
+ "net/http"
+ "net/url"
+ "path"
+ "strings"
+ "sync"
+
+ "github.com/pb33f/libopenapi/datamodel/high/base"
+ v3 "github.com/pb33f/libopenapi/datamodel/high/v3"
+ "github.com/pb33f/libopenapi/orderedmap"
+)
+
+const (
+ // OpenAPI 3.1.0 is the default version with broader compatibility.
+ openAPIVersion31 = "3.1.0"
+ // OpenAPI 3.2.0 supports advanced features like itemSchema for SSE streams.
+ openAPIVersion32 = "3.2.0"
+)
+
+// OpenAPIOptions configures the OpenAPI spec builder.
+type OpenAPIOptions struct {
+ // IncludePaths filters which paths to include in the spec.
+ // If empty, all paths are included.
+ // Paths are matched by prefix (e.g., "/query" matches "/query" and "/query_range").
+ IncludePaths []string
+
+ // ExternalURL is the external URL of the Prometheus server (e.g., "http://prometheus.example.com:9090").
+ ExternalURL string
+
+ // Version is the API version to include in the OpenAPI spec.
+ // If empty, defaults to "0.0.1-undefined".
+ Version string
+}
+
+// OpenAPIBuilder builds and caches OpenAPI specifications.
+type OpenAPIBuilder struct {
+ mu sync.RWMutex
+ cachedYAML31 []byte // Cached OpenAPI 3.1 spec.
+ cachedYAML32 []byte // Cached OpenAPI 3.2 spec.
+ options OpenAPIOptions
+ logger *slog.Logger
+}
+
+// NewOpenAPIBuilder creates a new OpenAPI builder with the given options.
+func NewOpenAPIBuilder(opts OpenAPIOptions, logger *slog.Logger) *OpenAPIBuilder {
+ b := &OpenAPIBuilder{
+ options: opts,
+ logger: logger,
+ }
+
+ b.rebuild()
+ return b
+}
+
+// rebuild constructs the OpenAPI specs for both 3.1 and 3.2 versions based on current options.
+func (b *OpenAPIBuilder) rebuild() {
+ b.mu.Lock()
+ defer b.mu.Unlock()
+
+ // Build OpenAPI 3.1 spec.
+ doc31 := b.buildDocument(openAPIVersion31)
+ yamlBytes31, err := doc31.Render()
+ if err != nil {
+ b.logger.Error("failed to render OpenAPI 3.1 spec - this is a bug, please report it", "err", err)
+ return
+ }
+ b.cachedYAML31 = yamlBytes31
+
+ // Build OpenAPI 3.2 spec.
+ doc32 := b.buildDocument(openAPIVersion32)
+ yamlBytes32, err := doc32.Render()
+ if err != nil {
+ b.logger.Error("failed to render OpenAPI 3.2 spec - this is a bug, please report it", "err", err)
+ return
+ }
+ b.cachedYAML32 = yamlBytes32
+}
+
+// ServeOpenAPI returns the OpenAPI specification as YAML.
+// By default, serves OpenAPI 3.1.0. Use ?openapi_version=3.2 for OpenAPI 3.2.0.
+func (b *OpenAPIBuilder) ServeOpenAPI(w http.ResponseWriter, r *http.Request) {
+ // Parse query parameter to determine which version to serve.
+ requestedVersion := r.URL.Query().Get("openapi_version")
+
+ b.mu.RLock()
+ var yamlData []byte
+ switch requestedVersion {
+ case "3.2", "3.2.0":
+ yamlData = b.cachedYAML32
+ case "3.1", "3.1.0":
+ yamlData = b.cachedYAML31
+ default:
+ // Default to OpenAPI 3.1.0 for broader compatibility.
+ yamlData = b.cachedYAML31
+ }
+ b.mu.RUnlock()
+
+ w.Header().Set("Content-Type", "application/yaml; charset=utf-8")
+ w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
+ w.WriteHeader(http.StatusOK)
+ w.Write(yamlData)
+}
+
+// WrapHandler returns the handler unchanged (no validation).
+func (*OpenAPIBuilder) WrapHandler(next http.HandlerFunc) http.HandlerFunc {
+ return next
+}
+
+// shouldIncludePath checks if a path should be included based on options.
+func (b *OpenAPIBuilder) shouldIncludePath(path string) bool {
+ if len(b.options.IncludePaths) == 0 {
+ return true
+ }
+ for _, include := range b.options.IncludePaths {
+ if strings.HasPrefix(path, include) || path == include {
+ return true
+ }
+ }
+ return false
+}
+
+// shouldIncludePathForVersion checks if a path should be included for a specific OpenAPI version.
+func (b *OpenAPIBuilder) shouldIncludePathForVersion(path, version string) bool {
+ // First check IncludePaths filter.
+ if !b.shouldIncludePath(path) {
+ return false
+ }
+
+ // OpenAPI 3.1 excludes paths that require 3.2 features.
+ // The /notifications/live endpoint uses itemSchema which is a 3.2-only feature.
+ if version == openAPIVersion31 && path == "/notifications/live" {
+ return false
+ }
+
+ return true
+}
+
+// buildDocument creates the OpenAPI document for the specified version using high-level structs.
+func (b *OpenAPIBuilder) buildDocument(version string) *v3.Document {
+ return &v3.Document{
+ Version: version,
+ Info: b.buildInfo(),
+ Servers: b.buildServers(),
+ Tags: b.buildTags(version),
+ Paths: b.buildPaths(version),
+ Components: b.buildComponents(),
+ }
+}
+
+// buildInfo constructs the info section.
+func (b *OpenAPIBuilder) buildInfo() *base.Info {
+ apiVersion := b.options.Version
+ if apiVersion == "" {
+ apiVersion = "0.0.1-undefined"
+ }
+ return &base.Info{
+ Title: "Prometheus API",
+ Description: "Prometheus is an Open-Source monitoring system with a dimensional data model, flexible query language, efficient time series database and modern alerting approach.",
+ Version: apiVersion,
+ Contact: &base.Contact{
+ Name: "Prometheus Community",
+ URL: "https://prometheus.io/community/",
+ },
+ }
+}
+
+// buildServers constructs the servers section.
+func (b *OpenAPIBuilder) buildServers() []*v3.Server {
+ // ExternalURL is always set by computeExternalURL in main.go.
+ // It includes scheme, host, port, and optional path prefix (without trailing slash).
+ serverURL := "/api/v1"
+ if b.options.ExternalURL != "" {
+ baseURL, err := url.Parse(b.options.ExternalURL)
+ if err == nil {
+ // Use path.Join to properly append /api/v1 to the existing path.
+ // Then use ResolveReference to construct the full URL.
+ baseURL.Path = path.Join(baseURL.Path, "/api/v1")
+ serverURL = baseURL.String()
+ }
+ }
+ return []*v3.Server{
+ {URL: serverURL},
+ }
+}
+
+// buildTags constructs the global tags list.
+// Tag summary is an OpenAPI 3.2 feature, excluded from 3.1.
+// Tag description is supported in both 3.1 and 3.2.
+func (*OpenAPIBuilder) buildTags(version string) []*base.Tag {
+ // Define tags with all metadata.
+ tagData := []struct {
+ name string
+ summary string
+ description string
+ }{
+ {"query", "Query", "Query and evaluate PromQL expressions."},
+ {"metadata", "Metadata", "Retrieve metric metadata such as type and unit."},
+ {"labels", "Labels", "Query label names and values."},
+ {"series", "Series", "Query and manage time series."},
+ {"targets", "Targets", "Retrieve target and scrape pool information."},
+ {"rules", "Rules", "Query recording and alerting rules."},
+ {"alerts", "Alerts", "Query active alerts and alertmanager discovery."},
+ {"status", "Status", "Retrieve server status and configuration."},
+ {"admin", "Admin", "Administrative operations for TSDB management."},
+ {"features", "Features", "Query enabled features."},
+ {"remote", "Remote Storage", "Remote read and write endpoints."},
+ {"otlp", "OTLP", "OpenTelemetry Protocol metrics ingestion."},
+ {"notifications", "Notifications", "Server notifications and events."},
+ }
+
+ tags := make([]*base.Tag, 0, len(tagData))
+ for _, td := range tagData {
+ tag := &base.Tag{
+ Name: td.name,
+ Description: td.description, // Description is supported in both 3.1 and 3.2.
+ }
+
+ // Summary is an OpenAPI 3.2 feature only.
+ if version == openAPIVersion32 {
+ tag.Summary = td.summary
+ }
+
+ tags = append(tags, tag)
+ }
+
+ return tags
+}
+
+// buildPaths constructs all API path definitions.
+func (b *OpenAPIBuilder) buildPaths(version string) *v3.Paths {
+ pathItems := orderedmap.New[string, *v3.PathItem]()
+
+ allPaths := b.getAllPathDefinitions()
+ for pair := allPaths.First(); pair != nil; pair = pair.Next() {
+ if b.shouldIncludePathForVersion(pair.Key(), version) {
+ pathItems.Set(pair.Key(), pair.Value())
+ }
+ }
+
+ return &v3.Paths{PathItems: pathItems}
+}
+
+// getAllPathDefinitions returns all path definitions.
+func (b *OpenAPIBuilder) getAllPathDefinitions() *orderedmap.Map[string, *v3.PathItem] {
+ paths := orderedmap.New[string, *v3.PathItem]()
+
+ // Query endpoints.
+ paths.Set("/query", b.queryPath())
+ paths.Set("/query_range", b.queryRangePath())
+ paths.Set("/query_exemplars", b.queryExemplarsPath())
+ paths.Set("/format_query", b.formatQueryPath())
+ paths.Set("/parse_query", b.parseQueryPath())
+
+ // Label endpoints.
+ paths.Set("/labels", b.labelsPath())
+ paths.Set("/label/{name}/values", b.labelValuesPath())
+
+ // Series endpoints.
+ paths.Set("/series", b.seriesPath())
+
+ // Metadata endpoints.
+ paths.Set("/metadata", b.metadataPath())
+
+ // Target endpoints.
+ paths.Set("/scrape_pools", b.scrapePoolsPath())
+ paths.Set("/targets", b.targetsPath())
+ paths.Set("/targets/metadata", b.targetsMetadataPath())
+ paths.Set("/targets/relabel_steps", b.targetsRelabelStepsPath())
+
+ // Rules and alerts endpoints.
+ paths.Set("/rules", b.rulesPath())
+ paths.Set("/alerts", b.alertsPath())
+ paths.Set("/alertmanagers", b.alertmanagersPath())
+
+ // Status endpoints.
+ paths.Set("/status/config", b.statusConfigPath())
+ paths.Set("/status/runtimeinfo", b.statusRuntimeInfoPath())
+ paths.Set("/status/buildinfo", b.statusBuildInfoPath())
+ paths.Set("/status/flags", b.statusFlagsPath())
+ paths.Set("/status/tsdb", b.statusTSDBPath())
+ paths.Set("/status/tsdb/blocks", b.statusTSDBBlocksPath())
+ paths.Set("/status/walreplay", b.statusWALReplayPath())
+
+ // Admin endpoints.
+ paths.Set("/admin/tsdb/delete_series", b.adminDeleteSeriesPath())
+ paths.Set("/admin/tsdb/clean_tombstones", b.adminCleanTombstonesPath())
+ paths.Set("/admin/tsdb/snapshot", b.adminSnapshotPath())
+
+ // Remote endpoints.
+ paths.Set("/read", b.remoteReadPath())
+ paths.Set("/write", b.remoteWritePath())
+ paths.Set("/otlp/v1/metrics", b.otlpWritePath())
+
+ // Notifications endpoints.
+ paths.Set("/notifications", b.notificationsPath())
+ paths.Set("/notifications/live", b.notificationsLivePath())
+
+ // Features endpoint.
+ paths.Set("/features", b.featuresPath())
+
+ return paths
+}
diff --git a/web/api/v1/openapi_coverage_test.go b/web/api/v1/openapi_coverage_test.go
new file mode 100644
index 0000000000..103f82e08e
--- /dev/null
+++ b/web/api/v1/openapi_coverage_test.go
@@ -0,0 +1,258 @@
+// 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 v1
+
+import (
+ _ "embed"
+ "go/ast"
+ "go/parser"
+ "go/token"
+ "strconv"
+ "strings"
+ "testing"
+
+ v3 "github.com/pb33f/libopenapi/datamodel/high/v3"
+ "github.com/prometheus/common/promslog"
+ "github.com/stretchr/testify/require"
+)
+
+//go:embed api.go
+var apiGoSource string
+
+// routeInfo represents a route extracted from the Register function.
+type routeInfo struct {
+ method string
+ path string
+}
+
+// extractRoutesFromRegister parses the api.go source and extracts all routes
+// registered in the (*API) Register function using AST.
+func extractRoutesFromRegister(t *testing.T, source string) []routeInfo {
+ fset := token.NewFileSet()
+ f, err := parser.ParseFile(fset, "api.go", source, parser.ParseComments)
+ require.NoError(t, err, "failed to parse api.go")
+
+ var registerFunc *ast.FuncDecl
+
+ // Find the Register method on *API.
+ ast.Inspect(f, func(n ast.Node) bool {
+ fn, ok := n.(*ast.FuncDecl)
+ if !ok || fn.Body == nil {
+ return true
+ }
+
+ if fn.Name.Name != "Register" {
+ return true
+ }
+
+ // Ensure it's a method on *API.
+ if fn.Recv == nil || len(fn.Recv.List) != 1 {
+ return true
+ }
+
+ star, ok := fn.Recv.List[0].Type.(*ast.StarExpr)
+ if !ok {
+ return true
+ }
+
+ ident, ok := star.X.(*ast.Ident)
+ if !ok || ident.Name != "API" {
+ return true
+ }
+
+ registerFunc = fn
+ return false // Stop walking once found.
+ })
+
+ require.NotNil(t, registerFunc, "Register method not found")
+
+ var routes []routeInfo
+
+ // Extract all r.Get, r.Post, r.Put, r.Delete, r.Options calls.
+ ast.Inspect(registerFunc.Body, func(n ast.Node) bool {
+ call, ok := n.(*ast.CallExpr)
+ if !ok {
+ return true
+ }
+
+ sel, ok := call.Fun.(*ast.SelectorExpr)
+ if !ok {
+ return true
+ }
+
+ // Check if it's a router method call.
+ method := sel.Sel.Name
+ if method != "Get" && method != "Post" && method != "Put" && method != "Delete" && method != "Del" && method != "Options" {
+ return true
+ }
+
+ // Ensure the receiver is 'r'.
+ if x, ok := sel.X.(*ast.Ident); !ok || x.Name != "r" {
+ return true
+ }
+
+ if len(call.Args) == 0 {
+ return true
+ }
+
+ // Extract the path from the first argument.
+ lit, ok := call.Args[0].(*ast.BasicLit)
+ if !ok || lit.Kind != token.STRING {
+ return true
+ }
+
+ path, err := strconv.Unquote(lit.Value)
+ if err != nil {
+ return true
+ }
+
+ // Normalize Del to DELETE.
+ if method == "Del" {
+ method = "Delete"
+ }
+
+ routes = append(routes, routeInfo{
+ method: strings.ToUpper(method),
+ path: path,
+ })
+ return true
+ })
+
+ return routes
+}
+
+// normalizePathForOpenAPI converts route paths with colon parameters to OpenAPI format.
+// e.g., "/label/:name/values" -> "/label/{name}/values".
+func normalizePathForOpenAPI(path string) string {
+ // Replace :param with {param}.
+ parts := strings.Split(path, "/")
+ for i, part := range parts {
+ if trimmed, ok := strings.CutPrefix(part, ":"); ok {
+ parts[i] = "{" + trimmed + "}"
+ }
+ }
+ return strings.Join(parts, "/")
+}
+
+// TestOpenAPICoverage verifies that all routes registered in the Register function
+// are documented in the OpenAPI specification.
+func TestOpenAPICoverage(t *testing.T) {
+ // Extract routes from api.go using AST.
+ routes := extractRoutesFromRegister(t, apiGoSource)
+ require.NotEmpty(t, routes, "no routes found in Register function")
+
+ // Build OpenAPI spec.
+ builder := NewOpenAPIBuilder(OpenAPIOptions{}, promslog.NewNopLogger())
+ allPaths := builder.getAllPathDefinitions()
+
+ // Create a map of OpenAPI paths for quick lookup.
+ // Key is the normalized path, value is the PathItem.
+ openAPIPaths := make(map[string]bool)
+ for pair := allPaths.First(); pair != nil; pair = pair.Next() {
+ pathItem := pair.Value()
+ path := pair.Key()
+
+ // Track which methods are defined for this path.
+ if pathItem.Get != nil {
+ openAPIPaths[path+":GET"] = true
+ }
+ if pathItem.Post != nil {
+ openAPIPaths[path+":POST"] = true
+ }
+ if pathItem.Put != nil {
+ openAPIPaths[path+":PUT"] = true
+ }
+ if pathItem.Delete != nil {
+ openAPIPaths[path+":DELETE"] = true
+ }
+ if pathItem.Options != nil {
+ openAPIPaths[path+":OPTIONS"] = true
+ }
+ }
+
+ // Check coverage for each route.
+ var missingRoutes []string
+ ignoredRoutes := map[string]bool{
+ "/*path:OPTIONS": true, // Wildcard OPTIONS handler.
+ "/openapi.yaml:GET": true, // Self-referential endpoint.
+ "/notifications/live:GET": true, // SSE endpoint (version-specific).
+ }
+
+ for _, route := range routes {
+ normalizedPath := normalizePathForOpenAPI(route.path)
+ key := normalizedPath + ":" + route.method
+
+ // Skip ignored routes.
+ if ignoredRoutes[key] {
+ continue
+ }
+
+ if !openAPIPaths[key] {
+ missingRoutes = append(missingRoutes, key)
+ }
+ }
+
+ if len(missingRoutes) > 0 {
+ t.Errorf("The following routes are registered but not documented in OpenAPI spec:\n%s",
+ strings.Join(missingRoutes, "\n"))
+ }
+}
+
+// TestOpenAPIHasNoExtraRoutes verifies that the OpenAPI spec doesn't document
+// routes that aren't actually registered.
+func TestOpenAPIHasNoExtraRoutes(t *testing.T) {
+ // Extract routes from api.go using AST.
+ routes := extractRoutesFromRegister(t, apiGoSource)
+ require.NotEmpty(t, routes, "no routes found in Register function")
+
+ // Create a map of registered routes.
+ registeredRoutes := make(map[string]bool)
+ for _, route := range routes {
+ normalizedPath := normalizePathForOpenAPI(route.path)
+ key := normalizedPath + ":" + route.method
+ registeredRoutes[key] = true
+ }
+
+ // Build OpenAPI spec.
+ builder := NewOpenAPIBuilder(OpenAPIOptions{}, promslog.NewNopLogger())
+ allPaths := builder.getAllPathDefinitions()
+
+ // Check if any OpenAPI paths are not registered.
+ var extraRoutes []string
+
+ for pair := allPaths.First(); pair != nil; pair = pair.Next() {
+ pathItem := pair.Value()
+ path := pair.Key()
+
+ checkMethod := func(method string, op *v3.Operation) {
+ if op != nil {
+ key := path + ":" + method
+ if !registeredRoutes[key] {
+ extraRoutes = append(extraRoutes, key)
+ }
+ }
+ }
+
+ checkMethod("GET", pathItem.Get)
+ checkMethod("POST", pathItem.Post)
+ checkMethod("PUT", pathItem.Put)
+ checkMethod("DELETE", pathItem.Delete)
+ checkMethod("OPTIONS", pathItem.Options)
+ }
+
+ if len(extraRoutes) > 0 {
+ t.Errorf("The following routes are documented in OpenAPI but not registered:\n%s",
+ strings.Join(extraRoutes, "\n"))
+ }
+}
diff --git a/web/api/v1/openapi_examples.go b/web/api/v1/openapi_examples.go
new file mode 100644
index 0000000000..50e155b184
--- /dev/null
+++ b/web/api/v1/openapi_examples.go
@@ -0,0 +1,1013 @@
+// 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.
+
+// This file contains example request bodies and response data for OpenAPI documentation.
+// Examples are included in the generated spec to provide realistic usage scenarios for API consumers.
+package v1
+
+import (
+ "github.com/pb33f/libopenapi/datamodel/high/base"
+ "github.com/pb33f/libopenapi/orderedmap"
+
+ "github.com/prometheus/prometheus/model/labels"
+ "github.com/prometheus/prometheus/promql"
+)
+
+// Example builders for request bodies.
+
+func queryPostExamples() *orderedmap.Map[string, *base.Example] {
+ examples := orderedmap.New[string, *base.Example]()
+
+ examples.Set("simpleQuery", &base.Example{
+ Summary: "Simple instant query",
+ Value: createYAMLNode(map[string]any{"query": "up"}),
+ })
+
+ examples.Set("queryWithTime", &base.Example{
+ Summary: "Query with specific timestamp",
+ Value: createYAMLNode(map[string]any{
+ "query": "up{job=\"prometheus\"}",
+ "time": "2026-01-02T13:37:00.000Z",
+ }),
+ })
+
+ examples.Set("queryWithLimit", &base.Example{
+ Summary: "Query with limit and statistics",
+ Value: createYAMLNode(map[string]any{
+ "query": "rate(prometheus_http_requests_total{handler=\"/api/v1/query\"}[5m])",
+ "limit": 100,
+ "stats": "all",
+ }),
+ })
+
+ return examples
+}
+
+// queryRangePostExamples returns examples for POST /query_range endpoint.
+func queryRangePostExamples() *orderedmap.Map[string, *base.Example] {
+ examples := orderedmap.New[string, *base.Example]()
+
+ examples.Set("basicRange", &base.Example{
+ Summary: "Basic range query",
+ Value: createYAMLNode(map[string]any{
+ "query": "up",
+ "start": "2026-01-02T12:37:00.000Z",
+ "end": "2026-01-02T13:37:00.000Z",
+ "step": "15s",
+ }),
+ })
+
+ examples.Set("rateQuery", &base.Example{
+ Summary: "Rate calculation over time range",
+ Value: createYAMLNode(map[string]any{
+ "query": "rate(prometheus_http_requests_total{handler=\"/api/v1/query\"}[5m])",
+ "start": "2026-01-02T12:37:00.000Z",
+ "end": "2026-01-02T13:37:00.000Z",
+ "step": "30s",
+ "timeout": "30s",
+ }),
+ })
+
+ return examples
+}
+
+// queryExemplarsPostExamples returns examples for POST /query_exemplars endpoint.
+func queryExemplarsPostExamples() *orderedmap.Map[string, *base.Example] {
+ examples := orderedmap.New[string, *base.Example]()
+
+ examples.Set("basicExemplar", &base.Example{
+ Summary: "Query exemplars for a metric",
+ Value: createYAMLNode(map[string]any{"query": "prometheus_http_requests_total"}),
+ })
+
+ examples.Set("exemplarWithTimeRange", &base.Example{
+ Summary: "Exemplars within specific time range",
+ Value: createYAMLNode(map[string]any{
+ "query": "prometheus_http_requests_total{job=\"prometheus\"}",
+ "start": "2026-01-02T12:37:00.000Z",
+ "end": "2026-01-02T13:37:00.000Z",
+ }),
+ })
+
+ return examples
+}
+
+// formatQueryPostExamples returns examples for POST /format_query endpoint.
+func formatQueryPostExamples() *orderedmap.Map[string, *base.Example] {
+ examples := orderedmap.New[string, *base.Example]()
+
+ examples.Set("simpleFormat", &base.Example{
+ Summary: "Format a simple query",
+ Value: createYAMLNode(map[string]any{"query": "up{job=\"prometheus\"}"}),
+ })
+
+ examples.Set("complexFormat", &base.Example{
+ Summary: "Format a complex query",
+ Value: createYAMLNode(map[string]any{"query": "sum(rate(http_requests_total[5m])) by (job, status)"}),
+ })
+
+ return examples
+}
+
+// parseQueryPostExamples returns examples for POST /parse_query endpoint.
+func parseQueryPostExamples() *orderedmap.Map[string, *base.Example] {
+ examples := orderedmap.New[string, *base.Example]()
+
+ examples.Set("simpleParse", &base.Example{
+ Summary: "Parse a simple query",
+ Value: createYAMLNode(map[string]any{"query": "up"}),
+ })
+
+ examples.Set("complexParse", &base.Example{
+ Summary: "Parse a complex query",
+ Value: createYAMLNode(map[string]any{"query": "rate(http_requests_total{job=\"api\"}[5m])"}),
+ })
+
+ return examples
+}
+
+// labelsPostExamples returns examples for POST /labels endpoint.
+func labelsPostExamples() *orderedmap.Map[string, *base.Example] {
+ examples := orderedmap.New[string, *base.Example]()
+
+ examples.Set("allLabels", &base.Example{
+ Summary: "Get all label names",
+ Value: createYAMLNode(map[string]any{}),
+ })
+
+ examples.Set("labelsWithTimeRange", &base.Example{
+ Summary: "Get label names within time range",
+ Value: createYAMLNode(map[string]any{
+ "start": "2026-01-02T12:37:00.000Z",
+ "end": "2026-01-02T13:37:00.000Z",
+ }),
+ })
+
+ examples.Set("labelsWithMatch", &base.Example{
+ Summary: "Get label names matching series selector",
+ Value: createYAMLNode(map[string]any{
+ "match[]": []string{"up", "process_start_time_seconds{job=\"prometheus\"}"},
+ }),
+ })
+
+ return examples
+}
+
+// seriesPostExamples returns examples for POST /series endpoint.
+func seriesPostExamples() *orderedmap.Map[string, *base.Example] {
+ examples := orderedmap.New[string, *base.Example]()
+
+ examples.Set("seriesMatch", &base.Example{
+ Summary: "Find series by label matchers",
+ Value: createYAMLNode(map[string]any{
+ "match[]": []string{"up"},
+ }),
+ })
+
+ examples.Set("seriesWithTimeRange", &base.Example{
+ Summary: "Find series with time range",
+ Value: createYAMLNode(map[string]any{
+ "match[]": []string{"up", "process_cpu_seconds_total{job=\"prometheus\"}"},
+ "start": "2026-01-02T12:37:00.000Z",
+ "end": "2026-01-02T13:37:00.000Z",
+ }),
+ })
+
+ return examples
+}
+
+// Example builders for response bodies.
+
+// queryResponseExamples returns examples for /query response.
+func queryResponseExamples() *orderedmap.Map[string, *base.Example] {
+ examples := orderedmap.New[string, *base.Example]()
+
+ vectorResult := promql.Vector{
+ promql.Sample{
+ Metric: labels.FromStrings("__name__", "up", "job", "prometheus", "instance", "demo.prometheus.io:9090"),
+ T: 1767436620000,
+ F: 1,
+ },
+ promql.Sample{
+ Metric: labels.FromStrings("__name__", "up", "env", "demo", "job", "alertmanager", "instance", "demo.prometheus.io:9093"),
+ T: 1767436620000,
+ F: 1,
+ },
+ }
+
+ examples.Set("vectorResult", &base.Example{
+ Summary: "Instant vector query: up",
+ Value: vectorExample(vectorResult),
+ })
+
+ examples.Set("scalarResult", &base.Example{
+ Summary: "Scalar query: scalar(42)",
+ Value: createYAMLNode(map[string]any{
+ "status": "success",
+ "data": map[string]any{
+ "resultType": "scalar",
+ "result": []any{1767436620, "42"},
+ },
+ }),
+ })
+
+ matrixResult := promql.Matrix{
+ promql.Series{
+ Metric: labels.FromStrings("__name__", "up", "job", "prometheus", "instance", "demo.prometheus.io:9090"),
+ Floats: []promql.FPoint{
+ {T: 1767436320000, F: 1},
+ {T: 1767436620000, F: 1},
+ },
+ },
+ }
+
+ examples.Set("matrixResult", &base.Example{
+ Summary: "Range vector query: up[5m]",
+ Value: matrixExample(matrixResult),
+ })
+
+ // TODO: Add native histogram example.
+
+ return examples
+}
+
+// queryRangeResponseExamples returns examples for /query_range response.
+func queryRangeResponseExamples() *orderedmap.Map[string, *base.Example] {
+ examples := orderedmap.New[string, *base.Example]()
+
+ matrixResult := promql.Matrix{
+ promql.Series{
+ Metric: labels.FromStrings("__name__", "up", "job", "prometheus", "instance", "demo.prometheus.io:9090"),
+ Floats: []promql.FPoint{
+ {T: 1767433020000, F: 1},
+ {T: 1767434820000, F: 1},
+ {T: 1767436620000, F: 1},
+ },
+ },
+ }
+
+ examples.Set("matrixResult", &base.Example{
+ Summary: "Range query: rate(prometheus_http_requests_total[5m])",
+ Value: matrixExample(matrixResult),
+ })
+
+ // TODO: Add native histogram example.
+
+ return examples
+}
+
+// labelsResponseExamples returns examples for /labels response.
+func labelsResponseExamples() *orderedmap.Map[string, *base.Example] {
+ examples := orderedmap.New[string, *base.Example]()
+
+ examples.Set("labelNames", &base.Example{
+ Summary: "List of label names",
+ Value: createYAMLNode(map[string]any{
+ "status": "success",
+ "data": []string{
+ "__name__", "active", "address", "alertmanager", "alertname", "alertstate",
+ "backend", "branch", "code", "collector", "component", "device",
+ "env", "endpoint", "fstype", "handler", "instance", "job",
+ "le", "method", "mode", "name",
+ },
+ }),
+ })
+
+ return examples
+}
+
+// seriesResponseExamples returns examples for /series response.
+func seriesResponseExamples() *orderedmap.Map[string, *base.Example] {
+ examples := orderedmap.New[string, *base.Example]()
+
+ examples.Set("seriesList", &base.Example{
+ Summary: "List of series matching the selector",
+ Value: createYAMLNode(map[string]any{
+ "status": "success",
+ "data": []map[string]string{
+ {
+ "__name__": "up",
+ "env": "demo",
+ "instance": "demo.prometheus.io:8080",
+ "job": "cadvisor",
+ },
+ {
+ "__name__": "up",
+ "env": "demo",
+ "instance": "demo.prometheus.io:9093",
+ "job": "alertmanager",
+ },
+ {
+ "__name__": "up",
+ "env": "demo",
+ "instance": "demo.prometheus.io:9100",
+ "job": "node",
+ },
+ {
+ "__name__": "up",
+ "instance": "demo.prometheus.io:3000",
+ "job": "grafana",
+ },
+ {
+ "__name__": "up",
+ "instance": "demo.prometheus.io:8996",
+ "job": "random",
+ },
+ },
+ }),
+ })
+
+ return examples
+}
+
+// targetsResponseExamples returns examples for /targets response.
+func targetsResponseExamples() *orderedmap.Map[string, *base.Example] {
+ examples := orderedmap.New[string, *base.Example]()
+
+ examples.Set("targetsList", &base.Example{
+ Summary: "Active and dropped targets",
+ Value: createYAMLNode(map[string]any{
+ "status": "success",
+ "data": map[string]any{
+ "activeTargets": []map[string]any{
+ {
+ "discoveredLabels": map[string]string{
+ "__address__": "demo.prometheus.io:9093",
+ "__meta_filepath": "/etc/prometheus/file_sd/alertmanager.yml",
+ "__metrics_path__": "/metrics",
+ "__scheme__": "http",
+ "env": "demo",
+ "job": "alertmanager",
+ },
+ "labels": map[string]string{
+ "env": "demo",
+ "instance": "demo.prometheus.io:9093",
+ "job": "alertmanager",
+ },
+ "scrapePool": "alertmanager",
+ "scrapeUrl": "http://demo.prometheus.io:9093/metrics",
+ "globalUrl": "http://demo.prometheus.io:9093/metrics",
+ "lastError": "",
+ "lastScrape": "2026-01-02T13:36:40.200Z",
+ "lastScrapeDuration": 0.006576866,
+ "health": "up",
+ "scrapeInterval": "15s",
+ "scrapeTimeout": "10s",
+ },
+ },
+ "droppedTargets": []map[string]any{},
+ "droppedTargetCounts": map[string]int{
+ "alertmanager": 0,
+ "blackbox": 0,
+ "caddy": 0,
+ "cadvisor": 0,
+ "grafana": 0,
+ "node": 0,
+ "prometheus": 0,
+ "random": 0,
+ },
+ },
+ }),
+ })
+
+ return examples
+}
+
+// rulesResponseExamples returns examples for /rules response.
+func rulesResponseExamples() *orderedmap.Map[string, *base.Example] {
+ examples := orderedmap.New[string, *base.Example]()
+
+ examples.Set("ruleGroups", &base.Example{
+ Summary: "Alerting and recording rules",
+ Value: createYAMLNode(map[string]any{
+ "status": "success",
+ "data": map[string]any{
+ "groups": []map[string]any{
+ {
+ "name": "ansible managed alert rules",
+ "file": "/etc/prometheus/rules/ansible_managed.yml",
+ "interval": 15,
+ "limit": 0,
+ "rules": []map[string]any{
+ {
+ "state": "firing",
+ "name": "Watchdog",
+ "query": "vector(1)",
+ "duration": 600,
+ "keepFiringFor": 0,
+ "labels": map[string]string{"severity": "warning"},
+ "annotations": map[string]string{"description": "This is an alert meant to ensure that the entire alerting pipeline is functional. This alert is always firing, therefore it should always be firing in Alertmanager and always fire against a receiver. There are integrations with various notification mechanisms that send a notification when this alert is not firing. For example the \"DeadMansSnitch\" integration in PagerDuty.", "summary": "Ensure entire alerting pipeline is functional"},
+ "health": "ok",
+ "evaluationTime": 0.000356688,
+ "lastEvaluation": "2026-01-02T13:36:56.874Z",
+ "type": "alerting",
+ },
+ },
+ "evaluationTime": 0.000561635,
+ "lastEvaluation": "2026-01-02T13:36:56.874Z",
+ },
+ },
+ },
+ }),
+ })
+
+ return examples
+}
+
+// alertsResponseExamples returns examples for /alerts response.
+func alertsResponseExamples() *orderedmap.Map[string, *base.Example] {
+ examples := orderedmap.New[string, *base.Example]()
+
+ examples.Set("activeAlerts", &base.Example{
+ Summary: "Currently active alerts",
+ Value: createYAMLNode(map[string]any{
+ "status": "success",
+ "data": map[string]any{
+ "alerts": []map[string]any{
+ {
+ "labels": map[string]string{
+ "alertname": "Watchdog",
+ "severity": "warning",
+ },
+ "annotations": map[string]string{
+ "description": "This is an alert meant to ensure that the entire alerting pipeline is functional. This alert is always firing, therefore it should always be firing in Alertmanager and always fire against a receiver. There are integrations with various notification mechanisms that send a notification when this alert is not firing. For example the \"DeadMansSnitch\" integration in PagerDuty.",
+ "summary": "Ensure entire alerting pipeline is functional",
+ },
+ "state": "firing",
+ "activeAt": "2026-01-02T13:30:00.000Z",
+ "value": "1e+00",
+ },
+ },
+ },
+ }),
+ })
+
+ return examples
+}
+
+// queryExemplarsResponseExamples returns examples for /query_exemplars response.
+func queryExemplarsResponseExamples() *orderedmap.Map[string, *base.Example] {
+ examples := orderedmap.New[string, *base.Example]()
+
+ examples.Set("exemplarsResult", &base.Example{
+ Summary: "Exemplars for a metric with trace IDs",
+ Value: createYAMLNode(map[string]any{
+ "status": "success",
+ "data": []map[string]any{
+ {
+ "seriesLabels": map[string]string{
+ "__name__": "http_requests_total",
+ "job": "api-server",
+ "method": "GET",
+ },
+ "exemplars": []map[string]any{
+ {
+ "labels": map[string]string{
+ "traceID": "abc123def456",
+ },
+ "value": "1.5",
+ "timestamp": 1689956451.781,
+ },
+ },
+ },
+ },
+ }),
+ })
+
+ return examples
+}
+
+// formatQueryResponseExamples returns examples for /format_query response.
+func formatQueryResponseExamples() *orderedmap.Map[string, *base.Example] {
+ examples := orderedmap.New[string, *base.Example]()
+
+ examples.Set("formattedQuery", &base.Example{
+ Summary: "Formatted PromQL query",
+ Value: createYAMLNode(map[string]any{
+ "status": "success",
+ "data": "sum by(job, status) (rate(http_requests_total[5m]))",
+ }),
+ })
+
+ return examples
+}
+
+// parseQueryResponseExamples returns examples for /parse_query response.
+func parseQueryResponseExamples() *orderedmap.Map[string, *base.Example] {
+ examples := orderedmap.New[string, *base.Example]()
+
+ examples.Set("parsedQuery", &base.Example{
+ Summary: "Parsed PromQL expression tree",
+ Value: createYAMLNode(map[string]any{
+ "status": "success",
+ "data": map[string]any{
+ "resultType": "vector",
+ },
+ }),
+ })
+
+ return examples
+}
+
+// labelValuesResponseExamples returns examples for /label/{name}/values response.
+func labelValuesResponseExamples() *orderedmap.Map[string, *base.Example] {
+ examples := orderedmap.New[string, *base.Example]()
+
+ examples.Set("labelValues", &base.Example{
+ Summary: "List of values for a label",
+ Value: createYAMLNode(map[string]any{
+ "status": "success",
+ "data": []string{"alertmanager", "blackbox", "caddy", "cadvisor", "grafana", "node", "prometheus", "random"},
+ }),
+ })
+
+ return examples
+}
+
+// metadataResponseExamples returns examples for /metadata response.
+func metadataResponseExamples() *orderedmap.Map[string, *base.Example] {
+ examples := orderedmap.New[string, *base.Example]()
+
+ examples.Set("metricMetadata", &base.Example{
+ Summary: "Metadata for metrics",
+ Value: createYAMLNode(map[string]any{
+ "status": "success",
+ "data": map[string][]map[string]any{
+ "prometheus_rule_group_iterations_missed_total": {
+ {
+ "type": "counter",
+ "help": "The total number of rule group evaluations missed due to slow rule group evaluation.",
+ "unit": "",
+ },
+ },
+ "prometheus_sd_updates_total": {
+ {
+ "type": "counter",
+ "help": "Total number of update events sent to the SD consumers.",
+ "unit": "",
+ },
+ },
+ "go_gc_stack_starting_size_bytes": {
+ {
+ "type": "gauge",
+ "help": "The stack size of new goroutines. Sourced from /gc/stack/starting-size:bytes.",
+ "unit": "",
+ },
+ },
+ },
+ }),
+ })
+
+ return examples
+}
+
+// scrapePoolsResponseExamples returns examples for /scrape_pools response.
+func scrapePoolsResponseExamples() *orderedmap.Map[string, *base.Example] {
+ examples := orderedmap.New[string, *base.Example]()
+
+ examples.Set("scrapePoolsList", &base.Example{
+ Summary: "List of scrape pool names",
+ Value: createYAMLNode(map[string]any{
+ "status": "success",
+ "data": map[string]any{
+ "scrapePools": []string{"alertmanager", "blackbox", "caddy", "cadvisor", "grafana", "node", "prometheus", "random"},
+ },
+ }),
+ })
+
+ return examples
+}
+
+// targetsMetadataResponseExamples returns examples for /targets/metadata response.
+func targetsMetadataResponseExamples() *orderedmap.Map[string, *base.Example] {
+ examples := orderedmap.New[string, *base.Example]()
+
+ examples.Set("targetMetadata", &base.Example{
+ Summary: "Metadata for targets",
+ Value: createYAMLNode(map[string]any{
+ "status": "success",
+ "data": []map[string]any{
+ {
+ "target": map[string]string{
+ "instance": "localhost:9090",
+ "job": "prometheus",
+ },
+ "type": "gauge",
+ "help": "The current health status of the target",
+ "unit": "",
+ "metric": "up",
+ },
+ },
+ }),
+ })
+
+ return examples
+}
+
+// targetsRelabelStepsResponseExamples returns examples for /targets/relabel_steps response.
+func targetsRelabelStepsResponseExamples() *orderedmap.Map[string, *base.Example] {
+ examples := orderedmap.New[string, *base.Example]()
+
+ examples.Set("relabelSteps", &base.Example{
+ Summary: "Relabel steps for a target",
+ Value: createYAMLNode(map[string]any{
+ "status": "success",
+ "data": map[string]any{
+ "steps": []map[string]any{
+ {
+ "rule": map[string]any{
+ "source_labels": []string{"__address__"},
+ "target_label": "instance",
+ "action": "replace",
+ "regex": "(.*)",
+ "replacement": "$1",
+ },
+ "output": map[string]string{
+ "__address__": "localhost:9090",
+ "instance": "localhost:9090",
+ "job": "prometheus",
+ },
+ "keep": true,
+ },
+ },
+ },
+ }),
+ })
+
+ return examples
+}
+
+// alertmanagersResponseExamples returns examples for /alertmanagers response.
+func alertmanagersResponseExamples() *orderedmap.Map[string, *base.Example] {
+ examples := orderedmap.New[string, *base.Example]()
+
+ examples.Set("alertmanagerDiscovery", &base.Example{
+ Summary: "Alertmanager discovery results",
+ Value: createYAMLNode(map[string]any{
+ "status": "success",
+ "data": map[string]any{
+ "activeAlertmanagers": []map[string]any{
+ {
+ "url": "http://demo.prometheus.io:9093/api/v2/alerts",
+ },
+ },
+ "droppedAlertmanagers": []map[string]any{},
+ },
+ }),
+ })
+
+ return examples
+}
+
+// statusConfigResponseExamples returns examples for /status/config response.
+func statusConfigResponseExamples() *orderedmap.Map[string, *base.Example] {
+ examples := orderedmap.New[string, *base.Example]()
+
+ examples.Set("configYAML", &base.Example{
+ Summary: "Prometheus configuration",
+ Value: createYAMLNode(map[string]any{
+ "status": "success",
+ "data": map[string]any{
+ "yaml": "global:\n scrape_interval: 15s\n scrape_timeout: 10s\n evaluation_interval: 15s\n external_labels:\n environment: demo-prometheus-io\nalerting:\n alertmanagers:\n - scheme: http\n static_configs:\n - targets:\n - demo.prometheus.io:9093\nrule_files:\n- /etc/prometheus/rules/*.yml\n",
+ },
+ }),
+ })
+
+ return examples
+}
+
+// statusRuntimeInfoResponseExamples returns examples for /status/runtimeinfo response.
+func statusRuntimeInfoResponseExamples() *orderedmap.Map[string, *base.Example] {
+ examples := orderedmap.New[string, *base.Example]()
+
+ examples.Set("runtimeInfo", &base.Example{
+ Summary: "Runtime information",
+ Value: createYAMLNode(map[string]any{
+ "status": "success",
+ "data": map[string]any{
+ "startTime": "2026-01-01T13:37:00.000Z",
+ "CWD": "/",
+ "hostname": "demo-prometheus-io",
+ "serverTime": "2026-01-02T13:37:00.000Z",
+ "reloadConfigSuccess": true,
+ "lastConfigTime": "2026-01-01T13:37:00.000Z",
+ "corruptionCount": 0,
+ "goroutineCount": 88,
+ "GOMAXPROCS": 2,
+ "GOMEMLIMIT": int64(3703818240),
+ "GOGC": "75",
+ "GODEBUG": "",
+ "storageRetention": "31d",
+ },
+ }),
+ })
+
+ return examples
+}
+
+// statusBuildInfoResponseExamples returns examples for /status/buildinfo response.
+func statusBuildInfoResponseExamples() *orderedmap.Map[string, *base.Example] {
+ examples := orderedmap.New[string, *base.Example]()
+
+ examples.Set("buildInfo", &base.Example{
+ Summary: "Build information",
+ Value: createYAMLNode(map[string]any{
+ "status": "success",
+ "data": map[string]any{
+ "version": "3.7.3",
+ "revision": "0a41f0000705c69ab8e0f9a723fc73e39ed62b07",
+ "branch": "HEAD",
+ "buildUser": "root@08c890a84441",
+ "buildDate": "20251030-07:26:10",
+ "goVersion": "go1.25.3",
+ },
+ }),
+ })
+
+ return examples
+}
+
+// statusFlagsResponseExamples returns examples for /status/flags response.
+func statusFlagsResponseExamples() *orderedmap.Map[string, *base.Example] {
+ examples := orderedmap.New[string, *base.Example]()
+
+ examples.Set("flags", &base.Example{
+ Summary: "Command-line flags",
+ Value: createYAMLNode(map[string]any{
+ "status": "success",
+ "data": map[string]string{
+ "agent": "false",
+ "alertmanager.notification-queue-capacity": "10000",
+ "config.file": "/etc/prometheus/prometheus.yml",
+ "enable-feature": "exemplar-storage,native-histograms",
+ "query.max-concurrency": "20",
+ "query.timeout": "2m",
+ "storage.tsdb.path": "/prometheus",
+ "storage.tsdb.retention.time": "15d",
+ "web.console.libraries": "/usr/share/prometheus/console_libraries",
+ "web.console.templates": "/usr/share/prometheus/consoles",
+ "web.enable-admin-api": "true",
+ "web.enable-lifecycle": "true",
+ "web.listen-address": "0.0.0.0:9090",
+ "web.page-title": "Prometheus Time Series Collection and Processing Server",
+ },
+ }),
+ })
+
+ return examples
+}
+
+// statusTSDBResponseExamples returns examples for /status/tsdb response.
+func statusTSDBResponseExamples() *orderedmap.Map[string, *base.Example] {
+ examples := orderedmap.New[string, *base.Example]()
+
+ examples.Set("tsdbStats", &base.Example{
+ Summary: "TSDB statistics",
+ Value: createYAMLNode(map[string]any{
+ "status": "success",
+ "data": map[string]any{
+ "headStats": map[string]any{
+ "numSeries": 9925,
+ "numLabelPairs": 2512,
+ "chunkCount": 37525,
+ "minTime": int64(1767362400712),
+ "maxTime": int64(1767436620000),
+ },
+ "seriesCountByMetricName": []map[string]any{
+ {
+ "name": "up",
+ "value": 100,
+ },
+ {
+ "name": "http_requests_total",
+ "value": 500,
+ },
+ },
+ "labelValueCountByLabelName": []map[string]any{
+ {
+ "name": "__name__",
+ "value": 5,
+ },
+ {
+ "name": "job",
+ "value": 3,
+ },
+ },
+ "memoryInBytesByLabelName": []map[string]any{
+ {
+ "name": "__name__",
+ "value": 1024,
+ },
+ {
+ "name": "job",
+ "value": 512,
+ },
+ },
+ "seriesCountByLabelValuePair": []map[string]any{
+ {
+ "name": "job=prometheus",
+ "value": 100,
+ },
+ {
+ "name": "instance=localhost:9090",
+ "value": 100,
+ },
+ },
+ },
+ }),
+ })
+
+ return examples
+}
+
+// statusTSDBBlocksResponseExamples returns examples for /status/tsdb/blocks response.
+func statusTSDBBlocksResponseExamples() *orderedmap.Map[string, *base.Example] {
+ examples := orderedmap.New[string, *base.Example]()
+
+ examples.Set("tsdbBlocks", &base.Example{
+ Summary: "TSDB block information",
+ Value: createYAMLNode(map[string]any{
+ "status": "success",
+ "data": map[string]any{
+ "blocks": []map[string]any{
+ {
+ "ulid": "01KC4D6GXQA4CRHYKV78NEBVAE",
+ "minTime": int64(1764568801099),
+ "maxTime": int64(1764763200000),
+ "stats": map[string]any{
+ "numSamples": 129505582,
+ "numSeries": 10661,
+ "numChunks": 1073962,
+ },
+ "compaction": map[string]any{
+ "level": 4,
+ "sources": []string{
+ "01KBCJ7TR8A4QAJ3AA1J651P5S",
+ "01KBCS3J0E34567YPB8Y5W0E24",
+ "01KBCZZ9KRTYGG3E7HVQFGC3S3",
+ },
+ },
+ "version": 1,
+ },
+ },
+ },
+ }),
+ })
+
+ return examples
+}
+
+// statusWALReplayResponseExamples returns examples for /status/walreplay response.
+func statusWALReplayResponseExamples() *orderedmap.Map[string, *base.Example] {
+ examples := orderedmap.New[string, *base.Example]()
+
+ examples.Set("walReplay", &base.Example{
+ Summary: "WAL replay status",
+ Value: createYAMLNode(map[string]any{
+ "status": "success",
+ "data": map[string]any{
+ "min": 3209,
+ "max": 3214,
+ "current": 3214,
+ },
+ }),
+ })
+
+ return examples
+}
+
+// deleteSeriesResponseExamples returns examples for /admin/tsdb/delete_series response.
+func deleteSeriesResponseExamples() *orderedmap.Map[string, *base.Example] {
+ examples := orderedmap.New[string, *base.Example]()
+
+ examples.Set("deletionSuccess", &base.Example{
+ Summary: "Successful series deletion",
+ Value: createYAMLNode(map[string]any{
+ "status": "success",
+ }),
+ })
+
+ return examples
+}
+
+// cleanTombstonesResponseExamples returns examples for /admin/tsdb/clean_tombstones response.
+func cleanTombstonesResponseExamples() *orderedmap.Map[string, *base.Example] {
+ examples := orderedmap.New[string, *base.Example]()
+
+ examples.Set("tombstonesCleaned", &base.Example{
+ Summary: "Tombstones cleaned successfully",
+ Value: createYAMLNode(map[string]any{
+ "status": "success",
+ }),
+ })
+
+ return examples
+}
+
+// seriesDeleteResponseExamples returns examples for DELETE /series response.
+func seriesDeleteResponseExamples() *orderedmap.Map[string, *base.Example] {
+ examples := orderedmap.New[string, *base.Example]()
+
+ examples.Set("seriesDeleted", &base.Example{
+ Summary: "Series marked for deletion",
+ Value: createYAMLNode(map[string]any{
+ "status": "success",
+ }),
+ })
+
+ return examples
+}
+
+// snapshotResponseExamples returns examples for /admin/tsdb/snapshot response.
+func snapshotResponseExamples() *orderedmap.Map[string, *base.Example] {
+ examples := orderedmap.New[string, *base.Example]()
+
+ examples.Set("snapshotCreated", &base.Example{
+ Summary: "Snapshot created successfully",
+ Value: createYAMLNode(map[string]any{
+ "status": "success",
+ "data": map[string]any{
+ "name": "20260102T133700Z-a1b2c3d4e5f67890",
+ },
+ }),
+ })
+
+ return examples
+}
+
+// notificationsResponseExamples returns examples for /notifications response.
+func notificationsResponseExamples() *orderedmap.Map[string, *base.Example] {
+ examples := orderedmap.New[string, *base.Example]()
+
+ examples.Set("notifications", &base.Example{
+ Summary: "Server notifications",
+ Value: createYAMLNode(map[string]any{
+ "status": "success",
+ "data": []map[string]any{
+ {
+ "text": "Configuration reload has failed.",
+ "date": "2026-01-02T16:14:50.046Z",
+ "active": true,
+ },
+ },
+ }),
+ })
+
+ return examples
+}
+
+// notificationLiveExamples provides example SSE messages for the live notifications endpoint.
+func notificationLiveExamples() *orderedmap.Map[string, *base.Example] {
+ examples := orderedmap.New[string, *base.Example]()
+
+ examples.Set("activeNotification", &base.Example{
+ Summary: "Active notification SSE message",
+ Description: "An SSE message containing an active server notification.",
+ Value: createYAMLNode(map[string]any{
+ "data": "{\"text\":\"Configuration reload has failed.\",\"date\":\"2026-01-02T16:14:50.046Z\",\"active\":true}",
+ }),
+ })
+
+ return examples
+}
+
+// featuresResponseExamples returns examples for /features response.
+func featuresResponseExamples() *orderedmap.Map[string, *base.Example] {
+ examples := orderedmap.New[string, *base.Example]()
+
+ examples.Set("enabledFeatures", &base.Example{
+ Summary: "Enabled feature flags",
+ Value: createYAMLNode(map[string]any{
+ "status": "success",
+ "data": []string{"exemplar-storage", "remote-write-receiver"},
+ }),
+ })
+
+ return examples
+}
+
+// errorResponseExamples returns examples for error responses.
+func errorResponseExamples() *orderedmap.Map[string, *base.Example] {
+ examples := orderedmap.New[string, *base.Example]()
+
+ examples.Set("tsdbNotReady", &base.Example{
+ Summary: "TSDB not ready",
+ Value: createYAMLNode(map[string]any{
+ "status": "error",
+ "errorType": "internal",
+ "error": "TSDB not ready",
+ }),
+ })
+
+ return examples
+}
diff --git a/web/api/v1/openapi_golden_test.go b/web/api/v1/openapi_golden_test.go
new file mode 100644
index 0000000000..468d56e46d
--- /dev/null
+++ b/web/api/v1/openapi_golden_test.go
@@ -0,0 +1,176 @@
+// 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 v1
+
+import (
+ "flag"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+ "go.yaml.in/yaml/v3"
+
+ "github.com/prometheus/prometheus/web/api/testhelpers"
+)
+
+var updateOpenAPISpec = flag.Bool("update-openapi-spec", false, "update openapi golden files with the current specs")
+
+// TestOpenAPIGolden_3_1 verifies that the OpenAPI 3.1 spec matches the golden file.
+func TestOpenAPIGolden_3_1(t *testing.T) {
+ // Create an API instance to serve the OpenAPI spec.
+ api := newTestAPI(t, testhelpers.APIConfig{})
+
+ // Fetch the OpenAPI 3.1 spec from the API (default, no query param).
+ resp := testhelpers.GET(t, api, "/api/v1/openapi.yaml")
+ require.Equal(t, 200, resp.StatusCode, "expected HTTP 200 for OpenAPI spec endpoint")
+ require.NotEmpty(t, resp.Body, "OpenAPI spec should not be empty")
+
+ goldenPath := filepath.Join("testdata", "openapi_3.1_golden.yaml")
+
+ if *updateOpenAPISpec {
+ // Update mode: write the current spec to the golden file.
+ t.Logf("Updating golden file: %s", goldenPath)
+
+ // Ensure the testdata directory exists.
+ err := os.MkdirAll(filepath.Dir(goldenPath), 0o755)
+ require.NoError(t, err, "failed to create testdata directory")
+
+ // Write the golden file.
+ err = os.WriteFile(goldenPath, []byte(resp.Body), 0o644)
+ require.NoError(t, err, "failed to write golden file")
+
+ t.Logf("Golden file updated successfully")
+ return
+ }
+
+ // Comparison mode: verify the spec matches the golden file.
+ goldenData, err := os.ReadFile(goldenPath)
+ require.NoError(t, err, "failed to read golden file (run with -update-openapi-spec to generate it)")
+
+ require.Equal(t, string(goldenData), resp.Body,
+ "OpenAPI 3.1 spec does not match golden file. Run 'go test -update-openapi-spec' to update.")
+
+ // Verify version field is 3.1.0.
+ var spec map[string]any
+ err = yaml.Unmarshal([]byte(resp.Body), &spec)
+ require.NoError(t, err)
+ require.Equal(t, "3.1.0", spec["openapi"], "OpenAPI version should be 3.1.0")
+
+ // Verify /notifications/live is NOT present in 3.1 spec.
+ paths := spec["paths"].(map[string]any)
+ _, found := paths["/notifications/live"]
+ require.False(t, found, "/notifications/live should not be in OpenAPI 3.1 spec")
+}
+
+// TestOpenAPIGolden_3_2 verifies that the OpenAPI 3.2 spec matches the golden file.
+func TestOpenAPIGolden_3_2(t *testing.T) {
+ // Create an API instance to serve the OpenAPI spec.
+ api := newTestAPI(t, testhelpers.APIConfig{})
+
+ // Fetch the OpenAPI 3.2 spec from the API with query parameter.
+ resp := testhelpers.GET(t, api, "/api/v1/openapi.yaml?openapi_version=3.2")
+ require.Equal(t, 200, resp.StatusCode, "expected HTTP 200 for OpenAPI spec endpoint")
+ require.NotEmpty(t, resp.Body, "OpenAPI spec should not be empty")
+
+ goldenPath := filepath.Join("testdata", "openapi_3.2_golden.yaml")
+
+ if *updateOpenAPISpec {
+ // Update mode: write the current spec to the golden file.
+ t.Logf("Updating golden file: %s", goldenPath)
+
+ // Ensure the testdata directory exists.
+ err := os.MkdirAll(filepath.Dir(goldenPath), 0o755)
+ require.NoError(t, err, "failed to create testdata directory")
+
+ // Write the golden file.
+ err = os.WriteFile(goldenPath, []byte(resp.Body), 0o644)
+ require.NoError(t, err, "failed to write golden file")
+
+ t.Logf("Golden file updated successfully")
+ return
+ }
+
+ // Comparison mode: verify the spec matches the golden file.
+ goldenData, err := os.ReadFile(goldenPath)
+ require.NoError(t, err, "failed to read golden file (run with -update-openapi-spec to generate it)")
+
+ require.Equal(t, string(goldenData), resp.Body,
+ "OpenAPI 3.2 spec does not match golden file. Run 'go test -update-openapi-spec' to update.")
+
+ // Verify version field is 3.2.0.
+ var spec map[string]any
+ err = yaml.Unmarshal([]byte(resp.Body), &spec)
+ require.NoError(t, err)
+ require.Equal(t, "3.2.0", spec["openapi"], "OpenAPI version should be 3.2.0")
+
+ // Verify /notifications/live IS present in 3.2 spec.
+ paths := spec["paths"].(map[string]any)
+ _, found := paths["/notifications/live"]
+ require.True(t, found, "/notifications/live should be in OpenAPI 3.2 spec")
+}
+
+// TestOpenAPIVersionSelection verifies version query parameter handling.
+func TestOpenAPIVersionSelection(t *testing.T) {
+ api := newTestAPI(t, testhelpers.APIConfig{})
+
+ tests := []struct {
+ name string
+ url string
+ expectedVersion string
+ expectLivePath bool
+ }{
+ {
+ name: "default to 3.1.0",
+ url: "/api/v1/openapi.yaml",
+ expectedVersion: "3.1.0",
+ expectLivePath: false,
+ },
+ {
+ name: "explicit 3.1",
+ url: "/api/v1/openapi.yaml?openapi_version=3.1",
+ expectedVersion: "3.1.0",
+ expectLivePath: false,
+ },
+ {
+ name: "explicit 3.2",
+ url: "/api/v1/openapi.yaml?openapi_version=3.2",
+ expectedVersion: "3.2.0",
+ expectLivePath: true,
+ },
+ {
+ name: "invalid version defaults to 3.1.0",
+ url: "/api/v1/openapi.yaml?openapi_version=4.0",
+ expectedVersion: "3.1.0",
+ expectLivePath: false,
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ resp := testhelpers.GET(t, api, tc.url)
+ require.Equal(t, 200, resp.StatusCode)
+
+ var spec map[string]any
+ err := yaml.Unmarshal([]byte(resp.Body), &spec)
+ require.NoError(t, err)
+
+ require.Equal(t, tc.expectedVersion, spec["openapi"])
+
+ paths := spec["paths"].(map[string]any)
+ _, found := paths["/notifications/live"]
+ require.Equal(t, tc.expectLivePath, found)
+ })
+ }
+}
diff --git a/web/api/v1/openapi_helpers.go b/web/api/v1/openapi_helpers.go
new file mode 100644
index 0000000000..76f6001693
--- /dev/null
+++ b/web/api/v1/openapi_helpers.go
@@ -0,0 +1,343 @@
+// 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 v1
+
+import (
+ "time"
+
+ jsoniter "github.com/json-iterator/go"
+ "github.com/pb33f/libopenapi/datamodel/high/base"
+ v3 "github.com/pb33f/libopenapi/datamodel/high/v3"
+ "github.com/pb33f/libopenapi/orderedmap"
+ yaml "go.yaml.in/yaml/v4"
+
+ "github.com/prometheus/prometheus/promql"
+)
+
+// Helper functions for building common structures.
+
+// exampleTime is a reference time used for timestamp examples.
+var exampleTime = time.Date(2026, 1, 2, 13, 37, 0, 0, time.UTC)
+
+func boolPtr(b bool) *bool {
+ return &b
+}
+
+func int64Ptr(i int64) *int64 {
+ return &i
+}
+
+type example struct {
+ name string
+ value any
+}
+
+// exampleMap creates an Examples map from the provided examples.
+func exampleMap(exs []example) *orderedmap.Map[string, *base.Example] {
+ examples := orderedmap.New[string, *base.Example]()
+ for _, ex := range exs {
+ examples.Set(ex.name, &base.Example{
+ Value: createYAMLNode(ex.value),
+ })
+ }
+ return examples
+}
+
+func schemaRef(ref string) *base.SchemaProxy {
+ return base.CreateSchemaProxyRef(ref)
+}
+
+func schemaFromType(t string) *base.SchemaProxy {
+ return base.CreateSchemaProxy(&base.Schema{Type: []string{t}})
+}
+
+func stringSchema() *base.SchemaProxy {
+ return schemaFromType("string")
+}
+
+func integerSchema() *base.SchemaProxy {
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"integer"},
+ Format: "int64",
+ })
+}
+
+func stringSchemaWithDescription(description string) *base.SchemaProxy {
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"string"},
+ Description: description,
+ })
+}
+
+func stringSchemaWithDescriptionAndExample(description string, example any) *base.SchemaProxy {
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"string"},
+ Description: description,
+ Example: createYAMLNode(example),
+ })
+}
+
+func integerSchemaWithDescription(description string) *base.SchemaProxy {
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"integer"},
+ Format: "int64",
+ Description: description,
+ })
+}
+
+func integerSchemaWithDescriptionAndExample(description string, example any) *base.SchemaProxy {
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"integer"},
+ Format: "int64",
+ Description: description,
+ Example: createYAMLNode(example),
+ })
+}
+
+func stringArraySchemaWithDescription(description string) *base.SchemaProxy {
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"array"},
+ Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: stringSchema()},
+ Description: description,
+ })
+}
+
+func stringArraySchemaWithDescriptionAndExample(description string, example any) *base.SchemaProxy {
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"array"},
+ Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: stringSchema()},
+ Description: description,
+ Example: createYAMLNode(example),
+ })
+}
+
+func statusSchema() *base.SchemaProxy {
+ successNode := &yaml.Node{Kind: yaml.ScalarNode, Value: "success"}
+ errorNode := &yaml.Node{Kind: yaml.ScalarNode, Value: "error"}
+ exampleNode := &yaml.Node{Kind: yaml.ScalarNode, Value: "success"}
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"string"},
+ Enum: []*yaml.Node{successNode, errorNode},
+ Description: "Response status.",
+ Example: exampleNode,
+ })
+}
+
+func warningsSchema() *base.SchemaProxy {
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"array"},
+ Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: stringSchema()},
+ Description: "Only set if there were warnings while executing the request. There will still be data in the data field.",
+ })
+}
+
+func infosSchema() *base.SchemaProxy {
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"array"},
+ Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: stringSchema()},
+ Description: "Only set if there were info-level annotations while executing the request.",
+ })
+}
+
+func timestampSchema() *base.SchemaProxy {
+ return base.CreateSchemaProxy(&base.Schema{
+ OneOf: []*base.SchemaProxy{
+ base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"string"},
+ Format: "date-time",
+ Description: "RFC3339 timestamp.",
+ }),
+ base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"number"},
+ Format: "unixtime",
+ Description: "Unix timestamp in seconds.",
+ }),
+ },
+ Description: "Timestamp in RFC3339 format or Unix timestamp in seconds.",
+ })
+}
+
+func stringSchemaWithConstValue(value string) *base.SchemaProxy {
+ node := &yaml.Node{Kind: yaml.ScalarNode, Value: value}
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"string"},
+ Enum: []*yaml.Node{node},
+ })
+}
+
+func dateTimeSchemaWithDescription(description string) *base.SchemaProxy {
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"string"},
+ Format: "date-time",
+ Description: description,
+ })
+}
+
+func numberSchemaWithDescription(description string) *base.SchemaProxy {
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"number"},
+ Format: "double",
+ Description: description,
+ })
+}
+
+func errorResponse() *v3.Response {
+ content := orderedmap.New[string, *v3.MediaType]()
+ content.Set("application/json", &v3.MediaType{
+ Schema: schemaRef("#/components/schemas/Error"),
+ })
+ return &v3.Response{
+ Description: "Error",
+ Content: content,
+ }
+}
+
+func noContentResponse() *v3.Response {
+ return &v3.Response{Description: "No Content"}
+}
+
+func responsesNoContent() *v3.Responses {
+ codes := orderedmap.New[string, *v3.Response]()
+ codes.Set("204", noContentResponse())
+ codes.Set("default", errorResponse())
+ return &v3.Responses{Codes: codes}
+}
+
+func pathParam(name, description string, schema *base.SchemaProxy) *v3.Parameter {
+ return &v3.Parameter{
+ Name: name,
+ In: "path",
+ Description: description,
+ Required: boolPtr(true),
+ Schema: schema,
+ }
+}
+
+// createYAMLNode converts Go data to yaml.Node for use in examples.
+func createYAMLNode(data any) *yaml.Node {
+ node := &yaml.Node{}
+ bytes, _ := yaml.Marshal(data)
+ _ = yaml.Unmarshal(bytes, node)
+ return node
+}
+
+// formRequestBodyWithExamples creates a form-encoded request body with examples.
+func formRequestBodyWithExamples(schemaRef string, examples *orderedmap.Map[string, *base.Example], description string) *v3.RequestBody {
+ content := orderedmap.New[string, *v3.MediaType]()
+ mediaType := &v3.MediaType{
+ Schema: base.CreateSchemaProxyRef("#/components/schemas/" + schemaRef),
+ }
+ if examples != nil {
+ mediaType.Examples = examples
+ }
+ content.Set("application/x-www-form-urlencoded", mediaType)
+ return &v3.RequestBody{
+ Required: boolPtr(true),
+ Description: description,
+ Content: content,
+ }
+}
+
+// jsonResponseWithExamples creates a JSON response with examples.
+func jsonResponseWithExamples(schemaRef string, examples *orderedmap.Map[string, *base.Example], description string) *v3.Response {
+ content := orderedmap.New[string, *v3.MediaType]()
+ mediaType := &v3.MediaType{
+ Schema: base.CreateSchemaProxyRef("#/components/schemas/" + schemaRef),
+ }
+ if examples != nil {
+ mediaType.Examples = examples
+ }
+ content.Set("application/json", mediaType)
+ return &v3.Response{
+ Description: description,
+ Content: content,
+ }
+}
+
+// responsesWithErrorExamples creates responses with both success and error examples.
+func responsesWithErrorExamples(okSchemaRef string, successExamples, errorExamples *orderedmap.Map[string, *base.Example], successDescription, errorDescription string) *v3.Responses {
+ codes := orderedmap.New[string, *v3.Response]()
+ codes.Set("200", jsonResponseWithExamples(okSchemaRef, successExamples, successDescription))
+ codes.Set("default", jsonResponseWithExamples("Error", errorExamples, errorDescription))
+ return &v3.Responses{Codes: codes}
+}
+
+// timestampExamples returns examples for timestamp parameters (RFC3339 and epoch).
+func timestampExamples(t time.Time) []example {
+ return []example{
+ {"RFC3339", t.Format(time.RFC3339Nano)},
+ {"epoch", t.Unix()},
+ }
+}
+
+// queryParamWithExample creates a query parameter with examples.
+func queryParamWithExample(name, description string, required bool, schema *base.SchemaProxy, examples []example) *v3.Parameter {
+ param := &v3.Parameter{
+ Name: name,
+ In: "query",
+ Description: description,
+ Required: &required,
+ Explode: boolPtr(false),
+ Schema: schema,
+ }
+ if len(examples) > 0 {
+ param.Examples = exampleMap(examples)
+ }
+ return param
+}
+
+// marshalToYAMLNode marshals a value using jsoniter (production marshaling) and converts to yaml.Node.
+// The result is an inline JSON representation that preserves integer types for timestamps.
+func marshalToYAMLNode(v any) *yaml.Node {
+ jsonAPI := jsoniter.ConfigCompatibleWithStandardLibrary
+ jsonBytes, err := jsonAPI.Marshal(v)
+ if err != nil {
+ panic(err)
+ }
+ node := &yaml.Node{}
+ if err := yaml.Unmarshal(jsonBytes, node); err != nil {
+ panic(err)
+ }
+ return node
+}
+
+// vectorExample creates an example for a vector query response using production marshaling.
+func vectorExample(v promql.Vector) *yaml.Node {
+ type response struct {
+ Status string `json:"status"`
+ Data struct {
+ ResultType string `json:"resultType"`
+ Result promql.Vector `json:"result"`
+ } `json:"data"`
+ }
+ resp := response{Status: "success"}
+ resp.Data.ResultType = "vector"
+ resp.Data.Result = v
+ return marshalToYAMLNode(resp)
+}
+
+// matrixExample creates an example for a matrix query response using production marshaling.
+func matrixExample(m promql.Matrix) *yaml.Node {
+ type response struct {
+ Status string `json:"status"`
+ Data struct {
+ ResultType string `json:"resultType"`
+ Result promql.Matrix `json:"result"`
+ } `json:"data"`
+ }
+ resp := response{Status: "success"}
+ resp.Data.ResultType = "matrix"
+ resp.Data.Result = m
+ return marshalToYAMLNode(resp)
+}
diff --git a/web/api/v1/openapi_paths.go b/web/api/v1/openapi_paths.go
new file mode 100644
index 0000000000..2f5ab592f7
--- /dev/null
+++ b/web/api/v1/openapi_paths.go
@@ -0,0 +1,626 @@
+// 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.
+
+// This file defines all API path specifications including parameters, request bodies,
+// and response schemas. Each path definition corresponds to an endpoint registered in api.go.
+package v1
+
+import (
+ "time"
+
+ "github.com/pb33f/libopenapi/datamodel/high/base"
+ v3 "github.com/pb33f/libopenapi/datamodel/high/v3"
+ "github.com/pb33f/libopenapi/orderedmap"
+)
+
+// Path definition methods for API endpoints.
+
+func (*OpenAPIBuilder) queryPath() *v3.PathItem {
+ params := []*v3.Parameter{
+ queryParamWithExample("limit", "The maximum number of metrics to return.", false, integerSchema(), []example{{"example", 100}}),
+ queryParamWithExample("time", "The evaluation timestamp (optional, defaults to current time).", false, timestampSchema(), timestampExamples(exampleTime)),
+ queryParamWithExample("query", "The PromQL query to execute.", true, stringSchema(), []example{{"example", "up"}}),
+ queryParamWithExample("timeout", "Evaluation timeout. Optional. Defaults to and is capped by the value of the -query.timeout flag.", false, stringSchema(), []example{{"example", "30s"}}),
+ queryParamWithExample("lookback_delta", "Override the lookback period for this query. Optional.", false, stringSchema(), []example{{"example", "5m"}}),
+ queryParamWithExample("stats", "When provided, include query statistics in the response. The special value 'all' enables more comprehensive statistics.", false, stringSchema(), []example{{"example", "all"}}),
+ }
+ return &v3.PathItem{
+ Get: &v3.Operation{
+ OperationId: "query",
+ Summary: "Evaluate an instant query",
+ Tags: []string{"query"},
+ Parameters: params,
+ Responses: responsesWithErrorExamples("QueryOutputBody", queryResponseExamples(), errorResponseExamples(), "Query executed successfully.", "Error executing query."),
+ },
+ Post: &v3.Operation{
+ OperationId: "query-post",
+ Summary: "Evaluate an instant query",
+ Tags: []string{"query"},
+ RequestBody: formRequestBodyWithExamples("QueryPostInputBody", queryPostExamples(), "Submit an instant query. This endpoint accepts the same parameters as the GET version."),
+ Responses: responsesWithErrorExamples("QueryOutputBody", queryResponseExamples(), errorResponseExamples(), "Instant query executed successfully.", "Error executing instant query."),
+ },
+ }
+}
+
+func (*OpenAPIBuilder) queryRangePath() *v3.PathItem {
+ params := []*v3.Parameter{
+ queryParamWithExample("limit", "The maximum number of metrics to return.", false, integerSchema(), []example{{"example", 100}}),
+ queryParamWithExample("start", "The start time of the query.", true, timestampSchema(), timestampExamples(exampleTime.Add(-1*time.Hour))),
+ queryParamWithExample("end", "The end time of the query.", true, timestampSchema(), timestampExamples(exampleTime)),
+ queryParamWithExample("step", "The step size of the query.", true, stringSchema(), []example{{"example", "15s"}}),
+ queryParamWithExample("query", "The query to execute.", true, stringSchema(), []example{{"example", "rate(prometheus_http_requests_total{handler=\"/api/v1/query\"}[5m])"}}),
+ queryParamWithExample("timeout", "Evaluation timeout. Optional. Defaults to and is capped by the value of the -query.timeout flag.", false, stringSchema(), []example{{"example", "30s"}}),
+ queryParamWithExample("lookback_delta", "Override the lookback period for this query. Optional.", false, stringSchema(), []example{{"example", "5m"}}),
+ queryParamWithExample("stats", "When provided, include query statistics in the response. The special value 'all' enables more comprehensive statistics.", false, stringSchema(), []example{{"example", "all"}}),
+ }
+ return &v3.PathItem{
+ Get: &v3.Operation{
+ OperationId: "query-range",
+ Summary: "Evaluate a range query",
+ Tags: []string{"query"},
+ Parameters: params,
+ Responses: responsesWithErrorExamples("QueryRangeOutputBody", queryRangeResponseExamples(), errorResponseExamples(), "Range query executed successfully.", "Error executing range query."),
+ },
+ Post: &v3.Operation{
+ OperationId: "query-range-post",
+ Summary: "Evaluate a range query",
+ Tags: []string{"query"},
+ RequestBody: formRequestBodyWithExamples("QueryRangePostInputBody", queryRangePostExamples(), "Submit a range query. This endpoint accepts the same parameters as the GET version."),
+ Responses: responsesWithErrorExamples("QueryRangeOutputBody", queryRangeResponseExamples(), errorResponseExamples(), "Range query executed successfully.", "Error executing range query."),
+ },
+ }
+}
+
+func (*OpenAPIBuilder) queryExemplarsPath() *v3.PathItem {
+ params := []*v3.Parameter{
+ queryParamWithExample("start", "Start timestamp for exemplars query.", false, timestampSchema(), timestampExamples(exampleTime.Add(-1*time.Hour))),
+ queryParamWithExample("end", "End timestamp for exemplars query.", false, timestampSchema(), timestampExamples(exampleTime)),
+ queryParamWithExample("query", "PromQL query to extract exemplars for.", true, stringSchema(), []example{{"example", "prometheus_http_requests_total"}}),
+ }
+ return &v3.PathItem{
+ Get: &v3.Operation{
+ OperationId: "query-exemplars",
+ Summary: "Query exemplars",
+ Tags: []string{"query"},
+ Parameters: params,
+ Responses: responsesWithErrorExamples("QueryExemplarsOutputBody", queryExemplarsResponseExamples(), errorResponseExamples(), "Exemplars retrieved successfully.", "Error retrieving exemplars."),
+ },
+ Post: &v3.Operation{
+ OperationId: "query-exemplars-post",
+ Summary: "Query exemplars",
+ Tags: []string{"query"},
+ RequestBody: formRequestBodyWithExamples("QueryExemplarsPostInputBody", queryExemplarsPostExamples(), "Submit an exemplars query. This endpoint accepts the same parameters as the GET version."),
+ Responses: responsesWithErrorExamples("QueryExemplarsOutputBody", queryExemplarsResponseExamples(), errorResponseExamples(), "Exemplars query completed successfully.", "Error processing exemplars query."),
+ },
+ }
+}
+
+func (*OpenAPIBuilder) formatQueryPath() *v3.PathItem {
+ params := []*v3.Parameter{
+ queryParamWithExample("query", "PromQL expression to format.", true, stringSchema(), []example{{"example", "sum(rate(http_requests_total[5m])) by (job)"}}),
+ }
+ return &v3.PathItem{
+ Get: &v3.Operation{
+ OperationId: "format-query",
+ Summary: "Format a PromQL query",
+ Tags: []string{"query"},
+ Parameters: params,
+ Responses: responsesWithErrorExamples("FormatQueryOutputBody", formatQueryResponseExamples(), errorResponseExamples(), "Query formatted successfully.", "Error formatting query."),
+ },
+ Post: &v3.Operation{
+ OperationId: "format-query-post",
+ Summary: "Format a PromQL query",
+ Tags: []string{"query"},
+ RequestBody: formRequestBodyWithExamples("FormatQueryPostInputBody", formatQueryPostExamples(), "Submit a PromQL query to format. This endpoint accepts the same parameters as the GET version."),
+ Responses: responsesWithErrorExamples("FormatQueryOutputBody", formatQueryResponseExamples(), errorResponseExamples(), "Query formatting completed successfully.", "Error formatting query."),
+ },
+ }
+}
+
+func (*OpenAPIBuilder) parseQueryPath() *v3.PathItem {
+ params := []*v3.Parameter{
+ queryParamWithExample("query", "PromQL expression to parse.", true, stringSchema(), []example{{"example", "up{job=\"prometheus\"}"}}),
+ }
+ return &v3.PathItem{
+ Get: &v3.Operation{
+ OperationId: "parse-query",
+ Summary: "Parse a PromQL query",
+ Tags: []string{"query"},
+ Parameters: params,
+ Responses: responsesWithErrorExamples("ParseQueryOutputBody", parseQueryResponseExamples(), errorResponseExamples(), "Query parsed successfully.", "Error parsing query."),
+ },
+ Post: &v3.Operation{
+ OperationId: "parse-query-post",
+ Summary: "Parse a PromQL query",
+ Tags: []string{"query"},
+ RequestBody: formRequestBodyWithExamples("ParseQueryPostInputBody", parseQueryPostExamples(), "Submit a PromQL query to parse. This endpoint accepts the same parameters as the GET version."),
+ Responses: responsesWithErrorExamples("ParseQueryOutputBody", parseQueryResponseExamples(), errorResponseExamples(), "Query parsed successfully via POST.", "Error parsing query via POST."),
+ },
+ }
+}
+
+func (*OpenAPIBuilder) labelsPath() *v3.PathItem {
+ params := []*v3.Parameter{
+ queryParamWithExample("start", "Start timestamp for label names query.", false, timestampSchema(), timestampExamples(exampleTime.Add(-1*time.Hour))),
+ queryParamWithExample("end", "End timestamp for label names query.", false, timestampSchema(), timestampExamples(exampleTime)),
+ queryParamWithExample("match[]", "Series selector argument.", false, base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"array"},
+ Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: stringSchema()},
+ }), []example{{"example", []string{"{job=\"prometheus\"}"}}}),
+ queryParamWithExample("limit", "Maximum number of label names to return.", false, integerSchema(), []example{{"example", 100}}),
+ }
+ return &v3.PathItem{
+ Get: &v3.Operation{
+ OperationId: "labels",
+ Summary: "Get label names",
+ Tags: []string{"labels"},
+ Parameters: params,
+ Responses: responsesWithErrorExamples("LabelsOutputBody", labelsResponseExamples(), errorResponseExamples(), "Label names retrieved successfully.", "Error retrieving label names."),
+ },
+ Post: &v3.Operation{
+ OperationId: "labels-post",
+ Summary: "Get label names",
+ Tags: []string{"labels"},
+ RequestBody: formRequestBodyWithExamples("LabelsPostInputBody", labelsPostExamples(), "Submit a label names query. This endpoint accepts the same parameters as the GET version."),
+ Responses: responsesWithErrorExamples("LabelsOutputBody", labelsResponseExamples(), errorResponseExamples(), "Label names retrieved successfully via POST.", "Error retrieving label names via POST."),
+ },
+ }
+}
+
+func (*OpenAPIBuilder) labelValuesPath() *v3.PathItem {
+ params := []*v3.Parameter{
+ pathParam("name", "Label name.", stringSchema()),
+ queryParamWithExample("start", "Start timestamp for label values query.", false, timestampSchema(), timestampExamples(exampleTime.Add(-1*time.Hour))),
+ queryParamWithExample("end", "End timestamp for label values query.", false, timestampSchema(), timestampExamples(exampleTime)),
+ queryParamWithExample("match[]", "Series selector argument.", false, base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"array"},
+ Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: stringSchema()},
+ }), []example{{"example", []string{"{job=\"prometheus\"}"}}}),
+ queryParamWithExample("limit", "Maximum number of label values to return.", false, integerSchema(), []example{{"example", 1000}}),
+ }
+ return &v3.PathItem{
+ Get: &v3.Operation{
+ OperationId: "label-values",
+ Summary: "Get label values",
+ Tags: []string{"labels"},
+ Parameters: params,
+ Responses: responsesWithErrorExamples("LabelValuesOutputBody", labelValuesResponseExamples(), errorResponseExamples(), "Label values retrieved successfully.", "Error retrieving label values."),
+ },
+ }
+}
+
+func (*OpenAPIBuilder) seriesPath() *v3.PathItem {
+ params := []*v3.Parameter{
+ queryParamWithExample("start", "Start timestamp for series query.", false, timestampSchema(), timestampExamples(exampleTime.Add(-1*time.Hour))),
+ queryParamWithExample("end", "End timestamp for series query.", false, timestampSchema(), timestampExamples(exampleTime)),
+ queryParamWithExample("match[]", "Series selector argument.", true, base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"array"},
+ Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: stringSchema()},
+ }), []example{{"example", []string{"{job=\"prometheus\"}"}}}),
+ queryParamWithExample("limit", "Maximum number of series to return.", false, integerSchema(), []example{{"example", 100}}),
+ }
+ return &v3.PathItem{
+ Get: &v3.Operation{
+ OperationId: "series",
+ Summary: "Find series by label matchers",
+ Tags: []string{"series"},
+ Parameters: params,
+ Responses: responsesWithErrorExamples("SeriesOutputBody", seriesResponseExamples(), errorResponseExamples(), "Series returned matching the provided label matchers.", "Error retrieving series."),
+ },
+ Post: &v3.Operation{
+ OperationId: "series-post",
+ Summary: "Find series by label matchers",
+ Tags: []string{"series"},
+ RequestBody: formRequestBodyWithExamples("SeriesPostInputBody", seriesPostExamples(), "Submit a series query. This endpoint accepts the same parameters as the GET version."),
+ Responses: responsesWithErrorExamples("SeriesOutputBody", seriesResponseExamples(), errorResponseExamples(), "Series returned matching the provided label matchers via POST.", "Error retrieving series via POST."),
+ },
+ Delete: &v3.Operation{
+ OperationId: "delete-series",
+ Summary: "Delete series",
+ Description: "Delete series matching selectors. Note: This is deprecated, use POST /admin/tsdb/delete_series instead.",
+ Tags: []string{"series"},
+ Responses: responsesWithErrorExamples("SeriesDeleteOutputBody", seriesDeleteResponseExamples(), errorResponseExamples(), "Series marked for deletion.", "Error deleting series."),
+ },
+ }
+}
+
+func (*OpenAPIBuilder) metadataPath() *v3.PathItem {
+ params := []*v3.Parameter{
+ queryParamWithExample("limit", "The maximum number of metrics to return.", false, integerSchema(), []example{{"example", 100}}),
+ queryParamWithExample("limit_per_metric", "The maximum number of metadata entries per metric.", false, integerSchema(), []example{{"example", 10}}),
+ queryParamWithExample("metric", "A metric name to filter metadata for.", false, stringSchema(), []example{{"example", "http_requests_total"}}),
+ }
+ return &v3.PathItem{
+ Get: &v3.Operation{
+ OperationId: "get-metadata",
+ Summary: "Get metadata",
+ Tags: []string{"metadata"},
+ Parameters: params,
+ Responses: responsesWithErrorExamples("MetadataOutputBody", metadataResponseExamples(), errorResponseExamples(), "Metric metadata retrieved successfully.", "Error retrieving metadata."),
+ },
+ }
+}
+
+func (*OpenAPIBuilder) scrapePoolsPath() *v3.PathItem {
+ return &v3.PathItem{
+ Get: &v3.Operation{
+ OperationId: "get-scrape-pools",
+ Summary: "Get scrape pools",
+ Tags: []string{"targets"},
+ Responses: responsesWithErrorExamples("ScrapePoolsOutputBody", scrapePoolsResponseExamples(), errorResponseExamples(), "Scrape pools retrieved successfully.", "Error retrieving scrape pools."),
+ },
+ }
+}
+
+func (*OpenAPIBuilder) targetsPath() *v3.PathItem {
+ params := []*v3.Parameter{
+ queryParamWithExample("scrapePool", "Filter targets by scrape pool name.", false, stringSchema(), []example{{"example", "prometheus"}}),
+ queryParamWithExample("state", "Filter by state: active, dropped, or any.", false, stringSchema(), []example{{"example", "active"}}),
+ }
+ return &v3.PathItem{
+ Get: &v3.Operation{
+ OperationId: "get-targets",
+ Summary: "Get targets",
+ Tags: []string{"targets"},
+ Parameters: params,
+ Responses: responsesWithErrorExamples("TargetsOutputBody", targetsResponseExamples(), errorResponseExamples(), "Target discovery information retrieved successfully.", "Error retrieving targets."),
+ },
+ }
+}
+
+func (*OpenAPIBuilder) targetsMetadataPath() *v3.PathItem {
+ params := []*v3.Parameter{
+ queryParamWithExample("match_target", "Label selector to filter targets.", false, stringSchema(), []example{{"example", "{job=\"prometheus\"}"}}),
+ queryParamWithExample("metric", "Metric name to retrieve metadata for.", false, stringSchema(), []example{{"example", "http_requests_total"}}),
+ queryParamWithExample("limit", "Maximum number of targets to match.", false, integerSchema(), []example{{"example", 10}}),
+ }
+ return &v3.PathItem{
+ Get: &v3.Operation{
+ OperationId: "get-targets-metadata",
+ Summary: "Get targets metadata",
+ Tags: []string{"targets"},
+ Parameters: params,
+ Responses: responsesWithErrorExamples("TargetMetadataOutputBody", targetsMetadataResponseExamples(), errorResponseExamples(), "Target metadata retrieved successfully.", "Error retrieving target metadata."),
+ },
+ }
+}
+
+func (*OpenAPIBuilder) targetsRelabelStepsPath() *v3.PathItem {
+ params := []*v3.Parameter{
+ queryParamWithExample("scrapePool", "Name of the scrape pool.", true, stringSchema(), []example{{"example", "prometheus"}}),
+ queryParamWithExample("labels", "JSON-encoded labels to apply relabel rules to.", true, stringSchema(), []example{{"example", "{\"__address__\":\"localhost:9090\",\"job\":\"prometheus\"}"}}),
+ }
+ return &v3.PathItem{
+ Get: &v3.Operation{
+ OperationId: "get-targets-relabel-steps",
+ Summary: "Get targets relabel steps",
+ Tags: []string{"targets"},
+ Parameters: params,
+ Responses: responsesWithErrorExamples("TargetRelabelStepsOutputBody", targetsRelabelStepsResponseExamples(), errorResponseExamples(), "Relabel steps retrieved successfully.", "Error retrieving relabel steps."),
+ },
+ }
+}
+
+func (*OpenAPIBuilder) rulesPath() *v3.PathItem {
+ params := []*v3.Parameter{
+ queryParamWithExample("type", "Filter by rule type: alert or record.", false, stringSchema(), []example{{"example", "alert"}}),
+ queryParamWithExample("rule_name[]", "Filter by rule name.", false, base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"array"},
+ Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: stringSchema()},
+ }), []example{{"example", []string{"HighErrorRate"}}}),
+ queryParamWithExample("rule_group[]", "Filter by rule group name.", false, base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"array"},
+ Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: stringSchema()},
+ }), []example{{"example", []string{"example_alerts"}}}),
+ queryParamWithExample("file[]", "Filter by file path.", false, base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"array"},
+ Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: stringSchema()},
+ }), []example{{"example", []string{"/etc/prometheus/rules.yml"}}}),
+ queryParamWithExample("match[]", "Label matchers to filter rules.", false, base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"array"},
+ Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: stringSchema()},
+ }), []example{{"example", []string{"{severity=\"critical\"}"}}}),
+ queryParamWithExample("exclude_alerts", "Exclude active alerts from response.", false, stringSchema(), []example{{"example", "false"}}),
+ queryParamWithExample("group_limit", "Maximum number of rule groups to return.", false, integerSchema(), []example{{"example", 100}}),
+ queryParamWithExample("group_next_token", "Pagination token for next page.", false, stringSchema(), []example{{"example", "abc123"}}),
+ }
+ return &v3.PathItem{
+ Get: &v3.Operation{
+ OperationId: "rules",
+ Summary: "Get alerting and recording rules",
+ Tags: []string{"rules"},
+ Parameters: params,
+ Responses: responsesWithErrorExamples("RulesOutputBody", rulesResponseExamples(), errorResponseExamples(), "Rules retrieved successfully.", "Error retrieving rules."),
+ },
+ }
+}
+
+func (*OpenAPIBuilder) alertsPath() *v3.PathItem {
+ return &v3.PathItem{
+ Get: &v3.Operation{
+ OperationId: "alerts",
+ Summary: "Get active alerts",
+ Tags: []string{"alerts"},
+ Responses: responsesWithErrorExamples("AlertsOutputBody", alertsResponseExamples(), errorResponseExamples(), "Active alerts retrieved successfully.", "Error retrieving alerts."),
+ },
+ }
+}
+
+func (*OpenAPIBuilder) alertmanagersPath() *v3.PathItem {
+ return &v3.PathItem{
+ Get: &v3.Operation{
+ OperationId: "alertmanagers",
+ Summary: "Get Alertmanager discovery",
+ Tags: []string{"alerts"},
+ Responses: responsesWithErrorExamples("AlertmanagersOutputBody", alertmanagersResponseExamples(), errorResponseExamples(), "Alertmanager targets retrieved successfully.", "Error retrieving Alertmanager targets."),
+ },
+ }
+}
+
+func (*OpenAPIBuilder) statusConfigPath() *v3.PathItem {
+ return &v3.PathItem{
+ Get: &v3.Operation{
+ OperationId: "get-status-config",
+ Summary: "Get status config",
+ Tags: []string{"status"},
+ Responses: responsesWithErrorExamples("StatusConfigOutputBody", statusConfigResponseExamples(), errorResponseExamples(), "Configuration retrieved successfully.", "Error retrieving configuration."),
+ },
+ }
+}
+
+func (*OpenAPIBuilder) statusRuntimeInfoPath() *v3.PathItem {
+ return &v3.PathItem{
+ Get: &v3.Operation{
+ OperationId: "get-status-runtimeinfo",
+ Summary: "Get status runtimeinfo",
+ Tags: []string{"status"},
+ Responses: responsesWithErrorExamples("StatusRuntimeInfoOutputBody", statusRuntimeInfoResponseExamples(), errorResponseExamples(), "Runtime information retrieved successfully.", "Error retrieving runtime information."),
+ },
+ }
+}
+
+func (*OpenAPIBuilder) statusBuildInfoPath() *v3.PathItem {
+ return &v3.PathItem{
+ Get: &v3.Operation{
+ OperationId: "get-status-buildinfo",
+ Summary: "Get status buildinfo",
+ Tags: []string{"status"},
+ Responses: responsesWithErrorExamples("StatusBuildInfoOutputBody", statusBuildInfoResponseExamples(), errorResponseExamples(), "Build information retrieved successfully.", "Error retrieving build information."),
+ },
+ }
+}
+
+func (*OpenAPIBuilder) statusFlagsPath() *v3.PathItem {
+ return &v3.PathItem{
+ Get: &v3.Operation{
+ OperationId: "get-status-flags",
+ Summary: "Get status flags",
+ Tags: []string{"status"},
+ Responses: responsesWithErrorExamples("StatusFlagsOutputBody", statusFlagsResponseExamples(), errorResponseExamples(), "Command-line flags retrieved successfully.", "Error retrieving flags."),
+ },
+ }
+}
+
+func (*OpenAPIBuilder) statusTSDBPath() *v3.PathItem {
+ params := []*v3.Parameter{
+ queryParamWithExample("limit", "The maximum number of items to return per category.", false, integerSchema(), []example{{"example", 10}}),
+ }
+ return &v3.PathItem{
+ Get: &v3.Operation{
+ OperationId: "status-tsdb",
+ Summary: "Get TSDB status",
+ Tags: []string{"status"},
+ Parameters: params,
+ Responses: responsesWithErrorExamples("StatusTSDBOutputBody", statusTSDBResponseExamples(), errorResponseExamples(), "TSDB status retrieved successfully.", "Error retrieving TSDB status."),
+ },
+ }
+}
+
+func (*OpenAPIBuilder) statusTSDBBlocksPath() *v3.PathItem {
+ return &v3.PathItem{
+ Get: &v3.Operation{
+ OperationId: "status-tsdb-blocks",
+ Summary: "Get TSDB blocks information",
+ Tags: []string{"status"},
+ Responses: responsesWithErrorExamples("StatusTSDBBlocksOutputBody", statusTSDBBlocksResponseExamples(), errorResponseExamples(), "TSDB blocks information retrieved successfully.", "Error retrieving TSDB blocks."),
+ },
+ }
+}
+
+func (*OpenAPIBuilder) statusWALReplayPath() *v3.PathItem {
+ return &v3.PathItem{
+ Get: &v3.Operation{
+ OperationId: "get-status-walreplay",
+ Summary: "Get status walreplay",
+ Tags: []string{"status"},
+ Responses: responsesWithErrorExamples("StatusWALReplayOutputBody", statusWALReplayResponseExamples(), errorResponseExamples(), "WAL replay status retrieved successfully.", "Error retrieving WAL replay status."),
+ },
+ }
+}
+
+func (*OpenAPIBuilder) adminDeleteSeriesPath() *v3.PathItem {
+ params := []*v3.Parameter{
+ queryParamWithExample("match[]", "Series selectors to identify series to delete.", true, base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"array"},
+ Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: stringSchema()},
+ }), []example{{"example", []string{"{__name__=~\"test.*\"}"}}}),
+ queryParamWithExample("start", "Start timestamp for deletion.", false, timestampSchema(), timestampExamples(exampleTime.Add(-1*time.Hour))),
+ queryParamWithExample("end", "End timestamp for deletion.", false, timestampSchema(), timestampExamples(exampleTime)),
+ }
+ return &v3.PathItem{
+ Post: &v3.Operation{
+ OperationId: "deleteSeriesPost",
+ Summary: "Delete series matching selectors",
+ Description: "Deletes data for a selection of series in a time range.",
+ Tags: []string{"admin"},
+ Parameters: params,
+ Responses: responsesWithErrorExamples("DeleteSeriesOutputBody", deleteSeriesResponseExamples(), errorResponseExamples(), "Series deleted successfully.", "Error deleting series."),
+ },
+ Put: &v3.Operation{
+ OperationId: "deleteSeriesPut",
+ Summary: "Delete series matching selectors via PUT",
+ Description: "Deletes data for a selection of series in a time range using PUT method.",
+ Tags: []string{"admin"},
+ Parameters: params,
+ Responses: responsesWithErrorExamples("DeleteSeriesOutputBody", deleteSeriesResponseExamples(), errorResponseExamples(), "Series deleted successfully via PUT.", "Error deleting series via PUT."),
+ },
+ }
+}
+
+func (*OpenAPIBuilder) adminCleanTombstonesPath() *v3.PathItem {
+ return &v3.PathItem{
+ Post: &v3.Operation{
+ OperationId: "cleanTombstonesPost",
+ Summary: "Clean tombstones in the TSDB",
+ Description: "Removes deleted data from disk and cleans up existing tombstones.",
+ Tags: []string{"admin"},
+ Responses: responsesWithErrorExamples("CleanTombstonesOutputBody", cleanTombstonesResponseExamples(), errorResponseExamples(), "Tombstones cleaned successfully.", "Error cleaning tombstones."),
+ },
+ Put: &v3.Operation{
+ OperationId: "cleanTombstonesPut",
+ Summary: "Clean tombstones in the TSDB via PUT",
+ Description: "Removes deleted data from disk and cleans up existing tombstones using PUT method.",
+ Tags: []string{"admin"},
+ Responses: responsesWithErrorExamples("CleanTombstonesOutputBody", cleanTombstonesResponseExamples(), errorResponseExamples(), "Tombstones cleaned successfully via PUT.", "Error cleaning tombstones via PUT."),
+ },
+ }
+}
+
+func (*OpenAPIBuilder) adminSnapshotPath() *v3.PathItem {
+ params := []*v3.Parameter{
+ queryParamWithExample("skip_head", "If true, do not snapshot data in the head block.", false, stringSchema(), []example{{"example", "false"}}),
+ }
+ return &v3.PathItem{
+ Post: &v3.Operation{
+ OperationId: "snapshotPost",
+ Summary: "Create a snapshot of the TSDB",
+ Description: "Creates a snapshot of all current data.",
+ Tags: []string{"admin"},
+ Parameters: params,
+ Responses: responsesWithErrorExamples("SnapshotOutputBody", snapshotResponseExamples(), errorResponseExamples(), "Snapshot created successfully.", "Error creating snapshot."),
+ },
+ Put: &v3.Operation{
+ OperationId: "snapshotPut",
+ Summary: "Create a snapshot of the TSDB via PUT",
+ Description: "Creates a snapshot of all current data using PUT method.",
+ Tags: []string{"admin"},
+ Parameters: params,
+ Responses: responsesWithErrorExamples("SnapshotOutputBody", snapshotResponseExamples(), errorResponseExamples(), "Snapshot created successfully via PUT.", "Error creating snapshot via PUT."),
+ },
+ }
+}
+
+func (*OpenAPIBuilder) remoteReadPath() *v3.PathItem {
+ return &v3.PathItem{
+ Post: &v3.Operation{
+ OperationId: "remoteRead",
+ Summary: "Remote read endpoint",
+ Description: "Prometheus remote read endpoint for federated queries. Accepts and returns Protocol Buffer encoded data.",
+ Tags: []string{"remote"},
+ Responses: responsesNoContent(),
+ },
+ }
+}
+
+func (*OpenAPIBuilder) remoteWritePath() *v3.PathItem {
+ return &v3.PathItem{
+ Post: &v3.Operation{
+ OperationId: "remoteWrite",
+ Summary: "Remote write endpoint",
+ Description: "Prometheus remote write endpoint for sending metrics. Accepts Protocol Buffer encoded write requests.",
+ Tags: []string{"remote"},
+ Responses: responsesNoContent(),
+ },
+ }
+}
+
+func (*OpenAPIBuilder) otlpWritePath() *v3.PathItem {
+ return &v3.PathItem{
+ Post: &v3.Operation{
+ OperationId: "otlpWrite",
+ Summary: "OTLP metrics write endpoint",
+ Description: "OpenTelemetry Protocol metrics ingestion endpoint. Accepts OTLP/HTTP metrics in Protocol Buffer format.",
+ Tags: []string{"otlp"},
+ Responses: responsesNoContent(),
+ },
+ }
+}
+
+func (*OpenAPIBuilder) notificationsPath() *v3.PathItem {
+ return &v3.PathItem{
+ Get: &v3.Operation{
+ OperationId: "get-notifications",
+ Summary: "Get notifications",
+ Tags: []string{"notifications"},
+ Responses: responsesWithErrorExamples("NotificationsOutputBody", notificationsResponseExamples(), errorResponseExamples(), "Notifications retrieved successfully.", "Error retrieving notifications."),
+ },
+ }
+}
+
+// notificationsLivePath defines the /notifications/live endpoint.
+// This endpoint uses OpenAPI 3.2's itemSchema feature for documenting SSE streams.
+// It is excluded from the OpenAPI 3.1 specification.
+func (*OpenAPIBuilder) notificationsLivePath() *v3.PathItem {
+ codes := orderedmap.New[string, *v3.Response]()
+ content := orderedmap.New[string, *v3.MediaType]()
+
+ // Create a schema for the SSE message structure.
+ // Each SSE message has a 'data' field containing JSON.
+ sseItemProps := orderedmap.New[string, *base.SchemaProxy]()
+ sseItemProps.Set("data", base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"string"},
+ Description: "SSE data field containing JSON-encoded notification.",
+ ContentMediaType: "application/json",
+ ContentSchema: schemaRef("#/components/schemas/Notification"),
+ }))
+
+ content.Set("text/event-stream", &v3.MediaType{
+ // Use ItemSchema (OpenAPI 3.2) instead of Schema to describe each SSE message.
+ ItemSchema: base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Title: "Server Sent Event Message",
+ Description: "A single SSE message. The data field contains a JSON-encoded Notification object.",
+ Properties: sseItemProps,
+ Required: []string{"data"},
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
+ }),
+ Examples: notificationLiveExamples(),
+ })
+
+ codes.Set("200", &v3.Response{
+ Description: "Server-sent events stream established.",
+ Content: content,
+ })
+ codes.Set("default", errorResponse())
+
+ return &v3.PathItem{
+ Get: &v3.Operation{
+ OperationId: "notifications-live",
+ Summary: "Stream live notifications via Server-Sent Events",
+ Description: "Subscribe to real-time server notifications using SSE. Each event contains a JSON-encoded Notification object in the data field.",
+ Tags: []string{"notifications"},
+ Responses: &v3.Responses{Codes: codes},
+ },
+ }
+}
+
+func (*OpenAPIBuilder) featuresPath() *v3.PathItem {
+ return &v3.PathItem{
+ Get: &v3.Operation{
+ OperationId: "get-features",
+ Summary: "Get features",
+ Tags: []string{"features"},
+ Responses: responsesWithErrorExamples("FeaturesOutputBody", featuresResponseExamples(), errorResponseExamples(), "Feature flags retrieved successfully.", "Error retrieving features."),
+ },
+ }
+}
diff --git a/web/api/v1/openapi_schemas.go b/web/api/v1/openapi_schemas.go
new file mode 100644
index 0000000000..de39b43e37
--- /dev/null
+++ b/web/api/v1/openapi_schemas.go
@@ -0,0 +1,1296 @@
+// 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.
+
+// This file defines all OpenAPI schema definitions for API request and response types.
+// Schemas are organized by functional area: query, labels, series, metadata, targets,
+// rules, alerts, and status endpoints.
+package v1
+
+import (
+ "github.com/pb33f/libopenapi/datamodel/high/base"
+ v3 "github.com/pb33f/libopenapi/datamodel/high/v3"
+ "github.com/pb33f/libopenapi/orderedmap"
+)
+
+// Schema definitions and components builder.
+
+func (b *OpenAPIBuilder) buildComponents() *v3.Components {
+ schemas := orderedmap.New[string, *base.SchemaProxy]()
+
+ // Core schemas.
+ schemas.Set("Error", b.errorSchema())
+ schemas.Set("Labels", b.labelsSchema())
+
+ // Query schemas.
+ schemas.Set("QueryOutputBody", b.responseBodySchema("QueryData", "Response body for instant query."))
+ schemas.Set("QueryRangeOutputBody", b.responseBodySchema("QueryData", "Response body for range query."))
+ schemas.Set("QueryPostInputBody", b.queryPostInputBodySchema())
+ schemas.Set("QueryRangePostInputBody", b.queryRangePostInputBodySchema())
+ schemas.Set("QueryExemplarsOutputBody", b.simpleResponseBodySchema())
+ schemas.Set("QueryExemplarsPostInputBody", b.queryExemplarsPostInputBodySchema())
+ schemas.Set("FormatQueryOutputBody", b.formatQueryOutputBodySchema())
+ schemas.Set("FormatQueryPostInputBody", b.formatQueryPostInputBodySchema())
+ schemas.Set("ParseQueryOutputBody", b.simpleResponseBodySchema())
+ schemas.Set("ParseQueryPostInputBody", b.parseQueryPostInputBodySchema())
+ schemas.Set("QueryData", b.queryDataSchema())
+ schemas.Set("QueryStats", b.queryStatsSchema())
+ schemas.Set("FloatSample", b.floatSampleSchema())
+ schemas.Set("HistogramSample", b.histogramSampleSchema())
+ schemas.Set("FloatSeries", b.floatSeriesSchema())
+ schemas.Set("HistogramSeries", b.histogramSeriesSchema())
+ schemas.Set("HistogramValue", b.histogramValueSchema())
+
+ // Label schemas.
+ schemas.Set("LabelsOutputBody", b.stringArrayResponseBodySchema())
+ schemas.Set("LabelsPostInputBody", b.labelsPostInputBodySchema())
+ schemas.Set("LabelValuesOutputBody", b.stringArrayResponseBodySchema())
+
+ // Series schemas.
+ schemas.Set("SeriesOutputBody", b.labelsArrayResponseBodySchema())
+ schemas.Set("SeriesPostInputBody", b.seriesPostInputBodySchema())
+ schemas.Set("SeriesDeleteOutputBody", b.simpleResponseBodySchema())
+
+ // Metadata schemas.
+ schemas.Set("Metadata", b.metadataSchema())
+ schemas.Set("MetadataOutputBody", b.metadataOutputBodySchema())
+ schemas.Set("MetricMetadata", b.metricMetadataSchema())
+
+ // Target schemas.
+ schemas.Set("Target", b.targetSchema())
+ schemas.Set("DroppedTarget", b.droppedTargetSchema())
+ schemas.Set("TargetDiscovery", b.targetDiscoverySchema())
+ schemas.Set("TargetsOutputBody", b.refResponseBodySchema("TargetDiscovery", "Response body for targets endpoint."))
+ schemas.Set("TargetMetadataOutputBody", b.metricMetadataArrayResponseBodySchema())
+ schemas.Set("ScrapePoolsDiscovery", b.scrapePoolsDiscoverySchema())
+ schemas.Set("ScrapePoolsOutputBody", b.refResponseBodySchema("ScrapePoolsDiscovery", "Response body for scrape pools endpoint."))
+
+ // Relabel schemas.
+ schemas.Set("Config", b.configSchema())
+ schemas.Set("RelabelStep", b.relabelStepSchema())
+ schemas.Set("RelabelStepsResponse", b.relabelStepsResponseSchema())
+ schemas.Set("TargetRelabelStepsOutputBody", b.refResponseBodySchema("RelabelStepsResponse", "Response body for target relabel steps endpoint."))
+
+ // Rule schemas.
+ schemas.Set("RuleGroup", b.ruleGroupSchema())
+ schemas.Set("RuleDiscovery", b.ruleDiscoverySchema())
+ schemas.Set("RulesOutputBody", b.refResponseBodySchema("RuleDiscovery", "Response body for rules endpoint."))
+
+ // Alert schemas.
+ schemas.Set("Alert", b.alertSchema())
+ schemas.Set("AlertDiscovery", b.alertDiscoverySchema())
+ schemas.Set("AlertsOutputBody", b.refResponseBodySchema("AlertDiscovery", "Response body for alerts endpoint."))
+ schemas.Set("AlertmanagerTarget", b.alertmanagerTargetSchema())
+ schemas.Set("AlertmanagerDiscovery", b.alertmanagerDiscoverySchema())
+ schemas.Set("AlertmanagersOutputBody", b.refResponseBodySchema("AlertmanagerDiscovery", "Response body for alertmanagers endpoint."))
+
+ // Status schemas.
+ schemas.Set("StatusConfigData", b.statusConfigDataSchema())
+ schemas.Set("StatusConfigOutputBody", b.refResponseBodySchema("StatusConfigData", "Response body for status config endpoint."))
+ schemas.Set("RuntimeInfo", b.runtimeInfoSchema())
+ schemas.Set("StatusRuntimeInfoOutputBody", b.refResponseBodySchema("RuntimeInfo", "Response body for status runtime info endpoint."))
+ schemas.Set("PrometheusVersion", b.prometheusVersionSchema())
+ schemas.Set("StatusBuildInfoOutputBody", b.refResponseBodySchema("PrometheusVersion", "Response body for status build info endpoint."))
+ schemas.Set("StatusFlagsOutputBody", b.statusFlagsOutputBodySchema())
+ schemas.Set("HeadStats", b.headStatsSchema())
+ schemas.Set("TSDBStat", b.tsdbStatSchema())
+ schemas.Set("TSDBStatus", b.tsdbStatusSchema())
+ schemas.Set("StatusTSDBOutputBody", b.refResponseBodySchema("TSDBStatus", "Response body for status TSDB endpoint."))
+ schemas.Set("BlockDesc", b.blockDescSchema())
+ schemas.Set("BlockStats", b.blockStatsSchema())
+ schemas.Set("BlockMetaCompaction", b.blockMetaCompactionSchema())
+ schemas.Set("BlockMeta", b.blockMetaSchema())
+ schemas.Set("StatusTSDBBlocksData", b.statusTSDBBlocksDataSchema())
+ schemas.Set("StatusTSDBBlocksOutputBody", b.refResponseBodySchema("StatusTSDBBlocksData", "Response body for status TSDB blocks endpoint."))
+ schemas.Set("StatusWALReplayData", b.statusWALReplayDataSchema())
+ schemas.Set("StatusWALReplayOutputBody", b.refResponseBodySchema("StatusWALReplayData", "Response body for status WAL replay endpoint."))
+
+ // Admin schemas.
+ schemas.Set("DeleteSeriesOutputBody", b.statusOnlyResponseBodySchema())
+ schemas.Set("CleanTombstonesOutputBody", b.statusOnlyResponseBodySchema())
+ schemas.Set("DataStruct", b.dataStructSchema())
+ schemas.Set("SnapshotOutputBody", b.refResponseBodySchema("DataStruct", "Response body for snapshot endpoint."))
+
+ // Notification schemas.
+ schemas.Set("Notification", b.notificationSchema())
+ schemas.Set("NotificationsOutputBody", b.notificationArrayResponseBodySchema())
+
+ // Features schema.
+ schemas.Set("FeaturesOutputBody", b.simpleResponseBodySchema())
+
+ return &v3.Components{Schemas: schemas}
+}
+
+// Schema definitions using high-level structs.
+
+func (*OpenAPIBuilder) errorSchema() *base.SchemaProxy {
+ props := orderedmap.New[string, *base.SchemaProxy]()
+ props.Set("status", statusSchema())
+ props.Set("errorType", stringSchemaWithDescriptionAndExample("Type of error that occurred.", "bad_data"))
+ props.Set("error", stringSchemaWithDescriptionAndExample("Human-readable error message.", "invalid parameter"))
+
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Description: "Error response.",
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
+ Required: []string{"status", "errorType", "error"},
+ Properties: props,
+ })
+}
+
+func (*OpenAPIBuilder) labelsSchema() *base.SchemaProxy {
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Description: "Label set represented as a key-value map.",
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: true},
+ })
+}
+
+func (*OpenAPIBuilder) responseBodySchema(dataSchemaRef, description string) *base.SchemaProxy {
+ props := orderedmap.New[string, *base.SchemaProxy]()
+ props.Set("status", statusSchema())
+ props.Set("data", schemaRef("#/components/schemas/"+dataSchemaRef))
+ props.Set("warnings", warningsSchema())
+ props.Set("infos", infosSchema())
+
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Description: description,
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
+ Required: []string{"status", "data"},
+ Properties: props,
+ })
+}
+
+func (b *OpenAPIBuilder) refResponseBodySchema(dataSchemaRef, description string) *base.SchemaProxy {
+ return b.responseBodySchema(dataSchemaRef, description)
+}
+
+func (*OpenAPIBuilder) simpleResponseBodySchema() *base.SchemaProxy {
+ props := orderedmap.New[string, *base.SchemaProxy]()
+ props.Set("status", statusSchema())
+ props.Set("data", base.CreateSchemaProxy(&base.Schema{
+ Description: "Response data (structure varies by endpoint).",
+ Example: createYAMLNode(map[string]any{"result": "ok"}),
+ }))
+ props.Set("warnings", warningsSchema())
+ props.Set("infos", infosSchema())
+
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Description: "Generic response body.",
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
+ Required: []string{"status", "data"},
+ Properties: props,
+ })
+}
+
+func (*OpenAPIBuilder) statusOnlyResponseBodySchema() *base.SchemaProxy {
+ props := orderedmap.New[string, *base.SchemaProxy]()
+ props.Set("status", statusSchema())
+ props.Set("warnings", warningsSchema())
+ props.Set("infos", infosSchema())
+
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Description: "Response body containing only status.",
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
+ Required: []string{"status"},
+ Properties: props,
+ })
+}
+
+func (*OpenAPIBuilder) stringArrayResponseBodySchema() *base.SchemaProxy {
+ props := orderedmap.New[string, *base.SchemaProxy]()
+ props.Set("status", statusSchema())
+ props.Set("data", base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"array"},
+ Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: stringSchema()},
+ Example: createYAMLNode([]string{"__name__", "job", "instance"}),
+ }))
+ props.Set("warnings", warningsSchema())
+ props.Set("infos", infosSchema())
+
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Description: "Response body with an array of strings.",
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
+ Required: []string{"status", "data"},
+ Properties: props,
+ })
+}
+
+func (*OpenAPIBuilder) labelsArrayResponseBodySchema() *base.SchemaProxy {
+ props := orderedmap.New[string, *base.SchemaProxy]()
+ props.Set("status", statusSchema())
+ props.Set("data", base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"array"},
+ Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: schemaRef("#/components/schemas/Labels")},
+ Example: createYAMLNode([]map[string]string{{"__name__": "up", "job": "prometheus", "instance": "localhost:9090"}}),
+ }))
+ props.Set("warnings", warningsSchema())
+ props.Set("infos", infosSchema())
+
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Description: "Response body with an array of label sets.",
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
+ Required: []string{"status", "data"},
+ Properties: props,
+ })
+}
+
+func (*OpenAPIBuilder) metricMetadataArrayResponseBodySchema() *base.SchemaProxy {
+ props := orderedmap.New[string, *base.SchemaProxy]()
+ props.Set("status", statusSchema())
+ props.Set("data", base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"array"},
+ Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: schemaRef("#/components/schemas/MetricMetadata")},
+ Example: createYAMLNode([]map[string]any{
+ {
+ "target": map[string]string{
+ "instance": "localhost:9090",
+ "job": "prometheus",
+ },
+ "metric": "up",
+ "type": "gauge",
+ "help": "The current health status of the target",
+ "unit": "",
+ },
+ }),
+ }))
+ props.Set("warnings", warningsSchema())
+ props.Set("infos", infosSchema())
+
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Description: "Response body with an array of metric metadata.",
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
+ Required: []string{"status", "data"},
+ Properties: props,
+ })
+}
+
+func (*OpenAPIBuilder) notificationArrayResponseBodySchema() *base.SchemaProxy {
+ props := orderedmap.New[string, *base.SchemaProxy]()
+ props.Set("status", statusSchema())
+ props.Set("data", base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"array"},
+ Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: schemaRef("#/components/schemas/Notification")},
+ Example: createYAMLNode([]map[string]any{
+ {"text": "Server is running", "date": "2023-07-21T20:00:00.000Z", "active": true},
+ }),
+ }))
+ props.Set("warnings", warningsSchema())
+ props.Set("infos", infosSchema())
+
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Description: "Response body with an array of notifications.",
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
+ Required: []string{"status", "data"},
+ Properties: props,
+ })
+}
+
+func (*OpenAPIBuilder) floatSampleSchema() *base.SchemaProxy {
+ props := orderedmap.New[string, *base.SchemaProxy]()
+ props.Set("metric", schemaRef("#/components/schemas/Labels"))
+ props.Set("value", base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"array"},
+ Description: "Timestamp and float value as [unixTimestamp, stringValue].",
+ Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: base.CreateSchemaProxy(&base.Schema{
+ OneOf: []*base.SchemaProxy{
+ base.CreateSchemaProxy(&base.Schema{Type: []string{"number"}}),
+ stringSchema(),
+ },
+ })},
+ MinItems: int64Ptr(2),
+ MaxItems: int64Ptr(2),
+ Example: createYAMLNode([]any{1767436620, "1"}),
+ }))
+
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Description: "A sample with a float value.",
+ Required: []string{"metric", "value"},
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
+ Properties: props,
+ })
+}
+
+func (*OpenAPIBuilder) histogramValueSchema() *base.SchemaProxy {
+ props := orderedmap.New[string, *base.SchemaProxy]()
+ props.Set("count", stringSchemaWithDescription("Total count of observations."))
+ props.Set("sum", stringSchemaWithDescription("Sum of all observed values."))
+ props.Set("buckets", base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"array"},
+ Description: "Histogram buckets as [boundary_rule, lower, upper, count].",
+ Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"array"},
+ Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: base.CreateSchemaProxy(&base.Schema{
+ OneOf: []*base.SchemaProxy{
+ base.CreateSchemaProxy(&base.Schema{Type: []string{"number"}}),
+ stringSchema(),
+ },
+ })},
+ })},
+ }))
+
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Description: "Native histogram value representation.",
+ Required: []string{"count", "sum"},
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
+ Properties: props,
+ })
+}
+
+func (*OpenAPIBuilder) histogramSampleSchema() *base.SchemaProxy {
+ props := orderedmap.New[string, *base.SchemaProxy]()
+ props.Set("metric", schemaRef("#/components/schemas/Labels"))
+ props.Set("histogram", base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"array"},
+ Description: "Timestamp and histogram value as [unixTimestamp, histogramObject].",
+ Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: base.CreateSchemaProxy(&base.Schema{
+ OneOf: []*base.SchemaProxy{
+ base.CreateSchemaProxy(&base.Schema{Type: []string{"number"}}),
+ schemaRef("#/components/schemas/HistogramValue"),
+ },
+ })},
+ MinItems: int64Ptr(2),
+ MaxItems: int64Ptr(2),
+ Example: createYAMLNode([]any{1767436620, map[string]any{"count": "60", "sum": "120", "buckets": []any{}}}),
+ }))
+
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Description: "A sample with a native histogram value.",
+ Required: []string{"metric", "histogram"},
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
+ Properties: props,
+ })
+}
+
+func (*OpenAPIBuilder) floatSeriesSchema() *base.SchemaProxy {
+ props := orderedmap.New[string, *base.SchemaProxy]()
+ props.Set("metric", schemaRef("#/components/schemas/Labels"))
+ props.Set("values", base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"array"},
+ Description: "Array of [timestamp, stringValue] pairs for float values.",
+ Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"array"},
+ Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: base.CreateSchemaProxy(&base.Schema{
+ OneOf: []*base.SchemaProxy{
+ base.CreateSchemaProxy(&base.Schema{Type: []string{"number"}}),
+ stringSchema(),
+ },
+ })},
+ MinItems: int64Ptr(2),
+ MaxItems: int64Ptr(2),
+ })},
+ }))
+
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Description: "A time series with float values.",
+ Required: []string{"metric", "values"},
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
+ Properties: props,
+ })
+}
+
+func (*OpenAPIBuilder) histogramSeriesSchema() *base.SchemaProxy {
+ props := orderedmap.New[string, *base.SchemaProxy]()
+ props.Set("metric", schemaRef("#/components/schemas/Labels"))
+ props.Set("histograms", base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"array"},
+ Description: "Array of [timestamp, histogramObject] pairs for histogram values.",
+ Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"array"},
+ Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: base.CreateSchemaProxy(&base.Schema{
+ OneOf: []*base.SchemaProxy{
+ base.CreateSchemaProxy(&base.Schema{Type: []string{"number"}}),
+ schemaRef("#/components/schemas/HistogramValue"),
+ },
+ })},
+ MinItems: int64Ptr(2),
+ MaxItems: int64Ptr(2),
+ })},
+ }))
+
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Description: "A time series with native histogram values.",
+ Required: []string{"metric", "histograms"},
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
+ Properties: props,
+ })
+}
+
+func (*OpenAPIBuilder) queryDataSchema() *base.SchemaProxy {
+ // Vector query result.
+ vectorProps := orderedmap.New[string, *base.SchemaProxy]()
+ vectorProps.Set("resultType", stringSchemaWithConstValue("vector"))
+ vectorProps.Set("result", base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"array"},
+ Description: "Array of samples (either float or histogram).",
+ Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: base.CreateSchemaProxy(&base.Schema{
+ AnyOf: []*base.SchemaProxy{
+ schemaRef("#/components/schemas/FloatSample"),
+ schemaRef("#/components/schemas/HistogramSample"),
+ },
+ })},
+ }))
+ vectorProps.Set("stats", schemaRef("#/components/schemas/QueryStats"))
+
+ // Matrix query result.
+ matrixProps := orderedmap.New[string, *base.SchemaProxy]()
+ matrixProps.Set("resultType", stringSchemaWithConstValue("matrix"))
+ matrixProps.Set("result", base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"array"},
+ Description: "Array of time series (either float or histogram).",
+ Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: base.CreateSchemaProxy(&base.Schema{
+ AnyOf: []*base.SchemaProxy{
+ schemaRef("#/components/schemas/FloatSeries"),
+ schemaRef("#/components/schemas/HistogramSeries"),
+ },
+ })},
+ }))
+ matrixProps.Set("stats", schemaRef("#/components/schemas/QueryStats"))
+
+ // Scalar query result.
+ scalarProps := orderedmap.New[string, *base.SchemaProxy]()
+ scalarProps.Set("resultType", stringSchemaWithConstValue("scalar"))
+ scalarProps.Set("result", base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"array"},
+ Description: "Scalar value as [timestamp, stringValue].",
+ Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: base.CreateSchemaProxy(&base.Schema{
+ OneOf: []*base.SchemaProxy{
+ base.CreateSchemaProxy(&base.Schema{Type: []string{"number"}}),
+ stringSchema(),
+ },
+ })},
+ MinItems: int64Ptr(2),
+ MaxItems: int64Ptr(2),
+ }))
+ scalarProps.Set("stats", schemaRef("#/components/schemas/QueryStats"))
+
+ // String query result.
+ stringResultProps := orderedmap.New[string, *base.SchemaProxy]()
+ stringResultProps.Set("resultType", stringSchemaWithConstValue("string"))
+ stringResultProps.Set("result", base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"array"},
+ Description: "String value as [timestamp, stringValue].",
+ Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: stringSchema()},
+ MinItems: int64Ptr(2),
+ MaxItems: int64Ptr(2),
+ }))
+ stringResultProps.Set("stats", schemaRef("#/components/schemas/QueryStats"))
+
+ return base.CreateSchemaProxy(&base.Schema{
+ Description: "Query result data. The structure of 'result' depends on 'resultType'.",
+ AnyOf: []*base.SchemaProxy{
+ // resultType: vector -> result: array of samples.
+ base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Required: []string{"resultType", "result"},
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
+ Properties: vectorProps,
+ }),
+ // resultType: matrix -> result: array of series.
+ base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Required: []string{"resultType", "result"},
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
+ Properties: matrixProps,
+ }),
+ // resultType: scalar -> result: [timestamp, value].
+ base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Required: []string{"resultType", "result"},
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
+ Properties: scalarProps,
+ }),
+ // resultType: string -> result: [timestamp, stringValue].
+ base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Required: []string{"resultType", "result"},
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
+ Properties: stringResultProps,
+ }),
+ },
+ Example: createYAMLNode(map[string]any{
+ "resultType": "vector",
+ "result": []map[string]any{
+ {
+ "metric": map[string]string{"__name__": "up", "job": "prometheus"},
+ "value": []any{1627845600, "1"},
+ },
+ },
+ }),
+ })
+}
+
+func (*OpenAPIBuilder) queryStatsSchema() *base.SchemaProxy {
+ // Timings object.
+ timingsProps := orderedmap.New[string, *base.SchemaProxy]()
+ timingsProps.Set("evalTotalTime", base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"number"},
+ Description: "Total evaluation time in seconds.",
+ }))
+ timingsProps.Set("resultSortTime", base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"number"},
+ Description: "Time spent sorting results in seconds.",
+ }))
+ timingsProps.Set("queryPreparationTime", base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"number"},
+ Description: "Query preparation time in seconds.",
+ }))
+ timingsProps.Set("innerEvalTime", base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"number"},
+ Description: "Inner evaluation time in seconds.",
+ }))
+ timingsProps.Set("execQueueTime", base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"number"},
+ Description: "Execution queue wait time in seconds.",
+ }))
+ timingsProps.Set("execTotalTime", base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"number"},
+ Description: "Total execution time in seconds.",
+ }))
+
+ // Samples object.
+ samplesProps := orderedmap.New[string, *base.SchemaProxy]()
+ samplesProps.Set("totalQueryableSamples", base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"integer"},
+ Description: "Total number of samples that were queryable.",
+ }))
+ samplesProps.Set("peakSamples", base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"integer"},
+ Description: "Peak number of samples in memory.",
+ }))
+ samplesProps.Set("totalQueryableSamplesPerStep", base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"array"},
+ Description: "Total queryable samples per step (only included with stats=all).",
+ Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"array"},
+ Description: "Timestamp and sample count as [timestamp, count].",
+ Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: base.CreateSchemaProxy(&base.Schema{Type: []string{"number"}})},
+ MinItems: int64Ptr(2),
+ MaxItems: int64Ptr(2),
+ })},
+ }))
+
+ // Main stats object.
+ statsProps := orderedmap.New[string, *base.SchemaProxy]()
+ statsProps.Set("timings", base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Properties: timingsProps,
+ }))
+ statsProps.Set("samples", base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Properties: samplesProps,
+ }))
+
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Description: "Query execution statistics (included when the stats query parameter is provided).",
+ Properties: statsProps,
+ })
+}
+
+func (*OpenAPIBuilder) queryPostInputBodySchema() *base.SchemaProxy {
+ props := orderedmap.New[string, *base.SchemaProxy]()
+ props.Set("query", stringSchemaWithDescriptionAndExample("Form field: The PromQL query to execute.", "up"))
+ props.Set("time", stringSchemaWithDescriptionAndExample("Form field: The evaluation timestamp (optional, defaults to current time).", "2023-07-21T20:10:51.781Z"))
+ props.Set("limit", integerSchemaWithDescriptionAndExample("Form field: The maximum number of metrics to return.", 100))
+ props.Set("timeout", stringSchemaWithDescriptionAndExample("Form field: Evaluation timeout (optional, defaults to and is capped by the value of the -query.timeout flag).", "30s"))
+ props.Set("lookback_delta", stringSchemaWithDescriptionAndExample("Form field: Override the lookback period for this query (optional).", "5m"))
+ props.Set("stats", stringSchemaWithDescriptionAndExample("Form field: When provided, include query statistics in the response (the special value 'all' enables more comprehensive statistics).", "all"))
+
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Description: "POST request body for instant query.",
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
+ Required: []string{"query"},
+ Properties: props,
+ })
+}
+
+func (*OpenAPIBuilder) queryRangePostInputBodySchema() *base.SchemaProxy {
+ props := orderedmap.New[string, *base.SchemaProxy]()
+ props.Set("query", stringSchemaWithDescriptionAndExample("Form field: The query to execute.", "rate(http_requests_total[5m])"))
+ props.Set("start", stringSchemaWithDescriptionAndExample("Form field: The start time of the query.", "2023-07-21T20:10:30.781Z"))
+ props.Set("end", stringSchemaWithDescriptionAndExample("Form field: The end time of the query.", "2023-07-21T20:20:30.781Z"))
+ props.Set("step", stringSchemaWithDescriptionAndExample("Form field: The step size of the query.", "15s"))
+ props.Set("limit", integerSchemaWithDescriptionAndExample("Form field: The maximum number of metrics to return.", 100))
+ props.Set("timeout", stringSchemaWithDescriptionAndExample("Form field: Evaluation timeout (optional, defaults to and is capped by the value of the -query.timeout flag).", "30s"))
+ props.Set("lookback_delta", stringSchemaWithDescriptionAndExample("Form field: Override the lookback period for this query (optional).", "5m"))
+ props.Set("stats", stringSchemaWithDescriptionAndExample("Form field: When provided, include query statistics in the response (the special value 'all' enables more comprehensive statistics).", "all"))
+
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Description: "POST request body for range query.",
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
+ Required: []string{"query", "start", "end", "step"},
+ Properties: props,
+ })
+}
+
+func (*OpenAPIBuilder) queryExemplarsPostInputBodySchema() *base.SchemaProxy {
+ props := orderedmap.New[string, *base.SchemaProxy]()
+ props.Set("query", stringSchemaWithDescriptionAndExample("Form field: The query to execute.", "http_requests_total"))
+ props.Set("start", stringSchemaWithDescriptionAndExample("Form field: The start time of the query.", "2023-07-21T20:00:00.000Z"))
+ props.Set("end", stringSchemaWithDescriptionAndExample("Form field: The end time of the query.", "2023-07-21T21:00:00.000Z"))
+
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Description: "POST request body for exemplars query.",
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
+ Required: []string{"query"},
+ Properties: props,
+ })
+}
+
+func (*OpenAPIBuilder) formatQueryOutputBodySchema() *base.SchemaProxy {
+ props := orderedmap.New[string, *base.SchemaProxy]()
+ props.Set("status", statusSchema())
+ props.Set("data", stringSchemaWithDescriptionAndExample("Formatted query string.", "sum by(status) (rate(http_requests_total[5m]))"))
+ props.Set("warnings", warningsSchema())
+ props.Set("infos", infosSchema())
+
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Description: "Response body for format query endpoint.",
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
+ Required: []string{"status", "data"},
+ Properties: props,
+ })
+}
+
+func (*OpenAPIBuilder) formatQueryPostInputBodySchema() *base.SchemaProxy {
+ props := orderedmap.New[string, *base.SchemaProxy]()
+ props.Set("query", stringSchemaWithDescriptionAndExample("Form field: The query to format.", "sum(rate(http_requests_total[5m])) by (status)"))
+
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Description: "POST request body for format query.",
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
+ Required: []string{"query"},
+ Properties: props,
+ })
+}
+
+func (*OpenAPIBuilder) parseQueryPostInputBodySchema() *base.SchemaProxy {
+ props := orderedmap.New[string, *base.SchemaProxy]()
+ props.Set("query", stringSchemaWithDescriptionAndExample("Form field: The query to parse.", "sum(rate(http_requests_total[5m]))"))
+
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Description: "POST request body for parse query.",
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
+ Required: []string{"query"},
+ Properties: props,
+ })
+}
+
+func (*OpenAPIBuilder) labelsPostInputBodySchema() *base.SchemaProxy {
+ props := orderedmap.New[string, *base.SchemaProxy]()
+ props.Set("start", stringSchemaWithDescriptionAndExample("Form field: The start time of the query.", "2023-07-21T20:00:00.000Z"))
+ props.Set("end", stringSchemaWithDescriptionAndExample("Form field: The end time of the query.", "2023-07-21T21:00:00.000Z"))
+ props.Set("match[]", stringArraySchemaWithDescriptionAndExample("Form field: Series selector argument that selects the series from which to read the label names.", []string{"{job=\"prometheus\"}"}))
+ props.Set("limit", integerSchemaWithDescriptionAndExample("Form field: The maximum number of label names to return.", 100))
+
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Description: "POST request body for labels query.",
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
+ Properties: props,
+ })
+}
+
+func (*OpenAPIBuilder) seriesPostInputBodySchema() *base.SchemaProxy {
+ props := orderedmap.New[string, *base.SchemaProxy]()
+ props.Set("start", stringSchemaWithDescriptionAndExample("Form field: The start time of the query.", "2023-07-21T20:00:00.000Z"))
+ props.Set("end", stringSchemaWithDescriptionAndExample("Form field: The end time of the query.", "2023-07-21T21:00:00.000Z"))
+ props.Set("match[]", stringArraySchemaWithDescriptionAndExample("Form field: Series selector argument that selects the series to return.", []string{"{job=\"prometheus\"}"}))
+ props.Set("limit", integerSchemaWithDescriptionAndExample("Form field: The maximum number of series to return.", 100))
+
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Description: "POST request body for series query.",
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
+ Required: []string{"match[]"},
+ Properties: props,
+ })
+}
+
+func (*OpenAPIBuilder) metadataSchema() *base.SchemaProxy {
+ props := orderedmap.New[string, *base.SchemaProxy]()
+ props.Set("type", stringSchemaWithDescription("Metric type (counter, gauge, histogram, summary, or untyped)."))
+ props.Set("unit", stringSchemaWithDescription("Unit of the metric."))
+ props.Set("help", stringSchemaWithDescription("Help text describing the metric."))
+
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Description: "Metric metadata.",
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
+ Required: []string{"type", "unit", "help"},
+ Properties: props,
+ })
+}
+
+func (*OpenAPIBuilder) metadataOutputBodySchema() *base.SchemaProxy {
+ props := orderedmap.New[string, *base.SchemaProxy]()
+ props.Set("status", statusSchema())
+ props.Set("data", base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{
+ A: base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"array"},
+ Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: schemaRef("#/components/schemas/Metadata")},
+ }),
+ },
+ }))
+ props.Set("warnings", warningsSchema())
+ props.Set("infos", infosSchema())
+
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Description: "Response body for metadata endpoint.",
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
+ Required: []string{"status", "data"},
+ Properties: props,
+ })
+}
+
+func (*OpenAPIBuilder) metricMetadataSchema() *base.SchemaProxy {
+ props := orderedmap.New[string, *base.SchemaProxy]()
+ props.Set("target", schemaRef("#/components/schemas/Labels"))
+ props.Set("metric", stringSchemaWithDescription("Metric name."))
+ props.Set("type", stringSchemaWithDescription("Metric type (counter, gauge, histogram, summary, or untyped)."))
+ props.Set("help", stringSchemaWithDescription("Help text describing the metric."))
+ props.Set("unit", stringSchemaWithDescription("Unit of the metric."))
+
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Description: "Target metric metadata.",
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
+ Required: []string{"target", "type", "help", "unit"},
+ Properties: props,
+ })
+}
+
+func (*OpenAPIBuilder) targetSchema() *base.SchemaProxy {
+ props := orderedmap.New[string, *base.SchemaProxy]()
+ props.Set("discoveredLabels", schemaRef("#/components/schemas/Labels"))
+ props.Set("labels", schemaRef("#/components/schemas/Labels"))
+ props.Set("scrapePool", stringSchemaWithDescription("Name of the scrape pool."))
+ props.Set("scrapeUrl", stringSchemaWithDescription("URL of the target."))
+ props.Set("globalUrl", stringSchemaWithDescription("Global URL of the target."))
+ props.Set("lastError", stringSchemaWithDescription("Last error message from scraping."))
+ props.Set("lastScrape", dateTimeSchemaWithDescription("Timestamp of the last scrape."))
+ props.Set("lastScrapeDuration", numberSchemaWithDescription("Duration of the last scrape in seconds."))
+ props.Set("health", stringSchemaWithDescription("Health status of the target (up, down, or unknown)."))
+ props.Set("scrapeInterval", stringSchemaWithDescription("Scrape interval for this target."))
+ props.Set("scrapeTimeout", stringSchemaWithDescription("Scrape timeout for this target."))
+
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Description: "Scrape target information.",
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
+ Required: []string{"discoveredLabels", "labels", "scrapePool", "scrapeUrl", "globalUrl", "lastError", "lastScrape", "lastScrapeDuration", "health", "scrapeInterval", "scrapeTimeout"},
+ Properties: props,
+ })
+}
+
+func (*OpenAPIBuilder) droppedTargetSchema() *base.SchemaProxy {
+ props := orderedmap.New[string, *base.SchemaProxy]()
+ props.Set("discoveredLabels", schemaRef("#/components/schemas/Labels"))
+ props.Set("scrapePool", stringSchemaWithDescription("Name of the scrape pool."))
+
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Description: "Dropped target information.",
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
+ Required: []string{"discoveredLabels", "scrapePool"},
+ Properties: props,
+ })
+}
+
+func (*OpenAPIBuilder) targetDiscoverySchema() *base.SchemaProxy {
+ props := orderedmap.New[string, *base.SchemaProxy]()
+ props.Set("activeTargets", base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"array"},
+ Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: schemaRef("#/components/schemas/Target")},
+ }))
+ props.Set("droppedTargets", base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"array"},
+ Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: schemaRef("#/components/schemas/DroppedTarget")},
+ }))
+ props.Set("droppedTargetCounts", base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{A: integerSchema()},
+ }))
+
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Description: "Target discovery information including active and dropped targets.",
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
+ Required: []string{"activeTargets", "droppedTargets", "droppedTargetCounts"},
+ Properties: props,
+ })
+}
+
+func (*OpenAPIBuilder) scrapePoolsDiscoverySchema() *base.SchemaProxy {
+ props := orderedmap.New[string, *base.SchemaProxy]()
+ props.Set("scrapePools", base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"array"},
+ Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: stringSchema()},
+ }))
+
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Description: "List of all configured scrape pools.",
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
+ Required: []string{"scrapePools"},
+ Properties: props,
+ })
+}
+
+func (*OpenAPIBuilder) configSchema() *base.SchemaProxy {
+ props := orderedmap.New[string, *base.SchemaProxy]()
+ props.Set("source_labels", stringArraySchemaWithDescription("Source labels for relabeling."))
+ props.Set("separator", stringSchemaWithDescription("Separator for source label values."))
+ props.Set("regex", stringSchemaWithDescription("Regular expression for matching."))
+ props.Set("modulus", integerSchemaWithDescription("Modulus for hash-based relabeling."))
+ props.Set("target_label", stringSchemaWithDescription("Target label name."))
+ props.Set("replacement", stringSchemaWithDescription("Replacement value."))
+ props.Set("action", stringSchemaWithDescription("Relabel action."))
+
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Description: "Relabel configuration.",
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
+ Properties: props,
+ })
+}
+
+func (*OpenAPIBuilder) relabelStepSchema() *base.SchemaProxy {
+ props := orderedmap.New[string, *base.SchemaProxy]()
+ props.Set("rule", schemaRef("#/components/schemas/Config"))
+ props.Set("output", schemaRef("#/components/schemas/Labels"))
+ props.Set("keep", base.CreateSchemaProxy(&base.Schema{Type: []string{"boolean"}}))
+
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Description: "Relabel step showing the rule, output, and whether the target was kept.",
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
+ Required: []string{"rule", "output", "keep"},
+ Properties: props,
+ })
+}
+
+func (*OpenAPIBuilder) relabelStepsResponseSchema() *base.SchemaProxy {
+ props := orderedmap.New[string, *base.SchemaProxy]()
+ props.Set("steps", base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"array"},
+ Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: schemaRef("#/components/schemas/RelabelStep")},
+ }))
+
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Description: "Relabeling steps response.",
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
+ Required: []string{"steps"},
+ Properties: props,
+ })
+}
+
+func (*OpenAPIBuilder) ruleGroupSchema() *base.SchemaProxy {
+ props := orderedmap.New[string, *base.SchemaProxy]()
+ props.Set("name", stringSchemaWithDescription("Name of the rule group."))
+ props.Set("file", stringSchemaWithDescription("File containing the rule group."))
+ props.Set("rules", base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"array"},
+ Description: "Rules in this group.",
+ Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: base.CreateSchemaProxy(&base.Schema{Type: []string{"object"}, Description: "Rule definition."})},
+ }))
+ props.Set("interval", numberSchemaWithDescription("Evaluation interval in seconds."))
+ props.Set("limit", integerSchemaWithDescription("Maximum number of alerts for this group."))
+ props.Set("evaluationTime", numberSchemaWithDescription("Time taken to evaluate the group in seconds."))
+ props.Set("lastEvaluation", dateTimeSchemaWithDescription("Timestamp of the last evaluation."))
+
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Description: "Rule group information.",
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
+ Required: []string{"name", "file", "rules", "interval", "limit", "evaluationTime", "lastEvaluation"},
+ Properties: props,
+ })
+}
+
+func (*OpenAPIBuilder) ruleDiscoverySchema() *base.SchemaProxy {
+ props := orderedmap.New[string, *base.SchemaProxy]()
+ props.Set("groups", base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"array"},
+ Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: schemaRef("#/components/schemas/RuleGroup")},
+ }))
+ props.Set("groupNextToken", stringSchemaWithDescription("Pagination token for the next page of groups."))
+
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Description: "Rule discovery information containing all rule groups.",
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
+ Required: []string{"groups"},
+ Properties: props,
+ })
+}
+
+func (*OpenAPIBuilder) alertSchema() *base.SchemaProxy {
+ props := orderedmap.New[string, *base.SchemaProxy]()
+ props.Set("labels", schemaRef("#/components/schemas/Labels"))
+ props.Set("annotations", schemaRef("#/components/schemas/Labels"))
+ props.Set("state", stringSchemaWithDescription("State of the alert (pending, firing, or inactive)."))
+ props.Set("value", stringSchemaWithDescription("Value of the alert expression."))
+ props.Set("activeAt", dateTimeSchemaWithDescription("Timestamp when the alert became active."))
+ props.Set("keepFiringSince", dateTimeSchemaWithDescription("Timestamp since the alert has been kept firing."))
+
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Description: "Alert information.",
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
+ Required: []string{"labels", "annotations", "state", "value"},
+ Properties: props,
+ })
+}
+
+func (*OpenAPIBuilder) alertDiscoverySchema() *base.SchemaProxy {
+ props := orderedmap.New[string, *base.SchemaProxy]()
+ props.Set("alerts", base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"array"},
+ Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: schemaRef("#/components/schemas/Alert")},
+ }))
+
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Description: "Alert discovery information containing all active alerts.",
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
+ Required: []string{"alerts"},
+ Properties: props,
+ })
+}
+
+func (*OpenAPIBuilder) alertmanagerTargetSchema() *base.SchemaProxy {
+ props := orderedmap.New[string, *base.SchemaProxy]()
+ props.Set("url", stringSchemaWithDescription("URL of the Alertmanager instance."))
+
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Description: "Alertmanager target information.",
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
+ Required: []string{"url"},
+ Properties: props,
+ })
+}
+
+func (*OpenAPIBuilder) alertmanagerDiscoverySchema() *base.SchemaProxy {
+ props := orderedmap.New[string, *base.SchemaProxy]()
+ props.Set("activeAlertmanagers", base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"array"},
+ Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: schemaRef("#/components/schemas/AlertmanagerTarget")},
+ }))
+ props.Set("droppedAlertmanagers", base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"array"},
+ Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: schemaRef("#/components/schemas/AlertmanagerTarget")},
+ }))
+
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Description: "Alertmanager discovery information including active and dropped instances.",
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
+ Required: []string{"activeAlertmanagers", "droppedAlertmanagers"},
+ Properties: props,
+ })
+}
+
+func (*OpenAPIBuilder) statusConfigDataSchema() *base.SchemaProxy {
+ props := orderedmap.New[string, *base.SchemaProxy]()
+ props.Set("yaml", stringSchemaWithDescription("Prometheus configuration in YAML format."))
+
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Description: "Prometheus configuration.",
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
+ Required: []string{"yaml"},
+ Properties: props,
+ })
+}
+
+func (*OpenAPIBuilder) runtimeInfoSchema() *base.SchemaProxy {
+ props := orderedmap.New[string, *base.SchemaProxy]()
+ props.Set("startTime", base.CreateSchemaProxy(&base.Schema{Type: []string{"string"}, Format: "date-time"}))
+ props.Set("CWD", stringSchema())
+ props.Set("hostname", stringSchema())
+ props.Set("serverTime", base.CreateSchemaProxy(&base.Schema{Type: []string{"string"}, Format: "date-time"}))
+ props.Set("reloadConfigSuccess", base.CreateSchemaProxy(&base.Schema{Type: []string{"boolean"}}))
+ props.Set("lastConfigTime", base.CreateSchemaProxy(&base.Schema{Type: []string{"string"}, Format: "date-time"}))
+ props.Set("corruptionCount", integerSchema())
+ props.Set("goroutineCount", integerSchema())
+ props.Set("GOMAXPROCS", integerSchema())
+ props.Set("GOMEMLIMIT", integerSchema())
+ props.Set("GOGC", stringSchema())
+ props.Set("GODEBUG", stringSchema())
+ props.Set("storageRetention", stringSchema())
+
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Description: "Prometheus runtime information.",
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
+ Required: []string{"startTime", "CWD", "hostname", "serverTime", "reloadConfigSuccess", "lastConfigTime", "corruptionCount", "goroutineCount", "GOMAXPROCS", "GOMEMLIMIT", "GOGC", "GODEBUG", "storageRetention"},
+ Properties: props,
+ })
+}
+
+func (*OpenAPIBuilder) prometheusVersionSchema() *base.SchemaProxy {
+ props := orderedmap.New[string, *base.SchemaProxy]()
+ props.Set("version", stringSchema())
+ props.Set("revision", stringSchema())
+ props.Set("branch", stringSchema())
+ props.Set("buildUser", stringSchema())
+ props.Set("buildDate", stringSchema())
+ props.Set("goVersion", stringSchema())
+
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Description: "Prometheus version information.",
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
+ Required: []string{"version", "revision", "branch", "buildUser", "buildDate", "goVersion"},
+ Properties: props,
+ })
+}
+
+func (*OpenAPIBuilder) statusFlagsOutputBodySchema() *base.SchemaProxy {
+ props := orderedmap.New[string, *base.SchemaProxy]()
+ props.Set("status", statusSchema())
+ props.Set("data", base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{A: stringSchema()},
+ }))
+ props.Set("warnings", warningsSchema())
+ props.Set("infos", infosSchema())
+
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Description: "Response body for status flags endpoint.",
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
+ Required: []string{"status", "data"},
+ Properties: props,
+ })
+}
+
+func (*OpenAPIBuilder) headStatsSchema() *base.SchemaProxy {
+ props := orderedmap.New[string, *base.SchemaProxy]()
+ props.Set("numSeries", integerSchema())
+ props.Set("numLabelPairs", integerSchema())
+ props.Set("chunkCount", integerSchema())
+ props.Set("minTime", integerSchema())
+ props.Set("maxTime", integerSchema())
+
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Description: "TSDB head statistics.",
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
+ Required: []string{"numSeries", "numLabelPairs", "chunkCount", "minTime", "maxTime"},
+ Properties: props,
+ })
+}
+
+func (*OpenAPIBuilder) tsdbStatSchema() *base.SchemaProxy {
+ props := orderedmap.New[string, *base.SchemaProxy]()
+ props.Set("name", stringSchema())
+ props.Set("value", integerSchema())
+
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Description: "TSDB statistic.",
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
+ Required: []string{"name", "value"},
+ Properties: props,
+ })
+}
+
+func (*OpenAPIBuilder) tsdbStatusSchema() *base.SchemaProxy {
+ props := orderedmap.New[string, *base.SchemaProxy]()
+ props.Set("headStats", schemaRef("#/components/schemas/HeadStats"))
+ props.Set("seriesCountByMetricName", base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"array"},
+ Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: schemaRef("#/components/schemas/TSDBStat")},
+ }))
+ props.Set("labelValueCountByLabelName", base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"array"},
+ Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: schemaRef("#/components/schemas/TSDBStat")},
+ }))
+ props.Set("memoryInBytesByLabelName", base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"array"},
+ Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: schemaRef("#/components/schemas/TSDBStat")},
+ }))
+ props.Set("seriesCountByLabelValuePair", base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"array"},
+ Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: schemaRef("#/components/schemas/TSDBStat")},
+ }))
+
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Description: "TSDB status information.",
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
+ Required: []string{"headStats", "seriesCountByMetricName", "labelValueCountByLabelName", "memoryInBytesByLabelName", "seriesCountByLabelValuePair"},
+ Properties: props,
+ })
+}
+
+func (*OpenAPIBuilder) blockDescSchema() *base.SchemaProxy {
+ props := orderedmap.New[string, *base.SchemaProxy]()
+ props.Set("ulid", stringSchema())
+ props.Set("minTime", integerSchema())
+ props.Set("maxTime", integerSchema())
+
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Description: "Block descriptor.",
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
+ Required: []string{"ulid", "minTime", "maxTime"},
+ Properties: props,
+ })
+}
+
+func (*OpenAPIBuilder) blockStatsSchema() *base.SchemaProxy {
+ props := orderedmap.New[string, *base.SchemaProxy]()
+ props.Set("numSamples", integerSchema())
+ props.Set("numSeries", integerSchema())
+ props.Set("numChunks", integerSchema())
+ props.Set("numTombstones", integerSchema())
+ props.Set("numFloatSamples", integerSchema())
+ props.Set("numHistogramSamples", integerSchema())
+
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Description: "Block statistics.",
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
+ Properties: props,
+ })
+}
+
+func (*OpenAPIBuilder) blockMetaCompactionSchema() *base.SchemaProxy {
+ props := orderedmap.New[string, *base.SchemaProxy]()
+ props.Set("level", integerSchema())
+ props.Set("sources", base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"array"},
+ Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: stringSchema()},
+ }))
+ props.Set("parents", base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"array"},
+ Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: schemaRef("#/components/schemas/BlockDesc")},
+ }))
+ props.Set("failed", base.CreateSchemaProxy(&base.Schema{Type: []string{"boolean"}}))
+ props.Set("deletable", base.CreateSchemaProxy(&base.Schema{Type: []string{"boolean"}}))
+ props.Set("hints", base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"array"},
+ Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: stringSchema()},
+ }))
+
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Description: "Block compaction metadata.",
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
+ Required: []string{"level"},
+ Properties: props,
+ })
+}
+
+func (*OpenAPIBuilder) blockMetaSchema() *base.SchemaProxy {
+ props := orderedmap.New[string, *base.SchemaProxy]()
+ props.Set("ulid", stringSchema())
+ props.Set("minTime", integerSchema())
+ props.Set("maxTime", integerSchema())
+ props.Set("stats", schemaRef("#/components/schemas/BlockStats"))
+ props.Set("compaction", schemaRef("#/components/schemas/BlockMetaCompaction"))
+ props.Set("version", integerSchema())
+
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Description: "Block metadata.",
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
+ Required: []string{"ulid", "minTime", "maxTime", "compaction", "version"},
+ Properties: props,
+ })
+}
+
+func (*OpenAPIBuilder) statusTSDBBlocksDataSchema() *base.SchemaProxy {
+ props := orderedmap.New[string, *base.SchemaProxy]()
+ props.Set("blocks", base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"array"},
+ Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: schemaRef("#/components/schemas/BlockMeta")},
+ }))
+
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Description: "TSDB blocks information.",
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
+ Required: []string{"blocks"},
+ Properties: props,
+ })
+}
+
+func (*OpenAPIBuilder) statusWALReplayDataSchema() *base.SchemaProxy {
+ props := orderedmap.New[string, *base.SchemaProxy]()
+ props.Set("min", integerSchema())
+ props.Set("max", integerSchema())
+ props.Set("current", integerSchema())
+
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Description: "WAL replay status.",
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
+ Required: []string{"min", "max", "current"},
+ Properties: props,
+ })
+}
+
+func (*OpenAPIBuilder) dataStructSchema() *base.SchemaProxy {
+ props := orderedmap.New[string, *base.SchemaProxy]()
+ props.Set("name", stringSchema())
+
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Description: "Generic data structure with a name field.",
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
+ Required: []string{"name"},
+ Properties: props,
+ })
+}
+
+func (*OpenAPIBuilder) notificationSchema() *base.SchemaProxy {
+ props := orderedmap.New[string, *base.SchemaProxy]()
+ props.Set("text", stringSchema())
+ props.Set("date", base.CreateSchemaProxy(&base.Schema{Type: []string{"string"}, Format: "date-time"}))
+ props.Set("active", base.CreateSchemaProxy(&base.Schema{Type: []string{"boolean"}}))
+
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Description: "Server notification.",
+ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
+ Required: []string{"text", "date", "active"},
+ Properties: props,
+ })
+}
diff --git a/web/api/v1/openapi_test.go b/web/api/v1/openapi_test.go
new file mode 100644
index 0000000000..21547734c2
--- /dev/null
+++ b/web/api/v1/openapi_test.go
@@ -0,0 +1,289 @@
+// 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 v1
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+
+ "github.com/prometheus/common/promslog"
+ "github.com/stretchr/testify/require"
+ "go.yaml.in/yaml/v2"
+)
+
+// TestOpenAPIHTTPHandler verifies that the OpenAPI endpoint serves a valid specification
+// with correct headers, structure conforming to OpenAPI 3.1 standards, and consistent responses.
+func TestOpenAPIHTTPHandler(t *testing.T) {
+ builder := NewOpenAPIBuilder(OpenAPIOptions{}, promslog.NewNopLogger())
+
+ // First request.
+ req1 := httptest.NewRequest(http.MethodGet, "/api/v1/openapi.yaml", nil)
+ rec1 := httptest.NewRecorder()
+ builder.ServeOpenAPI(rec1, req1)
+
+ // Verify status code and headers.
+ require.Equal(t, http.StatusOK, rec1.Code)
+ require.True(t, strings.HasPrefix(rec1.Header().Get("Content-Type"), "application/yaml"), "Content-Type should start with application/yaml")
+ require.Equal(t, "no-cache, no-store, must-revalidate", rec1.Header().Get("Cache-Control"))
+
+ // Verify it is valid YAML.
+ var spec map[string]any
+ err := yaml.Unmarshal(rec1.Body.Bytes(), &spec)
+ require.NoError(t, err)
+
+ // Verify structure.
+ require.Contains(t, spec, "openapi")
+ require.Contains(t, spec, "info")
+ require.Contains(t, spec, "paths")
+ require.Contains(t, spec, "components")
+
+ // Verify OpenAPI version (default is 3.1.0).
+ require.Equal(t, "3.1.0", spec["openapi"])
+
+ // Verify info section.
+ info, ok := spec["info"].(map[any]any)
+ require.True(t, ok, "info should be a map")
+ require.Equal(t, "Prometheus API", info["title"])
+
+ // Verify paths exist.
+ paths, ok := spec["paths"].(map[any]any)
+ require.True(t, ok, "paths should be a map")
+ require.NotEmpty(t, paths, "paths should not be empty")
+
+ // Second request to verify response consistency.
+ req2 := httptest.NewRequest(http.MethodGet, "/api/v1/openapi.yaml", nil)
+ rec2 := httptest.NewRecorder()
+ builder.ServeOpenAPI(rec2, req2)
+
+ // Both responses should be identical.
+ require.Equal(t, rec1.Body.String(), rec2.Body.String())
+}
+
+// TestOpenAPIPathFiltering verifies that the IncludePaths option correctly filters
+// which API paths are included in the generated specification.
+func TestOpenAPIPathFiltering(t *testing.T) {
+ tests := []struct {
+ name string
+ includePaths []string
+ wantPaths []string
+ excludePaths []string
+ }{
+ {
+ name: "no filter includes all",
+ includePaths: nil,
+ wantPaths: []string{"/query", "/labels", "/alerts", "/targets"},
+ },
+ {
+ name: "filter query paths",
+ includePaths: []string{"/query"},
+ wantPaths: []string{"/query", "/query_range", "/query_exemplars"},
+ excludePaths: []string{"/labels", "/alerts", "/targets"},
+ },
+ {
+ name: "filter status paths",
+ includePaths: []string{"/status"},
+ wantPaths: []string{"/status/config", "/status/flags", "/status/runtimeinfo"},
+ excludePaths: []string{"/query", "/alerts", "/targets"},
+ },
+ {
+ name: "filter multiple prefixes",
+ includePaths: []string{"/label", "/series"},
+ wantPaths: []string{"/labels", "/label/{name}/values", "/series"},
+ excludePaths: []string{"/query", "/alerts", "/targets"},
+ },
+ {
+ name: "exact path match",
+ includePaths: []string{"/alerts"},
+ wantPaths: []string{"/alerts"},
+ excludePaths: []string{"/alertmanagers", "/query"},
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ builder := NewOpenAPIBuilder(OpenAPIOptions{
+ IncludePaths: tc.includePaths,
+ }, promslog.NewNopLogger())
+
+ req := httptest.NewRequest(http.MethodGet, "/api/v1/openapi.yaml", nil)
+ rec := httptest.NewRecorder()
+ builder.ServeOpenAPI(rec, req)
+
+ require.Equal(t, http.StatusOK, rec.Code)
+
+ var spec map[string]any
+ err := yaml.Unmarshal(rec.Body.Bytes(), &spec)
+ require.NoError(t, err)
+
+ paths, ok := spec["paths"].(map[any]any)
+ require.True(t, ok, "paths should be a map")
+
+ for _, want := range tc.wantPaths {
+ require.Contains(t, paths, want)
+ }
+
+ for _, exclude := range tc.excludePaths {
+ require.NotContains(t, paths, exclude)
+ }
+ })
+ }
+}
+
+// TestOpenAPISchemaCompleteness verifies that all referenced schemas in paths
+// are defined in the components/schemas section of the specification.
+func TestOpenAPISchemaCompleteness(t *testing.T) {
+ builder := NewOpenAPIBuilder(OpenAPIOptions{}, promslog.NewNopLogger())
+
+ req := httptest.NewRequest(http.MethodGet, "/api/v1/openapi.yaml", nil)
+ rec := httptest.NewRecorder()
+ builder.ServeOpenAPI(rec, req)
+
+ var spec map[string]any
+ err := yaml.Unmarshal(rec.Body.Bytes(), &spec)
+ require.NoError(t, err)
+
+ components, ok := spec["components"].(map[any]any)
+ require.True(t, ok, "components should be a map")
+
+ schemas, ok := components["schemas"].(map[any]any)
+ require.True(t, ok, "schemas should be a map")
+
+ // Verify essential schemas are present.
+ essentialSchemas := []string{
+ "Error",
+ "Labels",
+ "QueryOutputBody",
+ "LabelsOutputBody",
+ "SeriesOutputBody",
+ "TargetsOutputBody",
+ "AlertsOutputBody",
+ "RulesOutputBody",
+ "StatusConfigOutputBody",
+ "StatusFlagsOutputBody",
+ "PrometheusVersion",
+ }
+
+ for _, schema := range essentialSchemas {
+ require.Contains(t, schemas, schema)
+ }
+}
+
+// TODO: Add test to verify all routes from api.go Register() are covered in OpenAPI spec.
+// Consider wrapping Router to track registered paths and cross-check with OpenAPI paths.
+
+// TestOpenAPIShouldIncludePath verifies the shouldIncludePath method correctly
+// matches paths against the IncludePaths filter configuration.
+func TestOpenAPIShouldIncludePath(t *testing.T) {
+ tests := []struct {
+ name string
+ includePaths []string
+ path string
+ expected bool
+ }{
+ {
+ name: "empty filter includes all",
+ includePaths: nil,
+ path: "/query",
+ expected: true,
+ },
+ {
+ name: "exact match",
+ includePaths: []string{"/query"},
+ path: "/query",
+ expected: true,
+ },
+ {
+ name: "prefix match",
+ includePaths: []string{"/query"},
+ path: "/query_range",
+ expected: true,
+ },
+ {
+ name: "no match",
+ includePaths: []string{"/query"},
+ path: "/labels",
+ expected: false,
+ },
+ {
+ name: "multiple filters with match",
+ includePaths: []string{"/labels", "/series"},
+ path: "/series",
+ expected: true,
+ },
+ {
+ name: "multiple filters without match",
+ includePaths: []string{"/labels", "/series"},
+ path: "/query",
+ expected: false,
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ builder := &OpenAPIBuilder{
+ options: OpenAPIOptions{
+ IncludePaths: tc.includePaths,
+ },
+ }
+
+ result := builder.shouldIncludePath(tc.path)
+ require.Equal(t, tc.expected, result)
+ })
+ }
+}
+
+// TestOpenAPIVersionConsistency verifies that both OpenAPI versions are properly generated
+// and that 3.2 has exactly one more path than 3.1 (/notifications/live).
+func TestOpenAPIVersionConsistency(t *testing.T) {
+ builder := NewOpenAPIBuilder(OpenAPIOptions{}, promslog.NewNopLogger())
+
+ // Fetch OpenAPI 3.1 spec (default).
+ req31 := httptest.NewRequest(http.MethodGet, "/api/v1/openapi.yaml", nil)
+ rec31 := httptest.NewRecorder()
+ builder.ServeOpenAPI(rec31, req31)
+
+ require.Equal(t, http.StatusOK, rec31.Code)
+
+ // Fetch OpenAPI 3.2 spec.
+ req32 := httptest.NewRequest(http.MethodGet, "/api/v1/openapi.yaml?openapi_version=3.2", nil)
+ rec32 := httptest.NewRecorder()
+ builder.ServeOpenAPI(rec32, req32)
+
+ require.Equal(t, http.StatusOK, rec32.Code)
+
+ // Parse both specs.
+ var spec31, spec32 map[string]any
+ err := yaml.Unmarshal(rec31.Body.Bytes(), &spec31)
+ require.NoError(t, err)
+ err = yaml.Unmarshal(rec32.Body.Bytes(), &spec32)
+ require.NoError(t, err)
+
+ // Verify versions are different.
+ require.Equal(t, "3.1.0", spec31["openapi"])
+ require.Equal(t, "3.2.0", spec32["openapi"])
+
+ // Verify /notifications/live is only in 3.2.
+ paths31 := spec31["paths"].(map[any]any)
+ paths32 := spec32["paths"].(map[any]any)
+
+ require.NotContains(t, paths31, "/notifications/live")
+
+ require.Contains(t, paths32, "/notifications/live")
+
+ // Verify 3.2 has exactly one more path than 3.1.
+ require.Len(t, paths32, len(paths31)+1,
+ "OpenAPI 3.2 should have exactly one more path than 3.1")
+}
diff --git a/web/api/v1/test_helpers.go b/web/api/v1/test_helpers.go
new file mode 100644
index 0000000000..873a80c238
--- /dev/null
+++ b/web/api/v1/test_helpers.go
@@ -0,0 +1,159 @@
+// 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 v1
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ "github.com/prometheus/common/route"
+
+ "github.com/prometheus/prometheus/promql/parser"
+ "github.com/prometheus/prometheus/web/api/testhelpers"
+)
+
+// newTestAPI creates a new API instance for testing using testhelpers.
+func newTestAPI(t *testing.T, cfg testhelpers.APIConfig) *testhelpers.APIWrapper {
+ t.Helper()
+
+ params := testhelpers.PrepareAPI(t, cfg)
+
+ // Adapt the testhelpers interfaces to v1 interfaces.
+ api := NewAPI(
+ params.QueryEngine,
+ params.Queryable,
+ nil, nil, // appendables
+ params.ExemplarQueryable,
+ func(ctx context.Context) ScrapePoolsRetriever {
+ return adaptScrapePoolsRetriever(params.ScrapePoolsRetriever(ctx))
+ },
+ func(ctx context.Context) TargetRetriever {
+ return adaptTargetRetriever(params.TargetRetriever(ctx))
+ },
+ func(ctx context.Context) AlertmanagerRetriever {
+ return adaptAlertmanagerRetriever(params.AlertmanagerRetriever(ctx))
+ },
+ params.ConfigFunc,
+ params.FlagsMap,
+ GlobalURLOptions{},
+ params.ReadyFunc,
+ adaptTSDBAdminStats(params.TSDBAdmin),
+ params.DBDir,
+ false, // enableAdmin
+ params.Logger,
+ func(ctx context.Context) RulesRetriever {
+ return adaptRulesRetriever(params.RulesRetriever(ctx))
+ },
+ 0, // remoteReadSampleLimit
+ 0, // remoteReadConcurrencyLimit
+ 0, // remoteReadMaxBytesInFrame
+ false, // isAgent
+ nil, // corsOrigin
+ func() (RuntimeInfo, error) {
+ info, err := params.RuntimeInfoFunc()
+ return RuntimeInfo{
+ StartTime: info.StartTime,
+ CWD: info.CWD,
+ Hostname: info.Hostname,
+ ServerTime: info.ServerTime,
+ ReloadConfigSuccess: info.ReloadConfigSuccess,
+ LastConfigTime: info.LastConfigTime,
+ CorruptionCount: info.CorruptionCount,
+ GoroutineCount: info.GoroutineCount,
+ GOMAXPROCS: info.GOMAXPROCS,
+ GOMEMLIMIT: info.GOMEMLIMIT,
+ GOGC: info.GOGC,
+ GODEBUG: info.GODEBUG,
+ StorageRetention: info.StorageRetention,
+ }, err
+ },
+ &PrometheusVersion{
+ Version: params.BuildInfo.Version,
+ Revision: params.BuildInfo.Revision,
+ Branch: params.BuildInfo.Branch,
+ BuildUser: params.BuildInfo.BuildUser,
+ BuildDate: params.BuildInfo.BuildDate,
+ GoVersion: params.BuildInfo.GoVersion,
+ },
+ params.NotificationsGetter,
+ params.NotificationsSub,
+ params.Gatherer,
+ params.Registerer,
+ nil, // statsRenderer
+ false, // rwEnabled
+ nil, // acceptRemoteWriteProtoMsgs
+ false, // otlpEnabled
+ false, // otlpDeltaToCumulative
+ false, // otlpNativeDeltaIngestion
+ false, // stZeroIngestionEnabled
+ 5*time.Minute, // lookbackDelta
+ false, // enableTypeAndUnitLabels
+ false, // appendMetadata
+ nil, // overrideErrorCode
+ nil, // featureRegistry
+ OpenAPIOptions{}, // openAPIOptions
+ parser.NewParser(parser.Options{}), // promqlParser
+ )
+
+ // Register routes.
+ router := route.New()
+ api.Register(router.WithPrefix("/api/v1"))
+
+ return &testhelpers.APIWrapper{
+ Handler: router,
+ }
+}
+
+// Adapter functions to convert testhelpers interfaces to v1 interfaces.
+
+type rulesRetrieverAdapter struct {
+ testhelpers.RulesRetriever
+}
+
+func adaptRulesRetriever(r testhelpers.RulesRetriever) RulesRetriever {
+ return &rulesRetrieverAdapter{r}
+}
+
+type targetRetrieverAdapter struct {
+ testhelpers.TargetRetriever
+}
+
+func adaptTargetRetriever(t testhelpers.TargetRetriever) TargetRetriever {
+ return &targetRetrieverAdapter{t}
+}
+
+type scrapePoolsRetrieverAdapter struct {
+ testhelpers.ScrapePoolsRetriever
+}
+
+func adaptScrapePoolsRetriever(s testhelpers.ScrapePoolsRetriever) ScrapePoolsRetriever {
+ return &scrapePoolsRetrieverAdapter{s}
+}
+
+type alertmanagerRetrieverAdapter struct {
+ testhelpers.AlertmanagerRetriever
+}
+
+func adaptAlertmanagerRetriever(a testhelpers.AlertmanagerRetriever) AlertmanagerRetriever {
+ return &alertmanagerRetrieverAdapter{a}
+}
+
+type tsdbAdminStatsAdapter struct {
+ testhelpers.TSDBAdminStats
+}
+
+func adaptTSDBAdminStats(t testhelpers.TSDBAdminStats) TSDBAdminStats {
+ return &tsdbAdminStatsAdapter{t}
+}
diff --git a/web/api/v1/testdata/openapi_3.1_golden.yaml b/web/api/v1/testdata/openapi_3.1_golden.yaml
new file mode 100644
index 0000000000..b1514f209d
--- /dev/null
+++ b/web/api/v1/testdata/openapi_3.1_golden.yaml
@@ -0,0 +1,4453 @@
+openapi: 3.1.0
+info:
+ title: Prometheus API
+ description: Prometheus is an Open-Source monitoring system with a dimensional data model, flexible query language, efficient time series database and modern alerting approach.
+ contact:
+ name: Prometheus Community
+ url: https://prometheus.io/community/
+ version: 0.0.1-undefined
+servers:
+ - url: /api/v1
+paths:
+ /query:
+ get:
+ tags:
+ - query
+ summary: Evaluate an instant query
+ operationId: query
+ parameters:
+ - name: limit
+ in: query
+ description: The maximum number of metrics to return.
+ required: false
+ explode: false
+ schema:
+ type: integer
+ format: int64
+ examples:
+ example:
+ value: 100
+ - name: time
+ in: query
+ description: The evaluation timestamp (optional, defaults to current time).
+ required: false
+ explode: false
+ schema:
+ oneOf:
+ - type: string
+ format: date-time
+ description: RFC3339 timestamp.
+ - type: number
+ format: unixtime
+ description: Unix timestamp in seconds.
+ description: Timestamp in RFC3339 format or Unix timestamp in seconds.
+ examples:
+ RFC3339:
+ value: "2026-01-02T13:37:00Z"
+ epoch:
+ value: 1767361020
+ - name: query
+ in: query
+ description: The PromQL query to execute.
+ required: true
+ explode: false
+ schema:
+ type: string
+ examples:
+ example:
+ value: up
+ - name: timeout
+ in: query
+ description: Evaluation timeout. Optional. Defaults to and is capped by the value of the -query.timeout flag.
+ required: false
+ explode: false
+ schema:
+ type: string
+ examples:
+ example:
+ value: 30s
+ - name: lookback_delta
+ in: query
+ description: Override the lookback period for this query. Optional.
+ required: false
+ explode: false
+ schema:
+ type: string
+ examples:
+ example:
+ value: 5m
+ - name: stats
+ in: query
+ description: When provided, include query statistics in the response. The special value 'all' enables more comprehensive statistics.
+ required: false
+ explode: false
+ schema:
+ type: string
+ examples:
+ example:
+ value: all
+ responses:
+ "200":
+ description: Query executed successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/QueryOutputBody'
+ examples:
+ vectorResult:
+ summary: 'Instant vector query: up'
+ value: {"status": "success", "data": {"resultType": "vector", "result": [{"metric": {"__name__": "up", "instance": "demo.prometheus.io:9090", "job": "prometheus"}, "value": [1767436620, "1"]}, {"metric": {"__name__": "up", "env": "demo", "instance": "demo.prometheus.io:9093", "job": "alertmanager"}, "value": [1767436620, "1"]}]}}
+ scalarResult:
+ summary: 'Scalar query: scalar(42)'
+ value:
+ data:
+ result:
+ - 1767436620
+ - "42"
+ resultType: scalar
+ status: success
+ matrixResult:
+ summary: 'Range vector query: up[5m]'
+ value: {"status": "success", "data": {"resultType": "matrix", "result": [{"metric": {"__name__": "up", "instance": "demo.prometheus.io:9090", "job": "prometheus"}, "values": [[1767436320, "1"], [1767436620, "1"]]}]}}
+ default:
+ description: Error executing query.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ post:
+ tags:
+ - query
+ summary: Evaluate an instant query
+ operationId: query-post
+ requestBody:
+ description: Submit an instant query. This endpoint accepts the same parameters as the GET version.
+ content:
+ application/x-www-form-urlencoded:
+ schema:
+ $ref: '#/components/schemas/QueryPostInputBody'
+ examples:
+ simpleQuery:
+ summary: Simple instant query
+ value:
+ query: up
+ queryWithTime:
+ summary: Query with specific timestamp
+ value:
+ query: up{job="prometheus"}
+ time: "2026-01-02T13:37:00.000Z"
+ queryWithLimit:
+ summary: Query with limit and statistics
+ value:
+ limit: 100
+ query: rate(prometheus_http_requests_total{handler="/api/v1/query"}[5m])
+ stats: all
+ required: true
+ responses:
+ "200":
+ description: Instant query executed successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/QueryOutputBody'
+ examples:
+ vectorResult:
+ summary: 'Instant vector query: up'
+ value: {"status": "success", "data": {"resultType": "vector", "result": [{"metric": {"__name__": "up", "instance": "demo.prometheus.io:9090", "job": "prometheus"}, "value": [1767436620, "1"]}, {"metric": {"__name__": "up", "env": "demo", "instance": "demo.prometheus.io:9093", "job": "alertmanager"}, "value": [1767436620, "1"]}]}}
+ scalarResult:
+ summary: 'Scalar query: scalar(42)'
+ value:
+ data:
+ result:
+ - 1767436620
+ - "42"
+ resultType: scalar
+ status: success
+ matrixResult:
+ summary: 'Range vector query: up[5m]'
+ value: {"status": "success", "data": {"resultType": "matrix", "result": [{"metric": {"__name__": "up", "instance": "demo.prometheus.io:9090", "job": "prometheus"}, "values": [[1767436320, "1"], [1767436620, "1"]]}]}}
+ default:
+ description: Error executing instant query.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ /query_range:
+ get:
+ tags:
+ - query
+ summary: Evaluate a range query
+ operationId: query-range
+ parameters:
+ - name: limit
+ in: query
+ description: The maximum number of metrics to return.
+ required: false
+ explode: false
+ schema:
+ type: integer
+ format: int64
+ examples:
+ example:
+ value: 100
+ - name: start
+ in: query
+ description: The start time of the query.
+ required: true
+ explode: false
+ schema:
+ oneOf:
+ - type: string
+ format: date-time
+ description: RFC3339 timestamp.
+ - type: number
+ format: unixtime
+ description: Unix timestamp in seconds.
+ description: Timestamp in RFC3339 format or Unix timestamp in seconds.
+ examples:
+ RFC3339:
+ value: "2026-01-02T12:37:00Z"
+ epoch:
+ value: 1767357420
+ - name: end
+ in: query
+ description: The end time of the query.
+ required: true
+ explode: false
+ schema:
+ oneOf:
+ - type: string
+ format: date-time
+ description: RFC3339 timestamp.
+ - type: number
+ format: unixtime
+ description: Unix timestamp in seconds.
+ description: Timestamp in RFC3339 format or Unix timestamp in seconds.
+ examples:
+ RFC3339:
+ value: "2026-01-02T13:37:00Z"
+ epoch:
+ value: 1767361020
+ - name: step
+ in: query
+ description: The step size of the query.
+ required: true
+ explode: false
+ schema:
+ type: string
+ examples:
+ example:
+ value: 15s
+ - name: query
+ in: query
+ description: The query to execute.
+ required: true
+ explode: false
+ schema:
+ type: string
+ examples:
+ example:
+ value: rate(prometheus_http_requests_total{handler="/api/v1/query"}[5m])
+ - name: timeout
+ in: query
+ description: Evaluation timeout. Optional. Defaults to and is capped by the value of the -query.timeout flag.
+ required: false
+ explode: false
+ schema:
+ type: string
+ examples:
+ example:
+ value: 30s
+ - name: lookback_delta
+ in: query
+ description: Override the lookback period for this query. Optional.
+ required: false
+ explode: false
+ schema:
+ type: string
+ examples:
+ example:
+ value: 5m
+ - name: stats
+ in: query
+ description: When provided, include query statistics in the response. The special value 'all' enables more comprehensive statistics.
+ required: false
+ explode: false
+ schema:
+ type: string
+ examples:
+ example:
+ value: all
+ responses:
+ "200":
+ description: Range query executed successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/QueryRangeOutputBody'
+ examples:
+ matrixResult:
+ summary: 'Range query: rate(prometheus_http_requests_total[5m])'
+ value: {"status": "success", "data": {"resultType": "matrix", "result": [{"metric": {"__name__": "up", "instance": "demo.prometheus.io:9090", "job": "prometheus"}, "values": [[1767433020, "1"], [1767434820, "1"], [1767436620, "1"]]}]}}
+ default:
+ description: Error executing range query.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ post:
+ tags:
+ - query
+ summary: Evaluate a range query
+ operationId: query-range-post
+ requestBody:
+ description: Submit a range query. This endpoint accepts the same parameters as the GET version.
+ content:
+ application/x-www-form-urlencoded:
+ schema:
+ $ref: '#/components/schemas/QueryRangePostInputBody'
+ examples:
+ basicRange:
+ summary: Basic range query
+ value:
+ end: "2026-01-02T13:37:00.000Z"
+ query: up
+ start: "2026-01-02T12:37:00.000Z"
+ step: 15s
+ rateQuery:
+ summary: Rate calculation over time range
+ value:
+ end: "2026-01-02T13:37:00.000Z"
+ query: rate(prometheus_http_requests_total{handler="/api/v1/query"}[5m])
+ start: "2026-01-02T12:37:00.000Z"
+ step: 30s
+ timeout: 30s
+ required: true
+ responses:
+ "200":
+ description: Range query executed successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/QueryRangeOutputBody'
+ examples:
+ matrixResult:
+ summary: 'Range query: rate(prometheus_http_requests_total[5m])'
+ value: {"status": "success", "data": {"resultType": "matrix", "result": [{"metric": {"__name__": "up", "instance": "demo.prometheus.io:9090", "job": "prometheus"}, "values": [[1767433020, "1"], [1767434820, "1"], [1767436620, "1"]]}]}}
+ default:
+ description: Error executing range query.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ /query_exemplars:
+ get:
+ tags:
+ - query
+ summary: Query exemplars
+ operationId: query-exemplars
+ parameters:
+ - name: start
+ in: query
+ description: Start timestamp for exemplars query.
+ required: false
+ explode: false
+ schema:
+ oneOf:
+ - type: string
+ format: date-time
+ description: RFC3339 timestamp.
+ - type: number
+ format: unixtime
+ description: Unix timestamp in seconds.
+ description: Timestamp in RFC3339 format or Unix timestamp in seconds.
+ examples:
+ RFC3339:
+ value: "2026-01-02T12:37:00Z"
+ epoch:
+ value: 1767357420
+ - name: end
+ in: query
+ description: End timestamp for exemplars query.
+ required: false
+ explode: false
+ schema:
+ oneOf:
+ - type: string
+ format: date-time
+ description: RFC3339 timestamp.
+ - type: number
+ format: unixtime
+ description: Unix timestamp in seconds.
+ description: Timestamp in RFC3339 format or Unix timestamp in seconds.
+ examples:
+ RFC3339:
+ value: "2026-01-02T13:37:00Z"
+ epoch:
+ value: 1767361020
+ - name: query
+ in: query
+ description: PromQL query to extract exemplars for.
+ required: true
+ explode: false
+ schema:
+ type: string
+ examples:
+ example:
+ value: prometheus_http_requests_total
+ responses:
+ "200":
+ description: Exemplars retrieved successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/QueryExemplarsOutputBody'
+ examples:
+ exemplarsResult:
+ summary: Exemplars for a metric with trace IDs
+ value:
+ data:
+ - exemplars:
+ - labels:
+ traceID: abc123def456
+ timestamp: 1.689956451781e+09
+ value: "1.5"
+ seriesLabels:
+ __name__: http_requests_total
+ job: api-server
+ method: GET
+ status: success
+ default:
+ description: Error retrieving exemplars.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ post:
+ tags:
+ - query
+ summary: Query exemplars
+ operationId: query-exemplars-post
+ requestBody:
+ description: Submit an exemplars query. This endpoint accepts the same parameters as the GET version.
+ content:
+ application/x-www-form-urlencoded:
+ schema:
+ $ref: '#/components/schemas/QueryExemplarsPostInputBody'
+ examples:
+ basicExemplar:
+ summary: Query exemplars for a metric
+ value:
+ query: prometheus_http_requests_total
+ exemplarWithTimeRange:
+ summary: Exemplars within specific time range
+ value:
+ end: "2026-01-02T13:37:00.000Z"
+ query: prometheus_http_requests_total{job="prometheus"}
+ start: "2026-01-02T12:37:00.000Z"
+ required: true
+ responses:
+ "200":
+ description: Exemplars query completed successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/QueryExemplarsOutputBody'
+ examples:
+ exemplarsResult:
+ summary: Exemplars for a metric with trace IDs
+ value:
+ data:
+ - exemplars:
+ - labels:
+ traceID: abc123def456
+ timestamp: 1.689956451781e+09
+ value: "1.5"
+ seriesLabels:
+ __name__: http_requests_total
+ job: api-server
+ method: GET
+ status: success
+ default:
+ description: Error processing exemplars query.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ /format_query:
+ get:
+ tags:
+ - query
+ summary: Format a PromQL query
+ operationId: format-query
+ parameters:
+ - name: query
+ in: query
+ description: PromQL expression to format.
+ required: true
+ explode: false
+ schema:
+ type: string
+ examples:
+ example:
+ value: sum(rate(http_requests_total[5m])) by (job)
+ responses:
+ "200":
+ description: Query formatted successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/FormatQueryOutputBody'
+ examples:
+ formattedQuery:
+ summary: Formatted PromQL query
+ value:
+ data: sum by(job, status) (rate(http_requests_total[5m]))
+ status: success
+ default:
+ description: Error formatting query.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ post:
+ tags:
+ - query
+ summary: Format a PromQL query
+ operationId: format-query-post
+ requestBody:
+ description: Submit a PromQL query to format. This endpoint accepts the same parameters as the GET version.
+ content:
+ application/x-www-form-urlencoded:
+ schema:
+ $ref: '#/components/schemas/FormatQueryPostInputBody'
+ examples:
+ simpleFormat:
+ summary: Format a simple query
+ value:
+ query: up{job="prometheus"}
+ complexFormat:
+ summary: Format a complex query
+ value:
+ query: sum(rate(http_requests_total[5m])) by (job, status)
+ required: true
+ responses:
+ "200":
+ description: Query formatting completed successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/FormatQueryOutputBody'
+ examples:
+ formattedQuery:
+ summary: Formatted PromQL query
+ value:
+ data: sum by(job, status) (rate(http_requests_total[5m]))
+ status: success
+ default:
+ description: Error formatting query.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ /parse_query:
+ get:
+ tags:
+ - query
+ summary: Parse a PromQL query
+ operationId: parse-query
+ parameters:
+ - name: query
+ in: query
+ description: PromQL expression to parse.
+ required: true
+ explode: false
+ schema:
+ type: string
+ examples:
+ example:
+ value: up{job="prometheus"}
+ responses:
+ "200":
+ description: Query parsed successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ParseQueryOutputBody'
+ examples:
+ parsedQuery:
+ summary: Parsed PromQL expression tree
+ value:
+ data:
+ resultType: vector
+ status: success
+ default:
+ description: Error parsing query.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ post:
+ tags:
+ - query
+ summary: Parse a PromQL query
+ operationId: parse-query-post
+ requestBody:
+ description: Submit a PromQL query to parse. This endpoint accepts the same parameters as the GET version.
+ content:
+ application/x-www-form-urlencoded:
+ schema:
+ $ref: '#/components/schemas/ParseQueryPostInputBody'
+ examples:
+ simpleParse:
+ summary: Parse a simple query
+ value:
+ query: up
+ complexParse:
+ summary: Parse a complex query
+ value:
+ query: rate(http_requests_total{job="api"}[5m])
+ required: true
+ responses:
+ "200":
+ description: Query parsed successfully via POST.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ParseQueryOutputBody'
+ examples:
+ parsedQuery:
+ summary: Parsed PromQL expression tree
+ value:
+ data:
+ resultType: vector
+ status: success
+ default:
+ description: Error parsing query via POST.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ /labels:
+ get:
+ tags:
+ - labels
+ summary: Get label names
+ operationId: labels
+ parameters:
+ - name: start
+ in: query
+ description: Start timestamp for label names query.
+ required: false
+ explode: false
+ schema:
+ oneOf:
+ - type: string
+ format: date-time
+ description: RFC3339 timestamp.
+ - type: number
+ format: unixtime
+ description: Unix timestamp in seconds.
+ description: Timestamp in RFC3339 format or Unix timestamp in seconds.
+ examples:
+ RFC3339:
+ value: "2026-01-02T12:37:00Z"
+ epoch:
+ value: 1767357420
+ - name: end
+ in: query
+ description: End timestamp for label names query.
+ required: false
+ explode: false
+ schema:
+ oneOf:
+ - type: string
+ format: date-time
+ description: RFC3339 timestamp.
+ - type: number
+ format: unixtime
+ description: Unix timestamp in seconds.
+ description: Timestamp in RFC3339 format or Unix timestamp in seconds.
+ examples:
+ RFC3339:
+ value: "2026-01-02T13:37:00Z"
+ epoch:
+ value: 1767361020
+ - name: match[]
+ in: query
+ description: Series selector argument.
+ required: false
+ explode: false
+ schema:
+ type: array
+ items:
+ type: string
+ examples:
+ example:
+ value:
+ - '{job="prometheus"}'
+ - name: limit
+ in: query
+ description: Maximum number of label names to return.
+ required: false
+ explode: false
+ schema:
+ type: integer
+ format: int64
+ examples:
+ example:
+ value: 100
+ responses:
+ "200":
+ description: Label names retrieved successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/LabelsOutputBody'
+ examples:
+ labelNames:
+ summary: List of label names
+ value:
+ data:
+ - __name__
+ - active
+ - address
+ - alertmanager
+ - alertname
+ - alertstate
+ - backend
+ - branch
+ - code
+ - collector
+ - component
+ - device
+ - env
+ - endpoint
+ - fstype
+ - handler
+ - instance
+ - job
+ - le
+ - method
+ - mode
+ - name
+ status: success
+ default:
+ description: Error retrieving label names.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ post:
+ tags:
+ - labels
+ summary: Get label names
+ operationId: labels-post
+ requestBody:
+ description: Submit a label names query. This endpoint accepts the same parameters as the GET version.
+ content:
+ application/x-www-form-urlencoded:
+ schema:
+ $ref: '#/components/schemas/LabelsPostInputBody'
+ examples:
+ allLabels:
+ summary: Get all label names
+ value: {}
+ labelsWithTimeRange:
+ summary: Get label names within time range
+ value:
+ end: "2026-01-02T13:37:00.000Z"
+ start: "2026-01-02T12:37:00.000Z"
+ labelsWithMatch:
+ summary: Get label names matching series selector
+ value:
+ match[]:
+ - up
+ - process_start_time_seconds{job="prometheus"}
+ required: true
+ responses:
+ "200":
+ description: Label names retrieved successfully via POST.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/LabelsOutputBody'
+ examples:
+ labelNames:
+ summary: List of label names
+ value:
+ data:
+ - __name__
+ - active
+ - address
+ - alertmanager
+ - alertname
+ - alertstate
+ - backend
+ - branch
+ - code
+ - collector
+ - component
+ - device
+ - env
+ - endpoint
+ - fstype
+ - handler
+ - instance
+ - job
+ - le
+ - method
+ - mode
+ - name
+ status: success
+ default:
+ description: Error retrieving label names via POST.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ /label/{name}/values:
+ get:
+ tags:
+ - labels
+ summary: Get label values
+ operationId: label-values
+ parameters:
+ - name: name
+ in: path
+ description: Label name.
+ required: true
+ schema:
+ type: string
+ - name: start
+ in: query
+ description: Start timestamp for label values query.
+ required: false
+ explode: false
+ schema:
+ oneOf:
+ - type: string
+ format: date-time
+ description: RFC3339 timestamp.
+ - type: number
+ format: unixtime
+ description: Unix timestamp in seconds.
+ description: Timestamp in RFC3339 format or Unix timestamp in seconds.
+ examples:
+ RFC3339:
+ value: "2026-01-02T12:37:00Z"
+ epoch:
+ value: 1767357420
+ - name: end
+ in: query
+ description: End timestamp for label values query.
+ required: false
+ explode: false
+ schema:
+ oneOf:
+ - type: string
+ format: date-time
+ description: RFC3339 timestamp.
+ - type: number
+ format: unixtime
+ description: Unix timestamp in seconds.
+ description: Timestamp in RFC3339 format or Unix timestamp in seconds.
+ examples:
+ RFC3339:
+ value: "2026-01-02T13:37:00Z"
+ epoch:
+ value: 1767361020
+ - name: match[]
+ in: query
+ description: Series selector argument.
+ required: false
+ explode: false
+ schema:
+ type: array
+ items:
+ type: string
+ examples:
+ example:
+ value:
+ - '{job="prometheus"}'
+ - name: limit
+ in: query
+ description: Maximum number of label values to return.
+ required: false
+ explode: false
+ schema:
+ type: integer
+ format: int64
+ examples:
+ example:
+ value: 1000
+ responses:
+ "200":
+ description: Label values retrieved successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/LabelValuesOutputBody'
+ examples:
+ labelValues:
+ summary: List of values for a label
+ value:
+ data:
+ - alertmanager
+ - blackbox
+ - caddy
+ - cadvisor
+ - grafana
+ - node
+ - prometheus
+ - random
+ status: success
+ default:
+ description: Error retrieving label values.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ /series:
+ get:
+ tags:
+ - series
+ summary: Find series by label matchers
+ operationId: series
+ parameters:
+ - name: start
+ in: query
+ description: Start timestamp for series query.
+ required: false
+ explode: false
+ schema:
+ oneOf:
+ - type: string
+ format: date-time
+ description: RFC3339 timestamp.
+ - type: number
+ format: unixtime
+ description: Unix timestamp in seconds.
+ description: Timestamp in RFC3339 format or Unix timestamp in seconds.
+ examples:
+ RFC3339:
+ value: "2026-01-02T12:37:00Z"
+ epoch:
+ value: 1767357420
+ - name: end
+ in: query
+ description: End timestamp for series query.
+ required: false
+ explode: false
+ schema:
+ oneOf:
+ - type: string
+ format: date-time
+ description: RFC3339 timestamp.
+ - type: number
+ format: unixtime
+ description: Unix timestamp in seconds.
+ description: Timestamp in RFC3339 format or Unix timestamp in seconds.
+ examples:
+ RFC3339:
+ value: "2026-01-02T13:37:00Z"
+ epoch:
+ value: 1767361020
+ - name: match[]
+ in: query
+ description: Series selector argument.
+ required: true
+ explode: false
+ schema:
+ type: array
+ items:
+ type: string
+ examples:
+ example:
+ value:
+ - '{job="prometheus"}'
+ - name: limit
+ in: query
+ description: Maximum number of series to return.
+ required: false
+ explode: false
+ schema:
+ type: integer
+ format: int64
+ examples:
+ example:
+ value: 100
+ responses:
+ "200":
+ description: Series returned matching the provided label matchers.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/SeriesOutputBody'
+ examples:
+ seriesList:
+ summary: List of series matching the selector
+ value:
+ data:
+ - __name__: up
+ env: demo
+ instance: demo.prometheus.io:8080
+ job: cadvisor
+ - __name__: up
+ env: demo
+ instance: demo.prometheus.io:9093
+ job: alertmanager
+ - __name__: up
+ env: demo
+ instance: demo.prometheus.io:9100
+ job: node
+ - __name__: up
+ instance: demo.prometheus.io:3000
+ job: grafana
+ - __name__: up
+ instance: demo.prometheus.io:8996
+ job: random
+ status: success
+ default:
+ description: Error retrieving series.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ post:
+ tags:
+ - series
+ summary: Find series by label matchers
+ operationId: series-post
+ requestBody:
+ description: Submit a series query. This endpoint accepts the same parameters as the GET version.
+ content:
+ application/x-www-form-urlencoded:
+ schema:
+ $ref: '#/components/schemas/SeriesPostInputBody'
+ examples:
+ seriesMatch:
+ summary: Find series by label matchers
+ value:
+ match[]:
+ - up
+ seriesWithTimeRange:
+ summary: Find series with time range
+ value:
+ end: "2026-01-02T13:37:00.000Z"
+ match[]:
+ - up
+ - process_cpu_seconds_total{job="prometheus"}
+ start: "2026-01-02T12:37:00.000Z"
+ required: true
+ responses:
+ "200":
+ description: Series returned matching the provided label matchers via POST.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/SeriesOutputBody'
+ examples:
+ seriesList:
+ summary: List of series matching the selector
+ value:
+ data:
+ - __name__: up
+ env: demo
+ instance: demo.prometheus.io:8080
+ job: cadvisor
+ - __name__: up
+ env: demo
+ instance: demo.prometheus.io:9093
+ job: alertmanager
+ - __name__: up
+ env: demo
+ instance: demo.prometheus.io:9100
+ job: node
+ - __name__: up
+ instance: demo.prometheus.io:3000
+ job: grafana
+ - __name__: up
+ instance: demo.prometheus.io:8996
+ job: random
+ status: success
+ default:
+ description: Error retrieving series via POST.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ delete:
+ tags:
+ - series
+ summary: Delete series
+ description: 'Delete series matching selectors. Note: This is deprecated, use POST /admin/tsdb/delete_series instead.'
+ operationId: delete-series
+ responses:
+ "200":
+ description: Series marked for deletion.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/SeriesDeleteOutputBody'
+ examples:
+ seriesDeleted:
+ summary: Series marked for deletion
+ value:
+ status: success
+ default:
+ description: Error deleting series.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ /metadata:
+ get:
+ tags:
+ - metadata
+ summary: Get metadata
+ operationId: get-metadata
+ parameters:
+ - name: limit
+ in: query
+ description: The maximum number of metrics to return.
+ required: false
+ explode: false
+ schema:
+ type: integer
+ format: int64
+ examples:
+ example:
+ value: 100
+ - name: limit_per_metric
+ in: query
+ description: The maximum number of metadata entries per metric.
+ required: false
+ explode: false
+ schema:
+ type: integer
+ format: int64
+ examples:
+ example:
+ value: 10
+ - name: metric
+ in: query
+ description: A metric name to filter metadata for.
+ required: false
+ explode: false
+ schema:
+ type: string
+ examples:
+ example:
+ value: http_requests_total
+ responses:
+ "200":
+ description: Metric metadata retrieved successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/MetadataOutputBody'
+ examples:
+ metricMetadata:
+ summary: Metadata for metrics
+ value:
+ data:
+ go_gc_stack_starting_size_bytes:
+ - help: The stack size of new goroutines. Sourced from /gc/stack/starting-size:bytes.
+ type: gauge
+ unit: ""
+ prometheus_rule_group_iterations_missed_total:
+ - help: The total number of rule group evaluations missed due to slow rule group evaluation.
+ type: counter
+ unit: ""
+ prometheus_sd_updates_total:
+ - help: Total number of update events sent to the SD consumers.
+ type: counter
+ unit: ""
+ status: success
+ default:
+ description: Error retrieving metadata.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ /scrape_pools:
+ get:
+ tags:
+ - targets
+ summary: Get scrape pools
+ operationId: get-scrape-pools
+ responses:
+ "200":
+ description: Scrape pools retrieved successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ScrapePoolsOutputBody'
+ examples:
+ scrapePoolsList:
+ summary: List of scrape pool names
+ value:
+ data:
+ scrapePools:
+ - alertmanager
+ - blackbox
+ - caddy
+ - cadvisor
+ - grafana
+ - node
+ - prometheus
+ - random
+ status: success
+ default:
+ description: Error retrieving scrape pools.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ /targets:
+ get:
+ tags:
+ - targets
+ summary: Get targets
+ operationId: get-targets
+ parameters:
+ - name: scrapePool
+ in: query
+ description: Filter targets by scrape pool name.
+ required: false
+ explode: false
+ schema:
+ type: string
+ examples:
+ example:
+ value: prometheus
+ - name: state
+ in: query
+ description: 'Filter by state: active, dropped, or any.'
+ required: false
+ explode: false
+ schema:
+ type: string
+ examples:
+ example:
+ value: active
+ responses:
+ "200":
+ description: Target discovery information retrieved successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/TargetsOutputBody'
+ examples:
+ targetsList:
+ summary: Active and dropped targets
+ value:
+ data:
+ activeTargets:
+ - discoveredLabels:
+ __address__: demo.prometheus.io:9093
+ __meta_filepath: /etc/prometheus/file_sd/alertmanager.yml
+ __metrics_path__: /metrics
+ __scheme__: http
+ env: demo
+ job: alertmanager
+ globalUrl: http://demo.prometheus.io:9093/metrics
+ health: up
+ labels:
+ env: demo
+ instance: demo.prometheus.io:9093
+ job: alertmanager
+ lastError: ""
+ lastScrape: "2026-01-02T13:36:40.200Z"
+ lastScrapeDuration: 0.006576866
+ scrapeInterval: 15s
+ scrapePool: alertmanager
+ scrapeTimeout: 10s
+ scrapeUrl: http://demo.prometheus.io:9093/metrics
+ droppedTargetCounts:
+ alertmanager: 0
+ blackbox: 0
+ caddy: 0
+ cadvisor: 0
+ grafana: 0
+ node: 0
+ prometheus: 0
+ random: 0
+ droppedTargets: []
+ status: success
+ default:
+ description: Error retrieving targets.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ /targets/metadata:
+ get:
+ tags:
+ - targets
+ summary: Get targets metadata
+ operationId: get-targets-metadata
+ parameters:
+ - name: match_target
+ in: query
+ description: Label selector to filter targets.
+ required: false
+ explode: false
+ schema:
+ type: string
+ examples:
+ example:
+ value: '{job="prometheus"}'
+ - name: metric
+ in: query
+ description: Metric name to retrieve metadata for.
+ required: false
+ explode: false
+ schema:
+ type: string
+ examples:
+ example:
+ value: http_requests_total
+ - name: limit
+ in: query
+ description: Maximum number of targets to match.
+ required: false
+ explode: false
+ schema:
+ type: integer
+ format: int64
+ examples:
+ example:
+ value: 10
+ responses:
+ "200":
+ description: Target metadata retrieved successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/TargetMetadataOutputBody'
+ examples:
+ targetMetadata:
+ summary: Metadata for targets
+ value:
+ data:
+ - help: The current health status of the target
+ metric: up
+ target:
+ instance: localhost:9090
+ job: prometheus
+ type: gauge
+ unit: ""
+ status: success
+ default:
+ description: Error retrieving target metadata.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ /targets/relabel_steps:
+ get:
+ tags:
+ - targets
+ summary: Get targets relabel steps
+ operationId: get-targets-relabel-steps
+ parameters:
+ - name: scrapePool
+ in: query
+ description: Name of the scrape pool.
+ required: true
+ explode: false
+ schema:
+ type: string
+ examples:
+ example:
+ value: prometheus
+ - name: labels
+ in: query
+ description: JSON-encoded labels to apply relabel rules to.
+ required: true
+ explode: false
+ schema:
+ type: string
+ examples:
+ example:
+ value: '{"__address__":"localhost:9090","job":"prometheus"}'
+ responses:
+ "200":
+ description: Relabel steps retrieved successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/TargetRelabelStepsOutputBody'
+ examples:
+ relabelSteps:
+ summary: Relabel steps for a target
+ value:
+ data:
+ steps:
+ - keep: true
+ output:
+ __address__: localhost:9090
+ instance: localhost:9090
+ job: prometheus
+ rule:
+ action: replace
+ regex: (.*)
+ replacement: $1
+ source_labels:
+ - __address__
+ target_label: instance
+ status: success
+ default:
+ description: Error retrieving relabel steps.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ /rules:
+ get:
+ tags:
+ - rules
+ summary: Get alerting and recording rules
+ operationId: rules
+ parameters:
+ - name: type
+ in: query
+ description: 'Filter by rule type: alert or record.'
+ required: false
+ explode: false
+ schema:
+ type: string
+ examples:
+ example:
+ value: alert
+ - name: rule_name[]
+ in: query
+ description: Filter by rule name.
+ required: false
+ explode: false
+ schema:
+ type: array
+ items:
+ type: string
+ examples:
+ example:
+ value:
+ - HighErrorRate
+ - name: rule_group[]
+ in: query
+ description: Filter by rule group name.
+ required: false
+ explode: false
+ schema:
+ type: array
+ items:
+ type: string
+ examples:
+ example:
+ value:
+ - example_alerts
+ - name: file[]
+ in: query
+ description: Filter by file path.
+ required: false
+ explode: false
+ schema:
+ type: array
+ items:
+ type: string
+ examples:
+ example:
+ value:
+ - /etc/prometheus/rules.yml
+ - name: match[]
+ in: query
+ description: Label matchers to filter rules.
+ required: false
+ explode: false
+ schema:
+ type: array
+ items:
+ type: string
+ examples:
+ example:
+ value:
+ - '{severity="critical"}'
+ - name: exclude_alerts
+ in: query
+ description: Exclude active alerts from response.
+ required: false
+ explode: false
+ schema:
+ type: string
+ examples:
+ example:
+ value: "false"
+ - name: group_limit
+ in: query
+ description: Maximum number of rule groups to return.
+ required: false
+ explode: false
+ schema:
+ type: integer
+ format: int64
+ examples:
+ example:
+ value: 100
+ - name: group_next_token
+ in: query
+ description: Pagination token for next page.
+ required: false
+ explode: false
+ schema:
+ type: string
+ examples:
+ example:
+ value: abc123
+ responses:
+ "200":
+ description: Rules retrieved successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/RulesOutputBody'
+ examples:
+ ruleGroups:
+ summary: Alerting and recording rules
+ value:
+ data:
+ groups:
+ - evaluationTime: 0.000561635
+ file: /etc/prometheus/rules/ansible_managed.yml
+ interval: 15
+ lastEvaluation: "2026-01-02T13:36:56.874Z"
+ limit: 0
+ name: ansible managed alert rules
+ rules:
+ - annotations:
+ description: This is an alert meant to ensure that the entire alerting pipeline is functional. This alert is always firing, therefore it should always be firing in Alertmanager and always fire against a receiver. There are integrations with various notification mechanisms that send a notification when this alert is not firing. For example the "DeadMansSnitch" integration in PagerDuty.
+ summary: Ensure entire alerting pipeline is functional
+ duration: 600
+ evaluationTime: 0.000356688
+ health: ok
+ keepFiringFor: 0
+ labels:
+ severity: warning
+ lastEvaluation: "2026-01-02T13:36:56.874Z"
+ name: Watchdog
+ query: vector(1)
+ state: firing
+ type: alerting
+ status: success
+ default:
+ description: Error retrieving rules.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ /alerts:
+ get:
+ tags:
+ - alerts
+ summary: Get active alerts
+ operationId: alerts
+ responses:
+ "200":
+ description: Active alerts retrieved successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/AlertsOutputBody'
+ examples:
+ activeAlerts:
+ summary: Currently active alerts
+ value:
+ data:
+ alerts:
+ - activeAt: "2026-01-02T13:30:00.000Z"
+ annotations:
+ description: This is an alert meant to ensure that the entire alerting pipeline is functional. This alert is always firing, therefore it should always be firing in Alertmanager and always fire against a receiver. There are integrations with various notification mechanisms that send a notification when this alert is not firing. For example the "DeadMansSnitch" integration in PagerDuty.
+ summary: Ensure entire alerting pipeline is functional
+ labels:
+ alertname: Watchdog
+ severity: warning
+ state: firing
+ value: "1e+00"
+ status: success
+ default:
+ description: Error retrieving alerts.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ /alertmanagers:
+ get:
+ tags:
+ - alerts
+ summary: Get Alertmanager discovery
+ operationId: alertmanagers
+ responses:
+ "200":
+ description: Alertmanager targets retrieved successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/AlertmanagersOutputBody'
+ examples:
+ alertmanagerDiscovery:
+ summary: Alertmanager discovery results
+ value:
+ data:
+ activeAlertmanagers:
+ - url: http://demo.prometheus.io:9093/api/v2/alerts
+ droppedAlertmanagers: []
+ status: success
+ default:
+ description: Error retrieving Alertmanager targets.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ /status/config:
+ get:
+ tags:
+ - status
+ summary: Get status config
+ operationId: get-status-config
+ responses:
+ "200":
+ description: Configuration retrieved successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/StatusConfigOutputBody'
+ examples:
+ configYAML:
+ summary: Prometheus configuration
+ value:
+ data:
+ yaml: |
+ global:
+ scrape_interval: 15s
+ scrape_timeout: 10s
+ evaluation_interval: 15s
+ external_labels:
+ environment: demo-prometheus-io
+ alerting:
+ alertmanagers:
+ - scheme: http
+ static_configs:
+ - targets:
+ - demo.prometheus.io:9093
+ rule_files:
+ - /etc/prometheus/rules/*.yml
+ status: success
+ default:
+ description: Error retrieving configuration.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ /status/runtimeinfo:
+ get:
+ tags:
+ - status
+ summary: Get status runtimeinfo
+ operationId: get-status-runtimeinfo
+ responses:
+ "200":
+ description: Runtime information retrieved successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/StatusRuntimeInfoOutputBody'
+ examples:
+ runtimeInfo:
+ summary: Runtime information
+ value:
+ data:
+ CWD: /
+ GODEBUG: ""
+ GOGC: "75"
+ GOMAXPROCS: 2
+ GOMEMLIMIT: 3703818240
+ corruptionCount: 0
+ goroutineCount: 88
+ hostname: demo-prometheus-io
+ lastConfigTime: "2026-01-01T13:37:00.000Z"
+ reloadConfigSuccess: true
+ serverTime: "2026-01-02T13:37:00.000Z"
+ startTime: "2026-01-01T13:37:00.000Z"
+ storageRetention: 31d
+ status: success
+ default:
+ description: Error retrieving runtime information.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ /status/buildinfo:
+ get:
+ tags:
+ - status
+ summary: Get status buildinfo
+ operationId: get-status-buildinfo
+ responses:
+ "200":
+ description: Build information retrieved successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/StatusBuildInfoOutputBody'
+ examples:
+ buildInfo:
+ summary: Build information
+ value:
+ data:
+ branch: HEAD
+ buildDate: 20251030-07:26:10
+ buildUser: root@08c890a84441
+ goVersion: go1.25.3
+ revision: 0a41f0000705c69ab8e0f9a723fc73e39ed62b07
+ version: 3.7.3
+ status: success
+ default:
+ description: Error retrieving build information.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ /status/flags:
+ get:
+ tags:
+ - status
+ summary: Get status flags
+ operationId: get-status-flags
+ responses:
+ "200":
+ description: Command-line flags retrieved successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/StatusFlagsOutputBody'
+ examples:
+ flags:
+ summary: Command-line flags
+ value:
+ data:
+ agent: "false"
+ alertmanager.notification-queue-capacity: "10000"
+ config.file: /etc/prometheus/prometheus.yml
+ enable-feature: exemplar-storage,native-histograms
+ query.max-concurrency: "20"
+ query.timeout: 2m
+ storage.tsdb.path: /prometheus
+ storage.tsdb.retention.time: 15d
+ web.console.libraries: /usr/share/prometheus/console_libraries
+ web.console.templates: /usr/share/prometheus/consoles
+ web.enable-admin-api: "true"
+ web.enable-lifecycle: "true"
+ web.listen-address: 0.0.0.0:9090
+ web.page-title: Prometheus Time Series Collection and Processing Server
+ status: success
+ default:
+ description: Error retrieving flags.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ /status/tsdb:
+ get:
+ tags:
+ - status
+ summary: Get TSDB status
+ operationId: status-tsdb
+ parameters:
+ - name: limit
+ in: query
+ description: The maximum number of items to return per category.
+ required: false
+ explode: false
+ schema:
+ type: integer
+ format: int64
+ examples:
+ example:
+ value: 10
+ responses:
+ "200":
+ description: TSDB status retrieved successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/StatusTSDBOutputBody'
+ examples:
+ tsdbStats:
+ summary: TSDB statistics
+ value:
+ data:
+ headStats:
+ chunkCount: 37525
+ maxTime: 1767436620000
+ minTime: 1767362400712
+ numLabelPairs: 2512
+ numSeries: 9925
+ labelValueCountByLabelName:
+ - name: __name__
+ value: 5
+ - name: job
+ value: 3
+ memoryInBytesByLabelName:
+ - name: __name__
+ value: 1024
+ - name: job
+ value: 512
+ seriesCountByLabelValuePair:
+ - name: job=prometheus
+ value: 100
+ - name: instance=localhost:9090
+ value: 100
+ seriesCountByMetricName:
+ - name: up
+ value: 100
+ - name: http_requests_total
+ value: 500
+ status: success
+ default:
+ description: Error retrieving TSDB status.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ /status/tsdb/blocks:
+ get:
+ tags:
+ - status
+ summary: Get TSDB blocks information
+ operationId: status-tsdb-blocks
+ responses:
+ "200":
+ description: TSDB blocks information retrieved successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/StatusTSDBBlocksOutputBody'
+ examples:
+ tsdbBlocks:
+ summary: TSDB block information
+ value:
+ data:
+ blocks:
+ - compaction:
+ level: 4
+ sources:
+ - 01KBCJ7TR8A4QAJ3AA1J651P5S
+ - 01KBCS3J0E34567YPB8Y5W0E24
+ - 01KBCZZ9KRTYGG3E7HVQFGC3S3
+ maxTime: 1764763200000
+ minTime: 1764568801099
+ stats:
+ numChunks: 1073962
+ numSamples: 129505582
+ numSeries: 10661
+ ulid: 01KC4D6GXQA4CRHYKV78NEBVAE
+ version: 1
+ status: success
+ default:
+ description: Error retrieving TSDB blocks.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ /status/walreplay:
+ get:
+ tags:
+ - status
+ summary: Get status walreplay
+ operationId: get-status-walreplay
+ responses:
+ "200":
+ description: WAL replay status retrieved successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/StatusWALReplayOutputBody'
+ examples:
+ walReplay:
+ summary: WAL replay status
+ value:
+ data:
+ current: 3214
+ max: 3214
+ min: 3209
+ status: success
+ default:
+ description: Error retrieving WAL replay status.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ /admin/tsdb/delete_series:
+ put:
+ tags:
+ - admin
+ summary: Delete series matching selectors via PUT
+ description: Deletes data for a selection of series in a time range using PUT method.
+ operationId: deleteSeriesPut
+ parameters:
+ - name: match[]
+ in: query
+ description: Series selectors to identify series to delete.
+ required: true
+ explode: false
+ schema:
+ type: array
+ items:
+ type: string
+ examples:
+ example:
+ value:
+ - '{__name__=~"test.*"}'
+ - name: start
+ in: query
+ description: Start timestamp for deletion.
+ required: false
+ explode: false
+ schema:
+ oneOf:
+ - type: string
+ format: date-time
+ description: RFC3339 timestamp.
+ - type: number
+ format: unixtime
+ description: Unix timestamp in seconds.
+ description: Timestamp in RFC3339 format or Unix timestamp in seconds.
+ examples:
+ RFC3339:
+ value: "2026-01-02T12:37:00Z"
+ epoch:
+ value: 1767357420
+ - name: end
+ in: query
+ description: End timestamp for deletion.
+ required: false
+ explode: false
+ schema:
+ oneOf:
+ - type: string
+ format: date-time
+ description: RFC3339 timestamp.
+ - type: number
+ format: unixtime
+ description: Unix timestamp in seconds.
+ description: Timestamp in RFC3339 format or Unix timestamp in seconds.
+ examples:
+ RFC3339:
+ value: "2026-01-02T13:37:00Z"
+ epoch:
+ value: 1767361020
+ responses:
+ "200":
+ description: Series deleted successfully via PUT.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/DeleteSeriesOutputBody'
+ examples:
+ deletionSuccess:
+ summary: Successful series deletion
+ value:
+ status: success
+ default:
+ description: Error deleting series via PUT.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ post:
+ tags:
+ - admin
+ summary: Delete series matching selectors
+ description: Deletes data for a selection of series in a time range.
+ operationId: deleteSeriesPost
+ parameters:
+ - name: match[]
+ in: query
+ description: Series selectors to identify series to delete.
+ required: true
+ explode: false
+ schema:
+ type: array
+ items:
+ type: string
+ examples:
+ example:
+ value:
+ - '{__name__=~"test.*"}'
+ - name: start
+ in: query
+ description: Start timestamp for deletion.
+ required: false
+ explode: false
+ schema:
+ oneOf:
+ - type: string
+ format: date-time
+ description: RFC3339 timestamp.
+ - type: number
+ format: unixtime
+ description: Unix timestamp in seconds.
+ description: Timestamp in RFC3339 format or Unix timestamp in seconds.
+ examples:
+ RFC3339:
+ value: "2026-01-02T12:37:00Z"
+ epoch:
+ value: 1767357420
+ - name: end
+ in: query
+ description: End timestamp for deletion.
+ required: false
+ explode: false
+ schema:
+ oneOf:
+ - type: string
+ format: date-time
+ description: RFC3339 timestamp.
+ - type: number
+ format: unixtime
+ description: Unix timestamp in seconds.
+ description: Timestamp in RFC3339 format or Unix timestamp in seconds.
+ examples:
+ RFC3339:
+ value: "2026-01-02T13:37:00Z"
+ epoch:
+ value: 1767361020
+ responses:
+ "200":
+ description: Series deleted successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/DeleteSeriesOutputBody'
+ examples:
+ deletionSuccess:
+ summary: Successful series deletion
+ value:
+ status: success
+ default:
+ description: Error deleting series.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ /admin/tsdb/clean_tombstones:
+ put:
+ tags:
+ - admin
+ summary: Clean tombstones in the TSDB via PUT
+ description: Removes deleted data from disk and cleans up existing tombstones using PUT method.
+ operationId: cleanTombstonesPut
+ responses:
+ "200":
+ description: Tombstones cleaned successfully via PUT.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CleanTombstonesOutputBody'
+ examples:
+ tombstonesCleaned:
+ summary: Tombstones cleaned successfully
+ value:
+ status: success
+ default:
+ description: Error cleaning tombstones via PUT.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ post:
+ tags:
+ - admin
+ summary: Clean tombstones in the TSDB
+ description: Removes deleted data from disk and cleans up existing tombstones.
+ operationId: cleanTombstonesPost
+ responses:
+ "200":
+ description: Tombstones cleaned successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CleanTombstonesOutputBody'
+ examples:
+ tombstonesCleaned:
+ summary: Tombstones cleaned successfully
+ value:
+ status: success
+ default:
+ description: Error cleaning tombstones.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ /admin/tsdb/snapshot:
+ put:
+ tags:
+ - admin
+ summary: Create a snapshot of the TSDB via PUT
+ description: Creates a snapshot of all current data using PUT method.
+ operationId: snapshotPut
+ parameters:
+ - name: skip_head
+ in: query
+ description: If true, do not snapshot data in the head block.
+ required: false
+ explode: false
+ schema:
+ type: string
+ examples:
+ example:
+ value: "false"
+ responses:
+ "200":
+ description: Snapshot created successfully via PUT.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/SnapshotOutputBody'
+ examples:
+ snapshotCreated:
+ summary: Snapshot created successfully
+ value:
+ data:
+ name: 20260102T133700Z-a1b2c3d4e5f67890
+ status: success
+ default:
+ description: Error creating snapshot via PUT.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ post:
+ tags:
+ - admin
+ summary: Create a snapshot of the TSDB
+ description: Creates a snapshot of all current data.
+ operationId: snapshotPost
+ parameters:
+ - name: skip_head
+ in: query
+ description: If true, do not snapshot data in the head block.
+ required: false
+ explode: false
+ schema:
+ type: string
+ examples:
+ example:
+ value: "false"
+ responses:
+ "200":
+ description: Snapshot created successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/SnapshotOutputBody'
+ examples:
+ snapshotCreated:
+ summary: Snapshot created successfully
+ value:
+ data:
+ name: 20260102T133700Z-a1b2c3d4e5f67890
+ status: success
+ default:
+ description: Error creating snapshot.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ /read:
+ post:
+ tags:
+ - remote
+ summary: Remote read endpoint
+ description: Prometheus remote read endpoint for federated queries. Accepts and returns Protocol Buffer encoded data.
+ operationId: remoteRead
+ responses:
+ "204":
+ description: No Content
+ default:
+ description: Error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ /write:
+ post:
+ tags:
+ - remote
+ summary: Remote write endpoint
+ description: Prometheus remote write endpoint for sending metrics. Accepts Protocol Buffer encoded write requests.
+ operationId: remoteWrite
+ responses:
+ "204":
+ description: No Content
+ default:
+ description: Error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ /otlp/v1/metrics:
+ post:
+ tags:
+ - otlp
+ summary: OTLP metrics write endpoint
+ description: OpenTelemetry Protocol metrics ingestion endpoint. Accepts OTLP/HTTP metrics in Protocol Buffer format.
+ operationId: otlpWrite
+ responses:
+ "204":
+ description: No Content
+ default:
+ description: Error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ /notifications:
+ get:
+ tags:
+ - notifications
+ summary: Get notifications
+ operationId: get-notifications
+ responses:
+ "200":
+ description: Notifications retrieved successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/NotificationsOutputBody'
+ examples:
+ notifications:
+ summary: Server notifications
+ value:
+ data:
+ - active: true
+ date: "2026-01-02T16:14:50.046Z"
+ text: Configuration reload has failed.
+ status: success
+ default:
+ description: Error retrieving notifications.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ /features:
+ get:
+ tags:
+ - features
+ summary: Get features
+ operationId: get-features
+ responses:
+ "200":
+ description: Feature flags retrieved successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/FeaturesOutputBody'
+ examples:
+ enabledFeatures:
+ summary: Enabled feature flags
+ value:
+ data:
+ - exemplar-storage
+ - remote-write-receiver
+ status: success
+ default:
+ description: Error retrieving features.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+components:
+ schemas:
+ Error:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ errorType:
+ type: string
+ description: Type of error that occurred.
+ example: bad_data
+ error:
+ type: string
+ description: Human-readable error message.
+ example: invalid parameter
+ required:
+ - status
+ - errorType
+ - error
+ additionalProperties: false
+ description: Error response.
+ Labels:
+ type: object
+ additionalProperties: true
+ description: Label set represented as a key-value map.
+ QueryOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ data:
+ $ref: '#/components/schemas/QueryData'
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ - data
+ additionalProperties: false
+ description: Response body for instant query.
+ QueryRangeOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ data:
+ $ref: '#/components/schemas/QueryData'
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ - data
+ additionalProperties: false
+ description: Response body for range query.
+ QueryPostInputBody:
+ type: object
+ properties:
+ query:
+ type: string
+ description: 'Form field: The PromQL query to execute.'
+ example: up
+ time:
+ type: string
+ description: 'Form field: The evaluation timestamp (optional, defaults to current time).'
+ example: "2023-07-21T20:10:51.781Z"
+ limit:
+ type: integer
+ format: int64
+ description: 'Form field: The maximum number of metrics to return.'
+ example: 100
+ timeout:
+ type: string
+ description: 'Form field: Evaluation timeout (optional, defaults to and is capped by the value of the -query.timeout flag).'
+ example: 30s
+ lookback_delta:
+ type: string
+ description: 'Form field: Override the lookback period for this query (optional).'
+ example: 5m
+ stats:
+ type: string
+ description: 'Form field: When provided, include query statistics in the response (the special value ''all'' enables more comprehensive statistics).'
+ example: all
+ required:
+ - query
+ additionalProperties: false
+ description: POST request body for instant query.
+ QueryRangePostInputBody:
+ type: object
+ properties:
+ query:
+ type: string
+ description: 'Form field: The query to execute.'
+ example: rate(http_requests_total[5m])
+ start:
+ type: string
+ description: 'Form field: The start time of the query.'
+ example: "2023-07-21T20:10:30.781Z"
+ end:
+ type: string
+ description: 'Form field: The end time of the query.'
+ example: "2023-07-21T20:20:30.781Z"
+ step:
+ type: string
+ description: 'Form field: The step size of the query.'
+ example: 15s
+ limit:
+ type: integer
+ format: int64
+ description: 'Form field: The maximum number of metrics to return.'
+ example: 100
+ timeout:
+ type: string
+ description: 'Form field: Evaluation timeout (optional, defaults to and is capped by the value of the -query.timeout flag).'
+ example: 30s
+ lookback_delta:
+ type: string
+ description: 'Form field: Override the lookback period for this query (optional).'
+ example: 5m
+ stats:
+ type: string
+ description: 'Form field: When provided, include query statistics in the response (the special value ''all'' enables more comprehensive statistics).'
+ example: all
+ required:
+ - query
+ - start
+ - end
+ - step
+ additionalProperties: false
+ description: POST request body for range query.
+ QueryExemplarsOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ data:
+ description: Response data (structure varies by endpoint).
+ example:
+ result: ok
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ - data
+ additionalProperties: false
+ description: Generic response body.
+ QueryExemplarsPostInputBody:
+ type: object
+ properties:
+ query:
+ type: string
+ description: 'Form field: The query to execute.'
+ example: http_requests_total
+ start:
+ type: string
+ description: 'Form field: The start time of the query.'
+ example: "2023-07-21T20:00:00.000Z"
+ end:
+ type: string
+ description: 'Form field: The end time of the query.'
+ example: "2023-07-21T21:00:00.000Z"
+ required:
+ - query
+ additionalProperties: false
+ description: POST request body for exemplars query.
+ FormatQueryOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ data:
+ type: string
+ description: Formatted query string.
+ example: sum by(status) (rate(http_requests_total[5m]))
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ - data
+ additionalProperties: false
+ description: Response body for format query endpoint.
+ FormatQueryPostInputBody:
+ type: object
+ properties:
+ query:
+ type: string
+ description: 'Form field: The query to format.'
+ example: sum(rate(http_requests_total[5m])) by (status)
+ required:
+ - query
+ additionalProperties: false
+ description: POST request body for format query.
+ ParseQueryOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ data:
+ description: Response data (structure varies by endpoint).
+ example:
+ result: ok
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ - data
+ additionalProperties: false
+ description: Generic response body.
+ ParseQueryPostInputBody:
+ type: object
+ properties:
+ query:
+ type: string
+ description: 'Form field: The query to parse.'
+ example: sum(rate(http_requests_total[5m]))
+ required:
+ - query
+ additionalProperties: false
+ description: POST request body for parse query.
+ QueryData:
+ anyOf:
+ - type: object
+ properties:
+ resultType:
+ type: string
+ enum:
+ - vector
+ result:
+ type: array
+ items:
+ anyOf:
+ - $ref: '#/components/schemas/FloatSample'
+ - $ref: '#/components/schemas/HistogramSample'
+ description: Array of samples (either float or histogram).
+ stats:
+ $ref: '#/components/schemas/QueryStats'
+ required:
+ - resultType
+ - result
+ additionalProperties: false
+ - type: object
+ properties:
+ resultType:
+ type: string
+ enum:
+ - matrix
+ result:
+ type: array
+ items:
+ anyOf:
+ - $ref: '#/components/schemas/FloatSeries'
+ - $ref: '#/components/schemas/HistogramSeries'
+ description: Array of time series (either float or histogram).
+ stats:
+ $ref: '#/components/schemas/QueryStats'
+ required:
+ - resultType
+ - result
+ additionalProperties: false
+ - type: object
+ properties:
+ resultType:
+ type: string
+ enum:
+ - scalar
+ result:
+ type: array
+ items:
+ oneOf:
+ - type: number
+ - type: string
+ maxItems: 2
+ minItems: 2
+ description: Scalar value as [timestamp, stringValue].
+ stats:
+ $ref: '#/components/schemas/QueryStats'
+ required:
+ - resultType
+ - result
+ additionalProperties: false
+ - type: object
+ properties:
+ resultType:
+ type: string
+ enum:
+ - string
+ result:
+ type: array
+ items:
+ type: string
+ maxItems: 2
+ minItems: 2
+ description: String value as [timestamp, stringValue].
+ stats:
+ $ref: '#/components/schemas/QueryStats'
+ required:
+ - resultType
+ - result
+ additionalProperties: false
+ description: Query result data. The structure of 'result' depends on 'resultType'.
+ example:
+ result:
+ - metric:
+ __name__: up
+ job: prometheus
+ value:
+ - 1627845600
+ - "1"
+ resultType: vector
+ QueryStats:
+ type: object
+ properties:
+ timings:
+ type: object
+ properties:
+ evalTotalTime:
+ type: number
+ description: Total evaluation time in seconds.
+ resultSortTime:
+ type: number
+ description: Time spent sorting results in seconds.
+ queryPreparationTime:
+ type: number
+ description: Query preparation time in seconds.
+ innerEvalTime:
+ type: number
+ description: Inner evaluation time in seconds.
+ execQueueTime:
+ type: number
+ description: Execution queue wait time in seconds.
+ execTotalTime:
+ type: number
+ description: Total execution time in seconds.
+ samples:
+ type: object
+ properties:
+ totalQueryableSamples:
+ type: integer
+ description: Total number of samples that were queryable.
+ peakSamples:
+ type: integer
+ description: Peak number of samples in memory.
+ totalQueryableSamplesPerStep:
+ type: array
+ items:
+ type: array
+ items:
+ type: number
+ maxItems: 2
+ minItems: 2
+ description: Timestamp and sample count as [timestamp, count].
+ description: Total queryable samples per step (only included with stats=all).
+ description: Query execution statistics (included when the stats query parameter is provided).
+ FloatSample:
+ type: object
+ properties:
+ metric:
+ $ref: '#/components/schemas/Labels'
+ value:
+ type: array
+ items:
+ oneOf:
+ - type: number
+ - type: string
+ maxItems: 2
+ minItems: 2
+ description: Timestamp and float value as [unixTimestamp, stringValue].
+ example:
+ - 1767436620
+ - "1"
+ required:
+ - metric
+ - value
+ additionalProperties: false
+ description: A sample with a float value.
+ HistogramSample:
+ type: object
+ properties:
+ metric:
+ $ref: '#/components/schemas/Labels'
+ histogram:
+ type: array
+ items:
+ oneOf:
+ - type: number
+ - $ref: '#/components/schemas/HistogramValue'
+ maxItems: 2
+ minItems: 2
+ description: Timestamp and histogram value as [unixTimestamp, histogramObject].
+ example:
+ - 1767436620
+ - buckets: []
+ count: "60"
+ sum: "120"
+ required:
+ - metric
+ - histogram
+ additionalProperties: false
+ description: A sample with a native histogram value.
+ FloatSeries:
+ type: object
+ properties:
+ metric:
+ $ref: '#/components/schemas/Labels'
+ values:
+ type: array
+ items:
+ type: array
+ items:
+ oneOf:
+ - type: number
+ - type: string
+ maxItems: 2
+ minItems: 2
+ description: Array of [timestamp, stringValue] pairs for float values.
+ required:
+ - metric
+ - values
+ additionalProperties: false
+ description: A time series with float values.
+ HistogramSeries:
+ type: object
+ properties:
+ metric:
+ $ref: '#/components/schemas/Labels'
+ histograms:
+ type: array
+ items:
+ type: array
+ items:
+ oneOf:
+ - type: number
+ - $ref: '#/components/schemas/HistogramValue'
+ maxItems: 2
+ minItems: 2
+ description: Array of [timestamp, histogramObject] pairs for histogram values.
+ required:
+ - metric
+ - histograms
+ additionalProperties: false
+ description: A time series with native histogram values.
+ HistogramValue:
+ type: object
+ properties:
+ count:
+ type: string
+ description: Total count of observations.
+ sum:
+ type: string
+ description: Sum of all observed values.
+ buckets:
+ type: array
+ items:
+ type: array
+ items:
+ oneOf:
+ - type: number
+ - type: string
+ description: Histogram buckets as [boundary_rule, lower, upper, count].
+ required:
+ - count
+ - sum
+ additionalProperties: false
+ description: Native histogram value representation.
+ LabelsOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ data:
+ type: array
+ items:
+ type: string
+ example:
+ - __name__
+ - job
+ - instance
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ - data
+ additionalProperties: false
+ description: Response body with an array of strings.
+ LabelsPostInputBody:
+ type: object
+ properties:
+ start:
+ type: string
+ description: 'Form field: The start time of the query.'
+ example: "2023-07-21T20:00:00.000Z"
+ end:
+ type: string
+ description: 'Form field: The end time of the query.'
+ example: "2023-07-21T21:00:00.000Z"
+ match[]:
+ type: array
+ items:
+ type: string
+ description: 'Form field: Series selector argument that selects the series from which to read the label names.'
+ example:
+ - '{job="prometheus"}'
+ limit:
+ type: integer
+ format: int64
+ description: 'Form field: The maximum number of label names to return.'
+ example: 100
+ additionalProperties: false
+ description: POST request body for labels query.
+ LabelValuesOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ data:
+ type: array
+ items:
+ type: string
+ example:
+ - __name__
+ - job
+ - instance
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ - data
+ additionalProperties: false
+ description: Response body with an array of strings.
+ SeriesOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ data:
+ type: array
+ items:
+ $ref: '#/components/schemas/Labels'
+ example:
+ - __name__: up
+ instance: localhost:9090
+ job: prometheus
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ - data
+ additionalProperties: false
+ description: Response body with an array of label sets.
+ SeriesPostInputBody:
+ type: object
+ properties:
+ start:
+ type: string
+ description: 'Form field: The start time of the query.'
+ example: "2023-07-21T20:00:00.000Z"
+ end:
+ type: string
+ description: 'Form field: The end time of the query.'
+ example: "2023-07-21T21:00:00.000Z"
+ match[]:
+ type: array
+ items:
+ type: string
+ description: 'Form field: Series selector argument that selects the series to return.'
+ example:
+ - '{job="prometheus"}'
+ limit:
+ type: integer
+ format: int64
+ description: 'Form field: The maximum number of series to return.'
+ example: 100
+ required:
+ - match[]
+ additionalProperties: false
+ description: POST request body for series query.
+ SeriesDeleteOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ data:
+ description: Response data (structure varies by endpoint).
+ example:
+ result: ok
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ - data
+ additionalProperties: false
+ description: Generic response body.
+ Metadata:
+ type: object
+ properties:
+ type:
+ type: string
+ description: Metric type (counter, gauge, histogram, summary, or untyped).
+ unit:
+ type: string
+ description: Unit of the metric.
+ help:
+ type: string
+ description: Help text describing the metric.
+ required:
+ - type
+ - unit
+ - help
+ additionalProperties: false
+ description: Metric metadata.
+ MetadataOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ data:
+ type: object
+ additionalProperties:
+ type: array
+ items:
+ $ref: '#/components/schemas/Metadata'
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ - data
+ additionalProperties: false
+ description: Response body for metadata endpoint.
+ MetricMetadata:
+ type: object
+ properties:
+ target:
+ $ref: '#/components/schemas/Labels'
+ metric:
+ type: string
+ description: Metric name.
+ type:
+ type: string
+ description: Metric type (counter, gauge, histogram, summary, or untyped).
+ help:
+ type: string
+ description: Help text describing the metric.
+ unit:
+ type: string
+ description: Unit of the metric.
+ required:
+ - target
+ - type
+ - help
+ - unit
+ additionalProperties: false
+ description: Target metric metadata.
+ Target:
+ type: object
+ properties:
+ discoveredLabels:
+ $ref: '#/components/schemas/Labels'
+ labels:
+ $ref: '#/components/schemas/Labels'
+ scrapePool:
+ type: string
+ description: Name of the scrape pool.
+ scrapeUrl:
+ type: string
+ description: URL of the target.
+ globalUrl:
+ type: string
+ description: Global URL of the target.
+ lastError:
+ type: string
+ description: Last error message from scraping.
+ lastScrape:
+ type: string
+ format: date-time
+ description: Timestamp of the last scrape.
+ lastScrapeDuration:
+ type: number
+ format: double
+ description: Duration of the last scrape in seconds.
+ health:
+ type: string
+ description: Health status of the target (up, down, or unknown).
+ scrapeInterval:
+ type: string
+ description: Scrape interval for this target.
+ scrapeTimeout:
+ type: string
+ description: Scrape timeout for this target.
+ required:
+ - discoveredLabels
+ - labels
+ - scrapePool
+ - scrapeUrl
+ - globalUrl
+ - lastError
+ - lastScrape
+ - lastScrapeDuration
+ - health
+ - scrapeInterval
+ - scrapeTimeout
+ additionalProperties: false
+ description: Scrape target information.
+ DroppedTarget:
+ type: object
+ properties:
+ discoveredLabels:
+ $ref: '#/components/schemas/Labels'
+ scrapePool:
+ type: string
+ description: Name of the scrape pool.
+ required:
+ - discoveredLabels
+ - scrapePool
+ additionalProperties: false
+ description: Dropped target information.
+ TargetDiscovery:
+ type: object
+ properties:
+ activeTargets:
+ type: array
+ items:
+ $ref: '#/components/schemas/Target'
+ droppedTargets:
+ type: array
+ items:
+ $ref: '#/components/schemas/DroppedTarget'
+ droppedTargetCounts:
+ type: object
+ additionalProperties:
+ type: integer
+ format: int64
+ required:
+ - activeTargets
+ - droppedTargets
+ - droppedTargetCounts
+ additionalProperties: false
+ description: Target discovery information including active and dropped targets.
+ TargetsOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ data:
+ $ref: '#/components/schemas/TargetDiscovery'
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ - data
+ additionalProperties: false
+ description: Response body for targets endpoint.
+ TargetMetadataOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ data:
+ type: array
+ items:
+ $ref: '#/components/schemas/MetricMetadata'
+ example:
+ - help: The current health status of the target
+ metric: up
+ target:
+ instance: localhost:9090
+ job: prometheus
+ type: gauge
+ unit: ""
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ - data
+ additionalProperties: false
+ description: Response body with an array of metric metadata.
+ ScrapePoolsDiscovery:
+ type: object
+ properties:
+ scrapePools:
+ type: array
+ items:
+ type: string
+ required:
+ - scrapePools
+ additionalProperties: false
+ description: List of all configured scrape pools.
+ ScrapePoolsOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ data:
+ $ref: '#/components/schemas/ScrapePoolsDiscovery'
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ - data
+ additionalProperties: false
+ description: Response body for scrape pools endpoint.
+ Config:
+ type: object
+ properties:
+ source_labels:
+ type: array
+ items:
+ type: string
+ description: Source labels for relabeling.
+ separator:
+ type: string
+ description: Separator for source label values.
+ regex:
+ type: string
+ description: Regular expression for matching.
+ modulus:
+ type: integer
+ format: int64
+ description: Modulus for hash-based relabeling.
+ target_label:
+ type: string
+ description: Target label name.
+ replacement:
+ type: string
+ description: Replacement value.
+ action:
+ type: string
+ description: Relabel action.
+ additionalProperties: false
+ description: Relabel configuration.
+ RelabelStep:
+ type: object
+ properties:
+ rule:
+ $ref: '#/components/schemas/Config'
+ output:
+ $ref: '#/components/schemas/Labels'
+ keep:
+ type: boolean
+ required:
+ - rule
+ - output
+ - keep
+ additionalProperties: false
+ description: Relabel step showing the rule, output, and whether the target was kept.
+ RelabelStepsResponse:
+ type: object
+ properties:
+ steps:
+ type: array
+ items:
+ $ref: '#/components/schemas/RelabelStep'
+ required:
+ - steps
+ additionalProperties: false
+ description: Relabeling steps response.
+ TargetRelabelStepsOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ data:
+ $ref: '#/components/schemas/RelabelStepsResponse'
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ - data
+ additionalProperties: false
+ description: Response body for target relabel steps endpoint.
+ RuleGroup:
+ type: object
+ properties:
+ name:
+ type: string
+ description: Name of the rule group.
+ file:
+ type: string
+ description: File containing the rule group.
+ rules:
+ type: array
+ items:
+ type: object
+ description: Rule definition.
+ description: Rules in this group.
+ interval:
+ type: number
+ format: double
+ description: Evaluation interval in seconds.
+ limit:
+ type: integer
+ format: int64
+ description: Maximum number of alerts for this group.
+ evaluationTime:
+ type: number
+ format: double
+ description: Time taken to evaluate the group in seconds.
+ lastEvaluation:
+ type: string
+ format: date-time
+ description: Timestamp of the last evaluation.
+ required:
+ - name
+ - file
+ - rules
+ - interval
+ - limit
+ - evaluationTime
+ - lastEvaluation
+ additionalProperties: false
+ description: Rule group information.
+ RuleDiscovery:
+ type: object
+ properties:
+ groups:
+ type: array
+ items:
+ $ref: '#/components/schemas/RuleGroup'
+ groupNextToken:
+ type: string
+ description: Pagination token for the next page of groups.
+ required:
+ - groups
+ additionalProperties: false
+ description: Rule discovery information containing all rule groups.
+ RulesOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ data:
+ $ref: '#/components/schemas/RuleDiscovery'
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ - data
+ additionalProperties: false
+ description: Response body for rules endpoint.
+ Alert:
+ type: object
+ properties:
+ labels:
+ $ref: '#/components/schemas/Labels'
+ annotations:
+ $ref: '#/components/schemas/Labels'
+ state:
+ type: string
+ description: State of the alert (pending, firing, or inactive).
+ value:
+ type: string
+ description: Value of the alert expression.
+ activeAt:
+ type: string
+ format: date-time
+ description: Timestamp when the alert became active.
+ keepFiringSince:
+ type: string
+ format: date-time
+ description: Timestamp since the alert has been kept firing.
+ required:
+ - labels
+ - annotations
+ - state
+ - value
+ additionalProperties: false
+ description: Alert information.
+ AlertDiscovery:
+ type: object
+ properties:
+ alerts:
+ type: array
+ items:
+ $ref: '#/components/schemas/Alert'
+ required:
+ - alerts
+ additionalProperties: false
+ description: Alert discovery information containing all active alerts.
+ AlertsOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ data:
+ $ref: '#/components/schemas/AlertDiscovery'
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ - data
+ additionalProperties: false
+ description: Response body for alerts endpoint.
+ AlertmanagerTarget:
+ type: object
+ properties:
+ url:
+ type: string
+ description: URL of the Alertmanager instance.
+ required:
+ - url
+ additionalProperties: false
+ description: Alertmanager target information.
+ AlertmanagerDiscovery:
+ type: object
+ properties:
+ activeAlertmanagers:
+ type: array
+ items:
+ $ref: '#/components/schemas/AlertmanagerTarget'
+ droppedAlertmanagers:
+ type: array
+ items:
+ $ref: '#/components/schemas/AlertmanagerTarget'
+ required:
+ - activeAlertmanagers
+ - droppedAlertmanagers
+ additionalProperties: false
+ description: Alertmanager discovery information including active and dropped instances.
+ AlertmanagersOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ data:
+ $ref: '#/components/schemas/AlertmanagerDiscovery'
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ - data
+ additionalProperties: false
+ description: Response body for alertmanagers endpoint.
+ StatusConfigData:
+ type: object
+ properties:
+ yaml:
+ type: string
+ description: Prometheus configuration in YAML format.
+ required:
+ - yaml
+ additionalProperties: false
+ description: Prometheus configuration.
+ StatusConfigOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ data:
+ $ref: '#/components/schemas/StatusConfigData'
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ - data
+ additionalProperties: false
+ description: Response body for status config endpoint.
+ RuntimeInfo:
+ type: object
+ properties:
+ startTime:
+ type: string
+ format: date-time
+ CWD:
+ type: string
+ hostname:
+ type: string
+ serverTime:
+ type: string
+ format: date-time
+ reloadConfigSuccess:
+ type: boolean
+ lastConfigTime:
+ type: string
+ format: date-time
+ corruptionCount:
+ type: integer
+ format: int64
+ goroutineCount:
+ type: integer
+ format: int64
+ GOMAXPROCS:
+ type: integer
+ format: int64
+ GOMEMLIMIT:
+ type: integer
+ format: int64
+ GOGC:
+ type: string
+ GODEBUG:
+ type: string
+ storageRetention:
+ type: string
+ required:
+ - startTime
+ - CWD
+ - hostname
+ - serverTime
+ - reloadConfigSuccess
+ - lastConfigTime
+ - corruptionCount
+ - goroutineCount
+ - GOMAXPROCS
+ - GOMEMLIMIT
+ - GOGC
+ - GODEBUG
+ - storageRetention
+ additionalProperties: false
+ description: Prometheus runtime information.
+ StatusRuntimeInfoOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ data:
+ $ref: '#/components/schemas/RuntimeInfo'
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ - data
+ additionalProperties: false
+ description: Response body for status runtime info endpoint.
+ PrometheusVersion:
+ type: object
+ properties:
+ version:
+ type: string
+ revision:
+ type: string
+ branch:
+ type: string
+ buildUser:
+ type: string
+ buildDate:
+ type: string
+ goVersion:
+ type: string
+ required:
+ - version
+ - revision
+ - branch
+ - buildUser
+ - buildDate
+ - goVersion
+ additionalProperties: false
+ description: Prometheus version information.
+ StatusBuildInfoOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ data:
+ $ref: '#/components/schemas/PrometheusVersion'
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ - data
+ additionalProperties: false
+ description: Response body for status build info endpoint.
+ StatusFlagsOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ data:
+ type: object
+ additionalProperties:
+ type: string
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ - data
+ additionalProperties: false
+ description: Response body for status flags endpoint.
+ HeadStats:
+ type: object
+ properties:
+ numSeries:
+ type: integer
+ format: int64
+ numLabelPairs:
+ type: integer
+ format: int64
+ chunkCount:
+ type: integer
+ format: int64
+ minTime:
+ type: integer
+ format: int64
+ maxTime:
+ type: integer
+ format: int64
+ required:
+ - numSeries
+ - numLabelPairs
+ - chunkCount
+ - minTime
+ - maxTime
+ additionalProperties: false
+ description: TSDB head statistics.
+ TSDBStat:
+ type: object
+ properties:
+ name:
+ type: string
+ value:
+ type: integer
+ format: int64
+ required:
+ - name
+ - value
+ additionalProperties: false
+ description: TSDB statistic.
+ TSDBStatus:
+ type: object
+ properties:
+ headStats:
+ $ref: '#/components/schemas/HeadStats'
+ seriesCountByMetricName:
+ type: array
+ items:
+ $ref: '#/components/schemas/TSDBStat'
+ labelValueCountByLabelName:
+ type: array
+ items:
+ $ref: '#/components/schemas/TSDBStat'
+ memoryInBytesByLabelName:
+ type: array
+ items:
+ $ref: '#/components/schemas/TSDBStat'
+ seriesCountByLabelValuePair:
+ type: array
+ items:
+ $ref: '#/components/schemas/TSDBStat'
+ required:
+ - headStats
+ - seriesCountByMetricName
+ - labelValueCountByLabelName
+ - memoryInBytesByLabelName
+ - seriesCountByLabelValuePair
+ additionalProperties: false
+ description: TSDB status information.
+ StatusTSDBOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ data:
+ $ref: '#/components/schemas/TSDBStatus'
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ - data
+ additionalProperties: false
+ description: Response body for status TSDB endpoint.
+ BlockDesc:
+ type: object
+ properties:
+ ulid:
+ type: string
+ minTime:
+ type: integer
+ format: int64
+ maxTime:
+ type: integer
+ format: int64
+ required:
+ - ulid
+ - minTime
+ - maxTime
+ additionalProperties: false
+ description: Block descriptor.
+ BlockStats:
+ type: object
+ properties:
+ numSamples:
+ type: integer
+ format: int64
+ numSeries:
+ type: integer
+ format: int64
+ numChunks:
+ type: integer
+ format: int64
+ numTombstones:
+ type: integer
+ format: int64
+ numFloatSamples:
+ type: integer
+ format: int64
+ numHistogramSamples:
+ type: integer
+ format: int64
+ additionalProperties: false
+ description: Block statistics.
+ BlockMetaCompaction:
+ type: object
+ properties:
+ level:
+ type: integer
+ format: int64
+ sources:
+ type: array
+ items:
+ type: string
+ parents:
+ type: array
+ items:
+ $ref: '#/components/schemas/BlockDesc'
+ failed:
+ type: boolean
+ deletable:
+ type: boolean
+ hints:
+ type: array
+ items:
+ type: string
+ required:
+ - level
+ additionalProperties: false
+ description: Block compaction metadata.
+ BlockMeta:
+ type: object
+ properties:
+ ulid:
+ type: string
+ minTime:
+ type: integer
+ format: int64
+ maxTime:
+ type: integer
+ format: int64
+ stats:
+ $ref: '#/components/schemas/BlockStats'
+ compaction:
+ $ref: '#/components/schemas/BlockMetaCompaction'
+ version:
+ type: integer
+ format: int64
+ required:
+ - ulid
+ - minTime
+ - maxTime
+ - compaction
+ - version
+ additionalProperties: false
+ description: Block metadata.
+ StatusTSDBBlocksData:
+ type: object
+ properties:
+ blocks:
+ type: array
+ items:
+ $ref: '#/components/schemas/BlockMeta'
+ required:
+ - blocks
+ additionalProperties: false
+ description: TSDB blocks information.
+ StatusTSDBBlocksOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ data:
+ $ref: '#/components/schemas/StatusTSDBBlocksData'
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ - data
+ additionalProperties: false
+ description: Response body for status TSDB blocks endpoint.
+ StatusWALReplayData:
+ type: object
+ properties:
+ min:
+ type: integer
+ format: int64
+ max:
+ type: integer
+ format: int64
+ current:
+ type: integer
+ format: int64
+ required:
+ - min
+ - max
+ - current
+ additionalProperties: false
+ description: WAL replay status.
+ StatusWALReplayOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ data:
+ $ref: '#/components/schemas/StatusWALReplayData'
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ - data
+ additionalProperties: false
+ description: Response body for status WAL replay endpoint.
+ DeleteSeriesOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ additionalProperties: false
+ description: Response body containing only status.
+ CleanTombstonesOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ additionalProperties: false
+ description: Response body containing only status.
+ DataStruct:
+ type: object
+ properties:
+ name:
+ type: string
+ required:
+ - name
+ additionalProperties: false
+ description: Generic data structure with a name field.
+ SnapshotOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ data:
+ $ref: '#/components/schemas/DataStruct'
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ - data
+ additionalProperties: false
+ description: Response body for snapshot endpoint.
+ Notification:
+ type: object
+ properties:
+ text:
+ type: string
+ date:
+ type: string
+ format: date-time
+ active:
+ type: boolean
+ required:
+ - text
+ - date
+ - active
+ additionalProperties: false
+ description: Server notification.
+ NotificationsOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ data:
+ type: array
+ items:
+ $ref: '#/components/schemas/Notification'
+ example:
+ - active: true
+ date: "2023-07-21T20:00:00.000Z"
+ text: Server is running
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ - data
+ additionalProperties: false
+ description: Response body with an array of notifications.
+ FeaturesOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ data:
+ description: Response data (structure varies by endpoint).
+ example:
+ result: ok
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ - data
+ additionalProperties: false
+ description: Generic response body.
+tags:
+ - name: query
+ description: Query and evaluate PromQL expressions.
+ - name: metadata
+ description: Retrieve metric metadata such as type and unit.
+ - name: labels
+ description: Query label names and values.
+ - name: series
+ description: Query and manage time series.
+ - name: targets
+ description: Retrieve target and scrape pool information.
+ - name: rules
+ description: Query recording and alerting rules.
+ - name: alerts
+ description: Query active alerts and alertmanager discovery.
+ - name: status
+ description: Retrieve server status and configuration.
+ - name: admin
+ description: Administrative operations for TSDB management.
+ - name: features
+ description: Query enabled features.
+ - name: remote
+ description: Remote read and write endpoints.
+ - name: otlp
+ description: OpenTelemetry Protocol metrics ingestion.
+ - name: notifications
+ description: Server notifications and events.
diff --git a/web/api/v1/testdata/openapi_3.2_golden.yaml b/web/api/v1/testdata/openapi_3.2_golden.yaml
new file mode 100644
index 0000000000..fa79fffecc
--- /dev/null
+++ b/web/api/v1/testdata/openapi_3.2_golden.yaml
@@ -0,0 +1,4504 @@
+openapi: 3.2.0
+info:
+ title: Prometheus API
+ description: Prometheus is an Open-Source monitoring system with a dimensional data model, flexible query language, efficient time series database and modern alerting approach.
+ contact:
+ name: Prometheus Community
+ url: https://prometheus.io/community/
+ version: 0.0.1-undefined
+servers:
+ - url: /api/v1
+paths:
+ /query:
+ get:
+ tags:
+ - query
+ summary: Evaluate an instant query
+ operationId: query
+ parameters:
+ - name: limit
+ in: query
+ description: The maximum number of metrics to return.
+ required: false
+ explode: false
+ schema:
+ type: integer
+ format: int64
+ examples:
+ example:
+ value: 100
+ - name: time
+ in: query
+ description: The evaluation timestamp (optional, defaults to current time).
+ required: false
+ explode: false
+ schema:
+ oneOf:
+ - type: string
+ format: date-time
+ description: RFC3339 timestamp.
+ - type: number
+ format: unixtime
+ description: Unix timestamp in seconds.
+ description: Timestamp in RFC3339 format or Unix timestamp in seconds.
+ examples:
+ RFC3339:
+ value: "2026-01-02T13:37:00Z"
+ epoch:
+ value: 1767361020
+ - name: query
+ in: query
+ description: The PromQL query to execute.
+ required: true
+ explode: false
+ schema:
+ type: string
+ examples:
+ example:
+ value: up
+ - name: timeout
+ in: query
+ description: Evaluation timeout. Optional. Defaults to and is capped by the value of the -query.timeout flag.
+ required: false
+ explode: false
+ schema:
+ type: string
+ examples:
+ example:
+ value: 30s
+ - name: lookback_delta
+ in: query
+ description: Override the lookback period for this query. Optional.
+ required: false
+ explode: false
+ schema:
+ type: string
+ examples:
+ example:
+ value: 5m
+ - name: stats
+ in: query
+ description: When provided, include query statistics in the response. The special value 'all' enables more comprehensive statistics.
+ required: false
+ explode: false
+ schema:
+ type: string
+ examples:
+ example:
+ value: all
+ responses:
+ "200":
+ description: Query executed successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/QueryOutputBody'
+ examples:
+ vectorResult:
+ summary: 'Instant vector query: up'
+ value: {"status": "success", "data": {"resultType": "vector", "result": [{"metric": {"__name__": "up", "instance": "demo.prometheus.io:9090", "job": "prometheus"}, "value": [1767436620, "1"]}, {"metric": {"__name__": "up", "env": "demo", "instance": "demo.prometheus.io:9093", "job": "alertmanager"}, "value": [1767436620, "1"]}]}}
+ scalarResult:
+ summary: 'Scalar query: scalar(42)'
+ value:
+ data:
+ result:
+ - 1767436620
+ - "42"
+ resultType: scalar
+ status: success
+ matrixResult:
+ summary: 'Range vector query: up[5m]'
+ value: {"status": "success", "data": {"resultType": "matrix", "result": [{"metric": {"__name__": "up", "instance": "demo.prometheus.io:9090", "job": "prometheus"}, "values": [[1767436320, "1"], [1767436620, "1"]]}]}}
+ default:
+ description: Error executing query.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ post:
+ tags:
+ - query
+ summary: Evaluate an instant query
+ operationId: query-post
+ requestBody:
+ description: Submit an instant query. This endpoint accepts the same parameters as the GET version.
+ content:
+ application/x-www-form-urlencoded:
+ schema:
+ $ref: '#/components/schemas/QueryPostInputBody'
+ examples:
+ simpleQuery:
+ summary: Simple instant query
+ value:
+ query: up
+ queryWithTime:
+ summary: Query with specific timestamp
+ value:
+ query: up{job="prometheus"}
+ time: "2026-01-02T13:37:00.000Z"
+ queryWithLimit:
+ summary: Query with limit and statistics
+ value:
+ limit: 100
+ query: rate(prometheus_http_requests_total{handler="/api/v1/query"}[5m])
+ stats: all
+ required: true
+ responses:
+ "200":
+ description: Instant query executed successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/QueryOutputBody'
+ examples:
+ vectorResult:
+ summary: 'Instant vector query: up'
+ value: {"status": "success", "data": {"resultType": "vector", "result": [{"metric": {"__name__": "up", "instance": "demo.prometheus.io:9090", "job": "prometheus"}, "value": [1767436620, "1"]}, {"metric": {"__name__": "up", "env": "demo", "instance": "demo.prometheus.io:9093", "job": "alertmanager"}, "value": [1767436620, "1"]}]}}
+ scalarResult:
+ summary: 'Scalar query: scalar(42)'
+ value:
+ data:
+ result:
+ - 1767436620
+ - "42"
+ resultType: scalar
+ status: success
+ matrixResult:
+ summary: 'Range vector query: up[5m]'
+ value: {"status": "success", "data": {"resultType": "matrix", "result": [{"metric": {"__name__": "up", "instance": "demo.prometheus.io:9090", "job": "prometheus"}, "values": [[1767436320, "1"], [1767436620, "1"]]}]}}
+ default:
+ description: Error executing instant query.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ /query_range:
+ get:
+ tags:
+ - query
+ summary: Evaluate a range query
+ operationId: query-range
+ parameters:
+ - name: limit
+ in: query
+ description: The maximum number of metrics to return.
+ required: false
+ explode: false
+ schema:
+ type: integer
+ format: int64
+ examples:
+ example:
+ value: 100
+ - name: start
+ in: query
+ description: The start time of the query.
+ required: true
+ explode: false
+ schema:
+ oneOf:
+ - type: string
+ format: date-time
+ description: RFC3339 timestamp.
+ - type: number
+ format: unixtime
+ description: Unix timestamp in seconds.
+ description: Timestamp in RFC3339 format or Unix timestamp in seconds.
+ examples:
+ RFC3339:
+ value: "2026-01-02T12:37:00Z"
+ epoch:
+ value: 1767357420
+ - name: end
+ in: query
+ description: The end time of the query.
+ required: true
+ explode: false
+ schema:
+ oneOf:
+ - type: string
+ format: date-time
+ description: RFC3339 timestamp.
+ - type: number
+ format: unixtime
+ description: Unix timestamp in seconds.
+ description: Timestamp in RFC3339 format or Unix timestamp in seconds.
+ examples:
+ RFC3339:
+ value: "2026-01-02T13:37:00Z"
+ epoch:
+ value: 1767361020
+ - name: step
+ in: query
+ description: The step size of the query.
+ required: true
+ explode: false
+ schema:
+ type: string
+ examples:
+ example:
+ value: 15s
+ - name: query
+ in: query
+ description: The query to execute.
+ required: true
+ explode: false
+ schema:
+ type: string
+ examples:
+ example:
+ value: rate(prometheus_http_requests_total{handler="/api/v1/query"}[5m])
+ - name: timeout
+ in: query
+ description: Evaluation timeout. Optional. Defaults to and is capped by the value of the -query.timeout flag.
+ required: false
+ explode: false
+ schema:
+ type: string
+ examples:
+ example:
+ value: 30s
+ - name: lookback_delta
+ in: query
+ description: Override the lookback period for this query. Optional.
+ required: false
+ explode: false
+ schema:
+ type: string
+ examples:
+ example:
+ value: 5m
+ - name: stats
+ in: query
+ description: When provided, include query statistics in the response. The special value 'all' enables more comprehensive statistics.
+ required: false
+ explode: false
+ schema:
+ type: string
+ examples:
+ example:
+ value: all
+ responses:
+ "200":
+ description: Range query executed successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/QueryRangeOutputBody'
+ examples:
+ matrixResult:
+ summary: 'Range query: rate(prometheus_http_requests_total[5m])'
+ value: {"status": "success", "data": {"resultType": "matrix", "result": [{"metric": {"__name__": "up", "instance": "demo.prometheus.io:9090", "job": "prometheus"}, "values": [[1767433020, "1"], [1767434820, "1"], [1767436620, "1"]]}]}}
+ default:
+ description: Error executing range query.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ post:
+ tags:
+ - query
+ summary: Evaluate a range query
+ operationId: query-range-post
+ requestBody:
+ description: Submit a range query. This endpoint accepts the same parameters as the GET version.
+ content:
+ application/x-www-form-urlencoded:
+ schema:
+ $ref: '#/components/schemas/QueryRangePostInputBody'
+ examples:
+ basicRange:
+ summary: Basic range query
+ value:
+ end: "2026-01-02T13:37:00.000Z"
+ query: up
+ start: "2026-01-02T12:37:00.000Z"
+ step: 15s
+ rateQuery:
+ summary: Rate calculation over time range
+ value:
+ end: "2026-01-02T13:37:00.000Z"
+ query: rate(prometheus_http_requests_total{handler="/api/v1/query"}[5m])
+ start: "2026-01-02T12:37:00.000Z"
+ step: 30s
+ timeout: 30s
+ required: true
+ responses:
+ "200":
+ description: Range query executed successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/QueryRangeOutputBody'
+ examples:
+ matrixResult:
+ summary: 'Range query: rate(prometheus_http_requests_total[5m])'
+ value: {"status": "success", "data": {"resultType": "matrix", "result": [{"metric": {"__name__": "up", "instance": "demo.prometheus.io:9090", "job": "prometheus"}, "values": [[1767433020, "1"], [1767434820, "1"], [1767436620, "1"]]}]}}
+ default:
+ description: Error executing range query.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ /query_exemplars:
+ get:
+ tags:
+ - query
+ summary: Query exemplars
+ operationId: query-exemplars
+ parameters:
+ - name: start
+ in: query
+ description: Start timestamp for exemplars query.
+ required: false
+ explode: false
+ schema:
+ oneOf:
+ - type: string
+ format: date-time
+ description: RFC3339 timestamp.
+ - type: number
+ format: unixtime
+ description: Unix timestamp in seconds.
+ description: Timestamp in RFC3339 format or Unix timestamp in seconds.
+ examples:
+ RFC3339:
+ value: "2026-01-02T12:37:00Z"
+ epoch:
+ value: 1767357420
+ - name: end
+ in: query
+ description: End timestamp for exemplars query.
+ required: false
+ explode: false
+ schema:
+ oneOf:
+ - type: string
+ format: date-time
+ description: RFC3339 timestamp.
+ - type: number
+ format: unixtime
+ description: Unix timestamp in seconds.
+ description: Timestamp in RFC3339 format or Unix timestamp in seconds.
+ examples:
+ RFC3339:
+ value: "2026-01-02T13:37:00Z"
+ epoch:
+ value: 1767361020
+ - name: query
+ in: query
+ description: PromQL query to extract exemplars for.
+ required: true
+ explode: false
+ schema:
+ type: string
+ examples:
+ example:
+ value: prometheus_http_requests_total
+ responses:
+ "200":
+ description: Exemplars retrieved successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/QueryExemplarsOutputBody'
+ examples:
+ exemplarsResult:
+ summary: Exemplars for a metric with trace IDs
+ value:
+ data:
+ - exemplars:
+ - labels:
+ traceID: abc123def456
+ timestamp: 1.689956451781e+09
+ value: "1.5"
+ seriesLabels:
+ __name__: http_requests_total
+ job: api-server
+ method: GET
+ status: success
+ default:
+ description: Error retrieving exemplars.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ post:
+ tags:
+ - query
+ summary: Query exemplars
+ operationId: query-exemplars-post
+ requestBody:
+ description: Submit an exemplars query. This endpoint accepts the same parameters as the GET version.
+ content:
+ application/x-www-form-urlencoded:
+ schema:
+ $ref: '#/components/schemas/QueryExemplarsPostInputBody'
+ examples:
+ basicExemplar:
+ summary: Query exemplars for a metric
+ value:
+ query: prometheus_http_requests_total
+ exemplarWithTimeRange:
+ summary: Exemplars within specific time range
+ value:
+ end: "2026-01-02T13:37:00.000Z"
+ query: prometheus_http_requests_total{job="prometheus"}
+ start: "2026-01-02T12:37:00.000Z"
+ required: true
+ responses:
+ "200":
+ description: Exemplars query completed successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/QueryExemplarsOutputBody'
+ examples:
+ exemplarsResult:
+ summary: Exemplars for a metric with trace IDs
+ value:
+ data:
+ - exemplars:
+ - labels:
+ traceID: abc123def456
+ timestamp: 1.689956451781e+09
+ value: "1.5"
+ seriesLabels:
+ __name__: http_requests_total
+ job: api-server
+ method: GET
+ status: success
+ default:
+ description: Error processing exemplars query.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ /format_query:
+ get:
+ tags:
+ - query
+ summary: Format a PromQL query
+ operationId: format-query
+ parameters:
+ - name: query
+ in: query
+ description: PromQL expression to format.
+ required: true
+ explode: false
+ schema:
+ type: string
+ examples:
+ example:
+ value: sum(rate(http_requests_total[5m])) by (job)
+ responses:
+ "200":
+ description: Query formatted successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/FormatQueryOutputBody'
+ examples:
+ formattedQuery:
+ summary: Formatted PromQL query
+ value:
+ data: sum by(job, status) (rate(http_requests_total[5m]))
+ status: success
+ default:
+ description: Error formatting query.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ post:
+ tags:
+ - query
+ summary: Format a PromQL query
+ operationId: format-query-post
+ requestBody:
+ description: Submit a PromQL query to format. This endpoint accepts the same parameters as the GET version.
+ content:
+ application/x-www-form-urlencoded:
+ schema:
+ $ref: '#/components/schemas/FormatQueryPostInputBody'
+ examples:
+ simpleFormat:
+ summary: Format a simple query
+ value:
+ query: up{job="prometheus"}
+ complexFormat:
+ summary: Format a complex query
+ value:
+ query: sum(rate(http_requests_total[5m])) by (job, status)
+ required: true
+ responses:
+ "200":
+ description: Query formatting completed successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/FormatQueryOutputBody'
+ examples:
+ formattedQuery:
+ summary: Formatted PromQL query
+ value:
+ data: sum by(job, status) (rate(http_requests_total[5m]))
+ status: success
+ default:
+ description: Error formatting query.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ /parse_query:
+ get:
+ tags:
+ - query
+ summary: Parse a PromQL query
+ operationId: parse-query
+ parameters:
+ - name: query
+ in: query
+ description: PromQL expression to parse.
+ required: true
+ explode: false
+ schema:
+ type: string
+ examples:
+ example:
+ value: up{job="prometheus"}
+ responses:
+ "200":
+ description: Query parsed successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ParseQueryOutputBody'
+ examples:
+ parsedQuery:
+ summary: Parsed PromQL expression tree
+ value:
+ data:
+ resultType: vector
+ status: success
+ default:
+ description: Error parsing query.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ post:
+ tags:
+ - query
+ summary: Parse a PromQL query
+ operationId: parse-query-post
+ requestBody:
+ description: Submit a PromQL query to parse. This endpoint accepts the same parameters as the GET version.
+ content:
+ application/x-www-form-urlencoded:
+ schema:
+ $ref: '#/components/schemas/ParseQueryPostInputBody'
+ examples:
+ simpleParse:
+ summary: Parse a simple query
+ value:
+ query: up
+ complexParse:
+ summary: Parse a complex query
+ value:
+ query: rate(http_requests_total{job="api"}[5m])
+ required: true
+ responses:
+ "200":
+ description: Query parsed successfully via POST.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ParseQueryOutputBody'
+ examples:
+ parsedQuery:
+ summary: Parsed PromQL expression tree
+ value:
+ data:
+ resultType: vector
+ status: success
+ default:
+ description: Error parsing query via POST.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ /labels:
+ get:
+ tags:
+ - labels
+ summary: Get label names
+ operationId: labels
+ parameters:
+ - name: start
+ in: query
+ description: Start timestamp for label names query.
+ required: false
+ explode: false
+ schema:
+ oneOf:
+ - type: string
+ format: date-time
+ description: RFC3339 timestamp.
+ - type: number
+ format: unixtime
+ description: Unix timestamp in seconds.
+ description: Timestamp in RFC3339 format or Unix timestamp in seconds.
+ examples:
+ RFC3339:
+ value: "2026-01-02T12:37:00Z"
+ epoch:
+ value: 1767357420
+ - name: end
+ in: query
+ description: End timestamp for label names query.
+ required: false
+ explode: false
+ schema:
+ oneOf:
+ - type: string
+ format: date-time
+ description: RFC3339 timestamp.
+ - type: number
+ format: unixtime
+ description: Unix timestamp in seconds.
+ description: Timestamp in RFC3339 format or Unix timestamp in seconds.
+ examples:
+ RFC3339:
+ value: "2026-01-02T13:37:00Z"
+ epoch:
+ value: 1767361020
+ - name: match[]
+ in: query
+ description: Series selector argument.
+ required: false
+ explode: false
+ schema:
+ type: array
+ items:
+ type: string
+ examples:
+ example:
+ value:
+ - '{job="prometheus"}'
+ - name: limit
+ in: query
+ description: Maximum number of label names to return.
+ required: false
+ explode: false
+ schema:
+ type: integer
+ format: int64
+ examples:
+ example:
+ value: 100
+ responses:
+ "200":
+ description: Label names retrieved successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/LabelsOutputBody'
+ examples:
+ labelNames:
+ summary: List of label names
+ value:
+ data:
+ - __name__
+ - active
+ - address
+ - alertmanager
+ - alertname
+ - alertstate
+ - backend
+ - branch
+ - code
+ - collector
+ - component
+ - device
+ - env
+ - endpoint
+ - fstype
+ - handler
+ - instance
+ - job
+ - le
+ - method
+ - mode
+ - name
+ status: success
+ default:
+ description: Error retrieving label names.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ post:
+ tags:
+ - labels
+ summary: Get label names
+ operationId: labels-post
+ requestBody:
+ description: Submit a label names query. This endpoint accepts the same parameters as the GET version.
+ content:
+ application/x-www-form-urlencoded:
+ schema:
+ $ref: '#/components/schemas/LabelsPostInputBody'
+ examples:
+ allLabels:
+ summary: Get all label names
+ value: {}
+ labelsWithTimeRange:
+ summary: Get label names within time range
+ value:
+ end: "2026-01-02T13:37:00.000Z"
+ start: "2026-01-02T12:37:00.000Z"
+ labelsWithMatch:
+ summary: Get label names matching series selector
+ value:
+ match[]:
+ - up
+ - process_start_time_seconds{job="prometheus"}
+ required: true
+ responses:
+ "200":
+ description: Label names retrieved successfully via POST.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/LabelsOutputBody'
+ examples:
+ labelNames:
+ summary: List of label names
+ value:
+ data:
+ - __name__
+ - active
+ - address
+ - alertmanager
+ - alertname
+ - alertstate
+ - backend
+ - branch
+ - code
+ - collector
+ - component
+ - device
+ - env
+ - endpoint
+ - fstype
+ - handler
+ - instance
+ - job
+ - le
+ - method
+ - mode
+ - name
+ status: success
+ default:
+ description: Error retrieving label names via POST.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ /label/{name}/values:
+ get:
+ tags:
+ - labels
+ summary: Get label values
+ operationId: label-values
+ parameters:
+ - name: name
+ in: path
+ description: Label name.
+ required: true
+ schema:
+ type: string
+ - name: start
+ in: query
+ description: Start timestamp for label values query.
+ required: false
+ explode: false
+ schema:
+ oneOf:
+ - type: string
+ format: date-time
+ description: RFC3339 timestamp.
+ - type: number
+ format: unixtime
+ description: Unix timestamp in seconds.
+ description: Timestamp in RFC3339 format or Unix timestamp in seconds.
+ examples:
+ RFC3339:
+ value: "2026-01-02T12:37:00Z"
+ epoch:
+ value: 1767357420
+ - name: end
+ in: query
+ description: End timestamp for label values query.
+ required: false
+ explode: false
+ schema:
+ oneOf:
+ - type: string
+ format: date-time
+ description: RFC3339 timestamp.
+ - type: number
+ format: unixtime
+ description: Unix timestamp in seconds.
+ description: Timestamp in RFC3339 format or Unix timestamp in seconds.
+ examples:
+ RFC3339:
+ value: "2026-01-02T13:37:00Z"
+ epoch:
+ value: 1767361020
+ - name: match[]
+ in: query
+ description: Series selector argument.
+ required: false
+ explode: false
+ schema:
+ type: array
+ items:
+ type: string
+ examples:
+ example:
+ value:
+ - '{job="prometheus"}'
+ - name: limit
+ in: query
+ description: Maximum number of label values to return.
+ required: false
+ explode: false
+ schema:
+ type: integer
+ format: int64
+ examples:
+ example:
+ value: 1000
+ responses:
+ "200":
+ description: Label values retrieved successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/LabelValuesOutputBody'
+ examples:
+ labelValues:
+ summary: List of values for a label
+ value:
+ data:
+ - alertmanager
+ - blackbox
+ - caddy
+ - cadvisor
+ - grafana
+ - node
+ - prometheus
+ - random
+ status: success
+ default:
+ description: Error retrieving label values.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ /series:
+ get:
+ tags:
+ - series
+ summary: Find series by label matchers
+ operationId: series
+ parameters:
+ - name: start
+ in: query
+ description: Start timestamp for series query.
+ required: false
+ explode: false
+ schema:
+ oneOf:
+ - type: string
+ format: date-time
+ description: RFC3339 timestamp.
+ - type: number
+ format: unixtime
+ description: Unix timestamp in seconds.
+ description: Timestamp in RFC3339 format or Unix timestamp in seconds.
+ examples:
+ RFC3339:
+ value: "2026-01-02T12:37:00Z"
+ epoch:
+ value: 1767357420
+ - name: end
+ in: query
+ description: End timestamp for series query.
+ required: false
+ explode: false
+ schema:
+ oneOf:
+ - type: string
+ format: date-time
+ description: RFC3339 timestamp.
+ - type: number
+ format: unixtime
+ description: Unix timestamp in seconds.
+ description: Timestamp in RFC3339 format or Unix timestamp in seconds.
+ examples:
+ RFC3339:
+ value: "2026-01-02T13:37:00Z"
+ epoch:
+ value: 1767361020
+ - name: match[]
+ in: query
+ description: Series selector argument.
+ required: true
+ explode: false
+ schema:
+ type: array
+ items:
+ type: string
+ examples:
+ example:
+ value:
+ - '{job="prometheus"}'
+ - name: limit
+ in: query
+ description: Maximum number of series to return.
+ required: false
+ explode: false
+ schema:
+ type: integer
+ format: int64
+ examples:
+ example:
+ value: 100
+ responses:
+ "200":
+ description: Series returned matching the provided label matchers.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/SeriesOutputBody'
+ examples:
+ seriesList:
+ summary: List of series matching the selector
+ value:
+ data:
+ - __name__: up
+ env: demo
+ instance: demo.prometheus.io:8080
+ job: cadvisor
+ - __name__: up
+ env: demo
+ instance: demo.prometheus.io:9093
+ job: alertmanager
+ - __name__: up
+ env: demo
+ instance: demo.prometheus.io:9100
+ job: node
+ - __name__: up
+ instance: demo.prometheus.io:3000
+ job: grafana
+ - __name__: up
+ instance: demo.prometheus.io:8996
+ job: random
+ status: success
+ default:
+ description: Error retrieving series.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ post:
+ tags:
+ - series
+ summary: Find series by label matchers
+ operationId: series-post
+ requestBody:
+ description: Submit a series query. This endpoint accepts the same parameters as the GET version.
+ content:
+ application/x-www-form-urlencoded:
+ schema:
+ $ref: '#/components/schemas/SeriesPostInputBody'
+ examples:
+ seriesMatch:
+ summary: Find series by label matchers
+ value:
+ match[]:
+ - up
+ seriesWithTimeRange:
+ summary: Find series with time range
+ value:
+ end: "2026-01-02T13:37:00.000Z"
+ match[]:
+ - up
+ - process_cpu_seconds_total{job="prometheus"}
+ start: "2026-01-02T12:37:00.000Z"
+ required: true
+ responses:
+ "200":
+ description: Series returned matching the provided label matchers via POST.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/SeriesOutputBody'
+ examples:
+ seriesList:
+ summary: List of series matching the selector
+ value:
+ data:
+ - __name__: up
+ env: demo
+ instance: demo.prometheus.io:8080
+ job: cadvisor
+ - __name__: up
+ env: demo
+ instance: demo.prometheus.io:9093
+ job: alertmanager
+ - __name__: up
+ env: demo
+ instance: demo.prometheus.io:9100
+ job: node
+ - __name__: up
+ instance: demo.prometheus.io:3000
+ job: grafana
+ - __name__: up
+ instance: demo.prometheus.io:8996
+ job: random
+ status: success
+ default:
+ description: Error retrieving series via POST.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ delete:
+ tags:
+ - series
+ summary: Delete series
+ description: 'Delete series matching selectors. Note: This is deprecated, use POST /admin/tsdb/delete_series instead.'
+ operationId: delete-series
+ responses:
+ "200":
+ description: Series marked for deletion.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/SeriesDeleteOutputBody'
+ examples:
+ seriesDeleted:
+ summary: Series marked for deletion
+ value:
+ status: success
+ default:
+ description: Error deleting series.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ /metadata:
+ get:
+ tags:
+ - metadata
+ summary: Get metadata
+ operationId: get-metadata
+ parameters:
+ - name: limit
+ in: query
+ description: The maximum number of metrics to return.
+ required: false
+ explode: false
+ schema:
+ type: integer
+ format: int64
+ examples:
+ example:
+ value: 100
+ - name: limit_per_metric
+ in: query
+ description: The maximum number of metadata entries per metric.
+ required: false
+ explode: false
+ schema:
+ type: integer
+ format: int64
+ examples:
+ example:
+ value: 10
+ - name: metric
+ in: query
+ description: A metric name to filter metadata for.
+ required: false
+ explode: false
+ schema:
+ type: string
+ examples:
+ example:
+ value: http_requests_total
+ responses:
+ "200":
+ description: Metric metadata retrieved successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/MetadataOutputBody'
+ examples:
+ metricMetadata:
+ summary: Metadata for metrics
+ value:
+ data:
+ go_gc_stack_starting_size_bytes:
+ - help: The stack size of new goroutines. Sourced from /gc/stack/starting-size:bytes.
+ type: gauge
+ unit: ""
+ prometheus_rule_group_iterations_missed_total:
+ - help: The total number of rule group evaluations missed due to slow rule group evaluation.
+ type: counter
+ unit: ""
+ prometheus_sd_updates_total:
+ - help: Total number of update events sent to the SD consumers.
+ type: counter
+ unit: ""
+ status: success
+ default:
+ description: Error retrieving metadata.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ /scrape_pools:
+ get:
+ tags:
+ - targets
+ summary: Get scrape pools
+ operationId: get-scrape-pools
+ responses:
+ "200":
+ description: Scrape pools retrieved successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ScrapePoolsOutputBody'
+ examples:
+ scrapePoolsList:
+ summary: List of scrape pool names
+ value:
+ data:
+ scrapePools:
+ - alertmanager
+ - blackbox
+ - caddy
+ - cadvisor
+ - grafana
+ - node
+ - prometheus
+ - random
+ status: success
+ default:
+ description: Error retrieving scrape pools.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ /targets:
+ get:
+ tags:
+ - targets
+ summary: Get targets
+ operationId: get-targets
+ parameters:
+ - name: scrapePool
+ in: query
+ description: Filter targets by scrape pool name.
+ required: false
+ explode: false
+ schema:
+ type: string
+ examples:
+ example:
+ value: prometheus
+ - name: state
+ in: query
+ description: 'Filter by state: active, dropped, or any.'
+ required: false
+ explode: false
+ schema:
+ type: string
+ examples:
+ example:
+ value: active
+ responses:
+ "200":
+ description: Target discovery information retrieved successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/TargetsOutputBody'
+ examples:
+ targetsList:
+ summary: Active and dropped targets
+ value:
+ data:
+ activeTargets:
+ - discoveredLabels:
+ __address__: demo.prometheus.io:9093
+ __meta_filepath: /etc/prometheus/file_sd/alertmanager.yml
+ __metrics_path__: /metrics
+ __scheme__: http
+ env: demo
+ job: alertmanager
+ globalUrl: http://demo.prometheus.io:9093/metrics
+ health: up
+ labels:
+ env: demo
+ instance: demo.prometheus.io:9093
+ job: alertmanager
+ lastError: ""
+ lastScrape: "2026-01-02T13:36:40.200Z"
+ lastScrapeDuration: 0.006576866
+ scrapeInterval: 15s
+ scrapePool: alertmanager
+ scrapeTimeout: 10s
+ scrapeUrl: http://demo.prometheus.io:9093/metrics
+ droppedTargetCounts:
+ alertmanager: 0
+ blackbox: 0
+ caddy: 0
+ cadvisor: 0
+ grafana: 0
+ node: 0
+ prometheus: 0
+ random: 0
+ droppedTargets: []
+ status: success
+ default:
+ description: Error retrieving targets.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ /targets/metadata:
+ get:
+ tags:
+ - targets
+ summary: Get targets metadata
+ operationId: get-targets-metadata
+ parameters:
+ - name: match_target
+ in: query
+ description: Label selector to filter targets.
+ required: false
+ explode: false
+ schema:
+ type: string
+ examples:
+ example:
+ value: '{job="prometheus"}'
+ - name: metric
+ in: query
+ description: Metric name to retrieve metadata for.
+ required: false
+ explode: false
+ schema:
+ type: string
+ examples:
+ example:
+ value: http_requests_total
+ - name: limit
+ in: query
+ description: Maximum number of targets to match.
+ required: false
+ explode: false
+ schema:
+ type: integer
+ format: int64
+ examples:
+ example:
+ value: 10
+ responses:
+ "200":
+ description: Target metadata retrieved successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/TargetMetadataOutputBody'
+ examples:
+ targetMetadata:
+ summary: Metadata for targets
+ value:
+ data:
+ - help: The current health status of the target
+ metric: up
+ target:
+ instance: localhost:9090
+ job: prometheus
+ type: gauge
+ unit: ""
+ status: success
+ default:
+ description: Error retrieving target metadata.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ /targets/relabel_steps:
+ get:
+ tags:
+ - targets
+ summary: Get targets relabel steps
+ operationId: get-targets-relabel-steps
+ parameters:
+ - name: scrapePool
+ in: query
+ description: Name of the scrape pool.
+ required: true
+ explode: false
+ schema:
+ type: string
+ examples:
+ example:
+ value: prometheus
+ - name: labels
+ in: query
+ description: JSON-encoded labels to apply relabel rules to.
+ required: true
+ explode: false
+ schema:
+ type: string
+ examples:
+ example:
+ value: '{"__address__":"localhost:9090","job":"prometheus"}'
+ responses:
+ "200":
+ description: Relabel steps retrieved successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/TargetRelabelStepsOutputBody'
+ examples:
+ relabelSteps:
+ summary: Relabel steps for a target
+ value:
+ data:
+ steps:
+ - keep: true
+ output:
+ __address__: localhost:9090
+ instance: localhost:9090
+ job: prometheus
+ rule:
+ action: replace
+ regex: (.*)
+ replacement: $1
+ source_labels:
+ - __address__
+ target_label: instance
+ status: success
+ default:
+ description: Error retrieving relabel steps.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ /rules:
+ get:
+ tags:
+ - rules
+ summary: Get alerting and recording rules
+ operationId: rules
+ parameters:
+ - name: type
+ in: query
+ description: 'Filter by rule type: alert or record.'
+ required: false
+ explode: false
+ schema:
+ type: string
+ examples:
+ example:
+ value: alert
+ - name: rule_name[]
+ in: query
+ description: Filter by rule name.
+ required: false
+ explode: false
+ schema:
+ type: array
+ items:
+ type: string
+ examples:
+ example:
+ value:
+ - HighErrorRate
+ - name: rule_group[]
+ in: query
+ description: Filter by rule group name.
+ required: false
+ explode: false
+ schema:
+ type: array
+ items:
+ type: string
+ examples:
+ example:
+ value:
+ - example_alerts
+ - name: file[]
+ in: query
+ description: Filter by file path.
+ required: false
+ explode: false
+ schema:
+ type: array
+ items:
+ type: string
+ examples:
+ example:
+ value:
+ - /etc/prometheus/rules.yml
+ - name: match[]
+ in: query
+ description: Label matchers to filter rules.
+ required: false
+ explode: false
+ schema:
+ type: array
+ items:
+ type: string
+ examples:
+ example:
+ value:
+ - '{severity="critical"}'
+ - name: exclude_alerts
+ in: query
+ description: Exclude active alerts from response.
+ required: false
+ explode: false
+ schema:
+ type: string
+ examples:
+ example:
+ value: "false"
+ - name: group_limit
+ in: query
+ description: Maximum number of rule groups to return.
+ required: false
+ explode: false
+ schema:
+ type: integer
+ format: int64
+ examples:
+ example:
+ value: 100
+ - name: group_next_token
+ in: query
+ description: Pagination token for next page.
+ required: false
+ explode: false
+ schema:
+ type: string
+ examples:
+ example:
+ value: abc123
+ responses:
+ "200":
+ description: Rules retrieved successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/RulesOutputBody'
+ examples:
+ ruleGroups:
+ summary: Alerting and recording rules
+ value:
+ data:
+ groups:
+ - evaluationTime: 0.000561635
+ file: /etc/prometheus/rules/ansible_managed.yml
+ interval: 15
+ lastEvaluation: "2026-01-02T13:36:56.874Z"
+ limit: 0
+ name: ansible managed alert rules
+ rules:
+ - annotations:
+ description: This is an alert meant to ensure that the entire alerting pipeline is functional. This alert is always firing, therefore it should always be firing in Alertmanager and always fire against a receiver. There are integrations with various notification mechanisms that send a notification when this alert is not firing. For example the "DeadMansSnitch" integration in PagerDuty.
+ summary: Ensure entire alerting pipeline is functional
+ duration: 600
+ evaluationTime: 0.000356688
+ health: ok
+ keepFiringFor: 0
+ labels:
+ severity: warning
+ lastEvaluation: "2026-01-02T13:36:56.874Z"
+ name: Watchdog
+ query: vector(1)
+ state: firing
+ type: alerting
+ status: success
+ default:
+ description: Error retrieving rules.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ /alerts:
+ get:
+ tags:
+ - alerts
+ summary: Get active alerts
+ operationId: alerts
+ responses:
+ "200":
+ description: Active alerts retrieved successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/AlertsOutputBody'
+ examples:
+ activeAlerts:
+ summary: Currently active alerts
+ value:
+ data:
+ alerts:
+ - activeAt: "2026-01-02T13:30:00.000Z"
+ annotations:
+ description: This is an alert meant to ensure that the entire alerting pipeline is functional. This alert is always firing, therefore it should always be firing in Alertmanager and always fire against a receiver. There are integrations with various notification mechanisms that send a notification when this alert is not firing. For example the "DeadMansSnitch" integration in PagerDuty.
+ summary: Ensure entire alerting pipeline is functional
+ labels:
+ alertname: Watchdog
+ severity: warning
+ state: firing
+ value: "1e+00"
+ status: success
+ default:
+ description: Error retrieving alerts.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ /alertmanagers:
+ get:
+ tags:
+ - alerts
+ summary: Get Alertmanager discovery
+ operationId: alertmanagers
+ responses:
+ "200":
+ description: Alertmanager targets retrieved successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/AlertmanagersOutputBody'
+ examples:
+ alertmanagerDiscovery:
+ summary: Alertmanager discovery results
+ value:
+ data:
+ activeAlertmanagers:
+ - url: http://demo.prometheus.io:9093/api/v2/alerts
+ droppedAlertmanagers: []
+ status: success
+ default:
+ description: Error retrieving Alertmanager targets.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ /status/config:
+ get:
+ tags:
+ - status
+ summary: Get status config
+ operationId: get-status-config
+ responses:
+ "200":
+ description: Configuration retrieved successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/StatusConfigOutputBody'
+ examples:
+ configYAML:
+ summary: Prometheus configuration
+ value:
+ data:
+ yaml: |
+ global:
+ scrape_interval: 15s
+ scrape_timeout: 10s
+ evaluation_interval: 15s
+ external_labels:
+ environment: demo-prometheus-io
+ alerting:
+ alertmanagers:
+ - scheme: http
+ static_configs:
+ - targets:
+ - demo.prometheus.io:9093
+ rule_files:
+ - /etc/prometheus/rules/*.yml
+ status: success
+ default:
+ description: Error retrieving configuration.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ /status/runtimeinfo:
+ get:
+ tags:
+ - status
+ summary: Get status runtimeinfo
+ operationId: get-status-runtimeinfo
+ responses:
+ "200":
+ description: Runtime information retrieved successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/StatusRuntimeInfoOutputBody'
+ examples:
+ runtimeInfo:
+ summary: Runtime information
+ value:
+ data:
+ CWD: /
+ GODEBUG: ""
+ GOGC: "75"
+ GOMAXPROCS: 2
+ GOMEMLIMIT: 3703818240
+ corruptionCount: 0
+ goroutineCount: 88
+ hostname: demo-prometheus-io
+ lastConfigTime: "2026-01-01T13:37:00.000Z"
+ reloadConfigSuccess: true
+ serverTime: "2026-01-02T13:37:00.000Z"
+ startTime: "2026-01-01T13:37:00.000Z"
+ storageRetention: 31d
+ status: success
+ default:
+ description: Error retrieving runtime information.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ /status/buildinfo:
+ get:
+ tags:
+ - status
+ summary: Get status buildinfo
+ operationId: get-status-buildinfo
+ responses:
+ "200":
+ description: Build information retrieved successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/StatusBuildInfoOutputBody'
+ examples:
+ buildInfo:
+ summary: Build information
+ value:
+ data:
+ branch: HEAD
+ buildDate: 20251030-07:26:10
+ buildUser: root@08c890a84441
+ goVersion: go1.25.3
+ revision: 0a41f0000705c69ab8e0f9a723fc73e39ed62b07
+ version: 3.7.3
+ status: success
+ default:
+ description: Error retrieving build information.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ /status/flags:
+ get:
+ tags:
+ - status
+ summary: Get status flags
+ operationId: get-status-flags
+ responses:
+ "200":
+ description: Command-line flags retrieved successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/StatusFlagsOutputBody'
+ examples:
+ flags:
+ summary: Command-line flags
+ value:
+ data:
+ agent: "false"
+ alertmanager.notification-queue-capacity: "10000"
+ config.file: /etc/prometheus/prometheus.yml
+ enable-feature: exemplar-storage,native-histograms
+ query.max-concurrency: "20"
+ query.timeout: 2m
+ storage.tsdb.path: /prometheus
+ storage.tsdb.retention.time: 15d
+ web.console.libraries: /usr/share/prometheus/console_libraries
+ web.console.templates: /usr/share/prometheus/consoles
+ web.enable-admin-api: "true"
+ web.enable-lifecycle: "true"
+ web.listen-address: 0.0.0.0:9090
+ web.page-title: Prometheus Time Series Collection and Processing Server
+ status: success
+ default:
+ description: Error retrieving flags.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ /status/tsdb:
+ get:
+ tags:
+ - status
+ summary: Get TSDB status
+ operationId: status-tsdb
+ parameters:
+ - name: limit
+ in: query
+ description: The maximum number of items to return per category.
+ required: false
+ explode: false
+ schema:
+ type: integer
+ format: int64
+ examples:
+ example:
+ value: 10
+ responses:
+ "200":
+ description: TSDB status retrieved successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/StatusTSDBOutputBody'
+ examples:
+ tsdbStats:
+ summary: TSDB statistics
+ value:
+ data:
+ headStats:
+ chunkCount: 37525
+ maxTime: 1767436620000
+ minTime: 1767362400712
+ numLabelPairs: 2512
+ numSeries: 9925
+ labelValueCountByLabelName:
+ - name: __name__
+ value: 5
+ - name: job
+ value: 3
+ memoryInBytesByLabelName:
+ - name: __name__
+ value: 1024
+ - name: job
+ value: 512
+ seriesCountByLabelValuePair:
+ - name: job=prometheus
+ value: 100
+ - name: instance=localhost:9090
+ value: 100
+ seriesCountByMetricName:
+ - name: up
+ value: 100
+ - name: http_requests_total
+ value: 500
+ status: success
+ default:
+ description: Error retrieving TSDB status.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ /status/tsdb/blocks:
+ get:
+ tags:
+ - status
+ summary: Get TSDB blocks information
+ operationId: status-tsdb-blocks
+ responses:
+ "200":
+ description: TSDB blocks information retrieved successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/StatusTSDBBlocksOutputBody'
+ examples:
+ tsdbBlocks:
+ summary: TSDB block information
+ value:
+ data:
+ blocks:
+ - compaction:
+ level: 4
+ sources:
+ - 01KBCJ7TR8A4QAJ3AA1J651P5S
+ - 01KBCS3J0E34567YPB8Y5W0E24
+ - 01KBCZZ9KRTYGG3E7HVQFGC3S3
+ maxTime: 1764763200000
+ minTime: 1764568801099
+ stats:
+ numChunks: 1073962
+ numSamples: 129505582
+ numSeries: 10661
+ ulid: 01KC4D6GXQA4CRHYKV78NEBVAE
+ version: 1
+ status: success
+ default:
+ description: Error retrieving TSDB blocks.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ /status/walreplay:
+ get:
+ tags:
+ - status
+ summary: Get status walreplay
+ operationId: get-status-walreplay
+ responses:
+ "200":
+ description: WAL replay status retrieved successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/StatusWALReplayOutputBody'
+ examples:
+ walReplay:
+ summary: WAL replay status
+ value:
+ data:
+ current: 3214
+ max: 3214
+ min: 3209
+ status: success
+ default:
+ description: Error retrieving WAL replay status.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ /admin/tsdb/delete_series:
+ put:
+ tags:
+ - admin
+ summary: Delete series matching selectors via PUT
+ description: Deletes data for a selection of series in a time range using PUT method.
+ operationId: deleteSeriesPut
+ parameters:
+ - name: match[]
+ in: query
+ description: Series selectors to identify series to delete.
+ required: true
+ explode: false
+ schema:
+ type: array
+ items:
+ type: string
+ examples:
+ example:
+ value:
+ - '{__name__=~"test.*"}'
+ - name: start
+ in: query
+ description: Start timestamp for deletion.
+ required: false
+ explode: false
+ schema:
+ oneOf:
+ - type: string
+ format: date-time
+ description: RFC3339 timestamp.
+ - type: number
+ format: unixtime
+ description: Unix timestamp in seconds.
+ description: Timestamp in RFC3339 format or Unix timestamp in seconds.
+ examples:
+ RFC3339:
+ value: "2026-01-02T12:37:00Z"
+ epoch:
+ value: 1767357420
+ - name: end
+ in: query
+ description: End timestamp for deletion.
+ required: false
+ explode: false
+ schema:
+ oneOf:
+ - type: string
+ format: date-time
+ description: RFC3339 timestamp.
+ - type: number
+ format: unixtime
+ description: Unix timestamp in seconds.
+ description: Timestamp in RFC3339 format or Unix timestamp in seconds.
+ examples:
+ RFC3339:
+ value: "2026-01-02T13:37:00Z"
+ epoch:
+ value: 1767361020
+ responses:
+ "200":
+ description: Series deleted successfully via PUT.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/DeleteSeriesOutputBody'
+ examples:
+ deletionSuccess:
+ summary: Successful series deletion
+ value:
+ status: success
+ default:
+ description: Error deleting series via PUT.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ post:
+ tags:
+ - admin
+ summary: Delete series matching selectors
+ description: Deletes data for a selection of series in a time range.
+ operationId: deleteSeriesPost
+ parameters:
+ - name: match[]
+ in: query
+ description: Series selectors to identify series to delete.
+ required: true
+ explode: false
+ schema:
+ type: array
+ items:
+ type: string
+ examples:
+ example:
+ value:
+ - '{__name__=~"test.*"}'
+ - name: start
+ in: query
+ description: Start timestamp for deletion.
+ required: false
+ explode: false
+ schema:
+ oneOf:
+ - type: string
+ format: date-time
+ description: RFC3339 timestamp.
+ - type: number
+ format: unixtime
+ description: Unix timestamp in seconds.
+ description: Timestamp in RFC3339 format or Unix timestamp in seconds.
+ examples:
+ RFC3339:
+ value: "2026-01-02T12:37:00Z"
+ epoch:
+ value: 1767357420
+ - name: end
+ in: query
+ description: End timestamp for deletion.
+ required: false
+ explode: false
+ schema:
+ oneOf:
+ - type: string
+ format: date-time
+ description: RFC3339 timestamp.
+ - type: number
+ format: unixtime
+ description: Unix timestamp in seconds.
+ description: Timestamp in RFC3339 format or Unix timestamp in seconds.
+ examples:
+ RFC3339:
+ value: "2026-01-02T13:37:00Z"
+ epoch:
+ value: 1767361020
+ responses:
+ "200":
+ description: Series deleted successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/DeleteSeriesOutputBody'
+ examples:
+ deletionSuccess:
+ summary: Successful series deletion
+ value:
+ status: success
+ default:
+ description: Error deleting series.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ /admin/tsdb/clean_tombstones:
+ put:
+ tags:
+ - admin
+ summary: Clean tombstones in the TSDB via PUT
+ description: Removes deleted data from disk and cleans up existing tombstones using PUT method.
+ operationId: cleanTombstonesPut
+ responses:
+ "200":
+ description: Tombstones cleaned successfully via PUT.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CleanTombstonesOutputBody'
+ examples:
+ tombstonesCleaned:
+ summary: Tombstones cleaned successfully
+ value:
+ status: success
+ default:
+ description: Error cleaning tombstones via PUT.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ post:
+ tags:
+ - admin
+ summary: Clean tombstones in the TSDB
+ description: Removes deleted data from disk and cleans up existing tombstones.
+ operationId: cleanTombstonesPost
+ responses:
+ "200":
+ description: Tombstones cleaned successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CleanTombstonesOutputBody'
+ examples:
+ tombstonesCleaned:
+ summary: Tombstones cleaned successfully
+ value:
+ status: success
+ default:
+ description: Error cleaning tombstones.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ /admin/tsdb/snapshot:
+ put:
+ tags:
+ - admin
+ summary: Create a snapshot of the TSDB via PUT
+ description: Creates a snapshot of all current data using PUT method.
+ operationId: snapshotPut
+ parameters:
+ - name: skip_head
+ in: query
+ description: If true, do not snapshot data in the head block.
+ required: false
+ explode: false
+ schema:
+ type: string
+ examples:
+ example:
+ value: "false"
+ responses:
+ "200":
+ description: Snapshot created successfully via PUT.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/SnapshotOutputBody'
+ examples:
+ snapshotCreated:
+ summary: Snapshot created successfully
+ value:
+ data:
+ name: 20260102T133700Z-a1b2c3d4e5f67890
+ status: success
+ default:
+ description: Error creating snapshot via PUT.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ post:
+ tags:
+ - admin
+ summary: Create a snapshot of the TSDB
+ description: Creates a snapshot of all current data.
+ operationId: snapshotPost
+ parameters:
+ - name: skip_head
+ in: query
+ description: If true, do not snapshot data in the head block.
+ required: false
+ explode: false
+ schema:
+ type: string
+ examples:
+ example:
+ value: "false"
+ responses:
+ "200":
+ description: Snapshot created successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/SnapshotOutputBody'
+ examples:
+ snapshotCreated:
+ summary: Snapshot created successfully
+ value:
+ data:
+ name: 20260102T133700Z-a1b2c3d4e5f67890
+ status: success
+ default:
+ description: Error creating snapshot.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ /read:
+ post:
+ tags:
+ - remote
+ summary: Remote read endpoint
+ description: Prometheus remote read endpoint for federated queries. Accepts and returns Protocol Buffer encoded data.
+ operationId: remoteRead
+ responses:
+ "204":
+ description: No Content
+ default:
+ description: Error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ /write:
+ post:
+ tags:
+ - remote
+ summary: Remote write endpoint
+ description: Prometheus remote write endpoint for sending metrics. Accepts Protocol Buffer encoded write requests.
+ operationId: remoteWrite
+ responses:
+ "204":
+ description: No Content
+ default:
+ description: Error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ /otlp/v1/metrics:
+ post:
+ tags:
+ - otlp
+ summary: OTLP metrics write endpoint
+ description: OpenTelemetry Protocol metrics ingestion endpoint. Accepts OTLP/HTTP metrics in Protocol Buffer format.
+ operationId: otlpWrite
+ responses:
+ "204":
+ description: No Content
+ default:
+ description: Error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ /notifications:
+ get:
+ tags:
+ - notifications
+ summary: Get notifications
+ operationId: get-notifications
+ responses:
+ "200":
+ description: Notifications retrieved successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/NotificationsOutputBody'
+ examples:
+ notifications:
+ summary: Server notifications
+ value:
+ data:
+ - active: true
+ date: "2026-01-02T16:14:50.046Z"
+ text: Configuration reload has failed.
+ status: success
+ default:
+ description: Error retrieving notifications.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+ /notifications/live:
+ get:
+ tags:
+ - notifications
+ summary: Stream live notifications via Server-Sent Events
+ description: Subscribe to real-time server notifications using SSE. Each event contains a JSON-encoded Notification object in the data field.
+ operationId: notifications-live
+ responses:
+ "200":
+ description: Server-sent events stream established.
+ content:
+ text/event-stream:
+ itemSchema:
+ type: object
+ properties:
+ data:
+ type: string
+ contentSchema:
+ $ref: '#/components/schemas/Notification'
+ description: SSE data field containing JSON-encoded notification.
+ contentMediaType: application/json
+ title: Server Sent Event Message
+ required:
+ - data
+ additionalProperties: false
+ description: A single SSE message. The data field contains a JSON-encoded Notification object.
+ examples:
+ activeNotification:
+ summary: Active notification SSE message
+ description: An SSE message containing an active server notification.
+ value:
+ data: '{"text":"Configuration reload has failed.","date":"2026-01-02T16:14:50.046Z","active":true}'
+ default:
+ description: Error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ /features:
+ get:
+ tags:
+ - features
+ summary: Get features
+ operationId: get-features
+ responses:
+ "200":
+ description: Feature flags retrieved successfully.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/FeaturesOutputBody'
+ examples:
+ enabledFeatures:
+ summary: Enabled feature flags
+ value:
+ data:
+ - exemplar-storage
+ - remote-write-receiver
+ status: success
+ default:
+ description: Error retrieving features.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ examples:
+ tsdbNotReady:
+ summary: TSDB not ready
+ value:
+ error: TSDB not ready
+ errorType: internal
+ status: error
+components:
+ schemas:
+ Error:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ errorType:
+ type: string
+ description: Type of error that occurred.
+ example: bad_data
+ error:
+ type: string
+ description: Human-readable error message.
+ example: invalid parameter
+ required:
+ - status
+ - errorType
+ - error
+ additionalProperties: false
+ description: Error response.
+ Labels:
+ type: object
+ additionalProperties: true
+ description: Label set represented as a key-value map.
+ QueryOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ data:
+ $ref: '#/components/schemas/QueryData'
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ - data
+ additionalProperties: false
+ description: Response body for instant query.
+ QueryRangeOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ data:
+ $ref: '#/components/schemas/QueryData'
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ - data
+ additionalProperties: false
+ description: Response body for range query.
+ QueryPostInputBody:
+ type: object
+ properties:
+ query:
+ type: string
+ description: 'Form field: The PromQL query to execute.'
+ example: up
+ time:
+ type: string
+ description: 'Form field: The evaluation timestamp (optional, defaults to current time).'
+ example: "2023-07-21T20:10:51.781Z"
+ limit:
+ type: integer
+ format: int64
+ description: 'Form field: The maximum number of metrics to return.'
+ example: 100
+ timeout:
+ type: string
+ description: 'Form field: Evaluation timeout (optional, defaults to and is capped by the value of the -query.timeout flag).'
+ example: 30s
+ lookback_delta:
+ type: string
+ description: 'Form field: Override the lookback period for this query (optional).'
+ example: 5m
+ stats:
+ type: string
+ description: 'Form field: When provided, include query statistics in the response (the special value ''all'' enables more comprehensive statistics).'
+ example: all
+ required:
+ - query
+ additionalProperties: false
+ description: POST request body for instant query.
+ QueryRangePostInputBody:
+ type: object
+ properties:
+ query:
+ type: string
+ description: 'Form field: The query to execute.'
+ example: rate(http_requests_total[5m])
+ start:
+ type: string
+ description: 'Form field: The start time of the query.'
+ example: "2023-07-21T20:10:30.781Z"
+ end:
+ type: string
+ description: 'Form field: The end time of the query.'
+ example: "2023-07-21T20:20:30.781Z"
+ step:
+ type: string
+ description: 'Form field: The step size of the query.'
+ example: 15s
+ limit:
+ type: integer
+ format: int64
+ description: 'Form field: The maximum number of metrics to return.'
+ example: 100
+ timeout:
+ type: string
+ description: 'Form field: Evaluation timeout (optional, defaults to and is capped by the value of the -query.timeout flag).'
+ example: 30s
+ lookback_delta:
+ type: string
+ description: 'Form field: Override the lookback period for this query (optional).'
+ example: 5m
+ stats:
+ type: string
+ description: 'Form field: When provided, include query statistics in the response (the special value ''all'' enables more comprehensive statistics).'
+ example: all
+ required:
+ - query
+ - start
+ - end
+ - step
+ additionalProperties: false
+ description: POST request body for range query.
+ QueryExemplarsOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ data:
+ description: Response data (structure varies by endpoint).
+ example:
+ result: ok
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ - data
+ additionalProperties: false
+ description: Generic response body.
+ QueryExemplarsPostInputBody:
+ type: object
+ properties:
+ query:
+ type: string
+ description: 'Form field: The query to execute.'
+ example: http_requests_total
+ start:
+ type: string
+ description: 'Form field: The start time of the query.'
+ example: "2023-07-21T20:00:00.000Z"
+ end:
+ type: string
+ description: 'Form field: The end time of the query.'
+ example: "2023-07-21T21:00:00.000Z"
+ required:
+ - query
+ additionalProperties: false
+ description: POST request body for exemplars query.
+ FormatQueryOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ data:
+ type: string
+ description: Formatted query string.
+ example: sum by(status) (rate(http_requests_total[5m]))
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ - data
+ additionalProperties: false
+ description: Response body for format query endpoint.
+ FormatQueryPostInputBody:
+ type: object
+ properties:
+ query:
+ type: string
+ description: 'Form field: The query to format.'
+ example: sum(rate(http_requests_total[5m])) by (status)
+ required:
+ - query
+ additionalProperties: false
+ description: POST request body for format query.
+ ParseQueryOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ data:
+ description: Response data (structure varies by endpoint).
+ example:
+ result: ok
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ - data
+ additionalProperties: false
+ description: Generic response body.
+ ParseQueryPostInputBody:
+ type: object
+ properties:
+ query:
+ type: string
+ description: 'Form field: The query to parse.'
+ example: sum(rate(http_requests_total[5m]))
+ required:
+ - query
+ additionalProperties: false
+ description: POST request body for parse query.
+ QueryData:
+ anyOf:
+ - type: object
+ properties:
+ resultType:
+ type: string
+ enum:
+ - vector
+ result:
+ type: array
+ items:
+ anyOf:
+ - $ref: '#/components/schemas/FloatSample'
+ - $ref: '#/components/schemas/HistogramSample'
+ description: Array of samples (either float or histogram).
+ stats:
+ $ref: '#/components/schemas/QueryStats'
+ required:
+ - resultType
+ - result
+ additionalProperties: false
+ - type: object
+ properties:
+ resultType:
+ type: string
+ enum:
+ - matrix
+ result:
+ type: array
+ items:
+ anyOf:
+ - $ref: '#/components/schemas/FloatSeries'
+ - $ref: '#/components/schemas/HistogramSeries'
+ description: Array of time series (either float or histogram).
+ stats:
+ $ref: '#/components/schemas/QueryStats'
+ required:
+ - resultType
+ - result
+ additionalProperties: false
+ - type: object
+ properties:
+ resultType:
+ type: string
+ enum:
+ - scalar
+ result:
+ type: array
+ items:
+ oneOf:
+ - type: number
+ - type: string
+ maxItems: 2
+ minItems: 2
+ description: Scalar value as [timestamp, stringValue].
+ stats:
+ $ref: '#/components/schemas/QueryStats'
+ required:
+ - resultType
+ - result
+ additionalProperties: false
+ - type: object
+ properties:
+ resultType:
+ type: string
+ enum:
+ - string
+ result:
+ type: array
+ items:
+ type: string
+ maxItems: 2
+ minItems: 2
+ description: String value as [timestamp, stringValue].
+ stats:
+ $ref: '#/components/schemas/QueryStats'
+ required:
+ - resultType
+ - result
+ additionalProperties: false
+ description: Query result data. The structure of 'result' depends on 'resultType'.
+ example:
+ result:
+ - metric:
+ __name__: up
+ job: prometheus
+ value:
+ - 1627845600
+ - "1"
+ resultType: vector
+ QueryStats:
+ type: object
+ properties:
+ timings:
+ type: object
+ properties:
+ evalTotalTime:
+ type: number
+ description: Total evaluation time in seconds.
+ resultSortTime:
+ type: number
+ description: Time spent sorting results in seconds.
+ queryPreparationTime:
+ type: number
+ description: Query preparation time in seconds.
+ innerEvalTime:
+ type: number
+ description: Inner evaluation time in seconds.
+ execQueueTime:
+ type: number
+ description: Execution queue wait time in seconds.
+ execTotalTime:
+ type: number
+ description: Total execution time in seconds.
+ samples:
+ type: object
+ properties:
+ totalQueryableSamples:
+ type: integer
+ description: Total number of samples that were queryable.
+ peakSamples:
+ type: integer
+ description: Peak number of samples in memory.
+ totalQueryableSamplesPerStep:
+ type: array
+ items:
+ type: array
+ items:
+ type: number
+ maxItems: 2
+ minItems: 2
+ description: Timestamp and sample count as [timestamp, count].
+ description: Total queryable samples per step (only included with stats=all).
+ description: Query execution statistics (included when the stats query parameter is provided).
+ FloatSample:
+ type: object
+ properties:
+ metric:
+ $ref: '#/components/schemas/Labels'
+ value:
+ type: array
+ items:
+ oneOf:
+ - type: number
+ - type: string
+ maxItems: 2
+ minItems: 2
+ description: Timestamp and float value as [unixTimestamp, stringValue].
+ example:
+ - 1767436620
+ - "1"
+ required:
+ - metric
+ - value
+ additionalProperties: false
+ description: A sample with a float value.
+ HistogramSample:
+ type: object
+ properties:
+ metric:
+ $ref: '#/components/schemas/Labels'
+ histogram:
+ type: array
+ items:
+ oneOf:
+ - type: number
+ - $ref: '#/components/schemas/HistogramValue'
+ maxItems: 2
+ minItems: 2
+ description: Timestamp and histogram value as [unixTimestamp, histogramObject].
+ example:
+ - 1767436620
+ - buckets: []
+ count: "60"
+ sum: "120"
+ required:
+ - metric
+ - histogram
+ additionalProperties: false
+ description: A sample with a native histogram value.
+ FloatSeries:
+ type: object
+ properties:
+ metric:
+ $ref: '#/components/schemas/Labels'
+ values:
+ type: array
+ items:
+ type: array
+ items:
+ oneOf:
+ - type: number
+ - type: string
+ maxItems: 2
+ minItems: 2
+ description: Array of [timestamp, stringValue] pairs for float values.
+ required:
+ - metric
+ - values
+ additionalProperties: false
+ description: A time series with float values.
+ HistogramSeries:
+ type: object
+ properties:
+ metric:
+ $ref: '#/components/schemas/Labels'
+ histograms:
+ type: array
+ items:
+ type: array
+ items:
+ oneOf:
+ - type: number
+ - $ref: '#/components/schemas/HistogramValue'
+ maxItems: 2
+ minItems: 2
+ description: Array of [timestamp, histogramObject] pairs for histogram values.
+ required:
+ - metric
+ - histograms
+ additionalProperties: false
+ description: A time series with native histogram values.
+ HistogramValue:
+ type: object
+ properties:
+ count:
+ type: string
+ description: Total count of observations.
+ sum:
+ type: string
+ description: Sum of all observed values.
+ buckets:
+ type: array
+ items:
+ type: array
+ items:
+ oneOf:
+ - type: number
+ - type: string
+ description: Histogram buckets as [boundary_rule, lower, upper, count].
+ required:
+ - count
+ - sum
+ additionalProperties: false
+ description: Native histogram value representation.
+ LabelsOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ data:
+ type: array
+ items:
+ type: string
+ example:
+ - __name__
+ - job
+ - instance
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ - data
+ additionalProperties: false
+ description: Response body with an array of strings.
+ LabelsPostInputBody:
+ type: object
+ properties:
+ start:
+ type: string
+ description: 'Form field: The start time of the query.'
+ example: "2023-07-21T20:00:00.000Z"
+ end:
+ type: string
+ description: 'Form field: The end time of the query.'
+ example: "2023-07-21T21:00:00.000Z"
+ match[]:
+ type: array
+ items:
+ type: string
+ description: 'Form field: Series selector argument that selects the series from which to read the label names.'
+ example:
+ - '{job="prometheus"}'
+ limit:
+ type: integer
+ format: int64
+ description: 'Form field: The maximum number of label names to return.'
+ example: 100
+ additionalProperties: false
+ description: POST request body for labels query.
+ LabelValuesOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ data:
+ type: array
+ items:
+ type: string
+ example:
+ - __name__
+ - job
+ - instance
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ - data
+ additionalProperties: false
+ description: Response body with an array of strings.
+ SeriesOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ data:
+ type: array
+ items:
+ $ref: '#/components/schemas/Labels'
+ example:
+ - __name__: up
+ instance: localhost:9090
+ job: prometheus
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ - data
+ additionalProperties: false
+ description: Response body with an array of label sets.
+ SeriesPostInputBody:
+ type: object
+ properties:
+ start:
+ type: string
+ description: 'Form field: The start time of the query.'
+ example: "2023-07-21T20:00:00.000Z"
+ end:
+ type: string
+ description: 'Form field: The end time of the query.'
+ example: "2023-07-21T21:00:00.000Z"
+ match[]:
+ type: array
+ items:
+ type: string
+ description: 'Form field: Series selector argument that selects the series to return.'
+ example:
+ - '{job="prometheus"}'
+ limit:
+ type: integer
+ format: int64
+ description: 'Form field: The maximum number of series to return.'
+ example: 100
+ required:
+ - match[]
+ additionalProperties: false
+ description: POST request body for series query.
+ SeriesDeleteOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ data:
+ description: Response data (structure varies by endpoint).
+ example:
+ result: ok
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ - data
+ additionalProperties: false
+ description: Generic response body.
+ Metadata:
+ type: object
+ properties:
+ type:
+ type: string
+ description: Metric type (counter, gauge, histogram, summary, or untyped).
+ unit:
+ type: string
+ description: Unit of the metric.
+ help:
+ type: string
+ description: Help text describing the metric.
+ required:
+ - type
+ - unit
+ - help
+ additionalProperties: false
+ description: Metric metadata.
+ MetadataOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ data:
+ type: object
+ additionalProperties:
+ type: array
+ items:
+ $ref: '#/components/schemas/Metadata'
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ - data
+ additionalProperties: false
+ description: Response body for metadata endpoint.
+ MetricMetadata:
+ type: object
+ properties:
+ target:
+ $ref: '#/components/schemas/Labels'
+ metric:
+ type: string
+ description: Metric name.
+ type:
+ type: string
+ description: Metric type (counter, gauge, histogram, summary, or untyped).
+ help:
+ type: string
+ description: Help text describing the metric.
+ unit:
+ type: string
+ description: Unit of the metric.
+ required:
+ - target
+ - type
+ - help
+ - unit
+ additionalProperties: false
+ description: Target metric metadata.
+ Target:
+ type: object
+ properties:
+ discoveredLabels:
+ $ref: '#/components/schemas/Labels'
+ labels:
+ $ref: '#/components/schemas/Labels'
+ scrapePool:
+ type: string
+ description: Name of the scrape pool.
+ scrapeUrl:
+ type: string
+ description: URL of the target.
+ globalUrl:
+ type: string
+ description: Global URL of the target.
+ lastError:
+ type: string
+ description: Last error message from scraping.
+ lastScrape:
+ type: string
+ format: date-time
+ description: Timestamp of the last scrape.
+ lastScrapeDuration:
+ type: number
+ format: double
+ description: Duration of the last scrape in seconds.
+ health:
+ type: string
+ description: Health status of the target (up, down, or unknown).
+ scrapeInterval:
+ type: string
+ description: Scrape interval for this target.
+ scrapeTimeout:
+ type: string
+ description: Scrape timeout for this target.
+ required:
+ - discoveredLabels
+ - labels
+ - scrapePool
+ - scrapeUrl
+ - globalUrl
+ - lastError
+ - lastScrape
+ - lastScrapeDuration
+ - health
+ - scrapeInterval
+ - scrapeTimeout
+ additionalProperties: false
+ description: Scrape target information.
+ DroppedTarget:
+ type: object
+ properties:
+ discoveredLabels:
+ $ref: '#/components/schemas/Labels'
+ scrapePool:
+ type: string
+ description: Name of the scrape pool.
+ required:
+ - discoveredLabels
+ - scrapePool
+ additionalProperties: false
+ description: Dropped target information.
+ TargetDiscovery:
+ type: object
+ properties:
+ activeTargets:
+ type: array
+ items:
+ $ref: '#/components/schemas/Target'
+ droppedTargets:
+ type: array
+ items:
+ $ref: '#/components/schemas/DroppedTarget'
+ droppedTargetCounts:
+ type: object
+ additionalProperties:
+ type: integer
+ format: int64
+ required:
+ - activeTargets
+ - droppedTargets
+ - droppedTargetCounts
+ additionalProperties: false
+ description: Target discovery information including active and dropped targets.
+ TargetsOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ data:
+ $ref: '#/components/schemas/TargetDiscovery'
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ - data
+ additionalProperties: false
+ description: Response body for targets endpoint.
+ TargetMetadataOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ data:
+ type: array
+ items:
+ $ref: '#/components/schemas/MetricMetadata'
+ example:
+ - help: The current health status of the target
+ metric: up
+ target:
+ instance: localhost:9090
+ job: prometheus
+ type: gauge
+ unit: ""
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ - data
+ additionalProperties: false
+ description: Response body with an array of metric metadata.
+ ScrapePoolsDiscovery:
+ type: object
+ properties:
+ scrapePools:
+ type: array
+ items:
+ type: string
+ required:
+ - scrapePools
+ additionalProperties: false
+ description: List of all configured scrape pools.
+ ScrapePoolsOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ data:
+ $ref: '#/components/schemas/ScrapePoolsDiscovery'
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ - data
+ additionalProperties: false
+ description: Response body for scrape pools endpoint.
+ Config:
+ type: object
+ properties:
+ source_labels:
+ type: array
+ items:
+ type: string
+ description: Source labels for relabeling.
+ separator:
+ type: string
+ description: Separator for source label values.
+ regex:
+ type: string
+ description: Regular expression for matching.
+ modulus:
+ type: integer
+ format: int64
+ description: Modulus for hash-based relabeling.
+ target_label:
+ type: string
+ description: Target label name.
+ replacement:
+ type: string
+ description: Replacement value.
+ action:
+ type: string
+ description: Relabel action.
+ additionalProperties: false
+ description: Relabel configuration.
+ RelabelStep:
+ type: object
+ properties:
+ rule:
+ $ref: '#/components/schemas/Config'
+ output:
+ $ref: '#/components/schemas/Labels'
+ keep:
+ type: boolean
+ required:
+ - rule
+ - output
+ - keep
+ additionalProperties: false
+ description: Relabel step showing the rule, output, and whether the target was kept.
+ RelabelStepsResponse:
+ type: object
+ properties:
+ steps:
+ type: array
+ items:
+ $ref: '#/components/schemas/RelabelStep'
+ required:
+ - steps
+ additionalProperties: false
+ description: Relabeling steps response.
+ TargetRelabelStepsOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ data:
+ $ref: '#/components/schemas/RelabelStepsResponse'
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ - data
+ additionalProperties: false
+ description: Response body for target relabel steps endpoint.
+ RuleGroup:
+ type: object
+ properties:
+ name:
+ type: string
+ description: Name of the rule group.
+ file:
+ type: string
+ description: File containing the rule group.
+ rules:
+ type: array
+ items:
+ type: object
+ description: Rule definition.
+ description: Rules in this group.
+ interval:
+ type: number
+ format: double
+ description: Evaluation interval in seconds.
+ limit:
+ type: integer
+ format: int64
+ description: Maximum number of alerts for this group.
+ evaluationTime:
+ type: number
+ format: double
+ description: Time taken to evaluate the group in seconds.
+ lastEvaluation:
+ type: string
+ format: date-time
+ description: Timestamp of the last evaluation.
+ required:
+ - name
+ - file
+ - rules
+ - interval
+ - limit
+ - evaluationTime
+ - lastEvaluation
+ additionalProperties: false
+ description: Rule group information.
+ RuleDiscovery:
+ type: object
+ properties:
+ groups:
+ type: array
+ items:
+ $ref: '#/components/schemas/RuleGroup'
+ groupNextToken:
+ type: string
+ description: Pagination token for the next page of groups.
+ required:
+ - groups
+ additionalProperties: false
+ description: Rule discovery information containing all rule groups.
+ RulesOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ data:
+ $ref: '#/components/schemas/RuleDiscovery'
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ - data
+ additionalProperties: false
+ description: Response body for rules endpoint.
+ Alert:
+ type: object
+ properties:
+ labels:
+ $ref: '#/components/schemas/Labels'
+ annotations:
+ $ref: '#/components/schemas/Labels'
+ state:
+ type: string
+ description: State of the alert (pending, firing, or inactive).
+ value:
+ type: string
+ description: Value of the alert expression.
+ activeAt:
+ type: string
+ format: date-time
+ description: Timestamp when the alert became active.
+ keepFiringSince:
+ type: string
+ format: date-time
+ description: Timestamp since the alert has been kept firing.
+ required:
+ - labels
+ - annotations
+ - state
+ - value
+ additionalProperties: false
+ description: Alert information.
+ AlertDiscovery:
+ type: object
+ properties:
+ alerts:
+ type: array
+ items:
+ $ref: '#/components/schemas/Alert'
+ required:
+ - alerts
+ additionalProperties: false
+ description: Alert discovery information containing all active alerts.
+ AlertsOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ data:
+ $ref: '#/components/schemas/AlertDiscovery'
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ - data
+ additionalProperties: false
+ description: Response body for alerts endpoint.
+ AlertmanagerTarget:
+ type: object
+ properties:
+ url:
+ type: string
+ description: URL of the Alertmanager instance.
+ required:
+ - url
+ additionalProperties: false
+ description: Alertmanager target information.
+ AlertmanagerDiscovery:
+ type: object
+ properties:
+ activeAlertmanagers:
+ type: array
+ items:
+ $ref: '#/components/schemas/AlertmanagerTarget'
+ droppedAlertmanagers:
+ type: array
+ items:
+ $ref: '#/components/schemas/AlertmanagerTarget'
+ required:
+ - activeAlertmanagers
+ - droppedAlertmanagers
+ additionalProperties: false
+ description: Alertmanager discovery information including active and dropped instances.
+ AlertmanagersOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ data:
+ $ref: '#/components/schemas/AlertmanagerDiscovery'
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ - data
+ additionalProperties: false
+ description: Response body for alertmanagers endpoint.
+ StatusConfigData:
+ type: object
+ properties:
+ yaml:
+ type: string
+ description: Prometheus configuration in YAML format.
+ required:
+ - yaml
+ additionalProperties: false
+ description: Prometheus configuration.
+ StatusConfigOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ data:
+ $ref: '#/components/schemas/StatusConfigData'
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ - data
+ additionalProperties: false
+ description: Response body for status config endpoint.
+ RuntimeInfo:
+ type: object
+ properties:
+ startTime:
+ type: string
+ format: date-time
+ CWD:
+ type: string
+ hostname:
+ type: string
+ serverTime:
+ type: string
+ format: date-time
+ reloadConfigSuccess:
+ type: boolean
+ lastConfigTime:
+ type: string
+ format: date-time
+ corruptionCount:
+ type: integer
+ format: int64
+ goroutineCount:
+ type: integer
+ format: int64
+ GOMAXPROCS:
+ type: integer
+ format: int64
+ GOMEMLIMIT:
+ type: integer
+ format: int64
+ GOGC:
+ type: string
+ GODEBUG:
+ type: string
+ storageRetention:
+ type: string
+ required:
+ - startTime
+ - CWD
+ - hostname
+ - serverTime
+ - reloadConfigSuccess
+ - lastConfigTime
+ - corruptionCount
+ - goroutineCount
+ - GOMAXPROCS
+ - GOMEMLIMIT
+ - GOGC
+ - GODEBUG
+ - storageRetention
+ additionalProperties: false
+ description: Prometheus runtime information.
+ StatusRuntimeInfoOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ data:
+ $ref: '#/components/schemas/RuntimeInfo'
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ - data
+ additionalProperties: false
+ description: Response body for status runtime info endpoint.
+ PrometheusVersion:
+ type: object
+ properties:
+ version:
+ type: string
+ revision:
+ type: string
+ branch:
+ type: string
+ buildUser:
+ type: string
+ buildDate:
+ type: string
+ goVersion:
+ type: string
+ required:
+ - version
+ - revision
+ - branch
+ - buildUser
+ - buildDate
+ - goVersion
+ additionalProperties: false
+ description: Prometheus version information.
+ StatusBuildInfoOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ data:
+ $ref: '#/components/schemas/PrometheusVersion'
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ - data
+ additionalProperties: false
+ description: Response body for status build info endpoint.
+ StatusFlagsOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ data:
+ type: object
+ additionalProperties:
+ type: string
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ - data
+ additionalProperties: false
+ description: Response body for status flags endpoint.
+ HeadStats:
+ type: object
+ properties:
+ numSeries:
+ type: integer
+ format: int64
+ numLabelPairs:
+ type: integer
+ format: int64
+ chunkCount:
+ type: integer
+ format: int64
+ minTime:
+ type: integer
+ format: int64
+ maxTime:
+ type: integer
+ format: int64
+ required:
+ - numSeries
+ - numLabelPairs
+ - chunkCount
+ - minTime
+ - maxTime
+ additionalProperties: false
+ description: TSDB head statistics.
+ TSDBStat:
+ type: object
+ properties:
+ name:
+ type: string
+ value:
+ type: integer
+ format: int64
+ required:
+ - name
+ - value
+ additionalProperties: false
+ description: TSDB statistic.
+ TSDBStatus:
+ type: object
+ properties:
+ headStats:
+ $ref: '#/components/schemas/HeadStats'
+ seriesCountByMetricName:
+ type: array
+ items:
+ $ref: '#/components/schemas/TSDBStat'
+ labelValueCountByLabelName:
+ type: array
+ items:
+ $ref: '#/components/schemas/TSDBStat'
+ memoryInBytesByLabelName:
+ type: array
+ items:
+ $ref: '#/components/schemas/TSDBStat'
+ seriesCountByLabelValuePair:
+ type: array
+ items:
+ $ref: '#/components/schemas/TSDBStat'
+ required:
+ - headStats
+ - seriesCountByMetricName
+ - labelValueCountByLabelName
+ - memoryInBytesByLabelName
+ - seriesCountByLabelValuePair
+ additionalProperties: false
+ description: TSDB status information.
+ StatusTSDBOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ data:
+ $ref: '#/components/schemas/TSDBStatus'
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ - data
+ additionalProperties: false
+ description: Response body for status TSDB endpoint.
+ BlockDesc:
+ type: object
+ properties:
+ ulid:
+ type: string
+ minTime:
+ type: integer
+ format: int64
+ maxTime:
+ type: integer
+ format: int64
+ required:
+ - ulid
+ - minTime
+ - maxTime
+ additionalProperties: false
+ description: Block descriptor.
+ BlockStats:
+ type: object
+ properties:
+ numSamples:
+ type: integer
+ format: int64
+ numSeries:
+ type: integer
+ format: int64
+ numChunks:
+ type: integer
+ format: int64
+ numTombstones:
+ type: integer
+ format: int64
+ numFloatSamples:
+ type: integer
+ format: int64
+ numHistogramSamples:
+ type: integer
+ format: int64
+ additionalProperties: false
+ description: Block statistics.
+ BlockMetaCompaction:
+ type: object
+ properties:
+ level:
+ type: integer
+ format: int64
+ sources:
+ type: array
+ items:
+ type: string
+ parents:
+ type: array
+ items:
+ $ref: '#/components/schemas/BlockDesc'
+ failed:
+ type: boolean
+ deletable:
+ type: boolean
+ hints:
+ type: array
+ items:
+ type: string
+ required:
+ - level
+ additionalProperties: false
+ description: Block compaction metadata.
+ BlockMeta:
+ type: object
+ properties:
+ ulid:
+ type: string
+ minTime:
+ type: integer
+ format: int64
+ maxTime:
+ type: integer
+ format: int64
+ stats:
+ $ref: '#/components/schemas/BlockStats'
+ compaction:
+ $ref: '#/components/schemas/BlockMetaCompaction'
+ version:
+ type: integer
+ format: int64
+ required:
+ - ulid
+ - minTime
+ - maxTime
+ - compaction
+ - version
+ additionalProperties: false
+ description: Block metadata.
+ StatusTSDBBlocksData:
+ type: object
+ properties:
+ blocks:
+ type: array
+ items:
+ $ref: '#/components/schemas/BlockMeta'
+ required:
+ - blocks
+ additionalProperties: false
+ description: TSDB blocks information.
+ StatusTSDBBlocksOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ data:
+ $ref: '#/components/schemas/StatusTSDBBlocksData'
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ - data
+ additionalProperties: false
+ description: Response body for status TSDB blocks endpoint.
+ StatusWALReplayData:
+ type: object
+ properties:
+ min:
+ type: integer
+ format: int64
+ max:
+ type: integer
+ format: int64
+ current:
+ type: integer
+ format: int64
+ required:
+ - min
+ - max
+ - current
+ additionalProperties: false
+ description: WAL replay status.
+ StatusWALReplayOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ data:
+ $ref: '#/components/schemas/StatusWALReplayData'
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ - data
+ additionalProperties: false
+ description: Response body for status WAL replay endpoint.
+ DeleteSeriesOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ additionalProperties: false
+ description: Response body containing only status.
+ CleanTombstonesOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ additionalProperties: false
+ description: Response body containing only status.
+ DataStruct:
+ type: object
+ properties:
+ name:
+ type: string
+ required:
+ - name
+ additionalProperties: false
+ description: Generic data structure with a name field.
+ SnapshotOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ data:
+ $ref: '#/components/schemas/DataStruct'
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ - data
+ additionalProperties: false
+ description: Response body for snapshot endpoint.
+ Notification:
+ type: object
+ properties:
+ text:
+ type: string
+ date:
+ type: string
+ format: date-time
+ active:
+ type: boolean
+ required:
+ - text
+ - date
+ - active
+ additionalProperties: false
+ description: Server notification.
+ NotificationsOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ data:
+ type: array
+ items:
+ $ref: '#/components/schemas/Notification'
+ example:
+ - active: true
+ date: "2023-07-21T20:00:00.000Z"
+ text: Server is running
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ - data
+ additionalProperties: false
+ description: Response body with an array of notifications.
+ FeaturesOutputBody:
+ type: object
+ properties:
+ status:
+ type: string
+ enum:
+ - success
+ - error
+ description: Response status.
+ example: success
+ data:
+ description: Response data (structure varies by endpoint).
+ example:
+ result: ok
+ warnings:
+ type: array
+ items:
+ type: string
+ description: Only set if there were warnings while executing the request. There will still be data in the data field.
+ infos:
+ type: array
+ items:
+ type: string
+ description: Only set if there were info-level annotations while executing the request.
+ required:
+ - status
+ - data
+ additionalProperties: false
+ description: Generic response body.
+tags:
+ - name: query
+ summary: Query
+ description: Query and evaluate PromQL expressions.
+ - name: metadata
+ summary: Metadata
+ description: Retrieve metric metadata such as type and unit.
+ - name: labels
+ summary: Labels
+ description: Query label names and values.
+ - name: series
+ summary: Series
+ description: Query and manage time series.
+ - name: targets
+ summary: Targets
+ description: Retrieve target and scrape pool information.
+ - name: rules
+ summary: Rules
+ description: Query recording and alerting rules.
+ - name: alerts
+ summary: Alerts
+ description: Query active alerts and alertmanager discovery.
+ - name: status
+ summary: Status
+ description: Retrieve server status and configuration.
+ - name: admin
+ summary: Admin
+ description: Administrative operations for TSDB management.
+ - name: features
+ summary: Features
+ description: Query enabled features.
+ - name: remote
+ summary: Remote Storage
+ description: Remote read and write endpoints.
+ - name: otlp
+ summary: OTLP
+ description: OpenTelemetry Protocol metrics ingestion.
+ - name: notifications
+ summary: Notifications
+ description: Server notifications and events.
diff --git a/web/api/v1/translate_ast.go b/web/api/v1/translate_ast.go
index dc2e7e2901..3c2bc09943 100644
--- a/web/api/v1/translate_ast.go
+++ b/web/api/v1/translate_ast.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Prometheus Authors
+// 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
@@ -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/federate.go b/web/federate.go
index 443fd73568..730c0cf8e2 100644
--- a/web/federate.go
+++ b/web/federate.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Prometheus Authors
+// 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
@@ -32,7 +32,6 @@ import (
"github.com/prometheus/prometheus/model/timestamp"
"github.com/prometheus/prometheus/model/value"
"github.com/prometheus/prometheus/promql"
- "github.com/prometheus/prometheus/promql/parser"
"github.com/prometheus/prometheus/storage"
"github.com/prometheus/prometheus/tsdb"
"github.com/prometheus/prometheus/tsdb/chunkenc"
@@ -64,7 +63,7 @@ func (h *Handler) federation(w http.ResponseWriter, req *http.Request) {
return
}
- matcherSets, err := parser.ParseMetricSelectors(req.Form["match[]"])
+ matcherSets, err := h.options.Parser.ParseMetricSelectors(req.Form["match[]"])
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
diff --git a/web/federate_test.go b/web/federate_test.go
index 55e20c6b2f..1254bf6644 100644
--- a/web/federate_test.go
+++ b/web/federate_test.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The Prometheus Authors
+// 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
@@ -35,6 +35,7 @@ import (
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/model/textparse"
"github.com/prometheus/prometheus/promql"
+ "github.com/prometheus/prometheus/promql/parser"
"github.com/prometheus/prometheus/promql/promqltest"
"github.com/prometheus/prometheus/storage"
"github.com/prometheus/prometheus/tsdb"
@@ -42,6 +43,8 @@ import (
"github.com/prometheus/prometheus/util/testutil"
)
+var testParser = parser.NewParser(parser.Options{})
+
var scenarios = map[string]struct {
params string
externalLabels labels.Labels
@@ -212,7 +215,6 @@ func TestFederation(t *testing.T) {
test_metric_stale 1+10x99 stale
test_metric_old 1+10x98
`)
- t.Cleanup(func() { storage.Close() })
h := &Handler{
localStorage: &dbAdapter{storage.DB},
@@ -221,6 +223,7 @@ func TestFederation(t *testing.T) {
config: &config.Config{
GlobalConfig: config.GlobalConfig{},
},
+ options: &Options{Parser: testParser},
}
for name, scenario := range scenarios {
@@ -265,6 +268,7 @@ func TestFederation_NotReady(t *testing.T) {
ExternalLabels: scenario.externalLabels,
},
},
+ options: &Options{Parser: testParser},
}
req := httptest.NewRequest(http.MethodGet, "http://example.org/federate?"+scenario.params, nil)
@@ -303,7 +307,6 @@ func normalizeBody(body *bytes.Buffer) string {
func TestFederationWithNativeHistograms(t *testing.T) {
storage := teststorage.New(t)
- t.Cleanup(func() { storage.Close() })
var expVec promql.Vector
@@ -442,6 +445,7 @@ func TestFederationWithNativeHistograms(t *testing.T) {
config: &config.Config{
GlobalConfig: config.GlobalConfig{},
},
+ options: &Options{Parser: testParser},
}
req := httptest.NewRequest(http.MethodGet, "http://example.org/federate?match[]=test_metric", nil)
diff --git a/web/ui/assets_embed.go b/web/ui/assets_embed.go
index a5f8f5ddfa..48e4a2c6f1 100644
--- a/web/ui/assets_embed.go
+++ b/web/ui/assets_embed.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
diff --git a/web/ui/mantine-ui/package.json b/web/ui/mantine-ui/package.json
index 90f23898e4..8f35318090 100644
--- a/web/ui/mantine-ui/package.json
+++ b/web/ui/mantine-ui/package.json
@@ -1,7 +1,7 @@
{
"name": "@prometheus-io/mantine-ui",
"private": true,
- "version": "0.307.3",
+ "version": "0.309.1",
"type": "module",
"scripts": {
"start": "vite",
@@ -12,63 +12,63 @@
"test": "vitest"
},
"dependencies": {
- "@codemirror/autocomplete": "^6.19.0",
- "@codemirror/language": "^6.11.3",
- "@codemirror/lint": "^6.8.5",
- "@codemirror/state": "^6.5.2",
- "@codemirror/view": "^6.38.4",
- "@floating-ui/dom": "^1.7.4",
- "@lezer/common": "^1.2.3",
- "@lezer/highlight": "^1.2.1",
- "@mantine/code-highlight": "^8.3.5",
- "@mantine/core": "^8.3.5",
- "@mantine/dates": "^8.3.5",
- "@mantine/hooks": "^8.3.5",
- "@mantine/notifications": "^8.3.5",
+ "@codemirror/autocomplete": "^6.20.0",
+ "@codemirror/language": "^6.12.1",
+ "@codemirror/lint": "^6.9.3",
+ "@codemirror/state": "^6.5.4",
+ "@codemirror/view": "^6.39.12",
+ "@floating-ui/dom": "^1.7.5",
+ "@lezer/common": "^1.5.1",
+ "@lezer/highlight": "^1.2.3",
+ "@mantine/code-highlight": "^8.3.14",
+ "@mantine/core": "^8.3.14",
+ "@mantine/dates": "^8.3.14",
+ "@mantine/hooks": "^8.3.14",
+ "@mantine/notifications": "^8.3.14",
"@microsoft/fetch-event-source": "^2.0.1",
"@nexucis/fuzzy": "^0.5.1",
"@nexucis/kvsearch": "^0.9.1",
- "@prometheus-io/codemirror-promql": "0.307.3",
- "@reduxjs/toolkit": "^2.9.0",
- "@tabler/icons-react": "^3.35.0",
- "@tanstack/react-query": "^5.90.2",
- "@testing-library/jest-dom": "^6.9.0",
- "@testing-library/react": "^16.3.0",
- "@types/lodash": "^4.17.20",
+ "@prometheus-io/codemirror-promql": "0.309.1",
+ "@reduxjs/toolkit": "^2.11.2",
+ "@tabler/icons-react": "^3.36.1",
+ "@tanstack/react-query": "^5.90.20",
+ "@testing-library/jest-dom": "^6.9.1",
+ "@testing-library/react": "^16.3.2",
+ "@types/lodash": "^4.17.23",
"@types/sanitize-html": "^2.16.0",
- "@uiw/react-codemirror": "^4.25.2",
+ "@uiw/react-codemirror": "^4.25.4",
"clsx": "^2.1.1",
- "dayjs": "^1.11.18",
+ "dayjs": "^1.11.19",
"highlight.js": "^11.11.1",
- "lodash": "^4.17.21",
- "react": "^19.1.1",
- "react-dom": "^19.1.1",
- "react-infinite-scroll-component": "^6.1.0",
+ "lodash": "^4.17.23",
+ "react": "^19.2.4",
+ "react-dom": "^19.2.4",
+ "react-infinite-scroll-component": "^6.1.1",
"react-redux": "^9.2.0",
- "react-router-dom": "^7.9.3",
+ "react-router-dom": "^7.13.0",
"sanitize-html": "^2.17.0",
"uplot": "^1.6.32",
"uplot-react": "^1.2.4",
- "use-query-params": "^2.2.1"
+ "use-query-params": "^2.2.2"
},
"devDependencies": {
- "@eslint/compat": "^1.4.0",
- "@eslint/eslintrc": "^3.3.1",
- "@eslint/js": "^9.36.0",
- "@types/react": "^19.1.16",
- "@types/react-dom": "^19.1.9",
- "@typescript-eslint/eslint-plugin": "^8.45.0",
- "@typescript-eslint/parser": "^8.45.0",
+ "@eslint/compat": "^1.4.1",
+ "@eslint/eslintrc": "^3.3.3",
+ "@eslint/js": "^9.39.2",
+ "@types/react": "^19.2.13",
+ "@types/react-dom": "^19.2.3",
+ "@typescript-eslint/eslint-plugin": "^8.54.0",
+ "@typescript-eslint/parser": "^8.54.0",
"@vitejs/plugin-react": "^4.7.0",
- "eslint": "^9.36.0",
+ "eslint": "^9.39.2",
"eslint-plugin-react-hooks": "^5.2.0",
- "eslint-plugin-react-refresh": "^0.4.22",
- "globals": "^16.4.0",
+ "eslint-plugin-react-refresh": "^0.5.0",
+ "globals": "^16.5.0",
"jsdom": "^25.0.1",
"postcss": "^8.5.6",
"postcss-preset-mantine": "^1.18.0",
"postcss-simple-vars": "^7.0.1",
- "vite": "^6.3.6",
+ "vite": "^6.4.1",
"vitest": "^3.2.4"
}
}
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/ExplainViews/Selector.tsx b/web/ui/mantine-ui/src/pages/query/ExplainViews/Selector.tsx
index 2c564d3a4a..a83a0141d5 100644
--- a/web/ui/mantine-ui/src/pages/query/ExplainViews/Selector.tsx
+++ b/web/ui/mantine-ui/src/pages/query/ExplainViews/Selector.tsx
@@ -126,7 +126,7 @@ const matchingCriteriaList = (
};
const SelectorExplainView: FC = ({ node }) => {
- const baseMetricName = node.name.replace(/(_count|_sum|_bucket)$/, "");
+ const baseMetricName = node.name.replace(/(_count|_sum|_bucket|_total)$/, "");
const { lookbackDelta } = useSettings();
// Try to get metadata for the full unchanged metric name first.
diff --git a/web/ui/mantine-ui/src/pages/query/ExpressionInput.tsx b/web/ui/mantine-ui/src/pages/query/ExpressionInput.tsx
index a4b26cd910..2193dba267 100644
--- a/web/ui/mantine-ui/src/pages/query/ExpressionInput.tsx
+++ b/web/ui/mantine-ui/src/pages/query/ExpressionInput.tsx
@@ -11,7 +11,6 @@ import {
useComputedColorScheme,
} from "@mantine/core";
import {
- CompleteStrategy,
PromQLExtension,
newCompleteStrategy,
} from "@prometheus-io/codemirror-promql";
@@ -36,12 +35,9 @@ import {
bracketMatching,
indentOnInput,
syntaxHighlighting,
- syntaxTree,
} from "@codemirror/language";
import classes from "./ExpressionInput.module.css";
import {
- CompletionContext,
- CompletionResult,
autocompletion,
closeBrackets,
closeBracketsKeymap,
@@ -58,6 +54,7 @@ import { lintKeymap } from "@codemirror/lint";
import {
IconAlignJustified,
IconBinaryTree,
+ IconCopy,
IconDotsVertical,
IconSearch,
IconTerminal,
@@ -70,50 +67,10 @@ import MetricsExplorer from "./MetricsExplorer/MetricsExplorer";
import ErrorBoundary from "../../components/ErrorBoundary";
import { useAppSelector } from "../../state/hooks";
import { inputIconStyle, menuIconStyle } from "../../styles";
+import { HistoryCompleteStrategy } from "./HistoryCompleteStrategy";
const promqlExtension = new PromQLExtension();
-// Autocompletion strategy that wraps the main one and enriches
-// it with past query items.
-export class HistoryCompleteStrategy implements CompleteStrategy {
- private complete: CompleteStrategy;
- private queryHistory: string[];
- constructor(complete: CompleteStrategy, queryHistory: string[]) {
- this.complete = complete;
- this.queryHistory = queryHistory;
- }
-
- promQL(
- context: CompletionContext
- ): Promise | CompletionResult | null {
- return Promise.resolve(this.complete.promQL(context)).then((res) => {
- const { state, pos } = context;
- const tree = syntaxTree(state).resolve(pos, -1);
- const start = res != null ? res.from : tree.from;
-
- if (start !== 0) {
- return res;
- }
-
- const historyItems: CompletionResult = {
- from: start,
- to: pos,
- options: this.queryHistory.map((q) => ({
- label: q.length < 80 ? q : q.slice(0, 76).concat("..."),
- detail: "past query",
- apply: q,
- info: q.length < 80 ? undefined : q,
- })),
- validFor: /^[a-zA-Z0-9_:]+$/,
- };
-
- if (res !== null) {
- historyItems.options = historyItems.options.concat(res.options);
- }
- return historyItems;
- });
- }
-}
interface ExpressionInputProps {
initialExpr: string;
@@ -121,6 +78,7 @@ interface ExpressionInputProps {
executeQuery: (expr: string) => void;
treeShown: boolean;
setShowTree: (showTree: boolean) => void;
+ duplicatePanel: (expr: string) => void;
removePanel: () => void;
}
@@ -128,6 +86,7 @@ const ExpressionInput: FC = ({
initialExpr,
metricNames,
executeQuery,
+ duplicatePanel,
removePanel,
treeShown,
setShowTree,
@@ -250,6 +209,12 @@ const ExpressionInput: FC = ({
>
{treeShown ? "Hide" : "Show"} tree view
+ }
+ onClick={() => duplicatePanel(expr)}
+ >
+ Duplicate query
+
}
diff --git a/web/ui/mantine-ui/src/pages/query/Graph.tsx b/web/ui/mantine-ui/src/pages/query/Graph.tsx
index a30d52c822..fda14210b9 100644
--- a/web/ui/mantine-ui/src/pages/query/Graph.tsx
+++ b/web/ui/mantine-ui/src/pages/query/Graph.tsx
@@ -25,6 +25,7 @@ export interface GraphProps {
resolution: GraphResolution;
showExemplars: boolean;
displayMode: GraphDisplayMode;
+ yAxisMin: number | null;
retriggerIdx: number;
onSelectRange: (start: number, end: number) => void;
}
@@ -37,6 +38,7 @@ const Graph: FC = ({
resolution,
showExemplars,
displayMode,
+ yAxisMin,
retriggerIdx,
onSelectRange,
}) => {
@@ -222,6 +224,7 @@ const Graph: FC = ({
width={width}
showExemplars={showExemplars}
displayMode={displayMode}
+ yAxisMin={yAxisMin}
onSelectRange={onSelectRange}
/>
diff --git a/web/ui/mantine-ui/src/pages/query/HistoryCompleteStrategy.tsx b/web/ui/mantine-ui/src/pages/query/HistoryCompleteStrategy.tsx
new file mode 100644
index 0000000000..e56f645fc8
--- /dev/null
+++ b/web/ui/mantine-ui/src/pages/query/HistoryCompleteStrategy.tsx
@@ -0,0 +1,45 @@
+// Autocompletion strategy that wraps the main one and enriches
+// it with past query items.
+import {CompleteStrategy} from "@prometheus-io/codemirror-promql";
+import {CompletionContext, CompletionResult} from "@codemirror/autocomplete";
+import {syntaxTree} from "@codemirror/language";
+
+export class HistoryCompleteStrategy implements CompleteStrategy {
+ private complete: CompleteStrategy;
+ private queryHistory: string[];
+ constructor(complete: CompleteStrategy, queryHistory: string[]) {
+ this.complete = complete;
+ this.queryHistory = queryHistory;
+ }
+
+ promQL(
+ context: CompletionContext
+ ): Promise | CompletionResult | null {
+ return Promise.resolve(this.complete.promQL(context)).then((res) => {
+ const { state, pos } = context;
+ const tree = syntaxTree(state).resolve(pos, -1);
+ const start = res != null ? res.from : tree.from;
+
+ if (start !== 0) {
+ return res;
+ }
+
+ const historyItems: CompletionResult = {
+ from: start,
+ to: pos,
+ options: this.queryHistory.map((q) => ({
+ label: q.length < 80 ? q : q.slice(0, 76).concat("..."),
+ detail: "past query",
+ apply: q,
+ info: q.length < 80 ? undefined : q,
+ })),
+ validFor: /^[a-zA-Z0-9_:]+$/,
+ };
+
+ if (res !== null) {
+ historyItems.options = historyItems.options.concat(res.options);
+ }
+ return historyItems;
+ });
+ }
+}
\ No newline at end of file
diff --git a/web/ui/mantine-ui/src/pages/query/MetricsExplorer/MetricsExplorer.tsx b/web/ui/mantine-ui/src/pages/query/MetricsExplorer/MetricsExplorer.tsx
index 9c33a3df75..c351984698 100644
--- a/web/ui/mantine-ui/src/pages/query/MetricsExplorer/MetricsExplorer.tsx
+++ b/web/ui/mantine-ui/src/pages/query/MetricsExplorer/MetricsExplorer.tsx
@@ -73,7 +73,7 @@ const MetricsExplorer: FC = ({
// histogram/summary suffixes, it may be a metric that is not following naming
// conventions, see https://github.com/prometheus/prometheus/issues/16907).
data.data[m] ??
- data.data[m.replace(/(_count|_sum|_bucket)$/, "")] ?? [
+ data.data[m.replace(/(_count|_sum|_bucket|_total)$/, "")] ?? [
{ help: "unknown", type: "unknown", unit: "unknown" },
]
);
diff --git a/web/ui/mantine-ui/src/pages/query/QueryPanel.tsx b/web/ui/mantine-ui/src/pages/query/QueryPanel.tsx
index 24eb8c59cb..fcc7648a77 100644
--- a/web/ui/mantine-ui/src/pages/query/QueryPanel.tsx
+++ b/web/ui/mantine-ui/src/pages/query/QueryPanel.tsx
@@ -7,8 +7,12 @@ import {
SegmentedControl,
Stack,
Skeleton,
+ ActionIcon,
+ Popover,
+ Checkbox,
} from "@mantine/core";
import {
+ IconAdjustmentsHorizontal,
IconChartAreaFilled,
IconChartLine,
IconGraph,
@@ -19,6 +23,7 @@ import { FC, Suspense, useCallback, useMemo, useState } from "react";
import { useAppDispatch, useAppSelector } from "../../state/hooks";
import {
addQueryToHistory,
+ duplicatePanel,
GraphDisplayMode,
GraphResolution,
removePanel,
@@ -37,6 +42,7 @@ import ErrorBoundary from "../../components/ErrorBoundary";
import ASTNode from "../../promql/ast";
import serializeNode from "../../promql/serialize";
import ExplainView from "./ExplainViews/ExplainView";
+import { actionIconStyle } from "../../styles";
export interface PanelProps {
idx: number;
@@ -106,6 +112,9 @@ const QueryPanel: FC = ({ idx, metricNames }) => {
setSelectedNode(null);
}
}}
+ duplicatePanel={(expr: string) => {
+ dispatch(duplicatePanel({ idx, expr }));
+ }}
removePanel={() => {
dispatch(removePanel(idx));
}}
@@ -290,6 +299,39 @@ const QueryPanel: FC = ({ idx, metricNames }) => {
// },
]}
/>
+
+
+
+
+
+
+
+
+ dispatch(
+ setVisualizer({
+ idx,
+ visualizer: {
+ ...panel.visualizer,
+ yAxisMin: event.currentTarget.checked ? 0 : null,
+ },
+ })
+ )
+ }
+ />
+
+
@@ -301,6 +343,7 @@ const QueryPanel: FC = ({ idx, metricNames }) => {
resolution={panel.visualizer.resolution}
showExemplars={panel.visualizer.showExemplars}
displayMode={panel.visualizer.displayMode}
+ yAxisMin={panel.visualizer.yAxisMin}
retriggerIdx={retriggerIdx}
onSelectRange={onSelectRange}
/>
diff --git a/web/ui/mantine-ui/src/pages/query/UPlotChart.tsx b/web/ui/mantine-ui/src/pages/query/UPlotChart.tsx
index b3b2d75578..11e57f40f9 100644
--- a/web/ui/mantine-ui/src/pages/query/UPlotChart.tsx
+++ b/web/ui/mantine-ui/src/pages/query/UPlotChart.tsx
@@ -24,6 +24,7 @@ export interface UPlotChartProps {
width: number;
showExemplars: boolean;
displayMode: GraphDisplayMode;
+ yAxisMin: number | null;
onSelectRange: (start: number, end: number) => void;
}
@@ -34,6 +35,7 @@ const UPlotChart: FC = ({
range: { startTime, endTime, resolution },
width,
displayMode,
+ yAxisMin,
onSelectRange,
}) => {
const [options, setOptions] = useState(null);
@@ -60,6 +62,7 @@ const UPlotChart: FC = ({
width,
data,
useLocalTime,
+ yAxisMin,
theme === "light",
onSelectRange
);
@@ -81,6 +84,7 @@ const UPlotChart: FC = ({
useLocalTime,
theme,
onSelectRange,
+ yAxisMin,
]);
if (options === null || processedData === null) {
diff --git a/web/ui/mantine-ui/src/pages/query/uPlotChartHelpers.ts b/web/ui/mantine-ui/src/pages/query/uPlotChartHelpers.ts
index 3249afd454..ba6cdbae41 100644
--- a/web/ui/mantine-ui/src/pages/query/uPlotChartHelpers.ts
+++ b/web/ui/mantine-ui/src/pages/query/uPlotChartHelpers.ts
@@ -289,6 +289,7 @@ export const getUPlotOptions = (
width: number,
result: RangeSamples[],
useLocalTime: boolean,
+ yAxisMin: number | null,
light: boolean,
onSelectRange: (_start: number, _end: number) => void
): uPlot.Options => ({
@@ -330,6 +331,17 @@ export const getUPlotOptions = (
focus: {
alpha: 1,
},
+ scales:
+ yAxisMin !== null
+ ? {
+ y: {
+ range: (_u, _min, max) => {
+ const minMax = uPlot.rangeNum(yAxisMin, max, 0.1, true);
+ return [yAxisMin, minMax[1]];
+ },
+ },
+ }
+ : undefined,
axes: [
// X axis (time).
{
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 ca9988e60d..a20a6fae36 100644
--- a/web/ui/mantine-ui/src/pages/query/urlStateEncoding.ts
+++ b/web/ui/mantine-ui/src/pages/query/urlStateEncoding.ts
@@ -63,6 +63,9 @@ export const decodePanelOptionsFromURLParams = (query: string): Panel[] => {
panel.visualizer.displayMode =
value === "1" ? GraphDisplayMode.Stacked : GraphDisplayMode.Lines;
});
+ decodeSetting("y_axis_min", (value) => {
+ panel.visualizer.yAxisMin = value === "" ? null : parseFloat(value);
+ });
decodeSetting("show_exemplars", (value) => {
panel.visualizer.showExemplars = value === "1";
});
@@ -171,6 +174,11 @@ export const encodePanelOptionsToURLParams = (
}
addParam(idx, "display_mode", p.visualizer.displayMode);
+
+ 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 f4b883f678..8602c65a82 100644
--- a/web/ui/mantine-ui/src/promql/format.tsx
+++ b/web/ui/mantine-ui/src/promql/format.tsx
@@ -265,23 +265,21 @@ const formatNodeInternal = (
case nodeType.binaryExpr: {
let matching = <>>;
let grouping = <>>;
+ let fill = <>>;
const vm = node.matching;
- if (vm !== null && (vm.labels.length > 0 || vm.on)) {
- if (vm.on) {
+ if (vm !== null) {
+ if (
+ vm.labels.length > 0 ||
+ vm.on ||
+ vm.card === vectorMatchCardinality.manyToOne ||
+ vm.card === vectorMatchCardinality.oneToMany
+ ) {
matching = (
<>
{" "}
- on
- (
- {labelNameList(vm.labels)}
- )
- >
- );
- } else {
- matching = (
- <>
- {" "}
- ignoring
+
+ {vm.on ? "on" : "ignoring"}
+
(
{labelNameList(vm.labels)}
)
@@ -308,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 (
@@ -330,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/functionDocs.tsx b/web/ui/mantine-ui/src/promql/functionDocs.tsx
index 91221666d7..c7f744ba6f 100644
--- a/web/ui/mantine-ui/src/promql/functionDocs.tsx
+++ b/web/ui/mantine-ui/src/promql/functionDocs.tsx
@@ -1,28 +1,31 @@
-import React from 'react';
+import React from "react";
const funcDocs: Record = {
abs: (
<>
- abs(v instant-vector) returns the input vector with all sample values converted to their absolute value.
+ abs(v instant-vector) returns a vector containing all float samples in the input vector converted
+ to their absolute value. Histogram samples in the input vector are ignored silently.
>
),
absent: (
<>
- absent(v instant-vector) returns an empty vector if the vector passed to it has any elements (floats or
- native histograms) and a 1-element vector with the value 1 if the vector passed to it has no elements.
+ absent(v instant-vector) returns an empty vector if the vector passed to it has any elements (float
+ samples or histogram samples) and a 1-element vector with the value 1 if the vector passed to it has no
+ elements.
This is useful for alerting on when no time series exist for a given metric name and label combination.
- absent(nonexistent{'{'}job="myjob"{'}'}) # => {'{'}job="myjob"{'}'}
- absent(nonexistent{'{'}job="myjob",instance=~".*"{'}'}) # => {'{'}job="myjob"{'}'}
- absent(sum(nonexistent{'{'}job="myjob"{'}'})) # => {'{'}
- {'}'}
+ absent(nonexistent{"{"}job="myjob"{"}"}) # => {"{"}job="myjob"{"}"}
+ absent(nonexistent{"{"}job="myjob",instance=~".*"{"}"}) # => {"{"}job="myjob"
+ {"}"}
+ absent(sum(nonexistent{"{"}job="myjob"{"}"})) # => {"{"}
+ {"}"}
@@ -36,83 +39,83 @@ const funcDocs: Record = {
<>
absent_over_time(v range-vector) returns an empty vector if the range vector passed to it has any
- elements (floats or native histograms) and a 1-element vector with the value 1 if the range vector passed to it has
- no elements.
+ elements (float samples or histogram samples) and a 1-element vector with the value 1 if the range vector passed
+ to it has no elements.
- This is useful for alerting on when no time series exist for a given metric name and label combination for a certain
- amount of time.
+ This is useful for alerting on when no time series exist for a given metric name and label combination for a
+ certain amount of time.
- absent_over_time(nonexistent{'{'}job="myjob"{'}'}[1h]) # => {'{'}job="myjob"{'}'}
- absent_over_time(nonexistent{'{'}job="myjob",instance=~".*"{'}'}[1h]) # => {'{'}
- job="myjob"{'}'}
- absent_over_time(sum(nonexistent{'{'}job="myjob"{'}'})[1h:]) # => {'{'}
- {'}'}
+ absent_over_time(nonexistent{"{"}job="myjob"{"}"}[1h]) # => {"{"}job="myjob"{"}"}
+ absent_over_time(nonexistent{"{"}job="myjob",instance=~".*"{"}"}[1h]) # => {"{"}
+ job="myjob"{"}"}
+ absent_over_time(sum(nonexistent{"{"}job="myjob"{"}"})[1h:]) # => {"{"}
+ {"}"}
- In the first two examples, absent_over_time() tries to be smart about deriving labels of the 1-element
- output vector from the input vector.
+ In the first two examples, absent_over_time() tries to be smart about deriving labels of the
+ 1-element output vector from the input vector.
>
),
acos: (
<>
- The trigonometric functions work in radians:
+ The trigonometric functions work in radians. They ignore histogram samples in the input vector.
- acos(v instant-vector): calculates the arccosine of all elements in v (
+ acos(v instant-vector): calculates the arccosine of all float samples in v (
special cases ).
- acosh(v instant-vector): calculates the inverse hyperbolic cosine of all elements in v (
- special cases ).
+ acosh(v instant-vector): calculates the inverse hyperbolic cosine of all float samples in{" "}
+ v (special cases ).
- asin(v instant-vector): calculates the arcsine of all elements in v (
+ asin(v instant-vector): calculates the arcsine of all float samples in v (
special cases ).
- asinh(v instant-vector): calculates the inverse hyperbolic sine of all elements in v (
- special cases ).
+ asinh(v instant-vector): calculates the inverse hyperbolic sine of all float samples in{" "}
+ v (special cases ).
- atan(v instant-vector): calculates the arctangent of all elements in v (
+ atan(v instant-vector): calculates the arctangent of all float samples in v (
special cases ).
- atanh(v instant-vector): calculates the inverse hyperbolic tangent of all elements in v (
- special cases ).
+ atanh(v instant-vector): calculates the inverse hyperbolic tangent of all float samples in{" "}
+ v (special cases ).
- cos(v instant-vector): calculates the cosine of all elements in v (
+ cos(v instant-vector): calculates the cosine of all float samples in v (
special cases ).
- cosh(v instant-vector): calculates the hyperbolic cosine of all elements in v (
+ cosh(v instant-vector): calculates the hyperbolic cosine of all float samples in v (
special cases ).
- sin(v instant-vector): calculates the sine of all elements in v (
+ sin(v instant-vector): calculates the sine of all float samples in v (
special cases ).
- sinh(v instant-vector): calculates the hyperbolic sine of all elements in v (
+ sinh(v instant-vector): calculates the hyperbolic sine of all float samples in v (
special cases ).
- tan(v instant-vector): calculates the tangent of all elements in v (
+ tan(v instant-vector): calculates the tangent of all float samples in v (
special cases ).
- tanh(v instant-vector): calculates the hyperbolic tangent of all elements in v (
- special cases ).
+ tanh(v instant-vector): calculates the hyperbolic tangent of all float samples in v{" "}
+ (special cases ).
@@ -120,69 +123,69 @@ const funcDocs: Record = {
- deg(v instant-vector): converts radians to degrees for all elements in v.
+ deg(v instant-vector): converts radians to degrees for all float samples in v.
pi(): returns pi.
- rad(v instant-vector): converts degrees to radians for all elements in v.
+ rad(v instant-vector): converts degrees to radians for all float samples in v.
>
),
acosh: (
<>
- The trigonometric functions work in radians:
+ The trigonometric functions work in radians. They ignore histogram samples in the input vector.
- acos(v instant-vector): calculates the arccosine of all elements in v (
+ acos(v instant-vector): calculates the arccosine of all float samples in v (
special cases ).
- acosh(v instant-vector): calculates the inverse hyperbolic cosine of all elements in v (
- special cases ).
+ acosh(v instant-vector): calculates the inverse hyperbolic cosine of all float samples in{" "}
+ v (special cases ).
- asin(v instant-vector): calculates the arcsine of all elements in v (
+ asin(v instant-vector): calculates the arcsine of all float samples in v (
special cases ).
- asinh(v instant-vector): calculates the inverse hyperbolic sine of all elements in v (
- special cases ).
+ asinh(v instant-vector): calculates the inverse hyperbolic sine of all float samples in{" "}
+ v (special cases ).
- atan(v instant-vector): calculates the arctangent of all elements in v (
+ atan(v instant-vector): calculates the arctangent of all float samples in v (
special cases ).
- atanh(v instant-vector): calculates the inverse hyperbolic tangent of all elements in v (
- special cases ).
+ atanh(v instant-vector): calculates the inverse hyperbolic tangent of all float samples in{" "}
+ v (special cases ).
- cos(v instant-vector): calculates the cosine of all elements in v (
+ cos(v instant-vector): calculates the cosine of all float samples in v (
special cases ).
- cosh(v instant-vector): calculates the hyperbolic cosine of all elements in v (
+ cosh(v instant-vector): calculates the hyperbolic cosine of all float samples in v (
special cases ).
- sin(v instant-vector): calculates the sine of all elements in v (
+ sin(v instant-vector): calculates the sine of all float samples in v (
special cases ).
- sinh(v instant-vector): calculates the hyperbolic sine of all elements in v (
+ sinh(v instant-vector): calculates the hyperbolic sine of all float samples in v (
special cases ).
- tan(v instant-vector): calculates the tangent of all elements in v (
+ tan(v instant-vector): calculates the tangent of all float samples in v (
special cases ).
- tanh(v instant-vector): calculates the hyperbolic tangent of all elements in v (
- special cases ).
+ tanh(v instant-vector): calculates the hyperbolic tangent of all float samples in v{" "}
+ (special cases ).
@@ -190,69 +193,69 @@ const funcDocs: Record = {
- deg(v instant-vector): converts radians to degrees for all elements in v.
+ deg(v instant-vector): converts radians to degrees for all float samples in v.
pi(): returns pi.
- rad(v instant-vector): converts degrees to radians for all elements in v.
+ rad(v instant-vector): converts degrees to radians for all float samples in v.
>
),
asin: (
<>
- The trigonometric functions work in radians:
+ The trigonometric functions work in radians. They ignore histogram samples in the input vector.
- acos(v instant-vector): calculates the arccosine of all elements in v (
+ acos(v instant-vector): calculates the arccosine of all float samples in v (
special cases ).
- acosh(v instant-vector): calculates the inverse hyperbolic cosine of all elements in v (
- special cases ).
+ acosh(v instant-vector): calculates the inverse hyperbolic cosine of all float samples in{" "}
+ v (special cases ).
- asin(v instant-vector): calculates the arcsine of all elements in v (
+ asin(v instant-vector): calculates the arcsine of all float samples in v (
special cases ).
- asinh(v instant-vector): calculates the inverse hyperbolic sine of all elements in v (
- special cases ).
+ asinh(v instant-vector): calculates the inverse hyperbolic sine of all float samples in{" "}
+ v (special cases ).
- atan(v instant-vector): calculates the arctangent of all elements in v (
+ atan(v instant-vector): calculates the arctangent of all float samples in v (
special cases ).
- atanh(v instant-vector): calculates the inverse hyperbolic tangent of all elements in v (
- special cases ).
+ atanh(v instant-vector): calculates the inverse hyperbolic tangent of all float samples in{" "}
+ v (special cases ).
- cos(v instant-vector): calculates the cosine of all elements in v (
+ cos(v instant-vector): calculates the cosine of all float samples in v (
special cases ).
- cosh(v instant-vector): calculates the hyperbolic cosine of all elements in v (
+ cosh(v instant-vector): calculates the hyperbolic cosine of all float samples in v (
special cases ).
- sin(v instant-vector): calculates the sine of all elements in v (
+ sin(v instant-vector): calculates the sine of all float samples in v (
special cases ).
- sinh(v instant-vector): calculates the hyperbolic sine of all elements in v (
+ sinh(v instant-vector): calculates the hyperbolic sine of all float samples in v (
special cases ).
- tan(v instant-vector): calculates the tangent of all elements in v (
+ tan(v instant-vector): calculates the tangent of all float samples in v (
special cases ).
- tanh(v instant-vector): calculates the hyperbolic tangent of all elements in v (
- special cases ).
+ tanh(v instant-vector): calculates the hyperbolic tangent of all float samples in v{" "}
+ (special cases ).
@@ -260,69 +263,69 @@ const funcDocs: Record = {
- deg(v instant-vector): converts radians to degrees for all elements in v.
+ deg(v instant-vector): converts radians to degrees for all float samples in v.
pi(): returns pi.
- rad(v instant-vector): converts degrees to radians for all elements in v.
+ rad(v instant-vector): converts degrees to radians for all float samples in v.
>
),
asinh: (
<>
- The trigonometric functions work in radians:
+ The trigonometric functions work in radians. They ignore histogram samples in the input vector.
- acos(v instant-vector): calculates the arccosine of all elements in v (
+ acos(v instant-vector): calculates the arccosine of all float samples in v (
special cases ).
- acosh(v instant-vector): calculates the inverse hyperbolic cosine of all elements in v (
- special cases ).
+ acosh(v instant-vector): calculates the inverse hyperbolic cosine of all float samples in{" "}
+ v (special cases ).
- asin(v instant-vector): calculates the arcsine of all elements in v (
+ asin(v instant-vector): calculates the arcsine of all float samples in v (
special cases ).
- asinh(v instant-vector): calculates the inverse hyperbolic sine of all elements in v (
- special cases ).
+ asinh(v instant-vector): calculates the inverse hyperbolic sine of all float samples in{" "}
+ v (special cases ).
- atan(v instant-vector): calculates the arctangent of all elements in v (
+ atan(v instant-vector): calculates the arctangent of all float samples in v (
special cases ).
- atanh(v instant-vector): calculates the inverse hyperbolic tangent of all elements in v (
- special cases ).
+ atanh(v instant-vector): calculates the inverse hyperbolic tangent of all float samples in{" "}
+ v (special cases ).
- cos(v instant-vector): calculates the cosine of all elements in v (
+ cos(v instant-vector): calculates the cosine of all float samples in v (
special cases ).
- cosh(v instant-vector): calculates the hyperbolic cosine of all elements in v (
+ cosh(v instant-vector): calculates the hyperbolic cosine of all float samples in v (
special cases ).
- sin(v instant-vector): calculates the sine of all elements in v (
+ sin(v instant-vector): calculates the sine of all float samples in v (
special cases ).
- sinh(v instant-vector): calculates the hyperbolic sine of all elements in v (
+ sinh(v instant-vector): calculates the hyperbolic sine of all float samples in v (
special cases ).
- tan(v instant-vector): calculates the tangent of all elements in v (
+ tan(v instant-vector): calculates the tangent of all float samples in v (
special cases ).
- tanh(v instant-vector): calculates the hyperbolic tangent of all elements in v (
- special cases ).
+ tanh(v instant-vector): calculates the hyperbolic tangent of all float samples in v{" "}
+ (special cases ).
@@ -330,69 +333,69 @@ const funcDocs: Record = {
- deg(v instant-vector): converts radians to degrees for all elements in v.
+ deg(v instant-vector): converts radians to degrees for all float samples in v.
pi(): returns pi.
- rad(v instant-vector): converts degrees to radians for all elements in v.
+ rad(v instant-vector): converts degrees to radians for all float samples in v.
>
),
atan: (
<>
- The trigonometric functions work in radians:
+ The trigonometric functions work in radians. They ignore histogram samples in the input vector.
- acos(v instant-vector): calculates the arccosine of all elements in v (
+ acos(v instant-vector): calculates the arccosine of all float samples in v (
special cases ).
- acosh(v instant-vector): calculates the inverse hyperbolic cosine of all elements in v (
- special cases ).
+ acosh(v instant-vector): calculates the inverse hyperbolic cosine of all float samples in{" "}
+ v (special cases ).
- asin(v instant-vector): calculates the arcsine of all elements in v (
+ asin(v instant-vector): calculates the arcsine of all float samples in v (
special cases ).
- asinh(v instant-vector): calculates the inverse hyperbolic sine of all elements in v (
- special cases ).
+ asinh(v instant-vector): calculates the inverse hyperbolic sine of all float samples in{" "}
+ v (special cases ).
- atan(v instant-vector): calculates the arctangent of all elements in v (
+ atan(v instant-vector): calculates the arctangent of all float samples in v (
special cases ).
- atanh(v instant-vector): calculates the inverse hyperbolic tangent of all elements in v (
- special cases ).
+ atanh(v instant-vector): calculates the inverse hyperbolic tangent of all float samples in{" "}
+ v (special cases ).
- cos(v instant-vector): calculates the cosine of all elements in v (
+ cos(v instant-vector): calculates the cosine of all float samples in v (
special cases ).
- cosh(v instant-vector): calculates the hyperbolic cosine of all elements in v (
+ cosh(v instant-vector): calculates the hyperbolic cosine of all float samples in v (
special cases ).
- sin(v instant-vector): calculates the sine of all elements in v (
+ sin(v instant-vector): calculates the sine of all float samples in v (
special cases ).
- sinh(v instant-vector): calculates the hyperbolic sine of all elements in v (
+ sinh(v instant-vector): calculates the hyperbolic sine of all float samples in v (
special cases ).
- tan(v instant-vector): calculates the tangent of all elements in v (
+ tan(v instant-vector): calculates the tangent of all float samples in v (
special cases ).
- tanh(v instant-vector): calculates the hyperbolic tangent of all elements in v (
- special cases ).
+ tanh(v instant-vector): calculates the hyperbolic tangent of all float samples in v{" "}
+ (special cases ).
@@ -400,69 +403,69 @@ const funcDocs: Record = {
- deg(v instant-vector): converts radians to degrees for all elements in v.
+ deg(v instant-vector): converts radians to degrees for all float samples in v.
pi(): returns pi.
- rad(v instant-vector): converts degrees to radians for all elements in v.
+ rad(v instant-vector): converts degrees to radians for all float samples in v.
>
),
atanh: (
<>
- The trigonometric functions work in radians:
+ The trigonometric functions work in radians. They ignore histogram samples in the input vector.
- acos(v instant-vector): calculates the arccosine of all elements in v (
+ acos(v instant-vector): calculates the arccosine of all float samples in v (
special cases ).
- acosh(v instant-vector): calculates the inverse hyperbolic cosine of all elements in v (
- special cases ).
+ acosh(v instant-vector): calculates the inverse hyperbolic cosine of all float samples in{" "}
+ v (special cases ).
- asin(v instant-vector): calculates the arcsine of all elements in v (
+ asin(v instant-vector): calculates the arcsine of all float samples in v (
special cases ).
- asinh(v instant-vector): calculates the inverse hyperbolic sine of all elements in v (
- special cases ).
+ asinh(v instant-vector): calculates the inverse hyperbolic sine of all float samples in{" "}
+ v (special cases ).
- atan(v instant-vector): calculates the arctangent of all elements in v (
+ atan(v instant-vector): calculates the arctangent of all float samples in v (
special cases ).
- atanh(v instant-vector): calculates the inverse hyperbolic tangent of all elements in v (
- special cases ).
+ atanh(v instant-vector): calculates the inverse hyperbolic tangent of all float samples in{" "}
+ v (special cases ).
- cos(v instant-vector): calculates the cosine of all elements in v (
+ cos(v instant-vector): calculates the cosine of all float samples in v (
special cases ).
- cosh(v instant-vector): calculates the hyperbolic cosine of all elements in v (
+ cosh(v instant-vector): calculates the hyperbolic cosine of all float samples in v (
special cases ).
- sin(v instant-vector): calculates the sine of all elements in v (
+ sin(v instant-vector): calculates the sine of all float samples in v (
special cases ).
- sinh(v instant-vector): calculates the hyperbolic sine of all elements in v (
+ sinh(v instant-vector): calculates the hyperbolic sine of all float samples in v (
special cases ).
- tan(v instant-vector): calculates the tangent of all elements in v (
+ tan(v instant-vector): calculates the tangent of all float samples in v (
special cases ).
- tanh(v instant-vector): calculates the hyperbolic tangent of all elements in v (
- special cases ).
+ tanh(v instant-vector): calculates the hyperbolic tangent of all float samples in v{" "}
+ (special cases ).
@@ -470,13 +473,13 @@ const funcDocs: Record = {
- deg(v instant-vector): converts radians to degrees for all elements in v.
+ deg(v instant-vector): converts radians to degrees for all float samples in v.
pi(): returns pi.
- rad(v instant-vector): converts degrees to radians for all elements in v.
+ rad(v instant-vector): converts degrees to radians for all float samples in v.
>
@@ -484,40 +487,42 @@ const funcDocs: Record = {
avg_over_time: (
<>
- The following functions allow aggregating each series of a given range vector over time and return an instant vector
- with per-series aggregation results:
+ The following functions allow aggregating each series of a given range vector over time and return an instant
+ vector with per-series aggregation results:
- avg_over_time(range-vector): the average value of all points in the specified interval.
+ avg_over_time(range-vector): the average value of all float or histogram samples in the specified
+ interval (see details below).
- min_over_time(range-vector): the minimum value of all points in the specified interval.
+ min_over_time(range-vector): the minimum value of all float samples in the specified interval.
- max_over_time(range-vector): the maximum value of all points in the specified interval.
+ max_over_time(range-vector): the maximum value of all float samples in the specified interval.
- sum_over_time(range-vector): the sum of all values in the specified interval.
+ sum_over_time(range-vector): the sum of all float or histogram samples in the specified interval
+ (see details below).
- count_over_time(range-vector): the count of all values in the specified interval.
+ count_over_time(range-vector): the count of all samples in the specified interval.
- quantile_over_time(scalar, range-vector): the φ-quantile (0 ≤ φ ≤ 1) of the values in the specified
- interval.
+ quantile_over_time(scalar, range-vector): the φ-quantile (0 ≤ φ ≤ 1) of all float samples in the
+ specified interval.
- stddev_over_time(range-vector): the population standard deviation of the values in the specified
- interval.
+ stddev_over_time(range-vector): the population standard deviation of all float samples in the
+ specified interval.
- stdvar_over_time(range-vector): the population standard variance of the values in the specified
- interval.
+ stdvar_over_time(range-vector): the population standard variance of all float samples in the
+ specified interval.
- last_over_time(range-vector): the most recent point value in the specified interval.
+ last_over_time(range-vector): the most recent sample in the specified interval.
present_over_time(range-vector): the value 1 for any series in the specified interval.
@@ -526,32 +531,75 @@ const funcDocs: Record = {
If the feature flag
- --enable-feature=promql-experimental-functions is set, the following additional functions are available:
+ --enable-feature=promql-experimental-functions is set, the following additional functions are
+ available:
- mad_over_time(range-vector): the median absolute deviation of all points in the specified interval.
+ mad_over_time(range-vector): the median absolute deviation of all float samples in the specified
+ interval.
+
+
+ ts_of_min_over_time(range-vector): the timestamp of the last float sample that has the minimum
+ value of all float samples in the specified interval.
+
+
+ ts_of_max_over_time(range-vector): the timestamp of the last float sample that has the maximum
+ value of all float samples in the specified interval.
+
+
+ ts_of_last_over_time(range-vector): the timestamp of last sample in the specified interval.
+
+
+ first_over_time(range-vector): the oldest sample in the specified interval.
+
+
+ ts_of_first_over_time(range-vector): the timestamp of earliest sample in the specified interval.
- Note that all values in the specified interval have the same weight in the aggregation even if the values are not
- equally spaced throughout the interval.
+ Note that all values in the specified interval have the same weight in the aggregation even if the values are
+ not equally spaced throughout the interval.
+ These functions act on histograms in the following way:
+
+
+
+ count_over_time, first_over_time, last_over_time, and
+ present_over_time() act on float and histogram samples in the same way.
+
+
+ avg_over_time() and sum_over_time() act on histogram samples in a way that
+ corresponds to the respective aggregation operators. If a series contains a mix of float samples and histogram
+ samples within the range, the corresponding result is removed entirely from the output vector. Such a removal
+ is flagged by a warn-level annotation.
+
+
+ All other functions ignore histogram samples in the following way: Input ranges containing only histogram
+ samples are silently removed from the output. For ranges with a mix of histogram and float samples, only the
+ float samples are processed and the omission of the histogram samples is flagged by an info-level annotation.
+
+
+
- avg_over_time, sum_over_time, count_over_time, last_over_time,
- and
- present_over_time handle native histograms as expected. All other functions ignore histogram samples.
+ first_over_time(m[1m]) differs from m offset 1m in that the former will select the
+ first sample of m within the 1m range, where m offset 1m will select the most
+ recent sample within the lookback interval outside and prior to the 1m offset. This is particularly
+ useful with first_over_time(m[step()])
+ in range queries (available when --enable-feature=promql-duration-expr is set) to ensure that the
+ sample selected is within the range step.
>
),
ceil: (
<>
- ceil(v instant-vector) rounds the sample values of all elements in v up to the nearest
- integer value greater than or equal to v.
+ ceil(v instant-vector) returns a vector containing all float samples in the input vector rounded up
+ to the nearest integer value greater than or equal to their original value. Histogram samples in the input
+ vector are ignored silently.
@@ -573,17 +621,19 @@ const funcDocs: Record = {
changes: (
<>
- For each input time series, changes(v range-vector) returns the number of times its value has changed
- within the provided time range as an instant vector.
+ For each input time series, changes(v range-vector) returns the number of times its value has
+ changed within the provided time range as an instant vector. A float sample followed by a histogram sample, or
+ vice versa, counts as a change. A counter histogram sample followed by a gauge histogram sample with otherwise
+ exactly the same values, or vice versa, does not count as a change.
>
),
clamp: (
<>
- clamp(v instant-vector, min scalar, max scalar)
- clamps the sample values of all elements in v to have a lower limit of min and an upper
- limit of max.
+ clamp(v instant-vector, min scalar, max scalar) clamps the values of all float samples in{" "}
+ v to have a lower limit of min and an upper limit of
+ max. Histogram samples in the input vector are ignored silently.
Special cases:
@@ -593,7 +643,7 @@ const funcDocs: Record = {
Return an empty vector if min > max
- Return NaN if min or max is NaN
+ Float samples are clamped to NaN if min or max is NaN
>
@@ -601,71 +651,71 @@ const funcDocs: Record = {
clamp_max: (
<>
- clamp_max(v instant-vector, max scalar) clamps the sample values of all elements in v to
- have an upper limit of max.
+ clamp_max(v instant-vector, max scalar) clamps the values of all float samples in v to
+ have an upper limit of max. Histogram samples in the input vector are ignored silently.
>
),
clamp_min: (
<>
- clamp_min(v instant-vector, min scalar) clamps the sample values of all elements in v to
- have a lower limit of min.
+ clamp_min(v instant-vector, min scalar) clamps the values of all float samples in v to
+ have a lower limit of min. Histogram samples in the input vector are ignored silently.
>
),
cos: (
<>
- The trigonometric functions work in radians:
+ The trigonometric functions work in radians. They ignore histogram samples in the input vector.
- acos(v instant-vector): calculates the arccosine of all elements in v (
+ acos(v instant-vector): calculates the arccosine of all float samples in v (
special cases ).
- acosh(v instant-vector): calculates the inverse hyperbolic cosine of all elements in v (
- special cases ).
+ acosh(v instant-vector): calculates the inverse hyperbolic cosine of all float samples in{" "}
+ v (special cases ).
- asin(v instant-vector): calculates the arcsine of all elements in v (
+ asin(v instant-vector): calculates the arcsine of all float samples in v (
special cases ).
- asinh(v instant-vector): calculates the inverse hyperbolic sine of all elements in v (
- special cases ).
+ asinh(v instant-vector): calculates the inverse hyperbolic sine of all float samples in{" "}
+ v (special cases ).
- atan(v instant-vector): calculates the arctangent of all elements in v (
+ atan(v instant-vector): calculates the arctangent of all float samples in v (
special cases ).
- atanh(v instant-vector): calculates the inverse hyperbolic tangent of all elements in v (
- special cases ).
+ atanh(v instant-vector): calculates the inverse hyperbolic tangent of all float samples in{" "}
+ v (special cases ).
- cos(v instant-vector): calculates the cosine of all elements in v (
+ cos(v instant-vector): calculates the cosine of all float samples in v (
special cases ).
- cosh(v instant-vector): calculates the hyperbolic cosine of all elements in v (
+ cosh(v instant-vector): calculates the hyperbolic cosine of all float samples in v (
special cases ).
- sin(v instant-vector): calculates the sine of all elements in v (
+ sin(v instant-vector): calculates the sine of all float samples in v (
special cases ).
- sinh(v instant-vector): calculates the hyperbolic sine of all elements in v (
+ sinh(v instant-vector): calculates the hyperbolic sine of all float samples in v (
special cases ).
- tan(v instant-vector): calculates the tangent of all elements in v (
+ tan(v instant-vector): calculates the tangent of all float samples in v (
special cases ).
- tanh(v instant-vector): calculates the hyperbolic tangent of all elements in v (
- special cases ).
+ tanh(v instant-vector): calculates the hyperbolic tangent of all float samples in v{" "}
+ (special cases ).
@@ -673,69 +723,69 @@ const funcDocs: Record = {
- deg(v instant-vector): converts radians to degrees for all elements in v.
+ deg(v instant-vector): converts radians to degrees for all float samples in v.
pi(): returns pi.
- rad(v instant-vector): converts degrees to radians for all elements in v.
+ rad(v instant-vector): converts degrees to radians for all float samples in v.
>
),
cosh: (
<>
- The trigonometric functions work in radians:
+ The trigonometric functions work in radians. They ignore histogram samples in the input vector.
- acos(v instant-vector): calculates the arccosine of all elements in v (
+ acos(v instant-vector): calculates the arccosine of all float samples in v (
special cases ).
- acosh(v instant-vector): calculates the inverse hyperbolic cosine of all elements in v (
- special cases ).
+ acosh(v instant-vector): calculates the inverse hyperbolic cosine of all float samples in{" "}
+ v (special cases ).
- asin(v instant-vector): calculates the arcsine of all elements in v (
+ asin(v instant-vector): calculates the arcsine of all float samples in v (
special cases ).
- asinh(v instant-vector): calculates the inverse hyperbolic sine of all elements in v (
- special cases ).
+ asinh(v instant-vector): calculates the inverse hyperbolic sine of all float samples in{" "}
+ v (special cases ).
- atan(v instant-vector): calculates the arctangent of all elements in v (
+ atan(v instant-vector): calculates the arctangent of all float samples in v (
special cases ).
- atanh(v instant-vector): calculates the inverse hyperbolic tangent of all elements in v (
- special cases ).
+ atanh(v instant-vector): calculates the inverse hyperbolic tangent of all float samples in{" "}
+ v (special cases ).
- cos(v instant-vector): calculates the cosine of all elements in v (
+ cos(v instant-vector): calculates the cosine of all float samples in v (
special cases ).
- cosh(v instant-vector): calculates the hyperbolic cosine of all elements in v (
+ cosh(v instant-vector): calculates the hyperbolic cosine of all float samples in v (
special cases ).
- sin(v instant-vector): calculates the sine of all elements in v (
+ sin(v instant-vector): calculates the sine of all float samples in v (
special cases ).
- sinh(v instant-vector): calculates the hyperbolic sine of all elements in v (
+ sinh(v instant-vector): calculates the hyperbolic sine of all float samples in v (
special cases ).
- tan(v instant-vector): calculates the tangent of all elements in v (
+ tan(v instant-vector): calculates the tangent of all float samples in v (
special cases ).
- tanh(v instant-vector): calculates the hyperbolic tangent of all elements in v (
- special cases ).
+ tanh(v instant-vector): calculates the hyperbolic tangent of all float samples in v{" "}
+ (special cases ).
@@ -743,13 +793,13 @@ const funcDocs: Record = {
- deg(v instant-vector): converts radians to degrees for all elements in v.
+ deg(v instant-vector): converts radians to degrees for all float samples in v.
pi(): returns pi.
- rad(v instant-vector): converts degrees to radians for all elements in v.
+ rad(v instant-vector): converts degrees to radians for all float samples in v.
>
@@ -757,40 +807,42 @@ const funcDocs: Record = {
count_over_time: (
<>
- The following functions allow aggregating each series of a given range vector over time and return an instant vector
- with per-series aggregation results:
+ The following functions allow aggregating each series of a given range vector over time and return an instant
+ vector with per-series aggregation results:
- avg_over_time(range-vector): the average value of all points in the specified interval.
+ avg_over_time(range-vector): the average value of all float or histogram samples in the specified
+ interval (see details below).
- min_over_time(range-vector): the minimum value of all points in the specified interval.
+ min_over_time(range-vector): the minimum value of all float samples in the specified interval.
- max_over_time(range-vector): the maximum value of all points in the specified interval.
+ max_over_time(range-vector): the maximum value of all float samples in the specified interval.
- sum_over_time(range-vector): the sum of all values in the specified interval.
+ sum_over_time(range-vector): the sum of all float or histogram samples in the specified interval
+ (see details below).
- count_over_time(range-vector): the count of all values in the specified interval.
+ count_over_time(range-vector): the count of all samples in the specified interval.
- quantile_over_time(scalar, range-vector): the φ-quantile (0 ≤ φ ≤ 1) of the values in the specified
- interval.
+ quantile_over_time(scalar, range-vector): the φ-quantile (0 ≤ φ ≤ 1) of all float samples in the
+ specified interval.
- stddev_over_time(range-vector): the population standard deviation of the values in the specified
- interval.
+ stddev_over_time(range-vector): the population standard deviation of all float samples in the
+ specified interval.
- stdvar_over_time(range-vector): the population standard variance of the values in the specified
- interval.
+ stdvar_over_time(range-vector): the population standard variance of all float samples in the
+ specified interval.
- last_over_time(range-vector): the most recent point value in the specified interval.
+ last_over_time(range-vector): the most recent sample in the specified interval.
present_over_time(range-vector): the value 1 for any series in the specified interval.
@@ -799,111 +851,161 @@ const funcDocs: Record = {
If the feature flag
- --enable-feature=promql-experimental-functions is set, the following additional functions are available:
+ --enable-feature=promql-experimental-functions is set, the following additional functions are
+ available:
- mad_over_time(range-vector): the median absolute deviation of all points in the specified interval.
+ mad_over_time(range-vector): the median absolute deviation of all float samples in the specified
+ interval.
+
+
+ ts_of_min_over_time(range-vector): the timestamp of the last float sample that has the minimum
+ value of all float samples in the specified interval.
+
+
+ ts_of_max_over_time(range-vector): the timestamp of the last float sample that has the maximum
+ value of all float samples in the specified interval.
+
+
+ ts_of_last_over_time(range-vector): the timestamp of last sample in the specified interval.
+
+
+ first_over_time(range-vector): the oldest sample in the specified interval.
+
+
+ ts_of_first_over_time(range-vector): the timestamp of earliest sample in the specified interval.
- Note that all values in the specified interval have the same weight in the aggregation even if the values are not
- equally spaced throughout the interval.
+ Note that all values in the specified interval have the same weight in the aggregation even if the values are
+ not equally spaced throughout the interval.
+ These functions act on histograms in the following way:
+
+
+
+ count_over_time, first_over_time, last_over_time, and
+ present_over_time() act on float and histogram samples in the same way.
+
+
+ avg_over_time() and sum_over_time() act on histogram samples in a way that
+ corresponds to the respective aggregation operators. If a series contains a mix of float samples and histogram
+ samples within the range, the corresponding result is removed entirely from the output vector. Such a removal
+ is flagged by a warn-level annotation.
+
+
+ All other functions ignore histogram samples in the following way: Input ranges containing only histogram
+ samples are silently removed from the output. For ranges with a mix of histogram and float samples, only the
+ float samples are processed and the omission of the histogram samples is flagged by an info-level annotation.
+
+
+
- avg_over_time, sum_over_time, count_over_time, last_over_time,
- and
- present_over_time handle native histograms as expected. All other functions ignore histogram samples.
+ first_over_time(m[1m]) differs from m offset 1m in that the former will select the
+ first sample of m within the 1m range, where m offset 1m will select the most
+ recent sample within the lookback interval outside and prior to the 1m offset. This is particularly
+ useful with first_over_time(m[step()])
+ in range queries (available when --enable-feature=promql-duration-expr is set) to ensure that the
+ sample selected is within the range step.
>
),
day_of_month: (
<>
- day_of_month(v=vector(time()) instant-vector) returns the day of the month for each of the given times
- in UTC. Returned values are from 1 to 31.
+ day_of_month(v=vector(time()) instant-vector) interprets float samples in
+ v as timestamps (number of seconds since January 1, 1970 UTC) and returns the day of the month (in
+ UTC) for each of those timestamps. Returned values are from 1 to 31. Histogram samples in the input vector are
+ ignored silently.
>
),
day_of_week: (
<>
- day_of_week(v=vector(time()) instant-vector) returns the day of the week for each of the given times in
- UTC. Returned values are from 0 to 6, where 0 means Sunday etc.
+ day_of_week(v=vector(time()) instant-vector) interprets float samples in v
+ as timestamps (number of seconds since January 1, 1970 UTC) and returns the day of the week (in UTC) for each of
+ those timestamps. Returned values are from 0 to 6, where 0 means Sunday etc. Histogram samples in the input
+ vector are ignored silently.
>
),
day_of_year: (
<>
- day_of_year(v=vector(time()) instant-vector) returns the day of the year for each of the given times in
- UTC. Returned values are from 1 to 365 for non-leap years, and 1 to 366 in leap years.
+ day_of_year(v=vector(time()) instant-vector) interprets float samples in v
+ as timestamps (number of seconds since January 1, 1970 UTC) and returns the day of the year (in UTC) for each of
+ those timestamps. Returned values are from 1 to 365 for non-leap years, and 1 to 366 in leap years. Histogram
+ samples in the input vector are ignored silently.
>
),
days_in_month: (
<>
- days_in_month(v=vector(time()) instant-vector) returns number of days in the month for each of the given
- times in UTC. Returned values are from 28 to 31.
+ days_in_month(v=vector(time()) instant-vector) interprets float samples in
+ v as timestamps (number of seconds since January 1, 1970 UTC) and returns the number of days in the
+ month of each of those timestamps (in UTC). Returned values are from 28 to 31. Histogram samples in the input
+ vector are ignored silently.
>
),
deg: (
<>
- The trigonometric functions work in radians:
+ The trigonometric functions work in radians. They ignore histogram samples in the input vector.
- acos(v instant-vector): calculates the arccosine of all elements in v (
+ acos(v instant-vector): calculates the arccosine of all float samples in v (
special cases ).
- acosh(v instant-vector): calculates the inverse hyperbolic cosine of all elements in v (
- special cases ).
+ acosh(v instant-vector): calculates the inverse hyperbolic cosine of all float samples in{" "}
+ v (special cases ).
- asin(v instant-vector): calculates the arcsine of all elements in v (
+ asin(v instant-vector): calculates the arcsine of all float samples in v (
special cases ).
- asinh(v instant-vector): calculates the inverse hyperbolic sine of all elements in v (
- special cases ).
+ asinh(v instant-vector): calculates the inverse hyperbolic sine of all float samples in{" "}
+ v (special cases ).
- atan(v instant-vector): calculates the arctangent of all elements in v (
+ atan(v instant-vector): calculates the arctangent of all float samples in v (
special cases ).
- atanh(v instant-vector): calculates the inverse hyperbolic tangent of all elements in v (
- special cases ).
+ atanh(v instant-vector): calculates the inverse hyperbolic tangent of all float samples in{" "}
+ v (special cases ).
- cos(v instant-vector): calculates the cosine of all elements in v (
+ cos(v instant-vector): calculates the cosine of all float samples in v (
special cases ).
- cosh(v instant-vector): calculates the hyperbolic cosine of all elements in v (
+ cosh(v instant-vector): calculates the hyperbolic cosine of all float samples in v (
special cases ).
- sin(v instant-vector): calculates the sine of all elements in v (
+ sin(v instant-vector): calculates the sine of all float samples in v (
special cases ).
- sinh(v instant-vector): calculates the hyperbolic sine of all elements in v (
+ sinh(v instant-vector): calculates the hyperbolic sine of all float samples in v (
special cases ).
- tan(v instant-vector): calculates the tangent of all elements in v (
+ tan(v instant-vector): calculates the tangent of all float samples in v (
special cases ).
- tanh(v instant-vector): calculates the hyperbolic tangent of all elements in v (
- special cases ).
+ tanh(v instant-vector): calculates the hyperbolic tangent of all float samples in v{" "}
+ (special cases ).
@@ -911,13 +1013,13 @@ const funcDocs: Record = {
- deg(v instant-vector): converts radians to degrees for all elements in v.
+ deg(v instant-vector): converts radians to degrees for all float samples in v.
pi(): returns pi.
- rad(v instant-vector): converts degrees to radians for all elements in v.
+ rad(v instant-vector): converts degrees to radians for all float samples in v.
>
@@ -925,52 +1027,86 @@ const funcDocs: Record = {
delta: (
<>
- delta(v range-vector) calculates the difference between the first and last value of each time series
- element in a range vector v, returning an instant vector with the given deltas and equivalent labels.
- The delta is extrapolated to cover the full time range as specified in the range vector selector, so that it is
- possible to get a non-integer result even if the sample values are all integers.
+ delta(v range-vector) calculates the difference between the first and last value of each time
+ series element in a range vector v, returning an instant vector with the given deltas and
+ equivalent labels. The delta is extrapolated to cover the full time range as specified in the range vector
+ selector, so that it is possible to get a non-integer result even if the sample values are all integers.
The following example expression returns the difference in CPU temperature between now and 2 hours ago:
- delta(cpu_temp_celsius{'{'}host="zeus"{'}'}[2h])
+ delta(cpu_temp_celsius{"{"}host="zeus"{"}"}[2h])
- delta acts on native histograms by calculating a new histogram where each component (sum and count of
- observations, buckets) is the difference between the respective component in the first and last native histogram in
- v. However, each element in v that contains a mix of float and native histogram samples
- within the range, will be missing from the result vector.
+ delta acts on histogram samples by calculating a new histogram where each component (sum and count
+ of observations, buckets) is the difference between the respective component in the first and last native
+ histogram in v. However, each element in v that contains a mix of float samples and
+ histogram samples within the range will be omitted from the result vector, flagged by a warn-level annotation.
- delta should only be used with gauges and native histograms where the components behave like gauges
- (so-called gauge histograms).
+ delta should only be used with gauges (for both floats and histograms).
>
),
deriv: (
<>
- deriv(v range-vector) calculates the per-second derivative of the time series in a range vector{' '}
- v, using simple linear regression .
- The range vector must have at least two samples in order to perform the calculation. When +Inf or
+ deriv(v range-vector) calculates the per-second derivative of each float time series in the range
+ vector v, using{" "}
+ simple linear regression . The range vector
+ must have at least two float samples in order to perform the calculation. When +Inf or{" "}
-Inf are found in the range vector, the slope and offset value calculated will be NaN.
- deriv should only be used with gauges.
+ deriv should only be used with gauges and only works for float samples. Elements in the range
+ vector that contain only histogram samples are ignored entirely. For elements that contain a mix of float and
+ histogram samples, only the float samples are used as input, which is flagged by an info-level annotation.
+
+ >
+ ),
+ double_exponential_smoothing: (
+ <>
+
+
+ This function has to be enabled via the{" "}
+ feature flag
+ --enable-feature=promql-experimental-functions.
+
+
+
+
+ double_exponential_smoothing(v range-vector, sf scalar, tf scalar) produces a smoothed value for
+ each float time series in the range in v. The lower the smoothing factor sf, the more
+ importance is given to old data. The higher the trend factor tf, the more trends in the data is
+ considered. Both sf and
+ tf must be between 0 and 1. For additional details, refer to{" "}
+
+ NIST Engineering Statistics Handbook
+
+ . In Prometheus V2 this function was called holt_winters. This caused confusion since the
+ Holt-Winters method usually refers to triple exponential smoothing. Double exponential smoothing as implemented
+ here is also referred to as “Holt Linear”.
+
+
+
+ double_exponential_smoothing should only be used with gauges and only works for float samples.
+ Elements in the range vector that contain only histogram samples are ignored entirely. For elements that contain
+ a mix of float and histogram samples, only the float samples are used as input, which is flagged by an
+ info-level annotation.
>
),
exp: (
<>
- exp(v instant-vector) calculates the exponential function for all elements in v. Special
- cases are:
+ exp(v instant-vector) calculates the exponential function for all float samples in v.
+ Histogram samples are ignored silently. Special cases are:
@@ -983,11 +1119,122 @@ const funcDocs: Record = {
>
),
+ first_over_time: (
+ <>
+
+ The following functions allow aggregating each series of a given range vector over time and return an instant
+ vector with per-series aggregation results:
+
+
+
+
+ avg_over_time(range-vector): the average value of all float or histogram samples in the specified
+ interval (see details below).
+
+
+ min_over_time(range-vector): the minimum value of all float samples in the specified interval.
+
+
+ max_over_time(range-vector): the maximum value of all float samples in the specified interval.
+
+
+ sum_over_time(range-vector): the sum of all float or histogram samples in the specified interval
+ (see details below).
+
+
+ count_over_time(range-vector): the count of all samples in the specified interval.
+
+
+ quantile_over_time(scalar, range-vector): the φ-quantile (0 ≤ φ ≤ 1) of all float samples in the
+ specified interval.
+
+
+ stddev_over_time(range-vector): the population standard deviation of all float samples in the
+ specified interval.
+
+
+ stdvar_over_time(range-vector): the population standard variance of all float samples in the
+ specified interval.
+
+
+ last_over_time(range-vector): the most recent sample in the specified interval.
+
+
+ present_over_time(range-vector): the value 1 for any series in the specified interval.
+
+
+
+
+ If the feature flag
+ --enable-feature=promql-experimental-functions is set, the following additional functions are
+ available:
+
+
+
+
+ mad_over_time(range-vector): the median absolute deviation of all float samples in the specified
+ interval.
+
+
+ ts_of_min_over_time(range-vector): the timestamp of the last float sample that has the minimum
+ value of all float samples in the specified interval.
+
+
+ ts_of_max_over_time(range-vector): the timestamp of the last float sample that has the maximum
+ value of all float samples in the specified interval.
+
+
+ ts_of_last_over_time(range-vector): the timestamp of last sample in the specified interval.
+
+
+ first_over_time(range-vector): the oldest sample in the specified interval.
+
+
+ ts_of_first_over_time(range-vector): the timestamp of earliest sample in the specified interval.
+
+
+
+
+ Note that all values in the specified interval have the same weight in the aggregation even if the values are
+ not equally spaced throughout the interval.
+
+
+ These functions act on histograms in the following way:
+
+
+
+ count_over_time, first_over_time, last_over_time, and
+ present_over_time() act on float and histogram samples in the same way.
+
+
+ avg_over_time() and sum_over_time() act on histogram samples in a way that
+ corresponds to the respective aggregation operators. If a series contains a mix of float samples and histogram
+ samples within the range, the corresponding result is removed entirely from the output vector. Such a removal
+ is flagged by a warn-level annotation.
+
+
+ All other functions ignore histogram samples in the following way: Input ranges containing only histogram
+ samples are silently removed from the output. For ranges with a mix of histogram and float samples, only the
+ float samples are processed and the omission of the histogram samples is flagged by an info-level annotation.
+
+
+
+
+ first_over_time(m[1m]) differs from m offset 1m in that the former will select the
+ first sample of m within the 1m range, where m offset 1m will select the most
+ recent sample within the lookback interval outside and prior to the 1m offset. This is particularly
+ useful with first_over_time(m[step()])
+ in range queries (available when --enable-feature=promql-duration-expr is set) to ensure that the
+ sample selected is within the range step.
+
+ >
+ ),
floor: (
<>
- floor(v instant-vector) rounds the sample values of all elements in v down to the nearest
- integer value smaller than or equal to v.
+ floor(v instant-vector) returns a vector containing all float samples in the input vector rounded
+ down to the nearest integer value smaller than or equal to their original value. Histogram samples in the input
+ vector are ignored silently.
@@ -1009,19 +1256,13 @@ const funcDocs: Record = {
histogram_avg: (
<>
-
- This function only acts on native histograms.
-
+ histogram_avg(v instant-vector) returns the arithmetic average of observed values stored in each
+ histogram sample in v. Float samples are ignored and do not show up in the returned vector.
- histogram_avg(v instant-vector) returns the arithmetic average of observed values stored in a native
- histogram. Samples that are not native histograms are ignored and do not show up in the returned vector.
-
-
-
- Use histogram_avg as demonstrated below to compute the average request duration over a 5-minute window
- from a native histogram:
+ Use histogram_avg as demonstrated below to compute the average request duration over a 5-minute
+ window from a native histogram:
@@ -1032,32 +1273,28 @@ const funcDocs: Record = {
- {' '}
- histogram_sum(rate(http_request_duration_seconds[5m])) / histogram_count(rate(http_request_duration_seconds[5m]))
+ {" "}
+ histogram_sum(rate(http_request_duration_seconds[5m])) /
+ histogram_count(rate(http_request_duration_seconds[5m]))
>
),
- 'histogram_count()` and `histogram_sum': (
+ histogram_count: (
<>
-
- Both functions only act on native histograms.
-
+ histogram_count(v instant-vector) returns the count of observations stored in each histogram sample
+ in v. Float samples are ignored and do not show up in the returned vector.
- histogram_count(v instant-vector) returns the count of observations stored in a native histogram.
- Samples that are not native histograms are ignored and do not show up in the returned vector.
+ Similarly, histogram_sum(v instant-vector) returns the sum of observations stored in each histogram
+ sample.
- Similarly, histogram_sum(v instant-vector) returns the sum of observations stored in a native histogram.
-
-
-
- Use histogram_count in the following way to calculate a rate of observations (in this case corresponding
- to “requests per second”) from a native histogram:
+ Use histogram_count in the following way to calculate a rate of observations (in this case
+ corresponding to “requests per second”) from a series of histogram samples:
@@ -1068,20 +1305,28 @@ const funcDocs: Record = {
histogram_fraction: (
<>
-
- This function only acts on native histograms.
-
+ histogram_fraction(lower scalar, upper scalar, b instant-vector) returns the estimated fraction of
+ observations between the provided lower and upper values for each classic or native histogram contained in{" "}
+ b. Float samples in b are considered the counts of observations in each bucket of one
+ or more classic histograms, while native histogram samples in b are treated each individually as a
+ separate histogram. This works in the same way as for histogram_quantile(). (See there for more
+ details.)
- For a native histogram, histogram_fraction(lower scalar, upper scalar, v instant-vector) returns the
- estimated fraction of observations between the provided lower and upper values. Samples that are not native
- histograms are ignored and do not show up in the returned vector.
+ If the provided lower and upper values do not coincide with bucket boundaries, the calculated fraction is an
+ estimate, using the same interpolation method as for
+ histogram_quantile(). (See there for more details.) Especially with classic histograms, it is easy
+ to accidentally pick lower or upper values that are very far away from any bucket boundary, leading to large
+ margins of error. Rather than using histogram_fraction() with classic histograms, it is often a
+ more robust approach to directly act on the bucket series when calculating fractions. See the
+ calculation of the Apdex score
+ as a typical example.
- For example, the following expression calculates the fraction of HTTP requests over the last hour that took 200ms or
- less:
+ For example, the following expression calculates the fraction of HTTP requests over the last hour that took
+ 200ms or less:
@@ -1089,48 +1334,56 @@ const funcDocs: Record = {
- The error of the estimation depends on the resolution of the underlying native histogram and how closely the provided
- boundaries are aligned with the bucket boundaries in the histogram.
+ The error of the estimation depends on the resolution of the underlying native histogram and how closely the
+ provided boundaries are aligned with the bucket boundaries in the histogram.
- +Inf and -Inf are valid boundary values. For example, if the histogram in the expression
- above included negative observations (which shouldn’t be the case for request durations), the appropriate lower
- boundary to include all observations less than or equal 0.2 would be -Inf rather than 0.
+ +Inf and -Inf are valid boundary values. For example, if the histogram in the
+ expression above included negative observations (which shouldn’t be the case for request durations), the
+ appropriate lower boundary to include all observations less than or equal 0.2 would be -Inf rather
+ than 0.
- Whether the provided boundaries are inclusive or exclusive is only relevant if the provided boundaries are precisely
- aligned with bucket boundaries in the underlying native histogram. In this case, the behavior depends on the schema
- definition of the histogram. The currently supported schemas all feature inclusive upper boundaries and exclusive
- lower boundaries for positive values (and vice versa for negative values). Without a precise alignment of boundaries,
- the function uses linear interpolation to estimate the fraction. With the resulting uncertainty, it becomes
- irrelevant if the boundaries are inclusive or exclusive.
+ Whether the provided boundaries are inclusive or exclusive is only relevant if the provided boundaries are
+ precisely aligned with bucket boundaries in the underlying native histogram. In this case, the behavior depends
+ on the schema definition of the histogram. (The usual standard exponential schemas all feature inclusive upper
+ boundaries and exclusive lower boundaries for positive values, and vice versa for negative values.) Without a
+ precise alignment of boundaries, the function uses interpolation to estimate the fraction. With the resulting
+ uncertainty, it becomes irrelevant if the boundaries are inclusive or exclusive.
+
+
+
+ Special case for native histograms with standard exponential buckets:
+ NaN observations are considered outside of any buckets in this case.
+ histogram_fraction(-Inf, +Inf, b) effectively returns the fraction of non-NaN{" "}
+ observations and may therefore be less than 1.
>
),
histogram_quantile: (
<>
- histogram_quantile(φ scalar, b instant-vector) calculates the φ-quantile (0 ≤ φ ≤ 1) from a{' '}
+ histogram_quantile(φ scalar, b instant-vector) calculates the φ-quantile (0 ≤ φ ≤ 1) from a{" "}
classic histogram or from a native
- histogram. (See histograms and summaries for a detailed
- explanation of φ-quantiles and the usage of the (classic) histogram metric type in general.)
+ histogram. (See histograms and summaries for a
+ detailed explanation of φ-quantiles and the usage of the (classic) histogram metric type in general.)
- The float samples in b are considered the counts of observations in each bucket of one or more classic
- histograms. Each float sample must have a label
- le where the label value denotes the inclusive upper bound of the bucket. (Float samples without such a
- label are silently ignored.) The other labels and the metric name are used to identify the buckets belonging to each
- classic histogram. The{' '}
+ The float samples in b are considered the counts of observations in each bucket of one or more
+ classic histograms. Each float sample must have a label
+ le where the label value denotes the inclusive upper bound of the bucket. (Float samples without
+ such a label are silently ignored.) The other labels and the metric name are used to identify the buckets
+ belonging to each classic histogram. The{" "}
histogram metric type
automatically provides time series with the _bucket suffix and the appropriate labels.
- The native histogram samples in b are treated each individually as a separate histogram to calculate the
- quantile from.
+ The (native) histogram samples in b are treated each individually as a separate histogram to
+ calculate the quantile from.
@@ -1142,10 +1395,10 @@ const funcDocs: Record = {
- Example: A histogram metric is called http_request_duration_seconds (and therefore the metric name for
- the buckets of a classic histogram is
- http_request_duration_seconds_bucket). To calculate the 90th percentile of request durations over the
- last 10m, use the following expression in case
+ Example: A histogram metric is called http_request_duration_seconds (and therefore the metric name
+ for the buckets of a classic histogram is
+ http_request_duration_seconds_bucket). To calculate the 90th percentile of request durations over
+ the last 10m, use the following expression in case
http_request_duration_seconds is a classic histogram:
@@ -1161,9 +1414,9 @@ const funcDocs: Record = {
The quantile is calculated for each label combination in
- http_request_duration_seconds. To aggregate, use the sum() aggregator around the{' '}
+ http_request_duration_seconds. To aggregate, use the sum() aggregator around the{" "}
rate() function. Since the le label is required by
- histogram_quantile() to deal with classic histograms, it has to be included in the by{' '}
+ histogram_quantile() to deal with classic histograms, it has to be included in the by{" "}
clause. The following expression aggregates the 90th percentile by job for classic histograms:
@@ -1194,23 +1447,67 @@ const funcDocs: Record = {
- The histogram_quantile() function interpolates quantile values by assuming a linear distribution within
- a bucket.
+ In the (common) case that a quantile value does not coincide with a bucket boundary, the{" "}
+ histogram_quantile() function interpolates the quantile value within the bucket the quantile value
+ falls into. For classic histograms, for native histograms with custom bucket boundaries, and for the zero bucket
+ of other native histograms, it assumes a uniform distribution of observations within the bucket (also called{" "}
+ linear interpolation ). For the non-zero-buckets of native histograms with a standard exponential
+ bucketing schema, the interpolation is done under the assumption that the samples within the bucket are
+ distributed in a way that they would uniformly populate the buckets in a hypothetical histogram with higher
+ resolution. (This is also called exponential interpolation . See the{" "}
+
+ native histogram specification
+
+ for more details.)
- If b has 0 observations, NaN is returned. For φ < 0, -Inf is returned. For
- φ > 1, +Inf is returned. For φ = NaN, NaN is returned.
+ If b has 0 observations, NaN is returned. For φ < 0, -Inf is returned.
+ For φ > 1, +Inf is returned. For φ = NaN, NaN is returned.
-
- The following is only relevant for classic histograms: If b contains fewer than two buckets,{' '}
- NaN is returned. The highest bucket must have an upper bound of +Inf. (Otherwise,{' '}
- NaN is returned.) If a quantile is located in the highest bucket, the upper bound of the second highest
- bucket is returned. A lower limit of the lowest bucket is assumed to be 0 if the upper bound of that bucket is
- greater than 0. In that case, the usual linear interpolation is applied within that bucket. Otherwise, the upper
- bound of the lowest bucket is returned for quantiles located in the lowest bucket.
-
+ Special cases for classic histograms:
+
+
+
+ If b contains fewer than two buckets, NaN is returned.
+
+
+ The highest bucket must have an upper bound of +Inf. (Otherwise, NaN is returned.)
+
+
+ If a quantile is located in the highest bucket, the upper bound of the second highest bucket is returned.
+
+
+ The lower limit of the lowest bucket is assumed to be 0 if the upper bound of that bucket is greater than 0.
+ In that case, the usual linear interpolation is applied within that bucket. Otherwise, the upper bound of the
+ lowest bucket is returned for quantiles located in the lowest bucket.
+
+
+
+ Special cases for native histograms:
+
+
+
+ If a native histogram with standard exponential buckets has NaN
+ observations and the quantile falls into one of the existing exponential buckets, the result is skewed towards
+ higher values due to NaN
+ observations treated as +Inf. This is flagged with an info level annotation.
+
+
+ If a native histogram with standard exponential buckets has NaN
+ observations and the quantile falls above all of the existing exponential buckets, NaN is
+ returned. This is flagged with an info level annotation.
+
+
+ A zero bucket with finite width is assumed to contain no negative observations if the histogram has
+ observations in positive buckets, but none in negative buckets.
+
+
+ A zero bucket with finite width is assumed to contain no positive observations if the histogram has
+ observations in negative buckets, but none in positive buckets.
+
+
You can use histogram_quantile(0, v instant-vector) to get the estimated minimum value stored in a
@@ -1227,78 +1524,127 @@ const funcDocs: Record = {
The counts in the buckets are monotonically increasing (strictly non-decreasing).
- A lack of observations between the upper limits of two consecutive buckets results in equal counts in those two
- buckets.
+ A lack of observations between the upper limits of two consecutive buckets results in equal counts in those
+ two buckets.
- However, floating point precision issues (e.g. small discrepancies introduced by computing of buckets with{' '}
- sum(rate(...))) or invalid data might violate these assumptions. In that case,
- histogram_quantile would be unable to return meaningful results. To mitigate the issue,
+ However, floating point precision issues (e.g. small discrepancies introduced by computing of buckets with{" "}
+ sum(rate(...))) or invalid data might violate these assumptions. In that case,{" "}
+ histogram_quantile would be unable to return meaningful results. To mitigate the issue,{" "}
histogram_quantile assumes that tiny relative differences between consecutive buckets are happening
because of floating point precision errors and ignores them. (The threshold to ignore a difference between two
- buckets is a trillionth (1e-12) of the sum of both buckets.) Furthermore, if there are non-monotonic bucket counts
- even after this adjustment, they are increased to the value of the previous buckets to enforce monotonicity. The
- latter is evidence for an actual issue with the input data and is therefore flagged with an informational annotation
- reading input to histogram_quantile needed to be fixed for monotonicity. If you encounter this
- annotation, you should find and remove the source of the invalid data.
+ buckets is a trillionth (1e-12) of the sum of both buckets.) Furthermore, if there are non-monotonic bucket
+ counts even after this adjustment, they are increased to the value of the previous buckets to enforce
+ monotonicity. The latter is evidence for an actual issue with the input data and is therefore flagged by an
+ info-level annotation reading input to histogram_quantile needed to be fixed for monotonicity. If
+ you encounter this annotation, you should find and remove the source of the invalid data.
>
),
- 'histogram_stddev()` and `histogram_stdvar': (
+ histogram_quantiles: (
<>
-
- Both functions only act on native histograms.
-
+
+ This function has to be enabled via the{" "}
+ feature flag
+ --enable-feature=promql-experimental-functions.
+
-
- histogram_stddev(v instant-vector) returns the estimated standard deviation of observations in a native
- histogram. For this estimation, all observations in a bucket are assumed to have the value of the mean of the bucket boundaries.
- For the zero bucket and for buckets with custom boundaries, the arithmetic mean is used. For the usual exponential buckets,
- the geometric mean is used. Samples that are not native histograms are ignored and do not show up in the returned vector.
+ histogram_quantiles(v instant-vector, quantile_label string, φ_1 scalar, φ_2 scalar, ...){" "}
+ calculates multiple (between 1 and 10) φ-quantiles (0 ≤ φ ≤ 1) from a{" "}
+ classic histogram or from a native
+ histogram. Quantile calculation works the same way as in histogram_quantile(). The second argument
+ (a string) specifies the label name that is used to identify different quantiles in the query result.
+
+
+
+
+ histogram_quantiles(sum(rate(foo[1m])), "quantile", 0.9, 0.99) # => {"{"}quantile="0.9"
+ {"}"} 123
+ {"{"}quantile="0.99"{"}"} 128
+
+
+ >
+ ),
+ histogram_stddev: (
+ <>
+
+ histogram_stddev(v instant-vector) returns the estimated standard deviation of observations for
+ each histogram sample in v. For this estimation, all observations in a bucket are assumed to have
+ the value of the mean of the bucket boundaries. For the zero bucket and for buckets with custom boundaries, the
+ arithmetic mean is used. For the usual exponential buckets, the geometric mean is used. Float samples are
+ ignored and do not show up in the returned vector.
- Similarly, histogram_stdvar(v instant-vector) returns the estimated standard variance of observations in
- a native histogram.
+ Similarly, histogram_stdvar(v instant-vector) returns the estimated standard variance of
+ observations for each histogram sample in v.
>
),
- double_exponential_smoothing: (
+ histogram_stdvar: (
<>
- double_exponential_smoothing(v range-vector, sf scalar, tf scalar) produces a smoothed value for time series based on
- the range in v. The lower the smoothing factor sf, the more importance is given to old
- data. The higher the trend factor tf, the more trends in the data is considered. Both sf{' '}
- and tf must be between 0 and 1.
+ histogram_stddev(v instant-vector) returns the estimated standard deviation of observations for
+ each histogram sample in v. For this estimation, all observations in a bucket are assumed to have
+ the value of the mean of the bucket boundaries. For the zero bucket and for buckets with custom boundaries, the
+ arithmetic mean is used. For the usual exponential buckets, the geometric mean is used. Float samples are
+ ignored and do not show up in the returned vector.
- double_exponential_smoothing should only be used with gauges.
+ Similarly, histogram_stdvar(v instant-vector) returns the estimated standard variance of
+ observations for each histogram sample in v.
>
),
+ histogram_sum: (
+ <>
+
+ histogram_count(v instant-vector) returns the count of observations stored in each histogram sample
+ in v. Float samples are ignored and do not show up in the returned vector.
+
+
+
+ Similarly, histogram_sum(v instant-vector) returns the sum of observations stored in each histogram
+ sample.
+
+
+
+ Use histogram_count in the following way to calculate a rate of observations (in this case
+ corresponding to “requests per second”) from a series of histogram samples:
+
+
+
+ histogram_count(rate(http_request_duration_seconds[10m]))
+
+ >
+ ),
hour: (
<>
- hour(v=vector(time()) instant-vector) returns the hour of the day for each of the given times in UTC.
- Returned values are from 0 to 23.
+ hour(v=vector(time()) instant-vector) interprets float samples in v as timestamps
+ (number of seconds since January 1, 1970 UTC) and returns the hour of the day (in UTC) for each of those
+ timestamps. Returned values are from 0 to 23. Histogram samples in the input vector are ignored silently.
>
),
idelta: (
<>
- idelta(v range-vector) calculates the difference between the last two samples in the range vector{' '}
- v, returning an instant vector with the given deltas and equivalent labels.
+ idelta(v range-vector) calculates the difference between the last two samples in the range vector{" "}
+ v, returning an instant vector with the given deltas and equivalent labels. Both samples must be
+ either float samples or histogram samples. Elements in v where one of the last two samples is a
+ float sample and the other is a histogram sample will be omitted from the result vector, flagged by a warn-level
+ annotation.
- idelta should only be used with gauges.
+ idelta should only be used with gauges (for both floats and histograms).
>
),
@@ -1307,79 +1653,214 @@ const funcDocs: Record = {
increase(v range-vector) calculates the increase in the time series in the range vector. Breaks in
monotonicity (such as counter resets due to target restarts) are automatically adjusted for. The increase is
- extrapolated to cover the full time range as specified in the range vector selector, so that it is possible to get a
- non-integer result even if a counter increases only by integer increments.
+ extrapolated to cover the full time range as specified in the range vector selector, so that it is possible to
+ get a non-integer result even if a counter increases only by integer increments.
- The following example expression returns the number of HTTP requests as measured over the last 5 minutes, per time
- series in the range vector:
+ The following example expression returns the number of HTTP requests as measured over the last 5 minutes, per
+ time series in the range vector:
- increase(http_requests_total{'{'}job="api-server"{'}'}[5m])
+ increase(http_requests_total{"{"}job="api-server"{"}"}[5m])
- increase acts on native histograms by calculating a new histogram where each component (sum and count of
- observations, buckets) is the increase between the respective component in the first and last native histogram in
- v. However, each element in v that contains a mix of float and native histogram samples
- within the range, will be missing from the result vector.
+ increase acts on histogram samples by calculating a new histogram where each component (sum and
+ count of observations, buckets) is the increase between the respective component in the first and last native
+ histogram in v. However, each element in v that contains a mix of float samples and
+ histogram samples within the range, will be omitted from the result vector, flagged by a warn-level annotation.
- increase should only be used with counters and native histograms where the components behave like
- counters. It is syntactic sugar for rate(v) multiplied by the number of seconds under the specified time
- range window, and should be used primarily for human readability. Use rate in recording rules so that
- increases are tracked consistently on a per-second basis.
+ increase should only be used with counters (for both floats and histograms). It is syntactic sugar
+ for rate(v) multiplied by the number of seconds under the specified time range window, and should
+ be used primarily for human readability. Use rate in recording rules so that increases are tracked
+ consistently on a per-second basis.
+
+ >
+ ),
+ info: (
+ <>
+
+ _The info function is an experiment to improve UX around including labels from{" "}
+
+ info metrics
+
+ . The behavior of this function may change in future versions of Prometheus, including its removal from PromQL.{" "}
+ info has to be enabled via the
+ feature flag {" "}
+ --enable-feature=promql-experimental-functions._
+
+
+
+ info(v instant-vector, [data-label-selector instant-vector]) finds, for each time series in{" "}
+ v, all info series with matching identifying labels (more on this later), and adds the
+ union of their data (i.e., non-identifying) labels to the time series. The second argument{" "}
+ data-label-selector is optional. It is not a real instant vector, but uses a subset of its syntax.
+ It must start and end with curly braces (
+
+ {"{"} ... {"}"}
+
+ ) and may only contain label matchers. The label matchers are used to constrain which info series to consider
+ and which data labels to add to v.
+
+
+
+ Identifying labels of an info series are the subset of labels that uniquely identify the info series. The
+ remaining labels are considered
+ data labels (also called non-identifying). (Note that Prometheus’s concept of time series
+ identity always includes all the labels. For the sake of the info
+ function, we “logically” define info series identity in a different way than in the conventional Prometheus
+ view.) The identifying labels of an info series are used to join it to regular (non-info) series, i.e. those
+ series that have the same labels as the identifying labels of the info series. The data labels, which are the
+ ones added to the regular series by the info function, effectively encode metadata key value pairs.
+ (This implies that a change in the data labels in the conventional Prometheus view constitutes the end of one
+ info series and the beginning of a new info series, while the “logical” view of the info function
+ is that the same info series continues to exist, just with different “data”.)
+
+
+
+ The conventional approach of adding data labels is sometimes called a “join query”, as illustrated by the
+ following example:
+
+
+
+
+ {" "}
+ rate(http_server_request_duration_seconds_count[2m]) * on (job, instance) group_left (k8s_cluster_name)
+ target_info
+
+
+
+
+ The core of the query is the expression rate(http_server_request_duration_seconds_count[2m]). But
+ to add data labels from an info metric, the user has to use elaborate (and not very obvious) syntax to specify
+ which info metric to use (target_info), what the identifying labels are (
+ on (job, instance)), and which data labels to add (group_left (k8s_cluster_name)).
+
+
+
+ This query is not only verbose and hard to write, it might also run into an “identity crisis”: If any of the
+ data labels of target_info changes, Prometheus sees that as a change of series (as alluded to
+ above, Prometheus just has no native concept of non-identifying labels). If the old target_info{" "}
+ series is not properly marked as stale (which can happen with certain ingestion paths), the query above will
+ fail for up to 5m (the lookback delta) because it will find a conflicting match with both the old and the new
+ version of target_info.
+
+
+
+ The info function not only resolves this conflict in favor of the newer series, it also simplifies
+ the syntax because it knows about the available info series and what their identifying labels are. The example
+ query looks like this with the info function:
+
+
+
+
+ info( rate(http_server_request_duration_seconds_count[2m]),
+ {"{"}k8s_cluster_name=~".+"{"}"})
+
+
+
+
+ The common case of adding all data labels can be achieved by omitting the 2nd argument of the{" "}
+ info function entirely, simplifying the example even more:
+
+
+
+ info(rate(http_server_request_duration_seconds_count[2m]))
+
+
+
+ While info normally automatically finds all matching info series, it’s possible to restrict
+ them by providing a __name__ label matcher, e.g.
+
+ {"{"}__name__="target_info"{"}"}
+
+ .
+
+
+
+ Note that if there are any time series in v that match the data-label-selector (or the
+ default target_info if that argument is not specified), they will be treated as info series and
+ will be returned unchanged.
+
+
+ Limitations
+
+
+ In its current iteration, info defaults to considering only info series with the name{" "}
+ target_info. It also assumes that the identifying info series labels are
+ instance and job. info does support other info series names however,
+ through
+ __name__ label matchers. E.g., one can explicitly say to consider both
+ target_info and build_info as follows:
+
+ {"{"}__name__=~"(target|build)_info"{"}"}
+
+ . However, the identifying labels always have to be instance and job.
+
+
+
+ These limitations are partially defeating the purpose of the info function. At the current stage,
+ this is an experiment to find out how useful the approach turns out to be in practice. A final version of the{" "}
+ info function will indeed consider all matching info series and with their appropriate identifying
+ labels.
>
),
irate: (
<>
- irate(v range-vector) calculates the per-second instant rate of increase of the time series in the range
- vector. This is based on the last two data points. Breaks in monotonicity (such as counter resets due to target
- restarts) are automatically adjusted for.
+ irate(v range-vector) calculates the per-second instant rate of increase of the time series in the
+ range vector. This is based on the last two data points. Breaks in monotonicity (such as counter resets due to
+ target restarts) are automatically adjusted for. Both samples must be either float samples or histogram samples.
+ Elements in v where one of the last two samples is a float sample and the other is a histogram
+ sample will be omitted from the result vector, flagged by a warn-level annotation.
- The following example expression returns the per-second rate of HTTP requests looking up to 5 minutes back for the
- two most recent data points, per time series in the range vector:
+ irate should only be used with counters (for both floats and histograms).
+
+
+
+ The following example expression returns the per-second rate of HTTP requests looking up to 5 minutes back for
+ the two most recent data points, per time series in the range vector:
- irate(http_requests_total{'{'}job="api-server"{'}'}[5m])
+ irate(http_requests_total{"{"}job="api-server"{"}"}[5m])
- irate should only be used when graphing volatile, fast-moving counters. Use rate for alerts
- and slow-moving counters, as brief changes in the rate can reset the FOR clause and graphs consisting
- entirely of rare spikes are hard to read.
+ irate should only be used when graphing volatile, fast-moving counters. Use rate for
+ alerts and slow-moving counters, as brief changes in the rate can reset the FOR clause and graphs
+ consisting entirely of rare spikes are hard to read.
Note that when combining irate() with an
aggregation operator (e.g. sum()) or a function
- aggregating over time (any function ending in _over_time), always take a irate() first,
- then aggregate. Otherwise irate() cannot detect counter resets when your target restarts.
+ aggregating over time (any function ending in _over_time), always take an irate(){" "}
+ first, then aggregate. Otherwise irate() cannot detect counter resets when your target restarts.
>
),
label_join: (
<>
- For each timeseries in v,{' '}
+ For each timeseries in v,{" "}
label_join(v instant-vector, dst_label string, separator string, src_label_1 string, src_label_2 string, ...)
- {' '}
+
{" "}
joins all the values of all the src_labels
- using separator and returns the timeseries with the label dst_label containing the joined
- value. There can be any number of src_labels in this function.
+ using separator and returns the timeseries with the label dst_label containing the
+ joined value. There can be any number of src_labels in this function.
@@ -1387,13 +1868,13 @@ const funcDocs: Record = {
- This example will return a vector with each time series having a foo label with the value{' '}
+ This example will return a vector with each time series having a foo label with the value{" "}
a,b,c added to it:
- label_join(up{'{'}job="api-server",src1="a",src2="b",src3="c"{'}'},
+ label_join(up{"{"}job="api-server",src1="a",src2="b",src3="c"{"}"},
"foo", ",", "src1", "src2", "src3")
@@ -1402,14 +1883,17 @@ const funcDocs: Record = {
label_replace: (
<>
- For each timeseries in v,{' '}
- label_replace(v instant-vector, dst_label string, replacement string, src_label string, regex string)
- matches the regular expression regex against the
+ For each timeseries in v,{" "}
+
+ label_replace(v instant-vector, dst_label string, replacement string, src_label string, regex string)
+
+ matches the regular expression regex against the
value of the label src_label. If it matches, the value of the label dst_label in the
returned timeseries will be the expansion of replacement, together with the original labels in the
- input. Capturing groups in the regular expression can be referenced with $1, $2, etc. Named
- capturing groups in the regular expression can be referenced with $name (where name is the
- capturing group name). If the regular expression doesn’t match then the timeseries is returned unchanged.
+ input. Capturing groups in the regular expression can be referenced with $1, $2, etc.
+ Named capturing groups in the regular expression can be referenced with $name (where{" "}
+ name is the capturing group name). If the regular expression doesn’t match then the
+ timeseries is returned unchanged.
@@ -1417,23 +1901,25 @@ const funcDocs: Record = {
- This example will return timeseries with the values a:c at label service and a{' '}
- at label foo:
+ This example will return timeseries with the values a:c at label service and{" "}
+ a at label foo:
- label_replace(up{'{'}job="api-server",service="a:c"{'}'}, "foo", "$1",
+ label_replace(up{"{"}job="api-server",service="a:c"{"}"}, "foo", "$1",
"service", "(.*):.*")
- This second example has the same effect than the first example, and illustrates use of named capturing groups:
+
+ This second example has the same effect than the first example, and illustrates use of named capturing groups:
+
- label_replace(up{'{'}job="api-server",service="a:c"{'}'}, "foo", "$name",
- "service", "(?P<name>.*):(?P<version>.*)")
+ label_replace(up{"{"}job="api-server",service="a:c"{"}"}, "foo",
+ "$name", "service", "(?P<name>.*):(?P<version>.*)")
>
@@ -1441,40 +1927,42 @@ const funcDocs: Record = {
last_over_time: (
<>
- The following functions allow aggregating each series of a given range vector over time and return an instant vector
- with per-series aggregation results:
+ The following functions allow aggregating each series of a given range vector over time and return an instant
+ vector with per-series aggregation results:
- avg_over_time(range-vector): the average value of all points in the specified interval.
+ avg_over_time(range-vector): the average value of all float or histogram samples in the specified
+ interval (see details below).
- min_over_time(range-vector): the minimum value of all points in the specified interval.
+ min_over_time(range-vector): the minimum value of all float samples in the specified interval.
- max_over_time(range-vector): the maximum value of all points in the specified interval.
+ max_over_time(range-vector): the maximum value of all float samples in the specified interval.
- sum_over_time(range-vector): the sum of all values in the specified interval.
+ sum_over_time(range-vector): the sum of all float or histogram samples in the specified interval
+ (see details below).
- count_over_time(range-vector): the count of all values in the specified interval.
+ count_over_time(range-vector): the count of all samples in the specified interval.
- quantile_over_time(scalar, range-vector): the φ-quantile (0 ≤ φ ≤ 1) of the values in the specified
- interval.
+ quantile_over_time(scalar, range-vector): the φ-quantile (0 ≤ φ ≤ 1) of all float samples in the
+ specified interval.
- stddev_over_time(range-vector): the population standard deviation of the values in the specified
- interval.
+ stddev_over_time(range-vector): the population standard deviation of all float samples in the
+ specified interval.
- stdvar_over_time(range-vector): the population standard variance of the values in the specified
- interval.
+ stdvar_over_time(range-vector): the population standard variance of all float samples in the
+ specified interval.
- last_over_time(range-vector): the most recent point value in the specified interval.
+ last_over_time(range-vector): the most recent sample in the specified interval.
present_over_time(range-vector): the value 1 for any series in the specified interval.
@@ -1483,32 +1971,74 @@ const funcDocs: Record = {
If the feature flag
- --enable-feature=promql-experimental-functions is set, the following additional functions are available:
+ --enable-feature=promql-experimental-functions is set, the following additional functions are
+ available:
- mad_over_time(range-vector): the median absolute deviation of all points in the specified interval.
+ mad_over_time(range-vector): the median absolute deviation of all float samples in the specified
+ interval.
+
+
+ ts_of_min_over_time(range-vector): the timestamp of the last float sample that has the minimum
+ value of all float samples in the specified interval.
+
+
+ ts_of_max_over_time(range-vector): the timestamp of the last float sample that has the maximum
+ value of all float samples in the specified interval.
+
+
+ ts_of_last_over_time(range-vector): the timestamp of last sample in the specified interval.
+
+
+ first_over_time(range-vector): the oldest sample in the specified interval.
+
+
+ ts_of_first_over_time(range-vector): the timestamp of earliest sample in the specified interval.
- Note that all values in the specified interval have the same weight in the aggregation even if the values are not
- equally spaced throughout the interval.
+ Note that all values in the specified interval have the same weight in the aggregation even if the values are
+ not equally spaced throughout the interval.
+ These functions act on histograms in the following way:
+
+
+
+ count_over_time, first_over_time, last_over_time, and
+ present_over_time() act on float and histogram samples in the same way.
+
+
+ avg_over_time() and sum_over_time() act on histogram samples in a way that
+ corresponds to the respective aggregation operators. If a series contains a mix of float samples and histogram
+ samples within the range, the corresponding result is removed entirely from the output vector. Such a removal
+ is flagged by a warn-level annotation.
+
+
+ All other functions ignore histogram samples in the following way: Input ranges containing only histogram
+ samples are silently removed from the output. For ranges with a mix of histogram and float samples, only the
+ float samples are processed and the omission of the histogram samples is flagged by an info-level annotation.
+
+
+
- avg_over_time, sum_over_time, count_over_time, last_over_time,
- and
- present_over_time handle native histograms as expected. All other functions ignore histogram samples.
+ first_over_time(m[1m]) differs from m offset 1m in that the former will select the
+ first sample of m within the 1m range, where m offset 1m will select the most
+ recent sample within the lookback interval outside and prior to the 1m offset. This is particularly
+ useful with first_over_time(m[step()])
+ in range queries (available when --enable-feature=promql-duration-expr is set) to ensure that the
+ sample selected is within the range step.
>
),
ln: (
<>
- ln(v instant-vector) calculates the natural logarithm for all elements in v. Special cases
- are:
+ ln(v instant-vector) calculates the natural logarithm for all float samples in v.
+ Histogram samples in the input vector are ignored silently. Special cases are:
@@ -1530,56 +2060,60 @@ const funcDocs: Record = {
log10: (
<>
- log10(v instant-vector) calculates the decimal logarithm for all elements in v. The special
- cases are equivalent to those in ln.
+ log10(v instant-vector) calculates the decimal logarithm for all float samples in v.
+ Histogram samples in the input vector are ignored silently. The special cases are equivalent to those in{" "}
+ ln.
>
),
log2: (
<>
- log2(v instant-vector) calculates the binary logarithm for all elements in v. The special
- cases are equivalent to those in ln.
+ log2(v instant-vector) calculates the binary logarithm for all float samples in v.
+ Histogram samples in the input vector are ignored silently. The special cases are equivalent to those in{" "}
+ ln.
>
),
mad_over_time: (
<>
- The following functions allow aggregating each series of a given range vector over time and return an instant vector
- with per-series aggregation results:
+ The following functions allow aggregating each series of a given range vector over time and return an instant
+ vector with per-series aggregation results:
- avg_over_time(range-vector): the average value of all points in the specified interval.
+ avg_over_time(range-vector): the average value of all float or histogram samples in the specified
+ interval (see details below).
- min_over_time(range-vector): the minimum value of all points in the specified interval.
+ min_over_time(range-vector): the minimum value of all float samples in the specified interval.
- max_over_time(range-vector): the maximum value of all points in the specified interval.
+ max_over_time(range-vector): the maximum value of all float samples in the specified interval.
- sum_over_time(range-vector): the sum of all values in the specified interval.
+ sum_over_time(range-vector): the sum of all float or histogram samples in the specified interval
+ (see details below).
- count_over_time(range-vector): the count of all values in the specified interval.
+ count_over_time(range-vector): the count of all samples in the specified interval.
- quantile_over_time(scalar, range-vector): the φ-quantile (0 ≤ φ ≤ 1) of the values in the specified
- interval.
+ quantile_over_time(scalar, range-vector): the φ-quantile (0 ≤ φ ≤ 1) of all float samples in the
+ specified interval.
- stddev_over_time(range-vector): the population standard deviation of the values in the specified
- interval.
+ stddev_over_time(range-vector): the population standard deviation of all float samples in the
+ specified interval.
- stdvar_over_time(range-vector): the population standard variance of the values in the specified
- interval.
+ stdvar_over_time(range-vector): the population standard variance of all float samples in the
+ specified interval.
- last_over_time(range-vector): the most recent point value in the specified interval.
+ last_over_time(range-vector): the most recent sample in the specified interval.
present_over_time(range-vector): the value 1 for any series in the specified interval.
@@ -1588,64 +2122,108 @@ const funcDocs: Record = {
If the feature flag
- --enable-feature=promql-experimental-functions is set, the following additional functions are available:
+ --enable-feature=promql-experimental-functions is set, the following additional functions are
+ available:
- mad_over_time(range-vector): the median absolute deviation of all points in the specified interval.
+ mad_over_time(range-vector): the median absolute deviation of all float samples in the specified
+ interval.
+
+
+ ts_of_min_over_time(range-vector): the timestamp of the last float sample that has the minimum
+ value of all float samples in the specified interval.
+
+
+ ts_of_max_over_time(range-vector): the timestamp of the last float sample that has the maximum
+ value of all float samples in the specified interval.
+
+
+ ts_of_last_over_time(range-vector): the timestamp of last sample in the specified interval.
+
+
+ first_over_time(range-vector): the oldest sample in the specified interval.
+
+
+ ts_of_first_over_time(range-vector): the timestamp of earliest sample in the specified interval.
- Note that all values in the specified interval have the same weight in the aggregation even if the values are not
- equally spaced throughout the interval.
+ Note that all values in the specified interval have the same weight in the aggregation even if the values are
+ not equally spaced throughout the interval.
+ These functions act on histograms in the following way:
+
+
+
+ count_over_time, first_over_time, last_over_time, and
+ present_over_time() act on float and histogram samples in the same way.
+
+
+ avg_over_time() and sum_over_time() act on histogram samples in a way that
+ corresponds to the respective aggregation operators. If a series contains a mix of float samples and histogram
+ samples within the range, the corresponding result is removed entirely from the output vector. Such a removal
+ is flagged by a warn-level annotation.
+
+
+ All other functions ignore histogram samples in the following way: Input ranges containing only histogram
+ samples are silently removed from the output. For ranges with a mix of histogram and float samples, only the
+ float samples are processed and the omission of the histogram samples is flagged by an info-level annotation.
+
+
+
- avg_over_time, sum_over_time, count_over_time, last_over_time,
- and
- present_over_time handle native histograms as expected. All other functions ignore histogram samples.
+ first_over_time(m[1m]) differs from m offset 1m in that the former will select the
+ first sample of m within the 1m range, where m offset 1m will select the most
+ recent sample within the lookback interval outside and prior to the 1m offset. This is particularly
+ useful with first_over_time(m[step()])
+ in range queries (available when --enable-feature=promql-duration-expr is set) to ensure that the
+ sample selected is within the range step.
>
),
max_over_time: (
<>
- The following functions allow aggregating each series of a given range vector over time and return an instant vector
- with per-series aggregation results:
+ The following functions allow aggregating each series of a given range vector over time and return an instant
+ vector with per-series aggregation results:
- avg_over_time(range-vector): the average value of all points in the specified interval.
+ avg_over_time(range-vector): the average value of all float or histogram samples in the specified
+ interval (see details below).
- min_over_time(range-vector): the minimum value of all points in the specified interval.
+ min_over_time(range-vector): the minimum value of all float samples in the specified interval.
- max_over_time(range-vector): the maximum value of all points in the specified interval.
+ max_over_time(range-vector): the maximum value of all float samples in the specified interval.
- sum_over_time(range-vector): the sum of all values in the specified interval.
+ sum_over_time(range-vector): the sum of all float or histogram samples in the specified interval
+ (see details below).
- count_over_time(range-vector): the count of all values in the specified interval.
+ count_over_time(range-vector): the count of all samples in the specified interval.
- quantile_over_time(scalar, range-vector): the φ-quantile (0 ≤ φ ≤ 1) of the values in the specified
- interval.
+ quantile_over_time(scalar, range-vector): the φ-quantile (0 ≤ φ ≤ 1) of all float samples in the
+ specified interval.
- stddev_over_time(range-vector): the population standard deviation of the values in the specified
- interval.
+ stddev_over_time(range-vector): the population standard deviation of all float samples in the
+ specified interval.
- stdvar_over_time(range-vector): the population standard variance of the values in the specified
- interval.
+ stdvar_over_time(range-vector): the population standard variance of all float samples in the
+ specified interval.
- last_over_time(range-vector): the most recent point value in the specified interval.
+ last_over_time(range-vector): the most recent sample in the specified interval.
present_over_time(range-vector): the value 1 for any series in the specified interval.
@@ -1654,64 +2232,108 @@ const funcDocs: Record = {
If the feature flag
- --enable-feature=promql-experimental-functions is set, the following additional functions are available:
+ --enable-feature=promql-experimental-functions is set, the following additional functions are
+ available:
- mad_over_time(range-vector): the median absolute deviation of all points in the specified interval.
+ mad_over_time(range-vector): the median absolute deviation of all float samples in the specified
+ interval.
+
+
+ ts_of_min_over_time(range-vector): the timestamp of the last float sample that has the minimum
+ value of all float samples in the specified interval.
+
+
+ ts_of_max_over_time(range-vector): the timestamp of the last float sample that has the maximum
+ value of all float samples in the specified interval.
+
+
+ ts_of_last_over_time(range-vector): the timestamp of last sample in the specified interval.
+
+
+ first_over_time(range-vector): the oldest sample in the specified interval.
+
+
+ ts_of_first_over_time(range-vector): the timestamp of earliest sample in the specified interval.
- Note that all values in the specified interval have the same weight in the aggregation even if the values are not
- equally spaced throughout the interval.
+ Note that all values in the specified interval have the same weight in the aggregation even if the values are
+ not equally spaced throughout the interval.
+ These functions act on histograms in the following way:
+
+
+
+ count_over_time, first_over_time, last_over_time, and
+ present_over_time() act on float and histogram samples in the same way.
+
+
+ avg_over_time() and sum_over_time() act on histogram samples in a way that
+ corresponds to the respective aggregation operators. If a series contains a mix of float samples and histogram
+ samples within the range, the corresponding result is removed entirely from the output vector. Such a removal
+ is flagged by a warn-level annotation.
+
+
+ All other functions ignore histogram samples in the following way: Input ranges containing only histogram
+ samples are silently removed from the output. For ranges with a mix of histogram and float samples, only the
+ float samples are processed and the omission of the histogram samples is flagged by an info-level annotation.
+
+
+
- avg_over_time, sum_over_time, count_over_time, last_over_time,
- and
- present_over_time handle native histograms as expected. All other functions ignore histogram samples.
+ first_over_time(m[1m]) differs from m offset 1m in that the former will select the
+ first sample of m within the 1m range, where m offset 1m will select the most
+ recent sample within the lookback interval outside and prior to the 1m offset. This is particularly
+ useful with first_over_time(m[step()])
+ in range queries (available when --enable-feature=promql-duration-expr is set) to ensure that the
+ sample selected is within the range step.
>
),
min_over_time: (
<>
- The following functions allow aggregating each series of a given range vector over time and return an instant vector
- with per-series aggregation results:
+ The following functions allow aggregating each series of a given range vector over time and return an instant
+ vector with per-series aggregation results:
- avg_over_time(range-vector): the average value of all points in the specified interval.
+ avg_over_time(range-vector): the average value of all float or histogram samples in the specified
+ interval (see details below).
- min_over_time(range-vector): the minimum value of all points in the specified interval.
+ min_over_time(range-vector): the minimum value of all float samples in the specified interval.
- max_over_time(range-vector): the maximum value of all points in the specified interval.
+ max_over_time(range-vector): the maximum value of all float samples in the specified interval.
- sum_over_time(range-vector): the sum of all values in the specified interval.
+ sum_over_time(range-vector): the sum of all float or histogram samples in the specified interval
+ (see details below).
- count_over_time(range-vector): the count of all values in the specified interval.
+ count_over_time(range-vector): the count of all samples in the specified interval.
- quantile_over_time(scalar, range-vector): the φ-quantile (0 ≤ φ ≤ 1) of the values in the specified
- interval.
+ quantile_over_time(scalar, range-vector): the φ-quantile (0 ≤ φ ≤ 1) of all float samples in the
+ specified interval.
- stddev_over_time(range-vector): the population standard deviation of the values in the specified
- interval.
+ stddev_over_time(range-vector): the population standard deviation of all float samples in the
+ specified interval.
- stdvar_over_time(range-vector): the population standard variance of the values in the specified
- interval.
+ stdvar_over_time(range-vector): the population standard variance of all float samples in the
+ specified interval.
- last_over_time(range-vector): the most recent point value in the specified interval.
+ last_over_time(range-vector): the most recent sample in the specified interval.
present_over_time(range-vector): the value 1 for any series in the specified interval.
@@ -1720,95 +2342,140 @@ const funcDocs: Record = {
If the feature flag
- --enable-feature=promql-experimental-functions is set, the following additional functions are available:
+ --enable-feature=promql-experimental-functions is set, the following additional functions are
+ available:
- mad_over_time(range-vector): the median absolute deviation of all points in the specified interval.
+ mad_over_time(range-vector): the median absolute deviation of all float samples in the specified
+ interval.
+
+
+ ts_of_min_over_time(range-vector): the timestamp of the last float sample that has the minimum
+ value of all float samples in the specified interval.
+
+
+ ts_of_max_over_time(range-vector): the timestamp of the last float sample that has the maximum
+ value of all float samples in the specified interval.
+
+
+ ts_of_last_over_time(range-vector): the timestamp of last sample in the specified interval.
+
+
+ first_over_time(range-vector): the oldest sample in the specified interval.
+
+
+ ts_of_first_over_time(range-vector): the timestamp of earliest sample in the specified interval.
- Note that all values in the specified interval have the same weight in the aggregation even if the values are not
- equally spaced throughout the interval.
+ Note that all values in the specified interval have the same weight in the aggregation even if the values are
+ not equally spaced throughout the interval.
+ These functions act on histograms in the following way:
+
+
+
+ count_over_time, first_over_time, last_over_time, and
+ present_over_time() act on float and histogram samples in the same way.
+
+
+ avg_over_time() and sum_over_time() act on histogram samples in a way that
+ corresponds to the respective aggregation operators. If a series contains a mix of float samples and histogram
+ samples within the range, the corresponding result is removed entirely from the output vector. Such a removal
+ is flagged by a warn-level annotation.
+
+
+ All other functions ignore histogram samples in the following way: Input ranges containing only histogram
+ samples are silently removed from the output. For ranges with a mix of histogram and float samples, only the
+ float samples are processed and the omission of the histogram samples is flagged by an info-level annotation.
+
+
+
- avg_over_time, sum_over_time, count_over_time, last_over_time,
- and
- present_over_time handle native histograms as expected. All other functions ignore histogram samples.
+ first_over_time(m[1m]) differs from m offset 1m in that the former will select the
+ first sample of m within the 1m range, where m offset 1m will select the most
+ recent sample within the lookback interval outside and prior to the 1m offset. This is particularly
+ useful with first_over_time(m[step()])
+ in range queries (available when --enable-feature=promql-duration-expr is set) to ensure that the
+ sample selected is within the range step.
>
),
minute: (
<>
- minute(v=vector(time()) instant-vector) returns the minute of the hour for each of the given times in
- UTC. Returned values are from 0 to 59.
+ minute(v=vector(time()) instant-vector) interprets float samples in v as timestamps
+ (number of seconds since January 1, 1970 UTC) and returns the minute of the hour (in UTC) for each of those
+ timestamps. Returned values are from 0 to 59. Histogram samples in the input vector are ignored silently.
>
),
month: (
<>
- month(v=vector(time()) instant-vector) returns the month of the year for each of the given times in UTC.
- Returned values are from 1 to 12, where 1 means January etc.
+ month(v=vector(time()) instant-vector) interprets float samples in v as timestamps
+ (number of seconds since January 1, 1970 UTC) and returns the month of the year (in UTC) for each of those
+ timestamps. Returned values are from 1 to 12, where 1 means January etc. Histogram samples in the input vector
+ are ignored silently.
>
),
pi: (
<>
- The trigonometric functions work in radians:
+ The trigonometric functions work in radians. They ignore histogram samples in the input vector.
- acos(v instant-vector): calculates the arccosine of all elements in v (
+ acos(v instant-vector): calculates the arccosine of all float samples in v (
special cases ).
- acosh(v instant-vector): calculates the inverse hyperbolic cosine of all elements in v (
- special cases ).
+ acosh(v instant-vector): calculates the inverse hyperbolic cosine of all float samples in{" "}
+ v (special cases ).
- asin(v instant-vector): calculates the arcsine of all elements in v (
+ asin(v instant-vector): calculates the arcsine of all float samples in v (
special cases ).
- asinh(v instant-vector): calculates the inverse hyperbolic sine of all elements in v (
- special cases ).
+ asinh(v instant-vector): calculates the inverse hyperbolic sine of all float samples in{" "}
+ v (special cases ).
- atan(v instant-vector): calculates the arctangent of all elements in v (
+ atan(v instant-vector): calculates the arctangent of all float samples in v (
special cases ).
- atanh(v instant-vector): calculates the inverse hyperbolic tangent of all elements in v (
- special cases ).
+ atanh(v instant-vector): calculates the inverse hyperbolic tangent of all float samples in{" "}
+ v (special cases ).
- cos(v instant-vector): calculates the cosine of all elements in v (
+ cos(v instant-vector): calculates the cosine of all float samples in v (
special cases ).
- cosh(v instant-vector): calculates the hyperbolic cosine of all elements in v (
+ cosh(v instant-vector): calculates the hyperbolic cosine of all float samples in v (
special cases ).
- sin(v instant-vector): calculates the sine of all elements in v (
+ sin(v instant-vector): calculates the sine of all float samples in v (
special cases ).
- sinh(v instant-vector): calculates the hyperbolic sine of all elements in v (
+ sinh(v instant-vector): calculates the hyperbolic sine of all float samples in v (
special cases ).
- tan(v instant-vector): calculates the tangent of all elements in v (
+ tan(v instant-vector): calculates the tangent of all float samples in v (
special cases ).
- tanh(v instant-vector): calculates the hyperbolic tangent of all elements in v (
- special cases ).
+ tanh(v instant-vector): calculates the hyperbolic tangent of all float samples in v{" "}
+ (special cases ).
@@ -1816,13 +2483,13 @@ const funcDocs: Record = {
- deg(v instant-vector): converts radians to degrees for all elements in v.
+ deg(v instant-vector): converts radians to degrees for all float samples in v.
pi(): returns pi.
- rad(v instant-vector): converts degrees to radians for all elements in v.
+ rad(v instant-vector): converts degrees to radians for all float samples in v.
>
@@ -1831,54 +2498,58 @@ const funcDocs: Record = {
<>
predict_linear(v range-vector, t scalar) predicts the value of time series
- t seconds from now, based on the range vector v, using{' '}
- simple linear regression . The range vector must
- have at least two samples in order to perform the calculation. When +Inf or -Inf are found
- in the range vector, the slope and offset value calculated will be NaN.
+ t seconds from now, based on the range vector v, using{" "}
+ simple linear regression . The range vector
+ must have at least two float samples in order to perform the calculation. When +Inf or{" "}
+ -Inf are found in the range vector, the predicted value will be NaN.
- predict_linear should only be used with gauges.
+ predict_linear should only be used with gauges and only works for float samples. Elements in the
+ range vector that contain only histogram samples are ignored entirely. For elements that contain a mix of float
+ and histogram samples, only the float samples are used as input, which is flagged by an info-level annotation.
>
),
present_over_time: (
<>
- The following functions allow aggregating each series of a given range vector over time and return an instant vector
- with per-series aggregation results:
+ The following functions allow aggregating each series of a given range vector over time and return an instant
+ vector with per-series aggregation results:
- avg_over_time(range-vector): the average value of all points in the specified interval.
+ avg_over_time(range-vector): the average value of all float or histogram samples in the specified
+ interval (see details below).
- min_over_time(range-vector): the minimum value of all points in the specified interval.
+ min_over_time(range-vector): the minimum value of all float samples in the specified interval.
- max_over_time(range-vector): the maximum value of all points in the specified interval.
+ max_over_time(range-vector): the maximum value of all float samples in the specified interval.
- sum_over_time(range-vector): the sum of all values in the specified interval.
+ sum_over_time(range-vector): the sum of all float or histogram samples in the specified interval
+ (see details below).
- count_over_time(range-vector): the count of all values in the specified interval.
+ count_over_time(range-vector): the count of all samples in the specified interval.
- quantile_over_time(scalar, range-vector): the φ-quantile (0 ≤ φ ≤ 1) of the values in the specified
- interval.
+ quantile_over_time(scalar, range-vector): the φ-quantile (0 ≤ φ ≤ 1) of all float samples in the
+ specified interval.
- stddev_over_time(range-vector): the population standard deviation of the values in the specified
- interval.
+ stddev_over_time(range-vector): the population standard deviation of all float samples in the
+ specified interval.
- stdvar_over_time(range-vector): the population standard variance of the values in the specified
- interval.
+ stdvar_over_time(range-vector): the population standard variance of all float samples in the
+ specified interval.
- last_over_time(range-vector): the most recent point value in the specified interval.
+ last_over_time(range-vector): the most recent sample in the specified interval.
present_over_time(range-vector): the value 1 for any series in the specified interval.
@@ -1887,64 +2558,108 @@ const funcDocs: Record = {
If the feature flag
- --enable-feature=promql-experimental-functions is set, the following additional functions are available:
+ --enable-feature=promql-experimental-functions is set, the following additional functions are
+ available:
- mad_over_time(range-vector): the median absolute deviation of all points in the specified interval.
+ mad_over_time(range-vector): the median absolute deviation of all float samples in the specified
+ interval.
+
+
+ ts_of_min_over_time(range-vector): the timestamp of the last float sample that has the minimum
+ value of all float samples in the specified interval.
+
+
+ ts_of_max_over_time(range-vector): the timestamp of the last float sample that has the maximum
+ value of all float samples in the specified interval.
+
+
+ ts_of_last_over_time(range-vector): the timestamp of last sample in the specified interval.
+
+
+ first_over_time(range-vector): the oldest sample in the specified interval.
+
+
+ ts_of_first_over_time(range-vector): the timestamp of earliest sample in the specified interval.
- Note that all values in the specified interval have the same weight in the aggregation even if the values are not
- equally spaced throughout the interval.
+ Note that all values in the specified interval have the same weight in the aggregation even if the values are
+ not equally spaced throughout the interval.
+ These functions act on histograms in the following way:
+
+
+
+ count_over_time, first_over_time, last_over_time, and
+ present_over_time() act on float and histogram samples in the same way.
+
+
+ avg_over_time() and sum_over_time() act on histogram samples in a way that
+ corresponds to the respective aggregation operators. If a series contains a mix of float samples and histogram
+ samples within the range, the corresponding result is removed entirely from the output vector. Such a removal
+ is flagged by a warn-level annotation.
+
+
+ All other functions ignore histogram samples in the following way: Input ranges containing only histogram
+ samples are silently removed from the output. For ranges with a mix of histogram and float samples, only the
+ float samples are processed and the omission of the histogram samples is flagged by an info-level annotation.
+
+
+
- avg_over_time, sum_over_time, count_over_time, last_over_time,
- and
- present_over_time handle native histograms as expected. All other functions ignore histogram samples.
+ first_over_time(m[1m]) differs from m offset 1m in that the former will select the
+ first sample of m within the 1m range, where m offset 1m will select the most
+ recent sample within the lookback interval outside and prior to the 1m offset. This is particularly
+ useful with first_over_time(m[step()])
+ in range queries (available when --enable-feature=promql-duration-expr is set) to ensure that the
+ sample selected is within the range step.
>
),
quantile_over_time: (
<>
- The following functions allow aggregating each series of a given range vector over time and return an instant vector
- with per-series aggregation results:
+ The following functions allow aggregating each series of a given range vector over time and return an instant
+ vector with per-series aggregation results:
- avg_over_time(range-vector): the average value of all points in the specified interval.
+ avg_over_time(range-vector): the average value of all float or histogram samples in the specified
+ interval (see details below).
- min_over_time(range-vector): the minimum value of all points in the specified interval.
+ min_over_time(range-vector): the minimum value of all float samples in the specified interval.
- max_over_time(range-vector): the maximum value of all points in the specified interval.
+ max_over_time(range-vector): the maximum value of all float samples in the specified interval.
- sum_over_time(range-vector): the sum of all values in the specified interval.
+ sum_over_time(range-vector): the sum of all float or histogram samples in the specified interval
+ (see details below).
- count_over_time(range-vector): the count of all values in the specified interval.
+ count_over_time(range-vector): the count of all samples in the specified interval.
- quantile_over_time(scalar, range-vector): the φ-quantile (0 ≤ φ ≤ 1) of the values in the specified
- interval.
+ quantile_over_time(scalar, range-vector): the φ-quantile (0 ≤ φ ≤ 1) of all float samples in the
+ specified interval.
- stddev_over_time(range-vector): the population standard deviation of the values in the specified
- interval.
+ stddev_over_time(range-vector): the population standard deviation of all float samples in the
+ specified interval.
- stdvar_over_time(range-vector): the population standard variance of the values in the specified
- interval.
+ stdvar_over_time(range-vector): the population standard variance of all float samples in the
+ specified interval.
- last_over_time(range-vector): the most recent point value in the specified interval.
+ last_over_time(range-vector): the most recent sample in the specified interval.
present_over_time(range-vector): the value 1 for any series in the specified interval.
@@ -1953,79 +2668,121 @@ const funcDocs: Record = {
If the feature flag
- --enable-feature=promql-experimental-functions is set, the following additional functions are available:
+ --enable-feature=promql-experimental-functions is set, the following additional functions are
+ available:
- mad_over_time(range-vector): the median absolute deviation of all points in the specified interval.
+ mad_over_time(range-vector): the median absolute deviation of all float samples in the specified
+ interval.
+
+
+ ts_of_min_over_time(range-vector): the timestamp of the last float sample that has the minimum
+ value of all float samples in the specified interval.
+
+
+ ts_of_max_over_time(range-vector): the timestamp of the last float sample that has the maximum
+ value of all float samples in the specified interval.
+
+
+ ts_of_last_over_time(range-vector): the timestamp of last sample in the specified interval.
+
+
+ first_over_time(range-vector): the oldest sample in the specified interval.
+
+
+ ts_of_first_over_time(range-vector): the timestamp of earliest sample in the specified interval.
- Note that all values in the specified interval have the same weight in the aggregation even if the values are not
- equally spaced throughout the interval.
+ Note that all values in the specified interval have the same weight in the aggregation even if the values are
+ not equally spaced throughout the interval.
+ These functions act on histograms in the following way:
+
+
+
+ count_over_time, first_over_time, last_over_time, and
+ present_over_time() act on float and histogram samples in the same way.
+
+
+ avg_over_time() and sum_over_time() act on histogram samples in a way that
+ corresponds to the respective aggregation operators. If a series contains a mix of float samples and histogram
+ samples within the range, the corresponding result is removed entirely from the output vector. Such a removal
+ is flagged by a warn-level annotation.
+
+
+ All other functions ignore histogram samples in the following way: Input ranges containing only histogram
+ samples are silently removed from the output. For ranges with a mix of histogram and float samples, only the
+ float samples are processed and the omission of the histogram samples is flagged by an info-level annotation.
+
+
+
- avg_over_time, sum_over_time, count_over_time, last_over_time,
- and
- present_over_time handle native histograms as expected. All other functions ignore histogram samples.
+ first_over_time(m[1m]) differs from m offset 1m in that the former will select the
+ first sample of m within the 1m range, where m offset 1m will select the most
+ recent sample within the lookback interval outside and prior to the 1m offset. This is particularly
+ useful with first_over_time(m[step()])
+ in range queries (available when --enable-feature=promql-duration-expr is set) to ensure that the
+ sample selected is within the range step.
>
),
rad: (
<>
- The trigonometric functions work in radians:
+ The trigonometric functions work in radians. They ignore histogram samples in the input vector.
- acos(v instant-vector): calculates the arccosine of all elements in v (
+ acos(v instant-vector): calculates the arccosine of all float samples in v (
special cases ).
- acosh(v instant-vector): calculates the inverse hyperbolic cosine of all elements in v (
- special cases ).
+ acosh(v instant-vector): calculates the inverse hyperbolic cosine of all float samples in{" "}
+ v (special cases ).
- asin(v instant-vector): calculates the arcsine of all elements in v (
+ asin(v instant-vector): calculates the arcsine of all float samples in v (
special cases ).
- asinh(v instant-vector): calculates the inverse hyperbolic sine of all elements in v (
- special cases ).
+ asinh(v instant-vector): calculates the inverse hyperbolic sine of all float samples in{" "}
+ v (special cases ).
- atan(v instant-vector): calculates the arctangent of all elements in v (
+ atan(v instant-vector): calculates the arctangent of all float samples in v (
special cases ).
- atanh(v instant-vector): calculates the inverse hyperbolic tangent of all elements in v (
- special cases ).
+ atanh(v instant-vector): calculates the inverse hyperbolic tangent of all float samples in{" "}
+ v (special cases ).
- cos(v instant-vector): calculates the cosine of all elements in v (
+ cos(v instant-vector): calculates the cosine of all float samples in v (
special cases ).
- cosh(v instant-vector): calculates the hyperbolic cosine of all elements in v (
+ cosh(v instant-vector): calculates the hyperbolic cosine of all float samples in v (
special cases ).
- sin(v instant-vector): calculates the sine of all elements in v (
+ sin(v instant-vector): calculates the sine of all float samples in v (
special cases ).
- sinh(v instant-vector): calculates the hyperbolic sine of all elements in v (
+ sinh(v instant-vector): calculates the hyperbolic sine of all float samples in v (
special cases ).
- tan(v instant-vector): calculates the tangent of all elements in v (
+ tan(v instant-vector): calculates the tangent of all float samples in v (
special cases ).
- tanh(v instant-vector): calculates the hyperbolic tangent of all elements in v (
- special cases ).
+ tanh(v instant-vector): calculates the hyperbolic tangent of all float samples in v{" "}
+ (special cases ).
@@ -2033,13 +2790,13 @@ const funcDocs: Record = {
- deg(v instant-vector): converts radians to degrees for all elements in v.
+ deg(v instant-vector): converts radians to degrees for all float samples in v.
pi(): returns pi.
- rad(v instant-vector): converts degrees to radians for all elements in v.
+ rad(v instant-vector): converts degrees to radians for all float samples in v.
>
@@ -2047,40 +2804,40 @@ const funcDocs: Record = {
rate: (
<>
- rate(v range-vector) calculates the per-second average rate of increase of the time series in the range
- vector. Breaks in monotonicity (such as counter resets due to target restarts) are automatically adjusted for. Also,
- the calculation extrapolates to the ends of the time range, allowing for missed scrapes or imperfect alignment of
- scrape cycles with the range’s time period.
+ rate(v range-vector) calculates the per-second average rate of increase of the time series in the
+ range vector. Breaks in monotonicity (such as counter resets due to target restarts) are automatically adjusted
+ for. Also, the calculation extrapolates to the ends of the time range, allowing for missed scrapes or imperfect
+ alignment of scrape cycles with the range’s time period.
- The following example expression returns the per-second rate of HTTP requests as measured over the last 5 minutes,
+ The following example expression returns the per-second average rate of HTTP requests over the last 5 minutes,
per time series in the range vector:
- rate(http_requests_total{'{'}job="api-server"{'}'}[5m])
+ rate(http_requests_total{"{"}job="api-server"{"}"}[5m])
- rate acts on native histograms by calculating a new histogram where each component (sum and count of
- observations, buckets) is the rate of increase between the respective component in the first and last native
- histogram in
- v. However, each element in v that contains a mix of float and native histogram samples
- within the range, will be missing from the result vector.
+ rate acts on native histograms by calculating a new histogram where each component (sum and count
+ of observations, buckets) is the rate of increase between the respective component in the first and last native
+ histogram in v. However, each element in v that contains a mix of float and native
+ histogram samples within the range, will be omitted from the result vector, flagged by a warn-level annotation.
- rate should only be used with counters and native histograms where the components behave like counters.
- It is best suited for alerting, and for graphing of slow-moving counters.
+ rate should only be used with counters (for both floats and histograms). It is best suited for
+ alerting, and for graphing of slow-moving counters.
- Note that when combining rate() with an aggregation operator (e.g. sum()) or a function
- aggregating over time (any function ending in _over_time), always take a rate() first, then
- aggregate. Otherwise rate() cannot detect counter resets when your target restarts.
+ Note that when combining rate() with an aggregation operator (e.g. sum()) or a
+ function aggregating over time (any function ending in _over_time), always take a{" "}
+ rate() first, then aggregate. Otherwise rate() cannot detect counter resets when your
+ target restarts.
>
),
@@ -2089,102 +2846,104 @@ const funcDocs: Record = {
For each input time series, resets(v range-vector) returns the number of counter resets within the
provided time range as an instant vector. Any decrease in the value between two consecutive float samples is
- interpreted as a counter reset. A reset in a native histogram is detected in a more complex way: Any decrease in any
- bucket, including the zero bucket, or in the count of observation constitutes a counter reset, but also the
- disappearance of any previously populated bucket, an increase in bucket resolution, or a decrease of the zero-bucket
- width.
+ interpreted as a counter reset. A reset in a native histogram is detected in a more complex way: Any decrease in
+ any bucket, including the zero bucket, or in the count of observation constitutes a counter reset, but also the
+ disappearance of any previously populated bucket, a decrease of the zero-bucket width, or any schema change that
+ is not a compatible decrease of resolution.
- resets should only be used with counters and counter-like native histograms.
+ resets should only be used with counters (for both floats and histograms).
- If the range vector contains a mix of float and histogram samples for the same series, counter resets are detected
- separately and their numbers added up. The change from a float to a histogram sample is not considered a
- counter reset. Each float sample is compared to the next float sample, and each histogram is comprared to the next
- histogram.
+ A float sample followed by a histogram sample, or vice versa, counts as a reset. A counter histogram sample
+ followed by a gauge histogram sample, or vice versa, also counts as a reset (but note that resets{" "}
+ should not be used on gauges in the first place, see above).
>
),
round: (
<>
- round(v instant-vector, to_nearest=1 scalar) rounds the sample values of all elements in v{' '}
- to the nearest integer. Ties are resolved by rounding up. The optional to_nearest argument allows
- specifying the nearest multiple to which the sample values should be rounded. This multiple may also be a fraction.
+ round(v instant-vector, to_nearest=1 scalar) rounds the sample values of all elements in{" "}
+ v to the nearest integer. Ties are resolved by rounding up. The optional to_nearest{" "}
+ argument allows specifying the nearest multiple to which the sample values should be rounded. This multiple may
+ also be a fraction. Histogram samples in the input vector are ignored silently.
>
),
scalar: (
<>
- Given a single-element input vector, scalar(v instant-vector) returns the sample value of that single
- element as a scalar. If the input vector does not have exactly one element, scalar will return{' '}
- NaN.
+ Given an input vector that contains only one element with a float sample,
+ scalar(v instant-vector) returns the sample value of that float sample as a scalar. If the input
+ vector does not have exactly one element with a float sample, scalar will return NaN.
+ Histogram samples in the input vector are ignored silently.
>
),
sgn: (
<>
- sgn(v instant-vector) returns a vector with all sample values converted to their sign, defined as this:
- 1 if v is positive, -1 if v is negative and 0 if v is equal to zero.
+ sgn(v instant-vector) returns a vector with all float sample values converted to their sign,
+ defined as this: 1 if v is positive, -1 if v is negative and 0 if v is equal to zero. Histogram samples in the
+ input vector are ignored silently.
>
),
sin: (
<>
- The trigonometric functions work in radians:
+ The trigonometric functions work in radians. They ignore histogram samples in the input vector.
- acos(v instant-vector): calculates the arccosine of all elements in v (
+ acos(v instant-vector): calculates the arccosine of all float samples in v (
special cases ).
- acosh(v instant-vector): calculates the inverse hyperbolic cosine of all elements in v (
- special cases ).
+ acosh(v instant-vector): calculates the inverse hyperbolic cosine of all float samples in{" "}
+ v (special cases ).
- asin(v instant-vector): calculates the arcsine of all elements in v (
+ asin(v instant-vector): calculates the arcsine of all float samples in v (
special cases ).
- asinh(v instant-vector): calculates the inverse hyperbolic sine of all elements in v (
- special cases ).
+ asinh(v instant-vector): calculates the inverse hyperbolic sine of all float samples in{" "}
+ v (special cases ).
- atan(v instant-vector): calculates the arctangent of all elements in v (
+ atan(v instant-vector): calculates the arctangent of all float samples in v (
special cases ).
- atanh(v instant-vector): calculates the inverse hyperbolic tangent of all elements in v (
- special cases ).
+ atanh(v instant-vector): calculates the inverse hyperbolic tangent of all float samples in{" "}
+ v (special cases ).
- cos(v instant-vector): calculates the cosine of all elements in v (
+ cos(v instant-vector): calculates the cosine of all float samples in v (
special cases ).
- cosh(v instant-vector): calculates the hyperbolic cosine of all elements in v (
+ cosh(v instant-vector): calculates the hyperbolic cosine of all float samples in v (
special cases ).
- sin(v instant-vector): calculates the sine of all elements in v (
+ sin(v instant-vector): calculates the sine of all float samples in v (
special cases ).
- sinh(v instant-vector): calculates the hyperbolic sine of all elements in v (
+ sinh(v instant-vector): calculates the hyperbolic sine of all float samples in v (
special cases ).
- tan(v instant-vector): calculates the tangent of all elements in v (
+ tan(v instant-vector): calculates the tangent of all float samples in v (
special cases ).
- tanh(v instant-vector): calculates the hyperbolic tangent of all elements in v (
- special cases ).
+ tanh(v instant-vector): calculates the hyperbolic tangent of all float samples in v{" "}
+ (special cases ).
@@ -2192,69 +2951,69 @@ const funcDocs: Record = {
- deg(v instant-vector): converts radians to degrees for all elements in v.
+ deg(v instant-vector): converts radians to degrees for all float samples in v.
pi(): returns pi.
- rad(v instant-vector): converts degrees to radians for all elements in v.
+ rad(v instant-vector): converts degrees to radians for all float samples in v.
>
),
sinh: (
<>
- The trigonometric functions work in radians:
+ The trigonometric functions work in radians. They ignore histogram samples in the input vector.
- acos(v instant-vector): calculates the arccosine of all elements in v (
+ acos(v instant-vector): calculates the arccosine of all float samples in v (
special cases ).
- acosh(v instant-vector): calculates the inverse hyperbolic cosine of all elements in v (
- special cases ).
+ acosh(v instant-vector): calculates the inverse hyperbolic cosine of all float samples in{" "}
+ v (special cases ).
- asin(v instant-vector): calculates the arcsine of all elements in v (
+ asin(v instant-vector): calculates the arcsine of all float samples in v (
special cases ).
- asinh(v instant-vector): calculates the inverse hyperbolic sine of all elements in v (
- special cases ).
+ asinh(v instant-vector): calculates the inverse hyperbolic sine of all float samples in{" "}
+ v (special cases ).
- atan(v instant-vector): calculates the arctangent of all elements in v (
+ atan(v instant-vector): calculates the arctangent of all float samples in v (
special cases ).
- atanh(v instant-vector): calculates the inverse hyperbolic tangent of all elements in v (
- special cases ).
+ atanh(v instant-vector): calculates the inverse hyperbolic tangent of all float samples in{" "}
+ v (special cases ).
- cos(v instant-vector): calculates the cosine of all elements in v (
+ cos(v instant-vector): calculates the cosine of all float samples in v (
special cases ).
- cosh(v instant-vector): calculates the hyperbolic cosine of all elements in v (
+ cosh(v instant-vector): calculates the hyperbolic cosine of all float samples in v (
special cases ).
- sin(v instant-vector): calculates the sine of all elements in v (
+ sin(v instant-vector): calculates the sine of all float samples in v (
special cases ).
- sinh(v instant-vector): calculates the hyperbolic sine of all elements in v (
+ sinh(v instant-vector): calculates the hyperbolic sine of all float samples in v (
special cases ).
- tan(v instant-vector): calculates the tangent of all elements in v (
+ tan(v instant-vector): calculates the tangent of all float samples in v (
special cases ).
- tanh(v instant-vector): calculates the hyperbolic tangent of all elements in v (
- special cases ).
+ tanh(v instant-vector): calculates the hyperbolic tangent of all float samples in v{" "}
+ (special cases ).
@@ -2262,13 +3021,13 @@ const funcDocs: Record = {
- deg(v instant-vector): converts radians to degrees for all elements in v.
+ deg(v instant-vector): converts radians to degrees for all float samples in v.
pi(): returns pi.
- rad(v instant-vector): converts degrees to radians for all elements in v.
+ rad(v instant-vector): converts degrees to radians for all float samples in v.
>
@@ -2276,13 +3035,13 @@ const funcDocs: Record = {
sort: (
<>
- sort(v instant-vector) returns vector elements sorted by their sample values, in ascending order. Native
- histograms are sorted by their sum of observations.
+ sort(v instant-vector) returns vector elements sorted by their float sample values, in ascending
+ order. Histogram samples in the input vector are ignored silently.
- Please note that sort only affects the results of instant queries, as range query results always have a
- fixed output ordering.
+ Please note that sort only affects the results of instant queries, as range query results always
+ have a fixed output ordering.
>
),
@@ -2290,24 +3049,27 @@ const funcDocs: Record = {
<>
- This function has to be enabled via the{' '}
- feature flag {' '}
+ This function has to be enabled via the{" "}
+ feature flag
--enable-feature=promql-experimental-functions.
- sort_by_label(v instant-vector, label string, ...) returns vector elements sorted by the values of the
- given labels in ascending order. In case these label values are equal, elements are sorted by their full label sets.
+ sort_by_label(v instant-vector, label string, ...) returns vector elements sorted by the values of
+ the given labels in ascending order. In case these label values are equal, elements are sorted by their full
+ label sets.
+ sort_by_label acts on float and histogram samples in the same way.
- Please note that the sort by label functions only affect the results of instant queries, as range query results
+ Please note that sort_by_label only affects the results of instant queries, as range query results
always have a fixed output ordering.
- This function uses natural sort order .
+ sort_by_label uses{" "}
+ natural sort order .
>
),
@@ -2315,8 +3077,8 @@ const funcDocs: Record = {
<>
- This function has to be enabled via the{' '}
- feature flag {' '}
+ This function has to be enabled via the{" "}
+ feature flag
--enable-feature=promql-experimental-functions.
@@ -2324,15 +3086,6 @@ const funcDocs: Record = {
Same as sort_by_label, but sorts in descending order.
-
-
- Please note that the sort by label functions only affect the results of instant queries, as range query results
- always have a fixed output ordering.
-
-
-
- This function uses natural sort order .
-
>
),
sort_desc: (
@@ -2340,57 +3093,55 @@ const funcDocs: Record = {
Same as sort, but sorts in descending order.
-
-
- Like sort, sort_desc only affects the results of instant queries, as range query results
- always have a fixed output ordering.
-
>
),
sqrt: (
<>
- sqrt(v instant-vector) calculates the square root of all elements in v.
+ sqrt(v instant-vector) calculates the square root of all float samples in
+ v. Histogram samples in the input vector are ignored silently.
>
),
stddev_over_time: (
<>
- The following functions allow aggregating each series of a given range vector over time and return an instant vector
- with per-series aggregation results:
+ The following functions allow aggregating each series of a given range vector over time and return an instant
+ vector with per-series aggregation results:
- avg_over_time(range-vector): the average value of all points in the specified interval.
+ avg_over_time(range-vector): the average value of all float or histogram samples in the specified
+ interval (see details below).
- min_over_time(range-vector): the minimum value of all points in the specified interval.
+ min_over_time(range-vector): the minimum value of all float samples in the specified interval.
- max_over_time(range-vector): the maximum value of all points in the specified interval.
+ max_over_time(range-vector): the maximum value of all float samples in the specified interval.
- sum_over_time(range-vector): the sum of all values in the specified interval.
+ sum_over_time(range-vector): the sum of all float or histogram samples in the specified interval
+ (see details below).
- count_over_time(range-vector): the count of all values in the specified interval.
+ count_over_time(range-vector): the count of all samples in the specified interval.
- quantile_over_time(scalar, range-vector): the φ-quantile (0 ≤ φ ≤ 1) of the values in the specified
- interval.
+ quantile_over_time(scalar, range-vector): the φ-quantile (0 ≤ φ ≤ 1) of all float samples in the
+ specified interval.
- stddev_over_time(range-vector): the population standard deviation of the values in the specified
- interval.
+ stddev_over_time(range-vector): the population standard deviation of all float samples in the
+ specified interval.
- stdvar_over_time(range-vector): the population standard variance of the values in the specified
- interval.
+ stdvar_over_time(range-vector): the population standard variance of all float samples in the
+ specified interval.
- last_over_time(range-vector): the most recent point value in the specified interval.
+ last_over_time(range-vector): the most recent sample in the specified interval.
present_over_time(range-vector): the value 1 for any series in the specified interval.
@@ -2399,64 +3150,108 @@ const funcDocs: Record = {
If the feature flag
- --enable-feature=promql-experimental-functions is set, the following additional functions are available:
+ --enable-feature=promql-experimental-functions is set, the following additional functions are
+ available:
- mad_over_time(range-vector): the median absolute deviation of all points in the specified interval.
+ mad_over_time(range-vector): the median absolute deviation of all float samples in the specified
+ interval.
+
+
+ ts_of_min_over_time(range-vector): the timestamp of the last float sample that has the minimum
+ value of all float samples in the specified interval.
+
+
+ ts_of_max_over_time(range-vector): the timestamp of the last float sample that has the maximum
+ value of all float samples in the specified interval.
+
+
+ ts_of_last_over_time(range-vector): the timestamp of last sample in the specified interval.
+
+
+ first_over_time(range-vector): the oldest sample in the specified interval.
+
+
+ ts_of_first_over_time(range-vector): the timestamp of earliest sample in the specified interval.
- Note that all values in the specified interval have the same weight in the aggregation even if the values are not
- equally spaced throughout the interval.
+ Note that all values in the specified interval have the same weight in the aggregation even if the values are
+ not equally spaced throughout the interval.
+ These functions act on histograms in the following way:
+
+
+
+ count_over_time, first_over_time, last_over_time, and
+ present_over_time() act on float and histogram samples in the same way.
+
+
+ avg_over_time() and sum_over_time() act on histogram samples in a way that
+ corresponds to the respective aggregation operators. If a series contains a mix of float samples and histogram
+ samples within the range, the corresponding result is removed entirely from the output vector. Such a removal
+ is flagged by a warn-level annotation.
+
+
+ All other functions ignore histogram samples in the following way: Input ranges containing only histogram
+ samples are silently removed from the output. For ranges with a mix of histogram and float samples, only the
+ float samples are processed and the omission of the histogram samples is flagged by an info-level annotation.
+
+
+
- avg_over_time, sum_over_time, count_over_time, last_over_time,
- and
- present_over_time handle native histograms as expected. All other functions ignore histogram samples.
+ first_over_time(m[1m]) differs from m offset 1m in that the former will select the
+ first sample of m within the 1m range, where m offset 1m will select the most
+ recent sample within the lookback interval outside and prior to the 1m offset. This is particularly
+ useful with first_over_time(m[step()])
+ in range queries (available when --enable-feature=promql-duration-expr is set) to ensure that the
+ sample selected is within the range step.
>
),
stdvar_over_time: (
<>
- The following functions allow aggregating each series of a given range vector over time and return an instant vector
- with per-series aggregation results:
+ The following functions allow aggregating each series of a given range vector over time and return an instant
+ vector with per-series aggregation results:
- avg_over_time(range-vector): the average value of all points in the specified interval.
+ avg_over_time(range-vector): the average value of all float or histogram samples in the specified
+ interval (see details below).
- min_over_time(range-vector): the minimum value of all points in the specified interval.
+ min_over_time(range-vector): the minimum value of all float samples in the specified interval.
- max_over_time(range-vector): the maximum value of all points in the specified interval.
+ max_over_time(range-vector): the maximum value of all float samples in the specified interval.
- sum_over_time(range-vector): the sum of all values in the specified interval.
+ sum_over_time(range-vector): the sum of all float or histogram samples in the specified interval
+ (see details below).
- count_over_time(range-vector): the count of all values in the specified interval.
+ count_over_time(range-vector): the count of all samples in the specified interval.
- quantile_over_time(scalar, range-vector): the φ-quantile (0 ≤ φ ≤ 1) of the values in the specified
- interval.
+ quantile_over_time(scalar, range-vector): the φ-quantile (0 ≤ φ ≤ 1) of all float samples in the
+ specified interval.
- stddev_over_time(range-vector): the population standard deviation of the values in the specified
- interval.
+ stddev_over_time(range-vector): the population standard deviation of all float samples in the
+ specified interval.
- stdvar_over_time(range-vector): the population standard variance of the values in the specified
- interval.
+ stdvar_over_time(range-vector): the population standard variance of all float samples in the
+ specified interval.
- last_over_time(range-vector): the most recent point value in the specified interval.
+ last_over_time(range-vector): the most recent sample in the specified interval.
present_over_time(range-vector): the value 1 for any series in the specified interval.
@@ -2465,64 +3260,108 @@ const funcDocs: Record = {
If the feature flag
- --enable-feature=promql-experimental-functions is set, the following additional functions are available:
+ --enable-feature=promql-experimental-functions is set, the following additional functions are
+ available:
- mad_over_time(range-vector): the median absolute deviation of all points in the specified interval.
+ mad_over_time(range-vector): the median absolute deviation of all float samples in the specified
+ interval.
+
+
+ ts_of_min_over_time(range-vector): the timestamp of the last float sample that has the minimum
+ value of all float samples in the specified interval.
+
+
+ ts_of_max_over_time(range-vector): the timestamp of the last float sample that has the maximum
+ value of all float samples in the specified interval.
+
+
+ ts_of_last_over_time(range-vector): the timestamp of last sample in the specified interval.
+
+
+ first_over_time(range-vector): the oldest sample in the specified interval.
+
+
+ ts_of_first_over_time(range-vector): the timestamp of earliest sample in the specified interval.
- Note that all values in the specified interval have the same weight in the aggregation even if the values are not
- equally spaced throughout the interval.
+ Note that all values in the specified interval have the same weight in the aggregation even if the values are
+ not equally spaced throughout the interval.
+ These functions act on histograms in the following way:
+
+
+
+ count_over_time, first_over_time, last_over_time, and
+ present_over_time() act on float and histogram samples in the same way.
+
+
+ avg_over_time() and sum_over_time() act on histogram samples in a way that
+ corresponds to the respective aggregation operators. If a series contains a mix of float samples and histogram
+ samples within the range, the corresponding result is removed entirely from the output vector. Such a removal
+ is flagged by a warn-level annotation.
+
+
+ All other functions ignore histogram samples in the following way: Input ranges containing only histogram
+ samples are silently removed from the output. For ranges with a mix of histogram and float samples, only the
+ float samples are processed and the omission of the histogram samples is flagged by an info-level annotation.
+
+
+
- avg_over_time, sum_over_time, count_over_time, last_over_time,
- and
- present_over_time handle native histograms as expected. All other functions ignore histogram samples.
+ first_over_time(m[1m]) differs from m offset 1m in that the former will select the
+ first sample of m within the 1m range, where m offset 1m will select the most
+ recent sample within the lookback interval outside and prior to the 1m offset. This is particularly
+ useful with first_over_time(m[step()])
+ in range queries (available when --enable-feature=promql-duration-expr is set) to ensure that the
+ sample selected is within the range step.
>
),
sum_over_time: (
<>
- The following functions allow aggregating each series of a given range vector over time and return an instant vector
- with per-series aggregation results:
+ The following functions allow aggregating each series of a given range vector over time and return an instant
+ vector with per-series aggregation results:
- avg_over_time(range-vector): the average value of all points in the specified interval.
+ avg_over_time(range-vector): the average value of all float or histogram samples in the specified
+ interval (see details below).
- min_over_time(range-vector): the minimum value of all points in the specified interval.
+ min_over_time(range-vector): the minimum value of all float samples in the specified interval.
- max_over_time(range-vector): the maximum value of all points in the specified interval.
+ max_over_time(range-vector): the maximum value of all float samples in the specified interval.
- sum_over_time(range-vector): the sum of all values in the specified interval.
+ sum_over_time(range-vector): the sum of all float or histogram samples in the specified interval
+ (see details below).
- count_over_time(range-vector): the count of all values in the specified interval.
+ count_over_time(range-vector): the count of all samples in the specified interval.
- quantile_over_time(scalar, range-vector): the φ-quantile (0 ≤ φ ≤ 1) of the values in the specified
- interval.
+ quantile_over_time(scalar, range-vector): the φ-quantile (0 ≤ φ ≤ 1) of all float samples in the
+ specified interval.
- stddev_over_time(range-vector): the population standard deviation of the values in the specified
- interval.
+ stddev_over_time(range-vector): the population standard deviation of all float samples in the
+ specified interval.
- stdvar_over_time(range-vector): the population standard variance of the values in the specified
- interval.
+ stdvar_over_time(range-vector): the population standard variance of all float samples in the
+ specified interval.
- last_over_time(range-vector): the most recent point value in the specified interval.
+ last_over_time(range-vector): the most recent sample in the specified interval.
present_over_time(range-vector): the value 1 for any series in the specified interval.
@@ -2531,79 +3370,121 @@ const funcDocs: Record = {
If the feature flag
- --enable-feature=promql-experimental-functions is set, the following additional functions are available:
+ --enable-feature=promql-experimental-functions is set, the following additional functions are
+ available:
- mad_over_time(range-vector): the median absolute deviation of all points in the specified interval.
+ mad_over_time(range-vector): the median absolute deviation of all float samples in the specified
+ interval.
+
+
+ ts_of_min_over_time(range-vector): the timestamp of the last float sample that has the minimum
+ value of all float samples in the specified interval.
+
+
+ ts_of_max_over_time(range-vector): the timestamp of the last float sample that has the maximum
+ value of all float samples in the specified interval.
+
+
+ ts_of_last_over_time(range-vector): the timestamp of last sample in the specified interval.
+
+
+ first_over_time(range-vector): the oldest sample in the specified interval.
+
+
+ ts_of_first_over_time(range-vector): the timestamp of earliest sample in the specified interval.
- Note that all values in the specified interval have the same weight in the aggregation even if the values are not
- equally spaced throughout the interval.
+ Note that all values in the specified interval have the same weight in the aggregation even if the values are
+ not equally spaced throughout the interval.
+ These functions act on histograms in the following way:
+
+
+
+ count_over_time, first_over_time, last_over_time, and
+ present_over_time() act on float and histogram samples in the same way.
+
+
+ avg_over_time() and sum_over_time() act on histogram samples in a way that
+ corresponds to the respective aggregation operators. If a series contains a mix of float samples and histogram
+ samples within the range, the corresponding result is removed entirely from the output vector. Such a removal
+ is flagged by a warn-level annotation.
+
+
+ All other functions ignore histogram samples in the following way: Input ranges containing only histogram
+ samples are silently removed from the output. For ranges with a mix of histogram and float samples, only the
+ float samples are processed and the omission of the histogram samples is flagged by an info-level annotation.
+
+
+
- avg_over_time, sum_over_time, count_over_time, last_over_time,
- and
- present_over_time handle native histograms as expected. All other functions ignore histogram samples.
+ first_over_time(m[1m]) differs from m offset 1m in that the former will select the
+ first sample of m within the 1m range, where m offset 1m will select the most
+ recent sample within the lookback interval outside and prior to the 1m offset. This is particularly
+ useful with first_over_time(m[step()])
+ in range queries (available when --enable-feature=promql-duration-expr is set) to ensure that the
+ sample selected is within the range step.
>
),
tan: (
<>
- The trigonometric functions work in radians:
+ The trigonometric functions work in radians. They ignore histogram samples in the input vector.
- acos(v instant-vector): calculates the arccosine of all elements in v (
+ acos(v instant-vector): calculates the arccosine of all float samples in v (
special cases ).
- acosh(v instant-vector): calculates the inverse hyperbolic cosine of all elements in v (
- special cases ).
+ acosh(v instant-vector): calculates the inverse hyperbolic cosine of all float samples in{" "}
+ v (special cases ).
- asin(v instant-vector): calculates the arcsine of all elements in v (
+ asin(v instant-vector): calculates the arcsine of all float samples in v (
special cases ).
- asinh(v instant-vector): calculates the inverse hyperbolic sine of all elements in v (
- special cases ).
+ asinh(v instant-vector): calculates the inverse hyperbolic sine of all float samples in{" "}
+ v (special cases ).
- atan(v instant-vector): calculates the arctangent of all elements in v (
+ atan(v instant-vector): calculates the arctangent of all float samples in v (
special cases ).
- atanh(v instant-vector): calculates the inverse hyperbolic tangent of all elements in v (
- special cases ).
+ atanh(v instant-vector): calculates the inverse hyperbolic tangent of all float samples in{" "}
+ v (special cases ).
- cos(v instant-vector): calculates the cosine of all elements in v (
+ cos(v instant-vector): calculates the cosine of all float samples in v (
special cases ).
- cosh(v instant-vector): calculates the hyperbolic cosine of all elements in v (
+ cosh(v instant-vector): calculates the hyperbolic cosine of all float samples in v (
special cases ).
- sin(v instant-vector): calculates the sine of all elements in v (
+ sin(v instant-vector): calculates the sine of all float samples in v (
special cases ).
- sinh(v instant-vector): calculates the hyperbolic sine of all elements in v (
+ sinh(v instant-vector): calculates the hyperbolic sine of all float samples in v (
special cases ).
- tan(v instant-vector): calculates the tangent of all elements in v (
+ tan(v instant-vector): calculates the tangent of all float samples in v (
special cases ).
- tanh(v instant-vector): calculates the hyperbolic tangent of all elements in v (
- special cases ).
+ tanh(v instant-vector): calculates the hyperbolic tangent of all float samples in v{" "}
+ (special cases ).
@@ -2611,69 +3492,69 @@ const funcDocs: Record = {
- deg(v instant-vector): converts radians to degrees for all elements in v.
+ deg(v instant-vector): converts radians to degrees for all float samples in v.
pi(): returns pi.
- rad(v instant-vector): converts degrees to radians for all elements in v.
+ rad(v instant-vector): converts degrees to radians for all float samples in v.
>
),
tanh: (
<>
- The trigonometric functions work in radians:
+ The trigonometric functions work in radians. They ignore histogram samples in the input vector.
- acos(v instant-vector): calculates the arccosine of all elements in v (
+ acos(v instant-vector): calculates the arccosine of all float samples in v (
special cases ).
- acosh(v instant-vector): calculates the inverse hyperbolic cosine of all elements in v (
- special cases ).
+ acosh(v instant-vector): calculates the inverse hyperbolic cosine of all float samples in{" "}
+ v (special cases ).
- asin(v instant-vector): calculates the arcsine of all elements in v (
+ asin(v instant-vector): calculates the arcsine of all float samples in v (
special cases ).
- asinh(v instant-vector): calculates the inverse hyperbolic sine of all elements in v (
- special cases ).
+ asinh(v instant-vector): calculates the inverse hyperbolic sine of all float samples in{" "}
+ v (special cases ).
- atan(v instant-vector): calculates the arctangent of all elements in v (
+ atan(v instant-vector): calculates the arctangent of all float samples in v (
special cases ).
- atanh(v instant-vector): calculates the inverse hyperbolic tangent of all elements in v (
- special cases ).
+ atanh(v instant-vector): calculates the inverse hyperbolic tangent of all float samples in{" "}
+ v (special cases ).
- cos(v instant-vector): calculates the cosine of all elements in v (
+ cos(v instant-vector): calculates the cosine of all float samples in v (
special cases ).
- cosh(v instant-vector): calculates the hyperbolic cosine of all elements in v (
+ cosh(v instant-vector): calculates the hyperbolic cosine of all float samples in v (
special cases ).
- sin(v instant-vector): calculates the sine of all elements in v (
+ sin(v instant-vector): calculates the sine of all float samples in v (
special cases ).
- sinh(v instant-vector): calculates the hyperbolic sine of all elements in v (
+ sinh(v instant-vector): calculates the hyperbolic sine of all float samples in v (
special cases ).
- tan(v instant-vector): calculates the tangent of all elements in v (
+ tan(v instant-vector): calculates the tangent of all float samples in v (
special cases ).
- tanh(v instant-vector): calculates the hyperbolic tangent of all elements in v (
- special cases ).
+ tanh(v instant-vector): calculates the hyperbolic tangent of all float samples in v{" "}
+ (special cases ).
@@ -2681,13 +3562,13 @@ const funcDocs: Record = {
- deg(v instant-vector): converts radians to degrees for all elements in v.
+ deg(v instant-vector): converts radians to degrees for all float samples in v.
pi(): returns pi.
- rad(v instant-vector): converts degrees to radians for all elements in v.
+ rad(v instant-vector): converts degrees to radians for all float samples in v.
>
@@ -2695,8 +3576,8 @@ const funcDocs: Record = {
time: (
<>
- time() returns the number of seconds since January 1, 1970 UTC. Note that this does not actually return
- the current time, but the time at which the expression is to be evaluated.
+ time() returns the number of seconds since January 1, 1970 UTC. Note that this does not actually
+ return the current time, but the time at which the expression is to be evaluated.
>
),
@@ -2704,14 +3585,455 @@ const funcDocs: Record = {
<>
timestamp(v instant-vector) returns the timestamp of each of the samples of the given vector as the
- number of seconds since January 1, 1970 UTC. It also works with histogram samples.
+ number of seconds since January 1, 1970 UTC. It acts on float and histogram samples in the same way.
+
+ >
+ ),
+ ts_of_first_over_time: (
+ <>
+
+ The following functions allow aggregating each series of a given range vector over time and return an instant
+ vector with per-series aggregation results:
+
+
+
+
+ avg_over_time(range-vector): the average value of all float or histogram samples in the specified
+ interval (see details below).
+
+
+ min_over_time(range-vector): the minimum value of all float samples in the specified interval.
+
+
+ max_over_time(range-vector): the maximum value of all float samples in the specified interval.
+
+
+ sum_over_time(range-vector): the sum of all float or histogram samples in the specified interval
+ (see details below).
+
+
+ count_over_time(range-vector): the count of all samples in the specified interval.
+
+
+ quantile_over_time(scalar, range-vector): the φ-quantile (0 ≤ φ ≤ 1) of all float samples in the
+ specified interval.
+
+
+ stddev_over_time(range-vector): the population standard deviation of all float samples in the
+ specified interval.
+
+
+ stdvar_over_time(range-vector): the population standard variance of all float samples in the
+ specified interval.
+
+
+ last_over_time(range-vector): the most recent sample in the specified interval.
+
+
+ present_over_time(range-vector): the value 1 for any series in the specified interval.
+
+
+
+
+ If the feature flag
+ --enable-feature=promql-experimental-functions is set, the following additional functions are
+ available:
+
+
+
+
+ mad_over_time(range-vector): the median absolute deviation of all float samples in the specified
+ interval.
+
+
+ ts_of_min_over_time(range-vector): the timestamp of the last float sample that has the minimum
+ value of all float samples in the specified interval.
+
+
+ ts_of_max_over_time(range-vector): the timestamp of the last float sample that has the maximum
+ value of all float samples in the specified interval.
+
+
+ ts_of_last_over_time(range-vector): the timestamp of last sample in the specified interval.
+
+
+ first_over_time(range-vector): the oldest sample in the specified interval.
+
+
+ ts_of_first_over_time(range-vector): the timestamp of earliest sample in the specified interval.
+
+
+
+
+ Note that all values in the specified interval have the same weight in the aggregation even if the values are
+ not equally spaced throughout the interval.
+
+
+ These functions act on histograms in the following way:
+
+
+
+ count_over_time, first_over_time, last_over_time, and
+ present_over_time() act on float and histogram samples in the same way.
+
+
+ avg_over_time() and sum_over_time() act on histogram samples in a way that
+ corresponds to the respective aggregation operators. If a series contains a mix of float samples and histogram
+ samples within the range, the corresponding result is removed entirely from the output vector. Such a removal
+ is flagged by a warn-level annotation.
+
+
+ All other functions ignore histogram samples in the following way: Input ranges containing only histogram
+ samples are silently removed from the output. For ranges with a mix of histogram and float samples, only the
+ float samples are processed and the omission of the histogram samples is flagged by an info-level annotation.
+
+
+
+
+ first_over_time(m[1m]) differs from m offset 1m in that the former will select the
+ first sample of m within the 1m range, where m offset 1m will select the most
+ recent sample within the lookback interval outside and prior to the 1m offset. This is particularly
+ useful with first_over_time(m[step()])
+ in range queries (available when --enable-feature=promql-duration-expr is set) to ensure that the
+ sample selected is within the range step.
+
+ >
+ ),
+ ts_of_last_over_time: (
+ <>
+
+ The following functions allow aggregating each series of a given range vector over time and return an instant
+ vector with per-series aggregation results:
+
+
+
+
+ avg_over_time(range-vector): the average value of all float or histogram samples in the specified
+ interval (see details below).
+
+
+ min_over_time(range-vector): the minimum value of all float samples in the specified interval.
+
+
+ max_over_time(range-vector): the maximum value of all float samples in the specified interval.
+
+
+ sum_over_time(range-vector): the sum of all float or histogram samples in the specified interval
+ (see details below).
+
+
+ count_over_time(range-vector): the count of all samples in the specified interval.
+
+
+ quantile_over_time(scalar, range-vector): the φ-quantile (0 ≤ φ ≤ 1) of all float samples in the
+ specified interval.
+
+
+ stddev_over_time(range-vector): the population standard deviation of all float samples in the
+ specified interval.
+
+
+ stdvar_over_time(range-vector): the population standard variance of all float samples in the
+ specified interval.
+
+
+ last_over_time(range-vector): the most recent sample in the specified interval.
+
+
+ present_over_time(range-vector): the value 1 for any series in the specified interval.
+
+
+
+
+ If the feature flag
+ --enable-feature=promql-experimental-functions is set, the following additional functions are
+ available:
+
+
+
+
+ mad_over_time(range-vector): the median absolute deviation of all float samples in the specified
+ interval.
+
+
+ ts_of_min_over_time(range-vector): the timestamp of the last float sample that has the minimum
+ value of all float samples in the specified interval.
+
+
+ ts_of_max_over_time(range-vector): the timestamp of the last float sample that has the maximum
+ value of all float samples in the specified interval.
+
+
+ ts_of_last_over_time(range-vector): the timestamp of last sample in the specified interval.
+
+
+ first_over_time(range-vector): the oldest sample in the specified interval.
+
+
+ ts_of_first_over_time(range-vector): the timestamp of earliest sample in the specified interval.
+
+
+
+
+ Note that all values in the specified interval have the same weight in the aggregation even if the values are
+ not equally spaced throughout the interval.
+
+
+ These functions act on histograms in the following way:
+
+
+
+ count_over_time, first_over_time, last_over_time, and
+ present_over_time() act on float and histogram samples in the same way.
+
+
+ avg_over_time() and sum_over_time() act on histogram samples in a way that
+ corresponds to the respective aggregation operators. If a series contains a mix of float samples and histogram
+ samples within the range, the corresponding result is removed entirely from the output vector. Such a removal
+ is flagged by a warn-level annotation.
+
+
+ All other functions ignore histogram samples in the following way: Input ranges containing only histogram
+ samples are silently removed from the output. For ranges with a mix of histogram and float samples, only the
+ float samples are processed and the omission of the histogram samples is flagged by an info-level annotation.
+
+
+
+
+ first_over_time(m[1m]) differs from m offset 1m in that the former will select the
+ first sample of m within the 1m range, where m offset 1m will select the most
+ recent sample within the lookback interval outside and prior to the 1m offset. This is particularly
+ useful with first_over_time(m[step()])
+ in range queries (available when --enable-feature=promql-duration-expr is set) to ensure that the
+ sample selected is within the range step.
+
+ >
+ ),
+ ts_of_max_over_time: (
+ <>
+
+ The following functions allow aggregating each series of a given range vector over time and return an instant
+ vector with per-series aggregation results:
+
+
+
+
+ avg_over_time(range-vector): the average value of all float or histogram samples in the specified
+ interval (see details below).
+
+
+ min_over_time(range-vector): the minimum value of all float samples in the specified interval.
+
+
+ max_over_time(range-vector): the maximum value of all float samples in the specified interval.
+
+
+ sum_over_time(range-vector): the sum of all float or histogram samples in the specified interval
+ (see details below).
+
+
+ count_over_time(range-vector): the count of all samples in the specified interval.
+
+
+ quantile_over_time(scalar, range-vector): the φ-quantile (0 ≤ φ ≤ 1) of all float samples in the
+ specified interval.
+
+
+ stddev_over_time(range-vector): the population standard deviation of all float samples in the
+ specified interval.
+
+
+ stdvar_over_time(range-vector): the population standard variance of all float samples in the
+ specified interval.
+
+
+ last_over_time(range-vector): the most recent sample in the specified interval.
+
+
+ present_over_time(range-vector): the value 1 for any series in the specified interval.
+
+
+
+
+ If the feature flag
+ --enable-feature=promql-experimental-functions is set, the following additional functions are
+ available:
+
+
+
+
+ mad_over_time(range-vector): the median absolute deviation of all float samples in the specified
+ interval.
+
+
+ ts_of_min_over_time(range-vector): the timestamp of the last float sample that has the minimum
+ value of all float samples in the specified interval.
+
+
+ ts_of_max_over_time(range-vector): the timestamp of the last float sample that has the maximum
+ value of all float samples in the specified interval.
+
+
+ ts_of_last_over_time(range-vector): the timestamp of last sample in the specified interval.
+
+
+ first_over_time(range-vector): the oldest sample in the specified interval.
+
+
+ ts_of_first_over_time(range-vector): the timestamp of earliest sample in the specified interval.
+
+
+
+
+ Note that all values in the specified interval have the same weight in the aggregation even if the values are
+ not equally spaced throughout the interval.
+
+
+ These functions act on histograms in the following way:
+
+
+
+ count_over_time, first_over_time, last_over_time, and
+ present_over_time() act on float and histogram samples in the same way.
+
+
+ avg_over_time() and sum_over_time() act on histogram samples in a way that
+ corresponds to the respective aggregation operators. If a series contains a mix of float samples and histogram
+ samples within the range, the corresponding result is removed entirely from the output vector. Such a removal
+ is flagged by a warn-level annotation.
+
+
+ All other functions ignore histogram samples in the following way: Input ranges containing only histogram
+ samples are silently removed from the output. For ranges with a mix of histogram and float samples, only the
+ float samples are processed and the omission of the histogram samples is flagged by an info-level annotation.
+
+
+
+
+ first_over_time(m[1m]) differs from m offset 1m in that the former will select the
+ first sample of m within the 1m range, where m offset 1m will select the most
+ recent sample within the lookback interval outside and prior to the 1m offset. This is particularly
+ useful with first_over_time(m[step()])
+ in range queries (available when --enable-feature=promql-duration-expr is set) to ensure that the
+ sample selected is within the range step.
+
+ >
+ ),
+ ts_of_min_over_time: (
+ <>
+
+ The following functions allow aggregating each series of a given range vector over time and return an instant
+ vector with per-series aggregation results:
+
+
+
+
+ avg_over_time(range-vector): the average value of all float or histogram samples in the specified
+ interval (see details below).
+
+
+ min_over_time(range-vector): the minimum value of all float samples in the specified interval.
+
+
+ max_over_time(range-vector): the maximum value of all float samples in the specified interval.
+
+
+ sum_over_time(range-vector): the sum of all float or histogram samples in the specified interval
+ (see details below).
+
+
+ count_over_time(range-vector): the count of all samples in the specified interval.
+
+
+ quantile_over_time(scalar, range-vector): the φ-quantile (0 ≤ φ ≤ 1) of all float samples in the
+ specified interval.
+
+
+ stddev_over_time(range-vector): the population standard deviation of all float samples in the
+ specified interval.
+
+
+ stdvar_over_time(range-vector): the population standard variance of all float samples in the
+ specified interval.
+
+
+ last_over_time(range-vector): the most recent sample in the specified interval.
+
+
+ present_over_time(range-vector): the value 1 for any series in the specified interval.
+
+
+
+
+ If the feature flag
+ --enable-feature=promql-experimental-functions is set, the following additional functions are
+ available:
+
+
+
+
+ mad_over_time(range-vector): the median absolute deviation of all float samples in the specified
+ interval.
+
+
+ ts_of_min_over_time(range-vector): the timestamp of the last float sample that has the minimum
+ value of all float samples in the specified interval.
+
+
+ ts_of_max_over_time(range-vector): the timestamp of the last float sample that has the maximum
+ value of all float samples in the specified interval.
+
+
+ ts_of_last_over_time(range-vector): the timestamp of last sample in the specified interval.
+
+
+ first_over_time(range-vector): the oldest sample in the specified interval.
+
+
+ ts_of_first_over_time(range-vector): the timestamp of earliest sample in the specified interval.
+
+
+
+
+ Note that all values in the specified interval have the same weight in the aggregation even if the values are
+ not equally spaced throughout the interval.
+
+
+ These functions act on histograms in the following way:
+
+
+
+ count_over_time, first_over_time, last_over_time, and
+ present_over_time() act on float and histogram samples in the same way.
+
+
+ avg_over_time() and sum_over_time() act on histogram samples in a way that
+ corresponds to the respective aggregation operators. If a series contains a mix of float samples and histogram
+ samples within the range, the corresponding result is removed entirely from the output vector. Such a removal
+ is flagged by a warn-level annotation.
+
+
+ All other functions ignore histogram samples in the following way: Input ranges containing only histogram
+ samples are silently removed from the output. For ranges with a mix of histogram and float samples, only the
+ float samples are processed and the omission of the histogram samples is flagged by an info-level annotation.
+
+
+
+
+ first_over_time(m[1m]) differs from m offset 1m in that the former will select the
+ first sample of m within the 1m range, where m offset 1m will select the most
+ recent sample within the lookback interval outside and prior to the 1m offset. This is particularly
+ useful with first_over_time(m[step()])
+ in range queries (available when --enable-feature=promql-duration-expr is set) to ensure that the
+ sample selected is within the range step.
>
),
vector: (
<>
- vector(s scalar) returns the scalar s as a vector with no labels.
+ vector(s scalar) converts the scalar s to a float sample and returns it as a
+ single-element instant vector with no labels.
>
),
@@ -2719,6 +4041,7 @@ const funcDocs: Record = {
<>
year(v=vector(time()) instant-vector) returns the year for each of the given times in UTC.
+ Histogram samples in the input vector are ignored silently.
>
),
diff --git a/web/ui/mantine-ui/src/promql/functionSignatures.ts b/web/ui/mantine-ui/src/promql/functionSignatures.ts
index 472d54ac5a..837a271dce 100644
--- a/web/ui/mantine-ui/src/promql/functionSignatures.ts
+++ b/web/ui/mantine-ui/src/promql/functionSignatures.ts
@@ -1,140 +1,202 @@
-import { valueType, Func } from './ast';
+import { valueType, Func } from "./ast";
export const functionSignatures: Record = {
- abs: { name: 'abs', argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
- absent: { name: 'absent', argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
- absent_over_time: { name: 'absent_over_time', argTypes: [valueType.matrix], variadic: 0, returnType: valueType.vector },
- acos: { name: 'acos', argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
- acosh: { name: 'acosh', argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
- asin: { name: 'asin', argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
- asinh: { name: 'asinh', argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
- atan: { name: 'atan', argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
- atanh: { name: 'atanh', argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
- avg_over_time: { name: 'avg_over_time', argTypes: [valueType.matrix], variadic: 0, returnType: valueType.vector },
- ceil: { name: 'ceil', argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
- changes: { name: 'changes', argTypes: [valueType.matrix], variadic: 0, returnType: valueType.vector },
+ abs: { name: "abs", argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
+ absent: { name: "absent", argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
+ absent_over_time: {
+ name: "absent_over_time",
+ argTypes: [valueType.matrix],
+ variadic: 0,
+ returnType: valueType.vector,
+ },
+ acos: { name: "acos", argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
+ acosh: { name: "acosh", argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
+ asin: { name: "asin", argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
+ asinh: { name: "asinh", argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
+ atan: { name: "atan", argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
+ atanh: { name: "atanh", argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
+ avg_over_time: { name: "avg_over_time", argTypes: [valueType.matrix], variadic: 0, returnType: valueType.vector },
+ ceil: { name: "ceil", argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
+ changes: { name: "changes", argTypes: [valueType.matrix], variadic: 0, returnType: valueType.vector },
clamp: {
- name: 'clamp',
+ name: "clamp",
argTypes: [valueType.vector, valueType.scalar, valueType.scalar],
variadic: 0,
returnType: valueType.vector,
},
clamp_max: {
- name: 'clamp_max',
+ name: "clamp_max",
argTypes: [valueType.vector, valueType.scalar],
variadic: 0,
returnType: valueType.vector,
},
clamp_min: {
- name: 'clamp_min',
+ name: "clamp_min",
argTypes: [valueType.vector, valueType.scalar],
variadic: 0,
returnType: valueType.vector,
},
- cos: { name: 'cos', argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
- cosh: { name: 'cosh', argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
- count_over_time: { name: 'count_over_time', argTypes: [valueType.matrix], variadic: 0, returnType: valueType.vector },
- day_of_month: { name: 'day_of_month', argTypes: [valueType.vector], variadic: 1, returnType: valueType.vector },
- day_of_week: { name: 'day_of_week', argTypes: [valueType.vector], variadic: 1, returnType: valueType.vector },
- day_of_year: { name: 'day_of_year', argTypes: [valueType.vector], variadic: 1, returnType: valueType.vector },
- days_in_month: { name: 'days_in_month', argTypes: [valueType.vector], variadic: 1, returnType: valueType.vector },
- deg: { name: 'deg', argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
- delta: { name: 'delta', argTypes: [valueType.matrix], variadic: 0, returnType: valueType.vector },
- deriv: { name: 'deriv', argTypes: [valueType.matrix], variadic: 0, returnType: valueType.vector },
- exp: { name: 'exp', argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
- floor: { name: 'floor', argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
- histogram_avg: { name: 'histogram_avg', argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
- histogram_count: { name: 'histogram_count', argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
+ cos: { name: "cos", argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
+ cosh: { name: "cosh", argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
+ count_over_time: { name: "count_over_time", argTypes: [valueType.matrix], variadic: 0, returnType: valueType.vector },
+ day_of_month: { name: "day_of_month", argTypes: [valueType.vector], variadic: 1, returnType: valueType.vector },
+ day_of_week: { name: "day_of_week", argTypes: [valueType.vector], variadic: 1, returnType: valueType.vector },
+ day_of_year: { name: "day_of_year", argTypes: [valueType.vector], variadic: 1, returnType: valueType.vector },
+ days_in_month: { name: "days_in_month", argTypes: [valueType.vector], variadic: 1, returnType: valueType.vector },
+ deg: { name: "deg", argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
+ delta: { name: "delta", argTypes: [valueType.matrix], variadic: 0, returnType: valueType.vector },
+ deriv: { name: "deriv", argTypes: [valueType.matrix], variadic: 0, returnType: valueType.vector },
+ double_exponential_smoothing: {
+ name: "double_exponential_smoothing",
+ argTypes: [valueType.matrix, valueType.scalar, valueType.scalar],
+ variadic: 0,
+ returnType: valueType.vector,
+ },
+ exp: { name: "exp", argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
+ first_over_time: { name: "first_over_time", argTypes: [valueType.matrix], variadic: 0, returnType: valueType.vector },
+ floor: { name: "floor", argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
+ histogram_avg: { name: "histogram_avg", argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
+ histogram_count: { name: "histogram_count", argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
histogram_fraction: {
- name: 'histogram_fraction',
+ name: "histogram_fraction",
argTypes: [valueType.scalar, valueType.scalar, valueType.vector],
variadic: 0,
returnType: valueType.vector,
},
histogram_quantile: {
- name: 'histogram_quantile',
+ name: "histogram_quantile",
argTypes: [valueType.scalar, valueType.vector],
variadic: 0,
returnType: valueType.vector,
},
- histogram_stddev: { name: 'histogram_stddev', argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
- histogram_stdvar: { name: 'histogram_stdvar', argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
- histogram_sum: { name: 'histogram_sum', argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
- double_exponential_smoothing: {
- name: 'double_exponential_smoothing',
- argTypes: [valueType.matrix, valueType.scalar, valueType.scalar],
+ histogram_quantiles: {
+ name: "histogram_quantiles",
+ argTypes: [valueType.vector, valueType.string, valueType.scalar, valueType.scalar],
+ variadic: 9,
+ returnType: valueType.vector,
+ },
+ histogram_stddev: {
+ name: "histogram_stddev",
+ argTypes: [valueType.vector],
variadic: 0,
returnType: valueType.vector,
},
- hour: { name: 'hour', argTypes: [valueType.vector], variadic: 1, returnType: valueType.vector },
- idelta: { name: 'idelta', argTypes: [valueType.matrix], variadic: 0, returnType: valueType.vector },
- increase: { name: 'increase', argTypes: [valueType.matrix], variadic: 0, returnType: valueType.vector },
- irate: { name: 'irate', argTypes: [valueType.matrix], variadic: 0, returnType: valueType.vector },
+ histogram_stdvar: {
+ name: "histogram_stdvar",
+ argTypes: [valueType.vector],
+ variadic: 0,
+ returnType: valueType.vector,
+ },
+ histogram_sum: { name: "histogram_sum", argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
+ hour: { name: "hour", argTypes: [valueType.vector], variadic: 1, returnType: valueType.vector },
+ idelta: { name: "idelta", argTypes: [valueType.matrix], variadic: 0, returnType: valueType.vector },
+ increase: { name: "increase", argTypes: [valueType.matrix], variadic: 0, returnType: valueType.vector },
+ info: { name: "info", argTypes: [valueType.vector, valueType.vector], variadic: 1, returnType: valueType.vector },
+ irate: { name: "irate", argTypes: [valueType.matrix], variadic: 0, returnType: valueType.vector },
label_join: {
- name: 'label_join',
+ name: "label_join",
argTypes: [valueType.vector, valueType.string, valueType.string, valueType.string],
variadic: -1,
returnType: valueType.vector,
},
label_replace: {
- name: 'label_replace',
+ name: "label_replace",
argTypes: [valueType.vector, valueType.string, valueType.string, valueType.string, valueType.string],
variadic: 0,
returnType: valueType.vector,
},
- last_over_time: { name: 'last_over_time', argTypes: [valueType.matrix], variadic: 0, returnType: valueType.vector },
- ln: { name: 'ln', argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
- log10: { name: 'log10', argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
- log2: { name: 'log2', argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
- mad_over_time: { name: 'mad_over_time', argTypes: [valueType.matrix], variadic: 0, returnType: valueType.vector },
- max_over_time: { name: 'max_over_time', argTypes: [valueType.matrix], variadic: 0, returnType: valueType.vector },
- min_over_time: { name: 'min_over_time', argTypes: [valueType.matrix], variadic: 0, returnType: valueType.vector },
- minute: { name: 'minute', argTypes: [valueType.vector], variadic: 1, returnType: valueType.vector },
- month: { name: 'month', argTypes: [valueType.vector], variadic: 1, returnType: valueType.vector },
- pi: { name: 'pi', argTypes: [], variadic: 0, returnType: valueType.scalar },
+ last_over_time: { name: "last_over_time", argTypes: [valueType.matrix], variadic: 0, returnType: valueType.vector },
+ ln: { name: "ln", argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
+ log10: { name: "log10", argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
+ log2: { name: "log2", argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
+ mad_over_time: { name: "mad_over_time", argTypes: [valueType.matrix], variadic: 0, returnType: valueType.vector },
+ max_over_time: { name: "max_over_time", argTypes: [valueType.matrix], variadic: 0, returnType: valueType.vector },
+ min_over_time: { name: "min_over_time", argTypes: [valueType.matrix], variadic: 0, returnType: valueType.vector },
+ minute: { name: "minute", argTypes: [valueType.vector], variadic: 1, returnType: valueType.vector },
+ month: { name: "month", argTypes: [valueType.vector], variadic: 1, returnType: valueType.vector },
+ pi: { name: "pi", argTypes: [], variadic: 0, returnType: valueType.scalar },
predict_linear: {
- name: 'predict_linear',
+ name: "predict_linear",
argTypes: [valueType.matrix, valueType.scalar],
variadic: 0,
returnType: valueType.vector,
},
- present_over_time: { name: 'present_over_time', argTypes: [valueType.matrix], variadic: 0, returnType: valueType.vector },
+ present_over_time: {
+ name: "present_over_time",
+ argTypes: [valueType.matrix],
+ variadic: 0,
+ returnType: valueType.vector,
+ },
quantile_over_time: {
- name: 'quantile_over_time',
+ name: "quantile_over_time",
argTypes: [valueType.scalar, valueType.matrix],
variadic: 0,
returnType: valueType.vector,
},
- rad: { name: 'rad', argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
- rate: { name: 'rate', argTypes: [valueType.matrix], variadic: 0, returnType: valueType.vector },
- resets: { name: 'resets', argTypes: [valueType.matrix], variadic: 0, returnType: valueType.vector },
- round: { name: 'round', argTypes: [valueType.vector, valueType.scalar], variadic: 1, returnType: valueType.vector },
- scalar: { name: 'scalar', argTypes: [valueType.vector], variadic: 0, returnType: valueType.scalar },
- sgn: { name: 'sgn', argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
- sin: { name: 'sin', argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
- sinh: { name: 'sinh', argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
- sort: { name: 'sort', argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
+ rad: { name: "rad", argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
+ rate: { name: "rate", argTypes: [valueType.matrix], variadic: 0, returnType: valueType.vector },
+ resets: { name: "resets", argTypes: [valueType.matrix], variadic: 0, returnType: valueType.vector },
+ round: { name: "round", argTypes: [valueType.vector, valueType.scalar], variadic: 1, returnType: valueType.vector },
+ scalar: { name: "scalar", argTypes: [valueType.vector], variadic: 0, returnType: valueType.scalar },
+ sgn: { name: "sgn", argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
+ sin: { name: "sin", argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
+ sinh: { name: "sinh", argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
+ sort: { name: "sort", argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
sort_by_label: {
- name: 'sort_by_label',
+ name: "sort_by_label",
argTypes: [valueType.vector, valueType.string],
variadic: -1,
returnType: valueType.vector,
},
sort_by_label_desc: {
- name: 'sort_by_label_desc',
+ name: "sort_by_label_desc",
argTypes: [valueType.vector, valueType.string],
variadic: -1,
returnType: valueType.vector,
},
- sort_desc: { name: 'sort_desc', argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
- sqrt: { name: 'sqrt', argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
- stddev_over_time: { name: 'stddev_over_time', argTypes: [valueType.matrix], variadic: 0, returnType: valueType.vector },
- stdvar_over_time: { name: 'stdvar_over_time', argTypes: [valueType.matrix], variadic: 0, returnType: valueType.vector },
- sum_over_time: { name: 'sum_over_time', argTypes: [valueType.matrix], variadic: 0, returnType: valueType.vector },
- tan: { name: 'tan', argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
- tanh: { name: 'tanh', argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
- time: { name: 'time', argTypes: [], variadic: 0, returnType: valueType.scalar },
- timestamp: { name: 'timestamp', argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
- vector: { name: 'vector', argTypes: [valueType.scalar], variadic: 0, returnType: valueType.vector },
- year: { name: 'year', argTypes: [valueType.vector], variadic: 1, returnType: valueType.vector },
+ sort_desc: { name: "sort_desc", argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
+ sqrt: { name: "sqrt", argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
+ stddev_over_time: {
+ name: "stddev_over_time",
+ argTypes: [valueType.matrix],
+ variadic: 0,
+ returnType: valueType.vector,
+ },
+ stdvar_over_time: {
+ name: "stdvar_over_time",
+ argTypes: [valueType.matrix],
+ variadic: 0,
+ returnType: valueType.vector,
+ },
+ sum_over_time: { name: "sum_over_time", argTypes: [valueType.matrix], variadic: 0, returnType: valueType.vector },
+ tan: { name: "tan", argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
+ tanh: { name: "tanh", argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
+ time: { name: "time", argTypes: [], variadic: 0, returnType: valueType.scalar },
+ timestamp: { name: "timestamp", argTypes: [valueType.vector], variadic: 0, returnType: valueType.vector },
+ ts_of_first_over_time: {
+ name: "ts_of_first_over_time",
+ argTypes: [valueType.matrix],
+ variadic: 0,
+ returnType: valueType.vector,
+ },
+ ts_of_last_over_time: {
+ name: "ts_of_last_over_time",
+ argTypes: [valueType.matrix],
+ variadic: 0,
+ returnType: valueType.vector,
+ },
+ ts_of_max_over_time: {
+ name: "ts_of_max_over_time",
+ argTypes: [valueType.matrix],
+ variadic: 0,
+ returnType: valueType.vector,
+ },
+ ts_of_min_over_time: {
+ name: "ts_of_min_over_time",
+ argTypes: [valueType.matrix],
+ variadic: 0,
+ returnType: valueType.vector,
+ },
+ vector: { name: "vector", argTypes: [valueType.scalar], variadic: 0, returnType: valueType.vector },
+ year: { name: "year", argTypes: [valueType.vector], variadic: 1, returnType: valueType.vector },
};
diff --git a/web/ui/mantine-ui/src/promql/serialize.ts b/web/ui/mantine-ui/src/promql/serialize.ts
index bbccede708..50c32c49e4 100644
--- a/web/ui/mantine-ui/src/promql/serialize.ts
+++ b/web/ui/mantine-ui/src/promql/serialize.ts
@@ -135,12 +135,16 @@ const serializeNode = (
case nodeType.binaryExpr: {
let matching = "";
let grouping = "";
+ let fill = "";
const vm = node.matching;
- if (vm !== null && (vm.labels.length > 0 || vm.on)) {
- if (vm.on) {
- matching = ` on(${labelNameList(vm.labels)})`;
- } else {
- matching = ` ignoring(${labelNameList(vm.labels)})`;
+ if (vm !== null) {
+ if (
+ vm.labels.length > 0 ||
+ vm.on ||
+ vm.card === vectorMatchCardinality.manyToOne ||
+ vm.card === vectorMatchCardinality.oneToMany
+ ) {
+ matching = ` ${vm.on ? "on" : "ignoring"}(${labelNameList(vm.labels)})`;
}
if (
@@ -149,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 62b10cd781..f9ff039882 100644
--- a/web/ui/mantine-ui/src/promql/serializeAndFormat.test.ts
+++ b/web/ui/mantine-ui/src/promql/serializeAndFormat.test.ts
@@ -192,8 +192,7 @@ describe("serializeNode and formatNode", () => {
anchored: false,
smoothed: false,
},
- output:
- '{label1="value1"}',
+ output: '{label1="value1"}',
},
// Anchored and smoothed modifiers.
@@ -659,6 +658,7 @@ describe("serializeNode and formatNode", () => {
labels: [],
on: false,
include: [],
+ fillValues: { lhs: null, rhs: null },
},
bool: false,
},
@@ -678,6 +678,7 @@ describe("serializeNode and formatNode", () => {
labels: [],
on: true,
include: [],
+ fillValues: { lhs: null, rhs: null },
},
bool: false,
},
@@ -697,6 +698,7 @@ describe("serializeNode and formatNode", () => {
labels: ["label1", "label2"],
on: true,
include: [],
+ fillValues: { lhs: null, rhs: null },
},
bool: false,
},
@@ -716,12 +718,55 @@ describe("serializeNode and formatNode", () => {
labels: ["label1", "label2"],
on: false,
include: [],
+ fillValues: { lhs: null, rhs: null },
},
bool: false,
},
output: "… + ignoring(label1, label2) …",
prettyOutput: ` …
+ ignoring(label1, label2)
+ …`,
+ },
+ {
+ // Empty ignoring() without group modifiers can be stripped away.
+ node: {
+ type: nodeType.binaryExpr,
+ op: binaryOperatorType.add,
+ lhs: { type: nodeType.placeholder, children: [] },
+ rhs: { type: nodeType.placeholder, children: [] },
+ matching: {
+ card: vectorMatchCardinality.oneToOne,
+ labels: [],
+ on: false,
+ include: [],
+ fillValues: { lhs: null, rhs: null },
+ },
+ bool: false,
+ },
+ output: "… + …",
+ prettyOutput: ` …
++
+ …`,
+ },
+ {
+ // Empty ignoring() with group modifiers may not be stripped away.
+ node: {
+ type: nodeType.binaryExpr,
+ op: binaryOperatorType.add,
+ lhs: { type: nodeType.placeholder, children: [] },
+ rhs: { type: nodeType.placeholder, children: [] },
+ matching: {
+ card: vectorMatchCardinality.manyToOne,
+ labels: [],
+ on: false,
+ include: ["__name__"],
+ fillValues: { lhs: null, rhs: null },
+ },
+ bool: false,
+ },
+ output: "… + ignoring() group_left(__name__) …",
+ prettyOutput: ` …
++ ignoring() group_left(__name__)
…`,
},
{
@@ -735,6 +780,7 @@ describe("serializeNode and formatNode", () => {
labels: ["label1", "label2"],
on: true,
include: [],
+ fillValues: { lhs: null, rhs: null },
},
bool: false,
},
@@ -754,6 +800,7 @@ describe("serializeNode and formatNode", () => {
labels: ["label1", "label2"],
on: true,
include: ["label3"],
+ fillValues: { lhs: null, rhs: null },
},
bool: false,
},
@@ -773,6 +820,7 @@ describe("serializeNode and formatNode", () => {
labels: ["label1", "label2"],
on: true,
include: [],
+ fillValues: { lhs: null, rhs: null },
},
bool: false,
},
@@ -792,6 +840,7 @@ describe("serializeNode and formatNode", () => {
labels: ["label1", "label2"],
on: true,
include: ["label3"],
+ fillValues: { lhs: null, rhs: null },
},
bool: false,
},
@@ -825,6 +874,7 @@ describe("serializeNode and formatNode", () => {
labels: ["label1", "label2"],
on: true,
include: ["label3"],
+ fillValues: { lhs: null, rhs: null },
},
bool: true,
},
@@ -872,6 +922,7 @@ describe("serializeNode and formatNode", () => {
include: ["c", "ü"],
labels: ["b", "ö"],
on: true,
+ fillValues: { lhs: null, rhs: null },
},
op: binaryOperatorType.div,
rhs: {
@@ -909,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/gen_functions_docs/main.go b/web/ui/mantine-ui/src/promql/tools/gen_functions_docs/main.go
index 89545c1e5e..74e8ac0354 100644
--- a/web/ui/mantine-ui/src/promql/tools/gen_functions_docs/main.go
+++ b/web/ui/mantine-ui/src/promql/tools/gen_functions_docs/main.go
@@ -1,4 +1,4 @@
-// Copyright 2022 The Prometheus Authors
+// 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
@@ -18,7 +18,7 @@ import (
"fmt"
"io"
"log"
- "net/http"
+ "os"
"sort"
"strings"
@@ -26,20 +26,23 @@ import (
"github.com/russross/blackfriday/v2"
)
-var funcDocsRe = regexp.MustCompile("^## `(.+)\\(\\)`\n$|^## (Trigonometric Functions)\n$")
+var funcDocsRe = regexp.MustCompile("^## `([^)]+)\\(\\)` and `([^)]+)\\(\\)`\n$|^## `(.+)\\(\\)`\n$|^## (Trigonometric Functions)\n$")
func main() {
- resp, err := http.Get("https://raw.githubusercontent.com/prometheus/prometheus/master/docs/querying/functions.md")
+ // Read from local file instead of fetching from upstream.
+ if len(os.Args) < 2 {
+ log.Fatalln("Usage: gen_functions_docs ")
+ }
+ functionsPath := os.Args[1]
+ file, err := os.Open(functionsPath)
if err != nil {
- log.Fatalln("Failed to fetch function docs:", err)
- }
- if resp.StatusCode != 200 {
- log.Fatalln("Bad status code while fetching function docs:", resp.Status)
+ log.Fatalln("Failed to open function docs:", err)
}
+ defer file.Close()
funcDocs := map[string]string{}
- r := bufio.NewReader(resp.Body)
+ r := bufio.NewReader(file)
currentFunc := ""
currentDocs := ""
@@ -58,6 +61,11 @@ func main() {
"last_over_time",
"present_over_time",
"mad_over_time",
+ "first_over_time",
+ "ts_of_first_over_time",
+ "ts_of_last_over_time",
+ "ts_of_max_over_time",
+ "ts_of_min_over_time",
} {
funcDocs[fn] = currentDocs
}
@@ -81,6 +89,12 @@ func main() {
} {
funcDocs[fn] = currentDocs
}
+ case "histogram_count_and_histogram_sum":
+ funcDocs["histogram_count"] = currentDocs
+ funcDocs["histogram_sum"] = currentDocs
+ case "histogram_stddev_and_histogram_stdvar":
+ funcDocs["histogram_stddev"] = currentDocs
+ funcDocs["histogram_stdvar"] = currentDocs
default:
funcDocs[currentFunc] = currentDocs
}
@@ -103,10 +117,16 @@ func main() {
}
currentDocs = ""
- currentFunc = string(matches[1])
- if matches[2] != "" {
- // This is the case for "## Trigonometric Functions"
- currentFunc = matches[2]
+ if matches[1] != "" && matches[2] != "" {
+ // Combined functions: "## `function1()` and `function2()`"
+ // Store as "function1_and_function2" and handle in saveCurrent.
+ currentFunc = matches[1] + "_and_" + matches[2]
+ } else if matches[3] != "" {
+ // Single function: "## `function_name()`"
+ currentFunc = string(matches[3])
+ } else if matches[4] != "" {
+ // Special section: "## Trigonometric Functions"
+ currentFunc = matches[4]
}
} else {
currentDocs += line
diff --git a/web/ui/mantine-ui/src/promql/tools/gen_functions_list/main.go b/web/ui/mantine-ui/src/promql/tools/gen_functions_list/main.go
index f479b6d36a..6b77f368c8 100644
--- a/web/ui/mantine-ui/src/promql/tools/gen_functions_list/main.go
+++ b/web/ui/mantine-ui/src/promql/tools/gen_functions_list/main.go
@@ -1,4 +1,4 @@
-// Copyright 2022 The Prometheus Authors
+// 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
@@ -41,10 +41,10 @@ func main() {
sort.Strings(fnNames)
fmt.Println(`import { valueType, Func } from './ast';
- export const functionSignatures: Record = {`)
+export const functionSignatures: Record = {`)
for _, fnName := range fnNames {
fn := parser.Functions[fnName]
fmt.Printf(" %s: { name: '%s', argTypes: [%s], variadic: %d, returnType: %s },\n", fn.Name, fn.Name, formatValueTypes(fn.ArgTypes), fn.Variadic, formatValueType(fn.ReturnType))
}
- fmt.Println("}")
+ fmt.Println("};")
}
diff --git a/web/ui/mantine-ui/src/promql/tools/go.mod b/web/ui/mantine-ui/src/promql/tools/go.mod
index 6983cf4fe6..af604b1964 100644
--- a/web/ui/mantine-ui/src/promql/tools/go.mod
+++ b/web/ui/mantine-ui/src/promql/tools/go.mod
@@ -1,26 +1,41 @@
module github.com/prometheus/prometheus/web/ui/mantine-ui/src/promql/tools
-go 1.24.0
+go 1.25.5
require (
- github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc
- github.com/prometheus/prometheus v0.54.1
+ github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853
+ github.com/prometheus/prometheus v0.309.1
github.com/russross/blackfriday/v2 v2.1.0
)
require (
+ github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dennwc/varint v1.0.0 // indirect
- github.com/go-kit/log v0.2.1 // indirect
- github.com/go-logfmt/logfmt v0.6.0 // indirect
+ github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
+ github.com/klauspost/compress v1.18.4 // indirect
+ github.com/kr/text v0.2.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
- github.com/prometheus/client_golang v1.19.1 // indirect
- github.com/prometheus/client_model v0.6.1 // indirect
- github.com/prometheus/common v0.55.0 // indirect
- github.com/prometheus/procfs v0.15.1 // indirect
+ github.com/prometheus/client_golang v1.23.2 // indirect
+ github.com/prometheus/client_golang/exp v0.0.0-20260108101519-fb0838f53562 // indirect
+ github.com/prometheus/client_model v0.6.2 // indirect
+ github.com/prometheus/common v0.67.5 // indirect
+ github.com/prometheus/procfs v0.16.1 // indirect
+ github.com/prometheus/sigv4 v0.4.1 // indirect
+ github.com/rogpeppe/go-internal v1.14.1 // indirect
+ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
- golang.org/x/sys v0.22.0 // indirect
- golang.org/x/text v0.16.0 // indirect
- google.golang.org/protobuf v1.34.2 // indirect
+ go.yaml.in/yaml/v2 v2.4.3 // indirect
+ golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect
+ golang.org/x/net v0.49.0 // indirect
+ golang.org/x/oauth2 v0.35.0 // indirect
+ golang.org/x/sys v0.41.0 // indirect
+ golang.org/x/text v0.34.0 // indirect
+ google.golang.org/api v0.266.0 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
+ google.golang.org/protobuf v1.36.11 // indirect
+ k8s.io/client-go v0.35.0 // indirect
)
+
+replace cloud.google.com/go => cloud.google.com/go v0.123.0
diff --git a/web/ui/mantine-ui/src/promql/tools/go.sum b/web/ui/mantine-ui/src/promql/tools/go.sum
index e7ed7cec79..6f43b2da98 100644
--- a/web/ui/mantine-ui/src/promql/tools/go.sum
+++ b/web/ui/mantine-ui/src/promql/tools/go.sum
@@ -1,47 +1,88 @@
-github.com/Azure/azure-sdk-for-go/sdk/azcore v1.13.0 h1:GJHeeA2N7xrG3q30L2UXDyuWRzDM900/65j70wcM4Ww=
-github.com/Azure/azure-sdk-for-go/sdk/azcore v1.13.0/go.mod h1:l38EPgmsp71HHLq9j7De57JcKOWPyhrsW1Awm1JS6K0=
-github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 h1:tfLQ34V6F7tVSwoTf/4lH5sE0o6eCJuNDTmH09nDpbc=
-github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg=
-github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY=
-github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY=
-github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU=
-github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
-github.com/alecthomas/units v0.0.0-20240626203959-61d1e3462e30 h1:t3eaIm0rUkzbrIewtiFmMK5RXHej2XnoXNhxVsAYUfg=
-github.com/alecthomas/units v0.0.0-20240626203959-61d1e3462e30/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs=
-github.com/aws/aws-sdk-go v1.54.19 h1:tyWV+07jagrNiCcGRzRhdtVjQs7Vy41NwsuOcl0IbVI=
-github.com/aws/aws-sdk-go v1.54.19/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
+cloud.google.com/go/auth v0.18.1 h1:IwTEx92GFUo2pJ6Qea0EU3zYvKnTAeRCODxfA/G5UWs=
+cloud.google.com/go/auth v0.18.1/go.mod h1:GfTYoS9G3CWpRA3Va9doKN9mjPGRS+v41jmZAhBzbrA=
+cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
+cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
+cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
+cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI=
+github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs=
+github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
+github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0=
+github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs=
+github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU=
+github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
+github.com/aws/aws-sdk-go-v2/config v1.32.7 h1:vxUyWGUwmkQ2g19n7JY/9YL8MfAIl7bTesIUykECXmY=
+github.com/aws/aws-sdk-go-v2/config v1.32.7/go.mod h1:2/Qm5vKUU/r7Y+zUk/Ptt2MDAEKAfUtKc1+3U1Mo3oY=
+github.com/aws/aws-sdk-go-v2/credentials v1.19.7 h1:tHK47VqqtJxOymRrNtUXN5SP/zUTvZKeLx4tH6PGQc8=
+github.com/aws/aws-sdk-go-v2/credentials v1.19.7/go.mod h1:qOZk8sPDrxhf+4Wf4oT2urYJrYt3RejHSzgAquYeppw=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 h1:I0GyV8wiYrP8XpA70g1HBcQO1JlQxCMTW9npl5UbDHY=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17/go.mod h1:tyw7BOl5bBe/oqvoIeECFJjMdzXoa/dfVz3QQ5lgHGA=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17/go.mod h1:5M5CI3D12dNOtH3/mk6minaRwI2/37ifCURZISxA/IQ=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 h1:WWLqlh79iO48yLkj1v3ISRNiv+3KdQoZ6JWyfcsyQik=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17/go.mod h1:EhG22vHRrvF8oXSTYStZhJc1aUgKtnJe+aOiFEV90cM=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 h1:RuNSMoozM8oXlgLG/n6WLaFGoea7/CddrCfIiSA+xdY=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17/go.mod h1:F2xxQ9TZz5gDWsclCtPQscGpP0VUOc8RqgFM3vDENmU=
+github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 h1:VrhDvQib/i0lxvr3zqlUwLwJP4fpmpyD9wYG1vfSu+Y=
+github.com/aws/aws-sdk-go-v2/service/signin v1.0.5/go.mod h1:k029+U8SY30/3/ras4G/Fnv/b88N4mAfliNn08Dem4M=
+github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 h1:v6EiMvhEYBoHABfbGB4alOYmCIrcgyPPiBE1wZAEbqk=
+github.com/aws/aws-sdk-go-v2/service/sso v1.30.9/go.mod h1:yifAsgBxgJWn3ggx70A3urX2AN49Y5sJTD1UQFlfqBw=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 h1:gd84Omyu9JLriJVCbGApcLzVR3XtmC4ZDPcAI6Ftvds=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13/go.mod h1:sTGThjphYE4Ohw8vJiRStAcu3rbjtXRsdNB0TvZ5wwo=
+github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/pJ1jOWYlFDJTjRQ=
+github.com/aws/aws-sdk-go-v2/service/sts v1.41.6/go.mod h1:qgFDZQSD/Kys7nJnVqYlWKnh0SSdMjAi0uSwON4wgYQ=
+github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
+github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 h1:6df1vn4bBlDDo4tARvBm7l6KA9iVMnE3NWizDeWSrps=
github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3/go.mod h1:CIWtjkly68+yqLPbvwwR/fjNJA/idrtULjZWh2v1ys0=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dennwc/varint v1.0.0 h1:kGNFFSSw8ToIy3obO/kKr8U9GZYUAxQEVuix4zfDWzE=
github.com/dennwc/varint v1.0.0/go.mod h1:hnItb35rvZvJrbTALZtY/iQfDs48JKRG1RPpgziApxA=
-github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU=
-github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
-github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
-github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
-github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
-github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
-github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
-github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
-github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
-github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
-github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
-github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
+github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
+github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
+github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
+github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
+github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
+github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248=
-github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk=
-github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
-github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
+github.com/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao=
+github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8=
+github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc=
+github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY=
+github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 h1:cLN4IBkmkYZNnk7EAJ0BHIethd+J6LqxFNw5mSiI2bM=
+github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk=
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
-github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
-github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
+github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
+github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
@@ -49,59 +90,85 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
-github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
+github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s=
+github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
-github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
-github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
-github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
-github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
-github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
-github.com/prometheus/common/sigv4 v0.1.0 h1:qoVebwtwwEhS85Czm2dSROY5fTo2PAPEVdDeppTwGX4=
-github.com/prometheus/common/sigv4 v0.1.0/go.mod h1:2Jkxxk9yYvCkE5G1sQT7GuEXm57JrvHu9k5YwTjsNtI=
-github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
-github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
-github.com/prometheus/prometheus v0.54.1 h1:vKuwQNjnYN2/mDoWfHXDhAsz/68q/dQDb+YbcEqU7MQ=
-github.com/prometheus/prometheus v0.54.1/go.mod h1:xlLByHhk2g3ycakQGrMaU8K7OySZx98BzeCR99991NY=
+github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
+github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
+github.com/prometheus/client_golang/exp v0.0.0-20260108101519-fb0838f53562 h1:vwqZvuobg82U0gcG2eVrFH27806bUbNr32SvfRbvdsg=
+github.com/prometheus/client_golang/exp v0.0.0-20260108101519-fb0838f53562/go.mod h1:PmAYDB13uBFBG9qE1qxZZgZWhg7Rg6SfKM5DMK7hjyI=
+github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
+github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
+github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
+github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
+github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos=
+github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM=
+github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
+github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
+github.com/prometheus/prometheus v0.309.1 h1:jutK6eCYDpWdPTUbVbkcQsNCMO9CCkSwjQRMLds4jSo=
+github.com/prometheus/prometheus v0.309.1/go.mod h1:d+dOGiVhuNDa4MaFXHVdnUBy/CzqlcNTooR8oM1wdTU=
+github.com/prometheus/sigv4 v0.4.1 h1:EIc3j+8NBea9u1iV6O5ZAN8uvPq2xOIUPcqCTivHuXs=
+github.com/prometheus/sigv4 v0.4.1/go.mod h1:eu+ZbRvsc5TPiHwqh77OWuCnWK73IdkETYY46P4dXOU=
+github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
+github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
-github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
+go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
+go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
+go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
+go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
+go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
+go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
+go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
-golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
-golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
-golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA=
-golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
-golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
-golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
-golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
-golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
-golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
-golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
-golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
-golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
-golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
-golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
-golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
-google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
-google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
-go.yaml.in/yaml/v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
-go.yaml.in/yaml/v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
+go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
+golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
+golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
+golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
+golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
+golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
+golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
+golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
+golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
+golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
+golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
+golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
+golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
+golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
+golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
+golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
+google.golang.org/api v0.266.0 h1:hco+oNCf9y7DmLeAtHJi/uBAY7n/7XC9mZPxu1ROiyk=
+google.golang.org/api v0.266.0/go.mod h1:Jzc0+ZfLnyvXma3UtaTl023TdhZu6OMBP9tJ+0EmFD0=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
+google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
+google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
+google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
+google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-k8s.io/apimachinery v0.29.3 h1:2tbx+5L7RNvqJjn7RIuIKu9XTsIZ9Z5wX2G22XAa5EU=
-k8s.io/apimachinery v0.29.3/go.mod h1:hx/S4V2PNW4OMg3WizRrHutyB5la0iCUbZym+W0EQIU=
-k8s.io/client-go v0.29.3 h1:R/zaZbEAxqComZ9FHeQwOh3Y1ZUs7FaHKZdQtIc2WZg=
-k8s.io/client-go v0.29.3/go.mod h1:tkDisCvgPfiRpxGnOORfkljmS+UrW+WtXAy2fTvXJB0=
+k8s.io/apimachinery v0.35.0 h1:Z2L3IHvPVv/MJ7xRxHEtk6GoJElaAqDCCU0S6ncYok8=
+k8s.io/apimachinery v0.35.0/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
+k8s.io/client-go v0.35.0 h1:IAW0ifFbfQQwQmga0UdoH0yvdqrbwMdq9vIFEhRpxBE=
+k8s.io/client-go v0.35.0/go.mod h1:q2E5AAyqcbeLGPdoRB+Nxe3KYTfPce1Dnu1myQdqz9o=
k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
-k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI=
-k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
+k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
+k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
diff --git a/web/ui/mantine-ui/src/state/queryPageSlice.ts b/web/ui/mantine-ui/src/state/queryPageSlice.ts
index 253b3ee9c6..7a4f7b257a 100644
--- a/web/ui/mantine-ui/src/state/queryPageSlice.ts
+++ b/web/ui/mantine-ui/src/state/queryPageSlice.ts
@@ -58,6 +58,7 @@ export interface Visualizer {
resolution: GraphResolution;
displayMode: GraphDisplayMode;
showExemplars: boolean;
+ yAxisMin: number | null;
}
export type Panel = {
@@ -86,6 +87,7 @@ export const newDefaultPanel = (): Panel => ({
resolution: { type: "auto", density: "medium" },
displayMode: GraphDisplayMode.Lines,
showExemplars: false,
+ yAxisMin: null,
},
});
@@ -113,6 +115,19 @@ export const queryPageSlice = createSlice({
state.panels.push(newDefaultPanel());
updateURL(state.panels);
},
+ duplicatePanel: (
+ state,
+ { payload }: PayloadAction<{ idx: number; expr: string }>
+ ) => {
+ const newPanel = {
+ ...state.panels[payload.idx],
+ id: randomId(),
+ expr: payload.expr,
+ };
+ // Insert the duplicated panel just below the original panel.
+ state.panels.splice(payload.idx + 1, 0, newPanel);
+ updateURL(state.panels);
+ },
removePanel: (state, { payload }: PayloadAction) => {
state.panels.splice(payload, 1);
updateURL(state.panels);
@@ -151,6 +166,7 @@ export const {
setPanels,
addPanel,
removePanel,
+ duplicatePanel,
setExpr,
addQueryToHistory,
setShowTree,
diff --git a/web/ui/mantine-ui/src/state/settingsSlice.ts b/web/ui/mantine-ui/src/state/settingsSlice.ts
index 8b4a33bf76..a3e133380a 100644
--- a/web/ui/mantine-ui/src/state/settingsSlice.ts
+++ b/web/ui/mantine-ui/src/state/settingsSlice.ts
@@ -102,7 +102,7 @@ export const initialState: Settings = {
),
showAnnotations: initializeFromLocalStorage(
localStorageKeyShowAnnotations,
- true
+ false
),
showQueryWarnings: initializeFromLocalStorage(
localStorageKeyShowQueryWarnings,
diff --git a/web/ui/module/codemirror-promql/package.json b/web/ui/module/codemirror-promql/package.json
index f40dc65432..5208513eab 100644
--- a/web/ui/module/codemirror-promql/package.json
+++ b/web/ui/module/codemirror-promql/package.json
@@ -1,6 +1,6 @@
{
"name": "@prometheus-io/codemirror-promql",
- "version": "0.307.3",
+ "version": "0.309.1",
"description": "a CodeMirror mode for the PromQL language",
"types": "dist/esm/index.d.ts",
"module": "dist/esm/index.js",
@@ -29,19 +29,19 @@
},
"homepage": "https://github.com/prometheus/prometheus/blob/main/web/ui/module/codemirror-promql/README.md",
"dependencies": {
- "@prometheus-io/lezer-promql": "0.307.3",
- "lru-cache": "^11.2.2"
+ "@prometheus-io/lezer-promql": "0.309.1",
+ "lru-cache": "^11.2.5"
},
"devDependencies": {
- "@codemirror/autocomplete": "^6.19.0",
- "@codemirror/language": "^6.11.3",
- "@codemirror/lint": "^6.8.5",
- "@codemirror/state": "^6.5.2",
- "@codemirror/view": "^6.38.4",
- "@lezer/common": "^1.2.3",
- "@lezer/highlight": "^1.2.1",
- "@lezer/lr": "^1.4.2",
- "eslint-plugin-prettier": "^5.5.4",
+ "@codemirror/autocomplete": "^6.20.0",
+ "@codemirror/language": "^6.12.1",
+ "@codemirror/lint": "^6.9.3",
+ "@codemirror/state": "^6.5.4",
+ "@codemirror/view": "^6.39.12",
+ "@lezer/common": "^1.5.1",
+ "@lezer/highlight": "^1.2.3",
+ "@lezer/lr": "^1.4.8",
+ "eslint-plugin-prettier": "^5.5.5",
"isomorphic-fetch": "^3.0.0",
"nock": "^14.0.10"
},
diff --git a/web/ui/module/codemirror-promql/src/client/prometheus.test.ts b/web/ui/module/codemirror-promql/src/client/prometheus.test.ts
new file mode 100644
index 0000000000..c872edbb69
--- /dev/null
+++ b/web/ui/module/codemirror-promql/src/client/prometheus.test.ts
@@ -0,0 +1,97 @@
+// Copyright 2025 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.
+
+import { HTTPPrometheusClient, CachedPrometheusClient } from './prometheus';
+
+describe('HTTPPrometheusClient destroy', () => {
+ it('should be safe to call destroy multiple times', () => {
+ const client = new HTTPPrometheusClient({ url: 'http://localhost:8080' });
+ // First call
+ client.destroy();
+ // Second call should not throw
+ expect(() => client.destroy()).not.toThrow();
+ });
+
+ it('should abort in-flight requests when destroy is called', async () => {
+ let abortSignal: AbortSignal | null | undefined;
+
+ const mockFetch = (_url: RequestInfo, init?: RequestInit): Promise => {
+ abortSignal = init?.signal;
+ // Return a promise that never resolves to simulate an in-flight request
+ return new Promise(() => {});
+ };
+
+ const client = new HTTPPrometheusClient({
+ url: 'http://localhost:8080',
+ fetchFn: mockFetch,
+ });
+
+ // Start a request (don't await it)
+ client.labelNames();
+
+ // Verify the signal was captured and not aborted yet
+ expect(abortSignal).toBeDefined();
+ expect(abortSignal?.aborted).toBe(false);
+
+ // Destroy the client
+ client.destroy();
+
+ // Verify the request was aborted
+ expect(abortSignal?.aborted).toBe(true);
+ });
+});
+
+describe('CachedPrometheusClient destroy', () => {
+ it('should be safe to call destroy multiple times', () => {
+ const httpClient = new HTTPPrometheusClient({ url: 'http://localhost:8080' });
+ const cachedClient = new CachedPrometheusClient(httpClient);
+
+ // First call
+ cachedClient.destroy();
+ // Second call should not throw
+ expect(() => cachedClient.destroy()).not.toThrow();
+ });
+
+ it('should call destroy on the underlying HTTPPrometheusClient', () => {
+ const httpClient = new HTTPPrometheusClient({ url: 'http://localhost:8080' });
+
+ let destroyCalled = false;
+ const originalDestroy = httpClient.destroy.bind(httpClient);
+ httpClient.destroy = () => {
+ destroyCalled = true;
+ originalDestroy();
+ };
+
+ const cachedClient = new CachedPrometheusClient(httpClient);
+ cachedClient.destroy();
+
+ expect(destroyCalled).toBe(true);
+ });
+
+ it('should handle underlying clients without destroy method', () => {
+ // Create a minimal PrometheusClient without destroy
+ const minimalClient = {
+ labelNames: () => Promise.resolve([]),
+ labelValues: () => Promise.resolve([]),
+ metricMetadata: () => Promise.resolve({}),
+ series: () => Promise.resolve([]),
+ metricNames: () => Promise.resolve([]),
+ flags: () => Promise.resolve({}),
+ };
+
+ const cachedClient = new CachedPrometheusClient(minimalClient);
+
+ // Should not throw even though underlying client has no destroy
+ expect(() => cachedClient.destroy()).not.toThrow();
+ });
+});
diff --git a/web/ui/module/codemirror-promql/src/client/prometheus.ts b/web/ui/module/codemirror-promql/src/client/prometheus.ts
index 165549ac82..91de148f3c 100644
--- a/web/ui/module/codemirror-promql/src/client/prometheus.ts
+++ b/web/ui/module/codemirror-promql/src/client/prometheus.ts
@@ -39,6 +39,9 @@ export interface PrometheusClient {
// flags returns flag values that prometheus was configured with.
flags(): Promise>;
+
+ // destroy is called to release all resources held by this client
+ destroy?(): void;
}
export interface CacheConfig {
@@ -88,6 +91,7 @@ export class HTTPPrometheusClient implements PrometheusClient {
// when calling it, thus the indirection via another function wrapper.
private readonly fetchFn: FetchFn = (input: RequestInfo, init?: RequestInit): Promise => fetch(input, init);
private requestHeaders: Headers = new Headers();
+ private readonly abortControllers: Set = new Set();
constructor(config: PrometheusConfig) {
this.url = config.url ? config.url : '';
@@ -199,11 +203,22 @@ export class HTTPPrometheusClient implements PrometheusClient {
});
}
+ destroy(): void {
+ for (const controller of this.abortControllers) {
+ controller.abort();
+ }
+ this.abortControllers.clear();
+ }
+
private fetchAPI(resource: string, init?: RequestInit): Promise {
+ const controller = new AbortController();
+ this.abortControllers.add(controller);
+
if (init) {
init.headers = this.requestHeaders;
+ init.signal = controller.signal;
} else {
- init = { headers: this.requestHeaders };
+ init = { headers: this.requestHeaders, signal: controller.signal };
}
return this.fetchFn(this.url + resource, init)
.then((res) => {
@@ -221,6 +236,9 @@ export class HTTPPrometheusClient implements PrometheusClient {
throw new Error('missing "data" field in response JSON');
}
return apiRes.data;
+ })
+ .finally(() => {
+ this.abortControllers.delete(controller);
});
}
@@ -448,4 +466,8 @@ export class CachedPrometheusClient implements PrometheusClient {
return flags;
});
}
+
+ destroy(): void {
+ this.client.destroy?.();
+ }
}
diff --git a/web/ui/module/codemirror-promql/src/complete/hybrid.test.ts b/web/ui/module/codemirror-promql/src/complete/hybrid.test.ts
index 587b31e743..facda35ac8 100644
--- a/web/ui/module/codemirror-promql/src/complete/hybrid.test.ts
+++ b/web/ui/module/codemirror-promql/src/complete/hybrid.test.ts
@@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { analyzeCompletion, computeStartCompletePosition, ContextKind } from './hybrid';
+import { analyzeCompletion, computeStartCompletePosition, computeEndCompletePosition, ContextKind, durationWithUnitRegexp } from './hybrid';
import { createEditorState, mockedMetricsTerms, mockPrometheusServer } from '../test/utils-test';
import { Completion, CompletionContext } from '@codemirror/autocomplete';
import {
@@ -29,6 +29,7 @@ import {
import { EqlSingle, Neq } from '@prometheus-io/lezer-promql';
import { syntaxTree } from '@codemirror/language';
import { newCompleteStrategy } from './index';
+import nock from 'nock';
describe('analyzeCompletion test', () => {
const testCases = [
@@ -299,6 +300,12 @@ describe('analyzeCompletion test', () => {
pos: 33, // cursor is between the bracket after the comma
expectedContext: [{ kind: ContextKind.LabelName, metricName: 'metric_name' }],
},
+ {
+ title: 'no label suggestions after closing matcher',
+ expr: 'up{job="prometheus"}',
+ pos: 20, // cursor is right after the closing curly bracket
+ expectedContext: [],
+ },
{
title: 'continue autocomplete labelName that defined a metric',
expr: '{myL}',
@@ -553,6 +560,18 @@ describe('analyzeCompletion test', () => {
pos: 28,
expectedContext: [{ kind: ContextKind.Duration }],
},
+ {
+ title: 'do not autocomplete duration when unit already present in matrixSelector',
+ expr: 'rate(foo[5m])',
+ pos: 10,
+ expectedContext: [],
+ },
+ {
+ title: 'do not autocomplete duration when multi char unit already present in matrixSelector',
+ expr: 'rate(foo[5ms])',
+ pos: 10,
+ expectedContext: [],
+ },
{
title: 'autocomplete duration for a subQuery',
expr: 'go[5d:5]',
@@ -619,7 +638,43 @@ describe('analyzeCompletion test', () => {
const state = createEditorState(value.expr);
const node = syntaxTree(state).resolve(value.pos, -1);
const result = analyzeCompletion(state, node, value.pos);
- expect(value.expectedContext).toEqual(result);
+ expect(result).toEqual(value.expectedContext);
+ });
+ });
+});
+
+describe('durationWithUnitRegexp test', () => {
+ it('should match complete durations with units', () => {
+ const testCases = [
+ { input: '5m', expected: true },
+ { input: '30s', expected: true },
+ { input: '1h', expected: true },
+ { input: '500ms', expected: true },
+ { input: '2d', expected: true },
+ { input: '1w', expected: true },
+ { input: '1y', expected: true },
+ { input: '1d2h', expected: true },
+ { input: '2h30m', expected: true },
+ { input: '1h2m3s', expected: true },
+ { input: '250ms2s', expected: true },
+ { input: '2h3m4s5ms', expected: true },
+ { input: '5', expected: false },
+ { input: '5m5', expected: false },
+ { input: 'm', expected: false },
+ { input: 'd', expected: false },
+ { input: '', expected: false },
+ { input: '1hms', expected: false },
+ { input: '2x', expected: false },
+ ];
+ testCases.forEach(({ input, expected }) => {
+ expect(durationWithUnitRegexp.test(input)).toBe(expected);
+ });
+ });
+
+ it('should not match durations without units or partial units', () => {
+ const testCases = ['5', '30', '100', '5m5', 'm', 'd'];
+ testCases.forEach((input) => {
+ expect(durationWithUnitRegexp.test(input)).toBe(false);
});
});
});
@@ -806,7 +861,146 @@ describe('computeStartCompletePosition test', () => {
const state = createEditorState(value.expr);
const node = syntaxTree(state).resolve(value.pos, -1);
const result = computeStartCompletePosition(state, node, value.pos);
- expect(value.expectedStart).toEqual(result);
+ expect(result).toEqual(value.expectedStart);
+ });
+ });
+});
+
+describe('computeEndCompletePosition test', () => {
+ const testCases = [
+ {
+ title: 'cursor at end of metric name',
+ expr: 'metric_name',
+ pos: 11, // cursor is at the end
+ expectedEnd: 11,
+ },
+ {
+ title: 'cursor in middle of metric name - should extend to end',
+ expr: 'coredns_cache_hits_total',
+ pos: 14, // cursor is after 'coredns_cache_' (before 'hits')
+ expectedEnd: 24, // should extend to end of 'coredns_cache_hits_total'
+ },
+ {
+ title: 'cursor in middle of metric name inside rate() - should extend to end',
+ expr: 'rate(coredns_cache_hits_total[2m])',
+ pos: 19, // cursor is after 'coredns_cache_' (before 'hits')
+ expectedEnd: 29, // should extend to end of 'coredns_cache_hits_total'
+ },
+ {
+ title: 'cursor in middle of metric name inside sum(rate()) - should extend to end',
+ expr: 'sum(rate(coredns_cache_hits_total[2m]))',
+ pos: 24, // cursor is after 'coredns_cache_' (before 'hits')
+ expectedEnd: 33, // should extend to end of 'coredns_cache_hits_total'
+ },
+ {
+ title: 'cursor at beginning of metric name - should extend to end',
+ expr: 'metric_name',
+ pos: 1, // cursor after 'm'
+ expectedEnd: 11,
+ },
+ {
+ title: 'cursor in middle of incomplete function name - should extend to end',
+ expr: 'sum_ov',
+ pos: 4, // cursor after 'sum_' (before 'ov')
+ expectedEnd: 6, // should extend to end of 'sum_ov'
+ },
+ {
+ title: 'cursor in middle of incomplete function name within aggregator - should extend to end',
+ expr: 'sum(sum_ov(foo[5m]))',
+ pos: 8, // cursor after 'sum_' (before 'ov')
+ expectedEnd: 10, // should extend to end of 'sum_ov'
+ },
+ {
+ title: 'empty bracket - ends before the closing bracket',
+ expr: '{}',
+ pos: 1,
+ expectedEnd: 1,
+ },
+ {
+ title: 'cursor in label matchers - ends before the closing bracket',
+ expr: 'metric_name{label="value"}',
+ pos: 12, // cursor after '{'
+ expectedEnd: 25,
+ },
+ {
+ title: 'cursor in middle of label name in grouping clause - should extend to end',
+ expr: 'sum by (instance_name)',
+ pos: 12, // cursor after 'inst' (before 'ance')
+ expectedEnd: 21, // should extend to end of 'instance_name'
+ },
+ {
+ title: 'cursor in middle of label name in label matcher - should extend to end',
+ expr: 'metric{instance_name="value"}',
+ pos: 11, // cursor after 'inst' (before 'ance')
+ expectedEnd: 20, // should extend to end of 'instance_name'
+ },
+ {
+ title: 'cursor in middle of label name in on() modifier - should extend to end',
+ expr: 'a / on(instance_name) b',
+ pos: 11, // cursor after 'inst' (before 'ance')
+ expectedEnd: 20, // should extend to end of 'instance_name'
+ },
+ {
+ title: 'cursor in middle of label name in ignoring() modifier - should extend to end',
+ expr: 'a / ignoring(instance_name) b',
+ pos: 17, // cursor after 'inst' (before 'ance')
+ expectedEnd: 26, // should extend to end of 'instance_name'
+ },
+ {
+ title: 'cursor in middle of function name rate - should extend to end',
+ expr: 'rate(foo[5m])',
+ pos: 2, // cursor after 'ra' (before 'te')
+ expectedEnd: 4, // should extend to end of 'rate'
+ },
+ {
+ title: 'cursor in middle of function name histogram_quantile - should extend to end',
+ expr: 'histogram_quantile(0.9, rate(foo[5m]))',
+ pos: 10, // cursor after 'histogram_' (before 'quantile')
+ expectedEnd: 18, // should extend to end of 'histogram_quantile'
+ },
+ {
+ title: 'cursor in middle of aggregator sum - should extend to end',
+ expr: 'sum(rate(foo[5m]))',
+ pos: 2, // cursor after 'su' (before 'm')
+ expectedEnd: 3, // should extend to end of 'sum'
+ },
+ {
+ title: 'cursor in middle of aggregator count_values - should extend to end',
+ expr: 'count_values("label", foo)',
+ pos: 6, // cursor after 'count_' (before 'values')
+ expectedEnd: 12, // should extend to end of 'count_values'
+ },
+ {
+ title: 'cursor in middle of nested function - should extend to end',
+ expr: 'sum(rate(foo[5m]))',
+ pos: 6, // cursor after 'ra' inside rate (before 'te')
+ expectedEnd: 8, // should extend to end of 'rate'
+ },
+ {
+ title: 'cursor at beginning of aggregator - should extend to end',
+ expr: 'avg by (instance) (rate(foo[5m]))',
+ pos: 1, // cursor after 'a' (before 'vg')
+ expectedEnd: 3, // should extend to end of 'avg'
+ },
+ {
+ title: 'cursor in middle of function name with binary op - should extend to end',
+ expr: 'rate(foo[5m]) / irate(bar[5m])',
+ pos: 17, // cursor after 'ir' inside irate (before 'ate')
+ expectedEnd: 21, // should extend to end of 'irate'
+ },
+ {
+ title: 'error node - returns pos (cursor position)',
+ expr: 'metric_name !',
+ pos: 13, // cursor at '!' (error node)
+ expectedEnd: 13, // error node returns pos
+ },
+ ];
+ testCases.forEach((value) => {
+ it(value.title, () => {
+ const state = createEditorState(value.expr);
+ const node = syntaxTree(state).resolve(value.pos, -1);
+ const result = computeEndCompletePosition(node, value.pos);
+ expect(result).toEqual(value.expectedEnd);
});
});
});
@@ -860,6 +1054,28 @@ describe('autocomplete promQL test', () => {
validFor: /^[a-zA-Z0-9_:]+$/,
},
},
+ {
+ title: 'cursor in middle of metric name - to should extend to end (issue #15839)',
+ expr: 'sum(coredns_cache_hits_total)',
+ pos: 18, // cursor is after 'coredns_cache_' (before 'hits')
+ expectedResult: {
+ options: ([] as Completion[]).concat(functionIdentifierTerms, aggregateOpTerms, snippets),
+ from: 4,
+ to: 28, // should extend to end of 'coredns_cache_hits_total'
+ validFor: /^[a-zA-Z0-9_:]+$/,
+ },
+ },
+ {
+ title: 'cursor in middle of metric name inside rate() - to should extend to end (issue #15839)',
+ expr: 'rate(coredns_cache_hits_total[2m])',
+ pos: 19, // cursor is after 'coredns_cache_' (before 'hits')
+ expectedResult: {
+ options: ([] as Completion[]).concat(functionIdentifierTerms, aggregateOpTerms, snippets),
+ from: 5,
+ to: 29, // should extend to end of 'coredns_cache_hits_total'
+ validFor: /^[a-zA-Z0-9_:]+$/,
+ },
+ },
{
title: 'offline function/aggregation autocompletion in aggregation 3',
expr: 'sum(rate())',
@@ -1223,6 +1439,28 @@ describe('autocomplete promQL test', () => {
validFor: undefined,
},
},
+ {
+ title: 'offline do not autocomplete duration when unit already present in matrixSelector',
+ expr: 'rate(foo[5m])',
+ pos: 10,
+ expectedResult: {
+ options: [],
+ from: 10,
+ to: 11,
+ validFor: /^[a-zA-Z0-9_:]+$/,
+ },
+ },
+ {
+ title: 'offline do not autocomplete duration when multi char unit already present in matrixSelector',
+ expr: 'rate(foo[5ms])',
+ pos: 10,
+ expectedResult: {
+ options: [],
+ from: 10,
+ to: 12,
+ validFor: /^[a-zA-Z0-9_:]+$/,
+ },
+ },
{
title: 'offline autocomplete duration for a subQuery',
expr: 'go[5d:5]',
@@ -1374,7 +1612,39 @@ describe('autocomplete promQL test', () => {
const context = new CompletionContext(state, value.pos, true);
const completion = newCompleteStrategy(value.conf);
const result = await completion.promQL(context);
- expect(value.expectedResult).toEqual(result);
+ expect(result).toEqual(value.expectedResult);
});
});
+
+ it('online autocomplete of openmetrics counter', async () => {
+ const metricName = 'direct_notifications_total';
+ const baseMetricName = 'direct_notifications';
+ nock('http://localhost:8080')
+ .get('/api/v1/label/__name__/values')
+ .query(true)
+ .reply(200, { status: 'success', data: [metricName] });
+ nock('http://localhost:8080')
+ .get('/api/v1/metadata')
+ .query(true)
+ .reply(200, {
+ status: 'success',
+ data: {
+ [baseMetricName]: [
+ {
+ type: 'counter',
+ help: 'Number of direct notifications.',
+ unit: '',
+ },
+ ],
+ },
+ });
+ const state = createEditorState(metricName);
+ const context = new CompletionContext(state, metricName.length, true);
+ const completion = newCompleteStrategy({ remote: { url: 'http://localhost:8080' } });
+ const result = await completion.promQL(context);
+ // nock only mocks the HTTP endpoints; this test just ensures remote completion works
+ // when metadata for an OpenMetrics _total counter is stored under its base metric name.
+ expect(result).not.toBeNull();
+ expect((result as NonNullable).options.length).toBeGreaterThan(0);
+ });
});
diff --git a/web/ui/module/codemirror-promql/src/complete/hybrid.ts b/web/ui/module/codemirror-promql/src/complete/hybrid.ts
index b2d439d2fe..84c101b43c 100644
--- a/web/ui/module/codemirror-promql/src/complete/hybrid.ts
+++ b/web/ui/module/codemirror-promql/src/complete/hybrid.ts
@@ -166,6 +166,49 @@ function arrayToCompletionResult(data: Completion[], from: number, to: number, i
} as CompletionResult;
}
+// computeEndCompletePosition calculates the end position for autocompletion replacement.
+// When the cursor is in the middle of a token, this ensures the entire token is replaced,
+// not just the portion before the cursor. This fixes issue #15839.
+// Note: this method is exported only for testing purpose.
+export function computeEndCompletePosition(node: SyntaxNode, pos: number): number {
+ // For error nodes, use the cursor position as the end position
+ if (node.type.id === 0) {
+ return pos;
+ }
+
+ if (
+ node.type.id === LabelMatchers ||
+ node.type.id === GroupingLabels ||
+ node.type.id === FunctionCallBody ||
+ node.type.id === MatrixSelector ||
+ node.type.id === SubqueryExpr
+ ) {
+ // When we're inside empty brackets, we want to replace up to just before the closing bracket.
+ return node.to - 1;
+ }
+
+ if (node.type.id === StringLiteral && (node.parent?.type.id === UnquotedLabelMatcher || node.parent?.type.id === QuotedLabelMatcher)) {
+ // For label values, we want to replace all content inside the quotes.
+ return node.parent.to - 1;
+ }
+
+ // For all other nodes, extend the end position to include the entire token.
+ return node.to;
+}
+
+// Matches complete PromQL durations, including compound units (e.g., 5m, 1d2h, 1h30m, etc.).
+// Duration units are a fixed, safe set (no regex metacharacters), so no escaping is needed.
+export const durationWithUnitRegexp = new RegExp(`^(\\d+(${durationTerms.map((term) => term.label).join('|')}))+$`);
+
+// Determines if a duration already has a complete time unit to prevent autocomplete insertion (issue #15452)
+function hasCompleteDurationUnit(state: EditorState, node: SyntaxNode): boolean {
+ if (node.from >= node.to) {
+ return false;
+ }
+ const nodeContent = state.sliceDoc(node.from, node.to);
+ return durationWithUnitRegexp.test(nodeContent);
+}
+
// computeStartCompleteLabelPositionInLabelMatcherOrInGroupingLabel calculates the start position only when the node is a LabelMatchers or a GroupingLabels
function computeStartCompleteLabelPositionInLabelMatcherOrInGroupingLabel(node: SyntaxNode, pos: number): number {
// Here we can have two different situations:
@@ -400,12 +443,18 @@ export function analyzeCompletion(state: EditorState, node: SyntaxNode, pos: num
// so we have or to autocomplete any kind of labelName or to autocomplete only the labelName associated to the metric
result.push({ kind: ContextKind.LabelName, metricName: getMetricNameInGroupBy(node, state) });
break;
- case LabelMatchers:
+ case LabelMatchers: {
+ if (pos >= node.to) {
+ // Cursor is outside of the label matcher block (e.g. right after `}`),
+ // so don't offer label-related completions anymore.
+ break;
+ }
// In that case we are in the given situation:
// metric_name{} or {}
// so we have or to autocomplete any kind of labelName or to autocomplete only the labelName associated to the metric
result.push({ kind: ContextKind.LabelName, metricName: getMetricNameInVectorSelector(node, state) });
break;
+ }
case LabelName:
if (node.parent?.type.id === GroupingLabels) {
// In this case we are in the given situation:
@@ -471,12 +520,18 @@ export function analyzeCompletion(state: EditorState, node: SyntaxNode, pos: num
// Duration, Duration, ⚠(NumberLiteral)
// )
// So we should continue to autocomplete a duration
- result.push({ kind: ContextKind.Duration });
+ if (!hasCompleteDurationUnit(state, node)) {
+ result.push({ kind: ContextKind.Duration });
+ }
} else {
result.push({ kind: ContextKind.Number });
}
break;
case NumberDurationLiteralInDurationContext:
+ if (!hasCompleteDurationUnit(state, node)) {
+ result.push({ kind: ContextKind.Duration });
+ }
+ break;
case OffsetExpr:
result.push({ kind: ContextKind.Duration });
break;
@@ -550,6 +605,10 @@ export class HybridComplete implements CompleteStrategy {
return this.prometheusClient;
}
+ destroy(): void {
+ this.prometheusClient?.destroy?.();
+ }
+
promQL(context: CompletionContext): Promise | CompletionResult | null {
const { state, pos } = context;
const tree = syntaxTree(state).resolve(pos, -1);
@@ -638,7 +697,13 @@ export class HybridComplete implements CompleteStrategy {
}
}
return asyncResult.then((result) => {
- return arrayToCompletionResult(result, computeStartCompletePosition(state, tree, pos), pos, completeSnippet, span);
+ return arrayToCompletionResult(
+ result,
+ computeStartCompletePosition(state, tree, pos),
+ computeEndCompletePosition(tree, pos),
+ completeSnippet,
+ span
+ );
});
}
@@ -664,11 +729,10 @@ export class HybridComplete implements CompleteStrategy {
.then((metricMetadata) => {
if (metricMetadata) {
for (const [metricName, node] of metricCompletion) {
- // First check if the full metric name has metadata (even if it has one of the
- // histogram/summary suffixes, it may be a metric that is not following naming
- // conventions, see https://github.com/prometheus/prometheus/issues/16907).
- // Then fall back to the base metric name if full metadata doesn't exist.
- const metadata = metricMetadata[metricName] ?? metricMetadata[metricName.replace(/(_count|_sum|_bucket)$/, '')];
+ // First check if the full metric name has metadata (even if it has one of the histogram/summary/openmetrics suffixes
+ // it may be a metric that is not following naming conventions)
+ // Then fall back to the base metric name if full metadata doesn't exist
+ const metadata = metricMetadata[metricName] ?? metricMetadata[metricName.replace(/(_count|_sum|_bucket|_total)$/, '')];
if (metadata) {
if (metadata.length > 1) {
// it means the metricName has different possible helper and type
diff --git a/web/ui/module/codemirror-promql/src/complete/index.ts b/web/ui/module/codemirror-promql/src/complete/index.ts
index b3902c3b6b..dd73857639 100644
--- a/web/ui/module/codemirror-promql/src/complete/index.ts
+++ b/web/ui/module/codemirror-promql/src/complete/index.ts
@@ -19,6 +19,7 @@ import { CompletionContext, CompletionResult } from '@codemirror/autocomplete';
// Every different completion mode must implement this interface.
export interface CompleteStrategy {
promQL(context: CompletionContext): Promise | CompletionResult | null;
+ destroy?(): void;
}
// CompleteConfiguration should be used to customize the autocompletion.
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..68d7b06553 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 = [
@@ -239,6 +243,12 @@ export const functionIdentifierTerms = [
info: 'Calculate quantiles from native histograms and from conventional histogram buckets',
type: 'function',
},
+ {
+ label: 'histogram_quantiles',
+ detail: 'function',
+ info: 'Calculate multiple quantiles from native histograms and from conventional histogram buckets',
+ type: 'function',
+ },
{
label: 'histogram_sum',
detail: 'function',
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/promql.test.ts b/web/ui/module/codemirror-promql/src/promql.test.ts
new file mode 100644
index 0000000000..787747cc5e
--- /dev/null
+++ b/web/ui/module/codemirror-promql/src/promql.test.ts
@@ -0,0 +1,58 @@
+// Copyright 2025 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.
+
+import { PromQLExtension } from './promql';
+import { CompleteStrategy } from './complete';
+import { CompletionResult } from '@codemirror/autocomplete';
+
+describe('PromQLExtension destroy', () => {
+ it('should be safe to call destroy multiple times', () => {
+ const extension = new PromQLExtension();
+ // First call
+ extension.destroy();
+ // Second call should not throw
+ expect(() => extension.destroy()).not.toThrow();
+ });
+
+ it('should call destroy on the complete strategy if available', () => {
+ const extension = new PromQLExtension();
+
+ // Set up a mock complete strategy with destroy
+ let destroyCalled = false;
+ const mockCompleteStrategy: CompleteStrategy = {
+ promQL: (): CompletionResult | null => null,
+ destroy: () => {
+ destroyCalled = true;
+ },
+ };
+
+ extension.setComplete({ completeStrategy: mockCompleteStrategy });
+ extension.destroy();
+
+ expect(destroyCalled).toBe(true);
+ });
+
+ it('should handle complete strategies without destroy method', () => {
+ const extension = new PromQLExtension();
+
+ // Set up a mock complete strategy without destroy
+ const mockCompleteStrategy: CompleteStrategy = {
+ promQL: (): CompletionResult | null => null,
+ };
+
+ extension.setComplete({ completeStrategy: mockCompleteStrategy });
+
+ // Should not throw even though complete strategy has no destroy
+ expect(() => extension.destroy()).not.toThrow();
+ });
+});
diff --git a/web/ui/module/codemirror-promql/src/promql.ts b/web/ui/module/codemirror-promql/src/promql.ts
index 506cd1348b..859442559f 100644
--- a/web/ui/module/codemirror-promql/src/promql.ts
+++ b/web/ui/module/codemirror-promql/src/promql.ts
@@ -79,6 +79,10 @@ export class PromQLExtension {
return this;
}
+ destroy(): void {
+ this.complete.destroy?.();
+ }
+
asExtension(languageType = LanguageType.PromQL): Extension {
const language = promQLLanguage(languageType);
let extension: Extension = [language];
diff --git a/web/ui/module/codemirror-promql/src/types/function.ts b/web/ui/module/codemirror-promql/src/types/function.ts
index cfbf3524b5..cc1c0524fb 100644
--- a/web/ui/module/codemirror-promql/src/types/function.ts
+++ b/web/ui/module/codemirror-promql/src/types/function.ts
@@ -44,6 +44,7 @@ import {
HistogramCount,
HistogramFraction,
HistogramQuantile,
+ HistogramQuantiles,
HistogramStdDev,
HistogramStdVar,
HistogramSum,
@@ -306,6 +307,12 @@ const promqlFunctions: { [key: number]: PromQLFunction } = {
variadic: 0,
returnType: ValueType.vector,
},
+ [HistogramQuantiles]: {
+ name: 'histogram_quantiles',
+ argTypes: [ValueType.vector, ValueType.string, ValueType.scalar, ValueType.scalar],
+ variadic: 10,
+ returnType: ValueType.vector,
+ },
[HistogramStdDev]: {
name: 'histogram_stddev',
argTypes: [ValueType.vector],
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/package.json b/web/ui/module/lezer-promql/package.json
index 5e1ab771f1..7a969b57e4 100644
--- a/web/ui/module/lezer-promql/package.json
+++ b/web/ui/module/lezer-promql/package.json
@@ -1,6 +1,6 @@
{
"name": "@prometheus-io/lezer-promql",
- "version": "0.307.3",
+ "version": "0.309.1",
"description": "lezer-based PromQL grammar",
"main": "dist/index.cjs",
"type": "module",
@@ -32,9 +32,9 @@
},
"devDependencies": {
"@lezer/generator": "^1.8.0",
- "@lezer/highlight": "^1.2.1",
- "@lezer/lr": "^1.4.2",
- "@rollup/plugin-node-resolve": "^16.0.1"
+ "@lezer/highlight": "^1.2.3",
+ "@lezer/lr": "^1.4.8",
+ "@rollup/plugin-node-resolve": "^16.0.3"
},
"peerDependencies": {
"@lezer/highlight": "^1.1.2",
diff --git a/web/ui/module/lezer-promql/src/highlight.js b/web/ui/module/lezer-promql/src/highlight.js
index 9c1b5601a3..b452373345 100644
--- a/web/ui/module/lezer-promql/src/highlight.js
+++ b/web/ui/module/lezer-promql/src/highlight.js
@@ -1,4 +1,4 @@
-// Copyright 2022 The Prometheus Authors
+// 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
diff --git a/web/ui/module/lezer-promql/src/promql.grammar b/web/ui/module/lezer-promql/src/promql.grammar
index 5fe8d4d025..e4308186bb 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 {
@@ -148,6 +167,7 @@ FunctionIdentifier {
HistogramCount |
HistogramFraction |
HistogramQuantile |
+ HistogramQuantiles |
HistogramStdDev |
HistogramStdVar |
HistogramSum |
@@ -366,7 +386,10 @@ NumberDurationLiteralInDurationContext {
Start,
End,
Smoothed,
- Anchored
+ Anchored,
+ Fill,
+ FillLeft,
+ FillRight
}
@external propSource promQLHighLight from "./highlight"
@@ -404,6 +427,7 @@ NumberDurationLiteralInDurationContext {
HistogramCount { condFn<"histogram_count"> }
HistogramFraction { condFn<"histogram_fraction"> }
HistogramQuantile { condFn<"histogram_quantile"> }
+ HistogramQuantiles { condFn<"histogram_quantiles"> }
HistogramStdDev { condFn<"histogram_stddev"> }
HistogramStdVar { condFn<"histogram_stdvar"> }
HistogramSum { condFn<"histogram_sum"> }
diff --git a/web/ui/module/lezer-promql/src/tokens.js b/web/ui/module/lezer-promql/src/tokens.js
index 1695ae1d87..6fd681f1f8 100644
--- a/web/ui/module/lezer-promql/src/tokens.js
+++ b/web/ui/module/lezer-promql/src/tokens.js
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// 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
@@ -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/ui/package-lock.json b/web/ui/package-lock.json
index 144a5c76d5..7669399b66 100644
--- a/web/ui/package-lock.json
+++ b/web/ui/package-lock.json
@@ -1,125 +1,110 @@
{
"name": "prometheus-io",
- "version": "0.307.3",
+ "version": "0.309.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "prometheus-io",
- "version": "0.307.3",
+ "version": "0.309.1",
"workspaces": [
"mantine-ui",
"module/*"
],
"devDependencies": {
"@types/jest": "^29.5.14",
- "@typescript-eslint/eslint-plugin": "^8.45.0",
- "@typescript-eslint/parser": "^8.45.0",
+ "@typescript-eslint/eslint-plugin": "^8.54.0",
+ "@typescript-eslint/parser": "^8.54.0",
"eslint-config-prettier": "^10.1.8",
- "prettier": "^3.6.2",
- "ts-jest": "^29.4.4",
+ "prettier": "^3.8.1",
+ "ts-jest": "^29.4.6",
"typescript": "^5.9.3",
- "vite": "^6.3.6"
+ "vite": "^6.4.1"
}
},
"mantine-ui": {
"name": "@prometheus-io/mantine-ui",
- "version": "0.307.3",
+ "version": "0.309.1",
"dependencies": {
- "@codemirror/autocomplete": "^6.19.0",
- "@codemirror/language": "^6.11.3",
- "@codemirror/lint": "^6.8.5",
- "@codemirror/state": "^6.5.2",
- "@codemirror/view": "^6.38.4",
- "@floating-ui/dom": "^1.7.4",
- "@lezer/common": "^1.2.3",
- "@lezer/highlight": "^1.2.1",
- "@mantine/code-highlight": "^8.3.5",
- "@mantine/core": "^8.3.5",
- "@mantine/dates": "^8.3.5",
- "@mantine/hooks": "^8.3.5",
- "@mantine/notifications": "^8.3.5",
+ "@codemirror/autocomplete": "^6.20.0",
+ "@codemirror/language": "^6.12.1",
+ "@codemirror/lint": "^6.9.3",
+ "@codemirror/state": "^6.5.4",
+ "@codemirror/view": "^6.39.12",
+ "@floating-ui/dom": "^1.7.5",
+ "@lezer/common": "^1.5.1",
+ "@lezer/highlight": "^1.2.3",
+ "@mantine/code-highlight": "^8.3.14",
+ "@mantine/core": "^8.3.14",
+ "@mantine/dates": "^8.3.14",
+ "@mantine/hooks": "^8.3.14",
+ "@mantine/notifications": "^8.3.14",
"@microsoft/fetch-event-source": "^2.0.1",
"@nexucis/fuzzy": "^0.5.1",
"@nexucis/kvsearch": "^0.9.1",
- "@prometheus-io/codemirror-promql": "0.307.3",
- "@reduxjs/toolkit": "^2.9.0",
- "@tabler/icons-react": "^3.35.0",
- "@tanstack/react-query": "^5.90.2",
- "@testing-library/jest-dom": "^6.9.0",
- "@testing-library/react": "^16.3.0",
- "@types/lodash": "^4.17.20",
+ "@prometheus-io/codemirror-promql": "0.309.1",
+ "@reduxjs/toolkit": "^2.11.2",
+ "@tabler/icons-react": "^3.36.1",
+ "@tanstack/react-query": "^5.90.20",
+ "@testing-library/jest-dom": "^6.9.1",
+ "@testing-library/react": "^16.3.2",
+ "@types/lodash": "^4.17.23",
"@types/sanitize-html": "^2.16.0",
- "@uiw/react-codemirror": "^4.25.2",
+ "@uiw/react-codemirror": "^4.25.4",
"clsx": "^2.1.1",
- "dayjs": "^1.11.18",
+ "dayjs": "^1.11.19",
"highlight.js": "^11.11.1",
- "lodash": "^4.17.21",
- "react": "^19.1.1",
- "react-dom": "^19.1.1",
- "react-infinite-scroll-component": "^6.1.0",
+ "lodash": "^4.17.23",
+ "react": "^19.2.4",
+ "react-dom": "^19.2.4",
+ "react-infinite-scroll-component": "^6.1.1",
"react-redux": "^9.2.0",
- "react-router-dom": "^7.9.3",
+ "react-router-dom": "^7.13.0",
"sanitize-html": "^2.17.0",
"uplot": "^1.6.32",
"uplot-react": "^1.2.4",
- "use-query-params": "^2.2.1"
+ "use-query-params": "^2.2.2"
},
"devDependencies": {
- "@eslint/compat": "^1.4.0",
- "@eslint/eslintrc": "^3.3.1",
- "@eslint/js": "^9.36.0",
- "@types/react": "^19.1.16",
- "@types/react-dom": "^19.1.9",
- "@typescript-eslint/eslint-plugin": "^8.45.0",
- "@typescript-eslint/parser": "^8.45.0",
+ "@eslint/compat": "^1.4.1",
+ "@eslint/eslintrc": "^3.3.3",
+ "@eslint/js": "^9.39.2",
+ "@types/react": "^19.2.13",
+ "@types/react-dom": "^19.2.3",
+ "@typescript-eslint/eslint-plugin": "^8.54.0",
+ "@typescript-eslint/parser": "^8.54.0",
"@vitejs/plugin-react": "^4.7.0",
- "eslint": "^9.36.0",
+ "eslint": "^9.39.2",
"eslint-plugin-react-hooks": "^5.2.0",
- "eslint-plugin-react-refresh": "^0.4.22",
- "globals": "^16.4.0",
+ "eslint-plugin-react-refresh": "^0.5.0",
+ "globals": "^16.5.0",
"jsdom": "^25.0.1",
"postcss": "^8.5.6",
"postcss-preset-mantine": "^1.18.0",
"postcss-simple-vars": "^7.0.1",
- "vite": "^6.3.6",
+ "vite": "^6.4.1",
"vitest": "^3.2.4"
}
},
- "mantine-ui/node_modules/@floating-ui/react": {
- "version": "0.27.16",
- "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.16.tgz",
- "integrity": "sha512-9O8N4SeG2z++TSM8QA/KTeKFBVCNEz/AGS7gWPJf6KFRzmRWixFRnCnkPHRDwSVZW6QPDO6uT0P2SpWNKCc9/g==",
- "license": "MIT",
- "dependencies": {
- "@floating-ui/react-dom": "^2.1.6",
- "@floating-ui/utils": "^0.2.10",
- "tabbable": "^6.0.0"
- },
- "peerDependencies": {
- "react": ">=17.0.0",
- "react-dom": ">=17.0.0"
- }
- },
"mantine-ui/node_modules/@mantine/code-highlight": {
- "version": "8.3.5",
- "resolved": "https://registry.npmjs.org/@mantine/code-highlight/-/code-highlight-8.3.5.tgz",
- "integrity": "sha512-KZhYPilo6hUbJf/ls0e/lCjXKWI6/cnzkMkSRLAEVLD9HrhJKIBonwQtLRFgJbLmxKu9IE9KuJjq9lA5kUJCFQ==",
+ "version": "8.3.14",
+ "resolved": "https://registry.npmjs.org/@mantine/code-highlight/-/code-highlight-8.3.14.tgz",
+ "integrity": "sha512-7ywMnadaw4O/QG9sQOCIWPZKh6Q97ibyZgkH2cjVNvVbChmZKXIlcHW/QbQJUS84Bs/eGDhnkxwnq78v9w16gQ==",
"license": "MIT",
"dependencies": {
"clsx": "^2.1.1"
},
"peerDependencies": {
- "@mantine/core": "8.3.5",
- "@mantine/hooks": "8.3.5",
+ "@mantine/core": "8.3.14",
+ "@mantine/hooks": "8.3.14",
"react": "^18.x || ^19.x",
"react-dom": "^18.x || ^19.x"
}
},
"mantine-ui/node_modules/@mantine/core": {
- "version": "8.3.5",
- "resolved": "https://registry.npmjs.org/@mantine/core/-/core-8.3.5.tgz",
- "integrity": "sha512-PdVNLMgOS2vFhOujRi6/VC9ic8w3UDyKX7ftwDeJ7yQT8CiepUxfbWWYpVpnq23bdWh/7fIT2Pn1EY8r8GOk7g==",
+ "version": "8.3.14",
+ "resolved": "https://registry.npmjs.org/@mantine/core/-/core-8.3.14.tgz",
+ "integrity": "sha512-ZOxggx65Av1Ii1NrckCuqzluRpmmG+8DyEw24wDom3rmwsPg9UV+0le2QTyI5Eo60LzPfUju1KuEPiUzNABIPg==",
"license": "MIT",
"dependencies": {
"@floating-ui/react": "^0.27.16",
@@ -130,56 +115,56 @@
"type-fest": "^4.41.0"
},
"peerDependencies": {
- "@mantine/hooks": "8.3.5",
+ "@mantine/hooks": "8.3.14",
"react": "^18.x || ^19.x",
"react-dom": "^18.x || ^19.x"
}
},
"mantine-ui/node_modules/@mantine/dates": {
- "version": "8.3.5",
- "resolved": "https://registry.npmjs.org/@mantine/dates/-/dates-8.3.5.tgz",
- "integrity": "sha512-LkIdC4eWPNQFv1BU1c52U3Z3RuA3yU1asvTgMEIQ/MdJsGK8GePwpgMH/jKQ8ba/AW9NfksdvtOJ6uIqPwjCkg==",
+ "version": "8.3.14",
+ "resolved": "https://registry.npmjs.org/@mantine/dates/-/dates-8.3.14.tgz",
+ "integrity": "sha512-NdStRo2ZQ55MoMF5B9vjhpBpHRDHF1XA9Dkb1kKSdNuLlaFXKlvoaZxj/3LfNPpn7Nqlns78nWt4X8/cgC2YIg==",
"license": "MIT",
"dependencies": {
"clsx": "^2.1.1"
},
"peerDependencies": {
- "@mantine/core": "8.3.5",
- "@mantine/hooks": "8.3.5",
+ "@mantine/core": "8.3.14",
+ "@mantine/hooks": "8.3.14",
"dayjs": ">=1.0.0",
"react": "^18.x || ^19.x",
"react-dom": "^18.x || ^19.x"
}
},
"mantine-ui/node_modules/@mantine/hooks": {
- "version": "8.3.5",
- "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-8.3.5.tgz",
- "integrity": "sha512-0Wf08eWLKi3WkKlxnV1W5vfuN6wcvAV2VbhQlOy0R9nrWorGTtonQF6qqBE3PnJFYF1/ZE+HkYZQ/Dr7DmYSMQ==",
+ "version": "8.3.14",
+ "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-8.3.14.tgz",
+ "integrity": "sha512-0SbHnGEuHcF2QyjzBBcqidpjNmIb6n7TC3obnhkBToYhUTbMcJSK/8ei/yHtAelridJH4CPeohRlQdc0HajHyQ==",
"license": "MIT",
"peerDependencies": {
"react": "^18.x || ^19.x"
}
},
"mantine-ui/node_modules/@mantine/notifications": {
- "version": "8.3.5",
- "resolved": "https://registry.npmjs.org/@mantine/notifications/-/notifications-8.3.5.tgz",
- "integrity": "sha512-8TvzrPxfdtOLGTalv7Ei1hy2F6KbR3P7/V73yw3AOKhrf1ydS89sqV2ShbsucHGJk9Pto0wjdTPd8Q7pm5MAYw==",
+ "version": "8.3.14",
+ "resolved": "https://registry.npmjs.org/@mantine/notifications/-/notifications-8.3.14.tgz",
+ "integrity": "sha512-+ia97wrcU9Zfv+jXYvgr2GdISqKTHbQE9nnEIZvGUBPAqKr9b2JAsaXQS/RsAdoXUI+kKDEtH2fyVYS7zrSi/Q==",
"license": "MIT",
"dependencies": {
- "@mantine/store": "8.3.5",
+ "@mantine/store": "8.3.14",
"react-transition-group": "4.4.5"
},
"peerDependencies": {
- "@mantine/core": "8.3.5",
- "@mantine/hooks": "8.3.5",
+ "@mantine/core": "8.3.14",
+ "@mantine/hooks": "8.3.14",
"react": "^18.x || ^19.x",
"react-dom": "^18.x || ^19.x"
}
},
"mantine-ui/node_modules/@mantine/store": {
- "version": "8.3.5",
- "resolved": "https://registry.npmjs.org/@mantine/store/-/store-8.3.5.tgz",
- "integrity": "sha512-qN4fFsDMy86IV9oh1gZlDTv41RAsO0grjx90FGyT5QCv7NTgcavwxB74GBkhp45W8xn+Ms/awKy+6NxnmLmW1w==",
+ "version": "8.3.14",
+ "resolved": "https://registry.npmjs.org/@mantine/store/-/store-8.3.14.tgz",
+ "integrity": "sha512-bgW+fYHDOp7Pk4+lcEm3ZF7dD/sIMKHyR985cOqSHAYJPRcVFb+zcEK/SWoFZqlyA4qh08CNrASOaod8N0XKfA==",
"license": "MIT",
"peerDependencies": {
"react": "^18.x || ^19.x"
@@ -187,22 +172,22 @@
},
"module/codemirror-promql": {
"name": "@prometheus-io/codemirror-promql",
- "version": "0.307.3",
+ "version": "0.309.1",
"license": "Apache-2.0",
"dependencies": {
- "@prometheus-io/lezer-promql": "0.307.3",
- "lru-cache": "^11.2.2"
+ "@prometheus-io/lezer-promql": "0.309.1",
+ "lru-cache": "^11.2.5"
},
"devDependencies": {
- "@codemirror/autocomplete": "^6.19.0",
- "@codemirror/language": "^6.11.3",
- "@codemirror/lint": "^6.8.5",
- "@codemirror/state": "^6.5.2",
- "@codemirror/view": "^6.38.4",
- "@lezer/common": "^1.2.3",
- "@lezer/highlight": "^1.2.1",
- "@lezer/lr": "^1.4.2",
- "eslint-plugin-prettier": "^5.5.4",
+ "@codemirror/autocomplete": "^6.20.0",
+ "@codemirror/language": "^6.12.1",
+ "@codemirror/lint": "^6.9.3",
+ "@codemirror/state": "^6.5.4",
+ "@codemirror/view": "^6.39.12",
+ "@lezer/common": "^1.5.1",
+ "@lezer/highlight": "^1.2.3",
+ "@lezer/lr": "^1.4.8",
+ "eslint-plugin-prettier": "^5.5.5",
"isomorphic-fetch": "^3.0.0",
"nock": "^14.0.10"
},
@@ -220,13 +205,13 @@
},
"module/lezer-promql": {
"name": "@prometheus-io/lezer-promql",
- "version": "0.307.3",
+ "version": "0.309.1",
"license": "Apache-2.0",
"devDependencies": {
"@lezer/generator": "^1.8.0",
- "@lezer/highlight": "^1.2.1",
- "@lezer/lr": "^1.4.2",
- "@rollup/plugin-node-resolve": "^16.0.1"
+ "@lezer/highlight": "^1.2.3",
+ "@lezer/lr": "^1.4.8",
+ "@rollup/plugin-node-resolve": "^16.0.3"
},
"peerDependencies": {
"@lezer/highlight": "^1.1.2",
@@ -826,9 +811,9 @@
"peer": true
},
"node_modules/@codemirror/autocomplete": {
- "version": "6.19.0",
- "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.19.0.tgz",
- "integrity": "sha512-61Hfv3cF07XvUxNeC3E7jhG8XNi1Yom1G0lRC936oLnlF+jrbrv8rc/J98XlYzcsAoTVupfsf5fLej1aI8kyIg==",
+ "version": "6.20.0",
+ "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.0.tgz",
+ "integrity": "sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
@@ -850,23 +835,23 @@
}
},
"node_modules/@codemirror/language": {
- "version": "6.11.3",
- "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz",
- "integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==",
+ "version": "6.12.1",
+ "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.1.tgz",
+ "integrity": "sha512-Fa6xkSiuGKc8XC8Cn96T+TQHYj4ZZ7RdFmXA3i9xe/3hLHfwPZdM+dqfX0Cp0zQklBKhVD8Yzc8LS45rkqcwpQ==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.23.0",
- "@lezer/common": "^1.1.0",
+ "@lezer/common": "^1.5.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0",
"style-mod": "^4.0.0"
}
},
"node_modules/@codemirror/lint": {
- "version": "6.8.5",
- "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.5.tgz",
- "integrity": "sha512-s3n3KisH7dx3vsoeGMxsbRAgKe4O1vbrnKBClm99PU0fWxmxsx5rR2PfqQgIt+2MMJBHbiJ5rfIdLYfB9NNvsA==",
+ "version": "6.9.3",
+ "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.3.tgz",
+ "integrity": "sha512-y3YkYhdnhjDBAe0VIA0c4wVoFOvnp8CnAvfLqi0TqotIv92wIlAAP7HELOpLBsKwjAX6W92rSflA6an/2zBvXw==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
@@ -886,9 +871,9 @@
}
},
"node_modules/@codemirror/state": {
- "version": "6.5.2",
- "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz",
- "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
+ "version": "6.5.4",
+ "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.4.tgz",
+ "integrity": "sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw==",
"license": "MIT",
"dependencies": {
"@marijn/find-cluster-break": "^1.0.0"
@@ -907,9 +892,9 @@
}
},
"node_modules/@codemirror/view": {
- "version": "6.38.4",
- "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.4.tgz",
- "integrity": "sha512-hduz0suCcUSC/kM8Fq3A9iLwInJDl8fD1xLpTIk+5xkNm8z/FT7UsIa9sOXrkpChh+XXc18RzswE8QqELsVl+g==",
+ "version": "6.39.12",
+ "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.12.tgz",
+ "integrity": "sha512-f+/VsHVn/kOA9lltk/GFzuYwVVAKmOnNjxbrhkk3tPHntFqjWeI2TbIXx006YkBkqC10wZ4NsnWXCQiFPeAISQ==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.5.0",
@@ -1344,9 +1329,9 @@
}
},
"node_modules/@eslint-community/eslint-utils": {
- "version": "4.9.0",
- "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
- "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==",
+ "version": "4.9.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
+ "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1376,9 +1361,9 @@
}
},
"node_modules/@eslint-community/regexpp": {
- "version": "4.12.1",
- "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
- "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1386,13 +1371,12 @@
}
},
"node_modules/@eslint/compat": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.4.0.tgz",
- "integrity": "sha512-DEzm5dKeDBPm3r08Ixli/0cmxr8LkRdwxMRUIJBlSCpAwSrvFEJpVBzV+66JhDxiaqKxnRzCXhtiMiczF7Hglg==",
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.4.1.tgz",
+ "integrity": "sha512-cfO82V9zxxGBxcQDr1lfaYB7wykTa0b00mGa36FrJl7iTFd0Z2cHfEYuxcBRP/iNijCsWsEkA+jzT8hGYmv33w==",
"dev": true,
- "license": "Apache-2.0",
"dependencies": {
- "@eslint/core": "^0.16.0"
+ "@eslint/core": "^0.17.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1407,13 +1391,12 @@
}
},
"node_modules/@eslint/config-array": {
- "version": "0.21.0",
- "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz",
- "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==",
+ "version": "0.21.1",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz",
+ "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==",
"dev": true,
- "license": "Apache-2.0",
"dependencies": {
- "@eslint/object-schema": "^2.1.6",
+ "@eslint/object-schema": "^2.1.7",
"debug": "^4.3.1",
"minimatch": "^3.1.2"
},
@@ -1422,21 +1405,22 @@
}
},
"node_modules/@eslint/config-helpers": {
- "version": "0.3.1",
- "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz",
- "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==",
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
+ "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
"dev": true,
- "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0"
+ },
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@eslint/core": {
- "version": "0.16.0",
- "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz",
- "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==",
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
+ "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
"dev": true,
- "license": "Apache-2.0",
"dependencies": {
"@types/json-schema": "^7.0.15"
},
@@ -1445,9 +1429,9 @@
}
},
"node_modules/@eslint/eslintrc": {
- "version": "3.3.1",
- "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz",
- "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==",
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz",
+ "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1457,7 +1441,7 @@
"globals": "^14.0.0",
"ignore": "^5.2.0",
"import-fresh": "^3.2.1",
- "js-yaml": "^4.1.0",
+ "js-yaml": "^4.1.1",
"minimatch": "^3.1.2",
"strip-json-comments": "^3.1.1"
},
@@ -1481,9 +1465,9 @@
}
},
"node_modules/@eslint/js": {
- "version": "9.36.0",
- "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.36.0.tgz",
- "integrity": "sha512-uhCbYtYynH30iZErszX78U+nR3pJU3RHGQ57NXy5QupD4SBVwDeU8TNBy+MjMngc1UyIW9noKqsRqfjQTBU2dw==",
+ "version": "9.39.2",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz",
+ "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1494,66 +1478,64 @@
}
},
"node_modules/@eslint/object-schema": {
- "version": "2.1.6",
- "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz",
- "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==",
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
+ "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
"dev": true,
- "license": "Apache-2.0",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@eslint/plugin-kit": {
- "version": "0.3.5",
- "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz",
- "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==",
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
+ "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
"dev": true,
- "license": "Apache-2.0",
"dependencies": {
- "@eslint/core": "^0.15.2",
+ "@eslint/core": "^0.17.0",
"levn": "^0.4.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
- "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": {
- "version": "0.15.2",
- "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz",
- "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@types/json-schema": "^7.0.15"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- }
- },
"node_modules/@floating-ui/core": {
- "version": "1.7.3",
- "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
- "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
+ "version": "1.7.4",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.4.tgz",
+ "integrity": "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==",
"license": "MIT",
"dependencies": {
"@floating-ui/utils": "^0.2.10"
}
},
"node_modules/@floating-ui/dom": {
- "version": "1.7.4",
- "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz",
- "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
+ "version": "1.7.5",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.5.tgz",
+ "integrity": "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==",
"license": "MIT",
"dependencies": {
- "@floating-ui/core": "^1.7.3",
+ "@floating-ui/core": "^1.7.4",
"@floating-ui/utils": "^0.2.10"
}
},
+ "node_modules/@floating-ui/react": {
+ "version": "0.27.16",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.16.tgz",
+ "integrity": "sha512-9O8N4SeG2z++TSM8QA/KTeKFBVCNEz/AGS7gWPJf6KFRzmRWixFRnCnkPHRDwSVZW6QPDO6uT0P2SpWNKCc9/g==",
+ "dependencies": {
+ "@floating-ui/react-dom": "^2.1.6",
+ "@floating-ui/utils": "^0.2.10",
+ "tabbable": "^6.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=17.0.0",
+ "react-dom": ">=17.0.0"
+ }
+ },
"node_modules/@floating-ui/react-dom": {
"version": "2.1.6",
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz",
"integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==",
- "license": "MIT",
"dependencies": {
"@floating-ui/dom": "^1.7.4"
},
@@ -1676,9 +1658,9 @@
}
},
"node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": {
- "version": "3.14.1",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
- "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
+ "version": "3.14.2",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
+ "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
"dev": true,
"license": "MIT",
"peer": true,
@@ -2149,9 +2131,10 @@
}
},
"node_modules/@lezer/common": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz",
- "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA=="
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.1.tgz",
+ "integrity": "sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw==",
+ "license": "MIT"
},
"node_modules/@lezer/generator": {
"version": "1.8.0",
@@ -2168,18 +2151,17 @@
}
},
"node_modules/@lezer/highlight": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz",
- "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==",
- "license": "MIT",
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz",
+ "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==",
"dependencies": {
- "@lezer/common": "^1.0.0"
+ "@lezer/common": "^1.3.0"
}
},
"node_modules/@lezer/lr": {
- "version": "1.4.2",
- "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz",
- "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==",
+ "version": "1.4.8",
+ "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.8.tgz",
+ "integrity": "sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.0.0"
@@ -2229,44 +2211,6 @@
"@nexucis/fuzzy": "^0.5.1"
}
},
- "node_modules/@nodelib/fs.scandir": {
- "version": "2.1.5",
- "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
- "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@nodelib/fs.stat": "2.0.5",
- "run-parallel": "^1.1.9"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/@nodelib/fs.stat": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
- "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/@nodelib/fs.walk": {
- "version": "1.2.8",
- "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
- "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@nodelib/fs.scandir": "2.1.5",
- "fastq": "^1.6.0"
- },
- "engines": {
- "node": ">= 8"
- }
- },
"node_modules/@open-draft/deferred-promise": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz",
@@ -2318,14 +2262,14 @@
"link": true
},
"node_modules/@reduxjs/toolkit": {
- "version": "2.9.0",
- "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.9.0.tgz",
- "integrity": "sha512-fSfQlSRu9Z5yBkvsNhYF2rPS8cGXn/TZVrlwN1948QyZ8xMZ0JvP50S2acZNaf+o63u6aEeMjipFyksjIcWrog==",
+ "version": "2.11.2",
+ "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz",
+ "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==",
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.0.0",
"@standard-schema/utils": "^0.3.0",
- "immer": "^10.0.3",
+ "immer": "^11.0.0",
"redux": "^5.0.1",
"redux-thunk": "^3.1.0",
"reselect": "^5.1.0"
@@ -2351,11 +2295,10 @@
"license": "MIT"
},
"node_modules/@rollup/plugin-node-resolve": {
- "version": "16.0.1",
- "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.1.tgz",
- "integrity": "sha512-tk5YCxJWIG81umIvNkSod2qK5KyQW19qcBF/B78n1bjtOON6gzKoVeSzAE8yHCZEDmqkHKkxplExA8KzdJLJpA==",
+ "version": "16.0.3",
+ "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.3.tgz",
+ "integrity": "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==",
"dev": true,
- "license": "MIT",
"dependencies": {
"@rollup/pluginutils": "^5.0.1",
"@types/resolve": "1.20.2",
@@ -2723,12 +2666,12 @@
}
},
"node_modules/@tabler/icons-react": {
- "version": "3.35.0",
- "resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-3.35.0.tgz",
- "integrity": "sha512-XG7t2DYf3DyHT5jxFNp5xyLVbL4hMJYJhiSdHADzAjLRYfL7AnjlRfiHDHeXxkb2N103rEIvTsBRazxXtAUz2g==",
+ "version": "3.36.1",
+ "resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-3.36.1.tgz",
+ "integrity": "sha512-/8nOXeNeMoze9xY/QyEKG65wuvRhkT3q9aytaur6Gj8bYU2A98YVJyLc9MRmc5nVvpy+bRlrrwK/Ykr8WGyUWg==",
"license": "MIT",
"dependencies": {
- "@tabler/icons": "3.35.0"
+ "@tabler/icons": ""
},
"funding": {
"type": "github",
@@ -2739,9 +2682,9 @@
}
},
"node_modules/@tanstack/query-core": {
- "version": "5.90.2",
- "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.2.tgz",
- "integrity": "sha512-k/TcR3YalnzibscALLwxeiLUub6jN5EDLwKDiO7q5f4ICEoptJ+n9+7vcEFy5/x/i6Q+Lb/tXrsKCggf5uQJXQ==",
+ "version": "5.90.20",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.20.tgz",
+ "integrity": "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==",
"license": "MIT",
"funding": {
"type": "github",
@@ -2749,12 +2692,12 @@
}
},
"node_modules/@tanstack/react-query": {
- "version": "5.90.2",
- "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.2.tgz",
- "integrity": "sha512-CLABiR+h5PYfOWr/z+vWFt5VsOA2ekQeRQBFSKlcoW6Ndx/f8rfyVmq4LbgOM4GG2qtxAxjLYLOpCNTYm4uKzw==",
+ "version": "5.90.20",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.20.tgz",
+ "integrity": "sha512-vXBxa+qeyveVO7OA0jX1z+DeyCA4JKnThKv411jd5SORpBKgkcVnYKCiBgECvADvniBX7tobwBmg01qq9JmMJw==",
"license": "MIT",
"dependencies": {
- "@tanstack/query-core": "5.90.2"
+ "@tanstack/query-core": "5.90.20"
},
"funding": {
"type": "github",
@@ -2785,10 +2728,9 @@
}
},
"node_modules/@testing-library/jest-dom": {
- "version": "6.9.0",
- "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.0.tgz",
- "integrity": "sha512-QHdxYMJ0YPGKYofMc6zYvo7LOViVhdc6nPg/OtM2cf9MQrwEcTxFCs7d/GJ5eSyPkHzOiBkc/KfLdFJBHzldtQ==",
- "license": "MIT",
+ "version": "6.9.1",
+ "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz",
+ "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==",
"dependencies": {
"@adobe/css-tools": "^4.4.0",
"aria-query": "^5.0.0",
@@ -2810,9 +2752,9 @@
"license": "MIT"
},
"node_modules/@testing-library/react": {
- "version": "16.3.0",
- "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz",
- "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==",
+ "version": "16.3.2",
+ "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz",
+ "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.12.5"
@@ -2998,13 +2940,12 @@
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
- "dev": true,
- "license": "MIT"
+ "dev": true
},
"node_modules/@types/lodash": {
- "version": "4.17.20",
- "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz",
- "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==",
+ "version": "4.17.23",
+ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.23.tgz",
+ "integrity": "sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==",
"license": "MIT"
},
"node_modules/@types/node": {
@@ -3017,23 +2958,23 @@
}
},
"node_modules/@types/react": {
- "version": "19.1.16",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.16.tgz",
- "integrity": "sha512-WBM/nDbEZmDUORKnh5i1bTnAz6vTohUf9b8esSMu+b24+srbaxa04UbJgWx78CVfNXA20sNu0odEIluZDFdCog==",
+ "version": "19.2.13",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.13.tgz",
+ "integrity": "sha512-KkiJeU6VbYbUOp5ITMIc7kBfqlYkKA5KhEHVrGMmUUMt7NeaZg65ojdPk+FtNrBAOXNVM5QM72jnADjM+XVRAQ==",
"devOptional": true,
"license": "MIT",
"dependencies": {
- "csstype": "^3.0.2"
+ "csstype": "^3.2.2"
}
},
"node_modules/@types/react-dom": {
- "version": "19.1.9",
- "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.9.tgz",
- "integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==",
+ "version": "19.2.3",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
+ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"devOptional": true,
"license": "MIT",
"peerDependencies": {
- "@types/react": "^19.0.0"
+ "@types/react": "^19.2.0"
}
},
"node_modules/@types/resolve": {
@@ -3083,21 +3024,20 @@
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
- "version": "8.45.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.45.0.tgz",
- "integrity": "sha512-HC3y9CVuevvWCl/oyZuI47dOeDF9ztdMEfMH8/DW/Mhwa9cCLnK1oD7JoTVGW/u7kFzNZUKUoyJEqkaJh5y3Wg==",
+ "version": "8.54.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.54.0.tgz",
+ "integrity": "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@eslint-community/regexpp": "^4.10.0",
- "@typescript-eslint/scope-manager": "8.45.0",
- "@typescript-eslint/type-utils": "8.45.0",
- "@typescript-eslint/utils": "8.45.0",
- "@typescript-eslint/visitor-keys": "8.45.0",
- "graphemer": "^1.4.0",
- "ignore": "^7.0.0",
+ "@eslint-community/regexpp": "^4.12.2",
+ "@typescript-eslint/scope-manager": "8.54.0",
+ "@typescript-eslint/type-utils": "8.54.0",
+ "@typescript-eslint/utils": "8.54.0",
+ "@typescript-eslint/visitor-keys": "8.54.0",
+ "ignore": "^7.0.5",
"natural-compare": "^1.4.0",
- "ts-api-utils": "^2.1.0"
+ "ts-api-utils": "^2.4.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -3107,7 +3047,7 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "@typescript-eslint/parser": "^8.45.0",
+ "@typescript-eslint/parser": "^8.54.0",
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
@@ -3123,17 +3063,17 @@
}
},
"node_modules/@typescript-eslint/parser": {
- "version": "8.45.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.45.0.tgz",
- "integrity": "sha512-TGf22kon8KW+DeKaUmOibKWktRY8b2NSAZNdtWh798COm1NWx8+xJ6iFBtk3IvLdv6+LGLJLRlyhrhEDZWargQ==",
+ "version": "8.54.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.54.0.tgz",
+ "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/scope-manager": "8.45.0",
- "@typescript-eslint/types": "8.45.0",
- "@typescript-eslint/typescript-estree": "8.45.0",
- "@typescript-eslint/visitor-keys": "8.45.0",
- "debug": "^4.3.4"
+ "@typescript-eslint/scope-manager": "8.54.0",
+ "@typescript-eslint/types": "8.54.0",
+ "@typescript-eslint/typescript-estree": "8.54.0",
+ "@typescript-eslint/visitor-keys": "8.54.0",
+ "debug": "^4.4.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -3148,15 +3088,15 @@
}
},
"node_modules/@typescript-eslint/project-service": {
- "version": "8.45.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.45.0.tgz",
- "integrity": "sha512-3pcVHwMG/iA8afdGLMuTibGR7pDsn9RjDev6CCB+naRsSYs2pns5QbinF4Xqw6YC/Sj3lMrm/Im0eMfaa61WUg==",
+ "version": "8.54.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.54.0.tgz",
+ "integrity": "sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/tsconfig-utils": "^8.45.0",
- "@typescript-eslint/types": "^8.45.0",
- "debug": "^4.3.4"
+ "@typescript-eslint/tsconfig-utils": "^8.54.0",
+ "@typescript-eslint/types": "^8.54.0",
+ "debug": "^4.4.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -3170,14 +3110,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
- "version": "8.45.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.45.0.tgz",
- "integrity": "sha512-clmm8XSNj/1dGvJeO6VGH7EUSeA0FMs+5au/u3lrA3KfG8iJ4u8ym9/j2tTEoacAffdW1TVUzXO30W1JTJS7dA==",
+ "version": "8.54.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.54.0.tgz",
+ "integrity": "sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.45.0",
- "@typescript-eslint/visitor-keys": "8.45.0"
+ "@typescript-eslint/types": "8.54.0",
+ "@typescript-eslint/visitor-keys": "8.54.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -3188,9 +3128,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
- "version": "8.45.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.45.0.tgz",
- "integrity": "sha512-aFdr+c37sc+jqNMGhH+ajxPXwjv9UtFZk79k8pLoJ6p4y0snmYpPA52GuWHgt2ZF4gRRW6odsEj41uZLojDt5w==",
+ "version": "8.54.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.54.0.tgz",
+ "integrity": "sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3205,17 +3145,17 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
- "version": "8.45.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.45.0.tgz",
- "integrity": "sha512-bpjepLlHceKgyMEPglAeULX1vixJDgaKocp0RVJ5u4wLJIMNuKtUXIczpJCPcn2waII0yuvks/5m5/h3ZQKs0A==",
+ "version": "8.54.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.54.0.tgz",
+ "integrity": "sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.45.0",
- "@typescript-eslint/typescript-estree": "8.45.0",
- "@typescript-eslint/utils": "8.45.0",
- "debug": "^4.3.4",
- "ts-api-utils": "^2.1.0"
+ "@typescript-eslint/types": "8.54.0",
+ "@typescript-eslint/typescript-estree": "8.54.0",
+ "@typescript-eslint/utils": "8.54.0",
+ "debug": "^4.4.3",
+ "ts-api-utils": "^2.4.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -3230,9 +3170,9 @@
}
},
"node_modules/@typescript-eslint/types": {
- "version": "8.45.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.45.0.tgz",
- "integrity": "sha512-WugXLuOIq67BMgQInIxxnsSyRLFxdkJEJu8r4ngLR56q/4Q5LrbfkFRH27vMTjxEK8Pyz7QfzuZe/G15qQnVRA==",
+ "version": "8.54.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.54.0.tgz",
+ "integrity": "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3244,22 +3184,21 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
- "version": "8.45.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.45.0.tgz",
- "integrity": "sha512-GfE1NfVbLam6XQ0LcERKwdTTPlLvHvXXhOeUGC1OXi4eQBoyy1iVsW+uzJ/J9jtCz6/7GCQ9MtrQ0fml/jWCnA==",
+ "version": "8.54.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.54.0.tgz",
+ "integrity": "sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/project-service": "8.45.0",
- "@typescript-eslint/tsconfig-utils": "8.45.0",
- "@typescript-eslint/types": "8.45.0",
- "@typescript-eslint/visitor-keys": "8.45.0",
- "debug": "^4.3.4",
- "fast-glob": "^3.3.2",
- "is-glob": "^4.0.3",
- "minimatch": "^9.0.4",
- "semver": "^7.6.0",
- "ts-api-utils": "^2.1.0"
+ "@typescript-eslint/project-service": "8.54.0",
+ "@typescript-eslint/tsconfig-utils": "8.54.0",
+ "@typescript-eslint/types": "8.54.0",
+ "@typescript-eslint/visitor-keys": "8.54.0",
+ "debug": "^4.4.3",
+ "minimatch": "^9.0.5",
+ "semver": "^7.7.3",
+ "tinyglobby": "^0.2.15",
+ "ts-api-utils": "^2.4.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -3299,16 +3238,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
- "version": "8.45.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.45.0.tgz",
- "integrity": "sha512-bxi1ht+tLYg4+XV2knz/F7RVhU0k6VrSMc9sb8DQ6fyCTrGQLHfo7lDtN0QJjZjKkLA2ThrKuCdHEvLReqtIGg==",
+ "version": "8.54.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.54.0.tgz",
+ "integrity": "sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@eslint-community/eslint-utils": "^4.7.0",
- "@typescript-eslint/scope-manager": "8.45.0",
- "@typescript-eslint/types": "8.45.0",
- "@typescript-eslint/typescript-estree": "8.45.0"
+ "@eslint-community/eslint-utils": "^4.9.1",
+ "@typescript-eslint/scope-manager": "8.54.0",
+ "@typescript-eslint/types": "8.54.0",
+ "@typescript-eslint/typescript-estree": "8.54.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -3323,13 +3262,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
- "version": "8.45.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.45.0.tgz",
- "integrity": "sha512-qsaFBA3e09MIDAGFUrTk+dzqtfv1XPVz8t8d1f0ybTzrCY7BKiMC5cjrl1O/P7UmHsNyW90EYSkU/ZWpmXelag==",
+ "version": "8.54.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.54.0.tgz",
+ "integrity": "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.45.0",
+ "@typescript-eslint/types": "8.54.0",
"eslint-visitor-keys": "^4.2.1"
},
"engines": {
@@ -3341,9 +3280,9 @@
}
},
"node_modules/@uiw/codemirror-extensions-basic-setup": {
- "version": "4.25.2",
- "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.25.2.tgz",
- "integrity": "sha512-s2fbpdXrSMWEc86moll/d007ZFhu6jzwNu5cWv/2o7egymvLeZO52LWkewgbr+BUCGWGPsoJVWeaejbsb/hLcw==",
+ "version": "4.25.4",
+ "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.25.4.tgz",
+ "integrity": "sha512-YzNwkm0AbPv1EXhCHYR5v0nqfemG2jEB0Z3Att4rBYqKrlG7AA9Rhjc3IyBaOzsBu18wtrp9/+uhTyu7TXSRng==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
@@ -3368,16 +3307,16 @@
}
},
"node_modules/@uiw/react-codemirror": {
- "version": "4.25.2",
- "resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.25.2.tgz",
- "integrity": "sha512-XP3R1xyE0CP6Q0iR0xf3ed+cJzJnfmbLelgJR6osVVtMStGGZP3pGQjjwDRYptmjGHfEELUyyBLdY25h0BQg7w==",
+ "version": "4.25.4",
+ "resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.25.4.tgz",
+ "integrity": "sha512-ipO067oyfUw+DVaXhQCxkB0ZD9b7RnY+ByrprSYSKCHaULvJ3sqWYC/Zen6zVQ8/XC4o5EPBfatGiX20kC7XGA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.18.6",
"@codemirror/commands": "^6.1.0",
"@codemirror/state": "^6.1.1",
"@codemirror/theme-one-dark": "^6.0.0",
- "@uiw/codemirror-extensions-basic-setup": "4.25.2",
+ "@uiw/codemirror-extensions-basic-setup": "4.25.4",
"codemirror": "^6.0.0"
},
"funding": {
@@ -3837,9 +3776,9 @@
}
},
"node_modules/brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3936,6 +3875,20 @@
"node": ">=8"
}
},
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -4172,12 +4125,16 @@
"license": "MIT"
},
"node_modules/cookie": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
- "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
+ "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
"license": "MIT",
"engines": {
"node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
}
},
"node_modules/create-jest": {
@@ -4256,9 +4213,9 @@
}
},
"node_modules/csstype": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
- "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"license": "MIT"
},
"node_modules/data-urls": {
@@ -4276,10 +4233,9 @@
}
},
"node_modules/dayjs": {
- "version": "1.11.18",
- "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.18.tgz",
- "integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==",
- "license": "MIT"
+ "version": "1.11.19",
+ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz",
+ "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw=="
},
"node_modules/debug": {
"version": "4.4.3",
@@ -4464,6 +4420,21 @@
"url": "https://github.com/fb55/domutils?sponsor=1"
}
},
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/electron-to-chromium": {
"version": "1.5.228",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.228.tgz",
@@ -4516,6 +4487,26 @@
"is-arrayish": "^0.2.1"
}
},
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/es-module-lexer": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
@@ -4523,6 +4514,35 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/esbuild": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz",
@@ -4587,25 +4607,24 @@
}
},
"node_modules/eslint": {
- "version": "9.36.0",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.36.0.tgz",
- "integrity": "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==",
+ "version": "9.39.2",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz",
+ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
- "@eslint/config-array": "^0.21.0",
- "@eslint/config-helpers": "^0.3.1",
- "@eslint/core": "^0.15.2",
+ "@eslint/config-array": "^0.21.1",
+ "@eslint/config-helpers": "^0.4.2",
+ "@eslint/core": "^0.17.0",
"@eslint/eslintrc": "^3.3.1",
- "@eslint/js": "9.36.0",
- "@eslint/plugin-kit": "^0.3.5",
+ "@eslint/js": "9.39.2",
+ "@eslint/plugin-kit": "^0.4.1",
"@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1",
"@humanwhocodes/retry": "^0.4.2",
"@types/estree": "^1.0.6",
- "@types/json-schema": "^7.0.15",
"ajv": "^6.12.4",
"chalk": "^4.0.0",
"cross-spawn": "^7.0.6",
@@ -4664,14 +4683,14 @@
}
},
"node_modules/eslint-plugin-prettier": {
- "version": "5.5.4",
- "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.4.tgz",
- "integrity": "sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==",
+ "version": "5.5.5",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.5.tgz",
+ "integrity": "sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "prettier-linter-helpers": "^1.0.0",
- "synckit": "^0.11.7"
+ "prettier-linter-helpers": "^1.0.1",
+ "synckit": "^0.11.12"
},
"engines": {
"node": "^14.18.0 || >=16.0.0"
@@ -4708,13 +4727,13 @@
}
},
"node_modules/eslint-plugin-react-refresh": {
- "version": "0.4.22",
- "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.22.tgz",
- "integrity": "sha512-atkAG6QaJMGoTLc4MDAP+rqZcfwQuTIh2IqHWFLy2TEjxr0MOK+5BSG4RzL2564AAPpZkDRsZXAUz68kjnU6Ug==",
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.0.tgz",
+ "integrity": "sha512-ZYvmh7VfVgqR/7wR71I3Zl6hK/C5CcxdWYKZSpHawS5JCNgE4efhQWg/+/WPpgGAp9Ngp/rRZYyaIwmPQBq/lA==",
"dev": true,
"license": "MIT",
"peerDependencies": {
- "eslint": ">=8.40"
+ "eslint": ">=9"
}
},
"node_modules/eslint-scope": {
@@ -4747,19 +4766,6 @@
"url": "https://opencollective.com/eslint"
}
},
- "node_modules/eslint/node_modules/@eslint/core": {
- "version": "0.15.2",
- "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz",
- "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@types/json-schema": "^7.0.15"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- }
- },
"node_modules/espree": {
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
@@ -4925,36 +4931,6 @@
"dev": true,
"license": "Apache-2.0"
},
- "node_modules/fast-glob": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
- "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@nodelib/fs.stat": "^2.0.2",
- "@nodelib/fs.walk": "^1.2.3",
- "glob-parent": "^5.1.2",
- "merge2": "^1.3.0",
- "micromatch": "^4.0.4"
- },
- "engines": {
- "node": ">=8.6.0"
- }
- },
- "node_modules/fast-glob/node_modules/glob-parent": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
- "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "is-glob": "^4.0.1"
- },
- "engines": {
- "node": ">= 6"
- }
- },
"node_modules/fast-json-stable-stringify": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
@@ -4969,16 +4945,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/fastq": {
- "version": "1.17.1",
- "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
- "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "reusify": "^1.0.4"
- }
- },
"node_modules/fb-watchman": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz",
@@ -5052,14 +5018,16 @@
"dev": true
},
"node_modules/form-data": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
- "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"dev": true,
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
@@ -5120,6 +5088,31 @@
"node": "6.* || 8.* || >= 10.*"
}
},
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/get-nonce": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
@@ -5139,6 +5132,20 @@
"node": ">=8.0.0"
}
},
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/get-stream": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
@@ -5190,11 +5197,10 @@
}
},
"node_modules/globals": {
- "version": "16.4.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz",
- "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==",
+ "version": "16.5.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz",
+ "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">=18"
},
@@ -5202,6 +5208,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@@ -5209,13 +5228,6 @@
"dev": true,
"license": "ISC"
},
- "node_modules/graphemer": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
- "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/handlebars": {
"version": "4.7.8",
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz",
@@ -5247,6 +5259,35 @@
"node": ">=8"
}
},
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@@ -5372,9 +5413,9 @@
}
},
"node_modules/immer": {
- "version": "10.1.1",
- "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz",
- "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==",
+ "version": "11.1.3",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.3.tgz",
+ "integrity": "sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q==",
"license": "MIT",
"funding": {
"type": "opencollective",
@@ -6622,9 +6663,9 @@
"license": "MIT"
},
"node_modules/js-yaml": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
- "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -6804,9 +6845,9 @@
}
},
"node_modules/lodash": {
- "version": "4.17.21",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
- "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+ "version": "4.17.23",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
+ "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
"license": "MIT"
},
"node_modules/lodash.memoize": {
@@ -6842,10 +6883,10 @@
"license": "MIT"
},
"node_modules/lru-cache": {
- "version": "11.2.2",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz",
- "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==",
- "license": "ISC",
+ "version": "11.2.5",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz",
+ "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==",
+ "license": "BlueOak-1.0.0",
"engines": {
"node": "20 || >=22"
}
@@ -6905,6 +6946,16 @@
"tmpl": "1.0.5"
}
},
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/merge-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
@@ -6913,16 +6964,6 @@
"license": "MIT",
"peer": true
},
- "node_modules/merge2": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
- "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 8"
- }
- },
"node_modules/micromatch": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
@@ -7630,9 +7671,9 @@
}
},
"node_modules/prettier": {
- "version": "3.6.2",
- "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
- "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
+ "version": "3.8.1",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz",
+ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
"dev": true,
"license": "MIT",
"bin": {
@@ -7646,9 +7687,9 @@
}
},
"node_modules/prettier-linter-helpers": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
- "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==",
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz",
+ "integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -7754,52 +7795,31 @@
"license": "MIT",
"peer": true
},
- "node_modules/queue-microtask": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
- "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "license": "MIT"
- },
"node_modules/react": {
- "version": "19.1.1",
- "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz",
- "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==",
+ "version": "19.2.4",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
+ "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
- "version": "19.1.1",
- "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz",
- "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==",
+ "version": "19.2.4",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
+ "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
"license": "MIT",
"dependencies": {
- "scheduler": "^0.26.0"
+ "scheduler": "^0.27.0"
},
"peerDependencies": {
- "react": "^19.1.1"
+ "react": "^19.2.4"
}
},
"node_modules/react-infinite-scroll-component": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/react-infinite-scroll-component/-/react-infinite-scroll-component-6.1.0.tgz",
- "integrity": "sha512-SQu5nCqy8DxQWpnUVLx7V7b7LcA37aM7tvoWjTLZp1dk6EJibM5/4EJKzOnl07/BsM1Y40sKLuqjCwwH/xV0TQ==",
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/react-infinite-scroll-component/-/react-infinite-scroll-component-6.1.1.tgz",
+ "integrity": "sha512-R8YoOyiNDynSWmfVme5LHslsKrP+/xcRUWR2ies8UgUab9dtyw5ECnMCVPPmnmjjF4MWQmfVdRwRWcWaDgeyMA==",
"license": "MIT",
"dependencies": {
"throttle-debounce": "^2.1.0"
@@ -7905,9 +7925,9 @@
}
},
"node_modules/react-router": {
- "version": "7.9.3",
- "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.3.tgz",
- "integrity": "sha512-4o2iWCFIwhI/eYAIL43+cjORXYn/aRQPgtFRRZb3VzoyQ5Uej0Bmqj7437L97N9NJW4wnicSwLOLS+yCXfAPgg==",
+ "version": "7.13.0",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.0.tgz",
+ "integrity": "sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw==",
"license": "MIT",
"dependencies": {
"cookie": "^1.0.1",
@@ -7927,12 +7947,12 @@
}
},
"node_modules/react-router-dom": {
- "version": "7.9.3",
- "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.3.tgz",
- "integrity": "sha512-1QSbA0TGGFKTAc/aWjpfW/zoEukYfU4dc1dLkT/vvf54JoGMkW+fNA+3oyo2gWVW1GM7BxjJVHz5GnPJv40rvg==",
+ "version": "7.13.0",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.0.tgz",
+ "integrity": "sha512-5CO/l5Yahi2SKC6rGZ+HDEjpjkGaG/ncEP7eWFTvFxbHP8yeeI0PxTDjimtpXYlR3b3i9/WIL4VJttPrESIf2g==",
"license": "MIT",
"dependencies": {
- "react-router": "7.9.3"
+ "react-router": "7.13.0"
},
"engines": {
"node": ">=20.0.0"
@@ -8110,17 +8130,6 @@
"node": ">=10"
}
},
- "node_modules/reusify": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
- "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "iojs": ">=1.0.0",
- "node": ">=0.10.0"
- }
- },
"node_modules/rollup": {
"version": "4.34.9",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.9.tgz",
@@ -8167,30 +8176,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/run-parallel": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
- "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "queue-microtask": "^1.2.2"
- }
- },
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
@@ -8226,17 +8211,15 @@
}
},
"node_modules/scheduler": {
- "version": "0.26.0",
- "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
- "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==",
- "license": "MIT"
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="
},
"node_modules/semver": {
- "version": "7.7.2",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
- "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "version": "7.7.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"dev": true,
- "license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
@@ -8245,15 +8228,15 @@
}
},
"node_modules/serialize-query-params": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/serialize-query-params/-/serialize-query-params-2.0.2.tgz",
- "integrity": "sha512-1chMo1dST4pFA9RDXAtF0Rbjaut4is7bzFbI1Z26IuMub68pNCILku85aYmeFhvnY//BXUPUhoRMjYcsT93J/Q==",
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/serialize-query-params/-/serialize-query-params-2.0.4.tgz",
+ "integrity": "sha512-y9WzzDj3BsGgKLCh0ugiinufS//YqOfao/yVJjkXA4VLuyNCfHOLU/cbulGPxs3aeCqhvROw7qPL04JSZnCo0w==",
"license": "ISC"
},
"node_modules/set-cookie-parser": {
- "version": "2.7.1",
- "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
- "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
+ "version": "2.7.2",
+ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
+ "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
"license": "MIT"
},
"node_modules/shebang-command": {
@@ -8568,9 +8551,9 @@
"license": "MIT"
},
"node_modules/synckit": {
- "version": "0.11.11",
- "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz",
- "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==",
+ "version": "0.11.12",
+ "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz",
+ "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -8584,9 +8567,9 @@
}
},
"node_modules/tabbable": {
- "version": "6.2.0",
- "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
- "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew=="
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.3.0.tgz",
+ "integrity": "sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ=="
},
"node_modules/test-exclude": {
"version": "6.0.0",
@@ -8770,9 +8753,9 @@
}
},
"node_modules/ts-api-utils": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
- "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
+ "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -8783,9 +8766,9 @@
}
},
"node_modules/ts-jest": {
- "version": "29.4.4",
- "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.4.tgz",
- "integrity": "sha512-ccVcRABct5ZELCT5U0+DZwkXMCcOCLi2doHRrKy1nK/s7J7bch6TzJMsrY09WxgUUIP/ITfmcDS8D2yl63rnXw==",
+ "version": "29.4.6",
+ "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.6.tgz",
+ "integrity": "sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -8795,7 +8778,7 @@
"json5": "^2.2.3",
"lodash.memoize": "^4.1.2",
"make-error": "^1.3.6",
- "semver": "^7.7.2",
+ "semver": "^7.7.3",
"type-fest": "^4.41.0",
"yargs-parser": "^21.1.1"
},
@@ -9038,12 +9021,12 @@
}
},
"node_modules/use-query-params": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/use-query-params/-/use-query-params-2.2.1.tgz",
- "integrity": "sha512-i6alcyLB8w9i3ZK3caNftdb+UnbfBRNPDnc89CNQWkGRmDrm/gfydHvMBfVsQJRq3NoHOM2dt/ceBWG2397v1Q==",
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/use-query-params/-/use-query-params-2.2.2.tgz",
+ "integrity": "sha512-OwGab8u8/x2xZp9uSyBsx0kXlkR9IR436zbygsYVGikPYY3OJosvve6IJVGwIJPcfyb/YHwvPrUNu65/JR++Kw==",
"license": "ISC",
"dependencies": {
- "serialize-query-params": "^2.0.2"
+ "serialize-query-params": "^2.0.3"
},
"peerDependencies": {
"@reach/router": "^1.2.1",
@@ -9114,11 +9097,10 @@
}
},
"node_modules/vite": {
- "version": "6.3.6",
- "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz",
- "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==",
+ "version": "6.4.1",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
+ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"dev": true,
- "license": "MIT",
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
diff --git a/web/ui/package.json b/web/ui/package.json
index 4f4372a745..172e646aeb 100644
--- a/web/ui/package.json
+++ b/web/ui/package.json
@@ -1,7 +1,7 @@
{
"name": "prometheus-io",
"description": "Monorepo for the Prometheus UI",
- "version": "0.307.3",
+ "version": "0.309.1",
"private": true,
"scripts": {
"build": "bash build_ui.sh --all",
@@ -16,12 +16,12 @@
],
"devDependencies": {
"@types/jest": "^29.5.14",
- "@typescript-eslint/eslint-plugin": "^8.45.0",
- "@typescript-eslint/parser": "^8.45.0",
+ "@typescript-eslint/eslint-plugin": "^8.54.0",
+ "@typescript-eslint/parser": "^8.54.0",
"eslint-config-prettier": "^10.1.8",
- "prettier": "^3.6.2",
- "ts-jest": "^29.4.4",
+ "prettier": "^3.8.1",
+ "ts-jest": "^29.4.6",
"typescript": "^5.9.3",
- "vite": "^6.3.6"
+ "vite": "^6.4.1"
}
}
diff --git a/web/ui/react-app/src/globals.ts b/web/ui/react-app/src/globals.ts
index d2a5f1d50a..7a59bdbffd 100644
--- a/web/ui/react-app/src/globals.ts
+++ b/web/ui/react-app/src/globals.ts
@@ -1,6 +1,5 @@
import jquery from 'jquery';
+import moment from 'moment';
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-(window as any).jQuery = jquery;
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-(window as any).moment = require('moment');
+window.jQuery = jquery;
+window.moment = moment;
diff --git a/web/ui/react-app/src/pages/graph/HistogramChart.test.tsx b/web/ui/react-app/src/pages/graph/HistogramChart.test.tsx
index 27018c50ca..e9529282b1 100644
--- a/web/ui/react-app/src/pages/graph/HistogramChart.test.tsx
+++ b/web/ui/react-app/src/pages/graph/HistogramChart.test.tsx
@@ -68,7 +68,6 @@ describe('HistogramChart', () => {
scale: 'linear' as 'linear' | 'exponential',
};
-
beforeEach(() => {
mockFormat.mockClear();
mockResolvedOptions.mockClear();
@@ -163,7 +162,9 @@ describe('HistogramChart', () => {
describe('Exponential Scale', () => {
beforeEach(() => {
- wrapper = mount( );
+ wrapper = mount(
+
+ );
});
it('renders the correct number of buckets', () => {
@@ -225,17 +226,24 @@ describe('HistogramChart', () => {
expect(b4.find('.histogram-bucket').prop('style')).toHaveProperty('height', `${b4Height}%`);
expect(parseFloat(b4.prop('style')?.left as string)).toBeGreaterThan(0);
expect(parseFloat(b4.prop('style')?.width as string)).toBeGreaterThan(0);
- expect(parseFloat(b4.prop('style')?.left as string) + parseFloat(b4.prop('style')?.width as string)).toBeLessThanOrEqual(100.01);
+ expect(
+ parseFloat(b4.prop('style')?.left as string) + parseFloat(b4.prop('style')?.width as string)
+ ).toBeLessThanOrEqual(100.01);
});
it('handles zero-crossing bucket correctly in exponential scale', () => {
- wrapper = mount( );
+ wrapper = mount(
+
+ );
const buckets = wrapper.find('.histogram-bucket-slot');
const countMax = 15;
const b2 = buckets.at(1);
const b2Height = (5 / countMax) * 100;
- expect(b2.find('.histogram-bucket').prop('style')).toHaveProperty('height', expect.stringContaining(b2Height.toFixed(1)));
+ expect(b2.find('.histogram-bucket').prop('style')).toHaveProperty(
+ 'height',
+ expect.stringContaining(b2Height.toFixed(1))
+ );
expect(parseFloat(b2.prop('style')?.left as string)).toBeGreaterThanOrEqual(0);
expect(parseFloat(b2.prop('style')?.width as string)).toBeGreaterThan(0);
});
diff --git a/web/ui/react-app/src/pages/graph/HistorgramHelpers.test.tsx b/web/ui/react-app/src/pages/graph/HistorgramHelpers.test.tsx
index ea70a17d08..480fb3716f 100644
--- a/web/ui/react-app/src/pages/graph/HistorgramHelpers.test.tsx
+++ b/web/ui/react-app/src/pages/graph/HistorgramHelpers.test.tsx
@@ -37,34 +37,31 @@ describe('HistogramHelpers', () => {
];
const bucketsStartingWithZeroCross: Bucket[] = [
- [0, '-1', '1', '5'],
- [0, '1', '10', '20'],
- [0, '10', '100', '8'],
+ [0, '-1', '1', '5'],
+ [0, '1', '10', '20'],
+ [0, '10', '100', '8'],
];
- const bucketsEndingWithZeroCross: Bucket[] = [
- [0, '-100', '-10', '10'],
- [0, '-10', '-1', '15'],
- [0, '-1', '1', '5'],
+ const bucketsEndingWithZeroCross: Bucket[] = [
+ [0, '-100', '-10', '10'],
+ [0, '-10', '-1', '15'],
+ [0, '-1', '1', '5'],
];
- const singleZeroBucket: Bucket[] = [
- [0, '0', '0', '10'],
- ];
+ const singleZeroBucket: Bucket[] = [[0, '0', '0', '10']];
- const emptyBuckets: Bucket[] = [];
+ const emptyBuckets: Bucket[] = [];
- const bucketsWithZeroFallback: Bucket[] = [
- [0, '1', '10', '5'],
- [0, '10', '100', '15'],
- [0, '0', '0', '2']
- ];
-
- const bucketsNegThenPosNoCross: Bucket[] = [
- [0, '-10', '-1', '15'],
- [0, '5', '10', '20'],
- ];
+ const bucketsWithZeroFallback: Bucket[] = [
+ [0, '1', '10', '5'],
+ [0, '10', '100', '15'],
+ [0, '0', '0', '2'],
+ ];
+ const bucketsNegThenPosNoCross: Bucket[] = [
+ [0, '-10', '-1', '15'],
+ [0, '5', '10', '20'],
+ ];
describe('calculateDefaultExpBucketWidth', () => {
it('calculates width for a standard positive bucket', () => {
@@ -75,29 +72,30 @@ describe('HistogramHelpers', () => {
it('calculates width for a standard negative bucket', () => {
const lastBucket = bucketsAllNegative[bucketsAllNegative.length - 1];
- const expectedAbs = Math.abs(Math.log(Math.abs(parseFloat(lastBucket[2]))) - Math.log(Math.abs(parseFloat(lastBucket[1]))));
+ const expectedAbs = Math.abs(
+ Math.log(Math.abs(parseFloat(lastBucket[2]))) - Math.log(Math.abs(parseFloat(lastBucket[1])))
+ );
expect(calculateDefaultExpBucketWidth(lastBucket, bucketsAllNegative)).toBeCloseTo(expectedAbs);
});
it('uses the previous bucket if the last bucket is [0, 0]', () => {
- const lastBucket = bucketsWithZeroFallback[bucketsWithZeroFallback.length - 1];
- const expected = Math.log(100) - Math.log(10);
- expect(calculateDefaultExpBucketWidth(lastBucket, bucketsWithZeroFallback)).toBeCloseTo(expected);
+ const lastBucket = bucketsWithZeroFallback[bucketsWithZeroFallback.length - 1];
+ const expected = Math.log(100) - Math.log(10);
+ expect(calculateDefaultExpBucketWidth(lastBucket, bucketsWithZeroFallback)).toBeCloseTo(expected);
});
it('throws an error if only a single [0, 0] bucket exists', () => {
- const lastBucket = singleZeroBucket[0];
- expect(() => calculateDefaultExpBucketWidth(lastBucket, singleZeroBucket)).toThrow(
- 'Only one bucket in histogram ([-0, 0]). Cannot calculate defaultExpBucketWidth.'
- );
+ const lastBucket = singleZeroBucket[0];
+ expect(() => calculateDefaultExpBucketWidth(lastBucket, singleZeroBucket)).toThrow(
+ 'Only one bucket in histogram ([-0, 0]). Cannot calculate defaultExpBucketWidth.'
+ );
});
});
-
describe('findMinPositive', () => {
- it('returns the first positive left bound when all are positive', () => {
- expect(findMinPositive(bucketsAllPositive)).toEqual(1);
- });
+ it('returns the first positive left bound when all are positive', () => {
+ expect(findMinPositive(bucketsAllPositive)).toEqual(1);
+ });
it('returns the left bound when it is the first positive value', () => {
expect(findMinPositive(bucketsNegThenPosNoCross)).toBe(5);
@@ -108,43 +106,42 @@ describe('HistogramHelpers', () => {
});
it('returns the right bound when the first bucket crosses zero', () => {
- expect(findMinPositive(bucketsStartingWithZeroCross)).toBe(1);
+ expect(findMinPositive(bucketsStartingWithZeroCross)).toBe(1);
});
it('returns the right bound when the last bucket crosses zero', () => {
expect(findMinPositive(bucketsEndingWithZeroCross)).toBe(1);
});
- it('returns 0 when all buckets are negative', () => {
- expect(findMinPositive(bucketsAllNegative)).toBe(0);
- });
+ it('returns 0 when all buckets are negative', () => {
+ expect(findMinPositive(bucketsAllNegative)).toBe(0);
+ });
it('returns 0 for empty buckets', () => {
expect(findMinPositive(emptyBuckets)).toBe(0);
});
- it('returns 0 for only zero bucket', () => {
- expect(findMinPositive(singleZeroBucket)).toBe(0);
- });
+ it('returns 0 for only zero bucket', () => {
+ expect(findMinPositive(singleZeroBucket)).toBe(0);
+ });
it('returns 0 when buckets is undefined', () => {
expect(findMinPositive(undefined as any)).toBe(0);
});
- it('returns the correct positive bound with exact zero bucket present', () => {
- expect(findMinPositive(bucketsWithExactZeroBucket)).toBe(1);
- });
+ it('returns the correct positive bound with exact zero bucket present', () => {
+ expect(findMinPositive(bucketsWithExactZeroBucket)).toBe(1);
+ });
});
-
describe('findMaxNegative', () => {
- it('returns 0 when all buckets are positive', () => {
- expect(findMaxNegative(bucketsAllPositive)).toBe(0);
- });
+ it('returns 0 when all buckets are positive', () => {
+ expect(findMaxNegative(bucketsAllPositive)).toBe(0);
+ });
- it('returns the right bound of the last negative bucket when all are negative', () => {
- expect(findMaxNegative(bucketsAllNegative)).toEqual(-1);
- });
+ it('returns the right bound of the last negative bucket when all are negative', () => {
+ expect(findMaxNegative(bucketsAllNegative)).toEqual(-1);
+ });
it('returns the right bound of the bucket before the middle zero-crossing bucket', () => {
expect(findMaxNegative(bucketsCrossingZeroMid)).toEqual(-1);
@@ -155,7 +152,7 @@ describe('HistogramHelpers', () => {
});
it('returns the right bound of the bucket before the last zero-crossing bucket', () => {
- expect(findMaxNegative(bucketsEndingWithZeroCross)).toEqual(-1);
+ expect(findMaxNegative(bucketsEndingWithZeroCross)).toEqual(-1);
});
it('returns 0 for empty buckets', () => {
@@ -171,23 +168,28 @@ describe('HistogramHelpers', () => {
});
it('returns the right bound of the bucket before an exact zero bucket', () => {
- expect(findMaxNegative(bucketsWithExactZeroBucket)).toEqual(-1);
+ expect(findMaxNegative(bucketsWithExactZeroBucket)).toEqual(-1);
});
});
-
describe('findZeroBucket', () => {
it('returns the index of bucket strictly containing zero', () => {
expect(findZeroBucket(bucketsCrossingZeroMid)).toBe(2);
});
it('returns the index of bucket with zero as left boundary', () => {
- const buckets: Bucket[] = [[0, '-5','-1', '10'], [0, '0', '5', '15']];
+ const buckets: Bucket[] = [
+ [0, '-5', '-1', '10'],
+ [0, '0', '5', '15'],
+ ];
expect(findZeroBucket(buckets)).toBe(1);
});
it('returns the index of bucket with zero as right boundary', () => {
- const buckets: Bucket[] = [[0, '-5', '0', '10'], [0, '1', '5', '15']];
+ const buckets: Bucket[] = [
+ [0, '-5', '0', '10'],
+ [0, '1', '5', '15'],
+ ];
expect(findZeroBucket(buckets)).toBe(0);
});
@@ -208,49 +210,51 @@ describe('HistogramHelpers', () => {
});
it('returns 0 if the first bucket crosses zero', () => {
- expect(findZeroBucket(bucketsStartingWithZeroCross)).toBe(0);
+ expect(findZeroBucket(bucketsStartingWithZeroCross)).toBe(0);
});
- it('returns the last index if the last bucket crosses zero', () => {
- expect(findZeroBucket(bucketsEndingWithZeroCross)).toBe(2);
- });
+ it('returns the last index if the last bucket crosses zero', () => {
+ expect(findZeroBucket(bucketsEndingWithZeroCross)).toBe(2);
+ });
it('returns -1 when buckets array is empty', () => {
expect(findZeroBucket(emptyBuckets)).toBe(-1);
});
});
-
describe('findZeroAxisLeft', () => {
it('calculates correctly for linear scale crossing zero', () => {
- const rangeMin = -100; const rangeMax = 100;
+ const rangeMin = -100;
+ const rangeMax = 100;
const expected = '50%';
const result = findZeroAxisLeft('linear', rangeMin, rangeMax, 1, -1, 2, 0, 0, 0);
expect(result).toEqual(expected);
});
- it('calculates correctly for asymmetric linear scale crossing zero', () => {
- const rangeMin = -10; const rangeMax = 90;
- const expectedNumber = ((0 - rangeMin) / (rangeMax - rangeMin)) * 100;
- const resultString = findZeroAxisLeft('linear', rangeMin, rangeMax, 1, -1, 0, 0, 0, 0);
- expect(parseFloat(resultString)).toBeCloseTo(expectedNumber, 1);
- });
+ it('calculates correctly for asymmetric linear scale crossing zero', () => {
+ const rangeMin = -10;
+ const rangeMax = 90;
+ const expectedNumber = ((0 - rangeMin) / (rangeMax - rangeMin)) * 100;
+ const resultString = findZeroAxisLeft('linear', rangeMin, rangeMax, 1, -1, 0, 0, 0, 0);
+ expect(parseFloat(resultString)).toBeCloseTo(expectedNumber, 1);
+ });
it('calculates correctly for linear scale all positive (off-scale left)', () => {
- const rangeMin = 10; const rangeMax = 100;
+ const rangeMin = 10;
+ const rangeMax = 100;
const expectedNumber = ((0 - rangeMin) / (rangeMax - rangeMin)) * 100;
const resultString = findZeroAxisLeft('linear', rangeMin, rangeMax, 10, 0, -1, 0, 0, 0);
expect(parseFloat(resultString)).toBeCloseTo(expectedNumber, 1);
});
it('calculates correctly for linear scale all negative (off-scale right)', () => {
- const rangeMin = -100; const rangeMax = -10;
+ const rangeMin = -100;
+ const rangeMax = -10;
const expectedNumber = ((0 - rangeMin) / (rangeMax - rangeMin)) * 100;
const resultString = findZeroAxisLeft('linear', rangeMin, rangeMax, 0, -10, -1, 0, 0, 0);
expect(parseFloat(resultString)).toBeCloseTo(expectedNumber, 1);
});
-
const expMinPos = 1;
const expMaxNeg = -1;
const expZeroIdx = 2;
@@ -264,22 +268,46 @@ describe('HistogramHelpers', () => {
});
it('returns 100% for exponential scale when minPositive is 0', () => {
- expect(findZeroAxisLeft('exponential', -100, -1, 0, -1, -1, expNegWidth, expNegWidth + defaultExpBW, defaultExpBW)).toEqual('100%');
+ expect(
+ findZeroAxisLeft('exponential', -100, -1, 0, -1, -1, expNegWidth, expNegWidth + defaultExpBW, defaultExpBW)
+ ).toEqual('100%');
});
it('calculates position between buckets when zeroBucketIdx is -1 (exponential)', () => {
- const minPos = 5; const maxNeg = -1; const zeroIdx = -1;
+ const minPos = 5;
+ const maxNeg = -1;
+ const zeroIdx = -1;
const negW = Math.log(Math.abs(-1)) - Math.log(Math.abs(-10));
const posW = Math.log(10) - Math.log(5);
const totalW = Math.abs(negW) + posW + defaultExpBW;
const expectedNumber = (Math.abs(negW) / totalW) * 100;
- const resultString = findZeroAxisLeft('exponential', -10, 10, minPos, maxNeg, zeroIdx, Math.abs(negW), totalW, defaultExpBW);
+ const resultString = findZeroAxisLeft(
+ 'exponential',
+ -10,
+ 10,
+ minPos,
+ maxNeg,
+ zeroIdx,
+ Math.abs(negW),
+ totalW,
+ defaultExpBW
+ );
expect(parseFloat(resultString)).toBeCloseTo(expectedNumber, 1);
});
it('calculates position using bucket width when zeroBucketIdx exists (exponential)', () => {
const expectedNumber = ((expNegWidth + 0.5 * defaultExpBW) / expTotalWidth) * 100;
- const resultString = findZeroAxisLeft('exponential', -100, 100, expMinPos, expMaxNeg, expZeroIdx, expNegWidth, expTotalWidth, defaultExpBW);
+ const resultString = findZeroAxisLeft(
+ 'exponential',
+ -100,
+ 100,
+ expMinPos,
+ expMaxNeg,
+ expZeroIdx,
+ expNegWidth,
+ expTotalWidth,
+ defaultExpBW
+ );
expect(parseFloat(resultString)).toBeCloseTo(expectedNumber, 1);
});
@@ -288,7 +316,6 @@ describe('HistogramHelpers', () => {
});
});
-
describe('showZeroAxis', () => {
it('returns true when axis is between 5% and 95%', () => {
expect(showZeroAxis('5.01%')).toBe(true);
@@ -308,4 +335,4 @@ describe('HistogramHelpers', () => {
expect(showZeroAxis('120%')).toBe(false);
});
});
-});
\ No newline at end of file
+});
diff --git a/web/ui/react-app/src/types/index.d.ts b/web/ui/react-app/src/types/index.d.ts
index addf1cc702..9cf8fbd7cc 100644
--- a/web/ui/react-app/src/types/index.d.ts
+++ b/web/ui/react-app/src/types/index.d.ts
@@ -68,3 +68,8 @@ interface JQueryStatic {
scale: () => Color;
};
}
+
+interface Window {
+ jQuery: JQueryStatic;
+ moment: typeof import('moment');
+}
diff --git a/web/ui/react-app/src/utils/utils.test.ts b/web/ui/react-app/src/utils/utils.test.ts
index 93174df87b..61fcd733ab 100644
--- a/web/ui/react-app/src/utils/utils.test.ts
+++ b/web/ui/react-app/src/utils/utils.test.ts
@@ -333,13 +333,13 @@ describe('Utils', () => {
expect(parsePrometheusFloat('-1.7e+01')).toEqual(-17);
});
});
- describe('createExpressionLink',()=>{
- it('<....>builds link',()=>{
+ describe('createExpressionLink', () => {
+ it('<....>builds link', () => {
expect(createExpressionLink('up')).toEqual(
`../graph?g0.expr=up&g0.tab=1&g0.display_mode=${GraphDisplayMode.Lines}&g0.show_exemplars=0&g0.range_input=1h`
);
});
- it('url-encodes PromQL',() =>{
+ it('url-encodes PromQL', () => {
expect(createExpressionLink('ALERTS{alertname="HighCPU"}')).toEqual(
`../graph?g0.expr=ALERTS%7Balertname%3D%22High%20CPU%22%7D&g0.tab=1&g0.display_mode=${GraphDisplayMode.Lines}&g0.show_exemplars=0&g0.range_input=1h`
);
diff --git a/web/ui/ui.go b/web/ui/ui.go
index 2585951d4d..c427dcf119 100644
--- a/web/ui/ui.go
+++ b/web/ui/ui.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Prometheus Authors
+// 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
diff --git a/web/web.go b/web/web.go
index 2d353a8af8..583492abc9 100644
--- a/web/web.go
+++ b/web/web.go
@@ -1,4 +1,4 @@
-// Copyright 2013 The Prometheus Authors
+// 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
@@ -36,6 +36,7 @@ import (
"time"
"github.com/alecthomas/units"
+ "github.com/felixge/fgprof"
"github.com/grafana/regexp"
"github.com/mwitkow/go-conntrack"
remoteapi "github.com/prometheus/client_golang/exp/api/remote"
@@ -53,10 +54,12 @@ import (
"github.com/prometheus/prometheus/config"
"github.com/prometheus/prometheus/notifier"
"github.com/prometheus/prometheus/promql"
+ "github.com/prometheus/prometheus/promql/parser"
"github.com/prometheus/prometheus/rules"
"github.com/prometheus/prometheus/scrape"
"github.com/prometheus/prometheus/storage"
"github.com/prometheus/prometheus/template"
+ "github.com/prometheus/prometheus/util/features"
"github.com/prometheus/prometheus/util/httputil"
"github.com/prometheus/prometheus/util/netconnlimit"
"github.com/prometheus/prometheus/util/notifications"
@@ -111,6 +114,8 @@ const (
Stopping
)
+var fgprofHandler = fgprof.Handler()
+
// withStackTracer logs the stack trace in case the request panics. The function
// will re-raise the error which will then be handled by the net/http package.
// It is needed because the go-kit log package doesn't manage properly the
@@ -293,15 +298,19 @@ type Options struct {
ConvertOTLPDelta bool
NativeOTLPDeltaIngestion bool
IsAgent bool
- CTZeroIngestionEnabled bool
+ STZeroIngestionEnabled bool
EnableTypeAndUnitLabels bool
AppendMetadata bool
AppName string
AcceptRemoteWriteProtoMsgs remoteapi.MessageTypes
- Gatherer prometheus.Gatherer
- Registerer prometheus.Registerer
+ Gatherer prometheus.Gatherer
+ Registerer prometheus.Registerer
+ FeatureRegistry features.Collector
+
+ // Parser is the PromQL parser used for parsing query expressions.
+ Parser parser.Parser
}
// New initializes a new web Handler.
@@ -309,6 +318,9 @@ func New(logger *slog.Logger, o *Options) *Handler {
if logger == nil {
logger = promslog.NewNopLogger()
}
+ if o.Parser == nil {
+ o.Parser = parser.NewParser(parser.Options{})
+ }
m := newMetrics(o.Registerer)
router := route.New().
@@ -354,12 +366,20 @@ func New(logger *slog.Logger, o *Options) *Handler {
factoryAr := func(context.Context) api_v1.AlertmanagerRetriever { return h.notifier }
FactoryRr := func(context.Context) api_v1.RulesRetriever { return h.ruleManager }
- var app storage.Appendable
+ var (
+ app storage.Appendable
+ appV2 storage.AppendableV2
+ )
if o.EnableRemoteWriteReceiver || o.EnableOTLPWriteReceiver {
- app = h.storage
+ app, appV2 = h.storage, h.storage
}
- h.apiV1 = api_v1.NewAPI(h.queryEngine, h.storage, app, h.exemplarStorage, factorySPr, factoryTr, factoryAr,
+ version := ""
+ if o.Version != nil {
+ version = o.Version.Version
+ }
+
+ h.apiV1 = api_v1.NewAPI(h.queryEngine, h.storage, app, appV2, h.exemplarStorage, factorySPr, factoryTr, factoryAr,
func() config.Config {
h.mtx.RLock()
defer h.mtx.RUnlock()
@@ -394,13 +414,39 @@ func New(logger *slog.Logger, o *Options) *Handler {
o.EnableOTLPWriteReceiver,
o.ConvertOTLPDelta,
o.NativeOTLPDeltaIngestion,
- o.CTZeroIngestionEnabled,
+ o.STZeroIngestionEnabled,
o.LookbackDelta,
o.EnableTypeAndUnitLabels,
o.AppendMetadata,
nil,
+ o.FeatureRegistry,
+ api_v1.OpenAPIOptions{
+ ExternalURL: o.ExternalURL.String(),
+ Version: version,
+ },
+ o.Parser,
)
+ if r := o.FeatureRegistry; r != nil {
+ // Set dynamic API features (based on configuration).
+ r.Set(features.API, "lifecycle", o.EnableLifecycle)
+ r.Set(features.API, "admin", o.EnableAdminAPI)
+ r.Set(features.API, "remote_write_receiver", o.EnableRemoteWriteReceiver)
+ r.Set(features.API, "otlp_write_receiver", o.EnableOTLPWriteReceiver)
+ r.Set(features.OTLPReceiver, "delta_conversion", o.ConvertOTLPDelta)
+ r.Set(features.OTLPReceiver, "native_delta_ingestion", o.NativeOTLPDeltaIngestion)
+ r.Enable(features.API, "label_values_match") // match[] parameter for label values endpoint.
+ r.Enable(features.API, "query_warnings") // warnings in query responses.
+ r.Enable(features.API, "query_stats") // stats parameter for query endpoints.
+ r.Enable(features.API, "time_range_series") // start/end parameters for /series endpoint.
+ r.Enable(features.API, "time_range_labels") // start/end parameters for /labels endpoints.
+ r.Enable(features.API, "exclude_alerts") // exclude_alerts parameter for /rules endpoint.
+ r.Enable(features.API, "openapi_3.1") // OpenAPI 3.1 specification support.
+ r.Enable(features.API, "openapi_3.2") // OpenAPI 3.2 specification support.
+ r.Set(features.UI, "ui_v3", !o.UseOldUI)
+ r.Set(features.UI, "ui_v2", o.UseOldUI)
+ }
+
if o.RoutePrefix != "/" {
// If the prefix is missing for the root path, prepend it.
router.Get("/", func(w http.ResponseWriter, r *http.Request) {
@@ -434,13 +480,6 @@ func New(logger *slog.Logger, o *Options) *Handler {
reactAssetsRoot = "/static/react-app"
}
- // The console library examples at 'console_libraries/prom.lib' still depend on old asset files being served under `classic`.
- router.Get("/classic/static/*filepath", func(w http.ResponseWriter, r *http.Request) {
- r.URL.Path = path.Join("/static", route.Param(r.Context(), "filepath"))
- fs := server.StaticFileServer(ui.Assets)
- fs.ServeHTTP(w, r)
- })
-
router.Get("/version", h.version)
router.Get("/metrics", promhttp.Handler().ServeHTTP)
@@ -590,6 +629,8 @@ func serveDebug(w http.ResponseWriter, req *http.Request) {
pprof.Symbol(w, req)
case "trace":
pprof.Trace(w, req)
+ case "fgprof":
+ fgprofHandler.ServeHTTP(w, req)
default:
req.URL.Path = "/debug/pprof/" + subpath
pprof.Index(w, req)
@@ -620,8 +661,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 b07e26cfa8..5ead252cbe 100644
--- a/web/web_test.go
+++ b/web/web_test.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The Prometheus Authors
+// 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
@@ -118,12 +118,10 @@ func TestReadyAndHealthy(t *testing.T) {
}
}()
- // Give some time for the web goroutine to run since we need the server
- // to be up before starting tests.
- time.Sleep(5 * time.Second)
-
baseURL := "http://localhost" + port
+ waitForServerReady(t, baseURL, 5*time.Second)
+
resp, err := http.Get(baseURL + "/-/healthy")
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
@@ -140,11 +138,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)
}
@@ -235,12 +254,10 @@ func TestRoutePrefix(t *testing.T) {
}
}()
- // Give some time for the web goroutine to run since we need the server
- // to be up before starting tests.
- time.Sleep(5 * time.Second)
-
baseURL := "http://localhost" + port
+ waitForServerReady(t, baseURL+opts.RoutePrefix, 5*time.Second)
+
resp, err := http.Get(baseURL + opts.RoutePrefix + "/-/healthy")
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
@@ -307,6 +324,7 @@ func TestDebugHandler(t *testing.T) {
Host: "localhost.localdomain:9090",
Scheme: "http",
},
+ Version: &PrometheusVersion{},
}
handler := New(nil, opts)
handler.SetReady(Ready)
@@ -332,6 +350,7 @@ func TestHTTPMetrics(t *testing.T) {
Host: "localhost.localdomain:9090",
Scheme: "http",
},
+ Version: &PrometheusVersion{},
})
getReady := func() int {
t.Helper()
@@ -426,9 +445,9 @@ func TestShutdownWithStaleConnection(t *testing.T) {
close(closed)
}()
- // Give some time for the web goroutine to run since we need the server
- // to be up before starting tests.
- time.Sleep(5 * time.Second)
+ baseURL := "http://localhost" + port
+
+ waitForServerReady(t, baseURL, 5*time.Second)
// Open a socket, and don't use it. This connection should then be closed
// after the ReadTimeout.
@@ -477,23 +496,19 @@ func TestHandleMultipleQuitRequests(t *testing.T) {
close(closed)
}()
- // Give some time for the web goroutine to run since we need the server
- // to be up before starting tests.
- time.Sleep(5 * time.Second)
-
baseURL := opts.ExternalURL.Scheme + "://" + opts.ExternalURL.Host
+ waitForServerReady(t, baseURL, 5*time.Second)
+
start := make(chan struct{})
var wg sync.WaitGroup
for range 3 {
- wg.Add(1)
- go func() {
- defer wg.Done()
+ wg.Go(func() {
<-start
resp, err := http.Post(baseURL+"/-/quit", "", strings.NewReader(""))
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
- }()
+ })
}
close(start)
wg.Wait()
@@ -555,11 +570,10 @@ func TestAgentAPIEndPoints(t *testing.T) {
}
}()
- // Give some time for the web goroutine to run since we need the server
- // to be up before starting tests.
- time.Sleep(5 * time.Second)
baseURL := "http://localhost" + port + "/api/v1"
+ waitForServerReady(t, "http://localhost"+port, 5*time.Second)
+
// Test for non-available endpoints in the Agent mode.
for path, methods := range map[string][]string{
"/labels": {http.MethodGet, http.MethodPost},
@@ -688,9 +702,7 @@ func TestMultipleListenAddresses(t *testing.T) {
}
}()
- // Give some time for the web goroutine to run since we need the server
- // to be up before starting tests.
- time.Sleep(5 * time.Second)
+ waitForServerReady(t, "http://localhost"+port1, 5*time.Second)
// Set to ready.
webHandler.SetReady(Ready)
@@ -709,3 +721,24 @@ func TestMultipleListenAddresses(t *testing.T) {
cleanupTestResponse(t, resp)
}
}
+
+// Give some time for the web goroutine to run since we need the server
+// to be up before starting tests.
+func waitForServerReady(t *testing.T, baseURL string, timeout time.Duration) {
+ t.Helper()
+
+ interval := 100 * time.Millisecond
+ deadline := time.Now().Add(timeout)
+
+ for time.Now().Before(deadline) {
+ resp, err := http.Get(baseURL + "/-/healthy")
+ if resp != nil {
+ cleanupTestResponse(t, resp)
+ }
+ if err == nil && resp.StatusCode == http.StatusOK {
+ return
+ }
+ time.Sleep(interval)
+ }
+ t.Fatalf("Server did not become ready within %v", timeout)
+}