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..98624f28f 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 { IOTelWebSdkConfig } from "../../../../src/interfaces/otel/config/IOTelWebSdkConfig"; // 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[]): IOTelWebSdkConfig { + const resource: IOTelResource = { + attributes: {} as IOTelAttributes, + merge: () => resource, + getRawAttributes: () => [] as OTelRawResourceAttribute[] + }; + return { + resource: resource, + errorHandlers: {}, + logProcessors: processors + } as IOTelWebSdkConfig; + } } 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..a8dff007d 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 { IOTelWebSdkConfig } from "../../../../src/interfaces/otel/config/IOTelWebSdkConfig"; +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({ - processors: [logRecordProcessor] - }); + const provider = createLoggerProvider(this._cfg({ + logProcessors: [logRecordProcessor] + })); const sharedState = this._getSharedState(provider); const activeProcessor = sharedState.activeProcessor as MultiLogRecordProcessorInstance; Assert.equal(activeProcessor.processors.length, 1, "Should register one processor"); @@ -64,9 +66,9 @@ 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(); + 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({ logProcessors: [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({ logProcessors: [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({ logProcessors: [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({ logProcessors: [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({ logProcessors: [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 IOTelWebSdkConfig with required fields plus any overrides. + */ + private _cfg(overrides?: Partial): IOTelWebSdkConfig { + let cfg: IOTelWebSdkConfig = { + resource: this._createTestResource(), + errorHandlers: {} as IOTelErrorHandlers + } as IOTelWebSdkConfig; + if (overrides) { + if (overrides.resource !== undefined) { + cfg.resource = overrides.resource; + } + if (overrides.errorHandlers !== undefined) { + cfg.errorHandlers = overrides.errorHandlers; + } + if (overrides.logProcessors !== undefined) { + cfg.logProcessors = overrides.logProcessors; + } + 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..3f73b4251 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 { IOTelWebSdkConfig } from "../../../../src/interfaces/otel/config/IOTelWebSdkConfig"; +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[]): IOTelWebSdkConfig { + return { + resource: _testResource(), + errorHandlers: {}, + logProcessors: processors + } as IOTelWebSdkConfig; +} + 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..cf5f453ec 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 @@ -235,7 +238,7 @@ export { // OpenTelemetry Error Handlers export { - handleAttribError, handleSpanError, handleDebug, handleWarn, handleError, handleNotImplemented + OTelErrorHandlerSource, handleAttribError, handleSpanError, handleDebug, handleWarn, handleError, handleNotImplemented } from "./internal/handleErrors"; // OpenTelemetry Error Classes @@ -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/config/IOTelWebSdkConfig.ts b/shared/otel-core/src/interfaces/otel/config/IOTelWebSdkConfig.ts index 9cd791f40..0096b7803 100644 --- a/shared/otel-core/src/interfaces/otel/config/IOTelWebSdkConfig.ts +++ b/shared/otel-core/src/interfaces/otel/config/IOTelWebSdkConfig.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import { IOTelContextManager } from "../context/IOTelContextManager"; +import { IOTelLogRecordLimits } from "../logs/IOTelLogRecordLimits"; import { IOTelLogRecordProcessor } from "../logs/IOTelLogRecordProcessor"; import { IOTelResource } from "../resources/IOTelResource"; import { IOTelIdGenerator } from "../trace/IOTelIdGenerator"; @@ -98,6 +99,17 @@ export interface IOTelWebSdkConfig { */ logProcessors?: IOTelLogRecordProcessor[]; + /** + * How long the forceFlush can run before it is cancelled. + * The default value is 30000ms. + */ + forceFlushTimeoutMillis?: number; + + /** + * Log record limits controlling attribute counts and value lengths. + */ + logRecordLimits?: IOTelLogRecordLimits; + // TODO: Phase 2 - Uncomment when IOTelSpanProcessor is implemented // /** // * Span processors for the trace pipeline. 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..1e783af9b --- /dev/null +++ b/shared/otel-core/src/interfaces/otel/context/IContextManagerConfig.ts @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { IOTelContext } from "./IOTelContext"; + +/** + * Configuration interface for creating a context manager. + * + * @remarks + * Error handlers are inherited from the SDK/core config (IOTelConfig/IOTelWebSdkConfig) + * to avoid duplicating them in each component config. + * + * @since 4.0.0 + */ +export interface IContextManagerConfig { + /** + * The parent / root context to use if there is no active context. + */ + parentContext?: IOTelContext; +} diff --git a/shared/otel-core/src/interfaces/otel/logs/IOTelLoggerProviderConfig.ts b/shared/otel-core/src/interfaces/otel/logs/IOTelLoggerProviderConfig.ts index 7d24c2ab3..ef1652a4d 100644 --- a/shared/otel-core/src/interfaces/otel/logs/IOTelLoggerProviderConfig.ts +++ b/shared/otel-core/src/interfaces/otel/logs/IOTelLoggerProviderConfig.ts @@ -5,9 +5,24 @@ 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` property is required + * - Error handlers are inherited from the SDK/core config (IOTelWebSdkConfig) + * - 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; /** * How long the forceFlush can run before it is cancelled. @@ -15,7 +30,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/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/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..9948079ab --- /dev/null +++ b/shared/otel-core/src/interfaces/otel/trace/ITracerProviderConfig.ts @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { ITraceHost } from "../../ai/ITraceProvider"; + +/** + * Configuration interface for creating a TracerProvider. + * + * @remarks + * The TracerProvider manages Tracer instances and delegates span creation + * to the configured trace host. + * Error handlers are inherited from the host's config (host.config.errorHandlers). + * + * @since 4.0.0 + */ +export interface ITracerProviderConfig { + /** + * The trace host that provides span creation and context management. + * + * @see {@link ITraceHost} + */ + host: ITraceHost; +} diff --git a/shared/otel-core/src/internal/handleErrors.ts b/shared/otel-core/src/internal/handleErrors.ts index c035a7698..a84994a02 100644 --- a/shared/otel-core/src/internal/handleErrors.ts +++ b/shared/otel-core/src/internal/handleErrors.ts @@ -4,16 +4,44 @@ import { dumpObj, fnApply } from "@nevware21/ts-utils"; import { IOTelErrorHandlers } from "../interfaces/otel/config/IOTelErrorHandlers"; +/** + * A source for error handlers — either the handlers directly or a config + * object that contains an `errorHandlers` property (e.g., IOTelConfig, IOTelWebSdkConfig). + * This allows handle* functions to accept the SDK/core config directly, putting + * the dereferencing logic in one place rather than in every component. + */ +export type OTelErrorHandlerSource = IOTelErrorHandlers | { errorHandlers?: IOTelErrorHandlers }; + +/** + * Resolves the error handlers from a source that may be either direct handlers + * or a config object containing an `errorHandlers` property. + * @param source - The error handler source to resolve + * @returns The resolved IOTelErrorHandlers, never null + */ +function _resolveHandlers(source: OTelErrorHandlerSource): IOTelErrorHandlers { + if (!source) { + return {}; + } + + let asConfig = source as { errorHandlers?: IOTelErrorHandlers }; + if (asConfig.errorHandlers) { + return asConfig.errorHandlers; + } + + return source as IOTelErrorHandlers; +} + /** * Handle / report an error. * When not provided the default is to generally throw an {@link OTelInvalidAttributeError} - * @param handlers - The error handlers configuration + * @param source - The error handlers or a config object with errorHandlers * @param message - The error message to report * @param key - The attribute key that caused the error * @param value - The attribute value that caused the error */ /*#__NO_SIDE_EFFECTS__*/ -export function handleAttribError(handlers: IOTelErrorHandlers, message: string, key: string, value: any) { +export function handleAttribError(source: OTelErrorHandlerSource, message: string, key: string, value: any) { + let handlers = _resolveHandlers(source); if (handlers.attribError) { handlers.attribError(message, key, value); } else { @@ -23,12 +51,13 @@ export function handleAttribError(handlers: IOTelErrorHandlers, message: string, /** * There was an error with the span. - * @param handlers - The error handlers configuration + * @param source - The error handlers or a config object with errorHandlers * @param message - The message to report * @param spanName - The name of the span */ /*#__NO_SIDE_EFFECTS__*/ -export function handleSpanError(handlers: IOTelErrorHandlers, message: string, spanName: string) { +export function handleSpanError(source: OTelErrorHandlerSource, message: string, spanName: string) { + let handlers = _resolveHandlers(source); if (handlers.spanError) { handlers.spanError(message, spanName); } else { @@ -38,11 +67,12 @@ export function handleSpanError(handlers: IOTelErrorHandlers, message: string, s /** * Report a general debug message, should not be treated as fatal - * @param handlers - The error handlers configuration + * @param source - The error handlers or a config object with errorHandlers * @param message - The debug message to report */ /*#__NO_SIDE_EFFECTS__*/ -export function handleDebug(handlers: IOTelErrorHandlers, message: string) { +export function handleDebug(source: OTelErrorHandlerSource, message: string) { + let handlers = _resolveHandlers(source); if (handlers.debug) { handlers.debug(message); } else { @@ -55,11 +85,12 @@ export function handleDebug(handlers: IOTelErrorHandlers, message: string) { /** * Report a general warning, should not be treated as fatal - * @param handlers - The error handlers configuration + * @param source - The error handlers or a config object with errorHandlers * @param message - The warning message to report */ /*#__NO_SIDE_EFFECTS__*/ -export function handleWarn(handlers: IOTelErrorHandlers, message: string) { +export function handleWarn(source: OTelErrorHandlerSource, message: string) { + let handlers = _resolveHandlers(source); if (handlers.warn) { handlers.warn(message); } else { @@ -72,11 +103,12 @@ export function handleWarn(handlers: IOTelErrorHandlers, message: string) { /** * Report a general error, should not be treated as fatal - * @param handlers - The error handlers configuration + * @param source - The error handlers or a config object with errorHandlers * @param message - The error message to report */ /*#__NO_SIDE_EFFECTS__*/ -export function handleError(handlers: IOTelErrorHandlers, message: string) { +export function handleError(source: OTelErrorHandlerSource, message: string) { + let handlers = _resolveHandlers(source); if (handlers.error) { handlers.error(message); } else if (handlers.warn) { @@ -91,11 +123,12 @@ export function handleError(handlers: IOTelErrorHandlers, message: string) { /** * A general error handler for not implemented methods. - * @param handlers - The error handlers configuration + * @param source - The error handlers or a config object with errorHandlers * @param message - The message to report */ /*#__NO_SIDE_EFFECTS__*/ -export function handleNotImplemented(handlers: IOTelErrorHandlers, message: string) { +export function handleNotImplemented(source: OTelErrorHandlerSource, message: string) { + let handlers = _resolveHandlers(source); if (handlers.notImplemented) { handlers.notImplemented(message); } else { 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..daccc42ee 100644 --- a/shared/otel-core/src/otel/api/context/contextManager.ts +++ b/shared/otel-core/src/otel/api/context/contextManager.ts @@ -2,16 +2,57 @@ // Licensed under the MIT License. import { arrSlice, fnApply, isFunction, objDefine } from "@nevware21/ts-utils"; +import { onConfigChange } from "../../../config/DynamicConfig"; +import { IUnloadHook } from "../../../interfaces/ai/IUnloadHook"; +import { IOTelConfig } from "../../../interfaces/otel/config/IOTelConfig"; 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 + * provided parent context when no context is explicitly active. + * + * @param config - Optional SDK/core config (IOTelConfig) for error handlers. If provided, + * it must already be a dynamic config so that `onConfigChange` can track changes. + * @param parentContext - Optional parent / root context to use if there is no active context. + * @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 + * - Error handlers are inherited from the SDK/core config + * + * @example + * ```typescript + * const ctxMgr = createContextManager(sdkConfig, rootContext); + * 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?: IOTelConfig, parentContext?: IOTelContext): IOTelContextManager { + let _unloadHooks: IUnloadHook[] = []; + + // Error handlers are read from the SDK/core config via onConfigChange + let _config: IOTelConfig = config || {}; + + // Register for config changes using onConfigChange (works with already-dynamic config) + if (config) { + let _configUnload = onConfigChange(config, function () { + _config = config; + }); + _unloadHooks.push(_configUnload); + } + let enabled = false; let activeContext: IOTelContext | null; @@ -41,6 +82,7 @@ export function createContextManager(parentContext?: IOTelContext): IOTelContext }); } + handleWarn(_config, "bind() called with non-function target, returning target as-is"); return target; }, enable: () => { @@ -53,10 +95,11 @@ export function createContextManager(parentContext?: IOTelContext): IOTelContext }, disable: () => { activeContext = null; - enabled = false + enabled = false; + 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..0f89e025f 100644 --- a/shared/otel-core/src/otel/api/trace/span.ts +++ b/shared/otel-core/src/otel/api/trace/span.ts @@ -24,13 +24,31 @@ 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 || {}; + + // Error handlers are read from the SDK/core config (IOTelConfig) directly. + // Spans are short-lived and do not need onConfigChange; the handleErrors + // functions accept the config object and dereference errorHandlers internally. let spanStartTime: ILazyValue = getDeferred(() => { if (isNullOrUndefined(spanCtx.startTime)) { return hrTime(perfStartTime); @@ -61,7 +79,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(otelCfg, "Span {traceID: " + spanContext.traceId + ", spanId: " + spanContext.spanId + "} has ended - operation [" + operation + "] unsuccessful" + (extraMsg ? (" - " + extraMsg) : STR_EMPTY) + ".", spanName); } return isEnded; @@ -84,7 +102,7 @@ export function createSpan(spanCtx: IOTelSpanCtx, orgName: string, kind: OTelSpa } if (message) { - handleAttribError(errorHandlers, message, key, value); + handleAttribError(otelCfg, message, key, value); localDroppedAttributes++; } else if (attributes){ attributes.v.set(key, value); @@ -112,7 +130,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(otelCfg, "maxEvents reached (" + maxEvents + ") - dropping event: " + droppedEvent.name); localDroppedEvents++; } @@ -132,10 +150,10 @@ export function createSpan(spanCtx: IOTelSpanCtx, orgName: string, kind: OTelSpa }) } else { localDroppedEvents++; - handleWarn(errorHandlers, "Span.addEvent: " + name + " not added - No events allowed"); + handleWarn(otelCfg, "Span.addEvent: " + name + " not added - No events allowed"); } - handleNotImplemented(errorHandlers, "Span.addEvent: " + name + " not added"); + handleNotImplemented(otelCfg, "Span.addEvent: " + name + " not added"); } else { localDroppedEvents++; } @@ -144,20 +162,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(otelCfg, "Span.addLink: " + link + " not added"); } else { localDroppedLinks++; - handleWarn(errorHandlers, "Span.addLink: " + link + " not added - No links allowed"); + handleWarn(otelCfg, "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(otelCfg, "Span.addLinks: " + links + " not added"); } else { localDroppedLinks += links.length; - handleWarn(errorHandlers, "Span.addLinks: " + links + " not added - No links allowed"); + handleWarn(otelCfg, "Span.addLinks: " + links + " not added - No links allowed"); } return theSpan; @@ -197,13 +215,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(otelCfg, "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(otelCfg, "Dropped " + localDroppedEvents + " events"); } // We don't mark as ended until after the onEnd callback to ensure that it can @@ -222,7 +240,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(otelCfg, "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..c99ab8543 100644 --- a/shared/otel-core/src/otel/api/trace/tracerProvider.ts +++ b/shared/otel-core/src/otel/api/trace/tracerProvider.ts @@ -2,38 +2,86 @@ // Licensed under the MIT License. import { IPromise } from "@nevware21/ts-async"; -import { ITraceHost } from "../../../interfaces/ai/ITraceProvider"; +import { onConfigChange } from "../../../config/DynamicConfig"; +import { IUnloadHook } from "../../../interfaces/ai/IUnloadHook"; 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 inherited from `host.config.errorHandlers` + * - Local config caching uses `onConfigChange` callbacks + * - Call `shutdown()` to release cached tracers and unregister config listeners + * - After shutdown, `getTracer()` returns null + * + * @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[] = []; + + // Get host and error handlers from host's config + let _host = config.host; + + // Register for config changes using onConfigChange on the host's config + if (_host && _host.config) { + let _configUnload = onConfigChange(_host.config, function () { + // Re-read from config in case host reference changed + _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(_host && _host.config, "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(_host && _host.config, "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..f6483b0b9 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,18 @@ export function createResource(resourceCtx: IOTelResourceCtx): IOTelResource { attributes: null, waitForAsyncAttributes: _waitForAsyncAttributes, merge: _merge, - getRawAttributes: _getRawAttributes + 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; + 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..3fed0668b 100644 --- a/shared/otel-core/src/otel/sdk/OTelLoggerProvider.ts +++ b/shared/otel-core/src/otel/sdk/OTelLoggerProvider.ts @@ -2,65 +2,113 @@ // Licensed under the MIT License. import { IPromise } from "@nevware21/ts-async"; -import { IOTelErrorHandlers } from "../../interfaces/otel/config/IOTelErrorHandlers"; +import { onConfigChange } from "../../config/DynamicConfig"; +import { IUnloadHook } from "../../interfaces/ai/IUnloadHook"; 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 { IOTelWebSdkConfig } from "../../interfaces/otel/config/IOTelWebSdkConfig"; 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 SDK config (IOTelWebSdkConfig) with required dependencies injected. + * Must include `resource` and `errorHandlers`. The provider reads `logProcessors` + * from the config for processors. + * @returns An initialized IOTelLoggerProvider instance with `forceFlush` and `shutdown` support. + * + * @remarks + * - All dependencies are inherited from the SDK config — no separate config interfaces + * - 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(sdkConfig); + * + * const logger = provider.getLogger("my-service", "1.0.0"); + * ``` + * + * @since 3.4.0 + */ export function createLoggerProvider( - config: IOTelLoggerProviderConfig = {} + config: IOTelWebSdkConfig ): IOTelLoggerProvider & { forceFlush(): IPromise; shutdown(): IPromise; readonly _sharedState: IOTelLoggerProviderSharedState; } { - const defaults = loadDefaultConfig(); - const forceFlushTimeoutMillis = config.forceFlushTimeoutMillis !== undefined + let _resource: IOTelResource; + + let defaults = loadDefaultConfig(); + let forceFlushTimeoutMillis: number; + let logRecordLimits; + + let _isShutdown = false; + let _unloadHooks: IUnloadHook[] = []; + + // Read initial config values from the SDK config + _resource = config.resource; + forceFlushTimeoutMillis = config.forceFlushTimeoutMillis !== undefined ? config.forceFlushTimeoutMillis : defaults.forceFlushTimeoutMillis; - const logRecordLimits = config.logRecordLimits || defaults.logRecordLimits; + logRecordLimits = config.logRecordLimits || defaults.logRecordLimits; - let resource = config.resource; - if (!resource) { - resource = createResource({ cfg: { errorHandlers: {} }, attribs: [] }); + if (!_resource) { + handleError(config, "Resource must be provided to LoggerProvider"); } - const sharedState = createLoggerProviderSharedState( - resource, + let sharedState = createLoggerProviderSharedState( + _resource, forceFlushTimeoutMillis, reconfigureLimits(logRecordLimits), - config && config.processors ? config.processors : [] + config.logProcessors || [] ); - let isShutdown = false; - const handlers: IOTelErrorHandlers = {}; + // Register for config changes using onConfigChange on the already-dynamic SDK config + let _configUnload = onConfigChange(config, function () { + _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, options?: IOTelLoggerOptions ): IOTelLogger | null { - if (isShutdown) { - handleWarn(handlers, "A shutdown LoggerProvider cannot provide a Logger"); + if (_isShutdown) { + handleWarn(config, "A shutdown LoggerProvider cannot provide a Logger"); return null; } if (!name) { - handleWarn(handlers, "Logger requested without instrumentation scope name."); + handleWarn(config, "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 +119,13 @@ export function createLoggerProvider( ); } - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return sharedState.loggers.get(key)!; + let logger = sharedState.loggers.get(key); + return logger || null; } function forceFlush(): IPromise { - if (isShutdown) { - handleWarn(handlers, "invalid attempt to force flush after LoggerProvider shutdown"); + if (_isShutdown) { + handleWarn(config, "invalid attempt to force flush after LoggerProvider shutdown"); return Promise.resolve(); } @@ -85,12 +133,19 @@ export function createLoggerProvider( } function shutdown(): IPromise { - if (isShutdown) { - handleWarn(handlers, "shutdown may only be called once per LoggerProvider"); + if (_isShutdown) { + handleWarn(config, "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..25bd56333 100644 --- a/shared/otel-core/src/otel/sdk/OTelWebSdk.ts +++ b/shared/otel-core/src/otel/sdk/OTelWebSdk.ts @@ -149,11 +149,9 @@ 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({ - resource: _resource, - processors: _sdkConfig.logProcessors || [] - }); + // Create the logger provider — pass the SDK config directly so the provider + // inherits error handlers and resource from the shared config. + let _loggerProvider = createLoggerProvider(_sdkConfig); /** * Returns the current active context from the context manager, falling back