From 8543b3be141c4d70f51b98df6ea9872648ef5d5a Mon Sep 17 00:00:00 2001 From: alexcos20 Date: Thu, 27 Aug 2026 08:03:29 +0300 Subject: [PATCH 1/2] add metrics --- Dockerfile | 2 +- README.md | 40 +- package-lock.json | 1017 ++++++++++++++++++++++++++++++++++++++ package.json | 11 +- src/index.ts | 24 + src/telemetry/config.ts | 68 +++ src/telemetry/gauges.ts | Bin 0 -> 5048 bytes src/telemetry/log.ts | 29 ++ src/telemetry/metrics.ts | 77 +++ src/telemetry/otel.ts | 104 ++++ src/telemetry/peerId.ts | 45 ++ 11 files changed, 1414 insertions(+), 3 deletions(-) create mode 100644 src/telemetry/config.ts create mode 100644 src/telemetry/gauges.ts create mode 100644 src/telemetry/log.ts create mode 100644 src/telemetry/metrics.ts create mode 100644 src/telemetry/otel.ts create mode 100644 src/telemetry/peerId.ts diff --git a/Dockerfile b/Dockerfile index a0eaa18..754daf5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -72,4 +72,4 @@ LABEL io.oceanprotocol.bootstrap.nofile-minimum="hard limit >= P2P_MAX_CONNECTIO io.oceanprotocol.bootstrap.nofile-howto="--ulimit nofile=65536:65536 (docker run), ulimits.nofile (compose), LimitNOFILE on the node container runtime (kubernetes); see README.md" ENTRYPOINT ["dumb-init", "--"] -CMD ["node", "--max-old-space-size=28784", "--trace-warnings", "--experimental-specifier-resolution=node", "dist/index.js"] +CMD ["node", "--import", "./dist/telemetry/otel.js", "--max-old-space-size=28784", "--trace-warnings", "--experimental-specifier-resolution=node", "dist/index.js"] diff --git a/README.md b/README.md index 2d69c79..e172179 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,12 @@ server), and which optionally publishes peer updates to RabbitMQ. | `P2P_ADMIN_PORT` | no | `9100` | port for `/health` and `/ready` - see "Health and readiness" below | | `P2P_READY_MIN_ROUTING_TABLE_PEERS` | no | `1` | minimum DHT routing-table size for `/ready` to report ready | | `P2P_ANNOUNCE_PRIVATE` | no | `false` | when `true`, the DHT stops filtering private addresses out of `FIND_NODE`/`GET_PROVIDERS` responses - see "Private-address hygiene" below | +| `OTEL_EXPORTER_OTLP_ENDPOINT` | no | unset | OTLP/HTTP base endpoint of an OpenTelemetry collector (e.g. `http://otel-collector:4318`). **Setting it is what turns telemetry on** - see "Metrics" below | +| `TELEMETRY_ENABLED` | no | unset | master switch; set to `off` to force telemetry off even when an endpoint is configured. Any other value (or unset) leaves it on when an endpoint is set | +| `OTEL_METRIC_EXPORT_INTERVAL` | no | `60000` | metric push interval in ms | +| `OTEL_SERVICE_NAME` | no | `ocean-node-bootstrap` | overrides the `service.name` resource attribute | +| `DEPLOYMENT_ENVIRONMENT` | no | `NODE_ENV` or `development` | `deployment.environment` resource attribute | +| `OCEAN_NETWORK_LABEL` | no | unset | optional `ocean.network` resource attribute, to group fleets in a central collector | The remaining `P2P_*` connection-manager knobs (`P2P_connectionsMaxParallelDials`, `P2P_connectionsDialTimeout`, `P2P_MAXPEERADDRSTODIAL`, @@ -136,7 +142,39 @@ authenticated external access. peers (default `1`). 503 otherwise, with a `checks` object showing which condition(s) failed. -Prometheus metrics are **not** exposed yet; that is planned separately. +## Metrics + +Metrics are exported over **OpenTelemetry**, **push-only**: the process pushes OTLP/HTTP to +an OpenTelemetry collector at `OTEL_EXPORTER_OTLP_ENDPOINT`, which fans out to Prometheus +(metrics) and Tempo (traces) for Grafana. There is **no `/metrics` scrape endpoint** - the +admin server stays loopback-only `/health` + `/ready`, and this push model is exactly what +lets a loopback-bound process still be observed. Telemetry is a **hard no-op** until +`OTEL_EXPORTER_OTLP_ENDPOINT` is set (and `TELEMETRY_ENABLED` is not `off`): an unconfigured +node emits nothing and its behaviour is unchanged. + +The SDK is loaded via `node --import ./dist/telemetry/otel.js` (already in the `start` script +and the Dockerfile `CMD`), before `dist/index.js`. Each process stamps a resource identity: +`service.name` (`ocean-node-bootstrap`), `service.version`, `deployment.environment`, +`ocean.node.role` (`bootstrap`/`relay` from `ROLE`), optional `ocean.network`, and +`service.instance.id` = the node's **libp2p peerId** (derived from `PRIVATE_KEY`; a random +UUID if the key is missing). Instance identity lives on the resource, never as a metric +label - metric labels are bounded enums only (no peerId / multiaddr / IP). + +Instruments emitted (OTel dotted names; Prometheus mangles dots to `_` and appends `_total` +to counters): + +- Counters: `ocean.p2p.peer.connect`, `ocean.p2p.peer.disconnect`, `ocean.p2p.peer.discovery`, + and `ocean.bootstrap.rabbitmq.published` (peer-update messages accepted by the RabbitMQ + discovery feed, `ROLE=bootstrap` only). +- Observable gauges: `ocean.p2p.connections` (labels `direction`, `limited`), + `ocean.p2p.dht.routing_table_peers`, `ocean.p2p.dht.mode` (`1` = server, `0` = client - + this restores the removed `ocean_bootstrap_dht_mode`), `ocean.p2p.relay_reservations`, and + `ocean.p2p.dial_queue` (label `status`). +- Plus Node runtime metrics (`@opentelemetry/instrumentation-runtime-node`: V8 heap, + event-loop delay, GC) and host/process metrics (`@opentelemetry/host-metrics`). + +A ready-to-run collector + Prometheus + Tempo + Grafana stack lives in the ocean-node repo +under `deploy/telemetry/`; point `OTEL_EXPORTER_OTLP_ENDPOINT` at that collector. ## Private-address hygiene diff --git a/package-lock.json b/package-lock.json index fac6ea7..2c5602a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,15 @@ "@libp2p/tls": "^3.1.8", "@libp2p/websockets": "^10.1.19", "@multiformats/multiaddr": "^13.0.3", + "@opentelemetry/api": "^1.9.1", + "@opentelemetry/exporter-metrics-otlp-http": "^0.221.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.221.0", + "@opentelemetry/host-metrics": "^0.39.0", + "@opentelemetry/instrumentation-runtime-node": "^0.34.0", + "@opentelemetry/resources": "^2.10.0", + "@opentelemetry/sdk-metrics": "^2.10.0", + "@opentelemetry/sdk-node": "^0.221.0", + "@opentelemetry/semantic-conventions": "^1.43.0", "amqplib": "^2.0.1", "datastore-level": "^13.0.1", "interface-datastore": "^10.0.1", @@ -262,6 +271,37 @@ "node": "^20.19.0 || ^22.13.0 || >=24" } }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.4", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.4.tgz", + "integrity": "sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.1.tgz", + "integrity": "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@humanfs/core": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", @@ -706,6 +746,16 @@ "multiformats": "^14.0.0" } }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", @@ -1952,6 +2002,548 @@ "@octokit/openapi-types": "^28.0.0" } }, + "node_modules/@opentelemetry/api": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", + "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/api-logs": { + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.221.0.tgz", + "integrity": "sha512-OlanaW1vv7ufTqQ3/fPLI4arGt5ZoM+P8abOMki6uEYnpRazepSWDwDnnw+la7kE26SHVC18//SMccrDvLKOXQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/configuration": { + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/configuration/-/configuration-0.221.0.tgz", + "integrity": "sha512-uE9y56Zdi9Gt/RdxYnVOo3YmFZkKJJMA0gqtBe8wh8gdtF5Asqe+Oh/TWiDtFb1s+31jNY4CWgnfIB1KOITfFA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "yaml": "^2.8.3" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0" + } + }, + "node_modules/@opentelemetry/context-async-hooks": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.10.0.tgz", + "integrity": "sha512-bvyMcgLEkozzSzpEEEo1OMoeQ97bxj6Qs2uN3mPrSdDvObMI1myffD/BPqcLlzZO9//d1SqQA/WPw7Cz2AiqhA==", + "license": "Apache-2.0", + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/core": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.10.0.tgz", + "integrity": "sha512-/wNZ8twnEQQA4HoHu22+vcsdru6pWPWxW+7w+FlxT6Id7PE/WIbZmVKkte+PF72e0F2dnImFeHD2syyE1Mw6MQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-grpc": { + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-grpc/-/exporter-logs-otlp-grpc-0.221.0.tgz", + "integrity": "sha512-txG1G0IrYSsKKMeiWZfj/i5cQmWB+h+hf3HzPpF3RqZVwp+iQQEIsv8Vtmzy6RWVdHdJZfygmVrBI39YTBvWcw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0", + "@opentelemetry/sdk-logs": "0.221.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-http": { + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.221.0.tgz", + "integrity": "sha512-nKXkr4Tomi6fjYVOf+ytcW3dZAVr4v4Bv5gsT6dr2gvpUPJpKgHB4XbMufMsPotRE3g0XH2GwVVCkN2w6SON+Q==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0", + "@opentelemetry/sdk-logs": "0.221.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto": { + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-proto/-/exporter-logs-otlp-proto-0.221.0.tgz", + "integrity": "sha512-AH6EY+47gXFaWYgG3hfeOneGiE9xIZGtDBk+9g0sM8NZWzsQhhmqPbQQXJzS7pyCh5jRRr2nYNXVrkCmoojRvQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0", + "@opentelemetry/sdk-logs": "0.221.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-grpc": { + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-grpc/-/exporter-metrics-otlp-grpc-0.221.0.tgz", + "integrity": "sha512-KOgCtO15FC6C1T/xOqBcr7EyUs7B+7yomGNb5Y97d3s38rPbCCk5sewkmE2b0/itOkQ/PptX8CLlD+kn2mEtTg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/exporter-metrics-otlp-http": "0.221.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http": { + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.221.0.tgz", + "integrity": "sha512-sRfCKbOzgy8xZQV2as0RzIZlnCmCseCKZGLfRcrpo2CBngJDr+rPtX0zkG0+oUCV5kfQPUoW3W3C96Ag3Y/Clg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-metrics": "2.10.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-proto": { + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-proto/-/exporter-metrics-otlp-proto-0.221.0.tgz", + "integrity": "sha512-YMF4LveY2I3yhw61rn6nmC9FE8U24IZHPeKU1Duc5+sbwjMd8FwZAwba318ImdThCg/HuVQvhm2y6bfgNPnfYg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/exporter-metrics-otlp-http": "0.221.0", + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-prometheus": { + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-prometheus/-/exporter-prometheus-0.221.0.tgz", + "integrity": "sha512-kW79a20qWESIuAdDrxzg9WKM98twV/NBWBFRAH57ap/+ssZhiCo0hckzKT0zpuwR/gSHrFAQhJL0bYDrnEM34g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-metrics": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-grpc": { + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.221.0.tgz", + "integrity": "sha512-zXminlZedtq9LvOW64CnNkOqk15zV75k8JgtdTuWFge6+jk2m4GmAUm6L2eIiG1o2a2bZxXw2PDrszm+bps0IA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0", + "@opentelemetry/sdk-trace": "2.10.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-http": { + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.221.0.tgz", + "integrity": "sha512-AySXiKoC+meiWm6zdVj5T2LnPDZuatveBby1cMOeQteIWsYXAUxs8Sru13G2pVSPrUXz6vF+og7QVBX6GdC/oQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0", + "@opentelemetry/sdk-trace": "2.10.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto": { + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.221.0.tgz", + "integrity": "sha512-Z9i2T7vgZbWe9rSLYxXVIbeW+XyzUq4rZanW3ZyVNwVDqCsh0EJKUgBWWQ0CZfeuUA+RQPzKgJQHMuWAUnKqXw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0", + "@opentelemetry/sdk-trace": "2.10.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-zipkin": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-zipkin/-/exporter-zipkin-2.10.0.tgz", + "integrity": "sha512-7gsvgf0UDoJ4l9ObrwBmz5G/ZogiPk+lq+g5GpLp24YQF/vPM/BSsnOfcLnfinast5ASUgLo78uSC/ObjlnXgg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-trace": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/host-metrics": { + "version": "0.39.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/host-metrics/-/host-metrics-0.39.0.tgz", + "integrity": "sha512-aDYyvqGAbsOqzkQiKl28NPnswNQSfuC6m29duVh2g7IrQLLS5kEDBntlNVma829pBUoawAqT0D0oJQ5K5brPgA==", + "license": "Apache-2.0", + "dependencies": { + "systeminformation": "^5.31.6" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation": { + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.221.0.tgz", + "integrity": "sha512-cCk80Z/iRDf/5gfsKMB4f74LqVA5yKETB/9ojPzVW/6/f70iu89nJvGxsFCxx4XfSohaOofkU19kiYm84AiAlw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.221.0", + "import-in-the-middle": "^3.0.0", + "require-in-the-middle": "^8.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-runtime-node": { + "version": "0.34.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-runtime-node/-/instrumentation-runtime-node-0.34.0.tgz", + "integrity": "sha512-Yb3PcmuK/iIOWY49GSEcSGl7fR4r6UqhZnuy5EWvFaqKqqs9SYnQeyQUEAbnBnSougRpwFBDbdN1SSGcvMhbtQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "^0.221.0", + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.221.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.221.0.tgz", + "integrity": "sha512-UFPIq80OH3Ns/oPFHRj14d4DTOxUo+MUFU8hUiCq5jTqFhdeJnfVSANHT+xp92409cA+oxzvlZCe6NM1wvCuBA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/otlp-transformer": "0.221.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-grpc-exporter-base": { + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.221.0.tgz", + "integrity": "sha512-rQDmNgyiGCTrescjnzH2ntVyUKVIq6I2UjuK8+stT/Xg0ZOT71FVJqwjFdspQl6Yol/Yqsut9bDo+ame8oTmDQ==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.14.3", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer": { + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.221.0.tgz", + "integrity": "sha512-lg6lkOU08Az23jVcn/0Els9HP+V8PnR4Km6p0KgpTggS0n/WuhnmY64rSh83Of9iR9nD+dpWr6adlcX8KzAwjg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.221.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-logs": "0.221.0", + "@opentelemetry/sdk-metrics": "2.10.0", + "@opentelemetry/sdk-trace": "2.10.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/propagator-b3": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-b3/-/propagator-b3-2.10.0.tgz", + "integrity": "sha512-GnA5B24H+1w8BO21J0q+IWNB0z1v+AGbcquTdIt/dufibhnhgxaA8YKvz0I3akRZhB1jHT+/tlzK+qlAjEDybQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/propagator-jaeger": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-jaeger/-/propagator-jaeger-2.10.0.tgz", + "integrity": "sha512-yw/IX8DL470dSMZJoE82ScfYGp7JWZ/G8kFJo35ZILUVTB2jFPTOaioN+8s09pH0RHsWNhweVZb+ZnjJJpCChg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.10.0.tgz", + "integrity": "sha512-q6MMm2zhggzsHVNbabYwut+a6nbuQQe3URUoxaojM/8K1IBfwwPzvxIjNi2/lI1TFe+fMHMW9MWhrtDLEXEnkA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs": { + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.221.0.tgz", + "integrity": "sha512-FaDcazjyMp7TZZZAsqbo4IkovP0UegoCu0EBkiNt+qCqvUf7FPAsfcrZ3+ZEkKgXZ/jHafop+JoGPDk3A0SmLg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.221.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-metrics": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.10.0.tgz", + "integrity": "sha512-t6r1VSvXNtSDnPXU1FbZeetJb7yyovHmgu0wRSoftxtE0g2rSNhQZQUy69sRUCL+iioJpX8SN/S6wq6ZtvLySQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-node": { + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-node/-/sdk-node-0.221.0.tgz", + "integrity": "sha512-UbYuvtBrQQB5Prsh9KOKy4kxzexFxfMs5MkteHeWMoswsEB7kiNhyUVkAOFW/qsEzNHtrkgyghrD2ilZJa+5YA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.221.0", + "@opentelemetry/configuration": "0.221.0", + "@opentelemetry/context-async-hooks": "2.10.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/exporter-logs-otlp-grpc": "0.221.0", + "@opentelemetry/exporter-logs-otlp-http": "0.221.0", + "@opentelemetry/exporter-logs-otlp-proto": "0.221.0", + "@opentelemetry/exporter-metrics-otlp-grpc": "0.221.0", + "@opentelemetry/exporter-metrics-otlp-http": "0.221.0", + "@opentelemetry/exporter-metrics-otlp-proto": "0.221.0", + "@opentelemetry/exporter-prometheus": "0.221.0", + "@opentelemetry/exporter-trace-otlp-grpc": "0.221.0", + "@opentelemetry/exporter-trace-otlp-http": "0.221.0", + "@opentelemetry/exporter-trace-otlp-proto": "0.221.0", + "@opentelemetry/exporter-zipkin": "2.10.0", + "@opentelemetry/instrumentation": "0.221.0", + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.221.0", + "@opentelemetry/propagator-b3": "2.10.0", + "@opentelemetry/propagator-jaeger": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-logs": "0.221.0", + "@opentelemetry/sdk-metrics": "2.10.0", + "@opentelemetry/sdk-trace": "2.10.0", + "@opentelemetry/sdk-trace-base": "2.10.0", + "@opentelemetry/sdk-trace-node": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.10.0.tgz", + "integrity": "sha512-MfQGq3GRmTh5fM/y+OjaO0vj6+luCB1XO2gfXCalKCfgKw0eHL++sm75DNweC6ohlp+aFvACqeE0fYayqdRaoQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.10.0.tgz", + "integrity": "sha512-GuYQQT7QD2EeO8lcZLRQzcbOyhqAzL+6WWTKTU9mSUBYBazkEDl+VrQcXQhbB08OWM9anD1aHleVadzulpOaUQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-trace": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-node": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.10.0.tgz", + "integrity": "sha512-GZK/G6oZyBLGlH1pUgeDch7D91KoHd2uotUGIkWCPi9GI5T9X0p4L7nNAMDR1BQjkRYoDqo+ddfVx9t5Uhys+Q==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/context-async-hooks": "2.10.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/sdk-trace-base": "2.10.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.43.0.tgz", + "integrity": "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/@peculiar/asn1-cms": { "version": "2.9.4", "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.9.4.tgz", @@ -2193,6 +2785,63 @@ "url": "https://opencollective.com/pkgr" } }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", + "license": "BSD-3-Clause" + }, "node_modules/@sindresorhus/fnv1a": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@sindresorhus/fnv1a/-/fnv1a-3.1.0.tgz", @@ -2638,6 +3287,21 @@ "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/any-signal": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/any-signal/-/any-signal-4.2.0.tgz", @@ -2935,6 +3599,12 @@ "node": ">=8" } }, + "node_modules/cjs-module-lexer": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.1.tgz", + "integrity": "sha512-Ca8swihM+/4yKecYHY52kgJd300hi2lADU/a1RxNTRe+RJ9jvqQlESpbz9DnG9mowez8qwXHB8qYdIUw9e+F5Q==", + "license": "MIT" + }, "node_modules/classic-level": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/classic-level/-/classic-level-3.0.0.tgz", @@ -2990,6 +3660,55 @@ "node": ">= 12" } }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/clone-regexp": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-3.0.0.tgz", @@ -3005,6 +3724,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -3298,6 +4035,12 @@ "node": ">= 0.4" } }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -3316,6 +4059,12 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", @@ -3343,6 +4092,15 @@ "node": ">= 0.4" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -3854,6 +4612,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-east-asian-width": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", @@ -4172,6 +4939,20 @@ "node": ">=8" } }, + "node_modules/import-in-the-middle": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.3.tgz", + "integrity": "sha512-AiohS3H80sXO6owEltjGX+glb7qXaDhBoJb9XcQVH4UI207xu/bDLUcadVKp7Qe576reg9yr/PXZjV5qx8gfbA==", + "license": "Apache-2.0", + "dependencies": { + "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^2.2.0", + "module-details-from-path": "^1.0.4" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -4297,6 +5078,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -4823,6 +5613,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, "node_modules/lodash.capitalize": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/lodash.capitalize/-/lodash.capitalize-4.2.1.tgz", @@ -4882,6 +5678,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, "node_modules/lru-cache": { "version": "7.18.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", @@ -4989,6 +5791,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/module-details-from-path": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", + "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==", + "license": "MIT" + }, "node_modules/module-error": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/module-error/-/module-error-1.0.2.tgz", @@ -5556,6 +6364,29 @@ "integrity": "sha512-82DVc5tI36neVB3IjdXR11ztwGuoBc98em9ijzubeZKxI47OlV2Znq6mlPqE5xPDzO2Uw98GHiQSjj2favBCRQ==", "license": "Apache-2.0 OR MIT" }, + "node_modules/protobufjs": { + "version": "7.6.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz", + "integrity": "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/protocols": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/protocols/-/protocols-2.0.2.tgz", @@ -5859,6 +6690,28 @@ "node": ">=20.18.1" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-in-the-middle": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-8.0.1.tgz", + "integrity": "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "module-details-from-path": "^1.0.3" + }, + "engines": { + "node": ">=9.3.0 || >=8.10.0 <9.0.0" + } + }, "node_modules/resolve-from": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", @@ -6171,6 +7024,32 @@ "url": "https://opencollective.com/synckit" } }, + "node_modules/systeminformation": { + "version": "5.33.4", + "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.33.4.tgz", + "integrity": "sha512-poXpX8M9NBll1NDMW1ho6qRFkjrlttfm8E/YtPmn5x0TQ1z8LoJUHg9kwr/sRDmCjktqfnFRxqsoUNpaRakeMQ==", + "license": "MIT", + "os": [ + "darwin", + "linux", + "win32", + "freebsd", + "openbsd", + "netbsd", + "sunos", + "android" + ], + "bin": { + "systeminformation": "lib/cli.js" + }, + "engines": { + "node": ">=10.0.0" + }, + "funding": { + "type": "Buy me a coffee", + "url": "https://www.buymeacoffee.com/systeminfo" + } + }, "node_modules/thunky": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", @@ -6565,6 +7444,58 @@ "dev": true, "license": "MIT" }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/ws": { "version": "8.21.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", @@ -6603,6 +7534,92 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index bb06578..c660baa 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "build": "npm run clean && npm run build:tsc", "build:tsc": "tsc --sourceMap", "clean": "rm -rf ./dist/", - "start": "node --max-old-space-size=28784 --trace-warnings --experimental-specifier-resolution=node dist/index.js", + "start": "node --import ./dist/telemetry/otel.js --max-old-space-size=28784 --trace-warnings --experimental-specifier-resolution=node dist/index.js", "lint": "eslint . && npm run type-check", "test": "node --test test/*.test.mjs", "lint:fix": "eslint . --fix", @@ -27,6 +27,15 @@ }, "dependencies": { "@chainsafe/libp2p-noise": "^17.0.0", + "@opentelemetry/api": "^1.9.1", + "@opentelemetry/exporter-metrics-otlp-http": "^0.221.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.221.0", + "@opentelemetry/host-metrics": "^0.39.0", + "@opentelemetry/instrumentation-runtime-node": "^0.34.0", + "@opentelemetry/resources": "^2.10.0", + "@opentelemetry/sdk-metrics": "^2.10.0", + "@opentelemetry/sdk-node": "^0.221.0", + "@opentelemetry/semantic-conventions": "^1.43.0", "@chainsafe/libp2p-yamux": "^8.0.1", "@ipshipyard/libp2p-auto-tls": "^2.0.2", "@libp2p/bootstrap": "^12.0.29", diff --git a/src/index.ts b/src/index.ts index 0d46db3..e3dd09e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -27,6 +27,13 @@ import type { Channel, ChannelModel, RecoveringChannelModel } from 'amqplib' import { multiaddr } from '@multiformats/multiaddr' import ipaddr from 'ipaddr.js' import { createHash } from 'node:crypto' +import { + p2pPeerConnect, + p2pPeerDisconnect, + p2pPeerDiscovery, + rabbitmqPublished +} from './telemetry/metrics.js' +import { registerBootstrapGauges } from './telemetry/gauges.js' /** Same defaults as ocean-node `DEFAULT_FILTER_ANNOUNCED_ADDRESSES` */ const DEFAULT_FILTER_ANNOUNCED_ADDRESSES = [ @@ -794,6 +801,10 @@ async function start() { handleSelfPeerUpdate(evt) }) + // observable-gauge callbacks over the running libp2p handle. A no-op when telemetry is + // unconfigured (the meter has no provider), and every probe inside is guarded. + registerBootstrapGauges(libp2p) + if (ROLE === 'relay') { instrumentRelayReservations(libp2p) logEvent('info', 'rabbitmq:disabled', { @@ -1244,6 +1255,15 @@ async function shutdown(signal: string): Promise { logEvent('error', 'shutdown:datastore-close-failed', errorFields(e)) } } + // flush the final metric batch before exit. Lazy import so the OTel SDK is never pulled + // into the graph from here - it is already loaded via `--import`, so this resolves from + // the module cache, and `shutdownTelemetry()` is a no-op when telemetry is disabled. + try { + const { shutdownTelemetry } = await import('./telemetry/otel.js') + await shutdownTelemetry() + } catch (e) { + logEvent('error', 'shutdown:telemetry-flush-failed', errorFields(e)) + } logEvent('info', 'shutdown:complete', { signal }) } finally { clearTimeout(forceExit) @@ -1468,6 +1488,7 @@ async function notifyQueue( // no fingerprint recorded, so the next `peer:update` publishes this peer again return } + rabbitmqPublished.add(1) rememberFingerprint(peerId, fingerprint) logEvent('debug', 'queue:published', { peerId, @@ -1512,6 +1533,7 @@ function handlePeerConnect(details: any) { if (details) { const peerId = details.detail logEvent('debug', 'peer:connect', { peerId: peerId.toString() }) + p2pPeerConnect.add(1) // notifyQueue('connect', peerId.toString(), null) } } @@ -1547,6 +1569,7 @@ function handlePeerDisconnect(details: any) { if (details) { const peerId = details.detail logEvent('debug', 'peer:disconnect', { peerId: peerId.toString() }) + p2pPeerDisconnect.add(1) } } @@ -1554,6 +1577,7 @@ function handlePeerDiscovery(details: any) { try { const peerInfo = details.detail logEvent('debug', 'peer:discovery', { peerId: peerInfo.id.toString() }) + p2pPeerDiscovery.add(1) if (!libp2p) return const currentConnections = libp2p.getConnections().length diff --git a/src/telemetry/config.ts b/src/telemetry/config.ts new file mode 100644 index 0000000..ce124f5 --- /dev/null +++ b/src/telemetry/config.ts @@ -0,0 +1,68 @@ +/** + * Telemetry configuration, parsed once from the environment. + * + * Telemetry is a **hard no-op** unless an OTLP endpoint is configured and the master + * switch is not turned off. This module depends on nothing but `process.env`, so importing + * it can never pull the OpenTelemetry SDK into the import graph ahead of the `--import` + * bootstrap - keeping it side-effect-free is what lets `metrics.ts` be imported from + * `index.ts` cheaply. + */ + +export type TelemetryConfig = { + /** Master switch: an OTLP endpoint is set and TELEMETRY_ENABLED is not 'off'. */ + enabled: boolean + /** Why telemetry is off, for a single startup log line. `undefined` when enabled. */ + disabledReason?: string + endpoint?: string + serviceName: string + serviceVersion: string + environment: string + exportIntervalMs: number + /** `bootstrap` | `relay` - the node's ROLE, surfaced as `ocean.node.role`. */ + role: string + /** Optional operator tag (`OCEAN_NETWORK_LABEL`) to group fleets, as `ocean.network`. */ + networkLabel?: string +} + +/** + * Positive **integer** milliseconds, or the fallback. + * + * `Number(env.X)` alone is not enough: `X=""` yields `0` and `X=abc` yields `NaN`, and both + * then reach the OTel export interval as a busy loop or an immediate throw. Integrality is + * part of the contract - `0.1` is finite and positive but a 0.1 ms interval is a busy loop. + */ +export function readPositiveInt(value: string | undefined, fallback: number): number { + if (value === undefined || value.trim() === '') return fallback + const parsed = Number(value) + if (!Number.isInteger(parsed) || parsed <= 0) return fallback + return parsed +} + +let cached: TelemetryConfig | undefined + +export function telemetryConfig(env: NodeJS.ProcessEnv = process.env): TelemetryConfig { + if (cached) return cached + + const endpoint = env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim() || undefined + const enabled = env.TELEMETRY_ENABLED !== 'off' && !!endpoint + + cached = { + enabled, + endpoint, + serviceName: env.OTEL_SERVICE_NAME?.trim() || 'ocean-node-bootstrap', + serviceVersion: env.npm_package_version || '0.0.0', + environment: env.DEPLOYMENT_ENVIRONMENT || env.NODE_ENV || 'development', + exportIntervalMs: readPositiveInt(env.OTEL_METRIC_EXPORT_INTERVAL, 60_000), + role: env.ROLE?.trim() || 'bootstrap', + networkLabel: env.OCEAN_NETWORK_LABEL?.trim() || undefined, + disabledReason: enabled + ? undefined + : 'OTEL_EXPORTER_OTLP_ENDPOINT unset or TELEMETRY_ENABLED=off' + } + return cached +} + +/** Test-only: drop the memoized config so a test can re-parse a mutated environment. */ +export function resetTelemetryConfigForTest(): void { + cached = undefined +} diff --git a/src/telemetry/gauges.ts b/src/telemetry/gauges.ts new file mode 100644 index 0000000000000000000000000000000000000000..f9a02ea2daaa50f20530b565568b96b50d8f7558 GIT binary patch literal 5048 zcmbVQ+j84B5bd+SV$&nP zNnM0=tW|x3id>o}SPtyT6>y$rA03J_KI@pei#zu5yF+e zhBPRwD{W=|HS9N5ikxp{#Vv$+>5}9>yoGF$-BJ$hQn~`5Fi$E|+(|NPQ3ECc$Tonp zEOD1gQI$!d#tiVlu&BAw*(9qrb)}@TX7cX)cN2dzu_(&IZc}UK(HF1Cz=BfJT;%yr z!e;Bv$LGPhb6OS}iYt{#s-=#GI!D}zG5{)aL-3=hM{GsLKFvM&X2jlLAe|rJv2|s~ zHK3)eGB%|@89FOVxsjGVY^YILrIy92(GY@@gfGyHFM_43YFyP$FZ^Tx?yBIRvjyC#nsDXsm>@F()1-=4bSL`J^|kl_Vf{^GwA%6 zLSE2PB}4pvfEy7KyWiTQyy4{G8t-FP_E6>oJv;6;mdu6b3_-EQ9q+0lcV@#TV|Of@62p>s zJp8G2C2e2NkS|{>DS7rvYq3oWd&k_Wp=U6& zDNDjps~b6>Ga4*WRApbnt=!`D{F{%hu7|I_`55Fr%$=@q?bJw{VE>4cxB^glWk|5^ z`H;S)?_5mbR1bt}cP2v^*c*l+93JjKaNX)}Bj^T(;b>}Vl(b~<)qrIaAwtp+ILTg$ z0z4=gp$r^KqX%zVh~|r5uSok`9$QXkRLOt29nC|$aPAgpHEF)KY1rwqKIM(`qk-Hn z4o{C{XUMS-jwT^6)^@gI)XZ5GW14FpfcAo>QyTd297M?XAncp%YnnTHfETwU5{AB` zgiVopNE3RFSVtL9mJ|LccnA_MjmhvhXrXdG9WQ`^NIA^-9k?Pi&v^HzPp&-xBK+dY z!1f_ufK;qaz-lk~-R*bu^bRL&J@MOEl*eem+PX!L)nlHx2wkLiAWE45_A?|Iav+vu zZj4&bgP-Au08JbhW3c#%I#L}C)YQzTt(}A zpia9;*}>~Q$H~shId=nhRp07ESqx&1X1h;@l!ucVrI6^HgJcMYwZK+L4Oj8um;}Mjx%rk*z*G2fU>_ zRkI&a8y_gg!|A|ihc|P!fNMDI!mPc_BV~NYK{56zNZ3Q8@4`KL=FB z?vaKT@KMLQf{{B+iwp8AYcVUjy1a1HMtoFo1Luguk|#f3QCt^-AfBXCdJi5?7_j+( zpBVFk=N|mrp0VioK=A(JHIV=n0|?wMUx}Qa^zrdN>f=0iGosHyR^koir-e2Nnc`u@3RXD6&7JYQ z8~s=gW*!{!`dLuypS}6`Gur~u`OJ=^jrb9zZ7d$jdx&UWxA)l_8v5&pOt}9wva|l$ zftgXCdo5@U3rB9;Usuq>+?~jnU+qH#M*akYTEUM$2>lp`gjXI { + if (started) return telemetryConfig() + started = true + + const config = telemetryConfig(env) + + if (!config.enabled) { + if (config.disabledReason) { + telemetryLog(`disabled - ${config.disabledReason}`) + } + return config + } + + try { + // service.instance.id is the node's libp2p peerId, derived deterministically from + // PRIVATE_KEY before libp2p starts. A random UUID is the fallback when the key is + // missing or malformed, so telemetry still works but the node is not pinnable in + // dashboards by its peerId. + const peerId = await derivePeerId(env.PRIVATE_KEY).catch(() => undefined) + const instanceId = peerId ?? randomUUID() + + const resource = resourceFromAttributes({ + 'service.name': config.serviceName, + 'service.version': config.serviceVersion, + 'deployment.environment': config.environment, + 'service.instance.id': instanceId, + 'ocean.node.role': config.role, + ...(config.networkLabel ? { 'ocean.network': config.networkLabel } : {}) + }) + + sdk = new NodeSDK({ + resource, + traceExporter: new OTLPTraceExporter(), + metricReader: new PeriodicExportingMetricReader({ + exporter: new OTLPMetricExporter(), + exportIntervalMillis: config.exportIntervalMs + }), + instrumentations: [new RuntimeNodeInstrumentation()] + }) + + sdk.start() + + const hostMetrics = new HostMetrics({ name: config.serviceName }) + hostMetrics.start() + + telemetryLog( + `enabled - exporting to ${config.endpoint} as service.name=${config.serviceName} ` + + `(role=${config.role}, instance=${instanceId})` + ) + + // No signal handlers here on purpose. `index.ts` owns SIGINT/SIGTERM and calls + // `process.exit(0)`; it awaits `shutdownTelemetry()` before exiting so the final metric + // batch flushes deterministically rather than being killed mid-flush. + } catch (error) { + telemetryLog('failed to initialize - continuing without telemetry', error) + } + + return config +} + +export async function shutdownTelemetry(): Promise { + if (!sdk) return + try { + await sdk.shutdown() + } catch (error) { + telemetryLog('shutdown failed', error) + } +} + +// Auto-start when loaded via `--import`. Top-level await is fine in an ESM module; the +// `started` guard keeps a stray import (a test, or `index.ts`'s lazy shutdown import) from +// starting a second SDK. +await initTelemetry() diff --git a/src/telemetry/peerId.ts b/src/telemetry/peerId.ts new file mode 100644 index 0000000..41cd9f9 --- /dev/null +++ b/src/telemetry/peerId.ts @@ -0,0 +1,45 @@ +/** + * Derive the libp2p peerId string from `PRIVATE_KEY`, for `service.instance.id`. + * + * The peerId is the canonical per-node identity in the dashboards, and it is deterministic + * from the node key - so it can be computed here, at telemetry-bootstrap time, without + * waiting for libp2p to start. This mirrors `getPeerIdFromPrivateKey()` in `index.ts` + * (kept minimal and self-contained so the telemetry module has no dependency on `index.ts`). + * + * Async so a future KMS-backed key provider can slot in without changing the call site. + * Returns `undefined` on any failure - a missing or malformed key must never abort telemetry + * startup; the caller falls back to a random UUID. + */ +import { privateKeyFromRaw } from '@libp2p/crypto/keys' +import { peerIdFromPrivateKey } from '@libp2p/peer-id' + +function hexStringToByteArray(hexString: string): Uint8Array { + const hex = hexString.startsWith('0x') ? hexString.slice(2) : hexString + if (hex.length % 2 !== 0) { + throw new Error('Must have an even number of hex digits to convert to bytes') + } + const numBytes = hex.length / 2 + const byteArray = new Uint8Array(numBytes) + for (let i = 0; i < numBytes; i++) { + byteArray[i] = parseInt(hex.substring(i * 2, i * 2 + 2), 16) + } + return byteArray +} + +// Not declared `async` (the repo's `require-await` rule forbids an async function with no +// `await`), but it returns a Promise so a future KMS-backed, genuinely-async key provider can +// slot in behind the same signature - and so `otel.ts` can `await` it. +export function derivePeerId( + privateKey: string | undefined = process.env.PRIVATE_KEY +): Promise { + try { + if (privateKey === undefined || privateKey.trim() === '') { + return Promise.resolve(undefined) + } + const bytes = hexStringToByteArray(privateKey.trim()) + const key = privateKeyFromRaw(bytes) + return Promise.resolve(peerIdFromPrivateKey(key).toString()) + } catch { + return Promise.resolve(undefined) + } +} From a3bf86e615fdf0b31adab2277ba8b17e5c26cef9 Mon Sep 17 00:00:00 2001 From: alexcos20 Date: Fri, 28 Aug 2026 07:58:01 +0300 Subject: [PATCH 2/2] fix comments --- src/telemetry/config.ts | 2 +- src/telemetry/peerId.ts | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/telemetry/config.ts b/src/telemetry/config.ts index ce124f5..8b6fd56 100644 --- a/src/telemetry/config.ts +++ b/src/telemetry/config.ts @@ -50,7 +50,7 @@ export function telemetryConfig(env: NodeJS.ProcessEnv = process.env): Telemetry enabled, endpoint, serviceName: env.OTEL_SERVICE_NAME?.trim() || 'ocean-node-bootstrap', - serviceVersion: env.npm_package_version || '0.0.0', + serviceVersion: env.OTEL_SERVICE_VERSION?.trim() || env.npm_package_version || '0.0.0', environment: env.DEPLOYMENT_ENVIRONMENT || env.NODE_ENV || 'development', exportIntervalMs: readPositiveInt(env.OTEL_METRIC_EXPORT_INTERVAL, 60_000), role: env.ROLE?.trim() || 'bootstrap', diff --git a/src/telemetry/peerId.ts b/src/telemetry/peerId.ts index 41cd9f9..7b44396 100644 --- a/src/telemetry/peerId.ts +++ b/src/telemetry/peerId.ts @@ -18,6 +18,12 @@ function hexStringToByteArray(hexString: string): Uint8Array { if (hex.length % 2 !== 0) { throw new Error('Must have an even number of hex digits to convert to bytes') } + // `parseInt` silently accepts partial/invalid input ('1z' -> 1, 'zz' -> NaN -> 0), + // so validate the whole string is hex up front - a malformed PRIVATE_KEY must fail + // (and fall back to a random UUID) rather than derive a wrong peer ID. + if (hex.length > 0 && !/^[0-9a-fA-F]+$/.test(hex)) { + throw new Error('Hex string contains non-hexadecimal characters') + } const numBytes = hex.length / 2 const byteArray = new Uint8Array(numBytes) for (let i = 0; i < numBytes; i++) {