├── .flake8 ├── requirements.txt ├── pyproject.toml ├── .gitignore ├── mypy.ini ├── test-bitcoin.conf ├── Dockerfile ├── Makefile ├── .pre-commit-config.yaml ├── compose.yaml ├── LICENSE ├── README.md ├── CHANGELOG.md ├── bitcoind-monitor.py └── dashboard └── bitcoin-grafana.json /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 100 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | prometheus_client 2 | python-bitcoinlib 3 | riprova 4 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 100 3 | target_version = ['py312'] 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .venv/ 2 | venv/ 3 | .mypy_cache/ 4 | 5 | # python 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # vim 11 | *.swn 12 | *.swo 13 | *.swp 14 | *~ 15 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | 3 | [mypy-bitcoin.rpc] 4 | ignore_missing_imports = True 5 | 6 | [mypy-prometheus_client] 7 | ignore_missing_imports = True 8 | 9 | [mypy-riprova] 10 | ignore_missing_imports = True 11 | -------------------------------------------------------------------------------- /test-bitcoin.conf: -------------------------------------------------------------------------------- 1 | # server portion 2 | server=1 3 | rpcauth=alice:8bfdd22c39bc8839b6c8e73659f4bbca$f46a37d99b29771a4b42dec9c7614dabe5400deff1ced0e98bb3d4aa643bed5e 4 | rpcbind=0.0.0.0 5 | rpcport=8332 6 | rpcallowip=0.0.0.0/0 7 | 8 | # client portion 9 | rpcconnect=bitcoind 10 | rpcuser=alice 11 | rpcpassword=password=123/&z!=@e 12 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM docker.io/library/python:3.13-alpine 2 | 3 | LABEL org.opencontainers.image.title="bitcoin-prometheus-exporter" 4 | LABEL org.opencontainers.image.description="Prometheus exporter for bitcoin nodes" 5 | 6 | # Dependencies for python-bitcoinlib and sanity check. 7 | RUN apk --no-cache add \ 8 | binutils \ 9 | libressl-dev && \ 10 | python -c "import ctypes, ctypes.util; ctypes.cdll.LoadLibrary('/usr/lib/libssl.so')" 11 | 12 | RUN pip install --no-cache-dir \ 13 | prometheus_client \ 14 | python-bitcoinlib \ 15 | riprova 16 | 17 | RUN mkdir -p /monitor 18 | COPY --chmod=555 ./bitcoind-monitor.py /monitor 19 | 20 | USER nobody 21 | 22 | CMD ["/monitor/bitcoind-monitor.py"] 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION ?= v0.9.0 2 | REMOTE ?= origin 3 | DOCKER_REPO ?= jvstein/bitcoin-prometheus-exporter 4 | PLATFORMS ?= linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/ppc64le 5 | LATEST ?= latest 6 | 7 | # Builds for local platform only. 8 | docker: 9 | docker buildx build \ 10 | --pull \ 11 | --load \ 12 | -t $(DOCKER_REPO):$(LATEST) \ 13 | -t $(DOCKER_REPO):$(VERSION) \ 14 | $(PWD) 15 | 16 | # Builds and pushes for all platforms. 17 | docker-release: 18 | docker buildx build \ 19 | --pull \ 20 | --push \ 21 | --platform $(PLATFORMS) \ 22 | -t $(DOCKER_REPO):$(LATEST) \ 23 | -t $(DOCKER_REPO):$(VERSION) \ 24 | $(PWD) 25 | 26 | git-tag: 27 | git tag -s $(VERSION) -m "Release $(VERSION)" 28 | 29 | git-tag-push: 30 | git push --tags $(REMOTE) 31 | git push $(REMOTE) main 32 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | default_language_version: 3 | python: python3.12 4 | repos: 5 | - repo: https://github.com/myint/autoflake 6 | rev: v2.3.1 7 | hooks: 8 | - id: autoflake 9 | args: ['--in-place', '--remove-all-unused-imports', '--remove-unused-variables'] 10 | # Only using py3-plus to avoid killing python3.5 support with f-strings. 11 | - repo: https://github.com/asottile/pyupgrade 12 | rev: v3.19.0 13 | hooks: 14 | - id: pyupgrade 15 | args: ['--py3-plus'] 16 | - repo: https://github.com/psf/black 17 | rev: 24.10.0 18 | hooks: 19 | - id: black 20 | - repo: https://github.com/PyCQA/flake8 21 | rev: 7.1.1 22 | hooks: 23 | - id: flake8 24 | args: ['--config=.flake8'] 25 | - repo: https://github.com/pre-commit/mirrors-mypy 26 | rev: v1.13.0 27 | hooks: 28 | - id: mypy 29 | -------------------------------------------------------------------------------- /compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | bitcoind: 3 | # DO NOT USE THIS IMAGE EXCEPT FOR TESTING! 4 | image: jvstein/bitcoin-test-node:latest 5 | command: 6 | - bitcoind 7 | - -printtoconsole 8 | - -conf=/etc/bitcoin/bitcoin.conf 9 | volumes: 10 | - ./test-bitcoin.conf:/etc/bitcoin/bitcoin.conf 11 | 12 | exporter: 13 | image: jvstein/bitcoin-prometheus-exporter:latest 14 | build: . 15 | ports: 16 | - "9332:9332" 17 | environment: 18 | BITCOIN_RPC_HOST: bitcoind 19 | BITCOIN_RPC_USER: "alice" 20 | BITCOIN_RPC_PASSWORD: "password=123/&z!=@e" 21 | #BITCOIN_CONF_PATH: /etc/bitcoin/bitcoin.conf 22 | REFRESH_SECONDS: 1 23 | LOG_LEVEL: "INFO" 24 | depends_on: 25 | - bitcoind 26 | #volumes: 27 | # For explicit config file path (with BITCOIN_CONF_PATH above). 28 | # - ./test-bitcoin.conf:/etc/bitcoin/bitcoin.conf 29 | # For default bitcoin config location (nobody user). 30 | # - ./test-bitcoin.conf:/.bitcoin/bitcoin.conf 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright 2018 Kevin M. Gallagher 4 | Copyright 2019-2024 Jeff Stein 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its contributors 17 | may be used to endorse or promote products derived from this software without 18 | specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 21 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 22 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bitcoin Core Prometheus Exporter 2 | 3 | A [Prometheus] exporter for [Bitcoin Core] nodes written in python and packaged for running as a container. 4 | 5 | A rudimentary Grafana [dashboard] is available in the [`dashboard/bitcoin-grafana.json`](dashboard/bitcoin-grafana.json) 6 | file. 7 | 8 | The main script is a modified version of [`bitcoin-monitor.py`][source-gist], updated to remove the need for the 9 | `bitcoin-cli` binary, packaged into a [Docker image][docker-image], and expanded to export additional metrics. 10 | 11 | [Bitcoin Core]: https://github.com/bitcoin/bitcoin 12 | [Prometheus]: https://github.com/prometheus/prometheus 13 | [docker-image]: https://hub.docker.com/r/jvstein/bitcoin-prometheus-exporter 14 | 15 | [source-gist]: https://gist.github.com/ageis/a0623ae6ec9cfc72e5cb6bde5754ab1f 16 | [python-bitcoinlib]: https://github.com/petertodd/python-bitcoinlib 17 | [dashboard]: https://grafana.com/grafana/dashboards/11274 18 | 19 | # Run the container 20 | ``` 21 | docker run \ 22 | --name=bitcoin-exporter \ 23 | -p 9332:9332 \ 24 | -e BITCOIN_RPC_HOST=bitcoin-node \ 25 | -e BITCOIN_RPC_USER=alice \ 26 | -e BITCOIN_RPC_PASSWORD=DONT_USE_THIS_YOU_WILL_GET_ROBBED_8ak1gI25KFTvjovL3gAM967mies3E= \ 27 | jvstein/bitcoin-prometheus-exporter:v0.9.0 28 | ``` 29 | 30 | ## Basic Testing 31 | There's a [`docker-compose.yml`](docker-compose.yml) file in the repository that references a test bitcoin node. To 32 | test changes to the exporter in docker, run the following commands. 33 | 34 | ``` 35 | docker-compose down 36 | docker-compose build 37 | docker-compose up 38 | ``` 39 | 40 | If you see a lot of `ConnectionRefusedError` errors, run `chmod og+r test-bitcoin.conf`. 41 | 42 | # [Change Log](CHANGELOG.md) 43 | See the [`CHANGELOG.md`](CHANGELOG.md) file for changes. 44 | 45 | # Other Exporters 46 | - [Rust port](https://github.com/eburghar/bitcoin-exporter) 47 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | Changes to the project. 3 | 4 | ## [Unreleased] 5 | 6 | ### Fixed 7 | - Fix log level used for exception ([#42][issue-42]) 8 | 9 | [issue-42]: https://github.com/jvstein/bitcoin-prometheus-exporter/issues/42 10 | 11 | 12 | ## [0.9.0] - 2025-09-24 13 | 14 | ### Changed 15 | - Exporter now exits cleanly on SIGTERM. SIGINT now exits with a non-zero code. ([#38][pr-38]) 16 | - Username and password are now properly handled when they contain URL reserved characters. ([#40][issue-40]) 17 | 18 | [pr-38]: https://github.com/jvstein/bitcoin-prometheus-exporter/pull/38 19 | [issue-40]: https://github.com/jvstein/bitcoin-prometheus-exporter/issues/40 20 | 21 | 22 | ## [0.8.0] - 2025-06-25 23 | 24 | BREAKING CHANGE: The `bitcoin_ban_created` and `bitcoin_banned_until` metrics are now disabled by default. Re-enable, 25 | if desired, using `BAN_ADDRESS_METRICS=true`. 26 | 27 | ### Changed 28 | - Update docker base image to be fully qualifed for better podman support ([#25][pr-25]) 29 | - Add metric for minimum fee ([#31][pr-31]) 30 | - Upgrade to Python 3.13 31 | - Remove support for s390x docker image 32 | - Disable per-address metrics for banned peers. Add `bitcoin_banned_peers` metric for summary information. ([#13][issue-13]) 33 | - Remove unused `RETRIES` variable ([#36][issue-36]) 34 | 35 | [pr-25]: https://github.com/jvstein/bitcoin-prometheus-exporter/pull/25 36 | [pr-31]: https://github.com/jvstein/bitcoin-prometheus-exporter/pull/31 37 | [issue-13]: https://github.com/jvstein/bitcoin-prometheus-exporter/issues/13 38 | [issue-36]: https://github.com/jvstein/bitcoin-prometheus-exporter/issues/36 39 | 40 | 41 | ## [0.7.0] - 2022-01-14 42 | 43 | **BREAKING CHANGE: Default port number has changed to 9332! Use `METRICS_PORT=8334` to use old port.** 44 | 45 | ### Added 46 | - Add `bitcoin_rpc_active` stat for number of active RPC calls. 47 | - [Source](https://github.com/EchterAgo/bitcoin-prometheus-exporter/commit/cc2a804e214556414c7184830166242c34d42457) 48 | - Add `bitcoin_txcount` stat for total number of transactions since the genesis block. 49 | - [Source 1](https://github.com/EchterAgo/bitcoin-prometheus-exporter/commit/b368138574641e4e26a8a2dfc8be6eede82f4a73) 50 | - [Source 2](https://github.com/EchterAgo/bitcoin-prometheus-exporter/commit/a9a6b250f463906c1b1f9446d9a689c78c4add6d) 51 | - Support for Bitcoin Cash nodes ([#22][pr-22]). 52 | 53 | [pr-22]: https://github.com/jvstein/bitcoin-prometheus-exporter/pull/22 54 | 55 | ### Changed 56 | - Default port changed from `8334` to `9332` to avoid conflicts with Bitcoin Tor port. 57 | - Update metrics on HTTP request, instead of timer ([#12][issue-12]). 58 | - [Source 1](https://github.com/EchterAgo/bitcoin-prometheus-exporter/commit/c8382240b7a931503dfdd4c8cf89a8415326caf6) 59 | - [Source 2](https://github.com/EchterAgo/bitcoin-prometheus-exporter/commit/89212072386307fcb6a9f062ee7f958a266b1075) 60 | - Improve performance of block statistics. 61 | - [Source](https://github.com/EchterAgo/bitcoin-prometheus-exporter/commit/9c018bf081bfdc604af03d8dedd125197401b2de) 62 | - The number of blocks for the `bitcoin_hashps` metrics is now configurable via `HASHPS_BLOCKS` ([#22][pr-22]). 63 | 64 | [issue-12]: https://github.com/jvstein/bitcoin-prometheus-exporter/issues/12 65 | 66 | 67 | ## [0.6.0] - 2021-06-05 68 | 69 | ### Added 70 | - Support changing bind address with `METRICS_ADDR` environment variable ([#11][pr-11]). 71 | - Add `requirements.txt` file. 72 | - Set default `bad_reason` to "manually added" to support Bitcoin Core 0.20.1 ([#16][pr-16]). 73 | - Remove premature URL encoding of bitcoind rpc credentials ([#18][pr-18]). 74 | - New metrics for Bitcoin Core 0.21.0. 75 | - Support for ARM docker image builds via `buildx`. 76 | 77 | [pr-11]: https://github.com/jvstein/bitcoin-prometheus-exporter/pull/11 78 | [pr-16]: https://github.com/jvstein/bitcoin-prometheus-exporter/pull/16 79 | [pr-18]: https://github.com/jvstein/bitcoin-prometheus-exporter/pull/18 80 | 81 | ### Fixed 82 | - Fix port number in README ([#17][pr-17]). 83 | 84 | [pr-17]: https://github.com/jvstein/bitcoin-prometheus-exporter/pull/17 85 | 86 | ## Changed 87 | - Modify type annotations to run on python 3.5. 88 | 89 | 90 | ## [0.5.0] - 2020-02-10 91 | 92 | ### Fixed 93 | - Avoid crash on `socket.timeout` errors. Retry the request in that case. 94 | 95 | ### Changed 96 | - Switch to python 3.8 and alpine for base image. 97 | - Update docker container to use `nobody` instead of default `root` account. 98 | - Update shebang to use PATH ([#6][pr-6]). 99 | - Support loading bitcoin config from `BITCOIN_CONF_PATH` environment variable ([#7][pr-7]). 100 | - Reuse the `Proxy` RPC client to avoid repeatedly looking for config file. 101 | - Config file examples in the docker-compose file. 102 | - Retry on `ConnectionError`, not just `ConnectionRefusedError` ([#9][pr-9]). 103 | - Pass `TIMEOUT` environment value to bitcoin client as well as retry library. 104 | - Rely on python-bitcoinlib to handle bitcoin config location detection ([#10][pr-10]). 105 | 106 | [pr-6]: https://github.com/jvstein/bitcoin-prometheus-exporter/pull/6 107 | [pr-7]: https://github.com/jvstein/bitcoin-prometheus-exporter/pull/7 108 | [pr-9]: https://github.com/jvstein/bitcoin-prometheus-exporter/pull/9 109 | [pr-10]: https://github.com/jvstein/bitcoin-prometheus-exporter/pull/10 110 | 111 | 112 | ## [0.4.0] - 2020-01-05 113 | 114 | ### Added 115 | - New counter metric `bitcoin_exporter_process_time_total` for time spent refreshing the metrics. 116 | - New `bitcoin_verification_progress` metric to track chain verification progress ([#5][pr-5]). 117 | - Use `logging` for output messages and improve level of output for errors ([#4][issue-4]). 118 | - Add docker-compose config with basic setup for testing regressions. 119 | 120 | [pr-5]: https://github.com/jvstein/bitcoin-prometheus-exporter/pull/5 121 | [issue-4]: https://github.com/jvstein/bitcoin-prometheus-exporter/issues/4 122 | 123 | ### Changed 124 | - Retry failed RPC calls with exponential timeout using riprova and keep track of retry exceptions using new 125 | `bitcoin_exporter_errors` metric. 126 | - Improved error message when credentials are incorrect. 127 | - Make smartfee metrics configurable using `SMARTFEE_BLOCKS` environment variable. 128 | - Update script shebang to use PATH ([#6][pr-6]) 129 | - Prefer the `$HOME/.bitcoin/bitcoin.conf` file over `BITCOIN_RPC_HOST`, `BITCOIN_RPC_USER`, ... if it exists ([#7][pr-7]). 130 | - Add pre-commit hooks for catching style and code issues. 131 | - Added type annotations and no longer attempting to support less than Python 3.7. 132 | 133 | [pr-6]: https://github.com/jvstein/bitcoin-prometheus-exporter/pull/6 134 | [pr-7]: https://github.com/jvstein/bitcoin-prometheus-exporter/pull/7 135 | 136 | ### Fixed 137 | - Avoid crashing on node restart by ignoring `bitcoin.rpc.InWarmupError` exception. 138 | - Prevent KeyError when smartfee values are not calculable ([#2][issue-2]). 139 | - Fix duplicate sleep call introduced in 5d83f9e ([#3][issue-3]). 140 | 141 | [issue-2]: https://github.com/jvstein/bitcoin-prometheus-exporter/issues/2 142 | [issue-3]: https://github.com/jvstein/bitcoin-prometheus-exporter/issues/3 143 | 144 | 145 | ## [0.3.0] - 2019-11-25 146 | 147 | ### Added 148 | - Include explicit 3-clause BSD LICENSE. 149 | - New `bitcoin_latest_block_weight` and `bitcoin_latest_block_height` metrics using value from [getblock]. 150 | - Include my rudimentary dashboard. 151 | 152 | ### Changed 153 | - Update `bitcoin_latest_block_txs` to use the `nTx` value returned by [getblock] instead of `len(tx)`. No observed change in value. 154 | 155 | ### Removed 156 | - Dead code cleanup (`find_bitcoin_cli` and `BITCOIN_CLI_PATH`). 157 | 158 | [getblock]: https://bitcoincore.org/en/doc/0.18.0/rpc/blockchain/getblock/ 159 | 160 | 161 | ## [0.2.0] - 2019-10-20 162 | 163 | ### Added 164 | - New metrics from [getmemoryinfo] with the `bitcoin_meminfo_` prefix. 165 | - `bitcoin_size_on_disk` metric with data from [getblockchaininfo]. 166 | 167 | [getmemoryinfo]: https://bitcoincore.org/en/doc/0.18.0/rpc/control/getmemoryinfo/ 168 | [getblockchaininfo]: https://bitcoincore.org/en/doc/0.18.0/rpc/blockchain/getblockchaininfo/ 169 | 170 | ### Changed 171 | - Move changelog to separate file. 172 | - Make binding port configurable using `METRICS_PORT` environment variable. 173 | 174 | ### Fixed 175 | - Fix example commands in README. 176 | - Handle SIGTERM gracefully to avoid later SIGKILL. 177 | 178 | 179 | ## [0.1.0] - 2019-07-27 180 | 181 | Initial release of project. Changes are relative to the [`bitcoin-monitor.md`][source-gist] gist, which was commited 182 | as-is in the first commit. 183 | 184 | [source-gist]: https://gist.github.com/ageis/a0623ae6ec9cfc72e5cb6bde5754ab1f 185 | 186 | ### Added 187 | - Packaged for docker and modified to pull settings from environment variables. 188 | - `bitcoin_hashps_1` and `bitcoin_hashps_neg1` for estimated hash rates associated with only the last block and for all blocks with the same difficulty. 189 | - `bitcoin_est_smart_fee_*` metrics for estimated fee per kilobyte for confirmation within a number of blocks. 190 | - `bitcoin_latest_block_value` for the transaction value of the last block. 191 | - `bitcoin_server_version` and `bitcoin_protocol_version` to track upgrades of the bitcoin server. 192 | - `bitcoin_mempool_usage` metric. 193 | - `bitcoin_ban_created` and `bitcoin_banned_until` to track peer bans. 194 | 195 | ### Changed 196 | - Use RPC calls using [python-bitcoinlib] instead of relying on the `bitcoin-cli` binary. 197 | - Remove need for `txindex=` to be set on the bitcoin server. Transactions are now pulled using the `getblock` call by setting `verbosity=2`. 198 | 199 | [python-bitcoinlib]: https://github.com/petertodd/python-bitcoinlib 200 | 201 | [Unreleased]: https://github.com/jvstein/bitcoin-prometheus-exporter/compare/v0.9.0...HEAD 202 | [0.9.0]: https://github.com/jvstein/bitcoin-prometheus-exporter/compare/v0.8.0...v0.9.0 203 | [0.8.0]: https://github.com/jvstein/bitcoin-prometheus-exporter/compare/v0.7.0...v0.8.0 204 | [0.7.0]: https://github.com/jvstein/bitcoin-prometheus-exporter/compare/v0.6.0...v0.7.0 205 | [0.6.0]: https://github.com/jvstein/bitcoin-prometheus-exporter/compare/v0.5.0...v0.6.0 206 | [0.5.0]: https://github.com/jvstein/bitcoin-prometheus-exporter/compare/v0.4.0...v0.5.0 207 | [0.4.0]: https://github.com/jvstein/bitcoin-prometheus-exporter/compare/v0.3.0...v0.4.0 208 | [0.3.0]: https://github.com/jvstein/bitcoin-prometheus-exporter/compare/v0.2.0...v0.3.0 209 | [0.2.0]: https://github.com/jvstein/bitcoin-prometheus-exporter/compare/v0.1.0...v0.2.0 210 | [0.1.0]: https://github.com/jvstein/bitcoin-prometheus-exporter/compare/5abac0a8c58a9c0a79c6493b3273e04fda7b050f...v0.1.0 211 | -------------------------------------------------------------------------------- /bitcoind-monitor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # bitcoind-monitor.py 3 | # 4 | # An exporter for Prometheus and Bitcoin Core. 5 | # 6 | # Copyright 2018 Kevin M. Gallagher 7 | # Copyright 2019-2024 Jeff Stein 8 | # 9 | # Published at https://github.com/jvstein/bitcoin-prometheus-exporter 10 | # Licensed under BSD 3-clause (see LICENSE). 11 | # 12 | # Dependency licenses (retrieved 2020-05-31): 13 | # prometheus_client: Apache 2.0 14 | # python-bitcoinlib: LGPLv3 15 | # riprova: MIT 16 | 17 | import base64 18 | import json 19 | import logging 20 | import time 21 | import os 22 | import signal 23 | import sys 24 | import socket 25 | from decimal import Decimal 26 | from datetime import datetime 27 | from functools import lru_cache 28 | from typing import Any 29 | from typing import Dict 30 | from typing import List 31 | from typing import Union 32 | from wsgiref.simple_server import make_server 33 | 34 | import riprova 35 | 36 | from bitcoin.rpc import JSONRPCError, InWarmupError, Proxy 37 | from prometheus_client import make_wsgi_app, Gauge, Counter 38 | 39 | 40 | logger = logging.getLogger("bitcoin-exporter") 41 | 42 | # Create Prometheus metrics to track bitcoind stats. 43 | BITCOIN_BLOCKS = Gauge("bitcoin_blocks", "Block height") 44 | BITCOIN_DIFFICULTY = Gauge("bitcoin_difficulty", "Difficulty") 45 | BITCOIN_PEERS = Gauge("bitcoin_peers", "Number of peers") 46 | BITCOIN_CONN_IN = Gauge("bitcoin_conn_in", "Number of connections in") 47 | BITCOIN_CONN_OUT = Gauge("bitcoin_conn_out", "Number of connections out") 48 | 49 | BITCOIN_HASHPS_GAUGES = {} # type: Dict[int, Gauge] 50 | BITCOIN_ESTIMATED_SMART_FEE_GAUGES = {} # type: Dict[int, Gauge] 51 | 52 | BITCOIN_WARNINGS = Counter("bitcoin_warnings", "Number of network or blockchain warnings detected") 53 | BITCOIN_UPTIME = Gauge("bitcoin_uptime", "Number of seconds the Bitcoin daemon has been running") 54 | 55 | BITCOIN_MEMINFO_USED = Gauge("bitcoin_meminfo_used", "Number of bytes used") 56 | BITCOIN_MEMINFO_FREE = Gauge("bitcoin_meminfo_free", "Number of bytes available") 57 | BITCOIN_MEMINFO_TOTAL = Gauge("bitcoin_meminfo_total", "Number of bytes managed") 58 | BITCOIN_MEMINFO_LOCKED = Gauge("bitcoin_meminfo_locked", "Number of bytes locked") 59 | BITCOIN_MEMINFO_CHUNKS_USED = Gauge("bitcoin_meminfo_chunks_used", "Number of allocated chunks") 60 | BITCOIN_MEMINFO_CHUNKS_FREE = Gauge("bitcoin_meminfo_chunks_free", "Number of unused chunks") 61 | 62 | BITCOIN_MEMPOOL_BYTES = Gauge("bitcoin_mempool_bytes", "Size of mempool in bytes") 63 | BITCOIN_MEMPOOL_SIZE = Gauge( 64 | "bitcoin_mempool_size", "Number of unconfirmed transactions in mempool" 65 | ) 66 | BITCOIN_MEMPOOL_USAGE = Gauge("bitcoin_mempool_usage", "Total memory usage for the mempool") 67 | BITCOIN_MEMPOOL_MINFEE = Gauge( 68 | "bitcoin_mempool_minfee", "Minimum fee rate in BTC/kB for tx to be accepted in mempool" 69 | ) 70 | BITCOIN_MEMPOOL_UNBROADCAST = Gauge( 71 | "bitcoin_mempool_unbroadcast", "Number of transactions waiting for acknowledgment" 72 | ) 73 | 74 | BITCOIN_LATEST_BLOCK_HEIGHT = Gauge( 75 | "bitcoin_latest_block_height", "Height or index of latest block" 76 | ) 77 | BITCOIN_LATEST_BLOCK_WEIGHT = Gauge( 78 | "bitcoin_latest_block_weight", "Weight of latest block according to BIP 141" 79 | ) 80 | BITCOIN_LATEST_BLOCK_SIZE = Gauge("bitcoin_latest_block_size", "Size of latest block in bytes") 81 | BITCOIN_LATEST_BLOCK_TXS = Gauge( 82 | "bitcoin_latest_block_txs", "Number of transactions in latest block" 83 | ) 84 | 85 | BITCOIN_TXCOUNT = Gauge("bitcoin_txcount", "Number of TX since the genesis block") 86 | 87 | BITCOIN_NUM_CHAINTIPS = Gauge("bitcoin_num_chaintips", "Number of known blockchain branches") 88 | 89 | BITCOIN_TOTAL_BYTES_RECV = Gauge("bitcoin_total_bytes_recv", "Total bytes received") 90 | BITCOIN_TOTAL_BYTES_SENT = Gauge("bitcoin_total_bytes_sent", "Total bytes sent") 91 | 92 | BITCOIN_LATEST_BLOCK_INPUTS = Gauge( 93 | "bitcoin_latest_block_inputs", "Number of inputs in transactions of latest block" 94 | ) 95 | BITCOIN_LATEST_BLOCK_OUTPUTS = Gauge( 96 | "bitcoin_latest_block_outputs", "Number of outputs in transactions of latest block" 97 | ) 98 | BITCOIN_LATEST_BLOCK_VALUE = Gauge( 99 | "bitcoin_latest_block_value", "Bitcoin value of all transactions in the latest block" 100 | ) 101 | BITCOIN_LATEST_BLOCK_FEE = Gauge( 102 | "bitcoin_latest_block_fee", "Total fee to process the latest block" 103 | ) 104 | 105 | BAN_ADDRESS_METRICS = os.environ.get("BAN_ADDRESS_METRICS", "false").lower() == "true" 106 | BITCOIN_BANNED_PEERS = Gauge("bitcoin_banned_peers", "Number of peers that have been banned") 107 | BITCOIN_BAN_CREATED = None 108 | BITCOIN_BANNED_UNTIL = None 109 | if BAN_ADDRESS_METRICS: 110 | BITCOIN_BAN_CREATED = Gauge( 111 | "bitcoin_ban_created", "Time the ban was created", labelnames=["address", "reason"] 112 | ) 113 | BITCOIN_BANNED_UNTIL = Gauge( 114 | "bitcoin_banned_until", "Time the ban expires", labelnames=["address", "reason"] 115 | ) 116 | 117 | BITCOIN_SERVER_VERSION = Gauge("bitcoin_server_version", "The server version") 118 | BITCOIN_PROTOCOL_VERSION = Gauge("bitcoin_protocol_version", "The protocol version of the server") 119 | 120 | BITCOIN_SIZE_ON_DISK = Gauge("bitcoin_size_on_disk", "Estimated size of the block and undo files") 121 | 122 | BITCOIN_VERIFICATION_PROGRESS = Gauge( 123 | "bitcoin_verification_progress", "Estimate of verification progress [0..1]" 124 | ) 125 | 126 | BITCOIN_RPC_ACTIVE = Gauge("bitcoin_rpc_active", "Number of RPC calls being processed") 127 | 128 | EXPORTER_ERRORS = Counter( 129 | "bitcoin_exporter_errors", "Number of errors encountered by the exporter", labelnames=["type"] 130 | ) 131 | PROCESS_TIME = Counter( 132 | "bitcoin_exporter_process_time", "Time spent processing metrics from bitcoin node" 133 | ) 134 | 135 | SATS_PER_COIN = Decimal(1e8) 136 | 137 | BITCOIN_RPC_SCHEME = os.environ.get("BITCOIN_RPC_SCHEME", "http") 138 | BITCOIN_RPC_HOST = os.environ.get("BITCOIN_RPC_HOST", "localhost") 139 | BITCOIN_RPC_PORT = os.environ.get("BITCOIN_RPC_PORT", "8332") 140 | BITCOIN_RPC_USER = os.environ.get("BITCOIN_RPC_USER") 141 | BITCOIN_RPC_PASSWORD = os.environ.get("BITCOIN_RPC_PASSWORD") 142 | BITCOIN_CONF_PATH = os.environ.get("BITCOIN_CONF_PATH") 143 | HASHPS_BLOCKS = [int(b) for b in os.environ.get("HASHPS_BLOCKS", "-1,1,120").split(",") if b != ""] 144 | SMART_FEES = [int(f) for f in os.environ.get("SMARTFEE_BLOCKS", "2,3,5,20").split(",") if f != ""] 145 | METRICS_ADDR = os.environ.get("METRICS_ADDR", "") # empty = any address 146 | METRICS_PORT = int(os.environ.get("METRICS_PORT", "9332")) 147 | TIMEOUT = int(os.environ.get("TIMEOUT", 30)) 148 | RATE_LIMIT_SECONDS = int(os.environ.get("RATE_LIMIT", 5)) 149 | LOG_LEVEL = os.environ.get("LOG_LEVEL", "INFO") 150 | 151 | 152 | RETRY_EXCEPTIONS = (InWarmupError, ConnectionError, socket.timeout) 153 | 154 | RpcResult = Union[Dict[str, Any], List[Any], str, int, float, bool, None] 155 | 156 | 157 | def on_retry(err: Exception, next_try: float) -> None: 158 | err_type = type(err) 159 | exception_name = err_type.__module__ + "." + err_type.__name__ 160 | EXPORTER_ERRORS.labels(**{"type": exception_name}).inc() 161 | logger.error("Retry after exception %s: %s", exception_name, err) 162 | 163 | 164 | def error_evaluator(e: Exception) -> bool: 165 | return isinstance(e, RETRY_EXCEPTIONS) 166 | 167 | 168 | class UsernamePasswordProxy(Proxy): 169 | def __init__(self, *args, username=None, password=None, **kwargs) -> None: 170 | super().__init__(*args, **kwargs) 171 | 172 | # Modify the authentication header used by the underlying Proxy class. 173 | if username and password: 174 | authpair = f"{username}:{password}".encode("utf-8") 175 | authheader = b"Basic " + base64.b64encode(authpair) 176 | 177 | # Try to make the name mangling resilient to class name changes. 178 | key = next((k for k in self.__dict__.keys() if k.endswith("__auth_header")), None) 179 | if not key: 180 | raise Exception("Failed to find __auth_header in base class") 181 | self.__dict__[key] = authheader 182 | 183 | 184 | @lru_cache(maxsize=1) 185 | def rpc_client_factory(): 186 | # Configuration is done in this order of precedence: 187 | # - Explicit config file. 188 | # - BITCOIN_RPC_USER and BITCOIN_RPC_PASSWORD environment variables. 189 | # - Default bitcoin config file (as handled by Proxy.__init__). 190 | use_conf = ( 191 | (BITCOIN_CONF_PATH is not None) 192 | or (BITCOIN_RPC_USER is None) 193 | or (BITCOIN_RPC_PASSWORD is None) 194 | ) 195 | 196 | if use_conf: 197 | logger.info("Using config file: %s", BITCOIN_CONF_PATH or "") 198 | return lambda: Proxy(btc_conf_file=BITCOIN_CONF_PATH, timeout=TIMEOUT) 199 | else: 200 | host = BITCOIN_RPC_HOST 201 | if BITCOIN_RPC_PORT: 202 | host = "{}:{}".format(host, BITCOIN_RPC_PORT) 203 | service_url = f"{BITCOIN_RPC_SCHEME}://{host}" 204 | logger.info("Using environment configuration") 205 | 206 | # The underlying library can't parse the service_url parameter if the username or password 207 | # are encoded. The subclass modifies the header directly instead. 208 | return lambda: UsernamePasswordProxy( 209 | service_url=service_url, 210 | username=BITCOIN_RPC_USER, 211 | password=BITCOIN_RPC_PASSWORD, 212 | timeout=TIMEOUT 213 | ) 214 | 215 | 216 | def rpc_client(): 217 | return rpc_client_factory()() 218 | 219 | 220 | @riprova.retry( 221 | timeout=TIMEOUT, 222 | backoff=riprova.ExponentialBackOff(), 223 | on_retry=on_retry, 224 | error_evaluator=error_evaluator, 225 | ) 226 | def bitcoinrpc(*args) -> RpcResult: 227 | if logger.isEnabledFor(logging.DEBUG): 228 | logger.debug("RPC call: " + " ".join(str(a) for a in args)) 229 | 230 | result = rpc_client().call(*args) 231 | 232 | logger.debug("Result: %s", result) 233 | return result 234 | 235 | 236 | @lru_cache(maxsize=1) 237 | def getblockstats(block_hash: str): 238 | try: 239 | block = bitcoinrpc( 240 | "getblockstats", 241 | block_hash, 242 | ["total_size", "total_weight", "totalfee", "txs", "height", "ins", "outs", "total_out"], 243 | ) 244 | except Exception: 245 | logger.exception("Failed to retrieve block " + block_hash + " statistics from bitcoind.") 246 | return None 247 | return block 248 | 249 | 250 | def smartfee_gauge(num_blocks: int) -> Gauge: 251 | gauge = BITCOIN_ESTIMATED_SMART_FEE_GAUGES.get(num_blocks) 252 | if gauge is None: 253 | gauge = Gauge( 254 | "bitcoin_est_smart_fee_%d" % num_blocks, 255 | "Estimated smart fee per kilobyte for confirmation in %d blocks" % num_blocks, 256 | ) 257 | BITCOIN_ESTIMATED_SMART_FEE_GAUGES[num_blocks] = gauge 258 | return gauge 259 | 260 | 261 | def do_smartfee(num_blocks: int) -> None: 262 | smartfee = bitcoinrpc("estimatesmartfee", num_blocks).get("feerate") 263 | if smartfee is not None: 264 | gauge = smartfee_gauge(num_blocks) 265 | gauge.set(smartfee) 266 | 267 | 268 | def hashps_gauge_suffix(nblocks): 269 | if nblocks < 0: 270 | return "_neg%d" % -nblocks 271 | if nblocks == 120: 272 | return "" 273 | return "_%d" % nblocks 274 | 275 | 276 | def hashps_gauge(num_blocks: int) -> Gauge: 277 | gauge = BITCOIN_HASHPS_GAUGES.get(num_blocks) 278 | if gauge is None: 279 | desc_end = "for the last %d blocks" % num_blocks 280 | if num_blocks == -1: 281 | desc_end = "since the last difficulty change" 282 | gauge = Gauge( 283 | "bitcoin_hashps%s" % hashps_gauge_suffix(num_blocks), 284 | "Estimated network hash rate per second %s" % desc_end, 285 | ) 286 | BITCOIN_HASHPS_GAUGES[num_blocks] = gauge 287 | return gauge 288 | 289 | 290 | def do_hashps_gauge(num_blocks: int) -> None: 291 | hps = float(bitcoinrpc("getnetworkhashps", num_blocks)) 292 | if hps is not None: 293 | gauge = hashps_gauge(num_blocks) 294 | gauge.set(hps) 295 | 296 | 297 | def refresh_metrics() -> None: 298 | uptime = int(bitcoinrpc("uptime")) 299 | meminfo = bitcoinrpc("getmemoryinfo", "stats")["locked"] 300 | blockchaininfo = bitcoinrpc("getblockchaininfo") 301 | networkinfo = bitcoinrpc("getnetworkinfo") 302 | chaintips = len(bitcoinrpc("getchaintips")) 303 | mempool = bitcoinrpc("getmempoolinfo") 304 | nettotals = bitcoinrpc("getnettotals") 305 | rpcinfo = bitcoinrpc("getrpcinfo") 306 | txstats = bitcoinrpc("getchaintxstats") 307 | latest_blockstats = getblockstats(str(blockchaininfo["bestblockhash"])) 308 | 309 | banned = bitcoinrpc("listbanned") 310 | 311 | BITCOIN_UPTIME.set(uptime) 312 | BITCOIN_BLOCKS.set(blockchaininfo["blocks"]) 313 | BITCOIN_PEERS.set(networkinfo["connections"]) 314 | if "connections_in" in networkinfo: 315 | BITCOIN_CONN_IN.set(networkinfo["connections_in"]) 316 | if "connections_out" in networkinfo: 317 | BITCOIN_CONN_OUT.set(networkinfo["connections_out"]) 318 | BITCOIN_DIFFICULTY.set(blockchaininfo["difficulty"]) 319 | 320 | BITCOIN_SERVER_VERSION.set(networkinfo["version"]) 321 | BITCOIN_PROTOCOL_VERSION.set(networkinfo["protocolversion"]) 322 | BITCOIN_SIZE_ON_DISK.set(blockchaininfo["size_on_disk"]) 323 | BITCOIN_VERIFICATION_PROGRESS.set(blockchaininfo["verificationprogress"]) 324 | 325 | for smartfee in SMART_FEES: 326 | do_smartfee(smartfee) 327 | 328 | for hashps_block in HASHPS_BLOCKS: 329 | do_hashps_gauge(hashps_block) 330 | 331 | BITCOIN_BANNED_PEERS.set(len(banned)) 332 | if BAN_ADDRESS_METRICS: 333 | for ban in banned: 334 | if BITCOIN_BAN_CREATED: 335 | BITCOIN_BAN_CREATED.labels( 336 | address=ban["address"], reason=ban.get("ban_reason", "manually added") 337 | ).set(ban["ban_created"]) 338 | if BITCOIN_BANNED_UNTIL: 339 | BITCOIN_BANNED_UNTIL.labels( 340 | address=ban["address"], reason=ban.get("ban_reason", "manually added") 341 | ).set(ban["banned_until"]) 342 | 343 | if networkinfo["warnings"]: 344 | BITCOIN_WARNINGS.inc() 345 | 346 | BITCOIN_TXCOUNT.set(txstats["txcount"]) 347 | 348 | BITCOIN_NUM_CHAINTIPS.set(chaintips) 349 | 350 | BITCOIN_MEMINFO_USED.set(meminfo["used"]) 351 | BITCOIN_MEMINFO_FREE.set(meminfo["free"]) 352 | BITCOIN_MEMINFO_TOTAL.set(meminfo["total"]) 353 | BITCOIN_MEMINFO_LOCKED.set(meminfo["locked"]) 354 | BITCOIN_MEMINFO_CHUNKS_USED.set(meminfo["chunks_used"]) 355 | BITCOIN_MEMINFO_CHUNKS_FREE.set(meminfo["chunks_free"]) 356 | 357 | BITCOIN_MEMPOOL_BYTES.set(mempool["bytes"]) 358 | BITCOIN_MEMPOOL_SIZE.set(mempool["size"]) 359 | BITCOIN_MEMPOOL_USAGE.set(mempool["usage"]) 360 | BITCOIN_MEMPOOL_MINFEE.set(mempool["mempoolminfee"]) 361 | if "unbroadcastcount" in mempool: 362 | BITCOIN_MEMPOOL_UNBROADCAST.set(mempool["unbroadcastcount"]) 363 | 364 | BITCOIN_TOTAL_BYTES_RECV.set(nettotals["totalbytesrecv"]) 365 | BITCOIN_TOTAL_BYTES_SENT.set(nettotals["totalbytessent"]) 366 | 367 | if latest_blockstats is not None: 368 | BITCOIN_LATEST_BLOCK_SIZE.set(latest_blockstats["total_size"]) 369 | BITCOIN_LATEST_BLOCK_TXS.set(latest_blockstats["txs"]) 370 | BITCOIN_LATEST_BLOCK_HEIGHT.set(latest_blockstats["height"]) 371 | BITCOIN_LATEST_BLOCK_WEIGHT.set(latest_blockstats["total_weight"]) 372 | BITCOIN_LATEST_BLOCK_INPUTS.set(latest_blockstats["ins"]) 373 | BITCOIN_LATEST_BLOCK_OUTPUTS.set(latest_blockstats["outs"]) 374 | BITCOIN_LATEST_BLOCK_VALUE.set(latest_blockstats["total_out"] / SATS_PER_COIN) 375 | BITCOIN_LATEST_BLOCK_FEE.set(latest_blockstats["totalfee"] / SATS_PER_COIN) 376 | 377 | # Subtract one because we don't want to count the "getrpcinfo" call itself 378 | BITCOIN_RPC_ACTIVE.set(len(rpcinfo["active_commands"]) - 1) 379 | 380 | 381 | def signal_handler(signum, frame) -> None: 382 | signame = signal.Signals(signum).name 383 | exit_code = 128 + signum 384 | logger.critical(f"Received {signame}. Exiting.") 385 | sys.exit(exit_code) 386 | 387 | 388 | def exception_count(e: Exception) -> None: 389 | err_type = type(e) 390 | exception_name = err_type.__module__ + "." + err_type.__name__ 391 | EXPORTER_ERRORS.labels(**{"type": exception_name}).inc() 392 | 393 | 394 | def main(): 395 | # Set up logging to look similar to bitcoin logs (UTC). 396 | logging.basicConfig( 397 | format="%(asctime)s %(levelname)s %(message)s", datefmt="%Y-%m-%dT%H:%M:%SZ" 398 | ) 399 | logging.Formatter.converter = time.gmtime 400 | logger.setLevel(LOG_LEVEL) 401 | 402 | # Handle signals gracefully. 403 | signal.signal(signal.SIGTERM, signal_handler) 404 | signal.signal(signal.SIGINT, signal_handler) 405 | 406 | app = make_wsgi_app() 407 | 408 | last_refresh = datetime.fromtimestamp(0) 409 | 410 | def refresh_app(*args, **kwargs): 411 | nonlocal last_refresh 412 | process_start = datetime.now() 413 | 414 | # Only refresh every RATE_LIMIT_SECONDS seconds. 415 | if (process_start - last_refresh).total_seconds() < RATE_LIMIT_SECONDS: 416 | return app(*args, **kwargs) 417 | 418 | # Allow riprova.MaxRetriesExceeded and unknown exceptions to crash the process. 419 | try: 420 | refresh_metrics() 421 | except riprova.exceptions.RetryError as e: 422 | logger.exception("Refresh failed during retry.") 423 | exception_count(e) 424 | except JSONRPCError as e: 425 | logger.exception("Bitcoin RPC error refresh", exc_info=True) 426 | exception_count(e) 427 | except json.decoder.JSONDecodeError as e: 428 | logger.exception("RPC call did not return JSON. Bad credentials?") 429 | sys.exit(1) 430 | 431 | duration = datetime.now() - process_start 432 | PROCESS_TIME.inc(duration.total_seconds()) 433 | logger.info("Refresh took %s seconds", duration) 434 | last_refresh = process_start 435 | 436 | return app(*args, **kwargs) 437 | 438 | httpd = make_server(METRICS_ADDR, METRICS_PORT, refresh_app) 439 | httpd.serve_forever() 440 | 441 | 442 | if __name__ == "__main__": 443 | main() 444 | -------------------------------------------------------------------------------- /dashboard/bitcoin-grafana.json: -------------------------------------------------------------------------------- 1 | { 2 | "__inputs": [ 3 | { 4 | "name": "DS_PROMETHEUS", 5 | "label": "prometheus", 6 | "description": "", 7 | "type": "datasource", 8 | "pluginId": "prometheus", 9 | "pluginName": "Prometheus" 10 | } 11 | ], 12 | "__requires": [ 13 | { 14 | "type": "grafana", 15 | "id": "grafana", 16 | "name": "Grafana", 17 | "version": "6.4.1" 18 | }, 19 | { 20 | "type": "panel", 21 | "id": "graph", 22 | "name": "Graph", 23 | "version": "" 24 | }, 25 | { 26 | "type": "datasource", 27 | "id": "prometheus", 28 | "name": "Prometheus", 29 | "version": "1.0.0" 30 | } 31 | ], 32 | "annotations": { 33 | "list": [ 34 | { 35 | "builtIn": 1, 36 | "datasource": "-- Grafana --", 37 | "enable": true, 38 | "hide": true, 39 | "iconColor": "rgba(0, 211, 255, 1)", 40 | "name": "Annotations & Alerts", 41 | "type": "dashboard" 42 | } 43 | ] 44 | }, 45 | "editable": true, 46 | "gnetId": null, 47 | "graphTooltip": 1, 48 | "id": null, 49 | "links": [], 50 | "panels": [ 51 | { 52 | "aliasColors": {}, 53 | "bars": false, 54 | "dashLength": 10, 55 | "dashes": false, 56 | "datasource": "${DS_PROMETHEUS}", 57 | "description": "Number of blocks created per day using different trailing rates.", 58 | "fill": 1, 59 | "fillGradient": 0, 60 | "gridPos": { 61 | "h": 8, 62 | "w": 12, 63 | "x": 0, 64 | "y": 0 65 | }, 66 | "id": 18, 67 | "legend": { 68 | "avg": false, 69 | "current": false, 70 | "max": false, 71 | "min": false, 72 | "show": true, 73 | "total": false, 74 | "values": false 75 | }, 76 | "lines": true, 77 | "linewidth": 1, 78 | "nullPointMode": "null", 79 | "options": { 80 | "dataLinks": [] 81 | }, 82 | "percentage": false, 83 | "pointradius": 2, 84 | "points": false, 85 | "renderer": "flot", 86 | "seriesOverrides": [], 87 | "spaceLength": 10, 88 | "stack": false, 89 | "steppedLine": false, 90 | "targets": [ 91 | { 92 | "expr": "floor(sum(rate(bitcoin_blocks[3h])) * 3600 * 24)", 93 | "hide": false, 94 | "legendFormat": "3h", 95 | "refId": "C" 96 | }, 97 | { 98 | "expr": "floor(sum(rate(bitcoin_blocks[6h])) * 3600 * 24)", 99 | "hide": false, 100 | "legendFormat": "6h", 101 | "refId": "E" 102 | }, 103 | { 104 | "expr": "floor(sum(rate(bitcoin_blocks[12h])) * 3600 * 24)", 105 | "hide": false, 106 | "legendFormat": "12h", 107 | "refId": "A" 108 | }, 109 | { 110 | "expr": "floor(sum(rate(bitcoin_blocks[7d])) * 3600 * 24)", 111 | "legendFormat": "7d", 112 | "refId": "B" 113 | } 114 | ], 115 | "thresholds": [], 116 | "timeFrom": null, 117 | "timeRegions": [], 118 | "timeShift": null, 119 | "title": "Block Creation Rate", 120 | "tooltip": { 121 | "shared": true, 122 | "sort": 0, 123 | "value_type": "individual" 124 | }, 125 | "type": "graph", 126 | "xaxis": { 127 | "buckets": null, 128 | "mode": "time", 129 | "name": null, 130 | "show": true, 131 | "values": [] 132 | }, 133 | "yaxes": [ 134 | { 135 | "format": "short", 136 | "label": null, 137 | "logBase": 1, 138 | "max": null, 139 | "min": null, 140 | "show": true 141 | }, 142 | { 143 | "format": "short", 144 | "label": null, 145 | "logBase": 1, 146 | "max": null, 147 | "min": null, 148 | "show": true 149 | } 150 | ], 151 | "yaxis": { 152 | "align": false, 153 | "alignLevel": null 154 | } 155 | }, 156 | { 157 | "aliasColors": {}, 158 | "bars": false, 159 | "dashLength": 10, 160 | "dashes": false, 161 | "datasource": "${DS_PROMETHEUS}", 162 | "description": "Difficulty value", 163 | "fill": 1, 164 | "fillGradient": 0, 165 | "gridPos": { 166 | "h": 8, 167 | "w": 12, 168 | "x": 12, 169 | "y": 0 170 | }, 171 | "id": 6, 172 | "legend": { 173 | "avg": false, 174 | "current": false, 175 | "max": false, 176 | "min": false, 177 | "show": false, 178 | "total": false, 179 | "values": false 180 | }, 181 | "lines": true, 182 | "linewidth": 1, 183 | "links": [], 184 | "nullPointMode": "null", 185 | "options": { 186 | "dataLinks": [] 187 | }, 188 | "percentage": false, 189 | "pointradius": 2, 190 | "points": false, 191 | "renderer": "flot", 192 | "seriesOverrides": [], 193 | "spaceLength": 10, 194 | "stack": false, 195 | "steppedLine": false, 196 | "targets": [ 197 | { 198 | "expr": "avg(bitcoin_difficulty)", 199 | "format": "time_series", 200 | "intervalFactor": 1, 201 | "legendFormat": "difficulty", 202 | "refId": "A" 203 | } 204 | ], 205 | "thresholds": [], 206 | "timeFrom": null, 207 | "timeRegions": [], 208 | "timeShift": null, 209 | "title": "Difficulty", 210 | "tooltip": { 211 | "shared": true, 212 | "sort": 0, 213 | "value_type": "individual" 214 | }, 215 | "type": "graph", 216 | "xaxis": { 217 | "buckets": null, 218 | "mode": "time", 219 | "name": null, 220 | "show": true, 221 | "values": [] 222 | }, 223 | "yaxes": [ 224 | { 225 | "decimals": 1, 226 | "format": "short", 227 | "label": null, 228 | "logBase": 1, 229 | "max": null, 230 | "min": null, 231 | "show": true 232 | }, 233 | { 234 | "format": "short", 235 | "label": null, 236 | "logBase": 1, 237 | "max": null, 238 | "min": null, 239 | "show": true 240 | } 241 | ], 242 | "yaxis": { 243 | "align": false, 244 | "alignLevel": null 245 | } 246 | }, 247 | { 248 | "aliasColors": {}, 249 | "bars": false, 250 | "dashLength": 10, 251 | "dashes": false, 252 | "datasource": "${DS_PROMETHEUS}", 253 | "description": "Estimated network hashes per second", 254 | "fill": 1, 255 | "fillGradient": 0, 256 | "gridPos": { 257 | "h": 8, 258 | "w": 12, 259 | "x": 0, 260 | "y": 8 261 | }, 262 | "id": 8, 263 | "legend": { 264 | "avg": false, 265 | "current": false, 266 | "max": false, 267 | "min": false, 268 | "show": true, 269 | "total": false, 270 | "values": false 271 | }, 272 | "lines": true, 273 | "linewidth": 1, 274 | "links": [], 275 | "nullPointMode": "null", 276 | "options": { 277 | "dataLinks": [] 278 | }, 279 | "percentage": false, 280 | "pointradius": 2, 281 | "points": false, 282 | "renderer": "flot", 283 | "seriesOverrides": [], 284 | "spaceLength": 10, 285 | "stack": false, 286 | "steppedLine": false, 287 | "targets": [ 288 | { 289 | "expr": "avg(bitcoin_hashps)", 290 | "format": "time_series", 291 | "intervalFactor": 1, 292 | "legendFormat": "120-blocks", 293 | "refId": "B" 294 | }, 295 | { 296 | "expr": "avg(bitcoin_hashps_neg1)", 297 | "format": "time_series", 298 | "intervalFactor": 1, 299 | "legendFormat": "last-difficulty", 300 | "refId": "C" 301 | } 302 | ], 303 | "thresholds": [], 304 | "timeFrom": null, 305 | "timeRegions": [], 306 | "timeShift": null, 307 | "title": "Network Hash Rate", 308 | "tooltip": { 309 | "shared": true, 310 | "sort": 0, 311 | "value_type": "individual" 312 | }, 313 | "type": "graph", 314 | "xaxis": { 315 | "buckets": null, 316 | "mode": "time", 317 | "name": null, 318 | "show": true, 319 | "values": [] 320 | }, 321 | "yaxes": [ 322 | { 323 | "decimals": 0, 324 | "format": "short", 325 | "label": null, 326 | "logBase": 1, 327 | "max": null, 328 | "min": null, 329 | "show": true 330 | }, 331 | { 332 | "format": "short", 333 | "label": null, 334 | "logBase": 1, 335 | "max": null, 336 | "min": null, 337 | "show": true 338 | } 339 | ], 340 | "yaxis": { 341 | "align": false, 342 | "alignLevel": null 343 | } 344 | }, 345 | { 346 | "aliasColors": {}, 347 | "bars": false, 348 | "dashLength": 10, 349 | "dashes": false, 350 | "datasource": "${DS_PROMETHEUS}", 351 | "description": "Value of the block transactions in BTC.", 352 | "fill": 1, 353 | "fillGradient": 0, 354 | "gridPos": { 355 | "h": 8, 356 | "w": 12, 357 | "x": 12, 358 | "y": 8 359 | }, 360 | "id": 2, 361 | "legend": { 362 | "avg": false, 363 | "current": false, 364 | "max": false, 365 | "min": false, 366 | "show": true, 367 | "total": false, 368 | "values": false 369 | }, 370 | "lines": true, 371 | "linewidth": 1, 372 | "links": [], 373 | "nullPointMode": "null", 374 | "options": { 375 | "dataLinks": [] 376 | }, 377 | "percentage": false, 378 | "pointradius": 2, 379 | "points": false, 380 | "renderer": "flot", 381 | "seriesOverrides": [], 382 | "spaceLength": 10, 383 | "stack": false, 384 | "steppedLine": false, 385 | "targets": [ 386 | { 387 | "expr": "sum(bitcoin_latest_block_value)", 388 | "format": "time_series", 389 | "intervalFactor": 1, 390 | "legendFormat": "BTC", 391 | "refId": "A" 392 | } 393 | ], 394 | "thresholds": [], 395 | "timeFrom": null, 396 | "timeRegions": [], 397 | "timeShift": null, 398 | "title": "Block value", 399 | "tooltip": { 400 | "shared": true, 401 | "sort": 0, 402 | "value_type": "individual" 403 | }, 404 | "type": "graph", 405 | "xaxis": { 406 | "buckets": null, 407 | "mode": "time", 408 | "name": null, 409 | "show": true, 410 | "values": [] 411 | }, 412 | "yaxes": [ 413 | { 414 | "format": "short", 415 | "label": null, 416 | "logBase": 1, 417 | "max": null, 418 | "min": null, 419 | "show": true 420 | }, 421 | { 422 | "format": "short", 423 | "label": null, 424 | "logBase": 1, 425 | "max": null, 426 | "min": null, 427 | "show": true 428 | } 429 | ], 430 | "yaxis": { 431 | "align": false, 432 | "alignLevel": null 433 | } 434 | }, 435 | { 436 | "aliasColors": {}, 437 | "bars": false, 438 | "dashLength": 10, 439 | "dashes": false, 440 | "datasource": "${DS_PROMETHEUS}", 441 | "description": "Number of tips seen in the chain.", 442 | "fill": 1, 443 | "fillGradient": 0, 444 | "gridPos": { 445 | "h": 8, 446 | "w": 12, 447 | "x": 0, 448 | "y": 16 449 | }, 450 | "id": 12, 451 | "legend": { 452 | "avg": false, 453 | "current": false, 454 | "max": false, 455 | "min": false, 456 | "show": true, 457 | "total": false, 458 | "values": false 459 | }, 460 | "lines": true, 461 | "linewidth": 1, 462 | "links": [], 463 | "nullPointMode": "null", 464 | "options": { 465 | "dataLinks": [] 466 | }, 467 | "percentage": false, 468 | "pointradius": 2, 469 | "points": false, 470 | "renderer": "flot", 471 | "seriesOverrides": [], 472 | "spaceLength": 10, 473 | "stack": false, 474 | "steppedLine": false, 475 | "targets": [ 476 | { 477 | "expr": "avg(bitcoin_num_chaintips)", 478 | "format": "time_series", 479 | "intervalFactor": 1, 480 | "legendFormat": "tips", 481 | "refId": "A" 482 | } 483 | ], 484 | "thresholds": [], 485 | "timeFrom": null, 486 | "timeRegions": [], 487 | "timeShift": null, 488 | "title": "Chain Branches", 489 | "tooltip": { 490 | "shared": true, 491 | "sort": 0, 492 | "value_type": "individual" 493 | }, 494 | "type": "graph", 495 | "xaxis": { 496 | "buckets": null, 497 | "mode": "time", 498 | "name": null, 499 | "show": true, 500 | "values": [] 501 | }, 502 | "yaxes": [ 503 | { 504 | "decimals": 0, 505 | "format": "short", 506 | "label": null, 507 | "logBase": 1, 508 | "max": null, 509 | "min": null, 510 | "show": true 511 | }, 512 | { 513 | "decimals": 0, 514 | "format": "short", 515 | "label": null, 516 | "logBase": 1, 517 | "max": null, 518 | "min": null, 519 | "show": true 520 | } 521 | ], 522 | "yaxis": { 523 | "align": false, 524 | "alignLevel": null 525 | } 526 | }, 527 | { 528 | "aliasColors": {}, 529 | "bars": false, 530 | "dashLength": 10, 531 | "dashes": false, 532 | "datasource": "${DS_PROMETHEUS}", 533 | "description": "Sent and received traffic rates", 534 | "fill": 1, 535 | "fillGradient": 0, 536 | "gridPos": { 537 | "h": 8, 538 | "w": 12, 539 | "x": 12, 540 | "y": 16 541 | }, 542 | "id": 10, 543 | "legend": { 544 | "avg": false, 545 | "current": false, 546 | "max": false, 547 | "min": false, 548 | "show": true, 549 | "total": false, 550 | "values": false 551 | }, 552 | "lines": true, 553 | "linewidth": 1, 554 | "links": [], 555 | "nullPointMode": "null", 556 | "options": { 557 | "dataLinks": [] 558 | }, 559 | "percentage": false, 560 | "pointradius": 2, 561 | "points": false, 562 | "renderer": "flot", 563 | "seriesOverrides": [], 564 | "spaceLength": 10, 565 | "stack": false, 566 | "steppedLine": false, 567 | "targets": [ 568 | { 569 | "expr": "sum(irate(bitcoin_total_bytes_sent[5m]))", 570 | "format": "time_series", 571 | "intervalFactor": 1, 572 | "legendFormat": "sent", 573 | "refId": "A" 574 | }, 575 | { 576 | "expr": "-sum(irate(bitcoin_total_bytes_recv[10m]))", 577 | "format": "time_series", 578 | "intervalFactor": 1, 579 | "legendFormat": "received", 580 | "refId": "B" 581 | } 582 | ], 583 | "thresholds": [], 584 | "timeFrom": null, 585 | "timeRegions": [], 586 | "timeShift": null, 587 | "title": "Network traffic", 588 | "tooltip": { 589 | "shared": true, 590 | "sort": 0, 591 | "value_type": "individual" 592 | }, 593 | "type": "graph", 594 | "xaxis": { 595 | "buckets": null, 596 | "mode": "time", 597 | "name": null, 598 | "show": true, 599 | "values": [] 600 | }, 601 | "yaxes": [ 602 | { 603 | "decimals": 0, 604 | "format": "Bps", 605 | "label": null, 606 | "logBase": 1, 607 | "max": null, 608 | "min": null, 609 | "show": true 610 | }, 611 | { 612 | "format": "short", 613 | "label": null, 614 | "logBase": 1, 615 | "max": null, 616 | "min": null, 617 | "show": true 618 | } 619 | ], 620 | "yaxis": { 621 | "align": false, 622 | "alignLevel": null 623 | } 624 | }, 625 | { 626 | "aliasColors": {}, 627 | "bars": false, 628 | "dashLength": 10, 629 | "dashes": false, 630 | "datasource": "${DS_PROMETHEUS}", 631 | "description": "The estimated smart fee for confirmation of transactions in a given number of blocks.", 632 | "fill": 1, 633 | "fillGradient": 0, 634 | "gridPos": { 635 | "h": 8, 636 | "w": 12, 637 | "x": 0, 638 | "y": 24 639 | }, 640 | "id": 4, 641 | "legend": { 642 | "avg": false, 643 | "current": false, 644 | "max": false, 645 | "min": false, 646 | "show": true, 647 | "total": false, 648 | "values": false 649 | }, 650 | "lines": true, 651 | "linewidth": 1, 652 | "links": [], 653 | "nullPointMode": "null", 654 | "options": { 655 | "dataLinks": [] 656 | }, 657 | "percentage": false, 658 | "pointradius": 2, 659 | "points": false, 660 | "renderer": "flot", 661 | "seriesOverrides": [], 662 | "spaceLength": 10, 663 | "stack": false, 664 | "steppedLine": false, 665 | "targets": [ 666 | { 667 | "expr": "avg(bitcoin_est_smart_fee_2)", 668 | "format": "time_series", 669 | "interval": "2m", 670 | "intervalFactor": 1, 671 | "legendFormat": "2-blocks", 672 | "refId": "A" 673 | }, 674 | { 675 | "expr": "avg(bitcoin_est_smart_fee_20)", 676 | "format": "time_series", 677 | "intervalFactor": 1, 678 | "legendFormat": "20-blocks", 679 | "refId": "B" 680 | }, 681 | { 682 | "expr": "avg(bitcoin_est_smart_fee_3)", 683 | "format": "time_series", 684 | "intervalFactor": 1, 685 | "legendFormat": "3-blocks", 686 | "refId": "C" 687 | }, 688 | { 689 | "expr": "avg(bitcoin_est_smart_fee_5)", 690 | "format": "time_series", 691 | "intervalFactor": 1, 692 | "legendFormat": "5-blocks", 693 | "refId": "D" 694 | } 695 | ], 696 | "thresholds": [], 697 | "timeFrom": null, 698 | "timeRegions": [], 699 | "timeShift": null, 700 | "title": "Estimated Fee", 701 | "tooltip": { 702 | "shared": true, 703 | "sort": 2, 704 | "value_type": "individual" 705 | }, 706 | "type": "graph", 707 | "xaxis": { 708 | "buckets": null, 709 | "mode": "time", 710 | "name": null, 711 | "show": true, 712 | "values": [] 713 | }, 714 | "yaxes": [ 715 | { 716 | "format": "short", 717 | "label": null, 718 | "logBase": 1, 719 | "max": null, 720 | "min": null, 721 | "show": true 722 | }, 723 | { 724 | "format": "short", 725 | "label": null, 726 | "logBase": 1, 727 | "max": null, 728 | "min": null, 729 | "show": true 730 | } 731 | ], 732 | "yaxis": { 733 | "align": false, 734 | "alignLevel": null 735 | } 736 | }, 737 | { 738 | "aliasColors": {}, 739 | "bars": false, 740 | "dashLength": 10, 741 | "dashes": false, 742 | "datasource": "${DS_PROMETHEUS}", 743 | "description": "The Bitcoin protocol and server versions.", 744 | "fill": 1, 745 | "fillGradient": 0, 746 | "gridPos": { 747 | "h": 8, 748 | "w": 12, 749 | "x": 12, 750 | "y": 24 751 | }, 752 | "id": 14, 753 | "legend": { 754 | "avg": false, 755 | "current": false, 756 | "max": false, 757 | "min": false, 758 | "show": true, 759 | "total": false, 760 | "values": false 761 | }, 762 | "lines": true, 763 | "linewidth": 1, 764 | "links": [], 765 | "nullPointMode": "null", 766 | "options": { 767 | "dataLinks": [] 768 | }, 769 | "percentage": false, 770 | "pointradius": 2, 771 | "points": false, 772 | "renderer": "flot", 773 | "seriesOverrides": [ 774 | { 775 | "alias": "protocol", 776 | "yaxis": 1 777 | }, 778 | { 779 | "alias": "server", 780 | "yaxis": 2 781 | } 782 | ], 783 | "spaceLength": 10, 784 | "stack": false, 785 | "steppedLine": false, 786 | "targets": [ 787 | { 788 | "expr": "avg(bitcoin_protocol_version)", 789 | "format": "time_series", 790 | "intervalFactor": 1, 791 | "legendFormat": "protocol", 792 | "refId": "A" 793 | }, 794 | { 795 | "expr": "avg(bitcoin_server_version)", 796 | "format": "time_series", 797 | "intervalFactor": 1, 798 | "legendFormat": "server", 799 | "refId": "B" 800 | } 801 | ], 802 | "thresholds": [], 803 | "timeFrom": null, 804 | "timeRegions": [], 805 | "timeShift": null, 806 | "title": "Versions", 807 | "tooltip": { 808 | "shared": true, 809 | "sort": 0, 810 | "value_type": "individual" 811 | }, 812 | "type": "graph", 813 | "xaxis": { 814 | "buckets": null, 815 | "mode": "time", 816 | "name": null, 817 | "show": true, 818 | "values": [] 819 | }, 820 | "yaxes": [ 821 | { 822 | "decimals": null, 823 | "format": "none", 824 | "label": "protocol version", 825 | "logBase": 1, 826 | "max": null, 827 | "min": null, 828 | "show": true 829 | }, 830 | { 831 | "format": "none", 832 | "label": "server version", 833 | "logBase": 1, 834 | "max": null, 835 | "min": null, 836 | "show": true 837 | } 838 | ], 839 | "yaxis": { 840 | "align": false, 841 | "alignLevel": null 842 | } 843 | }, 844 | { 845 | "aliasColors": {}, 846 | "bars": false, 847 | "dashLength": 10, 848 | "dashes": false, 849 | "datasource": "${DS_PROMETHEUS}", 850 | "description": "Number of peers that have been banned and the reason for the ban.", 851 | "fill": 1, 852 | "fillGradient": 0, 853 | "gridPos": { 854 | "h": 8, 855 | "w": 12, 856 | "x": 0, 857 | "y": 32 858 | }, 859 | "id": 16, 860 | "legend": { 861 | "avg": false, 862 | "current": false, 863 | "max": false, 864 | "min": false, 865 | "show": true, 866 | "total": false, 867 | "values": false 868 | }, 869 | "lines": true, 870 | "linewidth": 1, 871 | "links": [], 872 | "nullPointMode": "null", 873 | "options": { 874 | "dataLinks": [] 875 | }, 876 | "percentage": false, 877 | "pointradius": 2, 878 | "points": false, 879 | "renderer": "flot", 880 | "seriesOverrides": [], 881 | "spaceLength": 10, 882 | "stack": false, 883 | "steppedLine": false, 884 | "targets": [ 885 | { 886 | "expr": "count(bitcoin_banned_until) by (reason)", 887 | "format": "time_series", 888 | "intervalFactor": 1, 889 | "legendFormat": "ban - {{ reason }}", 890 | "refId": "B" 891 | }, 892 | { 893 | "expr": "sum(bitcoin_peers)", 894 | "format": "time_series", 895 | "intervalFactor": 1, 896 | "legendFormat": "peers", 897 | "refId": "A" 898 | } 899 | ], 900 | "thresholds": [], 901 | "timeFrom": null, 902 | "timeRegions": [], 903 | "timeShift": null, 904 | "title": "Peers", 905 | "tooltip": { 906 | "shared": true, 907 | "sort": 0, 908 | "value_type": "individual" 909 | }, 910 | "type": "graph", 911 | "xaxis": { 912 | "buckets": null, 913 | "mode": "time", 914 | "name": null, 915 | "show": true, 916 | "values": [] 917 | }, 918 | "yaxes": [ 919 | { 920 | "decimals": 0, 921 | "format": "none", 922 | "label": null, 923 | "logBase": 1, 924 | "max": null, 925 | "min": null, 926 | "show": true 927 | }, 928 | { 929 | "format": "short", 930 | "label": null, 931 | "logBase": 1, 932 | "max": null, 933 | "min": null, 934 | "show": true 935 | } 936 | ], 937 | "yaxis": { 938 | "align": false, 939 | "alignLevel": null 940 | } 941 | } 942 | ], 943 | "refresh": "5m", 944 | "schemaVersion": 20, 945 | "style": "dark", 946 | "tags": [ 947 | "bitcoin" 948 | ], 949 | "templating": { 950 | "list": [] 951 | }, 952 | "time": { 953 | "from": "now-1w", 954 | "to": "now" 955 | }, 956 | "timepicker": { 957 | "refresh_intervals": [ 958 | "5s", 959 | "10s", 960 | "30s", 961 | "1m", 962 | "5m", 963 | "15m", 964 | "30m", 965 | "1h", 966 | "2h", 967 | "1d" 968 | ], 969 | "time_options": [ 970 | "5m", 971 | "15m", 972 | "1h", 973 | "6h", 974 | "12h", 975 | "24h", 976 | "2d", 977 | "7d", 978 | "30d" 979 | ] 980 | }, 981 | "timezone": "", 982 | "title": "Bitcoin", 983 | "uid": "OTLC2jHWz", 984 | "version": 1 985 | } --------------------------------------------------------------------------------