diff --git a/.circleci/config.yml b/.circleci/config.yml
deleted file mode 100644
index 965b2739410..00000000000
--- a/.circleci/config.yml
+++ /dev/null
@@ -1,33 +0,0 @@
----
-# Prometheus has switched to GitHub action.
-# Circle CI is not disabled repository-wise so that previous pull requests
-# continue working.
-# This file does not generate any CircleCI workflow.
-
-version: 2.1
-
-executors:
- golang:
- docker:
- - image: busybox
-
-jobs:
- noopjob:
- executor: golang
-
- steps:
- - run:
- command: "true"
-
-workflows:
- version: 2
- prometheus:
- jobs:
- - noopjob
- triggers:
- - schedule:
- cron: "0 0 30 2 *"
- filters:
- branches:
- only:
- - main
diff --git a/.dockerignore b/.dockerignore
index 5eca8e1b808..09670991552 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -3,7 +3,8 @@ data/
.tarballs/
!.build/linux-amd64/
-!.build/linux-armv7/
!.build/linux-arm64/
+!.build/linux-armv7/
!.build/linux-ppc64le/
+!.build/linux-riscv64/
!.build/linux-s390x/
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 00000000000..432caee6f78
--- /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 7f7cec9cda9..00000000000
--- 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/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index f4d17b3596b..bb4e2d24c99 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -1,4 +1,4 @@
-blank_issues_enabled: false
+blank_issues_enabled: true
contact_links:
- name: Prometheus Community Support
url: https://prometheus.io/community/
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index cf90177b1d1..58641d17d8a 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1,9 +1,5 @@
+
+#### Which issue(s) does the PR fix:
+
+
+#### Release notes for end users (**ALL** commits must be considered).
+*Reviewers should verify clarity and quality.*
+
+
+```release-notes
+
+```
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
deleted file mode 100644
index 89b2f4d0b6f..00000000000
--- a/.github/dependabot.yml
+++ /dev/null
@@ -1,35 +0,0 @@
-version: 2
-updates:
- - package-ecosystem: "gomod"
- directory: "/"
- schedule:
- interval: "monthly"
- groups:
- k8s.io:
- patterns:
- - "k8s.io/*"
- go.opentelemetry.io:
- patterns:
- - "go.opentelemetry.io/*"
- open-pull-requests-limit: 20
- - package-ecosystem: "gomod"
- directory: "/documentation/examples/remote_storage"
- schedule:
- interval: "monthly"
- - package-ecosystem: "npm"
- directory: "/web/ui"
- schedule:
- interval: "monthly"
- open-pull-requests-limit: 20
- - package-ecosystem: "github-actions"
- directory: "/"
- schedule:
- interval: "monthly"
- - package-ecosystem: "github-actions"
- directory: "/scripts"
- schedule:
- interval: "monthly"
- - package-ecosystem: "docker"
- directory: "/"
- schedule:
- interval: "monthly"
diff --git a/.github/workflows/automerge-dependabot.yml b/.github/workflows/automerge-dependabot.yml
new file mode 100644
index 00000000000..8b07f4df959
--- /dev/null
+++ b/.github/workflows/automerge-dependabot.yml
@@ -0,0 +1,30 @@
+---
+name: Dependabot auto-merge
+on: pull_request
+
+concurrency:
+ group: ${{ github.workflow }}-${{ (github.event.pull_request && github.event.pull_request.number) || github.ref || github.run_id }}
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+
+jobs:
+ dependabot:
+ permissions:
+ contents: write
+ pull-requests: write
+ runs-on: ubuntu-latest
+ if: ${{ github.event.pull_request.user.login == 'dependabot[bot]' && github.repository_owner == 'prometheus' }}
+ steps:
+ - name: Dependabot metadata
+ id: metadata
+ uses: dependabot/fetch-metadata@21025c705c08248db411dc16f3619e6b5f9ea21a # v2.5.0
+ with:
+ github-token: "${{ secrets.GITHUB_TOKEN }}"
+ - name: Enable auto-merge for Dependabot PRs
+ if: ${{steps.metadata.outputs.update-type == 'version-update:semver-minor' || steps.metadata.outputs.update-type == 'version-update:semver-patch'}}
+ run: gh pr merge --auto --merge "$PR_URL"
+ env:
+ PR_URL: ${{github.event.pull_request.html_url}}
+ GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
diff --git a/.github/workflows/buf-lint.yml b/.github/workflows/buf-lint.yml
index 3f6cf76e16f..d2c78cd6def 100644
--- a/.github/workflows/buf-lint.yml
+++ b/.github/workflows/buf-lint.yml
@@ -12,8 +12,10 @@ jobs:
name: lint
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- - uses: bufbuild/buf-setup-action@54abbed4fe8d8d45173eca4798b0c39a53a7b658 # v1.39.0
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
+ with:
+ persist-credentials: false
+ - uses: bufbuild/buf-setup-action@a47c93e0b1648d5651a065437926377d060baa99 # v1.50.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
- uses: bufbuild/buf-lint-action@06f9dd823d873146471cfaaf108a993fe00e5325 # v1.1.1
diff --git a/.github/workflows/buf.yml b/.github/workflows/buf.yml
index 632d38cb009..fcfdc43c6df 100644
--- a/.github/workflows/buf.yml
+++ b/.github/workflows/buf.yml
@@ -12,8 +12,10 @@ jobs:
runs-on: ubuntu-latest
if: github.repository_owner == 'prometheus'
steps:
- - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- - uses: bufbuild/buf-setup-action@54abbed4fe8d8d45173eca4798b0c39a53a7b658 # v1.39.0
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
+ with:
+ persist-credentials: false
+ - uses: bufbuild/buf-setup-action@a47c93e0b1648d5651a065437926377d060baa99 # v1.50.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
- uses: bufbuild/buf-lint-action@06f9dd823d873146471cfaaf108a993fe00e5325 # v1.1.1
@@ -23,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
new file mode 100644
index 00000000000..c2bd1b1d640
--- /dev/null
+++ b/.github/workflows/check_release_notes.yml
@@ -0,0 +1,28 @@
+name: 'Check release notes'
+on:
+ pull_request:
+ branches: [main, 'release-*']
+ types:
+ - opened
+ - reopened
+ - edited
+ - synchronize
+permissions:
+ contents: read
+ pull-requests: read
+
+jobs:
+ check_release_notes:
+ name: check
+ runs-on: ubuntu-latest
+ # Don't run this workflow on forks.
+ # 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
+ with:
+ persist-credentials: false
+ - env:
+ PR_DESCRIPTION: ${{ github.event.pull_request.body }}
+ run: |
+ echo "$PR_DESCRIPTION" | ./scripts/check_release_notes.sh
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 98d3d9a754f..7d9e613b8d3 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -3,6 +3,11 @@ name: CI
on:
pull_request:
push:
+ branches: [main, 'release-*']
+ tags: ['v*']
+
+permissions:
+ contents: read
jobs:
test_go:
@@ -11,13 +16,16 @@ jobs:
container:
# Whenever the Go version is updated here, .promu.yml
# should also be updated.
- image: quay.io/prometheus/golang-builder:1.23-base
+ image: quay.io/prometheus/golang-builder:1.26-base
steps:
- - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- - uses: prometheus/promci@45166329da36d74895901808f1c8c97efafc7f84 # v0.3.0
- - uses: ./.github/promci/actions/setup_environment
- - run: make GOOPTS=--tags=stringlabels GO_ONLY=1 SKIP_GOLANGCI_LINT=1
- - run: go test --tags=stringlabels ./tsdb/ -test.tsdb-isolation=false
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
+ with:
+ persist-credentials: false
+ - uses: prometheus/promci-setup@5af30ba8c199a91d6c04ebdc3c48e630e355f62d # v0.1.0
+ with:
+ enable_npm: true
+ - run: make GO_ONLY=1 SKIP_GOLANGCI_LINT=1
+ - run: go test ./tsdb/ -test.tsdb-isolation=false
- run: make -C documentation/examples/remote_storage
- run: make -C documentation/examples
@@ -25,16 +33,64 @@ jobs:
name: More Go tests
runs-on: ubuntu-latest
container:
- image: quay.io/prometheus/golang-builder:1.23-base
+ image: quay.io/prometheus/golang-builder:1.26-base
steps:
- - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- - uses: prometheus/promci@45166329da36d74895901808f1c8c97efafc7f84 # v0.3.0
- - uses: ./.github/promci/actions/setup_environment
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
+ with:
+ persist-credentials: false
+ - uses: prometheus/promci-setup@5af30ba8c199a91d6c04ebdc3c48e630e355f62d # v0.1.0
- run: go test --tags=dedupelabels ./...
- - run: GOARCH=386 go test ./cmd/prometheus
- - uses: ./.github/promci/actions/check_proto
+ - run: go test --tags=slicelabels -race ./cmd/prometheus ./model/textparse ./prompb/...
+ - run: go test --tags=forcedirectio -race ./tsdb/
+ - run: make protoc
+ - run: make proto
+ - run: git diff --exit-code
+
+ test_go_386:
+ name: Go tests for 32-bit x86
+ runs-on: ubuntu-latest
+ container:
+ image: quay.io/prometheus/golang-builder:1.26-base
+ steps:
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
- version: "3.15.8"
+ persist-credentials: false
+ - uses: prometheus/promci-setup@5af30ba8c199a91d6c04ebdc3c48e630e355f62d # v0.1.0
+ # NOTE(bwplotka): We limit concurrency to avoid issues around too many concurrent mmaps
+ # caused by parallel tests. See context: https://github.com/prometheus/prometheus/pull/18709
+ # Alternatively we could adjust each relevant test to have 386 aware parallelization setting.
+ - run: GOARCH=386 go test -parallel=1 ./...
+
+ list_lts_releases:
+ name: List active LTS releases
+ runs-on: ubuntu-latest
+ outputs:
+ versions: ${{ steps.lts.outputs.versions }}
+ steps:
+ - id: lts
+ # Emit the active LTS versions as a JSON array consumed by the matrix below.
+ run: |
+ versions=$(curl -sSf https://prometheus.io/download.json \
+ | jq -c '[.prometheus[] | select(.lts == true) | (.version | ltrimstr("v"))]')
+ echo "versions=$versions" >> "$GITHUB_OUTPUT"
+
+ test_version_upgrade:
+ name: Go tests for Prometheus upgrades and downgrades
+ needs: list_lts_releases
+ runs-on: ubuntu-latest
+ strategy:
+ # Run every LTS release even if one fails.
+ fail-fast: false
+ matrix:
+ lts_version: ${{ fromJSON(needs.list_lts_releases.outputs.versions) }}
+ container:
+ image: quay.io/prometheus/golang-builder:1.26-base
+ steps:
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
+ with:
+ persist-credentials: false
+ - uses: prometheus/promci-setup@5af30ba8c199a91d6c04ebdc3c48e630e355f62d # v0.1.0
+ - run: go test -v --race ./cmd/prometheus/ --test.version-upgrade=true --test.lts-version=${{ matrix.lts_version }} -run TestVersionUpgrade
test_go_oldest:
name: Go tests with previous Go version
@@ -44,9 +100,11 @@ jobs:
GOTOOLCHAIN: local
container:
# The go version in this image should be N-1 wrt test_go.
- image: quay.io/prometheus/golang-builder:1.22-base
+ image: quay.io/prometheus/golang-builder:1.25-base
steps:
- - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
+ with:
+ persist-credentials: false
- run: make build
# Don't run NPM build; don't run race-detector.
- run: make test GO_ONLY=1 test-flags=""
@@ -57,19 +115,20 @@ jobs:
# Whenever the Go version is updated here, .promu.yml
# should also be updated.
container:
- image: quay.io/prometheus/golang-builder:1.23-base
+ image: quay.io/prometheus/golang-builder:1.26-base
steps:
- - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- - uses: prometheus/promci@45166329da36d74895901808f1c8c97efafc7f84 # v0.3.0
- - uses: ./.github/promci/actions/setup_environment
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
+ with:
+ persist-credentials: false
+ - uses: prometheus/promci-setup@5af30ba8c199a91d6c04ebdc3c48e630e355f62d # v0.1.0
with:
enable_go: false
enable_npm: true
- run: make assets-tarball
- run: make ui-lint
- run: make ui-test
- - uses: ./.github/promci/actions/save_artifacts
+ - uses: prometheus/promci-artifacts/save@f9a587dbc0b2c78a0c54f8ad1cde71ea29a4b76f # v0.1.0
with:
directory: .tarballs
@@ -77,12 +136,14 @@ jobs:
name: Go tests on Windows
runs-on: windows-latest
steps:
- - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
+ with:
+ persist-credentials: false
+ - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
- go-version: 1.23.x
+ go-version: 1.26.x
- run: |
- $TestTargets = go list ./... | Where-Object { $_ -NotMatch "(github.com/prometheus/prometheus/discovery.*|github.com/prometheus/prometheus/config|github.com/prometheus/prometheus/web)"}
+ $TestTargets = go list ./... | Where-Object { $_ -NotMatch "(github.com/prometheus/prometheus/config|github.com/prometheus/prometheus/web)"}
go test $TestTargets -vet=off -v
shell: powershell
@@ -92,9 +153,11 @@ jobs:
# Whenever the Go version is updated here, .promu.yml
# should also be updated.
container:
- image: quay.io/prometheus/golang-builder:1.23-base
+ image: quay.io/prometheus/golang-builder:1.26-base
steps:
- - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
+ with:
+ persist-credentials: false
- run: go install ./cmd/promtool/.
- run: go install github.com/google/go-jsonnet/cmd/jsonnet@latest
- run: go install github.com/google/go-jsonnet/cmd/jsonnetfmt@latest
@@ -104,12 +167,35 @@ 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.26-base
+ steps:
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
+ with:
+ persist-credentials: false
+ - uses: prometheus/promci-setup@5af30ba8c199a91d6c04ebdc3c48e630e355f62d # v0.1.0
+ 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 -v --tags=compliance ./compliance/...
+
build:
name: Build Prometheus for common architectures
runs-on: ubuntu-latest
+ # Reuse the web UI built and tested by test_ui instead of rebuilding it in
+ # every crossbuild container.
+ needs: [test_ui]
if: |
!(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v2.'))
&&
+ !(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v3.'))
+ &&
!(github.event_name == 'pull_request' && startsWith(github.event.pull_request.base.ref, 'release-'))
&&
!(github.event_name == 'push' && github.event.ref == 'refs/heads/main')
@@ -117,19 +203,33 @@ jobs:
matrix:
thread: [ 0, 1, 2 ]
steps:
- - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- - uses: prometheus/promci@45166329da36d74895901808f1c8c97efafc7f84 # v0.3.0
- - uses: ./.github/promci/actions/build
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
+ with:
+ persist-credentials: false
+ - uses: prometheus/promci-artifacts/restore@f9a587dbc0b2c78a0c54f8ad1cde71ea29a4b76f # v0.1.0
+ - name: Extract pre-built UI assets
+ run: |
+ mkdir -p .prebuilt-ui
+ tar -xzvf .tarballs/prometheus-web-ui-*.tar.gz -C .prebuilt-ui
+ - uses: prometheus/promci/build@c7d527128ffb38ad3d7934795da35b252c7d51dd # v0.8.3
with:
- promu_opts: "-p linux/amd64 -p windows/amd64 -p linux/arm64 -p darwin/amd64 -p darwin/arm64 -p linux/386"
+ checkout: false
+ # Feed the restored UI assets into the crossbuild containers so make
+ # skips the UI build (see PREBUILT_ASSETS_STATIC_DIR in the Makefile).
+ promu_opts: "-p linux/amd64 -p windows/amd64 -p linux/arm64 -p darwin/amd64 -p darwin/arm64 -p linux/386 --env PREBUILT_ASSETS_STATIC_DIR=.prebuilt-ui/static"
parallelism: 3
thread: ${{ matrix.thread }}
build_all:
name: Build Prometheus for all architectures
runs-on: ubuntu-latest
+ # Reuse the web UI built and tested by test_ui instead of rebuilding it in
+ # every crossbuild container.
+ needs: [test_ui]
if: |
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v2.'))
||
+ (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v3.'))
+ ||
(github.event_name == 'pull_request' && startsWith(github.event.pull_request.base.ref, 'release-'))
||
(github.event_name == 'push' && github.event.ref == 'refs/heads/main')
@@ -140,17 +240,38 @@ jobs:
# Whenever the Go version is updated here, .promu.yml
# should also be updated.
steps:
- - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- - uses: prometheus/promci@45166329da36d74895901808f1c8c97efafc7f84 # v0.3.0
- - uses: ./.github/promci/actions/build
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
+ with:
+ persist-credentials: false
+ - uses: prometheus/promci-artifacts/restore@f9a587dbc0b2c78a0c54f8ad1cde71ea29a4b76f # v0.1.0
+ - name: Extract pre-built UI assets
+ run: |
+ mkdir -p .prebuilt-ui
+ tar -xzvf .tarballs/prometheus-web-ui-*.tar.gz -C .prebuilt-ui
+ - uses: prometheus/promci/build@c7d527128ffb38ad3d7934795da35b252c7d51dd # v0.8.3
with:
+ checkout: false
parallelism: 12
thread: ${{ matrix.thread }}
+ # Feed the restored UI assets into the crossbuild containers so make
+ # skips the UI build (see PREBUILT_ASSETS_STATIC_DIR in the Makefile).
+ promu_opts: --env PREBUILT_ASSETS_STATIC_DIR=.prebuilt-ui/static
build_all_status:
+ # This status check aggregates the individual matrix jobs of the "Build
+ # Prometheus for all architectures" step into a final status. Fails if a
+ # single matrix job fails, succeeds if all matrix jobs succeed.
+ # See https://github.com/orgs/community/discussions/4324 for why this is
+ # needed
name: Report status of build Prometheus for all architectures
runs-on: ubuntu-latest
needs: [build_all]
- if: github.event_name == 'pull_request' && startsWith(github.event.pull_request.base.ref, 'release-')
+ # The run condition needs to include always(). Otherwise actions
+ # behave unexpected:
+ # only "needs" will make the Status Report be skipped if one of the builds fails https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/using-jobs-in-a-workflow#defining-prerequisite-jobs
+ # And skipped is treated as success https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborat[…]n-repositories-with-code-quality-features/about-status-checks
+ # Adding always ensures that the status check is run independently of the
+ # results of Build All
+ if: always() && github.event_name == 'pull_request' && startsWith(github.event.pull_request.base.ref, 'release-')
steps:
- name: Successful build
if: ${{ !(contains(needs.*.result, 'failure')) && !(contains(needs.*.result, 'cancelled')) }}
@@ -159,105 +280,158 @@ 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.26-base
steps:
- name: Checkout repository
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- - name: Install Go
- uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
- cache: false
- go-version: 1.23.x
- - name: Run goyacc and check for diff
- run: make install-goyacc check-generated-parser
+ persist-credentials: false
+ - uses: prometheus/promci-setup@5af30ba8c199a91d6c04ebdc3c48e630e355f62d # v0.1.0
+ with:
+ 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@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
+ with:
+ persist-credentials: false
- name: Install Go
- uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
+ uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
- go-version: 1.23.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'
+ - name: Get golangci-lint version
+ id: golangci-lint-version
+ run: echo "version=$(make print-golangci-lint-version)" >> $GITHUB_OUTPUT
- name: Lint
- uses: golangci/golangci-lint-action@aaa42aa0628b4ae2578232a66b541047968fac86 # v6.1.0
+ uses: golangci/golangci-lint-action@82606bf257cbaff209d206a39f5134f0cfbfd2ee # v9.2.1
with:
args: --verbose
- # Make sure to sync this with Makefile.common and scripts/golangci-lint.yml.
- version: v1.60.2
+ version: ${{ steps.golangci-lint-version.outputs.version }}
+ - name: Lint with slicelabels
+ uses: golangci/golangci-lint-action@82606bf257cbaff209d206a39f5134f0cfbfd2ee # v9.2.1
+ with:
+ args: --verbose --build-tags=slicelabels
+ version: ${{ steps.golangci-lint-version.outputs.version }}
+ - name: Lint with dedupelabels
+ uses: golangci/golangci-lint-action@82606bf257cbaff209d206a39f5134f0cfbfd2ee # v9.2.1
+ with:
+ args: --verbose --build-tags=dedupelabels
+ version: ${{ steps.golangci-lint-version.outputs.version }}
+ - name: Lint in compliance
+ uses: golangci/golangci-lint-action@82606bf257cbaff209d206a39f5134f0cfbfd2ee # v9.2.1
+ with:
+ args: --verbose
+ working-directory: compliance
+ version: ${{ steps.golangci-lint-version.outputs.version }}
+ - name: Lint in documentation/examples/remote_storage
+ uses: golangci/golangci-lint-action@82606bf257cbaff209d206a39f5134f0cfbfd2ee # v9.2.1
+ 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'
codeql:
uses: ./.github/workflows/codeql-analysis.yml
+ permissions:
+ contents: read
+ security-events: write
publish_main:
name: Publish main branch artifacts
runs-on: ubuntu-latest
+ permissions:
+ packages: write
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@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- - uses: prometheus/promci@45166329da36d74895901808f1c8c97efafc7f84 # v0.3.0
- - uses: ./.github/promci/actions/publish_main
+ - uses: prometheus/promci/publish_main@c7d527128ffb38ad3d7934795da35b252c7d51dd # v0.8.3
with:
docker_hub_login: ${{ secrets.docker_hub_login }}
docker_hub_password: ${{ secrets.docker_hub_password }}
+ ghcr_io_password: ${{ github.token }}
quay_io_login: ${{ secrets.quay_io_login }}
quay_io_password: ${{ secrets.quay_io_password }}
publish_release:
name: Publish release artefacts
runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ packages: write
needs: [test_ui, test_go, test_go_more, test_go_oldest, test_windows, golangci, codeql, build_all]
- if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v2.')
+ if: |
+ (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v2.'))
+ ||
+ (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v3.'))
steps:
- - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- - uses: prometheus/promci@45166329da36d74895901808f1c8c97efafc7f84 # v0.3.0
- - uses: ./.github/promci/actions/publish_release
+ - uses: prometheus/promci/publish_release@c7d527128ffb38ad3d7934795da35b252c7d51dd # v0.8.3
with:
docker_hub_login: ${{ secrets.docker_hub_login }}
docker_hub_password: ${{ secrets.docker_hub_password }}
+ ghcr_io_password: ${{ github.token }}
quay_io_login: ${{ secrets.quay_io_login }}
quay_io_password: ${{ secrets.quay_io_password }}
- github_token: ${{ secrets.PROMBOT_GITHUB_TOKEN }}
+ github_token: ${{ github.token }}
publish_ui_release:
name: Publish UI on npm Registry
runs-on: ubuntu-latest
needs: [test_ui, codeql]
+ permissions:
+ contents: read
+ # Required for npm trusted publishing via OIDC.
+ id-token: write
steps:
- name: Checkout
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- - uses: prometheus/promci@45166329da36d74895901808f1c8c97efafc7f84 # v0.3.0
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
+ with:
+ persist-credentials: false
- name: Install nodejs
- uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: "web/ui/.nvmrc"
registry-url: "https://registry.npmjs.org"
- - uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
+ - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
- path: ~/.npm
- key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
+ version: 11
+ - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
+ with:
+ path: ~/.local/share/pnpm/store
+ key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
- ${{ runner.os }}-node-
+ ${{ runner.os }}-pnpm-
- name: Check libraries version
- if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v2.')
- run: ./scripts/ui_release.sh --check-package "$(echo ${{ github.ref_name }}|sed s/v2/v0/)"
+ if: |
+ (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v2.'))
+ ||
+ (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v3.'))
+ run: ./scripts/ui_release.sh --check-package "$(./scripts/get_module_version.sh ${GH_REF_NAME})"
+ env:
+ GH_REF_NAME: ${{ github.ref_name }}
- name: build
run: make assets
- name: Copy files before publishing libs
run: ./scripts/ui_release.sh --copy
- name: Publish dry-run libraries
- if: "!(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v2.'))"
+ if: |
+ !(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v2.'))
+ &&
+ !(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v3.'))
run: ./scripts/ui_release.sh --publish dry-run
- name: Publish libraries
- if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v2.')
+ if: |
+ (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v2.'))
+ ||
+ (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v3.'))
run: ./scripts/ui_release.sh --publish
- env:
- # The setup-node action writes an .npmrc file with this env variable
- # as the placeholder for the auth token
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 89aa2ba29b1..51c08d879e9 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -6,15 +6,14 @@ on:
schedule:
- cron: "26 14 * * 1"
-permissions:
- contents: read
- security-events: write
+permissions: {}
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
+ contents: read
security-events: write
strategy:
@@ -24,15 +23,17 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
+ with:
+ persist-credentials: false
- name: Initialize CodeQL
- uses: github/codeql-action/init@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6
+ uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
with:
languages: ${{ matrix.language }}
- name: Autobuild
- uses: github/codeql-action/autobuild@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6
+ uses: github/codeql-action/autobuild@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6
+ uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
diff --git a/.github/workflows/container_description.yml b/.github/workflows/container_description.yml
index 8ddbc34aebb..3bb36ccf43d 100644
--- a/.github/workflows/container_description.yml
+++ b/.github/workflows/container_description.yml
@@ -18,7 +18,9 @@ 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@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
+ with:
+ persist-credentials: false
- name: Set docker hub repo name
run: echo "DOCKER_REPO_NAME=$(make docker-repo-name)" >> $GITHUB_ENV
- name: Push README to Dockerhub
@@ -40,7 +42,9 @@ 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@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
+ with:
+ persist-credentials: false
- name: Set quay.io org name
run: echo "DOCKER_REPO=$(echo quay.io/${GITHUB_REPOSITORY_OWNER} | tr -d '-')" >> $GITHUB_ENV
- name: Set quay.io repo name
diff --git a/.github/workflows/funcbench.yml b/.github/workflows/funcbench.yml
deleted file mode 100644
index 8959f82142e..00000000000
--- a/.github/workflows/funcbench.yml
+++ /dev/null
@@ -1,61 +0,0 @@
-on:
- repository_dispatch:
- types: [funcbench_start]
-name: Funcbench Workflow
-permissions:
- contents: read
-
-jobs:
- run_funcbench:
- name: Running funcbench
- if: github.event.action == 'funcbench_start'
- runs-on: ubuntu-latest
- env:
- AUTH_FILE: ${{ secrets.TEST_INFRA_PROVIDER_AUTH }}
- BRANCH: ${{ github.event.client_payload.BRANCH }}
- BENCH_FUNC_REGEX: ${{ github.event.client_payload.BENCH_FUNC_REGEX }}
- PACKAGE_PATH: ${{ github.event.client_payload.PACKAGE_PATH }}
- GITHUB_TOKEN: ${{ secrets.PROMBOT_GITHUB_TOKEN }}
- GITHUB_ORG: prometheus
- GITHUB_REPO: prometheus
- GITHUB_STATUS_TARGET_URL: https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}
- LAST_COMMIT_SHA: ${{ github.event.client_payload.LAST_COMMIT_SHA }}
- GKE_PROJECT_ID: macro-mile-203600
- PR_NUMBER: ${{ github.event.client_payload.PR_NUMBER }}
- PROVIDER: gke
- ZONE: europe-west3-a
- steps:
- - name: Update status to pending
- run: >-
- curl -i -X POST
- -H "Authorization: Bearer $GITHUB_TOKEN"
- -H "Content-Type: application/json"
- --data '{"state":"pending","context":"funcbench-status","target_url":"'$GITHUB_STATUS_TARGET_URL'"}'
- "https://api.github.com/repos/$GITHUB_REPOSITORY/statuses/$LAST_COMMIT_SHA"
- - name: Prepare nodepool
- uses: docker://prominfra/funcbench:master
- with:
- entrypoint: "docker_entrypoint"
- args: make deploy
- - name: Delete all resources
- if: always()
- uses: docker://prominfra/funcbench:master
- with:
- entrypoint: "docker_entrypoint"
- args: make clean
- - name: Update status to failure
- if: failure()
- run: >-
- curl -i -X POST
- -H "Authorization: Bearer $GITHUB_TOKEN"
- -H "Content-Type: application/json"
- --data '{"state":"failure","context":"funcbench-status","target_url":"'$GITHUB_STATUS_TARGET_URL'"}'
- "https://api.github.com/repos/$GITHUB_REPOSITORY/statuses/$LAST_COMMIT_SHA"
- - name: Update status to success
- if: success()
- run: >-
- curl -i -X POST
- -H "Authorization: Bearer $GITHUB_TOKEN"
- -H "Content-Type: application/json"
- --data '{"state":"success","context":"funcbench-status","target_url":"'$GITHUB_STATUS_TARGET_URL'"}'
- "https://api.github.com/repos/$GITHUB_REPOSITORY/statuses/$LAST_COMMIT_SHA"
diff --git a/.github/workflows/fuzzing.yml b/.github/workflows/fuzzing.yml
index f3953cb2a48..856e01e35cb 100644
--- a/.github/workflows/fuzzing.yml
+++ b/.github/workflows/fuzzing.yml
@@ -1,28 +1,49 @@
-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], [FuzzXORChunk, FuzzXOR2Chunk], [FuzzParseProtobuf]]
steps:
- - name: Build Fuzzers
- id: build
- uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master
+ - name: Checkout repository
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
- oss-fuzz-project-name: "prometheus"
- dry-run: false
- - name: Run Fuzzers
- uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master
+ persist-credentials: false
+ - name: Install Go
+ uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
- oss-fuzz-project-name: "prometheus"
- fuzz-seconds: 600
- dry-run: false
- - name: Upload Crash
- uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
- if: failure() && steps.build.outcome == 'success'
+ go-version: 1.26.x
+ - name: Run Fuzzing
+ run: |
+ for fuzz_test in ${{ join(matrix.fuzz_test, ' ') }}; do
+ go test -fuzz="${fuzz_test}$" -fuzztime=4m ./util/fuzzing
+ done
+ id: fuzz
+ - name: Upload Crash Artifacts
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ if: failure()
with:
- name: artifacts
- path: ./out/artifacts
+ name: fuzz-artifacts-${{ join(matrix.fuzz_test, '-') }}
+ path: util/fuzzing/testdata/fuzz/
+ 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/go-test.yml b/.github/workflows/go-test.yml
new file mode 100644
index 00000000000..de9ea09c2a2
--- /dev/null
+++ b/.github/workflows/go-test.yml
@@ -0,0 +1,26 @@
+name: go-test
+
+on:
+ push:
+ branches: [ main ]
+ pull_request:
+ branches: [ main ]
+
+# Lifted from https://github.com/airbnb/prometheus/blob/main/.circleci/config.yml
+jobs:
+ test:
+ name: go test
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: prometheus/setup-environment
+ run: make promu
+ - name: run make
+ run: make GO_ONLY=1 test
+ env:
+ GOGC: 20
+ GOOPTS: "-p 2"
+ GOMAXPROCS: "2"
+ GO111MODULE: "on"
+ - name: test tsdb
+ run: go test ./tsdb/ -test.tsdb-isolation=false
diff --git a/.github/workflows/govulncheck.yml b/.github/workflows/govulncheck.yml
new file mode 100644
index 00000000000..621476dec41
--- /dev/null
+++ b/.github/workflows/govulncheck.yml
@@ -0,0 +1,30 @@
+---
+name: govulncheck
+on:
+ pull_request:
+ paths:
+ - VERSION
+ - .github/workflows/govulncheck.yml
+ push:
+ branches:
+ - main
+ - master
+ schedule:
+ - cron: '33 2 * * *'
+
+permissions:
+ contents: read
+
+jobs:
+ govulncheck:
+ runs-on: ubuntu-latest
+ name: Run govulncheck
+ steps:
+ - name: Install snmp_exporter/generator dependencies
+ id: snmp-deps
+ run: sudo apt-get update && sudo apt-get -y install libsnmp-dev
+ if: github.repository == 'prometheus/snmp_exporter'
+ - id: govulncheck
+ uses: golang/govulncheck-action@3fa7bd9cee2cfdf3499a8803b226e43de7b7cdb4 # master
+ env:
+ GOOS: ${{ contains(github.repository, 'windows_exporter') && 'windows' || '' }}
diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml
index e7e813e3b64..de630953056 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@89ae32b08ed1a541efecbab17912962a5e38981c # v6.0.2
with:
process-only: 'issues'
issue-inactive-days: '180'
diff --git a/.github/workflows/prombench.yml b/.github/workflows/prombench.yml
index 6ee172662bf..34bea41d546 100644
--- a/.github/workflows/prombench.yml
+++ b/.github/workflows/prombench.yml
@@ -2,6 +2,8 @@ on:
repository_dispatch:
types: [prombench_start, prombench_restart, prombench_stop]
name: Prombench Workflow
+permissions:
+ contents: read
env:
AUTH_FILE: ${{ secrets.TEST_INFRA_PROVIDER_AUTH }}
CLUSTER_NAME: test-infra
@@ -15,11 +17,15 @@ env:
PR_NUMBER: ${{ github.event.client_payload.PR_NUMBER }}
PROVIDER: gke
RELEASE: ${{ github.event.client_payload.RELEASE }}
+ BENCHMARK_VERSION: ${{ github.event.client_payload.BENCHMARK_VERSION }}
+ BENCHMARK_DIRECTORY: ${{ github.event.client_payload.BENCHMARK_DIRECTORY }}
ZONE: europe-west3-a
jobs:
benchmark_start:
name: Benchmark Start
if: github.event.action == 'prombench_start'
+ permissions:
+ statuses: write
runs-on: ubuntu-latest
steps:
- name: Update status to pending
@@ -34,8 +40,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: >-
@@ -55,6 +61,8 @@ jobs:
benchmark_cancel:
name: Benchmark Cancel
if: github.event.action == 'prombench_stop'
+ permissions:
+ statuses: write
runs-on: ubuntu-latest
steps:
- name: Update status to pending
@@ -69,8 +77,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: >-
@@ -90,6 +98,8 @@ jobs:
benchmark_restart:
name: Benchmark Restart
if: github.event.action == 'prombench_restart'
+ permissions:
+ statuses: write
runs-on: ubuntu-latest
steps:
- name: Update status to pending
@@ -104,10 +114,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 537e9abd84d..9395f2d036f 100644
--- a/.github/workflows/repo_sync.yml
+++ b/.github/workflows/repo_sync.yml
@@ -3,6 +3,7 @@ name: Sync repo files
on:
schedule:
- cron: '44 17 * * *'
+ workflow_dispatch: {}
permissions:
contents: read
@@ -13,7 +14,11 @@ jobs:
container:
image: quay.io/prometheus/golang-builder
steps:
- - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
+ with:
+ persist-credentials: false
- run: ./scripts/sync_repo_files.sh
env:
- GITHUB_TOKEN: ${{ secrets.PROMBOT_GITHUB_TOKEN }}
+ GITHUB_TOKEN: ${{ secrets.PROMBOT_REPOSYNC_TOKEN }}
+ GITHUB_TOKEN_PROMETHEUS: ${{ secrets.PROMBOT_REPOSYNC_TOKEN_PROMETHEUS }}
+ GITHUB_TOKEN_PROMETHEUS_COMMUNITY: ${{ secrets.PROMBOT_REPOSYNC_TOKEN_PROMETHEUS_COMMUNITY }}
diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml
index 82cccb2bc13..bfaad919c92 100644
--- a/.github/workflows/scorecards.yml
+++ b/.github/workflows/scorecards.yml
@@ -21,12 +21,12 @@ jobs:
steps:
- name: "Checkout code"
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # tag=v4.1.6
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: "Run analysis"
- uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # tag=v2.4.0
+ uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # tag=v2.4.3
with:
results_file: results.sarif
results_format: sarif
@@ -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@0b2256b8c012f0828dc542b3febcab082c67f72b # tag=v4.3.4
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
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@4dd16135b69a43b6c8efb853346f8437d92d3c93 # tag=v3.26.6
+ uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
with:
sarif_file: results.sarif
diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml
index d71bcbc9d83..74d037f8f17 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@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0
+ - uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.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 e85d766b095..cc4cf38e75b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,12 +22,14 @@ benchmark.txt
/documentation/examples/remote_storage/example_write_adapter/example_write_adapter
npm_licenses.tar.bz2
-/web/ui/static/react
+/web/ui/static
/vendor
/.build
+/go.work.sum
/**/node_modules
+.pnpm-store
# Ignore parser debug
y.output
diff --git a/.gitpod.Dockerfile b/.gitpod.Dockerfile
deleted file mode 100644
index 2370ec5f5c2..00000000000
--- a/.gitpod.Dockerfile
+++ /dev/null
@@ -1,33 +0,0 @@
-FROM gitpod/workspace-full
-
-# Set Node.js version as an environment variable.
-ENV CUSTOM_NODE_VERSION=16
-
-# Install and use the specified Node.js version via nvm.
-RUN bash -c ". .nvm/nvm.sh && nvm install ${CUSTOM_NODE_VERSION} && nvm use ${CUSTOM_NODE_VERSION} && nvm alias default ${CUSTOM_NODE_VERSION}"
-
-# Ensure nvm uses the default Node.js version in all new shells.
-RUN echo "nvm use default &>/dev/null" >> ~/.bashrc.d/51-nvm-fix
-
-# Remove any existing Go installation in $HOME path.
-RUN rm -rf $HOME/go $HOME/go-packages
-
-# Export go environment variables.
-RUN echo "export GOPATH=/workspace/go" >> ~/.bashrc.d/300-go && \
- echo "export GOBIN=\$GOPATH/bin" >> ~/.bashrc.d/300-go && \
- echo "export GOROOT=${HOME}/go" >> ~/.bashrc.d/300-go && \
- echo "export PATH=\$GOROOT/bin:\$GOBIN:\$PATH" >> ~/.bashrc
-
-# Reload the environment variables to ensure go environment variables are
-# available in subsequent commands.
-RUN bash -c "source ~/.bashrc && source ~/.bashrc.d/300-go"
-
-# Fetch the Go version dynamically from the Prometheus go.mod file and Install Go in $HOME path.
-RUN export CUSTOM_GO_VERSION=$(curl -sSL "https://raw.githubusercontent.com/prometheus/prometheus/main/go.mod" | awk '/^go/{print $2".0"}') && \
- curl -fsSL "https://dl.google.com/go/go${CUSTOM_GO_VERSION}.linux-amd64.tar.gz" | \
- tar -xz -C $HOME
-
-# Fetch the goyacc parser version dynamically from the Prometheus Makefile
-# and install it globally in $GOBIN path.
-RUN GOYACC_VERSION=$(curl -fsSL "https://raw.githubusercontent.com/prometheus/prometheus/main/Makefile" | awk -F'=' '/GOYACC_VERSION \?=/{gsub(/ /, "", $2); print $2}') && \
- go install "golang.org/x/tools/cmd/goyacc@${GOYACC_VERSION}"
diff --git a/.gitpod.yml b/.gitpod.yml
deleted file mode 100644
index bb38f6d2c4b..00000000000
--- a/.gitpod.yml
+++ /dev/null
@@ -1,20 +0,0 @@
-image:
- file: .gitpod.Dockerfile
-tasks:
- - init:
- make build
- command: |
- gp sync-done build
- ./prometheus --config.file=documentation/examples/prometheus.yml
- - command: |
- cd web/ui/
- gp sync-await build
- unset BROWSER
- export DANGEROUSLY_DISABLE_HOST_CHECK=true
- npm start
- openMode: split-right
-ports:
- - port: 3000
- onOpen: open-preview
- - port: 9090
- onOpen: ignore
diff --git a/.golangci.yml b/.golangci.yml
index 303cd33d8b0..b85ba104663 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -1,166 +1,243 @@
-run:
- timeout: 15m
+formatters:
+ enable:
+ - gci
+ - gofumpt
+ - goimports
+ settings:
+ gci:
+ sections:
+ - standard
+ - default
+ - prefix(github.com/prometheus/prometheus)
+ gofumpt:
+ extra-rules: true
+ goimports:
+ local-prefixes:
+ - github.com/prometheus/prometheus
-output:
- sort-results: true
+issues:
+ max-issues-per-linter: 0
+ max-same-issues: 0
linters:
+ # Keep this list sorted alphabetically
enable:
- depguard
- errorlint
+ - exptostd
+ - fatcontext
- gocritic
- godot
- - gofumpt
- - goimports
+ - govet
+ - loggercheck
- misspell
- - nolintlint
+ - modernize
+ - nilnesserr
+ # TODO(bwplotka): Enable once https://github.com/golangci/golangci-lint/issues/3228 is fixed.
+ # - nolintlint
- perfsprint
- predeclared
- revive
+ - sloglint
+ - staticcheck
- testifylint
- unconvert
- unused
- usestdlibvars
- whitespace
- - loggercheck
-issues:
- max-issues-per-linter: 0
- max-same-issues: 0
- # The default exclusions are too aggressive. For one, they
- # essentially disable any linting on doc comments. We disable
- # default exclusions here and add exclusions fitting our codebase
- # further down.
- exclude-use-default: false
- exclude-files:
- # Skip autogenerated files.
- - ^.*\.(pb|y)\.go$
- exclude-dirs:
- # Copied it from a different source.
- - storage/remote/otlptranslator/prometheusremotewrite
- - storage/remote/otlptranslator/prometheus
- exclude-rules:
- - linters:
- - errcheck
- # Taken from the default exclusions (that are otherwise disabled above).
- text: Error return value of .((os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*print(f|ln)?|os\.(Un)?Setenv). is not checked
- - linters:
- - govet
- # We use many Seek methods that do not follow the usual pattern.
- text: "stdmethods: method Seek.* should have signature Seek"
- - linters:
- - revive
- # We have stopped at some point to write doc comments on exported symbols.
- # TODO(beorn7): Maybe we should enforce this again? There are ~500 offenders right now.
- text: exported (.+) should have comment( \(or a comment on this block\))? or be unexported
- - linters:
- - gocritic
- text: "appendAssign"
- - path: _test.go
- linters:
- - errcheck
- - path: "tsdb/head_wal.go"
- linters:
- - errorlint
- - linters:
- - godot
- source: "^// ==="
- - linters:
- - perfsprint
- text: "fmt.Sprintf can be replaced with string concatenation"
-linters-settings:
- depguard:
- rules:
- main:
- deny:
- - pkg: "sync/atomic"
- desc: "Use go.uber.org/atomic instead of sync/atomic"
- - pkg: "github.com/stretchr/testify/assert"
- desc: "Use github.com/stretchr/testify/require instead of github.com/stretchr/testify/assert"
- - pkg: "github.com/go-kit/kit/log"
- desc: "Use github.com/go-kit/log instead of github.com/go-kit/kit/log"
- - pkg: "io/ioutil"
- desc: "Use corresponding 'os' or 'io' functions instead."
- - pkg: "regexp"
- desc: "Use github.com/grafana/regexp instead of regexp"
- - pkg: "github.com/pkg/errors"
- desc: "Use 'errors' or 'fmt' instead of github.com/pkg/errors"
- - pkg: "gzip"
- desc: "Use github.com/klauspost/compress instead of gzip"
- - pkg: "zlib"
- desc: "Use github.com/klauspost/compress instead of zlib"
- - pkg: "golang.org/x/exp/slices"
- desc: "Use 'slices' instead."
- errcheck:
- exclude-functions:
- # Don't flag lines such as "io.Copy(io.Discard, resp.Body)".
- - io.Copy
- # The next two are used in HTTP handlers, any error is handled by the server itself.
- - io.WriteString
- - (net/http.ResponseWriter).Write
- # No need to check for errors on server's shutdown.
- - (*net/http.Server).Shutdown
- # Never check for logger errors.
- - (github.com/go-kit/log.Logger).Log
- # Never check for rollback errors as Rollback() is called when a previous error was detected.
- - (github.com/prometheus/prometheus/storage.Appender).Rollback
- goimports:
- local-prefixes: github.com/prometheus/prometheus
- gofumpt:
- extra-rules: true
- perfsprint:
- # Optimizes `fmt.Errorf`.
- errorf: false
- revive:
- # By default, revive will enable only the linting rules that are named in the configuration file.
- # So, it's needed to explicitly enable all required rules here.
+ exclusions:
+ paths:
+ # Skip autogenerated files.
+ - ^.*\.(l|pb|y)\.go$
rules:
- # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md
- - name: blank-imports
- - name: comment-spacings
- - name: context-as-argument
- arguments:
- # Allow functions with test or bench signatures.
- - allowTypesBefore: "*testing.T,testing.TB"
- - name: context-keys-type
- - name: dot-imports
- # A lot of false positives: incorrectly identifies channel draining as "empty code block".
- # See https://github.com/mgechev/revive/issues/386
- - name: empty-block
- disabled: true
- - name: error-naming
- - name: error-return
- - name: error-strings
- - name: errorf
- - name: exported
- - name: increment-decrement
- - name: indent-error-flow
- - name: package-comments
- # TODO(beorn7): Currently, we have a lot of missing package doc comments. Maybe we should have them.
- disabled: true
- - name: range
- - name: receiver-naming
- - name: redefines-builtin-id
- - name: superfluous-else
- - name: time-naming
- - name: unexported-return
- - name: unreachable-code
- - name: unused-parameter
- disabled: true
- - name: var-declaration
- - name: var-naming
- testifylint:
- disable:
- - float-compare
- - go-require
- enable:
- - bool-compare
- - compares
- - empty
- - error-is-as
- - error-nil
- - expected-actual
- - len
- - require-error
- - suite-dont-use-pkg
- - suite-extra-assert-call
+ - linters:
+ - errcheck
+ # Taken from the default exclusions in v1.
+ text: Error return value of .((os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*print(f|ln)?|os\.(Un)?Setenv). is not checked
+ - linters:
+ - govet
+ # We use many Seek methods that do not follow the usual pattern.
+ text: "stdmethods: method Seek.* should have signature Seek"
+ - linters:
+ - revive
+ # We have stopped at some point to write doc comments on exported symbols.
+ # TODO(beorn7): Maybe we should enforce this again? There are ~500 offenders right now.
+ text: exported (.+) should have comment( \(or a comment on this block\))? or be unexported
+ - linters:
+ - errcheck
+ path: _test.go
+ - linters:
+ - errorlint
+ path: "tsdb/head_wal.go"
+ - linters:
+ - godot
+ source: "^// ==="
+ - linters:
+ - staticcheck
+ text: '(labels.MetricName|v1\.(Endpoints|EndpointSubset|EndpointPort|EndpointAddress)) is deprecated: .*'
+ warn-unused: true
+ settings:
+ depguard:
+ rules:
+ main:
+ deny:
+ - pkg: "sync/atomic"
+ desc: "Use go.uber.org/atomic instead of sync/atomic"
+ - pkg: "github.com/stretchr/testify/assert"
+ desc: "Use github.com/stretchr/testify/require instead of github.com/stretchr/testify/assert"
+ - pkg: "github.com/go-kit/kit/log"
+ desc: "Use github.com/go-kit/log instead of github.com/go-kit/kit/log"
+ - pkg: "io/ioutil"
+ desc: "Use corresponding 'os' or 'io' functions instead."
+ - pkg: "regexp"
+ desc: "Use github.com/grafana/regexp instead of regexp"
+ - pkg: "github.com/pkg/errors"
+ desc: "Use 'errors' or 'fmt' instead of github.com/pkg/errors"
+ - pkg: "gzip"
+ desc: "Use github.com/klauspost/compress instead of gzip"
+ - pkg: "zlib"
+ 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)".
+ - io.Copy
+ # The next two are used in HTTP handlers, any error is handled by the server itself.
+ - io.WriteString
+ - (net/http.ResponseWriter).Write
+ # No need to check for errors on server's shutdown.
+ - (*net/http.Server).Shutdown
+ # Never check for rollback errors as Rollback() is called when a previous error was detected.
+ - (github.com/prometheus/prometheus/storage.Appender).Rollback
+ gocritic:
+ enable-all: true
+ disabled-checks:
+ - appendAssign
+ - appendCombine
+ - badLock
+ - boolExprSimplify
+ - commentedOutCode
+ - deferInLoop
+ - docStub
+ - equalFold
+ - evalOrder
+ - exposedSyncMutex
+ - hexLiteral
+ - hugeParam
+ - importShadow
+ - nestingReduce
+ - ptrToRefParam
+ - preferStringWriter
+ - rangeValCopy
+ - redundantSprint
+ - regexpSimplify
+ - returnAfterHttpError
+ - sloppyReassign
+ - sprintfQuotedString
+ - stringsCompare
+ - todoCommentWithoutDetail
+ - tooManyResultsChecker
+ - typeUnparen
+ - truncateCmp
+ - typeDefFirst
+ - unlabelStmt
+ - unnamedResult
+ - unnecessaryBlock
+ - unnecessaryDefer
+ - whyNoLint
+ govet:
+ disable:
+ - 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
+ # Disable newexpr check for now since it introduces too many changes in our existing codebase.
+ # To be re-enabled as a part of https://github.com/prometheus/prometheus/issues/18066.
+ - newexpr
+ perfsprint:
+ # Optimizes even if it requires an int or uint type cast.
+ int-conversion: true
+ # Optimizes into `err.Error()` even if it is only equivalent for non-nil errors.
+ err-error: true
+ # Optimizes `fmt.Errorf`.
+ errorf: true
+ # Optimizes `fmt.Sprintf` with only one argument.
+ sprintf1: true
+ # Optimizes into strings concatenation.
+ strconcat: false
+ # Disable optimization of concat loop.
+ concat-loop: false
+ revive:
+ # By default, revive will enable only the linting rules that are named in the configuration file.
+ # So, it's needed to explicitly enable all required rules here.
+ rules:
+ # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md
+ - name: blank-imports
+ - name: comment-spacings
+ - name: context-as-argument
+ arguments:
+ # Allow functions with test or bench signatures.
+ - allowTypesBefore: '*testing.T,testing.TB'
+ - name: context-keys-type
+ - name: dot-imports
+ - name: early-return
+ arguments:
+ - "preserveScope"
+ # A lot of false positives: incorrectly identifies channel draining as "empty code block".
+ # See https://github.com/mgechev/revive/issues/386
+ - name: empty-block
+ disabled: true
+ - name: error-naming
+ - name: error-return
+ - name: error-strings
+ - name: errorf
+ - name: exported
+ - name: increment-decrement
+ - name: indent-error-flow
+ arguments:
+ - "preserveScope"
+ - name: package-comments
+ # TODO(beorn7): Currently, we have a lot of missing package doc comments. Maybe we should have them.
+ disabled: true
+ - name: range
+ - name: receiver-naming
+ - name: redefines-builtin-id
+ - name: superfluous-else
+ arguments:
+ - "preserveScope"
+ - name: time-naming
+ - name: unexported-return
+ - name: unreachable-code
+ - name: unused-parameter
+ - 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
+ - go-require
+ enable-all: true
+
+output:
+ show-stats: false
+
+run:
+ timeout: 15m
+
+version: "2"
diff --git a/.promu.yml b/.promu.yml
index 699a7fa5859..15b2f0a4977 100644
--- a/.promu.yml
+++ b/.promu.yml
@@ -1,7 +1,7 @@
go:
# Whenever the Go version is updated here,
# .github/workflows should also be updated.
- version: 1.23
+ version: 1.26
repository:
path: github.com/prometheus/prometheus
build:
@@ -14,10 +14,8 @@ build:
all:
- netgo
- builtinassets
- - stringlabels
windows:
- builtinassets
- - stringlabels
ldflags: |
-X github.com/prometheus/common/version.Version={{.Version}}
-X github.com/prometheus/common/version.Revision={{.Revision}}
@@ -28,14 +26,12 @@ tarball:
# Whenever there are new files to include in the tarball,
# remember to make sure the new files will be generated after `make build`.
files:
- - consoles
- - console_libraries
- documentation/examples/prometheus.yml
- LICENSE
- NOTICE
- - npm_licenses.tar.bz2
crossbuild:
platforms:
+ - aix
- darwin
- dragonfly
- freebsd
diff --git a/.yamllint b/.yamllint
index 1859cb624bd..b329f464fb3 100644
--- a/.yamllint
+++ b/.yamllint
@@ -1,7 +1,8 @@
---
extends: default
ignore: |
- ui/react-app/node_modules
+ **/node_modules
+ web/api/v1/testdata/openapi_*_golden.yaml
rules:
braces:
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 00000000000..bdc352b8f09
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,148 @@
+# Agents Guide for Prometheus
+
+This document captures patterns and preferences observed from maintainer reviews
+of recently merged pull requests. Use it to align your contributions with what
+maintainers expect.
+
+---
+
+## PR Title Format
+
+Titles must follow `area: short description`, using a prefix that identifies the
+subsystem. Examples from merged PRs:
+
+```
+tsdb/wlog: optimize WAL watcher reads
+fix(PromQL): do not skip histogram buckets when trimming
+feat(agent): fix ST append; add compliance RW sender test
+chore: fix emptyStringTest issues from gocritic
+ci: add statuses write permission to prombench workflow
+docs: clarify that `lookback_delta` query parameter takes either a duration or number of seconds
+```
+
+Common area prefixes: `tsdb`, `tsdb/wlog`, `promql`, `discovery/`, `agent`,
+`alerting`, `textparse`, `ui`, `build`, `ci`, `docs`, `chore`.
+
+For performance work, append `[PERF]` to the area segment or use the `perf(area):`
+convention.
+
+---
+
+## Commits
+
+- Each commit must compile and pass tests independently, except when one commit adds a test to expose a bug and then the next commit fixes the bug.
+- Keep commits small and focused. Do not bundle unrelated changes in one commit.
+- Sign off every commit with `git commit -s` to satisfy the DCO requirement.
+- Do not include unrelated local changes in the PR.
+
+---
+
+## Release Notes Block
+
+Every PR must include a `release-notes` fenced code block in the description.
+If there is no user-facing change, write `NONE`:
+
+````
+```release-notes
+NONE
+```
+````
+
+Otherwise use one of these prefixes, matching the CHANGELOG style:
+
+```
+[FEATURE] new capability
+[ENHANCEMENT] improvement to existing behaviour
+[PERF] performance improvement
+[BUGFIX] bug fix
+[SECURITY] security fix
+[CHANGE] breaking or behavioural change
+```
+
+Example:
+````
+```release-notes
+[BUGFIX] PromQL: Do not skip histogram buckets in queries where histogram trimming is used.
+```
+````
+
+---
+
+## Tests
+
+- Bug fixes require a test that reproduces the bug.
+- New behaviour or exported API changes require unit or e2e tests.
+- Tests should attempt to mirror realistic data and/or behaviour.
+- Use only exported APIs in tests where possible — this keeps tests closer to
+ real library usage and simplifies review.
+
+---
+
+## Performance Work
+
+Maintainers take performance seriously. For any PERF PR:
+
+- Performance improvements require a benchmark that demonstrates the improvement.
+- Run benchmarks before and after the change using `go test -count=6 -benchmem -bench `
+- Provide benchmark numbers in the PR body using `benchstat` output.
+- If a subset of benchmark results show a regression, address this or explain why the case is not important.
+- Reuse allocations in hot paths where possible (slices, buffers).
+- When reusing buffers passed to interfaces, document that callers must copy
+ the contents and must not retain references.
+- Link to supporting analysis (Google Doc, issue, etc.) for complex changes.
+
+---
+
+## Code Style
+
+- Follow [Go Code Review Comments](https://go.dev/wiki/CodeReviewComments)
+ and the formatting/style section of
+ [Go: Best Practices for Production Environments](https://peter.bourgon.org/go-in-production/#formatting-and-style).
+- State your assumptions.
+- Interface contracts: when ownership or lifetime semantics (e.g. buffer reuse) are important,
+ document it at the interface definition, not just in the implementation.
+- All exposed objects must have a doc comment.
+- All comments must start with a capital letter and end with a full stop.
+- Run `make lint` before submitting. The project uses `golangci-lint` including
+ `gocritic` rules such as `emptyStringTest` — fix linter findings rather than
+ suppressing them with `//nolint` unless there is a clear false-positive.
+- Use `//nolint:linter1[,linter2,...]` sparingly; prefer fixing the code.
+
+---
+
+## Linking Issues
+
+Use GitHub closing keywords in the PR body so the linked issue closes
+automatically on merge:
+
+```
+Fixes #18243
+```
+
+---
+
+## Scope Discipline
+
+- Do not include unrelated changes in a PR; make a separate PR instead.
+- If a refactor is necessary to make a change, do those in separate commits.
+- If a PR is large, split it into preparatory and follow-up PRs and reference
+ them with "Part of #NNNN" or "Depends on #NNNN".
+
+---
+
+## Documentation Changes
+
+- Docs PRs are welcome for clarifying ambiguous parameter descriptions,
+ fixing Markdown formatting, and keeping the OpenAPI spec consistent with
+ the implementation.
+- When changing documented behaviour, update any relevant text in the docs/ directory.
+ Check whether the OpenAPI spec also needs updating.
+
+---
+
+## CI / Workflow Changes
+
+- Workflow files need the correct GitHub token permissions declared explicitly.
+ Missing permissions (e.g. `statuses: write`) cause silent 403 failures.
+
+---
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 19af3f460fc..98e0e9894f6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,8 +1,671 @@
# Changelog
-## unreleased
-
-## 2.55.1 / 2024-01-04
+## 3.13.1 / 2026-07-10
+
+- [BUGFIX] TSDB: Fix the head-chunk cache returning samples from the wrong chunk, or spurious not-found errors, to range queries after head-chunk truncation. #19134
+
+## 3.13.0 / 2026-07-01
+
+- [SECURITY] UI: Bump `sanitize-html` to fix a cross-site scripting vulnerability (CVE-2026-44990). #18697
+- [CHANGE] UI: Third-party npm dependency licenses are now embedded in the Prometheus binary and served at `/assets/third-party-licenses.txt`, replacing the `npm_licenses.tar.bz2` archive previously shipped in release tarballs and container images. #18997
+- [CHANGE] API: Use SHA-256 instead of SHA-1 to generate rule group pagination tokens. #18927
+- [CHANGE] HTTP clients: Credentials (Authorization header, basic auth, bearer token, OAuth2, configured headers) are no longer forwarded when following a redirect to a different host; affects scraping, remote read/write, alerting, and service discovery. Via prometheus/common v0.69.0 (CVE-2025-4673 CVE-2023-45289). #18949
+- [CHANGE] promtool: Relative file paths in the file passed to `--http.config.file` are now resolved relative to that config file's directory instead of its parent directory. Via prometheus/common v0.69.0. #18949
+- [CHANGE] PromQL: Rename the `min()` and `max()` duration-expression functions (experimental feature flag `experimental-duration-expr`) to `min_of()` and `max_of()` to avoid confusion with the `min` and `max` aggregate operators. #18687
+- [FEATURE] API: Add experimental search endpoints to search metric names, label names, and label values. #18573
+- [FEATURE] Discovery/AWS: Add ability to filter RDS instances. #18859
+- [FEATURE] PromQL: Add `min_of(a, b)` and `max_of(a, b)` scalar experimental functions, returning the smaller or larger of two scalar values. #18687
+- [FEATURE] PromQL: Add support for smoothed/anchored rate with native histograms. #18564
+- [FEATURE] PromQL: Expose per-query `samplesRead` (and `samplesReadPerStep` with `stats=all` and the `promql-per-step-stats` feature flag) in the query stats response, and add the `prometheus_engine_query_samples_read_total` engine counter. `samplesRead` reflects storage I/O distinct from `totalQueryableSamples`, which counts samples loaded into the evaluator (and so over-counts when a sample is reused across multiple range-vector windows). #18081
+- [FEATURE] Scrape: Add `__convert_classic_histograms_to_nhcb__` internal label to allow per-target override of `convert_classic_histograms_to_nhcb` scrape configuration via relabeling. #18840
+- [FEATURE] TSDB: Add `storage.tsdb.chunk_encoding.floats` configuration field to select float chunk encoding (`xor` or `xor2`) at runtime, independently of the `--enable-feature=xor2-encoding` flag. #18769
+- [FEATURE] remote_write: Add Certificate support for ingesting data into an Azure Monitor Workspace. #18217
+- [FEATURE] Scrape: Add `__always_scrape_classic_histograms__` and `__scrape_native_histograms__` internal labels to allow per-target override of the `always_scrape_classic_histograms` and `scrape_native_histograms` scrape configuration via relabeling. #18929
+- [ENHANCEMENT] Release: Container images are now also published to the GitHub Container Registry (ghcr.io). #18791
+- [ENHANCEMENT] PromQL: Prettify `fill_left(x) fill_right(x)` as `fill(x)` when both fill values are equal. #18851
+- [ENHANCEMENT] UI: Improve autocompletion after closing a function bracket. #18894
+- [PERF] Labels: Add case-insensitive prefix matching to speed up evaluation of long case-insensitive regular expressions (up to ~2x faster). #18540
+- [PERF] TSDB: Reduce per-sample overhead in chunk population, speeding up affected queries by ~12-15% in benchmarks. #18699
+- [PERF] TSDB: Eliminate unnecessary heap allocations in the V2 histogram WAL decoder, reducing allocations by up to 50% and memory by up to 10% for deployments using native histograms with created-timestamp storage enabled (`--enable-feature=created-timestamp-zero-ingestion`). #18813
+- [BUGFIX] Discovery/AWS: Fix failure when processing an AWS RDS cluster without instances. #18845
+- [BUGFIX] Fix race condition in initTime that could cause ErrOutOfBounds. #18629
+- [BUGFIX] PromQL: A range query whose `end` was not aligned to `step` caused subqueries inside it to evaluate past the parent's last actual step, inflating `peakSamples` in the query stats and against the `query.max-samples` limit, and wasting storage I/O reading samples that were never used in the result. #18081
+- [BUGFIX] PromQL: A range query containing an at-modifier-unsafe function over a range-vector with an `@` modifier (e.g. `predict_linear(metric[60s] @ T, X)`) silently under-counted `totalQueryableSamples` for steps after step 0. #18081
+- [BUGFIX] PromQL: Fix `fill_left`/`fill_right` producing missing samples in range queries when using `group_left`/`group_right`. #18850
+- [BUGFIX] PromQL: Fix for resets() and changes() in anchored range extenders with histograms. #18906
+- [BUGFIX] PromQL: Fix panic on `1[5m] smoothed` and similar expressions when extended range selectors are enabled. #18764
+- [BUGFIX] PromQL: Fix panic when a `smoothed` instant vector selector produces no samples for a series. #18943
+- [BUGFIX] PromQL: Fix panic when using a parenthesised plain number as an offset (e.g. `foo offset -(5)`). #18768
+- [BUGFIX] promtool: Fix panic when parsing exposition text containing empty braces `{}`. Via prometheus/common v0.69.0. #18949
+- [BUGFIX] Promtool: Fix `check healthy` and `check ready` when `--url` ends with a trailing slash. #18854
+- [BUGFIX] Rules: Close PromQL query after each rule evaluation to ensure resources are released. #18733
+- [BUGFIX] Scaleway SD: Resolve VPC/IPAM-only instances that have no legacy `private_ip` or `public_ip` field, but do have private NICs attached. #18772
+- [BUGFIX] TSDB: Do not leak head series when an integer histogram append is rejected (e.g. out-of-order). #18838
+- [BUGFIX] UI: Escape label values offered by PromQL autocomplete. #18658
+- [BUGFIX] TSDB: Fix chunk snapshot encoding for EncXOR2 chunks, preventing corruption on TSDB restart when EncXOR2-encoded series were present. #18739
+- [BUGFIX] TSDB: Store a millisecond timestamp (not a WAL segment number) in walExpiries when a series is evicted via CompactStaleHead/CompactSelectedSeries, so the series's label record is correctly retained in the next WAL checkpoint and replays cleanly. #18847
+- [BUGFIX] TSDB: Prevent loss of samples at the chunk-range boundary when CompactSelectedSeries (and CompactStaleHead) evict the series — the per-slice compaction loop now runs one more iteration so the boundary timestamp is captured in a block before the in-memory copy is removed. #18849
+
+## 3.12.0 / 2026-05-28
+
+- [SECURITY] Remote-write: Reject snappy-compressed requests whose declared decoded length exceeds the 32MB. Thanks to @hibrian827 for reporting it. #18642
+- [SECURITY] STACKIT SD: Fix secrets being exposed in plaintext via `/-/config` endpoint. Thanks to @August829 and @Phaxma for reporting. GHSA-39j6-789q-qxvh #18649
+- [CHANGE] TSDB/Agent: Adds Start Timestamp field to all WAL Histogram samples in memory; used `st-storage` flag is enabled. #18221
+- [FEATURE] API: Add `/api/v1/status/self_metrics` endpoint returning the current state of the Prometheus server's own metrics about itself as JSON. #18411
+- [FEATURE] Discovery: Add DigitalOcean Managed Databases service discovery #18287
+- [FEATURE] Prometheus: Add support for the aix/ppc64 compilation target #18321
+- [FEATURE] Discovery: Add Outscale VM service discovery (`outscale_sd_configs`) for discovering scrape targets from the Outscale Cloud API. #18139
+- [FEATURE] PromQL: Emit a warning when `sort`, `sort_by_label` or `sort_by_label_desc` is used within range (matrix) queries, as these functions do not have effect in that context. #18498
+- [FEATURE] PromQL: Add `start()`, `end()`, `range()`, and `step()` experimental functions #17877
+- [FEATURE] PromQL: Update `resets()` function to consider start timestamp resets. Hidden behind `use-start-timestamps` feature flag. #18627
+- [FEATURE] Prometheus: Promote auto-reload-config as stable #18620
+- [FEATURE] TSDB/Agent: Add `CheckpointFromInMemorySeries` option to `agent.DB` that enables checkpoint based on in-memory series. #17948
+- [FEATURE] UI: Add a web interface for deleting time series and cleaning tombstones, accessible from the Status menu. #18390
+- [FEATURE] PromQL: Use start timestamps for `rate()`, `irate(), and `increase()` calculations, behind a feature flag `use-start-timestamps`. Doesn't work together with extended range selectors `anchored` and `smoothed`. #18344
+- [FEATURE] Scrape: Added a feature flag `st-synthesis` which synthesizes unknown STs for scraped cumulative metrics. Useful when Remote Writing 2.0 with delta or Otel-based backends. #18279
+- [FEATURE] promqltest: support `@st` annotation in `load` blocks to specify per-sample start timestamps. #18360
+- [ENHANCEMENT] API: reject concurrent fgprof profiles. #18651
+- [ENHANCEMENT] AWS SD: Add optional `external_id` field to ECS/MSK/RDS/Elasticache. #18579
+- [ENHANCEMENT] AWS SD: Add optional `external_id` field. #17171
+- [ENHANCEMENT] Discovery: Propagate SD target updates faster by introducing dynamic backoff interval instead of static 5s interval for throttling. #18187
+- [ENHANCEMENT] Promtool: Add `--header` flag to `query instant` command, matching existing `query range` behaviour. #18418
+- [ENHANCEMENT]: AWS SD: Allows EC2 service discovery to discover IPv6 addresses to communicate with target endpoints. The private IPv4 address remains the default when both IPv4 and IPv6 addresses are present. #16088
+- [PERF] TSDB: Make head chunk lookup in range queries constant time instead of quadratic time #18302
+- [PERF] TSDB: Skip entire stripes in mmapHeadChunks when no series need mmapping, reducing CPU utilization significantly at production-relevant scales. #18541
+- [PERF] TSDB: Skip clean series during periodic head chunk mmap using cached head chunk count #18272
+- [PERF] PromQL: Address FloatHistogram.KahanAdd performance regression on Go 1.26. #18568
+- [BUGFIX] PromQL: Fix `info()` function incorrectly handling negated `__name__` matchers #17932
+- [BUGFIX] API: Return duration expressions in `/parse_ast`. #18624
+- [BUGFIX] API: correctly document formats accepted for duration query request parameters (step, timeout and lookback delta) in OpenAPI spec #18305
+- [BUGFIX] Scrape: AppenderV2 now tracks staleness even when OOO/duplicate series errors happen similar to AppenderV1 #18567
+- [BUGFIX] Config: Validate remote_write queue_config fields at load time to prevent runtime panic and silent misconfiguration. #18209
+- [BUGFIX] Discovery/Consul: Add `health_filter` for Health API filtering, fixing breakage when using Catalog-only fields like `ServiceTags` in `filter`. #18479 #18499
+- [BUGFIX] OTLP: limit decompressed body size for gzip-encoded OTLP write requests. #18408
+- [BUGFIX] PromQL: Fix `smoothed` rate/increase returning zero instead of no result when all data falls strictly after the query range. #18523
+- [BUGFIX] PromQL: Fix metric name not being dropped when last_over_time or first_over_time is applied to subqueries containing name-dropping functions like abs(). #18409
+- [BUGFIX] PromQL: Fix missing warning when mixing exponential and custom-bucket histograms in stats queries. #18660
+- [BUGFIX] PromQL: Fix parsing of `range()` keyword in duration expressions such as `foo[5m+range()]`. #18623
+- [BUGFIX] PromQL: Fix smoothed vector selector returning no results in binary operations when the `@` modifier is used. #18531
+- [BUGFIX] PromQL: Reject NaN, infinite, and out-of-range duration expressions instead of silently producing an out-of-range time.Duration. #18639
+- [BUGFIX] Scrape: Fix panic when scraping malformed native histograms. #18414
+- [BUGFIX] Scrape: fix panic when scraping a target exposing a summary with no quantiles via the protobuf format. #18382
+- [BUGFIX] Scrape: fix scrape failure log file occasionally not applied after a configuration reload. #18421
+- [BUGFIX] TSDB: Allow retention percentage with new data path. #18628
+- [BUGFIX] TSDB: Preserve decimal precision in percentage-based retention #18374
+- [BUGFIX] TSDB: fix prometheus_tsdb_head_chunks going negative after WAL replay #18401
+- [BUGFIX] TSDB: panic with native histograms during query of overlapping chunks. #18692
+- [BUGFIX] Tracing: fix startup failure for insecure OTLP HTTP tracing #18469
+- [BUGFIX] UI: Escape label values offered by PromQL autocomplete. #18658
+- [BUGFIX] UI: Improve Y-axis tick label precision for graph values over small ranges. #18682
+- [BUGFIX] `prometheus_sd_refresh*` and `prometheus_sd_discovered_targets` metrics for specific scrape jobs are deleted when the scrape job is removed. #17614
+- [BUGFIX] Remote: fixed validation for received RW2 requests when parsing metadata unit symbols. This fixes a case when request would cause (recovered) handler panic. #18641
+- [BUGFIX] TSDB/Agent: fix race in agent appender where concurrent appends for the same label set could produce duplicate in-memory series and duplicate WAL records. #18292
+- [BUGFIX] Config: Update `--enable-feature` flag description and sort feature names. #18487
+
+## 3.11.3 / 2026-04-27
+
+This release fixes mutiple security issues.
+
+We would like to thank the following people for the responsible disclosures:
+- Shadowbyte (4c1dr3aper) - Charlie Lewis for the Remote-Read snappy decode vulnerability.
+- Brett Gervasoni for the AzureAD OAuth `client_secret` vulnerability.
+- @iiihaiii and @Ngocnn97 for the Old UI XSS vulnerability.
+
+- [SECURITY] AzureAD remote write: Fix OAuth `client_secret` being exposed in plaintext via `/-/config` endpoint. GHSA-wg65-39gg-5wfj / CVE-2026-42151 #18590
+- [SECURITY] Remote-read: Reject snappy-compressed requests whose declared decoded length exceeds the decode limit. GHSA-8rm2-7qqf-34qm / CVE-2026-42154 #18584
+- [SECURITY] UI: Fix stored XSS via unescaped `le` label values in old UI heatmap chart tick labels. GHSA-fw8g-cg8f-9j28 #18588
+
+## 3.11.2 / 2026-04-13
+
+This release has a fix for a Stored XSS vulnerability that can be triggered via crafted metric names and label values in Prometheus web UI tooltips and metrics explorer. Thanks to Duc Anh Nguyen from TinyxLab for reporting it.
+
+- [SECURITY] UI: Fix stored XSS via unescaped metric names and labels. CVE-2026-40179. #18506
+- [ENHANCEMENT] Consul SD: Introduce `health_filter` field for Health API filtering. #18499
+- [BUGFIX] Consul SD: Fix filter parameter being incorrectly applied to the Health API. #18499
+
+## 3.11.1 / 2026-04-07
+
+- [BUGFIX] Tracing: Fix startup failure for OTLP HTTP tracing with `insecure: true`. #18469
+
+## 3.11.0 / 2026-04-02
+
+- [CHANGE] Hetzner SD: The `__meta_hetzner_datacenter` label is deprecated for the role `robot` but kept for backward compatibility, use the `__meta_hetzner_robot_datacenter` label instead. For the role `hcloud`, the label is deprecated and will stop working after the 1 July 2026. #17850
+- [CHANGE] Hetzner SD: The `__meta_hetzner_hcloud_datacenter_location` and `__meta_hetzner_hcloud_datacenter_location_network_zone` labels are deprecated, use the `__meta_hetzner_hcloud_location` and `__meta_hetzner_hcloud_location_network_zone` labels instead. #17850
+- [CHANGE] Promtool: Redirect debug output to stderr to avoid interfering with stdout-based tool output. #18346
+- [FEATURE] AWS SD: Add Elasticache Role. #18099
+- [FEATURE] AWS SD: Add RDS Role. #18206
+- [FEATURE] Azure SD: Add support for Azure Workload Identity authentication method. #17207
+- [FEATURE] Discovery: Introduce `prometheus_sd_last_update_timestamp_seconds` metric to track the last time a service discovery update was sent to consumers. #18194
+- [FEATURE] Kubernetes SD: Add support for node role selectors for pod roles. #18006
+- [FEATURE] Kubernetes SD: Introduce pod-based labels for deployment, cronjob, and job controller names: `__meta_kubernetes_pod_deployment_name`, `__meta_kubernetes_pod_cronjob_name` and `__meta_kubernetes_pod_job_name`, respectively. #17774
+- [FEATURE] PromQL: Add `` and `>/` operators for trimming observations from native histograms. #17904
+- [FEATURE] PromQL: Add experimental `histogram_quantiles` variadic function for computing multiple quantiles at once. #17285
+- [FEATURE] TSDB: Add `storage.tsdb.retention.percentage` configuration to configure the maximum percent of disk usable for TSDB storage. #18080
+- [FEATURE] TSDB: Add an experimental `fast-startup` feature flag that writes a `series_state.json` file to the WAL directory to track active series state across restarts. #18303
+- [FEATURE] TSDB: Add an experimental `st-storage` feature flag. When enabled, Prometheus stores ingested start timestamps (ST, previously called Created Timestamp) from scrape or OTLP in the TSDB and Agent WAL, and exposes them via Remote Write 2. #18062
+- [FEATURE] TSDB: Add an experimental `xor2-encoding` feature flag for the new TSDB block float sample chunk encoding that is optimized for scraped data and allows encoding start timestamps. #18062
+- [ENHANCEMENT] HTTP client: Add AWS `external_id` support for sigv4. #17916
+- [ENHANCEMENT] Kubernetes SD: Deduplicate deprecation warning logs from the Kubernetes API to reduce noise. #17829
+- [ENHANCEMENT] TSDB: Remove old temporary checkpoints when creating a Checkpoint. #17598
+- [ENHANCEMENT] UI: Add autocomplete support for experimental `first_over_time` and `ts_of_first_over_time` PromQL functions. #18318
+- [ENHANCEMENT] Vultr SD: Upgrade govultr library from v2 to v3 for continued security patches and maintenance. #18347
+- [PERF] PromQL: Improve performance and reduce heap allocations in joins (VectorBinop)/And/Or/Unless. #17159
+- [PERF] PromQL: Partially address performance regression in native histogram aggregations due to using `KahanAdd`. #18252
+- [PERF] Remote write: Optimize WAL watching used for RW sending to reuse internal buffers. #18250
+- [PERF] TSDB: Optimize LabelValues intersection performance for matchers. #18069
+- [PERF] UI: Skip restacking on hover in stacked series charts. #18230
+- [BUGFIX] AWS SD: Fix EC2 SD ignoring the configured `endpoint` option, a regression from the AWS SDK v2 migration. #18133
+- [BUGFIX] AWS SD: Fix panic in EC2 SD when DescribeAvailabilityZones returns nil ZoneName or ZoneId. #18133
+- [BUGFIX] Agent: Fix memory leak caused by duplicate SeriesRefs being loaded as active series. #17538
+- [BUGFIX] Alerting: Fix alert state incorrectly resetting to pending when the FOR period is increased in the config file. #18244
+- [BUGFIX] Azure SD: Fix system-assigned managed identity not working when `client_id` is empty. #18323
+- [BUGFIX] Consul SD: Fix filter parameter not being applied to health service endpoint, causing Node and Node.Meta filters to be ignored. #17349
+- [BUGFIX] Kubernetes SD: Fix duplicate targets generated by `*DualStack` EndpointSlices policies. #18192
+- [BUGFIX] OTLP: Fix ErrTooOldSample being returned as HTTP 500 instead of 400 in PRW v2 histogram write paths, preventing infinite client retry loops. #18084
+- [BUGFIX] OTLP: Fix exemplars getting mixed between incorrect parts of a histogram. #18056
+- [BUGFIX] PromQL: Do not skip histogram buckets in queries where histogram trimming is used. #18263
+- [BUGFIX] Remote write: Fix `prometheus_remote_storage_sent_batch_duration_seconds` measuring before the request was sent. #18214
+- [BUGFIX] Rules: Fix alert state restoration when rule labels contain Go template expressions. #18375
+- [BUGFIX] Scrape: Fix panic when parsing bare label names without an equal sign in brace-only metric notation. #18229
+- [BUGFIX] TSDB: Fail early when `use-uncached-io` feature flag is set on unsupported environments. #18219
+- [BUGFIX] TSDB: Fall back to CLI flag values when retention is removed from config file. #18200
+- [BUGFIX] TSDB: Fix memory leaks in buffer pools by clearing reference fields before returning buffers to pools. #17895
+- [BUGFIX] TSDB: Fix missing mmap of histogram chunks during WAL replay. #18306
+- [BUGFIX] TSDB: Fix storage.tsdb.retention.time unit mismatch in file causing retention to be 1e6 times longer than configured. #18200
+- [BUGFIX] Tracing: Fix missing traceID in query log when tracing is enabled, previously only spanID was emitted. #18189
+- [BUGFIX] UI: Fix tooltip Y-offset drift when using multiple graph panels. #18228
+- [BUGFIX] UI: Update retention display in runtime info when config is reloaded. #18200
+
+## 3.10.0 / 2026-02-24
+
+Prometheus now offers a distroless Docker image variant alongside the default
+busybox image. The distroless variant provides enhanced security with a minimal
+base image, uses UID/GID 65532 (nonroot) instead of nobody, and removes the
+VOLUME declaration. Both variants are available with `-busybox` and `-distroless`
+tag suffixes (e.g., `prom/prometheus:latest-busybox`, `prom/prometheus:latest-distroless`).
+The busybox image remains the default with no suffix for backwards compatibility
+(e.g., `prom/prometheus:latest` points to the busybox variant).
+
+For users migrating existing **named** volumes from the busybox image to the distroless variant, the ownership can be adjusted with:
+```
+docker run --rm -v prometheus-data:/prometheus alpine chown -R 65532:65532 /prometheus
+```
+Then, the container can be started with the old volume with:
+```
+docker run -v prometheus-data:/prometheus prom/prometheus:latest-distroless
+```
+User migrating from bind mounts might need to ajust permissions too, depending on their setup.
+
+- [CHANGE] Alerting: Add `alertmanager` dimension to following metrics: `prometheus_notifications_dropped_total`, `prometheus_notifications_queue_capacity`, `prometheus_notifications_queue_length`. #16355
+- [CHANGE] UI: Hide expanded alert annotations by default, enabling more information density on the `/alerts` page. #17611
+- [FEATURE] AWS SD: Add MSK Role. #17600
+- [FEATURE] PromQL: Add `fill()` / `fill_left()` / `fill_right()` binop modifiers for specifying default values for missing series. #17644
+- [FEATURE] Web: Add OpenAPI 3.2 specification for the HTTP API at `/api/v1/openapi.yaml`. #17825
+- [FEATURE] Dockerfile: Add distroless image variant using UID/GID 65532 and no VOLUME declaration. Busybox image remains default. #17876
+- [FEATURE] Web: Add on-demand wall time profiling under `/debug/pprof/fgprof`. #18027
+- [ENHANCEMENT] PromQL: Add more detail to histogram quantile monotonicity info annotations. #15578
+- [ENHANCEMENT] Alerting: Independent alertmanager sendloops. #16355
+- [ENHANCEMENT] TSDB: Experimental support for early compaction of stale series in the memory with configurable threshold `stale_series_compaction_threshold` in the config file. #16929
+- [ENHANCEMENT] Service Discovery: Service discoveries are now removable from the Prometheus binary through the Go build tag `remove_all_sd` and individual service discoveries can be re-added with the build tags `enable__sd`. Users can build a custom Prometheus with only the necessary SDs for a smaller binary size. #17736
+- [ENHANCEMENT] Promtool: Support promql syntax features `promql-duration-expr` and `promql-extended-range-selectors`. #17926
+- [PERF] PromQL: Avoid unnecessary label extraction in PromQL functions. #17676
+- [PERF] PromQL: Improve performance of regex matchers like `.*-.*-.*`. #17707
+- [PERF] OTLP: Add label caching for OTLP-to-Prometheus conversion to reduce allocations and improve latency. #17860
+- [PERF] API: Compute `/api/v1/targets/relabel_steps` in a single pass instead of re-running relabeling for each prefix. #17969
+- [PERF] tsdb: Optimize LabelValues intersection performance for matchers. #18069
+- [BUGFIX] PromQL: Prevent query strings containing only UTF-8 continuation bytes from crashing Prometheus. #17735
+- [BUGFIX] Web: Fix missing `X-Prometheus-Stopping` header for `/-/ready` endpoint in `NotReady` state. #17795
+- [BUGFIX] PromQL: Fix PromQL `info()` function returning empty results when filtering by a label that exists on both the input metric and `target_info`. #17817
+- [BUGFIX] TSDB: Fix a bug during exemplar buffer grow/shrink that could cause exemplars to be incorrectly discarded. #17863
+- [BUGFIX] UI: Fix broken graph display after page reload, due to broken Y axis min encoding/decoding. #17869
+- [BUGFIX] TSDB: Fix memory leaks in buffer pools by clearing reference fields (Labels, Histogram pointers, metadata strings) before returning buffers to pools. #17879
+- [BUGFIX] PromQL: info function: fix series without identifying labels not being returned. #17898
+- [BUGFIX] OTLP: Filter `__name__` from OTLP attributes to prevent duplicate labels. #17917
+- [BUGFIX] TSDB: Fix division by zero when computing stale series ratio with empty head. #17952
+- [BUGFIX] OTLP: Fix potential silent data loss for sum metrics. #17954
+- [BUGFIX] PromQL: Fix smoothed interpolation across counter resets. #17988
+- [BUGFIX] PromQL: Fix panic with `@` modifier on empty ranges. #18020
+- [BUGFIX] PromQL: Fix `avg_over_time` for a single native histogram. #18058
+
+## 3.9.1 / 2026-01-07
+
+ - [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
+
+* [SECURITY] Remote-Write: Reject snappy-compressed requests whose declared decoded length exceeds the decode limit. #17683
+* [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
+
+* [BUGFIX] UI: Revert changed (and breaking) redirect behavior for `-web.external-url` if `-web.route-prefix` is configured, which was introduced in #17240. #17389
+* [BUGFIX] Fix federation of some native histograms. #17299 #17409
+* [BUGFIX] promtool: `check config` would fail when `--lint=none` flag was set. #17399 #17414
+* [BUGFIX] Remote-write: fix a deadlock in the queue resharding logic that can lead to suboptimal queue behavior. #17412
+
+## 3.7.2 / 2025-10-22
+
+* [BUGFIX] AWS SD: Fix AWS SDK v2 credentials handling for EC2 and Lightsail discovery. #17355
+* [BUGFIX] AWS SD: Load AWS region from IMDS when not set. #17376
+* [BUGFIX] Relabeling: Fix `labelmap` action validation with the legacy metric name validation scheme. #17372
+* [BUGFIX] PromQL: Fix parsing failure when `anchored` and `smoothed` are used as metric names and label names. #17353
+* [BUGFIX] PromQL: Fix formatting of range vector selectors with `smoothed`/`anchored` modifier. #17354
+
+## 3.7.1 / 2025-10-16
+
+* [BUGFIX] OTLP: Prefix `key_` to label name when translating an OTel attribute name starting with a single underscore, and keep multiple consecutive underscores in label name when translating an OTel attribute name. This reverts the breaking changes introduced in 3.7.0. #17344
+
+## 3.7.0 / 2025-10-15
+
+* [CHANGE] Remote-write: the following metrics are deprecated:
+ - `prometheus_remote_storage_samples_in_total`, use `prometheus_wal_watcher_records_read_total{type="samples"}` and `prometheus_remote_storage_samples_dropped_total` instead,
+ - `prometheus_remote_storage_histograms_in_total`, use `prometheus_wal_watcher_records_read_total{type=~".*histogram_samples"}` and `prometheus_remote_storage_histograms_dropped_total` instead,
+ - `prometheus_remote_storage_exemplars_in_total`, use `prometheus_wal_watcher_records_read_total{type="exemplars"}` and `prometheus_remote_storage_exemplars_dropped_total` instead,
+ - `prometheus_remote_storage_highest_timestamp_in_seconds`, use the more accurate `prometheus_remote_storage_queue_highest_timestamp_seconds` instead in dashboards and alerts to properly account for relabeling and for more accuracy. #17065
+* [FEATURE] PromQL: Add support for experimental anchored and smoothed rate behind feature flag `promql-extended-range-selectors`. #16457
+* [FEATURE] Federation: Add support for native histograms with custom buckets (NHCB). #17215
+* [FEATURE] PromQL: Add `first_over_time(...)` and `ts_of_first_over_time(...)` behind feature flag `experimental-promql-functions`. #16963 #17021
+* [FEATURE] Remote-write: Add support for Azure Workload Identity as an authentication method for the receiver. #16788
+* [FEATURE] Remote-write: Add type and unit labels to outgoing time series in remote-write 2.0 when the `type-and-unit-labels` feature flag is enabled. #17033
+* [FEATURE] OTLP: Write start time of metrics as created time zero samples into TSDB when `created-timestamp-zero-ingestion` feature flag is enabled. #16951
+* [ENHANCEMENT] PromQL: Add warn-level annotations for counter reset conflicts in certain histogram operations. #17051 #17094
+* [ENHANCEMENT] UI: Add scrape interval and scrape timeout to targets page. #17158
+* [ENHANCEMENT] TSDB: Reduce the resolution of native histograms read from chunks or remote read if the schema is exponential. #17213
+* [ENHANCEMENT] Remote write: Add logging for unexpected metadata in sample batches, when metadata entries are found in samples-only batches. #17034 #17082
+* [ENHANCEMENT] Rules: Support concurrent evaluation for rules querying `ALERTS` and `ALERTS_FOR_STATE`. #17064
+* [ENHANCEMENT] TSDB: Add logs to improve visibility into internal operations. #17074
+* [PERF] OTLP: Write directly to TSDB instead of passing through a Remote-Write adapter when receiving OTLP metrics. #16951
+* [PERF] OTLP: Reduce number of logs emitted from OTLP endpoint. No need to log duplicate sample errors. #17201
+* [PERF] PromQL: Move more work to preprocessing step. #16896
+* [PERF] PromQL: Reduce allocations when walking the syntax tree. #16593
+* [PERF] TSDB: Optimize appender creation, slightly speeding up startup. #16922
+* [PERF] TSDB: Improve speed of querying a series with multiple matchers. #13971
+* [BUGFIX] Alerting: Mutating alerts relabeling (using `replace` actions, etc.) within a `alertmanager_config.alert_relabel_configs` block is now scoped correctly and no longer yields altered alerts to subsequent blocks. #17063
+* [BUGFIX] Config: Infer valid escaping scheme when scrape config validation scheme is set. #16923
+* [BUGFIX] TSDB: Correctly handle appending mixed-typed samples to the same series. #17071 #17241 #17290 #17295 #17296
+* [BUGFIX] Remote-write: Prevent sending unsupported native histograms with custom buckets (NHCB) over Remote-write 1.0, log warning. #17146
+* [BUGFIX] TSDB: Fix metadata entries handling on `metadata-wal-records` experimental feature for native histograms with custom buckets (NHCB) in protobuf scraping. #17156
+* [BUGFIX] TSDB: Ignore Native Histograms with invalid schemas during WAL/WBL replay. #17214
+* [BUGFIX] PromQL: Avoid empty metric names in annotations for `histogram_quantile()`. #16794
+* [BUGFIX] PromQL: Correct inaccurate character positions in errors for some aggregate expressions. #16996 #17031
+* [BUGFIX] PromQL: Fix `info()` function on churning series. #17135
+* [BUGFIX] PromQL: Set native histogram to gauge type when subtracting or multiplying/dividing with negative factors. #17004
+* [BUGFIX] TSDB: Reject unsupported native histogram schemas when attempting to append to TSDB. For scrape and remote-write implement reducing the resolution to fit the maximum if the schema is within the -9 to 52. #17189
+* [BUGFIX] Remote-write: Fix HTTP handler to return after writing error response for invalid compression. #17050
+* [BUGFIX] Remote-write: Return HTTP error `400` instead of `5xx` for wrongly formatted Native Histograms. #17210
+* [BUGFIX] Scrape: Prevent staleness markers from generating unnecessary series. #16429
+* [BUGFIX] TSDB: Avoid misleading `Failed to calculate size of \"wal\" dir` error logs during WAL clean-up. #17006
+* [BUGFIX] TSDB: Prevent erroneously dropping series records during WAL checkpoints. #17029
+* [BUGFIX] UI: Fix redirect to path of `-web.external-url` if `-web.route-prefix` is configured. #17240
+* [BUGFIX] Remote-write: Do not panic on invalid symbol table in remote-write 2.0. #17160
+
+## 3.6.0 / 2025-09-17
+
+* [FEATURE] PromQL: Add `step()`, and `min()` and `max()` on durations, behind feature flag `promql-duration-expr`. #16777
+* [FEATURE] API: Add a `/v1/status/tsdb/blocks` endpoint exposing metadata about loaded blocks. #16695
+* [FEATURE] Templates: Add `toDuration()` and `now()` functions. #16619
+* [ENHANCEMENT] Discovery: Add support for attaching namespace metadata to targets. #16831
+* [ENHANCEMENT] OTLP: Support new `UnderscoreEscapingWithoutSuffixes` strategy via `otlp.translation_strategy`. #16849
+* [ENHANCEMENT] OTLP: Support including scope metadata as metric labels via `otlp.promote_scope_metadata`. #16878
+* [ENHANCEMENT] OTLP: Add `__type__` and `__unit__` labels when feature flag `type-and-unit-labels` is enabled. #16630
+* [ENHANCEMENT] Tracing: Send the traceparent HTTP header during scrapes. #16425
+* [ENHANCEMENT] UI: Add option to disable info and warning query messages under `Query page settings`. #16901
+* [ENHANCEMENT] UI: Improve metadata handling for `_count/_sum/_bucket` suffixes. #16910
+* [ENHANCEMENT] TSDB: Track stale series in the Head block via the `prometheus_tsdb_head_stale_series` metric. #16925
+* [PERF] PromQL: Improve performance due to internal optimizations. #16797
+* [BUGFIX] Config: Fix "unknown global name escaping method" error messages produced during config validation. #16801
+* [BUGFIX] Discovery: Fix race condition during shutdown. #16820
+* [BUGFIX] OTLP: Generate `target_info` samples between the earliest and latest samples per resource. #16737
+* [BUGFIX] PromQL: Fail when `NaN` is passed as parameter to `topk()`, `bottomk()`, `limitk()` and `limit_ratio()`. #16725
+* [BUGFIX] PromQL: Fix extrapolation for native counter histograms. #16828
+* [BUGFIX] PromQL: Reduce numerical errors by disabling some optimizations. #16895
+* [BUGFIX] PromQL: Fix inconsistencies when using native histograms in subqueries. #16879
+* [BUGFIX] PromQL: Fix inconsistent annotations for `rate()` and `increase()` on histograms when feature flag `type-and-unit-labels` is enabled. #16915
+* [BUGFIX] Scraping: Fix memory corruption in `slicelabels` builds. #16946
+* [BUGFIX] TSDB: Fix panic on append when feature flag `created-timestamp-zero-ingestion` is enabled. #16332
+* [BUGFIX] TSDB: Fix panic on append for native histograms with empty buckets. #16893
+
+## 3.5.0 / 2025-07-14
+
+* [FEATURE] PromQL: Add experimental type and unit metadata labels, behind feature flag `type-and-unit-labels`. #16228 #16632 #16718 #16743
+* [FEATURE] PromQL: Add `ts_of_(min|max|last)_over_time`, behind feature flag `experimental-promql-functions`. #16722 #16733
+* [FEATURE] Scraping: Add global option `always_scrape_classic_histograms` to scrape a classic histogram even if it is also exposed as native. #16452
+* [FEATURE] OTLP: New config options `promote_all_resource_attributes` and `ignore_resource_attributes`. #16426
+* [FEATURE] Discovery: New service discovery for STACKIT Cloud. #16401
+* [ENHANCEMENT] Hetzner SD: Add `label_selector` to filter servers. #16512
+* [ENHANCEMENT] PromQL: support non-constant parameter in aggregations like `quantile` and `topk`. #16404
+* [ENHANCEMENT] UI: Better total target count display when using `keep_dropped_targets` option. #16604
+* [ENHANCEMENT] UI: Add simple filtering on the `/rules` page. #16605
+* [ENHANCEMENT] UI: Display query stats in hover tooltip over table query tab. #16723
+* [ENHANCEMENT] UI: Clear search field on `/targets` page. #16567
+* [ENHANCEMENT] Rules: Check that rules parse without error earlier at startup. #16601
+* [ENHANCEMENT] Promtool: Optional fuzzy float64 comparison in rules unittests. #16395
+* [PERF] PromQL: Reuse `histogramStatsIterator` where possible. #16686
+* [PERF] PromQL: Reuse storage for custom bucket values for native histograms. #16565
+* [PERF] UI: Optimize memoization and search debouncing on `/targets` page. #16589
+* [PERF] UI: Fix full-page re-rendering when opening status nav menu. #16590
+* [PERF] Kubernetes SD: use service cache.Indexer to achieve better performance. #16365
+* [PERF] TSDB: Optionally use Direct IO for chunks writing. #15365
+* [PERF] TSDB: When fetching label values, stop work earlier if the limit is reached. #16158
+* [PERF] Labels: Simpler/faster stringlabels encoding. #16069
+* [PERF] Scraping: Reload scrape pools concurrently. #16595 #16783
+* [BUGFIX] Top-level: Update GOGC before loading TSDB. #16491
+* [BUGFIX] Config: Respect GOGC environment variable if no "runtime" block exists. #16558
+* [BUGFIX] PromQL: Fix native histogram `last_over_time`. #16744
+* [BUGFIX] PromQL: Fix reported parser position range in errors for aggregations wrapped in ParenExpr #16041 #16754
+* [BUGFIX] PromQL: Don't emit a value from `histogram_fraction` or `histogram_quantile` if classic and native histograms are present at the same timestamp. #16552
+* [BUGFIX] PromQL: Incorrect rounding of `[1001ms]` to `[1s]` and similar. #16478
+* [BUGFIX] PromQL: Fix inconsistent / sometimes negative `histogram_count` and `histogram_sum`. #16682
+* [BUGFIX] PromQL: Improve handling of NaNs in native histograms. #16724
+* [BUGFIX] PromQL: Fix unary operator precedence in duration expressions. #16713
+* [BUGFIX] PromQL: Improve consistency of `avg` aggregation and `avg_over_time`. #16569 #16773
+* [BUGFIX] UI: Add query warnings and info to graph view. #16753 #16759
+* [BUGFIX] API: Add HTTP `Vary: Origin` header to responses to avoid cache poisoning. #16008
+* [BUGFIX] Discovery: Avoid deadlocks by taking locks in consistent order. #16587
+* [BUGFIX] Remote-write: For Azure AD auth, allow empty `client_id` to suppport system assigned managed identity. #16421
+* [BUGFIX] Scraping: Fix rare memory corruption bug. #16623
+* [BUGFIX] Scraping: continue handling custom-bucket histograms after an exponential histogram is encountered. #16720
+* [BUGFIX] OTLP: Default config not respected when `otlp:` block is unset. #16693
+
+## 3.4.2 / 2025-06-26
+
+* [BUGFIX] OTLP receiver: Fix default configuration not being respected if the `otlp:` block is unset in the config file. #16693
+
+## 3.4.1 / 2025-05-31
+
+* [BUGFIX] Parser: Add reproducer for a dangling-reference issue in parsers. #16633
+
+## 3.4.0 / 2025-05-17
+
+* [CHANGE] Config: Make setting out-of-order native histograms feature (`--enable-feature=ooo-native-histograms`) a no-op. Out-of-order native histograms are now always enabled when `out_of_order_time_window` is greater than zero and `--enable-feature=native-histograms` is set. #16207
+* [FEATURE] OTLP translate: Add feature flag for optionally translating OTel explicit bucket histograms into native histograms with custom buckets. #15850
+* [FEATURE] OTLP translate: Add option to receive OTLP metrics without translating names or attributes. #16441
+* [FEATURE] PromQL: allow arithmetic operations in durations in PromQL parser. #16249
+* [FEATURE] OTLP receiver: Add primitive support for ingesting OTLP delta metrics as-is. #16360
+* [ENHANCEMENT] PromQL: histogram_fraction for bucket histograms. #16095
+* [ENHANCEMENT] TSDB: add `prometheus_tsdb_wal_replay_unknown_refs_total` and `prometheus_tsdb_wbl_replay_unknown_refs_total` metrics to track unknown series references during WAL/WBL replay. #16166
+* [ENHANCEMENT] Scraping: Add config option for escaping scheme request. #16066
+* [ENHANCEMENT] Config: Add global config option for convert_classic_histograms_to_nhcb. #16226
+* [ENHANCEMENT] Alerting: make batch size configurable (`--alertmanager.notification-batch-size`). #16254
+* [PERF] Kubernetes SD: make endpointSlice discovery more efficient. #16433
+* [BUGFIX] Config: Fix auto-reload on changes to rule and scrape config files. #16340
+* [BUGFIX] Scraping: Skip native histogram series if ingestion is disabled. #16218
+* [BUGFIX] TSDB: Handle metadata/tombstones/exemplars for duplicate series during WAL replay. #16231
+* [BUGFIX] TSDB: Avoid processing exemplars outside the valid time range during WAL replay. #16242
+* [BUGFIX] Promtool: Add feature flags for PromQL features. #16443
+* [BUGFIX] Rules: correct logging of alert name & template data. #15093
+* [BUGFIX] PromQL: Use arithmetic mean for `histogram_stddev()` and `histogram_stdvar()` . #16444
+
+## 3.3.0 / 2025-04-15
+
+* [FEATURE] PromQL: Implement `idelta()` and `irate()` for native histograms. #15853
+* [ENHANCEMENT] Scaleway SD: Add `__meta_scaleway_instance_public_ipv4_addresses` and `__meta_scaleway_instance_public_ipv6_addresses` labels. #14228
+* [ENHANCEMENT] TSDB: Reduce locking while reloading blocks. #12920
+* [ENHANCEMENT] PromQL: Allow UTF-8 labels in `label_replace()`. #15974
+* [ENHANCEMENT] Promtool: `tsdb create-blocks-from openmetrics` can now read from a Pipe. #16011
+* [ENHANCEMENT] Rules: Add support for anchors and aliases in rule files. #14957
+* [ENHANCEMENT] Dockerfile: Make `/prometheus` writable. #16073
+* [ENHANCEMENT] API: Include scrape pool name for dropped targets in `/api/v1/targets`. #16085
+* [ENHANCEMENT] UI: Improve time formatting and copying of selectors. #15999 #16165
+* [ENHANCEMENT] UI: Bring back vertical grid lines and graph legend series toggling instructions. #16163 #16164
+* [ENHANCEMENT] Mixin: The `cluster` label can be customized using `clusterLabel`. #15826
+* [PERF] TSDB: Optimize some operations on head chunks by taking shortcuts. #12659
+* [PERF] TSDB & Agent: Reduce memory footprint during WL replay. #15778
+* [PERF] Remote-Write: Reduce memory footprint during WAL replay. #16197
+* [PERF] API: Reduce memory footprint during header parsing. #16001
+* [PERF] Rules: Improve dependency evaluation, enabling better concurrency. #16039
+* [PERF] Scraping: Improve scraping performance for native histograms. #15731
+* [PERF] Scraping: Improve parsing of created timestamps. #16072
+* [BUGFIX] Scraping: Bump cache iteration after error to avoid false duplicate detections. #16174
+* [BUGFIX] Scraping: Skip native histograms series when ingestion is disabled. #16218
+* [BUGFIX] PromQL: Fix counter reset detection for native histograms. #15902 #15987
+* [BUGFIX] PromQL: Fix inconsistent behavior with an empty range. #15970
+* [BUGFIX] PromQL: Fix inconsistent annotation in `quantile_over_time()`. #16018
+* [BUGFIX] PromQL: Prevent `label_join()` from producing duplicates. #15975
+* [BUGFIX] PromQL: Ignore native histograms in `scalar()`, `sort()` and `sort_desc()`. #15964
+* [BUGFIX] PromQL: Fix annotations for binary operations between incompatible native histograms. #15895
+* [BUGFIX] Alerting: Consider alert relabeling when deciding whether alerts are dropped. #15979
+* [BUGFIX] Config: Set `GoGC` to the default value in case of an empty configuration. #16052
+* [BUGFIX] TSDB: Fix unknown series errors and potential data loss during WAL replay when inactive series are removed from the head and reappear before the next WAL checkpoint. #16060
+* [BUGFIX] Scaleway SD: The public IP will no longer be set to `__meta_meta_scaleway_instance_public_ipv4` if it is an IPv6 address. #14228
+* [BUGFIX] UI: Display the correct value of Alerting rules' `keep_firing_for`. #16211
+
+## 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
+
+## 3.2.0 / 2025-02-17
+
+* [CHANGE] relabel: Replace actions can now use UTF-8 characters in `targetLabel` field. Note that `$` or `${}` will be expanded. This also apply to `replacement` field for `LabelMap` action. #15851
+* [CHANGE] rulefmt: Rule names can use UTF-8 characters, except `{` and `}` characters (due to common mistake checks). #15851
+* [FEATURE] remote/otlp: Add feature flag `otlp-deltatocumulative` to support conversion from delta to cumulative. #15165
+* [ENHANCEMENT] openstack SD: Discover Octavia loadbalancers. #15539
+* [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] 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
+* [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
+* [BUGFIX] ui: Merge duplicate "Alerts page settings" sections. #15810
+* [BUGFIX] PromQL: Fix `` functions with histograms. #15711
+
+## 3.1.0 / 2025-01-02
+
+ * [SECURITY] upgrade golang.org/x/crypto to address reported CVE-2024-45337. #15691
+ * [CHANGE] Notifier: Increment prometheus_notifications_errors_total by the number of affected alerts rather than per batch. #15428
+ * [CHANGE] API: list rules field "groupNextToken:omitempty" renamed to "groupNextToken". #15400
+ * [ENHANCEMENT] OTLP translate: keep identifying attributes in target_info. #15448
+ * [ENHANCEMENT] Paginate rule groups, add infinite scroll to rules within groups. #15677
+ * [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
+ * [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: 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
+ * [PERF] TSDB: reduce memory allocations. #15465, #15427
+ * [PERF] Storage: Implement limit in mergeGenericQuerier. #14489
+ * [PERF] TSDB: Optimize inverse matching. #14144
+ * [PERF] Regex: use stack memory for lowercase copy of string. #15210
+ * [PERF] TSDB: When deleting from postings index, pause to unlock and let readers read. #15242
+ * [BUGFIX] Main: Avoid possible segfault at exit. (#15724)
+ * [BUGFIX] Rules: Do not run rules concurrently if uncertain about dependencies. #15560
+ * [BUGFIX] PromQL: Adds test for `absent`, `absent_over_time` and `deriv` func with histograms. #15667
+ * [BUGFIX] PromQL: Fix various bugs related to quoting UTF-8 characters. #15531
+ * [BUGFIX] Scrape: fix nil panic after scrape loop reload. #15563
+ * [BUGFIX] Remote-write: fix panic on repeated log message. #15562
+ * [BUGFIX] Scrape: reload would ignore always_scrape_classic_histograms and convert_classic_histograms_to_nhcb configs. #15489
+ * [BUGFIX] TSDB: fix data corruption in experimental native histograms. #15482
+ * [BUGFIX] PromQL: Ignore histograms in all time related functions. #15479
+ * [BUGFIX] OTLP receiver: Convert metric metadata. #15416
+ * [BUGFIX] PromQL: Fix `resets` function for histograms. #15527
+ * [BUGFIX] PromQL: Fix behaviour of `changes()` for mix of histograms and floats. #15469
+ * [BUGFIX] PromQL: Fix behaviour of some aggregations with histograms. #15432
+ * [BUGFIX] allow quoted exemplar keys in openmetrics text format. #15260
+ * [BUGFIX] TSDB: fixes for rare conditions when loading write-behind-log (WBL). #15380
+ * [BUGFIX] `round()` function did not remove `__name__` label. #15250
+ * [BUGFIX] Promtool: analyze block shows metric name with 0 cardinality. #15438
+ * [BUGFIX] PromQL: Fix `count_values` for histograms. #15422
+ * [BUGFIX] PromQL: fix issues with comparison binary operations with `bool` modifier and native histograms. #15413
+ * [BUGFIX] PromQL: fix incorrect "native histogram ignored in aggregation" annotations. #15414
+ * [BUGFIX] PromQL: Corrects the behaviour of some operator and aggregators with Native Histograms. #15245
+ * [BUGFIX] TSDB: Always return unknown hint for first sample in non-gauge histogram chunk. #15343
+ * [BUGFIX] PromQL: Clamp functions: Ignore any points with native histograms. #15169
+ * [BUGFIX] TSDB: Fix race on stale values in headAppender. #15322
+ * [BUGFIX] UI: Fix selector / series formatting for empty metric names. #15340
+ * [BUGFIX] OTLP receiver: Allow colons in non-standard units. #15710
+
+## 3.0.1 / 2024-11-28
+
+The first bug fix release for Prometheus 3.
+
+* [BUGFIX] Promql: Make subqueries left open. #15431
+* [BUGFIX] Fix memory leak when query log is enabled. #15434
+* [BUGFIX] Support utf8 names on /v1/label/:name/values endpoint. #15399
+
+## 3.0.0 / 2024-11-14
+
+This release includes new features such as a brand new UI and UTF-8 support enabled by default. As this marks the first new major version in seven years, several breaking changes are introduced. The breaking changes are mainly around the removal of deprecated feature flags and CLI arguments, and the full list can be found below. For users that want to upgrade we recommend to read through our [migration guide](https://prometheus.io/docs/prometheus/3.0/migration/).
+
+* [CHANGE] Set the `GOMAXPROCS` variable automatically to match the Linux CPU quota. Use `--no-auto-gomaxprocs` to disable it. The `auto-gomaxprocs` feature flag was removed. #15376
+* [CHANGE] Set the `GOMEMLIMIT` variable automatically to match the Linux container memory limit. Use `--no-auto-gomemlimit` to disable it. The `auto-gomemlimit` feature flag was removed. #15373
+* [CHANGE] Scraping: Remove implicit fallback to the Prometheus text format in case of invalid/missing Content-Type and fail the scrape instead. Add ability to specify a `fallback_scrape_protocol` in the scrape config. #15136
+* [CHANGE] Remote-write: default enable_http2 to false. #15219
+* [CHANGE] Scraping: normalize "le" and "quantile" label values upon ingestion. #15164
+* [CHANGE] Scraping: config `scrape_classic_histograms` was renamed to `always_scrape_classic_histograms`. #15178
+* [CHANGE] Config: remove expand-external-labels flag, expand external labels env vars by default. #14657
+* [CHANGE] Disallow configuring AM with the v1 api. #13883
+* [CHANGE] regexp `.` now matches all characters (performance improvement). #14505
+* [CHANGE] `holt_winters` is now called `double_exponential_smoothing` and moves behind the [experimental-promql-functions feature flag](https://prometheus.io/docs/prometheus/latest/feature_flags/#experimental-promql-functions). #14930
+* [CHANGE] API: The OTLP receiver endpoint can now be enabled using `--web.enable-otlp-receiver` instead of `--enable-feature=otlp-write-receiver`. #14894
+* [CHANGE] Prometheus will not add or remove port numbers from the target address. `no-default-scrape-port` feature flag removed. #14160
+* [CHANGE] Logging: the format of log lines has changed a little, along with the adoption of Go's Structured Logging package. #14906
+* [CHANGE] Don't create extra `_created` timeseries if feature-flag `created-timestamp-zero-ingestion` is enabled. #14738
+* [CHANGE] Float literals and time durations being the same is now a stable fetaure. #15111
+* [CHANGE] UI: The old web UI has been replaced by a completely new one that is less cluttered and adds a few new features (PromLens-style tree view, better metrics explorer, "Explain" tab). However, it is still missing some features of the old UI (notably, exemplar display and heatmaps). To switch back to the old UI, you can use the feature flag `--enable-feature=old-ui` for the time being. #14872
+* [CHANGE] PromQL: Range selectors and the lookback delta are now left-open, i.e. a sample coinciding with the lower time limit is excluded rather than included. #13904
+* [CHANGE] Kubernetes SD: Remove support for `discovery.k8s.io/v1beta1` API version of EndpointSlice. This version is no longer served as of Kubernetes v1.25. #14365
+* [CHANGE] Kubernetes SD: Remove support for `networking.k8s.io/v1beta1` API version of Ingress. This version is no longer served as of Kubernetes v1.22. #14365
+* [CHANGE] UTF-8: Enable UTF-8 support by default. Prometheus now allows all UTF-8 characters in metric and label names. The corresponding `utf8-name` feature flag has been removed. #14705, #15258
+* [CHANGE] Console: Remove example files for the console feature. Users can continue using the console feature by supplying their own JavaScript and templates. #14807
+* [CHANGE] SD: Enable the new service discovery manager by default. This SD manager does not restart unchanged discoveries upon reloading. This makes reloads faster and reduces pressure on service discoveries' sources. The corresponding `new-service-discovery-manager` feature flag has been removed. #14770
+* [CHANGE] Agent mode has been promoted to stable. The feature flag `agent` has been removed. To run Prometheus in Agent mode, use the new `--agent` cmdline arg instead. #14747
+* [CHANGE] Remove deprecated `remote-write-receiver`,`promql-at-modifier`, and `promql-negative-offset` feature flags. #13456, #14526
+* [CHANGE] Remove deprecated `storage.tsdb.allow-overlapping-blocks`, `alertmanager.timeout`, and `storage.tsdb.retention` flags. #14640, #14643
+* [FEATURE] OTLP receiver: Ability to skip UTF-8 normalization using `otlp.translation_strategy = NoUTF8EscapingWithSuffixes` configuration option. #15384
+* [FEATURE] Support config reload automatically - feature flag `auto-reload-config`. #14769, #15011
+* [ENHANCEMENT] Scraping, rules: handle targets reappearing, or rules moving group, when out-of-order is enabled. #14710
+* [ENHANCEMENT] Tools: add debug printouts to promtool rules unit testing #15196
+* [ENHANCEMENT] Scraping: support Created-Timestamp feature on native histograms. #14694
+* [ENHANCEMENT] UI: Many fixes and improvements. #14898, #14899, #14907, #14908, #14912, #14913, #14914, #14931, #14940, #14945, #14946, #14972, #14981, #14982, #14994, #15096
+* [ENHANCEMENT] UI: Web UI now displays notifications, e.g. when starting up and shutting down. #15082
+* [ENHANCEMENT] PromQL: Introduce exponential interpolation for native histograms. #14677
+* [ENHANCEMENT] TSDB: Add support for ingestion of out-of-order native histogram samples. #14850, #14546
+* [ENHANCEMENT] Alerts: remove metrics for removed Alertmanagers. #13909
+* [ENHANCEMENT] Kubernetes SD: Support sidecar containers in endpoint discovery. #14929
+* [ENHANCEMENT] Consul SD: Support catalog filters. #11224
+* [ENHANCEMENT] Move AM discovery page from "Monitoring status" to "Server status". #14875
+* [PERF] TSDB: Parallelize deletion of postings after head compaction. #14975
+* [PERF] TSDB: Chunk encoding: shorten some write sequences. #14932
+* [PERF] TSDB: Grow postings by doubling. #14721
+* [PERF] Relabeling: Optimize adding a constant label pair. #12180
+* [BUGFIX] Scraping: Don't log errors on empty scrapes. #15357
+* [BUGFIX] UI: fix selector / series formatting for empty metric names. #15341
+* [BUGFIX] PromQL: Fix stddev+stdvar aggregations to always ignore native histograms. #14941
+* [BUGFIX] PromQL: Fix stddev+stdvar aggregations to treat Infinity consistently. #14941
+* [BUGFIX] OTLP receiver: Preserve colons when generating metric names in suffix adding mode (this mode is always enabled, unless one uses Prometheus as a library). #15251
+* [BUGFIX] Scraping: Unit was missing when using protobuf format. #15095
+* [BUGFIX] PromQL: Only return "possible non-counter" annotation when `rate` returns points. #14910
+* [BUGFIX] TSDB: Chunks could have one unnecessary zero byte at the end. #14854
+* [BUGFIX] "superfluous response.WriteHeader call" messages in log. #14884
+* [BUGFIX] PromQL: Unary negation of native histograms. #14821
+* [BUGFIX] PromQL: Handle stale marker in native histogram series (e.g. if series goes away and comes back). #15025
+* [BUGFIX] Autoreload: Reload invalid yaml files. #14947
+* [BUGFIX] Scrape: Do not override target parameter labels with config params. #11029
+
+## 2.53.5 / 2025-06-30
+
+* [ENHANCEMENT] TSDB: Add backward compatibility with the upcoming TSDB block index v3 #16762
+* [BUGFIX] Top-level: Update GOGC before loading TSDB #16521
+
+## 2.53.4 / 2025-03-18
+
+* [BUGFIX] Runtime: fix GOGC is being set to 0 when installed with empty prometheus.yml file resulting high cpu usage. #16090
+* [BUGFIX] Scrape: fix dropping valid metrics after previous scrape failed. #16220
+
+## 2.53.3 / 2024-11-04
+
+* [BUGFIX] Scraping: allow multiple samples on same series, with explicit timestamps. #14685, #14740
+
+## 2.53.2 / 2024-08-09
+
+Fix a bug where Prometheus would crash with a segmentation fault if a remote-read
+request accessed a block on disk at about the same time as TSDB created a new block.
+
+* [BUGFIX] Remote-Read: Resolve occasional segmentation fault on query. #14515,#14523
+
+## 2.55.1 / 2024-11-04
* [BUGFIX] `round()` function did not remove `__name__` label. #15250
@@ -42,7 +705,7 @@
## 2.54.1 / 2024-08-27
-* [BUGFIX] Scraping: allow multiple samples on same series, with explicit timestamps. #14685
+* [BUGFIX] Scraping: allow multiple samples on same series, with explicit timestamps (mixing samples of the same series with and without timestamps is still rejected). #14685
* [BUGFIX] Docker SD: fix crash in `match_first_network` mode when container is reconnected to a new network. #14654
* [BUGFIX] PromQL: fix experimental native histograms getting corrupted due to vector selector bug in range queries. #14538
* [BUGFIX] PromQL: fix experimental native histogram counter reset detection on stale samples. #14514
@@ -124,6 +787,7 @@ This release changes the default for GOGC, the Go runtime control for the trade-
## 2.52.0 / 2024-05-07
* [CHANGE] TSDB: Fix the predicate checking for blocks which are beyond the retention period to include the ones right at the retention boundary. #9633
+* [CHANGE] Scrape: Multiple samples (even with different timestamps) are treated as duplicates during one scrape.
* [FEATURE] Kubernetes SD: Add a new metric `prometheus_sd_kubernetes_failures_total` to track failed requests to Kubernetes API. #13554
* [FEATURE] Kubernetes SD: Add node and zone metadata labels when using the endpointslice role. #13935
* [FEATURE] Azure SD/Remote Write: Allow usage of Azure authorization SDK. #13099
@@ -137,7 +801,7 @@ This release changes the default for GOGC, the Go runtime control for the trade-
* [ENHANCEMENT] TSDB: Pause regular block compactions if the head needs to be compacted (prioritize head as it increases memory consumption). #13754
* [ENHANCEMENT] Observability: Improved logging during signal handling termination. #13772
* [ENHANCEMENT] Observability: All log lines for drop series use "num_dropped" key consistently. #13823
-* [ENHANCEMENT] Observability: Log chunk snapshot and mmaped chunk replay duration during WAL replay. #13838
+* [ENHANCEMENT] Observability: Log chunk snapshot and mmapped chunk replay duration during WAL replay. #13838
* [ENHANCEMENT] Observability: Log if the block is being created from WBL during compaction. #13846
* [BUGFIX] PromQL: Fix inaccurate sample number statistic when querying histograms. #13667
* [BUGFIX] PromQL: Fix `histogram_stddev` and `histogram_stdvar` for cases where the histogram has negative buckets. #13852
@@ -674,7 +1338,7 @@ The binaries published with this release are built with Go1.17.8 to avoid [CVE-2
## 2.33.0 / 2022-01-29
-* [CHANGE] PromQL: Promote negative offset and `@` modifer to stable features. #10121
+* [CHANGE] PromQL: Promote negative offset and `@` modifier to stable features. #10121
* [CHANGE] Web: Promote remote-write-receiver to stable. #10119
* [FEATURE] Config: Add `stripPort` template function. #10002
* [FEATURE] Promtool: Add cardinality analysis to `check metrics`, enabled by flag `--extended`. #10045
@@ -911,7 +1575,7 @@ This vulnerability has been reported by Aaron Devaney from MDSec.
* [ENHANCEMENT] Templating: Enable parsing strings in `humanize` functions. #8682
* [BUGFIX] UI: Provide errors instead of blank page on TSDB Status Page. #8654 #8659
* [BUGFIX] TSDB: Do not panic when writing very large records to the WAL. #8790
-* [BUGFIX] TSDB: Avoid panic when mmaped memory is referenced after the file is closed. #8723
+* [BUGFIX] TSDB: Avoid panic when mmapped memory is referenced after the file is closed. #8723
* [BUGFIX] Scaleway Discovery: Fix nil pointer dereference. #8737
* [BUGFIX] Consul Discovery: Restart no longer required after config update with no targets. #8766
@@ -1837,7 +2501,7 @@ information, read the announcement blog post and migration guide.
## 1.7.0 / 2017-06-06
* [CHANGE] Compress remote storage requests and responses with unframed/raw snappy.
-* [CHANGE] Properly ellide secrets in config.
+* [CHANGE] Properly elide secrets in config.
* [FEATURE] Add OpenStack service discovery.
* [FEATURE] Add ability to limit Kubernetes service discovery to certain namespaces.
* [FEATURE] Add metric for discovered number of Alertmanagers.
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 00000000000..43c994c2d36
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1 @@
+@AGENTS.md
diff --git a/CODEOWNERS b/CODEOWNERS
new file mode 100644
index 00000000000..1822bf63d18
--- /dev/null
+++ b/CODEOWNERS
@@ -0,0 +1,33 @@
+#
+# 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
+
+# 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
+/tsdb/agent @prometheus/default-maintainers @jesusvazquez @codesome @bwplotka @krajorama @kgeckhart
+
+# Service discovery.
+/discovery/kubernetes @prometheus/default-maintainers @brancz @rexagod
+/discovery/stackit @prometheus/default-maintainers @jkroepke
+/discovery/aws/ @prometheus/default-maintainers @matt-gp @sysadmind
+/discovery/consul @prometheus/default-maintainers @mrvarmazyar
+/discovery/hetzner @prometheus/default-maintainers @jooola @apricote
+/discovery/scaleway @prometheus/default-maintainers @remyleone
+# 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 9b1b286ccf4..cfb346e4d0d 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 b47f77dcd69..88a64ac490d 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -2,27 +2,30 @@ ARG ARCH="amd64"
ARG OS="linux"
FROM quay.io/prometheus/busybox-${OS}-${ARCH}:latest
LABEL maintainer="The Prometheus Authors "
+LABEL org.opencontainers.image.authors="The Prometheus Authors" \
+ org.opencontainers.image.vendor="Prometheus" \
+ org.opencontainers.image.title="Prometheus" \
+ org.opencontainers.image.description="The Prometheus monitoring system and time series database" \
+ 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" \
+ io.prometheus.image.variant="busybox"
ARG ARCH="amd64"
ARG OS="linux"
COPY .build/${OS}-${ARCH}/prometheus /bin/prometheus
COPY .build/${OS}-${ARCH}/promtool /bin/promtool
COPY documentation/examples/prometheus.yml /etc/prometheus/prometheus.yml
-COPY console_libraries/ /usr/share/prometheus/console_libraries/
-COPY consoles/ /usr/share/prometheus/consoles/
COPY LICENSE /LICENSE
COPY NOTICE /NOTICE
-COPY npm_licenses.tar.bz2 /npm_licenses.tar.bz2
WORKDIR /prometheus
-RUN ln -s /usr/share/prometheus/console_libraries /usr/share/prometheus/consoles/ /etc/prometheus/ && \
- chown -R nobody:nobody /etc/prometheus /prometheus
+RUN chown -R nobody:nobody /etc/prometheus /prometheus && chmod g+w /prometheus
USER nobody
EXPOSE 9090
VOLUME [ "/prometheus" ]
ENTRYPOINT [ "/bin/prometheus" ]
CMD [ "--config.file=/etc/prometheus/prometheus.yml", \
- "--storage.tsdb.path=/prometheus", \
- "--web.console.libraries=/usr/share/prometheus/console_libraries", \
- "--web.console.templates=/usr/share/prometheus/consoles" ]
+ "--storage.tsdb.path=/prometheus" ]
diff --git a/Dockerfile.distroless b/Dockerfile.distroless
new file mode 100644
index 00000000000..7cf85012c28
--- /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 /
+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 3661ddaa0a1..57dc9811e9c 100644
--- a/MAINTAINERS.md
+++ b/MAINTAINERS.md
@@ -1,28 +1,34 @@
# Maintainers
+## Please keep this file in sync with the CODEOWNERS file!
+
General maintainers:
* Bryan Boreham (bjboreham@gmail.com / @bboreham)
-* Levi Harrison (levi@leviharrison.dev / @LeviHarrison)
* Ayoub Mrini (ayoubmrini424@gmail.com / @machine424)
* Julien Pivotto (roidelapluie@prometheus.io / @roidelapluie)
+* György Krajcsovits ( / @krajorama)
+* Bartłomiej Płotka ( / @bwplotka)
+* Arve Knudsen ( / @aknuds1)
Maintainers for specific parts of the codebase:
* `cmd`
* `promtool`: David Leadbeater ( / @dgl)
* `discovery`
- * `k8s`: Frederic Branczyk ( / @brancz)
+ * `hetzner`: (@jooola) (@apricote)
+ * `k8s`: Frederic Branczyk ( / @brancz), Pranshu Srivastava ( / @rexagod)
+ * `stackit`: Jan-Otto Kröpke ( / @jkroepke)
+ * `consul`: Mohammad Varmazyar ( / @mrvarmazyar)
+ * `scaleway`: Rémy Léone (rleone@scaleway.com / @remyleone)
* `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)
- * `otlptranslator`: Arve Knudsen ( / @aknuds1), Jesús Vázquez ( / @jesusvazquez)
-* `tsdb`: Ganesh Vernekar ( / @codesome), Bartłomiej Płotka ( / @bwplotka), Jesús Vázquez ( / @jesusvazquez)
- * `agent`: Robert Fratto ( / @rfratto)
+ * `remote`: Callum Styan ( / @cstyan), Tom Wilkie (tom.wilkie@gmail.com / @tomwilkie), Alex Greenbank ( / @alexgreenbank)
+ * `otlptranslator`: Arthur Silva Sens ( / @ArthurSens, Jesús Vázquez ( / @jesusvazquez)
+* `tsdb`: Ganesh Vernekar ( / @codesome), Jesús Vázquez ( / @jesusvazquez)
+ * `agent`: Kyle Eckhart ( / @kgeckhart)
* `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
@@ -32,7 +38,7 @@ incomplete and out of date. However the listed maintainer(s) should be able to
direct a PR/question to the right person.
v3 release coordinators:
-* Alex Greenbank ( / @alexgreenbank)
+* Alex Greenbank ( / @alexgreenbank)
* Carrie Edwards ( / @carrieedwards)
* Fiona Liao ( / @fionaliao)
* Jan Fajerski ( / @jan--f)
diff --git a/Makefile b/Makefile
index f2bb3fcb7a5..d1033a2a582 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,11 +12,9 @@
# limitations under the License.
# Needs to be defined before including Makefile.common to auto-generate targets
-DOCKER_ARCHS ?= amd64 armv7 arm64 ppc64le s390x
-
UI_PATH = web/ui
+BUILD_UI ?= all
UI_NODE_MODULES_PATH = $(UI_PATH)/node_modules
-REACT_APP_NPM_LICENSES_TARBALL = "npm_licenses.tar.bz2"
PROMTOOL = ./promtool
TSDB_BENCHMARK_NUM_METRICS ?= 1000
@@ -28,8 +26,22 @@ GOYACC_VERSION ?= v0.6.0
include Makefile.common
+ifeq (arm, $(GOHOSTARCH))
+ PROTOC_ARCH ?= aarch_64
+else
+ PROTOC_ARCH ?= x86_64
+endif
+
+PROTOC_VERSION ?= 3.15.8
+PROTOC_URL ?= https://github.com/protocolbuffers/protobuf/releases/download/v$(PROTOC_VERSION)/protoc-$(PROTOC_VERSION)-linux-$(PROTOC_ARCH).zip
+
DOCKER_IMAGE_NAME ?= prometheus
+# Only build UI if PREBUILT_ASSETS_STATIC_DIR is not set
+ifdef PREBUILT_ASSETS_STATIC_DIR
+ SKIP_UI_BUILD = true
+endif
+
.PHONY: update-npm-deps
update-npm-deps:
@echo ">> updating npm dependencies"
@@ -42,32 +54,64 @@ upgrade-npm-deps:
.PHONY: ui-bump-version
ui-bump-version:
- version=$$(sed s/2/0/ < VERSION) && ./scripts/ui_release.sh --bump-version "$${version}"
- cd web/ui && npm install
- git add "./web/ui/package-lock.json" "./**/package.json"
+ version=$$(./scripts/get_module_version.sh) && ./scripts/ui_release.sh --bump-version "$${version}"
+ cd web/ui && pnpm install
+ cd web/ui/react-app && pnpm install
+ git add "./web/ui/pnpm-lock.yaml" "./web/ui/react-app/pnpm-lock.yaml" "./**/package.json"
.PHONY: ui-install
ui-install:
- cd $(UI_PATH) && npm install
+ cd $(UI_PATH) && pnpm install
+ # The old React app has been separated from the npm workspaces setup to avoid
+ # issues with conflicting dependencies. This is a temporary solution until the
+ # new Mantine-based UI is fully integrated and the old app can be removed.
+ cd $(UI_PATH)/react-app && pnpm install
.PHONY: ui-build
ui-build:
- cd $(UI_PATH) && CI="" npm run build
+ifeq ($(BUILD_UI),mantine)
+ cd $(UI_PATH) && CI="" pnpm run build:mantine-ui
+else
+ cd $(UI_PATH) && CI="" pnpm run build
+endif
.PHONY: ui-build-module
ui-build-module:
- cd $(UI_PATH) && npm run build:module
+ cd $(UI_PATH) && pnpm run build:module
.PHONY: ui-test
ui-test:
- cd $(UI_PATH) && CI=true npm run test
+ cd $(UI_PATH) && CI=true pnpm run test
.PHONY: ui-lint
ui-lint:
- cd $(UI_PATH) && npm run lint
+ cd $(UI_PATH) && pnpm run lint
+ # The old React app has been separated from the npm workspaces setup to avoid
+ # issues with conflicting dependencies. This is a temporary solution until the
+ # new Mantine-based UI is fully integrated and the old app can be removed.
+ cd $(UI_PATH)/react-app && pnpm 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 && pnpm exec 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
-assets: ui-install ui-build
+ifndef SKIP_UI_BUILD
+assets: check-node-version ui-install ui-build
+else
+assets:
+ @echo '>> skipping assets build, pre-built assets provided'
+endif
.PHONY: assets-compress
assets-compress: assets
@@ -79,6 +123,15 @@ assets-tarball: assets
@echo '>> packaging assets'
scripts/package_assets.sh
+.PHONY: protoc
+protoc:
+ @echo ">> Installing protoc"
+ $(eval PROTOC_TMP := $(shell mktemp))
+ curl -s -o $(PROTOC_TMP) -L $(PROTOC_URL)
+ unzip -u $(PROTOC_TMP) bin/protoc -d $(FIRST_GOPATH)
+ chmod +x $(FIRST_GOPATH)/bin/protoc
+ rm -v $(PROTOC_TMP)
+
.PHONY: parser
parser:
@echo ">> running goyacc to generate the .go file."
@@ -96,7 +149,12 @@ promql/parser/generated_parser.y.go: promql/parser/generated_parser.y
.PHONY: clean-parser
clean-parser:
@echo ">> cleaning generated parser"
+ifeq (, $(shell command -v goyacc 2> /dev/null))
+ @echo "goyacc not installed so skipping"
+ @echo "To install: \"go install golang.org/x/tools/cmd/goyacc@$(GOYACC_VERSION)\" or run \"make install-goyacc\""
+else
@rm -f promql/parser/generated_parser.y.go
+endif
.PHONY: check-generated-parser
check-generated-parser: clean-parser promql/parser/generated_parser.y.go
@@ -114,32 +172,17 @@ install-goyacc:
ifeq ($(GO_ONLY),1)
test: common-test check-go-mod-version
else
-test: check-generated-parser common-test ui-build-module ui-test ui-lint check-go-mod-version
+test: check-generated-parser common-test check-node-version ui-build-module ui-test ui-lint check-go-mod-version
endif
-.PHONY: npm_licenses
-npm_licenses: ui-install
- @echo ">> bundling npm licenses"
- rm -f $(REACT_APP_NPM_LICENSES_TARBALL) npm_licenses
- ln -s . npm_licenses
- find npm_licenses/$(UI_NODE_MODULES_PATH) -iname "license*" | tar cfj $(REACT_APP_NPM_LICENSES_TARBALL) --files-from=-
- rm -f npm_licenses
-
.PHONY: tarball
-tarball: npm_licenses common-tarball
+tarball: 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
+docker: common-docker
.PHONY: build
-build: assets npm_licenses assets-compress plugins common-build
+build: assets assets-compress common-build
.PHONY: bench_tsdb
bench_tsdb: $(PROMU)
@@ -163,11 +206,37 @@ 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 compliance
+
.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 \
- $(GO) get -d $$m; \
+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:
+ @./scripts/check-node-version.sh
+
+.PHONY: bump-go-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 34d65bb56de..a7c5f553e12 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,14 @@ ifneq ($(shell command -v gotestsum 2> /dev/null),)
endif
endif
-PROMU_VERSION ?= 0.17.0
+PROMU_VERSION ?= 0.20.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 ?= v1.60.2
+GOLANGCI_LINT_VERSION ?= v2.11.4
+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.
ifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux darwin))
@@ -81,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
-DOCKER_ARCHS ?= amd64
+# 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 arm64 armv7 ppc64le riscv64 s390x
+DOCKERFILE_VARIANTS ?= $(wildcard Dockerfile 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))
@@ -111,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."; \
@@ -121,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:
@@ -138,7 +166,7 @@ common-deps:
update-go-deps:
@echo ">> updating Go dependencies"
@for m in $$($(GO) list -mod=readonly -m -f '{{ if and (not .Indirect) (not .Main)}}{{.Path}}{{end}}' all); do \
- $(GO) get -d $$m; \
+ $(GO) get $$m; \
done
$(GO) mod tidy
@@ -156,9 +184,13 @@ $(GOTEST_DIR):
@mkdir -p $@
.PHONY: common-format
-common-format:
+common-format: $(GOLANGCI_LINT)
@echo ">> formatting code"
$(GO) fmt $(pkgs)
+ifdef GOLANGCI_LINT
+ @echo ">> formatting code with golangci-lint"
+ $(GOLANGCI_LINT) fmt $(GOLANGCI_FMT_OPTS)
+endif
.PHONY: common-vet
common-vet:
@@ -215,28 +247,142 @@ 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"; \
+ refs=""; \
+ for arch in $(DOCKER_ARCHS); do \
+ refs="$$refs $(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$$arch:$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name"; \
+ done; \
+ if [ -z "$$refs" ]; then \
+ echo "Skipping manifest for $$variant_name variant (no supported architectures)"; \
+ continue; \
+ fi; \
+ DOCKER_CLI_EXPERIMENTAL=enabled docker manifest create -a "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name" $$refs; \
+ 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"; \
+ refs=""; \
+ for arch in $(DOCKER_ARCHS); do \
+ refs="$$refs $(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$$arch:$(SANITIZED_DOCKER_IMAGE_TAG)"; \
+ done; \
+ if [ -z "$$refs" ]; then \
+ echo "Skipping default variant manifest (no supported architectures)"; \
+ continue; \
+ fi; \
+ DOCKER_CLI_EXPERIMENTAL=enabled docker manifest create -a "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)" $$refs; \
+ 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"; \
+ refs=""; \
+ for arch in $(DOCKER_ARCHS); do \
+ refs="$$refs $(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$$arch:v$(DOCKER_MAJOR_VERSION_TAG)-$$variant_name"; \
+ done; \
+ if [ -z "$$refs" ]; then \
+ echo "Skipping version-tag manifest for $$variant_name variant (no supported architectures)"; \
+ continue; \
+ fi; \
+ DOCKER_CLI_EXPERIMENTAL=enabled docker manifest create -a "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):v$(DOCKER_MAJOR_VERSION_TAG)-$$variant_name" $$refs; \
+ 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"; \
+ refs=""; \
+ for arch in $(DOCKER_ARCHS); do \
+ refs="$$refs $(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$$arch:v$(DOCKER_MAJOR_VERSION_TAG)"; \
+ done; \
+ if [ -z "$$refs" ]; then \
+ echo "Skipping default variant version-tag manifest (no supported architectures)"; \
+ continue; \
+ fi; \
+ DOCKER_CLI_EXPERIMENTAL=enabled docker manifest create -a "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):v$(DOCKER_MAJOR_VERSION_TAG)" $$refs; \
+ DOCKER_CLI_EXPERIMENTAL=enabled docker manifest push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):v$(DOCKER_MAJOR_VERSION_TAG)"; \
+ fi; \
+ fi; \
+ done
.PHONY: promu
promu: $(PROMU)
@@ -248,8 +394,8 @@ $(PROMU):
cp $(PROMU_TMP)/promu-$(PROMU_VERSION).$(GO_BUILD_PLATFORM)/promu $(FIRST_GOPATH)/bin/promu
rm -r $(PROMU_TMP)
-.PHONY: proto
-proto:
+.PHONY: common-proto
+common-proto:
@echo ">> generating code from proto files"
@./scripts/genproto.sh
@@ -261,6 +407,10 @@ $(GOLANGCI_LINT):
| sh -s -- -b $(FIRST_GOPATH)/bin $(GOLANGCI_LINT_VERSION)
endif
+.PHONY: common-print-golangci-lint-version
+common-print-golangci-lint-version:
+ @echo $(GOLANGCI_LINT_VERSION)
+
.PHONY: precheck
precheck::
diff --git a/NOTICE b/NOTICE
index 8605c258e32..b82d284fb19 100644
--- a/NOTICE
+++ b/NOTICE
@@ -101,8 +101,19 @@ https://github.com/microsoft/vscode-codicons
Copyright (c) Microsoft Corporation and other contributors
See https://github.com/microsoft/vscode-codicons/blob/main/LICENSE for license details.
+Mantine UI
+https://github.com/mantinedev/mantine
+Copyright (c) 2021 Vitaly Rtishchev
+See https://github.com/mantinedev/mantine/blob/master/LICENSE for license details.
+
We also use code from a large number of npm packages. For details, see:
- https://github.com/prometheus/prometheus/blob/main/web/ui/react-app/package.json
- https://github.com/prometheus/prometheus/blob/main/web/ui/react-app/package-lock.json
-- The individual package licenses as copied from the node_modules directory can be found in
- the npm_licenses.tar.bz2 archive in release tarballs and Docker images.
+- https://github.com/prometheus/prometheus/blob/main/web/ui/mantine-ui/package.json
+- https://github.com/prometheus/prometheus/blob/main/web/ui/pnpm-lock.yaml
+- The individual licenses of the packages bundled into the new (mantine) web UI
+ are collected at build time, embedded in the Prometheus binary, and served by
+ the web UI at /assets/third-party-licenses.txt.
+- The licenses of the packages bundled into the old (react-app) web UI, served
+ via --enable-feature=old-ui, are extracted at build time into a
+ *.LICENSE.txt file that is embedded alongside the old UI's JavaScript bundle.
diff --git a/README.md b/README.md
index df974e1097b..9f644d57dc4 100644
--- a/README.md
+++ b/README.md
@@ -12,9 +12,9 @@ examples and guides.
[][hub]
[](https://goreportcard.com/report/github.com/prometheus/prometheus)
[](https://bestpractices.coreinfrastructure.org/projects/486)
+[](https://github.com/prometheus/prometheus/actions/workflows/govulncheck.yml)
[](https://securityscorecards.dev/viewer/?uri=github.com/prometheus/prometheus)
[](https://clomonitor.io/projects/cncf/prometheus)
-[](https://gitpod.io/#https://github.com/prometheus/prometheus)
[](https://bugs.chromium.org/p/oss-fuzz/issues/list?sort=-opened&can=1&q=proj:prometheus)
@@ -40,14 +40,14 @@ The features that distinguish Prometheus from other metrics and monitoring syste
## Install
-There are various ways of installing Prometheus.
+There are various ways to install Prometheus.
### Precompiled binaries
Precompiled binaries for released versions are available in the
[*download* section](https://prometheus.io/download/)
on [prometheus.io](https://prometheus.io). Using the latest production release binary
-is the recommended way of installing Prometheus.
+is the recommended way to install Prometheus.
See the [Installing](https://prometheus.io/docs/introduction/install/)
chapter in the documentation for all the details.
@@ -65,11 +65,11 @@ Prometheus will now be reachable at .
### Building from source
-To build Prometheus from source code, You need:
+To build Prometheus from source code, you need:
-* Go [version 1.17 or greater](https://golang.org/doc/install).
-* NodeJS [version 16 or greater](https://nodejs.org/).
-* npm [version 7 or greater](https://www.npmjs.com/).
+* Go: Version specified in [go.mod](./go.mod) or greater.
+* NodeJS: Version specified in [.nvmrc](./web/ui/.nvmrc) or greater.
+* 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-formated 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
@@ -130,18 +145,28 @@ always, be extra careful when loading third party code.
### Building the Docker image
-The `make docker` target is designed for use in our CI system.
You can build a docker image locally with the following commands:
```bash
make promu
promu crossbuild -p linux/amd64
-make npm_licenses
make common-docker-amd64
```
+The `make docker` target is intended only for use in our CI system and will not
+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
@@ -158,8 +183,19 @@ This is experimental.
### Prometheus code base
In order to comply with [go mod](https://go.dev/ref/mod#versions) rules,
-Prometheus release number do not exactly match Go module releases. For the
-Prometheus v2.y.z releases, we are publishing equivalent v0.y.z tags.
+Prometheus release number do not exactly match Go module releases.
+
+For the
+Prometheus v3.y.z releases, we are publishing equivalent v0.3y.z tags. The y in v0.3y.z is always padded to two digits, with a leading zero if needed.
+
+Therefore, a user that would want to use Prometheus v3.0.0 as a library could do:
+
+```shell
+go get github.com/prometheus/prometheus@v0.300.0
+```
+
+For the
+Prometheus v2.y.z releases, we published the equivalent v0.y.z tags.
Therefore, a user that would want to use Prometheus v2.35.0 as a library could do:
@@ -177,7 +213,7 @@ For more information on building, running, and developing on the React-based UI,
## More information
-* Godoc documentation is available via [pkg.go.dev](https://pkg.go.dev/github.com/prometheus/prometheus). Due to peculiarities of Go Modules, v2.x.y will be displayed as v0.x.y.
+* Godoc documentation is available via [pkg.go.dev](https://pkg.go.dev/github.com/prometheus/prometheus). Due to peculiarities of Go Modules, v3.y.z will be displayed as v0.3y.z (the y in v0.3y.z is always padded to two digits, with a leading zero if needed), while v2.y.z will be displayed as v0.y.z.
* See the [Community page](https://prometheus.io/community) for how to reach the Prometheus developers and users on various communication channels.
## Contributing
diff --git a/RELEASE.md b/RELEASE.md
index b978a3c2261..cc309d1c3cd 100644
--- a/RELEASE.md
+++ b/RELEASE.md
@@ -5,61 +5,27 @@ This page describes the release process and the currently planned schedule for u
## Release schedule
Release cadence of first pre-releases being cut is 6 weeks.
-
-| release series | date of first pre-release (year-month-day) | release shepherd |
-|----------------|--------------------------------------------|---------------------------------------------|
-| v2.4 | 2018-09-06 | Goutham Veeramachaneni (GitHub: @gouthamve) |
-| v2.5 | 2018-10-24 | Frederic Branczyk (GitHub: @brancz) |
-| v2.6 | 2018-12-05 | Simon Pasquier (GitHub: @simonpasquier) |
-| v2.7 | 2019-01-16 | Goutham Veeramachaneni (GitHub: @gouthamve) |
-| v2.8 | 2019-02-27 | Ganesh Vernekar (GitHub: @codesome) |
-| v2.9 | 2019-04-10 | Brian Brazil (GitHub: @brian-brazil) |
-| v2.10 | 2019-05-22 | Björn Rabenstein (GitHub: @beorn7) |
-| v2.11 | 2019-07-03 | Frederic Branczyk (GitHub: @brancz) |
-| v2.12 | 2019-08-14 | Julius Volz (GitHub: @juliusv) |
-| v2.13 | 2019-09-25 | Krasi Georgiev (GitHub: @krasi-georgiev) |
-| v2.14 | 2019-11-06 | Chris Marchbanks (GitHub: @csmarchbanks) |
-| v2.15 | 2019-12-18 | Bartek Plotka (GitHub: @bwplotka) |
-| v2.16 | 2020-01-29 | Callum Styan (GitHub: @cstyan) |
-| v2.17 | 2020-03-11 | Julien Pivotto (GitHub: @roidelapluie) |
-| v2.18 | 2020-04-22 | Bartek Plotka (GitHub: @bwplotka) |
-| v2.19 | 2020-06-03 | Ganesh Vernekar (GitHub: @codesome) |
-| v2.20 | 2020-07-15 | Björn Rabenstein (GitHub: @beorn7) |
-| v2.21 | 2020-08-26 | Julien Pivotto (GitHub: @roidelapluie) |
-| v2.22 | 2020-10-07 | Frederic Branczyk (GitHub: @brancz) |
-| v2.23 | 2020-11-18 | Ganesh Vernekar (GitHub: @codesome) |
-| v2.24 | 2020-12-30 | Björn Rabenstein (GitHub: @beorn7) |
-| v2.25 | 2021-02-10 | Julien Pivotto (GitHub: @roidelapluie) |
-| v2.26 | 2021-03-24 | Bartek Plotka (GitHub: @bwplotka) |
-| v2.27 | 2021-05-05 | Chris Marchbanks (GitHub: @csmarchbanks) |
-| v2.28 | 2021-06-16 | Julius Volz (GitHub: @juliusv) |
-| v2.29 | 2021-07-28 | Frederic Branczyk (GitHub: @brancz) |
-| v2.30 | 2021-09-08 | Ganesh Vernekar (GitHub: @codesome) |
-| v2.31 | 2021-10-20 | Julien Pivotto (GitHub: @roidelapluie) |
-| v2.32 | 2021-12-01 | Julius Volz (GitHub: @juliusv) |
-| v2.33 | 2022-01-12 | Björn Rabenstein (GitHub: @beorn7) |
-| v2.34 | 2022-02-23 | Chris Marchbanks (GitHub: @csmarchbanks) |
-| v2.35 | 2022-04-06 | Augustin Husson (GitHub: @nexucis) |
-| v2.36 | 2022-05-18 | Matthias Loibl (GitHub: @metalmatze) |
-| v2.37 LTS | 2022-06-29 | Julien Pivotto (GitHub: @roidelapluie) |
-| v2.38 | 2022-08-10 | Julius Volz (GitHub: @juliusv) |
-| v2.39 | 2022-09-21 | Ganesh Vernekar (GitHub: @codesome) |
-| v2.40 | 2022-11-02 | Ganesh Vernekar (GitHub: @codesome) |
-| v2.41 | 2022-12-14 | Julien Pivotto (GitHub: @roidelapluie) |
-| v2.42 | 2023-01-25 | Kemal Akkoyun (GitHub: @kakkoyun) |
-| v2.43 | 2023-03-08 | Julien Pivotto (GitHub: @roidelapluie) |
-| v2.44 | 2023-04-19 | Bryan Boreham (GitHub: @bboreham) |
-| v2.45 LTS | 2023-05-31 | Jesus Vazquez (Github: @jesusvazquez) |
-| v2.46 | 2023-07-12 | Julien Pivotto (GitHub: @roidelapluie) |
-| v2.47 | 2023-08-23 | Bryan Boreham (GitHub: @bboreham) |
-| v2.48 | 2023-10-04 | Levi Harrison (GitHub: @LeviHarrison) |
-| v2.49 | 2023-12-05 | Bartek Plotka (GitHub: @bwplotka) |
-| v2.50 | 2024-01-16 | Augustin Husson (GitHub: @nexucis) |
-| v2.51 | 2024-03-07 | Bryan Boreham (GitHub: @bboreham) |
-| v2.52 | 2024-04-22 | Arthur Silva Sens (GitHub: @ArthurSens) |
-| v2.53 LTS | 2024-06-03 | George Krajcsovits (GitHub: @krajorama) |
-| v2.54 | 2024-07-17 | Bryan Boreham (GitHub: @bboreham) |
-| v2.55 | 2024-09-17 | Bryan Boreham (GitHub: @bboreham) |
+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 György 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-25 | Julien Pivotto (GitHub: @roidelapluie) |
+| v3.12 | 2026-05-06 | Bartek Plotka (GitHub: @bwplotka) |
+| v3.13 | 2026-06-17 | György Krajcsovits (Github: @krajorama) |
+| v3.14 | 2026-07-29 | **volunteer welcome** |
+| v3.15 | 2026-09-09 | **volunteer welcome** |
+| v3.16 | 2026-10-21 | **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.
@@ -82,7 +48,7 @@ These instructions are currently valid for the Prometheus server, i.e. the [prom
We use [Semantic Versioning](https://semver.org/).
-We maintain a separate branch for each minor release, named `release-.`, e.g. `release-1.1`, `release-2.0`.
+We maintain a separate branch for each minor release, named `release-.`, e.g. `release-2.1`, `release-3.0`.
Note that branch protection kicks in automatically for any branches whose name starts with `release-`. Never use names starting with `release-` for branches that are not release branches.
@@ -145,25 +111,24 @@ though this may be done at convenient times (e.g. by the UI maintainers) that ar
### 1. Prepare your release
-At the start of a new major or minor release cycle create the corresponding release branch based on the main branch. For example if we're releasing `2.17.0` and the previous stable release is `2.16.0` we need to create a `release-2.17` branch. Note that all releases are handled in protected release branches, see the above `Branch management and versioning` section. Release candidates and patch releases for any given major or minor release happen in the same `release-.` branch. Do not create `release-` for patch or release candidate releases.
+At the start of a new major or minor release cycle create the corresponding release branch based on the main branch. For example if we're releasing `3.7.0` and the previous stable release is `3.6.0` we need to create a `release-3.7` branch. Note that all releases are handled in protected release branches, see the [Branch management and versioning strategy](#branch-management-and-versioning-strategy) section. Release candidates and patch releases for any given major or minor release happen in the same `release-.` branch. Do not create `release-` for patch or release candidate releases.
Changes for a patch release or release candidate should be merged into the previously mentioned release branch via pull request.
-Bump the version in the `VERSION` file and update `CHANGELOG.md`. Do this in a proper PR pointing to the release branch as this gives others the opportunity to chime in on the release in general and on the addition to the changelog in particular. For a release candidate, append something like `-rc.0` to the version (with the corresponding changes to the tag name, the release name etc.).
+Bump the version in the `VERSION` file and update `CHANGELOG.md`. Do this in a proper PR pointing to the release branch as this gives others the opportunity to chime in on the release in general and on the addition to `CHANGELOG.md` in particular. For a release candidate, append something like `-rc.0` to the version (with the corresponding changes to the tag name, the release name etc.).
-When updating the `CHANGELOG.md` look at all PRs included in the release since the last release and verify if they need a changelog entry.
+Use [`scripts/generate_release_notes.sh`](scripts/generate_release_notes.sh) to produce a starting point for the `CHANGELOG.md` entries. It requires the [`release-notes`](https://github.com/kubernetes/release/tree/master/cmd/release-notes) tool and a `GITHUB_TOKEN` with read access to the repository:
-Note that `CHANGELOG.md` should only document changes relevant to users of Prometheus, including external API changes, performance improvements, and new features. Do not document changes of internal interfaces, code refactorings and clean-ups, changes to the build process, etc. People interested in these are asked to refer to the git history.
+Run it for the target release version (RC, final, or patch; for example `v3.11.0-rc.0`, `v3.11.0`, or `v3.11.1`):
-For release candidates still update `CHANGELOG.md`, but when you cut the final release later, merge all the changes from the pre-releases into the one final update.
+```bash
+GITHUB_TOKEN= scripts/generate_release_notes.sh 3.Y_SET_ME.Z_SET_ME
+```
-Entries in the `CHANGELOG.md` are meant to be in this order:
+Review the output carefully before adding it to `CHANGELOG.md`:
-* `[SECURITY]` - A bugfix that specifically fixes a security issue.
-* `[CHANGE]`
-* `[FEATURE]`
-* `[ENHANCEMENT]`
-* `[BUGFIX]`
+- Remove entries already present in a previous release's Changelog. Duplicates can appear for fixes merged to the previous release branch.
+- `CHANGELOG.md` should only document changes relevant to users of Prometheus, including external API changes, performance improvements, and new features. Do not document changes of internal interfaces, code refactorings and clean-ups, changes to the build process, etc. People interested in these are asked to refer to the git history.
Then bump the UI module version:
@@ -171,6 +136,9 @@ Then bump the UI module version:
make ui-bump-version
```
+All of the above goes to make a PR, which you should create targeting the `release-x.y` branch, then wait for CI to complete and get an approval.
+Once the PR is ready, merge it into the release branch and pull it down to your work area. Then you are ready for the next step:
+
### 2. Draft the new release
Tag the new release via the following commands:
@@ -181,19 +149,7 @@ git tag -s "${tag}" -m "${tag}"
git push origin "${tag}"
```
-Go modules versioning requires strict use of semver. Because we do not commit to
-avoid code-level breaking changes for the libraries between minor releases of
-the Prometheus server, we use major version zero releases for the libraries.
-
-Tag the new library release via the following commands:
-
-```bash
-tag="v$(sed s/2/0/ < VERSION)"
-git tag -s "${tag}" -m "${tag}"
-git push origin "${tag}"
-```
-
-Optionally, you can use this handy `.gitconfig` alias.
+Alternatively, you can use this handy `.gitconfig` alias.
```ini
[alias]
@@ -204,14 +160,29 @@ Then release with `git tag-release`.
Signing a tag with a GPG key is appreciated, but in case you can't add a GPG key to your Github account using the following [procedure](https://help.github.com/articles/generating-a-gpg-key/), you can replace the `-s` flag by `-a` flag of the `git tag` command to only annotate the tag without signing.
-Once a tag is created, the release process through CircleCI will be triggered for this tag and Circle CI will draft the GitHub release using the `prombot` account.
+Once a tag is created, the release process through Github Actions will be triggered for this tag and Github Actions will draft the GitHub release using the `prombot` account.
Finally, wait for the build step for the tag to finish. The point here is to wait for tarballs to be uploaded to the Github release and the container images to be pushed to the Docker Hub and Quay.io. Once that has happened, click _Publish release_, which will make the release publicly visible and create a GitHub notification.
**Note:** for a release candidate version ensure the _This is a pre-release_ box is checked when drafting the release in the Github UI. The CI job should take care of this but it's a good idea to double check before clicking _Publish release_.`
-### 3. Wrapping up
+### 3. Tag the library release
+
+Go modules versioning requires strict use of semver. Because we do not commit to
+avoid code-level breaking changes for the libraries between minor releases of
+the Prometheus server, we use major version zero releases for the libraries.
+
+Tagging the new library release works similar to the normal release tagging,
+but without the subsequent build and publish steps. Use the following commands:
+
+```bash
+tag="v$(./scripts/get_module_version.sh)"
+git tag -s "${tag}" -m "${tag}"
+git push origin "${tag}"
+```
+
+### 4. Wrapping up
-For release candidate versions (`v2.16.0-rc.0`), run the benchmark for 3 days using the `/prombench vX.Y.Z` command, `vX.Y.Z` being the latest stable patch release's tag of the previous minor release series, such as `v2.15.2`.
+For release candidate versions (`v3.6.0-rc.0`), run the benchmark for 3 days using the `/prombench vX.Y.Z` command, `vX.Y.Z` being the latest stable patch release's tag of the previous minor release series, such as `v3.5.2`.
If the release has happened in the latest release branch, merge the changes into main.
diff --git a/SECURITY.md b/SECURITY.md
index fed02d85c79..5e6f976dbff 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -3,4 +3,4 @@
The Prometheus security policy, including how to report vulnerabilities, can be
found here:
-
+[https://prometheus.io/docs/operating/security/](https://prometheus.io/docs/operating/security/)
diff --git a/VERSION b/VERSION
index 0a756ea0a78..c10780c628a 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-2.55.1
+3.13.1
diff --git a/cmd/prometheus/features_test.go b/cmd/prometheus/features_test.go
new file mode 100644
index 00000000000..5907c87247c
--- /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 b3bcb78b78b..6795c6bb4cd 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,19 +16,22 @@ package main
import (
"context"
+ "encoding/json"
"errors"
"fmt"
+ "log/slog"
"math"
"math/bits"
"net"
"net/http"
- _ "net/http/pprof" // Comment this line to disable pprof endpoint.
"net/url"
"os"
"os/signal"
"path/filepath"
+ goregexp "regexp" //nolint:depguard // The Prometheus client library requires us to pass a regexp from this package.
"runtime"
"runtime/debug"
+ "slices"
"strconv"
"strings"
"sync"
@@ -38,28 +41,26 @@ import (
"github.com/KimMachineGun/automemlimit/memlimit"
"github.com/alecthomas/kingpin/v2"
"github.com/alecthomas/units"
- "github.com/go-kit/log"
- "github.com/go-kit/log/level"
"github.com/grafana/regexp"
"github.com/mwitkow/go-conntrack"
"github.com/oklog/run"
+ remoteapi "github.com/prometheus/client_golang/exp/api/remote"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/collectors"
versioncollector "github.com/prometheus/client_golang/prometheus/collectors/version"
"github.com/prometheus/common/model"
- "github.com/prometheus/common/promlog"
- promlogflag "github.com/prometheus/common/promlog/flag"
+ "github.com/prometheus/common/promslog"
+ promslogflag "github.com/prometheus/common/promslog/flag"
"github.com/prometheus/common/version"
toolkit_web "github.com/prometheus/exporter-toolkit/web"
"go.uber.org/atomic"
"go.uber.org/automaxprocs/maxprocs"
+ "k8s.io/client-go/rest"
"k8s.io/klog"
klogv2 "k8s.io/klog/v2"
"github.com/prometheus/prometheus/config"
"github.com/prometheus/prometheus/discovery"
- "github.com/prometheus/prometheus/discovery/legacymanager"
- "github.com/prometheus/prometheus/discovery/targetgroup"
"github.com/prometheus/prometheus/model/exemplar"
"github.com/prometheus/prometheus/model/histogram"
"github.com/prometheus/prometheus/model/labels"
@@ -73,16 +74,60 @@ 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/tsdb/wlog"
+ "github.com/prometheus/prometheus/tsdb/chunkenc"
+ "github.com/prometheus/prometheus/tsdb/fileutil"
+ "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"
"github.com/prometheus/prometheus/web"
)
+// klogv1OutputCallDepth is the stack depth where we can find the origin of this call.
+const klogv1OutputCallDepth = 6
+
+// klogv1DefaultPrefixLength is the length of the log prefix that we have to strip out.
+const klogv1DefaultPrefixLength = 53
+
+// klogv1Writer is used in SetOutputBySeverity call below to redirect any calls
+// to klogv1 to end up in klogv2.
+// This is a hack to support klogv1 without use of go-kit/log. It is inspired
+// by klog's upstream klogv1/v2 coexistence example:
+// https://github.com/kubernetes/klog/blob/main/examples/coexist_klog_v1_and_v2/coexist_klog_v1_and_v2.go
+type klogv1Writer struct{}
+
+// Write redirects klogv1 calls to klogv2.
+// This is a hack to support klogv1 without use of go-kit/log. It is inspired
+// by klog's upstream klogv1/v2 coexistence example:
+// https://github.com/kubernetes/klog/blob/main/examples/coexist_klog_v1_and_v2/coexist_klog_v1_and_v2.go
+func (klogv1Writer) Write(p []byte) (n int, err error) {
+ if len(p) < klogv1DefaultPrefixLength {
+ klogv2.InfoDepth(klogv1OutputCallDepth, string(p))
+ return len(p), nil
+ }
+
+ switch p[0] {
+ case 'I':
+ klogv2.InfoDepth(klogv1OutputCallDepth, string(p[klogv1DefaultPrefixLength:]))
+ case 'W':
+ klogv2.WarningDepth(klogv1OutputCallDepth, string(p[klogv1DefaultPrefixLength:]))
+ case 'E':
+ klogv2.ErrorDepth(klogv1OutputCallDepth, string(p[klogv1DefaultPrefixLength:]))
+ case 'F':
+ klogv2.FatalDepth(klogv1OutputCallDepth, string(p[klogv1DefaultPrefixLength:]))
+ default:
+ klogv2.InfoDepth(klogv1OutputCallDepth, string(p[klogv1DefaultPrefixLength:]))
+ }
+
+ return len(p), nil
+}
+
var (
appName = "prometheus"
@@ -103,6 +148,10 @@ var (
)
func init() {
+ // This can be removed when the legacy global mode is fully deprecated.
+ //nolint:staticcheck
+ model.NameValidationScheme = model.UTF8Validation
+
prometheus.MustRegister(versioncollector.NewCollector(strings.ReplaceAll(appName, "-", "_")))
var err error
@@ -115,7 +164,7 @@ func init() {
// serverOnlyFlag creates server-only kingpin flag.
func serverOnlyFlag(app *kingpin.Application, name, help string) *kingpin.FlagClause {
return app.Flag(name, fmt.Sprintf("%s Use with server mode only.", help)).
- PreAction(func(parseContext *kingpin.ParseContext) error {
+ PreAction(func(*kingpin.ParseContext) error {
// This will be invoked only if flag is actually provided by user.
serverOnlyFlags = append(serverOnlyFlags, "--"+name)
return nil
@@ -125,7 +174,7 @@ func serverOnlyFlag(app *kingpin.Application, name, help string) *kingpin.FlagCl
// agentOnlyFlag creates agent-only kingpin flag.
func agentOnlyFlag(app *kingpin.Application, name, help string) *kingpin.FlagClause {
return app.Flag(name, fmt.Sprintf("%s Use with agent mode only.", help)).
- PreAction(func(parseContext *kingpin.ParseContext) error {
+ PreAction(func(*kingpin.ParseContext) error {
// This will be invoked only if flag is actually provided by user.
agentOnlyFlags = append(agentOnlyFlags, "--"+name)
return nil
@@ -135,141 +184,190 @@ func agentOnlyFlag(app *kingpin.Application, name, help string) *kingpin.FlagCla
type flagConfig struct {
configFile string
- agentStoragePath string
- serverStoragePath string
- notifier notifier.Options
- forGracePeriod model.Duration
- outageTolerance model.Duration
- resendDelay model.Duration
- maxConcurrentEvals int64
- web web.Options
- scrape scrape.Options
- tsdb tsdbOptions
- agent agentOptions
- lookbackDelta model.Duration
- webTimeout model.Duration
- queryTimeout model.Duration
- queryConcurrency int
- queryMaxSamples int
- RemoteFlushDeadline model.Duration
- nameEscapingScheme string
-
- featureList []string
- memlimitRatio float64
+ agentStoragePath string
+ serverStoragePath string
+ notifier notifier.Options
+ forGracePeriod model.Duration
+ outageTolerance model.Duration
+ resendDelay model.Duration
+ maxConcurrentEvals int64
+ web web.Options
+ scrape scrape.Options
+ tsdb tsdbOptions
+ agent agentOptions
+ lookbackDelta model.Duration
+ webTimeout model.Duration
+ queryTimeout model.Duration
+ queryConcurrency int
+ queryMaxSamples int
+ RemoteFlushDeadline model.Duration
+ maxNotificationsSubscribers int
+
+ enableAutoReload bool
+ autoReloadInterval model.Duration
+
+ maxprocsEnable bool
+ memlimitEnable bool
+ memlimitRatio float64
+
+ featureList []string
// These options are extracted from featureList
// for ease of use.
- enableExpandExternalLabels bool
- enableNewSDManager bool
- enablePerStepStats bool
- enableAutoGOMAXPROCS bool
- enableAutoGOMEMLIMIT bool
- enableConcurrentRuleEval bool
+ enablePerStepStats bool
+ enableConcurrentRuleEval bool
+ useStartTimestamps bool
prometheusURL string
corsRegexString string
- promlogConfig promlog.Config
-
promqlEnableDelayedNameRemoval bool
+
+ parserOpts parser.Options
+
+ promslogConfig promslog.Config
}
// setFeatureListOptions sets the corresponding options from the featureList.
-func (c *flagConfig) setFeatureListOptions(logger log.Logger) error {
+func (c *flagConfig) setFeatureListOptions(logger *slog.Logger) error {
for _, f := range c.featureList {
- opts := strings.Split(f, ",")
- for _, o := range opts {
+ for o := range strings.SplitSeq(f, ",") {
switch o {
- case "remote-write-receiver":
- c.web.EnableRemoteWriteReceiver = true
- level.Warn(logger).Log("msg", "Remote write receiver enabled via feature flag remote-write-receiver. This is DEPRECATED. Use --web.enable-remote-write-receiver.")
- case "otlp-write-receiver":
- c.web.EnableOTLPWriteReceiver = true
- level.Info(logger).Log("msg", "Experimental OTLP write receiver enabled")
- case "expand-external-labels":
- c.enableExpandExternalLabels = true
- level.Info(logger).Log("msg", "Experimental expand-external-labels enabled")
case "exemplar-storage":
c.tsdb.EnableExemplarStorage = true
- level.Info(logger).Log("msg", "Experimental in-memory exemplar storage enabled")
+ logger.Info("Experimental in-memory exemplar storage enabled")
case "memory-snapshot-on-shutdown":
c.tsdb.EnableMemorySnapshotOnShutdown = true
- level.Info(logger).Log("msg", "Experimental memory snapshot on shutdown enabled")
+ logger.Info("Experimental memory snapshot on shutdown enabled")
case "extra-scrape-metrics":
- c.scrape.ExtraMetrics = true
- level.Info(logger).Log("msg", "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
- level.Info(logger).Log("msg", "Experimental metadata records in WAL enabled, required for remote write 2.0")
- case "new-service-discovery-manager":
- c.enableNewSDManager = true
- level.Info(logger).Log("msg", "Experimental service discovery manager")
- case "agent":
- agentMode = true
- level.Info(logger).Log("msg", "Experimental agent mode enabled.")
+ 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
- level.Info(logger).Log("msg", "Experimental per-step statistics reporting")
- case "auto-gomaxprocs":
- c.enableAutoGOMAXPROCS = true
- level.Info(logger).Log("msg", "Automatically set GOMAXPROCS to match Linux container CPU quota")
- case "auto-gomemlimit":
- c.enableAutoGOMEMLIMIT = true
- level.Info(logger).Log("msg", "Automatically set GOMEMLIMIT to match Linux container or system memory limit")
+ logger.Info("Experimental per-step statistics reporting")
+ case "auto-reload-config":
+ c.enableAutoReload = true
+ logger.Warn("This option for --enable-feature is deprecated. Use --config.auto-reload instead.", "option", o)
case "concurrent-rule-eval":
c.enableConcurrentRuleEval = true
- level.Info(logger).Log("msg", "Experimental concurrent rule evaluation enabled.")
- case "no-default-scrape-port":
- c.scrape.NoDefaultPort = true
- level.Info(logger).Log("msg", "No default port will be appended to scrape targets' addresses.")
+ logger.Info("Experimental concurrent rule evaluation enabled.")
case "promql-experimental-functions":
- parser.EnableExperimentalFunctions = true
- level.Info(logger).Log("msg", "Experimental PromQL functions enabled.")
+ c.parserOpts.EnableExperimentalFunctions = true
+ logger.Info("Experimental PromQL functions enabled.")
+ case "promql-duration-expr":
+ c.parserOpts.ExperimentalDurationExpr = true
+ logger.Info("Experimental duration expression parsing enabled.")
case "native-histograms":
- c.tsdb.EnableNativeHistograms = true
- c.scrape.EnableNativeHistogramsIngestion = true
+ 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":
+ // NOTE(bwplotka): Once AppendableV1 is removed, there will be only the TSDB and agent flags.
+ c.scrape.EnableStartTimestampZeroIngestion = true
+ c.scrape.ParseST = 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
- level.Info(logger).Log("msg", "Experimental native histogram support enabled. Changed default scrape_protocols to prefer PrometheusProto format.", "global.scrape_protocols", fmt.Sprintf("%v", config.DefaultGlobalConfig.ScrapeProtocols))
- case "created-timestamp-zero-ingestion":
- c.scrape.EnableCreatedTimestampZeroIngestion = true
- // Change relevant global variables. Hacky, but it's hard to pass a new option or default to unmarshallers.
+ logger.Info("Experimental start timestamp zero ingestion enabled. OpenMetrics 1.0 parsing will parse _created metrics as ST instead of normal sample. Changed default scrape_protocols to prefer PrometheusProto format.", "global.scrape_protocols", fmt.Sprintf("%v", config.DefaultGlobalConfig.ScrapeProtocols))
+ case "xor2-encoding":
+ c.tsdb.FloatChunkEncoding = chunkenc.EncXOR2
+ logger.Info("Experimental XOR2 chunk encoding enabled.")
+ case "st-synthesis":
+ // TODO(ridwanmsharif): Move this to scrape configuration once stable.
+ c.scrape.SynthesizeST = true
+ features.Enable(features.Scrape, "st-synthesis")
+ logger.Info("Experimental start timestamp synthesis enabled.")
+ case "st-storage":
+ c.scrape.ParseST = true
+ 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
- level.Info(logger).Log("msg", "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 storage enabled. OpenMetrics 1.0 parsing will parse _created metrics as ST instead of normal sample. Changed default scrape_protocols to prefer PrometheusProto format.", "global.scrape_protocols", fmt.Sprintf("%v", config.DefaultGlobalConfig.ScrapeProtocols))
+ case "use-start-timestamps":
+ c.useStartTimestamps = true
+ logger.Info("Experimental usage of start timestamps in PromQL engine is enabled.")
case "delayed-compaction":
c.tsdb.EnableDelayedCompaction = true
- level.Info(logger).Log("msg", "Experimental delayed compaction is enabled.")
+ logger.Info("Experimental delayed compaction is enabled.")
case "promql-delayed-name-removal":
c.promqlEnableDelayedNameRemoval = true
- level.Info(logger).Log("msg", "Experimental PromQL delayed name removal enabled.")
- case "utf8-names":
- model.NameValidationScheme = model.UTF8Validation
- level.Info(logger).Log("msg", "Experimental UTF-8 support enabled")
- case "":
- continue
- case "promql-at-modifier", "promql-negative-offset":
- level.Warn(logger).Log("msg", "This option for --enable-feature is now permanently enabled and therefore a no-op.", "option", o)
+ logger.Info("Experimental PromQL delayed name removal enabled.")
+ case "promql-extended-range-selectors":
+ 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 "old-ui":
+ c.web.UseOldUI = true
+ logger.Info("Serving previous version of the Prometheus web UI.")
+ case "otlp-deltatocumulative":
+ c.web.ConvertOTLPDelta = true
+ logger.Info("Converting delta OTLP metrics to cumulative")
+ case "otlp-native-delta-ingestion":
+ // Experimental OTLP native delta ingestion.
+ // This currently just stores the raw delta value as-is with unknown metric type. Better typing and
+ // type-aware functions may come later.
+ // See proposal: https://github.com/prometheus/proposals/pull/48
+ c.web.NativeOTLPDeltaIngestion = true
+ logger.Info("Enabling native ingestion of delta OTLP metrics, storing the raw sample values without conversion. WARNING: Delta support is in an early stage of development. The ingestion and querying process is likely to change over time.")
+ case "type-and-unit-labels":
+ c.scrape.EnableTypeAndUnitLabels = true
+ c.web.EnableTypeAndUnitLabels = true
+ logger.Info("Experimental type and unit labels enabled")
+ case "use-uncached-io":
+ if !fileutil.UncachedIOSupported() {
+ return errors.New("experimental Uncached IO is not supported")
+ }
+ c.tsdb.UseUncachedIO = true
+ logger.Info("Experimental Uncached IO is enabled.")
+ case "fast-startup":
+ c.tsdb.EnableFastStartup = true
+ logger.Info("Experimental fast startup is enabled.")
+ case "search-api":
+ c.web.EnableSearch = true
+ logger.Info("Experimental search API enabled.")
default:
- level.Warn(logger).Log("msg", "Unknown option for --enable-feature", "option", o)
+ logger.Warn("Unknown option for --enable-feature", "option", o)
}
}
}
+ if c.web.ConvertOTLPDelta && c.web.NativeOTLPDeltaIngestion {
+ return errors.New("cannot enable otlp-deltatocumulative and otlp-native-delta-ingestion features at the same time")
+ }
+
return nil
}
+// parseCompressionType parses the two compression-related configuration values and returns the CompressionType.
+func parseCompressionType(compress bool, compressType compression.Type) compression.Type {
+ if compress {
+ return compressType
+ }
+ return compression.None
+}
+
func main() {
if os.Getenv("DEBUG") != "" {
runtime.SetBlockProfileRate(20)
runtime.SetMutexProfileFraction(20)
}
- var (
- oldFlagRetentionDuration model.Duration
- newFlagRetentionDuration model.Duration
- )
-
// Unregister the default GoCollector, and reregister with our defaults.
if prometheus.Unregister(collectors.NewGoCollector()) {
prometheus.MustRegister(
@@ -277,6 +375,7 @@ func main() {
collectors.WithGoCollectorRuntimeMetrics(
collectors.MetricsGC,
collectors.MetricsScheduler,
+ collectors.GoRuntimeMetricsRule{Matcher: goregexp.MustCompile(`^/sync/mutex/wait/total:seconds$`)},
),
),
)
@@ -287,10 +386,18 @@ 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,
+ },
+ tsdb: tsdbOptions{
+ // Default to XOR encoding; xor2-encoding feature flag overrides this to EncXOR2.
+ FloatChunkEncoding: chunkenc.EncXOR,
},
- promlogConfig: promlog.Config{},
}
a := kingpin.New(filepath.Base(os.Args[0]), "The Prometheus monitoring server").UsageWriter(os.Stdout)
@@ -302,9 +409,19 @@ func main() {
a.Flag("config.file", "Prometheus configuration file path.").
Default("prometheus.yml").StringVar(&cfg.configFile)
+ a.Flag("config.auto-reload", "Enable automatic configuration file reloading. See also --config.auto-reload-interval.").
+ Default("false").BoolVar(&cfg.enableAutoReload)
+
+ a.Flag("config.auto-reload-interval", "Specifies the interval for checking and automatically reloading the Prometheus configuration file upon detecting changes. Only used when --config.auto-reload is set.").
+ Default("30s").SetValue(&cfg.autoReloadInterval)
+
a.Flag("web.listen-address", "Address to listen on for UI, API, and telemetry. Can be repeated.").
Default("0.0.0.0:9090").StringsVar(&cfg.web.ListenAddresses)
+ a.Flag("auto-gomaxprocs", "Automatically set GOMAXPROCS to match Linux container CPU quota").
+ Default("true").BoolVar(&cfg.maxprocsEnable)
+ a.Flag("auto-gomemlimit", "Automatically set GOMEMLIMIT to match Linux container or system memory limit").
+ Default("true").BoolVar(&cfg.memlimitEnable)
a.Flag("auto-gomemlimit.ratio", "The ratio of reserved GOMEMLIMIT memory to the detected maximum container or system memory").
Default("0.9").FloatVar(&cfg.memlimitRatio)
@@ -320,6 +437,9 @@ func main() {
a.Flag("web.max-connections", "Maximum number of simultaneous connections across all listeners.").
Default("512").IntVar(&cfg.web.MaxConnections)
+ a.Flag("web.max-notifications-subscribers", "Limits the maximum number of subscribers that can concurrently receive live notifications. If the limit is reached, new subscription requests will be denied until existing connections close.").
+ Default("16").IntVar(&cfg.maxNotificationsSubscribers)
+
a.Flag("web.external-url",
"The URL under which Prometheus is externally reachable (for example, if Prometheus is served via a reverse proxy). Used for generating relative and absolute links back to Prometheus itself. If the URL has a path portion, it will be used to prefix all HTTP endpoints served by Prometheus. If omitted, relevant URL components will be derived automatically.").
PlaceHolder("").StringVar(&cfg.prometheusURL)
@@ -342,10 +462,13 @@ func main() {
a.Flag("web.enable-remote-write-receiver", "Enable API endpoint accepting remote write requests.").
Default("false").BoolVar(&cfg.web.EnableRemoteWriteReceiver)
- supportedRemoteWriteProtoMsgs := config.RemoteWriteProtoMsgs{config.RemoteWriteProtoMsgV1, config.RemoteWriteProtoMsgV2}
+ supportedRemoteWriteProtoMsgs := remoteapi.MessageTypes{remoteapi.WriteV1MessageType, remoteapi.WriteV2MessageType}
a.Flag("web.remote-write-receiver.accepted-protobuf-messages", fmt.Sprintf("List of the remote write protobuf messages to accept when receiving the remote writes. Supported values: %v", supportedRemoteWriteProtoMsgs.String())).
Default(supportedRemoteWriteProtoMsgs.Strings()...).SetValue(rwProtoMsgFlagValue(&cfg.web.AcceptRemoteWriteProtoMsgs))
+ a.Flag("web.enable-otlp-receiver", "Enable API endpoint accepting OTLP write requests.").
+ Default("false").BoolVar(&cfg.web.EnableOTLPWriteReceiver)
+
a.Flag("web.console.templates", "Path to the console template directory, available at /consoles.").
Default("consoles").StringVar(&cfg.web.ConsoleTemplatesPath)
@@ -376,31 +499,28 @@ func main() {
"Size at which to split the tsdb WAL segment files. Example: 100MB").
Hidden().PlaceHolder("").BytesVar(&cfg.tsdb.WALSegmentSize)
- serverOnlyFlag(a, "storage.tsdb.retention", "[DEPRECATED] How long to retain samples in storage. This flag has been deprecated, use \"storage.tsdb.retention.time\" instead.").
- SetValue(&oldFlagRetentionDuration)
-
- serverOnlyFlag(a, "storage.tsdb.retention.time", "How long to retain samples in storage. When this flag is set it overrides \"storage.tsdb.retention\". If neither this flag nor \"storage.tsdb.retention\" nor \"storage.tsdb.retention.size\" is set, the retention time defaults to "+defaultRetentionString+". Units Supported: y, w, d, h, m, s, ms.").
- SetValue(&newFlagRetentionDuration)
+ serverOnlyFlag(a, "storage.tsdb.retention.time", "[DEPRECATED] How long to retain samples in storage. If neither this flag nor \"storage.tsdb.retention.size\" is set, the retention time defaults to "+defaultRetentionString+". Units Supported: y, w, d, h, m, s, ms. This flag has been deprecated, use the storage.tsdb.retention.time field in the config file instead.").
+ SetValue(&cfg.tsdb.RetentionDuration)
- serverOnlyFlag(a, "storage.tsdb.retention.size", "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.").
+ serverOnlyFlag(a, "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.").
BytesVar(&cfg.tsdb.MaxBytes)
serverOnlyFlag(a, "storage.tsdb.no-lockfile", "Do not create lockfile in data directory.").
Default("false").BoolVar(&cfg.tsdb.NoLockfile)
- // TODO: Remove in Prometheus 3.0.
- var b bool
- serverOnlyFlag(a, "storage.tsdb.allow-overlapping-blocks", "[DEPRECATED] This flag has no effect. Overlapping blocks are enabled by default now.").
- Default("true").Hidden().BoolVar(&b)
-
serverOnlyFlag(a, "storage.tsdb.allow-overlapping-compaction", "Allow compaction of overlapping blocks. If set to false, TSDB stops vertical compaction and leaves overlapping blocks there. The use case is to let another component handle the compaction of overlapping blocks.").
Default("true").Hidden().BoolVar(&cfg.tsdb.EnableOverlappingCompaction)
- serverOnlyFlag(a, "storage.tsdb.wal-compression", "Compress the tsdb WAL.").
- Hidden().Default("true").BoolVar(&cfg.tsdb.WALCompression)
+ var (
+ 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)
- serverOnlyFlag(a, "storage.tsdb.wal-compression-type", "Compression algorithm for the tsdb WAL.").
- Hidden().Default(string(wlog.CompressionSnappy)).EnumVar(&cfg.tsdb.WALCompressionType, string(wlog.CompressionSnappy), string(wlog.CompressionZstd))
+ serverOnlyFlag(a, "storage.tsdb.wal-compression-type", "Compression algorithm for the tsdb WAL, used when --storage.tsdb.wal-compression is true.").
+ Hidden().Default(compression.Snappy).EnumVar(&tsdbWALCompressionType, compression.Snappy, compression.Zstd)
serverOnlyFlag(a, "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.").
Default("0").IntVar(&cfg.tsdb.HeadChunksWriteQueueSize)
@@ -408,6 +528,15 @@ func main() {
serverOnlyFlag(a, "storage.tsdb.samples-per-chunk", "Target number of samples per chunk.").
Default("120").Hidden().IntVar(&cfg.tsdb.SamplesPerChunk)
+ 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)
@@ -415,23 +544,37 @@ func main() {
"Size at which to split WAL segment files. Example: 100MB").
Hidden().PlaceHolder("").BytesVar(&cfg.agent.WALSegmentSize)
- agentOnlyFlag(a, "storage.agent.wal-compression", "Compress the agent WAL.").
- Default("true").BoolVar(&cfg.agent.WALCompression)
+ var (
+ agentWALCompression bool
+ agentWALCompressionType string
+ )
+ agentOnlyFlag(a, "storage.agent.wal-compression", "Compress the agent WAL. If false, the --storage.agent.wal-compression-type flag is ignored.").
+ Default("true").BoolVar(&agentWALCompression)
- agentOnlyFlag(a, "storage.agent.wal-compression-type", "Compression algorithm for the agent WAL.").
- Hidden().Default(string(wlog.CompressionSnappy)).EnumVar(&cfg.agent.WALCompressionType, string(wlog.CompressionSnappy), string(wlog.CompressionZstd))
+ agentOnlyFlag(a, "storage.agent.wal-compression-type", "Compression algorithm for the agent WAL, used when --storage.agent.wal-compression is true.").
+ Hidden().Default(compression.Snappy).EnumVar(&agentWALCompressionType, compression.Snappy, compression.Zstd)
agentOnlyFlag(a, "storage.agent.wal-truncate-frequency",
"The frequency at which to truncate the WAL and remove old data.").
Hidden().PlaceHolder("").SetValue(&cfg.agent.TruncateFrequency)
+ // Dynamically calculate the format strings from the tsdb/agent constants
+ agentDefaultMinWALTime := model.Duration(agent.DefaultMinWALTime * int64(time.Millisecond)).String()
+ agentDefaultMaxWALTime := model.Duration(agent.DefaultMaxWALTime * int64(time.Millisecond)).String()
+
agentOnlyFlag(a, "storage.agent.retention.min-time",
"Minimum age samples may be before being considered for deletion when the WAL is truncated").
- SetValue(&cfg.agent.MinWALTime)
+ Default(agentDefaultMinWALTime).SetValue(&cfg.agent.MinWALTime)
agentOnlyFlag(a, "storage.agent.retention.max-time",
"Maximum age samples may be before being forcibly deleted when the WAL is truncated").
- SetValue(&cfg.agent.MaxWALTime)
+ Default(agentDefaultMaxWALTime).SetValue(&cfg.agent.MaxWALTime)
+
+ agentOnlyFlag(a, "storage.agent.checkpoint-from-in-memory-series", "Use only in-memory series data when building a checkpoint.").
+ Default("false").BoolVar(&cfg.agent.CheckpointFromInMemorySeries)
+
+ agentOnlyFlag(a, "storage.agent.checkpoint-batch-size", "Size of a single WAL log entry chunk to be flushed. Has no effect without --storage.agent.checkpoint-from-in-memory-series flag.").
+ Default("1000").IntVar(&cfg.agent.CheckpointBatchSize)
agentOnlyFlag(a, "storage.agent.no-lockfile", "Do not create lockfile in data directory.").
Default("false").BoolVar(&cfg.agent.NoLockfile)
@@ -448,6 +591,9 @@ func main() {
serverOnlyFlag(a, "storage.remote.read-max-bytes-in-frame", "Maximum number of bytes in a single frame for streaming remote read response types before marshalling. Note that client might have limit on frame size as well. 1MB as recommended by protobuf by default.").
Default("1048576").IntVar(&cfg.web.RemoteReadBytesInFrame)
+ serverOnlyFlag(a, "web.search.max-limit", "Hard upper bound on the \"limit\" query parameter accepted by the experimental search API (--enable-feature=search-api). Requests with a higher limit are rejected with HTTP 400. 0 disables the cap.").
+ Default("10000").IntVar(&cfg.web.MaxSearchLimit)
+
serverOnlyFlag(a, "rules.alert.for-outage-tolerance", "Max time to tolerate prometheus outage for restoring \"for\" state of alert.").
Default("1h").SetValue(&cfg.outageTolerance)
@@ -469,12 +615,12 @@ func main() {
serverOnlyFlag(a, "alertmanager.notification-queue-capacity", "The capacity of the queue for pending Alertmanager notifications.").
Default("10000").IntVar(&cfg.notifier.QueueCapacity)
+ serverOnlyFlag(a, "alertmanager.notification-batch-size", "The maximum number of notifications per batch to send to the Alertmanager.").
+ Default(strconv.Itoa(notifier.DefaultMaxBatchSize)).IntVar(&cfg.notifier.MaxBatchSize)
+
serverOnlyFlag(a, "alertmanager.drain-notification-queue-on-shutdown", "Send any outstanding Alertmanager notifications when shutting down. If false, any outstanding Alertmanager notifications will be dropped when shutting down.").
Default("true").BoolVar(&cfg.notifier.DrainOnShutdown)
- // TODO: Remove in Prometheus 3.0.
- alertmanagerTimeout := a.Flag("alertmanager.timeout", "[DEPRECATED] This flag has no effect.").Hidden().String()
-
serverOnlyFlag(a, "query.lookback-delta", "The maximum lookback duration for retrieving metrics during expression evaluations and federation.").
Default("5m").SetValue(&cfg.lookbackDelta)
@@ -490,14 +636,14 @@ 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("scrape.name-escaping-scheme", `Method for escaping legacy invalid names when sending to Prometheus that does not support UTF-8. Can be one of "values", "underscores", or "dots".`).Default(scrape.DefaultNameEscapingScheme.String()).StringVar(&cfg.nameEscapingScheme)
+ a.Flag("enable-feature", "Comma separated feature names to enable. Valid options: concurrent-rule-eval, created-timestamp-zero-ingestion, delayed-compaction, exemplar-storage, extra-scrape-metrics, memory-snapshot-on-shutdown, metadata-wal-records, old-ui, otlp-deltatocumulative, otlp-native-delta-ingestion, promql-binop-fill-modifiers, promql-delayed-name-removal, promql-duration-expr, promql-experimental-functions, promql-extended-range-selectors, promql-per-step-stats, search-api, st-storage, st-synthesis, type-and-unit-labels, use-start-timestamps, use-uncached-io, xor2-encoding. See https://prometheus.io/docs/prometheus/latest/feature_flags/ for more details.").
+ StringsVar(&cfg.featureList)
- a.Flag("enable-feature", "Comma separated feature names to enable. Valid options: agent, auto-gomaxprocs, auto-gomemlimit, concurrent-rule-eval, created-timestamp-zero-ingestion, delayed-compaction, exemplar-storage, expand-external-labels, extra-scrape-metrics, memory-snapshot-on-shutdown, native-histograms, new-service-discovery-manager, no-default-scrape-port, otlp-write-receiver, promql-experimental-functions, promql-delayed-name-removal, promql-per-step-stats, remote-write-receiver (DEPRECATED), utf8-names. 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)
- promlogflag.AddFlags(a, &cfg.promlogConfig)
+ promslogflag.AddFlags(a, &cfg.promslogConfig)
- a.Flag("write-documentation", "Generate command line documentation. Internal use.").Hidden().Action(func(ctx *kingpin.ParseContext) error {
+ a.Flag("write-documentation", "Generate command line documentation. Internal use.").Hidden().Action(func(*kingpin.ParseContext) error {
if err := documentcli.GenerateMarkdown(a.Model(), os.Stdout); err != nil {
os.Exit(1)
return err
@@ -508,27 +654,38 @@ func main() {
_, err := a.Parse(os.Args[1:])
if err != nil {
- fmt.Fprintln(os.Stderr, fmt.Errorf("Error parsing command line arguments: %w", err))
+ fmt.Fprintf(os.Stderr, "Error parsing command line arguments: %s\n", err)
a.Usage(os.Args[1:])
os.Exit(2)
}
- logger := promlog.New(&cfg.promlogConfig)
+ logger := promslog.New(&cfg.promslogConfig)
+ slog.SetDefault(logger)
+
+ notifs := notifications.NewNotifications(cfg.maxNotificationsSubscribers, prometheus.DefaultRegisterer)
+ cfg.web.NotificationsSub = notifs.Sub
+ cfg.web.NotificationsGetter = notifs.Get
+ notifs.AddNotification(notifications.StartingUp)
if err := cfg.setFeatureListOptions(logger); err != nil {
- fmt.Fprintln(os.Stderr, fmt.Errorf("Error parsing feature list: %w", err))
+ fmt.Fprintf(os.Stderr, "Error parsing feature list: %s\n", err)
os.Exit(1)
}
- if cfg.nameEscapingScheme != "" {
- scheme, err := model.ToEscapingScheme(cfg.nameEscapingScheme)
- if err != nil {
- fmt.Fprintf(os.Stderr, `Invalid name escaping scheme: %q; Needs to be one of "values", "underscores", or "dots"`, cfg.nameEscapingScheme)
- os.Exit(1)
+ if cfg.enableAutoReload {
+ if s := time.Duration(cfg.autoReloadInterval).Seconds(); s < 1 {
+ cfg.autoReloadInterval, _ = model.ParseDuration("1s")
}
- model.NameEscapingScheme = scheme
+ logger.Info("Automatic configuration file reloading enabled", "interval", cfg.autoReloadInterval)
}
+ if cfg.web.MaxSearchLimit < 0 {
+ fmt.Fprintf(os.Stderr, "--web.search.max-limit must be non-negative; got %d (use 0 to disable the cap)\n", cfg.web.MaxSearchLimit)
+ 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)
@@ -539,6 +696,11 @@ func main() {
os.Exit(3)
}
+ if agentMode && cfg.agent.CheckpointBatchSize <= 0 {
+ fmt.Fprintln(os.Stderr, "--storage.agent.checkpoint-batch-size must be greater than 0.")
+ os.Exit(1)
+ }
+
if cfg.memlimitRatio <= 0.0 || cfg.memlimitRatio > 1.0 {
fmt.Fprintf(os.Stderr, "--auto-gomemlimit.ratio must be greater than 0 and less than or equal to 1.")
os.Exit(1)
@@ -561,36 +723,95 @@ func main() {
os.Exit(2)
}
- if *alertmanagerTimeout != "" {
- level.Warn(logger).Log("msg", "The flag --alertmanager.timeout has no effect and will be removed in the future.")
+ // Set TSDB retention defaults from CLI flags before any config file is loaded.
+ // This makes CLI flags act as the default when no retention section is present.
+ cliRetentionDuration := cfg.tsdb.RetentionDuration
+ cliMaxBytes := cfg.tsdb.MaxBytes
+ if cliRetentionDuration == 0 && cliMaxBytes == 0 {
+ cliRetentionDuration = defaultRetentionDuration
+ }
+ config.DefaultTSDBRetentionConfig = config.TSDBRetentionConfig{
+ Time: cliRetentionDuration,
+ Size: cliMaxBytes,
}
// Throw error for invalid config before starting other components.
var cfgFile *config.Config
- if cfgFile, err = config.LoadFile(cfg.configFile, agentMode, false, log.NewNopLogger()); err != nil {
+ if cfgFile, err = config.LoadFile(cfg.configFile, agentMode, promslog.NewNopLogger()); err != nil {
absPath, pathErr := filepath.Abs(cfg.configFile)
if pathErr != nil {
absPath = cfg.configFile
}
- level.Error(logger).Log("msg", fmt.Sprintf("Error loading config (--config.file=%s)", cfg.configFile), "file", absPath, "err", err)
+ logger.Error(fmt.Sprintf("Error loading config (--config.file=%s)", cfg.configFile), "file", absPath, "err", err)
os.Exit(2)
}
+ // Get scrape configs to validate dynamically loaded scrape_config_files.
+ // They can change over time, but do the extra validation on startup for better experience.
if _, err := cfgFile.GetScrapeConfigs(); err != nil {
absPath, pathErr := filepath.Abs(cfg.configFile)
if pathErr != nil {
absPath = cfg.configFile
}
- level.Error(logger).Log("msg", fmt.Sprintf("Error loading scrape config files from config (--config.file=%q)", cfg.configFile), "file", absPath, "err", err)
+ logger.Error(fmt.Sprintf("Error loading dynamic scrape config files from config (--config.file=%q)", cfg.configFile), "file", absPath, "err", err)
os.Exit(2)
}
+
+ // Parse rule files to verify they exist and contain valid rules.
+ if err := rules.ParseFiles(cfgFile.RuleFiles, cfgFile.GlobalConfig.MetricNameValidationScheme, promqlParser, logger); err != nil {
+ absPath, pathErr := filepath.Abs(cfg.configFile)
+ if pathErr != nil {
+ absPath = cfg.configFile
+ }
+ logger.Error(fmt.Sprintf("Error loading rule file patterns from config (--config.file=%q)", cfg.configFile), "file", absPath, "err", err)
+ os.Exit(2)
+ }
+
if cfg.tsdb.EnableExemplarStorage {
if cfgFile.StorageConfig.ExemplarsConfig == nil {
cfgFile.StorageConfig.ExemplarsConfig = &config.DefaultExemplarsConfig
}
cfg.tsdb.MaxExemplars = cfgFile.StorageConfig.ExemplarsConfig.MaxExemplars
}
- if cfgFile.StorageConfig.TSDBConfig != nil {
- cfg.tsdb.OutOfOrderTimeWindow = cfgFile.StorageConfig.TSDBConfig.OutOfOrderTimeWindow
+ 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)
+ }
+ cfg.tsdb.OutOfOrderTimeWindow = cfgFile.StorageConfig.TSDBConfig.OutOfOrderTimeWindow
+ cfg.tsdb.StaleSeriesCompactionThreshold = cfgFile.StorageConfig.TSDBConfig.StaleSeriesCompactionThreshold
+ cfg.tsdb.RetentionDuration = cfgFile.StorageConfig.TSDBConfig.Retention.Time
+ cfg.tsdb.MaxBytes = cfgFile.StorageConfig.TSDBConfig.Retention.Size
+ cfg.tsdb.MaxPercentage = cfgFile.StorageConfig.TSDBConfig.Retention.Percentage
+
+ // Set Go runtime parameters before we get too far into initialization.
+ updateGoGC(cfgFile, logger)
+ if cfg.maxprocsEnable {
+ l := func(format string, a ...any) {
+ logger.Info(fmt.Sprintf(strings.TrimPrefix(format, "maxprocs: "), a...), "component", "automaxprocs")
+ }
+ if _, err := maxprocs.Set(maxprocs.Logger(l)); err != nil {
+ logger.Warn("Failed to set GOMAXPROCS automatically", "component", "automaxprocs", "err", err)
+ }
+ }
+
+ if cfg.memlimitEnable {
+ if _, err := memlimit.SetGoMemLimitWithOpts(
+ memlimit.WithRatio(cfg.memlimitRatio),
+ memlimit.WithProvider(
+ memlimit.ApplyFallback(
+ memlimit.FromCgroup,
+ memlimit.FromSystem,
+ ),
+ ),
+ memlimit.WithLogger(logger.With("component", "automemlimit")),
+ ); err != nil {
+ logger.Warn("automemlimit", "msg", "Failed to set GOMEMLIMIT automatically", "err", err)
+ }
+ }
+
+ 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
@@ -611,22 +832,6 @@ func main() {
cfg.web.RoutePrefix = "/" + strings.Trim(cfg.web.RoutePrefix, "/")
if !agentMode {
- // Time retention settings.
- if oldFlagRetentionDuration != 0 {
- level.Warn(logger).Log("deprecation_notice", "'storage.tsdb.retention' flag is deprecated use 'storage.tsdb.retention.time' instead.")
- cfg.tsdb.RetentionDuration = oldFlagRetentionDuration
- }
-
- // When the new flag is set it takes precedence.
- if newFlagRetentionDuration != 0 {
- cfg.tsdb.RetentionDuration = newFlagRetentionDuration
- }
-
- if cfg.tsdb.RetentionDuration == 0 && cfg.tsdb.MaxBytes == 0 {
- cfg.tsdb.RetentionDuration = defaultRetentionDuration
- level.Info(logger).Log("msg", "No time or size retention was set so using the default time retention", "duration", defaultRetentionDuration)
- }
-
// Check for overflows. This limits our max retention to 100y.
if cfg.tsdb.RetentionDuration < 0 {
y, err := model.ParseDuration("100y")
@@ -634,7 +839,17 @@ func main() {
panic(err)
}
cfg.tsdb.RetentionDuration = y
- level.Warn(logger).Log("msg", "Time retention value is too high. Limiting to: "+y.String())
+ logger.Warn("Time retention value is too high. Limiting to: " + y.String())
+ }
+
+ if cfg.tsdb.MaxPercentage > 0 {
+ if cfg.tsdb.MaxBytes > 0 {
+ logger.Warn("storage.tsdb.retention.size is ignored, because storage.tsdb.retention.percentage is specified")
+ }
+ if storagePathFsSize(localStoragePath) == 0 {
+ fmt.Fprintln(os.Stderr, fmt.Errorf("unable to detect total capacity of metric storage at %s, please disable retention percentage (%g%%)", localStoragePath, cfg.tsdb.MaxPercentage))
+ os.Exit(2)
+ }
}
// Max block size settings.
@@ -650,16 +865,26 @@ func main() {
cfg.tsdb.MaxBlockDuration = maxBlockDuration
}
+
+ // Delayed compaction checks
+ if cfg.tsdb.EnableDelayedCompaction && (cfg.tsdb.CompactionDelayMaxPercent > 100 || cfg.tsdb.CompactionDelayMaxPercent <= 0) {
+ logger.Warn("The --storage.tsdb.delayed-compaction.max-percent should have a value between 1 and 100. Using default", "default", tsdb.DefaultCompactionDelayMaxPercent)
+ cfg.tsdb.CompactionDelayMaxPercent = tsdb.DefaultCompactionDelayMaxPercent
+ }
+
+ cfg.tsdb.WALCompressionType = parseCompressionType(tsdbWALCompression, tsdbWALCompressionType)
+ } else {
+ cfg.agent.WALCompressionType = parseCompressionType(agentWALCompression, agentWALCompressionType)
}
noStepSubqueryInterval := &safePromQLNoStepSubqueryInterval{}
noStepSubqueryInterval.Set(config.DefaultGlobalConfig.EvaluationInterval)
- // Above level 6, the k8s client would log bearer tokens in clear-text.
- klog.ClampLevel(6)
- klog.SetLogger(log.With(logger, "component", "k8s_client_runtime"))
- klogv2.ClampLevel(6)
- klogv2.SetLogger(log.With(logger, "component", "k8s_client_runtime"))
+ klogv2.SetSlogLogger(logger.With("component", "k8s_client_runtime"))
+ klog.SetOutputBySeverity("INFO", klogv1Writer{})
+ // Avoid duplicate API deprecation warnings (e.g., "v1 Endpoints is deprecated in v1.33+...")
+ // that can pollute the logs.
+ rest.SetDefaultWarningHandlerWithContext(logging.NewDedupDeprecationWarningLogger())
modeAppName := "Prometheus Server"
mode := "server"
@@ -668,20 +893,28 @@ func main() {
mode = "agent"
}
- level.Info(logger).Log("msg", "Starting "+modeAppName, "mode", mode, "version", version.Info())
+ logger.Info("Starting "+modeAppName, "mode", mode, "version", version.Info())
if bits.UintSize < 64 {
- level.Warn(logger).Log("msg", "This Prometheus binary has not been compiled for a 64-bit architecture. Due to virtual memory constraints of 32-bit systems, it is highly recommended to switch to a 64-bit binary of Prometheus.", "GOARCH", runtime.GOARCH)
+ logger.Warn("This Prometheus binary has not been compiled for a 64-bit architecture. Due to virtual memory constraints of 32-bit systems, it is highly recommended to switch to a 64-bit binary of Prometheus.", "GOARCH", runtime.GOARCH)
}
- level.Info(logger).Log("build_context", version.BuildContext())
- level.Info(logger).Log("host_details", prom_runtime.Uname())
- level.Info(logger).Log("fd_limits", prom_runtime.FdLimits())
- level.Info(logger).Log("vm_limits", prom_runtime.VMLimits())
+ logger.Info("operational information",
+ "build_context", version.BuildContext(),
+ "host_details", prom_runtime.Uname(),
+ "fd_limits", prom_runtime.FdLimits(),
+ "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{}
- remoteStorage = remote.NewStorage(log.With(logger, "component", "remote"), prometheus.DefaultRegisterer, localStorage.StartTime, localStoragePath, time.Duration(cfg.RemoteFlushDeadline), scraper, cfg.scrape.AppendMetadata)
+ remoteStorage = remote.NewStorage(logger.With("component", "remote"), prometheus.DefaultRegisterer, localStorage.StartTime, localStoragePath, time.Duration(cfg.RemoteFlushDeadline), scraper, cfg.scrape.EnableTypeAndUnitLabels)
fanoutStorage = storage.NewFanout(logger, localStorage, remoteStorage)
)
@@ -689,12 +922,12 @@ func main() {
ctxWeb, cancelWeb = context.WithCancel(context.Background())
ctxRule = context.Background()
- notifierManager = notifier.NewManager(&cfg.notifier, log.With(logger, "component", "notifier"))
+ notifierManager = notifier.NewManager(&cfg.notifier, cfgFile.GlobalConfig.MetricNameValidationScheme, logger.With("component", "notifier"))
ctxScrape, cancelScrape = context.WithCancel(context.Background())
ctxNotify, cancelNotify = context.WithCancel(context.Background())
- discoveryManagerScrape discoveryManager
- discoveryManagerNotify discoveryManager
+ discoveryManagerScrape *discovery.Manager
+ discoveryManagerNotify *discovery.Manager
)
// Kubernetes client metrics are used by Kubernetes SD.
@@ -704,63 +937,37 @@ func main() {
// they are not specific to an SD instance.
err = discovery.RegisterK8sClientMetricsWithPrometheus(prometheus.DefaultRegisterer)
if err != nil {
- level.Error(logger).Log("msg", "failed to register Kubernetes client metrics", "err", err)
+ logger.Error("failed to register Kubernetes client metrics", "err", err)
os.Exit(1)
}
sdMetrics, err := discovery.CreateAndRegisterSDMetrics(prometheus.DefaultRegisterer)
if err != nil {
- level.Error(logger).Log("msg", "failed to register service discovery metrics", "err", err)
+ logger.Error("failed to register service discovery metrics", "err", err)
os.Exit(1)
}
- if cfg.enableNewSDManager {
- {
- discMgr := discovery.NewManager(ctxScrape, log.With(logger, "component", "discovery manager scrape"), prometheus.DefaultRegisterer, sdMetrics, discovery.Name("scrape"))
- if discMgr == nil {
- level.Error(logger).Log("msg", "failed to create a discovery manager scrape")
- os.Exit(1)
- }
- discoveryManagerScrape = discMgr
- }
-
- {
- discMgr := discovery.NewManager(ctxNotify, log.With(logger, "component", "discovery manager notify"), prometheus.DefaultRegisterer, sdMetrics, discovery.Name("notify"))
- if discMgr == nil {
- level.Error(logger).Log("msg", "failed to create a discovery manager notify")
- os.Exit(1)
- }
- discoveryManagerNotify = discMgr
- }
- } else {
- {
- discMgr := legacymanager.NewManager(ctxScrape, log.With(logger, "component", "discovery manager scrape"), prometheus.DefaultRegisterer, sdMetrics, legacymanager.Name("scrape"))
- if discMgr == nil {
- level.Error(logger).Log("msg", "failed to create a discovery manager scrape")
- os.Exit(1)
- }
- discoveryManagerScrape = discMgr
- }
+ 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)
+ }
- {
- discMgr := legacymanager.NewManager(ctxNotify, log.With(logger, "component", "discovery manager notify"), prometheus.DefaultRegisterer, sdMetrics, legacymanager.Name("notify"))
- if discMgr == nil {
- level.Error(logger).Log("msg", "failed to create a discovery manager notify")
- os.Exit(1)
- }
- discoveryManagerNotify = discMgr
- }
+ 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)
}
scrapeManager, err := scrape.NewManager(
&cfg.scrape,
- log.With(logger, "component", "scrape manager"),
- func(s string) (log.Logger, error) { return logging.NewJSONFileLogger(s) },
- fanoutStorage,
+ logger.With("component", "scrape manager"),
+ logging.NewJSONFileLogger,
+ nil, fanoutStorage,
prometheus.DefaultRegisterer,
)
if err != nil {
- level.Error(logger).Log("msg", "failed to create a scrape manager", "err", err)
+ logger.Error("failed to create a scrape manager", "err", err)
os.Exit(1)
}
@@ -771,36 +978,13 @@ func main() {
ruleManager *rules.Manager
)
- if cfg.enableAutoGOMAXPROCS {
- l := func(format string, a ...interface{}) {
- level.Info(logger).Log("component", "automaxprocs", "msg", fmt.Sprintf(strings.TrimPrefix(format, "maxprocs: "), a...))
- }
- if _, err := maxprocs.Set(maxprocs.Logger(l)); err != nil {
- level.Warn(logger).Log("component", "automaxprocs", "msg", "Failed to set GOMAXPROCS automatically", "err", err)
- }
- }
-
- if cfg.enableAutoGOMEMLIMIT {
- if _, err := memlimit.SetGoMemLimitWithOpts(
- memlimit.WithRatio(cfg.memlimitRatio),
- memlimit.WithProvider(
- memlimit.ApplyFallback(
- memlimit.FromCgroup,
- memlimit.FromSystem,
- ),
- ),
- ); err != nil {
- level.Warn(logger).Log("component", "automemlimit", "msg", "Failed to set GOMEMLIMIT automatically", "err", err)
- }
- }
-
if !agentMode {
opts := promql.EngineOpts{
- Logger: log.With(logger, "component", "query engine"),
+ Logger: logger.With("component", "query engine"),
Reg: prometheus.DefaultRegisterer,
MaxSamples: cfg.queryMaxSamples,
Timeout: time.Duration(cfg.queryTimeout),
- ActiveQueryTracker: promql.NewActiveQueryTracker(localStoragePath, cfg.queryConcurrency, log.With(logger, "component", "activeQueryTracker")),
+ ActiveQueryTracker: promql.NewActiveQueryTracker(localStoragePath, cfg.queryConcurrency, logger.With("component", "activeQueryTracker")),
LookbackDelta: time.Duration(cfg.lookbackDelta),
NoStepSubqueryIntervalFn: noStepSubqueryInterval.Get,
// EnableAtModifier and EnableNegativeOffset have to be
@@ -809,11 +993,16 @@ func main() {
EnableNegativeOffset: true,
EnablePerStepStats: cfg.enablePerStepStats,
EnableDelayedNameRemoval: cfg.promqlEnableDelayedNameRemoval,
+ EnableTypeAndUnitLabels: cfg.scrape.EnableTypeAndUnitLabels,
+ UseStartTimestamps: cfg.useStartTimestamps,
+ FeatureRegistry: features.DefaultRegistry,
+ Parser: promqlParser,
}
queryEngine = promql.NewEngine(opts)
ruleManager = rules.NewManager(&rules.ManagerOptions{
+ NameValidationScheme: cfgFile.GlobalConfig.MetricNameValidationScheme,
Appendable: fanoutStorage,
Queryable: localStorage,
QueryFunc: rules.EngineQueryFunc(queryEngine, fanoutStorage),
@@ -821,7 +1010,7 @@ func main() {
Context: ctxRule,
ExternalURL: cfg.web.ExternalURL,
Registerer: prometheus.DefaultRegisterer,
- Logger: log.With(logger, "component", "rule manager"),
+ Logger: logger.With("component", "rule manager"),
OutageTolerance: time.Duration(cfg.outageTolerance),
ForGracePeriod: time.Duration(cfg.forGracePeriod),
ResendDelay: time.Duration(cfg.resendDelay),
@@ -830,6 +1019,8 @@ func main() {
DefaultRuleQueryOffset: func() time.Duration {
return time.Duration(cfgFile.GlobalConfig.RuleQueryOffset)
},
+ FeatureRegistry: features.DefaultRegistry,
+ Parser: promqlParser,
})
}
@@ -838,6 +1029,7 @@ func main() {
cfg.web.Context = ctxWeb
cfg.web.TSDBRetentionDuration = cfg.tsdb.RetentionDuration
cfg.web.TSDBMaxBytes = cfg.tsdb.MaxBytes
+ cfg.web.TSDBMaxPercentage = cfg.tsdb.MaxPercentage
cfg.web.TSDBDir = localStoragePath
cfg.web.LocalStorage = localStorage
cfg.web.Storage = fanoutStorage
@@ -849,6 +1041,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,
@@ -872,7 +1065,7 @@ func main() {
}
// Depends on cfg.web.ScrapeManager so needs to be after cfg.web.ScrapeManager = scrapeManager.
- webHandler := web.New(log.With(logger, "component", "web"), &cfg.web)
+ webHandler := web.New(logger.With("component", "web"), &cfg.web)
// Monitor outgoing connections on default transport with conntrack.
http.DefaultTransport.(*http.Transport).DialContext = conntrack.NewDialContextFunc(
@@ -884,8 +1077,29 @@ func main() {
reloaders := []reloader{
{
- name: "db_storage",
- reloader: localStorage.ApplyConfig,
+ name: "db_storage",
+ reloader: func() func(*config.Config) error {
+ lastTSDBRetention := config.TSDBRetentionConfig{}
+ return func(cfg *config.Config) error {
+ err := localStorage.ApplyConfig(cfg)
+ if err != nil || agentMode || cfg.StorageConfig.TSDBConfig == nil || cfg.StorageConfig.TSDBConfig.Retention == nil {
+ return err
+ }
+
+ curr := cfg.StorageConfig.TSDBConfig.Retention
+ if *curr == lastTSDBRetention {
+ return nil
+ }
+
+ logger.Info("TSDB retention updated",
+ "duration", curr.Time,
+ "size", curr.Size,
+ "percentage", curr.Percentage,
+ )
+ lastTSDBRetention = *curr
+ return nil
+ }
+ }(),
}, {
name: "remote_storage",
reloader: remoteStorage.ApplyConfig,
@@ -999,19 +1213,13 @@ func main() {
listeners, err := webHandler.Listeners()
if err != nil {
- level.Error(logger).Log("msg", "Unable to start web listeners", "err", err)
- if err := queryEngine.Close(); err != nil {
- level.Warn(logger).Log("msg", "Closing query engine failed", "err", err)
- }
+ logger.Error("Unable to start web listener", "err", err)
os.Exit(1)
}
err = toolkit_web.Validate(*webConfig)
if err != nil {
- level.Error(logger).Log("msg", "Unable to validate web configuration file", "err", err)
- if err := queryEngine.Close(); err != nil {
- level.Warn(logger).Log("msg", "Closing query engine failed", "err", err)
- }
+ logger.Error("Unable to validate web configuration file", "err", err)
os.Exit(1)
}
@@ -1026,21 +1234,19 @@ func main() {
// Don't forget to release the reloadReady channel so that waiting blocks can exit normally.
select {
case sig := <-term:
- level.Warn(logger).Log("msg", "Received an OS signal, exiting gracefully...", "signal", sig.String())
+ logger.Warn("Received an OS signal, exiting gracefully...", "signal", sig.String())
reloadReady.Close()
case <-webHandler.Quit():
- level.Warn(logger).Log("msg", "Received termination request via web service, exiting gracefully...")
+ logger.Warn("Received termination request via web service, exiting gracefully...")
case <-cancel:
reloadReady.Close()
}
- if err := queryEngine.Close(); err != nil {
- level.Warn(logger).Log("msg", "Closing query engine failed", "err", err)
- }
return nil
},
- func(err error) {
+ func(error) {
close(cancel)
- webHandler.SetReady(false)
+ webHandler.SetReady(web.Stopping)
+ notifs.AddNotification(notifications.ShuttingDown)
},
)
}
@@ -1049,11 +1255,11 @@ func main() {
g.Add(
func() error {
err := discoveryManagerScrape.Run()
- level.Info(logger).Log("msg", "Scrape discovery manager stopped")
+ logger.Info("Scrape discovery manager stopped")
return err
},
- func(err error) {
- level.Info(logger).Log("msg", "Stopping scrape discovery manager...")
+ func(error) {
+ logger.Info("Stopping scrape discovery manager...")
cancelScrape()
},
)
@@ -1063,11 +1269,11 @@ func main() {
g.Add(
func() error {
err := discoveryManagerNotify.Run()
- level.Info(logger).Log("msg", "Notify discovery manager stopped")
+ logger.Info("Notify discovery manager stopped")
return err
},
- func(err error) {
- level.Info(logger).Log("msg", "Stopping notify discovery manager...")
+ func(error) {
+ logger.Info("Stopping notify discovery manager...")
cancelNotify()
},
)
@@ -1078,9 +1284,11 @@ func main() {
func() error {
<-reloadReady.C
ruleManager.Run()
+ logger.Info("Rule manager stopped")
return nil
},
- func(err error) {
+ func(error) {
+ logger.Info("Stopping rule manager manager...")
ruleManager.Stop()
},
)
@@ -1096,15 +1304,15 @@ func main() {
<-reloadReady.C
err := scrapeManager.Run(discoveryManagerScrape.SyncCh())
- level.Info(logger).Log("msg", "Scrape manager stopped")
+ logger.Info("Scrape manager stopped")
return err
},
- func(err error) {
+ func(error) {
// Scrape manager needs to be stopped before closing the local TSDB
// so that it doesn't try to write samples to a closed storage.
// We should also wait for rule manager to be fully stopped to ensure
// we don't trigger any false positive alerts for rules using absent().
- level.Info(logger).Log("msg", "Stopping scrape manager...")
+ logger.Info("Stopping scrape manager...")
scrapeManager.Stop()
},
)
@@ -1115,9 +1323,11 @@ func main() {
func() error {
<-reloadReady.C
tracingManager.Run()
+ logger.Info("Tracing manager stopped")
return nil
},
- func(err error) {
+ func(error) {
+ logger.Info("Stopping tracing manager...")
tracingManager.Stop()
},
)
@@ -1130,6 +1340,23 @@ func main() {
hup := make(chan os.Signal, 1)
signal.Notify(hup, syscall.SIGHUP)
cancel := make(chan struct{})
+
+ var checksum string
+ if cfg.enableAutoReload {
+ checksum, err = config.GenerateChecksum(cfg.configFile)
+ if err != nil {
+ logger.Error("Failed to generate initial checksum for configuration file", "err", err)
+ }
+ }
+
+ callback := func(success bool) {
+ if success {
+ notifs.DeleteNotification(notifications.ConfigurationUnsuccessful)
+ return
+ }
+ notifs.AddNotification(notifications.ConfigurationUnsuccessful)
+ }
+
g.Add(
func() error {
<-reloadReady.C
@@ -1137,24 +1364,55 @@ func main() {
for {
select {
case <-hup:
- if err := reloadConfig(cfg.configFile, cfg.enableExpandExternalLabels, cfg.tsdb.EnableExemplarStorage, logger, noStepSubqueryInterval, reloaders...); err != nil {
- level.Error(logger).Log("msg", "Error reloading config", "err", err)
+ if err := reloadConfig(cfg.configFile, cfg.tsdb.EnableExemplarStorage, logger, noStepSubqueryInterval, callback, reloaders...); err != nil {
+ logger.Error("Error reloading config", "err", err)
+ } else if cfg.enableAutoReload {
+ checksum, err = config.GenerateChecksum(cfg.configFile)
+ if err != nil {
+ logger.Error("Failed to generate checksum during configuration reload", "err", err)
+ }
}
case rc := <-webHandler.Reload():
- if err := reloadConfig(cfg.configFile, cfg.enableExpandExternalLabels, cfg.tsdb.EnableExemplarStorage, logger, noStepSubqueryInterval, reloaders...); err != nil {
- level.Error(logger).Log("msg", "Error reloading config", "err", err)
+ if err := reloadConfig(cfg.configFile, cfg.tsdb.EnableExemplarStorage, logger, noStepSubqueryInterval, callback, reloaders...); err != nil {
+ logger.Error("Error reloading config", "err", err)
rc <- err
} else {
rc <- nil
+ if cfg.enableAutoReload {
+ checksum, err = config.GenerateChecksum(cfg.configFile)
+ if err != nil {
+ logger.Error("Failed to generate checksum during configuration reload", "err", err)
+ }
+ }
+ }
+ case <-time.Tick(time.Duration(cfg.autoReloadInterval)):
+ if !cfg.enableAutoReload {
+ continue
+ }
+ currentChecksum, err := config.GenerateChecksum(cfg.configFile)
+ if err != nil {
+ checksum = currentChecksum
+ logger.Error("Failed to generate checksum during configuration reload", "err", err)
+ } else if currentChecksum == checksum {
+ continue
+ }
+ logger.Info("Configuration file change detected, reloading the configuration.")
+
+ if err := reloadConfig(cfg.configFile, cfg.tsdb.EnableExemplarStorage, logger, noStepSubqueryInterval, callback, reloaders...); err != nil {
+ logger.Error("Error reloading config", "err", err)
+ } else {
+ checksum = currentChecksum
}
case <-cancel:
+ logger.Info("Reloaders stopped")
return nil
}
}
},
- func(err error) {
+ 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{}{}
},
)
@@ -1172,18 +1430,19 @@ func main() {
return nil
}
- if err := reloadConfig(cfg.configFile, cfg.enableExpandExternalLabels, cfg.tsdb.EnableExemplarStorage, logger, noStepSubqueryInterval, reloaders...); err != nil {
+ if err := reloadConfig(cfg.configFile, cfg.tsdb.EnableExemplarStorage, logger, noStepSubqueryInterval, func(bool) {}, reloaders...); err != nil {
return fmt.Errorf("error loading config from %q: %w", cfg.configFile, err)
}
reloadReady.Close()
- webHandler.SetReady(true)
- level.Info(logger).Log("msg", "Server is ready to receive web requests.")
+ webHandler.SetReady(web.Ready)
+ notifs.DeleteNotification(notifications.StartingUp)
+ logger.Info("Server is ready to receive web requests.")
<-cancel
return nil
},
- func(err error) {
+ func(error) {
close(cancel)
},
)
@@ -1194,7 +1453,7 @@ func main() {
cancel := make(chan struct{})
g.Add(
func() error {
- level.Info(logger).Log("msg", "Starting TSDB ...")
+ logger.Info("Starting TSDB ...")
if cfg.tsdb.WALSegmentSize != 0 {
if cfg.tsdb.WALSegmentSize < 10*1024*1024 || cfg.tsdb.WALSegmentSize > 256*1024*1024 {
return errors.New("flag 'storage.tsdb.wal-segment-size' must be set between 10MB and 256MB")
@@ -1211,22 +1470,26 @@ func main() {
return fmt.Errorf("opening storage failed: %w", err)
}
- switch fsType := prom_runtime.Statfs(localStoragePath); fsType {
+ switch fsType := prom_runtime.FsType(localStoragePath); fsType {
case "NFS_SUPER_MAGIC":
- level.Warn(logger).Log("fs_type", fsType, "msg", "This filesystem is not supported and may lead to data corruption and data loss. Please carefully read https://prometheus.io/docs/prometheus/latest/storage/ to learn more about supported filesystems.")
+ logger.Warn("This filesystem is not supported and may lead to data corruption and data loss. Please carefully read https://prometheus.io/docs/prometheus/latest/storage/ to learn more about supported filesystems.", "fs_type", fsType)
default:
- level.Info(logger).Log("fs_type", fsType)
+ logger.Info("filesystem information", "fs_type", fsType)
}
- level.Info(logger).Log("msg", "TSDB started")
- level.Debug(logger).Log("msg", "TSDB options",
+ logger.Info("TSDB started")
+ logger.Debug("TSDB options",
"MinBlockDuration", cfg.tsdb.MinBlockDuration,
"MaxBlockDuration", cfg.tsdb.MaxBlockDuration,
"MaxBytes", cfg.tsdb.MaxBytes,
+ "MaxPercentage", cfg.tsdb.MaxPercentage,
"NoLockfile", cfg.tsdb.NoLockfile,
"RetentionDuration", cfg.tsdb.RetentionDuration,
"WALSegmentSize", cfg.tsdb.WALSegmentSize,
- "WALCompression", cfg.tsdb.WALCompression,
+ "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)
@@ -1234,11 +1497,13 @@ func main() {
db.SetWriteNotified(remoteStorage)
close(dbOpen)
<-cancel
+ logger.Info("TSDB stopped")
return nil
},
- func(err error) {
+ func(error) {
+ logger.Info("Stopping storage...")
if err := fanoutStorage.Close(); err != nil {
- level.Error(logger).Log("msg", "Error stopping storage", "err", err)
+ logger.Error("Error stopping storage", "err", err)
}
close(cancel)
},
@@ -1250,7 +1515,7 @@ func main() {
cancel := make(chan struct{})
g.Add(
func() error {
- level.Info(logger).Log("msg", "Starting WAL storage ...")
+ logger.Info("Starting WAL storage ...")
if cfg.agent.WALSegmentSize != 0 {
if cfg.agent.WALSegmentSize < 10*1024*1024 || cfg.agent.WALSegmentSize > 256*1024*1024 {
return errors.New("flag 'storage.agent.wal-segment-size' must be set between 10MB and 256MB")
@@ -1267,33 +1532,37 @@ func main() {
return fmt.Errorf("opening storage failed: %w", err)
}
- switch fsType := prom_runtime.Statfs(localStoragePath); fsType {
+ switch fsType := prom_runtime.FsType(localStoragePath); fsType {
case "NFS_SUPER_MAGIC":
- level.Warn(logger).Log("fs_type", fsType, "msg", "This filesystem is not supported and may lead to data corruption and data loss. Please carefully read https://prometheus.io/docs/prometheus/latest/storage/ to learn more about supported filesystems.")
+ logger.Warn(fsType, "msg", "This filesystem is not supported and may lead to data corruption and data loss. Please carefully read https://prometheus.io/docs/prometheus/latest/storage/ to learn more about supported filesystems.")
default:
- level.Info(logger).Log("fs_type", fsType)
+ logger.Info(fsType)
}
- level.Info(logger).Log("msg", "Agent WAL storage started")
- level.Debug(logger).Log("msg", "Agent WAL storage options",
+ logger.Info("Agent WAL storage started")
+ logger.Debug("Agent WAL storage options",
"WALSegmentSize", cfg.agent.WALSegmentSize,
- "WALCompression", cfg.agent.WALCompression,
+ "WALCompressionType", cfg.agent.WALCompressionType,
"StripeSize", cfg.agent.StripeSize,
"TruncateFrequency", cfg.agent.TruncateFrequency,
"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(e error) {
+ func(error) {
+ logger.Info("Stopping agent WAL storage...")
if err := fanoutStorage.Close(); err != nil {
- level.Error(logger).Log("msg", "Error stopping storage", "err", err)
+ logger.Error("Error stopping storage", "err", err)
}
close(cancel)
},
@@ -1306,9 +1575,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(err error) {
+ func(error) {
+ logger.Info("Stopping web handler...")
cancelWeb()
},
)
@@ -1327,25 +1598,28 @@ func main() {
<-reloadReady.C
notifierManager.Run(discoveryManagerNotify.SyncCh())
- level.Info(logger).Log("msg", "Notifier manager stopped")
+ logger.Info("Notifier manager stopped")
return nil
},
- func(err error) {
+ func(error) {
+ logger.Info("Stopping notifier manager...")
notifierManager.Stop()
},
)
}
- if err := g.Run(); err != nil {
- level.Error(logger).Log("err", err)
- os.Exit(1)
- }
- level.Info(logger).Log("msg", "See you next time!")
+ func() { // This function exists so the top of the stack is named 'main.main.funcxxx' and not 'oklog'.
+ if err := g.Run(); err != nil {
+ logger.Error("Fatal error", "err", err)
+ os.Exit(1)
+ }
+ }()
+ logger.Info("See you next time!")
}
-func openDBWithMetrics(dir string, logger log.Logger, reg prometheus.Registerer, opts *tsdb.Options, stats *tsdb.DBStats) (*tsdb.DB, error) {
+func openDBWithMetrics(dir string, logger *slog.Logger, reg prometheus.Registerer, opts *tsdb.Options, stats *tsdb.DBStats) (*tsdb.DB, error) {
db, err := tsdb.Open(
dir,
- log.With(logger, "component", "tsdb"),
+ logger.With("component", "tsdb"),
reg,
opts,
stats,
@@ -1398,21 +1672,23 @@ type reloader struct {
reloader func(*config.Config) error
}
-func reloadConfig(filename string, expandExternalLabels, enableExemplarStorage bool, logger log.Logger, noStepSuqueryInterval *safePromQLNoStepSubqueryInterval, rls ...reloader) (err error) {
+func reloadConfig(filename string, enableExemplarStorage bool, logger *slog.Logger, noStepSubqueryInterval *safePromQLNoStepSubqueryInterval, callback func(bool), rls ...reloader) (err error) {
start := time.Now()
- timings := []interface{}{}
- level.Info(logger).Log("msg", "Loading configuration file", "filename", filename)
+ timingsLogger := logger
+ logger.Info("Loading configuration file", "filename", filename)
defer func() {
if err == nil {
configSuccess.Set(1)
configSuccessTime.SetToCurrentTime()
+ callback(true)
} else {
configSuccess.Set(0)
+ callback(false)
}
}()
- conf, err := config.LoadFile(filename, agentMode, expandExternalLabels, logger)
+ conf, err := config.LoadFile(filename, agentMode, logger)
if err != nil {
return fmt.Errorf("couldn't load configuration (--config.file=%q): %w", filename, err)
}
@@ -1427,18 +1703,25 @@ func reloadConfig(filename string, expandExternalLabels, enableExemplarStorage b
for _, rl := range rls {
rstart := time.Now()
if err := rl.reloader(conf); err != nil {
- level.Error(logger).Log("msg", "Failed to apply configuration", "err", err)
+ logger.Error("Failed to apply configuration", "err", err)
failed = true
}
- timings = append(timings, 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)
}
+ updateGoGC(conf, logger)
+ noStepSubqueryInterval.Set(conf.GlobalConfig.EvaluationInterval)
+ timingsLogger.Info("Completed loading of configuration file", "filename", filename, "totalDuration", time.Since(start))
+ return nil
+}
+
+func updateGoGC(conf *config.Config, logger *slog.Logger) {
oldGoGC := debug.SetGCPercent(conf.Runtime.GoGC)
if oldGoGC != conf.Runtime.GoGC {
- level.Info(logger).Log("msg", "updated GOGC", "old", oldGoGC, "new", conf.Runtime.GoGC)
+ logger.Info("updated GOGC", "old", oldGoGC, "new", conf.Runtime.GoGC)
}
// Write the new setting out to the ENV var for runtime API output.
if conf.Runtime.GoGC >= 0 {
@@ -1446,11 +1729,6 @@ func reloadConfig(filename string, expandExternalLabels, enableExemplarStorage b
} else {
os.Setenv("GOGC", "off")
}
-
- noStepSuqueryInterval.Set(conf.GlobalConfig.EvaluationInterval)
- l := []interface{}{"msg", "Completed loading of configuration file", "filename", filename, "totalDuration", time.Since(start)}
- level.Info(logger).Log(append(l, timings...)...)
- return nil
}
func startsOrEndsWithQuote(s string) bool {
@@ -1500,6 +1778,25 @@ func computeExternalURL(u, listenAddr string) (*url.URL, error) {
return eu, nil
}
+// storagePathFsSize returns the filesystem size for path or its closest existing parent.
+func storagePathFsSize(path string) uint64 {
+ for {
+ if size := prom_runtime.FsSize(path); size > 0 {
+ return size
+ }
+
+ if _, err := os.Stat(path); !errors.Is(err, os.ErrNotExist) {
+ return 0
+ }
+
+ parent := filepath.Dir(path)
+ if parent == path {
+ return 0
+ }
+ path = parent
+ }
+}
+
// readyStorage implements the Storage interface while allowing to set the actual
// storage at a later point in time.
type readyStorage struct {
@@ -1601,31 +1898,55 @@ 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{}
-func (n notReadyAppender) Append(ref storage.SeriesRef, l labels.Labels, t int64, v float64) (storage.SeriesRef, error) {
+// SetOptions does nothing in this appender implementation.
+func (notReadyAppender) SetOptions(*storage.AppendOptions) {}
+
+func (notReadyAppender) Append(storage.SeriesRef, labels.Labels, int64, float64) (storage.SeriesRef, error) {
return 0, tsdb.ErrNotReady
}
-func (n notReadyAppender) AppendExemplar(ref storage.SeriesRef, l labels.Labels, e exemplar.Exemplar) (storage.SeriesRef, error) {
+func (notReadyAppender) AppendExemplar(storage.SeriesRef, labels.Labels, exemplar.Exemplar) (storage.SeriesRef, error) {
return 0, tsdb.ErrNotReady
}
-func (n notReadyAppender) AppendHistogram(ref storage.SeriesRef, l labels.Labels, t int64, h *histogram.Histogram, fh *histogram.FloatHistogram) (storage.SeriesRef, error) {
+func (notReadyAppender) AppendHistogram(storage.SeriesRef, labels.Labels, int64, *histogram.Histogram, *histogram.FloatHistogram) (storage.SeriesRef, error) {
return 0, tsdb.ErrNotReady
}
-func (n notReadyAppender) UpdateMetadata(ref storage.SeriesRef, l labels.Labels, m metadata.Metadata) (storage.SeriesRef, error) {
+func (notReadyAppender) AppendHistogramSTZeroSample(storage.SeriesRef, labels.Labels, int64, int64, *histogram.Histogram, *histogram.FloatHistogram) (storage.SeriesRef, error) {
return 0, tsdb.ErrNotReady
}
-func (n notReadyAppender) AppendCTZeroSample(ref storage.SeriesRef, l labels.Labels, t, ct int64) (storage.SeriesRef, error) {
+func (notReadyAppender) UpdateMetadata(storage.SeriesRef, labels.Labels, metadata.Metadata) (storage.SeriesRef, error) {
return 0, tsdb.ErrNotReady
}
-func (n notReadyAppender) Commit() error { return tsdb.ErrNotReady }
+func (notReadyAppender) AppendSTZeroSample(storage.SeriesRef, labels.Labels, int64, int64) (storage.SeriesRef, error) {
+ return 0, tsdb.ErrNotReady
+}
-func (n notReadyAppender) Rollback() error { return tsdb.ErrNotReady }
+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 {
@@ -1650,6 +1971,21 @@ func (s *readyStorage) CleanTombstones() error {
return tsdb.ErrNotReady
}
+// BlockMetas implements the api_v1.TSDBAdminStats and api_v2.TSDBAdmin interfaces.
+func (s *readyStorage) BlockMetas() ([]tsdb.BlockMeta, error) {
+ if x := s.get(); x != nil {
+ switch db := x.(type) {
+ case *tsdb.DB:
+ return db.BlockMetas(), nil
+ case *agent.DB:
+ return nil, agent.ErrUnsupported
+ default:
+ panic(fmt.Sprintf("unknown storage type %T", db))
+ }
+ }
+ return nil, tsdb.ErrNotReady
+}
+
// Delete implements the api_v1.TSDBAdminStats and api_v2.TSDBAdmin interfaces.
func (s *readyStorage) Delete(ctx context.Context, mint, maxt int64, ms ...*labels.Matcher) error {
if x := s.get(); x != nil {
@@ -1704,7 +2040,7 @@ func (s *readyStorage) WALReplayStatus() (tsdb.WALReplayStatus, error) {
}
// ErrNotReady is returned if the underlying scrape manager is not ready yet.
-var ErrNotReady = errors.New("Scrape manager not ready")
+var ErrNotReady = errors.New("scrape manager not ready")
// ReadyScrapeManager allows a scrape manager to be retrieved. Even if it's set at a later point in time.
type readyScrapeManager struct {
@@ -1739,9 +2075,9 @@ type tsdbOptions struct {
MaxBlockChunkSegmentSize units.Base2Bytes
RetentionDuration model.Duration
MaxBytes units.Base2Bytes
+ MaxPercentage float64
NoLockfile bool
- WALCompression bool
- WALCompressionType string
+ WALCompressionType compression.Type
HeadChunksWriteQueueSize int
SamplesPerChunk int
StripeSize int
@@ -1751,9 +2087,17 @@ type tsdbOptions struct {
EnableExemplarStorage bool
MaxExemplars int64
EnableMemorySnapshotOnShutdown bool
- EnableNativeHistograms bool
EnableDelayedCompaction bool
+ CompactionDelayMaxPercent int
EnableOverlappingCompaction bool
+ UseUncachedIO bool
+ BlockCompactionExcludeFunc tsdb.BlockExcludeFilterFunc
+ BlockReloadInterval model.Duration
+ EnableSTAsZeroSample bool
+ EnableSTStorage bool
+ StaleSeriesCompactionThreshold float64
+ EnableFastStartup bool
+ FloatChunkEncoding chunkenc.Encoding
}
func (opts tsdbOptions) ToTSDBOptions() tsdb.Options {
@@ -1762,8 +2106,9 @@ func (opts tsdbOptions) ToTSDBOptions() tsdb.Options {
MaxBlockChunkSegmentSize: int64(opts.MaxBlockChunkSegmentSize),
RetentionDuration: int64(time.Duration(opts.RetentionDuration) / time.Millisecond),
MaxBytes: int64(opts.MaxBytes),
+ MaxPercentage: opts.MaxPercentage,
NoLockfile: opts.NoLockfile,
- WALCompression: wlog.ParseCompressionType(opts.WALCompression, opts.WALCompressionType),
+ WALCompression: opts.WALCompressionType,
HeadChunksWriteQueueSize: opts.HeadChunksWriteQueueSize,
SamplesPerChunk: opts.SamplesPerChunk,
StripeSize: opts.StripeSize,
@@ -1772,24 +2117,36 @@ func (opts tsdbOptions) ToTSDBOptions() tsdb.Options {
EnableExemplarStorage: opts.EnableExemplarStorage,
MaxExemplars: opts.MaxExemplars,
EnableMemorySnapshotOnShutdown: opts.EnableMemorySnapshotOnShutdown,
- EnableNativeHistograms: opts.EnableNativeHistograms,
OutOfOrderTimeWindow: opts.OutOfOrderTimeWindow,
EnableDelayedCompaction: opts.EnableDelayedCompaction,
+ 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,
+ EnableFastStartup: opts.EnableFastStartup,
+ FloatChunkEncoding: opts.FloatChunkEncoding,
}
}
// agentOptions is a version of agent.Options with defined units. This is required
// as agent.Option fields are unit agnostic (time).
type agentOptions struct {
- WALSegmentSize units.Base2Bytes
- WALCompression bool
- WALCompressionType string
- StripeSize int
- TruncateFrequency model.Duration
- MinWALTime, MaxWALTime model.Duration
- NoLockfile bool
- OutOfOrderTimeWindow int64
+ WALSegmentSize units.Base2Bytes
+ WALCompressionType compression.Type
+ StripeSize int
+ TruncateFrequency model.Duration
+ MinWALTime, MaxWALTime model.Duration
+ NoLockfile bool
+ OutOfOrderTimeWindow int64 // TODO(bwplotka): Unused option, fix it or remove.
+ EnableSTAsZeroSample bool
+ EnableSTStorage bool
+ CheckpointFromInMemorySeries bool
+ CheckpointBatchSize int
}
func (opts agentOptions) ToAgentOptions(outOfOrderTimeWindow int64) agent.Options {
@@ -1797,37 +2154,32 @@ func (opts agentOptions) ToAgentOptions(outOfOrderTimeWindow int64) agent.Option
outOfOrderTimeWindow = 0
}
return agent.Options{
- WALSegmentSize: int(opts.WALSegmentSize),
- WALCompression: wlog.ParseCompressionType(opts.WALCompression, opts.WALCompressionType),
- StripeSize: opts.StripeSize,
- TruncateFrequency: time.Duration(opts.TruncateFrequency),
- MinWALTime: durationToInt64Millis(time.Duration(opts.MinWALTime)),
- MaxWALTime: durationToInt64Millis(time.Duration(opts.MaxWALTime)),
- NoLockfile: opts.NoLockfile,
- OutOfOrderTimeWindow: outOfOrderTimeWindow,
+ WALSegmentSize: int(opts.WALSegmentSize),
+ WALCompression: opts.WALCompressionType,
+ StripeSize: opts.StripeSize,
+ TruncateFrequency: time.Duration(opts.TruncateFrequency),
+ MinWALTime: durationToInt64Millis(time.Duration(opts.MinWALTime)),
+ MaxWALTime: durationToInt64Millis(time.Duration(opts.MaxWALTime)),
+ NoLockfile: opts.NoLockfile,
+ OutOfOrderTimeWindow: outOfOrderTimeWindow,
+ EnableSTAsZeroSample: opts.EnableSTAsZeroSample,
+ EnableSTStorage: opts.EnableSTStorage,
+ CheckpointFromInMemorySeries: opts.CheckpointFromInMemorySeries,
+ CheckpointBatchSize: opts.CheckpointBatchSize,
}
}
-// discoveryManager interfaces the discovery manager. This is used to keep using
-// the manager that restarts SD's on reload for a few releases until we feel
-// the new manager can be enabled for all users.
-type discoveryManager interface {
- ApplyConfig(cfg map[string]discovery.Configs) error
- Run() error
- SyncCh() <-chan map[string][]*targetgroup.Group
-}
-
-// rwProtoMsgFlagParser is a custom parser for config.RemoteWriteProtoMsg enum.
+// rwProtoMsgFlagParser is a custom parser for remoteapi.WriteMessageType enum.
type rwProtoMsgFlagParser struct {
- msgs *[]config.RemoteWriteProtoMsg
+ msgs *remoteapi.MessageTypes
}
-func rwProtoMsgFlagValue(msgs *[]config.RemoteWriteProtoMsg) kingpin.Value {
+func rwProtoMsgFlagValue(msgs *remoteapi.MessageTypes) kingpin.Value {
return &rwProtoMsgFlagParser{msgs: msgs}
}
// IsCumulative is used by kingpin to tell if it's an array or not.
-func (p *rwProtoMsgFlagParser) IsCumulative() bool {
+func (*rwProtoMsgFlagParser) IsCumulative() bool {
return true
}
@@ -1840,15 +2192,58 @@ func (p *rwProtoMsgFlagParser) String() string {
}
func (p *rwProtoMsgFlagParser) Set(opt string) error {
- t := config.RemoteWriteProtoMsg(opt)
+ t := remoteapi.WriteMessageType(opt)
if err := t.Validate(); err != nil {
return err
}
- for _, prev := range *p.msgs {
- if prev == t {
- return fmt.Errorf("duplicated %v flag value, got %v already", t, *p.msgs)
- }
+ if slices.Contains(*p.msgs, t) {
+ return fmt.Errorf("duplicated %v flag value, got %v already", t, *p.msgs)
}
*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 c827812e607..216bb882c80 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
@@ -20,28 +20,41 @@ import (
"fmt"
"io"
"math"
+ "net/http"
+ "net/http/httptest"
"os"
"os/exec"
"path/filepath"
"runtime"
+ "slices"
"strconv"
"strings"
+ "sync"
"syscall"
"testing"
"time"
"github.com/alecthomas/kingpin/v2"
- "github.com/go-kit/log"
+ "github.com/grafana/regexp"
+ remoteapi "github.com/prometheus/client_golang/exp/api/remote"
"github.com/prometheus/client_golang/prometheus"
+ "github.com/prometheus/common/expfmt"
"github.com/prometheus/common/model"
+ "github.com/prometheus/common/promslog"
"github.com/stretchr/testify/require"
- "github.com/prometheus/prometheus/config"
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/notifier"
"github.com/prometheus/prometheus/rules"
+ "github.com/prometheus/prometheus/util/testutil"
)
+func init() {
+ // This can be removed when the legacy global mode is fully deprecated.
+ //nolint:staticcheck
+ model.NameValidationScheme = model.UTF8Validation
+}
+
const startupTime = 10 * time.Second
var (
@@ -120,6 +133,7 @@ func TestFailedStartupExitCode(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode.")
}
+ t.Parallel()
fakeInputFile := "fake-input-file"
expectedExitStatus := 2
@@ -134,6 +148,47 @@ func TestFailedStartupExitCode(t *testing.T) {
require.Equal(t, expectedExitStatus, status.ExitStatus())
}
+func TestRetentionPercentageStartsWithMissingStoragePath(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping test in short mode.")
+ }
+ t.Parallel()
+
+ tmpDir := t.TempDir()
+ if storagePathFsSize(tmpDir) == 0 {
+ t.Skip("skipping test because filesystem size detection is unavailable.")
+ }
+
+ configFile := filepath.Join(tmpDir, "prometheus.yml")
+ storagePath := filepath.Join(tmpDir, "missing", "data")
+
+ require.NoError(t, os.WriteFile(configFile, []byte(`
+storage:
+ tsdb:
+ retention:
+ percentage: 1.5
+`), 0o777))
+
+ port := testutil.RandomUnprivilegedPort(t)
+ prom := prometheusCommandWithLogging(
+ t,
+ configFile,
+ port,
+ "--storage.tsdb.path="+storagePath,
+ )
+ require.NoError(t, prom.Start())
+
+ require.Eventually(t, func() bool {
+ r, err := http.Get(fmt.Sprintf("http://127.0.0.1:%d/metrics", port))
+ if err != nil {
+ return false
+ }
+ defer r.Body.Close()
+ return r.StatusCode == http.StatusOK
+ }, startupTime, 100*time.Millisecond)
+ require.DirExists(t, storagePath)
+}
+
type senderFunc func(alerts ...*notifier.Alert)
func (s senderFunc) Send(alerts ...*notifier.Alert) {
@@ -191,7 +246,6 @@ func TestSendAlerts(t *testing.T) {
}
for i, tc := range testCases {
- tc := tc
t.Run(strconv.Itoa(i), func(t *testing.T) {
senderFunc := senderFunc(func(alerts ...*notifier.Alert) {
require.NotEmpty(t, tc.in, "sender called with 0 alert")
@@ -206,83 +260,241 @@ func TestWALSegmentSizeBounds(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode.")
}
+ t.Parallel()
- for size, expectedExitStatus := range map[string]int{"9MB": 1, "257MB": 1, "10": 2, "1GB": 1, "12MB": 0} {
- prom := exec.Command(promPath, "-test.main", "--storage.tsdb.wal-segment-size="+size, "--web.listen-address=0.0.0.0:0", "--config.file="+promConfig, "--storage.tsdb.path="+filepath.Join(t.TempDir(), "data"))
+ for _, tc := range []struct {
+ size string
+ exitCode int
+ }{
+ {
+ size: "9MB",
+ exitCode: 1,
+ },
+ {
+ size: "257MB",
+ exitCode: 1,
+ },
+ {
+ size: "10",
+ exitCode: 2,
+ },
+ {
+ size: "1GB",
+ exitCode: 1,
+ },
+ {
+ size: "12MB",
+ exitCode: 0,
+ },
+ } {
+ t.Run(tc.size, func(t *testing.T) {
+ t.Parallel()
+ prom := exec.Command(promPath, "-test.main", "--storage.tsdb.wal-segment-size="+tc.size, "--web.listen-address=0.0.0.0:0", "--config.file="+promConfig, "--storage.tsdb.path="+filepath.Join(t.TempDir(), "data"))
- // Log stderr in case of failure.
- stderr, err := prom.StderrPipe()
- require.NoError(t, err)
- go func() {
- slurp, _ := io.ReadAll(stderr)
- t.Log(string(slurp))
- }()
+ // Log stderr in case of failure.
+ stderr, err := prom.StderrPipe()
+ require.NoError(t, err)
- err = prom.Start()
- require.NoError(t, err)
+ // WaitGroup is used to ensure that we don't call t.Log() after the test has finished.
+ var wg sync.WaitGroup
+ wg.Add(1)
+ defer wg.Wait()
+
+ go func() {
+ defer wg.Done()
+ slurp, _ := io.ReadAll(stderr)
+ t.Log(string(slurp))
+ }()
+
+ err = prom.Start()
+ require.NoError(t, err)
- if expectedExitStatus == 0 {
- done := make(chan error, 1)
- go func() { done <- prom.Wait() }()
- select {
- case err := <-done:
- require.Fail(t, "prometheus should be still running: %v", err)
- case <-time.After(startupTime):
- prom.Process.Kill()
- <-done
+ if tc.exitCode == 0 {
+ done := make(chan error, 1)
+ go func() { done <- prom.Wait() }()
+ select {
+ case err := <-done:
+ t.Fatalf("prometheus should be still running: %v", err)
+ case <-time.After(startupTime):
+ prom.Process.Kill()
+ <-done
+ }
+ return
}
- continue
- }
- err = prom.Wait()
- require.Error(t, err)
- var exitError *exec.ExitError
- require.ErrorAs(t, err, &exitError)
- status := exitError.Sys().(syscall.WaitStatus)
- require.Equal(t, expectedExitStatus, status.ExitStatus())
+ err = prom.Wait()
+ require.Error(t, err)
+ var exitError *exec.ExitError
+ require.ErrorAs(t, err, &exitError)
+ status := exitError.Sys().(syscall.WaitStatus)
+ require.Equal(t, tc.exitCode, status.ExitStatus())
+ })
}
}
func TestMaxBlockChunkSegmentSizeBounds(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping test in short mode.")
+ }
t.Parallel()
+ for _, tc := range []struct {
+ size string
+ exitCode int
+ }{
+ {
+ size: "512KB",
+ exitCode: 1,
+ },
+ {
+ size: "1MB",
+ exitCode: 0,
+ },
+ } {
+ t.Run(tc.size, func(t *testing.T) {
+ t.Parallel()
+ prom := exec.Command(promPath, "-test.main", "--storage.tsdb.max-block-chunk-segment-size="+tc.size, "--web.listen-address=0.0.0.0:0", "--config.file="+promConfig, "--storage.tsdb.path="+filepath.Join(t.TempDir(), "data"))
+
+ // Log stderr in case of failure.
+ stderr, err := prom.StderrPipe()
+ require.NoError(t, err)
+
+ // WaitGroup is used to ensure that we don't call t.Log() after the test has finished.
+ var wg sync.WaitGroup
+ wg.Add(1)
+ defer wg.Wait()
+
+ go func() {
+ defer wg.Done()
+ slurp, _ := io.ReadAll(stderr)
+ t.Log(string(slurp))
+ }()
+
+ err = prom.Start()
+ require.NoError(t, err)
+
+ if tc.exitCode == 0 {
+ done := make(chan error, 1)
+ go func() { done <- prom.Wait() }()
+ select {
+ case err := <-done:
+ t.Fatalf("prometheus should be still running: %v", err)
+ case <-time.After(startupTime):
+ prom.Process.Kill()
+ <-done
+ }
+ return
+ }
+
+ err = prom.Wait()
+ require.Error(t, err)
+ var exitError *exec.ExitError
+ require.ErrorAs(t, err, &exitError)
+ status := exitError.Sys().(syscall.WaitStatus)
+ require.Equal(t, tc.exitCode, status.ExitStatus())
+ })
+ }
+}
+
+func TestChunkEncodingStartupValidation(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode.")
}
+ t.Parallel()
- for size, expectedExitStatus := range map[string]int{"512KB": 1, "1MB": 0} {
- prom := exec.Command(promPath, "-test.main", "--storage.tsdb.max-block-chunk-segment-size="+size, "--web.listen-address=0.0.0.0:0", "--config.file="+promConfig, "--storage.tsdb.path="+filepath.Join(t.TempDir(), "data"))
+ for _, tc := range []struct {
+ name string
+ config string
+ features string
+ exitCode int
+ }{
+ {
+ name: "xor2 without xor2-encoding feature",
+ config: `
+storage:
+ tsdb:
+ chunk_encoding:
+ floats: xor2`,
+ features: "",
+ exitCode: 1,
+ },
+ {
+ name: "xor2 with xor2-encoding feature",
+ config: `
+storage:
+ tsdb:
+ chunk_encoding:
+ floats: xor2`,
+ features: "xor2-encoding",
+ exitCode: 0,
+ },
+ {
+ name: "xor with st-storage and xor2-encoding features",
+ config: `
+storage:
+ tsdb:
+ chunk_encoding:
+ floats: xor`,
+ features: "st-storage,xor2-encoding",
+ exitCode: 1,
+ },
+ {
+ name: "xor without st-storage feature",
+ config: `
+storage:
+ tsdb:
+ chunk_encoding:
+ floats: xor`,
+ features: "",
+ exitCode: 0,
+ },
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+ tmpDir := t.TempDir()
+ configFile := filepath.Join(tmpDir, "prometheus.yml")
+ require.NoError(t, os.WriteFile(configFile, []byte(tc.config), 0o777))
+
+ args := []string{"-test.main", "--web.listen-address=0.0.0.0:0", "--config.file=" + configFile, "--storage.tsdb.path=" + filepath.Join(tmpDir, "data")}
+ if tc.features != "" {
+ args = append(args, "--enable-feature="+tc.features)
+ }
+ prom := exec.Command(promPath, args...)
- // Log stderr in case of failure.
- stderr, err := prom.StderrPipe()
- require.NoError(t, err)
- go func() {
- slurp, _ := io.ReadAll(stderr)
- t.Log(string(slurp))
- }()
+ stderr, err := prom.StderrPipe()
+ require.NoError(t, err)
- err = prom.Start()
- require.NoError(t, err)
+ var wg sync.WaitGroup
+ wg.Add(1)
+ defer wg.Wait()
+ go func() {
+ defer wg.Done()
+ slurp, _ := io.ReadAll(stderr)
+ t.Log(string(slurp))
+ }()
- if expectedExitStatus == 0 {
- done := make(chan error, 1)
- go func() { done <- prom.Wait() }()
- select {
- case err := <-done:
- require.Fail(t, "prometheus should be still running: %v", err)
- case <-time.After(startupTime):
- prom.Process.Kill()
- <-done
+ require.NoError(t, prom.Start())
+
+ if tc.exitCode == 0 {
+ done := make(chan error, 1)
+ go func() { done <- prom.Wait() }()
+ select {
+ case err := <-done:
+ t.Fatalf("prometheus should be still running: %v", err)
+ case <-time.After(startupTime):
+ prom.Process.Kill()
+ <-done
+ }
+ return
}
- continue
- }
- err = prom.Wait()
- require.Error(t, err)
- var exitError *exec.ExitError
- require.ErrorAs(t, err, &exitError)
- status := exitError.Sys().(syscall.WaitStatus)
- require.Equal(t, expectedExitStatus, status.ExitStatus())
+ err = prom.Wait()
+ require.Error(t, err)
+ var exitError *exec.ExitError
+ require.ErrorAs(t, err, &exitError)
+ status := exitError.Sys().(syscall.WaitStatus)
+ require.Equal(t, tc.exitCode, status.ExitStatus())
+ })
}
}
@@ -290,7 +502,7 @@ func TestTimeMetrics(t *testing.T) {
tmpDir := t.TempDir()
reg := prometheus.NewRegistry()
- db, err := openDBWithMetrics(tmpDir, log.NewNopLogger(), reg, nil, nil)
+ db, err := openDBWithMetrics(tmpDir, promslog.NewNopLogger(), reg, nil, nil)
require.NoError(t, err)
defer func() {
require.NoError(t, db.Close())
@@ -328,6 +540,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)
@@ -348,7 +561,9 @@ func getCurrentGaugeValuesFor(t *testing.T, reg prometheus.Gatherer, metricNames
}
func TestAgentSuccessfulStartup(t *testing.T) {
- prom := exec.Command(promPath, "-test.main", "--enable-feature=agent", "--web.listen-address=0.0.0.0:0", "--config.file="+agentConfig)
+ t.Parallel()
+
+ prom := exec.Command(promPath, "-test.main", "--agent", "--web.listen-address=0.0.0.0:0", "--config.file="+agentConfig)
require.NoError(t, prom.Start())
actualExitStatus := 0
@@ -357,7 +572,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()
@@ -366,7 +581,9 @@ func TestAgentSuccessfulStartup(t *testing.T) {
}
func TestAgentFailedStartupWithServerFlag(t *testing.T) {
- prom := exec.Command(promPath, "-test.main", "--enable-feature=agent", "--storage.tsdb.path=.", "--web.listen-address=0.0.0.0:0", "--config.file="+promConfig)
+ t.Parallel()
+
+ prom := exec.Command(promPath, "-test.main", "--agent", "--storage.tsdb.path=.", "--web.listen-address=0.0.0.0:0", "--config.file="+promConfig)
output := bytes.Buffer{}
prom.Stderr = &output
@@ -393,7 +610,9 @@ func TestAgentFailedStartupWithServerFlag(t *testing.T) {
}
func TestAgentFailedStartupWithInvalidConfig(t *testing.T) {
- prom := exec.Command(promPath, "-test.main", "--enable-feature=agent", "--web.listen-address=0.0.0.0:0", "--config.file="+promConfig)
+ t.Parallel()
+
+ prom := exec.Command(promPath, "-test.main", "--agent", "--web.listen-address=0.0.0.0:0", "--config.file="+promConfig)
require.NoError(t, prom.Start())
actualExitStatus := 0
@@ -414,6 +633,7 @@ func TestModeSpecificFlags(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode.")
}
+ t.Parallel()
testcases := []struct {
mode string
@@ -428,10 +648,11 @@ func TestModeSpecificFlags(t *testing.T) {
for _, tc := range testcases {
t.Run(fmt.Sprintf("%s mode with option %s", tc.mode, tc.arg), func(t *testing.T) {
+ t.Parallel()
args := []string{"-test.main", tc.arg, t.TempDir(), "--web.listen-address=0.0.0.0:0"}
if tc.mode == "agent" {
- args = append(args, "--enable-feature=agent", "--config.file="+agentConfig)
+ args = append(args, "--agent", "--config.file="+agentConfig)
} else {
args = append(args, "--config.file="+promConfig)
}
@@ -441,7 +662,14 @@ func TestModeSpecificFlags(t *testing.T) {
// Log stderr in case of failure.
stderr, err := prom.StderrPipe()
require.NoError(t, err)
+
+ // WaitGroup is used to ensure that we don't call t.Log() after the test has finished.
+ var wg sync.WaitGroup
+ wg.Add(1)
+ defer wg.Wait()
+
go func() {
+ defer wg.Done()
slurp, _ := io.ReadAll(stderr)
t.Log(string(slurp))
}()
@@ -479,6 +707,8 @@ func TestDocumentation(t *testing.T) {
if runtime.GOOS == "windows" {
t.SkipNow()
}
+ t.Parallel()
+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
@@ -487,12 +717,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"))
@@ -503,13 +728,15 @@ func TestDocumentation(t *testing.T) {
}
func TestRwProtoMsgFlagParser(t *testing.T) {
- defaultOpts := config.RemoteWriteProtoMsgs{
- config.RemoteWriteProtoMsgV1, config.RemoteWriteProtoMsgV2,
+ t.Parallel()
+
+ defaultOpts := remoteapi.MessageTypes{
+ remoteapi.WriteV1MessageType, remoteapi.WriteV2MessageType,
}
for _, tcase := range []struct {
args []string
- expected []config.RemoteWriteProtoMsg
+ expected remoteapi.MessageTypes
expectedErr error
}{
{
@@ -518,25 +745,25 @@ func TestRwProtoMsgFlagParser(t *testing.T) {
},
{
args: []string{"--test-proto-msgs", "test"},
- expectedErr: errors.New("unknown remote write protobuf message test, supported: prometheus.WriteRequest, io.prometheus.write.v2.Request"),
+ expectedErr: errors.New("unknown type for remote write protobuf message test, supported: prometheus.WriteRequest, io.prometheus.write.v2.Request"),
},
{
args: []string{"--test-proto-msgs", "io.prometheus.write.v2.Request"},
- expected: config.RemoteWriteProtoMsgs{config.RemoteWriteProtoMsgV2},
+ expected: remoteapi.MessageTypes{remoteapi.WriteV2MessageType},
},
{
args: []string{
"--test-proto-msgs", "io.prometheus.write.v2.Request",
"--test-proto-msgs", "io.prometheus.write.v2.Request",
},
- expectedErr: errors.New("duplicated io.prometheus.write.v2.Request flag value, got [io.prometheus.write.v2.Request] already"),
+ expectedErr: errors.New("duplicated io.prometheus.write.v2.Request flag value, got io.prometheus.write.v2.Request already"),
},
{
args: []string{
"--test-proto-msgs", "io.prometheus.write.v2.Request",
"--test-proto-msgs", "prometheus.WriteRequest",
},
- expected: config.RemoteWriteProtoMsgs{config.RemoteWriteProtoMsgV2, config.RemoteWriteProtoMsgV1},
+ expected: remoteapi.MessageTypes{remoteapi.WriteV2MessageType, remoteapi.WriteV1MessageType},
},
{
args: []string{
@@ -544,12 +771,12 @@ func TestRwProtoMsgFlagParser(t *testing.T) {
"--test-proto-msgs", "prometheus.WriteRequest",
"--test-proto-msgs", "io.prometheus.write.v2.Request",
},
- expectedErr: errors.New("duplicated io.prometheus.write.v2.Request flag value, got [io.prometheus.write.v2.Request prometheus.WriteRequest] already"),
+ expectedErr: errors.New("duplicated io.prometheus.write.v2.Request flag value, got io.prometheus.write.v2.Request, prometheus.WriteRequest already"),
},
} {
t.Run(strings.Join(tcase.args, ","), func(t *testing.T) {
a := kingpin.New("test", "")
- var opt []config.RemoteWriteProtoMsg
+ var opt remoteapi.MessageTypes
a.Flag("test-proto-msgs", "").Default(defaultOpts.Strings()...).SetValue(rwProtoMsgFlagValue(&opt))
_, err := a.Parse(tcase.args)
@@ -563,3 +790,447 @@ func TestRwProtoMsgFlagParser(t *testing.T) {
})
}
}
+
+// reloadPrometheusConfig sends a reload request to the Prometheus server to apply
+// updated configurations.
+func reloadPrometheusConfig(t *testing.T, reloadURL string) {
+ t.Helper()
+
+ r, err := http.Post(reloadURL, "text/plain", nil)
+ require.NoError(t, err, "Failed to reload Prometheus")
+ require.Equal(t, http.StatusOK, r.StatusCode, "Unexpected status code when reloading Prometheus")
+}
+
+func getMetricValue(t *testing.T, body io.Reader, metricType model.MetricType, metricName string) (float64, error) {
+ t.Helper()
+
+ p := expfmt.NewTextParser(model.UTF8Validation)
+ metricFamilies, err := p.TextToMetricFamilies(body)
+ if err != nil {
+ return 0, err
+ }
+ metricFamily, ok := metricFamilies[metricName]
+ if !ok {
+ return 0, errors.New("metric family not found")
+ }
+ metric := metricFamily.GetMetric()
+ if len(metric) != 1 {
+ return 0, errors.New("metric not found")
+ }
+ switch metricType {
+ case model.MetricTypeGauge:
+ return metric[0].GetGauge().GetValue(), nil
+ case model.MetricTypeCounter:
+ return metric[0].GetCounter().GetValue(), nil
+ default:
+ t.Fatalf("metric type %s not supported", metricType)
+ }
+
+ return 0, errors.New("cannot get value")
+}
+
+func TestRuntimeGOGCConfig(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping test in short mode.")
+ }
+ t.Parallel()
+
+ for _, tc := range []struct {
+ name string
+ config string
+ gogcEnvVar string
+ expectedGOGC float64
+ }{
+ {
+ name: "empty config file",
+ expectedGOGC: 75,
+ },
+ {
+ name: "empty config file with GOGC env var set",
+ gogcEnvVar: "66",
+ expectedGOGC: 66,
+ },
+ {
+ name: "gogc set through config",
+ config: `
+runtime:
+ gogc: 77`,
+ expectedGOGC: 77.0,
+ },
+ {
+ name: "gogc set through config and env var",
+ config: `
+runtime:
+ gogc: 77`,
+ gogcEnvVar: "88",
+ expectedGOGC: 77.0,
+ },
+ {
+ name: "incomplete runtime block",
+ config: `
+runtime:`,
+ expectedGOGC: 75.0,
+ },
+ {
+ name: "incomplete runtime block and GOGC env var set",
+ config: `
+runtime:`,
+ gogcEnvVar: "88",
+ expectedGOGC: 88.0,
+ },
+ {
+ name: "unrelated config and GOGC env var set",
+ config: `
+global:
+ scrape_interval: 500ms`,
+ gogcEnvVar: "80",
+ expectedGOGC: 80,
+ },
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+
+ tmpDir := t.TempDir()
+ configFile := filepath.Join(tmpDir, "prometheus.yml")
+
+ port := testutil.RandomUnprivilegedPort(t)
+ require.NoError(t, os.WriteFile(configFile, []byte(tc.config), 0o777))
+ prom := prometheusCommandWithLogging(
+ t,
+ configFile,
+ port,
+ fmt.Sprintf("--storage.tsdb.path=%s", tmpDir),
+ "--web.enable-lifecycle",
+ )
+ // Inject GOGC when set.
+ prom.Env = os.Environ()
+ if tc.gogcEnvVar != "" {
+ prom.Env = append(prom.Env, fmt.Sprintf("GOGC=%s", tc.gogcEnvVar))
+ }
+ require.NoError(t, prom.Start())
+
+ ensureGOGCValue := func(val float64) {
+ var (
+ r *http.Response
+ err error
+ )
+ // Wait for the /metrics endpoint to be ready.
+ require.Eventually(t, func() bool {
+ r, err = http.Get(fmt.Sprintf("http://127.0.0.1:%d/metrics", port))
+ if err != nil {
+ return false
+ }
+ return r.StatusCode == http.StatusOK
+ }, 5*time.Second, 50*time.Millisecond)
+ defer r.Body.Close()
+
+ // Check the final GOGC that's set, consider go_gc_gogc_percent from /metrics as source of truth.
+ gogc, err := getMetricValue(t, r.Body, model.MetricTypeGauge, "go_gc_gogc_percent")
+ require.NoError(t, err)
+ require.Equal(t, val, gogc)
+ }
+
+ // The value is applied on startup.
+ ensureGOGCValue(tc.expectedGOGC)
+
+ // After a reload with the same config, the value stays the same.
+ reloadURL := fmt.Sprintf("http://127.0.0.1:%d/-/reload", port)
+ reloadPrometheusConfig(t, reloadURL)
+ ensureGOGCValue(tc.expectedGOGC)
+
+ // After a reload with different config, the value gets updated.
+ newConfig := `
+runtime:
+ gogc: 99`
+ require.NoError(t, os.WriteFile(configFile, []byte(newConfig), 0o777))
+ reloadPrometheusConfig(t, reloadURL)
+ ensureGOGCValue(99.0)
+ })
+ }
+}
+
+// TestHeadCompactionWhileScraping verifies that running a head compaction
+// concurrently with a scrape does not trigger the data race described in
+// https://github.com/prometheus/prometheus/issues/16490.
+func TestHeadCompactionWhileScraping(t *testing.T) {
+ t.Parallel()
+
+ // To increase the chance of reproducing the data race
+ for i := range 5 {
+ t.Run(strconv.Itoa(i), func(t *testing.T) {
+ t.Parallel()
+
+ tmpDir := t.TempDir()
+ configFile := filepath.Join(tmpDir, "prometheus.yml")
+
+ port := testutil.RandomUnprivilegedPort(t)
+ config := fmt.Sprintf(`
+scrape_configs:
+ - job_name: 'self1'
+ scrape_interval: 61ms
+ static_configs:
+ - targets: ['localhost:%d']
+ - job_name: 'self2'
+ scrape_interval: 67ms
+ static_configs:
+ - targets: ['localhost:%d']
+`, port, port)
+ require.NoError(t, os.WriteFile(configFile, []byte(config), 0o777))
+
+ prom := prometheusCommandWithLogging(
+ t,
+ configFile,
+ port,
+ fmt.Sprintf("--storage.tsdb.path=%s", tmpDir),
+ "--storage.tsdb.min-block-duration=100ms",
+ )
+ require.NoError(t, prom.Start())
+
+ require.Eventually(t, func() bool {
+ r, err := http.Get(fmt.Sprintf("http://127.0.0.1:%d/metrics", port))
+ if err != nil {
+ return false
+ }
+ defer r.Body.Close()
+ if r.StatusCode != http.StatusOK {
+ return false
+ }
+ metrics, err := io.ReadAll(r.Body)
+ if err != nil {
+ return false
+ }
+
+ // Wait for some compactions to run
+ compactions, err := getMetricValue(t, bytes.NewReader(metrics), model.MetricTypeCounter, "prometheus_tsdb_compactions_total")
+ if err != nil {
+ return false
+ }
+ if compactions < 3 {
+ return false
+ }
+
+ // Sanity check: Some actual scraping was done.
+ series, err := getMetricValue(t, bytes.NewReader(metrics), model.MetricTypeCounter, "prometheus_tsdb_head_series_created_total")
+ require.NoError(t, err)
+ require.NotZero(t, series)
+
+ // No compaction must have failed
+ failures, err := getMetricValue(t, bytes.NewReader(metrics), model.MetricTypeCounter, "prometheus_tsdb_compactions_failed_total")
+ require.NoError(t, err)
+ require.Zero(t, failures)
+ return true
+ }, 15*time.Second, 500*time.Millisecond)
+ })
+ }
+}
+
+// This test verifies that metrics for the highest timestamps per queue account for relabelling.
+// See: https://github.com/prometheus/prometheus/pull/17065.
+func TestRemoteWrite_PerQueueMetricsAfterRelabeling(t *testing.T) {
+ t.Parallel()
+
+ tmpDir := t.TempDir()
+ configFile := filepath.Join(tmpDir, "prometheus.yml")
+
+ port := testutil.RandomUnprivilegedPort(t)
+ targetPort := testutil.RandomUnprivilegedPort(t)
+
+ server := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
+ body, err := io.ReadAll(r.Body)
+ require.NoError(t, err)
+ require.Fail(t, "should never be reached because the remote write relabeling shouldn't yield anything", "header: %v, body: %s", r.Header, body)
+ }))
+ t.Cleanup(server.Close)
+
+ // Simulate a remote write relabeling that doesn't yield any series.
+ config := fmt.Sprintf(`
+global:
+ scrape_interval: 1s
+scrape_configs:
+ - job_name: 'self'
+ static_configs:
+ - targets: ['localhost:%d']
+ - job_name: 'target'
+ static_configs:
+ - targets: ['localhost:%d']
+
+remote_write:
+ - url: %s
+ write_relabel_configs:
+ - source_labels: [job,__name__]
+ regex: 'target,special_metric'
+ action: keep
+`, port, targetPort, server.URL)
+ require.NoError(t, os.WriteFile(configFile, []byte(config), 0o777))
+
+ prom := prometheusCommandWithLogging(
+ t,
+ configFile,
+ port,
+ fmt.Sprintf("--storage.tsdb.path=%s", tmpDir),
+ )
+ require.NoError(t, prom.Start())
+
+ require.Eventually(t, func() bool {
+ r, err := http.Get(fmt.Sprintf("http://127.0.0.1:%d/metrics", port))
+ if err != nil {
+ return false
+ }
+ defer r.Body.Close()
+ if r.StatusCode != http.StatusOK {
+ return false
+ }
+
+ metrics, err := io.ReadAll(r.Body)
+ if err != nil {
+ return false
+ }
+
+ gHighestTimestamp, err := getMetricValue(t, bytes.NewReader(metrics), model.MetricTypeGauge, "prometheus_remote_storage_highest_timestamp_in_seconds")
+ // The highest timestamp at storage level sees all samples, it should also consider the ones that are filtered out by relabeling.
+ if err != nil || gHighestTimestamp == 0 {
+ return false
+ }
+
+ // The queue shouldn't see and send any sample, all samples are dropped due to relabeling, the metrics should reflect that.
+ droppedSamples, err := getMetricValue(t, bytes.NewReader(metrics), model.MetricTypeCounter, "prometheus_remote_storage_samples_dropped_total")
+ if err != nil || droppedSamples == 0 {
+ return false
+ }
+
+ highestTimestamp, err := getMetricValue(t, bytes.NewReader(metrics), model.MetricTypeGauge, "prometheus_remote_storage_queue_highest_timestamp_seconds")
+ require.NoError(t, err)
+ require.Zero(t, highestTimestamp)
+
+ highestSentTimestamp, err := getMetricValue(t, bytes.NewReader(metrics), model.MetricTypeGauge, "prometheus_remote_storage_queue_highest_sent_timestamp_seconds")
+ require.NoError(t, err)
+ require.Zero(t, highestSentTimestamp)
+ return true
+ }, 10*time.Second, 100*time.Millisecond)
+}
+
+// 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.Parallel()
+
+ tmpDir := t.TempDir()
+ configFile := filepath.Join(tmpDir, "prometheus.yml")
+
+ port := testutil.RandomUnprivilegedPort(t)
+
+ server := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
+ time.Sleep(time.Second)
+ }))
+ t.Cleanup(server.Close)
+
+ config := fmt.Sprintf(`
+global:
+ # Using a smaller interval may cause the scrape to time out.
+ scrape_interval: 1s
+scrape_configs:
+ - job_name: 'self'
+ static_configs:
+ - targets: ['localhost:%d']
+
+remote_write:
+ - url: %s
+ 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))
+
+ prom := prometheusCommandWithLogging(
+ t,
+ configFile,
+ port,
+ fmt.Sprintf("--storage.tsdb.path=%s", tmpDir),
+ "--log.level=debug",
+ )
+ require.NoError(t, prom.Start())
+
+ 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 nil, err
+ }
+ defer r.Body.Close()
+ if r.StatusCode != http.StatusOK {
+ 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)
+
+ // 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, time.Second)
+}
+
+// TestFeatureFlagsDocumented ensures the --enable-feature help text in main.go
+// and the documented flags in docs/feature_flags.md list the same set of flags.
+func TestFeatureFlagsDocumented(t *testing.T) {
+ if runtime.GOOS == "windows" {
+ t.SkipNow()
+ }
+ h, err := os.ReadFile(filepath.Join("..", "..", "cmd", "prometheus", "main.go"))
+ require.NoError(t, err)
+ m := regexp.MustCompile(`a\.Flag\("enable-feature", "Comma separated feature names to enable. Valid options: (.+?)\.`).FindSubmatch(h)
+ require.NotNil(t, m)
+ var helpFlags []string
+ for f := range strings.SplitSeq(string(m[1]), ",") {
+ helpFlags = append(helpFlags, strings.TrimSpace(f))
+ }
+ require.NotEmpty(t, helpFlags)
+
+ d, err := os.ReadFile(filepath.Join("..", "..", "docs", "feature_flags.md"))
+ require.NoError(t, err)
+ var docFlags []string
+ for _, dm := range regexp.MustCompile("(?m)^`--enable-feature=(.+)`$").FindAllSubmatch(d, -1) {
+ docFlags = append(docFlags, string(dm[1]))
+ }
+ require.NotEmpty(t, docFlags)
+ require.True(t, slices.IsSorted(helpFlags))
+ require.ElementsMatch(t, helpFlags, docFlags)
+}
diff --git a/cmd/prometheus/main_unix_test.go b/cmd/prometheus/main_unix_test.go
index 2011fb123f3..ea130b3bf9d 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
@@ -34,6 +34,7 @@ func TestStartupInterrupt(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode.")
}
+ t.Parallel()
port := fmt.Sprintf(":%d", testutil.RandomUnprivilegedPort(t))
@@ -52,7 +53,7 @@ func TestStartupInterrupt(t *testing.T) {
url := "http://localhost" + port + "/graph"
Loop:
- for x := 0; x < 10; x++ {
+ for range 10 {
// error=nil means prometheus has started, so we can send the interrupt
// signal and wait for the graceful shutdown.
if _, err := http.Get(url); err == nil {
diff --git a/cmd/prometheus/main_upgrade_test.go b/cmd/prometheus/main_upgrade_test.go
new file mode 100644
index 00000000000..65840b36589
--- /dev/null
+++ b/cmd/prometheus/main_upgrade_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 main
+
+import (
+ "archive/tar"
+ "bufio"
+ "compress/gzip"
+ "encoding/json"
+ "flag"
+ "fmt"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "os"
+ "path/filepath"
+ "runtime"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/prometheus/common/model"
+ "github.com/stretchr/testify/require"
+
+ "github.com/prometheus/prometheus/util/testutil"
+)
+
+const prometheusDownloadManifestURL = "https://prometheus.io/download.json"
+
+// ltsRelease identifies an LTS release and where to download it for the current OS/arch.
+type ltsRelease struct {
+ version string
+ assetURL string
+}
+
+// fetchLTSReleases fetches all current LTS releases and their download URLs for the
+// current OS/arch from the Prometheus download manifest. There may be more than one
+// LTS release active at the same time.
+func fetchLTSReleases(t *testing.T) []ltsRelease {
+ t.Helper()
+
+ resp, err := http.Get(prometheusDownloadManifestURL)
+ require.NoError(t, err)
+ defer resp.Body.Close()
+ require.Equal(t, http.StatusOK, resp.StatusCode)
+
+ var manifest struct {
+ Prometheus []struct {
+ Version string `json:"version"`
+ LTS bool `json:"lts"`
+ Files []struct {
+ URL string `json:"url"`
+ OS string `json:"os"`
+ Arch string `json:"arch"`
+ Kind string `json:"kind"`
+ } `json:"files"`
+ } `json:"prometheus"`
+ }
+ require.NoError(t, json.NewDecoder(resp.Body).Decode(&manifest))
+
+ var releases []ltsRelease
+ for _, r := range manifest.Prometheus {
+ if !r.LTS {
+ continue
+ }
+ var found bool
+ for _, f := range r.Files {
+ if f.Kind == "archive" && f.OS == runtime.GOOS && f.Arch == runtime.GOARCH {
+ releases = append(releases, ltsRelease{
+ version: strings.TrimPrefix(r.Version, "v"),
+ assetURL: f.URL,
+ })
+ found = true
+ break
+ }
+ }
+ require.True(t, found, "no archive found for current OS/arch in LTS release %s (os=%s arch=%s)", r.Version, runtime.GOOS, runtime.GOARCH)
+ }
+ require.NotEmpty(t, releases, "no LTS release found in download manifest %s", prometheusDownloadManifestURL)
+ return releases
+}
+
+func getPrometheusMetricValue(t *testing.T, port int, metricType model.MetricType, metricName string) (float64, error) {
+ t.Helper()
+
+ resp, err := http.Get(fmt.Sprintf("http://127.0.0.1:%d/metrics", port))
+ if err != nil {
+ return 0, err
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode != http.StatusOK {
+ return 0, fmt.Errorf("bad status code %d", resp.StatusCode)
+ }
+ return getMetricValue(t, resp.Body, metricType, metricName)
+}
+
+type versionChangeTest struct {
+ start time.Time
+
+ ltsAssetURL string
+ ltsVersionBinPath string
+
+ prometheusPort int
+ prometheusDataPath string
+ prometheusConfigFilePath string
+ rulesFilePath string
+ remoteWriteURL string
+}
+
+func (c versionChangeTest) downloadAndExtractLatestLTS(t *testing.T) {
+ const prometheusBinName = "prometheus"
+
+ resp, err := http.Get(c.ltsAssetURL)
+ require.NoError(t, err)
+ defer resp.Body.Close()
+ require.Equal(t, http.StatusOK, resp.StatusCode)
+
+ gzReader, err := gzip.NewReader(resp.Body)
+ require.NoError(t, err)
+ defer gzReader.Close()
+
+ tarReader := tar.NewReader(gzReader)
+ for {
+ header, err := tarReader.Next()
+ if err == io.EOF {
+ break
+ }
+ require.NoError(t, err)
+
+ if header.Typeflag == tar.TypeReg && filepath.Base(header.Name) == prometheusBinName {
+ out, err := os.OpenFile(c.ltsVersionBinPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o755)
+ require.NoError(t, err)
+ defer out.Close()
+
+ _, err = io.Copy(out, tarReader)
+ require.NoError(t, err)
+ return
+ }
+ }
+ require.FailNow(t, "prometheus binary not found in LTS tarball", "URL", c.ltsAssetURL)
+}
+
+// ensureHealthyMetrics polls metrics until all health invariants are satisfied. It checks
+// both liveness (expected operations completed) and safety (no failures).
+func (c versionChangeTest) ensureHealthyMetrics(t *testing.T) {
+ t.Helper()
+ checkStartTime := time.Now()
+
+ for _, mc := range []struct {
+ mType model.MetricType
+ mName string
+ check func(float64) bool
+ }{
+ {model.MetricTypeGauge, "prometheus_ready", func(v float64) bool { return v == 1 }},
+ {model.MetricTypeGauge, "prometheus_config_last_reload_successful", func(v float64) bool { return v == 1 }},
+
+ {model.MetricTypeCounter, "prometheus_target_scrape_pools_total", func(v float64) bool { return v == 3 }},
+ {model.MetricTypeCounter, "prometheus_target_scrape_pools_failed_total", func(v float64) bool { return v == 0 }},
+ {model.MetricTypeCounter, "prometheus_target_scrape_pool_reloads_failed_total", func(v float64) bool { return v == 0 }},
+
+ {model.MetricTypeGauge, "prometheus_remote_storage_highest_timestamp_in_seconds", func(v float64) bool { return v > float64(checkStartTime.Unix()) }},
+
+ {model.MetricTypeGauge, "prometheus_rule_group_rules", func(v float64) bool { return v == 2 }},
+ {model.MetricTypeCounter, "prometheus_rule_evaluations_total", func(v float64) bool { return v >= 1 }},
+ {model.MetricTypeCounter, "prometheus_rule_evaluation_failures_total", func(v float64) bool { return v == 0 }},
+
+ {model.MetricTypeCounter, "prometheus_tsdb_compactions_triggered_total", func(v float64) bool { return v >= 1 }},
+ {model.MetricTypeCounter, "prometheus_tsdb_compactions_total", func(v float64) bool { return v >= 1 }},
+ {model.MetricTypeCounter, "prometheus_tsdb_compactions_failed_total", func(v float64) bool { return v == 0 }},
+
+ {model.MetricTypeGauge, "prometheus_tsdb_head_series", func(v float64) bool { return v >= 1 }},
+ {model.MetricTypeGauge, "prometheus_tsdb_head_chunks", func(v float64) bool { return v >= 1 }},
+ {model.MetricTypeGauge, "prometheus_tsdb_head_chunks_storage_size_bytes", func(v float64) bool { return v >= 1 }},
+ {model.MetricTypeCounter, "prometheus_tsdb_head_series_created_total", func(v float64) bool { return v >= 1 }},
+ {model.MetricTypeGauge, "prometheus_tsdb_blocks_loaded", func(v float64) bool { return v >= 1 }},
+ {model.MetricTypeGauge, "prometheus_tsdb_storage_blocks_bytes", func(v float64) bool { return v >= 1 }},
+ {model.MetricTypeCounter, "prometheus_tsdb_reloads_total", func(v float64) bool { return v >= 1 }},
+ {model.MetricTypeCounter, "prometheus_tsdb_reloads_failures_total", func(v float64) bool { return v == 0 }},
+
+ {model.MetricTypeCounter, "prometheus_tsdb_head_chunks_created_total", func(v float64) bool { return v >= 1 }},
+ {model.MetricTypeCounter, "prometheus_tsdb_head_chunks_removed_total", func(v float64) bool { return v >= 1 }},
+ {model.MetricTypeCounter, "prometheus_tsdb_mmap_chunks_total", func(v float64) bool { return v >= 1 }},
+ {model.MetricTypeCounter, "prometheus_tsdb_mmap_chunk_corruptions_total", func(v float64) bool { return v == 0 }},
+
+ {model.MetricTypeCounter, "prometheus_tsdb_checkpoint_creations_total", func(v float64) bool { return v >= 1 }},
+ {model.MetricTypeCounter, "prometheus_tsdb_checkpoint_creations_failed_total", func(v float64) bool { return v == 0 }},
+ {model.MetricTypeCounter, "prometheus_tsdb_checkpoint_deletions_total", func(v float64) bool { return v >= 1 }},
+ {model.MetricTypeCounter, "prometheus_tsdb_checkpoint_deletions_failed_total", func(v float64) bool { return v == 0 }},
+
+ {model.MetricTypeCounter, "prometheus_tsdb_head_truncations_total", func(v float64) bool { return v >= 1 }},
+ {model.MetricTypeCounter, "prometheus_tsdb_head_truncations_failed_total", func(v float64) bool { return v == 0 }},
+
+ {model.MetricTypeGauge, "prometheus_tsdb_wal_storage_size_bytes", func(v float64) bool { return v >= 1 }},
+ {model.MetricTypeCounter, "prometheus_tsdb_wal_completed_pages_total", func(v float64) bool { return v >= 1 }},
+ {model.MetricTypeCounter, "prometheus_tsdb_wal_page_flushes_total", func(v float64) bool { return v >= 1 }},
+ {model.MetricTypeCounter, "prometheus_tsdb_wal_truncations_total", func(v float64) bool { return v >= 1 }},
+ {model.MetricTypeCounter, "prometheus_tsdb_wal_writes_failed_total", func(v float64) bool { return v == 0 }},
+ {model.MetricTypeCounter, "prometheus_tsdb_wal_corruptions_total", func(v float64) bool { return v == 0 }},
+ {model.MetricTypeCounter, "prometheus_tsdb_wal_truncations_failed_total", func(v float64) bool { return v == 0 }},
+ } {
+ require.Eventually(t, func() bool {
+ val, err := getPrometheusMetricValue(t, c.prometheusPort, mc.mType, mc.mName)
+ if err != nil {
+ return false
+ }
+ if mc.check(val) {
+ t.Logf("[%s] metric %s=%g is healthy", time.Since(c.start), mc.mName, val)
+ return true
+ }
+ return false
+ }, 7*time.Minute, 1*time.Second)
+ }
+}
+
+func (c versionChangeTest) generatePrometheusConfig(t *testing.T) {
+ t.Helper()
+
+ rulesContent := `
+groups:
+ - name: upgrade_test
+ interval: 3s
+ rules:
+ - record: upgrade_test:up:sum
+ expr: sum(up)
+ - alert: UpgradeTestAlwaysFiring
+ expr: vector(1)
+ for: 1s
+ labels:
+ severity: foo
+`
+ require.NoError(t, os.WriteFile(c.rulesFilePath, []byte(rulesContent), 0o644))
+
+ promConfig := fmt.Sprintf(`
+rule_files:
+ - %s
+
+scrape_configs:
+ - job_name: 'self1'
+ scrape_interval: 1s
+ static_configs:
+ - targets: ['localhost:%d']
+ - job_name: 'self2'
+ scrape_interval: 2s
+ static_configs:
+ - targets: ['localhost:%d']
+ - job_name: 'self3'
+ scrape_interval: 3s
+ static_configs:
+ - targets: ['localhost:%d']
+
+remote_write:
+ - url: %s`, c.rulesFilePath, c.prometheusPort, c.prometheusPort, c.prometheusPort, c.remoteWriteURL)
+
+ require.NoError(t, os.WriteFile(c.prometheusConfigFilePath, []byte(promConfig), 0o644))
+}
+
+// snapshotQueryRange queries all metrics over [c.start, end].
+// The result is deterministic and can be compared across Prometheus versions to detect data loss or corruption.
+func (c versionChangeTest) snapshotQueryRange(t *testing.T, end time.Time) string {
+ t.Helper()
+
+ u := fmt.Sprintf("http://127.0.0.1:%d/api/v1/query_range?query=%s&start=%d&end=%d&step=15",
+ c.prometheusPort, url.QueryEscape(`{__name__=~".+"}`), c.start.Unix(), end.Unix())
+ resp, err := http.Get(u)
+ require.NoError(t, err)
+ defer resp.Body.Close()
+ require.Equal(t, http.StatusOK, resp.StatusCode)
+
+ var parsed struct {
+ Data struct {
+ Result json.RawMessage `json:"result"`
+ } `json:"data"`
+ }
+ require.NoError(t, json.NewDecoder(resp.Body).Decode(&parsed))
+ require.NotEqual(t, "[]", string(parsed.Data.Result))
+ return string(parsed.Data.Result)
+}
+
+func ensureHealthyLogs(t *testing.T, r io.Reader) {
+ scanner := bufio.NewScanner(r)
+ for scanner.Scan() {
+ line := scanner.Text()
+ t.Log(line)
+ require.NotContains(t, line, "level=ERROR")
+ }
+ require.NoError(t, scanner.Err())
+}
+
+var (
+ testVersionUpgrade = flag.Bool("test.version-upgrade", false, "run resource-intensive and probably slow version upgrade tests")
+ testLTSVersion = flag.String("test.lts-version", "", "restrict the version upgrade test to a single LTS version (e.g. 3.5.0); if empty, all active LTS releases are tested")
+)
+
+// TestVersionUpgrade_UpgradeDowngradeLatestLTS verifies that Prometheus can
+// upgrade from each current LTS release to the current build and then downgrade
+// back without errors (data loss, corruption, etc.). There may be more than one
+// active LTS release, in which case each is tested independently.
+//
+// NOTE: If this test is renamed, update the corresponding invocation in CI.
+func TestVersionUpgrade_UpgradeDowngradeLatestLTS(t *testing.T) {
+ if !*testVersionUpgrade {
+ t.Skip("test can be slow, resource-intensive or requires internet access")
+ }
+
+ start := time.Now()
+
+ ltsReleases := fetchLTSReleases(t)
+ t.Logf("[%s] found %d LTS release(s) from %s", time.Since(start), len(ltsReleases), prometheusDownloadManifestURL)
+
+ if *testLTSVersion != "" {
+ want := strings.TrimPrefix(*testLTSVersion, "v")
+ filtered := ltsReleases[:0]
+ for _, lts := range ltsReleases {
+ if lts.version == want {
+ filtered = append(filtered, lts)
+ }
+ }
+ require.NotEmpty(t, filtered, "LTS version %q not found among active LTS releases in %s", want, prometheusDownloadManifestURL)
+ ltsReleases = filtered
+ }
+
+ for _, lts := range ltsReleases {
+ t.Run(lts.version, func(t *testing.T) {
+ testUpgradeDowngradeLTS(t, start, lts)
+ })
+ }
+}
+
+func testUpgradeDowngradeLTS(t *testing.T, start time.Time, lts ltsRelease) {
+ rootDir := t.TempDir()
+
+ t.Logf("[%s] using LTS %s from %s", time.Since(start), lts.version, lts.assetURL)
+
+ rwServer := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}))
+ t.Cleanup(rwServer.Close)
+
+ c := versionChangeTest{
+ start: start,
+
+ ltsAssetURL: lts.assetURL,
+
+ ltsVersionBinPath: filepath.Join(rootDir, fmt.Sprintf("prometheus-%s", lts.version)),
+
+ prometheusPort: testutil.RandomUnprivilegedPort(t),
+ prometheusDataPath: filepath.Join(rootDir, "data"),
+ prometheusConfigFilePath: filepath.Join(rootDir, "prometheus.yml"),
+ rulesFilePath: filepath.Join(rootDir, "rules.yml"),
+ remoteWriteURL: rwServer.URL,
+ }
+
+ t.Logf("[%s] downloading and preparing LTS %s", time.Since(start), lts.version)
+ // TODO: Cache if downloads are expensive in CI.
+ c.downloadAndExtractLatestLTS(t)
+ t.Logf("[%s] downloaded and prepared LTS %s at %s", time.Since(start), lts.version, c.ltsVersionBinPath)
+
+ c.generatePrometheusConfig(t)
+
+ commonArgs := []string{
+ fmt.Sprintf("--config.file=%s", c.prometheusConfigFilePath),
+ fmt.Sprintf("--web.listen-address=0.0.0.0:%d", c.prometheusPort),
+ fmt.Sprintf("--storage.tsdb.path=%s", c.prometheusDataPath),
+ // Accelerate compaction.
+ "--storage.tsdb.min-block-duration=65s",
+ // Accelerate chunks mmapping.
+ "--storage.tsdb.samples-per-chunk=10",
+ "--log.level=debug",
+ }
+
+ runLTS := append([]string{c.ltsVersionBinPath}, commonArgs...)
+ runCurrent := append([]string{os.Args[0], "-test.main"}, commonArgs...)
+
+ var (
+ queryRangeSnapshotEnd time.Time
+ queryRangeBaseline string
+ )
+
+ t.Run("Running the LTS version", func(t *testing.T) {
+ cmd := commandWithLogging(t, ensureHealthyLogs, runLTS[0], runLTS[1:]...)
+ require.NoError(t, cmd.Start())
+ c.ensureHealthyMetrics(t)
+ queryRangeSnapshotEnd = time.Now().Add(-3 * time.Second)
+ queryRangeBaseline = c.snapshotQueryRange(t, queryRangeSnapshotEnd)
+ })
+
+ t.Run("Upgrading to the current build", func(t *testing.T) {
+ cmd := commandWithLogging(t, ensureHealthyLogs, runCurrent[0], runCurrent[1:]...)
+ require.NoError(t, cmd.Start())
+ c.ensureHealthyMetrics(t)
+ require.Equal(t, queryRangeBaseline, c.snapshotQueryRange(t, queryRangeSnapshotEnd))
+ // Take a new snapshot after the upgrade.
+ queryRangeSnapshotEnd = time.Now().Add(-3 * time.Second)
+ queryRangeBaseline = c.snapshotQueryRange(t, queryRangeSnapshotEnd)
+ })
+
+ t.Run("Downgrading to the LTS version", func(t *testing.T) {
+ cmd := commandWithLogging(t, ensureHealthyLogs, runLTS[0], runLTS[1:]...)
+ require.NoError(t, cmd.Start())
+ c.ensureHealthyMetrics(t)
+ require.Equal(t, queryRangeBaseline, c.snapshotQueryRange(t, queryRangeSnapshotEnd))
+ })
+}
diff --git a/cmd/prometheus/query_log_test.go b/cmd/prometheus/query_log_test.go
index 62e317bf8b1..b3fd5e1bdb3 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
@@ -68,16 +68,16 @@ func (p *queryLogTest) skip(t *testing.T) {
}
// waitForPrometheus waits for Prometheus to be ready.
-func (p *queryLogTest) waitForPrometheus() error {
- var err error
- for x := 0; x < 20; x++ {
- var r *http.Response
- if r, err = http.Get(fmt.Sprintf("http://%s:%d%s/-/ready", p.host, p.port, p.prefix)); err == nil && r.StatusCode == http.StatusOK {
- break
+func (p *queryLogTest) waitForPrometheus(t *testing.T) {
+ t.Helper()
+ require.Eventually(t, func() bool {
+ r, err := http.Get(fmt.Sprintf("http://%s:%d%s/-/ready", p.host, p.port, p.prefix))
+ if err != nil {
+ return false
}
- time.Sleep(500 * time.Millisecond)
- }
- return err
+ r.Body.Close()
+ return r.StatusCode == http.StatusOK
+ }, 20*time.Second, 500*time.Millisecond, "prometheus at %s:%d did not become ready in time", p.host, p.port)
}
// setQueryLog alters the configuration file to enable or disable the query log,
@@ -88,20 +88,13 @@ func (p *queryLogTest) setQueryLog(t *testing.T, queryLogFile string) {
_, err = p.configFile.Seek(0, 0)
require.NoError(t, err)
if queryLogFile != "" {
- _, err = p.configFile.Write([]byte(fmt.Sprintf("global:\n query_log_file: %s\n", queryLogFile)))
+ _, err = fmt.Fprintf(p.configFile, "global:\n query_log_file: %s\n", queryLogFile)
require.NoError(t, err)
}
_, err = p.configFile.Write([]byte(p.configuration()))
require.NoError(t, err)
}
-// reloadConfig reloads the configuration using POST.
-func (p *queryLogTest) reloadConfig(t *testing.T) {
- r, err := http.Post(fmt.Sprintf("http://%s:%d%s/-/reload", p.host, p.port, p.prefix), "text/plain", nil)
- require.NoError(t, err)
- require.Equal(t, 200, r.StatusCode)
-}
-
// query runs a query according to the test origin.
func (p *queryLogTest) query(t *testing.T) {
switch p.origin {
@@ -125,12 +118,61 @@ func (p *queryLogTest) query(t *testing.T) {
require.NoError(t, err)
require.Equal(t, 200, r.StatusCode)
case ruleOrigin:
- time.Sleep(2 * time.Second)
+ // Poll the /api/v1/rules endpoint until a new rule evaluation is detected.
+ var lastEvalTime time.Time
+ for {
+ r, err := http.Get(fmt.Sprintf("http://%s:%d/api/v1/rules", p.host, p.port))
+ require.NoError(t, err)
+
+ rulesBody, err := io.ReadAll(r.Body)
+ require.NoError(t, err)
+ defer r.Body.Close()
+
+ // Parse the rules response to find the last evaluation time.
+ newEvalTime := parseLastEvaluation(rulesBody)
+ if newEvalTime.After(lastEvalTime) {
+ if !lastEvalTime.IsZero() {
+ break
+ }
+ lastEvalTime = newEvalTime
+ }
+
+ time.Sleep(100 * time.Millisecond)
+ }
default:
panic("can't query this origin")
}
}
+// parseLastEvaluation extracts the last evaluation timestamp from the /api/v1/rules response.
+func parseLastEvaluation(rulesBody []byte) time.Time {
+ var ruleResponse struct {
+ Status string `json:"status"`
+ Data struct {
+ Groups []struct {
+ Rules []struct {
+ LastEvaluation string `json:"lastEvaluation"`
+ } `json:"rules"`
+ } `json:"groups"`
+ } `json:"data"`
+ }
+
+ err := json.Unmarshal(rulesBody, &ruleResponse)
+ if err != nil {
+ return time.Time{}
+ }
+
+ for _, group := range ruleResponse.Data.Groups {
+ for _, rule := range group.Rules {
+ if evalTime, err := time.Parse(time.RFC3339Nano, rule.LastEvaluation); err == nil {
+ return evalTime
+ }
+ }
+ }
+
+ return time.Time{}
+}
+
// queryString returns the expected queryString of a this test.
func (p *queryLogTest) queryString() string {
switch p.origin {
@@ -208,7 +250,7 @@ func (p *queryLogTest) params() []string {
s = append(s, "--web.route-prefix="+p.prefix)
}
if p.origin == consoleOrigin {
- s = append(s, "--web.console.templates="+filepath.Join("testdata", "consoles"))
+ s = append(s, "--web.console.templates="+filepath.Join(p.cwd, "testdata", "consoles"))
}
return s
}
@@ -259,6 +301,7 @@ func (p *queryLogTest) run(t *testing.T) {
}, p.params()...)
prom := exec.Command(promPath, params...)
+ reloadURL := fmt.Sprintf("http://%s:%d%s/-/reload", p.host, p.port, p.prefix)
// Log stderr in case of failure.
stderr, err := prom.StderrPipe()
@@ -280,18 +323,19 @@ func (p *queryLogTest) run(t *testing.T) {
prom.Process.Kill()
prom.Wait()
}()
- require.NoError(t, p.waitForPrometheus())
+ p.waitForPrometheus(t)
if !p.enabledAtStart {
p.query(t)
require.Empty(t, readQueryLog(t, queryLogFile.Name()))
p.setQueryLog(t, queryLogFile.Name())
- p.reloadConfig(t)
+ reloadPrometheusConfig(t, reloadURL)
}
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)
@@ -301,7 +345,7 @@ func (p *queryLogTest) run(t *testing.T) {
p.validateLastQuery(t, ql)
p.setQueryLog(t, "")
- p.reloadConfig(t)
+ reloadPrometheusConfig(t, reloadURL)
if !p.exactQueryCount() {
qc = len(readQueryLog(t, queryLogFile.Name()))
}
@@ -313,16 +357,17 @@ func (p *queryLogTest) run(t *testing.T) {
qc = len(ql)
p.setQueryLog(t, queryLogFile.Name())
- p.reloadConfig(t)
+ reloadPrometheusConfig(t, reloadURL)
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 {
- require.Greater(t, len(ql), qc, "no queries logged")
+ require.GreaterOrEqual(t, len(ql), qc, "no queries logged")
}
p.validateLastQuery(t, ql)
qc = len(ql)
@@ -349,19 +394,21 @@ 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 {
- require.Greater(t, len(ql), qc, "no queries logged")
+ require.GreaterOrEqual(t, len(ql), qc, "no queries logged")
}
p.validateLastQuery(t, ql)
- p.reloadConfig(t)
+ reloadPrometheusConfig(t, reloadURL)
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)
@@ -393,6 +440,7 @@ func readQueryLog(t *testing.T, path string) []queryLogLine {
file, err := os.Open(path)
require.NoError(t, err)
defer file.Close()
+
scanner := bufio.NewScanner(file)
for scanner.Scan() {
var q queryLogLine
@@ -402,10 +450,23 @@ 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.")
}
+ t.Parallel()
cwd, err := os.Getwd()
require.NoError(t, err)
@@ -424,6 +485,7 @@ func TestQueryLog(t *testing.T) {
}
t.Run(p.String(), func(t *testing.T) {
+ t.Parallel()
p.run(t)
})
}
diff --git a/cmd/prometheus/reload_test.go b/cmd/prometheus/reload_test.go
new file mode 100644
index 00000000000..1a75b410cf3
--- /dev/null
+++ b/cmd/prometheus/reload_test.go
@@ -0,0 +1,243 @@
+// Copyright The Prometheus Authors
+// Licensed under the Apache License, Version 2.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 (
+ "bufio"
+ "encoding/json"
+ "io"
+ "net/http"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "sync"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/prometheus/prometheus/util/testutil"
+)
+
+const configReloadMetric = "prometheus_config_last_reload_successful"
+
+func TestAutoReloadConfig_ValidToValid(t *testing.T) {
+ steps := []struct {
+ configText string
+ expectedInterval string
+ expectedMetric float64
+ }{
+ {
+ configText: `
+global:
+ scrape_interval: 30s
+`,
+ expectedInterval: "30s",
+ expectedMetric: 1,
+ },
+ {
+ configText: `
+global:
+ scrape_interval: 15s
+`,
+ expectedInterval: "15s",
+ expectedMetric: 1,
+ },
+ {
+ configText: `
+global:
+ scrape_interval: 30s
+`,
+ expectedInterval: "30s",
+ expectedMetric: 1,
+ },
+ }
+
+ runTestSteps(t, steps)
+}
+
+func TestAutoReloadConfig_ValidToInvalidToValid(t *testing.T) {
+ steps := []struct {
+ configText string
+ expectedInterval string
+ expectedMetric float64
+ }{
+ {
+ configText: `
+global:
+ scrape_interval: 30s
+`,
+ expectedInterval: "30s",
+ expectedMetric: 1,
+ },
+ {
+ configText: `
+global:
+ scrape_interval: 15s
+invalid_syntax
+`,
+ expectedInterval: "30s",
+ expectedMetric: 0,
+ },
+ {
+ configText: `
+global:
+ scrape_interval: 30s
+`,
+ expectedInterval: "30s",
+ expectedMetric: 1,
+ },
+ }
+
+ runTestSteps(t, steps)
+}
+
+func runTestSteps(t *testing.T, steps []struct {
+ configText string
+ expectedInterval string
+ expectedMetric float64
+},
+) {
+ configDir := t.TempDir()
+ configFilePath := filepath.Join(configDir, "prometheus.yml")
+
+ t.Logf("Config file path: %s", configFilePath)
+
+ require.NoError(t, os.WriteFile(configFilePath, []byte(steps[0].configText), 0o644), "Failed to write initial config file")
+
+ port := testutil.RandomUnprivilegedPort(t)
+ prom := prometheusCommandWithLogging(t, configFilePath, port, "--config.auto-reload", "--config.auto-reload-interval=1s")
+ require.NoError(t, prom.Start())
+
+ baseURL := "http://localhost:" + strconv.Itoa(port)
+ 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
+ }, 5*time.Second, 100*time.Millisecond, "Prometheus didn't become ready in time")
+
+ for i, step := range steps {
+ t.Logf("Step %d", i)
+ require.NoError(t, os.WriteFile(configFilePath, []byte(step.configText), 0o644), "Failed to write config file for step")
+
+ require.Eventually(t, func() bool {
+ return verifyScrapeInterval(t, baseURL, step.expectedInterval) &&
+ verifyConfigReloadMetric(t, baseURL, step.expectedMetric)
+ }, 10*time.Second, 500*time.Millisecond, "Prometheus config reload didn't happen in time")
+ }
+}
+
+func verifyScrapeInterval(t *testing.T, baseURL, expectedInterval string) bool {
+ resp, err := http.Get(baseURL + "/api/v1/status/config")
+ require.NoError(t, err)
+ defer resp.Body.Close()
+
+ body, err := io.ReadAll(resp.Body)
+ require.NoError(t, err)
+
+ config := struct {
+ Data struct {
+ YAML string `json:"yaml"`
+ } `json:"data"`
+ }{}
+
+ require.NoError(t, json.Unmarshal(body, &config))
+ return strings.Contains(config.Data.YAML, "scrape_interval: "+expectedInterval)
+}
+
+func verifyConfigReloadMetric(t *testing.T, baseURL string, expectedValue float64) bool {
+ resp, err := http.Get(baseURL + "/metrics")
+ require.NoError(t, err)
+ defer resp.Body.Close()
+
+ body, err := io.ReadAll(resp.Body)
+ require.NoError(t, err)
+
+ lines := string(body)
+ var actualValue float64
+ found := false
+
+ for line := range strings.SplitSeq(lines, "\n") {
+ if strings.HasPrefix(line, configReloadMetric) {
+ parts := strings.Fields(line)
+ if len(parts) >= 2 {
+ actualValue, err = strconv.ParseFloat(parts[1], 64)
+ require.NoError(t, err)
+ found = true
+ break
+ }
+ }
+ }
+
+ return found && actualValue == expectedValue
+}
+
+func captureLogsToTLog(t *testing.T, r io.Reader) {
+ scanner := bufio.NewScanner(r)
+ for scanner.Scan() {
+ t.Log(scanner.Text())
+ }
+ if err := scanner.Err(); err != nil {
+ t.Logf("Error reading logs: %v", err)
+ }
+}
+
+func commandWithLogging(t *testing.T, logProcessor func(*testing.T, io.Reader), name string, args ...string) *exec.Cmd {
+ if logProcessor == nil {
+ logProcessor = captureLogsToTLog
+ }
+
+ stdoutPipe, stdoutWriter := io.Pipe()
+ stderrPipe, stderrWriter := io.Pipe()
+
+ var wg sync.WaitGroup
+ wg.Add(2)
+
+ cmd := exec.Command(name, args...)
+ cmd.Stdout = stdoutWriter
+ cmd.Stderr = stderrWriter
+
+ go func() {
+ defer wg.Done()
+ logProcessor(t, stdoutPipe)
+ }()
+ go func() {
+ defer wg.Done()
+ logProcessor(t, stderrPipe)
+ }()
+
+ t.Cleanup(func() {
+ cmd.Process.Kill()
+ cmd.Wait()
+ stdoutWriter.Close()
+ stderrWriter.Close()
+ wg.Wait()
+ })
+ return cmd
+}
+
+func prometheusCommandWithLogging(t *testing.T, configFilePath string, port int, extraArgs ...string) *exec.Cmd {
+ args := []string{
+ "-test.main",
+ "--config.file=" + configFilePath,
+ "--web.listen-address=0.0.0.0:" + strconv.Itoa(port),
+ }
+ args = append(args, extraArgs...)
+ return commandWithLogging(t, nil, promPath, args...)
+}
diff --git a/cmd/prometheus/scrape_failure_log_test.go b/cmd/prometheus/scrape_failure_log_test.go
index 8d86d719f98..c3f459f6014 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
@@ -112,7 +112,8 @@ scrape_configs:
require.NoError(t, err, "Failed to update Prometheus configuration file")
// Reload Prometheus with the updated configuration.
- reloadPrometheus(t, port)
+ reloadURL := fmt.Sprintf("http://127.0.0.1:%d/-/reload", port)
+ reloadPrometheusConfig(t, reloadURL)
// Count the number of lines in the scrape failure log file before any
// further requests.
@@ -146,7 +147,7 @@ scrape_configs:
require.NoError(t, err, "Failed to update Prometheus configuration file")
// Reload Prometheus with the updated configuration.
- reloadPrometheus(t, port)
+ reloadPrometheusConfig(t, reloadURL)
// Wait for at least two more requests to the mock server and verify that
// new log entries are created.
@@ -160,18 +161,10 @@ scrape_configs:
require.Greater(t, countLinesInFile(scrapeFailureLogFile), postReloadLogLineCount, "New lines should be added to the scrape failure log file after re-adding the log setting")
}
-// reloadPrometheus sends a reload request to the Prometheus server to apply
-// updated configurations.
-func reloadPrometheus(t *testing.T, port int) {
- resp, err := http.Post(fmt.Sprintf("http://127.0.0.1:%d/-/reload", port), "", nil)
- require.NoError(t, err, "Failed to reload Prometheus")
- require.Equal(t, http.StatusOK, resp.StatusCode, "Unexpected status code when reloading Prometheus")
-}
-
// startGarbageServer sets up a mock server that returns a 500 Internal Server Error
// for all requests. It also increments the request count each time it's hit.
func startGarbageServer(t *testing.T, requestCount *atomic.Int32) string {
- server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
requestCount.Inc()
w.WriteHeader(http.StatusInternalServerError)
}))
diff --git a/cmd/prometheus/testdata/features.json b/cmd/prometheus/testdata/features.json
new file mode 100644
index 00000000000..ae7490c5b7d
--- /dev/null
+++ b/cmd/prometheus/testdata/features.json
@@ -0,0 +1,272 @@
+{
+ "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,
+ "search": false,
+ "search_fuzz_alg_jarowinkler": true,
+ "search_fuzz_alg_subsequence": true,
+ "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,
+ "end": 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_of": false,
+ "max_over_time": true,
+ "min_of": false,
+ "min_over_time": true,
+ "minute": true,
+ "month": true,
+ "pi": true,
+ "predict_linear": true,
+ "present_over_time": true,
+ "quantile_over_time": true,
+ "rad": true,
+ "range": false,
+ "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,
+ "start": false,
+ "stddev_over_time": true,
+ "stdvar_over_time": true,
+ "step": false,
+ "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,
+ "@": 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,
+ "outscale": true,
+ "ovhcloud": true,
+ "puppetdb": true,
+ "rds": 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,
+ "st_storage": false,
+ "xor2_encoding": false,
+ "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 00000000000..97a98351a70
--- /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 c1f523de525..a725772f5dc 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
@@ -34,8 +34,8 @@ import (
)
var (
- errNotNativeHistogram = fmt.Errorf("not a native histogram")
- errNotEnoughData = fmt.Errorf("not enough data")
+ errNotNativeHistogram = errors.New("not a native histogram")
+ errNotEnoughData = errors.New("not enough data")
outputHeader = `Bucket stats for each histogram series over time
------------------------------------------------
@@ -169,7 +169,7 @@ func querySamples(ctx context.Context, api v1.API, query string, end time.Time)
matrix, ok := values.(model.Matrix)
if !ok {
- return nil, fmt.Errorf("query of buckets resulted in non-Matrix")
+ return nil, errors.New("query of buckets resulted in non-Matrix")
}
return matrix, nil
@@ -207,7 +207,7 @@ func calcClassicBucketStatistics(matrix model.Matrix) (*statistics, error) {
sortMatrix(matrix)
totalPop := 0
- for timeIdx := 0; timeIdx < numSamples; timeIdx++ {
+ for timeIdx := range numSamples {
curr, err := getBucketCountsAtTime(matrix, numBuckets, timeIdx)
if err != nil {
return stats, err
@@ -259,7 +259,7 @@ func getBucketCountsAtTime(matrix model.Matrix, numBuckets, timeIdx int) ([]int,
prev := matrix[i].Values[timeIdx]
// Assume the results are nicely aligned.
if curr.Timestamp != prev.Timestamp {
- return counts, fmt.Errorf("matrix result is not time aligned")
+ return counts, errors.New("matrix result is not time aligned")
}
counts[i+1] = int(curr.Value - prev.Value)
}
diff --git a/cmd/promtool/analyze_test.go b/cmd/promtool/analyze_test.go
index 83d2ac4a3db..d2e81da2c8c 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
@@ -17,9 +17,8 @@ import (
"fmt"
"testing"
- "github.com/stretchr/testify/require"
-
"github.com/prometheus/common/model"
+ "github.com/stretchr/testify/require"
)
var (
@@ -109,6 +108,7 @@ func init() {
}
func TestGetBucketCountsAtTime(t *testing.T) {
+ t.Parallel()
cases := []struct {
matrix model.Matrix
length int
@@ -137,6 +137,7 @@ func TestGetBucketCountsAtTime(t *testing.T) {
for _, c := range cases {
t.Run(fmt.Sprintf("exampleMatrix@%d", c.timeIdx), func(t *testing.T) {
+ t.Parallel()
res, err := getBucketCountsAtTime(c.matrix, c.length, c.timeIdx)
require.NoError(t, err)
require.Equal(t, c.expected, res)
@@ -145,6 +146,7 @@ func TestGetBucketCountsAtTime(t *testing.T) {
}
func TestCalcClassicBucketStatistics(t *testing.T) {
+ t.Parallel()
cases := []struct {
matrix model.Matrix
expected *statistics
@@ -162,6 +164,7 @@ func TestCalcClassicBucketStatistics(t *testing.T) {
for i, c := range cases {
t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
+ t.Parallel()
res, err := calcClassicBucketStatistics(c.matrix)
require.NoError(t, err)
require.Equal(t, c.expected, res)
diff --git a/cmd/promtool/archive.go b/cmd/promtool/archive.go
index 7b565c57cc2..23baea27003 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 16491f0416f..e7a9a7f18ab 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
@@ -21,13 +21,12 @@ import (
"math"
"time"
- "github.com/go-kit/log"
- "github.com/oklog/ulid"
+ "github.com/oklog/ulid/v2"
+ "github.com/prometheus/common/promslog"
"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) {
@@ -48,7 +47,7 @@ func getMinAndMaxTimestamps(p textparse.Parser) (int64, int64, error) {
_, ts, _ := p.Series()
if ts == nil {
- return 0, 0, fmt.Errorf("expected timestamp for series got none")
+ return 0, 0, errors.New("expected timestamp for series got none")
}
if *ts > maxt {
@@ -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 (
@@ -120,12 +119,12 @@ func createBlocks(input []byte, mint, maxt, maxBlockDuration int64, maxSamplesIn
// also need to append samples throughout the whole block range. To allow that, we
// pretend that the block is twice as large here, but only really add sample in the
// original interval later.
- w, err := tsdb.NewBlockWriter(log.NewNopLogger(), outputDir, 2*blockDuration)
+ w, err := tsdb.NewBlockWriter(promslog.NewNopLogger(), outputDir, 2*blockDuration)
if err != nil {
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()
@@ -148,7 +147,7 @@ func createBlocks(input []byte, mint, maxt, maxBlockDuration int64, maxSamplesIn
_, ts, v := p.Series()
if ts == nil {
l := labels.Labels{}
- p.Metric(&l)
+ p.Labels(&l)
return fmt.Errorf("expected timestamp for series %v, got none", l)
}
if *ts < t {
@@ -162,7 +161,7 @@ func createBlocks(input []byte, mint, maxt, maxBlockDuration int64, maxSamplesIn
}
l := labels.Labels{}
- p.Metric(&l)
+ p.Labels(&l)
lb.Reset(l)
for name, value := range customLabels {
diff --git a/cmd/promtool/backfill_test.go b/cmd/promtool/backfill_test.go
index b818194e865..499b90e99aa 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
@@ -45,7 +45,7 @@ func sortSamples(samples []backfillSample) {
})
}
-func queryAllSeries(t testing.TB, q storage.Querier, expectedMinTime, expectedMaxTime int64) []backfillSample {
+func queryAllSeries(t testing.TB, q storage.Querier, _, _ int64) []backfillSample {
ss := q.Select(context.Background(), false, nil, labels.MustNewMatcher(labels.MatchRegexp, "", ".*"))
samples := []backfillSample{}
for ss.Next() {
@@ -86,6 +86,7 @@ func testBlocks(t *testing.T, db *tsdb.DB, expectedMinTime, expectedMaxTime, exp
}
func TestBackfill(t *testing.T) {
+ t.Parallel()
tests := []struct {
ToParse string
IsOk bool
@@ -729,6 +730,7 @@ after_eof 1 2
}
for _, test := range tests {
t.Run(test.Description, func(t *testing.T) {
+ t.Parallel()
t.Logf("Test:%s", test.Description)
outputDir := t.TempDir()
diff --git a/cmd/promtool/debug.go b/cmd/promtool/debug.go
index 6383aaface0..b6e82ef981f 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 d1b6c0fcd98..a6dd4e0c05b 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
@@ -20,6 +20,7 @@ import (
"errors"
"fmt"
"io"
+ "log/slog"
"math"
"net/http"
"net/url"
@@ -32,20 +33,18 @@ import (
"time"
"github.com/alecthomas/kingpin/v2"
- "github.com/go-kit/log"
"github.com/google/pprof/profile"
"github.com/prometheus/client_golang/api"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/testutil/promlint"
- config_util "github.com/prometheus/common/config"
- "github.com/prometheus/common/model"
- "github.com/prometheus/common/version"
- "github.com/prometheus/exporter-toolkit/web"
- "gopkg.in/yaml.v2"
-
dto "github.com/prometheus/client_model/go"
promconfig "github.com/prometheus/common/config"
"github.com/prometheus/common/expfmt"
+ "github.com/prometheus/common/model"
+ "github.com/prometheus/common/promslog"
+ "github.com/prometheus/common/version"
+ "github.com/prometheus/exporter-toolkit/web"
+ "go.yaml.in/yaml/v2"
"github.com/prometheus/prometheus/config"
"github.com/prometheus/prometheus/discovery"
@@ -58,24 +57,44 @@ import (
_ "github.com/prometheus/prometheus/plugins" // Register plugins.
"github.com/prometheus/prometheus/promql/parser"
"github.com/prometheus/prometheus/promql/promqltest"
+ "github.com/prometheus/prometheus/rules"
"github.com/prometheus/prometheus/scrape"
"github.com/prometheus/prometheus/util/documentcli"
)
+var (
+ promqlEnableDelayedNameRemoval = false
+ promtoolParserOpts parser.Options
+ logger = promslog.New(&promslog.Config{})
+)
+
+func init() {
+ // This can be removed when the legacy global mode is fully deprecated.
+ //nolint:staticcheck
+ model.NameValidationScheme = model.UTF8Validation
+}
+
const (
successExitCode = 0
failureExitCode = 1
// Exit code 3 is used for "one or more lint issues detected".
lintErrExitCode = 3
- lintOptionAll = "all"
- lintOptionDuplicateRules = "duplicate-rules"
- lintOptionNone = "none"
- checkHealth = "/-/healthy"
- checkReadiness = "/-/ready"
+ lintOptionAll = "all"
+ lintOptionDuplicateRules = "duplicate-rules"
+ lintOptionTooLongScrapeInterval = "too-long-scrape-interval"
+ lintOptionNone = "none"
+ checkHealth = "/-/healthy"
+ checkReadiness = "/-/ready"
)
-var lintOptions = []string{lintOptionAll, lintOptionDuplicateRules, lintOptionNone}
+var (
+ lintRulesOptions = []string{lintOptionAll, lintOptionDuplicateRules, lintOptionNone}
+ // Same as lintRulesOptions, but including scrape config linting options as well.
+ lintConfigOptions = append(append([]string{}, lintRulesOptions...), lintOptionTooLongScrapeInterval)
+)
+
+const httpConfigFileDescription = "HTTP client configuration file, see details at https://prometheus.io/docs/prometheus/latest/configuration/promtool"
func main() {
var (
@@ -92,6 +111,10 @@ func main() {
app.HelpFlag.Short('h')
checkCmd := app.Command("check", "Check the resources for validity.")
+ checkLookbackDelta := checkCmd.Flag(
+ "query.lookback-delta",
+ "The server's maximum query lookback duration.",
+ ).Default("5m").Duration()
experimental := app.Flag("experimental", "Enable experimental commands.").Bool()
@@ -108,11 +131,12 @@ func main() {
checkConfigSyntaxOnly := checkConfigCmd.Flag("syntax-only", "Only check the config file syntax, ignoring file and content validation referenced in the config").Bool()
checkConfigLint := checkConfigCmd.Flag(
"lint",
- "Linting checks to apply to the rules specified in the config. Available options are: "+strings.Join(lintOptions, ", ")+". Use --lint=none to disable linting",
+ "Linting checks to apply to the rules/scrape configs specified in the config. Available options are: "+strings.Join(lintConfigOptions, ", ")+". Use --lint=none to disable linting",
).Default(lintOptionDuplicateRules).String()
checkConfigLintFatal := checkConfigCmd.Flag(
"lint-fatal",
"Make lint errors exit with exit code 3.").Default("false").Bool()
+ checkConfigIgnoreUnknownFields := checkConfigCmd.Flag("ignore-unknown-fields", "Ignore unknown fields in the rule groups read by the config 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()
checkWebConfigCmd := checkCmd.Command("web-config", "Check if the web config files are valid or not.")
webConfigFiles := checkWebConfigCmd.Arg(
@@ -121,11 +145,11 @@ func main() {
).Required().ExistingFiles()
checkServerHealthCmd := checkCmd.Command("healthy", "Check if the Prometheus server is healthy.")
- checkServerHealthCmd.Flag("http.config.file", "HTTP client configuration file for promtool to connect to Prometheus.").PlaceHolder("").ExistingFileVar(&httpConfigFilePath)
+ checkServerHealthCmd.Flag("http.config.file", httpConfigFileDescription).PlaceHolder("").ExistingFileVar(&httpConfigFilePath)
checkServerHealthCmd.Flag("url", "The URL for the Prometheus server.").Default("http://localhost:9090").URLVar(&serverURL)
checkServerReadyCmd := checkCmd.Command("ready", "Check if the Prometheus server is ready.")
- checkServerReadyCmd.Flag("http.config.file", "HTTP client configuration file for promtool to connect to Prometheus.").PlaceHolder("").ExistingFileVar(&httpConfigFilePath)
+ checkServerReadyCmd.Flag("http.config.file", httpConfigFileDescription).PlaceHolder("").ExistingFileVar(&httpConfigFilePath)
checkServerReadyCmd.Flag("url", "The URL for the Prometheus server.").Default("http://localhost:9090").URLVar(&serverURL)
checkRulesCmd := checkCmd.Command("rules", "Check if the rule files are valid or not.")
@@ -135,24 +159,30 @@ func main() {
).ExistingFiles()
checkRulesLint := checkRulesCmd.Flag(
"lint",
- "Linting checks to apply. Available options are: "+strings.Join(lintOptions, ", ")+". Use --lint=none to disable linting",
+ "Linting checks to apply. Available options are: "+strings.Join(lintRulesOptions, ", ")+". Use --lint=none to disable linting",
).Default(lintOptionDuplicateRules).String()
checkRulesLintFatal := checkRulesCmd.Flag(
"lint-fatal",
"Make lint errors exit with exit code 3.").Default("false").Bool()
+ 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.")
queryCmdFmt := queryCmd.Flag("format", "Output format of the query.").Short('o').Default("promql").Enum("promql", "json")
- queryCmd.Flag("http.config.file", "HTTP client configuration file for promtool to connect to Prometheus.").PlaceHolder("").ExistingFileVar(&httpConfigFilePath)
+ queryCmd.Flag("http.config.file", httpConfigFileDescription).PlaceHolder("").ExistingFileVar(&httpConfigFilePath)
queryInstantCmd := queryCmd.Command("instant", "Run instant query.")
queryInstantCmd.Arg("server", "Prometheus server to query.").Required().URLVar(&serverURL)
queryInstantExpr := queryInstantCmd.Arg("expr", "PromQL query expression.").Required().String()
queryInstantTime := queryInstantCmd.Flag("time", "Query evaluation time (RFC3339 or Unix timestamp).").String()
+ queryInstantHeaders := queryInstantCmd.Flag("header", "Extra headers to send to server.").StringMap()
queryRangeCmd := queryCmd.Command("range", "Run range query.")
queryRangeCmd.Arg("server", "Prometheus server to query.").Required().URLVar(&serverURL)
@@ -192,7 +222,7 @@ func main() {
queryAnalyzeCmd.Flag("match", "Series selector. Can be specified multiple times.").Required().StringsVar(&queryAnalyzeCfg.matchers)
pushCmd := app.Command("push", "Push to a Prometheus server.")
- pushCmd.Flag("http.config.file", "HTTP client configuration file for promtool to connect to Prometheus.").PlaceHolder("").ExistingFileVar(&httpConfigFilePath)
+ pushCmd.Flag("http.config.file", httpConfigFileDescription).PlaceHolder("").ExistingFileVar(&httpConfigFilePath)
pushMetricsCmd := pushCmd.Command("metrics", "Push metrics to a prometheus remote write (for testing purpose only).")
pushMetricsCmd.Arg("remote-write-url", "Prometheus remote write url to push metrics.").Required().URLVar(&remoteWriteURL)
metricFiles := pushMetricsCmd.Arg(
@@ -202,6 +232,7 @@ func main() {
pushMetricsLabels := pushMetricsCmd.Flag("label", "Label to attach to metrics. Can be specified multiple times.").Default("job=promtool").StringMap()
pushMetricsTimeout := pushMetricsCmd.Flag("timeout", "The time to wait for pushing metrics.").Default("30s").Duration()
pushMetricsHeaders := pushMetricsCmd.Flag("header", "Prometheus remote write header.").StringMap()
+ pushMetricsProtoMsg := pushMetricsCmd.Flag("protobuf_message", "Protobuf message to use when writing (prometheus.WriteRequest or io.prometheus.write.v2.Request).").Default("prometheus.WriteRequest").String()
testCmd := app.Command("test", "Unit testing.")
junitOutFile := testCmd.Flag("junit", "File path to store JUnit XML test results.").OpenFile(os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644)
@@ -211,7 +242,9 @@ func main() {
"test-rule-file",
"The unit test file.",
).Required().ExistingFiles()
+ testRulesDebug := testRulesCmd.Flag("debug", "Enable unit test debugging.").Default("false").Bool()
testRulesDiff := testRulesCmd.Flag("diff", "[Experimental] Print colored differential output between expected & received output.").Default("false").Bool()
+ testRulesIgnoreUnknownFields := testRulesCmd.Flag("ignore-unknown-fields", "Ignore unknown fields in the test 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()
defaultDBPath := "data/"
tsdbCmd := app.Command("tsdb", "Run tsdb commands.")
@@ -234,18 +267,19 @@ 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.").Default(strconv.FormatInt(math.MinInt64, 10)).Int64()
- dumpMaxTime := tsdbDumpCmd.Flag("max-time", "Maximum timestamp to dump.").Default(strconv.FormatInt(math.MaxInt64, 10)).Int64()
+ 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()
dumpOpenMetricsSandboxDirRoot := tsdbDumpOpenMetricsCmd.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()
- dumpOpenMetricsMinTime := tsdbDumpOpenMetricsCmd.Flag("min-time", "Minimum timestamp to dump.").Default(strconv.FormatInt(math.MinInt64, 10)).Int64()
- dumpOpenMetricsMaxTime := tsdbDumpOpenMetricsCmd.Flag("max-time", "Maximum timestamp to dump.").Default(strconv.FormatInt(math.MaxInt64, 10)).Int64()
+ dumpOpenMetricsMinTime := tsdbDumpOpenMetricsCmd.Flag("min-time", "Minimum timestamp to dump, in milliseconds since the Unix epoch.").Default(strconv.FormatInt(math.MinInt64, 10)).Int64()
+ dumpOpenMetricsMaxTime := tsdbDumpOpenMetricsCmd.Flag("max-time", "Maximum timestamp to dump, in milliseconds since the Unix epoch.").Default(strconv.FormatInt(math.MaxInt64, 10)).Int64()
dumpOpenMetricsMatch := tsdbDumpOpenMetricsCmd.Flag("match", "Series selector. Can be specified multiple times.").Default("{__name__=~'(?s:.*)'}").Strings()
importCmd := tsdbCmd.Command("create-blocks-from", "[Experimental] Import samples from input and produce TSDB blocks. Please refer to the storage docs for more details.")
@@ -257,7 +291,7 @@ func main() {
importFilePath := openMetricsImportCmd.Arg("input file", "OpenMetrics file to read samples from.").Required().String()
importDBPath := openMetricsImportCmd.Arg("output directory", "Output directory for generated blocks.").Default(defaultDBPath).String()
importRulesCmd := importCmd.Command("rules", "Create blocks of data for new recording rules.")
- importRulesCmd.Flag("http.config.file", "HTTP client configuration file for promtool to connect to Prometheus.").PlaceHolder("").ExistingFileVar(&httpConfigFilePath)
+ importRulesCmd.Flag("http.config.file", httpConfigFileDescription).PlaceHolder("").ExistingFileVar(&httpConfigFilePath)
importRulesCmd.Flag("url", "The URL for the Prometheus API with the data where the rule will be backfilled from.").Default("http://localhost:9090").URLVar(&serverURL)
importRulesStart := importRulesCmd.Flag("start", "The time to start backfilling the new rule from. Must be a RFC3339 formatted date or Unix timestamp. Required.").
Required().String()
@@ -286,7 +320,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 (only PromQL related and no-default-scrape-port). See https://prometheus.io/docs/prometheus/latest/feature_flags/ for the options and 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()
@@ -305,40 +339,43 @@ func main() {
kingpin.Fatalf("Cannot set base auth in the server URL and use a http.config.file at the same time")
}
var err error
- httpConfig, _, err := config_util.LoadHTTPConfigFile(httpConfigFilePath)
+ httpConfig, _, err := promconfig.LoadHTTPConfigFile(httpConfigFilePath)
if err != nil {
kingpin.Fatalf("Failed to load HTTP config file: %v", err)
}
- httpRoundTripper, err = promconfig.NewRoundTripperFromConfig(*httpConfig, "promtool", config_util.WithUserAgent("promtool/"+version.Version))
+ httpRoundTripper, err = promconfig.NewRoundTripperFromConfig(*httpConfig, "promtool", promconfig.WithUserAgent(version.ComponentUserAgent("promtool")))
if err != nil {
kingpin.Fatalf("Failed to create a new HTTP round tripper: %v", err)
}
}
- var noDefaultScrapePort bool
for _, f := range *featureList {
- opts := strings.Split(f, ",")
- for _, o := range opts {
+ for o := range strings.SplitSeq(f, ",") {
switch o {
- case "no-default-scrape-port":
- noDefaultScrapePort = true
+ case "promql-experimental-functions":
+ 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
- case "promql-at-modifier", "promql-negative-offset":
- fmt.Printf(" WARNING: Option for --enable-feature is a no-op after promotion to a stable feature: %q\n", o)
default:
- fmt.Printf(" WARNING: Unknown option for --enable-feature: %q\n", o)
+ fmt.Printf(" WARNING: Unknown feature passed to --enable-feature: %s", o)
}
}
}
+ promtoolParser := parser.NewParser(promtoolParserOpts)
switch parsedCmd {
case sdCheckCmd.FullCommand():
- os.Exit(CheckSD(*sdConfigFile, *sdJobName, *sdTimeout, noDefaultScrapePort, prometheus.DefaultRegisterer))
+ os.Exit(CheckSD(*sdConfigFile, *sdJobName, *sdTimeout, prometheus.DefaultRegisterer))
case checkConfigCmd.FullCommand():
- os.Exit(CheckConfig(*agentMode, *checkConfigSyntaxOnly, newLintConfig(*checkConfigLint, *checkConfigLintFatal), *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)))
@@ -350,16 +387,16 @@ func main() {
os.Exit(CheckWebConfig(*webConfigFiles...))
case checkRulesCmd.FullCommand():
- os.Exit(CheckRules(newLintConfig(*checkRulesLint, *checkRulesLintFatal), *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, *pushMetricsLabels, *metricFiles...))
+ os.Exit(PushMetrics(remoteWriteURL, httpRoundTripper, *pushMetricsHeaders, *pushMetricsTimeout, *pushMetricsProtoMsg, *pushMetricsLabels, *metricFiles...))
case queryInstantCmd.FullCommand():
- os.Exit(QueryInstant(serverURL, httpRoundTripper, *queryInstantExpr, *queryInstantTime, p))
+ os.Exit(QueryInstant(serverURL, httpRoundTripper, *queryInstantHeaders, *queryInstantExpr, *queryInstantTime, p))
case queryRangeCmd.FullCommand():
os.Exit(QueryRange(serverURL, httpRoundTripper, *queryRangeHeaders, *queryRangeExpr, *queryRangeBegin, *queryRangeEnd, *queryRangeStep, p))
@@ -386,11 +423,15 @@ func main() {
}
os.Exit(RulesUnitTestResult(results,
promqltest.LazyLoaderOpts{
- EnableAtModifier: true,
- EnableNegativeOffset: true,
+ EnableAtModifier: true,
+ EnableNegativeOffset: true,
+ EnableDelayedNameRemoval: promqlEnableDelayedNameRemoval,
},
+ promtoolParser,
*testRulesRun,
*testRulesDiff,
+ *testRulesDebug,
+ *testRulesIgnoreUnknownFields,
*testRulesFiles...),
)
@@ -398,21 +439,26 @@ 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))
case importRulesCmd.FullCommand():
- os.Exit(checkErr(importRules(serverURL, httpRoundTripper, *importRulesStart, *importRulesEnd, *importRulesOutputDir, *importRulesEvalInterval, *maxBlockDuration, *importRulesFiles...)))
+ os.Exit(checkErr(importRules(serverURL, httpRoundTripper, *importRulesStart, *importRulesEnd, *importRulesOutputDir, *importRulesEvalInterval, *maxBlockDuration, model.UTF8Validation, *importRulesFiles...)))
case queryAnalyzeCmd.FullCommand():
os.Exit(checkErr(queryAnalyzeCfg.run(serverURL, httpRoundTripper)))
@@ -422,15 +468,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)))
}
}
@@ -441,20 +487,26 @@ func checkExperimental(f bool) {
}
}
-var errLint = fmt.Errorf("lint error")
+var errLint = errors.New("lint error")
-type lintConfig struct {
- all bool
- duplicateRules bool
- fatal bool
+type rulesLintConfig struct {
+ all bool
+ duplicateRules bool
+ fatal bool
+ ignoreUnknownFields bool
+ nameValidationScheme model.ValidationScheme
}
-func newLintConfig(stringVal string, fatal bool) lintConfig {
- items := strings.Split(stringVal, ",")
- ls := lintConfig{
- fatal: fatal,
+func newRulesLintConfig(stringVal string, fatal, ignoreUnknownFields bool, nameValidationScheme model.ValidationScheme) rulesLintConfig {
+ ls := rulesLintConfig{
+ fatal: fatal,
+ ignoreUnknownFields: ignoreUnknownFields,
+ nameValidationScheme: nameValidationScheme,
}
- for _, setting := range items {
+ if stringVal == "" {
+ return ls
+ }
+ for setting := range strings.SplitSeq(stringVal, ",") {
switch setting {
case lintOptionAll:
ls.all = true
@@ -462,24 +514,66 @@ func newLintConfig(stringVal string, fatal bool) lintConfig {
ls.duplicateRules = true
case lintOptionNone:
default:
- fmt.Printf("WARNING: unknown lint option %s\n", setting)
+ fmt.Printf("WARNING: unknown lint option: %q\n", setting)
}
}
return ls
}
-func (ls lintConfig) lintDuplicateRules() bool {
+func (ls rulesLintConfig) lintDuplicateRules() bool {
return ls.all || ls.duplicateRules
}
+type configLintConfig struct {
+ rulesLintConfig
+
+ lookbackDelta model.Duration
+}
+
+func newConfigLintConfig(optionsStr string, fatal, ignoreUnknownFields bool, nameValidationScheme model.ValidationScheme, lookbackDelta model.Duration) configLintConfig {
+ c := configLintConfig{
+ rulesLintConfig: rulesLintConfig{
+ fatal: fatal,
+ },
+ }
+
+ lintNone := false
+ var rulesOptions []string
+ for option := range strings.SplitSeq(optionsStr, ",") {
+ switch option {
+ case lintOptionAll, lintOptionTooLongScrapeInterval:
+ c.lookbackDelta = lookbackDelta
+ if option == lintOptionAll {
+ rulesOptions = append(rulesOptions, lintOptionAll)
+ }
+ case lintOptionNone:
+ lintNone = true
+ default:
+ rulesOptions = append(rulesOptions, option)
+ }
+ }
+
+ if lintNone {
+ c.lookbackDelta = 0
+ rulesOptions = nil
+ }
+
+ c.rulesLintConfig = newRulesLintConfig(strings.Join(rulesOptions, ","), fatal, ignoreUnknownFields, nameValidationScheme)
+
+ return c
+}
+
// CheckServerStatus - healthy & ready.
func CheckServerStatus(serverURL *url.URL, checkEndpoint string, roundTripper http.RoundTripper) error {
if serverURL.Scheme == "" {
serverURL.Scheme = "http"
}
+ // Join the endpoint onto the server URL path so that a trailing slash in
+ // the user-provided URL does not result in a doubled slash (e.g.
+ // "//-/healthy"), which the Prometheus router does not match.
config := api.Config{
- Address: serverURL.String() + checkEndpoint,
+ Address: serverURL.JoinPath(checkEndpoint).String(),
RoundTripper: roundTripper,
}
@@ -490,7 +584,7 @@ func CheckServerStatus(serverURL *url.URL, checkEndpoint string, roundTripper ht
return err
}
- request, err := http.NewRequest(http.MethodGet, config.Address, nil)
+ request, err := http.NewRequest(http.MethodGet, config.Address, http.NoBody)
if err != nil {
return err
}
@@ -510,12 +604,12 @@ func CheckServerStatus(serverURL *url.URL, checkEndpoint string, roundTripper ht
}
// CheckConfig validates configuration files.
-func CheckConfig(agentMode, checkSyntaxOnly bool, lintSettings lintConfig, files ...string) int {
+func CheckConfig(agentMode, checkSyntaxOnly bool, lintSettings configLintConfig, p parser.Parser, files ...string) int {
failed := false
hasErrors := false
for _, f := range files {
- ruleFiles, err := checkConfig(agentMode, f, checkSyntaxOnly)
+ ruleFiles, scrapeConfigs, err := checkConfig(agentMode, f, checkSyntaxOnly)
if err != nil {
fmt.Fprintln(os.Stderr, " FAILED:", err)
hasErrors = true
@@ -528,12 +622,12 @@ func CheckConfig(agentMode, checkSyntaxOnly bool, lintSettings lintConfig, files
}
fmt.Println()
- rulesFailed, rulesHasErrors := checkRules(ruleFiles, lintSettings)
- if rulesFailed {
- failed = rulesFailed
- }
- if rulesHasErrors {
- hasErrors = rulesHasErrors
+ if !checkSyntaxOnly {
+ scrapeConfigsFailed := lintScrapeConfigs(scrapeConfigs, lintSettings)
+ failed = failed || scrapeConfigsFailed
+ rulesFailed, rulesHaveErrors := checkRules(ruleFiles, lintSettings.rulesLintConfig, p, logger)
+ failed = failed || rulesFailed
+ hasErrors = hasErrors || rulesHaveErrors
}
}
if failed && hasErrors {
@@ -572,12 +666,12 @@ func checkFileExists(fn string) error {
return err
}
-func checkConfig(agentMode bool, filename string, checkSyntaxOnly bool) ([]string, error) {
+func checkConfig(agentMode bool, filename string, checkSyntaxOnly bool) ([]string, []*config.ScrapeConfig, error) {
fmt.Println("Checking", filename)
- cfg, err := config.LoadFile(filename, agentMode, false, log.NewNopLogger())
+ cfg, err := config.LoadFile(filename, agentMode, logger)
if err != nil {
- return nil, err
+ return nil, nil, err
}
var ruleFiles []string
@@ -585,15 +679,15 @@ func checkConfig(agentMode bool, filename string, checkSyntaxOnly bool) ([]strin
for _, rf := range cfg.RuleFiles {
rfs, err := filepath.Glob(rf)
if err != nil {
- return nil, err
+ return nil, nil, err
}
// If an explicit file was given, error if it is not accessible.
if !strings.Contains(rf, "*") {
if len(rfs) == 0 {
- return nil, fmt.Errorf("%q does not point to an existing file", rf)
+ return nil, nil, fmt.Errorf("%q does not point to an existing file", rf)
}
if err := checkFileExists(rfs[0]); err != nil {
- return nil, fmt.Errorf("error checking rule file %q: %w", rfs[0], err)
+ return nil, nil, fmt.Errorf("error checking rule file %q: %w", rfs[0], err)
}
}
ruleFiles = append(ruleFiles, rfs...)
@@ -607,26 +701,26 @@ func checkConfig(agentMode bool, filename string, checkSyntaxOnly bool) ([]strin
var err error
scfgs, err = cfg.GetScrapeConfigs()
if err != nil {
- return nil, fmt.Errorf("error loading scrape configs: %w", err)
+ return nil, nil, fmt.Errorf("error loading scrape configs: %w", err)
}
}
for _, scfg := range scfgs {
if !checkSyntaxOnly && scfg.HTTPClientConfig.Authorization != nil {
if err := checkFileExists(scfg.HTTPClientConfig.Authorization.CredentialsFile); err != nil {
- return nil, fmt.Errorf("error checking authorization credentials or bearer token file %q: %w", scfg.HTTPClientConfig.Authorization.CredentialsFile, err)
+ return nil, nil, fmt.Errorf("error checking authorization credentials or bearer token file %q: %w", scfg.HTTPClientConfig.Authorization.CredentialsFile, err)
}
}
if err := checkTLSConfig(scfg.HTTPClientConfig.TLSConfig, checkSyntaxOnly); err != nil {
- return nil, err
+ return nil, nil, err
}
for _, c := range scfg.ServiceDiscoveryConfigs {
switch c := c.(type) {
case *kubernetes.SDConfig:
if err := checkTLSConfig(c.HTTPClientConfig.TLSConfig, checkSyntaxOnly); err != nil {
- return nil, err
+ return nil, nil, err
}
case *file.SDConfig:
if checkSyntaxOnly {
@@ -635,17 +729,17 @@ func checkConfig(agentMode bool, filename string, checkSyntaxOnly bool) ([]strin
for _, file := range c.Files {
files, err := filepath.Glob(file)
if err != nil {
- return nil, err
+ return nil, nil, err
}
if len(files) != 0 {
for _, f := range files {
var targetGroups []*targetgroup.Group
targetGroups, err = checkSDFile(f)
if err != nil {
- return nil, fmt.Errorf("checking SD file %q: %w", file, err)
+ return nil, nil, fmt.Errorf("checking SD file %q: %w", file, err)
}
if err := checkTargetGroupsForScrapeConfig(targetGroups, scfg); err != nil {
- return nil, err
+ return nil, nil, err
}
}
continue
@@ -654,7 +748,7 @@ func checkConfig(agentMode bool, filename string, checkSyntaxOnly bool) ([]strin
}
case discovery.StaticConfig:
if err := checkTargetGroupsForScrapeConfig(c, scfg); err != nil {
- return nil, err
+ return nil, nil, err
}
}
}
@@ -671,18 +765,18 @@ func checkConfig(agentMode bool, filename string, checkSyntaxOnly bool) ([]strin
for _, file := range c.Files {
files, err := filepath.Glob(file)
if err != nil {
- return nil, err
+ return nil, nil, err
}
if len(files) != 0 {
for _, f := range files {
var targetGroups []*targetgroup.Group
targetGroups, err = checkSDFile(f)
if err != nil {
- return nil, fmt.Errorf("checking SD file %q: %w", file, err)
+ return nil, nil, fmt.Errorf("checking SD file %q: %w", file, err)
}
if err := checkTargetGroupsForAlertmanager(targetGroups, amcfg); err != nil {
- return nil, err
+ return nil, nil, err
}
}
continue
@@ -691,19 +785,19 @@ func checkConfig(agentMode bool, filename string, checkSyntaxOnly bool) ([]strin
}
case discovery.StaticConfig:
if err := checkTargetGroupsForAlertmanager(c, amcfg); err != nil {
- return nil, err
+ return nil, nil, err
}
}
}
}
- return ruleFiles, nil
+ return ruleFiles, scfgs, nil
}
-func checkTLSConfig(tlsConfig config_util.TLSConfig, checkSyntaxOnly bool) error {
- if len(tlsConfig.CertFile) > 0 && len(tlsConfig.KeyFile) == 0 {
+func checkTLSConfig(tlsConfig promconfig.TLSConfig, checkSyntaxOnly bool) error {
+ if tlsConfig.CertFile != "" && tlsConfig.KeyFile == "" {
return fmt.Errorf("client cert file %q specified without client key file", tlsConfig.CertFile)
}
- if len(tlsConfig.KeyFile) > 0 && len(tlsConfig.CertFile) == 0 {
+ if tlsConfig.KeyFile != "" && tlsConfig.CertFile == "" {
return fmt.Errorf("client key file %q specified without client cert file", tlsConfig.KeyFile)
}
@@ -758,13 +852,13 @@ func checkSDFile(filename string) ([]*targetgroup.Group, error) {
}
// CheckRules validates rule files.
-func CheckRules(ls lintConfig, 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, logger)
} else {
- failed, hasErrors = checkRules(files, ls)
+ failed, hasErrors = checkRules(files, ls, p, logger)
}
if failed && hasErrors {
@@ -778,7 +872,7 @@ func CheckRules(ls lintConfig, files ...string) int {
}
// checkRulesFromStdin validates rule from stdin.
-func checkRulesFromStdin(ls lintConfig) (bool, bool) {
+func checkRulesFromStdin(ls rulesLintConfig, p parser.Parser, logger *slog.Logger) (bool, bool) {
failed := false
hasErrors := false
fmt.Println("Checking standard input")
@@ -787,7 +881,7 @@ func checkRulesFromStdin(ls lintConfig) (bool, bool) {
fmt.Fprintln(os.Stderr, " FAILED:", err)
return true, true
}
- rgs, errs := rulefmt.Parse(data)
+ rgs, errs := rulefmt.Parse(data, ls.ignoreUnknownFields, ls.nameValidationScheme, p, logger)
if errs != nil {
failed = true
fmt.Fprintln(os.Stderr, " FAILED:")
@@ -816,12 +910,12 @@ func checkRulesFromStdin(ls lintConfig) (bool, bool) {
}
// checkRules validates rule files.
-func checkRules(files []string, ls lintConfig) (bool, bool) {
+func checkRules(files []string, ls rulesLintConfig, p parser.Parser, logger *slog.Logger) (bool, bool) {
failed := false
hasErrors := false
for _, f := range files {
fmt.Println("Checking", f)
- rgs, errs := rulefmt.ParseFile(f)
+ rgs, errs := rulefmt.ParseFile(f, ls.ignoreUnknownFields, ls.nameValidationScheme, p, logger)
if errs != nil {
failed = true
fmt.Fprintln(os.Stderr, " FAILED:")
@@ -850,7 +944,7 @@ func checkRules(files []string, ls lintConfig) (bool, bool) {
return failed, hasErrors
}
-func checkRuleGroups(rgs *rulefmt.RuleGroups, lintSettings lintConfig) (int, []error) {
+func checkRuleGroups(rgs *rulefmt.RuleGroups, lintSettings rulesLintConfig) (int, []error) {
numRules := 0
for _, rg := range rgs.Groups {
numRules += len(rg.Rules)
@@ -859,21 +953,32 @@ func checkRuleGroups(rgs *rulefmt.RuleGroups, lintSettings lintConfig) (int, []e
if lintSettings.lintDuplicateRules() {
dRules := checkDuplicates(rgs.Groups)
if len(dRules) != 0 {
- errMessage := fmt.Sprintf("%d duplicate rule(s) found.\n", len(dRules))
+ var errMessage strings.Builder
+ fmt.Fprintf(&errMessage, "%d duplicate rule(s) found.\n", len(dRules))
for _, n := range dRules {
- errMessage += 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 += fmt.Sprintf("\t%s: %s\n", l.Name, l.Value)
+ fmt.Fprintf(&errMessage, "\t%s: %s\n", l.Name, l.Value)
})
}
- errMessage += "Might cause inconsistency while recording expressions"
- return 0, []error{fmt.Errorf("%w %s", errLint, errMessage)}
+ errMessage.WriteString("Might cause inconsistency while recording expressions")
+ return 0, []error{fmt.Errorf("%w %s", errLint, errMessage.String())}
}
}
return numRules, nil
}
+func lintScrapeConfigs(scrapeConfigs []*config.ScrapeConfig, lintSettings configLintConfig) bool {
+ for _, scfg := range scrapeConfigs {
+ if lintSettings.lookbackDelta > 0 && scfg.ScrapeInterval >= lintSettings.lookbackDelta {
+ fmt.Fprintf(os.Stderr, " FAILED: too long scrape interval found, data point will be marked as stale - job: %s, interval: %s\n", scfg.JobName, scfg.ScrapeInterval)
+ return true
+ }
+ }
+ return false
+}
+
type compareRuleType struct {
metric string
label labels.Labels
@@ -895,73 +1000,90 @@ func compare(a, b compareRuleType) int {
func checkDuplicates(groups []rulefmt.RuleGroup) []compareRuleType {
var duplicates []compareRuleType
- var rules compareRuleTypes
+ var cRules compareRuleTypes
for _, group := range groups {
for _, rule := range group.Rules {
- rules = append(rules, compareRuleType{
+ cRules = append(cRules, compareRuleType{
metric: ruleMetric(rule),
- label: labels.FromMap(rule.Labels),
+ label: rules.FromMaps(group.Labels, rule.Labels),
})
}
}
- if len(rules) < 2 {
+ if len(cRules) < 2 {
return duplicates
}
- sort.Sort(rules)
+ sort.Sort(cRules)
- last := rules[0]
- for i := 1; i < len(rules); i++ {
- if compare(last, rules[i]) == 0 {
+ last := cRules[0]
+ for i := 1; i < len(cRules); i++ {
+ if compare(last, cRules[i]) == 0 {
// Don't add a duplicated rule multiple times.
if len(duplicates) == 0 || compare(last, duplicates[len(duplicates)-1]) != 0 {
- duplicates = append(duplicates, rules[i])
+ duplicates = append(duplicates, cRules[i])
}
}
- last = rules[i]
+ last = cRules[i]
}
return duplicates
}
-func ruleMetric(rule rulefmt.RuleNode) string {
- if rule.Alert.Value != "" {
- return rule.Alert.Value
+func ruleMetric(rule rulefmt.Rule) string {
+ if rule.Alert != "" {
+ return rule.Alert
}
- return rule.Record.Value
+ return rule.Record
}
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 len(problems) > 0 {
- return lintErrExitCode
+ 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
}
+ 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
@@ -975,6 +1097,10 @@ func CheckMetrics(extended bool) int {
w.Flush()
}
+ if hasLintProblems {
+ return lintErrExitCode
+ }
+
return successExitCode
}
@@ -985,7 +1111,10 @@ type metricStat struct {
}
func checkMetricsExtended(r io.Reader) ([]metricStat, int, error) {
- p := expfmt.TextParser{}
+ // Lacking information about what the intended validation scheme is, use the
+ // deprecated library bool.
+ //nolint:staticcheck
+ p := expfmt.NewTextParser(model.NameValidationScheme)
metricFamilies, err := p.TextToMetricFamilies(r)
if err != nil {
return nil, 0, fmt.Errorf("error while parsing text to metric families: %w", err)
@@ -1113,17 +1242,17 @@ type printer interface {
type promqlPrinter struct{}
-func (p *promqlPrinter) printValue(v model.Value) {
+func (*promqlPrinter) printValue(v model.Value) {
fmt.Println(v)
}
-func (p *promqlPrinter) printSeries(val []model.LabelSet) {
+func (*promqlPrinter) printSeries(val []model.LabelSet) {
for _, v := range val {
fmt.Println(v)
}
}
-func (p *promqlPrinter) printLabelValues(val model.LabelValues) {
+func (*promqlPrinter) printLabelValues(val model.LabelValues) {
for _, v := range val {
fmt.Println(v)
}
@@ -1131,24 +1260,24 @@ func (p *promqlPrinter) printLabelValues(val model.LabelValues) {
type jsonPrinter struct{}
-func (j *jsonPrinter) printValue(v model.Value) {
+func (*jsonPrinter) printValue(v model.Value) {
//nolint:errcheck
json.NewEncoder(os.Stdout).Encode(v)
}
-func (j *jsonPrinter) printSeries(v []model.LabelSet) {
+func (*jsonPrinter) printSeries(v []model.LabelSet) {
//nolint:errcheck
json.NewEncoder(os.Stdout).Encode(v)
}
-func (j *jsonPrinter) printLabelValues(v model.LabelValues) {
+func (*jsonPrinter) printLabelValues(v model.LabelValues) {
//nolint:errcheck
json.NewEncoder(os.Stdout).Encode(v)
}
// importRules backfills recording rules from the files provided. The output are blocks of data
// at the outputDir location.
-func importRules(url *url.URL, roundTripper http.RoundTripper, start, end, outputDir string, evalInterval, maxBlockDuration time.Duration, files ...string) error {
+func importRules(url *url.URL, roundTripper http.RoundTripper, start, end, outputDir string, evalInterval, maxBlockDuration time.Duration, nameValidationScheme model.ValidationScheme, files ...string) error {
ctx := context.Background()
var stime, etime time.Time
var err error
@@ -1171,18 +1300,19 @@ func importRules(url *url.URL, roundTripper http.RoundTripper, start, end, outpu
}
cfg := ruleImporterConfig{
- outputDir: outputDir,
- start: stime,
- end: etime,
- evalInterval: evalInterval,
- maxBlockDuration: maxBlockDuration,
+ outputDir: outputDir,
+ start: stime,
+ end: etime,
+ evalInterval: evalInterval,
+ maxBlockDuration: maxBlockDuration,
+ nameValidationScheme: nameValidationScheme,
}
api, err := newAPI(url, roundTripper, nil)
if err != nil {
return fmt.Errorf("new api client error: %w", err)
}
- ruleImporter := newRuleImporter(log.NewLogfmtLogger(log.NewSyncWriter(os.Stderr)), cfg, api)
+ ruleImporter := newRuleImporter(logger, cfg, api)
errs := ruleImporter.loadGroups(ctx, files)
for _, err := range errs {
if err != nil {
@@ -1216,7 +1346,7 @@ func checkTargetGroupsForScrapeConfig(targetGroups []*targetgroup.Group, scfg *c
lb := labels.NewBuilder(labels.EmptyLabels())
for _, tg := range targetGroups {
var failures []error
- targets, failures = scrape.TargetsFromGroup(tg, scfg, false, targets, lb)
+ targets, failures = scrape.TargetsFromGroup(tg, scfg, targets, lb)
if len(failures) > 0 {
first := failures[0]
return first
@@ -1226,8 +1356,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
}
@@ -1236,8 +1366,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
}
@@ -1256,7 +1386,7 @@ func labelsSetPromQL(query, labelMatchType, name, value string) error {
return fmt.Errorf("invalid label match type: %s", labelMatchType)
}
- parser.Inspect(expr, func(node parser.Node, path []parser.Node) error {
+ parser.Inspect(expr, func(node parser.Node, _ []parser.Node) error {
if n, ok := node.(*parser.VectorSelector); ok {
var found bool
for i, l := range n.LabelMatchers {
@@ -1281,13 +1411,13 @@ 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
}
- parser.Inspect(expr, func(node parser.Node, path []parser.Node) error {
+ parser.Inspect(expr, func(node parser.Node, _ []parser.Node) error {
if n, ok := node.(*parser.VectorSelector); ok {
for i, l := range n.LabelMatchers {
if l.Name == name {
diff --git a/cmd/promtool/main_test.go b/cmd/promtool/main_test.go
index 2b086851f36..e42ca69441b 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"
@@ -31,13 +32,22 @@ import (
"testing"
"time"
+ "github.com/prometheus/common/model"
+ "github.com/prometheus/common/promslog"
"github.com/stretchr/testify/require"
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/model/rulefmt"
+ "github.com/prometheus/prometheus/promql/parser"
"github.com/prometheus/prometheus/promql/promqltest"
)
+func init() {
+ // This can be removed when the legacy global mode is fully deprecated.
+ //nolint:staticcheck
+ model.NameValidationScheme = model.UTF8Validation
+}
+
var promtoolPath = os.Args[0]
func TestMain(m *testing.M) {
@@ -54,6 +64,7 @@ func TestMain(m *testing.M) {
}
func TestQueryRange(t *testing.T) {
+ t.Parallel()
s, getRequest := mockServer(200, `{"status": "success", "data": {"resultType": "matrix", "result": []}}`)
defer s.Close()
@@ -77,6 +88,7 @@ func TestQueryRange(t *testing.T) {
}
func TestQueryInstant(t *testing.T) {
+ t.Parallel()
s, getRequest := mockServer(200, `{"status": "success", "data": {"resultType": "vector", "result": []}}`)
defer s.Close()
@@ -84,7 +96,7 @@ func TestQueryInstant(t *testing.T) {
require.NoError(t, err)
p := &promqlPrinter{}
- exitCode := QueryInstant(urlObject, http.DefaultTransport, "up", "300", p)
+ exitCode := QueryInstant(urlObject, http.DefaultTransport, map[string]string{}, "up", "300", p)
require.Equal(t, "/api/v1/query", getRequest().URL.Path)
form := getRequest().Form
require.Equal(t, "up", form.Get("query"))
@@ -92,6 +104,51 @@ func TestQueryInstant(t *testing.T) {
require.Equal(t, 0, exitCode)
}
+func TestQueryInstantHeaders(t *testing.T) {
+ t.Parallel()
+ s, getRequest := mockServer(200, `{"status": "success", "data": {"resultType": "vector", "result": []}}`)
+ defer s.Close()
+
+ urlObject, err := url.Parse(s.URL)
+ require.NoError(t, err)
+
+ p := &promqlPrinter{}
+ headers := map[string]string{"X-Scope-OrgID": "prom", "X-Custom": "value"}
+ exitCode := QueryInstant(urlObject, http.DefaultTransport, headers, "up", "300", p)
+ require.Equal(t, 0, exitCode)
+ require.Equal(t, "prom", getRequest().Header.Get("X-Scope-OrgID"))
+ require.Equal(t, "value", getRequest().Header.Get("X-Custom"))
+}
+
+func TestCheckServerStatus(t *testing.T) {
+ t.Parallel()
+
+ for _, tc := range []struct {
+ name string
+ urlSuffix string
+ }{
+ {name: "no trailing slash", urlSuffix: ""},
+ {name: "trailing slash", urlSuffix: "/"},
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+ s, getRequest := mockServer(200, "Prometheus is Healthy.\n")
+ defer s.Close()
+
+ urlObject, err := url.Parse(s.URL + tc.urlSuffix)
+ require.NoError(t, err)
+
+ err = CheckServerStatus(urlObject, checkHealth, http.DefaultTransport)
+ require.NoError(t, err)
+ // The endpoint path must be requested verbatim regardless of a
+ // trailing slash in the user-provided URL. A naive string
+ // concatenation would request "//-/healthy" here, which the
+ // Prometheus router does not match.
+ require.Equal(t, checkHealth, getRequest().URL.Path)
+ })
+ }
+}
+
func mockServer(code int, body string) (*httptest.Server, func() *http.Request) {
var req *http.Request
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -108,6 +165,7 @@ func mockServer(code int, body string) (*httptest.Server, func() *http.Request)
}
func TestCheckSDFile(t *testing.T) {
+ t.Parallel()
cases := []struct {
name string
file string
@@ -127,8 +185,8 @@ func TestCheckSDFile(t *testing.T) {
},
{
name: "bad file extension",
- file: "./testdata/bad-sd-file-extension.nonexistant",
- err: "invalid file extension: \".nonexistant\"",
+ file: "./testdata/bad-sd-file-extension.nonexistent",
+ err: "invalid file extension: \".nonexistent\"",
},
{
name: "bad format",
@@ -138,9 +196,10 @@ func TestCheckSDFile(t *testing.T) {
}
for _, test := range cases {
t.Run(test.name, func(t *testing.T) {
+ t.Parallel()
_, err := checkSDFile(test.file)
if test.err != "" {
- require.Equalf(t, test.err, err.Error(), "Expected error %q, got %q", test.err, err.Error())
+ require.EqualErrorf(t, err, test.err, "Expected error %q, got %q", test.err, err.Error())
return
}
require.NoError(t, err)
@@ -149,6 +208,7 @@ func TestCheckSDFile(t *testing.T) {
}
func TestCheckDuplicates(t *testing.T) {
+ t.Parallel()
cases := []struct {
name string
ruleFile string
@@ -173,7 +233,8 @@ func TestCheckDuplicates(t *testing.T) {
for _, test := range cases {
c := test
t.Run(c.name, func(t *testing.T) {
- rgs, err := rulefmt.ParseFile(c.ruleFile)
+ t.Parallel()
+ rgs, err := rulefmt.ParseFile(c.ruleFile, false, model.UTF8Validation, parser.NewParser(parser.Options{}), promslog.NewNopLogger())
require.Empty(t, err)
dups := checkDuplicates(rgs.Groups)
require.Equal(t, c.expectedDups, dups)
@@ -182,16 +243,16 @@ func TestCheckDuplicates(t *testing.T) {
}
func BenchmarkCheckDuplicates(b *testing.B) {
- rgs, err := rulefmt.ParseFile("./testdata/rules_large.yml")
+ rgs, err := rulefmt.ParseFile("./testdata/rules_large.yml", false, model.UTF8Validation, parser.NewParser(parser.Options{}), promslog.NewNopLogger())
require.Empty(b, err)
- b.ResetTimer()
- for i := 0; i < b.N; i++ {
+ for b.Loop() {
checkDuplicates(rgs.Groups)
}
}
func TestCheckTargetConfig(t *testing.T) {
+ t.Parallel()
cases := []struct {
name string
file string
@@ -220,9 +281,10 @@ func TestCheckTargetConfig(t *testing.T) {
}
for _, test := range cases {
t.Run(test.name, func(t *testing.T) {
- _, err := checkConfig(false, "testdata/"+test.file, false)
+ t.Parallel()
+ _, _, err := checkConfig(false, "testdata/"+test.file, false)
if test.err != "" {
- require.Equalf(t, test.err, err.Error(), "Expected error %q, got %q", test.err, err.Error())
+ require.EqualErrorf(t, err, test.err, "Expected error %q, got %q", test.err, err.Error())
return
}
require.NoError(t, err)
@@ -231,6 +293,7 @@ func TestCheckTargetConfig(t *testing.T) {
}
func TestCheckConfigSyntax(t *testing.T) {
+ t.Parallel()
cases := []struct {
name string
file string
@@ -282,7 +345,7 @@ func TestCheckConfigSyntax(t *testing.T) {
err: "error checking client cert file \"testdata/nonexistent_cert_file.yml\": " +
"stat testdata/nonexistent_cert_file.yml: no such file or directory",
errWindows: "error checking client cert file \"testdata\\\\nonexistent_cert_file.yml\": " +
- "CreateFile testdata\\nonexistent_cert_file.yml: The system cannot find the file specified.",
+ "GetFileAttributesEx testdata\\nonexistent_cert_file.yml: The system cannot find the file specified.",
},
{
name: "check with syntax only succeeds with nonexistent credentials file",
@@ -298,18 +361,19 @@ func TestCheckConfigSyntax(t *testing.T) {
err: "error checking authorization credentials or bearer token file \"/random/file/which/does/not/exist.yml\": " +
"stat /random/file/which/does/not/exist.yml: no such file or directory",
errWindows: "error checking authorization credentials or bearer token file \"testdata\\\\random\\\\file\\\\which\\\\does\\\\not\\\\exist.yml\": " +
- "CreateFile testdata\\random\\file\\which\\does\\not\\exist.yml: The system cannot find the path specified.",
+ "GetFileAttributesEx testdata\\random\\file\\which\\does\\not\\exist.yml: The system cannot find the path specified.",
},
}
for _, test := range cases {
t.Run(test.name, func(t *testing.T) {
- _, err := checkConfig(false, "testdata/"+test.file, test.syntaxOnly)
+ t.Parallel()
+ _, _, err := checkConfig(false, "testdata/"+test.file, test.syntaxOnly)
expectedErrMsg := test.err
if strings.Contains(runtime.GOOS, "windows") {
expectedErrMsg = test.errWindows
}
if expectedErrMsg != "" {
- require.Equalf(t, expectedErrMsg, err.Error(), "Expected error %q, got %q", test.err, err.Error())
+ require.EqualErrorf(t, err, expectedErrMsg, "Expected error %q, got %q", test.err, err.Error())
return
}
require.NoError(t, err)
@@ -318,6 +382,7 @@ func TestCheckConfigSyntax(t *testing.T) {
}
func TestAuthorizationConfig(t *testing.T) {
+ t.Parallel()
cases := []struct {
name string
file string
@@ -337,9 +402,10 @@ func TestAuthorizationConfig(t *testing.T) {
for _, test := range cases {
t.Run(test.name, func(t *testing.T) {
- _, err := checkConfig(false, "testdata/"+test.file, false)
+ t.Parallel()
+ _, _, err := checkConfig(false, "testdata/"+test.file, false)
if test.err != "" {
- require.Contains(t, err.Error(), test.err, "Expected error to contain %q, got %q", test.err, err.Error())
+ require.ErrorContains(t, err, test.err, "Expected error to contain %q, got %q", test.err, err.Error())
return
}
require.NoError(t, err)
@@ -351,6 +417,7 @@ func TestCheckMetricsExtended(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Skipping on windows")
}
+ t.Parallel()
f, err := os.Open("testdata/metrics-test.prom")
require.NoError(t, err)
@@ -383,10 +450,104 @@ 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.")
}
+ t.Parallel()
for _, c := range []struct {
file string
@@ -411,8 +572,10 @@ func TestExitCodes(t *testing.T) {
},
} {
t.Run(c.file, func(t *testing.T) {
+ t.Parallel()
for _, lintFatal := range []bool{true, false} {
t.Run(strconv.FormatBool(lintFatal), func(t *testing.T) {
+ t.Parallel()
args := []string{"-test.main", "check", "config", "testdata/" + c.file}
if lintFatal {
args = append(args, "--lint-fatal")
@@ -443,6 +606,7 @@ func TestDocumentation(t *testing.T) {
if runtime.GOOS == "windows" {
t.SkipNow()
}
+ t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
@@ -485,8 +649,8 @@ func TestCheckRules(t *testing.T) {
defer func(v *os.File) { os.Stdin = v }(os.Stdin)
os.Stdin = r
- exitCode := CheckRules(newLintConfig(lintOptionDuplicateRules, false))
- require.Equal(t, successExitCode, exitCode, "")
+ exitCode := CheckRules(newRulesLintConfig(lintOptionDuplicateRules, false, false, model.UTF8Validation), parser.NewParser(parser.Options{}))
+ require.Equal(t, successExitCode, exitCode)
})
t.Run("rules-bad", func(t *testing.T) {
@@ -507,8 +671,8 @@ func TestCheckRules(t *testing.T) {
defer func(v *os.File) { os.Stdin = v }(os.Stdin)
os.Stdin = r
- exitCode := CheckRules(newLintConfig(lintOptionDuplicateRules, false))
- require.Equal(t, failureExitCode, exitCode, "")
+ exitCode := CheckRules(newRulesLintConfig(lintOptionDuplicateRules, false, false, model.UTF8Validation), parser.NewParser(parser.Options{}))
+ require.Equal(t, failureExitCode, exitCode)
})
t.Run("rules-lint-fatal", func(t *testing.T) {
@@ -529,38 +693,95 @@ func TestCheckRules(t *testing.T) {
defer func(v *os.File) { os.Stdin = v }(os.Stdin)
os.Stdin = r
- exitCode := CheckRules(newLintConfig(lintOptionDuplicateRules, true))
- require.Equal(t, lintErrExitCode, exitCode, "")
+ exitCode := CheckRules(newRulesLintConfig(lintOptionDuplicateRules, true, false, model.UTF8Validation), parser.NewParser(parser.Options{}))
+ require.Equal(t, lintErrExitCode, exitCode)
})
}
+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", "--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)
+}
+
func TestCheckRulesWithRuleFiles(t *testing.T) {
t.Run("rules-good", func(t *testing.T) {
- exitCode := CheckRules(newLintConfig(lintOptionDuplicateRules, false), "./testdata/rules.yml")
- require.Equal(t, successExitCode, exitCode, "")
+ t.Parallel()
+ 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) {
- exitCode := CheckRules(newLintConfig(lintOptionDuplicateRules, false), "./testdata/rules-bad.yml")
- require.Equal(t, failureExitCode, exitCode, "")
+ t.Parallel()
+ 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) {
- exitCode := CheckRules(newLintConfig(lintOptionDuplicateRules, true), "./testdata/prometheus-rules.lint.yml")
- require.Equal(t, lintErrExitCode, exitCode, "")
+ t.Parallel()
+ exitCode := CheckRules(newRulesLintConfig(lintOptionDuplicateRules, true, false, model.UTF8Validation), parser.NewParser(parser.Options{}), "./testdata/prometheus-rules.lint.yml")
+ require.Equal(t, lintErrExitCode, exitCode)
})
}
+func TestCheckScrapeConfigs(t *testing.T) {
+ for _, tc := range []struct {
+ name string
+ lookbackDelta model.Duration
+ expectError bool
+ }{
+ {
+ name: "scrape interval less than lookback delta",
+ lookbackDelta: model.Duration(11 * time.Minute),
+ expectError: false,
+ },
+ {
+ name: "scrape interval greater than lookback delta",
+ lookbackDelta: model.Duration(5 * time.Minute),
+ expectError: true,
+ },
+ {
+ name: "scrape interval same as lookback delta",
+ lookbackDelta: model.Duration(10 * time.Minute),
+ expectError: true,
+ },
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ // Non-fatal linting.
+ 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), 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), 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), 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`)
+ })
+ }
+}
+
func TestTSDBDumpCommand(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode.")
}
+ t.Parallel()
storage := promqltest.LoadedStorage(t, `
load 1m
metric{foo="bar"} 1 2 3
`)
- t.Cleanup(func() { storage.Close() })
for _, c := range []struct {
name string
@@ -587,6 +808,7 @@ func TestTSDBDumpCommand(t *testing.T) {
},
} {
t.Run(c.name, func(t *testing.T) {
+ t.Parallel()
args := []string{"-test.main", "tsdb", c.subCmd, storage.Dir()}
cmd := exec.Command(promtoolPath, args...)
require.NoError(t, cmd.Run())
diff --git a/cmd/promtool/metrics.go b/cmd/promtool/metrics.go
index 4c91d1d6fea..b1a2beb72ef 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
@@ -23,43 +23,46 @@ import (
"os"
"time"
- "github.com/golang/snappy"
- config_util "github.com/prometheus/common/config"
- "github.com/prometheus/common/model"
+ remoteapi "github.com/prometheus/client_golang/exp/api/remote"
- "github.com/prometheus/prometheus/storage/remote"
"github.com/prometheus/prometheus/util/fmtutil"
)
// PushMetrics to a prometheus remote write (for testing purpose only).
-func PushMetrics(url *url.URL, roundTripper http.RoundTripper, headers map[string]string, timeout time.Duration, labels map[string]string, files ...string) int {
+func PushMetrics(url *url.URL, roundTripper http.RoundTripper, headers map[string]string, timeout time.Duration, protoMsg string, labels map[string]string, files ...string) int {
addressURL, err := url.Parse(url.String())
if err != nil {
fmt.Fprintln(os.Stderr, err)
return failureExitCode
}
- // build remote write client
- writeClient, err := remote.NewWriteClient("remote-write", &remote.ClientConfig{
- URL: &config_util.URL{URL: addressURL},
- Timeout: model.Duration(timeout),
- })
+ // Build HTTP client with custom transport for headers.
+ httpClient := &http.Client{
+ Transport: &setHeadersTransport{
+ RoundTripper: roundTripper,
+ headers: headers,
+ },
+ Timeout: timeout,
+ }
+
+ // Create remote write API client.
+ writeAPI, err := remoteapi.NewAPI(addressURL.String(), remoteapi.WithAPIHTTPClient(httpClient))
if err != nil {
fmt.Fprintln(os.Stderr, err)
return failureExitCode
}
- // set custom tls config from httpConfigFilePath
- // set custom headers to every request
- client, ok := writeClient.(*remote.Client)
- if !ok {
- fmt.Fprintln(os.Stderr, fmt.Errorf("unexpected type %T", writeClient))
+ // Determine message type based on protobuf_message flag.
+ var messageType remoteapi.WriteMessageType
+ switch protoMsg {
+ case "io.prometheus.write.v2.Request":
+ messageType = remoteapi.WriteV2MessageType
+ case "prometheus.WriteRequest":
+ messageType = remoteapi.WriteV1MessageType
+ default:
+ fmt.Fprintf(os.Stderr, " FAILED: invalid protobuf message %q, must be prometheus.WriteRequest or io.prometheus.write.v2.Request\n", protoMsg)
return failureExitCode
}
- client.Client.Transport = &setHeadersTransport{
- RoundTripper: roundTripper,
- headers: headers,
- }
var data []byte
var failed bool
@@ -71,7 +74,7 @@ func PushMetrics(url *url.URL, roundTripper http.RoundTripper, headers map[strin
return failureExitCode
}
fmt.Printf("Parsing standard input\n")
- if parseAndPushMetrics(client, data, labels) {
+ if parseAndPushMetrics(writeAPI, messageType, data, labels) {
fmt.Printf(" SUCCESS: metrics pushed to remote write.\n")
return successExitCode
}
@@ -87,7 +90,7 @@ func PushMetrics(url *url.URL, roundTripper http.RoundTripper, headers map[strin
}
fmt.Printf("Parsing metrics file %s\n", file)
- if parseAndPushMetrics(client, data, labels) {
+ if parseAndPushMetrics(writeAPI, messageType, data, labels) {
fmt.Printf(" SUCCESS: metrics file %s pushed to remote write.\n", file)
continue
}
@@ -101,23 +104,15 @@ func PushMetrics(url *url.URL, roundTripper http.RoundTripper, headers map[strin
return successExitCode
}
-// TODO(bwplotka): Add PRW 2.0 support.
-func parseAndPushMetrics(client *remote.Client, data []byte, labels map[string]string) bool {
+func parseAndPushMetrics(writeAPI *remoteapi.API, messageType remoteapi.WriteMessageType, data []byte, labels map[string]string) bool {
metricsData, err := fmtutil.MetricTextToWriteRequest(bytes.NewReader(data), labels)
if err != nil {
fmt.Fprintln(os.Stderr, " FAILED:", err)
return false
}
- raw, err := metricsData.Marshal()
- if err != nil {
- fmt.Fprintln(os.Stderr, " FAILED:", err)
- return false
- }
-
- // Encode the request body into snappy encoding.
- compressed := snappy.Encode(nil, raw)
- _, err = client.Store(context.Background(), compressed, 0)
+ // Use remoteapi.Write which handles marshaling and compression internally.
+ _, err = writeAPI.Write(context.Background(), messageType, metricsData)
if err != nil {
fmt.Fprintln(os.Stderr, " FAILED:", err)
return false
diff --git a/cmd/promtool/metrics_test.go b/cmd/promtool/metrics_test.go
new file mode 100644
index 00000000000..d5a3bf63cc0
--- /dev/null
+++ b/cmd/promtool/metrics_test.go
@@ -0,0 +1,162 @@
+// Copyright The Prometheus Authors
+// Licensed under the Apache License, Version 2.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 (
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "os"
+ "testing"
+ "time"
+
+ remoteapi "github.com/prometheus/client_golang/exp/api/remote"
+ "github.com/stretchr/testify/require"
+)
+
+// setupTestServer creates a test server with a mock storage for remote write testing.
+func setupTestServer(t *testing.T, supportedTypes remoteapi.MessageTypes) (*httptest.Server, *mockStorage, *url.URL) {
+ t.Helper()
+
+ store := &mockStorage{}
+ handler := remoteapi.NewWriteHandler(store, supportedTypes)
+ server := httptest.NewServer(handler)
+ t.Cleanup(server.Close)
+
+ serverURL, err := url.Parse(server.URL)
+ require.NoError(t, err)
+
+ return server, store, serverURL
+}
+
+// createMetricsFile creates a temporary file with the given metrics data.
+func createMetricsFile(t *testing.T, metricsData string) string {
+ t.Helper()
+
+ tmpFile := t.TempDir() + "/metrics.txt"
+ err := os.WriteFile(tmpFile, []byte(metricsData), 0o644)
+ require.NoError(t, err)
+
+ return tmpFile
+}
+
+func TestPushMetrics(t *testing.T) {
+ tests := []struct {
+ name string
+ metricsData string
+ protoMsg string
+ serverTypes remoteapi.MessageTypes
+ }{
+ {
+ name: "successful push with gauge metrics (V1)",
+ metricsData: `# HELP test_metric A test metric
+# TYPE test_metric gauge
+test_metric{label="value1"} 42.0
+test_metric{label="value2"} 43.0
+`,
+ protoMsg: "prometheus.WriteRequest",
+ serverTypes: remoteapi.MessageTypes{remoteapi.WriteV1MessageType},
+ },
+ {
+ name: "successful push with counter metrics (V1)",
+ metricsData: `# HELP test_counter A test counter
+# TYPE test_counter counter
+test_counter 100
+`,
+ protoMsg: "prometheus.WriteRequest",
+ serverTypes: remoteapi.MessageTypes{remoteapi.WriteV1MessageType},
+ },
+ {
+ name: "successful push with gauge metrics (V2)",
+ metricsData: `# HELP test_metric A test metric
+# TYPE test_metric gauge
+test_metric{label="value1"} 42.0
+test_metric{label="value2"} 43.0
+`,
+ protoMsg: "io.prometheus.write.v2.Request",
+ serverTypes: remoteapi.MessageTypes{remoteapi.WriteV2MessageType},
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ _, store, serverURL := setupTestServer(t, tc.serverTypes)
+ tmpFile := createMetricsFile(t, tc.metricsData)
+
+ status := PushMetrics(
+ serverURL,
+ http.DefaultTransport,
+ map[string]string{},
+ 30*time.Second,
+ tc.protoMsg,
+ map[string]string{"job": "test"},
+ tmpFile,
+ )
+
+ require.Equal(t, successExitCode, status)
+ require.True(t, store.called, "Handler should have been called")
+ require.NoError(t, store.lastErr, "Handler should not have returned an error")
+ require.NotEmpty(t, store.receivedData, "Request should contain data (compression and decompression successful)")
+ require.Contains(t, store.receivedContentType, "application/x-protobuf", "Content-Type should be protobuf")
+ })
+ }
+}
+
+// mockStorage is a simple mock for testing the remote write handler.
+type mockStorage struct {
+ called bool
+ lastErr error
+ receivedData []byte
+ receivedContentType string
+}
+
+func (m *mockStorage) Store(req *http.Request, _ remoteapi.WriteMessageType) (*remoteapi.WriteResponse, error) {
+ m.called = true
+
+ // Capture content-type header.
+ m.receivedContentType = req.Header.Get("Content-Type")
+
+ if req.Body != nil {
+ data, err := io.ReadAll(req.Body)
+ if err == nil {
+ m.receivedData = data
+ }
+ }
+
+ if m.lastErr != nil {
+ return nil, m.lastErr
+ }
+ resp := remoteapi.NewWriteResponse()
+ resp.SetStatusCode(http.StatusNoContent)
+ return resp, nil
+}
+
+func TestPushMetricsInvalidProtoMsg(t *testing.T) {
+ _, store, serverURL := setupTestServer(t, remoteapi.MessageTypes{remoteapi.WriteV1MessageType})
+ tmpFile := createMetricsFile(t, "test_metric 1\n")
+
+ status := PushMetrics(
+ serverURL,
+ http.DefaultTransport,
+ map[string]string{},
+ 30*time.Second,
+ "blablalba", // Invalid protoMsg.
+ map[string]string{"job": "test"},
+ tmpFile,
+ )
+
+ require.Equal(t, failureExitCode, status)
+ require.False(t, store.called, "Handler should not have been called for invalid protoMsg")
+}
diff --git a/cmd/promtool/query.go b/cmd/promtool/query.go
index 0d7cb12cf42..ef0a2368ef3 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
@@ -61,8 +61,8 @@ func newAPI(url *url.URL, roundTripper http.RoundTripper, headers map[string]str
}
// QueryInstant performs an instant query against a Prometheus server.
-func QueryInstant(url *url.URL, roundTripper http.RoundTripper, query, evalTime string, p printer) int {
- api, err := newAPI(url, roundTripper, nil)
+func QueryInstant(url *url.URL, roundTripper http.RoundTripper, headers map[string]string, query, evalTime string, p printer) int {
+ api, err := newAPI(url, roundTripper, headers)
if err != nil {
fmt.Fprintln(os.Stderr, "error creating API client:", err)
return failureExitCode
diff --git a/cmd/promtool/rules.go b/cmd/promtool/rules.go
index 5a18644842b..bb45178e9c8 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,20 +15,20 @@ package main
import (
"context"
+ "errors"
"fmt"
+ "log/slog"
"time"
- "github.com/go-kit/log"
- "github.com/go-kit/log/level"
v1 "github.com/prometheus/client_golang/api/prometheus/v1"
"github.com/prometheus/common/model"
+ "github.com/prometheus/common/promslog"
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/model/timestamp"
"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
@@ -38,7 +38,7 @@ type queryRangeAPI interface {
}
type ruleImporter struct {
- logger log.Logger
+ logger *slog.Logger
config ruleImporterConfig
apiClient queryRangeAPI
@@ -48,28 +48,31 @@ type ruleImporter struct {
}
type ruleImporterConfig struct {
- outputDir string
- start time.Time
- end time.Time
- evalInterval time.Duration
- maxBlockDuration time.Duration
+ outputDir string
+ start time.Time
+ end time.Time
+ evalInterval time.Duration
+ maxBlockDuration time.Duration
+ nameValidationScheme model.ValidationScheme
}
// newRuleImporter creates a new rule importer that can be used to parse and evaluate recording rule files and create new series
// written to disk in blocks.
-func newRuleImporter(logger log.Logger, config ruleImporterConfig, apiClient queryRangeAPI) *ruleImporter {
- level.Info(logger).Log("backfiller", "new rule importer", "start", config.start.Format(time.RFC822), "end", config.end.Format(time.RFC822))
+func newRuleImporter(logger *slog.Logger, config ruleImporterConfig, apiClient queryRangeAPI) *ruleImporter {
+ logger.Info("new rule importer", "component", "backfiller", "start", config.start.Format(time.RFC822), "end", config.end.Format(time.RFC822))
return &ruleImporter{
- logger: logger,
- config: config,
- apiClient: apiClient,
- ruleManager: rules.NewManager(&rules.ManagerOptions{}),
+ logger: logger,
+ config: config,
+ apiClient: apiClient,
+ ruleManager: rules.NewManager(&rules.ManagerOptions{
+ NameValidationScheme: config.nameValidationScheme,
+ }),
}
}
// loadGroups parses groups from a list of recording rule files.
func (importer *ruleImporter) loadGroups(_ context.Context, filenames []string) (errs []error) {
- groups, errs := importer.ruleManager.LoadGroups(importer.config.evalInterval, labels.Labels{}, "", nil, filenames...)
+ groups, errs := importer.ruleManager.LoadGroups(importer.config.evalInterval, labels.Labels{}, "", nil, false, filenames...)
if errs != nil {
return errs
}
@@ -80,10 +83,10 @@ func (importer *ruleImporter) loadGroups(_ context.Context, filenames []string)
// importAll evaluates all the recording rules and creates new time series and writes them to disk in blocks.
func (importer *ruleImporter) importAll(ctx context.Context) (errs []error) {
for name, group := range importer.groups {
- level.Info(importer.logger).Log("backfiller", "processing group", "name", name)
+ importer.logger.Info("processing group", "component", "backfiller", "name", name)
for i, r := range group.Rules() {
- level.Info(importer.logger).Log("backfiller", "processing rule", "id", i, "name", r.Name())
+ importer.logger.Info("processing rule", "component", "backfiller", "id", i, "name", r.Name())
if err := importer.importRule(ctx, r.Query().String(), r.Name(), r.Labels(), importer.config.start, importer.config.end, int64(importer.config.maxBlockDuration/time.Millisecond), group); err != nil {
errs = append(errs, err)
}
@@ -124,7 +127,7 @@ func (importer *ruleImporter) importRule(ctx context.Context, ruleExpr, ruleName
return fmt.Errorf("query range: %w", err)
}
if warnings != nil {
- level.Warn(importer.logger).Log("msg", "Range query returned warnings.", "warnings", warnings)
+ importer.logger.Warn("Range query returned warnings.", "warnings", warnings)
}
// To prevent races with compaction, a block writer only allows appending samples
@@ -133,14 +136,14 @@ func (importer *ruleImporter) importRule(ctx context.Context, ruleExpr, ruleName
// also need to append samples throughout the whole block range. To allow that, we
// pretend that the block is twice as large here, but only really add sample in the
// original interval later.
- w, err := tsdb.NewBlockWriter(log.NewNopLogger(), importer.config.outputDir, 2*blockDuration)
+ w, err := tsdb.NewBlockWriter(promslog.NewNopLogger(), importer.config.outputDir, 2*blockDuration)
if err != nil {
return fmt.Errorf("new block writer: %w", err)
}
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)
@@ -178,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 d55fb0c8963..678e2b4d506 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
@@ -21,9 +21,9 @@ import (
"testing"
"time"
- "github.com/go-kit/log"
v1 "github.com/prometheus/client_golang/api/prometheus/v1"
"github.com/prometheus/common/model"
+ "github.com/prometheus/common/promslog"
"github.com/stretchr/testify/require"
"github.com/prometheus/prometheus/model/labels"
@@ -35,7 +35,7 @@ type mockQueryRangeAPI struct {
samples model.Matrix
}
-func (mockAPI mockQueryRangeAPI) QueryRange(_ context.Context, query string, r v1.Range, opts ...v1.Option) (model.Value, v1.Warnings, error) {
+func (mockAPI mockQueryRangeAPI) QueryRange(context.Context, string, v1.Range, ...v1.Option) (model.Value, v1.Warnings, error) {
return mockAPI.samples, v1.Warnings{}, nil
}
@@ -43,6 +43,7 @@ const defaultBlockDuration = time.Duration(tsdb.DefaultBlockDuration) * time.Mil
// TestBackfillRuleIntegration is an integration test that runs all the rule importer code to confirm the parts work together.
func TestBackfillRuleIntegration(t *testing.T) {
+ t.Parallel()
const (
testMaxSampleCount = 50
testValue = 123
@@ -72,6 +73,7 @@ func TestBackfillRuleIntegration(t *testing.T) {
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
tmpDir := t.TempDir()
ctx := context.Background()
@@ -161,13 +163,14 @@ func TestBackfillRuleIntegration(t *testing.T) {
}
func newTestRuleImporter(_ context.Context, start time.Time, tmpDir string, testSamples model.Matrix, maxBlockDuration time.Duration) (*ruleImporter, error) {
- logger := log.NewNopLogger()
+ logger := promslog.NewNopLogger()
cfg := ruleImporterConfig{
- outputDir: tmpDir,
- start: start.Add(-10 * time.Hour),
- end: start.Add(-7 * time.Hour),
- evalInterval: 60 * time.Second,
- maxBlockDuration: maxBlockDuration,
+ outputDir: tmpDir,
+ start: start.Add(-10 * time.Hour),
+ end: start.Add(-7 * time.Hour),
+ evalInterval: 60 * time.Second,
+ maxBlockDuration: maxBlockDuration,
+ nameValidationScheme: model.UTF8Validation,
}
return newRuleImporter(logger, cfg, mockQueryRangeAPI{
@@ -210,6 +213,7 @@ func createMultiRuleTestFiles(path string) error {
// TestBackfillLabels confirms that the labels in the rule file override the labels from the metrics
// received from Prometheus Query API, including the __name__ label.
func TestBackfillLabels(t *testing.T) {
+ t.Parallel()
tmpDir := t.TempDir()
ctx := context.Background()
@@ -251,6 +255,7 @@ func TestBackfillLabels(t *testing.T) {
require.NoError(t, err)
t.Run("correct-labels", func(t *testing.T) {
+ t.Parallel()
selectedSeries := q.Select(ctx, false, nil, labels.MustNewMatcher(labels.MatchRegexp, "", ".*"))
for selectedSeries.Next() {
series := selectedSeries.At()
diff --git a/cmd/promtool/sd.go b/cmd/promtool/sd.go
index e65262d439f..6b844c699ab 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
@@ -20,9 +20,9 @@ import (
"os"
"time"
- "github.com/go-kit/log"
"github.com/google/go-cmp/cmp"
"github.com/prometheus/client_golang/prometheus"
+ "github.com/prometheus/common/promslog"
"github.com/prometheus/prometheus/config"
"github.com/prometheus/prometheus/discovery"
@@ -38,10 +38,10 @@ type sdCheckResult struct {
}
// CheckSD performs service discovery for the given job name and reports the results.
-func CheckSD(sdConfigFiles, sdJobName string, sdTimeout time.Duration, noDefaultScrapePort bool, registerer prometheus.Registerer) int {
- logger := log.NewLogfmtLogger(log.NewSyncWriter(os.Stderr))
+func CheckSD(sdConfigFiles, sdJobName string, sdTimeout time.Duration, _ prometheus.Registerer) int {
+ logger := promslog.New(&promslog.Config{})
- cfg, err := config.LoadFile(sdConfigFiles, false, false, logger)
+ cfg, err := config.LoadFile(sdConfigFiles, false, logger)
if err != nil {
fmt.Fprintln(os.Stderr, "Cannot load config", err)
return failureExitCode
@@ -114,7 +114,7 @@ outerLoop:
}
results := []sdCheckResult{}
for _, tgs := range sdCheckResults {
- results = append(results, getSDCheckResult(tgs, scrapeConfig, noDefaultScrapePort)...)
+ results = append(results, getSDCheckResult(tgs, scrapeConfig)...)
}
res, err := json.MarshalIndent(results, "", " ")
@@ -127,7 +127,7 @@ outerLoop:
return successExitCode
}
-func getSDCheckResult(targetGroups []*targetgroup.Group, scrapeConfig *config.ScrapeConfig, noDefaultScrapePort bool) []sdCheckResult {
+func getSDCheckResult(targetGroups []*targetgroup.Group, scrapeConfig *config.ScrapeConfig) []sdCheckResult {
sdCheckResults := []sdCheckResult{}
lb := labels.NewBuilder(labels.EmptyLabels())
for _, targetGroup := range targetGroups {
@@ -144,7 +144,9 @@ func getSDCheckResult(targetGroups []*targetgroup.Group, scrapeConfig *config.Sc
}
}
- res, orig, err := scrape.PopulateLabels(lb, scrapeConfig, noDefaultScrapePort)
+ scrape.PopulateDiscoveredLabels(lb, scrapeConfig, target, targetGroup.Labels)
+ orig := lb.Labels()
+ res, err := scrape.PopulateLabels(lb, scrapeConfig, target, targetGroup.Labels)
result := sdCheckResult{
DiscoveredLabels: orig,
Labels: res,
diff --git a/cmd/promtool/sd_test.go b/cmd/promtool/sd_test.go
index cb65ee72aa6..3d561aeae3f 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
@@ -18,17 +18,17 @@ import (
"time"
"github.com/prometheus/common/model"
+ "github.com/stretchr/testify/require"
"github.com/prometheus/prometheus/config"
"github.com/prometheus/prometheus/discovery/targetgroup"
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/model/relabel"
"github.com/prometheus/prometheus/util/testutil"
-
- "github.com/stretchr/testify/require"
)
func TestSDCheckResult(t *testing.T) {
+ t.Parallel()
targetGroups := []*targetgroup.Group{{
Targets: []model.LabelSet{
map[model.LabelName]model.LabelValue{"__address__": "localhost:8080", "foo": "bar"},
@@ -42,11 +42,12 @@ func TestSDCheckResult(t *testing.T) {
ScrapeInterval: model.Duration(1 * time.Minute),
ScrapeTimeout: model.Duration(10 * time.Second),
RelabelConfigs: []*relabel.Config{{
- SourceLabels: model.LabelNames{"foo"},
- Action: relabel.Replace,
- TargetLabel: "newfoo",
- Regex: reg,
- Replacement: "$1",
+ SourceLabels: model.LabelNames{"foo"},
+ Action: relabel.Replace,
+ TargetLabel: "newfoo",
+ Regex: reg,
+ Replacement: "$1",
+ NameValidationScheme: model.UTF8Validation,
}},
}
@@ -54,13 +55,19 @@ func TestSDCheckResult(t *testing.T) {
{
DiscoveredLabels: labels.FromStrings(
"__address__", "localhost:8080",
+ "__always_scrape_classic_histograms__", "false",
+ "__convert_classic_histograms_to_nhcb__", "false",
"__scrape_interval__", "1m",
+ "__scrape_native_histograms__", "false",
"__scrape_timeout__", "10s",
"foo", "bar",
),
Labels: labels.FromStrings(
"__address__", "localhost:8080",
+ "__always_scrape_classic_histograms__", "false",
+ "__convert_classic_histograms_to_nhcb__", "false",
"__scrape_interval__", "1m",
+ "__scrape_native_histograms__", "false",
"__scrape_timeout__", "10s",
"foo", "bar",
"instance", "localhost:8080",
@@ -70,5 +77,5 @@ func TestSDCheckResult(t *testing.T) {
},
}
- testutil.RequireEqual(t, expectedSDCheckResult, getSDCheckResult(targetGroups, scrapeConfig, true))
+ testutil.RequireEqual(t, expectedSDCheckResult, getSDCheckResult(targetGroups, scrapeConfig))
}
diff --git a/cmd/promtool/testdata/bad-sd-file-extension.nonexistant b/cmd/promtool/testdata/bad-sd-file-extension.nonexistent
similarity index 100%
rename from cmd/promtool/testdata/bad-sd-file-extension.nonexistant
rename to cmd/promtool/testdata/bad-sd-file-extension.nonexistent
diff --git a/cmd/promtool/testdata/config_with_service_discovery_files.yml b/cmd/promtool/testdata/config_with_service_discovery_files.yml
index 13b6d7faffb..6a550a84034 100644
--- a/cmd/promtool/testdata/config_with_service_discovery_files.yml
+++ b/cmd/promtool/testdata/config_with_service_discovery_files.yml
@@ -6,7 +6,7 @@ scrape_configs:
alerting:
alertmanagers:
- scheme: http
- api_version: v1
+ api_version: v2
file_sd_configs:
- files:
- nonexistent_file.yml
diff --git a/cmd/promtool/testdata/dump-series-1.prom b/cmd/promtool/testdata/dump-series-1.prom
new file mode 100644
index 00000000000..5e44c0bf1b4
--- /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 00000000000..fefefa6d1b8
--- /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 00000000000..dd98e8707d8
--- /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
new file mode 100644
index 00000000000..946e07d0d76
--- /dev/null
+++ b/cmd/promtool/testdata/features.yml
@@ -0,0 +1,10 @@
+groups:
+ - name: features
+ rules:
+ # 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/prometheus-config.lint.too_long_scrape_interval.yml b/cmd/promtool/testdata/prometheus-config.lint.too_long_scrape_interval.yml
new file mode 100644
index 00000000000..fe21d21707f
--- /dev/null
+++ b/cmd/promtool/testdata/prometheus-config.lint.too_long_scrape_interval.yml
@@ -0,0 +1,5 @@
+scrape_configs:
+ - job_name: too_long_scrape_interval_test
+ scrape_interval: 10m
+rule_files:
+ - prometheus-config.rules.good.yml
diff --git a/cmd/promtool/testdata/prometheus-config.rules.good.yml b/cmd/promtool/testdata/prometheus-config.rules.good.yml
new file mode 100644
index 00000000000..4529a3ec865
--- /dev/null
+++ b/cmd/promtool/testdata/prometheus-config.rules.good.yml
@@ -0,0 +1,3 @@
+groups:
+ - name: rules
+ rules: []
diff --git a/cmd/promtool/testdata/rules_extrafields.yml b/cmd/promtool/testdata/rules_extrafields.yml
new file mode 100644
index 00000000000..85ef079bb83
--- /dev/null
+++ b/cmd/promtool/testdata/rules_extrafields.yml
@@ -0,0 +1,33 @@
+# This is the rules file. It has an extra "ownership"
+# field in the second group. promtool should ignore this field
+# and not return an error with --ignore-unknown-fields.
+
+groups:
+ - name: alerts
+ namespace: "foobar"
+ rules:
+ - alert: InstanceDown
+ expr: up == 0
+ for: 5m
+ labels:
+ severity: page
+ annotations:
+ summary: "Instance {{ $labels.instance }} down"
+ description: "{{ $labels.instance }} of job {{ $labels.job }} has been down for more than 5 minutes."
+ - alert: AlwaysFiring
+ expr: 1
+
+ - name: rules
+ ownership:
+ service: "test"
+ rules:
+ - record: job:test:count_over_time1m
+ expr: sum without(instance) (count_over_time(test[1m]))
+
+ # A recording rule that doesn't depend on input series.
+ - record: fixed_data
+ expr: 1
+
+ # Subquery with default resolution test.
+ - record: suquery_interval_test
+ expr: count_over_time(up[5m:])
diff --git a/cmd/promtool/testdata/rules_run_extrafields.yml b/cmd/promtool/testdata/rules_run_extrafields.yml
new file mode 100644
index 00000000000..86879fc396b
--- /dev/null
+++ b/cmd/promtool/testdata/rules_run_extrafields.yml
@@ -0,0 +1,21 @@
+# Minimal test case to see that --ignore-unknown-fields
+# is working as expected. It should not return an error
+# when any extra fields are present in the rules file.
+rule_files:
+ - rules_extrafields.yml
+
+evaluation_interval: 1m
+
+
+tests:
+ - name: extra ownership field test
+ input_series:
+ - series: test
+ values: 1
+
+ promql_expr_test:
+ - expr: test
+ eval_time: 0
+ exp_samples:
+ - value: 1
+ labels: test
diff --git a/cmd/promtool/testdata/rules_run_fuzzy.yml b/cmd/promtool/testdata/rules_run_fuzzy.yml
new file mode 100644
index 00000000000..3bf4e47a45b
--- /dev/null
+++ b/cmd/promtool/testdata/rules_run_fuzzy.yml
@@ -0,0 +1,43 @@
+# Minimal test case to see that fuzzy compare is working as expected.
+# It should allow slight floating point differences through. Larger
+# floating point differences should still fail.
+
+evaluation_interval: 1m
+fuzzy_compare: true
+
+tests:
+ - name: correct fuzzy match
+ input_series:
+ - series: test_low
+ values: 2.9999999999999996
+ - series: test_high
+ values: 3.0000000000000004
+ promql_expr_test:
+ - expr: test_low
+ eval_time: 0
+ exp_samples:
+ - labels: test_low
+ value: 3
+ - expr: test_high
+ eval_time: 0
+ exp_samples:
+ - labels: test_high
+ value: 3
+
+ - name: wrong fuzzy match
+ input_series:
+ - series: test_low
+ values: 2.9999999999999987
+ - series: test_high
+ values: 3.0000000000000013
+ promql_expr_test:
+ - expr: test_low
+ eval_time: 0
+ exp_samples:
+ - labels: test_low
+ value: 3
+ - expr: test_high
+ eval_time: 0
+ exp_samples:
+ - labels: test_high
+ value: 3
diff --git a/cmd/promtool/testdata/rules_run_no_fuzzy.yml b/cmd/promtool/testdata/rules_run_no_fuzzy.yml
new file mode 100644
index 00000000000..eba201a28c1
--- /dev/null
+++ b/cmd/promtool/testdata/rules_run_no_fuzzy.yml
@@ -0,0 +1,24 @@
+# Minimal test case to see that fuzzy compare can be turned off,
+# and slight floating point differences fail matching.
+
+evaluation_interval: 1m
+fuzzy_compare: false
+
+tests:
+ - name: correct fuzzy match
+ input_series:
+ - series: test_low
+ values: 2.9999999999999996
+ - series: test_high
+ values: 3.0000000000000004
+ promql_expr_test:
+ - expr: test_low
+ eval_time: 0
+ exp_samples:
+ - labels: test_low
+ value: 3
+ - expr: test_high
+ eval_time: 0
+ exp_samples:
+ - labels: test_high
+ value: 3
diff --git a/cmd/promtool/testdata/start-time-test.yml b/cmd/promtool/testdata/start-time-test.yml
new file mode 100644
index 00000000000..b7365366f47
--- /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/testdata/unittest.yml b/cmd/promtool/testdata/unittest.yml
index ff511729ba3..e2a8230902e 100644
--- a/cmd/promtool/testdata/unittest.yml
+++ b/cmd/promtool/testdata/unittest.yml
@@ -69,13 +69,13 @@ tests:
eval_time: 2m
exp_samples:
- labels: "test_histogram_repeat"
- histogram: "{{count:2 sum:3 buckets:[2]}}"
+ histogram: "{{count:2 sum:3 counter_reset_hint:not_reset buckets:[2]}}"
- expr: test_histogram_increase
eval_time: 2m
exp_samples:
- labels: "test_histogram_increase"
- histogram: "{{count:4 sum:5.6 buckets:[4]}}"
+ histogram: "{{count:4 sum:5.6 counter_reset_hint:not_reset buckets:[4]}}"
# Ensure a value is stale as soon as it is marked as such.
- expr: test_stale
@@ -89,11 +89,11 @@ tests:
# Ensure lookback delta is respected, when a value is missing.
- expr: timestamp(test_missing)
- eval_time: 5m
+ eval_time: 4m59s
exp_samples:
- value: 0
- expr: timestamp(test_missing)
- eval_time: 5m1s
+ eval_time: 5m
exp_samples: []
# Minimal test case to check edge case of a single sample.
@@ -113,7 +113,7 @@ tests:
- expr: count_over_time(fixed_data[1h])
eval_time: 1h
exp_samples:
- - value: 61
+ - value: 60
- expr: timestamp(fixed_data)
eval_time: 1h
exp_samples:
@@ -183,7 +183,7 @@ tests:
- expr: job:test:count_over_time1m
eval_time: 1m
exp_samples:
- - value: 61
+ - value: 60
labels: 'job:test:count_over_time1m{job="test"}'
- expr: timestamp(job:test:count_over_time1m)
eval_time: 1m10s
@@ -194,7 +194,7 @@ tests:
- expr: job:test:count_over_time1m
eval_time: 2m
exp_samples:
- - value: 61
+ - value: 60
labels: 'job:test:count_over_time1m{job="test"}'
- expr: timestamp(job:test:count_over_time1m)
eval_time: 2m59s999ms
diff --git a/cmd/promtool/tsdb.go b/cmd/promtool/tsdb.go
index 971ea8ab000..13c544d5afc 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,9 +17,11 @@ import (
"bufio"
"bytes"
"context"
+ "encoding/json"
"errors"
"fmt"
"io"
+ "log/slog"
"os"
"path/filepath"
"runtime"
@@ -32,7 +34,7 @@ import (
"time"
"github.com/alecthomas/units"
- "github.com/go-kit/log"
+ "github.com/prometheus/common/promslog"
"go.uber.org/atomic"
"github.com/prometheus/prometheus/model/labels"
@@ -41,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"
)
@@ -60,7 +61,7 @@ type writeBenchmark struct {
memprof *os.File
blockprof *os.File
mtxprof *os.File
- logger log.Logger
+ logger *slog.Logger
}
func benchmarkWrite(outPath, samplesFile string, numMetrics, numScrapes int) error {
@@ -68,7 +69,7 @@ func benchmarkWrite(outPath, samplesFile string, numMetrics, numScrapes int) err
outPath: outPath,
samplesFile: samplesFile,
numMetrics: numMetrics,
- logger: log.NewLogfmtLogger(log.NewSyncWriter(os.Stderr)),
+ logger: promslog.New(&promslog.Config{}),
}
if b.outPath == "" {
dir, err := os.MkdirTemp("", "tsdb_bench")
@@ -87,9 +88,7 @@ func benchmarkWrite(outPath, samplesFile string, numMetrics, numScrapes int) err
dir := filepath.Join(b.outPath, "storage")
- l := log.With(b.logger, "ts", log.DefaultTimestampUTC, "caller", log.DefaultCaller)
-
- st, err := tsdb.Open(dir, l, nil, &tsdb.Options{
+ st, err := tsdb.Open(dir, b.logger, nil, &tsdb.Options{
RetentionDuration: int64(15 * 24 * time.Hour / time.Millisecond),
MinBlockDuration: int64(2 * time.Hour / time.Millisecond),
}, tsdb.NewDBStats())
@@ -156,24 +155,18 @@ func (b *writeBenchmark) ingestScrapes(lbls []labels.Labels, scrapeCount int) (u
var wg sync.WaitGroup
lbls := lbls
for len(lbls) > 0 {
- l := 1000
- if len(lbls) < 1000 {
- l = len(lbls)
- }
+ l := min(len(lbls), 1000)
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()
}
@@ -201,7 +194,7 @@ func (b *writeBenchmark) ingestScrapesShard(lbls []labels.Labels, scrapeCount in
}
total := uint64(0)
- for i := 0; i < scrapeCount; i++ {
+ for range scrapeCount {
app := b.storage.Appender(context.TODO())
ts += timeDelta
@@ -315,12 +308,11 @@ func readPrometheusLabels(r io.Reader, n int) ([]labels.Labels, error) {
i := 0
for scanner.Scan() && i < n {
- m := make([]labels.Label, 0, 10)
-
r := strings.NewReplacer("\"", "", "{", "", "}", "")
s := r.Replace(scanner.Text())
labelChunks := strings.Split(s, ",")
+ m := make([]labels.Label, 0, len(labelChunks))
for _, labelChunk := range labelChunks {
split := strings.Split(labelChunk, ":")
m = append(m, labels.Label{Name: split[0], Value: split[1]})
@@ -343,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 {
@@ -367,25 +359,25 @@ func printBlocks(blocks []tsdb.BlockReader, writeHeader, humanReadable bool) {
fmt.Fprintf(tw,
"%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\n",
meta.ULID,
- getFormatedTime(meta.MinTime, humanReadable),
- getFormatedTime(meta.MaxTime, humanReadable),
+ getFormattedTime(meta.MinTime, humanReadable),
+ getFormattedTime(meta.MaxTime, humanReadable),
time.Duration(meta.MaxTime-meta.MinTime)*time.Millisecond,
meta.Stats.NumSamples,
meta.Stats.NumChunks,
meta.Stats.NumSeries,
- getFormatedBytes(b.Size(), humanReadable),
+ getFormattedBytes(b.Size(), humanReadable),
)
}
}
-func getFormatedTime(timestamp int64, humanReadable bool) string {
+func getFormattedTime(timestamp int64, humanReadable bool) string {
if humanReadable {
return time.Unix(timestamp/1000, 0).UTC().String()
}
return strconv.FormatInt(timestamp, 10)
}
-func getFormatedBytes(bytes int64, humanReadable bool) string {
+func getFormattedBytes(bytes int64, humanReadable bool) string {
if humanReadable {
return units.Base2Bytes(bytes).String()
}
@@ -405,7 +397,7 @@ func openBlock(path, blockID string) (*tsdb.DBReadOnly, tsdb.BlockReader, error)
}
}
- b, err := db.Block(blockID)
+ b, err := db.Block(blockID, tsdb.DefaultPostingsDecoderFactory)
if err != nil {
return nil, nil, err
}
@@ -413,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)
+ if matchers != "" {
+ selectors, err = p.ParseMetricSelector(matchers)
if err != nil {
return err
}
@@ -429,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()
@@ -437,7 +429,7 @@ func analyzeBlock(ctx context.Context, path, blockID string, limit int, runExten
// Presume 1ms resolution that Prometheus uses.
fmt.Printf("Duration: %s\n", (time.Duration(meta.MaxTime-meta.MinTime) * 1e6).String())
fmt.Printf("Total Series: %d\n", meta.Stats.NumSeries)
- if len(matchers) > 0 {
+ if matchers != "" {
fmt.Printf("Matcher: %s\n", matchers)
}
ir, err := block.Index()
@@ -483,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...)
+ if matchers != "" {
+ 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
}
@@ -508,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.
@@ -522,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)
@@ -554,7 +546,7 @@ func analyzeBlock(ctx context.Context, path, blockID string, limit int, runExten
postingInfos = postingInfos[:0]
for _, n := range allLabelNames {
- values, err := ir.SortedLabelValues(ctx, n, selectors...)
+ values, err := ir.SortedLabelValues(ctx, n, nil, selectors...)
if err != nil {
return err
}
@@ -570,7 +562,7 @@ func analyzeBlock(ctx context.Context, path, blockID string, limit int, runExten
postingInfos = postingInfos[:0]
for _, n := range allLabelNames {
- lv, err := ir.SortedLabelValues(ctx, n, selectors...)
+ lv, err := ir.SortedLabelValues(ctx, n, nil, selectors...)
if err != nil {
return err
}
@@ -580,7 +572,7 @@ func analyzeBlock(ctx context.Context, path, blockID string, limit int, runExten
printInfo(postingInfos)
postingInfos = postingInfos[:0]
- lv, err := ir.SortedLabelValues(ctx, "__name__", selectors...)
+ lv, err := ir.SortedLabelValues(ctx, "__name__", nil, selectors...)
if err != nil {
return err
}
@@ -589,7 +581,10 @@ func analyzeBlock(ctx context.Context, path, blockID string, limit int, runExten
if err != nil {
return err
}
- postings = index.Intersect(postings, index.NewListPostings(refs))
+ // Only intersect postings if matchers are specified.
+ if matchers != "" {
+ postings = index.Intersect(postings, index.NewListPostings(refs))
+ }
count := 0
for postings.Next() {
count++
@@ -626,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
@@ -662,7 +657,7 @@ func analyzeCompaction(ctx context.Context, block tsdb.BlockReader, indexr tsdb.
histogramChunkSize = append(histogramChunkSize, len(chk.Bytes()))
fhchk, ok := chk.(*chunkenc.FloatHistogramChunk)
if !ok {
- return fmt.Errorf("chunk is not FloatHistogramChunk")
+ return errors.New("chunk is not FloatHistogramChunk")
}
it := fhchk.Iterator(nil)
bucketCount := 0
@@ -677,7 +672,7 @@ func analyzeCompaction(ctx context.Context, block tsdb.BlockReader, indexr tsdb.
histogramChunkSize = append(histogramChunkSize, len(chk.Bytes()))
hchk, ok := chk.(*chunkenc.HistogramChunk)
if !ok {
- return fmt.Errorf("chunk is not HistogramChunk")
+ return errors.New("chunk is not HistogramChunk")
}
it := hchk.Iterator(nil)
bucketCount := 0
@@ -708,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 {
@@ -722,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
}
@@ -733,7 +728,7 @@ func dumpSamples(ctx context.Context, dbDir, sandboxDirRoot string, mint, maxt i
for _, mset := range matcherSets {
sets = append(sets, q.Select(ctx, true, nil, mset...))
}
- ss = storage.NewMergeSeriesSet(sets, storage.ChainedSeriesMerge)
+ ss = storage.NewMergeSeriesSet(sets, 0, storage.ChainedSeriesMerge)
} else {
ss = q.Select(ctx, false, nil, matcherSets[0]...)
}
@@ -744,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 {
@@ -796,12 +791,36 @@ 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()
lbs := series.Labels()
metricName := lbs.Get(labels.MetricName)
- lbs = lbs.DropMetricName()
+ lbs = lbs.DropReserved(func(n string) bool { return n == labels.MetricName })
it := series.Iterator(nil)
for it.Next() == chunkenc.ValFloat {
ts, val := it.At()
@@ -824,17 +843,31 @@ func checkErr(err error) int {
}
func backfillOpenMetrics(path, outputDir string, humanReadable, quiet bool, maxBlockDuration time.Duration, customLabels map[string]string) int {
- inputFile, err := fileutil.OpenMmapFile(path)
+ var buf []byte
+ info, err := os.Stat(path)
if err != nil {
return checkErr(err)
}
- defer inputFile.Close()
+ if info.Mode()&(os.ModeNamedPipe|os.ModeCharDevice) != 0 {
+ // Read the pipe chunks by chunks as it cannot be mmap-ed
+ buf, err = os.ReadFile(path)
+ if err != nil {
+ return checkErr(err)
+ }
+ } else {
+ inputFile, err := fileutil.OpenMmapFile(path)
+ if err != nil {
+ return checkErr(err)
+ }
+ defer inputFile.Close()
+ buf = inputFile.Bytes()
+ }
if err := os.MkdirAll(outputDir, 0o777); err != nil {
return checkErr(fmt.Errorf("create output dir: %w", err))
}
- return checkErr(backfill(5000, inputFile.Bytes(), outputDir, humanReadable, quiet, maxBlockDuration, customLabels))
+ return checkErr(backfill(5000, buf, outputDir, humanReadable, quiet, maxBlockDuration, customLabels))
}
func displayHistogram(dataType string, datas []int, total int) {
@@ -877,5 +910,5 @@ func generateBucket(minVal, maxVal int) (start, end, step int) {
start = minVal - minVal%step
end = maxVal - maxVal%step + step
- return
+ return start, end, step
}
diff --git a/cmd/promtool/tsdb_posix_test.go b/cmd/promtool/tsdb_posix_test.go
new file mode 100644
index 00000000000..9d0034844f7
--- /dev/null
+++ b/cmd/promtool/tsdb_posix_test.go
@@ -0,0 +1,69 @@
+// Copyright The Prometheus Authors
+// Licensed under the Apache License, Version 2.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 !windows
+
+package main
+
+import (
+ "bytes"
+ "io"
+ "math"
+ "os"
+ "path"
+ "syscall"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/prometheus/prometheus/tsdb"
+)
+
+func TestTSDBDumpOpenMetricsRoundTripPipe(t *testing.T) {
+ initialMetrics, err := os.ReadFile("testdata/dump-openmetrics-roundtrip-test.prom")
+ require.NoError(t, err)
+ initialMetrics = normalizeNewLine(initialMetrics)
+
+ pipeDir := t.TempDir()
+ dbDir := t.TempDir()
+
+ // create pipe
+ pipe := path.Join(pipeDir, "pipe")
+ err = syscall.Mkfifo(pipe, 0o666)
+ require.NoError(t, err)
+
+ go func() {
+ // open pipe to write
+ in, err := os.OpenFile(pipe, os.O_WRONLY, os.ModeNamedPipe)
+ require.NoError(t, err)
+ defer func() { require.NoError(t, in.Close()) }()
+ _, err = io.Copy(in, bytes.NewReader(initialMetrics))
+ require.NoError(t, err)
+ }()
+
+ // Import samples from OM format
+ code := backfillOpenMetrics(pipe, dbDir, false, false, 2*time.Hour, map[string]string{})
+ require.Equal(t, 0, code)
+ db, err := tsdb.Open(dbDir, nil, nil, tsdb.DefaultOptions(), nil)
+ require.NoError(t, err)
+ t.Cleanup(func() {
+ require.NoError(t, db.Close())
+ })
+
+ // Dump the blocks into OM format
+ dumpedMetrics := getDumpedSamples(t, dbDir, "", math.MinInt64, math.MaxInt64, []string{"{__name__=~'(?s:.*)'}"}, formatSeriesSetOpenMetrics)
+
+ // Should get back the initial metrics.
+ require.Equal(t, string(initialMetrics), dumpedMetrics)
+}
diff --git a/cmd/promtool/tsdb_test.go b/cmd/promtool/tsdb_test.go
index 90192e31ab9..86d7c67d774 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,11 +27,13 @@ import (
"github.com/stretchr/testify/require"
+ "github.com/prometheus/prometheus/promql/parser"
"github.com/prometheus/prometheus/promql/promqltest"
"github.com/prometheus/prometheus/tsdb"
)
func TestGenerateBucket(t *testing.T) {
+ t.Parallel()
tcs := []struct {
min, max int
start, end, step int
@@ -62,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,
@@ -70,6 +72,7 @@ func getDumpedSamples(t *testing.T, databasePath, sandboxDirRoot string, mint, m
maxt,
match,
formatter,
+ parser.NewParser(parser.Options{}),
)
require.NoError(t, err)
@@ -96,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
@@ -105,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",
@@ -120,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 {
@@ -165,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))
})
}
}
@@ -181,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 7030635d1c0..d63908efc6a 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
@@ -19,20 +19,21 @@ import (
"errors"
"fmt"
"io"
+ "math"
"os"
"path/filepath"
+ "slices"
"sort"
"strconv"
"strings"
"time"
- "github.com/go-kit/log"
"github.com/google/go-cmp/cmp"
"github.com/grafana/regexp"
"github.com/nsf/jsondiff"
- "gopkg.in/yaml.v2"
-
"github.com/prometheus/common/model"
+ "github.com/prometheus/common/promslog"
+ "go.yaml.in/yaml/v2"
"github.com/prometheus/prometheus/model/histogram"
"github.com/prometheus/prometheus/model/labels"
@@ -46,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 bool, files ...string) int {
- return RulesUnitTestResult(io.Discard, queryOpts, runStrings, diffFlag, 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 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{}
@@ -60,7 +61,7 @@ func RulesUnitTestResult(results io.Writer, queryOpts promqltest.LazyLoaderOpts,
}
for _, f := range files {
- if errs := ruleUnitTest(f, queryOpts, run, diffFlag, 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())
@@ -82,7 +83,7 @@ func RulesUnitTestResult(results io.Writer, queryOpts promqltest.LazyLoaderOpts,
return successExitCode
}
-func ruleUnitTest(filename string, queryOpts promqltest.LazyLoaderOpts, run *regexp.Regexp, diffFlag 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)
@@ -131,7 +132,8 @@ func ruleUnitTest(filename string, queryOpts promqltest.LazyLoaderOpts, run *reg
if t.Interval == 0 {
t.Interval = unitTestInp.EvaluationInterval
}
- ers := t.test(evalInterval, groupOrderMap, queryOpts, diffFlag, unitTestInp.RuleFiles...)
+ t.parser = p
+ ers := t.test(testname, evalInterval, groupOrderMap, queryOpts, diffFlag, debug, ignoreUnknownFields, unitTestInp.FuzzyCompare, unitTestInp.RuleFiles...)
if ers != nil {
for _, e := range ers {
tc.Fail(e.Error())
@@ -160,6 +162,7 @@ type unitTestFile struct {
EvaluationInterval model.Duration `yaml:"evaluation_interval,omitempty"`
GroupEvalOrder []string `yaml:"group_eval_order"`
Tests []testGroup `yaml:"tests"`
+ FuzzyCompare bool `yaml:"fuzzy_compare,omitempty"`
}
// resolveAndGlobFilepaths joins all relative paths in a configuration
@@ -186,20 +189,53 @@ 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.
-func (tg *testGroup) test(evalInterval time.Duration, groupOrderMap map[string]int, queryOpts promqltest.LazyLoaderOpts, diffFlag bool, ruleFiles ...string) (outErr []error) {
+func (tg *testGroup) test(testname string, evalInterval time.Duration, groupOrderMap map[string]int, queryOpts promqltest.LazyLoaderOpts, diffFlag, debug, ignoreUnknownFields, fuzzyCompare bool, ruleFiles ...string) (outErr []error) {
+ if debug {
+ testStart := time.Now()
+ fmt.Fprintf(os.Stderr, "DEBUG: Starting test %s\n", testname)
+ defer func() {
+ fmt.Fprintf(os.Stderr, "DEBUG: Test %s finished, took %v\n", testname, time.Since(testStart))
+ }()
+ }
// 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}
@@ -217,20 +253,34 @@ func (tg *testGroup) test(evalInterval time.Duration, groupOrderMap map[string]i
QueryFunc: rules.EngineQueryFunc(suite.QueryEngine(), suite.Storage()),
Appendable: suite.Storage(),
Context: context.Background(),
- NotifyFunc: func(ctx context.Context, expr string, alerts ...*rules.Alert) {},
- Logger: log.NewNopLogger(),
+ 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, ruleFiles...)
+ groupsMap, ers := m.LoadGroups(time.Duration(tg.Interval), tg.ExternalLabels, tg.ExternalURL, nil, ignoreUnknownFields, ruleFiles...)
if ers != nil {
return ers
}
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.
+ var compareFloat64 cmp.Option = cmp.Options{}
+ if fuzzyCompare {
+ compareFloat64 = cmp.Comparer(func(x, y float64) bool {
+ return x == y || math.Nextafter(x, math.Inf(-1)) == y || math.Nextafter(x, math.Inf(1)) == y
+ })
+ }
+
// Pre-processing some data for testing alerts.
// All this preparation is so that we can test alerts as we evaluate the rules.
// This avoids storing them in memory, as the number of evals might be high.
@@ -262,9 +312,7 @@ func (tg *testGroup) test(evalInterval time.Duration, groupOrderMap map[string]i
for k := range alertEvalTimesMap {
alertEvalTimes = append(alertEvalTimes, k)
}
- sort.Slice(alertEvalTimes, func(i, j int) bool {
- return alertEvalTimes[i] < alertEvalTimes[j]
- })
+ slices.Sort(alertEvalTimes)
// Current index in alertEvalTimes what we are looking at.
curr := 0
@@ -305,12 +353,8 @@ func (tg *testGroup) test(evalInterval time.Duration, groupOrderMap map[string]i
return errs
}
- for {
- if !(curr < len(alertEvalTimes) && ts.Sub(mint) <= time.Duration(alertEvalTimes[curr]) &&
- time.Duration(alertEvalTimes[curr]) < ts.Add(evalInterval).Sub(mint)) {
- break
- }
-
+ for curr < len(alertEvalTimes) && ts.Sub(mint) <= time.Duration(alertEvalTimes[curr]) &&
+ time.Duration(alertEvalTimes[curr]) < ts.Add(evalInterval).Sub(mint) {
// We need to check alerts for this time.
// If 'ts <= `eval_time=alertEvalTimes[curr]` < ts+evalInterval'
// then we compare alerts with the Eval at `ts`.
@@ -368,7 +412,7 @@ func (tg *testGroup) test(evalInterval time.Duration, groupOrderMap map[string]i
sort.Sort(gotAlerts)
sort.Sort(expAlerts)
- if !cmp.Equal(expAlerts, gotAlerts, cmp.Comparer(labels.Equal)) {
+ if !cmp.Equal(expAlerts, gotAlerts, cmp.Comparer(labels.Equal), compareFloat64) {
var testName string
if tg.TestGroupName != "" {
testName = fmt.Sprintf(" name: %s,\n", tg.TestGroupName)
@@ -442,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
@@ -476,12 +520,38 @@ Outer:
sort.Slice(gotSamples, func(i, j int) bool {
return labels.Compare(gotSamples[i].Labels, gotSamples[j].Labels) <= 0
})
- if !cmp.Equal(expSamples, gotSamples, cmp.Comparer(labels.Equal)) {
+ if !cmp.Equal(expSamples, gotSamples, cmp.Comparer(labels.Equal), compareFloat64) {
errs = append(errs, fmt.Errorf(" expr: %q, time: %s,\n exp: %v\n got: %v", testCase.Expr,
testCase.EvalTime.String(), parsedSamplesString(expSamples), parsedSamplesString(gotSamples)))
}
}
+ if debug {
+ ts := tg.maxEvalTime()
+ // Potentially a test can be specified at a time with fractional seconds,
+ // which PromQL cannot represent, so round up to the next whole second.
+ ts = (ts + time.Second).Truncate(time.Second)
+ expr := fmt.Sprintf(`{__name__=~".+"}[%v]`, ts)
+ q, err := suite.QueryEngine().NewInstantQuery(context.Background(), suite.Queryable(), nil, expr, mint.Add(ts))
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "DEBUG: Failed querying, expr: %q, err: %v\n", expr, err)
+ return errs
+ }
+ res := q.Exec(suite.Context())
+ if res.Err != nil {
+ fmt.Fprintf(os.Stderr, "DEBUG: Failed query exec, expr: %q, err: %v\n", expr, res.Err)
+ return errs
+ }
+ switch v := res.Value.(type) {
+ case promql.Matrix:
+ fmt.Fprintf(os.Stderr, "DEBUG: Dump of all data (input_series and rules) at %v:\n", ts)
+ fmt.Fprintln(os.Stderr, v.String())
+ default:
+ fmt.Fprintf(os.Stderr, "DEBUG: Got unexpected type %T\n", v)
+ return errs
+ }
+ }
+
if len(errs) > 0 {
return errs
}
@@ -490,11 +560,12 @@ Outer:
// seriesLoadingString returns the input series in PromQL notation.
func (tg *testGroup) seriesLoadingString() string {
- result := fmt.Sprintf("load %v\n", shortDuration(tg.Interval))
+ var result strings.Builder
+ fmt.Fprintf(&result, "load %v\n", shortDuration(tg.Interval))
for _, is := range tg.InputSeries {
- result += fmt.Sprintf(" %v %v\n", is.Series, is.Values)
+ fmt.Fprintf(&result, " %v %v\n", is.Series, is.Values)
}
- return result
+ return result.String()
}
func shortDuration(d model.Duration) string {
@@ -593,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 {
@@ -650,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 9bbac28e9fb..ce317e5e414 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,11 +21,13 @@ import (
"github.com/stretchr/testify/require"
+ "github.com/prometheus/prometheus/promql/parser"
"github.com/prometheus/prometheus/promql/promqltest"
"github.com/prometheus/prometheus/util/junitxml"
)
func TestRulesUnitTest(t *testing.T) {
+ t.Parallel()
type args struct {
files []string
}
@@ -128,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{}
@@ -141,14 +153,16 @@ func TestRulesUnitTest(t *testing.T) {
reuseCount[tt.want] += len(tt.args.files)
}
t.Run(tt.name, func(t *testing.T) {
- if got := RulesUnitTest(tt.queryOpts, nil, false, tt.args.files...); got != tt.want {
+ t.Parallel()
+ 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)
}
})
}
t.Run("Junit xml output ", func(t *testing.T) {
+ t.Parallel()
var buf bytes.Buffer
- if got := RulesUnitTestResult(&buf, promqltest.LazyLoaderOpts{}, nil, 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
@@ -185,15 +199,17 @@ func TestRulesUnitTest(t *testing.T) {
}
func TestRulesUnitTestRun(t *testing.T) {
+ t.Parallel()
type args struct {
run []string
files []string
}
tests := []struct {
- name string
- args args
- queryOpts promqltest.LazyLoaderOpts
- want int
+ name string
+ args args
+ queryOpts promqltest.LazyLoaderOpts
+ want int
+ ignoreUnknownFields bool
}{
{
name: "Test all without run arg",
@@ -227,10 +243,42 @@ func TestRulesUnitTestRun(t *testing.T) {
},
want: 1,
},
+ {
+ name: "Test all with extra fields",
+ args: args{
+ files: []string{"./testdata/rules_run_extrafields.yml"},
+ },
+ ignoreUnknownFields: true,
+ want: 0,
+ },
+ {
+ name: "Test precise floating point comparison expected failure",
+ args: args{
+ files: []string{"./testdata/rules_run_no_fuzzy.yml"},
+ },
+ want: 1,
+ },
+ {
+ name: "Test fuzzy floating point comparison correct match",
+ args: args{
+ run: []string{"correct"},
+ files: []string{"./testdata/rules_run_fuzzy.yml"},
+ },
+ want: 0,
+ },
+ {
+ name: "Test fuzzy floating point comparison wrong match",
+ args: args{
+ run: []string{"wrong"},
+ files: []string{"./testdata/rules_run_fuzzy.yml"},
+ },
+ want: 1,
+ },
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- got := RulesUnitTest(tt.queryOpts, tt.args.run, false, tt.args.files...)
+ t.Parallel()
+ 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 00000000000..7006b63344e
--- /dev/null
+++ b/compliance/go.mod
@@ -0,0 +1,26 @@
+module compliance
+
+go 1.25.0
+
+require github.com/prometheus/compliance/remotewrite v0.0.0-20260223092825-818283e1171e
+
+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.6 // 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-20260602051030-3537b20ac86b // indirect
+ github.com/prometheus/client_model v0.6.2 // indirect
+ github.com/prometheus/common v0.69.0 // indirect
+ github.com/prometheus/prometheus v0.307.4-0.20251119130332-1174b0ce4f1f // indirect
+ github.com/rogpeppe/go-internal v1.14.1 // indirect
+ github.com/stretchr/testify v1.11.1 // indirect
+ golang.org/x/text v0.38.0 // indirect
+ google.golang.org/protobuf v1.36.11 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+)
diff --git a/compliance/go.sum b/compliance/go.sum
new file mode 100644
index 00000000000..b0f18f5f791
--- /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.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
+github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
+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-20260602051030-3537b20ac86b h1:633sracZPrB7O7T6r5skFtwqXDOrXlQkE9Wr5DnYVJE=
+github.com/prometheus/client_golang/exp v0.0.0-20260602051030-3537b20ac86b/go.mod h1:7hAEIbflIgnK0HubVroVy6UgJYYKryF6p3mP/dcyay8=
+github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
+github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
+github.com/prometheus/common v0.69.0 h1:OA85nJQS/T/MaYh/Q2CcgDKSGWqNIgrBDvDH85CuiNk=
+github.com/prometheus/common v0.69.0/go.mod h1:ZzL3f6u94qUxh9p+tJTrF+FvBS1XXbbRAZCQkytAL0Y=
+github.com/prometheus/compliance/remotewrite v0.0.0-20260223092825-818283e1171e h1:tT/KBv0aSFq4AElo/bSVvUd+yNKj72hkRsyiKU45nIQ=
+github.com/prometheus/compliance/remotewrite v0.0.0-20260223092825-818283e1171e/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.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
+github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
+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.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
+go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
+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.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
+golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
+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.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=
diff --git a/compliance/remote_write_sender_test.go b/compliance/remote_write_sender_test.go
new file mode 100644
index 00000000000..38c3bc3531c
--- /dev/null
+++ b/compliance/remote_write_sender_test.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 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 {
+ agentMode bool
+}
+
+func (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)
+
+ args := []string{
+ "run", ".",
+ "--web.listen-address=0.0.0.0:0",
+ fmt.Sprintf("--config.file=%s", configFile),
+ }
+ if p.agentMode {
+ // Agent mode: st-storage only (agent WAL does not use XOR2 chunks).
+ args = append(args, "--enable-feature=st-storage")
+ args = append(args, fmt.Sprintf("--storage.agent.path=%v", dir), "--agent")
+ } else {
+ // Server mode: st-storage requires XOR2 chunk encoding so that start
+ // timestamps can be stored in float chunks.
+ args = append(args, "--enable-feature=st-storage,xor2-encoding")
+ args = append(args, fmt.Sprintf("--storage.tsdb.path=%v", dir))
+ }
+ return sender.RunCommand(ctx, "../cmd/prometheus", nil, "go", args...)
+}
+
+var _ sender.Sender = internalPrometheus{}
+
+// TestRemoteWriteSender runs remote write sender compliance tests defined in
+// https://github.com/prometheus/compliance/tree/main/remotewrite/sender against
+// both agent and server modes.
+func TestRemoteWriteSender(t *testing.T) {
+ t.Run("mode=server", func(t *testing.T) {
+ t.Parallel()
+ sender.RunTests(t, internalPrometheus{}, sender.ComplianceTests())
+ })
+ t.Run("mode=agent", func(t *testing.T) {
+ t.Parallel()
+ sender.RunTests(t, internalPrometheus{agentMode: true}, sender.ComplianceTests())
+ })
+}
diff --git a/config/config.go b/config/config.go
index 4f80b551bc6..cdf914b891c 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
@@ -16,22 +16,25 @@ package config
import (
"errors"
"fmt"
+ "log/slog"
+ "mime"
"net/url"
"os"
"path/filepath"
+ "slices"
"sort"
"strconv"
"strings"
"time"
"github.com/alecthomas/units"
- "github.com/go-kit/log"
- "github.com/go-kit/log/level"
"github.com/grafana/regexp"
+ remoteapi "github.com/prometheus/client_golang/exp/api/remote"
"github.com/prometheus/common/config"
"github.com/prometheus/common/model"
- "github.com/prometheus/common/sigv4"
- "gopkg.in/yaml.v2"
+ "github.com/prometheus/otlptranslator"
+ "github.com/prometheus/sigv4"
+ "go.yaml.in/yaml/v2"
"github.com/prometheus/prometheus/discovery"
"github.com/prometheus/prometheus/model/labels"
@@ -67,13 +70,8 @@ var (
}
)
-const (
- LegacyValidationConfig = "legacy"
- UTF8ValidationConfig = "utf8"
-)
-
// Load parses the YAML input s into a Config.
-func Load(s string, expandExternalLabels bool, logger log.Logger) (*Config, error) {
+func Load(s string, logger *slog.Logger) (*Config, error) {
cfg := &Config{}
// If the entire config body is empty the UnmarshalYAML method is
// never called. We thus have to set the DefaultConfig at the entry
@@ -85,8 +83,11 @@ func Load(s string, expandExternalLabels bool, logger log.Logger) (*Config, erro
return nil, err
}
- if !expandExternalLabels {
- return cfg, nil
+ // When the config body is empty, UnmarshalYAML is never called, so
+ // TSDBConfig may still be nil.
+ if cfg.StorageConfig.TSDBConfig == nil {
+ retention := DefaultTSDBRetentionConfig
+ cfg.StorageConfig.TSDBConfig = &TSDBConfig{Retention: &retention}
}
b := labels.NewScratchBuilder(0)
@@ -98,26 +99,41 @@ func Load(s string, expandExternalLabels bool, logger log.Logger) (*Config, erro
if v := os.Getenv(s); v != "" {
return v
}
- level.Warn(logger).Log("msg", "Empty environment variable", "name", s)
+ logger.Warn("Empty environment variable", "name", s)
return ""
})
if newV != v.Value {
- level.Debug(logger).Log("msg", "External label replaced", "label", v.Name, "input", v.Value, "output", newV)
+ logger.Debug("External label replaced", "label", v.Name, "input", v.Value, "output", newV)
}
// Note newV can be blank. https://github.com/prometheus/prometheus/issues/11024
b.Add(v.Name, newV)
})
- cfg.GlobalConfig.ExternalLabels = b.Labels()
+ if !b.Labels().IsEmpty() {
+ cfg.GlobalConfig.ExternalLabels = b.Labels()
+ }
+
+ switch cfg.OTLPConfig.TranslationStrategy {
+ case otlptranslator.UnderscoreEscapingWithSuffixes, otlptranslator.UnderscoreEscapingWithoutSuffixes:
+ case "":
+ case otlptranslator.NoTranslation, otlptranslator.NoUTF8EscapingWithSuffixes:
+ if cfg.GlobalConfig.MetricNameValidationScheme == model.LegacyValidation {
+ return nil, fmt.Errorf("OTLP translation strategy %q is not allowed when UTF8 is disabled", cfg.OTLPConfig.TranslationStrategy)
+ }
+ default:
+ return nil, fmt.Errorf("unsupported OTLP translation strategy %q", cfg.OTLPConfig.TranslationStrategy)
+ }
+ cfg.loaded = true
return cfg, nil
}
-// LoadFile parses the given YAML file into a Config.
-func LoadFile(filename string, agentMode, expandExternalLabels bool, logger log.Logger) (*Config, error) {
+// LoadFile parses and validates the given YAML file into a read-only Config.
+// Callers should never write to or shallow copy the returned Config.
+func LoadFile(filename string, agentMode bool, logger *slog.Logger) (*Config, error) {
content, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
- cfg, err := Load(string(content), expandExternalLabels, logger)
+ cfg, err := Load(string(content), logger)
if err != nil {
return nil, fmt.Errorf("parsing YAML file %s: %w", filename, err)
}
@@ -140,11 +156,17 @@ func LoadFile(filename string, agentMode, expandExternalLabels bool, logger log.
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.
DefaultConfig = Config{
GlobalConfig: DefaultGlobalConfig,
+ Runtime: DefaultRuntimeConfig,
+ OTLPConfig: DefaultOTLPConfig,
}
// DefaultGlobalConfig is the default global configuration.
@@ -153,26 +175,39 @@ var (
ScrapeTimeout: model.Duration(10 * time.Second),
EvaluationInterval: model.Duration(1 * time.Minute),
RuleQueryOffset: model.Duration(0 * time.Minute),
- // When native histogram feature flag is enabled, ScrapeProtocols default
- // changes to DefaultNativeHistogramScrapeProtocols.
- ScrapeProtocols: DefaultScrapeProtocols,
+ // This is nil to be able to distinguish between the case when
+ // the normal default should be used and the case when a
+ // new default is needed due to an enabled feature flag.
+ // E.g. set to `DefaultProtoFirstScrapeProtocols` when
+ // the feature flag `created-timestamp-zero-ingestion` is set.
+ ScrapeProtocols: nil,
+ // When the native histogram feature flag is enabled,
+ // ScrapeNativeHistograms default changes to true.
+ ScrapeNativeHistograms: boolPtr(false),
+ ConvertClassicHistogramsToNHCB: false,
+ AlwaysScrapeClassicHistograms: false,
+ ExtraScrapeMetrics: boolPtr(false),
+ MetricNameValidationScheme: model.UTF8Validation,
+ MetricNameEscapingScheme: model.AllowUTF8,
}
DefaultRuntimeConfig = RuntimeConfig{
// Go runtime tuning.
- GoGC: 75,
+ GoGC: getGoGC(),
}
- // DefaultScrapeConfig is the default scrape configuration.
+ // DefaultScrapeConfig is the default scrape configuration. Users of this
+ // default MUST call Validate() on the config after creation, even if it's
+ // used unaltered, to check for parameter correctness and fill out default
+ // values that can't be set inline in this declaration.
DefaultScrapeConfig = ScrapeConfig{
- // ScrapeTimeout, ScrapeInterval and ScrapeProtocols default to the configured globals.
- ScrapeClassicHistograms: false,
- MetricsPath: "/metrics",
- Scheme: "http",
- HonorLabels: false,
- HonorTimestamps: true,
- HTTPClientConfig: config.DefaultHTTPClientConfig,
- EnableCompression: true,
+ // ScrapeTimeout, ScrapeInterval, ScrapeProtocols, AlwaysScrapeClassicHistograms, and ConvertClassicHistogramsToNHCB default to the configured globals.
+ MetricsPath: "/metrics",
+ Scheme: "http",
+ HonorLabels: false,
+ HonorTimestamps: true,
+ HTTPClientConfig: config.DefaultHTTPClientConfig,
+ EnableCompression: true,
}
// DefaultAlertmanagerConfig is the default alertmanager configuration.
@@ -183,13 +218,18 @@ var (
HTTPClientConfig: config.DefaultHTTPClientConfig,
}
+ DefaultRemoteWriteHTTPClientConfig = config.HTTPClientConfig{
+ FollowRedirects: true,
+ EnableHTTP2: false,
+ }
+
// DefaultRemoteWriteConfig is the default remote write configuration.
DefaultRemoteWriteConfig = RemoteWriteConfig{
RemoteTimeout: model.Duration(30 * time.Second),
- ProtobufMessage: RemoteWriteProtoMsgV1,
+ ProtobufMessage: remoteapi.WriteV1MessageType,
QueueConfig: DefaultQueueConfig,
MetadataConfig: DefaultMetadataConfig,
- HTTPClientConfig: config.DefaultHTTPClientConfig,
+ HTTPClientConfig: DefaultRemoteWriteHTTPClientConfig,
}
// DefaultQueueConfig is the default remote queue configuration.
@@ -236,7 +276,16 @@ var (
}
// DefaultOTLPConfig is the default OTLP configuration.
- DefaultOTLPConfig = OTLPConfig{}
+ DefaultOTLPConfig = OTLPConfig{
+ TranslationStrategy: otlptranslator.UnderscoreEscapingWithSuffixes,
+ // For backwards compatibility.
+ LabelNameUnderscoreSanitization: true,
+ // For backwards compatibility.
+ LabelNamePreserveMultipleUnderscores: true,
+ }
+
+ // DefaultTSDBRetentionConfig is the default TSDB retention configuration.
+ DefaultTSDBRetentionConfig TSDBRetentionConfig
)
// Config is the top-level configuration for Prometheus's config files.
@@ -253,9 +302,12 @@ type Config struct {
RemoteWriteConfigs []*RemoteWriteConfig `yaml:"remote_write,omitempty"`
RemoteReadConfigs []*RemoteReadConfig `yaml:"remote_read,omitempty"`
OTLPConfig OTLPConfig `yaml:"otlp,omitempty"`
+
+ loaded bool // Certain methods require configuration to use Load validation.
}
// SetDirectory joins any relative file paths with dir.
+// This method writes to config, and it's not concurrency safe.
func (c *Config) SetDirectory(dir string) {
c.GlobalConfig.SetDirectory(dir)
c.AlertingConfig.SetDirectory(dir)
@@ -285,24 +337,26 @@ func (c Config) String() string {
return string(b)
}
-// GetScrapeConfigs returns the scrape configurations.
+// GetScrapeConfigs returns the read-only, validated scrape configurations including
+// the ones from the scrape_config_files.
+// This method does not write to config, and it's concurrency safe (the pointer receiver is for efficiency).
+// This method also assumes the Config was created by Load or LoadFile function, it returns error
+// if it was not. We can't re-validate or apply globals here due to races,
+// read more https://github.com/prometheus/prometheus/issues/15538.
func (c *Config) GetScrapeConfigs() ([]*ScrapeConfig, error) {
- scfgs := make([]*ScrapeConfig, len(c.ScrapeConfigs))
+ if !c.loaded {
+ // Programmatic error, we warn before more confusing errors would happen due to lack of the globalization.
+ return nil, errors.New("scrape config cannot be fetched, main config was not validated and loaded correctly; should not happen")
+ }
+ scfgs := make([]*ScrapeConfig, len(c.ScrapeConfigs))
jobNames := map[string]string{}
for i, scfg := range c.ScrapeConfigs {
- // We do these checks for library users that would not call validate in
- // Unmarshal.
- if err := scfg.Validate(c.GlobalConfig); err != nil {
- return nil, err
- }
-
- if _, ok := jobNames[scfg.JobName]; ok {
- return nil, fmt.Errorf("found multiple scrape configs with job name %q", scfg.JobName)
- }
jobNames[scfg.JobName] = "main config file"
scfgs[i] = scfg
}
+
+ // Re-read and validate the dynamic scrape config rules.
for _, pat := range c.ScrapeConfigFiles {
fs, err := filepath.Glob(pat)
if err != nil {
@@ -338,7 +392,8 @@ func (c *Config) GetScrapeConfigs() ([]*ScrapeConfig, error) {
}
// UnmarshalYAML implements the yaml.Unmarshaler interface.
-func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
+// NOTE: This method should not be used outside of this package. Use Load or LoadFile instead.
+func (c *Config) UnmarshalYAML(unmarshal func(any) error) error {
*c = DefaultConfig
// We want to set c to the defaults and then overwrite it with the input.
// To make unmarshal fill the plain data struct rather than calling UnmarshalYAML
@@ -358,8 +413,13 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
// We have to restore it here.
if c.Runtime.isZero() {
c.Runtime = DefaultRuntimeConfig
- // Use the GOGC env var value if the runtime section is empty.
- c.Runtime.GoGC = getGoGCEnv()
+ }
+
+ // If no storage.tsdb section is present, TSDBConfig is nil and its
+ // UnmarshalYAML never runs. Inject the default retention here.
+ if c.StorageConfig.TSDBConfig == nil {
+ retention := DefaultTSDBRetentionConfig
+ c.StorageConfig.TSDBConfig = &TSDBConfig{Retention: &retention}
}
for _, rf := range c.RuleFiles {
@@ -374,18 +434,22 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
}
}
- // Do global overrides and validate unique names.
+ // Do global overrides and validation.
jobNames := map[string]struct{}{}
for _, scfg := range c.ScrapeConfigs {
if err := scfg.Validate(c.GlobalConfig); err != nil {
return err
}
-
if _, ok := jobNames[scfg.JobName]; ok {
return fmt.Errorf("found multiple scrape configs with job name %q", scfg.JobName)
}
jobNames[scfg.JobName] = struct{}{}
}
+
+ if err := c.AlertingConfig.Validate(c.GlobalConfig.MetricNameValidationScheme); err != nil {
+ return err
+ }
+
rwNames := map[string]struct{}{}
for _, rwcfg := range c.RemoteWriteConfigs {
if rwcfg == nil {
@@ -395,6 +459,9 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
if _, ok := rwNames[rwcfg.Name]; ok && rwcfg.Name != "" {
return fmt.Errorf("found multiple remote write configs with job name %q", rwcfg.Name)
}
+ if err := rwcfg.Validate(c.GlobalConfig.MetricNameValidationScheme); err != nil {
+ return err
+ }
rwNames[rwcfg.Name] = struct{}{}
}
rrNames := map[string]struct{}{}
@@ -421,7 +488,7 @@ type GlobalConfig struct {
// The protocols to negotiate during a scrape. It tells clients what
// protocol are accepted by Prometheus and with what weight (most wanted is first).
// Supported values (case sensitive): PrometheusProto, OpenMetricsText0.0.1,
- // OpenMetricsText1.0.0, PrometheusText0.0.4.
+ // OpenMetricsText1.0.0, PrometheusText1.0.0, PrometheusText0.0.4
ScrapeProtocols []ScrapeProtocol `yaml:"scrape_protocols,omitempty"`
// How frequently to evaluate rules by default.
EvaluationInterval model.Duration `yaml:"evaluation_interval,omitempty"`
@@ -454,8 +521,23 @@ type GlobalConfig struct {
// Keep no more than this many dropped targets per job.
// 0 means no limit.
KeepDroppedTargets uint `yaml:"keep_dropped_targets,omitempty"`
- // Allow UTF8 Metric and Label Names.
- MetricNameValidationScheme string `yaml:"metric_name_validation_scheme,omitempty"`
+ // Allow UTF8 Metric and Label Names. Can be blank in config files but must
+ // have a value if a GlobalConfig is created programmatically.
+ MetricNameValidationScheme model.ValidationScheme `yaml:"metric_name_validation_scheme,omitempty"`
+ // Metric name escaping mode to request through content negotiation. Can be
+ // 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 scrape native histograms.
+ ScrapeNativeHistograms *bool `yaml:"scrape_native_histograms,omitempty"`
+ // Whether to convert all scraped classic histograms into native histograms with custom buckets.
+ 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.
@@ -476,9 +558,22 @@ func (s ScrapeProtocol) Validate() error {
return nil
}
+// HeaderMediaType returns the MIME mediaType for a particular ScrapeProtocol.
+func (s ScrapeProtocol) HeaderMediaType() string {
+ if _, ok := ScrapeProtocolsHeaders[s]; !ok {
+ return ""
+ }
+ mediaType, _, err := mime.ParseMediaType(ScrapeProtocolsHeaders[s])
+ if err != nil {
+ return ""
+ }
+ return mediaType
+}
+
var (
PrometheusProto ScrapeProtocol = "PrometheusProto"
PrometheusText0_0_4 ScrapeProtocol = "PrometheusText0.0.4"
+ PrometheusText1_0_0 ScrapeProtocol = "PrometheusText1.0.0"
OpenMetricsText0_0_1 ScrapeProtocol = "OpenMetricsText0.0.1"
OpenMetricsText1_0_0 ScrapeProtocol = "OpenMetricsText1.0.0"
UTF8NamesHeader string = model.EscapingKey + "=" + model.AllowUTF8
@@ -486,6 +581,7 @@ var (
ScrapeProtocolsHeaders = map[ScrapeProtocol]string{
PrometheusProto: "application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=delimited",
PrometheusText0_0_4: "text/plain;version=0.0.4",
+ PrometheusText1_0_0: "text/plain;version=1.0.0",
OpenMetricsText0_0_1: "application/openmetrics-text;version=0.0.1",
OpenMetricsText1_0_0: "application/openmetrics-text;version=1.0.0",
}
@@ -495,17 +591,19 @@ var (
DefaultScrapeProtocols = []ScrapeProtocol{
OpenMetricsText1_0_0,
OpenMetricsText0_0_1,
+ PrometheusText1_0_0,
PrometheusText0_0_4,
}
// DefaultProtoFirstScrapeProtocols is like DefaultScrapeProtocols, but it
// favors protobuf Prometheus exposition format.
- // Used by default for certain feature-flags like
- // "native-histograms" and "created-timestamp-zero-ingestion".
+ // Used by default by the "scrape_native_histograms" option and for certain
+ // feature-flags like "created-timestamp-zero-ingestion".
DefaultProtoFirstScrapeProtocols = []ScrapeProtocol{
PrometheusProto,
OpenMetricsText1_0_0,
OpenMetricsText0_0_1,
+ PrometheusText1_0_0,
PrometheusText0_0_4,
}
)
@@ -535,7 +633,7 @@ func (c *GlobalConfig) SetDirectory(dir string) {
}
// UnmarshalYAML implements the yaml.Unmarshaler interface.
-func (c *GlobalConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
+func (c *GlobalConfig) UnmarshalYAML(unmarshal func(any) error) error {
// Create a clean global config as the previous one was already populated
// by the default due to the YAML parser behavior for empty blocks.
gc := &GlobalConfig{}
@@ -544,8 +642,14 @@ func (c *GlobalConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
return err
}
+ switch gc.MetricNameValidationScheme {
+ case model.UTF8Validation, model.LegacyValidation:
+ default:
+ gc.MetricNameValidationScheme = DefaultGlobalConfig.MetricNameValidationScheme
+ }
+
if err := gc.ExternalLabels.Validate(func(l labels.Label) error {
- if !model.LabelName(l.Name).IsValid() {
+ if !gc.MetricNameValidationScheme.IsValidLabelName(l.Name) {
return fmt.Errorf("%q is not a valid label name", l.Name)
}
if !model.LabelValue(l.Value).IsValid() {
@@ -565,21 +669,34 @@ func (c *GlobalConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
return errors.New("global scrape timeout greater than scrape interval")
}
if gc.ScrapeTimeout == 0 {
- if DefaultGlobalConfig.ScrapeTimeout > gc.ScrapeInterval {
- gc.ScrapeTimeout = gc.ScrapeInterval
- } else {
- gc.ScrapeTimeout = DefaultGlobalConfig.ScrapeTimeout
- }
+ gc.ScrapeTimeout = min(DefaultGlobalConfig.ScrapeTimeout, gc.ScrapeInterval)
}
if gc.EvaluationInterval == 0 {
gc.EvaluationInterval = DefaultGlobalConfig.EvaluationInterval
}
-
- if gc.ScrapeProtocols == nil {
- gc.ScrapeProtocols = DefaultGlobalConfig.ScrapeProtocols
+ if gc.ScrapeNativeHistograms == nil {
+ gc.ScrapeNativeHistograms = DefaultGlobalConfig.ScrapeNativeHistograms
+ }
+ if gc.ExtraScrapeMetrics == nil {
+ gc.ExtraScrapeMetrics = DefaultGlobalConfig.ExtraScrapeMetrics
}
- if err := validateAcceptScrapeProtocols(gc.ScrapeProtocols); err != nil {
- return fmt.Errorf("%w for global config", err)
+ if gc.ScrapeProtocols == nil {
+ if DefaultGlobalConfig.ScrapeProtocols != nil {
+ // This is the case where the defaults are set due to a feature flag.
+ // E.g. if the created-timestamp-zero-ingestion feature flag is
+ // used.
+ gc.ScrapeProtocols = DefaultGlobalConfig.ScrapeProtocols
+ }
+ // Otherwise, we leave ScrapeProtocols at nil for now. In the
+ // per-job scrape config, we have to recognize the unset case to
+ // correctly set the default depending on the local value of
+ // ScrapeNativeHistograms.
+ }
+ if gc.ScrapeProtocols != nil {
+ // Only validate if not-nil at this point.
+ if err := validateAcceptScrapeProtocols(gc.ScrapeProtocols); err != nil {
+ return fmt.Errorf("%w for global config", err)
+ }
}
*c = *gc
@@ -595,13 +712,43 @@ func (c *GlobalConfig) isZero() bool {
c.RuleQueryOffset == 0 &&
c.QueryLogFile == "" &&
c.ScrapeFailureLogFile == "" &&
- c.ScrapeProtocols == nil
+ c.ScrapeProtocols == nil &&
+ c.ScrapeNativeHistograms == nil &&
+ !c.ConvertClassicHistogramsToNHCB &&
+ !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
+
// RuntimeConfig configures the values for the process behavior.
type RuntimeConfig struct {
// The Go garbage collection target percentage.
GoGC int `yaml:"gogc,omitempty"`
+
+ // Below are guidelines for adding a new field:
+ //
+ // For config that shouldn't change after startup, you might want to use
+ // flags https://prometheus.io/docs/prometheus/latest/command-line/prometheus/.
+ //
+ // Consider when the new field is first applied: at the very beginning of instance
+ // startup, after the TSDB is loaded etc. See https://github.com/prometheus/prometheus/pull/16491
+ // for an example.
+ //
+ // Provide a test covering various scenarios: empty config file, empty or incomplete runtime
+ // config block, precedence over other inputs (e.g., env vars, if applicable) etc.
+ // See TestRuntimeGOGCConfig (or https://github.com/prometheus/prometheus/pull/15238).
+ // The test should also verify behavior on reloads, since this config should be
+ // adjustable at runtime.
}
// isZero returns true iff the global config is the zero value.
@@ -632,10 +779,19 @@ type ScrapeConfig struct {
// The protocols to negotiate during a scrape. It tells clients what
// protocol are accepted by Prometheus and with what preference (most wanted is first).
// Supported values (case sensitive): PrometheusProto, OpenMetricsText0.0.1,
- // OpenMetricsText1.0.0, PrometheusText0.0.4.
+ // OpenMetricsText1.0.0, PrometheusText1.0.0, PrometheusText0.0.4.
ScrapeProtocols []ScrapeProtocol `yaml:"scrape_protocols,omitempty"`
- // Whether to scrape a classic histogram that is also exposed as a native histogram.
- ScrapeClassicHistograms bool `yaml:"scrape_classic_histograms,omitempty"`
+ // The fallback protocol to use if the Content-Type provided by the target
+ // is not provided, blank, or not one of the expected values.
+ // Supported values (case sensitive): PrometheusProto, OpenMetricsText0.0.1,
+ // OpenMetricsText1.0.0, PrometheusText1.0.0, PrometheusText0.0.4.
+ ScrapeFallbackProtocol ScrapeProtocol `yaml:"fallback_scrape_protocol,omitempty"`
+ // Whether to scrape native histograms.
+ ScrapeNativeHistograms *bool `yaml:"scrape_native_histograms,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 convert all scraped classic histograms into a native histogram with custom buckets.
+ ConvertClassicHistogramsToNHCB *bool `yaml:"convert_classic_histograms_to_nhcb,omitempty"`
// File to which scrape failures are logged.
ScrapeFailureLogFile string `yaml:"scrape_failure_log_file,omitempty"`
// The HTTP resource path on which to fetch metrics from targets.
@@ -671,8 +827,18 @@ type ScrapeConfig struct {
// Keep no more than this many dropped targets per job.
// 0 means no limit.
KeepDroppedTargets uint `yaml:"keep_dropped_targets,omitempty"`
- // Allow UTF8 Metric and Label Names.
- MetricNameValidationScheme string `yaml:"metric_name_validation_scheme,omitempty"`
+ // Allow UTF8 Metric and Label Names. Can be blank in config files but must
+ // have a value if a ScrapeConfig is created programmatically.
+ MetricNameValidationScheme model.ValidationScheme `yaml:"metric_name_validation_scheme,omitempty"`
+ // Metric name escaping mode to request through content negotiation. Can be
+ // 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.
@@ -694,12 +860,12 @@ func (c *ScrapeConfig) SetDirectory(dir string) {
}
// UnmarshalYAML implements the yaml.Unmarshaler interface.
-func (c *ScrapeConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
+func (c *ScrapeConfig) UnmarshalYAML(unmarshal func(any) error) error {
*c = DefaultScrapeConfig
if err := discovery.UnmarshalYAMLWithInlineConfigs(c, unmarshal); err != nil {
return err
}
- if len(c.JobName) == 0 {
+ if c.JobName == "" {
return errors.New("job_name is empty")
}
@@ -745,11 +911,7 @@ func (c *ScrapeConfig) Validate(globalConfig GlobalConfig) error {
return fmt.Errorf("scrape timeout greater than scrape interval for scrape config with job name %q", c.JobName)
}
if c.ScrapeTimeout == 0 {
- if globalConfig.ScrapeTimeout > c.ScrapeInterval {
- c.ScrapeTimeout = c.ScrapeInterval
- } else {
- c.ScrapeTimeout = globalConfig.ScrapeTimeout
- }
+ c.ScrapeTimeout = min(globalConfig.ScrapeTimeout, c.ScrapeInterval)
}
if c.BodySizeLimit == 0 {
c.BodySizeLimit = globalConfig.BodySizeLimit
@@ -775,41 +937,214 @@ func (c *ScrapeConfig) Validate(globalConfig GlobalConfig) error {
if c.ScrapeFailureLogFile == "" {
c.ScrapeFailureLogFile = globalConfig.ScrapeFailureLogFile
}
+ if c.ScrapeNativeHistograms == nil {
+ c.ScrapeNativeHistograms = globalConfig.ScrapeNativeHistograms
+ }
+ if c.ExtraScrapeMetrics == nil {
+ c.ExtraScrapeMetrics = globalConfig.ExtraScrapeMetrics
+ }
if c.ScrapeProtocols == nil {
- c.ScrapeProtocols = globalConfig.ScrapeProtocols
+ switch {
+ case globalConfig.ScrapeProtocols != nil:
+ // global ScrapeProtocols either set explicitly or via a
+ // default triggered by a feature flag. This overrides
+ // the selection based on locally active scraping of
+ // native histograms.
+ c.ScrapeProtocols = globalConfig.ScrapeProtocols
+ case c.ScrapeNativeHistogramsEnabled():
+ c.ScrapeProtocols = DefaultProtoFirstScrapeProtocols
+ default:
+ c.ScrapeProtocols = DefaultScrapeProtocols
+ }
}
if err := validateAcceptScrapeProtocols(c.ScrapeProtocols); err != nil {
return fmt.Errorf("%w for scrape config with job name %q", err, c.JobName)
}
- switch globalConfig.MetricNameValidationScheme {
- case "", LegacyValidationConfig:
- case UTF8ValidationConfig:
- if model.NameValidationScheme != model.UTF8Validation {
- return fmt.Errorf("utf8 name validation requested but feature not enabled via --enable-feature=utf8-names")
+ if c.ScrapeFallbackProtocol != "" {
+ if err := c.ScrapeFallbackProtocol.Validate(); err != nil {
+ return fmt.Errorf("invalid fallback_scrape_protocol for scrape config with job name %q: %w", c.JobName, err)
}
+ }
+
+ //nolint:staticcheck
+ if model.NameValidationScheme != model.UTF8Validation {
+ return errors.New("model.NameValidationScheme must be set to UTF8")
+ }
+
+ switch globalConfig.MetricNameValidationScheme {
+ case model.LegacyValidation, model.UTF8Validation:
default:
- return fmt.Errorf("unknown name validation method specified, must be either 'legacy' or 'utf8', got %s", globalConfig.MetricNameValidationScheme)
+ return errors.New("global name validation method must be set")
}
- if c.MetricNameValidationScheme == "" {
+ // Scrapeconfig validation scheme matches global if left blank.
+ localValidationUnset := false
+ switch c.MetricNameValidationScheme {
+ case model.UnsetValidation:
+ localValidationUnset = true
c.MetricNameValidationScheme = globalConfig.MetricNameValidationScheme
+ case model.LegacyValidation, model.UTF8Validation:
+ default:
+ return fmt.Errorf("unknown scrape config name validation method specified, must be either '', 'legacy' or 'utf8', got %s", c.MetricNameValidationScheme)
+ }
+
+ // Escaping scheme is based on the validation scheme if left blank.
+ switch globalConfig.MetricNameEscapingScheme {
+ case "":
+ if globalConfig.MetricNameValidationScheme == model.LegacyValidation {
+ globalConfig.MetricNameEscapingScheme = model.EscapeUnderscores
+ } else {
+ globalConfig.MetricNameEscapingScheme = model.AllowUTF8
+ }
+ case model.AllowUTF8, model.EscapeUnderscores, model.EscapeDots, model.EscapeValues:
+ default:
+ return fmt.Errorf("unknown global name escaping method specified, must be one of '%s', '%s', '%s', or '%s', got %q", model.AllowUTF8, model.EscapeUnderscores, model.EscapeDots, model.EscapeValues, globalConfig.MetricNameEscapingScheme)
+ }
+
+ // Similarly, if ScrapeConfig escaping scheme is blank, infer it from the
+ // ScrapeConfig validation scheme if that was set, or the Global validation
+ // scheme if the ScrapeConfig validation scheme was also not set. This ensures
+ // that local ScrapeConfigs that only specify Legacy validation do not inherit
+ // the global AllowUTF8 escaping setting, which is an error.
+ if c.MetricNameEscapingScheme == "" {
+ //nolint:gocritic
+ if localValidationUnset {
+ c.MetricNameEscapingScheme = globalConfig.MetricNameEscapingScheme
+ } else if c.MetricNameValidationScheme == model.LegacyValidation {
+ c.MetricNameEscapingScheme = model.EscapeUnderscores
+ } else {
+ c.MetricNameEscapingScheme = model.AllowUTF8
+ }
+ }
+
+ switch c.MetricNameEscapingScheme {
+ case model.AllowUTF8:
+ if c.MetricNameValidationScheme != model.UTF8Validation {
+ return errors.New("utf8 metric names requested but validation scheme is not set to UTF8")
+ }
+ case model.EscapeUnderscores, model.EscapeDots, model.EscapeValues:
+ default:
+ return fmt.Errorf("unknown scrape config name escaping method specified, must be one of '%s', '%s', '%s', or '%s', got %q", model.AllowUTF8, model.EscapeUnderscores, model.EscapeDots, model.EscapeValues, c.MetricNameEscapingScheme)
+ }
+
+ if c.ConvertClassicHistogramsToNHCB == nil {
+ global := globalConfig.ConvertClassicHistogramsToNHCB
+ c.ConvertClassicHistogramsToNHCB = &global
+ }
+
+ if c.AlwaysScrapeClassicHistograms == nil {
+ global := globalConfig.AlwaysScrapeClassicHistograms
+ c.AlwaysScrapeClassicHistograms = &global
+ }
+
+ for _, rc := range c.RelabelConfigs {
+ if err := rc.Validate(c.MetricNameValidationScheme); err != nil {
+ return err
+ }
+ }
+ for _, rc := range c.MetricRelabelConfigs {
+ if err := rc.Validate(c.MetricNameValidationScheme); err != nil {
+ return err
+ }
}
return nil
}
// MarshalYAML implements the yaml.Marshaler interface.
-func (c *ScrapeConfig) MarshalYAML() (interface{}, error) {
+func (c *ScrapeConfig) MarshalYAML() (any, error) {
return discovery.MarshalYAMLWithInlineConfigs(c)
}
+// ToEscapingScheme wraps the equivalent common library function with the
+// desired default behavior based on the given validation scheme. This is a
+// workaround for third party exporters that don't set the escaping scheme.
+func ToEscapingScheme(s string, v model.ValidationScheme) (model.EscapingScheme, error) {
+ if s == "" {
+ switch v {
+ case model.UTF8Validation:
+ return model.NoEscaping, nil
+ case model.LegacyValidation:
+ return model.UnderscoreEscaping, nil
+ case model.UnsetValidation:
+ return model.NoEscaping, fmt.Errorf("ValidationScheme is unset: %s", v)
+ default:
+ panic(fmt.Errorf("unhandled validation scheme: %s", v))
+ }
+ }
+ return model.ToEscapingScheme(s)
+}
+
+// ScrapeNativeHistogramsEnabled returns whether to scrape native histograms.
+func (c *ScrapeConfig) ScrapeNativeHistogramsEnabled() bool {
+ return c.ScrapeNativeHistograms != nil && *c.ScrapeNativeHistograms
+}
+
+// ConvertClassicHistogramsToNHCBEnabled returns whether to convert classic histograms to NHCB.
+func (c *ScrapeConfig) ConvertClassicHistogramsToNHCBEnabled() bool {
+ return c.ConvertClassicHistogramsToNHCB != nil && *c.ConvertClassicHistogramsToNHCB
+}
+
+// AlwaysScrapeClassicHistogramsEnabled returns whether to always scrape classic histograms.
+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"`
ExemplarsConfig *ExemplarsConfig `yaml:"exemplars,omitempty"`
}
+// TSDBRetentionConfig holds the configuration retention of data in storage storage.
+type TSDBRetentionConfig struct {
+ // How long to retain samples in storage.
+ Time model.Duration `yaml:"time,omitempty"`
+
+ // Maximum number of bytes that can be stored for blocks.
+ Size units.Base2Bytes `yaml:"size,omitempty"`
+
+ // Maximum percentage of disk used for TSDB storage.
+ Percentage float64 `yaml:"percentage,omitempty"`
+}
+
+// UnmarshalYAML implements the yaml.Unmarshaler interface.
+func (t *TSDBRetentionConfig) UnmarshalYAML(unmarshal func(any) error) error {
+ *t = TSDBRetentionConfig{}
+ type plain TSDBRetentionConfig
+ if err := unmarshal((*plain)(t)); err != nil {
+ return err
+ }
+ if t.Size < 0 {
+ return fmt.Errorf("'storage.tsdb.retention.size' must be greater than or equal to 0, got %v", t.Size)
+ }
+ if t.Percentage < 0 || t.Percentage > 100 {
+ return fmt.Errorf("'storage.tsdb.retention.percentage' must be in the range [0, 100], got %v", t.Percentage)
+ }
+ return nil
+}
+
+const (
+ // FloatChunkEncodingXOR selects standard XOR encoding for float chunks.
+ FloatChunkEncodingXOR = "xor"
+ // FloatChunkEncodingXOR2 selects XOR2 encoding for float chunks; requires --enable-feature=xor2-encoding.
+ FloatChunkEncodingXOR2 = "xor2"
+)
+
+// ChunkEncodingConfig configures per-chunk-type encoding overrides.
+type ChunkEncodingConfig struct {
+ // Floats selects the encoding used for float chunks.
+ // Valid values are "xor", "xor2", and "" (empty/absent). When empty, the encoding
+ // follows the --enable-feature=xor2-encoding flag. Setting "xor2" requires the flag to be enabled.
+ Floats string `yaml:"floats,omitempty"`
+}
+
// TSDBConfig configures runtime reloadable configuration options.
type TSDBConfig struct {
// OutOfOrderTimeWindow sets how long back in time an out-of-order sample can be inserted
@@ -822,10 +1157,19 @@ type TSDBConfig struct {
// During unmarshall, this is converted into milliseconds and stored in OutOfOrderTimeWindow.
// 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"`
+
+ // ChunkEncoding configures per-chunk-type encoding overrides.
+ ChunkEncoding ChunkEncodingConfig `yaml:"chunk_encoding,omitempty"`
+
+ Retention *TSDBRetentionConfig `yaml:"retention,omitempty"`
}
// UnmarshalYAML implements the yaml.Unmarshaler interface.
-func (t *TSDBConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
+func (t *TSDBConfig) UnmarshalYAML(unmarshal func(any) error) error {
*t = TSDBConfig{}
type plain TSDBConfig
if err := unmarshal((*plain)(t)); err != nil {
@@ -834,6 +1178,18 @@ func (t *TSDBConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
t.OutOfOrderTimeWindow = time.Duration(t.OutOfOrderTimeWindowFlag).Milliseconds()
+ switch t.ChunkEncoding.Floats {
+ case "", FloatChunkEncodingXOR, FloatChunkEncodingXOR2:
+ // Valid; no action required.
+ default:
+ return fmt.Errorf("'storage.tsdb.chunk_encoding.floats' must be 'xor' or 'xor2', or the field must be omitted entirely, got %q", t.ChunkEncoding.Floats)
+ }
+
+ if t.Retention == nil {
+ retention := DefaultTSDBRetentionConfig
+ t.Retention = &retention
+ }
+
return nil
}
@@ -847,7 +1203,7 @@ const (
)
// UnmarshalYAML implements the yaml.Unmarshaler interface.
-func (t *TracingClientType) UnmarshalYAML(unmarshal func(interface{}) error) error {
+func (t *TracingClientType) UnmarshalYAML(unmarshal func(any) error) error {
*t = TracingClientType("")
type plain TracingClientType
if err := unmarshal((*plain)(t)); err != nil {
@@ -881,7 +1237,7 @@ func (t *TracingConfig) SetDirectory(dir string) {
}
// UnmarshalYAML implements the yaml.Unmarshaler interface.
-func (t *TracingConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
+func (t *TracingConfig) UnmarshalYAML(unmarshal func(any) error) error {
*t = TracingConfig{
ClientType: TracingClientGRPC,
}
@@ -919,6 +1275,20 @@ type AlertingConfig struct {
AlertmanagerConfigs AlertmanagerConfigs `yaml:"alertmanagers,omitempty"`
}
+func (c *AlertingConfig) Validate(nameValidationScheme model.ValidationScheme) error {
+ for _, rc := range c.AlertRelabelConfigs {
+ if err := rc.Validate(nameValidationScheme); err != nil {
+ return err
+ }
+ }
+ for _, rc := range c.AlertmanagerConfigs {
+ if err := rc.Validate(nameValidationScheme); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
// SetDirectory joins any relative file paths with dir.
func (c *AlertingConfig) SetDirectory(dir string) {
for _, c := range c.AlertmanagerConfigs {
@@ -927,7 +1297,7 @@ func (c *AlertingConfig) SetDirectory(dir string) {
}
// UnmarshalYAML implements the yaml.Unmarshaler interface.
-func (c *AlertingConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
+func (c *AlertingConfig) UnmarshalYAML(unmarshal func(any) error) error {
// Create a clean global config as the previous one was already populated
// by the default due to the YAML parser behavior for empty blocks.
*c = AlertingConfig{}
@@ -958,23 +1328,22 @@ func (a AlertmanagerConfigs) ToMap() map[string]*AlertmanagerConfig {
// AlertmanagerAPIVersion represents a version of the
// github.com/prometheus/alertmanager/api, e.g. 'v1' or 'v2'.
+// 'v1' is no longer supported.
type AlertmanagerAPIVersion string
// UnmarshalYAML implements the yaml.Unmarshaler interface.
-func (v *AlertmanagerAPIVersion) UnmarshalYAML(unmarshal func(interface{}) error) error {
+func (v *AlertmanagerAPIVersion) UnmarshalYAML(unmarshal func(any) error) error {
*v = AlertmanagerAPIVersion("")
type plain AlertmanagerAPIVersion
if err := unmarshal((*plain)(v)); err != nil {
return err
}
- for _, supportedVersion := range SupportedAlertmanagerAPIVersions {
- if *v == supportedVersion {
- return nil
- }
+ if !slices.Contains(SupportedAlertmanagerAPIVersions, *v) {
+ return fmt.Errorf("expected Alertmanager api version to be one of %v but got %v", SupportedAlertmanagerAPIVersions, *v)
}
- return fmt.Errorf("expected Alertmanager api version to be one of %v but got %v", SupportedAlertmanagerAPIVersions, *v)
+ return nil
}
const (
@@ -987,7 +1356,7 @@ const (
)
var SupportedAlertmanagerAPIVersions = []AlertmanagerAPIVersion{
- AlertmanagerAPIVersionV1, AlertmanagerAPIVersionV2,
+ AlertmanagerAPIVersionV2,
}
// AlertmanagerConfig configures how Alertmanagers can be discovered and communicated with.
@@ -1022,7 +1391,7 @@ func (c *AlertmanagerConfig) SetDirectory(dir string) {
}
// UnmarshalYAML implements the yaml.Unmarshaler interface.
-func (c *AlertmanagerConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
+func (c *AlertmanagerConfig) UnmarshalYAML(unmarshal func(any) error) error {
*c = DefaultAlertmanagerConfig
if err := discovery.UnmarshalYAMLWithInlineConfigs(c, unmarshal); err != nil {
return err
@@ -1039,7 +1408,7 @@ func (c *AlertmanagerConfig) UnmarshalYAML(unmarshal func(interface{}) error) er
c.HTTPClientConfig.Authorization != nil || c.HTTPClientConfig.OAuth2 != nil
if httpClientConfigAuthEnabled && c.SigV4Config != nil {
- return fmt.Errorf("at most one of basic_auth, authorization, oauth2, & sigv4 must be configured")
+ return errors.New("at most one of basic_auth, authorization, oauth2, & sigv4 must be configured")
}
// Check for users putting URLs in target groups.
@@ -1064,8 +1433,22 @@ func (c *AlertmanagerConfig) UnmarshalYAML(unmarshal func(interface{}) error) er
return nil
}
+func (c *AlertmanagerConfig) Validate(nameValidationScheme model.ValidationScheme) error {
+ for _, rc := range c.AlertRelabelConfigs {
+ if err := rc.Validate(nameValidationScheme); err != nil {
+ return err
+ }
+ }
+ for _, rc := range c.RelabelConfigs {
+ if err := rc.Validate(nameValidationScheme); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
// MarshalYAML implements the yaml.Marshaler interface.
-func (c *AlertmanagerConfig) MarshalYAML() (interface{}, error) {
+func (c *AlertmanagerConfig) MarshalYAML() (any, error) {
return discovery.MarshalYAMLWithInlineConfigs(c)
}
@@ -1095,50 +1478,6 @@ func CheckTargetAddress(address model.LabelValue) error {
return nil
}
-// RemoteWriteProtoMsg represents the known protobuf message for the remote write
-// 1.0 and 2.0 specs.
-type RemoteWriteProtoMsg string
-
-// Validate returns error if the given reference for the protobuf message is not supported.
-func (s RemoteWriteProtoMsg) Validate() error {
- switch s {
- case RemoteWriteProtoMsgV1, RemoteWriteProtoMsgV2:
- return nil
- default:
- return fmt.Errorf("unknown remote write protobuf message %v, supported: %v", s, RemoteWriteProtoMsgs{RemoteWriteProtoMsgV1, RemoteWriteProtoMsgV2}.String())
- }
-}
-
-type RemoteWriteProtoMsgs []RemoteWriteProtoMsg
-
-func (m RemoteWriteProtoMsgs) Strings() []string {
- ret := make([]string, 0, len(m))
- for _, typ := range m {
- ret = append(ret, string(typ))
- }
- return ret
-}
-
-func (m RemoteWriteProtoMsgs) String() string {
- return strings.Join(m.Strings(), ", ")
-}
-
-var (
- // RemoteWriteProtoMsgV1 represents the `prometheus.WriteRequest` protobuf
- // message introduced in the https://prometheus.io/docs/specs/remote_write_spec/,
- // which will eventually be deprecated.
- //
- // NOTE: This string is used for both HTTP header values and config value, so don't change
- // this reference.
- RemoteWriteProtoMsgV1 RemoteWriteProtoMsg = "prometheus.WriteRequest"
- // RemoteWriteProtoMsgV2 represents the `io.prometheus.write.v2.Request` protobuf
- // message introduced in https://prometheus.io/docs/specs/remote_write_spec_2_0/
- //
- // NOTE: This string is used for both HTTP header values and config value, so don't change
- // this reference.
- RemoteWriteProtoMsgV2 RemoteWriteProtoMsg = "io.prometheus.write.v2.Request"
-)
-
// RemoteWriteConfig is the configuration for writing to remote storage.
type RemoteWriteConfig struct {
URL *config.URL `yaml:"url"`
@@ -1148,9 +1487,10 @@ type RemoteWriteConfig struct {
Name string `yaml:"name,omitempty"`
SendExemplars bool `yaml:"send_exemplars,omitempty"`
SendNativeHistograms bool `yaml:"send_native_histograms,omitempty"`
+ RoundRobinDNS bool `yaml:"round_robin_dns,omitempty"`
// ProtobufMessage specifies the protobuf message to use against the remote
// receiver as specified in https://prometheus.io/docs/specs/remote_write_spec_2_0/
- ProtobufMessage RemoteWriteProtoMsg `yaml:"protobuf_message,omitempty"`
+ ProtobufMessage remoteapi.WriteMessageType `yaml:"protobuf_message,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.
@@ -1168,7 +1508,7 @@ func (c *RemoteWriteConfig) SetDirectory(dir string) {
}
// UnmarshalYAML implements the yaml.Unmarshaler interface.
-func (c *RemoteWriteConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
+func (c *RemoteWriteConfig) UnmarshalYAML(unmarshal func(any) error) error {
*c = DefaultRemoteWriteConfig
type plain RemoteWriteConfig
if err := unmarshal((*plain)(c)); err != nil {
@@ -1197,9 +1537,23 @@ func (c *RemoteWriteConfig) UnmarshalYAML(unmarshal func(interface{}) error) err
return err
}
+ if err := c.QueueConfig.Validate(); err != nil {
+ return err
+ }
+
return validateAuthConfigs(c)
}
+func (c *RemoteWriteConfig) Validate(nameValidationScheme model.ValidationScheme) error {
+ for _, rc := range c.WriteRelabelConfigs {
+ if err := rc.Validate(nameValidationScheme); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
// validateAuthConfigs validates that at most one of basic_auth, authorization, oauth2, sigv4, azuread or google_iam must be configured.
func validateAuthConfigs(c *RemoteWriteConfig) error {
var authConfigured []string
@@ -1279,6 +1633,29 @@ type QueueConfig struct {
SampleAgeLimit model.Duration `yaml:"sample_age_limit,omitempty"`
}
+// Validate checks QueueConfig fields for invalid values.
+func (c *QueueConfig) Validate() error {
+ if c.MaxShards <= 0 {
+ return errors.New("remote write queue max_shards must be positive")
+ }
+ if c.MinShards <= 0 {
+ return errors.New("remote write queue min_shards must be positive")
+ }
+ if c.MinShards > c.MaxShards {
+ return errors.New("remote write queue min_shards must not be greater than max_shards")
+ }
+ if c.MaxSamplesPerSend <= 0 {
+ return errors.New("remote write queue max_samples_per_send must be positive")
+ }
+ if c.Capacity <= 0 {
+ return errors.New("remote write queue capacity must be positive")
+ }
+ if c.MaxBackoff < c.MinBackoff {
+ return errors.New("remote write queue max_backoff must not be less than min_backoff")
+ }
+ return nil
+}
+
// MetadataConfig is the configuration for sending metadata to remote
// storage.
type MetadataConfig struct {
@@ -1323,7 +1700,7 @@ func (c *RemoteReadConfig) SetDirectory(dir string) {
}
// UnmarshalYAML implements the yaml.Unmarshaler interface.
-func (c *RemoteReadConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
+func (c *RemoteReadConfig) UnmarshalYAML(unmarshal func(any) error) error {
*c = DefaultRemoteReadConfig
type plain RemoteReadConfig
if err := unmarshal((*plain)(c)); err != nil {
@@ -1353,7 +1730,7 @@ func fileErr(filename string, err error) error {
return fmt.Errorf("%q: %w", filePath(filename), err)
}
-func getGoGCEnv() int {
+func getGoGC() int {
goGCEnv := os.Getenv("GOGC")
// If the GOGC env var is set, use the same logic as upstream Go.
if goGCEnv != "" {
@@ -1366,37 +1743,73 @@ func getGoGCEnv() int {
return i
}
}
- return DefaultRuntimeConfig.GoGC
+ return DefaultGoGCPercentage
}
// OTLPConfig is the configuration for writing to the OTLP endpoint.
type OTLPConfig struct {
- PromoteResourceAttributes []string `yaml:"promote_resource_attributes,omitempty"`
+ PromoteAllResourceAttributes bool `yaml:"promote_all_resource_attributes,omitempty"`
+ PromoteResourceAttributes []string `yaml:"promote_resource_attributes,omitempty"`
+ IgnoreResourceAttributes []string `yaml:"ignore_resource_attributes,omitempty"`
+ TranslationStrategy otlptranslator.TranslationStrategyOption `yaml:"translation_strategy,omitempty"`
+ KeepIdentifyingResourceAttributes bool `yaml:"keep_identifying_resource_attributes,omitempty"`
+ ConvertHistogramsToNHCB bool `yaml:"convert_histograms_to_nhcb,omitempty"`
+ // PromoteScopeMetadata controls whether to promote OTel scope metadata (i.e. name, version, schema URL, and attributes) to metric labels.
+ // As per OTel spec, the aforementioned scope metadata should be identifying, i.e. made into metric labels.
+ PromoteScopeMetadata bool `yaml:"promote_scope_metadata,omitempty"`
+ // LabelNameUnderscoreSanitization controls whether to enable prepending of 'key_' to labels
+ // starting with '_'. Reserved labels starting with `__` are not modified.
+ // This is only relevant when AllowUTF8 is false (i.e., when using underscore escaping).
+ LabelNameUnderscoreSanitization bool `yaml:"label_name_underscore_sanitization,omitempty"`
+ // LabelNamePreserveMultipleUnderscores enables preserving of multiple consecutive underscores
+ // in label names when AllowUTF8 is false. When false, multiple consecutive underscores are
+ // collapsed to a single underscore during label name sanitization.
+ LabelNamePreserveMultipleUnderscores bool `yaml:"label_name_preserve_multiple_underscores,omitempty"`
}
// UnmarshalYAML implements the yaml.Unmarshaler interface.
-func (c *OTLPConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
+func (c *OTLPConfig) UnmarshalYAML(unmarshal func(any) error) error {
*c = DefaultOTLPConfig
type plain OTLPConfig
if err := unmarshal((*plain)(c)); err != nil {
return err
}
+ if c.PromoteAllResourceAttributes {
+ if len(c.PromoteResourceAttributes) > 0 {
+ return errors.New("'promote_all_resource_attributes' and 'promote_resource_attributes' cannot be configured simultaneously")
+ }
+ if err := sanitizeAttributes(c.IgnoreResourceAttributes, "ignored"); err != nil {
+ return fmt.Errorf("invalid 'ignore_resource_attributes': %w", err)
+ }
+ } else {
+ if len(c.IgnoreResourceAttributes) > 0 {
+ return errors.New("'ignore_resource_attributes' cannot be configured unless 'promote_all_resource_attributes' is true")
+ }
+ if err := sanitizeAttributes(c.PromoteResourceAttributes, "promoted"); err != nil {
+ return fmt.Errorf("invalid 'promote_resource_attributes': %w", err)
+ }
+ }
+
+ return nil
+}
+
+func sanitizeAttributes(attributes []string, adjective string) error {
seen := map[string]struct{}{}
var err error
- for i, attr := range c.PromoteResourceAttributes {
+ for i, attr := range attributes {
attr = strings.TrimSpace(attr)
if attr == "" {
- err = errors.Join(err, fmt.Errorf("empty promoted OTel resource attribute"))
+ err = errors.Join(err, fmt.Errorf("empty %s OTel resource attribute", adjective))
continue
}
if _, exists := seen[attr]; exists {
- err = errors.Join(err, fmt.Errorf("duplicated promoted OTel resource attribute %q", attr))
+ err = errors.Join(err, fmt.Errorf("duplicated %s OTel resource attribute %q", adjective, attr))
continue
}
seen[attr] = struct{}{}
- c.PromoteResourceAttributes[i] = attr
+ attributes[i] = attr
}
return err
}
diff --git a/config/config_default_test.go b/config/config_default_test.go
index 31133f1e04b..ec7a1128241 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
@@ -18,8 +18,12 @@ package config
const ruleFilesConfigFile = "testdata/rules_abs_path.good.yml"
var ruleFilesExpectedConf = &Config{
- GlobalConfig: DefaultGlobalConfig,
- Runtime: DefaultRuntimeConfig,
+ loaded: true,
+
+ GlobalConfig: DefaultGlobalConfig,
+ Runtime: DefaultRuntimeConfig,
+ OTLPConfig: DefaultOTLPConfig,
+ StorageConfig: StorageConfig{TSDBConfig: &TSDBConfig{Retention: &TSDBRetentionConfig{}}},
RuleFiles: []string{
"testdata/first.rules",
"testdata/rules/second.rules",
diff --git a/config/config_test.go b/config/config_test.go
index ea1d3b11c99..79b8334d537 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
@@ -15,7 +15,6 @@ package config
import (
"crypto/tls"
- "encoding/json"
"fmt"
"net/url"
"os"
@@ -24,12 +23,16 @@ import (
"time"
"github.com/alecthomas/units"
- "github.com/go-kit/log"
+ "github.com/google/go-cmp/cmp"
+ "github.com/google/go-cmp/cmp/cmpopts"
"github.com/grafana/regexp"
+ remoteapi "github.com/prometheus/client_golang/exp/api/remote"
"github.com/prometheus/common/config"
"github.com/prometheus/common/model"
+ "github.com/prometheus/common/promslog"
+ "github.com/prometheus/otlptranslator"
"github.com/stretchr/testify/require"
- "gopkg.in/yaml.v2"
+ "go.yaml.in/yaml/v2"
"github.com/prometheus/prometheus/discovery"
"github.com/prometheus/prometheus/discovery/aws"
@@ -48,9 +51,11 @@ import (
"github.com/prometheus/prometheus/discovery/moby"
"github.com/prometheus/prometheus/discovery/nomad"
"github.com/prometheus/prometheus/discovery/openstack"
+ "github.com/prometheus/prometheus/discovery/outscale"
"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/targetgroup"
"github.com/prometheus/prometheus/discovery/triton"
"github.com/prometheus/prometheus/discovery/uyuni"
@@ -82,6 +87,7 @@ const (
)
var expectedConf = &Config{
+ loaded: true,
GlobalConfig: GlobalConfig{
ScrapeInterval: model.Duration(15 * time.Second),
ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
@@ -91,13 +97,17 @@ var expectedConf = &Config{
ExternalLabels: labels.FromStrings("foo", "bar", "monitor", "codelab"),
- BodySizeLimit: globBodySizeLimit,
- SampleLimit: globSampleLimit,
- TargetLimit: globTargetLimit,
- LabelLimit: globLabelLimit,
- LabelNameLengthLimit: globLabelNameLengthLimit,
- LabelValueLengthLimit: globLabelValueLengthLimit,
- ScrapeProtocols: DefaultGlobalConfig.ScrapeProtocols,
+ BodySizeLimit: globBodySizeLimit,
+ SampleLimit: globSampleLimit,
+ TargetLimit: globTargetLimit,
+ LabelLimit: globLabelLimit,
+ LabelNameLengthLimit: globLabelNameLengthLimit,
+ LabelValueLengthLimit: globLabelValueLengthLimit,
+ ScrapeNativeHistograms: boolPtr(false),
+ AlwaysScrapeClassicHistograms: false,
+ ConvertClassicHistogramsToNHCB: false,
+ ExtraScrapeMetrics: boolPtr(false),
+ MetricNameValidationScheme: model.UTF8Validation,
},
Runtime: RuntimeConfig{
@@ -112,16 +122,17 @@ var expectedConf = &Config{
RemoteWriteConfigs: []*RemoteWriteConfig{
{
URL: mustParseURL("http://remote1/push"),
- ProtobufMessage: RemoteWriteProtoMsgV1,
+ ProtobufMessage: remoteapi.WriteV1MessageType,
RemoteTimeout: model.Duration(30 * time.Second),
Name: "drop_expensive",
WriteRelabelConfigs: []*relabel.Config{
{
- SourceLabels: model.LabelNames{"__name__"},
- Separator: ";",
- Regex: relabel.MustNewRegexp("expensive.*"),
- Replacement: "$1",
- Action: relabel.Drop,
+ SourceLabels: model.LabelNames{"__name__"},
+ Separator: ";",
+ Regex: relabel.MustNewRegexp("expensive.*"),
+ Replacement: "$1",
+ Action: relabel.Drop,
+ NameValidationScheme: model.UTF8Validation,
},
},
QueueConfig: DefaultQueueConfig,
@@ -137,12 +148,12 @@ var expectedConf = &Config{
},
},
FollowRedirects: true,
- EnableHTTP2: true,
+ EnableHTTP2: false,
},
},
{
URL: mustParseURL("http://remote2/push"),
- ProtobufMessage: RemoteWriteProtoMsgV2,
+ ProtobufMessage: remoteapi.WriteV2MessageType,
RemoteTimeout: model.Duration(30 * time.Second),
QueueConfig: DefaultQueueConfig,
MetadataConfig: DefaultMetadataConfig,
@@ -153,7 +164,7 @@ var expectedConf = &Config{
KeyFile: filepath.FromSlash("testdata/valid_key_file"),
},
FollowRedirects: true,
- EnableHTTP2: true,
+ EnableHTTP2: false,
},
Headers: map[string]string{"name": "value"},
},
@@ -163,6 +174,9 @@ var expectedConf = &Config{
PromoteResourceAttributes: []string{
"k8s.cluster.name", "k8s.job.name", "k8s.namespace.name",
},
+ TranslationStrategy: otlptranslator.UnderscoreEscapingWithSuffixes,
+ LabelNameUnderscoreSanitization: true,
+ LabelNamePreserveMultipleUnderscores: true,
},
RemoteReadConfigs: []*RemoteReadConfig{
@@ -201,19 +215,26 @@ var expectedConf = &Config{
{
JobName: "prometheus",
- HonorLabels: true,
- HonorTimestamps: true,
- ScrapeInterval: model.Duration(15 * time.Second),
- ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
- EnableCompression: true,
- BodySizeLimit: globBodySizeLimit,
- SampleLimit: globSampleLimit,
- TargetLimit: globTargetLimit,
- LabelLimit: globLabelLimit,
- LabelNameLengthLimit: globLabelNameLengthLimit,
- LabelValueLengthLimit: globLabelValueLengthLimit,
- ScrapeProtocols: DefaultGlobalConfig.ScrapeProtocols,
- ScrapeFailureLogFile: "testdata/fail_prom.log",
+ HonorLabels: true,
+ HonorTimestamps: true,
+ ScrapeInterval: model.Duration(15 * time.Second),
+ ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
+ EnableCompression: true,
+ BodySizeLimit: globBodySizeLimit,
+ SampleLimit: globSampleLimit,
+ TargetLimit: globTargetLimit,
+ LabelLimit: globLabelLimit,
+ LabelNameLengthLimit: globLabelNameLengthLimit,
+ LabelValueLengthLimit: globLabelValueLengthLimit,
+ ScrapeProtocols: DefaultScrapeProtocols,
+ ScrapeFallbackProtocol: PrometheusText0_0_4,
+ ScrapeFailureLogFile: "testdata/fail_prom.log",
+ MetricNameValidationScheme: DefaultGlobalConfig.MetricNameValidationScheme,
+ MetricNameEscapingScheme: DefaultGlobalConfig.MetricNameEscapingScheme,
+ ScrapeNativeHistograms: boolPtr(false),
+ AlwaysScrapeClassicHistograms: boolPtr(false),
+ ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
MetricsPath: DefaultScrapeConfig.MetricsPath,
Scheme: DefaultScrapeConfig.Scheme,
@@ -265,68 +286,80 @@ var expectedConf = &Config{
RelabelConfigs: []*relabel.Config{
{
- SourceLabels: model.LabelNames{"job", "__meta_dns_name"},
- TargetLabel: "job",
- Separator: ";",
- Regex: relabel.MustNewRegexp("(.*)some-[regex]"),
- Replacement: "foo-${1}",
- Action: relabel.Replace,
+ SourceLabels: model.LabelNames{"job", "__meta_dns_name"},
+ TargetLabel: "job",
+ Separator: ";",
+ Regex: relabel.MustNewRegexp("(.*)some-[regex]"),
+ Replacement: "foo-${1}",
+ Action: relabel.Replace,
+ NameValidationScheme: model.UTF8Validation,
},
{
- SourceLabels: model.LabelNames{"abc"},
- TargetLabel: "cde",
- Separator: ";",
- Regex: relabel.DefaultRelabelConfig.Regex,
- Replacement: relabel.DefaultRelabelConfig.Replacement,
- Action: relabel.Replace,
+ SourceLabels: model.LabelNames{"abc"},
+ TargetLabel: "cde",
+ Separator: ";",
+ Regex: relabel.DefaultRelabelConfig.Regex,
+ Replacement: relabel.DefaultRelabelConfig.Replacement,
+ Action: relabel.Replace,
+ NameValidationScheme: model.UTF8Validation,
},
{
- TargetLabel: "abc",
- Separator: ";",
- Regex: relabel.DefaultRelabelConfig.Regex,
- Replacement: "static",
- Action: relabel.Replace,
+ TargetLabel: "abc",
+ Separator: ";",
+ Regex: relabel.DefaultRelabelConfig.Regex,
+ Replacement: "static",
+ Action: relabel.Replace,
+ NameValidationScheme: model.UTF8Validation,
},
{
- TargetLabel: "abc",
- Separator: ";",
- Regex: relabel.MustNewRegexp(""),
- Replacement: "static",
- Action: relabel.Replace,
+ TargetLabel: "abc",
+ Separator: ";",
+ Regex: relabel.MustNewRegexp(""),
+ Replacement: "static",
+ Action: relabel.Replace,
+ NameValidationScheme: model.UTF8Validation,
},
{
- SourceLabels: model.LabelNames{"foo"},
- TargetLabel: "abc",
- Action: relabel.KeepEqual,
- Regex: relabel.DefaultRelabelConfig.Regex,
- Replacement: relabel.DefaultRelabelConfig.Replacement,
- Separator: relabel.DefaultRelabelConfig.Separator,
+ SourceLabels: model.LabelNames{"foo"},
+ TargetLabel: "abc",
+ Action: relabel.KeepEqual,
+ Regex: relabel.DefaultRelabelConfig.Regex,
+ Replacement: relabel.DefaultRelabelConfig.Replacement,
+ Separator: relabel.DefaultRelabelConfig.Separator,
+ NameValidationScheme: model.UTF8Validation,
},
{
- SourceLabels: model.LabelNames{"foo"},
- TargetLabel: "abc",
- Action: relabel.DropEqual,
- Regex: relabel.DefaultRelabelConfig.Regex,
- Replacement: relabel.DefaultRelabelConfig.Replacement,
- Separator: relabel.DefaultRelabelConfig.Separator,
+ SourceLabels: model.LabelNames{"foo"},
+ TargetLabel: "abc",
+ Action: relabel.DropEqual,
+ Regex: relabel.DefaultRelabelConfig.Regex,
+ Replacement: relabel.DefaultRelabelConfig.Replacement,
+ Separator: relabel.DefaultRelabelConfig.Separator,
+ NameValidationScheme: model.UTF8Validation,
},
},
},
{
JobName: "service-x",
- HonorTimestamps: true,
- ScrapeInterval: model.Duration(50 * time.Second),
- ScrapeTimeout: model.Duration(5 * time.Second),
- EnableCompression: true,
- BodySizeLimit: 10 * units.MiB,
- SampleLimit: 1000,
- TargetLimit: 35,
- LabelLimit: 35,
- LabelNameLengthLimit: 210,
- LabelValueLengthLimit: 210,
- ScrapeProtocols: []ScrapeProtocol{PrometheusText0_0_4},
- ScrapeFailureLogFile: globScrapeFailureLogFile,
+ HonorTimestamps: true,
+ ScrapeInterval: model.Duration(50 * time.Second),
+ ScrapeTimeout: model.Duration(5 * time.Second),
+ EnableCompression: true,
+ BodySizeLimit: 10 * units.MiB,
+ SampleLimit: 1000,
+ TargetLimit: 35,
+ LabelLimit: 35,
+ LabelNameLengthLimit: 210,
+ LabelValueLengthLimit: 210,
+ ScrapeProtocols: []ScrapeProtocol{PrometheusText0_0_4},
+ ScrapeFailureLogFile: globScrapeFailureLogFile,
+ MetricNameValidationScheme: DefaultGlobalConfig.MetricNameValidationScheme,
+ MetricNameEscapingScheme: DefaultGlobalConfig.MetricNameEscapingScheme,
+ ScrapeNativeHistograms: boolPtr(false),
+ AlwaysScrapeClassicHistograms: boolPtr(false),
+ ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
HTTPClientConfig: config.HTTPClientConfig{
BasicAuth: &config.BasicAuth{
@@ -359,72 +392,85 @@ var expectedConf = &Config{
RelabelConfigs: []*relabel.Config{
{
- SourceLabels: model.LabelNames{"job"},
- Regex: relabel.MustNewRegexp("(.*)some-[regex]"),
- Separator: ";",
- Replacement: relabel.DefaultRelabelConfig.Replacement,
- Action: relabel.Drop,
+ SourceLabels: model.LabelNames{"job"},
+ Regex: relabel.MustNewRegexp("(.*)some-[regex]"),
+ Separator: ";",
+ Replacement: relabel.DefaultRelabelConfig.Replacement,
+ Action: relabel.Drop,
+ NameValidationScheme: model.UTF8Validation,
},
{
- SourceLabels: model.LabelNames{"__address__"},
- TargetLabel: "__tmp_hash",
- Regex: relabel.DefaultRelabelConfig.Regex,
- Replacement: relabel.DefaultRelabelConfig.Replacement,
- Modulus: 8,
- Separator: ";",
- Action: relabel.HashMod,
+ SourceLabels: model.LabelNames{"__address__"},
+ TargetLabel: "__tmp_hash",
+ Regex: relabel.DefaultRelabelConfig.Regex,
+ Replacement: relabel.DefaultRelabelConfig.Replacement,
+ Modulus: 8,
+ Separator: ";",
+ Action: relabel.HashMod,
+ NameValidationScheme: model.UTF8Validation,
},
{
- SourceLabels: model.LabelNames{"__tmp_hash"},
- Regex: relabel.MustNewRegexp("1"),
- Separator: ";",
- Replacement: relabel.DefaultRelabelConfig.Replacement,
- Action: relabel.Keep,
+ SourceLabels: model.LabelNames{"__tmp_hash"},
+ Regex: relabel.MustNewRegexp("1"),
+ Separator: ";",
+ Replacement: relabel.DefaultRelabelConfig.Replacement,
+ Action: relabel.Keep,
+ NameValidationScheme: model.UTF8Validation,
},
{
- Regex: relabel.MustNewRegexp("1"),
- Separator: ";",
- Replacement: relabel.DefaultRelabelConfig.Replacement,
- Action: relabel.LabelMap,
+ Regex: relabel.MustNewRegexp("1"),
+ Separator: ";",
+ Replacement: relabel.DefaultRelabelConfig.Replacement,
+ Action: relabel.LabelMap,
+ NameValidationScheme: model.UTF8Validation,
},
{
- Regex: relabel.MustNewRegexp("d"),
- Separator: ";",
- Replacement: relabel.DefaultRelabelConfig.Replacement,
- Action: relabel.LabelDrop,
+ Regex: relabel.MustNewRegexp("d"),
+ Separator: ";",
+ Replacement: relabel.DefaultRelabelConfig.Replacement,
+ Action: relabel.LabelDrop,
+ NameValidationScheme: model.UTF8Validation,
},
{
- Regex: relabel.MustNewRegexp("k"),
- Separator: ";",
- Replacement: relabel.DefaultRelabelConfig.Replacement,
- Action: relabel.LabelKeep,
+ Regex: relabel.MustNewRegexp("k"),
+ Separator: ";",
+ Replacement: relabel.DefaultRelabelConfig.Replacement,
+ Action: relabel.LabelKeep,
+ NameValidationScheme: model.UTF8Validation,
},
},
MetricRelabelConfigs: []*relabel.Config{
{
- SourceLabels: model.LabelNames{"__name__"},
- Regex: relabel.MustNewRegexp("expensive_metric.*"),
- Separator: ";",
- Replacement: relabel.DefaultRelabelConfig.Replacement,
- Action: relabel.Drop,
+ SourceLabels: model.LabelNames{"__name__"},
+ Regex: relabel.MustNewRegexp("expensive_metric.*"),
+ Separator: ";",
+ Replacement: relabel.DefaultRelabelConfig.Replacement,
+ Action: relabel.Drop,
+ NameValidationScheme: model.UTF8Validation,
},
},
},
{
JobName: "service-y",
- HonorTimestamps: true,
- ScrapeInterval: model.Duration(15 * time.Second),
- ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
- EnableCompression: true,
- BodySizeLimit: globBodySizeLimit,
- SampleLimit: globSampleLimit,
- TargetLimit: globTargetLimit,
- LabelLimit: globLabelLimit,
- LabelNameLengthLimit: globLabelNameLengthLimit,
- LabelValueLengthLimit: globLabelValueLengthLimit,
- ScrapeProtocols: DefaultGlobalConfig.ScrapeProtocols,
- ScrapeFailureLogFile: globScrapeFailureLogFile,
+ HonorTimestamps: true,
+ ScrapeInterval: model.Duration(15 * time.Second),
+ ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
+ EnableCompression: true,
+ BodySizeLimit: globBodySizeLimit,
+ SampleLimit: globSampleLimit,
+ TargetLimit: globTargetLimit,
+ LabelLimit: globLabelLimit,
+ LabelNameLengthLimit: globLabelNameLengthLimit,
+ LabelValueLengthLimit: globLabelValueLengthLimit,
+ ScrapeProtocols: DefaultScrapeProtocols,
+ ScrapeFailureLogFile: globScrapeFailureLogFile,
+ MetricNameValidationScheme: DefaultGlobalConfig.MetricNameValidationScheme,
+ MetricNameEscapingScheme: DefaultGlobalConfig.MetricNameEscapingScheme,
+ ScrapeNativeHistograms: boolPtr(false),
+ AlwaysScrapeClassicHistograms: boolPtr(false),
+ ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
MetricsPath: DefaultScrapeConfig.MetricsPath,
Scheme: DefaultScrapeConfig.Scheme,
@@ -436,6 +482,7 @@ var expectedConf = &Config{
PathPrefix: "/consul",
Token: "mysecret",
Services: []string{"nginx", "cache", "mysql"},
+ HealthFilter: `Service.Tags contains "canary"`,
ServiceTags: []string{"canary", "v1"},
NodeMeta: map[string]string{"rack": "123"},
TagSeparator: consul.DefaultSDConfig.TagSeparator,
@@ -457,30 +504,37 @@ var expectedConf = &Config{
RelabelConfigs: []*relabel.Config{
{
- SourceLabels: model.LabelNames{"__meta_sd_consul_tags"},
- Regex: relabel.MustNewRegexp("label:([^=]+)=([^,]+)"),
- Separator: ",",
- TargetLabel: "${1}",
- Replacement: "${2}",
- Action: relabel.Replace,
+ SourceLabels: model.LabelNames{"__meta_sd_consul_tags"},
+ Regex: relabel.MustNewRegexp("label:([^=]+)=([^,]+)"),
+ Separator: ",",
+ TargetLabel: "${1}",
+ Replacement: "${2}",
+ Action: relabel.Replace,
+ NameValidationScheme: model.UTF8Validation,
},
},
},
{
JobName: "service-z",
- HonorTimestamps: true,
- ScrapeInterval: model.Duration(15 * time.Second),
- ScrapeTimeout: model.Duration(10 * time.Second),
- EnableCompression: true,
- BodySizeLimit: globBodySizeLimit,
- SampleLimit: globSampleLimit,
- TargetLimit: globTargetLimit,
- LabelLimit: globLabelLimit,
- LabelNameLengthLimit: globLabelNameLengthLimit,
- LabelValueLengthLimit: globLabelValueLengthLimit,
- ScrapeProtocols: DefaultGlobalConfig.ScrapeProtocols,
- ScrapeFailureLogFile: globScrapeFailureLogFile,
+ HonorTimestamps: true,
+ ScrapeInterval: model.Duration(15 * time.Second),
+ ScrapeTimeout: model.Duration(10 * time.Second),
+ EnableCompression: true,
+ BodySizeLimit: globBodySizeLimit,
+ SampleLimit: globSampleLimit,
+ TargetLimit: globTargetLimit,
+ LabelLimit: globLabelLimit,
+ LabelNameLengthLimit: globLabelNameLengthLimit,
+ LabelValueLengthLimit: globLabelValueLengthLimit,
+ ScrapeProtocols: DefaultScrapeProtocols,
+ ScrapeFailureLogFile: globScrapeFailureLogFile,
+ MetricNameValidationScheme: DefaultGlobalConfig.MetricNameValidationScheme,
+ MetricNameEscapingScheme: DefaultGlobalConfig.MetricNameEscapingScheme,
+ ScrapeNativeHistograms: boolPtr(false),
+ AlwaysScrapeClassicHistograms: boolPtr(false),
+ ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
MetricsPath: "/metrics",
Scheme: "http",
@@ -503,18 +557,24 @@ var expectedConf = &Config{
{
JobName: "service-kubernetes",
- HonorTimestamps: true,
- ScrapeInterval: model.Duration(15 * time.Second),
- ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
- EnableCompression: true,
- BodySizeLimit: globBodySizeLimit,
- SampleLimit: globSampleLimit,
- TargetLimit: globTargetLimit,
- LabelLimit: globLabelLimit,
- LabelNameLengthLimit: globLabelNameLengthLimit,
- LabelValueLengthLimit: globLabelValueLengthLimit,
- ScrapeProtocols: DefaultGlobalConfig.ScrapeProtocols,
- ScrapeFailureLogFile: globScrapeFailureLogFile,
+ HonorTimestamps: true,
+ ScrapeInterval: model.Duration(15 * time.Second),
+ ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
+ EnableCompression: true,
+ BodySizeLimit: globBodySizeLimit,
+ SampleLimit: globSampleLimit,
+ TargetLimit: globTargetLimit,
+ LabelLimit: globLabelLimit,
+ LabelNameLengthLimit: globLabelNameLengthLimit,
+ LabelValueLengthLimit: globLabelValueLengthLimit,
+ ScrapeProtocols: DefaultScrapeProtocols,
+ ScrapeFailureLogFile: globScrapeFailureLogFile,
+ MetricNameValidationScheme: DefaultGlobalConfig.MetricNameValidationScheme,
+ MetricNameEscapingScheme: DefaultGlobalConfig.MetricNameEscapingScheme,
+ ScrapeNativeHistograms: boolPtr(false),
+ AlwaysScrapeClassicHistograms: boolPtr(false),
+ ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
MetricsPath: DefaultScrapeConfig.MetricsPath,
Scheme: DefaultScrapeConfig.Scheme,
@@ -543,18 +603,24 @@ var expectedConf = &Config{
{
JobName: "service-kubernetes-namespaces",
- HonorTimestamps: true,
- ScrapeInterval: model.Duration(15 * time.Second),
- ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
- EnableCompression: true,
- BodySizeLimit: globBodySizeLimit,
- SampleLimit: globSampleLimit,
- TargetLimit: globTargetLimit,
- LabelLimit: globLabelLimit,
- LabelNameLengthLimit: globLabelNameLengthLimit,
- LabelValueLengthLimit: globLabelValueLengthLimit,
- ScrapeProtocols: DefaultGlobalConfig.ScrapeProtocols,
- ScrapeFailureLogFile: globScrapeFailureLogFile,
+ HonorTimestamps: true,
+ ScrapeInterval: model.Duration(15 * time.Second),
+ ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
+ EnableCompression: true,
+ BodySizeLimit: globBodySizeLimit,
+ SampleLimit: globSampleLimit,
+ TargetLimit: globTargetLimit,
+ LabelLimit: globLabelLimit,
+ LabelNameLengthLimit: globLabelNameLengthLimit,
+ LabelValueLengthLimit: globLabelValueLengthLimit,
+ ScrapeProtocols: DefaultScrapeProtocols,
+ ScrapeFailureLogFile: globScrapeFailureLogFile,
+ MetricNameValidationScheme: DefaultGlobalConfig.MetricNameValidationScheme,
+ MetricNameEscapingScheme: DefaultGlobalConfig.MetricNameEscapingScheme,
+ ScrapeNativeHistograms: boolPtr(false),
+ AlwaysScrapeClassicHistograms: boolPtr(false),
+ ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
MetricsPath: DefaultScrapeConfig.MetricsPath,
Scheme: DefaultScrapeConfig.Scheme,
@@ -583,18 +649,24 @@ var expectedConf = &Config{
{
JobName: "service-kuma",
- HonorTimestamps: true,
- ScrapeInterval: model.Duration(15 * time.Second),
- ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
- EnableCompression: true,
- BodySizeLimit: globBodySizeLimit,
- SampleLimit: globSampleLimit,
- TargetLimit: globTargetLimit,
- LabelLimit: globLabelLimit,
- LabelNameLengthLimit: globLabelNameLengthLimit,
- LabelValueLengthLimit: globLabelValueLengthLimit,
- ScrapeProtocols: DefaultGlobalConfig.ScrapeProtocols,
- ScrapeFailureLogFile: globScrapeFailureLogFile,
+ HonorTimestamps: true,
+ ScrapeInterval: model.Duration(15 * time.Second),
+ ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
+ EnableCompression: true,
+ BodySizeLimit: globBodySizeLimit,
+ SampleLimit: globSampleLimit,
+ TargetLimit: globTargetLimit,
+ LabelLimit: globLabelLimit,
+ LabelNameLengthLimit: globLabelNameLengthLimit,
+ LabelValueLengthLimit: globLabelValueLengthLimit,
+ ScrapeProtocols: DefaultScrapeProtocols,
+ ScrapeFailureLogFile: globScrapeFailureLogFile,
+ MetricNameValidationScheme: DefaultGlobalConfig.MetricNameValidationScheme,
+ MetricNameEscapingScheme: DefaultGlobalConfig.MetricNameEscapingScheme,
+ ScrapeNativeHistograms: boolPtr(false),
+ AlwaysScrapeClassicHistograms: boolPtr(false),
+ ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
MetricsPath: DefaultScrapeConfig.MetricsPath,
Scheme: DefaultScrapeConfig.Scheme,
@@ -613,18 +685,24 @@ var expectedConf = &Config{
{
JobName: "service-marathon",
- HonorTimestamps: true,
- ScrapeInterval: model.Duration(15 * time.Second),
- ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
- EnableCompression: true,
- BodySizeLimit: globBodySizeLimit,
- SampleLimit: globSampleLimit,
- TargetLimit: globTargetLimit,
- LabelLimit: globLabelLimit,
- LabelNameLengthLimit: globLabelNameLengthLimit,
- LabelValueLengthLimit: globLabelValueLengthLimit,
- ScrapeProtocols: DefaultGlobalConfig.ScrapeProtocols,
- ScrapeFailureLogFile: globScrapeFailureLogFile,
+ HonorTimestamps: true,
+ ScrapeInterval: model.Duration(15 * time.Second),
+ ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
+ EnableCompression: true,
+ BodySizeLimit: globBodySizeLimit,
+ SampleLimit: globSampleLimit,
+ TargetLimit: globTargetLimit,
+ LabelLimit: globLabelLimit,
+ LabelNameLengthLimit: globLabelNameLengthLimit,
+ LabelValueLengthLimit: globLabelValueLengthLimit,
+ ScrapeProtocols: DefaultScrapeProtocols,
+ ScrapeFailureLogFile: globScrapeFailureLogFile,
+ MetricNameValidationScheme: DefaultGlobalConfig.MetricNameValidationScheme,
+ MetricNameEscapingScheme: DefaultGlobalConfig.MetricNameEscapingScheme,
+ ScrapeNativeHistograms: boolPtr(false),
+ AlwaysScrapeClassicHistograms: boolPtr(false),
+ ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
MetricsPath: DefaultScrapeConfig.MetricsPath,
Scheme: DefaultScrapeConfig.Scheme,
@@ -651,18 +729,24 @@ var expectedConf = &Config{
{
JobName: "service-nomad",
- HonorTimestamps: true,
- ScrapeInterval: model.Duration(15 * time.Second),
- ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
- EnableCompression: true,
- BodySizeLimit: globBodySizeLimit,
- SampleLimit: globSampleLimit,
- TargetLimit: globTargetLimit,
- LabelLimit: globLabelLimit,
- LabelNameLengthLimit: globLabelNameLengthLimit,
- LabelValueLengthLimit: globLabelValueLengthLimit,
- ScrapeProtocols: DefaultGlobalConfig.ScrapeProtocols,
- ScrapeFailureLogFile: globScrapeFailureLogFile,
+ HonorTimestamps: true,
+ ScrapeInterval: model.Duration(15 * time.Second),
+ ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
+ EnableCompression: true,
+ BodySizeLimit: globBodySizeLimit,
+ SampleLimit: globSampleLimit,
+ TargetLimit: globTargetLimit,
+ LabelLimit: globLabelLimit,
+ LabelNameLengthLimit: globLabelNameLengthLimit,
+ LabelValueLengthLimit: globLabelValueLengthLimit,
+ ScrapeProtocols: DefaultScrapeProtocols,
+ ScrapeFailureLogFile: globScrapeFailureLogFile,
+ MetricNameValidationScheme: DefaultGlobalConfig.MetricNameValidationScheme,
+ MetricNameEscapingScheme: DefaultGlobalConfig.MetricNameEscapingScheme,
+ ScrapeNativeHistograms: boolPtr(false),
+ AlwaysScrapeClassicHistograms: boolPtr(false),
+ ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
MetricsPath: DefaultScrapeConfig.MetricsPath,
Scheme: DefaultScrapeConfig.Scheme,
@@ -686,18 +770,24 @@ var expectedConf = &Config{
{
JobName: "service-ec2",
- HonorTimestamps: true,
- ScrapeInterval: model.Duration(15 * time.Second),
- ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
- EnableCompression: true,
- BodySizeLimit: globBodySizeLimit,
- SampleLimit: globSampleLimit,
- TargetLimit: globTargetLimit,
- LabelLimit: globLabelLimit,
- LabelNameLengthLimit: globLabelNameLengthLimit,
- LabelValueLengthLimit: globLabelValueLengthLimit,
- ScrapeProtocols: DefaultGlobalConfig.ScrapeProtocols,
- ScrapeFailureLogFile: globScrapeFailureLogFile,
+ HonorTimestamps: true,
+ ScrapeInterval: model.Duration(15 * time.Second),
+ ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
+ EnableCompression: true,
+ BodySizeLimit: globBodySizeLimit,
+ SampleLimit: globSampleLimit,
+ TargetLimit: globTargetLimit,
+ LabelLimit: globLabelLimit,
+ LabelNameLengthLimit: globLabelNameLengthLimit,
+ LabelValueLengthLimit: globLabelValueLengthLimit,
+ ScrapeProtocols: DefaultScrapeProtocols,
+ ScrapeFailureLogFile: globScrapeFailureLogFile,
+ MetricNameValidationScheme: DefaultGlobalConfig.MetricNameValidationScheme,
+ MetricNameEscapingScheme: DefaultGlobalConfig.MetricNameEscapingScheme,
+ ScrapeNativeHistograms: boolPtr(false),
+ AlwaysScrapeClassicHistograms: boolPtr(false),
+ ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
MetricsPath: DefaultScrapeConfig.MetricsPath,
Scheme: DefaultScrapeConfig.Scheme,
@@ -711,7 +801,7 @@ var expectedConf = &Config{
Profile: "profile",
RefreshInterval: model.Duration(60 * time.Second),
Port: 80,
- Filters: []*aws.EC2Filter{
+ Filters: []*aws.Filter{
{
Name: "tag:environment",
Values: []string{"prod"},
@@ -728,18 +818,24 @@ var expectedConf = &Config{
{
JobName: "service-lightsail",
- HonorTimestamps: true,
- ScrapeInterval: model.Duration(15 * time.Second),
- ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
- EnableCompression: true,
- BodySizeLimit: globBodySizeLimit,
- SampleLimit: globSampleLimit,
- TargetLimit: globTargetLimit,
- LabelLimit: globLabelLimit,
- LabelNameLengthLimit: globLabelNameLengthLimit,
- LabelValueLengthLimit: globLabelValueLengthLimit,
- ScrapeProtocols: DefaultGlobalConfig.ScrapeProtocols,
- ScrapeFailureLogFile: globScrapeFailureLogFile,
+ HonorTimestamps: true,
+ ScrapeInterval: model.Duration(15 * time.Second),
+ ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
+ EnableCompression: true,
+ BodySizeLimit: globBodySizeLimit,
+ SampleLimit: globSampleLimit,
+ TargetLimit: globTargetLimit,
+ LabelLimit: globLabelLimit,
+ LabelNameLengthLimit: globLabelNameLengthLimit,
+ LabelValueLengthLimit: globLabelValueLengthLimit,
+ ScrapeProtocols: DefaultScrapeProtocols,
+ ScrapeFailureLogFile: globScrapeFailureLogFile,
+ MetricNameValidationScheme: DefaultGlobalConfig.MetricNameValidationScheme,
+ MetricNameEscapingScheme: DefaultGlobalConfig.MetricNameEscapingScheme,
+ ScrapeNativeHistograms: boolPtr(false),
+ AlwaysScrapeClassicHistograms: boolPtr(false),
+ ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
MetricsPath: DefaultScrapeConfig.MetricsPath,
Scheme: DefaultScrapeConfig.Scheme,
@@ -760,18 +856,24 @@ var expectedConf = &Config{
{
JobName: "service-azure",
- HonorTimestamps: true,
- ScrapeInterval: model.Duration(15 * time.Second),
- ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
- EnableCompression: true,
- BodySizeLimit: globBodySizeLimit,
- SampleLimit: globSampleLimit,
- TargetLimit: globTargetLimit,
- LabelLimit: globLabelLimit,
- LabelNameLengthLimit: globLabelNameLengthLimit,
- LabelValueLengthLimit: globLabelValueLengthLimit,
- ScrapeProtocols: DefaultGlobalConfig.ScrapeProtocols,
- ScrapeFailureLogFile: globScrapeFailureLogFile,
+ HonorTimestamps: true,
+ ScrapeInterval: model.Duration(15 * time.Second),
+ ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
+ EnableCompression: true,
+ BodySizeLimit: globBodySizeLimit,
+ SampleLimit: globSampleLimit,
+ TargetLimit: globTargetLimit,
+ LabelLimit: globLabelLimit,
+ LabelNameLengthLimit: globLabelNameLengthLimit,
+ LabelValueLengthLimit: globLabelValueLengthLimit,
+ ScrapeProtocols: DefaultScrapeProtocols,
+ ScrapeFailureLogFile: globScrapeFailureLogFile,
+ MetricNameValidationScheme: DefaultGlobalConfig.MetricNameValidationScheme,
+ MetricNameEscapingScheme: DefaultGlobalConfig.MetricNameEscapingScheme,
+ ScrapeNativeHistograms: boolPtr(false),
+ AlwaysScrapeClassicHistograms: boolPtr(false),
+ ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
MetricsPath: DefaultScrapeConfig.MetricsPath,
Scheme: DefaultScrapeConfig.Scheme,
@@ -795,18 +897,24 @@ var expectedConf = &Config{
{
JobName: "service-nerve",
- HonorTimestamps: true,
- ScrapeInterval: model.Duration(15 * time.Second),
- ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
- EnableCompression: true,
- BodySizeLimit: globBodySizeLimit,
- SampleLimit: globSampleLimit,
- TargetLimit: globTargetLimit,
- LabelLimit: globLabelLimit,
- LabelNameLengthLimit: globLabelNameLengthLimit,
- LabelValueLengthLimit: globLabelValueLengthLimit,
- ScrapeProtocols: DefaultGlobalConfig.ScrapeProtocols,
- ScrapeFailureLogFile: globScrapeFailureLogFile,
+ HonorTimestamps: true,
+ ScrapeInterval: model.Duration(15 * time.Second),
+ ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
+ EnableCompression: true,
+ BodySizeLimit: globBodySizeLimit,
+ SampleLimit: globSampleLimit,
+ TargetLimit: globTargetLimit,
+ LabelLimit: globLabelLimit,
+ LabelNameLengthLimit: globLabelNameLengthLimit,
+ LabelValueLengthLimit: globLabelValueLengthLimit,
+ ScrapeProtocols: DefaultScrapeProtocols,
+ ScrapeFailureLogFile: globScrapeFailureLogFile,
+ MetricNameValidationScheme: DefaultGlobalConfig.MetricNameValidationScheme,
+ MetricNameEscapingScheme: DefaultGlobalConfig.MetricNameEscapingScheme,
+ ScrapeNativeHistograms: boolPtr(false),
+ AlwaysScrapeClassicHistograms: boolPtr(false),
+ ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
MetricsPath: DefaultScrapeConfig.MetricsPath,
Scheme: DefaultScrapeConfig.Scheme,
@@ -823,18 +931,24 @@ var expectedConf = &Config{
{
JobName: "0123service-xxx",
- HonorTimestamps: true,
- ScrapeInterval: model.Duration(15 * time.Second),
- ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
- EnableCompression: true,
- BodySizeLimit: globBodySizeLimit,
- SampleLimit: globSampleLimit,
- TargetLimit: globTargetLimit,
- LabelLimit: globLabelLimit,
- LabelNameLengthLimit: globLabelNameLengthLimit,
- LabelValueLengthLimit: globLabelValueLengthLimit,
- ScrapeProtocols: DefaultGlobalConfig.ScrapeProtocols,
- ScrapeFailureLogFile: globScrapeFailureLogFile,
+ HonorTimestamps: true,
+ ScrapeInterval: model.Duration(15 * time.Second),
+ ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
+ EnableCompression: true,
+ BodySizeLimit: globBodySizeLimit,
+ SampleLimit: globSampleLimit,
+ TargetLimit: globTargetLimit,
+ LabelLimit: globLabelLimit,
+ LabelNameLengthLimit: globLabelNameLengthLimit,
+ LabelValueLengthLimit: globLabelValueLengthLimit,
+ ScrapeProtocols: DefaultScrapeProtocols,
+ ScrapeFailureLogFile: globScrapeFailureLogFile,
+ MetricNameValidationScheme: DefaultGlobalConfig.MetricNameValidationScheme,
+ MetricNameEscapingScheme: DefaultGlobalConfig.MetricNameEscapingScheme,
+ ScrapeNativeHistograms: boolPtr(false),
+ AlwaysScrapeClassicHistograms: boolPtr(false),
+ ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
MetricsPath: DefaultScrapeConfig.MetricsPath,
Scheme: DefaultScrapeConfig.Scheme,
@@ -854,18 +968,24 @@ var expectedConf = &Config{
{
JobName: "badfederation",
- HonorTimestamps: false,
- ScrapeInterval: model.Duration(15 * time.Second),
- ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
- EnableCompression: true,
- BodySizeLimit: globBodySizeLimit,
- SampleLimit: globSampleLimit,
- TargetLimit: globTargetLimit,
- LabelLimit: globLabelLimit,
- LabelNameLengthLimit: globLabelNameLengthLimit,
- LabelValueLengthLimit: globLabelValueLengthLimit,
- ScrapeProtocols: DefaultGlobalConfig.ScrapeProtocols,
- ScrapeFailureLogFile: globScrapeFailureLogFile,
+ HonorTimestamps: false,
+ ScrapeInterval: model.Duration(15 * time.Second),
+ ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
+ EnableCompression: true,
+ BodySizeLimit: globBodySizeLimit,
+ SampleLimit: globSampleLimit,
+ TargetLimit: globTargetLimit,
+ LabelLimit: globLabelLimit,
+ LabelNameLengthLimit: globLabelNameLengthLimit,
+ LabelValueLengthLimit: globLabelValueLengthLimit,
+ ScrapeProtocols: DefaultScrapeProtocols,
+ ScrapeFailureLogFile: globScrapeFailureLogFile,
+ MetricNameValidationScheme: DefaultGlobalConfig.MetricNameValidationScheme,
+ MetricNameEscapingScheme: DefaultGlobalConfig.MetricNameEscapingScheme,
+ ScrapeNativeHistograms: boolPtr(false),
+ AlwaysScrapeClassicHistograms: boolPtr(false),
+ ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
MetricsPath: "/federate",
Scheme: DefaultScrapeConfig.Scheme,
@@ -885,18 +1005,24 @@ var expectedConf = &Config{
{
JobName: "測試",
- HonorTimestamps: true,
- ScrapeInterval: model.Duration(15 * time.Second),
- ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
- EnableCompression: true,
- BodySizeLimit: globBodySizeLimit,
- SampleLimit: globSampleLimit,
- TargetLimit: globTargetLimit,
- LabelLimit: globLabelLimit,
- LabelNameLengthLimit: globLabelNameLengthLimit,
- LabelValueLengthLimit: globLabelValueLengthLimit,
- ScrapeProtocols: DefaultGlobalConfig.ScrapeProtocols,
- ScrapeFailureLogFile: globScrapeFailureLogFile,
+ HonorTimestamps: true,
+ ScrapeInterval: model.Duration(15 * time.Second),
+ ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
+ EnableCompression: true,
+ BodySizeLimit: globBodySizeLimit,
+ SampleLimit: globSampleLimit,
+ TargetLimit: globTargetLimit,
+ LabelLimit: globLabelLimit,
+ LabelNameLengthLimit: globLabelNameLengthLimit,
+ LabelValueLengthLimit: globLabelValueLengthLimit,
+ ScrapeProtocols: DefaultScrapeProtocols,
+ ScrapeFailureLogFile: globScrapeFailureLogFile,
+ MetricNameValidationScheme: DefaultGlobalConfig.MetricNameValidationScheme,
+ MetricNameEscapingScheme: DefaultGlobalConfig.MetricNameEscapingScheme,
+ ScrapeNativeHistograms: boolPtr(false),
+ AlwaysScrapeClassicHistograms: boolPtr(false),
+ ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
MetricsPath: DefaultScrapeConfig.MetricsPath,
Scheme: DefaultScrapeConfig.Scheme,
@@ -916,18 +1042,24 @@ var expectedConf = &Config{
{
JobName: "httpsd",
- HonorTimestamps: true,
- ScrapeInterval: model.Duration(15 * time.Second),
- ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
- EnableCompression: true,
- BodySizeLimit: globBodySizeLimit,
- SampleLimit: globSampleLimit,
- TargetLimit: globTargetLimit,
- LabelLimit: globLabelLimit,
- LabelNameLengthLimit: globLabelNameLengthLimit,
- LabelValueLengthLimit: globLabelValueLengthLimit,
- ScrapeProtocols: DefaultGlobalConfig.ScrapeProtocols,
- ScrapeFailureLogFile: globScrapeFailureLogFile,
+ HonorTimestamps: true,
+ ScrapeInterval: model.Duration(15 * time.Second),
+ ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
+ EnableCompression: true,
+ BodySizeLimit: globBodySizeLimit,
+ SampleLimit: globSampleLimit,
+ TargetLimit: globTargetLimit,
+ LabelLimit: globLabelLimit,
+ LabelNameLengthLimit: globLabelNameLengthLimit,
+ LabelValueLengthLimit: globLabelValueLengthLimit,
+ ScrapeProtocols: DefaultScrapeProtocols,
+ ScrapeFailureLogFile: globScrapeFailureLogFile,
+ MetricNameValidationScheme: DefaultGlobalConfig.MetricNameValidationScheme,
+ MetricNameEscapingScheme: DefaultGlobalConfig.MetricNameEscapingScheme,
+ ScrapeNativeHistograms: boolPtr(false),
+ AlwaysScrapeClassicHistograms: boolPtr(false),
+ ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
MetricsPath: DefaultScrapeConfig.MetricsPath,
Scheme: DefaultScrapeConfig.Scheme,
@@ -944,18 +1076,24 @@ var expectedConf = &Config{
{
JobName: "service-triton",
- HonorTimestamps: true,
- ScrapeInterval: model.Duration(15 * time.Second),
- ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
- EnableCompression: true,
- BodySizeLimit: globBodySizeLimit,
- SampleLimit: globSampleLimit,
- TargetLimit: globTargetLimit,
- LabelLimit: globLabelLimit,
- LabelNameLengthLimit: globLabelNameLengthLimit,
- LabelValueLengthLimit: globLabelValueLengthLimit,
- ScrapeProtocols: DefaultGlobalConfig.ScrapeProtocols,
- ScrapeFailureLogFile: globScrapeFailureLogFile,
+ HonorTimestamps: true,
+ ScrapeInterval: model.Duration(15 * time.Second),
+ ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
+ EnableCompression: true,
+ BodySizeLimit: globBodySizeLimit,
+ SampleLimit: globSampleLimit,
+ TargetLimit: globTargetLimit,
+ LabelLimit: globLabelLimit,
+ LabelNameLengthLimit: globLabelNameLengthLimit,
+ LabelValueLengthLimit: globLabelValueLengthLimit,
+ ScrapeProtocols: DefaultScrapeProtocols,
+ ScrapeFailureLogFile: globScrapeFailureLogFile,
+ MetricNameValidationScheme: DefaultGlobalConfig.MetricNameValidationScheme,
+ MetricNameEscapingScheme: DefaultGlobalConfig.MetricNameEscapingScheme,
+ ScrapeNativeHistograms: boolPtr(false),
+ AlwaysScrapeClassicHistograms: boolPtr(false),
+ ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
MetricsPath: DefaultScrapeConfig.MetricsPath,
Scheme: DefaultScrapeConfig.Scheme,
@@ -980,18 +1118,24 @@ var expectedConf = &Config{
{
JobName: "digitalocean-droplets",
- HonorTimestamps: true,
- ScrapeInterval: model.Duration(15 * time.Second),
- ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
- EnableCompression: true,
- BodySizeLimit: globBodySizeLimit,
- SampleLimit: globSampleLimit,
- TargetLimit: globTargetLimit,
- LabelLimit: globLabelLimit,
- LabelNameLengthLimit: globLabelNameLengthLimit,
- LabelValueLengthLimit: globLabelValueLengthLimit,
- ScrapeProtocols: DefaultGlobalConfig.ScrapeProtocols,
- ScrapeFailureLogFile: globScrapeFailureLogFile,
+ HonorTimestamps: true,
+ ScrapeInterval: model.Duration(15 * time.Second),
+ ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
+ EnableCompression: true,
+ BodySizeLimit: globBodySizeLimit,
+ SampleLimit: globSampleLimit,
+ TargetLimit: globTargetLimit,
+ LabelLimit: globLabelLimit,
+ LabelNameLengthLimit: globLabelNameLengthLimit,
+ LabelValueLengthLimit: globLabelValueLengthLimit,
+ ScrapeProtocols: DefaultScrapeProtocols,
+ ScrapeFailureLogFile: globScrapeFailureLogFile,
+ MetricNameValidationScheme: DefaultGlobalConfig.MetricNameValidationScheme,
+ MetricNameEscapingScheme: DefaultGlobalConfig.MetricNameEscapingScheme,
+ ScrapeNativeHistograms: boolPtr(false),
+ AlwaysScrapeClassicHistograms: boolPtr(false),
+ ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
MetricsPath: DefaultScrapeConfig.MetricsPath,
Scheme: DefaultScrapeConfig.Scheme,
@@ -1009,24 +1153,31 @@ var expectedConf = &Config{
},
Port: 80,
RefreshInterval: model.Duration(60 * time.Second),
+ Role: "droplets",
},
},
},
{
JobName: "docker",
- HonorTimestamps: true,
- ScrapeInterval: model.Duration(15 * time.Second),
- ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
- EnableCompression: true,
- BodySizeLimit: globBodySizeLimit,
- SampleLimit: globSampleLimit,
- TargetLimit: globTargetLimit,
- LabelLimit: globLabelLimit,
- LabelNameLengthLimit: globLabelNameLengthLimit,
- LabelValueLengthLimit: globLabelValueLengthLimit,
- ScrapeProtocols: DefaultGlobalConfig.ScrapeProtocols,
- ScrapeFailureLogFile: globScrapeFailureLogFile,
+ HonorTimestamps: true,
+ ScrapeInterval: model.Duration(15 * time.Second),
+ ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
+ EnableCompression: true,
+ BodySizeLimit: globBodySizeLimit,
+ SampleLimit: globSampleLimit,
+ TargetLimit: globTargetLimit,
+ LabelLimit: globLabelLimit,
+ LabelNameLengthLimit: globLabelNameLengthLimit,
+ LabelValueLengthLimit: globLabelValueLengthLimit,
+ ScrapeProtocols: DefaultScrapeProtocols,
+ ScrapeFailureLogFile: globScrapeFailureLogFile,
+ MetricNameValidationScheme: DefaultGlobalConfig.MetricNameValidationScheme,
+ MetricNameEscapingScheme: DefaultGlobalConfig.MetricNameEscapingScheme,
+ ScrapeNativeHistograms: boolPtr(false),
+ AlwaysScrapeClassicHistograms: boolPtr(false),
+ ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
MetricsPath: DefaultScrapeConfig.MetricsPath,
Scheme: DefaultScrapeConfig.Scheme,
@@ -1047,18 +1198,24 @@ var expectedConf = &Config{
{
JobName: "dockerswarm",
- HonorTimestamps: true,
- ScrapeInterval: model.Duration(15 * time.Second),
- ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
- EnableCompression: true,
- BodySizeLimit: globBodySizeLimit,
- SampleLimit: globSampleLimit,
- TargetLimit: globTargetLimit,
- LabelLimit: globLabelLimit,
- LabelNameLengthLimit: globLabelNameLengthLimit,
- LabelValueLengthLimit: globLabelValueLengthLimit,
- ScrapeProtocols: DefaultGlobalConfig.ScrapeProtocols,
- ScrapeFailureLogFile: globScrapeFailureLogFile,
+ HonorTimestamps: true,
+ ScrapeInterval: model.Duration(15 * time.Second),
+ ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
+ EnableCompression: true,
+ BodySizeLimit: globBodySizeLimit,
+ SampleLimit: globSampleLimit,
+ TargetLimit: globTargetLimit,
+ LabelLimit: globLabelLimit,
+ LabelNameLengthLimit: globLabelNameLengthLimit,
+ LabelValueLengthLimit: globLabelValueLengthLimit,
+ ScrapeProtocols: DefaultScrapeProtocols,
+ ScrapeFailureLogFile: globScrapeFailureLogFile,
+ MetricNameValidationScheme: DefaultGlobalConfig.MetricNameValidationScheme,
+ MetricNameEscapingScheme: DefaultGlobalConfig.MetricNameEscapingScheme,
+ ScrapeNativeHistograms: boolPtr(false),
+ AlwaysScrapeClassicHistograms: boolPtr(false),
+ ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
MetricsPath: DefaultScrapeConfig.MetricsPath,
Scheme: DefaultScrapeConfig.Scheme,
@@ -1078,18 +1235,24 @@ var expectedConf = &Config{
{
JobName: "service-openstack",
- HonorTimestamps: true,
- ScrapeInterval: model.Duration(15 * time.Second),
- ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
- EnableCompression: true,
- BodySizeLimit: globBodySizeLimit,
- SampleLimit: globSampleLimit,
- TargetLimit: globTargetLimit,
- LabelLimit: globLabelLimit,
- LabelNameLengthLimit: globLabelNameLengthLimit,
- LabelValueLengthLimit: globLabelValueLengthLimit,
- ScrapeProtocols: DefaultGlobalConfig.ScrapeProtocols,
- ScrapeFailureLogFile: globScrapeFailureLogFile,
+ HonorTimestamps: true,
+ ScrapeInterval: model.Duration(15 * time.Second),
+ ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
+ EnableCompression: true,
+ BodySizeLimit: globBodySizeLimit,
+ SampleLimit: globSampleLimit,
+ TargetLimit: globTargetLimit,
+ LabelLimit: globLabelLimit,
+ LabelNameLengthLimit: globLabelNameLengthLimit,
+ LabelValueLengthLimit: globLabelValueLengthLimit,
+ ScrapeProtocols: DefaultScrapeProtocols,
+ ScrapeFailureLogFile: globScrapeFailureLogFile,
+ MetricNameValidationScheme: DefaultGlobalConfig.MetricNameValidationScheme,
+ MetricNameEscapingScheme: DefaultGlobalConfig.MetricNameEscapingScheme,
+ ScrapeNativeHistograms: boolPtr(false),
+ AlwaysScrapeClassicHistograms: boolPtr(false),
+ ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
MetricsPath: DefaultScrapeConfig.MetricsPath,
Scheme: DefaultScrapeConfig.Scheme,
@@ -1113,18 +1276,24 @@ var expectedConf = &Config{
{
JobName: "service-puppetdb",
- HonorTimestamps: true,
- ScrapeInterval: model.Duration(15 * time.Second),
- ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
- EnableCompression: true,
- BodySizeLimit: globBodySizeLimit,
- SampleLimit: globSampleLimit,
- TargetLimit: globTargetLimit,
- LabelLimit: globLabelLimit,
- LabelNameLengthLimit: globLabelNameLengthLimit,
- LabelValueLengthLimit: globLabelValueLengthLimit,
- ScrapeProtocols: DefaultGlobalConfig.ScrapeProtocols,
- ScrapeFailureLogFile: globScrapeFailureLogFile,
+ HonorTimestamps: true,
+ ScrapeInterval: model.Duration(15 * time.Second),
+ ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
+ EnableCompression: true,
+ BodySizeLimit: globBodySizeLimit,
+ SampleLimit: globSampleLimit,
+ TargetLimit: globTargetLimit,
+ LabelLimit: globLabelLimit,
+ LabelNameLengthLimit: globLabelNameLengthLimit,
+ LabelValueLengthLimit: globLabelValueLengthLimit,
+ ScrapeProtocols: DefaultScrapeProtocols,
+ ScrapeFailureLogFile: globScrapeFailureLogFile,
+ MetricNameValidationScheme: DefaultGlobalConfig.MetricNameValidationScheme,
+ MetricNameEscapingScheme: DefaultGlobalConfig.MetricNameEscapingScheme,
+ ScrapeNativeHistograms: boolPtr(false),
+ AlwaysScrapeClassicHistograms: boolPtr(false),
+ ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
MetricsPath: DefaultScrapeConfig.MetricsPath,
Scheme: DefaultScrapeConfig.Scheme,
@@ -1150,19 +1319,25 @@ var expectedConf = &Config{
},
},
{
- JobName: "hetzner",
- HonorTimestamps: true,
- ScrapeInterval: model.Duration(15 * time.Second),
- ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
- EnableCompression: true,
- BodySizeLimit: globBodySizeLimit,
- SampleLimit: globSampleLimit,
- TargetLimit: globTargetLimit,
- LabelLimit: globLabelLimit,
- LabelNameLengthLimit: globLabelNameLengthLimit,
- LabelValueLengthLimit: globLabelValueLengthLimit,
- ScrapeProtocols: DefaultGlobalConfig.ScrapeProtocols,
- ScrapeFailureLogFile: globScrapeFailureLogFile,
+ JobName: "hetzner",
+ HonorTimestamps: true,
+ ScrapeInterval: model.Duration(15 * time.Second),
+ ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
+ EnableCompression: true,
+ BodySizeLimit: globBodySizeLimit,
+ SampleLimit: globSampleLimit,
+ TargetLimit: globTargetLimit,
+ LabelLimit: globLabelLimit,
+ LabelNameLengthLimit: globLabelNameLengthLimit,
+ LabelValueLengthLimit: globLabelValueLengthLimit,
+ ScrapeProtocols: DefaultProtoFirstScrapeProtocols,
+ ScrapeFailureLogFile: globScrapeFailureLogFile,
+ MetricNameValidationScheme: DefaultGlobalConfig.MetricNameValidationScheme,
+ MetricNameEscapingScheme: DefaultGlobalConfig.MetricNameEscapingScheme,
+ ScrapeNativeHistograms: boolPtr(true),
+ AlwaysScrapeClassicHistograms: boolPtr(false),
+ ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
MetricsPath: DefaultScrapeConfig.MetricsPath,
Scheme: DefaultScrapeConfig.Scheme,
@@ -1170,12 +1345,13 @@ var expectedConf = &Config{
RelabelConfigs: []*relabel.Config{
{
- Action: relabel.Uppercase,
- Regex: relabel.DefaultRelabelConfig.Regex,
- Replacement: relabel.DefaultRelabelConfig.Replacement,
- Separator: relabel.DefaultRelabelConfig.Separator,
- SourceLabels: model.LabelNames{"instance"},
- TargetLabel: "instance",
+ Action: relabel.Uppercase,
+ Regex: relabel.DefaultRelabelConfig.Regex,
+ Replacement: relabel.DefaultRelabelConfig.Replacement,
+ Separator: relabel.DefaultRelabelConfig.Separator,
+ SourceLabels: model.LabelNames{"instance"},
+ TargetLabel: "instance",
+ NameValidationScheme: model.UTF8Validation,
},
},
@@ -1208,18 +1384,24 @@ var expectedConf = &Config{
{
JobName: "service-eureka",
- HonorTimestamps: true,
- ScrapeInterval: model.Duration(15 * time.Second),
- ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
- EnableCompression: true,
- BodySizeLimit: globBodySizeLimit,
- SampleLimit: globSampleLimit,
- TargetLimit: globTargetLimit,
- LabelLimit: globLabelLimit,
- LabelNameLengthLimit: globLabelNameLengthLimit,
- LabelValueLengthLimit: globLabelValueLengthLimit,
- ScrapeProtocols: DefaultGlobalConfig.ScrapeProtocols,
- ScrapeFailureLogFile: globScrapeFailureLogFile,
+ HonorTimestamps: true,
+ ScrapeInterval: model.Duration(15 * time.Second),
+ ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
+ EnableCompression: true,
+ BodySizeLimit: globBodySizeLimit,
+ SampleLimit: globSampleLimit,
+ TargetLimit: globTargetLimit,
+ LabelLimit: globLabelLimit,
+ LabelNameLengthLimit: globLabelNameLengthLimit,
+ LabelValueLengthLimit: globLabelValueLengthLimit,
+ ScrapeProtocols: DefaultScrapeProtocols,
+ ScrapeFailureLogFile: globScrapeFailureLogFile,
+ MetricNameValidationScheme: DefaultGlobalConfig.MetricNameValidationScheme,
+ MetricNameEscapingScheme: DefaultGlobalConfig.MetricNameEscapingScheme,
+ ScrapeNativeHistograms: boolPtr(false),
+ AlwaysScrapeClassicHistograms: boolPtr(false),
+ ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
MetricsPath: DefaultScrapeConfig.MetricsPath,
Scheme: DefaultScrapeConfig.Scheme,
@@ -1236,18 +1418,24 @@ var expectedConf = &Config{
{
JobName: "ovhcloud",
- HonorTimestamps: true,
- ScrapeInterval: model.Duration(15 * time.Second),
- ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
- EnableCompression: true,
- BodySizeLimit: globBodySizeLimit,
- SampleLimit: globSampleLimit,
- TargetLimit: globTargetLimit,
- LabelLimit: globLabelLimit,
- LabelNameLengthLimit: globLabelNameLengthLimit,
- LabelValueLengthLimit: globLabelValueLengthLimit,
- ScrapeProtocols: DefaultGlobalConfig.ScrapeProtocols,
- ScrapeFailureLogFile: globScrapeFailureLogFile,
+ HonorTimestamps: true,
+ ScrapeInterval: model.Duration(15 * time.Second),
+ ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
+ EnableCompression: true,
+ BodySizeLimit: globBodySizeLimit,
+ SampleLimit: globSampleLimit,
+ TargetLimit: globTargetLimit,
+ LabelLimit: globLabelLimit,
+ LabelNameLengthLimit: globLabelNameLengthLimit,
+ LabelValueLengthLimit: globLabelValueLengthLimit,
+ ScrapeProtocols: DefaultScrapeProtocols,
+ ScrapeFailureLogFile: globScrapeFailureLogFile,
+ MetricNameValidationScheme: DefaultGlobalConfig.MetricNameValidationScheme,
+ MetricNameEscapingScheme: DefaultGlobalConfig.MetricNameEscapingScheme,
+ ScrapeNativeHistograms: boolPtr(false),
+ AlwaysScrapeClassicHistograms: boolPtr(false),
+ ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
HTTPClientConfig: config.DefaultHTTPClientConfig,
MetricsPath: DefaultScrapeConfig.MetricsPath,
@@ -1275,18 +1463,24 @@ var expectedConf = &Config{
{
JobName: "scaleway",
- HonorTimestamps: true,
- ScrapeInterval: model.Duration(15 * time.Second),
- ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
- EnableCompression: true,
- BodySizeLimit: globBodySizeLimit,
- SampleLimit: globSampleLimit,
- TargetLimit: globTargetLimit,
- LabelLimit: globLabelLimit,
- LabelNameLengthLimit: globLabelNameLengthLimit,
- LabelValueLengthLimit: globLabelValueLengthLimit,
- ScrapeProtocols: DefaultGlobalConfig.ScrapeProtocols,
- ScrapeFailureLogFile: globScrapeFailureLogFile,
+ HonorTimestamps: true,
+ ScrapeInterval: model.Duration(15 * time.Second),
+ ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
+ EnableCompression: true,
+ BodySizeLimit: globBodySizeLimit,
+ SampleLimit: globSampleLimit,
+ TargetLimit: globTargetLimit,
+ LabelLimit: globLabelLimit,
+ LabelNameLengthLimit: globLabelNameLengthLimit,
+ LabelValueLengthLimit: globLabelValueLengthLimit,
+ ScrapeProtocols: DefaultScrapeProtocols,
+ ScrapeFailureLogFile: globScrapeFailureLogFile,
+ MetricNameValidationScheme: DefaultGlobalConfig.MetricNameValidationScheme,
+ MetricNameEscapingScheme: DefaultGlobalConfig.MetricNameEscapingScheme,
+ ScrapeNativeHistograms: boolPtr(false),
+ AlwaysScrapeClassicHistograms: boolPtr(false),
+ ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
HTTPClientConfig: config.DefaultHTTPClientConfig,
MetricsPath: DefaultScrapeConfig.MetricsPath,
@@ -1320,18 +1514,24 @@ var expectedConf = &Config{
{
JobName: "linode-instances",
- HonorTimestamps: true,
- ScrapeInterval: model.Duration(15 * time.Second),
- ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
- EnableCompression: true,
- BodySizeLimit: globBodySizeLimit,
- SampleLimit: globSampleLimit,
- TargetLimit: globTargetLimit,
- LabelLimit: globLabelLimit,
- LabelNameLengthLimit: globLabelNameLengthLimit,
- LabelValueLengthLimit: globLabelValueLengthLimit,
- ScrapeProtocols: DefaultGlobalConfig.ScrapeProtocols,
- ScrapeFailureLogFile: globScrapeFailureLogFile,
+ HonorTimestamps: true,
+ ScrapeInterval: model.Duration(15 * time.Second),
+ ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
+ EnableCompression: true,
+ BodySizeLimit: globBodySizeLimit,
+ SampleLimit: globSampleLimit,
+ TargetLimit: globTargetLimit,
+ LabelLimit: globLabelLimit,
+ LabelNameLengthLimit: globLabelNameLengthLimit,
+ LabelValueLengthLimit: globLabelValueLengthLimit,
+ ScrapeProtocols: DefaultScrapeProtocols,
+ ScrapeFailureLogFile: globScrapeFailureLogFile,
+ MetricNameValidationScheme: DefaultGlobalConfig.MetricNameValidationScheme,
+ MetricNameEscapingScheme: DefaultGlobalConfig.MetricNameEscapingScheme,
+ ScrapeNativeHistograms: boolPtr(false),
+ AlwaysScrapeClassicHistograms: boolPtr(false),
+ ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
MetricsPath: DefaultScrapeConfig.MetricsPath,
Scheme: DefaultScrapeConfig.Scheme,
@@ -1353,21 +1553,70 @@ var expectedConf = &Config{
},
},
},
+ {
+ JobName: "stackit-servers",
+ HonorTimestamps: true,
+ ScrapeInterval: model.Duration(15 * time.Second),
+ ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
+ EnableCompression: true,
+ BodySizeLimit: globBodySizeLimit,
+ SampleLimit: globSampleLimit,
+ TargetLimit: globTargetLimit,
+ LabelLimit: globLabelLimit,
+ LabelNameLengthLimit: globLabelNameLengthLimit,
+ LabelValueLengthLimit: globLabelValueLengthLimit,
+ ScrapeProtocols: DefaultScrapeProtocols,
+ ScrapeFailureLogFile: globScrapeFailureLogFile,
+ MetricNameValidationScheme: DefaultGlobalConfig.MetricNameValidationScheme,
+ MetricNameEscapingScheme: DefaultGlobalConfig.MetricNameEscapingScheme,
+ ScrapeNativeHistograms: boolPtr(false),
+ AlwaysScrapeClassicHistograms: boolPtr(false),
+ ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
+
+ MetricsPath: DefaultScrapeConfig.MetricsPath,
+ Scheme: DefaultScrapeConfig.Scheme,
+ HTTPClientConfig: config.DefaultHTTPClientConfig,
+ ServiceDiscoveryConfigs: discovery.Configs{
+ &stackit.SDConfig{
+ Project: "11111111-1111-1111-1111-111111111111",
+ ServiceAccountKey: "mysecret_sa_key",
+ PrivateKey: "mysecret_private_key",
+ Region: "eu01",
+ HTTPClientConfig: config.HTTPClientConfig{
+ Authorization: &config.Authorization{
+ Type: "Bearer",
+ Credentials: "abcdef",
+ },
+ FollowRedirects: true,
+ EnableHTTP2: true,
+ },
+ Port: 80,
+ RefreshInterval: model.Duration(60 * time.Second),
+ },
+ },
+ },
{
JobName: "uyuni",
- HonorTimestamps: true,
- ScrapeInterval: model.Duration(15 * time.Second),
- ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
- EnableCompression: true,
- BodySizeLimit: globBodySizeLimit,
- SampleLimit: globSampleLimit,
- TargetLimit: globTargetLimit,
- LabelLimit: globLabelLimit,
- LabelNameLengthLimit: globLabelNameLengthLimit,
- LabelValueLengthLimit: globLabelValueLengthLimit,
- ScrapeProtocols: DefaultGlobalConfig.ScrapeProtocols,
- ScrapeFailureLogFile: globScrapeFailureLogFile,
+ HonorTimestamps: true,
+ ScrapeInterval: model.Duration(15 * time.Second),
+ ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
+ EnableCompression: true,
+ BodySizeLimit: globBodySizeLimit,
+ SampleLimit: globSampleLimit,
+ TargetLimit: globTargetLimit,
+ LabelLimit: globLabelLimit,
+ LabelNameLengthLimit: globLabelNameLengthLimit,
+ LabelValueLengthLimit: globLabelValueLengthLimit,
+ ScrapeProtocols: DefaultScrapeProtocols,
+ ScrapeFailureLogFile: globScrapeFailureLogFile,
+ MetricNameValidationScheme: DefaultGlobalConfig.MetricNameValidationScheme,
+ MetricNameEscapingScheme: DefaultGlobalConfig.MetricNameEscapingScheme,
+ ScrapeNativeHistograms: boolPtr(false),
+ AlwaysScrapeClassicHistograms: boolPtr(false),
+ ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
HTTPClientConfig: config.DefaultHTTPClientConfig,
MetricsPath: DefaultScrapeConfig.MetricsPath,
@@ -1385,19 +1634,26 @@ var expectedConf = &Config{
},
},
{
- JobName: "ionos",
- HonorTimestamps: true,
- ScrapeInterval: model.Duration(15 * time.Second),
- ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
- EnableCompression: true,
- BodySizeLimit: globBodySizeLimit,
- SampleLimit: globSampleLimit,
- TargetLimit: globTargetLimit,
- LabelLimit: globLabelLimit,
- LabelNameLengthLimit: globLabelNameLengthLimit,
- LabelValueLengthLimit: globLabelValueLengthLimit,
- ScrapeProtocols: DefaultGlobalConfig.ScrapeProtocols,
- ScrapeFailureLogFile: globScrapeFailureLogFile,
+ JobName: "ionos",
+
+ HonorTimestamps: true,
+ ScrapeInterval: model.Duration(15 * time.Second),
+ ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
+ EnableCompression: true,
+ BodySizeLimit: globBodySizeLimit,
+ SampleLimit: globSampleLimit,
+ TargetLimit: globTargetLimit,
+ LabelLimit: globLabelLimit,
+ LabelNameLengthLimit: globLabelNameLengthLimit,
+ LabelValueLengthLimit: globLabelValueLengthLimit,
+ ScrapeProtocols: DefaultScrapeProtocols,
+ ScrapeFailureLogFile: globScrapeFailureLogFile,
+ MetricNameValidationScheme: DefaultGlobalConfig.MetricNameValidationScheme,
+ MetricNameEscapingScheme: DefaultGlobalConfig.MetricNameEscapingScheme,
+ ScrapeNativeHistograms: boolPtr(false),
+ AlwaysScrapeClassicHistograms: boolPtr(false),
+ ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
MetricsPath: DefaultScrapeConfig.MetricsPath,
Scheme: DefaultScrapeConfig.Scheme,
@@ -1419,18 +1675,24 @@ var expectedConf = &Config{
{
JobName: "vultr",
- HonorTimestamps: true,
- ScrapeInterval: model.Duration(15 * time.Second),
- ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
- EnableCompression: true,
- BodySizeLimit: globBodySizeLimit,
- SampleLimit: globSampleLimit,
- TargetLimit: globTargetLimit,
- LabelLimit: globLabelLimit,
- LabelNameLengthLimit: globLabelNameLengthLimit,
- LabelValueLengthLimit: globLabelValueLengthLimit,
- ScrapeProtocols: DefaultGlobalConfig.ScrapeProtocols,
- ScrapeFailureLogFile: globScrapeFailureLogFile,
+ HonorTimestamps: true,
+ ScrapeInterval: model.Duration(15 * time.Second),
+ ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
+ EnableCompression: true,
+ BodySizeLimit: globBodySizeLimit,
+ SampleLimit: globSampleLimit,
+ TargetLimit: globTargetLimit,
+ LabelLimit: globLabelLimit,
+ LabelNameLengthLimit: globLabelNameLengthLimit,
+ LabelValueLengthLimit: globLabelValueLengthLimit,
+ ScrapeProtocols: DefaultScrapeProtocols,
+ ScrapeFailureLogFile: globScrapeFailureLogFile,
+ MetricNameValidationScheme: DefaultGlobalConfig.MetricNameValidationScheme,
+ MetricNameEscapingScheme: DefaultGlobalConfig.MetricNameEscapingScheme,
+ ScrapeNativeHistograms: boolPtr(false),
+ AlwaysScrapeClassicHistograms: boolPtr(false),
+ ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
MetricsPath: DefaultScrapeConfig.MetricsPath,
Scheme: DefaultScrapeConfig.Scheme,
@@ -1451,6 +1713,44 @@ var expectedConf = &Config{
},
},
},
+ {
+ JobName: "outscale",
+
+ HonorTimestamps: true,
+ ScrapeInterval: model.Duration(15 * time.Second),
+ ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
+ EnableCompression: true,
+ BodySizeLimit: globBodySizeLimit,
+ SampleLimit: globSampleLimit,
+ TargetLimit: globTargetLimit,
+ LabelLimit: globLabelLimit,
+ LabelNameLengthLimit: globLabelNameLengthLimit,
+ LabelValueLengthLimit: globLabelValueLengthLimit,
+ ScrapeProtocols: DefaultScrapeProtocols,
+ ScrapeFailureLogFile: globScrapeFailureLogFile,
+ MetricNameValidationScheme: DefaultGlobalConfig.MetricNameValidationScheme,
+ MetricNameEscapingScheme: DefaultGlobalConfig.MetricNameEscapingScheme,
+ ScrapeNativeHistograms: boolPtr(false),
+ AlwaysScrapeClassicHistograms: boolPtr(false),
+ ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
+
+ MetricsPath: DefaultScrapeConfig.MetricsPath,
+ Scheme: DefaultScrapeConfig.Scheme,
+ HTTPClientConfig: config.DefaultHTTPClientConfig,
+
+ ServiceDiscoveryConfigs: discovery.Configs{
+ &outscale.SDConfig{
+ Endpoint: "https://api.eu-west-2.outscale.com/api/v1",
+ Region: "eu-west-2",
+ AccessKey: "A1B2C3D4E5F6G7H8I9J0",
+ SecretKey: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
+ Port: 80,
+ RefreshInterval: model.Duration(60 * time.Second),
+ HTTPClientConfig: config.DefaultHTTPClientConfig,
+ },
+ },
+ },
},
AlertingConfig: AlertingConfig{
AlertmanagerConfigs: []*AlertmanagerConfig{
@@ -1476,8 +1776,14 @@ 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,
+ Percentage: 28,
+ },
},
},
TracingConfig: TracingConfig{
@@ -1495,36 +1801,41 @@ var expectedConf = &Config{
},
}
+func TestYAMLNotLongerSupportedAMApi(t *testing.T) {
+ _, err := LoadFile("testdata/config_with_no_longer_supported_am_api_config.yml", false, promslog.NewNopLogger())
+ require.Error(t, err)
+}
+
func TestYAMLRoundtrip(t *testing.T) {
- want, err := LoadFile("testdata/roundtrip.good.yml", false, false, log.NewNopLogger())
+ want, err := LoadFile("testdata/roundtrip.good.yml", false, promslog.NewNopLogger())
require.NoError(t, err)
out, err := yaml.Marshal(want)
+ require.NoError(t, err)
+ got, err := Load(string(out), promslog.NewNopLogger())
require.NoError(t, err)
- got := &Config{}
- require.NoError(t, yaml.UnmarshalStrict(out, got))
require.Equal(t, want, got)
}
func TestRemoteWriteRetryOnRateLimit(t *testing.T) {
- want, err := LoadFile("testdata/remote_write_retry_on_rate_limit.good.yml", false, false, log.NewNopLogger())
+ want, err := LoadFile("testdata/remote_write_retry_on_rate_limit.good.yml", false, promslog.NewNopLogger())
require.NoError(t, err)
out, err := yaml.Marshal(want)
+ require.NoError(t, err)
+ got, err := Load(string(out), promslog.NewNopLogger())
require.NoError(t, err)
- got := &Config{}
- require.NoError(t, yaml.UnmarshalStrict(out, got))
require.True(t, got.RemoteWriteConfigs[0].QueueConfig.RetryOnRateLimit)
require.False(t, got.RemoteWriteConfigs[1].QueueConfig.RetryOnRateLimit)
}
func TestOTLPSanitizeResourceAttributes(t *testing.T) {
- t.Run("good config", func(t *testing.T) {
- want, err := LoadFile(filepath.Join("testdata", "otlp_sanitize_resource_attributes.good.yml"), false, false, log.NewNopLogger())
+ t.Run("good config - default resource attributes", func(t *testing.T) {
+ want, err := LoadFile(filepath.Join("testdata", "otlp_sanitize_default_resource_attributes.good.yml"), false, promslog.NewNopLogger())
require.NoError(t, err)
out, err := yaml.Marshal(want)
@@ -1532,29 +1843,303 @@ func TestOTLPSanitizeResourceAttributes(t *testing.T) {
var got Config
require.NoError(t, yaml.UnmarshalStrict(out, &got))
+ require.False(t, got.OTLPConfig.PromoteAllResourceAttributes)
+ require.Empty(t, got.OTLPConfig.IgnoreResourceAttributes)
+ require.Empty(t, got.OTLPConfig.PromoteResourceAttributes)
+ })
+
+ t.Run("good config - promote resource attributes", func(t *testing.T) {
+ want, err := LoadFile(filepath.Join("testdata", "otlp_sanitize_promote_resource_attributes.good.yml"), false, promslog.NewNopLogger())
+ require.NoError(t, err)
+
+ out, err := yaml.Marshal(want)
+ require.NoError(t, err)
+ var got Config
+ require.NoError(t, yaml.UnmarshalStrict(out, &got))
+
+ require.False(t, got.OTLPConfig.PromoteAllResourceAttributes)
+ require.Empty(t, got.OTLPConfig.IgnoreResourceAttributes)
require.Equal(t, []string{"k8s.cluster.name", "k8s.job.name", "k8s.namespace.name"}, got.OTLPConfig.PromoteResourceAttributes)
})
- t.Run("bad config", func(t *testing.T) {
- _, err := LoadFile(filepath.Join("testdata", "otlp_sanitize_resource_attributes.bad.yml"), false, false, log.NewNopLogger())
+ t.Run("bad config - promote resource attributes", func(t *testing.T) {
+ _, err := LoadFile(filepath.Join("testdata", "otlp_sanitize_promote_resource_attributes.bad.yml"), false, promslog.NewNopLogger())
+ require.ErrorContains(t, err, `invalid 'promote_resource_attributes'`)
require.ErrorContains(t, err, `duplicated promoted OTel resource attribute "k8s.job.name"`)
require.ErrorContains(t, err, `empty promoted OTel resource attribute`)
})
+
+ t.Run("good config - promote all resource attributes", func(t *testing.T) {
+ want, err := LoadFile(filepath.Join("testdata", "otlp_sanitize_resource_attributes_promote_all.good.yml"), false, promslog.NewNopLogger())
+ require.NoError(t, err)
+
+ out, err := yaml.Marshal(want)
+ require.NoError(t, err)
+ var got Config
+ require.NoError(t, yaml.UnmarshalStrict(out, &got))
+ require.True(t, got.OTLPConfig.PromoteAllResourceAttributes)
+ require.Empty(t, got.OTLPConfig.PromoteResourceAttributes)
+ require.Empty(t, got.OTLPConfig.IgnoreResourceAttributes)
+ })
+
+ t.Run("good config - ignore resource attributes", func(t *testing.T) {
+ want, err := LoadFile(filepath.Join("testdata", "otlp_sanitize_ignore_resource_attributes.good.yml"), false, promslog.NewNopLogger())
+ require.NoError(t, err)
+
+ out, err := yaml.Marshal(want)
+ require.NoError(t, err)
+ var got Config
+ require.NoError(t, yaml.UnmarshalStrict(out, &got))
+ require.True(t, got.OTLPConfig.PromoteAllResourceAttributes)
+ require.Empty(t, got.OTLPConfig.PromoteResourceAttributes)
+ require.Equal(t, []string{"k8s.cluster.name", "k8s.job.name", "k8s.namespace.name"}, got.OTLPConfig.IgnoreResourceAttributes)
+ })
+
+ t.Run("bad config - ignore resource attributes", func(t *testing.T) {
+ _, err := LoadFile(filepath.Join("testdata", "otlp_sanitize_ignore_resource_attributes.bad.yml"), false, promslog.NewNopLogger())
+ require.ErrorContains(t, err, `invalid 'ignore_resource_attributes'`)
+ require.ErrorContains(t, err, `duplicated ignored OTel resource attribute "k8s.job.name"`)
+ require.ErrorContains(t, err, `empty ignored OTel resource attribute`)
+ })
+
+ t.Run("bad config - conflict between promote all and promote specific resource attributes", func(t *testing.T) {
+ _, err := LoadFile(filepath.Join("testdata", "otlp_promote_all_resource_attributes.bad.yml"), false, promslog.NewNopLogger())
+ require.ErrorContains(t, err, `'promote_all_resource_attributes' and 'promote_resource_attributes' cannot be configured simultaneously`)
+ })
+
+ t.Run("bad config - configuring ignoring of resource attributes without also enabling promotion of all resource attributes", func(t *testing.T) {
+ _, err := LoadFile(filepath.Join("testdata", "otlp_ignore_resource_attributes_without_promote_all.bad.yml"), false, promslog.NewNopLogger())
+ require.ErrorContains(t, err, `'ignore_resource_attributes' cannot be configured unless 'promote_all_resource_attributes' is true`)
+ })
+}
+
+func TestOTLPAllowServiceNameInTargetInfo(t *testing.T) {
+ t.Run("good config", func(t *testing.T) {
+ want, err := LoadFile(filepath.Join("testdata", "otlp_allow_keep_identifying_resource_attributes.good.yml"), false, promslog.NewNopLogger())
+ require.NoError(t, err)
+
+ out, err := yaml.Marshal(want)
+ require.NoError(t, err)
+ var got Config
+ require.NoError(t, yaml.UnmarshalStrict(out, &got))
+
+ require.True(t, got.OTLPConfig.KeepIdentifyingResourceAttributes)
+ })
+}
+
+func TestOTLPConvertHistogramsToNHCB(t *testing.T) {
+ t.Run("good config", func(t *testing.T) {
+ want, err := LoadFile(filepath.Join("testdata", "otlp_convert_histograms_to_nhcb.good.yml"), false, promslog.NewNopLogger())
+ require.NoError(t, err)
+
+ out, err := yaml.Marshal(want)
+ require.NoError(t, err)
+ var got Config
+ require.NoError(t, yaml.UnmarshalStrict(out, &got))
+
+ require.True(t, got.OTLPConfig.ConvertHistogramsToNHCB)
+ })
+}
+
+func TestOTLPPromoteScopeMetadata(t *testing.T) {
+ t.Run("good config", func(t *testing.T) {
+ want, err := LoadFile(filepath.Join("testdata", "otlp_promote_scope_metadata.good.yml"), false, promslog.NewNopLogger())
+ require.NoError(t, err)
+
+ out, err := yaml.Marshal(want)
+ require.NoError(t, err)
+ var got Config
+ require.NoError(t, yaml.UnmarshalStrict(out, &got))
+
+ require.True(t, got.OTLPConfig.PromoteScopeMetadata)
+ })
+}
+
+func TestOTLPLabelUnderscoreSanitization(t *testing.T) {
+ t.Run("defaults to true", func(t *testing.T) {
+ conf, err := LoadFile(filepath.Join("testdata", "otlp_label_underscore_sanitization_defaults.good.yml"), false, promslog.NewNopLogger())
+ require.NoError(t, err)
+
+ // Test that default values are true
+ require.True(t, conf.OTLPConfig.LabelNameUnderscoreSanitization)
+ require.True(t, conf.OTLPConfig.LabelNamePreserveMultipleUnderscores)
+ })
+
+ t.Run("explicitly enabled", func(t *testing.T) {
+ conf, err := LoadFile(filepath.Join("testdata", "otlp_label_underscore_sanitization_enabled.good.yml"), false, promslog.NewNopLogger())
+ require.NoError(t, err)
+
+ out, err := yaml.Marshal(conf)
+ require.NoError(t, err)
+ var got Config
+ require.NoError(t, yaml.UnmarshalStrict(out, &got))
+
+ require.True(t, got.OTLPConfig.LabelNameUnderscoreSanitization)
+ require.True(t, got.OTLPConfig.LabelNamePreserveMultipleUnderscores)
+ })
+
+ t.Run("explicitly disabled", func(t *testing.T) {
+ conf, err := LoadFile(filepath.Join("testdata", "otlp_label_underscore_sanitization_disabled.good.yml"), false, promslog.NewNopLogger())
+ require.NoError(t, err)
+
+ // When explicitly set to false, they should be false
+ require.False(t, conf.OTLPConfig.LabelNameUnderscoreSanitization)
+ require.False(t, conf.OTLPConfig.LabelNamePreserveMultipleUnderscores)
+ })
+
+ t.Run("empty config uses defaults", func(t *testing.T) {
+ conf, err := LoadFile(filepath.Join("testdata", "otlp_empty.yml"), false, promslog.NewNopLogger())
+ require.NoError(t, err)
+
+ // Empty config should use default values (true)
+ require.True(t, conf.OTLPConfig.LabelNameUnderscoreSanitization)
+ require.True(t, conf.OTLPConfig.LabelNamePreserveMultipleUnderscores)
+ })
+}
+
+func TestOTLPAllowUTF8(t *testing.T) {
+ t.Run("good config - NoUTF8EscapingWithSuffixes", func(t *testing.T) {
+ fpath := filepath.Join("testdata", "otlp_allow_utf8.good.yml")
+ verify := func(t *testing.T, conf *Config, err error) {
+ t.Helper()
+ require.NoError(t, err)
+ require.Equal(t, otlptranslator.NoUTF8EscapingWithSuffixes, conf.OTLPConfig.TranslationStrategy)
+ }
+
+ t.Run("LoadFile", func(t *testing.T) {
+ conf, err := LoadFile(fpath, false, promslog.NewNopLogger())
+ verify(t, conf, err)
+ })
+ t.Run("Load", func(t *testing.T) {
+ content, err := os.ReadFile(fpath)
+ require.NoError(t, err)
+ conf, err := Load(string(content), promslog.NewNopLogger())
+ verify(t, conf, err)
+ })
+ })
+
+ t.Run("incompatible config - NoUTF8EscapingWithSuffixes", func(t *testing.T) {
+ fpath := filepath.Join("testdata", "otlp_allow_utf8.incompatible.yml")
+ verify := func(t *testing.T, err error) {
+ t.Helper()
+ require.ErrorContains(t, err, `OTLP translation strategy "NoUTF8EscapingWithSuffixes" is not allowed when UTF8 is disabled`)
+ }
+
+ t.Run("LoadFile", func(t *testing.T) {
+ _, err := LoadFile(fpath, false, promslog.NewNopLogger())
+ verify(t, err)
+ })
+ t.Run("Load", func(t *testing.T) {
+ content, err := os.ReadFile(fpath)
+ require.NoError(t, err)
+ _, err = Load(string(content), promslog.NewNopLogger())
+ t.Log("err", err)
+ verify(t, err)
+ })
+ })
+
+ t.Run("good config - NoTranslation", func(t *testing.T) {
+ fpath := filepath.Join("testdata", "otlp_no_translation.good.yml")
+ verify := func(t *testing.T, conf *Config, err error) {
+ t.Helper()
+ require.NoError(t, err)
+ require.Equal(t, otlptranslator.NoTranslation, conf.OTLPConfig.TranslationStrategy)
+ }
+
+ t.Run("LoadFile", func(t *testing.T) {
+ conf, err := LoadFile(fpath, false, promslog.NewNopLogger())
+ verify(t, conf, err)
+ })
+ t.Run("Load", func(t *testing.T) {
+ content, err := os.ReadFile(fpath)
+ require.NoError(t, err)
+ conf, err := Load(string(content), promslog.NewNopLogger())
+ verify(t, conf, err)
+ })
+ })
+
+ t.Run("incompatible config - NoTranslation", func(t *testing.T) {
+ fpath := filepath.Join("testdata", "otlp_no_translation.incompatible.yml")
+ verify := func(t *testing.T, err error) {
+ t.Helper()
+ require.ErrorContains(t, err, `OTLP translation strategy "NoTranslation" is not allowed when UTF8 is disabled`)
+ }
+
+ t.Run("LoadFile", func(t *testing.T) {
+ _, err := LoadFile(fpath, false, promslog.NewNopLogger())
+ verify(t, err)
+ })
+ t.Run("Load", func(t *testing.T) {
+ content, err := os.ReadFile(fpath)
+ require.NoError(t, err)
+ _, err = Load(string(content), promslog.NewNopLogger())
+ t.Log("err", err)
+ verify(t, err)
+ })
+ })
+
+ t.Run("bad config", func(t *testing.T) {
+ fpath := filepath.Join("testdata", "otlp_allow_utf8.bad.yml")
+ verify := func(t *testing.T, err error) {
+ t.Helper()
+ require.ErrorContains(t, err, `unsupported OTLP translation strategy "Invalid"`)
+ }
+
+ t.Run("LoadFile", func(t *testing.T) {
+ _, err := LoadFile(fpath, false, promslog.NewNopLogger())
+ verify(t, err)
+ })
+ t.Run("Load", func(t *testing.T) {
+ content, err := os.ReadFile(fpath)
+ require.NoError(t, err)
+ _, err = Load(string(content), promslog.NewNopLogger())
+ verify(t, err)
+ })
+ })
+
+ t.Run("good config - missing otlp config uses default", func(t *testing.T) {
+ fpath := filepath.Join("testdata", "otlp_empty.yml")
+ verify := func(t *testing.T, conf *Config, err error) {
+ t.Helper()
+ require.NoError(t, err)
+ require.Equal(t, otlptranslator.UnderscoreEscapingWithSuffixes, conf.OTLPConfig.TranslationStrategy)
+ }
+
+ t.Run("LoadFile", func(t *testing.T) {
+ conf, err := LoadFile(fpath, false, promslog.NewNopLogger())
+ verify(t, conf, err)
+ })
+ t.Run("Load", func(t *testing.T) {
+ content, err := os.ReadFile(fpath)
+ require.NoError(t, err)
+ conf, err := Load(string(content), promslog.NewNopLogger())
+ verify(t, conf, err)
+ })
+ })
}
func TestLoadConfig(t *testing.T) {
// Parse a valid file that sets a global scrape timeout. This tests whether parsing
// an overwritten default field in the global config permanently changes the default.
- _, err := LoadFile("testdata/global_timeout.good.yml", false, false, log.NewNopLogger())
+ _, err := LoadFile("testdata/global_timeout.good.yml", false, promslog.NewNopLogger())
require.NoError(t, err)
- c, err := LoadFile("testdata/conf.good.yml", false, false, log.NewNopLogger())
+ c, err := LoadFile("testdata/conf.good.yml", false, promslog.NewNopLogger())
+
require.NoError(t, err)
- require.Equal(t, expectedConf, c)
+ testutil.RequireEqualWithOptions(t, expectedConf, c, []cmp.Option{
+ cmpopts.IgnoreUnexported(config.ProxyConfig{}),
+ cmpopts.IgnoreUnexported(ionos.SDConfig{}),
+ cmpopts.IgnoreUnexported(outscale.SDConfig{}),
+ cmpopts.IgnoreUnexported(stackit.SDConfig{}),
+ cmpopts.IgnoreUnexported(regexp.Regexp{}),
+ cmpopts.IgnoreUnexported(hetzner.SDConfig{}),
+ cmpopts.IgnoreUnexported(Config{}),
+ })
}
func TestScrapeIntervalLarger(t *testing.T) {
- c, err := LoadFile("testdata/scrape_interval_larger.good.yml", false, false, log.NewNopLogger())
+ c, err := LoadFile("testdata/scrape_interval_larger.good.yml", false, promslog.NewNopLogger())
require.NoError(t, err)
require.Len(t, c.ScrapeConfigs, 1)
for _, sc := range c.ScrapeConfigs {
@@ -1564,7 +2149,7 @@ func TestScrapeIntervalLarger(t *testing.T) {
// YAML marshaling must not reveal authentication credentials.
func TestElideSecrets(t *testing.T) {
- c, err := LoadFile("testdata/conf.good.yml", false, false, log.NewNopLogger())
+ c, err := LoadFile("testdata/conf.good.yml", false, promslog.NewNopLogger())
require.NoError(t, err)
secretRe := regexp.MustCompile(`\\u003csecret\\u003e|`)
@@ -1574,38 +2159,38 @@ func TestElideSecrets(t *testing.T) {
yamlConfig := string(config)
matches := secretRe.FindAllStringIndex(yamlConfig, -1)
- require.Len(t, matches, 24, "wrong number of secret matches found")
+ require.Len(t, matches, 28, "wrong number of secret matches found")
require.NotContains(t, yamlConfig, "mysecret",
"yaml marshal reveals authentication credentials.")
}
func TestLoadConfigRuleFilesAbsolutePath(t *testing.T) {
// Parse a valid file that sets a rule files with an absolute path
- c, err := LoadFile(ruleFilesConfigFile, false, false, log.NewNopLogger())
+ c, err := LoadFile(ruleFilesConfigFile, false, promslog.NewNopLogger())
require.NoError(t, err)
require.Equal(t, ruleFilesExpectedConf, c)
}
func TestKubernetesEmptyAPIServer(t *testing.T) {
- _, err := LoadFile("testdata/kubernetes_empty_apiserver.good.yml", false, false, log.NewNopLogger())
+ _, err := LoadFile("testdata/kubernetes_empty_apiserver.good.yml", false, promslog.NewNopLogger())
require.NoError(t, err)
}
func TestKubernetesWithKubeConfig(t *testing.T) {
- _, err := LoadFile("testdata/kubernetes_kubeconfig_without_apiserver.good.yml", false, false, log.NewNopLogger())
+ _, err := LoadFile("testdata/kubernetes_kubeconfig_without_apiserver.good.yml", false, promslog.NewNopLogger())
require.NoError(t, err)
}
func TestKubernetesSelectors(t *testing.T) {
- _, err := LoadFile("testdata/kubernetes_selectors_endpoints.good.yml", false, false, log.NewNopLogger())
+ _, err := LoadFile("testdata/kubernetes_selectors_endpoints.good.yml", false, promslog.NewNopLogger())
require.NoError(t, err)
- _, err = LoadFile("testdata/kubernetes_selectors_node.good.yml", false, false, log.NewNopLogger())
+ _, err = LoadFile("testdata/kubernetes_selectors_node.good.yml", false, promslog.NewNopLogger())
require.NoError(t, err)
- _, err = LoadFile("testdata/kubernetes_selectors_ingress.good.yml", false, false, log.NewNopLogger())
+ _, err = LoadFile("testdata/kubernetes_selectors_ingress.good.yml", false, promslog.NewNopLogger())
require.NoError(t, err)
- _, err = LoadFile("testdata/kubernetes_selectors_pod.good.yml", false, false, log.NewNopLogger())
+ _, err = LoadFile("testdata/kubernetes_selectors_pod.good.yml", false, promslog.NewNopLogger())
require.NoError(t, err)
- _, err = LoadFile("testdata/kubernetes_selectors_service.good.yml", false, false, log.NewNopLogger())
+ _, err = LoadFile("testdata/kubernetes_selectors_service.good.yml", false, promslog.NewNopLogger())
require.NoError(t, err)
}
@@ -1627,11 +2212,7 @@ var expectedErrors = []struct {
},
{
filename: "labelname.bad.yml",
- errMsg: `"not$allowed" is not a valid label name`,
- },
- {
- filename: "labelname2.bad.yml",
- errMsg: `"not:allowed" is not a valid label name`,
+ errMsg: `"\xff" is not a valid label name`,
},
{
filename: "labelvalue.bad.yml",
@@ -1703,16 +2284,12 @@ var expectedErrors = []struct {
},
{
filename: "labelmap.bad.yml",
- errMsg: "\"l-$1\" is invalid 'replacement' for labelmap action",
+ errMsg: "!!binary value contains invalid base64 data",
},
{
filename: "lowercase.bad.yml",
errMsg: "relabel configuration for lowercase action requires 'target_label' value",
},
- {
- filename: "lowercase2.bad.yml",
- errMsg: "\"42lab\" is invalid 'target_label' for lowercase action",
- },
{
filename: "lowercase3.bad.yml",
errMsg: "'replacement' can not be set for lowercase action",
@@ -1721,10 +2298,6 @@ var expectedErrors = []struct {
filename: "uppercase.bad.yml",
errMsg: "relabel configuration for uppercase action requires 'target_label' value",
},
- {
- filename: "uppercase2.bad.yml",
- errMsg: "\"42lab\" is invalid 'target_label' for uppercase action",
- },
{
filename: "uppercase3.bad.yml",
errMsg: "'replacement' can not be set for uppercase action",
@@ -1787,7 +2360,7 @@ var expectedErrors = []struct {
},
{
filename: "kubernetes_selectors_pod.bad.yml",
- errMsg: "pod role supports only pod selectors",
+ errMsg: "pod role supports only pod, node selectors",
},
{
filename: "kubernetes_selectors_service.bad.yml",
@@ -1875,7 +2448,7 @@ var expectedErrors = []struct {
},
{
filename: "remote_write_wrong_msg.bad.yml",
- errMsg: `invalid protobuf_message value: unknown remote write protobuf message io.prometheus.writet.v2.Request, supported: prometheus.WriteRequest, io.prometheus.write.v2.Request`,
+ errMsg: `invalid protobuf_message value: unknown type for remote write protobuf message io.prometheus.writet.v2.Request, supported: prometheus.WriteRequest, io.prometheus.write.v2.Request`,
},
{
filename: "remote_write_url_missing.bad.yml",
@@ -1885,6 +2458,30 @@ var expectedErrors = []struct {
filename: "remote_write_dup.bad.yml",
errMsg: `found multiple remote write configs with job name "queue1"`,
},
+ {
+ filename: "remote_write_queue_max_samples_per_send_zero.bad.yml",
+ errMsg: `remote write queue max_samples_per_send must be positive`,
+ },
+ {
+ filename: "remote_write_queue_max_shards_zero.bad.yml",
+ errMsg: `remote write queue max_shards must be positive`,
+ },
+ {
+ filename: "remote_write_queue_min_shards_zero.bad.yml",
+ errMsg: `remote write queue min_shards must be positive`,
+ },
+ {
+ filename: "remote_write_queue_capacity_zero.bad.yml",
+ errMsg: `remote write queue capacity must be positive`,
+ },
+ {
+ filename: "remote_write_queue_min_shards_greater_than_max.bad.yml",
+ errMsg: `remote write queue min_shards must not be greater than max_shards`,
+ },
+ {
+ filename: "remote_write_queue_max_backoff_less_than_min.bad.yml",
+ errMsg: `remote write queue max_backoff must not be less than min_backoff`,
+ },
{
filename: "remote_read_dup.bad.yml",
errMsg: `found multiple remote read configs with job name "queue1"`,
@@ -1923,7 +2520,7 @@ var expectedErrors = []struct {
},
{
filename: "azure_authentication_method.bad.yml",
- errMsg: "unknown authentication_type \"invalid\". Supported types are \"OAuth\", \"ManagedIdentity\" or \"SDK\"",
+ errMsg: "unknown authentication_type \"invalid\". Supported types are \"OAuth\", \"ManagedIdentity\", \"SDK\" or \"WorkloadIdentity\"",
},
{
filename: "azure_bearertoken_basicauth.bad.yml",
@@ -2009,6 +2606,18 @@ var expectedErrors = []struct {
filename: "scaleway_two_secrets.bad.yml",
errMsg: "at most one of secret_key & secret_key_file must be configured",
},
+ {
+ filename: "outscale_no_region.bad.yml",
+ errMsg: "outscale SD configuration requires a region",
+ },
+ {
+ filename: "outscale_no_secret.bad.yml",
+ errMsg: "one of secret_key & secret_key_file must be configured",
+ },
+ {
+ filename: "outscale_two_secrets.bad.yml",
+ errMsg: "at most one of secret_key & secret_key_file must be configured",
+ },
{
filename: "scrape_body_size_limit.bad.yml",
errMsg: "units: unknown unit in 100",
@@ -2075,29 +2684,91 @@ var expectedErrors = []struct {
},
{
filename: "scrape_config_files_scrape_protocols.bad.yml",
- errMsg: `parsing YAML file testdata/scrape_config_files_scrape_protocols.bad.yml: scrape_protocols: unknown scrape protocol prometheusproto, supported: [OpenMetricsText0.0.1 OpenMetricsText1.0.0 PrometheusProto PrometheusText0.0.4] for scrape config with job name "node"`,
+ errMsg: `parsing YAML file testdata/scrape_config_files_scrape_protocols.bad.yml: scrape_protocols: unknown scrape protocol prometheusproto, supported: [OpenMetricsText0.0.1 OpenMetricsText1.0.0 PrometheusProto PrometheusText0.0.4 PrometheusText1.0.0] for scrape config with job name "node"`,
},
{
filename: "scrape_config_files_scrape_protocols2.bad.yml",
errMsg: `parsing YAML file testdata/scrape_config_files_scrape_protocols2.bad.yml: duplicated protocol in scrape_protocols, got [OpenMetricsText1.0.0 PrometheusProto OpenMetricsText1.0.0] for scrape config with job name "node"`,
},
+ {
+ filename: "scrape_config_files_fallback_scrape_protocol1.bad.yml",
+ errMsg: `parsing YAML file testdata/scrape_config_files_fallback_scrape_protocol1.bad.yml: invalid fallback_scrape_protocol for scrape config with job name "node": unknown scrape protocol prometheusproto, supported: [OpenMetricsText0.0.1 OpenMetricsText1.0.0 PrometheusProto PrometheusText0.0.4 PrometheusText1.0.0]`,
+ },
+ {
+ filename: "scrape_config_files_fallback_scrape_protocol2.bad.yml",
+ errMsg: `unmarshal errors`,
+ },
+ {
+ filename: "scrape_config_utf8_conflicting.bad.yml",
+ errMsg: `utf8 metric names requested but validation scheme is not set to UTF8`,
+ },
+ {
+ filename: "stackit_endpoint.bad.yml",
+ errMsg: "invalid endpoint",
+ },
+ {
+ filename: "tsdb_retention_time.bad.yml",
+ errMsg: `not a valid duration string: "-1h"`,
+ },
+ {
+ filename: "tsdb_retention_size.bad.yml",
+ errMsg: `'storage.tsdb.retention.size' must be greater than or equal to 0`,
+ },
+ {
+ filename: "tsdb_retention_percentage.bad.yml",
+ errMsg: `'storage.tsdb.retention.percentage' must be in the range [0, 100]`,
+ },
+ {
+ filename: "tsdb_retention_percentage_negative.bad.yml",
+ errMsg: "'storage.tsdb.retention.percentage' must be in the range [0, 100]",
+ },
+ {
+ filename: "tsdb_chunk_encoding_floats.bad.yml",
+ errMsg: `'storage.tsdb.chunk_encoding.floats' must be 'xor' or 'xor2', or the field must be omitted entirely, got "xor3"`,
+ },
+ {
+ filename: "tsdb_chunk_encoding_floats_wrong_case.bad.yml",
+ errMsg: `'storage.tsdb.chunk_encoding.floats' must be 'xor' or 'xor2', or the field must be omitted entirely, got "XOR"`,
+ },
+ {
+ filename: "tsdb_chunk_encoding_floats_wrong_case_xor2.bad.yml",
+ errMsg: `'storage.tsdb.chunk_encoding.floats' must be 'xor' or 'xor2', or the field must be omitted entirely, got "XOR2"`,
+ },
}
func TestBadConfigs(t *testing.T) {
for _, ee := range expectedErrors {
- _, err := LoadFile("testdata/"+ee.filename, false, false, log.NewNopLogger())
- require.Error(t, err, "%s", ee.filename)
- require.Contains(t, err.Error(), ee.errMsg,
+ _, err := LoadFile("testdata/"+ee.filename, false, promslog.NewNopLogger())
+ require.ErrorContains(t, err, ee.errMsg,
"Expected error for %s to contain %q but got: %s", ee.filename, ee.errMsg, err)
}
}
-func TestBadStaticConfigsJSON(t *testing.T) {
- content, err := os.ReadFile("testdata/static_config.bad.json")
+func TestTSDBRetentionPercentageFloat(t *testing.T) {
+ c, err := LoadFile("testdata/tsdb_retention_percentage_float.good.yml", false, promslog.NewNopLogger())
require.NoError(t, err)
- var tg targetgroup.Group
- err = json.Unmarshal(content, &tg)
- require.Error(t, err)
+ require.Equal(t, 0.5, c.StorageConfig.TSDBConfig.Retention.Percentage)
+}
+
+func TestTSDBChunkEncoding(t *testing.T) {
+ for _, tc := range []struct {
+ filename string
+ encoding string
+ }{
+ {filename: "tsdb_chunk_encoding_floats_xor.good.yml", encoding: FloatChunkEncodingXOR},
+ {filename: "tsdb_chunk_encoding_floats_xor2.good.yml", encoding: FloatChunkEncodingXOR2},
+ } {
+ t.Run(tc.encoding, func(t *testing.T) {
+ c, err := LoadFile("testdata/"+tc.filename, false, promslog.NewNopLogger())
+ require.NoError(t, err)
+ require.Equal(t, tc.encoding, c.StorageConfig.TSDBConfig.ChunkEncoding.Floats)
+ })
+ }
+
+ // Empty/absent value is also valid.
+ c, err := Load("", promslog.NewNopLogger())
+ require.NoError(t, err)
+ require.Empty(t, c.StorageConfig.TSDBConfig.ChunkEncoding.Floats)
}
func TestBadStaticConfigsYML(t *testing.T) {
@@ -2109,48 +2780,48 @@ func TestBadStaticConfigsYML(t *testing.T) {
}
func TestEmptyConfig(t *testing.T) {
- c, err := Load("", false, log.NewNopLogger())
+ c, err := Load("", promslog.NewNopLogger())
require.NoError(t, err)
exp := DefaultConfig
+ exp.loaded = true
+ retention := DefaultTSDBRetentionConfig
+ exp.StorageConfig.TSDBConfig = &TSDBConfig{Retention: &retention}
require.Equal(t, exp, *c)
+ require.Equal(t, 75, c.Runtime.GoGC)
}
func TestExpandExternalLabels(t *testing.T) {
// Cleanup ant TEST env variable that could exist on the system.
os.Setenv("TEST", "")
- c, err := LoadFile("testdata/external_labels.good.yml", false, false, log.NewNopLogger())
- require.NoError(t, err)
- testutil.RequireEqual(t, labels.FromStrings("bar", "foo", "baz", "foo${TEST}bar", "foo", "${TEST}", "qux", "foo$${TEST}", "xyz", "foo$$bar"), c.GlobalConfig.ExternalLabels)
-
- c, err = LoadFile("testdata/external_labels.good.yml", false, true, log.NewNopLogger())
+ c, err := LoadFile("testdata/external_labels.good.yml", false, promslog.NewNopLogger())
require.NoError(t, err)
testutil.RequireEqual(t, labels.FromStrings("bar", "foo", "baz", "foobar", "foo", "", "qux", "foo${TEST}", "xyz", "foo$bar"), c.GlobalConfig.ExternalLabels)
os.Setenv("TEST", "TestValue")
- c, err = LoadFile("testdata/external_labels.good.yml", false, true, log.NewNopLogger())
+ c, err = LoadFile("testdata/external_labels.good.yml", false, promslog.NewNopLogger())
require.NoError(t, err)
testutil.RequireEqual(t, labels.FromStrings("bar", "foo", "baz", "fooTestValuebar", "foo", "TestValue", "qux", "foo${TEST}", "xyz", "foo$bar"), c.GlobalConfig.ExternalLabels)
}
func TestAgentMode(t *testing.T) {
- _, err := LoadFile("testdata/agent_mode.with_alert_manager.yml", true, false, log.NewNopLogger())
+ _, err := LoadFile("testdata/agent_mode.with_alert_manager.yml", true, promslog.NewNopLogger())
require.ErrorContains(t, err, "field alerting is not allowed in agent mode")
- _, err = LoadFile("testdata/agent_mode.with_alert_relabels.yml", true, false, log.NewNopLogger())
+ _, err = LoadFile("testdata/agent_mode.with_alert_relabels.yml", true, promslog.NewNopLogger())
require.ErrorContains(t, err, "field alerting is not allowed in agent mode")
- _, err = LoadFile("testdata/agent_mode.with_rule_files.yml", true, false, log.NewNopLogger())
+ _, err = LoadFile("testdata/agent_mode.with_rule_files.yml", true, promslog.NewNopLogger())
require.ErrorContains(t, err, "field rule_files is not allowed in agent mode")
- _, err = LoadFile("testdata/agent_mode.with_remote_reads.yml", true, false, log.NewNopLogger())
+ _, err = LoadFile("testdata/agent_mode.with_remote_reads.yml", true, promslog.NewNopLogger())
require.ErrorContains(t, err, "field remote_read is not allowed in agent mode")
- c, err := LoadFile("testdata/agent_mode.without_remote_writes.yml", true, false, log.NewNopLogger())
+ c, err := LoadFile("testdata/agent_mode.without_remote_writes.yml", true, promslog.NewNopLogger())
require.NoError(t, err)
require.Empty(t, c.RemoteWriteConfigs)
- c, err = LoadFile("testdata/agent_mode.good.yml", true, false, log.NewNopLogger())
+ c, err = LoadFile("testdata/agent_mode.good.yml", true, promslog.NewNopLogger())
require.NoError(t, err)
require.Len(t, c.RemoteWriteConfigs, 1)
require.Equal(
@@ -2160,22 +2831,116 @@ func TestAgentMode(t *testing.T) {
)
}
-func TestEmptyGlobalBlock(t *testing.T) {
- c, err := Load("global:\n", false, log.NewNopLogger())
- require.NoError(t, err)
- exp := DefaultConfig
- exp.Runtime = DefaultRuntimeConfig
- 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
+ // TSDBConfig is always injected by Config.UnmarshalYAML even when no
+ // storage.tsdb section is present, so the expected config must include it.
+ retention := DefaultTSDBRetentionConfig
+ exp.StorageConfig.TSDBConfig = &TSDBConfig{Retention: &retention}
+ 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.
+type ScrapeConfigOptions struct {
+ JobName string
+ ScrapeInterval model.Duration
+ ScrapeTimeout model.Duration
+ ScrapeProtocols []ScrapeProtocol // Set to DefaultScrapeProtocols by default.
+ ScrapeNativeHistograms bool
+ AlwaysScrapeClassicHistograms bool
+ ConvertClassicHistToNHCB bool
+ ExtraScrapeMetrics bool
}
func TestGetScrapeConfigs(t *testing.T) {
- sc := func(jobName string, scrapeInterval, scrapeTimeout model.Duration) *ScrapeConfig {
- return &ScrapeConfig{
- JobName: jobName,
- HonorTimestamps: true,
- ScrapeInterval: scrapeInterval,
- ScrapeTimeout: scrapeTimeout,
- ScrapeProtocols: DefaultGlobalConfig.ScrapeProtocols,
+ // Helper function to create a scrape config with the given options.
+ sc := func(opts ScrapeConfigOptions) *ScrapeConfig {
+ sc := ScrapeConfig{
+ JobName: opts.JobName,
+ HonorTimestamps: true,
+ ScrapeInterval: opts.ScrapeInterval,
+ ScrapeTimeout: opts.ScrapeTimeout,
+ ScrapeProtocols: opts.ScrapeProtocols,
+ MetricNameValidationScheme: model.UTF8Validation,
+ MetricNameEscapingScheme: model.AllowUTF8,
MetricsPath: "/metrics",
Scheme: "http",
@@ -2193,7 +2958,15 @@ 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
}
+ return &sc
}
testCases := []struct {
@@ -2203,35 +2976,76 @@ func TestGetScrapeConfigs(t *testing.T) {
expectedError string
}{
{
- name: "An included config file should be a valid global config.",
- configFile: "testdata/scrape_config_files.good.yml",
- expectedResult: []*ScrapeConfig{sc("prometheus", model.Duration(60*time.Second), model.Duration(10*time.Second))},
+ name: "An included config file should be a valid global config.",
+ configFile: "testdata/scrape_config_files.good.yml",
+ expectedResult: []*ScrapeConfig{sc(ScrapeConfigOptions{
+ JobName: "prometheus",
+ ScrapeInterval: model.Duration(60 * time.Second),
+ ScrapeTimeout: model.Duration(10 * time.Second),
+ ScrapeNativeHistograms: false,
+ AlwaysScrapeClassicHistograms: false,
+ ConvertClassicHistToNHCB: false,
+ })},
},
{
- name: "An global config that only include a scrape config file.",
- configFile: "testdata/scrape_config_files_only.good.yml",
- expectedResult: []*ScrapeConfig{sc("prometheus", model.Duration(60*time.Second), model.Duration(10*time.Second))},
+ name: "A global config that only include a scrape config file.",
+ configFile: "testdata/scrape_config_files_only.good.yml",
+ expectedResult: []*ScrapeConfig{sc(ScrapeConfigOptions{
+ JobName: "prometheus",
+ ScrapeInterval: model.Duration(60 * time.Second),
+ ScrapeTimeout: model.Duration(10 * time.Second),
+ ScrapeNativeHistograms: false,
+ AlwaysScrapeClassicHistograms: false,
+ ConvertClassicHistToNHCB: false,
+ })},
},
{
- name: "An global config that combine scrape config files and scrape configs.",
+ name: "A global config that combine scrape config files and scrape configs.",
configFile: "testdata/scrape_config_files_combined.good.yml",
expectedResult: []*ScrapeConfig{
- sc("node", model.Duration(60*time.Second), model.Duration(10*time.Second)),
- sc("prometheus", model.Duration(60*time.Second), model.Duration(10*time.Second)),
- sc("alertmanager", model.Duration(60*time.Second), model.Duration(10*time.Second)),
+ sc(ScrapeConfigOptions{
+ JobName: "node",
+ ScrapeInterval: model.Duration(60 * time.Second),
+ ScrapeTimeout: model.Duration(10 * time.Second),
+ ScrapeNativeHistograms: false,
+ AlwaysScrapeClassicHistograms: false,
+ ConvertClassicHistToNHCB: false,
+ }),
+ sc(ScrapeConfigOptions{
+ JobName: "prometheus",
+ ScrapeInterval: model.Duration(60 * time.Second),
+ ScrapeTimeout: model.Duration(10 * time.Second),
+ ScrapeNativeHistograms: false,
+ AlwaysScrapeClassicHistograms: false,
+ ConvertClassicHistToNHCB: false,
+ }),
+ sc(ScrapeConfigOptions{
+ JobName: "alertmanager",
+ ScrapeInterval: model.Duration(60 * time.Second),
+ ScrapeTimeout: model.Duration(10 * time.Second),
+ ScrapeNativeHistograms: false,
+ AlwaysScrapeClassicHistograms: false,
+ ConvertClassicHistToNHCB: false,
+ }),
},
},
{
- name: "An global config that includes a scrape config file with globs",
+ name: "A global config that includes a scrape config file with globs",
configFile: "testdata/scrape_config_files_glob.good.yml",
expectedResult: []*ScrapeConfig{
{
JobName: "prometheus",
- HonorTimestamps: true,
- ScrapeInterval: model.Duration(60 * time.Second),
- ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
- ScrapeProtocols: DefaultGlobalConfig.ScrapeProtocols,
+ HonorTimestamps: true,
+ ScrapeInterval: model.Duration(60 * time.Second),
+ ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
+ ScrapeProtocols: DefaultScrapeProtocols,
+ MetricNameValidationScheme: model.UTF8Validation,
+ MetricNameEscapingScheme: model.AllowUTF8,
+ ScrapeNativeHistograms: boolPtr(false),
+ AlwaysScrapeClassicHistograms: boolPtr(false),
+ ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
MetricsPath: DefaultScrapeConfig.MetricsPath,
Scheme: DefaultScrapeConfig.Scheme,
@@ -2261,10 +3075,16 @@ func TestGetScrapeConfigs(t *testing.T) {
{
JobName: "node",
- HonorTimestamps: true,
- ScrapeInterval: model.Duration(15 * time.Second),
- ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
- ScrapeProtocols: DefaultGlobalConfig.ScrapeProtocols,
+ HonorTimestamps: true,
+ ScrapeInterval: model.Duration(15 * time.Second),
+ ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
+ ScrapeProtocols: DefaultScrapeProtocols,
+ MetricNameValidationScheme: model.UTF8Validation,
+ MetricNameEscapingScheme: model.AllowUTF8,
+ ScrapeNativeHistograms: boolPtr(false),
+ AlwaysScrapeClassicHistograms: boolPtr(false),
+ ConvertClassicHistogramsToNHCB: boolPtr(false),
+ ExtraScrapeMetrics: boolPtr(false),
HTTPClientConfig: config.HTTPClientConfig{
TLSConfig: config.TLSConfig{
@@ -2298,29 +3118,114 @@ func TestGetScrapeConfigs(t *testing.T) {
},
},
{
- name: "An global config that includes twice the same scrape configs.",
+ name: "A global config that includes twice the same scrape configs.",
configFile: "testdata/scrape_config_files_double_import.bad.yml",
expectedError: `found multiple scrape configs with job name "prometheus"`,
},
{
- name: "An global config that includes a scrape config identical to a scrape config in the main file.",
+ name: "A global config that includes a scrape config identical to a scrape config in the main file.",
configFile: "testdata/scrape_config_files_duplicate.bad.yml",
expectedError: `found multiple scrape configs with job name "prometheus"`,
},
{
- name: "An global config that includes a scrape config file with errors.",
+ name: "A global config that includes a scrape config file with errors.",
configFile: "testdata/scrape_config_files_global.bad.yml",
expectedError: `scrape timeout greater than scrape interval for scrape config with job name "prometheus"`,
},
+ {
+ name: "A global config that enables convert classic histograms to nhcb.",
+ configFile: "testdata/global_convert_classic_hist_to_nhcb.good.yml",
+ expectedResult: []*ScrapeConfig{sc(ScrapeConfigOptions{JobName: "prometheus", ScrapeInterval: model.Duration(60 * time.Second), ScrapeTimeout: model.Duration(10 * time.Second), AlwaysScrapeClassicHistograms: false, ConvertClassicHistToNHCB: true})},
+ },
+ {
+ name: "A global config that enables convert classic histograms to nhcb and scrape config that disables the conversion",
+ configFile: "testdata/local_disable_convert_classic_hist_to_nhcb.good.yml",
+ expectedResult: []*ScrapeConfig{sc(ScrapeConfigOptions{JobName: "prometheus", ScrapeInterval: model.Duration(60 * time.Second), ScrapeTimeout: model.Duration(10 * time.Second), AlwaysScrapeClassicHistograms: false, ConvertClassicHistToNHCB: false})},
+ },
+ {
+ name: "A global config that disables convert classic histograms to nhcb and scrape config that enables the conversion",
+ configFile: "testdata/local_convert_classic_hist_to_nhcb.good.yml",
+ expectedResult: []*ScrapeConfig{sc(ScrapeConfigOptions{JobName: "prometheus", ScrapeInterval: model.Duration(60 * time.Second), ScrapeTimeout: model.Duration(10 * time.Second), AlwaysScrapeClassicHistograms: false, ConvertClassicHistToNHCB: true})},
+ },
+ {
+ name: "A global config that enables always scrape classic histograms",
+ configFile: "testdata/global_enable_always_scrape_classic_hist.good.yml",
+ expectedResult: []*ScrapeConfig{sc(ScrapeConfigOptions{JobName: "prometheus", ScrapeInterval: model.Duration(60 * time.Second), ScrapeTimeout: model.Duration(10 * time.Second), AlwaysScrapeClassicHistograms: true, ConvertClassicHistToNHCB: false})},
+ },
+ {
+ name: "A global config that disables always scrape classic histograms",
+ configFile: "testdata/global_disable_always_scrape_classic_hist.good.yml",
+ expectedResult: []*ScrapeConfig{sc(ScrapeConfigOptions{JobName: "prometheus", ScrapeInterval: model.Duration(60 * time.Second), ScrapeTimeout: model.Duration(10 * time.Second), AlwaysScrapeClassicHistograms: false, ConvertClassicHistToNHCB: false})},
+ },
+ {
+ name: "A global config that disables always scrape classic histograms and scrape config that enables it",
+ configFile: "testdata/local_enable_always_scrape_classic_hist.good.yml",
+ expectedResult: []*ScrapeConfig{sc(ScrapeConfigOptions{JobName: "prometheus", ScrapeInterval: model.Duration(60 * time.Second), ScrapeTimeout: model.Duration(10 * time.Second), AlwaysScrapeClassicHistograms: true, ConvertClassicHistToNHCB: false})},
+ },
+ {
+ name: "A global config that enables always scrape classic histograms and scrape config that disables it",
+ configFile: "testdata/local_disable_always_scrape_classic_hist.good.yml",
+ expectedResult: []*ScrapeConfig{sc(ScrapeConfigOptions{JobName: "prometheus", ScrapeInterval: model.Duration(60 * time.Second), ScrapeTimeout: model.Duration(10 * time.Second), AlwaysScrapeClassicHistograms: false, ConvertClassicHistToNHCB: false})},
+ },
+ {
+ name: "A global config that enables scrape native histograms",
+ configFile: "testdata/global_enable_scrape_native_hist.good.yml",
+ expectedResult: []*ScrapeConfig{sc(ScrapeConfigOptions{JobName: "prometheus", ScrapeInterval: model.Duration(60 * time.Second), ScrapeTimeout: model.Duration(10 * time.Second), ScrapeNativeHistograms: true, ScrapeProtocols: DefaultProtoFirstScrapeProtocols})},
+ },
+ {
+ name: "A global config that enables scrape native histograms and sets scrape protocols explicitly",
+ configFile: "testdata/global_enable_scrape_native_hist_and_scrape_protocols.good.yml",
+ expectedResult: []*ScrapeConfig{sc(ScrapeConfigOptions{JobName: "prometheus", ScrapeInterval: model.Duration(60 * time.Second), ScrapeTimeout: model.Duration(10 * time.Second), ScrapeNativeHistograms: true, ScrapeProtocols: []ScrapeProtocol{PrometheusText0_0_4}})},
+ },
+ {
+ name: "A local config that enables scrape native histograms",
+ configFile: "testdata/local_enable_scrape_native_hist.good.yml",
+ expectedResult: []*ScrapeConfig{sc(ScrapeConfigOptions{JobName: "prometheus", ScrapeInterval: model.Duration(60 * time.Second), ScrapeTimeout: model.Duration(10 * time.Second), ScrapeNativeHistograms: true, ScrapeProtocols: DefaultProtoFirstScrapeProtocols})},
+ },
+ {
+ name: "A local config that enables scrape native histograms and sets scrape protocols explicitly",
+ configFile: "testdata/local_enable_scrape_native_hist_and_scrape_protocols.good.yml",
+ expectedResult: []*ScrapeConfig{sc(ScrapeConfigOptions{JobName: "prometheus", ScrapeInterval: model.Duration(60 * time.Second), ScrapeTimeout: model.Duration(10 * time.Second), ScrapeNativeHistograms: true, ScrapeProtocols: []ScrapeProtocol{PrometheusText0_0_4}})},
+ },
+ {
+ name: "A global config that enables scrape native histograms and scrape config that disables it",
+ configFile: "testdata/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: DefaultScrapeProtocols})},
+ },
+ {
+ name: "A global config that enables scrape native histograms and scrape protocols and scrape config that disables scrape native histograms but does not change scrape protocols",
+ 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 {
t.Run(tc.name, func(t *testing.T) {
- c, err := LoadFile(tc.configFile, false, false, log.NewNopLogger())
+ c, err := LoadFile(tc.configFile, false, promslog.NewNopLogger())
require.NoError(t, err)
scfgs, err := c.GetScrapeConfigs()
- if len(tc.expectedError) > 0 {
+ if tc.expectedError != "" {
require.ErrorContains(t, err, tc.expectedError)
}
require.Equal(t, tc.expectedResult, scfgs)
@@ -2328,13 +3233,106 @@ 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}
}
func TestScrapeConfigDisableCompression(t *testing.T) {
- want, err := LoadFile("testdata/scrape_config_disable_compression.good.yml", false, false, log.NewNopLogger())
+ want, err := LoadFile("testdata/scrape_config_disable_compression.good.yml", false, promslog.NewNopLogger())
require.NoError(t, err)
out, err := yaml.Marshal(want)
@@ -2347,41 +3345,47 @@ func TestScrapeConfigDisableCompression(t *testing.T) {
}
func TestScrapeConfigNameValidationSettings(t *testing.T) {
- model.NameValidationScheme = model.UTF8Validation
- defer func() {
- model.NameValidationScheme = model.LegacyValidation
- }()
-
tests := []struct {
- name string
- inputFile string
- expectScheme string
+ name string
+ inputFile string
+ expectScheme model.ValidationScheme
+ expectEscaping model.EscapingScheme
}{
{
- name: "blank config implies default",
- inputFile: "scrape_config_default_validation_mode",
- expectScheme: "",
+ name: "blank config implies default",
+ inputFile: "scrape_config_default_validation_mode",
+ expectScheme: model.UTF8Validation,
+ expectEscaping: model.NoEscaping,
+ },
+ {
+ name: "global setting implies local settings",
+ inputFile: "scrape_config_global_validation_mode",
+ expectScheme: model.LegacyValidation,
+ expectEscaping: model.DotsEscaping,
},
{
- name: "global setting implies local settings",
- inputFile: "scrape_config_global_validation_mode",
- expectScheme: "utf8",
+ name: "local setting",
+ inputFile: "scrape_config_local_validation_mode",
+ expectScheme: model.LegacyValidation,
+ expectEscaping: model.ValueEncodingEscaping,
},
{
- name: "local setting",
- inputFile: "scrape_config_local_validation_mode",
- expectScheme: "utf8",
+ name: "local setting overrides global setting",
+ inputFile: "scrape_config_local_global_validation_mode",
+ expectScheme: model.UTF8Validation,
+ expectEscaping: model.DotsEscaping,
},
{
- name: "local setting overrides global setting",
- inputFile: "scrape_config_local_global_validation_mode",
- expectScheme: "legacy",
+ name: "local validation implies underscores escaping",
+ inputFile: "scrape_config_local_infer_escaping",
+ expectScheme: model.LegacyValidation,
+ expectEscaping: model.UnderscoreEscaping,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
- want, err := LoadFile(fmt.Sprintf("testdata/%s.yml", tc.inputFile), false, false, log.NewNopLogger())
+ want, err := LoadFile(fmt.Sprintf("testdata/%s.yml", tc.inputFile), false, promslog.NewNopLogger())
require.NoError(t, err)
out, err := yaml.Marshal(want)
@@ -2391,6 +3395,126 @@ func TestScrapeConfigNameValidationSettings(t *testing.T) {
require.NoError(t, yaml.UnmarshalStrict(out, got))
require.Equal(t, tc.expectScheme, got.ScrapeConfigs[0].MetricNameValidationScheme)
+
+ escaping, err := model.ToEscapingScheme(got.ScrapeConfigs[0].MetricNameEscapingScheme)
+ require.NoError(t, err)
+ require.Equal(t, tc.expectEscaping, escaping)
})
}
}
+
+func TestScrapeConfigNameEscapingSettings(t *testing.T) {
+ tests := []struct {
+ name string
+ inputFile string
+ expectValidationScheme model.ValidationScheme
+ expectEscapingScheme string
+ }{
+ {
+ name: "blank config implies default",
+ inputFile: "scrape_config_default_validation_mode",
+ expectValidationScheme: model.UTF8Validation,
+ expectEscapingScheme: "allow-utf-8",
+ },
+ {
+ name: "global setting implies local settings",
+ inputFile: "scrape_config_global_validation_mode",
+ expectValidationScheme: model.LegacyValidation,
+ expectEscapingScheme: "dots",
+ },
+ {
+ name: "local setting",
+ inputFile: "scrape_config_local_validation_mode",
+ expectValidationScheme: model.LegacyValidation,
+ expectEscapingScheme: "values",
+ },
+ {
+ name: "local setting overrides global setting",
+ inputFile: "scrape_config_local_global_validation_mode",
+ expectValidationScheme: model.UTF8Validation,
+ expectEscapingScheme: "dots",
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ want, err := LoadFile(fmt.Sprintf("testdata/%s.yml", tc.inputFile), false, promslog.NewNopLogger())
+ require.NoError(t, err)
+
+ out, err := yaml.Marshal(want)
+
+ require.NoError(t, err)
+ got := &Config{}
+ require.NoError(t, yaml.UnmarshalStrict(out, got))
+
+ require.Equal(t, tc.expectValidationScheme, got.ScrapeConfigs[0].MetricNameValidationScheme)
+ require.Equal(t, tc.expectEscapingScheme, got.ScrapeConfigs[0].MetricNameEscapingScheme)
+ })
+ }
+}
+
+func TestScrapeProtocolHeader(t *testing.T) {
+ tests := []struct {
+ name string
+ proto ScrapeProtocol
+ expectedValue string
+ }{
+ {
+ name: "blank",
+ proto: ScrapeProtocol(""),
+ expectedValue: "",
+ },
+ {
+ name: "invalid",
+ proto: ScrapeProtocol("invalid"),
+ expectedValue: "",
+ },
+ {
+ name: "prometheus protobuf",
+ proto: PrometheusProto,
+ expectedValue: "application/vnd.google.protobuf",
+ },
+ {
+ name: "prometheus text 0.0.4",
+ proto: PrometheusText0_0_4,
+ expectedValue: "text/plain",
+ },
+ {
+ name: "prometheus text 1.0.0",
+ proto: PrometheusText1_0_0,
+ expectedValue: "text/plain",
+ },
+ {
+ name: "openmetrics 0.0.1",
+ proto: OpenMetricsText0_0_1,
+ expectedValue: "application/openmetrics-text",
+ },
+ {
+ name: "openmetrics 1.0.0",
+ proto: OpenMetricsText1_0_0,
+ expectedValue: "application/openmetrics-text",
+ },
+ }
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ mediaType := tc.proto.HeaderMediaType()
+
+ require.Equal(t, tc.expectedValue, mediaType)
+ })
+ }
+}
+
+// Regression test against https://github.com/prometheus/prometheus/issues/15538
+func TestGetScrapeConfigs_Loaded(t *testing.T) {
+ t.Run("without load", func(t *testing.T) {
+ c := &Config{}
+ _, err := c.GetScrapeConfigs()
+ require.EqualError(t, err, "scrape config cannot be fetched, main config was not validated and loaded correctly; should not happen")
+ })
+ t.Run("with load", func(t *testing.T) {
+ c, err := Load("", promslog.NewNopLogger())
+ require.NoError(t, err)
+ _, err = c.GetScrapeConfigs()
+ require.NoError(t, err)
+ })
+}
diff --git a/config/config_windows_test.go b/config/config_windows_test.go
index db4d46ef13a..e7627f562a2 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
@@ -16,8 +16,11 @@ package config
const ruleFilesConfigFile = "testdata/rules_abs_path_windows.good.yml"
var ruleFilesExpectedConf = &Config{
- GlobalConfig: DefaultGlobalConfig,
- Runtime: DefaultRuntimeConfig,
+ loaded: true,
+
+ GlobalConfig: DefaultGlobalConfig,
+ Runtime: DefaultRuntimeConfig,
+ StorageConfig: StorageConfig{TSDBConfig: &TSDBConfig{Retention: &TSDBRetentionConfig{}}},
RuleFiles: []string{
"testdata\\first.rules",
"testdata\\rules\\second.rules",
diff --git a/config/reload.go b/config/reload.go
new file mode 100644
index 00000000000..a250693169c
--- /dev/null
+++ b/config/reload.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 config
+
+import (
+ "crypto/sha256"
+ "encoding/hex"
+ "fmt"
+ "os"
+ "path/filepath"
+
+ promconfig "github.com/prometheus/common/config"
+ "go.yaml.in/yaml/v2"
+)
+
+type ExternalFilesConfig struct {
+ RuleFiles []string `yaml:"rule_files"`
+ ScrapeConfigFiles []string `yaml:"scrape_config_files"`
+}
+
+// GenerateChecksum generates a checksum of the YAML file and the files it references.
+func GenerateChecksum(yamlFilePath string) (string, error) {
+ hash := sha256.New()
+
+ yamlContent, err := os.ReadFile(yamlFilePath)
+ if err != nil {
+ return "", fmt.Errorf("error reading YAML file: %w", err)
+ }
+ _, err = hash.Write(yamlContent)
+ if err != nil {
+ return "", fmt.Errorf("error writing YAML file to hash: %w", err)
+ }
+
+ var config ExternalFilesConfig
+ if err := yaml.Unmarshal(yamlContent, &config); err != nil {
+ return "", fmt.Errorf("error unmarshalling YAML: %w", err)
+ }
+
+ dir := filepath.Dir(yamlFilePath)
+
+ for i, file := range config.RuleFiles {
+ config.RuleFiles[i] = promconfig.JoinDir(dir, file)
+ }
+ for i, file := range config.ScrapeConfigFiles {
+ config.ScrapeConfigFiles[i] = promconfig.JoinDir(dir, file)
+ }
+
+ files := map[string][]string{
+ "r": config.RuleFiles, // "r" for rule files
+ "s": config.ScrapeConfigFiles, // "s" for scrape config files
+ }
+
+ for _, prefix := range []string{"r", "s"} {
+ for _, pattern := range files[prefix] {
+ matchingFiles, err := filepath.Glob(pattern)
+ if err != nil {
+ return "", fmt.Errorf("error finding files with pattern %q: %w", pattern, err)
+ }
+
+ for _, file := range matchingFiles {
+ // Write prefix to the hash ("r" or "s") followed by \0, then
+ // the file path.
+ _, err = hash.Write([]byte(prefix + "\x00" + file + "\x00"))
+ if err != nil {
+ return "", fmt.Errorf("error writing %q path to hash: %w", file, err)
+ }
+
+ // Read and hash the content of the file.
+ content, err := os.ReadFile(file)
+ if err != nil {
+ return "", fmt.Errorf("error reading file %s: %w", file, err)
+ }
+ _, err = hash.Write(append(content, []byte("\x00")...))
+ if err != nil {
+ return "", fmt.Errorf("error writing %q content to hash: %w", file, err)
+ }
+ }
+ }
+ }
+
+ return hex.EncodeToString(hash.Sum(nil)), nil
+}
diff --git a/config/reload_test.go b/config/reload_test.go
new file mode 100644
index 00000000000..cb60d47651b
--- /dev/null
+++ b/config/reload_test.go
@@ -0,0 +1,246 @@
+// Copyright The Prometheus Authors
+// Licensed under the Apache License, Version 2.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 config
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestGenerateChecksum(t *testing.T) {
+ tmpDir := t.TempDir()
+
+ // Define paths for the temporary files.
+ yamlFilePath := filepath.Join(tmpDir, "test.yml")
+ ruleFile := "rule_file.yml"
+ ruleFilePath := filepath.Join(tmpDir, ruleFile)
+ scrapeConfigFile := "scrape_config.yml"
+ scrapeConfigFilePath := filepath.Join(tmpDir, scrapeConfigFile)
+
+ // Define initial and modified content for the files.
+ originalRuleContent := "groups:\n- name: example\n rules:\n - alert: ExampleAlert"
+ modifiedRuleContent := "groups:\n- name: example\n rules:\n - alert: ModifiedAlert"
+
+ originalScrapeConfigContent := "scrape_configs:\n- job_name: example"
+ modifiedScrapeConfigContent := "scrape_configs:\n- job_name: modified_example"
+
+ testCases := []struct {
+ name string
+ ruleFilePath string
+ scrapeConfigFilePath string
+ }{
+ {
+ name: "Auto reload using relative path.",
+ ruleFilePath: ruleFile,
+ scrapeConfigFilePath: scrapeConfigFile,
+ },
+ {
+ name: "Auto reload using absolute path.",
+ ruleFilePath: ruleFilePath,
+ scrapeConfigFilePath: scrapeConfigFilePath,
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ // Define YAML content referencing the rule and scrape config files.
+ yamlContent := fmt.Sprintf(`
+rule_files:
+ - %s
+scrape_config_files:
+ - %s
+`, tc.ruleFilePath, tc.scrapeConfigFilePath)
+
+ // Write initial content to files.
+ require.NoError(t, os.WriteFile(ruleFilePath, []byte(originalRuleContent), 0o644))
+ require.NoError(t, os.WriteFile(scrapeConfigFilePath, []byte(originalScrapeConfigContent), 0o644))
+ require.NoError(t, os.WriteFile(yamlFilePath, []byte(yamlContent), 0o644))
+
+ // Generate the original checksum.
+ originalChecksum := calculateChecksum(t, yamlFilePath)
+
+ t.Run("Rule File Change", func(t *testing.T) {
+ // Modify the rule file.
+ require.NoError(t, os.WriteFile(ruleFilePath, []byte(modifiedRuleContent), 0o644))
+
+ // Checksum should change.
+ modifiedChecksum := calculateChecksum(t, yamlFilePath)
+ require.NotEqual(t, originalChecksum, modifiedChecksum)
+
+ // Revert the rule file.
+ require.NoError(t, os.WriteFile(ruleFilePath, []byte(originalRuleContent), 0o644))
+
+ // Checksum should return to the original.
+ revertedChecksum := calculateChecksum(t, yamlFilePath)
+ require.Equal(t, originalChecksum, revertedChecksum)
+ })
+
+ t.Run("Scrape Config Change", func(t *testing.T) {
+ // Modify the scrape config file.
+ require.NoError(t, os.WriteFile(scrapeConfigFilePath, []byte(modifiedScrapeConfigContent), 0o644))
+
+ // Checksum should change.
+ modifiedChecksum := calculateChecksum(t, yamlFilePath)
+ require.NotEqual(t, originalChecksum, modifiedChecksum)
+
+ // Revert the scrape config file.
+ require.NoError(t, os.WriteFile(scrapeConfigFilePath, []byte(originalScrapeConfigContent), 0o644))
+
+ // Checksum should return to the original.
+ revertedChecksum := calculateChecksum(t, yamlFilePath)
+ require.Equal(t, originalChecksum, revertedChecksum)
+ })
+
+ t.Run("Rule File Deletion", func(t *testing.T) {
+ // Delete the rule file.
+ require.NoError(t, os.Remove(ruleFilePath))
+
+ // Checksum should change.
+ deletedChecksum := calculateChecksum(t, yamlFilePath)
+ require.NotEqual(t, originalChecksum, deletedChecksum)
+
+ // Restore the rule file.
+ require.NoError(t, os.WriteFile(ruleFilePath, []byte(originalRuleContent), 0o644))
+
+ // Checksum should return to the original.
+ revertedChecksum := calculateChecksum(t, yamlFilePath)
+ require.Equal(t, originalChecksum, revertedChecksum)
+ })
+
+ t.Run("Scrape Config Deletion", func(t *testing.T) {
+ // Delete the scrape config file.
+ require.NoError(t, os.Remove(scrapeConfigFilePath))
+
+ // Checksum should change.
+ deletedChecksum := calculateChecksum(t, yamlFilePath)
+ require.NotEqual(t, originalChecksum, deletedChecksum)
+
+ // Restore the scrape config file.
+ require.NoError(t, os.WriteFile(scrapeConfigFilePath, []byte(originalScrapeConfigContent), 0o644))
+
+ // Checksum should return to the original.
+ revertedChecksum := calculateChecksum(t, yamlFilePath)
+ require.Equal(t, originalChecksum, revertedChecksum)
+ })
+
+ t.Run("Main File Change", func(t *testing.T) {
+ // Modify the main YAML file.
+ modifiedYamlContent := fmt.Sprintf(`
+global:
+ scrape_interval: 3s
+rule_files:
+ - %s
+scrape_config_files:
+ - %s
+`, tc.ruleFilePath, tc.scrapeConfigFilePath)
+ require.NoError(t, os.WriteFile(yamlFilePath, []byte(modifiedYamlContent), 0o644))
+
+ // Checksum should change.
+ modifiedChecksum := calculateChecksum(t, yamlFilePath)
+ require.NotEqual(t, originalChecksum, modifiedChecksum)
+
+ // Revert the main YAML file.
+ require.NoError(t, os.WriteFile(yamlFilePath, []byte(yamlContent), 0o644))
+
+ // Checksum should return to the original.
+ revertedChecksum := calculateChecksum(t, yamlFilePath)
+ require.Equal(t, originalChecksum, revertedChecksum)
+ })
+
+ t.Run("Rule File Removed from YAML Config", func(t *testing.T) {
+ // Modify the YAML content to remove the rule file.
+ modifiedYamlContent := fmt.Sprintf(`
+scrape_config_files:
+ - %s
+`, tc.scrapeConfigFilePath)
+ require.NoError(t, os.WriteFile(yamlFilePath, []byte(modifiedYamlContent), 0o644))
+
+ // Checksum should change.
+ modifiedChecksum := calculateChecksum(t, yamlFilePath)
+ require.NotEqual(t, originalChecksum, modifiedChecksum)
+
+ // Revert the YAML content.
+ require.NoError(t, os.WriteFile(yamlFilePath, []byte(yamlContent), 0o644))
+
+ // Checksum should return to the original.
+ revertedChecksum := calculateChecksum(t, yamlFilePath)
+ require.Equal(t, originalChecksum, revertedChecksum)
+ })
+
+ t.Run("Scrape Config Removed from YAML Config", func(t *testing.T) {
+ // Modify the YAML content to remove the scrape config file.
+ modifiedYamlContent := fmt.Sprintf(`
+rule_files:
+ - %s
+`, tc.ruleFilePath)
+ require.NoError(t, os.WriteFile(yamlFilePath, []byte(modifiedYamlContent), 0o644))
+
+ // Checksum should change.
+ modifiedChecksum := calculateChecksum(t, yamlFilePath)
+ require.NotEqual(t, originalChecksum, modifiedChecksum)
+
+ // Revert the YAML content.
+ require.NoError(t, os.WriteFile(yamlFilePath, []byte(yamlContent), 0o644))
+
+ // Checksum should return to the original.
+ revertedChecksum := calculateChecksum(t, yamlFilePath)
+ require.Equal(t, originalChecksum, revertedChecksum)
+ })
+
+ t.Run("Empty Rule File", func(t *testing.T) {
+ // Write an empty rule file.
+ require.NoError(t, os.WriteFile(ruleFilePath, []byte(""), 0o644))
+
+ // Checksum should change.
+ emptyChecksum := calculateChecksum(t, yamlFilePath)
+ require.NotEqual(t, originalChecksum, emptyChecksum)
+
+ // Restore the rule file.
+ require.NoError(t, os.WriteFile(ruleFilePath, []byte(originalRuleContent), 0o644))
+
+ // Checksum should return to the original.
+ revertedChecksum := calculateChecksum(t, yamlFilePath)
+ require.Equal(t, originalChecksum, revertedChecksum)
+ })
+
+ t.Run("Empty Scrape Config File", func(t *testing.T) {
+ // Write an empty scrape config file.
+ require.NoError(t, os.WriteFile(scrapeConfigFilePath, []byte(""), 0o644))
+
+ // Checksum should change.
+ emptyChecksum := calculateChecksum(t, yamlFilePath)
+ require.NotEqual(t, originalChecksum, emptyChecksum)
+
+ // Restore the scrape config file.
+ require.NoError(t, os.WriteFile(scrapeConfigFilePath, []byte(originalScrapeConfigContent), 0o644))
+
+ // Checksum should return to the original.
+ revertedChecksum := calculateChecksum(t, yamlFilePath)
+ require.Equal(t, originalChecksum, revertedChecksum)
+ })
+ })
+ }
+}
+
+// calculateChecksum generates a checksum for the given YAML file path.
+func calculateChecksum(t *testing.T, yamlFilePath string) string {
+ checksum, err := GenerateChecksum(yamlFilePath)
+ require.NoError(t, err)
+ require.NotEmpty(t, checksum)
+ return checksum
+}
diff --git a/config/testdata/conf.good.yml b/config/testdata/conf.good.yml
index 9eb79954325..a2f8459b24d 100644
--- a/config/testdata/conf.good.yml
+++ b/config/testdata/conf.good.yml
@@ -74,6 +74,8 @@ scrape_configs:
# metrics_path defaults to '/metrics'
# scheme defaults to 'http'.
+ fallback_scrape_protocol: PrometheusText0.0.4
+
scrape_failure_log_file: fail_prom.log
file_sd_configs:
- files:
@@ -180,6 +182,7 @@ scrape_configs:
token: mysecret
path_prefix: /consul
services: ["nginx", "cache", "mysql"]
+ health_filter: 'Service.Tags contains "canary"'
tags: ["canary", "v1"]
node_meta:
rack: "123"
@@ -367,6 +370,7 @@ scrape_configs:
key_file: valid_key_file
- job_name: hetzner
+ scrape_native_histograms: true
relabel_configs:
- action: uppercase
source_labels: [instance]
@@ -415,6 +419,14 @@ scrape_configs:
- authorization:
credentials: abcdef
+ - job_name: stackit-servers
+ stackit_sd_configs:
+ - project: 11111111-1111-1111-1111-111111111111
+ service_account_key: mysecret_sa_key
+ private_key: mysecret_private_key
+ authorization:
+ credentials: abcdef
+
- job_name: uyuni
uyuni_sd_configs:
- server: https://localhost:1234
@@ -432,6 +444,12 @@ scrape_configs:
- authorization:
credentials: abcdef
+ - job_name: outscale
+ outscale_sd_configs:
+ - region: eu-west-2
+ access_key: A1B2C3D4E5F6G7H8I9J0
+ secret_key: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+
alerting:
alertmanagers:
- scheme: https
@@ -444,6 +462,11 @@ alerting:
storage:
tsdb:
out_of_order_time_window: 30m
+ stale_series_compaction_threshold: 0.5
+ retention:
+ time: 1d
+ size: 1GB
+ percentage: 28
tracing:
endpoint: "localhost:4317"
diff --git a/config/testdata/config_with_deprecated_am_api_config.yml b/config/testdata/config_with_deprecated_am_api_config.yml
new file mode 100644
index 00000000000..ac89537ff1b
--- /dev/null
+++ b/config/testdata/config_with_deprecated_am_api_config.yml
@@ -0,0 +1,7 @@
+alerting:
+ alertmanagers:
+ - scheme: http
+ api_version: v1
+ file_sd_configs:
+ - files:
+ - nonexistent_file.yml
diff --git a/config/testdata/global_convert_classic_hist_to_nhcb.good.yml b/config/testdata/global_convert_classic_hist_to_nhcb.good.yml
new file mode 100644
index 00000000000..c97b7597afc
--- /dev/null
+++ b/config/testdata/global_convert_classic_hist_to_nhcb.good.yml
@@ -0,0 +1,6 @@
+global:
+ convert_classic_histograms_to_nhcb: true
+scrape_configs:
+ - job_name: prometheus
+ static_configs:
+ - targets: ['localhost:8080']
diff --git a/config/testdata/global_disable_always_scrape_classic_hist.good.yml b/config/testdata/global_disable_always_scrape_classic_hist.good.yml
new file mode 100644
index 00000000000..de28f1357aa
--- /dev/null
+++ b/config/testdata/global_disable_always_scrape_classic_hist.good.yml
@@ -0,0 +1,6 @@
+global:
+ always_scrape_classic_histograms: false
+scrape_configs:
+ - job_name: prometheus
+ static_configs:
+ - targets: ['localhost:8080']
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 00000000000..26c6e4b8b54
--- /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_always_scrape_classic_hist.good.yml b/config/testdata/global_enable_always_scrape_classic_hist.good.yml
new file mode 100644
index 00000000000..d42cf69cb69
--- /dev/null
+++ b/config/testdata/global_enable_always_scrape_classic_hist.good.yml
@@ -0,0 +1,6 @@
+global:
+ always_scrape_classic_histograms: true
+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 00000000000..1d7ea2db1c6
--- /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/global_enable_scrape_native_hist.good.yml b/config/testdata/global_enable_scrape_native_hist.good.yml
new file mode 100644
index 00000000000..fe727f8018e
--- /dev/null
+++ b/config/testdata/global_enable_scrape_native_hist.good.yml
@@ -0,0 +1,6 @@
+global:
+ scrape_native_histograms: true
+scrape_configs:
+ - job_name: prometheus
+ static_configs:
+ - targets: ['localhost:8080']
diff --git a/config/testdata/global_enable_scrape_native_hist_and_scrape_protocols.good.yml b/config/testdata/global_enable_scrape_native_hist_and_scrape_protocols.good.yml
new file mode 100644
index 00000000000..04474c53e3c
--- /dev/null
+++ b/config/testdata/global_enable_scrape_native_hist_and_scrape_protocols.good.yml
@@ -0,0 +1,7 @@
+global:
+ scrape_native_histograms: true
+ scrape_protocols: ['PrometheusText0.0.4']
+scrape_configs:
+ - job_name: prometheus
+ static_configs:
+ - targets: ['localhost:8080']
diff --git a/config/testdata/global_scrape_protocols_and_local_disable_scrape_native_hist.good.yml b/config/testdata/global_scrape_protocols_and_local_disable_scrape_native_hist.good.yml
new file mode 100644
index 00000000000..6cad07e3b0f
--- /dev/null
+++ b/config/testdata/global_scrape_protocols_and_local_disable_scrape_native_hist.good.yml
@@ -0,0 +1,8 @@
+global:
+ scrape_native_histograms: true
+ scrape_protocols: ['PrometheusText0.0.4']
+scrape_configs:
+ - job_name: prometheus
+ scrape_native_histograms: false
+ static_configs:
+ - targets: ['localhost:8080']
diff --git a/config/testdata/jobname_dup.bad.yml b/config/testdata/jobname_dup.bad.yml
index 0265493c30c..d03cb0cf979 100644
--- a/config/testdata/jobname_dup.bad.yml
+++ b/config/testdata/jobname_dup.bad.yml
@@ -1,4 +1,6 @@
# Two scrape configs with the same job names are not allowed.
+global:
+ metric_name_validation_scheme: legacy
scrape_configs:
- job_name: prometheus
- job_name: service-x
diff --git a/config/testdata/kubernetes_selectors_pod.bad.yml b/config/testdata/kubernetes_selectors_pod.bad.yml
index 3a1a83abd2d..24285c0ce0a 100644
--- a/config/testdata/kubernetes_selectors_pod.bad.yml
+++ b/config/testdata/kubernetes_selectors_pod.bad.yml
@@ -6,3 +6,6 @@ scrape_configs:
- role: "node"
label: "foo=bar"
field: "metadata.status=Running"
+ - role: "service"
+ label: "baz=que"
+ field: "metadata.status=Running"
diff --git a/config/testdata/kubernetes_selectors_pod.good.yml b/config/testdata/kubernetes_selectors_pod.good.yml
index 91da6ada177..49c17d72cee 100644
--- a/config/testdata/kubernetes_selectors_pod.good.yml
+++ b/config/testdata/kubernetes_selectors_pod.good.yml
@@ -11,3 +11,8 @@ scrape_configs:
- role: "pod"
label: "foo in (bar,baz)"
field: "metadata.status=Running"
+ - role: pod
+ selectors:
+ - role: "node"
+ label: "foo=bar"
+ field: "metadata.status=Running"
diff --git a/config/testdata/labelmap.bad.yml b/config/testdata/labelmap.bad.yml
index 29d2653990c..b8aa117acf8 100644
--- a/config/testdata/labelmap.bad.yml
+++ b/config/testdata/labelmap.bad.yml
@@ -2,4 +2,4 @@ scrape_configs:
- job_name: prometheus
relabel_configs:
- action: labelmap
- replacement: l-$1
+ replacement: !!binary "/w==$1"
diff --git a/config/testdata/labelname.bad.yml b/config/testdata/labelname.bad.yml
index c06853a26bc..0c9c5ef3b2a 100644
--- a/config/testdata/labelname.bad.yml
+++ b/config/testdata/labelname.bad.yml
@@ -1,3 +1,3 @@
global:
external_labels:
- not$allowed: value
+ !!binary "/w==": value
diff --git a/config/testdata/labelname2.bad.yml b/config/testdata/labelname2.bad.yml
deleted file mode 100644
index 7afcd6bcfc4..00000000000
--- a/config/testdata/labelname2.bad.yml
+++ /dev/null
@@ -1,3 +0,0 @@
-global:
- external_labels:
- 'not:allowed': value
diff --git a/config/testdata/local_convert_classic_hist_to_nhcb.good.yml b/config/testdata/local_convert_classic_hist_to_nhcb.good.yml
new file mode 100644
index 00000000000..0fd31727eb2
--- /dev/null
+++ b/config/testdata/local_convert_classic_hist_to_nhcb.good.yml
@@ -0,0 +1,7 @@
+global:
+ convert_classic_histograms_to_nhcb: false
+scrape_configs:
+ - job_name: prometheus
+ static_configs:
+ - targets: ['localhost:8080']
+ convert_classic_histograms_to_nhcb: true
diff --git a/config/testdata/local_disable_always_scrape_classic_hist.good.yml b/config/testdata/local_disable_always_scrape_classic_hist.good.yml
new file mode 100644
index 00000000000..9e668340dc9
--- /dev/null
+++ b/config/testdata/local_disable_always_scrape_classic_hist.good.yml
@@ -0,0 +1,7 @@
+global:
+ always_scrape_classic_histograms: true
+scrape_configs:
+ - job_name: prometheus
+ static_configs:
+ - targets: ['localhost:8080']
+ always_scrape_classic_histograms: false
diff --git a/config/testdata/local_disable_convert_classic_hist_to_nhcb.good.yml b/config/testdata/local_disable_convert_classic_hist_to_nhcb.good.yml
new file mode 100644
index 00000000000..b41af7e0a54
--- /dev/null
+++ b/config/testdata/local_disable_convert_classic_hist_to_nhcb.good.yml
@@ -0,0 +1,7 @@
+global:
+ convert_classic_histograms_to_nhcb: true
+scrape_configs:
+ - job_name: prometheus
+ static_configs:
+ - targets: ['localhost:8080']
+ convert_classic_histograms_to_nhcb: false
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 00000000000..a1b7c646fa6
--- /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_disable_scrape_native_hist.good.yml b/config/testdata/local_disable_scrape_native_hist.good.yml
new file mode 100644
index 00000000000..9cff5e74477
--- /dev/null
+++ b/config/testdata/local_disable_scrape_native_hist.good.yml
@@ -0,0 +1,7 @@
+global:
+ scrape_native_histograms: true
+scrape_configs:
+ - job_name: prometheus
+ scrape_native_histograms: false
+ static_configs:
+ - targets: ['localhost:8080']
diff --git a/config/testdata/local_enable_always_scrape_classic_hist.good.yml b/config/testdata/local_enable_always_scrape_classic_hist.good.yml
new file mode 100644
index 00000000000..165be077544
--- /dev/null
+++ b/config/testdata/local_enable_always_scrape_classic_hist.good.yml
@@ -0,0 +1,7 @@
+global:
+ always_scrape_classic_histograms: false
+scrape_configs:
+ - job_name: prometheus
+ static_configs:
+ - targets: ['localhost:8080']
+ always_scrape_classic_histograms: true
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 00000000000..a1c8b2808ee
--- /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/config/testdata/local_enable_scrape_native_hist.good.yml b/config/testdata/local_enable_scrape_native_hist.good.yml
new file mode 100644
index 00000000000..21d3a008562
--- /dev/null
+++ b/config/testdata/local_enable_scrape_native_hist.good.yml
@@ -0,0 +1,6 @@
+global:
+scrape_configs:
+ - job_name: prometheus
+ scrape_native_histograms: true
+ static_configs:
+ - targets: ['localhost:8080']
diff --git a/config/testdata/local_enable_scrape_native_hist_and_scrape_protocols.good.yml b/config/testdata/local_enable_scrape_native_hist_and_scrape_protocols.good.yml
new file mode 100644
index 00000000000..00e10304a8d
--- /dev/null
+++ b/config/testdata/local_enable_scrape_native_hist_and_scrape_protocols.good.yml
@@ -0,0 +1,7 @@
+global:
+scrape_configs:
+ - job_name: prometheus
+ scrape_native_histograms: true
+ scrape_protocols: ['PrometheusText0.0.4']
+ static_configs:
+ - targets: ['localhost:8080']
diff --git a/config/testdata/lowercase.bad.yml b/config/testdata/lowercase.bad.yml
index 9bc95833417..6dd72e6476a 100644
--- a/config/testdata/lowercase.bad.yml
+++ b/config/testdata/lowercase.bad.yml
@@ -1,3 +1,5 @@
+global:
+ metric_name_validation_scheme: legacy
scrape_configs:
- job_name: prometheus
relabel_configs:
diff --git a/config/testdata/lowercase2.bad.yml b/config/testdata/lowercase2.bad.yml
deleted file mode 100644
index bde8862c66c..00000000000
--- a/config/testdata/lowercase2.bad.yml
+++ /dev/null
@@ -1,6 +0,0 @@
-scrape_configs:
- - job_name: prometheus
- relabel_configs:
- - action: lowercase
- source_labels: [__name__]
- target_label: 42lab
diff --git a/config/testdata/otlp_allow_keep_identifying_resource_attributes.good.yml b/config/testdata/otlp_allow_keep_identifying_resource_attributes.good.yml
new file mode 100644
index 00000000000..63151e2a77a
--- /dev/null
+++ b/config/testdata/otlp_allow_keep_identifying_resource_attributes.good.yml
@@ -0,0 +1,2 @@
+otlp:
+ keep_identifying_resource_attributes: true
diff --git a/config/testdata/otlp_allow_utf8.bad.yml b/config/testdata/otlp_allow_utf8.bad.yml
new file mode 100644
index 00000000000..488e4b05584
--- /dev/null
+++ b/config/testdata/otlp_allow_utf8.bad.yml
@@ -0,0 +1,4 @@
+global:
+ metric_name_validation_scheme: legacy
+otlp:
+ translation_strategy: Invalid
diff --git a/config/testdata/otlp_allow_utf8.good.yml b/config/testdata/otlp_allow_utf8.good.yml
new file mode 100644
index 00000000000..f3069d2fddb
--- /dev/null
+++ b/config/testdata/otlp_allow_utf8.good.yml
@@ -0,0 +1,2 @@
+otlp:
+ translation_strategy: NoUTF8EscapingWithSuffixes
diff --git a/config/testdata/otlp_allow_utf8.incompatible.yml b/config/testdata/otlp_allow_utf8.incompatible.yml
new file mode 100644
index 00000000000..2625c24131e
--- /dev/null
+++ b/config/testdata/otlp_allow_utf8.incompatible.yml
@@ -0,0 +1,4 @@
+global:
+ metric_name_validation_scheme: legacy
+otlp:
+ translation_strategy: NoUTF8EscapingWithSuffixes
diff --git a/config/testdata/otlp_convert_histograms_to_nhcb.good.yml b/config/testdata/otlp_convert_histograms_to_nhcb.good.yml
new file mode 100644
index 00000000000..1462cafe9b3
--- /dev/null
+++ b/config/testdata/otlp_convert_histograms_to_nhcb.good.yml
@@ -0,0 +1,2 @@
+otlp:
+ convert_histograms_to_nhcb: true
diff --git a/config/testdata/otlp_empty.yml b/config/testdata/otlp_empty.yml
new file mode 100644
index 00000000000..7085e9246bd
--- /dev/null
+++ b/config/testdata/otlp_empty.yml
@@ -0,0 +1 @@
+global:
diff --git a/config/testdata/otlp_ignore_resource_attributes_without_promote_all.bad.yml b/config/testdata/otlp_ignore_resource_attributes_without_promote_all.bad.yml
new file mode 100644
index 00000000000..be4ee60f2ab
--- /dev/null
+++ b/config/testdata/otlp_ignore_resource_attributes_without_promote_all.bad.yml
@@ -0,0 +1,2 @@
+otlp:
+ ignore_resource_attributes: ["k8s.job.name"]
diff --git a/config/testdata/otlp_label_underscore_sanitization_defaults.good.yml b/config/testdata/otlp_label_underscore_sanitization_defaults.good.yml
new file mode 100644
index 00000000000..3b1e9796d39
--- /dev/null
+++ b/config/testdata/otlp_label_underscore_sanitization_defaults.good.yml
@@ -0,0 +1,2 @@
+otlp:
+ promote_resource_attributes: ["service.name"]
diff --git a/config/testdata/otlp_label_underscore_sanitization_disabled.good.yml b/config/testdata/otlp_label_underscore_sanitization_disabled.good.yml
new file mode 100644
index 00000000000..f8fe4ea6699
--- /dev/null
+++ b/config/testdata/otlp_label_underscore_sanitization_disabled.good.yml
@@ -0,0 +1,3 @@
+otlp:
+ label_name_underscore_sanitization: false
+ label_name_preserve_multiple_underscores: false
diff --git a/config/testdata/otlp_label_underscore_sanitization_enabled.good.yml b/config/testdata/otlp_label_underscore_sanitization_enabled.good.yml
new file mode 100644
index 00000000000..8ce347495ea
--- /dev/null
+++ b/config/testdata/otlp_label_underscore_sanitization_enabled.good.yml
@@ -0,0 +1,3 @@
+otlp:
+ label_name_underscore_sanitization: true
+ label_name_preserve_multiple_underscores: true
diff --git a/config/testdata/otlp_no_translation.good.yml b/config/testdata/otlp_no_translation.good.yml
new file mode 100644
index 00000000000..e5c44608425
--- /dev/null
+++ b/config/testdata/otlp_no_translation.good.yml
@@ -0,0 +1,2 @@
+otlp:
+ translation_strategy: NoTranslation
diff --git a/config/testdata/otlp_no_translation.incompatible.yml b/config/testdata/otlp_no_translation.incompatible.yml
new file mode 100644
index 00000000000..33c5a756f53
--- /dev/null
+++ b/config/testdata/otlp_no_translation.incompatible.yml
@@ -0,0 +1,4 @@
+global:
+ metric_name_validation_scheme: legacy
+otlp:
+ translation_strategy: NoTranslation
diff --git a/config/testdata/otlp_promote_all_resource_attributes.bad.yml b/config/testdata/otlp_promote_all_resource_attributes.bad.yml
new file mode 100644
index 00000000000..2be54ec155b
--- /dev/null
+++ b/config/testdata/otlp_promote_all_resource_attributes.bad.yml
@@ -0,0 +1,3 @@
+otlp:
+ promote_all_resource_attributes: true
+ promote_resource_attributes: ["k8s.cluster.name", " k8s.job.name ", "k8s.namespace.name", "k8s.job.name"]
diff --git a/config/testdata/otlp_promote_scope_metadata.good.yml b/config/testdata/otlp_promote_scope_metadata.good.yml
new file mode 100644
index 00000000000..d88f657abd8
--- /dev/null
+++ b/config/testdata/otlp_promote_scope_metadata.good.yml
@@ -0,0 +1,2 @@
+otlp:
+ promote_scope_metadata: true
diff --git a/config/testdata/otlp_sanitize_default_resource_attributes.good.yml b/config/testdata/otlp_sanitize_default_resource_attributes.good.yml
new file mode 100644
index 00000000000..abdd98dc7a2
--- /dev/null
+++ b/config/testdata/otlp_sanitize_default_resource_attributes.good.yml
@@ -0,0 +1 @@
+otlp:
diff --git a/config/testdata/otlp_sanitize_ignore_resource_attributes.bad.yml b/config/testdata/otlp_sanitize_ignore_resource_attributes.bad.yml
new file mode 100644
index 00000000000..57ce0efac09
--- /dev/null
+++ b/config/testdata/otlp_sanitize_ignore_resource_attributes.bad.yml
@@ -0,0 +1,3 @@
+otlp:
+ promote_all_resource_attributes: true
+ ignore_resource_attributes: ["k8s.cluster.name", " k8s.job.name ", "k8s.namespace.name", "k8s.job.name", ""]
diff --git a/config/testdata/otlp_sanitize_ignore_resource_attributes.good.yml b/config/testdata/otlp_sanitize_ignore_resource_attributes.good.yml
new file mode 100644
index 00000000000..7a50fef4051
--- /dev/null
+++ b/config/testdata/otlp_sanitize_ignore_resource_attributes.good.yml
@@ -0,0 +1,3 @@
+otlp:
+ promote_all_resource_attributes: true
+ ignore_resource_attributes: ["k8s.cluster.name", " k8s.job.name ", "k8s.namespace.name"]
diff --git a/config/testdata/otlp_sanitize_resource_attributes.bad.yml b/config/testdata/otlp_sanitize_promote_resource_attributes.bad.yml
similarity index 100%
rename from config/testdata/otlp_sanitize_resource_attributes.bad.yml
rename to config/testdata/otlp_sanitize_promote_resource_attributes.bad.yml
diff --git a/config/testdata/otlp_sanitize_resource_attributes.good.yml b/config/testdata/otlp_sanitize_promote_resource_attributes.good.yml
similarity index 100%
rename from config/testdata/otlp_sanitize_resource_attributes.good.yml
rename to config/testdata/otlp_sanitize_promote_resource_attributes.good.yml
diff --git a/config/testdata/otlp_sanitize_resource_attributes_promote_all.good.yml b/config/testdata/otlp_sanitize_resource_attributes_promote_all.good.yml
new file mode 100644
index 00000000000..2c8360011bc
--- /dev/null
+++ b/config/testdata/otlp_sanitize_resource_attributes_promote_all.good.yml
@@ -0,0 +1,2 @@
+otlp:
+ promote_all_resource_attributes: true
diff --git a/config/testdata/outscale_no_region.bad.yml b/config/testdata/outscale_no_region.bad.yml
new file mode 100644
index 00000000000..f49102bb3d0
--- /dev/null
+++ b/config/testdata/outscale_no_region.bad.yml
@@ -0,0 +1,6 @@
+scrape_configs:
+ - job_name: outscale
+ outscale_sd_configs:
+ - region: ""
+ access_key: A1B2C3D4E5F6G7H8I9J0
+ secret_key: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
diff --git a/config/testdata/outscale_no_secret.bad.yml b/config/testdata/outscale_no_secret.bad.yml
new file mode 100644
index 00000000000..30602b44a99
--- /dev/null
+++ b/config/testdata/outscale_no_secret.bad.yml
@@ -0,0 +1,5 @@
+scrape_configs:
+ - job_name: prometheus
+ outscale_sd_configs:
+ - region: eu-west-2
+ access_key: A1B2C3D4E5F6G7H8I9J0
diff --git a/config/testdata/outscale_two_secrets.bad.yml b/config/testdata/outscale_two_secrets.bad.yml
new file mode 100644
index 00000000000..4e8e76c3529
--- /dev/null
+++ b/config/testdata/outscale_two_secrets.bad.yml
@@ -0,0 +1,7 @@
+scrape_configs:
+ - job_name: outscale
+ outscale_sd_configs:
+ - region: eu-west-2
+ access_key: A1B2C3D4E5F6G7H8I9J0
+ secret_key: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+ secret_key_file: /tmp/outscale-secret-key
diff --git a/config/testdata/remote_write_queue_capacity_zero.bad.yml b/config/testdata/remote_write_queue_capacity_zero.bad.yml
new file mode 100644
index 00000000000..775b1e21a9a
--- /dev/null
+++ b/config/testdata/remote_write_queue_capacity_zero.bad.yml
@@ -0,0 +1,4 @@
+remote_write:
+ - url: http://localhost:9090/api/v1/write
+ queue_config:
+ capacity: 0
diff --git a/config/testdata/remote_write_queue_max_backoff_less_than_min.bad.yml b/config/testdata/remote_write_queue_max_backoff_less_than_min.bad.yml
new file mode 100644
index 00000000000..e0629b206ee
--- /dev/null
+++ b/config/testdata/remote_write_queue_max_backoff_less_than_min.bad.yml
@@ -0,0 +1,5 @@
+remote_write:
+ - url: http://localhost:9090/api/v1/write
+ queue_config:
+ min_backoff: 10s
+ max_backoff: 1s
diff --git a/config/testdata/remote_write_queue_max_samples_per_send_zero.bad.yml b/config/testdata/remote_write_queue_max_samples_per_send_zero.bad.yml
new file mode 100644
index 00000000000..301e733632a
--- /dev/null
+++ b/config/testdata/remote_write_queue_max_samples_per_send_zero.bad.yml
@@ -0,0 +1,4 @@
+remote_write:
+ - url: http://localhost:9090/api/v1/write
+ queue_config:
+ max_samples_per_send: 0
diff --git a/config/testdata/remote_write_queue_max_shards_zero.bad.yml b/config/testdata/remote_write_queue_max_shards_zero.bad.yml
new file mode 100644
index 00000000000..5fd362e6923
--- /dev/null
+++ b/config/testdata/remote_write_queue_max_shards_zero.bad.yml
@@ -0,0 +1,4 @@
+remote_write:
+ - url: http://localhost:9090/api/v1/write
+ queue_config:
+ max_shards: 0
diff --git a/config/testdata/remote_write_queue_min_shards_greater_than_max.bad.yml b/config/testdata/remote_write_queue_min_shards_greater_than_max.bad.yml
new file mode 100644
index 00000000000..020ecde766f
--- /dev/null
+++ b/config/testdata/remote_write_queue_min_shards_greater_than_max.bad.yml
@@ -0,0 +1,5 @@
+remote_write:
+ - url: http://localhost:9090/api/v1/write
+ queue_config:
+ min_shards: 100
+ max_shards: 10
diff --git a/config/testdata/remote_write_queue_min_shards_zero.bad.yml b/config/testdata/remote_write_queue_min_shards_zero.bad.yml
new file mode 100644
index 00000000000..a925449c4af
--- /dev/null
+++ b/config/testdata/remote_write_queue_min_shards_zero.bad.yml
@@ -0,0 +1,4 @@
+remote_write:
+ - url: http://localhost:9090/api/v1/write
+ queue_config:
+ min_shards: 0
diff --git a/config/testdata/roundtrip.good.yml b/config/testdata/roundtrip.good.yml
index 24ab7d25927..d999dafd320 100644
--- a/config/testdata/roundtrip.good.yml
+++ b/config/testdata/roundtrip.good.yml
@@ -41,6 +41,7 @@ scrape_configs:
- server: localhost:1234
token:
services: [nginx, cache, mysql]
+ health_filter: 'Service.Tags contains "canary"'
tags: [canary, v1]
node_meta:
rack: "123"
diff --git a/config/testdata/scrape_config_files_fallback_scrape_protocol1.bad.yml b/config/testdata/scrape_config_files_fallback_scrape_protocol1.bad.yml
new file mode 100644
index 00000000000..07cfe47594e
--- /dev/null
+++ b/config/testdata/scrape_config_files_fallback_scrape_protocol1.bad.yml
@@ -0,0 +1,5 @@
+scrape_configs:
+ - job_name: node
+ fallback_scrape_protocol: "prometheusproto"
+ static_configs:
+ - targets: ['localhost:8080']
diff --git a/config/testdata/scrape_config_files_fallback_scrape_protocol2.bad.yml b/config/testdata/scrape_config_files_fallback_scrape_protocol2.bad.yml
new file mode 100644
index 00000000000..c5d133f9c46
--- /dev/null
+++ b/config/testdata/scrape_config_files_fallback_scrape_protocol2.bad.yml
@@ -0,0 +1,5 @@
+scrape_configs:
+ - job_name: node
+ fallback_scrape_protocol: ["OpenMetricsText1.0.0", "PrometheusText0.0.4"]
+ static_configs:
+ - targets: ['localhost:8080']
diff --git a/config/testdata/scrape_config_global_validation_mode.yml b/config/testdata/scrape_config_global_validation_mode.yml
index 1548554397c..fb4baf7b07c 100644
--- a/config/testdata/scrape_config_global_validation_mode.yml
+++ b/config/testdata/scrape_config_global_validation_mode.yml
@@ -1,4 +1,5 @@
global:
- metric_name_validation_scheme: utf8
+ metric_name_validation_scheme: legacy
+ metric_name_escaping_scheme: dots
scrape_configs:
- job_name: prometheus
diff --git a/config/testdata/scrape_config_local_global_validation_mode.yml b/config/testdata/scrape_config_local_global_validation_mode.yml
index d13605e21d9..29cd2b41409 100644
--- a/config/testdata/scrape_config_local_global_validation_mode.yml
+++ b/config/testdata/scrape_config_local_global_validation_mode.yml
@@ -1,5 +1,7 @@
global:
- metric_name_validation_scheme: utf8
+ metric_name_validation_scheme: legacy
+ metric_name_escaping_scheme: values
scrape_configs:
- job_name: prometheus
- metric_name_validation_scheme: legacy
+ metric_name_validation_scheme: utf8
+ metric_name_escaping_scheme: dots
diff --git a/config/testdata/scrape_config_local_infer_escaping.yml b/config/testdata/scrape_config_local_infer_escaping.yml
new file mode 100644
index 00000000000..90279ff0818
--- /dev/null
+++ b/config/testdata/scrape_config_local_infer_escaping.yml
@@ -0,0 +1,3 @@
+scrape_configs:
+ - job_name: prometheus
+ metric_name_validation_scheme: legacy
diff --git a/config/testdata/scrape_config_local_validation_mode.yml b/config/testdata/scrape_config_local_validation_mode.yml
index fad4235806a..b4d1ff05df9 100644
--- a/config/testdata/scrape_config_local_validation_mode.yml
+++ b/config/testdata/scrape_config_local_validation_mode.yml
@@ -1,3 +1,4 @@
scrape_configs:
- job_name: prometheus
- metric_name_validation_scheme: utf8
+ metric_name_validation_scheme: legacy
+ metric_name_escaping_scheme: values
diff --git a/config/testdata/scrape_config_utf8_conflicting.bad.yml b/config/testdata/scrape_config_utf8_conflicting.bad.yml
new file mode 100644
index 00000000000..3f1b8f87ac3
--- /dev/null
+++ b/config/testdata/scrape_config_utf8_conflicting.bad.yml
@@ -0,0 +1,5 @@
+global:
+ metric_name_validation_scheme: legacy
+ metric_name_escaping_scheme: allow-utf-8
+scrape_configs:
+ - job_name: prometheus
diff --git a/config/testdata/stackit_endpoint.bad.yml b/config/testdata/stackit_endpoint.bad.yml
new file mode 100644
index 00000000000..ecb0fefe9ef
--- /dev/null
+++ b/config/testdata/stackit_endpoint.bad.yml
@@ -0,0 +1,4 @@
+scrape_configs:
+ - job_name: stackit
+ stackit_sd_configs:
+ - endpoint: "://invalid"
diff --git a/config/testdata/static_config.bad.json b/config/testdata/static_config.bad.json
deleted file mode 100644
index 6050ed9c509..00000000000
--- a/config/testdata/static_config.bad.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "targets": ["1.2.3.4:9100"],
- "labels": {
- "some_valid_label": "foo",
- "oops:this-label-is-invalid": "bar"
- }
-}
diff --git a/config/testdata/static_config.bad.yml b/config/testdata/static_config.bad.yml
index 1d229ec5e73..7e9003dcbf4 100644
--- a/config/testdata/static_config.bad.yml
+++ b/config/testdata/static_config.bad.yml
@@ -1,4 +1,4 @@
targets: ['1.2.3.4:9001', '1.2.3.5:9090']
labels:
valid_label: foo
- not:valid_label: bar
+ !!binary "/w==": bar
diff --git a/config/testdata/tsdb_chunk_encoding_floats.bad.yml b/config/testdata/tsdb_chunk_encoding_floats.bad.yml
new file mode 100644
index 00000000000..4ca1cadb58d
--- /dev/null
+++ b/config/testdata/tsdb_chunk_encoding_floats.bad.yml
@@ -0,0 +1,4 @@
+storage:
+ tsdb:
+ chunk_encoding:
+ floats: xor3
diff --git a/config/testdata/tsdb_chunk_encoding_floats_wrong_case.bad.yml b/config/testdata/tsdb_chunk_encoding_floats_wrong_case.bad.yml
new file mode 100644
index 00000000000..d38459d1ac0
--- /dev/null
+++ b/config/testdata/tsdb_chunk_encoding_floats_wrong_case.bad.yml
@@ -0,0 +1,4 @@
+storage:
+ tsdb:
+ chunk_encoding:
+ floats: XOR
diff --git a/config/testdata/tsdb_chunk_encoding_floats_wrong_case_xor2.bad.yml b/config/testdata/tsdb_chunk_encoding_floats_wrong_case_xor2.bad.yml
new file mode 100644
index 00000000000..58965a9506b
--- /dev/null
+++ b/config/testdata/tsdb_chunk_encoding_floats_wrong_case_xor2.bad.yml
@@ -0,0 +1,4 @@
+storage:
+ tsdb:
+ chunk_encoding:
+ floats: XOR2
diff --git a/config/testdata/tsdb_chunk_encoding_floats_xor.good.yml b/config/testdata/tsdb_chunk_encoding_floats_xor.good.yml
new file mode 100644
index 00000000000..71d71a42ead
--- /dev/null
+++ b/config/testdata/tsdb_chunk_encoding_floats_xor.good.yml
@@ -0,0 +1,4 @@
+storage:
+ tsdb:
+ chunk_encoding:
+ floats: xor
diff --git a/config/testdata/tsdb_chunk_encoding_floats_xor2.good.yml b/config/testdata/tsdb_chunk_encoding_floats_xor2.good.yml
new file mode 100644
index 00000000000..e17c2cc753c
--- /dev/null
+++ b/config/testdata/tsdb_chunk_encoding_floats_xor2.good.yml
@@ -0,0 +1,4 @@
+storage:
+ tsdb:
+ chunk_encoding:
+ floats: xor2
diff --git a/config/testdata/tsdb_retention_percentage.bad.yml b/config/testdata/tsdb_retention_percentage.bad.yml
new file mode 100644
index 00000000000..cb57abe0c02
--- /dev/null
+++ b/config/testdata/tsdb_retention_percentage.bad.yml
@@ -0,0 +1,4 @@
+storage:
+ tsdb:
+ retention:
+ percentage: 101
diff --git a/config/testdata/tsdb_retention_percentage_float.good.yml b/config/testdata/tsdb_retention_percentage_float.good.yml
new file mode 100644
index 00000000000..cf82ebf5faf
--- /dev/null
+++ b/config/testdata/tsdb_retention_percentage_float.good.yml
@@ -0,0 +1,4 @@
+storage:
+ tsdb:
+ retention:
+ percentage: 0.5
diff --git a/config/testdata/tsdb_retention_percentage_negative.bad.yml b/config/testdata/tsdb_retention_percentage_negative.bad.yml
new file mode 100644
index 00000000000..2eeb60c091f
--- /dev/null
+++ b/config/testdata/tsdb_retention_percentage_negative.bad.yml
@@ -0,0 +1,4 @@
+storage:
+ tsdb:
+ retention:
+ percentage: -1
diff --git a/config/testdata/tsdb_retention_size.bad.yml b/config/testdata/tsdb_retention_size.bad.yml
new file mode 100644
index 00000000000..ecae64aae6b
--- /dev/null
+++ b/config/testdata/tsdb_retention_size.bad.yml
@@ -0,0 +1,4 @@
+storage:
+ tsdb:
+ retention:
+ size: -1GB
diff --git a/config/testdata/tsdb_retention_time.bad.yml b/config/testdata/tsdb_retention_time.bad.yml
new file mode 100644
index 00000000000..465b3cf5da5
--- /dev/null
+++ b/config/testdata/tsdb_retention_time.bad.yml
@@ -0,0 +1,4 @@
+storage:
+ tsdb:
+ retention:
+ time: -1h
diff --git a/config/testdata/uppercase2.bad.yml b/config/testdata/uppercase2.bad.yml
deleted file mode 100644
index 330b9aceb68..00000000000
--- a/config/testdata/uppercase2.bad.yml
+++ /dev/null
@@ -1,6 +0,0 @@
-scrape_configs:
- - job_name: prometheus
- relabel_configs:
- - action: uppercase
- source_labels: [__name__]
- target_label: 42lab
diff --git a/console_libraries/menu.lib b/console_libraries/menu.lib
deleted file mode 100644
index 199ebf9f480..00000000000
--- a/console_libraries/menu.lib
+++ /dev/null
@@ -1,82 +0,0 @@
-{{/* vim: set ft=html: */}}
-
-{{/* Navbar, should be passed . */}}
-{{ define "navbar" }}
-
-{{ end }}
-
-{{/* LHS menu, should be passed . */}}
-{{ define "menu" }}
-