├── .circleci └── config.yml ├── .dockerignore ├── .github ├── dependabot.yml └── workflows │ ├── container_description.yml │ └── golangci-lint.yml ├── .gitignore ├── .golangci.yml ├── .promu.yml ├── .yamllint ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── MAINTAINERS.md ├── Makefile ├── Makefile.common ├── NOTICE ├── README.md ├── SECURITY.md ├── VERSION ├── cmd ├── getool │ ├── backfill.go │ ├── backfill_others.go │ ├── backfill_test.go │ ├── main.go │ └── main_test.go └── graphite_exporter │ └── main.go ├── collector ├── collector.go ├── collector_benchmark_test.go └── collector_test.go ├── e2e ├── e2e_test.go ├── fixtures │ ├── backtrack.yml │ ├── issue90.yml │ ├── issue90_in.txt │ └── mapping.yml └── issue90_test.go ├── go.mod ├── go.sum └── reader ├── reader.go ├── reader_test.go └── testdata └── test-whisper └── load └── load ├── longterm.wsp ├── midterm.wsp └── shortterm.wsp /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | orbs: 3 | prometheus: prometheus/prometheus@0.17.1 4 | executors: 5 | # Whenever the Go version is updated here, .promu.yml should also be updated. 6 | golang: 7 | docker: 8 | - image: cimg/go:1.24 9 | jobs: 10 | test: 11 | executor: golang 12 | steps: 13 | - prometheus/setup_environment 14 | - run: make 15 | - run: git diff --exit-code 16 | - prometheus/store_artifact: 17 | file: graphite_exporter 18 | workflows: 19 | version: 2 20 | graphite_exporter: 21 | jobs: 22 | - test: 23 | filters: 24 | tags: 25 | only: /.*/ 26 | - prometheus/build: 27 | name: build 28 | filters: 29 | tags: 30 | only: /.*/ 31 | - prometheus/publish_master: 32 | context: org-context 33 | requires: 34 | - test 35 | - build 36 | filters: 37 | branches: 38 | only: master 39 | - prometheus/publish_release: 40 | context: org-context 41 | requires: 42 | - test 43 | - build 44 | filters: 45 | tags: 46 | only: /^v[0-9]+(\.[0-9]+){2}(-.+|[^-.]*)$/ 47 | branches: 48 | ignore: /.*/ 49 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .build/ 2 | .tarballs/ 3 | 4 | !.build/linux-amd64/ 5 | !.build/linux-armv7 6 | !.build/linux-arm64 7 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | -------------------------------------------------------------------------------- /.github/workflows/container_description.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Push README to Docker Hub 3 | on: 4 | push: 5 | paths: 6 | - "README.md" 7 | - "README-containers.md" 8 | - ".github/workflows/container_description.yml" 9 | branches: [ main, master ] 10 | 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | PushDockerHubReadme: 16 | runs-on: ubuntu-latest 17 | name: Push README to Docker Hub 18 | if: github.repository_owner == 'prometheus' || github.repository_owner == 'prometheus-community' # Don't run this workflow on forks. 19 | steps: 20 | - name: git checkout 21 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 22 | - name: Set docker hub repo name 23 | run: echo "DOCKER_REPO_NAME=$(make docker-repo-name)" >> $GITHUB_ENV 24 | - name: Push README to Dockerhub 25 | uses: christian-korneck/update-container-description-action@d36005551adeaba9698d8d67a296bd16fa91f8e8 # v1 26 | env: 27 | DOCKER_USER: ${{ secrets.DOCKER_HUB_LOGIN }} 28 | DOCKER_PASS: ${{ secrets.DOCKER_HUB_PASSWORD }} 29 | with: 30 | destination_container_repo: ${{ env.DOCKER_REPO_NAME }} 31 | provider: dockerhub 32 | short_description: ${{ env.DOCKER_REPO_NAME }} 33 | # Empty string results in README-containers.md being pushed if it 34 | # exists. Otherwise, README.md is pushed. 35 | readme_file: '' 36 | 37 | PushQuayIoReadme: 38 | runs-on: ubuntu-latest 39 | name: Push README to quay.io 40 | if: github.repository_owner == 'prometheus' || github.repository_owner == 'prometheus-community' # Don't run this workflow on forks. 41 | steps: 42 | - name: git checkout 43 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 44 | - name: Set quay.io org name 45 | run: echo "DOCKER_REPO=$(echo quay.io/${GITHUB_REPOSITORY_OWNER} | tr -d '-')" >> $GITHUB_ENV 46 | - name: Set quay.io repo name 47 | run: echo "DOCKER_REPO_NAME=$(make docker-repo-name)" >> $GITHUB_ENV 48 | - name: Push README to quay.io 49 | uses: christian-korneck/update-container-description-action@d36005551adeaba9698d8d67a296bd16fa91f8e8 # v1 50 | env: 51 | DOCKER_APIKEY: ${{ secrets.QUAY_IO_API_TOKEN }} 52 | with: 53 | destination_container_repo: ${{ env.DOCKER_REPO_NAME }} 54 | provider: quay 55 | # Empty string results in README-containers.md being pushed if it 56 | # exists. Otherwise, README.md is pushed. 57 | readme_file: '' 58 | -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # This action is synced from https://github.com/prometheus/prometheus 3 | name: golangci-lint 4 | on: 5 | push: 6 | paths: 7 | - "go.sum" 8 | - "go.mod" 9 | - "**.go" 10 | - "scripts/errcheck_excludes.txt" 11 | - ".github/workflows/golangci-lint.yml" 12 | - ".golangci.yml" 13 | pull_request: 14 | 15 | permissions: # added using https://github.com/step-security/secure-repo 16 | contents: read 17 | 18 | jobs: 19 | golangci: 20 | permissions: 21 | contents: read # for actions/checkout to fetch code 22 | pull-requests: read # for golangci/golangci-lint-action to fetch pull requests 23 | name: lint 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: Checkout repository 27 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 28 | - name: Install Go 29 | uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 30 | with: 31 | go-version: 1.24.x 32 | - name: Install snmp_exporter/generator dependencies 33 | run: sudo apt-get update && sudo apt-get -y install libsnmp-dev 34 | if: github.repository == 'prometheus/snmp_exporter' 35 | - name: Lint 36 | uses: golangci/golangci-lint-action@1481404843c368bc19ca9406f87d6e0fc97bdcfd # v7.0.0 37 | with: 38 | args: --verbose 39 | version: v2.1.5 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.build 2 | /.release 3 | /.tarballs 4 | /graphite_exporter 5 | *.tar.gz 6 | /getool 7 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | default: none 4 | enable: 5 | - errorlint 6 | - sloglint 7 | - staticcheck 8 | exclusions: 9 | generated: lax 10 | presets: 11 | - comments 12 | - common-false-positives 13 | - legacy 14 | - std-error-handling 15 | paths: 16 | - e2e 17 | - third_party$ 18 | - builtin$ 19 | - examples$ 20 | formatters: 21 | enable: 22 | - goimports 23 | settings: 24 | goimports: 25 | local-prefixes: 26 | - github.com/prometheus/graphite_exporter 27 | exclusions: 28 | generated: lax 29 | paths: 30 | - e2e 31 | - third_party$ 32 | - builtin$ 33 | - examples$ 34 | -------------------------------------------------------------------------------- /.promu.yml: -------------------------------------------------------------------------------- 1 | go: 2 | # Whenever the Go version is updated here, .travis.yml and 3 | # .circle/config.yml should also be updated. 4 | version: 1.24 5 | repository: 6 | path: github.com/prometheus/graphite_exporter 7 | build: 8 | binaries: 9 | - name: graphite_exporter 10 | path: ./cmd/graphite_exporter 11 | - name: getool 12 | path: ./cmd/getool 13 | ldflags: | 14 | -X github.com/prometheus/common/version.Version={{.Version}} 15 | -X github.com/prometheus/common/version.Revision={{.Revision}} 16 | -X github.com/prometheus/common/version.Branch={{.Branch}} 17 | -X github.com/prometheus/common/version.BuildUser={{user}}@{{host}} 18 | -X github.com/prometheus/common/version.BuildDate={{date "20060102-15:04:05"}} 19 | tarball: 20 | files: 21 | - LICENSE 22 | - NOTICE 23 | -------------------------------------------------------------------------------- /.yamllint: -------------------------------------------------------------------------------- 1 | --- 2 | extends: default 3 | ignore: | 4 | **/node_modules 5 | 6 | rules: 7 | braces: 8 | max-spaces-inside: 1 9 | level: error 10 | brackets: 11 | max-spaces-inside: 1 12 | level: error 13 | commas: disable 14 | comments: disable 15 | comments-indentation: disable 16 | document-start: disable 17 | indentation: 18 | spaces: consistent 19 | indent-sequences: consistent 20 | key-duplicates: 21 | ignore: | 22 | config/testdata/section_key_dup.bad.yml 23 | line-length: disable 24 | truthy: 25 | check-keys: false 26 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.16.0 / 2024-10-29 2 | 3 | * [CHANGE] Replace logging with Go slog library #277 4 | 5 | ## 0.15.2 / 2024-03-22 6 | 7 | * [SECURITY] Update Go to 1.22, update dependencies 8 | 9 | ## 0.15.1 / 2024-03-22 10 | 11 | * [SECURITY] Update dependencies, including google.golang.org/protobuf for CVE-2024-24786 12 | * [CHANGE] Use go standard errors instead of deprecated github.com/pkg/errors ([#242](https://github.com/prometheus/graphite_exporter/pull/242)) 13 | 14 | ## 0.15.0 / 2023-12-06 15 | 16 | * [FEATURE] Upgrade mapping library to bring in `honor_labels` support from prometheus/statsd_exporter#521 17 | 18 | ## 0.14.1 / 2023-12-06 19 | 20 | * [SECURITY] Maintenance release, updating dependencies & building with Go 1.21 21 | 22 | ## 0.14.0 / 2023-06-02 23 | 24 | * [FEATURE] Support scaling parameter in mapping ([#235](https://github.com/prometheus/graphite_exporter/pull/235)) 25 | * [SECURITY] Maintenance release, updating dependencies 26 | 27 | ## 0.13.3 / 2023-03-09 28 | 29 | Re-release due to CI issues, no functional change. 30 | 31 | ## 0.13.2 / 2023-03-08 32 | 33 | * [SECURITY] Update various dependencies ([#223](https://github.com/prometheus/graphite_exporter/pull/223) and others) 34 | 35 | ## 0.13.1 / 2022-12-05 36 | 37 | * [SECURITY] Fix authentication bypass [GHSA-7rg2-cxvp-9p7p](https://github.com/advisories/GHSA-7rg2-cxvp-9p7p) ([#209](https://github.com/prometheus/graphite_exporter/pull/209)) 38 | 39 | ## 0.12.4 / 2022-12-05 40 | 41 | * [SECURITY] Fix [GHSA-7rg2-cxvp-9p7p](https://github.com/advisories/GHSA-7rg2-cxvp-9p7p) (manual backport of [#209](https://github.com/prometheus/graphite_exporter/pull/209)) 42 | 43 | ## 0.13.0 / 2022-10-25 44 | 45 | * [ENHANCEMENT] Support systemd socket activation ([#206](https://github.com/prometheus/graphite_exporter/pull/206)) 46 | 47 | ## 0.12.3 / 2022-08-06 48 | 49 | * [BUGFIX] Fix crash on startup for some configurations ([#198](https://github.com/prometheus/graphite_exporter/pull/198)) 50 | 51 | For mappings that require backtracking, 0.12.2 would crash on startup due to an uninitialized logger. 52 | If this affected you, consider [changing the order of rules](https://github.com/prometheus/statsd_exporter#ordering-glob-rules) or enabling unordered rules for better performance. 53 | 54 | ## 0.12.2 / 2022-07-08 55 | 56 | * [CHANGE] Update all dependencies ([#193](https://github.com/prometheus/graphite_exporter/pull/193), [#194](https://github.com/prometheus/graphite_exporter/pull/194), [#195](https://github.com/prometheus/graphite_exporter/pull/195), [#196](https://github.com/prometheus/graphite_exporter/pull/196)) 57 | 58 | This is a comprehensive housekeeping release, bringing all dependencies and the compiler version up to date. 59 | 60 | It imports a bug fix in the mapper, allowing metrics with multiple dashes in a row. 61 | 62 | ## 0.12.1 / 2022-05-06 63 | 64 | This is a maintenance release, built with Go 1.17.9 to address security issues. 65 | 66 | ## 0.12.0 / 2021-12-01 67 | 68 | * [FEATURE] Support TLS on web UI and metrics ([#175](https://github.com/prometheus/graphite_exporter/pull/175)) 69 | 70 | ## 0.11.1 / 2021-11-26 71 | 72 | * [ENHANCEMENT] Build for windows/arm64 ([#174](https://github.com/prometheus/graphite_exporter/pull/174)) 73 | 74 | ## 0.11.0 / 2021-09-01 75 | 76 | * [ENHANCEMENT] Add experimental tool for converting historical data ([#145](https://github.com/prometheus/graphite_exporter/pull/145)) 77 | 78 | This release adds the `getool` binary to the release tarball. 79 | 80 | ## 0.10.1 / 2021-05-12 81 | 82 | No changes. 83 | This release will include an updated Busybox in the Docker image, which fixes [CVE-2018-1000500](https://nvd.nist.gov/vuln/detail/CVE-2018-1000500). 84 | This security issue does not affect you unless you extend the container and use gzip, but it trips security scanners, so we provide this version. 85 | 86 | ## 0.10.0 / 2021-04-13 87 | 88 | * [CHANGE] Reorganize repository ([#144](https://github.com/prometheus/graphite_exporter/pull/144)) 89 | * [ENHANCEMENT] Configuration check ([#146](https://github.com/prometheus/graphite_exporter/pull/146)) 90 | 91 | The main binary package is now `github.com/prometheus/graphite_exporter/cmd/graphite_exporter`. 92 | This has no effect on those using the binary release. 93 | 94 | ## 0.9.0 / 2020-07-21 95 | 96 | * [ENHANCEMENT] Generate labels from Graphite tags ([#133](https://github.com/prometheus/graphite_exporter/pull/133)) 97 | 98 | ## 0.8.0 / 2020-06-12 99 | 100 | * [CHANGE] Update metric mapper and other dependencies ([#127](https://github.com/prometheus/graphite_exporter/pull/127)) 101 | 102 | This brings the metric mapper to parity with [statsd_exporter 0.16.0](https://github.com/prometheus/statsd_exporter/blob/master/CHANGELOG.md#0160--2020-05-29). 103 | See the statsd exporter changelog for the detailed changes. 104 | Notably, we now support a random-replacement mapping cache. 105 | The changes for the timer type configuration do not affect this exporter as Graphite only supports gauge-type metrics. 106 | 107 | ## 0.7.1 / 2020-05-12 108 | 109 | * [BUGFIX] Fix "superfluous response.WriteHeader call" through dependency update ([#125](https://github.com/prometheus/graphite_exporter/pull/125)) 110 | 111 | ## 0.7.0 / 2020-02-28 112 | 113 | * [CHANGE] Update logging library and flags ([#109](https://github.com/prometheus/graphite_exporter/pull/109)) 114 | * [CHANGE] Updated prometheus golang client and statsd mapper dependency. ([#113](https://github.com/prometheus/graphite_exporter/pull/113)) 115 | 116 | This release updates several dependencies. Logging-related flags have changed. 117 | 118 | The metric mapping library is now at the level of [statsd exporter 0.14.1](https://github.com/prometheus/statsd_exporter/blob/master/CHANGELOG.md#0141--2010-01-13), bringing in various performance improvements. See the statsd exporter changelog for the detailed changes. 119 | 120 | ## 0.6.2 / 2019-06-03 121 | 122 | * [CHANGE] Do not run as root in the Docker container by default ([#85](https://github.com/prometheus/graphite_exporter/pull/85)) 123 | * [BUGFIX] Serialize processing of samples ([#94](https://github.com/prometheus/graphite_exporter/pull/94)) 124 | 125 | This issue fixes a race condition in sample processing that showed if multiple 126 | clients sent metrics simultaneously, or multiple metrics were sent in 127 | individual UDP packets. It would manifest as duplicate metrics being exported 128 | (0.4.x) or the metrics endpoint failing altogether (0.5.0). 129 | 130 | ## 0.5.0 / 2019-02-28 131 | 132 | * [ENHANCEMENT] Accept 'name' as a label ([#75](https://github.com/prometheus/graphite_exporter/pull/75)) 133 | * [BUGFIX] Update the mapper to fix captures being clobbered ([#77](https://github.com/prometheus/graphite_exporter/pull/77)) 134 | * [BUGFIX] Do not mask the pprof endpoints ([#67](https://github.com/prometheus/graphite_exporter/pull/67)) 135 | 136 | This release also pulls in a more recent version of the Prometheus client library with improved validation and performance. 137 | 138 | ## 0.4.2 / 2018-11-26 139 | 140 | * [BUGFIX] Fix segfault in mapper if mapping config is provided ([#63](https://github.com/prometheus/graphite_exporter/pull/63)) 141 | 142 | ## 0.4.1 / 2018-11-23 143 | 144 | No changes. 145 | 146 | ## 0.4.0 / 2018-11-23 147 | 148 | * [ENHANCEMENT] Log incoming and parsed samples if debug logging is enabled ([#58](https://github.com/prometheus/graphite_exporter/pull/58)) 149 | * [ENHANCEMENT] Speed up glob matching ([#59](https://github.com/prometheus/graphite_exporter/pull/59)) 150 | 151 | This release replaces the implementation of the glob matching mechanism, 152 | speeding it up significantly. In certain sub-optimal configurations, a warning 153 | is logged. 154 | 155 | This major enhancement was contributed by Wangchong Zhou in [prometheus/statsd_exporter#157](https://github.com/prometheus/statsd_exporter/pulls/157). 156 | 157 | ## 0.3.0 / 2018-08-22 158 | 159 | This release contains two major breaking changes: 160 | 161 | Flags now require two dashes (`--help` instead of `-help`). 162 | 163 | The configuration format is now YAML, and uses the same format as the [statsd exporter](https://github.com/prometheus/statsd_exporter), minus support for 164 | metric types other than gauges. 165 | There is a [conversion tool](https://github.com/bakins/statsd-exporter-convert) available. 166 | This change adds new features to the mappings: 167 | It is now possible to specify the "name" label. 168 | Regular expressions can be used to match on Graphite metric names beyond extracting dot-separated components. 169 | 170 | * [CHANGE] Use YAML configuration format and mapper from statsd exporter ([#52](https://github.com/prometheus/graphite_exporter/pull/52)) 171 | * [CHANGE] Switch to the Kingpin flag library ([#30](https://github.com/prometheus/graphite_exporter/30)) 172 | * [FEATURE] Add metric for the sample expiry setting ([#34](https://github.com/prometheus/graphite_exporter/34)) 173 | * [FEATURE] Add pprof endpoint ([#33](https://github.com/prometheus/graphite_exporter/33)) 174 | * [BUGFIX] Accept whitespace around the Graphite protocol lines ([#53](https://github.com/prometheus/graphite_exporter/53)) 175 | 176 | ## 0.2.0 / 2017-03-01 177 | 178 | * [FEATURE] Added flag to allow dropping of unmatched metrics 179 | * [ENHANCEMENT] Logging changes and standardisation 180 | 181 | ## 0.1.0 / 2015-05-05 182 | 183 | Initial release. 184 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Prometheus Community Code of Conduct 2 | 3 | Prometheus follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/main/code-of-conduct.md). 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Prometheus uses GitHub to manage reviews of pull requests. 4 | 5 | * If you have a trivial fix or improvement, go ahead and create a pull request, 6 | addressing (with `@...`) the maintainer of this repository (see 7 | [MAINTAINERS.md](MAINTAINERS.md)) in the description of the pull request. 8 | 9 | * If you plan to do something more involved, first discuss your ideas 10 | on our [mailing list](https://groups.google.com/forum/?fromgroups#!forum/prometheus-developers). 11 | This will avoid unnecessary work and surely give you and us a good deal 12 | of inspiration. 13 | 14 | * Relevant coding style guidelines are the [Go Code Review 15 | Comments](https://code.google.com/p/go-wiki/wiki/CodeReviewComments) 16 | and the _Formatting and style_ section of Peter Bourgon's [Go: Best 17 | Practices for Production 18 | Environments](http://peter.bourgon.org/go-in-production/#formatting-and-style). 19 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG ARCH="amd64" 2 | ARG OS="linux" 3 | FROM quay.io/prometheus/busybox-${OS}-${ARCH}:latest 4 | LABEL maintainer="The Prometheus Authors " 5 | 6 | ARG ARCH="amd64" 7 | ARG OS="linux" 8 | COPY .build/${OS}-${ARCH}/graphite_exporter /bin/graphite_exporter 9 | 10 | USER nobody 11 | EXPOSE 9108 9109 9109/udp 12 | ENTRYPOINT [ "/bin/graphite_exporter" ] 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | * Matthias Rampke 2 | * Pedro Tanaka [@pedro-stanaka](https://github.com/pedro-stanaka) 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2016 The Prometheus Authors 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | 14 | # Needs to be defined before including Makefile.common to auto-generate targets 15 | DOCKER_ARCHS ?= amd64 armv7 arm64 16 | 17 | include Makefile.common 18 | 19 | DOCKER_IMAGE_NAME ?= graphite-exporter 20 | 21 | # Make sure a binary is built before the e2e test is run 22 | common-test: common-build 23 | -------------------------------------------------------------------------------- /Makefile.common: -------------------------------------------------------------------------------- 1 | # Copyright 2018 The Prometheus Authors 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | 14 | 15 | # A common Makefile that includes rules to be reused in different prometheus projects. 16 | # !!! Open PRs only against the prometheus/prometheus/Makefile.common repository! 17 | 18 | # Example usage : 19 | # Create the main Makefile in the root project directory. 20 | # include Makefile.common 21 | # customTarget: 22 | # @echo ">> Running customTarget" 23 | # 24 | 25 | # Ensure GOBIN is not set during build so that promu is installed to the correct path 26 | unexport GOBIN 27 | 28 | GO ?= go 29 | GOFMT ?= $(GO)fmt 30 | FIRST_GOPATH := $(firstword $(subst :, ,$(shell $(GO) env GOPATH))) 31 | GOOPTS ?= 32 | GOHOSTOS ?= $(shell $(GO) env GOHOSTOS) 33 | GOHOSTARCH ?= $(shell $(GO) env GOHOSTARCH) 34 | 35 | GO_VERSION ?= $(shell $(GO) version) 36 | GO_VERSION_NUMBER ?= $(word 3, $(GO_VERSION)) 37 | PRE_GO_111 ?= $(shell echo $(GO_VERSION_NUMBER) | grep -E 'go1\.(10|[0-9])\.') 38 | 39 | PROMU := $(FIRST_GOPATH)/bin/promu 40 | pkgs = ./... 41 | 42 | ifeq (arm, $(GOHOSTARCH)) 43 | GOHOSTARM ?= $(shell GOARM= $(GO) env GOARM) 44 | GO_BUILD_PLATFORM ?= $(GOHOSTOS)-$(GOHOSTARCH)v$(GOHOSTARM) 45 | else 46 | GO_BUILD_PLATFORM ?= $(GOHOSTOS)-$(GOHOSTARCH) 47 | endif 48 | 49 | GOTEST := $(GO) test 50 | GOTEST_DIR := 51 | ifneq ($(CIRCLE_JOB),) 52 | ifneq ($(shell command -v gotestsum 2> /dev/null),) 53 | GOTEST_DIR := test-results 54 | GOTEST := gotestsum --junitfile $(GOTEST_DIR)/unit-tests.xml -- 55 | endif 56 | endif 57 | 58 | PROMU_VERSION ?= 0.17.0 59 | PROMU_URL := https://github.com/prometheus/promu/releases/download/v$(PROMU_VERSION)/promu-$(PROMU_VERSION).$(GO_BUILD_PLATFORM).tar.gz 60 | 61 | SKIP_GOLANGCI_LINT := 62 | GOLANGCI_LINT := 63 | GOLANGCI_LINT_OPTS ?= 64 | GOLANGCI_LINT_VERSION ?= v2.1.5 65 | GOLANGCI_FMT_OPTS ?= 66 | # golangci-lint only supports linux, darwin and windows platforms on i386/amd64/arm64. 67 | # windows isn't included here because of the path separator being different. 68 | ifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux darwin)) 69 | ifeq ($(GOHOSTARCH),$(filter $(GOHOSTARCH),amd64 i386 arm64)) 70 | # If we're in CI and there is an Actions file, that means the linter 71 | # is being run in Actions, so we don't need to run it here. 72 | ifneq (,$(SKIP_GOLANGCI_LINT)) 73 | GOLANGCI_LINT := 74 | else ifeq (,$(CIRCLE_JOB)) 75 | GOLANGCI_LINT := $(FIRST_GOPATH)/bin/golangci-lint 76 | else ifeq (,$(wildcard .github/workflows/golangci-lint.yml)) 77 | GOLANGCI_LINT := $(FIRST_GOPATH)/bin/golangci-lint 78 | endif 79 | endif 80 | endif 81 | 82 | PREFIX ?= $(shell pwd) 83 | BIN_DIR ?= $(shell pwd) 84 | DOCKER_IMAGE_TAG ?= $(subst /,-,$(shell git rev-parse --abbrev-ref HEAD)) 85 | DOCKERFILE_PATH ?= ./Dockerfile 86 | DOCKERBUILD_CONTEXT ?= ./ 87 | DOCKER_REPO ?= prom 88 | 89 | DOCKER_ARCHS ?= amd64 90 | 91 | BUILD_DOCKER_ARCHS = $(addprefix common-docker-,$(DOCKER_ARCHS)) 92 | PUBLISH_DOCKER_ARCHS = $(addprefix common-docker-publish-,$(DOCKER_ARCHS)) 93 | TAG_DOCKER_ARCHS = $(addprefix common-docker-tag-latest-,$(DOCKER_ARCHS)) 94 | 95 | SANITIZED_DOCKER_IMAGE_TAG := $(subst +,-,$(DOCKER_IMAGE_TAG)) 96 | 97 | ifeq ($(GOHOSTARCH),amd64) 98 | ifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux freebsd darwin windows)) 99 | # Only supported on amd64 100 | test-flags := -race 101 | endif 102 | endif 103 | 104 | # This rule is used to forward a target like "build" to "common-build". This 105 | # allows a new "build" target to be defined in a Makefile which includes this 106 | # one and override "common-build" without override warnings. 107 | %: common-% ; 108 | 109 | .PHONY: common-all 110 | common-all: precheck style check_license lint yamllint unused build test 111 | 112 | .PHONY: common-style 113 | common-style: 114 | @echo ">> checking code style" 115 | @fmtRes=$$($(GOFMT) -d $$(find . -path ./vendor -prune -o -name '*.go' -print)); \ 116 | if [ -n "$${fmtRes}" ]; then \ 117 | echo "gofmt checking failed!"; echo "$${fmtRes}"; echo; \ 118 | echo "Please ensure you are using $$($(GO) version) for formatting code."; \ 119 | exit 1; \ 120 | fi 121 | 122 | .PHONY: common-check_license 123 | common-check_license: 124 | @echo ">> checking license header" 125 | @licRes=$$(for file in $$(find . -type f -iname '*.go' ! -path './vendor/*') ; do \ 126 | awk 'NR<=3' $$file | grep -Eq "(Copyright|generated|GENERATED)" || echo $$file; \ 127 | done); \ 128 | if [ -n "$${licRes}" ]; then \ 129 | echo "license header checking failed:"; echo "$${licRes}"; \ 130 | exit 1; \ 131 | fi 132 | 133 | .PHONY: common-deps 134 | common-deps: 135 | @echo ">> getting dependencies" 136 | $(GO) mod download 137 | 138 | .PHONY: update-go-deps 139 | update-go-deps: 140 | @echo ">> updating Go dependencies" 141 | @for m in $$($(GO) list -mod=readonly -m -f '{{ if and (not .Indirect) (not .Main)}}{{.Path}}{{end}}' all); do \ 142 | $(GO) get -d $$m; \ 143 | done 144 | $(GO) mod tidy 145 | 146 | .PHONY: common-test-short 147 | common-test-short: $(GOTEST_DIR) 148 | @echo ">> running short tests" 149 | $(GOTEST) -short $(GOOPTS) $(pkgs) 150 | 151 | .PHONY: common-test 152 | common-test: $(GOTEST_DIR) 153 | @echo ">> running all tests" 154 | $(GOTEST) $(test-flags) $(GOOPTS) $(pkgs) 155 | 156 | $(GOTEST_DIR): 157 | @mkdir -p $@ 158 | 159 | .PHONY: common-format 160 | common-format: $(GOLANGCI_LINT) 161 | @echo ">> formatting code" 162 | $(GO) fmt $(pkgs) 163 | ifdef GOLANGCI_LINT 164 | @echo ">> formatting code with golangci-lint" 165 | $(GOLANGCI_LINT) fmt $(GOLANGCI_FMT_OPTS) 166 | endif 167 | 168 | .PHONY: common-vet 169 | common-vet: 170 | @echo ">> vetting code" 171 | $(GO) vet $(GOOPTS) $(pkgs) 172 | 173 | .PHONY: common-lint 174 | common-lint: $(GOLANGCI_LINT) 175 | ifdef GOLANGCI_LINT 176 | @echo ">> running golangci-lint" 177 | $(GOLANGCI_LINT) run $(GOLANGCI_LINT_OPTS) $(pkgs) 178 | endif 179 | 180 | .PHONY: common-lint-fix 181 | common-lint-fix: $(GOLANGCI_LINT) 182 | ifdef GOLANGCI_LINT 183 | @echo ">> running golangci-lint fix" 184 | $(GOLANGCI_LINT) run --fix $(GOLANGCI_LINT_OPTS) $(pkgs) 185 | endif 186 | 187 | .PHONY: common-yamllint 188 | common-yamllint: 189 | @echo ">> running yamllint on all YAML files in the repository" 190 | ifeq (, $(shell command -v yamllint 2> /dev/null)) 191 | @echo "yamllint not installed so skipping" 192 | else 193 | yamllint . 194 | endif 195 | 196 | # For backward-compatibility. 197 | .PHONY: common-staticcheck 198 | common-staticcheck: lint 199 | 200 | .PHONY: common-unused 201 | common-unused: 202 | @echo ">> running check for unused/missing packages in go.mod" 203 | $(GO) mod tidy 204 | @git diff --exit-code -- go.sum go.mod 205 | 206 | .PHONY: common-build 207 | common-build: promu 208 | @echo ">> building binaries" 209 | $(PROMU) build --prefix $(PREFIX) $(PROMU_BINARIES) 210 | 211 | .PHONY: common-tarball 212 | common-tarball: promu 213 | @echo ">> building release tarball" 214 | $(PROMU) tarball --prefix $(PREFIX) $(BIN_DIR) 215 | 216 | .PHONY: common-docker-repo-name 217 | common-docker-repo-name: 218 | @echo "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)" 219 | 220 | .PHONY: common-docker $(BUILD_DOCKER_ARCHS) 221 | common-docker: $(BUILD_DOCKER_ARCHS) 222 | $(BUILD_DOCKER_ARCHS): common-docker-%: 223 | docker build -t "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" \ 224 | -f $(DOCKERFILE_PATH) \ 225 | --build-arg ARCH="$*" \ 226 | --build-arg OS="linux" \ 227 | $(DOCKERBUILD_CONTEXT) 228 | 229 | .PHONY: common-docker-publish $(PUBLISH_DOCKER_ARCHS) 230 | common-docker-publish: $(PUBLISH_DOCKER_ARCHS) 231 | $(PUBLISH_DOCKER_ARCHS): common-docker-publish-%: 232 | docker push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" 233 | 234 | DOCKER_MAJOR_VERSION_TAG = $(firstword $(subst ., ,$(shell cat VERSION))) 235 | .PHONY: common-docker-tag-latest $(TAG_DOCKER_ARCHS) 236 | common-docker-tag-latest: $(TAG_DOCKER_ARCHS) 237 | $(TAG_DOCKER_ARCHS): common-docker-tag-latest-%: 238 | docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:latest" 239 | docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:v$(DOCKER_MAJOR_VERSION_TAG)" 240 | 241 | .PHONY: common-docker-manifest 242 | common-docker-manifest: 243 | DOCKER_CLI_EXPERIMENTAL=enabled docker manifest create -a "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)" $(foreach ARCH,$(DOCKER_ARCHS),$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$(ARCH):$(SANITIZED_DOCKER_IMAGE_TAG)) 244 | DOCKER_CLI_EXPERIMENTAL=enabled docker manifest push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)" 245 | 246 | .PHONY: promu 247 | promu: $(PROMU) 248 | 249 | $(PROMU): 250 | $(eval PROMU_TMP := $(shell mktemp -d)) 251 | curl -s -L $(PROMU_URL) | tar -xvzf - -C $(PROMU_TMP) 252 | mkdir -p $(FIRST_GOPATH)/bin 253 | cp $(PROMU_TMP)/promu-$(PROMU_VERSION).$(GO_BUILD_PLATFORM)/promu $(FIRST_GOPATH)/bin/promu 254 | rm -r $(PROMU_TMP) 255 | 256 | .PHONY: common-proto 257 | common-proto: 258 | @echo ">> generating code from proto files" 259 | @./scripts/genproto.sh 260 | 261 | ifdef GOLANGCI_LINT 262 | $(GOLANGCI_LINT): 263 | mkdir -p $(FIRST_GOPATH)/bin 264 | curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/$(GOLANGCI_LINT_VERSION)/install.sh \ 265 | | sed -e '/install -d/d' \ 266 | | sh -s -- -b $(FIRST_GOPATH)/bin $(GOLANGCI_LINT_VERSION) 267 | endif 268 | 269 | .PHONY: precheck 270 | precheck:: 271 | 272 | define PRECHECK_COMMAND_template = 273 | precheck:: $(1)_precheck 274 | 275 | PRECHECK_COMMAND_$(1) ?= $(1) $$(strip $$(PRECHECK_OPTIONS_$(1))) 276 | .PHONY: $(1)_precheck 277 | $(1)_precheck: 278 | @if ! $$(PRECHECK_COMMAND_$(1)) 1>/dev/null 2>&1; then \ 279 | echo "Execution of '$$(PRECHECK_COMMAND_$(1))' command failed. Is $(1) installed?"; \ 280 | exit 1; \ 281 | fi 282 | endef 283 | 284 | govulncheck: install-govulncheck 285 | govulncheck ./... 286 | 287 | install-govulncheck: 288 | command -v govulncheck > /dev/null || go install golang.org/x/vuln/cmd/govulncheck@latest 289 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Prometheus exporter for Graphite. 2 | Copyright 2015 The Prometheus Authors 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Graphite Exporter 2 | 3 | [![CircleCI](https://circleci.com/gh/prometheus/graphite_exporter/tree/master.svg?style=shield)][circleci] 4 | [![Docker Repository on Quay](https://quay.io/repository/prometheus/graphite-exporter/status)][quay] 5 | [![Docker Pulls](https://img.shields.io/docker/pulls/prom/graphite-exporter.svg?maxAge=604800)][hub] 6 | 7 | An exporter for metrics exported in the [Graphite plaintext 8 | protocol](http://graphite.readthedocs.org/en/latest/feeding-carbon.html#the-plaintext-protocol). 9 | It accepts data over both TCP and UDP, and transforms and exposes them for 10 | consumption by Prometheus. 11 | 12 | This exporter is useful for exporting metrics from existing Graphite setups, as 13 | well as for metrics which are not covered by the core Prometheus exporters such 14 | as the [Node Exporter](https://github.com/prometheus/node_exporter). 15 | 16 | ## Usage 17 | 18 | ```sh 19 | make 20 | ./graphite_exporter 21 | ``` 22 | 23 | Configure existing monitoring to send Graphite plaintext data to port 9109 on UDP or TCP. 24 | As a simple demonstration: 25 | 26 | ```sh 27 | echo "test_tcp 1234 $(date +%s)" | nc localhost 9109 28 | echo "test_udp 1234 $(date +%s)" | nc -u -w1 localhost 9109 29 | ``` 30 | 31 | Metrics will be available on [http://localhost:9108/metrics](http://localhost:9108/metrics). 32 | 33 | To avoid using unbounded memory, metrics will be garbage collected five minutes after 34 | they are last pushed to. This is configurable with the `--graphite.sample-expiry` flag. 35 | 36 | ## Graphite Tags 37 | 38 | The graphite_exporter accepts metrics in the [tagged carbon format](https://graphite.readthedocs.io/en/latest/tags.html). In the case where there are valid and invalid tags supplied in one metric, the invalid tags will be dropped and the `graphite_tag_parse_failures` counter will be incremented. The exporter accepts inconsistent label sets, but this may cause issues querying the data in Prometheus. 39 | 40 | By default, labels explicitly specified in configuration take precedence over labels from the metric. To set the label from the metric instead, use [`honor_labels`](https://github.com/prometheus/statsd_exporter/#honor-labels). 41 | 42 | 43 | ## Metric Mapping and Configuration 44 | 45 | **Please note there has been a breaking change in configuration after version 0.2.0. The YAML style config from [statsd_exporter](https://github.com/prometheus/statsd_exporter) is now used. See conversion instructions below** 46 | 47 | ### YAML Config 48 | 49 | The graphite_exporter can be configured to translate specific dot-separated 50 | graphite metrics into labeled Prometheus metrics via YAML configuration file. This file shares syntax and logic with [statsd_exporter](https://github.com/prometheus/statsd_exporter). Please follow the statsd_exporter documentation for usage information. However, graphite_exporter does not support *all* parsing features at this time. Any feature based on the 'timer_type' option will not function. Otherwise, regex matching, groups, match/drop behavior, should work as expected. 51 | 52 | Metrics that don't match any mapping in the configuration file are translated 53 | into Prometheus metrics without any labels and with names in which every 54 | non-alphanumeric character except `_` and `:` is replaced with `_`. 55 | 56 | If you have a very large set of metrics you may want to skip the ones that don't 57 | match the mapping configuration. If that is the case you can force this behaviour 58 | using the `--graphite.mapping-strict-match` flag, and it will only store those metrics 59 | you really want. 60 | 61 | An example mapping configuration: 62 | 63 | ```yaml 64 | mappings: 65 | - match: test.dispatcher.*.*.* 66 | name: dispatcher_events_total 67 | labels: 68 | action: $2 69 | job: test_dispatcher 70 | outcome: $3 71 | processor: $1 72 | - match: '*.signup.*.*' 73 | name: signup_events_total 74 | labels: 75 | job: ${1}_server 76 | outcome: $3 77 | provider: $2 78 | - match: 'servers\.(.*)\.networking\.subnetworks\.transmissions\.([a-z0-9-]+)\.(.*)' 79 | match_type: regex 80 | name: 'servers_networking_transmissions_${3}' 81 | labels: 82 | hostname: ${1} 83 | device: ${2} 84 | ``` 85 | 86 | This would transform these example graphite metrics into Prometheus metrics as 87 | follows: 88 | 89 | ```console 90 | test.dispatcher.FooProcessor.send.success 91 | => dispatcher_events_total{processor="FooProcessor", action="send", outcome="success", job="test_dispatcher"} 92 | 93 | foo_product.signup.facebook.failure 94 | => signup_events_total{provider="facebook", outcome="failure", job="foo_product_server"} 95 | 96 | test.web-server.foo.bar 97 | => test_web__server_foo_bar{} 98 | 99 | servers.rack-003-server-c4de.networking.subnetworks.transmissions.eth0.failure.mean_rate 100 | => servers_networking_transmissions_failure_mean_rate{device="eth0",hostname="rack-003-server-c4de"} 101 | ``` 102 | 103 | ### Conversion from legacy configuration 104 | 105 | If you have an existing config file using the legacy mapping syntax, you may use [statsd-exporter-convert](https://github.com/bakins/statsd-exporter-convert) to update to the new YAML based syntax. Here we convert the old example synatx: 106 | 107 | ```console 108 | $ go get -u github.com/bakins/statsd-exporter-convert 109 | 110 | $ cat example.conf 111 | test.dispatcher.*.*.* 112 | name="dispatcher_events_total" 113 | processor="$1" 114 | action="$2" 115 | outcome="$3" 116 | job="test_dispatcher" 117 | 118 | *.signup.*.* 119 | name="signup_events_total" 120 | provider="$2" 121 | outcome="$3" 122 | job="${1}_server" 123 | 124 | $ statsd-exporter-convert example.conf 125 | mappings: 126 | - match: test.dispatcher.*.*.* 127 | name: dispatcher_events_total 128 | labels: 129 | action: $2 130 | job: test_dispatcher 131 | outcome: $3 132 | processor: $1 133 | - match: '*.signup.*.*' 134 | name: signup_events_total 135 | labels: 136 | job: ${1}_server 137 | outcome: $3 138 | provider: $2 139 | ``` 140 | 141 | ## Using Docker 142 | 143 | You can deploy this exporter using the [prom/graphite-exporter][hub] Docker image. 144 | 145 | For example: 146 | 147 | ```bash 148 | docker pull prom/graphite-exporter 149 | 150 | docker run -d -p 9108:9108 -p 9109:9109 -p 9109:9109/udp \ 151 | -v ${PWD}/graphite_mapping.conf:/tmp/graphite_mapping.conf \ 152 | prom/graphite-exporter --graphite.mapping-config=/tmp/graphite_mapping.conf 153 | ``` 154 | 155 | ## **Experimental**: Importing Whisper data 156 | 157 | Import data from Graphite using the bundled `getool`. 158 | See `getool create-blocks --help` for usage. 159 | 160 | To import long-term data in a reasonable amount of resources, increase the duration per generated TSDB block. 161 | The `--block-duration` must be a power of two in hours, e.g. `4h`, `8h`, and so on. 162 | 163 | To merge the data into an existing Prometheus storage directory, start Prometheus with the `--storage.tsdb.allow-overlapping-blocks` flag. 164 | 165 | ## Incompatibility with Graphite bridge 166 | 167 | This exporter does not work in combination with the [Java client](https://prometheus.github.io/client_java/io/prometheus/client/bridge/Graphite.html) or [Python client](https://github.com/prometheus/client_python#graphite) Graphite bridge. 168 | In the transition to the Graphite data model and back, information is lost. 169 | Additionally, default metrics conflict between the client libraries and the exporter. 170 | 171 | Instead, configure Prometheus to scrape your application directly, without the exporter in the middle. 172 | For batch or ephemeral jobs, use the [pushgateway](https://prometheus.io/docs/practices/pushing/) [integration](https://github.com/prometheus/client_python#exporting-to-a-pushgateway). 173 | If you absolutely must push, consider [PushProx](https://github.com/prometheus-community/PushProx) or the [Grafana agent](https://github.com/grafana/agent) instead. 174 | 175 | ## TLS and basic authentication 176 | 177 | Graphite Exporter supports TLS and basic authentication. This enables better control of the various HTTP endpoints. 178 | 179 | To use TLS and/or basic authentication, you need to pass a configuration file using the `--web.config.file` parameter. The format of the file is described 180 | [in the exporter-toolkit repository](https://github.com/prometheus/exporter-toolkit/blob/master/docs/web-configuration.md). 181 | 182 | 183 | 184 | [circleci]: https://circleci.com/gh/prometheus/graphite_exporter 185 | [hub]: https://hub.docker.com/r/prom/graphite-exporter/ 186 | [travis]: https://travis-ci.org/prometheus/graphite_exporter 187 | [quay]: https://quay.io/repository/prometheus/graphite-exporter 188 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting a security issue 2 | 3 | The Prometheus security policy, including how to report vulnerabilities, can be 4 | found here: 5 | 6 | 7 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.16.0 2 | -------------------------------------------------------------------------------- /cmd/getool/backfill.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | //go:build !aix && !windows 15 | // +build !aix,!windows 16 | 17 | package main 18 | 19 | import ( 20 | "context" 21 | "errors" 22 | "fmt" 23 | "math" 24 | "os" 25 | "regexp" 26 | "strconv" 27 | "text/tabwriter" 28 | "time" 29 | 30 | "github.com/alecthomas/units" 31 | "github.com/prometheus/common/promslog" 32 | "github.com/prometheus/prometheus/model/labels" 33 | "github.com/prometheus/prometheus/tsdb" 34 | tsdb_errors "github.com/prometheus/prometheus/tsdb/errors" 35 | "github.com/prometheus/statsd_exporter/pkg/mapper" 36 | 37 | "github.com/prometheus/graphite_exporter/reader" 38 | ) 39 | 40 | var invalidMetricChars = regexp.MustCompile("[^a-zA-Z0-9_:]") 41 | 42 | func createBlocks(input reader.DBReader, mint, maxt, blockDuration int64, maxSamplesInAppender int, outputDir string, metricMapper *mapper.MetricMapper, strictMatch, humanReadable bool) (returnErr error) { 43 | mint = blockDuration * (mint / blockDuration) 44 | 45 | db, err := tsdb.OpenDBReadOnly(outputDir, "", nil) 46 | if err != nil { 47 | return err 48 | } 49 | defer func() { 50 | returnErr = tsdb_errors.NewMulti(returnErr, db.Close()).Err() 51 | }() 52 | 53 | var wroteHeader bool 54 | 55 | metrics, err := input.Metrics() 56 | if err != nil { 57 | return err 58 | } 59 | 60 | for t := mint; t <= maxt; t = t + blockDuration { 61 | tsUpper := t + blockDuration 62 | err := func() error { 63 | // To prevent races with compaction, a block writer only allows appending samples 64 | // that are at most half a block size older than the most recent sample appended so far. 65 | // However, in the way we use the block writer here, compaction doesn't happen, while we 66 | // also need to append samples throughout the whole block range. To allow that, we 67 | // pretend that the block is twice as large here, but only really add sample in the 68 | // original interval later. 69 | w, err := tsdb.NewBlockWriter(promslog.NewNopLogger(), outputDir, 2*blockDuration) 70 | if err != nil { 71 | return fmt.Errorf("block writer: %w", err) 72 | } 73 | defer func() { 74 | err = tsdb_errors.NewMulti(err, w.Close()).Err() 75 | }() 76 | 77 | ctx := context.Background() 78 | app := w.Appender(ctx) 79 | samplesCount := 0 80 | for _, m := range metrics { 81 | mapping, mappingLabels, mappingPresent := metricMapper.GetMapping(m, mapper.MetricTypeGauge) 82 | 83 | if (mappingPresent && mapping.Action == mapper.ActionTypeDrop) || (!mappingPresent && strictMatch) { 84 | continue 85 | } 86 | 87 | l := make(labels.Labels, 0) 88 | // add mapping labels to parsed labels· 89 | for k, v := range mappingLabels { 90 | l = append(l, labels.Label{Name: k, Value: v}) 91 | } 92 | 93 | var name string 94 | if mappingPresent { 95 | name = invalidMetricChars.ReplaceAllString(mapping.Name, "_") 96 | } else { 97 | name = invalidMetricChars.ReplaceAllString(m, "_") 98 | } 99 | l = append(l, labels.Label{Name: "__name__", Value: name}) 100 | 101 | points, err := input.Points(m, t, tsUpper) 102 | if err != nil { 103 | return err 104 | } 105 | for _, point := range points { 106 | if _, err := app.Append(0, l, point.Timestamp, point.Value); err != nil { 107 | return fmt.Errorf("add sample: %w", err) 108 | } 109 | 110 | samplesCount++ 111 | if samplesCount < maxSamplesInAppender { 112 | continue 113 | } 114 | 115 | // If we arrive here, the samples count is greater than the maxSamplesInAppender. 116 | // Therefore the old appender is committed and a new one is created. 117 | // This prevents keeping too many samples lined up in an appender and thus in RAM. 118 | if err := app.Commit(); err != nil { 119 | return fmt.Errorf("commit: %w", err) 120 | } 121 | 122 | app = w.Appender(ctx) 123 | samplesCount = 0 124 | } 125 | } 126 | 127 | if err := app.Commit(); err != nil { 128 | return fmt.Errorf("commit: %w", err) 129 | } 130 | 131 | block, err := w.Flush(ctx) 132 | switch { 133 | case err == nil: 134 | blocks, err := db.Blocks() 135 | if err != nil { 136 | return fmt.Errorf("get blocks: %w", err) 137 | } 138 | for _, b := range blocks { 139 | if b.Meta().ULID == block { 140 | printBlocks([]tsdb.BlockReader{b}, !wroteHeader, humanReadable) 141 | wroteHeader = true 142 | break 143 | } 144 | } 145 | case errors.Is(err, tsdb.ErrNoSeriesAppended): 146 | default: 147 | return fmt.Errorf("flush: %w", err) 148 | } 149 | 150 | return nil 151 | }() 152 | if err != nil { 153 | return fmt.Errorf("process blocks: %w", err) 154 | } 155 | } 156 | return nil 157 | } 158 | 159 | func printBlocks(blocks []tsdb.BlockReader, writeHeader, humanReadable bool) { 160 | tw := tabwriter.NewWriter(os.Stdout, 13, 0, 2, ' ', 0) 161 | defer tw.Flush() 162 | 163 | if writeHeader { 164 | fmt.Fprintln(tw, "BLOCK ULID\tMIN TIME\tMAX TIME\tDURATION\tNUM SAMPLES\tNUM CHUNKS\tNUM SERIES\tSIZE") 165 | } 166 | 167 | for _, b := range blocks { 168 | meta := b.Meta() 169 | 170 | fmt.Fprintf(tw, 171 | "%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\n", 172 | meta.ULID, 173 | getFormatedTime(meta.MinTime, humanReadable), 174 | getFormatedTime(meta.MaxTime, humanReadable), 175 | time.Duration(meta.MaxTime-meta.MinTime)*time.Millisecond, 176 | meta.Stats.NumSamples, 177 | meta.Stats.NumChunks, 178 | meta.Stats.NumSeries, 179 | getFormatedBytes(b.Size(), humanReadable), 180 | ) 181 | } 182 | } 183 | 184 | func getFormatedTime(timestamp int64, humanReadable bool) string { 185 | if humanReadable { 186 | return time.Unix(timestamp/1000, 0).UTC().String() 187 | } 188 | return strconv.FormatInt(timestamp, 10) 189 | } 190 | 191 | func getFormatedBytes(bytes int64, humanReadable bool) string { 192 | if humanReadable { 193 | return units.Base2Bytes(bytes).String() 194 | } 195 | return strconv.FormatInt(bytes, 10) 196 | } 197 | 198 | func backfill(maxSamplesInAppender int, inputDir, outputDir, mappingConfig string, strictMatch, humanReadable bool, blockDuration int64) (err error) { 199 | wdb := reader.NewReader(inputDir) 200 | mint, maxt, err := wdb.GetMinAndMaxTimestamps() 201 | if err != nil { 202 | return fmt.Errorf("getting min and max timestamp: %w", err) 203 | } 204 | metricMapper := &mapper.MetricMapper{} 205 | 206 | if mappingConfig != "" { 207 | err := metricMapper.InitFromFile(mappingConfig) 208 | if err != nil { 209 | logger := promslog.New(&promslog.Config{}) 210 | logger.Error("Error loading metric mapping config", "err", err) 211 | return err 212 | } 213 | } 214 | 215 | if err := createBlocks(wdb, mint, maxt, blockDuration, maxSamplesInAppender, outputDir, metricMapper, strictMatch, humanReadable); err != nil { 216 | return fmt.Errorf("block creation: %w", err) 217 | } 218 | return nil 219 | } 220 | 221 | func backfillWhisper(inputDir, outputDir, mappingConfig string, strictMatch, humanReadable bool, optBlockDuration time.Duration) (err error) { 222 | blockDuration := int64(time.Duration(optBlockDuration) / time.Millisecond) 223 | 224 | if !validateBlockDuration(blockDuration) { 225 | return fmt.Errorf("invalid block duration: %s", optBlockDuration.String()) 226 | } 227 | 228 | if err := os.MkdirAll(outputDir, 0o777); err != nil { 229 | return fmt.Errorf("create output dir: %w", err) 230 | } 231 | 232 | return backfill(5000, inputDir, outputDir, mappingConfig, strictMatch, humanReadable, blockDuration) 233 | } 234 | 235 | func validateBlockDuration(t int64) bool { 236 | i, f := math.Modf(float64(t) / float64(tsdb.DefaultBlockDuration)) 237 | if f != 0 { 238 | return false 239 | } 240 | 241 | i, f = math.Modf(math.Log2(i)) 242 | return f == 0 && i >= 0 243 | } 244 | -------------------------------------------------------------------------------- /cmd/getool/backfill_others.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | //go:build aix || windows 15 | // +build aix windows 16 | 17 | package main 18 | 19 | import ( 20 | "errors" 21 | "time" 22 | ) 23 | 24 | func backfillWhisper(inputDir, outputDir, mappingConfig string, strictMatch, humanReadable bool, optBlockDuration time.Duration) (err error) { 25 | return errors.New("backfilling is not supported for this architecture") 26 | } 27 | -------------------------------------------------------------------------------- /cmd/getool/backfill_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | //go:build !aix 15 | // +build !aix 16 | 17 | package main 18 | 19 | import ( 20 | "context" 21 | "io" 22 | "math" 23 | "os" 24 | "os/exec" 25 | "path/filepath" 26 | "testing" 27 | "time" 28 | 29 | "github.com/go-graphite/go-whisper" 30 | "github.com/prometheus/prometheus/model/labels" 31 | "github.com/prometheus/prometheus/storage" 32 | "github.com/prometheus/prometheus/tsdb" 33 | "github.com/prometheus/prometheus/tsdb/chunkenc" 34 | "github.com/stretchr/testify/require" 35 | ) 36 | 37 | func TestBackfill(t *testing.T) { 38 | if testing.Short() { 39 | t.Skip("skipping test in short mode.") 40 | } 41 | for _, tt := range []struct { 42 | name string 43 | metricName string 44 | labels map[string]string 45 | mappingConfig string 46 | strictMatch bool 47 | }{ 48 | { 49 | name: "default", 50 | metricName: "load_cpu_cpu0", 51 | }, 52 | { 53 | name: "with_mapping", 54 | mappingConfig: ` 55 | mappings: 56 | - match: "load.*.*" 57 | name: load_$1 58 | labels: 59 | cpu: $2`, 60 | metricName: "load_cpu", 61 | labels: map[string]string{"cpu": "cpu0"}, 62 | }, 63 | { 64 | name: "strict_match", 65 | strictMatch: true, 66 | mappingConfig: ` 67 | mappings: 68 | - match: load.*.* 69 | name: load_$1 70 | labels: 71 | cpu: $2`, 72 | metricName: "load_cpu", 73 | labels: map[string]string{"cpu": "cpu0"}, 74 | }, 75 | } { 76 | tt := tt // TODO(matthias): remove after upgrading to Go 1.22 77 | t.Run(tt.name, func(t *testing.T) { 78 | 79 | var ( 80 | metricTime = int(time.Now().Add(-30 * time.Minute).Unix()) 81 | tmpData = filepath.Join(os.TempDir(), "graphite_exporter_test") 82 | whisperDir = filepath.Join(tmpData, "whisper", "load", "cpu") 83 | ) 84 | 85 | defer os.RemoveAll(tmpData) 86 | 87 | require.NoError(t, os.MkdirAll(whisperDir, 0o777)) 88 | retentions, err := whisper.ParseRetentionDefs("1s:3600") 89 | require.NoError(t, err) 90 | wsp, err := whisper.Create(filepath.Join(whisperDir, "cpu0.wsp"), retentions, whisper.Sum, 0.5) 91 | require.NoError(t, err) 92 | require.NoError(t, wsp.Update(1234.5678, metricTime-1)) 93 | require.NoError(t, wsp.Update(12345.678, metricTime)) 94 | require.NoError(t, wsp.Close()) 95 | 96 | arguments := []string{ 97 | "-test.main", 98 | "create-blocks", 99 | } 100 | 101 | if tt.mappingConfig != "" { 102 | cfgFile := filepath.Join(tmpData, "mapping.yaml") 103 | err := os.WriteFile(cfgFile, []byte(tt.mappingConfig), 0644) 104 | require.NoError(t, err) 105 | arguments = append(arguments, "--graphite.mapping-config", cfgFile) 106 | } 107 | 108 | if tt.strictMatch { 109 | arguments = append(arguments, "--graphite.mapping-strict-match") 110 | } 111 | 112 | arguments = append(arguments, filepath.Join(tmpData, "whisper"), filepath.Join(tmpData, "data")) 113 | 114 | cmd := exec.Command(testPath, arguments...) 115 | 116 | // Log stderr in case of failure. 117 | stderr, err := cmd.StderrPipe() 118 | require.NoError(t, err) 119 | go func() { 120 | slurp, _ := io.ReadAll(stderr) 121 | t.Log(string(slurp)) 122 | }() 123 | 124 | err = cmd.Start() 125 | require.NoError(t, err) 126 | 127 | err = cmd.Wait() 128 | require.NoError(t, err) 129 | 130 | require.NoError(t, os.MkdirAll(filepath.Join(tmpData, "data", "wal"), 0o777)) 131 | 132 | db, err := tsdb.OpenDBReadOnly(filepath.Join(tmpData, "data"), "", nil) 133 | require.NoError(t, err) 134 | q, err := db.Querier(math.MinInt64, math.MaxInt64) 135 | require.NoError(t, err) 136 | 137 | s := queryAllSeries(t, q) 138 | 139 | // XXX: getool creates labels with the __name__ last, different from 140 | // the sorting produced by labels.New. Is this a problem? 141 | ll := labels.FromMap(tt.labels) 142 | ll = append(ll, labels.Label{ 143 | Name: "__name__", 144 | Value: tt.metricName, 145 | }) 146 | 147 | require.Equal(t, ll, s[0].Labels) 148 | require.Equal(t, 1000*int64(metricTime-1), s[0].Timestamp) 149 | require.Equal(t, 1234.5678, s[0].Value) 150 | require.Equal(t, ll, s[1].Labels) 151 | require.Equal(t, 1000*int64(metricTime), s[1].Timestamp) 152 | require.Equal(t, 12345.678, s[1].Value) 153 | }) 154 | } 155 | } 156 | 157 | type backfillSample struct { 158 | Timestamp int64 159 | Value float64 160 | Labels labels.Labels 161 | } 162 | 163 | func queryAllSeries(t *testing.T, q storage.Querier) []backfillSample { 164 | ss := q.Select(context.Background(), false, nil, labels.MustNewMatcher(labels.MatchRegexp, "", ".*")) 165 | samples := []backfillSample{} 166 | for ss.Next() { 167 | series := ss.At() 168 | it := series.Iterator(nil) 169 | require.NoError(t, it.Err()) 170 | for it.Next() != chunkenc.ValNone { 171 | ts, v := it.At() 172 | samples = append(samples, backfillSample{Timestamp: ts, Value: v, Labels: series.Labels()}) 173 | } 174 | } 175 | return samples 176 | } 177 | 178 | func TestValidateBlockSize(t *testing.T) { 179 | require.True(t, validateBlockDuration(int64(time.Duration(2*time.Hour)/time.Millisecond))) 180 | require.True(t, validateBlockDuration(int64(time.Duration(4*time.Hour)/time.Millisecond))) 181 | require.True(t, validateBlockDuration(int64(time.Duration(16*time.Hour)/time.Millisecond))) 182 | 183 | require.False(t, validateBlockDuration(0)) 184 | require.False(t, validateBlockDuration(int64(time.Duration(1*time.Hour)/time.Millisecond))) 185 | require.False(t, validateBlockDuration(int64(time.Duration(3*time.Hour)/time.Millisecond))) 186 | require.False(t, validateBlockDuration(int64(time.Duration(11*time.Hour)/time.Millisecond))) 187 | } 188 | -------------------------------------------------------------------------------- /cmd/getool/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "fmt" 18 | "os" 19 | "path/filepath" 20 | 21 | "github.com/alecthomas/kingpin/v2" 22 | "github.com/prometheus/common/version" 23 | ) 24 | 25 | func main() { 26 | app := kingpin.New(filepath.Base(os.Args[0]), "Tooling for the Graphite Exporter.") 27 | app.Version(version.Print("getool")) 28 | app.HelpFlag.Short('h') 29 | 30 | defaultDBPath := "data/" 31 | 32 | importCmd := app.Command("create-blocks", "Import samples from OpenMetrics input and produce TSDB blocks. Please refer to the exporter docs for more details.") 33 | // TODO(aSquare14): add flag to set default block duration 34 | importFilePath := importCmd.Arg("whisper directory", "Directory of the whisper database.").Required().String() 35 | importDBPath := importCmd.Arg("output directory", "Output directory for generated blocks.").Default(defaultDBPath).String() 36 | importHumanReadable := importCmd.Flag("human-readable", "Print human readable values.").Short('r').Bool() 37 | importBlockDuration := importCmd.Flag("block-duration", "TSDB block duration.").Default("2h").Duration() 38 | importMappingConfig := importCmd.Flag("graphite.mapping-config", "Metric mapping configuration file name.").Default("").String() 39 | importStrictMatch := importCmd.Flag("graphite.mapping-strict-match", "Only import metrics that match the mapping configuration.").Bool() 40 | 41 | parsedCmd := kingpin.MustParse(app.Parse(os.Args[1:])) 42 | 43 | switch parsedCmd { 44 | case importCmd.FullCommand(): 45 | os.Exit(checkErr(backfillWhisper(*importFilePath, *importDBPath, *importMappingConfig, *importStrictMatch, *importHumanReadable, *importBlockDuration))) 46 | } 47 | } 48 | 49 | func checkErr(err error) int { 50 | if err != nil { 51 | fmt.Fprintln(os.Stderr, err) 52 | return 1 53 | } 54 | return 0 55 | } 56 | -------------------------------------------------------------------------------- /cmd/getool/main_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "os" 18 | "testing" 19 | ) 20 | 21 | var ( 22 | testPath = os.Args[0] 23 | ) 24 | 25 | func TestMain(m *testing.M) { 26 | for i, arg := range os.Args { 27 | if arg == "-test.main" { 28 | os.Args = append(os.Args[:i], os.Args[i+1:]...) 29 | main() 30 | return 31 | } 32 | } 33 | 34 | os.Exit(m.Run()) 35 | } 36 | -------------------------------------------------------------------------------- /cmd/graphite_exporter/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "bufio" 18 | "bytes" 19 | "fmt" 20 | "log/slog" 21 | "net" 22 | "net/http" 23 | _ "net/http/pprof" 24 | "os" 25 | 26 | "github.com/alecthomas/kingpin/v2" 27 | "github.com/prometheus/client_golang/prometheus" 28 | clientVersion "github.com/prometheus/client_golang/prometheus/collectors/version" 29 | "github.com/prometheus/client_golang/prometheus/promhttp" 30 | "github.com/prometheus/common/promslog" 31 | "github.com/prometheus/common/promslog/flag" 32 | "github.com/prometheus/common/version" 33 | "github.com/prometheus/exporter-toolkit/web" 34 | "github.com/prometheus/exporter-toolkit/web/kingpinflag" 35 | "github.com/prometheus/statsd_exporter/pkg/mapper" 36 | "github.com/prometheus/statsd_exporter/pkg/mappercache/lru" 37 | "github.com/prometheus/statsd_exporter/pkg/mappercache/randomreplacement" 38 | 39 | "github.com/prometheus/graphite_exporter/collector" 40 | ) 41 | 42 | var ( 43 | metricsPath = kingpin.Flag("web.telemetry-path", "Path under which to expose Prometheus metrics.").Default("/metrics").String() 44 | graphiteAddress = kingpin.Flag("graphite.listen-address", "TCP and UDP address on which to accept samples.").Default(":9109").String() 45 | mappingConfig = kingpin.Flag("graphite.mapping-config", "Metric mapping configuration file name.").Default("").String() 46 | sampleExpiry = kingpin.Flag("graphite.sample-expiry", "How long a sample is valid for.").Default("5m").Duration() 47 | strictMatch = kingpin.Flag("graphite.mapping-strict-match", "Only store metrics that match the mapping configuration.").Bool() 48 | cacheSize = kingpin.Flag("graphite.cache-size", "Maximum size of your metric mapping cache. Relies on least recently used replacement policy if max size is reached.").Default("1000").Int() 49 | cacheType = kingpin.Flag("graphite.cache-type", "Metric mapping cache type. Valid options are \"lru\" and \"random\"").Default("lru").Enum("lru", "random") 50 | dumpFSMPath = kingpin.Flag("debug.dump-fsm", "The path to dump internal FSM generated for glob matching as Dot file.").Default("").String() 51 | checkConfig = kingpin.Flag("check-config", "Check configuration and exit.").Default("false").Bool() 52 | toolkitFlags = kingpinflag.AddFlags(kingpin.CommandLine, ":9108") 53 | ) 54 | 55 | func init() { 56 | prometheus.MustRegister(clientVersion.NewCollector("graphite_exporter")) 57 | } 58 | 59 | func dumpFSM(mapper *mapper.MetricMapper, dumpFilename string, logger *slog.Logger) error { 60 | if mapper.FSM == nil { 61 | return fmt.Errorf("no FSM available to be dumped, possibly because the mapping contains regex patterns") 62 | } 63 | f, err := os.Create(dumpFilename) 64 | if err != nil { 65 | return err 66 | } 67 | logger.Info("Start dumping FSM", "to", dumpFilename) 68 | w := bufio.NewWriter(f) 69 | mapper.FSM.DumpFSM(w) 70 | w.Flush() 71 | f.Close() 72 | logger.Info("Finish dumping FSM") 73 | return nil 74 | } 75 | 76 | func main() { 77 | promslogConfig := &promslog.Config{} 78 | flag.AddFlags(kingpin.CommandLine, promslogConfig) 79 | kingpin.Version(version.Print("graphite_exporter")) 80 | kingpin.HelpFlag.Short('h') 81 | kingpin.Parse() 82 | logger := promslog.New(promslogConfig) 83 | 84 | logger.Info("Starting graphite_exporter", "version_info", version.Info()) 85 | logger.Info(version.BuildContext()) 86 | 87 | http.Handle(*metricsPath, promhttp.Handler()) 88 | c := collector.NewGraphiteCollector(logger, *strictMatch, *sampleExpiry) 89 | prometheus.MustRegister(c) 90 | 91 | metricMapper := &mapper.MetricMapper{Logger: logger} 92 | if *mappingConfig != "" { 93 | err := metricMapper.InitFromFile(*mappingConfig) 94 | if err != nil { 95 | logger.Error("Error loading metric mapping config", "err", err) 96 | os.Exit(1) 97 | } 98 | } 99 | 100 | cache, err := getCache(*cacheSize, *cacheType, prometheus.DefaultRegisterer) 101 | if err != nil { 102 | logger.Error("error initializing mapper cache", "err", err) 103 | os.Exit(1) 104 | } 105 | metricMapper.UseCache(cache) 106 | 107 | if *checkConfig { 108 | logger.Info("Configuration check successful, exiting") 109 | return 110 | } 111 | 112 | if *dumpFSMPath != "" { 113 | err := dumpFSM(metricMapper, *dumpFSMPath, logger) 114 | if err != nil { 115 | logger.Error("Error dumping FSM", "err", err) 116 | os.Exit(1) 117 | } 118 | } 119 | 120 | c.SetMapper(metricMapper) 121 | 122 | tcpSock, err := net.Listen("tcp", *graphiteAddress) 123 | if err != nil { 124 | logger.Error("Error binding to TCP socket", "err", err) 125 | os.Exit(1) 126 | } 127 | go func() { 128 | for { 129 | conn, err := tcpSock.Accept() 130 | if err != nil { 131 | logger.Error("Error accepting TCP connection", "err", err) 132 | continue 133 | } 134 | go func() { 135 | defer conn.Close() 136 | c.ProcessReader(conn) 137 | }() 138 | } 139 | }() 140 | 141 | udpAddress, err := net.ResolveUDPAddr("udp", *graphiteAddress) 142 | if err != nil { 143 | logger.Error("Error resolving UDP address", "err", err) 144 | os.Exit(1) 145 | } 146 | udpSock, err := net.ListenUDP("udp", udpAddress) 147 | if err != nil { 148 | logger.Error("Error listening to UDP address", "err", err) 149 | os.Exit(1) 150 | } 151 | go func() { 152 | defer udpSock.Close() 153 | for { 154 | buf := make([]byte, 65536) 155 | chars, srcAddress, err := udpSock.ReadFromUDP(buf) 156 | if err != nil { 157 | logger.Error("Error reading UDP packet", "from", srcAddress, "err", err) 158 | continue 159 | } 160 | go c.ProcessReader(bytes.NewReader(buf[0:chars])) 161 | } 162 | }() 163 | 164 | if *metricsPath != "/" { 165 | landingConfig := web.LandingConfig{ 166 | Name: "Graphite Exporter", 167 | Description: "Prometheus Graphite Exporter", 168 | ExtraHTML: `

