From 6dd518dab1253a9472f7e4348c4382f53973fffb Mon Sep 17 00:00:00 2001 From: Hector Hernandez <39923391+hectorhdzg@users.noreply.github.com> Date: Fri, 6 Mar 2026 16:07:07 -0800 Subject: [PATCH 1/6] OTel Web SDK Phase 1 --- .../otel-core/Tests/Unit/src/index.tests.ts | 2 + .../Tests/Unit/src/sdk/OTelWebSdk.Tests.ts | 1106 +++++++++++++++++ shared/otel-core/src/index.ts | 8 +- .../otel-core/src/interfaces/otel/IOTelSdk.ts | 12 - .../src/interfaces/otel/IOTelSdkCtx.ts | 57 - .../src/interfaces/otel/IOTelWebSdk.ts | 126 ++ .../otel/config/IOTelWebSdkConfig.ts | 136 ++ .../interfaces/otel/trace/IOTelTracerCtx.ts | 2 +- .../otel-core/src/otel/api/context/context.ts | 4 +- shared/otel-core/src/otel/sdk/OTelSdk.ts | 263 ---- shared/otel-core/src/otel/sdk/OTelWebSdk.ts | 446 +++++++ shared/otel-core/src/utils/DataCacheHelper.ts | 2 +- 12 files changed, 1825 insertions(+), 339 deletions(-) create mode 100644 shared/otel-core/Tests/Unit/src/sdk/OTelWebSdk.Tests.ts delete mode 100644 shared/otel-core/src/interfaces/otel/IOTelSdk.ts delete mode 100644 shared/otel-core/src/interfaces/otel/IOTelSdkCtx.ts create mode 100644 shared/otel-core/src/interfaces/otel/IOTelWebSdk.ts create mode 100644 shared/otel-core/src/interfaces/otel/config/IOTelWebSdkConfig.ts delete mode 100644 shared/otel-core/src/otel/sdk/OTelSdk.ts create mode 100644 shared/otel-core/src/otel/sdk/OTelWebSdk.ts diff --git a/shared/otel-core/Tests/Unit/src/index.tests.ts b/shared/otel-core/Tests/Unit/src/index.tests.ts index c81be1895..eca5a8c03 100644 --- a/shared/otel-core/Tests/Unit/src/index.tests.ts +++ b/shared/otel-core/Tests/Unit/src/index.tests.ts @@ -7,6 +7,7 @@ import { OTelMultiLogRecordProcessorTests } from "./sdk/OTelMultiLogRecordProces import { CommonUtilsTests } from "./sdk/commonUtils.Tests"; import { OpenTelemetryErrorsTests } from "./ai/errors.Tests"; import { OTelTraceApiTests } from "./trace/traceState.Tests"; +import { OTelWebSdkTests } from "./sdk/OTelWebSdk.Tests"; // AppInsightsCommon tests import { ApplicationInsightsTests } from "./ai/AppInsightsCommon.tests"; @@ -47,6 +48,7 @@ export function runTests() { new CommonUtilsTests().registerTests(); new OpenTelemetryErrorsTests().registerTests(); new OTelTraceApiTests().registerTests(); + new OTelWebSdkTests().registerTests(); new GlobalTestHooks().registerTests(); new DynamicTests().registerTests(); diff --git a/shared/otel-core/Tests/Unit/src/sdk/OTelWebSdk.Tests.ts b/shared/otel-core/Tests/Unit/src/sdk/OTelWebSdk.Tests.ts new file mode 100644 index 000000000..6003865e5 --- /dev/null +++ b/shared/otel-core/Tests/Unit/src/sdk/OTelWebSdk.Tests.ts @@ -0,0 +1,1106 @@ +import { AITestClass, Assert } from "@microsoft/ai-test-framework"; +import { createPromise, IPromise } from "@nevware21/ts-async"; + +import { createOTelWebSdk } from "../../../../src/otel/sdk/OTelWebSdk"; +import { IOTelWebSdkConfig } from "../../../../src/interfaces/otel/config/IOTelWebSdkConfig"; +import { IOTelWebSdk } from "../../../../src/interfaces/otel/IOTelWebSdk"; +import { IOTelErrorHandlers } from "../../../../src/interfaces/otel/config/IOTelErrorHandlers"; +import { IOTelResource, OTelRawResourceAttribute } from "../../../../src/interfaces/otel/resources/IOTelResource"; +import { IOTelContextManager } from "../../../../src/interfaces/otel/context/IOTelContextManager"; +import { IOTelIdGenerator } from "../../../../src/interfaces/otel/trace/IOTelIdGenerator"; +import { IOTelSampler } from "../../../../src/interfaces/otel/trace/IOTelSampler"; +import { IOTelLogRecordProcessor } from "../../../../src/interfaces/otel/logs/IOTelLogRecordProcessor"; +import { IOTelAttributes } from "../../../../src/interfaces/otel/IOTelAttributes"; +import { createResolvedPromise } from "@nevware21/ts-async"; +import { createContext } from "../../../../src/otel/api/context/context"; +import { eOTelSamplingDecision } from "../../../../src/enums/otel/OTelSamplingDecision"; +import { eOTelSpanKind } from "../../../../src/enums/otel/OTelSpanKind"; +import { eW3CTraceFlags } from "../../../../src/enums/W3CTraceFlags"; +import { IOTelSamplingResult } from "../../../../src/interfaces/otel/trace/IOTelSamplingResult"; +import { createContextManager } from "../../../../src/otel/api/context/contextManager"; +import { IReadableSpan } from "../../../../src/interfaces/otel/trace/IReadableSpan"; + +export class OTelWebSdkTests extends AITestClass { + private _sdk: IOTelWebSdk | null = null; + + public testInitialize() { + super.testInitialize(); + this._sdk = null; + } + + public testCleanup() { + if (this._sdk) { + this._sdk.shutdown(); + this._sdk = null; + } + super.testCleanup(); + } + + public registerTests() { + this._registerConstructionTests(); + this._registerValidationTests(); + this._registerTracerTests(); + this._registerSpanCreationTests(); + this._registerStartActiveSpanTests(); + this._registerSamplingTests(); + this._registerLoggerTests(); + this._registerShutdownTests(); + this._registerForceFlushTests(); + this._registerConfigTests(); + } + + private _registerConstructionTests(): void { + this.testCase({ + name: "OTelWebSdk: createOTelWebSdk should create an instance with valid config", + test: () => { + let config = this._createValidConfig(); + this._sdk = createOTelWebSdk(config); + Assert.ok(this._sdk, "SDK instance should be created"); + Assert.equal(typeof this._sdk.getTracer, "function", "Should have getTracer method"); + Assert.equal(typeof this._sdk.getLogger, "function", "Should have getLogger method"); + Assert.equal(typeof this._sdk.forceFlush, "function", "Should have forceFlush method"); + Assert.equal(typeof this._sdk.shutdown, "function", "Should have shutdown method"); + Assert.equal(typeof this._sdk.getConfig, "function", "Should have getConfig method"); + } + }); + + this.testCase({ + name: "OTelWebSdk: should support multiple independent instances", + test: () => { + let config1 = this._createValidConfig(); + let config2 = this._createValidConfig(); + let sdk1 = createOTelWebSdk(config1); + let sdk2 = createOTelWebSdk(config2); + + Assert.ok(sdk1, "First SDK instance should be created"); + Assert.ok(sdk2, "Second SDK instance should be created"); + Assert.notEqual(sdk1, sdk2, "SDK instances should be different objects"); + + // Each can provide independent tracers + let tracer1 = sdk1.getTracer("service-a"); + let tracer2 = sdk2.getTracer("service-b"); + Assert.ok(tracer1, "First SDK should provide a tracer"); + Assert.ok(tracer2, "Second SDK should provide a tracer"); + + sdk1.shutdown(); + sdk2.shutdown(); + } + }); + } + + private _registerValidationTests(): void { + this.testCase({ + name: "OTelWebSdk: should call error handler when resource is missing", + test: () => { + let errorCalled = false; + let config = this._createValidConfig(); + config.errorHandlers = { + error: function (msg) { + errorCalled = true; + } + }; + (config as any).resource = null; + this._sdk = createOTelWebSdk(config); + Assert.ok(errorCalled, "Error handler should be called for missing resource"); + } + }); + + this.testCase({ + name: "OTelWebSdk: should call error handler when contextManager is missing", + test: () => { + let errorCalled = false; + let config = this._createValidConfig(); + config.errorHandlers = { + error: function (msg) { + errorCalled = true; + } + }; + (config as any).contextManager = null; + this._sdk = createOTelWebSdk(config); + Assert.ok(errorCalled, "Error handler should be called for missing contextManager"); + } + }); + + this.testCase({ + name: "OTelWebSdk: should call error handler when idGenerator is missing", + test: () => { + let errorCalled = false; + let config = this._createValidConfig(); + config.errorHandlers = { + error: function (msg) { + errorCalled = true; + } + }; + (config as any).idGenerator = null; + this._sdk = createOTelWebSdk(config); + Assert.ok(errorCalled, "Error handler should be called for missing idGenerator"); + } + }); + + this.testCase({ + name: "OTelWebSdk: should call error handler when sampler is missing", + test: () => { + let errorCalled = false; + let config = this._createValidConfig(); + config.errorHandlers = { + error: function (msg) { + errorCalled = true; + } + }; + (config as any).sampler = null; + this._sdk = createOTelWebSdk(config); + Assert.ok(errorCalled, "Error handler should be called for missing sampler"); + } + }); + + this.testCase({ + name: "OTelWebSdk: should call error handler when performanceNow is missing", + test: () => { + let errorCalled = false; + let config = this._createValidConfig(); + config.errorHandlers = { + error: function (msg) { + errorCalled = true; + } + }; + (config as any).performanceNow = null; + this._sdk = createOTelWebSdk(config); + Assert.ok(errorCalled, "Error handler should be called for missing performanceNow"); + } + }); + + this.testCase({ + name: "OTelWebSdk: should warn when errorHandlers are missing", + test: () => { + let warnCalled = false; + let origWarn = console.warn; + console.warn = function () { + warnCalled = true; + }; + try { + let config = this._createValidConfig(); + (config as any).errorHandlers = null; + this._sdk = createOTelWebSdk(config); + Assert.ok(warnCalled, "Should warn about missing errorHandlers"); + } finally { + console.warn = origWarn; + } + } + }); + } + + private _registerTracerTests(): void { + this.testCase({ + name: "OTelWebSdk: getTracer should return a tracer instance", + test: () => { + this._sdk = createOTelWebSdk(this._createValidConfig()); + let tracer = this._sdk.getTracer("test-tracer"); + Assert.ok(tracer, "Should return a tracer"); + Assert.equal(typeof tracer.startSpan, "function", "Tracer should have startSpan method"); + Assert.equal(typeof tracer.startActiveSpan, "function", "Tracer should have startActiveSpan method"); + } + }); + + this.testCase({ + name: "OTelWebSdk: getTracer should cache tracers by name", + test: () => { + this._sdk = createOTelWebSdk(this._createValidConfig()); + let tracer1 = this._sdk.getTracer("test-tracer"); + let tracer2 = this._sdk.getTracer("test-tracer"); + Assert.equal(tracer1, tracer2, "Same name should return same tracer instance"); + } + }); + + this.testCase({ + name: "OTelWebSdk: getTracer should cache tracers by name and version", + test: () => { + this._sdk = createOTelWebSdk(this._createValidConfig()); + let tracer1 = this._sdk.getTracer("test-tracer", "1.0.0"); + let tracer2 = this._sdk.getTracer("test-tracer", "1.0.0"); + let tracer3 = this._sdk.getTracer("test-tracer", "2.0.0"); + Assert.equal(tracer1, tracer2, "Same name and version should return same tracer instance"); + Assert.notEqual(tracer1, tracer3, "Different versions should return different tracer instances"); + } + }); + + this.testCase({ + name: "OTelWebSdk: getTracer should return different tracers for different names", + test: () => { + this._sdk = createOTelWebSdk(this._createValidConfig()); + let tracer1 = this._sdk.getTracer("service-a"); + let tracer2 = this._sdk.getTracer("service-b"); + Assert.notEqual(tracer1, tracer2, "Different names should return different tracer instances"); + } + }); + + this.testCase({ + name: "OTelWebSdk: getTracer should handle schemaUrl in options", + test: () => { + this._sdk = createOTelWebSdk(this._createValidConfig()); + let tracer1 = this._sdk.getTracer("test-tracer", "1.0.0", { schemaUrl: "https://example.com/v1" }); + let tracer2 = this._sdk.getTracer("test-tracer", "1.0.0", { schemaUrl: "https://example.com/v2" }); + let tracer3 = this._sdk.getTracer("test-tracer", "1.0.0", { schemaUrl: "https://example.com/v1" }); + Assert.notEqual(tracer1, tracer2, "Different schemaUrls should return different tracers"); + Assert.equal(tracer1, tracer3, "Same schemaUrl should return same tracer"); + } + }); + + this.testCase({ + name: "OTelWebSdk: getTracer should use 'unknown' for empty name", + test: () => { + this._sdk = createOTelWebSdk(this._createValidConfig()); + let tracer = this._sdk.getTracer(""); + Assert.ok(tracer, "Should return a tracer even for empty name"); + } + }); + } + + private _registerSpanCreationTests(): void { + this.testCase({ + name: "OTelWebSdk: startSpan should create a functional span", + test: () => { + this._sdk = createOTelWebSdk(this._createValidConfig()); + let tracer = this._sdk.getTracer("test-tracer", "1.0.0"); + let result = tracer.startSpan("test-operation"); + Assert.ok(result, "startSpan should return a span (not null)"); + + let span = result as IReadableSpan; + Assert.equal(span.name, "test-operation", "Span name should match"); + Assert.equal(typeof span.spanContext, "function", "Span should have spanContext method"); + Assert.equal(typeof span.setAttribute, "function", "Span should have setAttribute method"); + Assert.equal(typeof span.setAttributes, "function", "Span should have setAttributes method"); + Assert.equal(typeof span.setStatus, "function", "Span should have setStatus method"); + Assert.equal(typeof span.end, "function", "Span should have end method"); + Assert.equal(typeof span.isRecording, "function", "Span should have isRecording method"); + } + }); + + this.testCase({ + name: "OTelWebSdk: startSpan should generate valid trace and span IDs", + test: () => { + this._sdk = createOTelWebSdk(this._createValidConfig()); + let tracer = this._sdk.getTracer("test-tracer"); + let span = tracer.startSpan("test-operation") as IReadableSpan; + Assert.ok(span, "Span should not be null"); + + let spanContext = span.spanContext(); + Assert.ok(spanContext, "Span should have a spanContext"); + Assert.ok(spanContext.traceId, "Span context should have a traceId"); + Assert.ok(spanContext.spanId, "Span context should have a spanId"); + Assert.equal(spanContext.traceId.length, 32, "traceId should be 32 hex chars"); + Assert.equal(spanContext.spanId.length, 16, "spanId should be 16 hex chars"); + + span.end(); + } + }); + + this.testCase({ + name: "OTelWebSdk: startSpan should create recording spans with AlwaysOn sampler", + test: () => { + this._sdk = createOTelWebSdk(this._createValidConfig()); + let tracer = this._sdk.getTracer("test-tracer"); + let span = tracer.startSpan("test-operation") as IReadableSpan; + Assert.ok(span, "Span should not be null"); + + Assert.ok(span.isRecording(), "Span should be recording with AlwaysOn sampler"); + Assert.equal(span.spanContext().traceFlags, eW3CTraceFlags.Sampled, "Trace flags should indicate sampled"); + + span.end(); + } + }); + + this.testCase({ + name: "OTelWebSdk: startSpan should set span kind from options", + test: () => { + this._sdk = createOTelWebSdk(this._createValidConfig()); + let tracer = this._sdk.getTracer("test-tracer"); + let span = tracer.startSpan("test-operation", { + kind: eOTelSpanKind.CLIENT + }) as IReadableSpan; + Assert.ok(span, "Span should not be null"); + + Assert.equal(span.kind, eOTelSpanKind.CLIENT, "Span kind should match options"); + + span.end(); + } + }); + + this.testCase({ + name: "OTelWebSdk: startSpan should default to INTERNAL span kind", + test: () => { + this._sdk = createOTelWebSdk(this._createValidConfig()); + let tracer = this._sdk.getTracer("test-tracer"); + let span = tracer.startSpan("test-operation") as IReadableSpan; + Assert.ok(span, "Span should not be null"); + + Assert.equal(span.kind, eOTelSpanKind.INTERNAL, "Default span kind should be INTERNAL"); + + span.end(); + } + }); + + this.testCase({ + name: "OTelWebSdk: startSpan should set attributes from options", + test: () => { + this._sdk = createOTelWebSdk(this._createValidConfig()); + let tracer = this._sdk.getTracer("test-tracer"); + let span = tracer.startSpan("test-operation", { + attributes: { "key1": "value1", "key2": 42 } + }) as IReadableSpan; + Assert.ok(span, "Span should not be null"); + + Assert.ok(span.attributes, "Span should have attributes"); + Assert.equal(span.attributes["key1"], "value1", "Should have key1 attribute"); + Assert.equal(span.attributes["key2"], 42, "Should have key2 attribute"); + + span.end(); + } + }); + + this.testCase({ + name: "OTelWebSdk: startSpan should support setAttribute after creation", + test: () => { + this._sdk = createOTelWebSdk(this._createValidConfig()); + let tracer = this._sdk.getTracer("test-tracer"); + let span = tracer.startSpan("test-operation") as IReadableSpan; + Assert.ok(span, "Span should not be null"); + + span.setAttribute("dynamic.key", "dynamic.value"); + Assert.equal(span.attributes["dynamic.key"], "dynamic.value", "Should have dynamically set attribute"); + + span.end(); + } + }); + + this.testCase({ + name: "OTelWebSdk: startSpan should create different spans with different IDs", + test: () => { + this._sdk = createOTelWebSdk(this._createValidConfig()); + let tracer = this._sdk.getTracer("test-tracer"); + let span1 = tracer.startSpan("operation-1") as IReadableSpan; + let span2 = tracer.startSpan("operation-2") as IReadableSpan; + Assert.ok(span1, "Span 1 should not be null"); + Assert.ok(span2, "Span 2 should not be null"); + + Assert.notEqual( + span1.spanContext().spanId, + span2.spanContext().spanId, + "Different spans should have different spanIds" + ); + + span1.end(); + span2.end(); + } + }); + + this.testCase({ + name: "OTelWebSdk: startSpan with root option should create new trace", + test: () => { + this._sdk = createOTelWebSdk(this._createValidConfig()); + let tracer = this._sdk.getTracer("test-tracer"); + let span1 = tracer.startSpan("root-1") as IReadableSpan; + let span2 = tracer.startSpan("root-2", { root: true }) as IReadableSpan; + Assert.ok(span1, "Span 1 should not be null"); + Assert.ok(span2, "Span 2 should not be null"); + + // Both are root spans (no active context), so they both get new traceIds + let traceId1 = span1.spanContext().traceId; + let traceId2 = span2.spanContext().traceId; + + Assert.ok(traceId1, "First span should have a traceId"); + Assert.ok(traceId2, "Second span should have a traceId"); + Assert.notEqual(traceId1, traceId2, "Root spans should have different traceIds"); + + span1.end(); + span2.end(); + } + }); + + this.testCase({ + name: "OTelWebSdk: span end should mark span as ended", + test: () => { + this._sdk = createOTelWebSdk(this._createValidConfig()); + let tracer = this._sdk.getTracer("test-tracer"); + let span = tracer.startSpan("test-operation") as IReadableSpan; + Assert.ok(span, "Span should not be null"); + + Assert.ok(span.isRecording(), "Span should be recording before end"); + Assert.ok(!span.ended, "Span should not be ended before end()"); + + span.end(); + + Assert.ok(!span.isRecording(), "Span should not be recording after end"); + Assert.ok(span.ended, "Span should be ended after end()"); + } + }); + + this.testCase({ + name: "OTelWebSdk: startSpan should return null after shutdown", + test: (): IPromise => { + let sdk = createOTelWebSdk(this._createValidConfig()); + this._sdk = sdk; + return createPromise(function (resolve, reject) { + sdk.shutdown().then(function () { + try { + let tracer = sdk.getTracer("test"); + let span = tracer.startSpan("test-span"); + Assert.equal(span, null, "startSpan after shutdown should return null"); + resolve(); + } catch (e) { + reject(e); + } + }).catch(reject); + }); + } + }); + } + + private _registerStartActiveSpanTests(): void { + this.testCase({ + name: "OTelWebSdk: startActiveSpan should execute callback with span", + test: () => { + let config = this._createValidConfig(); + config.contextManager = createContextManager(); + this._sdk = createOTelWebSdk(config); + let tracer = this._sdk.getTracer("test-tracer"); + let callbackExecuted = false; + + tracer.startActiveSpan("active-operation", function (span) { + callbackExecuted = true; + Assert.ok(span, "Callback should receive a span"); + let readable = span as IReadableSpan; + Assert.equal(readable.name, "active-operation", "Span name should match"); + Assert.ok(span.isRecording(), "Span should be recording"); + span.end(); + }); + + Assert.ok(callbackExecuted, "Callback should have been executed"); + } + }); + + this.testCase({ + name: "OTelWebSdk: startActiveSpan should return callback result", + test: () => { + let config = this._createValidConfig(); + config.contextManager = createContextManager(); + this._sdk = createOTelWebSdk(config); + let tracer = this._sdk.getTracer("test-tracer"); + + let result = tracer.startActiveSpan("active-operation", function (span) { + span.end(); + return 42; + }); + + Assert.equal(result, 42, "Should return the callback result"); + } + }); + + this.testCase({ + name: "OTelWebSdk: startActiveSpan with options should pass options to span", + test: () => { + let config = this._createValidConfig(); + config.contextManager = createContextManager(); + this._sdk = createOTelWebSdk(config); + let tracer = this._sdk.getTracer("test-tracer"); + + tracer.startActiveSpan("active-operation", { kind: eOTelSpanKind.SERVER }, function (span) { + let readable = span as IReadableSpan; + Assert.equal(readable.kind, eOTelSpanKind.SERVER, "Span should have the specified kind"); + span.end(); + }); + } + }); + + this.testCase({ + name: "OTelWebSdk: startActiveSpan should set span as parent for nested spans", + test: () => { + let config = this._createValidConfig(); + config.contextManager = createContextManager(); + this._sdk = createOTelWebSdk(config); + let tracer = this._sdk.getTracer("test-tracer"); + let parentTraceId: string = ""; + let childTraceId: string = ""; + let childParentSpanId: string = ""; + let parentSpanId: string = ""; + + tracer.startActiveSpan("parent-operation", function (parentSpan) { + parentTraceId = parentSpan.spanContext().traceId; + parentSpanId = parentSpan.spanContext().spanId; + + // Create a child span while parent is active + let childResult = tracer.startSpan("child-operation"); + Assert.ok(childResult, "Child span should not be null"); + let childSpan = childResult as IReadableSpan; + childTraceId = childSpan.spanContext().traceId; + childParentSpanId = childSpan.parentSpanId || ""; + + childSpan.end(); + parentSpan.end(); + }); + + Assert.equal(childTraceId, parentTraceId, "Child should inherit parent's traceId"); + Assert.equal(childParentSpanId, parentSpanId, "Child's parentSpanId should be parent's spanId"); + } + }); + + this.testCase({ + name: "OTelWebSdk: nested startActiveSpan should create proper hierarchy", + test: () => { + let config = this._createValidConfig(); + config.contextManager = createContextManager(); + this._sdk = createOTelWebSdk(config); + let tracer = this._sdk.getTracer("test-tracer"); + let grandparentSpanId: string = ""; + let parentSpanId: string = ""; + let childParentSpanId: string = ""; + let traceId: string = ""; + + tracer.startActiveSpan("grandparent", function (gp) { + traceId = gp.spanContext().traceId; + grandparentSpanId = gp.spanContext().spanId; + + tracer.startActiveSpan("parent", function (parent) { + let parentReadable = parent as IReadableSpan; + parentSpanId = parent.spanContext().spanId; + Assert.equal( + parent.spanContext().traceId, traceId, + "Parent should share grandparent's traceId" + ); + Assert.equal( + parentReadable.parentSpanId, grandparentSpanId, + "Parent's parentSpanId should be grandparent's spanId" + ); + + let childResult = tracer.startSpan("child"); + Assert.ok(childResult, "Child span should not be null"); + let child = childResult as IReadableSpan; + childParentSpanId = child.parentSpanId || ""; + Assert.equal( + child.spanContext().traceId, traceId, + "Child should share the same traceId" + ); + + child.end(); + parent.end(); + }); + + gp.end(); + }); + + Assert.equal(childParentSpanId, parentSpanId, "Child's parent should be the active parent span"); + } + }); + } + + private _registerSamplingTests(): void { + this.testCase({ + name: "OTelWebSdk: NOT_RECORD sampler should create non-recording spans", + test: () => { + let config = this._createValidConfig(); + config.sampler = { + shouldSample: function (): IOTelSamplingResult { + return { decision: eOTelSamplingDecision.NOT_RECORD }; + }, + toString: function () { return "NeverSampler"; } + }; + this._sdk = createOTelWebSdk(config); + let tracer = this._sdk.getTracer("test-tracer"); + let span = tracer.startSpan("test-operation") as IReadableSpan; + Assert.ok(span, "Span should still be created (non-recording)"); + + Assert.ok(!span.isRecording(), "Span should NOT be recording with NOT_RECORD decision"); + Assert.equal(span.spanContext().traceFlags, eW3CTraceFlags.None, "Trace flags should be None"); + + span.end(); + } + }); + + this.testCase({ + name: "OTelWebSdk: RECORD sampler should create recording but not sampled spans", + test: () => { + let config = this._createValidConfig(); + config.sampler = { + shouldSample: function (): IOTelSamplingResult { + return { decision: eOTelSamplingDecision.RECORD }; + }, + toString: function () { return "RecordOnlySampler"; } + }; + this._sdk = createOTelWebSdk(config); + let tracer = this._sdk.getTracer("test-tracer"); + let span = tracer.startSpan("test-operation") as IReadableSpan; + Assert.ok(span, "Span should not be null"); + + Assert.ok(span.isRecording(), "Span should be recording with RECORD decision"); + Assert.equal(span.spanContext().traceFlags, eW3CTraceFlags.None, "Trace flags should be None (not sampled)"); + + span.end(); + } + }); + + this.testCase({ + name: "OTelWebSdk: RECORD_AND_SAMPLED sampler should create sampled spans", + test: () => { + let config = this._createValidConfig(); + config.sampler = { + shouldSample: function (): IOTelSamplingResult { + return { decision: eOTelSamplingDecision.RECORD_AND_SAMPLED }; + }, + toString: function () { return "AlwaysOnSampler"; } + }; + this._sdk = createOTelWebSdk(config); + let tracer = this._sdk.getTracer("test-tracer"); + let span = tracer.startSpan("test-operation") as IReadableSpan; + Assert.ok(span, "Span should not be null"); + + Assert.ok(span.isRecording(), "Span should be recording"); + Assert.equal(span.spanContext().traceFlags, eW3CTraceFlags.Sampled, "Trace flags should indicate sampled"); + + span.end(); + } + }); + + this.testCase({ + name: "OTelWebSdk: sampler should receive span name and kind", + test: () => { + let receivedSpanName: string = ""; + let receivedSpanKind: number = -1; + let config = this._createValidConfig(); + config.sampler = { + shouldSample: function (_ctx: any, _traceId: string, spanName: string, spanKind: number): IOTelSamplingResult { + receivedSpanName = spanName; + receivedSpanKind = spanKind; + return { decision: eOTelSamplingDecision.RECORD_AND_SAMPLED }; + }, + toString: function () { return "TestSampler"; } + }; + this._sdk = createOTelWebSdk(config); + let tracer = this._sdk.getTracer("test-tracer"); + let span = tracer.startSpan("my-operation", { kind: eOTelSpanKind.SERVER }) as IReadableSpan; + Assert.ok(span, "Span should not be null"); + + Assert.equal(receivedSpanName, "my-operation", "Sampler should receive the span name"); + Assert.equal(receivedSpanKind, eOTelSpanKind.SERVER, "Sampler should receive the span kind"); + + span.end(); + } + }); + + this.testCase({ + name: "OTelWebSdk: sampler-provided attributes should be merged into span", + test: () => { + let config = this._createValidConfig(); + config.sampler = { + shouldSample: function (): IOTelSamplingResult { + return { + decision: eOTelSamplingDecision.RECORD_AND_SAMPLED, + attributes: { "sampler.key": "sampler.value" } + }; + }, + toString: function () { return "AttributeSampler"; } + }; + this._sdk = createOTelWebSdk(config); + let tracer = this._sdk.getTracer("test-tracer"); + let span = tracer.startSpan("test-operation", { + attributes: { "user.key": "user.value" } + }) as IReadableSpan; + Assert.ok(span, "Span should not be null"); + + Assert.equal(span.attributes["sampler.key"], "sampler.value", "Should include sampler attribute"); + Assert.equal(span.attributes["user.key"], "user.value", "Should include user attribute"); + + span.end(); + } + }); + + this.testCase({ + name: "OTelWebSdk: user attributes should override sampler attributes", + test: () => { + let config = this._createValidConfig(); + config.sampler = { + shouldSample: function (): IOTelSamplingResult { + return { + decision: eOTelSamplingDecision.RECORD_AND_SAMPLED, + attributes: { "shared.key": "sampler-wins" } + }; + }, + toString: function () { return "OverrideSampler"; } + }; + this._sdk = createOTelWebSdk(config); + let tracer = this._sdk.getTracer("test-tracer"); + let span = tracer.startSpan("test-operation", { + attributes: { "shared.key": "user-wins" } + }) as IReadableSpan; + Assert.ok(span, "Span should not be null"); + + Assert.equal(span.attributes["shared.key"], "user-wins", "User attributes should override sampler attributes"); + + span.end(); + } + }); + } + + private _registerLoggerTests(): void { + this.testCase({ + name: "OTelWebSdk: getLogger should return a logger instance", + test: () => { + this._sdk = createOTelWebSdk(this._createValidConfig()); + let logger = this._sdk.getLogger("test-logger"); + Assert.ok(logger, "Should return a logger"); + Assert.equal(typeof logger.emit, "function", "Logger should have emit method"); + } + }); + + this.testCase({ + name: "OTelWebSdk: getLogger should return loggers from the logger provider", + test: () => { + this._sdk = createOTelWebSdk(this._createValidConfig()); + let logger1 = this._sdk.getLogger("test-logger", "1.0.0"); + let logger2 = this._sdk.getLogger("test-logger", "1.0.0"); + Assert.ok(logger1, "Should return first logger"); + Assert.ok(logger2, "Should return second logger"); + Assert.equal(logger1, logger2, "Same name and version should return same logger"); + } + }); + + this.testCase({ + name: "OTelWebSdk: getLogger should return different loggers for different names", + test: () => { + this._sdk = createOTelWebSdk(this._createValidConfig()); + let logger1 = this._sdk.getLogger("logger-a"); + let logger2 = this._sdk.getLogger("logger-b"); + Assert.ok(logger1, "First logger should exist"); + Assert.ok(logger2, "Second logger should exist"); + Assert.notEqual(logger1, logger2, "Different names should return different loggers"); + } + }); + + this.testCase({ + name: "OTelWebSdk: getLogger should not throw when calling emit", + test: () => { + this._sdk = createOTelWebSdk(this._createValidConfig()); + let logger = this._sdk.getLogger("test-logger"); + let threw = false; + try { + logger.emit({ body: "test message" }); + } catch (e) { + threw = true; + } + Assert.ok(!threw, "emit should not throw"); + } + }); + } + + private _registerShutdownTests(): void { + this.testCase({ + name: "OTelWebSdk: shutdown should resolve successfully", + test: (): IPromise => { + let sdk = createOTelWebSdk(this._createValidConfig()); + this._sdk = sdk; + return createPromise(function (resolve, reject) { + sdk.shutdown().then(function () { + try { + Assert.ok(true, "Shutdown should resolve successfully"); + resolve(); + } catch (e) { + reject(e); + } + }).catch(reject); + }); + } + }); + + this.testCase({ + name: "OTelWebSdk: getTracer after shutdown should return no-op tracer", + test: (): IPromise => { + let sdk = createOTelWebSdk(this._createValidConfig()); + this._sdk = sdk; + return createPromise(function (resolve, reject) { + sdk.shutdown().then(function () { + try { + let tracer = sdk.getTracer("test"); + Assert.ok(tracer, "Should return a tracer (no-op) after shutdown"); + Assert.equal(typeof tracer.startSpan, "function", "No-op tracer should have startSpan"); + Assert.equal(typeof tracer.startActiveSpan, "function", "No-op tracer should have startActiveSpan"); + // Verify the no-op tracer returns null for startSpan + let span = tracer.startSpan("test-span"); + Assert.equal(span, null, "No-op tracer startSpan should return null"); + resolve(); + } catch (e) { + reject(e); + } + }).catch(reject); + }); + } + }); + + this.testCase({ + name: "OTelWebSdk: getLogger after shutdown should return no-op logger", + test: (): IPromise => { + let sdk = createOTelWebSdk(this._createValidConfig()); + this._sdk = sdk; + return createPromise(function (resolve, reject) { + sdk.shutdown().then(function () { + try { + let logger = sdk.getLogger("test"); + Assert.ok(logger, "Should return a logger (no-op) after shutdown"); + // Verify the no-op logger does not throw + let threw = false; + try { + logger.emit({ body: "after shutdown" }); + } catch (e) { + threw = true; + } + Assert.ok(!threw, "No-op logger emit should not throw"); + resolve(); + } catch (e) { + reject(e); + } + }).catch(reject); + }); + } + }); + + this.testCase({ + name: "OTelWebSdk: second shutdown should resolve without error", + test: (): IPromise => { + let sdk = createOTelWebSdk(this._createValidConfig()); + this._sdk = sdk; + return createPromise(function (resolve, reject) { + sdk.shutdown().then(function () { + sdk.shutdown().then(function () { + try { + Assert.ok(true, "Second shutdown should resolve successfully"); + resolve(); + } catch (e) { + reject(e); + } + }).catch(reject); + }).catch(reject); + }); + } + }); + + this.testCase({ + name: "OTelWebSdk: shutdown should warn on second call", + test: (): IPromise => { + let warnCalled = false; + let config = this._createValidConfig(); + config.errorHandlers = { + warn: function () { + warnCalled = true; + } + }; + let sdk = createOTelWebSdk(config); + this._sdk = sdk; + return createPromise(function (resolve, reject) { + sdk.shutdown().then(function () { + sdk.shutdown().then(function () { + try { + Assert.ok(warnCalled, "Should warn on second shutdown call"); + resolve(); + } catch (e) { + reject(e); + } + }).catch(reject); + }).catch(reject); + }); + } + }); + } + + private _registerForceFlushTests(): void { + this.testCase({ + name: "OTelWebSdk: forceFlush should resolve successfully", + test: (): IPromise => { + let sdk = createOTelWebSdk(this._createValidConfig()); + this._sdk = sdk; + return createPromise(function (resolve, reject) { + sdk.forceFlush().then(function () { + try { + Assert.ok(true, "forceFlush should resolve successfully"); + resolve(); + } catch (e) { + reject(e); + } + }).catch(reject); + }); + } + }); + + this.testCase({ + name: "OTelWebSdk: forceFlush after shutdown should not throw", + test: (): IPromise => { + let sdk = createOTelWebSdk(this._createValidConfig()); + this._sdk = sdk; + return createPromise(function (resolve, reject) { + sdk.shutdown().then(function () { + sdk.forceFlush().then(function () { + try { + Assert.ok(true, "forceFlush after shutdown should resolve"); + resolve(); + } catch (e) { + reject(e); + } + }).catch(reject); + }).catch(reject); + }); + } + }); + + this.testCase({ + name: "OTelWebSdk: forceFlush with log processors should invoke processor flush", + test: (): IPromise => { + let flushCalled = false; + let processor = this._createMockLogProcessor(); + processor.forceFlush = function () { + flushCalled = true; + return createResolvedPromise(undefined); + }; + let config = this._createValidConfig(); + config.logProcessors = [processor]; + let sdk = createOTelWebSdk(config); + this._sdk = sdk; + return createPromise(function (resolve, reject) { + sdk.forceFlush().then(function () { + try { + Assert.ok(flushCalled, "forceFlush should invoke processor forceFlush"); + resolve(); + } catch (e) { + reject(e); + } + }).catch(reject); + }); + } + }); + } + + private _registerConfigTests(): void { + this.testCase({ + name: "OTelWebSdk: getConfig should return the config object", + test: () => { + let config = this._createValidConfig(); + this._sdk = createOTelWebSdk(config); + let returnedConfig = this._sdk.getConfig(); + Assert.ok(returnedConfig, "getConfig should return a config object"); + Assert.equal(returnedConfig.resource, config.resource, "Config resource should match"); + Assert.equal(returnedConfig.contextManager, config.contextManager, "Config contextManager should match"); + Assert.equal(returnedConfig.idGenerator, config.idGenerator, "Config idGenerator should match"); + Assert.equal(returnedConfig.sampler, config.sampler, "Config sampler should match"); + Assert.equal(returnedConfig.performanceNow, config.performanceNow, "Config performanceNow should match"); + } + }); + + this.testCase({ + name: "OTelWebSdk: getConfig should return same config reference (not a copy)", + test: () => { + let config = this._createValidConfig(); + this._sdk = createOTelWebSdk(config); + let returnedConfig = this._sdk.getConfig(); + Assert.equal(returnedConfig, config, "getConfig should return the same config reference"); + } + }); + } + + // ============================= + // Helper methods + // ============================= + + private _createValidConfig(): IOTelWebSdkConfig { + return { + resource: this._createMockResource(), + errorHandlers: this._createMockErrorHandlers(), + contextManager: this._createMockContextManager(), + idGenerator: this._createMockIdGenerator(), + sampler: this._createMockSampler(), + performanceNow: function () { return Date.now(); }, + logProcessors: [] + }; + } + + private _createMockResource(): IOTelResource { + let rawAttributes: OTelRawResourceAttribute[] = [ + ["service.name", "test-service"], + ["service.version", "1.0.0"] + ]; + + let resource: IOTelResource = { + attributes: { "service.name": "test-service", "service.version": "1.0.0" } as IOTelAttributes, + merge: function (other: IOTelResource) { + return resource; + }, + getRawAttributes: function () { + return rawAttributes; + } + }; + + return resource; + } + + private _createMockErrorHandlers(): IOTelErrorHandlers { + return { + error: function (_msg: string) { /* noop */ }, + warn: function (_msg: string) { /* noop */ }, + debug: function (_msg: string) { /* noop */ } + }; + } + + private _createMockContextManager(): IOTelContextManager { + return { + active: function () { return null as any; }, + with: function (context: any, fn: any, thisArg?: any): any { + return fn.apply(thisArg, []); + }, + bind: function (context: any, target: T): T { + return target; + }, + enable: function () { return this; }, + disable: function () { return this; } + } as IOTelContextManager; + } + + private _createMockIdGenerator(): IOTelIdGenerator { + let traceCounter = 0; + let spanCounter = 0; + return { + generateTraceId: function () { + traceCounter++; + // 32 hex chars + let hex = traceCounter.toString(16); + while (hex.length < 32) { + hex = "0" + hex; + } + return hex; + }, + generateSpanId: function () { + spanCounter++; + // 16 hex chars + let hex = spanCounter.toString(16); + while (hex.length < 16) { + hex = "0" + hex; + } + return hex; + } + }; + } + + private _createMockSampler(): IOTelSampler { + return { + shouldSample: function (): IOTelSamplingResult { + return { + decision: eOTelSamplingDecision.RECORD_AND_SAMPLED + }; + }, + toString: function () { + return "AlwaysOnSampler"; + } + }; + } + + private _createMockLogProcessor(): IOTelLogRecordProcessor { + return { + onEmit: function () { /* noop */ }, + forceFlush: function () { return createResolvedPromise(undefined); }, + shutdown: function () { return createResolvedPromise(undefined); } + }; + } +} diff --git a/shared/otel-core/src/index.ts b/shared/otel-core/src/index.ts index 0d05a7e04..f83afe104 100644 --- a/shared/otel-core/src/index.ts +++ b/shared/otel-core/src/index.ts @@ -171,7 +171,6 @@ export { IOTelAttributes, OTelAttributeValue, ExtendedOTelAttributeValue } from export { OTelException, IOTelExceptionWithCode, IOTelExceptionWithMessage, IOTelExceptionWithName } from "./interfaces/IException"; export { IOTelHrTime, OTelTimeInput } from "./interfaces/IOTelHrTime"; export { createOTelApi } from "./otel/api/OTelApi"; -export { OTelSdk } from "./otel/sdk/OTelSdk"; // OpenTelemetry Trace Interfaces export { ITraceApi } from "./interfaces/otel/trace/IOTelTraceApi"; @@ -194,8 +193,8 @@ export { IOTelErrorHandlers } from "./interfaces/otel/config/IOTelErrorHandlers" export { ITraceCfg } from "./interfaces/otel/config/IOTelTraceCfg"; // OpenTelemetry SDK Interfaces -export { IOTelSdk } from "./interfaces/otel/IOTelSdk"; -export { IOTelSdkCtx } from "./interfaces/otel/IOTelSdkCtx"; +export { IOTelWebSdk } from "./interfaces/otel/IOTelWebSdk"; +export { IOTelWebSdkConfig } from "./interfaces/otel/config/IOTelWebSdkConfig"; // OpenTelemetry Context export { createContextManager } from "./otel/api/context/contextManager"; @@ -262,6 +261,9 @@ export { createLogger } from "./otel/sdk/OTelLogger"; export { createMultiLogRecordProcessor } from "./otel/sdk/OTelMultiLogRecordProcessor"; export { loadDefaultConfig, reconfigureLimits } from "./otel/sdk/config"; +// SDK Entry Point +export { createOTelWebSdk } from "./otel/sdk/OTelWebSdk"; + // ======================================== // Application Insights Common Exports // ======================================== diff --git a/shared/otel-core/src/interfaces/otel/IOTelSdk.ts b/shared/otel-core/src/interfaces/otel/IOTelSdk.ts deleted file mode 100644 index 3f89550dd..000000000 --- a/shared/otel-core/src/interfaces/otel/IOTelSdk.ts +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { IOTelApi } from "./IOTelApi"; -import { IOTelConfig } from "./config/IOTelConfig"; -import { IOTelTracerProvider } from "./trace/IOTelTracerProvider"; - -export interface IOTelSdk extends IOTelTracerProvider { - cfg: IOTelConfig; - - api: IOTelApi -} diff --git a/shared/otel-core/src/interfaces/otel/IOTelSdkCtx.ts b/shared/otel-core/src/interfaces/otel/IOTelSdkCtx.ts deleted file mode 100644 index 21c3733a9..000000000 --- a/shared/otel-core/src/interfaces/otel/IOTelSdkCtx.ts +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { IOTelContext } from "./context/IOTelContext"; -import { IOTelSpanOptions } from "./trace/IOTelSpanOptions"; -import { IOTelTracer } from "./trace/IOTelTracer"; -import { IOTelTracerOptions } from "./trace/IOTelTracerOptions"; -import { IReadableSpan } from "./trace/IReadableSpan"; - -/** - * The context for the current IOTelSdk instance and it's configuration - */ -export interface IOTelSdkCtx { - /** - * The current {@link IOTelApi} instance that is being used. - */ - //api: IOTelApi; - - /** - * The current {@link IOTelContext} for the current IOTelSdk instance - */ - context: IOTelContext; - - // ------------------------------------------------- - // Trace Support - // ------------------------------------------------- - - /** - * Returns a Tracer, creating one if one with the given name and version is - * not already created. This may return - * - The same Tracer instance if one has already been created with the same name and version - * - A new Tracer instance if one has not already been created with the same name and version - * - A non-operational Tracer if the provider is not operational - * - * @param name - The name of the tracer or instrumentation library. - * @param version - The version of the tracer or instrumentation library. - * @param options - The options of the tracer or instrumentation library. - * @returns Tracer A Tracer with the given name and version - */ - getTracer(name: string, version?: string, options?: IOTelTracerOptions): IOTelTracer; - - /** - * Starts a new {@link IOTelSpan}. Start the span without setting it on context. - * - * This method do NOT modify the current Context. - * - * @param name - The name of the span - * @param options - SpanOptions used for span creation - * @param context - Context to use to extract parent - * @returns Span The newly created span - * @example - * const span = tracer.startSpan('op'); - * span.setAttribute('key', 'value'); - * span.end(); - */ - startSpan: (name: string, options?: IOTelSpanOptions, context?: IOTelContext) => IReadableSpan; -} diff --git a/shared/otel-core/src/interfaces/otel/IOTelWebSdk.ts b/shared/otel-core/src/interfaces/otel/IOTelWebSdk.ts new file mode 100644 index 000000000..86441a903 --- /dev/null +++ b/shared/otel-core/src/interfaces/otel/IOTelWebSdk.ts @@ -0,0 +1,126 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { IPromise } from "@nevware21/ts-async"; +import { IOTelWebSdkConfig } from "./config/IOTelWebSdkConfig"; +import { IOTelLogger } from "./logs/IOTelLogger"; +import { IOTelLoggerOptions } from "./logs/IOTelLoggerOptions"; +import { IOTelTracer } from "./trace/IOTelTracer"; +import { IOTelTracerOptions } from "./trace/IOTelTracerOptions"; + +/** + * Main interface for the OpenTelemetry Web SDK. + * Provides access to tracer and logger providers, configuration management, + * and complete lifecycle control including unload/cleanup. + * + * @remarks + * - Supports multiple isolated instances without global state + * - All dependencies injected through {@link IOTelWebSdkConfig} + * - Complete unload support — every instance must fully clean up on unload + * + * @example + * ```typescript + * const sdk = createOTelWebSdk({ + * resource: myResource, + * errorHandlers: myHandlers, + * contextManager: myContextManager, + * idGenerator: myIdGenerator, + * sampler: myAlwaysOnSampler, + * performanceNow: () => performance.now() + * }); + * + * // Get a tracer and create spans + * const tracer = sdk.getTracer("my-service"); + * const span = tracer.startSpan("operation"); + * span.end(); + * + * // Get a logger and emit log records + * const logger = sdk.getLogger("my-service"); + * logger.emit({ body: "Hello, World!" }); + * + * // Cleanup when done + * sdk.shutdown(); + * ``` + * + * @since 4.0.0 + */ +export interface IOTelWebSdk { + /** + * Returns a Tracer for creating spans. + * Tracers are cached by name + version combination — requesting the same + * name and version returns the same Tracer instance. + * + * @param name - The name of the tracer or instrumentation library + * @param version - The version of the tracer or instrumentation library + * @param options - Additional tracer options (e.g., schemaUrl) + * @returns A Tracer with the given name and version + * + * @example + * ```typescript + * const tracer = sdk.getTracer("my-component", "1.0.0"); + * const span = tracer.startSpan("my-operation"); + * ``` + */ + getTracer(name: string, version?: string, options?: IOTelTracerOptions): IOTelTracer; + + /** + * Returns a Logger for emitting log records. + * Loggers are cached by name + version + schemaUrl combination — + * requesting the same combination returns the same Logger instance. + * + * @param name - The name of the logger or instrumentation library + * @param version - The version of the logger or instrumentation library + * @param options - Additional logger options (e.g., schemaUrl, scopeAttributes) + * @returns A Logger with the given name and version + * + * @example + * ```typescript + * const logger = sdk.getLogger("my-component", "1.0.0"); + * logger.emit({ body: "Operation completed", severityText: "INFO" }); + * ``` + */ + getLogger(name: string, version?: string, options?: IOTelLoggerOptions): IOTelLogger; + + // TODO: Phase 5 - Uncomment when metrics are implemented + // /** + // * Returns a Meter for recording metrics. + // * @param name - The name of the meter or instrumentation library + // * @param version - The version of the meter or instrumentation library + // * @param options - Additional meter options + // * @returns A Meter with the given name and version + // */ + // getMeter(name: string, version?: string, options?: IOTelMeterOptions): IOTelMeter; + + /** + * Forces all providers to flush any buffered data. + * This is useful before application shutdown to ensure all telemetry + * is exported. + * + * @returns A promise that resolves when the flush is complete + */ + forceFlush(): IPromise; + + /** + * Shuts down the SDK and releases all resources. + * After shutdown, the SDK instance is no longer usable — all + * subsequent calls to `getTracer` or `getLogger` will return + * no-op implementations. + * + * @remarks + * Shutdown performs the following: + * - Flushes all pending telemetry + * - Shuts down all providers (trace, log) + * - Removes all config change listeners (calls `IUnloadHook.rm()`) + * - Clears all cached instances + * + * @returns A promise that resolves when shutdown is complete + */ + shutdown(): IPromise; + + /** + * Gets the current SDK configuration (read-only snapshot). + * + * @returns The current SDK configuration + */ + getConfig(): Readonly; +} diff --git a/shared/otel-core/src/interfaces/otel/config/IOTelWebSdkConfig.ts b/shared/otel-core/src/interfaces/otel/config/IOTelWebSdkConfig.ts new file mode 100644 index 000000000..8dd264171 --- /dev/null +++ b/shared/otel-core/src/interfaces/otel/config/IOTelWebSdkConfig.ts @@ -0,0 +1,136 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { IOTelContextManager } from "../context/IOTelContextManager"; +import { IOTelLogRecordProcessor } from "../logs/IOTelLogRecordProcessor"; +import { IOTelResource } from "../resources/IOTelResource"; +import { IOTelIdGenerator } from "../trace/IOTelIdGenerator"; +import { IOTelSampler } from "../trace/IOTelSampler"; +import { IOTelErrorHandlers } from "./IOTelErrorHandlers"; + +/** + * Configuration interface for the OpenTelemetry Web SDK. + * Provides all configuration options required for SDK initialization. + * + * + * @remarks + * - All properties must be provided during SDK creation + * - Local caching of config values uses `onConfigChange` callbacks + * - Supports dynamic configuration — config values can change at runtime + * + * @example + * ```typescript + * const config: IOTelWebSdkConfig = { + * resource: myResource, + * errorHandlers: myErrorHandlers, + * contextManager: myContextManager, + * idGenerator: myIdGenerator, + * sampler: myAlwaysOnSampler, + * logProcessors: [myLogProcessor], + * performanceNow: () => performance.now() + * }; + * + * const sdk = createOTelWebSdk(config); + * ``` + * + * @since 4.0.0 + */ +export interface IOTelWebSdkConfig { + /** + * Resource information for telemetry source identification. + * Provides attributes that describe the entity producing telemetry, + * such as service name, version, and environment. + * + * @remarks + * The resource is shared across all providers (trace, log, metrics) + * within this SDK instance. + */ + resource: IOTelResource; + + /** + * Error handlers for SDK internal diagnostics. + * Provides hooks to customize how different types of errors and + * diagnostic messages are handled within the SDK. + * + * @remarks + * Error handlers are propagated to all sub-components created by the SDK. + * If individual handler callbacks are not provided, default behavior + * (console logging) is used. + * + * @see {@link IOTelErrorHandlers} + */ + errorHandlers: IOTelErrorHandlers; + + /** + * Context manager implementation. + * Manages the propagation of context (including active spans) across + * asynchronous operations. + * + * @see {@link IOTelContextManager} + */ + contextManager: IOTelContextManager; + + /** + * ID generator for span and trace IDs. + * Generates unique identifiers for distributed tracing. + * + * @see {@link IOTelIdGenerator} + */ + idGenerator: IOTelIdGenerator; + + /** + * Sampler implementation. + * Determines which traces/spans should be recorded and exported. + * + * @see {@link IOTelSampler} + */ + sampler: IOTelSampler; + + /** + * Performance timing function. + * Injected for testability — allows tests to control time measurement. + * + * @returns The current high-resolution time in milliseconds + * + * @example + * ```typescript + * // Production usage + * performanceNow: () => performance.now() + * + * // Test usage with fake timers + * performanceNow: () => fakeTimer.now() + * ``` + */ + performanceNow: () => number; + + /** + * Log record processors for the log pipeline. + * Each processor receives log records and can transform, filter, + * or export them. + * + * @remarks + * Processors are invoked in order. If not provided, defaults to + * an empty array (no log processing). + * + * @see {@link IOTelLogRecordProcessor} + */ + logProcessors?: IOTelLogRecordProcessor[]; + + // TODO: Phase 2 - Uncomment when IOTelSpanProcessor is implemented + // /** + // * Span processors for the trace pipeline. + // * Each processor receives spans and can transform, filter, + // * or export them. + // * + // * @see IOTelSpanProcessor + // */ + // spanProcessors?: IOTelSpanProcessor[]; + + // TODO: Phase 5 - Uncomment when IOTelMetricReader is implemented + // /** + // * Metric readers for the metric pipeline. + // * + // * @see IOTelMetricReader + // */ + // metricReaders?: IOTelMetricReader[]; +} diff --git a/shared/otel-core/src/interfaces/otel/trace/IOTelTracerCtx.ts b/shared/otel-core/src/interfaces/otel/trace/IOTelTracerCtx.ts index 191411d7b..0b8ac0b1a 100644 --- a/shared/otel-core/src/interfaces/otel/trace/IOTelTracerCtx.ts +++ b/shared/otel-core/src/interfaces/otel/trace/IOTelTracerCtx.ts @@ -8,7 +8,7 @@ import { IOTelSpanOptions } from "./IOTelSpanOptions"; import { IReadableSpan } from "./IReadableSpan"; /** - * The context for the current IOTelSdk instance and it's configuration + * The context for a tracer instance and its configuration * @since 3.4.0 */ export interface IOTelTracerCtx { diff --git a/shared/otel-core/src/otel/api/context/context.ts b/shared/otel-core/src/otel/api/context/context.ts index 7531e8106..6f2b1754c 100644 --- a/shared/otel-core/src/otel/api/context/context.ts +++ b/shared/otel-core/src/otel/api/context/context.ts @@ -54,9 +54,9 @@ export function createContext(otelApi: IOTelApi, parent?: IOTelContext): IOTelCo } function _setValue(key: symbol, value: unknown) { - let newContext = createContext(theContext.api, parent); + let newContext = createContext(theContext.api, theContext); ((newContext as any)[_InternalContextKey.v])[key] = value; - return theContext; + return newContext; } function _deleteValue(key: symbol) { diff --git a/shared/otel-core/src/otel/sdk/OTelSdk.ts b/shared/otel-core/src/otel/sdk/OTelSdk.ts deleted file mode 100644 index 2a516e536..000000000 --- a/shared/otel-core/src/otel/sdk/OTelSdk.ts +++ /dev/null @@ -1,263 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import dynamicProto from "@microsoft/dynamicproto-js"; -import { IAppInsightsCore } from "../../interfaces/ai/IAppInsightsCore"; -import { IConfiguration } from "../../interfaces/ai/IConfiguration"; -import { IProcessTelemetryContext } from "../../interfaces/ai/IProcessTelemetryContext"; -import { ITelemetryItem } from "../../interfaces/ai/ITelemetryItem"; -import { IPlugin } from "../../interfaces/ai/ITelemetryPlugin"; -import { ITelemetryPluginChain } from "../../interfaces/ai/ITelemetryPluginChain"; -import { IOTelApi } from "../../interfaces/otel/IOTelApi"; -import { IOTelSdk } from "../../interfaces/otel/IOTelSdk"; -import { IOTelConfig } from "../../interfaces/otel/config/IOTelConfig"; -import { IOTelTracer } from "../../interfaces/otel/trace/IOTelTracer"; -import { IOTelTracerOptions } from "../../interfaces/otel/trace/IOTelTracerOptions"; - -// interface TraceList { -// name: string; -// tracer: IOTelTracer; -// version?: string; -// schemaUrl?: string; -// } - -// TODO: Enable -// function _createSpanContext(parentSpanContext: SpanContext | null, idGenerator: IOTelIdGenerator): SpanContext { -// const spanId = idGenerator.generateSpanId(); -// let traceId: string; -// let traceState: TraceState; -// if (!parentSpanContext || isSpanContextValid(parentSpanContext)) { -// // if parentSpanContext is not valid, generate a new one -// traceId = idGenerator.generateTraceId(); -// } else { -// traceId = parentSpanContext.traceId; -// traceState = parentSpanContext.traceState; -// } - -// let traceFlags = parentSpanContext ? parentSpanContext.traceFlags : eW3CTraceFlags.None; - -// return { -// traceId: traceId, -// spanId: spanId, -// traceFlags: traceFlags, -// traceState: traceState, -// isRemote: false -// }; -// } - -// function _isSampledOut(sampler: IOTelSampler, context: Context, spanContext: SpanContext, kind: SpanKind, attributes: Attributes, links: Link[]): boolean { -// if (sampler) { -// const samplingResult = sampler.shouldSample(context, spanContext.traceId, spanContext.spanId, kind, attributes, links); -// spanContext.traceState = samplingResult.traceState || spanContext.traceState; -// if (samplingResult.decision === eOTelSamplingDecision.NOT_RECORD) { -// return true; -// } -// } - -// return false; -// } - -export class OTelSdk implements IOTelSdk { - public static identifier: string = "OTelSdk"; - - public identifier: string = OTelSdk.identifier; - public cfg: IOTelConfig; - public api: IOTelApi; - - constructor() { - // NOTE!: DON'T set default values here, instead set them in the _initDefaults() function as it is also called during teardown() - // let _configHandler: IDynamicConfigHandler; - // let _otelApi: ILazyValue; - // let _tracers: { [key: string]: TraceList[] }; - - dynamicProto(OTelSdk, this, (_self, _base) => { - // Set the default values (also called during teardown) - _initDefaults(); - - // objDefineProps(_self, { - // cfg: { g: () => _configHandler.cfg }, - // api: { g: () => _otelApi.v } - // }); - - // Creating the self.initialize = () - _self.initialize = (config: IConfiguration, core: IAppInsightsCore, extensions: IPlugin[], pluginChain?: ITelemetryPluginChain): void => { - // TODO: Enable - // if (!_self.isInitialized()) { - // _base.initialize(config, core, extensions, pluginChain); - - // _populateDefaults(config); - // } - }; - - // TODO: Enable - //_self.getTracer = _getTracer; - - - // function _getTracer(name: string, version?: string, options?: IOTelTracerOptions): IOTelTracer { - // let tracer: IOTelTracer; - - - // let tracerVer = version || STR_EMPTY; - // let tracerSchema = options ? options.schemaUrl : null; - // let keyName = normalizeJsName(name + "@" + tracerVer); - // let tracerList = _tracers?.[keyName]; - - // if (tracerList) { - // arrForEach(tracerList, (item) => { - // if (item.name == name && item.version == tracerVer && item.schemaUrl == tracerSchema) { - // tracer = item.tracer; - // return -1; - // } - // }); - // } else { - // // Ensure _tracers is initialized before accessing it - // if (!_tracers) { - // _tracers = {}; - // } - // tracerList = _tracers[keyName] = []; - // } - - // if (!tracer) { - // Ensure otelApi is available before accessing its properties - - //let otelApi = _otelApi.v; - // let tracerCtx: IOTelTracerCtx = { - // ctxMgr: otelApi?.context, - // //context: _otelSdkCtx.v.context, - // startSpan: _startSpan - // }; - - // tracer = createTracer(tracerCtx, { - // name, - // version, - // schemaUrl: options ? options.schemaUrl : null - // }); - - // // tracerList is guaranteed to be defined by the logic above - // tracerList.push({ name, version: tracerVer, schemaUrl: tracerSchema, tracer }); - // } - - // return tracer; - // } - - // function _startSpan(name: string, options?: SpanOptions, pContext?: Context): IOTelSpan | IReadableSpan { - // let spanOpts = options || {}; - // let kind = spanOpts.kind || SpanKind.INTERNAL; - // let otelApi = _otelApi.v; - // let theContext = pContext || otelApi?.context?.active(); - // let parentSpanContext: SpanContext | null = null; - - // if (spanOpts.root) { - // theContext = deleteContextSpan(theContext); - // } - - // const parentSpan = getContextSpan(theContext); - - // // if Tracing suppressed - // if (!isTracingSuppressed(theContext)) { - // let traceCfg = _configHandler.cfg.traceCfg; - // let idGenerator = traceCfg.idGenerator; - - // parentSpanContext = parentSpan && parentSpan.spanContext(); - // let spanContext = _createSpanContext(parentSpanContext, idGenerator); - // let attributes = spanOpts.attributes || {}; - // let links = spanOpts.links || []; - - // const sampler = traceCfg.sampler; - // if (!_isSampledOut(sampler, theContext, spanContext, kind, attributes, links)) { - - // let spanCtx: IOTelSpanCtx = { - // api: otelApi, - // resource: null, - // instrumentationScope: null, - // context: theContext, - // spanContext: spanContext, - // attributes: attributes, - // links: links, - // isRecording: true, - // startTime: spanOpts.startTime, - // parentSpanContext: parentSpanContext, - // onEnd: (span: IReadableSpan) => { - // _endSpan(this, span); - // } - // }; - - // return createSpan(spanCtx, name, kind); - // } - // } - - // return wrapSpanContext(parentSpanContext); - // } - - // function _endSpan(spanCtx: IOTelSpanCtx, span: IOTelSpan): void { - // if ((span.spanContext().traceFlags & eW3CTraceFlags.Sampled) === 0) { - // return; - // } - // // _self.core.trackTrace({ - // // message: span.name, - // // properties: span.attributes as Attributes - // // }); - // } - - function _initDefaults() { - // Use a default logger so initialization errors are not dropped on the floor with full logging - // TODO: Enable - //_configHandler = createDynamicConfig({} as IOTelConfig, traceApiDefaultConfigValues as any, _self.diagLog()); - // let otelConfig = _configHandler.cfg; - // _tracers = {}; - - - // _otelApi = createDeferredCachedValue(() => { - // let otelApiCtx: IOTelApiCtx = { - // otelCfg: null, - // traceProvider: { - // getTracer: _getTracer - // }, - // diagLogger: _self.diagLog() - // }; - - // // make the config lookup dynamic, so when the config changes we return the current - // objDefine(otelApiCtx, "otelCfg", { g: () => otelConfig }); - - // return createOTelApi(otelApiCtx) - // }); - } - - // function _populateDefaults(config: IConfiguration) { - // _self._addHook(onConfigChange(config, (details) => { - // let config = details.cfg; - // let ctx = createProcessTelemetryContext(null, config, _self.core); - // let _otelConfig = ctx.getExtCfg(OTelSdk.identifier, traceApiDefaultConfigValues, true); - // })); - // } - - }); - } - - public initialize(config: IConfiguration, core: IAppInsightsCore, extensions: IPlugin[], pluginChain?: ITelemetryPluginChain) { - // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging - } - - public processTelemetry(item: ITelemetryItem, itemCtx?: IProcessTelemetryContext) { - // TODO: Enable - //this.processNext(item, itemCtx); - } - - /** - * Returns a Tracer, creating one if one with the given name and version is - * not already created. This may return - * - The same Tracer instance if one has already been created with the same name and version - * - A new Tracer instance if one has not already been created with the same name and version - * - A non-operational Tracer if the provider is not operational - * - * @param name - The name of the tracer or instrumentation library. - * @param version - The version of the tracer or instrumentation library. - * @param options - The options of the tracer or instrumentation library. - * @returns A Tracer with the given name and version - */ - public getTracer(name: string, version?: string, options?: IOTelTracerOptions): IOTelTracer { - // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging - return null; - } - -} diff --git a/shared/otel-core/src/otel/sdk/OTelWebSdk.ts b/shared/otel-core/src/otel/sdk/OTelWebSdk.ts new file mode 100644 index 000000000..9d3a69a2e --- /dev/null +++ b/shared/otel-core/src/otel/sdk/OTelWebSdk.ts @@ -0,0 +1,446 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { IPromise, createAllPromise, createSyncPromise } from "@nevware21/ts-async"; +import { isFunction, objDefine, objForEachKey } from "@nevware21/ts-utils"; +import { createDynamicConfig, onConfigChange } from "../../config/DynamicConfig"; +import { createDistributedTraceContext } from "../../core/TelemetryHelpers"; +import { eW3CTraceFlags } from "../../enums/W3CTraceFlags"; +import { eOTelSamplingDecision } from "../../enums/otel/OTelSamplingDecision"; +import { OTelSpanKind, eOTelSpanKind } from "../../enums/otel/OTelSpanKind"; +import { IDistributedTraceContext } from "../../interfaces/ai/IDistributedTraceContext"; +import { IUnloadHook } from "../../interfaces/ai/IUnloadHook"; +import { IOTelApi } from "../../interfaces/otel/IOTelApi"; +import { IOTelWebSdk } from "../../interfaces/otel/IOTelWebSdk"; +import { IOTelConfig } from "../../interfaces/otel/config/IOTelConfig"; +import { IOTelErrorHandlers } from "../../interfaces/otel/config/IOTelErrorHandlers"; +import { IOTelWebSdkConfig } from "../../interfaces/otel/config/IOTelWebSdkConfig"; +import { IOTelContext } from "../../interfaces/otel/context/IOTelContext"; +import { IOTelLogRecord } from "../../interfaces/otel/logs/IOTelLogRecord"; +import { IOTelLogger } from "../../interfaces/otel/logs/IOTelLogger"; +import { IOTelLoggerOptions } from "../../interfaces/otel/logs/IOTelLoggerOptions"; +import { IOTelSpanCtx } from "../../interfaces/otel/trace/IOTelSpanCtx"; +import { IOTelSpanOptions } from "../../interfaces/otel/trace/IOTelSpanOptions"; +import { IOTelTracer } from "../../interfaces/otel/trace/IOTelTracer"; +import { IOTelTracerOptions } from "../../interfaces/otel/trace/IOTelTracerOptions"; +import { IReadableSpan } from "../../interfaces/otel/trace/IReadableSpan"; +import { handleError, handleWarn } from "../../internal/handleErrors"; +import { setProtoTypeName } from "../../utils/HelperFuncs"; +import { createContext } from "../api/context/context"; +import { createSpan } from "../api/trace/span"; +import { getContextSpan, setContextSpan } from "../api/trace/utils"; +import { createLoggerProvider } from "./OTelLoggerProvider"; + +/** + * Creates a no-op logger that silently discards all emitted log records. + * Used when the SDK has been shut down. + * @returns A no-op IOTelLogger instance + */ +function _createNoopLogger(): IOTelLogger { + return { + emit(_logRecord: IOTelLogRecord): void { + // noop - SDK is shut down + } + }; +} + +/** + * Creates an OpenTelemetry Web SDK instance. + * This is the main entry point factory for the SDK. + * + * The SDK coordinates trace and log providers, manages their lifecycle, + * and ensures complete cleanup on shutdown. + * + * @param config - The SDK configuration with all required dependencies injected + * @returns An initialized IOTelWebSdk instance + * + * @remarks + * - All dependencies must be injected through config — no global state + * - Multiple SDK instances can coexist without interference + * - Config is used directly — never copied with spread operator + * - Local config caching uses `onConfigChange` callbacks + * - Complete unload support — call `shutdown()` to release all resources + * + * @example + * ```typescript + * import { createOTelWebSdk } from "@microsoft/applicationinsights-otelwebsdk-js"; + * + * const sdk = createOTelWebSdk({ + * resource: myResource, + * errorHandlers: { warn: (msg) => console.warn(msg) }, + * contextManager: myContextManager, + * idGenerator: myIdGenerator, + * sampler: myAlwaysOnSampler, + * performanceNow: () => performance.now(), + * logProcessors: [myLogProcessor] + * }); + * + * // Use the SDK + * const tracer = sdk.getTracer("my-service", "1.0.0"); + * const logger = sdk.getLogger("my-service", "1.0.0"); + * + * // Clean up when done + * sdk.shutdown(); + * ``` + * + * @since 3.4.0 + */ +export function createOTelWebSdk(config: IOTelWebSdkConfig): IOTelWebSdk { + // Validate required dependencies upfront + let _handlers: IOTelErrorHandlers = config.errorHandlers; + + if (!config.resource) { + handleError(_handlers, "createOTelWebSdk: resource must be provided"); + } + if (!config.errorHandlers) { + // Use an empty handlers object as fallback so handleError/handleWarn don't fail + _handlers = {}; + handleWarn(_handlers, "createOTelWebSdk: errorHandlers should be provided"); + } + if (!config.contextManager) { + handleError(_handlers, "createOTelWebSdk: contextManager must be provided"); + } + if (!config.idGenerator) { + handleError(_handlers, "createOTelWebSdk: idGenerator must be provided"); + } + if (!config.sampler) { + handleError(_handlers, "createOTelWebSdk: sampler must be provided"); + } + if (!config.performanceNow) { + handleError(_handlers, "createOTelWebSdk: performanceNow must be provided"); + } + + // Private closure state + let _isShutdown = false; + let _tracers: { [key: string]: IOTelTracer } = {}; + let _unloadHooks: IUnloadHook[] = []; + + // Make the config dynamic so we can watch for changes, then cache values + let _sdkConfig = createDynamicConfig(config).cfg; + let _resource = _sdkConfig.resource; + let _contextManager = _sdkConfig.contextManager; + let _idGenerator = _sdkConfig.idGenerator; + let _sampler = _sdkConfig.sampler; + + // Create a minimal IOTelApi adapter that bridges the SDK config to what createSpan needs. + // createSpan() reads api.cfg.errorHandlers and api.cfg.traceCfg — we provide those from + // the SDK config. The host and trace properties are not used by createSpan. + let _otelCfg: IOTelConfig = { + errorHandlers: _handlers + }; + let _apiAdapter = { cfg: _otelCfg } as IOTelApi; + + // Watch for config changes and update cached values + api adapter + let _configUnload = onConfigChange(_sdkConfig, function () { + _resource = _sdkConfig.resource; + _contextManager = _sdkConfig.contextManager; + _idGenerator = _sdkConfig.idGenerator; + _sampler = _sdkConfig.sampler; + _handlers = _sdkConfig.errorHandlers || _handlers; + _otelCfg.errorHandlers = _handlers; + }); + _unloadHooks.push(_configUnload); + + // Create a root context for the SDK so that context operations always have a valid base + let _rootContext: IOTelContext = createContext(_apiAdapter); + + // Create the logger provider using existing factory + let _loggerProvider = createLoggerProvider({ + resource: _resource, + processors: _sdkConfig.logProcessors || [] + }); + + /** + * Returns the current active context from the context manager, falling back + * to the SDK root context if none is active. + * @returns The active IOTelContext + */ + function _getActiveContext(): IOTelContext { + return _contextManager.active() || _rootContext; + } + + // Build the SDK instance using closure pattern + let _self: IOTelWebSdk = {} as IOTelWebSdk; + + _self.getTracer = function (name: string, version?: string, options?: IOTelTracerOptions): IOTelTracer { + if (_isShutdown) { + handleWarn(_handlers, "A shutdown OTelWebSdk cannot provide a Tracer"); + // Return a no-op tracer + return _createNoopTracer(); + } + + let tracerName = name || "unknown"; + let tracerVersion = version || ""; + let schemaUrl = options ? options.schemaUrl || "" : ""; + let key = tracerName + "@" + tracerVersion + ":" + schemaUrl; + + if (!_tracers[key]) { + _tracers[key] = _createSdkTracer(tracerName, tracerVersion); + } + + return _tracers[key]; + }; + + _self.getLogger = function (name: string, version?: string, options?: IOTelLoggerOptions): IOTelLogger { + if (_isShutdown) { + handleWarn(_handlers, "A shutdown OTelWebSdk cannot provide a Logger"); + return _createNoopLogger(); + } + + return _loggerProvider.getLogger(name, version, options); + }; + + _self.forceFlush = function (): IPromise { + if (_isShutdown) { + handleWarn(_handlers, "Cannot force flush a shutdown OTelWebSdk"); + return createSyncPromise(function (resolve) { + resolve(); + }); + } + + let operations: IPromise[] = []; + + // Flush the logger provider + if (_loggerProvider.forceFlush) { + let result = _loggerProvider.forceFlush(); + if (result) { + operations.push(result); + } + } + + // TODO: Phase 2 - Flush span processors when available + + if (operations.length > 0) { + return createAllPromise(operations).then(function (): void { + // All flushed + }); + } + + return createSyncPromise(function (resolve) { + resolve(); + }); + }; + + _self.shutdown = function (): IPromise { + if (_isShutdown) { + handleWarn(_handlers, "shutdown may only be called once per OTelWebSdk"); + return createSyncPromise(function (resolve) { + resolve(); + }); + } + + _isShutdown = true; + + let operations: IPromise[] = []; + + // Shutdown the logger provider + if (_loggerProvider.shutdown) { + let result = _loggerProvider.shutdown(); + if (result) { + operations.push(result); + } + } + + // TODO: Phase 2 - Shutdown span processors when available + + // Remove all config change listeners + for (let i = 0; i < _unloadHooks.length; i++) { + _unloadHooks[i].rm(); + } + _unloadHooks = []; + + // Clear cached tracers + _tracers = {}; + + if (operations.length > 0) { + return createAllPromise(operations).then(function (): void { + // All shut down + }); + } + + return createSyncPromise(function (resolve) { + resolve(); + }); + }; + + _self.getConfig = function (): Readonly { + return _sdkConfig; + }; + + /** + * Creates a tracer instance for this SDK. + * The tracer creates spans using the SDK's context manager, ID generator, and sampler. + * Follows the OpenTelemetry Tracer specification for span creation and context management. + * + * @param tracerName - The name of the tracer (instrumentation library) + * @param tracerVersion - The version of the tracer (instrumentation library) + * @returns An IOTelTracer instance that creates functional spans + */ + function _createSdkTracer(tracerName: string, tracerVersion: string): IOTelTracer { + + /** + * Starts a new span without setting it on the current context. + * Handles ID generation, sampling, and parent span propagation. + * + * @param spanName - The name of the span + * @param options - Optional span creation options (kind, attributes, links, startTime, root) + * @param context - Optional context to extract parent span from; defaults to active context + * @returns A new IReadableSpan, or null if the SDK is shutdown + */ + function _startSpan(spanName: string, options?: IOTelSpanOptions, context?: IOTelContext): IReadableSpan | null { + if (_isShutdown) { + return null; + } + + let opts = options || {}; + let kind: OTelSpanKind = opts.kind || eOTelSpanKind.INTERNAL; + let activeCtx = context || _getActiveContext(); + let parentSpanCtx: IDistributedTraceContext = null; + let newCtx: IDistributedTraceContext; + + // Determine parent span context unless root span is requested + if (!opts.root && activeCtx) { + let parentSpan = getContextSpan(activeCtx); + if (parentSpan) { + parentSpanCtx = parentSpan.spanContext(); + } + } + + // Create the new span's distributed trace context + if (parentSpanCtx) { + // Child span — inherits traceId from parent + newCtx = createDistributedTraceContext(parentSpanCtx); + } else { + // Root span — new trace + newCtx = createDistributedTraceContext(); + newCtx.traceId = _idGenerator.generateTraceId(); + } + + // Always generate a new spanId for this span + newCtx.spanId = _idGenerator.generateSpanId(); + + // Run the sampler to decide whether to record this span + let attributes = opts.attributes || {}; + let links = opts.links || []; + let samplingResult = _sampler.shouldSample( + activeCtx, newCtx.traceId, spanName, kind, attributes, links + ); + + // Determine recording and sampled flags from the sampling decision + let isRecording = samplingResult.decision !== eOTelSamplingDecision.NOT_RECORD; + let isSampled = samplingResult.decision === eOTelSamplingDecision.RECORD_AND_SAMPLED; + + // Set trace flags based on sampling decision + newCtx.traceFlags = isSampled ? eW3CTraceFlags.Sampled : eW3CTraceFlags.None; + + // Apply trace state from sampler if provided + if (samplingResult.traceState) { + // The sampler may have provided an updated trace state + // Note: createDistributedTraceContext handles trace state internally + // For now we rely on the context's built-in trace state management + } + + // Merge sampler-provided attributes with user-provided attributes + let spanAttributes = attributes; + if (isRecording && samplingResult.attributes) { + // Merge: user attributes take precedence, sampler attributes fill gaps + spanAttributes = {}; + let samplerAttrs = samplingResult.attributes; + objForEachKey(samplerAttrs, function (key, value) { + spanAttributes[key] = value; + }); + objForEachKey(attributes, function (key, value) { + spanAttributes[key] = value; + }); + } + + // Build the span context for createSpan + let spanCtx: IOTelSpanCtx = { + api: _apiAdapter, + resource: _resource, + instrumentationScope: { name: tracerName, version: tracerVersion }, + spanContext: newCtx, + attributes: spanAttributes, + startTime: opts.startTime, + isRecording: isRecording + // TODO: Phase 2 - Add onEnd callback for span processor notification + }; + + // Set parent span context as a non-writable property if parent exists + if (parentSpanCtx) { + objDefine(spanCtx, "parentSpanContext", { + v: parentSpanCtx, + w: false + }); + } + + return createSpan(spanCtx, spanName, kind); + } + + let tracer: IOTelTracer = setProtoTypeName({ + startSpan: function (spanName: string, options?: IOTelSpanOptions, context?: IOTelContext): IReadableSpan | null { + return _startSpan(spanName, options, context); + }, + startActiveSpan: function unknown>( + spanNameArg: string, + optionsOrFn?: IOTelSpanOptions | F, + fnOrContext?: F | IOTelContext, + maybeFn?: F + ): ReturnType { + // Resolve overloaded parameters: + // Overload 1: startActiveSpan(name, fn) + // Overload 2: startActiveSpan(name, options, fn) + // Overload 3: startActiveSpan(name, options, context, fn) + let opts: IOTelSpanOptions = null; + let fn: F = null; + let ctx: IOTelContext = null; + + if (isFunction(optionsOrFn)) { + // Overload 1: (name, fn) + fn = optionsOrFn as F; + } else if (isFunction(fnOrContext)) { + // Overload 2: (name, options, fn) + opts = optionsOrFn as IOTelSpanOptions; + fn = fnOrContext as F; + } else { + // Overload 3: (name, options, context, fn) + opts = optionsOrFn as IOTelSpanOptions; + ctx = fnOrContext as IOTelContext; + fn = maybeFn; + } + + // Create the span using the resolved parameters + let span = _startSpan(spanNameArg, opts, ctx); + + // Set the span as active in a new context and execute the callback + let activeCtx = ctx || _getActiveContext(); + let contextWithSpan = setContextSpan(activeCtx, span); + + return _contextManager.with(contextWithSpan, function () { + return fn(span); + }) as ReturnType; + } + }, "OTelTracer (" + tracerName + "@" + tracerVersion + ")"); + + return tracer; + } + + /** + * Creates a no-op tracer that does not create any spans. + * Used when the SDK has been shut down. + * + * @returns A no-op IOTelTracer instance + */ + function _createNoopTracer(): IOTelTracer { + return setProtoTypeName({ + startSpan: function (): IReadableSpan | null { + return null; + }, + startActiveSpan: function (): undefined { + return undefined; + } + }, "OTelNoopTracer"); + } + + return setProtoTypeName(_self, "OTelWebSdk"); +} diff --git a/shared/otel-core/src/utils/DataCacheHelper.ts b/shared/otel-core/src/utils/DataCacheHelper.ts index cd02749a2..237c33993 100644 --- a/shared/otel-core/src/utils/DataCacheHelper.ts +++ b/shared/otel-core/src/utils/DataCacheHelper.ts @@ -6,7 +6,7 @@ import { STR_EMPTY } from "../constants/InternalConstants"; import { normalizeJsName } from "./HelperFuncs"; import { newId } from "./RandomHelper"; -const version = "#version#"; +const version = '0.0.1-alpha'; let instanceName = "." + newId(6); let _dataUid = 0; From 4b2e813d68742c35a8bf311ade6107c3a8ed026b Mon Sep 17 00:00:00 2001 From: Hector Hernandez <39923391+hectorhdzg@users.noreply.github.com> Date: Fri, 6 Mar 2026 16:37:42 -0800 Subject: [PATCH 2/6] Update shared/otel-core/src/utils/DataCacheHelper.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- shared/otel-core/src/utils/DataCacheHelper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/otel-core/src/utils/DataCacheHelper.ts b/shared/otel-core/src/utils/DataCacheHelper.ts index 237c33993..cd02749a2 100644 --- a/shared/otel-core/src/utils/DataCacheHelper.ts +++ b/shared/otel-core/src/utils/DataCacheHelper.ts @@ -6,7 +6,7 @@ import { STR_EMPTY } from "../constants/InternalConstants"; import { normalizeJsName } from "./HelperFuncs"; import { newId } from "./RandomHelper"; -const version = '0.0.1-alpha'; +const version = "#version#"; let instanceName = "." + newId(6); let _dataUid = 0; From 262b74193a4d973927b034133144b3d84cbdf6c0 Mon Sep 17 00:00:00 2001 From: Hector Hernandez <39923391+hectorhdzg@users.noreply.github.com> Date: Fri, 6 Mar 2026 16:38:01 -0800 Subject: [PATCH 3/6] Update shared/otel-core/src/otel/sdk/OTelWebSdk.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- shared/otel-core/src/otel/sdk/OTelWebSdk.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/otel-core/src/otel/sdk/OTelWebSdk.ts b/shared/otel-core/src/otel/sdk/OTelWebSdk.ts index 9d3a69a2e..f7b2397a4 100644 --- a/shared/otel-core/src/otel/sdk/OTelWebSdk.ts +++ b/shared/otel-core/src/otel/sdk/OTelWebSdk.ts @@ -83,7 +83,7 @@ function _createNoopLogger(): IOTelLogger { * sdk.shutdown(); * ``` * - * @since 3.4.0 + * @since 4.0.0 */ export function createOTelWebSdk(config: IOTelWebSdkConfig): IOTelWebSdk { // Validate required dependencies upfront From 1e23bd4fda4a8f2e8556d8d18c948299fb6fb3a3 Mon Sep 17 00:00:00 2001 From: Hector Hernandez <39923391+hectorhdzg@users.noreply.github.com> Date: Mon, 9 Mar 2026 11:38:37 -0700 Subject: [PATCH 4/6] Address comments --- .../src/interfaces/otel/IOTelWebSdk.ts | 3 +- shared/otel-core/src/otel/sdk/OTelWebSdk.ts | 70 ++++++++++++++++--- 2 files changed, 62 insertions(+), 11 deletions(-) diff --git a/shared/otel-core/src/interfaces/otel/IOTelWebSdk.ts b/shared/otel-core/src/interfaces/otel/IOTelWebSdk.ts index 86441a903..bb819a358 100644 --- a/shared/otel-core/src/interfaces/otel/IOTelWebSdk.ts +++ b/shared/otel-core/src/interfaces/otel/IOTelWebSdk.ts @@ -118,7 +118,8 @@ export interface IOTelWebSdk { shutdown(): IPromise; /** - * Gets the current SDK configuration (read-only snapshot). + * Gets the current SDK configuration as a live reference. + * Callers should treat the returned configuration as read-only. * * @returns The current SDK configuration */ diff --git a/shared/otel-core/src/otel/sdk/OTelWebSdk.ts b/shared/otel-core/src/otel/sdk/OTelWebSdk.ts index f7b2397a4..46bfcd983 100644 --- a/shared/otel-core/src/otel/sdk/OTelWebSdk.ts +++ b/shared/otel-core/src/otel/sdk/OTelWebSdk.ts @@ -89,25 +89,33 @@ export function createOTelWebSdk(config: IOTelWebSdkConfig): IOTelWebSdk { // Validate required dependencies upfront let _handlers: IOTelErrorHandlers = config.errorHandlers; - if (!config.resource) { - handleError(_handlers, "createOTelWebSdk: resource must be provided"); - } if (!config.errorHandlers) { // Use an empty handlers object as fallback so handleError/handleWarn don't fail _handlers = {}; handleWarn(_handlers, "createOTelWebSdk: errorHandlers should be provided"); } + + // Validate all required dependencies and fail fast with a no-op SDK if any are missing + let _hasMissing = false; + if (!config.resource) { + handleError(_handlers, "createOTelWebSdk: resource must be provided"); + _hasMissing = true; + } if (!config.contextManager) { handleError(_handlers, "createOTelWebSdk: contextManager must be provided"); + _hasMissing = true; } if (!config.idGenerator) { handleError(_handlers, "createOTelWebSdk: idGenerator must be provided"); + _hasMissing = true; } if (!config.sampler) { handleError(_handlers, "createOTelWebSdk: sampler must be provided"); + _hasMissing = true; } - if (!config.performanceNow) { - handleError(_handlers, "createOTelWebSdk: performanceNow must be provided"); + + if (_hasMissing) { + return _createNoopSdk(config); } // Private closure state @@ -123,8 +131,9 @@ export function createOTelWebSdk(config: IOTelWebSdkConfig): IOTelWebSdk { let _sampler = _sdkConfig.sampler; // Create a minimal IOTelApi adapter that bridges the SDK config to what createSpan needs. - // createSpan() reads api.cfg.errorHandlers and api.cfg.traceCfg — we provide those from - // the SDK config. The host and trace properties are not used by createSpan. + // createSpan() reads api.cfg.errorHandlers (provided here) and api.cfg.traceCfg (optional, + // accessed with safe-navigation so undefined is fine). The host and trace properties are + // not used by createSpan. let _otelCfg: IOTelConfig = { errorHandlers: _handlers }; @@ -175,7 +184,7 @@ export function createOTelWebSdk(config: IOTelWebSdkConfig): IOTelWebSdk { let key = tracerName + "@" + tracerVersion + ":" + schemaUrl; if (!_tracers[key]) { - _tracers[key] = _createSdkTracer(tracerName, tracerVersion); + _tracers[key] = _createSdkTracer(tracerName, tracerVersion, schemaUrl); } return _tracers[key]; @@ -276,7 +285,7 @@ export function createOTelWebSdk(config: IOTelWebSdkConfig): IOTelWebSdk { * @param tracerVersion - The version of the tracer (instrumentation library) * @returns An IOTelTracer instance that creates functional spans */ - function _createSdkTracer(tracerName: string, tracerVersion: string): IOTelTracer { + function _createSdkTracer(tracerName: string, tracerVersion: string, schemaUrl: string): IOTelTracer { /** * Starts a new span without setting it on the current context. @@ -358,7 +367,7 @@ export function createOTelWebSdk(config: IOTelWebSdkConfig): IOTelWebSdk { let spanCtx: IOTelSpanCtx = { api: _apiAdapter, resource: _resource, - instrumentationScope: { name: tracerName, version: tracerVersion }, + instrumentationScope: { name: tracerName, version: tracerVersion, schemaUrl: schemaUrl || undefined }, spanContext: newCtx, attributes: spanAttributes, startTime: opts.startTime, @@ -444,3 +453,44 @@ export function createOTelWebSdk(config: IOTelWebSdkConfig): IOTelWebSdk { return setProtoTypeName(_self, "OTelWebSdk"); } + +/** + * Creates a no-op SDK instance that silently discards all operations. + * Returned when required dependencies are missing to prevent runtime crashes. + * @param config - The original config, used for getConfig() + * @returns A safe no-op IOTelWebSdk instance + */ +function _createNoopSdk(config: IOTelWebSdkConfig): IOTelWebSdk { + let _resolvedPromise = createSyncPromise(function (resolve: () => void) { + resolve(); + }); + + return setProtoTypeName({ + getTracer: function (): IOTelTracer { + return setProtoTypeName({ + startSpan: function (): IReadableSpan | null { + return null; + }, + startActiveSpan: function (): undefined { + return undefined; + } + }, "OTelNoopTracer"); + }, + getLogger: function (): IOTelLogger { + return { + emit: function (): void { + // noop + } + }; + }, + forceFlush: function (): IPromise { + return _resolvedPromise; + }, + shutdown: function (): IPromise { + return _resolvedPromise; + }, + getConfig: function (): Readonly { + return config; + } + }, "OTelNoopWebSdk"); +} From 11fa3e197cf875b8176d1f1b197c8040e30643ed Mon Sep 17 00:00:00 2001 From: Hector Hernandez <39923391+hectorhdzg@users.noreply.github.com> Date: Tue, 17 Mar 2026 14:38:29 -0700 Subject: [PATCH 5/6] Update --- shared/otel-core/CONTEXT.md | 3 - .../Unit/src/sdk/OTelLoggerProvider.Tests.ts | 15 +- .../Tests/Unit/src/sdk/OTelWebSdk.Tests.ts | 117 ++----- .../src/interfaces/otel/IOTelWebSdk.ts | 15 +- .../otel/config/IOTelWebSdkConfig.ts | 20 +- .../otel/logs/IOTelLoggerProvider.ts | 2 +- .../src/interfaces/otel/trace/IOTelSpanCtx.ts | 3 +- .../src/otel/sdk/OTelLoggerProvider.ts | 14 +- shared/otel-core/src/otel/sdk/OTelWebSdk.ts | 293 +++++++----------- 9 files changed, 156 insertions(+), 326 deletions(-) diff --git a/shared/otel-core/CONTEXT.md b/shared/otel-core/CONTEXT.md index 107f49cba..7b45fa3a6 100644 --- a/shared/otel-core/CONTEXT.md +++ b/shared/otel-core/CONTEXT.md @@ -404,9 +404,6 @@ export interface IOTelWebSdkConfig { /** REQUIRED: Logger for SDK internal diagnostics */ logger: IOTelLogger; - /** REQUIRED: Performance timing function (injected for testability) */ - performanceNow: () => number; - /** REQUIRED: Span processors for trace pipeline */ spanProcessors: IOTelSpanProcessor[]; diff --git a/shared/otel-core/Tests/Unit/src/sdk/OTelLoggerProvider.Tests.ts b/shared/otel-core/Tests/Unit/src/sdk/OTelLoggerProvider.Tests.ts index e456c8ff9..d221f205b 100644 --- a/shared/otel-core/Tests/Unit/src/sdk/OTelLoggerProvider.Tests.ts +++ b/shared/otel-core/Tests/Unit/src/sdk/OTelLoggerProvider.Tests.ts @@ -39,7 +39,7 @@ export class OTelLoggerProviderTests extends AITestClass { }); this.testCase({ - name: "LoggerProvider: constructor without options should use noop processor by default", + name: "LoggerProvider: constructor without options should use default processor", test: (): IPromise => { const provider = createLoggerProvider(); const sharedState = this._getSharedState(provider); @@ -296,21 +296,14 @@ export class OTelLoggerProviderTests extends AITestClass { }); this.testCase({ - name: "LoggerProvider: shutdown should return noop logger for new requests", + name: "LoggerProvider: shutdown should return null for new requests", test: (): IPromise => { const provider = createLoggerProvider(); return createPromise((resolve, reject) => { provider.shutdown().then(() => { try { const logger = provider.getLogger("default", "1.0.0"); - Assert.equal(typeof logger.emit, "function", "Logger should expose emit function after shutdown"); - let threw = false; - try { - logger.emit({} as IOTelLogRecord); - } catch (e) { - threw = true; - } - Assert.ok(!threw, "Logger emit should not throw after shutdown"); + Assert.equal(logger, null, "Logger should be null after shutdown"); resolve(); } catch (e) { reject(e); @@ -394,7 +387,7 @@ export class OTelLoggerProviderTests extends AITestClass { /** * Creates a mock log record processor for testing purposes. - * This avoids dependency on the noop package. + * This avoids dependency on a separate mock package. */ private _createMockProcessor(): IOTelLogRecordProcessor { return { diff --git a/shared/otel-core/Tests/Unit/src/sdk/OTelWebSdk.Tests.ts b/shared/otel-core/Tests/Unit/src/sdk/OTelWebSdk.Tests.ts index 6003865e5..01557eeb0 100644 --- a/shared/otel-core/Tests/Unit/src/sdk/OTelWebSdk.Tests.ts +++ b/shared/otel-core/Tests/Unit/src/sdk/OTelWebSdk.Tests.ts @@ -90,101 +90,62 @@ export class OTelWebSdkTests extends AITestClass { private _registerValidationTests(): void { this.testCase({ - name: "OTelWebSdk: should call error handler when resource is missing", + name: "OTelWebSdk: should use default errorHandlers when not provided", test: () => { - let errorCalled = false; let config = this._createValidConfig(); - config.errorHandlers = { - error: function (msg) { - errorCalled = true; - } - }; - (config as any).resource = null; + (config as any).errorHandlers = null; this._sdk = createOTelWebSdk(config); - Assert.ok(errorCalled, "Error handler should be called for missing resource"); + Assert.ok(this._sdk, "SDK should be created with default errorHandlers"); + let returnedConfig = this._sdk.getConfig(); + Assert.ok(returnedConfig.errorHandlers, "Config should have default errorHandlers"); } }); this.testCase({ - name: "OTelWebSdk: should call error handler when contextManager is missing", + name: "OTelWebSdk: should create SDK with missing resource and still return tracer", test: () => { - let errorCalled = false; let config = this._createValidConfig(); - config.errorHandlers = { - error: function (msg) { - errorCalled = true; - } - }; - (config as any).contextManager = null; + (config as any).resource = null; this._sdk = createOTelWebSdk(config); - Assert.ok(errorCalled, "Error handler should be called for missing contextManager"); + Assert.ok(this._sdk, "SDK should be created even without resource"); + let tracer = this._sdk.getTracer("test"); + Assert.ok(tracer, "Should still return a tracer when resource is missing (resource is metadata, not a functional dependency)"); } }); this.testCase({ - name: "OTelWebSdk: should call error handler when idGenerator is missing", + name: "OTelWebSdk: should create SDK with missing contextManager and return null tracer", test: () => { - let errorCalled = false; let config = this._createValidConfig(); - config.errorHandlers = { - error: function (msg) { - errorCalled = true; - } - }; - (config as any).idGenerator = null; + (config as any).contextManager = null; this._sdk = createOTelWebSdk(config); - Assert.ok(errorCalled, "Error handler should be called for missing idGenerator"); + Assert.ok(this._sdk, "SDK should be created even without contextManager"); + let tracer = this._sdk.getTracer("test"); + Assert.equal(tracer, null, "Should return null when required dependencies are missing"); } }); this.testCase({ - name: "OTelWebSdk: should call error handler when sampler is missing", + name: "OTelWebSdk: should create SDK with missing idGenerator and return null tracer", test: () => { - let errorCalled = false; let config = this._createValidConfig(); - config.errorHandlers = { - error: function (msg) { - errorCalled = true; - } - }; - (config as any).sampler = null; + (config as any).idGenerator = null; this._sdk = createOTelWebSdk(config); - Assert.ok(errorCalled, "Error handler should be called for missing sampler"); + Assert.ok(this._sdk, "SDK should be created even without idGenerator"); + let tracer = this._sdk.getTracer("test"); + Assert.equal(tracer, null, "Should return null when required dependencies are missing"); } }); this.testCase({ - name: "OTelWebSdk: should call error handler when performanceNow is missing", + name: "OTelWebSdk: should create SDK with missing sampler and return null tracer", test: () => { - let errorCalled = false; let config = this._createValidConfig(); - config.errorHandlers = { - error: function (msg) { - errorCalled = true; - } - }; - (config as any).performanceNow = null; + (config as any).sampler = null; this._sdk = createOTelWebSdk(config); - Assert.ok(errorCalled, "Error handler should be called for missing performanceNow"); - } - }); - - this.testCase({ - name: "OTelWebSdk: should warn when errorHandlers are missing", - test: () => { - let warnCalled = false; - let origWarn = console.warn; - console.warn = function () { - warnCalled = true; - }; - try { - let config = this._createValidConfig(); - (config as any).errorHandlers = null; - this._sdk = createOTelWebSdk(config); - Assert.ok(warnCalled, "Should warn about missing errorHandlers"); - } finally { - console.warn = origWarn; - } + Assert.ok(this._sdk, "SDK should be created even without sampler"); + let tracer = this._sdk.getTracer("test"); + Assert.equal(tracer, null, "Should return null when required dependencies are missing"); } }); } @@ -435,7 +396,7 @@ export class OTelWebSdkTests extends AITestClass { }); this.testCase({ - name: "OTelWebSdk: startSpan should return null after shutdown", + name: "OTelWebSdk: getTracer should return null after shutdown", test: (): IPromise => { let sdk = createOTelWebSdk(this._createValidConfig()); this._sdk = sdk; @@ -443,8 +404,7 @@ export class OTelWebSdkTests extends AITestClass { sdk.shutdown().then(function () { try { let tracer = sdk.getTracer("test"); - let span = tracer.startSpan("test-span"); - Assert.equal(span, null, "startSpan after shutdown should return null"); + Assert.equal(tracer, null, "getTracer after shutdown should return null"); resolve(); } catch (e) { reject(e); @@ -810,7 +770,7 @@ export class OTelWebSdkTests extends AITestClass { }); this.testCase({ - name: "OTelWebSdk: getTracer after shutdown should return no-op tracer", + name: "OTelWebSdk: getTracer after shutdown should return null", test: (): IPromise => { let sdk = createOTelWebSdk(this._createValidConfig()); this._sdk = sdk; @@ -818,12 +778,7 @@ export class OTelWebSdkTests extends AITestClass { sdk.shutdown().then(function () { try { let tracer = sdk.getTracer("test"); - Assert.ok(tracer, "Should return a tracer (no-op) after shutdown"); - Assert.equal(typeof tracer.startSpan, "function", "No-op tracer should have startSpan"); - Assert.equal(typeof tracer.startActiveSpan, "function", "No-op tracer should have startActiveSpan"); - // Verify the no-op tracer returns null for startSpan - let span = tracer.startSpan("test-span"); - Assert.equal(span, null, "No-op tracer startSpan should return null"); + Assert.equal(tracer, null, "Should return null after shutdown"); resolve(); } catch (e) { reject(e); @@ -834,7 +789,7 @@ export class OTelWebSdkTests extends AITestClass { }); this.testCase({ - name: "OTelWebSdk: getLogger after shutdown should return no-op logger", + name: "OTelWebSdk: getLogger after shutdown should return null", test: (): IPromise => { let sdk = createOTelWebSdk(this._createValidConfig()); this._sdk = sdk; @@ -842,15 +797,7 @@ export class OTelWebSdkTests extends AITestClass { sdk.shutdown().then(function () { try { let logger = sdk.getLogger("test"); - Assert.ok(logger, "Should return a logger (no-op) after shutdown"); - // Verify the no-op logger does not throw - let threw = false; - try { - logger.emit({ body: "after shutdown" }); - } catch (e) { - threw = true; - } - Assert.ok(!threw, "No-op logger emit should not throw"); + Assert.equal(logger, null, "Should return null after shutdown"); resolve(); } catch (e) { reject(e); @@ -986,7 +933,6 @@ export class OTelWebSdkTests extends AITestClass { Assert.equal(returnedConfig.contextManager, config.contextManager, "Config contextManager should match"); Assert.equal(returnedConfig.idGenerator, config.idGenerator, "Config idGenerator should match"); Assert.equal(returnedConfig.sampler, config.sampler, "Config sampler should match"); - Assert.equal(returnedConfig.performanceNow, config.performanceNow, "Config performanceNow should match"); } }); @@ -1012,7 +958,6 @@ export class OTelWebSdkTests extends AITestClass { contextManager: this._createMockContextManager(), idGenerator: this._createMockIdGenerator(), sampler: this._createMockSampler(), - performanceNow: function () { return Date.now(); }, logProcessors: [] }; } diff --git a/shared/otel-core/src/interfaces/otel/IOTelWebSdk.ts b/shared/otel-core/src/interfaces/otel/IOTelWebSdk.ts index bb819a358..5b7a7a600 100644 --- a/shared/otel-core/src/interfaces/otel/IOTelWebSdk.ts +++ b/shared/otel-core/src/interfaces/otel/IOTelWebSdk.ts @@ -25,8 +25,7 @@ import { IOTelTracerOptions } from "./trace/IOTelTracerOptions"; * errorHandlers: myHandlers, * contextManager: myContextManager, * idGenerator: myIdGenerator, - * sampler: myAlwaysOnSampler, - * performanceNow: () => performance.now() + * sampler: myAlwaysOnSampler * }); * * // Get a tracer and create spans @@ -53,7 +52,8 @@ export interface IOTelWebSdk { * @param name - The name of the tracer or instrumentation library * @param version - The version of the tracer or instrumentation library * @param options - Additional tracer options (e.g., schemaUrl) - * @returns A Tracer with the given name and version + * @returns A Tracer with the given name and version, or null if the SDK is shutdown or + * required dependencies are not configured * * @example * ```typescript @@ -61,7 +61,7 @@ export interface IOTelWebSdk { * const span = tracer.startSpan("my-operation"); * ``` */ - getTracer(name: string, version?: string, options?: IOTelTracerOptions): IOTelTracer; + getTracer(name: string, version?: string, options?: IOTelTracerOptions): IOTelTracer | null; /** * Returns a Logger for emitting log records. @@ -71,7 +71,7 @@ export interface IOTelWebSdk { * @param name - The name of the logger or instrumentation library * @param version - The version of the logger or instrumentation library * @param options - Additional logger options (e.g., schemaUrl, scopeAttributes) - * @returns A Logger with the given name and version + * @returns A Logger with the given name and version, or null if the SDK is shutdown * * @example * ```typescript @@ -79,7 +79,7 @@ export interface IOTelWebSdk { * logger.emit({ body: "Operation completed", severityText: "INFO" }); * ``` */ - getLogger(name: string, version?: string, options?: IOTelLoggerOptions): IOTelLogger; + getLogger(name: string, version?: string, options?: IOTelLoggerOptions): IOTelLogger | null; // TODO: Phase 5 - Uncomment when metrics are implemented // /** @@ -103,8 +103,7 @@ export interface IOTelWebSdk { /** * Shuts down the SDK and releases all resources. * After shutdown, the SDK instance is no longer usable — all - * subsequent calls to `getTracer` or `getLogger` will return - * no-op implementations. + * subsequent calls to `getTracer` or `getLogger` will return null. * * @remarks * Shutdown performs the following: diff --git a/shared/otel-core/src/interfaces/otel/config/IOTelWebSdkConfig.ts b/shared/otel-core/src/interfaces/otel/config/IOTelWebSdkConfig.ts index 8dd264171..9cd791f40 100644 --- a/shared/otel-core/src/interfaces/otel/config/IOTelWebSdkConfig.ts +++ b/shared/otel-core/src/interfaces/otel/config/IOTelWebSdkConfig.ts @@ -26,8 +26,7 @@ import { IOTelErrorHandlers } from "./IOTelErrorHandlers"; * contextManager: myContextManager, * idGenerator: myIdGenerator, * sampler: myAlwaysOnSampler, - * logProcessors: [myLogProcessor], - * performanceNow: () => performance.now() + * logProcessors: [myLogProcessor] * }; * * const sdk = createOTelWebSdk(config); @@ -86,23 +85,6 @@ export interface IOTelWebSdkConfig { */ sampler: IOTelSampler; - /** - * Performance timing function. - * Injected for testability — allows tests to control time measurement. - * - * @returns The current high-resolution time in milliseconds - * - * @example - * ```typescript - * // Production usage - * performanceNow: () => performance.now() - * - * // Test usage with fake timers - * performanceNow: () => fakeTimer.now() - * ``` - */ - performanceNow: () => number; - /** * Log record processors for the log pipeline. * Each processor receives log records and can transform, filter, diff --git a/shared/otel-core/src/interfaces/otel/logs/IOTelLoggerProvider.ts b/shared/otel-core/src/interfaces/otel/logs/IOTelLoggerProvider.ts index 384ef4751..d25d13ebe 100644 --- a/shared/otel-core/src/interfaces/otel/logs/IOTelLoggerProvider.ts +++ b/shared/otel-core/src/interfaces/otel/logs/IOTelLoggerProvider.ts @@ -17,5 +17,5 @@ export interface IOTelLoggerProvider { * @param options The options of the logger or instrumentation library. * @returns Logger A Logger with the given name and version */ - getLogger(name: string, version?: string, options?: IOTelLoggerOptions): IOTelLogger; + getLogger(name: string, version?: string, options?: IOTelLoggerOptions): IOTelLogger | null; } diff --git a/shared/otel-core/src/interfaces/otel/trace/IOTelSpanCtx.ts b/shared/otel-core/src/interfaces/otel/trace/IOTelSpanCtx.ts index a7ed6a06d..54bbc9678 100644 --- a/shared/otel-core/src/interfaces/otel/trace/IOTelSpanCtx.ts +++ b/shared/otel-core/src/interfaces/otel/trace/IOTelSpanCtx.ts @@ -6,6 +6,7 @@ import { OTelTimeInput } from "../../IOTelHrTime"; import { IDistributedTraceContext } from "../../ai/IDistributedTraceContext"; import { IOTelApi } from "../IOTelApi"; import { IOTelAttributes } from "../IOTelAttributes"; +import { IAttributeContainer } from "../attribute/IAttributeContainer"; import { IOTelContext } from "../context/IOTelContext"; import { IOTelResource } from "../resources/IOTelResource"; import { IOTelInstrumentationScope } from "./IOTelInstrumentationScope"; @@ -50,7 +51,7 @@ export interface IOTelSpanCtx { parentSpanContext?: IDistributedTraceContext; - attributes?: IOTelAttributes; + attributes?: IOTelAttributes | IAttributeContainer; links?: IOTelLink[] diff --git a/shared/otel-core/src/otel/sdk/OTelLoggerProvider.ts b/shared/otel-core/src/otel/sdk/OTelLoggerProvider.ts index 535f94103..1a23f48aa 100644 --- a/shared/otel-core/src/otel/sdk/OTelLoggerProvider.ts +++ b/shared/otel-core/src/otel/sdk/OTelLoggerProvider.ts @@ -3,7 +3,6 @@ import { IPromise } from "@nevware21/ts-async"; import { IOTelErrorHandlers } from "../../interfaces/otel/config/IOTelErrorHandlers"; -import { IOTelLogRecord } from "../../interfaces/otel/logs/IOTelLogRecord"; import { IOTelLogger } from "../../interfaces/otel/logs/IOTelLogger"; import { IOTelLoggerOptions } from "../../interfaces/otel/logs/IOTelLoggerOptions"; import { IOTelLoggerProvider } from "../../interfaces/otel/logs/IOTelLoggerProvider"; @@ -17,15 +16,6 @@ import { loadDefaultConfig, reconfigureLimits } from "./config"; export const DEFAULT_LOGGER_NAME = "unknown"; -// Inline noop logger for shutdown scenarios -function _createInlineNoopLogger(): IOTelLogger { - return { - emit(_logRecord: IOTelLogRecord): void { - // noop - logger is shut down - } - }; -} - export function createLoggerProvider( config: IOTelLoggerProviderConfig = {} ): IOTelLoggerProvider & { @@ -58,10 +48,10 @@ export function createLoggerProvider( name: string, version?: string, options?: IOTelLoggerOptions - ): IOTelLogger { + ): IOTelLogger | null { if (isShutdown) { handleWarn(handlers, "A shutdown LoggerProvider cannot provide a Logger"); - return _createInlineNoopLogger(); + return null; } if (!name) { diff --git a/shared/otel-core/src/otel/sdk/OTelWebSdk.ts b/shared/otel-core/src/otel/sdk/OTelWebSdk.ts index 46bfcd983..18a3a6b5e 100644 --- a/shared/otel-core/src/otel/sdk/OTelWebSdk.ts +++ b/shared/otel-core/src/otel/sdk/OTelWebSdk.ts @@ -1,8 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { IPromise, createAllPromise, createSyncPromise } from "@nevware21/ts-async"; -import { isFunction, objDefine, objForEachKey } from "@nevware21/ts-utils"; +import { IPromise, createAllPromise } from "@nevware21/ts-async"; +import { isFunction, objDeepFreeze, objDefine } from "@nevware21/ts-utils"; +import { cfgDfMerge } from "../../config/ConfigDefaultHelpers"; import { createDynamicConfig, onConfigChange } from "../../config/DynamicConfig"; import { createDistributedTraceContext } from "../../core/TelemetryHelpers"; import { eW3CTraceFlags } from "../../enums/W3CTraceFlags"; @@ -10,21 +11,25 @@ import { eOTelSamplingDecision } from "../../enums/otel/OTelSamplingDecision"; import { OTelSpanKind, eOTelSpanKind } from "../../enums/otel/OTelSpanKind"; import { IDistributedTraceContext } from "../../interfaces/ai/IDistributedTraceContext"; import { IUnloadHook } from "../../interfaces/ai/IUnloadHook"; +import { IConfigDefaults } from "../../interfaces/config/IConfigDefaults"; import { IOTelApi } from "../../interfaces/otel/IOTelApi"; +import { IOTelAttributes } from "../../interfaces/otel/IOTelAttributes"; import { IOTelWebSdk } from "../../interfaces/otel/IOTelWebSdk"; +import { IAttributeContainer } from "../../interfaces/otel/attribute/IAttributeContainer"; import { IOTelConfig } from "../../interfaces/otel/config/IOTelConfig"; import { IOTelErrorHandlers } from "../../interfaces/otel/config/IOTelErrorHandlers"; import { IOTelWebSdkConfig } from "../../interfaces/otel/config/IOTelWebSdkConfig"; import { IOTelContext } from "../../interfaces/otel/context/IOTelContext"; -import { IOTelLogRecord } from "../../interfaces/otel/logs/IOTelLogRecord"; import { IOTelLogger } from "../../interfaces/otel/logs/IOTelLogger"; import { IOTelLoggerOptions } from "../../interfaces/otel/logs/IOTelLoggerOptions"; +import { IOTelSamplingResult } from "../../interfaces/otel/trace/IOTelSamplingResult"; import { IOTelSpanCtx } from "../../interfaces/otel/trace/IOTelSpanCtx"; import { IOTelSpanOptions } from "../../interfaces/otel/trace/IOTelSpanOptions"; import { IOTelTracer } from "../../interfaces/otel/trace/IOTelTracer"; import { IOTelTracerOptions } from "../../interfaces/otel/trace/IOTelTracerOptions"; import { IReadableSpan } from "../../interfaces/otel/trace/IReadableSpan"; -import { handleError, handleWarn } from "../../internal/handleErrors"; +import { handleWarn } from "../../internal/handleErrors"; +import { addAttributes, createAttributeContainer } from "../../otel/attribute/attributeContainer"; import { setProtoTypeName } from "../../utils/HelperFuncs"; import { createContext } from "../api/context/context"; import { createSpan } from "../api/trace/span"; @@ -32,17 +37,38 @@ import { getContextSpan, setContextSpan } from "../api/trace/utils"; import { createLoggerProvider } from "./OTelLoggerProvider"; /** - * Creates a no-op logger that silently discards all emitted log records. - * Used when the SDK has been shut down. - * @returns A no-op IOTelLogger instance + * Default configuration for the OTelWebSdk. + * Ensures every config property is defined so that createDynamicConfig + * makes all properties dynamic. */ -function _createNoopLogger(): IOTelLogger { - return { - emit(_logRecord: IOTelLogRecord): void { - // noop - SDK is shut down - } - }; -} +const _defaultConfig: IConfigDefaults = objDeepFreeze({ + resource: { + isVal: function (v: any) { + return !!v; + }, + v: null as any + }, + errorHandlers: cfgDfMerge({}), + contextManager: { + isVal: function (v: any) { + return !!v; + }, + v: null as any + }, + idGenerator: { + isVal: function (v: any) { + return !!v; + }, + v: null as any + }, + sampler: { + isVal: function (v: any) { + return !!v; + }, + v: null as any + }, + logProcessors: { ref: true, v: [] } +}); /** * Creates an OpenTelemetry Web SDK instance. @@ -71,7 +97,6 @@ function _createNoopLogger(): IOTelLogger { * contextManager: myContextManager, * idGenerator: myIdGenerator, * sampler: myAlwaysOnSampler, - * performanceNow: () => performance.now(), * logProcessors: [myLogProcessor] * }); * @@ -86,45 +111,14 @@ function _createNoopLogger(): IOTelLogger { * @since 4.0.0 */ export function createOTelWebSdk(config: IOTelWebSdkConfig): IOTelWebSdk { - // Validate required dependencies upfront - let _handlers: IOTelErrorHandlers = config.errorHandlers; - - if (!config.errorHandlers) { - // Use an empty handlers object as fallback so handleError/handleWarn don't fail - _handlers = {}; - handleWarn(_handlers, "createOTelWebSdk: errorHandlers should be provided"); - } - - // Validate all required dependencies and fail fast with a no-op SDK if any are missing - let _hasMissing = false; - if (!config.resource) { - handleError(_handlers, "createOTelWebSdk: resource must be provided"); - _hasMissing = true; - } - if (!config.contextManager) { - handleError(_handlers, "createOTelWebSdk: contextManager must be provided"); - _hasMissing = true; - } - if (!config.idGenerator) { - handleError(_handlers, "createOTelWebSdk: idGenerator must be provided"); - _hasMissing = true; - } - if (!config.sampler) { - handleError(_handlers, "createOTelWebSdk: sampler must be provided"); - _hasMissing = true; - } - - if (_hasMissing) { - return _createNoopSdk(config); - } - // Private closure state let _isShutdown = false; let _tracers: { [key: string]: IOTelTracer } = {}; let _unloadHooks: IUnloadHook[] = []; - // Make the config dynamic so we can watch for changes, then cache values - let _sdkConfig = createDynamicConfig(config).cfg; + // Make the config dynamic with defaults so every property is dynamic + let _sdkConfig = createDynamicConfig(config, _defaultConfig).cfg; + let _handlers: IOTelErrorHandlers = _sdkConfig.errorHandlers; let _resource = _sdkConfig.resource; let _contextManager = _sdkConfig.contextManager; let _idGenerator = _sdkConfig.idGenerator; @@ -145,7 +139,7 @@ export function createOTelWebSdk(config: IOTelWebSdkConfig): IOTelWebSdk { _contextManager = _sdkConfig.contextManager; _idGenerator = _sdkConfig.idGenerator; _sampler = _sdkConfig.sampler; - _handlers = _sdkConfig.errorHandlers || _handlers; + _handlers = _sdkConfig.errorHandlers; _otelCfg.errorHandlers = _handlers; }); _unloadHooks.push(_configUnload); @@ -171,11 +165,15 @@ export function createOTelWebSdk(config: IOTelWebSdkConfig): IOTelWebSdk { // Build the SDK instance using closure pattern let _self: IOTelWebSdk = {} as IOTelWebSdk; - _self.getTracer = function (name: string, version?: string, options?: IOTelTracerOptions): IOTelTracer { + _self.getTracer = function (name: string, version?: string, options?: IOTelTracerOptions): IOTelTracer | null { if (_isShutdown) { handleWarn(_handlers, "A shutdown OTelWebSdk cannot provide a Tracer"); - // Return a no-op tracer - return _createNoopTracer(); + return null; + } + + if (!_idGenerator || !_sampler || !_contextManager) { + handleWarn(_handlers, "OTelWebSdk: required dependencies not configured"); + return null; } let tracerName = name || "unknown"; @@ -190,10 +188,10 @@ export function createOTelWebSdk(config: IOTelWebSdkConfig): IOTelWebSdk { return _tracers[key]; }; - _self.getLogger = function (name: string, version?: string, options?: IOTelLoggerOptions): IOTelLogger { + _self.getLogger = function (name: string, version?: string, options?: IOTelLoggerOptions): IOTelLogger | null { if (_isShutdown) { handleWarn(_handlers, "A shutdown OTelWebSdk cannot provide a Logger"); - return _createNoopLogger(); + return null; } return _loggerProvider.getLogger(name, version, options); @@ -202,8 +200,8 @@ export function createOTelWebSdk(config: IOTelWebSdkConfig): IOTelWebSdk { _self.forceFlush = function (): IPromise { if (_isShutdown) { handleWarn(_handlers, "Cannot force flush a shutdown OTelWebSdk"); - return createSyncPromise(function (resolve) { - resolve(); + return createAllPromise([] as IPromise[]).then(function (): void { + // Already shut down }); } @@ -219,22 +217,16 @@ export function createOTelWebSdk(config: IOTelWebSdkConfig): IOTelWebSdk { // TODO: Phase 2 - Flush span processors when available - if (operations.length > 0) { - return createAllPromise(operations).then(function (): void { - // All flushed - }); - } - - return createSyncPromise(function (resolve) { - resolve(); + return createAllPromise(operations || []).then(function (): void { + // All flushed }); }; _self.shutdown = function (): IPromise { if (_isShutdown) { handleWarn(_handlers, "shutdown may only be called once per OTelWebSdk"); - return createSyncPromise(function (resolve) { - resolve(); + return createAllPromise([] as IPromise[]).then(function (): void { + // Already shut down }); } @@ -261,14 +253,8 @@ export function createOTelWebSdk(config: IOTelWebSdkConfig): IOTelWebSdk { // Clear cached tracers _tracers = {}; - if (operations.length > 0) { - return createAllPromise(operations).then(function (): void { - // All shut down - }); - } - - return createSyncPromise(function (resolve) { - resolve(); + return createAllPromise(operations || []).then(function (): void { + // All shut down }); }; @@ -331,9 +317,9 @@ export function createOTelWebSdk(config: IOTelWebSdkConfig): IOTelWebSdk { // Run the sampler to decide whether to record this span let attributes = opts.attributes || {}; let links = opts.links || []; - let samplingResult = _sampler.shouldSample( + let samplingResult: IOTelSamplingResult = _sampler ? _sampler.shouldSample( activeCtx, newCtx.traceId, spanName, kind, attributes, links - ); + ) : { decision: eOTelSamplingDecision.RECORD_AND_SAMPLED }; // Determine recording and sampled flags from the sampling decision let isRecording = samplingResult.decision !== eOTelSamplingDecision.NOT_RECORD; @@ -350,17 +336,13 @@ export function createOTelWebSdk(config: IOTelWebSdkConfig): IOTelWebSdk { } // Merge sampler-provided attributes with user-provided attributes - let spanAttributes = attributes; + // Use AttributeContainer to avoid copying/enumerating all attributes unless needed + let spanAttributes: IOTelAttributes | IAttributeContainer = attributes; if (isRecording && samplingResult.attributes) { - // Merge: user attributes take precedence, sampler attributes fill gaps - spanAttributes = {}; - let samplerAttrs = samplingResult.attributes; - objForEachKey(samplerAttrs, function (key, value) { - spanAttributes[key] = value; - }); - objForEachKey(attributes, function (key, value) { - spanAttributes[key] = value; - }); + // Sampler attributes are inherited; user attributes are added on top (take precedence) + let attrContainer = createAttributeContainer(_otelCfg, spanName, samplingResult.attributes); + addAttributes(attrContainer, attributes); + spanAttributes = attrContainer; } // Build the span context for createSpan @@ -386,111 +368,52 @@ export function createOTelWebSdk(config: IOTelWebSdkConfig): IOTelWebSdk { return createSpan(spanCtx, spanName, kind); } - let tracer: IOTelTracer = setProtoTypeName({ - startSpan: function (spanName: string, options?: IOTelSpanOptions, context?: IOTelContext): IReadableSpan | null { - return _startSpan(spanName, options, context); - }, - startActiveSpan: function unknown>( - spanNameArg: string, - optionsOrFn?: IOTelSpanOptions | F, - fnOrContext?: F | IOTelContext, - maybeFn?: F - ): ReturnType { - // Resolve overloaded parameters: - // Overload 1: startActiveSpan(name, fn) - // Overload 2: startActiveSpan(name, options, fn) - // Overload 3: startActiveSpan(name, options, context, fn) - let opts: IOTelSpanOptions = null; - let fn: F = null; - let ctx: IOTelContext = null; - - if (isFunction(optionsOrFn)) { - // Overload 1: (name, fn) - fn = optionsOrFn as F; - } else if (isFunction(fnOrContext)) { - // Overload 2: (name, options, fn) - opts = optionsOrFn as IOTelSpanOptions; - fn = fnOrContext as F; - } else { - // Overload 3: (name, options, context, fn) - opts = optionsOrFn as IOTelSpanOptions; - ctx = fnOrContext as IOTelContext; - fn = maybeFn; - } + function _startActiveSpan unknown>( + spanNameArg: string, + optionsOrFn?: IOTelSpanOptions | F, + fnOrContext?: F | IOTelContext, + maybeFn?: F + ): ReturnType { + // Resolve overloaded parameters: + // Overload 1: startActiveSpan(name, fn) + // Overload 2: startActiveSpan(name, options, fn) + // Overload 3: startActiveSpan(name, options, context, fn) + let opts: IOTelSpanOptions = null; + let fn: F = null; + let ctx: IOTelContext = null; + + if (isFunction(optionsOrFn)) { + // Overload 1: (name, fn) + fn = optionsOrFn as F; + } else if (isFunction(fnOrContext)) { + // Overload 2: (name, options, fn) + opts = optionsOrFn as IOTelSpanOptions; + fn = fnOrContext as F; + } else { + // Overload 3: (name, options, context, fn) + opts = optionsOrFn as IOTelSpanOptions; + ctx = fnOrContext as IOTelContext; + fn = maybeFn; + } - // Create the span using the resolved parameters - let span = _startSpan(spanNameArg, opts, ctx); + // Create the span using the resolved parameters + let span = _startSpan(spanNameArg, opts, ctx); - // Set the span as active in a new context and execute the callback - let activeCtx = ctx || _getActiveContext(); - let contextWithSpan = setContextSpan(activeCtx, span); + // Set the span as active in a new context and execute the callback + // TODO: Refactor to use withSpan/useSpan helpers once OTelWebSdk supports ITraceHost + let activeCtx = ctx || _getActiveContext(); + let contextWithSpan = setContextSpan(activeCtx, span); - return _contextManager.with(contextWithSpan, function () { - return fn(span); - }) as ReturnType; - } + return _contextManager.with(contextWithSpan, fn as (...args: any[]) => ReturnType, undefined, span) as ReturnType; + } + + let tracer: IOTelTracer = setProtoTypeName({ + startSpan: _startSpan, + startActiveSpan: _startActiveSpan }, "OTelTracer (" + tracerName + "@" + tracerVersion + ")"); return tracer; } - /** - * Creates a no-op tracer that does not create any spans. - * Used when the SDK has been shut down. - * - * @returns A no-op IOTelTracer instance - */ - function _createNoopTracer(): IOTelTracer { - return setProtoTypeName({ - startSpan: function (): IReadableSpan | null { - return null; - }, - startActiveSpan: function (): undefined { - return undefined; - } - }, "OTelNoopTracer"); - } - return setProtoTypeName(_self, "OTelWebSdk"); } - -/** - * Creates a no-op SDK instance that silently discards all operations. - * Returned when required dependencies are missing to prevent runtime crashes. - * @param config - The original config, used for getConfig() - * @returns A safe no-op IOTelWebSdk instance - */ -function _createNoopSdk(config: IOTelWebSdkConfig): IOTelWebSdk { - let _resolvedPromise = createSyncPromise(function (resolve: () => void) { - resolve(); - }); - - return setProtoTypeName({ - getTracer: function (): IOTelTracer { - return setProtoTypeName({ - startSpan: function (): IReadableSpan | null { - return null; - }, - startActiveSpan: function (): undefined { - return undefined; - } - }, "OTelNoopTracer"); - }, - getLogger: function (): IOTelLogger { - return { - emit: function (): void { - // noop - } - }; - }, - forceFlush: function (): IPromise { - return _resolvedPromise; - }, - shutdown: function (): IPromise { - return _resolvedPromise; - }, - getConfig: function (): Readonly { - return config; - } - }, "OTelNoopWebSdk"); -} From 7e647712881ec106fa88ea2836b3d9197be27c0a Mon Sep 17 00:00:00 2001 From: Hector Hernandez <39923391+hectorhdzg@users.noreply.github.com> Date: Thu, 19 Mar 2026 13:23:24 -0700 Subject: [PATCH 6/6] Addresing comments --- shared/otel-core/src/otel/sdk/OTelWebSdk.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/shared/otel-core/src/otel/sdk/OTelWebSdk.ts b/shared/otel-core/src/otel/sdk/OTelWebSdk.ts index 18a3a6b5e..86f09539b 100644 --- a/shared/otel-core/src/otel/sdk/OTelWebSdk.ts +++ b/shared/otel-core/src/otel/sdk/OTelWebSdk.ts @@ -49,6 +49,8 @@ const _defaultConfig: IConfigDefaults = objDeepFreeze({ v: null as any }, errorHandlers: cfgDfMerge({}), + // TODO: Review defaults for contextManager, idGenerator, and sampler. + // The SDK instance itself should be the default contextManager (or manage an internal one). contextManager: { isVal: function (v: any) { return !!v; @@ -399,6 +401,10 @@ export function createOTelWebSdk(config: IOTelWebSdkConfig): IOTelWebSdk { // Create the span using the resolved parameters let span = _startSpan(spanNameArg, opts, ctx); + if (!span) { + return undefined as ReturnType; + } + // Set the span as active in a new context and execute the callback // TODO: Refactor to use withSpan/useSpan helpers once OTelWebSdk supports ITraceHost let activeCtx = ctx || _getActiveContext();