├── .dockerignore ├── .github ├── dependabot.yml └── workflows │ ├── codeql-analysis.yml │ ├── pr.yml │ ├── release.yml │ └── shiftleft-analysis.yml ├── .gitignore ├── .golangci ├── .goreleaser.yml ├── .vscode ├── launch.json └── settings.json ├── Dockerfile ├── LICENSE ├── README.md ├── VERSION ├── bertmap.go ├── bertmap_test.go ├── config.example.json ├── config.go ├── config_test.go ├── curl.go ├── decoder.go ├── exporter.go ├── exporter_aliveness.go ├── exporter_connections.go ├── exporter_exchange.go ├── exporter_federation.go ├── exporter_memory.go ├── exporter_node.go ├── exporter_overview.go ├── exporter_queue.go ├── exporter_shovel.go ├── exporter_test.go ├── go.mod ├── go.sum ├── integration_test.go ├── jsonmap.go ├── jsonmap_test.go ├── main.go ├── main_test.go ├── metrics.go ├── metrics.md ├── pprof_test.go ├── rabbitClient.go ├── rabbitClient_test.go ├── renovate.json ├── service.go ├── service_windows.go ├── testdata ├── exchanges-3.6.8.bert ├── exchanges-3.6.8.json ├── exchanges-3.7.0.bert ├── exchanges-3.7.0.json ├── nodes-3.6.8.bert ├── nodes-3.6.8.json ├── nodes-3.7.0.bert ├── nodes-3.7.0.json ├── overview-3.6.8.bert ├── overview-3.6.8.json ├── overview-3.7.0.bert ├── overview-3.7.0.json ├── password_file ├── queue-max-length.bert ├── queue-max-length.json ├── queues-3.6.8.bert ├── queues-3.6.8.json ├── queues-3.7.0.bert ├── queues-3.7.0.json └── username_file ├── testenv ├── Dockerfile ├── rabbit.go └── testenv.go └── version.go /.dockerignore: -------------------------------------------------------------------------------- 1 | .build/ 2 | .tarballs/ 3 | 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Maintain dependencies for GitHub Actions 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" -------------------------------------------------------------------------------- /.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: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '25 19 * * 3' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'go' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v4 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v3 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v3 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v3 68 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: PR 2 | 3 | on: 4 | pull_request: 5 | branches: [ main ] 6 | 7 | jobs: 8 | tests: 9 | name: go tests 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Install Go 13 | uses: actions/setup-go@v5 14 | with: 15 | go-version: '1.21' 16 | - name: Check out code into the Go module directory 17 | uses: actions/checkout@v4 18 | - name: run tests 19 | run: | 20 | export GOPATH=$HOME/go 21 | export PATH=$PATH:$GOPATH/bin 22 | go test ./... -v 23 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | permissions: 9 | contents: write 10 | packages: write 11 | 12 | jobs: 13 | goreleaser: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - 17 | name: Checkout 18 | uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | - 22 | name: Set up Go 23 | uses: actions/setup-go@v5 24 | with: 25 | go-version: '1.21' 26 | - 27 | name: Docker Login 28 | uses: docker/login-action@v3 29 | with: 30 | registry: ghcr.io 31 | username: ${{ github.repository_owner }} 32 | password: ${{ secrets.GITHUB_TOKEN }} 33 | - 34 | name: Login to Docker Hub 35 | uses: docker/login-action@v3 36 | with: 37 | username: ${{ secrets.DOCKERHUB_USERNAME }} 38 | password: ${{ secrets.DOCKERHUB_TOKEN }} 39 | - 40 | name: Set up QEMU 41 | uses: docker/setup-qemu-action@v3 42 | - 43 | name: Docker Setup Buildx 44 | id: buildx 45 | uses: docker/setup-buildx-action@v3.6.1 46 | - 47 | name: Available buildx platforms 48 | run: echo ${{ steps.buildx.outputs.platforms }} 49 | - 50 | name: Run GoReleaser 51 | uses: goreleaser/goreleaser-action@v6 52 | with: 53 | distribution: goreleaser 54 | version: latest 55 | args: release --rm-dist 56 | env: 57 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 58 | -------------------------------------------------------------------------------- /.github/workflows/shiftleft-analysis.yml: -------------------------------------------------------------------------------- 1 | # This workflow integrates Scan with GitHub's code scanning feature 2 | # Scan is a free open-source security tool for modern DevOps teams from ShiftLeft 3 | # Visit https://slscan.io/en/latest/integrations/code-scan for help 4 | name: SL Scan 5 | 6 | # This section configures the trigger for the workflow. Feel free to customize depending on your convention 7 | on: push 8 | 9 | jobs: 10 | Scan-Build: 11 | # Scan runs on ubuntu, mac and windows 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | # Instructions 16 | # 1. Setup JDK, Node.js, Python etc depending on your project type 17 | # 2. Compile or build the project before invoking scan 18 | # Example: mvn compile, or npm install or pip install goes here 19 | # 3. Invoke Scan with the github token. Leave the workspace empty to use relative url 20 | 21 | - name: Perform Scan 22 | uses: ShiftLeftSecurity/scan-action@master 23 | env: 24 | WORKSPACE: "" 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | SCAN_AUTO_BUILD: true 27 | with: 28 | output: reports 29 | # Scan auto-detects the languages in your project. To override uncomment the below variable and set the type 30 | # type: credscan,java 31 | # type: python 32 | 33 | - name: Upload report 34 | uses: github/codeql-action/upload-sarif@v3 35 | with: 36 | sarif_file: reports 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .build 2 | dependencies-stamp 3 | rabbitmq_exporter 4 | rabbitmq_exporter.exe 5 | .tarballs/ 6 | coverage.out 7 | debug.test 8 | vendor/ 9 | dist/ 10 | -------------------------------------------------------------------------------- /.golangci: -------------------------------------------------------------------------------- 1 | linters-settings: 2 | goimports: 3 | local-prefixes: github.com/kbudde/rabbitmq_exporter 4 | golint: 5 | min-confidence: 0.8 6 | gocyclo: 7 | min-complexity: 15 8 | govet: 9 | check-shadowing: true 10 | misspell: 11 | locale: US 12 | nolintlint: 13 | allow-leading-space: false # require machine-readable nolint directives (with no leading space) 14 | allow-unused: false # report any unused nolint directives 15 | require-explanation: true # require an explanation for nolint directives 16 | require-specific: false # don't require nolint directives to be specific about which linter is being skipped 17 | 18 | linters: 19 | # please, do not use `enable-all`: it's deprecated and will be removed soon. 20 | # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint 21 | disable-all: true 22 | enable: 23 | - deadcode 24 | - errcheck 25 | - gosimple 26 | - govet 27 | - ineffassign 28 | - staticcheck 29 | - structcheck 30 | - typecheck 31 | - unused 32 | - varcheck 33 | - bodyclose 34 | - depguard 35 | - dupl 36 | - exportloopref 37 | - forcetypeassert 38 | - funlen 39 | - gci 40 | - gocognit 41 | - goconst 42 | - gocritic 43 | - gocyclo 44 | - gofumpt 45 | - gomnd 46 | - goprintffuncname 47 | - gosec 48 | - ifshort 49 | - misspell 50 | - noctx 51 | - nolintlint 52 | - revive 53 | - rowserrcheck 54 | - sqlclosecheck 55 | # - stylecheck 56 | - thelper 57 | - tparallel 58 | - unconvert 59 | - unparam 60 | - whitespace 61 | - errorlint 62 | # - goerr113 63 | # - wrapcheck 64 | issues: 65 | # enable issues excluded by default 66 | exclude-use-default: false -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | before: 2 | hooks: 3 | - go mod tidy 4 | builds: 5 | - env: 6 | - CGO_ENABLED=0 7 | #binary: rabbitmq_exporter-{{.Version}}.{{.Target}} 8 | targets: 9 | - linux_amd64 10 | - linux_arm64 11 | - linux_arm_6 12 | - linux_arm_7 13 | - windows_amd64 14 | - darwin_amd64 15 | - darwin_arm64 16 | tags: netgo staticbuild 17 | ldflags: 18 | - -X main.Version={{.Version}} 19 | - -X main.Revision={{.ShortCommit}} 20 | - -X main.Branch={{.Branch}} 21 | - -X main.BuildDate={{.CommitDate}} 22 | checksum: 23 | name_template: 'checksums.txt' 24 | changelog: 25 | use: github-native 26 | release: 27 | discussion_category_name: Announcements 28 | footer: | 29 | ## Docker images 30 | 31 | - `docker pull ghcr.io/kbudde/rabbitmq_exporter:{{.Version}}` 32 | - `docker pull ghcr.io/kbudde/rabbitmq_exporter:{{.ShortCommit}}` 33 | - `docker pull kbudde/rabbitmq-exporter:{{.Version}}` 34 | - `docker pull kbudde/rabbitmq-exporter:{{.ShortCommit}}` 35 | 36 | dockers: 37 | - goos: linux 38 | goarch: amd64 39 | image_templates: 40 | - "kbudde/rabbitmq-exporter:linux-amd64-{{.ShortCommit}}" 41 | - "ghcr.io/kbudde/rabbitmq_exporter:linux-amd64-{{.ShortCommit}}" 42 | use: buildx 43 | build_flag_templates: 44 | - "--label=org.opencontainers.image.created={{.Date}}" 45 | - "--label=org.opencontainers.image.title={{.ProjectName}}" 46 | - "--label=org.opencontainers.image.revision={{.FullCommit}}" 47 | - "--label=org.opencontainers.image.version={{.Version}}" 48 | - "--label=org.opencontainers.image.source=https://github.com/kbudde/rabbitmq_exporter" 49 | - "--platform=linux/amd64" 50 | 51 | - goos: linux 52 | goarch: arm 53 | goarm: "6" 54 | image_templates: 55 | - "kbudde/rabbitmq-exporter:linux-arm6-{{.ShortCommit}}" 56 | - "ghcr.io/kbudde/rabbitmq_exporter:linux-arm6-{{.ShortCommit}}" 57 | use: buildx 58 | build_flag_templates: 59 | - "--label=org.opencontainers.image.created={{.Date}}" 60 | - "--label=org.opencontainers.image.title={{.ProjectName}}" 61 | - "--label=org.opencontainers.image.revision={{.FullCommit}}" 62 | - "--label=org.opencontainers.image.version={{.Version}}" 63 | - "--label=org.opencontainers.image.source=https://github.com/kbudde/rabbitmq_exporter" 64 | - "--platform=linux/arm/v6" 65 | 66 | - goos: linux 67 | goarch: arm 68 | goarm: "7" 69 | image_templates: 70 | - "kbudde/rabbitmq-exporter:linux-arm7-{{.ShortCommit}}" 71 | - "ghcr.io/kbudde/rabbitmq_exporter:linux-arm7-{{.ShortCommit}}" 72 | use: buildx 73 | build_flag_templates: 74 | - "--label=org.opencontainers.image.created={{.Date}}" 75 | - "--label=org.opencontainers.image.title={{.ProjectName}}" 76 | - "--label=org.opencontainers.image.revision={{.FullCommit}}" 77 | - "--label=org.opencontainers.image.version={{.Version}}" 78 | - "--label=org.opencontainers.image.source=https://github.com/kbudde/rabbitmq_exporter" 79 | - "--platform=linux/arm/v7" 80 | 81 | - goos: linux 82 | goarch: arm64 83 | image_templates: 84 | - "kbudde/rabbitmq-exporter:linux-arm64-{{.ShortCommit}}" 85 | - "ghcr.io/kbudde/rabbitmq_exporter:linux-arm64-{{.ShortCommit}}" 86 | use: buildx 87 | build_flag_templates: 88 | - "--label=org.opencontainers.image.created={{.Date}}" 89 | - "--label=org.opencontainers.image.title={{.ProjectName}}" 90 | - "--label=org.opencontainers.image.revision={{.FullCommit}}" 91 | - "--label=org.opencontainers.image.version={{.Version}}" 92 | - "--label=org.opencontainers.image.source=https://github.com/kbudde/rabbitmq_exporter" 93 | - "--platform=linux/arm64" 94 | 95 | 96 | docker_manifests: 97 | - 98 | name_template: ghcr.io/kbudde/rabbitmq_exporter:latest 99 | image_templates: 100 | - ghcr.io/kbudde/rabbitmq_exporter:linux-amd64-{{.ShortCommit}} 101 | - ghcr.io/kbudde/rabbitmq_exporter:linux-arm6-{{.ShortCommit}} 102 | - ghcr.io/kbudde/rabbitmq_exporter:linux-arm7-{{.ShortCommit}} 103 | - ghcr.io/kbudde/rabbitmq_exporter:linux-arm64-{{.ShortCommit}} 104 | - 105 | name_template: kbudde/rabbitmq-exporter:latest 106 | image_templates: 107 | - kbudde/rabbitmq-exporter:linux-amd64-{{.ShortCommit}} 108 | - kbudde/rabbitmq-exporter:linux-arm6-{{.ShortCommit}} 109 | - kbudde/rabbitmq-exporter:linux-arm7-{{.ShortCommit}} 110 | - kbudde/rabbitmq-exporter:linux-arm64-{{.ShortCommit}} 111 | - 112 | name_template: ghcr.io/kbudde/rabbitmq_exporter:{{.Version}} 113 | image_templates: 114 | - ghcr.io/kbudde/rabbitmq_exporter:linux-amd64-{{.ShortCommit}} 115 | - ghcr.io/kbudde/rabbitmq_exporter:linux-arm6-{{.ShortCommit}} 116 | - ghcr.io/kbudde/rabbitmq_exporter:linux-arm7-{{.ShortCommit}} 117 | - ghcr.io/kbudde/rabbitmq_exporter:linux-arm64-{{.ShortCommit}} 118 | - 119 | name_template: kbudde/rabbitmq-exporter:{{.Version}} 120 | image_templates: 121 | - kbudde/rabbitmq-exporter:linux-amd64-{{.ShortCommit}} 122 | - kbudde/rabbitmq-exporter:linux-arm6-{{.ShortCommit}} 123 | - kbudde/rabbitmq-exporter:linux-arm7-{{.ShortCommit}} 124 | - kbudde/rabbitmq-exporter:linux-arm64-{{.ShortCommit}} -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug Tests", 6 | "type": "go", 7 | "request": "launch", 8 | "mode": "test", 9 | "remotePath": "", 10 | "port": 2345, 11 | "host": "127.0.0.1", 12 | "program": "${workspaceRoot}", 13 | "env": {}, 14 | "args": [], 15 | "showLog": true 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "go.lintTool": "golangci-lint" 3 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine AS builder 2 | 3 | # Install the Certificate-Authority certificates for the app to be able to make 4 | # calls to HTTPS endpoints. 5 | # Git is required for fetching the dependencies. 6 | RUN apk add --no-cache ca-certificates 7 | 8 | # Final stage: the running container. 9 | FROM scratch AS final 10 | 11 | # Add maintainer label in case somebody has questions. 12 | LABEL maintainer="Kris.Budde@gmail.com" 13 | 14 | # Import the Certificate-Authority certificates for enabling HTTPS. 15 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 16 | 17 | # Import the compiled executable from the first stage. 18 | COPY rabbitmq_exporter /rabbitmq_exporter 19 | 20 | # Declare the port on which the webserver will be exposed. 21 | # As we're going to run the executable as an unprivileged user, we can't bind 22 | # to ports below 1024. 23 | EXPOSE 9419 24 | 25 | # Perform any further action as an unprivileged user. 26 | USER 65535:65535 27 | 28 | # Check if exporter is alive; 10 retries gives prometheus some time to retrieve bad data (5 minutes) 29 | HEALTHCHECK --retries=10 CMD ["/rabbitmq_exporter", "-check-url", "http://localhost:9419/health"] 30 | 31 | # Run the compiled binary. 32 | ENTRYPOINT ["/rabbitmq_exporter"] 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Syncano 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 📣 EOL Announcement 2 | 3 | IMPORTANT: This exporter only works with RabbitMQ 3. Please use the official exporter for RabbitMQ 4 or newer. See https://github.com/kbudde/rabbitmq_exporter/issues/383 for details. 4 | 5 | # RabbitMQ Exporter [![Build Status](https://travis-ci.org/kbudde/rabbitmq_exporter.svg?branch=master)](https://travis-ci.org/kbudde/rabbitmq_exporter) [![Coverage Status](https://coveralls.io/repos/kbudde/rabbitmq_exporter/badge.svg?branch=master)](https://coveralls.io/r/kbudde/rabbitmq_exporter?branch=master) [![](https://images.microbadger.com/badges/image/kbudde/rabbitmq-exporter.svg)](http://microbadger.com/images/kbudde/rabbitmq-exporter "Get your own image badge on microbadger.com") 6 | 7 | Prometheus exporter for RabbitMQ metrics. 8 | Data is scraped by [prometheus](https://prometheus.io). 9 | 10 | Please note this an unofficial plugin. There is also an official plugin from [RabbitMQ.com](https://www.rabbitmq.com/prometheus.html). See [comparison to official exporter](#comparison-to-official-exporter) 11 | 12 | ## Installation 13 | 14 | ### Binary release 15 | 16 | You can download the latest release on the [release page](https://github.com/kbudde/rabbitmq_exporter/releases). 17 | Docker images are push to [docker hub](https://hub.docker.com/r/kbudde/rabbitmq-exporter/tags) 18 | 19 | ### docker: rabbitmq container with network sharing 20 | 21 | The rabbitmq_exporter is sharing the network interface with the rabbitmq container -> it is possible to use localhost and default user/password (guest). 22 | 23 | 1. Start rabbitMQ 24 | 25 | docker run -d -e RABBITMQ_NODENAME=my-rabbit --name my-rabbit -p 9419:9419 rabbitmq:3-management 26 | 27 | 1. Start rabbitmq_exporter in container. 28 | 29 | docker run -d --net=container:my-rabbit kbudde/rabbitmq-exporter 30 | 31 | Now your metrics are exposed through [http://host:9419/metrics](http://host:9419/metrics). The management plugin does not need to be exposed. 32 | 33 | ## Configuration 34 | 35 | Rabbitmq_exporter can be configured using json config file or environment variables for configuration. 36 | 37 | ### Config file 38 | 39 | Rabbitmq_exporter expects config file in "conf/rabbitmq.conf". If you are running the exporter in a container (docker/kubernetes) the config must be in "/conf/rabbitmq.conf" 40 | The name of the file can be overriden with flag: 41 | 42 | ./rabbitmq_exporter -config-file config.example.json 43 | 44 | You can find an example [here](config.example.json). *Note:* If you are using a config file, you must provide all values as there is no default value. 45 | 46 | ### Settings 47 | 48 | Environment variable|default|description 49 | --------------------|-------|------------ 50 | RABBIT_URL | | url to rabbitMQ management plugin (must start with http(s)://) 51 | RABBIT_USER | guest | username for rabbitMQ management plugin. User needs monitoring tag! 52 | RABBIT_PASSWORD | guest | password for rabbitMQ management plugin 53 | RABBIT_CONNECTION | direct | direct or loadbalancer, strips the self label when loadbalancer 54 | RABBIT_USER_FILE| | location of file with username (useful for docker secrets) 55 | RABBIT_PASSWORD_FILE | | location of file with password (useful for docker secrets) 56 | PUBLISH_PORT | 9419 | Listening port for the exporter 57 | PUBLISH_ADDR | "" | Listening host/IP for the exporter 58 | OUTPUT_FORMAT | TTY | Log ouput format. TTY and JSON are suported 59 | LOG_LEVEL | info | log level. possible values: "debug", "info", "warning", "error", "fatal", or "panic" 60 | CAFILE | ca.pem | path to root certificate for access management plugin. Just needed if self signed certificate is used. Will be ignored if the file does not exist 61 | CERTFILE | client-cert.pem | path to client certificate used to verify the exporter's authenticity. Will be ignored if the file does not exist 62 | KEYFILE | client-key.pem | path to private key used with certificate to verify the exporter's authenticity. Will be ignored if the file does not exist 63 | SKIPVERIFY | false | true/0 will ignore certificate errors of the management plugin 64 | SKIP_VHOST | ^$ |regex, matching vhost names are not exported. First performs INCLUDE_VHOST, then SKIP_VHOST. Applies to queues and exchanges 65 | INCLUDE_VHOST | .* | regex vhost filter. Only matching vhosts are exported. Applies to queues and exchanges 66 | INCLUDE_QUEUES | .* | regex queue filter. Just matching names are exported 67 | SKIP_QUEUES | ^$ |regex, matching queue names are not exported (useful for short-lived rpc queues). First performed INCLUDE, after SKIP 68 | INCLUDE_EXCHANGES | .* | regex exchange filter. (Only exchanges in matching vhosts are exported) 69 | SKIP_EXCHANGES | ^$ | regex, matching exchanges names are not exported. First performed INCLUDE, after SKIP 70 | RABBIT_CAPABILITIES | bert,no_sort | comma-separated list of extended scraping capabilities supported by the target RabbitMQ server 71 | RABBIT_EXPORTERS | exchange,node,queue | List of enabled modules. Possible modules: connections,shovel,federation,exchange,node,queue,memory 72 | RABBIT_TIMEOUT | 30 | timeout in seconds for retrieving data from management plugin. 73 | MAX_QUEUES | 0 | max number of queues before we drop metrics (disabled if set to 0) 74 | EXCLUDE_METRICS | | Metric names to exclude from export. comma-seperated. e.g. "recv_oct, recv_cnt". See exporter_*.go for names 75 | 76 | Example and recommended settings: 77 | 78 | SKIP_QUEUES="RPC_.*" MAX_QUEUES=5000 ./rabbitmq_exporter 79 | 80 | ### Extended RabbitMQ capabilities 81 | 82 | Newer version of RabbitMQ can provide some features that reduce 83 | overhead imposed by scraping the data needed by this exporter. The 84 | following capabilities are currently supported in 85 | `RABBIT_CAPABILITIES` env var: 86 | 87 | * `no_sort`: By default RabbitMQ management plugin sorts results using 88 | the default sort order of vhost/name. This sorting overhead can be 89 | avoided by passing empty sort argument (`?sort=`) to RabbitMQ 90 | starting from version 3.6.8. This option can be safely enabled on 91 | earlier 3.6.X versions, but it'll not give any performance 92 | improvements. And it's incompatible with 3.4.X and 3.5.X. 93 | * `bert`: Since 3.6.9 (see 94 | ) RabbitMQ 95 | supports BERT encoding as a JSON alternative. Given that BERT 96 | encoding is implemented in C inside the Erlang VM, it's way more 97 | effective than pure-Erlang JSON encoding. So this greatly reduces 98 | monitoring overhead when we have a lot of objects in RabbitMQ. 99 | 100 | ## Comparison to official exporter 101 | 102 | [official exporter](https://www.rabbitmq.com/prometheus.html): 103 | 104 | - has runtime/erlang metrics 105 | - aggregated or per-object metrics 106 | - missing filter 107 | 108 | This exporter: 109 | 110 | - works also with older versions of rabbitmq 111 | - has more configuration options/ filtering of objects 112 | - (bad) depends on data from management interface which can be slow/delayed 113 | 114 | probalby best solution is to use both exporters: 115 | [comment from shamil](https://github.com/kbudde/rabbitmq_exporter/issues/156#issuecomment-631979910) 116 | 117 | ## common errors / FAQ 118 | 119 | ### msg: Error while retrieving data from rabbitHost statusCode: 500 120 | 121 | This exporter expects capabilities from rabbitmq 3.6.8 or newer by default. 122 | If you are running older than 3.6.8 you must disable bert and no_sort with the setting RABBIT_CAPABILITIES=compat. 123 | If you are running 3.13.0 or newer you must disable no_sort with the setting RABBIT_CAPABILITIES=no_sort. 124 | 125 | ### missing data in graphs 126 | 127 | If there is a load balancer between the exporter and the RabbitMQApi, the setting `RABBIT_CONNECTION=loadbalancer` must be activated. 128 | See https://github.com/kbudde/rabbitmq_exporter/issues/131 for details. 129 | 130 | ## build and test 131 | 132 | This project uses goreleaser to build and release the project. You can build the project with the following command: 133 | 134 | goreleaser build --snapshot 135 | 136 | `go build` will also work, but it will not include the version information, uses cgo, etc. 137 | 138 | To run the tests, use the following command: 139 | 140 | go test -v ./... 141 | 142 | If you have docker installed, you can run the tests with the following command: 143 | 144 | go test -v ./... --tags integration 145 | 146 | This will start a rabbitmq container and run the tests against it. 147 | 148 | ## Metrics 149 | 150 | The metrics are documented in the [metrics.md](metrics.md) file. 151 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | main -------------------------------------------------------------------------------- /bertmap.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math/big" 6 | 7 | bert "github.com/kbudde/gobert" 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | // rabbitBERTReply (along with its RabbitReply interface 12 | // implementation) allow parsing of BERT-encoded RabbitMQ replies in a 13 | // way that's fully compatible with JSON parser from jsonmap.go 14 | type rabbitBERTReply struct { 15 | body []byte 16 | objects bert.Term 17 | } 18 | 19 | func makeBERTReply(body []byte) (RabbitReply, error) { 20 | rawObjects, err := bert.Decode(body) 21 | return &rabbitBERTReply{body, rawObjects}, err 22 | } 23 | 24 | func (rep *rabbitBERTReply) MakeStatsInfo(labels []string) []StatsInfo { 25 | rawObjects := rep.objects 26 | 27 | objects, ok := rawObjects.([]bert.Term) 28 | if !ok { 29 | log.WithField("got", rawObjects).Error("Statistics reply should contain a slice of objects") 30 | return make([]StatsInfo, 0) 31 | } 32 | 33 | statistics := make([]StatsInfo, 0, len(objects)) 34 | 35 | for _, v := range objects { 36 | obj, ok := parseSingleStatsObject(v, labels) 37 | if !ok { 38 | log.WithField("got", v).Error("Ignoring unparseable stats object") 39 | continue 40 | } 41 | statistics = append(statistics, *obj) 42 | } 43 | 44 | return statistics 45 | } 46 | 47 | func (rep *rabbitBERTReply) MakeMap() MetricMap { 48 | flMap := make(MetricMap) 49 | term := rep.objects 50 | 51 | err := parseProplist(&flMap, "", term) 52 | if err != nil { 53 | log.WithField("error", err).Warn("Error parsing rabbitmq reply (bert, MakeMap)") 54 | } 55 | return flMap 56 | } 57 | 58 | // iterateBertKV helps to traverse any map-like structures returned by 59 | // RabbitMQ with a user-provided function. We need it because 60 | // different versions of RabbitMQ can encode a map in a bunch of 61 | // different ways: 62 | // - proplist 63 | // - proplist additionally wrapped in a {struct, ...} tuple 64 | // - map type available in modern erlang versions 65 | // 66 | // Non-nil error return means that an object can't be interpreted as a map in any way 67 | // 68 | // Provided function can return 'false' value to stop traversal earlier 69 | func iterateBertKV(obj interface{}, elemFunc func(string, interface{}) bool) error { 70 | switch obj := obj.(type) { 71 | case []bert.Term: 72 | pairs, ok := assertBertProplistPairs(obj) 73 | if !ok { 74 | return bertError("Doesn't look like a proplist", obj) 75 | } 76 | for _, v := range pairs { 77 | key, value, ok := assertBertKeyedTuple(v) 78 | if ok { 79 | needToContinue := elemFunc(key, value) 80 | if !needToContinue { 81 | return nil 82 | } 83 | } 84 | } 85 | return nil 86 | case bert.Map: 87 | for keyRaw, value := range obj { 88 | key, ok := parseBertStringy(keyRaw) 89 | if ok { 90 | needToContinue := elemFunc(key, value) 91 | if !needToContinue { 92 | return nil 93 | } 94 | } 95 | } 96 | return nil 97 | default: 98 | return bertError("Can't iterate over non-KV object", obj) 99 | } 100 | } 101 | 102 | // parseSingleStatsObject extracts information about a named RabbitMQ 103 | // object: both its vhost/name information and then the usual 104 | // MetricMap. 105 | func parseSingleStatsObject(obj interface{}, labels []string) (*StatsInfo, bool) { 106 | var result StatsInfo 107 | var objectOk = true 108 | result.metrics = make(MetricMap) 109 | result.labels = make(map[string]string) 110 | for _, label := range labels { 111 | result.labels[label] = "" 112 | } 113 | err := iterateBertKV(obj, func(key string, value interface{}) bool { 114 | //Check if current key should be saved as label 115 | for _, label := range labels { 116 | if key == label { 117 | tmp, ok := parseBertStringy(value) 118 | if !ok { 119 | log.WithField("got", value).WithField("label", label).Error("Non-string field") 120 | objectOk = false 121 | return false 122 | } 123 | result.labels[label] = tmp 124 | } 125 | } 126 | 127 | arr, isSlice := assertBertSlice(value) 128 | _, iSPropList := assertBertProplistPairs(value) 129 | 130 | //save metrics for array length. 131 | // An array is a slice which is not a proplist. 132 | // Arrays with len()==0 are special. IsProplist is true 133 | if isSlice && (!iSPropList || len(arr) == 0) { 134 | result.metrics[key+"_len"] = float64(len(arr)) 135 | } 136 | 137 | if floatValue, ok := parseFloaty(value); ok { 138 | result.metrics[key] = floatValue 139 | return true 140 | } 141 | 142 | // Nested structures don't need special 143 | // processing, so we fallback to generic 144 | // parser. 145 | if err := parseProplist(&result.metrics, key, value); err == nil { 146 | return true 147 | } 148 | return true 149 | }) 150 | if err == nil && objectOk { 151 | return &result, true 152 | } 153 | return nil, false 154 | } 155 | 156 | // parseProplist descends into an erlang data structure and stores 157 | // everything remotely resembling a float in a toMap. 158 | func parseProplist(toMap *MetricMap, basename string, maybeProplist interface{}) error { 159 | prefix := "" 160 | if basename != "" { 161 | prefix = basename + "." 162 | } 163 | return iterateBertKV(maybeProplist, func(key string, value interface{}) bool { 164 | if floatValue, ok := parseFloaty(value); ok { 165 | (*toMap)[prefix+key] = floatValue 166 | return true 167 | } 168 | if arraySize, ok := parseArray(value); ok { 169 | (*toMap)[prefix+key+"_len"] = arraySize 170 | } 171 | 172 | err := parseProplist(toMap, prefix+key, value) // This can fail, but we don't care 173 | log.WithField("error", err).Debug("Error parsing rabbitmq reply (bert, parseProplist)") 174 | return true 175 | }) 176 | } 177 | 178 | // assertBertSlice checks whether the provided value is something 179 | // that's represented as a slice by BERT parcer (list or tuple). 180 | func assertBertSlice(maybeSlice interface{}) ([]bert.Term, bool) { 181 | switch it := maybeSlice.(type) { 182 | case []bert.Term: 183 | return it, true 184 | default: 185 | return nil, false 186 | } 187 | } 188 | 189 | // assertBertKeyedTuple checks whether the provided value looks like 190 | // an element of proplist - 2-element tuple where the first elemen is 191 | // an atom. 192 | func assertBertKeyedTuple(maybeTuple interface{}) (string, bert.Term, bool) { 193 | tuple, ok := assertBertSlice(maybeTuple) 194 | if !ok { 195 | return "", nil, false 196 | } 197 | if len(tuple) != 2 { 198 | return "", nil, false 199 | } 200 | key, ok := assertBertAtom(tuple[0]) 201 | if !ok { 202 | return "", nil, false 203 | } 204 | return key, tuple[1], true 205 | } 206 | 207 | func assertBertAtom(val interface{}) (string, bool) { 208 | if atom, ok := val.(bert.Atom); ok { 209 | return string(atom), true 210 | } 211 | return "", false 212 | } 213 | 214 | // assertBertProplistPairs checks whether the provided value points to 215 | // a proplist. Additional level of {struct, ...} wrapping can be 216 | // removed in process. 217 | func assertBertProplistPairs(maybeTaggedProplist interface{}) ([]bert.Term, bool) { 218 | terms, ok := assertBertSlice(maybeTaggedProplist) 219 | if !ok { 220 | return nil, false 221 | } 222 | 223 | if len(terms) == 0 { 224 | return terms, true 225 | } 226 | 227 | // Strip {struct, ...} tagging than is used to help RabbitMQ 228 | // JSON encoder 229 | key, value, ok := assertBertKeyedTuple(terms) 230 | if ok && key == "struct" { 231 | return assertBertProplistPairs(value) 232 | } 233 | 234 | // Minimal safety check - at least the first element should be 235 | // a proplist pair 236 | _, _, ok = assertBertKeyedTuple(terms[0]) 237 | if ok { 238 | return terms, true 239 | } 240 | return nil, false 241 | } 242 | 243 | // parseArray tries to interpret the provided BERT value as an array. 244 | // It returns the size of the array 245 | func parseArray(arr interface{}) (float64, bool) { 246 | switch t := arr.(type) { 247 | case []bert.Term: 248 | _, isPropList := assertBertProplistPairs(t) 249 | if !isPropList || len(t) == 0 { 250 | return float64(len(t)), true 251 | } 252 | } 253 | return 0, false 254 | } 255 | 256 | // parseFloaty tries to interpret the provided BERT value as a 257 | // float. Floats itself, integers and booleans are handled. 258 | func parseFloaty(num interface{}) (float64, bool) { 259 | switch num := num.(type) { 260 | case int: 261 | return float64(num), true 262 | case int8: 263 | return float64(num), true 264 | case int16: 265 | return float64(num), true 266 | case int32: 267 | return float64(num), true 268 | case int64: 269 | return float64(num), true 270 | case uint: 271 | return float64(num), true 272 | case uint8: 273 | return float64(num), true 274 | case uint16: 275 | return float64(num), true 276 | case uint32: 277 | return float64(num), true 278 | case uint64: 279 | return float64(num), true 280 | case float32: 281 | return float64(num), true 282 | case float64: 283 | return num, true 284 | case bert.Atom: 285 | if num == bert.TrueAtom { 286 | return 1, true 287 | } else if num == bert.FalseAtom { 288 | return 0, true 289 | } 290 | case big.Int: 291 | bigFloat := new(big.Float).SetInt(&num) 292 | result, _ := bigFloat.Float64() 293 | return result, true 294 | } 295 | return 0, false 296 | } 297 | 298 | // parseBertStringy tries to extract an Erlang value that can be 299 | // represented as a Go string (binary or atom). 300 | func parseBertStringy(val interface{}) (string, bool) { 301 | if stringer, ok := val.(fmt.Stringer); ok { 302 | return stringer.String(), true 303 | } else if atom, ok := val.(bert.Atom); ok { 304 | return string(atom), true 305 | } else if s, ok := val.(string); ok { 306 | return s, true 307 | } 308 | return "", false 309 | } 310 | 311 | type bertDecodeError struct { 312 | message string 313 | object interface{} 314 | } 315 | 316 | func (err *bertDecodeError) Error() string { 317 | return fmt.Sprintf("%s while decoding: %s", err.message, err.object) 318 | } 319 | 320 | func bertError(message string, object interface{}) error { 321 | return &bertDecodeError{message, object} 322 | } 323 | 324 | func (rep *rabbitBERTReply) GetString(label string) (string, bool) { 325 | var resValue string 326 | var result bool 327 | result = false 328 | 329 | err := iterateBertKV(rep.objects, func(key string, value interface{}) bool { 330 | //Check if current key should be saved as label 331 | 332 | if key == label { 333 | tmp, ok := parseBertStringy(value) 334 | if !ok { 335 | return false 336 | } 337 | resValue = tmp 338 | result = true 339 | return false 340 | } 341 | return true 342 | }) 343 | if err != nil { 344 | log.WithField("error", err).Warn("Error parsing rabbitmq reply (bert, GetString)") 345 | } 346 | return resValue, result 347 | } 348 | -------------------------------------------------------------------------------- /bertmap_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | "testing" 6 | 7 | "github.com/kylelemons/godebug/pretty" 8 | ) 9 | 10 | func TestStatsEquivalence(t *testing.T) { 11 | endpoints := []string{"queues", "exchanges", "nodes"} 12 | labels := map[string][]string{ 13 | "queues": queueLabelKeys, 14 | "exchanges": exchangeLabelKeys, 15 | "nodes": nodeLabelKeys, 16 | } 17 | versions := []string{"3.6.8", "3.7.0"} 18 | for _, version := range versions { 19 | for _, endpoint := range endpoints { 20 | base := endpoint + "-" + version 21 | assertBertStatsEquivalence(t, base, labels[endpoint]) 22 | } 23 | } 24 | } 25 | 26 | func TestNewFile(t *testing.T) { 27 | assertBertStatsEquivalence(t, "queue-max-length", nodeLabelKeys) 28 | } 29 | 30 | func TestMetricMapEquivalence(t *testing.T) { 31 | endpoints := []string{"overview"} 32 | versions := []string{"3.6.8", "3.7.0"} 33 | for _, version := range versions { 34 | for _, endpoint := range endpoints { 35 | base := endpoint + "-" + version 36 | assertBertMetricMapEquivalence(t, base) 37 | } 38 | } 39 | } 40 | 41 | func tryReadFiles(t *testing.T, base, firstExt, secondExt string) ([]byte, []byte) { 42 | firstFile := "testdata/" + base + "." + firstExt 43 | first, err := ioutil.ReadFile(firstFile) 44 | if err != nil { 45 | t.Fatalf("Error reading %s", firstFile) 46 | } 47 | 48 | secondFile := "testdata/" + base + "." + secondExt 49 | second, err := ioutil.ReadFile(secondFile) 50 | if err != nil { 51 | t.Fatalf("Error reading %s", secondFile) 52 | } 53 | return first, second 54 | } 55 | 56 | func assertBertStatsEquivalence(t *testing.T, baseFileName string, labels []string) { 57 | t.Helper() 58 | json, bert := tryReadFiles(t, baseFileName, "json", "bert") 59 | 60 | jsonReply, _ := makeJSONReply(json) 61 | bertReply, _ := makeBERTReply(bert) 62 | 63 | bertParsed := bertReply.MakeStatsInfo(labels) 64 | jsonParsed := jsonReply.MakeStatsInfo(labels) 65 | 66 | if diff := pretty.Compare(jsonParsed, bertParsed); diff != "" { 67 | t.Errorf("JSON/BERT mismatch for %s:\n%s", baseFileName, diff) 68 | } 69 | } 70 | 71 | func assertBertMetricMapEquivalence(t *testing.T, baseFileName string) { 72 | json, bert := tryReadFiles(t, baseFileName, "json", "bert") 73 | 74 | jsonReply, _ := makeJSONReply(json) 75 | bertReply, _ := makeBERTReply(bert) 76 | 77 | bertParsed := bertReply.MakeMap() 78 | jsonParsed := jsonReply.MakeMap() 79 | 80 | if diff := pretty.Compare(jsonParsed, bertParsed); diff != "" { 81 | t.Errorf("JSON/BERT mismatch for %s:\n%s", baseFileName, diff) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /config.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "rabbit_url": "http://127.0.0.1:15672", 3 | "rabbit_user": "guest", 4 | "rabbit_pass": "guest", 5 | "publish_port": "9419", 6 | "publish_addr": "", 7 | "output_format": "TTY", 8 | "ca_file": "ca.pem", 9 | "cert_file": "client-cert.pem", 10 | "key_file": "client-key.pem", 11 | "insecure_skip_verify": false, 12 | "exlude_metrics": [], 13 | "include_exchanges": ".*", 14 | "skip_exchanges": "^$", 15 | "include_queues": ".*", 16 | "skip_queues": "^$", 17 | "skip_vhost": "^$", 18 | "include_vhost": ".*", 19 | "rabbit_capabilities": "no_sort,bert", 20 | "aliveness_vhost": "/", 21 | "enabled_exporters": [ 22 | "exchange", 23 | "node", 24 | "overview", 25 | "queue", 26 | "aliveness" 27 | ], 28 | "timeout": 30, 29 | "max_queues": 0 30 | } -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "regexp" 8 | "strconv" 9 | "strings" 10 | 11 | "github.com/tkanos/gonfig" 12 | ) 13 | 14 | var ( 15 | config rabbitExporterConfig 16 | defaultConfig = rabbitExporterConfig{ 17 | RabbitURL: "http://127.0.0.1:15672", 18 | RabbitUsername: "guest", 19 | RabbitPassword: "guest", 20 | RabbitConnection: "direct", 21 | PublishPort: "9419", 22 | PublishAddr: "", 23 | OutputFormat: "TTY", //JSON 24 | CAFile: "ca.pem", 25 | CertFile: "client-cert.pem", 26 | KeyFile: "client-key.pem", 27 | InsecureSkipVerify: false, 28 | ExcludeMetrics: []string{}, 29 | SkipExchanges: regexp.MustCompile("^$"), 30 | IncludeExchanges: regexp.MustCompile(".*"), 31 | SkipQueues: regexp.MustCompile("^$"), 32 | IncludeQueues: regexp.MustCompile(".*"), 33 | SkipVHost: regexp.MustCompile("^$"), 34 | IncludeVHost: regexp.MustCompile(".*"), 35 | RabbitCapabilities: parseCapabilities("no_sort,bert"), 36 | AlivenessVhost: "/", 37 | EnabledExporters: []string{"exchange", "node", "overview", "queue"}, 38 | Timeout: 30, 39 | MaxQueues: 0, 40 | } 41 | ) 42 | 43 | type rabbitExporterConfig struct { 44 | RabbitURL string `json:"rabbit_url"` 45 | RabbitUsername string `json:"rabbit_user"` 46 | RabbitPassword string `json:"rabbit_pass"` 47 | RabbitConnection string `json:"rabbit_connection"` 48 | PublishPort string `json:"publish_port"` 49 | PublishAddr string `json:"publish_addr"` 50 | OutputFormat string `json:"output_format"` 51 | CAFile string `json:"ca_file"` 52 | CertFile string `json:"cert_file"` 53 | KeyFile string `json:"key_file"` 54 | InsecureSkipVerify bool `json:"insecure_skip_verify"` 55 | ExcludeMetrics []string `json:"exlude_metrics"` 56 | SkipExchanges *regexp.Regexp `json:"-"` 57 | IncludeExchanges *regexp.Regexp `json:"-"` 58 | SkipQueues *regexp.Regexp `json:"-"` 59 | IncludeQueues *regexp.Regexp `json:"-"` 60 | SkipVHost *regexp.Regexp `json:"-"` 61 | IncludeVHost *regexp.Regexp `json:"-"` 62 | IncludeExchangesString string `json:"include_exchanges"` 63 | SkipExchangesString string `json:"skip_exchanges"` 64 | IncludeQueuesString string `json:"include_queues"` 65 | SkipQueuesString string `json:"skip_queues"` 66 | SkipVHostString string `json:"skip_vhost"` 67 | IncludeVHostString string `json:"include_vhost"` 68 | RabbitCapabilitiesString string `json:"rabbit_capabilities"` 69 | RabbitCapabilities rabbitCapabilitySet `json:"-"` 70 | AlivenessVhost string `json:"aliveness_vhost"` 71 | EnabledExporters []string `json:"enabled_exporters"` 72 | Timeout int `json:"timeout"` 73 | MaxQueues int `json:"max_queues"` 74 | } 75 | 76 | type rabbitCapability string 77 | type rabbitCapabilitySet map[rabbitCapability]bool 78 | 79 | const ( 80 | rabbitCapNoSort rabbitCapability = "no_sort" 81 | rabbitCapBert rabbitCapability = "bert" 82 | ) 83 | 84 | var allRabbitCapabilities = rabbitCapabilitySet{ 85 | rabbitCapNoSort: true, 86 | rabbitCapBert: true, 87 | } 88 | 89 | func initConfigFromFile(configFile string) error { 90 | config = rabbitExporterConfig{} 91 | err := gonfig.GetConf(configFile, &config) 92 | if err != nil { 93 | return err 94 | } 95 | 96 | if url := config.RabbitURL; url != "" { 97 | if valid, _ := regexp.MatchString("https?://[a-zA-Z.0-9]+", strings.ToLower(url)); !valid { 98 | panic(fmt.Errorf("rabbit URL must start with http:// or https://")) 99 | } 100 | } 101 | 102 | config.SkipExchanges = regexp.MustCompile(config.SkipExchangesString) 103 | config.IncludeExchanges = regexp.MustCompile(config.IncludeExchangesString) 104 | config.SkipQueues = regexp.MustCompile(config.SkipQueuesString) 105 | config.IncludeQueues = regexp.MustCompile(config.IncludeQueuesString) 106 | config.SkipVHost = regexp.MustCompile(config.SkipVHostString) 107 | config.IncludeVHost = regexp.MustCompile(config.IncludeVHostString) 108 | config.RabbitCapabilities = parseCapabilities(config.RabbitCapabilitiesString) 109 | return nil 110 | } 111 | 112 | func initConfig() { 113 | config = defaultConfig 114 | if url := os.Getenv("RABBIT_URL"); url != "" { 115 | if valid, _ := regexp.MatchString("https?://[a-zA-Z.0-9]+", strings.ToLower(url)); valid { 116 | config.RabbitURL = url 117 | } else { 118 | panic(fmt.Errorf("rabbit URL must start with http:// or https://")) 119 | } 120 | } 121 | 122 | if connection := os.Getenv("RABBIT_CONNECTION"); connection != "" { 123 | if valid, _ := regexp.MatchString("(direct|loadbalancer)", connection); valid { 124 | config.RabbitConnection = connection 125 | } else { 126 | panic(fmt.Errorf("rabbit connection must be direct or loadbalancer")) 127 | } 128 | } 129 | 130 | var user string 131 | var pass string 132 | 133 | if len(os.Getenv("RABBIT_USER_FILE")) != 0 { 134 | fileContents, err := ioutil.ReadFile(os.Getenv("RABBIT_USER_FILE")) 135 | if err != nil { 136 | panic(err) 137 | } 138 | user = strings.TrimSpace(string(fileContents)) 139 | } else { 140 | user = os.Getenv("RABBIT_USER") 141 | } 142 | 143 | if user != "" { 144 | config.RabbitUsername = user 145 | } 146 | 147 | if len(os.Getenv("RABBIT_PASSWORD_FILE")) != 0 { 148 | fileContents, err := ioutil.ReadFile(os.Getenv("RABBIT_PASSWORD_FILE")) 149 | if err != nil { 150 | panic(err) 151 | } 152 | pass = strings.TrimSpace(string(fileContents)) 153 | } else { 154 | pass = os.Getenv("RABBIT_PASSWORD") 155 | } 156 | if pass != "" { 157 | config.RabbitPassword = pass 158 | } 159 | 160 | if port := os.Getenv("PUBLISH_PORT"); port != "" { 161 | if _, err := strconv.Atoi(port); err == nil { 162 | config.PublishPort = port 163 | } else { 164 | panic(fmt.Errorf("the configured port is not a valid number: %v", port)) 165 | } 166 | 167 | } 168 | 169 | if addr := os.Getenv("PUBLISH_ADDR"); addr != "" { 170 | config.PublishAddr = addr 171 | } 172 | 173 | if output := os.Getenv("OUTPUT_FORMAT"); output != "" { 174 | config.OutputFormat = output 175 | } 176 | 177 | if cafile := os.Getenv("CAFILE"); cafile != "" { 178 | config.CAFile = cafile 179 | } 180 | if certfile := os.Getenv("CERTFILE"); certfile != "" { 181 | config.CertFile = certfile 182 | } 183 | if keyfile := os.Getenv("KEYFILE"); keyfile != "" { 184 | config.KeyFile = keyfile 185 | } 186 | if insecureSkipVerify := os.Getenv("SKIPVERIFY"); insecureSkipVerify == "true" || insecureSkipVerify == "1" || insecureSkipVerify == "TRUE" { 187 | config.InsecureSkipVerify = true 188 | } 189 | 190 | if ExcludeMetrics := os.Getenv("EXCLUDE_METRICS"); ExcludeMetrics != "" { 191 | config.ExcludeMetrics = strings.Split(ExcludeMetrics, ",") 192 | } 193 | 194 | if SkipExchanges := os.Getenv("SKIP_EXCHANGES"); SkipExchanges != "" { 195 | config.SkipExchanges = regexp.MustCompile(SkipExchanges) 196 | } 197 | 198 | if IncludeExchanges := os.Getenv("INCLUDE_EXCHANGES"); IncludeExchanges != "" { 199 | config.IncludeExchanges = regexp.MustCompile(IncludeExchanges) 200 | } 201 | 202 | if SkipQueues := os.Getenv("SKIP_QUEUES"); SkipQueues != "" { 203 | config.SkipQueues = regexp.MustCompile(SkipQueues) 204 | } 205 | 206 | if IncludeQueues := os.Getenv("INCLUDE_QUEUES"); IncludeQueues != "" { 207 | config.IncludeQueues = regexp.MustCompile(IncludeQueues) 208 | } 209 | 210 | if SkipVHost := os.Getenv("SKIP_VHOST"); SkipVHost != "" { 211 | config.SkipVHost = regexp.MustCompile(SkipVHost) 212 | } 213 | 214 | if IncludeVHost := os.Getenv("INCLUDE_VHOST"); IncludeVHost != "" { 215 | config.IncludeVHost = regexp.MustCompile(IncludeVHost) 216 | } 217 | 218 | if rawCapabilities := os.Getenv("RABBIT_CAPABILITIES"); rawCapabilities != "" { 219 | config.RabbitCapabilities = parseCapabilities(rawCapabilities) 220 | } 221 | 222 | if enabledExporters := os.Getenv("RABBIT_EXPORTERS"); enabledExporters != "" { 223 | config.EnabledExporters = strings.Split(enabledExporters, ",") 224 | } 225 | 226 | if alivenessVhost := os.Getenv("ALIVENESS_VHOST"); alivenessVhost != "" { 227 | config.AlivenessVhost = alivenessVhost 228 | } 229 | 230 | if timeout := os.Getenv("RABBIT_TIMEOUT"); timeout != "" { 231 | t, err := strconv.Atoi(timeout) 232 | if err != nil { 233 | panic(fmt.Errorf("timeout is not a number: %v", err)) 234 | } 235 | config.Timeout = t 236 | } 237 | 238 | if maxQueues := os.Getenv("MAX_QUEUES"); maxQueues != "" { 239 | m, err := strconv.Atoi(maxQueues) 240 | if err != nil { 241 | panic(fmt.Errorf("maxQueues is not a number: %v", err)) 242 | } 243 | config.MaxQueues = m 244 | } 245 | } 246 | 247 | func parseCapabilities(raw string) rabbitCapabilitySet { 248 | result := make(rabbitCapabilitySet) 249 | candidates := strings.Split(raw, ",") 250 | for _, maybeCapStr := range candidates { 251 | maybeCap := rabbitCapability(strings.TrimSpace(maybeCapStr)) 252 | enabled, present := allRabbitCapabilities[maybeCap] 253 | if enabled && present { 254 | result[maybeCap] = true 255 | } 256 | } 257 | return result 258 | } 259 | 260 | func isCapEnabled(config rabbitExporterConfig, cap rabbitCapability) bool { 261 | exists, enabled := config.RabbitCapabilities[cap] 262 | return exists && enabled 263 | } 264 | 265 | func selfLabel(config rabbitExporterConfig, isSelf bool) string { 266 | if config.RabbitConnection == "loadbalancer" { 267 | return "lb" 268 | } else if isSelf { 269 | return "1" 270 | } else { 271 | return "0" 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /config_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/kylelemons/godebug/pretty" 9 | ) 10 | 11 | func TestEnvironmentSettingURL_HTTPS(t *testing.T) { 12 | newValue := "https://testURL" 13 | os.Setenv("RABBIT_URL", newValue) 14 | defer os.Unsetenv("RABBIT_URL") 15 | initConfig() 16 | if config.RabbitURL != newValue { 17 | t.Errorf("Expected config.RABBIT_URL to be modified. Found=%v, expected=%v", config.RabbitURL, newValue) 18 | } 19 | } 20 | 21 | func TestEnvironmentSettingURL_HTTP(t *testing.T) { 22 | newValue := "http://testURL" 23 | os.Setenv("RABBIT_URL", newValue) 24 | defer os.Unsetenv("RABBIT_URL") 25 | initConfig() 26 | if config.RabbitURL != newValue { 27 | t.Errorf("Expected config.RABBIT_URL to be modified. Found=%v, expected=%v", config.RabbitURL, newValue) 28 | } 29 | } 30 | 31 | func TestEnvironmentSettingUser(t *testing.T) { 32 | newValue := "username" 33 | os.Setenv("RABBIT_USER", newValue) 34 | defer os.Unsetenv("RABBIT_USER") 35 | initConfig() 36 | if config.RabbitUsername != newValue { 37 | t.Errorf("Expected config.RABBIT_USER to be modified. Found=%v, expected=%v", config.RabbitUsername, newValue) 38 | } 39 | } 40 | 41 | func TestEnvironmentSettingPassword(t *testing.T) { 42 | newValue := "password" 43 | os.Setenv("RABBIT_PASSWORD", newValue) 44 | defer os.Unsetenv("RABBIT_PASSWORD") 45 | initConfig() 46 | if config.RabbitPassword != newValue { 47 | t.Errorf("Expected config.RABBIT_PASSWORD to be modified. Found=%v, expected=%v", config.RabbitPassword, newValue) 48 | } 49 | } 50 | 51 | func TestEnvironmentSettingUserFile(t *testing.T) { 52 | fileValue := "./testdata/username_file" 53 | newValue := "username" 54 | os.Setenv("RABBIT_USER_FILE", fileValue) 55 | defer os.Unsetenv("RABBIT_USER_FILE") 56 | initConfig() 57 | if config.RabbitUsername != newValue { 58 | t.Errorf("Expected config.RABBIT_USER to be modified. Found=%v, expected=%v", config.RabbitUsername, newValue) 59 | } 60 | } 61 | 62 | func TestEnvironmentSettingPasswordFile(t *testing.T) { 63 | fileValue := "./testdata/password_file" 64 | newValue := "password" 65 | os.Setenv("RABBIT_PASSWORD_FILE", fileValue) 66 | defer os.Unsetenv("RABBIT_PASSWORD_FILE") 67 | initConfig() 68 | if config.RabbitPassword != newValue { 69 | t.Errorf("Expected config.RABBIT_PASSWORD to be modified. Found=%v, expected=%v", config.RabbitPassword, newValue) 70 | } 71 | } 72 | 73 | func TestEnvironmentSettingPort(t *testing.T) { 74 | newValue := "9091" 75 | os.Setenv("PUBLISH_PORT", newValue) 76 | defer os.Unsetenv("PUBLISH_PORT") 77 | initConfig() 78 | if config.PublishPort != newValue { 79 | t.Errorf("Expected config.PUBLISH_PORT to be modified. Found=%v, expected=%v", config.PublishPort, newValue) 80 | } 81 | } 82 | 83 | func TestEnvironmentSettingAddr(t *testing.T) { 84 | newValue := "localhost" 85 | os.Setenv("PUBLISH_ADDR", newValue) 86 | defer os.Unsetenv("PUBLISH_ADDR") 87 | initConfig() 88 | if config.PublishAddr != newValue { 89 | t.Errorf("Expected config.PUBLISH_ADDR to be modified. Found=%v, expected=%v", config.PublishAddr, newValue) 90 | } 91 | } 92 | 93 | func TestEnvironmentSettingFormat(t *testing.T) { 94 | newValue := "json" 95 | os.Setenv("OUTPUT_FORMAT", newValue) 96 | defer os.Unsetenv("OUTPUT_FORMAT") 97 | initConfig() 98 | if config.OutputFormat != newValue { 99 | t.Errorf("Expected config.OUTPUT_FORMAT to be modified. Found=%v, expected=%v", config.OutputFormat, newValue) 100 | } 101 | } 102 | 103 | func TestConfig_Port(t *testing.T) { 104 | defer func() { 105 | if r := recover(); r == nil { 106 | t.Errorf("initConfig should panic on invalid port config") 107 | } 108 | }() 109 | port := config.PublishPort 110 | os.Setenv("PUBLISH_PORT", "noNumber") 111 | defer os.Unsetenv("PUBLISH_PORT") 112 | initConfig() 113 | if config.PublishPort != port { 114 | t.Errorf("Invalid Portnumber. It should not be set. expected=%v,got=%v", port, config.PublishPort) 115 | } 116 | } 117 | 118 | func TestConfig_Addr(t *testing.T) { 119 | addr := config.PublishAddr 120 | os.Setenv("PUBLISH_ADDR", "") 121 | defer os.Unsetenv("PUBLISH_ADDR") 122 | initConfig() 123 | if config.PublishAddr != addr { 124 | t.Errorf("Invalid Addrress. It should not be set. expected=%v,got=%v", addr, config.PublishAddr) 125 | } 126 | } 127 | 128 | func TestConfig_Http_URL(t *testing.T) { 129 | defer func() { 130 | if r := recover(); r == nil { 131 | t.Errorf("initConfig should panic on invalid url config") 132 | } 133 | }() 134 | url := config.RabbitURL 135 | os.Setenv("RABBIT_URL", "ftp://test") 136 | defer os.Unsetenv("RABBIT_URL") 137 | initConfig() 138 | if config.RabbitURL != url { 139 | t.Errorf("Invalid URL. It should start with http(s)://. expected=%v,got=%v", url, config.RabbitURL) 140 | } 141 | } 142 | 143 | func TestConfig_Capabilities(t *testing.T) { 144 | defer os.Unsetenv("RABBIT_CAPABILITIES") 145 | 146 | os.Unsetenv("RABBIT_CAPABILITIES") 147 | initConfig() 148 | if !config.RabbitCapabilities[rabbitCapBert] { 149 | t.Error("Bert support should be enabled by default") 150 | } 151 | if !config.RabbitCapabilities[rabbitCapNoSort] { 152 | t.Error("No_sort support should be enabled by default") 153 | } 154 | 155 | var needToSupport = []rabbitCapability{"no_sort", "bert"} 156 | for _, cap := range needToSupport { 157 | os.Setenv("RABBIT_CAPABILITIES", "junk_cap, another_with_spaces_around , "+string(cap)+", done") 158 | initConfig() 159 | expected := rabbitCapabilitySet{cap: true} 160 | if !reflect.DeepEqual(config.RabbitCapabilities, expected) { 161 | t.Errorf("Capability '%s' wasn't properly detected from env", cap) 162 | } 163 | } 164 | //disable all capabilities 165 | os.Setenv("RABBIT_CAPABILITIES", " ") 166 | initConfig() 167 | expected := rabbitCapabilitySet{} 168 | if !reflect.DeepEqual(config.RabbitCapabilities, expected) { 169 | t.Errorf("Capabilities '%v' should be empty", config.RabbitCapabilities) 170 | } 171 | } 172 | 173 | func TestConfig_EnabledExporters(t *testing.T) { 174 | enabledExporters := []string{"overview", "connections"} 175 | os.Setenv("RABBIT_EXPORTERS", "overview,connections") 176 | defer os.Unsetenv("RABBIT_EXPORTERS") 177 | initConfig() 178 | if diff := pretty.Compare(config.EnabledExporters, enabledExporters); diff != "" { 179 | t.Errorf("Invalid Exporters list. diff\n%v", diff) 180 | } 181 | } 182 | 183 | func TestConfig_RabbitConnection_Default(t *testing.T) { 184 | defer os.Unsetenv("RABBIT_CONNECTION") 185 | 186 | os.Unsetenv("RABBIT_CONNECTION") 187 | initConfig() 188 | 189 | if config.RabbitConnection != "direct" { 190 | t.Errorf("RabbitConnection unspecified. It should default to direct. expected=%v,got=%v", "direct", config.RabbitConnection) 191 | } 192 | } 193 | 194 | func TestConfig_RabbitConnection_LoadBalaner(t *testing.T) { 195 | newValue := "loadbalancer" 196 | defer os.Unsetenv("RABBIT_CONNECTION") 197 | 198 | os.Setenv("RABBIT_CONNECTION", newValue) 199 | initConfig() 200 | 201 | if config.RabbitConnection != newValue { 202 | t.Errorf("RabbitConnection specified. It should be modified. expected=%v,got=%v", newValue, config.RabbitConnection) 203 | } 204 | } 205 | 206 | func TestConfig_RabbitConnection_Invalid(t *testing.T) { 207 | defer func() { 208 | if r := recover(); r == nil { 209 | t.Errorf("initConfig should panic on invalid rabbit connection config") 210 | } 211 | }() 212 | newValue := "invalid" 213 | defer os.Unsetenv("RABBIT_CONNECTION") 214 | 215 | os.Setenv("RABBIT_CONNECTION", newValue) 216 | initConfig() 217 | } 218 | -------------------------------------------------------------------------------- /curl.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "os" 7 | "time" 8 | ) 9 | 10 | // curl like check for an http url. 11 | // used inside docker image for checking health endpoint 12 | // will os.Exit(0) for http response code 200. Otherwise os.Exit(1) 13 | func curl(url string) { 14 | var client = &http.Client{Timeout: 30 * time.Second} 15 | resp, err := client.Get(url) 16 | if err != nil { 17 | fmt.Printf("Error checking url: %v\n", err) 18 | os.Exit(1) 19 | } 20 | if resp.StatusCode != http.StatusOK { 21 | fmt.Printf("Error checking url: Unexpected http code %v\n", resp.StatusCode) 22 | os.Exit(1) 23 | } 24 | os.Exit(0) 25 | } 26 | -------------------------------------------------------------------------------- /decoder.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //MetricMap maps name to float64 metric 4 | type MetricMap map[string]float64 5 | 6 | //StatsInfo describes one statistic (queue or exchange): its name, vhost it belongs to, and all associated metrics. 7 | type StatsInfo struct { 8 | labels map[string]string 9 | metrics MetricMap 10 | } 11 | 12 | // RabbitReply is an inteface responsible for extracting usable 13 | // information from RabbitMQ HTTP API replies, independent of the 14 | // actual transfer format used. 15 | type RabbitReply interface { 16 | // MakeMap makes a flat map from string to float values from a 17 | // RabbitMQ reply. Processing happens recursively and nesting 18 | // is represented by '.'-separated keys. Entries are added 19 | // only for values that can be reasonably converted to float 20 | // (numbers and booleans). Failure to parse should result in 21 | // an empty result map. 22 | MakeMap() MetricMap 23 | 24 | // MakeStatsInfo parses a list of details about some named 25 | // RabbitMQ objects (i.e. list of queues, exchanges, etc.). 26 | // Failure to parse should result in an empty result list. 27 | MakeStatsInfo([]string) []StatsInfo 28 | 29 | // GetString returns the string value for the given key 30 | // If the key cannot be found the second return is false 31 | GetString(key string) (string, bool) 32 | } 33 | 34 | // MakeReply instantiates the apropriate reply parser for a given 35 | // reply and the content-type header 36 | func MakeReply(contentType string, body []byte) (RabbitReply, error) { 37 | if contentType == "application/bert" { 38 | return makeBERTReply(body) 39 | } 40 | return makeJSONReply(body) 41 | } 42 | -------------------------------------------------------------------------------- /exporter.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | "time" 7 | 8 | "github.com/prometheus/client_golang/prometheus" 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | var ( 13 | exportersMu sync.RWMutex 14 | exporterFactories = make(map[string]func() Exporter) 15 | ) 16 | 17 | type contextValues string 18 | 19 | const ( 20 | endpointScrapeDuration contextValues = "endpointScrapeDuration" 21 | endpointUpMetric contextValues = "endpointUpMetric" 22 | nodeName contextValues = "node" 23 | clusterName contextValues = "cluster" 24 | totalQueues contextValues = "totalQueues" 25 | ) 26 | 27 | //RegisterExporter makes an exporter available by the provided name. 28 | func RegisterExporter(name string, f func() Exporter) { 29 | exportersMu.Lock() 30 | defer exportersMu.Unlock() 31 | if f == nil { 32 | panic("exporterFactory is nil") 33 | } 34 | exporterFactories[name] = f 35 | } 36 | 37 | type exporter struct { 38 | mutex sync.RWMutex 39 | upMetric *prometheus.GaugeVec 40 | endpointUpMetric *prometheus.GaugeVec 41 | endpointScrapeDurationMetric *prometheus.GaugeVec 42 | exporter map[string]Exporter 43 | overviewExporter *exporterOverview 44 | lastScrapeOK bool 45 | } 46 | 47 | //Exporter interface for prometheus metrics. Collect is fetching the data and therefore can return an error 48 | type Exporter interface { 49 | Collect(ctx context.Context, ch chan<- prometheus.Metric) error 50 | Describe(ch chan<- *prometheus.Desc) 51 | } 52 | 53 | func newExporter() *exporter { 54 | enabledExporter := make(map[string]Exporter) 55 | for _, e := range config.EnabledExporters { 56 | if _, ok := exporterFactories[e]; ok { 57 | enabledExporter[e] = exporterFactories[e]() 58 | } 59 | } 60 | 61 | return &exporter{ 62 | upMetric: newGaugeVec("up", "Was the last scrape of rabbitmq successful.", []string{"cluster", "node"}), 63 | endpointUpMetric: newGaugeVec("module_up", "Was the last scrape of rabbitmq successful per module.", []string{"cluster", "node", "module"}), 64 | endpointScrapeDurationMetric: newGaugeVec("module_scrape_duration_seconds", "Duration of the last scrape in seconds", []string{"cluster", "node", "module"}), 65 | exporter: enabledExporter, 66 | overviewExporter: newExporterOverview(), 67 | lastScrapeOK: true, //return true after start. Value will be updated with each scraping 68 | } 69 | } 70 | 71 | func (e *exporter) LastScrapeOK() bool { 72 | e.mutex.Lock() // To protect metrics from concurrent collects. 73 | defer e.mutex.Unlock() 74 | return e.lastScrapeOK 75 | } 76 | 77 | func (e *exporter) Describe(ch chan<- *prometheus.Desc) { 78 | 79 | e.overviewExporter.Describe(ch) 80 | for _, ex := range e.exporter { 81 | ex.Describe(ch) 82 | } 83 | 84 | e.upMetric.Describe(ch) 85 | e.endpointUpMetric.Describe(ch) 86 | e.endpointScrapeDurationMetric.Describe(ch) 87 | BuildInfo.Describe(ch) 88 | } 89 | 90 | func (e *exporter) Collect(ch chan<- prometheus.Metric) { 91 | e.mutex.Lock() // To protect metrics from concurrent collects. 92 | defer e.mutex.Unlock() 93 | 94 | e.upMetric.Reset() 95 | e.endpointUpMetric.Reset() 96 | e.endpointScrapeDurationMetric.Reset() 97 | 98 | start := time.Now() 99 | allUp := true 100 | 101 | if err := e.collectWithDuration(e.overviewExporter, "overview", ch); err != nil { 102 | log.WithError(err).Warn("retrieving overview failed") 103 | allUp = false 104 | } 105 | 106 | for name, ex := range e.exporter { 107 | if err := e.collectWithDuration(ex, name, ch); err != nil { 108 | log.WithError(err).Warn("retrieving " + name + " failed") 109 | allUp = false 110 | } 111 | } 112 | BuildInfo.Collect(ch) 113 | 114 | if allUp { 115 | e.upMetric.WithLabelValues(e.overviewExporter.NodeInfo().ClusterName, e.overviewExporter.NodeInfo().Node).Set(1) 116 | } else { 117 | e.upMetric.WithLabelValues(e.overviewExporter.NodeInfo().ClusterName, e.overviewExporter.NodeInfo().Node).Set(0) 118 | } 119 | e.lastScrapeOK = allUp 120 | 121 | if e.overviewExporter.NodeInfo().ClusterName != "" && e.overviewExporter.NodeInfo().Node != "" { 122 | e.upMetric.DeleteLabelValues("", "") 123 | } 124 | 125 | e.upMetric.Collect(ch) 126 | e.endpointUpMetric.Collect(ch) 127 | e.endpointScrapeDurationMetric.Collect(ch) 128 | log.WithField("duration", time.Since(start)).Info("Metrics updated") 129 | 130 | } 131 | 132 | func (e *exporter) collectWithDuration(ex Exporter, name string, ch chan<- prometheus.Metric) error { 133 | ctx := context.Background() 134 | ctx = context.WithValue(ctx, endpointScrapeDuration, e.endpointScrapeDurationMetric) 135 | ctx = context.WithValue(ctx, endpointUpMetric, e.endpointUpMetric) 136 | //use last know value, could be outdated or empty 137 | ctx = context.WithValue(ctx, nodeName, e.overviewExporter.NodeInfo().Node) 138 | ctx = context.WithValue(ctx, clusterName, e.overviewExporter.NodeInfo().ClusterName) 139 | ctx = context.WithValue(ctx, totalQueues, e.overviewExporter.NodeInfo().TotalQueues) 140 | 141 | startModule := time.Now() 142 | err := ex.Collect(ctx, ch) 143 | 144 | //use current data 145 | node := e.overviewExporter.NodeInfo().Node 146 | cluster := e.overviewExporter.NodeInfo().ClusterName 147 | 148 | if scrapeDuration, ok := ctx.Value(endpointScrapeDuration).(*prometheus.GaugeVec); ok { 149 | if cluster != "" && node != "" { //values are not available until first scrape of overview succeeded 150 | scrapeDuration.WithLabelValues(cluster, node, name).Set(time.Since(startModule).Seconds()) 151 | } 152 | } 153 | if up, ok := ctx.Value(endpointUpMetric).(*prometheus.GaugeVec); ok { 154 | if err != nil { 155 | up.WithLabelValues(cluster, node, name).Set(0) 156 | } else { 157 | up.WithLabelValues(cluster, node, name).Set(1) 158 | } 159 | if node != "" && cluster != "" { 160 | up.DeleteLabelValues("", "", name) 161 | } 162 | } 163 | return err 164 | } 165 | -------------------------------------------------------------------------------- /exporter_aliveness.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/prometheus/client_golang/prometheus" 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | func init() { 11 | RegisterExporter("aliveness", newExporterAliveness) 12 | } 13 | 14 | var ( 15 | alivenessLabels = []string{"vhost"} 16 | 17 | alivenessGaugeVec = map[string]*prometheus.GaugeVec{ 18 | "vhost.aliveness": newGaugeVec("aliveness_test", "vhost aliveness test", alivenessLabels), 19 | } 20 | 21 | rabbitmqAlivenessMetric = prometheus.NewGaugeVec( 22 | prometheus.GaugeOpts{ 23 | Name: "rabbitmq_aliveness_info", 24 | Help: "A metric with value 1 status:ok else 0 labeled by aliveness test status, error, reason", 25 | }, 26 | []string{"status", "error", "reason"}, 27 | ) 28 | ) 29 | 30 | type exporterAliveness struct { 31 | alivenessMetrics map[string]*prometheus.GaugeVec 32 | alivenessInfo AlivenessInfo 33 | } 34 | 35 | type AlivenessInfo struct { 36 | Status string 37 | Error string 38 | Reason string 39 | } 40 | 41 | func newExporterAliveness() Exporter { 42 | alivenessGaugeVecActual := alivenessGaugeVec 43 | 44 | if len(config.ExcludeMetrics) > 0 { 45 | for _, metric := range config.ExcludeMetrics { 46 | if alivenessGaugeVecActual[metric] != nil { 47 | delete(alivenessGaugeVecActual, metric) 48 | } 49 | } 50 | } 51 | 52 | return &exporterAliveness{ 53 | alivenessMetrics: alivenessGaugeVecActual, 54 | alivenessInfo: AlivenessInfo{}, 55 | } 56 | } 57 | 58 | func (e *exporterAliveness) Collect(ctx context.Context, ch chan<- prometheus.Metric) error { 59 | body, contentType, err := apiRequest(config, "aliveness-test") 60 | if err != nil { 61 | return err 62 | } 63 | 64 | reply, err := MakeReply(contentType, body) 65 | if err != nil { 66 | return err 67 | } 68 | 69 | rabbitMqAlivenessData := reply.MakeMap() 70 | 71 | e.alivenessInfo.Status, _ = reply.GetString("status") 72 | e.alivenessInfo.Error, _ = reply.GetString("error") 73 | e.alivenessInfo.Reason, _ = reply.GetString("reason") 74 | 75 | rabbitmqAlivenessMetric.Reset() 76 | var flag float64 = 0 77 | if e.alivenessInfo.Status == "ok" { 78 | flag = 1 79 | } 80 | rabbitmqAlivenessMetric.WithLabelValues(e.alivenessInfo.Status, e.alivenessInfo.Error, e.alivenessInfo.Reason).Set(flag) 81 | 82 | log.WithField("alivenesswData", rabbitMqAlivenessData).Debug("Aliveness data") 83 | for key, gauge := range e.alivenessMetrics { 84 | if value, ok := rabbitMqAlivenessData[key]; ok { 85 | log.WithFields(log.Fields{"key": key, "value": value}).Debug("Set aliveness metric for key") 86 | gauge.WithLabelValues(e.alivenessInfo.Status).Set(value) 87 | } 88 | } 89 | 90 | if ch != nil { 91 | rabbitmqAlivenessMetric.Collect(ch) 92 | for _, gauge := range e.alivenessMetrics { 93 | gauge.Collect(ch) 94 | } 95 | } 96 | return nil 97 | } 98 | 99 | func (e exporterAliveness) Describe(ch chan<- *prometheus.Desc) { 100 | rabbitmqVersionMetric.Describe(ch) 101 | for _, gauge := range e.alivenessMetrics { 102 | gauge.Describe(ch) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /exporter_connections.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/prometheus/client_golang/prometheus" 7 | ) 8 | 9 | func init() { 10 | RegisterExporter("connections", newExporterConnections) 11 | } 12 | 13 | var ( 14 | connectionLabels = []string{"cluster", "vhost", "node", "peer_host", "user", "self"} 15 | connectionLabelsStateMetric = []string{"cluster", "vhost", "node", "peer_host", "user", "state", "self"} 16 | connectionLabelKeys = []string{"vhost", "node", "peer_host", "user", "state", "node"} 17 | 18 | connectionGaugeVec = map[string]*prometheus.GaugeVec{ 19 | "channels": newGaugeVec("connection_channels", "number of channels in use", connectionLabels), 20 | "recv_oct": newGaugeVec("connection_received_bytes", "received bytes", connectionLabels), 21 | "recv_cnt": newGaugeVec("connection_received_packets", "received packets", connectionLabels), 22 | "send_oct": newGaugeVec("connection_send_bytes", "send bytes", connectionLabels), 23 | "send_cnt": newGaugeVec("connection_send_packets", "send packets", connectionLabels), 24 | "send_pend": newGaugeVec("connection_send_pending", "Send queue size", connectionLabels), 25 | } 26 | ) 27 | 28 | type exporterConnections struct { 29 | connectionMetricsG map[string]*prometheus.GaugeVec 30 | stateMetric *prometheus.GaugeVec 31 | } 32 | 33 | func newExporterConnections() Exporter { 34 | connectionGaugeVecActual := connectionGaugeVec 35 | 36 | if len(config.ExcludeMetrics) > 0 { 37 | for _, metric := range config.ExcludeMetrics { 38 | if connectionGaugeVecActual[metric] != nil { 39 | delete(connectionGaugeVecActual, metric) 40 | } 41 | } 42 | } 43 | 44 | return exporterConnections{ 45 | connectionMetricsG: connectionGaugeVecActual, 46 | stateMetric: newGaugeVec("connection_status", "Number of connections in a certain state aggregated per label combination.", connectionLabelsStateMetric), 47 | } 48 | } 49 | 50 | func (e exporterConnections) Collect(ctx context.Context, ch chan<- prometheus.Metric) error { 51 | rabbitConnectionResponses, err := getStatsInfo(config, "connections", connectionLabelKeys) 52 | 53 | if err != nil { 54 | return err 55 | } 56 | for _, gauge := range e.connectionMetricsG { 57 | gauge.Reset() 58 | } 59 | e.stateMetric.Reset() 60 | 61 | selfNode := "" 62 | if n, ok := ctx.Value(nodeName).(string); ok { 63 | selfNode = n 64 | } 65 | cluster := "" 66 | if n, ok := ctx.Value(clusterName).(string); ok { 67 | cluster = n 68 | } 69 | 70 | for key, gauge := range e.connectionMetricsG { 71 | for _, connD := range rabbitConnectionResponses { 72 | if value, ok := connD.metrics[key]; ok { 73 | self := selfLabel(config, connD.labels["node"] == selfNode) 74 | gauge.WithLabelValues(cluster, connD.labels["vhost"], connD.labels["node"], connD.labels["peer_host"], connD.labels["user"], self).Add(value) 75 | } 76 | } 77 | } 78 | 79 | for _, connD := range rabbitConnectionResponses { 80 | self := selfLabel(config, connD.labels["node"] == selfNode) 81 | e.stateMetric.WithLabelValues(cluster, connD.labels["vhost"], connD.labels["node"], connD.labels["peer_host"], connD.labels["user"], connD.labels["state"], self).Add(1) 82 | } 83 | 84 | for _, gauge := range e.connectionMetricsG { 85 | gauge.Collect(ch) 86 | } 87 | e.stateMetric.Collect(ch) 88 | return nil 89 | } 90 | 91 | func (e exporterConnections) Describe(ch chan<- *prometheus.Desc) { 92 | for _, nodeMetric := range e.connectionMetricsG { 93 | nodeMetric.Describe(ch) 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /exporter_exchange.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/prometheus/client_golang/prometheus" 7 | ) 8 | 9 | func init() { 10 | RegisterExporter("exchange", newExporterExchange) 11 | } 12 | 13 | var ( 14 | exchangeLabels = []string{"cluster", "vhost", "exchange"} 15 | exchangeLabelKeys = []string{"vhost", "name"} 16 | 17 | exchangeCounterVec = map[string]*prometheus.Desc{ 18 | "message_stats.publish": newDesc("exchange_messages_published_total", "Count of messages published.", exchangeLabels), 19 | "message_stats.publish_in": newDesc("exchange_messages_published_in_total", "Count of messages published in to an exchange, i.e. not taking account of routing.", exchangeLabels), 20 | "message_stats.publish_out": newDesc("exchange_messages_published_out_total", "Count of messages published out of an exchange, i.e. taking account of routing.", exchangeLabels), 21 | "message_stats.confirm": newDesc("exchange_messages_confirmed_total", "Count of messages confirmed. ", exchangeLabels), 22 | "message_stats.deliver": newDesc("exchange_messages_delivered_total", "Count of messages delivered in acknowledgement mode to consumers.", exchangeLabels), 23 | "message_stats.deliver_no_ack": newDesc("exchange_messages_delivered_noack_total", "Count of messages delivered in no-acknowledgement mode to consumers. ", exchangeLabels), 24 | "message_stats.get": newDesc("exchange_messages_get_total", "Count of messages delivered in acknowledgement mode in response to basic.get.", exchangeLabels), 25 | "message_stats.get_no_ack": newDesc("exchange_messages_get_noack_total", "Count of messages delivered in no-acknowledgement mode in response to basic.get.", exchangeLabels), 26 | "message_stats.ack": newDesc("exchange_messages_ack_total", "Count of messages delivered in acknowledgement mode in response to basic.get.", exchangeLabels), 27 | "message_stats.redeliver": newDesc("exchange_messages_redelivered_total", "Count of subset of messages in deliver_get which had the redelivered flag set.", exchangeLabels), 28 | "message_stats.return_unroutable": newDesc("exchange_messages_returned_total", "Count of messages returned to publisher as unroutable.", exchangeLabels), 29 | } 30 | ) 31 | 32 | type exporterExchange struct { 33 | exchangeMetrics map[string]*prometheus.Desc 34 | } 35 | 36 | func newExporterExchange() Exporter { 37 | exchangeCounterVecActual := exchangeCounterVec 38 | 39 | if len(config.ExcludeMetrics) > 0 { 40 | for _, metric := range config.ExcludeMetrics { 41 | if exchangeCounterVecActual[metric] != nil { 42 | delete(exchangeCounterVecActual, metric) 43 | } 44 | } 45 | } 46 | 47 | return exporterExchange{ 48 | exchangeMetrics: exchangeCounterVecActual, 49 | } 50 | } 51 | 52 | func (e exporterExchange) Collect(ctx context.Context, ch chan<- prometheus.Metric) error { 53 | exchangeData, err := getStatsInfo(config, "exchanges", exchangeLabelKeys) 54 | 55 | if err != nil { 56 | return err 57 | } 58 | cluster := "" 59 | if n, ok := ctx.Value(clusterName).(string); ok { 60 | cluster = n 61 | } 62 | 63 | for key, countvec := range e.exchangeMetrics { 64 | for _, exchange := range exchangeData { 65 | ename := exchange.labels["name"] 66 | vname := exchange.labels["vhost"] 67 | if vhostIncluded := config.IncludeVHost.MatchString(vname); !vhostIncluded { 68 | continue 69 | } 70 | if skipVhost := config.SkipVHost.MatchString(vname); skipVhost { 71 | continue 72 | } 73 | if exchangeIncluded := config.IncludeExchanges.MatchString(ename); !exchangeIncluded { 74 | continue 75 | } 76 | if exchangeSkipped := config.SkipExchanges.MatchString(ename); exchangeSkipped { 77 | continue 78 | } 79 | if value, ok := exchange.metrics[key]; ok { 80 | // log.WithFields(log.Fields{"vhost": exchange.vhost, "exchange": exchange.name, "key": key, "value": value}).Debug("Set exchange metric for key") 81 | ch <- prometheus.MustNewConstMetric(countvec, prometheus.CounterValue, value, cluster, exchange.labels["vhost"], exchange.labels["name"]) 82 | } 83 | } 84 | } 85 | 86 | return nil 87 | } 88 | 89 | func (e exporterExchange) Describe(ch chan<- *prometheus.Desc) { 90 | for _, exchangeMetric := range e.exchangeMetrics { 91 | ch <- exchangeMetric 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /exporter_federation.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/prometheus/client_golang/prometheus" 7 | ) 8 | 9 | func init() { 10 | RegisterExporter("federation", newExporterFederation) 11 | } 12 | 13 | var ( 14 | federationLabels = []string{"cluster", "vhost", "node", "queue", "exchange", "self", "status"} 15 | federationLabelsKeys = []string{"vhost", "status", "node", "queue", "exchange"} 16 | ) 17 | 18 | type exporterFederation struct { 19 | stateMetric *prometheus.GaugeVec 20 | } 21 | 22 | func newExporterFederation() Exporter { 23 | return exporterFederation{ 24 | stateMetric: newGaugeVec("federation_state", "A metric with a value of constant '1' for each federation in a certain state", federationLabels), 25 | } 26 | } 27 | 28 | func (e exporterFederation) Collect(ctx context.Context, ch chan<- prometheus.Metric) error { 29 | e.stateMetric.Reset() 30 | 31 | federationData, err := getStatsInfo(config, "federation-links", federationLabelsKeys) 32 | if err != nil { 33 | return err 34 | } 35 | 36 | cluster := "" 37 | if n, ok := ctx.Value(clusterName).(string); ok { 38 | cluster = n 39 | } 40 | selfNode := "" 41 | if n, ok := ctx.Value(nodeName).(string); ok { 42 | selfNode = n 43 | } 44 | 45 | for _, federation := range federationData { 46 | self := selfLabel(config, federation.labels["node"] == selfNode) 47 | e.stateMetric.WithLabelValues(cluster, federation.labels["vhost"], federation.labels["node"], federation.labels["queue"], federation.labels["exchange"], self, federation.labels["status"]).Set(1) 48 | } 49 | 50 | e.stateMetric.Collect(ch) 51 | return nil 52 | } 53 | 54 | func (e exporterFederation) Describe(ch chan<- *prometheus.Desc) { 55 | e.stateMetric.Describe(ch) 56 | } 57 | -------------------------------------------------------------------------------- /exporter_memory.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/prometheus/client_golang/prometheus" 8 | ) 9 | 10 | func init() { 11 | RegisterExporter("memory", newExporterMemory) 12 | } 13 | 14 | var ( 15 | memoryLabels = []string{"cluster", "node", "self"} 16 | 17 | memoryGaugeVec = map[string]*prometheus.GaugeVec{ 18 | "memory.allocated_unused": newGaugeVec( 19 | "memory_allocated_unused_bytes", 20 | "Memory preallocated by the runtime (VM allocators) but not yet used", 21 | memoryLabels, 22 | ), 23 | "memory.atom": newGaugeVec( 24 | "memory_atom_bytes", 25 | "Memory used by atoms. Should be fairly constant", 26 | memoryLabels, 27 | ), 28 | "memory.binary": newGaugeVec( 29 | "memory_binary_bytes", 30 | "Memory used by shared binary data in the runtime. Most of this memory is message bodies and metadata.)", 31 | memoryLabels, 32 | ), 33 | "memory.code": newGaugeVec( 34 | "memory_code_bytes", 35 | "Memory used by code (bytecode, module metadata). This section is usually fairly constant and relatively small (unless the node is entirely blank and stores no data).", 36 | memoryLabels, 37 | ), 38 | "memory.connection_channels": newGaugeVec( 39 | "memory_connection_channels_bytes", 40 | "Memory used by client connections - channels", 41 | memoryLabels, 42 | ), 43 | "memory.connection_other": newGaugeVec( 44 | "memory_connection_other_bytes", 45 | "Memory used by client connection - other", 46 | memoryLabels, 47 | ), 48 | "memory.connection_readers_bytes": newGaugeVec( 49 | "memory_connection_readers", 50 | "Memory used by processes responsible for connection parser and most of connection state. Most of their memory attributes to TCP buffers", 51 | memoryLabels, 52 | ), 53 | "memory.connection_writers": newGaugeVec( 54 | "memory_connection_writers_bytes", 55 | "Memory used by processes responsible for serialization of outgoing protocol frames and writing to client connections socktes", 56 | memoryLabels, 57 | ), 58 | "memory.metrics": newGaugeVec( 59 | "memory_metrics_bytes", 60 | "Node-local metrics. The more connections, channels, queues are node hosts, the more stats there are to collect and keep", 61 | memoryLabels, 62 | ), 63 | "memory.mgmt_db": newGaugeVec( 64 | "memory_mgmt_db_bytes", 65 | "Management DB ETS tables + processes", 66 | memoryLabels, 67 | ), 68 | "memory.mnesia": newGaugeVec( 69 | "memory_mnesia_bytes", 70 | "Internal database (Mnesia) tables keep an in-memory copy of all its data (even on disc nodes)", 71 | memoryLabels, 72 | ), 73 | "memory.msg_index": newGaugeVec( 74 | "memory_msg_index_bytes", 75 | "Message index ETS + processes", 76 | memoryLabels, 77 | ), 78 | "memory.other_ets": newGaugeVec( 79 | "memory_other_ets_bytes", 80 | "Other in-memory tables besides those belonging to the stats database and internal database tables", 81 | memoryLabels, 82 | ), 83 | "memory.other_proc": newGaugeVec( 84 | "memory_other_proc_bytes", 85 | "Memory used by all other processes that RabbitMQ cannot categorise", 86 | memoryLabels, 87 | ), 88 | "memory.other_system": newGaugeVec( 89 | "memory_other_system_bytes", 90 | "Memory used by all other system that RabbitMQ cannot categorise", 91 | memoryLabels, 92 | ), 93 | "memory.plugins": newGaugeVec( 94 | "memory_plugins_bytes", 95 | "Memory used by plugins (apart from the Erlang client which is counted under Connections, and the management database which is counted separately). ", 96 | memoryLabels, 97 | ), 98 | "memory.queue_procs": newGaugeVec( 99 | "memory_queue_procs_bytes", 100 | "Memory used by class queue masters, queue indices, queue state", 101 | memoryLabels, 102 | ), 103 | "memory.queue_slave_procs": newGaugeVec( 104 | "memory_queue_slave_procs_bytes", 105 | "Memory used by class queue mirrors, queue indices, queue state", 106 | memoryLabels, 107 | ), 108 | "memory.reserved_unallocated": newGaugeVec( 109 | "memory_reserved_unallocated_bytes", 110 | "Memory preallocated/reserved by the kernel but not the runtime", 111 | memoryLabels, 112 | ), 113 | "memory.total.allocated": newGaugeVec( 114 | "memory_total_allocated_bytes", 115 | "Node-local total memory - allocated", 116 | memoryLabels, 117 | ), 118 | "memory.total.rss": newGaugeVec( 119 | "memory_total_rss_bytes", 120 | "Node-local total memory - rss", 121 | memoryLabels, 122 | ), 123 | "memory.total.erlang": newGaugeVec( 124 | "memory_total_erlang_bytes", 125 | "Node-local total memory - erlang", 126 | memoryLabels, 127 | ), 128 | } 129 | ) 130 | 131 | type exporterMemory struct { 132 | memoryMetricsGauge map[string]*prometheus.GaugeVec 133 | } 134 | 135 | func newExporterMemory() Exporter { 136 | memoryGaugeVecActual := memoryGaugeVec 137 | 138 | if len(config.ExcludeMetrics) > 0 { 139 | for _, metric := range config.ExcludeMetrics { 140 | if memoryGaugeVecActual[metric] != nil { 141 | delete(memoryGaugeVecActual, metric) 142 | } 143 | } 144 | } 145 | 146 | return exporterMemory{ 147 | memoryMetricsGauge: memoryGaugeVecActual, 148 | } 149 | } 150 | 151 | func (e exporterMemory) Collect(ctx context.Context, ch chan<- prometheus.Metric) error { 152 | for _, gauge := range e.memoryMetricsGauge { 153 | gauge.Reset() 154 | } 155 | selfNode := "" 156 | if n, ok := ctx.Value(nodeName).(string); ok { 157 | selfNode = n 158 | } 159 | cluster := "" 160 | if n, ok := ctx.Value(clusterName).(string); ok { 161 | cluster = n 162 | } 163 | 164 | nodeData, err := getStatsInfo(config, "nodes", nodeLabelKeys) 165 | if err != nil { 166 | return err 167 | } 168 | 169 | for _, node := range nodeData { 170 | self := selfLabel(config, node.labels["name"] == selfNode) 171 | rabbitMemoryResponses, err := getMetricMap(config, fmt.Sprintf("nodes/%s/memory", node.labels["name"])) 172 | if err != nil { 173 | return err 174 | } 175 | for key, gauge := range e.memoryMetricsGauge { 176 | if value, ok := rabbitMemoryResponses[key]; ok { 177 | gauge.WithLabelValues(cluster, node.labels["name"], self).Set(value) 178 | } 179 | } 180 | } 181 | 182 | for _, gauge := range e.memoryMetricsGauge { 183 | gauge.Collect(ch) 184 | } 185 | return nil 186 | } 187 | 188 | func (e exporterMemory) Describe(ch chan<- *prometheus.Desc) { 189 | for _, gauge := range e.memoryMetricsGauge { 190 | gauge.Describe(ch) 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /exporter_node.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/prometheus/client_golang/prometheus" 7 | ) 8 | 9 | func init() { 10 | RegisterExporter("node", newExporterNode) 11 | } 12 | 13 | var ( 14 | nodeLabels = []string{"cluster", "node", "self"} 15 | nodeLabelKeys = []string{"name"} 16 | 17 | nodeGaugeVec = map[string]*prometheus.GaugeVec{ 18 | "uptime": newGaugeVec("uptime", "Uptime in milliseconds", nodeLabels), 19 | "running": newGaugeVec("running", "number of running nodes", nodeLabels), 20 | "mem_used": newGaugeVec("node_mem_used", "Memory used in bytes", nodeLabels), 21 | "mem_limit": newGaugeVec("node_mem_limit", "Point at which the memory alarm will go off", nodeLabels), 22 | "mem_alarm": newGaugeVec("node_mem_alarm", "Whether the memory alarm has gone off", nodeLabels), 23 | "disk_free": newGaugeVec("node_disk_free", "Disk free space in bytes.", nodeLabels), 24 | "disk_free_alarm": newGaugeVec("node_disk_free_alarm", "Whether the disk alarm has gone off.", nodeLabels), 25 | "disk_free_limit": newGaugeVec("node_disk_free_limit", "Point at which the disk alarm will go off.", nodeLabels), 26 | "fd_used": newGaugeVec("fd_used", "Used File descriptors", nodeLabels), 27 | "fd_total": newGaugeVec("fd_available", "File descriptors available", nodeLabels), 28 | "sockets_used": newGaugeVec("sockets_used", "File descriptors used as sockets.", nodeLabels), 29 | "sockets_total": newGaugeVec("sockets_available", "File descriptors available for use as sockets", nodeLabels), 30 | "partitions_len": newGaugeVec("partitions", "Current Number of network partitions. 0 is ok. If the cluster is splitted the value is at least 2", nodeLabels), 31 | } 32 | ) 33 | 34 | type exporterNode struct { 35 | nodeMetricsGauge map[string]*prometheus.GaugeVec 36 | } 37 | 38 | func newExporterNode() Exporter { 39 | nodeGaugeVecActual := nodeGaugeVec 40 | 41 | if len(config.ExcludeMetrics) > 0 { 42 | for _, metric := range config.ExcludeMetrics { 43 | if nodeGaugeVecActual[metric] != nil { 44 | delete(nodeGaugeVecActual, metric) 45 | } 46 | } 47 | } 48 | 49 | return exporterNode{ 50 | nodeMetricsGauge: nodeGaugeVecActual, 51 | } 52 | } 53 | 54 | func (e exporterNode) Collect(ctx context.Context, ch chan<- prometheus.Metric) error { 55 | selfNode := "" 56 | if n, ok := ctx.Value(nodeName).(string); ok { 57 | selfNode = n 58 | } 59 | cluster := "" 60 | if n, ok := ctx.Value(clusterName).(string); ok { 61 | cluster = n 62 | } 63 | 64 | nodeData, err := getStatsInfo(config, "nodes", nodeLabelKeys) 65 | 66 | if err != nil { 67 | return err 68 | } 69 | 70 | for _, gauge := range e.nodeMetricsGauge { 71 | gauge.Reset() 72 | } 73 | 74 | for key, gauge := range e.nodeMetricsGauge { 75 | for _, node := range nodeData { 76 | if value, ok := node.metrics[key]; ok { 77 | self := selfLabel(config, node.labels["name"] == selfNode) 78 | gauge.WithLabelValues(cluster, node.labels["name"], self).Set(value) 79 | } 80 | } 81 | } 82 | 83 | for _, gauge := range e.nodeMetricsGauge { 84 | gauge.Collect(ch) 85 | } 86 | return nil 87 | } 88 | 89 | func (e exporterNode) Describe(ch chan<- *prometheus.Desc) { 90 | for _, nodeMetric := range e.nodeMetricsGauge { 91 | nodeMetric.Describe(ch) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /exporter_overview.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/prometheus/client_golang/prometheus" 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | func init() { 11 | //RegisterExporter("overview", newExporterOverview) 12 | } 13 | 14 | var ( 15 | overviewLabels = []string{"cluster"} 16 | 17 | overviewMetricDescription = map[string]*prometheus.GaugeVec{ 18 | "object_totals.channels": newGaugeVec("channels", "Number of channels.", overviewLabels), 19 | "object_totals.connections": newGaugeVec("connections", "Number of connections.", overviewLabels), 20 | "object_totals.consumers": newGaugeVec("consumers", "Number of message consumers.", overviewLabels), 21 | "object_totals.queues": newGaugeVec("queues", "Number of queues in use.", overviewLabels), 22 | "object_totals.exchanges": newGaugeVec("exchanges", "Number of exchanges in use.", overviewLabels), 23 | "queue_totals.messages": newGaugeVec("queue_messages_global", "Number ready and unacknowledged messages in cluster.", overviewLabels), 24 | "queue_totals.messages_ready": newGaugeVec("queue_messages_ready_global", "Number of messages ready to be delivered to clients.", overviewLabels), 25 | "queue_totals.messages_unacknowledged": newGaugeVec("queue_messages_unacknowledged_global", "Number of messages delivered to clients but not yet acknowledged.", overviewLabels), 26 | "message_stats.publish_details.rate": newGaugeVec("messages_publish_rate", "Rate at which messages are entering the server.", overviewLabels), 27 | "message_stats.deliver_no_ack_details.rate": newGaugeVec("messages_deliver_no_ack_rate", "Rate at which messages are delivered to consumers that use automatic acknowledgements.", overviewLabels), 28 | "message_stats.deliver_details.rate": newGaugeVec("messages_deliver_rate", "Rate at which messages are delivered to consumers that use manual acknowledgements.", overviewLabels), 29 | } 30 | 31 | rabbitmqVersionMetric = prometheus.NewGaugeVec( 32 | prometheus.GaugeOpts{ 33 | Name: "rabbitmq_version_info", 34 | Help: "A metric with a constant '1' value labeled by rabbitmq version, erlang version, node, cluster.", 35 | }, 36 | []string{"rabbitmq", "erlang", "node", "cluster"}, 37 | ) 38 | ) 39 | 40 | type exporterOverview struct { 41 | overviewMetrics map[string]*prometheus.GaugeVec 42 | nodeInfo NodeInfo 43 | } 44 | 45 | //NodeInfo presents the name and version of fetched rabbitmq 46 | type NodeInfo struct { 47 | Node string 48 | RabbitmqVersion string 49 | ErlangVersion string 50 | ClusterName string 51 | TotalQueues int 52 | } 53 | 54 | func newExporterOverview() *exporterOverview { 55 | overviewMetricDescriptionActual := overviewMetricDescription 56 | 57 | if len(config.ExcludeMetrics) > 0 { 58 | for _, metric := range config.ExcludeMetrics { 59 | if overviewMetricDescriptionActual[metric] != nil { 60 | delete(overviewMetricDescriptionActual, metric) 61 | } 62 | } 63 | } 64 | 65 | return &exporterOverview{ 66 | overviewMetrics: overviewMetricDescriptionActual, 67 | nodeInfo: NodeInfo{}, 68 | } 69 | } 70 | 71 | func (e exporterOverview) NodeInfo() NodeInfo { 72 | return e.nodeInfo 73 | } 74 | 75 | func (e *exporterOverview) Collect(ctx context.Context, ch chan<- prometheus.Metric) error { 76 | body, contentType, err := apiRequest(config, "overview") 77 | if err != nil { 78 | return err 79 | } 80 | 81 | reply, err := MakeReply(contentType, body) 82 | if err != nil { 83 | return err 84 | } 85 | 86 | rabbitMqOverviewData := reply.MakeMap() 87 | 88 | e.nodeInfo.Node, _ = reply.GetString("node") 89 | e.nodeInfo.ErlangVersion, _ = reply.GetString("erlang_version") 90 | e.nodeInfo.RabbitmqVersion, _ = reply.GetString("rabbitmq_version") 91 | e.nodeInfo.ClusterName, _ = reply.GetString("cluster_name") 92 | e.nodeInfo.TotalQueues = (int)(rabbitMqOverviewData["object_totals.queues"]) 93 | 94 | rabbitmqVersionMetric.Reset() 95 | rabbitmqVersionMetric.WithLabelValues(e.nodeInfo.RabbitmqVersion, e.nodeInfo.ErlangVersion, e.nodeInfo.Node, e.nodeInfo.ClusterName).Set(1) 96 | 97 | log.WithField("overviewData", rabbitMqOverviewData).Debug("Overview data") 98 | for key, gauge := range e.overviewMetrics { 99 | if value, ok := rabbitMqOverviewData[key]; ok { 100 | log.WithFields(log.Fields{"key": key, "value": value}).Debug("Set overview metric for key") 101 | gauge.Reset() 102 | gauge.WithLabelValues(e.nodeInfo.ClusterName).Set(value) 103 | } 104 | } 105 | 106 | if ch != nil { 107 | rabbitmqVersionMetric.Collect(ch) 108 | for _, gauge := range e.overviewMetrics { 109 | gauge.Collect(ch) 110 | } 111 | } 112 | return nil 113 | } 114 | 115 | func (e exporterOverview) Describe(ch chan<- *prometheus.Desc) { 116 | rabbitmqVersionMetric.Describe(ch) 117 | 118 | for _, gauge := range e.overviewMetrics { 119 | gauge.Describe(ch) 120 | } 121 | 122 | } 123 | -------------------------------------------------------------------------------- /exporter_queue.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/prometheus/client_golang/prometheus" 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | func init() { 12 | RegisterExporter("queue", newExporterQueue) 13 | } 14 | 15 | var ( 16 | queueLabels = []string{"cluster", "vhost", "queue", "durable", "policy", "self"} 17 | queueLabelKeys = []string{"vhost", "name", "durable", "policy", "state", "node", "idle_since"} 18 | 19 | queueGaugeVec = map[string]*prometheus.GaugeVec{ 20 | "messages_ready": newGaugeVec("queue_messages_ready", "Number of messages ready to be delivered to clients.", queueLabels), 21 | "messages_unacknowledged": newGaugeVec("queue_messages_unacknowledged", "Number of messages delivered to clients but not yet acknowledged.", queueLabels), 22 | "messages": newGaugeVec("queue_messages", "Sum of ready and unacknowledged messages (queue depth).", queueLabels), 23 | "messages_ready_ram": newGaugeVec("queue_messages_ready_ram", "Number of messages from messages_ready which are resident in ram.", queueLabels), 24 | "messages_unacknowledged_ram": newGaugeVec("queue_messages_unacknowledged_ram", "Number of messages from messages_unacknowledged which are resident in ram.", queueLabels), 25 | "messages_ram": newGaugeVec("queue_messages_ram", "Total number of messages which are resident in ram.", queueLabels), 26 | "messages_persistent": newGaugeVec("queue_messages_persistent", "Total number of persistent messages in the queue (will always be 0 for transient queues).", queueLabels), 27 | "message_bytes": newGaugeVec("queue_message_bytes", "Sum of the size of all message bodies in the queue. This does not include the message properties (including headers) or any overhead.", queueLabels), 28 | "message_bytes_ready": newGaugeVec("queue_message_bytes_ready", "Like message_bytes but counting only those messages ready to be delivered to clients.", queueLabels), 29 | "message_bytes_unacknowledged": newGaugeVec("queue_message_bytes_unacknowledged", "Like message_bytes but counting only those messages delivered to clients but not yet acknowledged.", queueLabels), 30 | "message_bytes_ram": newGaugeVec("queue_message_bytes_ram", "Like message_bytes but counting only those messages which are in RAM.", queueLabels), 31 | "message_bytes_persistent": newGaugeVec("queue_message_bytes_persistent", "Like message_bytes but counting only those messages which are persistent.", queueLabels), 32 | "consumers": newGaugeVec("queue_consumers", "Number of consumers.", queueLabels), 33 | "consumer_utilisation": newGaugeVec("queue_consumer_utilisation", "Fraction of the time (between 0.0 and 1.0) that the queue is able to immediately deliver messages to consumers. This can be less than 1.0 if consumers are limited by network congestion or prefetch count.", queueLabels), 34 | "memory": newGaugeVec("queue_memory", "Bytes of memory consumed by the Erlang process associated with the queue, including stack, heap and internal structures.", queueLabels), 35 | "head_message_timestamp": newGaugeVec("queue_head_message_timestamp", "The timestamp property of the first message in the queue, if present. Timestamps of messages only appear when they are in the paged-in state.", queueLabels), //https://github.com/rabbitmq/rabbitmq-server/pull/54 36 | "garbage_collection.min_heap_size": newGaugeVec("queue_gc_min_heap", "Minimum heap size in words", queueLabels), 37 | "garbage_collection.min_bin_vheap_size": newGaugeVec("queue_gc_min_vheap", "Minimum binary virtual heap size in words", queueLabels), 38 | "garbage_collection.fullsweep_after": newGaugeVec("queue_gc_collections_before_fullsweep", "Maximum generational collections before fullsweep", queueLabels), 39 | "slave_nodes_len": newGaugeVec("queue_slaves_nodes_len", "Number of slave nodes attached to the queue", queueLabels), 40 | "synchronised_slave_nodes_len": newGaugeVec("queue_synchronised_slave_nodes_len", "Number of slave nodes in sync to the queue", queueLabels), 41 | "members_len": newGaugeVec("queue_member_nodes_len", "Number of quorum queue member nodes for the queue", queueLabels), 42 | "online_len": newGaugeVec("queue_online_nodes_len", "Number of online members nodes for the queue", queueLabels), 43 | "message_stats.publish_details.rate": newGaugeVec("queue_messages_publish_rate", "Rate at which messages are entering the server.", queueLabels), 44 | "message_stats.deliver_no_ack_details.rate": newGaugeVec("queue_messages_deliver_no_ack_rate", "Rate at which messages are delivered to consumers that use automatic acknowledgements.", queueLabels), 45 | "message_stats.deliver_details.rate": newGaugeVec("queue_messages_deliver_rate", "Rate at which messages are delivered to consumers that use manual acknowledgements.", queueLabels), 46 | } 47 | limitsGaugeVec = map[string]*prometheus.GaugeVec{ 48 | "max-length-bytes": newGaugeVec("queue_max_length_bytes", "Total body size for ready messages a queue can contain before it starts to drop them from its head.", queueLabels), 49 | "max-length": newGaugeVec("queue_max_length", "How many (ready) messages a queue can contain before it starts to drop them from its head.", queueLabels), 50 | } 51 | 52 | queueCounterVec = map[string]*prometheus.Desc{ 53 | "disk_reads": newDesc("queue_disk_reads_total", "Total number of times messages have been read from disk by this queue since it started.", queueLabels), 54 | "disk_writes": newDesc("queue_disk_writes_total", "Total number of times messages have been written to disk by this queue since it started.", queueLabels), 55 | "message_stats.publish": newDesc("queue_messages_published_total", "Count of messages published.", queueLabels), 56 | "message_stats.confirm": newDesc("queue_messages_confirmed_total", "Count of messages confirmed. ", queueLabels), 57 | "message_stats.deliver": newDesc("queue_messages_delivered_total", "Count of messages delivered in acknowledgement mode to consumers.", queueLabels), 58 | "message_stats.deliver_no_ack": newDesc("queue_messages_delivered_noack_total", "Count of messages delivered in no-acknowledgement mode to consumers. ", queueLabels), 59 | "message_stats.get": newDesc("queue_messages_get_total", "Count of messages delivered in acknowledgement mode in response to basic.get.", queueLabels), 60 | "message_stats.get_no_ack": newDesc("queue_messages_get_noack_total", "Count of messages delivered in no-acknowledgement mode in response to basic.get.", queueLabels), 61 | "message_stats.redeliver": newDesc("queue_messages_redelivered_total", "Count of subset of messages in deliver_get which had the redelivered flag set.", queueLabels), 62 | "message_stats.return": newDesc("queue_messages_returned_total", "Count of messages returned to publisher as unroutable.", queueLabels), 63 | "message_stats.ack": newDesc("queue_messages_ack_total", "Count of messages delivered in acknowledgement mode in response to basic.get.", queueLabels), 64 | "reductions": newDesc("queue_reductions_total", "Count of reductions which take place on this process. .", queueLabels), 65 | "garbage_collection.minor_gcs": newDesc("queue_gc_minor_collections_total", "Number of minor GCs", queueLabels), 66 | } 67 | ) 68 | 69 | type exporterQueue struct { 70 | limitsGauge map[string]*prometheus.GaugeVec 71 | queueMetricsGauge map[string]*prometheus.GaugeVec 72 | queueMetricsCounter map[string]*prometheus.Desc 73 | stateMetric *prometheus.GaugeVec 74 | idleSinceMetric *prometheus.GaugeVec 75 | } 76 | 77 | func newExporterQueue() Exporter { 78 | queueGaugeVecActual := queueGaugeVec 79 | queueCounterVecActual := queueCounterVec 80 | litmitsGaugeVecActual := limitsGaugeVec 81 | 82 | if len(config.ExcludeMetrics) > 0 { 83 | for _, metric := range config.ExcludeMetrics { 84 | if queueGaugeVecActual[metric] != nil { 85 | delete(queueGaugeVecActual, metric) 86 | } 87 | if queueCounterVecActual[metric] != nil { 88 | delete(queueCounterVecActual, metric) 89 | } 90 | if litmitsGaugeVecActual[metric] != nil { 91 | delete(litmitsGaugeVecActual, metric) 92 | } 93 | } 94 | } 95 | 96 | return exporterQueue{ 97 | limitsGauge: litmitsGaugeVecActual, 98 | queueMetricsGauge: queueGaugeVecActual, 99 | queueMetricsCounter: queueCounterVecActual, 100 | stateMetric: newGaugeVec("queue_state", "A metric with a value of constant '1' if the queue is in a certain state", append(queueLabels, "state")), 101 | idleSinceMetric: newGaugeVec("queue_idle_since_seconds", "starttime where the queue switched to idle state; in seconds since epoch (1970).", queueLabels), 102 | } 103 | } 104 | 105 | func collectLowerMetric(metricA, metricB string, stats StatsInfo) float64 { 106 | mA, okA := stats.metrics[metricA] 107 | mB, okB := stats.metrics[metricB] 108 | 109 | if okA && okB { 110 | if mA < mB { 111 | return mA 112 | } else { 113 | return mB 114 | } 115 | } 116 | if okA { 117 | return mA 118 | } 119 | if okB { 120 | return mB 121 | } 122 | return -1.0 123 | } 124 | 125 | func (e exporterQueue) Collect(ctx context.Context, ch chan<- prometheus.Metric) error { 126 | for _, gaugevec := range e.queueMetricsGauge { 127 | gaugevec.Reset() 128 | } 129 | for _, m := range e.limitsGauge { 130 | m.Reset() 131 | } 132 | e.stateMetric.Reset() 133 | e.idleSinceMetric.Reset() 134 | 135 | if config.MaxQueues > 0 { 136 | // Get overview info to check total queues 137 | totalQueues, ok := ctx.Value(totalQueues).(int) 138 | if !ok { 139 | return errors.New("total Queue counter missing") 140 | } 141 | 142 | if totalQueues > config.MaxQueues { 143 | log.WithFields(log.Fields{ 144 | "MaxQueues": config.MaxQueues, 145 | "TotalQueues": totalQueues, 146 | }).Debug("MaxQueues exceeded.") 147 | return nil 148 | } 149 | } 150 | 151 | selfNode := "" 152 | if n, ok := ctx.Value(nodeName).(string); ok { 153 | selfNode = n 154 | } 155 | cluster := "" 156 | if n, ok := ctx.Value(clusterName).(string); ok { 157 | cluster = n 158 | } 159 | 160 | rabbitMqQueueData, err := getStatsInfo(config, "queues", queueLabelKeys) 161 | if err != nil { 162 | return err 163 | } 164 | 165 | log.WithField("queueData", rabbitMqQueueData).Debug("Queue data") 166 | for _, queue := range rabbitMqQueueData { 167 | qname := queue.labels["name"] 168 | vname := queue.labels["vhost"] 169 | if vhostIncluded := config.IncludeVHost.MatchString(vname); !vhostIncluded { 170 | continue 171 | } 172 | if skipVhost := config.SkipVHost.MatchString(vname); skipVhost { 173 | continue 174 | } 175 | if queueIncluded := config.IncludeQueues.MatchString(qname); !queueIncluded { 176 | continue 177 | } 178 | if queueSkipped := config.SkipQueues.MatchString(qname); queueSkipped { 179 | continue 180 | } 181 | 182 | self := selfLabel(config, queue.labels["node"] == selfNode) 183 | labelValues := []string{cluster, queue.labels["vhost"], queue.labels["name"], queue.labels["durable"], queue.labels["policy"], self} 184 | 185 | for key, gaugevec := range e.queueMetricsGauge { 186 | if value, ok := queue.metrics[key]; ok { 187 | // log.WithFields(log.Fields{"vhost": queue.labels["vhost"], "queue": queue.labels["name"], "key": key, "value": value}).Info("Set queue metric for key") 188 | gaugevec.WithLabelValues(labelValues...).Set(value) 189 | } 190 | } 191 | 192 | for key, countvec := range e.queueMetricsCounter { 193 | if value, ok := queue.metrics[key]; ok { 194 | ch <- prometheus.MustNewConstMetric(countvec, prometheus.CounterValue, value, labelValues...) 195 | } else { 196 | ch <- prometheus.MustNewConstMetric(countvec, prometheus.CounterValue, 0, labelValues...) 197 | } 198 | } 199 | 200 | state := queue.labels["state"] 201 | idleSince, exists := queue.labels["idle_since"] 202 | if exists && idleSince != "" { 203 | 204 | if t, err := parseTime(idleSince); err == nil { 205 | unixSeconds := float64(t.UnixNano()) / 1e9 206 | 207 | if state == "running" { //replace running state with idle if idle_since time is provided. Other states (flow, etc.) are not replaced 208 | state = "idle" 209 | } 210 | e.idleSinceMetric.WithLabelValues(labelValues...).Set(unixSeconds) 211 | } else { 212 | log.WithError(err).WithField("idle_since", idleSince).Warn("error parsing idle since time") 213 | } 214 | } 215 | e.stateMetric.WithLabelValues(append(labelValues, state)...).Set(1) 216 | 217 | if _, ok := limitsGaugeVec["max-length"]; ok { 218 | if f := collectLowerMetric("arguments.x-max-length", "effective_policy_definition.max-length", queue); f >= 0 { 219 | limitsGaugeVec["max-length"].WithLabelValues(labelValues...).Set(f) 220 | } 221 | } 222 | 223 | if _, ok := limitsGaugeVec["max-length-bytes"]; ok { 224 | if f := collectLowerMetric("arguments.x-max-length-bytes", "effective_policy_definition.max-length-bytes", queue); f >= 0 { 225 | limitsGaugeVec["max-length-bytes"].WithLabelValues(labelValues...).Set(f) 226 | } 227 | } 228 | 229 | } 230 | 231 | for _, metric := range e.limitsGauge { 232 | metric.Collect(ch) 233 | } 234 | for _, gaugevec := range e.queueMetricsGauge { 235 | gaugevec.Collect(ch) 236 | } 237 | e.stateMetric.Collect(ch) 238 | e.idleSinceMetric.Collect(ch) 239 | 240 | return nil 241 | } 242 | 243 | func (e exporterQueue) Describe(ch chan<- *prometheus.Desc) { 244 | for _, metric := range e.limitsGauge { 245 | metric.Describe(ch) 246 | } 247 | for _, gaugevec := range e.queueMetricsGauge { 248 | gaugevec.Describe(ch) 249 | } 250 | e.stateMetric.Describe(ch) 251 | e.idleSinceMetric.Describe(ch) 252 | for _, countervec := range e.queueMetricsCounter { 253 | ch <- countervec 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /exporter_shovel.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/prometheus/client_golang/prometheus" 7 | ) 8 | 9 | func init() { 10 | RegisterExporter("shovel", newExporterShovel) 11 | } 12 | 13 | var ( 14 | //shovelLabels are the labels for all shovel mertrics 15 | shovelLabels = []string{"cluster", "vhost", "shovel", "type", "self", "state"} 16 | //shovelLabelKeys are the important keys to be extracted from json 17 | shovelLabelKeys = []string{"vhost", "name", "type", "node", "state"} 18 | ) 19 | 20 | type exporterShovel struct { 21 | stateMetric *prometheus.GaugeVec 22 | } 23 | 24 | func newExporterShovel() Exporter { 25 | return exporterShovel{ 26 | stateMetric: newGaugeVec("shovel_state", "A metric with a value of constant '1' for each shovel in a certain state", shovelLabels), 27 | } 28 | } 29 | 30 | func (e exporterShovel) Collect(ctx context.Context, ch chan<- prometheus.Metric) error { 31 | e.stateMetric.Reset() 32 | 33 | shovelData, err := getStatsInfo(config, "shovels", shovelLabelKeys) 34 | if err != nil { 35 | return err 36 | } 37 | 38 | cluster := "" 39 | if n, ok := ctx.Value(clusterName).(string); ok { 40 | cluster = n 41 | } 42 | selfNode := "" 43 | if n, ok := ctx.Value(nodeName).(string); ok { 44 | selfNode = n 45 | } 46 | 47 | for _, shovel := range shovelData { 48 | self := selfLabel(config, shovel.labels["node"] == selfNode) 49 | e.stateMetric.WithLabelValues(cluster, shovel.labels["vhost"], shovel.labels["name"], shovel.labels["type"], self, shovel.labels["state"]).Set(1) 50 | } 51 | 52 | e.stateMetric.Collect(ch) 53 | return nil 54 | } 55 | 56 | func (e exporterShovel) Describe(ch chan<- *prometheus.Desc) { 57 | e.stateMetric.Describe(ch) 58 | } 59 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kbudde/rabbitmq_exporter 2 | 3 | require ( 4 | github.com/kbudde/gobert v0.0.0-20220512191144-9767639f5c50 5 | github.com/kylelemons/godebug v1.1.0 6 | github.com/ory/dockertest/v3 v3.11.0 7 | github.com/prometheus/client_golang v1.19.0 8 | github.com/sirupsen/logrus v1.9.3 9 | github.com/streadway/amqp v1.1.0 10 | github.com/tkanos/gonfig v0.0.0-20210106201359-53e13348de2f 11 | golang.org/x/sys v0.21.0 12 | ) 13 | 14 | require ( 15 | dario.cat/mergo v1.0.0 // indirect 16 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect 17 | github.com/Microsoft/go-winio v0.6.2 // indirect 18 | github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect 19 | github.com/beorn7/perks v1.0.1 // indirect 20 | github.com/cenkalti/backoff/v4 v4.3.0 // indirect 21 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 22 | github.com/containerd/continuity v0.4.3 // indirect 23 | github.com/docker/cli v26.1.4+incompatible // indirect 24 | github.com/docker/docker v27.1.1+incompatible // indirect 25 | github.com/docker/go-connections v0.5.0 // indirect 26 | github.com/docker/go-units v0.5.0 // indirect 27 | github.com/ghodss/yaml v1.0.0 // indirect 28 | github.com/gogo/protobuf v1.3.2 // indirect 29 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect 30 | github.com/kr/text v0.2.0 // indirect 31 | github.com/lib/pq v1.10.9 // indirect 32 | github.com/mitchellh/mapstructure v1.5.0 // indirect 33 | github.com/moby/docker-image-spec v1.3.1 // indirect 34 | github.com/moby/term v0.5.0 // indirect 35 | github.com/opencontainers/go-digest v1.0.0 // indirect 36 | github.com/opencontainers/image-spec v1.1.0 // indirect 37 | github.com/opencontainers/runc v1.1.13 // indirect 38 | github.com/pkg/errors v0.9.1 // indirect 39 | github.com/prometheus/client_model v0.6.0 // indirect 40 | github.com/prometheus/common v0.50.0 // indirect 41 | github.com/prometheus/procfs v0.13.0 // indirect 42 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect 43 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect 44 | github.com/xeipuuv/gojsonschema v1.2.0 // indirect 45 | golang.org/x/mod v0.16.0 // indirect 46 | golang.org/x/tools v0.19.0 // indirect 47 | google.golang.org/protobuf v1.33.0 // indirect 48 | gopkg.in/yaml.v2 v2.4.0 // indirect 49 | ) 50 | 51 | go 1.22 52 | 53 | toolchain go1.22.6 54 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= 2 | dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= 3 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= 4 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= 5 | github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= 6 | github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= 7 | github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= 8 | github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 9 | github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= 10 | github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= 11 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 12 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 13 | github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= 14 | github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 15 | github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= 16 | github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 17 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 18 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 19 | github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8= 20 | github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= 21 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 22 | github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= 23 | github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 24 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 25 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 26 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 27 | github.com/docker/cli v25.0.4+incompatible h1:DatRkJ+nrFoYL2HZUzjM5Z5sAmcA5XGp+AW0oEw2+cA= 28 | github.com/docker/cli v25.0.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= 29 | github.com/docker/cli v26.1.4+incompatible h1:I8PHdc0MtxEADqYJZvhBrW9bo8gawKwwenxRM7/rLu8= 30 | github.com/docker/cli v26.1.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= 31 | github.com/docker/docker v25.0.4+incompatible h1:XITZTrq+52tZyZxUOtFIahUf3aH367FLxJzt9vZeAF8= 32 | github.com/docker/docker v25.0.4+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 33 | github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY= 34 | github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 35 | github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= 36 | github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= 37 | github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= 38 | github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 39 | github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= 40 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 41 | github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= 42 | github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 43 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 44 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 45 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 46 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 47 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= 48 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= 49 | github.com/kbudde/gobert v0.0.0-20220512191144-9767639f5c50 h1:zaDz2YfwvX72CdAqsVfv2mKr5EQ3bE7147RIHhoL9D8= 50 | github.com/kbudde/gobert v0.0.0-20220512191144-9767639f5c50/go.mod h1:wxNjohCuAvLH8uXUilrMINgVaz2EOfT9x6KlBPIE5dk= 51 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 52 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 53 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 54 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 55 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 56 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 57 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 58 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 59 | github.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk= 60 | github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 61 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 62 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 63 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 64 | github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= 65 | github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= 66 | github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= 67 | github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= 68 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 69 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 70 | github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= 71 | github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= 72 | github.com/opencontainers/runc v1.1.12 h1:BOIssBaW1La0/qbNZHXOOa71dZfZEQOzW7dqQf3phss= 73 | github.com/opencontainers/runc v1.1.12/go.mod h1:S+lQwSfncpBha7XTy/5lBwWgm5+y5Ma/O44Ekby9FK8= 74 | github.com/opencontainers/runc v1.1.13 h1:98S2srgG9vw0zWcDpFMn5TRrh8kLxa/5OFUstuUhmRs= 75 | github.com/opencontainers/runc v1.1.13/go.mod h1:R016aXacfp/gwQBYw2FDGa9m+n6atbLWrYY8hNMT/sA= 76 | github.com/ory/dockertest/v3 v3.10.0 h1:4K3z2VMe8Woe++invjaTB7VRyQXQy5UY+loujO4aNE4= 77 | github.com/ory/dockertest/v3 v3.10.0/go.mod h1:nr57ZbRWMqfsdGdFNLHz5jjNdDb7VVFnzAeW1n5N1Lg= 78 | github.com/ory/dockertest/v3 v3.11.0 h1:OiHcxKAvSDUwsEVh2BjxQQc/5EHz9n0va9awCtNGuyA= 79 | github.com/ory/dockertest/v3 v3.11.0/go.mod h1:VIPxS1gwT9NpPOrfD3rACs8Y9Z7yhzO4SB194iUDnUI= 80 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 81 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 82 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 83 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 84 | github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= 85 | github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k= 86 | github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos= 87 | github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8= 88 | github.com/prometheus/common v0.50.0 h1:YSZE6aa9+luNa2da6/Tik0q0A5AbR+U003TItK57CPQ= 89 | github.com/prometheus/common v0.50.0/go.mod h1:wHFBCEVWVmHMUpg7pYcOm2QUR/ocQdYSJVQJKnHc3xQ= 90 | github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o= 91 | github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g= 92 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 93 | github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 94 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 95 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 96 | github.com/streadway/amqp v1.1.0 h1:py12iX8XSyI7aN/3dUT8DFIDJazNJsVJdxNVEpnQTZM= 97 | github.com/streadway/amqp v1.1.0/go.mod h1:WYSrTEYHOXHd0nwFeUXAe2G2hRnQT+deZJJf88uS9Bg= 98 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 99 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 100 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 101 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 102 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 103 | github.com/tkanos/gonfig v0.0.0-20210106201359-53e13348de2f h1:xDFq4NVQD34ekH5UsedBSgfxsBuPU2aZf7v4t0tH2jY= 104 | github.com/tkanos/gonfig v0.0.0-20210106201359-53e13348de2f/go.mod h1:DaZPBuToMc2eezA9R9nDAnmS2RMwL7yEa5YD36ESQdI= 105 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= 106 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= 107 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= 108 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= 109 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= 110 | github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= 111 | github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= 112 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 113 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 114 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 115 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 116 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 117 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 118 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 119 | golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= 120 | golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 121 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 122 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 123 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 124 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 125 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 126 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 127 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 128 | golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= 129 | golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 130 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 131 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 132 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 133 | golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 134 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 135 | golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= 136 | golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 137 | golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= 138 | golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 139 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 140 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 141 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 142 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 143 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 144 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 145 | golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw= 146 | golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc= 147 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 148 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 149 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 150 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 151 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 152 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 153 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 154 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 155 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 156 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 157 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 158 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 159 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 160 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 161 | gotest.tools/v3 v3.3.0 h1:MfDY1b1/0xN1CyMlQDac0ziEy9zJQd9CXBRRDHw2jJo= 162 | gotest.tools/v3 v3.3.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A= 163 | -------------------------------------------------------------------------------- /integration_test.go: -------------------------------------------------------------------------------- 1 | //go:build integration 2 | // +build integration 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "testing" 10 | "time" 11 | 12 | "regexp" 13 | 14 | "strings" 15 | 16 | "github.com/kbudde/rabbitmq_exporter/testenv" 17 | ) 18 | 19 | func TestQueueCount(t *testing.T) { 20 | var env testenv.TestEnvironment 21 | var exporterURL string 22 | var rabbitManagementURL string 23 | 24 | t.Run("Setup test environment", func(t *testing.T) { 25 | env = testenv.NewEnvironment(t, testenv.RabbitMQ3Latest) 26 | }) 27 | 28 | defer env.CleanUp() // do not panic or exit fatally or the container will stay up 29 | 30 | t.Run("Preparation", func(t *testing.T) { 31 | exporterURL = fmt.Sprintf("http://localhost:%s/metrics", defaultConfig.PublishPort) 32 | rabbitManagementURL = env.ManagementURL() 33 | os.Setenv("RABBIT_URL", rabbitManagementURL) 34 | os.Setenv("RABBIT_CAPABILITIES", "bert") // no_sort not supported in rabbitmq 3.13+ 35 | defer os.Unsetenv("RABBIT_URL") 36 | defer os.Unsetenv("RABBIT_CAPABILITIES") 37 | 38 | go main() 39 | time.Sleep(2 * time.Second) 40 | }) 41 | 42 | t.Run("Ensure there are no queues", func(t *testing.T) { 43 | body := testenv.GetOrDie(exporterURL, 5*time.Second) 44 | 45 | r := regexp.MustCompile(`rabbitmq_queues{cluster="rabbit@localtest"} 0`) 46 | if s := r.FindString(body); s == "" { 47 | t.Fatalf("QueueCount 0 not found in body: %v", body) 48 | } 49 | }) 50 | 51 | t.Run("Add one queue and check again", func(t *testing.T) { 52 | env.Rabbit.DeclareQueue("QueueForCheckCount", false) 53 | 54 | body := testenv.GetOrDie(exporterURL, 5*time.Second) 55 | 56 | r := regexp.MustCompile(`rabbitmq_queues{cluster="rabbit@localtest"} 1`) 57 | if s := r.FindString(body); s == "" { 58 | // t.Logf("body: %s", body) 59 | t.Fatalf("QueueCount 1 not found ") 60 | } 61 | }) 62 | 63 | t.Run("Add message with timestamp", func(t *testing.T) { 64 | queue := "timestamp" 65 | env.Rabbit.DeclareQueue(queue, true) 66 | timestamp := time.Date(2017, 11, 27, 8, 25, 23, 0, time.UTC) 67 | env.Rabbit.SendMessageToQ("Test timestamp", queue, ×tamp) 68 | time.Sleep(10 * time.Second) // give rabbitmq management plugin a bit of time 69 | // log.Println(testenv.GetOrDie(env.ManagementURL()+"/api/queues", 5*time.Second)) 70 | body := testenv.GetOrDie(exporterURL, 5*time.Second) 71 | 72 | search := fmt.Sprintf(`rabbitmq_queue_head_message_timestamp{cluster="rabbit@localtest",durable="true",policy="",queue="%s",self="1",vhost="/"} %1.9e`, queue, float64(timestamp.Unix())) 73 | i := strings.Index(body, search) 74 | 75 | if i == -1 { 76 | t.Log(body, search) 77 | t.Fatalf("Timestamp not found") 78 | } 79 | }) 80 | 81 | t.Run("Queue durable true", func(t *testing.T) { 82 | queue := "dur-true" 83 | env.Rabbit.DeclareQueue("dur-true", true) 84 | 85 | time.Sleep(5 * time.Second) // give rabbitmq management plugin a bit of time 86 | body := testenv.GetOrDie(exporterURL, 5*time.Second) 87 | 88 | search := fmt.Sprintf(`rabbitmq_queue_messages{cluster="rabbit@localtest",durable="true",policy="",queue="%s",self="1",vhost="/"} 0`, queue) 89 | i := strings.Index(body, search) 90 | 91 | if i == -1 { 92 | t.Log(body, search) 93 | t.Fatalf("Queue dur-true not found") 94 | } 95 | }) 96 | 97 | t.Run("Queue durable false", func(t *testing.T) { 98 | queue := "dur-false" 99 | env.Rabbit.DeclareQueue("dur-false", false) 100 | 101 | time.Sleep(5 * time.Second) // give rabbitmq management plugin a bit of time 102 | 103 | body := testenv.GetOrDie(exporterURL, 5*time.Second) 104 | 105 | search := fmt.Sprintf(`rabbitmq_queue_messages{cluster="rabbit@localtest",durable="false",policy="",queue="%s",self="1",vhost="/"} 0`, queue) 106 | i := strings.Index(body, search) 107 | 108 | if i == -1 { 109 | t.Log(body, search) 110 | t.Fatalf("Queue dur-false not found") 111 | } 112 | }) 113 | 114 | t.Run("Queue policy", func(t *testing.T) { 115 | queue := "QueueWithPol" 116 | env.Rabbit.DeclareQueue(queue, false) 117 | 118 | policy := "QueuePolicy" 119 | env.MustSetPolicy(policy, "^.*$") 120 | 121 | time.Sleep(10 * time.Second) // give rabbitmq management plugin a bit of time 122 | body := testenv.GetOrDie(exporterURL, 5*time.Second) 123 | 124 | search := fmt.Sprintf(`rabbitmq_queue_messages{cluster="rabbit@localtest",durable="false",policy="%s",queue="%s",self="1",vhost="/"} 0`, policy, queue) 125 | i := strings.Index(body, search) 126 | if i == -1 { 127 | // t.Log(env.ManagementURL()) 128 | // t.Log(testenv.GetOrDie(env.ManagementURL()+"/api/queues", 5*time.Second)) 129 | t.Log(body, search) 130 | t.Fatalf("Queue with policy not found") 131 | } 132 | 133 | }) 134 | } 135 | -------------------------------------------------------------------------------- /jsonmap.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "strconv" 8 | "time" 9 | 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | type rabbitJSONReply struct { 14 | body []byte 15 | keys map[string]interface{} 16 | } 17 | 18 | func makeJSONReply(body []byte) (RabbitReply, error) { 19 | return &rabbitJSONReply{body, nil}, nil 20 | } 21 | 22 | //MakeStatsInfo creates a slice of StatsInfo from json input. Only keys with float values are mapped into `metrics`. 23 | func (rep *rabbitJSONReply) MakeStatsInfo(labels []string) []StatsInfo { 24 | var statistics []StatsInfo 25 | var jsonArr []map[string]interface{} 26 | decoder := json.NewDecoder(bytes.NewBuffer(rep.body)) 27 | if decoder == nil { 28 | log.Error("JSON decoder not iniatilized") 29 | return make([]StatsInfo, 0) 30 | } 31 | 32 | if err := decoder.Decode(&jsonArr); err != nil { 33 | log.WithField("error", err).Error("Error while decoding json") 34 | return make([]StatsInfo, 0) 35 | } 36 | 37 | for _, el := range jsonArr { 38 | field := "" 39 | if _, fieldName := el["name"]; fieldName { 40 | field = "name" 41 | } 42 | if _, fieldID := el["id"]; fieldID { 43 | field = "id" 44 | } 45 | if field != "" { 46 | log.WithFields(log.Fields{"element": el, "vhost": el["vhost"], field: el[field]}).Debug("Iterate over array") 47 | statsinfo := StatsInfo{} 48 | statsinfo.labels = make(map[string]string) 49 | 50 | for _, label := range labels { 51 | statsinfo.labels[label] = "" 52 | if tmp, ok := el[label]; ok { 53 | if v, ok := tmp.(string); ok { 54 | statsinfo.labels[label] = v 55 | } else if v, ok := tmp.(bool); ok { 56 | statsinfo.labels[label] = strconv.FormatBool(v) 57 | } 58 | } 59 | } 60 | 61 | statsinfo.metrics = make(MetricMap) 62 | addFields(&statsinfo.metrics, "", el) 63 | statistics = append(statistics, statsinfo) 64 | } 65 | } 66 | 67 | return statistics 68 | } 69 | 70 | //MakeMap creates a map from json input. Only keys with float values are mapped. 71 | func (rep *rabbitJSONReply) MakeMap() MetricMap { 72 | flMap := make(MetricMap) 73 | var output map[string]interface{} 74 | decoder := json.NewDecoder(bytes.NewBuffer(rep.body)) 75 | if decoder == nil { 76 | log.Error("JSON decoder not iniatilized") 77 | return flMap 78 | } 79 | 80 | if err := decoder.Decode(&output); err != nil { 81 | log.WithField("error", err).Error("Error while decoding json") 82 | return flMap 83 | } 84 | 85 | addFields(&flMap, "", output) 86 | 87 | return flMap 88 | } 89 | 90 | func addFields(toMap *MetricMap, basename string, source map[string]interface{}) { 91 | prefix := "" 92 | if basename != "" { 93 | prefix = basename + "." 94 | } 95 | for k, v := range source { 96 | switch value := v.(type) { 97 | case float64: 98 | (*toMap)[prefix+k] = value 99 | case []interface{}: 100 | (*toMap)[prefix+k+"_len"] = float64(len(value)) 101 | case map[string]interface{}: 102 | addFields(toMap, prefix+k, value) 103 | case bool: 104 | if value { 105 | (*toMap)[prefix+k] = 1 106 | } else { 107 | (*toMap)[prefix+k] = 0 108 | } 109 | } 110 | } 111 | } 112 | 113 | func (rep *rabbitJSONReply) GetString(key string) (string, bool) { 114 | if rep.keys == nil { 115 | keys := make(map[string]interface{}) 116 | decoder := json.NewDecoder(bytes.NewBuffer(rep.body)) 117 | if decoder == nil { 118 | log.Error("JSON decoder not iniatilized") 119 | return "", false 120 | } 121 | err := decoder.Decode(&keys) 122 | if err != nil { 123 | return "", false 124 | } 125 | rep.keys = keys 126 | } 127 | val, ok := rep.keys[key] 128 | if !ok { 129 | return "", false 130 | } 131 | value, ok := val.(string) 132 | return value, ok 133 | } 134 | 135 | func parseTime(s string) (time.Time, error) { 136 | t, err := time.Parse("2006-01-02 15:04:05", s) 137 | if err == nil { 138 | return t, nil 139 | } 140 | t, err = time.Parse("2006-01-02T15:04:05.999-07:00", s) 141 | if err == nil { 142 | return t, nil 143 | } 144 | return t, fmt.Errorf("time format does not match expectations") 145 | 146 | } 147 | -------------------------------------------------------------------------------- /jsonmap_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestWithInvalidJSON(t *testing.T) { 8 | invalidJSONReply, _ := makeJSONReply([]byte("I'm no json")) 9 | 10 | if mm := invalidJSONReply.MakeMap(); mm == nil { 11 | t.Errorf("Json is invalid. Empty map should be returned. Value: %v", mm) 12 | } 13 | if qi := invalidJSONReply.MakeStatsInfo(queueLabelKeys); qi == nil { 14 | t.Errorf("Json is invalid. Empty map should be returned. Value: %v", qi) 15 | } 16 | } 17 | 18 | func checkMap(flMap map[string]float64, t *testing.T, addValue float64) { 19 | if flMap == nil { 20 | t.Error("Map should not be nil") 21 | } 22 | 23 | if v := flMap["FloatKey"]; v != addValue+4 { 24 | t.Errorf("Map should contain FloatKey but found '%v'", v) 25 | } 26 | 27 | if v := flMap["nes.ted"]; v != addValue+5 { 28 | t.Errorf("Map should contain nes.ted key but found '%v'", v) 29 | } 30 | 31 | if v, ok := flMap["st"]; ok { 32 | t.Errorf("key 'st' should not be included in map as it contains a string. Value: %v", v) 33 | } 34 | } 35 | 36 | func TestMakeMap(t *testing.T) { 37 | reply, _ := makeJSONReply([]byte(`{"FloatKey":4, "st":"string","nes":{"ted":5}}`)) 38 | 39 | flMap := reply.MakeMap() 40 | checkMap(flMap, t, 0) 41 | } 42 | 43 | func TestMakeStatsInfo(t *testing.T) { 44 | reply, _ := makeJSONReply([]byte(`[{"name":"q1", "FloatKey":14,"nes":{"ted":15}},{"name":"q2", "vhost":"foo", "FloatKey":24,"nes":{"ted":25}}]`)) 45 | 46 | qinfo := reply.MakeStatsInfo(queueLabelKeys) 47 | t.Log(qinfo) 48 | if qinfo[0].labels["name"] != "q1" { 49 | t.Errorf("unexpected qinfo name: %v", qinfo[0].labels["name"]) 50 | } 51 | if qinfo[1].labels["name"] != "q2" { 52 | t.Errorf("unexpected qinfo name: %v", qinfo[0].labels["name"]) 53 | } 54 | if qinfo[1].labels["vhost"] != "foo" { 55 | t.Errorf("unexpected qinfo name: %v", qinfo[0].labels["name"]) 56 | } 57 | checkMap(qinfo[0].metrics, t, 10) 58 | checkMap(qinfo[1].metrics, t, 20) 59 | } 60 | 61 | func TestArraySize(t *testing.T) { 62 | reply, _ := makeJSONReply([]byte(`{"node":"node1","empty_partitions": [],"partitions": [{"name":"node1"},{"name":"node2"}]}`)) 63 | 64 | node := reply.MakeMap() 65 | t.Log(node) 66 | if v, ok := node["empty_partitions_len"]; !ok || v != 0 { 67 | t.Error("Unexpected (empty) partitions size", v) 68 | } 69 | if v, ok := node["partitions_len"]; !ok || v != 2 { 70 | t.Error("Unexpected partitions size", v) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "flag" 7 | "net/http" 8 | "os" 9 | "strings" 10 | "time" 11 | 12 | "github.com/prometheus/client_golang/prometheus/promhttp" 13 | 14 | "github.com/prometheus/client_golang/prometheus" 15 | log "github.com/sirupsen/logrus" 16 | ) 17 | 18 | const ( 19 | defaultLogLevel = log.InfoLevel 20 | ) 21 | 22 | func initLogger() { 23 | log.SetLevel(getLogLevel()) 24 | if strings.ToUpper(config.OutputFormat) == "JSON" { 25 | log.SetFormatter(&log.JSONFormatter{}) 26 | } else { 27 | // The TextFormatter is default, you don't actually have to do this. 28 | log.SetFormatter(&log.TextFormatter{}) 29 | } 30 | } 31 | 32 | func main() { 33 | var checkURL = flag.String("check-url", "", "Curl url and return exit code (http: 200 => 0, otherwise 1)") 34 | var configFile = flag.String("config-file", "conf/rabbitmq.conf", "path to json config") 35 | flag.Parse() 36 | 37 | if *checkURL != "" { // do a single http get request. Used in docker healthckecks as curl is not inside the image 38 | curl(*checkURL) 39 | return 40 | } 41 | 42 | err := initConfigFromFile(*configFile) //Try parsing config file 43 | if _, isPathError := err.(*os.PathError); isPathError { // No file => use environment variables 44 | initConfig() 45 | } else if err != nil { 46 | panic(err) 47 | } 48 | 49 | initLogger() 50 | initClient() 51 | exporter := newExporter() 52 | prometheus.MustRegister(exporter) 53 | 54 | log.WithFields(log.Fields{ 55 | "VERSION": Version, 56 | "REVISION": Revision, 57 | "BRANCH": Branch, 58 | "BUILD_DATE": BuildDate, 59 | // "RABBIT_PASSWORD": config.RABBIT_PASSWORD, 60 | }).Info("Starting RabbitMQ exporter") 61 | 62 | log.WithFields(log.Fields{ 63 | "PUBLISH_ADDR": config.PublishAddr, 64 | "PUBLISH_PORT": config.PublishPort, 65 | "RABBIT_URL": config.RabbitURL, 66 | "RABBIT_USER": config.RabbitUsername, 67 | "RABBIT_CONNECTION": config.RabbitConnection, 68 | "OUTPUT_FORMAT": config.OutputFormat, 69 | "RABBIT_CAPABILITIES": formatCapabilities(config.RabbitCapabilities), 70 | "RABBIT_EXPORTERS": config.EnabledExporters, 71 | "CAFILE": config.CAFile, 72 | "CERTFILE": config.CertFile, 73 | "KEYFILE": config.KeyFile, 74 | "SKIPVERIFY": config.InsecureSkipVerify, 75 | "EXCLUDE_METRICS": config.ExcludeMetrics, 76 | "SKIP_EXCHANGES": config.SkipExchanges.String(), 77 | "INCLUDE_EXCHANGES": config.IncludeExchanges.String(), 78 | "SKIP_QUEUES": config.SkipQueues.String(), 79 | "INCLUDE_QUEUES": config.IncludeQueues.String(), 80 | "SKIP_VHOST": config.SkipVHost.String(), 81 | "INCLUDE_VHOST": config.IncludeVHost.String(), 82 | "RABBIT_TIMEOUT": config.Timeout, 83 | "MAX_QUEUES": config.MaxQueues, 84 | // "RABBIT_PASSWORD": config.RABBIT_PASSWORD, 85 | }).Info("Active Configuration") 86 | 87 | handler := http.NewServeMux() 88 | handler.Handle("/metrics", promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{})) 89 | handler.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 90 | _, _ = w.Write([]byte(` 91 | RabbitMQ Exporter 92 | 93 |

