UNPKG

applicationinsights

Version:
188 lines 8.78 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.parseStack = exports._StackFrame = exports.AutoCollectExceptions = void 0; // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for details. const api_logs_1 = require("@opentelemetry/api-logs"); const util_1 = require("../shared/util"); const UNCAUGHT_EXCEPTION_HANDLER_NAME = "uncaughtException"; const UNHANDLED_REJECTION_HANDLER_NAME = "unhandledRejection"; const FALLBACK_ERROR_MESSAGE = "A promise was rejected without providing an error. Application Insights generated this error stack for you."; // Token-bucket rate limit for exception telemetry to prevent a misbehaving // workload from becoming a request-rate amplifier against the ingestion // endpoint. When the bucket is exhausted, exceptions are dropped and a single // summary record is emitted describing how many were suppressed. const EXCEPTION_RATE_LIMIT_CAPACITY = 50; // max exceptions tracked per window const EXCEPTION_RATE_LIMIT_WINDOW_MS = 60000; // 1 minute refill window class AutoCollectExceptions { constructor(client) { this._tokens = EXCEPTION_RATE_LIMIT_CAPACITY; this._lastRefillTime = Date.now(); this._suppressedCount = 0; this._client = client; this._exceptionListenerHandle = this._handleException.bind(this, true, UNCAUGHT_EXCEPTION_HANDLER_NAME); this._rejectionListenerHandle = this._handleException.bind(this, false, UNHANDLED_REJECTION_HANDLER_NAME); // never rethrows process.on(UNCAUGHT_EXCEPTION_HANDLER_NAME, this._exceptionListenerHandle); process.on(UNHANDLED_REJECTION_HANDLER_NAME, this._rejectionListenerHandle); } shutdown() { if (this._exceptionListenerHandle) { 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; } /** * Refill the token bucket if at least one full window has elapsed since * the last refill. If the bucket was previously exhausted, emit a single * summary describing how many exceptions were suppressed. */ _refillTokens() { const now = Date.now(); if (now - this._lastRefillTime >= EXCEPTION_RATE_LIMIT_WINDOW_MS) { this._lastRefillTime = now; this._tokens = EXCEPTION_RATE_LIMIT_CAPACITY; if (this._suppressedCount > 0 && this._client) { const suppressed = this._suppressedCount; this._suppressedCount = 0; try { this._client.trackException({ exception: new Error(`Application Insights suppressed ${suppressed} exception telemetry record(s) in the last ${EXCEPTION_RATE_LIMIT_WINDOW_MS / 1000}s due to client-side rate limiting.`), }); } catch (_a) { // best-effort; do not let summary emission throw from the handler } } } } _handleException(reThrow, name, error = new Error(FALLBACK_ERROR_MESSAGE)) { if (this._client) { this._refillTokens(); const isTerminal = reThrow && !!name && process.listeners(name).length === 1; // Always record the terminal/fatal exception, bypassing the rate // limit — by definition it can only occur once per process. if (isTerminal || this._tokens > 0) { if (!isTerminal) { this._tokens--; } this._client.trackException({ exception: error }); } else { this._suppressedCount++; } // Only force a flush on the terminal path where the process is // about to exit. Under normal operation, let the // BatchLogRecordProcessor batch exception records to avoid // amplifying request rate against the ingestion endpoint. if (isTerminal) { try { api_logs_1.logs.getLoggerProvider().forceFlush().then(() => { // eslint-disable-next-line no-console console.error(error); // eslint-disable-next-line no-process-exit process.exit(1); }, () => { // eslint-disable-next-line no-console console.error(error); // eslint-disable-next-line no-process-exit process.exit(1); }); } catch (flushError) { console.error(`Could not get the loggerProvider upon handling a tracked exception: ${flushError}`); // eslint-disable-next-line no-console console.error(error); // eslint-disable-next-line no-process-exit process.exit(1); } } } else { // eslint-disable-next-line no-console console.error(error); process.exit(1); } } } exports.AutoCollectExceptions = AutoCollectExceptions; // regex to match stack frames from ie/chrome/ff // methodName=$2, fileName=$4, lineNo=$5, column=$6 const stackFramesRegex = /^(\s+at)?(.*?)(\@|\s\(|\s)([^\(\n]+):(\d+):(\d+)(\)?)$/; class _StackFrame { constructor(frame, level) { this.sizeInBytes = 0; this._baseSize = 58; //'{"method":"","level":,"assembly":"","fileName":"","line":}'.length this.level = level; this.method = "<no_method>"; this.assembly = util_1.Util.getInstance().trim(frame); const matches = frame.match(stackFramesRegex); if (matches && matches.length >= 5) { this.method = util_1.Util.getInstance().trim(matches[2]) || this.method; this.fileName = util_1.Util.getInstance().trim(matches[4]) || "<no_filename>"; this.line = parseInt(matches[5]) || 0; } this.sizeInBytes += this.method.length; this.sizeInBytes += this.fileName.length; this.sizeInBytes += this.assembly.length; // todo: these might need to be removed depending on how the back-end settles on their size calculation this.sizeInBytes += this._baseSize; this.sizeInBytes += this.level.toString().length; this.sizeInBytes += this.line.toString().length; } } exports._StackFrame = _StackFrame; function parseStack(stack) { let parsedStack = undefined; if (typeof stack === "string") { const frames = stack.split("\n"); parsedStack = []; let level = 0; let totalSizeInBytes = 0; for (let i = 0; i <= frames.length; i++) { const frame = frames[i]; if (stackFramesRegex.test(frame)) { const parsedFrame = new _StackFrame(frames[i], level++); totalSizeInBytes += parsedFrame.sizeInBytes; parsedStack.push(parsedFrame); } } // DP Constraint - exception parsed stack must be < 32KB // remove frames from the middle to meet the threshold const exceptionParsedStackThreshold = 32 * 1024; if (totalSizeInBytes > exceptionParsedStackThreshold) { let left = 0; let right = parsedStack.length - 1; let size = 0; let acceptedLeft = left; let acceptedRight = right; while (left < right) { // check size const lSize = parsedStack[left].sizeInBytes; const rSize = parsedStack[right].sizeInBytes; size += lSize + rSize; if (size > exceptionParsedStackThreshold) { // remove extra frames from the middle const howMany = acceptedRight - acceptedLeft + 1; parsedStack.splice(acceptedLeft, howMany); break; } // update pointers acceptedLeft = left; acceptedRight = right; left++; right--; } } } return parsedStack; } exports.parseStack = parseStack; //# sourceMappingURL=exceptions.js.map