Accepting plaintext Graphite samples over TCP and UDP on ` + *graphiteAddress + `

`, 169 | Version: version.Info(), 170 | Links: []web.LandingLinks{ 171 | { 172 | Address: *metricsPath, 173 | Text: "Metrics", 174 | }, 175 | }, 176 | } 177 | landingPage, err := web.NewLandingPage(landingConfig) 178 | if err != nil { 179 | logger.Error(err.Error()) 180 | os.Exit(1) 181 | } 182 | http.Handle("/", landingPage) 183 | } 184 | 185 | server := &http.Server{} 186 | if err := web.ListenAndServe(server, toolkitFlags, logger); err != nil { 187 | logger.Error("error running HTTP server", "err", err) 188 | os.Exit(1) 189 | } 190 | } 191 | 192 | // TODO(mr): this is copied verbatim from statsd_exporter/main.go. It should be a 193 | // convenience function in mappercache, but that caused an import cycle. 194 | func getCache(cacheSize int, cacheType string, registerer prometheus.Registerer) (mapper.MetricMapperCache, error) { 195 | var cache mapper.MetricMapperCache 196 | var err error 197 | if cacheSize == 0 { 198 | return nil, nil 199 | } else { 200 | switch cacheType { 201 | case "lru": 202 | cache, err = lru.NewMetricMapperLRUCache(registerer, cacheSize) 203 | case "random": 204 | cache, err = randomreplacement.NewMetricMapperRRCache(registerer, cacheSize) 205 | default: 206 | err = fmt.Errorf("unsupported cache type %q", cacheType) 207 | } 208 | 209 | if err != nil { 210 | return nil, err 211 | } 212 | } 213 | 214 | return cache, nil 215 | } 216 | -------------------------------------------------------------------------------- /collector/collector.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package collector 15 | 16 | import ( 17 | "bufio" 18 | "fmt" 19 | "io" 20 | "log/slog" 21 | "math" 22 | _ "net/http/pprof" 23 | "regexp" 24 | "strconv" 25 | "strings" 26 | "sync" 27 | "time" 28 | 29 | "github.com/prometheus/client_golang/prometheus" 30 | "github.com/prometheus/statsd_exporter/pkg/mapper" 31 | ) 32 | 33 | var invalidMetricChars = regexp.MustCompile("[^a-zA-Z0-9_:]") 34 | 35 | type graphiteCollector struct { 36 | samples map[string]*graphiteSample 37 | mu *sync.Mutex 38 | mapper metricMapper 39 | sampleCh chan *graphiteSample 40 | lineCh chan string 41 | strictMatch bool 42 | logger *slog.Logger 43 | tagParseFailures prometheus.Counter 44 | lastProcessed prometheus.Gauge 45 | sampleExpiryMetric prometheus.Gauge 46 | sampleExpiry time.Duration 47 | } 48 | 49 | func NewGraphiteCollector(logger *slog.Logger, strictMatch bool, sampleExpiry time.Duration) *graphiteCollector { 50 | c := &graphiteCollector{ 51 | sampleCh: make(chan *graphiteSample), 52 | lineCh: make(chan string), 53 | mu: &sync.Mutex{}, 54 | samples: map[string]*graphiteSample{}, 55 | strictMatch: strictMatch, 56 | logger: logger, 57 | tagParseFailures: prometheus.NewCounter( 58 | prometheus.CounterOpts{ 59 | Name: "graphite_tag_parse_failures", 60 | Help: "Total count of samples with invalid tags", 61 | }), 62 | lastProcessed: prometheus.NewGauge( 63 | prometheus.GaugeOpts{ 64 | Name: "graphite_last_processed_timestamp_seconds", 65 | Help: "Unix timestamp of the last processed graphite metric.", 66 | }, 67 | ), 68 | sampleExpiry: sampleExpiry, 69 | sampleExpiryMetric: prometheus.NewGauge( 70 | prometheus.GaugeOpts{ 71 | Name: "graphite_sample_expiry_seconds", 72 | Help: "How long in seconds a metric sample is valid for.", 73 | }, 74 | ), 75 | } 76 | c.sampleExpiryMetric.Set(sampleExpiry.Seconds()) 77 | go c.processSamples() 78 | go c.processLines() 79 | return c 80 | } 81 | 82 | func (c *graphiteCollector) ProcessReader(reader io.Reader) { 83 | lineScanner := bufio.NewScanner(reader) 84 | for { 85 | if ok := lineScanner.Scan(); !ok { 86 | break 87 | } 88 | c.lineCh <- lineScanner.Text() 89 | } 90 | } 91 | 92 | func (c *graphiteCollector) SetMapper(m metricMapper) { 93 | c.mapper = m 94 | } 95 | 96 | func (c *graphiteCollector) processLines() { 97 | for line := range c.lineCh { 98 | c.processLine(line) 99 | } 100 | } 101 | 102 | func (c *graphiteCollector) parseMetricNameAndTags(name string) (string, prometheus.Labels, error) { 103 | var err error 104 | 105 | labels := make(prometheus.Labels) 106 | 107 | parts := strings.Split(name, ";") 108 | parsedName := parts[0] 109 | 110 | tags := parts[1:] 111 | for _, tag := range tags { 112 | kv := strings.SplitN(tag, "=", 2) 113 | if len(kv) != 2 { 114 | // don't add this tag, continue processing tags but return an error 115 | c.tagParseFailures.Inc() 116 | err = fmt.Errorf("error parsing tag %s", tag) 117 | continue 118 | } 119 | 120 | k := kv[0] 121 | v := kv[1] 122 | labels[k] = v 123 | } 124 | 125 | return parsedName, labels, err 126 | } 127 | 128 | func (c *graphiteCollector) processLine(line string) { 129 | line = strings.TrimSpace(line) 130 | c.logger.Debug("Incoming line", "line", line) 131 | 132 | parts := strings.Split(line, " ") 133 | if len(parts) != 3 { 134 | c.logger.Info("Invalid part count", "parts", len(parts), "line", line) 135 | return 136 | } 137 | 138 | originalName := parts[0] 139 | 140 | parsedName, labels, err := c.parseMetricNameAndTags(originalName) 141 | if err != nil { 142 | c.logger.Debug("Invalid tags", "line", line, "err", err.Error()) 143 | } 144 | 145 | mapping, mappingLabels, mappingPresent := c.mapper.GetMapping(parsedName, mapper.MetricTypeGauge) 146 | 147 | // add mapping labels to parsed labels 148 | for k, v := range mappingLabels { 149 | labels[k] = v 150 | } 151 | 152 | if (mappingPresent && mapping.Action == mapper.ActionTypeDrop) || (!mappingPresent && c.strictMatch) { 153 | return 154 | } 155 | 156 | var name string 157 | if mappingPresent { 158 | name = invalidMetricChars.ReplaceAllString(mapping.Name, "_") 159 | } else { 160 | name = invalidMetricChars.ReplaceAllString(parsedName, "_") 161 | } 162 | 163 | value, err := strconv.ParseFloat(parts[1], 64) 164 | if err != nil { 165 | c.logger.Info("Invalid value", "line", line) 166 | return 167 | } 168 | if mappingPresent && mapping.Scale.Set { 169 | value *= mapping.Scale.Val 170 | } 171 | 172 | timestamp, err := strconv.ParseFloat(parts[2], 64) 173 | if err != nil { 174 | c.logger.Info("Invalid timestamp", "line", line) 175 | return 176 | } 177 | sample := graphiteSample{ 178 | OriginalName: originalName, 179 | Name: name, 180 | Value: value, 181 | Labels: labels, 182 | Type: prometheus.GaugeValue, 183 | Help: fmt.Sprintf("Graphite metric %s", name), 184 | Timestamp: time.Unix(int64(timestamp), int64(math.Mod(timestamp, 1.0)*1e9)), 185 | } 186 | c.logger.Debug("Processing sample", "sample", sample) 187 | c.lastProcessed.Set(float64(time.Now().UnixNano()) / 1e9) 188 | c.sampleCh <- &sample 189 | } 190 | 191 | func (c *graphiteCollector) processSamples() { 192 | ticker := time.NewTicker(time.Minute).C 193 | 194 | for { 195 | select { 196 | case sample, ok := <-c.sampleCh: 197 | if sample == nil || !ok { 198 | return 199 | } 200 | c.mu.Lock() 201 | c.samples[sample.OriginalName] = sample 202 | c.mu.Unlock() 203 | case <-ticker: 204 | // Garbage collect expired samples. 205 | ageLimit := time.Now().Add(-c.sampleExpiry) 206 | c.mu.Lock() 207 | for k, sample := range c.samples { 208 | if ageLimit.After(sample.Timestamp) { 209 | delete(c.samples, k) 210 | } 211 | } 212 | c.mu.Unlock() 213 | } 214 | } 215 | } 216 | 217 | // Collect implements prometheus.Collector. 218 | func (c graphiteCollector) Collect(ch chan<- prometheus.Metric) { 219 | c.lastProcessed.Collect(ch) 220 | c.sampleExpiryMetric.Collect(ch) 221 | c.tagParseFailures.Collect(ch) 222 | 223 | c.mu.Lock() 224 | samples := make([]*graphiteSample, 0, len(c.samples)) 225 | for _, sample := range c.samples { 226 | samples = append(samples, sample) 227 | } 228 | c.mu.Unlock() 229 | 230 | ageLimit := time.Now().Add(-c.sampleExpiry) 231 | for _, sample := range samples { 232 | if ageLimit.After(sample.Timestamp) { 233 | continue 234 | } 235 | ch <- prometheus.MustNewConstMetric( 236 | prometheus.NewDesc(sample.Name, sample.Help, []string{}, sample.Labels), 237 | sample.Type, 238 | sample.Value, 239 | ) 240 | } 241 | } 242 | 243 | // Describe implements prometheus.Collector but does not yield a description 244 | // for Graphite metrics, allowing inconsistent label sets 245 | func (c graphiteCollector) Describe(ch chan<- *prometheus.Desc) { 246 | c.lastProcessed.Describe(ch) 247 | c.sampleExpiryMetric.Describe(ch) 248 | c.tagParseFailures.Describe(ch) 249 | } 250 | 251 | type graphiteSample struct { 252 | OriginalName string 253 | Name string 254 | Labels prometheus.Labels 255 | Help string 256 | Value float64 257 | Type prometheus.ValueType 258 | Timestamp time.Time 259 | } 260 | 261 | func (s graphiteSample) String() string { 262 | return fmt.Sprintf("%#v", s) 263 | } 264 | 265 | type metricMapper interface { 266 | GetMapping(string, mapper.MetricType) (*mapper.MetricMapping, prometheus.Labels, bool) 267 | InitFromFile(string) error 268 | } 269 | -------------------------------------------------------------------------------- /collector/collector_benchmark_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package collector 15 | 16 | import ( 17 | "fmt" 18 | "strings" 19 | "testing" 20 | "time" 21 | 22 | "github.com/prometheus/client_golang/prometheus" 23 | "github.com/prometheus/common/promslog" 24 | "github.com/prometheus/statsd_exporter/pkg/mapper" 25 | ) 26 | 27 | var ( 28 | logger = promslog.NewNopLogger() 29 | c = NewGraphiteCollector(logger, false, 5*time.Minute) 30 | 31 | now = time.Now() 32 | 33 | rawInput = `rspamd.actions.add_header 2 NOW 34 | rspamd.actions;action=greylist 0 NOW 35 | rspamd.actions;action=no_action 24 NOW 36 | rspamd.actions;action=reject 1 NOW 37 | rspamd.actions;action=rewrite_subject 0 NOW 38 | rspamd.actions.soft_reject 0 NOW 39 | rspamd.bytes_allocated 4165268944 NOW 40 | rspamd.chunks_allocated 4294966730 NOW 41 | rspamd.chunks_freed 0 NOW 42 | rspamd.chunks_oversized 1 NOW 43 | rspamd.connections 1 NOW 44 | rspamd.control_connections 1 NOW 45 | rspamd.ham_count 24 NOW 46 | rspamd.learned 2 NOW 47 | rspamd.pools_allocated 59 NOW 48 | rspamd.pools_freed 171 NOW 49 | rspamd.scanned 27 NOW 50 | rspamd.shared_chunks_allocated 34 NOW 51 | rspamd.spam_count 3 NOW` 52 | rawInput2 = strings.NewReplacer("NOW", fmt.Sprintf("%d", now.Unix())).Replace(rawInput) 53 | input = strings.Split(rawInput2, "\n") 54 | 55 | // The name should be the same length to ensure the only difference is the tag parsing 56 | untaggedLine = fmt.Sprintf("rspamd.actions 2 %d", now.Unix()) 57 | taggedLine = fmt.Sprintf("rspamd.actions;action=add_header;foo=bar 2 %d", now.Unix()) 58 | ) 59 | 60 | type mockMapper struct { 61 | labels prometheus.Labels 62 | present bool 63 | name string 64 | action mapper.ActionType 65 | scale mapper.MaybeFloat64 66 | } 67 | 68 | func (m *mockMapper) GetMapping(metricName string, metricType mapper.MetricType) (*mapper.MetricMapping, prometheus.Labels, bool) { 69 | mapping := mapper.MetricMapping{ 70 | Name: m.name, 71 | Action: m.action, 72 | Scale: m.scale, 73 | } 74 | return &mapping, m.labels, m.present 75 | } 76 | 77 | func (m *mockMapper) InitFromFile(string) error { 78 | return nil 79 | } 80 | 81 | func init() { 82 | c.mapper = &mockMapper{ 83 | name: "not_used", 84 | present: false, 85 | } 86 | } 87 | 88 | func benchmarkProcessLines(times int, b *testing.B, lines []string) { 89 | for n := 0; n < b.N; n++ { 90 | for i := 0; i < times; i++ { 91 | for _, l := range lines { 92 | c.processLine(l) 93 | } 94 | } 95 | } 96 | } 97 | 98 | func benchmarkProcessLine(b *testing.B, line string) { 99 | // always report allocations since this is a hot path 100 | b.ReportAllocs() 101 | 102 | for n := 0; n < b.N; n++ { 103 | c.processLine(line) 104 | } 105 | } 106 | 107 | // Mixed lines benchmarks 108 | func BenchmarkProcessLineMixed1(b *testing.B) { 109 | benchmarkProcessLines(1, b, input) 110 | } 111 | 112 | func BenchmarkProcessLineMixed5(b *testing.B) { 113 | benchmarkProcessLines(5, b, input) 114 | } 115 | 116 | func BenchmarkProcessLineMixed50(b *testing.B) { 117 | benchmarkProcessLines(50, b, input) 118 | } 119 | 120 | // Individual line benchmarks 121 | func BenchmarkProcessLineUntagged(b *testing.B) { 122 | benchmarkProcessLine(b, untaggedLine) 123 | } 124 | 125 | func BenchmarkProcessLineTagged(b *testing.B) { 126 | benchmarkProcessLine(b, taggedLine) 127 | } 128 | -------------------------------------------------------------------------------- /collector/collector_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package collector 15 | 16 | import ( 17 | "strings" 18 | "testing" 19 | "time" 20 | 21 | "github.com/prometheus/client_golang/prometheus" 22 | "github.com/prometheus/common/promslog" 23 | "github.com/prometheus/statsd_exporter/pkg/mapper" 24 | "github.com/stretchr/testify/assert" 25 | ) 26 | 27 | func TestParseNameAndTags(t *testing.T) { 28 | logger := promslog.NewNopLogger() 29 | c := NewGraphiteCollector(logger, false, 5*time.Minute) 30 | type testCase struct { 31 | line string 32 | parsedName string 33 | labels prometheus.Labels 34 | willError bool 35 | } 36 | 37 | testCases := map[string]testCase{ 38 | "good tags": { 39 | line: "my_simple_metric_with_tags;tag1=value1;tag2=value2", 40 | parsedName: "my_simple_metric_with_tags", 41 | labels: prometheus.Labels{ 42 | "tag1": "value1", 43 | "tag2": "value2", 44 | }, 45 | }, 46 | "no tag value": { 47 | line: "my_simple_metric_with_bad_tags;tag1=value3;tag2", 48 | parsedName: "my_simple_metric_with_bad_tags", 49 | labels: prometheus.Labels{ 50 | "tag1": "value3", 51 | }, 52 | willError: true, 53 | }, 54 | "no tag value in middle": { 55 | line: "my_simple_metric_with_bad_tags;tag1=value3;tag2;tag3=value4", 56 | parsedName: "my_simple_metric_with_bad_tags", 57 | labels: prometheus.Labels{ 58 | "tag1": "value3", 59 | "tag3": "value4", 60 | }, 61 | willError: true, 62 | }, 63 | } 64 | 65 | for name, testCase := range testCases { 66 | t.Run(name, func(t *testing.T) { 67 | n, parsedLabels, err := c.parseMetricNameAndTags(testCase.line) 68 | if !testCase.willError { 69 | assert.NoError(t, err, "Got unexpected error parsing %s", testCase.line) 70 | } 71 | assert.Equal(t, testCase.parsedName, n) 72 | assert.Equal(t, testCase.labels, parsedLabels) 73 | }) 74 | } 75 | } 76 | 77 | func TestProcessLine(t *testing.T) { 78 | type testCase struct { 79 | line string 80 | name string 81 | mappingLabels prometheus.Labels 82 | sampleLabels prometheus.Labels 83 | value float64 84 | mappingPresent bool 85 | willFail bool 86 | action mapper.ActionType 87 | strict bool 88 | scale mapper.MaybeFloat64 89 | } 90 | 91 | testCases := map[string]testCase{ 92 | "simple metric": { 93 | line: "my.simple.metric 9001 1534620625", 94 | name: "my_simple_metric", 95 | mappingLabels: prometheus.Labels{ 96 | "foo": "bar", 97 | "zip": "zot", 98 | "name": "alabel", 99 | }, 100 | sampleLabels: prometheus.Labels{ 101 | "foo": "bar", 102 | "zip": "zot", 103 | "name": "alabel", 104 | }, 105 | mappingPresent: true, 106 | value: float64(9001), 107 | }, 108 | "existing metric with different labels is accepted": { 109 | line: "my.simple.metric.baz 9002 1534620625", 110 | name: "my_simple_metric", 111 | mappingLabels: prometheus.Labels{ 112 | "baz": "bat", 113 | }, 114 | sampleLabels: prometheus.Labels{ 115 | "baz": "bat", 116 | }, 117 | mappingPresent: true, 118 | value: float64(9002), 119 | willFail: false, 120 | }, 121 | "mapped metric": { 122 | line: "my.simple.metric.new.baz 9002 1534620625", 123 | name: "my_simple_metric_new", 124 | mappingLabels: prometheus.Labels{ 125 | "baz": "bat", 126 | }, 127 | sampleLabels: prometheus.Labels{ 128 | "baz": "bat", 129 | }, 130 | mappingPresent: true, 131 | value: float64(9002), 132 | }, 133 | "no mapping metric": { 134 | line: "my.nomap.metric 9001 1534620625", 135 | name: "my_nomap_metric", 136 | value: float64(9001), 137 | sampleLabels: prometheus.Labels{}, 138 | mappingPresent: false, 139 | }, 140 | "no mapping metric with no value": { 141 | line: "my.nomap.metric.novalue 9001 ", 142 | name: "my_nomap_metric_novalue", 143 | value: float64(9001), 144 | willFail: true, 145 | }, 146 | "mapping type drop": { 147 | line: "my.mapped.metric.drop 55 1534620625", 148 | name: "my_mapped_metric_drop", 149 | mappingPresent: true, 150 | willFail: true, 151 | action: mapper.ActionTypeDrop, 152 | }, 153 | "strict mapped metric": { 154 | line: "my.mapped.strict.metric 55 1534620625", 155 | name: "my_mapped_strict_metric", 156 | value: float64(55), 157 | mappingPresent: true, 158 | mappingLabels: prometheus.Labels{}, 159 | sampleLabels: prometheus.Labels{}, 160 | willFail: false, 161 | strict: true, 162 | }, 163 | "strict unmapped metric will drop": { 164 | line: "my.mapped.strict.metric.drop 55 1534620625", 165 | name: "my_mapped_strict_metric_drop", 166 | mappingPresent: false, 167 | willFail: true, 168 | strict: true, 169 | }, 170 | "unmapped metric with tags": { 171 | line: "my.simple.metric.with.tags;tag1=value1;tag2=value2 9002 1534620625", 172 | name: "my_simple_metric_with_tags", 173 | sampleLabels: prometheus.Labels{ 174 | "tag1": "value1", 175 | "tag2": "value2", 176 | }, 177 | mappingPresent: false, 178 | value: float64(9002), 179 | }, 180 | "unmapped metric with different tag values": { 181 | // same tags, different values, should parse 182 | line: "my.simple.metric.with.tags;tag1=value3;tag2=value4 9002 1534620625", 183 | name: "my_simple_metric_with_tags", 184 | sampleLabels: prometheus.Labels{ 185 | "tag1": "value3", 186 | "tag2": "value4", 187 | }, 188 | mappingPresent: false, 189 | value: float64(9002), 190 | }, 191 | "mapping labels added to tags": { 192 | // labels in mapping should be added to sample labels 193 | line: "my.mapped.metric.with.tags;tag1=value3;tag2=value4 9003 1534620625", 194 | name: "my_mapped_metric_with_tags", 195 | mappingLabels: prometheus.Labels{ 196 | "foobar": "baz", 197 | }, 198 | sampleLabels: prometheus.Labels{ 199 | "tag1": "value3", 200 | "tag2": "value4", 201 | "foobar": "baz", 202 | }, 203 | mappingPresent: true, 204 | value: float64(9003), 205 | }, 206 | "scaled metric": { 207 | line: "my.scaled.metric 9001 1534620625", 208 | name: "my_scaled_metric", 209 | scale: mapper.MaybeFloat64{ 210 | Set: true, 211 | Val: 1024, 212 | }, 213 | sampleLabels: prometheus.Labels{}, 214 | mappingPresent: true, 215 | value: float64(9001) * 1024, 216 | }, 217 | } 218 | 219 | c := NewGraphiteCollector(promslog.NewNopLogger(), false, 5*time.Minute) 220 | 221 | for _, testCase := range testCases { 222 | if testCase.mappingPresent { 223 | c.mapper = &mockMapper{ 224 | name: testCase.name, 225 | labels: testCase.mappingLabels, 226 | action: testCase.action, 227 | present: testCase.mappingPresent, 228 | scale: testCase.scale, 229 | } 230 | } else { 231 | c.mapper = &mockMapper{ 232 | present: testCase.mappingPresent, 233 | } 234 | } 235 | 236 | c.strictMatch = testCase.strict 237 | c.processLine(testCase.line) 238 | } 239 | 240 | c.sampleCh <- nil 241 | for name, k := range testCases { 242 | t.Run(name, func(t *testing.T) { 243 | originalName := strings.Split(k.line, " ")[0] 244 | sample := c.samples[originalName] 245 | if k.willFail { 246 | assert.Nil(t, sample, "Found %s", k.name) 247 | } else { 248 | if assert.NotNil(t, sample, "Missing %s", k.name) { 249 | assert.Equal(t, k.name, sample.Name) 250 | assert.Equal(t, k.sampleLabels, sample.Labels) 251 | assert.Equal(t, k.value, sample.Value) 252 | } 253 | } 254 | }) 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /e2e/e2e_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package e2e 15 | 16 | import ( 17 | "fmt" 18 | "io" 19 | "net" 20 | "net/http" 21 | "os" 22 | "os/exec" 23 | "path" 24 | "path/filepath" 25 | "strings" 26 | "testing" 27 | "time" 28 | ) 29 | 30 | func TestIssue61(t *testing.T) { 31 | cwd, err := os.Getwd() 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | 36 | webAddr, graphiteAddr := fmt.Sprintf("127.0.0.1:%d", 9108), fmt.Sprintf("127.0.0.1:%d", 9109) 37 | exporter := exec.Command( 38 | filepath.Join(cwd, "..", "graphite_exporter"), 39 | "--web.listen-address", webAddr, 40 | "--graphite.listen-address", graphiteAddr, 41 | "--graphite.mapping-config", filepath.Join(cwd, "fixtures", "mapping.yml"), 42 | ) 43 | err = exporter.Start() 44 | if err != nil { 45 | t.Fatalf("execution error: %v", err) 46 | } 47 | defer exporter.Process.Kill() 48 | 49 | for i := 0; i < 20; i++ { 50 | if i > 0 { 51 | time.Sleep(1 * time.Second) 52 | } 53 | resp, err := http.Get("http://" + webAddr) 54 | if err != nil { 55 | continue 56 | } 57 | defer resp.Body.Close() 58 | if resp.StatusCode == http.StatusOK { 59 | break 60 | } 61 | } 62 | 63 | now := time.Now() 64 | 65 | input := `rspamd.actions.add_header 2 NOW 66 | rspamd.actions.greylist 0 NOW 67 | rspamd.actions.no_action 24 NOW 68 | rspamd.actions.reject 1 NOW 69 | rspamd.actions.rewrite_subject 0 NOW 70 | rspamd.actions.soft_reject 0 NOW 71 | rspamd.bytes_allocated 4165268944 NOW 72 | rspamd.chunks_allocated 4294966730 NOW 73 | rspamd.chunks_freed 0 NOW 74 | rspamd.chunks_oversized 1 NOW 75 | rspamd.connections 1 NOW 76 | rspamd.control_connections 1 NOW 77 | rspamd.ham_count 24 NOW 78 | rspamd.learned 2 NOW 79 | rspamd.pools_allocated 59 NOW 80 | rspamd.pools_freed 171 NOW 81 | rspamd.scanned 27 NOW 82 | rspamd.shared_chunks_allocated 34 NOW 83 | rspamd.spam_count 3 NOW` 84 | input = strings.NewReplacer("NOW", fmt.Sprintf("%d", now.Unix())).Replace(input) 85 | 86 | conn, err := net.Dial("tcp", graphiteAddr) 87 | if err != nil { 88 | t.Fatalf("connection error: %v", err) 89 | } 90 | defer conn.Close() 91 | _, err = conn.Write([]byte(input)) 92 | if err != nil { 93 | t.Fatalf("write error: %v", err) 94 | } 95 | 96 | time.Sleep(5 * time.Second) 97 | 98 | resp, err := http.Get("http://" + path.Join(webAddr, "metrics")) 99 | if err != nil { 100 | t.Fatalf("get error: %v", err) 101 | } 102 | defer resp.Body.Close() 103 | b, err := io.ReadAll(resp.Body) 104 | if err != nil { 105 | t.Fatalf("read error: %v", err) 106 | } 107 | for _, s := range []string{"rspamd_actions{action=\"add_header\"} 2", "rspamd_connections 1"} { 108 | if !strings.Contains(string(b), s) { 109 | t.Fatalf("Expected %q in %q – input: %q – time: %s", s, string(b), input, now) 110 | } 111 | } 112 | } 113 | 114 | func TestIssue111(t *testing.T) { 115 | cwd, err := os.Getwd() 116 | if err != nil { 117 | t.Fatal(err) 118 | } 119 | 120 | webAddr, graphiteAddr := fmt.Sprintf("127.0.0.1:%d", 9108), fmt.Sprintf("127.0.0.1:%d", 9109) 121 | exporter := exec.Command( 122 | filepath.Join(cwd, "..", "graphite_exporter"), 123 | "--web.listen-address", webAddr, 124 | "--graphite.listen-address", graphiteAddr, 125 | ) 126 | err = exporter.Start() 127 | if err != nil { 128 | t.Fatalf("execution error: %v", err) 129 | } 130 | defer exporter.Process.Kill() 131 | 132 | for i := 0; i < 20; i++ { 133 | if i > 0 { 134 | time.Sleep(1 * time.Second) 135 | } 136 | resp, err := http.Get("http://" + webAddr) 137 | if err != nil { 138 | continue 139 | } 140 | defer resp.Body.Close() 141 | if resp.StatusCode == http.StatusOK { 142 | break 143 | } 144 | } 145 | 146 | now := time.Now() 147 | 148 | input := `rspamd.actions.add_header 2 NOW 149 | rspamd.actions.greylist 0 NOW 150 | rspamd.actions.no_action 24 NOW 151 | rspamd.actions.reject 1 NOW 152 | rspamd.actions.rewrite_subject 0 NOW 153 | rspamd.actions.soft_reject 0 NOW 154 | rspamd.bytes_allocated 4165268944 NOW 155 | rspamd.chunks_allocated 4294966730 NOW 156 | rspamd.chunks_freed 0 NOW 157 | rspamd.chunks_oversized 1 NOW 158 | rspamd.connections 1 NOW 159 | rspamd.control_connections 1 NOW 160 | rspamd.ham_count 24 NOW 161 | rspamd.learned 2 NOW 162 | rspamd.pools_allocated 59 NOW 163 | rspamd.pools_freed 171 NOW 164 | rspamd.scanned 27 NOW 165 | rspamd.shared_chunks_allocated 34 NOW 166 | rspamd.spam_count 3 NOW` 167 | input = strings.NewReplacer("NOW", fmt.Sprintf("%d", now.Unix())).Replace(input) 168 | 169 | conn, err := net.Dial("tcp", graphiteAddr) 170 | if err != nil { 171 | t.Fatalf("connection error: %v", err) 172 | } 173 | defer conn.Close() 174 | _, err = conn.Write([]byte(input)) 175 | if err != nil { 176 | t.Fatalf("write error: %v", err) 177 | } 178 | 179 | time.Sleep(5 * time.Second) 180 | 181 | resp, err := http.Get("http://" + path.Join(webAddr, "metrics")) 182 | if err != nil { 183 | t.Fatalf("get error: %v", err) 184 | } 185 | defer resp.Body.Close() 186 | b, err := io.ReadAll(resp.Body) 187 | if err != nil { 188 | t.Fatalf("read error: %v", err) 189 | } 190 | for _, s := range []string{"rspamd_actions_add_header 2", "rspamd_connections 1"} { 191 | if !strings.Contains(string(b), s) { 192 | t.Fatalf("Expected %q in %q – input: %q – time: %s", s, string(b), input, now) 193 | } 194 | } 195 | } 196 | 197 | // Test to ensure that inconsistent label sets are accepted and exported correctly 198 | func TestInconsistentLabelsE2E(t *testing.T) { 199 | cwd, err := os.Getwd() 200 | if err != nil { 201 | t.Fatal(err) 202 | } 203 | 204 | webAddr, graphiteAddr := fmt.Sprintf("127.0.0.1:%d", 9108), fmt.Sprintf("127.0.0.1:%d", 9109) 205 | exporter := exec.Command( 206 | filepath.Join(cwd, "..", "graphite_exporter"), 207 | "--web.listen-address", webAddr, 208 | "--graphite.listen-address", graphiteAddr, 209 | "--graphite.mapping-config", filepath.Join(cwd, "fixtures", "mapping.yml"), 210 | ) 211 | err = exporter.Start() 212 | if err != nil { 213 | t.Fatalf("execution error: %v", err) 214 | } 215 | defer exporter.Process.Kill() 216 | 217 | for i := 0; i < 20; i++ { 218 | if i > 0 { 219 | time.Sleep(1 * time.Second) 220 | } 221 | resp, err := http.Get("http://" + webAddr) 222 | if err != nil { 223 | continue 224 | } 225 | defer resp.Body.Close() 226 | if resp.StatusCode == http.StatusOK { 227 | break 228 | } 229 | } 230 | 231 | now := time.Now() 232 | 233 | input := `rspamd.actions;action=add_header 2 NOW 234 | rspamd.actions;action=greylist 0 NOW 235 | rspamd.actions;action2=add_header 2 NOW 236 | rspamd.actions;action2=greylist 0 NOW 237 | ` 238 | input = strings.NewReplacer("NOW", fmt.Sprintf("%d", now.Unix())).Replace(input) 239 | 240 | output := []string{ 241 | "rspamd_actions{action=\"add_header\"} 2", 242 | "rspamd_actions{action=\"greylist\"} 0", 243 | "rspamd_actions{action2=\"add_header\"} 2", 244 | "rspamd_actions{action2=\"greylist\"} 0", 245 | } 246 | 247 | conn, err := net.Dial("tcp", graphiteAddr) 248 | if err != nil { 249 | t.Fatalf("connection error: %v", err) 250 | } 251 | defer conn.Close() 252 | _, err = conn.Write([]byte(input)) 253 | if err != nil { 254 | t.Fatalf("write error: %v", err) 255 | } 256 | 257 | time.Sleep(5 * time.Second) 258 | 259 | resp, err := http.Get("http://" + path.Join(webAddr, "metrics")) 260 | if err != nil { 261 | t.Fatalf("get error: %v", err) 262 | } 263 | defer resp.Body.Close() 264 | b, err := io.ReadAll(resp.Body) 265 | if err != nil { 266 | t.Fatalf("read error: %v", err) 267 | } 268 | for _, s := range output { 269 | if !strings.Contains(string(b), s) { 270 | t.Fatalf("Expected %q in %q – input: %q – time: %s", s, string(b), input, now) 271 | } 272 | } 273 | } 274 | 275 | // Test to ensure that inconsistent label sets are accepted and exported correctly 276 | func TestBacktracking(t *testing.T) { 277 | cwd, err := os.Getwd() 278 | if err != nil { 279 | t.Fatal(err) 280 | } 281 | 282 | webAddr, graphiteAddr := fmt.Sprintf("127.0.0.1:%d", 9108), fmt.Sprintf("127.0.0.1:%d", 9109) 283 | exporter := exec.Command( 284 | filepath.Join(cwd, "..", "graphite_exporter"), 285 | "--web.listen-address", webAddr, 286 | "--graphite.listen-address", graphiteAddr, 287 | "--graphite.mapping-config", filepath.Join(cwd, "fixtures", "backtrack.yml"), 288 | ) 289 | err = exporter.Start() 290 | if err != nil { 291 | t.Fatalf("execution error: %v", err) 292 | } 293 | defer exporter.Process.Kill() 294 | 295 | for i := 0; i < 20; i++ { 296 | if i > 0 { 297 | time.Sleep(1 * time.Second) 298 | } 299 | resp, err := http.Get("http://" + webAddr) 300 | if err != nil { 301 | continue 302 | } 303 | defer resp.Body.Close() 304 | if resp.StatusCode == http.StatusOK { 305 | break 306 | } 307 | } 308 | 309 | now := time.Now() 310 | 311 | input := `a.x.x.b 1 NOW 312 | a.x.y.b 2 NOW 313 | a.b.c.b 3 NOW 314 | a.b.c.d 4 NOW 315 | ` 316 | input = strings.NewReplacer("NOW", fmt.Sprintf("%d", now.Unix())).Replace(input) 317 | 318 | output := []string{ 319 | "axxb{one=\"x\",two=\"x\"} 1", 320 | "axxb{one=\"x\",two=\"y\"} 2", 321 | "axxb{one=\"b\",two=\"c\"} 3", 322 | "abcd 4", 323 | } 324 | 325 | conn, err := net.Dial("tcp", graphiteAddr) 326 | if err != nil { 327 | t.Fatalf("connection error: %v", err) 328 | } 329 | defer conn.Close() 330 | _, err = conn.Write([]byte(input)) 331 | if err != nil { 332 | t.Fatalf("write error: %v", err) 333 | } 334 | 335 | time.Sleep(5 * time.Second) 336 | 337 | resp, err := http.Get("http://" + path.Join(webAddr, "metrics")) 338 | if err != nil { 339 | t.Fatalf("get error: %v", err) 340 | } 341 | defer resp.Body.Close() 342 | b, err := io.ReadAll(resp.Body) 343 | if err != nil { 344 | t.Fatalf("read error: %v", err) 345 | } 346 | for _, s := range output { 347 | if !strings.Contains(string(b), s) { 348 | t.Fatalf("Expected %q in %q – input: %q – time: %s", s, string(b), input, now) 349 | } 350 | } 351 | } 352 | -------------------------------------------------------------------------------- /e2e/fixtures/backtrack.yml: -------------------------------------------------------------------------------- 1 | mappings: 2 | - match: a.*.*.b 3 | name: axxb 4 | labels: 5 | one: $1 6 | two: $2 7 | - match: a.b.c.d 8 | name: abcd 9 | -------------------------------------------------------------------------------- /e2e/fixtures/issue90.yml: -------------------------------------------------------------------------------- 1 | defaults: 2 | glob_disable_ordering: true 3 | mappings: 4 | - match: flink.tsk.*.*.*.*.* 5 | name: flink_${4}_${5} 6 | labels: 7 | job: $1 8 | subtask: $3 9 | task: $2 10 | -------------------------------------------------------------------------------- /e2e/fixtures/mapping.yml: -------------------------------------------------------------------------------- 1 | mappings: 2 | - match: rspamd.actions.* 3 | name: rspamd_actions 4 | labels: 5 | action: $1 6 | - match: rspamd.* 7 | name: rspamd_${1} 8 | -------------------------------------------------------------------------------- /e2e/issue90_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package e2e 15 | 16 | import ( 17 | "bytes" 18 | "fmt" 19 | "io" 20 | "net" 21 | "net/http" 22 | "os" 23 | "os/exec" 24 | "path" 25 | "path/filepath" 26 | "strconv" 27 | "testing" 28 | "time" 29 | ) 30 | 31 | func TestIssue90(t *testing.T) { 32 | cwd, err := os.Getwd() 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | 37 | webAddr, graphiteAddr := fmt.Sprintf("127.0.0.1:%d", 9118), fmt.Sprintf("127.0.0.1:%d", 9119) 38 | exporter := exec.Command( 39 | filepath.Join(cwd, "..", "graphite_exporter"), 40 | "--graphite.mapping-strict-match", 41 | "--web.listen-address", webAddr, 42 | "--graphite.listen-address", graphiteAddr, 43 | "--graphite.mapping-config", filepath.Join(cwd, "fixtures", "issue90.yml"), 44 | ) 45 | err = exporter.Start() 46 | if err != nil { 47 | t.Fatalf("execution error: %v", err) 48 | } 49 | defer exporter.Process.Kill() 50 | 51 | for i := 0; i < 20; i++ { 52 | if i > 0 { 53 | time.Sleep(1 * time.Second) 54 | } 55 | resp, err := http.Get("http://" + webAddr) 56 | if err != nil { 57 | continue 58 | } 59 | defer resp.Body.Close() 60 | if resp.StatusCode == http.StatusOK { 61 | break 62 | } 63 | } 64 | 65 | testInputs, err := os.ReadFile(filepath.Join(cwd, "fixtures", "issue90_in.txt")) 66 | if err != nil { 67 | t.Fatalf("failed to read input fixture: %v", err) 68 | } 69 | 70 | lines := bytes.Split(testInputs, []byte{'\n'}) 71 | currSec := time.Now().Unix() - 2 72 | for _, input := range lines { 73 | conn, err := net.Dial("udp", graphiteAddr) 74 | updateInput := bytes.ReplaceAll(input, []byte("NOW"), []byte(strconv.FormatInt(currSec, 10))) 75 | 76 | if err != nil { 77 | t.Fatalf("connection error: %v", err) 78 | } 79 | _, err = conn.Write(updateInput) 80 | if err != nil { 81 | t.Fatalf("write error: %v", err) 82 | } 83 | conn.Close() 84 | } 85 | 86 | time.Sleep(5 * time.Second) 87 | 88 | resp, err := http.Get("http://" + path.Join(webAddr, "metrics")) 89 | if err != nil { 90 | t.Fatalf("get error: %v", err) 91 | } 92 | defer resp.Body.Close() 93 | b, err := io.ReadAll(resp.Body) 94 | if err != nil { 95 | t.Fatalf("read error: %v", err) 96 | } 97 | 98 | if resp.StatusCode != 200 { 99 | t.Errorf("unexpected status, want 200, got %v, body: %s", resp.Status, b) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/prometheus/graphite_exporter 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/alecthomas/kingpin/v2 v2.4.0 7 | github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b 8 | github.com/go-graphite/go-whisper v0.0.0-20230526115116-e3110f57c01c 9 | github.com/prometheus/client_golang v1.22.0 10 | github.com/prometheus/common v0.64.0 11 | github.com/prometheus/exporter-toolkit v0.13.1 12 | github.com/prometheus/prometheus v0.300.1 13 | github.com/prometheus/statsd_exporter v0.28.0 14 | github.com/stretchr/testify v1.10.0 15 | ) 16 | 17 | require ( 18 | cloud.google.com/go/auth v0.9.5 // indirect 19 | cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect 20 | cloud.google.com/go/compute/metadata v0.5.2 // indirect 21 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0 // indirect 22 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 // indirect 23 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect 24 | github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect 25 | github.com/aws/aws-sdk-go v1.55.5 // indirect 26 | github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 // indirect 27 | github.com/beorn7/perks v1.0.1 // indirect 28 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 29 | github.com/coreos/go-systemd/v22 v22.5.0 // indirect 30 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 31 | github.com/dennwc/varint v1.0.0 // indirect 32 | github.com/felixge/httpsnoop v1.0.4 // indirect 33 | github.com/go-logr/logr v1.4.2 // indirect 34 | github.com/go-logr/stdr v1.2.2 // indirect 35 | github.com/golang-jwt/jwt/v5 v5.2.1 // indirect 36 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 37 | github.com/golang/snappy v0.0.4 // indirect 38 | github.com/google/go-cmp v0.7.0 // indirect 39 | github.com/google/s2a-go v0.1.8 // indirect 40 | github.com/google/uuid v1.6.0 // indirect 41 | github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect 42 | github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect 43 | github.com/jmespath/go-jmespath v0.4.0 // indirect 44 | github.com/jpillora/backoff v1.0.0 // indirect 45 | github.com/klauspost/compress v1.18.0 // indirect 46 | github.com/kylelemons/godebug v1.1.0 // indirect 47 | github.com/mdlayher/socket v0.5.1 // indirect 48 | github.com/mdlayher/vsock v1.2.1 // indirect 49 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 50 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect 51 | github.com/oklog/ulid v1.3.1 // indirect 52 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect 53 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 54 | github.com/prometheus/client_model v0.6.2 // indirect 55 | github.com/prometheus/common/sigv4 v0.1.0 // indirect 56 | github.com/prometheus/procfs v0.15.1 // indirect 57 | github.com/xhit/go-str2duration/v2 v2.1.0 // indirect 58 | go.opencensus.io v0.24.0 // indirect 59 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 // indirect 60 | go.opentelemetry.io/otel v1.31.0 // indirect 61 | go.opentelemetry.io/otel/metric v1.31.0 // indirect 62 | go.opentelemetry.io/otel/trace v1.31.0 // indirect 63 | go.uber.org/atomic v1.11.0 // indirect 64 | go.uber.org/goleak v1.3.0 // indirect 65 | golang.org/x/crypto v0.38.0 // indirect 66 | golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect 67 | golang.org/x/net v0.40.0 // indirect 68 | golang.org/x/oauth2 v0.30.0 // indirect 69 | golang.org/x/sync v0.14.0 // indirect 70 | golang.org/x/sys v0.33.0 // indirect 71 | golang.org/x/text v0.25.0 // indirect 72 | golang.org/x/time v0.6.0 // indirect 73 | google.golang.org/api v0.199.0 // indirect 74 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect 75 | google.golang.org/grpc v1.67.1 // indirect 76 | google.golang.org/protobuf v1.36.6 // indirect 77 | gopkg.in/yaml.v2 v2.4.0 // indirect 78 | gopkg.in/yaml.v3 v3.0.1 // indirect 79 | k8s.io/apimachinery v0.31.1 // indirect 80 | k8s.io/client-go v0.31.1 // indirect 81 | k8s.io/klog/v2 v2.130.1 // indirect 82 | k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect 83 | ) 84 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= 5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 6 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 7 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 8 | cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= 9 | cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= 10 | cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= 11 | cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= 12 | cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= 13 | cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= 14 | cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= 15 | cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= 16 | cloud.google.com/go/auth v0.9.5 h1:4CTn43Eynw40aFVr3GpPqsQponx2jv0BQpjvajsbbzw= 17 | cloud.google.com/go/auth v0.9.5/go.mod h1:Xo0n7n66eHyOWWCnitop6870Ilwo3PiZyodVkkH1xWM= 18 | cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY= 19 | cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc= 20 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 21 | cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= 22 | cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= 23 | cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= 24 | cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= 25 | cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= 26 | cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo= 27 | cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k= 28 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 29 | cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= 30 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 31 | cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= 32 | cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= 33 | cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= 34 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 35 | cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= 36 | cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= 37 | cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= 38 | cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= 39 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 40 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0 h1:nyQWyZvwGTvunIMxi1Y9uXkcyr+I7TeNrr/foo4Kpk8= 41 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0/go.mod h1:l38EPgmsp71HHLq9j7De57JcKOWPyhrsW1Awm1JS6K0= 42 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 h1:tfLQ34V6F7tVSwoTf/4lH5sE0o6eCJuNDTmH09nDpbc= 43 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg= 44 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY= 45 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY= 46 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5 v5.7.0 h1:LkHbJbgF3YyvC53aqYGR+wWQDn2Rdp9AQdGndf9QvY4= 47 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5 v5.7.0/go.mod h1:QyiQdW4f4/BIfB8ZutZ2s+28RAgfa/pT+zS++ZHyM1I= 48 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v4 v4.3.0 h1:bXwSugBiSbgtz7rOtbfGf+woewp4f06orW9OP5BjHLA= 49 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v4 v4.3.0/go.mod h1:Y/HgrePTmGy9HjdSGTqZNa+apUpTVIEVKXJyARP2lrk= 50 | github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= 51 | github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= 52 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 53 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 54 | github.com/Code-Hex/go-generics-cache v1.5.1 h1:6vhZGc5M7Y/YD8cIUcY8kcuQLB4cHR7U+0KMqAA0KcU= 55 | github.com/Code-Hex/go-generics-cache v1.5.1/go.mod h1:qxcC9kRVrct9rHeiYpFWSoW1vxyillCVzX13KZG8dl4= 56 | github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= 57 | github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= 58 | github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY= 59 | github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= 60 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 61 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 62 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 63 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 64 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= 65 | github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0= 66 | github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs= 67 | github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA= 68 | github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= 69 | github.com/aws/aws-sdk-go v1.38.35/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= 70 | github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU= 71 | github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= 72 | github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 h1:6df1vn4bBlDDo4tARvBm7l6KA9iVMnE3NWizDeWSrps= 73 | github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3/go.mod h1:CIWtjkly68+yqLPbvwwR/fjNJA/idrtULjZWh2v1ys0= 74 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 75 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 76 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 77 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 78 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 79 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 80 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 81 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 82 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 83 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 84 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 85 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 86 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 87 | github.com/cncf/xds/go v0.0.0-20240723142845-024c85f92f20 h1:N+3sFI5GUjRKBi+i0TxYVST9h4Ie192jJWpHvthBBgg= 88 | github.com/cncf/xds/go v0.0.0-20240723142845-024c85f92f20/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= 89 | github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= 90 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 91 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 92 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 93 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 94 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 95 | github.com/dennwc/varint v1.0.0 h1:kGNFFSSw8ToIy3obO/kKr8U9GZYUAxQEVuix4zfDWzE= 96 | github.com/dennwc/varint v1.0.0/go.mod h1:hnItb35rvZvJrbTALZtY/iQfDs48JKRG1RPpgziApxA= 97 | github.com/digitalocean/godo v1.126.0 h1:+Znh7VMQj/E8ArbjWnc7OKGjWfzC+I8OCSRp7r1MdD8= 98 | github.com/digitalocean/godo v1.126.0/go.mod h1:PU8JB6I1XYkQIdHFop8lLAY9ojp6M0XcU0TWaQSxbrc= 99 | github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= 100 | github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= 101 | github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI= 102 | github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 103 | github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= 104 | github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= 105 | github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= 106 | github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 107 | github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= 108 | github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 109 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 110 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 111 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 112 | github.com/envoyproxy/go-control-plane v0.13.0 h1:HzkeUz1Knt+3bK+8LG1bxOO/jzWZmdxpwC51i202les= 113 | github.com/envoyproxy/go-control-plane v0.13.0/go.mod h1:GRaKG3dwvFoTg4nj7aXdZnvMg4d7nvT/wl9WgVXn3Q8= 114 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 115 | github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM= 116 | github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4= 117 | github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= 118 | github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= 119 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 120 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 121 | github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= 122 | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 123 | github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= 124 | github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= 125 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 126 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 127 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 128 | github.com/go-graphite/go-whisper v0.0.0-20230526115116-e3110f57c01c h1:Edc/U64p3BAlAm/V8WLjEwK4WxC2eq1FOyLw3yO99zg= 129 | github.com/go-graphite/go-whisper v0.0.0-20230526115116-e3110f57c01c/go.mod h1:1edRhfqJoiHSjN72nYptHK0YR4yYt8aPC4NRHWhT0XE= 130 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 131 | github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 132 | github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= 133 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 134 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 135 | github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= 136 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 137 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 138 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 139 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 140 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 141 | github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q= 142 | github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs= 143 | github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU= 144 | github.com/go-openapi/jsonreference v0.20.4/go.mod h1:5pZJyJP2MnYCpoeoMAql78cCHauHj0V9Lhc506VOpw4= 145 | github.com/go-openapi/swag v0.22.9 h1:XX2DssF+mQKM2DHsbgZK74y/zj4mo9I99+89xUmuZCE= 146 | github.com/go-openapi/swag v0.22.9/go.mod h1:3/OXnFfnMAwBD099SwYRk7GD3xOrr1iL7d/XNLXVVwE= 147 | github.com/go-resty/resty/v2 v2.13.1 h1:x+LHXBI2nMB1vqndymf26quycC4aggYJ7DECYbiz03g= 148 | github.com/go-resty/resty/v2 v2.13.1/go.mod h1:GznXlLxkq6Nh4sU59rPmUw3VtgpO3aS96ORAI6Q7d+0= 149 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 150 | github.com/go-zookeeper/zk v1.0.4 h1:DPzxraQx7OrPyXq2phlGlNSIyWEsAox0RJmjTseMV6I= 151 | github.com/go-zookeeper/zk v1.0.4/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw= 152 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 153 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 154 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 155 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 156 | github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= 157 | github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 158 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 159 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 160 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 161 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 162 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 163 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 164 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 165 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 166 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 167 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 168 | github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 169 | github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 170 | github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 171 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 172 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 173 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 174 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 175 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 176 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 177 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 178 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 179 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 180 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 181 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 182 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 183 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 184 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 185 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 186 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 187 | github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= 188 | github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 189 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 190 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 191 | github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= 192 | github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= 193 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 194 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 195 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 196 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 197 | github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 198 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 199 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 200 | github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 201 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 202 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 203 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 204 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 205 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= 206 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 207 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 208 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 209 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 210 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 211 | github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 212 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 213 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 214 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 215 | github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 216 | github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 217 | github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 218 | github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 219 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 220 | github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= 221 | github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= 222 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 223 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 224 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 225 | github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= 226 | github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= 227 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 228 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 229 | github.com/gophercloud/gophercloud v1.14.1 h1:DTCNaTVGl8/cFu58O1JwWgis9gtISAFONqpMKNg/Vpw= 230 | github.com/gophercloud/gophercloud v1.14.1/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM= 231 | github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= 232 | github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 233 | github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248= 234 | github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk= 235 | github.com/hashicorp/consul/api v1.29.4 h1:P6slzxDLBOxUSj3fWo2o65VuKtbtOXFi7TSSgtXutuE= 236 | github.com/hashicorp/consul/api v1.29.4/go.mod h1:HUlfw+l2Zy68ceJavv2zAyArl2fqhGWnMycyt56sBgg= 237 | github.com/hashicorp/cronexpr v1.1.2 h1:wG/ZYIKT+RT3QkOdgYc+xsKWVRgnxJ1OJtjjy84fJ9A= 238 | github.com/hashicorp/cronexpr v1.1.2/go.mod h1:P4wA0KBl9C5q2hABiMO7cp6jcIg96CDh1Efb3g1PWA4= 239 | github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 240 | github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 241 | github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= 242 | github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= 243 | github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= 244 | github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= 245 | github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= 246 | github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= 247 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 248 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 249 | github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= 250 | github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= 251 | github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= 252 | github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= 253 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 254 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 255 | github.com/hashicorp/golang-lru v0.6.0 h1:uL2shRDx7RTrOrTCUZEGP/wJUFiUI8QT6E7z5o8jga4= 256 | github.com/hashicorp/golang-lru v0.6.0/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= 257 | github.com/hashicorp/nomad/api v0.0.0-20240717122358-3d93bd3778f3 h1:fgVfQ4AC1avVOnu2cfms8VAiD8lUq3vWI8mTocOXN/w= 258 | github.com/hashicorp/nomad/api v0.0.0-20240717122358-3d93bd3778f3/go.mod h1:svtxn6QnrQ69P23VvIWMR34tg3vmwLz4UdUzm1dSCgE= 259 | github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY= 260 | github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= 261 | github.com/hetznercloud/hcloud-go/v2 v2.13.1 h1:jq0GP4QaYE5d8xR/Zw17s9qoaESRJMXfGmtD1a/qckQ= 262 | github.com/hetznercloud/hcloud-go/v2 v2.13.1/go.mod h1:dhix40Br3fDiBhwaSG/zgaYOFFddpfBm/6R1Zz0IiF0= 263 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 264 | github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= 265 | github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= 266 | github.com/ionos-cloud/sdk-go/v6 v6.2.1 h1:mxxN+frNVmbFrmmFfXnBC3g2USYJrl6mc1LW2iNYbFY= 267 | github.com/ionos-cloud/sdk-go/v6 v6.2.1/go.mod h1:SXrO9OGyWjd2rZhAhEpdYN6VUAODzzqRdqA9BCviQtI= 268 | github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= 269 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 270 | github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= 271 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 272 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 273 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 274 | github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= 275 | github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= 276 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 277 | github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 278 | github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 279 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 280 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 281 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 282 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= 283 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 284 | github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 285 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 286 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 287 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 288 | github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b h1:udzkj9S/zlT5X367kqJis0QP7YMxobob6zhzq6Yre00= 289 | github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b/go.mod h1:pcaDhQK0/NJZEvtCO0qQPPropqV0sJOJ6YW7X+9kRwM= 290 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 291 | github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 292 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 293 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 294 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 295 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 296 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 297 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 298 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 299 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 300 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 301 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 302 | github.com/linode/linodego v1.41.0 h1:GcP7JIBr9iLRJ9FwAtb9/WCT1DuPJS/xUApapfdjtiY= 303 | github.com/linode/linodego v1.41.0/go.mod h1:Ow4/XZ0yvWBzt3iAHwchvhSx30AyLintsSMvvQ2/SJY= 304 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 305 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 306 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 307 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 308 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 309 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 310 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 311 | github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos= 312 | github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ= 313 | github.com/mdlayher/vsock v1.2.1 h1:pC1mTJTvjo1r9n9fbm7S1j04rCgCzhCOS5DY0zqHlnQ= 314 | github.com/mdlayher/vsock v1.2.1/go.mod h1:NRfCibel++DgeMD8z/hP+PPTjlNJsdPOmxcnENvE+SE= 315 | github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= 316 | github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= 317 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 318 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 319 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 320 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 321 | github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= 322 | github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= 323 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 324 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 325 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 326 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 327 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 328 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 329 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 330 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 331 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 332 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 333 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= 334 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 335 | github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= 336 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 337 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 338 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 339 | github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= 340 | github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= 341 | github.com/ovh/go-ovh v1.6.0 h1:ixLOwxQdzYDx296sXcgS35TOPEahJkpjMGtzPadCjQI= 342 | github.com/ovh/go-ovh v1.6.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c= 343 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= 344 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= 345 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 346 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 347 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 348 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 349 | github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= 350 | github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= 351 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 352 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 353 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 354 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 355 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 356 | github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= 357 | github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= 358 | github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= 359 | github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= 360 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 361 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 362 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 363 | github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 364 | github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= 365 | github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= 366 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 367 | github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= 368 | github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= 369 | github.com/prometheus/common v0.29.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= 370 | github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4= 371 | github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= 372 | github.com/prometheus/common/sigv4 v0.1.0 h1:qoVebwtwwEhS85Czm2dSROY5fTo2PAPEVdDeppTwGX4= 373 | github.com/prometheus/common/sigv4 v0.1.0/go.mod h1:2Jkxxk9yYvCkE5G1sQT7GuEXm57JrvHu9k5YwTjsNtI= 374 | github.com/prometheus/exporter-toolkit v0.13.1 h1:Evsh0gWQo2bdOHlnz9+0Nm7/OFfIwhE2Ws4A2jIlR04= 375 | github.com/prometheus/exporter-toolkit v0.13.1/go.mod h1:ujdv2YIOxtdFxxqtloLpbqmxd5J0Le6IITUvIRSWjj0= 376 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 377 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 378 | github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= 379 | github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= 380 | github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= 381 | github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= 382 | github.com/prometheus/prometheus v0.300.1 h1:9KKcTTq80gkzmXW0Et/QCFSrBPgmwiS3Hlcxc6o8KlM= 383 | github.com/prometheus/prometheus v0.300.1/go.mod h1:gtTPY/XVyCdqqnjA3NzDMb0/nc5H9hOu1RMame+gHyM= 384 | github.com/prometheus/statsd_exporter v0.28.0 h1:S3ZLyLm/hOKHYZFOF0h4zYmd0EeKyPF9R1pFBYXUgYY= 385 | github.com/prometheus/statsd_exporter v0.28.0/go.mod h1:Lq41vNkMLfiPANmI+uHb5/rpFFUTxPXiiNpmsAYLvDI= 386 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 387 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 388 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 389 | github.com/scaleway/scaleway-sdk-go v1.0.0-beta.30 h1:yoKAVkEVwAqbGbR8n87rHQ1dulL25rKloGadb3vm770= 390 | github.com/scaleway/scaleway-sdk-go v1.0.0-beta.30/go.mod h1:sH0u6fq6x4R5M7WxkoQFY/o7UaiItec0o1LinLCJNq8= 391 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 392 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 393 | github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= 394 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 395 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 396 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 397 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 398 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 399 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 400 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 401 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 402 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 403 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 404 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 405 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 406 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 407 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 408 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 409 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 410 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 411 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 412 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 413 | github.com/vultr/govultr/v2 v2.17.2 h1:gej/rwr91Puc/tgh+j33p/BLR16UrIPnSr+AIwYWZQs= 414 | github.com/vultr/govultr/v2 v2.17.2/go.mod h1:ZFOKGWmgjytfyjeyAdhQlSWwTjh2ig+X49cAp50dzXI= 415 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 416 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 417 | github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= 418 | github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= 419 | github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 420 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 421 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 422 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 423 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 424 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 425 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 426 | go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 427 | go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= 428 | go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= 429 | go.opentelemetry.io/collector/pdata v1.16.0 h1:g02K8jlRnmQ7TQDuXpdgVL6vIxIVqr5Gbb1qIR27rto= 430 | go.opentelemetry.io/collector/pdata v1.16.0/go.mod h1:YZZJIt2ehxosYf/Y1pbvexjNWsIGNNrzzlCTO9jC1F4= 431 | go.opentelemetry.io/collector/semconv v0.110.0 h1:KHQnOHe3gUz0zsxe8ph9kN5OTypCFD4V+06AiBTfeNk= 432 | go.opentelemetry.io/collector/semconv v0.110.0/go.mod h1:zCJ5njhWpejR+A40kiEoeFm1xq1uzyZwMnRNX6/D82A= 433 | go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.56.0 h1:4BZHA+B1wXEQoGNHxW8mURaLhcdGwvRnmhGbm+odRbc= 434 | go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.56.0/go.mod h1:3qi2EEwMgB4xnKgPLqsDP3j9qxnHDZeHsnAxfjQqTko= 435 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s= 436 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM= 437 | go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= 438 | go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= 439 | go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE= 440 | go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= 441 | go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= 442 | go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= 443 | go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= 444 | go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= 445 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 446 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 447 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 448 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 449 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 450 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 451 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 452 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 453 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 454 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 455 | golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= 456 | golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= 457 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 458 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 459 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 460 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 461 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 462 | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 463 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 464 | golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 465 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= 466 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= 467 | golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA= 468 | golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= 469 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 470 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 471 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 472 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 473 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 474 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 475 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 476 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 477 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 478 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= 479 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 480 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 481 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 482 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 483 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 484 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 485 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 486 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 487 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 488 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 489 | golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= 490 | golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 491 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 492 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 493 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 494 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 495 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 496 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 497 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 498 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 499 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 500 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 501 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 502 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 503 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 504 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 505 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 506 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 507 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 508 | golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 509 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 510 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 511 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 512 | golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 513 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 514 | golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 515 | golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 516 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 517 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 518 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 519 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 520 | golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 521 | golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= 522 | golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= 523 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 524 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 525 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 526 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 527 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 528 | golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 529 | golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= 530 | golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= 531 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 532 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 533 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 534 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 535 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 536 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 537 | golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 538 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 539 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 540 | golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= 541 | golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 542 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 543 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 544 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 545 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 546 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 547 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 548 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 549 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 550 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 551 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 552 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 553 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 554 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 555 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 556 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 557 | golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 558 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 559 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 560 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 561 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 562 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 563 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 564 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 565 | golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 566 | golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 567 | golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 568 | golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 569 | golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 570 | golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 571 | golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 572 | golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 573 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 574 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 575 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 576 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 577 | golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 578 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 579 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 580 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 581 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 582 | golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= 583 | golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= 584 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 585 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 586 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 587 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 588 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 589 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 590 | golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= 591 | golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= 592 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 593 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 594 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 595 | golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= 596 | golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 597 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 598 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 599 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 600 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 601 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 602 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 603 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 604 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 605 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 606 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 607 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 608 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 609 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 610 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 611 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 612 | golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 613 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 614 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 615 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 616 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 617 | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 618 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 619 | golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 620 | golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 621 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 622 | golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 623 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 624 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 625 | golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 626 | golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 627 | golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 628 | golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 629 | golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= 630 | golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 631 | golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 632 | golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 633 | golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 634 | golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 635 | golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 636 | golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 637 | golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= 638 | golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= 639 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 640 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 641 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 642 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 643 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 644 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 645 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 646 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 647 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 648 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 649 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 650 | google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 651 | google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 652 | google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 653 | google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 654 | google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 655 | google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 656 | google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 657 | google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= 658 | google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= 659 | google.golang.org/api v0.199.0 h1:aWUXClp+VFJmqE0JPvpZOK3LDQMyFKYIow4etYd9qxs= 660 | google.golang.org/api v0.199.0/go.mod h1:ohG4qSztDJmZdjK/Ar6MhbAmb/Rpi4JHOqagsh90K28= 661 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 662 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 663 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 664 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 665 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 666 | google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 667 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 668 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 669 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 670 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 671 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 672 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 673 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 674 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 675 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 676 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 677 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 678 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 679 | google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 680 | google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 681 | google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= 682 | google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 683 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 684 | google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 685 | google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 686 | google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 687 | google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 688 | google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 689 | google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 690 | google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= 691 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 692 | google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= 693 | google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 694 | google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 695 | google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 696 | google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 h1:BulPr26Jqjnd4eYDVe+YvyR7Yc2vJGkO5/0UxD0/jZU= 697 | google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc= 698 | google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I= 699 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= 700 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= 701 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 702 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 703 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 704 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 705 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 706 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 707 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 708 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 709 | google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= 710 | google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= 711 | google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 712 | google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 713 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= 714 | google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= 715 | google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= 716 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 717 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 718 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 719 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 720 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 721 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 722 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 723 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 724 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= 725 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 726 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 727 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 728 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 729 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 730 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 731 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 732 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 733 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 734 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 735 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 736 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 737 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 738 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= 739 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 740 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 741 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 742 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 743 | gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 744 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 745 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 746 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 747 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 748 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 749 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 750 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 751 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 752 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 753 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 754 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 755 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 756 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 757 | honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 758 | k8s.io/api v0.31.1 h1:Xe1hX/fPW3PXYYv8BlozYqw63ytA92snr96zMW9gWTU= 759 | k8s.io/api v0.31.1/go.mod h1:sbN1g6eY6XVLeqNsZGLnI5FwVseTrZX7Fv3O26rhAaI= 760 | k8s.io/apimachinery v0.31.1 h1:mhcUBbj7KUjaVhyXILglcVjuS4nYXiwC+KKFBgIVy7U= 761 | k8s.io/apimachinery v0.31.1/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= 762 | k8s.io/client-go v0.31.1 h1:f0ugtWSbWpxHR7sjVpQwuvw9a3ZKLXX0u0itkFXufb0= 763 | k8s.io/client-go v0.31.1/go.mod h1:sKI8871MJN2OyeqRlmA4W4KM9KBdBUpDLu/43eGemCg= 764 | k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= 765 | k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 766 | k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= 767 | k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= 768 | k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= 769 | k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 770 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 771 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= 772 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= 773 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= 774 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= 775 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= 776 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= 777 | sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= 778 | sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= 779 | -------------------------------------------------------------------------------- /reader/reader.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package reader 15 | 16 | import ( 17 | "fmt" 18 | "math" 19 | "os" 20 | "path" 21 | "path/filepath" 22 | "sort" 23 | "strings" 24 | 25 | "github.com/go-graphite/go-whisper" 26 | ) 27 | 28 | type DBReader interface { 29 | Metrics() ([]string, error) 30 | GetMinAndMaxTimestamps() (int64, int64, error) 31 | Points(string, int64, int64) ([]Point, error) 32 | } 33 | 34 | type Point struct { 35 | Timestamp int64 36 | Value float64 37 | } 38 | 39 | func NewReader(path string) DBReader { 40 | return &whisperReader{ 41 | path: path, 42 | } 43 | } 44 | 45 | type whisperReader struct { 46 | path string 47 | wdb whisper.Whisper 48 | } 49 | 50 | func (w *whisperReader) Metrics() ([]string, error) { 51 | metrics := make([]string, 0) 52 | err := filepath.Walk(w.path, func(path string, info os.FileInfo, err error) error { 53 | if !strings.HasSuffix(path, ".wsp") { 54 | return nil 55 | } 56 | path = strings.TrimPrefix(path, strings.TrimSuffix(w.path, string(os.PathSeparator))+string(os.PathSeparator)) 57 | path = strings.TrimSuffix(path, ".wsp") 58 | metrics = append(metrics, strings.ReplaceAll(path, string(os.PathSeparator), ".")) 59 | return nil 60 | }) 61 | if err != nil { 62 | return nil, err 63 | } 64 | return metrics, nil 65 | } 66 | 67 | func (w *whisperReader) opendb(metric string) (*whisper.Whisper, error) { 68 | path := path.Join(append([]string{w.path}, strings.Split(metric, ".")...)...) + ".wsp" 69 | flag := os.O_RDONLY 70 | return whisper.OpenWithOptions(path, &whisper.Options{ 71 | FLock: false, 72 | OpenFileFlag: &flag, 73 | }) 74 | } 75 | 76 | func (w *whisperReader) GetMinAndMaxTimestamps() (int64, int64, error) { 77 | var ( 78 | // Go-Graphite timestamps are int32. 79 | min = math.MaxInt32 80 | max = math.MinInt32 81 | ) 82 | metrics, err := w.Metrics() 83 | if err != nil { 84 | return 0, 0, err 85 | } 86 | for _, metric := range metrics { 87 | wdb, err := w.opendb(metric) 88 | if err != nil { 89 | return 0, 0, err 90 | } 91 | ts, err := wdb.Fetch(math.MinInt32, math.MaxInt32) 92 | if err != nil { 93 | return 0, 0, err 94 | } 95 | for _, sample := range ts.Points() { 96 | if math.IsNaN(sample.Value) { 97 | continue 98 | } 99 | if sample.Time < min { 100 | min = sample.Time 101 | } 102 | if sample.Time > max { 103 | max = sample.Time 104 | } 105 | } 106 | err = wdb.Close() 107 | if err != nil { 108 | return 0, 0, err 109 | } 110 | } 111 | if min > max { 112 | return 0, 0, fmt.Errorf("no valid sample found (min: %d, max: %v)", min, metrics) 113 | } 114 | return int64(1000 * min), int64(1000 * max), nil 115 | } 116 | 117 | func (w *whisperReader) Points(metric string, from, until int64) ([]Point, error) { 118 | wdb, err := w.opendb(metric) 119 | if err != nil { 120 | return nil, err 121 | } 122 | defer wdb.Close() 123 | ts, err := wdb.Fetch(int(from/1000), int(until/1000)) 124 | if err != nil { 125 | return nil, err 126 | } 127 | points := make([]Point, 0) 128 | for _, sample := range ts.Points() { 129 | if math.IsNaN(sample.Value) { 130 | continue 131 | } 132 | points = append(points, Point{Timestamp: 1000 * int64(sample.Time), Value: sample.Value}) 133 | } 134 | sort.Slice(points, func(i, j int) bool { return points[i].Timestamp < points[j].Timestamp }) 135 | return points, nil 136 | } 137 | -------------------------------------------------------------------------------- /reader/reader_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package reader 15 | 16 | import ( 17 | "math" 18 | "sort" 19 | "testing" 20 | "time" 21 | 22 | "github.com/go-graphite/go-whisper" 23 | "github.com/stretchr/testify/require" 24 | ) 25 | 26 | func init() { 27 | // Pin go-whisper to a fixed timestamp so that the test data is in the window of retention. 28 | whisper.Now = func() time.Time { return time.Unix(1640000000, 0) } 29 | } 30 | 31 | func TestListMetrics(t *testing.T) { 32 | reader := NewReader("testdata") 33 | metrics, err := reader.Metrics() 34 | require.NoError(t, err) 35 | 36 | sort.Strings(metrics) 37 | expected := []string{ 38 | "test-whisper.load.load.longterm", 39 | "test-whisper.load.load.shortterm", 40 | "test-whisper.load.load.midterm", 41 | } 42 | 43 | sort.Strings(expected) 44 | 45 | require.Equal(t, expected, metrics) 46 | } 47 | 48 | func TestGetMinAndMaxTimestamp(t *testing.T) { 49 | reader := NewReader("testdata") 50 | min, max, err := reader.GetMinAndMaxTimestamps() 51 | require.NoError(t, err) 52 | 53 | require.True(t, min > math.MinInt64) 54 | require.Equal(t, int64(1611068400000), max) 55 | } 56 | 57 | func TestGetPoints(t *testing.T) { 58 | reader := NewReader("testdata") 59 | points, err := reader.Points("test-whisper.load.load.longterm", 1000*math.MinInt32, 1000*math.MaxInt32) 60 | require.NoError(t, err) 61 | 62 | expectedLastPoints := []Point{ 63 | { 64 | Timestamp: 1611067800000, 65 | Value: 1.0511666666666666, 66 | }, 67 | { 68 | Timestamp: 1611068400000, 69 | Value: 1.0636666666666668, 70 | }, 71 | } 72 | 73 | for i, p := range expectedLastPoints { 74 | pos := len(points) - len(expectedLastPoints) + i 75 | require.Equal(t, p, points[pos]) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /reader/testdata/test-whisper/load/load/longterm.wsp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prometheus/graphite_exporter/40eeb62891347b0e50600ddf1ffabee6d1073e29/reader/testdata/test-whisper/load/load/longterm.wsp -------------------------------------------------------------------------------- /reader/testdata/test-whisper/load/load/midterm.wsp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prometheus/graphite_exporter/40eeb62891347b0e50600ddf1ffabee6d1073e29/reader/testdata/test-whisper/load/load/midterm.wsp -------------------------------------------------------------------------------- /reader/testdata/test-whisper/load/load/shortterm.wsp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prometheus/graphite_exporter/40eeb62891347b0e50600ddf1ffabee6d1073e29/reader/testdata/test-whisper/load/load/shortterm.wsp --------------------------------------------------------------------------------