├── .circleci └── config.yml ├── .github ├── dependabot.yml └── workflows │ ├── container_description.yml │ └── golangci-lint.yml ├── .gitignore ├── .golangci.yml ├── .promu.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── LICENSE ├── MAINTAINERS.md ├── Makefile ├── Makefile.common ├── README.md ├── SECURITY.md ├── VERSION ├── cmd ├── main.go └── main_test.go ├── config └── config.go ├── examples ├── animal-data.json ├── config.yml ├── data.json └── prometheus.yml ├── exporter ├── collector.go ├── util.go └── util_test.go ├── go.mod ├── go.sum ├── main.go └── test ├── config └── good.yml ├── response └── good.txt └── serve ├── good.json └── repeat-metric.json /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2.1 3 | orbs: 4 | prometheus: prometheus/prometheus@0.17.1 5 | executors: 6 | # This must match .promu.yml. 7 | golang: 8 | docker: 9 | - image: cimg/go:1.23 10 | jobs: 11 | test: 12 | executor: golang 13 | steps: 14 | - prometheus/setup_environment 15 | - run: make 16 | - prometheus/store_artifact: 17 | file: json_exporter 18 | workflows: 19 | version: 2 20 | json_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 | docker_hub_organization: prometheuscommunity 34 | quay_io_organization: prometheuscommunity 35 | requires: 36 | - test 37 | - build 38 | filters: 39 | branches: 40 | only: master 41 | - prometheus/publish_release: 42 | context: org-context 43 | docker_hub_organization: prometheuscommunity 44 | quay_io_organization: prometheuscommunity 45 | requires: 46 | - test 47 | - build 48 | filters: 49 | tags: 50 | only: /^v.*/ 51 | branches: 52 | ignore: /.*/ 53 | -------------------------------------------------------------------------------- /.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@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.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@55c2c1448f86e01eaae002a5a3a9624417608d84 # v6.5.2 37 | with: 38 | args: --verbose 39 | version: v1.64.6 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | /build 27 | /json_exporter 28 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | enable: 3 | - revive 4 | - sloglint 5 | 6 | issues: 7 | exclude-rules: 8 | - path: _test.go 9 | linters: 10 | - errcheck 11 | 12 | linters-settings: 13 | errcheck: 14 | exclude-functions: 15 | # Used in HTTP handlers, any error is handled by the server itself. 16 | - (net/http.ResponseWriter).Write 17 | revive: 18 | rules: 19 | - name: unused-parameter 20 | disabled: true 21 | -------------------------------------------------------------------------------- /.promu.yml: -------------------------------------------------------------------------------- 1 | go: 2 | # This must match .circle/config.yml. 3 | version: 1.23 4 | repository: 5 | path: github.com/prometheus-community/json_exporter 6 | build: 7 | binaries: 8 | - name: json_exporter 9 | ldflags: | 10 | -X github.com/prometheus/common/version.Version={{.Version}} 11 | -X github.com/prometheus/common/version.Revision={{.Revision}} 12 | -X github.com/prometheus/common/version.Branch={{.Branch}} 13 | -X github.com/prometheus/common/version.BuildUser={{user}}@{{host}} 14 | -X github.com/prometheus/common/version.BuildDate={{date "20060102-15:04:05"}} 15 | tarball: 16 | files: 17 | - LICENSE 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.7.0 / 2025-02-05 2 | 3 | * [CHANGE] adopt slog, drop go-kit/log #334 4 | 5 | ## 0.6.0 / 2023-06-06 6 | 7 | * [FEATURE] Allow timestamps from metrics #167 8 | * [FEATURE] Support Value Conversions #172 9 | 10 | ## 0.5.0 / 2022-07-03 11 | 12 | Breaking Change: 13 | 14 | The exporter config file format has changed. It now supports multiple modules 15 | to scrape different endpoints. 16 | 17 | * [FEATURE] Support custom valuetype #145 18 | * [FEATURE] Support modules configuration #146 19 | * [FEATURE] Accept non-2xx HTTP status #161 20 | 21 | ## 0.4.0 / 2022-01-15 22 | 23 | * [FEATURE] Add support for HTTP POST body content #123 24 | 25 | ## 0.3.0 / 2021-02-12 26 | 27 | :warning: Backward incompatible configuration with previous versions. 28 | * [CHANGE] Migrate JSONPath library [#74](https://github.com/prometheus-community/json_exporter/pull/74) 29 | * [CHANGE] Add TLS metrics support [#68](https://github.com/prometheus-community/json_exporter/pull/68) 30 | 31 | ## 0.2.0 / 2020-11-03 32 | 33 | * [CHANGE] This release is complete refactoring [#49](https://github.com/prometheus-community/json_exporter/pull/49) 34 | * [BUGFIX] Fix unchecked call to io.Copy [#57](https://github.com/prometheus-community/json_exporter/pull/57) 35 | 36 | ## 0.1.0 / 2020-07-27 37 | 38 | Initial prometheus-community release. 39 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG ARCH="amd64" 2 | ARG OS="linux" 3 | FROM quay.io/prometheus/busybox-${OS}-${ARCH}:glibc 4 | LABEL maintainer="The Prometheus Authors " 5 | 6 | ARG ARCH="amd64" 7 | ARG OS="linux" 8 | COPY .build/${OS}-${ARCH}/json_exporter /bin/json_exporter 9 | 10 | EXPOSE 7979 11 | USER nobody 12 | ENTRYPOINT [ "/bin/json_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 | * Ben Kochie @SuperQ 2 | * Ravi @rustycl0ck 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Ensure that 'all' is the default target otherwise it will be the first target from Makefile.common. 2 | all:: 3 | 4 | # Needs to be defined before including Makefile.common to auto-generate targets 5 | DOCKER_ARCHS ?= amd64 armv7 arm64 ppc64le 6 | DOCKER_REPO ?= prometheuscommunity 7 | 8 | include Makefile.common 9 | 10 | DOCKER_IMAGE_NAME ?= json-exporter 11 | -------------------------------------------------------------------------------- /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 ?= v1.64.6 65 | # golangci-lint only supports linux, darwin and windows platforms on i386/amd64/arm64. 66 | # windows isn't included here because of the path separator being different. 67 | ifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux darwin)) 68 | ifeq ($(GOHOSTARCH),$(filter $(GOHOSTARCH),amd64 i386 arm64)) 69 | # If we're in CI and there is an Actions file, that means the linter 70 | # is being run in Actions, so we don't need to run it here. 71 | ifneq (,$(SKIP_GOLANGCI_LINT)) 72 | GOLANGCI_LINT := 73 | else ifeq (,$(CIRCLE_JOB)) 74 | GOLANGCI_LINT := $(FIRST_GOPATH)/bin/golangci-lint 75 | else ifeq (,$(wildcard .github/workflows/golangci-lint.yml)) 76 | GOLANGCI_LINT := $(FIRST_GOPATH)/bin/golangci-lint 77 | endif 78 | endif 79 | endif 80 | 81 | PREFIX ?= $(shell pwd) 82 | BIN_DIR ?= $(shell pwd) 83 | DOCKER_IMAGE_TAG ?= $(subst /,-,$(shell git rev-parse --abbrev-ref HEAD)) 84 | DOCKERFILE_PATH ?= ./Dockerfile 85 | DOCKERBUILD_CONTEXT ?= ./ 86 | DOCKER_REPO ?= prom 87 | 88 | DOCKER_ARCHS ?= amd64 89 | 90 | BUILD_DOCKER_ARCHS = $(addprefix common-docker-,$(DOCKER_ARCHS)) 91 | PUBLISH_DOCKER_ARCHS = $(addprefix common-docker-publish-,$(DOCKER_ARCHS)) 92 | TAG_DOCKER_ARCHS = $(addprefix common-docker-tag-latest-,$(DOCKER_ARCHS)) 93 | 94 | SANITIZED_DOCKER_IMAGE_TAG := $(subst +,-,$(DOCKER_IMAGE_TAG)) 95 | 96 | ifeq ($(GOHOSTARCH),amd64) 97 | ifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux freebsd darwin windows)) 98 | # Only supported on amd64 99 | test-flags := -race 100 | endif 101 | endif 102 | 103 | # This rule is used to forward a target like "build" to "common-build". This 104 | # allows a new "build" target to be defined in a Makefile which includes this 105 | # one and override "common-build" without override warnings. 106 | %: common-% ; 107 | 108 | .PHONY: common-all 109 | common-all: precheck style check_license lint yamllint unused build test 110 | 111 | .PHONY: common-style 112 | common-style: 113 | @echo ">> checking code style" 114 | @fmtRes=$$($(GOFMT) -d $$(find . -path ./vendor -prune -o -name '*.go' -print)); \ 115 | if [ -n "$${fmtRes}" ]; then \ 116 | echo "gofmt checking failed!"; echo "$${fmtRes}"; echo; \ 117 | echo "Please ensure you are using $$($(GO) version) for formatting code."; \ 118 | exit 1; \ 119 | fi 120 | 121 | .PHONY: common-check_license 122 | common-check_license: 123 | @echo ">> checking license header" 124 | @licRes=$$(for file in $$(find . -type f -iname '*.go' ! -path './vendor/*') ; do \ 125 | awk 'NR<=3' $$file | grep -Eq "(Copyright|generated|GENERATED)" || echo $$file; \ 126 | done); \ 127 | if [ -n "$${licRes}" ]; then \ 128 | echo "license header checking failed:"; echo "$${licRes}"; \ 129 | exit 1; \ 130 | fi 131 | 132 | .PHONY: common-deps 133 | common-deps: 134 | @echo ">> getting dependencies" 135 | $(GO) mod download 136 | 137 | .PHONY: update-go-deps 138 | update-go-deps: 139 | @echo ">> updating Go dependencies" 140 | @for m in $$($(GO) list -mod=readonly -m -f '{{ if and (not .Indirect) (not .Main)}}{{.Path}}{{end}}' all); do \ 141 | $(GO) get -d $$m; \ 142 | done 143 | $(GO) mod tidy 144 | 145 | .PHONY: common-test-short 146 | common-test-short: $(GOTEST_DIR) 147 | @echo ">> running short tests" 148 | $(GOTEST) -short $(GOOPTS) $(pkgs) 149 | 150 | .PHONY: common-test 151 | common-test: $(GOTEST_DIR) 152 | @echo ">> running all tests" 153 | $(GOTEST) $(test-flags) $(GOOPTS) $(pkgs) 154 | 155 | $(GOTEST_DIR): 156 | @mkdir -p $@ 157 | 158 | .PHONY: common-format 159 | common-format: 160 | @echo ">> formatting code" 161 | $(GO) fmt $(pkgs) 162 | 163 | .PHONY: common-vet 164 | common-vet: 165 | @echo ">> vetting code" 166 | $(GO) vet $(GOOPTS) $(pkgs) 167 | 168 | .PHONY: common-lint 169 | common-lint: $(GOLANGCI_LINT) 170 | ifdef GOLANGCI_LINT 171 | @echo ">> running golangci-lint" 172 | $(GOLANGCI_LINT) run $(GOLANGCI_LINT_OPTS) $(pkgs) 173 | endif 174 | 175 | .PHONY: common-lint-fix 176 | common-lint-fix: $(GOLANGCI_LINT) 177 | ifdef GOLANGCI_LINT 178 | @echo ">> running golangci-lint fix" 179 | $(GOLANGCI_LINT) run --fix $(GOLANGCI_LINT_OPTS) $(pkgs) 180 | endif 181 | 182 | .PHONY: common-yamllint 183 | common-yamllint: 184 | @echo ">> running yamllint on all YAML files in the repository" 185 | ifeq (, $(shell command -v yamllint 2> /dev/null)) 186 | @echo "yamllint not installed so skipping" 187 | else 188 | yamllint . 189 | endif 190 | 191 | # For backward-compatibility. 192 | .PHONY: common-staticcheck 193 | common-staticcheck: lint 194 | 195 | .PHONY: common-unused 196 | common-unused: 197 | @echo ">> running check for unused/missing packages in go.mod" 198 | $(GO) mod tidy 199 | @git diff --exit-code -- go.sum go.mod 200 | 201 | .PHONY: common-build 202 | common-build: promu 203 | @echo ">> building binaries" 204 | $(PROMU) build --prefix $(PREFIX) $(PROMU_BINARIES) 205 | 206 | .PHONY: common-tarball 207 | common-tarball: promu 208 | @echo ">> building release tarball" 209 | $(PROMU) tarball --prefix $(PREFIX) $(BIN_DIR) 210 | 211 | .PHONY: common-docker-repo-name 212 | common-docker-repo-name: 213 | @echo "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)" 214 | 215 | .PHONY: common-docker $(BUILD_DOCKER_ARCHS) 216 | common-docker: $(BUILD_DOCKER_ARCHS) 217 | $(BUILD_DOCKER_ARCHS): common-docker-%: 218 | docker build -t "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" \ 219 | -f $(DOCKERFILE_PATH) \ 220 | --build-arg ARCH="$*" \ 221 | --build-arg OS="linux" \ 222 | $(DOCKERBUILD_CONTEXT) 223 | 224 | .PHONY: common-docker-publish $(PUBLISH_DOCKER_ARCHS) 225 | common-docker-publish: $(PUBLISH_DOCKER_ARCHS) 226 | $(PUBLISH_DOCKER_ARCHS): common-docker-publish-%: 227 | docker push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" 228 | 229 | DOCKER_MAJOR_VERSION_TAG = $(firstword $(subst ., ,$(shell cat VERSION))) 230 | .PHONY: common-docker-tag-latest $(TAG_DOCKER_ARCHS) 231 | common-docker-tag-latest: $(TAG_DOCKER_ARCHS) 232 | $(TAG_DOCKER_ARCHS): common-docker-tag-latest-%: 233 | docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:latest" 234 | docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:v$(DOCKER_MAJOR_VERSION_TAG)" 235 | 236 | .PHONY: common-docker-manifest 237 | common-docker-manifest: 238 | 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)) 239 | DOCKER_CLI_EXPERIMENTAL=enabled docker manifest push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)" 240 | 241 | .PHONY: promu 242 | promu: $(PROMU) 243 | 244 | $(PROMU): 245 | $(eval PROMU_TMP := $(shell mktemp -d)) 246 | curl -s -L $(PROMU_URL) | tar -xvzf - -C $(PROMU_TMP) 247 | mkdir -p $(FIRST_GOPATH)/bin 248 | cp $(PROMU_TMP)/promu-$(PROMU_VERSION).$(GO_BUILD_PLATFORM)/promu $(FIRST_GOPATH)/bin/promu 249 | rm -r $(PROMU_TMP) 250 | 251 | .PHONY: proto 252 | proto: 253 | @echo ">> generating code from proto files" 254 | @./scripts/genproto.sh 255 | 256 | ifdef GOLANGCI_LINT 257 | $(GOLANGCI_LINT): 258 | mkdir -p $(FIRST_GOPATH)/bin 259 | curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/$(GOLANGCI_LINT_VERSION)/install.sh \ 260 | | sed -e '/install -d/d' \ 261 | | sh -s -- -b $(FIRST_GOPATH)/bin $(GOLANGCI_LINT_VERSION) 262 | endif 263 | 264 | .PHONY: precheck 265 | precheck:: 266 | 267 | define PRECHECK_COMMAND_template = 268 | precheck:: $(1)_precheck 269 | 270 | PRECHECK_COMMAND_$(1) ?= $(1) $$(strip $$(PRECHECK_OPTIONS_$(1))) 271 | .PHONY: $(1)_precheck 272 | $(1)_precheck: 273 | @if ! $$(PRECHECK_COMMAND_$(1)) 1>/dev/null 2>&1; then \ 274 | echo "Execution of '$$(PRECHECK_COMMAND_$(1))' command failed. Is $(1) installed?"; \ 275 | exit 1; \ 276 | fi 277 | endef 278 | 279 | govulncheck: install-govulncheck 280 | govulncheck ./... 281 | 282 | install-govulncheck: 283 | command -v govulncheck > /dev/null || go install golang.org/x/vuln/cmd/govulncheck@latest 284 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | json_exporter 2 | ======================== 3 | [![CircleCI](https://circleci.com/gh/prometheus-community/json_exporter.svg?style=svg)](https://circleci.com/gh/prometheus-community/json_exporter) 4 | 5 | A [prometheus](https://prometheus.io/) exporter which scrapes remote JSON by JSONPath. 6 | 7 | - [Supported JSONPath Syntax](https://kubernetes.io/docs/reference/kubectl/jsonpath/) 8 | - [Examples configurations](/examples) 9 | 10 | ## Example Usage 11 | 12 | ```console 13 | ## SETUP 14 | 15 | $ make build 16 | $ ./json_exporter --config.file examples/config.yml & 17 | $ python3 -m http.server 8000 & 18 | Serving HTTP on :: port 8000 (http://[::]:8000/) ... 19 | 20 | 21 | ## TEST with 'default' module 22 | 23 | $ curl "http://localhost:7979/probe?module=default&target=http://localhost:8000/examples/data.json" 24 | # HELP example_global_value Example of a top-level global value scrape in the json 25 | # TYPE example_global_value untyped 26 | example_global_value{environment="beta",location="planet-mars"} 1234 27 | # HELP example_timestamped_value_count Example of a timestamped value scrape in the json 28 | # TYPE example_timestamped_value_count untyped 29 | example_timestamped_value_count{environment="beta"} 2 30 | # HELP example_value_active Example of sub-level value scrapes from a json 31 | # TYPE example_value_active untyped 32 | example_value_active{environment="beta",id="id-A"} 1 33 | example_value_active{environment="beta",id="id-C"} 1 34 | # HELP example_value_boolean Example of sub-level value scrapes from a json 35 | # TYPE example_value_boolean untyped 36 | example_value_boolean{environment="beta",id="id-A"} 1 37 | example_value_boolean{environment="beta",id="id-C"} 0 38 | # HELP example_value_count Example of sub-level value scrapes from a json 39 | # TYPE example_value_count untyped 40 | example_value_count{environment="beta",id="id-A"} 1 41 | example_value_count{environment="beta",id="id-C"} 3 42 | 43 | 44 | ## TEST with a different module for different json file 45 | 46 | $ curl "http://localhost:7979/probe?module=animals&target=http://localhost:8000/examples/animal-data.json" 47 | # HELP animal_population Example of top-level lists in a separate module 48 | # TYPE animal_population untyped 49 | animal_population{name="deer",predator="false"} 456 50 | animal_population{name="lion",predator="true"} 123 51 | animal_population{name="pigeon",predator="false"} 789 52 | 53 | 54 | ## TEST through prometheus: 55 | 56 | $ docker run --rm -it -p 9090:9090 -v $PWD/examples/prometheus.yml:/etc/prometheus/prometheus.yml prom/prometheus 57 | ``` 58 | Then head over to http://localhost:9090/graph?g0.range_input=1h&g0.expr=example_value_active&g0.tab=1 or http://localhost:9090/targets to check the scraped metrics or the targets. 59 | 60 | ## Using custom timestamps 61 | 62 | This exporter allows you to use a field of the metric as the (unix/epoch) timestamp for the data as an int64. However, this may lead to unexpected behaviour, as the prometheus implements a [Staleness](https://prometheus.io/docs/prometheus/latest/querying/basics/#staleness) mechanism. 63 | 64 | :warning: Including timestamps in metrics disables the staleness handling and can make data visible for longer than expected. 65 | 66 | ## Exposing metrics through HTTPS 67 | 68 | TLS configuration supported by this exporter can be found at [exporter-toolkit/web](https://github.com/prometheus/exporter-toolkit/blob/v0.9.0/docs/web-configuration.md) 69 | 70 | ## Sending body content for HTTP `POST` 71 | 72 | If `modules..body` paramater is set in config, it will be sent by the exporter as the body content in the scrape request. The HTTP method will also be set as 'POST' in this case. 73 | ```yaml 74 | body: 75 | content: | 76 | My static information: {"time_diff": "1m25s", "anotherVar": "some value"} 77 | ``` 78 | 79 | The body content can also be a [Go Template](https://golang.org/pkg/text/template). All the functions from the [Sprig library](https://masterminds.github.io/sprig/) can be used in the template. 80 | All the query parameters sent by prometheus in the scrape query to the exporter, are available as values while rendering the template. 81 | 82 | Example using template functions: 83 | ```yaml 84 | body: 85 | content: | 86 | {"time_diff": "{{ duration `95` }}","anotherVar": "{{ randInt 12 30 }}"} 87 | templatize: true 88 | ``` 89 | 90 | Example using template functions with values from the query parameters: 91 | ```yaml 92 | body: 93 | content: | 94 | {"time_diff": "{{ duration `95` }}","anotherVar": "{{ .myVal | first }}"} 95 | templatize: true 96 | ``` 97 | Then `curl "http://exporter:7979/probe?target=http://scrape_target:8080/test/data.json&myVal=something"`, would result in sending the following body as the HTTP POST payload to `http://scrape_target:8080/test/data.json`: 98 | ``` 99 | {"time_diff": "1m35s","anotherVar": "something"}. 100 | ``` 101 | 102 | ## Docker 103 | 104 | ```console 105 | $ docker run -v $PWD/examples/config.yml:/config.yml quay.io/prometheuscommunity/json-exporter --config.file=/config.yml 106 | ``` 107 | 108 | -------------------------------------------------------------------------------- /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.7.0 2 | -------------------------------------------------------------------------------- /cmd/main.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 cmd 15 | 16 | import ( 17 | "context" 18 | "encoding/json" 19 | "fmt" 20 | "log/slog" 21 | "net/http" 22 | "os" 23 | 24 | "github.com/alecthomas/kingpin/v2" 25 | "github.com/prometheus-community/json_exporter/config" 26 | "github.com/prometheus-community/json_exporter/exporter" 27 | "github.com/prometheus/client_golang/prometheus" 28 | "github.com/prometheus/client_golang/prometheus/promhttp" 29 | "github.com/prometheus/common/promslog" 30 | "github.com/prometheus/common/promslog/flag" 31 | "github.com/prometheus/common/version" 32 | "github.com/prometheus/exporter-toolkit/web" 33 | "github.com/prometheus/exporter-toolkit/web/kingpinflag" 34 | ) 35 | 36 | var ( 37 | configFile = kingpin.Flag("config.file", "JSON exporter configuration file.").Default("config.yml").ExistingFile() 38 | configCheck = kingpin.Flag("config.check", "If true validate the config file and then exit.").Default("false").Bool() 39 | metricsPath = kingpin.Flag( 40 | "web.telemetry-path", 41 | "Path under which to expose metrics.", 42 | ).Default("/metrics").String() 43 | toolkitFlags = kingpinflag.AddFlags(kingpin.CommandLine, ":7979") 44 | ) 45 | 46 | func Run() { 47 | 48 | promslogConfig := &promslog.Config{} 49 | 50 | flag.AddFlags(kingpin.CommandLine, promslogConfig) 51 | kingpin.Version(version.Print("json_exporter")) 52 | kingpin.HelpFlag.Short('h') 53 | kingpin.Parse() 54 | logger := promslog.New(promslogConfig) 55 | 56 | logger.Info("Starting json_exporter", "version", version.Info()) 57 | logger.Info("Build context", "build", version.BuildContext()) 58 | 59 | logger.Info("Loading config file", "file", *configFile) 60 | config, err := config.LoadConfig(*configFile) 61 | if err != nil { 62 | logger.Error("Error loading config", "err", err) 63 | os.Exit(1) 64 | } 65 | configJSON, err := json.Marshal(config) 66 | if err != nil { 67 | logger.Error("Failed to marshal config to JSON", "err", err) 68 | } 69 | logger.Info("Loaded config file", "config", string(configJSON)) 70 | 71 | if *configCheck { 72 | os.Exit(0) 73 | } 74 | 75 | http.Handle(*metricsPath, promhttp.Handler()) 76 | http.HandleFunc("/probe", func(w http.ResponseWriter, req *http.Request) { 77 | probeHandler(w, req, logger, config) 78 | }) 79 | if *metricsPath != "/" && *metricsPath != "" { 80 | landingConfig := web.LandingConfig{ 81 | Name: "JSON Exporter", 82 | Description: "Prometheus Exporter for converting json to metrics", 83 | Version: version.Info(), 84 | Links: []web.LandingLinks{ 85 | { 86 | Address: *metricsPath, 87 | Text: "Metrics", 88 | }, 89 | }, 90 | } 91 | landingPage, err := web.NewLandingPage(landingConfig) 92 | if err != nil { 93 | logger.Error("error creating landing page", "err", err) 94 | os.Exit(1) 95 | } 96 | http.Handle("/", landingPage) 97 | } 98 | 99 | server := &http.Server{} 100 | if err := web.ListenAndServe(server, toolkitFlags, logger); err != nil { 101 | logger.Error("Failed to start the server", "err", err) 102 | os.Exit(1) 103 | } 104 | } 105 | 106 | func probeHandler(w http.ResponseWriter, r *http.Request, logger *slog.Logger, config config.Config) { 107 | 108 | ctx, cancel := context.WithCancel(r.Context()) 109 | defer cancel() 110 | r = r.WithContext(ctx) 111 | 112 | module := r.URL.Query().Get("module") 113 | if module == "" { 114 | module = "default" 115 | } 116 | if _, ok := config.Modules[module]; !ok { 117 | http.Error(w, fmt.Sprintf("Unknown module %q", module), http.StatusBadRequest) 118 | logger.Debug("Unknown module", "module", module) 119 | return 120 | } 121 | 122 | registry := prometheus.NewPedanticRegistry() 123 | 124 | metrics, err := exporter.CreateMetricsList(config.Modules[module]) 125 | if err != nil { 126 | logger.Error("Failed to create metrics list from config", "err", err) 127 | } 128 | 129 | jsonMetricCollector := exporter.JSONMetricCollector{JSONMetrics: metrics} 130 | jsonMetricCollector.Logger = logger 131 | 132 | target := r.URL.Query().Get("target") 133 | if target == "" { 134 | http.Error(w, "Target parameter is missing", http.StatusBadRequest) 135 | return 136 | } 137 | 138 | fetcher := exporter.NewJSONFetcher(ctx, logger, config.Modules[module], r.URL.Query()) 139 | data, err := fetcher.FetchJSON(target) 140 | if err != nil { 141 | http.Error(w, "Failed to fetch JSON response. TARGET: "+target+", ERROR: "+err.Error(), http.StatusServiceUnavailable) 142 | return 143 | } 144 | 145 | jsonMetricCollector.Data = data 146 | 147 | registry.MustRegister(jsonMetricCollector) 148 | h := promhttp.HandlerFor(registry, promhttp.HandlerOpts{}) 149 | h.ServeHTTP(w, r) 150 | 151 | } 152 | -------------------------------------------------------------------------------- /cmd/main_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 cmd 15 | 16 | import ( 17 | "encoding/base64" 18 | "io" 19 | "net/http" 20 | "net/http/httptest" 21 | "os" 22 | "strings" 23 | "testing" 24 | 25 | "github.com/prometheus-community/json_exporter/config" 26 | pconfig "github.com/prometheus/common/config" 27 | "github.com/prometheus/common/promslog" 28 | ) 29 | 30 | func TestFailIfSelfSignedCA(t *testing.T) { 31 | target := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 32 | })) 33 | defer target.Close() 34 | 35 | req := httptest.NewRequest("GET", "http://example.com/foo"+"?module=default&target="+target.URL, nil) 36 | recorder := httptest.NewRecorder() 37 | probeHandler(recorder, req, promslog.NewNopLogger(), config.Config{Modules: map[string]config.Module{"default": {}}}) 38 | 39 | resp := recorder.Result() 40 | body, _ := io.ReadAll(resp.Body) 41 | 42 | if resp.StatusCode != http.StatusServiceUnavailable { 43 | t.Fatalf("Fail if (not strict) selfsigned CA test fails unexpectedly, got %s", body) 44 | } 45 | } 46 | 47 | func TestSucceedIfSelfSignedCA(t *testing.T) { 48 | c := config.Config{ 49 | Modules: map[string]config.Module{ 50 | "default": { 51 | HTTPClientConfig: pconfig.HTTPClientConfig{ 52 | TLSConfig: pconfig.TLSConfig{ 53 | InsecureSkipVerify: true, 54 | }, 55 | }, 56 | }}, 57 | } 58 | target := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 59 | })) 60 | defer target.Close() 61 | 62 | req := httptest.NewRequest("GET", "http://example.com/foo"+"?module=default&target="+target.URL, nil) 63 | recorder := httptest.NewRecorder() 64 | probeHandler(recorder, req, promslog.NewNopLogger(), c) 65 | 66 | resp := recorder.Result() 67 | body, _ := io.ReadAll(resp.Body) 68 | 69 | if resp.StatusCode != http.StatusOK { 70 | t.Fatalf("Succeed if (not strict) selfsigned CA test fails unexpectedly, got %s", body) 71 | } 72 | } 73 | 74 | func TestDefaultModule(t *testing.T) { 75 | target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 76 | })) 77 | defer target.Close() 78 | 79 | req := httptest.NewRequest("GET", "http://example.com/foo"+"?target="+target.URL, nil) 80 | recorder := httptest.NewRecorder() 81 | probeHandler(recorder, req, promslog.NewNopLogger(), config.Config{Modules: map[string]config.Module{"default": {}}}) 82 | 83 | resp := recorder.Result() 84 | if resp.StatusCode != http.StatusOK { 85 | t.Fatalf("Default module test fails unexpectedly, expected 200, got %d", resp.StatusCode) 86 | } 87 | 88 | // Module doesn't exist. 89 | recorder = httptest.NewRecorder() 90 | probeHandler(recorder, req, promslog.NewNopLogger(), config.Config{Modules: map[string]config.Module{"foo": {}}}) 91 | resp = recorder.Result() 92 | if resp.StatusCode != http.StatusBadRequest { 93 | t.Fatalf("Default module test fails unexpectedly, expected 400, got %d", resp.StatusCode) 94 | } 95 | } 96 | 97 | func TestFailIfTargetMissing(t *testing.T) { 98 | req := httptest.NewRequest("GET", "http://example.com/foo", nil) 99 | recorder := httptest.NewRecorder() 100 | probeHandler(recorder, req, promslog.NewNopLogger(), config.Config{}) 101 | 102 | resp := recorder.Result() 103 | body, _ := io.ReadAll(resp.Body) 104 | 105 | if resp.StatusCode != http.StatusBadRequest { 106 | t.Fatalf("Fail if 'target' query parameter missing test fails unexpectedly, got %s", body) 107 | } 108 | } 109 | 110 | func TestDefaultAcceptHeader(t *testing.T) { 111 | target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 112 | expected := "application/json" 113 | if got := r.Header.Get("Accept"); got != expected { 114 | t.Errorf("Default 'Accept' header mismatch, got %s, expected: %s", got, expected) 115 | w.WriteHeader(http.StatusNotAcceptable) 116 | } 117 | })) 118 | defer target.Close() 119 | 120 | req := httptest.NewRequest("GET", "http://example.com/foo"+"?module=default&target="+target.URL, nil) 121 | recorder := httptest.NewRecorder() 122 | probeHandler(recorder, req, promslog.NewNopLogger(), config.Config{Modules: map[string]config.Module{"default": {}}}) 123 | 124 | resp := recorder.Result() 125 | body, _ := io.ReadAll(resp.Body) 126 | 127 | if resp.StatusCode != http.StatusOK { 128 | t.Fatalf("Default 'Accept: application/json' header test fails unexpectedly, got %s", body) 129 | } 130 | } 131 | 132 | func TestCorrectResponse(t *testing.T) { 133 | tests := []struct { 134 | ConfigFile string 135 | ServeFile string 136 | ResponseFile string 137 | ShouldSucceed bool 138 | }{ 139 | {"../test/config/good.yml", "/serve/good.json", "../test/response/good.txt", true}, 140 | {"../test/config/good.yml", "/serve/repeat-metric.json", "../test/response/good.txt", false}, 141 | } 142 | 143 | target := httptest.NewServer(http.FileServer(http.Dir("../test"))) 144 | defer target.Close() 145 | 146 | for i, test := range tests { 147 | c, err := config.LoadConfig(test.ConfigFile) 148 | if err != nil { 149 | t.Fatalf("Failed to load config file %s", test.ConfigFile) 150 | } 151 | 152 | req := httptest.NewRequest("GET", "http://example.com/foo"+"?module=default&target="+target.URL+test.ServeFile, nil) 153 | recorder := httptest.NewRecorder() 154 | probeHandler(recorder, req, promslog.NewNopLogger(), c) 155 | 156 | resp := recorder.Result() 157 | body, _ := io.ReadAll(resp.Body) 158 | 159 | expected, _ := os.ReadFile(test.ResponseFile) 160 | 161 | if test.ShouldSucceed && string(body) != string(expected) { 162 | t.Fatalf("Correct response validation test %d fails unexpectedly.\nGOT:\n%s\nEXPECTED:\n%s", i, body, expected) 163 | } 164 | } 165 | } 166 | 167 | func TestBasicAuth(t *testing.T) { 168 | username := "myUser" 169 | password := "mySecretPassword" 170 | expected := "Basic " + base64.StdEncoding.EncodeToString([]byte(username+":"+password)) 171 | target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 172 | if got := r.Header.Get("Authorization"); got != expected { 173 | t.Errorf("BasicAuth mismatch, got: %s, expected: %s", got, expected) 174 | w.WriteHeader(http.StatusUnauthorized) 175 | } 176 | })) 177 | defer target.Close() 178 | 179 | req := httptest.NewRequest("GET", "http://example.com/foo"+"?module=default&target="+target.URL, nil) 180 | recorder := httptest.NewRecorder() 181 | c := config.Config{ 182 | Modules: map[string]config.Module{ 183 | "default": { 184 | HTTPClientConfig: pconfig.HTTPClientConfig{ 185 | BasicAuth: &pconfig.BasicAuth{ 186 | Username: username, 187 | Password: pconfig.Secret(password), 188 | }, 189 | }, 190 | }, 191 | }, 192 | } 193 | 194 | probeHandler(recorder, req, promslog.NewNopLogger(), c) 195 | 196 | resp := recorder.Result() 197 | body, _ := io.ReadAll(resp.Body) 198 | 199 | if resp.StatusCode != http.StatusOK { 200 | t.Fatalf("BasicAuth test fails unexpectedly. Got: %s", body) 201 | } 202 | } 203 | 204 | func TestBearerToken(t *testing.T) { 205 | token := "mySecretToken" 206 | target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 207 | expected := "Bearer " + token 208 | if got := r.Header.Get("Authorization"); got != expected { 209 | t.Errorf("BearerToken mismatch, got: %s, expected: %s", got, expected) 210 | w.WriteHeader(http.StatusUnauthorized) 211 | } 212 | })) 213 | defer target.Close() 214 | 215 | req := httptest.NewRequest("GET", "http://example.com/foo"+"?module=default&target="+target.URL, nil) 216 | recorder := httptest.NewRecorder() 217 | c := config.Config{ 218 | Modules: map[string]config.Module{"default": { 219 | HTTPClientConfig: pconfig.HTTPClientConfig{ 220 | BearerToken: pconfig.Secret(token), 221 | }, 222 | }}, 223 | } 224 | 225 | probeHandler(recorder, req, promslog.NewNopLogger(), c) 226 | 227 | resp := recorder.Result() 228 | body, _ := io.ReadAll(resp.Body) 229 | 230 | if resp.StatusCode != http.StatusOK { 231 | t.Fatalf("BearerToken test fails unexpectedly. Got: %s", body) 232 | } 233 | } 234 | 235 | func TestHTTPHeaders(t *testing.T) { 236 | headers := map[string]string{ 237 | "X-Dummy": "test", 238 | "User-Agent": "unsuspicious user", 239 | "Accept-Language": "en-US", 240 | } 241 | target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 242 | for key, value := range headers { 243 | if got := r.Header.Get(key); got != value { 244 | t.Errorf("Unexpected value of header %q: expected %q, got %q", key, value, got) 245 | } 246 | } 247 | w.WriteHeader(http.StatusOK) 248 | })) 249 | defer target.Close() 250 | 251 | req := httptest.NewRequest("GET", "http://example.com/foo"+"?module=default&target="+target.URL, nil) 252 | recorder := httptest.NewRecorder() 253 | c := config.Config{ 254 | Modules: map[string]config.Module{ 255 | "default": { 256 | Headers: headers, 257 | }, 258 | }, 259 | } 260 | 261 | probeHandler(recorder, req, promslog.NewNopLogger(), c) 262 | 263 | resp := recorder.Result() 264 | body, _ := io.ReadAll(resp.Body) 265 | 266 | if resp.StatusCode != http.StatusOK { 267 | t.Fatalf("Setting custom headers failed unexpectedly. Got: %s", body) 268 | } 269 | } 270 | 271 | // Test is the body template is correctly rendered 272 | func TestBodyPostTemplate(t *testing.T) { 273 | bodyTests := []struct { 274 | Body config.Body 275 | ShouldSucceed bool 276 | Result string 277 | }{ 278 | { 279 | Body: config.Body{Content: "something static like pi, 3.14"}, 280 | ShouldSucceed: true, 281 | }, 282 | { 283 | Body: config.Body{Content: "arbitrary dynamic value pass: {{ randInt 12 30 }}", Templatize: false}, 284 | ShouldSucceed: true, 285 | }, 286 | { 287 | Body: config.Body{Content: "arbitrary dynamic value fail: {{ randInt 12 30 }}", Templatize: true}, 288 | ShouldSucceed: false, 289 | }, 290 | { 291 | Body: config.Body{Content: "templatized mutated value: {{ upper `hello` }} is now all caps", Templatize: true}, 292 | Result: "templatized mutated value: HELLO is now all caps", 293 | ShouldSucceed: true, 294 | }, 295 | { 296 | Body: config.Body{Content: "value should be {{ lower `All Small` | trunc 3 }}", Templatize: true}, 297 | Result: "value should be all", 298 | ShouldSucceed: true, 299 | }, 300 | } 301 | 302 | for _, test := range bodyTests { 303 | target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 304 | expected := test.Body.Content 305 | if test.Result != "" { 306 | expected = test.Result 307 | } 308 | if got, _ := io.ReadAll(r.Body); string(got) != expected && test.ShouldSucceed { 309 | t.Errorf("POST request body content mismatch, got: %s, expected: %s", got, expected) 310 | } 311 | w.WriteHeader(http.StatusOK) 312 | })) 313 | 314 | req := httptest.NewRequest("POST", "http://example.com/foo"+"?module=default&target="+target.URL, strings.NewReader(test.Body.Content)) 315 | recorder := httptest.NewRecorder() 316 | c := config.Config{ 317 | Modules: map[string]config.Module{ 318 | "default": { 319 | Body: test.Body, 320 | }, 321 | }, 322 | } 323 | 324 | probeHandler(recorder, req, promslog.NewNopLogger(), c) 325 | 326 | resp := recorder.Result() 327 | respBody, _ := io.ReadAll(resp.Body) 328 | 329 | if resp.StatusCode != http.StatusOK { 330 | t.Fatalf("POST body content failed. Got: %s", respBody) 331 | } 332 | target.Close() 333 | } 334 | } 335 | 336 | // Test is the query parameters are correctly replaced in the provided body template 337 | func TestBodyPostQuery(t *testing.T) { 338 | bodyTests := []struct { 339 | Body config.Body 340 | ShouldSucceed bool 341 | Result string 342 | QueryParams map[string]string 343 | }{ 344 | { 345 | Body: config.Body{Content: "pi has {{ .piValue | first }} value", Templatize: true}, 346 | ShouldSucceed: true, 347 | Result: "pi has 3.14 value", 348 | QueryParams: map[string]string{"piValue": "3.14"}, 349 | }, 350 | { 351 | Body: config.Body{Content: `{ "pi": "{{ .piValue | first }}" }`, Templatize: true}, 352 | ShouldSucceed: true, 353 | Result: `{ "pi": "3.14" }`, 354 | QueryParams: map[string]string{"piValue": "3.14"}, 355 | }, 356 | { 357 | Body: config.Body{Content: "pi has {{ .anotherQuery | first }} value", Templatize: true}, 358 | ShouldSucceed: true, 359 | Result: "pi has very high value", 360 | QueryParams: map[string]string{"piValue": "3.14", "anotherQuery": "very high"}, 361 | }, 362 | { 363 | Body: config.Body{Content: "pi has {{ .piValue }} value", Templatize: true}, 364 | ShouldSucceed: false, 365 | QueryParams: map[string]string{"piValue": "3.14", "anotherQuery": "dummy value"}, 366 | }, 367 | { 368 | Body: config.Body{Content: "pi has {{ .piValue }} value", Templatize: true}, 369 | ShouldSucceed: true, 370 | Result: "pi has [3.14] value", 371 | QueryParams: map[string]string{"piValue": "3.14", "anotherQuery": "dummy value"}, 372 | }, 373 | { 374 | Body: config.Body{Content: "value of {{ upper `pi` | repeat 3 }} is {{ .anotherQuery | first }}", Templatize: true}, 375 | ShouldSucceed: true, 376 | Result: "value of PIPIPI is dummy value", 377 | QueryParams: map[string]string{"piValue": "3.14", "anotherQuery": "dummy value"}, 378 | }, 379 | { 380 | Body: config.Body{Content: "pi has {{ .piValue }} value", Templatize: true}, 381 | ShouldSucceed: true, 382 | Result: "pi has [] value", 383 | }, 384 | { 385 | Body: config.Body{Content: "pi has {{ .piValue | first }} value", Templatize: true}, 386 | ShouldSucceed: true, 387 | Result: "pi has value", 388 | }, 389 | { 390 | Body: config.Body{Content: "value of pi is 3.14", Templatize: true}, 391 | ShouldSucceed: true, 392 | }, 393 | } 394 | 395 | for _, test := range bodyTests { 396 | target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 397 | expected := test.Body.Content 398 | if test.Result != "" { 399 | expected = test.Result 400 | } 401 | if got, _ := io.ReadAll(r.Body); string(got) != expected && test.ShouldSucceed { 402 | t.Errorf("POST request body content mismatch (with query params), got: %s, expected: %s", got, expected) 403 | } 404 | w.WriteHeader(http.StatusOK) 405 | })) 406 | 407 | req := httptest.NewRequest("POST", "http://example.com/foo"+"?module=default&target="+target.URL, strings.NewReader(test.Body.Content)) 408 | q := req.URL.Query() 409 | for k, v := range test.QueryParams { 410 | q.Add(k, v) 411 | } 412 | req.URL.RawQuery = q.Encode() 413 | 414 | recorder := httptest.NewRecorder() 415 | c := config.Config{ 416 | Modules: map[string]config.Module{ 417 | "default": { 418 | Body: test.Body, 419 | }, 420 | }, 421 | } 422 | 423 | probeHandler(recorder, req, promslog.NewNopLogger(), c) 424 | 425 | resp := recorder.Result() 426 | respBody, _ := io.ReadAll(resp.Body) 427 | 428 | if resp.StatusCode != http.StatusOK { 429 | t.Fatalf("POST body content failed. Got: %s", respBody) 430 | } 431 | target.Close() 432 | } 433 | } 434 | -------------------------------------------------------------------------------- /config/config.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 config 15 | 16 | import ( 17 | "os" 18 | 19 | pconfig "github.com/prometheus/common/config" 20 | "gopkg.in/yaml.v2" 21 | ) 22 | 23 | // Metric contains values that define a metric 24 | type Metric struct { 25 | Name string 26 | Path string 27 | Labels map[string]string 28 | Type ScrapeType 29 | ValueType ValueType 30 | EpochTimestamp string 31 | Help string 32 | Values map[string]string 33 | } 34 | 35 | type ScrapeType string 36 | 37 | const ( 38 | ValueScrape ScrapeType = "value" // default 39 | ObjectScrape ScrapeType = "object" 40 | ) 41 | 42 | type ValueType string 43 | 44 | const ( 45 | ValueTypeGauge ValueType = "gauge" 46 | ValueTypeCounter ValueType = "counter" 47 | ValueTypeUntyped ValueType = "untyped" 48 | ) 49 | 50 | // Config contains multiple modules. 51 | type Config struct { 52 | Modules map[string]Module `yaml:"modules"` 53 | } 54 | 55 | // Module contains metrics and headers defining a configuration 56 | type Module struct { 57 | Headers map[string]string `yaml:"headers,omitempty"` 58 | Metrics []Metric `yaml:"metrics"` 59 | HTTPClientConfig pconfig.HTTPClientConfig `yaml:"http_client_config,omitempty"` 60 | Body Body `yaml:"body,omitempty"` 61 | ValidStatusCodes []int `yaml:"valid_status_codes,omitempty"` 62 | } 63 | 64 | type Body struct { 65 | Content string `yaml:"content"` 66 | Templatize bool `yaml:"templatize,omitempty"` 67 | } 68 | 69 | func LoadConfig(configPath string) (Config, error) { 70 | var config Config 71 | data, err := os.ReadFile(configPath) 72 | if err != nil { 73 | return config, err 74 | } 75 | 76 | if err := yaml.Unmarshal(data, &config); err != nil { 77 | return config, err 78 | } 79 | 80 | // Complete Defaults 81 | for _, module := range config.Modules { 82 | for i := 0; i < len(module.Metrics); i++ { 83 | if module.Metrics[i].Type == "" { 84 | module.Metrics[i].Type = ValueScrape 85 | } 86 | if module.Metrics[i].Help == "" { 87 | module.Metrics[i].Help = module.Metrics[i].Name 88 | } 89 | if module.Metrics[i].ValueType == "" { 90 | module.Metrics[i].ValueType = ValueTypeUntyped 91 | } 92 | } 93 | } 94 | 95 | return config, nil 96 | } 97 | -------------------------------------------------------------------------------- /examples/animal-data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "noun": "lion", 4 | "population": 123, 5 | "predator": true 6 | }, 7 | { 8 | "noun": "deer", 9 | "population": 456, 10 | "predator": false 11 | }, 12 | { 13 | "noun": "pigeon", 14 | "population": 789, 15 | "predator": false 16 | } 17 | ] 18 | -------------------------------------------------------------------------------- /examples/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | modules: 3 | default: 4 | headers: 5 | X-Dummy: my-test-header 6 | metrics: 7 | - name: example_global_value 8 | path: '{ .counter }' 9 | help: Example of a top-level global value scrape in the json 10 | labels: 11 | environment: beta # static label 12 | location: 'planet-{.location}' # dynamic label 13 | - name: example_timestamped_value 14 | type: object 15 | path: '{ .values[?(@.state == "INACTIVE")] }' 16 | epochTimestamp: '{ .timestamp }' 17 | help: Example of a timestamped value scrape in the json 18 | labels: 19 | environment: beta # static label 20 | values: 21 | count: '{.count}' # dynamic value 22 | - name: example_value 23 | type: object 24 | help: Example of sub-level value scrapes from a json 25 | path: '{.values[?(@.state == "ACTIVE")]}' 26 | labels: 27 | environment: beta # static label 28 | id: '{.id}' # dynamic label 29 | values: 30 | active: 1 # static value 31 | count: '{.count}' # dynamic value 32 | boolean: '{.some_boolean}' 33 | 34 | animals: 35 | metrics: 36 | - name: animal 37 | type: object 38 | help: Example of top-level lists in a separate module 39 | path: '{ [*] }' 40 | labels: 41 | name: '{ .noun }' 42 | predator: '{ .predator }' 43 | values: 44 | population: '{ .population }' 45 | 46 | ## HTTP connection configurations can be set in 'modules..http_client_config' field. For full http client config parameters, ref: https://pkg.go.dev/github.com/prometheus/common/config?tab=doc#HTTPClientConfig 47 | # 48 | # http_client_config: 49 | # tls_config: 50 | # insecure_skip_verify: true 51 | # basic_auth: 52 | # username: myuser 53 | # #password: veryverysecret 54 | # password_file: /tmp/mysecret.txt 55 | 56 | ## List of accepted status codes for this probe can be set in 'modules..valid_status_codes' field. Defaults to 2xx. 57 | # valid_status_codes: [ , ... | default = 2xx ] 58 | 59 | ## If 'modueles..body' field is set, it will be sent by the exporter as the body content in the scrape request. The HTTP method will also be set as 'POST' in this case. 60 | # body: 61 | # content: | 62 | # {"time_diff": "1m25s", "anotherVar": "some value"} 63 | 64 | ## The body content can also be a Go Template (https://golang.org/pkg/text/template), with all the functions from the Sprig library (https://masterminds.github.io/sprig/) available. All the query parameters sent by prometheus in the scrape query to the exporter, are available in the template. 65 | # body: 66 | # content: | 67 | # {"time_diff": "{{ duration `95` }}","anotherVar": "{{ .myVal | first }}"} 68 | # templatize: true 69 | 70 | -------------------------------------------------------------------------------- /examples/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "counter": 1234, 3 | "timestamp": 1657568506, 4 | "values": [ 5 | { 6 | "id": "id-A", 7 | "count": 1, 8 | "some_boolean": true, 9 | "state": "ACTIVE" 10 | }, 11 | { 12 | "id": "id-B", 13 | "count": 2, 14 | "some_boolean": true, 15 | "state": "INACTIVE" 16 | }, 17 | { 18 | "id": "id-C", 19 | "count": 3, 20 | "some_boolean": false, 21 | "state": "ACTIVE" 22 | } 23 | ], 24 | "location": "mars" 25 | } 26 | -------------------------------------------------------------------------------- /examples/prometheus.yml: -------------------------------------------------------------------------------- 1 | scrape_configs: 2 | 3 | ## gather metrics of prometheus itself 4 | - job_name: prometheus 5 | static_configs: 6 | - targets: 7 | - host.docker.internal:9090 # equivalent to "localhost:9090" 8 | 9 | ## gather the metrics of json_exporter application itself 10 | - job_name: json_exporter 11 | static_configs: 12 | - targets: 13 | ## Location of the json exporter's real : 14 | - host.docker.internal:7979 # equivalent to "localhost:7979" 15 | 16 | ## gather the metrics from third party json sources, via the json exporter 17 | - job_name: json 18 | metrics_path: /probe 19 | params: 20 | module: [default] 21 | static_configs: 22 | - targets: 23 | - http://host-1.foobar.com/dummy/data.json 24 | - http://host-2:8000/other-examples/data.json 25 | - http://localhost:8000/examples/data.json ## Used from the example steps in Readme 26 | relabel_configs: 27 | - source_labels: [__address__] 28 | target_label: __param_target 29 | - source_labels: [__param_target] 30 | target_label: instance 31 | - target_label: __address__ 32 | ## Location of the json exporter's real : 33 | replacement: host.docker.internal:7979 # equivalent to "localhost:7979" 34 | 35 | -------------------------------------------------------------------------------- /exporter/collector.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 exporter 15 | 16 | import ( 17 | "bytes" 18 | "encoding/json" 19 | "log/slog" 20 | "time" 21 | 22 | "github.com/prometheus-community/json_exporter/config" 23 | "github.com/prometheus/client_golang/prometheus" 24 | "k8s.io/client-go/util/jsonpath" 25 | ) 26 | 27 | type JSONMetricCollector struct { 28 | JSONMetrics []JSONMetric 29 | Data []byte 30 | Logger *slog.Logger 31 | } 32 | 33 | type JSONMetric struct { 34 | Desc *prometheus.Desc 35 | Type config.ScrapeType 36 | KeyJSONPath string 37 | ValueJSONPath string 38 | LabelsJSONPaths []string 39 | ValueType prometheus.ValueType 40 | EpochTimestampJSONPath string 41 | } 42 | 43 | func (mc JSONMetricCollector) Describe(ch chan<- *prometheus.Desc) { 44 | for _, m := range mc.JSONMetrics { 45 | ch <- m.Desc 46 | } 47 | } 48 | 49 | func (mc JSONMetricCollector) Collect(ch chan<- prometheus.Metric) { 50 | for _, m := range mc.JSONMetrics { 51 | switch m.Type { 52 | case config.ValueScrape: 53 | value, err := extractValue(mc.Logger, mc.Data, m.KeyJSONPath, false) 54 | if err != nil { 55 | mc.Logger.Error("Failed to extract value for metric", "path", m.KeyJSONPath, "err", err, "metric", m.Desc) 56 | continue 57 | } 58 | 59 | if floatValue, err := SanitizeValue(value); err == nil { 60 | metric := prometheus.MustNewConstMetric( 61 | m.Desc, 62 | m.ValueType, 63 | floatValue, 64 | extractLabels(mc.Logger, mc.Data, m.LabelsJSONPaths)..., 65 | ) 66 | ch <- timestampMetric(mc.Logger, m, mc.Data, metric) 67 | } else { 68 | mc.Logger.Error("Failed to convert extracted value to float64", "path", m.KeyJSONPath, "value", value, "err", err, "metric", m.Desc) 69 | continue 70 | } 71 | 72 | case config.ObjectScrape: 73 | values, err := extractValue(mc.Logger, mc.Data, m.KeyJSONPath, true) 74 | if err != nil { 75 | mc.Logger.Error("Failed to extract json objects for metric", "err", err, "metric", m.Desc) 76 | continue 77 | } 78 | 79 | var jsonData []interface{} 80 | if err := json.Unmarshal([]byte(values), &jsonData); err == nil { 81 | for _, data := range jsonData { 82 | jdata, err := json.Marshal(data) 83 | if err != nil { 84 | mc.Logger.Error("Failed to marshal data to json", "path", m.ValueJSONPath, "err", err, "metric", m.Desc, "data", data) 85 | continue 86 | } 87 | value, err := extractValue(mc.Logger, jdata, m.ValueJSONPath, false) 88 | if err != nil { 89 | mc.Logger.Error("Failed to extract value for metric", "path", m.ValueJSONPath, "err", err, "metric", m.Desc) 90 | continue 91 | } 92 | 93 | if floatValue, err := SanitizeValue(value); err == nil { 94 | metric := prometheus.MustNewConstMetric( 95 | m.Desc, 96 | m.ValueType, 97 | floatValue, 98 | extractLabels(mc.Logger, jdata, m.LabelsJSONPaths)..., 99 | ) 100 | ch <- timestampMetric(mc.Logger, m, jdata, metric) 101 | } else { 102 | mc.Logger.Error("Failed to convert extracted value to float64", "path", m.ValueJSONPath, "value", value, "err", err, "metric", m.Desc) 103 | continue 104 | } 105 | } 106 | } else { 107 | mc.Logger.Error("Failed to convert extracted objects to json", "err", err, "metric", m.Desc) 108 | continue 109 | } 110 | default: 111 | mc.Logger.Error("Unknown scrape config type", "type", m.Type, "metric", m.Desc) 112 | continue 113 | } 114 | } 115 | } 116 | 117 | // Returns the last matching value at the given json path 118 | func extractValue(logger *slog.Logger, data []byte, path string, enableJSONOutput bool) (string, error) { 119 | var jsonData interface{} 120 | buf := new(bytes.Buffer) 121 | 122 | j := jsonpath.New("jp") 123 | if enableJSONOutput { 124 | j.EnableJSONOutput(true) 125 | } 126 | 127 | if err := json.Unmarshal(data, &jsonData); err != nil { 128 | logger.Error("Failed to unmarshal data to json", "err", err, "data", data) 129 | return "", err 130 | } 131 | 132 | if err := j.Parse(path); err != nil { 133 | logger.Error("Failed to parse jsonpath", "err", err, "path", path, "data", data) 134 | return "", err 135 | } 136 | 137 | if err := j.Execute(buf, jsonData); err != nil { 138 | logger.Error("Failed to execute jsonpath", "err", err, "path", path, "data", data) 139 | return "", err 140 | } 141 | 142 | // Since we are finally going to extract only float64, unquote if necessary 143 | if res, err := jsonpath.UnquoteExtend(buf.String()); err == nil { 144 | return res, nil 145 | } 146 | 147 | return buf.String(), nil 148 | } 149 | 150 | // Returns the list of labels created from the list of provided json paths 151 | func extractLabels(logger *slog.Logger, data []byte, paths []string) []string { 152 | labels := make([]string, len(paths)) 153 | for i, path := range paths { 154 | if result, err := extractValue(logger, data, path, false); err == nil { 155 | labels[i] = result 156 | } else { 157 | logger.Error("Failed to extract label value", "err", err, "path", path, "data", data) 158 | } 159 | } 160 | return labels 161 | } 162 | 163 | func timestampMetric(logger *slog.Logger, m JSONMetric, data []byte, pm prometheus.Metric) prometheus.Metric { 164 | if m.EpochTimestampJSONPath == "" { 165 | return pm 166 | } 167 | ts, err := extractValue(logger, data, m.EpochTimestampJSONPath, false) 168 | if err != nil { 169 | logger.Error("Failed to extract timestamp for metric", "path", m.KeyJSONPath, "err", err, "metric", m.Desc) 170 | return pm 171 | } 172 | epochTime, err := SanitizeIntValue(ts) 173 | if err != nil { 174 | logger.Error("Failed to parse timestamp for metric", "path", m.KeyJSONPath, "err", err, "metric", m.Desc) 175 | return pm 176 | } 177 | timestamp := time.UnixMilli(epochTime) 178 | return prometheus.NewMetricWithTimestamp(timestamp, pm) 179 | } 180 | -------------------------------------------------------------------------------- /exporter/util.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 exporter 15 | 16 | import ( 17 | "context" 18 | "errors" 19 | "fmt" 20 | "io" 21 | "log/slog" 22 | "math" 23 | "net/http" 24 | "net/url" 25 | "strconv" 26 | "strings" 27 | "text/template" 28 | 29 | "github.com/Masterminds/sprig/v3" 30 | "github.com/prometheus-community/json_exporter/config" 31 | "github.com/prometheus/client_golang/prometheus" 32 | pconfig "github.com/prometheus/common/config" 33 | ) 34 | 35 | func MakeMetricName(parts ...string) string { 36 | return strings.Join(parts, "_") 37 | } 38 | 39 | func SanitizeValue(s string) (float64, error) { 40 | var err error 41 | var value float64 42 | var resultErr string 43 | 44 | if value, err = strconv.ParseFloat(s, 64); err == nil { 45 | return value, nil 46 | } 47 | resultErr = fmt.Sprintf("%s", err) 48 | 49 | if boolValue, err := strconv.ParseBool(s); err == nil { 50 | if boolValue { 51 | return 1.0, nil 52 | } 53 | return 0.0, nil 54 | } 55 | resultErr = resultErr + "; " + fmt.Sprintf("%s", err) 56 | 57 | if s == "" { 58 | return math.NaN(), nil 59 | } 60 | return value, errors.New(resultErr) 61 | } 62 | 63 | func SanitizeIntValue(s string) (int64, error) { 64 | var err error 65 | var value int64 66 | var resultErr string 67 | 68 | if value, err = strconv.ParseInt(s, 10, 64); err == nil { 69 | return value, nil 70 | } 71 | resultErr = fmt.Sprintf("%s", err) 72 | 73 | return value, errors.New(resultErr) 74 | } 75 | 76 | func CreateMetricsList(c config.Module) ([]JSONMetric, error) { 77 | var ( 78 | metrics []JSONMetric 79 | valueType prometheus.ValueType 80 | ) 81 | for _, metric := range c.Metrics { 82 | switch metric.ValueType { 83 | case config.ValueTypeGauge: 84 | valueType = prometheus.GaugeValue 85 | case config.ValueTypeCounter: 86 | valueType = prometheus.CounterValue 87 | default: 88 | valueType = prometheus.UntypedValue 89 | } 90 | switch metric.Type { 91 | case config.ValueScrape: 92 | var variableLabels, variableLabelsValues []string 93 | for k, v := range metric.Labels { 94 | variableLabels = append(variableLabels, k) 95 | variableLabelsValues = append(variableLabelsValues, v) 96 | } 97 | jsonMetric := JSONMetric{ 98 | Type: config.ValueScrape, 99 | Desc: prometheus.NewDesc( 100 | metric.Name, 101 | metric.Help, 102 | variableLabels, 103 | nil, 104 | ), 105 | KeyJSONPath: metric.Path, 106 | LabelsJSONPaths: variableLabelsValues, 107 | ValueType: valueType, 108 | EpochTimestampJSONPath: metric.EpochTimestamp, 109 | } 110 | metrics = append(metrics, jsonMetric) 111 | case config.ObjectScrape: 112 | for subName, valuePath := range metric.Values { 113 | name := MakeMetricName(metric.Name, subName) 114 | var variableLabels, variableLabelsValues []string 115 | for k, v := range metric.Labels { 116 | variableLabels = append(variableLabels, k) 117 | variableLabelsValues = append(variableLabelsValues, v) 118 | } 119 | jsonMetric := JSONMetric{ 120 | Type: config.ObjectScrape, 121 | Desc: prometheus.NewDesc( 122 | name, 123 | metric.Help, 124 | variableLabels, 125 | nil, 126 | ), 127 | KeyJSONPath: metric.Path, 128 | ValueJSONPath: valuePath, 129 | LabelsJSONPaths: variableLabelsValues, 130 | ValueType: valueType, 131 | EpochTimestampJSONPath: metric.EpochTimestamp, 132 | } 133 | metrics = append(metrics, jsonMetric) 134 | } 135 | default: 136 | return nil, fmt.Errorf("Unknown metric type: '%s', for metric: '%s'", metric.Type, metric.Name) 137 | } 138 | } 139 | return metrics, nil 140 | } 141 | 142 | type JSONFetcher struct { 143 | module config.Module 144 | ctx context.Context 145 | logger *slog.Logger 146 | method string 147 | body io.Reader 148 | } 149 | 150 | func NewJSONFetcher(ctx context.Context, logger *slog.Logger, m config.Module, tplValues url.Values) *JSONFetcher { 151 | method, body := renderBody(logger, m.Body, tplValues) 152 | return &JSONFetcher{ 153 | module: m, 154 | ctx: ctx, 155 | logger: logger, 156 | method: method, 157 | body: body, 158 | } 159 | } 160 | 161 | func (f *JSONFetcher) FetchJSON(endpoint string) ([]byte, error) { 162 | httpClientConfig := f.module.HTTPClientConfig 163 | client, err := pconfig.NewClientFromConfig(httpClientConfig, "fetch_json", pconfig.WithKeepAlivesDisabled(), pconfig.WithHTTP2Disabled()) 164 | if err != nil { 165 | f.logger.Error("Error generating HTTP client", "err", err) 166 | return nil, err 167 | } 168 | 169 | var req *http.Request 170 | req, err = http.NewRequest(f.method, endpoint, f.body) 171 | req = req.WithContext(f.ctx) 172 | if err != nil { 173 | f.logger.Error("Failed to create request", "err", err) 174 | return nil, err 175 | } 176 | 177 | for key, value := range f.module.Headers { 178 | req.Header.Add(key, value) 179 | } 180 | if req.Header.Get("Accept") == "" { 181 | req.Header.Add("Accept", "application/json") 182 | } 183 | resp, err := client.Do(req) 184 | if err != nil { 185 | return nil, err 186 | } 187 | 188 | defer func() { 189 | if _, err := io.Copy(io.Discard, resp.Body); err != nil { 190 | f.logger.Error("Failed to discard body", "err", err) 191 | } 192 | resp.Body.Close() 193 | }() 194 | 195 | if len(f.module.ValidStatusCodes) != 0 { 196 | success := false 197 | for _, code := range f.module.ValidStatusCodes { 198 | if resp.StatusCode == code { 199 | success = true 200 | break 201 | } 202 | } 203 | if !success { 204 | return nil, errors.New(resp.Status) 205 | } 206 | } else if resp.StatusCode/100 != 2 { 207 | return nil, errors.New(resp.Status) 208 | } 209 | 210 | data, err := io.ReadAll(resp.Body) 211 | if err != nil { 212 | return nil, err 213 | } 214 | 215 | return data, nil 216 | } 217 | 218 | // Use the configured template to render the body if enabled 219 | // Do not treat template errors as fatal, on such errors just log them 220 | // and continue with static body content 221 | func renderBody(logger *slog.Logger, body config.Body, tplValues url.Values) (method string, br io.Reader) { 222 | method = "POST" 223 | if body.Content == "" { 224 | return "GET", nil 225 | } 226 | br = strings.NewReader(body.Content) 227 | if body.Templatize { 228 | tpl, err := template.New("base").Funcs(sprig.TxtFuncMap()).Parse(body.Content) 229 | if err != nil { 230 | logger.Error("Failed to create a new template from body content", "err", err, "content", body.Content) 231 | return 232 | } 233 | tpl = tpl.Option("missingkey=zero") 234 | var b strings.Builder 235 | if err := tpl.Execute(&b, tplValues); err != nil { 236 | logger.Error("Failed to render template with values", "err", err, "tempalte", body.Content) 237 | 238 | // `tplValues` can contain sensitive values, so log it only when in debug mode 239 | logger.Debug("Failed to render template with values", "err", err, "tempalte", body.Content, "values", tplValues, "rendered_body", b.String()) 240 | return 241 | } 242 | br = strings.NewReader(b.String()) 243 | } 244 | return 245 | } 246 | -------------------------------------------------------------------------------- /exporter/util_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 exporter 15 | 16 | import ( 17 | "math" 18 | "testing" 19 | ) 20 | 21 | func TestSanitizeValue(t *testing.T) { 22 | tests := []struct { 23 | Input string 24 | ExpectedOutput float64 25 | ShouldSucceed bool 26 | }{ 27 | {"1234", 1234.0, true}, 28 | {"1234.5", 1234.5, true}, 29 | {"true", 1.0, true}, 30 | {"TRUE", 1.0, true}, 31 | {"False", 0.0, true}, 32 | {"FALSE", 0.0, true}, 33 | {"abcd", 0, false}, 34 | {"{}", 0, false}, 35 | {"[]", 0, false}, 36 | {"", 0, false}, 37 | {"''", 0, false}, 38 | } 39 | 40 | for i, test := range tests { 41 | actualOutput, err := SanitizeValue(test.Input) 42 | if err != nil && test.ShouldSucceed { 43 | t.Fatalf("Value snitization test %d failed with an unexpected error.\nINPUT:\n%q\nERR:\n%s", i, test.Input, err) 44 | } 45 | if test.ShouldSucceed && actualOutput != test.ExpectedOutput { 46 | t.Fatalf("Value sanitization test %d fails unexpectedly.\nGOT:\n%f\nEXPECTED:\n%f", i, actualOutput, test.ExpectedOutput) 47 | } 48 | } 49 | } 50 | 51 | func TestSanitizeValueNaN(t *testing.T) { 52 | actualOutput, err := SanitizeValue("") 53 | if err != nil { 54 | t.Fatal(err) 55 | } 56 | if !math.IsNaN(actualOutput) { 57 | t.Fatalf("Value sanitization test for %f fails unexpectedly.", math.NaN()) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/prometheus-community/json_exporter 2 | 3 | go 1.22.0 4 | 5 | require ( 6 | github.com/Masterminds/sprig/v3 v3.3.0 7 | github.com/alecthomas/kingpin/v2 v2.4.0 8 | github.com/prometheus/client_golang v1.21.0 9 | github.com/prometheus/common v0.62.0 10 | github.com/prometheus/exporter-toolkit v0.13.2 11 | gopkg.in/yaml.v2 v2.4.0 12 | k8s.io/client-go v0.31.5 13 | ) 14 | 15 | require ( 16 | dario.cat/mergo v1.0.1 // indirect 17 | github.com/Masterminds/goutils v1.1.1 // indirect 18 | github.com/Masterminds/semver/v3 v3.3.0 // indirect 19 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect 20 | github.com/beorn7/perks v1.0.1 // indirect 21 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 22 | github.com/coreos/go-systemd/v22 v22.5.0 // indirect 23 | github.com/google/uuid v1.6.0 // indirect 24 | github.com/huandu/xstrings v1.5.0 // indirect 25 | github.com/jpillora/backoff v1.0.0 // indirect 26 | github.com/klauspost/compress v1.17.11 // indirect 27 | github.com/mdlayher/socket v0.4.1 // indirect 28 | github.com/mdlayher/vsock v1.2.1 // indirect 29 | github.com/mitchellh/copystructure v1.2.0 // indirect 30 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 31 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 32 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect 33 | github.com/prometheus/client_model v0.6.1 // indirect 34 | github.com/prometheus/procfs v0.15.1 // indirect 35 | github.com/shopspring/decimal v1.4.0 // indirect 36 | github.com/spf13/cast v1.7.0 // indirect 37 | github.com/xhit/go-str2duration/v2 v2.1.0 // indirect 38 | golang.org/x/crypto v0.31.0 // indirect 39 | golang.org/x/net v0.33.0 // indirect 40 | golang.org/x/oauth2 v0.24.0 // indirect 41 | golang.org/x/sync v0.10.0 // indirect 42 | golang.org/x/sys v0.28.0 // indirect 43 | golang.org/x/text v0.21.0 // indirect 44 | google.golang.org/protobuf v1.36.1 // indirect 45 | ) 46 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= 2 | dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= 3 | github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= 4 | github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= 5 | github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= 6 | github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= 7 | github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= 8 | github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= 9 | github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY= 10 | github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= 11 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc= 12 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= 13 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 14 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 15 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 16 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 17 | github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= 18 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 19 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 20 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 21 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 22 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 23 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 24 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 25 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 26 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 27 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 28 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 29 | github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= 30 | github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= 31 | github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= 32 | github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= 33 | github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= 34 | github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= 35 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 36 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 37 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 38 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 39 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 40 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 41 | github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= 42 | github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= 43 | github.com/mdlayher/vsock v1.2.1 h1:pC1mTJTvjo1r9n9fbm7S1j04rCgCzhCOS5DY0zqHlnQ= 44 | github.com/mdlayher/vsock v1.2.1/go.mod h1:NRfCibel++DgeMD8z/hP+PPTjlNJsdPOmxcnENvE+SE= 45 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 46 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 47 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 48 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 49 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 50 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 51 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= 52 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 53 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 54 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 55 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 56 | github.com/prometheus/client_golang v1.21.0 h1:DIsaGmiaBkSangBgMtWdNfxbMNdku5IK6iNhrEqWvdA= 57 | github.com/prometheus/client_golang v1.21.0/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= 58 | github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= 59 | github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= 60 | github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= 61 | github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= 62 | github.com/prometheus/exporter-toolkit v0.13.2 h1:Z02fYtbqTMy2i/f+xZ+UK5jy/bl1Ex3ndzh06T/Q9DQ= 63 | github.com/prometheus/exporter-toolkit v0.13.2/go.mod h1:tCqnfx21q6qN1KA4U3Bfb8uWzXfijIrJz3/kTIqMV7g= 64 | github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= 65 | github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= 66 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 67 | github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 68 | github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= 69 | github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= 70 | github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= 71 | github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 72 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 73 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 74 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 75 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 76 | github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= 77 | github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= 78 | golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= 79 | golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= 80 | golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= 81 | golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= 82 | golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= 83 | golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= 84 | golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= 85 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 86 | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 87 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 88 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 89 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 90 | google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= 91 | google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 92 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 93 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 94 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 95 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 96 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 97 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 98 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 99 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 100 | k8s.io/client-go v0.31.5 h1:rmDswcUaIFAJ5vJaB82pjyqc52DgHCPv0G6af3OupO0= 101 | k8s.io/client-go v0.31.5/go.mod h1:js93IlRSzRHql9o9zP54N56rMR249uH4+srnSOcFLsU= 102 | -------------------------------------------------------------------------------- /main.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 main 15 | 16 | import ( 17 | "github.com/prometheus-community/json_exporter/cmd" 18 | ) 19 | 20 | func main() { 21 | cmd.Run() 22 | } 23 | -------------------------------------------------------------------------------- /test/config/good.yml: -------------------------------------------------------------------------------- 1 | --- 2 | modules: 3 | default: 4 | metrics: 5 | - name: example_global_value 6 | path: "{ .counter }" 7 | help: Example of a top-level global value scrape in the json 8 | valuetype: gauge 9 | labels: 10 | environment: beta # static label 11 | location: "planet-{.location}" # dynamic label 12 | 13 | - name: example_value 14 | type: object 15 | help: Example of sub-level value scrapes from a json 16 | path: '{.values[?(@.state == "ACTIVE")]}' 17 | valuetype: counter 18 | labels: 19 | environment: beta # static label 20 | id: '{.id}' # dynamic label 21 | values: 22 | active: 1 # static value 23 | count: '{.count}' # dynamic value 24 | boolean: '{.some_boolean}' 25 | -------------------------------------------------------------------------------- /test/response/good.txt: -------------------------------------------------------------------------------- 1 | # HELP example_global_value Example of a top-level global value scrape in the json 2 | # TYPE example_global_value gauge 3 | example_global_value{environment="beta",location="planet-mars"} 1234 4 | # HELP example_value_active Example of sub-level value scrapes from a json 5 | # TYPE example_value_active counter 6 | example_value_active{environment="beta",id="id-A"} 1 7 | example_value_active{environment="beta",id="id-C"} 1 8 | # HELP example_value_boolean Example of sub-level value scrapes from a json 9 | # TYPE example_value_boolean counter 10 | example_value_boolean{environment="beta",id="id-A"} 1 11 | example_value_boolean{environment="beta",id="id-C"} 0 12 | # HELP example_value_count Example of sub-level value scrapes from a json 13 | # TYPE example_value_count counter 14 | example_value_count{environment="beta",id="id-A"} 1 15 | example_value_count{environment="beta",id="id-C"} 3 16 | -------------------------------------------------------------------------------- /test/serve/good.json: -------------------------------------------------------------------------------- 1 | { 2 | "counter": 1234, 3 | "values": [ 4 | { 5 | "id": "id-A", 6 | "count": 1, 7 | "some_boolean": true, 8 | "state": "ACTIVE" 9 | }, 10 | { 11 | "id": "id-B", 12 | "count": 2, 13 | "some_boolean": true, 14 | "state": "INACTIVE" 15 | }, 16 | { 17 | "id": "id-C", 18 | "count": 3, 19 | "some_boolean": false, 20 | "state": "ACTIVE" 21 | } 22 | ], 23 | "location": "mars" 24 | } 25 | -------------------------------------------------------------------------------- /test/serve/repeat-metric.json: -------------------------------------------------------------------------------- 1 | { 2 | "counter": 1234, 3 | "values": [ 4 | { 5 | "id": "id-A", 6 | "count": 1, 7 | "some_boolean": true, 8 | "state": "ACTIVE" 9 | }, 10 | { 11 | "id": "id-B", 12 | "count": 2, 13 | "some_boolean": true, 14 | "state": "INACTIVE" 15 | }, 16 | { 17 | "id": "id-C", 18 | "count": 3, 19 | "some_boolean": true, 20 | "state": "ACTIVE" 21 | }, 22 | { 23 | "id": "id-C", 24 | "count": 4, 25 | "some_boolean": false, 26 | "state": "ACTIVE" 27 | } 28 | ], 29 | "location": "mars" 30 | } 31 | --------------------------------------------------------------------------------