@arizeai/phoenix-otel 0.4.2 → 0.4.3

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.
@@ -0,0 +1,99 @@
1
+ ---
2
+ title: "Manual Spans"
3
+ description: "Create manual spans with @arizeai/phoenix-otel"
4
+ ---
5
+
6
+ The package re-exports `trace`, `context`, and `SpanStatusCode` from OpenTelemetry so you can create custom spans after registration without pulling them from another package.
7
+
8
+ For most use cases, prefer the `@arizeai/openinference-core` helpers (`withSpan`, `traceChain`, `traceAgent`, `traceTool`) over raw `startActiveSpan`. They automatically set the correct OpenInference span kind, handle errors, and close the span for you.
9
+
10
+ <section className="hidden" data-agent-context="relevant-source-files" aria-label="Relevant source files">
11
+ <h2>Relevant Source Files</h2>
12
+ <ul>
13
+ <li><code>src/index.ts</code></li>
14
+ </ul>
15
+ </section>
16
+
17
+ ## Recommended — OpenInference Helpers
18
+
19
+ ```bash
20
+ npm install @arizeai/openinference-core @arizeai/openinference-semantic-conventions
21
+ ```
22
+
23
+ ```ts
24
+ import { withSpan, setSession, setMetadata } from "@arizeai/openinference-core";
25
+ import { OpenInferenceSpanKind } from "@arizeai/openinference-semantic-conventions";
26
+ import { context } from "@opentelemetry/api";
27
+ import { register } from "@arizeai/phoenix-otel";
28
+
29
+ register({ projectName: "support-bot" });
30
+
31
+ const retrieveDocs = withSpan(
32
+ async (query: string) => {
33
+ const response = await fetch(`/api/search?q=${query}`);
34
+ return response.json();
35
+ },
36
+ { name: "retrieve-docs", kind: OpenInferenceSpanKind.RETRIEVER }
37
+ );
38
+
39
+ const generateAnswer = withSpan(
40
+ async (query: string, docs: string[]) => {
41
+ return `Answer based on ${docs.length} documents`;
42
+ },
43
+ { name: "generate-answer", kind: OpenInferenceSpanKind.LLM }
44
+ );
45
+
46
+ const ragPipeline = withSpan(
47
+ async (query: string) => {
48
+ const docs = await retrieveDocs(query);
49
+ return generateAnswer(query, docs);
50
+ },
51
+ { name: "rag-pipeline", kind: OpenInferenceSpanKind.CHAIN }
52
+ );
53
+
54
+ // Session and metadata propagate to all child spans
55
+ await context.with(
56
+ setMetadata(
57
+ setSession(context.active(), { sessionId: "session-abc-123" }),
58
+ { environment: "production" }
59
+ ),
60
+ () => ragPipeline("What is Phoenix?")
61
+ );
62
+ ```
63
+
64
+ ## Raw OpenTelemetry Spans
65
+
66
+ Use raw spans when you need full control over attributes and timing:
67
+
68
+ ```ts
69
+ import {
70
+ SpanStatusCode,
71
+ register,
72
+ trace,
73
+ } from "@arizeai/phoenix-otel";
74
+
75
+ register({ projectName: "support-bot" });
76
+
77
+ const tracer = trace.getTracer("support-bot");
78
+
79
+ await tracer.startActiveSpan("lookup-customer", async (span) => {
80
+ try {
81
+ span.setAttribute("customer.id", "cust_123");
82
+ await Promise.resolve();
83
+ } catch (error) {
84
+ span.recordException(error as Error);
85
+ span.setStatus({ code: SpanStatusCode.ERROR });
86
+ throw error;
87
+ } finally {
88
+ span.end();
89
+ }
90
+ });
91
+ ```
92
+
93
+ <section className="hidden" data-agent-context="source-map" aria-label="Source map">
94
+ <h2>Source Map</h2>
95
+ <ul>
96
+ <li><code>src/index.ts</code></li>
97
+ <li><code>src/register.ts</code></li>
98
+ </ul>
99
+ </section>
@@ -0,0 +1,248 @@
1
+ ---
2
+ title: "Overview"
3
+ description: "Bundled docs for @arizeai/phoenix-otel"
4
+ ---
5
+
6
+ `@arizeai/phoenix-otel` is the Phoenix-focused OpenTelemetry package for Node.js. It wraps provider setup, span export, instrumentation registration, and a few convenience utilities for Phoenix-specific tracing workflows.
7
+
8
+ ## Install
9
+
10
+ Install the package and any OpenTelemetry instrumentations you want to use with it.
11
+
12
+ ```bash
13
+ npm install @arizeai/phoenix-otel
14
+ ```
15
+
16
+ ### Recommended — Add OpenInference Core
17
+
18
+ `@arizeai/openinference-core` provides tracing helpers (`withSpan`, `traceChain`, `traceAgent`, `traceTool`), decorators (`@observe`), and context setters (`setSession`, `setUser`, `setMetadata`, `setTag`) that work on top of the provider registered by phoenix-otel. Install it alongside phoenix-otel for the best tracing experience.
19
+
20
+ ```bash
21
+ npm install @arizeai/phoenix-otel @arizeai/openinference-core
22
+ ```
23
+
24
+ ### Add Instrumentations
25
+
26
+ ```bash
27
+ npm install \
28
+ @arizeai/phoenix-otel \
29
+ @opentelemetry/instrumentation-http \
30
+ @opentelemetry/instrumentation-express
31
+ ```
32
+
33
+ ### Typical Pairings
34
+
35
+ - `@arizeai/phoenix-otel` plus `@arizeai/openinference-core` for manual tracing with span-kind helpers and context propagation
36
+ - `@arizeai/phoenix-otel` plus OpenTelemetry instrumentations for automatic framework tracing
37
+ - `@arizeai/phoenix-otel` plus `@arizeai/phoenix-client` for experiment workflows
38
+ - `@arizeai/phoenix-otel` plus framework-specific OpenInference instrumentors (e.g. `@arizeai/openinference-instrumentation-openai`)
39
+
40
+ ## Quick Start
41
+
42
+ ```ts
43
+ import { register } from "@arizeai/phoenix-otel";
44
+ import { withSpan } from "@arizeai/openinference-core";
45
+ import { OpenInferenceSpanKind } from "@arizeai/openinference-semantic-conventions";
46
+
47
+ const provider = register({ projectName: "support-bot" });
48
+
49
+ const handleQuery = withSpan(
50
+ async (query: string) => {
51
+ return `Handled: ${query}`;
52
+ },
53
+ { name: "handle-query", kind: OpenInferenceSpanKind.CHAIN }
54
+ );
55
+
56
+ await handleQuery("Hello");
57
+ await provider.shutdown();
58
+ ```
59
+
60
+ ## Docs And Source In `node_modules`
61
+
62
+ After install, a coding agent can inspect the installed package directly:
63
+
64
+ ```text
65
+ node_modules/@arizeai/phoenix-otel/docs/
66
+ node_modules/@arizeai/phoenix-otel/src/
67
+ ```
68
+
69
+ The bundled docs cover configuration, instrumentation, manual spans, and troubleshooting.
70
+
71
+ ## Configuration
72
+
73
+ `register()` can use explicit options, environment variables, or both. If you pass a Phoenix base URL, the package normalizes it to the OTLP collector endpoint at `/v1/traces`.
74
+
75
+ ### Environment-Driven Setup
76
+
77
+ ```bash
78
+ export PHOENIX_COLLECTOR_ENDPOINT=http://localhost:6006
79
+ export PHOENIX_API_KEY=<your-api-key>
80
+ ```
81
+
82
+ ```ts
83
+ import { register } from "@arizeai/phoenix-otel";
84
+
85
+ const provider = register({
86
+ projectName: "support-bot",
87
+ });
88
+ ```
89
+
90
+ ### Explicit Setup
91
+
92
+ ```ts
93
+ import { DiagLogLevel, register } from "@arizeai/phoenix-otel";
94
+
95
+ const provider = register({
96
+ projectName: "support-bot",
97
+ url: "https://app.phoenix.arize.com",
98
+ apiKey: process.env.PHOENIX_API_KEY,
99
+ headers: {
100
+ "x-client-name": "support-bot-api",
101
+ "x-client-version": "1.4.2",
102
+ },
103
+ batch: true,
104
+ diagLogLevel: DiagLogLevel.INFO,
105
+ });
106
+ ```
107
+
108
+ ### Key Options
109
+
110
+ | Option | Description |
111
+ |--------|-------------|
112
+ | `projectName` | Phoenix project name attached to exported spans. Defaults to `default`. |
113
+ | `url` | Phoenix base URL or full collector endpoint. The package appends `/v1/traces` when needed. |
114
+ | `apiKey` | API key sent as a bearer token. Falls back to `PHOENIX_API_KEY`. |
115
+ | `headers` | Additional OTLP headers merged with the auth header. |
116
+ | `batch` | `true` for batched export, `false` for immediate export during debugging. |
117
+ | `instrumentations` | OpenTelemetry instrumentations to register with the provider. |
118
+ | `spanProcessors` | Custom span processors. When provided, these replace the default Phoenix exporter setup. |
119
+ | `global` | Whether to attach the provider to the global OpenTelemetry APIs automatically. |
120
+ | `diagLogLevel` | OpenTelemetry diagnostic logging level. |
121
+
122
+ ### Environment Variables
123
+
124
+ | Variable | Use |
125
+ |--------|-----|
126
+ | `PHOENIX_COLLECTOR_ENDPOINT` | Base Phoenix collector URL or full OTLP trace endpoint. |
127
+ | `PHOENIX_API_KEY` | API key used when you do not pass `apiKey` explicitly. |
128
+
129
+ ## Instrumentation
130
+
131
+ Pass OpenTelemetry instrumentations to `register()` to capture framework and library activity automatically.
132
+
133
+ ```ts
134
+ import { register } from "@arizeai/phoenix-otel";
135
+ import { ExpressInstrumentation } from "@opentelemetry/instrumentation-express";
136
+ import { HttpInstrumentation } from "@opentelemetry/instrumentation-http";
137
+
138
+ register({
139
+ projectName: "support-bot",
140
+ instrumentations: [
141
+ new HttpInstrumentation(),
142
+ new ExpressInstrumentation(),
143
+ ],
144
+ });
145
+ ```
146
+
147
+ - the package also re-exports `registerInstrumentations`
148
+ - auto-instrumentation setups often work best when they are initialized very early in process startup
149
+ - for custom providers, you can pass your own `spanProcessors`
150
+
151
+ ## Production
152
+
153
+ ### Enable Batch Processing
154
+
155
+ Always set `batch: true` (the default) in production. The batch span processor groups spans before export, improving compression and reducing the number of outgoing network requests. Disable batching only during local debugging when you need spans to appear immediately.
156
+
157
+ ```ts
158
+ const provider = register({
159
+ projectName: "support-bot",
160
+ url: "https://app.phoenix.arize.com",
161
+ apiKey: process.env.PHOENIX_API_KEY,
162
+ batch: true, // default — groups spans for efficient export
163
+ });
164
+ ```
165
+
166
+ ### Provider Lifecycle
167
+
168
+ In production you must flush and shut down the provider before your process exits, otherwise in-flight batches are lost.
169
+
170
+ ```ts
171
+ process.on("SIGTERM", async () => {
172
+ await provider.shutdown(); // flushes buffered spans, then tears down
173
+ process.exit(0);
174
+ });
175
+ ```
176
+
177
+ For short-lived processes (scripts, serverless functions, CLI tools) call `shutdown()` at the end of your work:
178
+
179
+ ```ts
180
+ await doWork();
181
+ await provider.shutdown(); // ensures the final batch is exported
182
+ ```
183
+
184
+ ### Custom Span Processors
185
+
186
+ If you need full control over export (for example, sending spans to a secondary backend), pass your own `spanProcessors`. This replaces the default Phoenix exporter setup entirely.
187
+
188
+ ```ts
189
+ import { register } from "@arizeai/phoenix-otel";
190
+ import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
191
+ import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
192
+
193
+ const exporter = new OTLPTraceExporter({
194
+ url: "https://app.phoenix.arize.com/v1/traces",
195
+ headers: { Authorization: `Bearer ${process.env.PHOENIX_API_KEY}` },
196
+ });
197
+
198
+ const provider = register({
199
+ projectName: "support-bot",
200
+ spanProcessors: [new BatchSpanProcessor(exporter)],
201
+ });
202
+ ```
203
+
204
+ ## Troubleshooting
205
+
206
+ When traces do not show up in Phoenix, check these common issues:
207
+
208
+ - confirm `projectName` is the project you are inspecting
209
+ - confirm `url` or `PHOENIX_COLLECTOR_ENDPOINT` points at the collector
210
+ - confirm `PHOENIX_API_KEY` is present for authenticated environments
211
+ - confirm registration happens before the instrumented work starts
212
+ - call `await provider.shutdown()` before process exit when using batched export
213
+
214
+ ### Enable Diagnostic Logging
215
+
216
+ ```ts
217
+ import { DiagLogLevel, register } from "@arizeai/phoenix-otel";
218
+
219
+ const provider = register({
220
+ projectName: "support-bot",
221
+ batch: false,
222
+ diagLogLevel: DiagLogLevel.DEBUG,
223
+ });
224
+
225
+ await provider.shutdown();
226
+ ```
227
+
228
+ ## Package Surface
229
+
230
+ - `register()` creates a `NodeTracerProvider` and Phoenix exporter setup
231
+ - `trace`, `SpanStatusCode`, and other OpenTelemetry re-exports support manual tracing
232
+ - `registerInstrumentations()` wires automatic instrumentation into the same provider
233
+ - `objectAsAttributes()` converts JSON-like objects into OpenTelemetry attribute maps
234
+
235
+ ## Where To Start
236
+
237
+ - [Manual spans](./manual-spans) for tracing with `@arizeai/openinference-core` helpers and raw OTel spans
238
+
239
+ <section className="hidden" data-agent-context="source-map" aria-label="Source map">
240
+ <h2>Source Map</h2>
241
+ <ul>
242
+ <li><code>src/index.ts</code></li>
243
+ <li><code>src/register.ts</code></li>
244
+ <li><code>src/config.ts</code></li>
245
+ <li><code>src/utils.ts</code></li>
246
+ <li><code>src/createNoOpProvider.ts</code></li>
247
+ </ul>
248
+ </section>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arizeai/phoenix-otel",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "Otel registration and convenience methods",
5
5
  "keywords": [
6
6
  "arize",
@@ -19,8 +19,12 @@
19
19
  "type": "git",
20
20
  "url": "git+https://github.com/Arize-ai/phoenix.git"
21
21
  },
