@devopsplaybook.io/otel-utils 1.5.3 → 1.6.0-beta.37.40daec3

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 CHANGED
@@ -23,19 +23,21 @@ All classes accept a `ConfigOTelInterface` object:
23
23
  | `OPENTELEMETRY_COLLECTOR_EXPORT_METRICS_INTERVAL_SECONDS` | `number?` | `60` | Metrics export interval |
24
24
  | `OPENTELEMETRY_COLLECT_AUTHORIZATION_HEADER` | `string?` | — | Bearer token for collector auth |
25
25
 
26
- When a collector endpoint is not provided, the corresponding signal (traces, logs, or metrics) is initialized with no export — the provider is still available but no data is sent.
26
+ When a collector endpoint is not provided, the corresponding signal (traces, logs, or metrics) is initialized with no export: traces and metrics still have a local provider but no data is sent, and `StandardLogger` creates no provider (its `getLogger()` returns `undefined`).
27
27
 
28
28
  ## Exported Classes
29
29
 
30
30
  ### StandardTracer
31
31
 
32
- Sets up a global `NodeTracerProvider` with:
32
+ Creates a `TracerProvider` with:
33
33
 
34
34
  - AWS X-Ray ID generator (`AWSXRayIdGenerator`)
35
35
  - OTLP HTTP trace exporter (optional, when `OPENTELEMETRY_COLLECTOR_HTTP_TRACES` is set)
36
- - `AsyncHooksContextManager` for automatic context propagation
36
+ - one process-wide `AsyncLocalStorageContextManager`, installed on first construction when no other context manager is registered (kept for the process lifetime)
37
37
  - Service resource attributes (name, version, hostname)
38
38
 
