/* * Copyright Elasticsearch B.V. and contributors * SPDX-License-Identifier: Apache-2.0 */ import opentelemetry, { SpanKind, SpanStatusCode } from '@opentelemetry/api'; import { suppressTracing } from '@opentelemetry/core'; import { MiddlewareName, MiddlewarePriority } from './types.js'; import { stripAuth } from '../connection/BaseConnection.js'; import { sanitizeJsonBody, sanitizeNdjsonBody, sanitizeStringQuery } from '../security.js'; import Serializer from '../Serializer.js'; import { transportVersion } from '../version.generated.js'; /** Endpoints whose body can be captured as `db.query.text`. */ export const SEARCH_LIKE_ENDPOINTS = new Set([ 'async_search.submit', 'esql.async_query', 'esql.query', 'fleet.msearch', 'fleet.search', 'knn_search', 'msearch', 'rollup.rollup_search', 'search', 'search_mvt', 'sql.query' ]); /** Endpoints whose body is an ES|QL/SQL string query (captured only when parameterized). */ export const STRING_QUERY_ENDPOINTS = new Set(['esql.async_query', 'esql.query', 'sql.query']); /** Endpoints whose body is NDJSON (header + query line pairs). */ export const NDJSON_ENDPOINTS = new Set(['fleet.msearch', 'msearch']); /** Max length of the `db.query.text` attribute, in characters. */ export const SEARCH_QUERY_MAX_LENGTH = 2048; function isStream(body) { return body != null && typeof body.pipe === 'function'; } export class OpenTelemetryMiddleware { constructor(transportOptions) { Object.defineProperty(this, "name", { enumerable: true, configurable: true, writable: true, value: MiddlewareName.OPEN_TELEMETRY }); Object.defineProperty(this, "priority", { enumerable: true, configurable: true, writable: true, value: MiddlewarePriority[MiddlewareName.OPEN_TELEMETRY] }); Object.defineProperty(this, "tracer", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "transportOptions", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "serializer", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "around", { enumerable: true, configurable: true, writable: true, value: async (ctx, next) => { var _a, _b, _c, _d; const otelOptions = Object.assign({}, this.transportOptions, (_a = ctx.options.openTelemetry) !== null && _a !== void 0 ? _a : {}); if (!((_b = otelOptions.enabled) !== null && _b !== void 0 ? _b : true) || ((_c = ctx.params.meta) === null || _c === void 0 ? void 0 : _c.name) == null) { return await next(); } let otelContext = opentelemetry.context.active(); if ((_d = otelOptions.suppressInternalInstrumentation) !== null && _d !== void 0 ? _d : false) { otelContext = suppressTracing(otelContext); } const attributes = this.buildAttributes(ctx, otelOptions); // startActiveSpan makes the span the active context for the duration of `next()`, // so spans created by the HTTP layer nest under this Elasticsearch span. return await this.tracer.startActiveSpan(ctx.params.meta.name, { attributes, kind: SpanKind.CLIENT }, otelContext, async (span) => { var _a; try { const result = await next(); this.setResponseAttributes(span, result); return result; } catch (error) { // ElasticsearchClientErrors carry the partial result on `.meta`. if ((error === null || error === void 0 ? void 0 : error.meta) != null) this.setResponseAttributes(span, error.meta); span.recordException(error); span.setStatus({ code: SpanStatusCode.ERROR }); span.setAttribute('error.type', (_a = error.name) !== null && _a !== void 0 ? _a : 'Error'); throw error; } finally { span.end(); } }); } }); this.tracer = opentelemetry.trace.getTracer('@elastic/transport', transportVersion); this.transportOptions = transportOptions; this.serializer = new Serializer(); } setResponseAttributes(span, result) { var _a, _b; // statusCode is 0 when the request failed before receiving a response. if (result.statusCode > 0) { span.setAttribute('db.response.status_code', result.statusCode.toString()); } if (((_a = result.headers) === null || _a === void 0 ? void 0 : _a['x-found-handling-cluster']) != null) { span.setAttribute('db.namespace', result.headers['x-found-handling-cluster']); } if (((_b = result.headers) === null || _b === void 0 ? void 0 : _b['x-found-handling-instance']) != null) { span.setAttribute('elasticsearch.node.name', result.headers['x-found-handling-instance']); } if (result.meta.connection != null) { const url = result.meta.connection.url; span.setAttributes({ 'url.full': stripAuth(url.toString()), 'server.address': url.hostname }); if (url.port === '') { span.setAttribute('server.port', url.protocol === 'https:' ? 443 : 80); } else { const port = parseInt(url.port, 10); if (!Number.isNaN(port)) span.setAttribute('server.port', port); } } } buildAttributes(ctx, otelOptions) { var _a, _b, _c, _d; const { params } = ctx; const attributes = { 'db.system': 'elasticsearch', 'http.request.method': params.method, 'db.operation.name': (_a = params.meta) === null || _a === void 0 ? void 0 : _a.name }; if (((_b = params.meta) === null || _b === void 0 ? void 0 : _b.pathParts) != null) { for (const [key, value] of Object.entries(params.meta.pathParts)) { if (value == null) continue; attributes[`db.operation.parameter.${key}`] = value.toString(); if (['index', '_index', 'indices'].includes(key)) { let indices = []; if (typeof value === 'string') { indices.push(value); } else if (Array.isArray(value)) { indices = indices.concat(value.map(v => v.toString())); } else if (typeof value === 'object') { try { indices = indices.concat(Object.keys(value).map(v => v.toString())); } catch { // ignore } } if (indices.length > 0) attributes['db.collection.name'] = indices.join(', '); } } } if (((_c = otelOptions.captureSearchQuery) !== null && _c !== void 0 ? _c : false) && ((_d = params.meta) === null || _d === void 0 ? void 0 : _d.name) != null && SEARCH_LIKE_ENDPOINTS.has(params.meta.name)) { const queryText = this.captureQueryText(params.meta.name, ctx); if (queryText != null) attributes['db.query.text'] = queryText; } return attributes; } captureQueryText(name, ctx) { const { params } = ctx; const rawBody = NDJSON_ENDPOINTS.has(name) ? params.bulkBody : params.body; if (rawBody == null || rawBody === '' || isStream(rawBody)) return null; let bodyStr; if (typeof rawBody === 'string') { bodyStr = rawBody; } else if (Array.isArray(rawBody)) { bodyStr = this.serializer.ndserialize(rawBody); } else { bodyStr = this.serializer.serialize(rawBody); } let sanitized; if (NDJSON_ENDPOINTS.has(name)) { sanitized = sanitizeNdjsonBody(bodyStr); } else if (STRING_QUERY_ENDPOINTS.has(name)) { sanitized = sanitizeStringQuery(bodyStr); } else { sanitized = sanitizeJsonBody(bodyStr); } if (sanitized == null) return null; // Sanitize first, then truncate, so no raw literal can survive near the cutoff. return sanitized.slice(0, SEARCH_QUERY_MAX_LENGTH); } } //# sourceMappingURL=OpenTelemetry.js.map