diff --git a/assets/donate.png b/.github/assets/donate.png similarity index 100% rename from assets/donate.png rename to .github/assets/donate.png diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..849ddff --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +dist/ diff --git a/.goreleaser.yml b/.goreleaser.yml index 448da61..0510dd8 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -24,13 +24,6 @@ builds: - 6 - 7 -archives: - - replacements: - darwin: Darwin - linux: Linux - windows: Windows - amd64: x86_64 - checksum: name_template: 'checksums.txt' diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..587d1c3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,30 @@ +ARG BUILDER_IMAGE=golang@sha256:4544ae57fc735d7e415603d194d9fb09589b8ad7acd4d66e928eabfb1ed85ff1 +ARG RUNTIME_IMAGE=gcr.io/distroless/static@sha256:c9f9b040044cc23e1088772814532d90adadfa1b86dcba17d07cb567db18dc4e + + +FROM $BUILDER_IMAGE as builder + + WORKDIR /workspace + + COPY .git .git + COPY go.mod go.mod + COPY go.sum go.sum + COPY pkg/ pkg/ + COPY cmd/ cmd/ + + RUN set -x && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on \ + go build -a -v \ + -ldflags "-s -w -X main.version=$(git tag) -X main.commit=$(git rev-parse HEAD)" \ + -tags osusergo,netgo,static_build \ + -trimpath \ + -o monero-exporter \ + ./cmd/monero-exporter + + +FROM $RUNTIME_IMAGE + + WORKDIR / + COPY --from=builder /workspace/monero-exporter . + USER 65532:65532 + + ENTRYPOINT ["/monero-exporter"] diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 0000000..865a843 --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,118 @@ +# Install + +## Using Go + +If you already have [Go] installed, you can use to Go toolchain to build from +source and install it for you (under `$GOPATH/bin`): + +```console +$ GO111MODULE=on go get github.com/cirocosta/monero-exporter/cmd/monero-exporter +go: downloading github.com/cirocosta/monero-exporter v0.0.3 + +$ monero-exporter --help +Prometheus exporter for monero metrics + +Usage: + monero-exporter [flags] +... +``` + +Note that this will install the latest tagged release (not necessarily the +latest code). + + +## From releases + +In the [releases page] you'll find the pre-compiled releases for each platform. + + +### "yeah yeah, I trust the interwebs" mode + +```bash +export VERSION=0.0.3 + +curl -SL -o- https://github.com/cirocosta/monero-exporter/releases/download/v$VERSION/monero-exporter_$VERSION_Linux_x86_64.tar.gz | \ + tar xvzf monero +mv ./monero-exporter /usr/local/bin +``` + + +### "trust, but verify" mode + + +1. fetch my public key and make sure it matches the expected fingerprint + +```console +$ curl -SOL https://utxo.com.br/pgp/public-key.txt +``` + +now, using `gpg`, derive the fingerprint. it should match the one advertised by +me: `9CD1 1313 8578 59CC 0FAD E93B 6B93 177A 62D0 1DB8` (should be the same as +you can find under my personal account on Twitter: http://twitter.com/cirowrc). + + +```console +$ gpg --keyid-format long --with-fingerprint ./public-key.txt +pub rsa3072/6B93177A62D01DB8 2021-07-19 [SC] [expires: 2023-07-19] + Key fingerprint = 9CD1 1313 8578 59CC 0FAD E93B 6B93 177A 62D0 1DB8 +``` + +then, import into the key to the keyring so it can be used to validate that I +indeed signed the content advertised. + +```console +$ gpg --import ./public-key.txt +gpg: key 6B93177A62D01DB8: public key "..." imported +gpg: Total number processed: 1 +gpg: imported: 1 +``` + + +2. download the archive for your platform as well as the checksums + +```console +$ curl -SOL https://github.com/cirocosta/monero-exporter/releases/download/v0.0.3/monero-exporter_0.0.3_Linux_x86_64.tar.gz +$ curl -SOL https://github.com/cirocosta/monero-exporter/releases/download/v0.0.3/checksums.txt.asc +``` + + +3. verify that you can trust the checksums (that it has been generated and + not tampered with), and then verify that the assets you downloaded are what + they supposed to be + +```console +$ gpg --verify ./checksums.txt.asc +gpg: Signature made Mon 19 Jul 2021 02:10:42 PM EDT +gpg: using RSA key 9CD11313857859CC0FADE93B6B93177A62D01DB8 +gpg: Good signature from "Ciro ... +``` + + +4. verify that the tarball is what it should be + +compute the digest of the tarball + +```console +$ sha256sum ./monero-exporter_0.0.3_Linux_x86_64.tar.gz +e2b2214c9371fe3c0333cca7feff3554c56d8d0f377180e39ff50d332639c22d ./monero-exporter_0.0.3_Linux_x86_64.tar.gz + + +see that it matches what you found in the signed checksums file + +$ cat ./checksums.txt.asc | grep e2b2214c9371fe3c0333cca7feff3554c56d8d0f377180e39ff50d332639c22d +e2b2214c9371fe3c0333cca7feff3554c56d8d0f377180e39ff50d332639c22d monero-exporter_0.0.3_Linux_x86_64.tar.gz +``` + +5. install + +```console +$ tar xvzf ./monero-exporter_0.0.3_Linux_x86_64.tar.gz monero +$ mv monero /usr/local/bin + +$ monero-exporter version +0.0.3 ... +``` + + +[Go]: https://golang.org/dl/ +[releases page]: https://github.com/cirocosta/monero-exporter/releases diff --git a/Makefile b/Makefile index cdef408..f105cde 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,19 @@ install: go install -v ./cmd/monero-exporter +run: + monero-exporter \ + --monero-addr=http://localhost:18081 \ + --bind-addr=:9000 \ + --geoip-filepath=./hack/geoip.mmdb + test: go test ./... lint: - go run github.com/golangci/golangci-lint/cmd/golangci-lint run --config=.golangci.yaml + go run github.com/golangci/golangci-lint/cmd/golangci-lint run \ + --config=.golangci.yaml + + +table-of-contents: + doctoc --notitle ./README.md diff --git a/README.md b/README.md index 08ff0e7..5674d9d 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,33 @@ # monero-exporter -[Prometheus](https://prometheus.io) exporter for [monero](https://getmonero.org). +[prometheus] exporter for [monero] nodes. -## Installation - -Using Go: - -``` -go get github.com/cirocosta/monero-exporter/cmd/monero-exporter -``` - -From releases: - -**TODO** + + -## Usage +- [About](#about) +- [Installation](#installation) +- [Metrics](#metrics) + - [Last block](#last-block) + - [Transaction pool](#transaction-pool) + - [RPC](#rpc) + - [P2P Connections](#p2p-connections) + - [Peers](#peers) + - [Net Stats](#net-stats) + - [Info](#info) +- [License](#license) +- [Thanks](#thanks) +- [Donate](#donate) + + + +## About + +`monero-exporter` is a single multiplatform binary that extracts metrics out of +a running [monero] node, allowing one to better observe the state of its own +daemon and the network as seen by it. ```console $ monero-exporter --help @@ -25,7 +36,6 @@ Prometheus exporter for monero metrics Usage: monero-exporter [flags] - monero-exporter [command] Available Commands: completion generate the autocompletion script for the specified shell @@ -33,90 +43,251 @@ Available Commands: version print the version of this CLI Flags: - --address string address of the monero node to collect metrics from - --geoip-filepath string filepath of a geoip database file for ip to country resolution + --bind-addr string address to bind the prometheus server to + (default ":9090") + --geoip-filepath string filepath of a geoip database file for ip to + country resolution -h, --help help for monero-exporter + --monero-addr string address of the monero instance to collect info + from (default "http://localhost:18081") + --telemetry-path string endpoint at which prometheus metrics are served + (default "/metrics") Use "monero-exporter [command] --help" for more information about a command. ``` +It works by issuing remote procedure calls (RPC) to a monero node and based on +the responses it gets, exposing metrics to a Prometheus server querying +`monero-exporter`. + + +``` + + PROMETHEUS ---- get /metrics -->---. + ^ | + | | + | MONERO-EXPORTER --- get /jsonrpc --> MONEROD + | | + | | + '-------------<-- samples ------' + +``` + +Typically, `monero-exporter` is just one piece of the stack though. A complete +setup usually looks like: + +- `monerod`, the monero daemon, is running exposing its daemon RPC for those + interested in it (see [monero-project/monero]) +- `monero-exporter` targets that `monerod` RPC port +- [prometheus], configured to scrape `monero-exporter`, asks it for metric + samples, then stores those in its internal timeseries database +- [grafana] then queries that timeseries data using the [promql], displaying + that in dashboards, alerting on thresholds, etc + + +``` + + (ui) GRAFANA + . + . + queries ..every dashboard refresh interval.. + . + . + . + (ts db) PROMETHEUS --- scrape ->-. ..every scrape interval.. + ^ | + | | + | | + (exporter) '--------------MONERO-EXPORTER + . + ...... rpc ......... ..on scrape.. + . + . + (daemon) MONEROD + +``` + + + +## Installation + +You can either install it by using [Go], building the latest tagged release +from scratch + +```bash +GO111MODULE=on \ + go get github.com/cirocosta/monero-exporter/cmd/monero-exporter +``` + +or fetching the binary for your corresponding distribution from the [releases +page]. + +See [INSTALL.md] for details and examples. + + + ## Metrics +Below you'll find the description of the metrics exposed by `monero-exporter` +that are retrieved by a prometheus server, stored in its timeseries database, +and made available for querying by something like Grafana. + +Keep in mind that Prometheus gather metrics in a pull-based fashion (see ["Why +do you pull rather than push?"]), and as such, metric samples are based on +information seen at the time that prometheus reached out to the exporter. + +While the pull-based approach is very useful for looking at long timeframes and +large number of hosts, it's not very suitable for realtime data (for that, +consider other implementations for push-based systems like [InfluxDB]). + + +### Last block + +This set of metrics is extracted from information gathered regarding the last +block seen by the node at the moment that Prometheus scraped samples from +`monero-exporter`. + +Think of it as _"at time X, when I looked at the last block, these were the +pieces of information I could gather from it"_, which when looked at over time +(say, 30d), gives you a pretty good picture of what "all" (more like, _most_) +blocks looked like as long as the interval between those scrapes where short +enough (i.e., in between the mean time for a block to be mined - 2min as of +today). + | name | description | | ---- | ----------- | -| monero_bans | number of nodes banned | -| monero_connections | connections info | -| monero_connections_livetime | peers livetime distribution | -| monero_fee_estimate | fee estimate for 1 grace block | -| monero_height_divergence | how much our peers diverge from us in block height | -| monero_info_alt_blocks_count | info for alt_blocks_count | -| monero_info_block_size_limit | info for block_size_limit | -| monero_info_block_size_median | info for block_size_median | -| monero_info_busy_syncing | info for busy_syncing | -| monero_info_cumulative_difficulty | info for cumulative_difficulty | -| monero_info_difficulty | info for difficulty | -| monero_info_free_space | info for free_space | -| monero_info_grey_peerlist_size | info for grey_peerlist_size | -| monero_info_height | info for height | -| monero_info_height_without_bootstrap | info for height_without_bootstrap | -| monero_info_incoming_connections_count | info for incoming_connections_count | -| monero_info_mainnet | info for mainnet | -| monero_info_offline | info for offline | -| monero_info_outgoing_connections_count | info for outgoing_connections_count | -| monero_info_rpc_connections_count | info for rpc_connections_count | -| monero_info_stagenet | info for stagenet | -| monero_info_start_time | info for start_time | -| monero_info_synchronized | info for synchronized | -| monero_info_target | info for target | -| monero_info_target_height | info for target_height | -| monero_info_testnet | info for testnet | -| monero_info_tx_count | info for tx_count | -| monero_info_tx_pool_size | info for tx_pool_size | -| monero_info_untrusted | info for untrusted | -| monero_info_was_bootstrap_ever_used | info for was_bootstrap_ever_used | -| monero_info_white_peerlist_size | info for white_peerlist_size | -| monero_last_block_header_block_size | info for block_size | -| monero_last_block_header_block_weight | info for block_weight | -| monero_last_block_header_cumulative_difficulty | info for cumulative_difficulty | -| monero_last_block_header_cumulative_difficulty_top64 | info for cumulative_difficulty_top64 | -| monero_last_block_header_depth | info for depth | -| monero_last_block_header_difficulty | info for difficulty | -| monero_last_block_header_difficulty_top64 | info for difficulty_top64 | -| monero_last_block_header_height | info for height | -| monero_last_block_header_long_term_weight | info for long_term_weight | -| monero_last_block_header_major_version | info for major_version | -| monero_last_block_header_minor_version | info for minor_version | -| monero_last_block_header_nonce | info for nonce | -| monero_last_block_header_num_txes | info for num_txes | -| monero_last_block_header_orphan_status | info for orphan_status | -| monero_last_block_header_reward | info for reward | -| monero_last_block_header_timestamp | info for timestamp | -| monero_last_block_txn_fee | distribution of outputs in last block | -| monero_last_block_txn_size | distribution of tx sizes | -| monero_last_block_vin | distribution of inputs in last block | -| monero_last_block_vout | distribution of outputs in last block | -| monero_mempool_bytes_max | info for bytes_max | -| monero_mempool_bytes_med | info for bytes_med | -| monero_mempool_bytes_min | info for bytes_min | -| monero_mempool_bytes_total | info for bytes_total | -| monero_mempool_fee_total | info for fee_total | -| monero_mempool_histo_98pc | info for histo_98pc | -| monero_mempool_num_10m | info for num_10m | -| monero_mempool_num_double_spends | info for num_double_spends | -| monero_mempool_num_failing | info for num_failing | -| monero_mempool_num_not_relayed | info for num_not_relayed | -| monero_mempool_oldest | info for oldest | -| monero_mempool_txs_total | info for txs_total | -| monero_net_total_in_bytes | network statistics | -| monero_net_total_out_bytes | network statistics | -| monero_peers_new | peers info | -| monero_rpc_count | todo | -| monero_rpc_time | todo | +| monero_lastblock_difficulty | difficulty used for the last block | +| monero_lastblock_fees_micronero_per_kb | distribution of the feeperkb utilized for txns | +| monero_lastblock_fees_monero | total amount of fees included in this block | +| monero_lastblock_height | height of the last block | +| monero_lastblock_reward_monero | total amount of rewards granted in the last block (subsidy + fees) | +| monero_lastblock_size_bytes | total size of the last block | +| monero_lastblock_subsidy_monero | newly minted monero for this block | +| monero_lastblock_transactions | number of transactions seen in the last block | +| monero_lastblock_transactions_inputs | distribution of inputs in the last block | +| monero_lastblock_transactions_outputs | distribution of outputs in the last block | +| monero_lastblock_transactions_size_bytes | distribution of the size of the transactions included | + + + +### Transaction pool + +These metrics give you a view of how the transaction pool of this particular +node looked like when Prometheus asked the exporter information about it. + +Given the pull-based nature of Prometheus, this will be only as granular as the +frequency of scraping configured for it. + + +| name | description | +| ---- | ----------- | +| monero_transaction_pool_double_spends | transactions doubly spending outputs | +| monero_transaction_pool_failing_transactions | number of transactions that are marked as failing | +| monero_transaction_pool_fees_micronero_per_kb | distribution of the feeperkb utilized for txns in the pool | +| monero_transaction_pool_fees_monero | total amount of fee being spent in the transaction pool | +| monero_transaction_pool_not_relayed | number of transactions that have not been relayed | +| monero_transaction_pool_older_than_10m | number of transactions that are older than 10m | +| monero_transaction_pool_size_bytes | total size of the transaction pool | +| monero_transaction_pool_spent_key_images | total number of key images spent across all transactions in the pool | +| monero_transaction_pool_transactions | number of transactions in the pool at the moment of the scrape | +| monero_transaction_pool_transactions_age | distribution of for how long transactions have been in the pool | +| monero_transaction_pool_transactions_inputs | distribution of inputs in the pool | +| monero_transaction_pool_transactions_outputs | distribution of outputs in the pool | +| monero_transaction_pool_transactions_size_bytes | distribution of the size of the transactions in the transaction pool | + + +### RPC + +RPC metrics provide an overview of the usage of the RPC endpoints. + +Note that `monerod` does not distinguish between `monero-exporter` and other +users (like, a wallet), which means that these metrics will include the +constant querying that `monero-exporter` performs to fetch statistics. + + +| name | description | +| ---- | ----------- | +| monero_rpc_hits_total | number of hits that a particular rpc method had since startup | +| monero_rpc_seconds_total | amount of time spent service the method since startup | + + +### P2P Connections + +Connection metrics aim at providing information about peers that are fully +connected to the node (thus, transmitting and receiving data to/from our node). + + +| name | description | +| ---- | ----------- | +| monero_p2p_connections | number of connections to/from this node | +| monero_p2p_connections_age | distribution of age of the connections we have | +| monero_p2p_connections_height | distribution the height of the peers connected to/from us | +| monero_p2p_connections_rx_rate_bps | distribution of data receive rate in bytes/s | +| monero_p2p_connections_tx_rate_bps | distribution of data transmit rate in bytes/s | + + +### Peerlist + +The monero daemon internally keeps track of potential peers to connect to +called peerlists, divided in anchor, white, and gray. + + +| name | description | +| ---- | ----------- | +| monero_peerlist | number of node entries in the peerlist | +| monero_peerlist_lastseen | distribution of when our peers have been seen | + + +### Net Stats + +Aggregated network statistics, not specific to P2P or RPC. + +| name | description | +| ---- | ----------- | +| monero_net_rx_bytes | number of bytes received by this node | +| monero_net_tx_bytes | number of bytes received by this node | + + +### Info + +General information about this node. + +| name | description | +| ---- | ----------- | +| monero_info_alternative_blocks | number of blocks alternative to the longest | +| monero_info_block_size_limit | maximum hard limit of a block | +| monero_info_block_size_median | current median size for computing dynamic fees | +| monero_info_mainnet | whether the node is connected to mainnet | +| monero_info_offline | whether the node is offline | +| monero_info_synchronized | | +| monero_info_uptime_seconds_total | for how long this node has been up | + + +## License + +See [LICENSE](./LICENSE). + + +## Thanks + +- maxmind geoip database: https://www.maxmind.com/en/geoip2-services-and-databases ## Donate -![xmr address](./assets/donate.png) +![donation qrcode] 891B5keCnwXN14hA9FoAzGFtaWmcuLjTDT5aRTp65juBLkbNpEhLNfgcBn6aWdGuBqBnSThqMPsGRjWVQadCrhoAT6CnSL3 + +["Why do you pull rather than push?"]: https://prometheus.io/docs/introduction/faq/#why-do-you-pull-rather-than-push +[Go]: https://golang.org/ +[INSTALL.md]: ./INSTALL.md +[InfluxDB]: https://github.com/influxdata/influxdb +[donation qrcode]: ./.github/assets/donate.png +[grafana]: https://github.com/grafana/grafana +[monero-project/monero]: https://github.com/monero-project/monero +[monero]: https://github.com/monero-project/monero +[prometheus]: https://prometheus.io +[promql]: https://prometheus.io/docs/prometheus/latest/querying/basics/ +[releases page]: https://github.com/cirocosta/monero-exporter/releases diff --git a/assets/pod.yaml b/assets/pod.yaml deleted file mode 100644 index 43f90b9..0000000 --- a/assets/pod.yaml +++ /dev/null @@ -1,24 +0,0 @@ -apiVersion: v1 -kind: Pod -metadata: - name: node-0 -spec: - restartPolicy: Never - containers: - - command: - - monerod - - --non-interactive - - --out-peers=128 - - --in-peers=128 - - --prune-blockchain - - --limit-rate=128000 - image: index.docker.io/utxobr/monerod@sha256:19ba5793c00375e7115469de9c14fcad928df5867c76ab5de099e83f646e175d - imagePullPolicy: IfNotPresent - name: monerod - ports: - - containerPort: 18080 - name: p2p - protocol: TCP - - containerPort: 18089 - name: restricted - protocol: TCP diff --git a/cmd/monero-exporter/root.go b/cmd/monero-exporter/root.go index 1b5e9c8..4a3437f 100644 --- a/cmd/monero-exporter/root.go +++ b/cmd/monero-exporter/root.go @@ -15,8 +15,10 @@ import ( ) type command struct { - address string + telemetryPath string + bindAddr string geoIPFilepath string + moneroAddr string } func (c *command) Cmd() *cobra.Command { @@ -26,13 +28,20 @@ func (c *command) Cmd() *cobra.Command { RunE: c.RunE, } - cmd.Flags().StringVar(&c.address, "address", - "", "address of the monero node to collect metrics from") - cmd.MarkFlagRequired("address") + cmd.Flags().StringVar(&c.bindAddr, "bind-addr", + ":9000", "address to bind the prometheus server to") + + cmd.Flags().StringVar(&c.telemetryPath, "telemetry-path", + "/metrics", "endpoint at which prometheus metrics are served") + + cmd.Flags().StringVar(&c.moneroAddr, "monero-addr", + "http://localhost:18081", "address of the monero instance to "+ + "collect info from") cmd.Flags().StringVar(&c.geoIPFilepath, "geoip-filepath", - "", "filepath of a geoip database file for ip to country resolution") - cmd.MarkFlagFilename("geoip-filepath") + "", "filepath of a geoip database file for ip to country "+ + "resolution") + _ = cmd.MarkFlagFilename("geoip-filepath") return cmd } @@ -41,15 +50,9 @@ func (c *command) RunE(_ *cobra.Command, _ []string) error { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - prometheusExporter, err := exporter.New() + rpcClient, err := rpc.NewClient(c.moneroAddr) if err != nil { - return fmt.Errorf("new exporter: %w", err) - } - defer prometheusExporter.Close() - - rpcClient, err := rpc.NewClient(c.address) - if err != nil { - return fmt.Errorf("new client '%s': %w", c.address, err) + return fmt.Errorf("new client '%s': %w", c.moneroAddr, err) } daemonClient := daemon.NewClient(rpcClient) @@ -66,20 +69,35 @@ func (c *command) RunE(_ *cobra.Command, _ []string) error { countryMapper := func(ip net.IP) (string, error) { res, err := db.Country(ip) if err != nil { - return "", fmt.Errorf("country '%s': %w", ip, err) + return "", fmt.Errorf( + "country '%s': %w", ip, err, + ) } return res.RegisteredCountry.IsoCode, nil } - collectorOpts = append(collectorOpts, collector.WithCountryMapper(countryMapper)) + collectorOpts = append(collectorOpts, + collector.WithCountryMapper(countryMapper), + ) } - if err := collector.Register(daemonClient, collectorOpts...); err != nil { + err = collector.Register(daemonClient, collectorOpts...) + if err != nil { return fmt.Errorf("collector register: %w", err) } - if err := prometheusExporter.Run(ctx); err != nil { + prometheusExporter, err := exporter.New( + exporter.WithBindAddress(c.bindAddr), + exporter.WithTelemetryPath(c.telemetryPath), + ) + if err != nil { + return fmt.Errorf("new exporter: %w", err) + } + defer prometheusExporter.Close() + + err = prometheusExporter.Run(ctx) + if err != nil { return fmt.Errorf("prometheus exporter run: %w", err) } diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..61a9c1b --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,44 @@ +version: '3' + +services: + prometheus: + image: prom/prometheus + container_name: prometheus + network_mode: host + volumes: + - prometheus:/prometheus + entrypoint: + - /bin/sh + - -c + - | + echo " + global: + scrape_interval: '15s' + evaluation_interval: '15s' + + scrape_configs: + - job_name: 'monerod' + static_configs: + - targets: + - '127.0.0.1:9000' + " > config.yml + + exec prometheus \ + --config.file=config.yml \ + --storage.tsdb.retention.time=30d \ + --storage.tsdb.path=/prometheus + + + grafana: + container_name: grafana + build: ./grafana + ports: + - 3000:3000 + environment: + - GF_INSTALL_PLUGINS=grafana-worldmap-panel + volumes: + - ./grafana/dashboards:/var/lib/grafana/dashboards:ro + + +volumes: + prometheus: diff --git a/go.mod b/go.mod index 238d126..96a682e 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,9 @@ module github.com/cirocosta/monero-exporter go 1.16 require ( + github.com/beorn7/perks v1.0.1 // indirect github.com/bmizerany/perks v0.0.0-20141205001514-d9a9656a3a4b - github.com/cirocosta/go-monero v0.0.2-0.20210711112505-425893dd54c5 + github.com/cirocosta/go-monero v0.0.5-0.20210731140604-e84b32b615af github.com/go-logr/logr v0.4.0 github.com/go-logr/zapr v0.4.0 github.com/golangci/golangci-lint v1.41.1 @@ -12,5 +13,6 @@ require ( github.com/prometheus/client_golang v1.11.0 github.com/spf13/cobra v1.2.1 go.uber.org/zap v1.17.0 + golang.org/x/net v0.0.0-20210716203947-853a461950ff // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c ) diff --git a/go.sum b/go.sum index eb6eb68..8f032b3 100644 --- a/go.sum +++ b/go.sum @@ -106,8 +106,8 @@ github.com/chavacava/garif v0.0.0-20210405164556-e8a0a408d6af/go.mod h1:Qjyv4H3/ github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/cirocosta/go-monero v0.0.2-0.20210711112505-425893dd54c5 h1:9FxmhvhnnNG8esoFBC6kjplzYaWQcTFv9HHjqtQHyb8= -github.com/cirocosta/go-monero v0.0.2-0.20210711112505-425893dd54c5/go.mod h1:hDrYMh+4izQypIwez8i3O1hkgJ5wDAPofkaVAtr3mbM= +github.com/cirocosta/go-monero v0.0.5-0.20210731140604-e84b32b615af h1:3KIUbswishjkyOM0rYfprGjMTWh+64VcXMOIQTnRcxE= +github.com/cirocosta/go-monero v0.0.5-0.20210731140604-e84b32b615af/go.mod h1:B62WVZVEggXr+Kg5wKPNgfDw/BGplrgLjk7hJYIR3FU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= @@ -209,6 +209,8 @@ github.com/go-toolsmith/typep v1.0.2 h1:8xdsa1+FSIH/RhEkgnD1j2CJOy5mNllW1Q9tRiYw github.com/go-toolsmith/typep v1.0.2/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU= github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b h1:khEcpUM4yFcxg4/FHQWkvVRmgijNXRfzkIDHh23ggEo= github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= +github.com/go-zeromq/goczmq/v4 v4.2.2/go.mod h1:Sm/lxrfxP/Oxqs0tnHD6WAhwkWrx+S+1MRrKzcxoaYE= +github.com/go-zeromq/zmq4 v0.13.0/go.mod h1:TrFwdPHMSLG7Rhp8OVhQBkb4bSajfucWv8rwoEFIgSY= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -850,8 +852,9 @@ golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6 h1:0PC75Fz/kyMGhL0e1QnypqK2kQMqKt9csD1GnMJR+Zk= golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20210716203947-853a461950ff h1:j2EK/QoxYNBsXI4R7fQkkRUk8y6wnOBI+6hgPdP/6Ds= +golang.org/x/net v0.0.0-20210716203947-853a461950ff/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= diff --git a/grafana/Dockerfile b/grafana/Dockerfile new file mode 100644 index 0000000..a470545 --- /dev/null +++ b/grafana/Dockerfile @@ -0,0 +1,4 @@ +FROM grafana/grafana + +ADD ./config.ini /etc/grafana/grafana.ini +ADD ./provisioning /etc/grafana/provisioning diff --git a/grafana/config.ini b/grafana/config.ini new file mode 100644 index 0000000..c1c31f9 --- /dev/null +++ b/grafana/config.ini @@ -0,0 +1,29 @@ +[analytics] +reporting_enabled = false +check_for_updates = false + + +[auth] +disable_login_form = true + + +[auth.anonymous] +enabled = true +org_role = Admin + + +[dashboards] +min_refresh_interval = 1m + + +[paths] +provisioning = /etc/grafana/provisioning + + +[server] +enable_gzip = true +read_timeout = 2m + + +[snapshots] +external_enabled = false diff --git a/grafana/dashboards/monero.json b/grafana/dashboards/monero.json new file mode 100644 index 0000000..c0bb0c8 --- /dev/null +++ b/grafana/dashboards/monero.json @@ -0,0 +1,3043 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "description": "", + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "id": 1, + "iteration": 1627858616924, + "links": [], + "panels": [ + { + "collapsed": false, + "datasource": null, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 16, + "panels": [], + "title": "Transaction Pool", + "type": "row" + }, + { + "datasource": null, + "description": "Total number of key images spent across all transactions in the pool", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": -1, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "stepBefore", + "lineWidth": 1, + "pointSize": 6, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 1 + }, + "id": 21, + "options": { + "legend": { + "calcs": [], + "displayMode": "hidden", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi" + } + }, + "targets": [ + { + "exemplar": true, + "expr": "monero_transaction_pool_spent_key_images", + "interval": "", + "legendFormat": "fee", + "refId": "A" + } + ], + "title": "Spent Key Images", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": null, + "description": "Total size of the block", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": -1, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "stepBefore", + "lineWidth": 1, + "pointSize": 6, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "decbytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 1 + }, + "id": 23, + "options": { + "legend": { + "calcs": [], + "displayMode": "hidden", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi" + } + }, + "targets": [ + { + "exemplar": true, + "expr": "monero_transaction_pool_size_bytes", + "interval": "", + "legendFormat": "size", + "refId": "A" + } + ], + "title": "Size", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": null, + "description": "Number of transactions in the transaction pool.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": -1, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "stepBefore", + "lineWidth": 1, + "pointSize": 6, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 1 + }, + "id": 18, + "options": { + "legend": { + "calcs": [], + "displayMode": "hidden", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi" + } + }, + "targets": [ + { + "exemplar": true, + "expr": "monero_transaction_pool_transactions", + "interval": "", + "legendFormat": "fee", + "refId": "A" + } + ], + "title": "Transactions", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": null, + "description": "The distribution of the sizes of the transactions in the pool.\n", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": -1, + "drawStyle": "points", + "fillOpacity": 26, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 3, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "decbytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "1" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-purple", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "0.99" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-blue", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "0.95" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-green", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "0.9" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-yellow", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "0.75" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-orange", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "0.5" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-red", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "0.25" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "light-red", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 8 + }, + "id": 26, + "options": { + "legend": { + "calcs": [], + "displayMode": "hidden", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi" + } + }, + "targets": [ + { + "exemplar": true, + "expr": "monero_transaction_pool_transactions_size_bytes{quantile=~\"$quantile\"}", + "interval": "", + "legendFormat": "{{ quantile }}", + "refId": "A" + } + ], + "title": "Transaction Size Distribution", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": null, + "description": "The distribution fees (in micronero) per kB for the transactions still in the transaction pool.\n", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": -1, + "drawStyle": "points", + "fillOpacity": 26, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 3, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "µɱ" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "1" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-purple", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "0.99" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-blue", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "0.95" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-green", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "0.9" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-yellow", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "0.75" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-orange", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "0.5" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-red", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "0.25" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "light-red", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 8 + }, + "id": 20, + "options": { + "legend": { + "calcs": [], + "displayMode": "hidden", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi" + } + }, + "targets": [ + { + "exemplar": true, + "expr": "monero_transaction_pool_fees_micronero_per_kb{quantile=~\"$quantile\"}", + "interval": "", + "legendFormat": "{{ quantile }}", + "refId": "A" + } + ], + "title": "Transaction Fees Distribution (per kB)", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": null, + "description": "Total amount of fees (in Monero) for the transactions in the transaction pool.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": -1, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "stepBefore", + "lineWidth": 1, + "pointSize": 6, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ɱ" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 8 + }, + "id": 24, + "options": { + "legend": { + "calcs": [], + "displayMode": "hidden", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi" + } + }, + "targets": [ + { + "exemplar": true, + "expr": "monero_transaction_pool_fees_monero", + "interval": "", + "legendFormat": "fee", + "refId": "A" + } + ], + "title": "Fees", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": null, + "description": "The distribution of the amount of inputs used in transactions still in the transaction pool. \n", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": -1, + "drawStyle": "points", + "fillOpacity": 26, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 3, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "1" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-purple", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "0.99" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-blue", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "0.95" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-green", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "0.9" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-yellow", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "0.75" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-orange", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "0.5" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-red", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "0.25" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "light-red", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 15 + }, + "id": 28, + "options": { + "legend": { + "calcs": [], + "displayMode": "hidden", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi" + } + }, + "targets": [ + { + "exemplar": true, + "expr": "monero_transaction_pool_transactions_inputs{quantile=~\"$quantile\"}", + "interval": "", + "legendFormat": "{{ quantile }}", + "refId": "A" + } + ], + "title": "Transaction Inputs", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": null, + "description": "The distribution of the amount of outputs used in transactions still in the transaction pool. \n", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": -1, + "drawStyle": "points", + "fillOpacity": 26, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 3, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "1" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-purple", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "0.99" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-blue", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "0.95" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-green", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "0.9" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-yellow", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "0.75" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-orange", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "0.5" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-red", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "0.25" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "light-red", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 15 + }, + "id": 29, + "options": { + "legend": { + "calcs": [], + "displayMode": "hidden", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi" + } + }, + "targets": [ + { + "exemplar": true, + "expr": "monero_transaction_pool_transactions_outputs{quantile=~\"$quantile\"}", + "interval": "", + "legendFormat": "{{ quantile }}", + "refId": "A" + } + ], + "title": "Transaction Outputs", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": null, + "description": "Total number of inputs and outputs from the transactions still in the pool.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": -1, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "stepBefore", + "lineWidth": 1, + "pointSize": 6, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 15 + }, + "id": 31, + "options": { + "legend": { + "calcs": [], + "displayMode": "hidden", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi" + } + }, + "targets": [ + { + "exemplar": true, + "expr": "monero_transaction_pool_transactions_inputs_sum", + "interval": "", + "legendFormat": "inputs", + "refId": "A" + }, + { + "exemplar": true, + "expr": "monero_transaction_pool_transactions_outputs_sum", + "hide": false, + "interval": "", + "legendFormat": "outputs", + "refId": "B" + } + ], + "title": "Inputs and Outputs", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": null, + "description": "A couple of statistics that reveals weird scenarios found in the transaction pool.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": -1, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "stepBefore", + "lineWidth": 1, + "pointSize": 6, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 22 + }, + "id": 32, + "options": { + "legend": { + "calcs": [], + "displayMode": "hidden", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi" + } + }, + "targets": [ + { + "exemplar": true, + "expr": "monero_transaction_pool_double_spends", + "interval": "", + "legendFormat": "double spends", + "refId": "A" + }, + { + "exemplar": true, + "expr": "monero_transaction_pool_older_than_10m", + "hide": false, + "interval": "", + "legendFormat": "older than 10m", + "refId": "B" + }, + { + "exemplar": true, + "expr": "monero_transaction_pool_failing_transactions", + "hide": false, + "interval": "", + "legendFormat": "failing transactions", + "refId": "C" + }, + { + "exemplar": true, + "expr": "monero_transaction_pool_not_relayed", + "hide": false, + "interval": "", + "legendFormat": "not relayed", + "refId": "D" + } + ], + "title": "Weird cases", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": null, + "description": "Distribution of for how long transactions have been sitting in the pool.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": -1, + "drawStyle": "points", + "fillOpacity": 26, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 3, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "1" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-purple", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "0.99" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-blue", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "0.95" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-green", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "0.9" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-yellow", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "0.75" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-orange", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "0.5" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-red", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "0.25" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "light-red", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 22 + }, + "id": 34, + "options": { + "legend": { + "calcs": [], + "displayMode": "hidden", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi" + } + }, + "targets": [ + { + "exemplar": true, + "expr": "monero_transaction_pool_transactions_age{quantile=~\"$quantile\"}", + "interval": "", + "legendFormat": "{{ quantile }}", + "refId": "A" + } + ], + "title": "Transaction Age", + "transparent": true, + "type": "timeseries" + }, + { + "collapsed": true, + "datasource": null, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 29 + }, + "id": 2, + "panels": [ + { + "datasource": null, + "description": "Height of the last block in the chain.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": -1, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "stepBefore", + "lineWidth": 1, + "pointSize": 6, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 2 + }, + "id": 36, + "options": { + "legend": { + "calcs": [], + "displayMode": "hidden", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi" + } + }, + "targets": [ + { + "exemplar": true, + "expr": "monero_lastblock_height", + "interval": "", + "legendFormat": "transactions", + "refId": "A" + } + ], + "title": "Height", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": null, + "description": "Total block rewards granted to the miner of this block (transaction fees + block subsidy).", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": -1, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "stepBefore", + "lineWidth": 1, + "pointSize": 6, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ɱ" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 2 + }, + "id": 6, + "options": { + "legend": { + "calcs": [], + "displayMode": "hidden", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi" + } + }, + "targets": [ + { + "exemplar": true, + "expr": "monero_lastblock_reward_monero", + "interval": "", + "legendFormat": "total rewards", + "refId": "A" + } + ], + "title": "Rewards", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": null, + "description": "Newly amount of Monero created in this block as a miner subsidy. ", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": -1, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "stepBefore", + "lineWidth": 1, + "pointSize": 6, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 6, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ɱ" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 2 + }, + "id": 7, + "options": { + "legend": { + "calcs": [], + "displayMode": "hidden", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi" + } + }, + "targets": [ + { + "exemplar": true, + "expr": "monero_lastblock_subsidy_monero", + "interval": "", + "legendFormat": "block subsidy", + "refId": "A" + } + ], + "title": "Subsidy", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": null, + "description": "Total amount of fees (in Monero) included in the last block.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": -1, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "stepBefore", + "lineWidth": 1, + "pointSize": 6, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ɱ" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 9 + }, + "id": 4, + "options": { + "legend": { + "calcs": [], + "displayMode": "hidden", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi" + } + }, + "targets": [ + { + "exemplar": true, + "expr": "monero_lastblock_fees_monero", + "interval": "", + "legendFormat": "fee", + "refId": "A" + } + ], + "title": "Fees", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": null, + "description": "The distribution of the amount of inputs used in transactions included in this block. \n", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": -1, + "drawStyle": "points", + "fillOpacity": 26, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 3, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "1" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-purple", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "0.99" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-blue", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "0.95" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-green", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "0.9" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-yellow", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "0.75" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-orange", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "0.5" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-red", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "0.25" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "light-red", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 9 + }, + "id": 8, + "options": { + "legend": { + "calcs": [], + "displayMode": "hidden", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi" + } + }, + "targets": [ + { + "exemplar": true, + "expr": "monero_lastblock_transactions_inputs{quantile=~\"$quantile\"}", + "interval": "", + "legendFormat": "{{ quantile }}", + "refId": "A" + } + ], + "title": "Transaction Inputs", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": null, + "description": "The distribution of the amount of outputs used in transactions included in this block. \n", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": -1, + "drawStyle": "points", + "fillOpacity": 26, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 3, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "1" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-purple", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "0.99" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-blue", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "0.95" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-green", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "0.9" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-yellow", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "0.75" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-orange", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "0.5" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-red", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "0.25" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "light-red", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 9 + }, + "id": 9, + "options": { + "legend": { + "calcs": [], + "displayMode": "hidden", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi" + } + }, + "targets": [ + { + "exemplar": true, + "expr": "monero_lastblock_transactions_outputs{quantile=~\"$quantile\"}", + "interval": "", + "legendFormat": "{{ quantile }}", + "refId": "A" + } + ], + "title": "Transaction Outputs", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": null, + "description": "The distribution fees (in micronero) per kB for the transactions included in the block seen as last at the time.\n", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": -1, + "drawStyle": "points", + "fillOpacity": 26, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 3, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "µɱ" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "1" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-purple", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "0.99" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-blue", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "0.95" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-green", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "0.9" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-yellow", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "0.75" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-orange", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "0.5" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-red", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "0.25" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "light-red", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 16 + }, + "id": 5, + "options": { + "legend": { + "calcs": [], + "displayMode": "hidden", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi" + } + }, + "targets": [ + { + "exemplar": true, + "expr": "monero_lastblock_fees_micronero_per_kb{quantile=~\"$quantile\"}", + "interval": "", + "legendFormat": "{{ quantile }}", + "refId": "A" + } + ], + "title": "Transaction Fees Distribution (per kB)", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": null, + "description": "Number of transactions included in the block", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": -1, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "stepBefore", + "lineWidth": 1, + "pointSize": 6, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 16 + }, + "id": 14, + "options": { + "legend": { + "calcs": [], + "displayMode": "hidden", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi" + } + }, + "targets": [ + { + "exemplar": true, + "expr": "monero_lastblock_transactions_inputs_sum", + "interval": "", + "legendFormat": "inputs", + "refId": "A" + }, + { + "exemplar": true, + "expr": "monero_lastblock_transactions_outputs_sum", + "hide": false, + "interval": "", + "legendFormat": "outputs", + "refId": "B" + } + ], + "title": "Inputs and Outputs", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": null, + "description": "Difficulty used for mining the last block", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": -1, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "stepBefore", + "lineWidth": 1, + "pointSize": 6, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "sci" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 16 + }, + "id": 12, + "options": { + "legend": { + "calcs": [], + "displayMode": "hidden", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi" + } + }, + "targets": [ + { + "exemplar": true, + "expr": "monero_lastblock_difficulty", + "interval": "", + "legendFormat": "size", + "refId": "A" + } + ], + "title": "Difficulty", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": null, + "description": "Number of transactions included in the block", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": -1, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "stepBefore", + "lineWidth": 1, + "pointSize": 6, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 23 + }, + "id": 10, + "options": { + "legend": { + "calcs": [], + "displayMode": "hidden", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi" + } + }, + "targets": [ + { + "exemplar": true, + "expr": "monero_lastblock_transactions", + "interval": "", + "legendFormat": "transactions", + "refId": "A" + } + ], + "title": "Transactions", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": null, + "description": "Total size of the block", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": -1, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "stepBefore", + "lineWidth": 1, + "pointSize": 6, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "decbytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 23 + }, + "id": 11, + "options": { + "legend": { + "calcs": [], + "displayMode": "hidden", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi" + } + }, + "targets": [ + { + "exemplar": true, + "expr": "monero_lastblock_size_bytes", + "interval": "", + "legendFormat": "size", + "refId": "A" + } + ], + "title": "Size", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": null, + "description": "The distribution of sizes of the transactions included in this block.\n", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": -1, + "drawStyle": "points", + "fillOpacity": 26, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 3, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "decbytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "1" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-purple", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "0.99" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-blue", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "0.95" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-green", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "0.9" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-yellow", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "0.75" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-orange", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "0.5" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-red", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "0.25" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "light-red", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 23 + }, + "id": 13, + "options": { + "legend": { + "calcs": [], + "displayMode": "hidden", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi" + } + }, + "targets": [ + { + "exemplar": true, + "expr": "monero_lastblock_transactions_size_bytes{quantile=~\"$quantile\"}", + "interval": "", + "legendFormat": "{{ quantile }}", + "refId": "A" + } + ], + "title": "Transaction Size Distribution", + "transparent": true, + "type": "timeseries" + } + ], + "title": "Last Block", + "type": "row" + } + ], + "refresh": "1m", + "schemaVersion": 30, + "style": "dark", + "tags": [], + "templating": { + "list": [ + { + "allValue": null, + "current": { + "selected": true, + "text": [ + "All" + ], + "value": [ + "$__all" + ] + }, + "datasource": null, + "definition": "label_values(monero_lastblock_transactions_inputs,quantile)", + "description": "quantile to filter for in the panels that use distributions", + "error": null, + "hide": 0, + "includeAll": true, + "label": "quantile", + "multi": true, + "name": "quantile", + "options": [], + "query": { + "query": "label_values(monero_lastblock_transactions_inputs,quantile)", + "refId": "StandardVariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + } + ] + }, + "time": { + "from": "now-3h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "", + "title": "Monero", + "uid": "K9dtfZG7z", + "version": 24 +} diff --git a/grafana/provisioning/dashboards/all.yaml b/grafana/provisioning/dashboards/all.yaml new file mode 100644 index 0000000..5a31c91 --- /dev/null +++ b/grafana/provisioning/dashboards/all.yaml @@ -0,0 +1,12 @@ +apiVersion: 1 + +providers: + - name: 'fs' + orgId: 1 + folder: '' + type: 'file' + updateIntervalSeconds: 30 + allowUiUpdates: true + options: + path: '/var/lib/grafana/dashboards' + foldersFromFilesStructure: true diff --git a/grafana/provisioning/datasources/all.yaml b/grafana/provisioning/datasources/all.yaml new file mode 100644 index 0000000..82aef7f --- /dev/null +++ b/grafana/provisioning/datasources/all.yaml @@ -0,0 +1,12 @@ +apiVersion: 1 + +datasources: + - name: prometheus + type: prometheus + access: proxy + orgId: 1 + url: http://xps.utxo.com.br:9090 + isDefault: true + version: 1 + editable: false + timeInterval: 30s diff --git a/hack/geoip.mmdb b/hack/geoip.mmdb new file mode 100644 index 0000000..0c6c4a7 Binary files /dev/null and b/hack/geoip.mmdb differ diff --git a/pkg/collector/collector.go b/pkg/collector/collector.go index f931877..ef893f7 100644 --- a/pkg/collector/collector.go +++ b/pkg/collector/collector.go @@ -3,13 +3,9 @@ package collector import ( "context" "fmt" - "math" "net" - "reflect" - "strconv" "time" - "github.com/bmizerany/perks/quantile" "github.com/go-logr/logr" "github.com/go-logr/zapr" "github.com/prometheus/client_golang/prometheus" @@ -63,6 +59,10 @@ func WithCountryMapper(v CountryMapper) func(c *Collector) { } } +func defaultCountryMapper(_ net.IP) (string, error) { + return "unknown", nil +} + // Register registers this collector with the global prometheus collectors // registry making it available for an exporter to collect our metrics. // @@ -74,8 +74,8 @@ func Register(client *daemon.Client, opts ...Option) error { c := &Collector{ client: client, - log: zapr.NewLogger(defaultLogger.Named("collector")), - countryMapper: func(_ net.IP) (string, error) { return "lol", nil }, + log: zapr.NewLogger(defaultLogger), + countryMapper: defaultCountryMapper, } for _, opt := range opts { @@ -97,8 +97,13 @@ type CollectFunc func(ctx context.Context, ch chan<- prometheus.Metric) error // Describe implements the Describe function of the Collector interface. // func (c *Collector) Describe(ch chan<- *prometheus.Desc) { - // Because we can present the description of the metrics at collection time, we - // don't need to write anything to the channel. + // Because we can present the description of the metrics at collection + // time, we don't need to write anything to the channel. +} + +type CustomCollector interface { + Name() string + Collect(ctx context.Context) error } // Collect implements the Collect function of the Collector interface. @@ -114,28 +119,21 @@ func (c *Collector) Collect(ch chan<- prometheus.Metric) { g, ctx = errgroup.WithContext(ctx) - for _, collector := range []struct { - name string - fn CollectFunc - }{ - {"info_stats", c.CollectInfoStats}, - {"mempool_stats", c.CollectMempoolStats}, - {"last_block_header", c.CollectLastBlockHeader}, - {"bans", c.CollectBans}, - {"peer_height_divergence", c.CollectPeerHeightDivergence}, - {"fee_estimate", c.CollectFeeEstimate}, - {"peers", c.CollectPeers}, - {"connections", c.CollectConnections}, - {"last_block_stats", c.CollectLastBlockStats}, - {"peers_live_time", c.CollectPeersLiveTime}, - {"net_stats", c.CollectNetStats}, - {"collect_rpc", c.CollectRPC}, + for _, collector := range []CustomCollector{ + NewLastBlockStatsCollector(c.client, ch), + NewTransactionPoolCollector(c.client, ch), + NewRPCCollector(c.client, ch), + NewConnectionsCollector(c.client, ch), + NewPeersCollector(c.client, ch), + NewNetStatsCollector(c.client, ch), + NewOverallCollector(c.client, ch), } { collector := collector g.Go(func() error { - if err := collector.fn(ctx, ch); err != nil { - return fmt.Errorf("collector fn '%s': %w", collector.name, err) + if err := collector.Collect(ctx); err != nil { + return fmt.Errorf("%s collect: %w", + collector.Name(), err) } return nil @@ -146,495 +144,3 @@ func (c *Collector) Collect(ch chan<- prometheus.Metric) { c.log.Error(err, "wait") } } - -// CollectConnections. -// -func (c *Collector) CollectConnections(ctx context.Context, ch chan<- prometheus.Metric) error { - res, err := c.client.GetConnections(ctx) - if err != nil { - return fmt.Errorf("get connections: %w", err) - } - - perCountryCounter := map[string]uint64{} - for _, conn := range res.Connections { - country, err := c.countryMapper(net.ParseIP(conn.Host)) - if err != nil { - return fmt.Errorf("to country '%s': %w", conn.Host, err) - } - - perCountryCounter[country]++ - } - - desc := prometheus.NewDesc( - "monero_connections", - "connections info", - []string{"country"}, nil, - ) - - for country, count := range perCountryCounter { - ch <- prometheus.MustNewConstMetric( - desc, - prometheus.GaugeValue, - float64(count), - country, - ) - } - - return nil -} - -// CollectPeers. -// -func (c *Collector) CollectPeers(ctx context.Context, ch chan<- prometheus.Metric) error { - res, err := c.client.GetPeerList(ctx) - if err != nil { - return fmt.Errorf("get peer list: %w", err) - } - - perCountryCounter := map[string]uint64{} - for _, peer := range res.WhiteList { - country, err := c.countryMapper(net.ParseIP(peer.Host)) - if err != nil { - return fmt.Errorf("to country '%s': %w", peer.Host, err) - } - - perCountryCounter[country]++ - } - - desc := prometheus.NewDesc( - "monero_peers_new", - "peers info", - []string{"country"}, nil, - ) - - for country, count := range perCountryCounter { - ch <- prometheus.MustNewConstMetric( - desc, - prometheus.GaugeValue, - float64(count), - country, - ) - } - - return nil -} - -// CollectLastBlockHeader. -// -func (c *Collector) CollectLastBlockHeader(ctx context.Context, ch chan<- prometheus.Metric) error { - res, err := c.client.GetLastBlockHeader(ctx) - if err != nil { - return fmt.Errorf("get last block header: %w", err) - } - - metrics, err := c.toMetrics("last_block_header", &res.BlockHeader) - if err != nil { - return fmt.Errorf("to metrics: %w", err) - } - - for _, metric := range metrics { - ch <- metric - } - - return nil -} - -// CollectInfoStats. -// -func (c *Collector) CollectInfoStats(ctx context.Context, ch chan<- prometheus.Metric) error { - res, err := c.client.GetInfo(ctx) - if err != nil { - return fmt.Errorf("get transaction pool: %w", err) - } - - metrics, err := c.toMetrics("info", res) - if err != nil { - return fmt.Errorf("to metrics: %w", err) - } - - for _, metric := range metrics { - ch <- metric - } - - return nil -} - -func (c *Collector) CollectLastBlockStats(ctx context.Context, ch chan<- prometheus.Metric) error { - lastBlockHeaderResp, err := c.client.GetLastBlockHeader(ctx) - if err != nil { - return fmt.Errorf("get last block header: %w", err) - } - - currentHeight := lastBlockHeaderResp.BlockHeader.Height - block, err := c.client.GetBlock(ctx, daemon.GetBlockRequestParameters{ - Height: currentHeight, - }) - if err != nil { - return fmt.Errorf("get block '%d': %w", currentHeight, err) - } - - blockJSON, err := block.InnerJSON() - if err != nil { - return fmt.Errorf("block inner json: %w", err) - } - - txnsResp, err := c.client.GetTransactions(ctx, blockJSON.TxHashes) - if err != nil { - return fmt.Errorf("get txns: %w", err) - } - - txns, err := txnsResp.GetTransactions() - if err != nil { - return fmt.Errorf("get transactions: %w", err) - } - - phis := []float64{0.25, 0.50, 0.75, 0.90, 0.95, 0.99, 1} - - var ( - streamTxnSize = quantile.NewTargeted(phis...) - sumTxnSize = float64(0) - quantilesTxnSize = make(map[float64]float64, len(phis)) - - streamTxnFee = quantile.NewTargeted(phis...) - sumTxnFee = float64(0) - quantilesTxnFee = make(map[float64]float64, len(phis)) - - streamVin = quantile.NewTargeted(phis...) - sumVin = float64(0) - quantilesVin = make(map[float64]float64, len(phis)) - - streamVout = quantile.NewTargeted(phis...) - sumVout = float64(0) - quantilesVout = make(map[float64]float64, len(phis)) - ) - - for _, txn := range txnsResp.TxsAsHex { - streamTxnSize.Insert(float64(len(txn))) - sumTxnSize += float64(len(txn)) - } - - for _, txn := range txns { - streamTxnFee.Insert(float64(txn.RctSignatures.Txnfee)) - sumTxnFee += float64(txn.RctSignatures.Txnfee) - - streamVin.Insert(float64(len(txn.Vin))) - sumVin += float64(len(txn.Vin)) - - streamVout.Insert(float64(len(txn.Vout))) - sumVout += float64(len(txn.Vout)) - } - - for _, phi := range phis { - quantilesTxnSize[phi] = streamTxnSize.Query(phi) - quantilesTxnFee[phi] = streamTxnFee.Query(phi) - quantilesVin[phi] = streamVin.Query(phi) - quantilesVout[phi] = streamVout.Query(phi) - } - - ch <- prometheus.MustNewConstSummary( - prometheus.NewDesc( - "monero_last_block_txn_size", - "distribution of tx sizes", - nil, nil, - ), - uint64(streamTxnSize.Count()), - sumTxnSize, - quantilesTxnSize, - ) - - ch <- prometheus.MustNewConstSummary( - prometheus.NewDesc( - "monero_last_block_txn_fee", - "distribution of outputs in last block", - nil, nil, - ), - uint64(streamTxnFee.Count()), - sumTxnFee, - quantilesTxnFee, - ) - - ch <- prometheus.MustNewConstSummary( - prometheus.NewDesc( - "monero_last_block_vin", - "distribution of inputs in last block", - nil, nil, - ), - uint64(streamVin.Count()), - sumVin, - quantilesVin, - ) - - ch <- prometheus.MustNewConstSummary( - prometheus.NewDesc( - "monero_last_block_vout", - "distribution of outputs in last block", - nil, nil, - ), - uint64(streamVout.Count()), - sumVout, - quantilesVout, - ) - - return nil -} - -func (c *Collector) CollectPeerHeightDivergence(ctx context.Context, ch chan<- prometheus.Metric) error { - blockCountRes, err := c.client.GetBlockCount(ctx) - if err != nil { - return fmt.Errorf("get block count: %w", err) - } - - res, err := c.client.GetConnections(ctx) - if err != nil { - return fmt.Errorf("get connections: %w", err) - } - - phis := []float64{0.25, 0.50, 0.55, 0.60, 0.65, 0.70, 0.75, 0.80, 0.85, 0.90, 0.95, 0.99} - stream := quantile.NewTargeted(phis...) - - sum := float64(0) - ourHeight := blockCountRes.Count - for _, conn := range res.Connections { - diff := math.Abs(float64(ourHeight - uint64(conn.Height))) - - stream.Insert(diff) - sum += diff - } - - quantiles := make(map[float64]float64, len(phis)) - for _, phi := range phis { - quantiles[phi] = stream.Query(phi) - } - - desc := prometheus.NewDesc( - "monero_height_divergence", - "how much our peers diverge from us in block height", - nil, nil, - ) - - ch <- prometheus.MustNewConstSummary( - desc, - uint64(stream.Count()), - sum, - quantiles, - ) - - return nil -} - -func (c *Collector) CollectPeersLiveTime(ctx context.Context, ch chan<- prometheus.Metric) error { - res, err := c.client.GetConnections(ctx) - if err != nil { - return fmt.Errorf("get connections: %w", err) - } - - var ( - phis = []float64{0.25, 0.50, 0.55, 0.60, 0.65, 0.70, 0.75, 0.80, 0.85, 0.90, 0.95, 0.99} - sum = float64(0) - stream = quantile.NewTargeted(phis...) - quantiles = make(map[float64]float64, len(phis)) - ) - - for _, conn := range res.Connections { - stream.Insert(float64(conn.LiveTime)) - sum += float64(conn.LiveTime) - } - - for _, phi := range phis { - quantiles[phi] = stream.Query(phi) - } - - desc := prometheus.NewDesc( - "monero_connections_livetime", - "peers livetime distribution", - nil, nil, - ) - - ch <- prometheus.MustNewConstSummary( - desc, - uint64(stream.Count()), - sum, - quantiles, - ) - - return nil -} - -func (c *Collector) CollectNetStats(ctx context.Context, ch chan<- prometheus.Metric) error { - res, err := c.client.GetNetStats(ctx) - if err != nil { - return fmt.Errorf("get fee estimate: %w", err) - } - - ch <- prometheus.MustNewConstMetric( - prometheus.NewDesc( - "monero_net_total_in_bytes", - "network statistics", - nil, nil, - ), - prometheus.CounterValue, - float64(res.TotalBytesIn), - ) - - ch <- prometheus.MustNewConstMetric( - prometheus.NewDesc( - "monero_net_total_out_bytes", - "network statistics", - nil, nil, - ), - prometheus.CounterValue, - float64(res.TotalBytesOut), - ) - - return nil -} - -func (c *Collector) CollectFeeEstimate(ctx context.Context, ch chan<- prometheus.Metric) error { - res, err := c.client.GetFeeEstimate(ctx, 1) - if err != nil { - return fmt.Errorf("get fee estimate: %w", err) - } - - desc := prometheus.NewDesc( - "monero_fee_estimate", - "fee estimate for 1 grace block", - nil, nil, - ) - - ch <- prometheus.MustNewConstMetric( - desc, - prometheus.GaugeValue, - float64(res.Fee), - ) - - return nil -} - -func (c *Collector) CollectRPC(ctx context.Context, ch chan<- prometheus.Metric) error { - res, err := c.client.RPCAccessTracking(ctx) - if err != nil { - return fmt.Errorf("rpc access tracking: %w", err) - } - - descCount := prometheus.NewDesc( - "monero_rpc_count", - "todo", - []string{"method"}, nil, - ) - - descTime := prometheus.NewDesc( - "monero_rpc_time", - "todo", - []string{"method"}, nil, - ) - - for _, d := range res.Data { - ch <- prometheus.MustNewConstMetric( - descCount, - prometheus.CounterValue, - float64(d.Count), - d.RPC, - ) - - ch <- prometheus.MustNewConstMetric( - descTime, - prometheus.CounterValue, - float64(d.Time), - d.RPC, - ) - } - - return nil -} - -func (c *Collector) CollectBans(ctx context.Context, ch chan<- prometheus.Metric) error { - res, err := c.client.GetBans(ctx) - if err != nil { - return fmt.Errorf("get bans: %w", err) - } - - desc := prometheus.NewDesc( - "monero_bans", - "number of nodes banned", - nil, nil, - ) - - ch <- prometheus.MustNewConstMetric( - desc, - prometheus.GaugeValue, - float64(len(res.Bans)), - ) - - return nil -} - -func (c *Collector) CollectMempoolStats(ctx context.Context, ch chan<- prometheus.Metric) error { - res, err := c.client.GetTransactionPoolStats(ctx) - if err != nil { - return fmt.Errorf("get transaction pool: %w", err) - } - - metrics, err := c.toMetrics("mempool", &res.PoolStats) - if err != nil { - return fmt.Errorf("to metrics: %w", err) - } - - for _, metric := range metrics { - ch <- metric - } - - return nil -} - -func (c *Collector) toMetrics(ns string, res interface{}) ([]prometheus.Metric, error) { - var ( - metrics = []prometheus.Metric{} - v = reflect.ValueOf(res).Elem() - err error - ) - - for i := 0; i < v.NumField(); i++ { - observation := float64(0) - field := v.Field(i) - - switch field.Type().Kind() { - case reflect.Bool: - if field.Bool() { - observation = float64(1) - } - - case - reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, - reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, - reflect.Float32, reflect.Float64, - reflect.Uintptr: - - observation, err = strconv.ParseFloat(fmt.Sprintf("%v", field.Interface()), 64) - if err != nil { - return nil, fmt.Errorf("parse float: %w", err) - } - - default: - c.log.Info("ignoring", - "field", v.Type().Field(i).Name, - "type", field.Type().Kind().String(), - ) - - continue - } - - tag := v.Type().Field(i).Tag.Get("json") - - metrics = append(metrics, prometheus.MustNewConstMetric( - prometheus.NewDesc( - "monero_"+ns+"_"+tag, - "info for "+tag, - nil, nil, - ), - prometheus.GaugeValue, - observation, - )) - } - - return metrics, nil -} diff --git a/pkg/collector/collector_connections.go b/pkg/collector/collector_connections.go new file mode 100644 index 0000000..ae36fd5 --- /dev/null +++ b/pkg/collector/collector_connections.go @@ -0,0 +1,151 @@ +package collector + +import ( + "context" + "fmt" + + "github.com/prometheus/client_golang/prometheus" + + "github.com/cirocosta/go-monero/pkg/rpc/daemon" +) + +type ConnectionsCollector struct { + client *daemon.Client + metricsC chan<- prometheus.Metric + + connections *daemon.GetConnectionsResult +} + +var _ CustomCollector = (*ConnectionsCollector)(nil) + +func NewConnectionsCollector( + client *daemon.Client, metricsC chan<- prometheus.Metric, +) *ConnectionsCollector { + return &ConnectionsCollector{ + client: client, + metricsC: metricsC, + } +} + +func (c *ConnectionsCollector) Name() string { + return "connections" +} + +func (c *ConnectionsCollector) Collect(ctx context.Context) error { + err := c.fetchData(ctx) + if err != nil { + return fmt.Errorf("fetch data: %w", err) + } + + c.collectConnectionsCount() + c.collectHeightDistribution() + c.collectDataRates() + c.collectConnectionAges() + + return nil +} + +func (c *ConnectionsCollector) collectConnectionAges() { + summary := NewSummary() + + for _, conn := range c.connections.Connections { + summary.Insert(float64(conn.LiveTime)) + } + + c.metricsC <- prometheus.MustNewConstSummary( + prometheus.NewDesc( + "monero_p2p_connections_age", + "distribution of age of the connections we have", + nil, nil, + ), + summary.Count(), summary.Sum(), summary.Quantiles(), + ) +} + +func (c *ConnectionsCollector) collectDataRates() { + summaryRx := NewSummary() + summaryTx := NewSummary() + + for _, conn := range c.connections.Connections { + summaryRx.Insert(float64(conn.RecvCount) / float64(conn.LiveTime)) + summaryTx.Insert(float64(conn.SendCount) / float64(conn.LiveTime)) + } + + c.metricsC <- prometheus.MustNewConstSummary( + prometheus.NewDesc( + "monero_p2p_connections_rx_rate_bps", + "distribution of data receive rate in bytes/s", + nil, nil, + ), + summaryRx.Count(), summaryRx.Sum(), summaryRx.Quantiles(), + ) + + c.metricsC <- prometheus.MustNewConstSummary( + prometheus.NewDesc( + "monero_p2p_connections_tx_rate_bps", + "distribution of data transmit rate in bytes/s", + nil, nil, + ), + summaryTx.Count(), summaryTx.Sum(), summaryTx.Quantiles(), + ) +} + +func (c *ConnectionsCollector) collectHeightDistribution() { + summary := NewSummary() + for _, conn := range c.connections.Connections { + summary.Insert(float64(conn.Height)) + } + + c.metricsC <- prometheus.MustNewConstSummary( + prometheus.NewDesc( + "monero_p2p_connections_height", + "distribution the height of the peers "+ + "connected to/from us", + nil, nil, + ), + summary.Count(), summary.Sum(), summary.Quantiles(), + ) +} + +func (c *ConnectionsCollector) collectConnectionsCount() { + desc := prometheus.NewDesc( + "monero_p2p_connections", + "number of connections to/from this node", + []string{"type", "state"}, nil, + ) + + type key struct { + ttype string + state string + } + + counters := map[key]float64{} + + for _, conn := range c.connections.Connections { + ttype := "in" + if !conn.Incoming { + ttype = "out" + } + + counters[key{ttype, conn.State}]++ + } + + for k, v := range counters { + c.metricsC <- prometheus.MustNewConstMetric( + desc, + prometheus.GaugeValue, + v, + k.ttype, k.state, + ) + } +} + +func (c *ConnectionsCollector) fetchData(ctx context.Context) error { + res, err := c.client.GetConnections(ctx) + if err != nil { + return fmt.Errorf("get connections: %w", err) + } + + c.connections = res + return nil +} diff --git a/pkg/collector/collector_lastblock.go b/pkg/collector/collector_lastblock.go new file mode 100644 index 0000000..7f1fa88 --- /dev/null +++ b/pkg/collector/collector_lastblock.go @@ -0,0 +1,300 @@ +package collector + +import ( + "context" + "fmt" + + "github.com/prometheus/client_golang/prometheus" + + "github.com/cirocosta/go-monero/pkg/constant" + "github.com/cirocosta/go-monero/pkg/rpc/daemon" +) + +type LastBlockStatsCollector struct { + client *daemon.Client + metricsC chan<- prometheus.Metric + + txns []*daemon.TransactionJSON + txnSizes []int + header daemon.BlockHeader +} + +var _ CustomCollector = (*LastBlockStatsCollector)(nil) + +func NewLastBlockStatsCollector( + client *daemon.Client, metricsC chan<- prometheus.Metric, +) *LastBlockStatsCollector { + return &LastBlockStatsCollector{ + client: client, + metricsC: metricsC, + } +} + +func (c *LastBlockStatsCollector) Name() string { + return "lastblock" +} + +func (c *LastBlockStatsCollector) Collect(ctx context.Context) error { + err := c.fetchData(ctx) + if err != nil { + return fmt.Errorf("fetch last block data: %w", err) + } + + c.collectBlockSize() + c.collectDifficulty() + c.collectFees() + c.collectHeight() + c.collectReward() + c.collectSubsidy() + c.collectTransactionsCount() + c.collectTransactionsFeePerKb() + c.collectTransactionsInputs() + c.collectTransactionsOutputs() + c.collectTransactionsSize() + + return nil +} + +func (c *LastBlockStatsCollector) fetchData(ctx context.Context) error { + lastBlockHeaderResp, err := c.client.GetLastBlockHeader(ctx) + if err != nil { + return fmt.Errorf("get last block header: %w", err) + } + + lastBlockHash := lastBlockHeaderResp.BlockHeader.Hash + + params := daemon.GetBlockRequestParameters{ + Hash: lastBlockHash, + } + blockResp, err := c.client.GetBlock(ctx, params) + if err != nil { + return fmt.Errorf("get block '%s': %w", lastBlockHash, err) + } + + blockJSON, err := blockResp.InnerJSON() + if err != nil { + return fmt.Errorf("block inner json: %w", err) + } + + txnsResp, err := c.client.GetTransactions(ctx, blockJSON.TxHashes) + if err != nil { + return fmt.Errorf("get txns: %w", err) + } + + txnSizes := make([]int, len(txnsResp.Txs)) + for idx, t := range txnsResp.Txs { + txnSizes[idx] = len(t.AsHex) / 2 + } + + txns, err := txnsResp.GetTransactions() + if err != nil { + return fmt.Errorf("get transactions: %w", err) + } + + c.txns = txns + c.txnSizes = txnSizes + c.header = blockResp.BlockHeader + + return nil +} + +func (c *LastBlockStatsCollector) collectBlockSize() { + desc := prometheus.NewDesc( + "monero_lastblock_size_bytes", + "total size of the last block", + nil, nil, + ) + + c.metricsC <- prometheus.MustNewConstMetric( + desc, + prometheus.GaugeValue, + float64(c.header.BlockSize), + ) +} + +func (c *LastBlockStatsCollector) collectDifficulty() { + desc := prometheus.NewDesc( + "monero_lastblock_difficulty", + "difficulty used for the last block", + nil, nil, + ) + + c.metricsC <- prometheus.MustNewConstMetric( + desc, + prometheus.GaugeValue, + float64(c.header.Difficulty), + ) +} + +func (c *LastBlockStatsCollector) collectFees() { + desc := prometheus.NewDesc( + "monero_lastblock_fees_monero", + "total amount of fees included in this block", + nil, nil, + ) + + c.metricsC <- prometheus.MustNewConstMetric( + desc, + prometheus.GaugeValue, + float64(c.gatherFees(c.txns))/constant.XMR, + ) +} + +func (c *LastBlockStatsCollector) collectHeight() { + desc := prometheus.NewDesc( + "monero_lastblock_height", + "height of the last block", + nil, nil, + ) + + c.metricsC <- prometheus.MustNewConstMetric( + desc, + prometheus.GaugeValue, + float64(c.header.Height), + ) +} + +func (c *LastBlockStatsCollector) collectReward() { + desc := prometheus.NewDesc( + "monero_lastblock_reward_monero", + "total amount of rewards granted in the last block "+ + "(subsidy + fees)", + nil, nil, + ) + + c.metricsC <- prometheus.MustNewConstMetric( + desc, + prometheus.GaugeValue, + float64(c.header.Reward)/constant.XMR, + ) +} + +func (c *LastBlockStatsCollector) collectSubsidy() { + totalReward := float64(c.header.Reward) + fees := float64(c.gatherFees(c.txns)) + subsidy := (totalReward - fees) / constant.XMR + + desc := prometheus.NewDesc( + "monero_lastblock_subsidy_monero", + "newly minted monero for this block", + nil, nil, + ) + + c.metricsC <- prometheus.MustNewConstMetric( + desc, + prometheus.GaugeValue, + subsidy, + ) +} + +func (c *LastBlockStatsCollector) collectTransactionsCount() { + desc := prometheus.NewDesc( + "monero_lastblock_transactions", + "number of transactions seen in the last block", + nil, nil, + ) + + c.metricsC <- prometheus.MustNewConstMetric( + desc, + prometheus.GaugeValue, + float64(c.header.NumTxes), + ) +} + +func (c *LastBlockStatsCollector) collectTransactionsFeePerKb() { + summary := NewSummary() + for idx, txn := range c.txns { + fee := float64(txn.RctSignatures.Txnfee) / constant.MicroXMR + size := float64(c.txnSizes[idx]) / 1024 + + summary.Insert(fee / size) + } + + c.metricsC <- prometheus.MustNewConstSummary( + prometheus.NewDesc( + "monero_lastblock_fees_micronero_per_kb", + "distribution of the feeperkb utilized for txns", + nil, nil, + ), + summary.Count(), summary.Sum(), summary.Quantiles(), + ) +} + +func (c *LastBlockStatsCollector) collectTransactionsSize() { + summary := NewSummary() + for _, size := range c.txnSizes { + summary.Insert(float64(size)) + } + + c.metricsC <- prometheus.MustNewConstSummary( + prometheus.NewDesc( + "monero_lastblock_transactions_size_bytes", + "distribution of the size of the transactions included", + nil, nil, + ), + summary.Count(), summary.Sum(), summary.Quantiles(), + ) +} + +func (c *LastBlockStatsCollector) collectTransactionsInputs() { + summary := NewSummary() + for _, txn := range c.txns { + summary.Insert(float64(len(txn.Vin))) + } + + c.metricsC <- prometheus.MustNewConstSummary( + prometheus.NewDesc( + "monero_lastblock_transactions_inputs", + "distribution of inputs in the last block", + nil, nil, + ), + summary.Count(), summary.Sum(), summary.Quantiles(), + ) +} + +func (c *LastBlockStatsCollector) collectTransactionsOutputs() { + summary := NewSummary() + for _, txn := range c.txns { + summary.Insert(float64(len(txn.Vout))) + } + + c.metricsC <- prometheus.MustNewConstSummary( + prometheus.NewDesc( + "monero_lastblock_transactions_outputs", + "distribution of outputs in the last block", + nil, nil, + ), + summary.Count(), summary.Sum(), summary.Quantiles(), + ) +} + +func (c *LastBlockStatsCollector) collectVersions() { + c.metricsC <- prometheus.MustNewConstMetric( + prometheus.NewDesc( + "monero_lastblock_version_major", + "major version of the block format", + nil, nil, + ), + prometheus.GaugeValue, + float64(c.header.MajorVersion), + ) + + c.metricsC <- prometheus.MustNewConstMetric( + prometheus.NewDesc( + "monero_lastblock_version_minor", + "minor version of the block format", + nil, nil, + ), + prometheus.GaugeValue, + float64(c.header.MinorVersion), + ) +} + +func (c *LastBlockStatsCollector) gatherFees(txns []*daemon.TransactionJSON) uint64 { + fees := uint64(0) + for _, txn := range txns { + fees += txn.RctSignatures.Txnfee + } + + return fees +} diff --git a/pkg/collector/collector_netstats.go b/pkg/collector/collector_netstats.go new file mode 100644 index 0000000..1a82d6b --- /dev/null +++ b/pkg/collector/collector_netstats.go @@ -0,0 +1,76 @@ +package collector + +import ( + "context" + "fmt" + + "github.com/prometheus/client_golang/prometheus" + + "github.com/cirocosta/go-monero/pkg/rpc/daemon" +) + +type NetStatsCollector struct { + client *daemon.Client + metricsC chan<- prometheus.Metric + + stats *daemon.GetNetStatsResult +} + +var _ CustomCollector = (*NetStatsCollector)(nil) + +func NewNetStatsCollector( + client *daemon.Client, metricsC chan<- prometheus.Metric, +) *NetStatsCollector { + return &NetStatsCollector{ + client: client, + metricsC: metricsC, + } +} + +func (c *NetStatsCollector) Name() string { + return "net" +} + +func (c *NetStatsCollector) Collect(ctx context.Context) error { + err := c.fetchData(ctx) + if err != nil { + return fmt.Errorf("fetch data: %w", err) + } + + c.collectRxTx() + + return nil +} + +func (c *NetStatsCollector) fetchData(ctx context.Context) error { + res, err := c.client.GetNetStats(ctx) + if err != nil { + return fmt.Errorf("get netstats: %w", err) + } + + c.stats = res + + return nil +} + +func (c *NetStatsCollector) collectRxTx() { + c.metricsC <- prometheus.MustNewConstMetric( + prometheus.NewDesc( + "monero_net_rx_bytes", + "number of bytes received by this node", + nil, nil, + ), + prometheus.GaugeValue, + float64(c.stats.TotalBytesIn), + ) + + c.metricsC <- prometheus.MustNewConstMetric( + prometheus.NewDesc( + "monero_net_tx_bytes", + "number of bytes received by this node", + nil, nil, + ), + prometheus.GaugeValue, + float64(c.stats.TotalBytesOut), + ) +} diff --git a/pkg/collector/collector_overall.go b/pkg/collector/collector_overall.go new file mode 100644 index 0000000..f63ce7d --- /dev/null +++ b/pkg/collector/collector_overall.go @@ -0,0 +1,139 @@ +package collector + +import ( + "context" + "fmt" + "time" + + "github.com/prometheus/client_golang/prometheus" + + "github.com/cirocosta/go-monero/pkg/rpc/daemon" +) + +type OverallCollector struct { + client *daemon.Client + metricsC chan<- prometheus.Metric + + info *daemon.GetInfoResult +} + +var _ CustomCollector = (*OverallCollector)(nil) + +func NewOverallCollector( + client *daemon.Client, metricsC chan<- prometheus.Metric, +) *OverallCollector { + return &OverallCollector{ + client: client, + metricsC: metricsC, + } +} + +func (c *OverallCollector) Name() string { + return "overall" +} + +func (c *OverallCollector) Collect(ctx context.Context) error { + err := c.fetchData(ctx) + if err != nil { + return fmt.Errorf("fetch data: %w", err) + } + + c.collect() + + return nil +} + +func (c *OverallCollector) fetchData(ctx context.Context) error { + res, err := c.client.GetInfo(ctx) + if err != nil { + return fmt.Errorf("get netstats: %w", err) + } + + c.info = res + + return nil +} + +func (c *OverallCollector) collect() { + now := time.Now() + + c.metricsC <- prometheus.MustNewConstMetric( + prometheus.NewDesc( + "monero_info_uptime_seconds_total", + "for how long this node has been up", + nil, nil, + ), + prometheus.GaugeValue, + float64(now. + Sub(time.Unix(int64(c.info.StartTime), 0)). + Seconds()), + ) + + c.metricsC <- prometheus.MustNewConstMetric( + prometheus.NewDesc( + "monero_info_alternative_blocks", + "number of blocks alternative to the longest", + nil, nil, + ), + prometheus.GaugeValue, + float64(c.info.AltBlocksCount), + ) + + c.metricsC <- prometheus.MustNewConstMetric( + prometheus.NewDesc( + "monero_info_offline", + "whether the node is offline", + nil, nil, + ), + prometheus.GaugeValue, + boolToFloat64(c.info.Offline), + ) + + c.metricsC <- prometheus.MustNewConstMetric( + prometheus.NewDesc( + "monero_info_mainnet", + "whether the node is connected to mainnet", + nil, nil, + ), + prometheus.GaugeValue, + boolToFloat64(c.info.Mainnet), + ) + + c.metricsC <- prometheus.MustNewConstMetric( + prometheus.NewDesc( + "monero_info_block_size_limit", + "maximum hard limit of a block", + nil, nil, + ), + prometheus.GaugeValue, + float64(c.info.BlockSizeLimit), + ) + + c.metricsC <- prometheus.MustNewConstMetric( + prometheus.NewDesc( + "monero_info_block_size_median", + "current median size for computing dynamic fees", + nil, nil, + ), + prometheus.GaugeValue, + float64(c.info.BlockSizeMedian), + ) + + c.metricsC <- prometheus.MustNewConstMetric( + prometheus.NewDesc( + "monero_info_synchronized", + "", + nil, nil, + ), + prometheus.GaugeValue, + boolToFloat64(c.info.Synchronized), + ) +} + +func boolToFloat64(b bool) float64 { + if b { + return 1 + } + + return 0 +} diff --git a/pkg/collector/collector_peers.go b/pkg/collector/collector_peers.go new file mode 100644 index 0000000..1bf7246 --- /dev/null +++ b/pkg/collector/collector_peers.go @@ -0,0 +1,100 @@ +package collector + +import ( + "context" + "fmt" + "time" + + "github.com/prometheus/client_golang/prometheus" + + "github.com/cirocosta/go-monero/pkg/rpc/daemon" +) + +type PeersCollector struct { + client *daemon.Client + metricsC chan<- prometheus.Metric + + graylist []daemon.Peer + whitelist []daemon.Peer +} + +var _ CustomCollector = (*PeersCollector)(nil) + +func NewPeersCollector( + client *daemon.Client, metricsC chan<- prometheus.Metric, +) *PeersCollector { + return &PeersCollector{ + client: client, + metricsC: metricsC, + } +} + +func (c *PeersCollector) Name() string { + return "peerlist" +} + +func (c *PeersCollector) Collect(ctx context.Context) error { + err := c.fetchData(ctx) + if err != nil { + return fmt.Errorf("fetch data: %w", err) + } + + c.collectPeersCount() + c.collectPeersLastSeen() + + return nil +} + +func (c *PeersCollector) fetchData(ctx context.Context) error { + resp, err := c.client.GetPeerList(ctx) + if err != nil { + return fmt.Errorf("get peerlist: %w", err) + } + + c.graylist = resp.GrayList + c.whitelist = resp.WhiteList + + return nil +} + +func (c *PeersCollector) collectPeersCount() { + desc := prometheus.NewDesc( + "monero_peerlist", + "number of node entries in the peerlist", + []string{"type"}, nil, + ) + + c.metricsC <- prometheus.MustNewConstMetric( + desc, + prometheus.GaugeValue, + float64(len(c.whitelist)), + "white", + ) + + c.metricsC <- prometheus.MustNewConstMetric( + desc, + prometheus.GaugeValue, + float64(len(c.graylist)), + "gray", + ) +} + +func (c *PeersCollector) collectPeersLastSeen() { + now := time.Now() + summary := NewSummary() + + for _, peer := range c.whitelist { + summary.Insert(now. + Sub(time.Unix(peer.LastSeen, 0)). + Seconds()) + } + + c.metricsC <- prometheus.MustNewConstSummary( + prometheus.NewDesc( + "monero_peerlist_lastseen", + "distribution of when our peers have been seen", + nil, nil, + ), + summary.Count(), summary.Sum(), summary.Quantiles(), + ) +} diff --git a/pkg/collector/collector_rpc.go b/pkg/collector/collector_rpc.go new file mode 100644 index 0000000..5d2b038 --- /dev/null +++ b/pkg/collector/collector_rpc.go @@ -0,0 +1,87 @@ +package collector + +import ( + "context" + "fmt" + "time" + + "github.com/prometheus/client_golang/prometheus" + + "github.com/cirocosta/go-monero/pkg/rpc/daemon" +) + +type RPCCollector struct { + client *daemon.Client + metricsC chan<- prometheus.Metric + + accessTracking *daemon.RPCAccessTrackingResult +} + +var _ CustomCollector = (*RPCCollector)(nil) + +func NewRPCCollector( + client *daemon.Client, metricsC chan<- prometheus.Metric, +) *RPCCollector { + return &RPCCollector{ + client: client, + metricsC: metricsC, + } +} + +func (c *RPCCollector) Name() string { + return "rpc" +} + +func (c *RPCCollector) Collect(ctx context.Context) error { + err := c.fetchData(ctx) + if err != nil { + return fmt.Errorf("fetch data: %w", err) + } + + c.collectRPC() + + return nil +} + +func (c *RPCCollector) collectRPC() { + countDesc := prometheus.NewDesc( + "monero_rpc_hits_total", + "number of hits that a particular rpc "+ + "method had since startup", + []string{"method"}, nil, + ) + + timeDesc := prometheus.NewDesc( + "monero_rpc_seconds_total", + "amount of time spent service the method "+ + "since startup", + []string{"method"}, nil, + ) + + for _, d := range c.accessTracking.Data { + c.metricsC <- prometheus.MustNewConstMetric( + countDesc, + prometheus.GaugeValue, + float64(d.Count), + d.RPC, + ) + + c.metricsC <- prometheus.MustNewConstMetric( + timeDesc, + prometheus.GaugeValue, + time.Duration(int64(d.Time)).Seconds(), + d.RPC, + ) + } +} + +func (c *RPCCollector) fetchData(ctx context.Context) error { + res, err := c.client.RPCAccessTracking(ctx) + if err != nil { + return fmt.Errorf("rpc access tracking: %w", err) + } + + c.accessTracking = res + + return nil +} diff --git a/pkg/collector/collector_transactionpool.go b/pkg/collector/collector_transactionpool.go new file mode 100644 index 0000000..ba7d816 --- /dev/null +++ b/pkg/collector/collector_transactionpool.go @@ -0,0 +1,277 @@ +package collector + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/prometheus/client_golang/prometheus" + + "github.com/cirocosta/go-monero/pkg/constant" + "github.com/cirocosta/go-monero/pkg/rpc/daemon" +) + +type TransactionPoolCollector struct { + client *daemon.Client + metricsC chan<- prometheus.Metric + + txns []*daemon.TransactionJSON + pool *daemon.GetTransactionPoolResult + stats *daemon.GetTransactionPoolStatsResult +} + +var _ CustomCollector = (*TransactionPoolCollector)(nil) + +func NewTransactionPoolCollector( + client *daemon.Client, metricsC chan<- prometheus.Metric, +) *TransactionPoolCollector { + return &TransactionPoolCollector{ + client: client, + metricsC: metricsC, + } +} + +func (c *TransactionPoolCollector) Name() string { + return "transaction_pool" +} + +func (c *TransactionPoolCollector) Collect(ctx context.Context) error { + err := c.fetchData(ctx) + if err != nil { + return fmt.Errorf("fetch data: %w", err) + } + + c.collectSpentKeyImages() + c.collectSize() + c.collectTransactionsSize() + c.collectTransactionsCount() + c.collectTransactionsFee() + c.collectTransactionsFeePerKb() + c.collectTransactionsInputs() + c.collectTransactionsOutputs() + c.collectTransactionsAgeDistribution() + c.collectWeirdCases() + + return nil +} + +func (c *TransactionPoolCollector) fetchData(ctx context.Context) error { + stats, err := c.client.GetTransactionPoolStats(ctx) + if err != nil { + return fmt.Errorf("get transactionpool stats: %w", err) + } + + pool, err := c.client.GetTransactionPool(ctx) + if err != nil { + return fmt.Errorf("get transaction pool: %w", err) + } + + c.stats = stats + c.pool = pool + + c.txns = make([]*daemon.TransactionJSON, len(pool.Transactions)) + + for idx, txn := range c.pool.Transactions { + c.txns[idx] = new(daemon.TransactionJSON) + + err := json.Unmarshal([]byte(txn.TxJSON), c.txns[idx]) + if err != nil { + return fmt.Errorf("unmarhsal tx json: %w", err) + } + } + + return nil +} + +func (c *TransactionPoolCollector) collectSpentKeyImages() { + desc := prometheus.NewDesc( + "monero_transaction_pool_spent_key_images", + "total number of key images spent across all transactions"+ + " in the pool", + nil, nil, + ) + + c.metricsC <- prometheus.MustNewConstMetric( + desc, + prometheus.GaugeValue, + float64(len(c.pool.SpentKeyImages)), + ) + +} + +func (c *TransactionPoolCollector) collectTransactionsCount() { + desc := prometheus.NewDesc( + "monero_transaction_pool_transactions", + "number of transactions in the pool at the moment of "+ + "the scrape", + nil, nil, + ) + + c.metricsC <- prometheus.MustNewConstMetric( + desc, + prometheus.GaugeValue, + float64(len(c.pool.Transactions)), + ) + +} + +func (c *TransactionPoolCollector) collectSize() { + desc := prometheus.NewDesc( + "monero_transaction_pool_size_bytes", + "total size of the transaction pool", + nil, nil, + ) + + c.metricsC <- prometheus.MustNewConstMetric( + desc, + prometheus.GaugeValue, + float64(c.stats.PoolStats.BytesTotal), + ) +} + +func (c *TransactionPoolCollector) collectTransactionsSize() { + summary := NewSummary() + for _, txn := range c.pool.Transactions { + summary.Insert(float64(txn.BlobSize)) + } + + c.metricsC <- prometheus.MustNewConstSummary( + prometheus.NewDesc( + "monero_transaction_pool_transactions_size_bytes", + "distribution of the size of the transactions "+ + "in the transaction pool", + nil, nil, + ), + summary.Count(), summary.Sum(), summary.Quantiles(), + ) +} + +func (c *TransactionPoolCollector) collectTransactionsFeePerKb() { + summary := NewSummary() + for _, txn := range c.pool.Transactions { + fee := float64(txn.Fee) / constant.MicroXMR + size := float64(txn.BlobSize) / 1024 + + summary.Insert(fee / size) + } + + c.metricsC <- prometheus.MustNewConstSummary( + prometheus.NewDesc( + "monero_transaction_pool_fees_micronero_per_kb", + "distribution of the feeperkb utilized for txns"+ + " in the pool", + nil, nil, + ), + summary.Count(), summary.Sum(), summary.Quantiles(), + ) +} + +func (c *TransactionPoolCollector) collectTransactionsInputs() { + summary := NewSummary() + for _, txn := range c.txns { + summary.Insert(float64(len(txn.Vin))) + } + + c.metricsC <- prometheus.MustNewConstSummary( + prometheus.NewDesc( + "monero_transaction_pool_transactions_inputs", + "distribution of inputs in the pool", + nil, nil, + ), + summary.Count(), summary.Sum(), summary.Quantiles(), + ) +} + +func (c *TransactionPoolCollector) collectTransactionsOutputs() { + summary := NewSummary() + for _, txn := range c.txns { + summary.Insert(float64(len(txn.Vout))) + } + + c.metricsC <- prometheus.MustNewConstSummary( + prometheus.NewDesc( + "monero_transaction_pool_transactions_outputs", + "distribution of outputs in the pool", + nil, nil, + ), + summary.Count(), summary.Sum(), summary.Quantiles(), + ) +} + +func (c *TransactionPoolCollector) collectTransactionsAgeDistribution() { + now := time.Now() + + summary := NewSummary() + for _, txn := range c.pool.Transactions { + summary.Insert( + now.Sub(time.Unix(txn.ReceiveTime, 0)).Seconds(), + ) + } + + c.metricsC <- prometheus.MustNewConstSummary( + prometheus.NewDesc( + "monero_transaction_pool_transactions_age", + "distribution of for how long transactions have "+ + "been in the pool", + nil, nil, + ), + summary.Count(), summary.Sum(), summary.Quantiles(), + ) +} + +func (c *TransactionPoolCollector) collectTransactionsFee() { + desc := prometheus.NewDesc( + "monero_transaction_pool_fees_monero", + "total amount of fee being spent in the transaction pool", + nil, nil, + ) + + c.metricsC <- prometheus.MustNewConstMetric( + desc, + prometheus.GaugeValue, + float64(c.stats.PoolStats.FeeTotal)/constant.XMR, + ) +} + +func (c *TransactionPoolCollector) collectWeirdCases() { + c.metricsC <- prometheus.MustNewConstMetric( + prometheus.NewDesc( + "monero_transaction_pool_failing_transactions", + "number of transactions that are marked as failing", + nil, nil, + ), + prometheus.GaugeValue, + float64(c.stats.PoolStats.NumFailing), + ) + + c.metricsC <- prometheus.MustNewConstMetric( + prometheus.NewDesc( + "monero_transaction_pool_double_spends", + "transactions doubly spending outputs", + nil, nil, + ), + prometheus.GaugeValue, + float64(c.stats.PoolStats.NumDoubleSpends), + ) + + c.metricsC <- prometheus.MustNewConstMetric( + prometheus.NewDesc( + "monero_transaction_pool_not_relayed", + "number of transactions that have not been relayed", + nil, nil, + ), + prometheus.GaugeValue, + float64(c.stats.PoolStats.NumNotRelayed), + ) + + c.metricsC <- prometheus.MustNewConstMetric( + prometheus.NewDesc( + "monero_transaction_pool_older_than_10m", + "number of transactions that are older than 10m", + nil, nil, + ), + prometheus.GaugeValue, + float64(c.stats.PoolStats.Num10M), + ) +} diff --git a/pkg/collector/summary.go b/pkg/collector/summary.go new file mode 100644 index 0000000..812d880 --- /dev/null +++ b/pkg/collector/summary.go @@ -0,0 +1,94 @@ +package collector + +import "github.com/beorn7/perks/quantile" + +// defaultQuantiles is the default quantiles to compute for a given data stream +// that we want to summarize. +// +// these (quantile -> epsilon) will be used by default by any Summary unless +// initialized with the `WithQuantiles` option to override it. +// +var defaultQuantiles = map[float64]float64{ + 0.05: 0.01, + 0.10: 0.01, + 0.25: 0.01, + 0.50: 0.01, + 0.75: 0.01, + 0.90: 0.01, + 0.95: 0.01, + 0.99: 0.01, + 1.00: 0.01, +} + +type Summary struct { + count uint64 + sum float64 + quantiles map[float64]float64 + + stream *quantile.Stream + computed bool +} + +type SummaryOption func(s *Summary) + +func WithQuantiles(v map[float64]float64) SummaryOption { + return func(s *Summary) { + s.quantiles = v + } +} + +func NewSummary(opts ...SummaryOption) *Summary { + summary := &Summary{ + count: uint64(0), + sum: float64(0), + quantiles: cloneMap(defaultQuantiles), + } + + for _, opt := range opts { + opt(summary) + } + + summary.stream = quantile.NewTargeted(summary.quantiles) + + return summary +} + +func (s *Summary) Insert(v float64) { + s.sum += v + s.stream.Insert(v) + s.count++ +} + +func (s *Summary) Count() uint64 { + s.compute() + return s.count +} + +func (s *Summary) Quantiles() map[float64]float64 { + s.compute() + return s.quantiles +} + +func (s *Summary) Sum() float64 { + s.compute() + return s.sum +} + +func (s *Summary) compute() { + if s.computed { + return + } + + for phi := range s.quantiles { + s.quantiles[phi] = s.stream.Query(phi) + } +} + +func cloneMap(o map[float64]float64) map[float64]float64 { + m := make(map[float64]float64, len(o)) + for k, v := range o { + m[k] = v + } + + return m +} diff --git a/pkg/exporter/exporter.go b/pkg/exporter/exporter.go index 024814f..9cbe39d 100644 --- a/pkg/exporter/exporter.go +++ b/pkg/exporter/exporter.go @@ -12,42 +12,55 @@ import ( "go.uber.org/zap" ) +const ( + defaultBindAddress = ":9000" + defaultTelemetryPath = "/metrics" +) + // Exporter is responsible for bringing up a web server that collects metrics // that have been globally registered via prometheus collectors (e.g., see // `pkg/collector`). // type Exporter struct { - // ListenAddress is the full address used by prometheus - // to listen for scraping requests. - // - // Examples: - // - :8080 - // - 127.0.0.2:1313 - // - listenAddress string - - // TelemetryPath configures the path under which - // the prometheus metrics are reported. - // - // For instance: - // - /metrics - // - /telemetry - // + bindAddress string telemetryPath string - // listener is the TCP listener used by the webserver. `nil` if no - // server is running. - // listener net.Listener - - log logr.Logger + log logr.Logger } -// Option. +// WithTelemetryPath overrides the default path under which the prometheus +// metrics are reported. +// +// For instance: +// - / +// - /metrics +// - /telemetry +// +func WithTelemetryPath(v string) Option { + return func(e *Exporter) { + e.telemetryPath = v + } +} + +// WithBindAddress overrides the default address at which the prometheus +// metrics HTTP server would bind to. +// +// Examples: +// - :8080 +// - 127.0.0.2:1313 +// +func WithBindAddress(v string) Option { + return func(e *Exporter) { + e.bindAddress = v + } +} + +// Option allows overriding the exporter's defaults // type Option func(e *Exporter) -// New. +// New instantiates a new exporter with defaults, unless options are passed. // func New(opts ...Option) (*Exporter, error) { defaultLogger, err := zap.NewDevelopment() @@ -56,8 +69,8 @@ func New(opts ...Option) (*Exporter, error) { } e := &Exporter{ - listenAddress: ":9000", - telemetryPath: "/metrics", + bindAddress: defaultBindAddress, + telemetryPath: defaultTelemetryPath, log: zapr.NewLogger(defaultLogger.Named("exporter")), } @@ -76,9 +89,9 @@ func New(opts ...Option) (*Exporter, error) { func (e *Exporter) Run(ctx context.Context) error { var err error - e.listener, err = net.Listen("tcp", e.listenAddress) + e.listener, err = net.Listen("tcp", e.bindAddress) if err != nil { - return fmt.Errorf("listen on '%s': %w", e.listenAddress, err) + return fmt.Errorf("listen on '%s': %w", e.bindAddress, err) } doneChan := make(chan error, 1) @@ -87,7 +100,7 @@ func (e *Exporter) Run(ctx context.Context) error { defer close(doneChan) e.log.WithValues( - "addr", e.listenAddress, + "addr", e.bindAddress, "path", e.telemetryPath, ).Info("listening") @@ -95,7 +108,7 @@ func (e *Exporter) Run(ctx context.Context) error { if err := http.Serve(e.listener, nil); err != nil { doneChan <- fmt.Errorf( "failed listening on address %s: %w", - e.listenAddress, err, + e.bindAddress, err, ) } }()