UNPKG

@libp2p/prometheus-metrics

Version:

Collect libp2p metrics for scraping by Prometheus or Graphana

329 lines • 10.8 kB
/** * @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'; // export helper functions for creating buckets export { linearBuckets, exponentialBuckets } from 'prom-client'; // prom-client metrics are global const metrics = new Map(); class PrometheusMetrics { log; transferStats; registry; constructor(components, init) { 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 = {}; 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 }; } }); } [Symbol.toStringTag] = '@libp2p/metrics-prometheus'; [serviceCapabilities] = [ '@libp2p/metrics' ]; start() { } stop() { this.transferStats.clear(); } /** * Increment the transfer stat for the passed key, making sure * it exists first */ _incrementValue(key, value) { 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, name) { 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) { this._track(maConn, 'global'); } trackProtocolStream(stream, connection) { 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, opts = {}) { 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, opts = {}) { 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, opts = {}) { 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, opts = {}) { 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, opts = {}) { 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, opts = {}) { 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, opts = {}) { 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, opts = {}) { 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() { // no-op } traceFunction(name, fn) { // no-op return fn; } } export function prometheusMetrics(init) { return (components) => { return new PrometheusMetrics(components, init); }; } //# sourceMappingURL=index.js.map