├── .circleci └── config.yml ├── .dockerignore ├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ ├── codeql-analysis.yml │ ├── golangci-lint.yml │ └── lint-pull-request.yml ├── .gitignore ├── .golangci.yml ├── .promu.yml ├── .yamllint ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── Makefile ├── Makefile.common ├── README.md ├── VERSION ├── cmd └── uwsgi_exporter │ ├── main.go │ ├── main_test.go │ └── testdata │ └── landing.html.golden ├── go.mod ├── go.sum ├── pkg └── collector │ ├── dialer_others.go │ ├── dialer_windows.go │ ├── exporter.go │ ├── exporter_test.go │ ├── mockserver_test.go │ ├── reader.go │ ├── reader_file.go │ ├── reader_file_test.go │ ├── reader_http.go │ ├── reader_http_test.go │ ├── reader_tcp.go │ ├── reader_tcp_test.go │ ├── reader_test.go │ ├── reader_unix.go │ ├── reader_unix_test.go │ ├── stats.go │ └── testdata │ ├── sample.json │ └── wrong.json └── scripts └── errcheck_excludes.txt /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | prometheus: prometheus/prometheus@0.17.1 5 | go: circleci/go@1.7.0 6 | 7 | executors: 8 | # Whenever the Go version is updated here, .promu.yml should also be updated. 9 | golang: 10 | docker: 11 | - image: cimg/go:1.20 12 | 13 | jobs: 14 | test: 15 | executor: golang 16 | steps: 17 | - prometheus/setup_environment 18 | - go/load-cache 19 | - run: 20 | command: make 21 | environment: 22 | # By default, Go uses GOMAXPROCS but a Circle CI executor has many 23 | # cores (> 30) while the CPU and RAM resources are throttled. If we 24 | # don't limit this to the number of allocated cores, the job is 25 | # likely to get OOMed and killed. 26 | GOOPTS: "-p 2" 27 | GOMAXPROCS: "2" 28 | - run: git diff --exit-code 29 | - go/save-cache: 30 | path: /go/pkg/mod 31 | 32 | workflows: 33 | version: 2 34 | uwsgi_exporter: 35 | jobs: 36 | - test: 37 | filters: 38 | tags: 39 | only: /.*/ 40 | - prometheus/build: 41 | name: build 42 | parallelism: 3 43 | filters: 44 | tags: 45 | only: /^v[0-9]+(\.[0-9]+){2}(-.+|[^-.]*)$/ 46 | - prometheus/publish_master: 47 | context: timonwong-context 48 | requires: 49 | - test 50 | - build 51 | filters: 52 | branches: 53 | only: master 54 | quay_io_organization: '' 55 | docker_hub_organization: timonwong 56 | - prometheus/publish_release: 57 | context: timonwong-context 58 | requires: 59 | - test 60 | - build 61 | filters: 62 | tags: 63 | only: /^v[0-9]+(\.[0-9]+){2}(-.+|[^-.]*)$/ 64 | branches: 65 | ignore: /.*/ 66 | quay_io_organization: '' 67 | docker_hub_organization: timonwong 68 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .build/ 2 | .tarballs/ 3 | 4 | !.build/linux-amd64/ 5 | !.build/linux-armv7/ 6 | !.build/linux-arm64/ 7 | !.build/linux-ppc64le/ 8 | !.build/linux-s390x/ 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | ########################################################### 4 | ; common 5 | ########################################################### 6 | 7 | [*] 8 | charset = utf-8 9 | 10 | end_of_line = LF 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | 14 | indent_style = space 15 | indent_size = 4 16 | 17 | [Makefile] 18 | indent_style = tab 19 | 20 | [Makefile.common] 21 | indent_style = tab 22 | 23 | [*.{yml,yaml}] 24 | indent_size = 2 25 | 26 | [*.go] 27 | indent_style = tab 28 | 29 | [*.golden] 30 | trim_trailing_whitespace = false 31 | insert_final_newline = false 32 | 33 | [VERSION] 34 | insert_final_newline = false 35 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "master" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "master" ] 20 | schedule: 21 | - cron: '31 17 * * 5' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'go' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v2 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v2 73 | -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | on: 3 | push: 4 | paths: 5 | - "go.sum" 6 | - "go.mod" 7 | - "**.go" 8 | - "scripts/errcheck_excludes.txt" 9 | - ".github/workflows/golangci-lint.yml" 10 | - ".golangci.yml" 11 | pull_request: 12 | 13 | jobs: 14 | golangci: 15 | name: lint 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v3 20 | - name: install Go 21 | uses: actions/setup-go@v2 22 | with: 23 | go-version: 1.20.x 24 | - name: Install snmp_exporter/generator dependencies 25 | run: sudo apt-get update && sudo apt-get -y install libsnmp-dev 26 | if: github.repository == 'prometheus/snmp_exporter' 27 | - name: Lint 28 | uses: golangci/golangci-lint-action@v3.2.0 29 | with: 30 | version: v1.52.1 31 | -------------------------------------------------------------------------------- /.github/workflows/lint-pull-request.yml: -------------------------------------------------------------------------------- 1 | name: Lint Pull Request 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | 10 | jobs: 11 | lint-pr-title: 12 | name: Lint PR Title 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: amannn/action-semantic-pull-request@v4 16 | name: Semantic Pull Request 17 | env: 18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 19 | -------------------------------------------------------------------------------- /.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 | dependencies-stamp 24 | /uwsgi_exporter 25 | /.build 26 | /.deps 27 | /.release 28 | /.tarballs 29 | 30 | coverage.txt 31 | 32 | # VSCode 33 | /.vscode 34 | 35 | # Intellij 36 | /.idea 37 | *.iml 38 | 39 | /vendor 40 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | run: 3 | deadline: 5m 4 | 5 | linters: 6 | enable: 7 | - depguard 8 | - errcheck 9 | - gocritic 10 | - gofumpt 11 | - goimports 12 | - gosec 13 | - loggercheck 14 | - misspell 15 | - revive 16 | - staticcheck 17 | - stylecheck 18 | - unused 19 | 20 | issues: 21 | max-issues-per-linter: 0 22 | max-same-issues: 0 23 | exclude-rules: 24 | - path: _test.go 25 | linters: 26 | - depguard 27 | - errcheck 28 | 29 | linters-settings: 30 | depguard: 31 | list-type: blacklist 32 | include-go-root: true 33 | packages: 34 | - sync/atomic 35 | packages-with-error-message: 36 | - sync/atomic: "Use go.uber.org/atomic instead of sync/atomic" 37 | - github.com/go-kit/kit/log: "Use github.com/go-kit/log instead of github.com/go-kit/kit/log" 38 | errcheck: 39 | exclude: scripts/errcheck_excludes.txt 40 | -------------------------------------------------------------------------------- /.promu.yml: -------------------------------------------------------------------------------- 1 | --- 2 | repository: 3 | path: github.com/timonwong/uwsgi_exporter 4 | go: 5 | version: 1.20 6 | cgo: false 7 | build: 8 | binaries: 9 | - name: uwsgi_exporter 10 | path: ./cmd/uwsgi_exporter 11 | flags: -a -tags 'netgo' 12 | ldflags: | 13 | -X github.com/prometheus/common/version.Version={{.Version}} 14 | -X github.com/prometheus/common/version.Revision={{.Revision}} 15 | -X github.com/prometheus/common/version.Branch={{.Branch}} 16 | -X github.com/prometheus/common/version.BuildUser={{user}}@{{host}} 17 | -X github.com/prometheus/common/version.BuildDate={{date "20060102-15:04:05"}} 18 | tarball: 19 | files: 20 | - LICENSE 21 | crossbuild: 22 | platforms: 23 | - darwin 24 | - dragonfly 25 | - freebsd 26 | - illumos 27 | - linux 28 | - netbsd 29 | - windows 30 | -------------------------------------------------------------------------------- /.yamllint: -------------------------------------------------------------------------------- 1 | --- 2 | extends: default 3 | 4 | rules: 5 | braces: 6 | max-spaces-inside: 1 7 | level: error 8 | brackets: 9 | max-spaces-inside: 1 10 | level: error 11 | commas: disable 12 | comments: disable 13 | comments-indentation: disable 14 | document-start: disable 15 | indentation: 16 | spaces: consistent 17 | indent-sequences: consistent 18 | line-length: disable 19 | truthy: 20 | ignore: | 21 | .github/workflows/*.yml 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.3.0 / 2023-03-23 2 | 3 | ### Major changes 4 | 5 | Now `--stats.timeout` is deprecated and takes no effect. The exporter now honors `X-Prometheus-Scrape-Timeout-Seconds` header 6 | from Prometheus to determine the timeout. If the header is not set, the exporter will use the default timeout value of 120 seconds. 7 | 8 | ### Changes 9 | 10 | * chore: Update readme about web.config file by @timonwong in https://github.com/timonwong/uwsgi_exporter/pull/65 11 | * feat: Add support to multi-targets by @timonwong in https://github.com/timonwong/uwsgi_exporter/pull/66 12 | * docs: add missing changelog by @timonwong in https://github.com/timonwong/uwsgi_exporter/pull/67 13 | * chore: fail scrape if stats reader cannot be created by @timonwong in https://github.com/timonwong/uwsgi_exporter/pull/69 14 | * feat: reduce sockets in TIME-WAIT state by @timonwong in https://github.com/timonwong/uwsgi_exporter/pull/70 15 | 16 | ## 1.2.0 / 2023-03-22 17 | 18 | * build(deps): Bump github.com/prometheus/client_golang from 1.12.1 to 1.13.0 by @dependabot in https://github.com/timonwong/uwsgi_exporter/pull/47 19 | * build(deps): Bump github.com/prometheus/common from 0.34.0 to 0.37.0 by @dependabot in https://github.com/timonwong/uwsgi_exporter/pull/46 20 | * chore: bump github.com/prometheus/client_model from 0.2.0 to 0.3.0 by @dependabot in https://github.com/timonwong/uwsgi_exporter/pull/49 21 | * Bump github.com/stretchr/testify from 1.8.0 to 1.8.1 by @dependabot in https://github.com/timonwong/uwsgi_exporter/pull/50 22 | * feat: allow disable timeout by @timonwong in https://github.com/timonwong/uwsgi_exporter/pull/61 23 | * feat: Add support for "idle" and "cheap" status by @timonwong in https://github.com/timonwong/uwsgi_exporter/pull/62 24 | * ci: fix build by @timonwong in https://github.com/timonwong/uwsgi_exporter/pull/63 25 | * chore: Use exporter toolkit by @timonwong in https://github.com/timonwong/uwsgi_exporter/pull/64 26 | 27 | ## 1.1.0 / 2022-08-11 28 | 29 | * No other changes, just go version bump. 30 | 31 | ## 1.0.0 / 2019-12-18 32 | 33 | *[CHANGE] Change default value `--collect.cores` to `false` #35 34 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG ARCH="amd64" 2 | ARG OS="linux" 3 | 4 | FROM quay.io/prometheus/busybox-${OS}-${ARCH}:latest 5 | LABEL maintainer="Timon Wong " 6 | 7 | ARG ARCH="amd64" 8 | ARG OS="linux" 9 | 10 | COPY .build/${OS}-${ARCH}/uwsgi_exporter /bin/uwsgi_exporter 11 | 12 | USER nobody 13 | EXPOSE 9117 14 | ENTRYPOINT [ "/bin/uwsgi_exporter" ] 15 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2015 The Prometheus Authors 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | 14 | # Needs to be defined before including Makefile.common to auto-generate targets 15 | DOCKER_ARCHS ?= amd64 armv7 arm64 16 | DOCKER_REPO ?= timonwong 17 | 18 | include Makefile.common 19 | 20 | DOCKER_IMAGE_NAME ?= uwsgi-exporter 21 | 22 | STATICCHECK_IGNORE = 23 | 24 | .PHONY: build 25 | build: common-build 26 | 27 | .PHONY: test 28 | test: common-test 29 | -------------------------------------------------------------------------------- /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 which gotestsum),) 53 | GOTEST_DIR := test-results 54 | GOTEST := gotestsum --junitfile $(GOTEST_DIR)/unit-tests.xml -- 55 | endif 56 | endif 57 | 58 | PROMU_VERSION ?= 0.14.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.52.1 65 | # golangci-lint only supports linux, darwin and windows platforms on i386/amd64. 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)) 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 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 | # 'go list' needs to be executed before staticcheck to prepopulate the modules cache. 173 | # Otherwise staticcheck might fail randomly for some reason not yet explained. 174 | $(GO) list -e -compiled -test=true -export=false -deps=true -find=false -tags= -- ./... > /dev/null 175 | $(GOLANGCI_LINT) run $(GOLANGCI_LINT_OPTS) $(pkgs) 176 | endif 177 | 178 | .PHONY: common-yamllint 179 | common-yamllint: 180 | @echo ">> running yamllint on all YAML files in the repository" 181 | ifeq (, $(shell which yamllint)) 182 | @echo "yamllint not installed so skipping" 183 | else 184 | yamllint . 185 | endif 186 | 187 | # For backward-compatibility. 188 | .PHONY: common-staticcheck 189 | common-staticcheck: lint 190 | 191 | .PHONY: common-unused 192 | common-unused: 193 | @echo ">> running check for unused/missing packages in go.mod" 194 | $(GO) mod tidy 195 | @git diff --exit-code -- go.sum go.mod 196 | 197 | .PHONY: common-build 198 | common-build: promu 199 | @echo ">> building binaries" 200 | $(PROMU) build --prefix $(PREFIX) $(PROMU_BINARIES) 201 | 202 | .PHONY: common-tarball 203 | common-tarball: promu 204 | @echo ">> building release tarball" 205 | $(PROMU) tarball --prefix $(PREFIX) $(BIN_DIR) 206 | 207 | .PHONY: common-docker $(BUILD_DOCKER_ARCHS) 208 | common-docker: $(BUILD_DOCKER_ARCHS) 209 | $(BUILD_DOCKER_ARCHS): common-docker-%: 210 | docker build -t "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" \ 211 | -f $(DOCKERFILE_PATH) \ 212 | --build-arg ARCH="$*" \ 213 | --build-arg OS="linux" \ 214 | $(DOCKERBUILD_CONTEXT) 215 | 216 | .PHONY: common-docker-publish $(PUBLISH_DOCKER_ARCHS) 217 | common-docker-publish: $(PUBLISH_DOCKER_ARCHS) 218 | $(PUBLISH_DOCKER_ARCHS): common-docker-publish-%: 219 | docker push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" 220 | 221 | DOCKER_MAJOR_VERSION_TAG = $(firstword $(subst ., ,$(shell cat VERSION))) 222 | .PHONY: common-docker-tag-latest $(TAG_DOCKER_ARCHS) 223 | common-docker-tag-latest: $(TAG_DOCKER_ARCHS) 224 | $(TAG_DOCKER_ARCHS): common-docker-tag-latest-%: 225 | docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:latest" 226 | docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:v$(DOCKER_MAJOR_VERSION_TAG)" 227 | 228 | .PHONY: common-docker-manifest 229 | common-docker-manifest: 230 | 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)) 231 | DOCKER_CLI_EXPERIMENTAL=enabled docker manifest push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)" 232 | 233 | .PHONY: promu 234 | promu: $(PROMU) 235 | 236 | $(PROMU): 237 | $(eval PROMU_TMP := $(shell mktemp -d)) 238 | curl -s -L $(PROMU_URL) | tar -xvzf - -C $(PROMU_TMP) 239 | mkdir -p $(FIRST_GOPATH)/bin 240 | cp $(PROMU_TMP)/promu-$(PROMU_VERSION).$(GO_BUILD_PLATFORM)/promu $(FIRST_GOPATH)/bin/promu 241 | rm -r $(PROMU_TMP) 242 | 243 | .PHONY: proto 244 | proto: 245 | @echo ">> generating code from proto files" 246 | @./scripts/genproto.sh 247 | 248 | ifdef GOLANGCI_LINT 249 | $(GOLANGCI_LINT): 250 | mkdir -p $(FIRST_GOPATH)/bin 251 | curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/$(GOLANGCI_LINT_VERSION)/install.sh \ 252 | | sed -e '/install -d/d' \ 253 | | sh -s -- -b $(FIRST_GOPATH)/bin $(GOLANGCI_LINT_VERSION) 254 | endif 255 | 256 | .PHONY: precheck 257 | precheck:: 258 | 259 | define PRECHECK_COMMAND_template = 260 | precheck:: $(1)_precheck 261 | 262 | PRECHECK_COMMAND_$(1) ?= $(1) $$(strip $$(PRECHECK_OPTIONS_$(1))) 263 | .PHONY: $(1)_precheck 264 | $(1)_precheck: 265 | @if ! $$(PRECHECK_COMMAND_$(1)) 1>/dev/null 2>&1; then \ 266 | echo "Execution of '$$(PRECHECK_COMMAND_$(1))' command failed. Is $(1) installed?"; \ 267 | exit 1; \ 268 | fi 269 | endef 270 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # uWSGI Exporter 2 | 3 | [![CircleCI](https://circleci.com/gh/timonwong/uwsgi_exporter/tree/master.svg?style=shield)][circleci] 4 | [![Docker Pulls](https://img.shields.io/docker/pulls/timonwong/uwsgi-exporter.svg?maxAge=604800)][hub] 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/timonwong/uwsgi_exporter)](https://goreportcard.com/report/github.com/timonwong/uwsgi_exporter) 6 | 7 | Prometheus exporter for [uWSGI] metrics. 8 | 9 | ## Building and running 10 | 11 | ### Build 12 | 13 | ```bash 14 | make 15 | ``` 16 | 17 | ### Running 18 | 19 | #### Single exporter mode 20 | 21 | ```bash 22 | ./uwsgi_exporter 23 | ``` 24 | 25 | #### Multi-target support (BETA) 26 | 27 | This exporter supports the [multi-target pattern](https://prometheus.io/docs/guides/multi-target-exporter/). This allows running a single instance of this exporter for multiple uWSGI targets. 28 | 29 | To use the multi-target functionality, send an http request to the endpoint /probe?target=http://uwsgi1.example.com:5432 where target is set to the URI of the uWSGI instance to scrape metrics from. 30 | 31 | On the prometheus side you can set a scrape config as follows 32 | 33 | Please note that only `http`, `https`, and `tcp` targets are supported (`file` and `unix` targets are disabled for security reasons). 34 | 35 | ```yaml 36 | scrape_configs: 37 | - job_name: uwsgi # To get metrics about the mysql exporter’s targets 38 | static_configs: 39 | - targets: 40 | # All uwsgi hostnames to monitor. 41 | - http://uwsgi1.example.com:5432 42 | - http://uwsgi2.example.com:5432 43 | relabel_configs: 44 | - source_labels: [__address__] 45 | target_label: __param_target 46 | - source_labels: [__param_target] 47 | target_label: instance 48 | - target_label: __address__ 49 | # The uwsgi_exporter host:port 50 | replacement: localhost:9117 51 | ``` 52 | 53 | ### Flags 54 | 55 | | Name | Description | 56 | |--------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------| 57 | | stats.uri | **required** URI for accessing uwsgi stats (currently supports: "http", "https", "unix", "tcp"). | 58 | | stats.timeout | Timeout for trying to get stats from uwsgi. (deprecated) | 59 | | timeout-offset | Offset to subtract from timeout in seconds. (default 0.25) | 60 | | collect.cores | Whether to collect cores information per uwsgi worker. **WARNING** may cause tremendous resource utilization when using gevent engine. (default: false) | 61 | | log.level | Logging verbosity. (default: info) | 62 | | web.config.file | Path to a [web configuration file](#tls-and-basic-authentication) | 63 | | web.listen-address | Address to listen on for web interface and telemetry. (default: ":9117") | 64 | | web.telemetry-path | Path under which to expose metrics. | 65 | | version | Print the version information. | 66 | 67 | ## TLS and basic authentication 68 | 69 | The uWSGI Exporter supports TLS and basic authentication. 70 | 71 | To use TLS and/or basic authentication, you need to pass a configuration file 72 | using the `--web.config.file` parameter. The format of the file is described 73 | [in the exporter-toolkit repository](https://github.com/prometheus/exporter-toolkit/blob/master/docs/web-configuration.md). 74 | 75 | ## Using Docker 76 | 77 | You can deploy this exporter using the Docker image from following registry: 78 | 79 | - [DockerHub]\: [timonwong/uwsgi-exporter](https://registry.hub.docker.com/u/timonwong/uwsgi-exporter/) 80 | 81 | For example: 82 | 83 | ```bash 84 | docker pull timonwong/uwsgi-exporter 85 | 86 | docker run -d -p 9117:9117 timonwong/uwsgi-exporter --stats.uri localhost:8001 87 | ``` 88 | 89 | (uWSGI Stats Server port, 8001 in this example, is configured in `ini` uWSGI configuration files) 90 | 91 | [uwsgi]: https://uwsgi-docs.readthedocs.io 92 | [circleci]: https://circleci.com/gh/timonwong/uwsgi_exporter 93 | [hub]: https://hub.docker.com/r/timonwong/uwsgi-exporter/ 94 | [travis]: https://travis-ci.org/timonwong/uwsgi_exporter 95 | [dockerhub]: https://hub.docker.com 96 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.3.0 2 | -------------------------------------------------------------------------------- /cmd/uwsgi_exporter/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | _ "net/http/pprof" //#nosec 9 | "os" 10 | "strconv" 11 | "time" 12 | 13 | "github.com/alecthomas/kingpin/v2" 14 | "github.com/go-kit/log" 15 | "github.com/go-kit/log/level" 16 | "github.com/prometheus/client_golang/prometheus" 17 | "github.com/prometheus/client_golang/prometheus/promhttp" 18 | "github.com/prometheus/common/promlog" 19 | "github.com/prometheus/common/promlog/flag" 20 | "github.com/prometheus/common/version" 21 | "github.com/prometheus/exporter-toolkit/web" 22 | webflag "github.com/prometheus/exporter-toolkit/web/kingpinflag" 23 | 24 | "github.com/timonwong/uwsgi_exporter/pkg/collector" 25 | ) 26 | 27 | var ( 28 | metricsPath = kingpin.Flag("web.telemetry-path", "Path under which to expose metrics.").Default("/metrics").String() 29 | statsURI = kingpin.Flag("stats.uri", "URI for accessing uwsgi stats.").Default("").String() 30 | timeoutOffset = kingpin.Flag("timeout-offset", "Offset to subtract from timeout in seconds.").Default("0.25").Float64() 31 | _ = kingpin.Flag("stats.timeout", "Timeout for trying to get stats from uwsgi (deprecated).").Duration() 32 | collectCores = kingpin.Flag("collect.cores", "Collect cores information per uwsgi worker.").Default("false").Bool() 33 | webConfig = webflag.AddFlags(kingpin.CommandLine, ":9117") 34 | ) 35 | 36 | func init() { 37 | prometheus.MustRegister(version.NewCollector("uwsgi_exporter")) 38 | } 39 | 40 | func main() { 41 | promlogConfig := &promlog.Config{} 42 | flag.AddFlags(kingpin.CommandLine, promlogConfig) 43 | 44 | kingpin.Version(version.Print("uwsgi_exporter")) 45 | kingpin.HelpFlag.Short('h') 46 | kingpin.Parse() 47 | 48 | logger := promlog.New(promlogConfig) 49 | level.Info(logger).Log("msg", "Starting uwsgi_exporter", "version", version.Info()) 50 | level.Info(logger).Log("msg", "Build context", "build", version.BuildContext()) 51 | 52 | handlerFunc := newHandler(collector.NewMetrics(), logger) 53 | http.Handle(*metricsPath, promhttp.InstrumentMetricHandler(prometheus.DefaultRegisterer, handlerFunc)) 54 | if *metricsPath != "/" && *metricsPath != "" { 55 | landingConfig := web.LandingConfig{ 56 | Name: "uWSGI Exporter", 57 | Description: "Prometheus Exporter for uWSGI.", 58 | Version: version.Info(), 59 | Links: []web.LandingLinks{ 60 | { 61 | Address: *metricsPath, 62 | Text: "Metrics", 63 | }, 64 | }, 65 | } 66 | landingPage, err := web.NewLandingPage(landingConfig) 67 | if err != nil { 68 | level.Error(logger).Log("error", err) 69 | os.Exit(1) 70 | } 71 | http.Handle("/", landingPage) 72 | } 73 | http.HandleFunc("/probe", handleProbe(collector.NewMetrics(), logger)) 74 | http.HandleFunc("/-/healthy", func(w http.ResponseWriter, r *http.Request) { 75 | w.WriteHeader(200) 76 | io.WriteString(w, "ok") 77 | }) 78 | 79 | srv := &http.Server{} //#nosec 80 | err := web.ListenAndServe(srv, webConfig, logger) 81 | if err != nil { 82 | level.Error(logger).Log("msg", "Failed to listen address", "error", err) 83 | os.Exit(1) 84 | } 85 | } 86 | 87 | func newHandler(metrics collector.Metrics, logger log.Logger) http.HandlerFunc { 88 | return func(w http.ResponseWriter, r *http.Request) { 89 | // Use request context for cancellation when connection gets closed. 90 | timeoutSeconds, err := getTimeout(r, *timeoutOffset, logger) 91 | if err != nil { 92 | http.Error(w, err.Error(), http.StatusInternalServerError) 93 | return 94 | } 95 | 96 | ctx, cancel := context.WithTimeout(r.Context(), time.Duration(timeoutSeconds*float64(time.Second))) 97 | defer cancel() 98 | r = r.WithContext(ctx) 99 | 100 | registry := prometheus.NewRegistry() 101 | 102 | if *statsURI != "" { 103 | statsReader, err := collector.NewStatsReader(*statsURI) 104 | if err != nil { 105 | level.Error(logger).Log("msg", "Failed to create stats reader", "error", err) 106 | http.Error(w, err.Error(), http.StatusBadRequest) 107 | return 108 | } 109 | 110 | registry.MustRegister(collector.New(ctx, *statsURI, statsReader, metrics, collector.ExporterOptions{ 111 | Logger: logger, 112 | CollectCores: *collectCores, 113 | })) 114 | } 115 | 116 | gatherers := prometheus.Gatherers{ 117 | prometheus.DefaultGatherer, 118 | registry, 119 | } 120 | // Delegate http serving to Prometheus client library, which will call collector.Collect. 121 | h := promhttp.HandlerFor(gatherers, promhttp.HandlerOpts{}) 122 | h.ServeHTTP(w, r) 123 | } 124 | } 125 | 126 | func handleProbe(metrics collector.Metrics, logger log.Logger) http.HandlerFunc { 127 | return func(w http.ResponseWriter, r *http.Request) { 128 | params := r.URL.Query() 129 | target := params.Get("target") 130 | if target == "" { 131 | http.Error(w, "target is required", http.StatusBadRequest) 132 | return 133 | } 134 | 135 | timeoutSeconds, err := getTimeout(r, *timeoutOffset, logger) 136 | if err != nil { 137 | http.Error(w, err.Error(), http.StatusInternalServerError) 138 | return 139 | } 140 | 141 | ctx, cancel := context.WithTimeout(r.Context(), time.Duration(timeoutSeconds*float64(time.Second))) 142 | defer cancel() 143 | r = r.WithContext(ctx) 144 | 145 | statsReader, err := collector.NewStatsReader(target, collector.WithRequireSafeScheme(true)) 146 | if err != nil { 147 | level.Error(logger).Log("msg", "Failed to create stats reader", "error", err) 148 | http.Error(w, err.Error(), http.StatusBadRequest) 149 | return 150 | } 151 | 152 | registry := prometheus.NewRegistry() 153 | registry.MustRegister(collector.New(ctx, target, statsReader, metrics, collector.ExporterOptions{ 154 | Logger: logger, 155 | CollectCores: *collectCores, 156 | })) 157 | 158 | h := promhttp.HandlerFor(registry, promhttp.HandlerOpts{}) 159 | h.ServeHTTP(w, r) 160 | } 161 | } 162 | 163 | func getTimeout(r *http.Request, offset float64, logger log.Logger) (timeoutSeconds float64, err error) { 164 | // If a timeout is configured via the Prometheus header, add it to the request. 165 | if v := r.Header.Get("X-Prometheus-Scrape-Timeout-Seconds"); v != "" { 166 | var err error 167 | timeoutSeconds, err = strconv.ParseFloat(v, 64) 168 | if err != nil { 169 | return 0, fmt.Errorf("failed to parse timeout from Prometheus header: %w", err) 170 | } 171 | } 172 | if timeoutSeconds == 0 { 173 | timeoutSeconds = 120 174 | } 175 | 176 | if offset >= timeoutSeconds { 177 | // Ignore timeout offset if it doesn't leave time to scrape. 178 | level.Error(logger).Log("msg", "Timeout offset should be lower than prometheus scrape timeout", "offset", offset, "prometheus_scrape_timeout", timeoutSeconds) 179 | } else { 180 | // Subtract timeout offset from timeout. 181 | timeoutSeconds -= offset 182 | } 183 | 184 | return timeoutSeconds, nil 185 | } 186 | -------------------------------------------------------------------------------- /cmd/uwsgi_exporter/main_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "context" 18 | _ "embed" 19 | "fmt" 20 | "io" 21 | "net" 22 | "net/http" 23 | "net/url" 24 | "os" 25 | "os/exec" 26 | "reflect" 27 | "runtime" 28 | "strings" 29 | "syscall" 30 | "testing" 31 | "time" 32 | 33 | "github.com/alecthomas/assert/v2" 34 | ) 35 | 36 | // bin stores information about path of executable and attached port 37 | type bin struct { 38 | path string 39 | port int 40 | } 41 | 42 | // TestBin builds, runs and tests binary. 43 | func TestBin(t *testing.T) { 44 | var err error 45 | binName := "uwsgi_exporter" 46 | binDir := t.TempDir() 47 | importpath := "github.com/prometheus/common" 48 | path := binDir + "/" + binName 49 | xVariables := map[string]string{ 50 | importpath + "/version.Version": "gotest-version", 51 | importpath + "/version.Branch": "gotest-branch", 52 | importpath + "/version.Revision": "gotest-revision", 53 | } 54 | var ldflags []string 55 | for x, value := range xVariables { 56 | ldflags = append(ldflags, fmt.Sprintf("-X %s=%s", x, value)) 57 | } 58 | cmd := exec.Command( 59 | "go", 60 | "build", 61 | "-o", 62 | path, 63 | "-ldflags", 64 | strings.Join(ldflags, " "), 65 | ) 66 | cmd.Stdout = os.Stdout 67 | cmd.Stderr = os.Stderr 68 | err = cmd.Run() 69 | assert.NoError(t, err) 70 | 71 | tests := []func(*testing.T, bin){ 72 | testLanding, 73 | testProbe, 74 | } 75 | 76 | portStart := 56000 77 | t.Run(binName, func(t *testing.T) { 78 | for _, f := range tests { 79 | f := f // capture range variable 80 | fName := runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name() 81 | portStart++ 82 | data := bin{ 83 | path: path, 84 | port: portStart, 85 | } 86 | t.Run(fName, func(t *testing.T) { 87 | t.Parallel() 88 | f(t, data) 89 | }) 90 | } 91 | }) 92 | } 93 | 94 | //go:embed testdata/landing.html.golden 95 | var landingHTMLData string 96 | 97 | func testLanding(t *testing.T, data bin) { 98 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 99 | defer cancel() 100 | 101 | // Run exporter. 102 | //#nosec 103 | cmd := exec.CommandContext( 104 | ctx, 105 | data.path, 106 | "--web.listen-address", fmt.Sprintf(":%d", data.port), 107 | ) 108 | if err := cmd.Start(); err != nil { 109 | t.Fatal(err) 110 | } 111 | defer cmd.Wait() 112 | defer cmd.Process.Kill() 113 | 114 | // Get the main page. 115 | urlToGet := fmt.Sprintf("http://127.0.0.1:%d", data.port) 116 | body, err := waitForBody(urlToGet) 117 | assert.NoError(t, err) 118 | got := string(body) 119 | 120 | assert.Equal(t, landingHTMLData, got) 121 | } 122 | 123 | func testProbe(t *testing.T, data bin) { 124 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 125 | defer cancel() 126 | 127 | // Run exporter. 128 | //#nosec 129 | cmd := exec.CommandContext( 130 | ctx, 131 | data.path, 132 | "--web.listen-address", fmt.Sprintf(":%d", data.port), 133 | ) 134 | if err := cmd.Start(); err != nil { 135 | t.Fatal(err) 136 | } 137 | defer cmd.Wait() 138 | defer cmd.Process.Kill() 139 | 140 | // Get the main page. 141 | urlToGet := fmt.Sprintf("http://127.0.0.1:%d/probe", data.port) 142 | body, err := waitForBody(urlToGet) 143 | assert.NoError(t, err) 144 | got := strings.TrimSpace(string(body)) 145 | 146 | expected := `target is required` 147 | 148 | assert.Equal(t, expected, got) 149 | } 150 | 151 | // waitForBody is a helper function which makes http calls until http server is up 152 | // and then returns body of the successful call. 153 | func waitForBody(urlToGet string) (body []byte, err error) { 154 | tries := 60 155 | 156 | // Get data, but we need to wait a bit for http server. 157 | for i := 0; i <= tries; i++ { 158 | // Try to get web page. 159 | body, err = getBody(urlToGet) 160 | if err == nil { 161 | return body, err 162 | } 163 | 164 | // If there is a syscall.ECONNREFUSED error (web server not available) then retry. 165 | if urlError, ok := err.(*url.Error); ok { 166 | if opError, ok := urlError.Err.(*net.OpError); ok { 167 | if osSyscallError, ok := opError.Err.(*os.SyscallError); ok { 168 | if osSyscallError.Err == syscall.ECONNREFUSED { 169 | time.Sleep(1 * time.Second) 170 | continue 171 | } 172 | } 173 | } 174 | } 175 | 176 | // There was an error, and it wasn't syscall.ECONNREFUSED. 177 | return nil, err 178 | } 179 | 180 | return nil, fmt.Errorf("failed to GET %s after %d tries: %w", urlToGet, tries, err) 181 | } 182 | 183 | // getBody is a helper function which retrieves http body from given address. 184 | func getBody(urlToGet string) ([]byte, error) { 185 | resp, err := http.Get(urlToGet) //#nosec 186 | if err != nil { 187 | return nil, err 188 | } 189 | defer resp.Body.Close() 190 | 191 | body, err := io.ReadAll(resp.Body) 192 | if err != nil { 193 | return nil, err 194 | } 195 | 196 | return body, nil 197 | } 198 | -------------------------------------------------------------------------------- /cmd/uwsgi_exporter/testdata/landing.html.golden: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | uWSGI Exporter 6 | 20 | 21 | 22 |
23 |

