@depup/elastic-apm-node 4.15.0-depup.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/LICENSE +26 -0
  2. package/NOTICE.md +442 -0
  3. package/README.md +48 -0
  4. package/changes.json +78 -0
  5. package/index.d.ts +398 -0
  6. package/index.js +11 -0
  7. package/lib/InflightEventSet.js +53 -0
  8. package/lib/activation-method.js +119 -0
  9. package/lib/agent.js +941 -0
  10. package/lib/apm-client/apm-client.js +313 -0
  11. package/lib/apm-client/http-apm-client/CHANGELOG.md +271 -0
  12. package/lib/apm-client/http-apm-client/README.md +485 -0
  13. package/lib/apm-client/http-apm-client/central-config.js +41 -0
  14. package/lib/apm-client/http-apm-client/container-info.js +111 -0
  15. package/lib/apm-client/http-apm-client/detect-hostname.js +96 -0
  16. package/lib/apm-client/http-apm-client/index.js +1975 -0
  17. package/lib/apm-client/http-apm-client/logging.js +31 -0
  18. package/lib/apm-client/http-apm-client/ndjson.js +20 -0
  19. package/lib/apm-client/http-apm-client/truncate.js +434 -0
  20. package/lib/apm-client/noop-apm-client.js +73 -0
  21. package/lib/async-hooks-polyfill.js +58 -0
  22. package/lib/cloud-metadata/aws.js +175 -0
  23. package/lib/cloud-metadata/azure.js +123 -0
  24. package/lib/cloud-metadata/callback-coordination.js +159 -0
  25. package/lib/cloud-metadata/gcp.js +133 -0
  26. package/lib/cloud-metadata/index.js +175 -0
  27. package/lib/config/config.js +458 -0
  28. package/lib/config/normalizers.js +701 -0
  29. package/lib/config/schema.js +1007 -0
  30. package/lib/constants.js +35 -0
  31. package/lib/errors.js +303 -0
  32. package/lib/filters/sanitize-field-names.js +69 -0
  33. package/lib/http-request.js +249 -0
  34. package/lib/instrumentation/azure-functions.js +519 -0
  35. package/lib/instrumentation/context.js +56 -0
  36. package/lib/instrumentation/dropped-spans-stats.js +112 -0
  37. package/lib/instrumentation/elasticsearch-shared.js +63 -0
  38. package/lib/instrumentation/express-utils.js +91 -0
  39. package/lib/instrumentation/generic-span.js +322 -0
  40. package/lib/instrumentation/http-shared.js +424 -0
  41. package/lib/instrumentation/ids.js +39 -0
  42. package/lib/instrumentation/index.js +1127 -0
  43. package/lib/instrumentation/modules/@apollo/server.js +30 -0
  44. package/lib/instrumentation/modules/@aws-sdk/client-dynamodb.js +143 -0
  45. package/lib/instrumentation/modules/@aws-sdk/client-s3.js +230 -0
  46. package/lib/instrumentation/modules/@aws-sdk/client-sns.js +197 -0
  47. package/lib/instrumentation/modules/@aws-sdk/client-sqs.js +336 -0
  48. package/lib/instrumentation/modules/@elastic/elasticsearch.js +343 -0
  49. package/lib/instrumentation/modules/@hapi/hapi.js +221 -0
  50. package/lib/instrumentation/modules/@opentelemetry/api.js +86 -0
  51. package/lib/instrumentation/modules/@opentelemetry/sdk-metrics.js +79 -0
  52. package/lib/instrumentation/modules/@redis/client/dist/lib/client/commands-queue.js +178 -0
  53. package/lib/instrumentation/modules/@redis/client/dist/lib/client/index.js +49 -0
  54. package/lib/instrumentation/modules/@smithy/smithy-client.js +198 -0
  55. package/lib/instrumentation/modules/_lambda-handler.js +40 -0
  56. package/lib/instrumentation/modules/apollo-server-core.js +49 -0
  57. package/lib/instrumentation/modules/aws-sdk/dynamodb.js +155 -0
  58. package/lib/instrumentation/modules/aws-sdk/s3.js +184 -0
  59. package/lib/instrumentation/modules/aws-sdk/sns.js +232 -0
  60. package/lib/instrumentation/modules/aws-sdk/sqs.js +361 -0
  61. package/lib/instrumentation/modules/aws-sdk.js +76 -0
  62. package/lib/instrumentation/modules/bluebird.js +93 -0
  63. package/lib/instrumentation/modules/cassandra-driver.js +280 -0
  64. package/lib/instrumentation/modules/elasticsearch.js +191 -0
  65. package/lib/instrumentation/modules/express-graphql.js +66 -0
  66. package/lib/instrumentation/modules/express-queue.js +28 -0
  67. package/lib/instrumentation/modules/express.js +162 -0
  68. package/lib/instrumentation/modules/fastify.js +172 -0
  69. package/lib/instrumentation/modules/finalhandler.js +41 -0
  70. package/lib/instrumentation/modules/generic-pool.js +85 -0
  71. package/lib/instrumentation/modules/graphql.js +256 -0
  72. package/lib/instrumentation/modules/handlebars.js +22 -0
  73. package/lib/instrumentation/modules/http.js +112 -0
  74. package/lib/instrumentation/modules/http2.js +320 -0
  75. package/lib/instrumentation/modules/https.js +68 -0
  76. package/lib/instrumentation/modules/ioredis.js +94 -0
  77. package/lib/instrumentation/modules/jade.js +18 -0
  78. package/lib/instrumentation/modules/kafkajs.js +476 -0
  79. package/lib/instrumentation/modules/knex.js +91 -0
  80. package/lib/instrumentation/modules/koa-router.js +74 -0
  81. package/lib/instrumentation/modules/koa.js +15 -0
  82. package/lib/instrumentation/modules/memcached.js +99 -0
  83. package/lib/instrumentation/modules/mimic-response.js +45 -0
  84. package/lib/instrumentation/modules/mongodb/lib/cmap/connection_pool.js +40 -0
  85. package/lib/instrumentation/modules/mongodb-core.js +206 -0
  86. package/lib/instrumentation/modules/mongodb.js +259 -0
  87. package/lib/instrumentation/modules/mysql.js +200 -0
  88. package/lib/instrumentation/modules/mysql2.js +140 -0
  89. package/lib/instrumentation/modules/pg.js +148 -0
  90. package/lib/instrumentation/modules/pug.js +18 -0
  91. package/lib/instrumentation/modules/redis.js +176 -0
  92. package/lib/instrumentation/modules/restify.js +52 -0
  93. package/lib/instrumentation/modules/tedious.js +159 -0
  94. package/lib/instrumentation/modules/undici.js +270 -0
  95. package/lib/instrumentation/modules/ws.js +59 -0
  96. package/lib/instrumentation/noop-transaction.js +81 -0
  97. package/lib/instrumentation/run-context/AbstractRunContextManager.js +215 -0
  98. package/lib/instrumentation/run-context/AsyncHooksRunContextManager.js +106 -0
  99. package/lib/instrumentation/run-context/AsyncLocalStorageRunContextManager.js +73 -0
  100. package/lib/instrumentation/run-context/BasicRunContextManager.js +82 -0
  101. package/lib/instrumentation/run-context/RunContext.js +151 -0
  102. package/lib/instrumentation/run-context/index.js +23 -0
  103. package/lib/instrumentation/shimmer.js +123 -0
  104. package/lib/instrumentation/span-compression.js +239 -0
  105. package/lib/instrumentation/span.js +621 -0
  106. package/lib/instrumentation/template-shared.js +43 -0
  107. package/lib/instrumentation/timer.js +84 -0
  108. package/lib/instrumentation/transaction.js +571 -0
  109. package/lib/lambda.js +992 -0
  110. package/lib/load-source-map.js +100 -0
  111. package/lib/logging.js +212 -0
  112. package/lib/metrics/index.js +92 -0
  113. package/lib/metrics/platforms/generic/index.js +40 -0
  114. package/lib/metrics/platforms/generic/process-cpu.js +22 -0
  115. package/lib/metrics/platforms/generic/process-top.js +157 -0
  116. package/lib/metrics/platforms/generic/stats.js +34 -0
  117. package/lib/metrics/platforms/generic/system-cpu.js +51 -0
  118. package/lib/metrics/platforms/linux/index.js +19 -0
  119. package/lib/metrics/platforms/linux/stats.js +213 -0
  120. package/lib/metrics/queue.js +90 -0
  121. package/lib/metrics/registry.js +52 -0
  122. package/lib/metrics/reporter.js +119 -0
  123. package/lib/metrics/runtime.js +77 -0
  124. package/lib/middleware/connect.js +16 -0
  125. package/lib/opentelemetry-bridge/OTelBridgeNonRecordingSpan.js +150 -0
  126. package/lib/opentelemetry-bridge/OTelBridgeRunContext.js +124 -0
  127. package/lib/opentelemetry-bridge/OTelContextManager.js +82 -0
  128. package/lib/opentelemetry-bridge/OTelSpan.js +344 -0
  129. package/lib/opentelemetry-bridge/OTelTracer.js +201 -0
  130. package/lib/opentelemetry-bridge/OTelTracerProvider.js +25 -0
  131. package/lib/opentelemetry-bridge/README.md +244 -0
  132. package/lib/opentelemetry-bridge/index.js +15 -0
  133. package/lib/opentelemetry-bridge/oblog.js +23 -0
  134. package/lib/opentelemetry-bridge/opentelemetry-core-mini/README.md +3 -0
  135. package/lib/opentelemetry-bridge/opentelemetry-core-mini/internal/validators.js +52 -0
  136. package/lib/opentelemetry-bridge/opentelemetry-core-mini/trace/TraceState.js +109 -0
  137. package/lib/opentelemetry-bridge/otelutils.js +99 -0
  138. package/lib/opentelemetry-bridge/setup.js +76 -0
  139. package/lib/opentelemetry-metrics/ElasticApmMetricExporter.js +285 -0
  140. package/lib/opentelemetry-metrics/index.js +50 -0
  141. package/lib/parsers.js +225 -0
  142. package/lib/propwrap.js +147 -0
  143. package/lib/stacktraces.js +537 -0
  144. package/lib/symbols.js +15 -0
  145. package/lib/tracecontext/index.js +118 -0
  146. package/lib/tracecontext/traceparent.js +185 -0
  147. package/lib/tracecontext/tracestate.js +388 -0
  148. package/lib/wildcard-matcher.js +52 -0
  149. package/loader.mjs +7 -0
  150. package/package.json +299 -0
  151. package/start.d.ts +8 -0
  152. package/start.js +29 -0
  153. package/types/aws-lambda.d.ts +98 -0
  154. package/types/connect.d.ts +23 -0