RabbitMQ Exporter

94 |

Metrics

95 | 96 | `)) 97 | }) 98 | handler.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { 99 | if exporter.LastScrapeOK() { 100 | w.WriteHeader(http.StatusOK) 101 | } else { 102 | w.WriteHeader(http.StatusGatewayTimeout) 103 | } 104 | }) 105 | 106 | server := &http.Server{Addr: config.PublishAddr + ":" + config.PublishPort, Handler: handler} 107 | 108 | go func() { 109 | if err := server.ListenAndServe(); err != nil { 110 | log.Fatal(err) 111 | } 112 | }() 113 | 114 | <-runService() 115 | log.Info("Shutting down") 116 | 117 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 118 | if err := server.Shutdown(ctx); err != nil { 119 | log.Fatal(err) 120 | } 121 | cancel() 122 | } 123 | 124 | func getLogLevel() log.Level { 125 | lvl := strings.ToLower(os.Getenv("LOG_LEVEL")) 126 | level, err := log.ParseLevel(lvl) 127 | if err != nil { 128 | level = defaultLogLevel 129 | } 130 | return level 131 | } 132 | 133 | func formatCapabilities(caps rabbitCapabilitySet) string { 134 | var buffer bytes.Buffer 135 | first := true 136 | for k := range caps { 137 | if !first { 138 | buffer.WriteString(",") 139 | } 140 | first = false 141 | buffer.WriteString(string(k)) 142 | } 143 | return buffer.String() 144 | } 145 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | log "github.com/sirupsen/logrus" 5 | "io/ioutil" 6 | ) 7 | 8 | func init() { 9 | log.SetOutput(ioutil.Discard) 10 | } 11 | -------------------------------------------------------------------------------- /metrics.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/prometheus/client_golang/prometheus" 4 | 5 | const ( 6 | namespace = "rabbitmq" 7 | ) 8 | 9 | func newGaugeVec(metricName string, docString string, labels []string) *prometheus.GaugeVec { 10 | return prometheus.NewGaugeVec( 11 | prometheus.GaugeOpts{ 12 | Namespace: namespace, 13 | Name: metricName, 14 | Help: docString, 15 | }, 16 | labels, 17 | ) 18 | } 19 | 20 | func newDesc(metricName string, docString string, labels []string) *prometheus.Desc { 21 | return prometheus.NewDesc( 22 | prometheus.BuildFQName(namespace, "", metricName), 23 | docString, 24 | labels, 25 | nil) 26 | } 27 | -------------------------------------------------------------------------------- /metrics.md: -------------------------------------------------------------------------------- 1 | # Metrics 2 | 3 | All metrics (except golang/prometheus metrics) are prefixed with "rabbitmq_". 4 | 5 | ## Global 6 | 7 | Always exported. 8 | 9 | metric | description 10 | -------| ------------ 11 | |up | Was the last scrape of rabbitmq successful. 12 | |module_up | Was the last scrape of rabbitmq module successful. labels: module 13 | |module_scrape_duration_seconds | Duration of the last scrape of rabbitmq module. labels: module 14 | |exporter_build_info | A metric with a constant '1' value labeled by version, revision, branch and build date on which the rabbitmq_exporter was built. 15 | 16 | ## Overview 17 | 18 | Always exported. 19 | Labels: cluster 20 | 21 | metric | description 22 | -------| ------------ 23 | |channels | Number of channels| 24 | |connections | Number of connections| 25 | |consumers | Number of message consumers| 26 | |queues | Number of queues in use| 27 | |exchanges | Number of exchanges in use| 28 | |queue_messages_global | Number ready and unacknowledged messages in cluster.| 29 | |queue_messages_ready_global | Number of messages ready to be delivered to clients.| 30 | |queue_messages_unacknowledged_global | Number of messages delivered to clients but not yet acknowledged.| 31 | |version_info| A metric with a constant '1' value labeled by rabbitmq version, erlang version, node, cluster.| 32 | 33 | ## Queues 34 | 35 | Labels: cluster, vhost, queue, durable, policy, self 36 | 37 | ### Queues - Gauge 38 | 39 | metric | description 40 | -------| ------------ 41 | |queue_messages_ready|Number of messages ready to be delivered to clients.| 42 | |queue_messages_unacknowledged|Number of messages delivered to clients but not yet acknowledged.| 43 | |queue_messages|Sum of ready and unacknowledged messages (queue depth).| 44 | |queue_messages_ack_total|Number of messages delivered in acknowledgement mode in response to basic.get.| 45 | |queue_messages_ready_ram|Number of messages from messages_ready which are resident in ram.| 46 | |queue_messages_unacknowledged_ram|Number of messages from messages_unacknowledged which are resident in ram.| 47 | |queue_messages_ram|Total number of messages which are resident in ram.| 48 | |queue_messages_persistent|Total number of persistent messages in the queue (will always be 0 for transient queues).| 49 | |queue_message_bytes|Sum of the size of all message bodies in the queue. This does not include the message properties (including headers) or any overhead.| 50 | |queue_message_bytes_ready|Like message_bytes but counting only those messages ready to be delivered to clients.| 51 | |queue_message_bytes_unacknowledged|Like message_bytes but counting only those messages delivered to clients but not yet acknowledged.| 52 | |queue_message_bytes_ram|Like message_bytes but counting only those messages which are in RAM.| 53 | |queue_message_bytes_persistent|Like message_bytes but counting only those messages which are persistent.| 54 | |queue_consumers|Number of consumers.| 55 | |queue_consumer_utilisation|Fraction of the time (between 0.0 and 1.0) that the queue is able to immediately deliver messages to consumers. This can be less than 1.0 if consumers are limited by network congestion or prefetch count.| 56 | |queue_memory|Bytes of memory consumed by the Erlang process associated with the queue, including stack, heap and internal structures.| 57 | |queue_head_message_timestamp|The timestamp property of the first message in the queue, if present. Timestamps of messages only appear when they are in the paged-in state.| 58 | |queue_max_length_bytes|Total body size for ready messages a queue can contain before it starts to drop them from its head.| 59 | |queue_max_length|How many (ready) messages a queue can contain before it starts to drop them from its head.| 60 | |queue_idle_since_seconds|starttime where the queue switched to idle state; seconds since epoch (1970); only set if queue state is idle| 61 | |queue_reductions_total|Number of reductions which take place on this process.| 62 | |queue_state|A metric with a value of constant '1' if the queue is in a certain state. Labels: vhost, queue, *state* (running, idle, flow,..)| 63 | |queue_slave_nodes_len|Number of slave nodes attached to the queue| 64 | |queue_synchronised_slave_nodes_len|Number of slave nodes in sync to the queue| 65 | 66 | ### Queues - Counter 67 | 68 | metric | description 69 | -------| ------------ 70 | |queue_disk_reads_total|Total number of times messages have been read from disk by this queue since it started.| 71 | |queue_disk_writes_total|Total number of times messages have been written to disk by this queue since it started.| 72 | |queue_messages_published_total|Count of messages published.| 73 | |queue_messages_confirmed_total|Count of messages confirmed. | 74 | |queue_messages_delivered_total|Count of messages delivered in acknowledgement mode to consumers.| 75 | |queue_messages_delivered_noack_total|Count of messages delivered in no-acknowledgement mode to consumers. | 76 | |queue_messages_get_total|Count of messages delivered in acknowledgement mode in response to basic.get.| 77 | |queue_messages_get_noack_total|Count of messages delivered in no-acknowledgement mode in response to basic.get.| 78 | |queue_messages_redelivered_total|Count of subset of messages in deliver_get which had the redelivered flag set.| 79 | |queue_messages_returned_total|Count of messages returned to publisher as unroutable.| 80 | 81 | ## Exchanges - Counter 82 | 83 | Labels: cluster, vhost, exchange 84 | 85 | metric | description 86 | -------| ------------ 87 | |exchange_messages_published_in_total|Count of messages published in to an exchange, i.e. not taking account of routing.| 88 | |exchange_messages_published_out_total|Count of messages published out of an exchange, i.e. taking account of routing.| 89 | 90 | ## Node - Counter 91 | 92 | Labels: cluster, node, self 93 | 94 | metric | description 95 | -------| ------------ 96 | |uptime|Uptime in milliseconds| 97 | |running|number of running nodes| 98 | |node_mem_used|Memory used in bytes| 99 | |node_mem_limit|Point at which the memory alarm will go off| 100 | |node_mem_alarm|Whether the memory alarm has gone off| 101 | |node_disk_free|Disk free space in bytes.| 102 | |node_disk_free_alarm|Whether the disk alarm has gone off.| 103 | |node_disk_free_limit|Point at which the disk alarm will go off.| 104 | |fd_used|Used File descriptors| 105 | |fd_available|File descriptors available| 106 | |sockets_used|File descriptors used as sockets.| 107 | |sockets_available|File descriptors available for use as sockets| 108 | |partitions | Current Number of network partitions. 0 is ok. If the cluster is splitted the value is at least 2| 109 | 110 | ## Connections - Gauge 111 | 112 | _disabled by default_. Depending on the environment and change rate it can create a high number of dead metrics. Otherwise it could be usefull and can be enabled. 113 | 114 | Labels: cluster, vhost, node, peer_host, user, self 115 | 116 | Please note: The data is aggregated by label values as it is possible that there are multiple connections for a certain combination of labels. 117 | 118 | metric | description 119 | -------| ------------ 120 | |connection_channels|number of channels in use| 121 | |connection_received_bytes|received bytes| 122 | |connection_received_packets|received packets| 123 | |connection_send_bytes|send bytes| 124 | |connection_send_packets|send packets| 125 | |connection_send_pending|Send queue size| 126 | 127 | Labels: vhost, node, peer_host, user, *state* (running, flow,..), self 128 | 129 | metric | description 130 | -------| ------------ 131 | |connection_status|Number of connections in a certain state aggregated per label combination. Metric will disappear if there are no connections in a state. | 132 | 133 | ## Shovel 134 | 135 | _disabled by default_ 136 | Labels: cluster, vhost, shovel, type, self, state 137 | 138 | metric | description 139 | -------| ------------ 140 | |shovel_state|A metric with a value of constant '1' for each shovel in a certain state| 141 | 142 | ## Memory 143 | 144 | _disabled by default_ 145 | Labels: cluster, node, self 146 | 147 | metric | description 148 | -------| ------------ 149 | |memory_allocated_unused_bytes|Memory preallocated by the runtime (VM allocators) but not yet used| 150 | |memory_atom_bytes|Memory used by atoms. Should be fairly constant| 151 | |memory_binary_bytes|Memory used by shared binary data in the runtime. Most of this memory is message bodies and metadata.)| 152 | |memory_code_bytes|Memory used by code (bytecode, module metadata). This section is usually fairly constant and relatively small (unless the node is entirely blank and stores no data).| 153 | |memory_connection_channels_bytes|Memory used by client connections - channels| 154 | |memory_connection_other_bytes|Memory used by client connection - other| 155 | |memory_connection_readers|Memory used by processes responsible for connection parser and most of connection state. Most of their memory attributes to TCP buffers| 156 | |memory_connection_writers_bytes|Memory used by processes responsible for serialization of outgoing protocol frames and writing to client connections socktes| 157 | |memory_mgmt_db_bytes|Management DB ETS tables + processes| 158 | |memory_mnesia_bytes|Internal database (Mnesia) tables keep an in-memory copy of all its data (even on disc nodes)| 159 | |memory_msg_index_bytes|Message index ETS + processes| 160 | |memory_other_ets_bytes|Other in-memory tables besides those belonging to the stats database and internal database tables| 161 | |memory_other_proc_bytes|Memory used by all other processes that RabbitMQ cannot categorise| 162 | |memory_other_system_bytes|Memory used by all other system that RabbitMQ cannot categorise| 163 | |memory_plugins_bytes|Memory used by plugins (apart from the Erlang client which is counted under Connections, and the management database which is counted separately).| 164 | |memory_queue_procs_bytes|Memory used by class queue masters, queue indices, queue state| 165 | |memory_queue_slave_procs_bytes|Memory used by class queue mirrors, queue indices, queue state| 166 | |memory_reserved_unallocated_bytes|Memory preallocated/reserved by the kernel but not the runtime| 167 | |memory_total_allocated_bytes|Node-local total memory - allocated| 168 | |memory_total_rss_bytes|Node-local total memory - rss| 169 | |memory_total_erlang_bytes|Node-local total memory - erlang| 170 | -------------------------------------------------------------------------------- /pprof_test.go: -------------------------------------------------------------------------------- 1 | //go:build pprof 2 | // +build pprof 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "testing" 10 | "time" 11 | 12 | "github.com/kbudde/rabbitmq_exporter/testenv" 13 | ) 14 | 15 | func TestPProf(t *testing.T) { 16 | // go test -v -run TestPProf -tags pprof -cpuprofile=cpuprof.out 17 | // go-torch rabbitmq_exporter.test cpuprof.out 18 | var env testenv.TestEnvironment 19 | var exporterURL string 20 | var rabbitManagementURL string 21 | t.Run("Preparation", func(t *testing.T) { 22 | env = testenv.NewEnvironment(t, testenv.RabbitMQ3Latest) 23 | 24 | exporterURL = fmt.Sprintf("http://localhost:%s/metrics", defaultConfig.PublishPort) 25 | rabbitManagementURL = env.ManagementURL() 26 | os.Setenv("LOG_LEVEL", "FATAL") 27 | defer os.Unsetenv("LOG_LEVEL") 28 | go main() 29 | 30 | for i := 0; i < 100; i++ { 31 | queue := fmt.Sprintf("queue-%d", i) 32 | env.Rabbit.DeclareQueue(queue, false) 33 | } 34 | 35 | time.Sleep(5 * time.Second) // give rabbitmq management plugin a bit of time 36 | }) 37 | defer env.CleanUp() // do not panic or exit fatally or the container will stay up 38 | os.Setenv("RABBIT_URL", rabbitManagementURL) 39 | defer os.Unsetenv("RABBIT_URL") 40 | 41 | t.Run("Fetch Exporter Bert, no sort", func(t *testing.T) { 42 | os.Setenv("RABBIT_CAPABILITIES", "bert,no_sort") 43 | defer os.Unsetenv("RABBIT_CAPABILITIES") 44 | initConfig() 45 | for i := 0; i < 100; i++ { 46 | testenv.GetOrDie(exporterURL, 5*time.Second) 47 | } 48 | }) 49 | t.Run("Fetch Exporter Json, no sort", func(t *testing.T) { 50 | os.Setenv("RABBIT_CAPABILITIES", "no_sort") 51 | defer os.Unsetenv("RABBIT_CAPABILITIES") 52 | initConfig() 53 | for i := 0; i < 100; i++ { 54 | testenv.GetOrDie(exporterURL, 5*time.Second) 55 | } 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /rabbitClient.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "errors" 7 | "io/ioutil" 8 | "net/http" 9 | "net/url" 10 | "os" 11 | "time" 12 | 13 | log "github.com/sirupsen/logrus" 14 | ) 15 | 16 | var client = &http.Client{Timeout: 15 * time.Second} //default client for test. Client is initialized in initClient() 17 | 18 | func initClient() { 19 | var roots *x509.CertPool 20 | 21 | if data, err := ioutil.ReadFile(config.CAFile); err == nil { 22 | roots = x509.NewCertPool() 23 | if !roots.AppendCertsFromPEM(data) { 24 | log.WithField("filename", config.CAFile).Error("Adding certificate to rootCAs failed") 25 | } 26 | } else { 27 | var err error 28 | log.Info("Using default certificate pool") 29 | roots, err = x509.SystemCertPool() 30 | if err != nil { 31 | log.WithError(err).Error("retriving system cert pool failed") 32 | } 33 | 34 | } 35 | 36 | tr := &http.Transport{ 37 | TLSClientConfig: &tls.Config{ 38 | InsecureSkipVerify: config.InsecureSkipVerify, 39 | RootCAs: roots, 40 | }, 41 | } 42 | 43 | _, errCertFile := os.Stat(config.CertFile) 44 | _, errKeyFile := os.Stat(config.KeyFile) 45 | if errCertFile == nil && errKeyFile == nil { 46 | log.Info("Using client certificate: " + config.CertFile + " and key: " + config.KeyFile) 47 | if cert, err := tls.LoadX509KeyPair(config.CertFile, config.KeyFile); err == nil { 48 | tr.TLSClientConfig.ClientAuth = tls.RequireAndVerifyClientCert 49 | tr.TLSClientConfig.Certificates = []tls.Certificate{cert} 50 | } else { 51 | log.WithField("certFile", config.CertFile). 52 | WithField("keyFile", config.KeyFile). 53 | Error("Loading client certificate and key failed: ", err) 54 | } 55 | } 56 | 57 | client = &http.Client{ 58 | Transport: tr, 59 | Timeout: time.Duration(config.Timeout) * time.Second, 60 | } 61 | 62 | } 63 | 64 | func apiRequest(config rabbitExporterConfig, endpoint string) ([]byte, string, error) { 65 | var args string 66 | enabled, exists := config.RabbitCapabilities[rabbitCapNoSort] 67 | if enabled && exists { 68 | args = "?sort=" 69 | } 70 | 71 | if endpoint == "aliveness-test" { 72 | escapeAlivenessVhost := url.QueryEscape(config.AlivenessVhost) 73 | args = "/" + escapeAlivenessVhost 74 | } 75 | 76 | req, err := http.NewRequest("GET", config.RabbitURL+"/api/"+endpoint+args, nil) 77 | if err != nil { 78 | log.WithFields(log.Fields{"error": err, "host": config.RabbitURL}).Error("Error while constructing rabbitHost request") 79 | return nil, "", errors.New("Error while constructing rabbitHost request") 80 | } 81 | 82 | req.SetBasicAuth(config.RabbitUsername, config.RabbitPassword) 83 | req.Header.Add("Accept", acceptContentType(config)) 84 | 85 | resp, err := client.Do(req) 86 | 87 | if err != nil || resp == nil || resp.StatusCode != 200 { 88 | status := 0 89 | if resp != nil { 90 | status = resp.StatusCode 91 | } 92 | log.WithFields(log.Fields{"error": err, "host": config.RabbitURL, "statusCode": status}).Error("Error while retrieving data from rabbitHost") 93 | return nil, "", errors.New("Error while retrieving data from rabbitHost") 94 | } 95 | 96 | body, err := ioutil.ReadAll(resp.Body) 97 | resp.Body.Close() 98 | content := resp.Header.Get("Content-type") 99 | if err != nil { 100 | return nil, "", err 101 | } 102 | log.WithFields(log.Fields{"body": string(body), "endpoint": endpoint}).Debug("Metrics loaded") 103 | 104 | return body, content, nil 105 | } 106 | 107 | func loadMetrics(config rabbitExporterConfig, endpoint string) (RabbitReply, error) { 108 | body, content, err := apiRequest(config, endpoint) 109 | if err != nil { 110 | return nil, err 111 | } 112 | return MakeReply(content, body) 113 | } 114 | 115 | func getStatsInfo(config rabbitExporterConfig, apiEndpoint string, labels []string) ([]StatsInfo, error) { 116 | var q []StatsInfo 117 | 118 | reply, err := loadMetrics(config, apiEndpoint) 119 | if err != nil { 120 | return q, err 121 | } 122 | 123 | q = reply.MakeStatsInfo(labels) 124 | 125 | return q, nil 126 | } 127 | 128 | func getMetricMap(config rabbitExporterConfig, apiEndpoint string) (MetricMap, error) { 129 | var overview MetricMap 130 | 131 | body, content, err := apiRequest(config, apiEndpoint) 132 | if err != nil { 133 | return overview, err 134 | } 135 | 136 | reply, err := MakeReply(content, body) 137 | if err != nil { 138 | return overview, err 139 | } 140 | 141 | return reply.MakeMap(), nil 142 | } 143 | 144 | func acceptContentType(config rabbitExporterConfig) string { 145 | if isCapEnabled(config, rabbitCapBert) { 146 | return "application/bert, application/json;q=0.1" 147 | } 148 | return "application/json" 149 | } 150 | -------------------------------------------------------------------------------- /rabbitClient_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | "reflect" 8 | "testing" 9 | ) 10 | 11 | func expect(t *testing.T, got interface{}, expected interface{}) { 12 | t.Helper() 13 | if got != expected { 14 | t.Errorf("Expected %v (type %v) - Got %v (type %v)", expected, reflect.TypeOf(expected), got, reflect.TypeOf(got)) 15 | } 16 | } 17 | 18 | func createTestserver(result int, answer string) *httptest.Server { 19 | return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 20 | w.WriteHeader(result) 21 | w.Header().Set("Content-Type", "application/json") 22 | fmt.Fprintln(w, answer) 23 | })) 24 | } 25 | 26 | func TestGetMetricMap(t *testing.T) { 27 | // Test server that always responds with 200 code, and specific payload 28 | server := createTestserver(200, `{"nonFloat":"bob@example.com","float1":1.23456789101112,"number":2}`) 29 | defer server.Close() 30 | 31 | config := &rabbitExporterConfig{ 32 | RabbitURL: server.URL, 33 | } 34 | 35 | overview, _ := getMetricMap(*config, "overview") 36 | 37 | expect(t, len(overview), 2) 38 | expect(t, overview["float1"], 1.23456789101112) 39 | expect(t, overview["number"], 2.0) 40 | 41 | //Unknown error Server 42 | errorServer := createTestserver(500, http.StatusText(500)) 43 | defer errorServer.Close() 44 | 45 | config = &rabbitExporterConfig{ 46 | RabbitURL: errorServer.URL, 47 | } 48 | 49 | overview, _ = getMetricMap(*config, "overview") 50 | 51 | expect(t, len(overview), 0) 52 | } 53 | 54 | func TestQueues(t *testing.T) { 55 | // Test server that always responds with 200 code, and specific payload 56 | server := createTestserver(200, `[{"name":"Queue1","nonFloat":"bob@example.com","float1":1.23456789101112,"number":2},{"name":"Queue2","vhost":"Vhost2","nonFloat":"bob@example.com","float1":3.23456789101112,"number":3}]`) 57 | defer server.Close() 58 | 59 | config := &rabbitExporterConfig{ 60 | RabbitURL: server.URL, 61 | } 62 | 63 | queues, err := getStatsInfo(*config, "queues", queueLabelKeys) 64 | expect(t, err, nil) 65 | expect(t, len(queues), 2) 66 | expect(t, queues[0].labels["name"], "Queue1") 67 | expect(t, queues[0].labels["vhost"], "") 68 | expect(t, queues[1].labels["name"], "Queue2") 69 | expect(t, queues[1].labels["vhost"], "Vhost2") 70 | expect(t, len(queues[0].metrics), 2) 71 | expect(t, len(queues[1].metrics), 2) 72 | expect(t, queues[0].metrics["float1"], 1.23456789101112) 73 | expect(t, queues[1].metrics["float1"], 3.23456789101112) 74 | expect(t, queues[0].metrics["number"], 2.0) 75 | expect(t, queues[1].metrics["number"], 3.0) 76 | 77 | //Unknown error Server 78 | errorServer := createTestserver(500, http.StatusText(500)) 79 | defer errorServer.Close() 80 | 81 | config = &rabbitExporterConfig{ 82 | RabbitURL: errorServer.URL, 83 | } 84 | 85 | queues, err = getStatsInfo(*config, "queues", queueLabelKeys) 86 | if err == nil { 87 | t.Errorf("Request failed. An error was expected but not found") 88 | } 89 | expect(t, len(queues), 0) 90 | } 91 | 92 | func TestExchanges(t *testing.T) { 93 | 94 | // Test server that always responds with 200 code, and specific payload 95 | server := createTestserver(200, exchangeAPIResponse) 96 | defer server.Close() 97 | 98 | config := &rabbitExporterConfig{ 99 | RabbitURL: server.URL, 100 | } 101 | 102 | exchanges, err := getStatsInfo(*config, "exchanges", exchangeLabelKeys) 103 | expect(t, err, nil) 104 | expect(t, len(exchanges), 12) 105 | expect(t, exchanges[0].labels["name"], "") 106 | expect(t, exchanges[0].labels["vhost"], "/") 107 | expect(t, exchanges[1].labels["name"], "amq.direct") 108 | expect(t, exchanges[1].labels["vhost"], "/") 109 | expect(t, len(exchanges[0].metrics), 3) 110 | expect(t, len(exchanges[1].metrics), 3) 111 | 112 | expect(t, exchanges[8].labels["name"], "myExchange") 113 | expect(t, exchanges[8].labels["vhost"], "/") 114 | expect(t, exchanges[8].metrics["message_stats.confirm"], 5.0) 115 | expect(t, exchanges[8].metrics["message_stats.publish_in"], 5.0) 116 | expect(t, exchanges[8].metrics["message_stats.ack"], 0.0) 117 | expect(t, exchanges[8].metrics["message_stats.return_unroutable"], 5.0) 118 | 119 | //Unknown error Server 120 | errorServer := createTestserver(500, http.StatusText(500)) 121 | defer errorServer.Close() 122 | 123 | config = &rabbitExporterConfig{ 124 | RabbitURL: errorServer.URL, 125 | } 126 | 127 | exchanges, err = getStatsInfo(*config, "exchanges", exchangeLabels) 128 | if err == nil { 129 | t.Errorf("Request failed. An error was expected but not found") 130 | } 131 | expect(t, len(exchanges), 0) 132 | } 133 | 134 | func TestNoSort(t *testing.T) { 135 | assertNoSortRespected(t, false) 136 | assertNoSortRespected(t, true) 137 | } 138 | 139 | func assertNoSortRespected(t *testing.T, enabled bool) { 140 | var args string 141 | if enabled { 142 | args = "?sort=" 143 | } 144 | 145 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 146 | w.WriteHeader(http.StatusOK) 147 | w.Header().Set("Content-Type", "application/json") 148 | if r.RequestURI == "/api/overview"+args { 149 | fmt.Fprintln(w, `{"nonFloat":"bob@example.com","float1":1.23456789101112,"number":2}`) 150 | } else { 151 | t.Errorf("Invalid request with enabled=%t. URI=%v", enabled, r.RequestURI) 152 | fmt.Fprintf(w, "Invalid request. URI=%v", r.RequestURI) 153 | } 154 | 155 | })) 156 | defer server.Close() 157 | 158 | config := &rabbitExporterConfig{ 159 | RabbitURL: server.URL, 160 | RabbitCapabilities: rabbitCapabilitySet{rabbitCapNoSort: enabled}, 161 | } 162 | 163 | if _, err := getMetricMap(*config, "overview"); err != nil { 164 | t.Errorf("Error getting overview: %v", err) 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ], 5 | "includeForks": true 6 | } 7 | -------------------------------------------------------------------------------- /service.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package main 5 | 6 | import ( 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | ) 11 | 12 | //runService wait for os interrupt 13 | func runService() chan bool { 14 | waitChan := make(chan bool) 15 | 16 | c := make(chan os.Signal, 1) 17 | signal.Notify(c, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) 18 | go func() { 19 | for range c { 20 | waitChan <- true 21 | return 22 | } 23 | }() 24 | return waitChan 25 | } 26 | -------------------------------------------------------------------------------- /service_windows.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | log "github.com/sirupsen/logrus" 5 | "golang.org/x/sys/windows/svc" 6 | ) 7 | 8 | const serviceName = "rabbitmq_exporter" 9 | 10 | func runService() chan bool { 11 | stopCh := make(chan bool) 12 | isInteractive, err := svc.IsAnInteractiveSession() 13 | if err != nil { 14 | log.Fatal(err) 15 | } 16 | if !isInteractive { 17 | go svc.Run(serviceName, &rmqExporterService{stopCh: stopCh}) 18 | } 19 | return stopCh 20 | } 21 | 22 | type rmqExporterService struct { 23 | stopCh chan<- bool 24 | } 25 | 26 | func (s *rmqExporterService) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (ssec bool, errno uint32) { 27 | const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown 28 | changes <- svc.Status{State: svc.StartPending} 29 | changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} 30 | loop: 31 | for { 32 | select { 33 | case c := <-r: 34 | switch c.Cmd { 35 | case svc.Interrogate: 36 | changes <- c.CurrentStatus 37 | case svc.Stop, svc.Shutdown: 38 | s.stopCh <- true 39 | break loop 40 | default: 41 | log.Error("unexpected control request ", c) 42 | } 43 | } 44 | changes <- svc.Status{State: svc.StopPending} 45 | } 46 | return 47 | } 48 | -------------------------------------------------------------------------------- /testdata/exchanges-3.6.8.bert: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbudde/rabbitmq_exporter/4dcae0489d93568daeb2c7c4e7d891bf8aab5df8/testdata/exchanges-3.6.8.bert -------------------------------------------------------------------------------- /testdata/exchanges-3.6.8.json: -------------------------------------------------------------------------------- 1 | [{"name":"","vhost":"/","type":"direct","durable":true,"auto_delete":false,"internal":false,"arguments":{},"message_stats":{"publish_out":1,"publish_out_details":{"rate":0.0},"publish_in":1,"publish_in_details":{"rate":0.0}}},{"name":"amq.direct","vhost":"/","type":"direct","durable":true,"auto_delete":false,"internal":false,"arguments":{}},{"name":"amq.fanout","vhost":"/","type":"fanout","durable":true,"auto_delete":false,"internal":false,"arguments":{}},{"name":"amq.headers","vhost":"/","type":"headers","durable":true,"auto_delete":false,"internal":false,"arguments":{}},{"name":"amq.match","vhost":"/","type":"headers","durable":true,"auto_delete":false,"internal":false,"arguments":{}},{"name":"amq.rabbitmq.log","vhost":"/","type":"topic","durable":true,"auto_delete":false,"internal":true,"arguments":{}},{"name":"amq.rabbitmq.trace","vhost":"/","type":"topic","durable":true,"auto_delete":false,"internal":true,"arguments":{}},{"name":"amq.topic","vhost":"/","type":"topic","durable":true,"auto_delete":false,"internal":false,"arguments":{}}] -------------------------------------------------------------------------------- /testdata/exchanges-3.7.0.bert: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbudde/rabbitmq_exporter/4dcae0489d93568daeb2c7c4e7d891bf8aab5df8/testdata/exchanges-3.7.0.bert -------------------------------------------------------------------------------- /testdata/exchanges-3.7.0.json: -------------------------------------------------------------------------------- 1 | [{"name":"","vhost":"/","type":"direct","durable":true,"auto_delete":false,"internal":false,"arguments":{},"user_who_performed_action":"rmq-internal"},{"name":"amq.direct","vhost":"/","type":"direct","durable":true,"auto_delete":false,"internal":false,"arguments":{},"user_who_performed_action":"rmq-internal"},{"name":"amq.fanout","vhost":"/","type":"fanout","durable":true,"auto_delete":false,"internal":false,"arguments":{},"user_who_performed_action":"rmq-internal"},{"name":"amq.headers","vhost":"/","type":"headers","durable":true,"auto_delete":false,"internal":false,"arguments":{},"user_who_performed_action":"rmq-internal"},{"name":"amq.match","vhost":"/","type":"headers","durable":true,"auto_delete":false,"internal":false,"arguments":{},"user_who_performed_action":"rmq-internal"},{"name":"amq.rabbitmq.log","vhost":"/","type":"topic","durable":true,"auto_delete":false,"internal":true,"arguments":{},"user_who_performed_action":"rmq-internal"},{"name":"amq.rabbitmq.trace","vhost":"/","type":"topic","durable":true,"auto_delete":false,"internal":true,"arguments":{},"user_who_performed_action":"rmq-internal"},{"name":"amq.topic","vhost":"/","type":"topic","durable":true,"auto_delete":false,"internal":false,"arguments":{},"user_who_performed_action":"rmq-internal"}] -------------------------------------------------------------------------------- /testdata/nodes-3.6.8.bert: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbudde/rabbitmq_exporter/4dcae0489d93568daeb2c7c4e7d891bf8aab5df8/testdata/nodes-3.6.8.bert -------------------------------------------------------------------------------- /testdata/nodes-3.6.8.json: -------------------------------------------------------------------------------- 1 | [{"partitions":[],"os_pid":"14935","fd_total":1024,"sockets_total":829,"mem_limit":6679527424,"mem_alarm":false,"disk_free_limit":50000000,"disk_free_alarm":false,"proc_total":262144,"rates_mode":"basic","uptime":76891943,"run_queue":0,"processors":4,"exchange_types":[{"name":"topic","description":"AMQP topic exchange, as per the AMQP specification","enabled":true},{"name":"direct","description":"AMQP direct exchange, as per the AMQP specification","enabled":true},{"name":"headers","description":"AMQP headers exchange, as per the AMQP specification","enabled":true},{"name":"fanout","description":"AMQP fanout exchange, as per the AMQP specification","enabled":true}],"auth_mechanisms":[{"name":"AMQPLAIN","description":"QPid AMQPLAIN mechanism","enabled":true},{"name":"PLAIN","description":"SASL PLAIN authentication mechanism","enabled":true},{"name":"RABBIT-CR-DEMO","description":"RabbitMQ Demo challenge-response authentication mechanism","enabled":false}],"applications":[{"name":"amqp_client","description":"RabbitMQ AMQP Client","version":"3.6.8+1.g1dcb221"},{"name":"asn1","description":"The Erlang ASN1 compiler version 4.0.2","version":"4.0.2"},{"name":"compiler","description":"ERTS CXC 138 10","version":"6.0.3"},{"name":"cowboy","description":"Small, fast, modular HTTP server.","version":"1.0.4"},{"name":"cowlib","description":"Support library for manipulating Web protocols.","version":"1.0.2"},{"name":"crypto","description":"CRYPTO","version":"3.6.3"},{"name":"inets","description":"INETS CXC 138 49","version":"6.2.4"},{"name":"kernel","description":"ERTS CXC 138 10","version":"4.2"},{"name":"mnesia","description":"MNESIA CXC 138 12","version":"4.13.4"},{"name":"os_mon","description":"CPO CXC 138 46","version":"2.4"},{"name":"public_key","description":"Public key infrastructure","version":"1.1.1"},{"name":"rabbit","description":"RabbitMQ","version":"3.6.8+1.g1dcb221"},{"name":"rabbit_common","description":"Modules shared by rabbitmq-server and rabbitmq-erlang-client","version":"3.6.8+1.g1dcb221"},{"name":"rabbitmq_management","description":"RabbitMQ Management Console","version":"3.6.8+1.g1dcb221"},{"name":"rabbitmq_management_agent","description":"RabbitMQ Management Agent","version":"3.6.8+1.g1dcb221"},{"name":"rabbitmq_web_dispatch","description":"RabbitMQ Web Dispatcher","version":"3.6.8+1.g1dcb221"},{"name":"ranch","description":"Socket acceptor pool for TCP protocols.","version":"1.3.0"},{"name":"sasl","description":"SASL CXC 138 11","version":"2.7"},{"name":"ssl","description":"Erlang/OTP SSL application","version":"7.3.3.1"},{"name":"stdlib","description":"ERTS CXC 138 10","version":"2.8"},{"name":"syntax_tools","description":"Syntax tools","version":"1.7"},{"name":"xmerl","description":"XML parser","version":"1.3.10"}],"contexts":[{"description":"RabbitMQ Management","path":"/","port":"15672"}],"log_file":"./el","sasl_log_file":"./sel","db_dir":"/home/binarin/mirantis-workspace/mgmt-no-sort/Mnesia.rabbit-dev@localhost","config_files":["/home/binarin/mirantis-workspace/mgmt-no-sort/vm.config"],"net_ticktime":60,"enabled_plugins":["rabbitmq_management"],"name":"rabbit-dev@localhost","type":"disc","running":true,"mem_used":52122128,"mem_used_details":{"rate":-21851.2},"fd_used":30,"fd_used_details":{"rate":0.4},"sockets_used":0,"sockets_used_details":{"rate":0.0},"proc_used":352,"proc_used_details":{"rate":0.0},"disk_free":51221843968,"disk_free_details":{"rate":0.0},"gc_num":582090,"gc_num_details":{"rate":6.4},"gc_bytes_reclaimed":14610041680,"gc_bytes_reclaimed_details":{"rate":145201.6},"context_switches":2476886,"context_switches_details":{"rate":34.0},"io_read_count":1,"io_read_count_details":{"rate":0.0},"io_read_bytes":1,"io_read_bytes_details":{"rate":0.0},"io_read_avg_time":0.249,"io_read_avg_time_details":{"rate":0.0},"io_write_count":0,"io_write_count_details":{"rate":0.0},"io_write_bytes":0,"io_write_bytes_details":{"rate":0.0},"io_write_avg_time":0.0,"io_write_avg_time_details":{"rate":0.0},"io_sync_count":0,"io_sync_count_details":{"rate":0.0},"io_sync_avg_time":0.0,"io_sync_avg_time_details":{"rate":0.0},"io_seek_count":0,"io_seek_count_details":{"rate":0.0},"io_seek_avg_time":0.0,"io_seek_avg_time_details":{"rate":0.0},"io_reopen_count":0,"io_reopen_count_details":{"rate":0.0},"mnesia_ram_tx_count":14,"mnesia_ram_tx_count_details":{"rate":0.0},"mnesia_disk_tx_count":0,"mnesia_disk_tx_count_details":{"rate":0.0},"msg_store_read_count":0,"msg_store_read_count_details":{"rate":0.0},"msg_store_write_count":0,"msg_store_write_count_details":{"rate":0.0},"queue_index_journal_write_count":0,"queue_index_journal_write_count_details":{"rate":0.0},"queue_index_write_count":0,"queue_index_write_count_details":{"rate":0.0},"queue_index_read_count":0,"queue_index_read_count_details":{"rate":0.0},"io_file_handle_open_attempt_count":10,"io_file_handle_open_attempt_count_details":{"rate":0.0},"io_file_handle_open_attempt_avg_time":0.037200000000000004,"io_file_handle_open_attempt_avg_time_details":{"rate":0.0},"cluster_links":[],"metrics_gc_queue_length":{"connection_closed":0,"channel_closed":0,"consumer_deleted":0,"exchange_deleted":0,"queue_deleted":0,"vhost_deleted":0,"node_node_deleted":0,"channel_consumer_deleted":0}}] -------------------------------------------------------------------------------- /testdata/nodes-3.7.0.bert: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbudde/rabbitmq_exporter/4dcae0489d93568daeb2c7c4e7d891bf8aab5df8/testdata/nodes-3.7.0.bert -------------------------------------------------------------------------------- /testdata/nodes-3.7.0.json: -------------------------------------------------------------------------------- 1 | [{"partitions":[],"os_pid":"21044","fd_total":1024,"sockets_total":829,"mem_limit":6679527424,"mem_alarm":false,"disk_free_limit":50000000,"disk_free_alarm":false,"proc_total":1048576,"rates_mode":"basic","uptime":48243,"run_queue":0,"processors":4,"exchange_types":[{"name":"topic","description":"AMQP topic exchange, as per the AMQP specification","enabled":true},{"name":"fanout","description":"AMQP fanout exchange, as per the AMQP specification","enabled":true},{"name":"headers","description":"AMQP headers exchange, as per the AMQP specification","enabled":true},{"name":"direct","description":"AMQP direct exchange, as per the AMQP specification","enabled":true}],"auth_mechanisms":[{"name":"PLAIN","description":"SASL PLAIN authentication mechanism","enabled":true},{"name":"AMQPLAIN","description":"QPid AMQPLAIN mechanism","enabled":true},{"name":"RABBIT-CR-DEMO","description":"RabbitMQ Demo challenge-response authentication mechanism","enabled":false}],"applications":[{"name":"amqp_client","description":"RabbitMQ AMQP Client","version":"3.7.0.milestone14+2.g98b59c2"},{"name":"asn1","description":"The Erlang ASN1 compiler version 4.0.2","version":"4.0.2"},{"name":"compiler","description":"ERTS CXC 138 10","version":"6.0.3"},{"name":"cowboy","description":"Small, fast, modular HTTP server.","version":"1.1.0"},{"name":"cowlib","description":"Support library for manipulating Web protocols.","version":"1.0.2"},{"name":"crypto","description":"CRYPTO","version":"3.6.3"},{"name":"goldrush","description":"Erlang event stream processor","version":"0.1.9"},{"name":"inets","description":"INETS CXC 138 49","version":"6.2.4"},{"name":"jsx","description":"a streaming, evented json parsing toolkit","version":"2.8.2"},{"name":"kernel","description":"ERTS CXC 138 10","version":"4.2"},{"name":"lager","description":"Erlang logging framework","version":"3.2.2"},{"name":"mnesia","description":"MNESIA CXC 138 12","version":"4.13.4"},{"name":"os_mon","description":"CPO CXC 138 46","version":"2.4"},{"name":"public_key","description":"Public key infrastructure","version":"1.1.1"},{"name":"rabbit","description":"RabbitMQ","version":"3.7.0.milestone14+12.gc8efc41"},{"name":"rabbit_common","description":"Modules shared by rabbitmq-server and rabbitmq-erlang-client","version":"3.7.0.milestone14+5.g3a808e7"},{"name":"rabbitmq_management","description":"RabbitMQ Management Console","version":"3.7.0.milestone14+13.g4b39832"},{"name":"rabbitmq_management_agent","description":"RabbitMQ Management Agent","version":"3.7.0.milestone13+11.gc2bc2cc"},{"name":"rabbitmq_web_dispatch","description":"RabbitMQ Web Dispatcher","version":"3.7.0.milestone12+1.g7fbb6eb"},{"name":"ranch","description":"Socket acceptor pool for TCP protocols.","version":"1.3.2"},{"name":"ranch_proxy_protocol","description":"Ranch Proxy Protocol Transport","version":"git"},{"name":"sasl","description":"SASL CXC 138 11","version":"2.7"},{"name":"ssl","description":"Erlang/OTP SSL application","version":"7.3.3.1"},{"name":"stdlib","description":"ERTS CXC 138 10","version":"2.8"},{"name":"syntax_tools","description":"Syntax tools","version":"1.7"},{"name":"xmerl","description":"XML parser","version":"1.3.10"}],"contexts":[{"description":"RabbitMQ Management","path":"/","port":"15672"}],"log_files":["/tmp/rabbitmq-test-instances/rabbit/log/rabbit.log","/tmp/rabbitmq-test-instances/rabbit/log/rabbit_upgrade.log"],"db_dir":"/tmp/rabbitmq-test-instances/rabbit/mnesia/rabbit","config_files":["/tmp/rabbitmq-test-instances/test.config"],"net_ticktime":60,"enabled_plugins":["amqp_client","cowboy","cowlib","rabbitmq_management","rabbitmq_management_agent","rabbitmq_web_dispatch"],"name":"rabbit@demandred","type":"disc","running":true,"mem_used":63606512,"mem_used_details":{"rate":22435.2},"fd_used":36,"fd_used_details":{"rate":0.0},"sockets_used":0,"sockets_used_details":{"rate":0.0},"proc_used":378,"proc_used_details":{"rate":0.0},"disk_free":39426990080,"disk_free_details":{"rate":0.0},"gc_num":4998,"gc_num_details":{"rate":9.8},"gc_bytes_reclaimed":308488464,"gc_bytes_reclaimed_details":{"rate":158065.6},"context_switches":68557,"context_switches_details":{"rate":25.2},"io_read_count":1,"io_read_count_details":{"rate":0.0},"io_read_bytes":1,"io_read_bytes_details":{"rate":0.0},"io_read_avg_time":0.028,"io_read_avg_time_details":{"rate":0.0},"io_write_count":0,"io_write_count_details":{"rate":0.0},"io_write_bytes":0,"io_write_bytes_details":{"rate":0.0},"io_write_avg_time":0.0,"io_write_avg_time_details":{"rate":0.0},"io_sync_count":0,"io_sync_count_details":{"rate":0.0},"io_sync_avg_time":0.0,"io_sync_avg_time_details":{"rate":0.0},"io_seek_count":0,"io_seek_count_details":{"rate":0.0},"io_seek_avg_time":0.0,"io_seek_avg_time_details":{"rate":0.0},"io_reopen_count":0,"io_reopen_count_details":{"rate":0.0},"mnesia_ram_tx_count":6,"mnesia_ram_tx_count_details":{"rate":0.0},"mnesia_disk_tx_count":13,"mnesia_disk_tx_count_details":{"rate":0.0},"msg_store_read_count":0,"msg_store_read_count_details":{"rate":0.0},"msg_store_write_count":0,"msg_store_write_count_details":{"rate":0.0},"queue_index_journal_write_count":0,"queue_index_journal_write_count_details":{"rate":0.0},"queue_index_write_count":0,"queue_index_write_count_details":{"rate":0.0},"queue_index_read_count":0,"queue_index_read_count_details":{"rate":0.0},"io_file_handle_open_attempt_count":10,"io_file_handle_open_attempt_count_details":{"rate":0.0},"io_file_handle_open_attempt_avg_time":0.0285,"io_file_handle_open_attempt_avg_time_details":{"rate":0.0},"cluster_links":[],"metrics_gc_queue_length":{"connection_closed":0,"channel_closed":0,"consumer_deleted":0,"exchange_deleted":0,"queue_deleted":0,"vhost_deleted":0,"node_node_deleted":0,"channel_consumer_deleted":0}}] -------------------------------------------------------------------------------- /testdata/overview-3.6.8.bert: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbudde/rabbitmq_exporter/4dcae0489d93568daeb2c7c4e7d891bf8aab5df8/testdata/overview-3.6.8.bert -------------------------------------------------------------------------------- /testdata/overview-3.6.8.json: -------------------------------------------------------------------------------- 1 | { 2 | "management_version": "3.6.8+1.g1dcb221", 3 | "rates_mode": "basic", 4 | "exchange_types": [ 5 | { 6 | "name": "topic", 7 | "description": "AMQP topic exchange, as per the AMQP specification", 8 | "enabled": true 9 | }, 10 | { 11 | "name": "direct", 12 | "description": "AMQP direct exchange, as per the AMQP specification", 13 | "enabled": true 14 | }, 15 | { 16 | "name": "headers", 17 | "description": "AMQP headers exchange, as per the AMQP specification", 18 | "enabled": true 19 | }, 20 | { 21 | "name": "fanout", 22 | "description": "AMQP fanout exchange, as per the AMQP specification", 23 | "enabled": true 24 | } 25 | ], 26 | "rabbitmq_version": "3.6.8+1.g1dcb221", 27 | "cluster_name": "rabbit-dev@demandred", 28 | "erlang_version": "18.3.4.4", 29 | "erlang_full_version": "Erlang/OTP 18 [erts-7.3.1.2] [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false]", 30 | "message_stats": { 31 | "publish": 1, 32 | "publish_details": { 33 | "rate": 0 34 | }, 35 | "confirm": 1, 36 | "confirm_details": { 37 | "rate": 0 38 | }, 39 | "return_unroutable": 0, 40 | "return_unroutable_details": { 41 | "rate": 0 42 | }, 43 | "disk_reads": 0, 44 | "disk_reads_details": { 45 | "rate": 0 46 | }, 47 | "disk_writes": 0, 48 | "disk_writes_details": { 49 | "rate": 0 50 | } 51 | }, 52 | "queue_totals": { 53 | "messages_ready": 1, 54 | "messages_ready_details": { 55 | "rate": 0 56 | }, 57 | "messages_unacknowledged": 0, 58 | "messages_unacknowledged_details": { 59 | "rate": 0 60 | }, 61 | "messages": 1, 62 | "messages_details": { 63 | "rate": 0 64 | } 65 | }, 66 | "object_totals": { 67 | "consumers": 0, 68 | "queues": 3, 69 | "exchanges": 8, 70 | "connections": 1, 71 | "channels": 1 72 | }, 73 | "statistics_db_event_queue": 0, 74 | "node": "rabbit-dev@localhost", 75 | "listeners": [ 76 | { 77 | "node": "rabbit-dev@localhost", 78 | "protocol": "amqp", 79 | "ip_address": "::", 80 | "port": 5672, 81 | "socket_opts": { 82 | "backlog": 128, 83 | "nodelay": true, 84 | "linger": [ 85 | true, 86 | 0 87 | ], 88 | "exit_on_close": false 89 | } 90 | }, 91 | { 92 | "node": "rabbit-dev@localhost", 93 | "protocol": "clustering", 94 | "ip_address": "::", 95 | "port": 42475, 96 | "socket_opts": [] 97 | }, 98 | { 99 | "node": "rabbit-dev@localhost", 100 | "protocol": "http", 101 | "ip_address": "::", 102 | "port": 15672, 103 | "socket_opts": { 104 | "port": 15672 105 | } 106 | } 107 | ], 108 | "contexts": [ 109 | { 110 | "node": "rabbit-dev@localhost", 111 | "description": "RabbitMQ Management", 112 | "path": "/", 113 | "port": "15672" 114 | } 115 | ] 116 | } -------------------------------------------------------------------------------- /testdata/overview-3.7.0.bert: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbudde/rabbitmq_exporter/4dcae0489d93568daeb2c7c4e7d891bf8aab5df8/testdata/overview-3.7.0.bert -------------------------------------------------------------------------------- /testdata/overview-3.7.0.json: -------------------------------------------------------------------------------- 1 | {"management_version":"3.7.0.milestone14+13.g4b39832","rates_mode":"basic","exchange_types":[{"name":"topic","description":"AMQP topic exchange, as per the AMQP specification","enabled":true},{"name":"fanout","description":"AMQP fanout exchange, as per the AMQP specification","enabled":true},{"name":"headers","description":"AMQP headers exchange, as per the AMQP specification","enabled":true},{"name":"direct","description":"AMQP direct exchange, as per the AMQP specification","enabled":true}],"rabbitmq_version":"3.7.0.milestone14+12.gc8efc41","cluster_name":"rabbit@demandred","erlang_version":"18.3.4.4","erlang_full_version":"Erlang/OTP 18 [erts-7.3.1.2] [source] [64-bit] [smp:4:4] [async-threads:64] [hipe] [kernel-poll:true]","message_stats":{"disk_reads":0,"disk_reads_details":{"rate":0.0},"disk_writes":0,"disk_writes_details":{"rate":0.0}},"queue_totals":{"messages":0,"messages_details":{"rate":0.0},"messages_ready":0,"messages_ready_details":{"rate":0.0},"messages_unacknowledged":0,"messages_unacknowledged_details":{"rate":0.0}},"object_totals":{"channels":1,"connections":1,"consumers":0,"exchanges":8,"queues":3},"statistics_db_event_queue":0,"node":"rabbit@demandred","listeners":[{"node":"rabbit@demandred","protocol":"amqp","ip_address":"::","port":5672,"socket_opts":{"backlog":128,"nodelay":true,"linger":[true,0],"exit_on_close":false}},{"node":"rabbit@demandred","protocol":"clustering","ip_address":"::","port":25672,"socket_opts":[]},{"node":"rabbit@demandred","protocol":"http","ip_address":"::","port":15672,"socket_opts":{"port":15672}}],"contexts":[{"node":"rabbit@demandred","description":"RabbitMQ Management","path":"/","port":"15672"}]} -------------------------------------------------------------------------------- /testdata/password_file: -------------------------------------------------------------------------------- 1 | password 2 | -------------------------------------------------------------------------------- /testdata/queue-max-length.bert: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbudde/rabbitmq_exporter/4dcae0489d93568daeb2c7c4e7d891bf8aab5df8/testdata/queue-max-length.bert -------------------------------------------------------------------------------- /testdata/queue-max-length.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "arguments": { 4 | "x-max-length-bytes": 99 5 | }, 6 | "auto_delete": false, 7 | "backing_queue_status": { 8 | "avg_ack_egress_rate": 0, 9 | "avg_ack_ingress_rate": 0, 10 | "avg_egress_rate": 0, 11 | "avg_ingress_rate": 0, 12 | "delta": [ 13 | "delta", 14 | "undefined", 15 | 0, 16 | 0, 17 | "undefined" 18 | ], 19 | "len": 0, 20 | "mode": "default", 21 | "next_seq_id": 0, 22 | "q1": 0, 23 | "q2": 0, 24 | "q3": 0, 25 | "q4": 0, 26 | "target_ram_count": "infinity" 27 | }, 28 | "consumer_utilisation": null, 29 | "consumers": 0, 30 | "durable": true, 31 | "effective_policy_definition": [], 32 | "exclusive": false, 33 | "exclusive_consumer_tag": null, 34 | "garbage_collection": { 35 | "fullsweep_after": 65535, 36 | "max_heap_size": 0, 37 | "min_bin_vheap_size": 46422, 38 | "min_heap_size": 233, 39 | "minor_gcs": 0 40 | }, 41 | "head_message_timestamp": null, 42 | "idle_since": "2019-02-28 19:57:53", 43 | "memory": 13872, 44 | "message_bytes": 0, 45 | "message_bytes_paged_out": 0, 46 | "message_bytes_persistent": 0, 47 | "message_bytes_ram": 0, 48 | "message_bytes_ready": 0, 49 | "message_bytes_unacknowledged": 0, 50 | "messages": 0, 51 | "messages_details": { 52 | "rate": 0 53 | }, 54 | "messages_paged_out": 0, 55 | "messages_persistent": 0, 56 | "messages_ram": 0, 57 | "messages_ready": 0, 58 | "messages_ready_details": { 59 | "rate": 0 60 | }, 61 | "messages_ready_ram": 0, 62 | "messages_unacknowledged": 0, 63 | "messages_unacknowledged_details": { 64 | "rate": 0 65 | }, 66 | "messages_unacknowledged_ram": 0, 67 | "name": "QueueWithMaxBytes99", 68 | "node": "rabbit@rabbitmq1", 69 | "operator_policy": null, 70 | "policy": null, 71 | "recoverable_slaves": null, 72 | "reductions": 3861, 73 | "reductions_details": { 74 | "rate": 0 75 | }, 76 | "state": "running", 77 | "vhost": "/" 78 | }, 79 | { 80 | "arguments": { 81 | "x-max-length": 55 82 | }, 83 | "auto_delete": false, 84 | "backing_queue_status": { 85 | "avg_ack_egress_rate": 0, 86 | "avg_ack_ingress_rate": 0, 87 | "avg_egress_rate": 0, 88 | "avg_ingress_rate": 0, 89 | "delta": [ 90 | "delta", 91 | "undefined", 92 | 0, 93 | 0, 94 | "undefined" 95 | ], 96 | "len": 0, 97 | "mode": "default", 98 | "next_seq_id": 0, 99 | "q1": 0, 100 | "q2": 0, 101 | "q3": 0, 102 | "q4": 0, 103 | "target_ram_count": "infinity" 104 | }, 105 | "consumer_utilisation": null, 106 | "consumers": 0, 107 | "durable": true, 108 | "effective_policy_definition": [], 109 | "exclusive": false, 110 | "exclusive_consumer_tag": null, 111 | "garbage_collection": { 112 | "fullsweep_after": 65535, 113 | "max_heap_size": 0, 114 | "min_bin_vheap_size": 46422, 115 | "min_heap_size": 233, 116 | "minor_gcs": 0 117 | }, 118 | "head_message_timestamp": null, 119 | "idle_since": "2019-02-28 19:56:48", 120 | "memory": 13864, 121 | "message_bytes": 0, 122 | "message_bytes_paged_out": 0, 123 | "message_bytes_persistent": 0, 124 | "message_bytes_ram": 0, 125 | "message_bytes_ready": 0, 126 | "message_bytes_unacknowledged": 0, 127 | "messages": 0, 128 | "messages_details": { 129 | "rate": 0 130 | }, 131 | "messages_paged_out": 0, 132 | "messages_persistent": 0, 133 | "messages_ram": 0, 134 | "messages_ready": 0, 135 | "messages_ready_details": { 136 | "rate": 0 137 | }, 138 | "messages_ready_ram": 0, 139 | "messages_unacknowledged": 0, 140 | "messages_unacknowledged_details": { 141 | "rate": 0 142 | }, 143 | "messages_unacknowledged_ram": 0, 144 | "name": "QueueWithMaxLength55", 145 | "node": "rabbit@rabbitmq1", 146 | "operator_policy": null, 147 | "policy": null, 148 | "recoverable_slaves": null, 149 | "reductions": 3873, 150 | "reductions_details": { 151 | "rate": 0 152 | }, 153 | "state": "running", 154 | "vhost": "/" 155 | } 156 | ] -------------------------------------------------------------------------------- /testdata/queues-3.6.8.bert: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbudde/rabbitmq_exporter/4dcae0489d93568daeb2c7c4e7d891bf8aab5df8/testdata/queues-3.6.8.bert -------------------------------------------------------------------------------- /testdata/queues-3.6.8.json: -------------------------------------------------------------------------------- 1 | [{"messages_details":{"rate":0.0},"messages":1,"messages_unacknowledged_details":{"rate":0.0},"messages_unacknowledged":0,"messages_ready_details":{"rate":0.0},"messages_ready":1,"reductions_details":{"rate":0.0},"reductions":20166,"message_stats":{"publish_details":{"rate":0.0},"publish":1},"node":"rabbit-dev@localhost","arguments":{},"exclusive":false,"auto_delete":false,"durable":false,"vhost":"/","name":"amq.gen---WBD642a0yMDq25x3DpvQ","message_bytes_paged_out":0,"messages_paged_out":0,"backing_queue_status":{"mode":"default","q1":0,"q2":0,"delta":["delta","undefined",0,0,"undefined"],"q3":0,"q4":1,"len":1,"target_ram_count":"infinity","next_seq_id":1,"avg_ingress_rate":0.00021650966423979837,"avg_egress_rate":0.0,"avg_ack_ingress_rate":0.0,"avg_ack_egress_rate":0.0},"head_message_timestamp":null,"message_bytes_persistent":0,"message_bytes_ram":13,"message_bytes_unacknowledged":0,"message_bytes_ready":13,"message_bytes":13,"messages_persistent":0,"messages_unacknowledged_ram":0,"messages_ready_ram":1,"messages_ram":1,"garbage_collection":{"minor_gcs":54,"fullsweep_after":65535,"min_heap_size":233,"min_bin_vheap_size":46422},"state":"running","recoverable_slaves":null,"memory":34712,"consumer_utilisation":null,"consumers":0,"exclusive_consumer_tag":null,"policy":null},{"messages_details":{"rate":0.0},"messages":0,"messages_unacknowledged_details":{"rate":0.0},"messages_unacknowledged":0,"messages_ready_details":{"rate":0.0},"messages_ready":0,"reductions_details":{"rate":0.0},"reductions":3958,"node":"rabbit-dev@localhost","arguments":{},"exclusive":false,"auto_delete":false,"durable":false,"vhost":"/","name":"amq.gen-AZWct5711RRwY4HXRtU-8w","message_bytes_paged_out":0,"messages_paged_out":0,"backing_queue_status":{"mode":"default","q1":0,"q2":0,"delta":["delta","undefined",0,0,"undefined"],"q3":0,"q4":0,"len":0,"target_ram_count":"infinity","next_seq_id":0,"avg_ingress_rate":0.0,"avg_egress_rate":0.0,"avg_ack_ingress_rate":0.0,"avg_ack_egress_rate":0.0},"head_message_timestamp":null,"message_bytes_persistent":0,"message_bytes_ram":0,"message_bytes_unacknowledged":0,"message_bytes_ready":0,"message_bytes":0,"messages_persistent":0,"messages_unacknowledged_ram":0,"messages_ready_ram":0,"messages_ram":0,"garbage_collection":{"minor_gcs":4,"fullsweep_after":65535,"min_heap_size":233,"min_bin_vheap_size":46422},"state":"running","recoverable_slaves":null,"consumers":0,"exclusive_consumer_tag":null,"policy":null,"consumer_utilisation":null,"idle_since":"2017-04-03 12:49:45","memory":42536},{"messages_details":{"rate":0.0},"messages":0,"messages_unacknowledged_details":{"rate":0.0},"messages_unacknowledged":0,"messages_ready_details":{"rate":0.0},"messages_ready":0,"reductions_details":{"rate":0.0},"reductions":3491,"node":"rabbit-dev@localhost","arguments":{},"exclusive":false,"auto_delete":false,"durable":false,"vhost":"/","name":"amq.gen-vPJMD2iLr8liaiqKRpSt_g","message_bytes_paged_out":0,"messages_paged_out":0,"backing_queue_status":{"mode":"default","q1":0,"q2":0,"delta":["delta","undefined",0,0,"undefined"],"q3":0,"q4":0,"len":0,"target_ram_count":"infinity","next_seq_id":0,"avg_ingress_rate":0.0,"avg_egress_rate":0.0,"avg_ack_ingress_rate":0.0,"avg_ack_egress_rate":0.0},"head_message_timestamp":null,"message_bytes_persistent":0,"message_bytes_ram":0,"message_bytes_unacknowledged":0,"message_bytes_ready":0,"message_bytes":0,"messages_persistent":0,"messages_unacknowledged_ram":0,"messages_ready_ram":0,"messages_ram":0,"garbage_collection":{"minor_gcs":3,"fullsweep_after":65535,"min_heap_size":233,"min_bin_vheap_size":46422},"state":"running","recoverable_slaves":null,"consumers":0,"exclusive_consumer_tag":null,"policy":null,"consumer_utilisation":null,"idle_since":"2017-04-03 12:49:39","memory":42536}] -------------------------------------------------------------------------------- /testdata/queues-3.7.0.bert: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbudde/rabbitmq_exporter/4dcae0489d93568daeb2c7c4e7d891bf8aab5df8/testdata/queues-3.7.0.bert -------------------------------------------------------------------------------- /testdata/queues-3.7.0.json: -------------------------------------------------------------------------------- 1 | [{"messages_details":{"rate":0.0},"messages":0,"messages_unacknowledged_details":{"rate":0.0},"messages_unacknowledged":0,"messages_ready_details":{"rate":0.0},"messages_ready":0,"reductions_details":{"rate":0.0},"reductions":4737,"node":"rabbit@demandred","arguments":{},"exclusive":false,"auto_delete":false,"durable":false,"vhost":"/","name":"amq.gen-1Nb3tSVlcjin_u_k_8Ur_A","message_bytes_paged_out":0,"messages_paged_out":0,"backing_queue_status":{"avg_ack_egress_rate":0.0,"avg_ack_ingress_rate":0.0,"avg_egress_rate":0.0,"avg_ingress_rate":0.0,"delta":["delta","undefined",0,0,"undefined"],"len":0,"mode":"default","next_seq_id":0,"q1":0,"q2":0,"q3":0,"q4":0,"target_ram_count":"infinity"},"head_message_timestamp":null,"message_bytes_persistent":0,"message_bytes_ram":0,"message_bytes_unacknowledged":0,"message_bytes_ready":0,"message_bytes":0,"messages_persistent":0,"messages_unacknowledged_ram":0,"messages_ready_ram":0,"messages_ram":0,"garbage_collection":{"minor_gcs":10,"fullsweep_after":65535,"min_heap_size":233,"min_bin_vheap_size":46422},"state":"running","recoverable_slaves":null,"consumers":0,"exclusive_consumer_tag":null,"effective_policy_definition":[],"operator_policy":null,"policy":null,"consumer_utilisation":null,"idle_since":"2017-04-05 14:32:42","memory":34632},{"messages_details":{"rate":0.0},"messages":0,"messages_unacknowledged_details":{"rate":0.0},"messages_unacknowledged":0,"messages_ready_details":{"rate":0.0},"messages_ready":0,"reductions_details":{"rate":94.0},"reductions":5208,"node":"rabbit@demandred","arguments":{},"exclusive":false,"auto_delete":false,"durable":false,"vhost":"/","name":"amq.gen-_uN9uFRYxbcWkNXY8GsCmQ","message_bytes_paged_out":0,"messages_paged_out":0,"backing_queue_status":{"avg_ack_egress_rate":0.0,"avg_ack_ingress_rate":0.0,"avg_egress_rate":0.0,"avg_ingress_rate":0.0,"delta":["delta","undefined",0,0,"undefined"],"len":0,"mode":"default","next_seq_id":0,"q1":0,"q2":0,"q3":0,"q4":0,"target_ram_count":"infinity"},"head_message_timestamp":null,"message_bytes_persistent":0,"message_bytes_ram":0,"message_bytes_unacknowledged":0,"message_bytes_ready":0,"message_bytes":0,"messages_persistent":0,"messages_unacknowledged_ram":0,"messages_ready_ram":0,"messages_ram":0,"garbage_collection":{"minor_gcs":11,"fullsweep_after":65535,"min_heap_size":233,"min_bin_vheap_size":46422},"state":"running","recoverable_slaves":null,"consumers":0,"exclusive_consumer_tag":null,"effective_policy_definition":[],"operator_policy":null,"policy":null,"consumer_utilisation":null,"idle_since":"2017-04-05 14:32:47","memory":34632},{"messages_details":{"rate":0.0},"messages":0,"messages_unacknowledged_details":{"rate":0.0},"messages_unacknowledged":0,"messages_ready_details":{"rate":0.0},"messages_ready":0,"reductions_details":{"rate":94.0},"reductions":5221,"node":"rabbit@demandred","arguments":{},"exclusive":false,"auto_delete":false,"durable":false,"vhost":"/","name":"amq.gen-qgYEo5W8l7zlozrznGMVdA","message_bytes_paged_out":0,"messages_paged_out":0,"backing_queue_status":{"avg_ack_egress_rate":0.0,"avg_ack_ingress_rate":0.0,"avg_egress_rate":0.0,"avg_ingress_rate":0.0,"delta":["delta","undefined",0,0,"undefined"],"len":0,"mode":"default","next_seq_id":0,"q1":0,"q2":0,"q3":0,"q4":0,"target_ram_count":"infinity"},"head_message_timestamp":null,"message_bytes_persistent":0,"message_bytes_ram":0,"message_bytes_unacknowledged":0,"message_bytes_ready":0,"message_bytes":0,"messages_persistent":0,"messages_unacknowledged_ram":0,"messages_ready_ram":0,"messages_ram":0,"garbage_collection":{"minor_gcs":11,"fullsweep_after":65535,"min_heap_size":233,"min_bin_vheap_size":46422},"state":"running","recoverable_slaves":null,"consumers":0,"exclusive_consumer_tag":null,"effective_policy_definition":[],"operator_policy":null,"policy":null,"consumer_utilisation":null,"idle_since":"2017-04-05 14:32:47","memory":34632}] -------------------------------------------------------------------------------- /testdata/username_file: -------------------------------------------------------------------------------- 1 | username 2 | -------------------------------------------------------------------------------- /testenv/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rabbitmq:3.13.6-management-alpine 2 | RUN rabbitmq-plugins enable --offline rabbitmq_shovel rabbitmq_shovel_management 3 | -------------------------------------------------------------------------------- /testenv/rabbit.go: -------------------------------------------------------------------------------- 1 | package testenv 2 | 3 | import ( 4 | "log" 5 | 6 | "time" 7 | 8 | "github.com/streadway/amqp" 9 | ) 10 | 11 | type rabbit struct { 12 | conn *amqp.Connection 13 | channel *amqp.Channel 14 | } 15 | 16 | func (r *rabbit) connect(url string) { 17 | conn, err := amqp.Dial(url) 18 | if err != nil { 19 | log.Fatalf("Failed to connect to RabbitMQ:%s", err) 20 | } 21 | r.conn = conn 22 | 23 | ch, err := conn.Channel() 24 | if err != nil { 25 | log.Fatalf("Failed to open a channel: %s", err) 26 | } 27 | r.channel = ch 28 | } 29 | 30 | func (r *rabbit) DeclareQueue(name string, durable bool) { 31 | _, err := r.channel.QueueDeclare( 32 | name, // name 33 | durable, // durable 34 | false, // delete when unused 35 | false, // exclusive 36 | false, // no-wait 37 | nil, // arguments 38 | ) 39 | if err != nil { 40 | log.Fatalf("Failed to declare a queue: %s", err) 41 | } 42 | } 43 | 44 | func (r *rabbit) SendMessageToQ(body string, routingKey string, timestamp *time.Time) { 45 | pub := amqp.Publishing{ 46 | ContentType: "text/plain", 47 | Body: []byte(body), 48 | } 49 | if timestamp != nil { 50 | pub.Timestamp = *timestamp 51 | } 52 | err := r.channel.Publish( 53 | "", // exchange 54 | routingKey, // routing key 55 | false, // mandatory 56 | false, // immediate 57 | pub) 58 | if err != nil { 59 | log.Fatalf("Failed to publish a message:%s . Error:%s", body, err) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /testenv/testenv.go: -------------------------------------------------------------------------------- 1 | // Package testenv provides a rabbitmq test environment in docker for a full set of integration tests. 2 | // Some usefull helper functions for rabbitmq interaction are included as well 3 | package testenv 4 | 5 | import ( 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "net/url" 10 | "strings" 11 | "testing" 12 | "time" 13 | 14 | "os" 15 | 16 | "log" 17 | 18 | dockertest "github.com/ory/dockertest/v3" 19 | ) 20 | 21 | // list of docker tags with rabbitmq versions 22 | const ( 23 | RabbitMQ3Latest = "3-management-alpine" 24 | ) 25 | 26 | // MaxWait is time before the docker setup will fail with timeout 27 | var MaxWait = 20 * time.Second 28 | 29 | // TestEnvironment contains all necessars 30 | type TestEnvironment struct { 31 | t *testing.T 32 | docker *dockertest.Pool 33 | resource *dockertest.Resource 34 | Rabbit rabbit 35 | } 36 | 37 | // NewEnvironment sets up a new environment. It will nlog fatal if something goes wrong 38 | func NewEnvironment(t *testing.T, dockerTag string) TestEnvironment { 39 | t.Helper() 40 | tenv := TestEnvironment{t: t} 41 | 42 | pool, err := dockertest.NewPool("") 43 | if err != nil { 44 | t.Fatalf("Could not connect to docker: %s", err) 45 | } 46 | 47 | tenv.docker = pool 48 | tenv.docker.MaxWait = MaxWait 49 | 50 | // pulls an image, creates a container based on it and runs it 51 | resource, err := pool.BuildAndRunWithOptions("./testenv/Dockerfile", &dockertest.RunOptions{Name: "rabbitmq-test", Hostname: "localtest"}) 52 | if err != nil { 53 | t.Fatalf("Could not start resource: %s", err) 54 | } 55 | if err := resource.Expire(120); err != nil { 56 | t.Fatalf("Could not set container expiration: %s", err) 57 | } 58 | 59 | tenv.resource = resource 60 | 61 | checkManagementWebsite := func() error { 62 | _, err := GetURL(tenv.ManagementURL(), 5*time.Second) 63 | return err 64 | } 65 | 66 | // exponential backoff-retry, because the application in the container might not be ready to accept connections yet 67 | if err := tenv.docker.Retry(checkManagementWebsite); err != nil { 68 | perr := tenv.docker.Purge(resource) 69 | log.Fatalf("Could not connect to docker: %s; Purge Error: %s", err, perr) 70 | } 71 | 72 | r := rabbit{} 73 | r.connect(tenv.AmqpURL(true)) 74 | tenv.Rabbit = r 75 | return tenv 76 | } 77 | 78 | func (tenv *TestEnvironment) getHost() string { 79 | url, err := url.Parse(tenv.docker.Client.Endpoint()) 80 | if err != nil { 81 | tenv.t.Fatal("url to docker host could was not parsed:", err) 82 | return "" 83 | } 84 | return url.Hostname() 85 | } 86 | 87 | // CleanUp removes the container. If not called the container will run forever 88 | func (tenv *TestEnvironment) CleanUp() { 89 | if err := tenv.docker.Purge(tenv.resource); err != nil { 90 | fmt.Fprintf(os.Stderr, "Could not purge resource: %s", err) 91 | } 92 | } 93 | 94 | // ManagementURL returns the full http url including username/password to the management api in the docker environment. 95 | // e.g. http://guest:guest@localhost:15672 96 | func (tenv *TestEnvironment) ManagementURL() string { 97 | return fmt.Sprintf("http://guest:guest@%s:%s", tenv.getHost(), tenv.resource.GetPort("15672/tcp")) 98 | } 99 | 100 | // AmqpURL returns the url to the rabbitmq server 101 | // e.g. amqp://localhost:5672 102 | func (tenv *TestEnvironment) AmqpURL(withCred bool) string { 103 | return fmt.Sprintf("amqp://%s:%s", tenv.getHost(), tenv.resource.GetPort("5672/tcp")) 104 | } 105 | 106 | // GetURL fetches the url. Will return error. 107 | func GetURL(url string, timeout time.Duration) (string, error) { 108 | maxTime := time.Duration(timeout) 109 | client := http.Client{ 110 | Timeout: maxTime, 111 | } 112 | resp, err := client.Get(url) 113 | if err != nil { 114 | return "", err 115 | } 116 | 117 | body, err := ioutil.ReadAll(resp.Body) 118 | resp.Body.Close() 119 | 120 | return string(body), err 121 | } 122 | 123 | // GetOrDie fetches the url. Will log.Fatal on error/timeout. 124 | func GetOrDie(url string, timeout time.Duration) string { 125 | body, err := GetURL(url, timeout) 126 | if err != nil { 127 | log.Fatalf("Failed to get url in time: %s", err) 128 | } 129 | return body 130 | } 131 | 132 | // MustSetPolicy adds a policy "name" to the default vhost. On error it will log.Fatal 133 | func (tenv *TestEnvironment) MustSetPolicy(name string, pattern string) { 134 | policy := fmt.Sprintf(`{"pattern":"%s", "definition": {"ha-mode":"all"}, "priority":0, "apply-to": "all"}`, pattern) 135 | url := fmt.Sprintf("%s/api/policies/%%2f/%s", tenv.ManagementURL(), name) 136 | 137 | client := &http.Client{} 138 | request, err := http.NewRequest("PUT", url, strings.NewReader(policy)) 139 | if err != nil { 140 | log.Fatal(fmt.Errorf("could not create NewRequest: %w", err)) 141 | } 142 | request.Header.Add("Content-Type", "application/json") 143 | request.ContentLength = int64(len(policy)) 144 | response, err := client.Do(request) 145 | if err != nil { 146 | log.Fatal(err) 147 | } else { 148 | response.Body.Close() 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/prometheus/client_golang/prometheus" 4 | 5 | var ( 6 | //Version of Rabbitmq Exporter is set during build. 7 | Version string 8 | //Revision of Rabbitmq Exporter is set during build. 9 | Revision string 10 | //Branch of Rabbitmq Exporter is set during build. 11 | Branch string 12 | //BuildDate of Rabbitmq Exporter is set during build. 13 | BuildDate string 14 | ) 15 | 16 | //BuildInfo is a metric with a constant '1' value labeled by version, revision, branch and build date on which the rabbitmq_exporter was built 17 | var BuildInfo *prometheus.GaugeVec 18 | 19 | func init() { 20 | BuildInfo = newBuildInfo() 21 | } 22 | 23 | func newBuildInfo() *prometheus.GaugeVec { 24 | metric := prometheus.NewGaugeVec( 25 | prometheus.GaugeOpts{ 26 | Name: "rabbitmq_exporter_build_info", 27 | Help: "A metric with a constant '1' value labeled by version, revision, branch and build date on which the rabbitmq_exporter was built.", 28 | }, 29 | []string{"version", "revision", "branch", "builddate"}, 30 | ) 31 | metric.WithLabelValues(Version, Revision, Branch, BuildDate).Set(1) 32 | return metric 33 | } 34 | --------------------------------------------------------------------------------