/* * Copyright The OpenTelemetry Authors * SPDX-License-Identifier: Apache-2.0 */ import { diag, SpanStatusCode } from '@opentelemetry/api'; import { addHrTimes, millisToHrTime, hrTime, hrTimeDuration, isAttributeValue, isTimeInput, isTimeInputHrTime, otperformance, sanitizeAttributes, } from '@opentelemetry/core'; import { ATTR_EXCEPTION_MESSAGE, ATTR_EXCEPTION_STACKTRACE, ATTR_EXCEPTION_TYPE, } from '@opentelemetry/semantic-conventions'; import { ExceptionEventName } from './enums'; /** * This class represents a span. */ export class SpanImpl { // Below properties are included to implement ReadableSpan for export // purposes but are not intended to be written-to directly. _spanContext; kind; parentSpanContext; attributes = {}; links = []; events = []; startTime; resource; instrumentationScope; _droppedAttributesCount = 0; _droppedEventsCount = 0; _droppedLinksCount = 0; _attributesCount = 0; name; status = { code: SpanStatusCode.UNSET, }; endTime = [0, 0]; _ended = false; _duration = [-1, -1]; _spanProcessor; _spanLimits; _attributeValueLengthLimit; _recordEndMetrics; _performanceStartTime; _performanceOffset; _startTimeProvided; /** * Constructs a new SpanImpl instance. */ constructor(opts) { const now = Date.now(); this._spanContext = opts.spanContext; this._performanceStartTime = otperformance.now(); this._performanceOffset = now - (this._performanceStartTime + otperformance.timeOrigin); this._startTimeProvided = opts.startTime != null; this._spanLimits = opts.spanLimits; this._attributeValueLengthLimit = this._spanLimits.attributeValueLengthLimit ?? 0; this._spanProcessor = opts.spanProcessor; this.name = opts.name; this.parentSpanContext = opts.parentSpanContext; this.kind = opts.kind; if (opts.links) { for (const link of opts.links) { this.addLink(link); } } this.startTime = this._getTime(opts.startTime ?? now); this.resource = opts.resource; this.instrumentationScope = opts.scope; this._recordEndMetrics = opts.recordEndMetrics; if (opts.attributes != null) { this.setAttributes(opts.attributes); } this._spanProcessor.onStart(this, opts.context); } spanContext() { return this._spanContext; } setAttribute(key, value) { if (value == null || this._isSpanEnded()) return this; if (key.length === 0) { diag.warn(`Invalid attribute key: ${key}`); return this; } if (!isAttributeValue(value)) { diag.warn(`Invalid attribute value set for key: ${key}`); return this; } const { attributeCountLimit } = this._spanLimits; const isNewKey = !Object.prototype.hasOwnProperty.call(this.attributes, key); if (attributeCountLimit !== undefined && this._attributesCount >= attributeCountLimit && isNewKey) { this._droppedAttributesCount++; return this; } this.attributes[key] = this._truncateToSize(value); if (isNewKey) { this._attributesCount++; } return this; } setAttributes(attributes) { for (const key in attributes) { if (Object.prototype.hasOwnProperty.call(attributes, key)) { this.setAttribute(key, attributes[key]); } } return this; } /** * * @param name Span Name * @param [attributesOrStartTime] Span attributes or start time * if type is {@type TimeInput} and 3rd param is undefined * @param [timeStamp] Specified time stamp for the event */ addEvent(name, attributesOrStartTime, timeStamp) { if (this._isSpanEnded()) return this; const { eventCountLimit } = this._spanLimits; if (eventCountLimit === 0) { diag.warn('No events allowed.'); this._droppedEventsCount++; return this; } if (eventCountLimit !== undefined && this.events.length >= eventCountLimit) { if (this._droppedEventsCount === 0) { diag.debug('Dropping extra events.'); } this.events.shift(); this._droppedEventsCount++; } if (isTimeInput(attributesOrStartTime)) { if (!isTimeInput(timeStamp)) { timeStamp = attributesOrStartTime; } attributesOrStartTime = undefined; } const sanitized = sanitizeAttributes(attributesOrStartTime); const { attributePerEventCountLimit } = this._spanLimits; const attributes = {}; let droppedAttributesCount = 0; let eventAttributesCount = 0; for (const attr in sanitized) { if (!Object.prototype.hasOwnProperty.call(sanitized, attr)) { continue; } const attrVal = sanitized[attr]; if (attributePerEventCountLimit !== undefined && eventAttributesCount >= attributePerEventCountLimit) { droppedAttributesCount++; continue; } attributes[attr] = this._truncateToSize(attrVal); eventAttributesCount++; } this.events.push({ name, attributes, time: this._getTime(timeStamp), droppedAttributesCount, }); return this; } addLink(link) { if (this._isSpanEnded()) return this; const { linkCountLimit } = this._spanLimits; if (linkCountLimit === 0) { this._droppedLinksCount++; return this; } if (linkCountLimit !== undefined && this.links.length >= linkCountLimit) { if (this._droppedLinksCount === 0) { diag.debug('Dropping extra links.'); } this.links.shift(); this._droppedLinksCount++; } const { attributePerLinkCountLimit } = this._spanLimits; const sanitized = sanitizeAttributes(link.attributes); const attributes = {}; let droppedAttributesCount = 0; let linkAttributesCount = 0; for (const attr in sanitized) { if (!Object.prototype.hasOwnProperty.call(sanitized, attr)) { continue; } const attrVal = sanitized[attr]; if (attributePerLinkCountLimit !== undefined && linkAttributesCount >= attributePerLinkCountLimit) { droppedAttributesCount++; continue; } attributes[attr] = this._truncateToSize(attrVal); linkAttributesCount++; } const processedLink = { context: link.context }; if (linkAttributesCount > 0) { processedLink.attributes = attributes; } if (droppedAttributesCount > 0) { processedLink.droppedAttributesCount = droppedAttributesCount; } this.links.push(processedLink); return this; } addLinks(links) { for (const link of links) { this.addLink(link); } return this; } setStatus(status) { if (this._isSpanEnded()) return this; if (status.code === SpanStatusCode.UNSET) return this; if (this.status.code === SpanStatusCode.OK) return this; const newStatus = { code: status.code }; // When using try-catch, the caught "error" is of type `any`. When then assigning `any` to `status.message`, // TypeScript will not error. While this can happen during use of any API, it is more common on Span#setStatus() // as it's likely used in a catch-block. Therefore, we validate if `status.message` is actually a string, null, or // undefined to avoid an incorrect type causing issues downstream. if (status.code === SpanStatusCode.ERROR) { if (typeof status.message === 'string') { newStatus.message = status.message; } else if (status.message != null) { diag.warn(`Dropping invalid status.message of type '${typeof status.message}', expected 'string'`); } } this.status = newStatus; return this; } updateName(name) { if (this._isSpanEnded()) return this; this.name = name; return this; } end(endTime) { if (this._isSpanEnded()) { diag.error(`${this.name} ${this._spanContext.traceId}-${this._spanContext.spanId} - You can only call end() on a span once.`); return; } this.endTime = this._getTime(endTime); this._duration = hrTimeDuration(this.startTime, this.endTime); if (this._duration[0] < 0) { diag.warn('Inconsistent start and end time, startTime > endTime. Setting span duration to 0ms.', this.startTime, this.endTime); this.endTime = this.startTime.slice(); this._duration = [0, 0]; } if (this._droppedEventsCount > 0) { diag.warn(`Dropped ${this._droppedEventsCount} events because eventCountLimit reached`); } if (this._droppedLinksCount > 0) { diag.warn(`Dropped ${this._droppedLinksCount} links because linkCountLimit reached`); } if (this._spanProcessor.onEnding) { this._spanProcessor.onEnding(this); } this._recordEndMetrics?.(); this._ended = true; this._spanProcessor.onEnd(this); } _getTime(inp) { if (typeof inp === 'number' && inp <= otperformance.now()) { // must be a performance timestamp // apply correction and convert to hrtime return hrTime(inp + this._performanceOffset); } if (typeof inp === 'number') { return millisToHrTime(inp); } if (inp instanceof Date) { return millisToHrTime(inp.getTime()); } if (isTimeInputHrTime(inp)) { return inp; } if (this._startTimeProvided) { // if user provided a time for the start manually // we can't use duration to calculate event/end times return millisToHrTime(Date.now()); } const msDuration = otperformance.now() - this._performanceStartTime; return addHrTimes(this.startTime, millisToHrTime(msDuration)); } isRecording() { return this._ended === false; } recordException(exception, time) { const attributes = {}; if (typeof exception === 'string') { attributes[ATTR_EXCEPTION_MESSAGE] = exception; } else if (exception) { if (exception.code) { attributes[ATTR_EXCEPTION_TYPE] = exception.code.toString(); } else if (exception.name) { attributes[ATTR_EXCEPTION_TYPE] = exception.name; } if (exception.message) { attributes[ATTR_EXCEPTION_MESSAGE] = exception.message; } if (exception.stack) { attributes[ATTR_EXCEPTION_STACKTRACE] = exception.stack; } } // these are minimum requirements from spec if (attributes[ATTR_EXCEPTION_TYPE] || attributes[ATTR_EXCEPTION_MESSAGE]) { this.addEvent(ExceptionEventName, attributes, time); } else { diag.warn(`Failed to record an exception ${exception}`); } } get duration() { return this._duration; } get ended() { return this._ended; } get droppedAttributesCount() { return this._droppedAttributesCount; } get droppedEventsCount() { return this._droppedEventsCount; } get droppedLinksCount() { return this._droppedLinksCount; } _isSpanEnded() { if (this._ended) { const error = new Error(`Operation attempted on ended Span {traceId: ${this._spanContext.traceId}, spanId: ${this._spanContext.spanId}}`); diag.warn(`Cannot execute the operation on ended Span {traceId: ${this._spanContext.traceId}, spanId: ${this._spanContext.spanId}}`, error); } return this._ended; } // Utility function to truncate given value within size // for value type of string, will truncate to given limit // for type of non-string, will return same value _truncateToLimitUtil(value, limit) { if (value.length <= limit) { return value; } return value.substring(0, limit); } /** * If the given attribute value is of type string and has more characters than given {@code attributeValueLengthLimit} then * return string with truncated to {@code attributeValueLengthLimit} characters * * If the given attribute value is array of strings then * return new array of strings with each element truncated to {@code attributeValueLengthLimit} characters * * Otherwise return same Attribute {@code value} * * @param value Attribute value * @returns truncated attribute value if required, otherwise same value */ _truncateToSize(value) { const limit = this._attributeValueLengthLimit; // Check limit if (limit <= 0) { // Negative values are invalid, so do not truncate diag.warn(`Attribute value limit must be positive, got ${limit}`); return value; } // String if (typeof value === 'string') { return this._truncateToLimitUtil(value, limit); } // Array of strings if (Array.isArray(value)) { return value.map(val => typeof val === 'string' ? this._truncateToLimitUtil(val, limit) : val); } // Other types, no need to apply value length limit return value; } } //# sourceMappingURL=Span.js.map