Skip to content

Commit 92fa55d

Browse files
fix(otel): mark peer dependencies as optional (#1812)
1 parent 0a8fbf6 commit 92fa55d

File tree

9 files changed

+107
-26
lines changed

9 files changed

+107
-26
lines changed

packages/interceptors-opentelemetry/package.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,14 @@
2727
"@temporalio/workflow": "file:../workflow"
2828
},
2929
"peerDependencies": {
30-
"@temporalio/activity": "file:../activity",
31-
"@temporalio/client": "file:../client",
3230
"@temporalio/common": "file:../common",
33-
"@temporalio/worker": "file:../worker",
3431
"@temporalio/workflow": "file:../workflow"
3532
},
33+
"peerDependenciesMeta": {
34+
"@temporalio/workflow": {
35+
"optional": true
36+
}
37+
},
3638
"bugs": {
3739
"url": "https://github.com/temporalio/sdk-typescript/issues"
3840
},

packages/interceptors-opentelemetry/src/client/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as otel from '@opentelemetry/api';
2-
import { Next, WorkflowClientInterceptor, WorkflowSignalInput, WorkflowStartInput } from '@temporalio/client';
2+
import type { Next, WorkflowSignalInput, WorkflowStartInput, WorkflowClientInterceptor } from '@temporalio/client';
33
import { instrument, headersWithContext, RUN_ID_ATTR_KEY } from '../instrumentation';
44
import { SpanName, SPAN_DELIMITER } from '../workflow';
55

packages/interceptors-opentelemetry/src/instrumentation.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@
33
* @module
44
*/
55
import * as otel from '@opentelemetry/api';
6-
import { ApplicationFailure, ApplicationFailureCategory, Headers, defaultPayloadConverter } from '@temporalio/common';
6+
import {
7+
type Headers,
8+
ApplicationFailure,
9+
ApplicationFailureCategory,
10+
defaultPayloadConverter,
11+
} from '@temporalio/common';
712

813
/** Default trace header for opentelemetry interceptors */
914
export const TRACE_HEADER = '_tracer-data';

