From 3340a5b89feede6c4c45aea0c7a5130d731540fc Mon Sep 17 00:00:00 2001 From: Hector Hernandez <39923391+hectorhdzg@users.noreply.github.com> Date: Fri, 20 Mar 2026 13:01:43 -0700 Subject: [PATCH 1/3] Phase 0 implementation --- .../Tests/Unit/src/sdk/OTelLogger.Tests.ts | 22 +++- .../Unit/src/sdk/OTelLoggerProvider.Tests.ts | 84 ++++++++----- .../sdk/OTelMultiLogRecordProcessor.Tests.ts | 28 ++++- .../otel-core/planning/IMPLEMENTATION_PLAN.md | 7 +- shared/otel-core/src/ext/ValueSanitizer.ts | 4 +- shared/otel-core/src/index.ts | 4 + .../otel/context/IContextManagerConfig.ts | 24 ++++ .../otel/logs/IOTelLoggerProviderConfig.ts | 30 ++++- .../otel/resources/IOTelResource.ts | 8 ++ .../otel/trace/ITracerProviderConfig.ts | 30 +++++ shared/otel-core/src/otel/api/OTelApi.ts | 4 +- .../src/otel/api/context/contextManager.ts | 67 +++++++++-- shared/otel-core/src/otel/api/trace/span.ts | 47 ++++++-- shared/otel-core/src/otel/api/trace/tracer.ts | 13 +- .../src/otel/api/trace/tracerProvider.ts | 79 +++++++++--- .../otel-core/src/otel/resource/resource.ts | 26 +++- .../src/otel/sdk/OTelLoggerProvider.ts | 112 +++++++++++++----- shared/otel-core/src/otel/sdk/OTelWebSdk.ts | 1 + 18 files changed, 474 insertions(+), 116 deletions(-) create mode 100644 shared/otel-core/src/interfaces/otel/context/IContextManagerConfig.ts create mode 100644 shared/otel-core/src/interfaces/otel/trace/ITracerProviderConfig.ts diff --git a/shared/otel-core/Tests/Unit/src/sdk/OTelLogger.Tests.ts b/shared/otel-core/Tests/Unit/src/sdk/OTelLogger.Tests.ts index bc3e64be3..cd5ad595f 100644 --- a/shared/otel-core/Tests/Unit/src/sdk/OTelLogger.Tests.ts +++ b/shared/otel-core/Tests/Unit/src/sdk/OTelLogger.Tests.ts @@ -11,6 +11,9 @@ import { setContextSpanContext } from "../../../../src/otel/api/trace/utils"; import { createLogger } from "../../../../src/otel/sdk/OTelLogger"; import { createResolvedPromise } from "@nevware21/ts-async"; import { IOTelApi, IOTelConfig } from "../../../../src"; +import { IOTelResource, OTelRawResourceAttribute } from "../../../../src/interfaces/otel/resources/IOTelResource"; +import { IOTelAttributes } from "../../../../src/interfaces/otel/IOTelAttributes"; +import { IOTelLoggerProviderConfig } from "../../../../src/interfaces/otel/logs/IOTelLoggerProviderConfig"; // W3C trace flags constant for sampled traces const eW3CTraceFlags_Sampled = 1; @@ -38,9 +41,7 @@ export class OTelLoggerTests extends AITestClass { private setup() { const logProcessor = this._createMockProcessor(); - const provider = createLoggerProvider({ - processors: [logProcessor] - }); + const provider = createLoggerProvider(this._cfg([logProcessor])); const logger = provider.getLogger("test name", "test version", { schemaUrl: "test schema url" }) as LoggerWithScope; @@ -53,7 +54,7 @@ export class OTelLoggerTests extends AITestClass { name: "Logger: factory returns logger instance", test: () => { const logProcessor = this._createMockProcessor(); - const provider = createLoggerProvider({ processors: [logProcessor] }); + const provider = createLoggerProvider(this._cfg([logProcessor])); const sharedState = provider._sharedState; const scope: IOTelInstrumentationScope = { name: "test name", @@ -151,4 +152,17 @@ export class OTelLoggerTests extends AITestClass { shutdown: () => createResolvedPromise(undefined) }; } + + private _cfg(processors: IOTelLogRecordProcessor[]): IOTelLoggerProviderConfig { + const resource: IOTelResource = { + attributes: {} as IOTelAttributes, + merge: () => resource, + getRawAttributes: () => [] as OTelRawResourceAttribute[] + }; + return { + resource: resource, + errorHandlers: {}, + processors: processors + }; + } } 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 d221f205b..3c0a943fa 100644 --- a/shared/otel-core/Tests/Unit/src/sdk/OTelLoggerProvider.Tests.ts +++ b/shared/otel-core/Tests/Unit/src/sdk/OTelLoggerProvider.Tests.ts @@ -11,6 +11,8 @@ import { createMultiLogRecordProcessor } from "../../../../src/otel/sdk/OTelMult import { loadDefaultConfig } from "../../../../src/otel/sdk/config"; import { IOTelResource, OTelRawResourceAttribute } from "../../../../src/interfaces/otel/resources/IOTelResource"; import { IOTelLogRecordProcessor } from "../../../../src/interfaces/otel/logs/IOTelLogRecordProcessor"; +import { IOTelLoggerProviderConfig } from "../../../../src/interfaces/otel/logs/IOTelLoggerProviderConfig"; +import { IOTelErrorHandlers } from "../../../../src/interfaces/otel/config/IOTelErrorHandlers"; import { createResolvedPromise } from "@nevware21/ts-async"; type LoggerProviderInstance = ReturnType; @@ -31,7 +33,7 @@ export class OTelLoggerProviderTests extends AITestClass { this.testCase({ name: "LoggerProvider: constructor without options should construct an instance", test: () => { - const provider = createLoggerProvider(); + const provider = createLoggerProvider(this._cfg()); Assert.equal(typeof provider.getLogger, "function", "Should create a LoggerProvider instance"); const sharedState = provider._sharedState; Assert.ok(sharedState.loggers instanceof Map, "Should expose shared state instance"); @@ -41,7 +43,7 @@ export class OTelLoggerProviderTests extends AITestClass { this.testCase({ name: "LoggerProvider: constructor without options should use default processor", test: (): IPromise => { - const provider = createLoggerProvider(); + const provider = createLoggerProvider(this._cfg()); const sharedState = this._getSharedState(provider); Assert.equal(sharedState.registeredLogRecordProcessors.length, 0, "Expected no processors to be registered by default"); const flushResult = sharedState.activeProcessor.forceFlush(); @@ -53,9 +55,9 @@ export class OTelLoggerProviderTests extends AITestClass { name: "LoggerProvider: constructor should register provided processors", test: () => { const logRecordProcessor = this._createMockProcessor(); - const provider = createLoggerProvider({ + const provider = createLoggerProvider(this._cfg({ processors: [logRecordProcessor] - }); + })); const sharedState = this._getSharedState(provider); const activeProcessor = sharedState.activeProcessor as MultiLogRecordProcessorInstance; Assert.equal(activeProcessor.processors.length, 1, "Should register one processor"); @@ -66,7 +68,7 @@ export class OTelLoggerProviderTests extends AITestClass { this.testCase({ name: "LoggerProvider: constructor should use default resource when not provided", test: () => { - const provider = createLoggerProvider(); + const provider = createLoggerProvider(this._cfg()); const sharedState = this._getSharedState(provider); const resource = sharedState.resource; Assert.ok(!!resource, "Should have a resource available"); @@ -78,9 +80,9 @@ export class OTelLoggerProviderTests extends AITestClass { name: "LoggerProvider: constructor should honor provided resource", test: () => { const passedInResource = this._createTestResource({ foo: "bar" }); - const provider = createLoggerProvider({ + const provider = createLoggerProvider(this._cfg({ resource: passedInResource - }); + })); const sharedState = this._getSharedState(provider); Assert.equal(sharedState.resource, passedInResource, "Should use the provided resource instance"); } @@ -89,7 +91,7 @@ export class OTelLoggerProviderTests extends AITestClass { this.testCase({ name: "LoggerProvider: constructor should use default forceFlushTimeoutMillis when omitted", test: () => { - const provider = createLoggerProvider(); + const provider = createLoggerProvider(this._cfg()); const sharedState = this._getSharedState(provider); Assert.equal(sharedState.forceFlushTimeoutMillis, loadDefaultConfig().forceFlushTimeoutMillis, "Should use default forceFlush timeout"); } @@ -98,7 +100,7 @@ export class OTelLoggerProviderTests extends AITestClass { this.testCase({ name: "LoggerProvider: logRecordLimits should default values when not provided", test: () => { - const provider = createLoggerProvider(); + const provider = createLoggerProvider(this._cfg()); const sharedState = this._getSharedState(provider); Assert.deepEqual(sharedState.logRecordLimits, { attributeCountLimit: 128, @@ -110,11 +112,11 @@ export class OTelLoggerProviderTests extends AITestClass { this.testCase({ name: "LoggerProvider: logRecordLimits should respect provided attributeCountLimit", test: () => { - const provider = createLoggerProvider({ + const provider = createLoggerProvider(this._cfg({ logRecordLimits: { attributeCountLimit: 100 } - }); + })); const sharedState = this._getSharedState(provider); Assert.equal(sharedState.logRecordLimits.attributeCountLimit, 100, "Should use provided attributeCountLimit"); } @@ -123,11 +125,11 @@ export class OTelLoggerProviderTests extends AITestClass { this.testCase({ name: "LoggerProvider: logRecordLimits should respect provided attributeValueLengthLimit", test: () => { - const provider = createLoggerProvider({ + const provider = createLoggerProvider(this._cfg({ logRecordLimits: { attributeValueLengthLimit: 10 } - }); + })); const sharedState = this._getSharedState(provider); Assert.equal(sharedState.logRecordLimits.attributeValueLengthLimit, 10, "Should use provided attributeValueLengthLimit"); } @@ -136,11 +138,11 @@ export class OTelLoggerProviderTests extends AITestClass { this.testCase({ name: "LoggerProvider: logRecordLimits should allow negative attributeValueLengthLimit", test: () => { - const provider = createLoggerProvider({ + const provider = createLoggerProvider(this._cfg({ logRecordLimits: { attributeValueLengthLimit: -10 } - }); + })); const sharedState = this._getSharedState(provider); Assert.equal(sharedState.logRecordLimits.attributeValueLengthLimit, -10, "Should preserve provided negative attributeValueLengthLimit"); } @@ -149,7 +151,7 @@ export class OTelLoggerProviderTests extends AITestClass { this.testCase({ name: "LoggerProvider: logRecordLimits should use default attributeValueLengthLimit when omitted", test: () => { - const provider = createLoggerProvider(); + const provider = createLoggerProvider(this._cfg()); const sharedState = this._getSharedState(provider); Assert.equal(sharedState.logRecordLimits.attributeValueLengthLimit, Infinity, "Should default attributeValueLengthLimit to Infinity"); } @@ -158,7 +160,7 @@ export class OTelLoggerProviderTests extends AITestClass { this.testCase({ name: "LoggerProvider: logRecordLimits should use default attributeCountLimit when omitted", test: () => { - const provider = createLoggerProvider(); + const provider = createLoggerProvider(this._cfg()); const sharedState = this._getSharedState(provider); Assert.equal(sharedState.logRecordLimits.attributeCountLimit, 128, "Should default attributeCountLimit to 128"); } @@ -167,7 +169,7 @@ export class OTelLoggerProviderTests extends AITestClass { this.testCase({ name: "LoggerProvider: getLogger should default name when invalid", test: () => { - const provider = createLoggerProvider(); + const provider = createLoggerProvider(this._cfg()); const logger = provider.getLogger("") as LoggerWithScope; Assert.equal(logger.instrumentationScope.name, DEFAULT_LOGGER_NAME, "Should use default logger name when name is invalid"); } @@ -176,7 +178,7 @@ export class OTelLoggerProviderTests extends AITestClass { this.testCase({ name: "LoggerProvider: getLogger should create new logger when name not seen", test: () => { - const provider = createLoggerProvider(); + const provider = createLoggerProvider(this._cfg()); const sharedState = this._getSharedState(provider); Assert.equal(sharedState.loggers.size, 0, "Should start with no loggers"); provider.getLogger("test name"); @@ -190,7 +192,7 @@ export class OTelLoggerProviderTests extends AITestClass { const testName = "test name"; const testVersion = "test version"; const testSchemaUrl = "test schema url"; - const provider = createLoggerProvider(); + const provider = createLoggerProvider(this._cfg()); const sharedState = this._getSharedState(provider); Assert.equal(sharedState.loggers.size, 0, "Should start with no loggers"); @@ -209,7 +211,7 @@ export class OTelLoggerProviderTests extends AITestClass { const testName = "test name"; const testVersion = "test version"; const testSchemaUrl = "test schema url"; - const provider = createLoggerProvider(); + const provider = createLoggerProvider(this._cfg()); const sharedState = this._getSharedState(provider); Assert.equal(sharedState.loggers.size, 0, "Should start with no loggers"); @@ -234,7 +236,7 @@ export class OTelLoggerProviderTests extends AITestClass { const processor2 = this._createMockProcessor(); const forceFlushStub1 = this.sandbox.stub(processor1, "forceFlush").resolves(); const forceFlushStub2 = this.sandbox.stub(processor2, "forceFlush").resolves(); - const provider = createLoggerProvider({ processors: [processor1, processor2] }); + const provider = createLoggerProvider(this._cfg({ processors: [processor1, processor2] })); return createPromise((resolve, reject) => { provider.forceFlush().then(() => { @@ -257,7 +259,7 @@ export class OTelLoggerProviderTests extends AITestClass { const processor2 = this._createMockProcessor(); const forceFlushStub1 = this.sandbox.stub(processor1, "forceFlush").rejects("Error"); const forceFlushStub2 = this.sandbox.stub(processor2, "forceFlush").rejects("Error"); - const provider = createLoggerProvider({ processors: [processor1, processor2] }); + const provider = createLoggerProvider(this._cfg({ processors: [processor1, processor2] })); return createPromise((resolve, reject) => { provider.forceFlush().then(() => { @@ -280,7 +282,7 @@ export class OTelLoggerProviderTests extends AITestClass { test: (): IPromise => { const processor = this._createMockProcessor(); const shutdownStub = this.sandbox.stub(processor, "shutdown").resolves(); - const provider = createLoggerProvider({ processors: [processor] }); + const provider = createLoggerProvider(this._cfg({ processors: [processor] })); return createPromise((resolve, reject) => { provider.shutdown().then(() => { @@ -298,7 +300,7 @@ export class OTelLoggerProviderTests extends AITestClass { this.testCase({ name: "LoggerProvider: shutdown should return null for new requests", test: (): IPromise => { - const provider = createLoggerProvider(); + const provider = createLoggerProvider(this._cfg()); return createPromise((resolve, reject) => { provider.shutdown().then(() => { try { @@ -317,7 +319,7 @@ export class OTelLoggerProviderTests extends AITestClass { name: "LoggerProvider: forceFlush after shutdown should not call processors", test: (): IPromise => { const logRecordProcessor = this._createMockProcessor(); - const provider = createLoggerProvider({ processors: [logRecordProcessor] }); + const provider = createLoggerProvider(this._cfg({ processors: [logRecordProcessor] })); const forceFlushStub = this.sandbox.stub(logRecordProcessor, "forceFlush").resolves(); const warnStub = this.sandbox.stub(console, "warn"); @@ -341,7 +343,7 @@ export class OTelLoggerProviderTests extends AITestClass { name: "LoggerProvider: second shutdown should not re-run processor shutdown", test: (): IPromise => { const logRecordProcessor = this._createMockProcessor(); - const provider = createLoggerProvider({ processors: [logRecordProcessor] }); + const provider = createLoggerProvider(this._cfg({ processors: [logRecordProcessor] })); const shutdownStub = this.sandbox.stub(logRecordProcessor, "shutdown").resolves(); const warnStub = this.sandbox.stub(console, "warn"); @@ -366,6 +368,34 @@ export class OTelLoggerProviderTests extends AITestClass { return provider._sharedState; } + /** + * Creates a valid IOTelLoggerProviderConfig with required fields plus any overrides. + */ + private _cfg(overrides?: Partial): IOTelLoggerProviderConfig { + let cfg: IOTelLoggerProviderConfig = { + resource: this._createTestResource(), + errorHandlers: {} as IOTelErrorHandlers + }; + if (overrides) { + if (overrides.resource !== undefined) { + cfg.resource = overrides.resource; + } + if (overrides.errorHandlers !== undefined) { + cfg.errorHandlers = overrides.errorHandlers; + } + if (overrides.processors !== undefined) { + cfg.processors = overrides.processors; + } + if (overrides.forceFlushTimeoutMillis !== undefined) { + cfg.forceFlushTimeoutMillis = overrides.forceFlushTimeoutMillis; + } + if (overrides.logRecordLimits !== undefined) { + cfg.logRecordLimits = overrides.logRecordLimits; + } + } + return cfg; + } + private _createTestResource(attributes: IOTelAttributes = {} as IOTelAttributes): IOTelResource { const resourceAttributes: IOTelAttributes = {} as IOTelAttributes; const rawAttributes: OTelRawResourceAttribute[] = []; diff --git a/shared/otel-core/Tests/Unit/src/sdk/OTelMultiLogRecordProcessor.Tests.ts b/shared/otel-core/Tests/Unit/src/sdk/OTelMultiLogRecordProcessor.Tests.ts index f32a5b36c..9163e9657 100644 --- a/shared/otel-core/Tests/Unit/src/sdk/OTelMultiLogRecordProcessor.Tests.ts +++ b/shared/otel-core/Tests/Unit/src/sdk/OTelMultiLogRecordProcessor.Tests.ts @@ -7,6 +7,9 @@ import { IOTelSdkLogRecord } from "../../../../src/interfaces/otel/logs/IOTelSdk import { createLoggerProvider } from "../../../../src/otel/sdk/OTelLoggerProvider"; import { createMultiLogRecordProcessor } from "../../../../src/otel/sdk/OTelMultiLogRecordProcessor"; import { loadDefaultConfig } from "../../../../src/otel/sdk/config"; +import { IOTelLoggerProviderConfig } from "../../../../src/interfaces/otel/logs/IOTelLoggerProviderConfig"; +import { IOTelResource, OTelRawResourceAttribute } from "../../../../src/interfaces/otel/resources/IOTelResource"; +import { IOTelAttributes } from "../../../../src/interfaces/otel/IOTelAttributes"; class TestProcessor implements IOTelLogRecordProcessor { public logRecords: IOTelSdkLogRecord[] = []; @@ -46,6 +49,23 @@ const setup = (processors?: IOTelLogRecordProcessor[]) => { return { multiProcessor, forceFlushTimeoutMillis }; }; +function _testResource(): IOTelResource { + const resource: IOTelResource = { + attributes: {} as IOTelAttributes, + merge: () => resource, + getRawAttributes: () => [] as OTelRawResourceAttribute[] + }; + return resource; +} + +function _cfg(processors: IOTelLogRecordProcessor[]): IOTelLoggerProviderConfig { + return { + resource: _testResource(), + errorHandlers: {}, + processors: processors + }; +} + export class OTelMultiLogRecordProcessorTests extends AITestClass { public testInitialize() { super.testInitialize(); @@ -70,7 +90,7 @@ export class OTelMultiLogRecordProcessorTests extends AITestClass { name: "MultiLogRecordProcessor: onEmit - should no-op when no processors registered", test: () => { const { multiProcessor } = setup(); - const provider = createLoggerProvider({ processors: [multiProcessor] }); + const provider = createLoggerProvider(_cfg([multiProcessor])); const logger = provider.getLogger("default"); logger.emit({ body: "message" }); Assert.ok(true, "Emit should not throw when no processors registered"); @@ -82,7 +102,7 @@ export class OTelMultiLogRecordProcessorTests extends AITestClass { test: () => { const processor = new TestProcessor(); const { multiProcessor } = setup([processor]); - const provider = createLoggerProvider({ processors: [multiProcessor] }); + const provider = createLoggerProvider(_cfg([multiProcessor])); const logger = provider.getLogger("default"); Assert.equal(processor.logRecords.length, 0, "Processor should start with no records"); logger.emit({ body: "one" }); @@ -96,7 +116,7 @@ export class OTelMultiLogRecordProcessorTests extends AITestClass { const processor1 = new TestProcessor(); const processor2 = new TestProcessor(); const { multiProcessor } = setup([processor1, processor2]); - const provider = createLoggerProvider({ processors: [multiProcessor] }); + const provider = createLoggerProvider(_cfg([multiProcessor])); const logger = provider.getLogger("default"); Assert.equal(processor1.logRecords.length, 0, "Processor1 should start empty"); @@ -208,7 +228,7 @@ export class OTelMultiLogRecordProcessorTests extends AITestClass { const processor1 = new TestProcessor(); const processor2 = new TestProcessor(); const { multiProcessor } = setup([processor1, processor2]); - const provider = createLoggerProvider({ processors: [multiProcessor] }); + const provider = createLoggerProvider(_cfg([multiProcessor])); const logger = provider.getLogger("default"); logger.emit({ body: "one" }); diff --git a/shared/otel-core/planning/IMPLEMENTATION_PLAN.md b/shared/otel-core/planning/IMPLEMENTATION_PLAN.md index 3bf04fcc9..8e5e874d9 100644 --- a/shared/otel-core/planning/IMPLEMENTATION_PLAN.md +++ b/shared/otel-core/planning/IMPLEMENTATION_PLAN.md @@ -1,6 +1,6 @@ # OTel Web SDK Implementation Plan -**Last Updated**: February 2026 +**Last Updated**: March 2026 **Reference**: [CONTEXT.md](../CONTEXT.md) --- @@ -77,7 +77,7 @@ ## Implementation Phases -### Phase 0: CONTEXT.md Compliance Fixes (Prerequisite) +### Phase 0: CONTEXT.md Compliance Fixes (Prerequisite) ✅ Complete Fix existing implementations to comply with CONTEXT.md before building new features. @@ -90,10 +90,9 @@ Fix existing implementations to comply with CONTEXT.md before building new featu | `createResource` | Add shutdown/cleanup method | | All | Add `IUnloadHook` management with `.rm()` calls during shutdown | | All | Add comprehensive TypeDoc documentation | - **Deliverable**: All existing implementations pass CONTEXT.md validation checklist. -### Phase 1: SDK Foundation (Critical) +### Phase 1: SDK Foundation (Critical) ✅ Complete | Component | Location | Description | |-----------|----------|-------------| diff --git a/shared/otel-core/src/ext/ValueSanitizer.ts b/shared/otel-core/src/ext/ValueSanitizer.ts index 80cc78315..239d10830 100644 --- a/shared/otel-core/src/ext/ValueSanitizer.ts +++ b/shared/otel-core/src/ext/ValueSanitizer.ts @@ -1,4 +1,6 @@ -import { arrForEach, arrIncludes, arrIndexOf, getLength, isNullOrUndefined, isString, objCreate, objForEachKey } from "@nevware21/ts-utils"; +import { + arrForEach, arrIncludes, arrIndexOf, getLength, isNullOrUndefined, isString, objCreate, objForEachKey +} from "@nevware21/ts-utils"; import { STR_EMPTY } from "../constants/InternalConstants"; import { FieldValueSanitizerType } from "../enums/ext/Enums"; import { diff --git a/shared/otel-core/src/index.ts b/shared/otel-core/src/index.ts index d3e61a46e..083d78588 100644 --- a/shared/otel-core/src/index.ts +++ b/shared/otel-core/src/index.ts @@ -135,6 +135,8 @@ export { IOTelSpanOptions } from "./interfaces/otel/trace/IOTelSpanOptions"; export { createOTelTraceState, isOTelTraceState } from "./otel/api/trace/traceState"; export { createSpan } from "./otel/api/trace/span"; export { createTraceProvider } from "./otel/api/trace/traceProvider"; +export { createTracerProvider } from "./otel/api/trace/tracerProvider"; +export { ITracerProviderConfig } from "./interfaces/otel/trace/ITracerProviderConfig"; export { isSpanContext, wrapDistributedTrace, createOTelSpanContext } from "./otel/api/trace/spanContext"; export { createNonRecordingSpan, deleteContextSpan, getContextSpan, setContextSpan, setContextSpanContext, getContextActiveSpanContext, isSpanContextValid, @@ -200,6 +202,7 @@ export { IOTelWebSdkConfig } from "./interfaces/otel/config/IOTelWebSdkConfig"; export { createContextManager } from "./otel/api/context/contextManager"; export { createContext } from "./otel/api/context/context"; export { IOTelContextManager } from "./interfaces/otel/context/IOTelContextManager"; +export { IContextManagerConfig } from "./interfaces/otel/context/IContextManagerConfig"; export { IOTelContext } from "./interfaces/otel/context/IOTelContext"; // OpenTelemetry Resources @@ -253,6 +256,7 @@ export { IOTelSdkLogRecord } from "./interfaces/otel/logs/IOTelSdkLogRecord"; export { IOTelLoggerProvider } from "./interfaces/otel/logs/IOTelLoggerProvider"; export { IOTelLoggerOptions } from "./interfaces/otel/logs/IOTelLoggerOptions"; export { IOTelLoggerProviderSharedState } from "./interfaces/otel/logs/IOTelLoggerProviderSharedState"; +export { IOTelLoggerProviderConfig } from "./interfaces/otel/logs/IOTelLoggerProviderConfig"; export { IOTelLogRecordLimits } from "./interfaces/otel/logs/IOTelLogRecordLimits"; // SDK Logs diff --git a/shared/otel-core/src/interfaces/otel/context/IContextManagerConfig.ts b/shared/otel-core/src/interfaces/otel/context/IContextManagerConfig.ts new file mode 100644 index 000000000..27f703a9f --- /dev/null +++ b/shared/otel-core/src/interfaces/otel/context/IContextManagerConfig.ts @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { IOTelErrorHandlers } from "../config/IOTelErrorHandlers"; +import { IOTelContext } from "./IOTelContext"; + +/** + * Configuration interface for creating a context manager. + * + * @since 4.0.0 + */ +export interface IContextManagerConfig { + /** + * The parent / root context to use if there is no active context. + */ + parentContext?: IOTelContext; + + /** + * Error handlers for internal diagnostics. + * + * @see {@link IOTelErrorHandlers} + */ + errorHandlers?: IOTelErrorHandlers; +} diff --git a/shared/otel-core/src/interfaces/otel/logs/IOTelLoggerProviderConfig.ts b/shared/otel-core/src/interfaces/otel/logs/IOTelLoggerProviderConfig.ts index 7d24c2ab3..fef06c1d5 100644 --- a/shared/otel-core/src/interfaces/otel/logs/IOTelLoggerProviderConfig.ts +++ b/shared/otel-core/src/interfaces/otel/logs/IOTelLoggerProviderConfig.ts @@ -1,13 +1,37 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +import { IOTelErrorHandlers } from "../config/IOTelErrorHandlers"; import { IOTelResource } from "../resources/IOTelResource"; import { IOTelLogRecordLimits } from "./IOTelLogRecordLimits"; import { IOTelLogRecordProcessor } from "./IOTelLogRecordProcessor"; +/** + * Configuration interface for the OpenTelemetry LoggerProvider. + * Provides all configuration options required for LoggerProvider initialization. + * + * @remarks + * - The `resource` and `errorHandlers` properties are required + * - Config is used directly — never copied with spread operator + * - Supports dynamic configuration via `onConfigChange` callbacks + * + * @since 3.4.0 + */ export interface IOTelLoggerProviderConfig { - /** Resource associated with trace telemetry */ - resource?: IOTelResource; + /** + * Resource information for telemetry source identification. + * Provides attributes that describe the entity producing telemetry. + */ + resource: IOTelResource; + + /** + * Error handlers for internal diagnostics. + * Provides hooks to customize how different types of errors and + * diagnostic messages are handled. + * + * @see {@link IOTelErrorHandlers} + */ + errorHandlers: IOTelErrorHandlers; /** * How long the forceFlush can run before it is cancelled. @@ -15,7 +39,7 @@ export interface IOTelLoggerProviderConfig { */ forceFlushTimeoutMillis?: number; - /** Log Record Limits*/ + /** Log Record Limits */ logRecordLimits?: IOTelLogRecordLimits; /** Log Record Processors */ diff --git a/shared/otel-core/src/interfaces/otel/resources/IOTelResource.ts b/shared/otel-core/src/interfaces/otel/resources/IOTelResource.ts index 7f3e09b30..5b0b8d0d0 100644 --- a/shared/otel-core/src/interfaces/otel/resources/IOTelResource.ts +++ b/shared/otel-core/src/interfaces/otel/resources/IOTelResource.ts @@ -47,4 +47,12 @@ export interface IOTelResource { merge(other: IOTelResource | null): IOTelResource; getRawAttributes(): OTelRawResourceAttribute[]; + + /** + * Releases internal resources and clears cached attribute containers. + * After shutdown, the resource should not be used. + * + * @since 4.0.0 + */ + shutdown?(): void; } diff --git a/shared/otel-core/src/interfaces/otel/trace/ITracerProviderConfig.ts b/shared/otel-core/src/interfaces/otel/trace/ITracerProviderConfig.ts new file mode 100644 index 000000000..d044a1bf3 --- /dev/null +++ b/shared/otel-core/src/interfaces/otel/trace/ITracerProviderConfig.ts @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { ITraceHost } from "../../ai/ITraceProvider"; +import { IOTelErrorHandlers } from "../config/IOTelErrorHandlers"; + +/** + * Configuration interface for creating a TracerProvider. + * + * @remarks + * The TracerProvider manages Tracer instances and delegates span creation + * to the configured trace host. + * + * @since 4.0.0 + */ +export interface ITracerProviderConfig { + /** + * The trace host that provides span creation and context management. + * + * @see {@link ITraceHost} + */ + host: ITraceHost; + + /** + * Error handlers for internal diagnostics. + * + * @see {@link IOTelErrorHandlers} + */ + errorHandlers?: IOTelErrorHandlers; +} diff --git a/shared/otel-core/src/otel/api/OTelApi.ts b/shared/otel-core/src/otel/api/OTelApi.ts index b379cebb8..9f21c4bbe 100644 --- a/shared/otel-core/src/otel/api/OTelApi.ts +++ b/shared/otel-core/src/otel/api/OTelApi.ts @@ -7,13 +7,13 @@ import { IOTelApiCtx } from "../../interfaces/otel/IOTelApiCtx"; import { ITraceApi } from "../../interfaces/otel/trace/IOTelTraceApi"; import { setProtoTypeName } from "../../utils/HelperFuncs"; import { _createTraceApi } from "./trace/traceApi"; -import { _createTracerProvider } from "./trace/tracerProvider"; +import { createTracerProvider } from "./trace/tracerProvider"; /*#__NO_SIDE_EFFECTS__*/ export function createOTelApi(otelApiCtx: IOTelApiCtx): IOTelApi { let _traceApi: ILazyValue; - let otelApi = setProtoTypeName(objDefineProps(_createTracerProvider(otelApiCtx.host) as IOTelApi, { + let otelApi = setProtoTypeName(objDefineProps(createTracerProvider({ host: otelApiCtx.host }) as IOTelApi, { cfg: { g: () => otelApiCtx.host.config }, trace: { g: () => _traceApi.v }, host: { g: () => otelApiCtx.host } diff --git a/shared/otel-core/src/otel/api/context/contextManager.ts b/shared/otel-core/src/otel/api/context/contextManager.ts index b4ba454b9..ccdd927eb 100644 --- a/shared/otel-core/src/otel/api/context/contextManager.ts +++ b/shared/otel-core/src/otel/api/context/contextManager.ts @@ -2,16 +2,61 @@ // Licensed under the MIT License. import { arrSlice, fnApply, isFunction, objDefine } from "@nevware21/ts-utils"; +import { createDynamicConfig } from "../../../config/DynamicConfig"; +import { IUnloadHook } from "../../../interfaces/ai/IUnloadHook"; +import { IOTelErrorHandlers } from "../../../interfaces/otel/config/IOTelErrorHandlers"; +import { IContextManagerConfig } from "../../../interfaces/otel/context/IContextManagerConfig"; import { IOTelContext } from "../../../interfaces/otel/context/IOTelContext"; import { IOTelContextManager } from "../../../interfaces/otel/context/IOTelContextManager"; +import { handleWarn } from "../../../internal/handleErrors"; /** - * Create a context manager using the provided parent context as the root context - * if there is no active context. - * @param parentContext - The parent / root context to use if there is no active context. - * @returns + * Creates a context manager that tracks the active context for the current execution scope. + * + * The context manager maintains a stack-based active context, falling back to the + * configured parent context when no context is explicitly active. + * + * @param config - Optional configuration for the context manager including parent context + * and error handlers. + * @returns An IOTelContextManager instance + * + * @remarks + * - Supports `with()` for scoped context activation + * - Supports `bind()` to associate a context with a callback + * - Must be enabled via `enable()` before use + * - Call `disable()` to clear active context, stop tracking, and unregister config listeners + * - Local config caching uses `onConfigChange` callbacks + * + * @example + * ```typescript + * const ctxMgr = createContextManager({ + * parentContext: rootContext, + * errorHandlers: myErrorHandlers + * }); + * ctxMgr.enable(); + * + * ctxMgr.with(myContext, () => { + * // myContext is now active within this callback + * }); + * ``` + * + * @since 4.0.0 */ -export function createContextManager(parentContext?: IOTelContext): IOTelContextManager { +export function createContextManager(config?: IContextManagerConfig): IOTelContextManager { + let _cfg = config || {}; + let _unloadHooks: IUnloadHook[] = []; + + // Local cached values — updated via onConfigChange + let _handlers: IOTelErrorHandlers; + let parentContext: IOTelContext; + + // Register for config changes — save the returned IUnloadHook + let _configUnload = createDynamicConfig(_cfg).watch(function () { + _handlers = _cfg.errorHandlers || {}; + parentContext = _cfg.parentContext; + }); + _unloadHooks.push(_configUnload); + let enabled = false; let activeContext: IOTelContext | null; @@ -41,6 +86,7 @@ export function createContextManager(parentContext?: IOTelContext): IOTelContext }); } + handleWarn(_handlers, "bind() called with non-function target, returning target as-is"); return target; }, enable: () => { @@ -53,10 +99,17 @@ export function createContextManager(parentContext?: IOTelContext): IOTelContext }, disable: () => { activeContext = null; - enabled = false + enabled = false; + + // Unregister config change listeners + for (let i = 0; i < _unloadHooks.length; i++) { + _unloadHooks[i].rm(); + } + _unloadHooks = []; + return theContextMgr; } }; - return theContextMgr + return theContextMgr; } diff --git a/shared/otel-core/src/otel/api/trace/span.ts b/shared/otel-core/src/otel/api/trace/span.ts index 0e4b590d0..cee3477a5 100644 --- a/shared/otel-core/src/otel/api/trace/span.ts +++ b/shared/otel-core/src/otel/api/trace/span.ts @@ -24,13 +24,34 @@ import { import { setProtoTypeName, updateProtoTypeName } from "../../../utils/HelperFuncs"; import { addAttributes, createAttributeContainer } from "../../attribute/attributeContainer"; +/** + * Creates a new span instance for tracking an operation. + * + * The span reads error handlers from the API config dynamically rather than + * caching them, ensuring it always reflects the latest configuration. + * Spans are typically short-lived and do not require `onConfigChange` listeners. + * + * @param spanCtx - The span creation context containing API config, resource, + * instrumentation scope, span context, and optional callbacks + * @param orgName - The original name of the span + * @param kind - The kind of the span (INTERNAL, SERVER, CLIENT, PRODUCER, CONSUMER) + * @returns An IReadableSpan instance + * + * @since 3.4.0 + */ export function createSpan(spanCtx: IOTelSpanCtx, orgName: string, kind: OTelSpanKind): IReadableSpan { let otelCfg = spanCtx.api.cfg; let perfStartTime: number = perfNow(); let spanContext = spanCtx.spanContext; let attributes: ILazyValue; let isEnded = false; - let errorHandlers = otelCfg.errorHandlers || {}; + + // Read errorHandlers from config dynamically — spans are short-lived and + // do not need onConfigChange; reading from the config reference ensures + // the latest handlers are used if config changes during the span lifetime. + function _errorHandlers() { + return otelCfg.errorHandlers || {}; + } let spanStartTime: ILazyValue = getDeferred(() => { if (isNullOrUndefined(spanCtx.startTime)) { return hrTime(perfStartTime); @@ -61,7 +82,7 @@ export function createSpan(spanCtx: IOTelSpanCtx, orgName: string, kind: OTelSpa function _handleIsEnded(operation: string, extraMsg?: string): boolean { if (isEnded) { - handleSpanError(errorHandlers, "Span {traceID: " + spanContext.traceId + ", spanId: " + spanContext.spanId + "} has ended - operation [" + operation + "] unsuccessful" + (extraMsg ? (" - " + extraMsg) : STR_EMPTY) + ".", spanName); + handleSpanError(_errorHandlers(), "Span {traceID: " + spanContext.traceId + ", spanId: " + spanContext.spanId + "} has ended - operation [" + operation + "] unsuccessful" + (extraMsg ? (" - " + extraMsg) : STR_EMPTY) + ".", spanName); } return isEnded; @@ -84,7 +105,7 @@ export function createSpan(spanCtx: IOTelSpanCtx, orgName: string, kind: OTelSpa } if (message) { - handleAttribError(errorHandlers, message, key, value); + handleAttribError(_errorHandlers(), message, key, value); localDroppedAttributes++; } else if (attributes){ attributes.v.set(key, value); @@ -112,7 +133,7 @@ export function createSpan(spanCtx: IOTelSpanCtx, orgName: string, kind: OTelSpa if (maxEvents > 0) { if (maxEvents > 0 && events.length >= maxEvents) { let droppedEvent = events.shift(); - handleWarn(errorHandlers, "maxEvents reached (" + maxEvents + ") - dropping event: " + droppedEvent.name); + handleWarn(_errorHandlers(), "maxEvents reached (" + maxEvents + ") - dropping event: " + droppedEvent.name); localDroppedEvents++; } @@ -132,10 +153,10 @@ export function createSpan(spanCtx: IOTelSpanCtx, orgName: string, kind: OTelSpa }) } else { localDroppedEvents++; - handleWarn(errorHandlers, "Span.addEvent: " + name + " not added - No events allowed"); + handleWarn(_errorHandlers(), "Span.addEvent: " + name + " not added - No events allowed"); } - handleNotImplemented(errorHandlers, "Span.addEvent: " + name + " not added"); + handleNotImplemented(_errorHandlers(), "Span.addEvent: " + name + " not added"); } else { localDroppedEvents++; } @@ -144,20 +165,20 @@ export function createSpan(spanCtx: IOTelSpanCtx, orgName: string, kind: OTelSpa }, addLink: (link: IOTelLink) => { if(!_handleIsEnded("addEvent") && isRecording) { - handleNotImplemented(errorHandlers, "Span.addLink: " + link + " not added"); + handleNotImplemented(_errorHandlers(), "Span.addLink: " + link + " not added"); } else { localDroppedLinks++; - handleWarn(errorHandlers, "Span.addLink: " + link + " not added - No links allowed"); + handleWarn(_errorHandlers(), "Span.addLink: " + link + " not added - No links allowed"); } return theSpan; }, addLinks: (links: IOTelLink[]) => { if (!_handleIsEnded("addLinks") && isRecording) { - handleNotImplemented(errorHandlers, "Span.addLinks: " + links + " not added"); + handleNotImplemented(_errorHandlers(), "Span.addLinks: " + links + " not added"); } else { localDroppedLinks += links.length; - handleWarn(errorHandlers, "Span.addLinks: " + links + " not added - No links allowed"); + handleWarn(_errorHandlers(), "Span.addLinks: " + links + " not added - No links allowed"); } return theSpan; @@ -197,13 +218,13 @@ export function createSpan(spanCtx: IOTelSpanCtx, orgName: string, kind: OTelSpa } if (calcDuration < 0) { - handleWarn(errorHandlers, "Span.end: duration is negative - startTime > endTime. Setting duration to 0 ms"); + handleWarn(_errorHandlers(), "Span.end: duration is negative - startTime > endTime. Setting duration to 0 ms"); spanDuration = zeroHrTime(); spanEndTime = spanStartTime.v; } if (localDroppedEvents > 0) { - handleWarn(errorHandlers, "Droped " + localDroppedEvents + " events"); + handleWarn(_errorHandlers(), "Droped " + localDroppedEvents + " events"); } // We don't mark as ended until after the onEnd callback to ensure that it can @@ -222,7 +243,7 @@ export function createSpan(spanCtx: IOTelSpanCtx, orgName: string, kind: OTelSpa if (spanCtx.onException) { spanCtx.onException(theSpan, exception, time); } else { - handleNotImplemented(errorHandlers, "Span.recordException: " + dumpObj(exception) + " not handled"); + handleNotImplemented(_errorHandlers(), "Span.recordException: " + dumpObj(exception) + " not handled"); } } }, diff --git a/shared/otel-core/src/otel/api/trace/tracer.ts b/shared/otel-core/src/otel/api/trace/tracer.ts index 1fdd313a2..78d16530e 100644 --- a/shared/otel-core/src/otel/api/trace/tracer.ts +++ b/shared/otel-core/src/otel/api/trace/tracer.ts @@ -12,9 +12,16 @@ import { startActiveSpan } from "./utils"; /** * @internal - * Create a tracer implementation. - * @param host - The ApplicationInsights core instance - * @returns A tracer object + * Creates a tracer implementation that delegates span creation to the provided trace host. + * + * This factory is used by the Application Insights / 1DS integration path + * where span creation is handled by the AI-core trace host. + * + * @param host - The trace host that provides span creation and context management + * @param name - Optional tracer name for debugging and identification + * @returns An IOTelTracer instance + * + * @since 3.4.0 */ export function _createTracer(host: ITraceHost, name?: string): IOTelTracer { let tracer: IOTelTracer = setProtoTypeName({ diff --git a/shared/otel-core/src/otel/api/trace/tracerProvider.ts b/shared/otel-core/src/otel/api/trace/tracerProvider.ts index 25993f8e4..6e36cee30 100644 --- a/shared/otel-core/src/otel/api/trace/tracerProvider.ts +++ b/shared/otel-core/src/otel/api/trace/tracerProvider.ts @@ -2,38 +2,85 @@ // Licensed under the MIT License. import { IPromise } from "@nevware21/ts-async"; -import { ITraceHost } from "../../../interfaces/ai/ITraceProvider"; +import { createDynamicConfig } from "../../../config/DynamicConfig"; +import { IUnloadHook } from "../../../interfaces/ai/IUnloadHook"; +import { IOTelErrorHandlers } from "../../../interfaces/otel/config/IOTelErrorHandlers"; import { IOTelTracer } from "../../../interfaces/otel/trace/IOTelTracer"; import { IOTelTracerProvider } from "../../../interfaces/otel/trace/IOTelTracerProvider"; +import { ITracerProviderConfig } from "../../../interfaces/otel/trace/ITracerProviderConfig"; +import { handleWarn } from "../../../internal/handleErrors"; import { _createTracer } from "./tracer"; /** - * @internal - * Create a trace implementation with tracer caching. - * @param core - The ApplicationInsights core instance - * @returns A trace object + * Creates a TracerProvider that manages Tracer instances with caching. + * + * Tracers are cached by name and version. Subsequent requests for a tracer with + * the same name and version return the cached instance. + * + * @param config - The TracerProvider configuration with required dependencies injected. + * Must include `host` which provides span creation. + * @returns An IOTelTracerProvider instance + * + * @remarks + * - Delegates span creation to the configured `host.startSpan()` + * - Error handlers are obtained from `config.errorHandlers` + * - Local config caching uses `onConfigChange` callbacks + * - Call `shutdown()` to release cached tracers and unregister config listeners + * + * @since 3.4.0 */ -export function _createTracerProvider(host: ITraceHost): IOTelTracerProvider { - let tracers: { [key: string]: IOTelTracer } = {}; +export function createTracerProvider(config: ITracerProviderConfig): IOTelTracerProvider { + let _tracers: { [key: string]: IOTelTracer } = {}; + let _isShutdown = false; + let _unloadHooks: IUnloadHook[] = []; + + // Local cached values — updated via onConfigChange + let _handlers: IOTelErrorHandlers; + let _host = config.host; + + // Register for config changes — save the returned IUnloadHook + let _configUnload = createDynamicConfig(config).watch(function () { + _handlers = config.errorHandlers || {}; + _host = config.host; + }); + _unloadHooks.push(_configUnload); return { getTracer(name: string, version?: string): IOTelTracer { - const tracerKey = (name|| "ai-web") + "@" + (version || "unknown"); - - if (!tracers[tracerKey]) { - tracers[tracerKey] = _createTracer(host); + if (_isShutdown) { + handleWarn(_handlers, "A shutdown TracerProvider cannot provide a Tracer"); + return null; + } + + let tracerKey = (name || "ai-web") + "@" + (version || "unknown"); + + if (!_tracers[tracerKey]) { + _tracers[tracerKey] = _createTracer(_host, name); } - - return tracers[tracerKey]; + + return _tracers[tracerKey]; }, forceFlush(): IPromise | void { // Nothing to flush return; }, shutdown(): IPromise | void { - // Just clear the locally cached IOTelTracer instances so they can be garbage collected - tracers = {}; - host = null; + if (_isShutdown) { + handleWarn(_handlers, "shutdown may only be called once per TracerProvider"); + return; + } + + _isShutdown = true; + + // Unregister config change listeners + for (let i = 0; i < _unloadHooks.length; i++) { + _unloadHooks[i].rm(); + } + _unloadHooks = []; + + // Clear the locally cached IOTelTracer instances so they can be garbage collected + _tracers = {}; + _host = null; return; } }; diff --git a/shared/otel-core/src/otel/resource/resource.ts b/shared/otel-core/src/otel/resource/resource.ts index eebf7c362..1664986bb 100644 --- a/shared/otel-core/src/otel/resource/resource.ts +++ b/shared/otel-core/src/otel/resource/resource.ts @@ -13,6 +13,24 @@ import { createAttributeContainer } from "../attribute/attributeContainer"; type ResourceKeyValue = [key: string, value: OTelAttributeValue | undefined]; +/** + * Creates an OpenTelemetry Resource instance that provides telemetry source identification. + * + * Resources hold key-value attributes describing the entity producing telemetry + * (e.g., service name, version, environment). They support both synchronous and + * asynchronous attribute resolution. + * + * @param resourceCtx - The resource context containing config and raw attributes. + * The `cfg.errorHandlers` property is used for error reporting. + * @returns An IOTelResource instance + * + * @remarks + * - Supports asynchronous resource attributes via promises + * - Uses error handlers from `resourceCtx.cfg.errorHandlers` + * - Call `shutdown()` to release internal attribute containers + * + * @since 3.4.0 + */ export function createResource(resourceCtx: IOTelResourceCtx): IOTelResource { let attribContainer: IAttributeContainer | null = null; @@ -141,7 +159,13 @@ export function createResource(resourceCtx: IOTelResourceCtx): IOTelResource { attributes: null, waitForAsyncAttributes: _waitForAsyncAttributes, merge: _merge, - getRawAttributes: _getRawAttributes + getRawAttributes: _getRawAttributes, + shutdown: function () { + attribContainer = null; + rawResources = null; + resolveAwaitingPromise = null; + awaitingPromise = null; + } }; objDefineProps(resource, { diff --git a/shared/otel-core/src/otel/sdk/OTelLoggerProvider.ts b/shared/otel-core/src/otel/sdk/OTelLoggerProvider.ts index 1a23f48aa..abb2a7f5d 100644 --- a/shared/otel-core/src/otel/sdk/OTelLoggerProvider.ts +++ b/shared/otel-core/src/otel/sdk/OTelLoggerProvider.ts @@ -2,65 +2,109 @@ // Licensed under the MIT License. import { IPromise } from "@nevware21/ts-async"; +import { createDynamicConfig } from "../../config/DynamicConfig"; +import { IUnloadHook } from "../../interfaces/ai/IUnloadHook"; import { IOTelErrorHandlers } from "../../interfaces/otel/config/IOTelErrorHandlers"; import { IOTelLogger } from "../../interfaces/otel/logs/IOTelLogger"; import { IOTelLoggerOptions } from "../../interfaces/otel/logs/IOTelLoggerOptions"; import { IOTelLoggerProvider } from "../../interfaces/otel/logs/IOTelLoggerProvider"; import { IOTelLoggerProviderConfig } from "../../interfaces/otel/logs/IOTelLoggerProviderConfig"; import { IOTelLoggerProviderSharedState } from "../../interfaces/otel/logs/IOTelLoggerProviderSharedState"; +import { IOTelResource } from "../../interfaces/otel/resources/IOTelResource"; import { createLoggerProviderSharedState } from "../../internal/LoggerProviderSharedState"; -import { handleWarn } from "../../internal/handleErrors"; -import { createResource } from "../resource/resource"; +import { handleError, handleWarn } from "../../internal/handleErrors"; import { createLogger } from "./OTelLogger"; import { loadDefaultConfig, reconfigureLimits } from "./config"; export const DEFAULT_LOGGER_NAME = "unknown"; +/** + * Creates an OpenTelemetry LoggerProvider instance. + * + * The LoggerProvider manages Logger instances and coordinates log record + * processing through registered processors. + * + * @param config - The LoggerProvider configuration with required dependencies injected. + * Must include `resource` and `errorHandlers`. + * @returns An initialized IOTelLoggerProvider instance with `forceFlush` and `shutdown` support. + * + * @remarks + * - All dependencies must be injected through config — no global state + * - Config is used directly — never copied with spread operator + * - Error handlers are obtained from `config.errorHandlers` + * - Local config caching uses `onConfigChange` callbacks + * - Complete unload support — call `shutdown()` to release all resources + * + * @example + * ```typescript + * const provider = createLoggerProvider({ + * resource: myResource, + * errorHandlers: myErrorHandlers, + * processors: [myLogProcessor] + * }); + * + * const logger = provider.getLogger("my-service", "1.0.0"); + * ``` + * + * @since 3.4.0 + */ export function createLoggerProvider( - config: IOTelLoggerProviderConfig = {} + config: IOTelLoggerProviderConfig ): IOTelLoggerProvider & { forceFlush(): IPromise; shutdown(): IPromise; readonly _sharedState: IOTelLoggerProviderSharedState; } { - const defaults = loadDefaultConfig(); - const forceFlushTimeoutMillis = config.forceFlushTimeoutMillis !== undefined - ? config.forceFlushTimeoutMillis - : defaults.forceFlushTimeoutMillis; - const logRecordLimits = config.logRecordLimits || defaults.logRecordLimits; - - let resource = config.resource; - if (!resource) { - resource = createResource({ cfg: { errorHandlers: {} }, attribs: [] }); + // Validate required dependencies upfront + let _handlers: IOTelErrorHandlers; + let _resource: IOTelResource; + + let defaults = loadDefaultConfig(); + let forceFlushTimeoutMillis: number; + let logRecordLimits; + + let _isShutdown = false; + let _unloadHooks: IUnloadHook[] = []; + + // Register for config changes — save the returned IUnloadHook + let _configUnload = createDynamicConfig(config).watch(function () { + _handlers = config.errorHandlers || {}; + _resource = config.resource; + forceFlushTimeoutMillis = config.forceFlushTimeoutMillis !== undefined + ? config.forceFlushTimeoutMillis + : defaults.forceFlushTimeoutMillis; + logRecordLimits = config.logRecordLimits || defaults.logRecordLimits; + }); + _unloadHooks.push(_configUnload); + + if (!_resource) { + handleError(_handlers, "Resource must be provided to LoggerProvider"); } - const sharedState = createLoggerProviderSharedState( - resource, + let sharedState = createLoggerProviderSharedState( + _resource, forceFlushTimeoutMillis, reconfigureLimits(logRecordLimits), - config && config.processors ? config.processors : [] + config.processors || [] ); - let isShutdown = false; - const handlers: IOTelErrorHandlers = {}; - function getLogger( name: string, version?: string, options?: IOTelLoggerOptions ): IOTelLogger | null { - if (isShutdown) { - handleWarn(handlers, "A shutdown LoggerProvider cannot provide a Logger"); + if (_isShutdown) { + handleWarn(_handlers, "A shutdown LoggerProvider cannot provide a Logger"); return null; } if (!name) { - handleWarn(handlers, "Logger requested without instrumentation scope name."); + handleWarn(_handlers, "Logger requested without instrumentation scope name."); } - const loggerName = name || DEFAULT_LOGGER_NAME; - const schemaUrl = options && options.schemaUrl; - const key = `${loggerName}@${version || ""}:${schemaUrl || ""}`; + let loggerName = name || DEFAULT_LOGGER_NAME; + let schemaUrl = options && options.schemaUrl; + let key = loggerName + "@" + (version || "") + ":" + (schemaUrl || ""); if (!sharedState.loggers.has(key)) { sharedState.loggers.set( key, @@ -71,13 +115,12 @@ export function createLoggerProvider( ); } - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return sharedState.loggers.get(key)!; + return sharedState.loggers.get(key); } function forceFlush(): IPromise { - if (isShutdown) { - handleWarn(handlers, "invalid attempt to force flush after LoggerProvider shutdown"); + if (_isShutdown) { + handleWarn(_handlers, "invalid attempt to force flush after LoggerProvider shutdown"); return Promise.resolve(); } @@ -85,12 +128,19 @@ export function createLoggerProvider( } function shutdown(): IPromise { - if (isShutdown) { - handleWarn(handlers, "shutdown may only be called once per LoggerProvider"); + if (_isShutdown) { + handleWarn(_handlers, "shutdown may only be called once per LoggerProvider"); return Promise.resolve(); } - isShutdown = true; + _isShutdown = true; + + // Remove all config change listeners + for (let i = 0; i < _unloadHooks.length; i++) { + _unloadHooks[i].rm(); + } + _unloadHooks = []; + return sharedState.activeProcessor.shutdown(); } diff --git a/shared/otel-core/src/otel/sdk/OTelWebSdk.ts b/shared/otel-core/src/otel/sdk/OTelWebSdk.ts index 86f09539b..bf1543445 100644 --- a/shared/otel-core/src/otel/sdk/OTelWebSdk.ts +++ b/shared/otel-core/src/otel/sdk/OTelWebSdk.ts @@ -152,6 +152,7 @@ export function createOTelWebSdk(config: IOTelWebSdkConfig): IOTelWebSdk { // Create the logger provider using existing factory let _loggerProvider = createLoggerProvider({ resource: _resource, + errorHandlers: _handlers, processors: _sdkConfig.logProcessors || [] }); From e2c61b2f373ddb0af4094c74fee4de2f156c929b Mon Sep 17 00:00:00 2001 From: Hector Hernandez <39923391+hectorhdzg@users.noreply.github.com> Date: Tue, 24 Mar 2026 14:27:36 -0700 Subject: [PATCH 2/3] Addressing comments --- .../Unit/src/sdk/OTelLoggerProvider.Tests.ts | 2 +- .../logs/IOTelLoggerProviderSharedState.ts | 6 +-- .../src/otel/api/context/contextManager.ts | 21 +++++------ shared/otel-core/src/otel/api/trace/span.ts | 2 +- .../src/otel/api/trace/tracerProvider.ts | 28 +++++++++++++- .../otel-core/src/otel/resource/resource.ts | 5 +++ .../src/otel/sdk/OTelLoggerProvider.ts | 37 +++++++++++++------ shared/otel-core/src/otel/sdk/OTelWebSdk.ts | 16 +++++++- 8 files changed, 86 insertions(+), 31 deletions(-) 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 3c0a943fa..9b2662d2e 100644 --- a/shared/otel-core/Tests/Unit/src/sdk/OTelLoggerProvider.Tests.ts +++ b/shared/otel-core/Tests/Unit/src/sdk/OTelLoggerProvider.Tests.ts @@ -66,7 +66,7 @@ export class OTelLoggerProviderTests extends AITestClass { }); this.testCase({ - name: "LoggerProvider: constructor should use default resource when not provided", + name: "LoggerProvider: constructor should expose configured default resource", test: () => { const provider = createLoggerProvider(this._cfg()); const sharedState = this._getSharedState(provider); diff --git a/shared/otel-core/src/interfaces/otel/logs/IOTelLoggerProviderSharedState.ts b/shared/otel-core/src/interfaces/otel/logs/IOTelLoggerProviderSharedState.ts index 416beb3c8..592eeef34 100644 --- a/shared/otel-core/src/interfaces/otel/logs/IOTelLoggerProviderSharedState.ts +++ b/shared/otel-core/src/interfaces/otel/logs/IOTelLoggerProviderSharedState.ts @@ -22,13 +22,13 @@ export interface IOTelLoggerProviderSharedState { /** * Resource describing the entity producing telemetry. */ - readonly resource: IOTelResource; + resource: IOTelResource; /** * Timeout applied when forcing processors to flush. */ - readonly forceFlushTimeoutMillis: number; + forceFlushTimeoutMillis: number; /** * Limits applied to log records created by the provider. */ - readonly logRecordLimits: Required; + logRecordLimits: Required; } diff --git a/shared/otel-core/src/otel/api/context/contextManager.ts b/shared/otel-core/src/otel/api/context/contextManager.ts index ccdd927eb..d660be8f1 100644 --- a/shared/otel-core/src/otel/api/context/contextManager.ts +++ b/shared/otel-core/src/otel/api/context/contextManager.ts @@ -47,15 +47,18 @@ export function createContextManager(config?: IContextManagerConfig): IOTelConte let _unloadHooks: IUnloadHook[] = []; // Local cached values — updated via onConfigChange - let _handlers: IOTelErrorHandlers; + let _handlers: IOTelErrorHandlers = {}; let parentContext: IOTelContext; // Register for config changes — save the returned IUnloadHook - let _configUnload = createDynamicConfig(_cfg).watch(function () { - _handlers = _cfg.errorHandlers || {}; - parentContext = _cfg.parentContext; - }); - _unloadHooks.push(_configUnload); + // Only set up dynamic config watcher when config is provided to avoid per-instance overhead + if (config) { + let _configUnload = createDynamicConfig(_cfg).watch(function () { + _handlers = _cfg.errorHandlers || {}; + parentContext = _cfg.parentContext; + }); + _unloadHooks.push(_configUnload); + } let enabled = false; let activeContext: IOTelContext | null; @@ -101,12 +104,6 @@ export function createContextManager(config?: IContextManagerConfig): IOTelConte activeContext = null; enabled = false; - // Unregister config change listeners - for (let i = 0; i < _unloadHooks.length; i++) { - _unloadHooks[i].rm(); - } - _unloadHooks = []; - return theContextMgr; } }; diff --git a/shared/otel-core/src/otel/api/trace/span.ts b/shared/otel-core/src/otel/api/trace/span.ts index cee3477a5..66e8dffde 100644 --- a/shared/otel-core/src/otel/api/trace/span.ts +++ b/shared/otel-core/src/otel/api/trace/span.ts @@ -224,7 +224,7 @@ export function createSpan(spanCtx: IOTelSpanCtx, orgName: string, kind: OTelSpa } if (localDroppedEvents > 0) { - handleWarn(_errorHandlers(), "Droped " + localDroppedEvents + " events"); + handleWarn(_errorHandlers(), "Dropped " + localDroppedEvents + " events"); } // We don't mark as ended until after the onEnd callback to ensure that it can diff --git a/shared/otel-core/src/otel/api/trace/tracerProvider.ts b/shared/otel-core/src/otel/api/trace/tracerProvider.ts index 6e36cee30..809bdb8ec 100644 --- a/shared/otel-core/src/otel/api/trace/tracerProvider.ts +++ b/shared/otel-core/src/otel/api/trace/tracerProvider.ts @@ -2,15 +2,41 @@ // Licensed under the MIT License. import { IPromise } from "@nevware21/ts-async"; +import { isFunction } from "@nevware21/ts-utils"; import { createDynamicConfig } from "../../../config/DynamicConfig"; import { IUnloadHook } from "../../../interfaces/ai/IUnloadHook"; import { IOTelErrorHandlers } from "../../../interfaces/otel/config/IOTelErrorHandlers"; +import { IOTelSpan } from "../../../interfaces/otel/trace/IOTelSpan"; +import { IOTelSpanOptions } from "../../../interfaces/otel/trace/IOTelSpanOptions"; import { IOTelTracer } from "../../../interfaces/otel/trace/IOTelTracer"; import { IOTelTracerProvider } from "../../../interfaces/otel/trace/IOTelTracerProvider"; import { ITracerProviderConfig } from "../../../interfaces/otel/trace/ITracerProviderConfig"; +import { IReadableSpan } from "../../../interfaces/otel/trace/IReadableSpan"; import { handleWarn } from "../../../internal/handleErrors"; import { _createTracer } from "./tracer"; +/** + * Non-recording tracer returned after shutdown. + * All operations are safe no-ops that return null spans. + */ +let _NOOP_TRACER: IOTelTracer = { + startSpan: function (_name: string, _options?: IOTelSpanOptions): IReadableSpan | null { + return null; + }, + startActiveSpan: function ReturnType>(name: string, arg2?: F | IOTelSpanOptions, arg3?: F, arg4?: F): ReturnType | undefined { + let fn: F; + if (isFunction(arg2)) { + fn = arg2 as F; + } else if (isFunction(arg3)) { + fn = arg3 as F; + } else { + fn = arg4 as F; + } + + return fn ? fn(null) : undefined; + } +}; + /** * Creates a TracerProvider that manages Tracer instances with caching. * @@ -49,7 +75,7 @@ export function createTracerProvider(config: ITracerProviderConfig): IOTelTracer getTracer(name: string, version?: string): IOTelTracer { if (_isShutdown) { handleWarn(_handlers, "A shutdown TracerProvider cannot provide a Tracer"); - return null; + return _NOOP_TRACER; } let tracerKey = (name || "ai-web") + "@" + (version || "unknown"); diff --git a/shared/otel-core/src/otel/resource/resource.ts b/shared/otel-core/src/otel/resource/resource.ts index 1664986bb..f6483b0b9 100644 --- a/shared/otel-core/src/otel/resource/resource.ts +++ b/shared/otel-core/src/otel/resource/resource.ts @@ -161,6 +161,11 @@ export function createResource(resourceCtx: IOTelResourceCtx): IOTelResource { merge: _merge, getRawAttributes: _getRawAttributes, shutdown: function () { + // Resolve any pending promise before clearing references + // so callers awaiting waitForAsyncAttributes() don't hang + if (resolveAwaitingPromise) { + resolveAwaitingPromise(); + } attribContainer = null; rawResources = null; resolveAwaitingPromise = null; diff --git a/shared/otel-core/src/otel/sdk/OTelLoggerProvider.ts b/shared/otel-core/src/otel/sdk/OTelLoggerProvider.ts index abb2a7f5d..35c47bfea 100644 --- a/shared/otel-core/src/otel/sdk/OTelLoggerProvider.ts +++ b/shared/otel-core/src/otel/sdk/OTelLoggerProvider.ts @@ -66,16 +66,13 @@ export function createLoggerProvider( let _isShutdown = false; let _unloadHooks: IUnloadHook[] = []; - // Register for config changes — save the returned IUnloadHook - let _configUnload = createDynamicConfig(config).watch(function () { - _handlers = config.errorHandlers || {}; - _resource = config.resource; - forceFlushTimeoutMillis = config.forceFlushTimeoutMillis !== undefined - ? config.forceFlushTimeoutMillis - : defaults.forceFlushTimeoutMillis; - logRecordLimits = config.logRecordLimits || defaults.logRecordLimits; - }); - _unloadHooks.push(_configUnload); + // Read initial config values + _handlers = config.errorHandlers || {}; + _resource = config.resource; + forceFlushTimeoutMillis = config.forceFlushTimeoutMillis !== undefined + ? config.forceFlushTimeoutMillis + : defaults.forceFlushTimeoutMillis; + logRecordLimits = config.logRecordLimits || defaults.logRecordLimits; if (!_resource) { handleError(_handlers, "Resource must be provided to LoggerProvider"); @@ -88,6 +85,23 @@ export function createLoggerProvider( config.processors || [] ); + // Register for config changes — save the returned IUnloadHook + // Updates both local cached values and sharedState so dynamic config changes propagate + let _configUnload = createDynamicConfig(config).watch(function () { + _handlers = config.errorHandlers || {}; + _resource = config.resource; + forceFlushTimeoutMillis = config.forceFlushTimeoutMillis !== undefined + ? config.forceFlushTimeoutMillis + : defaults.forceFlushTimeoutMillis; + logRecordLimits = config.logRecordLimits || defaults.logRecordLimits; + + // Propagate updated values to shared state + sharedState.resource = _resource; + sharedState.forceFlushTimeoutMillis = forceFlushTimeoutMillis; + sharedState.logRecordLimits = reconfigureLimits(logRecordLimits); + }); + _unloadHooks.push(_configUnload); + function getLogger( name: string, version?: string, @@ -115,7 +129,8 @@ export function createLoggerProvider( ); } - return sharedState.loggers.get(key); + let logger = sharedState.loggers.get(key); + return logger || null; } function forceFlush(): IPromise { diff --git a/shared/otel-core/src/otel/sdk/OTelWebSdk.ts b/shared/otel-core/src/otel/sdk/OTelWebSdk.ts index bf1543445..7940e4694 100644 --- a/shared/otel-core/src/otel/sdk/OTelWebSdk.ts +++ b/shared/otel-core/src/otel/sdk/OTelWebSdk.ts @@ -149,12 +149,24 @@ export function createOTelWebSdk(config: IOTelWebSdkConfig): IOTelWebSdk { // 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({ + // Create a shared config object for the logger provider that gets updated + // when the SDK config changes, so the provider picks up new values. + let _loggerProviderConfig = { resource: _resource, errorHandlers: _handlers, processors: _sdkConfig.logProcessors || [] + }; + + // Update the logger provider config when SDK config changes + let _loggerCfgUnload = onConfigChange(_sdkConfig, function () { + _loggerProviderConfig.resource = _resource; + _loggerProviderConfig.errorHandlers = _handlers; + _loggerProviderConfig.processors = _sdkConfig.logProcessors || []; }); + _unloadHooks.push(_loggerCfgUnload); + + // Create the logger provider using existing factory + let _loggerProvider = createLoggerProvider(_loggerProviderConfig); /** * Returns the current active context from the context manager, falling back From 61c2904a7d02dda339a12d85323b2a5498e9b916 Mon Sep 17 00:00:00 2001 From: Hector Hernandez <39923391+hectorhdzg@users.noreply.github.com> Date: Tue, 24 Mar 2026 15:11:31 -0700 Subject: [PATCH 3/3] Uodate --- shared/otel-core/src/otel/api/trace/tracerProvider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/otel-core/src/otel/api/trace/tracerProvider.ts b/shared/otel-core/src/otel/api/trace/tracerProvider.ts index 809bdb8ec..e75c10df9 100644 --- a/shared/otel-core/src/otel/api/trace/tracerProvider.ts +++ b/shared/otel-core/src/otel/api/trace/tracerProvider.ts @@ -10,8 +10,8 @@ import { IOTelSpan } from "../../../interfaces/otel/trace/IOTelSpan"; import { IOTelSpanOptions } from "../../../interfaces/otel/trace/IOTelSpanOptions"; import { IOTelTracer } from "../../../interfaces/otel/trace/IOTelTracer"; import { IOTelTracerProvider } from "../../../interfaces/otel/trace/IOTelTracerProvider"; -import { ITracerProviderConfig } from "../../../interfaces/otel/trace/ITracerProviderConfig"; import { IReadableSpan } from "../../../interfaces/otel/trace/IReadableSpan"; +import { ITracerProviderConfig } from "../../../interfaces/otel/trace/ITracerProviderConfig"; import { handleWarn } from "../../../internal/handleErrors"; import { _createTracer } from "./tracer";