Merge branch 'collector-round-2'

This commit is contained in:
Ciro S. Costa 2021-08-02 17:46:26 -04:00
commit 5dff9c1fba
27 changed files with 4895 additions and 686 deletions

View File

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

@ -24,13 +24,6 @@ builds:
- 6 - 6
- 7 - 7
archives:
- replacements:
darwin: Darwin
linux: Linux
windows: Windows
amd64: x86_64
checksum: checksum:
name_template: 'checksums.txt' name_template: 'checksums.txt'

30
Dockerfile Normal file
View File

@ -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"]

118
INSTALL.md Normal file
View File

@ -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

View File

@ -1,8 +1,19 @@
install: install:
go install -v ./cmd/monero-exporter go install -v ./cmd/monero-exporter
run:
monero-exporter \
--monero-addr=http://localhost:18081 \
--bind-addr=:9000 \
--geoip-filepath=./hack/geoip.mmdb
test: test:
go test ./... go test ./...
lint: 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

341
README.md
View File

@ -1,22 +1,33 @@
# monero-exporter # monero-exporter
[Prometheus](https://prometheus.io) exporter for [monero](https://getmonero.org). [prometheus] exporter for [monero] nodes.
## Installation <!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
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)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## 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 ```console
$ monero-exporter --help $ monero-exporter --help
@ -25,7 +36,6 @@ Prometheus exporter for monero metrics
Usage: Usage:
monero-exporter [flags] monero-exporter [flags]
monero-exporter [command]
Available Commands: Available Commands:
completion generate the autocompletion script for the specified shell completion generate the autocompletion script for the specified shell
@ -33,90 +43,251 @@ Available Commands:
version print the version of this CLI version print the version of this CLI
Flags: Flags:
--address string address of the monero node to collect metrics from --bind-addr string address to bind the prometheus server to
--geoip-filepath string filepath of a geoip database file for ip to country resolution (default ":9090")
--geoip-filepath string filepath of a geoip database file for ip to
country resolution
-h, --help help for monero-exporter -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. 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 ## 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 | | name | description |
| ---- | ----------- | | ---- | ----------- |
| monero_bans | number of nodes banned | | monero_lastblock_difficulty | difficulty used for the last block |
| monero_connections | connections info | | monero_lastblock_fees_micronero_per_kb | distribution of the feeperkb utilized for txns |
| monero_connections_livetime | peers livetime distribution | | monero_lastblock_fees_monero | total amount of fees included in this block |
| monero_fee_estimate | fee estimate for 1 grace block | | monero_lastblock_height | height of the last block |
| monero_height_divergence | how much our peers diverge from us in block height | | monero_lastblock_reward_monero | total amount of rewards granted in the last block (subsidy + fees) |
| monero_info_alt_blocks_count | info for alt_blocks_count | | monero_lastblock_size_bytes | total size of the last block |
| monero_info_block_size_limit | info for block_size_limit | | monero_lastblock_subsidy_monero | newly minted monero for this block |
| monero_info_block_size_median | info for block_size_median | | monero_lastblock_transactions | number of transactions seen in the last block |
| monero_info_busy_syncing | info for busy_syncing | | monero_lastblock_transactions_inputs | distribution of inputs in the last block |
| monero_info_cumulative_difficulty | info for cumulative_difficulty | | monero_lastblock_transactions_outputs | distribution of outputs in the last block |
| monero_info_difficulty | info for difficulty | | monero_lastblock_transactions_size_bytes | distribution of the size of the transactions included |
| 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 | ### Transaction pool
| monero_info_incoming_connections_count | info for incoming_connections_count |
| monero_info_mainnet | info for mainnet | These metrics give you a view of how the transaction pool of this particular
| monero_info_offline | info for offline | node looked like when Prometheus asked the exporter information about it.
| monero_info_outgoing_connections_count | info for outgoing_connections_count |
| monero_info_rpc_connections_count | info for rpc_connections_count | Given the pull-based nature of Prometheus, this will be only as granular as the
| monero_info_stagenet | info for stagenet | frequency of scraping configured for it.
| monero_info_start_time | info for start_time |
| monero_info_synchronized | info for synchronized |
| monero_info_target | info for target | | name | description |
| monero_info_target_height | info for target_height | | ---- | ----------- |
| monero_info_testnet | info for testnet | | monero_transaction_pool_double_spends | transactions doubly spending outputs |
| monero_info_tx_count | info for tx_count | | monero_transaction_pool_failing_transactions | number of transactions that are marked as failing |
| monero_info_tx_pool_size | info for tx_pool_size | | monero_transaction_pool_fees_micronero_per_kb | distribution of the feeperkb utilized for txns in the pool |
| monero_info_untrusted | info for untrusted | | monero_transaction_pool_fees_monero | total amount of fee being spent in the transaction pool |
| monero_info_was_bootstrap_ever_used | info for was_bootstrap_ever_used | | monero_transaction_pool_not_relayed | number of transactions that have not been relayed |
| monero_info_white_peerlist_size | info for white_peerlist_size | | monero_transaction_pool_older_than_10m | number of transactions that are older than 10m |
| monero_last_block_header_block_size | info for block_size | | monero_transaction_pool_size_bytes | total size of the transaction pool |
| monero_last_block_header_block_weight | info for block_weight | | monero_transaction_pool_spent_key_images | total number of key images spent across all transactions in the pool |
| monero_last_block_header_cumulative_difficulty | info for cumulative_difficulty | | monero_transaction_pool_transactions | number of transactions in the pool at the moment of the scrape |
| monero_last_block_header_cumulative_difficulty_top64 | info for cumulative_difficulty_top64 | | monero_transaction_pool_transactions_age | distribution of for how long transactions have been in the pool |
| monero_last_block_header_depth | info for depth | | monero_transaction_pool_transactions_inputs | distribution of inputs in the pool |
| monero_last_block_header_difficulty | info for difficulty | | monero_transaction_pool_transactions_outputs | distribution of outputs in the pool |
| monero_last_block_header_difficulty_top64 | info for difficulty_top64 | | monero_transaction_pool_transactions_size_bytes | distribution of the size of the transactions in the transaction pool |
| 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 | ### RPC
| monero_last_block_header_minor_version | info for minor_version |
| monero_last_block_header_nonce | info for nonce | RPC metrics provide an overview of the usage of the RPC endpoints.
| monero_last_block_header_num_txes | info for num_txes |
| monero_last_block_header_orphan_status | info for orphan_status | Note that `monerod` does not distinguish between `monero-exporter` and other
| monero_last_block_header_reward | info for reward | users (like, a wallet), which means that these metrics will include the
| monero_last_block_header_timestamp | info for timestamp | constant querying that `monero-exporter` performs to fetch statistics.
| 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 | | name | description |
| monero_last_block_vout | distribution of outputs in last block | | ---- | ----------- |
| monero_mempool_bytes_max | info for bytes_max | | monero_rpc_hits_total | number of hits that a particular rpc method had since startup |
| monero_mempool_bytes_med | info for bytes_med | | monero_rpc_seconds_total | amount of time spent service the method since startup |
| monero_mempool_bytes_min | info for bytes_min |
| monero_mempool_bytes_total | info for bytes_total |
| monero_mempool_fee_total | info for fee_total | ### P2P Connections
| monero_mempool_histo_98pc | info for histo_98pc |
| monero_mempool_num_10m | info for num_10m | Connection metrics aim at providing information about peers that are fully
| monero_mempool_num_double_spends | info for num_double_spends | connected to the node (thus, transmitting and receiving data to/from our node).
| monero_mempool_num_failing | info for num_failing |
| monero_mempool_num_not_relayed | info for num_not_relayed |
| monero_mempool_oldest | info for oldest | | name | description |
| monero_mempool_txs_total | info for txs_total | | ---- | ----------- |
| monero_net_total_in_bytes | network statistics | | monero_p2p_connections | number of connections to/from this node |
| monero_net_total_out_bytes | network statistics | | monero_p2p_connections_age | distribution of age of the connections we have |
| monero_peers_new | peers info | | monero_p2p_connections_height | distribution the height of the peers connected to/from us |
| monero_rpc_count | todo | | monero_p2p_connections_rx_rate_bps | distribution of data receive rate in bytes/s |
| monero_rpc_time | todo | | 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 ## Donate
![xmr address](./assets/donate.png) ![donation qrcode]
891B5keCnwXN14hA9FoAzGFtaWmcuLjTDT5aRTp65juBLkbNpEhLNfgcBn6aWdGuBqBnSThqMPsGRjWVQadCrhoAT6CnSL3 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

