@libp2p/prometheus-metrics
Version:
Collect libp2p metrics for scraping by Prometheus or Graphana
478 lines (389 loc) • 13.7 kB
text/typescript
/**
* @packageDocumentation
*
* Configure your libp2p node with Prometheus metrics:
*
* ```typescript
* import { createLibp2p } from 'libp2p'
* import { prometheusMetrics } from '@libp2p/prometheus-metrics'
*
* const node = await createLibp2p({
* metrics: prometheusMetrics()
* })
* ```
*
* Then use the `prom-client` module to supply metrics to the Prometheus/Graphana client using your http framework:
*
* ```JavaScript
* import client from 'prom-client'
*
* async function handler (request, h) {
* return h.response(await client.register.metrics())
* .type(client.register.contentType)
* }
* ```
*
* All Prometheus metrics are global so there's no other work required to extract them.
*
* ## Queries
*
* Some useful queries are:
*
* ### Data sent/received
*
* ```
* rate(libp2p_data_transfer_bytes_total[30s])
* ```
*
* ### CPU usage
*
* ```
* rate(process_cpu_user_seconds_total[30s]) * 100
* ```
*
* ### Memory usage
*
* ```
* nodejs_memory_usage_bytes
* ```
*
* ### DHT query time
*
* ```
* libp2p_kad_dht_wan_query_time_seconds
* ```
*
* or
*
* ```
* libp2p_kad_dht_lan_query_time_seconds
* ```
*
* ### TCP transport dialer errors
*
* ```
* rate(libp2p_tcp_dialer_errors_total[30s])
* ```
*/
import { statfs } from 'node:fs/promises'
import { totalmem } from 'node:os'
import { serviceCapabilities } from '@libp2p/interface'
import each from 'it-foreach'
import { collectDefaultMetrics, register } from 'prom-client'
import { PrometheusCounterGroup } from './counter-group.js'
import { PrometheusCounter } from './counter.js'
import { PrometheusHistogramGroup } from './histogram-group.js'
import { PrometheusHistogram } from './histogram.js'
import { PrometheusMetricGroup } from './metric-group.js'
import { PrometheusMetric } from './metric.js'
import { PrometheusSummaryGroup } from './summary-group.js'
import { PrometheusSummary } from './summary.js'
import type { ComponentLogger, Logger, MultiaddrConnection, Stream, Connection, CalculatedMetricOptions, Counter, CounterGroup, Metric, MetricGroup, MetricOptions, Metrics, CalculatedHistogramOptions, CalculatedSummaryOptions, HistogramOptions, Histogram, HistogramGroup, SummaryOptions, Summary, SummaryGroup } from '@libp2p/interface'
import type { Duplex } from 'it-stream-types'
import type { DefaultMetricsCollectorConfiguration, Registry, RegistryContentType } from 'prom-client'
import type { Uint8ArrayList } from 'uint8arraylist'
// export helper functions for creating buckets
export { linearBuckets, exponentialBuckets } from 'prom-client'
// prom-client metrics are global
const metrics = new Map<string, any>()
export interface PrometheusMetricsInit {
/**
* Use a custom registry to register metrics.
* By default, the global registry is used to register metrics.
*/
registry?: Registry
/**
* By default we collect default metrics - CPU, memory etc, to not do
* this, pass true here
*/
collectDefaultMetrics?: boolean
/**
* prom-client options to pass to the `collectDefaultMetrics` function
*/
defaultMetrics?: DefaultMetricsCollectorConfiguration<RegistryContentType>
/**
* All metrics in prometheus are global so to prevent clashes in naming
* we reset the global metrics registry on creation - to not do this,
* pass true here
*/
preserveExistingMetrics?: boolean
/**
* The current filesystem usage is reported as the metric
* `nodejs_fs_usage_bytes` using the `statfs` function from `node:fs` - the
* default location to stat is the current working directory, configured this
* location here
*/
statfsLocation?: string
}
export interface PrometheusCalculatedMetricOptions<T=number> extends CalculatedMetricOptions<T> {
registry?: Registry
}
export interface PrometheusCalculatedHistogramOptions<T=number> extends CalculatedHistogramOptions<T> {
registry?: Registry
}
export interface PrometheusCalculatedSummaryOptions<T=number> extends CalculatedSummaryOptions<T> {
registry?: Registry
}
export interface PrometheusMetricsComponents {
logger: ComponentLogger
}
class PrometheusMetrics implements Metrics {
private readonly log: Logger
private transferStats: Map<string, number>
private readonly registry?: Registry
constructor (components: PrometheusMetricsComponents, init?: Partial<PrometheusMetricsInit>) {
this.log = components.logger.forComponent('libp2p:prometheus-metrics')
this.registry = init?.registry
if (init?.preserveExistingMetrics !== true) {
this.log('Clearing existing metrics')
metrics.clear()
register?.clear()
}
if (init?.collectDefaultMetrics !== false) {
this.log('Collecting default metrics')
collectDefaultMetrics({ ...init?.defaultMetrics, register: this.registry ?? init?.defaultMetrics?.register })
}
// holds global and per-protocol sent/received stats
this.transferStats = new Map()
this.log('Collecting data transfer metrics')
this.registerCounterGroup('libp2p_data_transfer_bytes_total', {
label: 'protocol',
calculate: () => {
const output: Record<string, number> = {}
for (const [key, value] of this.transferStats.entries()) {
output[key] = value
}
// reset counts for next time
this.transferStats.clear()
return output
}
})
this.log('Collecting memory metrics')
this.registerMetricGroup('nodejs_memory_usage_bytes', {
label: 'memory',
calculate: () => {
return {
...process.memoryUsage()
}
}
})
const totalMemoryMetric = this.registerMetric('nodejs_memory_total_bytes')
totalMemoryMetric.update(totalmem())
this.log('Collecting filesystem metrics')
this.registerMetricGroup('nodejs_fs_usage_bytes', {
label: 'filesystem',
calculate: async () => {
const stats = await statfs(init?.statfsLocation ?? process.cwd())
const total = stats.bsize * stats.blocks
const available = stats.bsize * stats.bavail
return {
total,
free: stats.bsize * stats.bfree,
available,
used: (available / total) * 100
}
}
})
}
readonly [Symbol.toStringTag] = '@libp2p/metrics-prometheus'
readonly [serviceCapabilities]: string[] = [
'@libp2p/metrics'
]
start (): void {
}
stop (): void {
this.transferStats.clear()
}
/**
* Increment the transfer stat for the passed key, making sure
* it exists first
*/
_incrementValue (key: string, value: number): void {
const existing = this.transferStats.get(key) ?? 0
this.transferStats.set(key, existing + value)
}
/**
* Override the sink/source of the stream to count the bytes
* in and out
*/
_track (stream: Duplex<AsyncGenerator<Uint8Array | Uint8ArrayList>>, name: string): void {
const self = this
const sink = stream.sink
stream.sink = async function trackedSink (source) {
await sink(each(source, buf => {
self._incrementValue(`${name} sent`, buf.byteLength)
}))
}
const source = stream.source
stream.source = each(source, buf => {
self._incrementValue(`${name} received`, buf.byteLength)
})
}
trackMultiaddrConnection (maConn: MultiaddrConnection): void {
this._track(maConn, 'global')
}
trackProtocolStream (stream: Stream, connection: Connection): void {
if (stream.protocol == null) {
// protocol not negotiated yet, should not happen as the upgrader
// calls this handler after protocol negotiation
return
}
this._track(stream, stream.protocol)
}
registerMetric (name: string, opts: PrometheusCalculatedMetricOptions): void
registerMetric (name: string, opts?: MetricOptions): Metric
registerMetric (name: string, opts: any = {}): any {
if (name == null || name.trim() === '') {
throw new Error('Metric name is required')
}
let metric = metrics.get(name)
if (metric != null) {
this.log('reuse existing metric', name)
return metric
}
this.log('register metric', name)
metric = new PrometheusMetric(name, { registry: this.registry, ...opts })
metrics.set(name, metric)
if (opts.calculate == null) {
return metric
}
}
registerMetricGroup (name: string, opts: PrometheusCalculatedMetricOptions<Record<string, number>>): void
registerMetricGroup (name: string, opts?: MetricOptions): MetricGroup
registerMetricGroup (name: string, opts: any = {}): any {
if (name == null || name.trim() === '') {
throw new Error('Metric group name is required')
}
let metricGroup = metrics.get(name)
if (metricGroup != null) {
this.log('reuse existing metric', name)
return metricGroup
}
this.log('register metric group', name)
metricGroup = new PrometheusMetricGroup(name, { registry: this.registry, ...opts })
metrics.set(name, metricGroup)
if (opts.calculate == null) {
return metricGroup
}
}
registerCounter (name: string, opts: PrometheusCalculatedMetricOptions): void
registerCounter (name: string, opts?: MetricOptions): Counter
registerCounter (name: string, opts: any = {}): any {
if (name == null || name.trim() === '') {
throw new Error('Counter name is required')
}
let counter = metrics.get(name)
if (counter != null) {
this.log('reuse existing counter', name)
return counter
}
this.log('register counter', name)
counter = new PrometheusCounter(name, { registry: this.registry, ...opts })
metrics.set(name, counter)
if (opts.calculate == null) {
return counter
}
}
registerCounterGroup (name: string, opts: PrometheusCalculatedMetricOptions<Record<string, number>>): void
registerCounterGroup (name: string, opts?: MetricOptions): CounterGroup
registerCounterGroup (name: string, opts: any = {}): any {
if (name == null || name.trim() === '') {
throw new Error('Counter group name is required')
}
let counterGroup = metrics.get(name)
if (counterGroup != null) {
this.log('reuse existing counter group', name)
return counterGroup
}
this.log('register counter group', name)
counterGroup = new PrometheusCounterGroup(name, { registry: this.registry, ...opts })
metrics.set(name, counterGroup)
if (opts.calculate == null) {
return counterGroup
}
}
registerHistogram (name: string, opts: PrometheusCalculatedHistogramOptions): void
registerHistogram (name: string, opts?: HistogramOptions): Histogram
registerHistogram (name: string, opts: any = {}): any {
if (name == null || name.trim() === '') {
throw new Error('Histogram name is required')
}
let metric = metrics.get(name)
if (metric != null) {
this.log('reuse existing histogram', name)
return metric
}
this.log('register histogram', name)
metric = new PrometheusHistogram(name, { registry: this.registry, ...opts })
metrics.set(name, metric)
if (opts.calculate == null) {
return metric
}
}
registerHistogramGroup (name: string, opts: PrometheusCalculatedHistogramOptions<Record<string, number>>): void
registerHistogramGroup (name: string, opts?: HistogramOptions): HistogramGroup
registerHistogramGroup (name: string, opts: any = {}): any {
if (name == null || name.trim() === '') {
throw new Error('Histogram group name is required')
}
let metricGroup = metrics.get(name)
if (metricGroup != null) {
this.log('reuse existing histogram group', name)
return metricGroup
}
this.log('register histogram group', name)
metricGroup = new PrometheusHistogramGroup(name, { registry: this.registry, ...opts })
metrics.set(name, metricGroup)
if (opts.calculate == null) {
return metricGroup
}
}
registerSummary (name: string, opts: PrometheusCalculatedSummaryOptions): void
registerSummary (name: string, opts?: SummaryOptions): Summary
registerSummary (name: string, opts: any = {}): any {
if (name == null || name.trim() === '') {
throw new Error('Summary name is required')
}
let metric = metrics.get(name)
if (metric != null) {
this.log('reuse existing summary', name)
return metric
}
this.log('register summary', name)
metric = new PrometheusSummary(name, { registry: this.registry, ...opts })
metrics.set(name, metric)
if (opts.calculate == null) {
return metric
}
}
registerSummaryGroup (name: string, opts: PrometheusCalculatedSummaryOptions<Record<string, number>>): void
registerSummaryGroup (name: string, opts?: SummaryOptions): SummaryGroup
registerSummaryGroup (name: string, opts: any = {}): any {
if (name == null || name.trim() === '') {
throw new Error('Summary group name is required')
}
let metricGroup = metrics.get(name)
if (metricGroup != null) {
this.log('reuse existing summary group', name)
return metricGroup
}
this.log('register summary group', name)
metricGroup = new PrometheusSummaryGroup(name, { registry: this.registry, ...opts })
metrics.set(name, metricGroup)
if (opts.calculate == null) {
return metricGroup
}
}
createTrace (): any {
// no-op
}
traceFunction <T extends (...args: any[]) => any> (name: string, fn: T): T {
// no-op
return fn
}
}
export function prometheusMetrics (init?: Partial<PrometheusMetricsInit>): (components: PrometheusMetricsComponents) => Metrics {
return (components) => {
return new PrometheusMetrics(components, init)
}
}