UNPKG

@sentry/core

Version:
291 lines (235 loc) 9.62 kB
import { logger, timestampInSeconds } from '@sentry/utils'; import { getClient, getCurrentScope } from '../currentScopes.js'; import { DEBUG_BUILD } from '../debug-build.js'; import { SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON } from '../semanticAttributes.js'; import { hasTracingEnabled } from '../utils/hasTracingEnabled.js'; import { _setSpanForScope } from '../utils/spanOnScope.js'; import { getActiveSpan, spanToJSON, getSpanDescendants, spanTimeInputToSeconds, removeChildSpanFromSpan } from '../utils/spanUtils.js'; import { SentryNonRecordingSpan } from './sentryNonRecordingSpan.js'; import { SPAN_STATUS_ERROR } from './spanstatus.js'; import { startInactiveSpan } from './trace.js'; const TRACING_DEFAULTS = { idleTimeout: 1000, finalTimeout: 30000, childSpanTimeout: 15000, }; const FINISH_REASON_HEARTBEAT_FAILED = 'heartbeatFailed'; const FINISH_REASON_IDLE_TIMEOUT = 'idleTimeout'; const FINISH_REASON_FINAL_TIMEOUT = 'finalTimeout'; const FINISH_REASON_EXTERNAL_FINISH = 'externalFinish'; /** * An idle span is a span that automatically finishes. It does this by tracking child spans as activities. * An idle span is always the active span. */ function startIdleSpan(startSpanOptions, options = {}) { // Activities store a list of active spans const activities = new Map(); // We should not use heartbeat if we finished a span let _finished = false; // Timer that tracks idleTimeout let _idleTimeoutID; // The reason why the span was finished let _finishReason = FINISH_REASON_EXTERNAL_FINISH; let _autoFinishAllowed = !options.disableAutoFinish; const { idleTimeout = TRACING_DEFAULTS.idleTimeout, finalTimeout = TRACING_DEFAULTS.finalTimeout, childSpanTimeout = TRACING_DEFAULTS.childSpanTimeout, beforeSpanEnd, } = options; const client = getClient(); if (!client || !hasTracingEnabled()) { return new SentryNonRecordingSpan(); } const scope = getCurrentScope(); const previousActiveSpan = getActiveSpan(); const span = _startIdleSpan(startSpanOptions); function _endSpan(timestamp = timestampInSeconds()) { // Ensure we end with the last span timestamp, if possible const spans = getSpanDescendants(span).filter(child => child !== span); // If we have no spans, we just end, nothing else to do here if (!spans.length) { span.end(timestamp); return; } const childEndTimestamps = spans .map(span => spanToJSON(span).timestamp) .filter(timestamp => !!timestamp) ; const latestSpanEndTimestamp = childEndTimestamps.length ? Math.max(...childEndTimestamps) : undefined; const spanEndTimestamp = spanTimeInputToSeconds(timestamp); // In reality this should always exist here, but type-wise it may be undefined... const spanStartTimestamp = spanToJSON(span).start_timestamp; // The final endTimestamp should: // * Never be before the span start timestamp // * Be the latestSpanEndTimestamp, if there is one, and it is smaller than the passed span end timestamp // * Otherwise be the passed end timestamp // Final timestamp can never be after finalTimeout const endTimestamp = Math.min( spanStartTimestamp ? spanStartTimestamp + finalTimeout / 1000 : Infinity, Math.max(spanStartTimestamp || -Infinity, Math.min(spanEndTimestamp, latestSpanEndTimestamp || Infinity)), ); span.end(endTimestamp); } /** * Cancels the existing idle timeout, if there is one. */ function _cancelIdleTimeout() { if (_idleTimeoutID) { clearTimeout(_idleTimeoutID); _idleTimeoutID = undefined; } } /** * Restarts idle timeout, if there is no running idle timeout it will start one. */ function _restartIdleTimeout(endTimestamp) { _cancelIdleTimeout(); _idleTimeoutID = setTimeout(() => { if (!_finished && activities.size === 0 && _autoFinishAllowed) { _finishReason = FINISH_REASON_IDLE_TIMEOUT; _endSpan(endTimestamp); } }, idleTimeout); } /** * Restarts child span timeout, if there is none running it will start one. */ function _restartChildSpanTimeout(endTimestamp) { _idleTimeoutID = setTimeout(() => { if (!_finished && _autoFinishAllowed) { _finishReason = FINISH_REASON_HEARTBEAT_FAILED; _endSpan(endTimestamp); } }, childSpanTimeout); } /** * Start tracking a specific activity. * @param spanId The span id that represents the activity */ function _pushActivity(spanId) { _cancelIdleTimeout(); activities.set(spanId, true); const endTimestamp = timestampInSeconds(); // We need to add the timeout here to have the real endtimestamp of the idle span // Remember timestampInSeconds is in seconds, timeout is in ms _restartChildSpanTimeout(endTimestamp + childSpanTimeout / 1000); } /** * Remove an activity from usage * @param spanId The span id that represents the activity */ function _popActivity(spanId) { if (activities.has(spanId)) { activities.delete(spanId); } if (activities.size === 0) { const endTimestamp = timestampInSeconds(); // We need to add the timeout here to have the real endtimestamp of the idle span // Remember timestampInSeconds is in seconds, timeout is in ms _restartIdleTimeout(endTimestamp + idleTimeout / 1000); } } function onIdleSpanEnded() { _finished = true; activities.clear(); if (beforeSpanEnd) { beforeSpanEnd(span); } _setSpanForScope(scope, previousActiveSpan); const spanJSON = spanToJSON(span); const { timestamp: endTimestamp, start_timestamp: startTimestamp } = spanJSON; // This should never happen, but to make TS happy... if (!endTimestamp || !startTimestamp) { return; } const attributes = spanJSON.data || {}; if (!attributes[SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON]) { span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON, _finishReason); } logger.log(`[Tracing] Idle span "${spanJSON.op}" finished`); const childSpans = getSpanDescendants(span).filter(child => child !== span); let discardedSpans = 0; childSpans.forEach(childSpan => { // We cancel all pending spans with status "cancelled" to indicate the idle span was finished early if (childSpan.isRecording()) { childSpan.setStatus({ code: SPAN_STATUS_ERROR, message: 'cancelled' }); childSpan.end(endTimestamp); DEBUG_BUILD && logger.log('[Tracing] Cancelling span since span ended early', JSON.stringify(childSpan, undefined, 2)); } const childSpanJSON = spanToJSON(childSpan); const { timestamp: childEndTimestamp = 0, start_timestamp: childStartTimestamp = 0 } = childSpanJSON; const spanStartedBeforeIdleSpanEnd = childStartTimestamp <= endTimestamp; // Add a delta with idle timeout so that we prevent false positives const timeoutWithMarginOfError = (finalTimeout + idleTimeout) / 1000; const spanEndedBeforeFinalTimeout = childEndTimestamp - childStartTimestamp < timeoutWithMarginOfError; if (DEBUG_BUILD) { const stringifiedSpan = JSON.stringify(childSpan, undefined, 2); if (!spanStartedBeforeIdleSpanEnd) { logger.log('[Tracing] Discarding span since it happened after idle span was finished', stringifiedSpan); } else if (!spanEndedBeforeFinalTimeout) { logger.log('[Tracing] Discarding span since it finished after idle span final timeout', stringifiedSpan); } } if (!spanEndedBeforeFinalTimeout || !spanStartedBeforeIdleSpanEnd) { removeChildSpanFromSpan(span, childSpan); discardedSpans++; } }); if (discardedSpans > 0) { span.setAttribute('sentry.idle_span_discarded_spans', discardedSpans); } } client.on('spanStart', startedSpan => { // If we already finished the idle span, // or if this is the idle span itself being started, // or if the started span has already been closed, // we don't care about it for activity if (_finished || startedSpan === span || !!spanToJSON(startedSpan).timestamp) { return; } const allSpans = getSpanDescendants(span); // If the span that was just started is a child of the idle span, we should track it if (allSpans.includes(startedSpan)) { _pushActivity(startedSpan.spanContext().spanId); } }); client.on('spanEnd', endedSpan => { if (_finished) { return; } _popActivity(endedSpan.spanContext().spanId); if (endedSpan === span) { onIdleSpanEnded(); } }); client.on('idleSpanEnableAutoFinish', spanToAllowAutoFinish => { if (spanToAllowAutoFinish === span) { _autoFinishAllowed = true; _restartIdleTimeout(); if (activities.size) { _restartChildSpanTimeout(); } } }); // We only start the initial idle timeout if we are not delaying the auto finish if (!options.disableAutoFinish) { _restartIdleTimeout(); } setTimeout(() => { if (!_finished) { span.setStatus({ code: SPAN_STATUS_ERROR, message: 'deadline_exceeded' }); _finishReason = FINISH_REASON_FINAL_TIMEOUT; _endSpan(); } }, finalTimeout); return span; } function _startIdleSpan(options) { const span = startInactiveSpan(options); _setSpanForScope(getCurrentScope(), span); DEBUG_BUILD && logger.log('[Tracing] Started span is an idle span'); return span; } export { TRACING_DEFAULTS, startIdleSpan }; //# sourceMappingURL=idleSpan.js.map