uWSGI Exporter

24 |
25 |
26 |

Prometheus Exporter for uWSGI.

27 |
Version: (version=gotest-version, branch=gotest-branch, revision=gotest-revision)
28 |
29 | 34 |
35 |
36 | 37 | 38 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/timonwong/uwsgi_exporter 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/alecthomas/assert/v2 v2.2.2 7 | github.com/alecthomas/kingpin/v2 v2.3.2 8 | github.com/go-kit/log v0.2.1 9 | github.com/prometheus/client_golang v1.15.0 10 | github.com/prometheus/client_model v0.3.0 11 | github.com/prometheus/common v0.42.0 12 | github.com/prometheus/exporter-toolkit v0.9.1 13 | github.com/samber/lo v1.38.1 14 | ) 15 | 16 | require ( 17 | github.com/alecthomas/repr v0.2.0 // indirect 18 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect 19 | github.com/beorn7/perks v1.0.1 // indirect 20 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 21 | github.com/coreos/go-systemd/v22 v22.5.0 // indirect 22 | github.com/go-logfmt/logfmt v0.5.1 // indirect 23 | github.com/golang/protobuf v1.5.3 // indirect 24 | github.com/hexops/gotextdiff v1.0.3 // indirect 25 | github.com/jpillora/backoff v1.0.0 // indirect 26 | github.com/kr/text v0.2.0 // indirect 27 | github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect 28 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect 29 | github.com/prometheus/procfs v0.9.0 // indirect 30 | github.com/rogpeppe/go-internal v1.9.0 // indirect 31 | github.com/xhit/go-str2duration/v2 v2.1.0 // indirect 32 | golang.org/x/crypto v0.7.0 // indirect 33 | golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect 34 | golang.org/x/net v0.8.0 // indirect 35 | golang.org/x/oauth2 v0.6.0 // indirect 36 | golang.org/x/sync v0.1.0 // indirect 37 | golang.org/x/sys v0.6.0 // indirect 38 | golang.org/x/text v0.8.0 // indirect 39 | google.golang.org/appengine v1.6.7 // indirect 40 | google.golang.org/protobuf v1.30.0 // indirect 41 | gopkg.in/yaml.v2 v2.4.0 // indirect 42 | ) 43 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/alecthomas/assert/v2 v2.2.2 h1:Z/iVC0xZfWTaFNE6bA3z07T86hd45Xe2eLt6WVy2bbk= 2 | github.com/alecthomas/assert/v2 v2.2.2/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= 3 | github.com/alecthomas/kingpin/v2 v2.3.2 h1:H0aULhgmSzN8xQ3nX1uxtdlTHYoPLu5AhHxWrKI6ocU= 4 | github.com/alecthomas/kingpin/v2 v2.3.2/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= 5 | github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= 6 | github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= 7 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc= 8 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= 9 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 10 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 11 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 12 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 13 | github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= 14 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 15 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 16 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 17 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 18 | github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= 19 | github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= 20 | github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA= 21 | github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= 22 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 23 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 24 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 25 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 26 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 27 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 28 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 29 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 30 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 31 | github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= 32 | github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= 33 | github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= 34 | github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= 35 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 36 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 37 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 38 | github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= 39 | github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= 40 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= 41 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 42 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 43 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 44 | github.com/prometheus/client_golang v1.15.0 h1:5fCgGYogn0hFdhyhLbw7hEsWxufKtY9klyvdNfFlFhM= 45 | github.com/prometheus/client_golang v1.15.0/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk= 46 | github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= 47 | github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= 48 | github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= 49 | github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= 50 | github.com/prometheus/exporter-toolkit v0.9.1 h1:cNkC01riqiOS+kh3zdnNwRsbe/Blh0WwK3ij5rPJ9Sw= 51 | github.com/prometheus/exporter-toolkit v0.9.1/go.mod h1:iFlTmFISCix0vyuyBmm0UqOUCTao9+RsAsKJP3YM9ec= 52 | github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= 53 | github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= 54 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 55 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 56 | github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM= 57 | github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= 58 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 59 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 60 | github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= 61 | github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= 62 | github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= 63 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 64 | golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= 65 | golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= 66 | golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM= 67 | golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= 68 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 69 | golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= 70 | golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= 71 | golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw= 72 | golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= 73 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 74 | golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= 75 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 76 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 77 | golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= 78 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 79 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 80 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 81 | golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= 82 | golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 83 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 84 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 85 | google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= 86 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 87 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 88 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 89 | google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= 90 | google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 91 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 92 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 93 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 94 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 95 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 96 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 97 | -------------------------------------------------------------------------------- /pkg/collector/dialer_others.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | /* 5 | Copyright 2023 The Kubernetes Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package collector 21 | 22 | import ( 23 | "net" 24 | "syscall" 25 | ) 26 | 27 | // newDialer returns a dialer optimized for probes to avoid lingering sockets on TIME-WAIT state. 28 | // The dialer reduces the TIME-WAIT period to 1 seconds instead of the OS default of 60 seconds. 29 | // Using 1 second instead of 0 because SO_LINGER socket option to 0 causes pending data to be 30 | // discarded and the connection to be aborted with an RST rather than for the pending data to be 31 | // transmitted and the connection closed cleanly with a FIN. 32 | // Ref: https://issues.k8s.io/89898 33 | func newDialer() *net.Dialer { 34 | dialer := &net.Dialer{ 35 | Control: func(network, address string, c syscall.RawConn) error { 36 | return c.Control(func(fd uintptr) { 37 | syscall.SetsockoptLinger(int(fd), syscall.SOL_SOCKET, syscall.SO_LINGER, &syscall.Linger{Onoff: 1, Linger: 1}) //nolint:errcheck 38 | }) 39 | }, 40 | } 41 | return dialer 42 | } 43 | -------------------------------------------------------------------------------- /pkg/collector/dialer_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | /* 5 | Copyright 2023 The Kubernetes Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package collector 21 | 22 | import ( 23 | "net" 24 | "syscall" 25 | ) 26 | 27 | // newDialer returns a dialer optimized for probes to avoid lingering sockets on TIME-WAIT state. 28 | // The dialer reduces the TIME-WAIT period to 1 seconds instead of the OS default of 60 seconds. 29 | // Using 1 second instead of 0 because SO_LINGER socket option to 0 causes pending data to be 30 | // discarded and the connection to be aborted with an RST rather than for the pending data to be 31 | // transmitted and the connection closed cleanly with a FIN. 32 | // Ref: https://issues.k8s.io/89898 33 | func newDialer() *net.Dialer { 34 | dialer := &net.Dialer{ 35 | Control: func(network, address string, c syscall.RawConn) error { 36 | return c.Control(func(fd uintptr) { 37 | syscall.SetsockoptLinger(syscall.Handle(fd), syscall.SOL_SOCKET, syscall.SO_LINGER, &syscall.Linger{Onoff: 1, Linger: 1}) //nolint:errcheck 38 | }) 39 | }, 40 | } 41 | return dialer 42 | } 43 | -------------------------------------------------------------------------------- /pkg/collector/exporter.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "context" 5 | "strconv" 6 | "time" 7 | 8 | "github.com/go-kit/log" 9 | "github.com/go-kit/log/level" 10 | "github.com/prometheus/client_golang/prometheus" 11 | ) 12 | 13 | const ( 14 | namespace = "uwsgi" 15 | exporter = "exporter" 16 | 17 | usDivider = float64(time.Second / time.Microsecond) 18 | ) 19 | 20 | var subsystemDescriptors = SubsystemDescriptors{ 21 | MainSubsystem: initDescriptors("", map[string]string{ 22 | "listen_queue_length": "Length of listen queue.", 23 | "listen_queue_errors": "Number of listen queue errors.", 24 | "signal_queue_length": "Length of signal queue.", 25 | "workers": "Number of workers.", 26 | }, nil), 27 | 28 | SocketSubsystem: initDescriptors("socket", map[string]string{ 29 | "queue_length": "Length of socket queue.", 30 | "max_queue_length": "Max length of socket queue.", 31 | "shared": "Is shared socket?", 32 | "can_offload": "Can socket offload?", 33 | }, []string{"name", "proto"}), 34 | 35 | WorkerSubsystem: initDescriptors("worker", map[string]string{ 36 | "accepting": "Is this worker accepting requests?", 37 | "delta_requests": "Number of delta requests", 38 | "signal_queue_length": "Length of signal queue.", 39 | "rss_bytes": "Worker RSS bytes.", 40 | "vsz_bytes": "Worker VSZ bytes.", 41 | "running_time_seconds": "Worker running time in seconds.", 42 | "last_spawn_time_seconds": "Last spawn time in seconds since epoch.", 43 | "average_response_time_seconds": "Average response time in seconds.", 44 | "apps": "Number of apps.", 45 | "cores": "Number of cores.", 46 | 47 | "requests_total": "Total number of requests.", 48 | "exceptions_total": "Total number of exceptions.", 49 | "harakiri_count_total": "Total number of harakiri count.", 50 | "signals_total": "Total number of signals.", 51 | "respawn_count_total": "Total number of respawn count.", 52 | "transmitted_bytes_total": "Worker transmitted bytes.", 53 | 54 | // worker statuses (gauges) 55 | "busy": "Is core in busy?", 56 | "idle": "Is core in idle?", 57 | "cheap": "Is core in cheap mode?", 58 | }, []string{"worker_id"}), 59 | 60 | WorkerAppSubsystem: initDescriptors("worker_app", map[string]string{ 61 | "startup_time_seconds": "How long this app took to start.", 62 | 63 | "requests_total": "Total number of requests.", 64 | "exceptions_total": "Total number of exceptions.", 65 | }, []string{"worker_id", "app_id", "mountpoint", "chdir"}), 66 | 67 | WorkerCoreSubsystem: initDescriptors("worker_core", map[string]string{ 68 | "busy": "Is core busy", 69 | 70 | "requests_total": "Total number of requests.", 71 | "static_requests_total": "Total number of static requests.", 72 | "routed_requests_total": "Total number of routed requests.", 73 | "offloaded_requests_total": "Total number of offloaded requests.", 74 | "write_errors_total": "Total number of write errors.", 75 | "read_errors_total": "Total number of read errors.", 76 | }, []string{"worker_id", "core_id"}), 77 | 78 | CacheSubsystem: initDescriptors("cache", map[string]string{ 79 | "hits": "Total number of hits.", 80 | "misses": "Total number of misses.", 81 | "full": "Total Number of times cache full was hit.", 82 | "items": "Items in cache.", 83 | "max_items": "Max items for this cache.", 84 | }, []string{"name"}), 85 | } 86 | 87 | func initDescriptors(subsystem string, nameToHelp map[string]string, labels []string) Descriptors { 88 | descriptors := make(Descriptors, len(nameToHelp)) 89 | for name, help := range nameToHelp { 90 | labels := append([]string{"stats_uri"}, labels...) 91 | descriptors[name] = prometheus.NewDesc( 92 | prometheus.BuildFQName(namespace, subsystem, name), 93 | help, labels, nil) 94 | } 95 | 96 | return descriptors 97 | } 98 | 99 | // Descriptors is a map for `prometheus.Desc` pointer. 100 | type Descriptors map[string]*prometheus.Desc 101 | 102 | type SubsystemDescriptors struct { 103 | MainSubsystem Descriptors 104 | SocketSubsystem Descriptors 105 | WorkerSubsystem Descriptors 106 | WorkerAppSubsystem Descriptors 107 | WorkerCoreSubsystem Descriptors 108 | CacheSubsystem Descriptors 109 | } 110 | 111 | func (v *SubsystemDescriptors) Describe(ch chan<- *prometheus.Desc) { 112 | for _, descs := range []Descriptors{ 113 | v.MainSubsystem, 114 | v.SocketSubsystem, 115 | v.WorkerSubsystem, 116 | v.WorkerAppSubsystem, 117 | v.WorkerCoreSubsystem, 118 | v.CacheSubsystem, 119 | } { 120 | for _, desc := range descs { 121 | ch <- desc 122 | } 123 | } 124 | } 125 | 126 | // Exporter collects uwsgi metrics for prometheus. 127 | type Exporter struct { 128 | ctx context.Context 129 | uri string 130 | statsReader StatsReader 131 | metrics Metrics 132 | ExporterOptions 133 | } 134 | 135 | type ExporterOptions struct { 136 | Logger log.Logger 137 | CollectCores bool 138 | } 139 | 140 | // New creates a new uwsgi collector. 141 | func New(ctx context.Context, uri string, statsReader StatsReader, metrics Metrics, options ExporterOptions) *Exporter { 142 | return &Exporter{ 143 | ctx: ctx, 144 | uri: uri, 145 | statsReader: statsReader, 146 | metrics: metrics, 147 | ExporterOptions: options, 148 | } 149 | } 150 | 151 | // Describe describes all the metrics ever exported by the exporter. 152 | // It implements prometheus.Collector. 153 | func (e *Exporter) Describe(ch chan<- *prometheus.Desc) { 154 | ch <- e.metrics.TotalScrapes.Desc() 155 | ch <- e.metrics.Error.Desc() 156 | ch <- e.metrics.ScrapeDurations.Desc() 157 | ch <- e.metrics.ScrapeErrors.Desc() 158 | ch <- e.metrics.Up.Desc() 159 | 160 | subsystemDescriptors.Describe(ch) 161 | } 162 | 163 | // Collect fetches the stats from configured uwsgi stats location and 164 | // delivers them as Prometheus metrics. It implements prometheus.Collector. 165 | func (e *Exporter) Collect(ch chan<- prometheus.Metric) { 166 | e.scrape(e.ctx, ch) 167 | 168 | ch <- e.metrics.TotalScrapes 169 | ch <- e.metrics.Error 170 | ch <- e.metrics.ScrapeDurations 171 | ch <- e.metrics.ScrapeErrors 172 | ch <- e.metrics.Up 173 | } 174 | 175 | func (e *Exporter) scrape(ctx context.Context, ch chan<- prometheus.Metric) { 176 | e.metrics.TotalScrapes.Inc() 177 | 178 | scrapeTime := time.Now() 179 | 180 | e.metrics.Up.Set(1) 181 | e.metrics.Error.Set(0) 182 | 183 | uwsgiStats, err := e.statsReader.Read(ctx) 184 | if err != nil { 185 | level.Error(e.Logger).Log("msg", "Scrape failed", "error", err) 186 | 187 | e.metrics.ScrapeErrors.Inc() 188 | e.metrics.Up.Set(0) 189 | e.metrics.Error.Set(1) 190 | return 191 | } 192 | 193 | level.Debug(e.Logger).Log("msg", "Scrape successful") 194 | e.metrics.ScrapeDurations.Observe(time.Since(scrapeTime).Seconds()) 195 | 196 | // Collect metrics from stats 197 | e.collectMetrics(uwsgiStats, ch) 198 | } 199 | 200 | func (e *Exporter) mustNewGaugeMetric(desc *prometheus.Desc, value float64, labelValues ...string) prometheus.Metric { 201 | labelValues = append([]string{e.uri}, labelValues...) 202 | return prometheus.MustNewConstMetric(desc, prometheus.GaugeValue, value, labelValues...) 203 | } 204 | 205 | func (e *Exporter) mustNewCounterMetric(desc *prometheus.Desc, value float64, labelValues ...string) prometheus.Metric { 206 | labelValues = append([]string{e.uri}, labelValues...) 207 | return prometheus.MustNewConstMetric(desc, prometheus.CounterValue, value, labelValues...) 208 | } 209 | 210 | var availableWorkerStatuses = []string{"busy", "idle", "cheap"} 211 | 212 | func (e *Exporter) collectMetrics(stats *UwsgiStats, ch chan<- prometheus.Metric) { 213 | availableWorkers := make([]UwsgiWorker, 0, len(stats.Workers)) 214 | // Filter workers (filter out "stand-by" workers, in uwsgi's adaptive process respawn mode) 215 | for _, workerStats := range stats.Workers { 216 | if workerStats.ID == 0 { 217 | continue 218 | } 219 | 220 | availableWorkers = append(availableWorkers, workerStats) 221 | } 222 | 223 | // Main 224 | mainDescs := subsystemDescriptors.MainSubsystem 225 | ch <- e.mustNewGaugeMetric(mainDescs["listen_queue_length"], float64(stats.ListenQueue)) 226 | ch <- e.mustNewGaugeMetric(mainDescs["listen_queue_errors"], float64(stats.ListenQueueErrors)) 227 | ch <- e.mustNewGaugeMetric(mainDescs["signal_queue_length"], float64(stats.SignalQueue)) 228 | ch <- e.mustNewGaugeMetric(mainDescs["workers"], float64(len(availableWorkers))) 229 | 230 | // Sockets 231 | // NOTE(timonwong): Workaround bug #22 232 | type socketStatKey struct { 233 | Name string 234 | Proto string 235 | } 236 | sockets := make(map[socketStatKey]UwsgiSocket, len(stats.Sockets)) 237 | for _, socket := range stats.Sockets { 238 | key := socketStatKey{Name: socket.Name, Proto: socket.Proto} 239 | if _, ok := sockets[key]; !ok { 240 | // First one with the same key take precedence. 241 | sockets[key] = socket 242 | } 243 | } 244 | 245 | socketDescs := subsystemDescriptors.SocketSubsystem 246 | for key, socket := range sockets { 247 | labelValues := []string{key.Name, key.Proto} 248 | 249 | ch <- e.mustNewGaugeMetric(socketDescs["queue_length"], float64(socket.Queue), labelValues...) 250 | ch <- e.mustNewGaugeMetric(socketDescs["max_queue_length"], float64(socket.MaxQueue), labelValues...) 251 | ch <- e.mustNewGaugeMetric(socketDescs["shared"], float64(socket.Shared), labelValues...) 252 | ch <- e.mustNewGaugeMetric(socketDescs["can_offload"], float64(socket.CanOffload), labelValues...) 253 | } 254 | 255 | // Workers 256 | workerDescs := subsystemDescriptors.WorkerSubsystem 257 | workerAppDescs := subsystemDescriptors.WorkerAppSubsystem 258 | workerCoreDescs := subsystemDescriptors.WorkerCoreSubsystem 259 | cacheDescs := subsystemDescriptors.CacheSubsystem 260 | 261 | for _, workerStats := range availableWorkers { 262 | labelValues := []string{strconv.Itoa(workerStats.ID)} 263 | 264 | ch <- e.mustNewGaugeMetric(workerDescs["accepting"], float64(workerStats.Accepting), labelValues...) 265 | ch <- e.mustNewGaugeMetric(workerDescs["delta_requests"], float64(workerStats.DeltaRequests), labelValues...) 266 | ch <- e.mustNewGaugeMetric(workerDescs["signal_queue_length"], float64(workerStats.SignalQueue), labelValues...) 267 | ch <- e.mustNewGaugeMetric(workerDescs["rss_bytes"], float64(workerStats.RSS), labelValues...) 268 | ch <- e.mustNewGaugeMetric(workerDescs["vsz_bytes"], float64(workerStats.VSZ), labelValues...) 269 | ch <- e.mustNewGaugeMetric(workerDescs["running_time_seconds"], float64(workerStats.RunningTime)/usDivider, labelValues...) 270 | ch <- e.mustNewGaugeMetric(workerDescs["last_spawn_time_seconds"], float64(workerStats.LastSpawn), labelValues...) 271 | ch <- e.mustNewGaugeMetric(workerDescs["average_response_time_seconds"], float64(workerStats.AvgRt)/usDivider, labelValues...) 272 | 273 | for _, st := range availableWorkerStatuses { 274 | v := float64(0) 275 | if workerStats.Status == st { 276 | v = 1.0 277 | } 278 | ch <- e.mustNewGaugeMetric(workerDescs[st], v, labelValues...) 279 | } 280 | 281 | ch <- e.mustNewCounterMetric(workerDescs["requests_total"], float64(workerStats.Requests), labelValues...) 282 | ch <- e.mustNewCounterMetric(workerDescs["exceptions_total"], float64(workerStats.Exceptions), labelValues...) 283 | ch <- e.mustNewCounterMetric(workerDescs["harakiri_count_total"], float64(workerStats.HarakiriCount), labelValues...) 284 | ch <- e.mustNewCounterMetric(workerDescs["signals_total"], float64(workerStats.Signals), labelValues...) 285 | ch <- e.mustNewCounterMetric(workerDescs["respawn_count_total"], float64(workerStats.RespawnCount), labelValues...) 286 | ch <- e.mustNewCounterMetric(workerDescs["transmitted_bytes_total"], float64(workerStats.TX), labelValues...) 287 | 288 | // Worker Apps 289 | ch <- e.mustNewGaugeMetric(workerDescs["apps"], float64(len(workerStats.Apps)), labelValues...) 290 | for _, appStats := range workerStats.Apps { 291 | labelValues := []string{strconv.Itoa(workerStats.ID), strconv.Itoa(appStats.ID), appStats.MountPoint, appStats.Chdir} 292 | ch <- e.mustNewGaugeMetric(workerAppDescs["startup_time_seconds"], float64(appStats.StartupTime), labelValues...) 293 | 294 | ch <- e.mustNewCounterMetric(workerAppDescs["requests_total"], float64(appStats.Requests), labelValues...) 295 | ch <- e.mustNewCounterMetric(workerAppDescs["exceptions_total"], float64(appStats.Exceptions), labelValues...) 296 | } 297 | 298 | // Worker Cores 299 | ch <- e.mustNewGaugeMetric(workerDescs["cores"], float64(len(workerStats.Cores)), labelValues...) 300 | if e.CollectCores { 301 | for _, coreStats := range workerStats.Cores { 302 | labelValues := []string{strconv.Itoa(workerStats.ID), strconv.Itoa(coreStats.ID)} 303 | ch <- e.mustNewGaugeMetric(workerCoreDescs["busy"], float64(coreStats.InRequest), labelValues...) 304 | 305 | ch <- e.mustNewCounterMetric(workerCoreDescs["requests_total"], float64(coreStats.Requests), labelValues...) 306 | ch <- e.mustNewCounterMetric(workerCoreDescs["static_requests_total"], float64(coreStats.StaticRequests), labelValues...) 307 | ch <- e.mustNewCounterMetric(workerCoreDescs["routed_requests_total"], float64(coreStats.RoutedRequests), labelValues...) 308 | ch <- e.mustNewCounterMetric(workerCoreDescs["offloaded_requests_total"], float64(coreStats.OffloadedRequests), labelValues...) 309 | ch <- e.mustNewCounterMetric(workerCoreDescs["write_errors_total"], float64(coreStats.WriteErrors), labelValues...) 310 | ch <- e.mustNewCounterMetric(workerCoreDescs["read_errors_total"], float64(coreStats.ReadErrors), labelValues...) 311 | } 312 | } 313 | } 314 | 315 | for _, cacheStats := range stats.Caches { 316 | labelValues := []string{cacheStats.Name} 317 | ch <- e.mustNewCounterMetric(cacheDescs["hits"], float64(cacheStats.Hits), labelValues...) 318 | ch <- e.mustNewCounterMetric(cacheDescs["misses"], float64(cacheStats.Misses), labelValues...) 319 | ch <- e.mustNewCounterMetric(cacheDescs["full"], float64(cacheStats.Full), labelValues...) 320 | 321 | ch <- e.mustNewGaugeMetric(cacheDescs["items"], float64(cacheStats.Items), labelValues...) 322 | ch <- e.mustNewGaugeMetric(cacheDescs["max_items"], float64(cacheStats.MaxItems), labelValues...) 323 | } 324 | } 325 | 326 | // Metrics represents exporter metrics which values can be carried between http requests. 327 | type Metrics struct { 328 | TotalScrapes prometheus.Counter 329 | ScrapeErrors prometheus.Counter 330 | ScrapeDurations prometheus.Summary 331 | Error prometheus.Gauge 332 | Up prometheus.Gauge 333 | } 334 | 335 | // NewMetrics creates new Metrics instance. 336 | func NewMetrics() Metrics { 337 | subsystem := exporter 338 | return Metrics{ 339 | TotalScrapes: prometheus.NewCounter(prometheus.CounterOpts{ 340 | Namespace: namespace, 341 | Subsystem: subsystem, 342 | Name: "scrapes_total", 343 | Help: "Total number of times uWSGI was scraped for metrics.", 344 | }), 345 | ScrapeErrors: prometheus.NewCounter(prometheus.CounterOpts{ 346 | Namespace: namespace, 347 | Subsystem: subsystem, 348 | Name: "scrape_errors_total", 349 | Help: "Total number of times an error occurred scraping a uWSGI.", 350 | }), 351 | ScrapeDurations: prometheus.NewSummary(prometheus.SummaryOpts{ 352 | Namespace: namespace, 353 | Subsystem: subsystem, 354 | Name: "scrape_duration_seconds", 355 | Help: "Duration of uWSGI scrape job.", 356 | }), 357 | Error: prometheus.NewGauge(prometheus.GaugeOpts{ 358 | Namespace: namespace, 359 | Subsystem: subsystem, 360 | Name: "last_scrape_error", 361 | Help: "Whether the last scrape of metrics from uWSGI resulted in an error (1 for error, 0 for success).", 362 | }), 363 | Up: prometheus.NewGauge(prometheus.GaugeOpts{ 364 | Namespace: namespace, 365 | Name: "up", 366 | Help: "Whether the uWSGI server is up.", 367 | }), 368 | } 369 | } 370 | -------------------------------------------------------------------------------- /pkg/collector/exporter_test.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "context" 5 | _ "embed" 6 | "net/http" 7 | "net/http/httptest" 8 | "os" 9 | "path/filepath" 10 | "testing" 11 | 12 | "github.com/alecthomas/assert/v2" 13 | "github.com/go-kit/log" 14 | "github.com/prometheus/client_golang/prometheus" 15 | dto "github.com/prometheus/client_model/go" 16 | "github.com/samber/lo" 17 | ) 18 | 19 | var ( 20 | sampleUwsgiStatsFileName string 21 | //go:embed testdata/sample.json 22 | sampleUwsgiStatsJSON []byte 23 | //go:embed testdata/wrong.json 24 | wrongUwsgiStatsJSON []byte 25 | ) 26 | 27 | func init() { 28 | sampleUwsgiStatsFileName = lo.Must(filepath.Abs("testdata/sample.json")) 29 | } 30 | 31 | func newUwsgiStatsServer(response []byte) *httptest.Server { 32 | handlerFunc := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 33 | header := w.Header() 34 | header.Set("Content-Type", "application/json") 35 | w.Write(response) 36 | }) 37 | s := httptest.NewServer(handlerFunc) 38 | return s 39 | } 40 | 41 | type labelMap map[string]string 42 | 43 | type MetricResult struct { 44 | labels labelMap 45 | value float64 46 | metricType dto.MetricType 47 | } 48 | 49 | func readMetric(m prometheus.Metric) MetricResult { 50 | pb := &dto.Metric{} 51 | m.Write(pb) 52 | labels := make(labelMap, len(pb.Label)) 53 | for _, v := range pb.Label { 54 | labels[v.GetName()] = v.GetValue() 55 | } 56 | if pb.Gauge != nil { 57 | return MetricResult{labels: labels, value: pb.GetGauge().GetValue(), metricType: dto.MetricType_GAUGE} 58 | } 59 | if pb.Counter != nil { 60 | return MetricResult{labels: labels, value: pb.GetCounter().GetValue(), metricType: dto.MetricType_COUNTER} 61 | } 62 | if pb.Summary != nil { 63 | return MetricResult{labels: labels, value: 0, metricType: dto.MetricType_SUMMARY} 64 | } 65 | 66 | panic("Unsupported metric type") 67 | } 68 | 69 | func TestUwsgiExporter_CollectWrongJSON(t *testing.T) { 70 | s := newUwsgiStatsServer(wrongUwsgiStatsJSON) 71 | logger := log.NewLogfmtLogger(os.Stderr) 72 | ctx, cancel := context.WithTimeout(context.Background(), someTimeout) 73 | defer cancel() 74 | 75 | statsReader, err := NewStatsReader(s.URL) 76 | assert.NoError(t, err) 77 | 78 | exporter := New(ctx, s.URL, statsReader, NewMetrics(), ExporterOptions{ 79 | Logger: logger, 80 | CollectCores: false, 81 | }) 82 | 83 | ch := make(chan prometheus.Metric) 84 | 85 | go func() { 86 | defer close(ch) 87 | defer s.Close() 88 | exporter.Collect(ch) 89 | }() 90 | 91 | // total_scrapes 92 | expected := MetricResult{labels: labelMap{}, value: 1, metricType: dto.MetricType_COUNTER} 93 | got := readMetric(<-ch) 94 | assert.Equal(t, expected, got) 95 | 96 | // error 97 | expected = MetricResult{labels: labelMap{}, value: 1, metricType: dto.MetricType_GAUGE} 98 | got = readMetric(<-ch) 99 | assert.Equal(t, expected, got) 100 | 101 | // scrape duration 102 | expected = MetricResult{labels: labelMap{}, value: 0, metricType: dto.MetricType_SUMMARY} 103 | got = readMetric(<-ch) 104 | assert.Equal(t, expected, got) 105 | 106 | // scrape_errors 107 | expected = MetricResult{labels: labelMap{}, value: 1, metricType: dto.MetricType_COUNTER} 108 | got = readMetric(<-ch) 109 | assert.Equal(t, expected, got) 110 | 111 | // uwsgi_up 112 | expected = MetricResult{labels: labelMap{}, value: 0, metricType: dto.MetricType_GAUGE} 113 | got = readMetric(<-ch) 114 | assert.Equal(t, expected, got) 115 | } 116 | 117 | func TestUwsgiExporter_Collect(t *testing.T) { 118 | s := newUwsgiStatsServer(sampleUwsgiStatsJSON) 119 | logger := log.NewLogfmtLogger(os.Stderr) 120 | ctx, cancel := context.WithTimeout(context.Background(), someTimeout) 121 | defer cancel() 122 | 123 | statsReader, err := NewStatsReader(s.URL) 124 | assert.NoError(t, err) 125 | 126 | exporter := New(ctx, s.URL, statsReader, NewMetrics(), ExporterOptions{ 127 | Logger: logger, 128 | CollectCores: true, 129 | }) 130 | 131 | ch := make(chan prometheus.Metric) 132 | 133 | go func() { 134 | defer close(ch) 135 | defer s.Close() 136 | exporter.Collect(ch) 137 | }() 138 | 139 | // main 140 | labels := labelMap{"stats_uri": s.URL} 141 | mainMetricResults := []MetricResult{ 142 | // listen_queue_length 143 | {labels: labels, value: 0, metricType: dto.MetricType_GAUGE}, 144 | // listen_queue_errors 145 | {labels: labels, value: 0, metricType: dto.MetricType_GAUGE}, 146 | // signal_queue_length 147 | {labels: labels, value: 0, metricType: dto.MetricType_GAUGE}, 148 | // workers 149 | {labels: labels, value: 2, metricType: dto.MetricType_GAUGE}, 150 | } 151 | for id, expect := range mainMetricResults { 152 | got := readMetric(<-ch) 153 | assert.Equal(t, expect, got, "Wrong main stats at id: %d", id) 154 | } 155 | 156 | // sockets 157 | labels = labelMap{"name": "127.0.0.1:36577", "proto": "uwsgi", "stats_uri": s.URL} 158 | socketMetricResults := []MetricResult{ 159 | {labels: labels, value: 0, metricType: dto.MetricType_GAUGE}, 160 | {labels: labels, value: 100, metricType: dto.MetricType_GAUGE}, 161 | {labels: labels, value: 0, metricType: dto.MetricType_GAUGE}, 162 | {labels: labels, value: 0, metricType: dto.MetricType_GAUGE}, 163 | } 164 | for _, expect := range socketMetricResults { 165 | got := readMetric(<-ch) 166 | assert.Equal(t, expect, got, "Wrong socket stats") 167 | } 168 | 169 | // worker 170 | workerLabels := labelMap{"stats_uri": s.URL, "worker_id": "1"} 171 | workerMetricResults := []MetricResult{ 172 | {labels: workerLabels, value: 1, metricType: dto.MetricType_GAUGE}, 173 | {labels: workerLabels, value: 0, metricType: dto.MetricType_GAUGE}, 174 | {labels: workerLabels, value: 0, metricType: dto.MetricType_GAUGE}, 175 | {labels: workerLabels, value: 0, metricType: dto.MetricType_GAUGE}, 176 | {labels: workerLabels, value: 0, metricType: dto.MetricType_GAUGE}, 177 | {labels: workerLabels, value: 0, metricType: dto.MetricType_GAUGE}, 178 | {labels: workerLabels, value: 1457410597, metricType: dto.MetricType_GAUGE}, // last_spawn 179 | {labels: workerLabels, value: 0, metricType: dto.MetricType_GAUGE}, 180 | {labels: workerLabels, value: 0, metricType: dto.MetricType_GAUGE}, // busy 181 | {labels: workerLabels, value: 1, metricType: dto.MetricType_GAUGE}, // idle 182 | {labels: workerLabels, value: 0, metricType: dto.MetricType_GAUGE}, // cheap 183 | {labels: workerLabels, value: 0, metricType: dto.MetricType_COUNTER}, 184 | {labels: workerLabels, value: 0, metricType: dto.MetricType_COUNTER}, 185 | {labels: workerLabels, value: 0, metricType: dto.MetricType_COUNTER}, 186 | {labels: workerLabels, value: 0, metricType: dto.MetricType_COUNTER}, 187 | {labels: workerLabels, value: 1, metricType: dto.MetricType_COUNTER}, 188 | {labels: workerLabels, value: 0, metricType: dto.MetricType_COUNTER}, 189 | } 190 | for idx, expect := range workerMetricResults { 191 | got := readMetric(<-ch) 192 | assert.Equal(t, expect, got, "Wrong worker stats at idx %d", idx) 193 | } 194 | 195 | // worker apps 196 | labels = labelMap{"stats_uri": s.URL, "worker_id": "1", "chdir": "", "mountpoint": "", "app_id": "0"} 197 | workerAppMetricResults := []MetricResult{ 198 | {labels: workerLabels, value: 1, metricType: dto.MetricType_GAUGE}, // app count 199 | 200 | {labels: labels, value: 0, metricType: dto.MetricType_GAUGE}, 201 | 202 | {labels: labels, value: 0, metricType: dto.MetricType_COUNTER}, 203 | {labels: labels, value: 0, metricType: dto.MetricType_COUNTER}, 204 | } 205 | for _, expect := range workerAppMetricResults { 206 | got := readMetric(<-ch) 207 | assert.Equal(t, expect, got, "Wrong worker app stats") 208 | } 209 | 210 | // worker cores 211 | labels = labelMap{"stats_uri": s.URL, "worker_id": "1", "core_id": "0"} 212 | workerCoreMetricResults := []MetricResult{ 213 | {labels: workerLabels, value: 1, metricType: dto.MetricType_GAUGE}, // core count 214 | 215 | {labels: labels, value: 0, metricType: dto.MetricType_GAUGE}, // in_requests 216 | 217 | {labels: labels, value: 0, metricType: dto.MetricType_COUNTER}, // requests_total 218 | {labels: labels, value: 0, metricType: dto.MetricType_COUNTER}, 219 | {labels: labels, value: 0, metricType: dto.MetricType_COUNTER}, 220 | {labels: labels, value: 0, metricType: dto.MetricType_COUNTER}, 221 | {labels: labels, value: 0, metricType: dto.MetricType_COUNTER}, 222 | {labels: labels, value: 0, metricType: dto.MetricType_COUNTER}, 223 | } 224 | for _, expect := range workerCoreMetricResults { 225 | got := readMetric(<-ch) 226 | assert.Equal(t, expect, got, "Wrong worker core stats: %d") 227 | } 228 | 229 | // Drain 230 | for i := 0; i < len(workerMetricResults)+len(workerAppMetricResults)+len(workerCoreMetricResults); i++ { 231 | readMetric(<-ch) 232 | } 233 | 234 | // caches 235 | labels = labelMap{"stats_uri": s.URL, "name": "cache_1"} 236 | cacheMetricResults := []MetricResult{ 237 | {labels: labels, value: 56614, metricType: dto.MetricType_COUNTER}, // hits 238 | {labels: labels, value: 4931570, metricType: dto.MetricType_COUNTER}, // misses 239 | {labels: labels, value: 0, metricType: dto.MetricType_COUNTER}, // full 240 | 241 | {labels: labels, value: 39, metricType: dto.MetricType_GAUGE}, // items 242 | {labels: labels, value: 2000, metricType: dto.MetricType_GAUGE}, // max_items 243 | } 244 | for _, expect := range cacheMetricResults { 245 | got := readMetric(<-ch) 246 | assert.Equal(t, expect, got, "Wrong cache stats: %d") 247 | } 248 | 249 | // Drain 250 | for i := 0; i < len(cacheMetricResults); i++ { 251 | readMetric(<-ch) 252 | } 253 | 254 | // total_scrapes 255 | expected := MetricResult{labels: labelMap{}, value: 1, metricType: dto.MetricType_COUNTER} 256 | got := readMetric(<-ch) 257 | assert.Equal(t, expected, got) 258 | 259 | // error 260 | expected = MetricResult{labels: labelMap{}, value: 0, metricType: dto.MetricType_GAUGE} 261 | got = readMetric(<-ch) 262 | assert.Equal(t, expected, got) 263 | 264 | // scrape duration 265 | expected = MetricResult{labels: labelMap{}, value: 0, metricType: dto.MetricType_SUMMARY} 266 | got = readMetric(<-ch) 267 | assert.Equal(t, expected, got) 268 | 269 | // scrape_errors 270 | expected = MetricResult{labels: labelMap{}, value: 0, metricType: dto.MetricType_COUNTER} 271 | got = readMetric(<-ch) 272 | assert.Equal(t, expected, got) 273 | 274 | // uwsgi_up 275 | expected = MetricResult{labels: labelMap{}, value: 1, metricType: dto.MetricType_GAUGE} 276 | got = readMetric(<-ch) 277 | assert.Equal(t, expected, got) 278 | } 279 | -------------------------------------------------------------------------------- /pkg/collector/mockserver_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package collector 6 | 7 | import ( 8 | "fmt" 9 | "net" 10 | "os" 11 | "sync" 12 | "testing" 13 | "time" 14 | ) 15 | 16 | // someTimeout is used just to test that net.Conn implementations 17 | // don't explode when their SetFooDeadline methods are called. 18 | // It isn't actually used for testing timeouts. 19 | const someTimeout = 10 * time.Second 20 | 21 | // testUnixAddr uses os.CreateTemp to get a name that is unique. 22 | // It also uses /tmp directory in case it is prohibited to create UNIX 23 | // sockets in TMPDIR. 24 | func testUnixAddr() string { 25 | f, err := os.CreateTemp("", "uwsgi-exporter-test") 26 | if err != nil { 27 | panic(err) 28 | } 29 | addr := f.Name() 30 | f.Close() 31 | os.Remove(addr) 32 | return addr 33 | } 34 | 35 | func newLocalListener(network string) (net.Listener, error) { 36 | switch network { 37 | case "tcp": 38 | return net.Listen("tcp4", "127.0.0.1:0") 39 | case "unix": 40 | return net.Listen(network, testUnixAddr()) 41 | default: 42 | return nil, fmt.Errorf("%s is not supported", network) 43 | } 44 | } 45 | 46 | type localServer struct { 47 | mu sync.RWMutex 48 | net.Listener 49 | 50 | done chan struct{} // signal that indicates server stopped 51 | } 52 | 53 | func (ls *localServer) buildup(handler func(*localServer, net.Listener)) { 54 | go func() { 55 | handler(ls, ls.Listener) 56 | close(ls.done) 57 | }() 58 | } 59 | 60 | func (ls *localServer) teardown() error { 61 | ls.mu.Lock() 62 | defer ls.mu.Unlock() 63 | 64 | if ls.Listener != nil { 65 | network := ls.Listener.Addr().Network() 66 | address := ls.Listener.Addr().String() 67 | ls.Listener.Close() 68 | <-ls.done 69 | ls.Listener = nil 70 | if network == "unix" { 71 | os.Remove(address) 72 | } 73 | } 74 | return nil 75 | } 76 | 77 | func newLocalServer(t *testing.T, network string) (*localServer, error) { 78 | ln, err := newLocalListener(network) 79 | if err != nil { 80 | return nil, err 81 | } 82 | 83 | s := &localServer{Listener: ln, done: make(chan struct{})} 84 | t.Cleanup(func() { 85 | s.teardown() 86 | }) 87 | return s, nil 88 | } 89 | 90 | func justWriteHandler(content []byte, ch chan<- error) func(*localServer, net.Listener) { 91 | return func(ls *localServer, ln net.Listener) { 92 | defer close(ch) 93 | 94 | switch ln := ln.(type) { 95 | case *net.UnixListener: 96 | ln.SetDeadline(time.Now().Add(someTimeout)) 97 | case *net.TCPListener: 98 | ln.SetDeadline(time.Now().Add(someTimeout)) 99 | } 100 | c, err := ln.Accept() 101 | if err != nil { 102 | ch <- err 103 | return 104 | } 105 | defer c.Close() 106 | 107 | network := ln.Addr().Network() 108 | if c.LocalAddr().Network() != network || c.RemoteAddr().Network() != network { 109 | ch <- fmt.Errorf("got %v->%v; expected %v->%v", c.LocalAddr().Network(), c.RemoteAddr().Network(), network, network) 110 | return 111 | } 112 | 113 | c.SetDeadline(time.Now().Add(someTimeout)) 114 | 115 | if _, err := c.Write(content); err != nil { 116 | ch <- err 117 | return 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /pkg/collector/reader.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "net/url" 8 | "strings" 9 | ) 10 | 11 | // StatsReader reads uwsgi stats from specified uri. 12 | type StatsReader interface { 13 | Read(ctx context.Context) (*UwsgiStats, error) 14 | } 15 | 16 | // StatsReaderFunc is prototype for new stats reader 17 | type StatsReaderFunc func(u *url.URL) StatsReader 18 | 19 | var statsReaderFuncRegistry = make(map[string]StatsReaderFunc) 20 | 21 | func registerStatsReaderFunc(scheme string, creator StatsReaderFunc) { 22 | statsReaderFuncRegistry[scheme] = creator 23 | } 24 | 25 | type statsReaderOptions struct { 26 | requireSafeScheme bool 27 | } 28 | 29 | type StatsReaderOption func(*statsReaderOptions) 30 | 31 | func WithRequireSafeScheme(safe bool) StatsReaderOption { 32 | return func(opts *statsReaderOptions) { 33 | opts.requireSafeScheme = safe 34 | } 35 | } 36 | 37 | // NewStatsReader creates a StatsReader according to uri. 38 | func NewStatsReader(uri string, opts ...StatsReaderOption) (StatsReader, error) { 39 | options := &statsReaderOptions{} 40 | for _, opt := range opts { 41 | opt(options) 42 | } 43 | 44 | u, err := url.Parse(uri) 45 | if err != nil { 46 | return nil, fmt.Errorf("failed to parse uri: %w", err) 47 | } 48 | 49 | if !strings.Contains(uri, "://") && u.Host == "" { 50 | // Assume it's a http uri and parse again 51 | u, err = url.Parse("http://" + uri) 52 | if err != nil { 53 | return nil, fmt.Errorf("failed to parse uri (again): %w", err) 54 | } 55 | } 56 | 57 | // If in safe mode, we only accept tcp, http, and https 58 | if options.requireSafeScheme { 59 | switch u.Scheme { 60 | case "tcp", "http", "https": 61 | default: 62 | return nil, fmt.Errorf("unsafe scheme: %s", u.Scheme) 63 | } 64 | } 65 | 66 | fn := statsReaderFuncRegistry[u.Scheme] 67 | if fn == nil { 68 | return nil, fmt.Errorf("incompatible scheme: %s", u.Scheme) 69 | } 70 | 71 | return fn(u), nil 72 | } 73 | 74 | func setDeadLine(ctx context.Context, conn net.Conn) error { 75 | deadline, ok := ctx.Deadline() 76 | if !ok { 77 | return nil 78 | } 79 | 80 | return conn.SetDeadline(deadline) 81 | } 82 | -------------------------------------------------------------------------------- /pkg/collector/reader_file.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/url" 7 | "os" 8 | ) 9 | 10 | type fileStatsReader struct { 11 | filename string 12 | } 13 | 14 | func init() { 15 | registerStatsReaderFunc("file", newFileStatsReader) 16 | } 17 | 18 | func newFileStatsReader(u *url.URL) StatsReader { 19 | return &fileStatsReader{ 20 | filename: u.Path, 21 | } 22 | } 23 | 24 | func (r *fileStatsReader) Read(_ context.Context) (*UwsgiStats, error) { 25 | f, err := os.Open(r.filename) 26 | if err != nil { 27 | return nil, fmt.Errorf("unable to open file: %w", err) 28 | } 29 | defer f.Close() 30 | 31 | uwsgiStats, err := parseUwsgiStatsFromIO(f) 32 | if err != nil { 33 | return nil, fmt.Errorf("unable to unmarshal JSON: %w", err) 34 | } 35 | 36 | return uwsgiStats, nil 37 | } 38 | -------------------------------------------------------------------------------- /pkg/collector/reader_file_test.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/alecthomas/assert/v2" 9 | ) 10 | 11 | func TestFileStatsReader_Read(t *testing.T) { 12 | t.Parallel() 13 | 14 | uri := "file://" + sampleUwsgiStatsFileName 15 | 16 | reader, err := NewStatsReader(uri) 17 | assert.NoError(t, err) 18 | 19 | assert.Equal(t, reflect.TypeOf(&fileStatsReader{}).String(), reflect.TypeOf(reader).String()) 20 | 21 | ctx, cancel := context.WithTimeout(context.Background(), someTimeout) 22 | defer cancel() 23 | 24 | uwsgiStats, err := reader.Read(ctx) 25 | assert.NoError(t, err) 26 | 27 | assert.Equal(t, uwsgiStats.Version, "2.0.12") 28 | } 29 | -------------------------------------------------------------------------------- /pkg/collector/reader_http.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | ) 9 | 10 | type httpStatsReader struct { 11 | uri string 12 | 13 | client *http.Client 14 | } 15 | 16 | func init() { 17 | registerStatsReaderFunc("http", newHTTPStatsReader) 18 | registerStatsReaderFunc("https", newHTTPStatsReader) 19 | } 20 | 21 | func newHTTPStatsReader(u *url.URL) StatsReader { 22 | return &httpStatsReader{ 23 | uri: u.String(), 24 | client: &http.Client{}, 25 | } 26 | } 27 | 28 | func (r *httpStatsReader) Read(ctx context.Context) (*UwsgiStats, error) { 29 | req, err := http.NewRequestWithContext(ctx, http.MethodGet, r.uri, nil) 30 | if err != nil { 31 | return nil, fmt.Errorf("error creating request: %w", err) 32 | } 33 | 34 | resp, err := r.client.Do(req) 35 | if err != nil { 36 | return nil, fmt.Errorf("error querying uwsgi stats: %w", err) 37 | } 38 | defer resp.Body.Close() 39 | 40 | uwsgiStats, err := parseUwsgiStatsFromIO(resp.Body) 41 | if err != nil { 42 | return nil, fmt.Errorf("failed to parse uwsgi stats: %w", err) 43 | } 44 | return uwsgiStats, nil 45 | } 46 | -------------------------------------------------------------------------------- /pkg/collector/reader_http_test.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/alecthomas/assert/v2" 9 | ) 10 | 11 | func TestHTTPStatsReader_Read(t *testing.T) { 12 | t.Parallel() 13 | 14 | s := newUwsgiStatsServer(sampleUwsgiStatsJSON) 15 | defer s.Close() 16 | 17 | uri := s.URL 18 | reader, err := NewStatsReader(uri) 19 | assert.NoError(t, err) 20 | 21 | assert.Equal(t, reflect.TypeOf(&httpStatsReader{}).String(), reflect.TypeOf(reader).String()) 22 | 23 | ctx, cancel := context.WithTimeout(context.Background(), someTimeout) 24 | defer cancel() 25 | 26 | uwsgiStats, err := reader.Read(ctx) 27 | assert.NoError(t, err) 28 | 29 | assert.Equal(t, "2.0.12", uwsgiStats.Version) 30 | } 31 | -------------------------------------------------------------------------------- /pkg/collector/reader_tcp.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/url" 7 | ) 8 | 9 | type tcpStatsReader struct { 10 | host string 11 | } 12 | 13 | func init() { 14 | registerStatsReaderFunc("tcp", newTCPStatsReader) 15 | } 16 | 17 | func newTCPStatsReader(u *url.URL) StatsReader { 18 | return &tcpStatsReader{ 19 | host: u.Host, 20 | } 21 | } 22 | 23 | func (r *tcpStatsReader) Read(ctx context.Context) (*UwsgiStats, error) { 24 | d := newDialer() 25 | conn, err := d.DialContext(ctx, "tcp", r.host) 26 | if err != nil { 27 | return nil, fmt.Errorf("error reading stats from tcp: %w", err) 28 | } 29 | defer conn.Close() 30 | 31 | err = setDeadLine(ctx, conn) 32 | if err != nil { 33 | return nil, fmt.Errorf("failed to set deadline: %w", err) 34 | } 35 | 36 | uwsgiStats, err := parseUwsgiStatsFromIO(conn) 37 | if err != nil { 38 | return nil, fmt.Errorf("failed to unmarshal JSON: %w", err) 39 | } 40 | return uwsgiStats, nil 41 | } 42 | -------------------------------------------------------------------------------- /pkg/collector/reader_tcp_test.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/alecthomas/assert/v2" 9 | ) 10 | 11 | func TestTCPStatsReader_Read(t *testing.T) { 12 | t.Parallel() 13 | 14 | // Setup a local TCP server for testing 15 | ls, err := newLocalServer(t, "tcp") 16 | assert.NoError(t, err) 17 | 18 | ch := make(chan error, 1) 19 | 20 | ls.buildup(justWriteHandler(sampleUwsgiStatsJSON, ch)) 21 | 22 | uri := "tcp://" + ls.Listener.Addr().String() 23 | reader, err := NewStatsReader(uri) 24 | assert.NoError(t, err) 25 | 26 | assert.Equal(t, reflect.TypeOf(&tcpStatsReader{}).String(), reflect.TypeOf(reader).String()) 27 | 28 | ctx, cancel := context.WithTimeout(context.Background(), someTimeout) 29 | defer cancel() 30 | 31 | uwsgiStats, err := reader.Read(ctx) 32 | assert.NoError(t, err) 33 | 34 | assert.Equal(t, "2.0.12", uwsgiStats.Version) 35 | 36 | for err := range ch { 37 | assert.NoError(t, err) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /pkg/collector/reader_test.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/alecthomas/assert/v2" 7 | ) 8 | 9 | func TestNewStatsReaderNil(t *testing.T) { 10 | t.Parallel() 11 | 12 | unknownUris := []string{ 13 | "abc://xxx", 14 | "def://yyy", 15 | "socks://vvvv", 16 | } 17 | 18 | for _, uri := range unknownUris { 19 | reader, err := NewStatsReader(uri) 20 | assert.Equal(t, nil, reader) 21 | assert.Error(t, err) 22 | } 23 | } 24 | 25 | func TestNewStatsReader_Safe(t *testing.T) { 26 | testCases := []struct { 27 | name string 28 | uri string 29 | wantErr bool 30 | }{ 31 | { 32 | name: "tcp", 33 | uri: "tcp://xxx:123", 34 | }, 35 | { 36 | name: "http", 37 | uri: "http://10.1.1.2:1234", 38 | }, 39 | { 40 | name: "https", 41 | uri: "https://example.com:8443", 42 | }, 43 | { 44 | name: "file", 45 | uri: "file:///tmp/uwsgi.json", 46 | wantErr: true, 47 | }, 48 | { 49 | name: "unix", 50 | uri: "unix:///tmp/uwsgi.sock", 51 | wantErr: true, 52 | }, 53 | } 54 | 55 | for _, tc := range testCases { 56 | tc := tc 57 | t.Run(tc.name, func(t *testing.T) { 58 | t.Parallel() 59 | _, err := NewStatsReader(tc.uri, WithRequireSafeScheme(true)) 60 | if tc.wantErr { 61 | assert.Error(t, err) 62 | } else { 63 | assert.NoError(t, err) 64 | } 65 | }) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /pkg/collector/reader_unix.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/url" 7 | ) 8 | 9 | type unixStatsReader struct { 10 | filename string 11 | } 12 | 13 | func init() { 14 | registerStatsReaderFunc("unix", newUnixStatsReader) 15 | } 16 | 17 | func newUnixStatsReader(u *url.URL) StatsReader { 18 | return &unixStatsReader{ 19 | filename: u.Path, 20 | } 21 | } 22 | 23 | func (r *unixStatsReader) Read(ctx context.Context) (*UwsgiStats, error) { 24 | d := newDialer() 25 | conn, err := d.DialContext(ctx, "unix", r.filename) 26 | if err != nil { 27 | return nil, fmt.Errorf("error reading stats from unix socket %s: %w", r.filename, err) 28 | } 29 | defer conn.Close() 30 | 31 | err = setDeadLine(ctx, conn) 32 | if err != nil { 33 | return nil, fmt.Errorf("failed to set deadline: %w", err) 34 | } 35 | 36 | uwsgiStats, err := parseUwsgiStatsFromIO(conn) 37 | if err != nil { 38 | return nil, fmt.Errorf("failed to unmarshal JSON: %w", err) 39 | } 40 | return uwsgiStats, nil 41 | } 42 | -------------------------------------------------------------------------------- /pkg/collector/reader_unix_test.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/alecthomas/assert/v2" 9 | ) 10 | 11 | func TestUnixStatsReader_Read(t *testing.T) { 12 | t.Parallel() 13 | 14 | // Setup a local UDS server for testing 15 | ls, err := newLocalServer(t, "unix") 16 | assert.NoError(t, err) 17 | 18 | ch := make(chan error, 1) 19 | 20 | ls.buildup(justWriteHandler(sampleUwsgiStatsJSON, ch)) 21 | 22 | uri := "unix://" + ls.Listener.Addr().String() 23 | reader, err := NewStatsReader(uri) 24 | assert.NoError(t, err) 25 | 26 | assert.Equal(t, reflect.TypeOf(&unixStatsReader{}).String(), reflect.TypeOf(reader).String()) 27 | 28 | ctx, cancel := context.WithTimeout(context.Background(), someTimeout) 29 | defer cancel() 30 | 31 | uwsgiStats, err := reader.Read(ctx) 32 | assert.NoError(t, err) 33 | 34 | assert.Equal(t, "2.0.12", uwsgiStats.Version) 35 | 36 | for err := range ch { 37 | assert.NoError(t, err) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /pkg/collector/stats.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | ) 7 | 8 | type UwsgiSocket struct { 9 | Name string `json:"name"` 10 | Proto string `json:"proto"` 11 | Queue int `json:"queue"` 12 | MaxQueue int `json:"max_queue"` 13 | Shared int `json:"shared"` 14 | CanOffload int `json:"can_offload"` 15 | } 16 | 17 | type UwsgiWorker struct { 18 | ID int `json:"id"` 19 | PID int `json:"pid"` 20 | Accepting int `json:"accepting"` 21 | Requests int `json:"requests"` 22 | DeltaRequests int `json:"delta_requests"` 23 | Exceptions int `json:"exceptions"` 24 | HarakiriCount int `json:"harakiri_count"` 25 | Signals int `json:"signals"` 26 | SignalQueue int `json:"signal_queue"` 27 | Status string `json:"status"` 28 | RSS int `json:"rss"` 29 | VSZ int `json:"vsz"` 30 | RunningTime int64 `json:"running_time"` 31 | LastSpawn int64 `json:"last_spawn"` 32 | RespawnCount int `json:"respawn_count"` 33 | TX int `json:"tx"` 34 | AvgRt int `json:"avg_rt"` 35 | Apps []UwsgiApp `json:"apps"` 36 | Cores []UwsgiCore `json:"cores"` 37 | } 38 | 39 | type UwsgiApp struct { 40 | ID int `json:"id"` 41 | Modifier1 int `json:"modifier1"` 42 | MountPoint string `json:"mountpoint"` 43 | StartupTime int `json:"startup_time"` 44 | Requests int `json:"requests"` 45 | Exceptions int `json:"exceptions"` 46 | Chdir string `json:"chdir"` 47 | } 48 | 49 | type UwsgiCore struct { 50 | ID int `json:"id"` 51 | Requests int `json:"requests"` 52 | StaticRequests int `json:"static_requests"` 53 | RoutedRequests int `json:"routed_requests"` 54 | OffloadedRequests int `json:"offloaded_requests"` 55 | WriteErrors int `json:"write_errors"` 56 | ReadErrors int `json:"read_errors"` 57 | InRequest int `json:"in_request"` 58 | Vars []string `json:"vars"` 59 | } 60 | 61 | type UwsgiCache struct { 62 | Hits int `json:"hits"` 63 | Misses int `json:"miss"` 64 | Items int `json:"items"` 65 | MaxItems int `json:"max_items"` 66 | Full int `json:"full"` 67 | Hash string `json:"hash"` 68 | HashSize int `json:"hashsize"` 69 | KeySize int `json:"keysize"` 70 | Blocks int `json:"blocks"` 71 | BlockSize int `json:"blocksize"` 72 | LastModifiedAt int `json:"last_modified_at"` 73 | Name string `json:"name"` 74 | } 75 | 76 | type UwsgiStats struct { 77 | Version string `json:"version"` 78 | ListenQueue int `json:"listen_queue"` 79 | ListenQueueErrors int `json:"listen_queue_errors"` 80 | SignalQueue int `json:"signal_queue"` 81 | Load int `json:"load"` 82 | PID int `json:"pid"` 83 | UID int `json:"uid"` 84 | GID int `json:"gid"` 85 | CWD string `json:"cwd"` 86 | Sockets []UwsgiSocket `json:"sockets"` 87 | Workers []UwsgiWorker `json:"workers"` 88 | Caches []UwsgiCache `json:"caches"` 89 | } 90 | 91 | func parseUwsgiStatsFromIO(r io.Reader) (*UwsgiStats, error) { 92 | var uwsgiStats UwsgiStats 93 | decoder := json.NewDecoder(r) 94 | err := decoder.Decode(&uwsgiStats) 95 | if err != nil { 96 | return nil, err 97 | } 98 | return &uwsgiStats, nil 99 | } 100 | -------------------------------------------------------------------------------- /pkg/collector/testdata/sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.12", 3 | "listen_queue": 0, 4 | "listen_queue_errors": 0, 5 | "signal_queue": 0, 6 | "load": 0, 7 | "pid": 1, 8 | "uid": 1000, 9 | "gid": 0, 10 | "cwd": "/regent", 11 | "locks": [{ 12 | "user 0": 0 13 | }, { 14 | "signal": 0 15 | }, { 16 | "filemon": 0 17 | }, { 18 | "timer": 0 19 | }, { 20 | "rbtimer": 0 21 | }, { 22 | "cron": 0 23 | }, { 24 | "rpc": 0 25 | }, { 26 | "snmp": 0 27 | }], 28 | "caches":[{ 29 | "name":"cache_1", 30 | "hash":"djb33x", 31 | "hashsize":65536, 32 | "keysize":2048, 33 | "max_items":2000, 34 | "blocks":2000, 35 | "blocksize":65536, 36 | "items":39, 37 | "hits":56614, 38 | "miss":4931570, 39 | "full":0, 40 | "last_modified_at":0 41 | }, { 42 | "name":"cache_2", 43 | "hash":"djb33x", 44 | "hashsize":65536, 45 | "keysize":2048, 46 | "max_items":1000, 47 | "blocks":1000, 48 | "blocksize":65536, 49 | "items":0, 50 | "hits":5, 51 | "miss":9251, 52 | "full":0, 53 | "last_modified_at":0 }], 54 | "sockets": [{ 55 | "name": "127.0.0.1:36577", 56 | "proto": "uwsgi", 57 | "queue": 0, 58 | "max_queue": 100, 59 | "shared": 0, 60 | "can_offload": 0 61 | }, { 62 | "name": "127.0.0.1:36577", 63 | "proto": "uwsgi", 64 | "queue": 0, 65 | "max_queue": 0, 66 | "shared": 0, 67 | "can_offload": 0}], 68 | "workers": [{ 69 | "id": 1, 70 | "pid": 9, 71 | "accepting": 1, 72 | "requests": 0, 73 | "delta_requests": 0, 74 | "exceptions": 0, 75 | "harakiri_count": 0, 76 | "signals": 0, 77 | "signal_queue": 0, 78 | "status": "idle", 79 | "rss": 0, 80 | "vsz": 0, 81 | "running_time": 0, 82 | "last_spawn": 1457410597, 83 | "respawn_count": 1, 84 | "tx": 0, 85 | "avg_rt": 0, 86 | "apps": [{ 87 | "id": 0, 88 | "modifier1": 0, 89 | "mountpoint": "", 90 | "startup_time": 0, 91 | "requests": 0, 92 | "exceptions": 0, 93 | "chdir": "" 94 | }], 95 | "cores": [{ 96 | "id": 0, 97 | "requests": 0, 98 | "static_requests": 0, 99 | "routed_requests": 0, 100 | "offloaded_requests": 0, 101 | "write_errors": 0, 102 | "read_errors": 0, 103 | "in_request": 0, 104 | "vars": [] 105 | }] 106 | }, { 107 | "id": 2, 108 | "pid": 10, 109 | "accepting": 1, 110 | "requests": 0, 111 | "delta_requests": 0, 112 | "exceptions": 0, 113 | "harakiri_count": 0, 114 | "signals": 0, 115 | "signal_queue": 0, 116 | "status": "idle", 117 | "rss": 0, 118 | "vsz": 0, 119 | "running_time": 0, 120 | "last_spawn": 1457410597, 121 | "respawn_count": 1, 122 | "tx": 0, 123 | "avg_rt": 0, 124 | "apps": [{ 125 | "id": 0, 126 | "modifier1": 0, 127 | "mountpoint": "", 128 | "startup_time": 0, 129 | "requests": 0, 130 | "exceptions": 0, 131 | "chdir": "" 132 | }], 133 | "cores": [{ 134 | "id": 0, 135 | "requests": 0, 136 | "static_requests": 0, 137 | "routed_requests": 0, 138 | "offloaded_requests": 0, 139 | "write_errors": 0, 140 | "read_errors": 0, 141 | "in_request": 0, 142 | "vars": [] 143 | }] 144 | }] 145 | } 146 | -------------------------------------------------------------------------------- /pkg/collector/testdata/wrong.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.12", 3 | "listen_queue": 0, 4 | "listen_queue_errors": 0, 5 | -------------------------------------------------------------------------------- /scripts/errcheck_excludes.txt: -------------------------------------------------------------------------------- 1 | // Don't flag lines such as "io.Copy(ioutil.Discard, resp.Body)". 2 | io.Copy 3 | // The next two are used in HTTP handlers, any error is handled by the server itself. 4 | io.WriteString 5 | (net/http.ResponseWriter).Write 6 | // No need to check for errors on server's shutdown. 7 | (*net/http.Server).Shutdown 8 | 9 | // Never check for logger errors. 10 | (github.com/go-kit/log.Logger).Log 11 | --------------------------------------------------------------------------------