@@ -0,0 +1,336 @@
1
+ /*
2
+ * Copyright Elasticsearch B.V. and other contributors where applicable.
3
+ * Licensed under the BSD 2-Clause License; you may not use this file except in
4
+ * compliance with the BSD 2-Clause License.
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const {
10
+ MAX_MESSAGES_PROCESSED_FOR_TRACE_CONTEXT,
11
+ OUTCOME_FAILURE,
12
+ } = require('../../../constants');
13
+ const NAME = 'SQS';
14
+ const TYPE = 'messaging';
15
+ const SUBTYPE = 'sqs';
16
+ const elasticAPMStash = Symbol('elasticAPMStash');
17
+
18
+ // TODO: simthy-client already resolves span.action from the command name but does not have these values
19
+ // DeleteMessageCommand => span.action = 'DeleteMessage'
20
+ // do we need to have same messages (keep this key/val pairs) or its okay to have the ones form smithy-client?
21
+ const OPERATIONS_TO_ACTIONS = {
22
+ DeleteMessage: 'delete',
23
+ DeleteMessageBatch: 'delete_batch',
24
+ ReceiveMessage: 'poll',
25
+ SendMessageBatch: 'send_batch',
26
+ SendMessage: 'send',
27
+ unknown: 'unknown',
28
+ };
29
+ const OPERATIONS = Object.keys(OPERATIONS_TO_ACTIONS);
30
+ const MAX_SQS_MESSAGE_ATTRIBUTES = 10;
31
+ const queueMetrics = new Map();
32
+
33
+ /**
34
+ * Returns middlewares to instrument an S3Client instance
35
+ *
36
+ * @param {import('@aws-sdk/client-sqs').SQSClient} client
37
+ * @param {any} agent
38
+ * @returns {import('../@smithy/smithy-client').AWSMiddlewareEntry[]}
39
+ */
40
+ function sqsMiddlewareFactory(client, agent) {
41
+ return [
42
+ {
43
+ middleware: (next, context) => async (args) => {
44
+ const ins = agent._instrumentation;
45
+ const log = agent.logger;
46
+ const span = ins.currSpan();
47
+ const input = args.input;
48
+
49
+ // W3C trace-context propagation.
50
+ const commandName = context.commandName.replace('Command', '');
51
+ const runContext = ins.currRunContext();
52
+ const parentSpan =
53
+ span || runContext.currSpan() || runContext.currTransaction();
54
+
55
+ if (parentSpan) {
56
+ const toPropagate = [];
57
+
58
+ if (commandName === 'SendMessage' && input.MessageAttributes) {
59
+ toPropagate.push(input.MessageAttributes);
60
+ } else if (
61
+ commandName === 'SendMessageBatch' &&
62
+ Array.isArray(input.Entries)
63
+ ) {
64
+ for (const e of input.Entries) {
65
+ if (e && e.MessageAttributes) {
66
+ toPropagate.push(e.MessageAttributes);
67
+ }
68
+ }
69
+ }
70
+
71
+ // Though our spec only mentions a 10-message-attribute limit for *SQS*, we'll
72
+ // do the same limit here, because
73
+ // https://docs.aws.amazon.com/sns/latest/dg/sns-message-attributes.html
74
+ // mentions the 10-message-attribute limit for SQS subscriptions.
75
+ toPropagate.forEach((msgAttrs) => {
76
+ const attrsCount = Object.keys(msgAttrs).length + 2;
77
+ if (attrsCount > MAX_SQS_MESSAGE_ATTRIBUTES) {
78
+ log.warn(
79
+ { QueueUrl: input.QueueUrl },
80
+ 'cannot propagate trace-context with SQS message, too many MessageAttributes',
81
+ );
82
+ return;
83
+ }
84
+ parentSpan.propagateTraceContextHeaders(
85
+ msgAttrs,
86
+ function (msgAttrs, name, value) {
87
+ if (name.startsWith('elastic-')) {
88
+ return;
89
+ }
90
+ msgAttrs[name] = { DataType: 'String', StringValue: value };
91
+ },
92
+ );
93
+ });
94
+ }
95
+
96
+ // Ensure there is a span from the wrapped `client.send()`.
97
+ if (!span || !(span.type === TYPE && span.subtype === SUBTYPE)) {
98
+ return await next(args);
99
+ }
100
+
101
+ // Action is not equal to command/operation name, we have to map it
102
+ span.action = OPERATIONS_TO_ACTIONS[commandName] || 'unknown';
103
+
104
+ const queueName = getQueueNameFromCommand(args);
105
+ let toFrom = 'from';
106
+ if (span.action === 'send' || span.action === 'send_batch') {
107
+ toFrom = 'to';
108
+ }
109
+ span.name = `SQS ${span.action.toUpperCase()} ${toFrom} ${queueName}`;
110
+
111
+ let err;
112
+ let result;
113
+ let response;
114
+ let statusCode;
115
+ try {
116
+ result = await next(args);
117
+ response = result && result.response;
118
+ statusCode = response && response.statusCode;
119
+ } catch (ex) {
120
+ // Save the error for use in `finally` below, but re-throw it to
121
+ // not impact code flow.
122
+ err = ex;
123
+
124
+ // This code path happens with a GetObject conditional request
125
+ // that returns a 304 Not Modified.
126
+ statusCode = err && err.$metadata && err.$metadata.httpStatusCode;
127
+ throw ex;
128
+ } finally {
129
+ if (statusCode) {
130
+ span._setOutcomeFromHttpStatusCode(statusCode);
131
+ } else {
132
+ span._setOutcomeFromErrorCapture(OUTCOME_FAILURE);
133
+ }
134
+ if (err && (!statusCode || statusCode >= 400)) {
135
+ agent.captureError(err, { skipOutcome: true });
136
+ }
137
+
138
+ // Destination context.
139
+ const region = await client.config.region();
140
+ const service = { type: SUBTYPE };
141
+ const destCtx = { service };
142
+
143
+ if (context[elasticAPMStash]) {
144
+ destCtx.address = context[elasticAPMStash].hostname;
145
+ destCtx.port = context[elasticAPMStash].port;
146
+ }
147
+
148
+ if (region) {
149
+ destCtx.cloud = { region };
150
+ }
151
+
152
+ span._setDestinationContext(destCtx);
153
+
154
+ // Message context
155
+ span.setMessageContext({ queue: { name: queueName } });
156
+
157
+ const receiveMsgData =
158
+ span.action === 'poll' && result && result.output;
159
+ if (receiveMsgData) {
160
+ // Links
161
+ const links = getSpanLinksFromResponseData(result && result.output);
162
+ if (links) {
163
+ span.addLinks(links);
164
+ }
165
+
166
+ // Metrics
167
+ recordMetrics(queueName, receiveMsgData, agent);
168
+ }
169
+
170
+ span.end();
171
+ }
172
+
173
+ return result;
174
+ },
175
+ options: { step: 'initialize', priority: 'high', name: 'elasticAPMSpan' },
176
+ },
177
+ {
178
+ middleware: (next, context) => async (args) => {
179
+ const req = args.request;
180
+ let port = req.port;
181
+
182
+ // Resolve port for HTTP(S) protocols
183
+ if (port === undefined) {
184
+ if (req.protocol === 'https:') {
185
+ port = 443;
186
+ } else if (req.protocol === 'http:') {
187
+ port = 80;
188
+ }
189
+ }
190
+
191
+ context[elasticAPMStash] = {
192
+ hostname: req.hostname,
193
+ port,
194
+ };
195
+ return next(args);
196
+ },
197
+ options: { step: 'finalizeRequest', name: 'elasticAPMHTTPInfo' },
198
+ },
199
+ ];
200
+ }
201
+
202
+ /**
203
+ * Get the queue name from a command
204
+ * @param {import('@aws-sdk/types').Command} command the command sent by the SQS client
205
+ * @returns {string} the queue name
206
+ */
207
+ function getQueueNameFromCommand(command) {
208
+ const queueUrl = command && command.input && command.input.QueueUrl;
209
+
210
+ if (queueUrl) {
211
+ try {
212
+ const url = new URL(queueUrl);
213
+ return url.pathname.split('/').pop();
214
+ } catch {}
215
+ }
216
+
217
+ return 'unknown';
218
+ }
219
+
220
+ /**
221
+ * @typedef {import('@aws-sdk/client-sqs').Message} Message
222
+ */
223
+ /**
224
+ * Extract span links from up to 1000 messages in this batch.
225
+ * https://github.com/elastic/apm/blob/main/specs/agents/tracing-instrumentation-messaging.md#receiving-trace-context
226
+ *
227
+ * A span link is created from a `traceparent` message attribute in a message.
228
+ * `msg.messageAttributes` is of the form:
229
+ * { <attribute-name>: { DataType: <attr-type>, StringValue: <attr-value>, ... } }
230
+ * For example:
231
+ * { traceparent: { DataType: 'String', StringValue: 'test-traceparent' } }
232
+ *
233
+ * @param { {Messages?: Message[]} } data
234
+ * @returns { Array<{ context: string }> }
235
+ */
236
+ function getSpanLinksFromResponseData(data) {
237
+ if (!data || !data.Messages || data.Messages.length === 0) {
238
+ return null;
239
+ }
240
+ const links = [];
241
+ const limit = Math.min(
242
+ data.Messages.length,
243
+ MAX_MESSAGES_PROCESSED_FOR_TRACE_CONTEXT,
244
+ );
245
+ for (let i = 0; i < limit; i++) {
246
+ const attrs = data.Messages[i].MessageAttributes;
247
+ if (!attrs) {
248
+ continue;
249
+ }
250
+
251
+ let traceparent;
252
+ const attrNames = Object.keys(attrs);
253
+ for (let j = 0; j < attrNames.length; j++) {
254
+ const attrVal = attrs[attrNames[j]];
255
+ if (attrVal.DataType !== 'String') {
256
+ continue;
257
+ }
258
+ const attrNameLc = attrNames[j].toLowerCase();
259
+ if (attrNameLc === 'traceparent') {
260
+ traceparent = attrVal.StringValue;
261
+ break;
262
+ }
263
+ }
264
+ if (traceparent) {
265
+ links.push({ context: traceparent });
266
+ }
267
+ }
268
+ return links;
269
+ }
270
+
271
+ /**
272
+ * Record queue related metrics
273
+ *
274
+ * Creates metric collector objects on first run, and
275
+ * updates their data with data from received messages
276
+ * @param {string} queueName
277
+ * @param { {Messages?: Message[]} } data
278
+ * @param {any} agent
279
+ */
280
+ function recordMetrics(queueName, data, agent) {
281
+ const messages = data && data.Messages;
282
+ if (!messages || messages.length < 1) {
283
+ return;
284
+ }
285
+ if (!queueMetrics.get(queueName)) {
286
+ const collector = agent._metrics.createQueueMetricsCollector(queueName);
287
+ if (!collector) {
288
+ return;
289
+ }
290
+ queueMetrics.set(queueName, collector);
291
+ }
292
+ const metrics = queueMetrics.get(queueName);
293
+
294
+ for (const message of messages) {
295
+ const sentTimestamp =
296
+ message.Attributes && message.Attributes.SentTimestamp;
297
+ const delay = new Date().getTime() - sentTimestamp;
298
+ metrics.updateStats(delay);
299
+ }
300
+ }
301
+
302
+ /**
303
+ * Tells if the command needs to be ignored or not
304
+ * @param {import('@aws-sdk/types').Command} command the command sent by the SNS client
305
+ * @param {any} config the agent configuration
306
+ * @returns {boolean} false if the command should create a span
307
+ */
308
+ function sqsShouldIgnoreCommand(command, config) {
309
+ const commandName = command.constructor.name;
310
+ const operation = commandName.replace(/Command$/, '');
311
+
312
+ if (OPERATIONS.indexOf(operation) === -1) {
313
+ return true;
314
+ }
315
+
316
+ if (config.ignoreMessageQueuesRegExp) {
317
+ const queueName = getQueueNameFromCommand(command);
318
+ if (queueName) {
319
+ for (const rule of config.ignoreMessageQueuesRegExp) {
320
+ if (rule.test(queueName)) {
321
+ return true;
322
+ }
323
+ }
324
+ }
325
+ }
326
+
327
+ return false;
328
+ }
329
+
330
+ module.exports = {
331
+ SQS_NAME: NAME,
332
+ SQS_TYPE: TYPE,
333
+ SQS_SUBTYPE: SUBTYPE,
334
+ sqsMiddlewareFactory,
335
+ sqsShouldIgnoreCommand,
336
+ };
@@ -0,0 +1,343 @@
1
+ /*
2
+ * Copyright Elasticsearch B.V. and other contributors where applicable.
3
+ * Licensed under the BSD 2-Clause License; you may not use this file except in
4
+ * compliance with the BSD 2-Clause License.
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ // Instrument the @elastic/elasticsearch module.
10
+ //
11
+ // Limitations:
12
+ // - In @elastic/elasticsearch >=7.14 <8, the diagnostic events sent for ES
13
+ // spans started before the product-check is finished will have an incorrect
14
+ // `currentSpan`.
15
+ //
16
+ // An Elasticsearch (ES) request typically results in a single HTTP request to
17
+ // the server. For some of the later 7.x versions of @elastic/elasticsearch
18
+ // there is a product-check "GET /" that blocks the *first* request to the
19
+ // server. The handling of ES requests are effectively queued until that
20
+ // product-check is complete. When they *do* run, the async context is that
21
+ // of the initial ES span. This means that `apm.currentSpan` inside an ES
22
+ // client diagnostic event for these queued ES requests will be wrong.
23
+ // Currently the APM agent is not patching for this.
24
+
25
+ const semver = require('semver');
26
+ const { URL, URLSearchParams } = require('url');
27
+
28
+ const { getDBDestination } = require('../../context');
29
+ const { getElasticsearchDbStatement } = require('../../elasticsearch-shared');
30
+ const shimmer = require('../../shimmer');
31
+
32
+ /**
33
+ * Get the Elasticsearch cluster name, if possible.
34
+ *
35
+ * This is currently a partial implementation of:
36
+ * https://github.com/elastic/apm/blob/main/specs/agents/tracing-instrumentation-db.md#cluster-name
37
+ *
38
+ * @param {import("@elastic/elasticsearch").DiagnosticResult || null}
39
+ * @returns { string || null }
40
+ */
41
+ function getESClusterName(diagResult) {
42
+ if (diagResult && diagResult.headers) {
43
+ const clusterNameFromHeader =
44
+ diagResult.headers['x-found-handling-cluster'];
45
+ if (clusterNameFromHeader) {
46
+ return clusterNameFromHeader;
47
+ }
48
+ }
49
+ }
50
+
51
+ module.exports = function (elasticsearch, agent, { version, enabled }) {
52
+ if (!enabled) {
53
+ return elasticsearch;
54
+ }
55
+ if (!elasticsearch.Client) {
56
+ agent.logger.debug(
57
+ '@elastic/elasticsearch@%s is not supported (no `elasticsearch.Client`) - aborting...',
58
+ version,
59
+ );
60
+ return elasticsearch;
61
+ }
62
+
63
+ // Before v7.7.0 the Transport#request() implementation's Promises support
64
+ // would re-call `this.request(...)` inside a Promise.
65
+ const doubleCallsRequestIfNoCb = semver.lt(version, '7.7.0');
66
+ const ins = agent._instrumentation;
67
+ const isGteV8 = semver.satisfies(version, '>=8', { includePrerelease: true });
68
+ const elasticsearchCaptureBodyUrlsRegExp =
69
+ agent._conf.elasticsearchCaptureBodyUrlsRegExp;
70
+
71
+ agent.logger.debug(
72
+ 'shimming elasticsearch.Transport.prototype.{request,getConnection}',
73
+ );
74
+ shimmer.wrap(
75
+ elasticsearch.Transport && elasticsearch.Transport.prototype,
76
+ 'request',
77
+ wrapRequest,
78
+ );
79
+ shimmer.wrap(
80
+ elasticsearch.Transport && elasticsearch.Transport.prototype,
81
+ 'getConnection',
82
+ wrapGetConnection,
83
+ );
84
+ shimmer.wrap(elasticsearch, 'Client', wrapClient);
85
+
86
+ // Tracking the ES client Connection object and DiagnosticResult for each
87
+ // active span. Use WeakMap to avoid a leak from possible spans that don't
88
+ // end.
89
+ const connFromSpan = new WeakMap();
90
+ const diagResultFromSpan = new WeakMap();
91
+
92
+ return elasticsearch;
93
+
94
+ function wrapClient(OrigClient) {
95
+ class ClientTraced extends OrigClient {
96
+ constructor(...args) {
97
+ super(...args);
98
+ const diagnostic = isGteV8 ? this.diagnostic : this;
99
+ diagnostic.on('response', (_err, result) => {
100
+ if (result) {
101
+ const currSpan = ins.currSpan();
102
+ if (currSpan) {
103
+ diagResultFromSpan.set(currSpan, result);
104
+ }
105
+ }
106
+ });
107
+ }
108
+ }
109
+ return ClientTraced;
110
+ }
111
+
112
+ // Transport#request() calls Transport#getConnection() when it is ready to
113
+ // make the HTTP request. This returns the actual connection to be used for
114
+ // the request. This is limited, however:
115
+ // - `getConnection()` is not called if the request was aborted early.
116
+ // - If all connections are marked dead, then this returns null.
117
+ // - We are assuming this is called with the correct async context. See
118
+ // "Limitations" above.
119
+ function wrapGetConnection(origGetConnection) {
120
+ return function wrappedGetConnection(opts) {
121
+ const conn = origGetConnection.apply(this, arguments);
122
+ const currSpan = ins.currSpan();
123
+ if (conn && currSpan) {
124
+ connFromSpan.set(currSpan, conn);
125
+ }
126
+ return conn;
127
+ };
128
+ }
129
+
130
+ function wrapRequest(origRequest) {
131
+ return function wrappedRequest(params, options, cb) {
132
+ options = options || {};
133
+ if (typeof options === 'function') {
134
+ cb = options;
135
+ options = {};
136
+ }
137
+
138
+ if (typeof cb !== 'function' && doubleCallsRequestIfNoCb) {
139
+ return origRequest.apply(this, arguments);
140
+ }
141
+
142
+ const method = (params && params.method) || '<UnknownMethod>';
143
+ const path = (params && params.path) || '<UnknownPath>';
144
+ agent.logger.debug(
145
+ { method, path },
146
+ 'intercepted call to @elastic/elasticsearch.Transport.prototype.request',
147
+ );
148
+ const span = ins.createSpan(
149
+ `Elasticsearch: ${method} ${path}`,
150
+ 'db',
151
+ 'elasticsearch',
152
+ 'request',
153
+ { exitSpan: true },
154
+ );
155
+ if (!span) {
156
+ return origRequest.apply(this, arguments);
157
+ }
158
+
159
+ const parentRunContext = ins.currRunContext();
160
+ const spanRunContext = parentRunContext.enterSpan(span);
161
+ const finish = ins.bindFunctionToRunContext(
162
+ spanRunContext,
163
+ (err, result) => {
164
+ // Set destination context.
165
+ // Use the connection from wrappedGetConnection() above, if that worked.
166
+ // Otherwise, fallback to using the first connection on
167
+ // `Transport#connectionPool`, if any. (This is the best parsed
168
+ // representation of connection options passed to the Client ctor.)
169
+ let conn = connFromSpan.get(span);
170
+ if (conn) {
171
+ connFromSpan.delete(span);
172
+ } else if (this.connectionPool && this.connectionPool.connections) {
173
+ conn = this.connectionPool.connections[0];
174
+ }
175
+ const connUrl = conn && conn.url;
176
+ span._setDestinationContext(
177
+ getDBDestination(
178
+ connUrl && connUrl.hostname,
179
+ connUrl && connUrl.port,
180
+ ),
181
+ );
182
+
183
+ // Gather some HTTP context.
184
+ // We are *not* including the response headers b/c they are boring:
185
+ //
186
+ // X-elastic-product: Elasticsearch
187
+ // content-type: application/json
188
+ // content-length: ...
189
+ //
190
+ // Getting the ES client request "DiagnosticResult" object has some edge cases:
191
+ // - In v7 using a callback, we always get it as `result`.
192
+ // - In v7 using a Promise, if the promise is rejected, then `result` is
193
+ // not passed.
194
+ // - In v8, `result` only includes HTTP response info if `options.meta`
195
+ // is true. We use the diagnostic 'response' event instead.
196
+ // - In v7, see the limitation note above for the rare start case where
197
+ // the diagnostic 'response' event may have the wrong currentSpan.
198
+ // The result is that with Promise usage of v7, ES client requests that
199
+ // are queued behind the "product-check" and that reject, won't have a
200
+ // `diagResult`.
201
+ const httpContext = {};
202
+ let haveHttpContext = false;
203
+ let diagResult = isGteV8 ? null : result;
204
+ if (!diagResult) {
205
+ diagResult = diagResultFromSpan.get(span);
206
+ if (diagResult) {
207
+ diagResultFromSpan.delete(span);
208
+ }
209
+ }
210
+ if (diagResult) {
211
+ if (diagResult.statusCode) {
212
+ haveHttpContext = true;
213
+ httpContext.status_code = diagResult.statusCode;
214
+ }
215
+
216
+ if (diagResult.headers && 'content-length' in diagResult.headers) {
217
+ const contentLength = Number(
218
+ diagResult.headers['content-length'],
219
+ );
220
+ if (!isNaN(contentLength)) {
221
+ haveHttpContext = true;
222
+ httpContext.response = { encoded_body_size: contentLength };
223
+ }
224
+ }
225
+ }
226
+
227
+ // Reconstruct the full URL (including query params).
228
+ let origin;
229
+ if (connUrl) {
230
+ origin = connUrl.origin;
231
+ } else if (
232
+ diagResult &&
233
+ diagResult.meta &&
234
+ diagResult.meta.connection &&
235
+ diagResult.meta.connection.url
236
+ ) {
237
+ try {
238
+ origin = new URL(diagResult.meta.connection.url).origin;
239
+ } catch (_ignoredErr) {}
240
+ }
241
+ if (origin && params && params.path) {
242
+ const fullUrl = new URL(origin);
243
+ fullUrl.pathname = params.path;
244
+ fullUrl.search = new URLSearchParams(params.querystring).toString();
245
+ httpContext.url = fullUrl.toString();
246
+ haveHttpContext = true;
247
+ }
248
+
249
+ if (haveHttpContext) {
250
+ span.setHttpContext(httpContext);
251
+ }
252
+
253
+ // Set DB context.
254
+ const dbContext = {
255
+ type: 'elasticsearch',
256
+ };
257
+ if (params) {
258
+ const statement = getElasticsearchDbStatement(
259
+ params.path,
260
+ params.body || params.bulkBody,
261
+ elasticsearchCaptureBodyUrlsRegExp,
262
+ );
263
+ if (statement) {
264
+ dbContext.statement = statement;
265
+ }
266
+ }
267
+ const clusterName = getESClusterName(diagResult);
268
+ if (clusterName) {
269
+ dbContext.instance = clusterName;
270
+ }
271
+ span.setDbContext(dbContext);
272
+
273
+ if (err) {
274
+ // Error properties are specified here:
275
+ // https://github.com/elastic/elasticsearch-js/blob/master/lib/errors.d.ts
276
+ // - We capture some data from ResponseError, which is for
277
+ // Elasticsearch API errors:
278
+ // https://www.elastic.co/guide/en/elasticsearch/reference/current/common-options.html#common-options-error-options
279
+ // - Otherwise we explicitly turn off `captureAttributes` to avoid
280
+ // grabbing potentially large and sensitive properties like
281
+ // `err.data` on DeserializationError.
282
+ const errOpts = {
283
+ captureAttributes: false,
284
+ };
285
+ const errBody = err.body;
286
+ if (err.name === 'ResponseError' && errBody && errBody.error) {
287
+ // Include some data from the Elasticsearch API response body:
288
+ // https://www.elastic.co/guide/en/elasticsearch/reference/current/common-options.html#common-options-error-options
289
+ const errType = errBody.error.type;
290
+ if (errType) {
291
+ // Specialize `error.exception.type` for better error grouping.
292
+ errOpts.exceptionType = `ResponseError (${errType})`;
293
+ }
294
+ errOpts.custom = {
295
+ type: errType,
296
+ reason: errBody.error.reason,
297
+ status: errBody.status,
298
+ };
299
+ if (errBody.error.caused_by) {
300
+ errOpts.custom.caused_by = errBody.error.caused_by;
301
+ }
302
+ }
303
+ agent.captureError(err, errOpts);
304
+ }
305
+
306
+ span.end();
307
+ },
308
+ );
309
+
310
+ if (typeof cb === 'function') {
311
+ const wrappedCb = (err, result) => {
312
+ finish(err, result);
313
+ ins.withRunContext(parentRunContext, cb, this, err, result);
314
+ };
315
+ return ins.withRunContext(
316
+ spanRunContext,
317
+ origRequest,
318
+ this,
319
+ params,
320
+ options,
321
+ wrappedCb,
322
+ );
323
+ } else {
324
+ const origPromise = ins.withRunContext(
325
+ spanRunContext,
326
+ origRequest,
327
+ this,
328
+ ...arguments,
329
+ );
330
+ origPromise.then(
331
+ function onResolve(result) {
332
+ finish(null, result);
333
+ },
334
+ function onReject(err) {
335
+ finish(err, null);
336
+ },
337
+ );
338
+
339
+ return origPromise;
340
+ }
341
+ };
342
+ }
343
+ };