├── VERSION ├── MAINTAINERS.md ├── .golangci.yml ├── resources ├── static │ ├── favicon.ico │ ├── bootstrap4-glyphicons │ │ ├── fonts │ │ │ ├── fontawesome │ │ │ │ ├── fa-brands-400.eot │ │ │ │ ├── fa-brands-400.ttf │ │ │ │ ├── fa-brands-400.woff │ │ │ │ ├── fa-regular-400.eot │ │ │ │ ├── fa-regular-400.ttf │ │ │ │ ├── fa-solid-900.eot │ │ │ │ ├── fa-solid-900.ttf │ │ │ │ ├── fa-solid-900.woff │ │ │ │ ├── fa-solid-900.woff2 │ │ │ │ ├── fa-brands-400.woff2 │ │ │ │ ├── fa-regular-400.woff │ │ │ │ └── fa-regular-400.woff2 │ │ │ └── glyphicons │ │ │ │ ├── glyphicons-halflings-regular.eot │ │ │ │ ├── glyphicons-halflings-regular.ttf │ │ │ │ ├── glyphicons-halflings-regular.woff │ │ │ │ └── glyphicons-halflings-regular.woff2 │ │ └── css │ │ │ ├── bootstrap-glyphicons.min.css │ │ │ └── bootstrap-glyphicons.css │ ├── prometheus.css │ ├── bootstrap-4.3.1-dist │ │ ├── LICENSE │ │ └── css │ │ │ ├── bootstrap-reboot.min.css │ │ │ └── bootstrap-reboot.css │ ├── jquery-license │ └── functions.js └── template.html ├── .gitignore ├── .github ├── dependabot.yml ├── ISSUE_TEMPLATE.md └── workflows │ ├── golangci-lint.yml │ └── container_description.yml ├── .dockerignore ├── CODE_OF_CONDUCT.md ├── SECURITY.md ├── .gitattributes ├── Dockerfile ├── NOTICE ├── .yamllint ├── .promu.yml ├── asset ├── asset.go ├── doc.go └── asset_generate.go ├── Makefile ├── CONTRIBUTING.md ├── .circleci └── config.yml ├── handler ├── wipe.go ├── status_test.go ├── metrics.go ├── misc_test.go ├── delete.go ├── misc.go ├── status.go └── push.go ├── testutil └── metric_families.go ├── go.mod ├── README-containers.md ├── histogram └── prometheus_model.go ├── storage ├── interface.go └── diskmetricstore.go ├── api └── v1 │ ├── api.go │ └── api_test.go ├── go.sum ├── CHANGELOG.md ├── Makefile.common ├── main.go └── LICENSE /VERSION: -------------------------------------------------------------------------------- 1 | 1.11.2 2 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | * Björn Rabenstein @beorn7 2 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | default: none 4 | enable: 5 | - staticcheck 6 | -------------------------------------------------------------------------------- /resources/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prometheus/pushgateway/HEAD/resources/static/favicon.ico -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /pushgateway 2 | /.build 3 | /.release 4 | /.tarballs 5 | *.test 6 | *~ 7 | *.exe 8 | *.tar.gz 9 | /vendor 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .build/ 2 | .tarballs/ 3 | 4 | !.build/linux-amd64/ 5 | !.build/linux-armv7 6 | !.build/linux-arm64 7 | !.build/linux-ppc64le 8 | !.build/linux-s390x 9 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Prometheus Community Code of Conduct 2 | 3 | Prometheus follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/main/code-of-conduct.md). 4 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting a security issue 2 | 3 | The Prometheus security policy, including how to report vulnerabilities, can be 4 | found here: 5 | 6 | 7 | -------------------------------------------------------------------------------- /resources/static/bootstrap4-glyphicons/fonts/fontawesome/fa-brands-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prometheus/pushgateway/HEAD/resources/static/bootstrap4-glyphicons/fonts/fontawesome/fa-brands-400.eot -------------------------------------------------------------------------------- /resources/static/bootstrap4-glyphicons/fonts/fontawesome/fa-brands-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prometheus/pushgateway/HEAD/resources/static/bootstrap4-glyphicons/fonts/fontawesome/fa-brands-400.ttf -------------------------------------------------------------------------------- /resources/static/bootstrap4-glyphicons/fonts/fontawesome/fa-brands-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prometheus/pushgateway/HEAD/resources/static/bootstrap4-glyphicons/fonts/fontawesome/fa-brands-400.woff -------------------------------------------------------------------------------- /resources/static/bootstrap4-glyphicons/fonts/fontawesome/fa-regular-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prometheus/pushgateway/HEAD/resources/static/bootstrap4-glyphicons/fonts/fontawesome/fa-regular-400.eot -------------------------------------------------------------------------------- /resources/static/bootstrap4-glyphicons/fonts/fontawesome/fa-regular-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prometheus/pushgateway/HEAD/resources/static/bootstrap4-glyphicons/fonts/fontawesome/fa-regular-400.ttf -------------------------------------------------------------------------------- /resources/static/bootstrap4-glyphicons/fonts/fontawesome/fa-solid-900.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prometheus/pushgateway/HEAD/resources/static/bootstrap4-glyphicons/fonts/fontawesome/fa-solid-900.eot -------------------------------------------------------------------------------- /resources/static/bootstrap4-glyphicons/fonts/fontawesome/fa-solid-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prometheus/pushgateway/HEAD/resources/static/bootstrap4-glyphicons/fonts/fontawesome/fa-solid-900.ttf -------------------------------------------------------------------------------- /resources/static/bootstrap4-glyphicons/fonts/fontawesome/fa-solid-900.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prometheus/pushgateway/HEAD/resources/static/bootstrap4-glyphicons/fonts/fontawesome/fa-solid-900.woff -------------------------------------------------------------------------------- /resources/static/bootstrap4-glyphicons/fonts/fontawesome/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prometheus/pushgateway/HEAD/resources/static/bootstrap4-glyphicons/fonts/fontawesome/fa-solid-900.woff2 -------------------------------------------------------------------------------- /resources/static/bootstrap4-glyphicons/fonts/fontawesome/fa-brands-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prometheus/pushgateway/HEAD/resources/static/bootstrap4-glyphicons/fonts/fontawesome/fa-brands-400.woff2 -------------------------------------------------------------------------------- /resources/static/bootstrap4-glyphicons/fonts/fontawesome/fa-regular-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prometheus/pushgateway/HEAD/resources/static/bootstrap4-glyphicons/fonts/fontawesome/fa-regular-400.woff -------------------------------------------------------------------------------- /resources/static/bootstrap4-glyphicons/fonts/fontawesome/fa-regular-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prometheus/pushgateway/HEAD/resources/static/bootstrap4-glyphicons/fonts/fontawesome/fa-regular-400.woff2 -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | resources/static/bootstrap-4.3.1-dist/* linguist-vendored 2 | resources/static/bootstrap4-glyphicons/* linguist-vendored 3 | resources/static/jquery-3.4.1.min.js linguist-vendored 4 | vendor/* linguist-vendored 5 | -------------------------------------------------------------------------------- /resources/static/bootstrap4-glyphicons/fonts/glyphicons/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prometheus/pushgateway/HEAD/resources/static/bootstrap4-glyphicons/fonts/glyphicons/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /resources/static/bootstrap4-glyphicons/fonts/glyphicons/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prometheus/pushgateway/HEAD/resources/static/bootstrap4-glyphicons/fonts/glyphicons/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /resources/static/bootstrap4-glyphicons/fonts/glyphicons/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prometheus/pushgateway/HEAD/resources/static/bootstrap4-glyphicons/fonts/glyphicons/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /resources/static/bootstrap4-glyphicons/fonts/glyphicons/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prometheus/pushgateway/HEAD/resources/static/bootstrap4-glyphicons/fonts/glyphicons/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG ARCH="amd64" 2 | ARG OS="linux" 3 | FROM quay.io/prometheus/busybox-${OS}-${ARCH}:latest 4 | LABEL maintainer="The Prometheus Authors " 5 | 6 | ARG ARCH="amd64" 7 | ARG OS="linux" 8 | COPY --chown=nobody:nobody .build/${OS}-${ARCH}/pushgateway /bin/pushgateway 9 | 10 | EXPOSE 9091 11 | RUN mkdir -p /pushgateway && chown nobody:nobody /pushgateway 12 | WORKDIR /pushgateway 13 | 14 | USER 65534 15 | 16 | ENTRYPOINT [ "/bin/pushgateway" ] 17 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Pushgateway for ephemeral and batch jobs 2 | Copyright 2014-2015 The Prometheus Authors 3 | 4 | This product includes software developed at 5 | SoundCloud Ltd. (http://soundcloud.com/). 6 | 7 | 8 | The following components are included in this product: 9 | 10 | jQuery 11 | https://jquery.org 12 | Copyright jQuery Foundation and other contributors 13 | Licensed under the MIT License 14 | 15 | Bootstrap 16 | http://getbootstrap.com 17 | Copyright (c) 2011-2019 Twitter, Inc. 18 | Copyright (c) 2011-2019 The Bootstrap Authors 19 | Licensed under the MIT License 20 | -------------------------------------------------------------------------------- /.yamllint: -------------------------------------------------------------------------------- 1 | --- 2 | extends: default 3 | ignore: | 4 | **/node_modules 5 | 6 | rules: 7 | braces: 8 | max-spaces-inside: 1 9 | level: error 10 | brackets: 11 | max-spaces-inside: 1 12 | level: error 13 | commas: disable 14 | comments: disable 15 | comments-indentation: disable 16 | document-start: disable 17 | indentation: 18 | spaces: consistent 19 | indent-sequences: consistent 20 | key-duplicates: 21 | ignore: | 22 | config/testdata/section_key_dup.bad.yml 23 | line-length: disable 24 | truthy: 25 | check-keys: false 26 | -------------------------------------------------------------------------------- /.promu.yml: -------------------------------------------------------------------------------- 1 | --- 2 | go: 3 | # Whenever the Go version is updated here, .circle/config.yml should also 4 | # be updated. 5 | version: 1.25 6 | repository: 7 | path: github.com/prometheus/pushgateway 8 | build: 9 | ldflags: | 10 | -X github.com/prometheus/common/version.Version={{.Version}} 11 | -X github.com/prometheus/common/version.Revision={{.Revision}} 12 | -X github.com/prometheus/common/version.Branch={{.Branch}} 13 | -X github.com/prometheus/common/version.BuildUser={{user}}@{{host}} 14 | -X github.com/prometheus/common/version.BuildDate={{date "20060102-15:04:05"}} 15 | tarball: 16 | files: 17 | - LICENSE 18 | - NOTICE 19 | -------------------------------------------------------------------------------- /asset/asset.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | //go:build dev 15 | // +build dev 16 | 17 | package asset 18 | 19 | import ( 20 | "net/http" 21 | ) 22 | 23 | // Assets contains the project's assets. 24 | var Assets http.FileSystem = http.Dir("../resources") 25 | -------------------------------------------------------------------------------- /resources/static/prometheus.css: -------------------------------------------------------------------------------- 1 | /* Move down content because we have a fixed navbar that is 50px tall with 20px padding */ 2 | body { 3 | padding-top: 70px; 4 | padding-bottom: 20px; 5 | } 6 | 7 | .state_indicator { 8 | padding: 0 4px 0 4px; 9 | } 10 | 11 | .literal_output td { 12 | font-family: monospace; 13 | } 14 | 15 | .cursor-pointer { 16 | cursor: pointer; 17 | } 18 | 19 | .tooltip-inner { 20 | max-width: none; 21 | text-align: left; 22 | } 23 | 24 | .label { 25 | white-space: normal; 26 | } 27 | 28 | /* The navbar adds horizontal padding already */ 29 | .navbar .container-fluid { 30 | padding: 0; 31 | } 32 | 33 | /* This class provides style for containers that hold card like (without background) objects within container-fluid and out of accordion */ 34 | .blank-card { 35 | padding-bottom: 50px; 36 | padding-right: 25px; 37 | } 38 | -------------------------------------------------------------------------------- /asset/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // Package asset provides the assets via a virtual filesystem. 15 | package asset 16 | 17 | import ( 18 | // The blank import is to make govendor happy. 19 | _ "github.com/shurcooL/vfsgen" 20 | ) 21 | 22 | //go:generate go run -tags=dev asset_generate.go 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | ## Feature request 10 | **Use case. Why is this important?** 11 | 12 | *“Nice to have” is not a good use case. :)* 13 | 14 | ## Bug Report 15 | **What did you do?** 16 | 17 | **What did you expect to see?** 18 | 19 | **What did you see instead? Under which circumstances?** 20 | 21 | **Environment** 22 | 23 | * System information: 24 | 25 | Insert output of `uname -srm` here. 26 | 27 | * Pushgateway version: 28 | 29 | Insert output of `pushgateway --version` here. 30 | 31 | * Pushgateway command line: 32 | 33 | Insert full command line. 34 | 35 | * Logs: 36 | ``` 37 | Insert Pushgateway logs relevant to the issue here. 38 | ``` 39 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2016 The Prometheus Authors 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | 14 | # Needs to be defined before including Makefile.common to auto-generate targets 15 | DOCKER_ARCHS ?= amd64 armv7 arm64 ppc64le s390x 16 | 17 | include Makefile.common 18 | 19 | DOCKER_IMAGE_NAME ?= pushgateway 20 | 21 | assets: 22 | @echo ">> writing assets" 23 | @cd $(PREFIX)/asset && GO111MODULE=$(GO111MODULE) $(GO) generate && $(GOFMT) -w assets_vfsdata.go 24 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Prometheus uses GitHub to manage reviews of pull requests. 4 | 5 | * If you have a trivial fix or improvement, go ahead and create a pull request, 6 | addressing (with `@...`) the maintainer of this repository (see 7 | [MAINTAINERS.md](MAINTAINERS.md)) in the description of the pull request. 8 | 9 | * If you plan to do something more involved, first discuss your ideas 10 | on our [mailing list](https://groups.google.com/forum/?fromgroups#!forum/prometheus-developers). 11 | This will avoid unnecessary work and surely give you and us a good deal 12 | of inspiration. 13 | 14 | * Relevant coding style guidelines are the [Go Code Review 15 | Comments](https://code.google.com/p/go-wiki/wiki/CodeReviewComments) 16 | and the _Formatting and style_ section of Peter Bourgon's [Go: Best 17 | Practices for Production 18 | Environments](http://peter.bourgon.org/go-in-production/#formatting-and-style). 19 | 20 | * Be sure to sign off on the [DCO](https://github.com/probot/dco#how-it-works) 21 | -------------------------------------------------------------------------------- /asset/asset_generate.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | //go:build ignore 15 | // +build ignore 16 | 17 | package main 18 | 19 | import ( 20 | "log" 21 | 22 | "github.com/prometheus/pushgateway/asset" 23 | "github.com/shurcooL/vfsgen" 24 | ) 25 | 26 | func main() { 27 | err := vfsgen.Generate(asset.Assets, vfsgen.Options{ 28 | PackageName: "asset", 29 | BuildTags: "!dev", 30 | VariableName: "Assets", 31 | }) 32 | if err != nil { 33 | log.Fatalln(err) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /resources/static/bootstrap-4.3.1-dist/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2011-2019 Twitter, Inc. 4 | Copyright (c) 2011-2019 The Bootstrap Authors 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2.1 3 | orbs: 4 | prometheus: "prometheus/prometheus@0.17.1" 5 | executors: 6 | # Whenever the Go version is updated here, .promu.yml should also be updated. 7 | golang: 8 | docker: 9 | - image: cimg/go:1.25 10 | jobs: 11 | test: 12 | executor: golang 13 | steps: 14 | - prometheus/setup_environment 15 | - run: make 16 | - prometheus/store_artifact: 17 | file: pushgateway 18 | workflows: 19 | version: 2 20 | pushgateway: 21 | jobs: 22 | - test: 23 | filters: 24 | tags: 25 | only: /.*/ 26 | - prometheus/build: 27 | name: build 28 | filters: 29 | tags: 30 | only: /.*/ 31 | - prometheus/publish_master: 32 | context: org-context 33 | requires: 34 | - test 35 | - build 36 | filters: 37 | branches: 38 | only: master 39 | - prometheus/publish_release: 40 | context: org-context 41 | requires: 42 | - test 43 | - build 44 | filters: 45 | tags: 46 | only: /^v[0-9]+(\.[0-9]+){2}(-.+|[^-.]*)$/ 47 | branches: 48 | ignore: /.*/ 49 | -------------------------------------------------------------------------------- /handler/wipe.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package handler 15 | 16 | import ( 17 | "log/slog" 18 | "net/http" 19 | "time" 20 | 21 | "github.com/prometheus/pushgateway/storage" 22 | ) 23 | 24 | // WipeMetricStore deletes all the metrics in MetricStore. 25 | // 26 | // The returned handler is already instrumented for Prometheus. 27 | func WipeMetricStore( 28 | ms storage.MetricStore, 29 | logger *slog.Logger) http.Handler { 30 | 31 | return InstrumentWithCounter( 32 | "wipe", 33 | http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { 34 | w.WriteHeader(http.StatusAccepted) 35 | logger.Debug("start wiping metric store") 36 | // Delete all metric groups by sending write requests with MetricFamilies equal to nil. 37 | for _, group := range ms.GetMetricFamiliesMap() { 38 | ms.SubmitWriteRequest(storage.WriteRequest{ 39 | Labels: group.Labels, 40 | Timestamp: time.Now(), 41 | }) 42 | } 43 | 44 | })) 45 | } 46 | -------------------------------------------------------------------------------- /testutil/metric_families.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | package testutil 14 | 15 | import ( 16 | //nolint:staticcheck // Ignore SA1019. Dependencies use the deprecated package, so we have to, too. 17 | "github.com/golang/protobuf/proto" 18 | 19 | dto "github.com/prometheus/client_model/go" 20 | ) 21 | 22 | // MetricFamiliesMap creates the map needed in the MetricFamilies field of a 23 | // WriteRequest from the provided reference metric families. While doing so, it 24 | // creates deep copies of the metric families so that modifications that might 25 | // happen during processing of the WriteRequest will not affect the reference 26 | // metric families. 27 | func MetricFamiliesMap(mfs ...*dto.MetricFamily) map[string]*dto.MetricFamily { 28 | m := map[string]*dto.MetricFamily{} 29 | for _, mf := range mfs { 30 | buf, err := proto.Marshal(mf) 31 | if err != nil { 32 | panic(err) 33 | } 34 | mfCopy := &dto.MetricFamily{} 35 | if err := proto.Unmarshal(buf, mfCopy); err != nil { 36 | panic(err) 37 | } 38 | m[mf.GetName()] = mfCopy 39 | } 40 | return m 41 | } 42 | -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # This action is synced from https://github.com/prometheus/prometheus 3 | name: golangci-lint 4 | on: 5 | push: 6 | paths: 7 | - "go.sum" 8 | - "go.mod" 9 | - "**.go" 10 | - "scripts/errcheck_excludes.txt" 11 | - ".github/workflows/golangci-lint.yml" 12 | - ".golangci.yml" 13 | pull_request: 14 | 15 | permissions: # added using https://github.com/step-security/secure-repo 16 | contents: read 17 | 18 | jobs: 19 | golangci: 20 | permissions: 21 | contents: read # for actions/checkout to fetch code 22 | pull-requests: read # for golangci/golangci-lint-action to fetch pull requests 23 | name: lint 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: Checkout repository 27 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 28 | with: 29 | persist-credentials: false 30 | - name: Install Go 31 | uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 32 | with: 33 | go-version: 1.25.x 34 | - name: Install snmp_exporter/generator dependencies 35 | run: sudo apt-get update && sudo apt-get -y install libsnmp-dev 36 | if: github.repository == 'prometheus/snmp_exporter' 37 | - name: Get golangci-lint version 38 | id: golangci-lint-version 39 | run: echo "version=$(make print-golangci-lint-version)" >> $GITHUB_OUTPUT 40 | - name: Lint 41 | uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0 42 | with: 43 | args: --verbose 44 | version: ${{ steps.golangci-lint-version.outputs.version }} 45 | -------------------------------------------------------------------------------- /resources/static/jquery-license: -------------------------------------------------------------------------------- 1 | Copyright jQuery Foundation and other contributors, https://jquery.org/ 2 | 3 | This software consists of voluntary contributions made by many 4 | individuals. For exact contribution history, see the revision history 5 | available at https://github.com/jquery/jquery 6 | 7 | The following license applies to all parts of this software except as 8 | documented below: 9 | 10 | ==== 11 | 12 | Permission is hereby granted, free of charge, to any person obtaining 13 | a copy of this software and associated documentation files (the 14 | "Software"), to deal in the Software without restriction, including 15 | without limitation the rights to use, copy, modify, merge, publish, 16 | distribute, sublicense, and/or sell copies of the Software, and to 17 | permit persons to whom the Software is furnished to do so, subject to 18 | the following conditions: 19 | 20 | The above copyright notice and this permission notice shall be 21 | included in all copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 24 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 25 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 26 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 27 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 28 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 29 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 30 | 31 | ==== 32 | 33 | All files located in the node_modules and external directories are 34 | externally maintained libraries used by this software which have their 35 | own licenses; we recommend you read them, as their terms may differ from 36 | the terms above. 37 | -------------------------------------------------------------------------------- /handler/status_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package handler 15 | 16 | import ( 17 | "io" 18 | "net/http" 19 | "net/http/httptest" 20 | "strings" 21 | "testing" 22 | "time" 23 | 24 | "github.com/prometheus/pushgateway/asset" 25 | "github.com/prometheus/pushgateway/storage" 26 | ) 27 | 28 | func TestPathPrefixPresenceInPage(t *testing.T) { 29 | flags := map[string]string{ 30 | "web.listen-address": ":9091", 31 | "web.telemetry-path": "/metrics", 32 | "web.external-url": "http://web-external-url.com", 33 | } 34 | pathPrefix := "/foobar" 35 | 36 | ms := storage.NewDiskMetricStore("", time.Minute, nil, logger) 37 | status := Status(ms, asset.Assets, flags, pathPrefix, logger) 38 | defer ms.Shutdown() 39 | 40 | w := httptest.NewRecorder() 41 | status.ServeHTTP(w, &http.Request{}) 42 | 43 | if http.StatusOK != w.Code { 44 | t.Fatalf("Wanted status %d, got %d", http.StatusOK, w.Code) 45 | } 46 | 47 | rawBody, err := io.ReadAll(w.Result().Body) 48 | if err != nil { 49 | t.Fatal(err) 50 | } 51 | body := string(rawBody) 52 | 53 | if !strings.Contains(body, pathPrefix+"/static") { 54 | t.Errorf("Body does not contain %q.", pathPrefix+"/static") 55 | t.Log(body) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/prometheus/pushgateway 2 | 3 | go 1.24.0 4 | 5 | toolchain go1.24.1 6 | 7 | require ( 8 | github.com/alecthomas/kingpin/v2 v2.4.0 9 | github.com/golang/protobuf v1.5.4 10 | github.com/golang/snappy v1.0.0 11 | github.com/prometheus/client_golang v1.23.2 12 | github.com/prometheus/client_model v0.6.2 13 | github.com/prometheus/common v0.67.4 14 | github.com/prometheus/exporter-toolkit v0.15.0 15 | github.com/shurcooL/vfsgen v0.0.0-20230704071429-0000e147ea92 16 | google.golang.org/protobuf v1.36.10 17 | ) 18 | 19 | require ( 20 | github.com/golang-jwt/jwt/v5 v5.3.0 // indirect 21 | github.com/google/uuid v1.6.0 // indirect 22 | github.com/mdlayher/socket v0.4.1 // indirect 23 | github.com/mdlayher/vsock v1.2.1 // indirect 24 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 25 | go.yaml.in/yaml/v2 v2.4.3 // indirect 26 | golang.org/x/crypto v0.45.0 // indirect 27 | golang.org/x/net v0.47.0 // indirect 28 | golang.org/x/oauth2 v0.32.0 // indirect 29 | golang.org/x/time v0.13.0 // indirect 30 | golang.org/x/tools/godoc v0.1.0-deprecated // indirect 31 | ) 32 | 33 | require ( 34 | github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect 35 | github.com/beorn7/perks v1.0.1 // indirect 36 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 37 | github.com/coreos/go-systemd/v22 v22.6.0 // indirect 38 | github.com/jpillora/backoff v1.0.0 // indirect 39 | github.com/julienschmidt/httprouter v1.3.0 // indirect 40 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect 41 | github.com/prometheus/procfs v0.16.1 // indirect 42 | github.com/prometheus/prometheus v0.307.3 43 | github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c // indirect 44 | github.com/xhit/go-str2duration/v2 v2.1.0 // indirect 45 | golang.org/x/sync v0.18.0 // indirect 46 | golang.org/x/sys v0.38.0 // indirect 47 | golang.org/x/text v0.31.0 // indirect 48 | ) 49 | -------------------------------------------------------------------------------- /handler/metrics.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package handler 15 | 16 | import ( 17 | "net/http" 18 | 19 | "github.com/prometheus/client_golang/prometheus" 20 | "github.com/prometheus/client_golang/prometheus/promauto" 21 | "github.com/prometheus/client_golang/prometheus/promhttp" 22 | ) 23 | 24 | var ( 25 | httpCnt = promauto.NewCounterVec( 26 | prometheus.CounterOpts{ 27 | Name: "pushgateway_http_requests_total", 28 | Help: "Total HTTP requests processed by the Pushgateway, excluding scrapes.", 29 | }, 30 | []string{"handler", "code", "method"}, 31 | ) 32 | httpPushSize = promauto.NewSummaryVec( 33 | prometheus.SummaryOpts{ 34 | Name: "pushgateway_http_push_size_bytes", 35 | Help: "HTTP request size for pushes to the Pushgateway.", 36 | Objectives: map[float64]float64{0.1: 0.01, 0.5: 0.05, 0.9: 0.01}, 37 | }, 38 | []string{"method"}, 39 | ) 40 | httpPushDuration = promauto.NewSummaryVec( 41 | prometheus.SummaryOpts{ 42 | Name: "pushgateway_http_push_duration_seconds", 43 | Help: "HTTP request duration for pushes to the Pushgateway.", 44 | Objectives: map[float64]float64{0.1: 0.01, 0.5: 0.05, 0.9: 0.01}, 45 | }, 46 | []string{"method"}, 47 | ) 48 | ) 49 | 50 | func InstrumentWithCounter(handlerName string, handler http.Handler) http.HandlerFunc { 51 | return promhttp.InstrumentHandlerCounter( 52 | httpCnt.MustCurryWith(prometheus.Labels{"handler": handlerName}), 53 | handler, 54 | ) 55 | } 56 | -------------------------------------------------------------------------------- /handler/misc_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package handler 15 | 16 | import ( 17 | "fmt" 18 | "log" 19 | "net/http" 20 | "net/http/httptest" 21 | "os" 22 | "testing" 23 | ) 24 | 25 | type fakeFileSystem struct { 26 | files map[string]struct{} 27 | } 28 | 29 | // Open implements the http.FileSystem interface 30 | // 31 | // If a file is present, no error will be returned. 32 | // This implementation always returns a nil File. 33 | func (f *fakeFileSystem) Open(name string) (http.File, error) { 34 | log.Println("requesting" + name) 35 | 36 | if _, ok := f.files[name]; !ok { 37 | return nil, os.ErrNotExist 38 | } 39 | return os.Open("misc_test.go") 40 | // return nil, nil 41 | } 42 | 43 | func TestRoutePrefixForStatic(t *testing.T) { 44 | fs := &fakeFileSystem{map[string]struct{}{ 45 | "/index.js": struct{}{}, 46 | }} 47 | 48 | for _, test := range []struct { 49 | prefix string 50 | path string 51 | code int 52 | }{ 53 | {"/", "/index.js", 200}, 54 | {"/", "/missing.js", 404}, 55 | {"/route-prefix", "/index.js", 200}, 56 | {"/route-prefix", "/missing.js", 404}, 57 | } { 58 | t.Run(fmt.Sprintf("%v", test), func(t *testing.T) { 59 | t.Parallel() 60 | req, err := http.NewRequest( 61 | http.MethodGet, "http://example.com"+test.prefix+test.path, nil, 62 | ) 63 | if err != nil { 64 | t.Fatal(err) 65 | } 66 | w := httptest.NewRecorder() 67 | static := Static(fs, test.prefix) 68 | static.ServeHTTP(w, req) 69 | if test.code != w.Code { 70 | t.Errorf("Wanted %d, got %d.", test.code, w.Code) 71 | } 72 | }) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /handler/delete.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package handler 15 | 16 | import ( 17 | "fmt" 18 | "log/slog" 19 | "net/http" 20 | "time" 21 | 22 | "github.com/prometheus/common/route" 23 | 24 | "github.com/prometheus/pushgateway/storage" 25 | ) 26 | 27 | // Delete returns a handler that accepts delete requests. 28 | // 29 | // The returned handler is already instrumented for Prometheus. 30 | func Delete(ms storage.MetricStore, jobBase64Encoded bool, logger *slog.Logger) func(http.ResponseWriter, *http.Request) { 31 | instrumentedHandler := InstrumentWithCounter( 32 | "delete", 33 | http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 34 | job := route.Param(r.Context(), "job") 35 | if jobBase64Encoded { 36 | var err error 37 | if job, err = decodeBase64(job); err != nil { 38 | http.Error(w, fmt.Sprintf("invalid base64 encoding in job name %q: %v", job, err), http.StatusBadRequest) 39 | logger.Debug("invalid base64 encoding in job name", "job", job, "err", err.Error()) 40 | return 41 | } 42 | } 43 | labelsString := route.Param(r.Context(), "labels") 44 | labels, err := splitLabels(labelsString) 45 | if err != nil { 46 | http.Error(w, err.Error(), http.StatusBadRequest) 47 | logger.Debug("failed to parse URL", "url", labelsString, "err", err.Error()) 48 | return 49 | } 50 | if job == "" { 51 | http.Error(w, "job name is required", http.StatusBadRequest) 52 | logger.Debug("job name is required") 53 | return 54 | } 55 | labels["job"] = job 56 | ms.SubmitWriteRequest(storage.WriteRequest{ 57 | Labels: labels, 58 | Timestamp: time.Now(), 59 | }) 60 | w.WriteHeader(http.StatusAccepted) 61 | }), 62 | ) 63 | 64 | return func(w http.ResponseWriter, r *http.Request) { 65 | instrumentedHandler.ServeHTTP(w, r) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /handler/misc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package handler 15 | 16 | import ( 17 | "io" 18 | "net/http" 19 | 20 | "github.com/prometheus/common/server" 21 | 22 | "github.com/prometheus/pushgateway/storage" 23 | ) 24 | 25 | // Healthy is used to report the health of the Pushgateway. It currently only 26 | // uses the Healthy method of the MetricScore to detect healthy state. 27 | // 28 | // The returned handler is already instrumented for Prometheus. 29 | func Healthy(ms storage.MetricStore) http.Handler { 30 | return InstrumentWithCounter( 31 | "healthy", 32 | http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { 33 | err := ms.Healthy() 34 | if err == nil { 35 | io.WriteString(w, "OK") 36 | } else { 37 | http.Error(w, err.Error(), http.StatusInternalServerError) 38 | } 39 | }), 40 | ) 41 | } 42 | 43 | // Ready is used to report if the Pushgateway is ready to process requests. It 44 | // currently only uses the Ready method of the MetricScore to detect ready 45 | // state. 46 | // 47 | // The returned handler is already instrumented for Prometheus. 48 | func Ready(ms storage.MetricStore) http.Handler { 49 | return InstrumentWithCounter( 50 | "ready", 51 | http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { 52 | err := ms.Ready() 53 | if err == nil { 54 | io.WriteString(w, "OK") 55 | } else { 56 | http.Error(w, err.Error(), http.StatusInternalServerError) 57 | } 58 | }), 59 | ) 60 | } 61 | 62 | // Static serves the static files from the provided http.FileSystem. 63 | // 64 | // The returned handler is already instrumented for Prometheus. 65 | func Static(root http.FileSystem, prefix string) http.Handler { 66 | if prefix == "/" { 67 | prefix = "" 68 | } 69 | 70 | handler := server.StaticFileServer(root) 71 | return InstrumentWithCounter( 72 | "static", 73 | http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 74 | r.URL.Path = r.URL.Path[len(prefix):] 75 | handler.ServeHTTP(w, r) 76 | }), 77 | ) 78 | } 79 | -------------------------------------------------------------------------------- /.github/workflows/container_description.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Push README to Docker Hub 3 | on: 4 | push: 5 | paths: 6 | - "README.md" 7 | - "README-containers.md" 8 | - ".github/workflows/container_description.yml" 9 | branches: [ main, master ] 10 | 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | PushDockerHubReadme: 16 | runs-on: ubuntu-latest 17 | name: Push README to Docker Hub 18 | if: github.repository_owner == 'prometheus' || github.repository_owner == 'prometheus-community' # Don't run this workflow on forks. 19 | steps: 20 | - name: git checkout 21 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 22 | with: 23 | persist-credentials: false 24 | - name: Set docker hub repo name 25 | run: echo "DOCKER_REPO_NAME=$(make docker-repo-name)" >> $GITHUB_ENV 26 | - name: Push README to Dockerhub 27 | uses: christian-korneck/update-container-description-action@d36005551adeaba9698d8d67a296bd16fa91f8e8 # v1 28 | env: 29 | DOCKER_USER: ${{ secrets.DOCKER_HUB_LOGIN }} 30 | DOCKER_PASS: ${{ secrets.DOCKER_HUB_PASSWORD }} 31 | with: 32 | destination_container_repo: ${{ env.DOCKER_REPO_NAME }} 33 | provider: dockerhub 34 | short_description: ${{ env.DOCKER_REPO_NAME }} 35 | # Empty string results in README-containers.md being pushed if it 36 | # exists. Otherwise, README.md is pushed. 37 | readme_file: '' 38 | 39 | PushQuayIoReadme: 40 | runs-on: ubuntu-latest 41 | name: Push README to quay.io 42 | if: github.repository_owner == 'prometheus' || github.repository_owner == 'prometheus-community' # Don't run this workflow on forks. 43 | steps: 44 | - name: git checkout 45 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 46 | with: 47 | persist-credentials: false 48 | - name: Set quay.io org name 49 | run: echo "DOCKER_REPO=$(echo quay.io/${GITHUB_REPOSITORY_OWNER} | tr -d '-')" >> $GITHUB_ENV 50 | - name: Set quay.io repo name 51 | run: echo "DOCKER_REPO_NAME=$(make docker-repo-name)" >> $GITHUB_ENV 52 | - name: Push README to quay.io 53 | uses: christian-korneck/update-container-description-action@d36005551adeaba9698d8d67a296bd16fa91f8e8 # v1 54 | env: 55 | DOCKER_APIKEY: ${{ secrets.QUAY_IO_API_TOKEN }} 56 | with: 57 | destination_container_repo: ${{ env.DOCKER_REPO_NAME }} 58 | provider: quay 59 | # Empty string results in README-containers.md being pushed if it 60 | # exists. Otherwise, README.md is pushed. 61 | readme_file: '' 62 | -------------------------------------------------------------------------------- /README-containers.md: -------------------------------------------------------------------------------- 1 | # Prometheus Pushgateway 2 | 3 | [![CircleCI](https://circleci.com/gh/prometheus/pushgateway/tree/master.svg?style=shield)][circleci] 4 | [![Docker Repository on Quay](https://quay.io/repository/prometheus/pushgateway/status)][quay] 5 | [![Docker Pulls](https://img.shields.io/docker/pulls/prom/pushgateway.svg?maxAge=604800)][hub] 6 | 7 | The Prometheus Pushgateway exists to allow ephemeral and batch jobs to 8 | expose their metrics to Prometheus. Since these kinds of jobs may not 9 | exist long enough to be scraped, they can instead push their metrics 10 | to a Pushgateway. The Pushgateway then exposes these metrics to 11 | Prometheus. 12 | 13 | ## Non-goals 14 | 15 | First of all, the Pushgateway is not capable of turning Prometheus into a 16 | push-based monitoring system. For a general description of use cases for the 17 | Pushgateway, please read [When To Use The 18 | Pushgateway](https://prometheus.io/docs/practices/pushing/). 19 | 20 | The Pushgateway is explicitly not an _aggregator or distributed counter_ but 21 | rather a metrics cache. It does not have 22 | [statsd](https://github.com/etsy/statsd)-like semantics. The metrics pushed are 23 | exactly the same as you would present for scraping in a permanently running 24 | program. If you need distributed counting, you could either use the actual 25 | statsd in combination with the [Prometheus statsd 26 | exporter](https://github.com/prometheus/statsd_exporter), or have a look at the 27 | [prom-aggregation-gateway](https://github.com/zapier/prom-aggregation-gateway). With more 28 | experience gathered, the Prometheus project might one day be able to provide a 29 | native solution, separate from or possibly even as part of the Pushgateway. 30 | 31 | For machine-level metrics, the 32 | [textfile](https://github.com/prometheus/node_exporter/blob/master/README.md#textfile-collector) 33 | collector of the Node exporter is usually more appropriate. The Pushgateway is 34 | intended for service-level metrics. 35 | 36 | The Pushgateway is not an _event store_. While you can use Prometheus as a data 37 | source for 38 | [Grafana annotations](http://docs.grafana.org/reference/annotations/), tracking 39 | something like release events has to happen with some event-logging framework. 40 | 41 | A while ago, we 42 | [decided to not implement a “timeout” or TTL for pushed metrics](https://github.com/prometheus/pushgateway/issues/19) 43 | because almost all proposed use cases turned out to be anti-patterns we 44 | strongly discourage. You can follow a more recent discussion on the 45 | [prometheus-developers mailing list](https://groups.google.com/forum/#!topic/prometheus-developers/9IyUxRvhY7w). 46 | 47 | ## Run it 48 | 49 | Download binary releases for your platform from the 50 | [release page](https://github.com/prometheus/pushgateway/releases) and unpack 51 | the tarball. 52 | 53 | If you want to compile yourself from the sources, you need a working Go 54 | setup. Then use the provided Makefile (type `make`). 55 | 56 | For the most basic setup, just start the binary. To change the address 57 | to listen on, use the `--web.listen-address` flag (e.g. "0.0.0.0:9091" or ":9091"). 58 | By default, Pushgateway does not persist metrics. However, the `--persistence.file` flag 59 | allows you to specify a file in which the pushed metrics will be 60 | persisted (so that they survive restarts of the Pushgateway). 61 | 62 | ### Using Docker 63 | 64 | You can deploy the Pushgateway using the [prom/pushgateway](https://hub.docker.com/r/prom/pushgateway) Docker image. 65 | 66 | For example: 67 | 68 | ```bash 69 | docker pull prom/pushgateway 70 | 71 | docker run -d -p 9091:9091 prom/pushgateway 72 | ``` 73 | 74 | ## More documentation 75 | 76 | Please refer to the [full length 77 | README.md](https://github.com/prometheus/pushgateway/blob/master/README.md) for 78 | more documentation. 79 | -------------------------------------------------------------------------------- /resources/static/bootstrap-4.3.1-dist/css/bootstrap-reboot.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v4.3.1 (https://getbootstrap.com/) 3 | * Copyright 2011-2019 The Bootstrap Authors 4 | * Copyright 2011-2019 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) 7 | */*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important} 8 | /*# sourceMappingURL=bootstrap-reboot.min.css.map */ -------------------------------------------------------------------------------- /handler/status.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package handler 15 | 16 | import ( 17 | "encoding/base64" 18 | "fmt" 19 | "html" 20 | "html/template" 21 | "io" 22 | "log/slog" 23 | "net/http" 24 | "strconv" 25 | "time" 26 | 27 | "github.com/prometheus/common/version" 28 | 29 | dto "github.com/prometheus/client_model/go" 30 | 31 | "github.com/prometheus/pushgateway/histogram" 32 | "github.com/prometheus/pushgateway/storage" 33 | ) 34 | 35 | type data struct { 36 | MetricGroups storage.GroupingKeyToMetricGroup 37 | Flags map[string]string 38 | BuildInfo map[string]string 39 | Birth time.Time 40 | PathPrefix string 41 | counter int 42 | } 43 | 44 | func (d *data) Count() int { 45 | d.counter++ 46 | return d.counter 47 | } 48 | 49 | func (data) FormatTimestamp(ts int64) string { 50 | return time.Unix(ts/1000, ts%1000*1000000).String() 51 | } 52 | 53 | // Status serves the status page. 54 | // 55 | // The returned handler is already instrumented for Prometheus. 56 | func Status( 57 | ms storage.MetricStore, 58 | root http.FileSystem, 59 | flags map[string]string, 60 | pathPrefix string, 61 | logger *slog.Logger, 62 | ) http.Handler { 63 | birth := time.Now() 64 | return InstrumentWithCounter( 65 | "status", 66 | http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { 67 | t := template.New("status") 68 | t.Funcs(template.FuncMap{ 69 | "value": func(f float64) string { 70 | return strconv.FormatFloat(f, 'f', -1, 64) 71 | }, 72 | "timeFormat": func(t time.Time) string { 73 | return t.Format(time.RFC3339) 74 | }, 75 | "base64": func(s string) string { 76 | return base64.RawURLEncoding.EncodeToString([]byte(s)) 77 | }, 78 | "formatHistogram": func(m *dto.Histogram) string { 79 | h, fh := histogram.NewModelHistogram(m) 80 | if h == nil { 81 | return fh.String() 82 | } 83 | return h.String() 84 | }, 85 | "add": func(x, y int) int { 86 | return x + y 87 | }, 88 | "formatLabelName": func(s string) string { 89 | if !ValidationScheme.IsValidLabelName(s) { 90 | return fmt.Sprintf("%q", s) 91 | } 92 | return s 93 | }, 94 | }) 95 | 96 | f, err := root.Open("template.html") 97 | if err != nil { 98 | http.Error(w, err.Error(), http.StatusInternalServerError) 99 | logger.Error("error loading template.html", "err", err.Error()) 100 | return 101 | } 102 | defer f.Close() 103 | tpl, err := io.ReadAll(f) 104 | if err != nil { 105 | http.Error(w, err.Error(), http.StatusInternalServerError) 106 | logger.Error("error reading template.html", "err", err.Error()) 107 | return 108 | } 109 | _, err = t.Parse(string(tpl)) 110 | if err != nil { 111 | http.Error(w, err.Error(), http.StatusInternalServerError) 112 | logger.Error("error parsing template", "err", err.Error()) 113 | return 114 | } 115 | 116 | buildInfo := map[string]string{ 117 | "version": version.Version, 118 | "revision": version.Revision, 119 | "branch": version.Branch, 120 | "buildUser": version.BuildUser, 121 | "buildDate": version.BuildDate, 122 | "goVersion": version.GoVersion, 123 | } 124 | 125 | if pathPrefix == "" { 126 | pathPrefix = "." 127 | } 128 | 129 | d := &data{ 130 | MetricGroups: ms.GetMetricFamiliesMap(), 131 | BuildInfo: buildInfo, 132 | Birth: birth, 133 | PathPrefix: pathPrefix, 134 | Flags: flags, 135 | } 136 | 137 | err = t.Execute(w, d) 138 | if err != nil { 139 | // Hack to get a visible error message right at the top. 140 | fmt.Fprintf(w, `
Error executing template: %s
`, html.EscapeString(err.Error())) 141 | fmt.Fprintln(w, ``) 142 | } 143 | }), 144 | ) 145 | } 146 | -------------------------------------------------------------------------------- /resources/static/functions.js: -------------------------------------------------------------------------------- 1 | // Namespace. 2 | var pushgateway = {}; 3 | 4 | pushgateway.labels = {}; 5 | pushgateway.panel = null; 6 | 7 | pushgateway.switchToMetrics = function(){ 8 | $('#metrics-div').show(); 9 | $('#status-div').hide(); 10 | $('#metrics-li').addClass('active'); 11 | $('#status-li').removeClass('active'); 12 | } 13 | 14 | pushgateway.switchToStatus = function(){ 15 | $('#metrics-div').hide(); 16 | $('#status-div').show(); 17 | $('#metrics-li').removeClass('active'); 18 | $('#status-li').addClass('active'); 19 | } 20 | 21 | pushgateway.showDelModal = function(labels, labelsEncoded, panelID, event){ 22 | event.stopPropagation(); // Don't trigger accordion collapse. 23 | pushgateway.labels = labelsEncoded; 24 | pushgateway.panel = $('#' + panelID).parent(); 25 | 26 | var components = []; 27 | for (var ln in labels) { 28 | components.push(ln + '="' + labels[ln] + '"') 29 | } 30 | 31 | $('#del-modal-msg').text( 32 | 'Do you really want to delete all metrics of group {' + components.join(', ') + '}?' 33 | ); 34 | $('#del-modal').modal('show'); 35 | } 36 | 37 | pushgateway.showDelAllModal = function(){ 38 | if (!$('button#del-all').hasClass('disabled')) { 39 | $('#del-modal-all-msg').text( 40 | 'Do you really want to delete all metrics from all metric groups?' 41 | ); 42 | $('#del-all-modal').modal('show'); 43 | } 44 | } 45 | 46 | pushgateway.deleteGroup = function(){ 47 | var pathElements = []; 48 | for (var ln in pushgateway.labels) { 49 | if (ln != 'job') { 50 | pathElements.push(encodeURIComponent(ln+'@base64')); 51 | // Always add a padding '=' to ensure empty label values work. 52 | pathElements.push(encodeURIComponent(pushgateway.labels[ln]+'=')); 53 | } 54 | } 55 | var groupPath = pathElements.join('/'); 56 | if (groupPath.length > 0) { 57 | groupPath = '/' + groupPath; 58 | } 59 | 60 | $.ajax({ 61 | type: 'DELETE', 62 | url: 'metrics/job@base64/' + encodeURIComponent(pushgateway.labels['job']) + groupPath, 63 | success: function(data, textStatus, jqXHR) { 64 | pushgateway.panel.remove(); 65 | pushgateway.decreaseDelAllCounter(); 66 | $('#del-modal').modal('hide'); 67 | }, 68 | error: function(jqXHR, textStatus, error) { 69 | alert('Deleting metric group failed: ' + error); 70 | } 71 | }); 72 | } 73 | 74 | pushgateway.deleteAllGroup = function(){ 75 | $.ajax({ 76 | type: 'PUT', 77 | url: 'api/v1/admin/wipe', 78 | success: function(data, textStatus, jqXHR) { 79 | $('div').each(function() { 80 | id = $(this).attr("id"); 81 | if (typeof id != 'undefined' && id.match(/^group-panel-[0-9]{1,}$/)) { 82 | $(this).parent().remove(); 83 | } 84 | }); 85 | pushgateway.setDelAllCounter(0); 86 | $('#del-all-modal').modal('hide'); 87 | }, 88 | error: function(jqXHR, textStatus, error) { 89 | alert('Deleting all metric groups failed: ' + error); 90 | } 91 | }); 92 | } 93 | 94 | pushgateway.decreaseDelAllCounter = function(){ 95 | var counter = parseInt($('span#del-all-counter').text()); 96 | pushgateway.setDelAllCounter(--counter); 97 | } 98 | 99 | pushgateway.setDelAllCounter = function(n){ 100 | $('span#del-all-counter').text(n); 101 | if (n <= 0) { 102 | pushgateway.disableDelAllGroupButton(); 103 | return; 104 | } 105 | pushgateway.enableDelAllGroupButton(); 106 | } 107 | 108 | pushgateway.enableDelAllGroupButton = function(){ 109 | $('button#del-all').removeClass('disabled'); 110 | } 111 | 112 | pushgateway.disableDelAllGroupButton = function(){ 113 | $('button#del-all').addClass('disabled'); 114 | } 115 | 116 | $(function () { 117 | $('div.collapse').on('show.bs.collapse', function (event) { 118 | $(this).prev().find('span.toggle-icon') 119 | .removeClass('glyphicon-collapse-down') 120 | .addClass('glyphicon-collapse-up'); 121 | event.stopPropagation(); 122 | 123 | sessionStorage.setItem("coll_" + this.id, true); 124 | }) 125 | $('div.collapse').on('hide.bs.collapse', function (event) { 126 | $(this).prev().find('span.toggle-icon') 127 | .removeClass('glyphicon-collapse-up') 128 | .addClass('glyphicon-collapse-down'); 129 | event.stopPropagation(); 130 | 131 | sessionStorage.setItem("coll_" + this.id, false); 132 | }) 133 | 134 | $("div.collapse").each(function() { 135 | if (sessionStorage.getItem("coll_" + this.id) == "true") { 136 | $(this).collapse("show"); 137 | } 138 | }); 139 | }) 140 | -------------------------------------------------------------------------------- /histogram/prometheus_model.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | package histogram 14 | 15 | import ( 16 | "fmt" 17 | 18 | dto "github.com/prometheus/client_model/go" 19 | model "github.com/prometheus/prometheus/model/histogram" 20 | ) 21 | 22 | type APIBucket[BC model.BucketCount] struct { 23 | Boundaries uint64 24 | Lower, Upper float64 25 | Count BC 26 | } 27 | 28 | func NewModelHistogram(ch *dto.Histogram) (*model.Histogram, *model.FloatHistogram) { 29 | if ch.GetSampleCountFloat() > 0 || ch.GetZeroCountFloat() > 0 { 30 | // It is a float histogram. 31 | fh := model.FloatHistogram{ 32 | Count: ch.GetSampleCountFloat(), 33 | Sum: ch.GetSampleSum(), 34 | ZeroThreshold: ch.GetZeroThreshold(), 35 | ZeroCount: ch.GetZeroCountFloat(), 36 | Schema: ch.GetSchema(), 37 | PositiveSpans: make([]model.Span, len(ch.GetPositiveSpan())), 38 | PositiveBuckets: ch.GetPositiveCount(), 39 | NegativeSpans: make([]model.Span, len(ch.GetNegativeSpan())), 40 | NegativeBuckets: ch.GetNegativeCount(), 41 | } 42 | for i, span := range ch.GetPositiveSpan() { 43 | fh.PositiveSpans[i].Offset = span.GetOffset() 44 | fh.PositiveSpans[i].Length = span.GetLength() 45 | } 46 | for i, span := range ch.GetNegativeSpan() { 47 | fh.NegativeSpans[i].Offset = span.GetOffset() 48 | fh.NegativeSpans[i].Length = span.GetLength() 49 | } 50 | return nil, &fh 51 | } 52 | h := model.Histogram{ 53 | Count: ch.GetSampleCount(), 54 | Sum: ch.GetSampleSum(), 55 | ZeroThreshold: ch.GetZeroThreshold(), 56 | ZeroCount: ch.GetZeroCount(), 57 | Schema: ch.GetSchema(), 58 | PositiveSpans: make([]model.Span, len(ch.GetPositiveSpan())), 59 | PositiveBuckets: ch.GetPositiveDelta(), 60 | NegativeSpans: make([]model.Span, len(ch.GetNegativeSpan())), 61 | NegativeBuckets: ch.GetNegativeDelta(), 62 | } 63 | for i, span := range ch.GetPositiveSpan() { 64 | h.PositiveSpans[i].Offset = span.GetOffset() 65 | h.PositiveSpans[i].Length = span.GetLength() 66 | } 67 | for i, span := range ch.GetNegativeSpan() { 68 | h.NegativeSpans[i].Offset = span.GetOffset() 69 | h.NegativeSpans[i].Length = span.GetLength() 70 | } 71 | return &h, nil 72 | } 73 | 74 | func BucketsAsJson[BC model.BucketCount](buckets []APIBucket[BC]) [][]any { 75 | ret := make([][]any, len(buckets)) 76 | for i, b := range buckets { 77 | ret[i] = []any{b.Boundaries, fmt.Sprintf("%v", b.Lower), fmt.Sprintf("%v", b.Upper), fmt.Sprintf("%v", b.Count)} 78 | } 79 | return ret 80 | } 81 | 82 | func GetAPIBuckets(h *model.Histogram) []APIBucket[uint64] { 83 | var apiBuckets []APIBucket[uint64] 84 | var nBuckets []model.Bucket[uint64] 85 | for it := h.NegativeBucketIterator(); it.Next(); { 86 | bucket := it.At() 87 | if bucket.Count != 0 { 88 | nBuckets = append(nBuckets, it.At()) 89 | } 90 | } 91 | for i := len(nBuckets) - 1; i >= 0; i-- { 92 | apiBuckets = append(apiBuckets, makeBucket[uint64](nBuckets[i])) 93 | } 94 | 95 | if h.ZeroCount != 0 { 96 | apiBuckets = append(apiBuckets, makeBucket[uint64](h.ZeroBucket())) 97 | } 98 | 99 | for it := h.PositiveBucketIterator(); it.Next(); { 100 | bucket := it.At() 101 | if bucket.Count != 0 { 102 | apiBuckets = append(apiBuckets, makeBucket[uint64](bucket)) 103 | } 104 | } 105 | return apiBuckets 106 | } 107 | 108 | func GetAPIFloatBuckets(h *model.FloatHistogram) []APIBucket[float64] { 109 | var apiBuckets []APIBucket[float64] 110 | var nBuckets []model.Bucket[float64] 111 | for it := h.NegativeBucketIterator(); it.Next(); { 112 | bucket := it.At() 113 | if bucket.Count != 0 { 114 | nBuckets = append(nBuckets, it.At()) 115 | } 116 | } 117 | for i := len(nBuckets) - 1; i >= 0; i-- { 118 | apiBuckets = append(apiBuckets, makeBucket[float64](nBuckets[i])) 119 | } 120 | 121 | if h.ZeroCount != 0 { 122 | apiBuckets = append(apiBuckets, makeBucket[float64](h.ZeroBucket())) 123 | } 124 | 125 | for it := h.PositiveBucketIterator(); it.Next(); { 126 | bucket := it.At() 127 | if bucket.Count != 0 { 128 | apiBuckets = append(apiBuckets, makeBucket[float64](bucket)) 129 | } 130 | } 131 | return apiBuckets 132 | } 133 | 134 | func makeBucket[BC model.BucketCount](bucket model.Bucket[BC]) APIBucket[BC] { 135 | boundaries := uint64(2) // () Exclusive on both sides AKA open interval. 136 | if bucket.LowerInclusive { 137 | if bucket.UpperInclusive { 138 | boundaries = 3 // [] Inclusive on both sides AKA closed interval. 139 | } else { 140 | boundaries = 1 // [) Inclusive only on lower end AKA right open. 141 | } 142 | } else { 143 | if bucket.UpperInclusive { 144 | boundaries = 0 // (] Inclusive only on upper end AKA left open. 145 | } 146 | } 147 | return APIBucket[BC]{ 148 | Boundaries: boundaries, 149 | Lower: bucket.Lower, 150 | Upper: bucket.Upper, 151 | Count: bucket.Count, 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /resources/static/bootstrap-4.3.1-dist/css/bootstrap-reboot.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v4.3.1 (https://getbootstrap.com/) 3 | * Copyright 2011-2019 The Bootstrap Authors 4 | * Copyright 2011-2019 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) 7 | */ 8 | *, 9 | *::before, 10 | *::after { 11 | box-sizing: border-box; 12 | } 13 | 14 | html { 15 | font-family: sans-serif; 16 | line-height: 1.15; 17 | -webkit-text-size-adjust: 100%; 18 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 19 | } 20 | 21 | article, aside, figcaption, figure, footer, header, hgroup, main, nav, section { 22 | display: block; 23 | } 24 | 25 | body { 26 | margin: 0; 27 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 28 | font-size: 1rem; 29 | font-weight: 400; 30 | line-height: 1.5; 31 | color: #212529; 32 | text-align: left; 33 | background-color: #fff; 34 | } 35 | 36 | [tabindex="-1"]:focus { 37 | outline: 0 !important; 38 | } 39 | 40 | hr { 41 | box-sizing: content-box; 42 | height: 0; 43 | overflow: visible; 44 | } 45 | 46 | h1, h2, h3, h4, h5, h6 { 47 | margin-top: 0; 48 | margin-bottom: 0.5rem; 49 | } 50 | 51 | p { 52 | margin-top: 0; 53 | margin-bottom: 1rem; 54 | } 55 | 56 | abbr[title], 57 | abbr[data-original-title] { 58 | text-decoration: underline; 59 | -webkit-text-decoration: underline dotted; 60 | text-decoration: underline dotted; 61 | cursor: help; 62 | border-bottom: 0; 63 | -webkit-text-decoration-skip-ink: none; 64 | text-decoration-skip-ink: none; 65 | } 66 | 67 | address { 68 | margin-bottom: 1rem; 69 | font-style: normal; 70 | line-height: inherit; 71 | } 72 | 73 | ol, 74 | ul, 75 | dl { 76 | margin-top: 0; 77 | margin-bottom: 1rem; 78 | } 79 | 80 | ol ol, 81 | ul ul, 82 | ol ul, 83 | ul ol { 84 | margin-bottom: 0; 85 | } 86 | 87 | dt { 88 | font-weight: 700; 89 | } 90 | 91 | dd { 92 | margin-bottom: .5rem; 93 | margin-left: 0; 94 | } 95 | 96 | blockquote { 97 | margin: 0 0 1rem; 98 | } 99 | 100 | b, 101 | strong { 102 | font-weight: bolder; 103 | } 104 | 105 | small { 106 | font-size: 80%; 107 | } 108 | 109 | sub, 110 | sup { 111 | position: relative; 112 | font-size: 75%; 113 | line-height: 0; 114 | vertical-align: baseline; 115 | } 116 | 117 | sub { 118 | bottom: -.25em; 119 | } 120 | 121 | sup { 122 | top: -.5em; 123 | } 124 | 125 | a { 126 | color: #007bff; 127 | text-decoration: none; 128 | background-color: transparent; 129 | } 130 | 131 | a:hover { 132 | color: #0056b3; 133 | text-decoration: underline; 134 | } 135 | 136 | a:not([href]):not([tabindex]) { 137 | color: inherit; 138 | text-decoration: none; 139 | } 140 | 141 | a:not([href]):not([tabindex]):hover, a:not([href]):not([tabindex]):focus { 142 | color: inherit; 143 | text-decoration: none; 144 | } 145 | 146 | a:not([href]):not([tabindex]):focus { 147 | outline: 0; 148 | } 149 | 150 | pre, 151 | code, 152 | kbd, 153 | samp { 154 | font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 155 | font-size: 1em; 156 | } 157 | 158 | pre { 159 | margin-top: 0; 160 | margin-bottom: 1rem; 161 | overflow: auto; 162 | } 163 | 164 | figure { 165 | margin: 0 0 1rem; 166 | } 167 | 168 | img { 169 | vertical-align: middle; 170 | border-style: none; 171 | } 172 | 173 | svg { 174 | overflow: hidden; 175 | vertical-align: middle; 176 | } 177 | 178 | table { 179 | border-collapse: collapse; 180 | } 181 | 182 | caption { 183 | padding-top: 0.75rem; 184 | padding-bottom: 0.75rem; 185 | color: #6c757d; 186 | text-align: left; 187 | caption-side: bottom; 188 | } 189 | 190 | th { 191 | text-align: inherit; 192 | } 193 | 194 | label { 195 | display: inline-block; 196 | margin-bottom: 0.5rem; 197 | } 198 | 199 | button { 200 | border-radius: 0; 201 | } 202 | 203 | button:focus { 204 | outline: 1px dotted; 205 | outline: 5px auto -webkit-focus-ring-color; 206 | } 207 | 208 | input, 209 | button, 210 | select, 211 | optgroup, 212 | textarea { 213 | margin: 0; 214 | font-family: inherit; 215 | font-size: inherit; 216 | line-height: inherit; 217 | } 218 | 219 | button, 220 | input { 221 | overflow: visible; 222 | } 223 | 224 | button, 225 | select { 226 | text-transform: none; 227 | } 228 | 229 | select { 230 | word-wrap: normal; 231 | } 232 | 233 | button, 234 | [type="button"], 235 | [type="reset"], 236 | [type="submit"] { 237 | -webkit-appearance: button; 238 | } 239 | 240 | button:not(:disabled), 241 | [type="button"]:not(:disabled), 242 | [type="reset"]:not(:disabled), 243 | [type="submit"]:not(:disabled) { 244 | cursor: pointer; 245 | } 246 | 247 | button::-moz-focus-inner, 248 | [type="button"]::-moz-focus-inner, 249 | [type="reset"]::-moz-focus-inner, 250 | [type="submit"]::-moz-focus-inner { 251 | padding: 0; 252 | border-style: none; 253 | } 254 | 255 | input[type="radio"], 256 | input[type="checkbox"] { 257 | box-sizing: border-box; 258 | padding: 0; 259 | } 260 | 261 | input[type="date"], 262 | input[type="time"], 263 | input[type="datetime-local"], 264 | input[type="month"] { 265 | -webkit-appearance: listbox; 266 | } 267 | 268 | textarea { 269 | overflow: auto; 270 | resize: vertical; 271 | } 272 | 273 | fieldset { 274 | min-width: 0; 275 | padding: 0; 276 | margin: 0; 277 | border: 0; 278 | } 279 | 280 | legend { 281 | display: block; 282 | width: 100%; 283 | max-width: 100%; 284 | padding: 0; 285 | margin-bottom: .5rem; 286 | font-size: 1.5rem; 287 | line-height: inherit; 288 | color: inherit; 289 | white-space: normal; 290 | } 291 | 292 | progress { 293 | vertical-align: baseline; 294 | } 295 | 296 | [type="number"]::-webkit-inner-spin-button, 297 | [type="number"]::-webkit-outer-spin-button { 298 | height: auto; 299 | } 300 | 301 | [type="search"] { 302 | outline-offset: -2px; 303 | -webkit-appearance: none; 304 | } 305 | 306 | [type="search"]::-webkit-search-decoration { 307 | -webkit-appearance: none; 308 | } 309 | 310 | ::-webkit-file-upload-button { 311 | font: inherit; 312 | -webkit-appearance: button; 313 | } 314 | 315 | output { 316 | display: inline-block; 317 | } 318 | 319 | summary { 320 | display: list-item; 321 | cursor: pointer; 322 | } 323 | 324 | template { 325 | display: none; 326 | } 327 | 328 | [hidden] { 329 | display: none !important; 330 | } 331 | /*# sourceMappingURL=bootstrap-reboot.css.map */ -------------------------------------------------------------------------------- /handler/push.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package handler 15 | 16 | import ( 17 | "bufio" 18 | "encoding/base64" 19 | "fmt" 20 | "io" 21 | "log/slog" 22 | "mime" 23 | "net/http" 24 | "strings" 25 | "time" 26 | 27 | "github.com/prometheus/client_golang/prometheus/promhttp" 28 | "github.com/prometheus/common/expfmt" 29 | "github.com/prometheus/common/model" 30 | "github.com/prometheus/common/route" 31 | "google.golang.org/protobuf/encoding/protodelim" 32 | 33 | dto "github.com/prometheus/client_model/go" 34 | 35 | "github.com/prometheus/pushgateway/storage" 36 | ) 37 | 38 | const ( 39 | // Base64Suffix is appended to a label name in the request URL path to 40 | // mark the following label value as base64 encoded. 41 | Base64Suffix = "@base64" 42 | ) 43 | 44 | var ( 45 | // EscapingScheme is provided when unescaping label names in the 46 | // request URL path to define the escaping scheme that will be used. 47 | EscapingScheme = model.NoEscaping 48 | ValidationScheme = model.LegacyValidation 49 | ) 50 | 51 | // Push returns an http.Handler which accepts samples over HTTP and stores them 52 | // in the MetricStore. If replace is true, all metrics for the job and instance 53 | // given by the request are deleted before new ones are stored. If check is 54 | // true, the pushed metrics are immediately checked for consistency (with 55 | // existing metrics and themselves), and an inconsistent push is rejected with 56 | // http.StatusBadRequest. 57 | // 58 | // The returned handler is already instrumented for Prometheus. 59 | func Push( 60 | ms storage.MetricStore, 61 | replace, check, jobBase64Encoded bool, 62 | logger *slog.Logger, 63 | ) func(http.ResponseWriter, *http.Request) { 64 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 65 | job := route.Param(r.Context(), "job") 66 | if jobBase64Encoded { 67 | var err error 68 | if job, err = decodeBase64(job); err != nil { 69 | http.Error(w, fmt.Sprintf("invalid base64 encoding in job name %q: %v", job, err), http.StatusBadRequest) 70 | logger.Debug("invalid base64 encoding in job name", "job", job, "err", err.Error()) 71 | return 72 | } 73 | } 74 | labelsString := route.Param(r.Context(), "labels") 75 | labels, err := splitLabels(labelsString) 76 | if err != nil { 77 | http.Error(w, err.Error(), http.StatusBadRequest) 78 | logger.Debug("failed to parse URL", "url", labelsString, "err", err.Error()) 79 | return 80 | } 81 | if job == "" { 82 | http.Error(w, "job name is required", http.StatusBadRequest) 83 | logger.Debug("job name is required") 84 | return 85 | } 86 | labels["job"] = job 87 | 88 | var metricFamilies map[string]*dto.MetricFamily 89 | ctMediatype, ctParams, ctErr := mime.ParseMediaType(r.Header.Get("Content-Type")) 90 | if ctErr == nil && ctMediatype == "application/vnd.google.protobuf" && 91 | ctParams["encoding"] == "delimited" && 92 | ctParams["proto"] == "io.prometheus.client.MetricFamily" { 93 | metricFamilies = map[string]*dto.MetricFamily{} 94 | unmarshaler := protodelim.UnmarshalOptions{ 95 | MaxSize: -1, 96 | } 97 | in := bufio.NewReader(r.Body) 98 | for { 99 | mf := &dto.MetricFamily{} 100 | if err = unmarshaler.UnmarshalFrom(in, mf); err != nil { 101 | if err == io.EOF { 102 | err = nil 103 | } 104 | break 105 | } 106 | metricFamilies[mf.GetName()] = mf 107 | } 108 | } else { 109 | // We could do further content-type checks here, but the 110 | // fallback for now will anyway be the text format 111 | // version 0.0.4, so just go for it and see if it works. 112 | parser := expfmt.NewTextParser(ValidationScheme) 113 | metricFamilies, err = parser.TextToMetricFamilies(r.Body) 114 | } 115 | if err != nil { 116 | http.Error(w, err.Error(), http.StatusBadRequest) 117 | logger.Debug("failed to parse text", "source", r.RemoteAddr, "err", err.Error()) 118 | return 119 | } 120 | now := time.Now() 121 | if !check { 122 | ms.SubmitWriteRequest(storage.WriteRequest{ 123 | Labels: labels, 124 | Timestamp: now, 125 | MetricFamilies: metricFamilies, 126 | Replace: replace, 127 | }) 128 | w.WriteHeader(http.StatusAccepted) 129 | return 130 | } 131 | errCh := make(chan error, 1) 132 | errReceived := false 133 | ms.SubmitWriteRequest(storage.WriteRequest{ 134 | Labels: labels, 135 | Timestamp: now, 136 | MetricFamilies: metricFamilies, 137 | Replace: replace, 138 | Done: errCh, 139 | }) 140 | for err := range errCh { 141 | // Send only first error via HTTP, but log all of them. 142 | // TODO(beorn): Consider sending all errors once we 143 | // have a use case. (Currently, at most one error is 144 | // produced.) 145 | if !errReceived { 146 | http.Error( 147 | w, 148 | fmt.Sprintf("pushed metrics are invalid or inconsistent with existing metrics: %v", err), 149 | http.StatusBadRequest, 150 | ) 151 | } 152 | logger.Error( 153 | "pushed metrics are invalid or inconsistent with existing metrics", 154 | "method", r.Method, 155 | "source", r.RemoteAddr, 156 | "err", err.Error(), 157 | ) 158 | errReceived = true 159 | } 160 | }) 161 | 162 | instrumentedHandler := promhttp.InstrumentHandlerRequestSize( 163 | httpPushSize, promhttp.InstrumentHandlerDuration( 164 | httpPushDuration, InstrumentWithCounter("push", handler), 165 | )) 166 | 167 | return func(w http.ResponseWriter, r *http.Request) { 168 | instrumentedHandler.ServeHTTP(w, r) 169 | } 170 | } 171 | 172 | // decodeBase64 decodes the provided string using the “Base 64 Encoding with URL 173 | // and Filename Safe Alphabet” (RFC 4648). Padding characters (i.e. trailing 174 | // '=') are ignored. 175 | func decodeBase64(s string) (string, error) { 176 | b, err := base64.RawURLEncoding.DecodeString(strings.TrimRight(s, "=")) 177 | return string(b), err 178 | } 179 | 180 | // splitLabels splits a labels string into a label map mapping names to values. 181 | func splitLabels(labels string) (map[string]string, error) { 182 | result := map[string]string{} 183 | if len(labels) <= 1 { 184 | return result, nil 185 | } 186 | components := strings.Split(labels[1:], "/") 187 | if len(components)%2 != 0 { 188 | return nil, fmt.Errorf("odd number of components in label string %q", labels) 189 | } 190 | 191 | for i := 0; i < len(components)-1; i += 2 { 192 | name, value := components[i], components[i+1] 193 | trimmedName := strings.TrimSuffix(name, Base64Suffix) 194 | unescapedName := model.UnescapeName(trimmedName, EscapingScheme) 195 | if !ValidationScheme.IsValidLabelName(unescapedName) || 196 | strings.HasPrefix(trimmedName, model.ReservedLabelPrefix) { 197 | return nil, fmt.Errorf("improper label name %q", trimmedName) 198 | } 199 | if name == trimmedName { 200 | result[unescapedName] = value 201 | continue 202 | } 203 | decodedValue, err := decodeBase64(value) 204 | if err != nil { 205 | return nil, fmt.Errorf("invalid base64 encoding for label %s=%q: %v", trimmedName, value, err) 206 | } 207 | result[unescapedName] = decodedValue 208 | } 209 | return result, nil 210 | } 211 | -------------------------------------------------------------------------------- /storage/interface.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package storage 15 | 16 | import ( 17 | "sort" 18 | "time" 19 | 20 | //nolint:staticcheck // Ignore SA1019. Dependencies use the deprecated package, so we have to, too. 21 | "github.com/golang/protobuf/proto" 22 | 23 | dto "github.com/prometheus/client_model/go" 24 | ) 25 | 26 | // MetricStore is the interface to the storage layer for metrics. All its 27 | // methods must be safe to be called concurrently. 28 | type MetricStore interface { 29 | // SubmitWriteRequest submits a WriteRequest for processing. There is no 30 | // guarantee when a request will be processed, but it is guaranteed that 31 | // the requests are processed in the order of submission. 32 | SubmitWriteRequest(req WriteRequest) 33 | // GetMetricFamilies returns all the currently saved MetricFamilies. The 34 | // returned MetricFamilies are guaranteed to not be modified by the 35 | // MetricStore anymore. However, they may still be read somewhere else, 36 | // so the caller is not allowed to modify the returned MetricFamilies. 37 | // If different groups have saved MetricFamilies of the same name, they 38 | // are all merged into one MetricFamily by concatenating the contained 39 | // Metrics. Inconsistent help strings are logged, and one of the 40 | // versions will "win". Inconsistent types and inconsistent or duplicate 41 | // label sets will go undetected. 42 | GetMetricFamilies() []*dto.MetricFamily 43 | // GetMetricFamiliesMap returns a map grouping-key -> MetricGroup. The 44 | // MetricFamily pointed to by the Metrics map in each MetricGroup is 45 | // guaranteed to not be modified by the MetricStore anymore. However, 46 | // they may still be read somewhere else, so the caller is not allowed 47 | // to modify it. Otherwise, the returned nested map can be seen as a 48 | // deep copy of the internal state of the MetricStore and completely 49 | // owned by the caller. 50 | GetMetricFamiliesMap() GroupingKeyToMetricGroup 51 | // Shutdown must only be called after the caller has made sure that 52 | // SubmitWriteRequests is not called anymore. (If it is called later, 53 | // the request might get submitted, but not processed anymore.) The 54 | // Shutdown method waits for the write request queue to empty, then it 55 | // persists the content of the MetricStore (if supported by the 56 | // implementation). Also, all internal goroutines are stopped. This 57 | // method blocks until all of that is complete. If an error is 58 | // encountered, it is returned (whereupon the MetricStorage is in an 59 | // undefinded state). If nil is returned, the MetricStore cannot be 60 | // "restarted" again, but it can still be used for read operations. 61 | Shutdown() error 62 | // Healthy returns nil if the MetricStore is currently working as 63 | // expected. Otherwise, a non-nil error is returned. 64 | Healthy() error 65 | // Ready returns nil if the MetricStore is ready to be used (all files 66 | // are opened and checkpoints have been restored). Otherwise, a non-nil 67 | // error is returned. 68 | Ready() error 69 | } 70 | 71 | // WriteRequest is a request to change the MetricStore, i.e. to process it, a 72 | // write lock has to be acquired. 73 | // 74 | // If MetricFamilies is nil, this is a request to delete metrics that share the 75 | // given Labels as a grouping key. Otherwise, this is a request to update the 76 | // MetricStore with the MetricFamilies. 77 | // 78 | // If Replace is true, the MetricFamilies will completely replace the metrics 79 | // with the same grouping key. Otherwise, only those MetricFamilies with the 80 | // same name as new MetricFamilies will be replaced. 81 | // 82 | // The key in MetricFamilies is the name of the mapped metric family. 83 | // 84 | // When the WriteRequest is processed, the metrics in MetricFamilies will be 85 | // sanitized to have the same job and other labels as those in the Labels 86 | // fields. Also, if there is no instance label, an instance label with an empty 87 | // value will be set. This implies that the MetricFamilies in the WriteRequest 88 | // may be modified be the MetricStore during processing of the WriteRequest! 89 | // 90 | // The Timestamp field marks the time the request was received from the 91 | // network. It is not related to the TimestampMs field in the Metric proto 92 | // message. In fact, WriteRequests containing any Metrics with a TimestampMs set 93 | // are invalid and will be rejected. 94 | // 95 | // The Done channel may be nil. If it is not nil, it will be closed once the 96 | // write request is processed. Any errors occurring during processing are sent to 97 | // the channel before closing it. 98 | type WriteRequest struct { 99 | Labels map[string]string 100 | Timestamp time.Time 101 | MetricFamilies map[string]*dto.MetricFamily 102 | Replace bool 103 | Done chan error 104 | } 105 | 106 | // GroupingKeyToMetricGroup is the first level of the metric store, keyed by 107 | // grouping key. 108 | type GroupingKeyToMetricGroup map[string]MetricGroup 109 | 110 | // MetricGroup adds the grouping labels to a NameToTimestampedMetricFamilyMap. 111 | type MetricGroup struct { 112 | Labels map[string]string 113 | Metrics NameToTimestampedMetricFamilyMap 114 | } 115 | 116 | // SortedLabels returns the label names of the grouping labels sorted 117 | // lexicographically but with the "job" label always first. This method exists 118 | // for presentation purposes, see template.html. 119 | func (mg MetricGroup) SortedLabels() []string { 120 | lns := make([]string, 1, len(mg.Labels)) 121 | lns[0] = "job" 122 | for ln := range mg.Labels { 123 | if ln != "job" { 124 | lns = append(lns, ln) 125 | } 126 | } 127 | sort.Strings(lns[1:]) 128 | return lns 129 | } 130 | 131 | // LastPushSuccess returns false if the automatically added metric for the 132 | // timestamp of the last failed push has a value larger than the value of the 133 | // automatically added metric for the timestamp of the last successful push. In 134 | // all other cases, it returns true (including the case that one or both of 135 | // those metrics are missing for some reason.) 136 | func (mg MetricGroup) LastPushSuccess() bool { 137 | fail := mg.Metrics[pushFailedMetricName].GobbableMetricFamily 138 | if fail == nil { 139 | return true 140 | } 141 | success := mg.Metrics[pushMetricName].GobbableMetricFamily 142 | if success == nil { 143 | return true 144 | } 145 | return (*dto.MetricFamily)(fail).GetMetric()[0].GetGauge().GetValue() <= (*dto.MetricFamily)(success).GetMetric()[0].GetGauge().GetValue() 146 | } 147 | 148 | // NameToTimestampedMetricFamilyMap is the second level of the metric store, 149 | // keyed by metric name. 150 | type NameToTimestampedMetricFamilyMap map[string]TimestampedMetricFamily 151 | 152 | // TimestampedMetricFamily adds the push timestamp to a gobbable version of the 153 | // MetricFamily-DTO. 154 | type TimestampedMetricFamily struct { 155 | Timestamp time.Time 156 | GobbableMetricFamily *GobbableMetricFamily 157 | } 158 | 159 | // GetMetricFamily returns the normal GetMetricFamily DTO (without the gob additions). 160 | func (tmf TimestampedMetricFamily) GetMetricFamily() *dto.MetricFamily { 161 | return (*dto.MetricFamily)(tmf.GobbableMetricFamily) 162 | } 163 | 164 | // GobbableMetricFamily is a dto.MetricFamily that implements GobDecoder and 165 | // GobEncoder. 166 | type GobbableMetricFamily dto.MetricFamily 167 | 168 | // GobDecode implements gob.GobDecoder. 169 | func (gmf *GobbableMetricFamily) GobDecode(b []byte) error { 170 | return proto.Unmarshal(b, (*dto.MetricFamily)(gmf)) 171 | } 172 | 173 | // GobEncode implements gob.GobEncoder. 174 | func (gmf *GobbableMetricFamily) GobEncode() ([]byte, error) { 175 | return proto.Marshal((*dto.MetricFamily)(gmf)) 176 | } 177 | -------------------------------------------------------------------------------- /api/v1/api.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | package v1 14 | 15 | import ( 16 | "encoding/json" 17 | "fmt" 18 | "log/slog" 19 | "net/http" 20 | "time" 21 | 22 | "github.com/prometheus/common/promslog" 23 | "github.com/prometheus/common/route" 24 | 25 | dto "github.com/prometheus/client_model/go" 26 | 27 | "github.com/prometheus/pushgateway/handler" 28 | "github.com/prometheus/pushgateway/histogram" 29 | "github.com/prometheus/pushgateway/storage" 30 | ) 31 | 32 | type status string 33 | 34 | const ( 35 | statusSuccess status = "success" 36 | statusError status = "error" 37 | ) 38 | 39 | type errorType string 40 | 41 | const ( 42 | errorNone errorType = "" 43 | errorTimeout errorType = "timeout" 44 | errorCanceled errorType = "canceled" 45 | errorExec errorType = "execution" 46 | errorBadData errorType = "bad_data" 47 | errorInternal errorType = "internal" 48 | errorUnavailable errorType = "unavailable" 49 | errorNotFound errorType = "not_found" 50 | ) 51 | 52 | type apiError struct { 53 | typ errorType 54 | err error 55 | } 56 | 57 | func (e *apiError) Error() string { 58 | return fmt.Sprintf("%s: %s", e.typ, e.err) 59 | } 60 | 61 | var corsHeaders = map[string]string{ 62 | "Access-Control-Allow-Headers": "Accept, Authorization, Content-Type, Origin", 63 | "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS", 64 | "Access-Control-Allow-Origin": "*", 65 | "Access-Control-Expose-Headers": "Date", 66 | "Cache-Control": "no-cache, no-store, must-revalidate", 67 | } 68 | 69 | // Enables cross-site script calls. 70 | func setCORS(w http.ResponseWriter) { 71 | for h, v := range corsHeaders { 72 | w.Header().Set(h, v) 73 | } 74 | } 75 | 76 | // API provides registration of handlers for API routes. 77 | type API struct { 78 | logger *slog.Logger 79 | MetricStore storage.MetricStore 80 | Flags map[string]string 81 | StartTime time.Time 82 | BuildInfo map[string]string 83 | } 84 | 85 | // New returns a new API. The log.Logger can be nil, in which case no logging is performed. 86 | func New( 87 | l *slog.Logger, 88 | ms storage.MetricStore, 89 | flags map[string]string, 90 | buildInfo map[string]string, 91 | ) *API { 92 | if l == nil { 93 | l = promslog.NewNopLogger() 94 | } 95 | 96 | return &API{ 97 | StartTime: time.Now(), 98 | logger: l, 99 | MetricStore: ms, 100 | Flags: flags, 101 | BuildInfo: buildInfo, 102 | } 103 | } 104 | 105 | // Register registers the API handlers under their correct routes 106 | // in the given router. 107 | func (api *API) Register(r *route.Router) { 108 | wrap := func(handlerName string, f http.HandlerFunc) http.HandlerFunc { 109 | return handler.InstrumentWithCounter( 110 | handlerName, 111 | http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 112 | setCORS(w) 113 | f(w, r) 114 | }), 115 | ) 116 | } 117 | 118 | r.Options("/*path", wrap("api/v1/options", func(w http.ResponseWriter, r *http.Request) {})) 119 | 120 | r.Get("/status", wrap("api/v1/status", api.status)) 121 | r.Get("/metrics", wrap("api/v1/metrics", api.metrics)) 122 | } 123 | 124 | type metrics struct { 125 | Timestamp time.Time `json:"time_stamp"` 126 | Type string `json:"type"` 127 | Help string `json:"help,omitempty"` 128 | Metrics []encodableMetric `json:"metrics"` 129 | } 130 | 131 | func (api *API) metrics(w http.ResponseWriter, r *http.Request) { 132 | familyMaps := api.MetricStore.GetMetricFamiliesMap() 133 | res := []any{} 134 | for _, v := range familyMaps { 135 | metricResponse := map[string]any{} 136 | metricResponse["labels"] = v.Labels 137 | metricResponse["last_push_successful"] = v.LastPushSuccess() 138 | for name, metricValues := range v.Metrics { 139 | metricFamily := metricValues.GetMetricFamily() 140 | uniqueMetrics := metrics{ 141 | Type: metricFamily.GetType().String(), 142 | Help: metricFamily.GetHelp(), 143 | Timestamp: metricValues.Timestamp, 144 | Metrics: makeEncodableMetrics(metricFamily.GetMetric(), metricFamily.GetType()), 145 | } 146 | metricResponse[name] = uniqueMetrics 147 | } 148 | res = append(res, metricResponse) 149 | } 150 | 151 | api.respond(w, res) 152 | } 153 | 154 | func (api *API) status(w http.ResponseWriter, r *http.Request) { 155 | res := map[string]any{} 156 | res["flags"] = api.Flags 157 | res["start_time"] = api.StartTime 158 | res["build_information"] = api.BuildInfo 159 | 160 | api.respond(w, res) 161 | } 162 | 163 | type response struct { 164 | Status status `json:"status"` 165 | Data any `json:"data,omitempty"` 166 | ErrorType errorType `json:"errorType,omitempty"` 167 | Error string `json:"error,omitempty"` 168 | } 169 | 170 | func (api *API) respond(w http.ResponseWriter, data any) { 171 | w.Header().Set("Content-Type", "application/json") 172 | w.WriteHeader(http.StatusOK) 173 | 174 | b, err := json.Marshal(&response{ 175 | Status: statusSuccess, 176 | Data: data, 177 | }) 178 | if err != nil { 179 | api.logger.Error("error marshaling JSON", "err", err) 180 | api.respondError(w, apiError{ 181 | typ: errorBadData, 182 | err: err, 183 | }, "") 184 | } 185 | 186 | if _, err := w.Write(b); err != nil { 187 | api.logger.Error("failed to write data to connection", "err", err) 188 | } 189 | } 190 | 191 | func (api *API) respondError(w http.ResponseWriter, apiErr apiError, data any) { 192 | w.Header().Set("Content-Type", "application/json") 193 | 194 | switch apiErr.typ { 195 | case errorBadData: 196 | w.WriteHeader(http.StatusBadRequest) 197 | case errorInternal: 198 | w.WriteHeader(http.StatusInternalServerError) 199 | default: 200 | panic(fmt.Sprintf("unknown error type %q", apiErr.Error())) 201 | } 202 | 203 | b, err := json.Marshal(&response{ 204 | Status: statusError, 205 | ErrorType: apiErr.typ, 206 | Error: apiErr.err.Error(), 207 | Data: data, 208 | }) 209 | if err != nil { 210 | return 211 | } 212 | api.logger.Error("API error", "err", apiErr.Error()) 213 | 214 | if _, err := w.Write(b); err != nil { 215 | api.logger.Error("failed to write data to connection", "err", err) 216 | } 217 | } 218 | 219 | type encodableMetric map[string]any 220 | 221 | func makeEncodableMetrics(metrics []*dto.Metric, metricsType dto.MetricType) []encodableMetric { 222 | 223 | jsonMetrics := make([]encodableMetric, len(metrics)) 224 | 225 | for i, m := range metrics { 226 | 227 | metric := encodableMetric{} 228 | metric["labels"] = makeLabels(m) 229 | switch metricsType { 230 | case dto.MetricType_SUMMARY: 231 | metric["quantiles"] = makeQuantiles(m) 232 | metric["count"] = fmt.Sprint(m.GetSummary().GetSampleCount()) 233 | metric["sum"] = fmt.Sprint(m.GetSummary().GetSampleSum()) 234 | case dto.MetricType_HISTOGRAM: 235 | dtoH := m.GetHistogram() 236 | metric["sum"] = fmt.Sprint(dtoH.GetSampleSum()) 237 | if len(dtoH.GetNegativeSpan())+len(dtoH.GetPositiveSpan()) > 0 { 238 | h, fh := histogram.NewModelHistogram(dtoH) 239 | if h == nil { 240 | // float histogram 241 | metric["count"] = fmt.Sprint(fh.Count) 242 | metric["buckets"] = histogram.BucketsAsJson[float64](histogram.GetAPIFloatBuckets(fh)) 243 | } else { 244 | metric["count"] = fmt.Sprint(h.Count) 245 | metric["buckets"] = histogram.BucketsAsJson[uint64](histogram.GetAPIBuckets(h)) 246 | } 247 | } else { 248 | metric["buckets"] = makeBuckets(m) 249 | if count := dtoH.GetSampleCountFloat(); count > 0 { 250 | metric["count"] = fmt.Sprint(count) 251 | } else { 252 | metric["count"] = fmt.Sprint(dtoH.GetSampleCount()) 253 | } 254 | } 255 | default: 256 | metric["value"] = fmt.Sprint(getValue(m)) 257 | } 258 | jsonMetrics[i] = metric 259 | } 260 | return jsonMetrics 261 | } 262 | 263 | func makeLabels(m *dto.Metric) map[string]string { 264 | result := map[string]string{} 265 | for _, lp := range m.Label { 266 | result[lp.GetName()] = lp.GetValue() 267 | } 268 | return result 269 | } 270 | 271 | func makeQuantiles(m *dto.Metric) map[string]string { 272 | result := map[string]string{} 273 | for _, q := range m.GetSummary().Quantile { 274 | result[fmt.Sprint(q.GetQuantile())] = fmt.Sprint(q.GetValue()) 275 | } 276 | return result 277 | } 278 | 279 | func makeBuckets(m *dto.Metric) map[string]string { 280 | result := map[string]string{} 281 | for _, b := range m.GetHistogram().Bucket { 282 | if count := b.GetCumulativeCountFloat(); count > 0 { 283 | result[fmt.Sprint(b.GetUpperBound())] = fmt.Sprint(count) 284 | } else { 285 | result[fmt.Sprint(b.GetUpperBound())] = fmt.Sprint(b.GetCumulativeCount()) 286 | } 287 | } 288 | return result 289 | } 290 | 291 | func getValue(m *dto.Metric) float64 { 292 | switch { 293 | case m.Gauge != nil: 294 | return m.GetGauge().GetValue() 295 | case m.Counter != nil: 296 | return m.GetCounter().GetValue() 297 | case m.Untyped != nil: 298 | return m.GetUntyped().GetValue() 299 | default: 300 | return 0 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY= 2 | github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= 3 | github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0= 4 | github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs= 5 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 6 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 7 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 8 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 9 | github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo= 10 | github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU= 11 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 14 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 | github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= 16 | github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= 17 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 18 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 19 | github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= 20 | github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 21 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 22 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 23 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 24 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 25 | github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= 26 | github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= 27 | github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= 28 | github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 29 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 30 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 31 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 32 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 33 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 34 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 35 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 36 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 37 | github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= 38 | github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= 39 | github.com/mdlayher/vsock v1.2.1 h1:pC1mTJTvjo1r9n9fbm7S1j04rCgCzhCOS5DY0zqHlnQ= 40 | github.com/mdlayher/vsock v1.2.1/go.mod h1:NRfCibel++DgeMD8z/hP+PPTjlNJsdPOmxcnENvE+SE= 41 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 42 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 43 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= 44 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 45 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 46 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 47 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 48 | github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= 49 | github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= 50 | github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= 51 | github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= 52 | github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= 53 | github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= 54 | github.com/prometheus/exporter-toolkit v0.15.0 h1:Pcle5sSViwR1x0gdPd0wtYrPQENBieQAM7TmT0qtb2U= 55 | github.com/prometheus/exporter-toolkit v0.15.0/go.mod h1:OyRWd2iTo6Xge9Kedvv0IhCrJSBu36JCfJ2yVniRIYk= 56 | github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= 57 | github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= 58 | github.com/prometheus/prometheus v0.307.3 h1:zGIN3EpiKacbMatcUL2i6wC26eRWXdoXfNPjoBc2l34= 59 | github.com/prometheus/prometheus v0.307.3/go.mod h1:sPbNW+KTS7WmzFIafC3Inzb6oZVaGLnSvwqTdz2jxRQ= 60 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 61 | github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 62 | github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c h1:aqg5Vm5dwtvL+YgDpBcK1ITf3o96N/K7/wsRXQnUTEs= 63 | github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c/go.mod h1:owqhoLW1qZoYLZzLnBw+QkPP9WZnjlSWihhxAJC1+/M= 64 | github.com/shurcooL/vfsgen v0.0.0-20230704071429-0000e147ea92 h1:OfRzdxCzDhp+rsKWXuOO2I/quKMJ/+TQwVbIP/gltZg= 65 | github.com/shurcooL/vfsgen v0.0.0-20230704071429-0000e147ea92/go.mod h1:7/OT02F6S6I7v6WXb+IjhMuZEYfH/RJ5RwEWnEo5BMg= 66 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 67 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 68 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 69 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 70 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 71 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 72 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 73 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 74 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 75 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 76 | github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= 77 | github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= 78 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 79 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 80 | go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= 81 | go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= 82 | golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= 83 | golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= 84 | golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= 85 | golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= 86 | golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= 87 | golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= 88 | golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= 89 | golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 90 | golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= 91 | golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 92 | golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= 93 | golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= 94 | golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= 95 | golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= 96 | golang.org/x/tools/godoc v0.1.0-deprecated h1:o+aZ1BOj6Hsx/GBdJO/s815sqftjSnrZZwyYTHODvtk= 97 | golang.org/x/tools/godoc v0.1.0-deprecated/go.mod h1:qM63CriJ961IHWmnWa9CjZnBndniPt4a3CK0PVB9bIg= 98 | google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= 99 | google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= 100 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 101 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 102 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 103 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 104 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 105 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 106 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.11.2 / 2025-10-30 2 | 3 | * [BUGFIX] Update dependencies to pull in possibly relevant bugfixes. 4 | 5 | ## 1.11.1 / 2025-04-09 6 | 7 | * [BUGFIX] Update dependencies to pull in possibly relevant bugfixes. 8 | 9 | ## 1.11.0 / 2025-01-09 10 | 11 | * [FEATURE] Support full UTF-8 character set in metric and label names. #689 #710 12 | * [BUGFIX] Update dependencies to pull in possibly relevant bugfixes. 13 | 14 | ## 1.10.0 / 2024-09-19 15 | 16 | * [FEATURE] API: Support classic float histograms. #668 17 | * [BUGFIX] Update dependencies to pull in possibly relevant bugfixes. 18 | 19 | ## 1.9.0 / 2024-06-06 20 | 21 | * [CHANGE] Command line: Print --version to stdout, not stderr. #651 22 | * [FEATURE] UI: Support native histograms. #596 23 | * [ENHANCEMENT] Log when liveness probes fail due to full write queue. #645 24 | * [BUGFIX] Update dependencies to pull in possibly relevant bugfixes. 25 | 26 | ## 1.8.0 / 2024-04-03 27 | 28 | * [FEATURE] API: Support native histograms in api/v1/metrics. #611 29 | * [BUGFIX] Update dependencies to pull in possibly relevant bugfixes, in particular a bug concerning multiple metric families pushed via protobuf. 30 | 31 | ## 1.7.0 / 2024-01-18 32 | 33 | * [FEATURE] UI: Keep expansion state on page refresh. #581 34 | * [SECURITY] Build precompiled binaries with Go v1.21.6 to avoid security issues with previous Go releases. 35 | * [BUGFIX] Update dependencies to pull in possibly relevant bugfixes. 36 | 37 | ## 1.6.2 / 2023-09-13 38 | 39 | * [SECURITY] Build precompiled binaries with Go v1.21.1 to avoid [security issues with Go v1.21.0](https://groups.google.com/g/golang-announce/c/Fm51GRLNRvM). 40 | 41 | ## 1.6.1 / 2023-09-05 42 | 43 | * [SECURITY] Build precompiled binaries with current Go version (1.21.0) to avoid known bugs in older Go versions. 44 | * [BUGFIX] Update dependencies to pull in possibly relevant bugfixes. 45 | 46 | ## 1.6.0 / 2023-05-25 47 | 48 | * [FEATURE] Add snappy encoding support for `/metrics` endpoint. #518 49 | * [ENHANCEMENT] Add pre-built binaries for the s390x architecture. #537 50 | * [BUGFIX] Fix possible leak of a file handle when using compression. #518 51 | 52 | ## 1.5.1 / 2022-11-29 53 | 54 | * [SECURITY] Fix basic authentication bypass vulnerability (CVE-2022-46146). #516 55 | 56 | ## 1.5.0 / 2022-11-24 57 | 58 | * [FEATURE] Add multiple listeners and systemd socket support. #512 59 | * [FEATURE] Accept a gzip'd body of a POST or PUT request. #477 60 | 61 | ## 1.4.3 / 2022-05-30 62 | 63 | * [BUGFIX] Update dependencies to pull in possibly relevant bugfixes. 64 | 65 | ## 1.4.2 / 2021-10-11 66 | 67 | * [BUGFIX] Do not log an error upon graceful shutdown. #428 68 | * [BUGFIX] Update dependencies to pull in possibly relevant bugfixes. 69 | 70 | ## 1.4.1 / 2021-05-28 71 | 72 | * [BUGFIX] Persist labels properly when draining. #404 73 | * [BUGFIX] Use relative paths for web assets. #390 74 | 75 | ## 1.4.0 / 2021-01-23 76 | 77 | * [FEATURE] **Experimental!** Add TLS and basic authentication to HTTP endpoints. #381 78 | 79 | ## 1.3.1 / 2020-12-17 80 | 81 | * [ENHANCEMENT] Web UI: Improved metrics text alignment. #369 82 | * [BUGFIX] Web UI: Fix deletion of groups with empty label values. #377 83 | 84 | ## 1.3.0 / 2020-10-01 85 | 86 | * [FEATURE] Add Docker image build for ppc64le architecture. #339 87 | * [ENHANCEMENT] Web UI: Add scroll bare to list of pushed metrics. #354 88 | * [ENHANCEMENT] Logging: Show remote address when failing to parse pushed metrics. #361 89 | * [BUGFIX] Web UI: Update JQuery to v3.5.1 to address security concerns. #360 90 | 91 | ## 1.2.0 / 2020-03-11 92 | 93 | * [FEATURE] Add an HTTP API to query pushed metrics and runtime information. #184 94 | 95 | ## 1.1.0 / 2020-01-27 96 | 97 | * [FEATURE] Add flag `--push.disable-consistency-check`. #318 98 | 99 | ## 1.0.1 / 2019-12-21 100 | 101 | * [ENHANCEMENT] Remove excessive whitespace from HTML templates. #302 102 | * [BUGFIX] Fix docker manifest files for non-amd64 architectures. #310 103 | 104 | ## 1.0.0 / 2019-10-15 105 | 106 | _This release does not support the storage format of v0.5–v0.9 anymore. Only persistence files created by v0.10+ are usable. Upgrade to v0.10 first to convert existing persistence files._ 107 | 108 | * [CHANGE] Remove code to convert the legacy v0.5–v0.9 storage format. 109 | 110 | ## 0.10.0 / 2019-10-10 111 | 112 | _This release changes the storage format. v0.10 can read the storage format of v0.5–v0.9. It will then persist the new format so that a downgrade won't be possible anymore._ 113 | 114 | * [CHANGE] Change of the storage format (necessary for the hash collision bugfix below). #293 115 | * [CHANGE] Check pushed metrics immediately and reject them if inconsistent. Successful pushes now result in code 200 (not 202). Failures result in code 400 and are logged at error level. #290 116 | * [FEATURE] Shutdown via HTTP request. Enable with `--web.enable-lifecycle`. #292 117 | * [FEATURE] Wipe storage completely via HTTP request and via web UI. Enable with `--web.enable-admin-api`. #287 #285 118 | * [BUGFIX] Rule out hash collisions between metric groups. #293 119 | * [BUGFIX] Avoid multiple calls of `http.Error` in push handler. #291 120 | 121 | ## 0.9.1 / 2019-08-01 122 | 123 | * [BUGFIX] Make `--web.external-url` and `--web.route-prefix` work as documented. #274 124 | 125 | ## 0.9.0 / 2019-07-23 126 | 127 | * [CHANGE] Web: Update to Bootstrap 4.3.1 and jquery 3.4.1, changing appearance of the web UI to be more in line with the Prometheus server. Also add favicon and remove timestamp column. #261 128 | * [CHANGE] Update logging to be in line with other Prometheus projects, using gokit and promlog. #263 129 | * [FEATURE] Add optional base64 encoding for label values in the grouping key. #268 130 | * [FEATURE] Add ARM container images. #265 131 | * [FEATURE] Log errors during scrapes. #267 132 | * [BUGFIX] Web: Fixed Content-Type for js and css instead of using /etc/mime.types. #252 133 | 134 | ## 0.8.0 / 2019-04-13 135 | 136 | _If you use the prebuilt Docker container or you build your own one based on the provided Dockerfile, note that this release changes the user to `nobody`. Should you use a persistence file, make sure it is readable and writable by user `nobody`._ 137 | 138 | * [CHANGE] Run as user `nobody` in Docker. #242 139 | * [CHANGE] Adjust `--web.route-prefix` to work the same as in Prometheus. #190 140 | * [FEATURE] Add `--web.external-url` flag (like in Prometheus). #190 141 | 142 | ## 0.7.0 / 2018-12-07 143 | 144 | _As preparation for the 1.0.0 release, this release removes the long deprecated legacy HTTP push endpoint (which uses `/jobs/` rather than `/job/` in the URL)._ 145 | 146 | * [CHANGE] Remove legacy push API. #227 147 | * [ENHANCEMENT] Update dependencies. #230 148 | * [ENHANCEMENT] Support Go modules. #221 149 | * [BUGFIX] Avoid crash when started with v0.4 storage. #223 150 | 151 | ## 0.6.0 / 2018-10-17 152 | 153 | _Persistence storage prior to 0.5.0 is unsupported. Upgrade to 0.5.2 first for conversion._ 154 | 155 | * [CHANGE] Enforce consistency of help strings by changing them during exposition. (An INFO-level log message describes the change.) #194 156 | * [CHANGE] Drop support of pre-0.5 storage format. 157 | * [CHANGE] Use prometheus/client_golang v0.9, which changes the `http_...` metrics. (See README.md for full documentation of exposed metrics.) 158 | 159 | ## 0.5.2 / 2018-06-15 160 | 161 | * [BUGFIX] Update client_golang/prometheus vendoring to allow inconsistent 162 | labels. #185 163 | 164 | ## 0.5.1 / 2018-05-30 165 | 166 | * [BUGFIX] Fix conversion of old persistency format (0.4.0 and earlier). #179 167 | * [BUGFIX] Make _Delete Group_ button work again. #177 168 | * [BUGFIX] Don't display useless flags on status page. #176 169 | 170 | ## 0.5.0 / 2018-05-23 171 | 172 | Breaking change: 173 | * Flags now require double-dash. 174 | * The persistence storage format has been updated. Upgrade is transparent, but downgrade to 0.4.0 and prior is unsupported. 175 | * Persistence storage prior to 0.1.0 is unsupported. 176 | 177 | * [CHANGE] Replaced Flags with Kingpin #152 178 | * [CHANGE] Slightly changed disk format for persistence. v0.5 can still read the pre-v0.5 format. #172 179 | * [ENHANCEMENT] Debug level logging now shows client-induced errors #123 180 | * [FEATURE] Add /-/ready and /-/healthy #135 181 | * [FEATURE] Add web.route-prefix flag #146 182 | * [BUGFIX] Fix incorrect persistence of certain values in a metric family. #172 183 | 184 | ## 0.4.0 / 2017-06-09 185 | * [CHANGE] Pushes with timestamps are now rejected. 186 | * [FEATURE] Added push_time_seconds metric to each push. 187 | * [ENHANCEMENT] Point at community page rather than the dev list in the UI. 188 | * [BUGFIX] Return HTTP 400 on parse error, rather than 500. 189 | 190 | ## 0.3.1 / 2016-11-03 191 | * [BUGFIX] Fixed a race condition in the storage layer. 192 | * [ENHANCEMENT] Improved README.md. 193 | 194 | ## 0.3.0 / 2016-06-07 195 | * [CHANGE] Push now rejects improper and reserved labels. 196 | * [CHANGE] Required labels flag removed. 197 | * [BUGFIX] Docker image actually works now. 198 | * [ENHANCEMENT] Converted to Promu build process. 199 | * [CHANGE] As a consequence of the above, changed dir structure in tar ball. 200 | * [ENHANCEMENT] Updated dependencies, with all the necessary code changes. 201 | * [ENHANCEMENT] Dependencies now vendored. 202 | * [ENHANCEMENT] `bindata.go` checked in, Pushgateway now `go get`-able. 203 | * [ENHANCEMENT] Various documentation improvements. 204 | * [CLEANUP] Various code cleanups. 205 | 206 | ## 0.2.0 / 2015-06-25 207 | * [CHANGE] Support arbitrary grouping of metrics. 208 | * [CHANGE] Changed behavior of HTTP DELETE method (see README.md for details). 209 | 210 | ## 0.1.2 / 2015-06-08 211 | * [CHANGE] Move pushgateway binary in archive from bin/ to /. 212 | * [CHANGE] Migrate logging to prometheus/log. 213 | 214 | ## 0.1.1 / 2015-05-05 215 | * [BUGFIX] Properly display histograms in web status. 216 | * [BUGFIX] Fix value formatting. 217 | * [CHANGE] Make flag names consistent across projects. 218 | * [ENHANCEMENT] Auto-fill instance with IPv6 address. 219 | * [BUGFIX] Fix Go download link for several archs and OSes. 220 | * [BUGFIX] Use HTTPS and golang.org for Go download. 221 | * [BUGFIX] Re-add pprof endpoints. 222 | 223 | ## 0.1.0 / 2014-08-13 224 | * [FEATURE] When being scraped, metrics of the same name but with different job/instance label are now merged into one metric family. 225 | * [FEATURE] Added Dockerfile. 226 | * [CHANGE] Default HTTP port now 9091. 227 | * [BUGFIX] Fixed parsing of content-type header. 228 | * [BUGFIX] Fixed race condition in handlers. 229 | * [PERFORMANCE] Replaced Martini with Httprouter. 230 | * [ENHANCEMENT] Migrated to new client_golang. 231 | * [ENHANCEMENT] Made internal metrics more consistent. 232 | * [ENHANCEMENT] Added http instrumentation. 233 | 234 | -------------------------------------------------------------------------------- /resources/static/bootstrap4-glyphicons/css/bootstrap-glyphicons.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.7 (http://getbootstrap.com) 3 | * Copyright 2011-2018 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | @font-face{font-family:'Glyphicons Halflings';src:url('../fonts/glyphicons/glyphicons-halflings-regular.eot?');src:url('../fonts/glyphicons/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'),url('../fonts/glyphicons/glyphicons-halflings-regular.woff2') format('woff2'),url('../fonts/glyphicons/glyphicons-halflings-regular.woff') format('woff'),url('../fonts/glyphicons/glyphicons-halflings-regular.ttf') format('truetype'),url('../fonts/glyphicons/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:normal;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"*"}.glyphicon-plus:before{content:"+"}.glyphicon-euro:before,.glyphicon-eur:before{content:"€"}.glyphicon-minus:before{content:"−"}.glyphicon-cloud:before{content:"☁"}.glyphicon-envelope:before{content:"✉"}.glyphicon-pencil:before{content:"✏"}.glyphicon-glass:before{content:""}.glyphicon-music:before{content:""}.glyphicon-search:before{content:""}.glyphicon-heart:before{content:""}.glyphicon-star:before{content:""}.glyphicon-star-empty:before{content:""}.glyphicon-user:before{content:""}.glyphicon-film:before{content:""}.glyphicon-th-large:before{content:""}.glyphicon-th:before{content:""}.glyphicon-th-list:before{content:""}.glyphicon-ok:before{content:""}.glyphicon-remove:before{content:""}.glyphicon-zoom-in:before{content:""}.glyphicon-zoom-out:before{content:""}.glyphicon-off:before{content:""}.glyphicon-signal:before{content:""}.glyphicon-cog:before{content:""}.glyphicon-trash:before{content:""}.glyphicon-home:before{content:""}.glyphicon-file:before{content:""}.glyphicon-time:before{content:""}.glyphicon-road:before{content:""}.glyphicon-download-alt:before{content:""}.glyphicon-download:before{content:""}.glyphicon-upload:before{content:""}.glyphicon-inbox:before{content:""}.glyphicon-play-circle:before{content:""}.glyphicon-repeat:before{content:""}.glyphicon-refresh:before{content:""}.glyphicon-list-alt:before{content:""}.glyphicon-lock:before{content:""}.glyphicon-flag:before{content:""}.glyphicon-headphones:before{content:""}.glyphicon-volume-off:before{content:""}.glyphicon-volume-down:before{content:""}.glyphicon-volume-up:before{content:""}.glyphicon-qrcode:before{content:""}.glyphicon-barcode:before{content:""}.glyphicon-tag:before{content:""}.glyphicon-tags:before{content:""}.glyphicon-book:before{content:""}.glyphicon-bookmark:before{content:""}.glyphicon-print:before{content:""}.glyphicon-camera:before{content:""}.glyphicon-font:before{content:""}.glyphicon-bold:before{content:""}.glyphicon-italic:before{content:""}.glyphicon-text-height:before{content:""}.glyphicon-text-width:before{content:""}.glyphicon-align-left:before{content:""}.glyphicon-align-center:before{content:""}.glyphicon-align-right:before{content:""}.glyphicon-align-justify:before{content:""}.glyphicon-list:before{content:""}.glyphicon-indent-left:before{content:""}.glyphicon-indent-right:before{content:""}.glyphicon-facetime-video:before{content:""}.glyphicon-picture:before{content:""}.glyphicon-map-marker:before{content:""}.glyphicon-adjust:before{content:""}.glyphicon-tint:before{content:""}.glyphicon-edit:before{content:""}.glyphicon-share:before{content:""}.glyphicon-check:before{content:""}.glyphicon-move:before{content:""}.glyphicon-step-backward:before{content:""}.glyphicon-fast-backward:before{content:""}.glyphicon-backward:before{content:""}.glyphicon-play:before{content:""}.glyphicon-pause:before{content:""}.glyphicon-stop:before{content:""}.glyphicon-forward:before{content:""}.glyphicon-fast-forward:before{content:""}.glyphicon-step-forward:before{content:""}.glyphicon-eject:before{content:""}.glyphicon-chevron-left:before{content:""}.glyphicon-chevron-right:before{content:""}.glyphicon-plus-sign:before{content:""}.glyphicon-minus-sign:before{content:""}.glyphicon-remove-sign:before{content:""}.glyphicon-ok-sign:before{content:""}.glyphicon-question-sign:before{content:""}.glyphicon-info-sign:before{content:""}.glyphicon-screenshot:before{content:""}.glyphicon-remove-circle:before{content:""}.glyphicon-ok-circle:before{content:""}.glyphicon-ban-circle:before{content:""}.glyphicon-arrow-left:before{content:""}.glyphicon-arrow-right:before{content:""}.glyphicon-arrow-up:before{content:""}.glyphicon-arrow-down:before{content:""}.glyphicon-share-alt:before{content:""}.glyphicon-resize-full:before{content:""}.glyphicon-resize-small:before{content:""}.glyphicon-exclamation-sign:before{content:""}.glyphicon-gift:before{content:""}.glyphicon-leaf:before{content:""}.glyphicon-fire:before{content:""}.glyphicon-eye-open:before{content:""}.glyphicon-eye-close:before{content:""}.glyphicon-warning-sign:before{content:""}.glyphicon-plane:before{content:""}.glyphicon-calendar:before{content:""}.glyphicon-random:before{content:""}.glyphicon-comment:before{content:""}.glyphicon-magnet:before{content:""}.glyphicon-chevron-up:before{content:""}.glyphicon-chevron-down:before{content:""}.glyphicon-retweet:before{content:""}.glyphicon-shopping-cart:before{content:""}.glyphicon-folder-close:before{content:""}.glyphicon-folder-open:before{content:""}.glyphicon-resize-vertical:before{content:""}.glyphicon-resize-horizontal:before{content:""}.glyphicon-hdd:before{content:""}.glyphicon-bullhorn:before{content:""}.glyphicon-bell:before{content:""}.glyphicon-certificate:before{content:""}.glyphicon-thumbs-up:before{content:""}.glyphicon-thumbs-down:before{content:""}.glyphicon-hand-right:before{content:""}.glyphicon-hand-left:before{content:""}.glyphicon-hand-up:before{content:""}.glyphicon-hand-down:before{content:""}.glyphicon-circle-arrow-right:before{content:""}.glyphicon-circle-arrow-left:before{content:""}.glyphicon-circle-arrow-up:before{content:""}.glyphicon-circle-arrow-down:before{content:""}.glyphicon-globe:before{content:""}.glyphicon-wrench:before{content:""}.glyphicon-tasks:before{content:""}.glyphicon-filter:before{content:""}.glyphicon-briefcase:before{content:""}.glyphicon-fullscreen:before{content:""}.glyphicon-dashboard:before{content:""}.glyphicon-paperclip:before{content:""}.glyphicon-heart-empty:before{content:""}.glyphicon-link:before{content:""}.glyphicon-phone:before{content:""}.glyphicon-pushpin:before{content:""}.glyphicon-usd:before{content:""}.glyphicon-gbp:before{content:""}.glyphicon-sort:before{content:""}.glyphicon-sort-by-alphabet:before{content:""}.glyphicon-sort-by-alphabet-alt:before{content:""}.glyphicon-sort-by-order:before{content:""}.glyphicon-sort-by-order-alt:before{content:""}.glyphicon-sort-by-attributes:before{content:""}.glyphicon-sort-by-attributes-alt:before{content:""}.glyphicon-unchecked:before{content:""}.glyphicon-expand:before{content:""}.glyphicon-collapse-down:before{content:""}.glyphicon-collapse-up:before{content:""}.glyphicon-log-in:before{content:""}.glyphicon-flash:before{content:""}.glyphicon-log-out:before{content:""}.glyphicon-new-window:before{content:""}.glyphicon-record:before{content:""}.glyphicon-save:before{content:""}.glyphicon-open:before{content:""}.glyphicon-saved:before{content:""}.glyphicon-import:before{content:""}.glyphicon-export:before{content:""}.glyphicon-send:before{content:""}.glyphicon-floppy-disk:before{content:""}.glyphicon-floppy-saved:before{content:""}.glyphicon-floppy-remove:before{content:""}.glyphicon-floppy-save:before{content:""}.glyphicon-floppy-open:before{content:""}.glyphicon-credit-card:before{content:""}.glyphicon-transfer:before{content:""}.glyphicon-cutlery:before{content:""}.glyphicon-header:before{content:""}.glyphicon-compressed:before{content:""}.glyphicon-earphone:before{content:""}.glyphicon-phone-alt:before{content:""}.glyphicon-tower:before{content:""}.glyphicon-stats:before{content:""}.glyphicon-sd-video:before{content:""}.glyphicon-hd-video:before{content:""}.glyphicon-subtitles:before{content:""}.glyphicon-sound-stereo:before{content:""}.glyphicon-sound-dolby:before{content:""}.glyphicon-sound-5-1:before{content:""}.glyphicon-sound-6-1:before{content:""}.glyphicon-sound-7-1:before{content:""}.glyphicon-copyright-mark:before{content:""}.glyphicon-registration-mark:before{content:""}.glyphicon-cloud-download:before{content:""}.glyphicon-cloud-upload:before{content:""}.glyphicon-tree-conifer:before{content:""}.glyphicon-tree-deciduous:before{content:""}.glyphicon-cd:before{content:""}.glyphicon-save-file:before{content:""}.glyphicon-open-file:before{content:""}.glyphicon-level-up:before{content:""}.glyphicon-copy:before{content:""}.glyphicon-paste:before{content:""}.glyphicon-alert:before{content:""}.glyphicon-equalizer:before{content:""}.glyphicon-king:before{content:""}.glyphicon-queen:before{content:""}.glyphicon-pawn:before{content:""}.glyphicon-bishop:before{content:""}.glyphicon-knight:before{content:""}.glyphicon-baby-formula:before{content:""}.glyphicon-tent:before{content:"⛺"}.glyphicon-blackboard:before{content:""}.glyphicon-bed:before{content:""}.glyphicon-apple:before{content:""}.glyphicon-erase:before{content:""}.glyphicon-hourglass:before{content:"⌛"}.glyphicon-lamp:before{content:""}.glyphicon-duplicate:before{content:""}.glyphicon-piggy-bank:before{content:""}.glyphicon-scissors:before{content:""}.glyphicon-bitcoin:before{content:""}.glyphicon-btc:before{content:""}.glyphicon-xbt:before{content:""}.glyphicon-yen:before{content:"¥"}.glyphicon-jpy:before{content:"¥"}.glyphicon-ruble:before{content:"₽"}.glyphicon-rub:before{content:"₽"}.glyphicon-scale:before{content:""}.glyphicon-ice-lolly:before{content:""}.glyphicon-ice-lolly-tasted:before{content:""}.glyphicon-education:before{content:""}.glyphicon-option-horizontal:before{content:""}.glyphicon-option-vertical:before{content:""}.glyphicon-menu-hamburger:before{content:""}.glyphicon-modal-window:before{content:""}.glyphicon-oil:before{content:""}.glyphicon-grain:before{content:""}.glyphicon-sunglasses:before{content:""}.glyphicon-text-size:before{content:""}.glyphicon-text-color:before{content:""}.glyphicon-text-background:before{content:""}.glyphicon-object-align-top:before{content:""}.glyphicon-object-align-bottom:before{content:""}.glyphicon-object-align-horizontal:before{content:""}.glyphicon-object-align-left:before{content:""}.glyphicon-object-align-vertical:before{content:""}.glyphicon-object-align-right:before{content:""}.glyphicon-triangle-right:before{content:""}.glyphicon-triangle-left:before{content:""}.glyphicon-triangle-bottom:before{content:""}.glyphicon-triangle-top:before{content:""}.glyphicon-console:before{content:""}.glyphicon-superscript:before{content:""}.glyphicon-subscript:before{content:""}.glyphicon-menu-left:before{content:""}.glyphicon-menu-right:before{content:""}.glyphicon-menu-down:before{content:""}.glyphicon-menu-up:before{content:""} -------------------------------------------------------------------------------- /Makefile.common: -------------------------------------------------------------------------------- 1 | # Copyright 2018 The Prometheus Authors 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | 14 | 15 | # A common Makefile that includes rules to be reused in different prometheus projects. 16 | # !!! Open PRs only against the prometheus/prometheus/Makefile.common repository! 17 | 18 | # Example usage : 19 | # Create the main Makefile in the root project directory. 20 | # include Makefile.common 21 | # customTarget: 22 | # @echo ">> Running customTarget" 23 | # 24 | 25 | # Ensure GOBIN is not set during build so that promu is installed to the correct path 26 | unexport GOBIN 27 | 28 | GO ?= go 29 | GOFMT ?= $(GO)fmt 30 | FIRST_GOPATH := $(firstword $(subst :, ,$(shell $(GO) env GOPATH))) 31 | GOOPTS ?= 32 | GOHOSTOS ?= $(shell $(GO) env GOHOSTOS) 33 | GOHOSTARCH ?= $(shell $(GO) env GOHOSTARCH) 34 | 35 | GO_VERSION ?= $(shell $(GO) version) 36 | GO_VERSION_NUMBER ?= $(word 3, $(GO_VERSION)) 37 | PRE_GO_111 ?= $(shell echo $(GO_VERSION_NUMBER) | grep -E 'go1\.(10|[0-9])\.') 38 | 39 | PROMU := $(FIRST_GOPATH)/bin/promu 40 | pkgs = ./... 41 | 42 | ifeq (arm, $(GOHOSTARCH)) 43 | GOHOSTARM ?= $(shell GOARM= $(GO) env GOARM) 44 | GO_BUILD_PLATFORM ?= $(GOHOSTOS)-$(GOHOSTARCH)v$(GOHOSTARM) 45 | else 46 | GO_BUILD_PLATFORM ?= $(GOHOSTOS)-$(GOHOSTARCH) 47 | endif 48 | 49 | GOTEST := $(GO) test 50 | GOTEST_DIR := 51 | ifneq ($(CIRCLE_JOB),) 52 | ifneq ($(shell command -v gotestsum 2> /dev/null),) 53 | GOTEST_DIR := test-results 54 | GOTEST := gotestsum --junitfile $(GOTEST_DIR)/unit-tests.xml -- 55 | endif 56 | endif 57 | 58 | PROMU_VERSION ?= 0.17.0 59 | PROMU_URL := https://github.com/prometheus/promu/releases/download/v$(PROMU_VERSION)/promu-$(PROMU_VERSION).$(GO_BUILD_PLATFORM).tar.gz 60 | 61 | SKIP_GOLANGCI_LINT := 62 | GOLANGCI_LINT := 63 | GOLANGCI_LINT_OPTS ?= 64 | GOLANGCI_LINT_VERSION ?= v2.6.2 65 | GOLANGCI_FMT_OPTS ?= 66 | # golangci-lint only supports linux, darwin and windows platforms on i386/amd64/arm64. 67 | # windows isn't included here because of the path separator being different. 68 | ifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux darwin)) 69 | ifeq ($(GOHOSTARCH),$(filter $(GOHOSTARCH),amd64 i386 arm64)) 70 | # If we're in CI and there is an Actions file, that means the linter 71 | # is being run in Actions, so we don't need to run it here. 72 | ifneq (,$(SKIP_GOLANGCI_LINT)) 73 | GOLANGCI_LINT := 74 | else ifeq (,$(CIRCLE_JOB)) 75 | GOLANGCI_LINT := $(FIRST_GOPATH)/bin/golangci-lint 76 | else ifeq (,$(wildcard .github/workflows/golangci-lint.yml)) 77 | GOLANGCI_LINT := $(FIRST_GOPATH)/bin/golangci-lint 78 | endif 79 | endif 80 | endif 81 | 82 | PREFIX ?= $(shell pwd) 83 | BIN_DIR ?= $(shell pwd) 84 | DOCKER_IMAGE_TAG ?= $(subst /,-,$(shell git rev-parse --abbrev-ref HEAD)) 85 | DOCKERFILE_PATH ?= ./Dockerfile 86 | DOCKERBUILD_CONTEXT ?= ./ 87 | DOCKER_REPO ?= prom 88 | 89 | DOCKER_ARCHS ?= amd64 90 | 91 | BUILD_DOCKER_ARCHS = $(addprefix common-docker-,$(DOCKER_ARCHS)) 92 | PUBLISH_DOCKER_ARCHS = $(addprefix common-docker-publish-,$(DOCKER_ARCHS)) 93 | TAG_DOCKER_ARCHS = $(addprefix common-docker-tag-latest-,$(DOCKER_ARCHS)) 94 | 95 | SANITIZED_DOCKER_IMAGE_TAG := $(subst +,-,$(DOCKER_IMAGE_TAG)) 96 | 97 | ifeq ($(GOHOSTARCH),amd64) 98 | ifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux freebsd darwin windows)) 99 | # Only supported on amd64 100 | test-flags := -race 101 | endif 102 | endif 103 | 104 | # This rule is used to forward a target like "build" to "common-build". This 105 | # allows a new "build" target to be defined in a Makefile which includes this 106 | # one and override "common-build" without override warnings. 107 | %: common-% ; 108 | 109 | .PHONY: common-all 110 | common-all: precheck style check_license lint yamllint unused build test 111 | 112 | .PHONY: common-style 113 | common-style: 114 | @echo ">> checking code style" 115 | @fmtRes=$$($(GOFMT) -d $$(git ls-files '*.go' ':!:vendor/*' || find . -path ./vendor -prune -o -name '*.go' -print)); \ 116 | if [ -n "$${fmtRes}" ]; then \ 117 | echo "gofmt checking failed!"; echo "$${fmtRes}"; echo; \ 118 | echo "Please ensure you are using $$($(GO) version) for formatting code."; \ 119 | exit 1; \ 120 | fi 121 | 122 | .PHONY: common-check_license 123 | common-check_license: 124 | @echo ">> checking license header" 125 | @licRes=$$(for file in $$(git ls-files '*.go' ':!:vendor/*' || find . -path ./vendor -prune -o -type f -iname '*.go' -print) ; do \ 126 | awk 'NR<=3' $$file | grep -Eq "(Copyright|generated|GENERATED)" || echo $$file; \ 127 | done); \ 128 | if [ -n "$${licRes}" ]; then \ 129 | echo "license header checking failed:"; echo "$${licRes}"; \ 130 | exit 1; \ 131 | fi 132 | 133 | .PHONY: common-deps 134 | common-deps: 135 | @echo ">> getting dependencies" 136 | $(GO) mod download 137 | 138 | .PHONY: update-go-deps 139 | update-go-deps: 140 | @echo ">> updating Go dependencies" 141 | @for m in $$($(GO) list -mod=readonly -m -f '{{ if and (not .Indirect) (not .Main)}}{{.Path}}{{end}}' all); do \ 142 | $(GO) get $$m; \ 143 | done 144 | $(GO) mod tidy 145 | 146 | .PHONY: common-test-short 147 | common-test-short: $(GOTEST_DIR) 148 | @echo ">> running short tests" 149 | $(GOTEST) -short $(GOOPTS) $(pkgs) 150 | 151 | .PHONY: common-test 152 | common-test: $(GOTEST_DIR) 153 | @echo ">> running all tests" 154 | $(GOTEST) $(test-flags) $(GOOPTS) $(pkgs) 155 | 156 | $(GOTEST_DIR): 157 | @mkdir -p $@ 158 | 159 | .PHONY: common-format 160 | common-format: $(GOLANGCI_LINT) 161 | @echo ">> formatting code" 162 | $(GO) fmt $(pkgs) 163 | ifdef GOLANGCI_LINT 164 | @echo ">> formatting code with golangci-lint" 165 | $(GOLANGCI_LINT) fmt $(GOLANGCI_FMT_OPTS) 166 | endif 167 | 168 | .PHONY: common-vet 169 | common-vet: 170 | @echo ">> vetting code" 171 | $(GO) vet $(GOOPTS) $(pkgs) 172 | 173 | .PHONY: common-lint 174 | common-lint: $(GOLANGCI_LINT) 175 | ifdef GOLANGCI_LINT 176 | @echo ">> running golangci-lint" 177 | $(GOLANGCI_LINT) run $(GOLANGCI_LINT_OPTS) $(pkgs) 178 | endif 179 | 180 | .PHONY: common-lint-fix 181 | common-lint-fix: $(GOLANGCI_LINT) 182 | ifdef GOLANGCI_LINT 183 | @echo ">> running golangci-lint fix" 184 | $(GOLANGCI_LINT) run --fix $(GOLANGCI_LINT_OPTS) $(pkgs) 185 | endif 186 | 187 | .PHONY: common-yamllint 188 | common-yamllint: 189 | @echo ">> running yamllint on all YAML files in the repository" 190 | ifeq (, $(shell command -v yamllint 2> /dev/null)) 191 | @echo "yamllint not installed so skipping" 192 | else 193 | yamllint . 194 | endif 195 | 196 | # For backward-compatibility. 197 | .PHONY: common-staticcheck 198 | common-staticcheck: lint 199 | 200 | .PHONY: common-unused 201 | common-unused: 202 | @echo ">> running check for unused/missing packages in go.mod" 203 | $(GO) mod tidy 204 | @git diff --exit-code -- go.sum go.mod 205 | 206 | .PHONY: common-build 207 | common-build: promu 208 | @echo ">> building binaries" 209 | $(PROMU) build --prefix $(PREFIX) $(PROMU_BINARIES) 210 | 211 | .PHONY: common-tarball 212 | common-tarball: promu 213 | @echo ">> building release tarball" 214 | $(PROMU) tarball --prefix $(PREFIX) $(BIN_DIR) 215 | 216 | .PHONY: common-docker-repo-name 217 | common-docker-repo-name: 218 | @echo "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)" 219 | 220 | .PHONY: common-docker $(BUILD_DOCKER_ARCHS) 221 | common-docker: $(BUILD_DOCKER_ARCHS) 222 | $(BUILD_DOCKER_ARCHS): common-docker-%: 223 | docker build -t "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" \ 224 | -f $(DOCKERFILE_PATH) \ 225 | --build-arg ARCH="$*" \ 226 | --build-arg OS="linux" \ 227 | $(DOCKERBUILD_CONTEXT) 228 | 229 | .PHONY: common-docker-publish $(PUBLISH_DOCKER_ARCHS) 230 | common-docker-publish: $(PUBLISH_DOCKER_ARCHS) 231 | $(PUBLISH_DOCKER_ARCHS): common-docker-publish-%: 232 | docker push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" 233 | 234 | DOCKER_MAJOR_VERSION_TAG = $(firstword $(subst ., ,$(shell cat VERSION))) 235 | .PHONY: common-docker-tag-latest $(TAG_DOCKER_ARCHS) 236 | common-docker-tag-latest: $(TAG_DOCKER_ARCHS) 237 | $(TAG_DOCKER_ARCHS): common-docker-tag-latest-%: 238 | docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:latest" 239 | docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:v$(DOCKER_MAJOR_VERSION_TAG)" 240 | 241 | .PHONY: common-docker-manifest 242 | common-docker-manifest: 243 | 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)) 244 | DOCKER_CLI_EXPERIMENTAL=enabled docker manifest push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)" 245 | 246 | .PHONY: promu 247 | promu: $(PROMU) 248 | 249 | $(PROMU): 250 | $(eval PROMU_TMP := $(shell mktemp -d)) 251 | curl -s -L $(PROMU_URL) | tar -xvzf - -C $(PROMU_TMP) 252 | mkdir -p $(FIRST_GOPATH)/bin 253 | cp $(PROMU_TMP)/promu-$(PROMU_VERSION).$(GO_BUILD_PLATFORM)/promu $(FIRST_GOPATH)/bin/promu 254 | rm -r $(PROMU_TMP) 255 | 256 | .PHONY: common-proto 257 | common-proto: 258 | @echo ">> generating code from proto files" 259 | @./scripts/genproto.sh 260 | 261 | ifdef GOLANGCI_LINT 262 | $(GOLANGCI_LINT): 263 | mkdir -p $(FIRST_GOPATH)/bin 264 | curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/$(GOLANGCI_LINT_VERSION)/install.sh \ 265 | | sed -e '/install -d/d' \ 266 | | sh -s -- -b $(FIRST_GOPATH)/bin $(GOLANGCI_LINT_VERSION) 267 | endif 268 | 269 | .PHONY: common-print-golangci-lint-version 270 | common-print-golangci-lint-version: 271 | @echo $(GOLANGCI_LINT_VERSION) 272 | 273 | .PHONY: precheck 274 | precheck:: 275 | 276 | define PRECHECK_COMMAND_template = 277 | precheck:: $(1)_precheck 278 | 279 | PRECHECK_COMMAND_$(1) ?= $(1) $$(strip $$(PRECHECK_OPTIONS_$(1))) 280 | .PHONY: $(1)_precheck 281 | $(1)_precheck: 282 | @if ! $$(PRECHECK_COMMAND_$(1)) 1>/dev/null 2>&1; then \ 283 | echo "Execution of '$$(PRECHECK_COMMAND_$(1))' command failed. Is $(1) installed?"; \ 284 | exit 1; \ 285 | fi 286 | endef 287 | 288 | govulncheck: install-govulncheck 289 | govulncheck ./... 290 | 291 | install-govulncheck: 292 | command -v govulncheck > /dev/null || go install golang.org/x/vuln/cmd/govulncheck@latest 293 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "compress/gzip" 18 | "context" 19 | "fmt" 20 | "io" 21 | "log/slog" 22 | "net/http" 23 | "net/http/pprof" 24 | "net/url" 25 | "os" 26 | "os/signal" 27 | "path" 28 | "path/filepath" 29 | "strings" 30 | "syscall" 31 | 32 | "github.com/alecthomas/kingpin/v2" 33 | "github.com/golang/snappy" 34 | "github.com/prometheus/client_golang/prometheus" 35 | "github.com/prometheus/client_golang/prometheus/promhttp" 36 | "github.com/prometheus/common/model" 37 | "github.com/prometheus/common/promslog" 38 | "github.com/prometheus/common/route" 39 | "github.com/prometheus/common/version" 40 | "github.com/prometheus/exporter-toolkit/web" 41 | 42 | versioncollector "github.com/prometheus/client_golang/prometheus/collectors/version" 43 | dto "github.com/prometheus/client_model/go" 44 | promslogflag "github.com/prometheus/common/promslog/flag" 45 | webflag "github.com/prometheus/exporter-toolkit/web/kingpinflag" 46 | 47 | "github.com/prometheus/pushgateway/asset" 48 | "github.com/prometheus/pushgateway/handler" 49 | "github.com/prometheus/pushgateway/storage" 50 | 51 | api_v1 "github.com/prometheus/pushgateway/api/v1" 52 | ) 53 | 54 | func init() { 55 | prometheus.MustRegister(versioncollector.NewCollector("pushgateway")) 56 | } 57 | 58 | func main() { 59 | var ( 60 | app = kingpin.New(filepath.Base(os.Args[0]), "The Pushgateway").UsageWriter(os.Stdout) 61 | webConfig = webflag.AddFlags(app, ":9091") 62 | metricsPath = app.Flag("web.telemetry-path", "Path under which to expose metrics.").Default("/metrics").String() 63 | externalURL = app.Flag("web.external-url", "The URL under which the Pushgateway is externally reachable.").Default("").URL() 64 | routePrefix = app.Flag("web.route-prefix", "Prefix for the internal routes of web endpoints. Defaults to the path of --web.external-url.").Default("").String() 65 | enableLifeCycle = app.Flag("web.enable-lifecycle", "Enable shutdown via HTTP request.").Default("false").Bool() 66 | enableAdminAPI = app.Flag("web.enable-admin-api", "Enable API endpoints for admin control actions.").Default("false").Bool() 67 | persistenceFile = app.Flag("persistence.file", "File to persist metrics. If empty, metrics are only kept in memory.").Default("").String() 68 | persistenceInterval = app.Flag("persistence.interval", "The minimum interval at which to write out the persistence file.").Default("5m").Duration() 69 | pushUnchecked = app.Flag("push.disable-consistency-check", "Do not check consistency of pushed metrics. DANGEROUS.").Default("false").Bool() 70 | pushUTF8Names = app.Flag("push.enable-utf8-names", "Allow UTF-8 characters in metric and label names.").Default("false").Bool() 71 | promlogConfig = promslog.Config{Style: promslog.GoKitStyle} 72 | ) 73 | promslogflag.AddFlags(app, &promlogConfig) 74 | app.Version(version.Print("pushgateway")) 75 | app.HelpFlag.Short('h') 76 | kingpin.MustParse(app.Parse(os.Args[1:])) 77 | logger := promslog.New(&promlogConfig) 78 | 79 | *routePrefix = computeRoutePrefix(*routePrefix, *externalURL) 80 | externalPathPrefix := computeRoutePrefix("", *externalURL) 81 | 82 | logger.Info("starting pushgateway", "version", version.Info()) 83 | logger.Info("Build context", "build_context", version.BuildContext()) 84 | logger.Debug("external URL", "url", *externalURL) 85 | logger.Debug("path prefix used externally", "path", externalPathPrefix) 86 | logger.Debug("path prefix for internal routing", "path", *routePrefix) 87 | 88 | // flags is used to show command line flags on the status page. 89 | // Kingpin default flags are excluded as they would be confusing. 90 | flags := map[string]string{} 91 | boilerplateFlags := kingpin.New("", "").Version("") 92 | for _, f := range app.Model().Flags { 93 | if boilerplateFlags.GetFlag(f.Name) == nil { 94 | flags[f.Name] = f.Value.String() 95 | } 96 | } 97 | 98 | ms := storage.NewDiskMetricStore(*persistenceFile, *persistenceInterval, prometheus.DefaultGatherer, logger) 99 | 100 | if *pushUTF8Names { 101 | handler.EscapingScheme = model.ValueEncodingEscaping 102 | handler.ValidationScheme = model.UTF8Validation 103 | } else { 104 | handler.EscapingScheme = model.NoEscaping 105 | handler.ValidationScheme = model.LegacyValidation 106 | } 107 | 108 | // Create a Gatherer combining the DefaultGatherer and the metrics from the metric store. 109 | g := prometheus.Gatherers{ 110 | prometheus.DefaultGatherer, 111 | prometheus.GathererFunc(func() ([]*dto.MetricFamily, error) { return ms.GetMetricFamilies(), nil }), 112 | } 113 | 114 | r := route.New() 115 | r.Get(*routePrefix+"/-/healthy", handler.Healthy(ms).ServeHTTP) 116 | r.Get(*routePrefix+"/-/ready", handler.Ready(ms).ServeHTTP) 117 | r.Get( 118 | path.Join(*routePrefix, *metricsPath), 119 | promhttp.HandlerFor(g, promhttp.HandlerOpts{ 120 | ErrorLog: slog.NewLogLogger(logger.Handler(), slog.LevelError), 121 | }).ServeHTTP, 122 | ) 123 | 124 | // Handlers for pushing and deleting metrics. 125 | pushAPIPath := *routePrefix + "/metrics" 126 | for _, suffix := range []string{"", handler.Base64Suffix} { 127 | jobBase64Encoded := suffix == handler.Base64Suffix 128 | r.Put(pushAPIPath+"/job"+suffix+"/:job/*labels", handler.Push(ms, true, !*pushUnchecked, jobBase64Encoded, logger)) 129 | r.Post(pushAPIPath+"/job"+suffix+"/:job/*labels", handler.Push(ms, false, !*pushUnchecked, jobBase64Encoded, logger)) 130 | r.Del(pushAPIPath+"/job"+suffix+"/:job/*labels", handler.Delete(ms, jobBase64Encoded, logger)) 131 | r.Put(pushAPIPath+"/job"+suffix+"/:job", handler.Push(ms, true, !*pushUnchecked, jobBase64Encoded, logger)) 132 | r.Post(pushAPIPath+"/job"+suffix+"/:job", handler.Push(ms, false, !*pushUnchecked, jobBase64Encoded, logger)) 133 | r.Del(pushAPIPath+"/job"+suffix+"/:job", handler.Delete(ms, jobBase64Encoded, logger)) 134 | } 135 | r.Get(*routePrefix+"/static/*filepath", handler.Static(asset.Assets, *routePrefix).ServeHTTP) 136 | 137 | statusHandler := handler.Status(ms, asset.Assets, flags, externalPathPrefix, logger) 138 | r.Get(*routePrefix+"/status", statusHandler.ServeHTTP) 139 | r.Get(*routePrefix+"/", statusHandler.ServeHTTP) 140 | 141 | // Re-enable pprof. 142 | r.Get(*routePrefix+"/debug/pprof/*pprof", handlePprof) 143 | 144 | quitCh := make(chan struct{}) 145 | quitHandler := func(w http.ResponseWriter, r *http.Request) { 146 | fmt.Fprintf(w, "Requesting termination... Goodbye!") 147 | close(quitCh) 148 | } 149 | 150 | forbiddenAPINotEnabled := func(w http.ResponseWriter, _ *http.Request) { 151 | w.WriteHeader(http.StatusForbidden) 152 | w.Write([]byte("Lifecycle API is not enabled.")) 153 | } 154 | 155 | if *enableLifeCycle { 156 | r.Put(*routePrefix+"/-/quit", quitHandler) 157 | r.Post(*routePrefix+"/-/quit", quitHandler) 158 | } else { 159 | r.Put(*routePrefix+"/-/quit", forbiddenAPINotEnabled) 160 | r.Post(*routePrefix+"/-/quit", forbiddenAPINotEnabled) 161 | } 162 | 163 | r.Get("/-/quit", func(w http.ResponseWriter, _ *http.Request) { 164 | w.WriteHeader(http.StatusMethodNotAllowed) 165 | w.Write([]byte("Only POST or PUT requests allowed.")) 166 | }) 167 | 168 | mux := http.NewServeMux() 169 | mux.Handle("/", decodeRequest(r)) 170 | 171 | buildInfo := map[string]string{ 172 | "version": version.Version, 173 | "revision": version.Revision, 174 | "branch": version.Branch, 175 | "buildUser": version.BuildUser, 176 | "buildDate": version.BuildDate, 177 | "goVersion": version.GoVersion, 178 | } 179 | 180 | apiv1 := api_v1.New(logger, ms, flags, buildInfo) 181 | 182 | apiPath := "/api" 183 | if *routePrefix != "/" { 184 | apiPath = *routePrefix + apiPath 185 | } 186 | 187 | av1 := route.New() 188 | apiv1.Register(av1) 189 | if *enableAdminAPI { 190 | av1.Put("/admin/wipe", handler.WipeMetricStore(ms, logger).ServeHTTP) 191 | } 192 | 193 | mux.Handle(apiPath+"/v1/", http.StripPrefix(apiPath+"/v1", av1)) 194 | 195 | server := &http.Server{Handler: mux} 196 | 197 | go shutdownServerOnQuit(server, quitCh, logger) 198 | err := web.ListenAndServe(server, webConfig, logger) 199 | 200 | // In the case of a graceful shutdown, do not log the error. 201 | if err == http.ErrServerClosed { 202 | logger.Info("HTTP server stopped") 203 | } else { 204 | logger.Error("HTTP server stopped", "err", err) 205 | } 206 | 207 | if err := ms.Shutdown(); err != nil { 208 | logger.Error("problem shutting down metric storage", "err", err) 209 | } 210 | } 211 | 212 | func decodeRequest(h http.Handler) http.Handler { 213 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 214 | defer r.Body.Close() // Make sure the underlying io.Reader is closed. 215 | switch contentEncoding := r.Header.Get("Content-Encoding"); strings.ToLower(contentEncoding) { 216 | case "gzip": 217 | gr, err := gzip.NewReader(r.Body) 218 | if err != nil { 219 | http.Error(w, err.Error(), http.StatusBadRequest) 220 | return 221 | } 222 | defer gr.Close() 223 | r.Body = gr 224 | case "snappy": 225 | r.Body = io.NopCloser(snappy.NewReader(r.Body)) 226 | default: 227 | // Do nothing. 228 | } 229 | 230 | h.ServeHTTP(w, r) 231 | }) 232 | } 233 | 234 | func handlePprof(w http.ResponseWriter, r *http.Request) { 235 | switch route.Param(r.Context(), "pprof") { 236 | case "/cmdline": 237 | pprof.Cmdline(w, r) 238 | case "/profile": 239 | pprof.Profile(w, r) 240 | case "/symbol": 241 | pprof.Symbol(w, r) 242 | default: 243 | pprof.Index(w, r) 244 | } 245 | } 246 | 247 | // computeRoutePrefix returns the effective route prefix based on the 248 | // provided flag values for --web.route-prefix and 249 | // --web.external-url. With prefix empty, the path of externalURL is 250 | // used instead. A prefix "/" results in an empty returned prefix. Any 251 | // non-empty prefix is normalized to start, but not to end, with "/". 252 | func computeRoutePrefix(prefix string, externalURL *url.URL) string { 253 | if prefix == "" { 254 | prefix = externalURL.Path 255 | } 256 | 257 | if prefix == "/" { 258 | return "" 259 | } 260 | 261 | // Ensure prefix starts with "/". 262 | if !strings.HasPrefix(prefix, "/") { 263 | prefix = "/" + prefix 264 | } 265 | 266 | prefix = strings.TrimSuffix(prefix, "/") 267 | 268 | return prefix 269 | } 270 | 271 | // shutdownServerOnQuit shutdowns the provided server upon closing the provided 272 | // quitCh or upon receiving a SIGINT or SIGTERM. 273 | func shutdownServerOnQuit(server *http.Server, quitCh <-chan struct{}, logger *slog.Logger) error { 274 | notifier := make(chan os.Signal, 1) 275 | signal.Notify(notifier, os.Interrupt, syscall.SIGTERM) 276 | 277 | select { 278 | case <-notifier: 279 | logger.Info("received SIGINT/SIGTERM; exiting gracefully...") 280 | break 281 | case <-quitCh: 282 | logger.Warn("received termination request via web service, exiting gracefully...") 283 | break 284 | } 285 | return server.Shutdown(context.Background()) 286 | } 287 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /api/v1/api_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | package v1 14 | 15 | import ( 16 | "bytes" 17 | "encoding/json" 18 | "net/http" 19 | "net/http/httptest" 20 | "reflect" 21 | "testing" 22 | "time" 23 | 24 | //nolint:staticcheck // Ignore SA1019. Dependencies use the deprecated package, so we have to, too. 25 | "github.com/golang/protobuf/proto" 26 | "github.com/prometheus/common/promslog" 27 | 28 | dto "github.com/prometheus/client_model/go" 29 | 30 | "github.com/prometheus/pushgateway/storage" 31 | "github.com/prometheus/pushgateway/testutil" 32 | ) 33 | 34 | var ( 35 | logger = promslog.NewNopLogger() 36 | testFlags = map[string]string{ 37 | "flag1": "value1", 38 | "flag2": "value2", 39 | "flag3": "value3", 40 | } 41 | testBuildInfo = map[string]string{ 42 | "build1": "value1", 43 | "build2": "value2", 44 | "build3": "value3", 45 | } 46 | 47 | mf1 = &dto.MetricFamily{ 48 | Name: proto.String("mf1"), 49 | Type: dto.MetricType_SUMMARY.Enum(), 50 | Metric: []*dto.Metric{ 51 | { 52 | Label: []*dto.LabelPair{ 53 | { 54 | Name: proto.String("instance"), 55 | Value: proto.String(`inst'a"n\ce1`), 56 | }, 57 | { 58 | Name: proto.String("job"), 59 | Value: proto.String("Björn"), 60 | }, 61 | }, 62 | Summary: &dto.Summary{ 63 | SampleCount: proto.Uint64(0), 64 | SampleSum: proto.Float64(0), 65 | }, 66 | }, 67 | }, 68 | } 69 | mfh = &dto.MetricFamily{ 70 | Name: proto.String("mfh"), 71 | Type: dto.MetricType_HISTOGRAM.Enum(), 72 | Metric: []*dto.Metric{ 73 | { 74 | Label: []*dto.LabelPair{ 75 | { 76 | Name: proto.String("testing"), 77 | Value: proto.String("int classic histogram"), 78 | }, 79 | }, 80 | Histogram: &dto.Histogram{ 81 | SampleCount: proto.Uint64(20), 82 | SampleSum: proto.Float64(99.23), 83 | Bucket: []*dto.Bucket{ 84 | { 85 | UpperBound: proto.Float64(250000), 86 | CumulativeCount: proto.Uint64(3), 87 | }, 88 | { 89 | UpperBound: proto.Float64(500000), 90 | CumulativeCount: proto.Uint64(17), 91 | }, 92 | }, 93 | }, 94 | }, 95 | { 96 | Label: []*dto.LabelPair{ 97 | { 98 | Name: proto.String("testing"), 99 | Value: proto.String("float classic histogram"), 100 | }, 101 | }, 102 | Histogram: &dto.Histogram{ 103 | SampleCountFloat: proto.Float64(20), 104 | SampleSum: proto.Float64(99.23), 105 | Bucket: []*dto.Bucket{ 106 | { 107 | UpperBound: proto.Float64(250000), 108 | CumulativeCountFloat: proto.Float64(3), 109 | }, 110 | { 111 | UpperBound: proto.Float64(500000), 112 | CumulativeCountFloat: proto.Float64(17), 113 | }, 114 | }, 115 | }, 116 | }, 117 | { 118 | Label: []*dto.LabelPair{ 119 | { 120 | Name: proto.String("testing"), 121 | Value: proto.String("int native histogram"), 122 | }, 123 | }, 124 | Histogram: &dto.Histogram{ 125 | SampleCount: proto.Uint64(20), 126 | SampleSum: proto.Float64(99.23), 127 | Schema: proto.Int32(1), 128 | NegativeDelta: []int64{0, 2, -2, 0}, 129 | PositiveDelta: []int64{0, 2, -2, 0}, 130 | PositiveSpan: []*dto.BucketSpan{ 131 | { 132 | Offset: proto.Int32(0), 133 | Length: proto.Uint32(2), 134 | }, 135 | { 136 | Offset: proto.Int32(0), 137 | Length: proto.Uint32(2), 138 | }, 139 | }, 140 | NegativeSpan: []*dto.BucketSpan{ 141 | { 142 | Offset: proto.Int32(0), 143 | Length: proto.Uint32(2), 144 | }, 145 | { 146 | Offset: proto.Int32(0), 147 | Length: proto.Uint32(2), 148 | }, 149 | }, 150 | }, 151 | }, 152 | { 153 | Label: []*dto.LabelPair{ 154 | { 155 | Name: proto.String("testing"), 156 | Value: proto.String("float native histogram"), 157 | }, 158 | }, 159 | Histogram: &dto.Histogram{ 160 | SampleCountFloat: proto.Float64(20), 161 | SampleSum: proto.Float64(99.23), 162 | Schema: proto.Int32(1), 163 | NegativeCount: []float64{2, 2, -2, 0}, 164 | PositiveCount: []float64{2, 2, -2, 0}, 165 | PositiveSpan: []*dto.BucketSpan{ 166 | { 167 | Offset: proto.Int32(0), 168 | Length: proto.Uint32(2), 169 | }, 170 | { 171 | Offset: proto.Int32(0), 172 | Length: proto.Uint32(2), 173 | }, 174 | }, 175 | NegativeSpan: []*dto.BucketSpan{ 176 | { 177 | Offset: proto.Int32(0), 178 | Length: proto.Uint32(2), 179 | }, 180 | { 181 | Offset: proto.Int32(0), 182 | Length: proto.Uint32(2), 183 | }, 184 | }, 185 | }, 186 | }, 187 | }, 188 | } 189 | 190 | grouping1 = map[string]string{ 191 | "job": "Björn", 192 | "instance": `inst'a"n\ce1`, 193 | } 194 | ) 195 | 196 | func convertMap(m map[string]string) map[string]any { 197 | result := map[string]any{} 198 | for k, v := range m { 199 | result[k] = v 200 | } 201 | return result 202 | } 203 | 204 | func TestStatusAPI(t *testing.T) { 205 | dms := storage.NewDiskMetricStore("", 100*time.Millisecond, nil, logger) 206 | testAPI := New(logger, dms, testFlags, testBuildInfo) 207 | 208 | req, err := http.NewRequest("GET", "http://example.org/", &bytes.Buffer{}) 209 | if err != nil { 210 | t.Fatal(err) 211 | } 212 | 213 | w := httptest.NewRecorder() 214 | 215 | testResponse := response{} 216 | testAPI.status(w, req) 217 | json.Unmarshal(w.Body.Bytes(), &testResponse) 218 | jsonData := testResponse.Data.(map[string]any) 219 | responseFlagData := jsonData["flags"].(map[string]any) 220 | responseBuildInfo := jsonData["build_information"].(map[string]any) 221 | 222 | if expected, got := http.StatusOK, w.Code; expected != got { 223 | t.Errorf("Wanted status code %v, got %v.", expected, got) 224 | } 225 | 226 | if !reflect.DeepEqual(responseFlagData, convertMap(testFlags)) { 227 | t.Errorf("Wanted following flags %q, got %q.", testFlags, responseFlagData) 228 | } 229 | 230 | if !reflect.DeepEqual(responseBuildInfo, convertMap(testBuildInfo)) { 231 | t.Errorf("Wanted following build info %q, got %q.", testBuildInfo, responseBuildInfo) 232 | } 233 | } 234 | 235 | func TestMetricsAPI(t *testing.T) { 236 | dms := storage.NewDiskMetricStore("", 100*time.Millisecond, nil, logger) 237 | testAPI := New(logger, dms, testFlags, testBuildInfo) 238 | 239 | req, err := http.NewRequest("GET", "http://example.org/", &bytes.Buffer{}) 240 | if err != nil { 241 | t.Fatal(err) 242 | } 243 | 244 | w := httptest.NewRecorder() 245 | 246 | testAPI.metrics(w, req) 247 | 248 | if expected, got := http.StatusOK, w.Code; expected != got { 249 | t.Errorf("Wanted status code %v, got %v.", expected, got) 250 | } 251 | 252 | requiredResponse := `{"status":"success","data":[]}` 253 | 254 | if expected, got := requiredResponse, w.Body.String(); expected != got { 255 | t.Errorf("Wanted response %q, got %q.", requiredResponse, w.Body.String()) 256 | } 257 | 258 | testTime, _ := time.Parse(time.RFC3339Nano, "2020-03-10T00:54:08.025744841+05:30") 259 | 260 | errCh := make(chan error, 1) 261 | 262 | dms.SubmitWriteRequest(storage.WriteRequest{ 263 | Labels: grouping1, 264 | Timestamp: testTime, 265 | MetricFamilies: testutil.MetricFamiliesMap(mf1, mfh), 266 | Done: errCh, 267 | }) 268 | 269 | for err := range errCh { 270 | t.Fatal("Unexpected error:", err) 271 | } 272 | 273 | w = httptest.NewRecorder() 274 | 275 | testAPI.metrics(w, req) 276 | 277 | var prettyJSON bytes.Buffer 278 | json.Indent(&prettyJSON, w.Body.Bytes(), "", "\t") 279 | 280 | requiredResponse = `{ 281 | "status": "success", 282 | "data": [ 283 | { 284 | "labels": { 285 | "instance": "inst'a\"n\\ce1", 286 | "job": "Björn" 287 | }, 288 | "last_push_successful": true, 289 | "mf1": { 290 | "time_stamp": "2020-03-10T00:54:08.025744841+05:30", 291 | "type": "SUMMARY", 292 | "metrics": [ 293 | { 294 | "count": "0", 295 | "labels": { 296 | "instance": "inst'a\"n\\ce1", 297 | "job": "Björn" 298 | }, 299 | "quantiles": {}, 300 | "sum": "0" 301 | } 302 | ] 303 | }, 304 | "mfh": { 305 | "time_stamp": "2020-03-10T00:54:08.025744841+05:30", 306 | "type": "HISTOGRAM", 307 | "metrics": [ 308 | { 309 | "buckets": { 310 | "250000": "3", 311 | "500000": "17" 312 | }, 313 | "count": "20", 314 | "labels": { 315 | "instance": "inst'a\"n\\ce1", 316 | "job": "Björn", 317 | "testing": "int classic histogram" 318 | }, 319 | "sum": "99.23" 320 | }, 321 | { 322 | "buckets": { 323 | "250000": "3", 324 | "500000": "17" 325 | }, 326 | "count": "20", 327 | "labels": { 328 | "instance": "inst'a\"n\\ce1", 329 | "job": "Björn", 330 | "testing": "float classic histogram" 331 | }, 332 | "sum": "99.23" 333 | }, 334 | { 335 | "buckets": [ 336 | [ 337 | 1, 338 | "-1.414213562373095", 339 | "-1", 340 | "2" 341 | ], 342 | [ 343 | 0, 344 | "1", 345 | "1.414213562373095", 346 | "2" 347 | ] 348 | ], 349 | "count": "20", 350 | "labels": { 351 | "instance": "inst'a\"n\\ce1", 352 | "job": "Björn", 353 | "testing": "int native histogram" 354 | }, 355 | "sum": "99.23" 356 | }, 357 | { 358 | "buckets": [ 359 | [ 360 | 1, 361 | "-2", 362 | "-1.414213562373095", 363 | "-2" 364 | ], 365 | [ 366 | 1, 367 | "-1.414213562373095", 368 | "-1", 369 | "2" 370 | ], 371 | [ 372 | 1, 373 | "-1", 374 | "-0.7071067811865475", 375 | "2" 376 | ], 377 | [ 378 | 0, 379 | "0.7071067811865475", 380 | "1", 381 | "2" 382 | ], 383 | [ 384 | 0, 385 | "1", 386 | "1.414213562373095", 387 | "2" 388 | ], 389 | [ 390 | 0, 391 | "1.414213562373095", 392 | "2", 393 | "-2" 394 | ] 395 | ], 396 | "count": "20", 397 | "labels": { 398 | "instance": "inst'a\"n\\ce1", 399 | "job": "Björn", 400 | "testing": "float native histogram" 401 | }, 402 | "sum": "99.23" 403 | } 404 | ] 405 | }, 406 | "push_failure_time_seconds": { 407 | "time_stamp": "2020-03-10T00:54:08.025744841+05:30", 408 | "type": "GAUGE", 409 | "help": "Last Unix time when changing this group in the Pushgateway failed.", 410 | "metrics": [ 411 | { 412 | "labels": { 413 | "instance": "inst'a\"n\\ce1", 414 | "job": "Björn" 415 | }, 416 | "value": "0" 417 | } 418 | ] 419 | }, 420 | "push_time_seconds": { 421 | "time_stamp": "2020-03-10T00:54:08.025744841+05:30", 422 | "type": "GAUGE", 423 | "help": "Last Unix time when changing this group in the Pushgateway succeeded.", 424 | "metrics": [ 425 | { 426 | "labels": { 427 | "instance": "inst'a\"n\\ce1", 428 | "job": "Björn" 429 | }, 430 | "value": "1.583781848025745e+09" 431 | } 432 | ] 433 | } 434 | } 435 | ] 436 | }` 437 | 438 | if expected, got := http.StatusOK, w.Code; expected != got { 439 | t.Errorf("Wanted status code %v, got %v.", expected, got) 440 | } 441 | 442 | if expected, got := requiredResponse, prettyJSON.String(); expected != got { 443 | t.Errorf("Wanted response %q, got %q.", expected, got) 444 | } 445 | } 446 | -------------------------------------------------------------------------------- /resources/template.html: -------------------------------------------------------------------------------- 1 | 2 | {{/* 3 | Copyright 2014 The Prometheus Authors 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */}} 16 | 17 | 18 | 19 | 20 | Prometheus Pushgateway 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 56 |
57 | {{- $data := .}} 58 | {{- if eq (index .Flags "web.enable-admin-api") "true"}} 59 |
60 | 61 |
62 | {{- end}} 63 |
64 | {{- range .MetricGroups}} 65 | {{- $gCount := $data.Count}} 66 |
67 |
68 |