39
+ The instance exports its spans through its own provider (`tracer.startSpan(...)` is always routed to this instance). The first `StandardTracer` constructed in a process also registers its provider as the global one, so `trace.getTracer(...)` from `@opentelemetry/api` resolves to it. Later instances do not replace the global provider: they log a warning and keep exporting through their own provider — see [Provider lifecycle](#provider-lifecycle).
40
+
39
41
  ```typescript
40
42
  import { StandardTracer } from "@devopsplaybook.io/otel-utils";
41
43
 
@@ -49,17 +51,17 @@ span.end();
49
51
 
50
52
  - `startSpan(name, parentSpan?, options?)` — Creates a span. Span names are sanitized to `[a-zA-Z0-9-_/]`. When neither `parentSpan` nor `options` is given, the span is `SpanKind.INTERNAL` and carries `http.request_method=BACKEND` and a synthetic `http.route` attribute. Passing `options` (e.g. `{ kind: SpanKind.SERVER }`) forwards them to the OpenTelemetry SDK instead and skips those synthetic attributes, because the caller then describes the span itself.
51
53
  - `static updateHttpHeader(context, headers?)` — Injects W3C trace context into an HTTP headers object for propagation to downstream services.
52
- - `shutdown()` — Flushes buffered spans and shuts the tracer provider down.
54
+ - `shutdown()` — Flushes buffered spans and shuts the tracer provider down. Idempotent.
53
55
 
54
56
  ### StandardLogger
55
57
 
56
- Initializes a `LoggerProvider` with an OTLP log exporter and a `BatchLogRecordProcessor` (`maxQueueSize: 2048`). When `OPENTELEMETRY_COLLECTOR_HTTP_LOGS` is set, the provider is also registered as the global OTel logger provider, so `logs.getLogger(...)` from `@opentelemetry/api-logs` returns loggers wired to the configured exporter. Without that endpoint, `initOTel` still records the service identity but registers nothing globally and `getLogger()` returns `undefined`.
58
+ Initializes a `LoggerProvider` with an OTLP log exporter and a `BatchLogRecordProcessor` (`maxQueueSize: 2048`). When `OPENTELEMETRY_COLLECTOR_HTTP_LOGS` is set, the provider is also registered as the global OTel logger provider (first instance only), so `logs.getLogger(...)` from `@opentelemetry/api` returns loggers wired to the configured exporter. Each instance always logs through its own provider (`logger.createModuleLogger(...)`), so a second instance never mis-routes its records to the first one. Without that endpoint, `initOTel` still records the service identity but creates no provider and `getLogger()` returns `undefined`.
57
59
 
58
60
  ```typescript
59
61
  import { StandardLogger } from "@devopsplaybook.io/otel-utils";
60
62
 
61
63
  const logger = new StandardLogger();
62
- logger.initOTel(config);
64
+ await logger.initOTel(config);
63
65
 
64
66
  const moduleLog = logger.createModuleLogger("my-module");
65
67
  moduleLog.info("Hello world");
@@ -67,10 +69,10 @@ moduleLog.info("Hello world");
67
69
 
68
70
  **Methods:**
69
71
 
70
- - `initOTel(config)` — Initializes the logger provider and optional OTLP exporter, and registers the provider globally when a logs endpoint is configured.
71
- - `getLogger()` — Returns the underlying OTel `Logger` or `undefined`.
72
+ - `initOTel(config)` — Initializes the logger provider and optional OTLP exporter, and registers the provider globally when a logs endpoint is configured and no other logger provider is registered. Returns `Promise<void>`: the first call initializes synchronously; await it when re-initializing, because a previous provider is flushed and shut down first.
73
+ - `getLogger()` — Returns the underlying OTel `Logger` or `undefined` (also `undefined` after `shutdown()`).
72
74
  - `createModuleLogger(moduleName)` — Creates a `ModuleLogger` scoped to a module name.
73
- - `shutdown()` — Flushes buffered log records and shuts the logger provider down. No-op when no logs endpoint is configured.
75
+ - `shutdown()` — Flushes buffered log records and shuts the logger provider down. Idempotent. No-op when no logs endpoint is configured.
74
76
 
75
77
  ### ModuleLogger
76
78
 
@@ -94,7 +96,7 @@ When a `Span` is passed as `context`, the log record includes `span.id` and `tra
94
96
 
95
97
  ### StandardMeter
96
98
 
97
- Creates a `MeterProvider` with an optional `PeriodicExportingMetricReader` (`concurrencyLimit: 5`). Provides convenience factories for common metric types.
99
+ Creates a `MeterProvider` with an optional `PeriodicExportingMetricReader` (`concurrencyLimit: 5`). Provides convenience factories for common metric types. When `OPENTELEMETRY_COLLECTOR_HTTP_METRICS` is set, the provider is registered as the global meter provider (first instance only), so metrics emitted through `metrics.getMeter(...)` reach the configured exporter. Unconfigured meters leave the global API untouched (Noop).
98
100
 
99
101
  ```typescript
100
102
  import { StandardMeter } from "@devopsplaybook.io/otel-utils";
@@ -123,22 +125,44 @@ const cpuGauge = meter.createObservableGauge(
123
125
  - `createUpDownCounter(key)` — Creates an `UpDownCounter`.
124
126
  - `createHistogram(key, options?)` — Creates a `Histogram` named `${serviceName}.${key}`; with `{ unprefixed: true }` the histogram is created exactly as `key`, without the service-name prefix.
125
127
  - `createObservableGauge(key, callback, description?)` — Creates an `ObservableGauge` with a callback. Description is optional.
126
- - `shutdown()` — Flushes buffered metrics and shuts the meter provider down.
128
+ - `shutdown()` — Flushes buffered metrics and shuts the meter provider down. Idempotent.
129
+
130
+ ## Provider lifecycle
131
+
132
+ OpenTelemetry's global API accepts one provider per signal per process, and the registration is one-shot until the global API is disabled. `otel-utils` manages this as follows:
133
+
134
+ - **One provider per signal per process.** The first `StandardTracer`, configured `StandardLogger` and configured `StandardMeter` created in a process each register their provider globally. Global-API calls (`trace.getTracer`, `logs.getLogger`, `metrics.getMeter`) resolve to those first providers until they shut down.
135
+ - **Additional instances keep working, locally.** A second instance does not replace the global provider: it logs a `console.warn` and still exports through its own provider, with its own `service.name`/`service.version`. Its records/spans/metrics are never attributed to another service.
136
+ - **Re-initialisation.** Calling `StandardLogger.initOTel()` again on the same instance flushes and shuts down the previous provider (its buffered records are exported), then registers the new one as the global provider if possible. Await the returned promise when re-initializing.
137
+ - **Shutdown.** `shutdown()` flushes pending telemetry, shuts the provider down and is idempotent. If the instance owns the global provider, it also calls `trace.disable()` / `logs.disable()` / `metrics.disable()`, returning the global API to Noop and allowing a later instance to register again. Shutting down a non-owner leaves the global provider untouched. After `StandardLogger.shutdown()`, `getLogger()` is `undefined`.
138
+ - **Context propagation.** One `AsyncLocalStorageContextManager` is installed per process on the first `StandardTracer` construction, only when no other context manager is registered. It stays active for the process lifetime (it is never disabled), so re-initialisation keeps working.
127
139
 
128
140
  ## Graceful shutdown
129
141
 
130
- `StandardTracer`, `StandardLogger` and `StandardMeter` buffer telemetry: `BatchSpanProcessor`, `BatchLogRecordProcessor` and `PeriodicExportingMetricReader` hold data until their next export cycle. On process exit that buffered data would be lost, so wire the `shutdown()` methods to `SIGTERM`/`SIGINT` handlers:
142
+ `StandardTracer`, `StandardLogger` and `StandardMeter` buffer telemetry: `BatchSpanProcessor`, `BatchLogRecordProcessor` and `PeriodicExportingMetricReader` hold data until their next export cycle. On process exit that buffered data would be lost, so flush it on `SIGTERM`/`SIGINT`.
143
+
144
+ The opt-in `registerShutdownHooks` helper does this for every configured provider and exits the process with code 0 after flushing:
145
+
146
+ ```typescript
147
+ import { registerShutdownHooks } from "@devopsplaybook.io/otel-utils";
148
+
149
+ const cleanup = registerShutdownHooks([tracer, logger, meter]);
150
+ // later, if the application manages its own signals:
151
+ // cleanup();
152
+ ```
153
+
154
+ Register it once, with all configured providers. A second signal received while shutting down does not exit or flush again. The returned function only removes the signal listeners.
155
+
156
+ For custom flows, `shutdownAll` flushes every provider and always resolves — a failing provider is reported with `console.warn` and does not block the others:
131
157
 
132
158
  ```typescript
133
- const shutdown = async () => {
134
- await Promise.all([tracer.shutdown(), logger.shutdown(), meter.shutdown()]);
135
- process.exit(0);
136
- };
137
- process.on("SIGTERM", shutdown);
138
- process.on("SIGINT", shutdown);
159
+ import { shutdownAll } from "@devopsplaybook.io/otel-utils";
160
+
161
+ await shutdownAll([tracer, logger, meter]);
162
+ process.exit(0);
139
163
  ```
140
164
 
141
- Each `shutdown()` flushes pending telemetry to the collector before resolving; the methods are idempotent and can be called even when a signal was not configured (they resolve immediately).
165
+ Each `shutdown()` flushes pending telemetry to the collector before resolving; the methods are idempotent and can be called even when no collector was configured (they resolve immediately).
142
166
 
143
167
  ## Internal Utilities
144
168
 
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from "./src/models/ConfigOTelInterface";
2
2
  export * from "./src/models/StandardLoggerInterface";
3
3
  export * from "./src/ModuleLogger";
4
+ export * from "./src/ShutdownHooks";
4
5
  export * from "./src/StandardLogger";
5
6
  export * from "./src/StandardMeter";
6
7
  export * from "./src/StandardTracer";
package/dist/index.js CHANGED
@@ -17,6 +17,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./src/models/ConfigOTelInterface"), exports);
18
18
  __exportStar(require("./src/models/StandardLoggerInterface"), exports);
19
19
  __exportStar(require("./src/ModuleLogger"), exports);
20
+ __exportStar(require("./src/ShutdownHooks"), exports);
20
21
  __exportStar(require("./src/StandardLogger"), exports);
21
22
  __exportStar(require("./src/StandardMeter"), exports);
22
23
  __exportStar(require("./src/StandardTracer"), exports);
@@ -0,0 +1,16 @@
1
+ export interface ShutdownableInterface {
2
+ shutdown(): Promise<void>;
3
+ }
4
+ /**
5
+ * Flushes and shuts down every provider, resolving even when one of them
6
+ * fails (the failure is reported with `console.warn`).
7
+ */
8
+ export declare function shutdownAll(providers: ReadonlyArray<ShutdownableInterface>): Promise<void>;
9
+ /**
10
+ * Registers one-shot `SIGTERM`/`SIGINT` handlers that flush and shut down all
11
+ * providers, then exit the process with code 0.
12
+ *
13
+ * Register once with every configured provider. The returned function removes
14
+ * the handlers without shutting anything down.
15
+ */
16
+ export declare function registerShutdownHooks(providers: ReadonlyArray<ShutdownableInterface>): () => void;
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.shutdownAll = shutdownAll;
4
+ exports.registerShutdownHooks = registerShutdownHooks;
5
+ /**
6
+ * Flushes and shuts down every provider, resolving even when one of them
7
+ * fails (the failure is reported with `console.warn`).
8
+ */
9
+ async function shutdownAll(providers) {
10
+ await Promise.all(providers.map((provider) => provider.shutdown().catch((error) => {
11
+ console.warn("[otel-utils] shutdown failed", error);
12
+ })));
13
+ }
14
+ /**
15
+ * Registers one-shot `SIGTERM`/`SIGINT` handlers that flush and shut down all
16
+ * providers, then exit the process with code 0.
17
+ *
18
+ * Register once with every configured provider. The returned function removes
19
+ * the handlers without shutting anything down.
20
+ */
21
+ function registerShutdownHooks(providers) {
22
+ let shuttingDown = false;
23
+ const onSignal = () => {
24
+ if (shuttingDown) {
25
+ return;
26
+ }
27
+ shuttingDown = true;
28
+ void shutdownAll(providers).then(() => process.exit(0));
29
+ };
30
+ process.once("SIGTERM", onSignal);
31
+ process.once("SIGINT", onSignal);
32
+ return () => {
33
+ process.removeListener("SIGTERM", onSignal);
34
+ process.removeListener("SIGINT", onSignal);
35
+ };
36
+ }
@@ -7,8 +7,12 @@ export declare class StandardLogger implements StandardLoggerInterface {
7
7
  private loggerProvider?;
8
8
  private serviceVersion?;
9
9
  private serviceName?;
10
- initOTel(config: ConfigOTelInterface): void;
10
+ private ownsGlobalProvider;
11
+ private shutdownPromise?;
12
+ initOTel(config: ConfigOTelInterface): Promise<void>;
11
13
  shutdown(): Promise<void>;
12
14
  getLogger(): OTelLogger | undefined;
13
15
  createModuleLogger(moduleName: string): ModuleLogger;
16
+ private registerGlobalProvider;
17
+ private releaseProvider;
14
18
  }
@@ -6,38 +6,51 @@ const exporter_logs_otlp_http_1 = require("@opentelemetry/exporter-logs-otlp-htt
6
6
  const sdk_logs_1 = require("@opentelemetry/sdk-logs");
7
7
  const createResource_1 = require("./utils/createResource");
8
8
  const ModuleLogger_1 = require("./ModuleLogger");
9
+ let globalLoggerProvider;
10
+ const ALREADY_REGISTERED_WARNING = "[otel-utils] A global logger provider is already registered; StandardLogger will not replace it. This instance still logs through its own provider, but logs.getLogger() keeps using the existing provider.";
9
11
  class StandardLogger {
10
- initOTel(config) {
12
+ constructor() {
13
+ this.ownsGlobalProvider = false;
14
+ }
15
+ async initOTel(config) {
11
16
  var _a;
17
+ // Re-initialisation flushes and unregisters the previous provider first.
18
+ if (this.loggerProvider) {
19
+ await this.releaseProvider();
20
+ }
21
+ this.shutdownPromise = undefined;
12
22
  this.serviceName = config.SERVICE_ID;
13
23
  this.serviceVersion = config.VERSION;
14
- if (config.OPENTELEMETRY_COLLECTOR_HTTP_LOGS) {
15
- const exporterHeaders = {};
16
- if (config.OPENTELEMETRY_COLLECT_AUTHORIZATION_HEADER) {
17
- exporterHeaders["Authorization"] =
18
- `Bearer ${config.OPENTELEMETRY_COLLECT_AUTHORIZATION_HEADER}`;
19
- }
20
- const exporter = new exporter_logs_otlp_http_1.OTLPLogExporter({
21
- url: config.OPENTELEMETRY_COLLECTOR_HTTP_LOGS,
22
- headers: exporterHeaders,
23
- });
24
- this.loggerProvider = new sdk_logs_1.LoggerProvider({
25
- processors: [
26
- new sdk_logs_1.BatchLogRecordProcessor({
27
- exporter,
28
- maxQueueSize: 2048,
29
- scheduledDelayMillis: ((_a = config.OPENTELEMETRY_COLLECTOR_EXPORT_LOGS_INTERVAL_SECONDS) !== null && _a !== void 0 ? _a : 60) * 1000,
30
- }),
31
- ],
32
- resource: (0, createResource_1.createOTelResource)(this.serviceName, this.serviceVersion),
33
- });
34
- api_1.logs.setGlobalLoggerProvider(this.loggerProvider);
35
- this.logger = this.loggerProvider.getLogger(`${this.serviceName}:${this.serviceVersion}`);
24
+ if (!config.OPENTELEMETRY_COLLECTOR_HTTP_LOGS) {
25
+ return;
26
+ }
27
+ const exporterHeaders = {};
28
+ if (config.OPENTELEMETRY_COLLECT_AUTHORIZATION_HEADER) {
29
+ exporterHeaders["Authorization"] =
30
+ `Bearer ${config.OPENTELEMETRY_COLLECT_AUTHORIZATION_HEADER}`;
36
31
  }
32
+ const exporter = new exporter_logs_otlp_http_1.OTLPLogExporter({
33
+ url: config.OPENTELEMETRY_COLLECTOR_HTTP_LOGS,
34
+ headers: exporterHeaders,
35
+ });
36
+ this.loggerProvider = new sdk_logs_1.LoggerProvider({
37
+ processors: [
38
+ new sdk_logs_1.BatchLogRecordProcessor({
39
+ exporter,
40
+ maxQueueSize: 2048,
41
+ scheduledDelayMillis: ((_a = config.OPENTELEMETRY_COLLECTOR_EXPORT_LOGS_INTERVAL_SECONDS) !== null && _a !== void 0 ? _a : 60) * 1000,
42
+ }),
43
+ ],
44
+ resource: (0, createResource_1.createOTelResource)(this.serviceName, this.serviceVersion),
45
+ });
46
+ this.logger = this.loggerProvider.getLogger(`${this.serviceName}:${this.serviceVersion}`);
47
+ this.registerGlobalProvider();
37
48
  }
38
- async shutdown() {
39
- var _a;
40
- await ((_a = this.loggerProvider) === null || _a === void 0 ? void 0 : _a.shutdown());
49
+ shutdown() {
50
+ if (!this.shutdownPromise) {
51
+ this.shutdownPromise = this.releaseProvider();
52
+ }
53
+ return this.shutdownPromise;
41
54
  }
42
55
  getLogger() {
43
56
  return this.logger;
@@ -45,5 +58,40 @@ class StandardLogger {
45
58
  createModuleLogger(moduleName) {
46
59
  return new ModuleLogger_1.ModuleLogger(moduleName, this);
47
60
  }
61
+ registerGlobalProvider() {
62
+ if (globalLoggerProvider) {
63
+ if (globalLoggerProvider === api_1.logs.getLoggerProvider()) {
64
+ console.warn(ALREADY_REGISTERED_WARNING);
65
+ return;
66
+ }
67
+ // The previous provider is no longer registered (disabled elsewhere).
68
+ globalLoggerProvider = undefined;
69
+ }
70
+ if (this.loggerProvider &&
71
+ api_1.logs.setGlobalLoggerProvider(this.loggerProvider)) {
72
+ globalLoggerProvider = this.loggerProvider;
73
+ this.ownsGlobalProvider = true;
74
+ }
75
+ else {
76
+ console.warn(ALREADY_REGISTERED_WARNING);
77
+ }
78
+ }
79
+ async releaseProvider() {
80
+ const provider = this.loggerProvider;
81
+ const ownedGlobalProvider = this.ownsGlobalProvider;
82
+ this.logger = undefined;
83
+ this.loggerProvider = undefined;
84
+ this.ownsGlobalProvider = false;
85
+ if (!provider) {
86
+ return;
87
+ }
88
+ await provider.shutdown();
89
+ if (ownedGlobalProvider && api_1.logs.getLoggerProvider() === provider) {
90
+ api_1.logs.disable();
91
+ }
92
+ if (globalLoggerProvider === provider) {
93
+ globalLoggerProvider = undefined;
94
+ }
95
+ }
48
96
  }
49
97
  exports.StandardLogger = StandardLogger;
@@ -15,10 +15,14 @@ export declare class StandardMeter {
15
15
  private meterProvider;
16
16
  private serviceVersion;
17
17
  private serviceName;
18
+ private ownsGlobalProvider;
19
+ private shutdownPromise?;
18
20
  constructor(config: ConfigOTelInterface);
19
21
  shutdown(): Promise<void>;
20
22
  createCounter(key: string): Counter;
21
23
  createUpDownCounter(key: string): UpDownCounter;
22
24
  createHistogram(key: string, options?: StandardMeterCreateHistogramOptions): Histogram;
23
25
  createObservableGauge(key: string, callback: (observableResult: any) => void, description?: string): ObservableGauge;
26
+ private registerGlobalProvider;
27
+ private doShutdown;
24
28
  }
@@ -1,15 +1,21 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.StandardMeter = void 0;
4
+ const api_1 = require("@opentelemetry/api");
4
5
  const exporter_metrics_otlp_http_1 = require("@opentelemetry/exporter-metrics-otlp-http");
5
6
  const createResource_1 = require("./utils/createResource");
6
7
  const sdk_metrics_1 = require("@opentelemetry/sdk-metrics");
8
+ let globalMeterProvider;
9
+ const ALREADY_REGISTERED_WARNING = "[otel-utils] A global meter provider is already registered; StandardMeter will not replace it. This instance still records metrics through its own provider, but metrics.getMeter() keeps using the existing provider.";
7
10
  class StandardMeter {
8
11
  constructor(config) {
9
12
  var _a;
13
+ this.ownsGlobalProvider = false;
10
14
  this.serviceName = config.SERVICE_ID;
11
15
  this.serviceVersion = config.VERSION;
16
+ let hasCollector = false;
12
17
  if (config.OPENTELEMETRY_COLLECTOR_HTTP_METRICS) {
18
+ hasCollector = true;
13
19
  const collectorOptions = {
14
20
  url: config.OPENTELEMETRY_COLLECTOR_HTTP_METRICS,
15
21
  headers: {},
@@ -36,9 +42,15 @@ class StandardMeter {
36
42
  });
37
43
  }
38
44
  this.meter = this.meterProvider.getMeter(`${this.serviceName}:${this.serviceVersion}`);
45
+ if (hasCollector) {
46
+ this.registerGlobalProvider();
47
+ }
39
48
  }
40
- async shutdown() {
41
- await this.meterProvider.shutdown();
49
+ shutdown() {
50
+ if (!this.shutdownPromise) {
51
+ this.shutdownPromise = this.doShutdown();
52
+ }
53
+ return this.shutdownPromise;
42
54
  }
43
55
  createCounter(key) {
44
56
  return this.meter.createCounter(`${this.serviceName}.${key}`);
@@ -56,5 +68,33 @@ class StandardMeter {
56
68
  observableGauge.addCallback(callback);
57
69
  return observableGauge;
58
70
  }
71
+ registerGlobalProvider() {
72
+ if (globalMeterProvider) {
73
+ if (globalMeterProvider === api_1.metrics.getMeterProvider()) {
74
+ console.warn(ALREADY_REGISTERED_WARNING);
75
+ return;
76
+ }
77
+ // The previous provider is no longer registered (disabled elsewhere).
78
+ globalMeterProvider = undefined;
79
+ }
80
+ if (api_1.metrics.setGlobalMeterProvider(this.meterProvider)) {
81
+ globalMeterProvider = this.meterProvider;
82
+ this.ownsGlobalProvider = true;
83
+ }
84
+ else {
85
+ console.warn(ALREADY_REGISTERED_WARNING);
86
+ }
87
+ }
88
+ async doShutdown() {
89
+ await this.meterProvider.shutdown();
90
+ if (this.ownsGlobalProvider &&
91
+ api_1.metrics.getMeterProvider() === this.meterProvider) {
92
+ api_1.metrics.disable();
93
+ }
94
+ if (globalMeterProvider === this.meterProvider) {
95
+ globalMeterProvider = undefined;
96
+ }
97
+ this.ownsGlobalProvider = false;
98
+ }
59
99
  }
60
100
  exports.StandardMeter = StandardMeter;
@@ -22,8 +22,12 @@ export declare class StandardTracer {
22
22
  private tracer;
23
23
  private serviceVersion;
24
24
  private serviceName;
25
+ private ownsGlobalProvider;
26
+ private shutdownPromise?;
25
27
  constructor(config: ConfigOTelInterface);
26
28
  shutdown(): Promise<void>;
27
29
  startSpan(name: string, parentSpan?: Span, options?: StandardTracerStartSpanOptions): Span;
28
30
  static updateHttpHeader(context: ApiSpan, headers?: {}): any;
31
+ private registerGlobalProvider;
32
+ private doShutdown;
29
33
  }
@@ -1,40 +1,7 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
2
  Object.defineProperty(exports, "__esModule", { value: true });
36
3
  exports.StandardTracer = void 0;
37
- const api_1 = __importStar(require("@opentelemetry/api"));
4
+ const api_1 = require("@opentelemetry/api");
38
5
  const context_async_hooks_1 = require("@opentelemetry/context-async-hooks");
39
6
  const core_1 = require("@opentelemetry/core");
40
7
  const exporter_trace_otlp_http_1 = require("@opentelemetry/exporter-trace-otlp-http");
@@ -42,8 +9,18 @@ const id_generator_aws_xray_1 = require("@opentelemetry/id-generator-aws-xray");
42
9
  const sdk_trace_1 = require("@opentelemetry/sdk-trace");
43
10
  const semantic_conventions_1 = require("@opentelemetry/semantic-conventions");
44
11
  const createResource_1 = require("./utils/createResource");
12
+ let globalTracerProvider;
13
+ let contextManagerInitialized = false;
14
+ function getRegisteredTracerProvider() {
15
+ const provider = api_1.trace.getTracerProvider();
16
+ return typeof provider.getDelegate === "function"
17
+ ? provider.getDelegate()
18
+ : provider;
19
+ }
20
+ const ALREADY_REGISTERED_WARNING = "[otel-utils] A global tracer provider is already registered; StandardTracer will not replace it. This instance still exports spans through its own provider, but trace.getTracer() keeps using the existing provider.";
45
21
  class StandardTracer {
46
22
  constructor(config) {
23
+ this.ownsGlobalProvider = false;
47
24
  this.serviceName = config.SERVICE_ID;
48
25
  this.serviceVersion = config.VERSION;
49
26
  const spanProcessors = [];
@@ -64,21 +41,22 @@ class StandardTracer {
64
41
  resource: (0, createResource_1.createOTelResource)(this.serviceName, this.serviceVersion),
65
42
  spanProcessors,
66
43
  });
67
- // OTel v3 removed TracerProvider.register(): the provider and the context
68
- // manager must be installed on the global API explicitly.
69
- api_1.trace.setGlobalTracerProvider(this.traceProvider);
70
- const contextManager = new context_async_hooks_1.AsyncLocalStorageContextManager();
71
- contextManager.enable();
72
- api_1.default.context.setGlobalContextManager(contextManager);
73
- this.tracer = api_1.default.trace.getTracer(`${this.serviceName}:${this.serviceVersion}`);
44
+ // Resolve the tracer from this instance's provider: the global API may be
45
+ // owned by another instance and would silently export through it.
46
+ this.tracer = this.traceProvider.getTracer(`${this.serviceName}:${this.serviceVersion}`);
47
+ this.registerGlobalProvider();
48
+ initializeContextManager();
74
49
  }
75
- async shutdown() {
76
- await this.traceProvider.shutdown();
50
+ shutdown() {
51
+ if (!this.shutdownPromise) {
52
+ this.shutdownPromise = this.doShutdown();
53
+ }
54
+ return this.shutdownPromise;
77
55
  }
78
56
  startSpan(name, parentSpan, options) {
79
57
  const sanitizedName = name.replace(StandardTracer.SPAN_NAME_SANITIZE_RE, "_");
80
58
  if (parentSpan) {
81
- return this.tracer.startSpan(sanitizedName, options, api_1.default.trace.setSpan(api_1.default.context.active(), parentSpan));
59
+ return this.tracer.startSpan(sanitizedName, options, api_1.trace.setSpan(api_1.context.active(), parentSpan));
82
60
  }
83
61
  if (options) {
84
62
  return this.tracer.startSpan(sanitizedName, options);
@@ -95,7 +73,48 @@ class StandardTracer {
95
73
  headers, api_1.defaultTextMapSetter);
96
74
  return headers;
97
75
  }
76
+ registerGlobalProvider() {
77
+ if (globalTracerProvider) {
78
+ if (getRegisteredTracerProvider() === globalTracerProvider) {
79
+ console.warn(ALREADY_REGISTERED_WARNING);
80
+ return;
81
+ }
82
+ // The previous provider is no longer registered (disabled elsewhere).
83
+ globalTracerProvider = undefined;
84
+ }
85
+ if (api_1.trace.setGlobalTracerProvider(this.traceProvider)) {
86
+ globalTracerProvider = this.traceProvider;
87
+ this.ownsGlobalProvider = true;
88
+ }
89
+ else {
90
+ console.warn(ALREADY_REGISTERED_WARNING);
91
+ }
92
+ }
93
+ async doShutdown() {
94
+ await this.traceProvider.shutdown();
95
+ if (this.ownsGlobalProvider &&
96
+ getRegisteredTracerProvider() === this.traceProvider) {
97
+ api_1.trace.disable();
98
+ }
99
+ if (globalTracerProvider === this.traceProvider) {
100
+ globalTracerProvider = undefined;
101
+ }
102
+ this.ownsGlobalProvider = false;
103
+ }
98
104
  }
99
105
  exports.StandardTracer = StandardTracer;
100
106
  StandardTracer.SPAN_NAME_SANITIZE_RE = /[^a-zA-Z0-9-_/]/g;
101
107
  StandardTracer.PROPAGATOR = new core_1.W3CTraceContextPropagator();
108
+ function initializeContextManager() {
109
+ if (contextManagerInitialized) {
110
+ return;
111
+ }
112
+ contextManagerInitialized = true;
113
+ const contextManager = new context_async_hooks_1.AsyncLocalStorageContextManager();
114
+ if (api_1.context.setGlobalContextManager(contextManager)) {
115
+ contextManager.enable();
116
+ }
117
+ else {
118
+ console.warn("[otel-utils] A global context manager is already registered; StandardTracer will keep using it for context propagation.");
119
+ }
120
+ }
package/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from "./src/models/ConfigOTelInterface";
2
2
  export * from "./src/models/StandardLoggerInterface";
3
3
  export * from "./src/ModuleLogger";
4
+ export * from "./src/ShutdownHooks";
4
5
  export * from "./src/StandardLogger";
5
6
  export * from "./src/StandardMeter";
6
7
  export * from "./src/StandardTracer";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devopsplaybook.io/otel-utils",
3
- "version": "1.5.3",
3
+ "version": "1.6.0-beta.37.40daec3",
4
4
  "description": "Utility to simplify integration with Open Telemetry",
5
5
  "keywords": [
6
6
  "Open",