@goopil/clusterkit-otlp-meter 0.1.0
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/LICENSE +819 -0
- package/README.md +123 -0
- package/dist/index.cjs +166 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +42 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +42 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +136 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +92 -0
package/README.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# `@goopil/clusterkit-otlp-meter`
|
|
2
|
+
|
|
3
|
+
OpenTelemetry OTLP metrics plugin for `@goopil/clusterkit`.
|
|
4
|
+
|
|
5
|
+
This package exports orchestration metrics and per-worker host metrics via OTLP
|
|
6
|
+
(OpenTelemetry Protocol) to a collector. It supports both OTLP/HTTP and OTLP/gRPC
|
|
7
|
+
transports.
|
|
8
|
+
|
|
9
|
+
## Capabilities
|
|
10
|
+
|
|
11
|
+
| Capability | Details |
|
|
12
|
+
|------------|---------|
|
|
13
|
+
| Orchestration metrics | Tracks active workers, restarts, crashes, and circuit-breaker trips from orchestrator events |
|
|
14
|
+
| Host/process metrics | Optional Node.js process metrics (CPU, memory, GC, event loop) via `@opentelemetry/host-metrics` |
|
|
15
|
+
| OTLP/HTTP export | Push metrics to an OTLP/HTTP collector endpoint (default) |
|
|
16
|
+
| OTLP/gRPC export | Push metrics to an OTLP/gRPC collector endpoint (optional) |
|
|
17
|
+
| Primary/worker-aware behavior | Event listeners only on primary, host metrics on workers (or primary in single-worker mode) |
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pnpm add @goopil/clusterkit-otlp-meter @opentelemetry/api @opentelemetry/sdk-metrics @opentelemetry/resources @opentelemetry/semantic-conventions @opentelemetry/exporter-metrics-otlp-http
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
For gRPC transport, also install:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pnpm add @opentelemetry/exporter-metrics-otlp-grpc
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
For host/process metrics, also install:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pnpm add @opentelemetry/host-metrics
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { Orchestrator } from "@goopil/clusterkit";
|
|
41
|
+
import { createOtlpMeterPlugin } from "@goopil/clusterkit-otlp-meter";
|
|
42
|
+
|
|
43
|
+
const orchestrator = new Orchestrator({ logger: console });
|
|
44
|
+
|
|
45
|
+
const otlp = createOtlpMeterPlugin({
|
|
46
|
+
endpoint: "http://otel-collector:4318/v1/metrics",
|
|
47
|
+
protocol: "http",
|
|
48
|
+
serviceName: "my-app",
|
|
49
|
+
instrumentation: true,
|
|
50
|
+
exportIntervalMs: 30_000,
|
|
51
|
+
attributes: { environment: "production" },
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
orchestrator.use(otlp).run(async () => {
|
|
55
|
+
// your app bootstrap
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Options (`OtlpMeterPluginOptions`)
|
|
60
|
+
|
|
61
|
+
| Option | Type | Default | Description |
|
|
62
|
+
|--------|------|---------|-------------|
|
|
63
|
+
| `endpoint` | `string` | `http://localhost:4318/v1/metrics` (HTTP) or `localhost:4317` (gRPC) | OTLP collector endpoint URL |
|
|
64
|
+
| `protocol` | `'http' \| 'grpc'` | `'http'` | OTLP transport protocol |
|
|
65
|
+
| `instrumentation` | `boolean` | `true` | Collect Node.js host/process metrics |
|
|
66
|
+
| `prefix` | `string` | `'clusterkit.'` | Metric name prefix |
|
|
67
|
+
| `attributes` | `Record<string, string \| number \| boolean>` | `{}` | Static resource attributes |
|
|
68
|
+
| `exportIntervalMs` | `number` | `60000` | Export interval in milliseconds |
|
|
69
|
+
| `serviceName` | `string` | `'clusterkit'` | Service name for the OpenTelemetry Resource |
|
|
70
|
+
|
|
71
|
+
## API (`OtlpMeterPlugin`)
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
otlp.meterProvider; // MeterProvider | undefined
|
|
75
|
+
await otlp.shutdown(); // flush and close
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Custom metrics
|
|
79
|
+
|
|
80
|
+
The plugin registers its `MeterProvider` as the OpenTelemetry global, so you can create
|
|
81
|
+
custom metrics from anywhere in your application without a reference to the plugin instance:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
import { metrics } from "@opentelemetry/api";
|
|
85
|
+
|
|
86
|
+
const meter = metrics.getMeter("my-app");
|
|
87
|
+
const httpRequests = meter.createCounter("http.requests", {
|
|
88
|
+
description: "Total HTTP requests",
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
httpRequests.add(1);
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
This works in both primary and worker processes — each process has its own provider
|
|
95
|
+
pushing to the same collector endpoint.
|
|
96
|
+
|
|
97
|
+
## Metrics exposed
|
|
98
|
+
|
|
99
|
+
With the default prefix (`clusterkit.`):
|
|
100
|
+
|
|
101
|
+
- `clusterkit.active_workers` (ObservableGauge)
|
|
102
|
+
- `clusterkit.worker.restarts` (Counter)
|
|
103
|
+
- `clusterkit.worker.crashes` (Counter)
|
|
104
|
+
- `clusterkit.circuit_breaker.trips` (Counter)
|
|
105
|
+
|
|
106
|
+
Plus Node.js host/process metrics from `@opentelemetry/host-metrics` when `instrumentation: true`.
|
|
107
|
+
|
|
108
|
+
In single-worker mode (`workers: 1`), the orchestrator runs the app directly in the
|
|
109
|
+
primary process without forking. The plugin collects host metrics on the primary since
|
|
110
|
+
there are no worker processes to collect from.
|
|
111
|
+
|
|
112
|
+
## Security / exposure notes
|
|
113
|
+
|
|
114
|
+
- This plugin does not open listening sockets — it pushes to a collector endpoint.
|
|
115
|
+
- Your collector endpoint should be on a private network or behind access controls.
|
|
116
|
+
- Treat metrics as operationally sensitive because they can expose process, topology,
|
|
117
|
+
runtime, and workload details.
|
|
118
|
+
|
|
119
|
+
## Related docs
|
|
120
|
+
|
|
121
|
+
- [Root README](../../README.md)
|
|
122
|
+
- [Core package README](../worker-manager/README.md)
|
|
123
|
+
- [Prometheus plugin README](../plugin-prometheus/README.md)
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
//#region \0rolldown/runtime.js
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
12
|
+
key = keys[i];
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except) {
|
|
14
|
+
__defProp(to, key, {
|
|
15
|
+
get: ((k) => from[k]).bind(null, key),
|
|
16
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return to;
|
|
22
|
+
};
|
|
23
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
|
|
24
|
+
value: mod,
|
|
25
|
+
enumerable: true
|
|
26
|
+
}) : target, mod));
|
|
27
|
+
|
|
28
|
+
//#endregion
|
|
29
|
+
let node_cluster = require("node:cluster");
|
|
30
|
+
node_cluster = __toESM(node_cluster, 1);
|
|
31
|
+
let node_crypto = require("node:crypto");
|
|
32
|
+
let node_os = require("node:os");
|
|
33
|
+
node_os = __toESM(node_os, 1);
|
|
34
|
+
let _goopil_clusterkit = require("@goopil/clusterkit");
|
|
35
|
+
let _opentelemetry_api = require("@opentelemetry/api");
|
|
36
|
+
let _opentelemetry_resources = require("@opentelemetry/resources");
|
|
37
|
+
let _opentelemetry_sdk_metrics = require("@opentelemetry/sdk-metrics");
|
|
38
|
+
let _opentelemetry_semantic_conventions = require("@opentelemetry/semantic-conventions");
|
|
39
|
+
|
|
40
|
+
//#region src/index.ts
|
|
41
|
+
const DEFAULT_HTTP_ENDPOINT = "http://localhost:4318/v1/metrics";
|
|
42
|
+
const DEFAULT_GRPC_ENDPOINT = "localhost:4317";
|
|
43
|
+
const ATTR_HOST_NAME = "host.name";
|
|
44
|
+
const ATTR_PROCESS_PID = "process.pid";
|
|
45
|
+
const PLUGIN_VERSION = "0.1.0";
|
|
46
|
+
function isMissingModuleError(err) {
|
|
47
|
+
if (!(err instanceof Error)) return false;
|
|
48
|
+
const msg = err.message;
|
|
49
|
+
return msg.includes("Cannot find package") || msg.includes("MODULE_NOT_FOUND") || msg.includes("is not a constructor") || msg.includes("export is defined on the") || msg.includes("is not defined on the") || "code" in err;
|
|
50
|
+
}
|
|
51
|
+
function createOtlpMeterPlugin(options = {}) {
|
|
52
|
+
const { protocol = "http", instrumentation = true, prefix = "clusterkit.", attributes = {}, exportIntervalMs = 6e4, serviceName = "clusterkit", endpoint } = options;
|
|
53
|
+
if (!Number.isFinite(exportIntervalMs) || exportIntervalMs <= 0) throw new TypeError("otlp-meter plugin: exportIntervalMs must be a finite number > 0");
|
|
54
|
+
const resolvedEndpoint = endpoint ?? (protocol === "grpc" ? DEFAULT_GRPC_ENDPOINT : DEFAULT_HTTP_ENDPOINT);
|
|
55
|
+
if (protocol === "http") try {
|
|
56
|
+
new URL(resolvedEndpoint);
|
|
57
|
+
} catch {
|
|
58
|
+
throw new TypeError(`otlp-meter plugin: invalid endpoint URL "${resolvedEndpoint}"`);
|
|
59
|
+
}
|
|
60
|
+
let meterProvider;
|
|
61
|
+
let isShutdown = false;
|
|
62
|
+
let pluginLog = null;
|
|
63
|
+
let primaryOrchestrator;
|
|
64
|
+
const primaryListeners = [];
|
|
65
|
+
const clearPrimaryListeners = () => {
|
|
66
|
+
if (!primaryOrchestrator) return;
|
|
67
|
+
for (const { event, listener } of primaryListeners) primaryOrchestrator.off(event, listener);
|
|
68
|
+
primaryListeners.length = 0;
|
|
69
|
+
primaryOrchestrator = void 0;
|
|
70
|
+
};
|
|
71
|
+
async function createExporter() {
|
|
72
|
+
const exporterModuleName = protocol === "grpc" ? "@opentelemetry/exporter-metrics-otlp-grpc" : "@opentelemetry/exporter-metrics-otlp-http";
|
|
73
|
+
try {
|
|
74
|
+
return new (await (import(exporterModuleName))).OTLPMetricExporter({ url: resolvedEndpoint });
|
|
75
|
+
} catch (err) {
|
|
76
|
+
if (isMissingModuleError(err)) throw new Error(`otlp-meter plugin: protocol '${protocol}' requires ${exporterModuleName} — install it or use protocol '${protocol === "grpc" ? "http" : "grpc"}'`);
|
|
77
|
+
throw err;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async function startHostMetrics(provider) {
|
|
81
|
+
try {
|
|
82
|
+
new (await (import("@opentelemetry/host-metrics"))).HostMetrics({ meterProvider: provider }).start();
|
|
83
|
+
} catch (err) {
|
|
84
|
+
if (isMissingModuleError(err)) pluginLog?.warn("host-metrics package not installed — skipping process metrics");
|
|
85
|
+
else throw err;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async function shutdownProvider() {
|
|
89
|
+
if (isShutdown || !meterProvider) return;
|
|
90
|
+
isShutdown = true;
|
|
91
|
+
await meterProvider.shutdown();
|
|
92
|
+
meterProvider = void 0;
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
name: "otlp-meter",
|
|
96
|
+
get meterProvider() {
|
|
97
|
+
return meterProvider;
|
|
98
|
+
},
|
|
99
|
+
async install(orchestrator, logger, _config) {
|
|
100
|
+
const log = (0, _goopil_clusterkit.withLoggerPrefix)(logger, "clusterkit:otlp-meter");
|
|
101
|
+
pluginLog = log;
|
|
102
|
+
const resource = (0, _opentelemetry_resources.resourceFromAttributes)({
|
|
103
|
+
[_opentelemetry_semantic_conventions.ATTR_SERVICE_NAME]: serviceName,
|
|
104
|
+
[_opentelemetry_semantic_conventions.ATTR_SERVICE_INSTANCE_ID]: (0, node_crypto.randomUUID)(),
|
|
105
|
+
[ATTR_HOST_NAME]: node_os.default.hostname(),
|
|
106
|
+
[ATTR_PROCESS_PID]: process.pid,
|
|
107
|
+
...attributes
|
|
108
|
+
});
|
|
109
|
+
const exporter = await createExporter();
|
|
110
|
+
const metricReader = new _opentelemetry_sdk_metrics.PeriodicExportingMetricReader({
|
|
111
|
+
exporter,
|
|
112
|
+
exportIntervalMillis: exportIntervalMs
|
|
113
|
+
});
|
|
114
|
+
meterProvider = new _opentelemetry_sdk_metrics.MeterProvider({
|
|
115
|
+
resource,
|
|
116
|
+
readers: [metricReader]
|
|
117
|
+
});
|
|
118
|
+
_opentelemetry_api.metrics.setGlobalMeterProvider(meterProvider);
|
|
119
|
+
const meter = meterProvider.getMeter("@goopil/clusterkit", PLUGIN_VERSION);
|
|
120
|
+
if (node_cluster.default.isPrimary) {
|
|
121
|
+
clearPrimaryListeners();
|
|
122
|
+
log?.debug("Plugin installed on primary process");
|
|
123
|
+
primaryOrchestrator = orchestrator;
|
|
124
|
+
meter.createObservableGauge(`${prefix}active_workers`, { description: "Number of active cluster workers" }).addCallback((result) => {
|
|
125
|
+
result.observe(orchestrator.getMetrics().activeWorkers);
|
|
126
|
+
});
|
|
127
|
+
const workerRestartsCounter = meter.createCounter(`${prefix}worker.restarts`, { description: "Total number of worker restarts" });
|
|
128
|
+
const workerCrashesCounter = meter.createCounter(`${prefix}worker.crashes`, { description: "Total number of worker crashes" });
|
|
129
|
+
const circuitBreakerTripsCounter = meter.createCounter(`${prefix}circuit_breaker.trips`, { description: "Total number of circuit breaker trips" });
|
|
130
|
+
const bind = (event, listener) => {
|
|
131
|
+
orchestrator.on(event, listener);
|
|
132
|
+
primaryListeners.push({
|
|
133
|
+
event,
|
|
134
|
+
listener
|
|
135
|
+
});
|
|
136
|
+
};
|
|
137
|
+
bind("worker:crash", () => {
|
|
138
|
+
workerCrashesCounter.add(1);
|
|
139
|
+
});
|
|
140
|
+
bind("worker:restart", () => {
|
|
141
|
+
workerRestartsCounter.add(1);
|
|
142
|
+
});
|
|
143
|
+
bind("circuit-breaker:tripped", () => {
|
|
144
|
+
circuitBreakerTripsCounter.add(1);
|
|
145
|
+
});
|
|
146
|
+
if (orchestrator.workerCount === 1 && instrumentation) await startHostMetrics(meterProvider);
|
|
147
|
+
} else {
|
|
148
|
+
log?.debug("Plugin installed on worker process");
|
|
149
|
+
if (instrumentation) await startHostMetrics(meterProvider);
|
|
150
|
+
}
|
|
151
|
+
orchestrator.registerOnShutdown(async () => {
|
|
152
|
+
await shutdownProvider();
|
|
153
|
+
});
|
|
154
|
+
},
|
|
155
|
+
async uninstall() {
|
|
156
|
+
clearPrimaryListeners();
|
|
157
|
+
},
|
|
158
|
+
async shutdown() {
|
|
159
|
+
await shutdownProvider();
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
//#endregion
|
|
165
|
+
exports.createOtlpMeterPlugin = createOtlpMeterPlugin;
|
|
166
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["withLoggerPrefix","resourceFromAttributes","ATTR_SERVICE_NAME","ATTR_SERVICE_INSTANCE_ID","randomUUID","os","PeriodicExportingMetricReader","MeterProvider","cluster"],"sources":["../src/index.ts"],"sourcesContent":["import cluster from \"node:cluster\";\nimport { randomUUID } from \"node:crypto\";\nimport os from \"node:os\";\nimport { type Logger, type Orchestrator, type ResolvedConfig, withLoggerPrefix } from \"@goopil/clusterkit\";\nimport { metrics } from \"@opentelemetry/api\";\nimport { resourceFromAttributes } from \"@opentelemetry/resources\";\nimport { MeterProvider, PeriodicExportingMetricReader, type PushMetricExporter } from \"@opentelemetry/sdk-metrics\";\nimport { ATTR_SERVICE_INSTANCE_ID, ATTR_SERVICE_NAME } from \"@opentelemetry/semantic-conventions\";\nimport type { OtlpMeterPlugin, OtlpMeterPluginOptions } from \"./types.js\";\n\nexport type { OtlpMeterPlugin, OtlpMeterPluginOptions } from \"./types.js\";\n\ntype PrimaryEvent = \"worker:crash\" | \"worker:restart\" | \"circuit-breaker:tripped\";\n\nconst DEFAULT_HTTP_ENDPOINT = \"http://localhost:4318/v1/metrics\";\nconst DEFAULT_GRPC_ENDPOINT = \"localhost:4317\";\n\nconst ATTR_HOST_NAME = \"host.name\";\nconst ATTR_PROCESS_PID = \"process.pid\";\n\nconst PLUGIN_VERSION = \"0.1.0\";\n\nfunction isMissingModuleError(err: unknown): boolean {\n if (!(err instanceof Error)) return false;\n const msg = err.message;\n return (\n msg.includes(\"Cannot find package\") ||\n msg.includes(\"MODULE_NOT_FOUND\") ||\n msg.includes(\"is not a constructor\") ||\n msg.includes(\"export is defined on the\") ||\n msg.includes(\"is not defined on the\") ||\n \"code\" in err\n );\n}\n\nexport function createOtlpMeterPlugin(options: OtlpMeterPluginOptions = {}): OtlpMeterPlugin {\n const {\n protocol = \"http\",\n instrumentation = true,\n prefix = \"clusterkit.\",\n attributes = {},\n exportIntervalMs = 60_000,\n serviceName = \"clusterkit\",\n endpoint,\n } = options;\n\n if (!Number.isFinite(exportIntervalMs) || exportIntervalMs <= 0) {\n throw new TypeError(\"otlp-meter plugin: exportIntervalMs must be a finite number > 0\");\n }\n\n const resolvedEndpoint = endpoint ?? (protocol === \"grpc\" ? DEFAULT_GRPC_ENDPOINT : DEFAULT_HTTP_ENDPOINT);\n\n if (protocol === \"http\") {\n try {\n new URL(resolvedEndpoint);\n } catch {\n throw new TypeError(`otlp-meter plugin: invalid endpoint URL \"${resolvedEndpoint}\"`);\n }\n }\n\n let meterProvider: MeterProvider | undefined;\n let isShutdown = false;\n let pluginLog: Logger | null = null;\n let primaryOrchestrator: Orchestrator | undefined;\n const primaryListeners: Array<{ event: PrimaryEvent; listener: () => void }> = [];\n\n const clearPrimaryListeners = (): void => {\n if (!primaryOrchestrator) return;\n for (const { event, listener } of primaryListeners) {\n primaryOrchestrator.off(event, listener);\n }\n primaryListeners.length = 0;\n primaryOrchestrator = undefined;\n };\n\n async function createExporter(): Promise<PushMetricExporter> {\n const exporterModuleName =\n protocol === \"grpc\" ? \"@opentelemetry/exporter-metrics-otlp-grpc\" : \"@opentelemetry/exporter-metrics-otlp-http\";\n\n try {\n const mod = await import(exporterModuleName);\n return new mod.OTLPMetricExporter({ url: resolvedEndpoint }) as PushMetricExporter;\n } catch (err) {\n if (isMissingModuleError(err)) {\n throw new Error(\n `otlp-meter plugin: protocol '${protocol}' requires ${exporterModuleName} — install it or use protocol '${protocol === \"grpc\" ? \"http\" : \"grpc\"}'`,\n );\n }\n throw err;\n }\n }\n\n async function startHostMetrics(provider: MeterProvider): Promise<void> {\n try {\n const mod = await import(\"@opentelemetry/host-metrics\");\n const hostMetrics = new mod.HostMetrics({ meterProvider: provider });\n hostMetrics.start();\n } catch (err) {\n if (isMissingModuleError(err)) {\n pluginLog?.warn(\"host-metrics package not installed — skipping process metrics\");\n } else {\n throw err;\n }\n }\n }\n\n async function shutdownProvider(): Promise<void> {\n if (isShutdown || !meterProvider) return;\n isShutdown = true;\n await meterProvider.shutdown();\n meterProvider = undefined;\n }\n\n return {\n name: \"otlp-meter\",\n\n get meterProvider() {\n return meterProvider;\n },\n\n async install(orchestrator: Orchestrator, logger: Logger | null, _config: ResolvedConfig): Promise<void> {\n const log = withLoggerPrefix(logger, \"clusterkit:otlp-meter\");\n pluginLog = log;\n\n const resource = resourceFromAttributes({\n [ATTR_SERVICE_NAME]: serviceName,\n [ATTR_SERVICE_INSTANCE_ID]: randomUUID(),\n [ATTR_HOST_NAME]: os.hostname(),\n [ATTR_PROCESS_PID]: process.pid,\n ...attributes,\n });\n\n const exporter = await createExporter();\n\n const metricReader = new PeriodicExportingMetricReader({\n exporter,\n exportIntervalMillis: exportIntervalMs,\n });\n\n meterProvider = new MeterProvider({\n resource,\n readers: [metricReader],\n });\n\n metrics.setGlobalMeterProvider(meterProvider);\n\n const meter = meterProvider.getMeter(\"@goopil/clusterkit\", PLUGIN_VERSION);\n\n if (cluster.isPrimary) {\n clearPrimaryListeners();\n log?.debug(\"Plugin installed on primary process\");\n primaryOrchestrator = orchestrator;\n\n const activeWorkersGauge = meter.createObservableGauge(`${prefix}active_workers`, {\n description: \"Number of active cluster workers\",\n });\n activeWorkersGauge.addCallback((result) => {\n result.observe(orchestrator.getMetrics().activeWorkers);\n });\n\n const workerRestartsCounter = meter.createCounter(`${prefix}worker.restarts`, {\n description: \"Total number of worker restarts\",\n });\n const workerCrashesCounter = meter.createCounter(`${prefix}worker.crashes`, {\n description: \"Total number of worker crashes\",\n });\n const circuitBreakerTripsCounter = meter.createCounter(`${prefix}circuit_breaker.trips`, {\n description: \"Total number of circuit breaker trips\",\n });\n\n const bind = (event: PrimaryEvent, listener: () => void): void => {\n orchestrator.on(event, listener);\n primaryListeners.push({ event, listener });\n };\n\n bind(\"worker:crash\", () => {\n workerCrashesCounter.add(1);\n });\n bind(\"worker:restart\", () => {\n workerRestartsCounter.add(1);\n });\n bind(\"circuit-breaker:tripped\", () => {\n circuitBreakerTripsCounter.add(1);\n });\n\n const singleWorker = orchestrator.workerCount === 1;\n if (singleWorker && instrumentation) {\n await startHostMetrics(meterProvider);\n }\n } else {\n log?.debug(\"Plugin installed on worker process\");\n\n if (instrumentation) {\n await startHostMetrics(meterProvider);\n }\n }\n\n orchestrator.registerOnShutdown(async () => {\n await shutdownProvider();\n });\n },\n\n async uninstall(): Promise<void> {\n clearPrimaryListeners();\n },\n\n async shutdown(): Promise<void> {\n await shutdownProvider();\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAcA,MAAM,wBAAwB;AAC9B,MAAM,wBAAwB;AAE9B,MAAM,iBAAiB;AACvB,MAAM,mBAAmB;AAEzB,MAAM,iBAAiB;AAEvB,SAAS,qBAAqB,KAAuB;CACnD,IAAI,EAAE,eAAe,QAAQ,OAAO;CACpC,MAAM,MAAM,IAAI;CAChB,OACE,IAAI,SAAS,qBAAqB,KAClC,IAAI,SAAS,kBAAkB,KAC/B,IAAI,SAAS,sBAAsB,KACnC,IAAI,SAAS,0BAA0B,KACvC,IAAI,SAAS,uBAAuB,KACpC,UAAU;AAEd;AAEA,SAAgB,sBAAsB,UAAkC,CAAC,GAAoB;CAC3F,MAAM,EACJ,WAAW,QACX,kBAAkB,MAClB,SAAS,eACT,aAAa,CAAC,GACd,mBAAmB,KACnB,cAAc,cACd,aACE;CAEJ,IAAI,CAAC,OAAO,SAAS,gBAAgB,KAAK,oBAAoB,GAC5D,MAAM,IAAI,UAAU,iEAAiE;CAGvF,MAAM,mBAAmB,aAAa,aAAa,SAAS,wBAAwB;CAEpF,IAAI,aAAa,QACf,IAAI;EACF,IAAI,IAAI,gBAAgB;CAC1B,QAAQ;EACN,MAAM,IAAI,UAAU,4CAA4C,iBAAiB,EAAE;CACrF;CAGF,IAAI;CACJ,IAAI,aAAa;CACjB,IAAI,YAA2B;CAC/B,IAAI;CACJ,MAAM,mBAAyE,CAAC;CAEhF,MAAM,8BAAoC;EACxC,IAAI,CAAC,qBAAqB;EAC1B,KAAK,MAAM,EAAE,OAAO,cAAc,kBAChC,oBAAoB,IAAI,OAAO,QAAQ;EAEzC,iBAAiB,SAAS;EAC1B,sBAAsB;CACxB;CAEA,eAAe,iBAA8C;EAC3D,MAAM,qBACJ,aAAa,SAAS,8CAA8C;EAEtE,IAAI;GAEF,OAAO,KAAI,OADO,OAAO,qBACX,CAAC,mBAAmB,EAAE,KAAK,iBAAiB,CAAC;EAC7D,SAAS,KAAK;GACZ,IAAI,qBAAqB,GAAG,GAC1B,MAAM,IAAI,MACR,gCAAgC,SAAS,aAAa,mBAAmB,iCAAiC,aAAa,SAAS,SAAS,OAAO,EAClJ;GAEF,MAAM;EACR;CACF;CAEA,eAAe,iBAAiB,UAAwC;EACtE,IAAI;GAGF,KADwB,OADN,OAAO,gCACE,CAAC,YAAY,EAAE,eAAe,SAAS,CACxD,CAAC,CAAC,MAAM;EACpB,SAAS,KAAK;GACZ,IAAI,qBAAqB,GAAG,GAC1B,WAAW,KAAK,+DAA+D;QAE/E,MAAM;EAEV;CACF;CAEA,eAAe,mBAAkC;EAC/C,IAAI,cAAc,CAAC,eAAe;EAClC,aAAa;EACb,MAAM,cAAc,SAAS;EAC7B,gBAAgB;CAClB;CAEA,OAAO;EACL,MAAM;EAEN,IAAI,gBAAgB;GAClB,OAAO;EACT;EAEA,MAAM,QAAQ,cAA4B,QAAuB,SAAwC;GACvG,MAAM,UAAMA,qCAAiB,QAAQ,uBAAuB;GAC5D,YAAY;GAEZ,MAAM,eAAWC,iDAAuB;KACrCC,wDAAoB;KACpBC,mEAA2BC,wBAAW;KACtC,iBAAiBC,gBAAG,SAAS;KAC7B,mBAAmB,QAAQ;IAC5B,GAAG;GACL,CAAC;GAED,MAAM,WAAW,MAAM,eAAe;GAEtC,MAAM,eAAe,IAAIC,yDAA8B;IACrD;IACA,sBAAsB;GACxB,CAAC;GAED,gBAAgB,IAAIC,yCAAc;IAChC;IACA,SAAS,CAAC,YAAY;GACxB,CAAC;GAED,2BAAQ,uBAAuB,aAAa;GAE5C,MAAM,QAAQ,cAAc,SAAS,sBAAsB,cAAc;GAEzE,IAAIC,qBAAQ,WAAW;IACrB,sBAAsB;IACtB,KAAK,MAAM,qCAAqC;IAChD,sBAAsB;IAKtB,AAH2B,MAAM,sBAAsB,GAAG,OAAO,iBAAiB,EAChF,aAAa,mCACf,CACiB,CAAC,CAAC,aAAa,WAAW;KACzC,OAAO,QAAQ,aAAa,WAAW,CAAC,CAAC,aAAa;IACxD,CAAC;IAED,MAAM,wBAAwB,MAAM,cAAc,GAAG,OAAO,kBAAkB,EAC5E,aAAa,kCACf,CAAC;IACD,MAAM,uBAAuB,MAAM,cAAc,GAAG,OAAO,iBAAiB,EAC1E,aAAa,iCACf,CAAC;IACD,MAAM,6BAA6B,MAAM,cAAc,GAAG,OAAO,wBAAwB,EACvF,aAAa,wCACf,CAAC;IAED,MAAM,QAAQ,OAAqB,aAA+B;KAChE,aAAa,GAAG,OAAO,QAAQ;KAC/B,iBAAiB,KAAK;MAAE;MAAO;KAAS,CAAC;IAC3C;IAEA,KAAK,sBAAsB;KACzB,qBAAqB,IAAI,CAAC;IAC5B,CAAC;IACD,KAAK,wBAAwB;KAC3B,sBAAsB,IAAI,CAAC;IAC7B,CAAC;IACD,KAAK,iCAAiC;KACpC,2BAA2B,IAAI,CAAC;IAClC,CAAC;IAGD,IADqB,aAAa,gBAAgB,KAC9B,iBAClB,MAAM,iBAAiB,aAAa;GAExC,OAAO;IACL,KAAK,MAAM,oCAAoC;IAE/C,IAAI,iBACF,MAAM,iBAAiB,aAAa;GAExC;GAEA,aAAa,mBAAmB,YAAY;IAC1C,MAAM,iBAAiB;GACzB,CAAC;EACH;EAEA,MAAM,YAA2B;GAC/B,sBAAsB;EACxB;EAEA,MAAM,WAA0B;GAC9B,MAAM,iBAAiB;EACzB;CACF;AACF"}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { OrchestratorPlugin } from "@goopil/clusterkit";
|
|
2
|
+
import { MeterProvider } from "@opentelemetry/sdk-metrics";
|
|
3
|
+
//#region src/types.d.ts
|
|
4
|
+
interface OtlpMeterPluginOptions {
|
|
5
|
+
/**
|
|
6
|
+
* OTLP collector endpoint URL.
|
|
7
|
+
* For 'http' protocol: defaults to 'http://localhost:4318/v1/metrics'
|
|
8
|
+
* For 'grpc' protocol: defaults to 'localhost:4317'
|
|
9
|
+
* If you pass a full URL, it overrides the default for the selected protocol.
|
|
10
|
+
*/
|
|
11
|
+
endpoint?: string;
|
|
12
|
+
/** OTLP transport protocol. @default 'http' */
|
|
13
|
+
protocol?: "http" | "grpc";
|
|
14
|
+
/** Collect Node.js host/process metrics (CPU, memory, GC, event loop). @default true */
|
|
15
|
+
instrumentation?: boolean;
|
|
16
|
+
/** Metric name prefix. @default 'clusterkit.' */
|
|
17
|
+
prefix?: string;
|
|
18
|
+
/** Static resource attributes added to the OpenTelemetry Resource. @default {} */
|
|
19
|
+
attributes?: Record<string, string | number | boolean>;
|
|
20
|
+
/** Export interval in milliseconds (how often metrics push to the collector). @default 60000 */
|
|
21
|
+
exportIntervalMs?: number;
|
|
22
|
+
/** Service name for the OpenTelemetry Resource. @default 'clusterkit' */
|
|
23
|
+
serviceName?: string;
|
|
24
|
+
}
|
|
25
|
+
interface OtlpMeterPlugin extends OrchestratorPlugin {
|
|
26
|
+
/**
|
|
27
|
+
* The OpenTelemetry MeterProvider (primary only; undefined in workers
|
|
28
|
+
* and before install).
|
|
29
|
+
*/
|
|
30
|
+
readonly meterProvider: MeterProvider | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* Gracefully shut down the provider and flush pending exports.
|
|
33
|
+
* Also registered via orchestrator.registerOnShutdown() during install.
|
|
34
|
+
*/
|
|
35
|
+
shutdown(): Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region src/index.d.ts
|
|
39
|
+
declare function createOtlpMeterPlugin(options?: OtlpMeterPluginOptions): OtlpMeterPlugin;
|
|
40
|
+
//#endregion
|
|
41
|
+
export { type OtlpMeterPlugin, type OtlpMeterPluginOptions, createOtlpMeterPlugin };
|
|
42
|
+
//# sourceMappingURL=index.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/types.ts","../src/index.ts"],"mappings":";;;UAGiB;;;;;;;EAOf;;EAGA;;EAGA;;EAGA;;EAGA,aAAa;;EAGb;;EAGA;;UAGe,wBAAwB;;;;;WAK9B,eAAe;;;;;EAMxB,YAAY;;;;iBCPE,sBAAsB,UAAS,yBAA8B"}
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { OrchestratorPlugin } from "@goopil/clusterkit";
|
|
2
|
+
import { MeterProvider } from "@opentelemetry/sdk-metrics";
|
|
3
|
+
//#region src/types.d.ts
|
|
4
|
+
interface OtlpMeterPluginOptions {
|
|
5
|
+
/**
|
|
6
|
+
* OTLP collector endpoint URL.
|
|
7
|
+
* For 'http' protocol: defaults to 'http://localhost:4318/v1/metrics'
|
|
8
|
+
* For 'grpc' protocol: defaults to 'localhost:4317'
|
|
9
|
+
* If you pass a full URL, it overrides the default for the selected protocol.
|
|
10
|
+
*/
|
|
11
|
+
endpoint?: string;
|
|
12
|
+
/** OTLP transport protocol. @default 'http' */
|
|
13
|
+
protocol?: "http" | "grpc";
|
|
14
|
+
/** Collect Node.js host/process metrics (CPU, memory, GC, event loop). @default true */
|
|
15
|
+
instrumentation?: boolean;
|
|
16
|
+
/** Metric name prefix. @default 'clusterkit.' */
|
|
17
|
+
prefix?: string;
|
|
18
|
+
/** Static resource attributes added to the OpenTelemetry Resource. @default {} */
|
|
19
|
+
attributes?: Record<string, string | number | boolean>;
|
|
20
|
+
/** Export interval in milliseconds (how often metrics push to the collector). @default 60000 */
|
|
21
|
+
exportIntervalMs?: number;
|
|
22
|
+
/** Service name for the OpenTelemetry Resource. @default 'clusterkit' */
|
|
23
|
+
serviceName?: string;
|
|
24
|
+
}
|
|
25
|
+
interface OtlpMeterPlugin extends OrchestratorPlugin {
|
|
26
|
+
/**
|
|
27
|
+
* The OpenTelemetry MeterProvider (primary only; undefined in workers
|
|
28
|
+
* and before install).
|
|
29
|
+
*/
|
|
30
|
+
readonly meterProvider: MeterProvider | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* Gracefully shut down the provider and flush pending exports.
|
|
33
|
+
* Also registered via orchestrator.registerOnShutdown() during install.
|
|
34
|
+
*/
|
|
35
|
+
shutdown(): Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region src/index.d.ts
|
|
39
|
+
declare function createOtlpMeterPlugin(options?: OtlpMeterPluginOptions): OtlpMeterPlugin;
|
|
40
|
+
//#endregion
|
|
41
|
+
export { type OtlpMeterPlugin, type OtlpMeterPluginOptions, createOtlpMeterPlugin };
|
|
42
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/index.ts"],"mappings":";;;UAGiB;;;;;;;EAOf;;EAGA;;EAGA;;EAGA;;EAGA,aAAa;;EAGb;;EAGA;;UAGe,wBAAwB;;;;;WAK9B,eAAe;;;;;EAMxB,YAAY;;;;iBCPE,sBAAsB,UAAS,yBAA8B"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import cluster from "node:cluster";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import { withLoggerPrefix } from "@goopil/clusterkit";
|
|
5
|
+
import { metrics } from "@opentelemetry/api";
|
|
6
|
+
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
7
|
+
import { MeterProvider, PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
|
|
8
|
+
import { ATTR_SERVICE_INSTANCE_ID, ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
|
|
9
|
+
|
|
10
|
+
//#region src/index.ts
|
|
11
|
+
const DEFAULT_HTTP_ENDPOINT = "http://localhost:4318/v1/metrics";
|
|
12
|
+
const DEFAULT_GRPC_ENDPOINT = "localhost:4317";
|
|
13
|
+
const ATTR_HOST_NAME = "host.name";
|
|
14
|
+
const ATTR_PROCESS_PID = "process.pid";
|
|
15
|
+
const PLUGIN_VERSION = "0.1.0";
|
|
16
|
+
function isMissingModuleError(err) {
|
|
17
|
+
if (!(err instanceof Error)) return false;
|
|
18
|
+
const msg = err.message;
|
|
19
|
+
return msg.includes("Cannot find package") || msg.includes("MODULE_NOT_FOUND") || msg.includes("is not a constructor") || msg.includes("export is defined on the") || msg.includes("is not defined on the") || "code" in err;
|
|
20
|
+
}
|
|
21
|
+
function createOtlpMeterPlugin(options = {}) {
|
|
22
|
+
const { protocol = "http", instrumentation = true, prefix = "clusterkit.", attributes = {}, exportIntervalMs = 6e4, serviceName = "clusterkit", endpoint } = options;
|
|
23
|
+
if (!Number.isFinite(exportIntervalMs) || exportIntervalMs <= 0) throw new TypeError("otlp-meter plugin: exportIntervalMs must be a finite number > 0");
|
|
24
|
+
const resolvedEndpoint = endpoint ?? (protocol === "grpc" ? DEFAULT_GRPC_ENDPOINT : DEFAULT_HTTP_ENDPOINT);
|
|
25
|
+
if (protocol === "http") try {
|
|
26
|
+
new URL(resolvedEndpoint);
|
|
27
|
+
} catch {
|
|
28
|
+
throw new TypeError(`otlp-meter plugin: invalid endpoint URL "${resolvedEndpoint}"`);
|
|
29
|
+
}
|
|
30
|
+
let meterProvider;
|
|
31
|
+
let isShutdown = false;
|
|
32
|
+
let pluginLog = null;
|
|
33
|
+
let primaryOrchestrator;
|
|
34
|
+
const primaryListeners = [];
|
|
35
|
+
const clearPrimaryListeners = () => {
|
|
36
|
+
if (!primaryOrchestrator) return;
|
|
37
|
+
for (const { event, listener } of primaryListeners) primaryOrchestrator.off(event, listener);
|
|
38
|
+
primaryListeners.length = 0;
|
|
39
|
+
primaryOrchestrator = void 0;
|
|
40
|
+
};
|
|
41
|
+
async function createExporter() {
|
|
42
|
+
const exporterModuleName = protocol === "grpc" ? "@opentelemetry/exporter-metrics-otlp-grpc" : "@opentelemetry/exporter-metrics-otlp-http";
|
|
43
|
+
try {
|
|
44
|
+
return new (await (import(exporterModuleName))).OTLPMetricExporter({ url: resolvedEndpoint });
|
|
45
|
+
} catch (err) {
|
|
46
|
+
if (isMissingModuleError(err)) throw new Error(`otlp-meter plugin: protocol '${protocol}' requires ${exporterModuleName} — install it or use protocol '${protocol === "grpc" ? "http" : "grpc"}'`);
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function startHostMetrics(provider) {
|
|
51
|
+
try {
|
|
52
|
+
new (await (import("@opentelemetry/host-metrics"))).HostMetrics({ meterProvider: provider }).start();
|
|
53
|
+
} catch (err) {
|
|
54
|
+
if (isMissingModuleError(err)) pluginLog?.warn("host-metrics package not installed — skipping process metrics");
|
|
55
|
+
else throw err;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async function shutdownProvider() {
|
|
59
|
+
if (isShutdown || !meterProvider) return;
|
|
60
|
+
isShutdown = true;
|
|
61
|
+
await meterProvider.shutdown();
|
|
62
|
+
meterProvider = void 0;
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
name: "otlp-meter",
|
|
66
|
+
get meterProvider() {
|
|
67
|
+
return meterProvider;
|
|
68
|
+
},
|
|
69
|
+
async install(orchestrator, logger, _config) {
|
|
70
|
+
const log = withLoggerPrefix(logger, "clusterkit:otlp-meter");
|
|
71
|
+
pluginLog = log;
|
|
72
|
+
const resource = resourceFromAttributes({
|
|
73
|
+
[ATTR_SERVICE_NAME]: serviceName,
|
|
74
|
+
[ATTR_SERVICE_INSTANCE_ID]: randomUUID(),
|
|
75
|
+
[ATTR_HOST_NAME]: os.hostname(),
|
|
76
|
+
[ATTR_PROCESS_PID]: process.pid,
|
|
77
|
+
...attributes
|
|
78
|
+
});
|
|
79
|
+
const exporter = await createExporter();
|
|
80
|
+
const metricReader = new PeriodicExportingMetricReader({
|
|
81
|
+
exporter,
|
|
82
|
+
exportIntervalMillis: exportIntervalMs
|
|
83
|
+
});
|
|
84
|
+
meterProvider = new MeterProvider({
|
|
85
|
+
resource,
|
|
86
|
+
readers: [metricReader]
|
|
87
|
+
});
|
|
88
|
+
metrics.setGlobalMeterProvider(meterProvider);
|
|
89
|
+
const meter = meterProvider.getMeter("@goopil/clusterkit", PLUGIN_VERSION);
|
|
90
|
+
if (cluster.isPrimary) {
|
|
91
|
+
clearPrimaryListeners();
|
|
92
|
+
log?.debug("Plugin installed on primary process");
|
|
93
|
+
primaryOrchestrator = orchestrator;
|
|
94
|
+
meter.createObservableGauge(`${prefix}active_workers`, { description: "Number of active cluster workers" }).addCallback((result) => {
|
|
95
|
+
result.observe(orchestrator.getMetrics().activeWorkers);
|
|
96
|
+
});
|
|
97
|
+
const workerRestartsCounter = meter.createCounter(`${prefix}worker.restarts`, { description: "Total number of worker restarts" });
|
|
98
|
+
const workerCrashesCounter = meter.createCounter(`${prefix}worker.crashes`, { description: "Total number of worker crashes" });
|
|
99
|
+
const circuitBreakerTripsCounter = meter.createCounter(`${prefix}circuit_breaker.trips`, { description: "Total number of circuit breaker trips" });
|
|
100
|
+
const bind = (event, listener) => {
|
|
101
|
+
orchestrator.on(event, listener);
|
|
102
|
+
primaryListeners.push({
|
|
103
|
+
event,
|
|
104
|
+
listener
|
|
105
|
+
});
|
|
106
|
+
};
|
|
107
|
+
bind("worker:crash", () => {
|
|
108
|
+
workerCrashesCounter.add(1);
|
|
109
|
+
});
|
|
110
|
+
bind("worker:restart", () => {
|
|
111
|
+
workerRestartsCounter.add(1);
|
|
112
|
+
});
|
|
113
|
+
bind("circuit-breaker:tripped", () => {
|
|
114
|
+
circuitBreakerTripsCounter.add(1);
|
|
115
|
+
});
|
|
116
|
+
if (orchestrator.workerCount === 1 && instrumentation) await startHostMetrics(meterProvider);
|
|
117
|
+
} else {
|
|
118
|
+
log?.debug("Plugin installed on worker process");
|
|
119
|
+
if (instrumentation) await startHostMetrics(meterProvider);
|
|
120
|
+
}
|
|
121
|
+
orchestrator.registerOnShutdown(async () => {
|
|
122
|
+
await shutdownProvider();
|
|
123
|
+
});
|
|
124
|
+
},
|
|
125
|
+
async uninstall() {
|
|
126
|
+
clearPrimaryListeners();
|
|
127
|
+
},
|
|
128
|
+
async shutdown() {
|
|
129
|
+
await shutdownProvider();
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
//#endregion
|
|
135
|
+
export { createOtlpMeterPlugin };
|
|
136
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["import cluster from \"node:cluster\";\nimport { randomUUID } from \"node:crypto\";\nimport os from \"node:os\";\nimport { type Logger, type Orchestrator, type ResolvedConfig, withLoggerPrefix } from \"@goopil/clusterkit\";\nimport { metrics } from \"@opentelemetry/api\";\nimport { resourceFromAttributes } from \"@opentelemetry/resources\";\nimport { MeterProvider, PeriodicExportingMetricReader, type PushMetricExporter } from \"@opentelemetry/sdk-metrics\";\nimport { ATTR_SERVICE_INSTANCE_ID, ATTR_SERVICE_NAME } from \"@opentelemetry/semantic-conventions\";\nimport type { OtlpMeterPlugin, OtlpMeterPluginOptions } from \"./types.js\";\n\nexport type { OtlpMeterPlugin, OtlpMeterPluginOptions } from \"./types.js\";\n\ntype PrimaryEvent = \"worker:crash\" | \"worker:restart\" | \"circuit-breaker:tripped\";\n\nconst DEFAULT_HTTP_ENDPOINT = \"http://localhost:4318/v1/metrics\";\nconst DEFAULT_GRPC_ENDPOINT = \"localhost:4317\";\n\nconst ATTR_HOST_NAME = \"host.name\";\nconst ATTR_PROCESS_PID = \"process.pid\";\n\nconst PLUGIN_VERSION = \"0.1.0\";\n\nfunction isMissingModuleError(err: unknown): boolean {\n if (!(err instanceof Error)) return false;\n const msg = err.message;\n return (\n msg.includes(\"Cannot find package\") ||\n msg.includes(\"MODULE_NOT_FOUND\") ||\n msg.includes(\"is not a constructor\") ||\n msg.includes(\"export is defined on the\") ||\n msg.includes(\"is not defined on the\") ||\n \"code\" in err\n );\n}\n\nexport function createOtlpMeterPlugin(options: OtlpMeterPluginOptions = {}): OtlpMeterPlugin {\n const {\n protocol = \"http\",\n instrumentation = true,\n prefix = \"clusterkit.\",\n attributes = {},\n exportIntervalMs = 60_000,\n serviceName = \"clusterkit\",\n endpoint,\n } = options;\n\n if (!Number.isFinite(exportIntervalMs) || exportIntervalMs <= 0) {\n throw new TypeError(\"otlp-meter plugin: exportIntervalMs must be a finite number > 0\");\n }\n\n const resolvedEndpoint = endpoint ?? (protocol === \"grpc\" ? DEFAULT_GRPC_ENDPOINT : DEFAULT_HTTP_ENDPOINT);\n\n if (protocol === \"http\") {\n try {\n new URL(resolvedEndpoint);\n } catch {\n throw new TypeError(`otlp-meter plugin: invalid endpoint URL \"${resolvedEndpoint}\"`);\n }\n }\n\n let meterProvider: MeterProvider | undefined;\n let isShutdown = false;\n let pluginLog: Logger | null = null;\n let primaryOrchestrator: Orchestrator | undefined;\n const primaryListeners: Array<{ event: PrimaryEvent; listener: () => void }> = [];\n\n const clearPrimaryListeners = (): void => {\n if (!primaryOrchestrator) return;\n for (const { event, listener } of primaryListeners) {\n primaryOrchestrator.off(event, listener);\n }\n primaryListeners.length = 0;\n primaryOrchestrator = undefined;\n };\n\n async function createExporter(): Promise<PushMetricExporter> {\n const exporterModuleName =\n protocol === \"grpc\" ? \"@opentelemetry/exporter-metrics-otlp-grpc\" : \"@opentelemetry/exporter-metrics-otlp-http\";\n\n try {\n const mod = await import(exporterModuleName);\n return new mod.OTLPMetricExporter({ url: resolvedEndpoint }) as PushMetricExporter;\n } catch (err) {\n if (isMissingModuleError(err)) {\n throw new Error(\n `otlp-meter plugin: protocol '${protocol}' requires ${exporterModuleName} — install it or use protocol '${protocol === \"grpc\" ? \"http\" : \"grpc\"}'`,\n );\n }\n throw err;\n }\n }\n\n async function startHostMetrics(provider: MeterProvider): Promise<void> {\n try {\n const mod = await import(\"@opentelemetry/host-metrics\");\n const hostMetrics = new mod.HostMetrics({ meterProvider: provider });\n hostMetrics.start();\n } catch (err) {\n if (isMissingModuleError(err)) {\n pluginLog?.warn(\"host-metrics package not installed — skipping process metrics\");\n } else {\n throw err;\n }\n }\n }\n\n async function shutdownProvider(): Promise<void> {\n if (isShutdown || !meterProvider) return;\n isShutdown = true;\n await meterProvider.shutdown();\n meterProvider = undefined;\n }\n\n return {\n name: \"otlp-meter\",\n\n get meterProvider() {\n return meterProvider;\n },\n\n async install(orchestrator: Orchestrator, logger: Logger | null, _config: ResolvedConfig): Promise<void> {\n const log = withLoggerPrefix(logger, \"clusterkit:otlp-meter\");\n pluginLog = log;\n\n const resource = resourceFromAttributes({\n [ATTR_SERVICE_NAME]: serviceName,\n [ATTR_SERVICE_INSTANCE_ID]: randomUUID(),\n [ATTR_HOST_NAME]: os.hostname(),\n [ATTR_PROCESS_PID]: process.pid,\n ...attributes,\n });\n\n const exporter = await createExporter();\n\n const metricReader = new PeriodicExportingMetricReader({\n exporter,\n exportIntervalMillis: exportIntervalMs,\n });\n\n meterProvider = new MeterProvider({\n resource,\n readers: [metricReader],\n });\n\n metrics.setGlobalMeterProvider(meterProvider);\n\n const meter = meterProvider.getMeter(\"@goopil/clusterkit\", PLUGIN_VERSION);\n\n if (cluster.isPrimary) {\n clearPrimaryListeners();\n log?.debug(\"Plugin installed on primary process\");\n primaryOrchestrator = orchestrator;\n\n const activeWorkersGauge = meter.createObservableGauge(`${prefix}active_workers`, {\n description: \"Number of active cluster workers\",\n });\n activeWorkersGauge.addCallback((result) => {\n result.observe(orchestrator.getMetrics().activeWorkers);\n });\n\n const workerRestartsCounter = meter.createCounter(`${prefix}worker.restarts`, {\n description: \"Total number of worker restarts\",\n });\n const workerCrashesCounter = meter.createCounter(`${prefix}worker.crashes`, {\n description: \"Total number of worker crashes\",\n });\n const circuitBreakerTripsCounter = meter.createCounter(`${prefix}circuit_breaker.trips`, {\n description: \"Total number of circuit breaker trips\",\n });\n\n const bind = (event: PrimaryEvent, listener: () => void): void => {\n orchestrator.on(event, listener);\n primaryListeners.push({ event, listener });\n };\n\n bind(\"worker:crash\", () => {\n workerCrashesCounter.add(1);\n });\n bind(\"worker:restart\", () => {\n workerRestartsCounter.add(1);\n });\n bind(\"circuit-breaker:tripped\", () => {\n circuitBreakerTripsCounter.add(1);\n });\n\n const singleWorker = orchestrator.workerCount === 1;\n if (singleWorker && instrumentation) {\n await startHostMetrics(meterProvider);\n }\n } else {\n log?.debug(\"Plugin installed on worker process\");\n\n if (instrumentation) {\n await startHostMetrics(meterProvider);\n }\n }\n\n orchestrator.registerOnShutdown(async () => {\n await shutdownProvider();\n });\n },\n\n async uninstall(): Promise<void> {\n clearPrimaryListeners();\n },\n\n async shutdown(): Promise<void> {\n await shutdownProvider();\n },\n };\n}\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@goopil/clusterkit-otlp-meter",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "OpenTelemetry OTLP metrics plugin for @goopil/clusterkit: exports orchestration counters/gauges and per-worker host metrics via OTLP/HTTP or OTLP/gRPC to a collector.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"opentelemetry",
|
|
7
|
+
"otlp",
|
|
8
|
+
"metrics",
|
|
9
|
+
"cluster",
|
|
10
|
+
"clusterkit",
|
|
11
|
+
"monitoring",
|
|
12
|
+
"observability"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/Goopil/clusterkit#readme",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/Goopil/clusterkit/issues"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/Goopil/clusterkit.git",
|
|
21
|
+
"directory": "packages/plugin-otlp-meter"
|
|
22
|
+
},
|
|
23
|
+
"license": "LGPL-3.0-or-later",
|
|
24
|
+
"type": "module",
|
|
25
|
+
"main": "./dist/index.cjs",
|
|
26
|
+
"module": "./dist/index.mjs",
|
|
27
|
+
"types": "./dist/index.d.mts",
|
|
28
|
+
"sideEffects": false,
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=22.12.0"
|
|
31
|
+
},
|
|
32
|
+
"exports": {
|
|
33
|
+
".": {
|
|
34
|
+
"import": {
|
|
35
|
+
"types": "./dist/index.d.mts",
|
|
36
|
+
"default": "./dist/index.mjs"
|
|
37
|
+
},
|
|
38
|
+
"require": {
|
|
39
|
+
"types": "./dist/index.d.cts",
|
|
40
|
+
"default": "./dist/index.cjs"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"files": [
|
|
45
|
+
"dist"
|
|
46
|
+
],
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"@opentelemetry/api": "^1.9.0",
|
|
52
|
+
"@opentelemetry/resources": "^2.0.0",
|
|
53
|
+
"@opentelemetry/semantic-conventions": "^1.30.0",
|
|
54
|
+
"@opentelemetry/sdk-metrics": "^2.0.0",
|
|
55
|
+
"@goopil/clusterkit": "1.0.3"
|
|
56
|
+
},
|
|
57
|
+
"peerDependenciesMeta": {
|
|
58
|
+
"@opentelemetry/exporter-metrics-otlp-http": {
|
|
59
|
+
"optional": true
|
|
60
|
+
},
|
|
61
|
+
"@opentelemetry/exporter-metrics-otlp-grpc": {
|
|
62
|
+
"optional": true
|
|
63
|
+
},
|
|
64
|
+
"@opentelemetry/host-metrics": {
|
|
65
|
+
"optional": true
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"devDependencies": {
|
|
69
|
+
"@opentelemetry/api": "^1.9.1",
|
|
70
|
+
"@opentelemetry/exporter-metrics-otlp-http": "^0.221.0",
|
|
71
|
+
"@opentelemetry/exporter-metrics-otlp-grpc": "^0.221.0",
|
|
72
|
+
"@opentelemetry/host-metrics": "^0.39.0",
|
|
73
|
+
"@opentelemetry/otlp-transformer": "^0.221.0",
|
|
74
|
+
"@opentelemetry/resources": "^2.10.0",
|
|
75
|
+
"@opentelemetry/sdk-metrics": "^2.10.0",
|
|
76
|
+
"@opentelemetry/semantic-conventions": "^1.43.0",
|
|
77
|
+
"@types/node": "^26.2.0",
|
|
78
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
79
|
+
"tsdown": "^0.22.14",
|
|
80
|
+
"typescript": "^7.0.2",
|
|
81
|
+
"vitest": "^4.1.10",
|
|
82
|
+
"@goopil/clusterkit": "1.0.3"
|
|
83
|
+
},
|
|
84
|
+
"scripts": {
|
|
85
|
+
"build": "tsdown",
|
|
86
|
+
"dev": "tsdown --watch",
|
|
87
|
+
"test": "vitest run",
|
|
88
|
+
"test:watch": "vitest",
|
|
89
|
+
"test:coverage": "vitest run --coverage",
|
|
90
|
+
"clean": "rm -rf dist coverage"
|
|
91
|
+
}
|
|
92
|
+
}
|