View File

@ -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

View File

@ -15,8 +15,10 @@ import (
) )
type command struct { type command struct {
address string telemetryPath string
bindAddr string
geoIPFilepath string geoIPFilepath string
moneroAddr string
} }
func (c *command) Cmd() *cobra.Command { func (c *command) Cmd() *cobra.Command {
@ -26,13 +28,20 @@ func (c *command) Cmd() *cobra.Command {
RunE: c.RunE, RunE: c.RunE,
} }
cmd.Flags().StringVar(&c.address, "address", cmd.Flags().StringVar(&c.bindAddr, "bind-addr",
"", "address of the monero node to collect metrics from") ":9000", "address to bind the prometheus server to")
cmd.MarkFlagRequired("address")
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", cmd.Flags().StringVar(&c.geoIPFilepath, "geoip-filepath",
"", "filepath of a geoip database file for ip to country resolution") "", "filepath of a geoip database file for ip to country "+
cmd.MarkFlagFilename("geoip-filepath") "resolution")
_ = cmd.MarkFlagFilename("geoip-filepath")
return cmd return cmd
} }
@ -41,15 +50,9 @@ func (c *command) RunE(_ *cobra.Command, _ []string) error {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
prometheusExporter, err := exporter.New() rpcClient, err := rpc.NewClient(c.moneroAddr)
if err != nil { if err != nil {
return fmt.Errorf("new exporter: %w", err) return fmt.Errorf("new client '%s': %w", c.moneroAddr, err)
}
defer prometheusExporter.Close()
rpcClient, err := rpc.NewClient(c.address)
if err != nil {
return fmt.Errorf("new client '%s': %w", c.address, err)
} }
daemonClient := daemon.NewClient(rpcClient) daemonClient := daemon.NewClient(rpcClient)
@ -66,20 +69,35 @@ func (c *command) RunE(_ *cobra.Command, _ []string) error {
countryMapper := func(ip net.IP) (string, error) { countryMapper := func(ip net.IP) (string, error) {
res, err := db.Country(ip) res, err := db.Country(ip)
if err != nil { if err != nil {
return "", fmt.Errorf("country '%s': %w", ip, err) return "", fmt.Errorf(
"country '%s': %w", ip, err,
)
} }
return res.RegisteredCountry.IsoCode, nil 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) 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) return fmt.Errorf("prometheus exporter run: %w", err)
} }

