mirror of
https://github.com/cirocosta/monero-exporter.git
synced 2024-12-21 05:54:26 -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