UNPKG

elastic-apm-node

Version:

The official Elastic APM agent for Node.js

345 lines (308 loc) 9.39 kB
/* * Copyright Elasticsearch B.V. and other contributors where applicable. * Licensed under the BSD 2-Clause License; you may not use this file except in * compliance with the BSD 2-Clause License. */ 'use strict'; const assert = require('assert'); const URL = require('url').URL; const otel = require('@opentelemetry/api'); const GenericSpan = require('../instrumentation/generic-span'); const oblog = require('./oblog'); const { otelSpanContextFromTraceContext, epochMsFromOTelTimeInput, } = require('./otelutils'); const { RESULT_SUCCESS, OUTCOME_UNKNOWN, OUTCOME_SUCCESS, RESULT_FAILURE, OUTCOME_FAILURE, } = require('../constants'); const Span = require('../instrumentation/span'); const Transaction = require('../instrumentation/transaction'); // Based on `isHomogeneousAttributeValueArray` from // packages/opentelemetry-core/src/common/attributes.ts function isHomogeneousArrayOfStrNumBool(arr) { const len = arr.length; let elemType = null; for (let i = 0; i < len; i++) { const elem = arr[i]; if (elem === undefined || elem === null) { continue; } if (!elemType) { elemType = typeof elem; if ( !( elemType === 'string' || elemType === 'number' || elemType === 'boolean' ) ) { return false; } } else if (typeof elem !== elemType) { return false; } } return true; } // Set the given attribute key `k` and `v` according to these OTel rules: // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/common/README.md#attribute function maybeSetOTelAttr(attrs, k, v) { if (Array.isArray(v)) { // Is it homogeneous? Nulls and undefineds are allowed. if (isHomogeneousArrayOfStrNumBool(v)) { attrs[k] = v.slice(); } } else { switch (typeof v) { case 'number': case 'boolean': attrs[k] = v; break; case 'string': // Truncation (at 1024 bytes) is done in elastic-apm-http-client. attrs[k] = v; break; } } } // This wraps a core Transaction or Span in the OTel API's `inteface Span`. class OTelSpan { constructor(span) { assert(span instanceof GenericSpan); this._span = span; this._spanContext = null; } toString() { return `OTelSpan<${this._span.constructor.name}<${this._span.id}, "${this._span.name}">>`; } // ---- OTel interface Span // https://github.com/open-telemetry/opentelemetry-js-api/blob/v1.0.4/src/trace/span.ts spanContext() { oblog.apicall('%s.spanContext()', this); if (!this._spanContext) { this._spanContext = otelSpanContextFromTraceContext(this._span._context); } return this._spanContext; } setAttribute(key, value) { if (this._span.ended || !key || typeof key !== 'string') { return this; } const attrs = this._span._getOTelAttributes(); maybeSetOTelAttr(attrs, key, value); return this; } setAttributes(attributes) { if (this._span.ended || !attributes || typeof attributes !== 'object') { return this; } const attrs = this._span._getOTelAttributes(); for (const k in attributes) { if (k.length === 0) continue; maybeSetOTelAttr(attrs, k, attributes[k]); } return this; } // Span events are not currently supported. addEvent(name, attributesOrStartTime, startTime) { return this; } setStatus(otelSpanStatus) { if (this._span.ended) { return this; } switch (otelSpanStatus) { case otel.SpanStatusCode.ERROR: this._span.setOutcome(OUTCOME_FAILURE); break; case otel.SpanStatusCode.OK: this._span.setOutcome(OUTCOME_SUCCESS); break; case otel.SpanStatusCode.UNSET: this._span.setOutcome(OUTCOME_UNKNOWN); break; } // Also set transaction.result, similar to the Java APM agent. if (this._span instanceof Transaction) { switch (otelSpanStatus) { case otel.SpanStatusCode.ERROR: this._span.result = RESULT_FAILURE; break; case otel.SpanStatusCode.OK: this._span.result = RESULT_SUCCESS; break; } } return this; } updateName(name) { if (this._span.ended) { return this; } this._span.name = name; return this; } addLink(link) { this._span.addLink(link); return this; } addLinks(links) { this._span.addLinks(links); return this; } end(otelEndTime) { oblog.apicall('%s.end(endTime=%s)', this, otelEndTime); const endTime = otelEndTime === undefined ? undefined : epochMsFromOTelTimeInput(otelEndTime); if (this._span instanceof Transaction) { this._transCompatMapping(); this._span.end(undefined, endTime); } else { assert(this._span instanceof Span); this._spanCompatMapping(); this._span.end(endTime); } } // https://github.com/elastic/apm/blob/main/specs/agents/tracing-api-otel.md#compatibility-mapping _transCompatMapping() { const attrs = this._span._otelAttributes; let type = 'unknown'; if (!attrs) { this._span.type = type; return; } const otelKind = otel.SpanKind[this._span._otelKind]; // map from string to int form const isRpc = attrs['rpc.system'] !== undefined; const isHttp = attrs['http.url'] !== undefined || attrs['http.scheme'] !== undefined; const isMessaging = attrs['messaging.system'] !== undefined; if (otelKind === otel.SpanKind.SERVER && (isRpc || isHttp)) { type = 'request'; } else if (otelKind === otel.SpanKind.CONSUMER && isMessaging) { type = 'messaging'; } else { type = 'unknown'; } this._span.type = type; } // https://github.com/elastic/apm/blob/main/specs/agents/tracing-api-otel.md#compatibility-mapping _spanCompatMapping() { const attrs = this._span._otelAttributes; const otelKind = otel.SpanKind[this._span._otelKind]; // map from string to int form if (!attrs) { if (otelKind === otel.SpanKind.INTERNAL) { this._span.type = 'app'; this._span.subtype = 'internal'; } else { this._span.type = 'unknown'; } return; } let type; let subtype; let serviceTargetType = null; let serviceTargetName = null; const httpPortFromScheme = function (scheme) { if (scheme === 'http') { return 80; } else if (scheme === 'https') { return 443; } return -1; }; // Extracts 'host:port' from URL. const parseNetName = function (url) { let u; try { u = new URL(url); // https://developer.mozilla.org/en-US/docs/Web/API/URL } catch (_err) { return undefined; } if (u.port !== '') { return u.host; // host:port already in URL } else { var port = httpPortFromScheme( u.protocol.substring(0, u.protocol.length - 1), ); return port > 0 ? u.hostname + ':' + port : u.hostname; } }; let netPort = attrs['net.peer.port'] || -1; const netPeer = attrs['net.peer.name'] || attrs['net.peer.ip']; let netName = netPeer; // netName includes port, if provided if (netName && netPort > 0) { netName += ':' + netPort; } if (attrs['db.system']) { type = 'db'; subtype = attrs['db.system']; serviceTargetType = subtype; serviceTargetName = attrs['db.name'] || null; } else if (attrs['messaging.system']) { type = 'messaging'; subtype = attrs['messaging.system']; if (!netName && attrs['messaging.url']) { netName = parseNetName(attrs['messaging.url']); } serviceTargetType = subtype; serviceTargetName = attrs['messaging.destination'] || null; } else if (attrs['rpc.system']) { type = 'external'; subtype = attrs['rpc.system']; serviceTargetType = subtype; serviceTargetName = netName || attrs['rpc.service'] || null; } else if (attrs['http.url'] || attrs['http.scheme']) { type = 'external'; subtype = 'http'; serviceTargetType = 'http'; const httpHost = attrs['http.host'] || netPeer; if (httpHost) { if (netPort < 0) { netPort = httpPortFromScheme(attrs['http.scheme']); } serviceTargetName = netPort < 0 ? httpHost : httpHost + ':' + netPort; } else if (attrs['http.url']) { serviceTargetName = parseNetName(attrs['http.url']); } } if (type === undefined) { if (otelKind === otel.SpanKind.INTERNAL) { type = 'app'; subtype = 'internal'; } else { type = 'unknown'; } } this._span.type = type; if (subtype) { this._span.subtype = subtype; } if (serviceTargetType || serviceTargetName) { this._span.setServiceTarget(serviceTargetType, serviceTargetName); } } isRecording() { return !this._span.ended && this._span.sampled; } recordException(otelException, otelTime) { const errOpts = { parent: this._span, captureAttributes: false, skipOutcome: true, }; if (otelTime !== undefined) { errOpts.timestamp = epochMsFromOTelTimeInput(otelTime); } this._span._agent.captureError(otelException, errOpts); } } module.exports = { OTelSpan, };