44
docker-compose.yaml Normal file
View File

@ -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:

4
go.mod
View File

@ -3,8 +3,9 @@ module github.com/cirocosta/monero-exporter
go 1.16 go 1.16
require ( require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/bmizerany/perks v0.0.0-20141205001514-d9a9656a3a4b 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/logr v0.4.0
github.com/go-logr/zapr v0.4.0 github.com/go-logr/zapr v0.4.0
github.com/golangci/golangci-lint v1.41.1 github.com/golangci/golangci-lint v1.41.1
@ -12,5 +13,6 @@ require (
github.com/prometheus/client_golang v1.11.0 github.com/prometheus/client_golang v1.11.0
github.com/spf13/cobra v1.2.1 github.com/spf13/cobra v1.2.1
go.uber.org/zap v1.17.0 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 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
) )

9
go.sum
View File

@ -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/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/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/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.5-0.20210731140604-e84b32b615af h1:3KIUbswishjkyOM0rYfprGjMTWh+64VcXMOIQTnRcxE=
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/go.mod h1:B62WVZVEggXr+Kg5wKPNgfDw/BGplrgLjk7hJYIR3FU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 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-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 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-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 h1:khEcpUM4yFcxg4/FHQWkvVRmgijNXRfzkIDHh23ggEo=
github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= 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 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 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-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-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-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-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-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-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=

4
grafana/Dockerfile Normal file
View File

@ -0,0 +1,4 @@
FROM grafana/grafana
ADD ./config.ini /etc/grafana/grafana.ini
ADD ./provisioning /etc/grafana/provisioning

29
grafana/config.ini Normal file
View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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

BIN
hack/geoip.mmdb Normal file

Binary file not shown.

View File

