pax_global_header00006660000000000000000000000064146370143700014517gustar00rootroot0000000000000052 comment=0c7b585c7da330aae136aaa874cb4f89f5b3e5d9 golang-github-prometheus-common-0.55.0/000077500000000000000000000000001463701437000200145ustar00rootroot00000000000000golang-github-prometheus-common-0.55.0/.circleci/000077500000000000000000000000001463701437000216475ustar00rootroot00000000000000golang-github-prometheus-common-0.55.0/.circleci/config.yml000066400000000000000000000053441463701437000236450ustar00rootroot00000000000000--- version: 2.1 orbs: go: circleci/go@1.7.1 jobs: test: parameters: go_version: type: string use_gomod_cache: type: boolean default: true docker: - image: cimg/go:<< parameters.go_version >> environment: # Override Go 1.18 security deprecations. GODEBUG: "x509sha1=1,tls10default=1" steps: - checkout - when: condition: << parameters.use_gomod_cache >> steps: - go/load-cache: key: v1-go<< parameters.go_version >> - run: make test - run: make -C sigv4 test - when: condition: << parameters.use_gomod_cache >> steps: - go/save-cache: key: v1-go<< parameters.go_version >> - store_test_results: path: test-results test-assets: parameters: go_version: type: string use_gomod_cache: type: boolean default: true docker: - image: cimg/go:<< parameters.go_version >> steps: - checkout - when: condition: << parameters.use_gomod_cache >> steps: - go/load-cache: key: v1-go<< parameters.go_version >> - run: make -C assets test - when: condition: << parameters.use_gomod_cache >> steps: - go/save-cache: key: v1-go<< parameters.go_version >> - store_test_results: path: test-results style: parameters: go_version: type: string use_gomod_cache: type: boolean default: true docker: - image: cimg/go:<< parameters.go_version >> steps: - checkout - when: condition: << parameters.use_gomod_cache >> steps: - go/load-cache: key: v1-go<< parameters.go_version >> - run: make style - run: make -C sigv4 style - run: make -C assets style - run: make check-go-mod-version - when: condition: << parameters.use_gomod_cache >> steps: - go/save-cache: key: v1-go<< parameters.go_version >> - store_test_results: path: test-results workflows: version: 2 tests: jobs: # Supported Go versions are synced with github.com/prometheus/client_golang. - test: name: go-<< matrix.go_version >> matrix: parameters: go_version: - "1.20" - "1.21" - "1.22" - test-assets: name: assets-go-<< matrix.go_version >> matrix: parameters: go_version: - "1.22" - style: name: style go_version: "1.22" golang-github-prometheus-common-0.55.0/.github/000077500000000000000000000000001463701437000213545ustar00rootroot00000000000000golang-github-prometheus-common-0.55.0/.github/dependabot.yml000066400000000000000000000003111463701437000241770ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "gomod" directory: "/" schedule: interval: monthly - package-ecosystem: "gomod" directory: "/sigv4" schedule: interval: monthly golang-github-prometheus-common-0.55.0/.github/workflows/000077500000000000000000000000001463701437000234115ustar00rootroot00000000000000golang-github-prometheus-common-0.55.0/.github/workflows/golangci-lint.yml000066400000000000000000000023451463701437000266670ustar00rootroot00000000000000--- # This action is synced from https://github.com/prometheus/prometheus name: golangci-lint on: push: paths: - "go.sum" - "go.mod" - "**.go" - "scripts/errcheck_excludes.txt" - ".github/workflows/golangci-lint.yml" - ".golangci.yml" pull_request: permissions: # added using https://github.com/step-security/secure-repo contents: read jobs: golangci: permissions: contents: read # for actions/checkout to fetch code pull-requests: read # for golangci/golangci-lint-action to fetch pull requests name: lint runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - name: Install Go uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 with: go-version: 1.22.x - name: Install snmp_exporter/generator dependencies run: sudo apt-get update && sudo apt-get -y install libsnmp-dev if: github.repository == 'prometheus/snmp_exporter' - name: Lint uses: golangci/golangci-lint-action@a4f60bb28d35aeee14e6880718e0c85ff1882e64 # v6.0.1 with: args: --verbose version: v1.59.1 golang-github-prometheus-common-0.55.0/.gitignore000066400000000000000000000000071463701437000220010ustar00rootroot00000000000000vendor/golang-github-prometheus-common-0.55.0/.golangci.yml000066400000000000000000000014641463701437000224050ustar00rootroot00000000000000issues: max-issues-per-linter: 0 max-same-issues: 0 linters: enable: - errcheck - errorlint - gofumpt - goimports - gosimple - govet - ineffassign - misspell - revive - staticcheck - testifylint - unused linters-settings: goimports: local-prefixes: github.com/prometheus/common revive: rules: # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unused-parameter - name: unused-parameter severity: warning disabled: true testifylint: disable: - float-compare - go-require enable: - bool-compare - compares - empty - error-is-as - error-nil - expected-actual - len - require-error - suite-dont-use-pkg - suite-extra-assert-call golang-github-prometheus-common-0.55.0/.yamllint000066400000000000000000000007311463701437000216470ustar00rootroot00000000000000--- extends: default ignore: | ui/react-app/node_modules rules: braces: max-spaces-inside: 1 level: error brackets: max-spaces-inside: 1 level: error commas: disable comments: disable comments-indentation: disable document-start: disable indentation: spaces: consistent indent-sequences: consistent key-duplicates: ignore: | config/testdata/section_key_dup.bad.yml line-length: disable truthy: check-keys: false golang-github-prometheus-common-0.55.0/CODE_OF_CONDUCT.md000066400000000000000000000002301463701437000226060ustar00rootroot00000000000000# Prometheus Community Code of Conduct Prometheus follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/main/code-of-conduct.md). golang-github-prometheus-common-0.55.0/CONTRIBUTING.md000066400000000000000000000015461463701437000222530ustar00rootroot00000000000000# Contributing Prometheus uses GitHub to manage reviews of pull requests. * If you have a trivial fix or improvement, go ahead and create a pull request, addressing (with `@...`) the maintainer of this repository (see [MAINTAINERS.md](MAINTAINERS.md)) in the description of the pull request. * If you plan to do something more involved, first discuss your ideas on our [mailing list](https://groups.google.com/forum/?fromgroups#!forum/prometheus-developers). This will avoid unnecessary work and surely give you and us a good deal of inspiration. * Relevant coding style guidelines are the [Go Code Review Comments](https://code.google.com/p/go-wiki/wiki/CodeReviewComments) and the _Formatting and style_ section of Peter Bourgon's [Go: Best Practices for Production Environments](http://peter.bourgon.org/go-in-production/#formatting-and-style). golang-github-prometheus-common-0.55.0/LICENSE000066400000000000000000000261351463701437000210300ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. golang-github-prometheus-common-0.55.0/MAINTAINERS.md000066400000000000000000000002441463701437000221100ustar00rootroot00000000000000* Julien Pivotto @roidelapluie * Josue (Josh) Abreu @gotjosh * Arthur Sens @ArthurSens golang-github-prometheus-common-0.55.0/Makefile000066400000000000000000000015621463701437000214600ustar00rootroot00000000000000# Copyright 2018 The Prometheus Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. include Makefile.common .PHONY: test test:: deps check_license unused common-test lint .PHONY: generate-testdata generate-testdata: @cd config && go run generate.go .PHONY: check-go-mod-version check-go-mod-version: @echo ">> checking go.mod version matching" @./scripts/check-go-mod-version.sh golang-github-prometheus-common-0.55.0/Makefile.common000066400000000000000000000217151463701437000227510ustar00rootroot00000000000000# Copyright 2018 The Prometheus Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # A common Makefile that includes rules to be reused in different prometheus projects. # !!! Open PRs only against the prometheus/prometheus/Makefile.common repository! # Example usage : # Create the main Makefile in the root project directory. # include Makefile.common # customTarget: # @echo ">> Running customTarget" # # Ensure GOBIN is not set during build so that promu is installed to the correct path unexport GOBIN GO ?= go GOFMT ?= $(GO)fmt FIRST_GOPATH := $(firstword $(subst :, ,$(shell $(GO) env GOPATH))) GOOPTS ?= GOHOSTOS ?= $(shell $(GO) env GOHOSTOS) GOHOSTARCH ?= $(shell $(GO) env GOHOSTARCH) GO_VERSION ?= $(shell $(GO) version) GO_VERSION_NUMBER ?= $(word 3, $(GO_VERSION)) PRE_GO_111 ?= $(shell echo $(GO_VERSION_NUMBER) | grep -E 'go1\.(10|[0-9])\.') PROMU := $(FIRST_GOPATH)/bin/promu pkgs = ./... ifeq (arm, $(GOHOSTARCH)) GOHOSTARM ?= $(shell GOARM= $(GO) env GOARM) GO_BUILD_PLATFORM ?= $(GOHOSTOS)-$(GOHOSTARCH)v$(GOHOSTARM) else GO_BUILD_PLATFORM ?= $(GOHOSTOS)-$(GOHOSTARCH) endif GOTEST := $(GO) test GOTEST_DIR := ifneq ($(CIRCLE_JOB),) ifneq ($(shell command -v gotestsum 2> /dev/null),) GOTEST_DIR := test-results GOTEST := gotestsum --junitfile $(GOTEST_DIR)/unit-tests.xml -- endif endif PROMU_VERSION ?= 0.17.0 PROMU_URL := https://github.com/prometheus/promu/releases/download/v$(PROMU_VERSION)/promu-$(PROMU_VERSION).$(GO_BUILD_PLATFORM).tar.gz SKIP_GOLANGCI_LINT := GOLANGCI_LINT := GOLANGCI_LINT_OPTS ?= GOLANGCI_LINT_VERSION ?= v1.59.1 # golangci-lint only supports linux, darwin and windows platforms on i386/amd64/arm64. # windows isn't included here because of the path separator being different. ifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux darwin)) ifeq ($(GOHOSTARCH),$(filter $(GOHOSTARCH),amd64 i386 arm64)) # If we're in CI and there is an Actions file, that means the linter # is being run in Actions, so we don't need to run it here. ifneq (,$(SKIP_GOLANGCI_LINT)) GOLANGCI_LINT := else ifeq (,$(CIRCLE_JOB)) GOLANGCI_LINT := $(FIRST_GOPATH)/bin/golangci-lint else ifeq (,$(wildcard .github/workflows/golangci-lint.yml)) GOLANGCI_LINT := $(FIRST_GOPATH)/bin/golangci-lint endif endif endif PREFIX ?= $(shell pwd) BIN_DIR ?= $(shell pwd) DOCKER_IMAGE_TAG ?= $(subst /,-,$(shell git rev-parse --abbrev-ref HEAD)) DOCKERFILE_PATH ?= ./Dockerfile DOCKERBUILD_CONTEXT ?= ./ DOCKER_REPO ?= prom DOCKER_ARCHS ?= amd64 BUILD_DOCKER_ARCHS = $(addprefix common-docker-,$(DOCKER_ARCHS)) PUBLISH_DOCKER_ARCHS = $(addprefix common-docker-publish-,$(DOCKER_ARCHS)) TAG_DOCKER_ARCHS = $(addprefix common-docker-tag-latest-,$(DOCKER_ARCHS)) SANITIZED_DOCKER_IMAGE_TAG := $(subst +,-,$(DOCKER_IMAGE_TAG)) ifeq ($(GOHOSTARCH),amd64) ifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux freebsd darwin windows)) # Only supported on amd64 test-flags := -race endif endif # This rule is used to forward a target like "build" to "common-build". This # allows a new "build" target to be defined in a Makefile which includes this # one and override "common-build" without override warnings. %: common-% ; .PHONY: common-all common-all: precheck style check_license lint yamllint unused build test .PHONY: common-style common-style: @echo ">> checking code style" @fmtRes=$$($(GOFMT) -d $$(find . -path ./vendor -prune -o -name '*.go' -print)); \ if [ -n "$${fmtRes}" ]; then \ echo "gofmt checking failed!"; echo "$${fmtRes}"; echo; \ echo "Please ensure you are using $$($(GO) version) for formatting code."; \ exit 1; \ fi .PHONY: common-check_license common-check_license: @echo ">> checking license header" @licRes=$$(for file in $$(find . -type f -iname '*.go' ! -path './vendor/*') ; do \ awk 'NR<=3' $$file | grep -Eq "(Copyright|generated|GENERATED)" || echo $$file; \ done); \ if [ -n "$${licRes}" ]; then \ echo "license header checking failed:"; echo "$${licRes}"; \ exit 1; \ fi .PHONY: common-deps common-deps: @echo ">> getting dependencies" $(GO) mod download .PHONY: update-go-deps update-go-deps: @echo ">> updating Go dependencies" @for m in $$($(GO) list -mod=readonly -m -f '{{ if and (not .Indirect) (not .Main)}}{{.Path}}{{end}}' all); do \ $(GO) get -d $$m; \ done $(GO) mod tidy .PHONY: common-test-short common-test-short: $(GOTEST_DIR) @echo ">> running short tests" $(GOTEST) -short $(GOOPTS) $(pkgs) .PHONY: common-test common-test: $(GOTEST_DIR) @echo ">> running all tests" $(GOTEST) $(test-flags) $(GOOPTS) $(pkgs) $(GOTEST_DIR): @mkdir -p $@ .PHONY: common-format common-format: @echo ">> formatting code" $(GO) fmt $(pkgs) .PHONY: common-vet common-vet: @echo ">> vetting code" $(GO) vet $(GOOPTS) $(pkgs) .PHONY: common-lint common-lint: $(GOLANGCI_LINT) ifdef GOLANGCI_LINT @echo ">> running golangci-lint" $(GOLANGCI_LINT) run $(GOLANGCI_LINT_OPTS) $(pkgs) endif .PHONY: common-lint-fix common-lint-fix: $(GOLANGCI_LINT) ifdef GOLANGCI_LINT @echo ">> running golangci-lint fix" $(GOLANGCI_LINT) run --fix $(GOLANGCI_LINT_OPTS) $(pkgs) endif .PHONY: common-yamllint common-yamllint: @echo ">> running yamllint on all YAML files in the repository" ifeq (, $(shell command -v yamllint 2> /dev/null)) @echo "yamllint not installed so skipping" else yamllint . endif # For backward-compatibility. .PHONY: common-staticcheck common-staticcheck: lint .PHONY: common-unused common-unused: @echo ">> running check for unused/missing packages in go.mod" $(GO) mod tidy @git diff --exit-code -- go.sum go.mod .PHONY: common-build common-build: promu @echo ">> building binaries" $(PROMU) build --prefix $(PREFIX) $(PROMU_BINARIES) .PHONY: common-tarball common-tarball: promu @echo ">> building release tarball" $(PROMU) tarball --prefix $(PREFIX) $(BIN_DIR) .PHONY: common-docker-repo-name common-docker-repo-name: @echo "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)" .PHONY: common-docker $(BUILD_DOCKER_ARCHS) common-docker: $(BUILD_DOCKER_ARCHS) $(BUILD_DOCKER_ARCHS): common-docker-%: docker build -t "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" \ -f $(DOCKERFILE_PATH) \ --build-arg ARCH="$*" \ --build-arg OS="linux" \ $(DOCKERBUILD_CONTEXT) .PHONY: common-docker-publish $(PUBLISH_DOCKER_ARCHS) common-docker-publish: $(PUBLISH_DOCKER_ARCHS) $(PUBLISH_DOCKER_ARCHS): common-docker-publish-%: docker push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" DOCKER_MAJOR_VERSION_TAG = $(firstword $(subst ., ,$(shell cat VERSION))) .PHONY: common-docker-tag-latest $(TAG_DOCKER_ARCHS) common-docker-tag-latest: $(TAG_DOCKER_ARCHS) $(TAG_DOCKER_ARCHS): common-docker-tag-latest-%: docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:latest" docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:v$(DOCKER_MAJOR_VERSION_TAG)" .PHONY: common-docker-manifest common-docker-manifest: DOCKER_CLI_EXPERIMENTAL=enabled docker manifest create -a "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)" $(foreach ARCH,$(DOCKER_ARCHS),$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$(ARCH):$(SANITIZED_DOCKER_IMAGE_TAG)) DOCKER_CLI_EXPERIMENTAL=enabled docker manifest push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)" .PHONY: promu promu: $(PROMU) $(PROMU): $(eval PROMU_TMP := $(shell mktemp -d)) curl -s -L $(PROMU_URL) | tar -xvzf - -C $(PROMU_TMP) mkdir -p $(FIRST_GOPATH)/bin cp $(PROMU_TMP)/promu-$(PROMU_VERSION).$(GO_BUILD_PLATFORM)/promu $(FIRST_GOPATH)/bin/promu rm -r $(PROMU_TMP) .PHONY: proto proto: @echo ">> generating code from proto files" @./scripts/genproto.sh ifdef GOLANGCI_LINT $(GOLANGCI_LINT): mkdir -p $(FIRST_GOPATH)/bin curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/$(GOLANGCI_LINT_VERSION)/install.sh \ | sed -e '/install -d/d' \ | sh -s -- -b $(FIRST_GOPATH)/bin $(GOLANGCI_LINT_VERSION) endif .PHONY: precheck precheck:: define PRECHECK_COMMAND_template = precheck:: $(1)_precheck PRECHECK_COMMAND_$(1) ?= $(1) $$(strip $$(PRECHECK_OPTIONS_$(1))) .PHONY: $(1)_precheck $(1)_precheck: @if ! $$(PRECHECK_COMMAND_$(1)) 1>/dev/null 2>&1; then \ echo "Execution of '$$(PRECHECK_COMMAND_$(1))' command failed. Is $(1) installed?"; \ exit 1; \ fi endef golang-github-prometheus-common-0.55.0/NOTICE000066400000000000000000000002621463701437000207200ustar00rootroot00000000000000Common libraries shared by Prometheus Go components. Copyright 2015 The Prometheus Authors This product includes software developed at SoundCloud Ltd. (http://soundcloud.com/). golang-github-prometheus-common-0.55.0/README.md000066400000000000000000000014231463701437000212730ustar00rootroot00000000000000# Common ![circleci](https://circleci.com/gh/prometheus/common/tree/main.svg?style=shield) This repository contains Go libraries that are shared across Prometheus components and libraries. They are considered internal to Prometheus, without any stability guarantees for external usage. * **assets**: Embedding of static assets with gzip support * **config**: Common configuration structures * **expfmt**: Decoding and encoding for the exposition format * **model**: Shared data structures * **promlog**: A logging wrapper around [go-kit/log](https://github.com/go-kit/kit/tree/master/log) * **route**: A routing wrapper around [httprouter](https://github.com/julienschmidt/httprouter) using `context.Context` * **server**: Common servers * **version**: Version information and metrics golang-github-prometheus-common-0.55.0/RELEASE.md000066400000000000000000000024101463701437000214130ustar00rootroot00000000000000# Releases ## What to do know before cutting a release While `prometheus/common` does not have a formal release process. We strongly encourage you follow these steps: 1. Scan the list of available issues / PRs and make sure that You attempt to merge any pull requests that appear to be ready or almost ready 2. Notify the maintainers listed as part of [`MANTAINERS.md`](MAINTAINERS.md) that you're going to do a release. With those steps done, you can proceed to cut a release. ## How to cut an individual release There is no automated process for cutting a release in `prometheus/common`. A manual release using GitHub's release feature via [this link](https://github.com/prometheus/prometheus/releases/new) is the best way to go. The tag name must be prefixed with a `v` e.g. `v0.53.0` and then you can use the "Generate release notes" button to generate the release note automagically ✨. No need to create a discussion or mark it a pre-release, please do mark it as the latest release if needed. ## Versioning strategy We aim to adhere to [Semantic Versioning](https://semver.org/) as much as possible. For example, patch version (e.g. v0.0.x) releases should contain bugfixes only and any sort of major or minor version bump should be a minor or major release respectively. golang-github-prometheus-common-0.55.0/SECURITY.md000066400000000000000000000002541463701437000216060ustar00rootroot00000000000000# Reporting a security issue The Prometheus security policy, including how to report vulnerabilities, can be found here: golang-github-prometheus-common-0.55.0/assets/000077500000000000000000000000001463701437000213165ustar00rootroot00000000000000golang-github-prometheus-common-0.55.0/assets/Makefile000066400000000000000000000012751463701437000227630ustar00rootroot00000000000000# Copyright 2018 The Prometheus Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. include ../Makefile.common .PHONY: test @echo ">> Running assets tests" test:: deps check_license unused common-test golang-github-prometheus-common-0.55.0/assets/embed_gzip.go000066400000000000000000000056571463701437000237670ustar00rootroot00000000000000// Copyright 2021 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package assets import ( "compress/gzip" "embed" "io" "io/fs" "time" ) const ( gzipSuffix = ".gz" ) type FileSystem struct { embed embed.FS } func New(fs embed.FS) FileSystem { return FileSystem{fs} } // Open implements the fs.FS interface. func (compressed FileSystem) Open(path string) (fs.File, error) { // If we have the file in our embed FS, just return that as it could be a dir. var f fs.File if f, err := compressed.embed.Open(path); err == nil { return f, nil } f, err := compressed.embed.Open(path + gzipSuffix) if err != nil { return f, err } // Read the decompressed content into a buffer. gr, err := gzip.NewReader(f) if err != nil { return f, err } defer gr.Close() c, err := io.ReadAll(gr) if err != nil { return f, err } // Wrap everything in our custom File. return &File{file: f, content: c}, nil } type File struct { // The underlying file. file fs.File // The decrompressed content, needed to return an accurate size. content []byte // Offset for calls to Read(). offset int } // Stat implements the fs.File interface. func (f File) Stat() (fs.FileInfo, error) { stat, err := f.file.Stat() if err != nil { return stat, err } return FileInfo{stat, int64(len(f.content))}, nil } // Read implements the fs.File interface. func (f *File) Read(buf []byte) (int, error) { if len(buf) > len(f.content)-f.offset { buf = buf[0:len(f.content[f.offset:])] } n := copy(buf, f.content[f.offset:]) if n == len(f.content)-f.offset { return n, io.EOF } f.offset += n return n, nil } // Close implements the fs.File interface. func (f File) Close() error { return f.file.Close() } type FileInfo struct { fi fs.FileInfo actualSize int64 } // Name implements the fs.FileInfo interface. func (fi FileInfo) Name() string { name := fi.fi.Name() return name[:len(name)-len(gzipSuffix)] } // Size implements the fs.FileInfo interface. func (fi FileInfo) Size() int64 { return fi.actualSize } // Mode implements the fs.FileInfo interface. func (fi FileInfo) Mode() fs.FileMode { return fi.fi.Mode() } // ModTime implements the fs.FileInfo interface. func (fi FileInfo) ModTime() time.Time { return fi.fi.ModTime() } // IsDir implements the fs.FileInfo interface. func (fi FileInfo) IsDir() bool { return fi.fi.IsDir() } // Sys implements the fs.FileInfo interface. func (fi FileInfo) Sys() interface{} { return nil } golang-github-prometheus-common-0.55.0/assets/embed_gzip_test.go000066400000000000000000000041761463701437000250210ustar00rootroot00000000000000// Copyright 2021 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package assets import ( "embed" "io" "strings" "testing" ) //go:embed testdata var EmbedFS embed.FS var testFS = New(EmbedFS) func TestFS(t *testing.T) { cases := []struct { name string path string expectedSize int64 expectedContent string }{ { name: "uncompressed file", path: "testdata/uncompressed", expectedSize: 4, expectedContent: "foo\n", }, { name: "compressed file", path: "testdata/compressed", expectedSize: 4, expectedContent: "foo\n", }, { name: "both, open uncompressed", path: "testdata/both", expectedSize: 4, expectedContent: "foo\n", }, { name: "both, open compressed", path: "testdata/both.gz", expectedSize: 29, // we don't check content for a explicitly compressed file expectedContent: "", }, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { f, err := testFS.Open(c.path) if err != nil { t.Fatal(err) } stat, err := f.Stat() if err != nil { t.Fatal(err) } size := stat.Size() if size != c.expectedSize { t.Fatalf("size is wrong, expected %d, got %d", c.expectedSize, size) } if strings.HasSuffix(c.path, ".gz") { // don't read the comressed content return } content, err := io.ReadAll(f) if err != nil { t.Fatal(err) } if string(content) != c.expectedContent { t.Fatalf("content is wrong, expected %s, got %s", c.expectedContent, string(content)) } }) } } golang-github-prometheus-common-0.55.0/assets/go.mod000066400000000000000000000000641463701437000224240ustar00rootroot00000000000000module github.com/prometheus/common/assets go 1.20 golang-github-prometheus-common-0.55.0/assets/testdata/000077500000000000000000000000001463701437000231275ustar00rootroot00000000000000golang-github-prometheus-common-0.55.0/assets/testdata/both000066400000000000000000000000041463701437000240000ustar00rootroot00000000000000foo golang-github-prometheus-common-0.55.0/assets/testdata/both.gz000066400000000000000000000000351463701437000244230ustar00rootroot00000000000000ьabothKe2~golang-github-prometheus-common-0.55.0/assets/testdata/compressed.gz000066400000000000000000000000431463701437000256320ustar00rootroot00000000000000 ьacompressedKe2~golang-github-prometheus-common-0.55.0/assets/testdata/uncompressed000066400000000000000000000000041463701437000255530ustar00rootroot00000000000000foo golang-github-prometheus-common-0.55.0/config/000077500000000000000000000000001463701437000212615ustar00rootroot00000000000000golang-github-prometheus-common-0.55.0/config/config.go000066400000000000000000000052511463701437000230600ustar00rootroot00000000000000// Copyright 2016 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // This package no longer handles safe yaml parsing. In order to // ensure correct yaml unmarshalling, use "yaml.UnmarshalStrict()". package config import ( "encoding/json" "net/http" "path/filepath" ) const secretToken = "" // Secret special type for storing secrets. type Secret string // MarshalSecretValue if set to true will expose Secret type // through the marshal interfaces. Useful for outside projects // that load and marshal the Prometheus config. var MarshalSecretValue bool = false // MarshalYAML implements the yaml.Marshaler interface for Secrets. func (s Secret) MarshalYAML() (interface{}, error) { if MarshalSecretValue { return string(s), nil } if s != "" { return secretToken, nil } return nil, nil } // UnmarshalYAML implements the yaml.Unmarshaler interface for Secrets. func (s *Secret) UnmarshalYAML(unmarshal func(interface{}) error) error { type plain Secret return unmarshal((*plain)(s)) } // MarshalJSON implements the json.Marshaler interface for Secret. func (s Secret) MarshalJSON() ([]byte, error) { if MarshalSecretValue { return json.Marshal(string(s)) } if len(s) == 0 { return json.Marshal("") } return json.Marshal(secretToken) } type ProxyHeader map[string][]Secret func (h *ProxyHeader) HTTPHeader() http.Header { if h == nil || *h == nil { return nil } header := make(http.Header) for name, values := range *h { var s []string if values != nil { s = make([]string, 0, len(values)) for _, value := range values { s = append(s, string(value)) } } header[name] = s } return header } // DirectorySetter is a config type that contains file paths that may // be relative to the file containing the config. type DirectorySetter interface { // SetDirectory joins any relative file paths with dir. // Any paths that are empty or absolute remain unchanged. SetDirectory(dir string) } // JoinDir joins dir and path if path is relative. // If path is empty or absolute, it is returned unchanged. func JoinDir(dir, path string) string { if path == "" || filepath.IsAbs(path) { return path } return filepath.Join(dir, path) } golang-github-prometheus-common-0.55.0/config/config_test.go000066400000000000000000000136571463701437000241300ustar00rootroot00000000000000// Copyright 2021 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package config import ( "bytes" "encoding/json" "net/http" "reflect" "testing" "gopkg.in/yaml.v2" ) func TestJSONMarshalSecret(t *testing.T) { type tmp struct { S Secret } for _, tc := range []struct { desc string data tmp expected string marshalSecret bool testYAML bool }{ { desc: "inhabited", // u003c -> "<" // u003e -> ">" data: tmp{"test"}, expected: "{\"S\":\"\\u003csecret\\u003e\"}", }, { desc: "true value in JSON", data: tmp{"test"}, expected: `{"S":"test"}`, marshalSecret: true, }, { desc: "true value in YAML", data: tmp{"test"}, expected: `s: test `, marshalSecret: true, testYAML: true, }, { desc: "empty", data: tmp{}, expected: "{\"S\":\"\"}", }, } { t.Run(tc.desc, func(t *testing.T) { MarshalSecretValue = tc.marshalSecret var marshalFN func(any) ([]byte, error) if tc.testYAML { marshalFN = yaml.Marshal } else { marshalFN = json.Marshal } c, err := marshalFN(tc.data) if err != nil { t.Fatal(err) } if tc.expected != string(c) { t.Fatalf("Secret not marshaled correctly, got '%s'", string(c)) } }) } } func TestHeaderHTTPHeader(t *testing.T) { testcases := map[string]struct { header ProxyHeader expected http.Header }{ "basic": { header: ProxyHeader{ "single": []Secret{"v1"}, "multi": []Secret{"v1", "v2"}, "empty": []Secret{}, "nil": nil, }, expected: http.Header{ "single": []string{"v1"}, "multi": []string{"v1", "v2"}, "empty": []string{}, "nil": nil, }, }, "nil": { header: nil, expected: nil, }, } for name, tc := range testcases { t.Run(name, func(t *testing.T) { actual := tc.header.HTTPHeader() if !reflect.DeepEqual(actual, tc.expected) { t.Fatalf("expecting: %#v, actual: %#v", tc.expected, actual) } }) } } func TestHeaderYamlUnmarshal(t *testing.T) { testcases := map[string]struct { input string expected ProxyHeader }{ "void": { input: ``, }, "simple": { input: "single:\n- a\n", expected: ProxyHeader{"single": []Secret{"a"}}, }, "multi": { input: "multi:\n- a\n- b\n", expected: ProxyHeader{"multi": []Secret{"a", "b"}}, }, "empty": { input: "{}", expected: ProxyHeader{}, }, "empty value": { input: "empty:\n", expected: ProxyHeader{"empty": nil}, }, } for name, tc := range testcases { t.Run(name, func(t *testing.T) { var actual ProxyHeader err := yaml.Unmarshal([]byte(tc.input), &actual) if err != nil { t.Fatalf("error unmarshaling %s: %s", tc.input, err) } if !reflect.DeepEqual(actual, tc.expected) { t.Fatalf("expecting: %#v, actual: %#v", tc.expected, actual) } }) } } func TestHeaderYamlMarshal(t *testing.T) { testcases := map[string]struct { input ProxyHeader expected []byte }{ "void": { input: nil, expected: []byte("{}\n"), }, "simple": { input: ProxyHeader{"single": []Secret{"a"}}, expected: []byte("single:\n- \n"), }, "multi": { input: ProxyHeader{"multi": []Secret{"a", "b"}}, expected: []byte("multi:\n- \n- \n"), }, "empty": { input: ProxyHeader{"empty": nil}, expected: []byte("empty: []\n"), }, } for name, tc := range testcases { t.Run(name, func(t *testing.T) { actual, err := yaml.Marshal(tc.input) if err != nil { t.Fatalf("error unmarshaling %#v: %s", tc.input, err) } if !bytes.Equal(actual, tc.expected) { t.Fatalf("expecting: %q, actual: %q", tc.expected, actual) } }) } } func TestHeaderJsonUnmarshal(t *testing.T) { testcases := map[string]struct { input string expected ProxyHeader }{ "void": { input: `null`, }, "simple": { input: `{"single": ["a"]}`, expected: ProxyHeader{"single": []Secret{"a"}}, }, "multi": { input: `{"multi": ["a", "b"]}`, expected: ProxyHeader{"multi": []Secret{"a", "b"}}, }, "empty": { input: `{}`, expected: ProxyHeader{}, }, "empty value": { input: `{"empty":null}`, expected: ProxyHeader{"empty": nil}, }, } for name, tc := range testcases { t.Run(name, func(t *testing.T) { var actual ProxyHeader err := json.Unmarshal([]byte(tc.input), &actual) if err != nil { t.Fatalf("error unmarshaling %s: %s", tc.input, err) } if !reflect.DeepEqual(actual, tc.expected) { t.Fatalf("expecting: %#v, actual: %#v", tc.expected, actual) } }) } } func TestHeaderJsonMarshal(t *testing.T) { testcases := map[string]struct { input ProxyHeader expected []byte }{ "void": { input: nil, expected: []byte("null"), }, "simple": { input: ProxyHeader{"single": []Secret{"a"}}, expected: []byte("{\"single\":[\"\\u003csecret\\u003e\"]}"), }, "multi": { input: ProxyHeader{"multi": []Secret{"a", "b"}}, expected: []byte("{\"multi\":[\"\\u003csecret\\u003e\",\"\\u003csecret\\u003e\"]}"), }, "empty": { input: ProxyHeader{"empty": nil}, expected: []byte(`{"empty":null}`), }, } for name, tc := range testcases { t.Run(name, func(t *testing.T) { actual, err := json.Marshal(tc.input) if err != nil { t.Fatalf("error marshaling %#v: %s", tc.input, err) } if !bytes.Equal(actual, tc.expected) { t.Fatalf("expecting: %q, actual: %q", tc.expected, actual) } }) } } golang-github-prometheus-common-0.55.0/config/generate.go000066400000000000000000000144671463701437000234160ustar00rootroot00000000000000// Copyright 2020 The Prometheus-operator Authors // Copyright 2022 The Prometheus Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build ignore // +build ignore // Program generating TLS certificates and keys for the tests. package main import ( "bytes" "crypto/rand" "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "encoding/pem" "fmt" "io" "log" "math/big" "net" "os" "time" ) const ( validityPeriod = 50 * 365 * 24 * time.Hour ) func EncodeCertificate(w io.Writer, cert *x509.Certificate) error { return pem.Encode(w, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}) } func EncodeKey(w io.Writer, priv *rsa.PrivateKey) error { b, err := x509.MarshalPKCS8PrivateKey(priv) if err != nil { return fmt.Errorf("failed to marshal private key: %v", err) } return pem.Encode(w, &pem.Block{Type: "PRIVATE KEY", Bytes: b}) } var serialNumber *big.Int func init() { serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) var err error serialNumber, err = rand.Int(rand.Reader, serialNumberLimit) if err != nil { panic(fmt.Errorf("failed to generate serial number: %v", err)) } } func SerialNumber() *big.Int { var serial big.Int serial.Set(serialNumber) serialNumber.Add(&serial, big.NewInt(1)) return &serial } func GenerateCertificateAuthority(commonName string, parentCert *x509.Certificate, parentKey *rsa.PrivateKey) (*x509.Certificate, *rsa.PrivateKey, error) { now := time.Now() caKey, err := rsa.GenerateKey(rand.Reader, 4096) if err != nil { return nil, nil, fmt.Errorf("failed to generate CA private key: %v", err) } caCert := &x509.Certificate{ SerialNumber: SerialNumber(), Subject: pkix.Name{ Country: []string{"US"}, Organization: []string{"Prometheus"}, OrganizationalUnit: []string{"Prometheus Certificate Authority"}, CommonName: commonName, }, NotBefore: now, NotAfter: now.Add(validityPeriod), KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageCertSign, IsCA: true, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, BasicConstraintsValid: true, } if parentCert == nil && parentKey == nil { parentCert = caCert parentKey = caKey } b, err := x509.CreateCertificate(rand.Reader, caCert, parentCert, &caKey.PublicKey, parentKey) if err != nil { return nil, nil, fmt.Errorf("failed to create CA certificate: %v", err) } caCert, err = x509.ParseCertificate(b) if err != nil { return nil, nil, fmt.Errorf("failed to decode CA certificate: %v", err) } return caCert, caKey, nil } func GenerateCertificate(caCert *x509.Certificate, caKey *rsa.PrivateKey, server bool, name string, ipAddresses ...net.IP) (*x509.Certificate, *rsa.PrivateKey, error) { now := time.Now() key, err := rsa.GenerateKey(rand.Reader, 4096) if err != nil { return nil, nil, fmt.Errorf("failed to generate private key: %v", err) } cert := &x509.Certificate{ SerialNumber: SerialNumber(), Subject: pkix.Name{ Country: []string{"US"}, Organization: []string{"Prometheus"}, CommonName: name, }, NotBefore: now, NotAfter: now.Add(validityPeriod), KeyUsage: x509.KeyUsageKeyEncipherment, BasicConstraintsValid: true, } if server { cert.DNSNames = []string{name} cert.IPAddresses = ipAddresses cert.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth} } else { cert.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth} } if caCert == nil && caKey == nil { caCert = cert caKey = key } b, err := x509.CreateCertificate(rand.Reader, cert, caCert, &key.PublicKey, caKey) if err != nil { return nil, nil, fmt.Errorf("failed to create certificate: %v", err) } cert, err = x509.ParseCertificate(b) if err != nil { return nil, nil, fmt.Errorf("failed to decode certificate: %v", err) } return cert, key, nil } func writeCertificateAndKey(path string, cert *x509.Certificate, key *rsa.PrivateKey) error { var b bytes.Buffer if err := EncodeCertificate(&b, cert); err != nil { return err } if err := os.WriteFile(fmt.Sprintf("%s.crt", path), b.Bytes(), 0o644); err != nil { return err } b.Reset() if err := EncodeKey(&b, key); err != nil { return err } if err := os.WriteFile(fmt.Sprintf("%s.key", path), b.Bytes(), 0o644); err != nil { return err } return nil } func main() { log.Println("Generating root CA") rootCert, rootKey, err := GenerateCertificateAuthority("Prometheus Root CA", nil, nil) if err != nil { log.Fatal(err) } log.Println("Generating CA") caCert, caKey, err := GenerateCertificateAuthority("Prometheus TLS CA", rootCert, rootKey) if err != nil { log.Fatal(err) } log.Println("Generating server certificate") cert, key, err := GenerateCertificate(caCert, caKey, true, "localhost", net.IPv4(127, 0, 0, 1), net.IPv4(127, 0, 0, 0)) if err != nil { log.Fatal(err) } if err := writeCertificateAndKey("testdata/server", cert, key); err != nil { log.Fatal(err) } log.Println("Generating client certificate") cert, key, err = GenerateCertificate(caCert, caKey, false, "localhost") if err != nil { log.Fatal(err) } if err := writeCertificateAndKey("testdata/client", cert, key); err != nil { log.Fatal(err) } log.Println("Generating self-signed client certificate") cert, key, err = GenerateCertificate(nil, nil, false, "localhost") if err != nil { log.Fatal(err) } if err := writeCertificateAndKey("testdata/self-signed-client", cert, key); err != nil { log.Fatal(err) } log.Println("Generating CA bundle") var b bytes.Buffer if err := EncodeCertificate(&b, caCert); err != nil { log.Fatal(err) } if err := EncodeCertificate(&b, rootCert); err != nil { log.Fatal(err) } if err := os.WriteFile("testdata/tls-ca-chain.pem", b.Bytes(), 0o644); err != nil { log.Fatal(err) } } golang-github-prometheus-common-0.55.0/config/headers.go000066400000000000000000000101611463701437000232220ustar00rootroot00000000000000// Copyright 2024 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // This package no longer handles safe yaml parsing. In order to // ensure correct yaml unmarshalling, use "yaml.UnmarshalStrict()". package config import ( "encoding/json" "fmt" "net/http" "os" "strings" ) // reservedHeaders that change the connection, are set by Prometheus, or can // be changed otherwise. var reservedHeaders = map[string]struct{}{ "Authorization": {}, "Host": {}, "Content-Encoding": {}, "Content-Length": {}, "Content-Type": {}, "User-Agent": {}, "Connection": {}, "Keep-Alive": {}, "Proxy-Authenticate": {}, "Proxy-Authorization": {}, "Www-Authenticate": {}, "Accept-Encoding": {}, "X-Prometheus-Remote-Write-Version": {}, "X-Prometheus-Remote-Read-Version": {}, "X-Prometheus-Scrape-Timeout-Seconds": {}, // Added by SigV4. "X-Amz-Date": {}, "X-Amz-Security-Token": {}, "X-Amz-Content-Sha256": {}, } // Headers represents the configuration for HTTP headers. type Headers struct { Headers map[string]Header `yaml:",inline"` dir string } // Header represents the configuration for a single HTTP header. type Header struct { Values []string `yaml:"values,omitempty" json:"values,omitempty"` Secrets []Secret `yaml:"secrets,omitempty" json:"secrets,omitempty"` Files []string `yaml:"files,omitempty" json:"files,omitempty"` } func (h Headers) MarshalJSON() ([]byte, error) { // Inline the Headers map when serializing JSON because json encoder doesn't support "inline" directive. return json.Marshal(h.Headers) } // SetDirectory records the directory to make headers file relative to the // configuration file. func (h *Headers) SetDirectory(dir string) { if h == nil { return } h.dir = dir } // Validate validates the Headers config. func (h *Headers) Validate() error { for n, header := range h.Headers { if _, ok := reservedHeaders[http.CanonicalHeaderKey(n)]; ok { return fmt.Errorf("setting header %q is not allowed", http.CanonicalHeaderKey(n)) } for _, v := range header.Files { f := JoinDir(h.dir, v) _, err := os.ReadFile(f) if err != nil { return fmt.Errorf("unable to read header %q from file %s: %w", http.CanonicalHeaderKey(n), f, err) } } } return nil } // NewHeadersRoundTripper returns a RoundTripper that sets HTTP headers on // requests as configured. func NewHeadersRoundTripper(config *Headers, next http.RoundTripper) http.RoundTripper { if len(config.Headers) == 0 { return next } return &headersRoundTripper{ config: config, next: next, } } type headersRoundTripper struct { next http.RoundTripper config *Headers } // RoundTrip implements http.RoundTripper. func (rt *headersRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { req = cloneRequest(req) for n, h := range rt.config.Headers { for _, v := range h.Values { req.Header.Add(n, v) } for _, v := range h.Secrets { req.Header.Add(n, string(v)) } for _, v := range h.Files { f := JoinDir(rt.config.dir, v) b, err := os.ReadFile(f) if err != nil { return nil, fmt.Errorf("unable to read headers file %s: %w", f, err) } req.Header.Add(n, strings.TrimSpace(string(b))) } } return rt.next.RoundTrip(req) } // CloseIdleConnections implements closeIdler. func (rt *headersRoundTripper) CloseIdleConnections() { if ci, ok := rt.next.(closeIdler); ok { ci.CloseIdleConnections() } } golang-github-prometheus-common-0.55.0/config/headers_test.go000066400000000000000000000017721463701437000242710ustar00rootroot00000000000000// Copyright 2024 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // This package no longer handles safe yaml parsing. In order to // ensure correct yaml unmarshalling, use "yaml.UnmarshalStrict()". package config import ( "net/http" "testing" ) func TestReservedHeaders(t *testing.T) { for k := range reservedHeaders { l := http.CanonicalHeaderKey(k) if k != l { t.Errorf("reservedHeaders keys should be lowercase: got %q, expected %q", k, http.CanonicalHeaderKey(k)) } } } golang-github-prometheus-common-0.55.0/config/http_config.go000066400000000000000000001347021463701437000241230ustar00rootroot00000000000000// Copyright 2016 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package config import ( "bytes" "context" "crypto/sha256" "crypto/tls" "crypto/x509" "encoding/json" "errors" "fmt" "net" "net/http" "net/url" "os" "path/filepath" "strings" "sync" "time" conntrack "github.com/mwitkow/go-conntrack" "golang.org/x/net/http/httpproxy" "golang.org/x/net/http2" "golang.org/x/oauth2" "golang.org/x/oauth2/clientcredentials" "gopkg.in/yaml.v2" ) var ( // DefaultHTTPClientConfig is the default HTTP client configuration. DefaultHTTPClientConfig = HTTPClientConfig{ FollowRedirects: true, EnableHTTP2: true, } // defaultHTTPClientOptions holds the default HTTP client options. defaultHTTPClientOptions = httpClientOptions{ keepAlivesEnabled: true, http2Enabled: true, // 5 minutes is typically above the maximum sane scrape interval. So we can // use keepalive for all configurations. idleConnTimeout: 5 * time.Minute, } ) type closeIdler interface { CloseIdleConnections() } type TLSVersion uint16 var TLSVersions = map[string]TLSVersion{ "TLS13": (TLSVersion)(tls.VersionTLS13), "TLS12": (TLSVersion)(tls.VersionTLS12), "TLS11": (TLSVersion)(tls.VersionTLS11), "TLS10": (TLSVersion)(tls.VersionTLS10), } func (tv *TLSVersion) UnmarshalYAML(unmarshal func(interface{}) error) error { var s string err := unmarshal((*string)(&s)) if err != nil { return err } if v, ok := TLSVersions[s]; ok { *tv = v return nil } return fmt.Errorf("unknown TLS version: %s", s) } func (tv TLSVersion) MarshalYAML() (interface{}, error) { for s, v := range TLSVersions { if tv == v { return s, nil } } return nil, fmt.Errorf("unknown TLS version: %d", tv) } // MarshalJSON implements the json.Unmarshaler interface for TLSVersion. func (tv *TLSVersion) UnmarshalJSON(data []byte) error { var s string if err := json.Unmarshal(data, &s); err != nil { return err } if v, ok := TLSVersions[s]; ok { *tv = v return nil } return fmt.Errorf("unknown TLS version: %s", s) } // MarshalJSON implements the json.Marshaler interface for TLSVersion. func (tv TLSVersion) MarshalJSON() ([]byte, error) { for s, v := range TLSVersions { if tv == v { return json.Marshal(s) } } return nil, fmt.Errorf("unknown TLS version: %d", tv) } // String implements the fmt.Stringer interface for TLSVersion. func (tv *TLSVersion) String() string { if tv == nil || *tv == 0 { return "" } for s, v := range TLSVersions { if *tv == v { return s } } return fmt.Sprintf("%d", tv) } // BasicAuth contains basic HTTP authentication credentials. type BasicAuth struct { Username string `yaml:"username" json:"username"` UsernameFile string `yaml:"username_file,omitempty" json:"username_file,omitempty"` // UsernameRef is the name of the secret within the secret manager to use as the username. UsernameRef string `yaml:"username_ref,omitempty" json:"username_ref,omitempty"` Password Secret `yaml:"password,omitempty" json:"password,omitempty"` PasswordFile string `yaml:"password_file,omitempty" json:"password_file,omitempty"` // PasswordRef is the name of the secret within the secret manager to use as the password. PasswordRef string `yaml:"password_ref,omitempty" json:"password_ref,omitempty"` } // SetDirectory joins any relative file paths with dir. func (a *BasicAuth) SetDirectory(dir string) { if a == nil { return } a.PasswordFile = JoinDir(dir, a.PasswordFile) a.UsernameFile = JoinDir(dir, a.UsernameFile) } // Authorization contains HTTP authorization credentials. type Authorization struct { Type string `yaml:"type,omitempty" json:"type,omitempty"` Credentials Secret `yaml:"credentials,omitempty" json:"credentials,omitempty"` CredentialsFile string `yaml:"credentials_file,omitempty" json:"credentials_file,omitempty"` // CredentialsRef is the name of the secret within the secret manager to use as credentials. CredentialsRef string `yaml:"credentials_ref,omitempty" json:"credentials_ref,omitempty"` } // SetDirectory joins any relative file paths with dir. func (a *Authorization) SetDirectory(dir string) { if a == nil { return } a.CredentialsFile = JoinDir(dir, a.CredentialsFile) } // URL is a custom URL type that allows validation at configuration load time. type URL struct { *url.URL } // UnmarshalYAML implements the yaml.Unmarshaler interface for URLs. func (u *URL) UnmarshalYAML(unmarshal func(interface{}) error) error { var s string if err := unmarshal(&s); err != nil { return err } urlp, err := url.Parse(s) if err != nil { return err } u.URL = urlp return nil } // MarshalYAML implements the yaml.Marshaler interface for URLs. func (u URL) MarshalYAML() (interface{}, error) { if u.URL != nil { return u.Redacted(), nil } return nil, nil } // Redacted returns the URL but replaces any password with "xxxxx". func (u URL) Redacted() string { if u.URL == nil { return "" } ru := *u.URL if _, ok := ru.User.Password(); ok { // We can not use secretToken because it would be escaped. ru.User = url.UserPassword(ru.User.Username(), "xxxxx") } return ru.String() } // UnmarshalJSON implements the json.Marshaler interface for URL. func (u *URL) UnmarshalJSON(data []byte) error { var s string if err := json.Unmarshal(data, &s); err != nil { return err } urlp, err := url.Parse(s) if err != nil { return err } u.URL = urlp return nil } // MarshalJSON implements the json.Marshaler interface for URL. func (u URL) MarshalJSON() ([]byte, error) { if u.URL != nil { return json.Marshal(u.URL.String()) } return []byte("null"), nil } // OAuth2 is the oauth2 client configuration. type OAuth2 struct { ClientID string `yaml:"client_id" json:"client_id"` ClientSecret Secret `yaml:"client_secret" json:"client_secret"` ClientSecretFile string `yaml:"client_secret_file" json:"client_secret_file"` // ClientSecretRef is the name of the secret within the secret manager to use as the client // secret. ClientSecretRef string `yaml:"client_secret_ref" json:"client_secret_ref"` Scopes []string `yaml:"scopes,omitempty" json:"scopes,omitempty"` TokenURL string `yaml:"token_url" json:"token_url"` EndpointParams map[string]string `yaml:"endpoint_params,omitempty" json:"endpoint_params,omitempty"` TLSConfig TLSConfig `yaml:"tls_config,omitempty"` ProxyConfig `yaml:",inline"` } // UnmarshalYAML implements the yaml.Unmarshaler interface func (o *OAuth2) UnmarshalYAML(unmarshal func(interface{}) error) error { type plain OAuth2 if err := unmarshal((*plain)(o)); err != nil { return err } return o.ProxyConfig.Validate() } // UnmarshalJSON implements the json.Marshaler interface for URL. func (o *OAuth2) UnmarshalJSON(data []byte) error { type plain OAuth2 if err := json.Unmarshal(data, (*plain)(o)); err != nil { return err } return o.ProxyConfig.Validate() } // SetDirectory joins any relative file paths with dir. func (o *OAuth2) SetDirectory(dir string) { if o == nil { return } o.ClientSecretFile = JoinDir(dir, o.ClientSecretFile) o.TLSConfig.SetDirectory(dir) } // LoadHTTPConfig parses the YAML input s into a HTTPClientConfig. func LoadHTTPConfig(s string) (*HTTPClientConfig, error) { cfg := &HTTPClientConfig{} err := yaml.UnmarshalStrict([]byte(s), cfg) if err != nil { return nil, err } return cfg, nil } // LoadHTTPConfigFile parses the given YAML file into a HTTPClientConfig. func LoadHTTPConfigFile(filename string) (*HTTPClientConfig, []byte, error) { content, err := os.ReadFile(filename) if err != nil { return nil, nil, err } cfg, err := LoadHTTPConfig(string(content)) if err != nil { return nil, nil, err } cfg.SetDirectory(filepath.Dir(filepath.Dir(filename))) return cfg, content, nil } // HTTPClientConfig configures an HTTP client. type HTTPClientConfig struct { // The HTTP basic authentication credentials for the targets. BasicAuth *BasicAuth `yaml:"basic_auth,omitempty" json:"basic_auth,omitempty"` // The HTTP authorization credentials for the targets. Authorization *Authorization `yaml:"authorization,omitempty" json:"authorization,omitempty"` // The OAuth2 client credentials used to fetch a token for the targets. OAuth2 *OAuth2 `yaml:"oauth2,omitempty" json:"oauth2,omitempty"` // The bearer token for the targets. Deprecated in favour of // Authorization.Credentials. BearerToken Secret `yaml:"bearer_token,omitempty" json:"bearer_token,omitempty"` // The bearer token file for the targets. Deprecated in favour of // Authorization.CredentialsFile. BearerTokenFile string `yaml:"bearer_token_file,omitempty" json:"bearer_token_file,omitempty"` // TLSConfig to use to connect to the targets. TLSConfig TLSConfig `yaml:"tls_config,omitempty" json:"tls_config,omitempty"` // FollowRedirects specifies whether the client should follow HTTP 3xx redirects. // The omitempty flag is not set, because it would be hidden from the // marshalled configuration when set to false. FollowRedirects bool `yaml:"follow_redirects" json:"follow_redirects"` // EnableHTTP2 specifies whether the client should configure HTTP2. // The omitempty flag is not set, because it would be hidden from the // marshalled configuration when set to false. EnableHTTP2 bool `yaml:"enable_http2" json:"enable_http2"` // Proxy configuration. ProxyConfig `yaml:",inline"` // HTTPHeaders specify headers to inject in the requests. Those headers // could be marshalled back to the users. HTTPHeaders *Headers `yaml:"http_headers,omitempty" json:"http_headers,omitempty"` } // SetDirectory joins any relative file paths with dir. func (c *HTTPClientConfig) SetDirectory(dir string) { if c == nil { return } c.TLSConfig.SetDirectory(dir) c.BasicAuth.SetDirectory(dir) c.Authorization.SetDirectory(dir) c.OAuth2.SetDirectory(dir) c.HTTPHeaders.SetDirectory(dir) c.BearerTokenFile = JoinDir(dir, c.BearerTokenFile) } // nonZeroCount returns the amount of values that are non-zero. func nonZeroCount[T comparable](values ...T) int { count := 0 var zero T for _, value := range values { if value != zero { count += 1 } } return count } // Validate validates the HTTPClientConfig to check only one of BearerToken, // BasicAuth and BearerTokenFile is configured. It also validates that ProxyURL // is set if ProxyConnectHeader is set. func (c *HTTPClientConfig) Validate() error { // Backwards compatibility with the bearer_token field. if len(c.BearerToken) > 0 && len(c.BearerTokenFile) > 0 { return fmt.Errorf("at most one of bearer_token & bearer_token_file must be configured") } if (c.BasicAuth != nil || c.OAuth2 != nil) && (len(c.BearerToken) > 0 || len(c.BearerTokenFile) > 0) { return fmt.Errorf("at most one of basic_auth, oauth2, bearer_token & bearer_token_file must be configured") } if c.BasicAuth != nil && nonZeroCount(string(c.BasicAuth.Username) != "", c.BasicAuth.UsernameFile != "", c.BasicAuth.UsernameRef != "") > 1 { return fmt.Errorf("at most one of basic_auth username, username_file & username_ref must be configured") } if c.BasicAuth != nil && nonZeroCount(string(c.BasicAuth.Password) != "", c.BasicAuth.PasswordFile != "", c.BasicAuth.PasswordRef != "") > 1 { return fmt.Errorf("at most one of basic_auth password, password_file & password_ref must be configured") } if c.Authorization != nil { if len(c.BearerToken) > 0 || len(c.BearerTokenFile) > 0 { return fmt.Errorf("authorization is not compatible with bearer_token & bearer_token_file") } if nonZeroCount(string(c.Authorization.Credentials) != "", c.Authorization.CredentialsFile != "", c.Authorization.CredentialsRef != "") > 1 { return fmt.Errorf("at most one of authorization credentials & credentials_file must be configured") } c.Authorization.Type = strings.TrimSpace(c.Authorization.Type) if len(c.Authorization.Type) == 0 { c.Authorization.Type = "Bearer" } if strings.ToLower(c.Authorization.Type) == "basic" { return fmt.Errorf(`authorization type cannot be set to "basic", use "basic_auth" instead`) } if c.BasicAuth != nil || c.OAuth2 != nil { return fmt.Errorf("at most one of basic_auth, oauth2 & authorization must be configured") } } else { if len(c.BearerToken) > 0 { c.Authorization = &Authorization{Credentials: c.BearerToken} c.Authorization.Type = "Bearer" c.BearerToken = "" } if len(c.BearerTokenFile) > 0 { c.Authorization = &Authorization{CredentialsFile: c.BearerTokenFile} c.Authorization.Type = "Bearer" c.BearerTokenFile = "" } } if c.OAuth2 != nil { if c.BasicAuth != nil { return fmt.Errorf("at most one of basic_auth, oauth2 & authorization must be configured") } if len(c.OAuth2.ClientID) == 0 { return fmt.Errorf("oauth2 client_id must be configured") } if len(c.OAuth2.TokenURL) == 0 { return fmt.Errorf("oauth2 token_url must be configured") } if nonZeroCount(len(c.OAuth2.ClientSecret) > 0, len(c.OAuth2.ClientSecretFile) > 0, len(c.OAuth2.ClientSecretRef) > 0) > 1 { return fmt.Errorf("at most one of oauth2 client_secret, client_secret_file & client_secret_ref must be configured") } } if err := c.ProxyConfig.Validate(); err != nil { return err } if c.HTTPHeaders != nil { if err := c.HTTPHeaders.Validate(); err != nil { return err } } return nil } // UnmarshalYAML implements the yaml.Unmarshaler interface func (c *HTTPClientConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { type plain HTTPClientConfig *c = DefaultHTTPClientConfig if err := unmarshal((*plain)(c)); err != nil { return err } return c.Validate() } // UnmarshalJSON implements the json.Marshaler interface for URL. func (c *HTTPClientConfig) UnmarshalJSON(data []byte) error { type plain HTTPClientConfig *c = DefaultHTTPClientConfig if err := json.Unmarshal(data, (*plain)(c)); err != nil { return err } return c.Validate() } // UnmarshalYAML implements the yaml.Unmarshaler interface. func (a *BasicAuth) UnmarshalYAML(unmarshal func(interface{}) error) error { type plain BasicAuth return unmarshal((*plain)(a)) } // DialContextFunc defines the signature of the DialContext() function implemented // by net.Dialer. type DialContextFunc func(context.Context, string, string) (net.Conn, error) type httpClientOptions struct { dialContextFunc DialContextFunc keepAlivesEnabled bool http2Enabled bool idleConnTimeout time.Duration userAgent string host string secretManager SecretManager } // HTTPClientOption defines an option that can be applied to the HTTP client. type HTTPClientOption interface { applyToHTTPClientOptions(options *httpClientOptions) } type httpClientOptionFunc func(options *httpClientOptions) func (f httpClientOptionFunc) applyToHTTPClientOptions(options *httpClientOptions) { f(options) } // WithDialContextFunc allows you to override func gets used for the actual dialing. The default is `net.Dialer.DialContext`. func WithDialContextFunc(fn DialContextFunc) HTTPClientOption { return httpClientOptionFunc(func(opts *httpClientOptions) { opts.dialContextFunc = fn }) } // WithKeepAlivesDisabled allows to disable HTTP keepalive. func WithKeepAlivesDisabled() HTTPClientOption { return httpClientOptionFunc(func(opts *httpClientOptions) { opts.keepAlivesEnabled = false }) } // WithHTTP2Disabled allows to disable HTTP2. func WithHTTP2Disabled() HTTPClientOption { return httpClientOptionFunc(func(opts *httpClientOptions) { opts.http2Enabled = false }) } // WithIdleConnTimeout allows setting the idle connection timeout. func WithIdleConnTimeout(timeout time.Duration) HTTPClientOption { return httpClientOptionFunc(func(opts *httpClientOptions) { opts.idleConnTimeout = timeout }) } // WithUserAgent allows setting the user agent. func WithUserAgent(ua string) HTTPClientOption { return httpClientOptionFunc(func(opts *httpClientOptions) { opts.userAgent = ua }) } // WithHost allows setting the host header. func WithHost(host string) HTTPClientOption { return httpClientOptionFunc(func(opts *httpClientOptions) { opts.host = host }) } type secretManagerOption struct { secretManager SecretManager } func (s *secretManagerOption) applyToHTTPClientOptions(opts *httpClientOptions) { opts.secretManager = s.secretManager } func (s *secretManagerOption) applyToTLSConfigOptions(opts *tlsConfigOptions) { opts.secretManager = s.secretManager } // WithSecretManager allows setting the secret manager. func WithSecretManager(manager SecretManager) *secretManagerOption { return &secretManagerOption{ secretManager: manager, } } // NewClient returns a http.Client using the specified http.RoundTripper. func newClient(rt http.RoundTripper) *http.Client { return &http.Client{Transport: rt} } // NewClientFromConfig returns a new HTTP client configured for the // given config.HTTPClientConfig and config.HTTPClientOption. // The name is used as go-conntrack metric label. func NewClientFromConfig(cfg HTTPClientConfig, name string, optFuncs ...HTTPClientOption) (*http.Client, error) { rt, err := NewRoundTripperFromConfig(cfg, name, optFuncs...) if err != nil { return nil, err } client := newClient(rt) if !cfg.FollowRedirects { client.CheckRedirect = func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse } } return client, nil } // NewRoundTripperFromConfig returns a new HTTP RoundTripper configured for the // given config.HTTPClientConfig and config.HTTPClientOption. // The name is used as go-conntrack metric label. func NewRoundTripperFromConfig(cfg HTTPClientConfig, name string, optFuncs ...HTTPClientOption) (http.RoundTripper, error) { return NewRoundTripperFromConfigWithContext(context.Background(), cfg, name, optFuncs...) } // NewRoundTripperFromConfigWithContext returns a new HTTP RoundTripper configured for the // given config.HTTPClientConfig and config.HTTPClientOption. // The name is used as go-conntrack metric label. func NewRoundTripperFromConfigWithContext(ctx context.Context, cfg HTTPClientConfig, name string, optFuncs ...HTTPClientOption) (http.RoundTripper, error) { opts := defaultHTTPClientOptions for _, opt := range optFuncs { opt.applyToHTTPClientOptions(&opts) } var dialContext func(ctx context.Context, network, addr string) (net.Conn, error) if opts.dialContextFunc != nil { dialContext = conntrack.NewDialContextFunc( conntrack.DialWithDialContextFunc((func(context.Context, string, string) (net.Conn, error))(opts.dialContextFunc)), conntrack.DialWithTracing(), conntrack.DialWithName(name)) } else { dialContext = conntrack.NewDialContextFunc( conntrack.DialWithTracing(), conntrack.DialWithName(name)) } newRT := func(tlsConfig *tls.Config) (http.RoundTripper, error) { // The only timeout we care about is the configured scrape timeout. // It is applied on request. So we leave out any timings here. var rt http.RoundTripper = &http.Transport{ Proxy: cfg.ProxyConfig.Proxy(), ProxyConnectHeader: cfg.ProxyConfig.GetProxyConnectHeader(), MaxIdleConns: 20000, MaxIdleConnsPerHost: 1000, // see https://github.com/golang/go/issues/13801 DisableKeepAlives: !opts.keepAlivesEnabled, TLSClientConfig: tlsConfig, DisableCompression: true, IdleConnTimeout: opts.idleConnTimeout, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, DialContext: dialContext, } if opts.http2Enabled && cfg.EnableHTTP2 { // HTTP/2 support is golang had many problematic cornercases where // dead connections would be kept and used in connection pools. // https://github.com/golang/go/issues/32388 // https://github.com/golang/go/issues/39337 // https://github.com/golang/go/issues/39750 http2t, err := http2.ConfigureTransports(rt.(*http.Transport)) if err != nil { return nil, err } http2t.ReadIdleTimeout = time.Minute } // If a authorization_credentials is provided, create a round tripper that will set the // Authorization header correctly on each request. if cfg.Authorization != nil { credentialsSecret, err := toSecret(opts.secretManager, cfg.Authorization.Credentials, cfg.Authorization.CredentialsFile, cfg.Authorization.CredentialsRef) if err != nil { return nil, fmt.Errorf("unable to use credentials: %w", err) } rt = NewAuthorizationCredentialsRoundTripper(cfg.Authorization.Type, credentialsSecret, rt) } // Backwards compatibility, be nice with importers who would not have // called Validate(). if len(cfg.BearerToken) > 0 || len(cfg.BearerTokenFile) > 0 { bearerSecret, err := toSecret(opts.secretManager, cfg.BearerToken, cfg.BearerTokenFile, "") if err != nil { return nil, fmt.Errorf("unable to use bearer token: %w", err) } rt = NewAuthorizationCredentialsRoundTripper("Bearer", bearerSecret, rt) } if cfg.BasicAuth != nil { usernameSecret, err := toSecret(opts.secretManager, Secret(cfg.BasicAuth.Username), cfg.BasicAuth.UsernameFile, cfg.BasicAuth.UsernameRef) if err != nil { return nil, fmt.Errorf("unable to use username: %w", err) } passwordSecret, err := toSecret(opts.secretManager, cfg.BasicAuth.Password, cfg.BasicAuth.PasswordFile, cfg.BasicAuth.PasswordRef) if err != nil { return nil, fmt.Errorf("unable to use password: %w", err) } rt = NewBasicAuthRoundTripper(usernameSecret, passwordSecret, rt) } if cfg.OAuth2 != nil { clientSecret, err := toSecret(opts.secretManager, cfg.OAuth2.ClientSecret, cfg.OAuth2.ClientSecretFile, cfg.OAuth2.ClientSecretRef) if err != nil { return nil, fmt.Errorf("unable to use client secret: %w", err) } rt = NewOAuth2RoundTripper(clientSecret, cfg.OAuth2, rt, &opts) } if cfg.HTTPHeaders != nil { rt = NewHeadersRoundTripper(cfg.HTTPHeaders, rt) } if opts.userAgent != "" { rt = NewUserAgentRoundTripper(opts.userAgent, rt) } if opts.host != "" { rt = NewHostRoundTripper(opts.host, rt) } // Return a new configured RoundTripper. return rt, nil } tlsConfig, err := NewTLSConfig(&cfg.TLSConfig, WithSecretManager(opts.secretManager)) if err != nil { return nil, err } tlsSettings, err := cfg.TLSConfig.roundTripperSettings(opts.secretManager) if err != nil { return nil, err } if tlsSettings.CA == nil || tlsSettings.CA.Immutable() { // No need for a RoundTripper that reloads the CA file automatically. return newRT(tlsConfig) } return NewTLSRoundTripperWithContext(ctx, tlsConfig, tlsSettings, newRT) } // SecretManager manages secret data mapped to names known as "references" or "refs". type SecretManager interface { // Fetch returns the secret data given a secret name indicated by `secretRef`. Fetch(ctx context.Context, secretRef string) (string, error) } type SecretReader interface { Fetch(ctx context.Context) (string, error) Description() string Immutable() bool } type InlineSecret struct { text string } func NewInlineSecret(text string) *InlineSecret { return &InlineSecret{text: text} } func (s *InlineSecret) Fetch(context.Context) (string, error) { return s.text, nil } func (s *InlineSecret) Description() string { return "inline" } func (s *InlineSecret) Immutable() bool { return true } type FileSecret struct { file string } func NewFileSecret(file string) *FileSecret { return &FileSecret{file: file} } func (s *FileSecret) Fetch(ctx context.Context) (string, error) { fileBytes, err := os.ReadFile(s.file) if err != nil { return "", fmt.Errorf("unable to read file %s: %w", s.file, err) } return strings.TrimSpace(string(fileBytes)), nil } func (s *FileSecret) Description() string { return fmt.Sprintf("file %s", s.file) } func (s *FileSecret) Immutable() bool { return false } // refSecret fetches a single secret from a SecretManager. type refSecret struct { ref string manager SecretManager // manager is expected to be not nil. } func (s *refSecret) Fetch(ctx context.Context) (string, error) { return s.manager.Fetch(ctx, s.ref) } func (s *refSecret) Description() string { return fmt.Sprintf("ref %s", s.ref) } func (s *refSecret) Immutable() bool { return false } // toSecret returns a SecretReader from one of the given sources, assuming exactly // one or none of the sources are provided. func toSecret(secretManager SecretManager, text Secret, file, ref string) (SecretReader, error) { if text != "" { return NewInlineSecret(string(text)), nil } if file != "" { return NewFileSecret(file), nil } if ref != "" { if secretManager == nil { return nil, errors.New("cannot use secret ref without manager") } return &refSecret{ ref: ref, manager: secretManager, }, nil } return nil, nil } type authorizationCredentialsRoundTripper struct { authType string authCredentials SecretReader rt http.RoundTripper } // NewAuthorizationCredentialsRoundTripper adds the authorization credentials // read from the provided SecretReader to a request unless the authorization header // has already been set. func NewAuthorizationCredentialsRoundTripper(authType string, authCredentials SecretReader, rt http.RoundTripper) http.RoundTripper { return &authorizationCredentialsRoundTripper{authType, authCredentials, rt} } func (rt *authorizationCredentialsRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { if len(req.Header.Get("Authorization")) != 0 { return rt.rt.RoundTrip(req) } var authCredentials string if rt.authCredentials != nil { var err error authCredentials, err = rt.authCredentials.Fetch(req.Context()) if err != nil { return nil, fmt.Errorf("unable to read authorization credentials: %w", err) } } req = cloneRequest(req) req.Header.Set("Authorization", fmt.Sprintf("%s %s", rt.authType, authCredentials)) return rt.rt.RoundTrip(req) } func (rt *authorizationCredentialsRoundTripper) CloseIdleConnections() { if ci, ok := rt.rt.(closeIdler); ok { ci.CloseIdleConnections() } } type basicAuthRoundTripper struct { username SecretReader password SecretReader rt http.RoundTripper } // NewBasicAuthRoundTripper will apply a BASIC auth authorization header to a request unless it has // already been set. func NewBasicAuthRoundTripper(username SecretReader, password SecretReader, rt http.RoundTripper) http.RoundTripper { return &basicAuthRoundTripper{username, password, rt} } func (rt *basicAuthRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { if len(req.Header.Get("Authorization")) != 0 { return rt.rt.RoundTrip(req) } var username string var password string if rt.username != nil { var err error username, err = rt.username.Fetch(req.Context()) if err != nil { return nil, fmt.Errorf("unable to read basic auth username: %w", err) } } if rt.password != nil { var err error password, err = rt.password.Fetch(req.Context()) if err != nil { return nil, fmt.Errorf("unable to read basic auth password: %w", err) } } req = cloneRequest(req) req.SetBasicAuth(username, password) return rt.rt.RoundTrip(req) } func (rt *basicAuthRoundTripper) CloseIdleConnections() { if ci, ok := rt.rt.(closeIdler); ok { ci.CloseIdleConnections() } } type oauth2RoundTripper struct { mtx sync.RWMutex lastRT *oauth2.Transport lastSecret string // Required for interaction with Oauth2 server. config *OAuth2 clientSecret SecretReader opts *httpClientOptions client *http.Client } func NewOAuth2RoundTripper(clientSecret SecretReader, config *OAuth2, next http.RoundTripper, opts *httpClientOptions) http.RoundTripper { if clientSecret == nil { clientSecret = NewInlineSecret("") } return &oauth2RoundTripper{ config: config, // A correct tokenSource will be added later on. lastRT: &oauth2.Transport{Base: next}, opts: opts, clientSecret: clientSecret, } } func (rt *oauth2RoundTripper) newOauth2TokenSource(req *http.Request, secret string) (client *http.Client, source oauth2.TokenSource, err error) { tlsConfig, err := NewTLSConfig(&rt.config.TLSConfig, WithSecretManager(rt.opts.secretManager)) if err != nil { return nil, nil, err } tlsTransport := func(tlsConfig *tls.Config) (http.RoundTripper, error) { return &http.Transport{ TLSClientConfig: tlsConfig, Proxy: rt.config.ProxyConfig.Proxy(), ProxyConnectHeader: rt.config.ProxyConfig.GetProxyConnectHeader(), DisableKeepAlives: !rt.opts.keepAlivesEnabled, MaxIdleConns: 20, MaxIdleConnsPerHost: 1, // see https://github.com/golang/go/issues/13801 IdleConnTimeout: 10 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, }, nil } var t http.RoundTripper tlsSettings, err := rt.config.TLSConfig.roundTripperSettings(rt.opts.secretManager) if err != nil { return nil, nil, err } if tlsSettings.CA == nil || tlsSettings.CA.Immutable() { t, _ = tlsTransport(tlsConfig) } else { t, err = NewTLSRoundTripperWithContext(req.Context(), tlsConfig, tlsSettings, tlsTransport) if err != nil { return nil, nil, err } } if ua := req.UserAgent(); ua != "" { t = NewUserAgentRoundTripper(ua, t) } config := &clientcredentials.Config{ ClientID: rt.config.ClientID, ClientSecret: secret, Scopes: rt.config.Scopes, TokenURL: rt.config.TokenURL, EndpointParams: mapToValues(rt.config.EndpointParams), } client = &http.Client{Transport: t} ctx := context.WithValue(context.Background(), oauth2.HTTPClient, client) return client, config.TokenSource(ctx), nil } func (rt *oauth2RoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { var ( secret string needsInit bool ) rt.mtx.RLock() secret = rt.lastSecret needsInit = rt.lastRT.Source == nil rt.mtx.RUnlock() // Fetch the secret if it's our first run or always if the secret can change. if !rt.clientSecret.Immutable() || needsInit { newSecret, err := rt.clientSecret.Fetch(req.Context()) if err != nil { return nil, fmt.Errorf("unable to read oauth2 client secret: %w", err) } if newSecret != secret || needsInit { // Secret changed or it's a first run. Rebuilt oauth2 setup. client, source, err := rt.newOauth2TokenSource(req, newSecret) if err != nil { return nil, err } rt.mtx.Lock() rt.lastSecret = secret rt.lastRT.Source = source if rt.client != nil { rt.client.CloseIdleConnections() } rt.client = client rt.mtx.Unlock() } } rt.mtx.RLock() currentRT := rt.lastRT rt.mtx.RUnlock() return currentRT.RoundTrip(req) } func (rt *oauth2RoundTripper) CloseIdleConnections() { if rt.client != nil { rt.client.CloseIdleConnections() } if ci, ok := rt.lastRT.Base.(closeIdler); ok { ci.CloseIdleConnections() } } func mapToValues(m map[string]string) url.Values { v := url.Values{} for name, value := range m { v.Set(name, value) } return v } // cloneRequest returns a clone of the provided *http.Request. // The clone is a shallow copy of the struct and its Header map. func cloneRequest(r *http.Request) *http.Request { // Shallow copy of the struct. r2 := new(http.Request) *r2 = *r // Deep copy of the Header. r2.Header = make(http.Header) for k, s := range r.Header { r2.Header[k] = s } return r2 } type tlsConfigOptions struct { secretManager SecretManager } // TLSConfigOption defines an option that can be applied to the HTTP client. type TLSConfigOption interface { applyToTLSConfigOptions(options *tlsConfigOptions) } // NewTLSConfig creates a new tls.Config from the given TLSConfig. func NewTLSConfig(cfg *TLSConfig, optFuncs ...TLSConfigOption) (*tls.Config, error) { return NewTLSConfigWithContext(context.Background(), cfg, optFuncs...) } // NewTLSConfigWithContext creates a new tls.Config from the given TLSConfig. func NewTLSConfigWithContext(ctx context.Context, cfg *TLSConfig, optFuncs ...TLSConfigOption) (*tls.Config, error) { opts := tlsConfigOptions{} for _, opt := range optFuncs { opt.applyToTLSConfigOptions(&opts) } if err := cfg.Validate(); err != nil { return nil, err } tlsConfig := &tls.Config{ InsecureSkipVerify: cfg.InsecureSkipVerify, MinVersion: uint16(cfg.MinVersion), MaxVersion: uint16(cfg.MaxVersion), } if cfg.MaxVersion != 0 && cfg.MinVersion != 0 { if cfg.MaxVersion < cfg.MinVersion { return nil, fmt.Errorf("tls_config.max_version must be greater than or equal to tls_config.min_version if both are specified") } } // If a CA cert is provided then let's read it in so we can validate the // scrape target's certificate properly. caSecret, err := toSecret(opts.secretManager, Secret(cfg.CA), cfg.CAFile, cfg.CARef) if err != nil { return nil, fmt.Errorf("unable to use CA cert: %w", err) } if caSecret != nil { ca, err := caSecret.Fetch(ctx) if err != nil { return nil, fmt.Errorf("unable to read CA cert: %w", err) } if !updateRootCA(tlsConfig, []byte(ca)) { return nil, fmt.Errorf("unable to use specified CA cert %s", caSecret.Description()) } } if len(cfg.ServerName) > 0 { tlsConfig.ServerName = cfg.ServerName } // If a client cert & key is provided then configure TLS config accordingly. if cfg.usingClientCert() && cfg.usingClientKey() { // Verify that client cert and key are valid. if _, err := cfg.getClientCertificate(ctx, opts.secretManager); err != nil { return nil, err } tlsConfig.GetClientCertificate = func(cri *tls.CertificateRequestInfo) (*tls.Certificate, error) { var ctx context.Context if cri != nil { ctx = cri.Context() } return cfg.getClientCertificate(ctx, opts.secretManager) } } return tlsConfig, nil } // TLSConfig configures the options for TLS connections. type TLSConfig struct { // Text of the CA cert to use for the targets. CA string `yaml:"ca,omitempty" json:"ca,omitempty"` // Text of the client cert file for the targets. Cert string `yaml:"cert,omitempty" json:"cert,omitempty"` // Text of the client key file for the targets. Key Secret `yaml:"key,omitempty" json:"key,omitempty"` // The CA cert to use for the targets. CAFile string `yaml:"ca_file,omitempty" json:"ca_file,omitempty"` // The client cert file for the targets. CertFile string `yaml:"cert_file,omitempty" json:"cert_file,omitempty"` // The client key file for the targets. KeyFile string `yaml:"key_file,omitempty" json:"key_file,omitempty"` // CARef is the name of the secret within the secret manager to use as the CA cert for the // targets. CARef string `yaml:"ca_ref,omitempty" json:"ca_ref,omitempty"` // CertRef is the name of the secret within the secret manager to use as the client cert for // the targets. CertRef string `yaml:"cert_ref,omitempty" json:"cert_ref,omitempty"` // KeyRef is the name of the secret within the secret manager to use as the client key for // the targets. KeyRef string `yaml:"key_ref,omitempty" json:"key_ref,omitempty"` // Used to verify the hostname for the targets. ServerName string `yaml:"server_name,omitempty" json:"server_name,omitempty"` // Disable target certificate validation. InsecureSkipVerify bool `yaml:"insecure_skip_verify" json:"insecure_skip_verify"` // Minimum TLS version. MinVersion TLSVersion `yaml:"min_version,omitempty" json:"min_version,omitempty"` // Maximum TLS version. MaxVersion TLSVersion `yaml:"max_version,omitempty" json:"max_version,omitempty"` } // SetDirectory joins any relative file paths with dir. func (c *TLSConfig) SetDirectory(dir string) { if c == nil { return } c.CAFile = JoinDir(dir, c.CAFile) c.CertFile = JoinDir(dir, c.CertFile) c.KeyFile = JoinDir(dir, c.KeyFile) } // UnmarshalYAML implements the yaml.Unmarshaler interface. func (c *TLSConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { type plain TLSConfig if err := unmarshal((*plain)(c)); err != nil { return err } return c.Validate() } // Validate validates the TLSConfig to check that only one of the inlined or // file-based fields for the TLS CA, client certificate, and client key are // used. func (c *TLSConfig) Validate() error { if nonZeroCount(len(c.CA) > 0, len(c.CAFile) > 0, len(c.CARef) > 0) > 1 { return fmt.Errorf("at most one of ca, ca_file & ca_ref must be configured") } if nonZeroCount(len(c.Cert) > 0, len(c.CertFile) > 0, len(c.CertRef) > 0) > 1 { return fmt.Errorf("at most one of cert, cert_file & cert_ref must be configured") } if nonZeroCount(len(c.Key) > 0, len(c.KeyFile) > 0, len(c.KeyRef) > 0) > 1 { return fmt.Errorf("at most one of key and key_file must be configured") } if c.usingClientCert() && !c.usingClientKey() { return fmt.Errorf("exactly one of key or key_file must be configured when a client certificate is configured") } else if c.usingClientKey() && !c.usingClientCert() { return fmt.Errorf("exactly one of cert or cert_file must be configured when a client key is configured") } return nil } func (c *TLSConfig) usingClientCert() bool { return len(c.Cert) > 0 || len(c.CertFile) > 0 || len(c.CertRef) > 0 } func (c *TLSConfig) usingClientKey() bool { return len(c.Key) > 0 || len(c.KeyFile) > 0 || len(c.KeyRef) > 0 } func (c *TLSConfig) roundTripperSettings(secretManager SecretManager) (TLSRoundTripperSettings, error) { ca, err := toSecret(secretManager, Secret(c.CA), c.CAFile, c.CARef) if err != nil { return TLSRoundTripperSettings{}, err } cert, err := toSecret(secretManager, Secret(c.Cert), c.CertFile, c.CertRef) if err != nil { return TLSRoundTripperSettings{}, err } key, err := toSecret(secretManager, c.Key, c.KeyFile, c.KeyRef) if err != nil { return TLSRoundTripperSettings{}, err } return TLSRoundTripperSettings{ CA: ca, Cert: cert, Key: key, }, nil } // getClientCertificate reads the pair of client cert and key and returns a tls.Certificate. func (c *TLSConfig) getClientCertificate(ctx context.Context, secretManager SecretManager) (*tls.Certificate, error) { var ( certData, keyData string err error ) certSecret, err := toSecret(secretManager, Secret(c.Cert), c.CertFile, c.CertRef) if err != nil { return nil, fmt.Errorf("unable to use client cert: %w", err) } if certSecret != nil { certData, err = certSecret.Fetch(ctx) if err != nil { return nil, fmt.Errorf("unable to read specified client cert: %w", err) } } keySecret, err := toSecret(secretManager, Secret(c.Key), c.KeyFile, c.KeyRef) if err != nil { return nil, fmt.Errorf("unable to use client key: %w", err) } if keySecret != nil { keyData, err = keySecret.Fetch(ctx) if err != nil { return nil, fmt.Errorf("unable to read specified client key: %w", err) } } cert, err := tls.X509KeyPair([]byte(certData), []byte(keyData)) if err != nil { return nil, fmt.Errorf("unable to use specified client cert (%s) & key (%s): %w", certSecret.Description(), keySecret.Description(), err) } return &cert, nil } // updateRootCA parses the given byte slice as a series of PEM encoded certificates and updates tls.Config.RootCAs. func updateRootCA(cfg *tls.Config, b []byte) bool { caCertPool := x509.NewCertPool() if !caCertPool.AppendCertsFromPEM(b) { return false } cfg.RootCAs = caCertPool return true } // tlsRoundTripper is a RoundTripper that updates automatically its TLS // configuration whenever the content of the CA file changes. type tlsRoundTripper struct { settings TLSRoundTripperSettings // newRT returns a new RoundTripper. newRT func(*tls.Config) (http.RoundTripper, error) mtx sync.RWMutex rt http.RoundTripper hashCAData []byte hashCertData []byte hashKeyData []byte tlsConfig *tls.Config } type TLSRoundTripperSettings struct { CA SecretReader Cert SecretReader Key SecretReader } func NewTLSRoundTripper( cfg *tls.Config, settings TLSRoundTripperSettings, newRT func(*tls.Config) (http.RoundTripper, error), ) (http.RoundTripper, error) { return NewTLSRoundTripperWithContext(context.Background(), cfg, settings, newRT) } func NewTLSRoundTripperWithContext( ctx context.Context, cfg *tls.Config, settings TLSRoundTripperSettings, newRT func(*tls.Config) (http.RoundTripper, error), ) (http.RoundTripper, error) { t := &tlsRoundTripper{ settings: settings, newRT: newRT, tlsConfig: cfg, } rt, err := t.newRT(t.tlsConfig) if err != nil { return nil, err } t.rt = rt _, t.hashCAData, t.hashCertData, t.hashKeyData, err = t.getTLSDataWithHash(ctx) if err != nil { return nil, err } return t, nil } func (t *tlsRoundTripper) getTLSDataWithHash(ctx context.Context) ([]byte, []byte, []byte, []byte, error) { var caBytes, certBytes, keyBytes []byte if t.settings.CA != nil { ca, err := t.settings.CA.Fetch(ctx) if err != nil { return nil, nil, nil, nil, fmt.Errorf("unable to read CA cert: %w", err) } caBytes = []byte(ca) } if t.settings.Cert != nil { cert, err := t.settings.Cert.Fetch(ctx) if err != nil { return nil, nil, nil, nil, fmt.Errorf("unable to read client cert: %w", err) } certBytes = []byte(cert) } if t.settings.Key != nil { key, err := t.settings.Key.Fetch(ctx) if err != nil { return nil, nil, nil, nil, fmt.Errorf("unable to read client key: %w", err) } keyBytes = []byte(key) } var caHash, certHash, keyHash [32]byte if len(caBytes) > 0 { caHash = sha256.Sum256(caBytes) } if len(certBytes) > 0 { certHash = sha256.Sum256(certBytes) } if len(keyBytes) > 0 { keyHash = sha256.Sum256(keyBytes) } return caBytes, caHash[:], certHash[:], keyHash[:], nil } // RoundTrip implements the http.RoundTrip interface. func (t *tlsRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { caData, caHash, certHash, keyHash, err := t.getTLSDataWithHash(req.Context()) if err != nil { return nil, err } t.mtx.RLock() equal := bytes.Equal(caHash[:], t.hashCAData) && bytes.Equal(certHash[:], t.hashCertData) && bytes.Equal(keyHash[:], t.hashKeyData) rt := t.rt t.mtx.RUnlock() if equal { // The CA cert hasn't changed, use the existing RoundTripper. return rt.RoundTrip(req) } // Create a new RoundTripper. // The cert and key files are read separately by the client // using GetClientCertificate. tlsConfig := t.tlsConfig.Clone() if !updateRootCA(tlsConfig, caData) { return nil, fmt.Errorf("unable to use specified CA cert %s", t.settings.CA.Description()) } rt, err = t.newRT(tlsConfig) if err != nil { return nil, err } t.CloseIdleConnections() t.mtx.Lock() t.rt = rt t.hashCAData = caHash[:] t.hashCertData = certHash[:] t.hashKeyData = keyHash[:] t.mtx.Unlock() return rt.RoundTrip(req) } func (t *tlsRoundTripper) CloseIdleConnections() { t.mtx.RLock() defer t.mtx.RUnlock() if ci, ok := t.rt.(closeIdler); ok { ci.CloseIdleConnections() } } type userAgentRoundTripper struct { userAgent string rt http.RoundTripper } type hostRoundTripper struct { host string rt http.RoundTripper } // NewUserAgentRoundTripper adds the user agent every request header. func NewUserAgentRoundTripper(userAgent string, rt http.RoundTripper) http.RoundTripper { return &userAgentRoundTripper{userAgent, rt} } // NewHostRoundTripper sets the [http.Request.Host] of every request. func NewHostRoundTripper(host string, rt http.RoundTripper) http.RoundTripper { return &hostRoundTripper{host, rt} } func (rt *userAgentRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { req = cloneRequest(req) req.Header.Set("User-Agent", rt.userAgent) return rt.rt.RoundTrip(req) } func (rt *userAgentRoundTripper) CloseIdleConnections() { if ci, ok := rt.rt.(closeIdler); ok { ci.CloseIdleConnections() } } func (rt *hostRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { req = cloneRequest(req) req.Host = rt.host req.Header.Set("Host", rt.host) return rt.rt.RoundTrip(req) } func (rt *hostRoundTripper) CloseIdleConnections() { if ci, ok := rt.rt.(closeIdler); ok { ci.CloseIdleConnections() } } func (c HTTPClientConfig) String() string { b, err := yaml.Marshal(c) if err != nil { return fmt.Sprintf("", err) } return string(b) } type ProxyConfig struct { // HTTP proxy server to use to connect to the targets. ProxyURL URL `yaml:"proxy_url,omitempty" json:"proxy_url,omitempty"` // NoProxy contains addresses that should not use a proxy. NoProxy string `yaml:"no_proxy,omitempty" json:"no_proxy,omitempty"` // ProxyFromEnvironment makes use of net/http ProxyFromEnvironment function // to determine proxies. ProxyFromEnvironment bool `yaml:"proxy_from_environment,omitempty" json:"proxy_from_environment,omitempty"` // ProxyConnectHeader optionally specifies headers to send to // proxies during CONNECT requests. Assume that at least _some_ of // these headers are going to contain secrets and use Secret as the // value type instead of string. ProxyConnectHeader ProxyHeader `yaml:"proxy_connect_header,omitempty" json:"proxy_connect_header,omitempty"` proxyFunc func(*http.Request) (*url.URL, error) } // UnmarshalYAML implements the yaml.Unmarshaler interface. func (c *ProxyConfig) Validate() error { if len(c.ProxyConnectHeader) > 0 && (!c.ProxyFromEnvironment && (c.ProxyURL.URL == nil || c.ProxyURL.String() == "")) { return fmt.Errorf("if proxy_connect_header is configured, proxy_url or proxy_from_environment must also be configured") } if c.ProxyFromEnvironment && c.ProxyURL.URL != nil && c.ProxyURL.String() != "" { return fmt.Errorf("if proxy_from_environment is configured, proxy_url must not be configured") } if c.ProxyFromEnvironment && c.NoProxy != "" { return fmt.Errorf("if proxy_from_environment is configured, no_proxy must not be configured") } if c.ProxyURL.URL == nil && c.NoProxy != "" { return fmt.Errorf("if no_proxy is configured, proxy_url must also be configured") } return nil } // Proxy returns the Proxy URL for a request. func (c *ProxyConfig) Proxy() (fn func(*http.Request) (*url.URL, error)) { if c == nil { return nil } defer func() { fn = c.proxyFunc }() if c.proxyFunc != nil { return } if c.ProxyFromEnvironment { proxyFn := httpproxy.FromEnvironment().ProxyFunc() c.proxyFunc = func(req *http.Request) (*url.URL, error) { return proxyFn(req.URL) } return } if c.ProxyURL.URL != nil && c.ProxyURL.URL.String() != "" { if c.NoProxy == "" { c.proxyFunc = http.ProxyURL(c.ProxyURL.URL) return } proxy := &httpproxy.Config{ HTTPProxy: c.ProxyURL.String(), HTTPSProxy: c.ProxyURL.String(), NoProxy: c.NoProxy, } proxyFn := proxy.ProxyFunc() c.proxyFunc = func(req *http.Request) (*url.URL, error) { return proxyFn(req.URL) } } return } // ProxyConnectHeader() return the Proxy Connext Headers. func (c *ProxyConfig) GetProxyConnectHeader() http.Header { return c.ProxyConnectHeader.HTTPHeader() } golang-github-prometheus-common-0.55.0/config/http_config_test.go000066400000000000000000002057301463701437000251620ustar00rootroot00000000000000// Copyright 2015 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package config import ( "context" "crypto/tls" "crypto/x509" "encoding/json" "errors" "fmt" "io" "net" "net/http" "net/http/httptest" "net/url" "os" "path/filepath" "reflect" "strconv" "strings" "sync" "sync/atomic" "testing" "time" "github.com/stretchr/testify/require" "gopkg.in/yaml.v2" ) const ( TLSCAChainPath = "testdata/tls-ca-chain.pem" ServerCertificatePath = "testdata/server.crt" ServerKeyPath = "testdata/server.key" ClientCertificatePath = "testdata/client.crt" ClientKeyNoPassPath = "testdata/client.key" InvalidCA = "testdata/client.key" WrongClientCertPath = "testdata/self-signed-client.crt" WrongClientKeyPath = "testdata/self-signed-client.key" EmptyFile = "testdata/empty" MissingCA = "missing/ca.crt" MissingCert = "missing/cert.crt" MissingKey = "missing/secret.key" ExpectedMessage = "I'm here to serve you!!!" ExpectedError = "expected error" AuthorizationCredentials = "theanswertothegreatquestionoflifetheuniverseandeverythingisfortytwo" AuthorizationCredentialsFile = "testdata/bearer.token" AuthorizationType = "APIKEY" BearerToken = AuthorizationCredentials BearerTokenFile = AuthorizationCredentialsFile MissingBearerTokenFile = "missing/bearer.token" ExpectedBearer = "Bearer " + BearerToken ExpectedAuthenticationCredentials = AuthorizationType + " " + BearerToken ExpectedUsername = "arthurdent" ExpectedPassword = "42" ExpectedAccessToken = "12345" ) var invalidHTTPClientConfigs = []struct { httpClientConfigFile string errMsg string }{ { httpClientConfigFile: "testdata/http.conf.bearer-token-and-file-set.bad.yml", errMsg: "at most one of bearer_token & bearer_token_file must be configured", }, { httpClientConfigFile: "testdata/http.conf.empty.bad.yml", errMsg: "at most one of basic_auth, oauth2, bearer_token & bearer_token_file must be configured", }, { httpClientConfigFile: "testdata/http.conf.basic-auth.too-much.bad.yaml", errMsg: "at most one of basic_auth password, password_file & password_ref must be configured", }, { httpClientConfigFile: "testdata/http.conf.basic-auth.bad-username.yaml", errMsg: "at most one of basic_auth username, username_file & username_ref must be configured", }, { httpClientConfigFile: "testdata/http.conf.mix-bearer-and-creds.bad.yaml", errMsg: "authorization is not compatible with bearer_token & bearer_token_file", }, { httpClientConfigFile: "testdata/http.conf.auth-creds-and-file-set.too-much.bad.yaml", errMsg: "at most one of authorization credentials & credentials_file must be configured", }, { httpClientConfigFile: "testdata/http.conf.basic-auth-and-auth-creds.too-much.bad.yaml", errMsg: "at most one of basic_auth, oauth2 & authorization must be configured", }, { httpClientConfigFile: "testdata/http.conf.basic-auth-and-oauth2.too-much.bad.yaml", errMsg: "at most one of basic_auth, oauth2 & authorization must be configured", }, { httpClientConfigFile: "testdata/http.conf.auth-creds-no-basic.bad.yaml", errMsg: `authorization type cannot be set to "basic", use "basic_auth" instead`, }, { httpClientConfigFile: "testdata/http.conf.oauth2-secret-and-file-set.bad.yml", errMsg: "at most one of oauth2 client_secret, client_secret_file & client_secret_ref must be configured", }, { httpClientConfigFile: "testdata/http.conf.oauth2-no-client-id.bad.yaml", errMsg: "oauth2 client_id must be configured", }, { httpClientConfigFile: "testdata/http.conf.oauth2-no-token-url.bad.yaml", errMsg: "oauth2 token_url must be configured", }, { httpClientConfigFile: "testdata/http.conf.proxy-from-env.bad.yaml", errMsg: "if proxy_from_environment is configured, proxy_url must not be configured", }, { httpClientConfigFile: "testdata/http.conf.no-proxy.bad.yaml", errMsg: "if proxy_from_environment is configured, no_proxy must not be configured", }, { httpClientConfigFile: "testdata/http.conf.no-proxy-without-proxy-url.bad.yaml", errMsg: "if no_proxy is configured, proxy_url must also be configured", }, { httpClientConfigFile: "testdata/http.conf.headers-reserved.bad.yaml", errMsg: `setting header "User-Agent" is not allowed`, }, } func newTestServer(handler func(w http.ResponseWriter, r *http.Request)) (*httptest.Server, error) { testServer := httptest.NewUnstartedServer(http.HandlerFunc(handler)) tlsCAChain, err := os.ReadFile(TLSCAChainPath) if err != nil { return nil, fmt.Errorf("Can't read %s", TLSCAChainPath) } serverCertificate, err := tls.LoadX509KeyPair(ServerCertificatePath, ServerKeyPath) if err != nil { return nil, fmt.Errorf("Can't load X509 key pair %s - %s", ServerCertificatePath, ServerKeyPath) } rootCAs := x509.NewCertPool() rootCAs.AppendCertsFromPEM(tlsCAChain) testServer.TLS = &tls.Config{ Certificates: make([]tls.Certificate, 1), RootCAs: rootCAs, ClientAuth: tls.RequireAndVerifyClientCert, ClientCAs: rootCAs, } testServer.TLS.Certificates[0] = serverCertificate testServer.StartTLS() return testServer, nil } func TestNewClientFromConfig(t *testing.T) { newClientValidConfig := []struct { clientConfig HTTPClientConfig handler func(w http.ResponseWriter, r *http.Request) }{ { clientConfig: HTTPClientConfig{ TLSConfig: TLSConfig{ CAFile: "", CertFile: ClientCertificatePath, KeyFile: ClientKeyNoPassPath, ServerName: "", InsecureSkipVerify: true, }, }, handler: func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, ExpectedMessage) }, }, { clientConfig: HTTPClientConfig{ TLSConfig: TLSConfig{ CAFile: TLSCAChainPath, CertFile: ClientCertificatePath, KeyFile: ClientKeyNoPassPath, ServerName: "", InsecureSkipVerify: false, }, }, handler: func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, ExpectedMessage) }, }, { clientConfig: HTTPClientConfig{ BearerToken: BearerToken, TLSConfig: TLSConfig{ CAFile: TLSCAChainPath, CertFile: ClientCertificatePath, KeyFile: ClientKeyNoPassPath, ServerName: "", InsecureSkipVerify: false, }, }, handler: func(w http.ResponseWriter, r *http.Request) { bearer := r.Header.Get("Authorization") if bearer != ExpectedBearer { fmt.Fprintf(w, "The expected Bearer Authorization (%s) differs from the obtained Bearer Authorization (%s)", ExpectedBearer, bearer) } else { fmt.Fprint(w, ExpectedMessage) } }, }, { clientConfig: HTTPClientConfig{ BearerTokenFile: BearerTokenFile, TLSConfig: TLSConfig{ CAFile: TLSCAChainPath, CertFile: ClientCertificatePath, KeyFile: ClientKeyNoPassPath, ServerName: "", InsecureSkipVerify: false, }, }, handler: func(w http.ResponseWriter, r *http.Request) { bearer := r.Header.Get("Authorization") if bearer != ExpectedBearer { fmt.Fprintf(w, "The expected Bearer Authorization (%s) differs from the obtained Bearer Authorization (%s)", ExpectedBearer, bearer) } else { fmt.Fprint(w, ExpectedMessage) } }, }, { clientConfig: HTTPClientConfig{ Authorization: &Authorization{Credentials: BearerToken}, TLSConfig: TLSConfig{ CAFile: TLSCAChainPath, CertFile: ClientCertificatePath, KeyFile: ClientKeyNoPassPath, ServerName: "", InsecureSkipVerify: false, }, }, handler: func(w http.ResponseWriter, r *http.Request) { bearer := r.Header.Get("Authorization") if bearer != ExpectedBearer { fmt.Fprintf(w, "The expected Bearer Authorization (%s) differs from the obtained Bearer Authorization (%s)", ExpectedBearer, bearer) } else { fmt.Fprint(w, ExpectedMessage) } }, }, { clientConfig: HTTPClientConfig{ Authorization: &Authorization{CredentialsFile: AuthorizationCredentialsFile, Type: AuthorizationType}, TLSConfig: TLSConfig{ CAFile: TLSCAChainPath, CertFile: ClientCertificatePath, KeyFile: ClientKeyNoPassPath, ServerName: "", InsecureSkipVerify: false, }, }, handler: func(w http.ResponseWriter, r *http.Request) { bearer := r.Header.Get("Authorization") if bearer != ExpectedAuthenticationCredentials { fmt.Fprintf(w, "The expected Bearer Authorization (%s) differs from the obtained Bearer Authorization (%s)", ExpectedAuthenticationCredentials, bearer) } else { fmt.Fprint(w, ExpectedMessage) } }, }, { clientConfig: HTTPClientConfig{ Authorization: &Authorization{ Type: AuthorizationType, }, TLSConfig: TLSConfig{ CAFile: TLSCAChainPath, CertFile: ClientCertificatePath, KeyFile: ClientKeyNoPassPath, ServerName: "", InsecureSkipVerify: false, }, }, handler: func(w http.ResponseWriter, r *http.Request) { bearer := r.Header.Get("Authorization") if strings.TrimSpace(bearer) != AuthorizationType { fmt.Fprintf(w, "The expected Bearer Authorization (%s) differs from the obtained Bearer Authorization (%s)", AuthorizationType, bearer) } else { fmt.Fprint(w, ExpectedMessage) } }, }, { clientConfig: HTTPClientConfig{ Authorization: &Authorization{ Credentials: AuthorizationCredentials, Type: AuthorizationType, }, TLSConfig: TLSConfig{ CAFile: TLSCAChainPath, CertFile: ClientCertificatePath, KeyFile: ClientKeyNoPassPath, ServerName: "", InsecureSkipVerify: false, }, }, handler: func(w http.ResponseWriter, r *http.Request) { bearer := r.Header.Get("Authorization") if bearer != ExpectedAuthenticationCredentials { fmt.Fprintf(w, "The expected Bearer Authorization (%s) differs from the obtained Bearer Authorization (%s)", ExpectedAuthenticationCredentials, bearer) } else { fmt.Fprint(w, ExpectedMessage) } }, }, { clientConfig: HTTPClientConfig{ Authorization: &Authorization{ CredentialsFile: BearerTokenFile, }, TLSConfig: TLSConfig{ CAFile: TLSCAChainPath, CertFile: ClientCertificatePath, KeyFile: ClientKeyNoPassPath, ServerName: "", InsecureSkipVerify: false, }, }, handler: func(w http.ResponseWriter, r *http.Request) { bearer := r.Header.Get("Authorization") if bearer != ExpectedBearer { fmt.Fprintf(w, "The expected Bearer Authorization (%s) differs from the obtained Bearer Authorization (%s)", ExpectedBearer, bearer) } else { fmt.Fprint(w, ExpectedMessage) } }, }, { clientConfig: HTTPClientConfig{ BasicAuth: &BasicAuth{ Username: ExpectedUsername, Password: ExpectedPassword, }, TLSConfig: TLSConfig{ CAFile: TLSCAChainPath, CertFile: ClientCertificatePath, KeyFile: ClientKeyNoPassPath, ServerName: "", InsecureSkipVerify: false, }, }, handler: func(w http.ResponseWriter, r *http.Request) { username, password, ok := r.BasicAuth() if !ok { fmt.Fprintf(w, "The Authorization header wasn't set") } else if ExpectedUsername != username { fmt.Fprintf(w, "The expected username (%s) differs from the obtained username (%s).", ExpectedUsername, username) } else if ExpectedPassword != password { fmt.Fprintf(w, "The expected password (%s) differs from the obtained password (%s).", ExpectedPassword, password) } else { fmt.Fprint(w, ExpectedMessage) } }, }, { clientConfig: HTTPClientConfig{ FollowRedirects: true, TLSConfig: TLSConfig{ CAFile: TLSCAChainPath, CertFile: ClientCertificatePath, KeyFile: ClientKeyNoPassPath, ServerName: "", InsecureSkipVerify: false, }, }, handler: func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/redirected": fmt.Fprint(w, ExpectedMessage) default: w.Header().Set("Location", "/redirected") w.WriteHeader(http.StatusFound) fmt.Fprint(w, "It should follow the redirect.") } }, }, { clientConfig: HTTPClientConfig{ FollowRedirects: false, TLSConfig: TLSConfig{ CAFile: TLSCAChainPath, CertFile: ClientCertificatePath, KeyFile: ClientKeyNoPassPath, ServerName: "", InsecureSkipVerify: false, }, }, handler: func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/redirected": fmt.Fprint(w, "The redirection was followed.") default: w.Header().Set("Location", "/redirected") w.WriteHeader(http.StatusFound) fmt.Fprint(w, ExpectedMessage) } }, }, { clientConfig: HTTPClientConfig{ OAuth2: &OAuth2{ ClientID: "ExpectedUsername", TLSConfig: TLSConfig{ CAFile: TLSCAChainPath, CertFile: ClientCertificatePath, KeyFile: ClientKeyNoPassPath, ServerName: "", InsecureSkipVerify: false, }, }, TLSConfig: TLSConfig{ CAFile: TLSCAChainPath, CertFile: ClientCertificatePath, KeyFile: ClientKeyNoPassPath, ServerName: "", InsecureSkipVerify: false, }, }, handler: func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/token": res, _ := json.Marshal(oauth2TestServerResponse{ AccessToken: ExpectedAccessToken, TokenType: "Bearer", }) w.Header().Add("Content-Type", "application/json") _, _ = w.Write(res) default: authorization := r.Header.Get("Authorization") if authorization != "Bearer "+ExpectedAccessToken { fmt.Fprintf(w, "Expected Authorization header %q, got %q", "Bearer "+ExpectedAccessToken, authorization) } else { fmt.Fprint(w, ExpectedMessage) } } }, }, { clientConfig: HTTPClientConfig{ OAuth2: &OAuth2{ ClientID: "ExpectedUsername", ClientSecret: "ExpectedPassword", TLSConfig: TLSConfig{ CAFile: TLSCAChainPath, CertFile: ClientCertificatePath, KeyFile: ClientKeyNoPassPath, ServerName: "", InsecureSkipVerify: false, }, }, TLSConfig: TLSConfig{ CAFile: TLSCAChainPath, CertFile: ClientCertificatePath, KeyFile: ClientKeyNoPassPath, ServerName: "", InsecureSkipVerify: false, }, }, handler: func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/token": res, _ := json.Marshal(oauth2TestServerResponse{ AccessToken: ExpectedAccessToken, TokenType: "Bearer", }) w.Header().Add("Content-Type", "application/json") _, _ = w.Write(res) default: authorization := r.Header.Get("Authorization") if authorization != "Bearer "+ExpectedAccessToken { fmt.Fprintf(w, "Expected Authorization header %q, got %q", "Bearer "+ExpectedAccessToken, authorization) } else { fmt.Fprint(w, ExpectedMessage) } } }, }, } for _, validConfig := range newClientValidConfig { t.Run("", func(t *testing.T) { testServer, err := newTestServer(validConfig.handler) if err != nil { t.Fatal(err.Error()) } defer testServer.Close() if validConfig.clientConfig.OAuth2 != nil { // We don't have access to the test server's URL when configuring the test cases, // so it has to be specified here. validConfig.clientConfig.OAuth2.TokenURL = testServer.URL + "/token" } err = validConfig.clientConfig.Validate() if err != nil { t.Fatal(err.Error()) } client, err := NewClientFromConfig(validConfig.clientConfig, "test") if err != nil { t.Errorf("Can't create a client from this config: %+v", validConfig.clientConfig) return } response, err := client.Get(testServer.URL) if err != nil { t.Errorf("Can't connect to the test server using this config: %+v: %v", validConfig.clientConfig, err) return } message, err := io.ReadAll(response.Body) response.Body.Close() if err != nil { t.Errorf("Can't read the server response body using this config: %+v", validConfig.clientConfig) return } trimMessage := strings.TrimSpace(string(message)) if ExpectedMessage != trimMessage { t.Errorf("The expected message (%s) differs from the obtained message (%s) using this config: %+v", ExpectedMessage, trimMessage, validConfig.clientConfig) } }) } } func TestProxyConfiguration(t *testing.T) { testcases := map[string]struct { testFn string loader func(string) (*HTTPClientConfig, []byte, error) isValid bool }{ "good yaml": { testFn: "testdata/http.conf.proxy-headers.good.yml", loader: LoadHTTPConfigFile, isValid: true, }, "bad yaml": { testFn: "testdata/http.conf.proxy-headers.bad.yml", loader: LoadHTTPConfigFile, isValid: false, }, "good json": { testFn: "testdata/http.conf.proxy-headers.good.json", loader: loadHTTPConfigJSONFile, isValid: true, }, "bad json": { testFn: "testdata/http.conf.proxy-headers.bad.json", loader: loadHTTPConfigJSONFile, isValid: false, }, } for name, tc := range testcases { t.Run(name, func(t *testing.T) { _, _, err := tc.loader(tc.testFn) if tc.isValid { if err != nil { t.Fatalf("Error validating %s: %s", tc.testFn, err) } } else { if err == nil { t.Fatalf("Expecting error validating %s but got %s", tc.testFn, err) } } }) } } func TestNewClientFromInvalidConfig(t *testing.T) { newClientInvalidConfig := []struct { clientConfig HTTPClientConfig errorMsg string }{ { clientConfig: HTTPClientConfig{ TLSConfig: TLSConfig{ CAFile: MissingCA, InsecureSkipVerify: true, }, }, errorMsg: fmt.Sprintf("unable to read CA cert: unable to read file %s", MissingCA), }, { clientConfig: HTTPClientConfig{ TLSConfig: TLSConfig{ CAFile: InvalidCA, InsecureSkipVerify: true, }, }, errorMsg: fmt.Sprintf("unable to use specified CA cert file %s", InvalidCA), }, } for _, invalidConfig := range newClientInvalidConfig { client, err := NewClientFromConfig(invalidConfig.clientConfig, "test") if client != nil { t.Errorf("A client instance was returned instead of nil using this config: %+v", invalidConfig.clientConfig) } if err == nil { t.Errorf("No error was returned using this config: %+v", invalidConfig.clientConfig) } if !strings.Contains(err.Error(), invalidConfig.errorMsg) { t.Errorf("Expected error %q does not contain %q", err.Error(), invalidConfig.errorMsg) } } } func TestCustomDialContextFunc(t *testing.T) { dialFn := func(_ context.Context, _, _ string) (net.Conn, error) { return nil, errors.New(ExpectedError) } cfg := HTTPClientConfig{} client, err := NewClientFromConfig(cfg, "test", WithDialContextFunc(dialFn)) if err != nil { t.Fatalf("Can't create a client from this config: %+v", cfg) } _, err = client.Get("http://localhost") if err == nil || !strings.Contains(err.Error(), ExpectedError) { t.Errorf("Expected error %q but got %q", ExpectedError, err) } } func TestCustomIdleConnTimeout(t *testing.T) { timeout := time.Second * 5 cfg := HTTPClientConfig{} rt, err := NewRoundTripperFromConfig(cfg, "test", WithIdleConnTimeout(timeout)) if err != nil { t.Fatalf("Can't create a round-tripper from this config: %+v", cfg) } transport, ok := rt.(*http.Transport) if !ok { t.Fatalf("Unexpected transport: %+v", transport) } if transport.IdleConnTimeout != timeout { t.Fatalf("Unexpected idle connection timeout: %+v", timeout) } } func TestMissingBearerAuthFile(t *testing.T) { cfg := HTTPClientConfig{ BearerTokenFile: MissingBearerTokenFile, TLSConfig: TLSConfig{ CAFile: TLSCAChainPath, CertFile: ClientCertificatePath, KeyFile: ClientKeyNoPassPath, ServerName: "", InsecureSkipVerify: false, }, } handler := func(w http.ResponseWriter, r *http.Request) { bearer := r.Header.Get("Authorization") if bearer != ExpectedBearer { fmt.Fprintf(w, "The expected Bearer Authorization (%s) differs from the obtained Bearer Authorization (%s)", ExpectedBearer, bearer) } else { fmt.Fprint(w, ExpectedMessage) } } testServer, err := newTestServer(handler) if err != nil { t.Fatal(err.Error()) } defer testServer.Close() client, err := NewClientFromConfig(cfg, "test") if err != nil { t.Fatal(err) } _, err = client.Get(testServer.URL) if err == nil { t.Fatal("No error is returned here") } if !strings.Contains(err.Error(), "unable to read authorization credentials: unable to read file missing/bearer.token: open missing/bearer.token: no such file or directory") { t.Fatal("wrong error message being returned") } } func TestBearerAuthRoundTripper(t *testing.T) { const ( newBearerToken = "goodbyeandthankyouforthefish" ) fakeRoundTripper := NewRoundTripCheckRequest(func(req *http.Request) { bearer := req.Header.Get("Authorization") if bearer != ExpectedBearer { t.Errorf("The expected Bearer Authorization (%s) differs from the obtained Bearer Authorization (%s)", ExpectedBearer, bearer) } }, nil, nil) // Normal flow. bearerAuthRoundTripper := NewAuthorizationCredentialsRoundTripper("Bearer", NewInlineSecret(BearerToken), fakeRoundTripper) request, _ := http.NewRequest("GET", "/hitchhiker", nil) request.Header.Set("User-Agent", "Douglas Adams mind") _, err := bearerAuthRoundTripper.RoundTrip(request) if err != nil { t.Errorf("unexpected error while executing RoundTrip: %s", err.Error()) } // Should honor already Authorization header set. bearerAuthRoundTripperShouldNotModifyExistingAuthorization := NewAuthorizationCredentialsRoundTripper("Bearer", NewInlineSecret(newBearerToken), fakeRoundTripper) request, _ = http.NewRequest("GET", "/hitchhiker", nil) request.Header.Set("Authorization", ExpectedBearer) _, err = bearerAuthRoundTripperShouldNotModifyExistingAuthorization.RoundTrip(request) if err != nil { t.Errorf("unexpected error while executing RoundTrip: %s", err.Error()) } } func TestBearerAuthFileRoundTripper(t *testing.T) { fakeRoundTripper := NewRoundTripCheckRequest(func(req *http.Request) { bearer := req.Header.Get("Authorization") if bearer != ExpectedBearer { t.Errorf("The expected Bearer Authorization (%s) differs from the obtained Bearer Authorization (%s)", ExpectedBearer, bearer) } }, nil, nil) // Normal flow. bearerAuthRoundTripper := NewAuthorizationCredentialsRoundTripper("Bearer", &FileSecret{file: BearerTokenFile}, fakeRoundTripper) request, _ := http.NewRequest("GET", "/hitchhiker", nil) request.Header.Set("User-Agent", "Douglas Adams mind") _, err := bearerAuthRoundTripper.RoundTrip(request) if err != nil { t.Errorf("unexpected error while executing RoundTrip: %s", err.Error()) } // Should honor already Authorization header set. bearerAuthRoundTripperShouldNotModifyExistingAuthorization := NewAuthorizationCredentialsRoundTripper("Bearer", &FileSecret{file: MissingBearerTokenFile}, fakeRoundTripper) request, _ = http.NewRequest("GET", "/hitchhiker", nil) request.Header.Set("Authorization", ExpectedBearer) _, err = bearerAuthRoundTripperShouldNotModifyExistingAuthorization.RoundTrip(request) if err != nil { t.Errorf("unexpected error while executing RoundTrip: %s", err.Error()) } } func TestTLSConfig(t *testing.T) { configTLSConfig := TLSConfig{ CAFile: TLSCAChainPath, CertFile: ClientCertificatePath, KeyFile: ClientKeyNoPassPath, ServerName: "localhost", InsecureSkipVerify: false, } tlsCAChain, err := os.ReadFile(TLSCAChainPath) if err != nil { t.Fatalf("Can't read the CA certificate chain (%s)", TLSCAChainPath) } rootCAs := x509.NewCertPool() rootCAs.AppendCertsFromPEM(tlsCAChain) expectedTLSConfig := &tls.Config{ RootCAs: rootCAs, ServerName: configTLSConfig.ServerName, InsecureSkipVerify: configTLSConfig.InsecureSkipVerify, } tlsConfig, err := NewTLSConfig(&configTLSConfig) if err != nil { t.Fatalf("Can't create a new TLS Config from a configuration (%s).", err) } clientCertificate, err := tls.LoadX509KeyPair(ClientCertificatePath, ClientKeyNoPassPath) if err != nil { t.Fatalf("Can't load the client key pair ('%s' and '%s'). Reason: %s", ClientCertificatePath, ClientKeyNoPassPath, err) } cert, err := tlsConfig.GetClientCertificate(nil) if err != nil { t.Fatalf("unexpected error returned by tlsConfig.GetClientCertificate(): %s", err) } if !reflect.DeepEqual(cert, &clientCertificate) { t.Fatalf("Unexpected client certificate result: \n\n%+v\n expected\n\n%+v", cert, clientCertificate) } // tlsConfig.rootCAs.LazyCerts contains functions getCert() in go 1.16, which are // never equal. Compare the Subjects instead. //nolint:staticcheck // Ignore SA1019. (*CertPool).Subjects is deprecated because it may not include the system certs but it isn't the case here. if !reflect.DeepEqual(tlsConfig.RootCAs.Subjects(), expectedTLSConfig.RootCAs.Subjects()) { t.Fatalf("Unexpected RootCAs result: \n\n%+v\n expected\n\n%+v", tlsConfig.RootCAs.Subjects(), expectedTLSConfig.RootCAs.Subjects()) } tlsConfig.RootCAs = nil expectedTLSConfig.RootCAs = nil // Non-nil functions are never equal. tlsConfig.GetClientCertificate = nil if !reflect.DeepEqual(tlsConfig, expectedTLSConfig) { t.Fatalf("Unexpected TLS Config result: \n\n%+v\n expected\n\n%+v", tlsConfig, expectedTLSConfig) } } func TestTLSConfigEmpty(t *testing.T) { configTLSConfig := TLSConfig{ InsecureSkipVerify: true, } expectedTLSConfig := &tls.Config{ InsecureSkipVerify: configTLSConfig.InsecureSkipVerify, } tlsConfig, err := NewTLSConfig(&configTLSConfig) if err != nil { t.Fatalf("Can't create a new TLS Config from a configuration (%s).", err) } if !reflect.DeepEqual(tlsConfig, expectedTLSConfig) { t.Fatalf("Unexpected TLS Config result: \n\n%+v\n expected\n\n%+v", tlsConfig, expectedTLSConfig) } } func TestTLSConfigInvalidCA(t *testing.T) { invalidTLSConfig := []struct { configTLSConfig TLSConfig errorMessage string }{ { configTLSConfig: TLSConfig{ CAFile: MissingCA, CertFile: "", KeyFile: "", ServerName: "", InsecureSkipVerify: false, }, errorMessage: fmt.Sprintf("unable to read CA cert: unable to read file %s", MissingCA), }, { configTLSConfig: TLSConfig{ CAFile: "", CertFile: MissingCert, KeyFile: ClientKeyNoPassPath, ServerName: "", InsecureSkipVerify: false, }, errorMessage: fmt.Sprintf("unable to read specified client cert: unable to read file %s", MissingCert), }, { configTLSConfig: TLSConfig{ CAFile: "", CertFile: ClientCertificatePath, KeyFile: MissingKey, ServerName: "", InsecureSkipVerify: false, }, errorMessage: fmt.Sprintf("unable to read specified client key: unable to read file %s", MissingKey), }, { configTLSConfig: TLSConfig{ CAFile: "", Cert: readFile(t, ClientCertificatePath), CertFile: ClientCertificatePath, KeyFile: ClientKeyNoPassPath, ServerName: "", InsecureSkipVerify: false, }, errorMessage: "at most one of cert, cert_file & cert_ref must be configured", }, { configTLSConfig: TLSConfig{ CAFile: "", CertFile: ClientCertificatePath, Key: Secret(readFile(t, ClientKeyNoPassPath)), KeyFile: ClientKeyNoPassPath, ServerName: "", InsecureSkipVerify: false, }, errorMessage: "at most one of key and key_file must be configured", }, } for _, anInvalididTLSConfig := range invalidTLSConfig { tlsConfig, err := NewTLSConfig(&anInvalididTLSConfig.configTLSConfig) if tlsConfig != nil && err == nil { t.Errorf("The TLS Config could be created even with this %+v", anInvalididTLSConfig.configTLSConfig) continue } if !strings.Contains(err.Error(), anInvalididTLSConfig.errorMessage) { t.Errorf("The expected error should contain %s, but got %s", anInvalididTLSConfig.errorMessage, err) } } } func TestBasicAuthNoPassword(t *testing.T) { cfg, _, err := LoadHTTPConfigFile("testdata/http.conf.basic-auth.no-password.yaml") if err != nil { t.Fatalf("Error loading HTTP client config: %v", err) } client, err := NewClientFromConfig(*cfg, "test") if err != nil { t.Fatalf("Error creating HTTP Client: %v", err) } rt, ok := client.Transport.(*basicAuthRoundTripper) if !ok { t.Fatalf("Error casting to basic auth transport, %v", client.Transport) } if username, _ := rt.username.Fetch(context.Background()); username != "user" { t.Errorf("Bad HTTP client username: %s", username) } if rt.password != nil { t.Errorf("Expected empty HTTP client password") } } func TestBasicAuthNoUsername(t *testing.T) { cfg, _, err := LoadHTTPConfigFile("testdata/http.conf.basic-auth.no-username.yaml") if err != nil { t.Fatalf("Error loading HTTP client config: %v", err) } client, err := NewClientFromConfig(*cfg, "test") if err != nil { t.Fatalf("Error creating HTTP Client: %v", err) } rt, ok := client.Transport.(*basicAuthRoundTripper) if !ok { t.Fatalf("Error casting to basic auth transport, %v", client.Transport) } if rt.username != nil { t.Errorf("Got unexpected username") } if password, _ := rt.password.Fetch(context.Background()); password != "secret" { t.Errorf("Unexpected HTTP client password: %s", password) } } func TestBasicAuthPasswordFile(t *testing.T) { cfg, _, err := LoadHTTPConfigFile("testdata/http.conf.basic-auth.good.yaml") if err != nil { t.Fatalf("Error loading HTTP client config: %v", err) } client, err := NewClientFromConfig(*cfg, "test") if err != nil { t.Fatalf("Error creating HTTP Client: %v", err) } rt, ok := client.Transport.(*basicAuthRoundTripper) if !ok { t.Fatalf("Error casting to basic auth transport, %v", client.Transport) } if username, _ := rt.username.Fetch(context.Background()); username != "user" { t.Errorf("Bad HTTP client username: %s", username) } if password, _ := rt.password.Fetch(context.Background()); password != "foobar" { t.Errorf("Bad HTTP client password: %s", password) } } type secretManager struct { data map[string]string } func (m *secretManager) Fetch(ctx context.Context, secretRef string) (string, error) { secretData, ok := m.data[secretRef] if !ok { return "", fmt.Errorf("unknown secret %s", secretRef) } return secretData, nil } func TestBasicAuthSecretManager(t *testing.T) { cfg, _, err := LoadHTTPConfigFile("testdata/http.conf.basic-auth.ref.yaml") if err != nil { t.Fatalf("Error loading HTTP client config: %v", err) } manager := secretManager{ data: map[string]string{ "admin": "user", "pass": "foobar", }, } client, err := NewClientFromConfig(*cfg, "test", WithSecretManager(&manager)) if err != nil { t.Fatalf("Error creating HTTP Client: %v", err) } rt, ok := client.Transport.(*basicAuthRoundTripper) if !ok { t.Fatalf("Error casting to basic auth transport, %v", client.Transport) } if username, _ := rt.username.Fetch(context.Background()); username != "user" { t.Errorf("Bad HTTP client username: %s", username) } if password, _ := rt.password.Fetch(context.Background()); password != "foobar" { t.Errorf("Bad HTTP client password: %s", password) } } func TestBasicAuthSecretManagerNotFound(t *testing.T) { cfg, _, err := LoadHTTPConfigFile("testdata/http.conf.basic-auth.ref.yaml") if err != nil { t.Fatalf("Error loading HTTP client config: %v", err) } manager := secretManager{ data: map[string]string{ "admin1": "user", "foobar": "pass", }, } client, err := NewClientFromConfig(*cfg, "test", WithSecretManager(&manager)) if err != nil { t.Fatalf("Error creating HTTP Client: %v", err) } rt, ok := client.Transport.(*basicAuthRoundTripper) if !ok { t.Fatalf("Error casting to basic auth transport, %v", client.Transport) } if _, err := rt.username.Fetch(context.Background()); !strings.Contains(err.Error(), "unknown secret admin") { t.Errorf("Unexpected error message: %s", err) } if _, err := rt.password.Fetch(context.Background()); !strings.Contains(err.Error(), "unknown secret pass") { t.Errorf("Unexpected error message: %s", err) } } func TestBasicUsernameFile(t *testing.T) { cfg, _, err := LoadHTTPConfigFile("testdata/http.conf.basic-auth.username-file.good.yaml") if err != nil { t.Fatalf("Error loading HTTP client config: %v", err) } client, err := NewClientFromConfig(*cfg, "test") if err != nil { t.Fatalf("Error creating HTTP Client: %v", err) } rt, ok := client.Transport.(*basicAuthRoundTripper) if !ok { t.Fatalf("Error casting to basic auth transport, %v", client.Transport) } if username, _ := rt.username.Fetch(context.Background()); username != "testuser" { t.Errorf("Bad HTTP client username: %s", username) } if password, _ := rt.password.Fetch(context.Background()); password != "foobar" { t.Errorf("Bad HTTP client passwordFile: %s", password) } } func getCertificateBlobs(t *testing.T) map[string][]byte { files := []string{ TLSCAChainPath, ClientCertificatePath, ClientKeyNoPassPath, ServerCertificatePath, ServerKeyPath, WrongClientCertPath, WrongClientKeyPath, EmptyFile, } bs := make(map[string][]byte, len(files)+1) for _, f := range files { b, err := os.ReadFile(f) if err != nil { t.Fatal(err) } bs[f] = b } return bs } func writeCertificate(bs map[string][]byte, src string, dst string) { b, ok := bs[src] if !ok { panic(fmt.Sprintf("Couldn't find %q in bs", src)) } if err := os.WriteFile(dst, b, 0o664); err != nil { panic(err) } } func TestTLSRoundTripper(t *testing.T) { bs := getCertificateBlobs(t) tmpDir, err := os.MkdirTemp("", "tlsroundtripper") if err != nil { t.Fatal("Failed to create tmp dir", err) } defer os.RemoveAll(tmpDir) ca, cert, key := filepath.Join(tmpDir, "ca"), filepath.Join(tmpDir, "cert"), filepath.Join(tmpDir, "key") handler := func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, ExpectedMessage) } testServer, err := newTestServer(handler) if err != nil { t.Fatal(err.Error()) } defer testServer.Close() testCases := []struct { ca string cert string key string errMsg string }{ { // Valid certs. ca: TLSCAChainPath, cert: ClientCertificatePath, key: ClientKeyNoPassPath, }, { // CA not matching. ca: ClientCertificatePath, cert: ClientCertificatePath, key: ClientKeyNoPassPath, errMsg: "certificate signed by unknown authority", }, { // Invalid client cert+key. ca: TLSCAChainPath, cert: WrongClientCertPath, key: WrongClientKeyPath, errMsg: "remote error: tls", }, { // CA file empty ca: EmptyFile, cert: ClientCertificatePath, key: ClientKeyNoPassPath, errMsg: "unable to use specified CA cert", }, { // cert file empty ca: TLSCAChainPath, cert: EmptyFile, key: ClientKeyNoPassPath, errMsg: "failed to find any PEM data in certificate input", }, { // key file empty ca: TLSCAChainPath, cert: ClientCertificatePath, key: EmptyFile, errMsg: "failed to find any PEM data in key input", }, { // Valid certs again. ca: TLSCAChainPath, cert: ClientCertificatePath, key: ClientKeyNoPassPath, }, } cfg := HTTPClientConfig{ TLSConfig: TLSConfig{ CAFile: ca, CertFile: cert, KeyFile: key, InsecureSkipVerify: false, }, } var c *http.Client for i, tc := range testCases { tc := tc t.Run(strconv.Itoa(i), func(t *testing.T) { writeCertificate(bs, tc.ca, ca) writeCertificate(bs, tc.cert, cert) writeCertificate(bs, tc.key, key) if c == nil { c, err = NewClientFromConfig(cfg, "test") if err != nil { t.Fatalf("Error creating HTTP Client: %v", err) } } req, err := http.NewRequest(http.MethodGet, testServer.URL, nil) if err != nil { t.Fatalf("Error creating HTTP request: %v", err) } r, err := c.Do(req) if len(tc.errMsg) > 0 { if err == nil { r.Body.Close() t.Fatalf("Could connect to the test server.") } if !strings.Contains(err.Error(), tc.errMsg) { t.Fatalf("Expected error message to contain %q, got %q", tc.errMsg, err) } return } if err != nil { t.Fatalf("Can't connect to the test server") } b, err := io.ReadAll(r.Body) r.Body.Close() if err != nil { t.Errorf("Can't read the server response body") } got := strings.TrimSpace(string(b)) if ExpectedMessage != got { t.Errorf("The expected message %q differs from the obtained message %q", ExpectedMessage, got) } }) } } func TestTLSRoundTripper_Inline(t *testing.T) { handler := func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, ExpectedMessage) } testServer, err := newTestServer(handler) if err != nil { t.Fatal(err.Error()) } defer testServer.Close() testCases := []struct { caText, caFile string certText, certFile string keyText, keyFile string errMsg string }{ { // File-based everything. caFile: TLSCAChainPath, certFile: ClientCertificatePath, keyFile: ClientKeyNoPassPath, }, { // Inline CA. caText: readFile(t, TLSCAChainPath), certFile: ClientCertificatePath, keyFile: ClientKeyNoPassPath, }, { // Inline cert. caFile: TLSCAChainPath, certText: readFile(t, ClientCertificatePath), keyFile: ClientKeyNoPassPath, }, { // Inline key. caFile: TLSCAChainPath, certFile: ClientCertificatePath, keyText: readFile(t, ClientKeyNoPassPath), }, { // Inline everything. caText: readFile(t, TLSCAChainPath), certText: readFile(t, ClientCertificatePath), keyText: readFile(t, ClientKeyNoPassPath), }, { // Invalid inline CA. caText: "badca", certText: readFile(t, ClientCertificatePath), keyText: readFile(t, ClientKeyNoPassPath), errMsg: "unable to use specified CA cert inline", }, { // Invalid cert. caText: readFile(t, TLSCAChainPath), certText: "badcert", keyText: readFile(t, ClientKeyNoPassPath), errMsg: "failed to find any PEM data in certificate input", }, { // Invalid key. caText: readFile(t, TLSCAChainPath), certText: readFile(t, ClientCertificatePath), keyText: "badkey", errMsg: "failed to find any PEM data in key input", }, } for i, tc := range testCases { tc := tc t.Run(strconv.Itoa(i), func(t *testing.T) { cfg := HTTPClientConfig{ TLSConfig: TLSConfig{ CA: tc.caText, CAFile: tc.caFile, Cert: tc.certText, CertFile: tc.certFile, Key: Secret(tc.keyText), KeyFile: tc.keyFile, InsecureSkipVerify: false, }, } c, err := NewClientFromConfig(cfg, "test") if tc.errMsg != "" { if !strings.Contains(err.Error(), tc.errMsg) { t.Fatalf("Expected error message to contain %q, got %q", tc.errMsg, err) } return } else if err != nil { t.Fatalf("Error creating HTTP Client: %v", err) } req, err := http.NewRequest(http.MethodGet, testServer.URL, nil) if err != nil { t.Fatalf("Error creating HTTP request: %v", err) } r, err := c.Do(req) if err != nil { t.Fatalf("Can't connect to the test server") } b, err := io.ReadAll(r.Body) r.Body.Close() if err != nil { t.Errorf("Can't read the server response body") } got := strings.TrimSpace(string(b)) if ExpectedMessage != got { t.Errorf("The expected message %q differs from the obtained message %q", ExpectedMessage, got) } }) } } func TestTLSRoundTripperRaces(t *testing.T) { bs := getCertificateBlobs(t) tmpDir, err := os.MkdirTemp("", "tlsroundtripper") if err != nil { t.Fatal("Failed to create tmp dir", err) } defer os.RemoveAll(tmpDir) ca, cert, key := filepath.Join(tmpDir, "ca"), filepath.Join(tmpDir, "cert"), filepath.Join(tmpDir, "key") handler := func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, ExpectedMessage) } testServer, err := newTestServer(handler) if err != nil { t.Fatal(err.Error()) } defer testServer.Close() cfg := HTTPClientConfig{ TLSConfig: TLSConfig{ CAFile: ca, CertFile: cert, KeyFile: key, InsecureSkipVerify: false, }, } var c *http.Client writeCertificate(bs, TLSCAChainPath, ca) writeCertificate(bs, ClientCertificatePath, cert) writeCertificate(bs, ClientKeyNoPassPath, key) c, err = NewClientFromConfig(cfg, "test") if err != nil { t.Fatalf("Error creating HTTP Client: %v", err) } var wg sync.WaitGroup ch := make(chan struct{}) var total, ok int64 // Spawn 10 Go routines polling the server concurrently. for i := 0; i < 10; i++ { wg.Add(1) go func() { defer wg.Done() for { select { case <-ch: return default: atomic.AddInt64(&total, 1) r, err := c.Get(testServer.URL) if err == nil { r.Body.Close() atomic.AddInt64(&ok, 1) } } } }() } // Change the CA file every 10ms for 1 second. wg.Add(1) go func() { defer wg.Done() i := 0 for { tick := time.NewTicker(10 * time.Millisecond) <-tick.C if i%2 == 0 { writeCertificate(bs, ClientCertificatePath, ca) } else { writeCertificate(bs, TLSCAChainPath, ca) } i++ if i > 100 { close(ch) return } } }() wg.Wait() if ok == total { t.Fatalf("Expecting some requests to fail but got %d/%d successful requests", ok, total) } } func TestHideHTTPClientConfigSecrets(t *testing.T) { c, _, err := LoadHTTPConfigFile("testdata/http.conf.good.yml") if err != nil { t.Fatalf("Error parsing %s: %s", "testdata/http.conf.good.yml", err) } // String method must not reveal authentication credentials. s := c.String() if strings.Contains(s, "mysecret") { t.Fatal("http client config's String method reveals authentication credentials.") } } func TestDefaultFollowRedirect(t *testing.T) { cfg, _, err := LoadHTTPConfigFile("testdata/http.conf.good.yml") if err != nil { t.Errorf("Error loading HTTP client config: %v", err) } if !cfg.FollowRedirects { t.Errorf("follow_redirects should be true") } } func TestValidateHTTPConfig(t *testing.T) { cfg, _, err := LoadHTTPConfigFile("testdata/http.conf.good.yml") if err != nil { t.Errorf("Error loading HTTP client config: %v", err) } err = cfg.Validate() if err != nil { t.Fatalf("Error validating %s: %s", "testdata/http.conf.good.yml", err) } } func TestInvalidHTTPConfigs(t *testing.T) { for _, ee := range invalidHTTPClientConfigs { _, _, err := LoadHTTPConfigFile(ee.httpClientConfigFile) if err == nil { t.Errorf("Expected error with config %q but got none", ee.httpClientConfigFile) continue } if !strings.Contains(err.Error(), ee.errMsg) { t.Errorf("Expected error for invalid HTTP client configuration to contain %q but got: %s", ee.errMsg, err) } } } type roundTrip struct { theResponse *http.Response theError error } func (rt *roundTrip) RoundTrip(r *http.Request) (*http.Response, error) { return rt.theResponse, rt.theError } type roundTripCheckRequest struct { checkRequest func(*http.Request) roundTrip } func (rt *roundTripCheckRequest) RoundTrip(r *http.Request) (*http.Response, error) { rt.checkRequest(r) return rt.theResponse, rt.theError } // NewRoundTripCheckRequest creates a new instance of a type that implements http.RoundTripper, // which before returning theResponse and theError, executes checkRequest against a http.Request. func NewRoundTripCheckRequest(checkRequest func(*http.Request), theResponse *http.Response, theError error) http.RoundTripper { return &roundTripCheckRequest{ checkRequest: checkRequest, roundTrip: roundTrip{ theResponse: theResponse, theError: theError, }, } } type oauth2TestServerResponse struct { AccessToken string `json:"access_token"` TokenType string `json:"token_type"` } type testOAuthServer struct { tokenTS *httptest.Server ts *httptest.Server } // newTestOAuthServer returns a new test server with the expected base64 encoded client ID and secret. func newTestOAuthServer(t testing.TB, expectedAuth *string) testOAuthServer { var previousAuth string tokenTS := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { auth := r.Header.Get("Authorization") if auth != *expectedAuth { t.Fatalf("bad auth, expected %s, got %s", *expectedAuth, auth) } if auth == previousAuth { t.Fatal("token endpoint called twice") } previousAuth = auth res, _ := json.Marshal(oauth2TestServerResponse{ AccessToken: "12345", TokenType: "Bearer", }) w.Header().Add("Content-Type", "application/json") _, _ = w.Write(res) })) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { auth := r.Header.Get("Authorization") if auth != "Bearer 12345" { t.Fatalf("bad auth, expected %s, got %s", "Bearer 12345", auth) } fmt.Fprintln(w, "Hello, client") })) return testOAuthServer{ tokenTS: tokenTS, ts: ts, } } func (s *testOAuthServer) url() string { return s.ts.URL } func (s *testOAuthServer) tokenURL() string { return s.tokenTS.URL } func (s *testOAuthServer) close() { s.tokenTS.Close() s.ts.Close() } func TestOAuth2(t *testing.T) { var expectedAuth string ts := newTestOAuthServer(t, &expectedAuth) defer ts.close() yamlConfig := fmt.Sprintf(` client_id: 1 client_secret: 2 scopes: - A - B token_url: %s endpoint_params: hi: hello `, ts.tokenURL()) expectedConfig := OAuth2{ ClientID: "1", ClientSecret: "2", Scopes: []string{"A", "B"}, EndpointParams: map[string]string{"hi": "hello"}, TokenURL: ts.tokenURL(), } var unmarshalledConfig OAuth2 if err := yaml.Unmarshal([]byte(yamlConfig), &unmarshalledConfig); err != nil { t.Fatalf("Expected no error unmarshalling yaml, got %v", err) } if !reflect.DeepEqual(unmarshalledConfig, expectedConfig) { t.Fatalf("Got unmarshalled config %v, expected %v", unmarshalledConfig, expectedConfig) } rt := NewOAuth2RoundTripper(NewInlineSecret(string(expectedConfig.ClientSecret)), &expectedConfig, http.DefaultTransport, &defaultHTTPClientOptions) client := http.Client{ Transport: rt, } // Default secret. expectedAuth = "Basic MToy" resp, err := client.Get(ts.url()) if err != nil { t.Fatal(err) } authorization := resp.Request.Header.Get("Authorization") if authorization != "Bearer 12345" { t.Fatalf("Expected authorization header to be 'Bearer', got '%s'", authorization) } // Making a second request with the same secret should not re-call the token API. _, err = client.Get(ts.url()) if err != nil { t.Fatal(err) } // Empty secret. expectedAuth = "Basic MTo=" expectedConfig.ClientSecret = "" resp, err = client.Get(ts.url()) if err != nil { t.Fatal(err) } authorization = resp.Request.Header.Get("Authorization") if authorization != "Bearer 12345" { t.Fatalf("Expected authorization header to be 'Bearer 12345', got '%s'", authorization) } // Making a second request with the same secret should not re-call the token API. resp, err = client.Get(ts.url()) if err != nil { t.Fatal(err) } // Update secret. expectedAuth = "Basic MToxMjM0NTY3" expectedConfig.ClientSecret = "1234567" _, err = client.Get(ts.url()) if err != nil { t.Fatal(err) } // Making a second request with the same secret should not re-call the token API. _, err = client.Get(ts.url()) if err != nil { t.Fatal(err) } authorization = resp.Request.Header.Get("Authorization") if authorization != "Bearer 12345" { t.Fatalf("Expected authorization header to be 'Bearer 12345', got '%s'", authorization) } } func TestOAuth2UserAgent(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Header.Get("User-Agent") != "myuseragent" { t.Fatalf("Expected User-Agent header in oauth request to be 'myuseragent', got '%s'", r.Header.Get("User-Agent")) } res, _ := json.Marshal(oauth2TestServerResponse{ AccessToken: "12345", TokenType: "Bearer", }) w.Header().Add("Content-Type", "application/json") _, _ = w.Write(res) })) defer ts.Close() config := DefaultHTTPClientConfig config.OAuth2 = &OAuth2{ ClientID: "1", ClientSecret: "2", Scopes: []string{"A", "B"}, EndpointParams: map[string]string{"hi": "hello"}, TokenURL: fmt.Sprintf("%s/token", ts.URL), } rt, err := NewRoundTripperFromConfig(config, "test_oauth2", WithUserAgent("myuseragent")) if err != nil { t.Fatal(err) } client := http.Client{ Transport: rt, } resp, err := client.Get(ts.URL) if err != nil { t.Fatal(err) } authorization := resp.Request.Header.Get("Authorization") if authorization != "Bearer 12345" { t.Fatalf("Expected authorization header to be 'Bearer 12345', got '%s'", authorization) } } func TestHost(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Host != "localhost.localdomain" { t.Fatalf("Expected Host header in request to be 'localhost.localdomain', got '%s'", r.Host) } w.Header().Add("Content-Type", "application/json") })) defer ts.Close() config := DefaultHTTPClientConfig rt, err := NewRoundTripperFromConfig(config, "test_host", WithHost("localhost.localdomain")) if err != nil { t.Fatal(err) } client := http.Client{ Transport: rt, } _, err = client.Get(ts.URL) if err != nil { t.Fatal(err) } } func TestOAuth2WithFile(t *testing.T) { var expectedAuth string ts := newTestOAuthServer(t, &expectedAuth) defer ts.close() secretFile, err := os.CreateTemp("", "oauth2_secret") if err != nil { t.Fatal(err) } defer os.Remove(secretFile.Name()) yamlConfig := fmt.Sprintf(` client_id: 1 client_secret_file: %s scopes: - A - B token_url: %s endpoint_params: hi: hello `, secretFile.Name(), ts.tokenURL()) expectedConfig := OAuth2{ ClientID: "1", ClientSecretFile: secretFile.Name(), Scopes: []string{"A", "B"}, EndpointParams: map[string]string{"hi": "hello"}, TokenURL: ts.tokenURL(), } var unmarshalledConfig OAuth2 err = yaml.Unmarshal([]byte(yamlConfig), &unmarshalledConfig) if err != nil { t.Fatalf("Expected no error unmarshalling yaml, got %v", err) } if !reflect.DeepEqual(unmarshalledConfig, expectedConfig) { t.Fatalf("Got unmarshalled config %v, expected %v", unmarshalledConfig, expectedConfig) } rt := NewOAuth2RoundTripper(NewInlineSecret(string(expectedConfig.ClientSecret)), &expectedConfig, http.DefaultTransport, &defaultHTTPClientOptions) client := http.Client{ Transport: rt, } // Empty secret file. expectedAuth = "Basic MTo=" resp, err := client.Get(ts.url()) if err != nil { t.Fatal(err) } authorization := resp.Request.Header.Get("Authorization") if authorization != "Bearer 12345" { t.Fatalf("Expected authorization header to be 'Bearer', got '%s'", authorization) } // Making a second request with the same file content should not re-call the token API. _, err = client.Get(ts.url()) if err != nil { t.Fatal(err) } // File populated. expectedAuth = "Basic MToxMjM0NTY=" if _, err := secretFile.Write([]byte("123456")); err != nil { t.Fatal(err) } resp, err = client.Get(ts.url()) if err != nil { t.Fatal(err) } authorization = resp.Request.Header.Get("Authorization") if authorization != "Bearer 12345" { t.Fatalf("Expected authorization header to be 'Bearer 12345', got '%s'", authorization) } // Making a second request with the same file content should not re-call the token API. resp, err = client.Get(ts.url()) if err != nil { t.Fatal(err) } // Update file. expectedAuth = "Basic MToxMjM0NTY3" if _, err := secretFile.Write([]byte("7")); err != nil { t.Fatal(err) } _, err = client.Get(ts.url()) if err != nil { t.Fatal(err) } // Making a second request with the same file content should not re-call the token API. _, err = client.Get(ts.url()) if err != nil { t.Fatal(err) } authorization = resp.Request.Header.Get("Authorization") if authorization != "Bearer 12345" { t.Fatalf("Expected authorization header to be 'Bearer 12345', got '%s'", authorization) } } func TestMarshalURL(t *testing.T) { urlp, err := url.Parse("http://example.com/") if err != nil { t.Fatal(err) } u := &URL{urlp} c, err := json.Marshal(u) if err != nil { t.Fatal(err) } if string(c) != "\"http://example.com/\"" { t.Fatalf("URL not properly marshaled in JSON got '%s'", string(c)) } c, err = yaml.Marshal(u) if err != nil { t.Fatal(err) } if string(c) != "http://example.com/\n" { t.Fatalf("URL not properly marshaled in YAML got '%s'", string(c)) } } func TestMarshalURLWrapperWithNilValue(t *testing.T) { u := &URL{} c, err := json.Marshal(u) if err != nil { t.Fatal(err) } if string(c) != "null" { t.Fatalf("URL with nil value not properly marshaled into JSON, got %q", c) } c, err = yaml.Marshal(u) if err != nil { t.Fatal(err) } if string(c) != "null\n" { t.Fatalf("URL with nil value not properly marshaled into JSON, got %q", c) } } func TestUnmarshalNullURL(t *testing.T) { b := []byte(`null`) { var u URL err := json.Unmarshal(b, &u) if err != nil { t.Fatal(err) } if !isEmptyNonNilURL(u.URL) { t.Fatalf("`null` literal not properly unmarshaled from JSON as URL, got %#v", u.URL) } } { var u URL err := yaml.Unmarshal(b, &u) if err != nil { t.Fatal(err) } if u.URL != nil { // UnmarshalYAML is not called when parsing null literal. t.Fatalf("`null` literal not properly unmarshaled from YAML as URL, got %#v", u.URL) } } } func TestUnmarshalEmptyURL(t *testing.T) { b := []byte(`""`) { var u URL err := json.Unmarshal(b, &u) if err != nil { t.Fatal(err) } if !isEmptyNonNilURL(u.URL) { t.Fatalf("empty string not properly unmarshaled from JSON as URL, got %#v", u.URL) } } { var u URL err := yaml.Unmarshal(b, &u) if err != nil { t.Fatal(err) } if !isEmptyNonNilURL(u.URL) { t.Fatalf("empty string not properly unmarshaled from YAML as URL, got %#v", u.URL) } } } // checks if u equals to &url.URL{} func isEmptyNonNilURL(u *url.URL) bool { return u != nil && *u == url.URL{} } func TestUnmarshalURL(t *testing.T) { b := []byte(`"http://example.com/a b"`) var u URL err := json.Unmarshal(b, &u) if err != nil { t.Fatal(err) } if u.String() != "http://example.com/a%20b" { t.Fatalf("URL not properly unmarshaled in JSON, got '%s'", u.String()) } err = yaml.Unmarshal(b, &u) if err != nil { t.Fatal(err) } if u.String() != "http://example.com/a%20b" { t.Fatalf("URL not properly unmarshaled in YAML, got '%s'", u.String()) } } func TestMarshalURLWithSecret(t *testing.T) { var u URL err := yaml.Unmarshal([]byte("http://foo:bar@example.com"), &u) if err != nil { t.Fatal(err) } b, err := yaml.Marshal(u) if err != nil { t.Fatal(err) } if strings.TrimSpace(string(b)) != "http://foo:xxxxx@example.com" { t.Fatalf("URL not properly marshaled in YAML, got '%s'", string(b)) } } func TestHTTPClientConfig_Marshal(t *testing.T) { proxyURL, err := url.Parse("http://localhost:8080") require.NoError(t, err) t.Run("without HTTP headers", func(t *testing.T) { config := &HTTPClientConfig{ ProxyConfig: ProxyConfig{ ProxyURL: URL{proxyURL}, }, } t.Run("YAML", func(t *testing.T) { actualYAML, err := yaml.Marshal(config) require.NoError(t, err) require.YAMLEq(t, ` proxy_url: "http://localhost:8080" follow_redirects: false enable_http2: false `, string(actualYAML)) // Unmarshalling the YAML should get the same struct in input. actual := &HTTPClientConfig{} require.NoError(t, yaml.Unmarshal(actualYAML, actual)) require.Equal(t, config, actual) }) t.Run("JSON", func(t *testing.T) { actualJSON, err := json.Marshal(config) require.NoError(t, err) require.JSONEq(t, `{ "proxy_url":"http://localhost:8080", "tls_config":{"insecure_skip_verify":false}, "follow_redirects":false, "enable_http2":false }`, string(actualJSON)) // Unmarshalling the JSON should get the same struct in input. actual := &HTTPClientConfig{} require.NoError(t, json.Unmarshal(actualJSON, actual)) require.Equal(t, config, actual) }) }) t.Run("with HTTP headers", func(t *testing.T) { config := &HTTPClientConfig{ ProxyConfig: ProxyConfig{ ProxyURL: URL{proxyURL}, }, HTTPHeaders: &Headers{ Headers: map[string]Header{ "X-Test": { Values: []string{"Value-1", "Value-2"}, }, }, }, } actualYAML, err := yaml.Marshal(config) require.NoError(t, err) require.YAMLEq(t, ` proxy_url: "http://localhost:8080" follow_redirects: false enable_http2: false http_headers: X-Test: values: - Value-1 - Value-2 `, string(actualYAML)) actualJSON, err := json.Marshal(config) require.NoError(t, err) require.JSONEq(t, `{ "proxy_url":"http://localhost:8080", "tls_config":{"insecure_skip_verify":false}, "follow_redirects":false, "enable_http2":false, "http_headers":{"X-Test":{"values":["Value-1","Value-2"]}} }`, string(actualJSON)) }) } func TestOAuth2Proxy(t *testing.T) { _, _, err := LoadHTTPConfigFile("testdata/http.conf.oauth2-proxy.good.yml") if err != nil { t.Errorf("Error loading OAuth2 client config: %v", err) } } func TestModifyTLSCertificates(t *testing.T) { bs := getCertificateBlobs(t) tmpDir, err := os.MkdirTemp("", "modifytlscertificates") if err != nil { t.Fatal("Failed to create tmp dir", err) } defer os.RemoveAll(tmpDir) ca, cert, key := filepath.Join(tmpDir, "ca"), filepath.Join(tmpDir, "cert"), filepath.Join(tmpDir, "key") handler := func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, ExpectedMessage) } testServer, err := newTestServer(handler) if err != nil { t.Fatal(err.Error()) } defer testServer.Close() tests := []struct { ca string cert string key string errMsg string modification func() }{ { ca: ClientCertificatePath, cert: ClientCertificatePath, key: ClientKeyNoPassPath, errMsg: "certificate signed by unknown authority", modification: func() { writeCertificate(bs, TLSCAChainPath, ca) }, }, { ca: TLSCAChainPath, cert: WrongClientCertPath, key: ClientKeyNoPassPath, errMsg: "private key does not match public key", modification: func() { writeCertificate(bs, ClientCertificatePath, cert) }, }, { ca: TLSCAChainPath, cert: ClientCertificatePath, key: WrongClientCertPath, errMsg: "found a certificate rather than a key in the PEM for the private key", modification: func() { writeCertificate(bs, ClientKeyNoPassPath, key) }, }, } cfg := HTTPClientConfig{ TLSConfig: TLSConfig{ CAFile: ca, CertFile: cert, KeyFile: key, InsecureSkipVerify: false, }, } var c *http.Client for i, tc := range tests { t.Run(strconv.Itoa(i), func(t *testing.T) { writeCertificate(bs, tc.ca, ca) writeCertificate(bs, tc.cert, cert) writeCertificate(bs, tc.key, key) if c == nil { c, err = NewClientFromConfig(cfg, "test") if err != nil { t.Fatalf("Error creating HTTP Client: %v", err) } } req, err := http.NewRequest(http.MethodGet, testServer.URL, nil) if err != nil { t.Fatalf("Error creating HTTP request: %v", err) } r, err := c.Do(req) if err == nil { r.Body.Close() t.Fatalf("Could connect to the test server.") } if !strings.Contains(err.Error(), tc.errMsg) { t.Fatalf("Expected error message to contain %q, got %q", tc.errMsg, err) } tc.modification() r, err = c.Do(req) if err != nil { t.Fatalf("Expected no error, got %q", err) } b, err := io.ReadAll(r.Body) r.Body.Close() if err != nil { t.Errorf("Can't read the server response body") } got := strings.TrimSpace(string(b)) if ExpectedMessage != got { t.Errorf("The expected message %q differs from the obtained message %q", ExpectedMessage, got) } }) } } // loadHTTPConfigJSON parses the JSON input s into a HTTPClientConfig. func loadHTTPConfigJSON(buf []byte) (*HTTPClientConfig, error) { cfg := &HTTPClientConfig{} err := json.Unmarshal(buf, cfg) if err != nil { return nil, err } return cfg, nil } // loadHTTPConfigJSONFile parses the given JSON file into a HTTPClientConfig. func loadHTTPConfigJSONFile(filename string) (*HTTPClientConfig, []byte, error) { content, err := os.ReadFile(filename) if err != nil { return nil, nil, err } cfg, err := loadHTTPConfigJSON(content) if err != nil { return nil, nil, err } return cfg, content, nil } func TestProxyConfig_Proxy(t *testing.T) { var proxyServer *httptest.Server defer func() { if proxyServer != nil { proxyServer.Close() } }() proxyServerHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello, %s", r.URL.Path) }) proxyServer = httptest.NewServer(proxyServerHandler) testCases := []struct { name string proxyConfig string expectedProxyURL string targetURL string proxyEnv string noProxyEnv string }{ { name: "proxy from environment", proxyConfig: `proxy_from_environment: true`, expectedProxyURL: proxyServer.URL, proxyEnv: proxyServer.URL, targetURL: "http://prometheus.io/", }, { name: "proxy_from_environment with no_proxy", proxyConfig: `proxy_from_environment: true`, expectedProxyURL: "", proxyEnv: proxyServer.URL, noProxyEnv: "prometheus.io", targetURL: "http://prometheus.io/", }, { name: "proxy_from_environment and localhost", proxyConfig: `proxy_from_environment: true`, expectedProxyURL: "", proxyEnv: proxyServer.URL, targetURL: "http://localhost/", }, { name: "valid proxy_url and localhost", proxyConfig: fmt.Sprintf(`proxy_url: %s`, proxyServer.URL), expectedProxyURL: proxyServer.URL, targetURL: "http://localhost/", }, { name: "valid proxy_url and no_proxy and localhost", proxyConfig: fmt.Sprintf(`proxy_url: %s no_proxy: prometheus.io`, proxyServer.URL), expectedProxyURL: "", targetURL: "http://localhost/", }, { name: "valid proxy_url", proxyConfig: fmt.Sprintf(`proxy_url: %s`, proxyServer.URL), expectedProxyURL: proxyServer.URL, targetURL: "http://prometheus.io/", }, { name: "valid proxy url and no_proxy", proxyConfig: fmt.Sprintf(`proxy_url: %s no_proxy: prometheus.io`, proxyServer.URL), expectedProxyURL: "", targetURL: "http://prometheus.io/", }, { name: "valid proxy url and no_proxies", proxyConfig: fmt.Sprintf(`proxy_url: %s no_proxy: promcon.io,prometheus.io,cncf.io`, proxyServer.URL), expectedProxyURL: "", targetURL: "http://prometheus.io/", }, { name: "valid proxy url and no_proxies that do not include target", proxyConfig: fmt.Sprintf(`proxy_url: %s no_proxy: promcon.io,cncf.io`, proxyServer.URL), expectedProxyURL: proxyServer.URL, targetURL: "http://prometheus.io/", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { if proxyServer != nil { defer proxyServer.Close() } var proxyConfig ProxyConfig err := yaml.Unmarshal([]byte(tc.proxyConfig), &proxyConfig) if err != nil { t.Errorf("failed to unmarshal proxy config: %v", err) return } if tc.proxyEnv != "" { currentProxy := os.Getenv("HTTP_PROXY") t.Cleanup(func() { os.Setenv("HTTP_PROXY", currentProxy) }) os.Setenv("HTTP_PROXY", tc.proxyEnv) } if tc.noProxyEnv != "" { currentProxy := os.Getenv("NO_PROXY") t.Cleanup(func() { os.Setenv("NO_PROXY", currentProxy) }) os.Setenv("NO_PROXY", tc.noProxyEnv) } req := httptest.NewRequest("GET", tc.targetURL, nil) proxyFunc := proxyConfig.Proxy() resultURL, err := proxyFunc(req) if err != nil { t.Fatalf("expected no error, but got: %v", err) return } if tc.expectedProxyURL == "" && resultURL != nil { t.Fatalf("expected no result URL, but got: %s", resultURL.String()) return } if tc.expectedProxyURL != "" && resultURL == nil { t.Fatalf("expected result URL, but got nil") return } if tc.expectedProxyURL != "" && resultURL.String() != tc.expectedProxyURL { t.Fatalf("expected result URL: %s, but got: %s", tc.expectedProxyURL, resultURL.String()) } }) } } func readFile(t *testing.T, filename string) string { t.Helper() content, err := os.ReadFile(filename) if err != nil { t.Fatalf("Failed to read file %q: %s", filename, err) } return string(content) } func TestHeaders(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { for k, v := range map[string]string{ "One": "value1", "Two": "value2", "Three": "value3", } { if r.Header.Get(k) != v { t.Errorf("expected %q, got %q", v, r.Header.Get(k)) } } w.WriteHeader(http.StatusNoContent) })) t.Cleanup(ts.Close) cfg, _, err := LoadHTTPConfigFile("testdata/http.conf.headers.good.yaml") if err != nil { t.Fatalf("Error loading HTTP client config: %v", err) } client, err := NewClientFromConfig(*cfg, "test") if err != nil { t.Fatalf("Error creating HTTP Client: %v", err) } _, err = client.Get(ts.URL) if err != nil { t.Fatalf("can't fetch URL: %v", err) } } func TestMultipleHeaders(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { for k, v := range map[string][]string{ "One": {"value1a", "value1b", "value1c"}, "Two": {"value2a", "value2b", "value2c"}, "Three": {"value3a", "value3b", "value3c"}, } { if !reflect.DeepEqual(r.Header.Values(k), v) { t.Errorf("expected %v, got %v", v, r.Header.Values(k)) } } w.WriteHeader(http.StatusNoContent) })) t.Cleanup(ts.Close) cfg, _, err := LoadHTTPConfigFile("testdata/http.conf.headers-multiple.good.yaml") if err != nil { t.Fatalf("Error loading HTTP client config: %v", err) } client, err := NewClientFromConfig(*cfg, "test") if err != nil { t.Fatalf("Error creating HTTP Client: %v", err) } _, err = client.Get(ts.URL) if err != nil { t.Fatalf("can't fetch URL: %v", err) } } golang-github-prometheus-common-0.55.0/config/testdata/000077500000000000000000000000001463701437000230725ustar00rootroot00000000000000golang-github-prometheus-common-0.55.0/config/testdata/basic-auth-password000066400000000000000000000000071463701437000266720ustar00rootroot00000000000000foobar golang-github-prometheus-common-0.55.0/config/testdata/basic-auth-username000066400000000000000000000000101463701437000266410ustar00rootroot00000000000000testusergolang-github-prometheus-common-0.55.0/config/testdata/bearer.token000066400000000000000000000001051463701437000253700ustar00rootroot00000000000000theanswertothegreatquestionoflifetheuniverseandeverythingisfortytwo golang-github-prometheus-common-0.55.0/config/testdata/client.crt000066400000000000000000000036641463701437000250730ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIFgjCCA2qgAwIBAgIRAMMSh5NoexSCjSvDRf1fpgQwDQYJKoZIhvcNAQELBQAw aTELMAkGA1UEBhMCVVMxEzARBgNVBAoTClByb21ldGhldXMxKTAnBgNVBAsTIFBy b21ldGhldXMgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MRowGAYDVQQDExFQcm9tZXRo ZXVzIFRMUyBDQTAgFw0yMjA3MDgwOTE1MDhaGA8yMDcyMDYyNTA5MTUwOFowNjEL MAkGA1UEBhMCVVMxEzARBgNVBAoTClByb21ldGhldXMxEjAQBgNVBAMTCWxvY2Fs aG9zdDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKE5sMf63irOiAEo a5GMONLHDji9ATAVs1erm6NW/17UPOSjN1Q1n6JGTp2XLKb5gle7gdGdjXW9IB6n PhXwQp4ZvTaucMxcZ+Zik19tn+azKdfj/FXU0c9R5oEv4B/1jfKG258dQF5es/Ga A2WW3nWA6IwQkHcBcN7cBQCZZ1GcM81rxybuyU4k/FyMheehcJ5MN8iy0Y0YrMcZ KxmRfAR/EfVYjenWXjZNncsUXotQr5I4wBUJ/pj5pYQWpSuyO6oADX1EzcxuL6bO XoEHfGFqmr90lM/x19bHzllu1UxIwqmT8jW3Je89EhlBxb0htNWNg4hKY7658Khq L0tx0AsdIru/JuoQGXrDs4yf+3xL51zSeMr6jewl6AyGQKCc5E+c/zwklCdsVFw7 zapbT6Hok5HjSoMnRi/EGLtd33CQjvgGooPA4LLzWpbZhoA7QZLBXhvAG3qIkTXr 1SaDQcP6GvYItEo3Yvqle7hWqhJB5E7QJ2+0j0ztbOLZBkuQGmiT4Ebsx5IJrRaT jDCkqYzuHjdTAtwDQR6Tuy2Sc+AuAxI4kDH6EwpX5X7E2mkE2RyYusiu6o400K6F QhRysPf1BXxSwQgcvsQTjcl8InyY/JT+7q7TCOLaXoj5rQDwIQdao0IRgr1+M7FQ 5rsuLRD92EI/vLfSikk3MxcwZ1qzAgMBAAGjVjBUMA4GA1UdDwEB/wQEAwIFIDAT BgNVHSUEDDAKBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAAMB8GA1UdIwQYMBaAFMaa Hh5g0+YopeLd1IkizXyK9K/zMA0GCSqGSIb3DQEBCwUAA4ICAQA1qIgzzSid9YZS v3kfqaDmZ3ickDuoJg4DjOz4AoZF+o2SnS/kXrIs/pTABUcfhgxt6xNJUFPIi2Pa IQXkS24Ya85RJxNUrJmqwhavONoxNoC9RBdNqwQy30DxrBcB+881Y/Ln3VQu6mfj aLFk09LFddz3Uc26spc257GkWfvdKjki5xDiFYze8KO0s+J/OWluNOiBG1Pehj+c CkwPzy9lwX0JCbAhsDkJGSY4rh+MO/bg9RemuqCPrmOIH8laBnJFvMTZyZRUTQlB pAcS8Oa6Bth5DUV7XSwWD6ZOe8Jo5BzJmw5hd5/EA+0+LwZqxmB9d7lGMKgEOMJw rIQZCN5PlYYkp31y190rw5XklHMeUJUNzcZKa/tNhjwmU5Pj01gdS5/AnFqO3zRW w3jUI6GR7rqj8g4P/kigIUyuX1Our6K27HUWVmt/SC+DHrhF+J7xet0q3R+UwUx1 4wTzXnA1++s19G9wzo/HenCOTvU2bprl/WQ66/lICU+xxwHfs6kltY3SItvczqOf +iZrmDn/0jmoarkhaND0EpiG6FbsNWsCprPP1uj0ICqvcBD7VfqT4NWY8QWcoqqr JxiOAuuh0iNj8dmax3suNmd+XKIhVHZ3lRBRxrsqqi67axk3mgQby2j9sLxNmrqD Lc+UGxJB/WZg4NvzZSaj2MZmt4zOHQ== -----END CERTIFICATE----- golang-github-prometheus-common-0.55.0/config/testdata/client.key000066400000000000000000000063041463701437000250650ustar00rootroot00000000000000-----BEGIN PRIVATE KEY----- MIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQChObDH+t4qzogB KGuRjDjSxw44vQEwFbNXq5ujVv9e1DzkozdUNZ+iRk6dlyym+YJXu4HRnY11vSAe pz4V8EKeGb02rnDMXGfmYpNfbZ/msynX4/xV1NHPUeaBL+Af9Y3yhtufHUBeXrPx mgNllt51gOiMEJB3AXDe3AUAmWdRnDPNa8cm7slOJPxcjIXnoXCeTDfIstGNGKzH GSsZkXwEfxH1WI3p1l42TZ3LFF6LUK+SOMAVCf6Y+aWEFqUrsjuqAA19RM3Mbi+m zl6BB3xhapq/dJTP8dfWx85ZbtVMSMKpk/I1tyXvPRIZQcW9IbTVjYOISmO+ufCo ai9LcdALHSK7vybqEBl6w7OMn/t8S+dc0njK+o3sJegMhkCgnORPnP88JJQnbFRc O82qW0+h6JOR40qDJ0YvxBi7Xd9wkI74BqKDwOCy81qW2YaAO0GSwV4bwBt6iJE1 69Umg0HD+hr2CLRKN2L6pXu4VqoSQeRO0CdvtI9M7Wzi2QZLkBpok+BG7MeSCa0W k4wwpKmM7h43UwLcA0Eek7stknPgLgMSOJAx+hMKV+V+xNppBNkcmLrIruqONNCu hUIUcrD39QV8UsEIHL7EE43JfCJ8mPyU/u6u0wji2l6I+a0A8CEHWqNCEYK9fjOx UOa7Li0Q/dhCP7y30opJNzMXMGdaswIDAQABAoICAHKXAmLgl09tg5TvGaVVOH33 JNCG5XU7t0A0pGYvy0mnJ7CJoSWlB1TbC71OWVpENLQOfXJyvLxWM6IV1DbbkT21 pZpb2agmdWJ15bEJxYC/Dpp3XD3VCVqFJ4PidzW/3afm2en5bGqmfNbXVFq8JFj3 ylDi5QrwZzy+vH90iM6kat0yIVY2mbWE7CkLZ5D+WYDpQyzOi8nxI7xO0ydVFARO HIF480SkLEoEWIaib6AtNNyEoWFSvTYVGeMMBVFNWMK3Tt8eK/eEyTGRs/GZVHoY vuwc/Dff+Dybvrop4Ehb3p+Qm7I5/ihQC7EP4m9Oqayu7DHOTZ6docLR1dOVjPt4 F0qkeMGaGTDnfGmocqaKskGmhNWEnav5+aaYtFRXEqkLW53lIaGcWv2kyaFfvCYg L810FEn9D5OVmlLjgUrzeEctFmhO2Br33dLl90imtuVI3Kg/qzsM9fiV0KbsONzq I7aIvZZjXrevCOFtNSTfxNT8PrkyjWYN+2sbLWCR7hRvuzSTHI/qh2TzvyhqKeWc ZPVlIT2qvBN5OP+j42J54VXwJNIwUmbKfnETvHMp3Cht/UaEtj/vzAkYB0paEQUs O80vWwN4zk6H/qRV0HewUoNIGYlnTFLg/uOlLwbkctYH9ubEaobtVtwx6hsZ12AM m7N27FsiAf6KJOGN2CqhAoIBAQDBuQgDxtf3XaoUc8YJKnvGRFMmuq8VWIELF2E1 /u+IWP8f89BoUon7J5VMHvKiuvsVa6bOJpENrp/fV9+5IA7a925U7il8LmGis+v7 Sg5pWMJ6gUXq65jssXw0PPDyHEHL0WTwI6KlcI0+Pt8zPujq0TPeHBOadlaPHdg2 lHEWPvuoAeZknLnYWF7Eq0y3cD2LBiFiZWNRO0wccFf7CA1O5ToUDkFB0zXB5ZOJ RgVSUQ5Gnva2OSB+dfFc3HwOADqjnBW+nMDi/ofH2rQEysEp4iTV4N+HkWxpNUPU 9Z3KRUN645P1BK9ufwNnqsagJU8gKNR9EJKITiPU3jqKi/IvAoIBAQDVDjDi574a btsUQcUcip2na+D5jRts+/5lugA5OT6GzIRyYP8WgH7JMbwC91cB3avV08y5SHMB P1wo04qaBL+p1by19ewZ6f4Kfytoad7ZGb/P9tX8H30N8Q/k9kucn4igpJ6XaQXU tJIKWoBsNuUTZkPwa0+FMBBbRFRagu+mbOwnKR6zNIXNh18K7/LCJSb9jy73xG7k DEuRJH10Ow0Ijo4/UACm0CLdavtVtbkGfarETfZSUPuKMHs6dyAME94+IG3WgmWW B1WbtrWXw6RNhaecYDfjeW3iFOjgo+MpaQpnfiz7nqNrUu5zbteJYM2EdHI1baJ+ /VXsXsc4hdK9AoIBAEyWkJqdpIiBmVpYozTAfQrXvGAVcl7oDKyL47zrO1wWg1bo l76G01JeReJAYgEAF4BSfTIHgVV9cmtkXGjeScE8DXy6Y+BanfMrWuKQVr5Dfy/b p/7GgkEhsk8cwM2XalPgRx3BmO37X3v6c1fZSVB8wRrQ0tdAbdxLGk4JxePbpra3 eZTReZAU7/KlHsFvOIWcONqj5u4YmXCs4bu3ZTuJ2LpRIG+bxycPUpL1AemXbiNx eWx1jWkxy+jAqrMGWCiS7u3bH08e/iN/TaiPWGrso0+Dhhwc3FWD33t0V5u+Yn1V OAuofIsc4AW+OKTb2zqFqex//s6wxe3EpjRcO7UCggEAXVL5APtn3yY92pKwp77k LejoRAeWQtfi6GZgILC9fchqH7vzIMUqRDD/3QDA4PVbhq9e1q4wihRZ5xw6cxqv ZdJU9hOB1xwTBkAMIJF3ZvuLdKn3s5eLbKbyQmXMWw/ahht1yHbdcf2iltxrsnsd PrEmA1LOI1YZZBD7LiZ6mRjPHJw7cV4JWiz46c6PNJGXkau9dBRcSpJEK5CjT11q aRwgnQULNAaprvlknHecU4aKXbCUvBvzAuYXpFV3+TJewDHuSu8VVnFiA3I1+wNc ngR0ld/ju0V+Z3CnTXccUxBK2WiAhbtIdAOApZmg2fFINMPZHyQl8KBBmecuNskP tQKCAQALxoCzLhdq6Kl/mqqdPTlvncIuAoaH2VjEc5ZpMIHShPd1YfPv5/sQkD4B 8X7QNLPITaSGvNTevyg/KtVPuWyyCxEjmIXDXOCXkylmJFY9tgaaSGPLRJ62sIbz EJGmUUOBYD+/ybV+dQd3GgkGJ0Hytp+FM8NCWukCFRAxb1m56xfs+RTBuLdJpou7 AV+RafQV1roAQ+Pj3dFsoR6jBJIM4w0S5Q6609W062hrR6hBrlVBGfZpo/Mgmv5K HEnQ7X+AqPaK7BLdzBQb2Qd6hGF8DMVTSBRlc/THnhK/HlVCuWMNuEliGtmIuGYE 0FRrwC2EvZmAS7m/FHfkpry76CRU -----END PRIVATE KEY----- golang-github-prometheus-common-0.55.0/config/testdata/empty000066400000000000000000000000001463701437000241410ustar00rootroot00000000000000golang-github-prometheus-common-0.55.0/config/testdata/headers-file000066400000000000000000000000071463701437000253420ustar00rootroot00000000000000value3 golang-github-prometheus-common-0.55.0/config/testdata/headers-file-a000066400000000000000000000000121463701437000255540ustar00rootroot00000000000000value3a golang-github-prometheus-common-0.55.0/config/testdata/headers-file-b000066400000000000000000000000111463701437000255540ustar00rootroot00000000000000value3b golang-github-prometheus-common-0.55.0/config/testdata/headers-file-c000066400000000000000000000000101463701437000255540ustar00rootroot00000000000000value3c http.conf.auth-creds-and-file-set.too-much.bad.yaml000066400000000000000000000001061463701437000343620ustar00rootroot00000000000000golang-github-prometheus-common-0.55.0/config/testdataauthorization: credentials: bearertoken credentials_file: key.txt golang-github-prometheus-common-0.55.0/config/testdata/http.conf.auth-creds-no-basic.bad.yaml000066400000000000000000000000351463701437000321330ustar00rootroot00000000000000authorization: type: Basic http.conf.basic-auth-and-auth-creds.too-much.bad.yaml000066400000000000000000000001171463701437000346740ustar00rootroot00000000000000golang-github-prometheus-common-0.55.0/config/testdatabasic_auth: username: user password: foo authorization: credentials: foo http.conf.basic-auth-and-oauth2.too-much.bad.yaml000066400000000000000000000001331463701437000340350ustar00rootroot00000000000000golang-github-prometheus-common-0.55.0/config/testdatabasic_auth: username: user password: foo oauth2: client_id: foo client_secret: bar golang-github-prometheus-common-0.55.0/config/testdata/http.conf.basic-auth.bad-username.yaml000066400000000000000000000001321463701437000322360ustar00rootroot00000000000000basic_auth: username: user username_file: testdata/basic-auth-username password: foogolang-github-prometheus-common-0.55.0/config/testdata/http.conf.basic-auth.good.yaml000066400000000000000000000001131463701437000306220ustar00rootroot00000000000000basic_auth: username: user password_file: testdata/basic-auth-password golang-github-prometheus-common-0.55.0/config/testdata/http.conf.basic-auth.no-password.yaml000066400000000000000000000000351463701437000321510ustar00rootroot00000000000000basic_auth: username: user golang-github-prometheus-common-0.55.0/config/testdata/http.conf.basic-auth.no-username.yaml000066400000000000000000000000371463701437000321300ustar00rootroot00000000000000basic_auth: password: secret golang-github-prometheus-common-0.55.0/config/testdata/http.conf.basic-auth.ref.yaml000066400000000000000000000000661463701437000304550ustar00rootroot00000000000000basic_auth: username_ref: admin password_ref: passgolang-github-prometheus-common-0.55.0/config/testdata/http.conf.basic-auth.too-much.bad.yaml000066400000000000000000000001331463701437000321540ustar00rootroot00000000000000basic_auth: username: user password: foo password_file: testdata/basic-auth-password golang-github-prometheus-common-0.55.0/config/testdata/http.conf.basic-auth.username-file.good.yaml000066400000000000000000000001471463701437000333640ustar00rootroot00000000000000basic_auth: username_file: testdata/basic-auth-username password_file: testdata/basic-auth-passwordgolang-github-prometheus-common-0.55.0/config/testdata/http.conf.bearer-token-and-file-set.bad.yml000066400000000000000000000001471463701437000330720ustar00rootroot00000000000000basic_auth: username: username password: "mysecret" bearer_token: mysecret bearer_token_file: file golang-github-prometheus-common-0.55.0/config/testdata/http.conf.empty.bad.yml000066400000000000000000000001161463701437000274000ustar00rootroot00000000000000basic_auth: username: username password: mysecret bearer_token_file: file golang-github-prometheus-common-0.55.0/config/testdata/http.conf.good.yml000066400000000000000000000001531463701437000264460ustar00rootroot00000000000000basic_auth: username: username password: "mysecret" proxy_url: "http://remote.host" enable_http2: true golang-github-prometheus-common-0.55.0/config/testdata/http.conf.headers-multiple.good.yaml000066400000000000000000000003321463701437000320510ustar00rootroot00000000000000http_headers: one: values: [value1a, value1b, value1c] two: values: [value2a] secrets: [value2b, value2c] three: files: [testdata/headers-file-a, testdata/headers-file-b, testdata/headers-file-c] golang-github-prometheus-common-0.55.0/config/testdata/http.conf.headers-reserved.bad.yaml000066400000000000000000000000561463701437000316360ustar00rootroot00000000000000http_headers: user-Agent: values: [bar] golang-github-prometheus-common-0.55.0/config/testdata/http.conf.headers.good.yaml000066400000000000000000000001631463701437000302220ustar00rootroot00000000000000http_headers: one: values: [value1] two: secrets: [value2] three: files: [testdata/headers-file] golang-github-prometheus-common-0.55.0/config/testdata/http.conf.invalid-bearer-token-file.bad.yml000066400000000000000000000000301463701437000331540ustar00rootroot00000000000000bearer_token_file: file golang-github-prometheus-common-0.55.0/config/testdata/http.conf.mix-bearer-and-creds.bad.yaml000066400000000000000000000000601463701437000322720ustar00rootroot00000000000000authorization: type: APIKEY bearer_token: foo golang-github-prometheus-common-0.55.0/config/testdata/http.conf.no-proxy-without-proxy-url.bad.yaml000066400000000000000000000000241463701437000336540ustar00rootroot00000000000000no_proxy: 127.0.0.1 golang-github-prometheus-common-0.55.0/config/testdata/http.conf.no-proxy.bad.yaml000066400000000000000000000000611463701437000301750ustar00rootroot00000000000000proxy_from_environment: true no_proxy: 127.0.0.1 golang-github-prometheus-common-0.55.0/config/testdata/http.conf.oauth2-no-client-id.bad.yaml000066400000000000000000000000771463701437000320730ustar00rootroot00000000000000oauth2: client_secret: "mysecret" token_url: "http://auth" golang-github-prometheus-common-0.55.0/config/testdata/http.conf.oauth2-no-token-url.bad.yaml000066400000000000000000000000761463701437000321420ustar00rootroot00000000000000oauth2: client_id: "myclientid" client_secret: "mysecret" golang-github-prometheus-common-0.55.0/config/testdata/http.conf.oauth2-proxy.good.yml000066400000000000000000000001611463701437000310250ustar00rootroot00000000000000oauth2: client_id: "myclient" client_secret: "mysecret" token_url: "http://auth" proxy_url: "http://foo" golang-github-prometheus-common-0.55.0/config/testdata/http.conf.oauth2-secret-and-file-set.bad.yml000066400000000000000000000001701463701437000331750ustar00rootroot00000000000000oauth2: client_id: "myclient" client_secret: "mysecret" client_secret_file: "mysecret" token_url: "http://auth" golang-github-prometheus-common-0.55.0/config/testdata/http.conf.proxy-from-env.bad.yaml000066400000000000000000000000541463701437000313140ustar00rootroot00000000000000proxy_from_environment: true proxy_url: foo golang-github-prometheus-common-0.55.0/config/testdata/http.conf.proxy-headers.bad.json000066400000000000000000000001661463701437000312110ustar00rootroot00000000000000{ "proxy_connect_header": { "single": [ "value_0" ], "multi": [ "value_1", "value_2" ] } } golang-github-prometheus-common-0.55.0/config/testdata/http.conf.proxy-headers.bad.yml000066400000000000000000000001231463701437000310320ustar00rootroot00000000000000proxy_connect_header: single: - value_0 multi: - value_1 - value_2 golang-github-prometheus-common-0.55.0/config/testdata/http.conf.proxy-headers.good.json000066400000000000000000000002351463701437000314100ustar00rootroot00000000000000{ "proxy_url": "http://remote.host", "proxy_connect_header": { "single": [ "value_0" ], "multi": [ "value_1", "value_2" ] } } golang-github-prometheus-common-0.55.0/config/testdata/http.conf.proxy-headers.good.yml000066400000000000000000000001631463701437000312400ustar00rootroot00000000000000proxy_url: "http://remote.host" proxy_connect_header: single: - value_0 multi: - value_1 - value_2 golang-github-prometheus-common-0.55.0/config/testdata/self-signed-client.crt000066400000000000000000000035021463701437000272600ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIFLjCCAxagAwIBAgIRAMMSh5NoexSCjSvDRf1fpgUwDQYJKoZIhvcNAQELBQAw NjELMAkGA1UEBhMCVVMxEzARBgNVBAoTClByb21ldGhldXMxEjAQBgNVBAMTCWxv Y2FsaG9zdDAgFw0yMjA3MDgwOTE1MDlaGA8yMDcyMDYyNTA5MTUwOVowNjELMAkG A1UEBhMCVVMxEzARBgNVBAoTClByb21ldGhldXMxEjAQBgNVBAMTCWxvY2FsaG9z dDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALtrXxnHr7eUM7Xh7awY LwompmuznbTa/8+OsihSaelUN6RDsAdm7eOMA7KMqZB5NOfeDqEqMIUoaoQ1gzIm 0BJ4dCgi99SnA8b0MjAGqUpRJ3gLLSXsPa5647gxUSP5zQ0hWMMgGaw4rJ9LDOtU z2S8dtqKTHrXl34mpdsLrZyLXwyz8UJ83Jq2Ngx4cApZrbs+g1XlMRV8Vh89Z2bk bbKmDYmIOhTeE1wLdrZ/XecEOvkGZcj3bWiO/yTnP8mTER2hTvSxUrpyHn/55LkU 8PR6wCO7hntZ9LLWxg85XTRdWL7cIyjgJgfL9+hVQQyNEjWC2+LTq1QExqa+IxoH iL4xX/1y+6o1W5XKLf/uplgaWuSK+mjQeqc387DwYbj61QWOjCoaJA1wl6RHuGGV 6ygpdAO1l8o+2U8nuULHW5lx+1BtMG5ytAXy9dWPercs5L8gh1IRNCVXWKsQCCWg iG67nErFV5iRFLuAIX7ixLKJ5MGp/fVKUI9V1EViM2GUU46PVAPhhlZ1qcygjbZ5 CelBnQ/XvGof5b4zm4eEgCc0ZkqsQDeS5jPjTtES8/y5WEKqbyijmvx2P40nuO/d aTxNretMwaptWzu+WXHih0WG2Sq85m41070xsIMEwlqSfdiOOPdax6393NJgkdM7 5NKC3+pzcHK1S1+x/Guawv0NAgMBAAGjNTAzMA4GA1UdDwEB/wQEAwIFIDATBgNV HSUEDDAKBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IC AQBTLnU8jFCmYpPUBOqj/xzBqokiQK92axG/h/3JgB7fFSLzUCV3NtvwBVCU28rA wHwBYPjmGhi1vyHha/hb6V2WMPt0jhMRpNxCf16dAMoyIoWNas88vU2Mef90Chfj 8e6wLtzqAquX/ruwIfsOMnbcSGuh+y54DspCXgsTZ9cnCI2lnQroXZi4WUqi3Enj mFPpVc+mMlffGW6LISo3ehRLA7k3/01yJhqzpTQw44k9ZfJ7VXZTRJKJsaqeljzV VfzDbDfW8ftbZ8IWQGAOQfTa23aHIYcvJfvyxpfQRyrwRxjGytLHoOH/G+1TZuOt KBJ2Xdi9qrr+Wep4eNJm2cTBd1Fpr0hWZ9K27BwwYdZZF8Eu8eP8hSeRmA4PqzAj HauCl8PgWJIWzMloXVZaGxiYX7sGVs79m/Yl9A6+p8RTpK7DVB9+sDIiD2bhiZqL i9YWM8aD2cR20t2ZkuBBPlVTOouF/WotOWrLhT4J+SngkdmLkAjP/5jPFvpTfeGi THyAmp4gigwaM0nIZskPcPCbkk+zFYPToyS49ZJwQMzqK2hkjyQ9LyzUdo9vlDjL 8lFjlUZzqaR0DF3pbf8fs5/16gPurR65SU/ebOs+uxZLYJrP2zKmeISE+q4AMudc rQ0Z6KmGUiXnIvpB105UJ7jlXCxbsruc8gRTbjkgW7yoXg== -----END CERTIFICATE----- golang-github-prometheus-common-0.55.0/config/testdata/self-signed-client.key000066400000000000000000000063101463701437000272600ustar00rootroot00000000000000-----BEGIN PRIVATE KEY----- MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQC7a18Zx6+3lDO1 4e2sGC8KJqZrs5202v/PjrIoUmnpVDekQ7AHZu3jjAOyjKmQeTTn3g6hKjCFKGqE NYMyJtASeHQoIvfUpwPG9DIwBqlKUSd4Cy0l7D2ueuO4MVEj+c0NIVjDIBmsOKyf SwzrVM9kvHbaikx615d+JqXbC62ci18Ms/FCfNyatjYMeHAKWa27PoNV5TEVfFYf PWdm5G2ypg2JiDoU3hNcC3a2f13nBDr5BmXI921ojv8k5z/JkxEdoU70sVK6ch5/ +eS5FPD0esAju4Z7WfSy1sYPOV00XVi+3CMo4CYHy/foVUEMjRI1gtvi06tUBMam viMaB4i+MV/9cvuqNVuVyi3/7qZYGlrkivpo0HqnN/Ow8GG4+tUFjowqGiQNcJek R7hhlesoKXQDtZfKPtlPJ7lCx1uZcftQbTBucrQF8vXVj3q3LOS/IIdSETQlV1ir EAgloIhuu5xKxVeYkRS7gCF+4sSyieTBqf31SlCPVdRFYjNhlFOOj1QD4YZWdanM oI22eQnpQZ0P17xqH+W+M5uHhIAnNGZKrEA3kuYz407REvP8uVhCqm8oo5r8dj+N J7jv3Wk8Ta3rTMGqbVs7vllx4odFhtkqvOZuNdO9MbCDBMJakn3Yjjj3Wset/dzS YJHTO+TSgt/qc3BytUtfsfxrmsL9DQIDAQABAoICAAyGlIiIi/nc8cfKHbROuXYY Ny8jhfq8WDRq+QUw3Ns3QbC8xVr5ShTXGrgoJnz9XMfSU2/5/dwoY1YKrYYAig9x 9XFpRN71eo8lauVCzLWmzth7Br1uGIE8vVNmGGIrI8Uo4WHJF24nK4JJ5cckl+fH oLniXFIpbnqD4rnNAgFgXy3eKNWkuqmsW9hhhDts2uuUtfpbovgooyjbVbnOsnYq GuWCMT+LyAdyzLBNutzhr39NKihQQQOn6u1wdxbluVMdoMVBxKGpVth+vwaPm7r7 KTQ6KDa+QFhjekEyOERzqKa417C3qlMDEsJ4UCyikQD6ie+S7fRjjVM/ieEHd+AA 66CbJ8u3yfXxaicn+SPCeHVKd4GKmJgsg1KDSSg0+w5JWwmAiCJjEydX2HOdx2ys SV2C4o+gxhA48U8ZgGTVoom0OgouQ7rnMd6n3juBDq2/Xp1FeDcE39yEffN7t4XN vHfD7Hjp5capxVyEnpzu0tTVf8KP00NJKtS6I7d8IavUBCgFiJZFXJWdsbhgSsg9 UdypUMd6rW81VaaKvi3JSjWwFpmUVAhr3hFNyQB9+2rxvDCWhUqFKWqjWdPfMgxx qO6eam1S22vrZcyJVkfTzArFQd0J/41Ak0yErLJKLTDEYaBRxFPV0ujWskrmU96c f+m4/k7p3sD8KooXfrERAoIBAQDWSmsFzSOugShur9phJV162XrtbOnV7n1Ko0Vu U/ftohC5FNq0kHxAkY4kGMz2QHdJnqpQoJaCK8pJ+8nA1Osutt31tS3YrOotlNwk KsFSiy+i9xf4NcOr9xKoSEstFPJeM650xPfVP1p4sq87BB2Z3uWfLtWnRxTJnpA2 nwwtdrK5fO3pZnVlWQ4akqbndCjUWURXVOVxDHCyDdwoiz3BpGmVV6jCYanC3e3S E7/OlRLJfRAXoCEbzFsQpsOYncaEG7cAz9pBBXA6VVyEPlVyMG0GHs30W7aG5Bfp IcbhacGyjdV5Wwx8WGun1pOHoclLX7pJ6jOXLobpUVH4FUNTAoIBAQDf5gX9aBqK QxBYcqhZ0aby9K9ZAXSRr03drf4s+TXSU7rUdBqV4BRj1cjQLB6pxpo2ryLoHhkf tLVRnEWpRgSlfu7qSYxU8rNUacAKAPnebjQxU6NMVzFx7zDQz4TJT2StsxoSIw+l O4MwWDvIxHcpjIrl1eZh79BSzrq5dsf3vrPCM+Xxivdkx82WJqiVX/LrY3l9R+kC ud1b3O5vFdhpo8e0sygCdF0+sC0jwE82SCjMMGHMZWd74rmkuHFpJ1xSQf9/jRCf yKhITI/su21FS4rn1rApWpzAvhfhV7HqnwWzFTtmLeGsI+yW4fb1j6oK7t/rVZ+p lnwISXpOPBIfAoIBADnMttNIwsAV7F72pdOgLXeuY37Y6rWeb0MLiPW6RlxdY19Y pakgc7NCz3EjE120g7hiyJOYzR/tSdHszT1q8MiX4ISeyu/vq/aBeWNz+NMX4dB2 D4wOjGm86dZkMYrGZJ1OGVc7rZFiVjfKEoO7l3Rib9Mg4dYN0SiU0Vc6TSGSK6Dm dpGG5lFg1PIL7mLtrPmh3lIj/wMgFOGh5Wk2LYEmpKf4jfdoOk7qZ3RLiWfiQ7// MLD+qw+BbmquYIGwxNPrWdApQDhbjCrfzWWKHqf/Mdj9xBWOC0yVB3IFf0xbpzhP E255RYPgoaESupZR6CahenDnb+TuUstp+M8OhSsCggEBANw/9gJ65yi9ohWv7MY2 g+maI+gFk3tAnPOGFnR9TqGxdidKc2CeBtDS2/FUhXFzif5jOI5oFUToSjmW5bwH wchfXn0gjqh9+0T9pkjw/tv9QuCHKyuM1noC1t2CVliF/j8U4X+X9+sN6RakpWLx SVuZAoXnbfNHqoHbFToei8W9Vi2jSf7bOlRsbGPZcZtHwLonp7pDBAeHeSbF5dNn BPWehHTQjHolqBhjzHPP2NxIDcIXkg00b6Ehvoc4XXAYpSvR+pmp1gGorUo57pbt JSe2kVVRDwgPOAYuuWUWFFH9zuiE6WKxnb7ts+4VKRAVHCwXIjTpjN+Rxj+MsIDH fPcCggEBAIRgZPwB6eI+rvYOPUGSeU681O+8/ZgjyAi8HSOk3dCc3J2fX31m/GsR xM+FExbGYJ3BfdgB9YbLSI8eY7weJRodm0FoCuHePu81z4xj9yEi5hBodXhhDjQM /xbgsSWeotQ+5lTmc5hgve1hl+3t09qNttHaELWASD+0ixBC6A6J4GB68ZKRIunW +ZGiEvrNey6Uunf7T/Wgc+VDcA3HsniaY2yTZY/jWsmDxt/BAwUaQrNwAbHvm/1P J04mvCreWfOITe7CURcLq4FMGzsCEXtdQ77/uJllew1Uv2Yn2WFUiqVxH+UicR1P vOJ7/LvbOa8BlIMsprB2rz3PDSUSaIw= -----END PRIVATE KEY----- golang-github-prometheus-common-0.55.0/config/testdata/server.crt000066400000000000000000000037611463701437000251210ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIFsDCCA5igAwIBAgIRAMMSh5NoexSCjSvDRf1fpgMwDQYJKoZIhvcNAQELBQAw aTELMAkGA1UEBhMCVVMxEzARBgNVBAoTClByb21ldGhldXMxKTAnBgNVBAsTIFBy b21ldGhldXMgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MRowGAYDVQQDExFQcm9tZXRo ZXVzIFRMUyBDQTAgFw0yMjA3MDgwOTE1MDdaGA8yMDcyMDYyNTA5MTUwN1owNjEL MAkGA1UEBhMCVVMxEzARBgNVBAoTClByb21ldGhldXMxEjAQBgNVBAMTCWxvY2Fs aG9zdDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANPl1Iv/z+M8jHHU SggOhvCS/0IfNi82+OprwalmhSL1FyRrGeHDpKArIrHhal7oukizJq96wKTddUVu hjPR7srSYX7J2oPznjb2FmLHnD8y+zxO83XNA5WCDB0yA/KhWHhDmd2pihTTZOo9 jvGi3+LyIqXUeiwIpxuNnH2ghoUy+DTzNCknLkIKAVnDPoM1AI0Wu24rs14A8ZVW ivzY/P8xGwlMmDndrrHwJzMSEMeH7IJi9hx4zJalpoYTVq6Z0Rv0+7SpS+iswi/e MILDhmSvLw0R4x31xkzsPOtUsocVjgBCGGGHo70ISsAxsL6E9QFe2uwZSvbBKfou JaM0txRIZahMeHy5egh2+J08vuZKo9PDBWwKwqQZ4Kb7WtgekiycLmFa/OYHLUX+ Ow8QXu5HU9v9XlP9GV2FQDka2IuMTtS5JCEt5e9ddSb4KVbkRAhfL2snA+w0nmrf CBlrlThFz5Evy5QNAo1ORwiE+8gNUc12EAu9K3TK9WSUYNrLCbkN3oBL+DVp8Y6q quUpKEbElhsJ9V49Err3LPaXpz5aW7Th6oFq7UOB7chqKQ2SNl3/hTlNUw8wFb9Q i8AXs+4SzHo41IEe9QZBvpeucVmdewbJKvNS8Uxs2wmtTq2G2Ae3qGzWl682J7aU w1X6Y46OanQDNtDVQvGN1CW5kvCXAgMBAAGjgYMwgYAwDgYDVR0PAQH/BAQDAgUg MB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMB8G A1UdIwQYMBaAFMaaHh5g0+YopeLd1IkizXyK9K/zMCAGA1UdEQQZMBeCCWxvY2Fs aG9zdIcEfwAAAYcEfwAAADANBgkqhkiG9w0BAQsFAAOCAgEAUXL/lzbgbs6whVrE 3wkp0oDGVZ0Jti1hpeQk7Slt3PHsgu9OQOSGcv9QHs0ybhkDWZQjoCH6Nurx5QaY GnpNQjylfy3zAziO0c7C1uXf7Z9AEMQwbOHFLefnvq86MtnwJ7sadQo+ViwtMgOW He4YhkTyu2CqK8GFXRQUNm/SunffXp5zErPCNQURh4hrDUGlXPzyxgx1DyqFvF4S X8IpsoED3d7cbEL7E9dgXNl7wuy3qoPi9P9KydFTIELBGt1oco980S1attSM9159 t9iUIUMT4EdzmZxpIyJMCD+Lz9Y3zWVyz7DTqFWOtAtmhM4lu44K4S4d/JfAGEal 3h3SMCbBPKwpsloO4r9TeGi2f+T7hfiFMdCezEyG8sXrObCDyVudyUnXnxDkZ5TQ NOzqJaUJHeKzb+Z9WSovce3Pb8ok3GoDugmwqyjuN/rz/0jsDTJm18I6HHtONbUp AIV/H/4+Kewc+Ztv97J7MeQB/2VKcY3vpZpMSEkg2ummRhXUfi0haxfoSCKvRwiD BElUVtwHTsn3OBnKMGcBt32iLVsvbb/0AtNpohznPdQT7dqDVguejmwHn/fc4u4Q vfAay/ACARti9XKGplQi7xn+OoYcAVPLYitYBRNEc6t+4f3EKehrDIMRCnxOFBVX 9Dnm1DebturSQQEOuX5rP15lG1I= -----END CERTIFICATE----- golang-github-prometheus-common-0.55.0/config/testdata/server.key000066400000000000000000000063101463701437000251120ustar00rootroot00000000000000-----BEGIN PRIVATE KEY----- MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQDT5dSL/8/jPIxx 1EoIDobwkv9CHzYvNvjqa8GpZoUi9Rckaxnhw6SgKyKx4Wpe6LpIsyavesCk3XVF boYz0e7K0mF+ydqD85429hZix5w/Mvs8TvN1zQOVggwdMgPyoVh4Q5ndqYoU02Tq PY7xot/i8iKl1HosCKcbjZx9oIaFMvg08zQpJy5CCgFZwz6DNQCNFrtuK7NeAPGV Vor82Pz/MRsJTJg53a6x8CczEhDHh+yCYvYceMyWpaaGE1aumdEb9Pu0qUvorMIv 3jCCw4Zkry8NEeMd9cZM7DzrVLKHFY4AQhhhh6O9CErAMbC+hPUBXtrsGUr2wSn6 LiWjNLcUSGWoTHh8uXoIdvidPL7mSqPTwwVsCsKkGeCm+1rYHpIsnC5hWvzmBy1F /jsPEF7uR1Pb/V5T/RldhUA5GtiLjE7UuSQhLeXvXXUm+ClW5EQIXy9rJwPsNJ5q 3wgZa5U4Rc+RL8uUDQKNTkcIhPvIDVHNdhALvSt0yvVklGDaywm5Dd6AS/g1afGO qqrlKShGxJYbCfVePRK69yz2l6c+Wlu04eqBau1Dge3IaikNkjZd/4U5TVMPMBW/ UIvAF7PuEsx6ONSBHvUGQb6XrnFZnXsGySrzUvFMbNsJrU6thtgHt6hs1pevNie2 lMNV+mOOjmp0AzbQ1ULxjdQluZLwlwIDAQABAoICAQCxGs9jlBQ1YU4hdcXKphmy yan/ogavv8qcZCQhakasyRzmm32ubM8T7/m3oyg821eXm+Uhlf+dzFtQBOi2NyjW 7LAAQMYas2vxlA1x0lSNnhbOeU6Tjx8HvwJRBJS4HpLLMfVQh3uZnHYkMf9fhzqJ fMfowoa6dyD0ro+1kI3elpNN7lgSbWUEXUhztfRxxcMIKY/OrUflsfQ5VXQlkVck E+78/r/c3aQ9pPOeg+LyYnETKZN6iJy27Q0Z0uAIXxefvksC3N1NQ9eqGpOBN9sE HEe/LMwfJmTvtiPUrZ3pueJN5PBr0+rO/Dc+HEoVcxs0Yguoehtl0l07dYaPumep TmXdrKvCkwM5cwnbXSWrCpqMS8Medb3zWvNnWO/mjRwTZyhmNdscjh3Ilvo+YCus wM8HJFD4FuMtL3GtIfoKeszppACTkOOYiViGHmKUiQaSEwF7nhuIQqgN3ULCP7Z5 mhL2RhLWacPfATITNkm4g2o16mFohZ9HPZSkPGm8rw7yhB1s2emoocXsms2iR1oa mggNnUS3m87Z/HmOEyObIQZtYf1ZNuVAGGP4kmhhtNfMTmq3CPYM3oMRR1nb8Ci8 zYwjEIvLYuDVlZFff4+IA7tCBZPichieoioaxutnYtO+nvuzDRiitL4my2EcXeE7 tcIunkP9u5BNiXsfNcy3gQKCAQEA3X9eZ/IPF9Rrsjwtqkt7Oxn/uJ8JCotVBLnq SCd7sCSaM06jUzMjMoj4SYyjzBYLycH/q+euT4UoPdPMKCfwx2NgR87MfuehWzwG pmPbAbLJtLmZ+M/Bz5QzGS3J3f4qYxLptLHX971JgtTdcJhOAc+p/Elt3l43d/fr sMVrZ8hqHlXmA6WuwqHjHnGP1ML6xFfsjDZ2jQ3VEV17XKtinucgitvkVuHYmtdQ wm/yrM8vDkyglgk47j9CyfQdL10elBxe32WY5B0g9TmhIMypmlJk7inPPnAqJ4TF JJBMvZOB9cJAjrtsDN3tAW/1q+wPF1HLwurqTLluZEc5MVjaOQKCAQEA9OenKlxB 5HiANjH0riaokFDtjC27iHoeBkbEt+CyegGXVHEotVcKnG+N4Tw/GXcS9m33vu/X Lmeowp/Z2BKxB7xvw81jQh8gEoUHFlH6DgksTPjVVSEa4wnESrqlFjRquBexpU6e X//xVD72b0txAqJvpvtbxZC41WIwUBTBkHDlj2hegEzUvgzdO92FPRUDrAgB0wSv 05U6fh1/4c3XTHqIHK4/gxiVRmjnpEdjEbOZsfbN8LGQK2eq4FkIS870VKigUZ/U m2YB+8PKKyqKdXpWQHMZ9QvXoU9AwMw4Q+NEk4a/ZrnnMo59voKP1Qoqhd/rEAP7 xa1AMOAl2DhhTwKCAQBdY4Z6bSTP91AxJg5a7thWYu/e967oMzb1dy3AnmUYL1aU q2NRgQ4mEHofCJ1HP0RZHOKfqF9mR85fwx0hETYD23KM1DSEjUULIpPrM87zOF6z RE4XCgG9c87XnuauIqvceezvssxMOBL2hqmW/6BkQxp4tL0ONMtOWcmWDqbqayXT BISmpQS6K2eHPnpWSp9QiYHC3HO/pUVgvPl2aQx70xd1dKEhwLeDEaWLVYgMNI6y iLxshhbq3OFcJQDpJ2ntKMkXh86e32k1+8Zj/ebEmljT0ez/dmtPnjtA31Z71+XD qNNvWraD9k4nfP0oL69tNZ+j30hKcSSKQz1qAPyBAoIBAGBaI3KPCX2Ryx+HV/SM URU2Qb883uM66EUf4pVVWeKWbatTOejebdZOLUvIICsspdE+QpJkWgxvy/2GVnak I/IfOPmX/M0u4bdnjvpBFlgfU8aUv5nWhHV+ijO8aubpiHMVH1ciLz0lvRSgEOSI kdWvgq33houb/Jw3HTrkb6McR7S8IzHnCGwdM40yAhGeCuvL2qvi1CoyM+kaQg3c pi/4pURjaalyKoihDUGctGVqe7WAnFVuBoKNLrVFUfZBXe9QyIJUl5jr8SvUQ93n xsGhd/2zSysVlahpPdicgCZ1a61+/h60VTmWxfIF/ACdF03EYv7SEmQbXX3dMgZ3 aBECggEBALXqdEIkb9pBhwCvUHFG+c/IKBhS6j7BUj9PrZ3MATPXHo6Iy09d/dlV psFQzWVvBmf3pcI0MEi7xdUMSN0jhZ8xp1owDlOQSM8DCQPFLaC38sfhZNThIfz0 Q+fWYPe1lkRBtMVSokN1PtE5zETHlUKkh3fdQs0wihX4Wikc64rjCgXqXc8ng8Lk NCUNBY/7pNfrEm0Zxz+8CvmRaBbL4OT2/hFsdcMiO3P24mCdAPgJ4v97pr8KxRHe SmOyiSdaAyXHr/6+3KgO5pX8YUn9WiTF2hxo4SG3NQuuva0SBZT9B8iFXt1uFUtP Rri7hsjysanKPyaPM1oofbRyWApMyRo= -----END PRIVATE KEY----- golang-github-prometheus-common-0.55.0/config/testdata/tls-ca-chain.pem000066400000000000000000000100271463701437000260400ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIF1DCCA7ygAwIBAgIRAMMSh5NoexSCjSvDRf1fpgIwDQYJKoZIhvcNAQELBQAw ajELMAkGA1UEBhMCVVMxEzARBgNVBAoTClByb21ldGhldXMxKTAnBgNVBAsTIFBy b21ldGhldXMgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MRswGQYDVQQDExJQcm9tZXRo ZXVzIFJvb3QgQ0EwIBcNMjIwNzA4MDkxNTA2WhgPMjA3MjA2MjUwOTE1MDZaMGkx CzAJBgNVBAYTAlVTMRMwEQYDVQQKEwpQcm9tZXRoZXVzMSkwJwYDVQQLEyBQcm9t ZXRoZXVzIENlcnRpZmljYXRlIEF1dGhvcml0eTEaMBgGA1UEAxMRUHJvbWV0aGV1 cyBUTFMgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDXtUbZhHR2 xElyGJ+BwcZh4hm4dh1OhlJ6g98H2rEOK6bBxeO5YZnthfCnHI6WYN270ylusUc6 JVkuU/1PO7NLYsl1D4ZIrRKQBWfg88BYrDO38HUkrm4aohlpT0+f7SiA7eRl1Mb5 x6fi5BAVE5wnQJTE8VPBU+lXJB+SfZEixu+o1PlxVAdMYPAu1Yijakr1lDuZex+/ j/700mihSAcwOvJ/+p4u2WNj0CMvQWiV5+VBZYrfpRN4/201FoyWILIv3HLq5OKp Bpl/TvJ4J8oG1Cbzjm52qLgUOvHkAJ0I04DxWWywHF0VRumwLSqae0xo+KPPijj7 bdnCx+vy37PbFOghzKzSIbPuccfKivVpChgy9n0kkgQhm9cgFE5SBuO6jfRwto0g drSOMIzyXELDG0h0nB2gsPUHjD/OD1DT0VsW/9xXOPBfVgtPFn5LoZ8ninAFmk2r ZiRJhCXhh+Rlw2F/s2STP66RnUGVdfP2syV+UlgJlE7EPE8cDbyfQqg7FTflq+t+ HgXFCAkJ4S34+/qCbGv3DlbnC1lq+FiVwexm1TcfL/lYfhPr/J6VoeFZw4bjTPNa jUILpsXv6IQzgPfCBxeZC6dDkK1D0cEXAqRRYKEFxdLnMjBcUZlWUV9uTuk01fDc 58bmlHt5sEqhcdUqHrR5PdoWJVOSbFwYBwIDAQABo3QwcjAOBgNVHQ8BAf8EBAMC AqQwDwYDVR0lBAgwBgYEVR0lADAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTG mh4eYNPmKKXi3dSJIs18ivSv8zAfBgNVHSMEGDAWgBRJPrEOm2ZrMgr9AFTz9LZy 0fDNNjANBgkqhkiG9w0BAQsFAAOCAgEAoc0OImcyyKSbVK63QA8VmD2o9Xr7abxX o+f+QXWDqKAlNDAuXLYBjHMCc9YFsxXa9XkuKZeIxzop4h9iGG+fxMVPTx3T0gTm MAuHcPka10z4Gy6ZxLzDmxJPkJ46b1n0K2fsv9XshzsHERz3VavwHXbC5mBo1CwI 6xLLtTWMuJdoyt0261D7Dat1JAFIWm2j+kxGvyIP0gNtRsUKOFA22Tlt42sEYnXa 7wmY7b15rndG69Xg9ZiVI5Mb/10gDJQcym23PXRn+JEgssE+WcYhll8f/LRmD49v ZlBBD1dVoc9JyrgT+An+2Z8lE6wCSPqWSwhzvBW4dyB/u7Jn23dlV1SwJR8x/IaW j/DhCELNqD6cSlRK3yjE/a2/iK0F6pNrVgKDY+/9uwFxwkjIRwqfcFtT6YpZ33mg kSdTTbYpeg3XkLYZayE3ntzEhooyQdrJR6YyFVwsgcBCkeLrEbC7y/AG1MQEdKsZ i3q730vztGQBR1ymPwgbB6qzGOXhmnhJHnQjeP2CJWnzDeOh2Vs4CxLAQZJ/dhYd qrbYPAT8FJkp2PvoJP8zpmD7a8QC+6Gr17kl9OupPQrIIfxCXYZKDdGOlkDSUC16 6y0E1WZnI+LVbQB1M584lB2/8jU4xqMqUPfoIcbjkjih9nvVA6t547527MeeTvXT 0ig2QvMFWMw= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIFtDCCA5ygAwIBAgIRAMMSh5NoexSCjSvDRf1fpgEwDQYJKoZIhvcNAQELBQAw ajELMAkGA1UEBhMCVVMxEzARBgNVBAoTClByb21ldGhldXMxKTAnBgNVBAsTIFBy b21ldGhldXMgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MRswGQYDVQQDExJQcm9tZXRo ZXVzIFJvb3QgQ0EwIBcNMjIwNzA4MDkxNTA0WhgPMjA3MjA2MjUwOTE1MDRaMGox CzAJBgNVBAYTAlVTMRMwEQYDVQQKEwpQcm9tZXRoZXVzMSkwJwYDVQQLEyBQcm9t ZXRoZXVzIENlcnRpZmljYXRlIEF1dGhvcml0eTEbMBkGA1UEAxMSUHJvbWV0aGV1 cyBSb290IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEArkzRPi21 E299vXw4FBbMfCXI258SxvvjRVRuKdAHLOBpEEqkYH6r6ScbZaisBFtIePv4ddKl rmv+nDwN84/KS54OOtw1cWD4AnDB0kL3B0pWXjTS1F/u57hRLxM6Ta0UubKbta/h WqSOR/fAA5sgcl+JbbR61QWVeYYXg9bM8YGTwQMeJod26tIUeX/Reo9BHuiW4jPb pvVf7rsOs8E2cGwfYjZu6Zj2qcCxQ/ivCpopKFLNlaKko/KlGDGz9KxK5X3ik+sE fPK9LzLC0k2RLGc3EmcMkdyqE3VNih9nV9SalAXN5yBdYaWWjJXykty7ilU32MBF yO4myL48vif2K68pD/CFhG8YmIOud3woMm1IYS9xlsYKf7+f5CNlxqz+eSoOGhcG dSDNft3h5nuq9J/qb2rIgWMSc2puFNRsx+fis0kS5GvjVadR0lxtArbrNm4S+F22 EjGxeBF5VIWiu31uppbdASIw6DTKcrSVVoWxq+Fk3OOB+7q+rornosop9a/omXGH 0cTmgarjJtMqa0TEQiUPQPPnmpC1joeC7/kh7aks93wfHtY73uAVnTjLGTOwlr50 CgRShcRoLLN049V93l46AFHU/4HWns8dqgdcdGnvIdUCFik916pKDSvEc/DfMLGh H6w9Xlg4+2LgCyG2/FBEMTj+bLoraydzyaECAwEAAaNTMFEwDgYDVR0PAQH/BAQD AgKkMA8GA1UdJQQIMAYGBFUdJQAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU ST6xDptmazIK/QBU8/S2ctHwzTYwDQYJKoZIhvcNAQELBQADggIBAHM79R/uQwQX vsBDfKyBXWFlrhHAgX8XAwMKHjstpQYCcJoiGLRJaMMjxj31T1tylqPdcxz88THN uj9kVFYMo1GU5K9E9lq0LoWQBmX2R7/RgxWqB7FNS+S0xfGyeUb3YPVPI1yhtsKa 6mCtTuCVgsgs/hTa+umjtffxj7l+IQxD8Fq0RFBae+S0v5mjVC2sUVd6usqVt7F6 LUVuYShyAI705guIV9nkz8ZyLzUBJnQAJ8g6DU+nLmdizigUG+JoD/hBbK2hvcjX SL7JLAhYRI4kzWcYR0GUfDf2knFEWNhU8gCPnw70FHMD9QC3NKkQsPvyQRyJh99+ ipwUFbGJJRYWjFBbUxlqZNqBg6+ylZNFGEnG42u2KvPXjgPdivlQWkrX6nG0ayyl rYrvi0FawP3OBpCrhYhqsqkA2m+5L2Pl+J2SsDv4qmPB6fh7K0YDVB37AZSG+nfL oXXpUtwfc9tR71S7GmgkcqYOkHfSzl7ecxXtE2xyl3zhkUPR9YcG+rQhXRRp0lxF kR0EtGOGuvXMCQ/vBVPNEDS3jdceqIrIRI1yPUdhFkF7lrLsfFULllOt6qQWnhn2 A2ObxHToohwuyri/v8QhqNI2Bg0jJHcAJi8I8taToAstCWrtn+WXyfj/QknAik47 aOK9l5wSyyqPfkHybKvT6z9pqWUchJsz -----END CERTIFICATE----- golang-github-prometheus-common-0.55.0/config/testdata/tls_config.cert_no_key.bad.yml000066400000000000000000000000241463701437000307650ustar00rootroot00000000000000cert_file: somefile golang-github-prometheus-common-0.55.0/config/testdata/tls_config.empty.good.json000066400000000000000000000000031463701437000301710ustar00rootroot00000000000000{} golang-github-prometheus-common-0.55.0/config/testdata/tls_config.empty.good.yml000066400000000000000000000000001463701437000300160ustar00rootroot00000000000000golang-github-prometheus-common-0.55.0/config/testdata/tls_config.insecure.good.json000066400000000000000000000000371463701437000306570ustar00rootroot00000000000000{"insecure_skip_verify": true} golang-github-prometheus-common-0.55.0/config/testdata/tls_config.insecure.good.yml000066400000000000000000000000331463701437000305030ustar00rootroot00000000000000insecure_skip_verify: true golang-github-prometheus-common-0.55.0/config/testdata/tls_config.invalid_field.bad.yml000066400000000000000000000000301463701437000312520ustar00rootroot00000000000000something_invalid: true golang-github-prometheus-common-0.55.0/config/testdata/tls_config.key_no_cert.bad.yml000066400000000000000000000000231463701437000307640ustar00rootroot00000000000000key_file: somefile golang-github-prometheus-common-0.55.0/config/testdata/tls_config.max_and_min_version.bad.json000066400000000000000000000000611463701437000326540ustar00rootroot00000000000000{"max_version": "TLS11", "min_version": "TLS12"} golang-github-prometheus-common-0.55.0/config/testdata/tls_config.max_and_min_version.bad.yml000066400000000000000000000000451463701437000325060ustar00rootroot00000000000000max_version: TLS11 min_version: TLS12golang-github-prometheus-common-0.55.0/config/testdata/tls_config.max_and_min_version.good.json000066400000000000000000000000611463701437000330560ustar00rootroot00000000000000{"max_version": "TLS12", "min_version": "TLS11"} golang-github-prometheus-common-0.55.0/config/testdata/tls_config.max_and_min_version.good.yml000066400000000000000000000000451463701437000327100ustar00rootroot00000000000000max_version: TLS12 min_version: TLS11golang-github-prometheus-common-0.55.0/config/testdata/tls_config.max_and_min_version_same.good.json000066400000000000000000000000611463701437000340630ustar00rootroot00000000000000{"max_version": "TLS12", "min_version": "TLS12"} golang-github-prometheus-common-0.55.0/config/testdata/tls_config.max_and_min_version_same.good.yml000066400000000000000000000000451463701437000337150ustar00rootroot00000000000000max_version: TLS12 min_version: TLS12golang-github-prometheus-common-0.55.0/config/testdata/tls_config.max_version.good.json000066400000000000000000000000311463701437000313660ustar00rootroot00000000000000{"max_version": "TLS12"} golang-github-prometheus-common-0.55.0/config/testdata/tls_config.max_version.good.yml000066400000000000000000000000231463701437000312170ustar00rootroot00000000000000max_version: TLS12 golang-github-prometheus-common-0.55.0/config/testdata/tls_config.tlsversion.good.json000066400000000000000000000000311463701437000312440ustar00rootroot00000000000000{"min_version": "TLS11"} golang-github-prometheus-common-0.55.0/config/testdata/tls_config.tlsversion.good.yml000066400000000000000000000000231463701437000310750ustar00rootroot00000000000000min_version: TLS11 golang-github-prometheus-common-0.55.0/config/tls_config_test.go000066400000000000000000000127071463701437000250050ustar00rootroot00000000000000// Copyright 2016 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package config import ( "bytes" "crypto/tls" "encoding/json" "fmt" "os" "path/filepath" "reflect" "strings" "testing" "gopkg.in/yaml.v2" ) // LoadTLSConfig parses the given file into a tls.Config. func LoadTLSConfig(filename string) (*tls.Config, error) { content, err := os.ReadFile(filename) if err != nil { return nil, err } cfg := TLSConfig{} switch filepath.Ext(filename) { case ".yml": if err = yaml.UnmarshalStrict(content, &cfg); err != nil { return nil, err } case ".json": decoder := json.NewDecoder(bytes.NewReader(content)) decoder.DisallowUnknownFields() if err = decoder.Decode(&cfg); err != nil { return nil, err } default: return nil, fmt.Errorf("Unknown extension: %s", filepath.Ext(filename)) } return NewTLSConfig(&cfg) } var expectedTLSConfigs = []struct { filename string config *tls.Config }{ { filename: "tls_config.empty.good.json", config: &tls.Config{}, }, { filename: "tls_config.insecure.good.json", config: &tls.Config{InsecureSkipVerify: true}, }, { filename: "tls_config.tlsversion.good.json", config: &tls.Config{MinVersion: tls.VersionTLS11}, }, { filename: "tls_config.max_version.good.json", config: &tls.Config{MaxVersion: tls.VersionTLS12}, }, { filename: "tls_config.empty.good.yml", config: &tls.Config{}, }, { filename: "tls_config.insecure.good.yml", config: &tls.Config{InsecureSkipVerify: true}, }, { filename: "tls_config.tlsversion.good.yml", config: &tls.Config{MinVersion: tls.VersionTLS11}, }, { filename: "tls_config.max_version.good.yml", config: &tls.Config{MaxVersion: tls.VersionTLS12}, }, { filename: "tls_config.max_and_min_version.good.yml", config: &tls.Config{MaxVersion: tls.VersionTLS12, MinVersion: tls.VersionTLS11}, }, { filename: "tls_config.max_and_min_version_same.good.yml", config: &tls.Config{MaxVersion: tls.VersionTLS12, MinVersion: tls.VersionTLS12}, }, } func TestValidTLSConfig(t *testing.T) { for _, cfg := range expectedTLSConfigs { got, err := LoadTLSConfig("testdata/" + cfg.filename) if err != nil { t.Fatalf("Error parsing %s: %s", cfg.filename, err) } // non-nil functions are never equal. got.GetClientCertificate = nil if !reflect.DeepEqual(got, cfg.config) { t.Fatalf("%v: unexpected config result: \n\n%v\n expected\n\n%v", cfg.filename, got, cfg.config) } } } var invalidTLSConfigs = []struct { filename string errMsg string }{ { filename: "tls_config.max_and_min_version.bad.yml", errMsg: "tls_config.max_version must be greater than or equal to tls_config.min_version if both are specified", }, } func TestInvalidTLSConfig(t *testing.T) { for _, ee := range invalidTLSConfigs { _, err := LoadTLSConfig("testdata/" + ee.filename) if err == nil { t.Error("Expected error with config but got none") continue } if !strings.Contains(err.Error(), ee.errMsg) { t.Errorf("Expected error for invalid HTTP client configuration to contain %q but got: %s", ee.errMsg, err) } } } func TestTLSVersionStringer(t *testing.T) { if s := (TLSVersion)(tls.VersionTLS13); s.String() != "TLS13" { t.Fatalf("tls.VersionTLS13 string should be TLS13, got %s", s.String()) } } func TestTLSVersionMarshalYAML(t *testing.T) { tests := []struct { input TLSVersion expected string err error }{ { input: TLSVersions["TLS13"], expected: "TLS13\n", err: nil, }, { input: TLSVersions["TLS10"], expected: "TLS10\n", err: nil, }, { input: TLSVersion(999), expected: "", err: fmt.Errorf("unknown TLS version: 999"), }, } for _, test := range tests { t.Run(fmt.Sprintf("MarshalYAML(%d)", test.input), func(t *testing.T) { actualBytes, err := yaml.Marshal(&test.input) if err != nil { if test.err == nil || err.Error() != test.err.Error() { t.Fatalf("error %v, expected %v", err, test.err) } return } actual := string(actualBytes) if actual != test.expected { t.Fatalf("returned %s, expected %s", actual, test.expected) } }) } } func TestTLSVersionMarshalJSON(t *testing.T) { tests := []struct { input TLSVersion expected string err error }{ { input: TLSVersions["TLS13"], expected: `"TLS13"`, err: nil, }, { input: TLSVersions["TLS10"], expected: `"TLS10"`, err: nil, }, { input: TLSVersion(999), expected: "", err: fmt.Errorf("unknown TLS version: 999"), }, } for _, test := range tests { t.Run(fmt.Sprintf("MarshalJSON(%d)", test.input), func(t *testing.T) { actualBytes, err := json.Marshal(&test.input) if err != nil { if test.err == nil || !strings.HasSuffix(err.Error(), test.err.Error()) { t.Fatalf("error %v, expected %v", err, test.err) } return } actual := string(actualBytes) if actual != test.expected { t.Fatalf("returned %s, expected %s", actual, test.expected) } }) } } golang-github-prometheus-common-0.55.0/expfmt/000077500000000000000000000000001463701437000213175ustar00rootroot00000000000000golang-github-prometheus-common-0.55.0/expfmt/bench_test.go000066400000000000000000000117521463701437000237720ustar00rootroot00000000000000// Copyright 2015 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package expfmt import ( "bufio" "bytes" "compress/gzip" "errors" "io" "os" "testing" "google.golang.org/protobuf/encoding/protodelim" dto "github.com/prometheus/client_model/go" ) var parser TextParser // Benchmarks to show how much penalty text format parsing actually inflicts. // // Example results on Linux 3.13.0, Intel(R) Core(TM) i7-4700MQ CPU @ 2.40GHz, go1.4. // // BenchmarkParseText 1000 1188535 ns/op 205085 B/op 6135 allocs/op // BenchmarkParseTextGzip 1000 1376567 ns/op 246224 B/op 6151 allocs/op // BenchmarkParseProto 10000 172790 ns/op 52258 B/op 1160 allocs/op // BenchmarkParseProtoGzip 5000 324021 ns/op 94931 B/op 1211 allocs/op // BenchmarkParseProtoMap 10000 187946 ns/op 58714 B/op 1203 allocs/op // // CONCLUSION: The overhead for the map is negligible. Text format needs ~5x more allocations. // Without compression, it needs ~7x longer, but with compression (the more relevant scenario), // the difference becomes less relevant, only ~4x. // // The test data contains 248 samples. // BenchmarkParseText benchmarks the parsing of a text-format scrape into metric // family DTOs. func BenchmarkParseText(b *testing.B) { b.StopTimer() data, err := os.ReadFile("testdata/text") if err != nil { b.Fatal(err) } b.StartTimer() for i := 0; i < b.N; i++ { if _, err := parser.TextToMetricFamilies(bytes.NewReader(data)); err != nil { b.Fatal(err) } } } // BenchmarkParseTextGzip benchmarks the parsing of a gzipped text-format scrape // into metric family DTOs. func BenchmarkParseTextGzip(b *testing.B) { b.StopTimer() data, err := os.ReadFile("testdata/text.gz") if err != nil { b.Fatal(err) } b.StartTimer() for i := 0; i < b.N; i++ { in, err := gzip.NewReader(bytes.NewReader(data)) if err != nil { b.Fatal(err) } if _, err := parser.TextToMetricFamilies(in); err != nil { b.Fatal(err) } } } // BenchmarkParseProto benchmarks the parsing of a protobuf-format scrape into // metric family DTOs. Note that this does not build a map of metric families // (as the text version does), because it is not required for Prometheus // ingestion either. (However, it is required for the text-format parsing, as // the metric family might be sprinkled all over the text, while the // protobuf-format guarantees bundling at one place.) func BenchmarkParseProto(b *testing.B) { b.StopTimer() data, err := os.ReadFile("testdata/protobuf") if err != nil { b.Fatal(err) } b.StartTimer() for i := 0; i < b.N; i++ { family := &dto.MetricFamily{} in := bufio.NewReader(bytes.NewReader(data)) unmarshaler := protodelim.UnmarshalOptions{ MaxSize: -1, } for { family.Reset() if err := unmarshaler.UnmarshalFrom(in, family); err != nil { if errors.Is(err, io.EOF) { break } b.Fatal(err) } } } } // BenchmarkParseProtoGzip is like BenchmarkParseProto above, but parses gzipped // protobuf format. func BenchmarkParseProtoGzip(b *testing.B) { b.StopTimer() data, err := os.ReadFile("testdata/protobuf.gz") if err != nil { b.Fatal(err) } b.StartTimer() for i := 0; i < b.N; i++ { family := &dto.MetricFamily{} gz, err := gzip.NewReader(bytes.NewReader(data)) if err != nil { b.Fatal(err) } in := bufio.NewReader(gz) unmarshaler := protodelim.UnmarshalOptions{ MaxSize: -1, } for { family.Reset() if err := unmarshaler.UnmarshalFrom(in, family); err != nil { if errors.Is(err, io.EOF) { break } b.Fatal(err) } } } } // BenchmarkParseProtoMap is like BenchmarkParseProto but DOES put the parsed // metric family DTOs into a map. This is not happening during Prometheus // ingestion. It is just here to measure the overhead of that map creation and // separate it from the overhead of the text format parsing. func BenchmarkParseProtoMap(b *testing.B) { b.StopTimer() data, err := os.ReadFile("testdata/protobuf") if err != nil { b.Fatal(err) } b.StartTimer() for i := 0; i < b.N; i++ { families := map[string]*dto.MetricFamily{} in := bufio.NewReader(bytes.NewReader(data)) unmarshaler := protodelim.UnmarshalOptions{ MaxSize: -1, } for { family := &dto.MetricFamily{} if err := unmarshaler.UnmarshalFrom(in, family); err != nil { if errors.Is(err, io.EOF) { break } b.Fatal(err) } families[family.GetName()] = family } } } golang-github-prometheus-common-0.55.0/expfmt/decode.go000066400000000000000000000264271463701437000231040ustar00rootroot00000000000000// Copyright 2015 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package expfmt import ( "bufio" "fmt" "io" "math" "mime" "net/http" dto "github.com/prometheus/client_model/go" "google.golang.org/protobuf/encoding/protodelim" "github.com/prometheus/common/model" ) // Decoder types decode an input stream into metric families. type Decoder interface { Decode(*dto.MetricFamily) error } // DecodeOptions contains options used by the Decoder and in sample extraction. type DecodeOptions struct { // Timestamp is added to each value from the stream that has no explicit timestamp set. Timestamp model.Time } // ResponseFormat extracts the correct format from a HTTP response header. // If no matching format can be found FormatUnknown is returned. func ResponseFormat(h http.Header) Format { ct := h.Get(hdrContentType) mediatype, params, err := mime.ParseMediaType(ct) if err != nil { return fmtUnknown } const textType = "text/plain" switch mediatype { case ProtoType: if p, ok := params["proto"]; ok && p != ProtoProtocol { return fmtUnknown } if e, ok := params["encoding"]; ok && e != "delimited" { return fmtUnknown } return fmtProtoDelim case textType: if v, ok := params["version"]; ok && v != TextVersion { return fmtUnknown } return fmtText } return fmtUnknown } // NewDecoder returns a new decoder based on the given input format. // If the input format does not imply otherwise, a text format decoder is returned. func NewDecoder(r io.Reader, format Format) Decoder { switch format.FormatType() { case TypeProtoDelim: return &protoDecoder{r: bufio.NewReader(r)} } return &textDecoder{r: r} } // protoDecoder implements the Decoder interface for protocol buffers. type protoDecoder struct { r protodelim.Reader } // Decode implements the Decoder interface. func (d *protoDecoder) Decode(v *dto.MetricFamily) error { opts := protodelim.UnmarshalOptions{ MaxSize: -1, } if err := opts.UnmarshalFrom(d.r, v); err != nil { return err } if !model.IsValidMetricName(model.LabelValue(v.GetName())) { return fmt.Errorf("invalid metric name %q", v.GetName()) } for _, m := range v.GetMetric() { if m == nil { continue } for _, l := range m.GetLabel() { if l == nil { continue } if !model.LabelValue(l.GetValue()).IsValid() { return fmt.Errorf("invalid label value %q", l.GetValue()) } if !model.LabelName(l.GetName()).IsValid() { return fmt.Errorf("invalid label name %q", l.GetName()) } } } return nil } // textDecoder implements the Decoder interface for the text protocol. type textDecoder struct { r io.Reader fams map[string]*dto.MetricFamily err error } // Decode implements the Decoder interface. func (d *textDecoder) Decode(v *dto.MetricFamily) error { if d.err == nil { // Read all metrics in one shot. var p TextParser d.fams, d.err = p.TextToMetricFamilies(d.r) // If we don't get an error, store io.EOF for the end. if d.err == nil { d.err = io.EOF } } // Pick off one MetricFamily per Decode until there's nothing left. for key, fam := range d.fams { v.Name = fam.Name v.Help = fam.Help v.Type = fam.Type v.Metric = fam.Metric delete(d.fams, key) return nil } return d.err } // SampleDecoder wraps a Decoder to extract samples from the metric families // decoded by the wrapped Decoder. type SampleDecoder struct { Dec Decoder Opts *DecodeOptions f dto.MetricFamily } // Decode calls the Decode method of the wrapped Decoder and then extracts the // samples from the decoded MetricFamily into the provided model.Vector. func (sd *SampleDecoder) Decode(s *model.Vector) error { err := sd.Dec.Decode(&sd.f) if err != nil { return err } *s, err = extractSamples(&sd.f, sd.Opts) return err } // ExtractSamples builds a slice of samples from the provided metric // families. If an error occurs during sample extraction, it continues to // extract from the remaining metric families. The returned error is the last // error that has occurred. func ExtractSamples(o *DecodeOptions, fams ...*dto.MetricFamily) (model.Vector, error) { var ( all model.Vector lastErr error ) for _, f := range fams { some, err := extractSamples(f, o) if err != nil { lastErr = err continue } all = append(all, some...) } return all, lastErr } func extractSamples(f *dto.MetricFamily, o *DecodeOptions) (model.Vector, error) { switch f.GetType() { case dto.MetricType_COUNTER: return extractCounter(o, f), nil case dto.MetricType_GAUGE: return extractGauge(o, f), nil case dto.MetricType_SUMMARY: return extractSummary(o, f), nil case dto.MetricType_UNTYPED: return extractUntyped(o, f), nil case dto.MetricType_HISTOGRAM: return extractHistogram(o, f), nil } return nil, fmt.Errorf("expfmt.extractSamples: unknown metric family type %v", f.GetType()) } func extractCounter(o *DecodeOptions, f *dto.MetricFamily) model.Vector { samples := make(model.Vector, 0, len(f.Metric)) for _, m := range f.Metric { if m.Counter == nil { continue } lset := make(model.LabelSet, len(m.Label)+1) for _, p := range m.Label { lset[model.LabelName(p.GetName())] = model.LabelValue(p.GetValue()) } lset[model.MetricNameLabel] = model.LabelValue(f.GetName()) smpl := &model.Sample{ Metric: model.Metric(lset), Value: model.SampleValue(m.Counter.GetValue()), } if m.TimestampMs != nil { smpl.Timestamp = model.TimeFromUnixNano(*m.TimestampMs * 1000000) } else { smpl.Timestamp = o.Timestamp } samples = append(samples, smpl) } return samples } func extractGauge(o *DecodeOptions, f *dto.MetricFamily) model.Vector { samples := make(model.Vector, 0, len(f.Metric)) for _, m := range f.Metric { if m.Gauge == nil { continue } lset := make(model.LabelSet, len(m.Label)+1) for _, p := range m.Label { lset[model.LabelName(p.GetName())] = model.LabelValue(p.GetValue()) } lset[model.MetricNameLabel] = model.LabelValue(f.GetName()) smpl := &model.Sample{ Metric: model.Metric(lset), Value: model.SampleValue(m.Gauge.GetValue()), } if m.TimestampMs != nil { smpl.Timestamp = model.TimeFromUnixNano(*m.TimestampMs * 1000000) } else { smpl.Timestamp = o.Timestamp } samples = append(samples, smpl) } return samples } func extractUntyped(o *DecodeOptions, f *dto.MetricFamily) model.Vector { samples := make(model.Vector, 0, len(f.Metric)) for _, m := range f.Metric { if m.Untyped == nil { continue } lset := make(model.LabelSet, len(m.Label)+1) for _, p := range m.Label { lset[model.LabelName(p.GetName())] = model.LabelValue(p.GetValue()) } lset[model.MetricNameLabel] = model.LabelValue(f.GetName()) smpl := &model.Sample{ Metric: model.Metric(lset), Value: model.SampleValue(m.Untyped.GetValue()), } if m.TimestampMs != nil { smpl.Timestamp = model.TimeFromUnixNano(*m.TimestampMs * 1000000) } else { smpl.Timestamp = o.Timestamp } samples = append(samples, smpl) } return samples } func extractSummary(o *DecodeOptions, f *dto.MetricFamily) model.Vector { samples := make(model.Vector, 0, len(f.Metric)) for _, m := range f.Metric { if m.Summary == nil { continue } timestamp := o.Timestamp if m.TimestampMs != nil { timestamp = model.TimeFromUnixNano(*m.TimestampMs * 1000000) } for _, q := range m.Summary.Quantile { lset := make(model.LabelSet, len(m.Label)+2) for _, p := range m.Label { lset[model.LabelName(p.GetName())] = model.LabelValue(p.GetValue()) } // BUG(matt): Update other names to "quantile". lset[model.LabelName(model.QuantileLabel)] = model.LabelValue(fmt.Sprint(q.GetQuantile())) lset[model.MetricNameLabel] = model.LabelValue(f.GetName()) samples = append(samples, &model.Sample{ Metric: model.Metric(lset), Value: model.SampleValue(q.GetValue()), Timestamp: timestamp, }) } lset := make(model.LabelSet, len(m.Label)+1) for _, p := range m.Label { lset[model.LabelName(p.GetName())] = model.LabelValue(p.GetValue()) } lset[model.MetricNameLabel] = model.LabelValue(f.GetName() + "_sum") samples = append(samples, &model.Sample{ Metric: model.Metric(lset), Value: model.SampleValue(m.Summary.GetSampleSum()), Timestamp: timestamp, }) lset = make(model.LabelSet, len(m.Label)+1) for _, p := range m.Label { lset[model.LabelName(p.GetName())] = model.LabelValue(p.GetValue()) } lset[model.MetricNameLabel] = model.LabelValue(f.GetName() + "_count") samples = append(samples, &model.Sample{ Metric: model.Metric(lset), Value: model.SampleValue(m.Summary.GetSampleCount()), Timestamp: timestamp, }) } return samples } func extractHistogram(o *DecodeOptions, f *dto.MetricFamily) model.Vector { samples := make(model.Vector, 0, len(f.Metric)) for _, m := range f.Metric { if m.Histogram == nil { continue } timestamp := o.Timestamp if m.TimestampMs != nil { timestamp = model.TimeFromUnixNano(*m.TimestampMs * 1000000) } infSeen := false for _, q := range m.Histogram.Bucket { lset := make(model.LabelSet, len(m.Label)+2) for _, p := range m.Label { lset[model.LabelName(p.GetName())] = model.LabelValue(p.GetValue()) } lset[model.LabelName(model.BucketLabel)] = model.LabelValue(fmt.Sprint(q.GetUpperBound())) lset[model.MetricNameLabel] = model.LabelValue(f.GetName() + "_bucket") if math.IsInf(q.GetUpperBound(), +1) { infSeen = true } samples = append(samples, &model.Sample{ Metric: model.Metric(lset), Value: model.SampleValue(q.GetCumulativeCount()), Timestamp: timestamp, }) } lset := make(model.LabelSet, len(m.Label)+1) for _, p := range m.Label { lset[model.LabelName(p.GetName())] = model.LabelValue(p.GetValue()) } lset[model.MetricNameLabel] = model.LabelValue(f.GetName() + "_sum") samples = append(samples, &model.Sample{ Metric: model.Metric(lset), Value: model.SampleValue(m.Histogram.GetSampleSum()), Timestamp: timestamp, }) lset = make(model.LabelSet, len(m.Label)+1) for _, p := range m.Label { lset[model.LabelName(p.GetName())] = model.LabelValue(p.GetValue()) } lset[model.MetricNameLabel] = model.LabelValue(f.GetName() + "_count") count := &model.Sample{ Metric: model.Metric(lset), Value: model.SampleValue(m.Histogram.GetSampleCount()), Timestamp: timestamp, } samples = append(samples, count) if !infSeen { // Append an infinity bucket sample. lset := make(model.LabelSet, len(m.Label)+2) for _, p := range m.Label { lset[model.LabelName(p.GetName())] = model.LabelValue(p.GetValue()) } lset[model.LabelName(model.BucketLabel)] = model.LabelValue("+Inf") lset[model.MetricNameLabel] = model.LabelValue(f.GetName() + "_bucket") samples = append(samples, &model.Sample{ Metric: model.Metric(lset), Value: count.Value, Timestamp: timestamp, }) } } return samples } golang-github-prometheus-common-0.55.0/expfmt/decode_test.go000066400000000000000000000371361463701437000241420ustar00rootroot00000000000000// Copyright 2015 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package expfmt import ( "bufio" "bytes" "errors" "io" "math" "net/http" "os" "reflect" "sort" "strings" "testing" dto "github.com/prometheus/client_model/go" "google.golang.org/protobuf/proto" "github.com/prometheus/common/model" ) func TestTextDecoder(t *testing.T) { var ( ts = model.Now() in = ` # Only a quite simple scenario with two metric families. # More complicated tests of the parser itself can be found in the text package. # TYPE mf2 counter mf2 3 mf1{label="value1"} -3.14 123456 mf1{label="value2"} 42 mf2 4 ` out = model.Vector{ &model.Sample{ Metric: model.Metric{ model.MetricNameLabel: "mf1", "label": "value1", }, Value: -3.14, Timestamp: 123456, }, &model.Sample{ Metric: model.Metric{ model.MetricNameLabel: "mf1", "label": "value2", }, Value: 42, Timestamp: ts, }, &model.Sample{ Metric: model.Metric{ model.MetricNameLabel: "mf2", }, Value: 3, Timestamp: ts, }, &model.Sample{ Metric: model.Metric{ model.MetricNameLabel: "mf2", }, Value: 4, Timestamp: ts, }, } ) dec := &SampleDecoder{ Dec: &textDecoder{r: strings.NewReader(in)}, Opts: &DecodeOptions{ Timestamp: ts, }, } var all model.Vector for { var smpls model.Vector err := dec.Decode(&smpls) if err != nil && errors.Is(err, io.EOF) { break } if err != nil { t.Fatal(err) } all = append(all, smpls...) } sort.Sort(all) sort.Sort(out) if !reflect.DeepEqual(all, out) { t.Fatalf("output does not match") } } func TestProtoDecoder(t *testing.T) { testTime := model.Now() scenarios := []struct { in string expected model.Vector legacyNameFail bool fail bool }{ { in: "", }, { in: "\x8f\x01\n\rrequest_count\x12\x12Number of requests\x18\x00\"0\n#\n\x0fsome_!abel_name\x12\x10some_label_value\x1a\t\t\x00\x00\x00\x00\x00\x00E\xc0\"6\n)\n\x12another_label_name\x12\x13another_label_value\x1a\t\t\x00\x00\x00\x00\x00\x00U@", fail: true, }, { in: "\x8f\x01\n\rrequest_count\x12\x12Number of requests\x18\x00\"0\n#\n\x0fsome_label_name\x12\x10some_label_value\x1a\t\t\x00\x00\x00\x00\x00\x00E\xc0\"6\n)\n\x12another_label_name\x12\x13another_label_value\x1a\t\t\x00\x00\x00\x00\x00\x00U@", expected: model.Vector{ &model.Sample{ Metric: model.Metric{ model.MetricNameLabel: "request_count", "some_label_name": "some_label_value", }, Value: -42, Timestamp: testTime, }, &model.Sample{ Metric: model.Metric{ model.MetricNameLabel: "request_count", "another_label_name": "another_label_value", }, Value: 84, Timestamp: testTime, }, }, }, { in: "\xb9\x01\n\rrequest_count\x12\x12Number of requests\x18\x02\"O\n#\n\x0fsome_label_name\x12\x10some_label_value\"(\x1a\x12\t\xaeG\xe1z\x14\xae\xef?\x11\x00\x00\x00\x00\x00\x00E\xc0\x1a\x12\t+\x87\x16\xd9\xce\xf7\xef?\x11\x00\x00\x00\x00\x00\x00U\xc0\"A\n)\n\x12another_label_name\x12\x13another_label_value\"\x14\x1a\x12\t\x00\x00\x00\x00\x00\x00\xe0?\x11\x00\x00\x00\x00\x00\x00$@", expected: model.Vector{ &model.Sample{ Metric: model.Metric{ model.MetricNameLabel: "request_count_count", "some_label_name": "some_label_value", }, Value: 0, Timestamp: testTime, }, &model.Sample{ Metric: model.Metric{ model.MetricNameLabel: "request_count_sum", "some_label_name": "some_label_value", }, Value: 0, Timestamp: testTime, }, &model.Sample{ Metric: model.Metric{ model.MetricNameLabel: "request_count", "some_label_name": "some_label_value", "quantile": "0.99", }, Value: -42, Timestamp: testTime, }, &model.Sample{ Metric: model.Metric{ model.MetricNameLabel: "request_count", "some_label_name": "some_label_value", "quantile": "0.999", }, Value: -84, Timestamp: testTime, }, &model.Sample{ Metric: model.Metric{ model.MetricNameLabel: "request_count_count", "another_label_name": "another_label_value", }, Value: 0, Timestamp: testTime, }, &model.Sample{ Metric: model.Metric{ model.MetricNameLabel: "request_count_sum", "another_label_name": "another_label_value", }, Value: 0, Timestamp: testTime, }, &model.Sample{ Metric: model.Metric{ model.MetricNameLabel: "request_count", "another_label_name": "another_label_value", "quantile": "0.5", }, Value: 10, Timestamp: testTime, }, }, }, { in: "\x8d\x01\n\x1drequest_duration_microseconds\x12\x15The response latency.\x18\x04\"S:Q\b\x85\x15\x11\xcd\xcc\xccL\x8f\xcb:A\x1a\v\b{\x11\x00\x00\x00\x00\x00\x00Y@\x1a\f\b\x9c\x03\x11\x00\x00\x00\x00\x00\x00^@\x1a\f\b\xd0\x04\x11\x00\x00\x00\x00\x00\x00b@\x1a\f\b\xf4\v\x11\x9a\x99\x99\x99\x99\x99e@\x1a\f\b\x85\x15\x11\x00\x00\x00\x00\x00\x00\xf0\u007f", expected: model.Vector{ &model.Sample{ Metric: model.Metric{ model.MetricNameLabel: "request_duration_microseconds_bucket", "le": "100", }, Value: 123, Timestamp: testTime, }, &model.Sample{ Metric: model.Metric{ model.MetricNameLabel: "request_duration_microseconds_bucket", "le": "120", }, Value: 412, Timestamp: testTime, }, &model.Sample{ Metric: model.Metric{ model.MetricNameLabel: "request_duration_microseconds_bucket", "le": "144", }, Value: 592, Timestamp: testTime, }, &model.Sample{ Metric: model.Metric{ model.MetricNameLabel: "request_duration_microseconds_bucket", "le": "172.8", }, Value: 1524, Timestamp: testTime, }, &model.Sample{ Metric: model.Metric{ model.MetricNameLabel: "request_duration_microseconds_bucket", "le": "+Inf", }, Value: 2693, Timestamp: testTime, }, &model.Sample{ Metric: model.Metric{ model.MetricNameLabel: "request_duration_microseconds_sum", }, Value: 1756047.3, Timestamp: testTime, }, &model.Sample{ Metric: model.Metric{ model.MetricNameLabel: "request_duration_microseconds_count", }, Value: 2693, Timestamp: testTime, }, }, }, { in: "\u007f\n\x1drequest_duration_microseconds\x12\x15The response latency.\x18\x04\"E:C\b\x85\x15\x11\xcd\xcc\xccL\x8f\xcb:A\x1a\v\b{\x11\x00\x00\x00\x00\x00\x00Y@\x1a\f\b\x9c\x03\x11\x00\x00\x00\x00\x00\x00^@\x1a\f\b\xd0\x04\x11\x00\x00\x00\x00\x00\x00b@\x1a\f\b\xf4\v\x11\x9a\x99\x99\x99\x99\x99e@", expected: model.Vector{ &model.Sample{ Metric: model.Metric{ model.MetricNameLabel: "request_duration_microseconds_count", }, Value: 2693, Timestamp: testTime, }, &model.Sample{ Metric: model.Metric{ "le": "+Inf", model.MetricNameLabel: "request_duration_microseconds_bucket", }, Value: 2693, Timestamp: testTime, }, &model.Sample{ Metric: model.Metric{ model.MetricNameLabel: "request_duration_microseconds_sum", }, Value: 1756047.3, Timestamp: testTime, }, &model.Sample{ Metric: model.Metric{ "le": "172.8", model.MetricNameLabel: "request_duration_microseconds_bucket", }, Value: 1524, Timestamp: testTime, }, &model.Sample{ Metric: model.Metric{ "le": "144", model.MetricNameLabel: "request_duration_microseconds_bucket", }, Value: 592, Timestamp: testTime, }, &model.Sample{ Metric: model.Metric{ "le": "120", model.MetricNameLabel: "request_duration_microseconds_bucket", }, Value: 412, Timestamp: testTime, }, &model.Sample{ Metric: model.Metric{ "le": "100", model.MetricNameLabel: "request_duration_microseconds_bucket", }, Value: 123, Timestamp: testTime, }, }, }, { // The metric type is unset in this protobuf, which needs to be handled // correctly by the decoder. in: "\x1c\n\rrequest_count\"\v\x1a\t\t\x00\x00\x00\x00\x00\x00\xf0?", expected: model.Vector{ &model.Sample{ Metric: model.Metric{ model.MetricNameLabel: "request_count", }, Value: 1, Timestamp: testTime, }, }, }, { in: "\xa8\x01\n\ngauge.name\x12\x11gauge\ndoc\nstr\"ing\x18\x01\"T\n\x1b\n\x06name.1\x12\x11val with\nnew line\n*\n\x06name*2\x12 val with \\backslash and \"quotes\"\x12\t\t\x00\x00\x00\x00\x00\x00\xf0\x7f\"/\n\x10\n\x06name.1\x12\x06Björn\n\x10\n\x06name*2\x12\x06佖佥\x12\t\t\xd1\xcfD\xb9\xd0\x05\xc2H", legacyNameFail: true, expected: model.Vector{ &model.Sample{ Metric: model.Metric{ model.MetricNameLabel: "gauge.name", "name.1": "val with\nnew line", "name*2": "val with \\backslash and \"quotes\"", }, Value: model.SampleValue(math.Inf(+1)), Timestamp: testTime, }, &model.Sample{ Metric: model.Metric{ model.MetricNameLabel: "gauge.name", "name.1": "Björn", "name*2": "佖佥", }, Value: 3.14e42, Timestamp: testTime, }, }, }, } for i, scenario := range scenarios { dec := &SampleDecoder{ Dec: &protoDecoder{r: strings.NewReader(scenario.in)}, Opts: &DecodeOptions{ Timestamp: testTime, }, } var all model.Vector for { model.NameValidationScheme = model.LegacyValidation var smpls model.Vector err := dec.Decode(&smpls) if err != nil && errors.Is(err, io.EOF) { break } if scenario.legacyNameFail { if err == nil { t.Fatal("Expected error when decoding without UTF-8 support enabled but got none") } model.NameValidationScheme = model.UTF8Validation dec = &SampleDecoder{ Dec: &protoDecoder{r: strings.NewReader(scenario.in)}, Opts: &DecodeOptions{ Timestamp: testTime, }, } err = dec.Decode(&smpls) if errors.Is(err, io.EOF) { break } if err != nil { t.Fatalf("Unexpected error when decoding with UTF-8 support: %v", err) } } if scenario.fail { if err == nil { t.Fatal("Expected error but got none") } break } if err != nil { t.Fatal(err) } all = append(all, smpls...) } sort.Sort(all) sort.Sort(scenario.expected) if !reflect.DeepEqual(all, scenario.expected) { t.Fatalf("%d. output does not match, want: %#v, got %#v", i, scenario.expected, all) } } } func TestProtoMultiMessageDecoder(t *testing.T) { data, err := os.ReadFile("testdata/protobuf-multimessage") if err != nil { t.Fatalf("Reading file failed: %v", err) } buf := bytes.NewReader(data) decoder := NewDecoder(buf, fmtProtoDelim) var metrics []*dto.MetricFamily for { var mf dto.MetricFamily if err := decoder.Decode(&mf); err != nil { if errors.Is(err, io.EOF) { break } t.Fatalf("Unmarshalling failed: %v", err) } metrics = append(metrics, &mf) } if len(metrics) != 6 { t.Fatalf("Expected %d metrics but got %d!", 6, len(metrics)) } } func testDiscriminatorHTTPHeader(t testing.TB) { scenarios := []struct { input map[string]string output Format }{ { input: map[string]string{"Content-Type": `application/vnd.google.protobuf; proto="io.prometheus.client.MetricFamily"; encoding="delimited"`}, output: fmtProtoDelim, }, { input: map[string]string{"Content-Type": `application/vnd.google.protobuf; proto="illegal"; encoding="delimited"`}, output: fmtUnknown, }, { input: map[string]string{"Content-Type": `application/vnd.google.protobuf; proto="io.prometheus.client.MetricFamily"; encoding="illegal"`}, output: fmtUnknown, }, { input: map[string]string{"Content-Type": `text/plain; version=0.0.4`}, output: fmtText, }, { input: map[string]string{"Content-Type": `text/plain`}, output: fmtText, }, { input: map[string]string{"Content-Type": `text/plain; version=0.0.3`}, output: fmtUnknown, }, } for i, scenario := range scenarios { var header http.Header if len(scenario.input) > 0 { header = http.Header{} } for key, value := range scenario.input { header.Add(key, value) } actual := ResponseFormat(header) if scenario.output != actual { t.Errorf("%d. expected %s, got %s", i, scenario.output, actual) } } } func TestDiscriminatorHTTPHeader(t *testing.T) { testDiscriminatorHTTPHeader(t) } func BenchmarkDiscriminatorHTTPHeader(b *testing.B) { for i := 0; i < b.N; i++ { testDiscriminatorHTTPHeader(b) } } func TestExtractSamples(t *testing.T) { var ( goodMetricFamily1 = &dto.MetricFamily{ Name: proto.String("foo"), Help: proto.String("Help for foo."), Type: dto.MetricType_COUNTER.Enum(), Metric: []*dto.Metric{ { Counter: &dto.Counter{ Value: proto.Float64(4711), }, }, }, } goodMetricFamily2 = &dto.MetricFamily{ Name: proto.String("bar"), Help: proto.String("Help for bar."), Type: dto.MetricType_GAUGE.Enum(), Metric: []*dto.Metric{ { Gauge: &dto.Gauge{ Value: proto.Float64(3.14), }, }, }, } badMetricFamily = &dto.MetricFamily{ Name: proto.String("bad"), Help: proto.String("Help for bad."), Type: dto.MetricType(42).Enum(), Metric: []*dto.Metric{ { Gauge: &dto.Gauge{ Value: proto.Float64(2.7), }, }, }, } opts = &DecodeOptions{ Timestamp: 42, } ) got, err := ExtractSamples(opts, goodMetricFamily1, goodMetricFamily2) if err != nil { t.Error("Unexpected error from ExtractSamples:", err) } want := model.Vector{ &model.Sample{Metric: model.Metric{model.MetricNameLabel: "foo"}, Value: 4711, Timestamp: 42}, &model.Sample{Metric: model.Metric{model.MetricNameLabel: "bar"}, Value: 3.14, Timestamp: 42}, } if !reflect.DeepEqual(got, want) { t.Errorf("unexpected samples extracted, got: %v, want: %v", got, want) } got, err = ExtractSamples(opts, goodMetricFamily1, badMetricFamily, goodMetricFamily2) if err == nil { t.Error("Expected error from ExtractSamples") } if !reflect.DeepEqual(got, want) { t.Errorf("unexpected samples extracted, got: %v, want: %v", got, want) } } func TestTextDecoderWithBufioReader(t *testing.T) { example := ` # TYPE foo gauge foo 0 ` var decoded bool r := bufio.NewReader(strings.NewReader(example)) dec := NewDecoder(r, fmtText) for { var mf dto.MetricFamily if err := dec.Decode(&mf); err != nil { if errors.Is(err, io.EOF) { break } t.Fatalf("Unexpected error: %v", err) } if mf.GetName() != "foo" { t.Errorf("Unexpected metric name: got %v, expected %v", mf.GetName(), "foo") } if len(mf.Metric) != 1 { t.Errorf("Unexpected number of metrics: got %v, expected %v", len(mf.Metric), 1) } decoded = true } if !decoded { t.Fatal("Metric foo not decoded") } } golang-github-prometheus-common-0.55.0/expfmt/encode.go000066400000000000000000000154511463701437000231110ustar00rootroot00000000000000// Copyright 2015 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package expfmt import ( "fmt" "io" "net/http" "google.golang.org/protobuf/encoding/protodelim" "google.golang.org/protobuf/encoding/prototext" "github.com/prometheus/common/model" "github.com/munnerz/goautoneg" dto "github.com/prometheus/client_model/go" ) // Encoder types encode metric families into an underlying wire protocol. type Encoder interface { Encode(*dto.MetricFamily) error } // Closer is implemented by Encoders that need to be closed to finalize // encoding. (For example, OpenMetrics needs a final `# EOF` line.) // // Note that all Encoder implementations returned from this package implement // Closer, too, even if the Close call is a no-op. This happens in preparation // for adding a Close method to the Encoder interface directly in a (mildly // breaking) release in the future. type Closer interface { Close() error } type encoderCloser struct { encode func(*dto.MetricFamily) error close func() error } func (ec encoderCloser) Encode(v *dto.MetricFamily) error { return ec.encode(v) } func (ec encoderCloser) Close() error { return ec.close() } // Negotiate returns the Content-Type based on the given Accept header. If no // appropriate accepted type is found, FmtText is returned (which is the // Prometheus text format). This function will never negotiate FmtOpenMetrics, // as the support is still experimental. To include the option to negotiate // FmtOpenMetrics, use NegotiateOpenMetrics. func Negotiate(h http.Header) Format { escapingScheme := Format(fmt.Sprintf("; escaping=%s", Format(model.NameEscapingScheme.String()))) for _, ac := range goautoneg.ParseAccept(h.Get(hdrAccept)) { if escapeParam := ac.Params[model.EscapingKey]; escapeParam != "" { switch Format(escapeParam) { case model.AllowUTF8, model.EscapeUnderscores, model.EscapeDots, model.EscapeValues: escapingScheme = Format(fmt.Sprintf("; escaping=%s", escapeParam)) default: // If the escaping parameter is unknown, ignore it. } } ver := ac.Params["version"] if ac.Type+"/"+ac.SubType == ProtoType && ac.Params["proto"] == ProtoProtocol { switch ac.Params["encoding"] { case "delimited": return fmtProtoDelim + escapingScheme case "text": return fmtProtoText + escapingScheme case "compact-text": return fmtProtoCompact + escapingScheme } } if ac.Type == "text" && ac.SubType == "plain" && (ver == TextVersion || ver == "") { return fmtText + escapingScheme } } return fmtText + escapingScheme } // NegotiateIncludingOpenMetrics works like Negotiate but includes // FmtOpenMetrics as an option for the result. Note that this function is // temporary and will disappear once FmtOpenMetrics is fully supported and as // such may be negotiated by the normal Negotiate function. func NegotiateIncludingOpenMetrics(h http.Header) Format { escapingScheme := Format(fmt.Sprintf("; escaping=%s", Format(model.NameEscapingScheme.String()))) for _, ac := range goautoneg.ParseAccept(h.Get(hdrAccept)) { if escapeParam := ac.Params[model.EscapingKey]; escapeParam != "" { switch Format(escapeParam) { case model.AllowUTF8, model.EscapeUnderscores, model.EscapeDots, model.EscapeValues: escapingScheme = Format(fmt.Sprintf("; escaping=%s", escapeParam)) default: // If the escaping parameter is unknown, ignore it. } } ver := ac.Params["version"] if ac.Type+"/"+ac.SubType == ProtoType && ac.Params["proto"] == ProtoProtocol { switch ac.Params["encoding"] { case "delimited": return fmtProtoDelim + escapingScheme case "text": return fmtProtoText + escapingScheme case "compact-text": return fmtProtoCompact + escapingScheme } } if ac.Type == "text" && ac.SubType == "plain" && (ver == TextVersion || ver == "") { return fmtText + escapingScheme } if ac.Type+"/"+ac.SubType == OpenMetricsType && (ver == OpenMetricsVersion_0_0_1 || ver == OpenMetricsVersion_1_0_0 || ver == "") { switch ver { case OpenMetricsVersion_1_0_0: return fmtOpenMetrics_1_0_0 + escapingScheme default: return fmtOpenMetrics_0_0_1 + escapingScheme } } } return fmtText + escapingScheme } // NewEncoder returns a new encoder based on content type negotiation. All // Encoder implementations returned by NewEncoder also implement Closer, and // callers should always call the Close method. It is currently only required // for FmtOpenMetrics, but a future (breaking) release will add the Close method // to the Encoder interface directly. The current version of the Encoder // interface is kept for backwards compatibility. // In cases where the Format does not allow for UTF-8 names, the global // NameEscapingScheme will be applied. // // NewEncoder can be called with additional options to customize the OpenMetrics text output. // For example: // NewEncoder(w, FmtOpenMetrics_1_0_0, WithCreatedLines()) // // Extra options are ignored for all other formats. func NewEncoder(w io.Writer, format Format, options ...EncoderOption) Encoder { escapingScheme := format.ToEscapingScheme() switch format.FormatType() { case TypeProtoDelim: return encoderCloser{ encode: func(v *dto.MetricFamily) error { _, err := protodelim.MarshalTo(w, v) return err }, close: func() error { return nil }, } case TypeProtoCompact: return encoderCloser{ encode: func(v *dto.MetricFamily) error { _, err := fmt.Fprintln(w, model.EscapeMetricFamily(v, escapingScheme).String()) return err }, close: func() error { return nil }, } case TypeProtoText: return encoderCloser{ encode: func(v *dto.MetricFamily) error { _, err := fmt.Fprintln(w, prototext.Format(model.EscapeMetricFamily(v, escapingScheme))) return err }, close: func() error { return nil }, } case TypeTextPlain: return encoderCloser{ encode: func(v *dto.MetricFamily) error { _, err := MetricFamilyToText(w, model.EscapeMetricFamily(v, escapingScheme)) return err }, close: func() error { return nil }, } case TypeOpenMetrics: return encoderCloser{ encode: func(v *dto.MetricFamily) error { _, err := MetricFamilyToOpenMetrics(w, model.EscapeMetricFamily(v, escapingScheme), options...) return err }, close: func() error { _, err := FinalizeOpenMetrics(w) return err }, } } panic(fmt.Errorf("expfmt.NewEncoder: unknown format %q", format)) } golang-github-prometheus-common-0.55.0/expfmt/encode_test.go000066400000000000000000000305311463701437000241440ustar00rootroot00000000000000// Copyright 2018 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package expfmt import ( "bytes" "net/http" "testing" "google.golang.org/protobuf/proto" "github.com/prometheus/common/model" dto "github.com/prometheus/client_model/go" ) func TestNegotiate(t *testing.T) { acceptValuePrefix := "application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily" tests := []struct { name string acceptHeaderValue string expectedFmt string }{ { name: "delimited format", acceptHeaderValue: acceptValuePrefix + ";encoding=delimited", expectedFmt: "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited; escaping=underscores", }, { name: "text format", acceptHeaderValue: acceptValuePrefix + ";encoding=text", expectedFmt: "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=text; escaping=underscores", }, { name: "compact text format", acceptHeaderValue: acceptValuePrefix + ";encoding=compact-text", expectedFmt: "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=compact-text; escaping=underscores", }, { name: "plain text format", acceptHeaderValue: "text/plain;version=0.0.4", expectedFmt: "text/plain; version=0.0.4; charset=utf-8; escaping=underscores", }, { name: "delimited format utf-8", acceptHeaderValue: acceptValuePrefix + ";encoding=delimited; escaping=allow-utf-8;", expectedFmt: "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited; escaping=allow-utf-8", }, { name: "text format utf-8", acceptHeaderValue: acceptValuePrefix + ";encoding=text; escaping=allow-utf-8;", expectedFmt: "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=text; escaping=allow-utf-8", }, { name: "compact text format utf-8", acceptHeaderValue: acceptValuePrefix + ";encoding=compact-text; escaping=allow-utf-8;", expectedFmt: "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=compact-text; escaping=allow-utf-8", }, { name: "plain text format 0.0.4 with utf-8 not valid, falls back", acceptHeaderValue: "text/plain;version=0.0.4;", expectedFmt: "text/plain; version=0.0.4; charset=utf-8; escaping=underscores", }, { name: "plain text format 0.0.4 with utf-8 not valid, falls back", acceptHeaderValue: "text/plain;version=0.0.4; escaping=values;", expectedFmt: "text/plain; version=0.0.4; charset=utf-8; escaping=values", }, } oldDefault := model.NameEscapingScheme model.NameEscapingScheme = model.UnderscoreEscaping defer func() { model.NameEscapingScheme = oldDefault }() for i, test := range tests { t.Run(test.name, func(t *testing.T) { h := http.Header{} h.Add(hdrAccept, test.acceptHeaderValue) actualFmt := string(Negotiate(h)) if actualFmt != test.expectedFmt { t.Errorf("case %d: expected Negotiate to return format %s, but got %s instead", i, test.expectedFmt, actualFmt) } }) } } func TestNegotiateOpenMetrics(t *testing.T) { acceptValuePrefix := "application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily" tests := []struct { name string acceptHeaderValue string expectedFmt string }{ { name: "OM format, no version", acceptHeaderValue: "application/openmetrics-text", expectedFmt: "application/openmetrics-text; version=0.0.1; charset=utf-8; escaping=values", }, { name: "OM format, 0.0.1 version", acceptHeaderValue: "application/openmetrics-text;version=0.0.1; escaping=underscores", expectedFmt: "application/openmetrics-text; version=0.0.1; charset=utf-8; escaping=underscores", }, { name: "OM format, 1.0.0 version", acceptHeaderValue: "application/openmetrics-text;version=1.0.0", expectedFmt: "application/openmetrics-text; version=1.0.0; charset=utf-8; escaping=values", }, { name: "OM format, 0.0.1 version with utf-8 is not valid, falls back", acceptHeaderValue: "application/openmetrics-text;version=0.0.1", expectedFmt: "application/openmetrics-text; version=0.0.1; charset=utf-8; escaping=values", }, { name: "OM format, 1.0.0 version with utf-8 is not valid, falls back", acceptHeaderValue: "application/openmetrics-text;version=1.0.0; escaping=values;", expectedFmt: "application/openmetrics-text; version=1.0.0; charset=utf-8; escaping=values", }, { name: "OM format, invalid version", acceptHeaderValue: "application/openmetrics-text;version=0.0.4", expectedFmt: "text/plain; version=0.0.4; charset=utf-8; escaping=values", }, { name: "compact text format", acceptHeaderValue: acceptValuePrefix + ";encoding=compact-text; escaping=underscores", expectedFmt: "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=compact-text; escaping=underscores", }, { name: "plain text format", acceptHeaderValue: "text/plain;version=0.0.4", expectedFmt: "text/plain; version=0.0.4; charset=utf-8; escaping=values", }, { name: "plain text format 0.0.4", acceptHeaderValue: "text/plain;version=0.0.4; escaping=allow-utf-8", expectedFmt: "text/plain; version=0.0.4; charset=utf-8; escaping=allow-utf-8", }, { name: "delimited format utf-8", acceptHeaderValue: acceptValuePrefix + ";encoding=delimited; escaping=allow-utf-8;", expectedFmt: "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited; escaping=allow-utf-8", }, { name: "text format utf-8", acceptHeaderValue: acceptValuePrefix + ";encoding=text; escaping=allow-utf-8;", expectedFmt: "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=text; escaping=allow-utf-8", }, { name: "compact text format utf-8", acceptHeaderValue: acceptValuePrefix + ";encoding=compact-text; escaping=allow-utf-8;", expectedFmt: "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=compact-text; escaping=allow-utf-8", }, { name: "delimited format escaped", acceptHeaderValue: acceptValuePrefix + ";encoding=delimited; escaping=underscores;", expectedFmt: "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited; escaping=underscores", }, { name: "text format escaped", acceptHeaderValue: acceptValuePrefix + ";encoding=text; escaping=underscores;", expectedFmt: "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=text; escaping=underscores", }, { name: "compact text format escaped", acceptHeaderValue: acceptValuePrefix + ";encoding=compact-text; escaping=underscores;", expectedFmt: "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=compact-text; escaping=underscores", }, } oldDefault := model.NameEscapingScheme model.NameEscapingScheme = model.ValueEncodingEscaping defer func() { model.NameEscapingScheme = oldDefault }() for i, test := range tests { t.Run(test.name, func(t *testing.T) { h := http.Header{} h.Add(hdrAccept, test.acceptHeaderValue) actualFmt := string(NegotiateIncludingOpenMetrics(h)) if actualFmt != test.expectedFmt { t.Errorf("case %d: expected Negotiate to return format %s, but got %s instead", i, test.expectedFmt, actualFmt) } }) } } func TestEncode(t *testing.T) { metric1 := &dto.MetricFamily{ Name: proto.String("foo_metric"), Type: dto.MetricType_UNTYPED.Enum(), Unit: proto.String("seconds"), Metric: []*dto.Metric{ { Untyped: &dto.Untyped{ Value: proto.Float64(1.234), }, }, }, } scenarios := []struct { metric *dto.MetricFamily format Format options []EncoderOption expOut string }{ // 1: Untyped ProtoDelim { metric: metric1, format: fmtProtoDelim, }, // 2: Untyped fmtProtoCompact { metric: metric1, format: fmtProtoCompact, }, // 3: Untyped fmtProtoText { metric: metric1, format: fmtProtoText, }, // 4: Untyped fmtText { metric: metric1, format: fmtText, expOut: `# TYPE foo_metric untyped foo_metric 1.234 `, }, // 5: Untyped fmtOpenMetrics_0_0_1 { metric: metric1, format: fmtOpenMetrics_0_0_1, expOut: `# TYPE foo_metric unknown foo_metric 1.234 `, }, // 6: Untyped fmtOpenMetrics_1_0_0 { metric: metric1, format: fmtOpenMetrics_1_0_0, expOut: `# TYPE foo_metric unknown foo_metric 1.234 `, }, // 7: Simple Counter fmtOpenMetrics_0_0_1 unit opted in { metric: metric1, format: fmtOpenMetrics_0_0_1, options: []EncoderOption{WithUnit()}, expOut: `# TYPE foo_metric_seconds unknown # UNIT foo_metric_seconds seconds foo_metric_seconds 1.234 `, }, // 8: Simple Counter fmtOpenMetrics_1_0_0 unit opted out { metric: metric1, format: fmtOpenMetrics_1_0_0, expOut: `# TYPE foo_metric unknown foo_metric 1.234 `, }, } for i, scenario := range scenarios { out := bytes.NewBuffer(make([]byte, 0, len(scenario.expOut))) enc := NewEncoder(out, scenario.format, scenario.options...) err := enc.Encode(scenario.metric) if err != nil { t.Errorf("%d. error: %s", i, err) continue } if expected, got := len(scenario.expOut), len(out.Bytes()); expected != 0 && expected != got { t.Errorf( "%d. expected %d bytes written, got %d", i, expected, got, ) } if expected, got := scenario.expOut, out.String(); expected != "" && expected != got { t.Errorf( "%d. expected out=%q, got %q", i, expected, got, ) } if len(out.Bytes()) == 0 { t.Errorf( "%d. expected output not to be empty", i, ) } } } func TestEscapedEncode(t *testing.T) { var buff bytes.Buffer delimEncoder := NewEncoder(&buff, fmtProtoDelim+"; escaping=underscores") metric := &dto.MetricFamily{ Name: proto.String("foo.metric"), Type: dto.MetricType_UNTYPED.Enum(), Metric: []*dto.Metric{ { Untyped: &dto.Untyped{ Value: proto.Float64(1.234), }, }, { Label: []*dto.LabelPair{ { Name: proto.String("dotted.label.name"), Value: proto.String("my.label.value"), }, }, Untyped: &dto.Untyped{ Value: proto.Float64(8), }, }, }, } err := delimEncoder.Encode(metric) if err != nil { t.Errorf("unexpected error during encode: %s", err.Error()) } out := buff.Bytes() if len(out) == 0 { t.Errorf("expected the output bytes buffer to be non-empty") } buff.Reset() compactEncoder := NewEncoder(&buff, fmtProtoCompact) err = compactEncoder.Encode(metric) if err != nil { t.Errorf("unexpected error during encode: %s", err.Error()) } out = buff.Bytes() if len(out) == 0 { t.Errorf("expected the output bytes buffer to be non-empty") } buff.Reset() protoTextEncoder := NewEncoder(&buff, fmtProtoText) err = protoTextEncoder.Encode(metric) if err != nil { t.Errorf("unexpected error during encode: %s", err.Error()) } out = buff.Bytes() if len(out) == 0 { t.Errorf("expected the output bytes buffer to be non-empty") } buff.Reset() textEncoder := NewEncoder(&buff, fmtText) err = textEncoder.Encode(metric) if err != nil { t.Errorf("unexpected error during encode: %s", err.Error()) } out = buff.Bytes() if len(out) == 0 { t.Errorf("expected the output bytes buffer to be non-empty") } expected := `# TYPE U__foo_2e_metric untyped U__foo_2e_metric 1.234 U__foo_2e_metric{U__dotted_2e_label_2e_name="my.label.value"} 8 ` if string(out) != expected { t.Errorf("expected TextEncoder to return %s, but got %s instead", expected, string(out)) } } golang-github-prometheus-common-0.55.0/expfmt/expfmt.go000066400000000000000000000127461463701437000231630ustar00rootroot00000000000000// Copyright 2015 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package expfmt contains tools for reading and writing Prometheus metrics. package expfmt import ( "fmt" "strings" "github.com/prometheus/common/model" ) // Format specifies the HTTP content type of the different wire protocols. type Format string // Constants to assemble the Content-Type values for the different wire // protocols. The Content-Type strings here are all for the legacy exposition // formats, where valid characters for metric names and label names are limited. // Support for arbitrary UTF-8 characters in those names is already partially // implemented in this module (see model.ValidationScheme), but to actually use // it on the wire, new content-type strings will have to be agreed upon and // added here. const ( TextVersion = "0.0.4" ProtoType = `application/vnd.google.protobuf` ProtoProtocol = `io.prometheus.client.MetricFamily` protoFmt = ProtoType + "; proto=" + ProtoProtocol + ";" OpenMetricsType = `application/openmetrics-text` OpenMetricsVersion_0_0_1 = "0.0.1" OpenMetricsVersion_1_0_0 = "1.0.0" // The Content-Type values for the different wire protocols. Note that these // values are now unexported. If code was relying on comparisons to these // constants, instead use FormatType(). fmtUnknown Format = `` fmtText Format = `text/plain; version=` + TextVersion + `; charset=utf-8` fmtProtoDelim Format = protoFmt + ` encoding=delimited` fmtProtoText Format = protoFmt + ` encoding=text` fmtProtoCompact Format = protoFmt + ` encoding=compact-text` fmtOpenMetrics_1_0_0 Format = OpenMetricsType + `; version=` + OpenMetricsVersion_1_0_0 + `; charset=utf-8` fmtOpenMetrics_0_0_1 Format = OpenMetricsType + `; version=` + OpenMetricsVersion_0_0_1 + `; charset=utf-8` ) const ( hdrContentType = "Content-Type" hdrAccept = "Accept" ) // FormatType is a Go enum representing the overall category for the given // Format. As the number of Format permutations increases, doing basic string // comparisons are not feasible, so this enum captures the most useful // high-level attribute of the Format string. type FormatType int const ( TypeUnknown FormatType = iota TypeProtoCompact TypeProtoDelim TypeProtoText TypeTextPlain TypeOpenMetrics ) // NewFormat generates a new Format from the type provided. Mostly used for // tests, most Formats should be generated as part of content negotiation in // encode.go. If a type has more than one version, the latest version will be // returned. func NewFormat(t FormatType) Format { switch t { case TypeProtoCompact: return fmtProtoCompact case TypeProtoDelim: return fmtProtoDelim case TypeProtoText: return fmtProtoText case TypeTextPlain: return fmtText case TypeOpenMetrics: return fmtOpenMetrics_1_0_0 default: return fmtUnknown } } // NewOpenMetricsFormat generates a new OpenMetrics format matching the // specified version number. func NewOpenMetricsFormat(version string) (Format, error) { if version == OpenMetricsVersion_0_0_1 { return fmtOpenMetrics_0_0_1, nil } if version == OpenMetricsVersion_1_0_0 { return fmtOpenMetrics_1_0_0, nil } return fmtUnknown, fmt.Errorf("unknown open metrics version string") } // FormatType deduces an overall FormatType for the given format. func (f Format) FormatType() FormatType { toks := strings.Split(string(f), ";") params := make(map[string]string) for i, t := range toks { if i == 0 { continue } args := strings.Split(t, "=") if len(args) != 2 { continue } params[strings.TrimSpace(args[0])] = strings.TrimSpace(args[1]) } switch strings.TrimSpace(toks[0]) { case ProtoType: if params["proto"] != ProtoProtocol { return TypeUnknown } switch params["encoding"] { case "delimited": return TypeProtoDelim case "text": return TypeProtoText case "compact-text": return TypeProtoCompact default: return TypeUnknown } case OpenMetricsType: if params["charset"] != "utf-8" { return TypeUnknown } return TypeOpenMetrics case "text/plain": v, ok := params["version"] if !ok { return TypeTextPlain } if v == TextVersion { return TypeTextPlain } return TypeUnknown default: return TypeUnknown } } // ToEscapingScheme returns an EscapingScheme depending on the Format. Iff the // Format contains a escaping=allow-utf-8 term, it will select NoEscaping. If a valid // "escaping" term exists, that will be used. Otherwise, the global default will // be returned. func (format Format) ToEscapingScheme() model.EscapingScheme { for _, p := range strings.Split(string(format), ";") { toks := strings.Split(p, "=") if len(toks) != 2 { continue } key, value := strings.TrimSpace(toks[0]), strings.TrimSpace(toks[1]) if key == model.EscapingKey { scheme, err := model.ToEscapingScheme(value) if err != nil { return model.NameEscapingScheme } return scheme } } return model.NameEscapingScheme } golang-github-prometheus-common-0.55.0/expfmt/expfmt_test.go000066400000000000000000000063251463701437000242160ustar00rootroot00000000000000// Copyright 2024 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package expfmt import ( "testing" "github.com/prometheus/common/model" ) // Test Format to Escapting Scheme conversion // Path: expfmt/expfmt_test.go // Compare this snippet from expfmt/expfmt.go: func TestToFormatType(t *testing.T) { tests := []struct { format Format expected FormatType }{ { format: fmtProtoCompact, expected: TypeProtoCompact, }, { format: fmtProtoDelim, expected: TypeProtoDelim, }, { format: fmtProtoText, expected: TypeProtoText, }, { format: fmtOpenMetrics_1_0_0, expected: TypeOpenMetrics, }, { format: fmtText, expected: TypeTextPlain, }, { format: fmtOpenMetrics_0_0_1, expected: TypeOpenMetrics, }, { format: "application/vnd.google.protobuf; proto=BadProtocol; encoding=text", expected: TypeUnknown, }, { format: "application/vnd.google.protobuf", expected: TypeUnknown, }, { format: "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily=bad", expected: TypeUnknown, }, // encoding missing { format: "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily", expected: TypeUnknown, }, // invalid encoding { format: "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=textual", expected: TypeUnknown, }, // bad charset, must be utf-8 { format: "application/openmetrics-text; version=1.0.0; charset=ascii", expected: TypeUnknown, }, { format: "text/plain", expected: TypeTextPlain, }, { format: "text/plain; version=invalid", expected: TypeUnknown, }, { format: "gobbledygook", expected: TypeUnknown, }, } for _, test := range tests { if test.format.FormatType() != test.expected { t.Errorf("expected %v got %v", test.expected, test.format.FormatType()) } } } func TestToEscapingScheme(t *testing.T) { tests := []struct { format Format expected model.EscapingScheme }{ { format: fmtProtoCompact, expected: model.ValueEncodingEscaping, }, { format: "application/openmetrics-text; version=1.0.0; charset=utf-8; escaping=underscores", expected: model.UnderscoreEscaping, }, { format: "application/openmetrics-text; version=1.0.0; charset=utf-8; escaping=allow-utf-8", expected: model.NoEscaping, }, // error returns default { format: "application/openmetrics-text; version=1.0.0; charset=utf-8; escaping=invalid", expected: model.NameEscapingScheme, }, } for _, test := range tests { if test.format.ToEscapingScheme() != test.expected { t.Errorf("expected %v got %v", test.expected, test.format.ToEscapingScheme()) } } } golang-github-prometheus-common-0.55.0/expfmt/fuzz.go000066400000000000000000000020751463701437000226500ustar00rootroot00000000000000// Copyright 2014 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Build only when actually fuzzing //go:build gofuzz // +build gofuzz package expfmt import "bytes" // Fuzz text metric parser with with github.com/dvyukov/go-fuzz: // // go-fuzz-build github.com/prometheus/common/expfmt // go-fuzz -bin expfmt-fuzz.zip -workdir fuzz // // Further input samples should go in the folder fuzz/corpus. func Fuzz(in []byte) int { parser := TextParser{} _, err := parser.TextToMetricFamilies(bytes.NewReader(in)) if err != nil { return 0 } return 1 } golang-github-prometheus-common-0.55.0/expfmt/fuzz/000077500000000000000000000000001463701437000223155ustar00rootroot00000000000000golang-github-prometheus-common-0.55.0/expfmt/fuzz/corpus/000077500000000000000000000000001463701437000236305ustar00rootroot00000000000000golang-github-prometheus-common-0.55.0/expfmt/fuzz/corpus/from_test_parse_0000066400000000000000000000000021463701437000271560ustar00rootroot00000000000000 golang-github-prometheus-common-0.55.0/expfmt/fuzz/corpus/from_test_parse_1000066400000000000000000000002011463701437000271600ustar00rootroot00000000000000 minimal_metric 1.234 another_metric -3e3 103948 # Even that: no_labels{} 3 # HELP line for non-existing metric will be ignored. golang-github-prometheus-common-0.55.0/expfmt/fuzz/corpus/from_test_parse_2000066400000000000000000000005421463701437000271710ustar00rootroot00000000000000 # A normal comment. # # TYPE name counter name{labelname="val1",basename="basevalue"} NaN name {labelname="val2",basename="base\"v\\al\nue"} 0.23 1234567890 # HELP name two-line\n doc str\\ing # HELP name2 doc str"ing 2 # TYPE name2 gauge name2{labelname="val2" ,basename = "basevalue2" } +Inf 54321 name2{ labelname = "val1" , }-Inf golang-github-prometheus-common-0.55.0/expfmt/fuzz/corpus/from_test_parse_3000066400000000000000000000012011463701437000271630ustar00rootroot00000000000000 # TYPE my_summary summary my_summary{n1="val1",quantile="0.5"} 110 decoy -1 -2 my_summary{n1="val1",quantile="0.9"} 140 1 my_summary_count{n1="val1"} 42 # Latest timestamp wins in case of a summary. my_summary_sum{n1="val1"} 4711 2 fake_sum{n1="val1"} 2001 # TYPE another_summary summary another_summary_count{n2="val2",n1="val1"} 20 my_summary_count{n2="val2",n1="val1"} 5 5 another_summary{n1="val1",n2="val2",quantile=".3"} -1.2 my_summary_sum{n1="val2"} 08 15 my_summary{n1="val3", quantile="0.2"} 4711 my_summary{n1="val1",n2="val2",quantile="-12.34",} NaN # some # funny comments # HELP # HELP # HELP my_summary # HELP my_summary golang-github-prometheus-common-0.55.0/expfmt/fuzz/corpus/from_test_parse_4000066400000000000000000000007101463701437000271700ustar00rootroot00000000000000 # HELP request_duration_microseconds The response latency. # TYPE request_duration_microseconds histogram request_duration_microseconds_bucket{le="100"} 123 request_duration_microseconds_bucket{le="120"} 412 request_duration_microseconds_bucket{le="144"} 592 request_duration_microseconds_bucket{le="172.8"} 1524 request_duration_microseconds_bucket{le="+Inf"} 2693 request_duration_microseconds_sum 1.7560473e+06 request_duration_microseconds_count 2693 golang-github-prometheus-common-0.55.0/expfmt/fuzz/corpus/from_test_parse_error_0000066400000000000000000000000101463701437000303660ustar00rootroot00000000000000bla 3.14golang-github-prometheus-common-0.55.0/expfmt/fuzz/corpus/from_test_parse_error_1000066400000000000000000000000271463701437000303770ustar00rootroot00000000000000metric{label="\t"} 3.14golang-github-prometheus-common-0.55.0/expfmt/fuzz/corpus/from_test_parse_error_10000066400000000000000000000000351463701437000304560ustar00rootroot00000000000000metric{label="bla"} 3.14 2 3 golang-github-prometheus-common-0.55.0/expfmt/fuzz/corpus/from_test_parse_error_11000066400000000000000000000000321463701437000304540ustar00rootroot00000000000000metric{label="bla"} blubb golang-github-prometheus-common-0.55.0/expfmt/fuzz/corpus/from_test_parse_error_12000066400000000000000000000000451463701437000304610ustar00rootroot00000000000000 # HELP metric one # HELP metric two golang-github-prometheus-common-0.55.0/expfmt/fuzz/corpus/from_test_parse_error_13000066400000000000000000000000551463701437000304630ustar00rootroot00000000000000 # TYPE metric counter # TYPE metric untyped golang-github-prometheus-common-0.55.0/expfmt/fuzz/corpus/from_test_parse_error_14000066400000000000000000000000431463701437000304610ustar00rootroot00000000000000 metric 4.12 # TYPE metric counter golang-github-prometheus-common-0.55.0/expfmt/fuzz/corpus/from_test_parse_error_15000066400000000000000000000000231463701437000304600ustar00rootroot00000000000000 # TYPE metric bla golang-github-prometheus-common-0.55.0/expfmt/fuzz/corpus/from_test_parse_error_16000066400000000000000000000000201463701437000304560ustar00rootroot00000000000000 # TYPE met-ric golang-github-prometheus-common-0.55.0/expfmt/fuzz/corpus/from_test_parse_error_17000066400000000000000000000000421463701437000304630ustar00rootroot00000000000000@invalidmetric{label="bla"} 3.14 2golang-github-prometheus-common-0.55.0/expfmt/fuzz/corpus/from_test_parse_error_18000066400000000000000000000000241463701437000304640ustar00rootroot00000000000000{label="bla"} 3.14 2golang-github-prometheus-common-0.55.0/expfmt/fuzz/corpus/from_test_parse_error_19000066400000000000000000000000661463701437000304730ustar00rootroot00000000000000 # TYPE metric histogram metric_bucket{le="bla"} 3.14 golang-github-prometheus-common-0.55.0/expfmt/fuzz/corpus/from_test_parse_error_2000066400000000000000000000000371463701437000304010ustar00rootroot00000000000000 metric{label="new line"} 3.14 golang-github-prometheus-common-0.55.0/expfmt/fuzz/corpus/from_test_parse_error_3000066400000000000000000000000241463701437000303760ustar00rootroot00000000000000metric{@="bla"} 3.14golang-github-prometheus-common-0.55.0/expfmt/fuzz/corpus/from_test_parse_error_4000066400000000000000000000000331463701437000303770ustar00rootroot00000000000000metric{__name__="bla"} 3.14golang-github-prometheus-common-0.55.0/expfmt/fuzz/corpus/from_test_parse_error_5000066400000000000000000000000311463701437000303760ustar00rootroot00000000000000metric{label+="bla"} 3.14golang-github-prometheus-common-0.55.0/expfmt/fuzz/corpus/from_test_parse_error_6000066400000000000000000000000261463701437000304030ustar00rootroot00000000000000metric{label=bla} 3.14golang-github-prometheus-common-0.55.0/expfmt/fuzz/corpus/from_test_parse_error_7000066400000000000000000000000631463701437000304050ustar00rootroot00000000000000 # TYPE metric summary metric{quantile="bla"} 3.14 golang-github-prometheus-common-0.55.0/expfmt/fuzz/corpus/from_test_parse_error_8000066400000000000000000000000311463701437000304010ustar00rootroot00000000000000metric{label="bla"+} 3.14golang-github-prometheus-common-0.55.0/expfmt/fuzz/corpus/from_test_parse_error_9000066400000000000000000000000361463701437000304070ustar00rootroot00000000000000metric{label="bla"} 3.14 2.72 golang-github-prometheus-common-0.55.0/expfmt/fuzz/corpus/minimal000066400000000000000000000000061463701437000251750ustar00rootroot00000000000000m{} 0 golang-github-prometheus-common-0.55.0/expfmt/openmetrics_create.go000066400000000000000000000463721463701437000255350ustar00rootroot00000000000000// Copyright 2020 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package expfmt import ( "bufio" "bytes" "fmt" "io" "math" "strconv" "strings" "google.golang.org/protobuf/types/known/timestamppb" "github.com/prometheus/common/model" dto "github.com/prometheus/client_model/go" ) type encoderOption struct { withCreatedLines bool withUnit bool } type EncoderOption func(*encoderOption) // WithCreatedLines is an EncoderOption that configures the OpenMetrics encoder // to include _created lines (See // https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#counter-1). // Created timestamps can improve the accuracy of series reset detection, but // come with a bandwidth cost. // // At the time of writing, created timestamp ingestion is still experimental in // Prometheus and need to be enabled with the feature-flag // `--feature-flag=created-timestamp-zero-ingestion`, and breaking changes are // still possible. Therefore, it is recommended to use this feature with caution. func WithCreatedLines() EncoderOption { return func(t *encoderOption) { t.withCreatedLines = true } } // WithUnit is an EncoderOption enabling a set unit to be written to the output // and to be added to the metric name, if it's not there already, as a suffix. // Without opting in this way, the unit will not be added to the metric name and, // on top of that, the unit will not be passed onto the output, even if it // were declared in the *dto.MetricFamily struct, i.e. even if in.Unit !=nil. func WithUnit() EncoderOption { return func(t *encoderOption) { t.withUnit = true } } // MetricFamilyToOpenMetrics converts a MetricFamily proto message into the // OpenMetrics text format and writes the resulting lines to 'out'. It returns // the number of bytes written and any error encountered. The output will have // the same order as the input, no further sorting is performed. Furthermore, // this function assumes the input is already sanitized and does not perform any // sanity checks. If the input contains duplicate metrics or invalid metric or // label names, the conversion will result in invalid text format output. // // If metric names conform to the legacy validation pattern, they will be placed // outside the brackets in the traditional way, like `foo{}`. If the metric name // fails the legacy validation check, it will be placed quoted inside the // brackets: `{"foo"}`. As stated above, the input is assumed to be santized and // no error will be thrown in this case. // // Similar to metric names, if label names conform to the legacy validation // pattern, they will be unquoted as normal, like `foo{bar="baz"}`. If the label // name fails the legacy validation check, it will be quoted: // `foo{"bar"="baz"}`. As stated above, the input is assumed to be santized and // no error will be thrown in this case. // // This function fulfills the type 'expfmt.encoder'. // // Note that OpenMetrics requires a final `# EOF` line. Since this function acts // on individual metric families, it is the responsibility of the caller to // append this line to 'out' once all metric families have been written. // Conveniently, this can be done by calling FinalizeOpenMetrics. // // The output should be fully OpenMetrics compliant. However, there are a few // missing features and peculiarities to avoid complications when switching from // Prometheus to OpenMetrics or vice versa: // // - Counters are expected to have the `_total` suffix in their metric name. In // the output, the suffix will be truncated from the `# TYPE`, `# HELP` and `# UNIT` // lines. A counter with a missing `_total` suffix is not an error. However, // its type will be set to `unknown` in that case to avoid invalid OpenMetrics // output. // // - According to the OM specs, the `# UNIT` line is optional, but if populated, // the unit has to be present in the metric name as its suffix: // (see https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#unit). // However, in order to accommodate any potential scenario where such a change in the // metric name is not desirable, the users are here given the choice of either explicitly // opt in, in case they wish for the unit to be included in the output AND in the metric name // as a suffix (see the description of the WithUnit function above), // or not to opt in, in case they don't want for any of that to happen. // // - No support for the following (optional) features: info type, // stateset type, gaugehistogram type. // // - The size of exemplar labels is not checked (i.e. it's possible to create // exemplars that are larger than allowed by the OpenMetrics specification). // // - The value of Counters is not checked. (OpenMetrics doesn't allow counters // with a `NaN` value.) func MetricFamilyToOpenMetrics(out io.Writer, in *dto.MetricFamily, options ...EncoderOption) (written int, err error) { toOM := encoderOption{} for _, option := range options { option(&toOM) } name := in.GetName() if name == "" { return 0, fmt.Errorf("MetricFamily has no name: %s", in) } // Try the interface upgrade. If it doesn't work, we'll use a // bufio.Writer from the sync.Pool. w, ok := out.(enhancedWriter) if !ok { b := bufPool.Get().(*bufio.Writer) b.Reset(out) w = b defer func() { bErr := b.Flush() if err == nil { err = bErr } bufPool.Put(b) }() } var ( n int metricType = in.GetType() compliantName = name ) if metricType == dto.MetricType_COUNTER && strings.HasSuffix(compliantName, "_total") { compliantName = name[:len(name)-6] } if toOM.withUnit && in.Unit != nil && !strings.HasSuffix(compliantName, fmt.Sprintf("_%s", *in.Unit)) { compliantName = compliantName + fmt.Sprintf("_%s", *in.Unit) } // Comments, first HELP, then TYPE. if in.Help != nil { n, err = w.WriteString("# HELP ") written += n if err != nil { return } n, err = writeName(w, compliantName) written += n if err != nil { return } err = w.WriteByte(' ') written++ if err != nil { return } n, err = writeEscapedString(w, *in.Help, true) written += n if err != nil { return } err = w.WriteByte('\n') written++ if err != nil { return } } n, err = w.WriteString("# TYPE ") written += n if err != nil { return } n, err = writeName(w, compliantName) written += n if err != nil { return } switch metricType { case dto.MetricType_COUNTER: if strings.HasSuffix(name, "_total") { n, err = w.WriteString(" counter\n") } else { n, err = w.WriteString(" unknown\n") } case dto.MetricType_GAUGE: n, err = w.WriteString(" gauge\n") case dto.MetricType_SUMMARY: n, err = w.WriteString(" summary\n") case dto.MetricType_UNTYPED: n, err = w.WriteString(" unknown\n") case dto.MetricType_HISTOGRAM: n, err = w.WriteString(" histogram\n") default: return written, fmt.Errorf("unknown metric type %s", metricType.String()) } written += n if err != nil { return } if toOM.withUnit && in.Unit != nil { n, err = w.WriteString("# UNIT ") written += n if err != nil { return } n, err = writeName(w, compliantName) written += n if err != nil { return } err = w.WriteByte(' ') written++ if err != nil { return } n, err = writeEscapedString(w, *in.Unit, true) written += n if err != nil { return } err = w.WriteByte('\n') written++ if err != nil { return } } var createdTsBytesWritten int // Finally the samples, one line for each. if metricType == dto.MetricType_COUNTER && strings.HasSuffix(name, "_total") { compliantName = compliantName + "_total" } for _, metric := range in.Metric { switch metricType { case dto.MetricType_COUNTER: if metric.Counter == nil { return written, fmt.Errorf( "expected counter in metric %s %s", compliantName, metric, ) } n, err = writeOpenMetricsSample( w, compliantName, "", metric, "", 0, metric.Counter.GetValue(), 0, false, metric.Counter.Exemplar, ) if toOM.withCreatedLines && metric.Counter.CreatedTimestamp != nil { createdTsBytesWritten, err = writeOpenMetricsCreated(w, compliantName, "_total", metric, "", 0, metric.Counter.GetCreatedTimestamp()) n += createdTsBytesWritten } case dto.MetricType_GAUGE: if metric.Gauge == nil { return written, fmt.Errorf( "expected gauge in metric %s %s", compliantName, metric, ) } n, err = writeOpenMetricsSample( w, compliantName, "", metric, "", 0, metric.Gauge.GetValue(), 0, false, nil, ) case dto.MetricType_UNTYPED: if metric.Untyped == nil { return written, fmt.Errorf( "expected untyped in metric %s %s", compliantName, metric, ) } n, err = writeOpenMetricsSample( w, compliantName, "", metric, "", 0, metric.Untyped.GetValue(), 0, false, nil, ) case dto.MetricType_SUMMARY: if metric.Summary == nil { return written, fmt.Errorf( "expected summary in metric %s %s", compliantName, metric, ) } for _, q := range metric.Summary.Quantile { n, err = writeOpenMetricsSample( w, compliantName, "", metric, model.QuantileLabel, q.GetQuantile(), q.GetValue(), 0, false, nil, ) written += n if err != nil { return } } n, err = writeOpenMetricsSample( w, compliantName, "_sum", metric, "", 0, metric.Summary.GetSampleSum(), 0, false, nil, ) written += n if err != nil { return } n, err = writeOpenMetricsSample( w, compliantName, "_count", metric, "", 0, 0, metric.Summary.GetSampleCount(), true, nil, ) if toOM.withCreatedLines && metric.Summary.CreatedTimestamp != nil { createdTsBytesWritten, err = writeOpenMetricsCreated(w, compliantName, "", metric, "", 0, metric.Summary.GetCreatedTimestamp()) n += createdTsBytesWritten } case dto.MetricType_HISTOGRAM: if metric.Histogram == nil { return written, fmt.Errorf( "expected histogram in metric %s %s", compliantName, metric, ) } infSeen := false for _, b := range metric.Histogram.Bucket { n, err = writeOpenMetricsSample( w, compliantName, "_bucket", metric, model.BucketLabel, b.GetUpperBound(), 0, b.GetCumulativeCount(), true, b.Exemplar, ) written += n if err != nil { return } if math.IsInf(b.GetUpperBound(), +1) { infSeen = true } } if !infSeen { n, err = writeOpenMetricsSample( w, compliantName, "_bucket", metric, model.BucketLabel, math.Inf(+1), 0, metric.Histogram.GetSampleCount(), true, nil, ) written += n if err != nil { return } } n, err = writeOpenMetricsSample( w, compliantName, "_sum", metric, "", 0, metric.Histogram.GetSampleSum(), 0, false, nil, ) written += n if err != nil { return } n, err = writeOpenMetricsSample( w, compliantName, "_count", metric, "", 0, 0, metric.Histogram.GetSampleCount(), true, nil, ) if toOM.withCreatedLines && metric.Histogram.CreatedTimestamp != nil { createdTsBytesWritten, err = writeOpenMetricsCreated(w, compliantName, "", metric, "", 0, metric.Histogram.GetCreatedTimestamp()) n += createdTsBytesWritten } default: return written, fmt.Errorf( "unexpected type in metric %s %s", compliantName, metric, ) } written += n if err != nil { return } } return } // FinalizeOpenMetrics writes the final `# EOF\n` line required by OpenMetrics. func FinalizeOpenMetrics(w io.Writer) (written int, err error) { return w.Write([]byte("# EOF\n")) } // writeOpenMetricsSample writes a single sample in OpenMetrics text format to // w, given the metric name, the metric proto message itself, optionally an // additional label name with a float64 value (use empty string as label name if // not required), the value (optionally as float64 or uint64, determined by // useIntValue), and optionally an exemplar (use nil if not required). The // function returns the number of bytes written and any error encountered. func writeOpenMetricsSample( w enhancedWriter, name, suffix string, metric *dto.Metric, additionalLabelName string, additionalLabelValue float64, floatValue float64, intValue uint64, useIntValue bool, exemplar *dto.Exemplar, ) (int, error) { written := 0 n, err := writeOpenMetricsNameAndLabelPairs( w, name+suffix, metric.Label, additionalLabelName, additionalLabelValue, ) written += n if err != nil { return written, err } err = w.WriteByte(' ') written++ if err != nil { return written, err } if useIntValue { n, err = writeUint(w, intValue) } else { n, err = writeOpenMetricsFloat(w, floatValue) } written += n if err != nil { return written, err } if metric.TimestampMs != nil { err = w.WriteByte(' ') written++ if err != nil { return written, err } // TODO(beorn7): Format this directly without converting to a float first. n, err = writeOpenMetricsFloat(w, float64(*metric.TimestampMs)/1000) written += n if err != nil { return written, err } } if exemplar != nil && len(exemplar.Label) > 0 { n, err = writeExemplar(w, exemplar) written += n if err != nil { return written, err } } err = w.WriteByte('\n') written++ if err != nil { return written, err } return written, nil } // writeOpenMetricsNameAndLabelPairs works like writeOpenMetricsSample but // formats the float in OpenMetrics style. func writeOpenMetricsNameAndLabelPairs( w enhancedWriter, name string, in []*dto.LabelPair, additionalLabelName string, additionalLabelValue float64, ) (int, error) { var ( written int separator byte = '{' metricInsideBraces = false ) if name != "" { // If the name does not pass the legacy validity check, we must put the // metric name inside the braces, quoted. if !model.IsValidLegacyMetricName(model.LabelValue(name)) { metricInsideBraces = true err := w.WriteByte(separator) written++ if err != nil { return written, err } separator = ',' } n, err := writeName(w, name) written += n if err != nil { return written, err } } if len(in) == 0 && additionalLabelName == "" { if metricInsideBraces { err := w.WriteByte('}') written++ if err != nil { return written, err } } return written, nil } for _, lp := range in { err := w.WriteByte(separator) written++ if err != nil { return written, err } n, err := writeName(w, lp.GetName()) written += n if err != nil { return written, err } n, err = w.WriteString(`="`) written += n if err != nil { return written, err } n, err = writeEscapedString(w, lp.GetValue(), true) written += n if err != nil { return written, err } err = w.WriteByte('"') written++ if err != nil { return written, err } separator = ',' } if additionalLabelName != "" { err := w.WriteByte(separator) written++ if err != nil { return written, err } n, err := w.WriteString(additionalLabelName) written += n if err != nil { return written, err } n, err = w.WriteString(`="`) written += n if err != nil { return written, err } n, err = writeOpenMetricsFloat(w, additionalLabelValue) written += n if err != nil { return written, err } err = w.WriteByte('"') written++ if err != nil { return written, err } } err := w.WriteByte('}') written++ if err != nil { return written, err } return written, nil } // writeOpenMetricsCreated writes the created timestamp for a single time series // following OpenMetrics text format to w, given the metric name, the metric proto // message itself, optionally a suffix to be removed, e.g. '_total' for counters, // an additional label name with a float64 value (use empty string as label name if // not required) and the timestamp that represents the created timestamp. // The function returns the number of bytes written and any error encountered. func writeOpenMetricsCreated(w enhancedWriter, name, suffixToTrim string, metric *dto.Metric, additionalLabelName string, additionalLabelValue float64, createdTimestamp *timestamppb.Timestamp, ) (int, error) { written := 0 n, err := writeOpenMetricsNameAndLabelPairs( w, strings.TrimSuffix(name, suffixToTrim)+"_created", metric.Label, additionalLabelName, additionalLabelValue, ) written += n if err != nil { return written, err } err = w.WriteByte(' ') written++ if err != nil { return written, err } // TODO(beorn7): Format this directly from components of ts to // avoid overflow/underflow and precision issues of the float // conversion. n, err = writeOpenMetricsFloat(w, float64(createdTimestamp.AsTime().UnixNano())/1e9) written += n if err != nil { return written, err } err = w.WriteByte('\n') written++ if err != nil { return written, err } return written, nil } // writeExemplar writes the provided exemplar in OpenMetrics format to w. The // function returns the number of bytes written and any error encountered. func writeExemplar(w enhancedWriter, e *dto.Exemplar) (int, error) { written := 0 n, err := w.WriteString(" # ") written += n if err != nil { return written, err } n, err = writeOpenMetricsNameAndLabelPairs(w, "", e.Label, "", 0) written += n if err != nil { return written, err } err = w.WriteByte(' ') written++ if err != nil { return written, err } n, err = writeOpenMetricsFloat(w, e.GetValue()) written += n if err != nil { return written, err } if e.Timestamp != nil { err = w.WriteByte(' ') written++ if err != nil { return written, err } err = (*e).Timestamp.CheckValid() if err != nil { return written, err } ts := (*e).Timestamp.AsTime() // TODO(beorn7): Format this directly from components of ts to // avoid overflow/underflow and precision issues of the float // conversion. n, err = writeOpenMetricsFloat(w, float64(ts.UnixNano())/1e9) written += n if err != nil { return written, err } } return written, nil } // writeOpenMetricsFloat works like writeFloat but appends ".0" if the resulting // number would otherwise contain neither a "." nor an "e". func writeOpenMetricsFloat(w enhancedWriter, f float64) (int, error) { switch { case f == 1: return w.WriteString("1.0") case f == 0: return w.WriteString("0.0") case f == -1: return w.WriteString("-1.0") case math.IsNaN(f): return w.WriteString("NaN") case math.IsInf(f, +1): return w.WriteString("+Inf") case math.IsInf(f, -1): return w.WriteString("-Inf") default: bp := numBufPool.Get().(*[]byte) *bp = strconv.AppendFloat((*bp)[:0], f, 'g', -1, 64) if !bytes.ContainsAny(*bp, "e.") { *bp = append(*bp, '.', '0') } written, err := w.Write(*bp) numBufPool.Put(bp) return written, err } } // writeUint is like writeInt just for uint64. func writeUint(w enhancedWriter, u uint64) (int, error) { bp := numBufPool.Get().(*[]byte) *bp = strconv.AppendUint((*bp)[:0], u, 10) written, err := w.Write(*bp) numBufPool.Put(bp) return written, err } golang-github-prometheus-common-0.55.0/expfmt/openmetrics_create_test.go000066400000000000000000000604371463701437000265720ustar00rootroot00000000000000// Copyright 2020 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package expfmt import ( "bytes" "math" "strings" "testing" "time" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/timestamppb" dto "github.com/prometheus/client_model/go" "github.com/prometheus/common/model" ) func TestCreateOpenMetrics(t *testing.T) { openMetricsTimestamp := timestamppb.New(time.Unix(12345, 600000000)) if err := openMetricsTimestamp.CheckValid(); err != nil { t.Error(err) } oldDefaultScheme := model.NameEscapingScheme model.NameEscapingScheme = model.NoEscaping defer func() { model.NameEscapingScheme = oldDefaultScheme }() scenarios := []struct { in *dto.MetricFamily options []EncoderOption out string }{ // 0: Counter, timestamp given, no _total suffix. { in: &dto.MetricFamily{ Name: proto.String("name"), Help: proto.String("two-line\n doc str\\ing"), Type: dto.MetricType_COUNTER.Enum(), Metric: []*dto.Metric{ { Label: []*dto.LabelPair{ { Name: proto.String("labelname"), Value: proto.String("val1"), }, { Name: proto.String("basename"), Value: proto.String("basevalue"), }, }, Counter: &dto.Counter{ Value: proto.Float64(42), }, }, { Label: []*dto.LabelPair{ { Name: proto.String("labelname"), Value: proto.String("val2"), }, { Name: proto.String("basename"), Value: proto.String("basevalue"), }, }, Counter: &dto.Counter{ Value: proto.Float64(.23), }, TimestampMs: proto.Int64(1234567890), }, }, }, out: `# HELP name two-line\n doc str\\ing # TYPE name unknown name{labelname="val1",basename="basevalue"} 42.0 name{labelname="val2",basename="basevalue"} 0.23 1.23456789e+06 `, }, // 1: Dots in name { in: &dto.MetricFamily{ Name: proto.String("name.with.dots"), Help: proto.String("boring help"), Type: dto.MetricType_COUNTER.Enum(), Metric: []*dto.Metric{ { Label: []*dto.LabelPair{ { Name: proto.String("labelname"), Value: proto.String("val1"), }, { Name: proto.String("basename"), Value: proto.String("basevalue"), }, }, Counter: &dto.Counter{ Value: proto.Float64(42), }, }, { Label: []*dto.LabelPair{ { Name: proto.String("labelname"), Value: proto.String("val2"), }, { Name: proto.String("basename"), Value: proto.String("basevalue"), }, }, Counter: &dto.Counter{ Value: proto.Float64(.23), }, TimestampMs: proto.Int64(1234567890), }, }, }, out: `# HELP "name.with.dots" boring help # TYPE "name.with.dots" unknown {"name.with.dots",labelname="val1",basename="basevalue"} 42.0 {"name.with.dots",labelname="val2",basename="basevalue"} 0.23 1.23456789e+06 `, }, // 2: Dots in name, no labels { in: &dto.MetricFamily{ Name: proto.String("name.with.dots"), Help: proto.String("boring help"), Type: dto.MetricType_COUNTER.Enum(), Metric: []*dto.Metric{ { Counter: &dto.Counter{ Value: proto.Float64(42), }, }, { Counter: &dto.Counter{ Value: proto.Float64(.23), }, TimestampMs: proto.Int64(1234567890), }, }, }, out: `# HELP "name.with.dots" boring help # TYPE "name.with.dots" unknown {"name.with.dots"} 42.0 {"name.with.dots"} 0.23 1.23456789e+06 `, }, // 3: Gauge, some escaping required, +Inf as value, multi-byte characters in label values. { in: &dto.MetricFamily{ Name: proto.String("gauge_name"), Help: proto.String("gauge\ndoc\nstr\"ing"), Type: dto.MetricType_GAUGE.Enum(), Metric: []*dto.Metric{ { Label: []*dto.LabelPair{ { Name: proto.String("name_1"), Value: proto.String("val with\nnew line"), }, { Name: proto.String("name_2"), Value: proto.String("val with \\backslash and \"quotes\""), }, }, Gauge: &dto.Gauge{ Value: proto.Float64(math.Inf(+1)), }, }, { Label: []*dto.LabelPair{ { Name: proto.String("name_1"), Value: proto.String("Björn"), }, { Name: proto.String("name_2"), Value: proto.String("佖佥"), }, }, Gauge: &dto.Gauge{ Value: proto.Float64(3.14e42), }, }, }, }, out: `# HELP gauge_name gauge\ndoc\nstr\"ing # TYPE gauge_name gauge gauge_name{name_1="val with\nnew line",name_2="val with \\backslash and \"quotes\""} +Inf gauge_name{name_1="Björn",name_2="佖佥"} 3.14e+42 `, }, // 4: Gauge, utf-8, some escaping required, +Inf as value, multi-byte characters in label values. { in: &dto.MetricFamily{ Name: proto.String("gauge.name\""), Help: proto.String("gauge\ndoc\nstr\"ing"), Type: dto.MetricType_GAUGE.Enum(), Metric: []*dto.Metric{ { Label: []*dto.LabelPair{ { Name: proto.String("name.1"), Value: proto.String("val with\nnew line"), }, { Name: proto.String("name*2"), Value: proto.String("val with \\backslash and \"quotes\""), }, }, Gauge: &dto.Gauge{ Value: proto.Float64(math.Inf(+1)), }, }, { Label: []*dto.LabelPair{ { Name: proto.String("name.1"), Value: proto.String("Björn"), }, { Name: proto.String("name*2"), Value: proto.String("佖佥"), }, }, Gauge: &dto.Gauge{ Value: proto.Float64(3.14e42), }, }, }, }, out: `# HELP "gauge.name\"" gauge\ndoc\nstr\"ing # TYPE "gauge.name\"" gauge {"gauge.name\"","name.1"="val with\nnew line","name*2"="val with \\backslash and \"quotes\""} +Inf {"gauge.name\"","name.1"="Björn","name*2"="佖佥"} 3.14e+42 `, }, // 5: Unknown, no help, one sample with no labels and -Inf as value, another sample with one label. { in: &dto.MetricFamily{ Name: proto.String("unknown_name"), Type: dto.MetricType_UNTYPED.Enum(), Metric: []*dto.Metric{ { Untyped: &dto.Untyped{ Value: proto.Float64(math.Inf(-1)), }, }, { Label: []*dto.LabelPair{ { Name: proto.String("name_1"), Value: proto.String("value 1"), }, }, Untyped: &dto.Untyped{ Value: proto.Float64(-1.23e-45), }, }, }, }, out: `# TYPE unknown_name unknown unknown_name -Inf unknown_name{name_1="value 1"} -1.23e-45 `, }, // 6: Summary. { in: &dto.MetricFamily{ Name: proto.String("summary_name"), Help: proto.String("summary docstring"), Type: dto.MetricType_SUMMARY.Enum(), Metric: []*dto.Metric{ { Summary: &dto.Summary{ SampleCount: proto.Uint64(42), SampleSum: proto.Float64(-3.4567), Quantile: []*dto.Quantile{ { Quantile: proto.Float64(0.5), Value: proto.Float64(-1.23), }, { Quantile: proto.Float64(0.9), Value: proto.Float64(.2342354), }, { Quantile: proto.Float64(0.99), Value: proto.Float64(0), }, }, CreatedTimestamp: openMetricsTimestamp, }, }, { Label: []*dto.LabelPair{ { Name: proto.String("name_1"), Value: proto.String("value 1"), }, { Name: proto.String("name_2"), Value: proto.String("value 2"), }, }, Summary: &dto.Summary{ SampleCount: proto.Uint64(4711), SampleSum: proto.Float64(2010.1971), Quantile: []*dto.Quantile{ { Quantile: proto.Float64(0.5), Value: proto.Float64(1), }, { Quantile: proto.Float64(0.9), Value: proto.Float64(2), }, { Quantile: proto.Float64(0.99), Value: proto.Float64(3), }, }, CreatedTimestamp: openMetricsTimestamp, }, }, }, }, options: []EncoderOption{WithCreatedLines()}, out: `# HELP summary_name summary docstring # TYPE summary_name summary summary_name{quantile="0.5"} -1.23 summary_name{quantile="0.9"} 0.2342354 summary_name{quantile="0.99"} 0.0 summary_name_sum -3.4567 summary_name_count 42 summary_name_created 12345.6 summary_name{name_1="value 1",name_2="value 2",quantile="0.5"} 1.0 summary_name{name_1="value 1",name_2="value 2",quantile="0.9"} 2.0 summary_name{name_1="value 1",name_2="value 2",quantile="0.99"} 3.0 summary_name_sum{name_1="value 1",name_2="value 2"} 2010.1971 summary_name_count{name_1="value 1",name_2="value 2"} 4711 summary_name_created{name_1="value 1",name_2="value 2"} 12345.6 `, }, // 7: Histogram { in: &dto.MetricFamily{ Name: proto.String("request_duration_microseconds"), Help: proto.String("The response latency."), Type: dto.MetricType_HISTOGRAM.Enum(), Unit: proto.String("microseconds"), Metric: []*dto.Metric{ { Histogram: &dto.Histogram{ SampleCount: proto.Uint64(2693), SampleSum: proto.Float64(1756047.3), Bucket: []*dto.Bucket{ { UpperBound: proto.Float64(100), CumulativeCount: proto.Uint64(123), }, { UpperBound: proto.Float64(120), CumulativeCount: proto.Uint64(412), }, { UpperBound: proto.Float64(144), CumulativeCount: proto.Uint64(592), }, { UpperBound: proto.Float64(172.8), CumulativeCount: proto.Uint64(1524), }, { UpperBound: proto.Float64(math.Inf(+1)), CumulativeCount: proto.Uint64(2693), }, }, CreatedTimestamp: openMetricsTimestamp, }, }, }, }, options: []EncoderOption{WithCreatedLines(), WithUnit()}, out: `# HELP request_duration_microseconds The response latency. # TYPE request_duration_microseconds histogram # UNIT request_duration_microseconds microseconds request_duration_microseconds_bucket{le="100.0"} 123 request_duration_microseconds_bucket{le="120.0"} 412 request_duration_microseconds_bucket{le="144.0"} 592 request_duration_microseconds_bucket{le="172.8"} 1524 request_duration_microseconds_bucket{le="+Inf"} 2693 request_duration_microseconds_sum 1.7560473e+06 request_duration_microseconds_count 2693 request_duration_microseconds_created 12345.6 `, }, // 8: Histogram with missing +Inf bucket. { in: &dto.MetricFamily{ Name: proto.String("request_duration_microseconds"), Help: proto.String("The response latency."), Type: dto.MetricType_HISTOGRAM.Enum(), Unit: proto.String("microseconds"), Metric: []*dto.Metric{ { Histogram: &dto.Histogram{ SampleCount: proto.Uint64(2693), SampleSum: proto.Float64(1756047.3), Bucket: []*dto.Bucket{ { UpperBound: proto.Float64(100), CumulativeCount: proto.Uint64(123), }, { UpperBound: proto.Float64(120), CumulativeCount: proto.Uint64(412), }, { UpperBound: proto.Float64(144), CumulativeCount: proto.Uint64(592), }, { UpperBound: proto.Float64(172.8), CumulativeCount: proto.Uint64(1524), }, }, }, }, }, }, out: `# HELP request_duration_microseconds The response latency. # TYPE request_duration_microseconds histogram request_duration_microseconds_bucket{le="100.0"} 123 request_duration_microseconds_bucket{le="120.0"} 412 request_duration_microseconds_bucket{le="144.0"} 592 request_duration_microseconds_bucket{le="172.8"} 1524 request_duration_microseconds_bucket{le="+Inf"} 2693 request_duration_microseconds_sum 1.7560473e+06 request_duration_microseconds_count 2693 `, }, // 9: Histogram with missing +Inf bucket but with different exemplars. { in: &dto.MetricFamily{ Name: proto.String("request_duration_microseconds"), Help: proto.String("The response latency."), Type: dto.MetricType_HISTOGRAM.Enum(), Metric: []*dto.Metric{ { Histogram: &dto.Histogram{ SampleCount: proto.Uint64(2693), SampleSum: proto.Float64(1756047.3), Bucket: []*dto.Bucket{ { UpperBound: proto.Float64(100), CumulativeCount: proto.Uint64(123), }, { UpperBound: proto.Float64(120), CumulativeCount: proto.Uint64(412), Exemplar: &dto.Exemplar{ Label: []*dto.LabelPair{ { Name: proto.String("foo"), Value: proto.String("bar"), }, }, Value: proto.Float64(119.9), Timestamp: openMetricsTimestamp, }, }, { UpperBound: proto.Float64(144), CumulativeCount: proto.Uint64(592), Exemplar: &dto.Exemplar{ Label: []*dto.LabelPair{ { Name: proto.String("foo"), Value: proto.String("baz"), }, { Name: proto.String("dings"), Value: proto.String("bums"), }, }, Value: proto.Float64(140.14), }, }, { UpperBound: proto.Float64(172.8), CumulativeCount: proto.Uint64(1524), }, }, }, }, }, }, out: `# HELP request_duration_microseconds The response latency. # TYPE request_duration_microseconds histogram request_duration_microseconds_bucket{le="100.0"} 123 request_duration_microseconds_bucket{le="120.0"} 412 # {foo="bar"} 119.9 12345.6 request_duration_microseconds_bucket{le="144.0"} 592 # {foo="baz",dings="bums"} 140.14 request_duration_microseconds_bucket{le="172.8"} 1524 request_duration_microseconds_bucket{le="+Inf"} 2693 request_duration_microseconds_sum 1.7560473e+06 request_duration_microseconds_count 2693 `, }, // 10: Simple Counter. { in: &dto.MetricFamily{ Name: proto.String("foos_total"), Help: proto.String("Number of foos."), Type: dto.MetricType_COUNTER.Enum(), Metric: []*dto.Metric{ { Counter: &dto.Counter{ Value: proto.Float64(42), CreatedTimestamp: openMetricsTimestamp, }, }, }, }, options: []EncoderOption{WithCreatedLines()}, out: `# HELP foos Number of foos. # TYPE foos counter foos_total 42.0 foos_created 12345.6 `, }, // 11: Simple Counter without created line. { in: &dto.MetricFamily{ Name: proto.String("foos_total"), Help: proto.String("Number of foos."), Type: dto.MetricType_COUNTER.Enum(), Metric: []*dto.Metric{ { Counter: &dto.Counter{ Value: proto.Float64(42), CreatedTimestamp: openMetricsTimestamp, }, }, }, }, out: `# HELP foos Number of foos. # TYPE foos counter foos_total 42.0 `, }, // 12: No metric. { in: &dto.MetricFamily{ Name: proto.String("name_total"), Help: proto.String("doc string"), Type: dto.MetricType_COUNTER.Enum(), Metric: []*dto.Metric{}, }, out: `# HELP name doc string # TYPE name counter `, }, // 13: Simple Counter with exemplar that has empty label set: // ignore the exemplar, since OpenMetrics spec requires labels. { in: &dto.MetricFamily{ Name: proto.String("foos_total"), Help: proto.String("Number of foos."), Type: dto.MetricType_COUNTER.Enum(), Metric: []*dto.Metric{ { Counter: &dto.Counter{ Value: proto.Float64(42), Exemplar: &dto.Exemplar{ Label: []*dto.LabelPair{}, Value: proto.Float64(1), Timestamp: openMetricsTimestamp, }, }, }, }, }, out: `# HELP foos Number of foos. # TYPE foos counter foos_total 42.0 `, }, // 14: No metric plus unit. { in: &dto.MetricFamily{ Name: proto.String("name_seconds_total"), Help: proto.String("doc string"), Type: dto.MetricType_COUNTER.Enum(), Unit: proto.String("seconds"), Metric: []*dto.Metric{}, }, options: []EncoderOption{WithUnit()}, out: `# HELP name_seconds doc string # TYPE name_seconds counter # UNIT name_seconds seconds `, }, // 15: Histogram plus unit, but unit not opted in. { in: &dto.MetricFamily{ Name: proto.String("request_duration_microseconds"), Help: proto.String("The response latency."), Type: dto.MetricType_HISTOGRAM.Enum(), Unit: proto.String("microseconds"), Metric: []*dto.Metric{ { Histogram: &dto.Histogram{ SampleCount: proto.Uint64(2693), SampleSum: proto.Float64(1756047.3), Bucket: []*dto.Bucket{ { UpperBound: proto.Float64(100), CumulativeCount: proto.Uint64(123), }, { UpperBound: proto.Float64(120), CumulativeCount: proto.Uint64(412), }, { UpperBound: proto.Float64(144), CumulativeCount: proto.Uint64(592), }, { UpperBound: proto.Float64(172.8), CumulativeCount: proto.Uint64(1524), }, { UpperBound: proto.Float64(math.Inf(+1)), CumulativeCount: proto.Uint64(2693), }, }, }, }, }, }, out: `# HELP request_duration_microseconds The response latency. # TYPE request_duration_microseconds histogram request_duration_microseconds_bucket{le="100.0"} 123 request_duration_microseconds_bucket{le="120.0"} 412 request_duration_microseconds_bucket{le="144.0"} 592 request_duration_microseconds_bucket{le="172.8"} 1524 request_duration_microseconds_bucket{le="+Inf"} 2693 request_duration_microseconds_sum 1.7560473e+06 request_duration_microseconds_count 2693 `, }, // 16: No metric, unit opted in, no unit in name. { in: &dto.MetricFamily{ Name: proto.String("name_total"), Help: proto.String("doc string"), Type: dto.MetricType_COUNTER.Enum(), Unit: proto.String("seconds"), Metric: []*dto.Metric{}, }, options: []EncoderOption{WithUnit()}, out: `# HELP name_seconds doc string # TYPE name_seconds counter # UNIT name_seconds seconds `, }, // 17: No metric, unit opted in, BUT unit == nil. { in: &dto.MetricFamily{ Name: proto.String("name_total"), Help: proto.String("doc string"), Type: dto.MetricType_COUNTER.Enum(), Metric: []*dto.Metric{}, }, options: []EncoderOption{WithUnit()}, out: `# HELP name doc string # TYPE name counter `, }, // 18: Counter, timestamp given, unit opted in, _total suffix. { in: &dto.MetricFamily{ Name: proto.String("some_measure_total"), Help: proto.String("some testing measurement"), Type: dto.MetricType_COUNTER.Enum(), Unit: proto.String("seconds"), Metric: []*dto.Metric{ { Label: []*dto.LabelPair{ { Name: proto.String("labelname"), Value: proto.String("val1"), }, { Name: proto.String("basename"), Value: proto.String("basevalue"), }, }, Counter: &dto.Counter{ Value: proto.Float64(42), }, }, { Label: []*dto.LabelPair{ { Name: proto.String("labelname"), Value: proto.String("val2"), }, { Name: proto.String("basename"), Value: proto.String("basevalue"), }, }, Counter: &dto.Counter{ Value: proto.Float64(.23), }, TimestampMs: proto.Int64(1234567890), }, }, }, options: []EncoderOption{WithUnit()}, out: `# HELP some_measure_seconds some testing measurement # TYPE some_measure_seconds counter # UNIT some_measure_seconds seconds some_measure_seconds_total{labelname="val1",basename="basevalue"} 42.0 some_measure_seconds_total{labelname="val2",basename="basevalue"} 0.23 1.23456789e+06 `, }, } for i, scenario := range scenarios { out := bytes.NewBuffer(make([]byte, 0, len(scenario.out))) n, err := MetricFamilyToOpenMetrics(out, scenario.in, scenario.options...) if err != nil { t.Errorf("%d. error: %s", i, err) continue } if expected, got := len(scenario.out), n; expected != got { t.Errorf( "%d. expected %d bytes written, got %d", i, expected, got, ) } if expected, got := scenario.out, out.String(); expected != got { t.Errorf( "%d. expected out=%q, got %q", i, expected, got, ) } } } func BenchmarkOpenMetricsCreate(b *testing.B) { mf := &dto.MetricFamily{ Name: proto.String("request_duration_microseconds"), Help: proto.String("The response latency."), Type: dto.MetricType_HISTOGRAM.Enum(), Metric: []*dto.Metric{ { Label: []*dto.LabelPair{ { Name: proto.String("name_1"), Value: proto.String("val with\nnew line"), }, { Name: proto.String("name_2"), Value: proto.String("val with \\backslash and \"quotes\""), }, { Name: proto.String("name_3"), Value: proto.String("Just a quite long label value to test performance."), }, }, Histogram: &dto.Histogram{ SampleCount: proto.Uint64(2693), SampleSum: proto.Float64(1756047.3), Bucket: []*dto.Bucket{ { UpperBound: proto.Float64(100), CumulativeCount: proto.Uint64(123), }, { UpperBound: proto.Float64(120), CumulativeCount: proto.Uint64(412), }, { UpperBound: proto.Float64(144), CumulativeCount: proto.Uint64(592), }, { UpperBound: proto.Float64(172.8), CumulativeCount: proto.Uint64(1524), }, { UpperBound: proto.Float64(math.Inf(+1)), CumulativeCount: proto.Uint64(2693), }, }, }, }, { Label: []*dto.LabelPair{ { Name: proto.String("name_1"), Value: proto.String("Björn"), }, { Name: proto.String("name_2"), Value: proto.String("佖佥"), }, { Name: proto.String("name_3"), Value: proto.String("Just a quite long label value to test performance."), }, }, Histogram: &dto.Histogram{ SampleCount: proto.Uint64(5699), SampleSum: proto.Float64(49484343543.4343), Bucket: []*dto.Bucket{ { UpperBound: proto.Float64(100), CumulativeCount: proto.Uint64(120), }, { UpperBound: proto.Float64(120), CumulativeCount: proto.Uint64(412), }, { UpperBound: proto.Float64(144), CumulativeCount: proto.Uint64(596), }, { UpperBound: proto.Float64(172.8), CumulativeCount: proto.Uint64(1535), }, }, }, TimestampMs: proto.Int64(1234567890), }, }, } out := bytes.NewBuffer(make([]byte, 0, 1024)) for i := 0; i < b.N; i++ { _, err := MetricFamilyToOpenMetrics(out, mf) if err != nil { b.Fatal(err) } out.Reset() } } func TestOpenMetricsCreateError(t *testing.T) { scenarios := []struct { in *dto.MetricFamily err string }{ // 0: No metric name. { in: &dto.MetricFamily{ Help: proto.String("doc string"), Type: dto.MetricType_UNTYPED.Enum(), Metric: []*dto.Metric{ { Untyped: &dto.Untyped{ Value: proto.Float64(math.Inf(-1)), }, }, }, }, err: "MetricFamily has no name", }, // 1: Wrong type. { in: &dto.MetricFamily{ Name: proto.String("name"), Help: proto.String("doc string"), Type: dto.MetricType_COUNTER.Enum(), Metric: []*dto.Metric{ { Untyped: &dto.Untyped{ Value: proto.Float64(math.Inf(-1)), }, }, }, }, err: "expected counter in metric", }, } for i, scenario := range scenarios { var out bytes.Buffer _, err := MetricFamilyToOpenMetrics(&out, scenario.in) if err == nil { t.Errorf("%d. expected error, got nil", i) continue } if expected, got := scenario.err, err.Error(); strings.Index(got, expected) != 0 { t.Errorf( "%d. expected error starting with %q, got %q", i, expected, got, ) } } } golang-github-prometheus-common-0.55.0/expfmt/testdata/000077500000000000000000000000001463701437000231305ustar00rootroot00000000000000golang-github-prometheus-common-0.55.0/expfmt/testdata/json2000066400000000000000000000014111463701437000241030ustar00rootroot00000000000000[ { "baseLabels": { "__name__": "rpc_calls_total", "job": "batch_job" }, "docstring": "RPC calls.", "metric": { "type": "counter", "value": [ { "labels": { "service": "zed" }, "value": 25 }, { "labels": { "service": "bar" }, "value": 24 } ] } }, { "baseLabels": { "__name__": "rpc_latency_microseconds" }, "docstring": "RPC latency.", "metric": { "type": "histogram", "value": [ { "labels": { "service": "foo" }, "value": { "0.010000": 15, "0.990000": 17 } } ] } } ] golang-github-prometheus-common-0.55.0/expfmt/testdata/json2_bad000066400000000000000000000014121463701437000247120ustar00rootroot00000000000000[ { "baseLabels": { "__name__": "rpc_calls_total", "job": "batch_job" }, "docstring": "RPC calls.", "metric": { "type": "counter", "value": [ { "labels": { "servic|e": "zed" }, "value": 25 }, { "labels": { "service": "bar" }, "value": 24 } ] } }, { "baseLabels": { "__name__": "rpc_latency_microseconds" }, "docstring": "RPC latency.", "metric": { "type": "histogram", "value": [ { "labels": { "service": "foo" }, "value": { "0.010000": 15, "0.990000": 17 } } ] } } ] golang-github-prometheus-common-0.55.0/expfmt/testdata/protobuf000066400000000000000000000200571463701437000247170ustar00rootroot00000000000000L process_virtual_memory_bytesVirtual memory size in bytes."  (#A 9prometheus_local_storage_checkpoint_duration_millisecondsWThe duration (in milliseconds) it took to checkpoint in-memory metrics and head chunks."   5prometheus_local_storage_persist_latency_microseconds1A summary of latencies for persisting each chunk."J"H(\@ ?n`>@ ?h|?qi@ Gz?V턤@j /prometheus_local_storage_persist_queue_capacity(The total capacity of the persist queue."  @ -prometheus_notifications_latency_millisecondsXLatency quantiles for sending alert notifications (not including dropped notifications)."I"G ? ? Gz?\ process_cpu_seconds_total0Total user and system CPU time spent in seconds."  ?| http_requests_total#Total number of HTTP requests made."> code200  handler prometheus methodget ]@g /prometheus_local_storage_ingested_samples_total%The total number of samples ingested."  @@o *prometheus_local_storage_memory_chunkdescs2The current number of chunk descriptors in memory."  @` &prometheus_local_storage_memory_series'The current number of series in memory."  z@` !prometheus_samples_queue_capacity,Capacity of the queue for unwritten samples."  @ prometheus_samples_queue_lengthCurrent number of items in the queue for unwritten samples. Each item comprises all samples exposed by one target as one metric family (i.e. metrics of the same name)."   "http_request_duration_microseconds+The HTTP request latencies in microseconds."W handler/"G ? ? Gz?"]  handler/alerts"G ? ? Gz?"b  handler /api/metrics"G ? ? Gz?"`  handler /api/query"G ? ? Gz?"f  handler/api/query_range"G ? ? Gz?"b  handler /api/targets"G ? ? Gz?"`  handler /consoles/"G ? ? Gz?"\  handler/graph"G ? ? Gz?"[  handler/heap"G ? ? Gz?"^  handler/static/"G ? ? Gz?"`  handler prometheus"GwtA ?m@ ?J + @ Gz?T㥛@J process_max_fds(Maximum number of open file descriptors."  @ http_request_size_bytes The HTTP request sizes in bytes."W handler/"G ? ? Gz?"]  handler/alerts"G ? ? Gz?"b  handler /api/metrics"G ? ? Gz?"`  handler /api/query"G ? ? Gz?"f  handler/api/query_range"G ? ? Gz?"b  handler /api/targets"G ? ? Gz?"`  handler /consoles/"G ? ? Gz?"\  handler/graph"G ? ? Gz?"[  handler/heap"G ? ? Gz?"^  handler/static/"G ? ? Gz?"`  handler prometheus"Gw@ ?0r@ ?0r@ Gz?0r@O prometheus_dns_sd_lookups_totalThe number of DNS-SD lookups."  @ -prometheus_local_storage_indexing_batch_sizesAQuantiles for indexing batch sizes (number of metrics per batch)."I"G@ ?@ ?@ Gz?@m .prometheus_local_storage_indexing_queue_length,The number of metrics waiting to be indexed."  g %prometheus_notifications_queue_length/The number of alert notifications in the queue."  N process_resident_memory_bytesResident memory size in bytes."  Ah 'prometheus_notifications_queue_capacity.The capacity of the alert notifications queue."  Y@ )prometheus_local_storage_series_ops_total4The total number of series operations by their type."  typecreate @"*  typemaintenance_in_memory &@C process_open_fds Number of open file descriptors."  =@ (prometheus_local_storage_chunk_ops_total3The total number of chunk operations by their type."  typecreate @"  typepersist e@" typepin @"  type transcode y@" typeunpin @ !8uR>"\]f3@,.Nq&uXЍ͖d-ꪓ%Nk 3pz(O\O8?OǢ8&NtTd~ez&,LA{swwΔ̹Eew >W<&p1UmɏЭ!b)9C)0 1XGpN#}hF1CPf$EyxĮO\LIzm X cj+\v*EH-YjYjBak93s ##z}_ɆlT.XC^PUP_2L!zz@GGGZIQS?=L-,e nJNI~[\F9tŪCP&8+}!3`uʭ,J>4P}ԩ j؆b{ݒMnZ2X)P1 {3Ȳ0e ֵjhG69 o C=BGIjbrX"Gvv;zZӮDmIEI2.]σ7LdBMg 8jNbעtgWo}6KLkc}k mXcǥI:NO=k:\?v#(T%ӔJ*+I:Xٍ#(nZU֑ .Ƶh@TQώ#xaEJ pV$̧o/v(|Es4G$IKʔ|X#}4U^XJ3i6׏B?fc*Rf*%S[UǼpU%+Wƴ?d ćq+]sBqTYsUEP \:Us'&(<]D&DT-[+B|[f,Iý"1!vY(3f BUuSXPrY*i3ee@).e'ljlL1CrL"H-؄@ 0;ۉ["otF4GzEv 겄P /Ia}wk{g9| 3|Iis.y#12)X^⛧ \=|"7?> ha$1(9qWIsU臠n6kg? Vu> AKTM`涍!,ΒB<:MBq?XEw]7=lvH?a8 MUoA0vLTKFX (OYm:GTFOe:}xO8PsfVUjֵyGV x"kpTck^OK~p ^>RS7mՕ5vhMu;'LTÑ-k`#k7 Rgolang-github-prometheus-common-0.55.0/expfmt/text_create.go000066400000000000000000000312641463701437000241630ustar00rootroot00000000000000// Copyright 2014 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package expfmt import ( "bufio" "fmt" "io" "math" "strconv" "strings" "sync" "github.com/prometheus/common/model" dto "github.com/prometheus/client_model/go" ) // enhancedWriter has all the enhanced write functions needed here. bufio.Writer // implements it. type enhancedWriter interface { io.Writer WriteRune(r rune) (n int, err error) WriteString(s string) (n int, err error) WriteByte(c byte) error } const ( initialNumBufSize = 24 ) var ( bufPool = sync.Pool{ New: func() interface{} { return bufio.NewWriter(io.Discard) }, } numBufPool = sync.Pool{ New: func() interface{} { b := make([]byte, 0, initialNumBufSize) return &b }, } ) // MetricFamilyToText converts a MetricFamily proto message into text format and // writes the resulting lines to 'out'. It returns the number of bytes written // and any error encountered. The output will have the same order as the input, // no further sorting is performed. Furthermore, this function assumes the input // is already sanitized and does not perform any sanity checks. If the input // contains duplicate metrics or invalid metric or label names, the conversion // will result in invalid text format output. // // If metric names conform to the legacy validation pattern, they will be placed // outside the brackets in the traditional way, like `foo{}`. If the metric name // fails the legacy validation check, it will be placed quoted inside the // brackets: `{"foo"}`. As stated above, the input is assumed to be santized and // no error will be thrown in this case. // // Similar to metric names, if label names conform to the legacy validation // pattern, they will be unquoted as normal, like `foo{bar="baz"}`. If the label // name fails the legacy validation check, it will be quoted: // `foo{"bar"="baz"}`. As stated above, the input is assumed to be santized and // no error will be thrown in this case. // // This method fulfills the type 'prometheus.encoder'. func MetricFamilyToText(out io.Writer, in *dto.MetricFamily) (written int, err error) { // Fail-fast checks. if len(in.Metric) == 0 { return 0, fmt.Errorf("MetricFamily has no metrics: %s", in) } name := in.GetName() if name == "" { return 0, fmt.Errorf("MetricFamily has no name: %s", in) } // Try the interface upgrade. If it doesn't work, we'll use a // bufio.Writer from the sync.Pool. w, ok := out.(enhancedWriter) if !ok { b := bufPool.Get().(*bufio.Writer) b.Reset(out) w = b defer func() { bErr := b.Flush() if err == nil { err = bErr } bufPool.Put(b) }() } var n int // Comments, first HELP, then TYPE. if in.Help != nil { n, err = w.WriteString("# HELP ") written += n if err != nil { return } n, err = writeName(w, name) written += n if err != nil { return } err = w.WriteByte(' ') written++ if err != nil { return } n, err = writeEscapedString(w, *in.Help, false) written += n if err != nil { return } err = w.WriteByte('\n') written++ if err != nil { return } } n, err = w.WriteString("# TYPE ") written += n if err != nil { return } n, err = writeName(w, name) written += n if err != nil { return } metricType := in.GetType() switch metricType { case dto.MetricType_COUNTER: n, err = w.WriteString(" counter\n") case dto.MetricType_GAUGE: n, err = w.WriteString(" gauge\n") case dto.MetricType_SUMMARY: n, err = w.WriteString(" summary\n") case dto.MetricType_UNTYPED: n, err = w.WriteString(" untyped\n") case dto.MetricType_HISTOGRAM: n, err = w.WriteString(" histogram\n") default: return written, fmt.Errorf("unknown metric type %s", metricType.String()) } written += n if err != nil { return } // Finally the samples, one line for each. for _, metric := range in.Metric { switch metricType { case dto.MetricType_COUNTER: if metric.Counter == nil { return written, fmt.Errorf( "expected counter in metric %s %s", name, metric, ) } n, err = writeSample( w, name, "", metric, "", 0, metric.Counter.GetValue(), ) case dto.MetricType_GAUGE: if metric.Gauge == nil { return written, fmt.Errorf( "expected gauge in metric %s %s", name, metric, ) } n, err = writeSample( w, name, "", metric, "", 0, metric.Gauge.GetValue(), ) case dto.MetricType_UNTYPED: if metric.Untyped == nil { return written, fmt.Errorf( "expected untyped in metric %s %s", name, metric, ) } n, err = writeSample( w, name, "", metric, "", 0, metric.Untyped.GetValue(), ) case dto.MetricType_SUMMARY: if metric.Summary == nil { return written, fmt.Errorf( "expected summary in metric %s %s", name, metric, ) } for _, q := range metric.Summary.Quantile { n, err = writeSample( w, name, "", metric, model.QuantileLabel, q.GetQuantile(), q.GetValue(), ) written += n if err != nil { return } } n, err = writeSample( w, name, "_sum", metric, "", 0, metric.Summary.GetSampleSum(), ) written += n if err != nil { return } n, err = writeSample( w, name, "_count", metric, "", 0, float64(metric.Summary.GetSampleCount()), ) case dto.MetricType_HISTOGRAM: if metric.Histogram == nil { return written, fmt.Errorf( "expected histogram in metric %s %s", name, metric, ) } infSeen := false for _, b := range metric.Histogram.Bucket { n, err = writeSample( w, name, "_bucket", metric, model.BucketLabel, b.GetUpperBound(), float64(b.GetCumulativeCount()), ) written += n if err != nil { return } if math.IsInf(b.GetUpperBound(), +1) { infSeen = true } } if !infSeen { n, err = writeSample( w, name, "_bucket", metric, model.BucketLabel, math.Inf(+1), float64(metric.Histogram.GetSampleCount()), ) written += n if err != nil { return } } n, err = writeSample( w, name, "_sum", metric, "", 0, metric.Histogram.GetSampleSum(), ) written += n if err != nil { return } n, err = writeSample( w, name, "_count", metric, "", 0, float64(metric.Histogram.GetSampleCount()), ) default: return written, fmt.Errorf( "unexpected type in metric %s %s", name, metric, ) } written += n if err != nil { return } } return } // writeSample writes a single sample in text format to w, given the metric // name, the metric proto message itself, optionally an additional label name // with a float64 value (use empty string as label name if not required), and // the value. The function returns the number of bytes written and any error // encountered. func writeSample( w enhancedWriter, name, suffix string, metric *dto.Metric, additionalLabelName string, additionalLabelValue float64, value float64, ) (int, error) { written := 0 n, err := writeNameAndLabelPairs( w, name+suffix, metric.Label, additionalLabelName, additionalLabelValue, ) written += n if err != nil { return written, err } err = w.WriteByte(' ') written++ if err != nil { return written, err } n, err = writeFloat(w, value) written += n if err != nil { return written, err } if metric.TimestampMs != nil { err = w.WriteByte(' ') written++ if err != nil { return written, err } n, err = writeInt(w, *metric.TimestampMs) written += n if err != nil { return written, err } } err = w.WriteByte('\n') written++ if err != nil { return written, err } return written, nil } // writeNameAndLabelPairs converts a slice of LabelPair proto messages plus the // explicitly given metric name and additional label pair into text formatted as // required by the text format and writes it to 'w'. An empty slice in // combination with an empty string 'additionalLabelName' results in nothing // being written. Otherwise, the label pairs are written, escaped as required by // the text format, and enclosed in '{...}'. The function returns the number of // bytes written and any error encountered. If the metric name is not // legacy-valid, it will be put inside the brackets as well. Legacy-invalid // label names will also be quoted. func writeNameAndLabelPairs( w enhancedWriter, name string, in []*dto.LabelPair, additionalLabelName string, additionalLabelValue float64, ) (int, error) { var ( written int separator byte = '{' metricInsideBraces = false ) if name != "" { // If the name does not pass the legacy validity check, we must put the // metric name inside the braces. if !model.IsValidLegacyMetricName(model.LabelValue(name)) { metricInsideBraces = true err := w.WriteByte(separator) written++ if err != nil { return written, err } separator = ',' } n, err := writeName(w, name) written += n if err != nil { return written, err } } if len(in) == 0 && additionalLabelName == "" { if metricInsideBraces { err := w.WriteByte('}') written++ if err != nil { return written, err } } return written, nil } for _, lp := range in { err := w.WriteByte(separator) written++ if err != nil { return written, err } n, err := writeName(w, lp.GetName()) written += n if err != nil { return written, err } n, err = w.WriteString(`="`) written += n if err != nil { return written, err } n, err = writeEscapedString(w, lp.GetValue(), true) written += n if err != nil { return written, err } err = w.WriteByte('"') written++ if err != nil { return written, err } separator = ',' } if additionalLabelName != "" { err := w.WriteByte(separator) written++ if err != nil { return written, err } n, err := w.WriteString(additionalLabelName) written += n if err != nil { return written, err } n, err = w.WriteString(`="`) written += n if err != nil { return written, err } n, err = writeFloat(w, additionalLabelValue) written += n if err != nil { return written, err } err = w.WriteByte('"') written++ if err != nil { return written, err } } err := w.WriteByte('}') written++ if err != nil { return written, err } return written, nil } // writeEscapedString replaces '\' by '\\', new line character by '\n', and - if // includeDoubleQuote is true - '"' by '\"'. var ( escaper = strings.NewReplacer("\\", `\\`, "\n", `\n`) quotedEscaper = strings.NewReplacer("\\", `\\`, "\n", `\n`, "\"", `\"`) ) func writeEscapedString(w enhancedWriter, v string, includeDoubleQuote bool) (int, error) { if includeDoubleQuote { return quotedEscaper.WriteString(w, v) } return escaper.WriteString(w, v) } // writeFloat is equivalent to fmt.Fprint with a float64 argument but hardcodes // a few common cases for increased efficiency. For non-hardcoded cases, it uses // strconv.AppendFloat to avoid allocations, similar to writeInt. func writeFloat(w enhancedWriter, f float64) (int, error) { switch { case f == 1: return 1, w.WriteByte('1') case f == 0: return 1, w.WriteByte('0') case f == -1: return w.WriteString("-1") case math.IsNaN(f): return w.WriteString("NaN") case math.IsInf(f, +1): return w.WriteString("+Inf") case math.IsInf(f, -1): return w.WriteString("-Inf") default: bp := numBufPool.Get().(*[]byte) *bp = strconv.AppendFloat((*bp)[:0], f, 'g', -1, 64) written, err := w.Write(*bp) numBufPool.Put(bp) return written, err } } // writeInt is equivalent to fmt.Fprint with an int64 argument but uses // strconv.AppendInt with a byte slice taken from a sync.Pool to avoid // allocations. func writeInt(w enhancedWriter, i int64) (int, error) { bp := numBufPool.Get().(*[]byte) *bp = strconv.AppendInt((*bp)[:0], i, 10) written, err := w.Write(*bp) numBufPool.Put(bp) return written, err } // writeName writes a string as-is if it complies with the legacy naming // scheme, or escapes it in double quotes if not. func writeName(w enhancedWriter, name string) (int, error) { if model.IsValidLegacyMetricName(model.LabelValue(name)) { return w.WriteString(name) } var written int var err error err = w.WriteByte('"') written++ if err != nil { return written, err } var n int n, err = writeEscapedString(w, name, true) written += n if err != nil { return written, err } err = w.WriteByte('"') written++ return written, err } golang-github-prometheus-common-0.55.0/expfmt/text_create_test.go000066400000000000000000000364521463701437000252260ustar00rootroot00000000000000// Copyright 2014 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package expfmt import ( "bytes" "math" "strings" "testing" "google.golang.org/protobuf/proto" dto "github.com/prometheus/client_model/go" "github.com/prometheus/common/model" ) func TestCreate(t *testing.T) { oldDefaultScheme := model.NameEscapingScheme model.NameEscapingScheme = model.NoEscaping defer func() { model.NameEscapingScheme = oldDefaultScheme }() scenarios := []struct { in *dto.MetricFamily out string }{ // 0: Counter, NaN as value, timestamp given. { in: &dto.MetricFamily{ Name: proto.String("name"), Help: proto.String("two-line\n doc str\\ing"), Type: dto.MetricType_COUNTER.Enum(), Metric: []*dto.Metric{ { Label: []*dto.LabelPair{ { Name: proto.String("labelname"), Value: proto.String("val1"), }, { Name: proto.String("basename"), Value: proto.String("basevalue"), }, }, Counter: &dto.Counter{ Value: proto.Float64(math.NaN()), }, }, { Label: []*dto.LabelPair{ { Name: proto.String("labelname"), Value: proto.String("val2"), }, { Name: proto.String("basename"), Value: proto.String("basevalue"), }, }, Counter: &dto.Counter{ Value: proto.Float64(.23), }, TimestampMs: proto.Int64(1234567890), }, }, }, out: `# HELP name two-line\n doc str\\ing # TYPE name counter name{labelname="val1",basename="basevalue"} NaN name{labelname="val2",basename="basevalue"} 0.23 1234567890 `, }, // 1: Gauge, some escaping required, +Inf as value, multi-byte characters in label values. { in: &dto.MetricFamily{ Name: proto.String("gauge_name"), Help: proto.String("gauge\ndoc\nstr\"ing"), Type: dto.MetricType_GAUGE.Enum(), Metric: []*dto.Metric{ { Label: []*dto.LabelPair{ { Name: proto.String("name_1"), Value: proto.String("val with\nnew line"), }, { Name: proto.String("name_2"), Value: proto.String("val with \\backslash and \"quotes\""), }, }, Gauge: &dto.Gauge{ Value: proto.Float64(math.Inf(+1)), }, }, { Label: []*dto.LabelPair{ { Name: proto.String("name_1"), Value: proto.String("Björn"), }, { Name: proto.String("name_2"), Value: proto.String("佖佥"), }, }, Gauge: &dto.Gauge{ Value: proto.Float64(3.14e42), }, }, }, }, out: `# HELP gauge_name gauge\ndoc\nstr"ing # TYPE gauge_name gauge gauge_name{name_1="val with\nnew line",name_2="val with \\backslash and \"quotes\""} +Inf gauge_name{name_1="Björn",name_2="佖佥"} 3.14e+42 `, }, // 2: Gauge, utf-8, +Inf as value, multi-byte characters in label values. { in: &dto.MetricFamily{ Name: proto.String("gauge.name"), Help: proto.String("gauge\ndoc\nstr\"ing"), Type: dto.MetricType_GAUGE.Enum(), Metric: []*dto.Metric{ { Label: []*dto.LabelPair{ { Name: proto.String("name.1"), Value: proto.String("val with\nnew line"), }, { Name: proto.String("name*2"), Value: proto.String("val with \\backslash and \"quotes\""), }, }, Gauge: &dto.Gauge{ Value: proto.Float64(math.Inf(+1)), }, }, { Label: []*dto.LabelPair{ { Name: proto.String("name.1"), Value: proto.String("Björn"), }, { Name: proto.String("name*2"), Value: proto.String("佖佥"), }, }, Gauge: &dto.Gauge{ Value: proto.Float64(3.14e42), }, }, }, }, out: `# HELP "gauge.name" gauge\ndoc\nstr"ing # TYPE "gauge.name" gauge {"gauge.name","name.1"="val with\nnew line","name*2"="val with \\backslash and \"quotes\""} +Inf {"gauge.name","name.1"="Björn","name*2"="佖佥"} 3.14e+42 `, }, // 3: Untyped, no help, one sample with no labels and -Inf as value, another sample with one label. { in: &dto.MetricFamily{ Name: proto.String("untyped_name"), Type: dto.MetricType_UNTYPED.Enum(), Metric: []*dto.Metric{ { Untyped: &dto.Untyped{ Value: proto.Float64(math.Inf(-1)), }, }, { Label: []*dto.LabelPair{ { Name: proto.String("name_1"), Value: proto.String("value 1"), }, }, Untyped: &dto.Untyped{ Value: proto.Float64(-1.23e-45), }, }, }, }, out: `# TYPE untyped_name untyped untyped_name -Inf untyped_name{name_1="value 1"} -1.23e-45 `, }, // 4: Summary. { in: &dto.MetricFamily{ Name: proto.String("summary_name"), Help: proto.String("summary docstring"), Type: dto.MetricType_SUMMARY.Enum(), Metric: []*dto.Metric{ { Summary: &dto.Summary{ SampleCount: proto.Uint64(42), SampleSum: proto.Float64(-3.4567), Quantile: []*dto.Quantile{ { Quantile: proto.Float64(0.5), Value: proto.Float64(-1.23), }, { Quantile: proto.Float64(0.9), Value: proto.Float64(.2342354), }, { Quantile: proto.Float64(0.99), Value: proto.Float64(0), }, }, }, }, { Label: []*dto.LabelPair{ { Name: proto.String("name_1"), Value: proto.String("value 1"), }, { Name: proto.String("name_2"), Value: proto.String("value 2"), }, }, Summary: &dto.Summary{ SampleCount: proto.Uint64(4711), SampleSum: proto.Float64(2010.1971), Quantile: []*dto.Quantile{ { Quantile: proto.Float64(0.5), Value: proto.Float64(1), }, { Quantile: proto.Float64(0.9), Value: proto.Float64(2), }, { Quantile: proto.Float64(0.99), Value: proto.Float64(3), }, }, }, }, }, }, out: `# HELP summary_name summary docstring # TYPE summary_name summary summary_name{quantile="0.5"} -1.23 summary_name{quantile="0.9"} 0.2342354 summary_name{quantile="0.99"} 0 summary_name_sum -3.4567 summary_name_count 42 summary_name{name_1="value 1",name_2="value 2",quantile="0.5"} 1 summary_name{name_1="value 1",name_2="value 2",quantile="0.9"} 2 summary_name{name_1="value 1",name_2="value 2",quantile="0.99"} 3 summary_name_sum{name_1="value 1",name_2="value 2"} 2010.1971 summary_name_count{name_1="value 1",name_2="value 2"} 4711 `, }, // 5: Histogram { in: &dto.MetricFamily{ Name: proto.String("request_duration_microseconds"), Help: proto.String("The response latency."), Type: dto.MetricType_HISTOGRAM.Enum(), Metric: []*dto.Metric{ { Histogram: &dto.Histogram{ SampleCount: proto.Uint64(2693), SampleSum: proto.Float64(1756047.3), Bucket: []*dto.Bucket{ { UpperBound: proto.Float64(100), CumulativeCount: proto.Uint64(123), }, { UpperBound: proto.Float64(120), CumulativeCount: proto.Uint64(412), }, { UpperBound: proto.Float64(144), CumulativeCount: proto.Uint64(592), }, { UpperBound: proto.Float64(172.8), CumulativeCount: proto.Uint64(1524), }, { UpperBound: proto.Float64(math.Inf(+1)), CumulativeCount: proto.Uint64(2693), }, }, }, }, }, }, out: `# HELP request_duration_microseconds The response latency. # TYPE request_duration_microseconds histogram request_duration_microseconds_bucket{le="100"} 123 request_duration_microseconds_bucket{le="120"} 412 request_duration_microseconds_bucket{le="144"} 592 request_duration_microseconds_bucket{le="172.8"} 1524 request_duration_microseconds_bucket{le="+Inf"} 2693 request_duration_microseconds_sum 1.7560473e+06 request_duration_microseconds_count 2693 `, }, // 6: Histogram with missing +Inf bucket. { in: &dto.MetricFamily{ Name: proto.String("request_duration_microseconds"), Help: proto.String("The response latency."), Type: dto.MetricType_HISTOGRAM.Enum(), Metric: []*dto.Metric{ { Histogram: &dto.Histogram{ SampleCount: proto.Uint64(2693), SampleSum: proto.Float64(1756047.3), Bucket: []*dto.Bucket{ { UpperBound: proto.Float64(100), CumulativeCount: proto.Uint64(123), }, { UpperBound: proto.Float64(120), CumulativeCount: proto.Uint64(412), }, { UpperBound: proto.Float64(144), CumulativeCount: proto.Uint64(592), }, { UpperBound: proto.Float64(172.8), CumulativeCount: proto.Uint64(1524), }, }, }, }, }, }, out: `# HELP request_duration_microseconds The response latency. # TYPE request_duration_microseconds histogram request_duration_microseconds_bucket{le="100"} 123 request_duration_microseconds_bucket{le="120"} 412 request_duration_microseconds_bucket{le="144"} 592 request_duration_microseconds_bucket{le="172.8"} 1524 request_duration_microseconds_bucket{le="+Inf"} 2693 request_duration_microseconds_sum 1.7560473e+06 request_duration_microseconds_count 2693 `, }, // 7: No metric type, should result in default type Counter. { in: &dto.MetricFamily{ Name: proto.String("name"), Help: proto.String("doc string"), Metric: []*dto.Metric{ { Counter: &dto.Counter{ Value: proto.Float64(math.Inf(-1)), }, }, }, }, out: `# HELP name doc string # TYPE name counter name -Inf `, }, } for i, scenario := range scenarios { out := bytes.NewBuffer(make([]byte, 0, len(scenario.out))) n, err := MetricFamilyToText(out, scenario.in) if err != nil { t.Errorf("%d. error: %s", i, err) continue } if expected, got := len(scenario.out), n; expected != got { t.Errorf( "%d. expected %d bytes written, got %d", i, expected, got, ) } if expected, got := scenario.out, out.String(); expected != got { t.Errorf( "%d. expected out=%q, got %q", i, expected, got, ) } } } func BenchmarkCreate(b *testing.B) { mf := &dto.MetricFamily{ Name: proto.String("request_duration_microseconds"), Help: proto.String("The response latency."), Type: dto.MetricType_HISTOGRAM.Enum(), Metric: []*dto.Metric{ { Label: []*dto.LabelPair{ { Name: proto.String("name_1"), Value: proto.String("val with\nnew line"), }, { Name: proto.String("name_2"), Value: proto.String("val with \\backslash and \"quotes\""), }, { Name: proto.String("name_3"), Value: proto.String("Just a quite long label value to test performance."), }, }, Histogram: &dto.Histogram{ SampleCount: proto.Uint64(2693), SampleSum: proto.Float64(1756047.3), Bucket: []*dto.Bucket{ { UpperBound: proto.Float64(100), CumulativeCount: proto.Uint64(123), }, { UpperBound: proto.Float64(120), CumulativeCount: proto.Uint64(412), }, { UpperBound: proto.Float64(144), CumulativeCount: proto.Uint64(592), }, { UpperBound: proto.Float64(172.8), CumulativeCount: proto.Uint64(1524), }, { UpperBound: proto.Float64(math.Inf(+1)), CumulativeCount: proto.Uint64(2693), }, }, }, }, { Label: []*dto.LabelPair{ { Name: proto.String("name_1"), Value: proto.String("Björn"), }, { Name: proto.String("name_2"), Value: proto.String("佖佥"), }, { Name: proto.String("name_3"), Value: proto.String("Just a quite long label value to test performance."), }, }, Histogram: &dto.Histogram{ SampleCount: proto.Uint64(5699), SampleSum: proto.Float64(49484343543.4343), Bucket: []*dto.Bucket{ { UpperBound: proto.Float64(100), CumulativeCount: proto.Uint64(120), }, { UpperBound: proto.Float64(120), CumulativeCount: proto.Uint64(412), }, { UpperBound: proto.Float64(144), CumulativeCount: proto.Uint64(596), }, { UpperBound: proto.Float64(172.8), CumulativeCount: proto.Uint64(1535), }, }, }, TimestampMs: proto.Int64(1234567890), }, }, } out := bytes.NewBuffer(make([]byte, 0, 1024)) for i := 0; i < b.N; i++ { _, err := MetricFamilyToText(out, mf) if err != nil { b.Fatal(err) } out.Reset() } } func BenchmarkCreateBuildInfo(b *testing.B) { mf := &dto.MetricFamily{ Name: proto.String("benchmark_build_info"), Help: proto.String("Test the creation of constant 1-value build_info metric."), Type: dto.MetricType_GAUGE.Enum(), Metric: []*dto.Metric{ { Label: []*dto.LabelPair{ { Name: proto.String("version"), Value: proto.String("1.2.3"), }, { Name: proto.String("revision"), Value: proto.String("2e84f5e4eacdffb574035810305191ff390360fe"), }, { Name: proto.String("go_version"), Value: proto.String("1.11.1"), }, }, Gauge: &dto.Gauge{ Value: proto.Float64(1), }, }, }, } out := bytes.NewBuffer(make([]byte, 0, 1024)) for i := 0; i < b.N; i++ { _, err := MetricFamilyToText(out, mf) if err != nil { b.Fatal(err) } out.Reset() } } func TestCreateError(t *testing.T) { scenarios := []struct { in *dto.MetricFamily err string }{ // 0: No metric. { in: &dto.MetricFamily{ Name: proto.String("name"), Help: proto.String("doc string"), Type: dto.MetricType_COUNTER.Enum(), Metric: []*dto.Metric{}, }, err: "MetricFamily has no metrics", }, // 1: No metric name. { in: &dto.MetricFamily{ Help: proto.String("doc string"), Type: dto.MetricType_UNTYPED.Enum(), Metric: []*dto.Metric{ { Untyped: &dto.Untyped{ Value: proto.Float64(math.Inf(-1)), }, }, }, }, err: "MetricFamily has no name", }, // 2: Wrong type. { in: &dto.MetricFamily{ Name: proto.String("name"), Help: proto.String("doc string"), Type: dto.MetricType_COUNTER.Enum(), Metric: []*dto.Metric{ { Untyped: &dto.Untyped{ Value: proto.Float64(math.Inf(-1)), }, }, }, }, err: "expected counter in metric", }, } for i, scenario := range scenarios { var out bytes.Buffer _, err := MetricFamilyToText(&out, scenario.in) if err == nil { t.Errorf("%d. expected error, got nil", i) continue } if expected, got := scenario.err, err.Error(); strings.Index(got, expected) != 0 { t.Errorf( "%d. expected error starting with %q, got %q", i, expected, got, ) } } } golang-github-prometheus-common-0.55.0/expfmt/text_parse.go000066400000000000000000000626371463701437000240420ustar00rootroot00000000000000// Copyright 2014 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package expfmt import ( "bufio" "bytes" "errors" "fmt" "io" "math" "strconv" "strings" dto "github.com/prometheus/client_model/go" "google.golang.org/protobuf/proto" "github.com/prometheus/common/model" ) // A stateFn is a function that represents a state in a state machine. By // executing it, the state is progressed to the next state. The stateFn returns // another stateFn, which represents the new state. The end state is represented // by nil. type stateFn func() stateFn // ParseError signals errors while parsing the simple and flat text-based // exchange format. type ParseError struct { Line int Msg string } // Error implements the error interface. func (e ParseError) Error() string { return fmt.Sprintf("text format parsing error in line %d: %s", e.Line, e.Msg) } // TextParser is used to parse the simple and flat text-based exchange format. Its // zero value is ready to use. type TextParser struct { metricFamiliesByName map[string]*dto.MetricFamily buf *bufio.Reader // Where the parsed input is read through. err error // Most recent error. lineCount int // Tracks the line count for error messages. currentByte byte // The most recent byte read. currentToken bytes.Buffer // Re-used each time a token has to be gathered from multiple bytes. currentMF *dto.MetricFamily currentMetric *dto.Metric currentLabelPair *dto.LabelPair // The remaining member variables are only used for summaries/histograms. currentLabels map[string]string // All labels including '__name__' but excluding 'quantile'/'le' // Summary specific. summaries map[uint64]*dto.Metric // Key is created with LabelsToSignature. currentQuantile float64 // Histogram specific. histograms map[uint64]*dto.Metric // Key is created with LabelsToSignature. currentBucket float64 // These tell us if the currently processed line ends on '_count' or // '_sum' respectively and belong to a summary/histogram, representing the sample // count and sum of that summary/histogram. currentIsSummaryCount, currentIsSummarySum bool currentIsHistogramCount, currentIsHistogramSum bool } // TextToMetricFamilies reads 'in' as the simple and flat text-based exchange // format and creates MetricFamily proto messages. It returns the MetricFamily // proto messages in a map where the metric names are the keys, along with any // error encountered. // // If the input contains duplicate metrics (i.e. lines with the same metric name // and exactly the same label set), the resulting MetricFamily will contain // duplicate Metric proto messages. Similar is true for duplicate label // names. Checks for duplicates have to be performed separately, if required. // Also note that neither the metrics within each MetricFamily are sorted nor // the label pairs within each Metric. Sorting is not required for the most // frequent use of this method, which is sample ingestion in the Prometheus // server. However, for presentation purposes, you might want to sort the // metrics, and in some cases, you must sort the labels, e.g. for consumption by // the metric family injection hook of the Prometheus registry. // // Summaries and histograms are rather special beasts. You would probably not // use them in the simple text format anyway. This method can deal with // summaries and histograms if they are presented in exactly the way the // text.Create function creates them. // // This method must not be called concurrently. If you want to parse different // input concurrently, instantiate a separate Parser for each goroutine. func (p *TextParser) TextToMetricFamilies(in io.Reader) (map[string]*dto.MetricFamily, error) { p.reset(in) for nextState := p.startOfLine; nextState != nil; nextState = nextState() { // Magic happens here... } // Get rid of empty metric families. for k, mf := range p.metricFamiliesByName { if len(mf.GetMetric()) == 0 { delete(p.metricFamiliesByName, k) } } // If p.err is io.EOF now, we have run into a premature end of the input // stream. Turn this error into something nicer and more // meaningful. (io.EOF is often used as a signal for the legitimate end // of an input stream.) if p.err != nil && errors.Is(p.err, io.EOF) { p.parseError("unexpected end of input stream") } return p.metricFamiliesByName, p.err } func (p *TextParser) reset(in io.Reader) { p.metricFamiliesByName = map[string]*dto.MetricFamily{} if p.buf == nil { p.buf = bufio.NewReader(in) } else { p.buf.Reset(in) } p.err = nil p.lineCount = 0 if p.summaries == nil || len(p.summaries) > 0 { p.summaries = map[uint64]*dto.Metric{} } if p.histograms == nil || len(p.histograms) > 0 { p.histograms = map[uint64]*dto.Metric{} } p.currentQuantile = math.NaN() p.currentBucket = math.NaN() } // startOfLine represents the state where the next byte read from p.buf is the // start of a line (or whitespace leading up to it). func (p *TextParser) startOfLine() stateFn { p.lineCount++ if p.skipBlankTab(); p.err != nil { // This is the only place that we expect to see io.EOF, // which is not an error but the signal that we are done. // Any other error that happens to align with the start of // a line is still an error. if errors.Is(p.err, io.EOF) { p.err = nil } return nil } switch p.currentByte { case '#': return p.startComment case '\n': return p.startOfLine // Empty line, start the next one. } return p.readingMetricName } // startComment represents the state where the next byte read from p.buf is the // start of a comment (or whitespace leading up to it). func (p *TextParser) startComment() stateFn { if p.skipBlankTab(); p.err != nil { return nil // Unexpected end of input. } if p.currentByte == '\n' { return p.startOfLine } if p.readTokenUntilWhitespace(); p.err != nil { return nil // Unexpected end of input. } // If we have hit the end of line already, there is nothing left // to do. This is not considered a syntax error. if p.currentByte == '\n' { return p.startOfLine } keyword := p.currentToken.String() if keyword != "HELP" && keyword != "TYPE" { // Generic comment, ignore by fast forwarding to end of line. for p.currentByte != '\n' { if p.currentByte, p.err = p.buf.ReadByte(); p.err != nil { return nil // Unexpected end of input. } } return p.startOfLine } // There is something. Next has to be a metric name. if p.skipBlankTab(); p.err != nil { return nil // Unexpected end of input. } if p.readTokenAsMetricName(); p.err != nil { return nil // Unexpected end of input. } if p.currentByte == '\n' { // At the end of the line already. // Again, this is not considered a syntax error. return p.startOfLine } if !isBlankOrTab(p.currentByte) { p.parseError("invalid metric name in comment") return nil } p.setOrCreateCurrentMF() if p.skipBlankTab(); p.err != nil { return nil // Unexpected end of input. } if p.currentByte == '\n' { // At the end of the line already. // Again, this is not considered a syntax error. return p.startOfLine } switch keyword { case "HELP": return p.readingHelp case "TYPE": return p.readingType } panic(fmt.Sprintf("code error: unexpected keyword %q", keyword)) } // readingMetricName represents the state where the last byte read (now in // p.currentByte) is the first byte of a metric name. func (p *TextParser) readingMetricName() stateFn { if p.readTokenAsMetricName(); p.err != nil { return nil } if p.currentToken.Len() == 0 { p.parseError("invalid metric name") return nil } p.setOrCreateCurrentMF() // Now is the time to fix the type if it hasn't happened yet. if p.currentMF.Type == nil { p.currentMF.Type = dto.MetricType_UNTYPED.Enum() } p.currentMetric = &dto.Metric{} // Do not append the newly created currentMetric to // currentMF.Metric right now. First wait if this is a summary, // and the metric exists already, which we can only know after // having read all the labels. if p.skipBlankTabIfCurrentBlankTab(); p.err != nil { return nil // Unexpected end of input. } return p.readingLabels } // readingLabels represents the state where the last byte read (now in // p.currentByte) is either the first byte of the label set (i.e. a '{'), or the // first byte of the value (otherwise). func (p *TextParser) readingLabels() stateFn { // Summaries/histograms are special. We have to reset the // currentLabels map, currentQuantile and currentBucket before starting to // read labels. if p.currentMF.GetType() == dto.MetricType_SUMMARY || p.currentMF.GetType() == dto.MetricType_HISTOGRAM { p.currentLabels = map[string]string{} p.currentLabels[string(model.MetricNameLabel)] = p.currentMF.GetName() p.currentQuantile = math.NaN() p.currentBucket = math.NaN() } if p.currentByte != '{' { return p.readingValue } return p.startLabelName } // startLabelName represents the state where the next byte read from p.buf is // the start of a label name (or whitespace leading up to it). func (p *TextParser) startLabelName() stateFn { if p.skipBlankTab(); p.err != nil { return nil // Unexpected end of input. } if p.currentByte == '}' { if p.skipBlankTab(); p.err != nil { return nil // Unexpected end of input. } return p.readingValue } if p.readTokenAsLabelName(); p.err != nil { return nil // Unexpected end of input. } if p.currentToken.Len() == 0 { p.parseError(fmt.Sprintf("invalid label name for metric %q", p.currentMF.GetName())) return nil } p.currentLabelPair = &dto.LabelPair{Name: proto.String(p.currentToken.String())} if p.currentLabelPair.GetName() == string(model.MetricNameLabel) { p.parseError(fmt.Sprintf("label name %q is reserved", model.MetricNameLabel)) return nil } // Special summary/histogram treatment. Don't add 'quantile' and 'le' // labels to 'real' labels. if !(p.currentMF.GetType() == dto.MetricType_SUMMARY && p.currentLabelPair.GetName() == model.QuantileLabel) && !(p.currentMF.GetType() == dto.MetricType_HISTOGRAM && p.currentLabelPair.GetName() == model.BucketLabel) { p.currentMetric.Label = append(p.currentMetric.Label, p.currentLabelPair) } if p.skipBlankTabIfCurrentBlankTab(); p.err != nil { return nil // Unexpected end of input. } if p.currentByte != '=' { p.parseError(fmt.Sprintf("expected '=' after label name, found %q", p.currentByte)) return nil } // Check for duplicate label names. labels := make(map[string]struct{}) for _, l := range p.currentMetric.Label { lName := l.GetName() if _, exists := labels[lName]; !exists { labels[lName] = struct{}{} } else { p.parseError(fmt.Sprintf("duplicate label names for metric %q", p.currentMF.GetName())) return nil } } return p.startLabelValue } // startLabelValue represents the state where the next byte read from p.buf is // the start of a (quoted) label value (or whitespace leading up to it). func (p *TextParser) startLabelValue() stateFn { if p.skipBlankTab(); p.err != nil { return nil // Unexpected end of input. } if p.currentByte != '"' { p.parseError(fmt.Sprintf("expected '\"' at start of label value, found %q", p.currentByte)) return nil } if p.readTokenAsLabelValue(); p.err != nil { return nil } if !model.LabelValue(p.currentToken.String()).IsValid() { p.parseError(fmt.Sprintf("invalid label value %q", p.currentToken.String())) return nil } p.currentLabelPair.Value = proto.String(p.currentToken.String()) // Special treatment of summaries: // - Quantile labels are special, will result in dto.Quantile later. // - Other labels have to be added to currentLabels for signature calculation. if p.currentMF.GetType() == dto.MetricType_SUMMARY { if p.currentLabelPair.GetName() == model.QuantileLabel { if p.currentQuantile, p.err = parseFloat(p.currentLabelPair.GetValue()); p.err != nil { // Create a more helpful error message. p.parseError(fmt.Sprintf("expected float as value for 'quantile' label, got %q", p.currentLabelPair.GetValue())) return nil } } else { p.currentLabels[p.currentLabelPair.GetName()] = p.currentLabelPair.GetValue() } } // Similar special treatment of histograms. if p.currentMF.GetType() == dto.MetricType_HISTOGRAM { if p.currentLabelPair.GetName() == model.BucketLabel { if p.currentBucket, p.err = parseFloat(p.currentLabelPair.GetValue()); p.err != nil { // Create a more helpful error message. p.parseError(fmt.Sprintf("expected float as value for 'le' label, got %q", p.currentLabelPair.GetValue())) return nil } } else { p.currentLabels[p.currentLabelPair.GetName()] = p.currentLabelPair.GetValue() } } if p.skipBlankTab(); p.err != nil { return nil // Unexpected end of input. } switch p.currentByte { case ',': return p.startLabelName case '}': if p.skipBlankTab(); p.err != nil { return nil // Unexpected end of input. } return p.readingValue default: p.parseError(fmt.Sprintf("unexpected end of label value %q", p.currentLabelPair.GetValue())) return nil } } // readingValue represents the state where the last byte read (now in // p.currentByte) is the first byte of the sample value (i.e. a float). func (p *TextParser) readingValue() stateFn { // When we are here, we have read all the labels, so for the // special case of a summary/histogram, we can finally find out // if the metric already exists. if p.currentMF.GetType() == dto.MetricType_SUMMARY { signature := model.LabelsToSignature(p.currentLabels) if summary := p.summaries[signature]; summary != nil { p.currentMetric = summary } else { p.summaries[signature] = p.currentMetric p.currentMF.Metric = append(p.currentMF.Metric, p.currentMetric) } } else if p.currentMF.GetType() == dto.MetricType_HISTOGRAM { signature := model.LabelsToSignature(p.currentLabels) if histogram := p.histograms[signature]; histogram != nil { p.currentMetric = histogram } else { p.histograms[signature] = p.currentMetric p.currentMF.Metric = append(p.currentMF.Metric, p.currentMetric) } } else { p.currentMF.Metric = append(p.currentMF.Metric, p.currentMetric) } if p.readTokenUntilWhitespace(); p.err != nil { return nil // Unexpected end of input. } value, err := parseFloat(p.currentToken.String()) if err != nil { // Create a more helpful error message. p.parseError(fmt.Sprintf("expected float as value, got %q", p.currentToken.String())) return nil } switch p.currentMF.GetType() { case dto.MetricType_COUNTER: p.currentMetric.Counter = &dto.Counter{Value: proto.Float64(value)} case dto.MetricType_GAUGE: p.currentMetric.Gauge = &dto.Gauge{Value: proto.Float64(value)} case dto.MetricType_UNTYPED: p.currentMetric.Untyped = &dto.Untyped{Value: proto.Float64(value)} case dto.MetricType_SUMMARY: // *sigh* if p.currentMetric.Summary == nil { p.currentMetric.Summary = &dto.Summary{} } switch { case p.currentIsSummaryCount: p.currentMetric.Summary.SampleCount = proto.Uint64(uint64(value)) case p.currentIsSummarySum: p.currentMetric.Summary.SampleSum = proto.Float64(value) case !math.IsNaN(p.currentQuantile): p.currentMetric.Summary.Quantile = append( p.currentMetric.Summary.Quantile, &dto.Quantile{ Quantile: proto.Float64(p.currentQuantile), Value: proto.Float64(value), }, ) } case dto.MetricType_HISTOGRAM: // *sigh* if p.currentMetric.Histogram == nil { p.currentMetric.Histogram = &dto.Histogram{} } switch { case p.currentIsHistogramCount: p.currentMetric.Histogram.SampleCount = proto.Uint64(uint64(value)) case p.currentIsHistogramSum: p.currentMetric.Histogram.SampleSum = proto.Float64(value) case !math.IsNaN(p.currentBucket): p.currentMetric.Histogram.Bucket = append( p.currentMetric.Histogram.Bucket, &dto.Bucket{ UpperBound: proto.Float64(p.currentBucket), CumulativeCount: proto.Uint64(uint64(value)), }, ) } default: p.err = fmt.Errorf("unexpected type for metric name %q", p.currentMF.GetName()) } if p.currentByte == '\n' { return p.startOfLine } return p.startTimestamp } // startTimestamp represents the state where the next byte read from p.buf is // the start of the timestamp (or whitespace leading up to it). func (p *TextParser) startTimestamp() stateFn { if p.skipBlankTab(); p.err != nil { return nil // Unexpected end of input. } if p.readTokenUntilWhitespace(); p.err != nil { return nil // Unexpected end of input. } timestamp, err := strconv.ParseInt(p.currentToken.String(), 10, 64) if err != nil { // Create a more helpful error message. p.parseError(fmt.Sprintf("expected integer as timestamp, got %q", p.currentToken.String())) return nil } p.currentMetric.TimestampMs = proto.Int64(timestamp) if p.readTokenUntilNewline(false); p.err != nil { return nil // Unexpected end of input. } if p.currentToken.Len() > 0 { p.parseError(fmt.Sprintf("spurious string after timestamp: %q", p.currentToken.String())) return nil } return p.startOfLine } // readingHelp represents the state where the last byte read (now in // p.currentByte) is the first byte of the docstring after 'HELP'. func (p *TextParser) readingHelp() stateFn { if p.currentMF.Help != nil { p.parseError(fmt.Sprintf("second HELP line for metric name %q", p.currentMF.GetName())) return nil } // Rest of line is the docstring. if p.readTokenUntilNewline(true); p.err != nil { return nil // Unexpected end of input. } p.currentMF.Help = proto.String(p.currentToken.String()) return p.startOfLine } // readingType represents the state where the last byte read (now in // p.currentByte) is the first byte of the type hint after 'HELP'. func (p *TextParser) readingType() stateFn { if p.currentMF.Type != nil { p.parseError(fmt.Sprintf("second TYPE line for metric name %q, or TYPE reported after samples", p.currentMF.GetName())) return nil } // Rest of line is the type. if p.readTokenUntilNewline(false); p.err != nil { return nil // Unexpected end of input. } metricType, ok := dto.MetricType_value[strings.ToUpper(p.currentToken.String())] if !ok { p.parseError(fmt.Sprintf("unknown metric type %q", p.currentToken.String())) return nil } p.currentMF.Type = dto.MetricType(metricType).Enum() return p.startOfLine } // parseError sets p.err to a ParseError at the current line with the given // message. func (p *TextParser) parseError(msg string) { p.err = ParseError{ Line: p.lineCount, Msg: msg, } } // skipBlankTab reads (and discards) bytes from p.buf until it encounters a byte // that is neither ' ' nor '\t'. That byte is left in p.currentByte. func (p *TextParser) skipBlankTab() { for { if p.currentByte, p.err = p.buf.ReadByte(); p.err != nil || !isBlankOrTab(p.currentByte) { return } } } // skipBlankTabIfCurrentBlankTab works exactly as skipBlankTab but doesn't do // anything if p.currentByte is neither ' ' nor '\t'. func (p *TextParser) skipBlankTabIfCurrentBlankTab() { if isBlankOrTab(p.currentByte) { p.skipBlankTab() } } // readTokenUntilWhitespace copies bytes from p.buf into p.currentToken. The // first byte considered is the byte already read (now in p.currentByte). The // first whitespace byte encountered is still copied into p.currentByte, but not // into p.currentToken. func (p *TextParser) readTokenUntilWhitespace() { p.currentToken.Reset() for p.err == nil && !isBlankOrTab(p.currentByte) && p.currentByte != '\n' { p.currentToken.WriteByte(p.currentByte) p.currentByte, p.err = p.buf.ReadByte() } } // readTokenUntilNewline copies bytes from p.buf into p.currentToken. The first // byte considered is the byte already read (now in p.currentByte). The first // newline byte encountered is still copied into p.currentByte, but not into // p.currentToken. If recognizeEscapeSequence is true, two escape sequences are // recognized: '\\' translates into '\', and '\n' into a line-feed character. // All other escape sequences are invalid and cause an error. func (p *TextParser) readTokenUntilNewline(recognizeEscapeSequence bool) { p.currentToken.Reset() escaped := false for p.err == nil { if recognizeEscapeSequence && escaped { switch p.currentByte { case '\\': p.currentToken.WriteByte(p.currentByte) case 'n': p.currentToken.WriteByte('\n') default: p.parseError(fmt.Sprintf("invalid escape sequence '\\%c'", p.currentByte)) return } escaped = false } else { switch p.currentByte { case '\n': return case '\\': escaped = true default: p.currentToken.WriteByte(p.currentByte) } } p.currentByte, p.err = p.buf.ReadByte() } } // readTokenAsMetricName copies a metric name from p.buf into p.currentToken. // The first byte considered is the byte already read (now in p.currentByte). // The first byte not part of a metric name is still copied into p.currentByte, // but not into p.currentToken. func (p *TextParser) readTokenAsMetricName() { p.currentToken.Reset() if !isValidMetricNameStart(p.currentByte) { return } for { p.currentToken.WriteByte(p.currentByte) p.currentByte, p.err = p.buf.ReadByte() if p.err != nil || !isValidMetricNameContinuation(p.currentByte) { return } } } // readTokenAsLabelName copies a label name from p.buf into p.currentToken. // The first byte considered is the byte already read (now in p.currentByte). // The first byte not part of a label name is still copied into p.currentByte, // but not into p.currentToken. func (p *TextParser) readTokenAsLabelName() { p.currentToken.Reset() if !isValidLabelNameStart(p.currentByte) { return } for { p.currentToken.WriteByte(p.currentByte) p.currentByte, p.err = p.buf.ReadByte() if p.err != nil || !isValidLabelNameContinuation(p.currentByte) { return } } } // readTokenAsLabelValue copies a label value from p.buf into p.currentToken. // In contrast to the other 'readTokenAs...' functions, which start with the // last read byte in p.currentByte, this method ignores p.currentByte and starts // with reading a new byte from p.buf. The first byte not part of a label value // is still copied into p.currentByte, but not into p.currentToken. func (p *TextParser) readTokenAsLabelValue() { p.currentToken.Reset() escaped := false for { if p.currentByte, p.err = p.buf.ReadByte(); p.err != nil { return } if escaped { switch p.currentByte { case '"', '\\': p.currentToken.WriteByte(p.currentByte) case 'n': p.currentToken.WriteByte('\n') default: p.parseError(fmt.Sprintf("invalid escape sequence '\\%c'", p.currentByte)) return } escaped = false continue } switch p.currentByte { case '"': return case '\n': p.parseError(fmt.Sprintf("label value %q contains unescaped new-line", p.currentToken.String())) return case '\\': escaped = true default: p.currentToken.WriteByte(p.currentByte) } } } func (p *TextParser) setOrCreateCurrentMF() { p.currentIsSummaryCount = false p.currentIsSummarySum = false p.currentIsHistogramCount = false p.currentIsHistogramSum = false name := p.currentToken.String() if p.currentMF = p.metricFamiliesByName[name]; p.currentMF != nil { return } // Try out if this is a _sum or _count for a summary/histogram. summaryName := summaryMetricName(name) if p.currentMF = p.metricFamiliesByName[summaryName]; p.currentMF != nil { if p.currentMF.GetType() == dto.MetricType_SUMMARY { if isCount(name) { p.currentIsSummaryCount = true } if isSum(name) { p.currentIsSummarySum = true } return } } histogramName := histogramMetricName(name) if p.currentMF = p.metricFamiliesByName[histogramName]; p.currentMF != nil { if p.currentMF.GetType() == dto.MetricType_HISTOGRAM { if isCount(name) { p.currentIsHistogramCount = true } if isSum(name) { p.currentIsHistogramSum = true } return } } p.currentMF = &dto.MetricFamily{Name: proto.String(name)} p.metricFamiliesByName[name] = p.currentMF } func isValidLabelNameStart(b byte) bool { return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || b == '_' } func isValidLabelNameContinuation(b byte) bool { return isValidLabelNameStart(b) || (b >= '0' && b <= '9') } func isValidMetricNameStart(b byte) bool { return isValidLabelNameStart(b) || b == ':' } func isValidMetricNameContinuation(b byte) bool { return isValidLabelNameContinuation(b) || b == ':' } func isBlankOrTab(b byte) bool { return b == ' ' || b == '\t' } func isCount(name string) bool { return len(name) > 6 && name[len(name)-6:] == "_count" } func isSum(name string) bool { return len(name) > 4 && name[len(name)-4:] == "_sum" } func isBucket(name string) bool { return len(name) > 7 && name[len(name)-7:] == "_bucket" } func summaryMetricName(name string) string { switch { case isCount(name): return name[:len(name)-6] case isSum(name): return name[:len(name)-4] default: return name } } func histogramMetricName(name string) string { switch { case isCount(name): return name[:len(name)-6] case isSum(name): return name[:len(name)-4] case isBucket(name): return name[:len(name)-7] default: return name } } func parseFloat(s string) (float64, error) { if strings.ContainsAny(s, "pP_") { return 0, fmt.Errorf("unsupported character in float") } return strconv.ParseFloat(s, 64) } golang-github-prometheus-common-0.55.0/expfmt/text_parse_test.go000066400000000000000000000404331463701437000250670ustar00rootroot00000000000000// Copyright 2014 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package expfmt import ( "errors" "math" "strings" "testing" dto "github.com/prometheus/client_model/go" "google.golang.org/protobuf/proto" ) func testTextParse(t testing.TB) { scenarios := []struct { in string out []*dto.MetricFamily }{ // 0: Empty lines as input. { in: ` `, out: []*dto.MetricFamily{}, }, // 1: Minimal case. { in: ` minimal_metric 1.234 another_metric -3e3 103948 # Even that: no_labels{} 3 # HELP line for non-existing metric will be ignored. `, out: []*dto.MetricFamily{ { Name: proto.String("minimal_metric"), Type: dto.MetricType_UNTYPED.Enum(), Metric: []*dto.Metric{ { Untyped: &dto.Untyped{ Value: proto.Float64(1.234), }, }, }, }, { Name: proto.String("another_metric"), Type: dto.MetricType_UNTYPED.Enum(), Metric: []*dto.Metric{ { Untyped: &dto.Untyped{ Value: proto.Float64(-3e3), }, TimestampMs: proto.Int64(103948), }, }, }, { Name: proto.String("no_labels"), Type: dto.MetricType_UNTYPED.Enum(), Metric: []*dto.Metric{ { Untyped: &dto.Untyped{ Value: proto.Float64(3), }, }, }, }, }, }, // 2: Counters & gauges, docstrings, various whitespace, escape sequences. { in: ` # A normal comment. # # TYPE name counter name{labelname="val1",basename="basevalue"} NaN name {labelname="val2",basename="base\"v\\al\nue"} 0.23 1234567890 # HELP name two-line\n doc str\\ing # HELP name2 doc str"ing 2 # TYPE name2 gauge name2{labelname="val2" ,basename = "basevalue2" } +Inf 54321 name2{ labelname = "val1" , }-Inf `, out: []*dto.MetricFamily{ { Name: proto.String("name"), Help: proto.String("two-line\n doc str\\ing"), Type: dto.MetricType_COUNTER.Enum(), Metric: []*dto.Metric{ { Label: []*dto.LabelPair{ { Name: proto.String("labelname"), Value: proto.String("val1"), }, { Name: proto.String("basename"), Value: proto.String("basevalue"), }, }, Counter: &dto.Counter{ Value: proto.Float64(math.NaN()), }, }, { Label: []*dto.LabelPair{ { Name: proto.String("labelname"), Value: proto.String("val2"), }, { Name: proto.String("basename"), Value: proto.String("base\"v\\al\nue"), }, }, Counter: &dto.Counter{ Value: proto.Float64(.23), }, TimestampMs: proto.Int64(1234567890), }, }, }, { Name: proto.String("name2"), Help: proto.String("doc str\"ing 2"), Type: dto.MetricType_GAUGE.Enum(), Metric: []*dto.Metric{ { Label: []*dto.LabelPair{ { Name: proto.String("labelname"), Value: proto.String("val2"), }, { Name: proto.String("basename"), Value: proto.String("basevalue2"), }, }, Gauge: &dto.Gauge{ Value: proto.Float64(math.Inf(+1)), }, TimestampMs: proto.Int64(54321), }, { Label: []*dto.LabelPair{ { Name: proto.String("labelname"), Value: proto.String("val1"), }, }, Gauge: &dto.Gauge{ Value: proto.Float64(math.Inf(-1)), }, }, }, }, }, }, // 3: The evil summary, mixed with other types and funny comments. { in: ` # TYPE my_summary summary my_summary{n1="val1",quantile="0.5"} 110 decoy -1 -2 my_summary{n1="val1",quantile="0.9"} 140 1 my_summary_count{n1="val1"} 42 # Latest timestamp wins in case of a summary. my_summary_sum{n1="val1"} 4711 2 fake_sum{n1="val1"} 2001 # TYPE another_summary summary another_summary_count{n2="val2",n1="val1"} 20 my_summary_count{n2="val2",n1="val1"} 5 5 another_summary{n1="val1",n2="val2",quantile=".3"} -1.2 my_summary_sum{n1="val2"} 08 15 my_summary{n1="val3", quantile="0.2"} 4711 my_summary{n1="val1",n2="val2",quantile="-12.34",} NaN # some # funny comments # HELP # HELP # HELP my_summary # HELP my_summary `, out: []*dto.MetricFamily{ { Name: proto.String("fake_sum"), Type: dto.MetricType_UNTYPED.Enum(), Metric: []*dto.Metric{ { Label: []*dto.LabelPair{ { Name: proto.String("n1"), Value: proto.String("val1"), }, }, Untyped: &dto.Untyped{ Value: proto.Float64(2001), }, }, }, }, { Name: proto.String("decoy"), Type: dto.MetricType_UNTYPED.Enum(), Metric: []*dto.Metric{ { Untyped: &dto.Untyped{ Value: proto.Float64(-1), }, TimestampMs: proto.Int64(-2), }, }, }, { Name: proto.String("my_summary"), Type: dto.MetricType_SUMMARY.Enum(), Metric: []*dto.Metric{ { Label: []*dto.LabelPair{ { Name: proto.String("n1"), Value: proto.String("val1"), }, }, Summary: &dto.Summary{ SampleCount: proto.Uint64(42), SampleSum: proto.Float64(4711), Quantile: []*dto.Quantile{ { Quantile: proto.Float64(0.5), Value: proto.Float64(110), }, { Quantile: proto.Float64(0.9), Value: proto.Float64(140), }, }, }, TimestampMs: proto.Int64(2), }, { Label: []*dto.LabelPair{ { Name: proto.String("n2"), Value: proto.String("val2"), }, { Name: proto.String("n1"), Value: proto.String("val1"), }, }, Summary: &dto.Summary{ SampleCount: proto.Uint64(5), Quantile: []*dto.Quantile{ { Quantile: proto.Float64(-12.34), Value: proto.Float64(math.NaN()), }, }, }, TimestampMs: proto.Int64(5), }, { Label: []*dto.LabelPair{ { Name: proto.String("n1"), Value: proto.String("val2"), }, }, Summary: &dto.Summary{ SampleSum: proto.Float64(8), }, TimestampMs: proto.Int64(15), }, { Label: []*dto.LabelPair{ { Name: proto.String("n1"), Value: proto.String("val3"), }, }, Summary: &dto.Summary{ Quantile: []*dto.Quantile{ { Quantile: proto.Float64(0.2), Value: proto.Float64(4711), }, }, }, }, }, }, { Name: proto.String("another_summary"), Type: dto.MetricType_SUMMARY.Enum(), Metric: []*dto.Metric{ { Label: []*dto.LabelPair{ { Name: proto.String("n2"), Value: proto.String("val2"), }, { Name: proto.String("n1"), Value: proto.String("val1"), }, }, Summary: &dto.Summary{ SampleCount: proto.Uint64(20), Quantile: []*dto.Quantile{ { Quantile: proto.Float64(0.3), Value: proto.Float64(-1.2), }, }, }, }, }, }, }, }, // 4: The histogram. { in: ` # HELP request_duration_microseconds The response latency. # TYPE request_duration_microseconds histogram request_duration_microseconds_bucket{le="100"} 123 request_duration_microseconds_bucket{le="120"} 412 request_duration_microseconds_bucket{le="144"} 592 request_duration_microseconds_bucket{le="172.8"} 1524 request_duration_microseconds_bucket{le="+Inf"} 2693 request_duration_microseconds_sum 1.7560473e+06 request_duration_microseconds_count 2693 `, out: []*dto.MetricFamily{ { Name: proto.String("request_duration_microseconds"), Help: proto.String("The response latency."), Type: dto.MetricType_HISTOGRAM.Enum(), Metric: []*dto.Metric{ { Histogram: &dto.Histogram{ SampleCount: proto.Uint64(2693), SampleSum: proto.Float64(1756047.3), Bucket: []*dto.Bucket{ { UpperBound: proto.Float64(100), CumulativeCount: proto.Uint64(123), }, { UpperBound: proto.Float64(120), CumulativeCount: proto.Uint64(412), }, { UpperBound: proto.Float64(144), CumulativeCount: proto.Uint64(592), }, { UpperBound: proto.Float64(172.8), CumulativeCount: proto.Uint64(1524), }, { UpperBound: proto.Float64(math.Inf(+1)), CumulativeCount: proto.Uint64(2693), }, }, }, }, }, }, }, }, } for i, scenario := range scenarios { out, err := parser.TextToMetricFamilies(strings.NewReader(scenario.in)) if err != nil { t.Errorf("%d. error: %s", i, err) continue } if expected, got := len(scenario.out), len(out); expected != got { t.Errorf( "%d. expected %d MetricFamilies, got %d", i, expected, got, ) } for _, expected := range scenario.out { got, ok := out[expected.GetName()] if !ok { t.Errorf( "%d. expected MetricFamily %q, found none", i, expected.GetName(), ) continue } if expected.String() != got.String() { t.Errorf( "%d. expected MetricFamily %s, got %s", i, expected, got, ) } } } } func TestTextParse(t *testing.T) { testTextParse(t) } func BenchmarkTextParse(b *testing.B) { for i := 0; i < b.N; i++ { testTextParse(b) } } func testTextParseError(t testing.TB) { scenarios := []struct { in string err string }{ // 0: No new-line at end of input. { in: ` bla 3.14 blubber 42`, err: "text format parsing error in line 3: unexpected end of input stream", }, // 1: Invalid escape sequence in label value. { in: `metric{label="\t"} 3.14`, err: "text format parsing error in line 1: invalid escape sequence", }, // 2: Newline in label value. { in: ` metric{label="new line"} 3.14 `, err: `text format parsing error in line 2: label value "new" contains unescaped new-line`, }, // 3: { in: `metric{@="bla"} 3.14`, err: "text format parsing error in line 1: invalid label name for metric", }, // 4: { in: `metric{__name__="bla"} 3.14`, err: `text format parsing error in line 1: label name "__name__" is reserved`, }, // 5: { in: `metric{label+="bla"} 3.14`, err: "text format parsing error in line 1: expected '=' after label name", }, // 6: { in: `metric{label=bla} 3.14`, err: "text format parsing error in line 1: expected '\"' at start of label value", }, // 7: { in: ` # TYPE metric summary metric{quantile="bla"} 3.14 `, err: "text format parsing error in line 3: expected float as value for 'quantile' label", }, // 8: { in: `metric{label="bla"+} 3.14`, err: "text format parsing error in line 1: unexpected end of label value", }, // 9: { in: `metric{label="bla"} 3.14 2.72 `, err: "text format parsing error in line 1: expected integer as timestamp", }, // 10: { in: `metric{label="bla"} 3.14 2 3 `, err: "text format parsing error in line 1: spurious string after timestamp", }, // 11: { in: `metric{label="bla"} blubb `, err: "text format parsing error in line 1: expected float as value", }, // 12: { in: ` # HELP metric one # HELP metric two `, err: "text format parsing error in line 3: second HELP line for metric name", }, // 13: { in: ` # TYPE metric counter # TYPE metric untyped `, err: `text format parsing error in line 3: second TYPE line for metric name "metric", or TYPE reported after samples`, }, // 14: { in: ` metric 4.12 # TYPE metric counter `, err: `text format parsing error in line 3: second TYPE line for metric name "metric", or TYPE reported after samples`, }, // 14: { in: ` # TYPE metric bla `, err: "text format parsing error in line 2: unknown metric type", }, // 15: { in: ` # TYPE met-ric `, err: "text format parsing error in line 2: invalid metric name in comment", }, // 16: { in: `@invalidmetric{label="bla"} 3.14 2`, err: "text format parsing error in line 1: invalid metric name", }, // 17: { in: `{label="bla"} 3.14 2`, err: "text format parsing error in line 1: invalid metric name", }, // 18: { in: ` # TYPE metric histogram metric_bucket{le="bla"} 3.14 `, err: "text format parsing error in line 3: expected float as value for 'le' label", }, // 19: Invalid UTF-8 in label value. { in: "metric{l=\"\xbd\"} 3.14\n", err: "text format parsing error in line 1: invalid label value \"\\xbd\"", }, // 20: Go 1.13 sometimes allows underscores in numbers. { in: "foo 1_2\n", err: "text format parsing error in line 1: expected float as value", }, // 21: Go 1.13 supports hex floating point. { in: "foo 0x1p-3\n", err: "text format parsing error in line 1: expected float as value", }, // 22: Check for various other literals variants, just in case. { in: "foo 0x1P-3\n", err: "text format parsing error in line 1: expected float as value", }, // 23: { in: "foo 0B1\n", err: "text format parsing error in line 1: expected float as value", }, // 24: { in: "foo 0O1\n", err: "text format parsing error in line 1: expected float as value", }, // 25: { in: "foo 0X1\n", err: "text format parsing error in line 1: expected float as value", }, // 26: { in: "foo 0x1\n", err: "text format parsing error in line 1: expected float as value", }, // 27: { in: "foo 0b1\n", err: "text format parsing error in line 1: expected float as value", }, // 28: { in: "foo 0o1\n", err: "text format parsing error in line 1: expected float as value", }, // 29: { in: "foo 0x1\n", err: "text format parsing error in line 1: expected float as value", }, // 30: { in: "foo 0x1\n", err: "text format parsing error in line 1: expected float as value", }, // 31: Check histogram label. { in: ` # TYPE metric histogram metric_bucket{le="0x1p-3"} 3.14 `, err: "text format parsing error in line 3: expected float as value for 'le' label", }, // 32: Check quantile label. { in: ` # TYPE metric summary metric{quantile="0x1p-3"} 3.14 `, err: "text format parsing error in line 3: expected float as value for 'quantile' label", }, // 33: Check duplicate label. { in: `metric{label="bla",label="bla"} 3.14`, err: "text format parsing error in line 1: duplicate label names for metric", }, } for i, scenario := range scenarios { _, err := parser.TextToMetricFamilies(strings.NewReader(scenario.in)) if err == nil { t.Errorf("%d. expected error, got nil", i) continue } if expected, got := scenario.err, err.Error(); strings.Index(got, expected) != 0 { t.Errorf( "%d. expected error starting with %q, got %q", i, expected, got, ) } } } func TestTextParseError(t *testing.T) { testTextParseError(t) } func BenchmarkParseError(b *testing.B) { for i := 0; i < b.N; i++ { testTextParseError(b) } } func TestTextParserStartOfLine(t *testing.T) { t.Run("EOF", func(t *testing.T) { p := TextParser{} in := strings.NewReader("") p.reset(in) fn := p.startOfLine() if fn != nil { t.Errorf("Unexpected non-nil function: %v", fn) } if p.err != nil { t.Errorf("Unexpected error: %v", p.err) } }) t.Run("OtherError", func(t *testing.T) { p := TextParser{} in := &errReader{err: errors.New("unexpected error")} p.reset(in) fn := p.startOfLine() if fn != nil { t.Errorf("Unexpected non-nil function: %v", fn) } if p.err != nil && !errors.Is(p.err, in.err) { t.Errorf("Unexpected error: %v, expected %v", p.err, in.err) } }) } type errReader struct { err error } func (r *errReader) Read(p []byte) (int, error) { return 0, r.err } golang-github-prometheus-common-0.55.0/go.mod000066400000000000000000000026201463701437000211220ustar00rootroot00000000000000module github.com/prometheus/common go 1.20 require ( github.com/alecthomas/kingpin/v2 v2.4.0 github.com/go-kit/log v0.2.1 github.com/google/go-cmp v0.6.0 github.com/julienschmidt/httprouter v1.3.0 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f github.com/prometheus/client_model v0.6.1 github.com/stretchr/testify v1.9.0 golang.org/x/net v0.26.0 golang.org/x/oauth2 v0.21.0 google.golang.org/protobuf v1.34.2 gopkg.in/yaml.v2 v2.4.0 ) require ( github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-logfmt/logfmt v0.5.1 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.19.1 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/xhit/go-str2duration/v2 v2.1.0 // indirect golang.org/x/sys v0.21.0 // indirect golang.org/x/text v0.16.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) retract v0.50.0 // Critical bug in counter suffixes, please read issue https://github.com/prometheus/common/issues/605 golang-github-prometheus-common-0.55.0/go.sum000066400000000000000000000130601463701437000211470ustar00rootroot00000000000000github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY= github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc= github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA= github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= golang-github-prometheus-common-0.55.0/helpers/000077500000000000000000000000001463701437000214565ustar00rootroot00000000000000golang-github-prometheus-common-0.55.0/helpers/templates/000077500000000000000000000000001463701437000234545ustar00rootroot00000000000000golang-github-prometheus-common-0.55.0/helpers/templates/time.go000066400000000000000000000057111463701437000247450ustar00rootroot00000000000000// Copyright 2024 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package templates import ( "errors" "fmt" "math" "strconv" "time" "github.com/prometheus/common/model" ) var errNaNOrInf = errors.New("value is NaN or Inf") func ConvertToFloat(i interface{}) (float64, error) { switch v := i.(type) { case float64: return v, nil case string: return strconv.ParseFloat(v, 64) case int: return float64(v), nil case uint: return float64(v), nil case int64: return float64(v), nil case uint64: return float64(v), nil case time.Duration: return v.Seconds(), nil default: return 0, fmt.Errorf("can't convert %T to float", v) } } func FloatToTime(v float64) (*time.Time, error) { if math.IsNaN(v) || math.IsInf(v, 0) { return nil, errNaNOrInf } timestamp := v * 1e9 if timestamp > math.MaxInt64 || timestamp < math.MinInt64 { return nil, fmt.Errorf("%v cannot be represented as a nanoseconds timestamp since it overflows int64", v) } t := model.TimeFromUnixNano(int64(timestamp)).Time().UTC() return &t, nil } func HumanizeDuration(i interface{}) (string, error) { v, err := ConvertToFloat(i) if err != nil { return "", err } if math.IsNaN(v) || math.IsInf(v, 0) { return fmt.Sprintf("%.4g", v), nil } if v == 0 { return fmt.Sprintf("%.4gs", v), nil } if math.Abs(v) >= 1 { sign := "" if v < 0 { sign = "-" v = -v } duration := int64(v) seconds := duration % 60 minutes := (duration / 60) % 60 hours := (duration / 60 / 60) % 24 days := duration / 60 / 60 / 24 // For days to minutes, we display seconds as an integer. if days != 0 { return fmt.Sprintf("%s%dd %dh %dm %ds", sign, days, hours, minutes, seconds), nil } if hours != 0 { return fmt.Sprintf("%s%dh %dm %ds", sign, hours, minutes, seconds), nil } if minutes != 0 { return fmt.Sprintf("%s%dm %ds", sign, minutes, seconds), nil } // For seconds, we display 4 significant digits. return fmt.Sprintf("%s%.4gs", sign, v), nil } prefix := "" for _, p := range []string{"m", "u", "n", "p", "f", "a", "z", "y"} { if math.Abs(v) >= 1 { break } prefix = p v *= 1000 } return fmt.Sprintf("%.4g%ss", v, prefix), nil } func HumanizeTimestamp(i interface{}) (string, error) { v, err := ConvertToFloat(i) if err != nil { return "", err } tm, err := FloatToTime(v) switch { case errors.Is(err, errNaNOrInf): return fmt.Sprintf("%.4g", v), nil case err != nil: return "", err } return fmt.Sprint(tm), nil } golang-github-prometheus-common-0.55.0/helpers/templates/time_test.go000066400000000000000000000114221463701437000260000ustar00rootroot00000000000000// Copyright 2024 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package templates import ( "math" "testing" "github.com/stretchr/testify/require" ) func TestHumanizeDuration(t *testing.T) { tc := []struct { name string input interface{} expected string }{ // Integers {name: "zero", input: 0, expected: "0s"}, {name: "one second", input: 1, expected: "1s"}, {name: "one minute", input: 60, expected: "1m 0s"}, {name: "one hour", input: 3600, expected: "1h 0m 0s"}, {name: "one day", input: 86400, expected: "1d 0h 0m 0s"}, {name: "one day and one hour", input: 86400 + 3600, expected: "1d 1h 0m 0s"}, {name: "negative duration", input: -(86400*2 + 3600*3 + 60*4 + 5), expected: "-2d 3h 4m 5s"}, // Float64 with fractions {name: "using a float", input: 899.99, expected: "14m 59s"}, {name: "millseconds", input: .1, expected: "100ms"}, {name: "nanoseconds", input: .0001, expected: "100us"}, {name: "milliseconds + nanoseconds", input: .12345, expected: "123.5ms"}, {name: "minute + millisecond", input: 60.1, expected: "1m 0s"}, {name: "minute + milliseconds", input: 60.5, expected: "1m 0s"}, {name: "second + milliseconds", input: 1.2345, expected: "1.234s"}, {name: "second + milliseconds rounded", input: 12.345, expected: "12.35s"}, // String {name: "zero", input: "0", expected: "0s"}, {name: "second", input: "1", expected: "1s"}, {name: "minute", input: "60", expected: "1m 0s"}, {name: "hour", input: "3600", expected: "1h 0m 0s"}, {name: "day", input: "86400", expected: "1d 0h 0m 0s"}, // String with fractions {name: "millseconds", input: ".1", expected: "100ms"}, {name: "nanoseconds", input: ".0001", expected: "100us"}, {name: "milliseconds + nanoseconds", input: ".12345", expected: "123.5ms"}, {name: "minute + millisecond", input: "60.1", expected: "1m 0s"}, {name: "minute + milliseconds", input: "60.5", expected: "1m 0s"}, {name: "second + milliseconds", input: "1.2345", expected: "1.234s"}, {name: "second + milliseconds rounded", input: "12.345", expected: "12.35s"}, // Int {name: "zero", input: 0, expected: "0s"}, {name: "negative", input: -1, expected: "-1s"}, {name: "second", input: 1, expected: "1s"}, {name: "days", input: 1234567, expected: "14d 6h 56m 7s"}, } for _, tt := range tc { t.Run(tt.name, func(t *testing.T) { result, err := HumanizeDuration(tt.input) require.NoError(t, err) require.Equal(t, tt.expected, result) }) } } func TestHumanizeDurationErrorString(t *testing.T) { _, err := HumanizeDuration("one") require.Error(t, err) } func TestHumanizeTimestamp(t *testing.T) { tc := []struct { name string input interface{} expected string }{ // Int {name: "zero", input: 0, expected: "1970-01-01 00:00:00 +0000 UTC"}, {name: "negative", input: -1, expected: "1969-12-31 23:59:59 +0000 UTC"}, {name: "one", input: 1, expected: "1970-01-01 00:00:01 +0000 UTC"}, {name: "past", input: 1234567, expected: "1970-01-15 06:56:07 +0000 UTC"}, {name: "future", input: 9223372036, expected: "2262-04-11 23:47:16 +0000 UTC"}, // Uint {name: "zero", input: uint64(0), expected: "1970-01-01 00:00:00 +0000 UTC"}, {name: "one", input: uint64(1), expected: "1970-01-01 00:00:01 +0000 UTC"}, {name: "past", input: uint64(1234567), expected: "1970-01-15 06:56:07 +0000 UTC"}, {name: "future", input: uint64(9223372036), expected: "2262-04-11 23:47:16 +0000 UTC"}, // NaN/Inf, strings {name: "infinity", input: "+Inf", expected: "+Inf"}, {name: "minus infinity", input: "-Inf", expected: "-Inf"}, {name: "NaN", input: "NaN", expected: "NaN"}, // Nan/Inf, float64 {name: "infinity", input: math.Inf(1), expected: "+Inf"}, {name: "minus infinity", input: math.Inf(-1), expected: "-Inf"}, {name: "NaN", input: math.NaN(), expected: "NaN"}, // Sampled data {name: "sample float64", input: 1435065584.128, expected: "2015-06-23 13:19:44.128 +0000 UTC"}, {name: "sample string", input: "1435065584.128", expected: "2015-06-23 13:19:44.128 +0000 UTC"}, } for _, tt := range tc { t.Run(tt.name, func(t *testing.T) { result, err := HumanizeTimestamp(tt.input) require.NoError(t, err) require.Equal(t, tt.expected, result) }) } } func TestHumanizeTimestampError(t *testing.T) { _, err := HumanizeTimestamp(math.MaxInt64) require.Error(t, err) } golang-github-prometheus-common-0.55.0/model/000077500000000000000000000000001463701437000211145ustar00rootroot00000000000000golang-github-prometheus-common-0.55.0/model/alert.go000066400000000000000000000104071463701437000225540ustar00rootroot00000000000000// Copyright 2013 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package model import ( "fmt" "time" ) type AlertStatus string const ( AlertFiring AlertStatus = "firing" AlertResolved AlertStatus = "resolved" ) // Alert is a generic representation of an alert in the Prometheus eco-system. type Alert struct { // Label value pairs for purpose of aggregation, matching, and disposition // dispatching. This must minimally include an "alertname" label. Labels LabelSet `json:"labels"` // Extra key/value information which does not define alert identity. Annotations LabelSet `json:"annotations"` // The known time range for this alert. Both ends are optional. StartsAt time.Time `json:"startsAt,omitempty"` EndsAt time.Time `json:"endsAt,omitempty"` GeneratorURL string `json:"generatorURL"` } // Name returns the name of the alert. It is equivalent to the "alertname" label. func (a *Alert) Name() string { return string(a.Labels[AlertNameLabel]) } // Fingerprint returns a unique hash for the alert. It is equivalent to // the fingerprint of the alert's label set. func (a *Alert) Fingerprint() Fingerprint { return a.Labels.Fingerprint() } func (a *Alert) String() string { s := fmt.Sprintf("%s[%s]", a.Name(), a.Fingerprint().String()[:7]) if a.Resolved() { return s + "[resolved]" } return s + "[active]" } // Resolved returns true iff the activity interval ended in the past. func (a *Alert) Resolved() bool { return a.ResolvedAt(time.Now()) } // ResolvedAt returns true off the activity interval ended before // the given timestamp. func (a *Alert) ResolvedAt(ts time.Time) bool { if a.EndsAt.IsZero() { return false } return !a.EndsAt.After(ts) } // Status returns the status of the alert. func (a *Alert) Status() AlertStatus { return a.StatusAt(time.Now()) } // StatusAt returns the status of the alert at the given timestamp. func (a *Alert) StatusAt(ts time.Time) AlertStatus { if a.ResolvedAt(ts) { return AlertResolved } return AlertFiring } // Validate checks whether the alert data is inconsistent. func (a *Alert) Validate() error { if a.StartsAt.IsZero() { return fmt.Errorf("start time missing") } if !a.EndsAt.IsZero() && a.EndsAt.Before(a.StartsAt) { return fmt.Errorf("start time must be before end time") } if err := a.Labels.Validate(); err != nil { return fmt.Errorf("invalid label set: %w", err) } if len(a.Labels) == 0 { return fmt.Errorf("at least one label pair required") } if err := a.Annotations.Validate(); err != nil { return fmt.Errorf("invalid annotations: %w", err) } return nil } // Alert is a list of alerts that can be sorted in chronological order. type Alerts []*Alert func (as Alerts) Len() int { return len(as) } func (as Alerts) Swap(i, j int) { as[i], as[j] = as[j], as[i] } func (as Alerts) Less(i, j int) bool { if as[i].StartsAt.Before(as[j].StartsAt) { return true } if as[i].EndsAt.Before(as[j].EndsAt) { return true } return as[i].Fingerprint() < as[j].Fingerprint() } // HasFiring returns true iff one of the alerts is not resolved. func (as Alerts) HasFiring() bool { for _, a := range as { if !a.Resolved() { return true } } return false } // HasFiringAt returns true iff one of the alerts is not resolved // at the time ts. func (as Alerts) HasFiringAt(ts time.Time) bool { for _, a := range as { if !a.ResolvedAt(ts) { return true } } return false } // Status returns StatusFiring iff at least one of the alerts is firing. func (as Alerts) Status() AlertStatus { if as.HasFiring() { return AlertFiring } return AlertResolved } // StatusAt returns StatusFiring iff at least one of the alerts is firing // at the time ts. func (as Alerts) StatusAt(ts time.Time) AlertStatus { if as.HasFiringAt(ts) { return AlertFiring } return AlertResolved } golang-github-prometheus-common-0.55.0/model/alert_test.go000066400000000000000000000202761463701437000236200ustar00rootroot00000000000000// Copyright 2013 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package model import ( "fmt" "sort" "strings" "testing" "time" ) func TestAlertValidate(t *testing.T) { ts := time.Now() cases := []struct { alert *Alert err string }{ { alert: &Alert{ Labels: LabelSet{"a": "b"}, StartsAt: ts, }, }, { alert: &Alert{ Labels: LabelSet{"a": "b"}, }, err: "start time missing", }, { alert: &Alert{ Labels: LabelSet{"a": "b"}, StartsAt: ts, EndsAt: ts, }, }, { alert: &Alert{ Labels: LabelSet{"a": "b"}, StartsAt: ts, EndsAt: ts.Add(1 * time.Minute), }, }, { alert: &Alert{ Labels: LabelSet{"a": "b"}, StartsAt: ts, EndsAt: ts.Add(-1 * time.Minute), }, err: "start time must be before end time", }, { alert: &Alert{ StartsAt: ts, }, err: "at least one label pair required", }, { alert: &Alert{ Labels: LabelSet{"a": "b", "!bad": "label"}, StartsAt: ts, }, err: "invalid label set: invalid name", }, { alert: &Alert{ Labels: LabelSet{"a": "b", "bad": "\xfflabel"}, StartsAt: ts, }, err: "invalid label set: invalid value", }, { alert: &Alert{ Labels: LabelSet{"a": "b"}, Annotations: LabelSet{"!bad": "label"}, StartsAt: ts, }, err: "invalid annotations: invalid name", }, { alert: &Alert{ Labels: LabelSet{"a": "b"}, Annotations: LabelSet{"bad": "\xfflabel"}, StartsAt: ts, }, err: "invalid annotations: invalid value", }, } for i, c := range cases { err := c.alert.Validate() if err == nil { if c.err == "" { continue } t.Errorf("%d. Expected error %q but got none", i, c.err) continue } if c.err == "" { t.Errorf("%d. Expected no error but got %q", i, err) continue } if !strings.Contains(err.Error(), c.err) { t.Errorf("%d. Expected error to contain %q but got %q", i, c.err, err) } } } func TestAlert(t *testing.T) { // Verifying that an alert with no EndsAt field is unresolved and has firing status. alert := &Alert{ Labels: LabelSet{"foo": "bar", "lorem": "ipsum"}, StartsAt: time.Now(), } actual := fmt.Sprint(alert) expected := "[d181d0f][active]" if actual != expected { t.Errorf("expected %s, but got %s", expected, actual) } actualStatus := alert.Status() expectedStatus := AlertStatus("firing") if actualStatus != expectedStatus { t.Errorf("expected alertStatus %s, but got %s", expectedStatus, actualStatus) } // Verifying that an alert with an EndsAt time before the current time is resolved and has resolved status. ts := time.Now() ts1 := ts.Add(-2 * time.Minute) ts2 := ts.Add(-1 * time.Minute) alert = &Alert{ Labels: LabelSet{"foo": "bar", "lorem": "ipsum"}, StartsAt: ts1, EndsAt: ts2, } if !alert.Resolved() { t.Error("expected alert to be resolved, but it was not") } actual = fmt.Sprint(alert) expected = "[d181d0f][resolved]" if actual != expected { t.Errorf("expected %s, but got %s", expected, actual) } actualStatus = alert.Status() expectedStatus = "resolved" if actualStatus != expectedStatus { t.Errorf("expected alertStatus %s, but got %s", expectedStatus, actualStatus) } // Verifying that ResolvedAt works for different times if alert.ResolvedAt(ts1) { t.Error("unexpected alert was resolved at start time") } if alert.ResolvedAt(ts2.Add(-time.Millisecond)) { t.Error("unexpected alert was resolved before it ended") } if !alert.ResolvedAt(ts2) { t.Error("expected alert to be resolved at end time") } if !alert.ResolvedAt(ts2.Add(time.Millisecond)) { t.Error("expected alert to be resolved after it ended") } // Verifying that StatusAt works for different times actualStatus = alert.StatusAt(ts1) if actualStatus != "firing" { t.Errorf("expected alert to be firing at start time, but got %s", actualStatus) } actualStatus = alert.StatusAt(ts1.Add(-time.Millisecond)) if actualStatus != "firing" { t.Errorf("expected alert to be firing before it ended, but got %s", actualStatus) } actualStatus = alert.StatusAt(ts2) if actualStatus != "resolved" { t.Errorf("expected alert to be resolved at end time, but got %s", actualStatus) } actualStatus = alert.StatusAt(ts2.Add(time.Millisecond)) if actualStatus != "resolved" { t.Errorf("expected alert to be resolved after it ended, but got %s", actualStatus) } } func TestSortAlerts(t *testing.T) { ts := time.Now() alerts := Alerts{ { Labels: LabelSet{ "alertname": "InternalError", "dev": "sda3", }, StartsAt: ts.Add(-6 * time.Minute), EndsAt: ts.Add(-3 * time.Minute), }, { Labels: LabelSet{ "alertname": "DiskFull", "dev": "sda1", }, StartsAt: ts.Add(-5 * time.Minute), EndsAt: ts.Add(-4 * time.Minute), }, { Labels: LabelSet{ "alertname": "OutOfMemory", "dev": "sda1", }, StartsAt: ts.Add(-2 * time.Minute), EndsAt: ts.Add(-1 * time.Minute), }, { Labels: LabelSet{ "alertname": "DiskFull", "dev": "sda2", }, StartsAt: ts.Add(-2 * time.Minute), EndsAt: ts.Add(-3 * time.Minute), }, { Labels: LabelSet{ "alertname": "OutOfMemory", "dev": "sda2", }, StartsAt: ts.Add(-5 * time.Minute), EndsAt: ts.Add(-2 * time.Minute), }, } sort.Sort(alerts) expected := []string{ "DiskFull[5ffe595][resolved]", "InternalError[09cfd46][resolved]", "OutOfMemory[d43a602][resolved]", "DiskFull[5ff4595][resolved]", "OutOfMemory[d444602][resolved]", } for i := range alerts { if alerts[i].String() != expected[i] { t.Errorf("expected alert %s at index %d, but got %s", expected[i], i, alerts[i].String()) } } } func TestAlertsStatus(t *testing.T) { ts := time.Now() firingAlerts := Alerts{ { Labels: LabelSet{ "foo": "bar", }, StartsAt: ts, }, { Labels: LabelSet{ "bar": "baz", }, StartsAt: ts, }, } actualStatus := firingAlerts.Status() expectedStatus := AlertFiring if actualStatus != expectedStatus { t.Errorf("expected status %s, but got %s", expectedStatus, actualStatus) } actualStatus = firingAlerts.StatusAt(ts) if actualStatus != expectedStatus { t.Errorf("expected status %s, but got %s", expectedStatus, actualStatus) } ts = time.Now() resolvedAlerts := Alerts{ { Labels: LabelSet{ "foo": "bar", }, StartsAt: ts.Add(-1 * time.Minute), EndsAt: ts, }, { Labels: LabelSet{ "bar": "baz", }, StartsAt: ts.Add(-1 * time.Minute), EndsAt: ts, }, } actualStatus = resolvedAlerts.Status() expectedStatus = AlertResolved if actualStatus != expectedStatus { t.Errorf("expected status %s, but got %s", expectedStatus, actualStatus) } actualStatus = resolvedAlerts.StatusAt(ts) expectedStatus = AlertResolved if actualStatus != expectedStatus { t.Errorf("expected status %s, but got %s", expectedStatus, actualStatus) } ts = time.Now() mixedAlerts := Alerts{ { Labels: LabelSet{ "foo": "bar", }, StartsAt: ts.Add(-1 * time.Minute), EndsAt: ts.Add(5 * time.Minute), }, { Labels: LabelSet{ "bar": "baz", }, StartsAt: ts.Add(-1 * time.Minute), EndsAt: ts, }, } actualStatus = mixedAlerts.Status() expectedStatus = AlertFiring if actualStatus != expectedStatus { t.Errorf("expected status %s, but got %s", expectedStatus, actualStatus) } actualStatus = mixedAlerts.StatusAt(ts) expectedStatus = AlertFiring if actualStatus != expectedStatus { t.Errorf("expected status %s, but got %s", expectedStatus, actualStatus) } actualStatus = mixedAlerts.StatusAt(ts.Add(5 * time.Minute)) expectedStatus = AlertResolved if actualStatus != expectedStatus { t.Errorf("expected status %s, but got %s", expectedStatus, actualStatus) } } golang-github-prometheus-common-0.55.0/model/fingerprinting.go000066400000000000000000000050071463701437000244720ustar00rootroot00000000000000// Copyright 2013 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package model import ( "fmt" "strconv" ) // Fingerprint provides a hash-capable representation of a Metric. // For our purposes, FNV-1A 64-bit is used. type Fingerprint uint64 // FingerprintFromString transforms a string representation into a Fingerprint. func FingerprintFromString(s string) (Fingerprint, error) { num, err := strconv.ParseUint(s, 16, 64) return Fingerprint(num), err } // ParseFingerprint parses the input string into a fingerprint. func ParseFingerprint(s string) (Fingerprint, error) { num, err := strconv.ParseUint(s, 16, 64) if err != nil { return 0, err } return Fingerprint(num), nil } func (f Fingerprint) String() string { return fmt.Sprintf("%016x", uint64(f)) } // Fingerprints represents a collection of Fingerprint subject to a given // natural sorting scheme. It implements sort.Interface. type Fingerprints []Fingerprint // Len implements sort.Interface. func (f Fingerprints) Len() int { return len(f) } // Less implements sort.Interface. func (f Fingerprints) Less(i, j int) bool { return f[i] < f[j] } // Swap implements sort.Interface. func (f Fingerprints) Swap(i, j int) { f[i], f[j] = f[j], f[i] } // FingerprintSet is a set of Fingerprints. type FingerprintSet map[Fingerprint]struct{} // Equal returns true if both sets contain the same elements (and not more). func (s FingerprintSet) Equal(o FingerprintSet) bool { if len(s) != len(o) { return false } for k := range s { if _, ok := o[k]; !ok { return false } } return true } // Intersection returns the elements contained in both sets. func (s FingerprintSet) Intersection(o FingerprintSet) FingerprintSet { myLength, otherLength := len(s), len(o) if myLength == 0 || otherLength == 0 { return FingerprintSet{} } subSet := s superSet := o if otherLength < myLength { subSet = o superSet = s } out := FingerprintSet{} for k := range subSet { if _, ok := superSet[k]; ok { out[k] = struct{}{} } } return out } golang-github-prometheus-common-0.55.0/model/fingerprinting_test.go000066400000000000000000000116561463701437000255400ustar00rootroot00000000000000// Copyright 2019 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package model import ( "sort" "testing" ) func TestFingerprintFromString(t *testing.T) { fs := "4294967295" f, err := FingerprintFromString(fs) if err != nil { t.Errorf("unexpected error while getting Fingerprint from string: %s", err.Error()) } expected := Fingerprint(285960729237) if expected != f { t.Errorf("expected to get %d, but got %d instead", f, expected) } f, err = ParseFingerprint(fs) if err != nil { t.Errorf("unexpected error while getting Fingerprint from string: %s", err.Error()) } if expected != f { t.Errorf("expected to get %d, but got %d instead", f, expected) } } func TestFingerprintsSort(t *testing.T) { fingerPrints := Fingerprints{ 14695981039346656037, 285960729237, 0, 4294967295, 285960729237, 18446744073709551615, } sort.Sort(fingerPrints) expected := Fingerprints{ 0, 4294967295, 285960729237, 285960729237, 14695981039346656037, 18446744073709551615, } for i, f := range fingerPrints { if f != expected[i] { t.Errorf("expected Fingerprint %d, but got %d for index %d", expected[i], f, i) } } } func TestFingerprintSet(t *testing.T) { // Testing with two sets of unequal length. f := FingerprintSet{ 14695981039346656037: struct{}{}, 0: struct{}{}, 4294967295: struct{}{}, 285960729237: struct{}{}, 18446744073709551615: struct{}{}, } f2 := FingerprintSet{ 285960729237: struct{}{}, } if f.Equal(f2) { t.Errorf("expected two FingerPrintSets of unequal length to be unequal") } // Testing with two unequal sets of equal length. f = FingerprintSet{ 14695981039346656037: struct{}{}, 0: struct{}{}, 4294967295: struct{}{}, } f2 = FingerprintSet{ 14695981039346656037: struct{}{}, 0: struct{}{}, 285960729237: struct{}{}, } if f.Equal(f2) { t.Errorf("expected two FingerPrintSets of unequal content to be unequal") } // Testing with equal sets of equal length. f = FingerprintSet{ 14695981039346656037: struct{}{}, 0: struct{}{}, 4294967295: struct{}{}, } f2 = FingerprintSet{ 14695981039346656037: struct{}{}, 0: struct{}{}, 4294967295: struct{}{}, } if !f.Equal(f2) { t.Errorf("expected two FingerPrintSets of equal content to be equal") } } func TestFingerprintIntersection(t *testing.T) { scenarios := []struct { name string input1 FingerprintSet input2 FingerprintSet expected FingerprintSet }{ { name: "two empty sets", input1: FingerprintSet{}, input2: FingerprintSet{}, expected: FingerprintSet{}, }, { name: "one empty set", input1: FingerprintSet{ 0: struct{}{}, }, input2: FingerprintSet{}, expected: FingerprintSet{}, }, { name: "two non-empty unequal sets", input1: FingerprintSet{ 14695981039346656037: struct{}{}, 0: struct{}{}, 4294967295: struct{}{}, }, input2: FingerprintSet{ 14695981039346656037: struct{}{}, 0: struct{}{}, 4294967295: struct{}{}, }, expected: FingerprintSet{ 14695981039346656037: struct{}{}, 0: struct{}{}, 4294967295: struct{}{}, }, }, { name: "two non-empty equal sets", input1: FingerprintSet{ 14695981039346656037: struct{}{}, 0: struct{}{}, 285960729237: struct{}{}, }, input2: FingerprintSet{ 14695981039346656037: struct{}{}, 0: struct{}{}, 4294967295: struct{}{}, }, expected: FingerprintSet{ 14695981039346656037: struct{}{}, 0: struct{}{}, }, }, { name: "two non-empty equal sets of unequal length", input1: FingerprintSet{ 14695981039346656037: struct{}{}, 0: struct{}{}, 285960729237: struct{}{}, }, input2: FingerprintSet{ 14695981039346656037: struct{}{}, 0: struct{}{}, }, expected: FingerprintSet{ 14695981039346656037: struct{}{}, 0: struct{}{}, }, }, } for _, scenario := range scenarios { s1 := scenario.input1 s2 := scenario.input2 actual := s1.Intersection(s2) if !actual.Equal(scenario.expected) { t.Errorf("expected %v to be equal to %v", actual, scenario.expected) } } } golang-github-prometheus-common-0.55.0/model/fnv.go000066400000000000000000000022471463701437000222410ustar00rootroot00000000000000// Copyright 2015 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package model // Inline and byte-free variant of hash/fnv's fnv64a. const ( offset64 = 14695981039346656037 prime64 = 1099511628211 ) // hashNew initializes a new fnv64a hash value. func hashNew() uint64 { return offset64 } // hashAdd adds a string to a fnv64a hash value, returning the updated hash. func hashAdd(h uint64, s string) uint64 { for i := 0; i < len(s); i++ { h ^= uint64(s[i]) h *= prime64 } return h } // hashAddByte adds a byte to a fnv64a hash value, returning the updated hash. func hashAddByte(h uint64, b byte) uint64 { h ^= uint64(b) h *= prime64 return h } golang-github-prometheus-common-0.55.0/model/labels.go000066400000000000000000000143731463701437000227150ustar00rootroot00000000000000// Copyright 2013 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package model import ( "encoding/json" "fmt" "regexp" "strings" "unicode/utf8" ) const ( // AlertNameLabel is the name of the label containing the an alert's name. AlertNameLabel = "alertname" // ExportedLabelPrefix is the prefix to prepend to the label names present in // exported metrics if a label of the same name is added by the server. ExportedLabelPrefix = "exported_" // MetricNameLabel is the label name indicating the metric name of a // timeseries. MetricNameLabel = "__name__" // SchemeLabel is the name of the label that holds the scheme on which to // scrape a target. SchemeLabel = "__scheme__" // AddressLabel is the name of the label that holds the address of // a scrape target. AddressLabel = "__address__" // MetricsPathLabel is the name of the label that holds the path on which to // scrape a target. MetricsPathLabel = "__metrics_path__" // ScrapeIntervalLabel is the name of the label that holds the scrape interval // used to scrape a target. ScrapeIntervalLabel = "__scrape_interval__" // ScrapeTimeoutLabel is the name of the label that holds the scrape // timeout used to scrape a target. ScrapeTimeoutLabel = "__scrape_timeout__" // ReservedLabelPrefix is a prefix which is not legal in user-supplied // label names. ReservedLabelPrefix = "__" // MetaLabelPrefix is a prefix for labels that provide meta information. // Labels with this prefix are used for intermediate label processing and // will not be attached to time series. MetaLabelPrefix = "__meta_" // TmpLabelPrefix is a prefix for temporary labels as part of relabelling. // Labels with this prefix are used for intermediate label processing and // will not be attached to time series. This is reserved for use in // Prometheus configuration files by users. TmpLabelPrefix = "__tmp_" // ParamLabelPrefix is a prefix for labels that provide URL parameters // used to scrape a target. ParamLabelPrefix = "__param_" // JobLabel is the label name indicating the job from which a timeseries // was scraped. JobLabel = "job" // InstanceLabel is the label name used for the instance label. InstanceLabel = "instance" // BucketLabel is used for the label that defines the upper bound of a // bucket of a histogram ("le" -> "less or equal"). BucketLabel = "le" // QuantileLabel is used for the label that defines the quantile in a // summary. QuantileLabel = "quantile" ) // LabelNameRE is a regular expression matching valid label names. Note that the // IsValid method of LabelName performs the same check but faster than a match // with this regular expression. var LabelNameRE = regexp.MustCompile("^[a-zA-Z_][a-zA-Z0-9_]*$") // A LabelName is a key for a LabelSet or Metric. It has a value associated // therewith. type LabelName string // IsValid returns true iff name matches the pattern of LabelNameRE for legacy // names, and iff it's valid UTF-8 if NameValidationScheme is set to // UTF8Validation. For the legacy matching, it does not use LabelNameRE for the // check but a much faster hardcoded implementation. func (ln LabelName) IsValid() bool { if len(ln) == 0 { return false } switch NameValidationScheme { case LegacyValidation: for i, b := range ln { if !((b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || b == '_' || (b >= '0' && b <= '9' && i > 0)) { return false } } case UTF8Validation: return utf8.ValidString(string(ln)) default: panic(fmt.Sprintf("Invalid name validation scheme requested: %d", NameValidationScheme)) } return true } // UnmarshalYAML implements the yaml.Unmarshaler interface. func (ln *LabelName) UnmarshalYAML(unmarshal func(interface{}) error) error { var s string if err := unmarshal(&s); err != nil { return err } if !LabelName(s).IsValid() { return fmt.Errorf("%q is not a valid label name", s) } *ln = LabelName(s) return nil } // UnmarshalJSON implements the json.Unmarshaler interface. func (ln *LabelName) UnmarshalJSON(b []byte) error { var s string if err := json.Unmarshal(b, &s); err != nil { return err } if !LabelName(s).IsValid() { return fmt.Errorf("%q is not a valid label name", s) } *ln = LabelName(s) return nil } // LabelNames is a sortable LabelName slice. In implements sort.Interface. type LabelNames []LabelName func (l LabelNames) Len() int { return len(l) } func (l LabelNames) Less(i, j int) bool { return l[i] < l[j] } func (l LabelNames) Swap(i, j int) { l[i], l[j] = l[j], l[i] } func (l LabelNames) String() string { labelStrings := make([]string, 0, len(l)) for _, label := range l { labelStrings = append(labelStrings, string(label)) } return strings.Join(labelStrings, ", ") } // A LabelValue is an associated value for a LabelName. type LabelValue string // IsValid returns true iff the string is a valid UTF-8. func (lv LabelValue) IsValid() bool { return utf8.ValidString(string(lv)) } // LabelValues is a sortable LabelValue slice. It implements sort.Interface. type LabelValues []LabelValue func (l LabelValues) Len() int { return len(l) } func (l LabelValues) Less(i, j int) bool { return string(l[i]) < string(l[j]) } func (l LabelValues) Swap(i, j int) { l[i], l[j] = l[j], l[i] } // LabelPair pairs a name with a value. type LabelPair struct { Name LabelName Value LabelValue } // LabelPairs is a sortable slice of LabelPair pointers. It implements // sort.Interface. type LabelPairs []*LabelPair func (l LabelPairs) Len() int { return len(l) } func (l LabelPairs) Less(i, j int) bool { switch { case l[i].Name > l[j].Name: return false case l[i].Name < l[j].Name: return true case l[i].Value > l[j].Value: return false case l[i].Value < l[j].Value: return true default: return false } } func (l LabelPairs) Swap(i, j int) { l[i], l[j] = l[j], l[i] } golang-github-prometheus-common-0.55.0/model/labels_test.go000066400000000000000000000102641463701437000237470ustar00rootroot00000000000000// Copyright 2013 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package model import ( "sort" "testing" ) func testLabelNames(t testing.TB) { scenarios := []struct { in LabelNames out LabelNames }{ { in: LabelNames{"ZZZ", "zzz"}, out: LabelNames{"ZZZ", "zzz"}, }, { in: LabelNames{"aaa", "AAA"}, out: LabelNames{"AAA", "aaa"}, }, } for i, scenario := range scenarios { sort.Sort(scenario.in) for j, expected := range scenario.out { if expected != scenario.in[j] { t.Errorf("%d.%d expected %s, got %s", i, j, expected, scenario.in[j]) } } } } func TestLabelNames(t *testing.T) { testLabelNames(t) } func BenchmarkLabelNames(b *testing.B) { for i := 0; i < b.N; i++ { testLabelNames(b) } } func testLabelValues(t testing.TB) { scenarios := []struct { in LabelValues out LabelValues }{ { in: LabelValues{"ZZZ", "zzz"}, out: LabelValues{"ZZZ", "zzz"}, }, { in: LabelValues{"aaa", "AAA"}, out: LabelValues{"AAA", "aaa"}, }, } for i, scenario := range scenarios { sort.Sort(scenario.in) for j, expected := range scenario.out { if expected != scenario.in[j] { t.Errorf("%d.%d expected %s, got %s", i, j, expected, scenario.in[j]) } } } } func TestLabelValues(t *testing.T) { testLabelValues(t) } func BenchmarkLabelValues(b *testing.B) { for i := 0; i < b.N; i++ { testLabelValues(b) } } func TestLabelNameIsValid(t *testing.T) { scenarios := []struct { ln LabelName legacyValid bool utf8Valid bool }{ { ln: "Avalid_23name", legacyValid: true, utf8Valid: true, }, { ln: "_Avalid_23name", legacyValid: true, utf8Valid: true, }, { ln: "1valid_23name", legacyValid: false, utf8Valid: true, }, { ln: "avalid_23name", legacyValid: true, utf8Valid: true, }, { ln: "Ava:lid_23name", legacyValid: false, utf8Valid: true, }, { ln: "a lid_23name", legacyValid: false, utf8Valid: true, }, { ln: ":leading_colon", legacyValid: false, utf8Valid: true, }, { ln: "colon:in:the:middle", legacyValid: false, utf8Valid: true, }, { ln: "a\xc5z", legacyValid: false, utf8Valid: false, }, } for _, s := range scenarios { NameValidationScheme = LegacyValidation if s.ln.IsValid() != s.legacyValid { t.Errorf("Expected %v for %q using legacy IsValid method", s.legacyValid, s.ln) } if LabelNameRE.MatchString(string(s.ln)) != s.legacyValid { t.Errorf("Expected %v for %q using legacy regexp match", s.legacyValid, s.ln) } NameValidationScheme = UTF8Validation if s.ln.IsValid() != s.utf8Valid { t.Errorf("Expected %v for %q using UTF-8 IsValid method", s.legacyValid, s.ln) } } } func TestSortLabelPairs(t *testing.T) { labelPairs := LabelPairs{ { Name: "FooName", Value: "FooValue", }, { Name: "FooName", Value: "BarValue", }, { Name: "BarName", Value: "FooValue", }, { Name: "BazName", Value: "BazValue", }, { Name: "BarName", Value: "FooValue", }, { Name: "BazName", Value: "FazValue", }, } sort.Sort(labelPairs) expectedLabelPairs := LabelPairs{ { Name: "BarName", Value: "FooValue", }, { Name: "BarName", Value: "FooValue", }, { Name: "BazName", Value: "BazValue", }, { Name: "BazName", Value: "FazValue", }, { Name: "FooName", Value: "BarValue", }, } for i, expected := range expectedLabelPairs { if expected.Name != labelPairs[i].Name || expected.Value != labelPairs[i].Value { t.Errorf("%d expected %s, got %s", i, expected, labelPairs[i]) } } } golang-github-prometheus-common-0.55.0/model/labelset.go000066400000000000000000000077131463701437000232460ustar00rootroot00000000000000// Copyright 2013 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package model import ( "encoding/json" "fmt" "sort" ) // A LabelSet is a collection of LabelName and LabelValue pairs. The LabelSet // may be fully-qualified down to the point where it may resolve to a single // Metric in the data store or not. All operations that occur within the realm // of a LabelSet can emit a vector of Metric entities to which the LabelSet may // match. type LabelSet map[LabelName]LabelValue // Validate checks whether all names and values in the label set // are valid. func (ls LabelSet) Validate() error { for ln, lv := range ls { if !ln.IsValid() { return fmt.Errorf("invalid name %q", ln) } if !lv.IsValid() { return fmt.Errorf("invalid value %q", lv) } } return nil } // Equal returns true iff both label sets have exactly the same key/value pairs. func (ls LabelSet) Equal(o LabelSet) bool { if len(ls) != len(o) { return false } for ln, lv := range ls { olv, ok := o[ln] if !ok { return false } if olv != lv { return false } } return true } // Before compares the metrics, using the following criteria: // // If m has fewer labels than o, it is before o. If it has more, it is not. // // If the number of labels is the same, the superset of all label names is // sorted alphanumerically. The first differing label pair found in that order // determines the outcome: If the label does not exist at all in m, then m is // before o, and vice versa. Otherwise the label value is compared // alphanumerically. // // If m and o are equal, the method returns false. func (ls LabelSet) Before(o LabelSet) bool { if len(ls) < len(o) { return true } if len(ls) > len(o) { return false } lns := make(LabelNames, 0, len(ls)+len(o)) for ln := range ls { lns = append(lns, ln) } for ln := range o { lns = append(lns, ln) } // It's probably not worth it to de-dup lns. sort.Sort(lns) for _, ln := range lns { mlv, ok := ls[ln] if !ok { return true } olv, ok := o[ln] if !ok { return false } if mlv < olv { return true } if mlv > olv { return false } } return false } // Clone returns a copy of the label set. func (ls LabelSet) Clone() LabelSet { lsn := make(LabelSet, len(ls)) for ln, lv := range ls { lsn[ln] = lv } return lsn } // Merge is a helper function to non-destructively merge two label sets. func (l LabelSet) Merge(other LabelSet) LabelSet { result := make(LabelSet, len(l)) for k, v := range l { result[k] = v } for k, v := range other { result[k] = v } return result } // Fingerprint returns the LabelSet's fingerprint. func (ls LabelSet) Fingerprint() Fingerprint { return labelSetToFingerprint(ls) } // FastFingerprint returns the LabelSet's Fingerprint calculated by a faster hashing // algorithm, which is, however, more susceptible to hash collisions. func (ls LabelSet) FastFingerprint() Fingerprint { return labelSetToFastFingerprint(ls) } // UnmarshalJSON implements the json.Unmarshaler interface. func (l *LabelSet) UnmarshalJSON(b []byte) error { var m map[LabelName]LabelValue if err := json.Unmarshal(b, &m); err != nil { return err } // encoding/json only unmarshals maps of the form map[string]T. It treats // LabelName as a string and does not call its UnmarshalJSON method. // Thus, we have to replicate the behavior here. for ln := range m { if !ln.IsValid() { return fmt.Errorf("%q is not a valid label name", ln) } } *l = LabelSet(m) return nil } golang-github-prometheus-common-0.55.0/model/labelset_go120_test.go000066400000000000000000000070531463701437000252120ustar00rootroot00000000000000// Copyright 2024 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build !go1.21 package model import ( "encoding/json" "testing" ) func TestUnmarshalJSONLabelSet(t *testing.T) { type testConfig struct { LabelSet LabelSet `yaml:"labelSet,omitempty"` } // valid LabelSet JSON labelSetJSON := `{ "labelSet": { "monitor": "codelab", "foo": "bar", "foo2": "bar", "abc": "prometheus", "foo11": "bar11" } }` var c testConfig err := json.Unmarshal([]byte(labelSetJSON), &c) if err != nil { t.Errorf("unexpected error while marshalling JSON : %s", err.Error()) } labelSetString := c.LabelSet.String() expected := `{abc="prometheus", foo="bar", foo11="bar11", foo2="bar", monitor="codelab"}` if expected != labelSetString { t.Errorf("expected %s but got %s", expected, labelSetString) } // invalid LabelSet JSON invalidlabelSetJSON := `{ "labelSet": { "1nvalid_23name": "codelab", "foo": "bar" } }` NameValidationScheme = LegacyValidation err = json.Unmarshal([]byte(invalidlabelSetJSON), &c) expectedErr := `"1nvalid_23name" is not a valid label name` if err == nil || err.Error() != expectedErr { t.Errorf("expected an error with message '%s' to be thrown", expectedErr) } } func TestLabelSetClone(t *testing.T) { labelSet := LabelSet{ "monitor": "codelab", "foo": "bar", "bar": "baz", } cloneSet := labelSet.Clone() if len(labelSet) != len(cloneSet) { t.Errorf("expected the length of the cloned Label set to be %d, but got %d", len(labelSet), len(cloneSet)) } for ln, lv := range labelSet { expected := cloneSet[ln] if expected != lv { t.Errorf("expected to get LabelValue %s, but got %s for LabelName %s", expected, lv, ln) } } } func TestLabelSetMerge(t *testing.T) { labelSet := LabelSet{ "monitor": "codelab", "foo": "bar", "bar": "baz", } labelSet2 := LabelSet{ "monitor": "codelab", "dolor": "mi", "lorem": "ipsum", } expectedSet := LabelSet{ "monitor": "codelab", "foo": "bar", "bar": "baz", "dolor": "mi", "lorem": "ipsum", } mergedSet := labelSet.Merge(labelSet2) if len(mergedSet) != len(expectedSet) { t.Errorf("expected the length of the cloned Label set to be %d, but got %d", len(expectedSet), len(mergedSet)) } for ln, lv := range mergedSet { expected := expectedSet[ln] if expected != lv { t.Errorf("expected to get LabelValue %s, but got %s for LabelName %s", expected, lv, ln) } } } // Benchmark Results for LabelSet's String() method // --------------------------------------------------------------------------------------------------------- // goos: linux // goarch: amd64 // pkg: github.com/prometheus/common/model // cpu: 11th Gen Intel(R) Core(TM) i5-1145G7 @ 2.60GHz // BenchmarkLabelSetStringMethod-8 732376 1532 ns/op func BenchmarkLabelSetStringMethod(b *testing.B) { ls := make(LabelSet) ls["monitor"] = "codelab" ls["foo2"] = "bar" ls["foo"] = "bar" ls["abc"] = "prometheus" ls["foo11"] = "bar11" for i := 0; i < b.N; i++ { _ = ls.String() } } golang-github-prometheus-common-0.55.0/model/labelset_string.go000066400000000000000000000025301463701437000246240ustar00rootroot00000000000000// Copyright 2024 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build go1.21 package model import ( "bytes" "slices" "strconv" ) // String will look like `{foo="bar", more="less"}`. Names are sorted alphabetically. func (l LabelSet) String() string { var lna [32]string // On stack to avoid memory allocation for sorting names. labelNames := lna[:0] for name := range l { labelNames = append(labelNames, string(name)) } slices.Sort(labelNames) var bytea [1024]byte // On stack to avoid memory allocation while building the output. b := bytes.NewBuffer(bytea[:0]) b.WriteByte('{') for i, name := range labelNames { if i > 0 { b.WriteString(", ") } b.WriteString(name) b.WriteByte('=') b.Write(strconv.AppendQuote(b.AvailableBuffer(), string(l[LabelName(name)]))) } b.WriteByte('}') return b.String() } golang-github-prometheus-common-0.55.0/model/labelset_string_go120.go000066400000000000000000000024241463701437000255360ustar00rootroot00000000000000// Copyright 2024 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build !go1.21 package model import ( "fmt" "sort" "strings" ) // String was optimized using functions not available for go 1.20 // or lower. We keep the old implementation for compatibility with client_golang. // Once client golang drops support for go 1.20 (scheduled for August 2024), this // file can be removed. func (l LabelSet) String() string { labelNames := make([]string, 0, len(l)) for name := range l { labelNames = append(labelNames, string(name)) } sort.Strings(labelNames) lstrs := make([]string, 0, len(l)) for _, name := range labelNames { lstrs = append(lstrs, fmt.Sprintf("%s=%q", name, l[LabelName(name)])) } return fmt.Sprintf("{%s}", strings.Join(lstrs, ", ")) } golang-github-prometheus-common-0.55.0/model/labelset_test.go000066400000000000000000000102011463701437000242670ustar00rootroot00000000000000// Copyright 2019 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build go1.21 package model import ( "encoding/json" "testing" ) func TestUnmarshalJSONLabelSet(t *testing.T) { type testConfig struct { LabelSet LabelSet `yaml:"labelSet,omitempty"` } // valid LabelSet JSON labelSetJSON := `{ "labelSet": { "monitor": "codelab", "foo": "bar", "foo2": "bar", "abc": "prometheus", "foo11": "bar11" } }` var c testConfig err := json.Unmarshal([]byte(labelSetJSON), &c) if err != nil { t.Errorf("unexpected error while marshalling JSON : %s", err.Error()) } labelSetString := c.LabelSet.String() expected := `{abc="prometheus", foo="bar", foo11="bar11", foo2="bar", monitor="codelab"}` if expected != labelSetString { t.Errorf("expected %s but got %s", expected, labelSetString) } // invalid LabelSet JSON invalidlabelSetJSON := `{ "labelSet": { "1nvalid_23name": "codelab", "foo": "bar" } }` NameValidationScheme = LegacyValidation err = json.Unmarshal([]byte(invalidlabelSetJSON), &c) expectedErr := `"1nvalid_23name" is not a valid label name` if err == nil || err.Error() != expectedErr { t.Errorf("expected an error with message '%s' to be thrown", expectedErr) } } func TestLabelSetClone(t *testing.T) { labelSet := LabelSet{ "monitor": "codelab", "foo": "bar", "bar": "baz", } cloneSet := labelSet.Clone() if len(labelSet) != len(cloneSet) { t.Errorf("expected the length of the cloned Label set to be %d, but got %d", len(labelSet), len(cloneSet)) } for ln, lv := range labelSet { expected := cloneSet[ln] if expected != lv { t.Errorf("expected to get LabelValue %s, but got %s for LabelName %s", expected, lv, ln) } } } func TestLabelSetMerge(t *testing.T) { labelSet := LabelSet{ "monitor": "codelab", "foo": "bar", "bar": "baz", } labelSet2 := LabelSet{ "monitor": "codelab", "dolor": "mi", "lorem": "ipsum", } expectedSet := LabelSet{ "monitor": "codelab", "foo": "bar", "bar": "baz", "dolor": "mi", "lorem": "ipsum", } mergedSet := labelSet.Merge(labelSet2) if len(mergedSet) != len(expectedSet) { t.Errorf("expected the length of the cloned Label set to be %d, but got %d", len(expectedSet), len(mergedSet)) } for ln, lv := range mergedSet { expected := expectedSet[ln] if expected != lv { t.Errorf("expected to get LabelValue %s, but got %s for LabelName %s", expected, lv, ln) } } } func TestLabelSet_String(t *testing.T) { tests := []struct { input LabelSet want string }{ { input: nil, want: `{}`, }, { input: LabelSet{ "foo": "bar", }, want: `{foo="bar"}`, }, { input: LabelSet{ "foo": "bar", "foo2": "bar", "abc": "prometheus", "foo11": "bar11", }, want: `{abc="prometheus", foo="bar", foo11="bar11", foo2="bar"}`, }, } for _, tt := range tests { t.Run("test", func(t *testing.T) { if got := tt.input.String(); got != tt.want { t.Errorf("LabelSet.String() = %v, want %v", got, tt.want) } }) } } // Benchmark Results for LabelSet's String() method // --------------------------------------------------------------------------------------------------------- // goos: linux // goarch: amd64 // pkg: github.com/prometheus/common/model // cpu: 11th Gen Intel(R) Core(TM) i5-1145G7 @ 2.60GHz // BenchmarkLabelSetStringMethod-8 732376 1532 ns/op func BenchmarkLabelSetStringMethod(b *testing.B) { ls := make(LabelSet) ls["monitor"] = "codelab" ls["foo2"] = "bar" ls["foo"] = "bar" ls["abc"] = "prometheus" ls["foo11"] = "bar11" for i := 0; i < b.N; i++ { _ = ls.String() } } golang-github-prometheus-common-0.55.0/model/metadata.go000066400000000000000000000021041463701437000232200ustar00rootroot00000000000000// Copyright 2023 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package model // MetricType represents metric type values. type MetricType string const ( MetricTypeCounter = MetricType("counter") MetricTypeGauge = MetricType("gauge") MetricTypeHistogram = MetricType("histogram") MetricTypeGaugeHistogram = MetricType("gaugehistogram") MetricTypeSummary = MetricType("summary") MetricTypeInfo = MetricType("info") MetricTypeStateset = MetricType("stateset") MetricTypeUnknown = MetricType("unknown") ) golang-github-prometheus-common-0.55.0/model/metric.go000066400000000000000000000313721463701437000227340ustar00rootroot00000000000000// Copyright 2013 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package model import ( "fmt" "regexp" "sort" "strings" "unicode/utf8" dto "github.com/prometheus/client_model/go" "google.golang.org/protobuf/proto" ) var ( // NameValidationScheme determines the method of name validation to be used by // all calls to IsValidMetricName() and LabelName IsValid(). Setting UTF-8 mode // in isolation from other components that don't support UTF-8 may result in // bugs or other undefined behavior. This value is intended to be set by // UTF-8-aware binaries as part of their startup. To avoid need for locking, // this value should be set once, ideally in an init(), before multiple // goroutines are started. NameValidationScheme = LegacyValidation // NameEscapingScheme defines the default way that names will be // escaped when presented to systems that do not support UTF-8 names. If the // Content-Type "escaping" term is specified, that will override this value. NameEscapingScheme = ValueEncodingEscaping ) // ValidationScheme is a Go enum for determining how metric and label names will // be validated by this library. type ValidationScheme int const ( // LegacyValidation is a setting that requirets that metric and label names // conform to the original Prometheus character requirements described by // MetricNameRE and LabelNameRE. LegacyValidation ValidationScheme = iota // UTF8Validation only requires that metric and label names be valid UTF-8 // strings. UTF8Validation ) type EscapingScheme int const ( // NoEscaping indicates that a name will not be escaped. Unescaped names that // do not conform to the legacy validity check will use a new exposition // format syntax that will be officially standardized in future versions. NoEscaping EscapingScheme = iota // UnderscoreEscaping replaces all legacy-invalid characters with underscores. UnderscoreEscaping // DotsEscaping is similar to UnderscoreEscaping, except that dots are // converted to `_dot_` and pre-existing underscores are converted to `__`. DotsEscaping // ValueEncodingEscaping prepends the name with `U__` and replaces all invalid // characters with the unicode value, surrounded by underscores. Single // underscores are replaced with double underscores. ValueEncodingEscaping ) const ( // EscapingKey is the key in an Accept or Content-Type header that defines how // metric and label names that do not conform to the legacy character // requirements should be escaped when being scraped by a legacy prometheus // system. If a system does not explicitly pass an escaping parameter in the // Accept header, the default NameEscapingScheme will be used. EscapingKey = "escaping" // Possible values for Escaping Key: AllowUTF8 = "allow-utf-8" // No escaping required. EscapeUnderscores = "underscores" EscapeDots = "dots" EscapeValues = "values" ) // MetricNameRE is a regular expression matching valid metric // names. Note that the IsValidMetricName function performs the same // check but faster than a match with this regular expression. var MetricNameRE = regexp.MustCompile(`^[a-zA-Z_:][a-zA-Z0-9_:]*$`) // A Metric is similar to a LabelSet, but the key difference is that a Metric is // a singleton and refers to one and only one stream of samples. type Metric LabelSet // Equal compares the metrics. func (m Metric) Equal(o Metric) bool { return LabelSet(m).Equal(LabelSet(o)) } // Before compares the metrics' underlying label sets. func (m Metric) Before(o Metric) bool { return LabelSet(m).Before(LabelSet(o)) } // Clone returns a copy of the Metric. func (m Metric) Clone() Metric { clone := make(Metric, len(m)) for k, v := range m { clone[k] = v } return clone } func (m Metric) String() string { metricName, hasName := m[MetricNameLabel] numLabels := len(m) - 1 if !hasName { numLabels = len(m) } labelStrings := make([]string, 0, numLabels) for label, value := range m { if label != MetricNameLabel { labelStrings = append(labelStrings, fmt.Sprintf("%s=%q", label, value)) } } switch numLabels { case 0: if hasName { return string(metricName) } return "{}" default: sort.Strings(labelStrings) return fmt.Sprintf("%s{%s}", metricName, strings.Join(labelStrings, ", ")) } } // Fingerprint returns a Metric's Fingerprint. func (m Metric) Fingerprint() Fingerprint { return LabelSet(m).Fingerprint() } // FastFingerprint returns a Metric's Fingerprint calculated by a faster hashing // algorithm, which is, however, more susceptible to hash collisions. func (m Metric) FastFingerprint() Fingerprint { return LabelSet(m).FastFingerprint() } // IsValidMetricName returns true iff name matches the pattern of MetricNameRE // for legacy names, and iff it's valid UTF-8 if the UTF8Validation scheme is // selected. func IsValidMetricName(n LabelValue) bool { switch NameValidationScheme { case LegacyValidation: return IsValidLegacyMetricName(n) case UTF8Validation: if len(n) == 0 { return false } return utf8.ValidString(string(n)) default: panic(fmt.Sprintf("Invalid name validation scheme requested: %d", NameValidationScheme)) } } // IsValidLegacyMetricName is similar to IsValidMetricName but always uses the // legacy validation scheme regardless of the value of NameValidationScheme. // This function, however, does not use MetricNameRE for the check but a much // faster hardcoded implementation. func IsValidLegacyMetricName(n LabelValue) bool { if len(n) == 0 { return false } for i, b := range n { if !isValidLegacyRune(b, i) { return false } } return true } // EscapeMetricFamily escapes the given metric names and labels with the given // escaping scheme. Returns a new object that uses the same pointers to fields // when possible and creates new escaped versions so as not to mutate the // input. func EscapeMetricFamily(v *dto.MetricFamily, scheme EscapingScheme) *dto.MetricFamily { if v == nil { return nil } if scheme == NoEscaping { return v } out := &dto.MetricFamily{ Help: v.Help, Type: v.Type, Unit: v.Unit, } // If the name is nil, copy as-is, don't try to escape. if v.Name == nil || IsValidLegacyMetricName(LabelValue(v.GetName())) { out.Name = v.Name } else { out.Name = proto.String(EscapeName(v.GetName(), scheme)) } for _, m := range v.Metric { if !metricNeedsEscaping(m) { out.Metric = append(out.Metric, m) continue } escaped := &dto.Metric{ Gauge: m.Gauge, Counter: m.Counter, Summary: m.Summary, Untyped: m.Untyped, Histogram: m.Histogram, TimestampMs: m.TimestampMs, } for _, l := range m.Label { if l.GetName() == MetricNameLabel { if l.Value == nil || IsValidLegacyMetricName(LabelValue(l.GetValue())) { escaped.Label = append(escaped.Label, l) continue } escaped.Label = append(escaped.Label, &dto.LabelPair{ Name: proto.String(MetricNameLabel), Value: proto.String(EscapeName(l.GetValue(), scheme)), }) continue } if l.Name == nil || IsValidLegacyMetricName(LabelValue(l.GetName())) { escaped.Label = append(escaped.Label, l) continue } escaped.Label = append(escaped.Label, &dto.LabelPair{ Name: proto.String(EscapeName(l.GetName(), scheme)), Value: l.Value, }) } out.Metric = append(out.Metric, escaped) } return out } func metricNeedsEscaping(m *dto.Metric) bool { for _, l := range m.Label { if l.GetName() == MetricNameLabel && !IsValidLegacyMetricName(LabelValue(l.GetValue())) { return true } if !IsValidLegacyMetricName(LabelValue(l.GetName())) { return true } } return false } const ( lowerhex = "0123456789abcdef" ) // EscapeName escapes the incoming name according to the provided escaping // scheme. Depending on the rules of escaping, this may cause no change in the // string that is returned. (Especially NoEscaping, which by definition is a // noop). This function does not do any validation of the name. func EscapeName(name string, scheme EscapingScheme) string { if len(name) == 0 { return name } var escaped strings.Builder switch scheme { case NoEscaping: return name case UnderscoreEscaping: if IsValidLegacyMetricName(LabelValue(name)) { return name } for i, b := range name { if isValidLegacyRune(b, i) { escaped.WriteRune(b) } else { escaped.WriteRune('_') } } return escaped.String() case DotsEscaping: // Do not early return for legacy valid names, we still escape underscores. for i, b := range name { if b == '_' { escaped.WriteString("__") } else if b == '.' { escaped.WriteString("_dot_") } else if isValidLegacyRune(b, i) { escaped.WriteRune(b) } else { escaped.WriteRune('_') } } return escaped.String() case ValueEncodingEscaping: if IsValidLegacyMetricName(LabelValue(name)) { return name } escaped.WriteString("U__") for i, b := range name { if isValidLegacyRune(b, i) { escaped.WriteRune(b) } else if !utf8.ValidRune(b) { escaped.WriteString("_FFFD_") } else if b < 0x100 { escaped.WriteRune('_') for s := 4; s >= 0; s -= 4 { escaped.WriteByte(lowerhex[b>>uint(s)&0xF]) } escaped.WriteRune('_') } else if b < 0x10000 { escaped.WriteRune('_') for s := 12; s >= 0; s -= 4 { escaped.WriteByte(lowerhex[b>>uint(s)&0xF]) } escaped.WriteRune('_') } } return escaped.String() default: panic(fmt.Sprintf("invalid escaping scheme %d", scheme)) } } // lower function taken from strconv.atoi func lower(c byte) byte { return c | ('x' - 'X') } // UnescapeName unescapes the incoming name according to the provided escaping // scheme if possible. Some schemes are partially or totally non-roundtripable. // If any error is enountered, returns the original input. func UnescapeName(name string, scheme EscapingScheme) string { if len(name) == 0 { return name } switch scheme { case NoEscaping: return name case UnderscoreEscaping: // It is not possible to unescape from underscore replacement. return name case DotsEscaping: name = strings.ReplaceAll(name, "_dot_", ".") name = strings.ReplaceAll(name, "__", "_") return name case ValueEncodingEscaping: escapedName, found := strings.CutPrefix(name, "U__") if !found { return name } var unescaped strings.Builder TOP: for i := 0; i < len(escapedName); i++ { // All non-underscores are treated normally. if escapedName[i] != '_' { unescaped.WriteByte(escapedName[i]) continue } i++ if i >= len(escapedName) { return name } // A double underscore is a single underscore. if escapedName[i] == '_' { unescaped.WriteByte('_') continue } // We think we are in a UTF-8 code, process it. var utf8Val uint for j := 0; i < len(escapedName); j++ { // This is too many characters for a utf8 value. if j > 4 { return name } // Found a closing underscore, convert to a rune, check validity, and append. if escapedName[i] == '_' { utf8Rune := rune(utf8Val) if !utf8.ValidRune(utf8Rune) { return name } unescaped.WriteRune(utf8Rune) continue TOP } r := lower(escapedName[i]) utf8Val *= 16 if r >= '0' && r <= '9' { utf8Val += uint(r) - '0' } else if r >= 'a' && r <= 'f' { utf8Val += uint(r) - 'a' + 10 } else { return name } i++ } // Didn't find closing underscore, invalid. return name } return unescaped.String() default: panic(fmt.Sprintf("invalid escaping scheme %d", scheme)) } } func isValidLegacyRune(b rune, i int) bool { return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || b == '_' || b == ':' || (b >= '0' && b <= '9' && i > 0) } func (e EscapingScheme) String() string { switch e { case NoEscaping: return AllowUTF8 case UnderscoreEscaping: return EscapeUnderscores case DotsEscaping: return EscapeDots case ValueEncodingEscaping: return EscapeValues default: panic(fmt.Sprintf("unknown format scheme %d", e)) } } func ToEscapingScheme(s string) (EscapingScheme, error) { if s == "" { return NoEscaping, fmt.Errorf("got empty string instead of escaping scheme") } switch s { case AllowUTF8: return NoEscaping, nil case EscapeUnderscores: return UnderscoreEscaping, nil case EscapeDots: return DotsEscaping, nil case EscapeValues: return ValueEncodingEscaping, nil default: return NoEscaping, fmt.Errorf("unknown format scheme " + s) } } golang-github-prometheus-common-0.55.0/model/metric_test.go000066400000000000000000000410111463701437000237620ustar00rootroot00000000000000// Copyright 2013 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package model import ( "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" dto "github.com/prometheus/client_model/go" "google.golang.org/protobuf/proto" ) func testMetric(t testing.TB) { scenarios := []struct { input LabelSet fingerprint Fingerprint fastFingerprint Fingerprint }{ { input: LabelSet{}, fingerprint: 14695981039346656037, fastFingerprint: 14695981039346656037, }, { input: LabelSet{ "first_name": "electro", "occupation": "robot", "manufacturer": "westinghouse", }, fingerprint: 5911716720268894962, fastFingerprint: 11310079640881077873, }, { input: LabelSet{ "x": "y", }, fingerprint: 8241431561484471700, fastFingerprint: 13948396922932177635, }, { input: LabelSet{ "a": "bb", "b": "c", }, fingerprint: 3016285359649981711, fastFingerprint: 3198632812309449502, }, { input: LabelSet{ "a": "b", "bb": "c", }, fingerprint: 7122421792099404749, fastFingerprint: 5774953389407657638, }, } for i, scenario := range scenarios { input := Metric(scenario.input) if scenario.fingerprint != input.Fingerprint() { t.Errorf("%d. expected %d, got %d", i, scenario.fingerprint, input.Fingerprint()) } if scenario.fastFingerprint != input.FastFingerprint() { t.Errorf("%d. expected %d, got %d", i, scenario.fastFingerprint, input.FastFingerprint()) } } } func TestMetric(t *testing.T) { testMetric(t) } func BenchmarkMetric(b *testing.B) { for i := 0; i < b.N; i++ { testMetric(b) } } func TestMetricNameIsLegacyValid(t *testing.T) { scenarios := []struct { mn LabelValue legacyValid bool utf8Valid bool }{ { mn: "Avalid_23name", legacyValid: true, utf8Valid: true, }, { mn: "_Avalid_23name", legacyValid: true, utf8Valid: true, }, { mn: "1valid_23name", legacyValid: false, utf8Valid: true, }, { mn: "avalid_23name", legacyValid: true, utf8Valid: true, }, { mn: "Ava:lid_23name", legacyValid: true, utf8Valid: true, }, { mn: "a lid_23name", legacyValid: false, utf8Valid: true, }, { mn: ":leading_colon", legacyValid: true, utf8Valid: true, }, { mn: "colon:in:the:middle", legacyValid: true, utf8Valid: true, }, { mn: "", legacyValid: false, utf8Valid: false, }, { mn: "a\xc5z", legacyValid: false, utf8Valid: false, }, } for _, s := range scenarios { NameValidationScheme = LegacyValidation if IsValidMetricName(s.mn) != s.legacyValid { t.Errorf("Expected %v for %q using legacy IsValidMetricName method", s.legacyValid, s.mn) } if MetricNameRE.MatchString(string(s.mn)) != s.legacyValid { t.Errorf("Expected %v for %q using regexp matching", s.legacyValid, s.mn) } NameValidationScheme = UTF8Validation if IsValidMetricName(s.mn) != s.utf8Valid { t.Errorf("Expected %v for %q using utf-8 IsValidMetricName method", s.legacyValid, s.mn) } } } func TestMetricClone(t *testing.T) { m := Metric{ "first_name": "electro", "occupation": "robot", "manufacturer": "westinghouse", } m2 := m.Clone() if len(m) != len(m2) { t.Errorf("expected the length of the cloned metric to be equal to the input metric") } for ln, lv := range m2 { expected := m[ln] if expected != lv { t.Errorf("expected label value %s but got %s for label name %s", expected, lv, ln) } } } func TestMetricToString(t *testing.T) { scenarios := []struct { name string input Metric expected string }{ { name: "valid metric without __name__ label", input: Metric{ "first_name": "electro", "occupation": "robot", "manufacturer": "westinghouse", }, expected: `{first_name="electro", manufacturer="westinghouse", occupation="robot"}`, }, { name: "valid metric with __name__ label", input: Metric{ "__name__": "electro", "occupation": "robot", "manufacturer": "westinghouse", }, expected: `electro{manufacturer="westinghouse", occupation="robot"}`, }, { name: "empty metric with __name__ label", input: Metric{ "__name__": "fooname", }, expected: "fooname", }, { name: "empty metric", input: Metric{}, expected: "{}", }, } for _, scenario := range scenarios { t.Run(scenario.name, func(t *testing.T) { actual := scenario.input.String() if actual != scenario.expected { t.Errorf("expected string output %s but got %s", scenario.expected, actual) } }) } } func TestEscapeName(t *testing.T) { scenarios := []struct { name string input string expectedUnderscores string expectedDots string expectedUnescapedDots string expectedValue string }{ { name: "empty string", }, { name: "legacy valid name", input: "no:escaping_required", expectedUnderscores: "no:escaping_required", // Dots escaping will escape underscores even though it's not strictly // necessary for compatibility. expectedDots: "no:escaping__required", expectedUnescapedDots: "no:escaping_required", expectedValue: "no:escaping_required", }, { name: "name with dots", input: "mysystem.prod.west.cpu.load", expectedUnderscores: "mysystem_prod_west_cpu_load", expectedDots: "mysystem_dot_prod_dot_west_dot_cpu_dot_load", expectedUnescapedDots: "mysystem.prod.west.cpu.load", expectedValue: "U__mysystem_2e_prod_2e_west_2e_cpu_2e_load", }, { name: "name with dots and colon", input: "http.status:sum", expectedUnderscores: "http_status:sum", expectedDots: "http_dot_status:sum", expectedUnescapedDots: "http.status:sum", expectedValue: "U__http_2e_status:sum", }, { name: "name with unicode characters > 0x100", input: "花火", expectedUnderscores: "__", expectedDots: "__", // Dots-replacement does not know the difference between two replaced // characters and a single underscore. expectedUnescapedDots: "_", expectedValue: "U___82b1__706b_", }, } for _, scenario := range scenarios { t.Run(scenario.name, func(t *testing.T) { got := EscapeName(scenario.input, UnderscoreEscaping) if got != scenario.expectedUnderscores { t.Errorf("expected string output %s but got %s", scenario.expectedUnderscores, got) } // Unescaping with the underscore method is a noop. got = UnescapeName(got, UnderscoreEscaping) if got != scenario.expectedUnderscores { t.Errorf("expected unescaped string output %s but got %s", scenario.expectedUnderscores, got) } got = EscapeName(scenario.input, DotsEscaping) if got != scenario.expectedDots { t.Errorf("expected string output %s but got %s", scenario.expectedDots, got) } got = UnescapeName(got, DotsEscaping) if got != scenario.expectedUnescapedDots { t.Errorf("expected unescaped string output %s but got %s", scenario.expectedUnescapedDots, got) } got = EscapeName(scenario.input, ValueEncodingEscaping) if got != scenario.expectedValue { t.Errorf("expected string output %s but got %s", scenario.expectedValue, got) } // Unescaped result should always be identical to the original input. got = UnescapeName(got, ValueEncodingEscaping) if got != scenario.input { t.Errorf("expected unescaped string output %s but got %s", scenario.input, got) } }) } } func TestValueUnescapeErrors(t *testing.T) { scenarios := []struct { name string input string expected string }{ { name: "empty string", }, { name: "basic case, no error", input: "U__no:unescapingrequired", expected: "no:unescapingrequired", }, { name: "capitals ok, no error", input: "U__capitals_2E_ok", expected: "capitals.ok", }, { name: "underscores, no error", input: "U__underscores__doubled__", expected: "underscores_doubled_", }, { name: "invalid single underscore", input: "U__underscores_doubled_", expected: "U__underscores_doubled_", }, { name: "invalid single underscore, 2", input: "U__underscores__doubled_", expected: "U__underscores__doubled_", }, { name: "giant fake utf-8 code", input: "U__my__hack_2e_attempt_872348732fabdabbab_", expected: "U__my__hack_2e_attempt_872348732fabdabbab_", }, { name: "trailing utf-8", input: "U__my__hack_2e", expected: "U__my__hack_2e", }, { name: "invalid utf-8 value", input: "U__bad__utf_2eg_", expected: "U__bad__utf_2eg_", }, { name: "surrogate utf-8 value", input: "U__bad__utf_D900_", expected: "U__bad__utf_D900_", }, } for _, scenario := range scenarios { t.Run(scenario.name, func(t *testing.T) { got := UnescapeName(scenario.input, ValueEncodingEscaping) if got != scenario.expected { t.Errorf("expected unescaped string output %s but got %s", scenario.expected, got) } }) } } func TestEscapeMetricFamily(t *testing.T) { scenarios := []struct { name string input *dto.MetricFamily scheme EscapingScheme expected *dto.MetricFamily }{ { name: "empty", input: &dto.MetricFamily{}, scheme: ValueEncodingEscaping, expected: &dto.MetricFamily{}, }, { name: "simple, no escaping needed", scheme: ValueEncodingEscaping, input: &dto.MetricFamily{ Name: proto.String("my_metric"), Help: proto.String("some help text"), Type: dto.MetricType_COUNTER.Enum(), Metric: []*dto.Metric{ { Counter: &dto.Counter{ Value: proto.Float64(34.2), }, Label: []*dto.LabelPair{ { Name: proto.String("__name__"), Value: proto.String("my_metric"), }, { Name: proto.String("some_label"), Value: proto.String("labelvalue"), }, }, }, }, }, expected: &dto.MetricFamily{ Name: proto.String("my_metric"), Help: proto.String("some help text"), Type: dto.MetricType_COUNTER.Enum(), Metric: []*dto.Metric{ { Counter: &dto.Counter{ Value: proto.Float64(34.2), }, Label: []*dto.LabelPair{ { Name: proto.String("__name__"), Value: proto.String("my_metric"), }, { Name: proto.String("some_label"), Value: proto.String("labelvalue"), }, }, }, }, }, }, { name: "label name escaping needed", scheme: ValueEncodingEscaping, input: &dto.MetricFamily{ Name: proto.String("my_metric"), Help: proto.String("some help text"), Type: dto.MetricType_COUNTER.Enum(), Metric: []*dto.Metric{ { Counter: &dto.Counter{ Value: proto.Float64(34.2), }, Label: []*dto.LabelPair{ { Name: proto.String("__name__"), Value: proto.String("my_metric"), }, { Name: proto.String("some.label"), Value: proto.String("labelvalue"), }, }, }, }, }, expected: &dto.MetricFamily{ Name: proto.String("my_metric"), Help: proto.String("some help text"), Type: dto.MetricType_COUNTER.Enum(), Metric: []*dto.Metric{ { Counter: &dto.Counter{ Value: proto.Float64(34.2), }, Label: []*dto.LabelPair{ { Name: proto.String("__name__"), Value: proto.String("my_metric"), }, { Name: proto.String("U__some_2e_label"), Value: proto.String("labelvalue"), }, }, }, }, }, }, { name: "counter, escaping needed", scheme: ValueEncodingEscaping, input: &dto.MetricFamily{ Name: proto.String("my.metric"), Help: proto.String("some help text"), Type: dto.MetricType_COUNTER.Enum(), Metric: []*dto.Metric{ { Counter: &dto.Counter{ Value: proto.Float64(34.2), }, Label: []*dto.LabelPair{ { Name: proto.String("__name__"), Value: proto.String("my.metric"), }, { Name: proto.String("some?label"), Value: proto.String("label??value"), }, }, }, }, }, expected: &dto.MetricFamily{ Name: proto.String("U__my_2e_metric"), Help: proto.String("some help text"), Type: dto.MetricType_COUNTER.Enum(), Metric: []*dto.Metric{ { Counter: &dto.Counter{ Value: proto.Float64(34.2), }, Label: []*dto.LabelPair{ { Name: proto.String("__name__"), Value: proto.String("U__my_2e_metric"), }, { Name: proto.String("U__some_3f_label"), Value: proto.String("label??value"), }, }, }, }, }, }, { name: "gauge, escaping needed", scheme: DotsEscaping, input: &dto.MetricFamily{ Name: proto.String("unicode.and.dots.花火"), Help: proto.String("some help text"), Type: dto.MetricType_GAUGE.Enum(), Metric: []*dto.Metric{ { Gauge: &dto.Gauge{ Value: proto.Float64(34.2), }, Label: []*dto.LabelPair{ { Name: proto.String("__name__"), Value: proto.String("unicode.and.dots.花火"), }, { Name: proto.String("some_label"), Value: proto.String("label??value"), }, }, }, }, }, expected: &dto.MetricFamily{ Name: proto.String("unicode_dot_and_dot_dots_dot___"), Help: proto.String("some help text"), Type: dto.MetricType_GAUGE.Enum(), Metric: []*dto.Metric{ { Gauge: &dto.Gauge{ Value: proto.Float64(34.2), }, Label: []*dto.LabelPair{ { Name: proto.String("__name__"), Value: proto.String("unicode_dot_and_dot_dots_dot___"), }, { Name: proto.String("some_label"), Value: proto.String("label??value"), }, }, }, }, }, }, } unexportList := []interface{}{dto.MetricFamily{}, dto.Metric{}, dto.LabelPair{}, dto.Counter{}, dto.Gauge{}} for _, scenario := range scenarios { t.Run(scenario.name, func(t *testing.T) { original := proto.Clone(scenario.input) got := EscapeMetricFamily(scenario.input, scenario.scheme) if !cmp.Equal(scenario.expected, got, cmpopts.IgnoreUnexported(unexportList...)) { t.Errorf("unexpected difference in escaped output:" + cmp.Diff(scenario.expected, got, cmpopts.IgnoreUnexported(unexportList...))) } if !cmp.Equal(scenario.input, original, cmpopts.IgnoreUnexported(unexportList...)) { t.Errorf("input was mutated during escaping" + cmp.Diff(scenario.expected, got, cmpopts.IgnoreUnexported(unexportList...))) } }) } } // TestProtoFormatUnchanged checks to see if the proto format changed, in which // case EscapeMetricFamily will need to be updated. func TestProtoFormatUnchanged(t *testing.T) { scenarios := []struct { name string input proto.Message expectFields []string }{ { name: "MetricFamily", input: &dto.MetricFamily{}, expectFields: []string{"name", "help", "type", "metric", "unit"}, }, { name: "Metric", input: &dto.Metric{}, expectFields: []string{"label", "gauge", "counter", "summary", "untyped", "histogram", "timestamp_ms"}, }, { name: "LabelPair", input: &dto.LabelPair{}, expectFields: []string{"name", "value"}, }, } for _, scenario := range scenarios { t.Run(scenario.name, func(t *testing.T) { desc := scenario.input.ProtoReflect().Descriptor() fields := desc.Fields() if fields.Len() != len(scenario.expectFields) { t.Errorf("dto.MetricFamily changed length, expected %d, got %d", len(scenario.expectFields), fields.Len()) } for i := 0; i < fields.Len(); i++ { got := fields.Get(i).TextName() if got != scenario.expectFields[i] { t.Errorf("dto.MetricFamily field mismatch, expected %s got %s", scenario.expectFields[i], got) } } }) } } golang-github-prometheus-common-0.55.0/model/model.go000066400000000000000000000013171463701437000225450ustar00rootroot00000000000000// Copyright 2013 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package model contains common data structures that are shared across // Prometheus components and libraries. package model golang-github-prometheus-common-0.55.0/model/signature.go000066400000000000000000000105201463701437000234420ustar00rootroot00000000000000// Copyright 2014 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package model import ( "sort" ) // SeparatorByte is a byte that cannot occur in valid UTF-8 sequences and is // used to separate label names, label values, and other strings from each other // when calculating their combined hash value (aka signature aka fingerprint). const SeparatorByte byte = 255 // cache the signature of an empty label set. var emptyLabelSignature = hashNew() // LabelsToSignature returns a quasi-unique signature (i.e., fingerprint) for a // given label set. (Collisions are possible but unlikely if the number of label // sets the function is applied to is small.) func LabelsToSignature(labels map[string]string) uint64 { if len(labels) == 0 { return emptyLabelSignature } labelNames := make([]string, 0, len(labels)) for labelName := range labels { labelNames = append(labelNames, labelName) } sort.Strings(labelNames) sum := hashNew() for _, labelName := range labelNames { sum = hashAdd(sum, labelName) sum = hashAddByte(sum, SeparatorByte) sum = hashAdd(sum, labels[labelName]) sum = hashAddByte(sum, SeparatorByte) } return sum } // labelSetToFingerprint works exactly as LabelsToSignature but takes a LabelSet as // parameter (rather than a label map) and returns a Fingerprint. func labelSetToFingerprint(ls LabelSet) Fingerprint { if len(ls) == 0 { return Fingerprint(emptyLabelSignature) } labelNames := make(LabelNames, 0, len(ls)) for labelName := range ls { labelNames = append(labelNames, labelName) } sort.Sort(labelNames) sum := hashNew() for _, labelName := range labelNames { sum = hashAdd(sum, string(labelName)) sum = hashAddByte(sum, SeparatorByte) sum = hashAdd(sum, string(ls[labelName])) sum = hashAddByte(sum, SeparatorByte) } return Fingerprint(sum) } // labelSetToFastFingerprint works similar to labelSetToFingerprint but uses a // faster and less allocation-heavy hash function, which is more susceptible to // create hash collisions. Therefore, collision detection should be applied. func labelSetToFastFingerprint(ls LabelSet) Fingerprint { if len(ls) == 0 { return Fingerprint(emptyLabelSignature) } var result uint64 for labelName, labelValue := range ls { sum := hashNew() sum = hashAdd(sum, string(labelName)) sum = hashAddByte(sum, SeparatorByte) sum = hashAdd(sum, string(labelValue)) result ^= sum } return Fingerprint(result) } // SignatureForLabels works like LabelsToSignature but takes a Metric as // parameter (rather than a label map) and only includes the labels with the // specified LabelNames into the signature calculation. The labels passed in // will be sorted by this function. func SignatureForLabels(m Metric, labels ...LabelName) uint64 { if len(labels) == 0 { return emptyLabelSignature } sort.Sort(LabelNames(labels)) sum := hashNew() for _, label := range labels { sum = hashAdd(sum, string(label)) sum = hashAddByte(sum, SeparatorByte) sum = hashAdd(sum, string(m[label])) sum = hashAddByte(sum, SeparatorByte) } return sum } // SignatureWithoutLabels works like LabelsToSignature but takes a Metric as // parameter (rather than a label map) and excludes the labels with any of the // specified LabelNames from the signature calculation. func SignatureWithoutLabels(m Metric, labels map[LabelName]struct{}) uint64 { if len(m) == 0 { return emptyLabelSignature } labelNames := make(LabelNames, 0, len(m)) for labelName := range m { if _, exclude := labels[labelName]; !exclude { labelNames = append(labelNames, labelName) } } if len(labelNames) == 0 { return emptyLabelSignature } sort.Sort(labelNames) sum := hashNew() for _, labelName := range labelNames { sum = hashAdd(sum, string(labelName)) sum = hashAddByte(sum, SeparatorByte) sum = hashAdd(sum, string(m[labelName])) sum = hashAddByte(sum, SeparatorByte) } return sum } golang-github-prometheus-common-0.55.0/model/signature_test.go000066400000000000000000000221121463701437000245010ustar00rootroot00000000000000// Copyright 2014 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package model import ( "fmt" "runtime" "sync" "testing" ) func TestLabelsToSignature(t *testing.T) { scenarios := []struct { in map[string]string out uint64 }{ { in: map[string]string{}, out: 14695981039346656037, }, { in: map[string]string{"name": "garland, briggs", "fear": "love is not enough"}, out: 5799056148416392346, }, } for i, scenario := range scenarios { actual := LabelsToSignature(scenario.in) if actual != scenario.out { t.Errorf("%d. expected %d, got %d", i, scenario.out, actual) } } } func TestMetricToFingerprint(t *testing.T) { scenarios := []struct { in LabelSet out Fingerprint }{ { in: LabelSet{}, out: 14695981039346656037, }, { in: LabelSet{"name": "garland, briggs", "fear": "love is not enough"}, out: 5799056148416392346, }, } for i, scenario := range scenarios { actual := labelSetToFingerprint(scenario.in) if actual != scenario.out { t.Errorf("%d. expected %d, got %d", i, scenario.out, actual) } } } func TestMetricToFastFingerprint(t *testing.T) { scenarios := []struct { in LabelSet out Fingerprint }{ { in: LabelSet{}, out: 14695981039346656037, }, { in: LabelSet{"name": "garland, briggs", "fear": "love is not enough"}, out: 12952432476264840823, }, } for i, scenario := range scenarios { actual := labelSetToFastFingerprint(scenario.in) if actual != scenario.out { t.Errorf("%d. expected %d, got %d", i, scenario.out, actual) } } } func TestSignatureForLabels(t *testing.T) { scenarios := []struct { in Metric labels LabelNames out uint64 }{ { in: Metric{}, labels: nil, out: 14695981039346656037, }, { in: Metric{}, labels: LabelNames{"empty"}, out: 7187873163539638612, }, { in: Metric{"name": "garland, briggs", "fear": "love is not enough"}, labels: LabelNames{"empty"}, out: 7187873163539638612, }, { in: Metric{"name": "garland, briggs", "fear": "love is not enough"}, labels: LabelNames{"fear", "name"}, out: 5799056148416392346, }, { in: Metric{"name": "garland, briggs", "fear": "love is not enough", "foo": "bar"}, labels: LabelNames{"fear", "name"}, out: 5799056148416392346, }, { in: Metric{"name": "garland, briggs", "fear": "love is not enough"}, labels: LabelNames{}, out: 14695981039346656037, }, { in: Metric{"name": "garland, briggs", "fear": "love is not enough"}, labels: nil, out: 14695981039346656037, }, } for i, scenario := range scenarios { actual := SignatureForLabels(scenario.in, scenario.labels...) if actual != scenario.out { t.Errorf("%d. expected %d, got %d", i, scenario.out, actual) } } } func TestSignatureWithoutLabels(t *testing.T) { scenarios := []struct { in Metric labels map[LabelName]struct{} out uint64 }{ { in: Metric{}, labels: nil, out: 14695981039346656037, }, { in: Metric{"name": "garland, briggs", "fear": "love is not enough"}, labels: map[LabelName]struct{}{"fear": {}, "name": {}}, out: 14695981039346656037, }, { in: Metric{"name": "garland, briggs", "fear": "love is not enough", "foo": "bar"}, labels: map[LabelName]struct{}{"foo": {}}, out: 5799056148416392346, }, { in: Metric{"name": "garland, briggs", "fear": "love is not enough"}, labels: map[LabelName]struct{}{}, out: 5799056148416392346, }, { in: Metric{"name": "garland, briggs", "fear": "love is not enough"}, labels: nil, out: 5799056148416392346, }, } for i, scenario := range scenarios { actual := SignatureWithoutLabels(scenario.in, scenario.labels) if actual != scenario.out { t.Errorf("%d. expected %d, got %d", i, scenario.out, actual) } } } func benchmarkLabelToSignature(b *testing.B, l map[string]string, e uint64) { for i := 0; i < b.N; i++ { if a := LabelsToSignature(l); a != e { b.Fatalf("expected signature of %d for %s, got %d", e, l, a) } } } func BenchmarkLabelToSignatureScalar(b *testing.B) { benchmarkLabelToSignature(b, nil, 14695981039346656037) } func BenchmarkLabelToSignatureSingle(b *testing.B) { benchmarkLabelToSignature(b, map[string]string{"first-label": "first-label-value"}, 5146282821936882169) } func BenchmarkLabelToSignatureDouble(b *testing.B) { benchmarkLabelToSignature(b, map[string]string{"first-label": "first-label-value", "second-label": "second-label-value"}, 3195800080984914717) } func BenchmarkLabelToSignatureTriple(b *testing.B) { benchmarkLabelToSignature(b, map[string]string{"first-label": "first-label-value", "second-label": "second-label-value", "third-label": "third-label-value"}, 13843036195897128121) } func benchmarkMetricToFingerprint(b *testing.B, ls LabelSet, e Fingerprint) { for i := 0; i < b.N; i++ { if a := labelSetToFingerprint(ls); a != e { b.Fatalf("expected signature of %d for %s, got %d", e, ls, a) } } } func BenchmarkMetricToFingerprintScalar(b *testing.B) { benchmarkMetricToFingerprint(b, nil, 14695981039346656037) } func BenchmarkMetricToFingerprintSingle(b *testing.B) { benchmarkMetricToFingerprint(b, LabelSet{"first-label": "first-label-value"}, 5146282821936882169) } func BenchmarkMetricToFingerprintDouble(b *testing.B) { benchmarkMetricToFingerprint(b, LabelSet{"first-label": "first-label-value", "second-label": "second-label-value"}, 3195800080984914717) } func BenchmarkMetricToFingerprintTriple(b *testing.B) { benchmarkMetricToFingerprint(b, LabelSet{"first-label": "first-label-value", "second-label": "second-label-value", "third-label": "third-label-value"}, 13843036195897128121) } func benchmarkMetricToFastFingerprint(b *testing.B, ls LabelSet, e Fingerprint) { for i := 0; i < b.N; i++ { if a := labelSetToFastFingerprint(ls); a != e { b.Fatalf("expected signature of %d for %s, got %d", e, ls, a) } } } func BenchmarkMetricToFastFingerprintScalar(b *testing.B) { benchmarkMetricToFastFingerprint(b, nil, 14695981039346656037) } func BenchmarkMetricToFastFingerprintSingle(b *testing.B) { benchmarkMetricToFastFingerprint(b, LabelSet{"first-label": "first-label-value"}, 5147259542624943964) } func BenchmarkMetricToFastFingerprintDouble(b *testing.B) { benchmarkMetricToFastFingerprint(b, LabelSet{"first-label": "first-label-value", "second-label": "second-label-value"}, 18269973311206963528) } func BenchmarkMetricToFastFingerprintTriple(b *testing.B) { benchmarkMetricToFastFingerprint(b, LabelSet{"first-label": "first-label-value", "second-label": "second-label-value", "third-label": "third-label-value"}, 15738406913934009676) } func BenchmarkEmptyLabelSignature(b *testing.B) { input := []map[string]string{nil, {}} var ms runtime.MemStats runtime.ReadMemStats(&ms) alloc := ms.Alloc for _, labels := range input { LabelsToSignature(labels) } runtime.ReadMemStats(&ms) if got := ms.Alloc; alloc != got { b.Fatal("expected LabelsToSignature with empty labels not to perform allocations") } } func benchmarkMetricToFastFingerprintConc(b *testing.B, ls LabelSet, e Fingerprint, concLevel int) { var start, end sync.WaitGroup start.Add(1) end.Add(concLevel) errc := make(chan error, 1) for i := 0; i < concLevel; i++ { go func() { start.Wait() for j := b.N / concLevel; j >= 0; j-- { if a := labelSetToFastFingerprint(ls); a != e { select { case errc <- fmt.Errorf("expected signature of %d for %s, got %d", e, ls, a): default: } } } end.Done() }() } b.ResetTimer() start.Done() end.Wait() select { case err := <-errc: b.Fatal(err) default: } } func BenchmarkMetricToFastFingerprintTripleConc1(b *testing.B) { benchmarkMetricToFastFingerprintConc(b, LabelSet{"first-label": "first-label-value", "second-label": "second-label-value", "third-label": "third-label-value"}, 15738406913934009676, 1) } func BenchmarkMetricToFastFingerprintTripleConc2(b *testing.B) { benchmarkMetricToFastFingerprintConc(b, LabelSet{"first-label": "first-label-value", "second-label": "second-label-value", "third-label": "third-label-value"}, 15738406913934009676, 2) } func BenchmarkMetricToFastFingerprintTripleConc4(b *testing.B) { benchmarkMetricToFastFingerprintConc(b, LabelSet{"first-label": "first-label-value", "second-label": "second-label-value", "third-label": "third-label-value"}, 15738406913934009676, 4) } func BenchmarkMetricToFastFingerprintTripleConc8(b *testing.B) { benchmarkMetricToFastFingerprintConc(b, LabelSet{"first-label": "first-label-value", "second-label": "second-label-value", "third-label": "third-label-value"}, 15738406913934009676, 8) } golang-github-prometheus-common-0.55.0/model/silence.go000066400000000000000000000054241463701437000230720ustar00rootroot00000000000000// Copyright 2015 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package model import ( "encoding/json" "fmt" "regexp" "time" ) // Matcher describes a matches the value of a given label. type Matcher struct { Name LabelName `json:"name"` Value string `json:"value"` IsRegex bool `json:"isRegex"` } func (m *Matcher) UnmarshalJSON(b []byte) error { type plain Matcher if err := json.Unmarshal(b, (*plain)(m)); err != nil { return err } if len(m.Name) == 0 { return fmt.Errorf("label name in matcher must not be empty") } if m.IsRegex { if _, err := regexp.Compile(m.Value); err != nil { return err } } return nil } // Validate returns true iff all fields of the matcher have valid values. func (m *Matcher) Validate() error { if !m.Name.IsValid() { return fmt.Errorf("invalid name %q", m.Name) } if m.IsRegex { if _, err := regexp.Compile(m.Value); err != nil { return fmt.Errorf("invalid regular expression %q", m.Value) } } else if !LabelValue(m.Value).IsValid() || len(m.Value) == 0 { return fmt.Errorf("invalid value %q", m.Value) } return nil } // Silence defines the representation of a silence definition in the Prometheus // eco-system. type Silence struct { ID uint64 `json:"id,omitempty"` Matchers []*Matcher `json:"matchers"` StartsAt time.Time `json:"startsAt"` EndsAt time.Time `json:"endsAt"` CreatedAt time.Time `json:"createdAt,omitempty"` CreatedBy string `json:"createdBy"` Comment string `json:"comment,omitempty"` } // Validate returns true iff all fields of the silence have valid values. func (s *Silence) Validate() error { if len(s.Matchers) == 0 { return fmt.Errorf("at least one matcher required") } for _, m := range s.Matchers { if err := m.Validate(); err != nil { return fmt.Errorf("invalid matcher: %w", err) } } if s.StartsAt.IsZero() { return fmt.Errorf("start time missing") } if s.EndsAt.IsZero() { return fmt.Errorf("end time missing") } if s.EndsAt.Before(s.StartsAt) { return fmt.Errorf("start time must be before end time") } if s.CreatedBy == "" { return fmt.Errorf("creator information missing") } if s.Comment == "" { return fmt.Errorf("comment missing") } if s.CreatedAt.IsZero() { return fmt.Errorf("creation timestamp missing") } return nil } golang-github-prometheus-common-0.55.0/model/silence_test.go000066400000000000000000000127761463701437000241410ustar00rootroot00000000000000// Copyright 2015 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package model import ( "strings" "testing" "time" ) func TestMatcherValidate(t *testing.T) { cases := []struct { matcher *Matcher legacyErr string utf8Err string }{ { matcher: &Matcher{ Name: "name", Value: "value", }, }, { matcher: &Matcher{ Name: "name", Value: "value", IsRegex: true, }, }, { matcher: &Matcher{ Name: "name!", Value: "value", }, legacyErr: "invalid name", }, { matcher: &Matcher{ Name: "", Value: "value", }, legacyErr: "invalid name", utf8Err: "invalid name", }, { matcher: &Matcher{ Name: "name", Value: "value\xff", }, legacyErr: "invalid value", utf8Err: "invalid value", }, { matcher: &Matcher{ Name: "name", Value: "", }, legacyErr: "invalid value", utf8Err: "invalid value", }, { matcher: &Matcher{ Name: "a\xc5z", Value: "", }, legacyErr: "invalid name", utf8Err: "invalid name", }, } for i, c := range cases { NameValidationScheme = LegacyValidation legacyErr := c.matcher.Validate() NameValidationScheme = UTF8Validation utf8Err := c.matcher.Validate() if legacyErr == nil && utf8Err == nil { if c.legacyErr == "" && c.utf8Err == "" { continue } if c.legacyErr != "" { t.Errorf("%d. Expected error for legacy validation %q but got none", i, c.legacyErr) } if c.utf8Err != "" { t.Errorf("%d. Expected error for utf-8 validation %q but got none", i, c.utf8Err) } continue } if legacyErr != nil { if c.legacyErr == "" { t.Errorf("%d. Expected no legacy validation error but got %q", i, legacyErr) } else if !strings.Contains(legacyErr.Error(), c.legacyErr) { t.Errorf("%d. Expected error to contain %q but got %q", i, c.legacyErr, legacyErr) } } if utf8Err != nil { if c.utf8Err == "" { t.Errorf("%d. Expected no utf-8 validation error but got %q", i, utf8Err) continue } if !strings.Contains(utf8Err.Error(), c.utf8Err) { t.Errorf("%d. Expected error to contain %q but got %q", i, c.utf8Err, utf8Err) } } } } func TestSilenceValidate(t *testing.T) { ts := time.Now() cases := []struct { sil *Silence err string }{ { sil: &Silence{ Matchers: []*Matcher{ {Name: "name", Value: "value"}, }, StartsAt: ts, EndsAt: ts, CreatedAt: ts, CreatedBy: "name", Comment: "comment", }, }, { sil: &Silence{ Matchers: []*Matcher{ {Name: "name", Value: "value"}, {Name: "name", Value: "value"}, {Name: "name", Value: "value"}, {Name: "name", Value: "value", IsRegex: true}, }, StartsAt: ts, EndsAt: ts, CreatedAt: ts, CreatedBy: "name", Comment: "comment", }, }, { sil: &Silence{ Matchers: []*Matcher{ {Name: "name", Value: "value"}, }, StartsAt: ts, EndsAt: ts.Add(-1 * time.Minute), CreatedAt: ts, CreatedBy: "name", Comment: "comment", }, err: "start time must be before end time", }, { sil: &Silence{ Matchers: []*Matcher{ {Name: "name", Value: "value"}, }, StartsAt: ts, CreatedAt: ts, CreatedBy: "name", Comment: "comment", }, err: "end time missing", }, { sil: &Silence{ Matchers: []*Matcher{ {Name: "name", Value: "value"}, }, EndsAt: ts, CreatedAt: ts, CreatedBy: "name", Comment: "comment", }, err: "start time missing", }, { sil: &Silence{ Matchers: []*Matcher{ {Name: "!name", Value: "value"}, }, StartsAt: ts, EndsAt: ts, CreatedAt: ts, CreatedBy: "name", Comment: "comment", }, err: "invalid matcher", }, { sil: &Silence{ Matchers: []*Matcher{ {Name: "name", Value: "value"}, }, StartsAt: ts, EndsAt: ts, CreatedAt: ts, CreatedBy: "name", }, err: "comment missing", }, { sil: &Silence{ Matchers: []*Matcher{ {Name: "name", Value: "value"}, }, StartsAt: ts, EndsAt: ts, CreatedBy: "name", Comment: "comment", }, err: "creation timestamp missing", }, { sil: &Silence{ Matchers: []*Matcher{ {Name: "name", Value: "value"}, }, StartsAt: ts, EndsAt: ts, CreatedAt: ts, Comment: "comment", }, err: "creator information missing", }, { sil: &Silence{ Matchers: []*Matcher{}, StartsAt: ts, EndsAt: ts, CreatedAt: ts, Comment: "comment", }, err: "at least one matcher required", }, } for i, c := range cases { NameValidationScheme = LegacyValidation err := c.sil.Validate() if err == nil { if c.err == "" { continue } t.Errorf("%d. Expected error %q but got none", i, c.err) continue } if c.err == "" { t.Errorf("%d. Expected no error but got %q", i, err) continue } if !strings.Contains(err.Error(), c.err) { t.Errorf("%d. Expected error to contain %q but got %q", i, c.err, err) } } } golang-github-prometheus-common-0.55.0/model/time.go000066400000000000000000000205651463701437000224110ustar00rootroot00000000000000// Copyright 2013 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package model import ( "encoding/json" "errors" "fmt" "math" "strconv" "strings" "time" ) const ( // MinimumTick is the minimum supported time resolution. This has to be // at least time.Second in order for the code below to work. minimumTick = time.Millisecond // second is the Time duration equivalent to one second. second = int64(time.Second / minimumTick) // The number of nanoseconds per minimum tick. nanosPerTick = int64(minimumTick / time.Nanosecond) // Earliest is the earliest Time representable. Handy for // initializing a high watermark. Earliest = Time(math.MinInt64) // Latest is the latest Time representable. Handy for initializing // a low watermark. Latest = Time(math.MaxInt64) ) // Time is the number of milliseconds since the epoch // (1970-01-01 00:00 UTC) excluding leap seconds. type Time int64 // Interval describes an interval between two timestamps. type Interval struct { Start, End Time } // Now returns the current time as a Time. func Now() Time { return TimeFromUnixNano(time.Now().UnixNano()) } // TimeFromUnix returns the Time equivalent to the Unix Time t // provided in seconds. func TimeFromUnix(t int64) Time { return Time(t * second) } // TimeFromUnixNano returns the Time equivalent to the Unix Time // t provided in nanoseconds. func TimeFromUnixNano(t int64) Time { return Time(t / nanosPerTick) } // Equal reports whether two Times represent the same instant. func (t Time) Equal(o Time) bool { return t == o } // Before reports whether the Time t is before o. func (t Time) Before(o Time) bool { return t < o } // After reports whether the Time t is after o. func (t Time) After(o Time) bool { return t > o } // Add returns the Time t + d. func (t Time) Add(d time.Duration) Time { return t + Time(d/minimumTick) } // Sub returns the Duration t - o. func (t Time) Sub(o Time) time.Duration { return time.Duration(t-o) * minimumTick } // Time returns the time.Time representation of t. func (t Time) Time() time.Time { return time.Unix(int64(t)/second, (int64(t)%second)*nanosPerTick) } // Unix returns t as a Unix time, the number of seconds elapsed // since January 1, 1970 UTC. func (t Time) Unix() int64 { return int64(t) / second } // UnixNano returns t as a Unix time, the number of nanoseconds elapsed // since January 1, 1970 UTC. func (t Time) UnixNano() int64 { return int64(t) * nanosPerTick } // The number of digits after the dot. var dotPrecision = int(math.Log10(float64(second))) // String returns a string representation of the Time. func (t Time) String() string { return strconv.FormatFloat(float64(t)/float64(second), 'f', -1, 64) } // MarshalJSON implements the json.Marshaler interface. func (t Time) MarshalJSON() ([]byte, error) { return []byte(t.String()), nil } // UnmarshalJSON implements the json.Unmarshaler interface. func (t *Time) UnmarshalJSON(b []byte) error { p := strings.Split(string(b), ".") switch len(p) { case 1: v, err := strconv.ParseInt(string(p[0]), 10, 64) if err != nil { return err } *t = Time(v * second) case 2: v, err := strconv.ParseInt(string(p[0]), 10, 64) if err != nil { return err } v *= second prec := dotPrecision - len(p[1]) if prec < 0 { p[1] = p[1][:dotPrecision] } else if prec > 0 { p[1] = p[1] + strings.Repeat("0", prec) } va, err := strconv.ParseInt(p[1], 10, 32) if err != nil { return err } // If the value was something like -0.1 the negative is lost in the // parsing because of the leading zero, this ensures that we capture it. if len(p[0]) > 0 && p[0][0] == '-' && v+va > 0 { *t = Time(v+va) * -1 } else { *t = Time(v + va) } default: return fmt.Errorf("invalid time %q", string(b)) } return nil } // Duration wraps time.Duration. It is used to parse the custom duration format // from YAML. // This type should not propagate beyond the scope of input/output processing. type Duration time.Duration // Set implements pflag/flag.Value func (d *Duration) Set(s string) error { var err error *d, err = ParseDuration(s) return err } // Type implements pflag.Value func (d *Duration) Type() string { return "duration" } func isdigit(c byte) bool { return c >= '0' && c <= '9' } // Units are required to go in order from biggest to smallest. // This guards against confusion from "1m1d" being 1 minute + 1 day, not 1 month + 1 day. var unitMap = map[string]struct { pos int mult uint64 }{ "ms": {7, uint64(time.Millisecond)}, "s": {6, uint64(time.Second)}, "m": {5, uint64(time.Minute)}, "h": {4, uint64(time.Hour)}, "d": {3, uint64(24 * time.Hour)}, "w": {2, uint64(7 * 24 * time.Hour)}, "y": {1, uint64(365 * 24 * time.Hour)}, } // ParseDuration parses a string into a time.Duration, assuming that a year // always has 365d, a week always has 7d, and a day always has 24h. func ParseDuration(s string) (Duration, error) { switch s { case "0": // Allow 0 without a unit. return 0, nil case "": return 0, errors.New("empty duration string") } orig := s var dur uint64 lastUnitPos := 0 for s != "" { if !isdigit(s[0]) { return 0, fmt.Errorf("not a valid duration string: %q", orig) } // Consume [0-9]* i := 0 for ; i < len(s) && isdigit(s[i]); i++ { } v, err := strconv.ParseUint(s[:i], 10, 0) if err != nil { return 0, fmt.Errorf("not a valid duration string: %q", orig) } s = s[i:] // Consume unit. for i = 0; i < len(s) && !isdigit(s[i]); i++ { } if i == 0 { return 0, fmt.Errorf("not a valid duration string: %q", orig) } u := s[:i] s = s[i:] unit, ok := unitMap[u] if !ok { return 0, fmt.Errorf("unknown unit %q in duration %q", u, orig) } if unit.pos <= lastUnitPos { // Units must go in order from biggest to smallest. return 0, fmt.Errorf("not a valid duration string: %q", orig) } lastUnitPos = unit.pos // Check if the provided duration overflows time.Duration (> ~ 290years). if v > 1<<63/unit.mult { return 0, errors.New("duration out of range") } dur += v * unit.mult if dur > 1<<63-1 { return 0, errors.New("duration out of range") } } return Duration(dur), nil } func (d Duration) String() string { var ( ms = int64(time.Duration(d) / time.Millisecond) r = "" ) if ms == 0 { return "0s" } f := func(unit string, mult int64, exact bool) { if exact && ms%mult != 0 { return } if v := ms / mult; v > 0 { r += fmt.Sprintf("%d%s", v, unit) ms -= v * mult } } // Only format years and weeks if the remainder is zero, as it is often // easier to read 90d than 12w6d. f("y", 1000*60*60*24*365, true) f("w", 1000*60*60*24*7, true) f("d", 1000*60*60*24, false) f("h", 1000*60*60, false) f("m", 1000*60, false) f("s", 1000, false) f("ms", 1, false) return r } // MarshalJSON implements the json.Marshaler interface. func (d Duration) MarshalJSON() ([]byte, error) { return json.Marshal(d.String()) } // UnmarshalJSON implements the json.Unmarshaler interface. func (d *Duration) UnmarshalJSON(bytes []byte) error { var s string if err := json.Unmarshal(bytes, &s); err != nil { return err } dur, err := ParseDuration(s) if err != nil { return err } *d = dur return nil } // MarshalText implements the encoding.TextMarshaler interface. func (d *Duration) MarshalText() ([]byte, error) { return []byte(d.String()), nil } // UnmarshalText implements the encoding.TextUnmarshaler interface. func (d *Duration) UnmarshalText(text []byte) error { var err error *d, err = ParseDuration(string(text)) return err } // MarshalYAML implements the yaml.Marshaler interface. func (d Duration) MarshalYAML() (interface{}, error) { return d.String(), nil } // UnmarshalYAML implements the yaml.Unmarshaler interface. func (d *Duration) UnmarshalYAML(unmarshal func(interface{}) error) error { var s string if err := unmarshal(&s); err != nil { return err } dur, err := ParseDuration(s) if err != nil { return err } *d = dur return nil } golang-github-prometheus-common-0.55.0/model/time_test.go000066400000000000000000000174551463701437000234540ustar00rootroot00000000000000// Copyright 2013 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package model import ( "encoding/json" "strconv" "testing" "time" ) func TestComparators(t *testing.T) { t1a := TimeFromUnix(0) t1b := TimeFromUnix(0) t2 := TimeFromUnix(2*second - 1) if !t1a.Equal(t1b) { t.Fatalf("Expected %s to be equal to %s", t1a, t1b) } if t1a.Equal(t2) { t.Fatalf("Expected %s to not be equal to %s", t1a, t2) } if !t1a.Before(t2) { t.Fatalf("Expected %s to be before %s", t1a, t2) } if t1a.Before(t1b) { t.Fatalf("Expected %s to not be before %s", t1a, t1b) } if !t2.After(t1a) { t.Fatalf("Expected %s to be after %s", t2, t1a) } if t1b.After(t1a) { t.Fatalf("Expected %s to not be after %s", t1b, t1a) } } func TestTimeConversions(t *testing.T) { unixSecs := int64(1136239445) unixNsecs := int64(123456789) unixNano := unixSecs*1e9 + unixNsecs t1 := time.Unix(unixSecs, unixNsecs-unixNsecs%nanosPerTick) t2 := time.Unix(unixSecs, unixNsecs) ts := TimeFromUnixNano(unixNano) if !ts.Time().Equal(t1) { t.Fatalf("Expected %s, got %s", t1, ts.Time()) } // Test available precision. ts = TimeFromUnixNano(t2.UnixNano()) if !ts.Time().Equal(t1) { t.Fatalf("Expected %s, got %s", t1, ts.Time()) } if ts.UnixNano() != unixNano-unixNano%nanosPerTick { t.Fatalf("Expected %d, got %d", unixNano, ts.UnixNano()) } } func TestDuration(t *testing.T) { duration := time.Second + time.Minute + time.Hour goTime := time.Unix(1136239445, 0) ts := TimeFromUnix(goTime.Unix()) if !goTime.Add(duration).Equal(ts.Add(duration).Time()) { t.Fatalf("Expected %s to be equal to %s", goTime.Add(duration), ts.Add(duration)) } earlier := ts.Add(-duration) delta := ts.Sub(earlier) if delta != duration { t.Fatalf("Expected %s to be equal to %s", delta, duration) } } func TestParseDuration(t *testing.T) { cases := []struct { in string out time.Duration expectedString string }{ { in: "0", out: 0, expectedString: "0s", }, { in: "0w", out: 0, expectedString: "0s", }, { in: "0s", out: 0, }, { in: "324ms", out: 324 * time.Millisecond, }, { in: "3s", out: 3 * time.Second, }, { in: "5m", out: 5 * time.Minute, }, { in: "1h", out: time.Hour, }, { in: "4d", out: 4 * 24 * time.Hour, }, { in: "4d1h", out: 4*24*time.Hour + time.Hour, }, { in: "14d", out: 14 * 24 * time.Hour, expectedString: "2w", }, { in: "3w", out: 3 * 7 * 24 * time.Hour, }, { in: "3w2d1h", out: 3*7*24*time.Hour + 2*24*time.Hour + time.Hour, expectedString: "23d1h", }, { in: "10y", out: 10 * 365 * 24 * time.Hour, }, } for _, c := range cases { d, err := ParseDuration(c.in) if err != nil { t.Errorf("Unexpected error on input %q", c.in) } if time.Duration(d) != c.out { t.Errorf("Expected %v but got %v", c.out, d) } expectedString := c.expectedString if c.expectedString == "" { expectedString = c.in } if d.String() != expectedString { t.Errorf("Expected duration string %q but got %q", c.in, d.String()) } } } func TestDuration_UnmarshalText(t *testing.T) { cases := []struct { in string out time.Duration expectedString string }{ { in: "0", out: 0, expectedString: "0s", }, { in: "0w", out: 0, expectedString: "0s", }, { in: "0s", out: 0, }, { in: "324ms", out: 324 * time.Millisecond, }, { in: "3s", out: 3 * time.Second, }, { in: "5m", out: 5 * time.Minute, }, { in: "1h", out: time.Hour, }, { in: "4d", out: 4 * 24 * time.Hour, }, { in: "4d1h", out: 4*24*time.Hour + time.Hour, }, { in: "14d", out: 14 * 24 * time.Hour, expectedString: "2w", }, { in: "3w", out: 3 * 7 * 24 * time.Hour, }, { in: "3w2d1h", out: 3*7*24*time.Hour + 2*24*time.Hour + time.Hour, expectedString: "23d1h", }, { in: "10y", out: 10 * 365 * 24 * time.Hour, }, } for _, c := range cases { var d Duration err := d.UnmarshalText([]byte(c.in)) if err != nil { t.Errorf("Unexpected error on input %q", c.in) } if time.Duration(d) != c.out { t.Errorf("Expected %v but got %v", c.out, d) } expectedString := c.expectedString if c.expectedString == "" { expectedString = c.in } text, _ := d.MarshalText() // MarshalText returns hardcoded nil if string(text) != expectedString { t.Errorf("Expected duration string %q but got %q", c.in, d.String()) } } } func TestDuration_UnmarshalJSON(t *testing.T) { cases := []struct { in string out time.Duration expectedString string }{ { in: `"0"`, out: 0, expectedString: `"0s"`, }, { in: `"0w"`, out: 0, expectedString: `"0s"`, }, { in: `"0s"`, out: 0, }, { in: `"324ms"`, out: 324 * time.Millisecond, }, { in: `"3s"`, out: 3 * time.Second, }, { in: `"5m"`, out: 5 * time.Minute, }, { in: `"1h"`, out: time.Hour, }, { in: `"4d"`, out: 4 * 24 * time.Hour, }, { in: `"4d1h"`, out: 4*24*time.Hour + time.Hour, }, { in: `"14d"`, out: 14 * 24 * time.Hour, expectedString: `"2w"`, }, { in: `"3w"`, out: 3 * 7 * 24 * time.Hour, }, { in: `"3w2d1h"`, out: 3*7*24*time.Hour + 2*24*time.Hour + time.Hour, expectedString: `"23d1h"`, }, { in: `"10y"`, out: 10 * 365 * 24 * time.Hour, }, { in: `"289y"`, out: 289 * 365 * 24 * time.Hour, }, } for _, c := range cases { var d Duration err := json.Unmarshal([]byte(c.in), &d) if err != nil { t.Errorf("Unexpected error on input %q", c.in) } if time.Duration(d) != c.out { t.Errorf("Expected %v but got %v", c.out, d) } expectedString := c.expectedString if c.expectedString == "" { expectedString = c.in } bytes, err := json.Marshal(d) if err != nil { t.Errorf("Unexpected error on marshal of %v: %s", d, err) } if string(bytes) != expectedString { t.Errorf("Expected duration string %q but got %q", c.in, d.String()) } } } func TestParseBadDuration(t *testing.T) { cases := []string{ "1", "1y1m1d", "-1w", "1.5d", "d", "294y", "200y10400w", "107675d", "2584200h", "", } for _, c := range cases { _, err := ParseDuration(c) if err == nil { t.Errorf("Expected error on input %s", c) } } } func TestTimeJSON(t *testing.T) { tests := []struct { in Time out string }{ {Time(1), `0.001`}, {Time(-1), `-0.001`}, } for i, test := range tests { t.Run(strconv.Itoa(i), func(t *testing.T) { b, err := test.in.MarshalJSON() if err != nil { t.Fatalf("Error marshaling time: %v", err) } if string(b) != test.out { t.Errorf("Mismatch in marshal expected=%s actual=%s", test.out, b) } var tm Time if err := tm.UnmarshalJSON(b); err != nil { t.Fatalf("Error Unmarshaling time: %v", err) } if !test.in.Equal(tm) { t.Fatalf("Mismatch after Unmarshal expected=%v actual=%v", test.in, tm) } }) } } func BenchmarkParseDuration(b *testing.B) { const data = "30s" for i := 0; i < b.N; i++ { _, err := ParseDuration(data) if err != nil { b.Fatal(err) } } } golang-github-prometheus-common-0.55.0/model/value.go000066400000000000000000000215631463701437000225660ustar00rootroot00000000000000// Copyright 2013 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package model import ( "encoding/json" "fmt" "sort" "strconv" "strings" ) // ZeroSample is the pseudo zero-value of Sample used to signal a // non-existing sample. It is a Sample with timestamp Earliest, value 0.0, // and metric nil. Note that the natural zero value of Sample has a timestamp // of 0, which is possible to appear in a real Sample and thus not suitable // to signal a non-existing Sample. var ZeroSample = Sample{Timestamp: Earliest} // Sample is a sample pair associated with a metric. A single sample must either // define Value or Histogram but not both. Histogram == nil implies the Value // field is used, otherwise it should be ignored. type Sample struct { Metric Metric `json:"metric"` Value SampleValue `json:"value"` Timestamp Time `json:"timestamp"` Histogram *SampleHistogram `json:"histogram"` } // Equal compares first the metrics, then the timestamp, then the value. The // semantics of value equality is defined by SampleValue.Equal. func (s *Sample) Equal(o *Sample) bool { if s == o { return true } if !s.Metric.Equal(o.Metric) { return false } if !s.Timestamp.Equal(o.Timestamp) { return false } if s.Histogram != nil { return s.Histogram.Equal(o.Histogram) } return s.Value.Equal(o.Value) } func (s Sample) String() string { if s.Histogram != nil { return fmt.Sprintf("%s => %s", s.Metric, SampleHistogramPair{ Timestamp: s.Timestamp, Histogram: s.Histogram, }) } return fmt.Sprintf("%s => %s", s.Metric, SamplePair{ Timestamp: s.Timestamp, Value: s.Value, }) } // MarshalJSON implements json.Marshaler. func (s Sample) MarshalJSON() ([]byte, error) { if s.Histogram != nil { v := struct { Metric Metric `json:"metric"` Histogram SampleHistogramPair `json:"histogram"` }{ Metric: s.Metric, Histogram: SampleHistogramPair{ Timestamp: s.Timestamp, Histogram: s.Histogram, }, } return json.Marshal(&v) } v := struct { Metric Metric `json:"metric"` Value SamplePair `json:"value"` }{ Metric: s.Metric, Value: SamplePair{ Timestamp: s.Timestamp, Value: s.Value, }, } return json.Marshal(&v) } // UnmarshalJSON implements json.Unmarshaler. func (s *Sample) UnmarshalJSON(b []byte) error { v := struct { Metric Metric `json:"metric"` Value SamplePair `json:"value"` Histogram SampleHistogramPair `json:"histogram"` }{ Metric: s.Metric, Value: SamplePair{ Timestamp: s.Timestamp, Value: s.Value, }, Histogram: SampleHistogramPair{ Timestamp: s.Timestamp, Histogram: s.Histogram, }, } if err := json.Unmarshal(b, &v); err != nil { return err } s.Metric = v.Metric if v.Histogram.Histogram != nil { s.Timestamp = v.Histogram.Timestamp s.Histogram = v.Histogram.Histogram } else { s.Timestamp = v.Value.Timestamp s.Value = v.Value.Value } return nil } // Samples is a sortable Sample slice. It implements sort.Interface. type Samples []*Sample func (s Samples) Len() int { return len(s) } // Less compares first the metrics, then the timestamp. func (s Samples) Less(i, j int) bool { switch { case s[i].Metric.Before(s[j].Metric): return true case s[j].Metric.Before(s[i].Metric): return false case s[i].Timestamp.Before(s[j].Timestamp): return true default: return false } } func (s Samples) Swap(i, j int) { s[i], s[j] = s[j], s[i] } // Equal compares two sets of samples and returns true if they are equal. func (s Samples) Equal(o Samples) bool { if len(s) != len(o) { return false } for i, sample := range s { if !sample.Equal(o[i]) { return false } } return true } // SampleStream is a stream of Values belonging to an attached COWMetric. type SampleStream struct { Metric Metric `json:"metric"` Values []SamplePair `json:"values"` Histograms []SampleHistogramPair `json:"histograms"` } func (ss SampleStream) String() string { valuesLength := len(ss.Values) vals := make([]string, valuesLength+len(ss.Histograms)) for i, v := range ss.Values { vals[i] = v.String() } for i, v := range ss.Histograms { vals[i+valuesLength] = v.String() } return fmt.Sprintf("%s =>\n%s", ss.Metric, strings.Join(vals, "\n")) } func (ss SampleStream) MarshalJSON() ([]byte, error) { if len(ss.Histograms) > 0 && len(ss.Values) > 0 { v := struct { Metric Metric `json:"metric"` Values []SamplePair `json:"values"` Histograms []SampleHistogramPair `json:"histograms"` }{ Metric: ss.Metric, Values: ss.Values, Histograms: ss.Histograms, } return json.Marshal(&v) } else if len(ss.Histograms) > 0 { v := struct { Metric Metric `json:"metric"` Histograms []SampleHistogramPair `json:"histograms"` }{ Metric: ss.Metric, Histograms: ss.Histograms, } return json.Marshal(&v) } else { v := struct { Metric Metric `json:"metric"` Values []SamplePair `json:"values"` }{ Metric: ss.Metric, Values: ss.Values, } return json.Marshal(&v) } } func (ss *SampleStream) UnmarshalJSON(b []byte) error { v := struct { Metric Metric `json:"metric"` Values []SamplePair `json:"values"` Histograms []SampleHistogramPair `json:"histograms"` }{ Metric: ss.Metric, Values: ss.Values, Histograms: ss.Histograms, } if err := json.Unmarshal(b, &v); err != nil { return err } ss.Metric = v.Metric ss.Values = v.Values ss.Histograms = v.Histograms return nil } // Scalar is a scalar value evaluated at the set timestamp. type Scalar struct { Value SampleValue `json:"value"` Timestamp Time `json:"timestamp"` } func (s Scalar) String() string { return fmt.Sprintf("scalar: %v @[%v]", s.Value, s.Timestamp) } // MarshalJSON implements json.Marshaler. func (s Scalar) MarshalJSON() ([]byte, error) { v := strconv.FormatFloat(float64(s.Value), 'f', -1, 64) return json.Marshal([...]interface{}{s.Timestamp, string(v)}) } // UnmarshalJSON implements json.Unmarshaler. func (s *Scalar) UnmarshalJSON(b []byte) error { var f string v := [...]interface{}{&s.Timestamp, &f} if err := json.Unmarshal(b, &v); err != nil { return err } value, err := strconv.ParseFloat(f, 64) if err != nil { return fmt.Errorf("error parsing sample value: %w", err) } s.Value = SampleValue(value) return nil } // String is a string value evaluated at the set timestamp. type String struct { Value string `json:"value"` Timestamp Time `json:"timestamp"` } func (s *String) String() string { return s.Value } // MarshalJSON implements json.Marshaler. func (s String) MarshalJSON() ([]byte, error) { return json.Marshal([]interface{}{s.Timestamp, s.Value}) } // UnmarshalJSON implements json.Unmarshaler. func (s *String) UnmarshalJSON(b []byte) error { v := [...]interface{}{&s.Timestamp, &s.Value} return json.Unmarshal(b, &v) } // Vector is basically only an alias for Samples, but the // contract is that in a Vector, all Samples have the same timestamp. type Vector []*Sample func (vec Vector) String() string { entries := make([]string, len(vec)) for i, s := range vec { entries[i] = s.String() } return strings.Join(entries, "\n") } func (vec Vector) Len() int { return len(vec) } func (vec Vector) Swap(i, j int) { vec[i], vec[j] = vec[j], vec[i] } // Less compares first the metrics, then the timestamp. func (vec Vector) Less(i, j int) bool { switch { case vec[i].Metric.Before(vec[j].Metric): return true case vec[j].Metric.Before(vec[i].Metric): return false case vec[i].Timestamp.Before(vec[j].Timestamp): return true default: return false } } // Equal compares two sets of samples and returns true if they are equal. func (vec Vector) Equal(o Vector) bool { if len(vec) != len(o) { return false } for i, sample := range vec { if !sample.Equal(o[i]) { return false } } return true } // Matrix is a list of time series. type Matrix []*SampleStream func (m Matrix) Len() int { return len(m) } func (m Matrix) Less(i, j int) bool { return m[i].Metric.Before(m[j].Metric) } func (m Matrix) Swap(i, j int) { m[i], m[j] = m[j], m[i] } func (mat Matrix) String() string { matCp := make(Matrix, len(mat)) copy(matCp, mat) sort.Sort(matCp) strs := make([]string, len(matCp)) for i, ss := range matCp { strs[i] = ss.String() } return strings.Join(strs, "\n") } golang-github-prometheus-common-0.55.0/model/value_float.go000066400000000000000000000057141463701437000237530ustar00rootroot00000000000000// Copyright 2013 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package model import ( "encoding/json" "fmt" "math" "strconv" ) // ZeroSamplePair is the pseudo zero-value of SamplePair used to signal a // non-existing sample pair. It is a SamplePair with timestamp Earliest and // value 0.0. Note that the natural zero value of SamplePair has a timestamp // of 0, which is possible to appear in a real SamplePair and thus not // suitable to signal a non-existing SamplePair. var ZeroSamplePair = SamplePair{Timestamp: Earliest} // A SampleValue is a representation of a value for a given sample at a given // time. type SampleValue float64 // MarshalJSON implements json.Marshaler. func (v SampleValue) MarshalJSON() ([]byte, error) { return json.Marshal(v.String()) } // UnmarshalJSON implements json.Unmarshaler. func (v *SampleValue) UnmarshalJSON(b []byte) error { if len(b) < 2 || b[0] != '"' || b[len(b)-1] != '"' { return fmt.Errorf("sample value must be a quoted string") } f, err := strconv.ParseFloat(string(b[1:len(b)-1]), 64) if err != nil { return err } *v = SampleValue(f) return nil } // Equal returns true if the value of v and o is equal or if both are NaN. Note // that v==o is false if both are NaN. If you want the conventional float // behavior, use == to compare two SampleValues. func (v SampleValue) Equal(o SampleValue) bool { if v == o { return true } return math.IsNaN(float64(v)) && math.IsNaN(float64(o)) } func (v SampleValue) String() string { return strconv.FormatFloat(float64(v), 'f', -1, 64) } // SamplePair pairs a SampleValue with a Timestamp. type SamplePair struct { Timestamp Time Value SampleValue } func (s SamplePair) MarshalJSON() ([]byte, error) { t, err := json.Marshal(s.Timestamp) if err != nil { return nil, err } v, err := json.Marshal(s.Value) if err != nil { return nil, err } return []byte(fmt.Sprintf("[%s,%s]", t, v)), nil } // UnmarshalJSON implements json.Unmarshaler. func (s *SamplePair) UnmarshalJSON(b []byte) error { v := [...]json.Unmarshaler{&s.Timestamp, &s.Value} return json.Unmarshal(b, &v) } // Equal returns true if this SamplePair and o have equal Values and equal // Timestamps. The semantics of Value equality is defined by SampleValue.Equal. func (s *SamplePair) Equal(o *SamplePair) bool { return s == o || (s.Value.Equal(o.Value) && s.Timestamp.Equal(o.Timestamp)) } func (s SamplePair) String() string { return fmt.Sprintf("%s @[%s]", s.Value, s.Timestamp) } golang-github-prometheus-common-0.55.0/model/value_float_test.go000066400000000000000000000135741463701437000250150ustar00rootroot00000000000000// Copyright 2013 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package model import ( "encoding/json" "math" "reflect" "testing" ) var ( samplePairMatrixPlain = `[{"metric":{"__name__":"test_metric"},"values":[[1234.567,"123.1"],[12345.678,"123.12"]]},{"metric":{"foo":"bar"},"values":[[2234.567,"223.1"],[22345.678,"223.12"]]}]` samplePairMatrixValue = Matrix{ &SampleStream{ Metric: Metric{ MetricNameLabel: "test_metric", }, Values: []SamplePair{ { Value: 123.1, Timestamp: 1234567, }, { Value: 123.12, Timestamp: 12345678, }, }, }, &SampleStream{ Metric: Metric{ "foo": "bar", }, Values: []SamplePair{ { Value: 223.1, Timestamp: 2234567, }, { Value: 223.12, Timestamp: 22345678, }, }, }, } ) func TestEqualValues(t *testing.T) { tests := map[string]struct { in1, in2 SampleValue want bool }{ "equal floats": { in1: 3.14, in2: 3.14, want: true, }, "unequal floats": { in1: 3.14, in2: 3.1415, want: false, }, "positive infinities": { in1: SampleValue(math.Inf(+1)), in2: SampleValue(math.Inf(+1)), want: true, }, "negative infinities": { in1: SampleValue(math.Inf(-1)), in2: SampleValue(math.Inf(-1)), want: true, }, "different infinities": { in1: SampleValue(math.Inf(+1)), in2: SampleValue(math.Inf(-1)), want: false, }, "number and infinity": { in1: 42, in2: SampleValue(math.Inf(+1)), want: false, }, "number and NaN": { in1: 42, in2: SampleValue(math.NaN()), want: false, }, "NaNs": { in1: SampleValue(math.NaN()), in2: SampleValue(math.NaN()), want: true, // !!! }, } for name, test := range tests { got := test.in1.Equal(test.in2) if got != test.want { t.Errorf("Comparing %s, %f and %f: got %t, want %t", name, test.in1, test.in2, got, test.want) } } } func TestSamplePairJSON(t *testing.T) { input := []struct { plain string value SamplePair }{ { plain: `[1234.567,"123.1"]`, value: SamplePair{ Value: 123.1, Timestamp: 1234567, }, }, } for _, test := range input { b, err := json.Marshal(test.value) if err != nil { t.Error(err) continue } if string(b) != test.plain { t.Errorf("encoding error: expected %q, got %q", test.plain, b) continue } var sp SamplePair err = json.Unmarshal(b, &sp) if err != nil { t.Error(err) continue } if sp != test.value { t.Errorf("decoding error: expected %v, got %v", test.value, sp) } } } func TestSampleJSON(t *testing.T) { input := []struct { plain string value Sample }{ { plain: `{"metric":{"__name__":"test_metric"},"value":[1234.567,"123.1"]}`, value: Sample{ Metric: Metric{ MetricNameLabel: "test_metric", }, Value: 123.1, Timestamp: 1234567, }, }, } for _, test := range input { b, err := json.Marshal(test.value) if err != nil { t.Error(err) continue } if string(b) != test.plain { t.Errorf("encoding error: expected %q, got %q", test.plain, b) continue } var sv Sample err = json.Unmarshal(b, &sv) if err != nil { t.Error(err) continue } if !reflect.DeepEqual(sv, test.value) { t.Errorf("decoding error: expected %v, got %v", test.value, sv) } } } func TestVectorJSON(t *testing.T) { input := []struct { plain string value Vector }{ { plain: `[]`, value: Vector{}, }, { plain: `[{"metric":{"__name__":"test_metric"},"value":[1234.567,"123.1"]}]`, value: Vector{&Sample{ Metric: Metric{ MetricNameLabel: "test_metric", }, Value: 123.1, Timestamp: 1234567, }}, }, { plain: `[{"metric":{"__name__":"test_metric"},"value":[1234.567,"123.1"]},{"metric":{"foo":"bar"},"value":[1.234,"+Inf"]}]`, value: Vector{ &Sample{ Metric: Metric{ MetricNameLabel: "test_metric", }, Value: 123.1, Timestamp: 1234567, }, &Sample{ Metric: Metric{ "foo": "bar", }, Value: SampleValue(math.Inf(1)), Timestamp: 1234, }, }, }, } for _, test := range input { b, err := json.Marshal(test.value) if err != nil { t.Error(err) continue } if string(b) != test.plain { t.Errorf("encoding error: expected %q, got %q", test.plain, b) continue } var vec Vector err = json.Unmarshal(b, &vec) if err != nil { t.Error(err) continue } if !reflect.DeepEqual(vec, test.value) { t.Errorf("decoding error: expected %v, got %v", test.value, vec) } } } func TestMatrixJSON(t *testing.T) { input := []struct { plain string value Matrix }{ { plain: `[]`, value: Matrix{}, }, { plain: samplePairMatrixPlain, value: samplePairMatrixValue, }, } for _, test := range input { b, err := json.Marshal(test.value) if err != nil { t.Error(err) continue } if string(b) != test.plain { t.Errorf("encoding error: expected %q, got %q", test.plain, b) continue } var mat Matrix err = json.Unmarshal(b, &mat) if err != nil { t.Error(err) continue } if !reflect.DeepEqual(mat, test.value) { t.Errorf("decoding error: expected %v, got %v", test.value, mat) } } } func BenchmarkJSONMarshallingSamplePairMatrix(b *testing.B) { for i := 0; i < b.N; i++ { _, err := json.Marshal(samplePairMatrixValue) if err != nil { b.Fatal("error marshalling") } } } golang-github-prometheus-common-0.55.0/model/value_histogram.go000066400000000000000000000106251463701437000246400ustar00rootroot00000000000000// Copyright 2013 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package model import ( "encoding/json" "fmt" "strconv" "strings" ) type FloatString float64 func (v FloatString) String() string { return strconv.FormatFloat(float64(v), 'f', -1, 64) } func (v FloatString) MarshalJSON() ([]byte, error) { return json.Marshal(v.String()) } func (v *FloatString) UnmarshalJSON(b []byte) error { if len(b) < 2 || b[0] != '"' || b[len(b)-1] != '"' { return fmt.Errorf("float value must be a quoted string") } f, err := strconv.ParseFloat(string(b[1:len(b)-1]), 64) if err != nil { return err } *v = FloatString(f) return nil } type HistogramBucket struct { Boundaries int32 Lower FloatString Upper FloatString Count FloatString } func (s HistogramBucket) MarshalJSON() ([]byte, error) { b, err := json.Marshal(s.Boundaries) if err != nil { return nil, err } l, err := json.Marshal(s.Lower) if err != nil { return nil, err } u, err := json.Marshal(s.Upper) if err != nil { return nil, err } c, err := json.Marshal(s.Count) if err != nil { return nil, err } return []byte(fmt.Sprintf("[%s,%s,%s,%s]", b, l, u, c)), nil } func (s *HistogramBucket) UnmarshalJSON(buf []byte) error { tmp := []interface{}{&s.Boundaries, &s.Lower, &s.Upper, &s.Count} wantLen := len(tmp) if err := json.Unmarshal(buf, &tmp); err != nil { return err } if gotLen := len(tmp); gotLen != wantLen { return fmt.Errorf("wrong number of fields: %d != %d", gotLen, wantLen) } return nil } func (s *HistogramBucket) Equal(o *HistogramBucket) bool { return s == o || (s.Boundaries == o.Boundaries && s.Lower == o.Lower && s.Upper == o.Upper && s.Count == o.Count) } func (b HistogramBucket) String() string { var sb strings.Builder lowerInclusive := b.Boundaries == 1 || b.Boundaries == 3 upperInclusive := b.Boundaries == 0 || b.Boundaries == 3 if lowerInclusive { sb.WriteRune('[') } else { sb.WriteRune('(') } fmt.Fprintf(&sb, "%g,%g", b.Lower, b.Upper) if upperInclusive { sb.WriteRune(']') } else { sb.WriteRune(')') } fmt.Fprintf(&sb, ":%v", b.Count) return sb.String() } type HistogramBuckets []*HistogramBucket func (s HistogramBuckets) Equal(o HistogramBuckets) bool { if len(s) != len(o) { return false } for i, bucket := range s { if !bucket.Equal(o[i]) { return false } } return true } type SampleHistogram struct { Count FloatString `json:"count"` Sum FloatString `json:"sum"` Buckets HistogramBuckets `json:"buckets"` } func (s SampleHistogram) String() string { return fmt.Sprintf("Count: %f, Sum: %f, Buckets: %v", s.Count, s.Sum, s.Buckets) } func (s *SampleHistogram) Equal(o *SampleHistogram) bool { return s == o || (s.Count == o.Count && s.Sum == o.Sum && s.Buckets.Equal(o.Buckets)) } type SampleHistogramPair struct { Timestamp Time // Histogram should never be nil, it's only stored as pointer for efficiency. Histogram *SampleHistogram } func (s SampleHistogramPair) MarshalJSON() ([]byte, error) { if s.Histogram == nil { return nil, fmt.Errorf("histogram is nil") } t, err := json.Marshal(s.Timestamp) if err != nil { return nil, err } v, err := json.Marshal(s.Histogram) if err != nil { return nil, err } return []byte(fmt.Sprintf("[%s,%s]", t, v)), nil } func (s *SampleHistogramPair) UnmarshalJSON(buf []byte) error { tmp := []interface{}{&s.Timestamp, &s.Histogram} wantLen := len(tmp) if err := json.Unmarshal(buf, &tmp); err != nil { return err } if gotLen := len(tmp); gotLen != wantLen { return fmt.Errorf("wrong number of fields: %d != %d", gotLen, wantLen) } if s.Histogram == nil { return fmt.Errorf("histogram is null") } return nil } func (s SampleHistogramPair) String() string { return fmt.Sprintf("%s @[%s]", s.Histogram, s.Timestamp) } func (s *SampleHistogramPair) Equal(o *SampleHistogramPair) bool { return s == o || (s.Histogram.Equal(o.Histogram) && s.Timestamp.Equal(o.Timestamp)) } golang-github-prometheus-common-0.55.0/model/value_histogram_test.go000066400000000000000000000307001463701437000256730ustar00rootroot00000000000000// Copyright 2013 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package model import ( "encoding/json" "reflect" "regexp" "testing" ) var ( noWhitespace = regexp.MustCompile(`\s`) sampleHistogramPairMatrixPlain = `[ { "metric":{ "__name__":"test_metric" }, "histograms":[ [ 1234.567, { "count":"6", "sum":"3897", "buckets":[ [ 1, "-4870.992343051145", "-4466.7196729968955", "1" ], [ 1, "-861.0779292198035", "-789.6119426088657", "1" ], [ 1, "-558.3399591246119", "-512", "1" ], [ 0, "2048", "2233.3598364984477", "1" ], [ 0, "2896.3093757400984", "3158.4477704354626", "1" ], [ 0, "4466.7196729968955", "4870.992343051145", "1" ] ] } ], [ 12345.678, { "count":"6", "sum":"3897", "buckets":[ [ 1, "-4870.992343051145", "-4466.7196729968955", "1" ], [ 1, "-861.0779292198035", "-789.6119426088657", "1" ], [ 1, "-558.3399591246119", "-512", "1" ], [ 0, "2048", "2233.3598364984477", "1" ], [ 0, "2896.3093757400984", "3158.4477704354626", "1" ], [ 0, "4466.7196729968955", "4870.992343051145", "1" ] ] } ] ] }, { "metric":{ "foo":"bar" }, "histograms":[ [ 2234.567, { "count":"6", "sum":"3897", "buckets":[ [ 1, "-4870.992343051145", "-4466.7196729968955", "1" ], [ 1, "-861.0779292198035", "-789.6119426088657", "1" ], [ 1, "-558.3399591246119", "-512", "1" ], [ 0, "2048", "2233.3598364984477", "1" ], [ 0, "2896.3093757400984", "3158.4477704354626", "1" ], [ 0, "4466.7196729968955", "4870.992343051145", "1" ] ] } ], [ 22345.678, { "count":"6", "sum":"3897", "buckets":[ [ 1, "-4870.992343051145", "-4466.7196729968955", "1" ], [ 1, "-861.0779292198035", "-789.6119426088657", "1" ], [ 1, "-558.3399591246119", "-512", "1" ], [ 0, "2048", "2233.3598364984477", "1" ], [ 0, "2896.3093757400984", "3158.4477704354626", "1" ], [ 0, "4466.7196729968955", "4870.992343051145", "1" ] ] } ] ] } ]` sampleHistogramPairMatrixValue = Matrix{ &SampleStream{ Metric: Metric{ MetricNameLabel: "test_metric", }, Histograms: []SampleHistogramPair{ { Histogram: genSampleHistogram(), Timestamp: 1234567, }, { Histogram: genSampleHistogram(), Timestamp: 12345678, }, }, }, &SampleStream{ Metric: Metric{ "foo": "bar", }, Histograms: []SampleHistogramPair{ { Histogram: genSampleHistogram(), Timestamp: 2234567, }, { Histogram: genSampleHistogram(), Timestamp: 22345678, }, }, }, } ) func genSampleHistogram() *SampleHistogram { return &SampleHistogram{ Count: 6, Sum: 3897, Buckets: HistogramBuckets{ { Boundaries: 1, Lower: -4870.992343051145, Upper: -4466.7196729968955, Count: 1, }, { Boundaries: 1, Lower: -861.0779292198035, Upper: -789.6119426088657, Count: 1, }, { Boundaries: 1, Lower: -558.3399591246119, Upper: -512, Count: 1, }, { Boundaries: 0, Lower: 2048, Upper: 2233.3598364984477, Count: 1, }, { Boundaries: 0, Lower: 2896.3093757400984, Upper: 3158.4477704354626, Count: 1, }, { Boundaries: 0, Lower: 4466.7196729968955, Upper: 4870.992343051145, Count: 1, }, }, } } func TestSampleHistogramPairJSON(t *testing.T) { input := []struct { plain string value SampleHistogramPair }{ { plain: `[ 1234.567, { "count":"6", "sum":"3897", "buckets":[ [ 1, "-4870.992343051145", "-4466.7196729968955", "1" ], [ 1, "-861.0779292198035", "-789.6119426088657", "1" ], [ 1, "-558.3399591246119", "-512", "1" ], [ 0, "2048", "2233.3598364984477", "1" ], [ 0, "2896.3093757400984", "3158.4477704354626", "1" ], [ 0, "4466.7196729968955", "4870.992343051145", "1" ] ] } ]`, value: SampleHistogramPair{ Histogram: genSampleHistogram(), Timestamp: 1234567, }, }, } for _, test := range input { b, err := json.Marshal(test.value) if err != nil { t.Error(err) continue } trimmed := noWhitespace.ReplaceAllString(test.plain, "") if string(b) != trimmed { t.Errorf("encoding error: expected %q, got %q", trimmed, b) continue } var sp SampleHistogramPair err = json.Unmarshal(b, &sp) if err != nil { t.Error(err) continue } if !sp.Equal(&test.value) { t.Errorf("decoding error: expected %v, got %v", test.value, sp) } } } func TestInvalidSampleHistogramPairJSON(t *testing.T) { s1 := SampleHistogramPair{ Timestamp: 1, Histogram: nil, } d, err := json.Marshal(s1) if err == nil { t.Errorf("expected error when trying to marshal invalid SampleHistogramPair %s", string(d)) } var s2 SampleHistogramPair plain := "[0.001,null]" err = json.Unmarshal([]byte(plain), &s2) if err == nil { t.Errorf("expected error when trying to unmarshal invalid SampleHistogramPair %s", plain) } } func TestSampleHistogramJSON(t *testing.T) { input := []struct { plain string value Sample }{ { plain: `{ "metric":{ "__name__":"test_metric" }, "histogram":[ 1234.567, { "count":"6", "sum":"3897", "buckets":[ [ 1, "-4870.992343051145", "-4466.7196729968955", "1" ], [ 1, "-861.0779292198035", "-789.6119426088657", "1" ], [ 1, "-558.3399591246119", "-512", "1" ], [ 0, "2048", "2233.3598364984477", "1" ], [ 0, "2896.3093757400984", "3158.4477704354626", "1" ], [ 0, "4466.7196729968955", "4870.992343051145", "1" ] ] } ] }`, value: Sample{ Metric: Metric{ MetricNameLabel: "test_metric", }, Histogram: genSampleHistogram(), Timestamp: 1234567, }, }, } for _, test := range input { b, err := json.Marshal(test.value) if err != nil { t.Error(err) continue } trimmed := noWhitespace.ReplaceAllString(test.plain, "") if string(b) != trimmed { t.Errorf("encoding error: expected %q, got %q", trimmed, b) continue } var sv Sample err = json.Unmarshal(b, &sv) if err != nil { t.Error(err) continue } if !reflect.DeepEqual(sv, test.value) { t.Errorf("decoding error: expected %v, got %v", test.value, sv) } } } func TestVectorHistogramJSON(t *testing.T) { input := []struct { plain string value Vector }{ { plain: `[ { "metric":{ "__name__":"test_metric" }, "histogram":[ 1234.567, { "count":"6", "sum":"3897", "buckets":[ [ 1, "-4870.992343051145", "-4466.7196729968955", "1" ], [ 1, "-861.0779292198035", "-789.6119426088657", "1" ], [ 1, "-558.3399591246119", "-512", "1" ], [ 0, "2048", "2233.3598364984477", "1" ], [ 0, "2896.3093757400984", "3158.4477704354626", "1" ], [ 0, "4466.7196729968955", "4870.992343051145", "1" ] ] } ] } ]`, value: Vector{&Sample{ Metric: Metric{ MetricNameLabel: "test_metric", }, Histogram: genSampleHistogram(), Timestamp: 1234567, }}, }, { plain: `[ { "metric":{ "__name__":"test_metric" }, "histogram":[ 1234.567, { "count":"6", "sum":"3897", "buckets":[ [ 1, "-4870.992343051145", "-4466.7196729968955", "1" ], [ 1, "-861.0779292198035", "-789.6119426088657", "1" ], [ 1, "-558.3399591246119", "-512", "1" ], [ 0, "2048", "2233.3598364984477", "1" ], [ 0, "2896.3093757400984", "3158.4477704354626", "1" ], [ 0, "4466.7196729968955", "4870.992343051145", "1" ] ] } ] }, { "metric":{ "foo":"bar" }, "histogram":[ 1.234, { "count":"6", "sum":"3897", "buckets":[ [ 1, "-4870.992343051145", "-4466.7196729968955", "1" ], [ 1, "-861.0779292198035", "-789.6119426088657", "1" ], [ 1, "-558.3399591246119", "-512", "1" ], [ 0, "2048", "2233.3598364984477", "1" ], [ 0, "2896.3093757400984", "3158.4477704354626", "1" ], [ 0, "4466.7196729968955", "4870.992343051145", "1" ] ] } ] } ]`, value: Vector{ &Sample{ Metric: Metric{ MetricNameLabel: "test_metric", }, Histogram: genSampleHistogram(), Timestamp: 1234567, }, &Sample{ Metric: Metric{ "foo": "bar", }, Histogram: genSampleHistogram(), Timestamp: 1234, }, }, }, } for _, test := range input { b, err := json.Marshal(test.value) if err != nil { t.Error(err) continue } trimmed := noWhitespace.ReplaceAllString(test.plain, "") if string(b) != trimmed { t.Errorf("encoding error: expected %q, got %q", trimmed, b) continue } var vec Vector err = json.Unmarshal(b, &vec) if err != nil { t.Error(err) continue } if !reflect.DeepEqual(vec, test.value) { t.Errorf("decoding error: expected %v, got %v", test.value, vec) } } } func TestMatrixHistogramJSON(t *testing.T) { input := []struct { plain string value Matrix }{ { plain: `[]`, value: Matrix{}, }, { plain: sampleHistogramPairMatrixPlain, value: sampleHistogramPairMatrixValue, }, } for _, test := range input { b, err := json.Marshal(test.value) if err != nil { t.Error(err) continue } trimmed := noWhitespace.ReplaceAllString(test.plain, "") if string(b) != trimmed { t.Errorf("encoding error: expected %q, got %q", trimmed, b) continue } var mat Matrix err = json.Unmarshal(b, &mat) if err != nil { t.Error(err) continue } if !reflect.DeepEqual(mat, test.value) { t.Errorf("decoding error: expected %v, got %v", test.value, mat) } } } func BenchmarkJSONMarshallingSampleHistogramPairMatrix(b *testing.B) { for i := 0; i < b.N; i++ { _, err := json.Marshal(sampleHistogramPairMatrixValue) if err != nil { b.Fatal("error marshalling") } } } golang-github-prometheus-common-0.55.0/model/value_test.go000066400000000000000000000152151463701437000236220ustar00rootroot00000000000000// Copyright 2013 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package model import ( "encoding/json" "math" "sort" "testing" ) func TestEqualSamples(t *testing.T) { testSample := &Sample{} tests := map[string]struct { in1, in2 *Sample want bool }{ "equal pointers": { in1: testSample, in2: testSample, want: true, }, "different metrics": { in1: &Sample{Metric: Metric{"foo": "bar"}}, in2: &Sample{Metric: Metric{"foo": "biz"}}, want: false, }, "different timestamp": { in1: &Sample{Timestamp: 0}, in2: &Sample{Timestamp: 1}, want: false, }, "different value": { in1: &Sample{Value: 0}, in2: &Sample{Value: 1}, want: false, }, "equal samples": { in1: &Sample{ Metric: Metric{"foo": "bar"}, Timestamp: 0, Value: 1, }, in2: &Sample{ Metric: Metric{"foo": "bar"}, Timestamp: 0, Value: 1, }, want: true, }, "equal histograms": { in1: &Sample{ Metric: Metric{"foo": "bar"}, Timestamp: 0, Histogram: genSampleHistogram(), }, in2: &Sample{ Metric: Metric{"foo": "bar"}, Timestamp: 0, Histogram: genSampleHistogram(), }, want: true, }, "different histogram counts": { in1: &Sample{ Metric: Metric{"foo": "bar"}, Timestamp: 0, Histogram: &SampleHistogram{ Count: 2, Sum: 4500, Buckets: HistogramBuckets{ { Boundaries: 0, Lower: 4466.7196729968955, Upper: 4870.992343051145, Count: 1, }, }, }, }, in2: &Sample{ Metric: Metric{"foo": "bar"}, Timestamp: 0, Histogram: genSampleHistogram(), }, want: false, }, "different histogram sums": { in1: &Sample{ Metric: Metric{"foo": "bar"}, Timestamp: 0, Histogram: &SampleHistogram{ Count: 1, Sum: 4500.01, Buckets: HistogramBuckets{ { Boundaries: 0, Lower: 4466.7196729968955, Upper: 4870.992343051145, Count: 1, }, }, }, }, in2: &Sample{ Metric: Metric{"foo": "bar"}, Timestamp: 0, Histogram: genSampleHistogram(), }, want: false, }, "different histogram inner counts": { in1: &Sample{ Metric: Metric{"foo": "bar"}, Timestamp: 0, Histogram: &SampleHistogram{ Count: 1, Sum: 4500, Buckets: HistogramBuckets{ { Boundaries: 0, Lower: 4466.7196729968955, Upper: 4870.992343051145, Count: 2, }, }, }, }, in2: &Sample{ Metric: Metric{"foo": "bar"}, Timestamp: 0, Histogram: genSampleHistogram(), }, want: false, }, } for name, test := range tests { got := test.in1.Equal(test.in2) if got != test.want { t.Errorf("Comparing %s, %v and %v: got %t, want %t", name, test.in1, test.in2, got, test.want) } } } func TestScalarJSON(t *testing.T) { input := []struct { plain string value Scalar }{ { plain: `[123.456,"456"]`, value: Scalar{ Timestamp: 123456, Value: 456, }, }, { plain: `[123123.456,"+Inf"]`, value: Scalar{ Timestamp: 123123456, Value: SampleValue(math.Inf(1)), }, }, { plain: `[123123.456,"-Inf"]`, value: Scalar{ Timestamp: 123123456, Value: SampleValue(math.Inf(-1)), }, }, } for _, test := range input { b, err := json.Marshal(test.value) if err != nil { t.Error(err) continue } if string(b) != test.plain { t.Errorf("encoding error: expected %q, got %q", test.plain, b) continue } var sv Scalar err = json.Unmarshal(b, &sv) if err != nil { t.Error(err) continue } if sv != test.value { t.Errorf("decoding error: expected %v, got %v", test.value, sv) } } } func TestStringJSON(t *testing.T) { input := []struct { plain string value String }{ { plain: `[123.456,"test"]`, value: String{ Timestamp: 123456, Value: "test", }, }, { plain: `[123123.456,"台北"]`, value: String{ Timestamp: 123123456, Value: "台北", }, }, } for _, test := range input { b, err := json.Marshal(test.value) if err != nil { t.Error(err) continue } if string(b) != test.plain { t.Errorf("encoding error: expected %q, got %q", test.plain, b) continue } var sv String err = json.Unmarshal(b, &sv) if err != nil { t.Error(err) continue } if sv != test.value { t.Errorf("decoding error: expected %v, got %v", test.value, sv) } } } func TestVectorSort(t *testing.T) { input := Vector{ &Sample{ Metric: Metric{ MetricNameLabel: "A", }, Timestamp: 1, }, &Sample{ Metric: Metric{ MetricNameLabel: "A", }, Timestamp: 2, }, &Sample{ Metric: Metric{ MetricNameLabel: "C", }, Timestamp: 1, }, &Sample{ Metric: Metric{ MetricNameLabel: "C", }, Timestamp: 2, }, &Sample{ Metric: Metric{ MetricNameLabel: "B", }, Timestamp: 3, }, &Sample{ Metric: Metric{ MetricNameLabel: "B", }, Timestamp: 2, }, &Sample{ Metric: Metric{ MetricNameLabel: "B", }, Timestamp: 1, }, } expected := Vector{ &Sample{ Metric: Metric{ MetricNameLabel: "A", }, Timestamp: 1, }, &Sample{ Metric: Metric{ MetricNameLabel: "A", }, Timestamp: 2, }, &Sample{ Metric: Metric{ MetricNameLabel: "B", }, Timestamp: 1, }, &Sample{ Metric: Metric{ MetricNameLabel: "B", }, Timestamp: 2, }, &Sample{ Metric: Metric{ MetricNameLabel: "B", }, Timestamp: 3, }, &Sample{ Metric: Metric{ MetricNameLabel: "C", }, Timestamp: 1, }, &Sample{ Metric: Metric{ MetricNameLabel: "C", }, Timestamp: 2, }, } sort.Sort(input) for i, actual := range input { actualFp := actual.Metric.Fingerprint() expectedFp := expected[i].Metric.Fingerprint() if actualFp != expectedFp { t.Fatalf("%d. Incorrect fingerprint. Got %s; want %s", i, actualFp.String(), expectedFp.String()) } if actual.Timestamp != expected[i].Timestamp { t.Fatalf("%d. Incorrect timestamp. Got %s; want %s", i, actual.Timestamp, expected[i].Timestamp) } } } golang-github-prometheus-common-0.55.0/model/value_type.go000066400000000000000000000035501463701437000236230ustar00rootroot00000000000000// Copyright 2013 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package model import ( "encoding/json" "fmt" ) // Value is a generic interface for values resulting from a query evaluation. type Value interface { Type() ValueType String() string } func (Matrix) Type() ValueType { return ValMatrix } func (Vector) Type() ValueType { return ValVector } func (*Scalar) Type() ValueType { return ValScalar } func (*String) Type() ValueType { return ValString } type ValueType int const ( ValNone ValueType = iota ValScalar ValVector ValMatrix ValString ) // MarshalJSON implements json.Marshaler. func (et ValueType) MarshalJSON() ([]byte, error) { return json.Marshal(et.String()) } func (et *ValueType) UnmarshalJSON(b []byte) error { var s string if err := json.Unmarshal(b, &s); err != nil { return err } switch s { case "": *et = ValNone case "scalar": *et = ValScalar case "vector": *et = ValVector case "matrix": *et = ValMatrix case "string": *et = ValString default: return fmt.Errorf("unknown value type %q", s) } return nil } func (e ValueType) String() string { switch e { case ValNone: return "" case ValScalar: return "scalar" case ValVector: return "vector" case ValMatrix: return "matrix" case ValString: return "string" } panic("ValueType.String: unhandled value type") } golang-github-prometheus-common-0.55.0/promlog/000077500000000000000000000000001463701437000214735ustar00rootroot00000000000000golang-github-prometheus-common-0.55.0/promlog/flag/000077500000000000000000000000001463701437000224045ustar00rootroot00000000000000golang-github-prometheus-common-0.55.0/promlog/flag/flag.go000066400000000000000000000036341463701437000236520ustar00rootroot00000000000000// Copyright 2017 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package flag import ( "strings" kingpin "github.com/alecthomas/kingpin/v2" "github.com/prometheus/common/promlog" ) // LevelFlagName is the canonical flag name to configure the allowed log level // within Prometheus projects. const LevelFlagName = "log.level" // LevelFlagHelp is the help description for the log.level flag. var LevelFlagHelp = "Only log messages with the given severity or above. One of: [" + strings.Join(promlog.LevelFlagOptions, ", ") + "]" // FormatFlagName is the canonical flag name to configure the log format // within Prometheus projects. const FormatFlagName = "log.format" // FormatFlagHelp is the help description for the log.format flag. var FormatFlagHelp = "Output format of log messages. One of: [" + strings.Join(promlog.FormatFlagOptions, ", ") + "]" // AddFlags adds the flags used by this package to the Kingpin application. // To use the default Kingpin application, call AddFlags(kingpin.CommandLine) func AddFlags(a *kingpin.Application, config *promlog.Config) { config.Level = &promlog.AllowedLevel{} a.Flag(LevelFlagName, LevelFlagHelp). Default("info").HintOptions(promlog.LevelFlagOptions...). SetValue(config.Level) config.Format = &promlog.AllowedFormat{} a.Flag(FormatFlagName, FormatFlagHelp). Default("logfmt").HintOptions(promlog.FormatFlagOptions...). SetValue(config.Format) } golang-github-prometheus-common-0.55.0/promlog/log.go000066400000000000000000000121131463701437000226010ustar00rootroot00000000000000// Copyright 2017 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package promlog defines standardised ways to initialize Go kit loggers // across Prometheus components. // It should typically only ever be imported by main packages. package promlog import ( "fmt" "os" "sync" "time" "github.com/go-kit/log" "github.com/go-kit/log/level" ) var ( // This timestamp format differs from RFC3339Nano by using .000 instead // of .999999999 which changes the timestamp from 9 variable to 3 fixed // decimals (.130 instead of .130987456). timestampFormat = log.TimestampFormat( func() time.Time { return time.Now().UTC() }, "2006-01-02T15:04:05.000Z07:00", ) LevelFlagOptions = []string{"debug", "info", "warn", "error"} FormatFlagOptions = []string{"logfmt", "json"} ) // AllowedLevel is a settable identifier for the minimum level a log entry // must be have. type AllowedLevel struct { s string o level.Option } func (l *AllowedLevel) UnmarshalYAML(unmarshal func(interface{}) error) error { var s string type plain string if err := unmarshal((*plain)(&s)); err != nil { return err } if s == "" { return nil } lo := &AllowedLevel{} if err := lo.Set(s); err != nil { return err } *l = *lo return nil } func (l *AllowedLevel) String() string { return l.s } // Set updates the value of the allowed level. func (l *AllowedLevel) Set(s string) error { switch s { case "debug": l.o = level.AllowDebug() case "info": l.o = level.AllowInfo() case "warn": l.o = level.AllowWarn() case "error": l.o = level.AllowError() default: return fmt.Errorf("unrecognized log level %q", s) } l.s = s return nil } // AllowedFormat is a settable identifier for the output format that the logger can have. type AllowedFormat struct { s string } func (f *AllowedFormat) String() string { return f.s } // Set updates the value of the allowed format. func (f *AllowedFormat) Set(s string) error { switch s { case "logfmt", "json": f.s = s default: return fmt.Errorf("unrecognized log format %q", s) } return nil } // Config is a struct containing configurable settings for the logger type Config struct { Level *AllowedLevel Format *AllowedFormat } // New returns a new leveled oklog logger. Each logged line will be annotated // with a timestamp. The output always goes to stderr. func New(config *Config) log.Logger { if config.Format != nil && config.Format.s == "json" { return NewWithLogger(log.NewJSONLogger(log.NewSyncWriter(os.Stderr)), config) } return NewWithLogger(log.NewLogfmtLogger(log.NewSyncWriter(os.Stderr)), config) } // NewWithLogger returns a new leveled oklog logger with a custom log.Logger. // Each logged line will be annotated with a timestamp. func NewWithLogger(l log.Logger, config *Config) log.Logger { if config.Level != nil { l = log.With(l, "ts", timestampFormat, "caller", log.Caller(5)) l = level.NewFilter(l, config.Level.o) } else { l = log.With(l, "ts", timestampFormat, "caller", log.DefaultCaller) } return l } // NewDynamic returns a new leveled logger. Each logged line will be annotated // with a timestamp. The output always goes to stderr. Some properties can be // changed, like the level. func NewDynamic(config *Config) *logger { if config.Format != nil && config.Format.s == "json" { return NewDynamicWithLogger(log.NewJSONLogger(log.NewSyncWriter(os.Stderr)), config) } return NewDynamicWithLogger(log.NewLogfmtLogger(log.NewSyncWriter(os.Stderr)), config) } // NewDynamicWithLogger returns a new leveled logger with a custom io.Writer. // Each logged line will be annotated with a timestamp. // Some properties can be changed, like the level. func NewDynamicWithLogger(l log.Logger, config *Config) *logger { lo := &logger{ base: l, leveled: l, } if config.Level != nil { lo.SetLevel(config.Level) } return lo } type logger struct { base log.Logger leveled log.Logger currentLevel *AllowedLevel mtx sync.Mutex } // Log implements logger.Log. func (l *logger) Log(keyvals ...interface{}) error { l.mtx.Lock() defer l.mtx.Unlock() return l.leveled.Log(keyvals...) } // SetLevel changes the log level. func (l *logger) SetLevel(lvl *AllowedLevel) { l.mtx.Lock() defer l.mtx.Unlock() if lvl == nil { l.leveled = log.With(l.base, "ts", timestampFormat, "caller", log.DefaultCaller) l.currentLevel = nil return } if l.currentLevel != nil && l.currentLevel.s != lvl.s { _ = l.base.Log("msg", "Log level changed", "prev", l.currentLevel, "current", lvl) } l.currentLevel = lvl l.leveled = level.NewFilter(log.With(l.base, "ts", timestampFormat, "caller", log.Caller(5)), lvl.o) } golang-github-prometheus-common-0.55.0/promlog/log_test.go000066400000000000000000000055541463701437000236530ustar00rootroot00000000000000// Copyright 2020 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package promlog import ( "fmt" "testing" "github.com/go-kit/log/level" "gopkg.in/yaml.v2" ) // Make sure creating and using a logger with an empty configuration doesn't // result in a panic. func TestDefaultConfig(t *testing.T) { logger := New(&Config{}) if err := logger.Log("hello", "world"); err != nil { t.Fatal(err) } } func TestUnmarshallLevel(t *testing.T) { l := &AllowedLevel{} err := yaml.Unmarshal([]byte(`debug`), l) if err != nil { t.Error(err) } if l.s != "debug" { t.Errorf("expected %s, got %s", "debug", l.s) } } func TestUnmarshallEmptyLevel(t *testing.T) { l := &AllowedLevel{} err := yaml.Unmarshal([]byte(``), l) if err != nil { t.Error(err) } if l.s != "" { t.Errorf("expected empty level, got %s", l.s) } } func TestUnmarshallBadLevel(t *testing.T) { l := &AllowedLevel{} err := yaml.Unmarshal([]byte(`debugg`), l) if err == nil { t.Error("expected error") } expErr := `unrecognized log level "debugg"` if err.Error() != expErr { t.Errorf("expected error %s, got %s", expErr, err.Error()) } if l.s != "" { t.Errorf("expected empty level, got %s", l.s) } } type recordKeyvalLogger struct { count int } func (r *recordKeyvalLogger) Log(keyvals ...interface{}) error { for _, v := range keyvals { if fmt.Sprintf("%v", v) == "Log level changed" { return nil } } r.count++ return nil } func TestDynamic(t *testing.T) { logger := NewDynamic(&Config{}) debugLevel := &AllowedLevel{} if err := debugLevel.Set("debug"); err != nil { t.Fatal(err) } infoLevel := &AllowedLevel{} if err := infoLevel.Set("info"); err != nil { t.Fatal(err) } recorder := &recordKeyvalLogger{} logger.base = recorder logger.SetLevel(debugLevel) if err := level.Debug(logger).Log("hello", "world"); err != nil { t.Fatal(err) } if recorder.count != 1 { t.Fatal("log not found") } recorder.count = 0 logger.SetLevel(infoLevel) if err := level.Debug(logger).Log("hello", "world"); err != nil { t.Fatal(err) } if recorder.count != 0 { t.Fatal("log found") } if err := level.Info(logger).Log("hello", "world"); err != nil { t.Fatal(err) } if recorder.count != 1 { t.Fatal("log not found") } if err := level.Debug(logger).Log("hello", "world"); err != nil { t.Fatal(err) } if recorder.count != 1 { t.Fatal("extra log found") } } golang-github-prometheus-common-0.55.0/route/000077500000000000000000000000001463701437000211525ustar00rootroot00000000000000golang-github-prometheus-common-0.55.0/route/route.go000066400000000000000000000104701463701437000226410ustar00rootroot00000000000000// Copyright 2015 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package route import ( "context" "net/http" "github.com/julienschmidt/httprouter" ) type param string // Param returns param p for the context, or the empty string when // param does not exist in context. func Param(ctx context.Context, p string) string { if v := ctx.Value(param(p)); v != nil { return v.(string) } return "" } // WithParam returns a new context with param p set to v. func WithParam(ctx context.Context, p, v string) context.Context { return context.WithValue(ctx, param(p), v) } // Router wraps httprouter.Router and adds support for prefixed sub-routers, // per-request context injections and instrumentation. type Router struct { rtr *httprouter.Router prefix string instrh func(handlerName string, handler http.HandlerFunc) http.HandlerFunc } // New returns a new Router. func New() *Router { return &Router{ rtr: httprouter.New(), } } // WithInstrumentation returns a router with instrumentation support. func (r *Router) WithInstrumentation(instrh func(handlerName string, handler http.HandlerFunc) http.HandlerFunc) *Router { if r.instrh != nil { newInstrh := instrh instrh = func(handlerName string, handler http.HandlerFunc) http.HandlerFunc { return newInstrh(handlerName, r.instrh(handlerName, handler)) } } return &Router{rtr: r.rtr, prefix: r.prefix, instrh: instrh} } // WithPrefix returns a router that prefixes all registered routes with prefix. func (r *Router) WithPrefix(prefix string) *Router { return &Router{rtr: r.rtr, prefix: r.prefix + prefix, instrh: r.instrh} } // handle turns a HandlerFunc into an httprouter.Handle. func (r *Router) handle(handlerName string, h http.HandlerFunc) httprouter.Handle { if r.instrh != nil { // This needs to be outside the closure to avoid data race when reading and writing to 'h'. h = r.instrh(handlerName, h) } return func(w http.ResponseWriter, req *http.Request, params httprouter.Params) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() for _, p := range params { ctx = context.WithValue(ctx, param(p.Key), p.Value) } h(w, req.WithContext(ctx)) } } // Get registers a new GET route. func (r *Router) Get(path string, h http.HandlerFunc) { r.rtr.GET(r.prefix+path, r.handle(path, h)) } // Options registers a new OPTIONS route. func (r *Router) Options(path string, h http.HandlerFunc) { r.rtr.OPTIONS(r.prefix+path, r.handle(path, h)) } // Del registers a new DELETE route. func (r *Router) Del(path string, h http.HandlerFunc) { r.rtr.DELETE(r.prefix+path, r.handle(path, h)) } // Put registers a new PUT route. func (r *Router) Put(path string, h http.HandlerFunc) { r.rtr.PUT(r.prefix+path, r.handle(path, h)) } // Post registers a new POST route. func (r *Router) Post(path string, h http.HandlerFunc) { r.rtr.POST(r.prefix+path, r.handle(path, h)) } // Head registers a new HEAD route. func (r *Router) Head(path string, h http.HandlerFunc) { r.rtr.HEAD(r.prefix+path, r.handle(path, h)) } // Redirect takes an absolute path and sends an internal HTTP redirect for it, // prefixed by the router's path prefix. Note that this method does not include // functionality for handling relative paths or full URL redirects. func (r *Router) Redirect(w http.ResponseWriter, req *http.Request, path string, code int) { http.Redirect(w, req, r.prefix+path, code) } // ServeHTTP implements http.Handler. func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { r.rtr.ServeHTTP(w, req) } // FileServe returns a new http.HandlerFunc that serves files from dir. // Using routes must provide the *filepath parameter. func FileServe(dir string) http.HandlerFunc { fs := http.FileServer(http.Dir(dir)) return func(w http.ResponseWriter, r *http.Request) { r.URL.Path = Param(r.Context(), "filepath") fs.ServeHTTP(w, r) } } golang-github-prometheus-common-0.55.0/route/route_test.go000066400000000000000000000116511463701437000237020ustar00rootroot00000000000000// Copyright 2015 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package route import ( "net/http" "net/http/httptest" "testing" ) func TestRedirect(t *testing.T) { router := New().WithPrefix("/test/prefix") w := httptest.NewRecorder() r, err := http.NewRequest("GET", "http://localhost:9090/foo", nil) if err != nil { t.Fatalf("Error building test request: %s", err) } router.Redirect(w, r, "/some/endpoint", http.StatusFound) if w.Code != http.StatusFound { t.Fatalf("Unexpected redirect status code: got %d, want %d", w.Code, http.StatusFound) } want := "/test/prefix/some/endpoint" got := w.Header()["Location"][0] if want != got { t.Fatalf("Unexpected redirect location: got %s, want %s", got, want) } } func TestContext(t *testing.T) { router := New() router.Get("/test/:foo/", func(w http.ResponseWriter, r *http.Request) { want := "bar" got := Param(r.Context(), "foo") if want != got { t.Fatalf("Unexpected context value: want %q, got %q", want, got) } }) r, err := http.NewRequest("GET", "http://localhost:9090/test/bar/", nil) if err != nil { t.Fatalf("Error building test request: %s", err) } router.ServeHTTP(nil, r) } func TestContextWithValue(t *testing.T) { router := New() router.Get("/test/:foo/", func(w http.ResponseWriter, r *http.Request) { want := "bar" got := Param(r.Context(), "foo") if want != got { t.Fatalf("Unexpected context value: want %q, got %q", want, got) } want = "ipsum" got = Param(r.Context(), "lorem") if want != got { t.Fatalf("Unexpected context value: want %q, got %q", want, got) } want = "sit" got = Param(r.Context(), "dolor") if want != got { t.Fatalf("Unexpected context value: want %q, got %q", want, got) } }) r, err := http.NewRequest("GET", "http://localhost:9090/test/bar/", nil) if err != nil { t.Fatalf("Error building test request: %s", err) } params := map[string]string{ "lorem": "ipsum", "dolor": "sit", } ctx := r.Context() for p, v := range params { ctx = WithParam(ctx, p, v) } r = r.WithContext(ctx) router.ServeHTTP(nil, r) } func TestContextWithoutValue(t *testing.T) { router := New() router.Get("/test", func(w http.ResponseWriter, r *http.Request) { want := "" got := Param(r.Context(), "foo") if want != got { t.Fatalf("Unexpected context value: want %q, got %q", want, got) } }) r, err := http.NewRequest("GET", "http://localhost:9090/test", nil) if err != nil { t.Fatalf("Error building test request: %s", err) } router.ServeHTTP(nil, r) } func TestInstrumentation(t *testing.T) { var got string cases := []struct { router *Router want string }{ { router: New(), want: "", }, { router: New().WithInstrumentation(func(handlerName string, handler http.HandlerFunc) http.HandlerFunc { got = handlerName return handler }), want: "/foo", }, } for _, c := range cases { c.router.Get("/foo", func(w http.ResponseWriter, r *http.Request) {}) r, err := http.NewRequest("GET", "http://localhost:9090/foo", nil) if err != nil { t.Fatalf("Error building test request: %s", err) } c.router.ServeHTTP(nil, r) if c.want != got { t.Fatalf("Unexpected value: want %q, got %q", c.want, got) } } } func TestInstrumentations(t *testing.T) { got := make([]string, 0) cases := []struct { router *Router want []string }{ { router: New(), want: []string{}, }, { router: New(). WithInstrumentation( func(handlerName string, handler http.HandlerFunc) http.HandlerFunc { got = append(got, "1"+handlerName) return handler }). WithInstrumentation( func(handlerName string, handler http.HandlerFunc) http.HandlerFunc { got = append(got, "2"+handlerName) return handler }). WithInstrumentation( func(handlerName string, handler http.HandlerFunc) http.HandlerFunc { got = append(got, "3"+handlerName) return handler }), want: []string{"1/foo", "2/foo", "3/foo"}, }, } for _, c := range cases { c.router.Get("/foo", func(w http.ResponseWriter, r *http.Request) {}) r, err := http.NewRequest("GET", "http://localhost:9090/foo", nil) if err != nil { t.Fatalf("Error building test request: %s", err) } c.router.ServeHTTP(nil, r) if len(c.want) != len(got) { t.Fatalf("Unexpected value: want %q, got %q", c.want, got) } for i, v := range c.want { if v != got[i] { t.Fatalf("Unexpected value: want %q, got %q", c.want, got) } } } } golang-github-prometheus-common-0.55.0/scripts/000077500000000000000000000000001463701437000215035ustar00rootroot00000000000000golang-github-prometheus-common-0.55.0/scripts/check-go-mod-version.sh000077500000000000000000000004471463701437000257670ustar00rootroot00000000000000#!/usr/bin/env bash readarray -t mod_files < <(find . -type f -name go.mod) echo "Checking files ${mod_files[@]}" matches=$(awk '$1 == "go" {print $2}' "${mod_files[@]}" | sort -u | wc -l) if [[ "${matches}" -ne 1 ]]; then echo 'Not all go.mod files have matching go versions' exit 1 fi golang-github-prometheus-common-0.55.0/server/000077500000000000000000000000001463701437000213225ustar00rootroot00000000000000golang-github-prometheus-common-0.55.0/server/static_file_server.go000066400000000000000000000026541463701437000255340ustar00rootroot00000000000000// Copyright 2019 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package server import ( "net/http" "path/filepath" ) var mimeTypes = map[string]string{ ".cjs": "application/javascript", ".css": "text/css", ".eot": "font/eot", ".gif": "image/gif", ".ico": "image/x-icon", ".jpg": "image/jpeg", ".js": "application/javascript", ".json": "application/json", ".less": "text/plain", ".map": "application/json", ".otf": "font/otf", ".png": "image/png", ".svg": "image/svg+xml", ".ttf": "font/ttf", ".txt": "text/plain", ".woff": "font/woff", ".woff2": "font/woff2", } func StaticFileServer(root http.FileSystem) http.Handler { return http.HandlerFunc( func(w http.ResponseWriter, r *http.Request) { fileExt := filepath.Ext(r.URL.Path) if t, ok := mimeTypes[fileExt]; ok { w.Header().Set("Content-Type", t) } http.FileServer(root).ServeHTTP(w, r) }, ) } golang-github-prometheus-common-0.55.0/server/static_file_server_test.go000066400000000000000000000035601463701437000265700ustar00rootroot00000000000000// Copyright 2019 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package server import ( "net/http" "net/http/httptest" "testing" ) type dummyFileSystem struct{} func (fs dummyFileSystem) Open(path string) (http.File, error) { return http.Dir(".").Open(".") } func TestServeHttp(t *testing.T) { cases := []struct { name string path string contentType string }{ { name: "normal file", path: "index.html", contentType: "", }, { name: "javascript", path: "test.js", contentType: "application/javascript", }, { name: "css", path: "test.css", contentType: "text/css", }, { name: "png", path: "test.png", contentType: "image/png", }, { name: "jpg", path: "test.jpg", contentType: "image/jpeg", }, { name: "gif", path: "test.gif", contentType: "image/gif", }, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { rr := httptest.NewRecorder() req, err := http.NewRequest("GET", "http://localhost/"+c.path, nil) if err != nil { t.Fatal(err) } s := StaticFileServer(dummyFileSystem{}) s.ServeHTTP(rr, req) if rr.Header().Get("Content-Type") != c.contentType { t.Fatalf("Unexpected Content-Type: %s", rr.Header().Get("Content-Type")) } }) } } golang-github-prometheus-common-0.55.0/sigv4/000077500000000000000000000000001463701437000210505ustar00rootroot00000000000000golang-github-prometheus-common-0.55.0/sigv4/.yamllint000066400000000000000000000011201463701437000226740ustar00rootroot00000000000000--- extends: default rules: braces: max-spaces-inside: 1 level: error brackets: max-spaces-inside: 1 level: error commas: disable comments: disable comments-indentation: disable document-start: disable indentation: spaces: consistent key-duplicates: ignore: | config/testdata/section_key_dup.bad.yml line-length: disable truthy: ignore: | .github/workflows/codeql-analysis.yml .github/workflows/funcbench.yml .github/workflows/fuzzing.yml .github/workflows/prombench.yml .github/workflows/golangci-lint.yml golang-github-prometheus-common-0.55.0/sigv4/Makefile000066400000000000000000000012401463701437000225050ustar00rootroot00000000000000# Copyright 2018 The Prometheus Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. include ../Makefile.common .PHONY: test test:: deps check_license unused common-test lint golang-github-prometheus-common-0.55.0/sigv4/README.md000066400000000000000000000007611463701437000223330ustar00rootroot00000000000000github.com/prometheus/common/sigv4 module ========================================= sigv4 provides a http.RoundTripper that will sign requests using Amazon's Signature Verification V4 signing procedure, using credentials from the default AWS credential chain. This is a separate module from github.com/prometheus/common to prevent it from having and propagating a dependency on the AWS SDK. This module is considered internal to Prometheus, without any stability guarantees for external usage. golang-github-prometheus-common-0.55.0/sigv4/go.mod000066400000000000000000000021551463701437000221610ustar00rootroot00000000000000module github.com/prometheus/common/sigv4 go 1.20 replace github.com/prometheus/common => ../ require ( github.com/aws/aws-sdk-go v1.54.7 github.com/prometheus/client_golang v1.19.1 github.com/prometheus/common v0.53.0 github.com/stretchr/testify v1.9.0 gopkg.in/yaml.v2 v2.4.0 ) require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/kr/text v0.2.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/procfs v0.15.1 // indirect golang.org/x/net v0.26.0 // indirect golang.org/x/oauth2 v0.21.0 // indirect golang.org/x/sys v0.21.0 // indirect golang.org/x/text v0.16.0 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) golang-github-prometheus-common-0.55.0/sigv4/go.sum000066400000000000000000000110721463701437000222040ustar00rootroot00000000000000github.com/aws/aws-sdk-go v1.54.7 h1:k1wJ+NMOsXgq/Lsa0y1mS0DFoDeHFPcz2OjCq5H5Mjg= github.com/aws/aws-sdk-go v1.54.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= golang-github-prometheus-common-0.55.0/sigv4/sigv4.go000066400000000000000000000106611463701437000224370ustar00rootroot00000000000000// Copyright 2021 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package sigv4 import ( "bytes" "fmt" "io" "net/http" "net/textproto" "path" "sync" "time" "github.com/aws/aws-sdk-go/aws/endpoints" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/credentials/stscreds" "github.com/aws/aws-sdk-go/aws/session" signer "github.com/aws/aws-sdk-go/aws/signer/v4" ) var sigv4HeaderDenylist = []string{ "uber-trace-id", } type sigV4RoundTripper struct { region string next http.RoundTripper pool sync.Pool signer *signer.Signer } // NewSigV4RoundTripper returns a new http.RoundTripper that will sign requests // using Amazon's Signature Verification V4 signing procedure. The request will // then be handed off to the next RoundTripper provided by next. If next is nil, // http.DefaultTransport will be used. // // Credentials for signing are retrieved using the the default AWS credential // chain. If credentials cannot be found, an error will be returned. func NewSigV4RoundTripper(cfg *SigV4Config, next http.RoundTripper) (http.RoundTripper, error) { if next == nil { next = http.DefaultTransport } creds := credentials.NewStaticCredentials(cfg.AccessKey, string(cfg.SecretKey), "") if cfg.AccessKey == "" && cfg.SecretKey == "" { creds = nil } useFIPSSTSEndpoint := endpoints.FIPSEndpointStateDisabled if cfg.UseFIPSSTSEndpoint { useFIPSSTSEndpoint = endpoints.FIPSEndpointStateEnabled } sess, err := session.NewSessionWithOptions(session.Options{ Config: aws.Config{ Region: aws.String(cfg.Region), Credentials: creds, UseFIPSEndpoint: useFIPSSTSEndpoint, }, Profile: cfg.Profile, }) if err != nil { return nil, fmt.Errorf("could not create new AWS session: %w", err) } if _, err := sess.Config.Credentials.Get(); err != nil { return nil, fmt.Errorf("could not get SigV4 credentials: %w", err) } if aws.StringValue(sess.Config.Region) == "" { return nil, fmt.Errorf("region not configured in sigv4 or in default credentials chain") } signerCreds := sess.Config.Credentials if cfg.RoleARN != "" { signerCreds = stscreds.NewCredentials(sess, cfg.RoleARN) } rt := &sigV4RoundTripper{ region: cfg.Region, next: next, signer: signer.NewSigner(signerCreds), } rt.pool.New = rt.newBuf return rt, nil } func (rt *sigV4RoundTripper) newBuf() interface{} { return bytes.NewBuffer(make([]byte, 0, 1024)) } func (rt *sigV4RoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { // rt.signer.Sign needs a seekable body, so we replace the body with a // buffered reader filled with the contents of original body. buf := rt.pool.Get().(*bytes.Buffer) defer func() { buf.Reset() rt.pool.Put(buf) }() if _, err := io.Copy(buf, req.Body); err != nil { return nil, err } // Close the original body since we don't need it anymore. _ = req.Body.Close() // Ensure our seeker is back at the start of the buffer once we return. var seeker io.ReadSeeker = bytes.NewReader(buf.Bytes()) defer func() { _, _ = seeker.Seek(0, io.SeekStart) }() req.Body = io.NopCloser(seeker) // Clean path like documented in AWS documentation. // https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html req.URL.Path = path.Clean(req.URL.Path) // Clone the request and trim out headers that we don't want to sign. signReq := req.Clone(req.Context()) for _, header := range sigv4HeaderDenylist { signReq.Header.Del(header) } headers, err := rt.signer.Sign(signReq, seeker, "aps", rt.region, time.Now().UTC()) if err != nil { return nil, fmt.Errorf("failed to sign request: %w", err) } // Copy over signed headers. Authorization header is not returned by // rt.signer.Sign and needs to be copied separately. for k, v := range headers { req.Header[textproto.CanonicalMIMEHeaderKey(k)] = v } req.Header.Set("Authorization", signReq.Header.Get("Authorization")) return rt.next.RoundTrip(req) } golang-github-prometheus-common-0.55.0/sigv4/sigv4_config.go000066400000000000000000000032561463701437000237660ustar00rootroot00000000000000// Copyright 2021 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package sigv4 import ( "fmt" "github.com/prometheus/common/config" ) // SigV4Config is the configuration for signing remote write requests with // AWS's SigV4 verification process. Empty values will be retrieved using the // AWS default credentials chain. type SigV4Config struct { Region string `yaml:"region,omitempty"` AccessKey string `yaml:"access_key,omitempty"` SecretKey config.Secret `yaml:"secret_key,omitempty"` Profile string `yaml:"profile,omitempty"` RoleARN string `yaml:"role_arn,omitempty"` UseFIPSSTSEndpoint bool `yaml:"use_fips_sts_endpoint,omitempty"` } func (c *SigV4Config) Validate() error { if (c.AccessKey == "") != (c.SecretKey == "") { return fmt.Errorf("must provide a AWS SigV4 Access key and Secret Key if credentials are specified in the SigV4 config") } return nil } func (c *SigV4Config) UnmarshalYAML(unmarshal func(interface{}) error) error { type plain SigV4Config *c = SigV4Config{} if err := unmarshal((*plain)(c)); err != nil { return err } return c.Validate() } golang-github-prometheus-common-0.55.0/sigv4/sigv4_config_test.go000066400000000000000000000032441463701437000250220ustar00rootroot00000000000000// Copyright 2021 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package sigv4 import ( "os" "strings" "testing" "gopkg.in/yaml.v2" ) func loadSigv4Config(filename string) (*SigV4Config, error) { content, err := os.ReadFile(filename) if err != nil { return nil, err } cfg := SigV4Config{} if err = yaml.UnmarshalStrict(content, &cfg); err != nil { return nil, err } return &cfg, nil } func testGoodConfig(t *testing.T, filename string) { _, err := loadSigv4Config(filename) if err != nil { t.Fatalf("Unexpected error parsing %s: %s", filename, err) } } func TestGoodSigV4Configs(t *testing.T) { filesToTest := []string{"testdata/sigv4_good.yaml", "testdata/sigv4_good.yaml"} for _, filename := range filesToTest { testGoodConfig(t, filename) } } func TestBadSigV4Config(t *testing.T) { filename := "testdata/sigv4_bad.yaml" _, err := loadSigv4Config(filename) if err == nil { t.Fatalf("Did not receive expected error unmarshaling bad sigv4 config") } if !strings.Contains(err.Error(), "must provide a AWS SigV4 Access key and Secret Key") { t.Errorf("Received unexpected error from unmarshal of %s: %s", filename, err.Error()) } } golang-github-prometheus-common-0.55.0/sigv4/sigv4_test.go000066400000000000000000000061771463701437000235050ustar00rootroot00000000000000// Copyright 2021 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package sigv4 import ( "net/http" "os" "strings" "testing" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/session" signer "github.com/aws/aws-sdk-go/aws/signer/v4" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/stretchr/testify/require" ) func TestSigV4_Inferred_Region(t *testing.T) { os.Setenv("AWS_ACCESS_KEY_ID", "secret") os.Setenv("AWS_SECRET_ACCESS_KEY", "token") os.Setenv("AWS_REGION", "us-west-2") sess, err := session.NewSession(&aws.Config{ // Setting to an empty string to demostrate the default value from the yaml // won't override the environment's region. Region: aws.String(""), }) require.NoError(t, err) _, err = sess.Config.Credentials.Get() require.NoError(t, err) require.NotNil(t, sess.Config.Region) require.Equal(t, "us-west-2", *sess.Config.Region) } func TestSigV4RoundTripper(t *testing.T) { var gotReq *http.Request rt := &sigV4RoundTripper{ region: "us-east-2", next: promhttp.RoundTripperFunc(func(req *http.Request) (*http.Response, error) { gotReq = req return &http.Response{StatusCode: http.StatusOK}, nil }), signer: signer.NewSigner(credentials.NewStaticCredentials( "test-id", "secret", "token", )), } rt.pool.New = rt.newBuf cli := &http.Client{Transport: rt} req, err := http.NewRequest(http.MethodPost, "https://example.com", strings.NewReader("Hello, world!")) require.NoError(t, err) _, err = cli.Do(req) require.NoError(t, err) require.NotNil(t, gotReq) origReq := gotReq require.NotEmpty(t, origReq.Header.Get("Authorization")) require.NotEmpty(t, origReq.Header.Get("X-Amz-Date")) // Perform the same request but with a header that shouldn't included in the // signature; validate that the Authorization signature matches. t.Run("Ignored Headers", func(t *testing.T) { req, err := http.NewRequest(http.MethodPost, "https://example.com", strings.NewReader("Hello, world!")) require.NoError(t, err) req.Header.Add("Uber-Trace-Id", "some-trace-id") _, err = cli.Do(req) require.NoError(t, err) require.NotNil(t, gotReq) require.Equal(t, origReq.Header.Get("Authorization"), gotReq.Header.Get("Authorization")) }) t.Run("Escape URL", func(t *testing.T) { req, err := http.NewRequest(http.MethodPost, "https://example.com/test//test", strings.NewReader("Hello, world!")) require.NoError(t, err) require.Equal(t, "/test//test", req.URL.Path) _, err = cli.Do(req) require.NoError(t, err) require.NotNil(t, gotReq) require.Equal(t, "/test/test", gotReq.URL.Path) }) } golang-github-prometheus-common-0.55.0/sigv4/testdata/000077500000000000000000000000001463701437000226615ustar00rootroot00000000000000golang-github-prometheus-common-0.55.0/sigv4/testdata/sigv4_bad.yaml000066400000000000000000000001211463701437000254010ustar00rootroot00000000000000region: us-east-2 access_key: AccessKey profile: profile role_arn: blah:role/arn golang-github-prometheus-common-0.55.0/sigv4/testdata/sigv4_good.yaml000066400000000000000000000002031463701437000256040ustar00rootroot00000000000000region: us-east-2 access_key: AccessKey secret_key: SecretKey profile: profile role_arn: blah:role/arn use_fips_sts_endpoint: true golang-github-prometheus-common-0.55.0/sigv4/testdata/sigv4_good_empty_keys.yaml000066400000000000000000000000731463701437000300620ustar00rootroot00000000000000region: us-east-2 profile: profile role_arn: blah:role/arn golang-github-prometheus-common-0.55.0/version/000077500000000000000000000000001463701437000215015ustar00rootroot00000000000000golang-github-prometheus-common-0.55.0/version/info.go000066400000000000000000000056511463701437000227720ustar00rootroot00000000000000// Copyright 2016 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package version import ( "bytes" "fmt" "runtime" "runtime/debug" "strings" "text/template" ) // Build information. Populated at build-time. var ( Version string Revision string Branch string BuildUser string BuildDate string GoVersion = runtime.Version() GoOS = runtime.GOOS GoArch = runtime.GOARCH computedRevision string computedTags string ) // versionInfoTmpl contains the template used by Info. var versionInfoTmpl = ` {{.program}}, version {{.version}} (branch: {{.branch}}, revision: {{.revision}}) build user: {{.buildUser}} build date: {{.buildDate}} go version: {{.goVersion}} platform: {{.platform}} tags: {{.tags}} ` // Print returns version information. func Print(program string) string { m := map[string]string{ "program": program, "version": Version, "revision": GetRevision(), "branch": Branch, "buildUser": BuildUser, "buildDate": BuildDate, "goVersion": GoVersion, "platform": GoOS + "/" + GoArch, "tags": GetTags(), } t := template.Must(template.New("version").Parse(versionInfoTmpl)) var buf bytes.Buffer if err := t.ExecuteTemplate(&buf, "version", m); err != nil { panic(err) } return strings.TrimSpace(buf.String()) } // Info returns version, branch and revision information. func Info() string { return fmt.Sprintf("(version=%s, branch=%s, revision=%s)", Version, Branch, GetRevision()) } // BuildContext returns goVersion, platform, buildUser and buildDate information. func BuildContext() string { return fmt.Sprintf("(go=%s, platform=%s, user=%s, date=%s, tags=%s)", GoVersion, GoOS+"/"+GoArch, BuildUser, BuildDate, GetTags()) } func GetRevision() string { if Revision != "" { return Revision } return computedRevision } func GetTags() string { return computedTags } func init() { computedRevision, computedTags = computeRevision() } func computeRevision() (string, string) { var ( rev = "unknown" tags = "unknown" modified bool ) buildInfo, ok := debug.ReadBuildInfo() if !ok { return rev, tags } for _, v := range buildInfo.Settings { if v.Key == "vcs.revision" { rev = v.Value } if v.Key == "vcs.modified" { if v.Value == "true" { modified = true } } if v.Key == "-tags" { tags = v.Value } } if modified { return rev + "-modified", tags } return rev, tags }