@peerkit/metrics 0.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -0
- package/package.json +40 -0
- package/src/config.ts +110 -0
- package/src/diag.ts +35 -0
- package/src/index.ts +1 -0
- package/tests/config.test.ts +64 -0
- package/tsconfig.json +10 -0
package/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# @peerkit/metrics
|
|
2
|
+
|
|
3
|
+
App-side OpenTelemetry SDK bootstrap for peerkit. Configures a global OTel
|
|
4
|
+
`MeterProvider` that exports metrics over OTLP/HTTP, and bridges OTel
|
|
5
|
+
diagnostics into the `peerkit.metrics` logtape logger.
|
|
6
|
+
|
|
7
|
+
This package is consumed **only by applications**. Library packages (e.g.
|
|
8
|
+
`@peerkit/transport-libp2p-core`) depend on `@opentelemetry/api` directly
|
|
9
|
+
and call `metrics.getMeter(...)` to obtain their meter, per the
|
|
10
|
+
[OpenTelemetry library guidelines][otel-lib]. They do not depend on the
|
|
11
|
+
SDK, and do not depend on this package.
|
|
12
|
+
|
|
13
|
+
[otel-lib]: https://opentelemetry.io/docs/specs/otel/library-guidelines/#api-and-minimal-implementation
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @peerkit/metrics
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { initMetrics, shutdownMetrics } from "@peerkit/metrics";
|
|
25
|
+
|
|
26
|
+
await initMetrics({
|
|
27
|
+
serviceName: "my-peerkit-app",
|
|
28
|
+
serviceVersion: "1.0.0",
|
|
29
|
+
otlpEndpoint: "http://localhost:4318/v1/metrics",
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// On shutdown:
|
|
33
|
+
await shutdownMetrics();
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
If `initMetrics` is never called, library instruments bind to OpenTelemetry's
|
|
37
|
+
no-op meter — they still work but record nothing, at zero cost.
|
|
38
|
+
|
|
39
|
+
## Configuration
|
|
40
|
+
|
|
41
|
+
| Option | Default | Description |
|
|
42
|
+
| ---------------------- | ----------------------------------------- | ----------------------------------------- |
|
|
43
|
+
| `serviceName` | _required_ | `service.name` resource attribute |
|
|
44
|
+
| `serviceVersion` | _unset_ | `service.version` resource attribute |
|
|
45
|
+
| `otlpEndpoint` | `http://localhost:4318/v1/metrics` | OTLP/HTTP metrics endpoint |
|
|
46
|
+
| `exportIntervalMillis` | `60000` | Periodic export interval |
|
|
47
|
+
| `exportTimeoutMillis` | `30000` | Per-export timeout |
|
|
48
|
+
| `headers` | `{}` | Extra OTLP request headers (auth, tenant) |
|
|
49
|
+
| `resourceAttributes` | `{}` | Additional OTel resource attributes |
|
|
50
|
+
| `reader` | OTLP/HTTP `PeriodicExportingMetricReader` | Override reader (mainly for tests) |
|
|
51
|
+
|
|
52
|
+
Exporter diagnostics are routed to the `peerkit.metrics` logtape logger.
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@peerkit/metrics",
|
|
3
|
+
"version": "0.1.0-alpha.1",
|
|
4
|
+
"description": "OpenTelemetry-based metric collection for peerkit",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"peerkit",
|
|
7
|
+
"metrics",
|
|
8
|
+
"opentelemetry",
|
|
9
|
+
"otlp"
|
|
10
|
+
],
|
|
11
|
+
"author": "Holochain Dev Team <devcore@holochain.org>",
|
|
12
|
+
"license": "CAL-1.0",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/holochain/peerkit/issues"
|
|
15
|
+
},
|
|
16
|
+
"homepage": "https://github.com/holochain/peerkit#readme",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/holochain/peerkit.git"
|
|
20
|
+
},
|
|
21
|
+
"type": "module",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"import": "./dist/index.js",
|
|
25
|
+
"types": "./dist/index.d.ts"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"test": "vitest --run"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@logtape/logtape": "^2.0.5",
|
|
33
|
+
"@opentelemetry/api": "^1.9.0",
|
|
34
|
+
"@opentelemetry/exporter-metrics-otlp-http": "^0.217.0",
|
|
35
|
+
"@opentelemetry/resources": "^2.7.0",
|
|
36
|
+
"@opentelemetry/sdk-metrics": "^2.7.0",
|
|
37
|
+
"@opentelemetry/semantic-conventions": "^1.40.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {}
|
|
40
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { DiagLogLevel, metrics } from "@opentelemetry/api";
|
|
2
|
+
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
|
|
3
|
+
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
4
|
+
import {
|
|
5
|
+
MeterProvider,
|
|
6
|
+
PeriodicExportingMetricReader,
|
|
7
|
+
type MetricReader,
|
|
8
|
+
} from "@opentelemetry/sdk-metrics";
|
|
9
|
+
import {
|
|
10
|
+
ATTR_SERVICE_NAME,
|
|
11
|
+
ATTR_SERVICE_VERSION,
|
|
12
|
+
} from "@opentelemetry/semantic-conventions";
|
|
13
|
+
import { installDiagLogger, uninstallDiagLogger } from "./diag.js";
|
|
14
|
+
|
|
15
|
+
export interface MetricsConfig {
|
|
16
|
+
/** `service.name` resource attribute. Required. */
|
|
17
|
+
serviceName: string;
|
|
18
|
+
/** Optional `service.version` resource attribute. */
|
|
19
|
+
serviceVersion?: string;
|
|
20
|
+
/** OTLP/HTTP metrics endpoint. Default: `http://localhost:4318/v1/metrics`. */
|
|
21
|
+
otlpEndpoint?: string;
|
|
22
|
+
/** Export interval. Default: 60 000 ms. */
|
|
23
|
+
exportIntervalMillis?: number;
|
|
24
|
+
/** Export timeout. Default: 30 000 ms. */
|
|
25
|
+
exportTimeoutMillis?: number;
|
|
26
|
+
/** Extra OTLP request headers (auth, tenant, …). */
|
|
27
|
+
headers?: Record<string, string>;
|
|
28
|
+
/** Additional resource attributes. */
|
|
29
|
+
resourceAttributes?: Record<string, string>;
|
|
30
|
+
/**
|
|
31
|
+
* Optional metric reader to use instead of the default OTLP/HTTP reader.
|
|
32
|
+
* Primarily for tests with `InMemoryMetricExporter`.
|
|
33
|
+
*/
|
|
34
|
+
reader?: MetricReader;
|
|
35
|
+
/**
|
|
36
|
+
* Minimum level of OpenTelemetry SDK diagnostic messages forwarded to the
|
|
37
|
+
* `peerkit.metrics` logtape logger. Default: {@link DiagLogLevel.WARN}.
|
|
38
|
+
* Lower (e.g. {@link DiagLogLevel.DEBUG}) is helpful when investigating
|
|
39
|
+
* why metrics are not being exported.
|
|
40
|
+
*/
|
|
41
|
+
diagLogLevel?: DiagLogLevel;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let activeProvider: MeterProvider | undefined;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Initialise the global metrics pipeline.
|
|
48
|
+
*
|
|
49
|
+
* Builds a {@link MeterProvider} that exports to the configured OTLP endpoint
|
|
50
|
+
* (or to a caller-supplied {@link MetricReader}) and registers it as the
|
|
51
|
+
* global meter provider for `@opentelemetry/api`.
|
|
52
|
+
*
|
|
53
|
+
* Must be called at most once between {@link shutdownMetrics} calls; a second
|
|
54
|
+
* call without prior shutdown rejects.
|
|
55
|
+
*/
|
|
56
|
+
export async function initMetrics(cfg: MetricsConfig): Promise<void> {
|
|
57
|
+
if (activeProvider) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
"Metrics already initialised. Call shutdownMetrics() before re-initialising.",
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
installDiagLogger(cfg.diagLogLevel);
|
|
64
|
+
|
|
65
|
+
const resourceAttrs: Record<string, string> = {
|
|
66
|
+
[ATTR_SERVICE_NAME]: cfg.serviceName,
|
|
67
|
+
...(cfg.serviceVersion && { [ATTR_SERVICE_VERSION]: cfg.serviceVersion }),
|
|
68
|
+
...(cfg.resourceAttributes ?? {}),
|
|
69
|
+
};
|
|
70
|
+
const resource = resourceFromAttributes(resourceAttrs);
|
|
71
|
+
|
|
72
|
+
const reader =
|
|
73
|
+
cfg.reader ??
|
|
74
|
+
new PeriodicExportingMetricReader({
|
|
75
|
+
exporter: new OTLPMetricExporter({
|
|
76
|
+
url: cfg.otlpEndpoint ?? "http://localhost:4318/v1/metrics",
|
|
77
|
+
headers: cfg.headers,
|
|
78
|
+
timeoutMillis: cfg.exportTimeoutMillis ?? 30_000,
|
|
79
|
+
}),
|
|
80
|
+
exportIntervalMillis: cfg.exportIntervalMillis ?? 60_000,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const provider = new MeterProvider({ resource, readers: [reader] });
|
|
84
|
+
metrics.setGlobalMeterProvider(provider);
|
|
85
|
+
activeProvider = provider;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Flush pending metrics, shut the provider down, and restore the OpenTelemetry
|
|
90
|
+
* default no-op meter provider.
|
|
91
|
+
*
|
|
92
|
+
* Safe to call when metrics have not been initialised.
|
|
93
|
+
*/
|
|
94
|
+
export async function shutdownMetrics(): Promise<void> {
|
|
95
|
+
if (!activeProvider) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const provider = activeProvider;
|
|
99
|
+
activeProvider = undefined;
|
|
100
|
+
try {
|
|
101
|
+
await provider.forceFlush();
|
|
102
|
+
} finally {
|
|
103
|
+
try {
|
|
104
|
+
await provider.shutdown();
|
|
105
|
+
} finally {
|
|
106
|
+
metrics.disable();
|
|
107
|
+
uninstallDiagLogger();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
package/src/diag.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { getLogger } from "@logtape/logtape";
|
|
2
|
+
import { diag, DiagLogLevel, type DiagLogger } from "@opentelemetry/api";
|
|
3
|
+
|
|
4
|
+
const logger = getLogger(["peerkit", "metrics"]);
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Bridge OpenTelemetry's `diag` channel to the peerkit logtape logger,
|
|
8
|
+
* so exporter errors and SDK diagnostics surface through the standard
|
|
9
|
+
* peerkit logging pipeline.
|
|
10
|
+
*
|
|
11
|
+
* `level` filters messages on the OTel side before they reach logtape:
|
|
12
|
+
* - `ERROR` — only outright failures.
|
|
13
|
+
* - `WARN` (default) — failures + retries / dropped data.
|
|
14
|
+
* - `INFO` / `DEBUG` / `VERBOSE` — increasingly chatty; useful when
|
|
15
|
+
* diagnosing why metrics are not exporting.
|
|
16
|
+
*/
|
|
17
|
+
export function installDiagLogger(
|
|
18
|
+
level: DiagLogLevel = DiagLogLevel.WARN,
|
|
19
|
+
): void {
|
|
20
|
+
const diagLogger: DiagLogger = {
|
|
21
|
+
error: (message, ...args) => logger.error(message, { args }),
|
|
22
|
+
warn: (message, ...args) => logger.warn(message, { args }),
|
|
23
|
+
info: (message, ...args) => logger.info(message, { args }),
|
|
24
|
+
debug: (message, ...args) => logger.debug(message, { args }),
|
|
25
|
+
verbose: (message, ...args) => logger.debug(message, { args }),
|
|
26
|
+
};
|
|
27
|
+
diag.setLogger(diagLogger, level);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Restore the OpenTelemetry default diag logger.
|
|
32
|
+
*/
|
|
33
|
+
export function uninstallDiagLogger(): void {
|
|
34
|
+
diag.disable();
|
|
35
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./config.js";
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AggregationTemporality,
|
|
3
|
+
InMemoryMetricExporter,
|
|
4
|
+
PeriodicExportingMetricReader,
|
|
5
|
+
} from "@opentelemetry/sdk-metrics";
|
|
6
|
+
import { metrics } from "@opentelemetry/api";
|
|
7
|
+
import { afterEach, describe, expect, test } from "vitest";
|
|
8
|
+
import { initMetrics, shutdownMetrics } from "../src/index.js";
|
|
9
|
+
|
|
10
|
+
const makeReader = () => {
|
|
11
|
+
const exporter = new InMemoryMetricExporter(
|
|
12
|
+
AggregationTemporality.CUMULATIVE,
|
|
13
|
+
);
|
|
14
|
+
const reader = new PeriodicExportingMetricReader({
|
|
15
|
+
exporter,
|
|
16
|
+
exportIntervalMillis: 60_000,
|
|
17
|
+
});
|
|
18
|
+
return { exporter, reader };
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
describe("initMetrics", () => {
|
|
22
|
+
afterEach(async () => {
|
|
23
|
+
await shutdownMetrics();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("records a counter that the configured reader observes", async () => {
|
|
27
|
+
const { exporter, reader } = makeReader();
|
|
28
|
+
await initMetrics({ serviceName: "test-service", reader });
|
|
29
|
+
|
|
30
|
+
const counter = metrics.getMeter("test").createCounter("test.counter");
|
|
31
|
+
counter.add(7, { kind: "unit" });
|
|
32
|
+
|
|
33
|
+
await reader.forceFlush();
|
|
34
|
+
const collected = exporter.getMetrics();
|
|
35
|
+
const found = collected
|
|
36
|
+
.flatMap((rm) => rm.scopeMetrics)
|
|
37
|
+
.flatMap((sm) => sm.metrics)
|
|
38
|
+
.find((m) => m.descriptor.name === "test.counter");
|
|
39
|
+
expect(found).toBeDefined();
|
|
40
|
+
expect(found?.dataPoints[0]?.value).toBe(7);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("calling initMetrics twice without shutdown rejects", async () => {
|
|
44
|
+
const { reader: r1 } = makeReader();
|
|
45
|
+
const { reader: r2 } = makeReader();
|
|
46
|
+
await initMetrics({ serviceName: "svc", reader: r1 });
|
|
47
|
+
await expect(
|
|
48
|
+
initMetrics({ serviceName: "svc", reader: r2 }),
|
|
49
|
+
).rejects.toThrow(/already initialised/i);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("shutdownMetrics reverts the global provider to the OTel no-op", async () => {
|
|
53
|
+
const { reader } = makeReader();
|
|
54
|
+
await initMetrics({ serviceName: "svc", reader });
|
|
55
|
+
const beforeProvider = metrics.getMeterProvider();
|
|
56
|
+
await shutdownMetrics();
|
|
57
|
+
const afterProvider = metrics.getMeterProvider();
|
|
58
|
+
expect(afterProvider).not.toBe(beforeProvider);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("shutdownMetrics is safe to call when not initialised", async () => {
|
|
62
|
+
await expect(shutdownMetrics()).resolves.toBeUndefined();
|
|
63
|
+
});
|
|
64
|
+
});
|