Skip to content

Commit 0c21b36

Browse files
authored
fix(sdk): otel v1 resource backward compatibility (#648)
1 parent f0e98b5 commit 0c21b36

File tree

2 files changed

+67
-31
lines changed

2 files changed

+67
-31
lines changed

packages/traceloop-sdk/src/lib/tracing/index.ts

Lines changed: 19 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { NodeSDK } from "@opentelemetry/sdk-node";
22
import { SpanProcessor } from "@opentelemetry/sdk-trace-node";
33
import { context, diag } from "@opentelemetry/api";
44
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
5-
import { resourceFromAttributes, Resource } from "@opentelemetry/resources";
5+
import { Resource } from "@opentelemetry/resources";
66
import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
77
import { Instrumentation } from "@opentelemetry/instrumentation";
88
import { InitializeOptions } from "../interfaces";
@@ -307,35 +307,10 @@ export const startTracing = (options: InitializeOptions) => {
307307
spanProcessors.push(options.processor);
308308
}
309309

310-
// Create resource with proper detection and defensive handling for OTLP serialization
311-
const serviceName =
312-
options.appName || process.env.npm_package_name || "unknown-service";
313-
let resource: Resource;
314-
315-
try {
316-
// Create our custom resource with service name and let NodeSDK handle default detection
317-
resource = resourceFromAttributes({
318-
[ATTR_SERVICE_NAME]: serviceName,
319-
});
320-
321-
// Defensive check to prevent OTLP serialization errors
322-
if (!resource || typeof resource !== "object") {
323-
throw new Error("Invalid resource object");
324-
}
325-
326-
if (!resource.attributes || typeof resource.attributes !== "object") {
327-
throw new Error("Resource missing attributes");
328-
}
329-
} catch (error) {
330-
// Fallback: create a basic resource manually
331-
diag.warn(
332-
"Failed to create resource with resourceFromAttributes, using fallback",
333-
error,
334-
);
335-
resource = resourceFromAttributes({
336-
[ATTR_SERVICE_NAME]: serviceName,
337-
});
338-
}
310+
const resource = createResource({
311+
[ATTR_SERVICE_NAME]:
312+
options.appName || process.env.npm_package_name || "unknown_service",
313+
});
339314

340315
_sdk = new NodeSDK({
341316
resource,
@@ -378,3 +353,17 @@ export const shouldSendTraces = () => {
378353
export const forceFlush = async () => {
379354
await _spanProcessor.forceFlush();
380355
};
356+
357+
// Compatibility function for creating resources that works with both OTel v1.x and v2.x
358+
function createResource(attributes: Record<string, any>): Resource {
359+
// Import the resource module at runtime to handle both v1.x and v2.x
360+
const resourcesModule = require("@opentelemetry/resources");
361+
362+
// Try to use resourceFromAttributes if it exists (OTel v2.x)
363+
if (resourcesModule.resourceFromAttributes) {
364+
return resourcesModule.resourceFromAttributes(attributes);
365+
}
366+
367+
// Fallback to constructor for OTel v1.x
368+
return new resourcesModule.Resource(attributes);
369+
}

packages/traceloop-sdk/src/lib/tracing/span-processor.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,52 @@ const onSpanStart = (span: Span): void => {
157157
}
158158
};
159159

160+
/**
161+
* Ensures span compatibility between OTel v1.x and v2.x for OTLP transformer
162+
*/
163+
const ensureSpanCompatibility = (span: ReadableSpan): ReadableSpan => {
164+
const spanAny = span as any;
165+
166+
// If the span already has instrumentationLibrary, it's compatible (OTel v2.x)
167+
if (spanAny.instrumentationLibrary) {
168+
return span;
169+
}
170+
171+
// If it has instrumentationScope but no instrumentationLibrary (OTel v1.x),
172+
// add instrumentationLibrary as an alias to prevent OTLP transformer errors
173+
if (spanAny.instrumentationScope) {
174+
// Create a proxy that provides both properties
175+
return new Proxy(span, {
176+
get(target, prop) {
177+
if (prop === "instrumentationLibrary") {
178+
return (target as any).instrumentationScope;
179+
}
180+
return (target as any)[prop];
181+
},
182+
}) as ReadableSpan;
183+
}
184+
185+
// Fallback: add both properties with defaults
186+
return new Proxy(span, {
187+
get(target, prop) {
188+
if (
189+
prop === "instrumentationLibrary" ||
190+
prop === "instrumentationScope"
191+
) {
192+
return {
193+
name: "unknown",
194+
version: undefined,
195+
schemaUrl: undefined,
196+
};
197+
}
198+
return (target as any)[prop];
199+
},
200+
}) as ReadableSpan;
201+
};
202+
160203
/**
161204
* Handles span end event, adapting attributes for Vercel AI compatibility
205+
* and ensuring OTLP transformer compatibility
162206
*/
163207
const onSpanEnd = (
164208
originalOnEnd: (span: ReadableSpan) => void,
@@ -178,6 +222,9 @@ const onSpanEnd = (
178222
// Apply AI SDK transformations (if needed)
179223
transformAiSdkSpan(span);
180224

181-
originalOnEnd(span);
225+
// Ensure OTLP transformer compatibility
226+
const compatibleSpan = ensureSpanCompatibility(span);
227+
228+
originalOnEnd(compatibleSpan);
182229
};
183230
};

0 commit comments

Comments
 (0)