UNPKG

@sentry/node

Version:

Sentry Node SDK using OpenTelemetry for performance instrumentation

341 lines (337 loc) 13.3 kB
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); const diagch = require('diagnostics_channel'); const url = require('url'); const instrumentation = require('@opentelemetry/instrumentation'); const api = require('@opentelemetry/api'); const core$1 = require('@opentelemetry/core'); const semanticConventions = require('@opentelemetry/semantic-conventions'); const core = require('@sentry/core'); const PACKAGE_NAME = "@sentry/instrumentation-undici"; class UndiciInstrumentation extends instrumentation.InstrumentationBase { constructor(config = {}) { super(PACKAGE_NAME, core.SDK_VERSION, config); this._recordFromReq = /* @__PURE__ */ new WeakMap(); } // No need to instrument files/modules init() { return void 0; } disable() { super.disable(); this._channelSubs.forEach((sub) => sub.unsubscribe()); this._channelSubs.length = 0; } enable() { super.enable(); this._channelSubs = this._channelSubs || []; if (this._channelSubs.length > 0) { return; } this.subscribeToChannel("undici:request:create", this.onRequestCreated.bind(this)); this.subscribeToChannel("undici:client:sendHeaders", this.onRequestHeaders.bind(this)); this.subscribeToChannel("undici:request:headers", this.onResponseHeaders.bind(this)); this.subscribeToChannel("undici:request:trailers", this.onDone.bind(this)); this.subscribeToChannel("undici:request:error", this.onError.bind(this)); } _updateMetricInstruments() { this._httpClientDurationHistogram = this.meter.createHistogram(semanticConventions.METRIC_HTTP_CLIENT_REQUEST_DURATION, { description: "Measures the duration of outbound HTTP requests.", unit: "s", valueType: api.ValueType.DOUBLE, advice: { explicitBucketBoundaries: [5e-3, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10] } }); } subscribeToChannel(diagnosticChannel, onMessage) { const [major = 0, minor = 0] = process.version.replace("v", "").split(".").map((n) => Number(n)); const useNewSubscribe = major > 18 || major === 18 && minor >= 19; let unsubscribe; if (useNewSubscribe) { diagch.subscribe?.(diagnosticChannel, onMessage); unsubscribe = () => diagch.unsubscribe?.(diagnosticChannel, onMessage); } else { const channel = diagch.channel(diagnosticChannel); channel.subscribe(onMessage); unsubscribe = () => channel.unsubscribe(onMessage); } this._channelSubs.push({ name: diagnosticChannel, unsubscribe }); } parseRequestHeaders(request) { const result = /* @__PURE__ */ new Map(); if (Array.isArray(request.headers)) { for (let i = 0; i < request.headers.length; i += 2) { const key = request.headers[i]; const value = request.headers[i + 1]; if (typeof key === "string" && value !== void 0) { result.set(key.toLowerCase(), value); } } } else if (typeof request.headers === "string") { const headers = request.headers.split("\r\n"); for (const line of headers) { if (!line) { continue; } const colonIndex = line.indexOf(":"); if (colonIndex === -1) { continue; } const key = line.substring(0, colonIndex).toLowerCase(); const value = line.substring(colonIndex + 1).trim(); const allValues = result.get(key); if (allValues && Array.isArray(allValues)) { allValues.push(value); } else if (allValues) { result.set(key, [allValues, value]); } else { result.set(key, value); } } } return result; } // This is the 1st message we receive for each request (fired after request creation). Here we will // create the span and populate some atttributes, then link the span to the request for further // span processing onRequestCreated({ request }) { const config = this.getConfig(); const enabled = config.enabled !== false; const shouldIgnoreReq = instrumentation.safeExecuteInTheMiddle( () => !enabled || request.method === "CONNECT" || config.ignoreRequestHook?.(request), (e) => e && this._diag.error("caught ignoreRequestHook error: ", e), true ); if (shouldIgnoreReq) { return; } const startTime = core$1.hrTime(); let requestUrl; try { requestUrl = new url.URL(request.path, request.origin); } catch (err) { this._diag.warn("could not determine url.full:", err); return; } const urlScheme = requestUrl.protocol.replace(":", ""); const requestMethod = this.getRequestMethod(request.method); const attributes = { [semanticConventions.ATTR_HTTP_REQUEST_METHOD]: requestMethod, [semanticConventions.ATTR_HTTP_REQUEST_METHOD_ORIGINAL]: request.method, [semanticConventions.ATTR_URL_FULL]: requestUrl.toString(), [semanticConventions.ATTR_URL_PATH]: requestUrl.pathname, [semanticConventions.ATTR_URL_QUERY]: requestUrl.search, [semanticConventions.ATTR_URL_SCHEME]: urlScheme }; const schemePorts = { https: "443", http: "80" }; const serverAddress = requestUrl.hostname; const serverPort = requestUrl.port || schemePorts[urlScheme]; attributes[semanticConventions.ATTR_SERVER_ADDRESS] = serverAddress; if (serverPort && !isNaN(Number(serverPort))) { attributes[semanticConventions.ATTR_SERVER_PORT] = Number(serverPort); } const headersMap = this.parseRequestHeaders(request); const userAgentValues = headersMap.get("user-agent"); if (userAgentValues) { const userAgent = Array.isArray(userAgentValues) ? userAgentValues[userAgentValues.length - 1] : userAgentValues; attributes[semanticConventions.ATTR_USER_AGENT_ORIGINAL] = userAgent; } const hookAttributes = instrumentation.safeExecuteInTheMiddle( () => config.startSpanHook?.(request), (e) => e && this._diag.error("caught startSpanHook error: ", e), true ); if (hookAttributes) { Object.entries(hookAttributes).forEach(([key, val]) => { attributes[key] = val; }); } const activeCtx = api.context.active(); const currentSpan = api.trace.getSpan(activeCtx); let span; if (config.requireParentforSpans && (!currentSpan || !api.trace.isSpanContextValid(currentSpan.spanContext()))) { span = api.trace.wrapSpanContext(api.INVALID_SPAN_CONTEXT); } else { span = this.tracer.startSpan( requestMethod === "_OTHER" ? "HTTP" : requestMethod, { kind: api.SpanKind.CLIENT, attributes }, activeCtx ); } instrumentation.safeExecuteInTheMiddle( () => config.requestHook?.(span, request), (e) => e && this._diag.error("caught requestHook error: ", e), true ); const requestContext = api.trace.setSpan(api.context.active(), span); const addedHeaders = {}; api.propagation.inject(requestContext, addedHeaders); const headerEntries = Object.entries(addedHeaders); for (let i = 0; i < headerEntries.length; i++) { const pair = headerEntries[i]; if (!pair) { continue; } const [k, v] = pair; if (typeof request.addHeader === "function") { request.addHeader(k, v); } else if (typeof request.headers === "string") { request.headers += `${k}: ${v}\r `; } else if (Array.isArray(request.headers)) { request.headers.push(k, v); } } this._recordFromReq.set(request, { span, attributes, startTime }); } // This is the 2nd message we receive for each request. It is fired when connection with // the remote is established and about to send the first byte. Here we do have info about the // remote address and port so we can populate some `network.*` attributes into the span onRequestHeaders({ request, socket }) { const record = this._recordFromReq.get(request); if (!record) { return; } const config = this.getConfig(); const { span } = record; const { remoteAddress, remotePort } = socket; const spanAttributes = { [semanticConventions.ATTR_NETWORK_PEER_ADDRESS]: remoteAddress, [semanticConventions.ATTR_NETWORK_PEER_PORT]: remotePort }; if (config.headersToSpanAttributes?.requestHeaders) { const headersToAttribs = new Set(config.headersToSpanAttributes.requestHeaders.map((n) => n.toLowerCase())); const headersMap = this.parseRequestHeaders(request); for (const [name, value] of headersMap.entries()) { if (headersToAttribs.has(name)) { const attrValue = Array.isArray(value) ? value : [value]; spanAttributes[`http.request.header.${name}`] = attrValue; } } } span.setAttributes(spanAttributes); } // This is the 3rd message we get for each request and it's fired when the server // headers are received, body may not be accessible yet. // From the response headers we can set the status and content length onResponseHeaders({ request, response }) { const record = this._recordFromReq.get(request); if (!record) { return; } const { span, attributes } = record; const spanAttributes = { [semanticConventions.ATTR_HTTP_RESPONSE_STATUS_CODE]: response.statusCode }; const config = this.getConfig(); instrumentation.safeExecuteInTheMiddle( () => config.responseHook?.(span, { request, response }), (e) => e && this._diag.error("caught responseHook error: ", e), true ); if (config.headersToSpanAttributes?.responseHeaders) { const headersToAttribs = /* @__PURE__ */ new Set(); config.headersToSpanAttributes?.responseHeaders.forEach((name) => headersToAttribs.add(name.toLowerCase())); for (let idx = 0; idx < response.headers.length; idx = idx + 2) { const nameBuf = response.headers[idx]; const valueBuf = response.headers[idx + 1]; if (nameBuf === void 0 || valueBuf === void 0) { continue; } const name = nameBuf.toString().toLowerCase(); const value = valueBuf; if (headersToAttribs.has(name)) { const attrName = `http.response.header.${name}`; if (!Object.prototype.hasOwnProperty.call(spanAttributes, attrName)) { spanAttributes[attrName] = [value.toString()]; } else { spanAttributes[attrName].push(value.toString()); } } } } span.setAttributes(spanAttributes); span.setStatus({ code: response.statusCode >= 400 ? api.SpanStatusCode.ERROR : api.SpanStatusCode.UNSET }); record.attributes = Object.assign(attributes, spanAttributes); } // This is the last event we receive if the request went without any errors onDone({ request }) { const record = this._recordFromReq.get(request); if (!record) { return; } const { span, attributes, startTime } = record; span.end(); this._recordFromReq.delete(request); this.recordRequestDuration(attributes, startTime); } // This is the event we get when something is wrong in the request like // - invalid options when calling `fetch` global API or any undici method for request // - connectivity errors such as unreachable host // - requests aborted through an `AbortController.signal` // NOTE: server errors are considered valid responses and it's the lib consumer // who should deal with that. onError({ request, error }) { const record = this._recordFromReq.get(request); if (!record) { return; } const { span, attributes, startTime } = record; span.recordException(error); span.setStatus({ code: api.SpanStatusCode.ERROR, message: error.message }); span.end(); this._recordFromReq.delete(request); attributes[semanticConventions.ATTR_ERROR_TYPE] = error.message; this.recordRequestDuration(attributes, startTime); } recordRequestDuration(attributes, startTime) { const metricsAttributes = {}; const keysToCopy = [ semanticConventions.ATTR_HTTP_RESPONSE_STATUS_CODE, semanticConventions.ATTR_HTTP_REQUEST_METHOD, semanticConventions.ATTR_SERVER_ADDRESS, semanticConventions.ATTR_SERVER_PORT, semanticConventions.ATTR_URL_SCHEME, semanticConventions.ATTR_ERROR_TYPE ]; keysToCopy.forEach((key) => { if (key in attributes) { metricsAttributes[key] = attributes[key]; } }); const durationSeconds = core$1.hrTimeToMilliseconds(core$1.hrTimeDuration(startTime, core$1.hrTime())) / 1e3; this._httpClientDurationHistogram.record(durationSeconds, metricsAttributes); } getRequestMethod(original) { const knownMethods = { CONNECT: true, OPTIONS: true, HEAD: true, GET: true, POST: true, PUT: true, PATCH: true, DELETE: true, TRACE: true, // QUERY from https://datatracker.ietf.org/doc/draft-ietf-httpbis-safe-method-w-body/ QUERY: true }; if (original.toUpperCase() in knownMethods) { return original.toUpperCase(); } return "_OTHER"; } } exports.UndiciInstrumentation = UndiciInstrumentation; //# sourceMappingURL=undici.js.map