@libp2p/prometheus-metrics
Version:
Collect libp2p metrics for scraping by Prometheus or Graphana
329 lines • 10.8 kB
JavaScript
/**
* @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