@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/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":"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"}
@@ -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"}
@@ -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
+ }