mirror of
https://github.com/cirocosta/monero-exporter.git
synced 2024-12-21 22:05:02 -05:00
port over code from monero-operator
the code has been initially written under `monero-operator`, but it turns out that having the exporter available for non-kubernetes users is pretty good, thus, making it its own separate thing. Signed-off-by: Ciro S. Costa <utxobr@protonmail.com>
This commit is contained in:
parent
72891bcce9
commit
31f5a6f84b
23
.github/workflows/go.yml
vendored
Normal file
23
.github/workflows/go.yml
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
name: Go
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
pull_request:
|
||||||
|
branches: [master]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: 1.16
|
||||||
|
- name: Build
|
||||||
|
run: make install
|
||||||
|
- name: Test
|
||||||
|
run: make test
|
||||||
|
- name: Lint
|
||||||
|
run: make lint
|
62
.golangci.yaml
Normal file
62
.golangci.yaml
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
linters:
|
||||||
|
enable:
|
||||||
|
- asciicheck
|
||||||
|
- bodyclose
|
||||||
|
- cyclop
|
||||||
|
- deadcode
|
||||||
|
- dogsled
|
||||||
|
- dupl
|
||||||
|
- durationcheck
|
||||||
|
- errcheck
|
||||||
|
- errorlint
|
||||||
|
- exhaustive
|
||||||
|
- exportloopref
|
||||||
|
- forbidigo
|
||||||
|
- forcetypeassert
|
||||||
|
- funlen
|
||||||
|
- gocognit
|
||||||
|
- goconst
|
||||||
|
- gocritic
|
||||||
|
- gocyclo
|
||||||
|
- godot
|
||||||
|
- godox
|
||||||
|
- goerr113
|
||||||
|
- goimports
|
||||||
|
- gosec
|
||||||
|
- govet
|
||||||
|
- ifshort
|
||||||
|
- ineffassign
|
||||||
|
- lll
|
||||||
|
- makezero
|
||||||
|
- misspell
|
||||||
|
- nakedret
|
||||||
|
- nestif
|
||||||
|
- nilerr
|
||||||
|
- nlreturn
|
||||||
|
- noctx
|
||||||
|
- nolintlint
|
||||||
|
- paralleltest
|
||||||
|
- prealloc
|
||||||
|
- predeclared
|
||||||
|
- revive
|
||||||
|
- staticcheck
|
||||||
|
- structcheck
|
||||||
|
- tagliatelle
|
||||||
|
- testpackage
|
||||||
|
- typecheck
|
||||||
|
- unconvert
|
||||||
|
- unparam
|
||||||
|
- unused
|
||||||
|
- varcheck
|
||||||
|
- wastedassign
|
||||||
|
- whitespace
|
||||||
|
- wrapcheck
|
||||||
|
|
||||||
|
linters-settings:
|
||||||
|
goimports:
|
||||||
|
local-prefixes: github.com/cirocosta/monero-exporter
|
||||||
|
exhaustive:
|
||||||
|
default-signifies-exhaustive: true
|
||||||
|
|
||||||
|
run:
|
||||||
|
timeout: 5m
|
8
Makefile
Normal file
8
Makefile
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
install:
|
||||||
|
go install -v ./cmd/monero-exporter
|
||||||
|
|
||||||
|
test:
|
||||||
|
go test ./...
|
||||||
|
|
||||||
|
lint:
|
||||||
|
go run github.com/golangci/golangci-lint/cmd/golangci-lint run --config=.golangci.yaml
|
80
cmd/monero-exporter/main.go
Normal file
80
cmd/monero-exporter/main.go
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/cirocosta/go-monero/pkg/rpc"
|
||||||
|
"github.com/jessevdk/go-flags"
|
||||||
|
"github.com/oschwald/geoip2-golang"
|
||||||
|
|
||||||
|
"github.com/cirocosta/monero-exporter/pkg/collector"
|
||||||
|
"github.com/cirocosta/monero-exporter/pkg/exporter"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MetricsCommand struct {
|
||||||
|
// nolint:lll
|
||||||
|
MonerodAddress string `long:"monerod-address" default:"http://localhost:18081" required:"true" description:"address of monerod rpc (restricted if possible)"`
|
||||||
|
GeoIPFile string `long:"geoip-file" description:"filepath of geoip database"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cmd := &MetricsCommand{}
|
||||||
|
|
||||||
|
if _, err := flags.Parse(cmd); err != nil {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cmd.Execute(nil); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MetricsCommand) Execute(_ []string) error {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
prometheusExporter, err := exporter.New()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("new exporter: %w", err)
|
||||||
|
}
|
||||||
|
defer prometheusExporter.Close()
|
||||||
|
|
||||||
|
daemonClient, err := rpc.NewClient(c.MonerodAddress)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("new client '%s': %w", c.MonerodAddress, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
collectorOpts := []collector.Option{}
|
||||||
|
|
||||||
|
if c.GeoIPFile != "" {
|
||||||
|
db, err := geoip2.Open(c.GeoIPFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("geoip open: %w", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
countryMapper := func(ip net.IP) (string, error) {
|
||||||
|
res, err := db.Country(ip)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("country '%s': %w", ip, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.RegisteredCountry.IsoCode, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
collectorOpts = append(collectorOpts, collector.WithCountryMapper(countryMapper))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := collector.Register(daemonClient, collectorOpts...); err != nil {
|
||||||
|
return fmt.Errorf("collector register: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := prometheusExporter.Run(ctx); err != nil {
|
||||||
|
return fmt.Errorf("prometheus exporter run: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
16
go.mod
Normal file
16
go.mod
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
module github.com/cirocosta/monero-exporter
|
||||||
|
|
||||||
|
go 1.16
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/bmizerany/perks v0.0.0-20141205001514-d9a9656a3a4b
|
||||||
|
github.com/cirocosta/go-monero v0.0.0-20210613220451-207bd1f632c0
|
||||||
|
github.com/go-logr/logr v0.4.0
|
||||||
|
github.com/go-logr/zapr v0.4.0
|
||||||
|
github.com/golangci/golangci-lint v1.41.1 // indirect
|
||||||
|
github.com/jessevdk/go-flags v1.5.0
|
||||||
|
github.com/oschwald/geoip2-golang v1.5.0
|
||||||
|
github.com/prometheus/client_golang v1.11.0
|
||||||
|
go.uber.org/zap v1.17.0
|
||||||
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||||
|
)
|
23
hack/table-printer.awk
Executable file
23
hack/table-printer.awk
Executable file
@ -0,0 +1,23 @@
|
|||||||
|
#!/usr/bin/awk -f
|
||||||
|
|
||||||
|
# gh-table-printer - prints to `stdout` the metric descriptions
|
||||||
|
# in GitHub flavored markdown tables[1].
|
||||||
|
#
|
||||||
|
# [1] - https://help.github.com/articles/organizing-information-with-tables/
|
||||||
|
#
|
||||||
|
# Usage: `curl -s localhost:9100/metrics | grep container_ | ./gh-table-printer`
|
||||||
|
|
||||||
|
BEGIN {
|
||||||
|
print "| name | description |"
|
||||||
|
print "| ---- | ----------- |"
|
||||||
|
}
|
||||||
|
|
||||||
|
/HELP/ {
|
||||||
|
line="| " $3 " |"
|
||||||
|
for (i = 4; i <= NF; i++) {
|
||||||
|
line = line " "$i
|
||||||
|
}
|
||||||
|
|
||||||
|
line = line " |"
|
||||||
|
print line
|
||||||
|
}
|
640
pkg/collector/collector.go
Normal file
640
pkg/collector/collector.go
Normal file
@ -0,0 +1,640 @@
|
|||||||
|
package collector
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"net"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/bmizerany/perks/quantile"
|
||||||
|
"github.com/cirocosta/go-monero/pkg/rpc"
|
||||||
|
"github.com/go-logr/logr"
|
||||||
|
"github.com/go-logr/zapr"
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CountryMapper defines the signature of a function that given an IP,
|
||||||
|
// translates it into a country name.
|
||||||
|
//
|
||||||
|
// f(ip) -> CN
|
||||||
|
//
|
||||||
|
type CountryMapper func(net.IP) (string, error)
|
||||||
|
|
||||||
|
// Collector implements the prometheus Collector interface, providing monero
|
||||||
|
// metrics whenever a prometheus scrape is received.
|
||||||
|
//
|
||||||
|
type Collector struct {
|
||||||
|
// client is a Go client that communicated with a `monero` daemon via
|
||||||
|
// plain HTTP(S) RPC.
|
||||||
|
//
|
||||||
|
client *rpc.Client
|
||||||
|
|
||||||
|
// countryMapper is a function that knows how to translate IPs to
|
||||||
|
// country codes.
|
||||||
|
//
|
||||||
|
// optional: if nil, no country-mapping will take place.
|
||||||
|
//
|
||||||
|
countryMapper CountryMapper
|
||||||
|
|
||||||
|
log logr.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure that we implement prometheus' collector interface.
|
||||||
|
//
|
||||||
|
var _ prometheus.Collector = &Collector{}
|
||||||
|
|
||||||
|
// Option is a type used by functional arguments to mutate the collector to
|
||||||
|
// override default behavior.
|
||||||
|
//
|
||||||
|
type Option func(c *Collector)
|
||||||
|
|
||||||
|
// WithCountryMapper is a functional argument that overrides the default no-op
|
||||||
|
// country mapper.
|
||||||
|
//
|
||||||
|
func WithCountryMapper(v CountryMapper) func(c *Collector) {
|
||||||
|
return func(c *Collector) {
|
||||||
|
c.countryMapper = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register registers this collector with the global prometheus collectors
|
||||||
|
// registry making it available for an exporter to collect our metrics.
|
||||||
|
//
|
||||||
|
func Register(client *rpc.Client, opts ...Option) error {
|
||||||
|
defaultLogger, err := zap.NewDevelopment()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("zap new development: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c := &Collector{
|
||||||
|
client: client,
|
||||||
|
log: zapr.NewLogger(defaultLogger.Named("collector")),
|
||||||
|
countryMapper: func(_ net.IP) (string, error) { return "lol", nil },
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := prometheus.Register(c); err != nil {
|
||||||
|
return fmt.Errorf("register: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CollectFunc defines a standardized signature for functions that want to
|
||||||
|
// expose metrics for collection.
|
||||||
|
//
|
||||||
|
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.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect implements the Collect function of the Collector interface.
|
||||||
|
//
|
||||||
|
// Here is where all of the calls to a monero rpc endpoint is made, each being
|
||||||
|
// wrapped in its own function, all being called concurrently.
|
||||||
|
//
|
||||||
|
func (c *Collector) Collect(ch chan<- prometheus.Metric) {
|
||||||
|
var g *errgroup.Group
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
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},
|
||||||
|
} {
|
||||||
|
collector := collector
|
||||||
|
|
||||||
|
g.Go(func() error {
|
||||||
|
if err := collector.fn(ctx, ch); err != nil {
|
||||||
|
return fmt.Errorf("collector fn '%s': %w", collector.name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.Wait(); err != nil {
|
||||||
|
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, rpc.GetBlockRequestParameters{
|
||||||
|
Height: ¤tHeight,
|
||||||
|
})
|
||||||
|
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
|
||||||
|
}
|
8
pkg/collector/doc.go
Normal file
8
pkg/collector/doc.go
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// Package collector provides the core functionality of this exporter.
|
||||||
|
//
|
||||||
|
// It implements the Prometheus collector interface, providing `monero` metrics
|
||||||
|
// whenever a request hits this exporter, allowing us to not have to rely on a
|
||||||
|
// particular interval defined in this exporter (instead, rely on prometheus'
|
||||||
|
// scrape interval).
|
||||||
|
//
|
||||||
|
package collector
|
4
pkg/exporter/doc.go
Normal file
4
pkg/exporter/doc.go
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
// Package exporter provides the ability for one to instantiate a web server
|
||||||
|
// that on requests to it, provides prometheus metrics in the http response.
|
||||||
|
//
|
||||||
|
package exporter
|
128
pkg/exporter/exporter.go
Normal file
128
pkg/exporter/exporter.go
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
package exporter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/go-logr/logr"
|
||||||
|
"github.com/go-logr/zapr"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
//
|
||||||
|
telemetryPath string
|
||||||
|
|
||||||
|
// listener is the TCP listener used by the webserver. `nil` if no
|
||||||
|
// server is running.
|
||||||
|
//
|
||||||
|
listener net.Listener
|
||||||
|
|
||||||
|
log logr.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// Option.
|
||||||
|
//
|
||||||
|
type Option func(e *Exporter)
|
||||||
|
|
||||||
|
// New.
|
||||||
|
//
|
||||||
|
func New(opts ...Option) (*Exporter, error) {
|
||||||
|
defaultLogger, err := zap.NewDevelopment()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("zap new development: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
e := &Exporter{
|
||||||
|
listenAddress: ":9000",
|
||||||
|
telemetryPath: "/metrics",
|
||||||
|
log: zapr.NewLogger(defaultLogger.Named("exporter")),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run initiates the HTTP server to serve the metrics.
|
||||||
|
//
|
||||||
|
// ps.: this is a BLOCKING method - make sure you either make use of goroutines
|
||||||
|
// to not block if needed.
|
||||||
|
//
|
||||||
|
func (e *Exporter) Run(ctx context.Context) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
e.listener, err = net.Listen("tcp", e.listenAddress)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("listen on '%s': %w", e.listenAddress, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
doneChan := make(chan error, 1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer close(doneChan)
|
||||||
|
|
||||||
|
e.log.WithValues(
|
||||||
|
"addr", e.listenAddress,
|
||||||
|
"path", e.telemetryPath,
|
||||||
|
).Info("listening")
|
||||||
|
|
||||||
|
http.Handle(e.telemetryPath, promhttp.Handler())
|
||||||
|
if err := http.Serve(e.listener, nil); err != nil {
|
||||||
|
doneChan <- fmt.Errorf(
|
||||||
|
"failed listening on address %s: %w",
|
||||||
|
e.listenAddress, err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err = <-doneChan:
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("donechan err: %w", err)
|
||||||
|
}
|
||||||
|
case <-ctx.Done():
|
||||||
|
return fmt.Errorf("ctx err: %w", ctx.Err())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close gracefully closes the tcp listener associated with it.
|
||||||
|
//
|
||||||
|
func (e *Exporter) Close() (err error) {
|
||||||
|
if e.listener == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
e.log.Info("closing")
|
||||||
|
if err := e.listener.Close(); err != nil {
|
||||||
|
return fmt.Errorf("close: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
4
tools/doc.go
Normal file
4
tools/doc.go
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
// Package tools simply imports the tooling that we use to maintain this
|
||||||
|
// repository, like testing utilities, linting, etc.
|
||||||
|
//
|
||||||
|
package tools
|
7
tools/tools.go
Normal file
7
tools/tools.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
// +build tools
|
||||||
|
|
||||||
|
package tools
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "github.com/golangci/golangci-lint/cmd/golangci-lint"
|
||||||
|
)
|
Loading…
Reference in New Issue
Block a user