@ -3,13 +3,9 @@ package collector
import ( import (
"context" "context"
"fmt" "fmt"
"math"
"net" "net"
"reflect"
"strconv"
"time" "time"
"github.com/bmizerany/perks/quantile"
"github.com/go-logr/logr" "github.com/go-logr/logr"
"github.com/go-logr/zapr" "github.com/go-logr/zapr"
"github.com/prometheus/client_golang/prometheus" "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 // Register registers this collector with the global prometheus collectors
// registry making it available for an exporter to collect our metrics. // 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{ c := &Collector{
client: client, client: client,
log: zapr.NewLogger(defaultLogger.Named("collector")), countryMapper: defaultCountryMapper,
countryMapper: func(_ net.IP) (string, error) { return "unknown", nil }, log: zapr.NewLogger(defaultLogger),
} }
for _, opt := range opts { 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. // Describe implements the Describe function of the Collector interface.
// //
func (c *Collector) Describe(ch chan<- *prometheus.Desc) { func (c *Collector) Describe(ch chan<- *prometheus.Desc) {
// Because we can present the description of the metrics at collection time, we // Because we can present the description of the metrics at collection
// don't need to write anything to the channel. // 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. // 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) g, ctx = errgroup.WithContext(ctx)
for _, collector := range []struct { for _, collector := range []CustomCollector{
name string NewLastBlockStatsCollector(c.client, ch),
fn CollectFunc NewTransactionPoolCollector(c.client, ch),
}{ NewRPCCollector(c.client, ch),
{"info_stats", c.CollectInfoStats}, NewConnectionsCollector(c.client, ch),
{"mempool_stats", c.CollectMempoolStats}, NewPeersCollector(c.client, ch),
{"last_block_header", c.CollectLastBlockHeader}, NewNetStatsCollector(c.client, ch),
{"bans", c.CollectBans}, NewOverallCollector(c.client, ch),
{"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},
} { } {
collector := collector collector := collector
g.Go(func() error { g.Go(func() error {
if err := collector.fn(ctx, ch); err != nil { if err := collector.Collect(ctx); err != nil {
return fmt.Errorf("collector fn '%s': %w", collector.name, err) return fmt.Errorf("%s collect: %w",
collector.Name(), err)
} }
return nil return nil
@ -146,495 +144,3 @@ func (c *Collector) Collect(ch chan<- prometheus.Metric) {
c.log.Error(err, "wait") 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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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),
)
}

View File

@ -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
}

View File

@ -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(),
)
}

View File

@ -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
}

View File

@ -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),
)
}

94
pkg/collector/summary.go Normal file
View File

@ -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
}

View File

@ -12,42 +12,55 @@ import (
"go.uber.org/zap" "go.uber.org/zap"
) )
const (
defaultBindAddress = ":9000"
defaultTelemetryPath = "/metrics"
)
// Exporter is responsible for bringing up a web server that collects metrics // Exporter is responsible for bringing up a web server that collects metrics
// that have been globally registered via prometheus collectors (e.g., see // that have been globally registered via prometheus collectors (e.g., see
// `pkg/collector`). // `pkg/collector`).
// //
type Exporter struct { type Exporter struct {
// ListenAddress is the full address used by prometheus bindAddress string
// 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
//
telemetryPath string telemetryPath string
// listener is the TCP listener used by the webserver. `nil` if no
// server is running.
//
listener net.Listener 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) type Option func(e *Exporter)
// New. // New instantiates a new exporter with defaults, unless options are passed.
// //
func New(opts ...Option) (*Exporter, error) { func New(opts ...Option) (*Exporter, error) {
defaultLogger, err := zap.NewDevelopment() defaultLogger, err := zap.NewDevelopment()
@ -56,8 +69,8 @@ func New(opts ...Option) (*Exporter, error) {
} }
e := &Exporter{ e := &Exporter{
listenAddress: ":9000", bindAddress: defaultBindAddress,
telemetryPath: "/metrics", telemetryPath: defaultTelemetryPath,
log: zapr.NewLogger(defaultLogger.Named("exporter")), log: zapr.NewLogger(defaultLogger.Named("exporter")),
} }
@ -76,9 +89,9 @@ func New(opts ...Option) (*Exporter, error) {
func (e *Exporter) Run(ctx context.Context) error { func (e *Exporter) Run(ctx context.Context) error {
var err error var err error
e.listener, err = net.Listen("tcp", e.listenAddress) e.listener, err = net.Listen("tcp", e.bindAddress)
if err != nil { 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) doneChan := make(chan error, 1)
@ -87,7 +100,7 @@ func (e *Exporter) Run(ctx context.Context) error {
defer close(doneChan) defer close(doneChan)
e.log.WithValues( e.log.WithValues(
"addr", e.listenAddress, "addr", e.bindAddress,
"path", e.telemetryPath, "path", e.telemetryPath,
).Info("listening") ).Info("listening")
@ -95,7 +108,7 @@ func (e *Exporter) Run(ctx context.Context) error {
if err := http.Serve(e.listener, nil); err != nil { if err := http.Serve(e.listener, nil); err != nil {
doneChan <- fmt.Errorf( doneChan <- fmt.Errorf(
"failed listening on address %s: %w", "failed listening on address %s: %w",
e.listenAddress, err, e.bindAddress, err,
) )
} }
}() }()