69 | 76 | {{- if not $metricGroup.LastPushSuccess}}Last push failed!{{end}} 77 | 78 |

79 |
80 |
81 |
82 |
83 | {{- range $name, $tmf := .Metrics }} 84 | {{- $mCount := $data.Count}} 85 |
86 |
87 |

88 | 95 |

96 |
97 |
98 |
99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | {{- range $tmf.GetMetricFamily.Metric}} 108 | 109 | 114 | 186 | 187 | {{- end}} 188 | 189 |
LabelsValue
110 | {{- range .Label}} 111 | {{formatLabelName .Name}}="{{.GetValue}}" 112 | {{- end}} 113 | 115 | {{- with .Gauge}} 116 | {{- value .GetValue}} 117 | {{- else}} 118 | {{- with .Counter}} 119 | {{- value .GetValue}} 120 | {{- else}} 121 | {{- with .Untyped}} 122 | {{- value .GetValue}} 123 | {{- else}} 124 | {{- with .Summary}} 125 | 126 | {{- range .Quantile}} 127 | 128 | 129 | 130 | 131 | {{- end}} 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 |
Quantile {{.GetQuantile}}{{value .GetValue}}
Sample Count{{.GetSampleCount}}
Sample Sum{{value .GetSampleSum}}
141 | {{- else}} 142 | {{- $h := .Histogram}} 143 | {{- with .Histogram}} 144 | {{- $numBuckets := len .Bucket}} 145 | {{- $numNBuckets := add (len .PositiveSpan) (len .NegativeSpan)}} 146 | 147 | {{- if gt $numNBuckets 0}} 148 | 149 | 150 | 151 | 152 | {{- end}} 153 | {{- if gt $numBuckets 0}} 154 | {{- range .Bucket}} 155 | 156 | 157 | 158 | 159 | {{- end}} 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | {{- end}} 169 | {{- if eq (add $numNBuckets $numBuckets) 0}} 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | {{- end}} 179 |
Native Histogram representation{{formatHistogram $h}}
Sample values ≤ {{value .GetUpperBound}}{{.GetCumulativeCount}}
Total sample Count{{.GetSampleCount}}
Sample Sum{{value .GetSampleSum}}
Total sample Count{{.GetSampleCount}}
Sample Sum{{value .GetSampleSum}}
180 | {{- end}} 181 | {{- end}} 182 | {{- end}} 183 | {{- end}} 184 | {{- end}} 185 |
190 |
191 |
192 |
193 | {{- end}} 194 |
195 |
196 |
197 |
198 | {{- end}} 199 |
200 |
201 | 202 | 237 | 238 | 239 | 258 | 259 | 260 | 279 | 280 | 281 | 282 | -------------------------------------------------------------------------------- /resources/static/bootstrap4-glyphicons/css/bootstrap-glyphicons.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.7 (http://getbootstrap.com) 3 | * Copyright 2011-2018 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | 7 | @font-face { 8 | font-family: 'Glyphicons Halflings'; 9 | src: url('../fonts/glyphicons/glyphicons-halflings-regular.eot'); 10 | src: url('../fonts/glyphicons/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('../fonts/glyphicons/glyphicons-halflings-regular.woff2') format('woff2'), url('../fonts/glyphicons/glyphicons-halflings-regular.woff') format('woff'), url('../fonts/glyphicons/glyphicons-halflings-regular.ttf') format('truetype'), url('../fonts/glyphicons/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg'); 11 | } 12 | .glyphicon { 13 | position: relative; 14 | top: 1px; 15 | display: inline-block; 16 | font-family: 'Glyphicons Halflings'; 17 | font-style: normal; 18 | font-weight: normal; 19 | line-height: 1; 20 | -webkit-font-smoothing: antialiased; 21 | -moz-osx-font-smoothing: grayscale; 22 | } 23 | .glyphicon-asterisk:before { 24 | content: "\002a"; 25 | } 26 | .glyphicon-plus:before { 27 | content: "\002b"; 28 | } 29 | .glyphicon-euro:before, 30 | .glyphicon-eur:before { 31 | content: "\20ac"; 32 | } 33 | .glyphicon-minus:before { 34 | content: "\2212"; 35 | } 36 | .glyphicon-cloud:before { 37 | content: "\2601"; 38 | } 39 | .glyphicon-envelope:before { 40 | content: "\2709"; 41 | } 42 | .glyphicon-pencil:before { 43 | content: "\270f"; 44 | } 45 | .glyphicon-glass:before { 46 | content: "\e001"; 47 | } 48 | .glyphicon-music:before { 49 | content: "\e002"; 50 | } 51 | .glyphicon-search:before { 52 | content: "\e003"; 53 | } 54 | .glyphicon-heart:before { 55 | content: "\e005"; 56 | } 57 | .glyphicon-star:before { 58 | content: "\e006"; 59 | } 60 | .glyphicon-star-empty:before { 61 | content: "\e007"; 62 | } 63 | .glyphicon-user:before { 64 | content: "\e008"; 65 | } 66 | .glyphicon-film:before { 67 | content: "\e009"; 68 | } 69 | .glyphicon-th-large:before { 70 | content: "\e010"; 71 | } 72 | .glyphicon-th:before { 73 | content: "\e011"; 74 | } 75 | .glyphicon-th-list:before { 76 | content: "\e012"; 77 | } 78 | .glyphicon-ok:before { 79 | content: "\e013"; 80 | } 81 | .glyphicon-remove:before { 82 | content: "\e014"; 83 | } 84 | .glyphicon-zoom-in:before { 85 | content: "\e015"; 86 | } 87 | .glyphicon-zoom-out:before { 88 | content: "\e016"; 89 | } 90 | .glyphicon-off:before { 91 | content: "\e017"; 92 | } 93 | .glyphicon-signal:before { 94 | content: "\e018"; 95 | } 96 | .glyphicon-cog:before { 97 | content: "\e019"; 98 | } 99 | .glyphicon-trash:before { 100 | content: "\e020"; 101 | } 102 | .glyphicon-home:before { 103 | content: "\e021"; 104 | } 105 | .glyphicon-file:before { 106 | content: "\e022"; 107 | } 108 | .glyphicon-time:before { 109 | content: "\e023"; 110 | } 111 | .glyphicon-road:before { 112 | content: "\e024"; 113 | } 114 | .glyphicon-download-alt:before { 115 | content: "\e025"; 116 | } 117 | .glyphicon-download:before { 118 | content: "\e026"; 119 | } 120 | .glyphicon-upload:before { 121 | content: "\e027"; 122 | } 123 | .glyphicon-inbox:before { 124 | content: "\e028"; 125 | } 126 | .glyphicon-play-circle:before { 127 | content: "\e029"; 128 | } 129 | .glyphicon-repeat:before { 130 | content: "\e030"; 131 | } 132 | .glyphicon-refresh:before { 133 | content: "\e031"; 134 | } 135 | .glyphicon-list-alt:before { 136 | content: "\e032"; 137 | } 138 | .glyphicon-lock:before { 139 | content: "\e033"; 140 | } 141 | .glyphicon-flag:before { 142 | content: "\e034"; 143 | } 144 | .glyphicon-headphones:before { 145 | content: "\e035"; 146 | } 147 | .glyphicon-volume-off:before { 148 | content: "\e036"; 149 | } 150 | .glyphicon-volume-down:before { 151 | content: "\e037"; 152 | } 153 | .glyphicon-volume-up:before { 154 | content: "\e038"; 155 | } 156 | .glyphicon-qrcode:before { 157 | content: "\e039"; 158 | } 159 | .glyphicon-barcode:before { 160 | content: "\e040"; 161 | } 162 | .glyphicon-tag:before { 163 | content: "\e041"; 164 | } 165 | .glyphicon-tags:before { 166 | content: "\e042"; 167 | } 168 | .glyphicon-book:before { 169 | content: "\e043"; 170 | } 171 | .glyphicon-bookmark:before { 172 | content: "\e044"; 173 | } 174 | .glyphicon-print:before { 175 | content: "\e045"; 176 | } 177 | .glyphicon-camera:before { 178 | content: "\e046"; 179 | } 180 | .glyphicon-font:before { 181 | content: "\e047"; 182 | } 183 | .glyphicon-bold:before { 184 | content: "\e048"; 185 | } 186 | .glyphicon-italic:before { 187 | content: "\e049"; 188 | } 189 | .glyphicon-text-height:before { 190 | content: "\e050"; 191 | } 192 | .glyphicon-text-width:before { 193 | content: "\e051"; 194 | } 195 | .glyphicon-align-left:before { 196 | content: "\e052"; 197 | } 198 | .glyphicon-align-center:before { 199 | content: "\e053"; 200 | } 201 | .glyphicon-align-right:before { 202 | content: "\e054"; 203 | } 204 | .glyphicon-align-justify:before { 205 | content: "\e055"; 206 | } 207 | .glyphicon-list:before { 208 | content: "\e056"; 209 | } 210 | .glyphicon-indent-left:before { 211 | content: "\e057"; 212 | } 213 | .glyphicon-indent-right:before { 214 | content: "\e058"; 215 | } 216 | .glyphicon-facetime-video:before { 217 | content: "\e059"; 218 | } 219 | .glyphicon-picture:before { 220 | content: "\e060"; 221 | } 222 | .glyphicon-map-marker:before { 223 | content: "\e062"; 224 | } 225 | .glyphicon-adjust:before { 226 | content: "\e063"; 227 | } 228 | .glyphicon-tint:before { 229 | content: "\e064"; 230 | } 231 | .glyphicon-edit:before { 232 | content: "\e065"; 233 | } 234 | .glyphicon-share:before { 235 | content: "\e066"; 236 | } 237 | .glyphicon-check:before { 238 | content: "\e067"; 239 | } 240 | .glyphicon-move:before { 241 | content: "\e068"; 242 | } 243 | .glyphicon-step-backward:before { 244 | content: "\e069"; 245 | } 246 | .glyphicon-fast-backward:before { 247 | content: "\e070"; 248 | } 249 | .glyphicon-backward:before { 250 | content: "\e071"; 251 | } 252 | .glyphicon-play:before { 253 | content: "\e072"; 254 | } 255 | .glyphicon-pause:before { 256 | content: "\e073"; 257 | } 258 | .glyphicon-stop:before { 259 | content: "\e074"; 260 | } 261 | .glyphicon-forward:before { 262 | content: "\e075"; 263 | } 264 | .glyphicon-fast-forward:before { 265 | content: "\e076"; 266 | } 267 | .glyphicon-step-forward:before { 268 | content: "\e077"; 269 | } 270 | .glyphicon-eject:before { 271 | content: "\e078"; 272 | } 273 | .glyphicon-chevron-left:before { 274 | content: "\e079"; 275 | } 276 | .glyphicon-chevron-right:before { 277 | content: "\e080"; 278 | } 279 | .glyphicon-plus-sign:before { 280 | content: "\e081"; 281 | } 282 | .glyphicon-minus-sign:before { 283 | content: "\e082"; 284 | } 285 | .glyphicon-remove-sign:before { 286 | content: "\e083"; 287 | } 288 | .glyphicon-ok-sign:before { 289 | content: "\e084"; 290 | } 291 | .glyphicon-question-sign:before { 292 | content: "\e085"; 293 | } 294 | .glyphicon-info-sign:before { 295 | content: "\e086"; 296 | } 297 | .glyphicon-screenshot:before { 298 | content: "\e087"; 299 | } 300 | .glyphicon-remove-circle:before { 301 | content: "\e088"; 302 | } 303 | .glyphicon-ok-circle:before { 304 | content: "\e089"; 305 | } 306 | .glyphicon-ban-circle:before { 307 | content: "\e090"; 308 | } 309 | .glyphicon-arrow-left:before { 310 | content: "\e091"; 311 | } 312 | .glyphicon-arrow-right:before { 313 | content: "\e092"; 314 | } 315 | .glyphicon-arrow-up:before { 316 | content: "\e093"; 317 | } 318 | .glyphicon-arrow-down:before { 319 | content: "\e094"; 320 | } 321 | .glyphicon-share-alt:before { 322 | content: "\e095"; 323 | } 324 | .glyphicon-resize-full:before { 325 | content: "\e096"; 326 | } 327 | .glyphicon-resize-small:before { 328 | content: "\e097"; 329 | } 330 | .glyphicon-exclamation-sign:before { 331 | content: "\e101"; 332 | } 333 | .glyphicon-gift:before { 334 | content: "\e102"; 335 | } 336 | .glyphicon-leaf:before { 337 | content: "\e103"; 338 | } 339 | .glyphicon-fire:before { 340 | content: "\e104"; 341 | } 342 | .glyphicon-eye-open:before { 343 | content: "\e105"; 344 | } 345 | .glyphicon-eye-close:before { 346 | content: "\e106"; 347 | } 348 | .glyphicon-warning-sign:before { 349 | content: "\e107"; 350 | } 351 | .glyphicon-plane:before { 352 | content: "\e108"; 353 | } 354 | .glyphicon-calendar:before { 355 | content: "\e109"; 356 | } 357 | .glyphicon-random:before { 358 | content: "\e110"; 359 | } 360 | .glyphicon-comment:before { 361 | content: "\e111"; 362 | } 363 | .glyphicon-magnet:before { 364 | content: "\e112"; 365 | } 366 | .glyphicon-chevron-up:before { 367 | content: "\e113"; 368 | } 369 | .glyphicon-chevron-down:before { 370 | content: "\e114"; 371 | } 372 | .glyphicon-retweet:before { 373 | content: "\e115"; 374 | } 375 | .glyphicon-shopping-cart:before { 376 | content: "\e116"; 377 | } 378 | .glyphicon-folder-close:before { 379 | content: "\e117"; 380 | } 381 | .glyphicon-folder-open:before { 382 | content: "\e118"; 383 | } 384 | .glyphicon-resize-vertical:before { 385 | content: "\e119"; 386 | } 387 | .glyphicon-resize-horizontal:before { 388 | content: "\e120"; 389 | } 390 | .glyphicon-hdd:before { 391 | content: "\e121"; 392 | } 393 | .glyphicon-bullhorn:before { 394 | content: "\e122"; 395 | } 396 | .glyphicon-bell:before { 397 | content: "\e123"; 398 | } 399 | .glyphicon-certificate:before { 400 | content: "\e124"; 401 | } 402 | .glyphicon-thumbs-up:before { 403 | content: "\e125"; 404 | } 405 | .glyphicon-thumbs-down:before { 406 | content: "\e126"; 407 | } 408 | .glyphicon-hand-right:before { 409 | content: "\e127"; 410 | } 411 | .glyphicon-hand-left:before { 412 | content: "\e128"; 413 | } 414 | .glyphicon-hand-up:before { 415 | content: "\e129"; 416 | } 417 | .glyphicon-hand-down:before { 418 | content: "\e130"; 419 | } 420 | .glyphicon-circle-arrow-right:before { 421 | content: "\e131"; 422 | } 423 | .glyphicon-circle-arrow-left:before { 424 | content: "\e132"; 425 | } 426 | .glyphicon-circle-arrow-up:before { 427 | content: "\e133"; 428 | } 429 | .glyphicon-circle-arrow-down:before { 430 | content: "\e134"; 431 | } 432 | .glyphicon-globe:before { 433 | content: "\e135"; 434 | } 435 | .glyphicon-wrench:before { 436 | content: "\e136"; 437 | } 438 | .glyphicon-tasks:before { 439 | content: "\e137"; 440 | } 441 | .glyphicon-filter:before { 442 | content: "\e138"; 443 | } 444 | .glyphicon-briefcase:before { 445 | content: "\e139"; 446 | } 447 | .glyphicon-fullscreen:before { 448 | content: "\e140"; 449 | } 450 | .glyphicon-dashboard:before { 451 | content: "\e141"; 452 | } 453 | .glyphicon-paperclip:before { 454 | content: "\e142"; 455 | } 456 | .glyphicon-heart-empty:before { 457 | content: "\e143"; 458 | } 459 | .glyphicon-link:before { 460 | content: "\e144"; 461 | } 462 | .glyphicon-phone:before { 463 | content: "\e145"; 464 | } 465 | .glyphicon-pushpin:before { 466 | content: "\e146"; 467 | } 468 | .glyphicon-usd:before { 469 | content: "\e148"; 470 | } 471 | .glyphicon-gbp:before { 472 | content: "\e149"; 473 | } 474 | .glyphicon-sort:before { 475 | content: "\e150"; 476 | } 477 | .glyphicon-sort-by-alphabet:before { 478 | content: "\e151"; 479 | } 480 | .glyphicon-sort-by-alphabet-alt:before { 481 | content: "\e152"; 482 | } 483 | .glyphicon-sort-by-order:before { 484 | content: "\e153"; 485 | } 486 | .glyphicon-sort-by-order-alt:before { 487 | content: "\e154"; 488 | } 489 | .glyphicon-sort-by-attributes:before { 490 | content: "\e155"; 491 | } 492 | .glyphicon-sort-by-attributes-alt:before { 493 | content: "\e156"; 494 | } 495 | .glyphicon-unchecked:before { 496 | content: "\e157"; 497 | } 498 | .glyphicon-expand:before { 499 | content: "\e158"; 500 | } 501 | .glyphicon-collapse-down:before { 502 | content: "\e159"; 503 | } 504 | .glyphicon-collapse-up:before { 505 | content: "\e160"; 506 | } 507 | .glyphicon-log-in:before { 508 | content: "\e161"; 509 | } 510 | .glyphicon-flash:before { 511 | content: "\e162"; 512 | } 513 | .glyphicon-log-out:before { 514 | content: "\e163"; 515 | } 516 | .glyphicon-new-window:before { 517 | content: "\e164"; 518 | } 519 | .glyphicon-record:before { 520 | content: "\e165"; 521 | } 522 | .glyphicon-save:before { 523 | content: "\e166"; 524 | } 525 | .glyphicon-open:before { 526 | content: "\e167"; 527 | } 528 | .glyphicon-saved:before { 529 | content: "\e168"; 530 | } 531 | .glyphicon-import:before { 532 | content: "\e169"; 533 | } 534 | .glyphicon-export:before { 535 | content: "\e170"; 536 | } 537 | .glyphicon-send:before { 538 | content: "\e171"; 539 | } 540 | .glyphicon-floppy-disk:before { 541 | content: "\e172"; 542 | } 543 | .glyphicon-floppy-saved:before { 544 | content: "\e173"; 545 | } 546 | .glyphicon-floppy-remove:before { 547 | content: "\e174"; 548 | } 549 | .glyphicon-floppy-save:before { 550 | content: "\e175"; 551 | } 552 | .glyphicon-floppy-open:before { 553 | content: "\e176"; 554 | } 555 | .glyphicon-credit-card:before { 556 | content: "\e177"; 557 | } 558 | .glyphicon-transfer:before { 559 | content: "\e178"; 560 | } 561 | .glyphicon-cutlery:before { 562 | content: "\e179"; 563 | } 564 | .glyphicon-header:before { 565 | content: "\e180"; 566 | } 567 | .glyphicon-compressed:before { 568 | content: "\e181"; 569 | } 570 | .glyphicon-earphone:before { 571 | content: "\e182"; 572 | } 573 | .glyphicon-phone-alt:before { 574 | content: "\e183"; 575 | } 576 | .glyphicon-tower:before { 577 | content: "\e184"; 578 | } 579 | .glyphicon-stats:before { 580 | content: "\e185"; 581 | } 582 | .glyphicon-sd-video:before { 583 | content: "\e186"; 584 | } 585 | .glyphicon-hd-video:before { 586 | content: "\e187"; 587 | } 588 | .glyphicon-subtitles:before { 589 | content: "\e188"; 590 | } 591 | .glyphicon-sound-stereo:before { 592 | content: "\e189"; 593 | } 594 | .glyphicon-sound-dolby:before { 595 | content: "\e190"; 596 | } 597 | .glyphicon-sound-5-1:before { 598 | content: "\e191"; 599 | } 600 | .glyphicon-sound-6-1:before { 601 | content: "\e192"; 602 | } 603 | .glyphicon-sound-7-1:before { 604 | content: "\e193"; 605 | } 606 | .glyphicon-copyright-mark:before { 607 | content: "\e194"; 608 | } 609 | .glyphicon-registration-mark:before { 610 | content: "\e195"; 611 | } 612 | .glyphicon-cloud-download:before { 613 | content: "\e197"; 614 | } 615 | .glyphicon-cloud-upload:before { 616 | content: "\e198"; 617 | } 618 | .glyphicon-tree-conifer:before { 619 | content: "\e199"; 620 | } 621 | .glyphicon-tree-deciduous:before { 622 | content: "\e200"; 623 | } 624 | .glyphicon-cd:before { 625 | content: "\e201"; 626 | } 627 | .glyphicon-save-file:before { 628 | content: "\e202"; 629 | } 630 | .glyphicon-open-file:before { 631 | content: "\e203"; 632 | } 633 | .glyphicon-level-up:before { 634 | content: "\e204"; 635 | } 636 | .glyphicon-copy:before { 637 | content: "\e205"; 638 | } 639 | .glyphicon-paste:before { 640 | content: "\e206"; 641 | } 642 | .glyphicon-alert:before { 643 | content: "\e209"; 644 | } 645 | .glyphicon-equalizer:before { 646 | content: "\e210"; 647 | } 648 | .glyphicon-king:before { 649 | content: "\e211"; 650 | } 651 | .glyphicon-queen:before { 652 | content: "\e212"; 653 | } 654 | .glyphicon-pawn:before { 655 | content: "\e213"; 656 | } 657 | .glyphicon-bishop:before { 658 | content: "\e214"; 659 | } 660 | .glyphicon-knight:before { 661 | content: "\e215"; 662 | } 663 | .glyphicon-baby-formula:before { 664 | content: "\e216"; 665 | } 666 | .glyphicon-tent:before { 667 | content: "\26fa"; 668 | } 669 | .glyphicon-blackboard:before { 670 | content: "\e218"; 671 | } 672 | .glyphicon-bed:before { 673 | content: "\e219"; 674 | } 675 | .glyphicon-apple:before { 676 | content: "\f8ff"; 677 | } 678 | .glyphicon-erase:before { 679 | content: "\e221"; 680 | } 681 | .glyphicon-hourglass:before { 682 | content: "\231b"; 683 | } 684 | .glyphicon-lamp:before { 685 | content: "\e223"; 686 | } 687 | .glyphicon-duplicate:before { 688 | content: "\e224"; 689 | } 690 | .glyphicon-piggy-bank:before { 691 | content: "\e225"; 692 | } 693 | .glyphicon-scissors:before { 694 | content: "\e226"; 695 | } 696 | .glyphicon-bitcoin:before { 697 | content: "\e227"; 698 | } 699 | .glyphicon-btc:before { 700 | content: "\e227"; 701 | } 702 | .glyphicon-xbt:before { 703 | content: "\e227"; 704 | } 705 | .glyphicon-yen:before { 706 | content: "\00a5"; 707 | } 708 | .glyphicon-jpy:before { 709 | content: "\00a5"; 710 | } 711 | .glyphicon-ruble:before { 712 | content: "\20bd"; 713 | } 714 | .glyphicon-rub:before { 715 | content: "\20bd"; 716 | } 717 | .glyphicon-scale:before { 718 | content: "\e230"; 719 | } 720 | .glyphicon-ice-lolly:before { 721 | content: "\e231"; 722 | } 723 | .glyphicon-ice-lolly-tasted:before { 724 | content: "\e232"; 725 | } 726 | .glyphicon-education:before { 727 | content: "\e233"; 728 | } 729 | .glyphicon-option-horizontal:before { 730 | content: "\e234"; 731 | } 732 | .glyphicon-option-vertical:before { 733 | content: "\e235"; 734 | } 735 | .glyphicon-menu-hamburger:before { 736 | content: "\e236"; 737 | } 738 | .glyphicon-modal-window:before { 739 | content: "\e237"; 740 | } 741 | .glyphicon-oil:before { 742 | content: "\e238"; 743 | } 744 | .glyphicon-grain:before { 745 | content: "\e239"; 746 | } 747 | .glyphicon-sunglasses:before { 748 | content: "\e240"; 749 | } 750 | .glyphicon-text-size:before { 751 | content: "\e241"; 752 | } 753 | .glyphicon-text-color:before { 754 | content: "\e242"; 755 | } 756 | .glyphicon-text-background:before { 757 | content: "\e243"; 758 | } 759 | .glyphicon-object-align-top:before { 760 | content: "\e244"; 761 | } 762 | .glyphicon-object-align-bottom:before { 763 | content: "\e245"; 764 | } 765 | .glyphicon-object-align-horizontal:before { 766 | content: "\e246"; 767 | } 768 | .glyphicon-object-align-left:before { 769 | content: "\e247"; 770 | } 771 | .glyphicon-object-align-vertical:before { 772 | content: "\e248"; 773 | } 774 | .glyphicon-object-align-right:before { 775 | content: "\e249"; 776 | } 777 | .glyphicon-triangle-right:before { 778 | content: "\e250"; 779 | } 780 | .glyphicon-triangle-left:before { 781 | content: "\e251"; 782 | } 783 | .glyphicon-triangle-bottom:before { 784 | content: "\e252"; 785 | } 786 | .glyphicon-triangle-top:before { 787 | content: "\e253"; 788 | } 789 | .glyphicon-console:before { 790 | content: "\e254"; 791 | } 792 | .glyphicon-superscript:before { 793 | content: "\e255"; 794 | } 795 | .glyphicon-subscript:before { 796 | content: "\e256"; 797 | } 798 | .glyphicon-menu-left:before { 799 | content: "\e257"; 800 | } 801 | .glyphicon-menu-right:before { 802 | content: "\e258"; 803 | } 804 | .glyphicon-menu-down:before { 805 | content: "\e259"; 806 | } 807 | .glyphicon-menu-up:before { 808 | content: "\e260"; 809 | } 810 | -------------------------------------------------------------------------------- /storage/diskmetricstore.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package storage 15 | 16 | import ( 17 | "encoding/gob" 18 | "errors" 19 | "fmt" 20 | "log/slog" 21 | "maps" 22 | "os" 23 | "path" 24 | "sort" 25 | "strings" 26 | "sync" 27 | "time" 28 | 29 | //nolint:staticcheck // Ignore SA1019. Dependencies use the deprecated package, so we have to, too. 30 | "github.com/golang/protobuf/proto" 31 | "github.com/prometheus/client_golang/prometheus" 32 | "github.com/prometheus/common/model" 33 | "github.com/prometheus/common/promslog" 34 | 35 | dto "github.com/prometheus/client_model/go" 36 | ) 37 | 38 | const ( 39 | pushMetricName = "push_time_seconds" 40 | pushMetricHelp = "Last Unix time when changing this group in the Pushgateway succeeded." 41 | pushFailedMetricName = "push_failure_time_seconds" 42 | pushFailedMetricHelp = "Last Unix time when changing this group in the Pushgateway failed." 43 | writeQueueCapacity = 1000 44 | ) 45 | 46 | var errTimestamp = errors.New("pushed metrics must not have timestamps") 47 | 48 | // DiskMetricStore is an implementation of MetricStore that persists metrics to 49 | // disk. 50 | type DiskMetricStore struct { 51 | lock sync.RWMutex // Protects metricFamilies. 52 | writeQueue chan WriteRequest 53 | drain chan struct{} 54 | done chan error 55 | metricGroups GroupingKeyToMetricGroup 56 | persistenceFile string 57 | predefinedHelp map[string]string 58 | logger *slog.Logger 59 | } 60 | 61 | type mfStat struct { 62 | pos int // Where in the result slice is the MetricFamily? 63 | copied bool // Has the MetricFamily already been copied? 64 | } 65 | 66 | // NewDiskMetricStore returns a DiskMetricStore ready to use. To cleanly shut it 67 | // down and free resources, the Shutdown() method has to be called. 68 | // 69 | // If persistenceFile is the empty string, no persisting to disk will 70 | // happen. Otherwise, a file of that name is used for persisting metrics to 71 | // disk. If the file already exists, metrics are read from it as part of the 72 | // start-up. Persisting is happening upon shutdown and after every write action, 73 | // but the latter will only happen persistenceDuration after the previous 74 | // persisting. 75 | // 76 | // If a non-nil Gatherer is provided, the help strings of metrics gathered by it 77 | // will be used as standard. Pushed metrics with deviating help strings will be 78 | // adjusted to avoid inconsistent expositions. 79 | func NewDiskMetricStore( 80 | persistenceFile string, 81 | persistenceInterval time.Duration, 82 | gatherPredefinedHelpFrom prometheus.Gatherer, 83 | logger *slog.Logger, 84 | ) *DiskMetricStore { 85 | // TODO: Do that outside of the constructor to allow the HTTP server to 86 | // serve /-/healthy and /-/ready earlier. 87 | dms := &DiskMetricStore{ 88 | writeQueue: make(chan WriteRequest, writeQueueCapacity), 89 | drain: make(chan struct{}), 90 | done: make(chan error), 91 | metricGroups: GroupingKeyToMetricGroup{}, 92 | persistenceFile: persistenceFile, 93 | logger: logger, 94 | } 95 | if err := dms.restore(); err != nil { 96 | logger.Error("could not load persisted metrics", "err", err) 97 | } 98 | if helpStrings, err := extractPredefinedHelpStrings(gatherPredefinedHelpFrom); err == nil { 99 | dms.predefinedHelp = helpStrings 100 | } else { 101 | logger.Error("could not gather metrics for predefined help strings", "err", err) 102 | } 103 | 104 | go dms.loop(persistenceInterval) 105 | return dms 106 | } 107 | 108 | // SubmitWriteRequest implements the MetricStore interface. 109 | func (dms *DiskMetricStore) SubmitWriteRequest(req WriteRequest) { 110 | dms.writeQueue <- req 111 | } 112 | 113 | // Shutdown implements the MetricStore interface. 114 | func (dms *DiskMetricStore) Shutdown() error { 115 | close(dms.drain) 116 | return <-dms.done 117 | } 118 | 119 | // Healthy implements the MetricStore interface. 120 | func (dms *DiskMetricStore) Healthy() error { 121 | // By taking the lock we check that there is no deadlock. 122 | dms.lock.Lock() 123 | defer dms.lock.Unlock() 124 | 125 | // A pushgateway that cannot be written to should not be 126 | // considered as healthy. 127 | if len(dms.writeQueue) == cap(dms.writeQueue) { 128 | err := fmt.Errorf("write queue is full") 129 | dms.logger.Warn(err.Error()) 130 | return err 131 | } 132 | 133 | return nil 134 | } 135 | 136 | // Ready implements the MetricStore interface. 137 | func (dms *DiskMetricStore) Ready() error { 138 | return dms.Healthy() 139 | } 140 | 141 | // GetMetricFamilies implements the MetricStore interface. 142 | func (dms *DiskMetricStore) GetMetricFamilies() []*dto.MetricFamily { 143 | dms.lock.RLock() 144 | defer dms.lock.RUnlock() 145 | 146 | result := []*dto.MetricFamily{} 147 | mfStatByName := map[string]mfStat{} 148 | 149 | for _, group := range dms.metricGroups { 150 | for name, tmf := range group.Metrics { 151 | mf := tmf.GetMetricFamily() 152 | if mf == nil { 153 | dms.logger.Warn("storage corruption detected, consider wiping the persistence file") 154 | continue 155 | } 156 | stat, exists := mfStatByName[name] 157 | if exists { 158 | existingMF := result[stat.pos] 159 | if !stat.copied { 160 | mfStatByName[name] = mfStat{ 161 | pos: stat.pos, 162 | copied: true, 163 | } 164 | existingMF = copyMetricFamily(existingMF) 165 | result[stat.pos] = existingMF 166 | } 167 | if mf.GetHelp() != existingMF.GetHelp() { 168 | dms.logger.Info("metric families inconsistent help strings", "err", "Metric families have inconsistent help strings. The latter will have priority. This is bad. Fix your pushed metrics!", "new", mf, "old", existingMF) 169 | } 170 | // Type inconsistency cannot be fixed here. We will detect it during 171 | // gathering anyway, so no reason to log anything here. 172 | existingMF.Metric = append(existingMF.Metric, mf.Metric...) 173 | } else { 174 | copied := false 175 | if help, ok := dms.predefinedHelp[name]; ok && mf.GetHelp() != help { 176 | dms.logger.Info("metric families overlap", "err", "Metric family has the same name as a metric family used by the Pushgateway itself but it has a different help string. Changing it to the standard help string. This is bad. Fix your pushed metrics!", "metric_family", mf, "standard_help", help) 177 | mf = copyMetricFamily(mf) 178 | copied = true 179 | mf.Help = proto.String(help) 180 | } 181 | mfStatByName[name] = mfStat{ 182 | pos: len(result), 183 | copied: copied, 184 | } 185 | result = append(result, mf) 186 | } 187 | } 188 | } 189 | return result 190 | } 191 | 192 | // GetMetricFamiliesMap implements the MetricStore interface. 193 | func (dms *DiskMetricStore) GetMetricFamiliesMap() GroupingKeyToMetricGroup { 194 | dms.lock.RLock() 195 | defer dms.lock.RUnlock() 196 | groupsCopy := make(GroupingKeyToMetricGroup, len(dms.metricGroups)) 197 | for k, g := range dms.metricGroups { 198 | metricsCopy := make(NameToTimestampedMetricFamilyMap, len(g.Metrics)) 199 | groupsCopy[k] = MetricGroup{Labels: g.Labels, Metrics: metricsCopy} 200 | maps.Copy(metricsCopy, g.Metrics) 201 | } 202 | return groupsCopy 203 | } 204 | 205 | func (dms *DiskMetricStore) loop(persistenceInterval time.Duration) { 206 | lastPersist := time.Now() 207 | persistScheduled := false 208 | lastWrite := time.Time{} 209 | persistDone := make(chan time.Time) 210 | var persistTimer *time.Timer 211 | 212 | checkPersist := func() { 213 | if dms.persistenceFile != "" && !persistScheduled && lastWrite.After(lastPersist) { 214 | persistTimer = time.AfterFunc( 215 | persistenceInterval-lastWrite.Sub(lastPersist), 216 | func() { 217 | persistStarted := time.Now() 218 | if err := dms.persist(); err != nil { 219 | dms.logger.Error("error persisting metrics", "err", err) 220 | } else { 221 | dms.logger.Info("metrics persisted", "file", dms.persistenceFile) 222 | } 223 | persistDone <- persistStarted 224 | }, 225 | ) 226 | persistScheduled = true 227 | } 228 | } 229 | 230 | for { 231 | select { 232 | case wr := <-dms.writeQueue: 233 | lastWrite = time.Now() 234 | if dms.checkWriteRequest(wr) { 235 | dms.processWriteRequest(wr) 236 | } else { 237 | dms.setPushFailedTimestamp(wr) 238 | } 239 | if wr.Done != nil { 240 | close(wr.Done) 241 | } 242 | checkPersist() 243 | case lastPersist = <-persistDone: 244 | persistScheduled = false 245 | checkPersist() // In case something has been written in the meantime. 246 | case <-dms.drain: 247 | // Prevent a scheduled persist from firing later. 248 | if persistTimer != nil { 249 | persistTimer.Stop() 250 | } 251 | // Now draining... 252 | for { 253 | select { 254 | case wr := <-dms.writeQueue: 255 | if dms.checkWriteRequest(wr) { 256 | dms.processWriteRequest(wr) 257 | } else { 258 | dms.setPushFailedTimestamp(wr) 259 | } 260 | default: 261 | dms.done <- dms.persist() 262 | return 263 | } 264 | } 265 | } 266 | } 267 | } 268 | 269 | func (dms *DiskMetricStore) processWriteRequest(wr WriteRequest) { 270 | dms.lock.Lock() 271 | defer dms.lock.Unlock() 272 | 273 | key := groupingKeyFor(wr.Labels) 274 | 275 | if wr.MetricFamilies == nil { 276 | // No MetricFamilies means delete request. Delete the whole 277 | // metric group, and we are done here. 278 | delete(dms.metricGroups, key) 279 | return 280 | } 281 | // Otherwise, it's an update. 282 | group, ok := dms.metricGroups[key] 283 | if !ok { 284 | group = MetricGroup{ 285 | Labels: wr.Labels, 286 | Metrics: NameToTimestampedMetricFamilyMap{}, 287 | } 288 | dms.metricGroups[key] = group 289 | } else if wr.Replace { 290 | // For replace, we have to delete all metric families in the 291 | // group except pre-existing push timestamps. 292 | for name := range group.Metrics { 293 | if name != pushMetricName && name != pushFailedMetricName { 294 | delete(group.Metrics, name) 295 | } 296 | } 297 | } 298 | wr.MetricFamilies[pushMetricName] = newPushTimestampGauge(wr.Labels, wr.Timestamp) 299 | // Only add a zero push-failed metric if none is there yet, so that a 300 | // previously added fail timestamp is retained. 301 | if _, ok := group.Metrics[pushFailedMetricName]; !ok { 302 | wr.MetricFamilies[pushFailedMetricName] = newPushFailedTimestampGauge(wr.Labels, time.Time{}) 303 | } 304 | for name, mf := range wr.MetricFamilies { 305 | group.Metrics[name] = TimestampedMetricFamily{ 306 | Timestamp: wr.Timestamp, 307 | GobbableMetricFamily: (*GobbableMetricFamily)(mf), 308 | } 309 | } 310 | } 311 | 312 | func (dms *DiskMetricStore) setPushFailedTimestamp(wr WriteRequest) { 313 | dms.lock.Lock() 314 | defer dms.lock.Unlock() 315 | 316 | key := groupingKeyFor(wr.Labels) 317 | 318 | group, ok := dms.metricGroups[key] 319 | if !ok { 320 | group = MetricGroup{ 321 | Labels: wr.Labels, 322 | Metrics: NameToTimestampedMetricFamilyMap{}, 323 | } 324 | dms.metricGroups[key] = group 325 | } 326 | 327 | group.Metrics[pushFailedMetricName] = TimestampedMetricFamily{ 328 | Timestamp: wr.Timestamp, 329 | GobbableMetricFamily: (*GobbableMetricFamily)(newPushFailedTimestampGauge(wr.Labels, wr.Timestamp)), 330 | } 331 | // Only add a zero push metric if none is there yet, so that a 332 | // previously added push timestamp is retained. 333 | if _, ok := group.Metrics[pushMetricName]; !ok { 334 | group.Metrics[pushMetricName] = TimestampedMetricFamily{ 335 | Timestamp: wr.Timestamp, 336 | GobbableMetricFamily: (*GobbableMetricFamily)(newPushTimestampGauge(wr.Labels, time.Time{})), 337 | } 338 | } 339 | } 340 | 341 | // checkWriteRequest return if applying the provided WriteRequest will result in 342 | // a consistent state of metrics. The dms is not modified by the check. However, 343 | // the WriteRequest _will_ be sanitized: the MetricFamilies are ensured to 344 | // contain the grouping Labels after the check. If false is returned, the 345 | // causing error is written to the Done channel of the WriteRequest. 346 | // 347 | // Special case: If the WriteRequest has no Done channel set, the (expensive) 348 | // consistency check is skipped. The WriteRequest is still sanitized, and the 349 | // presence of timestamps still results in returning false. 350 | func (dms *DiskMetricStore) checkWriteRequest(wr WriteRequest) bool { 351 | if wr.MetricFamilies == nil { 352 | // Delete request cannot create inconsistencies, and nothing has 353 | // to be sanitized. 354 | return true 355 | } 356 | 357 | var err error 358 | defer func() { 359 | if err != nil && wr.Done != nil { 360 | wr.Done <- err 361 | } 362 | }() 363 | 364 | if timestampsPresent(wr.MetricFamilies) { 365 | err = errTimestamp 366 | return false 367 | } 368 | for _, mf := range wr.MetricFamilies { 369 | sanitizeLabels(mf, wr.Labels) 370 | } 371 | 372 | // Without Done channel, don't do the expensive consistency check. 373 | if wr.Done == nil { 374 | return true 375 | } 376 | 377 | // Construct a test dms, acting on a copy of the metrics, to test the 378 | // WriteRequest with. 379 | tdms := &DiskMetricStore{ 380 | metricGroups: dms.GetMetricFamiliesMap(), 381 | predefinedHelp: dms.predefinedHelp, 382 | logger: promslog.NewNopLogger(), 383 | } 384 | tdms.processWriteRequest(wr) 385 | 386 | // Construct a test Gatherer to check if consistent gathering is possible. 387 | tg := prometheus.Gatherers{ 388 | prometheus.DefaultGatherer, 389 | prometheus.GathererFunc(func() ([]*dto.MetricFamily, error) { 390 | return tdms.GetMetricFamilies(), nil 391 | }), 392 | } 393 | if _, err = tg.Gather(); err != nil { 394 | return false 395 | } 396 | return true 397 | } 398 | 399 | func (dms *DiskMetricStore) persist() error { 400 | // Check (again) if persistence is configured because some code paths 401 | // will call this method even if it is not. 402 | if dms.persistenceFile == "" { 403 | return nil 404 | } 405 | f, err := os.CreateTemp( 406 | path.Dir(dms.persistenceFile), 407 | path.Base(dms.persistenceFile)+".in_progress.", 408 | ) 409 | if err != nil { 410 | return err 411 | } 412 | inProgressFileName := f.Name() 413 | e := gob.NewEncoder(f) 414 | 415 | dms.lock.RLock() 416 | err = e.Encode(dms.metricGroups) 417 | dms.lock.RUnlock() 418 | if err != nil { 419 | f.Close() 420 | os.Remove(inProgressFileName) 421 | return err 422 | } 423 | if err := f.Close(); err != nil { 424 | os.Remove(inProgressFileName) 425 | return err 426 | } 427 | return os.Rename(inProgressFileName, dms.persistenceFile) 428 | } 429 | 430 | func (dms *DiskMetricStore) restore() error { 431 | if dms.persistenceFile == "" { 432 | return nil 433 | } 434 | f, err := os.Open(dms.persistenceFile) 435 | if os.IsNotExist(err) { 436 | return nil 437 | } 438 | if err != nil { 439 | return err 440 | } 441 | defer f.Close() 442 | d := gob.NewDecoder(f) 443 | if err := d.Decode(&dms.metricGroups); err != nil { 444 | return err 445 | } 446 | return nil 447 | } 448 | 449 | func copyMetricFamily(mf *dto.MetricFamily) *dto.MetricFamily { 450 | return &dto.MetricFamily{ 451 | Name: mf.Name, 452 | Help: mf.Help, 453 | Type: mf.Type, 454 | Metric: append([]*dto.Metric{}, mf.Metric...), 455 | } 456 | } 457 | 458 | // groupingKeyFor creates a grouping key from the provided map of grouping 459 | // labels. The grouping key is created by joining all label names and values 460 | // together with model.SeparatorByte as a separator. The label names are sorted 461 | // lexicographically before joining. In that way, the grouping key is both 462 | // reproducible and unique. 463 | func groupingKeyFor(labels map[string]string) string { 464 | if len(labels) == 0 { // Super fast path. 465 | return "" 466 | } 467 | 468 | labelNames := make([]string, 0, len(labels)) 469 | for labelName := range labels { 470 | labelNames = append(labelNames, labelName) 471 | } 472 | sort.Strings(labelNames) 473 | 474 | sb := strings.Builder{} 475 | for i, labelName := range labelNames { 476 | sb.WriteString(labelName) 477 | sb.WriteByte(model.SeparatorByte) 478 | sb.WriteString(labels[labelName]) 479 | if i+1 < len(labels) { // No separator at the end. 480 | sb.WriteByte(model.SeparatorByte) 481 | } 482 | } 483 | return sb.String() 484 | } 485 | 486 | // extractPredefinedHelpStrings extracts all the HELP strings from the provided 487 | // gatherer so that the DiskMetricStore can fix deviations in pushed metrics. 488 | func extractPredefinedHelpStrings(g prometheus.Gatherer) (map[string]string, error) { 489 | if g == nil { 490 | return nil, nil 491 | } 492 | mfs, err := g.Gather() 493 | if err != nil { 494 | return nil, err 495 | } 496 | result := map[string]string{} 497 | for _, mf := range mfs { 498 | result[mf.GetName()] = mf.GetHelp() 499 | } 500 | return result, nil 501 | } 502 | 503 | func newPushTimestampGauge(groupingLabels map[string]string, t time.Time) *dto.MetricFamily { 504 | return newTimestampGauge(pushMetricName, pushMetricHelp, groupingLabels, t) 505 | } 506 | 507 | func newPushFailedTimestampGauge(groupingLabels map[string]string, t time.Time) *dto.MetricFamily { 508 | return newTimestampGauge(pushFailedMetricName, pushFailedMetricHelp, groupingLabels, t) 509 | } 510 | 511 | func newTimestampGauge(name, help string, groupingLabels map[string]string, t time.Time) *dto.MetricFamily { 512 | var ts float64 513 | if !t.IsZero() { 514 | ts = float64(t.UnixNano()) / 1e9 515 | } 516 | mf := &dto.MetricFamily{ 517 | Name: proto.String(name), 518 | Help: proto.String(help), 519 | Type: dto.MetricType_GAUGE.Enum(), 520 | Metric: []*dto.Metric{ 521 | { 522 | Gauge: &dto.Gauge{ 523 | Value: proto.Float64(ts), 524 | }, 525 | }, 526 | }, 527 | } 528 | sanitizeLabels(mf, groupingLabels) 529 | return mf 530 | } 531 | 532 | // sanitizeLabels ensures that all the labels in groupingLabels and the 533 | // `instance` label are present in the MetricFamily. The label values from 534 | // groupingLabels are set in each Metric, no matter what. After that, if the 535 | // 'instance' label is not present at all in a Metric, it will be created (with 536 | // an empty string as value). 537 | // 538 | // Finally, sanitizeLabels sorts the label pairs of all metrics. 539 | func sanitizeLabels(mf *dto.MetricFamily, groupingLabels map[string]string) { 540 | gLabelsNotYetDone := make(map[string]string, len(groupingLabels)) 541 | 542 | metric: 543 | for _, m := range mf.GetMetric() { 544 | maps.Copy(gLabelsNotYetDone, groupingLabels) 545 | hasInstanceLabel := false 546 | for _, lp := range m.GetLabel() { 547 | ln := lp.GetName() 548 | if lv, ok := gLabelsNotYetDone[ln]; ok { 549 | lp.Value = proto.String(lv) 550 | delete(gLabelsNotYetDone, ln) 551 | } 552 | if ln == string(model.InstanceLabel) { 553 | hasInstanceLabel = true 554 | } 555 | if len(gLabelsNotYetDone) == 0 && hasInstanceLabel { 556 | sort.Sort(labelPairs(m.Label)) 557 | continue metric 558 | } 559 | } 560 | for ln, lv := range gLabelsNotYetDone { 561 | m.Label = append(m.Label, &dto.LabelPair{ 562 | Name: proto.String(ln), 563 | Value: proto.String(lv), 564 | }) 565 | if ln == string(model.InstanceLabel) { 566 | hasInstanceLabel = true 567 | } 568 | delete(gLabelsNotYetDone, ln) // To prepare map for next metric. 569 | } 570 | if !hasInstanceLabel { 571 | m.Label = append(m.Label, &dto.LabelPair{ 572 | Name: proto.String(string(model.InstanceLabel)), 573 | Value: proto.String(""), 574 | }) 575 | } 576 | sort.Sort(labelPairs(m.Label)) 577 | } 578 | } 579 | 580 | // Checks if any timestamps have been specified. 581 | func timestampsPresent(metricFamilies map[string]*dto.MetricFamily) bool { 582 | for _, mf := range metricFamilies { 583 | for _, m := range mf.GetMetric() { 584 | if m.TimestampMs != nil { 585 | return true 586 | } 587 | } 588 | } 589 | return false 590 | } 591 | 592 | // labelPairs implements sort.Interface. It provides a sortable version of a 593 | // slice of dto.LabelPair pointers. 594 | type labelPairs []*dto.LabelPair 595 | 596 | func (s labelPairs) Len() int { 597 | return len(s) 598 | } 599 | 600 | func (s labelPairs) Swap(i, j int) { 601 | s[i], s[j] = s[j], s[i] 602 | } 603 | 604 | func (s labelPairs) Less(i, j int) bool { 605 | return s[i].GetName() < s[j].GetName() 606 | } 607 | --------------------------------------------------------------------------------