22
+ "directories": {
23
+ "doc": "./docs"
24
+ },
22
25
  "files": [
23
26
  "dist",
27
+ "docs",
24
28
  "src",
25
29
  "package.json"
26
30
  ],
@@ -37,6 +41,7 @@
37
41
  "@arizeai/openinference-semantic-conventions": "^1.1.0",
38
42
  "@arizeai/openinference-vercel": "^2.7.0",
39
43
  "@opentelemetry/api": "^1.9.0",
44
+ "@opentelemetry/context-async-hooks": "^2.5.1",
40
45
  "@opentelemetry/core": "^1.25.1",
41
46
  "@opentelemetry/exporter-trace-otlp-proto": "^0.205.0",
42
47
  "@opentelemetry/instrumentation": "^0.57.2",
@@ -46,7 +51,7 @@
46
51
  },
47
52
  "devDependencies": {
48
53
  "@types/node": "^24.9.1",
49
- "vitest": "^4.0.10"
54
+ "vitest": "^4.1.0"
50
55
  },
51
56
  "scripts": {
52
57
  "build": "tsc --build tsconfig.json tsconfig.esm.json && tsc-alias -p tsconfig.esm.json",
package/src/index.ts CHANGED
@@ -5,6 +5,7 @@ export {
5
5
  type Tracer,
6
6
  SpanStatusCode,
7
7
  } from "@opentelemetry/api";
8
+ export { suppressTracing } from "@opentelemetry/core";
8
9
  export { registerInstrumentations } from "@opentelemetry/instrumentation";
9
10
  export { type Instrumentation } from "@opentelemetry/instrumentation";
10
11
  export { type NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
package/src/register.ts CHANGED
@@ -4,7 +4,22 @@ import {
4
4
  OpenInferenceSimpleSpanProcessor,
5
5
  } from "@arizeai/openinference-vercel";
6
6
  import type { DiagLogLevel } from "@opentelemetry/api";
7
- import { diag, DiagConsoleLogger } from "@opentelemetry/api";
7
+ import {
8
+ context,
9
+ type ContextManager,
10
+ diag,
11
+ DiagConsoleLogger,
12
+ propagation,
13
+ type TextMapPropagator,
14
+ trace,
15
+ type TracerProvider,
16
+ } from "@opentelemetry/api";
17
+ import { AsyncLocalStorageContextManager } from "@opentelemetry/context-async-hooks";
18
+ import {
19
+ CompositePropagator,
20
+ W3CBaggagePropagator,
21
+ W3CTraceContextPropagator,
22
+ } from "@opentelemetry/core";
8
23
  import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
9
24
  import type { Instrumentation } from "@opentelemetry/instrumentation";
10
25
  import { registerInstrumentations } from "@opentelemetry/instrumentation";
@@ -153,6 +168,214 @@ export type RegisterParams = {
153
168
  diagLogLevel?: DiagLogLevel;
154
169
  };
155
170
 
171
+ export type GlobalTracerProviderRegistration = {
172
+ /**
173
+ * Detach the global tracer provider and reset global tracing APIs to no-op state.
174
+ *
175
+ * This clears the OpenTelemetry tracer provider, context manager, and propagator
176
+ * so a different provider can be attached later in the same process.
177
+ */
178
+ detach: () => void;
179
+ };
180
+
181
+ type GlobalTelemetrySnapshot = {
182
+ contextManager?: ContextManager;
183
+ propagator?: TextMapPropagator;
184
+ tracerProvider?: TracerProvider;
185
+ };
186
+
187
+ type OpenTelemetryGlobalState = {
188
+ context?: ContextManager;
189
+ propagation?: TextMapPropagator;
190
+ trace?: TracerProvider;
191
+ version?: string;
192
+ };
193
+
194
+ type GlobalTracerProviderMount = {
195
+ id: number;
196
+ provider: NodeTracerProvider;
197
+ };
198
+
199
+ let nextGlobalTracerProviderMountId = 0;
200
+ let managedGlobalBaseSnapshot: GlobalTelemetrySnapshot | null = null;
201
+ const managedGlobalTracerProviderMounts: GlobalTracerProviderMount[] = [];
202
+ // OpenTelemetry v1 stores globals on this well-known symbol.
203
+ const OTEL_GLOBAL_SYMBOL = Symbol.for("opentelemetry.js.api.1");
204
+
205
+ function getOpenTelemetryGlobalState(): OpenTelemetryGlobalState | undefined {
206
+ return (
207
+ globalThis as typeof globalThis & {
208
+ [OTEL_GLOBAL_SYMBOL]?: OpenTelemetryGlobalState;
209
+ }
210
+ )[OTEL_GLOBAL_SYMBOL];
211
+ }
212
+
213
+ function getGlobalTelemetrySnapshot(): GlobalTelemetrySnapshot {
214
+ const globalState = getOpenTelemetryGlobalState();
215
+
216
+ return {
217
+ tracerProvider: globalState?.trace,
218
+ contextManager: globalState?.context,
219
+ propagator: globalState?.propagation,
220
+ };
221
+ }
222
+
223
+ function clearGlobalTelemetry(): void {
224
+ trace.disable();
225
+ context.disable();
226
+ propagation.disable();
227
+ }
228
+
229
+ function restoreGlobalTelemetrySnapshot(
230
+ snapshot: GlobalTelemetrySnapshot | null
231
+ ): void {
232
+ clearGlobalTelemetry();
233
+
234
+ if (!snapshot) {
235
+ return;
236
+ }
237
+
238
+ if (snapshot.tracerProvider) {
239
+ trace.setGlobalTracerProvider(snapshot.tracerProvider);
240
+ }
241
+
242
+ if (snapshot.contextManager) {
243
+ context.setGlobalContextManager(snapshot.contextManager);
244
+ }
245
+
246
+ if (snapshot.propagator) {
247
+ propagation.setGlobalPropagator(snapshot.propagator);
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Sets the given provider as the global OpenTelemetry provider using this
253
+ * module's own imports of `trace`, `context`, and `propagation`.
254
+ *
255
+ * We deliberately avoid calling `provider.register()` because the SDK's
256
+ * internal import of `@opentelemetry/api` may resolve to a different module
257
+ * instance in pnpm workspaces (different symlink paths → different module
258
+ * identity). By routing all global-state mutations through **our** copy of
259
+ * the OTEL API, snapshot/restore stays consistent.
260
+ */
261
+ function setGlobalProvider(provider: NodeTracerProvider): void {
262
+ trace.setGlobalTracerProvider(provider);
263
+
264
+ const contextManager = new AsyncLocalStorageContextManager();
265
+ contextManager.enable();
266
+ context.setGlobalContextManager(contextManager);
267
+
268
+ propagation.setGlobalPropagator(
269
+ new CompositePropagator({
270
+ propagators: [
271
+ new W3CTraceContextPropagator(),
272
+ new W3CBaggagePropagator(),
273
+ ],
274
+ })
275
+ );
276
+ }
277
+
278
+ function restorePreviousManagedGlobalTracerProvider(): void {
279
+ const previousMount =
280
+ managedGlobalTracerProviderMounts[
281
+ managedGlobalTracerProviderMounts.length - 1
282
+ ];
283
+
284
+ if (previousMount) {
285
+ clearGlobalTelemetry();
286
+ setGlobalProvider(previousMount.provider);
287
+ return;
288
+ }
289
+
290
+ restoreGlobalTelemetrySnapshot(managedGlobalBaseSnapshot);
291
+ managedGlobalBaseSnapshot = null;
292
+ }
293
+
294
+ function detachManagedGlobalTracerProvider(mountId?: number): void {
295
+ if (managedGlobalTracerProviderMounts.length === 0) {
296
+ clearGlobalTelemetry();
297
+ return;
298
+ }
299
+
300
+ const mountIndex =
301
+ mountId == null
302
+ ? managedGlobalTracerProviderMounts.length - 1
303
+ : managedGlobalTracerProviderMounts.findIndex(
304
+ (mount) => mount.id === mountId
305
+ );
306
+
307
+ if (mountIndex === -1) {
308
+ return;
309
+ }
310
+
311
+ const isTopMount =
312
+ mountIndex === managedGlobalTracerProviderMounts.length - 1;
313
+ managedGlobalTracerProviderMounts.splice(mountIndex, 1);
314
+
315
+ if (!isTopMount) {
316
+ return;
317
+ }
318
+
319
+ restorePreviousManagedGlobalTracerProvider();
320
+ }
321
+
322
+ /**
323
+ * Detaches the current global OpenTelemetry tracer provider and resets the
324
+ * global trace, context, and propagation APIs.
325
+ */
326
+ export function detachGlobalTracerProvider(): void {
327
+ detachManagedGlobalTracerProvider();
328
+ }
329
+
330
+ /**
331
+ * Attaches an existing tracer provider as the global OpenTelemetry provider.
332
+ *
333
+ * Returns a handle that can be used to detach it later so another provider can
334
+ * be attached in the same process.
335
+ */
336
+ export function attachGlobalTracerProvider(
337
+ provider: NodeTracerProvider
338
+ ): GlobalTracerProviderRegistration {
339
+ if (managedGlobalTracerProviderMounts.length === 0) {
340
+ managedGlobalBaseSnapshot = getGlobalTelemetrySnapshot();
341
+ }
342
+
343
+ const mountId = nextGlobalTracerProviderMountId++;
344
+ managedGlobalTracerProviderMounts.push({
345
+ id: mountId,
346
+ provider,
347
+ });
348
+
349
+ clearGlobalTelemetry();
350
+ setGlobalProvider(provider);
351
+
352
+ return {
353
+ detach: () => {
354
+ detachManagedGlobalTracerProvider(mountId);
355
+ },
356
+ };
357
+ }
358
+
359
+ function bindGlobalTracerProviderRegistrationToShutdown({
360
+ provider,
361
+ registration,
362
+ }: {
363
+ provider: NodeTracerProvider;
364
+ registration: GlobalTracerProviderRegistration;
365
+ }): void {
366
+ const shutdown = provider.shutdown.bind(provider);
367
+ let hasDetachedRegistration = false;
368
+
369
+ provider.shutdown = async (): Promise<void> => {
370
+ if (!hasDetachedRegistration) {
371
+ hasDetachedRegistration = true;
372
+ registration.detach();
373
+ }
374
+
375
+ await shutdown();
376
+ };
377
+ }
378
+
156
379
  /**
157
380
  * Registers Phoenix OpenTelemetry tracing with the specified configuration.
158
381
  *
@@ -261,7 +484,11 @@ export function register(params: RegisterParams): NodeTracerProvider {
261
484
  });
262
485
  }
263
486
  if (global) {
264
- provider.register();
487
+ const registration = attachGlobalTracerProvider(provider);
488
+ bindGlobalTracerProviderRegistrationToShutdown({
489
+ provider,
490
+ registration,
491
+ });
265
492
  }
266
493
  return provider;
267
494
  }