packages/interceptors-opentelemetry/src/worker/index.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
import * as otel from '@opentelemetry/api';
2-
import { Resource } from '@opentelemetry/resources';
3-
import { ReadableSpan, SpanExporter } from '@opentelemetry/sdk-trace-base';
4-
import { Context as ActivityContext } from '@temporalio/activity';
5-
import {
6-
ActivityExecuteInput,
2+
import type { Resource } from '@opentelemetry/resources';
3+
import type { ReadableSpan, SpanExporter } from '@opentelemetry/sdk-trace-base';
4+
import type { Context as ActivityContext } from '@temporalio/activity';
5+
import type {
6+
Next,
77
ActivityInboundCallsInterceptor,
88
ActivityOutboundCallsInterceptor,
9-
GetLogAttributesInput,
109
InjectedSink,
11-
Next,
10+
GetLogAttributesInput,
11+
ActivityExecuteInput,
1212
} from '@temporalio/worker';
1313
import { instrument, extractContextFromHeaders } from '../instrumentation';
14-
import { OpenTelemetryWorkflowExporter, SerializableSpan, SpanName, SPAN_DELIMITER } from '../workflow';
14+
import { type OpenTelemetryWorkflowExporter, type SerializableSpan, SpanName, SPAN_DELIMITER } from '../workflow';
1515

1616
export interface InterceptorOptions {
1717
readonly tracer?: otel.Tracer;

packages/interceptors-opentelemetry/src/workflow/context-manager.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
import * as otel from '@opentelemetry/api';
2-
import { AsyncLocalStorage } from '@temporalio/workflow';
2+
import { ensureWorkflowModuleLoaded, getWorkflowModuleIfAvailable } from './workflow-module-loader';
3+
4+
const AsyncLocalStorage = getWorkflowModuleIfAvailable()?.AsyncLocalStorage;
35

46
export class ContextManager implements otel.ContextManager {
5-
protected storage = new AsyncLocalStorage<otel.Context>();
7+
// If `@temporalio/workflow` is not available, ignore for now.
8+
// When ContextManager is constructed module resolution error will be thrown.
9+
protected storage = AsyncLocalStorage ? new AsyncLocalStorage<otel.Context>() : undefined;
10+
11+
public constructor() {
12+
ensureWorkflowModuleLoaded();
13+
}
614

715
active(): otel.Context {
8-
return this.storage.getStore() || otel.ROOT_CONTEXT;
16+
return this.storage!.getStore() || otel.ROOT_CONTEXT;
917
}
1018

1119
bind<T>(context: otel.Context, target: T): T {
@@ -36,7 +44,7 @@ export class ContextManager implements otel.ContextManager {
3644
}
3745

3846
disable(): this {
39-
this.storage.disable();
47+
this.storage!.disable();
4048
return this;
4149
}
4250

@@ -47,6 +55,6 @@ export class ContextManager implements otel.ContextManager {
4755
...args: A
4856
): ReturnType<F> {
4957
const cb = thisArg == null ? fn : fn.bind(thisArg);
50-
return this.storage.run(context, cb, ...args);
58+
return this.storage!.run(context, cb, ...args);
5159
}
5260
}

packages/interceptors-opentelemetry/src/workflow/index.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@
33
import './runtime'; // Patch the Workflow isolate runtime for opentelemetry
44
import * as otel from '@opentelemetry/api';
55
import * as tracing from '@opentelemetry/sdk-trace-base';
6-
import {
6+
import type {
77
ActivityInput,
8-
ContinueAsNew,
98
ContinueAsNewInput,
109
DisposeInput,
1110
GetLogAttributesInput,
@@ -16,14 +15,14 @@ import {
1615
StartChildWorkflowExecutionInput,
1716
WorkflowExecuteInput,
1817
WorkflowInboundCallsInterceptor,
19-
workflowInfo,
2018
WorkflowInternalsInterceptor,
2119
WorkflowOutboundCallsInterceptor,
2220
} from '@temporalio/workflow';
2321
import { instrument, extractContextFromHeaders, headersWithContext } from '../instrumentation';
2422
import { ContextManager } from './context-manager';
2523
import { SpanName, SPAN_DELIMITER } from './definitions';
2624
import { SpanExporter } from './span-exporter';
25+
import { ensureWorkflowModuleLoaded, getWorkflowModule } from './workflow-module-loader';
2726

2827
export * from './definitions';
2928

@@ -48,14 +47,21 @@ function getTracer(): otel.Tracer {
4847
*
4948
* Wraps the operation in an opentelemetry Span and links it to a parent Span context if one is
5049
* provided in the Workflow input headers.
50+
*
51+
* `@temporalio/workflow` must be provided by host package in order to function.
5152
*/
5253
export class OpenTelemetryInboundInterceptor implements WorkflowInboundCallsInterceptor {
5354
protected readonly tracer = getTracer();
5455

56+
public constructor() {
57+
ensureWorkflowModuleLoaded();
58+
}
59+
5560
public async execute(
5661
input: WorkflowExecuteInput,
5762
next: Next<WorkflowInboundCallsInterceptor, 'execute'>
5863
): Promise<unknown> {
64+
const { workflowInfo, ContinueAsNew } = getWorkflowModule();
5965
const context = await extractContextFromHeaders(input.headers);
6066
return await instrument({
6167
tracer: this.tracer,
@@ -84,10 +90,16 @@ export class OpenTelemetryInboundInterceptor implements WorkflowInboundCallsInte
8490
* Intercepts outbound calls to schedule an Activity
8591
*
8692
* Wraps the operation in an opentelemetry Span and passes it to the Activity via headers.
93+
*
94+
* `@temporalio/workflow` must be provided by host package in order to function.
8795
*/
8896
export class OpenTelemetryOutboundInterceptor implements WorkflowOutboundCallsInterceptor {
8997
protected readonly tracer = getTracer();
9098

99+
public constructor() {
100+
ensureWorkflowModuleLoaded();
101+
}
102+
91103
public async scheduleActivity(
92104
input: ActivityInput,
93105
next: Next<WorkflowOutboundCallsInterceptor, 'scheduleActivity'>
@@ -143,6 +155,7 @@ export class OpenTelemetryOutboundInterceptor implements WorkflowOutboundCallsIn
143155
input: ContinueAsNewInput,
144156
next: Next<WorkflowOutboundCallsInterceptor, 'continueAsNew'>
145157
): Promise<never> {
158+
const { ContinueAsNew } = getWorkflowModule();
146159
return await instrument({
147160
tracer: this.tracer,
148161
spanName: `${SpanName.CONTINUE_AS_NEW}${SPAN_DELIMITER}${input.options.workflowType}`,

packages/interceptors-opentelemetry/src/workflow/runtime.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
* Sets global variables required for importing opentelemetry in isolate
33
* @module
44
*/
5-
import { inWorkflowContext } from '@temporalio/workflow';
5+
import { getWorkflowModuleIfAvailable } from './workflow-module-loader';
66

7-
if (inWorkflowContext()) {
7+
const inWorkflowContext = getWorkflowModuleIfAvailable()?.inWorkflowContext;
8+
9+
if (inWorkflowContext?.()) {
810
// Required by opentelemetry (pretend to be a browser)
911
Object.assign(globalThis, {
1012
performance: {

packages/interceptors-opentelemetry/src/workflow/span-exporter.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
import * as tracing from '@opentelemetry/sdk-trace-base';
22
import { ExportResult, ExportResultCode } from '@opentelemetry/core';
3-
import * as wf from '@temporalio/workflow';
43
import { OpenTelemetrySinks, SerializableSpan } from './definitions';
4+
import { ensureWorkflowModuleLoaded, getWorkflowModuleIfAvailable } from './workflow-module-loader';
55

6-
const { exporter } = wf.proxySinks<OpenTelemetrySinks>();
6+
const exporter = getWorkflowModuleIfAvailable()?.proxySinks<OpenTelemetrySinks>()?.exporter;
77

88
export class SpanExporter implements tracing.SpanExporter {
9+
public constructor() {
10+
ensureWorkflowModuleLoaded();
11+
}
12+
913
public export(spans: tracing.ReadableSpan[], resultCallback: (result: ExportResult) => void): void {
10-
exporter.export(spans.map((span) => this.makeSerializable(span)));
14+
exporter!.export(spans.map((span) => this.makeSerializable(span)));
1115
resultCallback({ code: ExportResultCode.SUCCESS });
1216
}
1317

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/**
2+
* Utilities for working with a possibly missing `@temporalio/workflow` peer dependency
3+
* @module
4+
*/
5+
import type * as WorkflowModule from '@temporalio/workflow';
6+
7+
// @temporalio/workflow is an optional peer dependency.
8+
// It can be missing as long as the user isn't attempting to construct a workflow interceptor.
9+
// If we start shipping ES modules alongside CJS, we will have to reconsider
10+
// this dynamic import as `import` is async for ES modules.
11+
let workflowModule: typeof WorkflowModule | undefined;
12+
let workflowModuleLoadError: any | undefined;
13+
14+
try {
15+
// eslint-disable-next-line @typescript-eslint/no-require-imports
16+
workflowModule = require('@temporalio/workflow');
17+
} catch (err) {
18+
// Capture the module not found error to rethrow if the module is required
19+
workflowModuleLoadError = err;
20+
}
21+
22+
/**
23+
* Returns `@temporalio/workflow` module if present.
24+
* Throws if the module failed to load
25+
*/
26+
export function getWorkflowModule(): typeof WorkflowModule {
27+
if (workflowModuleLoadError) {
28+
throw workflowModuleLoadError;
29+
}
30+
return workflowModule!;
31+
}
32+
33+
/**
34+
* Checks if the workflow module loaded successfully and throws if not.
35+
*/
36+
export function ensureWorkflowModuleLoaded(): void {
37+
if (workflowModuleLoadError) {
38+
throw workflowModuleLoadError;
39+
}
40+
}
41+
42+
/**
43+
* Returns the workflow module if available, or undefined if it failed to load.
44+
*/
45+
export function getWorkflowModuleIfAvailable(): typeof WorkflowModule | undefined {
46+
return workflowModule;
47+
}

0 commit comments

Comments
 (0)