@goopil/clusterkit-otlp-meter 1.3.0 → 1.4.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 +3 -4
- package/dist/index.cjs +1 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +1 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ transports.
|
|
|
14
14
|
| Host/process metrics | Optional Node.js process metrics (CPU, memory, GC, event loop) via `@opentelemetry/host-metrics` |
|
|
15
15
|
| OTLP/HTTP export | Push metrics to an OTLP/HTTP collector endpoint (default) |
|
|
16
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
|
|
17
|
+
| Primary/worker-aware behavior | Event listeners only on primary, host metrics on workers |
|
|
18
18
|
|
|
19
19
|
## Installation
|
|
20
20
|
|
|
@@ -126,9 +126,8 @@ Worker-sourced series (`worker.rss_bytes`, `worker.heap_used_bytes`, `worker.eve
|
|
|
126
126
|
`worker.heartbeat_age_seconds`) only appear when core health monitoring is enabled and
|
|
127
127
|
heartbeats flow; the fleet gauges and event counters report regardless.
|
|
128
128
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
there are no worker processes to collect from.
|
|
129
|
+
At `workers: 1`, the app runs in a forked worker like any other count; worker-sourced
|
|
130
|
+
series appear as soon as heartbeats flow.
|
|
132
131
|
|
|
133
132
|
## Security / exposure notes
|
|
134
133
|
|
package/dist/index.cjs
CHANGED
|
@@ -38,7 +38,7 @@ let _opentelemetry_sdk_metrics = require("@opentelemetry/sdk-metrics");
|
|
|
38
38
|
let _opentelemetry_semantic_conventions = require("@opentelemetry/semantic-conventions");
|
|
39
39
|
|
|
40
40
|
//#region package.json
|
|
41
|
-
var version = "1.
|
|
41
|
+
var version = "1.4.0";
|
|
42
42
|
|
|
43
43
|
//#endregion
|
|
44
44
|
//#region src/index.ts
|
|
@@ -225,7 +225,6 @@ function createOtlpMeterPlugin(options = {}) {
|
|
|
225
225
|
bind("fleet:recovered", ({ degradedDurationMs }) => {
|
|
226
226
|
recoveryDurationGauge.record(degradedDurationMs / 1e3);
|
|
227
227
|
});
|
|
228
|
-
if (orchestrator.workerCount === 1 && instrumentation) await startHostMetrics(meterProvider);
|
|
229
228
|
} else {
|
|
230
229
|
log?.debug("Plugin installed on worker process");
|
|
231
230
|
if (instrumentation) await startHostMetrics(meterProvider);
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["pkgJson.version","withLoggerPrefix","resourceFromAttributes","ATTR_SERVICE_NAME","ATTR_SERVICE_INSTANCE_ID","randomUUID","os","PeriodicExportingMetricReader","MeterProvider","metrics","cluster"],"sources":["../package.json","../src/index.ts"],"sourcesContent":["","import cluster from \"node:cluster\";\nimport { randomUUID } from \"node:crypto\";\nimport os from \"node:os\";\nimport {\n type Logger,\n type Orchestrator,\n type OrchestratorEvents,\n type ResolvedConfig,\n withLoggerPrefix,\n} from \"@goopil/clusterkit\";\nimport { metrics, type ObservableResult } 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 pkgJson from \"../package.json\" with { type: \"json\" };\nimport type { OtlpMeterPlugin, OtlpMeterPluginOptions } from \"./types.js\";\n\nexport type { OtlpMeterPlugin, OtlpMeterPluginOptions } from \"./types.js\";\n\ntype PrimaryEvent =\n | \"worker:crash\"\n | \"worker:restart\"\n | \"circuit-breaker:tripped\"\n | \"worker:health\"\n | \"worker:exit\"\n | \"worker:recycle\"\n | \"worker:wedged\"\n | \"fleet:recovered\";\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: string = pkgJson.version;\n\nfunction isMissingModuleError(err: unknown): boolean {\n const code = (err as NodeJS.ErrnoException | undefined)?.code;\n return code === \"ERR_MODULE_NOT_FOUND\" || code === \"MODULE_NOT_FOUND\";\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 headers,\n } = options;\n\n if (!Number.isFinite(exportIntervalMs) || exportIntervalMs < 1_000) {\n throw new TypeError(\n \"otlp-meter plugin: exportIntervalMs must be a finite number >= 1000 (minimum 1s to avoid flooding the collector)\",\n );\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 pluginSetGlobalProvider = false;\n let pluginLog: Logger | null = null;\n let primaryOrchestrator: Orchestrator | undefined;\n interface WorkerHealthSample {\n pid: number;\n rss: number;\n heapUsed: number;\n eventLoopLagMs: number;\n lastBeatAt: number;\n }\n\n // Listeners stored payload-agnostic; the concrete payload type is inferred at\n // the bind() call site via OrchestratorEvents[E].\n const primaryListeners: Array<{ event: PrimaryEvent; listener: (...args: never[]) => void }> = [];\n const workerHealth = new Map<number, WorkerHealthSample>();\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 // The gRPC exporter config omits `headers` (gRPC uses `metadata` instead):\n // forwarding them would be silently dropped, leaving exports unauthenticated.\n if (protocol === \"grpc\" && headers && Object.keys(headers).length > 0) {\n pluginLog?.warn(\n \"headers are not supported by the gRPC exporter — configure metadata via the exporter's own options or use protocol: 'http'\",\n );\n }\n\n try {\n const mod = await import(exporterModuleName);\n const config = protocol === \"grpc\" ? { url: resolvedEndpoint } : { url: resolvedEndpoint, headers };\n return new mod.OTLPMetricExporter(config) 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 // Reinstalling the same instance after a shutdown must not orphan the old\n // provider or leave the latch stuck at `true` (the new provider's shutdown\n // would otherwise be a permanent no-op).\n if (meterProvider) await shutdownProvider();\n isShutdown = false;\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 // setGlobalMeterProvider() refuses to override an existing registration\n // and returns false — never clobber the app's own OTel setup.\n if (metrics.setGlobalMeterProvider(meterProvider)) {\n pluginSetGlobalProvider = true;\n } else {\n log?.warn(\n \"A global OpenTelemetry meter provider is already registered — leaving it untouched; clusterkit meters use the plugin's own provider\",\n );\n }\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 workerRssGauge = meter.createObservableGauge(`${prefix}worker.rss_bytes`, {\n description: \"Resident set size per worker from health heartbeats\",\n unit: \"By\",\n });\n const workerHeapGauge = meter.createObservableGauge(`${prefix}worker.heap_used_bytes`, {\n description: \"V8 heap used per worker from health heartbeats\",\n unit: \"By\",\n });\n const workerLagGauge = meter.createObservableGauge(`${prefix}worker.eventloop_lag_ms`, {\n description: \"Event loop lag per worker from health heartbeats\",\n unit: \"ms\",\n });\n const workerHeartbeatAgeGauge = meter.createObservableGauge(`${prefix}worker.heartbeat_age_seconds`, {\n description: \"Seconds since the last health heartbeat per worker\",\n unit: \"s\",\n });\n\n const observeWorkerHealth = (\n result: ObservableResult<number>,\n pick: (sample: WorkerHealthSample) => number,\n ): void => {\n for (const [workerId, sample] of workerHealth) {\n result.observe(pick(sample), { \"worker.id\": workerId, \"process.pid\": sample.pid });\n }\n };\n\n workerRssGauge.addCallback((result) => observeWorkerHealth(result, (s) => s.rss));\n workerHeapGauge.addCallback((result) => observeWorkerHealth(result, (s) => s.heapUsed));\n workerLagGauge.addCallback((result) => observeWorkerHealth(result, (s) => s.eventLoopLagMs));\n workerHeartbeatAgeGauge.addCallback((result) => {\n const now = Date.now();\n observeWorkerHealth(result, (s) => Math.max(0, (now - s.lastBeatAt) / 1000));\n });\n\n const workerRecyclesCounter = meter.createCounter(`${prefix}worker.recycles`, {\n description: \"Total number of worker recycles by reason\",\n });\n const wedgedKillsCounter = meter.createCounter(`${prefix}worker.wedged.kills`, {\n description: \"Total number of workers killed for being unresponsive\",\n });\n const recoveryDurationGauge = meter.createGauge(`${prefix}recovery.duration_seconds`, {\n description: \"Duration of the last fleet degraded-to-recovered cycle\",\n unit: \"s\",\n });\n\n const fleetTargetGauge = meter.createObservableGauge(`${prefix}fleet.target_workers`, {\n description: \"Target worker count (live fleet health)\",\n });\n const fleetActiveGauge = meter.createObservableGauge(`${prefix}fleet.active_workers`, {\n description: \"Currently active workers (live fleet health)\",\n });\n const fleetQuarantinedGauge = meter.createObservableGauge(`${prefix}fleet.quarantined_slots`, {\n description: \"Quarantined worker slots (live fleet health)\",\n });\n\n fleetTargetGauge.addCallback((result) => {\n result.observe(orchestrator.getFleetHealth().target);\n });\n fleetActiveGauge.addCallback((result) => {\n result.observe(orchestrator.getFleetHealth().active);\n });\n fleetQuarantinedGauge.addCallback((result) => {\n result.observe(orchestrator.getFleetHealth().quarantined);\n });\n\n const bind = <E extends PrimaryEvent>(event: E, listener: (...args: OrchestratorEvents[E]) => 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 bind(\"worker:health\", ({ workerId, pid, rss, heapUsed, eventLoopLagMs }) => {\n workerHealth.set(workerId, { pid, rss, heapUsed, eventLoopLagMs, lastBeatAt: Date.now() });\n });\n bind(\"worker:exit\", ({ workerId }) => {\n workerHealth.delete(workerId);\n });\n\n bind(\"worker:recycle\", ({ reason }) => {\n workerRecyclesCounter.add(1, { reason });\n });\n bind(\"worker:wedged\", () => {\n wedgedKillsCounter.add(1);\n });\n bind(\"fleet:recovered\", ({ degradedDurationMs }) => {\n recoveryDurationGauge.record(degradedDurationMs / 1000);\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 workerHealth.clear();\n // If the plugin's provider became the global one, release the slot:\n // metrics.disable() is the API's public unregister and restores the exact\n // pre-install state (getMeterProvider() falls back to the noop provider).\n // Restoring the captured noop prior via setGlobalMeterProvider() instead\n // would leave the slot occupied and block the app from registering later.\n if (pluginSetGlobalProvider) {\n metrics.disable();\n pluginSetGlobalProvider = false;\n }\n await shutdownProvider();\n },\n\n async shutdown(): Promise<void> {\n await shutdownProvider();\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["pkgJson.version","withLoggerPrefix","resourceFromAttributes","ATTR_SERVICE_NAME","ATTR_SERVICE_INSTANCE_ID","randomUUID","os","PeriodicExportingMetricReader","MeterProvider","metrics","cluster"],"sources":["../package.json","../src/index.ts"],"sourcesContent":["","import cluster from \"node:cluster\";\nimport { randomUUID } from \"node:crypto\";\nimport os from \"node:os\";\nimport {\n type Logger,\n type Orchestrator,\n type OrchestratorEvents,\n type ResolvedConfig,\n withLoggerPrefix,\n} from \"@goopil/clusterkit\";\nimport { metrics, type ObservableResult } 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 pkgJson from \"../package.json\" with { type: \"json\" };\nimport type { OtlpMeterPlugin, OtlpMeterPluginOptions } from \"./types.js\";\n\nexport type { OtlpMeterPlugin, OtlpMeterPluginOptions } from \"./types.js\";\n\ntype PrimaryEvent =\n | \"worker:crash\"\n | \"worker:restart\"\n | \"circuit-breaker:tripped\"\n | \"worker:health\"\n | \"worker:exit\"\n | \"worker:recycle\"\n | \"worker:wedged\"\n | \"fleet:recovered\";\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: string = pkgJson.version;\n\nfunction isMissingModuleError(err: unknown): boolean {\n const code = (err as NodeJS.ErrnoException | undefined)?.code;\n return code === \"ERR_MODULE_NOT_FOUND\" || code === \"MODULE_NOT_FOUND\";\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 headers,\n } = options;\n\n if (!Number.isFinite(exportIntervalMs) || exportIntervalMs < 1_000) {\n throw new TypeError(\n \"otlp-meter plugin: exportIntervalMs must be a finite number >= 1000 (minimum 1s to avoid flooding the collector)\",\n );\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 pluginSetGlobalProvider = false;\n let pluginLog: Logger | null = null;\n let primaryOrchestrator: Orchestrator | undefined;\n interface WorkerHealthSample {\n pid: number;\n rss: number;\n heapUsed: number;\n eventLoopLagMs: number;\n lastBeatAt: number;\n }\n\n // Listeners stored payload-agnostic; the concrete payload type is inferred at\n // the bind() call site via OrchestratorEvents[E].\n const primaryListeners: Array<{ event: PrimaryEvent; listener: (...args: never[]) => void }> = [];\n const workerHealth = new Map<number, WorkerHealthSample>();\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 // The gRPC exporter config omits `headers` (gRPC uses `metadata` instead):\n // forwarding them would be silently dropped, leaving exports unauthenticated.\n if (protocol === \"grpc\" && headers && Object.keys(headers).length > 0) {\n pluginLog?.warn(\n \"headers are not supported by the gRPC exporter — configure metadata via the exporter's own options or use protocol: 'http'\",\n );\n }\n\n try {\n const mod = await import(exporterModuleName);\n const config = protocol === \"grpc\" ? { url: resolvedEndpoint } : { url: resolvedEndpoint, headers };\n return new mod.OTLPMetricExporter(config) 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 // Reinstalling the same instance after a shutdown must not orphan the old\n // provider or leave the latch stuck at `true` (the new provider's shutdown\n // would otherwise be a permanent no-op).\n if (meterProvider) await shutdownProvider();\n isShutdown = false;\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 // setGlobalMeterProvider() refuses to override an existing registration\n // and returns false — never clobber the app's own OTel setup.\n if (metrics.setGlobalMeterProvider(meterProvider)) {\n pluginSetGlobalProvider = true;\n } else {\n log?.warn(\n \"A global OpenTelemetry meter provider is already registered — leaving it untouched; clusterkit meters use the plugin's own provider\",\n );\n }\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 workerRssGauge = meter.createObservableGauge(`${prefix}worker.rss_bytes`, {\n description: \"Resident set size per worker from health heartbeats\",\n unit: \"By\",\n });\n const workerHeapGauge = meter.createObservableGauge(`${prefix}worker.heap_used_bytes`, {\n description: \"V8 heap used per worker from health heartbeats\",\n unit: \"By\",\n });\n const workerLagGauge = meter.createObservableGauge(`${prefix}worker.eventloop_lag_ms`, {\n description: \"Event loop lag per worker from health heartbeats\",\n unit: \"ms\",\n });\n const workerHeartbeatAgeGauge = meter.createObservableGauge(`${prefix}worker.heartbeat_age_seconds`, {\n description: \"Seconds since the last health heartbeat per worker\",\n unit: \"s\",\n });\n\n const observeWorkerHealth = (\n result: ObservableResult<number>,\n pick: (sample: WorkerHealthSample) => number,\n ): void => {\n for (const [workerId, sample] of workerHealth) {\n result.observe(pick(sample), { \"worker.id\": workerId, \"process.pid\": sample.pid });\n }\n };\n\n workerRssGauge.addCallback((result) => observeWorkerHealth(result, (s) => s.rss));\n workerHeapGauge.addCallback((result) => observeWorkerHealth(result, (s) => s.heapUsed));\n workerLagGauge.addCallback((result) => observeWorkerHealth(result, (s) => s.eventLoopLagMs));\n workerHeartbeatAgeGauge.addCallback((result) => {\n const now = Date.now();\n observeWorkerHealth(result, (s) => Math.max(0, (now - s.lastBeatAt) / 1000));\n });\n\n const workerRecyclesCounter = meter.createCounter(`${prefix}worker.recycles`, {\n description: \"Total number of worker recycles by reason\",\n });\n const wedgedKillsCounter = meter.createCounter(`${prefix}worker.wedged.kills`, {\n description: \"Total number of workers killed for being unresponsive\",\n });\n const recoveryDurationGauge = meter.createGauge(`${prefix}recovery.duration_seconds`, {\n description: \"Duration of the last fleet degraded-to-recovered cycle\",\n unit: \"s\",\n });\n\n const fleetTargetGauge = meter.createObservableGauge(`${prefix}fleet.target_workers`, {\n description: \"Target worker count (live fleet health)\",\n });\n const fleetActiveGauge = meter.createObservableGauge(`${prefix}fleet.active_workers`, {\n description: \"Currently active workers (live fleet health)\",\n });\n const fleetQuarantinedGauge = meter.createObservableGauge(`${prefix}fleet.quarantined_slots`, {\n description: \"Quarantined worker slots (live fleet health)\",\n });\n\n fleetTargetGauge.addCallback((result) => {\n result.observe(orchestrator.getFleetHealth().target);\n });\n fleetActiveGauge.addCallback((result) => {\n result.observe(orchestrator.getFleetHealth().active);\n });\n fleetQuarantinedGauge.addCallback((result) => {\n result.observe(orchestrator.getFleetHealth().quarantined);\n });\n\n const bind = <E extends PrimaryEvent>(event: E, listener: (...args: OrchestratorEvents[E]) => 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 bind(\"worker:health\", ({ workerId, pid, rss, heapUsed, eventLoopLagMs }) => {\n workerHealth.set(workerId, { pid, rss, heapUsed, eventLoopLagMs, lastBeatAt: Date.now() });\n });\n bind(\"worker:exit\", ({ workerId }) => {\n workerHealth.delete(workerId);\n });\n\n bind(\"worker:recycle\", ({ reason }) => {\n workerRecyclesCounter.add(1, { reason });\n });\n bind(\"worker:wedged\", () => {\n wedgedKillsCounter.add(1);\n });\n bind(\"fleet:recovered\", ({ degradedDurationMs }) => {\n recoveryDurationGauge.record(degradedDurationMs / 1000);\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 workerHealth.clear();\n // If the plugin's provider became the global one, release the slot:\n // metrics.disable() is the API's public unregister and restores the exact\n // pre-install state (getMeterProvider() falls back to the noop provider).\n // Restoring the captured noop prior via setGlobalMeterProvider() instead\n // would leave the slot occupied and block the app from registering later.\n if (pluginSetGlobalProvider) {\n metrics.disable();\n pluginSetGlobalProvider = false;\n }\n await shutdownProvider();\n },\n\n async shutdown(): Promise<void> {\n await shutdownProvider();\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC6BA,MAAM,wBAAwB;AAC9B,MAAM,wBAAwB;AAE9B,MAAM,iBAAiB;AACvB,MAAM,mBAAmB;AAEzB,MAAM,iBAAyBA;AAE/B,SAAS,qBAAqB,KAAuB;CACnD,MAAM,OAAQ,KAA2C;CACzD,OAAO,SAAS,0BAA0B,SAAS;AACrD;AAEA,SAAgB,sBAAsB,UAAkC,CAAC,GAAoB;CAC3F,MAAM,EACJ,WAAW,QACX,kBAAkB,MAClB,SAAS,eACT,aAAa,CAAC,GACd,mBAAmB,KACnB,cAAc,cACd,UACA,YACE;CAEJ,IAAI,CAAC,OAAO,SAAS,gBAAgB,KAAK,mBAAmB,KAC3D,MAAM,IAAI,UACR,kHACF;CAGF,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,0BAA0B;CAC9B,IAAI,YAA2B;CAC/B,IAAI;CAWJ,MAAM,mBAAyF,CAAC;CAChG,MAAM,+BAAe,IAAI,IAAgC;CAEzD,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;EAItE,IAAI,aAAa,UAAU,WAAW,OAAO,KAAK,OAAO,CAAC,CAAC,SAAS,GAClE,WAAW,KACT,4HACF;EAGF,IAAI;GACF,MAAM,MAAM,MAAM,OAAO;GACzB,MAAM,SAAS,aAAa,SAAS,EAAE,KAAK,iBAAiB,IAAI;IAAE,KAAK;IAAkB;GAAQ;GAClG,OAAO,IAAI,IAAI,mBAAmB,MAAM;EAC1C,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,UAAMC,qCAAiB,QAAQ,uBAAuB;GAC5D,YAAY;GAKZ,IAAI,eAAe,MAAM,iBAAiB;GAC1C,aAAa;GAEb,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;GAID,IAAIC,2BAAQ,uBAAuB,aAAa,GAC9C,0BAA0B;QAE1B,KAAK,KACH,qIACF;GAGF,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,iBAAiB,MAAM,sBAAsB,GAAG,OAAO,mBAAmB;KAC9E,aAAa;KACb,MAAM;IACR,CAAC;IACD,MAAM,kBAAkB,MAAM,sBAAsB,GAAG,OAAO,yBAAyB;KACrF,aAAa;KACb,MAAM;IACR,CAAC;IACD,MAAM,iBAAiB,MAAM,sBAAsB,GAAG,OAAO,0BAA0B;KACrF,aAAa;KACb,MAAM;IACR,CAAC;IACD,MAAM,0BAA0B,MAAM,sBAAsB,GAAG,OAAO,+BAA+B;KACnG,aAAa;KACb,MAAM;IACR,CAAC;IAED,MAAM,uBACJ,QACA,SACS;KACT,KAAK,MAAM,CAAC,UAAU,WAAW,cAC/B,OAAO,QAAQ,KAAK,MAAM,GAAG;MAAE,aAAa;MAAU,eAAe,OAAO;KAAI,CAAC;IAErF;IAEA,eAAe,aAAa,WAAW,oBAAoB,SAAS,MAAM,EAAE,GAAG,CAAC;IAChF,gBAAgB,aAAa,WAAW,oBAAoB,SAAS,MAAM,EAAE,QAAQ,CAAC;IACtF,eAAe,aAAa,WAAW,oBAAoB,SAAS,MAAM,EAAE,cAAc,CAAC;IAC3F,wBAAwB,aAAa,WAAW;KAC9C,MAAM,MAAM,KAAK,IAAI;KACrB,oBAAoB,SAAS,MAAM,KAAK,IAAI,IAAI,MAAM,EAAE,cAAc,GAAI,CAAC;IAC7E,CAAC;IAED,MAAM,wBAAwB,MAAM,cAAc,GAAG,OAAO,kBAAkB,EAC5E,aAAa,4CACf,CAAC;IACD,MAAM,qBAAqB,MAAM,cAAc,GAAG,OAAO,sBAAsB,EAC7E,aAAa,wDACf,CAAC;IACD,MAAM,wBAAwB,MAAM,YAAY,GAAG,OAAO,4BAA4B;KACpF,aAAa;KACb,MAAM;IACR,CAAC;IAED,MAAM,mBAAmB,MAAM,sBAAsB,GAAG,OAAO,uBAAuB,EACpF,aAAa,0CACf,CAAC;IACD,MAAM,mBAAmB,MAAM,sBAAsB,GAAG,OAAO,uBAAuB,EACpF,aAAa,+CACf,CAAC;IACD,MAAM,wBAAwB,MAAM,sBAAsB,GAAG,OAAO,0BAA0B,EAC5F,aAAa,+CACf,CAAC;IAED,iBAAiB,aAAa,WAAW;KACvC,OAAO,QAAQ,aAAa,eAAe,CAAC,CAAC,MAAM;IACrD,CAAC;IACD,iBAAiB,aAAa,WAAW;KACvC,OAAO,QAAQ,aAAa,eAAe,CAAC,CAAC,MAAM;IACrD,CAAC;IACD,sBAAsB,aAAa,WAAW;KAC5C,OAAO,QAAQ,aAAa,eAAe,CAAC,CAAC,WAAW;IAC1D,CAAC;IAED,MAAM,QAAgC,OAAU,aAA6D;KAC3G,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;IAED,KAAK,kBAAkB,EAAE,UAAU,KAAK,KAAK,UAAU,qBAAqB;KAC1E,aAAa,IAAI,UAAU;MAAE;MAAK;MAAK;MAAU;MAAgB,YAAY,KAAK,IAAI;KAAE,CAAC;IAC3F,CAAC;IACD,KAAK,gBAAgB,EAAE,eAAe;KACpC,aAAa,OAAO,QAAQ;IAC9B,CAAC;IAED,KAAK,mBAAmB,EAAE,aAAa;KACrC,sBAAsB,IAAI,GAAG,EAAE,OAAO,CAAC;IACzC,CAAC;IACD,KAAK,uBAAuB;KAC1B,mBAAmB,IAAI,CAAC;IAC1B,CAAC;IACD,KAAK,oBAAoB,EAAE,yBAAyB;KAClD,sBAAsB,OAAO,qBAAqB,GAAI;IACxD,CAAC;GACH,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;GACtB,aAAa,MAAM;GAMnB,IAAI,yBAAyB;IAC3B,2BAAQ,QAAQ;IAChB,0BAA0B;GAC5B;GACA,MAAM,iBAAiB;EACzB;EAEA,MAAM,WAA0B;GAC9B,MAAM,iBAAiB;EACzB;CACF;AACF"}
|
package/dist/index.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import { MeterProvider, PeriodicExportingMetricReader } from "@opentelemetry/sdk
|
|
|
8
8
|
import { ATTR_SERVICE_INSTANCE_ID, ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
|
|
9
9
|
|
|
10
10
|
//#region package.json
|
|
11
|
-
var version = "1.
|
|
11
|
+
var version = "1.4.0";
|
|
12
12
|
|
|
13
13
|
//#endregion
|
|
14
14
|
//#region src/index.ts
|
|
@@ -195,7 +195,6 @@ function createOtlpMeterPlugin(options = {}) {
|
|
|
195
195
|
bind("fleet:recovered", ({ degradedDurationMs }) => {
|
|
196
196
|
recoveryDurationGauge.record(degradedDurationMs / 1e3);
|
|
197
197
|
});
|
|
198
|
-
if (orchestrator.workerCount === 1 && instrumentation) await startHostMetrics(meterProvider);
|
|
199
198
|
} else {
|
|
200
199
|
log?.debug("Plugin installed on worker process");
|
|
201
200
|
if (instrumentation) await startHostMetrics(meterProvider);
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["pkgJson.version"],"sources":["../package.json","../src/index.ts"],"sourcesContent":["","import cluster from \"node:cluster\";\nimport { randomUUID } from \"node:crypto\";\nimport os from \"node:os\";\nimport {\n type Logger,\n type Orchestrator,\n type OrchestratorEvents,\n type ResolvedConfig,\n withLoggerPrefix,\n} from \"@goopil/clusterkit\";\nimport { metrics, type ObservableResult } 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 pkgJson from \"../package.json\" with { type: \"json\" };\nimport type { OtlpMeterPlugin, OtlpMeterPluginOptions } from \"./types.js\";\n\nexport type { OtlpMeterPlugin, OtlpMeterPluginOptions } from \"./types.js\";\n\ntype PrimaryEvent =\n | \"worker:crash\"\n | \"worker:restart\"\n | \"circuit-breaker:tripped\"\n | \"worker:health\"\n | \"worker:exit\"\n | \"worker:recycle\"\n | \"worker:wedged\"\n | \"fleet:recovered\";\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: string = pkgJson.version;\n\nfunction isMissingModuleError(err: unknown): boolean {\n const code = (err as NodeJS.ErrnoException | undefined)?.code;\n return code === \"ERR_MODULE_NOT_FOUND\" || code === \"MODULE_NOT_FOUND\";\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 headers,\n } = options;\n\n if (!Number.isFinite(exportIntervalMs) || exportIntervalMs < 1_000) {\n throw new TypeError(\n \"otlp-meter plugin: exportIntervalMs must be a finite number >= 1000 (minimum 1s to avoid flooding the collector)\",\n );\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 pluginSetGlobalProvider = false;\n let pluginLog: Logger | null = null;\n let primaryOrchestrator: Orchestrator | undefined;\n interface WorkerHealthSample {\n pid: number;\n rss: number;\n heapUsed: number;\n eventLoopLagMs: number;\n lastBeatAt: number;\n }\n\n // Listeners stored payload-agnostic; the concrete payload type is inferred at\n // the bind() call site via OrchestratorEvents[E].\n const primaryListeners: Array<{ event: PrimaryEvent; listener: (...args: never[]) => void }> = [];\n const workerHealth = new Map<number, WorkerHealthSample>();\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 // The gRPC exporter config omits `headers` (gRPC uses `metadata` instead):\n // forwarding them would be silently dropped, leaving exports unauthenticated.\n if (protocol === \"grpc\" && headers && Object.keys(headers).length > 0) {\n pluginLog?.warn(\n \"headers are not supported by the gRPC exporter — configure metadata via the exporter's own options or use protocol: 'http'\",\n );\n }\n\n try {\n const mod = await import(exporterModuleName);\n const config = protocol === \"grpc\" ? { url: resolvedEndpoint } : { url: resolvedEndpoint, headers };\n return new mod.OTLPMetricExporter(config) 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 // Reinstalling the same instance after a shutdown must not orphan the old\n // provider or leave the latch stuck at `true` (the new provider's shutdown\n // would otherwise be a permanent no-op).\n if (meterProvider) await shutdownProvider();\n isShutdown = false;\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 // setGlobalMeterProvider() refuses to override an existing registration\n // and returns false — never clobber the app's own OTel setup.\n if (metrics.setGlobalMeterProvider(meterProvider)) {\n pluginSetGlobalProvider = true;\n } else {\n log?.warn(\n \"A global OpenTelemetry meter provider is already registered — leaving it untouched; clusterkit meters use the plugin's own provider\",\n );\n }\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 workerRssGauge = meter.createObservableGauge(`${prefix}worker.rss_bytes`, {\n description: \"Resident set size per worker from health heartbeats\",\n unit: \"By\",\n });\n const workerHeapGauge = meter.createObservableGauge(`${prefix}worker.heap_used_bytes`, {\n description: \"V8 heap used per worker from health heartbeats\",\n unit: \"By\",\n });\n const workerLagGauge = meter.createObservableGauge(`${prefix}worker.eventloop_lag_ms`, {\n description: \"Event loop lag per worker from health heartbeats\",\n unit: \"ms\",\n });\n const workerHeartbeatAgeGauge = meter.createObservableGauge(`${prefix}worker.heartbeat_age_seconds`, {\n description: \"Seconds since the last health heartbeat per worker\",\n unit: \"s\",\n });\n\n const observeWorkerHealth = (\n result: ObservableResult<number>,\n pick: (sample: WorkerHealthSample) => number,\n ): void => {\n for (const [workerId, sample] of workerHealth) {\n result.observe(pick(sample), { \"worker.id\": workerId, \"process.pid\": sample.pid });\n }\n };\n\n workerRssGauge.addCallback((result) => observeWorkerHealth(result, (s) => s.rss));\n workerHeapGauge.addCallback((result) => observeWorkerHealth(result, (s) => s.heapUsed));\n workerLagGauge.addCallback((result) => observeWorkerHealth(result, (s) => s.eventLoopLagMs));\n workerHeartbeatAgeGauge.addCallback((result) => {\n const now = Date.now();\n observeWorkerHealth(result, (s) => Math.max(0, (now - s.lastBeatAt) / 1000));\n });\n\n const workerRecyclesCounter = meter.createCounter(`${prefix}worker.recycles`, {\n description: \"Total number of worker recycles by reason\",\n });\n const wedgedKillsCounter = meter.createCounter(`${prefix}worker.wedged.kills`, {\n description: \"Total number of workers killed for being unresponsive\",\n });\n const recoveryDurationGauge = meter.createGauge(`${prefix}recovery.duration_seconds`, {\n description: \"Duration of the last fleet degraded-to-recovered cycle\",\n unit: \"s\",\n });\n\n const fleetTargetGauge = meter.createObservableGauge(`${prefix}fleet.target_workers`, {\n description: \"Target worker count (live fleet health)\",\n });\n const fleetActiveGauge = meter.createObservableGauge(`${prefix}fleet.active_workers`, {\n description: \"Currently active workers (live fleet health)\",\n });\n const fleetQuarantinedGauge = meter.createObservableGauge(`${prefix}fleet.quarantined_slots`, {\n description: \"Quarantined worker slots (live fleet health)\",\n });\n\n fleetTargetGauge.addCallback((result) => {\n result.observe(orchestrator.getFleetHealth().target);\n });\n fleetActiveGauge.addCallback((result) => {\n result.observe(orchestrator.getFleetHealth().active);\n });\n fleetQuarantinedGauge.addCallback((result) => {\n result.observe(orchestrator.getFleetHealth().quarantined);\n });\n\n const bind = <E extends PrimaryEvent>(event: E, listener: (...args: OrchestratorEvents[E]) => 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 bind(\"worker:health\", ({ workerId, pid, rss, heapUsed, eventLoopLagMs }) => {\n workerHealth.set(workerId, { pid, rss, heapUsed, eventLoopLagMs, lastBeatAt: Date.now() });\n });\n bind(\"worker:exit\", ({ workerId }) => {\n workerHealth.delete(workerId);\n });\n\n bind(\"worker:recycle\", ({ reason }) => {\n workerRecyclesCounter.add(1, { reason });\n });\n bind(\"worker:wedged\", () => {\n wedgedKillsCounter.add(1);\n });\n bind(\"fleet:recovered\", ({ degradedDurationMs }) => {\n recoveryDurationGauge.record(degradedDurationMs / 1000);\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 workerHealth.clear();\n // If the plugin's provider became the global one, release the slot:\n // metrics.disable() is the API's public unregister and restores the exact\n // pre-install state (getMeterProvider() falls back to the noop provider).\n // Restoring the captured noop prior via setGlobalMeterProvider() instead\n // would leave the slot occupied and block the app from registering later.\n if (pluginSetGlobalProvider) {\n metrics.disable();\n pluginSetGlobalProvider = false;\n }\n await shutdownProvider();\n },\n\n async shutdown(): Promise<void> {\n await shutdownProvider();\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["pkgJson.version"],"sources":["../package.json","../src/index.ts"],"sourcesContent":["","import cluster from \"node:cluster\";\nimport { randomUUID } from \"node:crypto\";\nimport os from \"node:os\";\nimport {\n type Logger,\n type Orchestrator,\n type OrchestratorEvents,\n type ResolvedConfig,\n withLoggerPrefix,\n} from \"@goopil/clusterkit\";\nimport { metrics, type ObservableResult } 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 pkgJson from \"../package.json\" with { type: \"json\" };\nimport type { OtlpMeterPlugin, OtlpMeterPluginOptions } from \"./types.js\";\n\nexport type { OtlpMeterPlugin, OtlpMeterPluginOptions } from \"./types.js\";\n\ntype PrimaryEvent =\n | \"worker:crash\"\n | \"worker:restart\"\n | \"circuit-breaker:tripped\"\n | \"worker:health\"\n | \"worker:exit\"\n | \"worker:recycle\"\n | \"worker:wedged\"\n | \"fleet:recovered\";\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: string = pkgJson.version;\n\nfunction isMissingModuleError(err: unknown): boolean {\n const code = (err as NodeJS.ErrnoException | undefined)?.code;\n return code === \"ERR_MODULE_NOT_FOUND\" || code === \"MODULE_NOT_FOUND\";\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 headers,\n } = options;\n\n if (!Number.isFinite(exportIntervalMs) || exportIntervalMs < 1_000) {\n throw new TypeError(\n \"otlp-meter plugin: exportIntervalMs must be a finite number >= 1000 (minimum 1s to avoid flooding the collector)\",\n );\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 pluginSetGlobalProvider = false;\n let pluginLog: Logger | null = null;\n let primaryOrchestrator: Orchestrator | undefined;\n interface WorkerHealthSample {\n pid: number;\n rss: number;\n heapUsed: number;\n eventLoopLagMs: number;\n lastBeatAt: number;\n }\n\n // Listeners stored payload-agnostic; the concrete payload type is inferred at\n // the bind() call site via OrchestratorEvents[E].\n const primaryListeners: Array<{ event: PrimaryEvent; listener: (...args: never[]) => void }> = [];\n const workerHealth = new Map<number, WorkerHealthSample>();\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 // The gRPC exporter config omits `headers` (gRPC uses `metadata` instead):\n // forwarding them would be silently dropped, leaving exports unauthenticated.\n if (protocol === \"grpc\" && headers && Object.keys(headers).length > 0) {\n pluginLog?.warn(\n \"headers are not supported by the gRPC exporter — configure metadata via the exporter's own options or use protocol: 'http'\",\n );\n }\n\n try {\n const mod = await import(exporterModuleName);\n const config = protocol === \"grpc\" ? { url: resolvedEndpoint } : { url: resolvedEndpoint, headers };\n return new mod.OTLPMetricExporter(config) 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 // Reinstalling the same instance after a shutdown must not orphan the old\n // provider or leave the latch stuck at `true` (the new provider's shutdown\n // would otherwise be a permanent no-op).\n if (meterProvider) await shutdownProvider();\n isShutdown = false;\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 // setGlobalMeterProvider() refuses to override an existing registration\n // and returns false — never clobber the app's own OTel setup.\n if (metrics.setGlobalMeterProvider(meterProvider)) {\n pluginSetGlobalProvider = true;\n } else {\n log?.warn(\n \"A global OpenTelemetry meter provider is already registered — leaving it untouched; clusterkit meters use the plugin's own provider\",\n );\n }\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 workerRssGauge = meter.createObservableGauge(`${prefix}worker.rss_bytes`, {\n description: \"Resident set size per worker from health heartbeats\",\n unit: \"By\",\n });\n const workerHeapGauge = meter.createObservableGauge(`${prefix}worker.heap_used_bytes`, {\n description: \"V8 heap used per worker from health heartbeats\",\n unit: \"By\",\n });\n const workerLagGauge = meter.createObservableGauge(`${prefix}worker.eventloop_lag_ms`, {\n description: \"Event loop lag per worker from health heartbeats\",\n unit: \"ms\",\n });\n const workerHeartbeatAgeGauge = meter.createObservableGauge(`${prefix}worker.heartbeat_age_seconds`, {\n description: \"Seconds since the last health heartbeat per worker\",\n unit: \"s\",\n });\n\n const observeWorkerHealth = (\n result: ObservableResult<number>,\n pick: (sample: WorkerHealthSample) => number,\n ): void => {\n for (const [workerId, sample] of workerHealth) {\n result.observe(pick(sample), { \"worker.id\": workerId, \"process.pid\": sample.pid });\n }\n };\n\n workerRssGauge.addCallback((result) => observeWorkerHealth(result, (s) => s.rss));\n workerHeapGauge.addCallback((result) => observeWorkerHealth(result, (s) => s.heapUsed));\n workerLagGauge.addCallback((result) => observeWorkerHealth(result, (s) => s.eventLoopLagMs));\n workerHeartbeatAgeGauge.addCallback((result) => {\n const now = Date.now();\n observeWorkerHealth(result, (s) => Math.max(0, (now - s.lastBeatAt) / 1000));\n });\n\n const workerRecyclesCounter = meter.createCounter(`${prefix}worker.recycles`, {\n description: \"Total number of worker recycles by reason\",\n });\n const wedgedKillsCounter = meter.createCounter(`${prefix}worker.wedged.kills`, {\n description: \"Total number of workers killed for being unresponsive\",\n });\n const recoveryDurationGauge = meter.createGauge(`${prefix}recovery.duration_seconds`, {\n description: \"Duration of the last fleet degraded-to-recovered cycle\",\n unit: \"s\",\n });\n\n const fleetTargetGauge = meter.createObservableGauge(`${prefix}fleet.target_workers`, {\n description: \"Target worker count (live fleet health)\",\n });\n const fleetActiveGauge = meter.createObservableGauge(`${prefix}fleet.active_workers`, {\n description: \"Currently active workers (live fleet health)\",\n });\n const fleetQuarantinedGauge = meter.createObservableGauge(`${prefix}fleet.quarantined_slots`, {\n description: \"Quarantined worker slots (live fleet health)\",\n });\n\n fleetTargetGauge.addCallback((result) => {\n result.observe(orchestrator.getFleetHealth().target);\n });\n fleetActiveGauge.addCallback((result) => {\n result.observe(orchestrator.getFleetHealth().active);\n });\n fleetQuarantinedGauge.addCallback((result) => {\n result.observe(orchestrator.getFleetHealth().quarantined);\n });\n\n const bind = <E extends PrimaryEvent>(event: E, listener: (...args: OrchestratorEvents[E]) => 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 bind(\"worker:health\", ({ workerId, pid, rss, heapUsed, eventLoopLagMs }) => {\n workerHealth.set(workerId, { pid, rss, heapUsed, eventLoopLagMs, lastBeatAt: Date.now() });\n });\n bind(\"worker:exit\", ({ workerId }) => {\n workerHealth.delete(workerId);\n });\n\n bind(\"worker:recycle\", ({ reason }) => {\n workerRecyclesCounter.add(1, { reason });\n });\n bind(\"worker:wedged\", () => {\n wedgedKillsCounter.add(1);\n });\n bind(\"fleet:recovered\", ({ degradedDurationMs }) => {\n recoveryDurationGauge.record(degradedDurationMs / 1000);\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 workerHealth.clear();\n // If the plugin's provider became the global one, release the slot:\n // metrics.disable() is the API's public unregister and restores the exact\n // pre-install state (getMeterProvider() falls back to the noop provider).\n // Restoring the captured noop prior via setGlobalMeterProvider() instead\n // would leave the slot occupied and block the app from registering later.\n if (pluginSetGlobalProvider) {\n metrics.disable();\n pluginSetGlobalProvider = false;\n }\n await shutdownProvider();\n },\n\n async shutdown(): Promise<void> {\n await shutdownProvider();\n },\n };\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goopil/clusterkit-otlp-meter",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
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
5
|
"keywords": [
|
|
6
6
|
"opentelemetry",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"@opentelemetry/resources": "^2.0.0",
|
|
53
53
|
"@opentelemetry/semantic-conventions": "^1.30.0",
|
|
54
54
|
"@opentelemetry/sdk-metrics": "^2.0.0",
|
|
55
|
-
"@goopil/clusterkit": "^
|
|
55
|
+
"@goopil/clusterkit": "^2.0.0"
|
|
56
56
|
},
|
|
57
57
|
"peerDependenciesMeta": {
|
|
58
58
|
"@opentelemetry/exporter-metrics-otlp-http": {
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
"tsdown": "^0.22.14",
|
|
80
80
|
"typescript": "^7.0.2",
|
|
81
81
|
"vitest": "^4.1.11",
|
|
82
|
-
"@goopil/clusterkit": "
|
|
82
|
+
"@goopil/clusterkit": "2.0.0"
|
|
83
83
|
},
|
|
84
84
|
"scripts": {
|
|
85
85
|
"build": "tsdown",
|