Skip to content

Commit

Permalink
[Beta] Fix Auto Exception Collection (#1282)
Browse files Browse the repository at this point in the history
* Fix auto exception collection.

* Add exception handling whenever telemetry providers are used.
  • Loading branch information
JacksonWeber committed Mar 3, 2024
1 parent 4d97211 commit b99d62d
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 121 deletions.
108 changes: 42 additions & 66 deletions src/logs/exceptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,86 +6,57 @@ import { LogApi } from "../shim/logsApi";
import { LoggerProvider } from "@opentelemetry/sdk-logs";

type ExceptionHandle = "uncaughtExceptionMonitor" | "uncaughtException" | "unhandledRejection";
const UNCAUGHT_EXCEPTION_MONITOR_HANDLER_NAME: ExceptionHandle = "uncaughtExceptionMonitor";
const UNCAUGHT_EXCEPTION_HANDLER_NAME: ExceptionHandle = "uncaughtException";
const UNHANDLED_REJECTION_HANDLER_NAME: ExceptionHandle = "unhandledRejection";
const FALLBACK_ERROR_MESSAGE =
"A promise was rejected without providing an error. Application Insights generated this error stack for you.";

export class AutoCollectExceptions {
private _canUseUncaughtExceptionMonitor = false;
private _exceptionListenerHandle?: (error: Error | undefined) => void;
private _rejectionListenerHandle?: (error: Error | undefined) => void;
private _client: LogApi;

constructor(client: LogApi) {
this._client = client;
// Only use for 13.7.0+
const nodeVer = process.versions.node.split(".");
this._canUseUncaughtExceptionMonitor =
parseInt(nodeVer[0]) > 13 || (parseInt(nodeVer[0]) === 13 && parseInt(nodeVer[1]) >= 7);

// For scenarios like Promise.reject(), an error won't be passed to the handle. Create a placeholder
// error for these scenarios.
if (this._canUseUncaughtExceptionMonitor) {
// Node.js >= 13.7.0, use uncaughtExceptionMonitor. It handles both promises and exceptions
this._exceptionListenerHandle = this._handleException.bind(
this,
false,
UNCAUGHT_EXCEPTION_MONITOR_HANDLER_NAME
); // never rethrows
(<any>process).on(
UNCAUGHT_EXCEPTION_MONITOR_HANDLER_NAME,
this._exceptionListenerHandle
);
} else {
this._exceptionListenerHandle = this._handleException.bind(
this,
true,
UNCAUGHT_EXCEPTION_HANDLER_NAME
);
this._rejectionListenerHandle = this._handleException.bind(
this,
false,
UNHANDLED_REJECTION_HANDLER_NAME
); // never rethrows
(<any>process).on(
UNCAUGHT_EXCEPTION_HANDLER_NAME,
this._exceptionListenerHandle
);
(<any>process).on(
UNHANDLED_REJECTION_HANDLER_NAME,
this._rejectionListenerHandle
);
}
this._exceptionListenerHandle = this._handleException.bind(
this,
true,
UNCAUGHT_EXCEPTION_HANDLER_NAME
);
this._rejectionListenerHandle = this._handleException.bind(
this,
false,
UNHANDLED_REJECTION_HANDLER_NAME
); // never rethrows
(<any>process).on(
UNCAUGHT_EXCEPTION_HANDLER_NAME,
this._exceptionListenerHandle
);
(<any>process).on(
UNHANDLED_REJECTION_HANDLER_NAME,
this._rejectionListenerHandle
);
}

public shutdown() {
if (this._exceptionListenerHandle) {
if (this._canUseUncaughtExceptionMonitor) {
if (this._exceptionListenerHandle) {
process.removeListener(
UNCAUGHT_EXCEPTION_MONITOR_HANDLER_NAME,
UNCAUGHT_EXCEPTION_HANDLER_NAME,
this._exceptionListenerHandle
);
} else {
if (this._exceptionListenerHandle) {
process.removeListener(
UNCAUGHT_EXCEPTION_HANDLER_NAME,
this._exceptionListenerHandle
);
}
if (this._rejectionListenerHandle) {
process.removeListener(
UNHANDLED_REJECTION_HANDLER_NAME,
this._rejectionListenerHandle
);
}
}
this._exceptionListenerHandle = undefined;
this._rejectionListenerHandle = undefined;
delete this._exceptionListenerHandle;
delete this._rejectionListenerHandle;
if (this._rejectionListenerHandle) {
process.removeListener(
UNHANDLED_REJECTION_HANDLER_NAME,
this._rejectionListenerHandle
);
}
}
this._exceptionListenerHandle = undefined;
this._rejectionListenerHandle = undefined;
delete this._exceptionListenerHandle;
delete this._rejectionListenerHandle;
}

private _handleException(
Expand All @@ -95,13 +66,18 @@ export class AutoCollectExceptions {
) {
if (this._client) {
this._client.trackException({ exception: error });
(logs.getLoggerProvider() as LoggerProvider).forceFlush();
// only rethrow when we are the only listener
if (reThrow && name && process.listeners(name as any).length === 1) {
// eslint-disable-next-line no-console
console.error(error);
// eslint-disable-next-line no-process-exit
process.exit(1);
try {
(logs.getLoggerProvider() as LoggerProvider).forceFlush().then(() => {
// only rethrow when we are the only listener
if (reThrow && name && process.listeners(name as any).length === 1) {
// eslint-disable-next-line no-console
console.error(error);
// eslint-disable-next-line no-process-exit
process.exit(1);
}
});
} catch (error) {
console.error(`Could not get the loggerProvider upon handling a tracked exception: ${error}`);
}
} else {
// eslint-disable-next-line no-console
Expand Down
12 changes: 8 additions & 4 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,14 @@ export function useAzureMonitor(options?: AzureMonitorOpenTelemetryOptions) {
exceptions = new AutoCollectExceptions(logApi);
}
if (internalConfig.enableAutoCollectPerformance) {
perfCounters = new PerformanceCounterMetrics(internalConfig);
// Add SpanProcessor to calculate Request Metrics
if (typeof (trace.getTracerProvider() as BasicTracerProvider).addSpanProcessor === "function") {
(trace.getTracerProvider() as BasicTracerProvider).addSpanProcessor(new AzureMonitorSpanProcessor(perfCounters));
try {
perfCounters = new PerformanceCounterMetrics(internalConfig);
// Add SpanProcessor to calculate Request Metrics
if (typeof (trace.getTracerProvider() as BasicTracerProvider).addSpanProcessor === "function") {
(trace.getTracerProvider() as BasicTracerProvider).addSpanProcessor(new AzureMonitorSpanProcessor(perfCounters));
}
} catch (err) {
diag.error("Failed to initialize PerformanceCounterMetrics: ", err);
}
}
autoCollectLogs.enable(internalConfig.instrumentationOptions);
Expand Down
28 changes: 18 additions & 10 deletions src/shim/telemetryClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,18 @@ export class TelemetryClient {
// Parse shim config to Azure Monitor options
this._options = this.config.parseConfig();
useAzureMonitor(this._options);
// LoggerProvider would be initialized when client is instantiated
// Get Logger from global provider
this._logApi = new LogApi(logs.getLogger("ApplicationInsightsLogger"));
this._attributeSpanProcessor = new AttributeSpanProcessor({ ...this.context.tags, ...this.commonProperties });
((trace.getTracerProvider() as ProxyTracerProvider).getDelegate() as NodeTracerProvider).addSpanProcessor(this._attributeSpanProcessor);
try {
// LoggerProvider would be initialized when client is instantiated
// Get Logger from global provider
this._logApi = new LogApi(logs.getLogger("ApplicationInsightsLogger"));
this._attributeSpanProcessor = new AttributeSpanProcessor({ ...this.context.tags, ...this.commonProperties });
((trace.getTracerProvider() as ProxyTracerProvider).getDelegate() as NodeTracerProvider).addSpanProcessor(this._attributeSpanProcessor);

this._attributeLogProcessor = new AttributeLogProcessor({ ...this.context.tags, ...this.commonProperties });
(logs.getLoggerProvider() as LoggerProvider).addLogRecordProcessor(this._attributeLogProcessor);
this._attributeLogProcessor = new AttributeLogProcessor({ ...this.context.tags, ...this.commonProperties });
(logs.getLoggerProvider() as LoggerProvider).addLogRecordProcessor(this._attributeLogProcessor);
} catch (error) {
diag.error(`Failed to initialize TelemetryClient ${error}`);
}
}

/**
Expand Down Expand Up @@ -124,9 +128,13 @@ export class TelemetryClient {
this.initialize();
}
// Create custom metric
const meter = metrics.getMeterProvider().getMeter("ApplicationInsightsMetrics");
const histogram = meter.createHistogram(telemetry.name);
histogram.record(telemetry.value, { ...telemetry.properties, ...this.commonProperties, ...this.context.tags });
try {
const meter = metrics.getMeterProvider().getMeter("ApplicationInsightsMetrics");
const histogram = meter.createHistogram(telemetry.name);
histogram.record(telemetry.value, { ...telemetry.properties, ...this.commonProperties, ...this.context.tags });
} catch (error) {
diag.error(`Failed to record metric: ${error}`);
}
}

/**
Expand Down
56 changes: 15 additions & 41 deletions test/unitTests/logs/exceptions.tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,54 +14,28 @@ describe("AutoCollection/Exceptions", () => {
sandbox.restore();
});

it("should use uncaughtExceptionMonitor for node 13.7.0+", () => {
const nodeVer = process.versions.node.split(".");
const expectation =
parseInt(nodeVer[0]) > 13 || (parseInt(nodeVer[0]) === 13 && parseInt(nodeVer[1]) >= 7);
const exceptions = new AutoCollectExceptions(null);
assert.equal(exceptions["_canUseUncaughtExceptionMonitor"], expectation);
});

it("enable auto collection", () => {
const processOnSpy = sandbox.spy(global.process, "on");
const exceptions = new AutoCollectExceptions(null);
if (exceptions["_canUseUncaughtExceptionMonitor"]) {
assert.equal(
processOnSpy.callCount,
1,
"After enabling exception auto collection, there should be 1 call to processOnSpy"
);
assert.equal(processOnSpy.getCall(0).args[0], "uncaughtExceptionMonitor");
} else {
assert.equal(
processOnSpy.callCount,
2,
"After enabling exception auto collection, there should be 2 calls to processOnSpy"
);
assert.equal(processOnSpy.getCall(0).args[0], "uncaughtException");
assert.equal(processOnSpy.getCall(1).args[0], "unhandledRejection");
}
new AutoCollectExceptions(null);
assert.equal(
processOnSpy.callCount,
2,
"After enabling exception auto collection, there should be 2 calls to processOnSpy"
);
assert.equal(processOnSpy.getCall(0).args[0], "uncaughtException");
assert.equal(processOnSpy.getCall(1).args[0], "unhandledRejection");
});

it("disables auto collection", () => {
const processRemoveListenerSpy = sandbox.spy(global.process, "removeListener");
const exceptions = new AutoCollectExceptions(null);
exceptions.shutdown();
if (exceptions["_canUseUncaughtExceptionMonitor"]) {
assert.equal(
processRemoveListenerSpy.callCount,
1,
"After enabling exception auto collection, there should be 1 call to processOnSpy"
);
assert.equal(processRemoveListenerSpy.getCall(0).args[0], "uncaughtExceptionMonitor");
} else {
assert.equal(
processRemoveListenerSpy.callCount,
2,
"After enabling exception auto collection, there should be 2 calls to processOnSpy"
);
assert.equal(processRemoveListenerSpy.getCall(0).args[0], "uncaughtException");
assert.equal(processRemoveListenerSpy.getCall(1).args[0], "unhandledRejection");
}
assert.equal(
processRemoveListenerSpy.callCount,
2,
"After enabling exception auto collection, there should be 2 calls to processOnSpy"
);
assert.equal(processRemoveListenerSpy.getCall(0).args[0], "uncaughtException");
assert.equal(processRemoveListenerSpy.getCall(1).args[0], "unhandledRejection");
});
});

0 comments on commit b99d62d

Please sign in to comment.